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

  1. Function Block hanya bisa mengembalikan satu nilai di Function, sedangkan tanggal terdiri dari enam komponen (Year, Month, Day, Hour, Minute, Second).
  2. 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

  1. Masukkan semua kode ke proyek Supcon DCS.
  2. Gunakan ULONG untuk menyimpan Epoch 2000.
  3. Untuk Function, gunakan structKDateTime agar semua komponen tanggal tersedia.
  4. FB bisa digunakan untuk historikal, Function untuk kalkulasi inline.
  5. 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 → 815473800
  • MyDate → {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.