Auto: twincat-1.2 — native UA TIME/DATE/DT/TOD
IEC 61131-3 TIME/TOD now surface as TimeSpan (UA Duration); DATE/DT surface as DateTime (UTC). The wire form stays UDINT — AdsTwinCATClient post-processes raw values in ReadValueAsync and OnAdsNotificationEx, and accepts native CLR types in ConvertForWrite. Added Duration to DriverDataType (back-compat: existing switches default to BaseDataType for unknown enum values) and mapped it to DataTypeIds.Duration in DriverNodeManager. Closes #306
This commit is contained in:
@@ -25,4 +25,11 @@ public enum DriverDataType
|
||||
|
||||
/// <summary>Galaxy-style attribute reference encoded as an OPC UA String.</summary>
|
||||
Reference,
|
||||
|
||||
/// <summary>
|
||||
/// OPC UA <c>Duration</c> — a Double-encoded period in milliseconds. Subtype of Double
|
||||
/// in the address space; surfaced as <see cref="System.TimeSpan"/> in the driver layer.
|
||||
/// Used by IEC 61131-3 <c>TIME</c> / <c>TOD</c> attributes (TwinCAT et al.).
|
||||
/// </summary>
|
||||
Duration,
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
var value = result.Value;
|
||||
if (bitIndex is int bit && type == TwinCATDataType.Bool && value is not bool)
|
||||
value = ExtractBit(value, bit);
|
||||
value = PostProcessIecTime(type, value);
|
||||
|
||||
return (value, TwinCATStatusMapper.Good);
|
||||
}
|
||||
@@ -143,6 +144,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
var value = args.Value;
|
||||
if (reg.BitIndex is int bit && reg.Type == TwinCATDataType.Bool && value is not bool)
|
||||
value = ExtractBit(value, bit);
|
||||
value = PostProcessIecTime(reg.Type, value);
|
||||
try { reg.OnChange(reg.SymbolPath, value); } catch { /* consumer-side errors don't crash the ADS thread */ }
|
||||
}
|
||||
|
||||
@@ -249,7 +251,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
_ => typeof(int),
|
||||
};
|
||||
|
||||
private static object ConvertForWrite(TwinCATDataType type, object? value) => type switch
|
||||
internal static object ConvertForWrite(TwinCATDataType type, object? value) => type switch
|
||||
{
|
||||
TwinCATDataType.Bool => Convert.ToBoolean(value),
|
||||
TwinCATDataType.SInt => Convert.ToSByte(value),
|
||||
@@ -263,11 +265,79 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
TwinCATDataType.Real => Convert.ToSingle(value),
|
||||
TwinCATDataType.LReal => Convert.ToDouble(value),
|
||||
TwinCATDataType.String or TwinCATDataType.WString => Convert.ToString(value) ?? string.Empty,
|
||||
TwinCATDataType.Time or TwinCATDataType.Date
|
||||
or TwinCATDataType.DateTime or TwinCATDataType.TimeOfDay => Convert.ToUInt32(value),
|
||||
// IEC durations (TIME / TOD) accept TimeSpan / Duration-as-Double-ms / raw UDINT.
|
||||
// IEC timestamps (DATE / DT) accept DateTime (UTC) / raw UDINT seconds-since-epoch.
|
||||
TwinCATDataType.Time or TwinCATDataType.TimeOfDay => DurationToUDInt(value),
|
||||
TwinCATDataType.Date or TwinCATDataType.DateTime => DateTimeToUDInt(value),
|
||||
_ => throw new NotSupportedException($"TwinCATDataType {type} not writable."),
|
||||
};
|
||||
|
||||
// IEC 61131-3 epoch is 1970-01-01 UTC for DATE / DT; TIME / TOD are unsigned ms counters.
|
||||
private static readonly DateTime IecEpochUtc = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>
|
||||
/// Convert the raw UDINT wire value for IEC TIME/DATE/DT/TOD into the native CLR type
|
||||
/// surfaced upstream — TimeSpan for durations, DateTime (UTC) for timestamps. Other
|
||||
/// types pass through unchanged.
|
||||
/// </summary>
|
||||
internal static object? PostProcessIecTime(TwinCATDataType type, object? value)
|
||||
{
|
||||
if (value is null) return null;
|
||||
var raw = TryGetUInt32(value);
|
||||
if (raw is null) return value;
|
||||
return type switch
|
||||
{
|
||||
// TIME / TOD — UDINT milliseconds.
|
||||
TwinCATDataType.Time or TwinCATDataType.TimeOfDay
|
||||
=> TimeSpan.FromMilliseconds(raw.Value),
|
||||
// DT — UDINT seconds since 1970-01-01 UTC.
|
||||
TwinCATDataType.DateTime
|
||||
=> IecEpochUtc.AddSeconds(raw.Value),
|
||||
// DATE — UDINT seconds since 1970-01-01 UTC, but TwinCAT runtimes pin the time
|
||||
// component to midnight; pass through the same conversion so we get a date-only
|
||||
// value at midnight UTC.
|
||||
TwinCATDataType.Date
|
||||
=> IecEpochUtc.AddSeconds(raw.Value),
|
||||
_ => value,
|
||||
};
|
||||
}
|
||||
|
||||
private static uint? TryGetUInt32(object value) => value switch
|
||||
{
|
||||
uint u => u,
|
||||
int i when i >= 0 => (uint)i,
|
||||
ushort us => (uint)us,
|
||||
short s when s >= 0 => (uint)s,
|
||||
long l when l >= 0 && l <= uint.MaxValue => (uint)l,
|
||||
ulong ul when ul <= uint.MaxValue => (uint)ul,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static uint DurationToUDInt(object? value) => value switch
|
||||
{
|
||||
TimeSpan ts => (uint)Math.Max(0, ts.TotalMilliseconds),
|
||||
// OPC UA Duration on the wire is a Double in milliseconds.
|
||||
double d => (uint)Math.Max(0, d),
|
||||
float f => (uint)Math.Max(0, f),
|
||||
_ => Convert.ToUInt32(value),
|
||||
};
|
||||
|
||||
private static uint DateTimeToUDInt(object? value)
|
||||
{
|
||||
if (value is DateTime dt)
|
||||
{
|
||||
var utc = dt.Kind == DateTimeKind.Unspecified
|
||||
? DateTime.SpecifyKind(dt, DateTimeKind.Utc)
|
||||
: dt.ToUniversalTime();
|
||||
var seconds = (long)(utc - IecEpochUtc).TotalSeconds;
|
||||
if (seconds < 0 || seconds > uint.MaxValue)
|
||||
throw new ArgumentOutOfRangeException(nameof(value),
|
||||
"DATE/DT value out of UDINT epoch range (1970-01-01..2106-02-07 UTC).");
|
||||
return (uint)seconds;
|
||||
}
|
||||
return Convert.ToUInt32(value);
|
||||
}
|
||||
|
||||
private static bool ExtractBit(object? rawWord, int bit) => rawWord switch
|
||||
{
|
||||
short s => (s & (1 << bit)) != 0,
|
||||
|
||||
@@ -42,8 +42,11 @@ public static class TwinCATDataTypeExtensions
|
||||
TwinCATDataType.Real => DriverDataType.Float32,
|
||||
TwinCATDataType.LReal => DriverDataType.Float64,
|
||||
TwinCATDataType.String or TwinCATDataType.WString => DriverDataType.String,
|
||||
TwinCATDataType.Time or TwinCATDataType.Date
|
||||
or TwinCATDataType.DateTime or TwinCATDataType.TimeOfDay => DriverDataType.Int32,
|
||||
// IEC 61131-3 TIME / TOD are durations (ms); DATE / DT are absolute timestamps.
|
||||
// The wire form is UDINT but the driver post-processes into TimeSpan / DateTime so the
|
||||
// address space surfaces native UA Duration / DateTime instead of opaque integers.
|
||||
TwinCATDataType.Time or TwinCATDataType.TimeOfDay => DriverDataType.Duration,
|
||||
TwinCATDataType.Date or TwinCATDataType.DateTime => DriverDataType.DateTime,
|
||||
TwinCATDataType.Structure => DriverDataType.String,
|
||||
_ => DriverDataType.Int32,
|
||||
};
|
||||
|
||||
@@ -310,6 +310,7 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
|
||||
DriverDataType.Float64 => DataTypeIds.Double,
|
||||
DriverDataType.String => DataTypeIds.String,
|
||||
DriverDataType.DateTime => DataTypeIds.DateTime,
|
||||
DriverDataType.Duration => DataTypeIds.Duration,
|
||||
_ => DataTypeIds.BaseDataType,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user