Skip to content

Commit

Permalink
Implemented Adapter and Agent commands (#13)
Browse files Browse the repository at this point in the history
* Implemented Adapter and Agent commands

 - Refactored accepted TCP commands into static classes

* Timestamp Updates

 - Update to TimeSeries DataItem to try and fix it's implementation
 - Added `TimestampAttribute` to try and help address potential inaccuracies of Timestamps sourced from the Adapter. Timestamps can now be supplied by the implementation. For example, if the Timestamp is provided by the `IAdapterSource`
   - Future discussions should be had about the potential for compensated Timestamps due to various Timestamp Drifts (now can be indicated with the `TimestampAttribute`)
 - Version number updates
 - Converted use of `DateTime.UtcNow` to use a static helper method that may be an option to override in the future (see Timestamp Drifts)
  • Loading branch information
tbm0115 authored Feb 7, 2023
1 parent 0ffc760 commit 7d58ff7
Show file tree
Hide file tree
Showing 21 changed files with 628 additions and 118 deletions.
25 changes: 23 additions & 2 deletions AdapterInterface/Adapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Data;
using System.Collections.Concurrent;
using System.Threading;
using System.Net.NetworkInformation;

namespace Mtconnect
{
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -414,5 +415,25 @@ public virtual void Stop()
source.OnDataReceived -= _source_OnDataReceived;
}
}

public string[] GetDataItemNames() => DataItems?.Select(o => o.Name)?.DefaultIfEmpty().ToArray();

/// <summary>
/// Handle an incoming command from a client.
/// </summary>
/// <param name="command">Reference to the incoming message command.</param>
/// <param name="clientId">Reference to the client that sent the command</param>
/// <returns></returns>
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;
}
}
}
94 changes: 94 additions & 0 deletions AdapterInterface/AdapterCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Net.NetworkInformation;
using System.Reflection;

namespace Mtconnect
{
/// <summary>
/// A collection of methods that return formatted responses to commands issued from a MTConnect Agent.
/// </summary>
public static class AdapterCommands
{
/// <summary>
/// Determines the appropriate response to the provided <paramref name="message"/>.
/// </summary>
/// <param name="adapter">Reference to the Adapter implementation.</param>
/// <param name="message">Reference to the command issued from the MTConnect Agent.</param>
/// <returns>Formatted response to the command issued by the MTConnect Agent. If the command was not recognized, then the response string is empty.</returns>
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;
}

/// <summary>
/// Handles the "<c>* PONG &lt;heartbeat&gt;</c>" command to the MTConnect Agent
/// </summary>
/// <returns>Informs the agent of all dataItems that can be published by the adapter</returns>
public static string Ping(Adapter adapter)
=> AgentCommands.Pong(adapter.Heartbeat);

/// <summary>
/// Handles the "<c>* dataItems: XXX</c>" command to the MTConnect Agent
/// </summary>
/// <returns>Informs the agent of all dataItems that can be published by the adapter</returns>
public static string DataItems(Adapter adapter)
=> $"* dataItems: {string.Join("|", adapter.GetDataItemNames())}";

/// <summary>
/// Handles the "<c>* dataItemDescription: XXX</c>" command to the MTConnect Agent
/// </summary>
/// <returns>Informs the agent of the DataItem description, if any, for the provided DataItem name</returns>
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}";
}

/// <summary>
/// Handles the "<c>* dataItem: XXX</c>" command to the MTConnect Agent
/// </summary>
/// <returns>Informs the agent of the current value for the provided DataItem</returns>
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]}";
}
}
}
93 changes: 85 additions & 8 deletions AdapterInterface/AdapterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public static bool TryAddDataItems(this Adapter adapter, IAdapterDataModel model
{
return adapter.TryAddDataItems(model, string.Empty, string.Empty);
}
/// <summary>
/// Gets a list of <see cref="PropertyInfo"/> that is either decorated with the <see cref="DataItemAttribute"/> or an implementation of the type <see cref="DataItem"/>.
/// </summary>
/// <param name="type">Reference to the model type to reflect upon.</param>
/// <returns>Collection of properties that represent <see cref="DataItem"/>s</returns>
private static PropertyInfo[] GetDataItemProperties(Type type)
{
return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
Expand All @@ -49,6 +54,18 @@ private static PropertyInfo[] GetDataItemProperties(Type type)
)
.ToArray();
}
/// <summary>
/// Gets a list of <see cref="PropertyInfo"/> that is decorated with the <see cref="TimestampAttribute"/>.
/// </summary>
/// <param name="type">Reference to the model type to reflect upon.</param>
/// <returns>Collection of properties that represent the value for <see cref="DataItem.LastChanged"/></returns>
private static PropertyInfo[] GetDataItemTimestampProperties(Type type)
{
return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(o => o.GetCustomAttribute(typeof(TimestampAttribute)) != null)
.ToArray();
}
private static ConcurrentDictionary<Type, Dictionary<string, PropertyInfo>> _dataItemTimestampProperties = new ConcurrentDictionary<Type, Dictionary<string, PropertyInfo>>();
private static ConcurrentDictionary<Type, PropertyInfo[]> _dataItemProperties = new ConcurrentDictionary<Type, PropertyInfo[]>();
private static HashSet<Type> _dataItemTypes = new HashSet<Type>
{
Expand All @@ -65,11 +82,33 @@ private static bool TryAddDataItems(this Adapter adapter, object model, string d
lock (_dataItemProperties)
{
Type dataModelType = model.GetType();

Dictionary<string, PropertyInfo> 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<string, PropertyInfo>());

var timestampProperties = GetDataItemTimestampProperties(dataModelType);
foreach (var timestampProperty in timestampProperties)
{
TimestampAttribute attr = timestampProperty.GetCustomAttribute<TimestampAttribute>();
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;
}

Expand All @@ -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))
{
Expand All @@ -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)
{
Expand All @@ -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;
Expand All @@ -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)
{
Expand Down Expand Up @@ -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<string, PropertyInfo> 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;
}
}
Expand Down
2 changes: 1 addition & 1 deletion AdapterInterface/AdapterInterface.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<ApplicationIcon>icon.ico</ApplicationIcon>
<PackageProjectUrl>https://github.com/TrueAnalyticsSolutions/MtconnectCore.Adapter</PackageProjectUrl>
<RepositoryUrl>$(ProjectUrl)</RepositoryUrl>
<Version>1.0.12</Version>
<Version>1.0.13</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit 7d58ff7

Please sign in to comment.