K_DateTime – Standar Epoch 2000 untuk Supcon DCS
Implementasi Function Block dan Function Supcon DCS untuk mengelola timestamp dengan epoch 2000, termasuk structKDateTime untuk mengembalikan seluruh komponen tanggal/jam.
Photo by Orlando Madrigal / Unsplash
K_DateTime – Standar Epoch 2000 untuk Supcon DCS
Ditulis oleh Ketut Kumajaya — 3 November 2025
Pendahuluan
Dalam sistem SCADA/DCS modern, pengelolaan timestamp sangat penting untuk pencatatan alarm, historikal, dan monitoring performa mesin. Secara umum, timestamp sering disimpan sebagai epoch (detik sejak suatu tanggal acuan).
Mayoritas sistem menggunakan epoch 1970, tetapi di PLC/DCS dengan tipe LONG 32-bit, hal ini memiliki keterbatasan:
- Nilai maksimum LONG signed: ±2.147.483.647
- Jika dihitung detik sejak 1970, akan overflow pada sekitar tahun 2038 (Y2K38 problem)
Untuk menghindari hal ini, kita dapat menggunakan epoch 2000 sebagai acuan. Dengan tipe ULONG 32-bit, kita bisa mencatat detik positif sampai sekitar tahun 2136, cukup untuk kebutuhan jangka panjang.
Masalah
- Function Block hanya bisa mengembalikan satu nilai di Function, sedangkan tanggal terdiri dari enam komponen (Year, Month, Day, Hour, Minute, Second).
- Perlu standar yang mudah digunakan untuk logging, historikal, dan alarm timestamp.
Solusi: Function Block & Function untuk Epoch 2000
Beberapa komponen siap pakai dibuat untuk Supcon DCS:
| Nama | Tipe | Fungsi |
|---|---|---|
K_DateToEpoch |
Function Block | Konversi Year/Month/Day/Hour/Minute/Second → Epoch (ULONG) |
K_EpochToDate |
Function Block | Konversi Epoch → Year/Month/Day/Hour/Minute/Second |
K_DateToEpochF |
Function | Konversi tanggal → Epoch, dikembalikan sebagai ULONG |
K_EpochToDateF |
Function | Konversi Epoch → tanggal, dikembalikan sebagai structKDateTime |
Struct untuk Function
TYPE structKDateTime :
STRUCT
Year : INT;
Month : INT;
Day : INT;
Hour : INT;
Minute : INT;
Second : INT;
END_STRUCT
END_TYPE
Struct ini memungkinkan Function mengembalikan satu variabel kompleks berisi semua komponen tanggal/jam.
K_DateToEpoch (Function Block)
(*
FUNCTION BLOCK : K_DateToEpoch
Deskripsi : Mengonversi tanggal & waktu (Y,M,D,H,M,S) menjadi Epoch 2000 (ULONG)
Input : Year, Month, Day, Hour, Minute, Second : INT
Output : EpochSec : ULONG
Catatan : Leap year sudah diperhitungkan. Validasi input termasuk early return.
*)
FUNCTION_BLOCK K_DateToEpoch
VAR_INPUT
Year, Month, Day, Hour, Minute, Second : INT;
END_VAR
VAR_OUTPUT
EpochSec : ULONG;
END_VAR
VAR
DaysSince2000 : ULONG;
i : INT;
MonthDays : ARRAY[1..12] OF INT := [31,28,31,30,31,30,31,31,30,31,30,31];
END_VAR
// --- Validasi Input ---
IF Month < 1 OR Month > 12 THEN EpochSec := 0; RETURN; END_IF;
IF Day < 1 OR Day > 31 THEN EpochSec := 0; RETURN; END_IF;
IF Hour < 0 OR Hour > 23 THEN EpochSec := 0; RETURN; END_IF;
IF Minute < 0 OR Minute > 59 THEN EpochSec := 0; RETURN; END_IF;
IF Second < 0 OR Second > 59 THEN EpochSec := 0; RETURN; END_IF;
// --- Hitung Epoch ---
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 + MonthDays[i];
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 (opsional) ---
IF Day > MonthDays[Month] + (IF (Month=2 AND Year MOD 4=0 AND (Year MOD 100<>0 OR Year MOD 400=0)) THEN 1 ELSE 0) THEN
EpochSec := 0;
RETURN;
END_IF
DaysSince2000 := DaysSince2000 + (Day - 1);
EpochSec := DaysSince2000 * 86400 + Hour * 3600 + Minute * 60 + Second;
END_FUNCTION_BLOCK
K_EpochToDate (Function Block)
(*
FUNCTION BLOCK : K_EpochToDate
Deskripsi : Mengonversi Epoch 2000 (ULONG) menjadi tanggal & waktu (Y,M,D,H,M,S)
Input : EpochSec : ULONG
Output : Year, Month, Day, Hour, Minute, Second : INT
Catatan : Epoch > 4294967295 dianggap invalid. Leap year sudah diperhitungkan.
*)
FUNCTION_BLOCK K_EpochToDate
VAR_INPUT
EpochSec : ULONG;
END_VAR
VAR_OUTPUT
Year, Month, Day, Hour, Minute, Second : INT;
END_VAR
VAR
Days, RemSec : ULONG;
i : INT;
MonthDays : ARRAY[1..12] OF INT := [31,28,31,30,31,30,31,31,30,31,30,31];
END_VAR
// --- Validasi Input ---
IF EpochSec > 4294967295 THEN
Year := 0; Month := 0; Day := 0;
Hour := 0; Minute := 0; Second := 0;
RETURN;
END_IF;
// --- Hitung tanggal/waktu ---
Days := EpochSec / 86400;
RemSec := EpochSec MOD 86400;
Hour := RemSec / 3600;
Minute := (RemSec MOD 3600) / 60;
Second := RemSec MOD 60;
Year := 2000;
WHILE TRUE DO
IF (Year MOD 4=0 AND (Year MOD 100<>0 OR Year MOD 400=0)) THEN
i := 366;
ELSE
i := 365;
END_IF;
IF Days >= i THEN
Days := Days - i;
Year := Year + 1;
ELSE
EXIT;
END_IF;
END_WHILE
Month := 1;
FOR i := 1 TO 12 DO
IF (Month = 2 AND Year MOD 4=0 AND (Year MOD 100<>0 OR Year MOD 400=0)) THEN
IF Days >= 29 THEN
Days := Days - 29;
Month := Month + 1;
ELSE
EXIT;
END_IF;
ELSE
IF Days >= MonthDays[i] THEN
Days := Days - MonthDays[i];
Month := Month + 1;
ELSE
EXIT;
END_IF;
END_IF;
END_FOR
Day := Days + 1;
END_FUNCTION_BLOCK
Klik untuk Lihat Implementasi Inline Function
K_DateToEpochF (Function)
(*
FUNCTION : K_DateToEpochF
Deskripsi : Mengonversi tanggal & waktu (Y,M,D,H,M,S) menjadi Epoch 2000 (ULONG)
Input : Year, Month, Day, Hour, Minute, Second : INT
Return : EpochSec : ULONG
Catatan : Sama seperti K_DateToEpoch, tapi dikemas sebagai inline Function.
*)
FUNCTION K_DateToEpochF : ULONG
VAR_INPUT
Year, Month, Day, Hour, Minute, Second : INT;
END_VAR
VAR
DaysSince2000 : ULONG;
i : INT;
MonthDays : ARRAY[1..12] OF INT := [31,28,31,30,31,30,31,31,30,31,30,31];
END_VAR
// --- Validasi Input ---
IF Month < 1 OR Month > 12 THEN K_DateToEpochF := 0; RETURN; END_IF;
IF Day < 1 OR Day > 31 THEN K_DateToEpochF := 0; RETURN; END_IF;
IF Hour < 0 OR Hour > 23 THEN K_DateToEpochF := 0; RETURN; END_IF;
IF Minute < 0 OR Minute > 59 THEN K_DateToEpochF := 0; RETURN; END_IF;
IF Second < 0 OR Second > 59 THEN K_DateToEpochF := 0; RETURN; END_IF;
// --- Hitung Epoch ---
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 + MonthDays[i];
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 (opsional) ---
IF Day > MonthDays[Month] + (IF (Month=2 AND Year MOD 4=0 AND (Year MOD 100<>0 OR Year MOD 400=0)) THEN 1 ELSE 0) THEN
K_DateToEpochF := 0;
RETURN;
END_IF
DaysSince2000 := DaysSince2000 + (Day - 1);
K_DateToEpochF := DaysSince2000 * 86400 + Hour * 3600 + Minute * 60 + Second;
END_FUNCTION
K_EpochToDateF (Function)
(*
FUNCTION : K_EpochToDateF
Deskripsi : Mengonversi Epoch 2000 (ULONG) menjadi tanggal & waktu (structKDateTime)
Input : EpochSec : ULONG
Return : structKDateTime {Year, Month, Day, Hour, Minute, Second}
Catatan : Sama seperti K_EpochToDate, tapi dikemas sebagai inline Function.
*)
FUNCTION K_EpochToDateF : structKDateTime
VAR_INPUT
EpochSec : ULONG;
END_VAR
VAR
Days, RemSec : ULONG;
i : INT;
MonthDays : ARRAY[1..12] OF INT := [31,28,31,30,31,30,31,31,30,31,30,31];
Result : structKDateTime;
END_VAR
// --- Validasi Input ---
IF EpochSec > 4294967295 THEN
Result.Year := 0; Result.Month := 0; Result.Day := 0;
Result.Hour := 0; Result.Minute := 0; Result.Second := 0;
K_EpochToDateF := Result;
RETURN;
END_IF;
// --- Hitung tanggal/waktu ---
Days := EpochSec / 86400;
RemSec := EpochSec MOD 86400;
Result.Hour := RemSec / 3600;
Result.Minute := (RemSec MOD 3600) / 60;
Result.Second := RemSec MOD 60;
Result.Year := 2000;
WHILE TRUE DO
IF (Result.Year MOD 4=0 AND (Result.Year MOD 100<>0 OR Result.Year MOD 400=0)) THEN
i := 366;
ELSE
i := 365;
END_IF;
IF Days >= i THEN
Days := Days - i;
Result.Year := Result.Year + 1;
ELSE
EXIT;
END_IF;
END_WHILE
Result.Month := 1;
FOR i := 1 TO 12 DO
IF (Result.Month = 2 AND Result.Year MOD 4=0 AND (Result.Year MOD 100<>0 OR Result.Year MOD 400=0)) THEN
IF Days >= 29 THEN
Days := Days - 29;
Result.Month := Result.Month + 1;
ELSE
EXIT;
END_IF;
ELSE
IF Days >= MonthDays[i] THEN
Days := Days - MonthDays[i];
Result.Month := Result.Month + 1;
ELSE
EXIT;
END_IF;
END_IF;
END_FOR
Result.Day := Days + 1;
K_EpochToDateF := Result;
END_FUNCTION
Tabel Validasi
Tabel berikut menunjukkan hasil validasi K_DateTime untuk berbagai kasus input, mulai dari tanggal normal, leap year, hingga input invalid. Kolom Actual Output menampilkan hasil simulasi, dan kolom Status menunjukkan apakah hasil sesuai dengan ekspektasi.
| Test Case | Input | Expected Output | Actual Output (Simulasi) | Status | Catatan |
|---|---|---|---|---|---|
| Contoh Hari Ini | 2025-11-03 08:30:00 | Epoch: 815473800 | 815473800 Balik: 2025-11-03 08:30:00 |
✅ Pass | Round-trip perfect. |
| Leap Year (2000) | 2000-02-29 00:00:00 | Epoch: 5097600 | 5097600 Balik: 2000-02-29 00:00:00 |
✅ Pass | Leap day ditangani benar (+1 hari). |
| Non-Leap Feb | 2025-02-28 23:59:59 | Epoch: 794102399 | 794102399 Balik: 2025-02-28 23:59:59 |
✅ Pass | Hitungan detik dari 1 Jan 2000 sesuai, round-trip valid. |
| Invalid Month | 2025-13-03 08:30:00 | Epoch: 0 (validasi) | 0 Balik: 2000-01-01 00:00:00 |
✅ Pass | Validasi early return bekerja. |
| Invalid Day (31 Apr) | 2025-04-31 08:30:00 | Epoch: 0 (validasi) | 0 Balik: 2000-01-01 00:00:00 |
✅ Pass | Presisi validasi cegah April 31 (max 30). |
| Invalid Day (Feb 30) | 2024-02-30 12:00:00 | Epoch: 0 (validasi, leap year) | 0 Balik: 2000-01-01 00:00:00 |
✅ Pass | 2024 leap (max 29), validasi + leap adjust tangkap ini. |
| Invalid Time (Hour) | 2025-11-03 25:00:00 | Epoch: 0 | 0 Balik: 2000-01-01 00:00:00 |
✅ Pass | Jam >23 ditolak. |
| Epoch 0 | Epoch: 0 | Date: 2000-01-01 00:00:00 | {2000,1,1,0,0,0} | ✅ Pass | Baseline acuan benar. |
| Max ULONG | Epoch: 4294967295 | ~2136-02-07 06:28:15 | {2136,2,7,6,28,15} | ✅ Pass | Batas 32-bit tepat (tidak overflow loop). |
| Overflow Check | Epoch: 4294967296 | {0,0,0,0,0,0} | {0,0,0,0,0,0} | ✅ Pass | Guard >2^32-1 mencegah loop infinite. |
Klik untuk Lihat Script Validasi
import pandas as pd
from datetime import datetime, timezone
# Simulasi MonthDays array dari ST
MONTH_DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] # Index 0 unused
def is_leap_year(year):
"""Simulasi aturan leap year Gregorian dari ST."""
return (year % 4 == 0 and (year % 100 != 0 or year % 400 == 0))
def date_to_epoch2000(year, month, day, hour, minute, second):
"""
Simulasi K_DateToEpochF / K_DateToEpoch.
Return: epoch_sec (int) atau 0 jika invalid.
"""
# Validasi input (sama seperti ST)
if not (1 <= month <= 12):
return 0
if not (1 <= day <= 31):
return 0
if not (0 <= hour <= 23):
return 0
if not (0 <= minute <= 59):
return 0
if not (0 <= second <= 59):
return 0
# Validasi day presisi (opsional, seperti update ST)
max_days = MONTH_DAYS[month]
if month == 2 and is_leap_year(year):
max_days += 1
if day > max_days:
return 0
# Hitung days since 2000
days_since_2000 = 0
# Loop tahun 2000 to year-1
for i in range(2000, year):
days_since_2000 += 366 if is_leap_year(i) else 365
# Loop bulan 1 to month-1
for i in range(1, month):
days_since_2000 += MONTH_DAYS[i]
# Leap day adjustment jika month >2 dan leap
if month > 2 and is_leap_year(year):
days_since_2000 += 1
# + (day - 1)
days_since_2000 += (day - 1)
# Epoch sec
epoch_sec = days_since_2000 * 86400 + hour * 3600 + minute * 60 + second
return epoch_sec
def epoch_to_date2000(epoch_sec):
"""
Simulasi K_EpochToDateF / K_EpochToDate.
Return: dict {'year': int, 'month': int, 'day': int, 'hour': int, 'minute': int, 'second': int}
atau {'year': 0, ...} jika invalid.
"""
# Validasi input (ULONG max ~4.29e9)
if epoch_sec > 4294967295:
return {'year': 0, 'month': 0, 'day': 0, 'hour': 0, 'minute': 0, 'second': 0}
# Hitung days dan rem sec
days = epoch_sec // 86400
rem_sec = epoch_sec % 86400
hour = rem_sec // 3600
minute = (rem_sec % 3600) // 60
second = rem_sec % 60
# Loop tahun dari 2000
year = 2000
while True:
days_in_year = 366 if is_leap_year(year) else 365
if days >= days_in_year:
days -= days_in_year
year += 1
else:
break
# Loop bulan
month = 1
for i in range(1, 13):
days_in_month = MONTH_DAYS[i]
if i == 2 and is_leap_year(year):
days_in_month = 29
if days >= days_in_month:
days -= days_in_month
month += 1
else:
break
day = days + 1
return {'year': year, 'month': month, 'day': day, 'hour': hour, 'minute': minute, 'second': second}
# Test Cases
test_cases = [
("Contoh Hari Ini", "date", (2025, 11, 3, 8, 30, 0), "Epoch: 815473800", "✅ Pass", "Round-trip perfect.", "2025-11-03 08:30:00"),
("Leap Year (2000)", "date", (2000, 2, 29, 0, 0, 0), "Epoch: 5097600", "✅ Pass", "Leap day ditangani benar (+1 hari).", "2000-02-29 00:00:00"),
("Non-Leap Feb", "date", (2025, 2, 28, 23, 59, 59), "Epoch: 794102399", "✅ Pass", "Hitungan detik dari 1 Jan 2000 sesuai, round-trip valid.", "2025-02-28 23:59:59"),
("Invalid Month", "date", (2025, 13, 3, 8, 30, 0), "Epoch: 0 (validasi)", "✅ Pass", "Validasi early return bekerja.", "2000-01-01 00:00:00"),
("Invalid Day (31 Apr)", "date", (2025, 4, 31, 8, 30, 0), "Epoch: 0 (validasi)", "✅ Pass", "Presisi validasi cegah April 31 (max 30).", "2000-01-01 00:00:00"),
("Invalid Day (Feb 30)", "date", (2024, 2, 30, 12, 0, 0), "Epoch: 0 (validasi, leap year)", "✅ Pass", "2024 leap (max 29), validasi + leap adjust tangkap ini.", "2000-01-01 00:00:00"),
("Invalid Time (Hour)", "date", (2025, 11, 3, 25, 0, 0), "Epoch: 0", "✅ Pass", "Jam >23 ditolak.", "2000-01-01 00:00:00"),
("Epoch 0", "epoch", 0, "Date: 2000-01-01 00:00:00", "✅ Pass", "Baseline acuan benar.", "{2000,1,1,0,0,0}"),
("Max ULONG", "epoch", 4294967295, "~2136-02-07 06:28:15", "✅ Pass", "Batas 32-bit tepat (tidak overflow loop).", "{2136,2,7,6,28,15}"),
("Overflow Check", "epoch", 4294967296, "{0,0,0,0,0,0}", "✅ Pass", "Guard >2^32-1 mencegah loop infinite.", "{0,0,0,0,0,0}"),
]
# Jalankan simulasi dan kumpulkan hasil
data = []
for name, input_type, input_val, expected_str, status, catatan, balik_str in test_cases:
if input_type == "date":
actual_epoch = date_to_epoch2000(*input_val)
round_trip = epoch_to_date2000(actual_epoch)
if actual_epoch == 0:
balik_formatted = "2000-01-01 00:00:00"
else:
balik_formatted = f"{round_trip['year']}-{round_trip['month']:02d}-{round_trip['day']:02d} {round_trip['hour']:02d}:{round_trip['minute']:02d}:{round_trip['second']:02d}"
actual_output = f"{actual_epoch}<br>Balik: {balik_formatted}"
rt_match = (balik_formatted == balik_str)
expected_val = int(expected_str.split(': ')[-1].split(' ')[0])
is_pass = (actual_epoch == expected_val and rt_match)
final_status = "✅ Pass" if is_pass else "❌ Fail"
catatan_final = catatan if is_pass else f"FAIL: Epoch mismatch (Expected: {expected_val}, Actual: {actual_epoch})"
data.append({
'Test Case': name,
'Input': f"{input_val[0]}-{input_val[1]:02d}-{input_val[2]:02d} {input_val[3]:02d}:{input_val[4]:02d}:{input_val[5]:02d}",
'Expected Output': expected_str,
'Actual Output (Simulasi)': actual_output,
'Status': final_status,
'Catatan': catatan_final
})
else: # epoch
actual_date = epoch_to_date2000(input_val)
actual_str = f"{{{actual_date['year']},{actual_date['month']},{actual_date['day']},{actual_date['hour']},{actual_date['minute']},{actual_date['second']}}}"
is_pass = (actual_str == balik_str)
final_status = "✅ Pass" if is_pass else "❌ Fail"
catatan_final = catatan if is_pass else f"FAIL: Date mismatch (Expected: {balik_str}, Actual: {actual_str})"
data.append({
'Test Case': name,
'Input': f"Epoch: {input_val}",
'Expected Output': expected_str,
'Actual Output (Simulasi)': actual_str,
'Status': final_status,
'Catatan': catatan_final
})
# Optional: Cross-check
print("\n=== CROSS-CHECK DENGAN DATETIME LIBRARY ===")
for name, input_type, input_val, _, _, _, _ in test_cases:
if input_type == "date" and name not in ["Invalid Month", "Invalid Day (31 Apr)", "Invalid Day (Feb 30)", "Invalid Time (Hour)"]:
dt = datetime(input_val[0], input_val[1], input_val[2], input_val[3], input_val[4], input_val[5], tzinfo=timezone.utc)
epoch_std = int(dt.timestamp()) - int(datetime(2000, 1, 1, tzinfo=timezone.utc).timestamp())
print(f"{name}: Simulasi={date_to_epoch2000(*input_val)}, Std={epoch_std} → Match: {date_to_epoch2000(*input_val) == epoch_std}")
# Simpan hasil ke markdown
print("\n=== SIMPAN HASIL KE MARKDOWN ===")
df = pd.DataFrame(data)
df.to_markdown('tabel_validasi.md', index=False)
print("Data validasi disimpan ke 'tabel_validasi.md'.")
Panduan Implementasi
- Masukkan semua kode ke proyek Supcon DCS.
- Gunakan ULONG untuk menyimpan Epoch 2000.
- Untuk Function, gunakan structKDateTime agar semua komponen tanggal tersedia.
- FB bisa digunakan untuk historikal, Function untuk kalkulasi inline.
- Praktik terbaik: selalu gunakan epoch 2000 untuk sistem jangka panjang.
Contoh Penggunaan
VAR
MyEpoch : ULONG;
MyDate : structKDateTime;
END_VAR
MyEpoch := K_DateToEpochF(2025,11,3,8,30,0);
MyDate := K_EpochToDateF(MyEpoch);
Hasil:
MyEpoch→ 815473800MyDate→ {Year=2025, Month=11, Day=3, Hour=8, Minute=30, Second=0}
Kesimpulan
- Epoch 2000 aman untuk PLC/DCS 32-bit dan menghindari overflow.
- Kombinasi Function Block dan Function memungkinkan fleksibilitas: historikal maupun kalkulasi inline.
- Dengan structKDateTime, Function dapat mengembalikan seluruh komponen tanggal sekaligus.
- Standar ini memudahkan integrasi timestamp di SCADA/DCS modern.