Skip to content

Commit

Permalink
TCP Connection Refactoring (#2)
Browse files Browse the repository at this point in the history
* Added TCP Connection class

* Added extra logs

* Additional logging and refactoring

 - Added cancellation token to starting adapters
 - Refactored code that adds DataItems to the Adapter into an extension class
 - Created `IAdapterDataModel` to help differentiate data models from Adapter service classes (see example)
 - Updated signature for `DataReceivedHandler` to send a type of `IAdapterDataModel`
   - The `Adapter` now consumes this type explicitly for tracking DataItems
 - Updated asynchronous programming in TCP adapter to use tasks instead of deprecated Thread programming
 - Updated `TcpAdapter` to manage client connection with custom class `TcpConnection`.
  • Loading branch information
tbm0115 authored Jan 18, 2023
1 parent 46f84fc commit edfb620
Show file tree
Hide file tree
Showing 13 changed files with 587 additions and 341 deletions.
128 changes: 33 additions & 95 deletions AdapterInterface/Adapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Mtconnect.AdapterInterface.Contracts.Attributes;
using System.Diagnostics.Tracing;
using EventAttribute = Mtconnect.AdapterInterface.Contracts.Attributes.EventAttribute;
using System.Threading;

namespace Mtconnect
{
Expand Down Expand Up @@ -124,11 +125,23 @@ public virtual double Heartbeat
/// Generic constructor of a new Adapter instance with basic AdapterOptions.
/// </summary>
/// <param name="options"><inheritdoc cref="AdapterOptions" path="/summary"/></param>
public Adapter(AdapterOptions options)
/// <param name="loggerFactory">Reference to the logger factory to handle logging.</param>
public Adapter(AdapterOptions options, ILogger<Adapter> logger = null)
{
_options = options;
Heartbeat = options.Heartbeat;
CanEnqueueDataItems = options.CanEnqueueDataItems;

_logger = logger;
}

public bool Contains(string dataItemName)
{
return _dataItems.ContainsKey(dataItemName);
}
public bool Contains(DataItem dataItem)
{
return Contains(dataItem.Name);
}

/// <summary>
Expand Down Expand Up @@ -156,6 +169,8 @@ public void AddDataItem(DataItem dataItem)
_dataItems[internalName].FormatValue = options?.Formatter;
if (!string.IsNullOrEmpty(options?.DataItemName) && options?.DataItemName != internalName)
_dataItems[internalName].Name = options?.DataItemName;

_logger?.LogTrace("Added DataItem {DataItemName}", _dataItems[internalName].Name);
}

private void Adapter_OnDataItemChanged(DataItem sender, DataItemChangedEventArgs e)
Expand Down Expand Up @@ -206,6 +221,7 @@ public void RemoveDataItem(DataItem dataItem)
/// </summary>
public void Unavailable()
{
_logger?.LogTrace("Setting all DataItem values to UNAVAILABLE");
foreach (var kvp in _dataItems)
kvp.Value.Unavailable();
}
Expand Down Expand Up @@ -236,6 +252,8 @@ public void Begin()
/// <param name="sendType">Flag for identifying which <see cref="ReportedValue"/>s to send.</param>
public virtual void Send(DataItemSendTypes sendType = DataItemSendTypes.Changed, string clientId = null)
{
_logger?.LogTrace("Sending {DataItemSendType} values", sendType.ToString());

var values = new List<ReportedValue>();
switch (sendType)
{
Expand Down Expand Up @@ -271,6 +289,8 @@ public virtual void Send(DataItemSendTypes sendType = DataItemSendTypes.Changed,
/// <param name="values">Collection of <see cref="ReportedValue"/>s to send values.</param>
protected void Send(IEnumerable<ReportedValue> values, string clientId = null)
{
_logger?.LogTrace($"Sending {values.Count()} values");

var orderedValues = values.OrderBy(o => o.Timestamp).ToList();
var individualValues = values.Where(o => o.HasNewLine).ToList();
var multiplicityValues = orderedValues.Except(individualValues).ToList();
Expand Down Expand Up @@ -319,17 +339,17 @@ public virtual void AddAsset(Asset asset)
Write(sb.ToString());
}

/// <summary>
/// The heartbeat thread for a client. This thread receives data from a client, closes the socket when it fails, and handles communication timeouts when the client does not send a heartbeat within 2x the heartbeat frequency. When the heartbeat is not received, the client is assumed to be unresponsive and the connection is closed. Waits for one ping to be received before enforcing the timeout.
/// </summary>
/// <param name="client">The client we are communicating with.</param>
protected abstract void HeartbeatClient(object client);
///// <summary>
///// The heartbeat thread for a client. This thread receives data from a client, closes the socket when it fails, and handles communication timeouts when the client does not send a heartbeat within 2x the heartbeat frequency. When the heartbeat is not received, the client is assumed to be unresponsive and the connection is closed. Waits for one ping to be received before enforcing the timeout.
///// </summary>
///// <param name="client">The client we are communicating with.</param>
//protected abstract void HeartbeatClient(object client);

/// <summary>
/// Start the listener thread.
/// </summary>
/// <param name="begin">Flag for whether or not to also call <see cref="Begin"/>.</param>
public abstract void Start(bool begin = true);
public abstract void Start(bool begin = true, CancellationToken token = default);

/// <summary>
/// Starts the listener thread and the provided <see cref="IAdapterSource"/>.
Expand All @@ -350,100 +370,17 @@ public void Start(IEnumerable<IAdapterSource> sources, bool begin = true)

foreach (var source in sources)
{
AddSource(source);
_sources.Add(source);
source.OnDataReceived += _source_OnDataReceived;
source.Start();
}
}

private void AddSource<T>(T source) where T : class, IAdapterSource
{
_sources.Add(source);
source.OnDataReceived += _source_OnDataReceived;

Type sourceType = source.GetType();
AddDataItemsFromSource(sourceType);
}

/// <summary>
/// Adds Data Items from properties in the <paramref name="sourceType"/> that are decorated with the <see cref="DataItemAttribute"/>.
/// </summary>
/// <param name="sourceType">Reference to the type to perform reflection on.</param>
/// <param name="dataItemPrefix">A recursive prefix for the Data Item names.</param>
private void AddDataItemsFromSource(Type sourceType, string dataItemPrefix = "")
private void _source_OnDataReceived(IAdapterDataModel data, DataReceivedEventArgs e)
{
// TODO: Cache the property map
var properties = sourceType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToArray();
var dataItemProperties = properties
.Where(o => o.GetCustomAttribute(typeof(DataItemAttribute)) != null)
.ToArray();
foreach (var property in dataItemProperties)
if (this.TryAddDataItems(data) && this.TryUpdateValues(data))
{
var dataItemAttribute = property.GetCustomAttribute<DataItemAttribute>();
string dataItemName = dataItemPrefix + dataItemAttribute.Name;

switch (dataItemAttribute)
{
case DataItemPartialAttribute _:
AddDataItemsFromSource(property.PropertyType, dataItemName);
break;
case EventAttribute _:
AddDataItem(new Event(dataItemName));
break;
case SampleAttribute _:
AddDataItem(new Sample(dataItemName));
break;
case ConditionAttribute _:
AddDataItem(new Condition(dataItemName));
break;
case TimeSeriesAttribute _:
AddDataItem(new TimeSeries(dataItemName));
break;
case MessageAttribute _:
AddDataItem(new Message(dataItemName));
break;
default:
break;
}
}
}

private void _source_OnDataReceived(object sender, DataReceivedEventArgs e)
{
updateValuesFromSource(sender);

Send(DataItemSendTypes.Changed);
}

/// <summary>
/// A recursive method for reflecting on the properties of the <paramref name="source"/>.
/// </summary>
/// <param name="source">Reference to the source object. This value will change throughout the recursion.</param>
/// <param name="dataItemPrefix">As the Data Item Prefix</param>
private void updateValuesFromSource(object source, string dataItemPrefix = "")
{
// TODO: Cache the property map
Type sourceType = source.GetType();
var dataItemProperties = sourceType.GetProperties().Where(o => o.GetCustomAttribute(typeof(DataItemAttribute)) != null);
foreach (var property in dataItemProperties)
{
var dataItemAttribute = property.GetCustomAttribute<DataItemAttribute>();
string dataItemName = dataItemPrefix + dataItemAttribute.Name;
switch (dataItemAttribute)
{
case DataItemPartialAttribute _:
updateValuesFromSource(property.GetValue(source), dataItemName);
break;
case EventAttribute _:
case SampleAttribute _:
case ConditionAttribute _:
case TimeSeriesAttribute _:
case MessageAttribute _:
this[dataItemName].Value = property.GetValue(source);
break;
default:
break;
}
Send(DataItemSendTypes.Changed);
}
}

Expand All @@ -455,6 +392,7 @@ public virtual void Stop()
foreach (var source in _sources)
{
source.Stop();
source.OnDataReceived -= _source_OnDataReceived;
}
}
}
Expand Down
139 changes: 139 additions & 0 deletions AdapterInterface/AdapterExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using Mtconnect.AdapterInterface.Contracts.Attributes;
using Mtconnect.AdapterInterface.DataItems;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Mtconnect
{
public static class AdapterExtensions
{
/// <summary>
/// Attempts to add a <paramref name="dataItem"/> to the <paramref name="adapter"/> if the adapter does not already contain such a <see cref="DataItem"/>.
/// </summary>
/// <param name="adapter">Reference to the adapter to add the data item onto.</param>
/// <param name="dataItem">Reference to the data item to be added.</param>
/// <returns>Flag for whether or not the data item has been added. Returns true if the data item has already been added.</returns>
public static bool TryAddDataItem(this Adapter adapter, DataItem dataItem)
{
if (adapter.Contains(dataItem)) return true;

adapter.AddDataItem(dataItem);

return true;
}

/// <summary>
/// Adds Data Items from properties in the <paramref name="model"/> that are decorated with the <see cref="DataItemAttribute"/>.
/// </summary>
/// <param name="adapter">Reference to the MTConnect <see cref="Adapter"/> to add the data items onto.</param>
/// <param name="model">Reference to a data model containing <see cref="DataItemAttribute"/>s.</param>
/// <returns>Flag for whether or not all decorated <see cref="DataItemAttribute"/>s were added to the adapter.</returns>
public static bool TryAddDataItems(this Adapter adapter, IAdapterDataModel model)
{
return adapter.TryAddDataItems(model.GetType());
}
private static PropertyInfo[] GetDataItemProperties(Type type)
{
return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(o => o.GetCustomAttribute(typeof(DataItemAttribute)) != null)
.ToArray();
}
private static Dictionary<Type, PropertyInfo[]> _dataItemProperties = new Dictionary<Type, PropertyInfo[]>();
private static bool TryAddDataItems(this Adapter adapter, Type dataModelType, string dataItemNamePrefix = "")
{
if (_dataItemProperties.TryGetValue(dataModelType, out PropertyInfo[] dataItemProperties)) return true;

dataItemProperties = GetDataItemProperties(dataModelType);
_dataItemProperties.Add(dataModelType, dataItemProperties);
bool allDataItemsAdded = true;


foreach (var property in dataItemProperties)
{
bool dataItemAdded = false;
var dataItemAttribute = property.GetCustomAttribute<DataItemAttribute>();
string dataItemName = dataItemNamePrefix + dataItemAttribute.Name;

switch (dataItemAttribute)
{
case DataItemPartialAttribute _:
dataItemAdded = adapter.TryAddDataItems(property.PropertyType, dataItemName);
break;
case EventAttribute _:
dataItemAdded = adapter.TryAddDataItem(new Event(dataItemName));
break;
case SampleAttribute _:
dataItemAdded = adapter.TryAddDataItem(new Sample(dataItemName));
break;
case ConditionAttribute _:
dataItemAdded = adapter.TryAddDataItem(new Condition(dataItemName));
break;
case TimeSeriesAttribute _:
dataItemAdded = adapter.TryAddDataItem(new TimeSeries(dataItemName));
break;
case MessageAttribute _:
dataItemAdded = adapter.TryAddDataItem(new Message(dataItemName));
break;
default:
dataItemAdded = false;
break;
}

if (!dataItemAdded) allDataItemsAdded = false;
}

return allDataItemsAdded;
}

/// <summary>
/// Attempts to update the <see cref="DataItem"/>s of the <paramref name="adapter"/>.
/// </summary>
/// <param name="adapter">Reference to the MTConnect <see cref="Adapter"/> to update the data items from.</param>
/// <param name="model">Reference to the data model to update the <paramref name="adapter"/>s <see cref="DataItem"/>s from.</param>
/// <returns>Flag for whether or not all <see cref="DataItem"/> values were updated from the <paramref name="model"/>.</returns>
public static bool TryUpdateValues(this Adapter adapter, IAdapterDataModel model)
{
return TryUpdateValues(adapter, model, string.Empty);
}
private static bool TryUpdateValues(this Adapter adapter, object model, string dataItemPrefix)
{
bool allDataItemsUpdated = true;

Type sourceType = model.GetType();

if (!_dataItemProperties.TryGetValue(sourceType, out PropertyInfo[] dataItemProperties))
{
dataItemProperties = GetDataItemProperties(sourceType);
}
foreach (var property in dataItemProperties)
{
bool dataItemUpdated = true;

var dataItemAttribute = property.GetCustomAttribute<DataItemAttribute>();
string dataItemName = dataItemPrefix + dataItemAttribute.Name;
switch (dataItemAttribute)
{
case DataItemPartialAttribute _:
dataItemUpdated = adapter.TryUpdateValues(property.GetValue(model), dataItemName);
break;
case EventAttribute _:
case SampleAttribute _:
case ConditionAttribute _:
case TimeSeriesAttribute _:
case MessageAttribute _:
adapter[dataItemName].Value = property.GetValue(model);
break;
default:
dataItemUpdated = false;
break;
}

if (!dataItemUpdated) allDataItemsUpdated = false;
}

return allDataItemsUpdated;
}
}
}
4 changes: 2 additions & 2 deletions AdapterInterface/AdapterInterface.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
<PackageIcon>icon.jpg</PackageIcon>
<RepositoryType>git</RepositoryType>
<PackageTags>MTConnect;Adapter;Interface</PackageTags>
<PackageReleaseNotes>Introduction of encryption of scripts within the App.config. These scripts can be used to transform the Data Item values.</PackageReleaseNotes>
<PackageReleaseNotes>Added extra logging and refactored DataItem modeling.</PackageReleaseNotes>
<ApplicationIcon>icon.ico</ApplicationIcon>
<PackageProjectUrl>https://github.com/TrueAnalyticsSolutions/MtconnectCore.Adapter</PackageProjectUrl>
<RepositoryUrl>$(ProjectUrl)</RepositoryUrl>
<Version>1.0.7-alpha</Version>
<Version>1.0.8-alpha</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
11 changes: 11 additions & 0 deletions AdapterInterface/IAdapterDataModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Threading;

namespace Mtconnect
{
/// <summary>
/// A generic interface for instantiating a source for a MTConnect Adapter.
/// </summary>
public interface IAdapterDataModel
{
}
}
Loading

0 comments on commit edfb620

Please sign in to comment.