#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.Runtime.InteropServices; using Technosoftware.DaAeHdaClient.Com.Da; using Technosoftware.DaAeHdaClient.Com.Utilities; using Technosoftware.DaAeHdaClient.Da; using Technosoftware.DaAeHdaClient.Utilities; using Technosoftware.OpcRcw.Comn; using Technosoftware.OpcRcw.Da; #endregion namespace Technosoftware.DaAeHdaClient.Com.Da20 { /// /// An in-process wrapper for a remote OPC Data Access 2.0X server. /// internal class Server : Da.Server { #region Constructors /// /// The default constructor for the object. /// internal Server() { } /// /// Initializes the object with the specified COM server. /// internal Server(OpcUrl url, object unknown) : base(url, unknown) { } #endregion #region IDisposable Members /// /// This must be called explicitly by clients to ensure the COM server is released. /// public new void Dispose() { lock (this) { if (subscription_ != null) { var methodName = "IOPCServer.RemoveGroup"; try { var server = BeginComCall(methodName, true); server.RemoveGroup(groupHandle_, 0); } catch (Exception e) { ComCallError(methodName, e); } finally { EndComCall(methodName); } Utilities.Interop.ReleaseServer(subscription_); subscription_ = null; groupHandle_ = 0; base.Dispose(); } } } #endregion //====================================================================== // Connection Management /// /// Connects to the server with the specified URL and credentials. /// public override void Initialize(OpcUrl url, OpcConnectData connectData) { lock (this) { // connect to server. base.Initialize(url, connectData); separators_ = null; var methodName = "IOPCCommon.GetLocaleID"; // create a global subscription required for various item level operations. var localeID = 0; try { // get the default locale for the server. var server = BeginComCall(methodName, true); server.GetLocaleID(out localeID); } catch (Exception e) { Uninitialize(); ComCallError(methodName, e); throw Utilities.Interop.CreateException(methodName, e); } finally { EndComCall(methodName); } // create a global subscription required for various item level operations. methodName = "IOPCServer.AddGroup"; try { // add the subscription. var iid = typeof(IOPCItemMgt).GUID; var server = BeginComCall(methodName, true); ((IOPCServer)server).AddGroup( "", 1, 500, 0, IntPtr.Zero, IntPtr.Zero, localeID, out groupHandle_, out var revisedUpdateRate, ref iid, out subscription_); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } } catch (Exception e) { Uninitialize(); ComCallError(methodName, e); throw Utilities.Interop.CreateException(methodName, e); } finally { EndComCall(methodName); } } } //====================================================================== // Private Members /// /// A global subscription used for various item level operations. /// private object subscription_ = null; /// /// The server handle for the global subscription. /// private int groupHandle_ = 0; /// /// True if BROWSE_TO is supported; otherwise false. /// private bool browseToSupported_ = true; /// /// A list of seperators used in the browse paths. /// private char[] separators_ = null; private readonly object separatorsLock_ = new(); //====================================================================== // Read /// /// Reads the values for the specified items. /// public override TsCDaItemValueResult[] Read(TsCDaItem[] items) { if (items == null) throw new ArgumentNullException(nameof(items)); // check if nothing to do. if (items.Length == 0) { return Array.Empty(); } lock (this) { if (server_ == null) throw new NotConnectedException(); // create temporary items. var temporaryItems = AddItems(items); var results = new TsCDaItemValueResult[items.Length]; try { // construct return values. var cacheItems = new ArrayList(items.Length); var cacheResults = new ArrayList(items.Length); var deviceItems = new ArrayList(items.Length); var deviceResults = new ArrayList(items.Length); for (var ii = 0; ii < items.Length; ii++) { results[ii] = new TsCDaItemValueResult(temporaryItems[ii]); if (temporaryItems[ii].Result.Failed()) { results[ii].Result = temporaryItems[ii].Result; results[ii].DiagnosticInfo = temporaryItems[ii].DiagnosticInfo; continue; } if (items[ii].MaxAgeSpecified && (items[ii].MaxAge < 0 || items[ii].MaxAge == int.MaxValue)) { cacheItems.Add(items[ii]); cacheResults.Add(results[ii]); } else { deviceItems.Add(items[ii]); deviceResults.Add(results[ii]); } } // read values from the cache. if (cacheResults.Count > 0) { var methodName = "IOPCItemMgt.SetActiveState"; // items must be active for cache reads. try { // create list of server handles. var serverHandles = new int[cacheResults.Count]; for (var ii = 0; ii < cacheResults.Count; ii++) { serverHandles[ii] = (int)((TsCDaItemValueResult)cacheResults[ii]).ServerHandle; } var pErrors = IntPtr.Zero; var subscription = BeginComCall(subscription_, methodName, true); subscription.SetActiveState( cacheResults.Count, serverHandles, 1, out pErrors); // free error array. Marshal.FreeCoTaskMem(pErrors); 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); } // read the values. ReadValues( (TsCDaItem[])cacheItems.ToArray(typeof(TsCDaItem)), (TsCDaItemValueResult[])cacheResults.ToArray(typeof(TsCDaItemValueResult)), true); } // read values from the device. if (deviceResults.Count > 0) { ReadValues( (TsCDaItem[])deviceItems.ToArray(typeof(TsCDaItem)), (TsCDaItemValueResult[])deviceResults.ToArray(typeof(TsCDaItemValueResult)), false); } } // remove temporary items after read. finally { RemoveItems(temporaryItems); } // return results. return results; } } //====================================================================== // Write /// /// Write the values for the specified items. /// public override OpcItemResult[] Write(TsCDaItemValue[] items) { if (items == null) throw new ArgumentNullException(nameof(items)); // check if nothing to do. if (items.Length == 0) { return Array.Empty(); } lock (this) { if (server_ == null) throw new NotConnectedException(); // create item objects to add temporary items. var groupItems = new TsCDaItem[items.Length]; for (var ii = 0; ii < items.Length; ii++) { groupItems[ii] = new TsCDaItem(items[ii]); } // create temporary items. var results = AddItems(groupItems); try { // construct list of valid items to write. var writeItems = new ArrayList(items.Length); var writeValues = new ArrayList(items.Length); for (var ii = 0; ii < items.Length; ii++) { if (results[ii].Result.Failed()) { continue; } if (items[ii].QualitySpecified || items[ii].TimestampSpecified) { results[ii].Result = OpcResult.Da.E_NO_WRITEQT; results[ii].DiagnosticInfo = null; continue; } writeItems.Add(results[ii]); writeValues.Add(items[ii]); } // read values from the cache. if (writeItems.Count > 0) { // initialize input parameters. var serverHandles = new int[writeItems.Count]; var values = new object[writeItems.Count]; for (var ii = 0; ii < serverHandles.Length; ii++) { serverHandles[ii] = (int)((OpcItemResult)writeItems[ii]).ServerHandle; values[ii] = Utilities.Interop.GetVARIANT(((TsCDaItemValue)writeValues[ii]).Value); } var pErrors = IntPtr.Zero; // write item values. var methodName = "IOPCSyncIO.Write"; try { var subscription = BeginComCall(subscription_, methodName, true); subscription.Write( writeItems.Count, serverHandles, values, out pErrors); 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); } // unmarshal results. var errors = Utilities.Interop.GetInt32s(ref pErrors, writeItems.Count, true); for (var ii = 0; ii < writeItems.Count; ii++) { var result = (OpcItemResult)writeItems[ii]; result.Result = Utilities.Interop.GetResultId(errors[ii]); result.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); } } } } // remove temporary items finally { RemoveItems(results); } // return results. return results; } } //====================================================================== // Browse /// /// Fetches child elements of the specified branch which match the filter criteria. /// public override TsCDaBrowseElement[] Browse( OpcItem itemId, TsCDaBrowseFilters filters, out TsCDaBrowsePosition position) { if (filters == null) throw new ArgumentNullException(nameof(filters)); position = null; lock (this) { if (server_ == null) throw new NotConnectedException(); BrowsePosition pos = null; var elements = new ArrayList(); // search for child branches. if (filters.BrowseFilter != TsCDaBrowseFilter.Item) { var branches = GetElements(elements.Count, itemId, filters, true, ref pos); if (branches != null) { elements.AddRange(branches); } position = pos; // return current set if browse halted. if (position != null) { return (TsCDaBrowseElement[])elements.ToArray(typeof(TsCDaBrowseElement)); } } // search for child items. if (filters.BrowseFilter != TsCDaBrowseFilter.Branch) { var items = GetElements(elements.Count, itemId, filters, false, ref pos); if (items != null) { elements.AddRange(items); } position = pos; } // return the elements. return (TsCDaBrowseElement[])elements.ToArray(typeof(TsCDaBrowseElement)); } } //====================================================================== // BrowseNext /// /// Continues a browse operation with previously specified search criteria. /// public override TsCDaBrowseElement[] BrowseNext(ref TsCDaBrowsePosition position) { lock (this) { if (server_ == null) throw new NotConnectedException(); // check for valid browse position object. if (position == null && position.GetType() != typeof(BrowsePosition)) { throw new OpcResultException(new OpcResult((int)OpcResult.E_FAIL.Code, OpcResult.FuncCallType.SysFuncCall, null), "The browse operation cannot continue"); } var pos = (BrowsePosition)position; var itemID = pos.ItemID; var filters = pos.Filters; var elements = new ArrayList(); // search for child branches. if (pos.IsBranch) { var branches = GetElements(elements.Count, itemID, filters, true, ref pos); if (branches != null) { elements.AddRange(branches); } position = pos; // return current set if browse halted. if (position != null) { return (TsCDaBrowseElement[])elements.ToArray(typeof(TsCDaBrowseElement)); } } // search for child items. if (filters.BrowseFilter != TsCDaBrowseFilter.Branch) { var items = GetElements(elements.Count, itemID, filters, false, ref pos); if (items != null) { elements.AddRange(items); } position = pos; } // return the elements. return (TsCDaBrowseElement[])elements.ToArray(typeof(TsCDaBrowseElement)); } } //====================================================================== // GetProperties /// /// Returns the specified properties for a set of items. /// public override TsCDaItemPropertyCollection[] GetProperties( OpcItem[] itemIds, TsDaPropertyID[] propertyIDs, bool returnValues) { if (itemIds == null) throw new ArgumentNullException(nameof(itemIds)); // check for trival case. if (itemIds.Length == 0) { return Array.Empty(); } lock (this) { if (server_ == null) throw new NotConnectedException(); // initialize list of property lists. var propertyLists = new TsCDaItemPropertyCollection[itemIds.Length]; for (var ii = 0; ii < itemIds.Length; ii++) { propertyLists[ii] = new TsCDaItemPropertyCollection { ItemName = itemIds[ii].ItemName, ItemPath = itemIds[ii].ItemPath }; // fetch properties for item. try { var properties = GetProperties(itemIds[ii].ItemName, propertyIDs, returnValues); if (properties != null) { propertyLists[ii].AddRange(properties); } propertyLists[ii].Result = OpcResult.S_OK; } catch (OpcResultException e) { propertyLists[ii].Result = e.Result; } catch (Exception e) { propertyLists[ii].Result = new OpcResult(Marshal.GetHRForException(e)); } } // return property lists. return propertyLists; } } //====================================================================== // Private Methods /// /// Adds a set of temporary items used for a read/write operation. /// private OpcItemResult[] AddItems(TsCDaItem[] items) { // add items to subscription. var count = items.Length; var definitions = Technosoftware.DaAeHdaClient.Com.Da.Interop.GetOPCITEMDEFs(items); // ensure all items are created as inactive. for (var ii = 0; ii < definitions.Length; ii++) { definitions[ii].bActive = 0; } // initialize output parameters. var pResults = IntPtr.Zero; var pErrors = IntPtr.Zero; // get the default current for the server. ((IOPCCommon)server_).GetLocaleID(out var localeID); var hLocale = GCHandle.Alloc(localeID, GCHandleType.Pinned); var methodName = "IOPCGroupStateMgt.SetState"; try { // ensure the current locale is correct. var subscription = BeginComCall(subscription_, methodName, true); ((IOPCGroupStateMgt)subscription).SetState( IntPtr.Zero, out var updateRate, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, hLocale.AddrOfPinnedObject(), IntPtr.Zero); 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 (hLocale.IsAllocated) hLocale.Free(); EndComCall(methodName); } // add items to subscription. methodName = "IOPCItemMgt.AddItems"; try { var subscription = BeginComCall(subscription_, methodName, true); subscription.AddItems( count, definitions, out pResults, out pErrors); 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); if (hLocale.IsAllocated) hLocale.Free(); } // unmarshal output parameters. var serverHandles = Technosoftware.DaAeHdaClient.Com.Da.Interop.GetItemResults(ref pResults, count, true); var errors = Utilities.Interop.GetInt32s(ref pErrors, count, true); // create results list. var results = new OpcItemResult[count]; for (var ii = 0; ii < count; ii++) { results[ii] = new OpcItemResult(items[ii]) { ServerHandle = null, Result = Utilities.Interop.GetResultId(errors[ii]), DiagnosticInfo = null }; if (results[ii].Result.Succeeded()) { results[ii].ServerHandle = serverHandles[ii]; } } // return results. return results; } /// /// Removes a set of temporary items used for a read/write operation. /// private void RemoveItems(OpcItemResult[] items) { try { // contruct array of valid server handles. var handles = new ArrayList(items.Length); foreach (var item in items) { if (item.Result.Succeeded() && item.ServerHandle.GetType() == typeof(int)) { handles.Add((int)item.ServerHandle); } } // check if nothing to do. if (handles.Count == 0) { return; } // remove items from server. var pErrors = IntPtr.Zero; var methodName = "IOPCItemMgt.RemoveItems"; try { var subscription = BeginComCall(subscription_, methodName, true); ((IOPCItemMgt)subscription).RemoveItems( handles.Count, (int[])handles.ToArray(typeof(int)), out pErrors); 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); // free returned error array. Utilities.Interop.GetInt32s(ref pErrors, handles.Count, true); } } catch { // ignore errors. } } /// /// Reads a set of values. /// private void ReadValues(TsCDaItem[] items, TsCDaItemValueResult[] results, bool cache) { if (items.Length == 0 || results.Length == 0) return; // marshal input parameters. var serverHandles = new int[results.Length]; for (var ii = 0; ii < results.Length; ii++) { serverHandles[ii] = Convert.ToInt32(results[ii].ServerHandle); } // initialize output parameters. var pValues = IntPtr.Zero; var pErrors = IntPtr.Zero; var methodName = "IOPCSyncIO.Read"; try { var subscription = BeginComCall(subscription_, methodName, true); subscription.Read( (cache) ? OPCDATASOURCE.OPC_DS_CACHE : OPCDATASOURCE.OPC_DS_DEVICE, results.Length, serverHandles, out pValues, out pErrors); 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); // free returned error array. } // unmarshal output parameters. var values = Technosoftware.DaAeHdaClient.Com.Da.Interop.GetItemValues(ref pValues, results.Length, true); var errors = Utilities.Interop.GetInt32s(ref pErrors, results.Length, true); // pre-fetch the current locale to use for data conversions. GetLocale(); // construct results list. for (var ii = 0; ii < results.Length; ii++) { results[ii].Result = Utilities.Interop.GetResultId(errors[ii]); results[ii].DiagnosticInfo = null; if (results[ii].Result.Succeeded()) { results[ii].Value = values[ii].Value; results[ii].Quality = values[ii].Quality; results[ii].QualitySpecified = values[ii].QualitySpecified; results[ii].Timestamp = values[ii].Timestamp; results[ii].TimestampSpecified = values[ii].TimestampSpecified; } // 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(results[ii].Value, items[ii].ReqType, "en-US"); } 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); } } } } } /// /// Returns the set of available properties for the item. /// private TsCDaItemProperty[] GetAvailableProperties(string itemID) { // validate argument. if (itemID == null || itemID.Length == 0) { throw new OpcResultException(OpcResult.Da.E_INVALID_ITEM_NAME); } // query for available properties. var count = 0; var pPropertyIDs = IntPtr.Zero; var pDescriptions = IntPtr.Zero; var pDataTypes = IntPtr.Zero; var methodName = "IOPCItemProperties.QueryAvailableProperties"; try { var server = BeginComCall(methodName, true); server.QueryAvailableProperties( itemID, out count, out pPropertyIDs, out pDescriptions, out pDataTypes); if (DCOMCallWatchdog.IsCancelled) { throw new Exception($"{methodName} call was cancelled due to response timeout"); } } catch (Exception e) { ComCallError(methodName, e); throw new OpcResultException(OpcResult.Da.E_UNKNOWN_ITEM_NAME); } finally { EndComCall(methodName); // free returned error array. } // unmarshal results. var propertyIDs = Utilities.Interop.GetInt32s(ref pPropertyIDs, count, true); var datatypes = Utilities.Interop.GetInt16s(ref pDataTypes, count, true); var descriptions = Utilities.Interop.GetUnicodeStrings(ref pDescriptions, count, true); // check for error condition. if (count == 0) { return null; } // initialize property objects. var properties = new TsCDaItemProperty[count]; for (var ii = 0; ii < count; ii++) { properties[ii] = new TsCDaItemProperty { ID = Technosoftware.DaAeHdaClient.Com.Da.Interop.GetPropertyID(propertyIDs[ii]), Description = descriptions[ii], DataType = Utilities.Interop.GetType((VarEnum)datatypes[ii]), ItemName = null, ItemPath = null, Result = OpcResult.S_OK, Value = null }; } // return property list. return properties; } /// /// Fetches the property item id for the specified set of properties. /// private void GetItemIDs(string itemID, TsCDaItemProperty[] properties) { try { // create input arguments; var propertyIDs = new int[properties.Length]; for (var ii = 0; ii < properties.Length; ii++) { propertyIDs[ii] = properties[ii].ID.Code; } // lookup item ids. var pItemIDs = IntPtr.Zero; var pErrors = IntPtr.Zero; ((IOPCItemProperties)server_).LookupItemIDs( itemID, properties.Length, propertyIDs, out pItemIDs, out pErrors); // unmarshal results. var itemIDs = Utilities.Interop.GetUnicodeStrings(ref pItemIDs, properties.Length, true); var errors = Utilities.Interop.GetInt32s(ref pErrors, properties.Length, true); // update property objects. for (var ii = 0; ii < properties.Length; ii++) { properties[ii].ItemName = null; properties[ii].ItemPath = null; if (errors[ii] >= 0) { properties[ii].ItemName = itemIDs[ii]; } } } catch (Exception) { // set item ids to null for all properties. foreach (var property in properties) { property.ItemName = null; property.ItemPath = null; } } } /// /// Fetches the property values for the specified set of properties. /// private void GetValues(string itemID, TsCDaItemProperty[] properties) { try { // create input arguments; var propertyIDs = new int[properties.Length]; for (var ii = 0; ii < properties.Length; ii++) { propertyIDs[ii] = properties[ii].ID.Code; } // lookup item ids. var pValues = IntPtr.Zero; var pErrors = IntPtr.Zero; ((IOPCItemProperties)server_).GetItemProperties( itemID, properties.Length, propertyIDs, out pValues, out pErrors); // unmarshal results. var values = Interop.GetVARIANTs(ref pValues, properties.Length, true); var errors = Utilities.Interop.GetInt32s(ref pErrors, properties.Length, true); // update property objects. for (var ii = 0; ii < properties.Length; ii++) { properties[ii].Value = null; // ignore value for invalid properties. if (!properties[ii].Result.Succeeded()) { continue; } properties[ii].Result = Utilities.Interop.GetResultId(errors[ii]); // substitute property reult code. if (errors[ii] == Result.E_BADRIGHTS) { properties[ii].Result = new OpcResult(OpcResult.Da.E_WRITEONLY, Result.E_BADRIGHTS); } if (properties[ii].Result.Succeeded()) { properties[ii].Value = Technosoftware.DaAeHdaClient.Com.Da.Interop.UnmarshalPropertyValue(properties[ii].ID, values[ii]); } } } catch (Exception e) { // set general error code as the result for each property. var result = new OpcResult(Marshal.GetHRForException(e)); foreach (var property in properties) { property.Value = null; property.Result = result; } } } /// /// Gets the specified properties for the specified item. /// private TsCDaItemProperty[] GetProperties(string itemID, TsDaPropertyID[] propertyIDs, bool returnValues) { TsCDaItemProperty[] properties; // return all available properties. if (propertyIDs == null) { properties = GetAvailableProperties(itemID); } // return on the selected properties. else { // get available properties. var availableProperties = GetAvailableProperties(itemID); // initialize result list. properties = new TsCDaItemProperty[propertyIDs.Length]; for (var ii = 0; ii < propertyIDs.Length; ii++) { // search available property list for specified property. foreach (var property in availableProperties) { if (property.ID == propertyIDs[ii]) { properties[ii] = (TsCDaItemProperty)property.Clone(); properties[ii].ID = propertyIDs[ii]; break; } } // property not valid for the item. if (properties[ii] == null) { properties[ii] = new TsCDaItemProperty { ID = propertyIDs[ii], Result = OpcResult.Da.E_INVALID_PID }; } } } // fill in missing fields in property objects. if (properties != null) { GetItemIDs(itemID, properties); if (returnValues) { GetValues(itemID, properties); } } // return property list. return properties; } /// /// Returns an enumerator for the children of the specified branch. /// private EnumString GetEnumerator(string itemID, TsCDaBrowseFilters filters, bool branches, bool flat) { var browser = (IOPCBrowseServerAddressSpace)server_; if (!flat) { if (itemID == null) { if (browseToSupported_) { // move to the root of the hierarchial address spaces. try { var id = string.Empty; browser.ChangeBrowsePosition(OPCBROWSEDIRECTION.OPC_BROWSE_TO, id); } catch (Exception e) { var message = string.Format("ChangeBrowsePosition to root with BROWSE_TO={0} failed with error {1}. BROWSE_TO not supported.", string.Empty, e.Message); Utils.Trace(e, message); browseToSupported_ = false; } } if (!browseToSupported_) { // browse to root. while (true) { try { browser.ChangeBrowsePosition(OPCBROWSEDIRECTION.OPC_BROWSE_UP, string.Empty); } catch (Exception) { break; } } } } else { // move to the specified branch for hierarchial address spaces. var id = itemID ?? ""; if (browseToSupported_) { try { browser.ChangeBrowsePosition(OPCBROWSEDIRECTION.OPC_BROWSE_TO, id); } catch (Exception) { browseToSupported_ = false; } } if (!browseToSupported_) { // try to browse down instead. try { browser.ChangeBrowsePosition(OPCBROWSEDIRECTION.OPC_BROWSE_DOWN, id); } catch (Exception) { // browse to root. while (true) { try { browser.ChangeBrowsePosition(OPCBROWSEDIRECTION.OPC_BROWSE_UP, string.Empty); } catch (Exception) { break; } } // parse the browse path. string[] paths = null; lock (separatorsLock_) { if (separators_ != null) { paths = id.Split(separators_); } else { paths = id.Split(separators_); } } // browse to correct location. for (var ii = 0; ii < paths.Length; ii++) { if (paths[ii] == null || paths[ii].Length == 0) { continue; } try { browser.ChangeBrowsePosition(OPCBROWSEDIRECTION.OPC_BROWSE_DOWN, paths[ii]); } catch (Exception) { throw new OpcResultException(OpcResult.Da.E_UNKNOWN_ITEM_NAME); } } } } } } try { // create the enumerator. var browseType = (branches) ? OPCBROWSETYPE.OPC_BRANCH : OPCBROWSETYPE.OPC_LEAF; if (flat) { browseType = OPCBROWSETYPE.OPC_FLAT; } browser.BrowseOPCItemIDs( browseType, filters.ElementNameFilter ?? "", (short)VarEnum.VT_EMPTY, 0, out var enumerator); // return the enumerator. return new EnumString(enumerator); } catch (Exception) { throw new OpcResultException(OpcResult.Da.E_UNKNOWN_ITEM_NAME); } } /// /// Detects the separators used in the item id. /// private void DetectAndSaveSeparators(string browseName, string itemID) { if (!itemID.EndsWith(browseName)) { return; } if (string.Compare(itemID, browseName, true) == 0) { return; } var separator = itemID[itemID.Length - browseName.Length - 1]; lock (separatorsLock_) { var index = -1; if (separators_ != null) { for (var ii = 0; ii < separators_.Length; ii++) { if (separators_[ii] == separator) { index = ii; break; } } if (index == -1) { var separators = new char[separators_.Length + 1]; Array.Copy(separators_, separators, separators_.Length); separators_ = separators; } } if (index == -1) { separators_ ??= new char[1]; separators_[separators_.Length - 1] = separator; } } } /// /// Reads a single value from the enumerator and returns a browse element. /// private TsCDaBrowseElement GetElement( OpcItem itemID, string name, TsCDaBrowseFilters filters, bool isBranch) { if (name == null) { return null; } var element = new TsCDaBrowseElement { Name = name, HasChildren = isBranch, ItemPath = null }; // get item id. try { ((IOPCBrowseServerAddressSpace)server_).GetItemID(element.Name, out var itemName); element.ItemName = itemName; // detect separator. if (element.ItemName != null) { DetectAndSaveSeparators(element.Name, element.ItemName); } } // this is an error that should not occur. catch { element.ItemName = name; } // check if element is an actual item or just a branch. try { var definition = new OPCITEMDEF { szItemID = element.ItemName, szAccessPath = null, hClient = 0, bActive = 0, vtRequestedDataType = (short)VarEnum.VT_EMPTY, dwBlobSize = 0, pBlob = IntPtr.Zero }; var pResults = IntPtr.Zero; var pErrors = IntPtr.Zero; // validate item. ((IOPCItemMgt)subscription_).ValidateItems( 1, new OPCITEMDEF[] { definition }, 0, out pResults, out pErrors); // free results. Technosoftware.DaAeHdaClient.Com.Da.Interop.GetItemResults(ref pResults, 1, true); var errors = Utilities.Interop.GetInt32s(ref pErrors, 1, true); // can only be an item if validation succeeded. element.IsItem = (errors[0] >= 0); } // this is an error that should not occur - must be a branch. catch { element.IsItem = false; // Because ABB Real-TPI server always return ItemName == null we use Name instead to fix browsing problem element.ItemName = element.Name; } // fetch item properties. try { if (filters.ReturnAllProperties) { element.Properties = GetProperties(element.ItemName, null, filters.ReturnPropertyValues); } else if (filters.PropertyIDs != null) { element.Properties = GetProperties(element.ItemName, filters.PropertyIDs, filters.ReturnPropertyValues); } } // return no properties if an error fetching properties occurred. catch { element.Properties = null; } // return new element. return element; } /// /// Returns a list of child elements that meet the filter criteria. /// private TsCDaBrowseElement[] GetElements( int elementsFound, OpcItem itemID, TsCDaBrowseFilters filters, bool branches, ref BrowsePosition position) { // get the enumerator. EnumString enumerator; if (position == null) { var browser = (IOPCBrowseServerAddressSpace)server_; // check the server address space type. OPCNAMESPACETYPE namespaceType; try { browser.QueryOrganization(out namespaceType); } catch (Exception e) { throw Utilities.Interop.CreateException("IOPCBrowseServerAddressSpace.QueryOrganization", e); } // return an empty list if requesting branches for a flat address space. if (namespaceType == OPCNAMESPACETYPE.OPC_NS_FLAT) { if (branches) { return Array.Empty(); } // check that root is browsed for flat address spaces. if (itemID != null && itemID.ItemName != null && itemID.ItemName.Length > 0) { throw new OpcResultException(OpcResult.Da.E_UNKNOWN_ITEM_NAME); } } // get the enumerator. enumerator = GetEnumerator( itemID?.ItemName, filters, branches, namespaceType == OPCNAMESPACETYPE.OPC_NS_FLAT); } else { enumerator = position.Enumerator; } var elements = new ArrayList(); var start = 0; string[] names = null; // get cached name list. if (position != null) { start = position.Index; names = position.Names; position = null; } do { if (names != null) { for (var ii = start; ii < names.Length; ii++) { // check if max returned elements is exceeded. if (filters.MaxElementsReturned != 0 && filters.MaxElementsReturned == elements.Count + elementsFound) { position = new BrowsePosition(itemID, filters, enumerator, branches) { Names = names, Index = ii }; break; } // read elements one at a time. // get next element. var element = GetElement(itemID, names[ii], filters, branches); if (element == null) { break; } // add element. elements.Add(element); } } // check if browse halted. if (position != null) { break; } // fetch next element name. names = enumerator.Next(10); start = 0; } while (names != null && names.Length > 0); // free enumerator. if (position == null) { enumerator.Dispose(); } // return list of elements. return (TsCDaBrowseElement[])elements.ToArray(typeof(TsCDaBrowseElement)); } //====================================================================== // Private Methods /// /// Creates a new instance of a subscription. /// protected override Technosoftware.DaAeHdaClient.Com.Da.Subscription CreateSubscription( object group, TsCDaSubscriptionState state, int filters) { return new Subscription(group, state, filters); } } /// /// Implements an object that handles multi-step browse operations for DA2.05 servers. /// [Serializable] internal class BrowsePosition : TsCDaBrowsePosition { /// /// The enumerator for a browse operation. /// internal EnumString Enumerator = null; /// /// Whether the current enumerator returns branches or leaves. /// internal bool IsBranch = true; /// /// The pre-fetched set of names. /// internal string[] Names = null; /// /// The current index in the pre-fetched names. /// internal int Index = 0; /// /// Initializes a browse position /// internal BrowsePosition( OpcItem itemID, TsCDaBrowseFilters filters, EnumString enumerator, bool isBranch) : base(itemID, filters) { Enumerator = enumerator; IsBranch = isBranch; } /// /// Releases unmanaged resources held by the object. /// public override void Dispose() { if (Enumerator != null) { Enumerator.Dispose(); Enumerator = null; } } /// /// Creates a deep copy of the object. /// public override object Clone() { var clone = (BrowsePosition)MemberwiseClone(); clone.Enumerator = Enumerator.Clone(); return clone; } } }