--- a/iec60870/iec60870.py Thu May 21 18:29:21 2026 +0200
+++ b/iec60870/iec60870.py Tue May 26 13:50:51 2026 +0200
@@ -17,7 +17,8 @@
# direction: "I" monitor / process info to master, "Q" command / control from master
# size_code: memory tree prefix — X bit, B byte, W word, D dword (see list.txt)
# Time-tagged ASDUs use the same IEC cell mapping as the corresponding NA_1 type;
-# CP24/CP56 time tags are carried only in the protocol, not in the PLC binding.
+# CP24/CP56 tags map monotonic last-change to Linux host wall until C_CS_NA_1 (Xenomai: CLOCK_HOST_REALTIME). +# C_CS_NA_1 binding stores master/client Unix seconds at last sync (not device time). # Monitoring — process information (1–21, 30–40)
"M_SP_NA_1 - Single-point information": (1, "BOOL", 1, "I", "X",
@@ -145,7 +146,7 @@
"Counter interrogation command"),
"C_RD_NA_1 - Read command": (102, "BOOL", 1, "Q", "X", "Read command"),
"C_CS_NA_1 - Clock synchronization command": (103, "UDINT", 32, "Q", "D",
- "Clock synchronization command"),
+ "Clock sync: master Unix seconds at last C_CS_NA_1 (client time)"), "C_TS_NA_1 - Test command": (104, "BOOL", 1, "Q", "X", "Test command"),
"C_RP_NA_1 - Reset process command": (105, "BOOL", 1, "Q", "X",
"Reset process command"),
@@ -1291,4 +1292,4 @@
\ No newline at end of file
--- a/iec60870/iec60870_runtime.c Thu May 21 18:29:21 2026 +0200
+++ b/iec60870/iec60870_runtime.c Tue May 26 13:50:51 2026 +0200
@@ -6,6 +6,13 @@
#include "iec_types_all.h"
#include "iec60870_common.h"
@@ -52,8 +59,33 @@
+ uint64_t sync_client_ms; + uint64_t sync_device_mono_ms; +static iec_bind_shadow_t iec_binding_shadow[IEC60870_NUM_BINDINGS_%(locstr)s]; +static uint64_t iec_binding_last_mono_ms[IEC60870_NUM_BINDINGS_%(locstr)s]; +void PLC_GetTime(IEC_TIME *CURRENT_TIME); +#define IEC_WALL_EPOCH_MIN_MS 978307200000ULL /* 2001-01-01 UTC */ +#ifndef CLOCK_HOST_REALTIME +#define IEC_CLOCK_HOST_REALTIME ((clockid_t)32) /* Xenomai Linux host wall clock */ +#define IEC_CLOCK_HOST_REALTIME CLOCK_HOST_REALTIME static struct iec60870_binding iec60870_bindings[] = {
@@ -82,17 +114,137 @@
*(IEC_BOOL *)p = v ? (IEC_BOOL)1 : (IEC_BOOL)0;
-/* CP24/CP56 helpers for time-tagged ASDUs (wall-clock; PLC does not drive protocol timestamps).
- * Buffers are caller-provided so concurrent iec_make_monitor_io() calls do not share static state. */
-static CP56Time2a iec_wall_cp56_buf(struct sCP56Time2a *buf) {
- CP56Time2a t = (CP56Time2a)buf;
- CP56Time2a_createFromMsTimestamp(t, (uint64_t)time(NULL) * 1000ULL);
+/* Device monotonic clock for post-sync offset + Linux host wall for pre-sync tags. + * Xenomai RT: PLC_GetTime/CLOCK_REALTIME are Cobalt time; use CLOCK_HOST_REALTIME. */ +static uint64_t iec_mono_ms(void) { + static LARGE_INTEGER freq; + if (freq.QuadPart == 0) + QueryPerformanceFrequency(&freq); + if (!QueryPerformanceCounter(&counter)) + return (uint64_t)(counter.QuadPart * 1000ULL / (uint64_t)freq.QuadPart); + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)ts.tv_nsec / 1000000ULL; +static bool iec_timespec_plausible_ms(const struct timespec *ts, uint64_t *out_ms) { + ms = (uint64_t)ts->tv_sec * 1000ULL + (uint64_t)ts->tv_nsec / 1000000ULL; + if (ms < IEC_WALL_EPOCH_MIN_MS) +static bool iec_try_clock_ms(clockid_t clk, uint64_t *out_ms) { + if (!out_ms || clock_gettime(clk, &ts) != 0) + return iec_timespec_plausible_ms(&ts, out_ms); -static CP24Time2a iec_zero_cp24_buf(struct sCP24Time2a *buf) {
+static uint64_t iec_os_wall_ms(void) { + return (uint64_t)now.tv_sec * 1000ULL + + (uint64_t)((now.tv_nsec >= 0) ? now.tv_nsec : 0) / 1000000ULL; + if (iec_try_clock_ms(IEC_CLOCK_HOST_REALTIME, &ms)) + if (iec_try_clock_ms(CLOCK_REALTIME, &ms)) + if (gettimeofday(&tv, NULL) == 0) { + gt.tv_nsec = (long)tv.tv_usec * 1000L; + if (iec_timespec_plausible_ms(>, &ms)) +static uint64_t iec_wall_ref_ms; +static uint64_t iec_wall_ref_mono_ms; +static bool iec_wall_ref_valid; +static uint64_t iec_wall_ref_last_try_mono_ms; +static void iec_wall_ref_maybe_refresh(void) { + uint64_t now_mono = iec_mono_ms(); + if (iec_wall_ref_valid && (now_mono - iec_wall_ref_last_try_mono_ms) < 1000ULL) + iec_wall_ref_last_try_mono_ms = now_mono; + wall_ms = iec_os_wall_ms(); + if (wall_ms < IEC_WALL_EPOCH_MIN_MS) + iec_wall_ref_ms = wall_ms; + iec_wall_ref_mono_ms = now_mono; + iec_wall_ref_valid = true; +static uint64_t iec_mono_to_wall_ms(uint64_t device_mono_ms) { + if (!iec_wall_ref_valid) + if (device_mono_ms >= iec_wall_ref_mono_ms) + return iec_wall_ref_ms + (device_mono_ms - iec_wall_ref_mono_ms); + return iec_wall_ref_ms; +static uint64_t iec_binding_tag_ms(const iec60870_srv_t *srv, size_t bind_idx) { + if (srv && srv->time_synced) + return srv->sync_client_ms + + (iec_binding_last_mono_ms[bind_idx] - srv->sync_device_mono_ms); + return iec_mono_to_wall_ms(iec_binding_last_mono_ms[bind_idx]); +static void iec_fill_cp56(struct sCP56Time2a *buf, uint64_t client_ms, bool valid) { memset(buf, 0, sizeof(*buf));
- return (CP24Time2a)buf;
+ CP56Time2a_createFromMsTimestamp((CP56Time2a)buf, client_ms); + CP56Time2a_setInvalid((CP56Time2a)buf, false); + CP56Time2a_setInvalid((CP56Time2a)buf, true); +static void iec_fill_cp24(struct sCP24Time2a *buf, uint64_t client_ms, bool valid) { + memset(buf, 0, sizeof(*buf)); + CP24Time2a_setInvalid((CP24Time2a)buf, true); + tod_ms = client_ms %% 86400000ULL; + CP24Time2a_setMillisecond((CP24Time2a)buf, (int)(tod_ms %% 1000ULL)); + CP24Time2a_setSecond((CP24Time2a)buf, (int)((tod_ms / 1000ULL) %% 60ULL)); + CP24Time2a_setMinute((CP24Time2a)buf, (int)((tod_ms / 60000ULL) %% 60ULL)); + CP24Time2a_setInvalid((CP24Time2a)buf, false); static CP16Time2a iec_zero_cp16_buf(struct sCP16Time2a *buf) {
@@ -100,12 +252,113 @@
+static void iec_binding_shadow_read(size_t idx, struct iec60870_binding *b, iec_bind_shadow_t *out) { + switch (b->bind_kind) { + out->b = *(IEC_BOOL *)b->iec_var; + out->by = *(IEC_BYTE *)b->iec_var; + out->i = *(IEC_INT *)b->iec_var; + out->w = *(IEC_UINT *)b->iec_var; + out->r = *(IEC_REAL *)b->iec_var; + out->u = *(UDINT *)b->iec_var; +static bool iec_binding_shadow_differs(size_t idx, struct iec60870_binding *b) { + iec_binding_shadow_read(idx, b, &cur); + switch (b->bind_kind) { + return cur.b != iec_binding_shadow[idx].b; + return cur.by != iec_binding_shadow[idx].by; + return cur.i != iec_binding_shadow[idx].i; + return cur.w != iec_binding_shadow[idx].w; + return memcmp(&cur.r, &iec_binding_shadow[idx].r, sizeof(IEC_REAL)) != 0; + return cur.u != iec_binding_shadow[idx].u; +static void iec_binding_shadow_store(size_t idx, struct iec60870_binding *b) { + iec_binding_shadow_read(idx, b, &iec_binding_shadow[idx]); +static void iec_binding_touch(size_t idx) { + iec_binding_last_mono_ms[idx] = iec_mono_ms(); + iec_binding_shadow_store(idx, &iec60870_bindings[idx]); +static void iec60870_init_binding_times(void) { + uint64_t now_mono = iec_mono_ms(); + iec_wall_ref_maybe_refresh(); + for (i = 0; i < iec60870_binding_count; i++) { + if (iec60870_bindings[i].server_index < 0 || !iec60870_bindings[i].iec_var) + iec_binding_last_mono_ms[i] = now_mono; + iec_binding_shadow_store(i, &iec60870_bindings[i]); +static void iec60870_poll_binding_changes(void) { + for (i = 0; i < iec60870_binding_count; i++) { + struct iec60870_binding *b = &iec60870_bindings[i]; + if (b->server_index < 0 || !b->iec_var) + if (iec_binding_shadow_differs(i, b)) +static bool iec_clock_sync_handler(void *parameter, IMasterConnection connection, CS101_ASDU asdu, + iec60870_srv_t *srv = (iec60870_srv_t *)parameter; + uint64_t client_ms = CP56Time2a_toMsTimestamp(newTime); + srv->sync_client_ms = client_ms; + srv->sync_device_mono_ms = iec_mono_ms(); + srv->time_synced = true; + *(UDINT *)srv->cs_plc_var = (UDINT)(client_ms / 1000ULL); static InformationObject iec_make_monitor_io(int type_id, int ioa, struct iec60870_binding *b) {
void *mem = malloc((size_t)InformationObject_getMaxSizeInMemory());
QualityDescriptor q = IEC60870_QUALITY_GOOD;
struct sCP56Time2a cp56_wall;
struct sCP24Time2a cp24_z;
struct sCP16Time2a cp16_z;
+ size_t bind_idx = (size_t)(b - iec60870_bindings); + (b->server_index >= 0) ? &iec60870_srv[b->server_index] : NULL; + uint64_t tag_ms = iec_binding_tag_ms(srv, bind_idx); struct sBinaryCounterReading bcr_st;
BinaryCounterReading bcr;
@@ -121,6 +374,9 @@
+ iec_fill_cp56(&cp56_wall, tag_ms, true); + iec_fill_cp24(&cp24_z, tag_ms, true); return (InformationObject)SinglePointInformation_create(
@@ -128,11 +384,11 @@
return (InformationObject)SinglePointWithCP24Time2a_create(
(SinglePointWithCP24Time2a)mem, ioa, iec_get_bool_var(b->iec_var), q,
- iec_zero_cp24_buf(&cp24_z));
return (InformationObject)SinglePointWithCP56Time2a_create(
(SinglePointWithCP56Time2a)mem, ioa, iec_get_bool_var(b->iec_var), q,
- iec_wall_cp56_buf(&cp56_wall));
+ (CP56Time2a)&cp56_wall); DoublePointValue dv = (DoublePointValue)(*(IEC_BYTE *)b->iec_var & (IEC_BYTE)3);
@@ -142,12 +398,12 @@
DoublePointValue dv = (DoublePointValue)(*(IEC_BYTE *)b->iec_var & (IEC_BYTE)3);
return (InformationObject)DoublePointWithCP24Time2a_create(
- (DoublePointWithCP24Time2a)mem, ioa, dv, q, iec_zero_cp24_buf(&cp24_z));
+ (DoublePointWithCP24Time2a)mem, ioa, dv, q, (CP24Time2a)&cp24_z); DoublePointValue dv = (DoublePointValue)(*(IEC_BYTE *)b->iec_var & (IEC_BYTE)3);
return (InformationObject)DoublePointWithCP56Time2a_create(
- (DoublePointWithCP56Time2a)mem, ioa, dv, q, iec_wall_cp56_buf(&cp56_wall));
+ (DoublePointWithCP56Time2a)mem, ioa, dv, q, (CP56Time2a)&cp56_wall); @@ -156,11 +412,11 @@
return (InformationObject)StepPositionWithCP24Time2a_create(
(StepPositionWithCP24Time2a)mem, ioa, (int)*(IEC_INT *)b->iec_var, false, q,
- iec_zero_cp24_buf(&cp24_z));
return (InformationObject)StepPositionWithCP56Time2a_create(
(StepPositionWithCP56Time2a)mem, ioa, (int)*(IEC_INT *)b->iec_var, false, q,
- iec_wall_cp56_buf(&cp56_wall));
+ (CP56Time2a)&cp56_wall); return (InformationObject)BitString32_createEx(
@@ -168,11 +424,11 @@
return (InformationObject)Bitstring32WithCP24Time2a_createEx(
(Bitstring32WithCP24Time2a)mem, ioa, (uint32_t)*(UDINT *)b->iec_var, q,
- iec_zero_cp24_buf(&cp24_z));
return (InformationObject)Bitstring32WithCP56Time2a_createEx(
(Bitstring32WithCP56Time2a)mem, ioa, (uint32_t)*(UDINT *)b->iec_var, q,
- iec_wall_cp56_buf(&cp56_wall));
+ (CP56Time2a)&cp56_wall); float nv = NormalizedValue_fromScaled((int)(int16_t)*(IEC_UINT *)b->iec_var);
@@ -182,12 +438,12 @@
float nv = NormalizedValue_fromScaled((int)(int16_t)*(IEC_UINT *)b->iec_var);
return (InformationObject)MeasuredValueNormalizedWithCP24Time2a_create(
- (MeasuredValueNormalizedWithCP24Time2a)mem, ioa, nv, q, iec_zero_cp24_buf(&cp24_z));
+ (MeasuredValueNormalizedWithCP24Time2a)mem, ioa, nv, q, (CP24Time2a)&cp24_z); float nv = NormalizedValue_fromScaled((int)(int16_t)*(IEC_UINT *)b->iec_var);
return (InformationObject)MeasuredValueNormalizedWithCP56Time2a_create(
- (MeasuredValueNormalizedWithCP56Time2a)mem, ioa, nv, q, iec_wall_cp56_buf(&cp56_wall));
+ (MeasuredValueNormalizedWithCP56Time2a)mem, ioa, nv, q, (CP56Time2a)&cp56_wall); float nv = NormalizedValue_fromScaled((int)(int16_t)*(IEC_UINT *)b->iec_var);
@@ -201,11 +457,11 @@
return (InformationObject)MeasuredValueScaledWithCP24Time2a_create(
(MeasuredValueScaledWithCP24Time2a)mem, ioa, (int)*(IEC_INT *)b->iec_var, q,
- iec_zero_cp24_buf(&cp24_z));
return (InformationObject)MeasuredValueScaledWithCP56Time2a_create(
(MeasuredValueScaledWithCP56Time2a)mem, ioa, (int)*(IEC_INT *)b->iec_var, q,
- iec_wall_cp56_buf(&cp56_wall));
+ (CP56Time2a)&cp56_wall); return (InformationObject)MeasuredValueShort_create(
@@ -213,11 +469,11 @@
return (InformationObject)MeasuredValueShortWithCP24Time2a_create(
(MeasuredValueShortWithCP24Time2a)mem, ioa, *(IEC_REAL *)b->iec_var, q,
- iec_zero_cp24_buf(&cp24_z));
return (InformationObject)MeasuredValueShortWithCP56Time2a_create(
(MeasuredValueShortWithCP56Time2a)mem, ioa, *(IEC_REAL *)b->iec_var, q,
- iec_wall_cp56_buf(&cp56_wall));
+ (CP56Time2a)&cp56_wall); bcr = BinaryCounterReading_create((BinaryCounterReading)&bcr_st,
@@ -227,12 +483,12 @@
bcr = BinaryCounterReading_create((BinaryCounterReading)&bcr_st,
(int32_t)(*(UDINT *)b->iec_var), 0, false, false, false);
return (InformationObject)IntegratedTotalsWithCP24Time2a_create(
- (IntegratedTotalsWithCP24Time2a)mem, ioa, bcr, iec_zero_cp24_buf(&cp24_z));
+ (IntegratedTotalsWithCP24Time2a)mem, ioa, bcr, (CP24Time2a)&cp24_z); bcr = BinaryCounterReading_create((BinaryCounterReading)&bcr_st,
(int32_t)(*(UDINT *)b->iec_var), 0, false, false, false);
return (InformationObject)IntegratedTotalsWithCP56Time2a_create(
- (IntegratedTotalsWithCP56Time2a)mem, ioa, bcr, iec_wall_cp56_buf(&cp56_wall));
+ (IntegratedTotalsWithCP56Time2a)mem, ioa, bcr, (CP56Time2a)&cp56_wall); pack_u32 = (uint32_t)*(UDINT *)b->iec_var;
@@ -241,20 +497,20 @@
SingleEvent_setEventState(se, (EventState)(pack_u32 & 3u));
SingleEvent_setQDP(se, (QualityDescriptorP)((pack_u32 >> 2) & 0xffu));
return (InformationObject)EventOfProtectionEquipment_create(
- (EventOfProtectionEquipment)mem, ioa, se, iec_zero_cp16_buf(&cp16_z), iec_zero_cp24_buf(&cp24_z));
+ (EventOfProtectionEquipment)mem, ioa, se, iec_zero_cp16_buf(&cp16_z), (CP24Time2a)&cp24_z); pack_u32 = (uint32_t)*(UDINT *)b->iec_var;
ste = (StartEvent)(pack_u32 & 0xffu);
qdp = (QualityDescriptorP)((pack_u32 >> 8) & 0xffu);
return (InformationObject)PackedStartEventsOfProtectionEquipment_create(
(PackedStartEventsOfProtectionEquipment)mem, ioa, ste, qdp, iec_zero_cp16_buf(&cp16_z),
- iec_zero_cp24_buf(&cp24_z));
pack_u32 = (uint32_t)*(UDINT *)b->iec_var;
oci = (OutputCircuitInfo)(pack_u32 & 0xffu);
qdp = (QualityDescriptorP)((pack_u32 >> 8) & 0xffu);
return (InformationObject)PackedOutputCircuitInfo_create(
- (PackedOutputCircuitInfo)mem, ioa, oci, qdp, iec_zero_cp16_buf(&cp16_z), iec_zero_cp24_buf(&cp24_z));
+ (PackedOutputCircuitInfo)mem, ioa, oci, qdp, iec_zero_cp16_buf(&cp16_z), (CP24Time2a)&cp24_z); pack_u32 = (uint32_t)*(UDINT *)b->iec_var;
@@ -264,21 +520,21 @@
SingleEvent_setQDP(se, (QualityDescriptorP)((pack_u32 >> 2) & 0xffu));
return (InformationObject)EventOfProtectionEquipmentWithCP56Time2a_create(
(EventOfProtectionEquipmentWithCP56Time2a)mem, ioa, se, iec_zero_cp16_buf(&cp16_z),
- iec_wall_cp56_buf(&cp56_wall));
+ (CP56Time2a)&cp56_wall); pack_u32 = (uint32_t)*(UDINT *)b->iec_var;
ste = (StartEvent)(pack_u32 & 0xffu);
qdp = (QualityDescriptorP)((pack_u32 >> 8) & 0xffu);
return (InformationObject)PackedStartEventsOfProtectionEquipmentWithCP56Time2a_create(
(PackedStartEventsOfProtectionEquipmentWithCP56Time2a)mem, ioa, ste, qdp,
- iec_zero_cp16_buf(&cp16_z), iec_wall_cp56_buf(&cp56_wall));
+ iec_zero_cp16_buf(&cp16_z), (CP56Time2a)&cp56_wall); pack_u32 = (uint32_t)*(UDINT *)b->iec_var;
oci = (OutputCircuitInfo)(pack_u32 & 0xffu);
qdp = (QualityDescriptorP)((pack_u32 >> 8) & 0xffu);
return (InformationObject)PackedOutputCircuitInfoWithCP56Time2a_create(
(PackedOutputCircuitInfoWithCP56Time2a)mem, ioa, oci, qdp, iec_zero_cp16_buf(&cp16_z),
- iec_wall_cp56_buf(&cp56_wall));
+ (CP56Time2a)&cp56_wall); memset(&scd_st, 0, sizeof(scd_st));
@@ -316,7 +572,7 @@
(FileSegment)mem, ioa, 0, 0, &seg_dummy, 0);
return (InformationObject)FileDirectory_create(
- (FileDirectory)mem, ioa, 0, (uint32_t)*(UDINT *)b->iec_var, 0, iec_wall_cp56_buf(&cp56_wall));
+ (FileDirectory)mem, ioa, 0, (uint32_t)*(UDINT *)b->iec_var, 0, (CP56Time2a)&cp56_wall); @@ -379,7 +635,7 @@
if (t >= C_SC_TA_1 && t <= C_BO_TA_1)
- if (t >= C_IC_NA_1 && t <= C_TS_TA_1)
+ if (t >= C_IC_NA_1 && t <= C_TS_TA_1 && t != C_CS_NA_1) if (t >= P_ME_NA_1 && t <= P_AC_NA_1)
@@ -505,12 +761,6 @@
iec_set_bool_var(match->iec_var, true);
- ClockSynchronizationCommand cs = (ClockSynchronizationCommand)io;
- *(UDINT *)match->iec_var =
- (UDINT)(CP56Time2a_toMsTimestamp(ClockSynchronizationCommand_getTime(cs)) & 0xffffffffu);
iec_set_bool_var(match->iec_var, TestCommand_isValid((TestCommand)io));
@@ -564,6 +814,7 @@
+ iec_binding_touch((size_t)(match - iec60870_bindings)); IMasterConnection_sendACT_CON(connection, asdu, false);
@@ -595,8 +846,10 @@
+ iec_wall_ref_maybe_refresh(); for (si = 0; si < IEC60870_NUM_SERVERS_%(locstr)s; si++) {
iec60870_srv_t *s = &iec60870_srv[si];
s->slave = CS104_Slave_create(200, 200);
@@ -628,11 +881,27 @@
CS104_Slave_setInterrogationHandler(s->slave, iec_interrogation_handler, s);
+ CS104_Slave_setClockSyncHandler(s->slave, iec_clock_sync_handler, s); CS104_Slave_setASDUHandler(s->slave, iec_asdu_handler, s);
CS104_Slave_setConnectionEventHandler(s->slave, iec_connection_event, s);
CS104_Slave_start(s->slave);
+ s->sync_device_mono_ms = 0; + s->time_synced = false; + for (bi = 0; bi < iec60870_binding_count; bi++) { + if (iec60870_bindings[bi].server_index != si) + if (!iec60870_bindings[bi].is_command) + if (iec60870_bindings[bi].type_id == C_CS_NA_1) { + s->cs_plc_var = iec60870_bindings[bi].iec_var; + iec60870_init_binding_times(); @@ -650,6 +919,8 @@
void __publish_%(locstr)s(void) {
+ iec_wall_ref_maybe_refresh(); + iec60870_poll_binding_changes(); int __cleanup_%(locstr)s(void) {
@@ -662,6 +933,8 @@
+ s->time_synced = false;
\ No newline at end of file