├── .gitattributes ├── .gitignore ├── MTCClient ├── Client.cs ├── Component.cs ├── DataItem.cs ├── DataItemSample.cs ├── Device.cs ├── IClient.cs ├── IComponent.cs ├── IDataItem.cs ├── IDataItemSample.cs ├── IDevice.cs ├── MTCItemBase.cs ├── MTConnectSharp.csproj ├── ParseUtility.cs └── packages.config ├── MTConnectSharp.sln ├── README.md └── TestConsoleApp ├── App.config ├── ExampleConsoleApp.csproj └── Program.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # Roslyn cache directories 20 | *.ide/ 21 | 22 | # MSTest test Results 23 | [Tt]est[Rr]esult*/ 24 | [Bb]uild[Ll]og.* 25 | 26 | #NUNIT 27 | *.VisualState.xml 28 | TestResult.xml 29 | 30 | # Build Results of an ATL Project 31 | [Dd]ebugPS/ 32 | [Rr]eleasePS/ 33 | dlldata.c 34 | 35 | *_i.c 36 | *_p.c 37 | *_i.h 38 | *.ilk 39 | *.meta 40 | *.obj 41 | *.pch 42 | *.pdb 43 | *.pgc 44 | *.pgd 45 | *.rsp 46 | *.sbr 47 | *.tlb 48 | *.tli 49 | *.tlh 50 | *.tmp 51 | *.tmp_proj 52 | *.log 53 | *.vspscc 54 | *.vssscc 55 | .builds 56 | *.pidb 57 | *.svclog 58 | *.scc 59 | 60 | # Chutzpah Test files 61 | _Chutzpah* 62 | 63 | # Visual C++ cache files 64 | ipch/ 65 | *.aps 66 | *.ncb 67 | *.opensdf 68 | *.sdf 69 | *.cachefile 70 | 71 | # Visual Studio profiler 72 | *.psess 73 | *.vsp 74 | *.vspx 75 | 76 | # TFS 2012 Local Workspace 77 | $tf/ 78 | 79 | # Guidance Automation Toolkit 80 | *.gpState 81 | 82 | # ReSharper is a .NET coding add-in 83 | _ReSharper*/ 84 | *.[Rr]e[Ss]harper 85 | *.DotSettings.user 86 | 87 | # JustCode is a .NET coding addin-in 88 | .JustCode 89 | 90 | # TeamCity is a build add-in 91 | _TeamCity* 92 | 93 | # DotCover is a Code Coverage Tool 94 | *.dotCover 95 | 96 | # NCrunch 97 | _NCrunch_* 98 | .*crunch*.local.xml 99 | 100 | # MightyMoose 101 | *.mm.* 102 | AutoTest.Net/ 103 | 104 | # Web workbench (sass) 105 | .sass-cache/ 106 | 107 | # Installshield output folder 108 | [Ee]xpress/ 109 | 110 | # DocProject is a documentation generator add-in 111 | DocProject/buildhelp/ 112 | DocProject/Help/*.HxT 113 | DocProject/Help/*.HxC 114 | DocProject/Help/*.hhc 115 | DocProject/Help/*.hhk 116 | DocProject/Help/*.hhp 117 | DocProject/Help/Html2 118 | DocProject/Help/html 119 | 120 | # Click-Once directory 121 | publish/ 122 | 123 | # Publish Web Output 124 | *.[Pp]ublish.xml 125 | *.azurePubxml 126 | ## TODO: Comment the next line if you want to checkin your 127 | ## web deploy settings but do note that will include unencrypted 128 | ## passwords 129 | #*.pubxml 130 | 131 | # NuGet Packages Directory 132 | packages/* 133 | ## TODO: If the tool you use requires repositories.config 134 | ## uncomment the next line 135 | #!packages/repositories.config 136 | 137 | # Enable "build/" folder in the NuGet Packages folder since 138 | # NuGet packages use it for MSBuild targets. 139 | # This line needs to be after the ignore of the build folder 140 | # (and the packages folder if the line above has been uncommented) 141 | !packages/build/ 142 | 143 | # Windows Azure Build Output 144 | csx/ 145 | *.build.csdef 146 | 147 | # Windows Store app package directory 148 | AppPackages/ 149 | 150 | # Others 151 | sql/ 152 | *.Cache 153 | ClientBin/ 154 | [Ss]tyle[Cc]op.* 155 | ~$* 156 | *~ 157 | *.dbmdl 158 | *.dbproj.schemaview 159 | *.pfx 160 | *.publishsettings 161 | node_modules/ 162 | 163 | # RIA/Silverlight projects 164 | Generated_Code/ 165 | 166 | # Backup & report files from converting an old project file 167 | # to a newer Visual Studio version. Backup files are not needed, 168 | # because we have git ;-) 169 | _UpgradeReport_Files/ 170 | Backup*/ 171 | UpgradeLog*.XML 172 | UpgradeLog*.htm 173 | 174 | # SQL Server files 175 | *.mdf 176 | *.ldf 177 | 178 | # Business Intelligence projects 179 | *.rdl.data 180 | *.bim.layout 181 | *.bim_*.settings 182 | 183 | # Microsoft Fakes 184 | FakesAssemblies/ 185 | 186 | # LightSwitch generated files 187 | GeneratedArtifacts/ 188 | _Pvt_Extensions/ 189 | ModelManifest.xml -------------------------------------------------------------------------------- /MTCClient/Client.cs: -------------------------------------------------------------------------------- 1 | using RestSharp; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Timers; 8 | using System.Xml.Linq; 9 | 10 | namespace MTConnectSharp 11 | { 12 | /// 13 | /// Connects to a single agent and streams data from it. 14 | /// 15 | public class MTConnectClient : IMTConnectClient, IDisposable 16 | { 17 | /// 18 | /// The probe response has been recieved and parsed 19 | /// 20 | public event EventHandler ProbeCompleted; 21 | 22 | /// 23 | /// The base uri of the agent 24 | /// 25 | public string AgentUri { get; set; } 26 | 27 | /// 28 | /// Time in milliseconds between sample queries when simulating a streaming connection 29 | /// 30 | public TimeSpan UpdateInterval { get; set; } 31 | 32 | /// 33 | /// Devices on the connected agent 34 | /// 35 | public ReadOnlyObservableCollection Devices 36 | { 37 | get; 38 | private set; 39 | } 40 | private ObservableCollection _devices; 41 | 42 | /// 43 | /// Dictionary Reference to all data items by id for better performance when streaming 44 | /// 45 | private Dictionary _dataItemsDictionary = new Dictionary(); 46 | 47 | /// 48 | /// RestSharp RestClient 49 | /// 50 | private RestClient _restClient; 51 | 52 | /// 53 | /// Not actually parsing multipart stream - this timer fires sample queries to simulate streaming 54 | /// 55 | private Timer _streamingTimer; 56 | 57 | /// 58 | /// Last sequence number read from current or sample 59 | /// 60 | private long _lastSequence; 61 | 62 | private bool _probeStarted = false; 63 | private bool _probeCompleted = false; 64 | 65 | /// 66 | /// Initializes a new Client 67 | /// 68 | public MTConnectClient() 69 | { 70 | UpdateInterval = TimeSpan.FromMilliseconds(2000); 71 | 72 | _devices = new ObservableCollection(); 73 | Devices = new ReadOnlyObservableCollection(_devices); 74 | } 75 | 76 | /// 77 | /// Starts sample polling and updating DataItem values as they change 78 | /// 79 | public void StartStreaming() 80 | { 81 | if (_streamingTimer?.Enabled == true) 82 | { 83 | return; 84 | } 85 | 86 | GetCurrentState(); 87 | 88 | _streamingTimer = new Timer(UpdateInterval.TotalMilliseconds); 89 | _streamingTimer.Elapsed += StreamingTimerElapsed; 90 | _streamingTimer.Start(); 91 | } 92 | 93 | /// 94 | /// Stops sample polling 95 | /// 96 | public void StopStreaming() 97 | { 98 | _streamingTimer.Stop(); 99 | } 100 | 101 | /// 102 | /// Gets current response and updates DataItems 103 | /// 104 | public void GetCurrentState() 105 | { 106 | if (!_probeCompleted) 107 | { 108 | throw new InvalidOperationException("Cannot get DataItem values. Agent has not been probed yet."); 109 | } 110 | 111 | var request = new RestRequest 112 | { 113 | Resource = "current" 114 | }; 115 | _restClient.ExecuteAsync(request, (a) => ParseStream(a)); 116 | } 117 | 118 | /// 119 | /// Gets probe response from the agent and populates the devices collection 120 | /// 121 | public void Probe() 122 | { 123 | if (_probeStarted && !_probeCompleted) 124 | { 125 | throw new InvalidOperationException("Cannot start a new Probe when one is still running."); 126 | } 127 | 128 | _restClient = new RestClient 129 | { 130 | BaseUrl = new Uri(AgentUri) 131 | }; 132 | 133 | var request = new RestRequest 134 | { 135 | Resource = "probe" 136 | }; 137 | 138 | try 139 | { 140 | _probeStarted = true; 141 | _restClient.ExecuteAsync(request, r => ParseProbeResponse(r)); 142 | } 143 | catch (Exception ex) 144 | { 145 | _probeStarted = false; 146 | throw new Exception("Probe request failed.\nAgent Uri: " + AgentUri, ex); 147 | } 148 | } 149 | 150 | /// 151 | /// Parses IRestResponse from a probe command into a Device collection 152 | /// 153 | /// An IRestResponse from a probe command 154 | private void ParseProbeResponse(IRestResponse response) 155 | { 156 | var xdoc = XDocument.Load(new StringReader(response.Content)); 157 | if (_devices.Any()) 158 | _devices.Clear(); 159 | 160 | _devices.AddRange(xdoc.Descendants() 161 | .Where(d => d.Name.LocalName == "Devices") 162 | .Take(1) // needed? 163 | .SelectMany(d => d.Elements()) 164 | .Select(d => new Device(d))); 165 | 166 | BuildDataItemDictionary(); 167 | 168 | _probeCompleted = true; 169 | _probeStarted = false; 170 | ProbeCompletedHandler(); 171 | } 172 | 173 | /// 174 | /// Loads DataItemRefList with all data items from all devices 175 | /// 176 | private void BuildDataItemDictionary() 177 | { 178 | _dataItemsDictionary = _devices.SelectMany(d => 179 | d.DataItems.Concat(GetAllDataItems(d.Components)) 180 | ).ToDictionary(i => i.Id, i => i); 181 | } 182 | 183 | /// 184 | /// Recursive function to get DataItems list from a Component collection 185 | /// 186 | /// Collection of Components 187 | /// Collection of DataItems from passed Component collection 188 | private static List GetAllDataItems(IReadOnlyList components) 189 | { 190 | var queue = new Queue(components); 191 | var dataItems = new List(); 192 | while(queue.Count > 0) 193 | { 194 | var component = queue.Dequeue(); 195 | foreach (var c in component.Components) 196 | queue.Enqueue(c); 197 | dataItems.AddRange(component.DataItems); 198 | } 199 | return dataItems; 200 | } 201 | 202 | private void StreamingTimerElapsed(object sender, ElapsedEventArgs e) 203 | { 204 | var request = new RestRequest 205 | { 206 | Resource = "sample" 207 | }; 208 | request.AddParameter("at", _lastSequence + 1); 209 | _restClient.ExecuteAsync(request, r => ParseStream(r)); 210 | } 211 | 212 | /// 213 | /// Parses response from a current or sample request, updates changed data items and fires events 214 | /// 215 | /// IRestResponse from the MTConnect request 216 | private void ParseStream(IRestResponse response) 217 | { 218 | using (StringReader sr = new StringReader(response.Content)) 219 | { 220 | var xdoc = XDocument.Load(sr); 221 | 222 | _lastSequence = Convert.ToInt64(xdoc.Descendants().First(e => e.Name.LocalName == "Header") 223 | .Attribute("lastSequence").Value); 224 | 225 | var xmlDataItems = xdoc.Descendants() 226 | .Where(e => e.Attributes().Any(a => a.Name.LocalName == "dataItemId")); 227 | if (xmlDataItems.Any()) 228 | { 229 | var dataItems = xmlDataItems.Select(e => new { 230 | id = e.Attribute("dataItemId").Value, 231 | timestamp = DateTime.Parse(e.Attribute("timestamp").Value, null, 232 | System.Globalization.DateTimeStyles.RoundtripKind), 233 | value = e.Value 234 | }) 235 | .OrderBy(i => i.timestamp) 236 | .ToList(); 237 | 238 | foreach (var item in dataItems) 239 | { 240 | var dataItem = _dataItemsDictionary[item.id]; 241 | var sample = new DataItemSample(item.value.ToString(), item.timestamp); 242 | dataItem.AddSample(sample); 243 | } 244 | } 245 | } 246 | } 247 | 248 | private void ProbeCompletedHandler() 249 | { 250 | ProbeCompleted?.Invoke(this, new EventArgs()); 251 | } 252 | 253 | /// 254 | /// Disposes unmanaged resources 255 | /// 256 | public void Dispose() 257 | { 258 | _streamingTimer?.Dispose(); 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /MTCClient/Component.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.Xml.Linq; 4 | 5 | namespace MTConnectSharp 6 | { 7 | /// 8 | /// Represents a component in the probe response document 9 | /// 10 | public class Component : MTCItemBase, IComponent 11 | { 12 | /// 13 | /// The component type 14 | /// 15 | public string Type { get; set; } 16 | 17 | /// 18 | /// Value of the nativeName attribute 19 | /// 20 | public string NativeName { get; set; } 21 | 22 | /// 23 | /// The DataItems which belong to this component 24 | /// 25 | private ObservableCollection _dataItems; 26 | 27 | /// 28 | /// The Components which belong to this component 29 | /// 30 | private ObservableCollection _components; 31 | 32 | /// 33 | /// Array of the DataItems collection for COM Interop 34 | /// 35 | public ReadOnlyObservableCollection DataItems 36 | { 37 | get; private set; 38 | } 39 | 40 | /// 41 | /// Array of the Components collection for COM Interop 42 | /// 43 | public ReadOnlyObservableCollection Components 44 | { 45 | get; 46 | private set; 47 | } 48 | 49 | /// 50 | /// Creates a new component 51 | /// 52 | internal Component(XElement xmlComponent) 53 | { 54 | Type = xmlComponent.Name?.ToString() ?? string.Empty; 55 | Id = ParseUtility.GetAttribute(xmlComponent, "id"); 56 | Name = ParseUtility.GetAttribute(xmlComponent, "name"); 57 | if (string.IsNullOrEmpty(Name)) 58 | Name = Id; 59 | NativeName = ParseUtility.GetAttribute(xmlComponent, "nativeName"); 60 | 61 | _dataItems = new ObservableCollection(ParseUtility.GetDataItems(xmlComponent)); 62 | DataItems = new ReadOnlyObservableCollection(_dataItems); 63 | 64 | _components = new ObservableCollection(ParseUtility.GetComponents(xmlComponent)); 65 | Components = new ReadOnlyObservableCollection(_components); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /MTCClient/DataItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Xml.Linq; 6 | 7 | namespace MTConnectSharp 8 | { 9 | /// 10 | /// Represents a DataItem in the MTConnect probe response 11 | /// 12 | public class DataItem : MTCItemBase, IDataItem 13 | { 14 | /// 15 | /// DataItemSample collection as a Queue for correct circular buffer behavior 16 | /// 17 | private ObservableCollection _dataItemSamples = new ObservableCollection(); 18 | 19 | /// 20 | /// Value of the category attribute 21 | /// 22 | public string Category { get; set; } 23 | 24 | /// 25 | /// Value of the type attribute 26 | /// 27 | public string Type { get; set; } 28 | 29 | /// 30 | /// Value of the subType attribute 31 | /// 32 | public string SubType { get; set; } 33 | 34 | /// 35 | /// Value of the units attribute 36 | /// 37 | public string Units { get; set; } 38 | 39 | /// 40 | /// Value of the nativeUnits attribute 41 | /// 42 | public string NativeUnits { get; set; } 43 | 44 | /// 45 | /// The maximum number of samples to keep in the value buffer 46 | /// 47 | public int BufferSize { get; set; } 48 | 49 | /// 50 | /// The value immediately before the value 51 | /// 52 | public DataItemSample PreviousSample 53 | { 54 | get 55 | { 56 | return _dataItemSamples.Skip(Math.Max(1, _dataItemSamples.Count - 1)).FirstOrDefault(); 57 | } 58 | } 59 | 60 | /// 61 | /// The current value of this DataItem 62 | /// 63 | public DataItemSample CurrentSample 64 | { 65 | get 66 | { 67 | return _dataItemSamples.Last(); 68 | } 69 | } 70 | 71 | /// 72 | /// Every value in the buffer 73 | /// 74 | public ReadOnlyObservableCollection SampleHistory 75 | { 76 | get; 77 | private set; 78 | } 79 | 80 | /// 81 | /// Creates a new DataItem 82 | /// 83 | /// The XElement which defines the DataItem 84 | internal DataItem(XElement xmlDataItem) 85 | { 86 | SampleHistory = new ReadOnlyObservableCollection(_dataItemSamples); 87 | 88 | BufferSize = 100; 89 | Id = ParseUtility.GetAttribute(xmlDataItem, "id"); 90 | Name = ParseUtility.GetAttribute(xmlDataItem, "name"); 91 | Category = ParseUtility.GetAttribute(xmlDataItem, "category"); 92 | Type = ParseUtility.GetAttribute(xmlDataItem, "type"); 93 | SubType = ParseUtility.GetAttribute(xmlDataItem, "subType"); 94 | Units = ParseUtility.GetAttribute(xmlDataItem, "units"); 95 | NativeUnits = ParseUtility.GetAttribute(xmlDataItem, "nativeUnits"); 96 | } 97 | 98 | /// 99 | /// Adds a sample to the value buffer and removes the oldest value if the buffer is full 100 | /// 101 | /// The new sample to add 102 | internal void AddSample(DataItemSample newSample) 103 | { 104 | _dataItemSamples.Add(newSample); 105 | _dataItemSamples.RemoveRange(0, Math.Max(0, _dataItemSamples.Count - BufferSize)); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /MTCClient/DataItemSample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MTConnectSharp 4 | { 5 | /// 6 | /// A single value from a current or sample response 7 | /// 8 | public class DataItemSample : MTConnectSharp.IDataItemSample 9 | { 10 | /// 11 | /// The value of the sample 12 | /// 13 | public string Value { get; set; } 14 | 15 | /// 16 | /// The timestamp of the sample 17 | /// 18 | public DateTime TimeStamp { get; set; } 19 | 20 | /// 21 | /// Creates a new sample with the current time as the timestamp 22 | /// 23 | /// Value of the sample 24 | internal DataItemSample(string value) 25 | { 26 | TimeStamp = DateTime.Now; 27 | Value = value; 28 | } 29 | 30 | /// 31 | /// Creates a new sample 32 | /// 33 | /// Value of the sample 34 | /// Timestamp of the sample 35 | internal DataItemSample(string value, DateTime timestamp) 36 | { 37 | TimeStamp = timestamp; 38 | Value = value; 39 | } 40 | 41 | /// 42 | /// Returns the Value 43 | /// 44 | public override string ToString() 45 | { 46 | return Value; 47 | } 48 | } 49 | 50 | } 51 | 52 | -------------------------------------------------------------------------------- /MTCClient/Device.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.Linq; 4 | using System.Xml.Linq; 5 | 6 | namespace MTConnectSharp 7 | { 8 | /// 9 | /// Represents a device in the MTConnect probe response 10 | /// 11 | public class Device : MTCItemBase, IDevice 12 | { 13 | /// 14 | /// Description of the device 15 | /// 16 | public string Description { get; set; } 17 | 18 | /// 19 | /// Manufacturer of the device 20 | /// 21 | public string Manufacturer { get; set; } 22 | 23 | /// 24 | /// Serial number of the device 25 | /// 26 | public string SerialNumber { get; set; } 27 | 28 | /// 29 | /// The DataItems which are direct children of the device 30 | /// 31 | private ObservableCollection _dataItems = new ObservableCollection(); 32 | 33 | /// 34 | /// The components which are direct children of the device 35 | /// 36 | private ObservableCollection _components = new ObservableCollection(); 37 | 38 | /// 39 | /// Array of the DataItems collection for COM Interop 40 | /// 41 | public ReadOnlyObservableCollection DataItems 42 | { 43 | get; 44 | private set; 45 | } 46 | 47 | /// 48 | /// Array of the Components collection for COM Interop 49 | /// 50 | public ReadOnlyObservableCollection Components 51 | { 52 | get; 53 | private set; 54 | } 55 | 56 | /// 57 | /// Creates a new device from an MTConnect XML device node 58 | /// 59 | /// The MTConnect XML node which defines the device 60 | internal Device(XElement xElem = null) 61 | { 62 | DataItems = new ReadOnlyObservableCollection(_dataItems); 63 | Components = new ReadOnlyObservableCollection(_components); 64 | 65 | if (xElem?.Name.LocalName == "Device") 66 | { 67 | // Populate basic fields 68 | Id = ParseUtility.GetAttribute(xElem, "id"); 69 | Name = ParseUtility.GetAttribute(xElem, "name"); 70 | 71 | var descXml = xElem.Descendants().First(x => x.Name.LocalName == "Description"); 72 | Description = descXml.Value ?? string.Empty; 73 | Manufacturer = ParseUtility.GetAttribute(descXml, "manufacturer"); 74 | SerialNumber = ParseUtility.GetAttribute(descXml, "serialNumber"); 75 | 76 | _dataItems.AddRange(ParseUtility.GetDataItems(xElem)); 77 | _components.AddRange(ParseUtility.GetComponents(xElem)); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /MTCClient/IClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace MTConnectSharp 6 | { 7 | public interface IMTConnectClient 8 | { 9 | string AgentUri { get; set; } 10 | void Probe(); 11 | void StartStreaming(); 12 | void StopStreaming(); 13 | void GetCurrentState(); 14 | ReadOnlyObservableCollection Devices { get; } 15 | TimeSpan UpdateInterval { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MTCClient/IComponent.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace MTConnectSharp 4 | { 5 | public interface IComponent 6 | { 7 | ReadOnlyObservableCollection Components { get; } 8 | ReadOnlyObservableCollection DataItems { get; } 9 | string Type { get; } 10 | string Id { get; } 11 | string Name { get; } 12 | string LongName { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MTCClient/IDataItem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace MTConnectSharp 4 | { 5 | public interface IDataItem 6 | { 7 | int BufferSize { get; set; } 8 | DataItemSample PreviousSample { get; } 9 | DataItemSample CurrentSample { get; } 10 | ReadOnlyObservableCollection SampleHistory { get; } 11 | string Id { get; } 12 | string Name { get; } 13 | string LongName { get; } 14 | string Category { get; } 15 | string Type { get; } 16 | string SubType { get; } 17 | string Units { get; } 18 | string NativeUnits { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MTCClient/IDataItemSample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MTConnectSharp 4 | { 5 | public interface IDataItemSample 6 | { 7 | DateTime TimeStamp { get; } 8 | string ToString(); 9 | string Value { get; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MTCClient/IDevice.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace MTConnectSharp 4 | { 5 | public interface IDevice 6 | { 7 | ReadOnlyObservableCollection Components { get; } 8 | ReadOnlyObservableCollection DataItems { get; } 9 | string Description { get; } 10 | string Manufacturer { get; } 11 | string SerialNumber { get; } 12 | string Id { get; } 13 | string Name { get; } 14 | string LongName { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MTCClient/MTCItemBase.cs: -------------------------------------------------------------------------------- 1 | namespace MTConnectSharp 2 | { 3 | /// 4 | /// The base MTConnect Item Class 5 | /// 6 | public abstract class MTCItemBase 7 | { 8 | /// 9 | /// Value of the id attribute 10 | /// 11 | public string Id { get; set; } 12 | 13 | /// 14 | /// Value of the name attribute 15 | /// 16 | public string Name { get; set; } 17 | 18 | /// 19 | /// Name and id of the item as a formatted string 20 | /// 21 | public string LongName 22 | { 23 | get 24 | { 25 | return $"{Name}({Id})"; 26 | } 27 | } 28 | 29 | /// 30 | /// Formatted string describing the item 31 | /// 32 | public override string ToString() 33 | { 34 | try 35 | { 36 | return $"{Name}({Id})"; 37 | } 38 | catch 39 | { 40 | return base.ToString(); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MTCClient/MTConnectSharp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | netcoreapp2.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /MTCClient/ParseUtility.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.Linq; 4 | using System.Xml.Linq; 5 | 6 | namespace MTConnectSharp 7 | { 8 | internal static class ParseUtility 9 | { 10 | public static void AddRange(this ObservableCollection collection, IEnumerable items) 11 | { 12 | foreach (var i in items) collection.Add(i); 13 | } 14 | 15 | public static void RemoveRange(this ObservableCollection collection, int start, int count) 16 | { 17 | for(var i = 0; i < count && i < collection.Count; ++i) 18 | collection.RemoveAt(start); 19 | } 20 | 21 | /// 22 | /// Gets a named attribute from an XElement 23 | /// 24 | /// The XElement to parse 25 | /// The attribute name 26 | /// Attribute value or an empty string if not found 27 | public static string GetAttribute(XElement xElem, string Attribute) 28 | { 29 | if (xElem.Attribute(Attribute) != null) 30 | { 31 | return xElem.Attribute(Attribute).Value; 32 | } 33 | return string.Empty; 34 | } 35 | 36 | /// 37 | /// Builds a collection of DataItems from an XElement 38 | /// 39 | /// The XElement to parse 40 | /// A Collection of DataItems 41 | public static List GetDataItems(XElement xElem) 42 | { 43 | return xElem.Elements() 44 | .Where(e => e.Name.LocalName == "DataItems") 45 | .Take(1) // needed? 46 | .SelectMany(d => d.Elements()) 47 | .Select(d => new DataItem(d)) 48 | .ToList(); 49 | } 50 | 51 | /// 52 | /// Builds a collection of Components from an XElement 53 | /// 54 | /// The XElement to parse 55 | /// A Collection of Components 56 | public static List GetComponents(XElement xElem) 57 | { 58 | return xElem.Elements() 59 | .Where(e => e.Name.LocalName == "Components") 60 | .Take(1) // needed? 61 | .SelectMany(d => d.Elements()) 62 | .Select(d => new Component(d)) 63 | .ToList(); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /MTCClient/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /MTConnectSharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2046 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MTConnectSharp", "MTCClient\MTConnectSharp.csproj", "{130CF922-9476-4459-952D-69B519B3822C}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleConsoleApp", "TestConsoleApp\ExampleConsoleApp.csproj", "{5D2099EA-FE35-4653-8978-5056626934CC}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F0D9D817-054E-4CDE-8D1A-C60F9D80A835}" 11 | ProjectSection(SolutionItems) = preProject 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {130CF922-9476-4459-952D-69B519B3822C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {130CF922-9476-4459-952D-69B519B3822C}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {130CF922-9476-4459-952D-69B519B3822C}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {130CF922-9476-4459-952D-69B519B3822C}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {5D2099EA-FE35-4653-8978-5056626934CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {5D2099EA-FE35-4653-8978-5056626934CC}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {5D2099EA-FE35-4653-8978-5056626934CC}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {5D2099EA-FE35-4653-8978-5056626934CC}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {C4E245DE-9EA3-45DA-B7E0-0B77136E5D74} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MTConnectSharp 2 | A Simple MTConnect C# Client Library 3 | 4 | Dependencies: RestSharp open source C# REST client library, referenced in the package.config file. 5 | 6 | This library was originally written to monitor individual data items as they changed to better facilitate rule development in the early days of MTconnect. As such it was written to be as simple as possible, so it does not support much beyond simple probe parsing and quasi-streaming the CDATA of dataitem tags as they change. 7 | 8 | The TestConsoleApp project provides a basic example of streaming data from agent.mtconnect.org. 9 | 10 | Because MTConnect is an HTTP protocol, this library handles all communication asynchronously. There are events which an application can subscribe to and react accordingly after successful responses. 11 | 12 | ## Usage Example 13 | 14 | The entry point is the `MTConnectClient` class, which can be initialized via the constructor: 15 | 16 | var client = new MTConnectSharp.MTConnectClient() 17 | { 18 | AgentUri = "http://agent.mtconnect.org", 19 | UpdateInterval = TimeSpan.FromSeconds(.5) 20 | }; 21 | 22 | The above code block initializes the client, probes the agent and populate the `Devices` collection of the `MTConnectClient`. When the probe parsing is complete the `ProbeCompleted` event fires. 23 | 24 | The `Devices` collection is observable and may contain `Component`s and `DataItem`s, and `Component`s may contain `Component`s and `DataItem`s, but `DataItem`s do not have child `DataItem`s or `Component`s, only `DataItemSample`s, which provide `Value`s and `Timestamp`s. All collections are exposed readonly observable collections to prevent modification and support change notification. 25 | 26 | After the probe is complete, we can start reading data items. There are two ways to initiate a data pull from the Agent: current and sample. `GetCurrentState()` sends a current command and processes the data, and `StartStreaming()` begins with a current, then requests samples at the `UpdateInterval` to get values for any DataItems which have changed. 27 | 28 | **Do not use repeated `GetCurrentState()` calls for streaming. It will burden the Agent and the client unneccessarily.** 29 | 30 | The `ReadOnlyObservableCollection` members of `Device` and `Component` will fire a `CollectionChanged` event when a new `DataItemSample` is added to or removed from its buffer. 31 | 32 | ## Known Issues ## 33 | 34 | * There are no tests. This project needs tests before further refactoring and features can be added. 35 | * Since this version is using the Net Core framework, there is no support for COM/VBA. 36 | * Potential error points do not have exception wrappers to help identify them when debugging. 37 | 38 | ## Future Enhancements ## 39 | 40 | * Add support for detailed events and conditions separate from sample-based data items 41 | * Support assets and asset change events 42 | -------------------------------------------------------------------------------- /TestConsoleApp/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /TestConsoleApp/ExampleConsoleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /TestConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Collections.Specialized; 5 | using System.Linq; 6 | using Terminal.Gui; 7 | 8 | namespace TestConsoleApp 9 | { 10 | /// 11 | /// Basic example using MTCSharp to stream data from an agent 12 | /// 13 | class Program 14 | { 15 | static HashSet Registered = new HashSet(); 16 | 17 | static Dictionary> Hellos = new Dictionary>(); 18 | static Dictionary> Goodbyes = new Dictionary>(); 19 | 20 | static void AddHelloGoodbye(Action hello, Action goodbye) 21 | { 22 | Hellos.Add(typeof(T), o => hello((T)o)); 23 | Goodbyes.Add(typeof(T), o => goodbye((T)o)); 24 | } 25 | 26 | static Dictionary Handlers = 27 | new Dictionary(); 28 | 29 | static NotifyCollectionChangedEventHandler MakeHandler() 30 | { 31 | var t = typeof(T); 32 | if (!Handlers.ContainsKey(t)) 33 | { 34 | Handlers.Add(t, (sender, info) => 35 | { 36 | switch (info.Action) 37 | { 38 | case NotifyCollectionChangedAction.Add: 39 | case NotifyCollectionChangedAction.Remove: 40 | case NotifyCollectionChangedAction.Replace: 41 | case NotifyCollectionChangedAction.Reset: 42 | if (info.NewItems != null) 43 | { 44 | foreach (T item in info.NewItems) 45 | { 46 | Hellos[typeof(T)](item); 47 | } 48 | } 49 | if (info.OldItems != null) 50 | { 51 | foreach (T item in info.OldItems) 52 | { 53 | Goodbyes[typeof(T)](item); 54 | } 55 | } 56 | break; 57 | 58 | case NotifyCollectionChangedAction.Move: 59 | break; 60 | } 61 | }); 62 | } 63 | 64 | return Handlers[t]; 65 | } 66 | 67 | static void Attach(ReadOnlyObservableCollection collection) 68 | { 69 | if (!Registered.Contains(collection)) 70 | { 71 | Registered.Add(collection); 72 | (collection as INotifyCollectionChanged).CollectionChanged += MakeHandler(); 73 | } 74 | foreach (var t in collection) 75 | { 76 | Hellos[typeof(T)](t); 77 | } 78 | } 79 | 80 | static void Detach(ReadOnlyObservableCollection collection) 81 | { 82 | if (Registered.Contains(collection)) 83 | { 84 | Registered.Remove(collection); 85 | (collection as INotifyCollectionChanged).CollectionChanged -= MakeHandler(); 86 | } 87 | foreach (var t in collection) 88 | { 89 | Goodbyes[typeof(T)](t); 90 | } 91 | } 92 | 93 | static void Main(string[] args) 94 | { 95 | Console.WriteLine("Hello World!"); 96 | var client = new MTConnectSharp.MTConnectClient() 97 | { 98 | AgentUri = "http://agent.mtconnect.org", 99 | UpdateInterval = TimeSpan.FromSeconds(.5) 100 | }; 101 | 102 | client.ProbeCompleted += (sender, info) => { 103 | var items = client.Devices 104 | .SelectMany(d => d.DataItems.Select(i => new { d = d.LongName, i = i.LongName })) 105 | .ToArray(); 106 | 107 | Console.WriteLine($"Number of DataItems: {items.Count()}"); 108 | 109 | client.StartStreaming(); 110 | }; 111 | 112 | AddHelloGoodbye( 113 | d => { Attach(d.Components); Attach(d.DataItems); }, 114 | d => { Detach(d.Components); Detach(d.DataItems); } 115 | ); 116 | AddHelloGoodbye( 117 | c => { Attach(c.Components); Attach(c.DataItems); }, 118 | c => { Detach(c.Components); Detach(c.DataItems); } 119 | ); 120 | AddHelloGoodbye( 121 | i => Attach(i.SampleHistory), 122 | i => Detach(i.SampleHistory) 123 | ); 124 | var samples = 0L; 125 | AddHelloGoodbye( 126 | s => samples++, 127 | s => samples-- 128 | ); 129 | 130 | /* UI */ 131 | Application.Init(); 132 | var top = Application.Top; 133 | var win = new Window(new Rect(0, 1, top.Frame.Width, top.Frame.Height - 1), "MyApp"); 134 | top.Add(win); 135 | 136 | var count = new Label(14, 1, " "); 137 | win.Add(new Label(3, 1, "Samples:"), count); 138 | Application.MainLoop.AddTimeout(TimeSpan.FromSeconds(.25), m => { 139 | count.Text = samples.ToString(); 140 | return true; 141 | }); 142 | 143 | Attach(client.Devices); 144 | client.Probe(); 145 | 146 | Application.Run(); 147 | } 148 | } 149 | } 150 | --------------------------------------------------------------------------------