diff --git a/AdapterInterface/Adapter.cs b/AdapterInterface/Adapter.cs index 050d80dc..dbde7e2d 100644 --- a/AdapterInterface/Adapter.cs +++ b/AdapterInterface/Adapter.cs @@ -11,6 +11,7 @@ using System.Data; using System.Collections.Concurrent; using System.Threading; +using System.Net.NetworkInformation; namespace Mtconnect { @@ -270,7 +271,7 @@ public virtual void Send(DataItemSendTypes sendType = DataItemSendTypes.Changed, break; case DataItemSendTypes.Changed: var changedDataItems = DataItems - .SelectMany(o => o.ItemList(sendType == DataItemSendTypes.All)) + .SelectMany(o => o.ItemList(sendType == DataItemSendTypes.Changed)) .Where(o => o.HasChanged); values = changedDataItems.Select(o => new ReportedValue(o)).ToList(); // TODO: Clear ConcurrentQueue of matching DataItems. @@ -330,7 +331,7 @@ public virtual void AddAsset(Asset asset) { StringBuilder sb = new StringBuilder(); - DateTime now = DateTime.UtcNow; + DateTime now = TimeHelper.GetNow(); sb.Append(now.ToString(DATE_TIME_FORMAT)); sb.Append("|@ASSET@|"); sb.Append(asset.AssetId); @@ -414,5 +415,25 @@ public virtual void Stop() source.OnDataReceived -= _source_OnDataReceived; } } + + public string[] GetDataItemNames() => DataItems?.Select(o => o.Name)?.DefaultIfEmpty().ToArray(); + + /// + /// Handle an incoming command from a client. + /// + /// Reference to the incoming message command. + /// Reference to the client that sent the command + /// + public virtual bool HandleCommand(string command, string clientId = null) + { + var response = AdapterCommands.GetResponse(this, command); + if (!string.IsNullOrEmpty(response)) + { + Write($"{response}\n", clientId); + return true; + } + + return false; + } } } \ No newline at end of file diff --git a/AdapterInterface/AdapterCommands.cs b/AdapterInterface/AdapterCommands.cs new file mode 100644 index 00000000..731c6fb0 --- /dev/null +++ b/AdapterInterface/AdapterCommands.cs @@ -0,0 +1,94 @@ +using System; +using System.Net.NetworkInformation; +using System.Reflection; + +namespace Mtconnect +{ + /// + /// A collection of methods that return formatted responses to commands issued from a MTConnect Agent. + /// + public static class AdapterCommands + { + /// + /// Determines the appropriate response to the provided . + /// + /// Reference to the Adapter implementation. + /// Reference to the command issued from the MTConnect Agent. + /// Formatted response to the command issued by the MTConnect Agent. If the command was not recognized, then the response string is empty. + public static string GetResponse(this Adapter adapter, string message) + { + const string PING = "* PING"; + const string GET_DATAITEMS = "* dataItems"; + const string GET_DATAITEM_VALUE = "* dataItem "; + const string GET_DATAITEM_DESCRIPTION = "* dataItemDescription "; + + message = message?.Trim(); + if (message.StartsWith(PING, StringComparison.OrdinalIgnoreCase)) + { + return AdapterCommands.Ping(adapter); + } + else if (message.Equals(GET_DATAITEMS, StringComparison.OrdinalIgnoreCase)) + { + return AdapterCommands.DataItems(adapter); + } + else if (message.StartsWith(GET_DATAITEM_DESCRIPTION, StringComparison.OrdinalIgnoreCase)) + { + return AdapterCommands.DataItemDescription(adapter, message); + } + else if (message.StartsWith(GET_DATAITEM_VALUE, StringComparison.OrdinalIgnoreCase)) + { + return AdapterCommands.DataItem(adapter, message); + } + + return string.Empty; + } + + /// + /// Handles the "* PONG <heartbeat>" command to the MTConnect Agent + /// + /// Informs the agent of all dataItems that can be published by the adapter + public static string Ping(Adapter adapter) + => AgentCommands.Pong(adapter.Heartbeat); + + /// + /// Handles the "* dataItems: XXX" command to the MTConnect Agent + /// + /// Informs the agent of all dataItems that can be published by the adapter + public static string DataItems(Adapter adapter) + => $"* dataItems: {string.Join("|", adapter.GetDataItemNames())}"; + + /// + /// Handles the "* dataItemDescription: XXX" command to the MTConnect Agent + /// + /// Informs the agent of the DataItem description, if any, for the provided DataItem name + public static string DataItemDescription(Adapter adapter, string message) + { + string dataItemName = message.Remove(0, message.LastIndexOf(' ') + 1); + if (!adapter.Contains(dataItemName)) + { + return AgentCommands.Error($"Cannot find DataItem '{dataItemName}'"); + } + + if (string.IsNullOrEmpty(adapter[dataItemName].Description)) + { + return AgentCommands.Error($"No description available for DataItem '{dataItemName}'"); + } + + return $"* dataItemDescription: {adapter[dataItemName].Description}"; + } + + /// + /// Handles the "* dataItem: XXX" command to the MTConnect Agent + /// + /// Informs the agent of the current value for the provided DataItem + public static string DataItem(Adapter adapter, string message) + { + string dataItemName = message.Remove(0, message.LastIndexOf(' ') + 1); + if (!adapter.Contains(dataItemName)) + { + return AgentCommands.Error($"Cannot find DataItem '{dataItemName}'"); + } + return $"* dataItem: {adapter[dataItemName]}"; + } + } +} \ No newline at end of file diff --git a/AdapterInterface/AdapterExtensions.cs b/AdapterInterface/AdapterExtensions.cs index 5cf8a599..a61451e6 100644 --- a/AdapterInterface/AdapterExtensions.cs +++ b/AdapterInterface/AdapterExtensions.cs @@ -40,6 +40,11 @@ public static bool TryAddDataItems(this Adapter adapter, IAdapterDataModel model { return adapter.TryAddDataItems(model, string.Empty, string.Empty); } + /// + /// Gets a list of that is either decorated with the or an implementation of the type . + /// + /// Reference to the model type to reflect upon. + /// Collection of properties that represent s private static PropertyInfo[] GetDataItemProperties(Type type) { return type.GetProperties(BindingFlags.Instance | BindingFlags.Public) @@ -49,6 +54,18 @@ private static PropertyInfo[] GetDataItemProperties(Type type) ) .ToArray(); } + /// + /// Gets a list of that is decorated with the . + /// + /// Reference to the model type to reflect upon. + /// Collection of properties that represent the value for + private static PropertyInfo[] GetDataItemTimestampProperties(Type type) + { + return type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(o => o.GetCustomAttribute(typeof(TimestampAttribute)) != null) + .ToArray(); + } + private static ConcurrentDictionary> _dataItemTimestampProperties = new ConcurrentDictionary>(); private static ConcurrentDictionary _dataItemProperties = new ConcurrentDictionary(); private static HashSet _dataItemTypes = new HashSet { @@ -65,11 +82,33 @@ private static bool TryAddDataItems(this Adapter adapter, object model, string d lock (_dataItemProperties) { Type dataModelType = model.GetType(); + + Dictionary timestampPropertyLookup; + _dataItemTimestampProperties.TryGetValue(dataModelType, out timestampPropertyLookup); + + // Try to get the PropertyInfo[] from the static cache if (!_dataItemProperties.TryGetValue(dataModelType, out PropertyInfo[] dataItemProperties)) { + // Reflect and fill the static cache dataItemProperties = GetDataItemProperties(dataModelType); adapter._logger?.LogDebug("Found {DataItemPropertyCount} DataItems from data model {DataModelType}: {@DataItemTypes}", dataItemProperties.Length, dataModelType.FullName, dataItemProperties); _dataItemProperties.TryAdd(dataModelType, dataItemProperties); + + // Reflect and fill static cache of Timestamp properties + _dataItemTimestampProperties.TryAdd(dataModelType, new Dictionary()); + + var timestampProperties = GetDataItemTimestampProperties(dataModelType); + foreach (var timestampProperty in timestampProperties) + { + TimestampAttribute attr = timestampProperty.GetCustomAttribute(); + if (attr != null && !_dataItemTimestampProperties[dataModelType].ContainsKey(attr.DataItemName)) + { + adapter._logger?.LogInformation("Registering Timestamp override for DataItem {DataItemName} from property {PropertyName}", attr.DataItemName, timestampProperty.Name); + _dataItemTimestampProperties[dataModelType].Add(attr.DataItemName, timestampProperty); + } + } + timestampPropertyLookup = _dataItemTimestampProperties[dataModelType]; + isCached = false; } @@ -89,6 +128,11 @@ private static bool TryAddDataItems(this Adapter adapter, object model, string d dataItemDescription = dataItemDescriptionPrefix + dataItemAttribute.Description; } + PropertyInfo timestampProperty = null; + timestampPropertyLookup.TryGetValue(dataItemName, out timestampProperty); + + DataItem dataItem = null; + // Check if the property is already of type DataItem if (_dataItemTypes.Contains(property.PropertyType)) { @@ -105,15 +149,16 @@ private static bool TryAddDataItems(this Adapter adapter, object model, string d continue; } + dataItem = dataItemProperty as DataItem; + if (dataItemAttribute != null) { // Update the name/description of the DataItem based on the decorating attribute. - (dataItemProperty as DataItem).Name = dataItemName; - (dataItemProperty as DataItem).Description = dataItemDescription; + dataItem.Name = dataItemName; + dataItem.Description = dataItemDescription; // TODO: Check for Type mismatch } - dataItemAdded = adapter.TryAddDataItem(dataItemProperty as DataItem, !isCached); } else if (dataItemAttribute != null) { @@ -123,19 +168,19 @@ private static bool TryAddDataItems(this Adapter adapter, object model, string d dataItemAdded = adapter.TryAddDataItems(property.PropertyType, dataItemName, dataItemDescription); break; case EventAttribute _: - dataItemAdded = adapter.TryAddDataItem(new Event(dataItemName, dataItemDescription), !isCached); + dataItem = new Event(dataItemName, dataItemDescription); break; case SampleAttribute _: - dataItemAdded = adapter.TryAddDataItem(new Sample(dataItemName, dataItemDescription), !isCached); + dataItem = new Sample(dataItemName, dataItemDescription); break; case ConditionAttribute _: - dataItemAdded = adapter.TryAddDataItem(new Condition(dataItemName, dataItemDescription), !isCached); + dataItem = new Condition(dataItemName, dataItemDescription); break; case TimeSeriesAttribute _: - dataItemAdded = adapter.TryAddDataItem(new TimeSeries(dataItemName, dataItemDescription), !isCached); + dataItem = new TimeSeries(dataItemName, dataItemDescription); break; case MessageAttribute _: - dataItemAdded = adapter.TryAddDataItem(new Message(dataItemName, dataItemDescription), !isCached); + dataItem = new Message(dataItemName, dataItemDescription); break; default: dataItemAdded = false; @@ -147,6 +192,16 @@ private static bool TryAddDataItems(this Adapter adapter, object model, string d adapter._logger?.LogDebug("DataItem {DataItemName} not handled", dataItemName); } + // Now add the DataItem if it was constructed + if (dataItem != null) + { + if (timestampProperty != null) + { + dataItem.HasTimestampOverride = true; + } + dataItemAdded = adapter.TryAddDataItem(dataItem, !isCached); + } + } catch (Exception ex) { @@ -211,6 +266,28 @@ private static bool TryUpdateValues(this Adapter adapter, object model, string d if (!dataItemUpdated) allDataItemsUpdated = false; } + if (_dataItemTimestampProperties.TryGetValue(sourceType, out Dictionary timestampProperties)) + { + foreach (var kvp in timestampProperties) + { + var property = kvp.Value; + var rawTimestamp = property.GetValue(model); + if (rawTimestamp != null) + { + var formattedTimestamp = rawTimestamp as DateTime?; + if (formattedTimestamp != null) + { + adapter[kvp.Key].LastChanged = formattedTimestamp; + } + else + { + var castException = new InvalidCastException("Could not cast to DateTime"); + adapter._logger?.LogError(castException, "Failed to cast Timestamp property {PropertyName} to proper DateTime", property.Name); + } + } + } + } + return allDataItemsUpdated; } } diff --git a/AdapterInterface/AdapterInterface.csproj b/AdapterInterface/AdapterInterface.csproj index 0191e191..cdf39f7a 100644 --- a/AdapterInterface/AdapterInterface.csproj +++ b/AdapterInterface/AdapterInterface.csproj @@ -21,7 +21,7 @@ icon.ico https://github.com/TrueAnalyticsSolutions/MtconnectCore.Adapter $(ProjectUrl) - 1.0.12 + 1.0.13 diff --git a/AdapterInterface/AgentCommands.cs b/AdapterInterface/AgentCommands.cs new file mode 100644 index 00000000..52d3b875 --- /dev/null +++ b/AdapterInterface/AgentCommands.cs @@ -0,0 +1,125 @@ +using System; +using System.Reflection; + +namespace Mtconnect +{ + /// + /// A collection of methods that return formatted commands to be issued to a MTConnect Agent. See C++ Reference Agent on GitHub. + /// + public static class AgentCommands + { + /// + /// Handles the "* PONG <heartbeat>" command to the MTConnect Agent + /// + /// Sends the response to * PING with a reference to the heartbeat + public static string Pong(double? heartbeat = null) + => heartbeat.HasValue + ? $"* PONG {heartbeat}" + : "* PONG"; + + /// + /// Handles the "* adapterVersion: <version>" command to the MTConnect Agent + /// + /// Specify the Adapter Software Version the adapter supports + public static string AdapterVersion() + => $"* adapterVersion: {Assembly.GetEntryAssembly()?.GetName()?.Version?.ToString()}"; + + /// + /// Handles the "* calibration: XXX" command to the MTConnect Agent + /// + /// Set the calibration in the device component of the associated device + /// + public static string Calibration() + => throw new NotImplementedException(); + + /// + /// Handles the "* conversionRequired: <yes|no>" command to the MTConnect Agent + /// + /// Tell the agent that the data coming from this adapter requires conversion + /// + public static string ConversionRequired() + => throw new NotImplementedException(); + + /// + /// Handles the "* device: <uuid|name>" command to the MTConnect Agent + /// + /// Specify the default device for this adapter. The device can be specified as either the device name or UUID + /// + public static string Device() + => throw new NotImplementedException(); + + /// + /// Handles the "* description: XXX" command to the MTConnect Agent + /// + /// Set the description in the device header of the associated device + /// + public static string Description() + => throw new NotImplementedException(); + + /// + /// Handles the "* manufacturer: XXX" command to the MTConnect Agent + /// + /// Set the manufacturer in the device header of the associated device + /// + public static string Manufacturer() + => throw new NotImplementedException(); + + /// + /// Handles the "* mtconnectVersion: <version>" command to the MTConnect Agent + /// + /// Specify the MTConnect Version the adapter supports + /// + public static string MtconnectVersion() + => throw new NotImplementedException(); + + /// + /// Handles the "* nativeName: XXX" command to the MTConnect Agent + /// + /// Set the nativeName in the device component of the associated device + /// + public static string NativeName() + => throw new NotImplementedException(); + + /// + /// Handles the "* realTime: <yes|no>" command to the MTConnect Agent + /// + /// Tell the agent that the data coming from this adapter would like real-time priority + /// + public static string RealTime() + => throw new NotImplementedException(); + + /// + /// Handles the "* relativeTime: <yes|no>" command to the MTConnect Agent + /// + /// Tell the agent that the data coming from this adapter is specified in relative time + public static string RelativeTime() + => $"* relativeTime: no"; + + /// + /// Handles the "* serialNumber: XXX" command to the MTConnect Agent + /// + /// Set the serialNumber in the device header of the associated device + /// + public static string SerialNumber() + => throw new NotImplementedException(); + + /// + /// Handles the "* shdrVersion: <version>" command to the MTConnect Agent + /// + /// Specify the version of the SHDR protocol delivered by the adapter. See ShdrVersion + /// + public static string ShdrVersion() + => throw new NotImplementedException(); + + /// + /// Handles the "* station: XXX" command to the MTConnect Agent + /// + /// Set the station in the device header of the associated device + /// + public static string Station() + => throw new NotImplementedException(); + + + public static string Error(string message) => $"* error: {message}"; + } +} \ No newline at end of file diff --git a/AdapterInterface/Contracts/AdapterStates.cs b/AdapterInterface/Contracts/AdapterStates.cs index 4b3e4ffc..7e3fadef 100644 --- a/AdapterInterface/Contracts/AdapterStates.cs +++ b/AdapterInterface/Contracts/AdapterStates.cs @@ -1,4 +1,6 @@  +using static System.Net.WebRequestMethods; + namespace Mtconnect.AdapterInterface.Contracts { /// diff --git a/AdapterInterface/Contracts/Attributes/DataItemAttribute.cs b/AdapterInterface/Contracts/Attributes/DataItemAttribute.cs index 02772480..cafaf499 100644 --- a/AdapterInterface/Contracts/Attributes/DataItemAttribute.cs +++ b/AdapterInterface/Contracts/Attributes/DataItemAttribute.cs @@ -1,7 +1,5 @@ using Mtconnect.AdapterInterface.DataItems; using System; -using System.Collections.Generic; -using System.Text; namespace Mtconnect.AdapterInterface.Contracts.Attributes { diff --git a/AdapterInterface/Contracts/Attributes/TimestampAttribute.cs b/AdapterInterface/Contracts/Attributes/TimestampAttribute.cs new file mode 100644 index 00000000..bb755162 --- /dev/null +++ b/AdapterInterface/Contracts/Attributes/TimestampAttribute.cs @@ -0,0 +1,48 @@ +using Mtconnect.AdapterInterface.DataItems; +using System; + +namespace Mtconnect.AdapterInterface.Contracts.Attributes +{ + /// + /// Indicates that a property can be the source for . + /// Important: the property must be within the same entity as the property (or a property decorated with ). + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] + public class TimestampAttribute : Attribute + { + /// + /// Reference to the DataItem for which this timestamp relates. + /// + public string DataItemName { get; } + + /// + /// Indicates the methodology for how the timestamp was originally synchronized. This is particularly useful when the timestamp originates from another known system. + /// + public TimestampSyncTypes SynchronizationType { get; } = TimestampSyncTypes.Unknown; + + /// + /// Indicates the accuracy of the timestamp. + /// + public TimestampResolutionTypes ResolutionType { get; } = TimestampResolutionTypes.Unknown; + + /// + /// Indicates any potential causes for timestamp drift that can affect the resolution. + /// + public TimestampDriftCauses DriftCauses { get; } = TimestampDriftCauses.None; + + /// + /// + /// + /// + /// + /// + /// + public TimestampAttribute(string dataItemName, TimestampSyncTypes syncType = TimestampSyncTypes.Unknown, TimestampResolutionTypes resolutionType = TimestampResolutionTypes.Unknown, TimestampDriftCauses driftCauses = TimestampDriftCauses.None) + { + DataItemName = dataItemName; + SynchronizationType = syncType; + ResolutionType = resolutionType; + DriftCauses = driftCauses; + } + } +} diff --git a/AdapterInterface/Contracts/TimestampDriftCauses.cs b/AdapterInterface/Contracts/TimestampDriftCauses.cs new file mode 100644 index 00000000..75f6d9c7 --- /dev/null +++ b/AdapterInterface/Contracts/TimestampDriftCauses.cs @@ -0,0 +1,40 @@ +using System; + +namespace Mtconnect.AdapterInterface.Contracts +{ + /// + /// Indicators for what may contribute to timestamp drift + /// + [Flags] + public enum TimestampDriftCauses + { + /// + /// There are no drift causes or they are unknown. + /// + None = 0, + /// + /// Timestamp drift can occur when clocks on different systems are not perfectly synchronized. This can cause timestamps generated on one system to be slightly different from the actual time of the event. + /// + ClockDrift = 1 << 0, + /// + /// Timestamp drift can occur due to the time it takes to write data to disk, as the actual time of the event may be slightly different from the time recorded in the timestamp. + /// + DiskWrite = 1 << 1, + /// + /// Disk read time refers to the amount of time it takes to read data from disk. If a system is reading a timestamp from disk to determine the time of an event, the actual time of the event may be slightly different from the time recorded in the timestamp due to the disk read time. This can cause timestamp drift, as the actual time of the event and the time recorded in the timestamp may not be perfectly synchronized. + /// + DiskRead = 1 << 2, + /// + /// Timestamp drift can occur due to the time it takes for a system to handle an interrupt, which can cause inaccuracies in timestamps generated by the system. + /// + InterruptHandlingDelay = 1 << 3, + /// + /// Timestamp drift can occur due to the amount of time it takes for a network packet to travel from the source to the destination. This delay can cause the actual time of the event to be different from the time recorded in the timestamp. + /// + NetworkDelay = 1 << 4, + /// + /// Timestamp drift can occur due to small variations in the time it takes for a CPU to perform operations, which can cause inaccuracies in timestamps generated by the system. + /// + CpuJitter = 1 << 5, + } +} diff --git a/AdapterInterface/Contracts/TimestampResolutionTypes.cs b/AdapterInterface/Contracts/TimestampResolutionTypes.cs new file mode 100644 index 00000000..91589df4 --- /dev/null +++ b/AdapterInterface/Contracts/TimestampResolutionTypes.cs @@ -0,0 +1,25 @@ +namespace Mtconnect.AdapterInterface.Contracts +{ + /// + /// Indicators for how accurate the timestamp may be to the true occurance of an event. + /// + public enum TimestampResolutionTypes + { + /// + /// Indicates that the accuracy of a timestamp is not known or quantifiable. + /// + Unknown = -1, + /// + /// Indicates that there is perfect resolution (or real-time) + /// + None = 0, + /// + /// Indicates that the accuracy of a timestamp is high. + /// + Fine = 1, + /// + /// Indicates that the accuracy of a timestamp is low. + /// + Coarse = 2, + } +} diff --git a/AdapterInterface/Contracts/TimestampSyncTypes.cs b/AdapterInterface/Contracts/TimestampSyncTypes.cs new file mode 100644 index 00000000..ac260f28 --- /dev/null +++ b/AdapterInterface/Contracts/TimestampSyncTypes.cs @@ -0,0 +1,35 @@ +using System; + +namespace Mtconnect.AdapterInterface.Contracts +{ + /// + /// Indicates the methodology used for ensuring the DateTime is accurate to standards. + /// + public enum TimestampSyncTypes + { + /// + /// The methodology for time synchronization is not known. + /// + Unknown = -1, + /// + /// Indicates that no time synchronization is used for a timestamp. + /// + None = 0, + /// + /// Network Time Protocol, uses a hierarchical architecture with a small number of highly accurate references clocks at the top and large number of less accurate clocks at the bottom. NTP clients use the NTP protocol to synchronize their clocks with the closest NTP server. + /// + NTP, + /// + /// Precision Time Protocol, designed specifically for industrial and scientific application where high precision time synchronization is required. PTP operates over Ethernet networks and uses a master-slave architecture to synchronize clocks. + /// + PTP, + /// + /// Global Positioning System is a satellite-based navigation system that can be used for time synchronization in some situations. GPS receivers use the timing signals from GPS satellites to synchronize their clocks. + /// + GPS, + /// + /// Deutsche Funkturm is a longwave time signal service transmitted from a radio transmitter located in Frankfurt, Germany. DCF77 can be used for time synchronization in Europe. + /// + DCF77 + } +} diff --git a/AdapterInterface/DataItems/Condition.Active.cs b/AdapterInterface/DataItems/Condition.Active.cs index e289f927..653a82e8 100644 --- a/AdapterInterface/DataItems/Condition.Active.cs +++ b/AdapterInterface/DataItems/Condition.Active.cs @@ -62,6 +62,7 @@ public Active(string name, Level level, string description = null, string text = Qualifier = qualifier; NativeSeverity = severity; HasNewLine = true; + LastChanged = TimeHelper.GetNow(); if (NativeCode.Length == 0 && (Level == Level.NORMAL || Level == Level.UNAVAILABLE)) mPlaceholder = true; @@ -87,7 +88,7 @@ public bool Set(Level level, string text = "", string qualifier = "", string sev Qualifier = qualifier; Text = text; NativeSeverity = severity; - LastChanged = DateTime.UtcNow; + LastChanged = TimeHelper.GetNow(); } mMarked = true; diff --git a/AdapterInterface/DataItems/Condition.cs b/AdapterInterface/DataItems/Condition.cs index b33dfc8b..744ffd0b 100644 --- a/AdapterInterface/DataItems/Condition.cs +++ b/AdapterInterface/DataItems/Condition.cs @@ -11,7 +11,6 @@ namespace Mtconnect.AdapterInterface.DataItems /// public partial class Condition : DataItem { - /// /// A flag to indicate that the mark and sweep has begun. /// @@ -40,7 +39,7 @@ public Condition(string name, string description = null, bool simple = false) : { HasNewLine = true; IsSimple = simple; - Add(Level.UNAVAILABLE); + Unavailable(); } /// @@ -132,37 +131,39 @@ public bool Add(Level level, string text = "", string code = "", string qualifie bool result = false; // Get the first activation - Active activation = null; + Active previousActivation = null; + Active newActivation = null; Func isFirstActivation = (o) => o == Level.NORMAL || o == Level.UNAVAILABLE; if (string.IsNullOrEmpty(code) && isFirstActivation(level)) { _activeList.Clear(); - activation = new Active(Name, level, Description, text, code, qualifier, severity); - _activeList.Add(string.Empty, activation); + newActivation = new Active(Name, level, Description, text, code, qualifier, severity); + _activeList.Add(string.Empty, newActivation); result = HasChanged = true; - } else if (!_activeList.TryGetValue(code, out activation)) + } else if (!_activeList.TryGetValue(code, out newActivation)) { if (_activeList.Count == 1 && isFirstActivation(_activeList.Values.FirstOrDefault().Level)) { _activeList.Clear(); } - activation = new Active(Name, level, Description, text, code, qualifier, severity); - _activeList.Add(code, activation); + newActivation = new Active(Name, level, Description, text, code, qualifier, severity); + _activeList.Add(code, newActivation); result = HasChanged = true; } if (_activeList.ContainsKey(code)) { - if (activation != null) + if (newActivation != null) { + previousActivation = newActivation; // Before we set the values for a potentially old Activation if (!result) { - result = activation.Set(level, text, qualifier, severity); + result = newActivation.Set(level, text, qualifier, severity); if (result) { - _activeList[code] = activation; + _activeList[code] = newActivation; } HasChanged = HasChanged || result; } @@ -172,6 +173,19 @@ public bool Add(Level level, string text = "", string code = "", string qualifie } } + if (result + && isReadyToUpdate(newActivation.Value) + && ((previousActivation?.Value == null && newActivation.Value != null) + || (previousActivation?.Value != null && newActivation.Value == null) + || (previousActivation?.Value.Equals(newActivation.Value) == false))) + { + var e = new DataItemChangedEventArgs(previousActivation?.Value, newActivation.Value, LastChanged, TimeHelper.GetNow()); + + LastChanged = newActivation.LastChanged; + + TriggerDataChangedEvent(e); + } + return result; } @@ -189,7 +203,7 @@ public bool Clear(string code) // If we've removed the last activation, go back to normal. if (_activeList.Count() == 1) { - Add(Level.NORMAL); + Normal(); } else { @@ -197,8 +211,8 @@ public bool Clear(string code) found.Set(Level.NORMAL); // Clear makes the activation be removed next sweep. found.Clear(); + LastChanged = found.LastChanged; } - HasChanged = true; return true; } diff --git a/AdapterInterface/DataItems/DataItem.cs b/AdapterInterface/DataItems/DataItem.cs index 2d75f367..bdf1d3c9 100644 --- a/AdapterInterface/DataItems/DataItem.cs +++ b/AdapterInterface/DataItems/DataItem.cs @@ -13,7 +13,7 @@ namespace Mtconnect.AdapterInterface.DataItems /// /// Simple base data item class. Has an abstract value and a name. It keeps track if it has changed since the last time it was reset. /// - public class DataItem + public abstract class DataItem { /// /// Occurrs when the value of a DataItem has changed. @@ -49,29 +49,26 @@ public object Value var updatedValue = value; if (FormatValue != null) updatedValue = FormatValue(updatedValue); - if (_isReadyToUpdate(updatedValue) + if (isReadyToUpdate(updatedValue) && ((_value == null && updatedValue != null) || (_value != null && updatedValue == null) || _value?.Equals(updatedValue) == false)) { - var now = DateTime.UtcNow; + var now = TimeHelper.GetNow(); var e = new DataItemChangedEventArgs(_value, updatedValue, LastChanged, now); _value = updatedValue; - LastChanged = now; + if (!HasTimestampOverride) LastChanged = now; HasChanged = true; - if (OnDataItemChanged != null) - { - OnDataItemChanged(this, e); - } + TriggerDataChangedEvent(e); } } get { return _value; } } - private bool _isReadyToUpdate(object value) + protected virtual bool isReadyToUpdate(object value) => _value?.Equals(Constants.UNAVAILABLE) == true ? (value is string ? !string.IsNullOrEmpty(value as string) : value != null) : value != null; @@ -79,7 +76,7 @@ private bool _isReadyToUpdate(object value) /// /// Timestamp of when the was last Changed. /// - public DateTime? LastChanged { get; protected set; } + public DateTime? LastChanged { get; set; } /// /// A flag to indicate if the data item's value has changed since it @@ -94,6 +91,8 @@ private bool _isReadyToUpdate(object value) /// public bool HasNewLine { get; protected set; } + public bool HasTimestampOverride { get; internal set; } + /// /// An expression that can be used to apply additional formatting or transformations to the DataItem value. /// @@ -119,7 +118,7 @@ public DataItem(string name, string description = null) /// Checks if the data item is unavailable. /// /// true if Unavailable - public bool IsUnavailable() => _value?.Equals(Constants.UNAVAILABLE) == true; + public bool IsUnavailable() => Value?.Equals(Constants.UNAVAILABLE) == true; /// /// Forces this to indicate that the has changed. @@ -127,6 +126,21 @@ public DataItem(string name, string description = null) public void ForceChanged() { HasChanged = true; + if (!HasTimestampOverride) LastChanged = TimeHelper.GetNow(); + } + + public override bool Equals(object obj) + { + if (obj is DataItem) + { + return (obj as DataItem).Value?.Equals(Value) == true; + } else if (obj is string) + { + if (!(Value is string)) return false; + return obj?.Equals(Value) == true; + } + + return base.Equals(obj); } /// @@ -172,5 +186,10 @@ public virtual List ItemList(bool all = false) list.Add(this); return list; } + + protected void TriggerDataChangedEvent(DataItemChangedEventArgs e) + { + OnDataItemChanged?.Invoke(this, e); + } } } diff --git a/AdapterInterface/DataItems/TimeSeries.cs b/AdapterInterface/DataItems/TimeSeries.cs index 511104d5..2e48b619 100644 --- a/AdapterInterface/DataItems/TimeSeries.cs +++ b/AdapterInterface/DataItems/TimeSeries.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; using System.Linq; namespace Mtconnect.AdapterInterface.DataItems @@ -20,8 +21,20 @@ public class TimeSeries : DataItem public double[] Values { set { + var updatedValue = value; + if (FormatValue != null) + { + var formattedValue = FormatValue(updatedValue); + if (formattedValue != null && (formattedValue as double[]) == null) + { + throw new InvalidCastException("Cannot cast object to double[]"); + } + updatedValue = formattedValue as double[]; + } + _values = value; - HasChanged = true; + + Value = String.Join(" ", Values.Select(p => p.ToString()).ToArray()); } get { return _values; } } @@ -38,6 +51,32 @@ public TimeSeries(string name, string description = null, double rate = 0.0) : b Rate = rate; } + public override bool Equals(object obj) + { + double[] doubles = null; + if (obj is double[]) + { + doubles = obj as double[]; + } else if (obj is TimeSeries) + { + doubles = (obj as TimeSeries).Values; + } else if (obj is string) + { + return obj.Equals(_value); + } else { + return false; + } + + if (doubles.Length != Values.Length) return false; + + for (int i = 0; i < doubles.Length; i++) + { + if (doubles[i] != Values[i]) return false; + } + + return true; + } + /// /// Simple string representation with pipe delim. /// @@ -45,16 +84,8 @@ public TimeSeries(string name, string description = null, double rate = 0.0) : b public override string ToString() { string rate = Rate == 0.0 ? "" : Rate.ToString(); - string v = string.Empty; - int count = 0; - - if (_values != null) - { - v = String.Join(" ", Values.Select(p => p.ToString()).ToArray()); - count = Values.Length; - } - return $"{Name}|{count}|{rate}|{v}"; + return $"{Name}|{Values?.Length ?? 0}|{rate}|{_value}"; } } } diff --git a/AdapterInterface/DataReceivedEventArgs.cs b/AdapterInterface/DataReceivedEventArgs.cs index 364f2687..10e71012 100644 --- a/AdapterInterface/DataReceivedEventArgs.cs +++ b/AdapterInterface/DataReceivedEventArgs.cs @@ -10,6 +10,6 @@ public class DataReceivedEventArgs : EventArgs /// /// Reference to the the data was received. /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; + public DateTime Timestamp { get; set; } = TimeHelper.GetNow(); } } diff --git a/AdapterInterface/TimeHelper.cs b/AdapterInterface/TimeHelper.cs new file mode 100644 index 00000000..4fae030b --- /dev/null +++ b/AdapterInterface/TimeHelper.cs @@ -0,0 +1,9 @@ +using System; + +namespace Mtconnect +{ + public static class TimeHelper + { + public static DateTime GetNow() => DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/MtconnectCore.TcpAdapter/TcpAdapter.cs b/MtconnectCore.TcpAdapter/TcpAdapter.cs index 6077ad4b..421528c2 100644 --- a/MtconnectCore.TcpAdapter/TcpAdapter.cs +++ b/MtconnectCore.TcpAdapter/TcpAdapter.cs @@ -205,6 +205,37 @@ private void ListenForClients() client.OnDisconnected += Client_OnConnectionDisconnected; client.OnDataReceived += Client_OnReceivedData; client.Connect(); + + // Send all commands that do not result in errors + Func[] agentCommands = new Func[] + { + AgentCommands.AdapterVersion, + AgentCommands.Calibration, + AgentCommands.ConversionRequired, + AgentCommands.Device, + AgentCommands.Description, + AgentCommands.Manufacturer, + AgentCommands.MtconnectVersion, + AgentCommands.NativeName, + AgentCommands.RealTime, + AgentCommands.RelativeTime, + AgentCommands.SerialNumber, + AgentCommands.ShdrVersion, + AgentCommands.Station + }; + foreach (var agentCommand in agentCommands) + { + try + { + string command = agentCommand(); + Write($"{command}\n", client.ClientId); + } + catch (Exception ex) + { + Write($"{AgentCommands.Error("Unsupported command '" + agentCommand.Method.Name + "'")}\n", client.ClientId); + } + } + // Issue command for underlying Adapter to send all DataItem current values to the newly added kvp Send(DataItemSendTypes.All, client.ClientId); } else @@ -228,81 +259,16 @@ private void ListenForClients() } } - private const string PING = "* PING"; - private const string GET_DATAITEMS = "* DATAITEMS"; - private const string GET_DATAITEM_DESCRIPTION = "* DATAITEM_DESCRIPTION"; - private const string GET_DATAITEM_VALUE = "* DATAITEM_VALUE"; /// /// ReceiveClient data from a kvp and implement heartbeat ping/pong protocol. /// private bool Client_OnReceivedData(TcpConnection connection, string message) { - message = message?.Trim(); - bool heartbeat = false; - if (message.StartsWith(PING) && Heartbeat > 0) - { - heartbeat = true; - lock (connection) - { - _logger?.LogInformation("Received PING from client {ClientId}, sending PONG", connection.ClientId); - connection.Write(PONG); - connection.Flush(); - } - } else if (message.Equals(GET_DATAITEMS)) - { - lock (connection) - { - _logger?.LogInformation("Received GET DATAITEMS from client {ClientId}, sending list of supported DataItems", connection.ClientId); - var keys = DataItems?.Select(o => o.Name)?.DefaultIfEmpty().ToArray(); - connection.Write(string.Join("|", keys)); - connection.Flush(); - } - } else if (message.StartsWith(GET_DATAITEM_DESCRIPTION)) - { - string dataItemName = message.Remove(0, message.LastIndexOf(' ') + 1); - if (Contains(dataItemName)) - { - if (!string.IsNullOrEmpty(this[dataItemName].Description)) - { - lock (connection) - { - _logger?.LogInformation("Received GET DATAITEM_DESCRIPTION from client {ClientId} for {DataItemName}, sending the description (if any)", connection.ClientId, dataItemName); - connection.Write(this[dataItemName].Description); - connection.Flush(); - } - } else - { - _logger?.LogWarning("Received GET DATAITEM_DESCRIPTION from client {ClientId} for {DataItemName}, but there is no description available", connection.ClientId, dataItemName); - } - } else - { - _logger?.LogWarning("Received GET DATAITEM_DESCRIPTION from client {ClientId} for {DataItemName}, but there is no such DataItem available", connection.ClientId, dataItemName); - } - } else if (message.StartsWith(GET_DATAITEM_VALUE)) - { - string dataItemName = message.Remove(0, message.LastIndexOf(' ') + 1); - if (Contains(dataItemName)) - { - lock (connection) - { - _logger?.LogInformation("Received GET DATAITEM_VALUE from client {ClientId} for {DataItemName}, sending the current value (if any)", connection.ClientId, dataItemName); - //string response = this[dataItemName].LastChanged.HasValue - // ? this[dataItemName].LastChanged.Value.ToString(DATE_TIME_FORMAT) - // : "never"; - //response += $"|{dataItemName}|{this[dataItemName].Value}"; - connection.Write(this[dataItemName].ToString()); - connection.Flush(); - } - } - else - { - _logger?.LogWarning("Received GET DATAITEM_VALUE from client {ClientId} for {DataItemName}, but there is no such DataItem available", connection.ClientId, dataItemName); - } - } + bool handled = HandleCommand(message, connection.ClientId); if (ClientDataReceived != null) ClientDataReceived(connection, message); - return heartbeat; + return handled; } private void Client_OnConnectionDisconnected(TcpConnection connection, Exception ex = null) diff --git a/MtconnectCore.TcpAdapter/TcpAdapter.csproj b/MtconnectCore.TcpAdapter/TcpAdapter.csproj index 9bd5ea3e..846b2ff0 100644 --- a/MtconnectCore.TcpAdapter/TcpAdapter.csproj +++ b/MtconnectCore.TcpAdapter/TcpAdapter.csproj @@ -19,7 +19,7 @@ git Mtconnect;Adapter;TCP;TAMS; Added extra logging and refined TcpConnections. - 1.0.12 + 1.0.13 diff --git a/SampleAdapter/PCModel.cs b/SampleAdapter/PCModel.cs index b480e5a0..f766e02e 100644 --- a/SampleAdapter/PCModel.cs +++ b/SampleAdapter/PCModel.cs @@ -43,11 +43,13 @@ private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) if (WindowHandles.GetCursorPos(out lpPoint)) { Model.XPosition.Value = lpPoint.X; + Model.XPosition_Time = new DateTime(2002, 01, 01); // Birthdate of C# Model.YPosition = lpPoint.Y; } else { Model.XPosition = null; + Model.XPosition_Time = null; Model.YPosition = null; } @@ -138,6 +140,8 @@ public class PCStatus : IAdapterDataModel [Sample("xPos", "user32.dll#GetCursorPos().X")] public Sample? XPosition { get; set; } = new Sample("xPos"); + [Timestamp("xPos")] + public DateTime? XPosition_Time { get; set; } [Sample("yPos", "user32.dll#GetCursorPos().Y")] public int? YPosition { get; set; } diff --git a/SampleAdapter/SampleAdapter.csproj b/SampleAdapter/SampleAdapter.csproj index 3ac153f5..55f7655f 100644 --- a/SampleAdapter/SampleAdapter.csproj +++ b/SampleAdapter/SampleAdapter.csproj @@ -5,6 +5,7 @@ net6.0 enable enable + 1.0.0.1