#region Copyright (c) 2011-2023 Technosoftware GmbH. All rights reserved //----------------------------------------------------------------------------- // Copyright (c) 2011-2023 Technosoftware GmbH. All rights reserved // Web: https://www.technosoftware.com // // The source code in this file is covered under a dual-license scenario: // - Owner of a purchased license: SCLA 1.0 // - GPL V3: everybody else // // SCLA license terms accompanied with this source code. // See SCLA 1.0: https://technosoftware.com/license/Source_Code_License_Agreement.pdf // // GNU General Public License as published by the Free Software Foundation; // version 3 of the License are accompanied with this source code. // See https://technosoftware.com/license/GPLv3License.txt // // This source code is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. //----------------------------------------------------------------------------- #endregion Copyright (c) 2011-2023 Technosoftware GmbH. All rights reserved #region Using Directives using System; using System.Collections; using System.Globalization; using System.Runtime.InteropServices; using System.Threading; using Technosoftware.DaAeHdaClient.Com.Utilities; using Technosoftware.DaAeHdaClient.Da; using Technosoftware.OpcRcw.Da; #endregion namespace Technosoftware.DaAeHdaClient.Com.Da { /// /// A .NET wrapper for a COM server that implements the DA server interfaces. /// internal class Server : Com.Server, ITsDaServer { #region Fields /// /// The default result filters for the server. /// private int filters_ = (int)TsCDaResultFilter.All | (int)TsCDaResultFilter.ClientHandle; /// /// A table of active subscriptions for the server. /// private readonly Hashtable subscriptions_ = new Hashtable(); #endregion #region Constructors /// /// Initializes the object. /// internal Server() { } /// /// Initializes the object with the specified COM server. /// internal Server(OpcUrl url, object server) { if (url == null) throw new ArgumentNullException(nameof(url)); url_ = (OpcUrl)url.Clone(); server_ = server; } #endregion #region IDisposable Members /// /// Dispose(bool disposing) executes in two distinct scenarios. /// If disposing equals true, the method has been called directly /// or indirectly by a user's code. Managed and unmanaged resources /// can be disposed. /// If disposing equals false, the method has been called by the /// runtime from inside the finalizer and you should not reference /// other objects. Only unmanaged resources can be disposed. /// /// If true managed and unmanaged resources can be disposed. If false only unmanaged resources. protected override void Dispose(bool disposing) { if (!disposed_) { lock (this) { if (disposing) { // Release managed resources. if (server_ != null) { // release all groups. foreach (Subscription subscription in subscriptions_.Values) { var methodName = "IOPCServer.RemoveGroup"; // remove subscription from server. try { var state = subscription.GetState(); if (state != null) { var server = BeginComCall(methodName, true); server?.RemoveGroup((int)state.ServerHandle, 0); } } catch { // Ignore error during Dispose } finally { EndComCall(methodName); } // dispose of the subscription object (disconnects all subscription connections). subscription.Dispose(); } // clear subscription table. subscriptions_.Clear(); } } // Release unmanaged resources. // Set large fields to null. if (server_ != null) { // release the COM server. Technosoftware.DaAeHdaClient.Com.Interop.ReleaseServer(server_); server_ = null; } } // Call Dispose on your base class. disposed_ = true; } base.Dispose(disposing); } private bool disposed_; #endregion #region Technosoftware.DaAeHdaClient.Com.Server Overrides /// /// Returns the localized text for the specified result code. /// /// The locale name in the format "[languagecode]-[country/regioncode]". /// The result code identifier. /// A message localized for the best match for the requested locale. public override string GetErrorText(string locale, OpcResult resultId) { lock (this) { if (server_ == null) throw new NotConnectedException(); var methodName = "IOPCServer.GetErrorString"; // invoke COM method. try { var server = BeginComCall(methodName, true); (server).GetErrorString( resultId.Code, Technosoftware.DaAeHdaClient.Com.Interop.GetLocale(locale), out var errorText); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } return errorText; } catch (Exception e) { ComCallError(methodName, e); throw Technosoftware.DaAeHdaClient.Com.Interop.CreateException("IOPCServer.GetErrorString", e); } finally { EndComCall(methodName); } } } #endregion #region Technosoftware.DaAeHdaClient.IOpcServer Members /// /// Returns the filters applied by the server to any item results returned to the client. /// /// A bit mask indicating which fields should be returned in any item results. public int GetResultFilters() { lock (this) { if (server_ == null) throw new NotConnectedException(); return filters_; } } /// /// Sets the filters applied by the server to any item results returned to the client. /// /// A bit mask indicating which fields should be returned in any item results. public void SetResultFilters(int filters) { lock (this) { if (server_ == null) throw new NotConnectedException(); filters_ = filters; } } /// /// Returns the current server status. /// /// The current server status. public OpcServerStatus GetServerStatus() { lock (this) { if (server_ == null) throw new NotConnectedException(); var methodName = "IOPCServer.GetStatus"; // initialize arguments. IntPtr pStatus; // invoke COM method. try { var server = BeginComCall(methodName, true); (server).GetStatus(out pStatus); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } } catch (Exception e) { ComCallError(methodName, e); throw Technosoftware.DaAeHdaClient.Com.Interop.CreateException(methodName, e); } finally { EndComCall(methodName); } // return status. return Interop.GetServerStatus(ref pStatus, true); } } /// /// Reads the current values for a set of items. /// /// The set of items to read. /// The results of the read operation for each item. public virtual TsCDaItemValueResult[] Read(TsCDaItem[] items) { if (items == null) throw new ArgumentNullException(nameof(items)); lock (this) { var methodName = "IOPCItemIO.Read"; if (server_ == null) throw new NotConnectedException(); var count = items.Length; if (count == 0) throw new ArgumentOutOfRangeException(nameof(items.Length), @"0"); // initialize arguments. var itemIDs = new string[count]; var maxAges = new int[count]; for (var ii = 0; ii < count; ii++) { itemIDs[ii] = items[ii].ItemName; maxAges[ii] = (items[ii].MaxAgeSpecified) ? items[ii].MaxAge : 0; } var pValues = IntPtr.Zero; var pQualities = IntPtr.Zero; var pTimestamps = IntPtr.Zero; var pErrors = IntPtr.Zero; // invoke COM method. try { var server = BeginComCall(methodName, true); server.Read( count, itemIDs, maxAges, out pValues, out pQualities, out pTimestamps, out pErrors); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } } catch (Exception e) { ComCallError(methodName, e); throw Technosoftware.DaAeHdaClient.Com.Interop.CreateException(methodName, e); } finally { EndComCall(methodName); } // unmarshal results. var values = Technosoftware.DaAeHdaClient.Com.Interop.GetVARIANTs(ref pValues, count, true); var qualities = Technosoftware.DaAeHdaClient.Com.Interop.GetInt16s(ref pQualities, count, true); var timestamps = Technosoftware.DaAeHdaClient.Com.Interop.GetFILETIMEs(ref pTimestamps, count, true); var errors = Technosoftware.DaAeHdaClient.Com.Interop.GetInt32s(ref pErrors, count, true); // pre-fetch the current locale to use for data conversions. var locale = GetLocale(); // construct result array. var results = new TsCDaItemValueResult[count]; for (var ii = 0; ii < results.Length; ii++) { results[ii] = new TsCDaItemValueResult(items[ii]); results[ii].ServerHandle = null; results[ii].Value = values[ii]; results[ii].Quality = new TsCDaQuality(qualities[ii]); results[ii].QualitySpecified = true; results[ii].Timestamp = timestamps[ii]; results[ii].TimestampSpecified = timestamps[ii] != DateTime.MinValue; results[ii].Result = Utilities.Interop.GetResultId(errors[ii]); results[ii].DiagnosticInfo = null; // convert COM code to unified DA code. if (errors[ii] == Result.E_BADRIGHTS) { results[ii].Result = new OpcResult(OpcResult.Da.E_WRITEONLY, Result.E_BADRIGHTS); } // convert the data type since the server does not support the feature. if (results[ii].Value != null && items[ii].ReqType != null) { try { results[ii].Value = ChangeType(values[ii], items[ii].ReqType, locale); } catch (Exception e) { results[ii].Value = null; results[ii].Quality = TsCDaQuality.Bad; results[ii].QualitySpecified = true; results[ii].Timestamp = DateTime.MinValue; results[ii].TimestampSpecified = false; if (e.GetType() == typeof(OverflowException)) { results[ii].Result = Utilities.Interop.GetResultId(Result.E_RANGE); } else { results[ii].Result = Utilities.Interop.GetResultId(Result.E_BADTYPE); } } } // apply request options. if ((filters_ & (int)TsCDaResultFilter.ItemName) == 0) results[ii].ItemName = null; if ((filters_ & (int)TsCDaResultFilter.ItemPath) == 0) results[ii].ItemPath = null; if ((filters_ & (int)TsCDaResultFilter.ClientHandle) == 0) results[ii].ClientHandle = null; if ((filters_ & (int)TsCDaResultFilter.ItemTime) == 0) { results[ii].Timestamp = DateTime.MinValue; results[ii].TimestampSpecified = false; } } // return results. return results; } } /// /// Writes the value, quality and timestamp for a set of items. /// /// The set of item values to write. /// The results of the write operation for each item. public virtual OpcItemResult[] Write(TsCDaItemValue[] items) { if (items == null) throw new ArgumentNullException(nameof(items)); lock (this) { if (server_ == null) throw new NotConnectedException(); var methodName = "IOPCItemIO.WriteVQT"; var count = items.Length; if (count == 0) throw new ArgumentOutOfRangeException("items.Length", "0"); // initialize arguments. var itemIDs = new string[count]; for (var ii = 0; ii < count; ii++) { itemIDs[ii] = items[ii].ItemName; } var values = Interop.GetOPCITEMVQTs(items); var pErrors = IntPtr.Zero; // invoke COM method. try { var server = BeginComCall(methodName, true); server.WriteVQT( count, itemIDs, values, out pErrors); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } } catch (Exception e) { ComCallError(methodName, e); throw Technosoftware.DaAeHdaClient.Com.Interop.CreateException(methodName, e); } finally { EndComCall(methodName); } // unmarshal results. var errors = Utilities.Interop.GetInt32s(ref pErrors, count, true); // construct result array. var results = new OpcItemResult[count]; for (var ii = 0; ii < count; ii++) { results[ii] = new OpcItemResult(items[ii]); results[ii].ServerHandle = null; results[ii].Result = Utilities.Interop.GetResultId(errors[ii]); results[ii].DiagnosticInfo = null; // convert COM code to unified DA code. if (errors[ii] == Result.E_BADRIGHTS) { results[ii].Result = new OpcResult(OpcResult.Da.E_READONLY, Result.E_BADRIGHTS); } // apply request options. if ((filters_ & (int)TsCDaResultFilter.ItemName) == 0) results[ii].ItemName = null; if ((filters_ & (int)TsCDaResultFilter.ItemPath) == 0) results[ii].ItemPath = null; if ((filters_ & (int)TsCDaResultFilter.ClientHandle) == 0) results[ii].ClientHandle = null; } // return results. return results; } } /// /// Creates a new subscription. /// /// The initial state of the subscription. /// The new subscription object. public ITsCDaSubscription CreateSubscription(TsCDaSubscriptionState state) { if (state == null) throw new ArgumentNullException(nameof(state)); lock (this) { if (server_ == null) throw new NotConnectedException(); var methodName = "IOPCServer.AddGroup"; // copy the subscription state. var result = (TsCDaSubscriptionState)state.Clone(); // initialize arguments. var iid = typeof(IOPCItemMgt).GUID; object group = null; var serverHandle = 0; var revisedUpdateRate = 0; var hDeadband = GCHandle.Alloc(result.Deadband, GCHandleType.Pinned); // invoke COM method. try { var server = BeginComCall(methodName, true); server.AddGroup( (result.Name != null) ? result.Name : "", (result.Active) ? 1 : 0, result.UpdateRate, 0, IntPtr.Zero, hDeadband.AddrOfPinnedObject(), Technosoftware.DaAeHdaClient.Com.Interop.GetLocale(result.Locale), out serverHandle, out revisedUpdateRate, ref iid, out group); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } } catch (Exception e) { ComCallError(methodName, e); throw Utilities.Interop.CreateException(methodName, e); } finally { if (hDeadband.IsAllocated) { hDeadband.Free(); } EndComCall(methodName); } if (group == null) throw new OpcResultException(OpcResult.E_FAIL, "The subscription was not created."); methodName = "IOPCGroupStateMgt2.SetKeepAlive"; // set the keep alive rate if requested. try { var keepAlive = 0; var comObject = BeginComCall(group, methodName, true); comObject.SetKeepAlive(result.KeepAlive, out keepAlive); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } result.KeepAlive = keepAlive; } catch (Exception e1) { result.KeepAlive = 0; ComCallError(methodName, e1); } finally { EndComCall(methodName); } // save server handle. result.ServerHandle = serverHandle; // set the revised update rate. if (revisedUpdateRate > result.UpdateRate) { result.UpdateRate = revisedUpdateRate; } // create the subscription object. var subscription = CreateSubscription(group, result, filters_); // index by server handle. subscriptions_[serverHandle] = subscription; // return subscription. return subscription; } } /// /// Cancels a subscription and releases all resources allocated for it. /// /// The subscription to cancel. public void CancelSubscription(ITsCDaSubscription subscription) { if (subscription == null) throw new ArgumentNullException(nameof(subscription)); lock (this) { if (server_ == null) throw new NotConnectedException(); var methodName = "IOPCServer.RemoveGroup"; // validate argument. if (!typeof(Subscription).IsInstanceOfType(subscription)) { throw new ArgumentException("Incorrect object type.", nameof(subscription)); } // get the subscription state. var state = subscription.GetState(); if (!subscriptions_.ContainsKey(state.ServerHandle)) { throw new ArgumentException("Handle not found.", nameof(subscription)); } subscriptions_.Remove(state.ServerHandle); // release all subscription resources. subscription.Dispose(); // invoke COM method. try { var server = BeginComCall(methodName, true); server.RemoveGroup((int)state.ServerHandle, 0); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } } catch (Exception e) { ComCallError(methodName, e); throw Utilities.Interop.CreateException(methodName, e); } finally { EndComCall(methodName); } } } /// /// Fetches the children of a branch that meet the filter criteria. /// /// The identifier of branch which is the target of the search. /// The filters to use to limit the set of child elements returned. /// An object used to continue a browse that could not be completed. /// The set of elements found. public virtual TsCDaBrowseElement[] Browse( OpcItem itemId, TsCDaBrowseFilters filters, out TsCDaBrowsePosition position) { if (filters == null) throw new ArgumentNullException(nameof(filters)); lock (this) { if (server_ == null) throw new NotConnectedException(); var methodName = "IOPCBrowse.Browse"; position = null; // initialize arguments. var count = 0; var moreElements = 0; var pContinuationPoint = IntPtr.Zero; var pElements = IntPtr.Zero; // invoke COM method. try { var server = BeginComCall(methodName, true); server.Browse( (itemId != null && itemId.ItemName != null) ? itemId.ItemName : "", ref pContinuationPoint, filters.MaxElementsReturned, Interop.GetBrowseFilter(filters.BrowseFilter), (filters.ElementNameFilter != null) ? filters.ElementNameFilter : "", (filters.VendorFilter != null) ? filters.VendorFilter : "", (filters.ReturnAllProperties) ? 1 : 0, (filters.ReturnPropertyValues) ? 1 : 0, (filters.PropertyIDs != null) ? filters.PropertyIDs.Length : 0, Interop.GetPropertyIDs(filters.PropertyIDs), out moreElements, out count, out pElements); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } } catch (Exception e) { ComCallError(methodName, e); throw Technosoftware.DaAeHdaClient.Com.Interop.CreateException(methodName, e); } finally { EndComCall(methodName); } // unmarshal results. var elements = Interop.GetBrowseElements(ref pElements, count, true); var continuationPoint = Marshal.PtrToStringUni(pContinuationPoint); Marshal.FreeCoTaskMem(pContinuationPoint); // check if more results exist. if (moreElements != 0 || (continuationPoint != null && continuationPoint != "")) { // allocate new browse position object. position = new BrowsePosition(itemId, filters, continuationPoint); } // process results. ProcessResults(elements, filters.PropertyIDs); return elements; } } /// /// Continues a browse operation with previously specified search criteria. /// /// An object containing the browse operation state information. /// The set of elements found. public virtual TsCDaBrowseElement[] BrowseNext(ref TsCDaBrowsePosition position) { lock (this) { if (server_ == null) throw new NotConnectedException(); var methodName = "IOPCBrowse.Browse"; // check for valid position object. if (position == null || position.GetType() != typeof(BrowsePosition)) { throw new BrowseCannotContinueException(); } var pos = (BrowsePosition)position; // check for valid continuation point. if (pos == null || pos.ContinuationPoint == null || pos.ContinuationPoint == "") { throw new BrowseCannotContinueException(); } // initialize arguments. var count = 0; var moreElements = 0; var itemID = ((BrowsePosition)position).ItemID; var filters = ((BrowsePosition)position).Filters; var pContinuationPoint = Marshal.StringToCoTaskMemUni(pos.ContinuationPoint); var pElements = IntPtr.Zero; // invoke COM method. try { var server = BeginComCall(methodName, true); server.Browse( (itemID != null && itemID.ItemName != null) ? itemID.ItemName : "", ref pContinuationPoint, filters.MaxElementsReturned, Interop.GetBrowseFilter(filters.BrowseFilter), (filters.ElementNameFilter != null) ? filters.ElementNameFilter : "", (filters.VendorFilter != null) ? filters.VendorFilter : "", (filters.ReturnAllProperties) ? 1 : 0, (filters.ReturnPropertyValues) ? 1 : 0, (filters.PropertyIDs != null) ? filters.PropertyIDs.Length : 0, Interop.GetPropertyIDs(filters.PropertyIDs), out moreElements, out count, out pElements); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } } catch (Exception e) { ComCallError(methodName, e); throw Technosoftware.DaAeHdaClient.Com.Interop.CreateException(methodName, e); } finally { EndComCall(methodName); } // unmarshal results. var elements = Interop.GetBrowseElements(ref pElements, count, true); pos.ContinuationPoint = Marshal.PtrToStringUni(pContinuationPoint); Marshal.FreeCoTaskMem(pContinuationPoint); // check if more no results exist. if (moreElements == 0 && (pos.ContinuationPoint == null || pos.ContinuationPoint == "")) { position = null; } // process results. ProcessResults(elements, filters.PropertyIDs); return elements; } } /// /// Returns the item properties for a set of items. /// /// A list of item identifiers. /// A list of properties to fetch for each item. /// Whether the property values should be returned with the properties. /// A list of properties for each item. public virtual TsCDaItemPropertyCollection[] GetProperties( OpcItem[] itemIds, TsDaPropertyID[] propertyIDs, bool returnValues) { if (itemIds == null) throw new ArgumentNullException(nameof(itemIds)); lock (this) { if (server_ == null) throw new NotConnectedException(); var methodName = "IOPCBrowse.GetProperties"; // initialize arguments. var pItemIDs = new string[itemIds.Length]; for (var ii = 0; ii < itemIds.Length; ii++) { pItemIDs[ii] = itemIds[ii].ItemName; } var pPropertyLists = IntPtr.Zero; // invoke COM method. try { var server = BeginComCall(methodName, true); server.GetProperties( itemIds.Length, pItemIDs, (returnValues) ? 1 : 0, (propertyIDs != null) ? propertyIDs.Length : 0, Interop.GetPropertyIDs(propertyIDs), out pPropertyLists); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } } catch (Exception e) { ComCallError(methodName, e); throw Technosoftware.DaAeHdaClient.Com.Interop.CreateException(methodName, e); } finally { EndComCall(methodName); } // unmarshal results. var resultLists = Interop.GetItemPropertyCollections(ref pPropertyLists, itemIds.Length, true); // replace integer codes with qnames passed in. if (propertyIDs != null && propertyIDs.Length > 0) { foreach (var resultList in resultLists) { for (var ii = 0; ii < resultList.Count; ii++) { resultList[ii].ID = propertyIDs[ii]; } } } // return the results. return resultLists; } } #endregion #region Private Methods /// /// Converts a value to the specified type using the specified locale. /// protected object ChangeType(object source, Type type, string locale) { var culture = Thread.CurrentThread.CurrentCulture; // override the current thread culture to ensure conversions happen correctly. try { Thread.CurrentThread.CurrentCulture = new CultureInfo(locale); } catch { Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); } try { var result = OpcConvert.ChangeType(source, type); // check for overflow converting to float. if (typeof(float) == type) { if (float.IsInfinity(Convert.ToSingle(result))) { throw new OverflowException(); } } return result; } // restore the current thread culture after conversion. finally { Thread.CurrentThread.CurrentCulture = culture; } } /// /// Creates a new instance of a subscription. /// protected virtual Subscription CreateSubscription( object group, TsCDaSubscriptionState state, int filters) { return new Subscription(group, state, filters); } /// /// Updates the properties to convert COM values to OPC .NET API results. /// private void ProcessResults(TsCDaBrowseElement[] elements, TsDaPropertyID[] propertyIds) { // check for null. if (elements == null) { return; } // process each element. foreach (var element in elements) { // check if no properties. if (element.Properties == null) { continue; } // process each property. foreach (var property in element.Properties) { // replace the property ids which on contain the codes with the proper qualified names passed in. if (propertyIds != null) { foreach (var propertyId in propertyIds) { if (property.ID.Code == propertyId.Code) { property.ID = propertyId; break; } } } } } } #endregion } }