Click or drag to resize

Demo3D.PLC.Comms.Builtin Namespace

The Demo3D.PLC.Comms.Builtin namespace provides default and example protocol implementations.
Classes
 ClassDescription
Public classBuiltinAddressSpace The address space for BuiltinMemAddress addresses.
Public classBuiltinMemAddress A Built-in Memory Server address.
Public classBuiltinMemoryProtocol The Built-in Memory Server Protocol.
Public classBuiltinMemoryProtocolBuitinConnection An instance of one connection to the Built-in Memory Server.
Public classBuiltinMixedProtocol The Built-in Mixed Server Protocol.
Public classBuiltinMixedProtocolBuiltinMixedConnection An instance of one connection to the Built-in Mixed Server.
Public classBuiltinNotifyMemoryProtocol The Built-in Notify Memory Server Protocol - a memory server with memory change notification.
Public classBuiltinNotifyMemoryProtocolBuitinConnection An instance of one connection to the Built-in Notify Memory Server. This extends the BuiltinMemoryProtocolBuitinConnection example to include the INotifyDirectMemoryAccessService.
Public classBuiltinTag A tag for accessing a Built-in Tag Server symbol.
Public classBuiltinTagAddress An example class for configuring the protocol address. Must inherit from ProtocolAddressPropertyBagEditor to use it with ProtocolAddressEditorAttribute as below.
Public classBuiltinTagProtocol The Built-in Tag Server Protocol.
Public classBuiltinTagProtocolBuiltinConnection An instance of one connection to the Built-in Tag Server.
Public classBuiltinTagServiceClient Exposes a tag list from the server.
Public classBuiltinTagServiceClientTagData TagData from the Demo3D server.
Public classBuiltinTagServicePeer Provides message passing and other utility functions for the BuiltinTagService protocol.
Public classBuiltinTagServiceProtocol The Built-in Tag Service Protocol.
Public classExampleCombinedServer An example server expecting some data to be accessed by tag name, and some to be accessed by memory address.
Public classExampleMemoryServer The Built-in Memory Server.
Public classExampleNotifyMemoryServer An extended Built-in Memory Server based on ExampleMemoryServer.
Public classExampleTagServer The Built-in Tag Server.
Public classServerConfiguration An example class for configuring the protocol properties.
Public classSymbol A symbol in the Built-in Tag Server symbol table.
Delegates
 DelegateDescription
Public delegateBuiltinTagServiceClientTagListChangedDelegate Represents a method that handles the TagListChanged event.
Enumerations
 EnumerationDescription
Protected enumerationBuiltinTagServicePeerRequestType List of protocol request types.
Example

This example is based on NetServer and the INotifyDirectTagAccessService.

C#
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());
        }
    }
}