Demo |
| 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; using System.Collections.Generic; using System.ComponentModel; using System.Linq; 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> /// Your server doesn't have to expose a symbol table at all, so this class is optional. /// But it's useful if you can, and very helpful for the user. /// </para> /// <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 allowed access rights for the symbol.</param> /// <param name="dataType">The symbol data type.</param> public Symbol(IBrowseItem symbolTable, string name, AccessRights access, DataType dataType) : base(symbolTable, name, true, false) { this.AllowedAccess = access; this.DataType = dataType; } /// <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 allowed access rights for the symbol.</param> /// <param name="dataType">The symbol data type.</param> public Symbol(IBrowseItem symbolTable, string name, AccessRights access, Type dataType) : this(symbolTable, name, access, 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, /// or simply not override it. /// </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 { /// <summary> /// The current value of the tag. /// </summary> /// <remarks> /// This is a cache of the data read/written to the PLC/server. /// </remarks> 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> public BuiltinTag(IAddress address, DataType tagType) : base(address, tagType) { } /// <summary> /// Called by our example 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> /// <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) { this.Value = value; NotifyDataChanged(batchNotify, value); } } /// <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; } } #region Example Server /// <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 = []; 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 = [ "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 (r == null) throw new NullReferenceException(); 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; } void PopulateSingleDim(IList array, Type elementType, IReadOnlyList<DataDimension> dimensions, int level) { var nextLevel = level +1; if (nextLevel != dimensions.Count) { for (int i = 0; i < array.Count; i++) PopulateArray((IList)array[i]!, elementType, dimensions, nextLevel); } else { for (int i = 0; i < array.Count; i++) array[i] = RandomValue(elementType); } } void PopulateMultiDim(Array array, Type elementType, IReadOnlyList<DataDimension> dimensions, int level) { var nextLevel = level +1; static IEnumerable<long[]> ForeachElement(IReadOnlyList<DataArrayBounds> bounds) { var indices = new long[bounds.Count]; for (;;) { yield return indices; for (int r = bounds.Count -1; ++indices[r] == bounds![r].Length; indices[r--] = 0) if (r == 0) yield break; } } if (nextLevel != dimensions.Count) { foreach (var indices in ForeachElement(dimensions[level].Bounds)) { PopulateArray((IList)array.GetValue(indices)!, elementType, dimensions, nextLevel); } } else { foreach (var indices in ForeachElement(dimensions[level].Bounds)) { array.SetValue(RandomValue(elementType), indices); } } } void PopulateArray(IList array, Type elementType, IReadOnlyList<DataDimension> dimensions, int level = 0) { if (dimensions[level].Rank > 1) PopulateMultiDim((Array)array, elementType, dimensions, level); else PopulateSingleDim(array, elementType, dimensions, level); } object? RandomArray(DataType dataType) { var dataArray = dataType.CreateArray(); PopulateArray(dataArray, dataType.ElementType, dataType.Dimensions); return dataArray; } while (connected) { using (var batchNotify = new DirectTag.BatchNotify()) { lock (activeTags) { var pe1 = activeTags.FirstOrDefault(t => t.AccessName == "PE1"); var pe2 = activeTags.FirstOrDefault(t => t.AccessName == "PE2"); var pe3 = activeTags.FirstOrDefault(t => t.AccessName == "PE3"); var mot2_running = activeTags.FirstOrDefault(t => t.AccessName == "MOT2_RUNNING"); var mot3_running = activeTags.FirstOrDefault(t => t.AccessName == "MOT3_RUNNING"); var mot4_running = activeTags.FirstOrDefault(t => t.AccessName == "MOT4_RUNNING"); var mot1_run = activeTags.FirstOrDefault(t => t.AccessName == "MOT1_RUN"); var mot2_run = activeTags.FirstOrDefault(t => t.AccessName == "MOT2_RUN"); var mot3_run = activeTags.FirstOrDefault(t => t.AccessName == "MOT3_RUN"); var inputTags = new HashSet<BuiltinTag?>() { pe1, pe2, pe3, mot2_running, mot3_running, mot4_running }; var diebackTags = new HashSet<BuiltinTag?>() { pe1, pe2, pe3, mot2_running, mot3_running, mot4_running, mot1_run, mot2_run, mot3_run }; diebackTags.Remove(null); foreach (var tag in activeTags) { if (!tag.AccessRights.CanWriteToPLC() && tag.TagType != null && !diebackTags.Contains(tag)) { var value = tag.TagType.IsArray ? RandomArray(tag.TagType) : RandomValue(tag.TagType?.ElementType); if (value != null) tag.UpdateValue(batchNotify, value); } } if (diebackTags.Count == 9) { var values = true; foreach (var t in inputTags) if (t!.Value == null) values = false; if (values) { mot1_run!.UpdateValue(batchNotify, ((bool)pe1!.Value! && (bool)mot2_running!.Value!) || !(bool)pe1!.Value!); mot2_run!.UpdateValue(batchNotify, ((bool)pe2!.Value! && (bool)mot3_running!.Value!) || !(bool)pe2!.Value!); mot3_run!.UpdateValue(batchNotify, ((bool)pe3!.Value! && (bool)mot4_running!.Value!) || !(bool)pe3!.Value!); } } // echo UDT tags var udt1 = activeTags.FirstOrDefault(t => t.AccessName == "UDT1"); var udt2 = activeTags.FirstOrDefault(t => t.AccessName == "UDT2"); if (udt1 != null && udt2 != null) { var udt2Value = udt2.Value; if (udt1.Value != udt2Value && udt2Value != null) { udt1.UpdateValue(batchNotify, udt2Value); } } } } 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 static IBrowseItem ReadSymbols() { var symbolTable = new BrowseItemBranch(); symbolTable.Add(new Symbol(symbolTable, "PE1", AccessRights.WriteToPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "PE2", AccessRights.WriteToPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "PE3", AccessRights.WriteToPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "MOT2_RUNNING", AccessRights.WriteToPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "MOT3_RUNNING", AccessRights.WriteToPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "MOT4_RUNNING", AccessRights.WriteToPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "MOT1_RUN", AccessRights.ReadFromPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "MOT2_RUN", AccessRights.ReadFromPLC, typeof(bool))); symbolTable.Add(new Symbol(symbolTable, "MOT3_RUN", AccessRights.ReadFromPLC, typeof(bool))); 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))); symbolTable.Add(new Symbol(symbolTable, "Double2", AccessRights.WriteToPLC, typeof(double))); symbolTable.Add(new Symbol(symbolTable, "Array1", AccessRights.Bidirectional, DataType.Double.MakeArray(10))); // double[10] symbolTable.Add(new Symbol(symbolTable, "Array2", AccessRights.Bidirectional, DataType.Double.MakeArray([ [ 3, 3 ] ]))); // double[3,3] var udt = new DataType.Builder("UDT") { { "BoolA", typeof(bool) }, { "BoolB", typeof(bool) }, { "IntA", typeof(int) }, { "IntB", typeof(int) }, }.Build(); symbolTable.Add(new Symbol(symbolTable, "UDT1", AccessRights.ReadFromPLC, udt)); symbolTable.Add(new Symbol(symbolTable, "UDT2", AccessRights.WriteToPLC, udt)); return symbolTable; } /// <summary> /// Returns a <see cref="BuiltinTag"/> for accessing <see cref="Symbol"/> data. /// </summary> /// <param name="symbols">The list of <see cref="Symbol"/> to access.</param> /// <returns>A <see cref="BuiltinTag"/> for accessing the <see cref="Symbol"/>.</returns> public IReadOnlyList<DirectTag> GetTags(IEnumerable<Symbol> symbols) { var list = new List<DirectTag>(); lock (activeTags) { foreach (var symbol in symbols) { var tag = new BuiltinTag(symbol, symbol.DataType); activeTags.Add(tag); list.Add(tag); } } return list; } /// <summary> /// Called when tags are no longer needed. /// </summary> public void DropTags(IEnumerable<BuiltinTag> tags) { lock (activeTags) { foreach (var tag in tags) { activeTags.Remove(tag); } } } } #endregion /// <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 => 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> 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 Task InternalCloseAsync(bool sync) { server?.Disconnect(); server = null; symbols = null; return Task.CompletedTask; } #endregion #region INotifyDirectTagAccessService /// <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 may 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 = ExampleTagServer.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> /// <remarks> /// If your server does not offer a symbol table, then you can return <see cref="IAddress"/> and you'll /// get any type of address, or perhaps <see cref="StringAddress"/> if you only want the string name of /// the symbol to access. /// </remarks> 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="tagRequests">The list of addresses of the tags (the symbols).</param> /// <returns>A list of <see cref="DirectTag"/> object for accessing the tag.</returns> /// <remarks> /// The type of the <paramref name="tagRequests"/> <see cref="TagRequest.Address"/> property will be the /// type returned by <see cref="IDirectTagAccessService.AddressType"/> above. /// </remarks> Task<IReadOnlyList<DirectTag>> IDirectTagAccessService.GetTagsAsync(bool sync, IReadOnlyList<DirectTagRequest> tagRequests) { if (server == null) throw new ClosedException(); return Task.FromResult(server.GetTags(tagRequests.Select(a => (Symbol)a.Address))); } /// <summary> /// Called to indicate that tag accesses are no longer required. /// </summary> /// <param name="sync">If true, the Task returned is guaranteed to be complete.</param> /// <param name="tags">Tags to close.</param> /// <param name="userState">Private user data.</param> Task IDirectTagAccessService.NotifyTagsClosingAsync(bool sync, IEnumerable<DirectTag> tags, object? userState) { server?.DropTags(tags.Cast<BuiltinTag>()); return Task.CompletedTask; } /// <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) { // The InternalReadV method is called with a list of read requests, and is asking you to return an object that, // when Executed, will read all the tags and return their values. // // In most cases it'd be better to do all this IO in as few real IO requests as possible, batching as many reads // into each request as you can. But for this example where we simply read the value our of the tag value cache, // we can just read each tag one by one in a simple loop. // // The ReadSequentially method will return a simple VectoredRequest that'll do just that. But for more efficient // IO, you may choose to replace this. 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); } #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 IDisposable? 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()); } } }