using Hylasoft.Opc.Common; using Opc; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using Factory = OpcCom.Factory; using OpcDa = Opc.Da; namespace Hylasoft.Opc.Da { /// <summary> /// Client Implementation for DA /// </summary> public class DaClient : IOpcClient { #region # Fields and Constructors /// <summary> /// Default monitor interval in Milliseconds /// </summary> private const int DefaultMonitorInterval = 100; /// <summary> /// OPC URL /// </summary> private readonly URL _url; /// <summary> /// OpcDa underlying server object /// </summary> private OpcDa.Server _server; /// <summary> /// Subscription auto identifier /// </summary> private long _sub; /// <summary> /// Initialize a new Data Access Client /// </summary> /// <param name="serverUrl">The url of the server to connect to. WARNING: If server URL includes /// spaces (ex. "RSLinx OPC Server") then pass the server URL in to the constructor as an Opc.URL object /// directly instead.</param> public DaClient(Uri serverUrl) { this._url = new URL(serverUrl.AbsolutePath) { Scheme = serverUrl.Scheme, HostName = serverUrl.Host }; } /// <summary> /// Initialize a new Data Access Client /// </summary> /// <param name="serverUrl">The url of the server to connect to</param> public DaClient(URL serverUrl) { this._url = serverUrl; } #endregion #region # Properties #region Monitor interval in Milliseconds —— int? MonitorInterval /// <summary> /// Monitor interval in Milliseconds /// </summary> public int? MonitorInterval { get; set; } #endregion #region Gets the current status of the OPC Client —— OpcStatus Status /// <summary> /// Gets the current status of the OPC Client /// </summary> public OpcStatus Status { get; private set; } #endregion #region OpcDa underlying server object —— OpcDa.Server Server /// <summary> /// OpcDa underlying server object /// </summary> public OpcDa.Server Server { get { return this._server; } } #endregion #endregion #region # Methods //Implements #region Connect the client to the OPC Server —— void Connect() /// <summary> /// Connect the client to the OPC Server /// </summary> public void Connect() { if (this.Status == OpcStatus.Connected) { return; } this._server = new OpcDa.Server(new Factory(), this._url); this._server.Connect(); this.Status = OpcStatus.Connected; } #endregion #region Gets the datatype of an OPC tag —— Type GetDataType(string tag) /// <summary> /// Gets the datatype of an OPC tag /// </summary> /// <param name="tag">Tag to get datatype of</param> /// <returns>System Type</returns> public System.Type GetDataType(string tag) { OpcDa.Item item = new OpcDa.Item { ItemName = tag }; OpcDa.ItemProperty result; try { OpcDa.ItemPropertyCollection propertyCollection = this._server.GetProperties(new ItemIdentifier[] { item }, new[] { new OpcDa.PropertyID(1) }, false)[0]; result = propertyCollection[0]; } catch (NullReferenceException) { throw new OpcException("Could not find node because server not connected."); } return result.DataType; } #endregion #region Read a tag —— ReadEvent Read(string tag) /// <summary> /// Read a tag /// </summary> /// <param name="tag">The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. /// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo`</param> /// <returns>The value retrieved from the OPC</returns> public ReadEvent Read(string tag) { OpcDa.Item item = new OpcDa.Item { ItemName = tag }; if (this.Status == OpcStatus.NotConnected) { throw new OpcException("Server not connected. Cannot read tag."); } OpcDa.ItemValueResult result = this._server.Read(new[] { item })[0]; ReadEvent readEvent = new ReadEvent(); readEvent.Value = result.Value; readEvent.SourceTimestamp = result.Timestamp; readEvent.ServerTimestamp = result.Timestamp; if (result.Quality == OpcDa.Quality.Good) readEvent.Quality = Quality.Good; if (result.Quality == OpcDa.Quality.Bad) readEvent.Quality = Quality.Bad; return readEvent; } #endregion #region Read a tag —— ReadEvent<T> Read<T>(string tag) /// <summary> /// Read a tag /// </summary> /// <typeparam name="T">The type of tag to read</typeparam> /// <param name="tag">The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. /// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo`</param> /// <returns>The value retrieved from the OPC</returns> public ReadEvent<T> Read<T>(string tag) { ReadEvent readEvent = this.Read(tag); ReadEvent<T> readEventGeneric = new ReadEvent<T> { Value = this.TryCastResult<T>(readEvent.Value), Quality = readEvent.Quality, ServerTimestamp = readEvent.ServerTimestamp, SourceTimestamp = readEvent.SourceTimestamp }; return readEventGeneric; } #endregion #region Read a tag asynchronusly —— Task<ReadEvent<T>> ReadAsync<T>(string tag) /// <summary> /// Read a tag asynchronusly /// </summary> public async Task<ReadEvent<T>> ReadAsync<T>(string tag) { return await Task.Run(() => this.Read<T>(tag)); } #endregion #region Read tags —— IDictionary<string, ReadEvent> Read(IEnumerable<string> tags) /// <summary> /// Read tags /// </summary> /// <param name="tags">The fully-qualified identifiers of the tags. You can specify a subfolder by using a comma delimited name. /// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo`</param> /// <returns>The key value pairs retrieved from the OPC</returns> public IDictionary<string, ReadEvent> Read(IEnumerable<string> tags) { #region # Check tags = tags?.Distinct().ToArray() ?? new string[0]; if (!tags.Any()) { return new Dictionary<string, ReadEvent>(); } if (this.Status == OpcStatus.NotConnected) { throw new OpcException("Server not connected. Cannot read tag."); } #endregion IEnumerable<OpcDa.Item> items = from tag in tags let item = new OpcDa.Item { ItemName = tag } select item; OpcDa.ItemValueResult[] results = this._server.Read(items.ToArray()); IDictionary<string, ReadEvent> readEvents = new ConcurrentDictionary<string, ReadEvent>(); foreach (OpcDa.ItemValueResult result in results) { ReadEvent readEvent = new ReadEvent(); readEvent.Value = result.Value; readEvent.SourceTimestamp = result.Timestamp; readEvent.ServerTimestamp = result.Timestamp; if (result.Quality == OpcDa.Quality.Good) readEvent.Quality = Quality.Good; if (result.Quality == OpcDa.Quality.Bad) readEvent.Quality = Quality.Bad; string tag = result.ItemName.ToString(); readEvents.Add(tag, readEvent); } return readEvents; } #endregion #region Write a value on the specified opc tag —— void Write<T>(string tag, T item) /// <summary> /// Write a value on the specified opc tag /// </summary> /// <typeparam name="T">The type of tag to write on</typeparam> /// <param name="tag">The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. /// E.g: the tag `foo.bar` writes on the tag `bar` on the folder `foo`</param> /// <param name="item"></param> public void Write<T>(string tag, T item) { OpcDa.ItemValue itmVal = new OpcDa.ItemValue { ItemName = tag, Value = item }; IdentifiedResult result = this._server.Write(new[] { itmVal })[0]; if (result == null) { throw new OpcException("The server replied with an empty response"); } if (result.ResultID.ToString() != "S_OK") { throw new OpcException($"Invalid response from the server. (Response Status: {result.ResultID}, Opc Tag: {tag})"); } } #endregion #region Write a value on the specified opc tag asynchronously —— Task WriteAsync<T>(string tag, T item) /// <summary> /// Write a value on the specified opc tag asynchronously /// </summary> public async Task WriteAsync<T>(string tag, T item) { await Task.Run(() => this.Write(tag, item)); } #endregion #region Monitor the specified tag for changes —— void Monitor(string tag, Action<ReadEvent, Action> callback) /// <summary> /// Monitor the specified tag for changes /// </summary> /// <param name="tag">The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. /// E.g: the tag `foo.bar` monitors the tag `bar` on the folder `foo`</param> /// <param name="callback">the callback to execute when the value is changed. /// The first parameter is a MonitorEvent object which represents the data point, the second is an `unsubscribe` function to unsubscribe the callback</param> public void Monitor(string tag, Action<ReadEvent, Action> callback) { OpcDa.SubscriptionState subItem = new OpcDa.SubscriptionState { Name = (++this._sub).ToString(CultureInfo.InvariantCulture), Active = true, UpdateRate = this.MonitorInterval ?? DefaultMonitorInterval }; OpcDa.ISubscription sub = this._server.CreateSubscription(subItem); // I have to start a new thread here because unsubscribing // the subscription during a datachanged event causes a deadlock Action unsubscribe = () => new Thread(o => this._server.CancelSubscription(sub)).Start(); sub.DataChanged += (handle, requestHandle, values) => { ReadEvent monitorEvent = new ReadEvent(); monitorEvent.Value = values[0].Value; monitorEvent.SourceTimestamp = values[0].Timestamp; monitorEvent.ServerTimestamp = values[0].Timestamp; if (values[0].Quality == OpcDa.Quality.Good) monitorEvent.Quality = Quality.Good; if (values[0].Quality == OpcDa.Quality.Bad) monitorEvent.Quality = Quality.Bad; callback(monitorEvent, unsubscribe); }; sub.AddItems(new[] { new OpcDa.Item { ItemName = tag } }); sub.SetEnabled(true); } #endregion #region Monitor the specified tag for changes —— void Monitor<T>(string tag, Action<ReadEvent<T>, Action> callback) /// <summary> /// Monitor the specified tag for changes /// </summary> /// <typeparam name="T">the type of tag to monitor</typeparam> /// <param name="tag">The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. /// E.g: the tag `foo.bar` monitors the tag `bar` on the folder `foo`</param> /// <param name="callback">the callback to execute when the value is changed. /// The first parameter is a MonitorEvent object which represents the data point, the second is an `unsubscribe` function to unsubscribe the callback</param> public void Monitor<T>(string tag, Action<ReadEvent<T>, Action> callback) { OpcDa.SubscriptionState subItem = new OpcDa.SubscriptionState { Name = (++this._sub).ToString(CultureInfo.InvariantCulture), Active = true, UpdateRate = this.MonitorInterval ?? DefaultMonitorInterval }; OpcDa.ISubscription sub = this._server.CreateSubscription(subItem); // I have to start a new thread here because unsubscribing // the subscription during a datachanged event causes a deadlock Action unsubscribe = () => new Thread(o => this._server.CancelSubscription(sub)).Start(); sub.DataChanged += (handle, requestHandle, values) => { T casted = this.TryCastResult<T>(values[0].Value); ReadEvent<T> monitorEvent = new ReadEvent<T>(); monitorEvent.Value = casted; monitorEvent.SourceTimestamp = values[0].Timestamp; monitorEvent.ServerTimestamp = values[0].Timestamp; if (values[0].Quality == OpcDa.Quality.Good) monitorEvent.Quality = Quality.Good; if (values[0].Quality == OpcDa.Quality.Bad) monitorEvent.Quality = Quality.Bad; callback(monitorEvent, unsubscribe); }; sub.AddItems(new[] { new OpcDa.Item { ItemName = tag } }); sub.SetEnabled(true); } #endregion #region Monitor the specified tags for changes —— void Monitor(IEnumerable<string> tags... /// <summary> /// Monitor the specified tags for changes /// </summary> /// <param name="tags">The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. /// E.g: the tag `foo.bar` monitors the tag `bar` on the folder `foo`</param> /// <param name="callback">the callback to execute when the value is changed. /// The first parameter is the new values of the nodes, the second is an `unsubscribe` function to unsubscribe the callback</param> public void Monitor(IEnumerable<string> tags, Action<IDictionary<string, ReadEvent>, Action> callback) { //Updated by Lee #region # Check tags = tags?.Distinct().ToArray() ?? new string[0]; if (!tags.Any()) { throw new ArgumentNullException(nameof(tags), "tags cannot be empty !"); } #endregion OpcDa.SubscriptionState subItem = new OpcDa.SubscriptionState { Name = (++this._sub).ToString(CultureInfo.InvariantCulture), Active = true, UpdateRate = this.MonitorInterval ?? DefaultMonitorInterval }; OpcDa.ISubscription sub = this._server.CreateSubscription(subItem); // I have to start a new thread here because unsubscribing // the subscription during a datachanged event causes a deadlock Action unsubscribe = () => new Thread(o => this._server.CancelSubscription(sub)).Start(); IDictionary<string, ReadEvent> readEvents = new ConcurrentDictionary<string, ReadEvent>(); sub.DataChanged += (handle, requestHandle, values) => { foreach (OpcDa.ItemValueResult itemValueResult in values) { ReadEvent monitorEvent = new ReadEvent(); monitorEvent.Value = itemValueResult.Value; monitorEvent.SourceTimestamp = itemValueResult.Timestamp; monitorEvent.ServerTimestamp = itemValueResult.Timestamp; if (itemValueResult.Quality == OpcDa.Quality.Good) monitorEvent.Quality = Quality.Good; if (itemValueResult.Quality == OpcDa.Quality.Bad) monitorEvent.Quality = Quality.Bad; string tag = itemValueResult.ItemName.ToString(); if (readEvents.ContainsKey(tag)) { readEvents[tag] = monitorEvent; } else { readEvents.Add(tag, monitorEvent); } } callback(readEvents, unsubscribe); }; sub.AddItems(tags.Select(tag => new OpcDa.Item { ItemName = tag }).ToArray()); sub.SetEnabled(true); } #endregion #region Monitor the specified tags for changes —— void MonitorChanges(IEnumerable<string> tags... /// <summary> /// Monitor the specified tags for changes /// </summary> /// <param name="tags">The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name. /// E.g: the tag `foo.bar` monitors the tag `bar` on the folder `foo`</param> /// <param name="callback">the callback to execute when the value is changed. /// The first parameter is the new values of the nodes, the second is an `unsubscribe` function to unsubscribe the callback</param> public void MonitorChanges(IEnumerable<string> tags, Action<IDictionary<string, ReadEvent>, Action> callback) { //Updated by Lee #region # Check tags = tags?.Distinct().ToArray() ?? new string[0]; if (!tags.Any()) { throw new ArgumentNullException(nameof(tags), "tags cannot be empty !"); } #endregion OpcDa.SubscriptionState subItem = new OpcDa.SubscriptionState { Name = (++this._sub).ToString(CultureInfo.InvariantCulture), Active = true, UpdateRate = this.MonitorInterval ?? DefaultMonitorInterval }; OpcDa.ISubscription sub = this._server.CreateSubscription(subItem); // I have to start a new thread here because unsubscribing // the subscription during a datachanged event causes a deadlock Action unsubscribe = () => new Thread(o => this._server.CancelSubscription(sub)).Start(); sub.DataChanged += (handle, requestHandle, values) => { IDictionary<string, ReadEvent> readEvents = new ConcurrentDictionary<string, ReadEvent>(); foreach (OpcDa.ItemValueResult itemValueResult in values) { ReadEvent monitorEvent = new ReadEvent(); monitorEvent.Value = itemValueResult.Value; monitorEvent.SourceTimestamp = itemValueResult.Timestamp; monitorEvent.ServerTimestamp = itemValueResult.Timestamp; if (itemValueResult.Quality == OpcDa.Quality.Good) monitorEvent.Quality = Quality.Good; if (itemValueResult.Quality == OpcDa.Quality.Bad) monitorEvent.Quality = Quality.Bad; string tag = itemValueResult.ItemName.ToString(); readEvents.Add(tag, monitorEvent); } callback(readEvents, unsubscribe); }; sub.AddItems(tags.Select(tag => new OpcDa.Item { ItemName = tag }).ToArray()); sub.SetEnabled(true); } #endregion #region Release unmanaged resources —— void Dispose() /// <summary> /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// </summary> public void Dispose() { if (this.Server != null) { this._server.Dispose(); this.Status = OpcStatus.NotConnected; GC.SuppressFinalize(this); } } #endregion //Private #region Casts result of monitoring and reading values —— T TryCastResult<T>(object value) /// <summary> /// Casts result of monitoring and reading values /// </summary> /// <param name="value">Value to convert</param> /// <typeparam name="T">Type of object to try to cast</typeparam> /// <returns>The casted result</returns> private T TryCastResult<T>(object value) { try { return (T)value; } catch (InvalidCastException) { throw new InvalidCastException($"Could not monitor tag. Cast failed for type \"{typeof(T)}\" on the new value \"{value}\" with type \"{value.GetType()}\". Make sure tag data type matches."); } } #endregion #endregion } }