/* ======================================================================== * Copyright (c) 2005-2019 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Threading; using Opc.Ua; namespace Quickstarts.DataAccessServer { /// /// An object that provides access to the underlying system. /// public class UnderlyingSystem : IDisposable { #region Constructors /// /// Initializes a new instance of the class. /// public UnderlyingSystem() { m_blocks = new Dictionary(); } #endregion #region IDisposable Members /// /// The finializer implementation. /// ~UnderlyingSystem() { Dispose(false); } /// /// Frees any unmanaged reblocks. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// An overrideable version of the Dispose. /// protected virtual void Dispose(bool disposing) { if (disposing) { if (m_simulationTimer != null) { m_simulationTimer.Dispose(); m_simulationTimer = null; } } } #endregion #region Public Members /// /// A database which stores all known block paths. /// /// /// These are hardcoded for an example but the real data could come from a DB, /// a file or any other system accessed with a non-UA API. /// /// The name of the block is the final path element. /// The same block can have many paths. /// Each preceding element is a segment. /// private string[] s_BlockPathDatabase = new string[] { "Factory/East/Boiler1/Pipe1001", "Factory/East/Boiler1/Drum1002", "Factory/East/Boiler1/Pipe1002", "Factory/East/Boiler1/FC1001", "Factory/East/Boiler1/LC1001", "Factory/East/Boiler1/CC1001", "Factory/West/Boiler2/Pipe2001", "Factory/West/Boiler2/Drum2002", "Factory/West/Boiler2/Pipe2002", "Factory/West/Boiler2/FC2001", "Factory/West/Boiler2/LC2001", "Factory/West/Boiler2/CC2001", "Assets/Sensors/Flow/Pipe1001", "Assets/Sensors/Level/Drum1002", "Assets/Sensors/Flow/Pipe1002", "Assets/Controllers/Flow/FC1001", "Assets/Controllers/Level/LC1001", "Assets/Controllers/Custom/CC1001", "Assets/Sensors/Flow/Pipe2001", "Assets/Sensors/Level/Drum2002", "Assets/Sensors/Flow/Pipe2002", "Assets/Controllers/Flow/FC2001", "Assets/Controllers/Level/LC2001", "Assets/Controllers/Custom/CC2001", "TestData/Static/FC1001", "TestData/Static/LC1001", "TestData/Static/CC1001", "TestData/Static/FC2001", "TestData/Static/LC2001", "TestData/Static/CC2001" }; /// /// A database which stores all known blocks. /// /// /// These are hardcoded for an example but the real data could come from a DB, /// a file or any other system accessed with a non-UA API. /// /// The name of the block is the first element. /// The type of block is the second element. /// private string[] s_BlockDatabase = new string[] { "Pipe1001/FlowSensor", "Drum1002/LevelSensor", "Pipe1002/FlowSensor", "Pipe2001/FlowSensor", "Drum2002/LevelSensor", "Pipe2002/FlowSensor", "FC1001/Controller", "LC1001/Controller", "CC1001/CustomController", "FC2001/Controller", "LC2001/Controller", "CC2001/CustomController" }; /// /// Returns the segment /// /// The path to the segment. /// The segment. Null if the segment path does not exist. public UnderlyingSystemSegment FindSegment(string segmentPath) { lock (m_lock) { // check for invalid path. if (string.IsNullOrEmpty(segmentPath)) { return null; } // extract the seqment name from the path. string segmentName = segmentPath; int index = segmentPath.LastIndexOf('/'); if (index != -1) { segmentName = segmentName.Substring(index+1); } if (string.IsNullOrEmpty(segmentName)) { return null; } // see if there is a block path that includes the segment. index = segmentPath.Length; for (int ii = 0; ii < s_BlockPathDatabase.Length; ii++) { string blockPath = s_BlockPathDatabase[ii]; if (index >= blockPath.Length || blockPath[index] != '/') { continue; } // segment found - return the info. if (blockPath.StartsWith(segmentPath)) { UnderlyingSystemSegment segment = new UnderlyingSystemSegment(); segment.Id = segmentPath; segment.Name = segmentName; segment.SegmentType = null; return segment; } } return null; } } /// /// Finds the segments belonging to the specified segment. /// /// The path to the segment to search. /// The list of segments found. Null if the segment path does not exist. public IList FindSegments(string segmentPath) { lock (m_lock) { // check for invalid path. if (String.IsNullOrEmpty(segmentPath)) { segmentPath = String.Empty; } Dictionary segments = new Dictionary(); // find all block paths that start with the specified segment. int length = segmentPath.Length; for (int ii = 0; ii < s_BlockPathDatabase.Length; ii++) { string blockPath = s_BlockPathDatabase[ii]; // check for segment path prefix in block path. if (length > 0) { if (length >= blockPath.Length || blockPath[length] != '/') { continue; } if (!blockPath.StartsWith(segmentPath)) { continue; } blockPath = blockPath.Substring(length+1); } // extract segment name. int index = blockPath.IndexOf('/'); if (index != -1) { string segmentName = blockPath.Substring(0, index); if (!segments.ContainsKey(segmentName)) { string segmentId = segmentName; if (!String.IsNullOrEmpty(segmentPath)) { segmentId = segmentPath; segmentId += "/"; segmentId += segmentName; } UnderlyingSystemSegment segment = new UnderlyingSystemSegment(); segment.Id = segmentId; segment.Name = segmentName; segment.SegmentType = null; segments.Add(segmentName, segment); } } } // return list. return new List(segments.Values); } } /// /// Finds the blocks belonging to the specified segment. /// /// The path to the segment to search. /// The list of blocks found. Null if the segment path does not exist. public IList FindBlocks(string segmentPath) { lock (m_lock) { // check for invalid path. if (String.IsNullOrEmpty(segmentPath)) { segmentPath = String.Empty; } List blocks = new List(); // look up the segment in the "DB". int length = segmentPath.Length; for (int ii = 0; ii < s_BlockPathDatabase.Length; ii++) { string blockPath = s_BlockPathDatabase[ii]; // check for segment path prefix in block path. if (length > 0) { if (length >= blockPath.Length || blockPath[length] != '/') { continue; } if (!blockPath.StartsWith(segmentPath)) { continue; } blockPath = blockPath.Substring(length+1); } // check if there are no more segments in the path. int index = blockPath.IndexOf('/'); if (index == -1) { blockPath = blockPath.Substring(index+1); if (!blocks.Contains(blockPath)) { blocks.Add(blockPath); } } } return blocks; } } /// /// Finds the parent segment for the specified segment. /// /// The segment path. /// The segment path for the the parent. public UnderlyingSystemSegment FindParentForSegment(string segmentPath) { lock (m_lock) { // check for invalid segment. UnderlyingSystemSegment segment = FindSegment(segmentPath); if (segment == null) { return null; } // check for root segment. int index = segmentPath.LastIndexOf('/'); if (index == -1) { return null; } // construct the parent. UnderlyingSystemSegment parent = new UnderlyingSystemSegment(); parent.Id = segmentPath.Substring(0, index); parent.SegmentType = null; index = parent.Id.LastIndexOf('/'); if (index == -1) { parent.Name = parent.Id; } else { parent.Name = parent.Id.Substring(0, index); } return parent; } } /// /// Finds a block. /// /// The block identifier. /// The block. public UnderlyingSystemBlock FindBlock(string blockId) { UnderlyingSystemBlock block = null; lock (m_lock) { // check for invalid name. if (String.IsNullOrEmpty(blockId)) { return null; } // look for cached block. if (m_blocks.TryGetValue(blockId, out block)) { return block; } // lookup block in database. string blockType = null; int length = blockId.Length; for (int ii = 0; ii < s_BlockDatabase.Length; ii++) { blockType = s_BlockDatabase[ii]; if (length >= blockType.Length || blockType[length] != '/') { continue; } if (blockType.StartsWith(blockId)) { blockType = blockType.Substring(length+1); break; } blockType = null; } // block not found. if (blockType == null) { return null; } // create a new block. block = new UnderlyingSystemBlock(); // create the block. block.Id = blockId; block.Name = blockId; block.BlockType = blockType; m_blocks.Add(blockId, block); // add the tags based on the block type. // note that the block and tag types used here are types defined by the underlying system. // the node manager will need to map these types to UA defined types. switch (block.BlockType) { case "FlowSensor": { block.CreateTag("Measurement", UnderlyingSystemDataType.Real4, UnderlyingSystemTagType.Analog, "liters/sec", false); block.CreateTag("Online", UnderlyingSystemDataType.Integer1, UnderlyingSystemTagType.Digital, null, false); break; } case "LevelSensor": { block.CreateTag("Measurement", UnderlyingSystemDataType.Real4, UnderlyingSystemTagType.Analog, "liters", false); block.CreateTag("Online", UnderlyingSystemDataType.Integer1, UnderlyingSystemTagType.Digital, null, false); break; } case "Controller": { block.CreateTag("SetPoint", UnderlyingSystemDataType.Real4, UnderlyingSystemTagType.Normal, null, true); block.CreateTag("Measurement", UnderlyingSystemDataType.Real4, UnderlyingSystemTagType.Normal, null, false); block.CreateTag("Output", UnderlyingSystemDataType.Real4, UnderlyingSystemTagType.Normal, null, false); block.CreateTag("Status", UnderlyingSystemDataType.Integer4, UnderlyingSystemTagType.Enumerated, null, false); break; } case "CustomController": { block.CreateTag("Input1", UnderlyingSystemDataType.Real4, UnderlyingSystemTagType.Normal, null, true); block.CreateTag("Input2", UnderlyingSystemDataType.Real4, UnderlyingSystemTagType.Normal, null, true); block.CreateTag("Input3", UnderlyingSystemDataType.Real4, UnderlyingSystemTagType.Normal, null, true); block.CreateTag("Output", UnderlyingSystemDataType.Real4, UnderlyingSystemTagType.Normal, null, false); break; } } } // return the new block. return block; } /// /// Finds the segments for block. /// /// The block id. /// The list of segments. public IList FindSegmentsForBlock(string blockId) { lock (m_lock) { // check for invalid path. if (String.IsNullOrEmpty(blockId)) { return null; } List segments = new List(); // look up the segment in the block path database. int length = blockId.Length; for (int ii = 0; ii < s_BlockPathDatabase.Length; ii++) { string blockPath = s_BlockPathDatabase[ii]; if (length >= blockPath.Length || blockPath[blockPath.Length-length] != '/') { continue; } if (!blockPath.EndsWith(blockId)) { continue; } string segmentPath = blockPath.Substring(0, blockPath.Length-length-1); // construct segment. UnderlyingSystemSegment segment = new UnderlyingSystemSegment(); segment.Id = segmentPath; segment.SegmentType = null; int index = segmentPath.LastIndexOf('/'); if (index == -1) { segment.Name = segmentPath; } else { segment.Name = segmentPath.Substring(0, index); } segments.Add(segment); } return segments; } } /// /// Starts a simulation which causes the tag states to change. /// /// /// This simulation randomly activates the tags that belong to the blocks. /// Once an tag is active it has to be acknowledged and confirmed. /// Once an tag is confirmed it go to the inactive state. /// If the tag stays active the severity will be gradually increased. /// public void StartSimulation() { lock (m_lock) { if (m_simulationTimer != null) { m_simulationTimer.Dispose(); m_simulationTimer = null; } m_generator = new Opc.Ua.Test.DataGenerator(null); m_simulationTimer = new Timer(DoSimulation, null, 1000, 1000); } } /// /// Stops the simulation. /// public void StopSimulation() { lock (m_lock) { if (m_simulationTimer != null) { m_simulationTimer.Dispose(); m_simulationTimer = null; } } } #endregion #region Private Methods /// /// Simulates a block by updating the state of the tags belonging to the condition. /// private void DoSimulation(object state) { try { // get the list of blocks. List blocks = null; lock (m_lock) { m_simulationCounter++; blocks = new List(m_blocks.Values); } // run simulation for each block. for (int ii = 0; ii < blocks.Count; ii++) { blocks[ii].DoSimulation(m_simulationCounter, ii, m_generator); } } catch (Exception e) { Utils.Trace(e, "Unexpected error running simulation for system"); } } #endregion #region Private Fields private object m_lock = new object(); private Dictionary m_blocks; private Timer m_simulationTimer; private long m_simulationCounter; private Opc.Ua.Test.DataGenerator m_generator; #endregion } /// /// USed to received notifications when a tag value changes. /// delegate void TagValueChangedEventHandler(string tagName, Variant value, DateTime timestamp); /// /// USed to received notifications when the tag metadata changes. /// delegate void TagMetadataChangedEventHandler(UnderlyingSystemTag tag); }