Demo3D.PLC.Comms.Builtin Namespace |
| Class | Description | |
|---|---|---|
| BuiltinAddressSpace |
The address space for BuiltinMemAddress addresses.
| |
| BuiltinMemAddress |
A Built-in Memory Server address.
| |
| BuiltinMemoryProtocol |
The Built-in Memory Server Protocol.
| |
| BuiltinMemoryProtocolBuitinConnection |
An instance of one connection to the Built-in Memory Server.
| |
| BuiltinMixedProtocol |
The Built-in Mixed Server Protocol.
| |
| BuiltinMixedProtocolBuiltinMixedConnection |
An instance of one connection to the Built-in Mixed Server.
| |
| BuiltinNotifyMemoryProtocol |
The Built-in Notify Memory Server Protocol - a memory server with memory change notification.
| |
| BuiltinNotifyMemoryProtocolBuitinConnection |
An instance of one connection to the Built-in Notify Memory Server.
This extends the BuiltinMemoryProtocolBuitinConnection example to include the
INotifyDirectMemoryAccessService.
| |
| BuiltinTag |
A tag for accessing a Built-in Tag Server symbol.
| |
| BuiltinTagAddress |
An example class for configuring the protocol address.
Must inherit from ProtocolAddressPropertyBagEditor to use it with
ProtocolAddressEditorAttribute as below.
| |
| BuiltinTagProtocol |
The Built-in Tag Server Protocol.
| |
| BuiltinTagProtocolBuiltinConnection |
An instance of one connection to the Built-in Tag Server.
| |
| BuiltinTagServiceClient |
Exposes a tag list from the server.
| |
| BuiltinTagServiceClientTagData |
TagData from the Demo3D server.
| |
| BuiltinTagServicePeer |
Provides message passing and other utility functions for the BuiltinTagService protocol.
| |
| BuiltinTagServiceProtocol |
The Built-in Tag Service Protocol.
| |
| ExampleCombinedServer |
An example server expecting some data to be accessed by tag name, and some to be accessed by memory address.
| |
| ExampleMemoryServer |
The Built-in Memory Server.
| |
| ExampleNotifyMemoryServer |
An extended Built-in Memory Server based on ExampleMemoryServer.
| |
| ExampleTagServer |
The Built-in Tag Server.
| |
| ServerConfiguration |
An example class for configuring the protocol properties.
| |
| Symbol |
A symbol in the Built-in Tag Server symbol table.
|
| Delegate | Description | |
|---|---|---|
| BuiltinTagServiceClientTagListChangedDelegate |
Represents a method that handles the TagListChanged event.
|
| Enumeration | Description | |
|---|---|---|
| BuiltinTagServicePeerRequestType |
List of protocol request types.
|
This example is based on NetServer and the INotifyDirectTagAccessService.
using System; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Demo3D.IO; using Demo3D.Net; using Demo3D.PLC.Comms.Tag; namespace Demo3D.PLC.Comms.Builtin { /// <summary> /// A symbol in the Built-in Tag Server symbol table. /// </summary> /// <remarks> /// <para> /// A symbol in the symbol table must implement <see cref="IBrowseItem"/>. /// <see cref="BrowseItemBase"/> is a generic implementation of <see cref="IBrowseItem"/>. /// </para> /// </remarks> public class Symbol : BrowseItemBase { /// <summary> /// The data type of the symbol. /// </summary> [Description("The data type of the symbol.")] // Public properties will be displayed in the property grid when you select the symbol. public DataType DataType { get; } /// <summary> /// The access allowed for this symbol. /// </summary> /// <remarks> /// Overriding this is optional (the default implementation returns Bidirectional), but /// if the address implies the access, then returning it can help the user. /// </remarks> [Description("The access allowed for this symbol.")] public override AccessRights AllowedAccess { get; } /// <summary> /// Constructs a new symbol for the Built-in Tag Server. /// </summary> /// <param name="symbolTable">The symbol table.</param> /// <param name="name">The name of the symbol.</param> /// <param name="access">The access rights for the symbol.</param> /// <param name="dataType">The symbol data type.</param> public Symbol(IBrowseItem symbolTable, string name, AccessRights access, Type dataType) : base(symbolTable, name, true, false) { this.AllowedAccess = access; this.DataType = DataType.Typeof(dataType); } /// <summary> /// Returns the data type for this symbol. /// </summary> /// <param name="type">Returns the symbol type (or null if not known).</param> /// <param name="stronglyTyped">Return true if this type is definitive.</param> /// <remarks> /// Overriding this is optional, but it allows the symbol to dictate the tag data type. /// If the data type of a particular symbol cannot be determined, then you can return null. /// </remarks> public override void GetDataType(out DataType type, out bool stronglyTyped) { type = this.DataType; stronglyTyped = true; // set to false if the type returned is a guess/hint } } /// <summary> /// A tag for accessing a Built-in Tag Server symbol. /// </summary> /// <remarks> /// <para> /// Although a symbol table may expose many symbols, the user is likely to want to access only a subset. /// A Tag represents a symbol that the user actually wants to access. /// </para> /// <para> /// Some servers (eg OPC) access data through their 'symbol' object, and distinguish between 'connected' /// and 'disconnected' symbols. Symbol and Tag are equivalent concepts in Demo3D: a Demo3D Symbol being /// the same as an OPC Symbol, and a Demo3D Tag being the same as an OPC 'connected' Symbol. /// </para> /// <para> /// The <see cref="INotifyDirectTagAccessService"/> and <see cref="IDirectTagAccessService"/> use /// <see cref="DirectTag"/> to represent a tag. /// </para> /// </remarks> public class BuiltinTag : DirectTag { readonly Action<BuiltinTag> releaseTag; // Called to release the tag. /// <summary> /// The current value of the tag. /// </summary> public object? Value { get; set; } /// <summary> /// Constructs a new Tag. /// </summary> /// <param name="address">The tag address.</param> /// <param name="tagType">The tag data type.</param> /// <param name="releaseTag">Action to release the tag.</param> public BuiltinTag(IAddress address, DataType tagType, Action<BuiltinTag> releaseTag) : base(address, tagType) { this.releaseTag = releaseTag; } /// <summary> /// Called by Server.RunSimulator to update the value of the tag. /// </summary> /// <param name="batchNotify">An object used to batch tag value updates together.</param> /// <param name="value">The new value of the tag.</param> /// <param name="signal">A trackable object.</param> /// <remarks> /// If your server cannot detect when tag values change, then simply don't include this method, and /// make <see cref="BuiltinTagProtocol.BuiltinConnection"/> implement <see cref="IDirectTagAccessService"/> /// instead of <see cref="INotifyDirectTagAccessService"/>. /// </remarks> public void UpdateValue(BatchNotify batchNotify, object value, Signal? signal) { this.Value = value; NotifyDataChanged(batchNotify, value, signal); } /// <summary> /// Called when the tag is no longer being used. /// </summary> public override void Dispose() { releaseTag(this); } } /// <summary> /// An example class for configuring the protocol address. /// Must inherit from <see cref="ProtocolAddressPropertyBagEditor"/> to use it with /// <see cref="ProtocolAddressEditorAttribute"/> as below. /// </summary> /// <remarks> /// Skip this class entirely if your protocol does not require an address. /// </remarks> public class BuiltinTagAddress : ProtocolAddressPropertyBagEditor { string? host; /// <summary> /// The hostname of the server. /// </summary> [Description("The hostname of the server.")] // This server does not require any configuration, so this is purely an example. public string? Host { get { return host; } set { if (host != value) { host = value; NotifyPropertyChanged(); } } } /// <summary> /// Returns a ProtocolAddress that represents the address defined by this object. /// </summary> /// <returns>The protocol address according to the current setting of the editor properties.</returns> public override ProtocolAddress GetAddress() { return new ProtocolAddressBuilder("builtintag", host).Address; } /// <summary> /// Extracts protocol address properties from the ProtocolAddress given. /// </summary> /// <param name="address">The current address.</param> public override void SetAddress(ProtocolAddress address) { host = address.Host; } /// <summary> /// Returns the next property that needs to be edited to complete the address. /// </summary> /// <returns>The name of the property, or null.</returns> public override string? NextProperty() { // If host is a required property to complete the address, and it hasn't been filled in // yet, then return the Host property name. /* if (string.IsNullOrEmpty(host)) return nameof(this.Host); */ // No more properties need to be filled in. return null; } } /// <summary> /// An example class for configuring the protocol properties. /// </summary> /// <remarks> /// Skip this class entirely if your protocol does not require configuration. /// </remarks> [TypeConverter(typeof(ExpandableObjectConverter))] // required to make it show in the property grid correctly public class ServerConfiguration : INotifyPropertyChanged { int updateRate = 50; /// <summary> /// Update rate in milliseconds. /// </summary> [Description("Update rate in milliseconds.")] [DefaultValue(50)] public int UpdateRate { get { return updateRate; } set { if (updateRate != value) { updateRate = value; NotifyPropertyChanged(); } } } #region INotifyPropertyChanged /// <summary> /// Occurs when a property value changes. /// </summary> public event PropertyChangedEventHandler? PropertyChanged; /// <summary> /// Raises the PropertyChanged event. /// </summary> /// <param name="e">A PropertyChangedEventArgs that contains the event data.</param> protected virtual void NotifyPropertyChanged(PropertyChangedEventArgs e) { this.PropertyChanged?.Invoke(this, e); } /// <summary> /// Raises the PropertyChanged event. /// </summary> /// <param name="propertyName">The name of the property that has changed.</param> protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { NotifyPropertyChanged(new PropertyChangedEventArgs(propertyName)); } #endregion /// <exclude /> public override string ToString() { return "Builtin-Tag configuration"; } } /// <summary> /// The Built-in Tag Server. /// </summary> /// <remarks> /// Normally, you'd replace this class entirely with the code required to access your server. /// </remarks> public class ExampleTagServer { readonly ServerConfiguration configuration; readonly List<BuiltinTag> activeTags = new(); bool connected; /// <exclude /> public ExampleTagServer(ServerConfiguration configuration) { this.configuration = configuration; } // A simple simulator that periodically updates the values of read-only tags with random values. void RunSimulator() { var r = new Random(Environment.TickCount); string[] randomStrings = new string[] { "If","you","are","involved","in","system","commissioning","Emulate3D","PLC","Controls","Testing","is","the","only","thoroughbred","solution","for","off-line","logic","controls","testing.","Now","you","can","reduce","on-site","commissioning","time","improve","control","system","quality","accelerate","ramp","to","full","production","and","reduce","project","costs." }; object? RandomValue(Type? type) { if (type == typeof(bool)) return (r.Next() % 2) == 1; if (type == typeof(int)) return r.Next(); if (type == typeof(double)) return (double)(r.Next() / (double)(r.Next() + 1)); if (type == typeof(string)) return randomStrings[r.Next() % randomStrings.Length]; if (type == typeof(DateTime)) return DateTime.Now + new TimeSpan(r.Next() % 1000, r.Next() % 24, r.Next() % 60, r.Next() % 60); return null; } while (connected) { using (var batchNotify = new DirectTag.BatchNotify()) { lock (activeTags) { foreach (var tag in activeTags) { if (!tag.AccessRights.CanWriteToPLC()) { var value = RandomValue(tag.TagType?.ElementType); if (value != null) tag.UpdateValue(batchNotify, value, null); } } } } Thread.Sleep(configuration.UpdateRate); } } /// <summary> /// Connects to the server. /// </summary> public void Connect() { connected = true; new Thread(new ThreadStart(RunSimulator)) { IsBackground = true }.Start(); } /// <summary> /// Disconnects from the server. /// </summary> public void Disconnect() { connected = false; } /// <summary> /// Reads the symbol table from the server. /// </summary> /// <returns>The symbol table.</returns> public IBrowseItem ReadSymbols() { var symbolTable = new BrowseItemBranch(); symbolTable.Add(new Symbol(symbolTable, "Boolean", AccessRights.ReadFromPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "Boolean2", AccessRights.WriteToPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "Integer", AccessRights.ReadFromPLC, typeof(int))); symbolTable.Add(new Symbol(symbolTable, "Integer2", AccessRights.WriteToPLC, typeof(int))); symbolTable.Add(new Symbol(symbolTable, "String", AccessRights.ReadFromPLC, typeof(string))); symbolTable.Add(new Symbol(symbolTable, "Date", AccessRights.ReadFromPLC, typeof(DateTime))); symbolTable.Add(new Symbol(symbolTable, "Double", AccessRights.ReadFromPLC, typeof(double))); return symbolTable; } /// <summary> /// Returns a <see cref="BuiltinTag"/> for accessing <see cref="Symbol"/> data. /// </summary> /// <param name="symbol">The <see cref="Symbol"/> to access.</param> /// <returns>A <see cref="BuiltinTag"/> for accessing the <see cref="Symbol"/>.</returns> public BuiltinTag GetTag(Symbol symbol) { var tag = new BuiltinTag(symbol, symbol.DataType, ReleaseTag); lock (activeTags) { activeTags.Add(tag); } return tag; } /// <summary> /// Called by <see cref="BuiltinTag.Dispose"/> when the tag is no longer needed. /// </summary> /// <param name="tag">The <see cref="BuiltinTag"/> being released.</param> public void ReleaseTag(BuiltinTag tag) { lock (activeTags) { activeTags.Remove(tag); } } } /// <summary> /// The Built-in Tag Server Protocol. /// </summary> /// <remarks> /// <para> /// Must be marked with the <see cref="ProtocolAddressEditorAttribute"/> in order for NetServer to show /// this protocol in its drop-down. NetServer only looks for protocols with this attribute. /// </para> /// <para> /// If you need additional information in the protocol address in order to identify a server, then you /// should also set <see cref="ProtocolAddressEditorAttribute.Editor"/> to an instance of a public class. /// Your editor class must inherit from <see cref="ProtocolAddressPropertyBagEditor"/>. Public properties /// on your editor class will be displayed in the Add Server Wizard and in the Address properties of your /// Tag Server. /// </para> /// <para> /// To have your protocol appear in the AddServer wizard, set the <see cref="ProtocolAddressEditorAttribute.ShowInAddServer"/> /// property. /// </para> /// <para> /// This example is based on NetServer and the <see cref="INotifyDirectTagAccessService"/>. /// See <see cref="BuiltinMemoryProtocol"/> for an equivalent example <see cref="Memory.IDirectMemoryAccessService"/>. /// </para> /// </remarks> [ProtocolAddressEditor(DisplayName = "Built-in Tag Server", Editor = typeof(BuiltinTagAddress) /*, ShowInAddServer = true */)] public class BuiltinTagProtocol : Protocol { /// <summary> /// An instance of one connection to the Built-in Tag Server. /// </summary> public class BuiltinConnection : ProtocolInstance, INotifyDirectTagAccessService { readonly ServerConfiguration configuration; // optional ExampleTagServer? server; IBrowseItem? symbols; /// <summary> /// Constructs a new <see cref="ProtocolInstance"/> for the Built-in Tag Server Protocol. /// </summary> /// <param name="protocol">The protocol; a required parameter of <see cref="ProtocolInstance"/>.</param> /// <param name="head">The protocol head; a required parameter of <see cref="ProtocolInstance"/>.</param> /// <param name="peerAddress">The address of the server being connected to.</param> /// <param name="configuration">Server configuration properties.</param> /// <remarks> /// <para> /// If your connection would benefit from user configurable properties (such as IO timeout configuration) /// then you should create a public class and pass an instance of it to the 'propertyBag' parameter of /// <see cref="ProtocolInstance(Protocol, ProtocolHead, ProtocolAddress, bool, object)"/>. Public properties /// on your class will be displayed in the Connection properties of your Tag Server. /// </para> /// <para> /// <see cref="ServerConfiguration"/> is an example - it's entirely optional. You can pass null into /// <see cref="ProtocolInstance(Protocol, ProtocolHead, ProtocolAddress, bool, object)"/> instead. /// </para> /// </remarks> public BuiltinConnection(Protocol protocol, ProtocolHead head, ProtocolAddress peerAddress, ServerConfiguration configuration) : base(protocol, head, peerAddress, false, configuration) { this.configuration = configuration; } #region Connection /// <summary> /// Returns true if the connection has been established. /// </summary> protected override bool InternalRunning { get { return server != null; } } /// <summary> /// Connects to the server. /// </summary> /// <param name="sync">If true, the Task returned must be guaranteed to be complete.</param> /// <param name="flags">The flags used to open the connection.</param> /// <returns>Nothing.</returns> protected override Task InternalOpenAsync(bool sync, Flags flags) { server = new ExampleTagServer(configuration); server.Connect(); return Task.CompletedTask; } /// <summary> /// Disconnects from the server. /// </summary> protected override void InternalClose() { server?.Disconnect(); server = null; symbols = null; } #endregion #region IDirectTagAccessService /// <summary> /// Gets the servers symbol table. /// </summary> /// <param name="sync">If true, the Task returned must be guaranteed to be complete.</param> /// <returns>The root symbol.</returns> /// <remarks> /// A Tag server must return a symbol table to facilitate IO. By default NetServer will connect to /// the server (calls <see cref="InternalOpenAsync(bool, Flags)"/>) before reading the symbols. If you want /// more control over how the symbol table is accessed, then make <see cref="BuiltinConnection"/> /// implement <see cref="ISymbolTable"/>. For example, it may be possible (and more efficient) to /// read the symbols from the server without first establishing a full connection. /// </remarks> Task<IBrowseItem?> IDirectTagAccessService.GetSymbolTableAsync(bool sync) { if (symbols == null && server != null) symbols = server.ReadSymbols(); return Task.FromResult(symbols); } /// <summary> /// Returns the .Net type of the addresses expected by this protocol. /// The type must implement <see cref="IAddress"/>. /// </summary> Type IDirectTagAccessService.AddressType => typeof(Symbol); /// <summary> /// Returns an object for accessing a tag in the server. /// </summary> /// <param name="sync">If true, the Task returned must be guaranteed to be complete.</param> /// <param name="address">The address of the tag (the symbol).</param> /// <returns>A <see cref="DirectTag"/> object for accessing the tag.</returns> /// <remarks> /// The type of <paramref name="address"/> will be the type returned by /// <see cref="IDirectTagAccessService.AddressType"/> above. /// </remarks> Task<DirectTag?> IDirectTagAccessService.GetTagAsync(bool sync, IAddress address) { if (server == null) throw new ClosedException(); return Task.FromResult<DirectTag?>(server.GetTag((Symbol)address)); } /// <summary> /// Vectored read request. /// </summary> /// <param name="requests">List of requests.</param> /// <returns>Object for performing IO.</returns> VectoredRequests IVectoredTagService<DirectTag>.InternalReadV(IReadOnlyList<VectoredTagRequest<DirectTag>> requests) { return requests.ReadSequentially<BuiltinTag>(tag => tag.Value); } /// <summary> /// Vectored write request. /// </summary> /// <param name="requests">List of requests.</param> /// <returns>An object to perform IO.</returns> VectoredRequests IVectoredTagService<DirectTag>.InternalWriteV(IReadOnlyList<VectoredTagRequest<DirectTag>> requests) { return requests.WriteSequentially<BuiltinTag>((tag, value) => tag.Value = value); } /// <summary> /// Returns true if data changes can be subscribed to. /// </summary> bool INotifyDirectTagAccessService.CanSubscribe { get { return true; } } #endregion } #region Register Protocol /// <summary> /// Constructs the Built-in Tag Server Protocol. /// </summary> /// <remarks> /// <para> /// The name passed in to the <see cref="Protocol"/> constructor will become the 'scheme' part of the /// protocol address URL. /// </para> /// <para> /// This example protocol supports one service, the <see cref="INotifyDirectTagAccessService"/>. This service /// is used to implement a simple tag server, and specifically one that will notify when tag data changes. /// If your tag server does not notify when data changes, then use <see cref="IDirectTagAccessService"/> /// instead. /// </para> /// </remarks> BuiltinTagProtocol() : base("BuiltinTag", typeof(INotifyDirectTagAccessService)) { } /// <summary> /// Registers the protocol. You should call this method once from your code at start-up. /// </summary> internal static void Register() { Registry.Register(new BuiltinTagProtocol()); } #endregion /// <summary> /// Creates a new instance of the protocol. /// </summary> /// <param name="head">A required parameter of the <see cref="ProtocolInstance"/> constructor.</param> /// <param name="protocolAddress">The address of the new connection.</param> /// <returns>A new instance of the protocol.</returns> protected override ProtocolInstance NewInstance(ProtocolHead head, ProtocolAddress protocolAddress) { return new BuiltinConnection(this, head, protocolAddress, new ServerConfiguration()); } } }
This example is based on NetServer and the IDirectMemoryAccessService.
using System; using System.Collections.Generic; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Demo3D.Common; using Demo3D.IO; using Demo3D.Net; using Demo3D.PLC.Comms.Memory; using Demo3D.Xml; using Buffer = Demo3D.IO.Buffer; namespace Demo3D.PLC.Comms.Builtin { /// <summary> /// The address space for <see cref="BuiltinMemAddress"/> addresses. /// </summary> /// <remarks> /// Some servers support more than one memory bank. The address space should identify which memory bank an /// address belongs to. For example, a Siemens PLC has separate memory banks for I, Q, M, etc. /// </remarks> public class BuiltinAddressSpace : IAddressSpace { /// <summary> /// In this example, we assume that the PLC has multiple address banks identified by an integer. /// </summary> public int AddressBank { get; } /// <summary> /// Creates a new Built-in address space. /// </summary> /// <param name="addressBank">The address bank of this address space.</param> public BuiltinAddressSpace(int addressBank) { this.AddressBank = addressBank; } /// <summary> /// Creates a new address with the specified parameters. /// </summary> /// <param name="bitAddress">The address offset.</param> /// <param name="dataType">The data type of the address.</param> /// <param name="stronglyTyped">The <paramref name="dataType"/> is strongly typed.</param> /// <returns>The new address.</returns> public MemoryAddress CreateAddress(long bitAddress, DataType dataType, bool stronglyTyped) { return new BuiltinMemAddress(this, bitAddress, dataType, stronglyTyped); } #region IComparable int CompareTo(BuiltinAddressSpace? other) { if (other is null) return -1; if (ReferenceEquals(other, this)) return 0; return this.AddressBank - other.AddressBank; } int IComparable<IAddressSpace>.CompareTo(IAddressSpace? obj) { return CompareTo(obj as BuiltinAddressSpace); } int IComparable.CompareTo(object? obj) { return CompareTo(obj as BuiltinAddressSpace); } #endregion #region Equality and Object Overrides /// <summary> /// Returns true if the specified address space equals the current address space. /// </summary> /// <param name="other">The address space to compare.</param> /// <returns>True if the specified address space equals the current address space.</returns> public bool Equals(BuiltinAddressSpace? other) { if (other is null) return false; if (ReferenceEquals(other, this)) return true; return this.AddressBank == other.AddressBank; } /// <summary> /// Returns true if the specified address space equals the current address space. /// </summary> /// <param name="obj">The address space to compare.</param> /// <returns>True if the specified address space equals the current address space.</returns> public override bool Equals(object? obj) { return Equals(obj as BuiltinAddressSpace); } /// <summary> /// Returns true if the specified address space equals the current address space. /// </summary> /// <param name="obj">The address space to compare.</param> /// <returns>True if the specified address space equals the current address space.</returns> public bool Equals(IAddressSpace? obj) { return Equals(obj as BuiltinAddressSpace); } /// <summary> /// Returns a hash code that represents this address space. /// </summary> /// <returns>A hash code that represents this address space.</returns> public override int GetHashCode() { return this.AddressBank; } #endregion /// <summary> /// Returns a string representation of the specified address. /// </summary> /// <param name="address">The address to convert to a string.</param> /// <returns>A string representation of the specified address.</returns> public string ToString(IAddress address) { return address?.ToString<BuiltinMemAddress>() ?? string.Empty; } /// <exclude /> public override string ToString() { return "AddressBank = " + this.AddressBank; } } /// <summary> /// A Built-in Memory Server address. /// </summary> /// <remarks> /// <para> /// A memory server is primarily accessed by address, and so you must provide a class that knows about /// the memory addresses for the server. /// </para> /// <para> /// Addresses must inherit from <see cref="MemoryAddress"/> and include both a pointer into memory and /// the length of the memory area being addressed. Optionally an address can also define the data type /// of the data being addressed, in which case it inherits from <see cref="TypedMemoryAddress"/>. /// </para> /// </remarks> [E3DSerializer("BuiltinAddress")] [TypeConverter(typeof(BuiltinAddressConverter))] public class BuiltinMemAddress : TypedMemoryAddress { /// <summary> /// Constructs a new address for the Built-in Memory Server. /// </summary> /// <param name="addressSpace">The address bank.</param> /// <param name="bitAddress">The offset (in bits) of the address into the address space.</param> /// <param name="dataType">The type of the address.</param> /// <param name="stronglyTyped">The address is strongly typed.</param> public BuiltinMemAddress(BuiltinAddressSpace addressSpace, long bitAddress, DataType dataType, bool stronglyTyped) : base(addressSpace, bitAddress, dataType, stronglyTyped) { } /// <summary> /// Constructs a new address for the Built-in Memory Server. /// </summary> /// <param name="addressBank">The address bank.</param> /// <param name="bitAddress">The offset (in bits) of the address into the address space.</param> /// <param name="numBits">The size of the address (in bits).</param> public BuiltinMemAddress(int addressBank, long bitAddress, long numBits) : base(new BuiltinAddressSpace(addressBank), bitAddress, numBits) { } /// <summary> /// The default constructor for deserializing addresses. /// </summary> public BuiltinMemAddress() { } /// <summary> /// Parse a string address. /// </summary> /// <param name="s">Address in the format "bank:address:length".</param> /// <returns>An address.</returns> public static BuiltinMemAddress ParseAddress(string s) { var parts = s.Split(':'); if (parts.Length == 3) { int addressBank = int.Parse(parts[0]); long addressInBits = long.Parse(parts[1]); int lengthInBits = int.Parse(parts[2]); return new BuiltinMemAddress(addressBank, addressInBits, lengthInBits); } throw new Exception("Invalid address \"" + s + "\""); } /// <summary> /// Printable string representation of the address. /// </summary> /// <remarks> /// Must be parseable by <see cref="ParseAddress(string)"/>. /// </remarks> [Browsable(false)] public override string AccessName { get { if (accessName is null) accessName = ((BuiltinAddressSpace)this.AddressSpace).AddressBank + ":" + this.BitAddress + ":" + this.NumBits; return accessName; } } /// <summary> /// A TypeConverter for converting addresses of different types into/from a <see cref="BuiltinMemAddress"/>. /// </summary> /// <remarks> /// Must inherit from <see cref="TypeConverter"/>, but inheriting from <see cref="MemoryAddress.MemoryAddressConverter{AddressType}"/> /// instead is simpler. MemoryAddressConverter itself inherits from ExpandableObjectConverter which makes /// addresses expandable in the GUI property grid. /// </remarks> sealed class BuiltinAddressConverter : MemoryAddressConverter<BuiltinMemAddress> { /// <summary> /// Parses a string address. /// </summary> /// <param name="str">The string to parse.</param> /// <returns>The parsed address.</returns> protected override BuiltinMemAddress ParseAddress(string str) { return BuiltinMemAddress.ParseAddress(str); } } } /// <summary> /// The Built-in Memory Server. /// </summary> /// <remarks> /// Normally, you'd replace this class entirely with whatever code is required to access your server. /// </remarks> public class ExampleMemoryServer { readonly byte[] memory = new byte[10240]; bool connected; void RunSimulator() { // Periodically update the first 50 bytes of memory with random values. var r = new Random(Environment.TickCount); while (connected) { memory[0] = (byte)~memory[0]; for (int i = 1; i < 50; i++) { memory[i] = (byte)r.Next(); } Thread.Sleep(500); } } /// <summary> /// Connects to the server. /// </summary> public virtual void Connect() { connected = true; new Thread(new ThreadStart(RunSimulator)) { IsBackground = true }.Start(); } /// <summary> /// Disconnects from the server. /// </summary> public virtual void Disconnect() { connected = false; } // If your server does not offer a symbol table, then you can ignore and remove this region entirely. #region Symbol table /// <summary> /// Read the symbol table from the PLC. /// </summary> /// <param name="socket">The socket to register the symbol table with.</param> /// <returns>The browse root (or null if the symbol table can't be read).</returns> public virtual MemorySymbolTable<BuiltinMemAddress> ReadSymbolTable(ProtocolSocket socket) { // If the PLC defines structures (user data types), then use DataType.Builder to describe them. var myStructTypeBuilder = new DataType.Builder("MyStruct") { { "TestInt", typeof(int) }, { "TestDouble", typeof(double) }, { "TestString", DataType.Typeof(typeof(string), sizeBits: 80) }, // a "string(10)" }; myStructTypeBuilder.SizeBits = 192; // 24 bytes, which includes a 2 byte header for the string var myStructType = myStructTypeBuilder.Build(); // Another structure definition. var myUDT = new DataType.Builder("MyUDT") { { "BoolA", typeof(bool) }, { "BoolB", typeof(bool) }, { "BoolC", typeof(bool) }, { "StructA", myStructType }, { "StructB", myStructType }, }.Build(); // Create a symbol table using MemorySymbolTable. // You don't need to use this class, but it helps in creating symbol tables for memory servers. var symbolTable = new MemorySymbolTable<BuiltinMemAddress>() { // Some symbols with random values. // The first 50 bytes are constantly updated with random values. { "FlashBool", typeof(bool), new BuiltinAddressSpace(0), 0 }, // a bool at offset 0 bits (occupies 1 byte) { "RandInt", typeof(int), new BuiltinAddressSpace(0), 32 }, // an int at offset 32 bits // An example data structure. { "Struct", myStructType, new BuiltinAddressSpace(0), 50 * 8 }, // a symbol using the struct data type we just created, at offset 400 bits { "UDT", myUDT }, // a UDT immediately after "Struct" { "UDTArray", myUDT.MakeArray(10) }, // an array of UDT's }; // Add a branch and some symbols within the branch. _ = new MemorySymbolTable<BuiltinMemAddress>(symbolTable, "Branch") { { "BranchInt", typeof(int) }, { "BranchDouble", typeof(double) }, { "BranchArray", DataType.Typeof(typeof(int), length: 10) }, // an "int[10]" }; symbolTable.RegisterAddressMap(socket); return symbolTable; } #endregion /// <summary> /// Some memory from the server. /// </summary> /// <param name="addressBank">The address bank id.</param> /// <returns>Array of data representing memory in the server.</returns> protected virtual byte[]? GetMemoryBank(int addressBank) { return memory; } /// <summary> /// Read data from server memory. /// </summary> /// <param name="buffer">The buffer to place the data in.</param> /// <param name="addressBank">The address bank to access.</param> /// <param name="startBit">The start memory address (in bits).</param> /// <param name="lengthBits">The amount of data to read (in bits).</param> public void Read(Buffer buffer, int addressBank, long startBit, long lengthBits) { var memory = GetMemoryBank(addressBank); if (memory != null) BitsArray.CopyBits(memory, startBit, buffer.Data, (buffer.From << 3) + (startBit & 7), lengthBits); } /// <summary> /// Write data into server memory. /// </summary> /// <param name="buffer">The buffer containing the data to write.</param> /// <param name="addressBank">The address bank to access.</param> /// <param name="startBit">The start memory address (in bits).</param> /// <param name="lengthBits">The amount of data to write (in bits).</param> public void Write(Buffer buffer, int addressBank, long startBit, long lengthBits) { var memory = GetMemoryBank(addressBank); if (memory != null) BitsArray.CopyBits(buffer.Data, (buffer.From << 3) + (startBit & 7), memory, startBit, lengthBits); } } /// <summary> /// The Built-in Memory Server Protocol. /// </summary> /// <remarks> /// <para> /// Must be marked with the <see cref="ProtocolAddressEditorAttribute"/> in order for NetServer to show /// this protocol in its drop-down. NetServer only looks for protocols with this attribute. /// </para> /// <para> /// If you need additional information in the protocol address in order to identify a server, then you /// should also set <see cref="ProtocolAddressEditorAttribute.Editor"/> to an instance of a public class. /// Your editor class must inherit from <see cref="ProtocolAddressPropertyBagEditor"/>. Public properties /// on your editor class will be displayed in the Add Server Wizard and in the Address properties of your /// Tag Server. See <see cref="BuiltinTagProtocol"/> and <see cref="BuiltinTagAddress"/> for an example. /// </para> /// <para> /// This example is based on NetServer and the <see cref="IDirectMemoryAccessService"/>. /// See <see cref="BuiltinTagProtocol"/> for an equivalent example <see cref="Tag.INotifyDirectTagAccessService"/>. /// </para> /// </remarks> [ProtocolAddressEditor(DisplayName = "Built-in Memory Server")] public class BuiltinMemoryProtocol : Protocol { /// <summary> /// An instance of one connection to the Built-in Memory Server. /// </summary> public class BuitinConnection : ProtocolInstance, ISymbolTable, IDirectMemoryAccessService { /// <summary> /// The example server. /// </summary> protected ExampleMemoryServer? server; IBrowseItem? symbolTable; /// <summary> /// Constructs a new <see cref="ProtocolInstance"/> for the Built-in Memory Server Protocol. /// </summary> /// <param name="protocol">The protocol; a required parameter of <see cref="ProtocolInstance"/>.</param> /// <param name="head">The protocol head; a required parameter of <see cref="ProtocolInstance"/>.</param> /// <param name="peerAddress">The address of the server being connected to.</param> /// <remarks> /// If your connection would benefit from user configurable properties (such as IO timeout configuration) /// then you should create a public class and pass an instance of it to the 'propertyBag' parameter of /// <see cref="ProtocolInstance(Protocol, ProtocolHead, ProtocolAddress, bool, object)"/>. Public properties /// on your class will be displayed in the Connection properties of your Tag Server. /// See <see cref="ServerConfiguration"/> and <see cref="BuiltinTagProtocol.BuiltinConnection"/> for an example. /// </remarks> internal BuitinConnection(Protocol protocol, ProtocolHead head, ProtocolAddress peerAddress) : base(protocol, head, peerAddress, false, null) { } #region Connection /// <summary> /// Returns true if the connection has been established. /// </summary> protected override bool InternalRunning { get { return server != null; } } /// <summary> /// Connects to the server. /// </summary> /// <param name="sync">If true, the Task returned must be guaranteed to be complete.</param> /// <param name="flags">Flags used when opening the connection.</param> /// <returns>Nothing.</returns> protected override Task InternalOpenAsync(bool sync, Flags flags) { server = new BuiltinMemoryServer(); // BuiltinServer is an implementation of Server (above). server.Connect(); return Task.CompletedTask; } /// <summary> /// Disconnects from the server. /// </summary> protected override void InternalClose() { server?.Disconnect(); server = null; symbolTable = null; } #endregion // If your server does not offer a symbol table, then you can ignore and remove this region entirely. #region ISymbolTable /// <summary> /// Returns whether GetSymbolTableAsync is expected to be able to read the PLC symbol table. /// </summary> CanReadSymbolsFlags ISymbolTable.CanReadSymbols => CanReadSymbolsFlags.YesWithConnection; /// <summary> /// Read the symbol table from the PLC. /// </summary> /// <param name="sync">If true, the Task returned must be guaranteed to be complete.</param> /// <returns>The browse root (or null if the symbol table can't be read).</returns> Task<IBrowseItem?> ISymbolTable.GetSymbolTableAsync(bool sync) { if (server != null && symbolTable == null) symbolTable = server.ReadSymbolTable(this); // read and cache the symbol table return Task.FromResult(symbolTable); } #endregion #region IDirectMemoryAccessService /// <summary> /// The type of address expected by this service. The type must implement <see cref="IAddress"/>. /// </summary> /// <remarks> /// <para> /// Addresses passed to our IO methods may not be exactly this type, but their /// <see cref="MemoryAddress.AddressSpace"/> will match. /// </para> /// </remarks> Type IDirectMemoryAccessService.AddressType => typeof(BuiltinMemAddress); /// <summary> /// Access parameters (or null to use the defaults). /// </summary> /// <remarks> /// The endianess of the underlying memory (defaults to big endian) is used to convert memory data into useful data types, such as integers. /// The default text encoding (or null if not known or not standard), is used in order to convert memory data into text. /// </remarks> AccessParameters IDirectMemoryAccessService.GetPreferredParameters(MemoryAddress address, AccessParameters requestedParameters) => new AccessParameters(endian: Endian.Big, textEncoding: BinaryTextEncoding.LengthEncodedASCII2BE); /// <summary> /// The maximum amount of data this server can access through its IO methods (or -1 to mean any amount). /// </summary> int IDirectMemoryAccessService.PduSize => -1; /// <summary> /// A measure in bytes of the overhead of creating a new "read" request (or -1). /// </summary> /// <remarks> /// This value is used to determine the best way of merging "read" requests. /// </remarks> int IDirectMemoryAccessService.RequestOverhead => -1; /// <summary> /// Preferred access to the address space. /// </summary> /// <remarks> /// The server may prefer the user to access more data than addressed. For this example, we widen /// bit-level memory accesses to the nearest byte array. /// </remarks> MemoryAddress IDirectMemoryAccessService.GetPreferredAddress(MemoryAddress address) { return new MemoryAddress(address.AddressSpace, address.Area.GetByteAligned()); } /// <summary> /// Required access to the address space. /// </summary> /// <remarks> /// Like <see cref="IDirectMemoryAccessService.GetPreferredAddress(MemoryAddress)"/> except that /// it mandates how accesses to <paramref name="address"/> should be performed. /// </remarks> MemoryAddress IDirectMemoryAccessService.GetRequiredAddress(MemoryAddress address) { return address; } /// <summary> /// Reads data from the server asynchronously. /// </summary> /// <param name="sync">If true, the Task returned is guaranteed to be complete.</param> /// <param name="request">The reqd request.</param> /// <param name="userState">User state.</param> /// <returns>True if the operation succeeded.</returns> Task<bool> ReadAsync(bool sync, VectoredMemoryRequest request, object? userState) { var server = this.server ?? throw new ClosedException(); var address = request.MemAddress; var addrSpace = (BuiltinAddressSpace)address.AddressSpace; var startAddrBits = address.BitAddress; var lenBits = address.NumBits; server.Read(request.Buffer, addrSpace.AddressBank, startAddrBits, lenBits); return Task.FromResult(true); } /// <summary> /// Returns an efficient method for reading multiple areas of memory. /// </summary> /// <param name="requests">The areas of memory to read.</param> /// <returns> /// Methods for reading the data efficiently. /// </returns> VectoredRequests IDirectMemoryAccessService.InternalReadV(IReadOnlyList<VectoredMemoryRequest> requests) { return new SequentialVectoredRead(ReadAsync, requests); } /// <summary> /// Writes data to the server. /// </summary> /// <param name="sync">If true, the Task returned is guaranteed to be complete.</param> /// <param name="request">Write request.</param> /// <param name="userState">User state.</param> /// <returns>True if the operation succeeded.</returns> Task<bool> WriteAsync(bool sync, VectoredMemoryRequest request, object? userState) { var server = this.server ?? throw new ClosedException(); var address = request.MemAddress; var addrSpace = (BuiltinAddressSpace)address.AddressSpace; var startAddrBits = address.BitAddress; var lenBits = address.NumBits; server.Write(request.Buffer, addrSpace.AddressBank, startAddrBits, lenBits); return Task.FromResult(true); } /// <summary> /// Returns an efficient method for writing to multiple areas of memory. /// </summary> /// <param name="requests">The areas of memory to write to.</param> /// <returns> /// Methods for writing the data efficiently. /// </returns> VectoredRequests IDirectMemoryAccessService.InternalWriteV(IReadOnlyList<VectoredMemoryRequest> requests) { return new SequentialVectoredWrite(WriteAsync, requests); } #endregion } #region Register Protocol /// <summary> /// Constructs the Built-in Memory Server Protocol. /// </summary> /// <remarks> /// <para> /// The name passed in to the <see cref="Protocol"/> constructor will become the 'scheme' part of the /// protocol address URL. /// </para> /// <para> /// This example protocol supports one service, the <see cref="IDirectMemoryAccessService"/>. This service /// is used to implement a simple memory server. /// </para> /// <para> /// A memory server differs from a Tag Server (see <see cref="BuiltinTagProtocol"/>) primarily in the /// way data is addressed. A Tag Server addresses data by tag name, and tag names are explicitly defined by /// the tag server in the symbol table. A Memory Server addresses data by memory address. Often there's no /// symbol table, and the user can use any valid memory address to access any part of the server memory. /// </para> /// <para> /// However, some Memory servers also expose a symbol table, where each symbol is an alias for a particular /// address. You can support this for your server (if need be) by including <see cref="ISymbolTable"/> /// in the service list and making <see cref="BuitinConnection"/> implement it. /// </para> /// <para> /// Rarely a Memory server may notify when data in memory changes. In this case you can opt to use the /// <see cref="INotifyDirectMemoryAccessService"/> instead of <see cref="IDirectMemoryAccessService"/>. /// Beckhoff's TwinCAT is an example, where you can subscribe to memory locations and be informed when data /// in memory at those locations changes. See <see cref="BuiltinNotifyMemoryProtocol"/> for an example. /// </para> /// </remarks> BuiltinMemoryProtocol() : base("BuiltinMem", new Type[] { typeof(IDirectMemoryAccessService), // This is the main interface we support for memory tag servers. typeof(ISymbolTable), // Include this only if you intend on exposing a symbol table. }) { } /// <summary> /// Registers the protocol. You should call this method once from your code at start-up. /// </summary> internal static void Register() { Registry.Register(new BuiltinMemoryProtocol()); } #endregion /// <summary> /// Creates a new instance of the protocol. /// </summary> /// <param name="head">A required parameter of the <see cref="ProtocolInstance"/> constructor.</param> /// <param name="protocolAddress">The address of the new connection.</param> /// <returns>A new instance of the protocol.</returns> protected override ProtocolInstance NewInstance(ProtocolHead head, ProtocolAddress protocolAddress) { return new BuitinConnection(this, head, protocolAddress); } } }
The example is extended to implement the INotifyDirectMemoryAccessService, which supports servers that can subscribe / publish memory change events.
using System; using System.Collections.Generic; using System.Threading.Tasks; using Demo3D.Net; using Demo3D.PLC.Comms.Memory; using Buffer = Demo3D.IO.Buffer; namespace Demo3D.PLC.Comms.Builtin { /// <summary> /// An extended Built-in Memory Server based on <see cref="ExampleMemoryServer"/>. /// </summary> /// <remarks> /// <para> /// Normally, you'd replace this class entirely with whatever code is required to access your server. /// </para> /// <para> /// This version of the example shows how to publish when memory changes. /// </para> /// </remarks> public class ExampleNotifyMemoryServer : ExampleMemoryServer { readonly byte[] memory = new byte[1000]; readonly List<(MemoryAddress address, Action<MemoryAddressChangedEventArgs> handler)> subscriptions = new(); /// <exclude /> protected override byte[]? GetMemoryBank(int addressBank) { return addressBank == 0 ? memory : null; } /// <summary> /// Put in here whatever is required to subscribe to memory changes. /// </summary> /// <param name="address">The address describing the area of memory being watched.</param> /// <param name="handler">The handler to call when memory changes.</param> public void Subscribe(MemoryAddress address, Action<MemoryAddressChangedEventArgs> handler) { lock (subscriptions) { subscriptions.Add((address, handler)); } } /// <summary> /// Put in here whatever is required to unsubscribe from memory changes. /// </summary> /// <param name="address">The address describing the area of memory being watched.</param> public void Unsubscribe(MemoryAddress address) { lock (subscriptions) { subscriptions.RemoveAll(x => x.address.Equals(address)); } } /// <summary> /// Example of raising publication events. /// </summary> /// <param name="address">The address that changed.</param> /// <param name="data">The data from the address that changed.</param> public void PublishChanges(MemoryAddress address, byte[] data) { lock (subscriptions) { foreach (var (subscribedAddress, handler) in subscriptions) { if (subscribedAddress.Overlaps(address)) { handler(new MemoryAddressChangedEventArgs(subscribedAddress, address.Area, new Buffer(data))); } } } } } /// <summary> /// The Built-in Notify Memory Server Protocol - a memory server with memory change notification. /// </summary> /// <remarks> /// <para> /// This is an extension to <see cref="BuiltinMemoryProtocol"/> to integrate with a server that will notify when /// subscribed areas of memory change. /// </para> /// </remarks> [ProtocolAddressEditor(DisplayName = "Built-in Notify Memory Server")] public class BuiltinNotifyMemoryProtocol : Protocol { /// <summary> /// An instance of one connection to the Built-in Notify Memory Server. /// This extends the <see cref="BuiltinMemoryProtocol.BuitinConnection"/> example to include the /// <see cref="INotifyDirectMemoryAccessService"/>. /// </summary> public class BuitinConnection : BuiltinMemoryProtocol.BuitinConnection, INotifyDirectMemoryAccessService { ExampleNotifyMemoryServer? notifyServer; /// <summary> /// Constructs a new <see cref="ProtocolInstance"/> for the Built-in Notify Memory Server Protocol. /// </summary> /// <param name="protocol">The protocol; a required parameter of <see cref="ProtocolInstance"/>.</param> /// <param name="head">The protocol head; a required parameter of <see cref="ProtocolInstance"/>.</param> /// <param name="peerAddress">The address of the server being connected to.</param> internal BuitinConnection(Protocol protocol, ProtocolHead head, ProtocolAddress peerAddress) : base(protocol, head, peerAddress) { } #region Connection /// <exclude /> protected override Task InternalOpenAsync(bool sync, Flags flags) { server = notifyServer = new ExampleNotifyMemoryServer(); server.Connect(); return Task.CompletedTask; } /// <exclude /> protected override void InternalClose() { notifyServer = null; base.InternalClose(); } #endregion #region INotifyDirectMemoryAccessService /// <summary> /// Returns true if data changes can be subscribed to. /// Return false to revert to <see cref="IDirectMemoryAccessService"/> behaviour. /// </summary> bool INotifyDirectMemoryAccessService.CanSubscribe => true; /// <summary> /// Called to subscribe to memory changed publications. /// </summary> /// <param name="address">The address describing the area of memory being watched.</param> /// <param name="handler">The handler to call when memory changes.</param> void INotifyDirectMemoryAccessService.AddDataChanged(MemoryAddress address, NotifyDataChangedEventHandler handler) { notifyServer?.Subscribe(address, (details) => handler(this, this, details)); } /// <summary> /// Called to unsubscribe from memory changed publications. /// </summary> /// <param name="address">The address describing the area of memory being watched.</param> /// <param name="handler">The handler to call when memory changes.</param> void INotifyDirectMemoryAccessService.RemoveDataChanged(MemoryAddress address, NotifyDataChangedEventHandler handler) { notifyServer?.Unsubscribe(address); } #endregion } #region Register Protocol /// <summary> /// Constructs the Built-in Notify Memory Server Protocol. /// </summary> /// <remarks> /// <para> /// The name passed in to the <see cref="Protocol"/> constructor will become the 'scheme' part of the /// protocol address URL. /// </para> /// <para> /// This example protocol supports one service, the <see cref="INotifyDirectMemoryAccessService"/>. This service /// is used to implement a memory server that can publish when sections of memory have changed. /// </para> /// </remarks> BuiltinNotifyMemoryProtocol() : base("BuiltinNotifyMem", new Type[] { typeof(IDirectMemoryAccessService), // This is the main interface we support for memory tag servers. typeof(INotifyDirectMemoryAccessService), // In addition, the memory server can publish when sections of memory change. typeof(ISymbolTable), // Include this only if you intend on exposing a symbol table. }) { } /// <summary> /// Registers the protocol. You should call this method once from your code at start-up. /// </summary> internal static void Register() { Registry.Register(new BuiltinNotifyMemoryProtocol()); } #endregion /// <summary> /// Creates a new instance of the protocol. /// </summary> /// <param name="head">A required parameter of the <see cref="ProtocolInstance"/> constructor.</param> /// <param name="protocolAddress">The address of the new connection.</param> /// <returns>A new instance of the protocol.</returns> protected override ProtocolInstance NewInstance(ProtocolHead head, ProtocolAddress protocolAddress) { return new BuitinConnection(this, head, protocolAddress); } } }
This example shows how to implement a protocol that accesses some data with tag names and some with memory accesses.
using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Demo3D.Common; using Demo3D.IO; using Demo3D.Net; using Demo3D.PLC.Comms.Memory; using Demo3D.PLC.Comms.Tag; using Buffer = Demo3D.IO.Buffer; namespace Demo3D.PLC.Comms.Builtin { /// <summary> /// An example server expecting some data to be accessed by tag name, and some to be accessed by memory address. /// </summary> /// <remarks> /// Normally, you'd replace this class entirely with whatever code is required to access your server. /// </remarks> public class ExampleCombinedServer { readonly byte[] memory = new byte[1024]; readonly List<BuiltinTag> activeTags = new List<BuiltinTag>(); bool connected; void RunSimulator() { // Periodically update the first 50 bytes of memory with random values, // and the tag values of read-only tags with random values. var r = new Random(Environment.TickCount); string[] randomStrings = new string[] { "If","you","are","involved","in","system","commissioning","Emulate3D","PLC","Controls","Testing","is","the","only","thoroughbred","solution","for","off-line","logic","controls","testing.","Now","you","can","reduce","on-site","commissioning","time","improve","control","system","quality","accelerate","ramp","to","full","production","and","reduce","project","costs." }; object? RandomValue(Type? type) { if (type == typeof(bool)) return (r.Next() % 2) == 1; if (type == typeof(int)) return r.Next(); if (type == typeof(double)) return (double)(r.Next() / (double)(r.Next() + 1)); if (type == typeof(string)) return randomStrings[r.Next() % randomStrings.Length]; if (type == typeof(DateTime)) return DateTime.Now + new TimeSpan(r.Next() % 1000, r.Next() % 24, r.Next() % 60, r.Next() % 60); return null; } while (connected) { memory[0] = (byte)~memory[0]; for (int i = 1; i < 50; i++) { memory[i] = (byte)r.Next(); } using (var batchNotify = new DirectTag.BatchNotify()) { lock (activeTags) { foreach (var tag in activeTags) { if (!tag.AccessRights.CanWriteToPLC()) { var value = RandomValue(tag.TagType?.ElementType); if (value != null) tag.UpdateValue(batchNotify, value, null); } } } } Thread.Sleep(500); } } /// <summary> /// Connects to the server. /// </summary> public void Connect() { connected = true; new Thread(new ThreadStart(RunSimulator)) { IsBackground = true }.Start(); } /// <summary> /// Disconnects from the server. /// </summary> public void Disconnect() { connected = false; } /// <summary> /// Read data from server memory. /// </summary> /// <param name="buffer">The buffer to place the data in.</param> /// <param name="addressBank">The address bank to access.</param> /// <param name="startBit">The start memory address (in bits).</param> /// <param name="lengthBits">The amount of data to read (in bits).</param> public void Read(Buffer buffer, int addressBank, long startBit, long lengthBits) { BitsArray.CopyBits(memory, startBit, buffer.Data, (buffer.From << 3) + (startBit & 7), lengthBits); } /// <summary> /// Write data into server memory. /// </summary> /// <param name="buffer">The buffer containing the data to write.</param> /// <param name="addressBank">The address bank to access.</param> /// <param name="startBit">The start memory address (in bits).</param> /// <param name="lengthBits">The amount of data to write (in bits).</param> public void Write(Buffer buffer, int addressBank, long startBit, long lengthBits) { BitsArray.CopyBits(buffer.Data, (buffer.From << 3) + (startBit & 7), memory, startBit, lengthBits); } /// <summary> /// Reads the symbol table from the server. /// </summary> /// <returns>The symbol table.</returns> public IBrowseItem ReadSymbols() { var symbolTable = new BrowseItemBranch(); symbolTable.Add(new Symbol(symbolTable, "Boolean", AccessRights.ReadFromPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "Boolean2", AccessRights.WriteToPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "Integer", AccessRights.ReadFromPLC, typeof(int))); symbolTable.Add(new Symbol(symbolTable, "String", AccessRights.ReadFromPLC, typeof(string))); symbolTable.Add(new Symbol(symbolTable, "Date", AccessRights.ReadFromPLC, typeof(DateTime))); symbolTable.Add(new Symbol(symbolTable, "Double", AccessRights.ReadFromPLC, typeof(double))); return symbolTable; } /// <summary> /// Returns a <see cref="BuiltinTag"/> for accessing <see cref="Symbol"/> data. /// </summary> /// <param name="symbol">The <see cref="Symbol"/> to access.</param> /// <returns>A <see cref="BuiltinTag"/> for accessing the <see cref="Symbol"/>.</returns> public BuiltinTag GetTag(Symbol symbol) { var tag = new BuiltinTag(symbol, symbol.DataType, ReleaseTag); lock (activeTags) { activeTags.Add(tag); } return tag; } /// <summary> /// Called by <see cref="BuiltinTag.Dispose"/> when the tag is no longer needed. /// </summary> /// <param name="tag">The <see cref="BuiltinTag"/> being released.</param> public void ReleaseTag(BuiltinTag tag) { lock (activeTags) { activeTags.Remove(tag); } } } /// <summary> /// The Built-in Mixed Server Protocol. /// </summary> /// <remarks> /// <para> /// Must be marked with the <see cref="ProtocolAddressEditorAttribute"/> in order for NetServer to show /// this protocol in its drop-down. NetServer only looks for protocols with this attribute. /// </para> /// <para> /// This is an extension to the <see cref="BuiltinMemoryProtocol"/> and <see cref="BuiltinTagProtocol"/> /// demonstrating a protocol that requires some data to be accessed with tag names and some with memory /// addresses. /// </para> /// </remarks> [ProtocolAddressEditor(DisplayName = "Built-in Mixed Server")] public class BuiltinMixedProtocol : Protocol { /// <summary> /// An instance of one connection to the Built-in Mixed Server. /// </summary> public class BuiltinMixedConnection : ProtocolInstance, ISymbolTable, IDirectMemoryAccessService, IDirectTagAccessService { ExampleCombinedServer? server; IBrowseItem? symbols; MultipleChannelTagService? mixed; /// <summary> /// Constructs a new <see cref="ProtocolInstance"/> for the Built-in Mixed Server Protocol. /// </summary> /// <param name="protocol">The protocol; a required parameter of <see cref="ProtocolInstance"/>.</param> /// <param name="head">The protocol head; a required parameter of <see cref="ProtocolInstance"/>.</param> /// <param name="peerAddress">The address of the server being connected to.</param> /// <remarks> /// See <see cref="BuiltinTagProtocol.BuiltinConnection.BuiltinConnection(Protocol, ProtocolHead, ProtocolAddress, ServerConfiguration)"/> /// for an example on server configuration. /// </remarks> internal BuiltinMixedConnection(Protocol protocol, ProtocolHead head, ProtocolAddress peerAddress) : base(protocol, head, peerAddress, false, null) { } #region Connection /// <summary> /// Returns true if the connection has been established. /// </summary> protected override bool InternalRunning { get { return server != null; } } /// <summary> /// Connects to the server. /// </summary> /// <param name="sync">If true, the Task returned must be guaranteed to be complete.</param> /// <param name="flags">The flags used to open the connection.</param> /// <returns>Nothing.</returns> protected override Task InternalOpenAsync(bool sync, Flags flags) { server = new ExampleCombinedServer(); server.Connect(); return Task.CompletedTask; } /// <summary> /// Disconnects from the server. /// </summary> protected override void InternalClose() { server?.Disconnect(); server = null; symbols = null; } #endregion #region ISymbolTable /// <summary> /// Defines how to read the symbol table. Normally we require a connection to the server /// to read the symbol table, but sometimes that's not necessary. /// </summary> CanReadSymbolsFlags ISymbolTable.CanReadSymbols => CanReadSymbolsFlags.YesWithConnection; /// <summary> /// Gets the servers symbol table. /// </summary> /// <param name="sync">If true, the Task returned must be guaranteed to be complete.</param> /// <returns>The root symbol.</returns> /// <remarks> /// A Tag server must return a symbol table to facilitate IO. The server should at least return symbols /// for the tags that it supports. It may also optionally return symbols for some memory locations too. /// </remarks> Task<IBrowseItem?> ISymbolTable.GetSymbolTableAsync(bool sync) { if (symbols == null && server != null) symbols = server.ReadSymbols(); return Task.FromResult(symbols); } #endregion /// <summary> /// Support for access to memory in the server. /// <see cref="BuiltinMemoryProtocol.BuitinConnection"/> for details. /// </summary> #region IDirectMemoryAccessService static readonly AccessParameters preferredParameters = new AccessParameters(endian: Endian.Big, textEncoding: BinaryTextEncoding.LengthEncodedASCII2BE); Type IDirectMemoryAccessService.AddressType => typeof(BuiltinMemAddress); int IDirectMemoryAccessService.PduSize => -1; int IDirectMemoryAccessService.RequestOverhead => -1; AccessParameters IDirectMemoryAccessService.GetPreferredParameters(MemoryAddress address, AccessParameters requestedParameters) => preferredParameters; MemoryAddress IDirectMemoryAccessService.GetPreferredAddress(MemoryAddress address) { return new MemoryAddress(address.AddressSpace, address.Area.GetByteAligned()); } MemoryAddress IDirectMemoryAccessService.GetRequiredAddress(MemoryAddress address) { return address; } Task<bool> ReadAsync(bool sync, VectoredMemoryRequest request, object? userState) { var server = this.server ?? throw new ClosedException(); var address = request.MemAddress; var addrSpace = (BuiltinAddressSpace)address.AddressSpace; var startAddrBits = address.BitAddress; var lenBits = address.NumBits; server.Read(request.Buffer, addrSpace.AddressBank, startAddrBits, lenBits); return Task.FromResult(true); } VectoredRequests IDirectMemoryAccessService.InternalReadV(IReadOnlyList<VectoredMemoryRequest> requests) { return new SequentialVectoredRead(ReadAsync, requests); } Task<bool> WriteAsync(bool sync, VectoredMemoryRequest request, object? userState) { var server = this.server ?? throw new ClosedException(); var address = request.MemAddress; var addrSpace = (BuiltinAddressSpace)address.AddressSpace; var startAddrBits = address.BitAddress; var lenBits = address.NumBits; server.Write(request.Buffer, addrSpace.AddressBank, startAddrBits, lenBits); return Task.FromResult(true); } VectoredRequests IDirectMemoryAccessService.InternalWriteV(IReadOnlyList<VectoredMemoryRequest> requests) { return new SequentialVectoredWrite(WriteAsync, requests); } #endregion /// <summary> /// Support for access to tags in the server. /// <see cref="BuiltinTagProtocol.BuiltinConnection"/> for details. /// </summary> #region IDirectTagAccessService Task<IBrowseItem?> IDirectTagAccessService.GetSymbolTableAsync(bool sync) => Task.FromResult<IBrowseItem?>(null); Type IDirectTagAccessService.AddressType => typeof(Symbol); Task<DirectTag?> IDirectTagAccessService.GetTagAsync(bool sync, IAddress address) { if (server == null) throw new ClosedException(); return Task.FromResult<DirectTag?>(server.GetTag((Symbol)address)); } VectoredRequests IVectoredTagService<DirectTag>.InternalReadV(IReadOnlyList<VectoredTagRequest<DirectTag>> requests) { return requests.ReadSequentially<BuiltinTag>(tag => tag.Value); } VectoredRequests IVectoredTagService<DirectTag>.InternalWriteV(IReadOnlyList<VectoredTagRequest<DirectTag>> requests) { return requests.WriteSequentially<BuiltinTag>((tag, value) => tag.Value = value); } #endregion /// <summary> /// Called to return an object that supports a particular service. /// </summary> /// <param name="channelInstance">The channel on which the service is being requested.</param> /// <param name="serviceType">The type of the service being requested.</param> /// <param name="propertyBag">Configuration properties.</param> /// <returns>An object that implements <paramref name="serviceType"/>.</returns> /// <remarks> /// The default implementation of TryCreateService normally returns 'this' for services requested, but it /// doesn't have to. You can override TryCreateService to return any object that implements a service. /// </remarks> protected override object? TryCreateService(ChannelInstance channelInstance, Type serviceType, object? propertyBag) { // The default channel is the main channel. We advertise (in the BuiltinMixedProtocol constructor below) that // we support INotifyTagService and ITagService. // // Rather than support those services ourselves, we return a MultipleChannelTagService which does the work for // us, and more importantly redirects calls to the correct channel based on the address being accessed. // // The effect is that accesses to Symbol's are routed through one channel which builds and maintains caches, etc // just for the tags. Accesses to BuiltinAddress's are routed through a separate channel which builds entirely // separate caches, etc for memory accesses. The caches and polling algorithms on each channel operate // independently. if (string.IsNullOrEmpty(channelInstance.Name) && (serviceType == typeof(INotifyTagService) || serviceType == typeof(ITagService) || serviceType == typeof(IOffloadingService))) { if (mixed == null) { mixed = new MultipleChannelTagService(this, typeof(BuiltinMemAddress), (typeof(Symbol), TagChannelName), (typeof(BuiltinMemAddress), MemChannelName)); } return mixed; } return base.TryCreateService(channelInstance, serviceType, propertyBag); } } // Names we're going to give to our channels. const string MemChannelName = "Mem"; const string TagChannelName = "Tag"; /// <summary> /// Constructs the Built-in Mixed Server Protocol. /// </summary> internal BuiltinMixedProtocol() : base("BuiltinMixed", // name new Channel[] { // We support three channels. new Channel(null, // The default channel is our public API which new Type[] { // offers these services to NetServer, and which are typeof(ISymbolTable), // implemented for us by MultipleChannelTagService. typeof(INotifyTagService), typeof(ITagService), typeof(IOffloadingService), } ), new Channel(MemChannelName, typeof(IDirectMemoryAccessService)), // The "memory" channel supports IDirectMemoryAccessService. new Channel(TagChannelName, typeof(IDirectTagAccessService)), // The "tag" channel support IDirectTagAccessService. }, null) { // required protocols/services } /// <summary> /// Registers the protocol. You should call this method once from your code at start-up. /// </summary> internal static void Register() { Registry.Register(new BuiltinMixedProtocol()); } /// <summary> /// Creates a new instance of the protocol. /// </summary> /// <param name="head">A required parameter of the <see cref="ProtocolInstance"/> constructor.</param> /// <param name="protocolAddress">The address of the new connection.</param> /// <returns>A new instance of the protocol.</returns> protected override ProtocolInstance NewInstance(ProtocolHead head, ProtocolAddress protocolAddress) { return new BuiltinMixedConnection(this, head, protocolAddress); } } }
This example is a simple implementation of a server that behaves in an OPC style. Unlike the previous examples, this is a server to which clients connect. Demo3D allows you to bind tags with any name, and those tags are then exposed by the server to its clients. Clients connect, read the tag list, and subscribe to data changes.
using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Demo3D.Common; using Demo3D.IO; using Demo3D.Net; using Demo3D.PLC.Comms.Tag; /* * Example usage: * WPF * <Window x:Class="BuiltinTagClient.MainWindow" * xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" * xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" * xmlns:d="http://schemas.microsoft.com/expression/blend/2008" * xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" * xmlns:core="clr-namespace:System;assembly=mscorlib" * xmlns:local="clr-namespace:BuiltinTagClient" * mc:Ignorable="d" * Title="Builtin Tag Client" Height="450" Width="600"> * <Window.Resources> * <ObjectDataProvider x:Key="myEnum" MethodName="GetValues" ObjectType="{x:Type core:Enum}"> * <ObjectDataProvider.MethodParameters> * <x:Type Type="local:AccessRights"/> * </ObjectDataProvider.MethodParameters> * </ObjectDataProvider> * </Window.Resources> * * <Grid> * <DataGrid Name="TagData" ItemsSource="{Binding}" CanUserAddRows="False" CanUserDeleteRows="False"/> * </Grid> * </Window> * * C# * using System; * using System.Collections.Generic; * using System.Collections.ObjectModel; * using System.ComponentModel; * using System.IO; * using System.Net.Sockets; * using System.Runtime.CompilerServices; * using System.Threading; * using System.Threading.Tasks; * using System.Windows; * * namespace BuiltinTagClient { * // TODO Copy the first two classes below into your code here... * public class BuiltinTagServicePeer ... * public BuiltinTagServiceClient ... * * public enum AccessRights { * Unknown = 0x00, * ReadFromPLC = 0x01, * WriteToPLC = 0x02, * Bidirectional = 0x03, * }; * * public partial class MainWindow : Window { * readonly ObservableCollection<BuiltinTagServiceClient.TagData> tagData = new ObservableCollection<BuiltinTagServiceClient.TagData>(); * * public MainWindow() { * InitializeComponent(); * * var client = new BuiltinTagServiceClient(); * * client.TagListChanged += Client_TagListChanged; * TagData.DataContext = tagData; * * client.Connect(new TcpClient("localhost", 6735)); * } * * void Client_TagListChanged(BuiltinTagServiceClient client, IReadOnlyList<BuiltinTagServiceClient.TagData> current, IReadOnlyList<BuiltinTagServiceClient.TagData> added, IReadOnlyList<BuiltinTagServiceClient.TagData> removed, IReadOnlyList<(BuiltinTagServiceClient.TagData, AccessRights oldAccess, AccessRights newAccess)> changed) { * if (!this.Dispatcher.CheckAccess()) { * this.Dispatcher.Invoke(() => Client_TagListChanged(client, current, added, removed, changed)); * * } else { * foreach (var tag in added) tagData.Add(tag); * foreach (var tag in removed) tagData.Remove(tag); * } * } * } * } */ namespace Demo3D.PLC.Comms.Builtin { /// <summary> /// Provides message passing and other utility functions for the BuiltinTagService protocol. /// </summary> public class BuiltinTagServicePeer : IDisposable { /// <summary> /// List of protocol request types. /// </summary> protected enum RequestType { /// <summary> /// No operation. /// </summary> Nop = 0, /// <summary> /// List all the tags. /// </summary> ListTags = 1, /// <summary> /// Read tag values. /// </summary> ReadValues = 2, /// <summary> /// Write tag values. /// </summary> WriteValues = 3, /// <summary> /// Subscribe to tag changes. /// </summary> Subscribe = 4, /// <summary> /// Publish tag value updates to the client. /// </summary> Publish = 5, /// <summary> /// Advise the client that the tag list changed. /// </summary> TagListChanged = 6, } readonly SemaphoreSlim readLock = new(1); readonly SemaphoreSlim writeLock = new(1); TcpClient? client; NetworkStream? stream; /// <summary> /// Constructs a new BuiltinTagServicePeer. /// </summary> /// <param name="client">The connected tcp client.</param> protected BuiltinTagServicePeer(TcpClient client) { Connect(client); } /// <summary> /// Constructs a new BuiltinTagServicePeer. /// </summary> protected BuiltinTagServicePeer() { } /// <summary> /// Set up connection to the client. /// </summary> /// <param name="client">The connected tcp client.</param> public virtual void Connect(TcpClient client) { if (this.client != null) throw new Exception("Already connected"); this.client = client ?? throw new ArgumentNullException(nameof(client)); stream = client.GetStream(); } /// <summary> /// Marshals a value into the data stream. /// </summary> /// <param name="writer">A BinaryWriter constructed on the data stream.</param> /// <param name="type">The data type of the value to write.</param> /// <param name="value">The value to write.</param> protected static void MarshalValue(BinaryWriter writer, Type type, object value) { switch (Type.GetTypeCode(type)) { case TypeCode.Boolean: writer.Write((bool)value); break; case TypeCode.Int16: writer.Write((short)value); break; case TypeCode.Int32: writer.Write((int)value); break; case TypeCode.String: writer.Write((string)value); break; default: throw new Exception($"Type {type.FullName} not supported"); } } /// <summary> /// Unmarshals a value from the data stream. /// </summary> /// <param name="reader">A BinaryReader constructed on the data stream.</param> /// <param name="type">The data type of the value to read.</param> /// <returns>The value unmarshalled.</returns> protected static object UnmarshalValue(BinaryReader reader, Type type) { return Type.GetTypeCode(type) switch { TypeCode.Boolean => reader.ReadBoolean(), TypeCode.Int16 => reader.ReadInt16(), TypeCode.Int32 => reader.ReadInt32(), TypeCode.String => reader.ReadString(), _ => throw new Exception($"Type {type.FullName} not supported"), }; } static readonly byte[] EmptyMessage = Array.Empty<byte>(); /// <summary> /// Reads a message from the stream. /// </summary> /// <returns>The message ID, protocol request type, and any message data.</returns> protected async Task<(ulong id, RequestType requestType, byte[] message)> ReadMessageAsync() { var stream = this.stream ?? throw new ClosedException(); async Task<byte[]> ReadBytesAsync(int length) { var data = new byte[length]; while (length > 0) { int got = await stream.ReadAsync(data, data.Length - length, length).ConfigureAwait(false); if (got == 0) throw new Exception("Disconnected"); length -= got; } return data; } await readLock.WaitAsync().ConfigureAwait(false); try { var header = await ReadBytesAsync(14).ConfigureAwait(false); var length = (int)BitConverter.ToUInt32(header, 0); var id = BitConverter.ToUInt64(header, 4); var requestType = (RequestType)BitConverter.ToUInt16(header, 12); var message = EmptyMessage; if (length > 0) message = await ReadBytesAsync(length).ConfigureAwait(false); return (id, requestType, message); } finally { readLock.Release(); } } /// <summary> /// Writes a message to the stream. /// </summary> /// <param name="id">The message id.</param> /// <param name="requestType">The protocol request type.</param> /// <param name="message">The message data (or null).</param> /// <returns>Nothing.</returns> protected async Task WriteMessageAsync(ulong id, RequestType requestType, byte[]? message) { var stream = this.stream ?? throw new ClosedException(); var length = message == null ? 0 : message.Length; var header = new byte[14]; Array.Copy(BitConverter.GetBytes(length), 0, header, 0, 4); Array.Copy(BitConverter.GetBytes(id), 0, header, 4, 8); Array.Copy(BitConverter.GetBytes((ushort)requestType), 0, header, 12, 2); await writeLock.WaitAsync().ConfigureAwait(false); try { await stream.WriteAsync(header, 0, 14).ConfigureAwait(false); if (length > 0) await stream.WriteAsync(message!, 0, length).ConfigureAwait(false); await stream.FlushAsync().ConfigureAwait(false); } finally { writeLock.Release(); } } /// <summary> /// Force the tcp client to shut down. /// </summary> public void Shutdown() { try { client?.Client.Shutdown(SocketShutdown.Both); } catch { } } /// <summary> /// Close the data stream and tcp client. /// </summary> protected void Close() { try { stream?.Dispose(); } catch { } try { client?.Close(); } catch { } stream = null; } /// <summary> /// Disposes the client, releasing all resources and shutting down the client connection. /// </summary> public virtual void Dispose() { Shutdown(); } /// <summary> /// Returns a string representation of the client. /// </summary> /// <returns>A string representation of the client.</returns> public override string? ToString() { try { return client?.Client.RemoteEndPoint?.ToString(); } catch { return "<Disconnected>"; } } } /// <summary> /// Exposes a tag list from the server. /// </summary> public class BuiltinTagServiceClient : BuiltinTagServicePeer { /// <summary> /// TagData from the Demo3D server. /// </summary> public sealed class TagData : INotifyPropertyChanged { /// <summary> /// Occurs when the tag access is changed by the server. /// </summary> public event Action<TagData, AccessRights, AccessRights>? AccessChanged; /// <summary> /// Occurs when the tag value is changed and the origin of the update was internal (from inside this application). /// </summary> public event Action<TagData, object>? ValueUpdatedInternally; /// <summary> /// Occurs when the tag value is changed and the origin of the update was external (from the Demo3D server). /// </summary> public event Action<TagData, object>? ValueUpdatedExternally; readonly int tagId; AccessRights access; object? value; /// <summary> /// True if the tag is currently active on the server. /// </summary> public bool Active { get; internal set; } /// <summary> /// The tag name in the server. /// </summary> public string Name { get; } /// <summary> /// The data type of the tag. /// </summary> public Type Type { get; } /// <summary> /// Constructs a new tag. /// </summary> /// <param name="tagId">Unique tag id.</param> /// <param name="name">The tag name.</param> /// <param name="type">The data type of the tag.</param> /// <param name="access">The tag access rights.</param> internal TagData(int tagId, string name, Type type, AccessRights access) { this.tagId = tagId; this.Active = true; this.Name = name; this.Type = type; this.access = access; } /// <summary> /// Gets the tag ID assigned to this tag. /// </summary> /// <returns>The tag ID assigned to this tag.</returns> public int GetTagID() => tagId; /// <summary> /// The tag access rights. /// </summary> public AccessRights Access { get { return access; } internal set { if (access != value) { var old = access; access = value; AccessChanged?.Invoke(this, old, value); NotifyPropertyChanged(); } } } /// <summary> /// Called to set the tag value. /// </summary> /// <param name="value">The new tag value.</param> /// <param name="notifyExternal">True to notify an external update.</param> /// <param name="notifyInternal">True to notify an internal update.</param> internal void SetValue(object? value, bool notifyExternal = false, bool notifyInternal = false) { try { value = Convert.ChangeType(value, this.Type); } catch { } if (value != null && ((this.value is null) || !this.value.Equals(value))) { this.value = value; if (notifyExternal) ValueUpdatedExternally?.Invoke(this, value); if (notifyInternal) ValueUpdatedInternally?.Invoke(this, value); NotifyPropertyChanged(nameof(this.Value)); NotifyPropertyChanged(nameof(this.Boolean)); } } /// <summary> /// The current tag value. /// </summary> public object? Value { get { return value; } set { SetValue(value, notifyInternal: true); } } /// <summary> /// The current tag value as a boolean. /// </summary> public bool Boolean { get { return this.Value is bool v && v; } set { if (this.Type == typeof(bool)) { this.Value = value; } } } #region INotifyPropertyChanged /// <summary> /// Occurs when a property value changes. /// </summary> public event PropertyChangedEventHandler? PropertyChanged; /// <summary> /// Raises the PropertyChanged event. /// </summary> /// <param name="e">A PropertyChangedEventArgs that contains the event data.</param> void NotifyPropertyChanged(PropertyChangedEventArgs e) { this.PropertyChanged?.Invoke(this, e); } /// <summary> /// Raises the PropertyChanged event. /// </summary> /// <param name="propertyName">The name of the property that has changed.</param> void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { NotifyPropertyChanged(new PropertyChangedEventArgs(propertyName)); } #endregion #region Object overrides /// <summary> /// Returns a string representation of this tag. /// </summary> /// <returns>A string representation of this tag.</returns> public override string ToString() { return this.Name; } #endregion } /// <summary> /// Represents a method that handles the <see cref="TagListChanged"/> event. /// </summary> /// <param name="client">The client that raised the event.</param> /// <param name="current">The current list of tags.</param> /// <param name="added">The tags that were added to the current list.</param> /// <param name="removed">The tags that were removed from the current list.</param> /// <param name="changed">Tags whose access has changed.</param> public delegate void TagListChangedDelegate(BuiltinTagServiceClient client, IReadOnlyList<TagData> current, IReadOnlyList<TagData> added, IReadOnlyList<TagData> removed, IReadOnlyList<(TagData, AccessRights oldAccess, AccessRights newAccess)> changed); /// <summary> /// Occurs when the tag list changes. /// </summary> public event TagListChangedDelegate? TagListChanged; /// <summary> /// Occurs when tag values are changed and the origin of the update was external (from the Demo3D server). /// </summary> public event Action<BuiltinTagServiceClient, List<TagData>>? ValuesUpdatedExternally; long nextRequestId; readonly Dictionary<(ulong, RequestType), Action<byte[]>> expected = new Dictionary<(ulong, RequestType), Action<byte[]>>(); readonly SemaphoreSlim activeTagsLock = new SemaphoreSlim(1); readonly Dictionary<int, TagData> activeTags = new Dictionary<int, TagData>(); /// <summary> /// Force a shutdown in response to an error. /// </summary> /// <param name="e">The error prompting the shutdown.</param> void Shutdown(Exception e) { Shutdown(); Console.WriteLine($"Error: Builtin Tag Service Server {this}: {e.Message}"); } #region Receive and Process Incoming Messages /// <summary> /// The main loop receiving and processing messages received from the Demo3D server. /// </summary> async Task RunClientAsync() { try { for (;;) { var (id, requestType, message) = await ReadMessageAsync().ConfigureAwait(false); switch (requestType) { case RequestType.TagListChanged: StartUpdateTagList(); break; case RequestType.Publish: await ReadPublicationsAsync(message).ConfigureAwait(false); break; default: Action<byte[]>? action; lock (expected) { if (!expected.TryGetValue((id, requestType), out action)) { throw new Exception("Unexpected message from server"); } } action?.Invoke(message); break; } } } catch (Exception e) { Close(); Console.WriteLine($"Error: Builtin Tag Service Server {this}: {e.Message}", e); } } #endregion #region Send and Receive Messages /// <summary> /// Sends a message and returns the response. /// </summary> /// <param name="requestType">The message protocol request type.</param> /// <param name="requestMessage">The message data (or null).</param> /// <returns>The response data.</returns> async Task<byte[]> RPCAsync(RequestType requestType, byte[]? requestMessage) { // Allocate a unique message id. var id = (ulong)Interlocked.Increment(ref nextRequestId); // Register that we're expecting a response with that id. var responseMessage = (byte[]?)null; var readDone = new SemaphoreSlim(0); void Received(byte[] message) { responseMessage = message; readDone.Release(); } lock (expected) { expected.Add((id, requestType), Received); } // Send the request. await WriteMessageAsync(id, requestType, requestMessage).ConfigureAwait(false); // Wait for the response and return the response data. await readDone.WaitAsync().ConfigureAwait(false); return responseMessage!; } /// <summary> /// Sends a message for which no response is expected. /// </summary> /// <param name="requestType">The message protocol request type.</param> /// <param name="requestMessage">The message data (or null).</param> Task WriteMessageAsync(RequestType requestType, byte[] requestMessage) { // Allocate a unique message id. var id = (ulong)Interlocked.Increment(ref nextRequestId); // Send the request. return WriteMessageAsync(id, requestType, requestMessage); } #endregion #region Protocol Requests // activeTagsLock held async Task<(List<TagData> added, List<TagData> removed, List<(TagData, AccessRights oldAccess, AccessRights newAccess)> changed)> ListTagsAsync() { // Send request, get reply, and read the new set of tags. var response = await RPCAsync(RequestType.ListTags, null).ConfigureAwait(false); var reader = new BinaryReader(new MemoryStream(response)); var newTags = new Dictionary<int, TagData>(); while (reader.BaseStream.Position < reader.BaseStream.Length) { var tagId = reader.ReadInt32(); var name = reader.ReadString(); var access = (AccessRights)reader.ReadInt32(); var typeName = reader.ReadString(); var type = Type.GetType(typeName); if (type == null) throw new Exception($"Unknown type {typeName}"); var tag = new TagData(tagId, name, type, access); newTags.Add(tagId, tag); } // Now work out the changes. // Tags can be added or removed, and their access can change. Other meta data (name and type) can't change. // If the server wants to change the name or type, then it must remove the tag and create a new one. var removed = new List<TagData>(); var added = new List<TagData>(); var changed = new List<(TagData, AccessRights oldAccess, AccessRights newAccess)>(); foreach (var kv in activeTags) { var existingTag = kv.Value; if (!newTags.TryGetValue(kv.Key, out var newTag)) removed.Add(existingTag); else if (newTag.Access != existingTag.Access) changed.Add((existingTag, existingTag.Access, newTag.Access)); } foreach (var kv in newTags) { if (!activeTags.ContainsKey(kv.Key)) added.Add(kv.Value); } // Apply the changes foreach (var removedTag in removed) { activeTags.Remove(removedTag.GetTagID()); removedTag.Active = false; } foreach (var addedTag in added) { activeTags.Add(addedTag.GetTagID(), addedTag); } foreach (var (changedTag, _, newAccess) in changed) { changedTag.Access = newAccess; } return (added, removed, changed); } async Task ReadValuesAsync(IEnumerable<TagData> tags) { var request = new MemoryStream(); using (var writer = new BinaryWriter(request)) { foreach (var tag in tags) { writer.Write(tag.GetTagID()); } } var message = request.ToArray(); if (message.Length == 0) return; var response = await RPCAsync(RequestType.ReadValues, message).ConfigureAwait(false); var reader = new BinaryReader(new MemoryStream(response)); foreach (var tag in tags) { var active = reader.ReadBoolean(); if (active) tag.SetValue(UnmarshalValue(reader, tag.Type), notifyExternal: true); else tag.Active = false; } } Task WriteValuesAsync(IEnumerable<TagData> tags) { var request = new MemoryStream(); using (var writer = new BinaryWriter(request)) { foreach (var tag in tags) { if (tag.Value != null) { writer.Write(tag.GetTagID()); MarshalValue(writer, tag.Type, tag.Value); } } } var message = request.ToArray(); if (message.Length == 0) return Task.CompletedTask; return WriteMessageAsync(RequestType.WriteValues, message); } Task SubscribeAsync(IEnumerable<TagData> tags) { var request = new MemoryStream(); using (var writer = new BinaryWriter(request)) { foreach (var tag in tags) { writer.Write(tag.GetTagID()); } } var message = request.ToArray(); if (message.Length == 0) return Task.CompletedTask; return WriteMessageAsync(RequestType.Subscribe, message); } async Task ReadPublicationsAsync(byte[] response) { var reader = new BinaryReader(new MemoryStream(response)); var read = new List<TagData>(); await activeTagsLock.WaitAsync().ConfigureAwait(false); try { while (reader.BaseStream.Position < reader.BaseStream.Length) { var tagId = reader.ReadInt32(); if (activeTags.TryGetValue(tagId, out var tag)) { tag.SetValue(UnmarshalValue(reader, tag.Type), notifyExternal: true); read.Add(tag); } } } finally { activeTagsLock.Release(); } // No locks held, so the tags we notify may already be stale. ValuesUpdatedExternally?.Invoke(this, read); } #endregion #region Tag Management void Tag_ValueUpdatedInternally(TagData tag, object value) { async void ValueUpdatedInternallyAsync() { try { await WriteValuesAsync(new TagData[] { tag }).ConfigureAwait(false); } catch (Exception e) { Shutdown(e); } } ValueUpdatedInternallyAsync(); } void StartUpdateTagList() { async Task UpdateTagListAsync() { List<TagData> current; List<TagData> added; List<TagData> removed; List<(TagData, AccessRights oldAccess, AccessRights newAccess)> changed; await activeTagsLock.WaitAsync().ConfigureAwait(false); try { // Update the activeTags list. (added, removed, changed) = await ListTagsAsync().ConfigureAwait(false); current = new List<TagData>(activeTags.Values); // Split the lists of tags up. var noLongerReadable = new List<TagData>(); var noLongerWriteable = new List<TagData>(); var readableTags = new List<TagData>(); var writeableTags = new List<TagData>(); noLongerReadable.AddRange(removed); noLongerWriteable.AddRange(removed); foreach (var (tag, oldAccess, newAccess) in changed) { if ((oldAccess & AccessRights.ReadFromPLC) != 0 && (newAccess & AccessRights.ReadFromPLC) == 0) noLongerReadable.Add(tag); if ((oldAccess & AccessRights.WriteToPLC) != 0 && (newAccess & AccessRights.WriteToPLC) == 0) noLongerWriteable.Add(tag); } foreach (var tag in added) { if ((tag.Access & AccessRights.ReadFromPLC) != 0) readableTags.Add(tag); if ((tag.Access & AccessRights.WriteToPLC) != 0) writeableTags.Add(tag); } // Unwire from the tags that are no longer readable. foreach (var tag in noLongerReadable) { tag.ValueUpdatedInternally -= Tag_ValueUpdatedInternally; } // Subscribe to the ValueUpdatedInternally event for each ReadFromPLC tag. foreach (var tag in readableTags) { tag.ValueUpdatedInternally += Tag_ValueUpdatedInternally; } // No point in writing the values of newly ReadFromPLC tags since we // won't yet know their value, so this call would do nothing. // WriteValuesAsync(readableTags); // Subscribe to all the WriteToPLC tags. await SubscribeAsync(writeableTags).ConfigureAwait(false); // Read all the values once. await ReadValuesAsync(writeableTags).ConfigureAwait(false); } finally { activeTagsLock.Release(); } // No locks held, so the tags we notify may already be stale. if (removed.Count > 0 || added.Count > 0) { TagListChanged?.Invoke(this, current, added, removed, changed); } } // Run in a separate thread/task. async Task StartUpdateTagListAsync() { try { await UpdateTagListAsync().ConfigureAwait(false); } catch (Exception e) { Shutdown(e); } } Task.Run(StartUpdateTagListAsync); } #endregion /// <summary> /// The current set of tags. /// </summary> public IReadOnlyList<TagData> Tags { get { activeTagsLock.Wait(); try { return new List<TagData>(activeTags.Values); } finally { activeTagsLock.Release(); } } } /// <summary> /// Connect to the server. /// </summary> public override void Connect(TcpClient client) { base.Connect(client); // Start the receiving thread. Task.Run(RunClientAsync); // Start getting an initial list of all the tags from the server. StartUpdateTagList(); } /// <summary> /// Batch write tag values. /// </summary> /// <param name="tagUpdates">A list of tag updates.</param> public Task WriteTagsAsync(IEnumerable<(TagData tag, object value)> tagUpdates) { var tags = new List<TagData>(); // Set the tag values without raising ValueUpdated events. foreach (var (tag, value) in tagUpdates) { tag.SetValue(value); // set value, no notify tags.Add(tag); } // Batch write the updates to the server. return WriteValuesAsync(tags); } /// <summary> /// Batch write tag values. /// </summary> /// <param name="tagUpdates">A list of tag updates.</param> public Task WriteTagsAsync(params (TagData tag, object value)[] tagUpdates) { return WriteTagsAsync((IEnumerable<(TagData tag, object value)>)tagUpdates); } } /// <summary> /// The Built-in Tag Service Protocol. /// </summary> /// <remarks> /// <para> /// Must be marked with the <see cref="ProtocolAddressEditorAttribute"/> in order for NetServer to show /// this protocol in its drop-down. NetServer only looks for protocols with this attribute. /// </para> /// <para> /// If you need additional information in the protocol address in order to identify a server, then you /// should also set <see cref="ProtocolAddressEditorAttribute.Editor"/> to an instance of a public class. /// Your editor class must inherit from <see cref="ProtocolAddressPropertyBagEditor"/>. Public properties /// on your editor class will be displayed in the Add Server Wizard and in the Address properties of your /// Tag Server. /// </para> /// <para> /// To have your protocol appear in the AddServer wizard, set the <see cref="ProtocolAddressEditorAttribute.ShowInAddServer"/> /// property. /// </para> /// <para> /// This example is based on NetServer and the <see cref="INotifyDirectTagAccessService"/>. /// See <see cref="BuiltinTagProtocol"/> for an equivalent example and definitions of the supporting classes. /// </para> /// </remarks> [ProtocolAddressEditor(DisplayName = "Built-in Tag Service" /*, ShowInAddServer = true */)] public class BuiltinTagServiceProtocol : Protocol { /// <summary> /// The Built-in Tag Service. /// </summary> /// <remarks> /// This is a singleton, just one exists serving port 6735. /// </remarks> class BuiltinTagService { /// <summary> /// Singleton configuration property bag. /// </summary> internal static ServerConfiguration Config { get; } = new ServerConfiguration(); event Action? OnStopServer; // Occurs when the tcp listener is stopped. event Action? OnTagListChanged; // Occurs when the tag list changes (tags added or removed). event Action<IEnumerable<ServiceTag>>? OnTagsWritten; // Occurs when tag values are updated. readonly Dictionary<int, ServiceTag> activeTags = new Dictionary<int, ServiceTag>(); TcpListener? tcp; BuiltinTagService() { } #region Tag Management internal class ServiceTag : BuiltinTag { /// <summary> /// Occurs when the tag value is written by the model. /// </summary> internal event Action<IEnumerable<ServiceTag>>? ValueUpdated; static int nextTagId; readonly ServiceTag[] thisTag; internal int TagID { get; } /// <summary> /// Constructs a new Tag. /// </summary> /// <param name="address">The tag address.</param> /// <param name="tagType">The tag data type.</param> /// <param name="releaseTag">Action to release the tag.</param> internal ServiceTag(IAddress address, DataType tagType, Action<BuiltinTag> releaseTag) : base(address, tagType, releaseTag) { this.TagID = Interlocked.Increment(ref nextTagId); thisTag = new ServiceTag[] { this }; } internal void NotifyValueUpdated() { this.ValueUpdated?.Invoke(thisTag); } } internal void NotifyTagsWritten(IEnumerable<ServiceTag> tags) { OnTagsWritten?.Invoke(tags); } Task NotifyTagAccessParametersChanged(bool sync, (DirectTag tag, AccessParameters accessParameters) args) { OnTagListChanged?.Invoke(); return Task.CompletedTask; } /// <summary> /// Returns a <see cref="BuiltinTag"/> for accessing an address. /// </summary> /// <param name="address">The address to access.</param> /// <returns>A <see cref="BuiltinTag"/> for accessing the address.</returns> /// <remarks> /// This server does not have a symbol table. Instead it 'learns' the names of the tags from /// calls to this method. The IAddress passed in will have the name of the tag and the expected /// data type. We use this to construct our list of activeTags. /// </remarks> internal BuiltinTag GetTag(IAddress address) { var type = address.GetDataType(); if (type == null || type.SystemType == typeof(object)) { throw new ArgumentException($"Must provide a data type for address \"{address}\""); } var tag = new ServiceTag(address, type, ReleaseTag); tag.ValueUpdated += NotifyTagsWritten; tag.PreferredParametersChangedAsync += NotifyTagAccessParametersChanged; lock (activeTags) { activeTags.Add(tag.TagID, tag); } OnTagListChanged?.Invoke(); return tag; } /// <summary> /// Called by <see cref="BuiltinTag.Dispose"/> when the tag is no longer needed. /// </summary> /// <param name="tag">The <see cref="BuiltinTag"/> being released.</param> internal void ReleaseTag(BuiltinTag tag) { if (tag is ServiceTag serviceTag) { lock (activeTags) { activeTags.Remove(serviceTag.TagID); } OnTagListChanged?.Invoke(); } } #endregion #region Service Client Requests /// <summary> /// Services one client connection. /// </summary> class ServiceClient : BuiltinTagServicePeer { readonly BuiltinTagService service; readonly object subscribedLock = new(); readonly HashSet<ServiceTag> subscribed = new(); // The list of tags the client has subscribed to. readonly object tagsWrittenLock = new(); HashSet<ServiceTag> tagsWritten = new(); // The list of tags whose value has changed since the last Publish. internal ServiceClient(BuiltinTagService service, TcpClient client) : base(client) { this.service = service; } /// <summary> /// Forces the client socket to shutdown in response to an error. /// </summary> /// <param name="e">The error prompting the shutdown.</param> void Shutdown(Exception e) { Shutdown(); ComponentLogger.Log(LogMessage.LogLevel.Error, $"{this}: {e.Message}", e); } /// <summary> /// Nop does nothing except return a response. /// Its use allows a client to turn an asynchronous protocol request into a synchronous protocol request. /// </summary> /// <param name="requestId">The clients request id.</param> async Task NopAsync(ulong requestId) { await WriteMessageAsync(requestId, RequestType.Nop, null).ConfigureAwait(false); } /// <summary> /// Lists all the tags and their meta data. /// </summary> /// <param name="requestId">The clients request id.</param> async Task ListTagsAsync(ulong requestId) { var response = new MemoryStream(); using (var writer = new BinaryWriter(response)) { lock (service.activeTags) { foreach (var tag in service.activeTags.Values) { writer.Write(tag.TagID); writer.Write(tag.AccessName); writer.Write((int)tag.AccessRights); writer.Write(tag.TagType!.SystemType!.FullName!); } } } await WriteMessageAsync(requestId, RequestType.ListTags, response.ToArray()).ConfigureAwait(false); } /// <summary> /// Returns the current values of a list of tags. /// </summary> /// <param name="requestId">The clients request id.</param> /// <param name="message">The list of tag id's.</param> Task ReadValuesAsync(ulong requestId, byte[] message) { var request = new MemoryStream(message); var response = new MemoryStream(); using (var reader = new BinaryReader(request)) { using var writer = new BinaryWriter(response); while (request.Position < request.Length) { var tagId = reader.ReadInt32(); Type? type = null; object? value = null; lock (service.activeTags) { if (service.activeTags.TryGetValue(tagId, out var tag)) { type = tag.TagType?.SystemType; value = tag.Value; } } writer.Write(type != null); if (type != null && value != null) MarshalValue(writer, type, value); } } return WriteMessageAsync(requestId, RequestType.ReadValues, response.ToArray()); } /// <summary> /// Updates tags with new values. /// </summary> /// <param name="message">The tag id's and values.</param> Task WriteValuesAsync(byte[] message) { var request = new MemoryStream(message); using (var batchNotify = new DirectTag.BatchNotify()) { using var reader = new BinaryReader(request); while (request.Position < request.Length) { var tagId = reader.ReadInt32(); lock (service.activeTags) { if (service.activeTags.TryGetValue(tagId, out var tag)) { var value = UnmarshalValue(reader, tag.TagType!.SystemType!); if ((tag.AccessRights & AccessRights.ReadFromPLC) != 0) { tag.UpdateValue(batchNotify, value, null); } } } } } return Task.CompletedTask; } /// <summary> /// Subscribes to a list of tags. /// </summary> /// <param name="message">The id's of the tags to subscribe to.</param> Task SubscribeAsync(byte[] message) { var request = new MemoryStream(message); using (var reader = new BinaryReader(request)) { while (request.Position < request.Length) { var tagId = reader.ReadInt32(); lock (service.activeTags) { if (service.activeTags.TryGetValue(tagId, out var tag)) { if ((tag.AccessRights & AccessRights.WriteToPLC) != 0) { lock (subscribedLock) { subscribed.Add(tag); } } } } } } return Task.CompletedTask; } /// <summary> /// Services a single client request. /// </summary> async Task ServiceClientAsync() { var (messageId, requestType, message) = await ReadMessageAsync().ConfigureAwait(false); switch (requestType) { case RequestType.Nop: await NopAsync(messageId).ConfigureAwait(false); break; case RequestType.ListTags: await ListTagsAsync(messageId).ConfigureAwait(false); break; case RequestType.ReadValues: await ReadValuesAsync(messageId, message).ConfigureAwait(false); break; case RequestType.WriteValues: await WriteValuesAsync(message).ConfigureAwait(false); break; case RequestType.Subscribe: await SubscribeAsync(message).ConfigureAwait(false); break; default: throw new Exception("Unexpected request"); } } /// <summary> /// Called when a tag is added or removed. /// </summary> void Service_OnTagListChanged() { void FilterOut(object syncRoot, HashSet<ServiceTag> hashSet) { lock (syncRoot) { var removed = new List<ServiceTag>(); foreach (var tag in hashSet) { if ( !service.activeTags.ContainsKey(tag.TagID) // tag no longer valid || (tag.AccessRights & AccessRights.WriteToPLC) == 0) { // tag no longer has WriteToPLC access removed.Add(tag); } } foreach (var tag in removed) { hashSet.Remove(tag); } } } lock (service.activeTags) { FilterOut(tagsWrittenLock, tagsWritten); FilterOut(subscribedLock, subscribed); } async void SendTagListChanged() { try { await WriteMessageAsync(0, RequestType.TagListChanged, null).ConfigureAwait(false); } catch (Exception e) { Shutdown(e); } } SendTagListChanged(); } /// <summary> /// Called when a tag value is updated. /// </summary> /// <param name="tags">Tags whose value has been updated.</param> void Service_OnTagsWritten(IEnumerable<ServiceTag> tags) { // A separate thread/task to publish tag values to the client. async Task PublishAsync() { try { // Delay for a short (configurable) time. This allows sequences of tag updates to be // batched together and sent to the client in a single protocol request. await Task.Delay(Config.UpdateRate).ConfigureAwait(false); // Get a list of tags to publish, and clear the tagsWritten list. HashSet<ServiceTag> publishTags; var newTagsWritten = new HashSet<ServiceTag>(); lock (tagsWrittenLock) { publishTags = tagsWritten; tagsWritten = newTagsWritten; } // Create and send the publish protocol message to the client. var publication = new MemoryStream(); using (var writer = new BinaryWriter(publication)) { foreach (var tag in publishTags) { writer.Write(tag.TagID); var val = tag.Value; if (val != null) MarshalValue(writer, tag.TagType!.SystemType!, val); } } var message = publication.ToArray(); if (message.Length != 0) await WriteMessageAsync(0, RequestType.Publish, message).ConfigureAwait(false); } catch (Exception e) { Shutdown(e); } } bool publish = false; // Whether to start publishing the updates. // Update tagsWritten to record the set of tags whose value has changed. lock (subscribedLock) { lock (tagsWrittenLock) { foreach (var tag in tags) { if (subscribed.Contains(tag)) { tagsWritten.Add(tag); publish = tagsWritten.Count == 1; } } } } // When the first tag is entered into tagsWritten, we start a new thread/task // to publish the tag values to the client. if (publish) Task.Run(PublishAsync); } /// <summary> /// The main loop receiving and processing messages received from the client. /// </summary> internal async Task RunClientAsync() { try { service.OnStopServer += Shutdown; service.OnTagListChanged += Service_OnTagListChanged; service.OnTagsWritten += Service_OnTagsWritten; for(;;) { await ServiceClientAsync().ConfigureAwait(false); } } catch (Exception e) { ComponentLogger.Log(LogMessage.LogLevel.Error, $"Builtin Tag Service client {ToString()}: {e.Message}", e); } finally { service.OnStopServer -= Shutdown; service.OnTagListChanged -= Service_OnTagListChanged; service.OnTagsWritten -= Service_OnTagsWritten; Close(); } } } #endregion #region Server /// <summary> /// Force the tcp listener to stop, and raise the OnStopServer event to shut down the clients. /// </summary> void StopServer() { try { var tcp = this.tcp; this.tcp = null; tcp?.Stop(); } catch { } try { OnStopServer?.Invoke(); } catch { } } /// <summary> /// The main loop accepting new connections from new clients. /// </summary> async Task RunServerAsync() { try { var tcp = new TcpListener(IPAddress.Any, 6735); this.tcp = tcp; tcp.Start(); for(;;) { var tcpClient = await tcp.AcceptTcpClientAsync().ConfigureAwait(false); var client = new ServiceClient(this, tcpClient); var clientTask = Task.Run(client.RunClientAsync); } } catch (ObjectDisposedException) { // StopServer() was called. } catch (Exception e) { ComponentLogger.Log(LogMessage.LogLevel.Error, $"Builtin Tag Service: {e.Message}", e); } StopServer(); } readonly static BuiltinTagService instance = new BuiltinTagService(); static int refCount; /// <summary> /// Starts the service. /// </summary> internal static BuiltinTagService Start() { if (Interlocked.Increment(ref refCount) == 1) { Task.Run(instance.RunServerAsync); } return instance; } /// <summary> /// Stops the service. /// </summary> internal void Stop() { if (Interlocked.Decrement(ref refCount) == 0) { instance.StopServer(); } } #endregion } /// <summary> /// An instance of one connection to the Built-in Tag Service. /// </summary> /// <remarks> /// There's only one <see cref="BuiltinTagService"/>, but /// there may be many connections from within Demo3D to the same service. /// </remarks> class BuiltinConnection : ProtocolInstance, INotifyDirectTagAccessService, ISymbolTable { BuiltinTagService? service; /// <summary> /// Constructs a new <see cref="ProtocolInstance"/> for the Built-in Tag Service Protocol. /// </summary> /// <param name="protocol">The protocol; a required parameter of <see cref="ProtocolInstance"/>.</param> /// <param name="head">The protocol head; a required parameter of <see cref="ProtocolInstance"/>.</param> /// <param name="peerAddress">The address of the server being connected to.</param> /// <param name="configuration">Server configuration properties.</param> /// <remarks> /// <para> /// If your connection would benefit from user configurable properties (such as IO timeout configuration) /// then you should create a public class and pass an instance of it to the 'propertyBag' parameter of /// <see cref="ProtocolInstance(Protocol, ProtocolHead, ProtocolAddress, bool, object)"/>. Public properties /// on your class will be displayed in the Connection properties of your Tag Server. /// </para> /// <para> /// <see cref="ServerConfiguration"/> is an example - it's entirely optional. You can pass null into /// <see cref="ProtocolInstance(Protocol, ProtocolHead, ProtocolAddress, bool, object)"/> instead. /// </para> /// </remarks> internal BuiltinConnection(Protocol protocol, ProtocolHead head, ProtocolAddress peerAddress, ServerConfiguration configuration) : base(protocol, head, peerAddress, false, configuration) { } #region Connection /// <summary> /// Returns true if the connection has been established. /// </summary> protected override bool InternalRunning { get { return service != null; } } /// <summary> /// Connects to the server. /// </summary> /// <param name="sync">If true, the Task returned must be guaranteed to be complete.</param> /// <param name="flags">The flags used to open the connection.</param> /// <returns>Nothing.</returns> protected override Task InternalOpenAsync(bool sync, Flags flags) { service = BuiltinTagService.Start(); return Task.CompletedTask; } /// <summary> /// Disconnects from the server. /// </summary> protected override void InternalClose() { service?.Stop(); service = null; } #endregion #region IDirectTagAccessService /// <summary> /// Gets the servers symbol table. /// </summary> /// <param name="sync">If true, the Task returned must be guaranteed to be complete.</param> /// <returns>The root symbol.</returns> /// <remarks> /// This server does not provide a symbol table. Instead it 'learns' the names of the tags that /// it exposes to its clients based on the addresses passed into calls to /// <see cref="IDirectTagAccessService.GetTagAsync(bool, IAddress)"/>. /// </remarks> Task<IBrowseItem?> IDirectTagAccessService.GetSymbolTableAsync(bool sync) { return Task.FromResult<IBrowseItem?>(null); } Task<IBrowseItem?> ISymbolTable.GetSymbolTableAsync(bool sync) { return Task.FromResult<IBrowseItem?>(null); } CanReadSymbolsFlags ISymbolTable.CanReadSymbols => CanReadSymbolsFlags.No; /// <summary> /// Returns the .Net type of the addresses expected by this protocol. /// The type must implement <see cref="IAddress"/>. /// </summary> /// <remarks> /// We don't care about the address type, since we learn the symbols from calls to GetTagAsync. /// </remarks> Type IDirectTagAccessService.AddressType => typeof(IAddress); /// <summary> /// Returns an object for accessing a tag in the server. /// </summary> /// <param name="sync">If true, the Task returned must be guaranteed to be complete.</param> /// <param name="address">The address of the tag (the symbol).</param> /// <returns>A <see cref="DirectTag"/> object for accessing the tag.</returns> Task<DirectTag?> IDirectTagAccessService.GetTagAsync(bool sync, IAddress address) { if (service == null) throw new ClosedException(); return Task.FromResult<DirectTag?>(service.GetTag(address)); } /// <summary> /// Vectored read request. /// </summary> /// <param name="requests">List of requests.</param> /// <returns>Object to implement IO.</returns> VectoredRequests IVectoredTagService<DirectTag>.InternalReadV(IReadOnlyList<VectoredTagRequest<DirectTag>> requests) { return requests.ReadSequentially<BuiltinTagService.ServiceTag>(tag => tag.Value); } /// <summary> /// Vectored write request. /// </summary> /// <param name="requests">List of requests.</param> /// <returns>Object to implement IO.</returns> VectoredRequests IVectoredTagService<DirectTag>.InternalWriteV(IReadOnlyList<VectoredTagRequest<DirectTag>> requests) { return requests.WriteSequentially<BuiltinTagService.ServiceTag>((tag, value) => { tag.Value = value; tag.NotifyValueUpdated(); }); } /// <summary> /// Returns true if data changes can be subscribed to. /// </summary> bool INotifyDirectTagAccessService.CanSubscribe { get { return true; } } #endregion } #region Register Protocol /// <summary> /// Constructs the Built-in Tag Service Protocol. /// </summary> /// <remarks> /// <para> /// The name passed in to the <see cref="Protocol"/> constructor will become the 'scheme' part of the /// protocol address URL. /// </para> /// <para> /// This example protocol supports one service, the <see cref="INotifyDirectTagAccessService"/>. This service /// is used to implement a simple tag service. /// </para> /// </remarks> BuiltinTagServiceProtocol() : base("BuiltinTagService", typeof(INotifyDirectTagAccessService)) { } /// <summary> /// Registers the protocol. You should call this method once from your code at start-up. /// </summary> internal static void Register() { Registry.Register(new BuiltinTagServiceProtocol()); } #endregion /// <summary> /// Creates a new instance of the protocol. /// </summary> /// <param name="head">A required parameter of the <see cref="ProtocolInstance"/> constructor.</param> /// <param name="protocolAddress">The address of the new connection.</param> /// <returns>A new instance of the protocol.</returns> protected override ProtocolInstance NewInstance(ProtocolHead head, ProtocolAddress protocolAddress) { return new BuiltinConnection(this, head, protocolAddress, BuiltinTagService.Config); } } }