From a8d769c478dbff897a80841137ff34fd5b54a689 Mon Sep 17 00:00:00 2001 From: Gareth Potter Date: Tue, 30 Sep 2025 17:00:57 +0100 Subject: [PATCH 1/7] wip --- src/ThingSet.Client/Schema/ThingSetNode.cs | 1 + src/ThingSet.Client/Schema/ThingSetSchema.cs | 1 + src/ThingSet.Client/ThingSetClient.cs | 20 +- .../CanClientTransport.cs | 2 +- .../CanServerTransport.cs | 20 +- .../ThingSet.Common.Transports.Can.csproj | 4 +- .../IpClientTransport.cs | 23 +- .../IpServerTransport.cs | 131 +++++++++++ src/ThingSet.Common.Transports.Ip/Protocol.cs | 22 ++ .../ThingSet.Common.Transports.Ip.csproj | 2 +- src/ThingSet.Common/Nodes/ThingSetFunction.cs | 44 ++++ .../Nodes/ThingSetFunctionParameter.cs | 16 ++ src/ThingSet.Common/Nodes/ThingSetGroup.cs | 21 ++ src/ThingSet.Common/Nodes/ThingSetNode.cs | 56 +++++ .../Nodes/ThingSetParentNode.cs | 23 ++ src/ThingSet.Common/Nodes/ThingSetProperty.cs | 17 ++ ...equest.cs => ThingSetBinaryRequestType.cs} | 2 +- ...tRequest.cs => ThingSetTextRequestType.cs} | 2 +- src/ThingSet.Common/ThingSet.Common.csproj | 4 +- .../{Protocols => }/ThingSetType.cs | 77 ++++++- .../Transports/IServerTransport.cs | 5 +- src/ThingSet.Net.sln | 6 + src/ThingSet.Server/ThingSet.Server.csproj | 31 +++ src/ThingSet.Server/ThingSetServer.cs | 204 ++++++++++++++++++ test/ThingSet.Test/TestClientServer.cs | 45 ++++ test/ThingSet.Test/TestIpClientTransport.cs | 27 +-- test/ThingSet.Test/TestNodes.cs | 21 ++ test/ThingSet.Test/ThingSet.Test.csproj | 10 +- 28 files changed, 783 insertions(+), 54 deletions(-) create mode 100644 src/ThingSet.Common.Transports.Ip/IpServerTransport.cs create mode 100644 src/ThingSet.Common.Transports.Ip/Protocol.cs create mode 100644 src/ThingSet.Common/Nodes/ThingSetFunction.cs create mode 100644 src/ThingSet.Common/Nodes/ThingSetFunctionParameter.cs create mode 100644 src/ThingSet.Common/Nodes/ThingSetGroup.cs create mode 100644 src/ThingSet.Common/Nodes/ThingSetNode.cs create mode 100644 src/ThingSet.Common/Nodes/ThingSetParentNode.cs create mode 100644 src/ThingSet.Common/Nodes/ThingSetProperty.cs rename src/ThingSet.Common/Protocols/Binary/{ThingSetRequest.cs => ThingSetBinaryRequestType.cs} (87%) rename src/ThingSet.Common/Protocols/Text/{ThingSetRequest.cs => ThingSetTextRequestType.cs} (94%) rename src/ThingSet.Common/{Protocols => }/ThingSetType.cs (50%) create mode 100644 src/ThingSet.Server/ThingSet.Server.csproj create mode 100644 src/ThingSet.Server/ThingSetServer.cs create mode 100644 test/ThingSet.Test/TestClientServer.cs create mode 100644 test/ThingSet.Test/TestNodes.cs diff --git a/src/ThingSet.Client/Schema/ThingSetNode.cs b/src/ThingSet.Client/Schema/ThingSetNode.cs index df1945b..7df9d63 100644 --- a/src/ThingSet.Client/Schema/ThingSetNode.cs +++ b/src/ThingSet.Client/Schema/ThingSetNode.cs @@ -5,6 +5,7 @@ */ using System; using System.Collections.ObjectModel; +using ThingSet.Common; using ThingSet.Common.Protocols; namespace ThingSet.Client.Schema; diff --git a/src/ThingSet.Client/Schema/ThingSetSchema.cs b/src/ThingSet.Client/Schema/ThingSetSchema.cs index 6243454..2b24d55 100644 --- a/src/ThingSet.Client/Schema/ThingSetSchema.cs +++ b/src/ThingSet.Client/Schema/ThingSetSchema.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using ThingSet.Common; using ThingSet.Common.Protocols; namespace ThingSet.Client.Schema; diff --git a/src/ThingSet.Client/ThingSetClient.cs b/src/ThingSet.Client/ThingSetClient.cs index d045b2b..a371584 100644 --- a/src/ThingSet.Client/ThingSetClient.cs +++ b/src/ThingSet.Client/ThingSetClient.cs @@ -85,17 +85,17 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Get(uint id) { - return DoRequest(ThingSetRequest.Get, cw => cw.WriteUInt32(id)); + return DoRequest(ThingSetBinaryRequestType.Get, cw => cw.WriteUInt32(id)); } public object? Get(string path) { - return DoRequest(ThingSetRequest.Get, cw => cw.WriteTextString(path)); + return DoRequest(ThingSetBinaryRequestType.Get, cw => cw.WriteTextString(path)); } public object? Fetch(uint id, object arg) { - return DoRequest(ThingSetRequest.Fetch, cw => + return DoRequest(ThingSetBinaryRequestType.Fetch, cw => { cw.WriteUInt32(id); CborSerialiser.Write(cw, arg); @@ -104,7 +104,7 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Fetch(uint id, params object[] args) { - return DoRequest(ThingSetRequest.Fetch, cw => + return DoRequest(ThingSetBinaryRequestType.Fetch, cw => { cw.WriteUInt32(id); if (args.Length == 0) @@ -120,7 +120,7 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Fetch(string path) { - return DoRequest(ThingSetRequest.Fetch, cw => + return DoRequest(ThingSetBinaryRequestType.Fetch, cw => { cw.WriteTextString(path); cw.WriteNull(); @@ -129,7 +129,7 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Update(string fullyQualifiedName, object value) { - return DoRequest(ThingSetRequest.Update, cw => + return DoRequest(ThingSetBinaryRequestType.Update, cw => { int index = fullyQualifiedName.LastIndexOf('/'); string pathToParent = index > 0 ? fullyQualifiedName.Substring(0, index) : String.Empty; @@ -145,7 +145,7 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Exec(uint id, params object[] args) { - return DoRequest(ThingSetRequest.Exec, cw => + return DoRequest(ThingSetBinaryRequestType.Exec, cw => { cw.WriteUInt32(id); CborSerialiser.Write(cw, args); @@ -154,14 +154,14 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Exec(string path, params object[] args) { - return DoRequest(ThingSetRequest.Exec, cw => + return DoRequest(ThingSetBinaryRequestType.Exec, cw => { cw.WriteTextString(path); CborSerialiser.Write(cw, args); }); } - private object? DoRequest(ThingSetRequest action, Action write) + private object? DoRequest(ThingSetBinaryRequestType action, Action write) { byte[] buffer = new byte[4095]; Span span = buffer; @@ -169,7 +169,7 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options if (TargetNodeID.HasValue) { // prefix the request with node to forward to - span[0] = (byte)ThingSetRequest.Forward; + span[0] = (byte)ThingSetBinaryRequestType.Forward; CborWriter w = new CborWriter(CborConformanceMode.Lax); w.WriteTextString($"{TargetNodeID.Value:x}"); w.Encode(span.Slice(1)); diff --git a/src/ThingSet.Common.Transports.Can/CanClientTransport.cs b/src/ThingSet.Common.Transports.Can/CanClientTransport.cs index 22bdf0f..a9aaf51 100644 --- a/src/ThingSet.Common.Transports.Can/CanClientTransport.cs +++ b/src/ThingSet.Common.Transports.Can/CanClientTransport.cs @@ -197,7 +197,7 @@ private void RunSubscriptionThread() if (type == MultiFrameMessageType.Single || type == MultiFrameMessageType.Last) { ReadOnlyMemory memory = buffer.Buffer; - if (buffer.Buffer[0] == (byte)ThingSetRequest.Report) + if (buffer.Buffer[0] == (byte)ThingSetBinaryRequestType.Report) { NotifyReport(memory.Slice(1, buffer.Position - 1)); } diff --git a/src/ThingSet.Common.Transports.Can/CanServerTransport.cs b/src/ThingSet.Common.Transports.Can/CanServerTransport.cs index 6f56ed2..95f2ca1 100644 --- a/src/ThingSet.Common.Transports.Can/CanServerTransport.cs +++ b/src/ThingSet.Common.Transports.Can/CanServerTransport.cs @@ -5,6 +5,7 @@ */ using System; using System.Collections.Concurrent; +using System.IO; using System.Threading; using System.Threading.Tasks; using SocketCANSharp; @@ -27,7 +28,7 @@ public class CanServerTransport : CanTransportBase, IServerTransport private bool _runPeerSocketHandlers = true; - private Func? _messageCallback; + private Func, Memory>? _messageCallback; public CanServerTransport(ThingSetCanInterface canInterface) : base(canInterface, leaveOpen: false) { @@ -40,7 +41,9 @@ public CanServerTransport(ThingSetCanInterface canInterface) : base(canInterface _addressClaimListener.AddressClaimed += OnAddressClaimed; } - public ValueTask ListenAsync(Func callback) + public event EventHandler? Error; + + public ValueTask ListenAsync(Func, Memory> callback) { _addressClaimListener.Listen(); @@ -120,6 +123,7 @@ private int WriteFrame(uint canId, byte[] buffer) private int WriteFdFrame(uint canId, byte[] buffer) { + CanFdFrame frame = new CanFdFrame { CanId = canId, @@ -154,8 +158,16 @@ private void RunPeerSocketHandler(object? state) int read = socket.Read(buffer); if (read > 0 && _messageCallback is not null) { - byte[] response = _messageCallback(peerId, buffer); - socket.Write(response); + try + { + Memory memory = buffer; + Memory response = _messageCallback(peerId, memory.Slice(0, read)); + socket.Write(response.ToArray()); + } + catch (Exception ex) + { + Error?.Invoke(this, new ErrorEventArgs(ex)); + } } } } diff --git a/src/ThingSet.Common.Transports.Can/ThingSet.Common.Transports.Can.csproj b/src/ThingSet.Common.Transports.Can/ThingSet.Common.Transports.Can.csproj index 0214125..c74c4e5 100644 --- a/src/ThingSet.Common.Transports.Can/ThingSet.Common.Transports.Can.csproj +++ b/src/ThingSet.Common.Transports.Can/ThingSet.Common.Transports.Can.csproj @@ -9,7 +9,7 @@ https://github.com/Brill-Power/ThingSet.Net https://github.com/Brill-Power/ThingSet.Net.git git - CAN transport for .NET ThingSet client + CAN transport for .NET ThingSet client and server ThingSet.Common.Transports.Can Apache-2.0 README.md @@ -17,7 +17,7 @@ - + diff --git a/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs b/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs index 63e205e..c64fee8 100644 --- a/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs +++ b/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs @@ -11,14 +11,15 @@ using System.Threading; using System.Threading.Tasks; using ThingSet.Common.Protocols.Binary; +using static ThingSet.Common.Transports.Ip.Protocol; namespace ThingSet.Common.Transports.Ip; +/// +/// ThingSet client transport for IP (TCP/UDP). +/// public class IpClientTransport : IClientTransport { - private const int MessageSize = 512; - private const int MessageTypePosition = 4; - private readonly string _hostname; private readonly int _port; @@ -31,6 +32,10 @@ public class IpClientTransport : IClientTransport private Action>? _callback; + public IpClientTransport(string hostname) : this(hostname, Protocol.RequestResponsePort) + { + } + public IpClientTransport(string hostname, int port) { _hostname = hostname; @@ -70,7 +75,7 @@ public ValueTask SubscribeAsync(Action> callback) { _callback = callback; _udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - _udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 9002)); + _udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, Protocol.PublishSubscribePort)); _subscriptionThread.Start(); return ValueTask.CompletedTask; } @@ -113,7 +118,7 @@ private async void RunSubscriptionThread() buffer.Append(result.Buffer); } if ((messageType == MessageType.Last || messageType == MessageType.Single) && - buffer.Buffer[0] == (byte)ThingSetRequest.Report) + buffer.Buffer[0] == (byte)ThingSetBinaryRequestType.Report) { ReadOnlyMemory memory = buffer.Buffer; var len = memory.Length; @@ -157,12 +162,4 @@ public void Append(byte[] buffer) Position += buffer.Length - 2; } } - - private enum MessageType - { - First = 0x0 << MessageTypePosition, - Consecutive = 0x1 << MessageTypePosition, - Last = 0x2 << MessageTypePosition, - Single = 0x3 << MessageTypePosition, - } } diff --git a/src/ThingSet.Common.Transports.Ip/IpServerTransport.cs b/src/ThingSet.Common.Transports.Ip/IpServerTransport.cs new file mode 100644 index 0000000..803ee39 --- /dev/null +++ b/src/ThingSet.Common.Transports.Ip/IpServerTransport.cs @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using static ThingSet.Common.Transports.Ip.Protocol; + +namespace ThingSet.Common.Transports.Ip; + +/// +/// ThingSet server transport for IP (TCP/UDP). +/// +public class IpServerTransport : IServerTransport +{ + private const int MessageSize = 512; + private const int HeaderSize = 2; + + private static readonly IPEndPoint Broadcast = new IPEndPoint(IPAddress.Broadcast, Protocol.PublishSubscribePort); + + private readonly TcpListener _listener; + private readonly UdpClient _udpClient; + + private readonly Thread _listenThread; + private bool _runListener = true; + + private byte _messageNumber; + + private Func, Memory>? _callback; + + public IpServerTransport() : this(IPAddress.Any) + { + } + + public event EventHandler? Error; + + public IpServerTransport(IPAddress listenAddress) + { + _listener = new TcpListener(listenAddress, Protocol.RequestResponsePort); + _listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + + _udpClient = new UdpClient(Protocol.PublishSubscribePort, AddressFamily.InterNetwork); + _udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + + _listenThread = new Thread(RunListener) + { + IsBackground = true, + }; + } + + public void Dispose() + { + _listener.Dispose(); + _udpClient.Dispose(); + } + + public ValueTask ListenAsync(Func, Memory> callback) + { + _callback = callback; + _listenThread.Start(); + return ValueTask.CompletedTask; + } + + public void PublishControl(ushort id, byte[] buffer) + { + throw new NotImplementedException(); + } + + public void PublishReport(byte[] buffer) + { + int written = 0; + MessageType messageType; + Span source = buffer; + Span frame = stackalloc byte[MessageSize + HeaderSize]; + byte sequenceNumber = 0; + while (written < buffer.Length) + { + int size = Math.Min(MessageSize, buffer.Length - written); + messageType = written == 0 ? + (buffer.Length < MessageSize ? + MessageType.Single : MessageType.First) : + (buffer.Length - written <= MessageSize) ? + MessageType.Last : MessageType.Consecutive; + frame[0] = (byte)((byte)messageType | (sequenceNumber++ & 0x0F)); + unchecked + { + frame[1] = _messageNumber++; + } + + Span slice = source.Slice(written, size); + slice.CopyTo(frame.Slice(HeaderSize)); + _udpClient.Send(frame.Slice(0, HeaderSize + size), Broadcast); + written += size; + } + } + + private async void RunListener() + { + _listener.Start(); + + while (_runListener) + { + TcpClient client = await _listener.AcceptTcpClientAsync(); + Task.Run(() => HandleRequest(client)).GetAwaiter(); + } + } + + private async Task HandleRequest(TcpClient client) + { + Memory buffer = new byte[8192]; + using (client) + { + try + { + NetworkStream stream = client.GetStream(); + int read = await stream.ReadAsync(buffer); + Memory response = _callback!(client.Client.RemoteEndPoint!, buffer.Slice(0, read)); + await stream.WriteAsync(response); + } + catch (Exception ex) + { + Error?.Invoke(this, new ErrorEventArgs(ex)); + } + } + } +} diff --git a/src/ThingSet.Common.Transports.Ip/Protocol.cs b/src/ThingSet.Common.Transports.Ip/Protocol.cs new file mode 100644 index 0000000..fe81be3 --- /dev/null +++ b/src/ThingSet.Common.Transports.Ip/Protocol.cs @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +namespace ThingSet.Common.Transports.Ip; + +public class Protocol +{ + public const int RequestResponsePort = 9001; + public const int PublishSubscribePort = 9002; + + private const int MessageTypePosition = 4; + + internal enum MessageType + { + First = 0x0 << MessageTypePosition, + Consecutive = 0x1 << MessageTypePosition, + Last = 0x2 << MessageTypePosition, + Single = 0x3 << MessageTypePosition, + } +} \ No newline at end of file diff --git a/src/ThingSet.Common.Transports.Ip/ThingSet.Common.Transports.Ip.csproj b/src/ThingSet.Common.Transports.Ip/ThingSet.Common.Transports.Ip.csproj index 14159f2..04070df 100644 --- a/src/ThingSet.Common.Transports.Ip/ThingSet.Common.Transports.Ip.csproj +++ b/src/ThingSet.Common.Transports.Ip/ThingSet.Common.Transports.Ip.csproj @@ -13,7 +13,7 @@ https://github.com/Brill-Power/ThingSet.Net https://github.com/Brill-Power/ThingSet.Net.git git - IP transport for .NET ThingSet client + IP transport for .NET ThingSet client and server ThingSet.Common.Transports.Ip Apache-2.0 README.md diff --git a/src/ThingSet.Common/Nodes/ThingSetFunction.cs b/src/ThingSet.Common/Nodes/ThingSetFunction.cs new file mode 100644 index 0000000..c68cce4 --- /dev/null +++ b/src/ThingSet.Common/Nodes/ThingSetFunction.cs @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ThingSet.Common.Nodes; + +public class ThingSetFunction : ThingSetParentNode + where TDelegate : Delegate +{ + private readonly TDelegate _function; + + public ThingSetFunction(ushort id, string name, TDelegate function) : base(id, name, GetParameters(function, id + 1)) + { + _function = function; + Type = ThingSetType.GetType(typeof(TDelegate)); + } + + public override ThingSetType Type { get; } + + private static IEnumerable GetParameters(TDelegate function, int startingId) + { + return function.Method + .GetParameters() + .Select((p, i) => + new ThingSetFunctionParameter( + (ushort)(startingId + i), + $"w{p.Name}" ?? $"wParam{i + 1}", // TODO: include type + ThingSetType.GetType(p.ParameterType))); + } +} + +public static class ThingSetFunction +{ + public static ThingSetFunction Create(ushort id, string name, TDelegate function) + where TDelegate : Delegate + { + return new ThingSetFunction(id, name, function); + } +} \ No newline at end of file diff --git a/src/ThingSet.Common/Nodes/ThingSetFunctionParameter.cs b/src/ThingSet.Common/Nodes/ThingSetFunctionParameter.cs new file mode 100644 index 0000000..26050eb --- /dev/null +++ b/src/ThingSet.Common/Nodes/ThingSetFunctionParameter.cs @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +namespace ThingSet.Common.Nodes; + +public class ThingSetFunctionParameter : ThingSetNode +{ + public ThingSetFunctionParameter(ushort id, string name, ThingSetType type) : base(id, name) + { + Type = type; + } + + public override ThingSetType Type { get; } +} diff --git a/src/ThingSet.Common/Nodes/ThingSetGroup.cs b/src/ThingSet.Common/Nodes/ThingSetGroup.cs new file mode 100644 index 0000000..2ca0bbe --- /dev/null +++ b/src/ThingSet.Common/Nodes/ThingSetGroup.cs @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +using System.Collections.Generic; + +namespace ThingSet.Common.Nodes; + +public class ThingSetGroup : ThingSetParentNode +{ + public ThingSetGroup(ushort id, string name, IEnumerable children) : base(id, name, children) + { + } + + public ThingSetGroup(ushort id, string name, params ThingSetNode[] children) : base(id, name, children) + { + } + + public override ThingSetType Type => ThingSetType.Group; +} diff --git a/src/ThingSet.Common/Nodes/ThingSetNode.cs b/src/ThingSet.Common/Nodes/ThingSetNode.cs new file mode 100644 index 0000000..2484eb5 --- /dev/null +++ b/src/ThingSet.Common/Nodes/ThingSetNode.cs @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace ThingSet.Common.Nodes; + +public abstract class ThingSetNode +{ + protected ThingSetNode(ushort id, string name) + { + Id = id; + Name = name; + ThingSetRegistry.Register(this); + } + + ~ThingSetNode() + { + ThingSetRegistry.Unregister(this); + } + + public ushort Id { get; } + public string Name { get; } + public abstract ThingSetType Type{ get; } +} + +public class ThingSetRegistry +{ + private static readonly ThingSetRegistry Instance = new ThingSetRegistry(); + + private readonly IDictionary _nodesById = new ConcurrentDictionary(); + + private ThingSetRegistry() + { + } + + public static bool TryGetNode(ushort id, [NotNullWhen(true)] out ThingSetNode? node) + { + return Instance._nodesById.TryGetValue(id, out node); + } + + internal static void Register(ThingSetNode node) + { + Instance._nodesById.Add(node.Id, node); + } + + internal static void Unregister(ThingSetNode node) + { + Instance._nodesById.Remove(node.Id); + } +} \ No newline at end of file diff --git a/src/ThingSet.Common/Nodes/ThingSetParentNode.cs b/src/ThingSet.Common/Nodes/ThingSetParentNode.cs new file mode 100644 index 0000000..382b9b2 --- /dev/null +++ b/src/ThingSet.Common/Nodes/ThingSetParentNode.cs @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +using System.Collections.Generic; +using System.Linq; + +namespace ThingSet.Common.Nodes; + +public abstract class ThingSetParentNode : ThingSetNode +{ + protected ThingSetParentNode(ushort id, string name, IEnumerable children) : this(id, name, children.ToList()) + { + } + + protected ThingSetParentNode(ushort id, string name, IReadOnlyCollection children) : base(id, name) + { + Children = children; + } + + public IReadOnlyCollection Children { get; } +} diff --git a/src/ThingSet.Common/Nodes/ThingSetProperty.cs b/src/ThingSet.Common/Nodes/ThingSetProperty.cs new file mode 100644 index 0000000..4ddabe6 --- /dev/null +++ b/src/ThingSet.Common/Nodes/ThingSetProperty.cs @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +namespace ThingSet.Common.Nodes; + +public class ThingSetProperty : ThingSetNode +{ + public ThingSetProperty(ushort id, string name) : base(id, name) + { + } + + public TValue? Value { get; set; } + + public override ThingSetType Type => ThingSetType.GetType(typeof(TValue)); +} \ No newline at end of file diff --git a/src/ThingSet.Common/Protocols/Binary/ThingSetRequest.cs b/src/ThingSet.Common/Protocols/Binary/ThingSetBinaryRequestType.cs similarity index 87% rename from src/ThingSet.Common/Protocols/Binary/ThingSetRequest.cs rename to src/ThingSet.Common/Protocols/Binary/ThingSetBinaryRequestType.cs index d442519..e7c4694 100644 --- a/src/ThingSet.Common/Protocols/Binary/ThingSetRequest.cs +++ b/src/ThingSet.Common/Protocols/Binary/ThingSetBinaryRequestType.cs @@ -5,7 +5,7 @@ */ namespace ThingSet.Common.Protocols.Binary; -public enum ThingSetRequest : byte +public enum ThingSetBinaryRequestType : byte { Get = 0x01, Exec = 0x02, diff --git a/src/ThingSet.Common/Protocols/Text/ThingSetRequest.cs b/src/ThingSet.Common/Protocols/Text/ThingSetTextRequestType.cs similarity index 94% rename from src/ThingSet.Common/Protocols/Text/ThingSetRequest.cs rename to src/ThingSet.Common/Protocols/Text/ThingSetTextRequestType.cs index 1c6fab4..7d66434 100644 --- a/src/ThingSet.Common/Protocols/Text/ThingSetRequest.cs +++ b/src/ThingSet.Common/Protocols/Text/ThingSetTextRequestType.cs @@ -5,7 +5,7 @@ */ namespace ThingSet.Common.Protocols.Text; -public enum ThingSetRequest : byte +public enum ThingSetTextRequestType : byte { GetFetch = (byte)'?', /**< Function code for GET and FETCH requests in text mode. */ Exec = (byte)'!', /**< Function code for EXEC request in text mode. */ diff --git a/src/ThingSet.Common/ThingSet.Common.csproj b/src/ThingSet.Common/ThingSet.Common.csproj index 28bd65a..d0da930 100644 --- a/src/ThingSet.Common/ThingSet.Common.csproj +++ b/src/ThingSet.Common/ThingSet.Common.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/src/ThingSet.Common/Protocols/ThingSetType.cs b/src/ThingSet.Common/ThingSetType.cs similarity index 50% rename from src/ThingSet.Common/Protocols/ThingSetType.cs rename to src/ThingSet.Common/ThingSetType.cs index c66da1f..cbb1e9f 100644 --- a/src/ThingSet.Common/Protocols/ThingSetType.cs +++ b/src/ThingSet.Common/ThingSetType.cs @@ -4,8 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ using System; +using System.Collections.Generic; +using System.Linq; -namespace ThingSet.Common.Protocols; +namespace ThingSet.Common; /// /// Represents the type of an item in ThingSet. @@ -22,6 +24,7 @@ public struct ThingSetType : IEquatable public static readonly ThingSetType UInt64 = "u64"; public static readonly ThingSetType Int64 = "i64"; public static readonly ThingSetType Float = "f32"; + public static readonly ThingSetType Double = "f64"; public static readonly ThingSetType Decimal = "decimal"; public static readonly ThingSetType String = "string"; public static readonly ThingSetType Buffer = "buffer"; @@ -59,6 +62,78 @@ private ThingSetType(string type) /// public ThingSetType? ElementType => IsArray ? Type.Substring(0, Type.Length - 2) : (ThingSetType?)null; + public static ThingSetType GetType(Type parameterType) + { + parameterType = Nullable.GetUnderlyingType(parameterType) ?? parameterType; + + switch (System.Type.GetTypeCode(parameterType)) + { + case TypeCode.Boolean: + return Boolean; + case TypeCode.Byte: + return UInt8; + case TypeCode.SByte: + return Int8; + case TypeCode.UInt16: + return UInt16; + case TypeCode.Int16: + return Int16; + case TypeCode.UInt32: + return UInt32; + case TypeCode.Int32: + return Int32; + case TypeCode.UInt64: + return UInt64; + case TypeCode.Int64: + return Int64; + case TypeCode.Single: + return Float; + case TypeCode.Double: + return Double; + case TypeCode.Decimal: + return Decimal; + case TypeCode.String: + return String; + default: + if (parameterType == typeof(void)) + { + return new ThingSetType(System.String.Empty); + } + if (parameterType.IsArray) + { + return $"{GetType(parameterType.GetElementType()!)}"; + } + if (parameterType.IsEnum) + { + return GetType(parameterType.GetEnumUnderlyingType()); + } + if (typeof(Delegate).IsAssignableFrom(parameterType)) + { + if (parameterType.IsGenericType) + { + Type[] types = parameterType.GetGenericArguments(); + Type delegateType = parameterType.GetGenericTypeDefinition(); + IEnumerable args = types; + Type returnType; + if (delegateType.Name.StartsWith("Func")) + { + // last arg is return type + args = types.SkipLast(1); + returnType = types[types.Length - 1]; + } + else + { + // returns void + returnType = typeof(void); + } + return $"({System.String.Join(",", args.Select(a => GetType(a)))})->({GetType(returnType)})"; + } + return "()->()"; // don't think we can reflectively get the type + } + return "unknown"; + } + } + public bool Equals(ThingSetType other) => Type == other.Type; public override bool Equals(object? other) => other is ThingSetType type ? Equals(type) : false; diff --git a/src/ThingSet.Common/Transports/IServerTransport.cs b/src/ThingSet.Common/Transports/IServerTransport.cs index 5333859..071590a 100644 --- a/src/ThingSet.Common/Transports/IServerTransport.cs +++ b/src/ThingSet.Common/Transports/IServerTransport.cs @@ -4,13 +4,16 @@ * SPDX-License-Identifier: Apache-2.0 */ using System; +using System.IO; using System.Threading.Tasks; namespace ThingSet.Common.Transports; public interface IServerTransport : ITransport { - ValueTask ListenAsync(Func callback); + ValueTask ListenAsync(Func, Memory> callback); + + event EventHandler? Error; void PublishControl(ushort id, byte[] buffer); void PublishReport(byte[] buffer); diff --git a/src/ThingSet.Net.sln b/src/ThingSet.Net.sln index 13c7846..fbd989d 100644 --- a/src/ThingSet.Net.sln +++ b/src/ThingSet.Net.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{CCD2CE5A-B EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThingSet.Test", "..\test\ThingSet.Test\ThingSet.Test.csproj", "{85D124D2-F684-491C-9790-C6B7CBF9D4F8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThingSet.Server", "ThingSet.Server\ThingSet.Server.csproj", "{E05B4B2F-6A55-4261-98ED-EC16B529B766}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,6 +43,10 @@ Global {85D124D2-F684-491C-9790-C6B7CBF9D4F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {85D124D2-F684-491C-9790-C6B7CBF9D4F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {85D124D2-F684-491C-9790-C6B7CBF9D4F8}.Release|Any CPU.Build.0 = Release|Any CPU + {E05B4B2F-6A55-4261-98ED-EC16B529B766}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E05B4B2F-6A55-4261-98ED-EC16B529B766}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E05B4B2F-6A55-4261-98ED-EC16B529B766}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E05B4B2F-6A55-4261-98ED-EC16B529B766}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ThingSet.Server/ThingSet.Server.csproj b/src/ThingSet.Server/ThingSet.Server.csproj new file mode 100644 index 0000000..ad6a6d0 --- /dev/null +++ b/src/ThingSet.Server/ThingSet.Server.csproj @@ -0,0 +1,31 @@ + + + + + + + + net8.0 + disable + enable + Brill Power, Gareth Potter + Copyright (c) Brill Power 2025 + https://github.com/Brill-Power/ThingSet.Net + https://github.com/Brill-Power/ThingSet.Net.git + git + .NET ThingSet server + ThingSet.Server + Apache-2.0 + README.md + iot + + + + + + + + + + + diff --git a/src/ThingSet.Server/ThingSetServer.cs b/src/ThingSet.Server/ThingSetServer.cs new file mode 100644 index 0000000..52ac3da --- /dev/null +++ b/src/ThingSet.Server/ThingSetServer.cs @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Formats.Cbor; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ThingSet.Common.Nodes; +using ThingSet.Common.Protocols; +using ThingSet.Common.Protocols.Binary; +using ThingSet.Common.Transports; + +namespace ThingSet.Server; + +public class ThingSetServer +{ + private readonly IServerTransport _transport; + + public ThingSetServer(IServerTransport transport) + { + _transport = transport; + } + + public async ValueTask ListenAsync() + { + await _transport.ListenAsync(OnRequestReceived); + } + + private Memory OnRequestReceived(object identifier, Memory request) + { + byte[] response = new byte[8192]; + Memory responseMem = response; + ThingSetRequestContextBase context; + Span span = request.Span; + int length; + if (span[0] > 0x20) + { + throw new NotSupportedException("Text mode is not currently supported."); + } + else + { + context = new ThingSetBinaryRequestContext(request); + } + + if (context.IsGet) + { + length = HandleGet(context, responseMem); + } + else if (context.IsFetch) + { + length = HandleFetch(context, responseMem); + } + else if (context.IsUpdate) + { + length = HandleUpdate(context, responseMem); + } + else if (context.IsExec) + { + length = HandleExec(context, responseMem); + } + else + { + response[0] = (byte)ThingSetStatus.BadRequest; + length = 1; + } + return responseMem.Slice(0, length); + // ThingSetResponse response = context.Handle(); + // byte[] buffer = new byte[response.Buffer.Length + 1]; + // buffer[0] = (byte)response.Status; + // Buffer.BlockCopy(response.Buffer, 0, buffer, 1, response.Buffer.Length); + // Console.WriteLine($"Sent response of {buffer.Length} bytes"); + // return buffer; + } + + private int HandleExec(ThingSetRequestContextBase context, Memory response) + { + throw new NotImplementedException(); + } + + private int HandleUpdate(ThingSetRequestContextBase context, Memory response) + { + throw new NotImplementedException(); + } + + private int HandleFetch(ThingSetRequestContextBase context, Memory response) + { + if (context.HasValidEndpoint) + { + Span responseSpan = response.Span; + responseSpan[0] = (byte)ThingSetStatus.Content; + CborWriter writer = new CborWriter(CborConformanceMode.Lax, allowMultipleRootLevelValues: true); + writer.WriteNull(); + CborReader reader = new CborReader(context.RequestBody, CborConformanceMode.Lax, allowMultipleRootLevelValues: true); + if (reader.PeekState() == CborReaderState.Null) + { + if (context.Endpoint is ThingSetParentNode parent) + { + if (context.UseIds) + { + CborSerialiser.Write(writer, parent.Children.Select(c => c.Id).ToArray()); + } + else + { + CborSerialiser.Write(writer, parent.Children.Select(c => c.Name).ToArray()); + } + writer.Encode(responseSpan.Slice(1)); + return writer.BytesWritten + 1; + } + else + { + responseSpan[0] = (byte)ThingSetStatus.BadRequest; + return 1; + } + } + else + { + responseSpan[0] = (byte)ThingSetStatus.NotImplemented; + return 1; + } + } + else + { + response.Span[0] = (byte)ThingSetStatus.NotFound; + return 1; + } + } + + private int HandleGet(ThingSetRequestContextBase context, Memory response) + { + throw new NotImplementedException(); + } +} + +internal abstract class ThingSetRequestContextBase +{ + protected readonly Memory _request; + + protected ThingSetRequestContextBase(Memory request) + { + _request = request; + } + + public string? Path { get; protected set; } + public ushort? Id { get; protected set; } + public ThingSetNode? Endpoint { get; protected set; } + + public bool UseIds => Id.HasValue; + [MemberNotNullWhen(true, nameof(Endpoint))] + public bool HasValidEndpoint => Endpoint is not null; + + public Memory RequestBody { get; protected set; } + + public abstract bool IsGet { get; } + public abstract bool IsFetch { get; } + public abstract bool IsUpdate { get; } + public abstract bool IsExec{ get; } +} + +internal class ThingSetBinaryRequestContext : ThingSetRequestContextBase +{ + private readonly ThingSetBinaryRequestType _requestType; + private readonly CborReader _cborReader; + + internal ThingSetBinaryRequestContext(Memory request) : base(request) + { + _requestType = (ThingSetBinaryRequestType)request.Span[0]; + _cborReader = new CborReader(request.Slice(1), CborConformanceMode.Lax, allowMultipleRootLevelValues: true); + + CborReaderState state = _cborReader.PeekState(); + switch (state) + { + case CborReaderState.TextString: + Path = _cborReader.ReadTextString(); + break; + case CborReaderState.UnsignedInteger: + Id = (ushort)_cborReader.ReadInt32(); + break; + case CborReaderState.NegativeInteger: // unlikey to be this, but let's be pragmatic + Id = (ushort)_cborReader.ReadInt32(); + break; + default: + throw new InvalidDataException($"Unexpected CBOR data of type {state} (expected string or integer)."); + } + + if (Id.HasValue && ThingSetRegistry.TryGetNode(Id.Value, out ThingSetNode? node)) + { + Endpoint = node; + } + else if (!String.IsNullOrEmpty(Path)) + { + // + } + RequestBody = request.Slice(request.Length - _cborReader.BytesRemaining); + } + + public override bool IsGet => _requestType == ThingSetBinaryRequestType.Get; + public override bool IsFetch => _requestType == ThingSetBinaryRequestType.Fetch; + public override bool IsUpdate => _requestType == ThingSetBinaryRequestType.Update; + public override bool IsExec => _requestType == ThingSetBinaryRequestType.Exec; +} diff --git a/test/ThingSet.Test/TestClientServer.cs b/test/ThingSet.Test/TestClientServer.cs new file mode 100644 index 0000000..15c057e --- /dev/null +++ b/test/ThingSet.Test/TestClientServer.cs @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +using System; +using System.Buffers.Text; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Text.Unicode; +using System.Threading.Tasks; +using ThingSet.Client; +using ThingSet.Common.Nodes; +using ThingSet.Common.Transports.Ip; +using ThingSet.Server; + +namespace ThingSet.Test; + +public class TestClientServer +{ + [Test] + public async Task TestBasic() + { + ThingSetProperty voltage = new ThingSetProperty(0x200, "voltage"); + var function = ThingSetFunction.Create(0x500, "xTest", (int x, int y) => x + y); + + IpClientTransport clientTransport = new IpClientTransport("127.0.0.1"); + IpServerTransport serverTransport = new IpServerTransport(IPAddress.Loopback); + ThingSetServer server = new ThingSetServer(serverTransport); + ThingSetClient client = new ThingSetClient(clientTransport); + await server.ListenAsync(); + await Task.Delay(10); + await client.ConnectAsync(); + object? result = client.Fetch(0x500); + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.TypeOf()); + if (result is object[] array) + { + Assert.That(array.Length, Is.EqualTo(2)); + Assert.That(array[0], Is.EqualTo(0x501)); + Assert.That(array[1], Is.EqualTo(0x502)); + } + } +} \ No newline at end of file diff --git a/test/ThingSet.Test/TestIpClientTransport.cs b/test/ThingSet.Test/TestIpClientTransport.cs index 7b87c73..2ccd0f4 100644 --- a/test/ThingSet.Test/TestIpClientTransport.cs +++ b/test/ThingSet.Test/TestIpClientTransport.cs @@ -12,17 +12,18 @@ namespace ThingSet.Test; public class TestIpClientTransport { - [Test] - public async Task TestDisposal() - { - using (TcpClient server = new TcpClient(new IPEndPoint(IPAddress.Loopback, 9001))) - { - server.Client.Listen(); - IpClientTransport transport = new IpClientTransport("127.0.0.1", 9001); - await transport.ConnectAsync(); - await transport.SubscribeAsync(delegate { }); - await Task.Delay(100); - Assert.DoesNotThrow(() => transport.Dispose()); - } - } + // [Test] + // public async Task TestDisposal() + // { + // using (TcpClient server = new TcpClient(new IPEndPoint(IPAddress.Loopback, 9001))) + // { + // server.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + // server.Client.Listen(); + // IpClientTransport transport = new IpClientTransport("127.0.0.1", 9001); + // await transport.ConnectAsync(); + // await transport.SubscribeAsync(delegate { }); + // await Task.Delay(100); + // Assert.DoesNotThrow(() => transport.Dispose()); + // } + // } } diff --git a/test/ThingSet.Test/TestNodes.cs b/test/ThingSet.Test/TestNodes.cs new file mode 100644 index 0000000..71ba970 --- /dev/null +++ b/test/ThingSet.Test/TestNodes.cs @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +using System; +using ThingSet.Common.Nodes; + +namespace ThingSet.Test; + +public class TestNodes +{ + [Test] + public void TestFunctions() + { + Func add = (x, y) => x + y; + var xAdd = ThingSetFunction.Create(0x1000, "xAdd", add); + Assert.That(xAdd.Type.Type, Is.EqualTo("(i32,i32)->(i32)")); + Assert.That(xAdd.Children.Count, Is.EqualTo(2)); + } +} \ No newline at end of file diff --git a/test/ThingSet.Test/ThingSet.Test.csproj b/test/ThingSet.Test/ThingSet.Test.csproj index 1b9dae2..d68a2a1 100644 --- a/test/ThingSet.Test/ThingSet.Test.csproj +++ b/test/ThingSet.Test/ThingSet.Test.csproj @@ -11,15 +11,17 @@ - - - - + + + + + + From c951bc13d9668d0bcc81fb22b2f8c099e4c4638c Mon Sep 17 00:00:00 2001 From: Gareth Potter Date: Tue, 30 Sep 2025 22:11:43 +0100 Subject: [PATCH 2/7] working exec --- src/ThingSet.Client/ThingSetClient.cs | 4 +- .../IpClientTransport.cs | 4 + .../IpServerTransport.cs | 39 ++++-- src/ThingSet.Common/Nodes/ThingSetFunction.cs | 9 +- src/ThingSet.Server/ThingSetServer.cs | 127 +++++++++++------- test/ThingSet.Test/TestClientServer.cs | 40 +++--- test/ThingSet.Test/TestIpClientTransport.cs | 29 ---- 7 files changed, 145 insertions(+), 107 deletions(-) delete mode 100644 test/ThingSet.Test/TestIpClientTransport.cs diff --git a/src/ThingSet.Client/ThingSetClient.cs b/src/ThingSet.Client/ThingSetClient.cs index a371584..ea534d6 100644 --- a/src/ThingSet.Client/ThingSetClient.cs +++ b/src/ThingSet.Client/ThingSetClient.cs @@ -191,11 +191,11 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options { throw new IOException("Could not connect to ThingSet endpoint."); } - _transport.Read(buffer); + int read = _transport.Read(buffer); ThingSetResponse response = (ThingSetStatus)buffer[0]; if (response.Success) { - CborReader reader = new CborReader(buffer.AsMemory().Slice(1), CborConformanceMode.Lax, allowMultipleRootLevelValues: true); + CborReader reader = new CborReader(buffer.AsMemory().Slice(1, read - 1), CborConformanceMode.Lax, allowMultipleRootLevelValues: true); reader.ReadNull(); return CborDeserialiser.Read(reader); } diff --git a/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs b/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs index c64fee8..b16cf3d 100644 --- a/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs +++ b/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs @@ -68,6 +68,10 @@ public void Dispose() public int Read(byte[] buffer) { + if (!_tcpClient.Connected) + { + _tcpClient.Connect(_hostname, _port); + } return _tcpClient.GetStream().Read(buffer, 0, buffer.Length); } diff --git a/src/ThingSet.Common.Transports.Ip/IpServerTransport.cs b/src/ThingSet.Common.Transports.Ip/IpServerTransport.cs index 803ee39..e1e8ebb 100644 --- a/src/ThingSet.Common.Transports.Ip/IpServerTransport.cs +++ b/src/ThingSet.Common.Transports.Ip/IpServerTransport.cs @@ -9,6 +9,7 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using ThingSet.Common.Protocols; using static ThingSet.Common.Transports.Ip.Protocol; namespace ThingSet.Common.Transports.Ip; @@ -26,8 +27,8 @@ public class IpServerTransport : IServerTransport private readonly TcpListener _listener; private readonly UdpClient _udpClient; + private readonly CancellationTokenSource _listenerCanceller = new CancellationTokenSource(); private readonly Thread _listenThread; - private bool _runListener = true; private byte _messageNumber; @@ -55,6 +56,8 @@ public IpServerTransport(IPAddress listenAddress) public void Dispose() { + _listenerCanceller.Cancel(); + _listener.Stop(); _listener.Dispose(); _udpClient.Dispose(); } @@ -103,24 +106,42 @@ private async void RunListener() { _listener.Start(); - while (_runListener) + while (!_listenerCanceller.IsCancellationRequested) { - TcpClient client = await _listener.AcceptTcpClientAsync(); - Task.Run(() => HandleRequest(client)).GetAwaiter(); + try + { + TcpClient client = await _listener.AcceptTcpClientAsync(_listenerCanceller.Token); + Task.Run(() => HandleRequest(client, _listenerCanceller.Token)).GetAwaiter(); + } + catch (OperationCanceledException) + { + } } } - private async Task HandleRequest(TcpClient client) + private async Task HandleRequest(TcpClient client, CancellationToken cancellationToken) { Memory buffer = new byte[8192]; using (client) { try { - NetworkStream stream = client.GetStream(); - int read = await stream.ReadAsync(buffer); - Memory response = _callback!(client.Client.RemoteEndPoint!, buffer.Slice(0, read)); - await stream.WriteAsync(response); + await using NetworkStream stream = client.GetStream(); + int read; + while ((read = await stream.ReadAsync(buffer, cancellationToken)) != 0) + { + Memory response; + try + { + response = _callback!(client.Client.RemoteEndPoint!, buffer.Slice(0, read)); + } + catch (Exception ex) + { + Error?.Invoke(this, new ErrorEventArgs(ex)); + response = new byte[] { (byte)ThingSetStatus.InternalServerError }; + } + await stream.WriteAsync(response, cancellationToken); + } } catch (Exception ex) { diff --git a/src/ThingSet.Common/Nodes/ThingSetFunction.cs b/src/ThingSet.Common/Nodes/ThingSetFunction.cs index c68cce4..452111b 100644 --- a/src/ThingSet.Common/Nodes/ThingSetFunction.cs +++ b/src/ThingSet.Common/Nodes/ThingSetFunction.cs @@ -9,7 +9,12 @@ namespace ThingSet.Common.Nodes; -public class ThingSetFunction : ThingSetParentNode +public interface IThingSetFunction +{ + public Delegate Function { get; } +} + +public class ThingSetFunction : ThingSetParentNode, IThingSetFunction where TDelegate : Delegate { private readonly TDelegate _function; @@ -20,6 +25,8 @@ public ThingSetFunction(ushort id, string name, TDelegate function) : base(id, n Type = ThingSetType.GetType(typeof(TDelegate)); } + public Delegate Function => _function; + public override ThingSetType Type { get; } private static IEnumerable GetParameters(TDelegate function, int startingId) diff --git a/src/ThingSet.Server/ThingSetServer.cs b/src/ThingSet.Server/ThingSetServer.cs index 52ac3da..72bfa3d 100644 --- a/src/ThingSet.Server/ThingSetServer.cs +++ b/src/ThingSet.Server/ThingSetServer.cs @@ -8,6 +8,7 @@ using System.Formats.Cbor; using System.IO; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using ThingSet.Common.Nodes; using ThingSet.Common.Protocols; @@ -16,7 +17,7 @@ namespace ThingSet.Server; -public class ThingSetServer +public class ThingSetServer : IDisposable { private readonly IServerTransport _transport; @@ -45,40 +46,77 @@ private Memory OnRequestReceived(object identifier, Memory request) { context = new ThingSetBinaryRequestContext(request); } - - if (context.IsGet) - { - length = HandleGet(context, responseMem); - } - else if (context.IsFetch) - { - length = HandleFetch(context, responseMem); - } - else if (context.IsUpdate) - { - length = HandleUpdate(context, responseMem); - } - else if (context.IsExec) + if (context.HasValidEndpoint) { - length = HandleExec(context, responseMem); + if (context.IsGet) + { + length = HandleGet(context, responseMem); + } + else if (context.IsFetch) + { + length = HandleFetch(context, responseMem); + } + else if (context.IsUpdate) + { + length = HandleUpdate(context, responseMem); + } + else if (context.IsExec) + { + length = HandleExec(context, responseMem); + } + else + { + response[0] = (byte)ThingSetStatus.BadRequest; + length = 1; + } } else { - response[0] = (byte)ThingSetStatus.BadRequest; + response[0] = (byte)ThingSetStatus.NotFound; length = 1; } return responseMem.Slice(0, length); - // ThingSetResponse response = context.Handle(); - // byte[] buffer = new byte[response.Buffer.Length + 1]; - // buffer[0] = (byte)response.Status; - // Buffer.BlockCopy(response.Buffer, 0, buffer, 1, response.Buffer.Length); - // Console.WriteLine($"Sent response of {buffer.Length} bytes"); - // return buffer; } private int HandleExec(ThingSetRequestContextBase context, Memory response) { - throw new NotImplementedException(); + Span responseSpan = response.Span; + if ((context.Endpoint?.Type.IsFunction ?? false) && + context.Endpoint is IThingSetFunction function) + { + CborReader reader = new CborReader(context.RequestBody, CborConformanceMode.Lax, allowMultipleRootLevelValues: true); + if (CborDeserialiser.Read(reader) is object[] args) + { + ParameterInfo[] parameters = function.Function.Method.GetParameters(); + if (args.Length != parameters.Length) + { + responseSpan[0] = (byte)ThingSetStatus.BadRequest; + return 1; + } + + for (int i = 0; i < args.Length; i++) + { + args[i] = Convert.ChangeType(args[i], parameters[i].ParameterType); + } + responseSpan[0] = (byte)ThingSetStatus.Changed; + object? result = function.Function.DynamicInvoke(args); + CborWriter writer = new CborWriter(CborConformanceMode.Lax, allowMultipleRootLevelValues: true); + writer.WriteNull(); + CborSerialiser.Write(writer, result); + writer.Encode(responseSpan.Slice(1)); + return writer.BytesWritten + 1; + } + else + { + responseSpan[0] = (byte)ThingSetStatus.BadRequest; + return 1; + } + } + else + { + responseSpan[0] = (byte)ThingSetStatus.MethodNotAllowed; + return 1; + } } private int HandleUpdate(ThingSetRequestContextBase context, Memory response) @@ -88,43 +126,35 @@ private int HandleUpdate(ThingSetRequestContextBase context, Memory respon private int HandleFetch(ThingSetRequestContextBase context, Memory response) { - if (context.HasValidEndpoint) + Span responseSpan = response.Span; + responseSpan[0] = (byte)ThingSetStatus.Content; + CborWriter writer = new CborWriter(CborConformanceMode.Lax, allowMultipleRootLevelValues: true); + writer.WriteNull(); + CborReader reader = new CborReader(context.RequestBody, CborConformanceMode.Lax, allowMultipleRootLevelValues: true); + if (reader.PeekState() == CborReaderState.Null) { - Span responseSpan = response.Span; - responseSpan[0] = (byte)ThingSetStatus.Content; - CborWriter writer = new CborWriter(CborConformanceMode.Lax, allowMultipleRootLevelValues: true); - writer.WriteNull(); - CborReader reader = new CborReader(context.RequestBody, CborConformanceMode.Lax, allowMultipleRootLevelValues: true); - if (reader.PeekState() == CborReaderState.Null) + if (context.Endpoint is ThingSetParentNode parent) { - if (context.Endpoint is ThingSetParentNode parent) + if (context.UseIds) { - if (context.UseIds) - { - CborSerialiser.Write(writer, parent.Children.Select(c => c.Id).ToArray()); - } - else - { - CborSerialiser.Write(writer, parent.Children.Select(c => c.Name).ToArray()); - } - writer.Encode(responseSpan.Slice(1)); - return writer.BytesWritten + 1; + CborSerialiser.Write(writer, parent.Children.Select(c => c.Id).ToArray()); } else { - responseSpan[0] = (byte)ThingSetStatus.BadRequest; - return 1; + CborSerialiser.Write(writer, parent.Children.Select(c => c.Name).ToArray()); } + writer.Encode(responseSpan.Slice(1)); + return writer.BytesWritten + 1; } else { - responseSpan[0] = (byte)ThingSetStatus.NotImplemented; + responseSpan[0] = (byte)ThingSetStatus.BadRequest; return 1; } } else { - response.Span[0] = (byte)ThingSetStatus.NotFound; + responseSpan[0] = (byte)ThingSetStatus.NotImplemented; return 1; } } @@ -133,6 +163,11 @@ private int HandleGet(ThingSetRequestContextBase context, Memory response) { throw new NotImplementedException(); } + + public void Dispose() + { + _transport.Dispose(); + } } internal abstract class ThingSetRequestContextBase diff --git a/test/ThingSet.Test/TestClientServer.cs b/test/ThingSet.Test/TestClientServer.cs index 15c057e..982f2dc 100644 --- a/test/ThingSet.Test/TestClientServer.cs +++ b/test/ThingSet.Test/TestClientServer.cs @@ -3,12 +3,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -using System; -using System.Buffers.Text; using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Text.Unicode; using System.Threading.Tasks; using ThingSet.Client; using ThingSet.Common.Nodes; @@ -20,26 +15,31 @@ namespace ThingSet.Test; public class TestClientServer { [Test] - public async Task TestBasic() + public async Task TestFunction() { ThingSetProperty voltage = new ThingSetProperty(0x200, "voltage"); var function = ThingSetFunction.Create(0x500, "xTest", (int x, int y) => x + y); - IpClientTransport clientTransport = new IpClientTransport("127.0.0.1"); - IpServerTransport serverTransport = new IpServerTransport(IPAddress.Loopback); - ThingSetServer server = new ThingSetServer(serverTransport); - ThingSetClient client = new ThingSetClient(clientTransport); - await server.ListenAsync(); - await Task.Delay(10); - await client.ConnectAsync(); - object? result = client.Fetch(0x500); - Assert.That(result, Is.Not.Null); - Assert.That(result, Is.TypeOf()); - if (result is object[] array) + using (IpClientTransport clientTransport = new IpClientTransport("127.0.0.1")) + using (IpServerTransport serverTransport = new IpServerTransport(IPAddress.Loopback)) + using (ThingSetServer server = new ThingSetServer(serverTransport)) + using (ThingSetClient client = new ThingSetClient(clientTransport)) { - Assert.That(array.Length, Is.EqualTo(2)); - Assert.That(array[0], Is.EqualTo(0x501)); - Assert.That(array[1], Is.EqualTo(0x502)); + await server.ListenAsync(); + await Task.Delay(10); + await client.ConnectAsync(); + object? result = client.Fetch(0x500); + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.TypeOf()); + if (result is object[] array) + { + Assert.That(array.Length, Is.EqualTo(2)); + Assert.That(array[0], Is.EqualTo(0x501)); + Assert.That(array[1], Is.EqualTo(0x502)); + } + + result = client.Exec(0x500, 1, 2); + Assert.That(result, Is.Not.Null); } } } \ No newline at end of file diff --git a/test/ThingSet.Test/TestIpClientTransport.cs b/test/ThingSet.Test/TestIpClientTransport.cs deleted file mode 100644 index 2ccd0f4..0000000 --- a/test/ThingSet.Test/TestIpClientTransport.cs +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2025 Brill Power. - * - * SPDX-License-Identifier: Apache-2.0 - */ -using System.Net; -using System.Net.Sockets; -using System.Threading.Tasks; -using ThingSet.Common.Transports.Ip; - -namespace ThingSet.Test; - -public class TestIpClientTransport -{ - // [Test] - // public async Task TestDisposal() - // { - // using (TcpClient server = new TcpClient(new IPEndPoint(IPAddress.Loopback, 9001))) - // { - // server.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - // server.Client.Listen(); - // IpClientTransport transport = new IpClientTransport("127.0.0.1", 9001); - // await transport.ConnectAsync(); - // await transport.SubscribeAsync(delegate { }); - // await Task.Delay(100); - // Assert.DoesNotThrow(() => transport.Dispose()); - // } - // } -} From 269c969908b520d2875994c91dca8b23ed5bc500 Mon Sep 17 00:00:00 2001 From: Gareth Potter Date: Tue, 30 Sep 2025 22:23:40 +0100 Subject: [PATCH 3/7] rudimentary get support --- .../Nodes/IThingSetFunction.cs | 13 ++++++ src/ThingSet.Common/Nodes/IThingSetValue.cs | 11 +++++ src/ThingSet.Common/Nodes/ThingSetFunction.cs | 5 --- src/ThingSet.Common/Nodes/ThingSetProperty.cs | 10 ++++- .../Protocols/Binary/CborDeserialiser.cs | 2 +- src/ThingSet.Server/ThingSetServer.cs | 44 ++++++++++++++++++- test/ThingSet.Test/TestClientServer.cs | 22 +++++++++- 7 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 src/ThingSet.Common/Nodes/IThingSetFunction.cs create mode 100644 src/ThingSet.Common/Nodes/IThingSetValue.cs diff --git a/src/ThingSet.Common/Nodes/IThingSetFunction.cs b/src/ThingSet.Common/Nodes/IThingSetFunction.cs new file mode 100644 index 0000000..325d8f3 --- /dev/null +++ b/src/ThingSet.Common/Nodes/IThingSetFunction.cs @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +using System; + +namespace ThingSet.Common.Nodes; + +public interface IThingSetFunction +{ + public Delegate Function { get; } +} diff --git a/src/ThingSet.Common/Nodes/IThingSetValue.cs b/src/ThingSet.Common/Nodes/IThingSetValue.cs new file mode 100644 index 0000000..54d3384 --- /dev/null +++ b/src/ThingSet.Common/Nodes/IThingSetValue.cs @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +namespace ThingSet.Common.Nodes; + +public interface IThingSetValue +{ + object? Value { get; set; } +} \ No newline at end of file diff --git a/src/ThingSet.Common/Nodes/ThingSetFunction.cs b/src/ThingSet.Common/Nodes/ThingSetFunction.cs index 452111b..10bcea5 100644 --- a/src/ThingSet.Common/Nodes/ThingSetFunction.cs +++ b/src/ThingSet.Common/Nodes/ThingSetFunction.cs @@ -9,11 +9,6 @@ namespace ThingSet.Common.Nodes; -public interface IThingSetFunction -{ - public Delegate Function { get; } -} - public class ThingSetFunction : ThingSetParentNode, IThingSetFunction where TDelegate : Delegate { diff --git a/src/ThingSet.Common/Nodes/ThingSetProperty.cs b/src/ThingSet.Common/Nodes/ThingSetProperty.cs index 4ddabe6..9d525b3 100644 --- a/src/ThingSet.Common/Nodes/ThingSetProperty.cs +++ b/src/ThingSet.Common/Nodes/ThingSetProperty.cs @@ -5,7 +5,7 @@ */ namespace ThingSet.Common.Nodes; -public class ThingSetProperty : ThingSetNode +public class ThingSetProperty : ThingSetNode, IThingSetValue { public ThingSetProperty(ushort id, string name) : base(id, name) { @@ -14,4 +14,10 @@ public ThingSetProperty(ushort id, string name) : base(id, name) public TValue? Value { get; set; } public override ThingSetType Type => ThingSetType.GetType(typeof(TValue)); -} \ No newline at end of file + + object? IThingSetValue.Value + { + get { return Value; } + set { Value = (TValue?)value; } + } +} diff --git a/src/ThingSet.Common/Protocols/Binary/CborDeserialiser.cs b/src/ThingSet.Common/Protocols/Binary/CborDeserialiser.cs index 32729c4..bf0b662 100644 --- a/src/ThingSet.Common/Protocols/Binary/CborDeserialiser.cs +++ b/src/ThingSet.Common/Protocols/Binary/CborDeserialiser.cs @@ -21,7 +21,7 @@ public static class CborDeserialiser case CborReaderState.UnsignedInteger: return ReadInteger(reader, state); case CborReaderState.HalfPrecisionFloat: - return reader.ReadHalf(); + return (float)reader.ReadHalf(); case CborReaderState.SinglePrecisionFloat: return reader.ReadSingle(); case CborReaderState.DoublePrecisionFloat: diff --git a/src/ThingSet.Server/ThingSetServer.cs b/src/ThingSet.Server/ThingSetServer.cs index 72bfa3d..5ca5640 100644 --- a/src/ThingSet.Server/ThingSetServer.cs +++ b/src/ThingSet.Server/ThingSetServer.cs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Formats.Cbor; using System.IO; @@ -161,7 +162,48 @@ private int HandleFetch(ThingSetRequestContextBase context, Memory respons private int HandleGet(ThingSetRequestContextBase context, Memory response) { - throw new NotImplementedException(); + Span responseSpan = response.Span; + responseSpan[0] = (byte)ThingSetStatus.Content; + CborWriter writer = new CborWriter(CborConformanceMode.Lax, allowMultipleRootLevelValues: true); + writer.WriteNull(); + if (context.Endpoint is ThingSetParentNode parent) + { + if (context.UseIds) + { + CborSerialiser.Write(writer, GetKeyValuePairs(n => n.Id, parent.Children)); + } + else + { + CborSerialiser.Write(writer, GetKeyValuePairs(n => n.Name, parent.Children)); + } + writer.Encode(responseSpan.Slice(1)); + return 1 + writer.BytesWritten; + } + else if (context.Endpoint is IThingSetValue value) + { + CborSerialiser.Write(writer, value.Value); + writer.Encode(responseSpan.Slice(1)); + return 1 + writer.BytesWritten; + } + else + { + responseSpan[0] = (byte)ThingSetStatus.UnsupportedFormat; + return 1; + } + } + + private Dictionary GetKeyValuePairs(Func keySelector, IEnumerable nodes) + where TKey : IEquatable + { + Dictionary keyValuePairs = new Dictionary(); + foreach (ThingSetNode node in nodes) + { + if (node is IThingSetValue value) + { + keyValuePairs.Add(keySelector(node), value.Value); + } + } + return keyValuePairs; } public void Dispose() diff --git a/test/ThingSet.Test/TestClientServer.cs b/test/ThingSet.Test/TestClientServer.cs index 982f2dc..dc25f4b 100644 --- a/test/ThingSet.Test/TestClientServer.cs +++ b/test/ThingSet.Test/TestClientServer.cs @@ -15,9 +15,29 @@ namespace ThingSet.Test; public class TestClientServer { [Test] - public async Task TestFunction() + public async Task TestProperty() { ThingSetProperty voltage = new ThingSetProperty(0x200, "voltage"); + voltage.Value = 24.0f; + + using (IpClientTransport clientTransport = new IpClientTransport("127.0.0.1")) + using (IpServerTransport serverTransport = new IpServerTransport(IPAddress.Loopback)) + using (ThingSetServer server = new ThingSetServer(serverTransport)) + using (ThingSetClient client = new ThingSetClient(clientTransport)) + { + await server.ListenAsync(); + await Task.Delay(10); + await client.ConnectAsync(); + object? result = client.Get(0x200); + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.TypeOf()); + Assert.That(result, Is.EqualTo(24.0f)); + } + } + + [Test] + public async Task TestFunction() + { var function = ThingSetFunction.Create(0x500, "xTest", (int x, int y) => x + y); using (IpClientTransport clientTransport = new IpClientTransport("127.0.0.1")) From 1b8b07daa8320b6d77a7bf8edd6b50759fbe361f Mon Sep 17 00:00:00 2001 From: Gareth Potter Date: Wed, 1 Oct 2025 11:00:47 +0100 Subject: [PATCH 4/7] metadata endpoint; fixes --- .../IpClientTransport.cs | 4 - .../Nodes/IThingSetParentNode.cs | 13 ++ src/ThingSet.Common/Nodes/ThingSetFunction.cs | 28 ++--- .../Nodes/ThingSetFunctionParameter.cs | 2 +- src/ThingSet.Common/Nodes/ThingSetGroup.cs | 8 +- src/ThingSet.Common/Nodes/ThingSetNode.cs | 37 +----- .../Nodes/ThingSetParentNode.cs | 12 +- src/ThingSet.Common/Nodes/ThingSetProperty.cs | 2 +- src/ThingSet.Common/Nodes/ThingSetRegistry.cs | 116 ++++++++++++++++++ .../Protocols/Binary/CborSerialiser.cs | 3 + src/ThingSet.Server/ThingSetServer.cs | 51 +++++++- test/ThingSet.Test/TestClientServer.cs | 20 ++- test/ThingSet.Test/TestNodes.cs | 5 +- 13 files changed, 223 insertions(+), 78 deletions(-) create mode 100644 src/ThingSet.Common/Nodes/IThingSetParentNode.cs create mode 100644 src/ThingSet.Common/Nodes/ThingSetRegistry.cs diff --git a/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs b/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs index b16cf3d..c64fee8 100644 --- a/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs +++ b/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs @@ -68,10 +68,6 @@ public void Dispose() public int Read(byte[] buffer) { - if (!_tcpClient.Connected) - { - _tcpClient.Connect(_hostname, _port); - } return _tcpClient.GetStream().Read(buffer, 0, buffer.Length); } diff --git a/src/ThingSet.Common/Nodes/IThingSetParentNode.cs b/src/ThingSet.Common/Nodes/IThingSetParentNode.cs new file mode 100644 index 0000000..b4aafb2 --- /dev/null +++ b/src/ThingSet.Common/Nodes/IThingSetParentNode.cs @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +using System.Collections.Generic; + +namespace ThingSet.Common.Nodes; + +public interface IThingSetParentNode +{ + IEnumerable Children { get; } +} diff --git a/src/ThingSet.Common/Nodes/ThingSetFunction.cs b/src/ThingSet.Common/Nodes/ThingSetFunction.cs index 10bcea5..c229dd7 100644 --- a/src/ThingSet.Common/Nodes/ThingSetFunction.cs +++ b/src/ThingSet.Common/Nodes/ThingSetFunction.cs @@ -9,38 +9,38 @@ namespace ThingSet.Common.Nodes; -public class ThingSetFunction : ThingSetParentNode, IThingSetFunction +public class ThingSetFunction : ThingSetNode, IThingSetParentNode, IThingSetFunction where TDelegate : Delegate { private readonly TDelegate _function; - public ThingSetFunction(ushort id, string name, TDelegate function) : base(id, name, GetParameters(function, id + 1)) + public ThingSetFunction(ushort id, string name, ushort parentId, TDelegate function) : base(id, name, parentId) { _function = function; Type = ThingSetType.GetType(typeof(TDelegate)); + Children = function.Method + .GetParameters() + .Select((p, i) => + new ThingSetFunctionParameter( + (ushort)(Id + 1 + i), + $"w{p.Name}" ?? $"wParam{i + 1}", // TODO: include type + Id, + ThingSetType.GetType(p.ParameterType))) + .ToList(); } public Delegate Function => _function; public override ThingSetType Type { get; } - private static IEnumerable GetParameters(TDelegate function, int startingId) - { - return function.Method - .GetParameters() - .Select((p, i) => - new ThingSetFunctionParameter( - (ushort)(startingId + i), - $"w{p.Name}" ?? $"wParam{i + 1}", // TODO: include type - ThingSetType.GetType(p.ParameterType))); - } + public IEnumerable Children { get; } } public static class ThingSetFunction { - public static ThingSetFunction Create(ushort id, string name, TDelegate function) + public static ThingSetFunction Create(ushort id, string name, ushort parentId, TDelegate function) where TDelegate : Delegate { - return new ThingSetFunction(id, name, function); + return new ThingSetFunction(id, name, parentId, function); } } \ No newline at end of file diff --git a/src/ThingSet.Common/Nodes/ThingSetFunctionParameter.cs b/src/ThingSet.Common/Nodes/ThingSetFunctionParameter.cs index 26050eb..b750671 100644 --- a/src/ThingSet.Common/Nodes/ThingSetFunctionParameter.cs +++ b/src/ThingSet.Common/Nodes/ThingSetFunctionParameter.cs @@ -7,7 +7,7 @@ namespace ThingSet.Common.Nodes; public class ThingSetFunctionParameter : ThingSetNode { - public ThingSetFunctionParameter(ushort id, string name, ThingSetType type) : base(id, name) + public ThingSetFunctionParameter(ushort id, string name, ushort parentId, ThingSetType type) : base(id, name, parentId) { Type = type; } diff --git a/src/ThingSet.Common/Nodes/ThingSetGroup.cs b/src/ThingSet.Common/Nodes/ThingSetGroup.cs index 2ca0bbe..9158771 100644 --- a/src/ThingSet.Common/Nodes/ThingSetGroup.cs +++ b/src/ThingSet.Common/Nodes/ThingSetGroup.cs @@ -3,17 +3,11 @@ * * SPDX-License-Identifier: Apache-2.0 */ -using System.Collections.Generic; - namespace ThingSet.Common.Nodes; public class ThingSetGroup : ThingSetParentNode { - public ThingSetGroup(ushort id, string name, IEnumerable children) : base(id, name, children) - { - } - - public ThingSetGroup(ushort id, string name, params ThingSetNode[] children) : base(id, name, children) + public ThingSetGroup(ushort id, string name, ushort parentId) : base(id, name, parentId) { } diff --git a/src/ThingSet.Common/Nodes/ThingSetNode.cs b/src/ThingSet.Common/Nodes/ThingSetNode.cs index 2484eb5..45567f6 100644 --- a/src/ThingSet.Common/Nodes/ThingSetNode.cs +++ b/src/ThingSet.Common/Nodes/ThingSetNode.cs @@ -3,18 +3,14 @@ * * SPDX-License-Identifier: Apache-2.0 */ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - namespace ThingSet.Common.Nodes; public abstract class ThingSetNode { - protected ThingSetNode(ushort id, string name) + protected ThingSetNode(ushort id, string name, ushort parentId) { Id = id; + ParentId = parentId; Name = name; ThingSetRegistry.Register(this); } @@ -26,31 +22,6 @@ protected ThingSetNode(ushort id, string name) public ushort Id { get; } public string Name { get; } - public abstract ThingSetType Type{ get; } + public ushort ParentId { get; } + public abstract ThingSetType Type { get; } } - -public class ThingSetRegistry -{ - private static readonly ThingSetRegistry Instance = new ThingSetRegistry(); - - private readonly IDictionary _nodesById = new ConcurrentDictionary(); - - private ThingSetRegistry() - { - } - - public static bool TryGetNode(ushort id, [NotNullWhen(true)] out ThingSetNode? node) - { - return Instance._nodesById.TryGetValue(id, out node); - } - - internal static void Register(ThingSetNode node) - { - Instance._nodesById.Add(node.Id, node); - } - - internal static void Unregister(ThingSetNode node) - { - Instance._nodesById.Remove(node.Id); - } -} \ No newline at end of file diff --git a/src/ThingSet.Common/Nodes/ThingSetParentNode.cs b/src/ThingSet.Common/Nodes/ThingSetParentNode.cs index 382b9b2..cb14129 100644 --- a/src/ThingSet.Common/Nodes/ThingSetParentNode.cs +++ b/src/ThingSet.Common/Nodes/ThingSetParentNode.cs @@ -4,20 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ using System.Collections.Generic; -using System.Linq; namespace ThingSet.Common.Nodes; -public abstract class ThingSetParentNode : ThingSetNode +public abstract class ThingSetParentNode : ThingSetNode, IThingSetParentNode { - protected ThingSetParentNode(ushort id, string name, IEnumerable children) : this(id, name, children.ToList()) + protected ThingSetParentNode(ushort id, string name, ushort parentId) : base(id, name, parentId) { } - protected ThingSetParentNode(ushort id, string name, IReadOnlyCollection children) : base(id, name) - { - Children = children; - } - - public IReadOnlyCollection Children { get; } + public IEnumerable Children => ThingSetRegistry.GetChildren(ParentId); } diff --git a/src/ThingSet.Common/Nodes/ThingSetProperty.cs b/src/ThingSet.Common/Nodes/ThingSetProperty.cs index 9d525b3..ae33a8b 100644 --- a/src/ThingSet.Common/Nodes/ThingSetProperty.cs +++ b/src/ThingSet.Common/Nodes/ThingSetProperty.cs @@ -7,7 +7,7 @@ namespace ThingSet.Common.Nodes; public class ThingSetProperty : ThingSetNode, IThingSetValue { - public ThingSetProperty(ushort id, string name) : base(id, name) + public ThingSetProperty(ushort id, string name, ushort parentId) : base(id, name, parentId) { } diff --git a/src/ThingSet.Common/Nodes/ThingSetRegistry.cs b/src/ThingSet.Common/Nodes/ThingSetRegistry.cs new file mode 100644 index 0000000..695723b --- /dev/null +++ b/src/ThingSet.Common/Nodes/ThingSetRegistry.cs @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2025 Brill Power. + * + * SPDX-License-Identifier: Apache-2.0 + */ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Json.Pointer; +using ThingSet.Common.Protocols; + +namespace ThingSet.Common.Nodes; + +public class ThingSetRegistry +{ + private static readonly ThingSetRegistry Instance = new ThingSetRegistry(); + + public static readonly ThingSetParentNode Root = new RootNode(); + public static readonly ThingSetNode Metadata = new OverlayNode(0x19, "_Metadata"); + + private readonly IDictionary _nodesById = new ConcurrentDictionary(); + + private ThingSetRegistry() + { + } + + public static bool TryGetNode(ushort id, [NotNullWhen(true)] out ThingSetNode? node) + { + return Instance._nodesById.TryGetValue(id, out node); + } + + public static bool TryGetNode(string path, [NotNullWhen(true)] out ThingSetNode? node, [NotNullWhen(false)] out ThingSetStatus? error) + { + if (String.IsNullOrEmpty(path)) + { + // root object + node = Root; + error = null; + return true; + } + + if (path[0] == '/') + { + // what if a gateway? + node = null; + error = ThingSetStatus.NotAGateway; + return false; + } + + if (!JsonPointer.TryParse("/" + path, out JsonPointer? pointer)) + { + node = null; + error = ThingSetStatus.BadRequest; + return false; + } + + node = Root; + foreach (string segment in pointer) + { + if (node is IThingSetParentNode parent) + { + foreach (ThingSetNode child in parent.Children) + { + if (child.Name == segment) + { + node = child; + break; + } + } + } + } + if (node == Root) + { + error = ThingSetStatus.NotFound; + return false; + } + + error = null; + return true; + } + + internal static void Register(ThingSetNode node) + { + Instance._nodesById.Add(node.Id, node); + } + + internal static void Unregister(ThingSetNode node) + { + Instance._nodesById.Remove(node.Id); + } + + internal static IEnumerable GetChildren(ushort parentId) + { + return Instance._nodesById.Values.Where(n => n.ParentId == parentId && n.Id != parentId); // double check for Root node + } + + private class RootNode : ThingSetParentNode + { + public RootNode() : base(0x0, String.Empty, 0x0) + { + } + + public override ThingSetType Type => ThingSetType.Group; + } + + private class OverlayNode : ThingSetNode + { + public OverlayNode(ushort id, string name) : base(id, name, 0x0) + { + } + + public override ThingSetType Type => "overlay"; + } +} \ No newline at end of file diff --git a/src/ThingSet.Common/Protocols/Binary/CborSerialiser.cs b/src/ThingSet.Common/Protocols/Binary/CborSerialiser.cs index 6e3d4fa..d2e5294 100644 --- a/src/ThingSet.Common/Protocols/Binary/CborSerialiser.cs +++ b/src/ThingSet.Common/Protocols/Binary/CborSerialiser.cs @@ -93,6 +93,9 @@ public static void Write(CborWriter writer, object? value) case Array a: WriteArray(writer, a); break; + case Enum e: + Write(writer, Convert.ChangeType(value, value.GetType().GetEnumUnderlyingType())); + break; default: if (value is null) { diff --git a/src/ThingSet.Server/ThingSetServer.cs b/src/ThingSet.Server/ThingSetServer.cs index 5ca5640..db35c3e 100644 --- a/src/ThingSet.Server/ThingSetServer.cs +++ b/src/ThingSet.Server/ThingSetServer.cs @@ -10,6 +10,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text; using System.Threading.Tasks; using ThingSet.Common.Nodes; using ThingSet.Common.Protocols; @@ -22,6 +23,10 @@ public class ThingSetServer : IDisposable { private readonly IServerTransport _transport; + private const ushort MetadataNameId = 0x1a; + private const ushort MetadataTypeId = 0x1b; + private const ushort MetadataAccessId = 0x1c; + public ThingSetServer(IServerTransport transport) { _transport = transport; @@ -41,7 +46,8 @@ private Memory OnRequestReceived(object identifier, Memory request) int length; if (span[0] > 0x20) { - throw new NotSupportedException("Text mode is not currently supported."); + // don't support text mode at the moment + return Encoding.UTF8.GetBytes($":{(byte)ThingSetStatus.NotImplemented:X2}"); } else { @@ -134,7 +140,7 @@ private int HandleFetch(ThingSetRequestContextBase context, Memory respons CborReader reader = new CborReader(context.RequestBody, CborConformanceMode.Lax, allowMultipleRootLevelValues: true); if (reader.PeekState() == CborReaderState.Null) { - if (context.Endpoint is ThingSetParentNode parent) + if (context.Endpoint is IThingSetParentNode parent) { if (context.UseIds) { @@ -153,6 +159,34 @@ private int HandleFetch(ThingSetRequestContextBase context, Memory respons return 1; } } + else if (context.Endpoint == ThingSetRegistry.Metadata && + reader.PeekState() == CborReaderState.StartArray) + { + if (CborDeserialiser.Read(reader) is object?[] ids) + { + List> metadata = new List>(); + foreach (uint id in ids.OfType()) + { + if (ThingSetRegistry.TryGetNode((ushort)id, out ThingSetNode? node)) + { + metadata.Add(new Dictionary + { + { MetadataNameId, node.Name }, + { MetadataTypeId, node.Type.Type }, + { MetadataAccessId, ThingSetAccess.AnyReadWrite }, // TODO + }); + } + } + CborSerialiser.Write(writer, metadata.ToArray()); + writer.Encode(responseSpan.Slice(1)); + return writer.BytesWritten + 1; + } + else + { + responseSpan[0] = (byte)ThingSetStatus.BadRequest; + return 1; + } + } else { responseSpan[0] = (byte)ThingSetStatus.NotImplemented; @@ -166,7 +200,7 @@ private int HandleGet(ThingSetRequestContextBase context, Memory response) responseSpan[0] = (byte)ThingSetStatus.Content; CborWriter writer = new CborWriter(CborConformanceMode.Lax, allowMultipleRootLevelValues: true); writer.WriteNull(); - if (context.Endpoint is ThingSetParentNode parent) + if (context.Endpoint is IThingSetParentNode parent) { if (context.UseIds) { @@ -267,9 +301,16 @@ internal ThingSetBinaryRequestContext(Memory request) : base(request) { Endpoint = node; } - else if (!String.IsNullOrEmpty(Path)) + else if (Path != null) // empty is valid { - // + if (ThingSetRegistry.TryGetNode(Path, out node, out ThingSetStatus? error)) + { + Endpoint = node; + } + else + { + // how to signal error? + } } RequestBody = request.Slice(request.Length - _cborReader.BytesRemaining); } diff --git a/test/ThingSet.Test/TestClientServer.cs b/test/ThingSet.Test/TestClientServer.cs index dc25f4b..8f1d3f8 100644 --- a/test/ThingSet.Test/TestClientServer.cs +++ b/test/ThingSet.Test/TestClientServer.cs @@ -3,9 +3,12 @@ * * SPDX-License-Identifier: Apache-2.0 */ +using System.Linq; using System.Net; using System.Threading.Tasks; using ThingSet.Client; +using ThingSet.Client.Schema; +using ThingSet.Common; using ThingSet.Common.Nodes; using ThingSet.Common.Transports.Ip; using ThingSet.Server; @@ -17,7 +20,7 @@ public class TestClientServer [Test] public async Task TestProperty() { - ThingSetProperty voltage = new ThingSetProperty(0x200, "voltage"); + ThingSetProperty voltage = new ThingSetProperty(0x200, "voltage", 0x0); voltage.Value = 24.0f; using (IpClientTransport clientTransport = new IpClientTransport("127.0.0.1")) @@ -28,6 +31,11 @@ public async Task TestProperty() await server.ListenAsync(); await Task.Delay(10); await client.ConnectAsync(); + var nodes = client.GetNodes(ThingSetNodeEnumerationOptions.All).ToDictionary(n => n.Id); + Assert.That(nodes, Contains.Key(0x200U)); + var node = nodes[0x200]; + Assert.That(node.Name, Is.EqualTo("voltage")); + Assert.That(node.Type, Is.EqualTo(ThingSetType.Float)); object? result = client.Get(0x200); Assert.That(result, Is.Not.Null); Assert.That(result, Is.TypeOf()); @@ -38,7 +46,7 @@ public async Task TestProperty() [Test] public async Task TestFunction() { - var function = ThingSetFunction.Create(0x500, "xTest", (int x, int y) => x + y); + var function = ThingSetFunction.Create(0x500, "xTest", 0x0, (int x, int y) => x + y); using (IpClientTransport clientTransport = new IpClientTransport("127.0.0.1")) using (IpServerTransport serverTransport = new IpServerTransport(IPAddress.Loopback)) @@ -58,6 +66,14 @@ public async Task TestFunction() Assert.That(array[1], Is.EqualTo(0x502)); } + var nodes = client.GetNodes(ThingSetNodeEnumerationOptions.All).ToDictionary(n => n.Id); + Assert.That(nodes, Contains.Key(0x500U)); + var node = nodes[0x500]; + Assert.That(node.Name, Is.EqualTo("xTest")); + Assert.That(node.Type.IsFunction, Is.True); + Assert.That(node.Type.Type, Is.EqualTo("(i32,i32)->(i32)")); + Assert.That(node.Children.Count, Is.EqualTo(2)); + result = client.Exec(0x500, 1, 2); Assert.That(result, Is.Not.Null); } diff --git a/test/ThingSet.Test/TestNodes.cs b/test/ThingSet.Test/TestNodes.cs index 71ba970..f43670a 100644 --- a/test/ThingSet.Test/TestNodes.cs +++ b/test/ThingSet.Test/TestNodes.cs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ using System; +using System.Linq; using ThingSet.Common.Nodes; namespace ThingSet.Test; @@ -14,8 +15,8 @@ public class TestNodes public void TestFunctions() { Func add = (x, y) => x + y; - var xAdd = ThingSetFunction.Create(0x1000, "xAdd", add); + var xAdd = ThingSetFunction.Create(0x1000, "xAdd", 0x0, add); Assert.That(xAdd.Type.Type, Is.EqualTo("(i32,i32)->(i32)")); - Assert.That(xAdd.Children.Count, Is.EqualTo(2)); + Assert.That(xAdd.Children.Count(), Is.EqualTo(2)); } } \ No newline at end of file From e85dd54f43a1f0857421ca793190d815abd44f53 Mon Sep 17 00:00:00 2001 From: Gareth Potter Date: Wed, 1 Oct 2025 11:31:16 +0100 Subject: [PATCH 5/7] move --- src/ThingSet.Common/ThingSet.Common.csproj | 1 - src/ThingSet.Net.sln | 6 ++++ .../Nodes/IThingSetFunction.cs | 2 +- .../Nodes/IThingSetParentNode.cs | 2 +- .../Nodes/IThingSetValue.cs | 2 +- .../Nodes/ThingSetFunction.cs | 3 +- .../Nodes/ThingSetFunctionParameter.cs | 4 ++- .../Nodes/ThingSetGroup.cs | 4 ++- .../Nodes/ThingSetNode.cs | 4 ++- .../Nodes/ThingSetParentNode.cs | 2 +- .../Nodes/ThingSetProperty.cs | 4 ++- .../Nodes/ThingSetRegistry.cs | 3 +- .../ThingSet.Server.Common.csproj | 30 +++++++++++++++++++ src/ThingSet.Server/ThingSet.Server.csproj | 1 + src/ThingSet.Server/ThingSetServer.cs | 2 +- test/ThingSet.Test/TestClientServer.cs | 2 +- test/ThingSet.Test/TestNodes.cs | 2 +- test/ThingSet.Test/ThingSet.Test.csproj | 1 + 18 files changed, 61 insertions(+), 14 deletions(-) rename src/{ThingSet.Common => ThingSet.Server.Common}/Nodes/IThingSetFunction.cs (81%) rename src/{ThingSet.Common => ThingSet.Server.Common}/Nodes/IThingSetParentNode.cs (84%) rename src/{ThingSet.Common => ThingSet.Server.Common}/Nodes/IThingSetValue.cs (79%) rename src/{ThingSet.Common => ThingSet.Server.Common}/Nodes/ThingSetFunction.cs (95%) rename src/{ThingSet.Common => ThingSet.Server.Common}/Nodes/ThingSetFunctionParameter.cs (84%) rename src/{ThingSet.Common => ThingSet.Server.Common}/Nodes/ThingSetGroup.cs (82%) rename src/{ThingSet.Common => ThingSet.Server.Common}/Nodes/ThingSetNode.cs (89%) rename src/{ThingSet.Common => ThingSet.Server.Common}/Nodes/ThingSetParentNode.cs (90%) rename src/{ThingSet.Common => ThingSet.Server.Common}/Nodes/ThingSetProperty.cs (88%) rename src/{ThingSet.Common => ThingSet.Server.Common}/Nodes/ThingSetRegistry.cs (97%) create mode 100644 src/ThingSet.Server.Common/ThingSet.Server.Common.csproj diff --git a/src/ThingSet.Common/ThingSet.Common.csproj b/src/ThingSet.Common/ThingSet.Common.csproj index d0da930..f7b356d 100644 --- a/src/ThingSet.Common/ThingSet.Common.csproj +++ b/src/ThingSet.Common/ThingSet.Common.csproj @@ -16,7 +16,6 @@ - diff --git a/src/ThingSet.Net.sln b/src/ThingSet.Net.sln index fbd989d..aa7c33e 100644 --- a/src/ThingSet.Net.sln +++ b/src/ThingSet.Net.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThingSet.Test", "..\test\Th EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThingSet.Server", "ThingSet.Server\ThingSet.Server.csproj", "{E05B4B2F-6A55-4261-98ED-EC16B529B766}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThingSet.Server.Common", "ThingSet.Server.Common\ThingSet.Server.Common.csproj", "{4390B592-24E5-40A2-BD91-593836F871E1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,6 +49,10 @@ Global {E05B4B2F-6A55-4261-98ED-EC16B529B766}.Debug|Any CPU.Build.0 = Debug|Any CPU {E05B4B2F-6A55-4261-98ED-EC16B529B766}.Release|Any CPU.ActiveCfg = Release|Any CPU {E05B4B2F-6A55-4261-98ED-EC16B529B766}.Release|Any CPU.Build.0 = Release|Any CPU + {4390B592-24E5-40A2-BD91-593836F871E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4390B592-24E5-40A2-BD91-593836F871E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4390B592-24E5-40A2-BD91-593836F871E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4390B592-24E5-40A2-BD91-593836F871E1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ThingSet.Common/Nodes/IThingSetFunction.cs b/src/ThingSet.Server.Common/Nodes/IThingSetFunction.cs similarity index 81% rename from src/ThingSet.Common/Nodes/IThingSetFunction.cs rename to src/ThingSet.Server.Common/Nodes/IThingSetFunction.cs index 325d8f3..e462517 100644 --- a/src/ThingSet.Common/Nodes/IThingSetFunction.cs +++ b/src/ThingSet.Server.Common/Nodes/IThingSetFunction.cs @@ -5,7 +5,7 @@ */ using System; -namespace ThingSet.Common.Nodes; +namespace ThingSet.Server.Common.Nodes; public interface IThingSetFunction { diff --git a/src/ThingSet.Common/Nodes/IThingSetParentNode.cs b/src/ThingSet.Server.Common/Nodes/IThingSetParentNode.cs similarity index 84% rename from src/ThingSet.Common/Nodes/IThingSetParentNode.cs rename to src/ThingSet.Server.Common/Nodes/IThingSetParentNode.cs index b4aafb2..bde98ea 100644 --- a/src/ThingSet.Common/Nodes/IThingSetParentNode.cs +++ b/src/ThingSet.Server.Common/Nodes/IThingSetParentNode.cs @@ -5,7 +5,7 @@ */ using System.Collections.Generic; -namespace ThingSet.Common.Nodes; +namespace ThingSet.Server.Common.Nodes; public interface IThingSetParentNode { diff --git a/src/ThingSet.Common/Nodes/IThingSetValue.cs b/src/ThingSet.Server.Common/Nodes/IThingSetValue.cs similarity index 79% rename from src/ThingSet.Common/Nodes/IThingSetValue.cs rename to src/ThingSet.Server.Common/Nodes/IThingSetValue.cs index 54d3384..1d99958 100644 --- a/src/ThingSet.Common/Nodes/IThingSetValue.cs +++ b/src/ThingSet.Server.Common/Nodes/IThingSetValue.cs @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -namespace ThingSet.Common.Nodes; +namespace ThingSet.Server.Common.Nodes; public interface IThingSetValue { diff --git a/src/ThingSet.Common/Nodes/ThingSetFunction.cs b/src/ThingSet.Server.Common/Nodes/ThingSetFunction.cs similarity index 95% rename from src/ThingSet.Common/Nodes/ThingSetFunction.cs rename to src/ThingSet.Server.Common/Nodes/ThingSetFunction.cs index c229dd7..51d21c3 100644 --- a/src/ThingSet.Common/Nodes/ThingSetFunction.cs +++ b/src/ThingSet.Server.Common/Nodes/ThingSetFunction.cs @@ -6,8 +6,9 @@ using System; using System.Collections.Generic; using System.Linq; +using ThingSet.Common; -namespace ThingSet.Common.Nodes; +namespace ThingSet.Server.Common.Nodes; public class ThingSetFunction : ThingSetNode, IThingSetParentNode, IThingSetFunction where TDelegate : Delegate diff --git a/src/ThingSet.Common/Nodes/ThingSetFunctionParameter.cs b/src/ThingSet.Server.Common/Nodes/ThingSetFunctionParameter.cs similarity index 84% rename from src/ThingSet.Common/Nodes/ThingSetFunctionParameter.cs rename to src/ThingSet.Server.Common/Nodes/ThingSetFunctionParameter.cs index b750671..1b4a732 100644 --- a/src/ThingSet.Common/Nodes/ThingSetFunctionParameter.cs +++ b/src/ThingSet.Server.Common/Nodes/ThingSetFunctionParameter.cs @@ -3,7 +3,9 @@ * * SPDX-License-Identifier: Apache-2.0 */ -namespace ThingSet.Common.Nodes; +using ThingSet.Common; + +namespace ThingSet.Server.Common.Nodes; public class ThingSetFunctionParameter : ThingSetNode { diff --git a/src/ThingSet.Common/Nodes/ThingSetGroup.cs b/src/ThingSet.Server.Common/Nodes/ThingSetGroup.cs similarity index 82% rename from src/ThingSet.Common/Nodes/ThingSetGroup.cs rename to src/ThingSet.Server.Common/Nodes/ThingSetGroup.cs index 9158771..56b3185 100644 --- a/src/ThingSet.Common/Nodes/ThingSetGroup.cs +++ b/src/ThingSet.Server.Common/Nodes/ThingSetGroup.cs @@ -3,7 +3,9 @@ * * SPDX-License-Identifier: Apache-2.0 */ -namespace ThingSet.Common.Nodes; +using ThingSet.Common; + +namespace ThingSet.Server.Common.Nodes; public class ThingSetGroup : ThingSetParentNode { diff --git a/src/ThingSet.Common/Nodes/ThingSetNode.cs b/src/ThingSet.Server.Common/Nodes/ThingSetNode.cs similarity index 89% rename from src/ThingSet.Common/Nodes/ThingSetNode.cs rename to src/ThingSet.Server.Common/Nodes/ThingSetNode.cs index 45567f6..4e6419a 100644 --- a/src/ThingSet.Common/Nodes/ThingSetNode.cs +++ b/src/ThingSet.Server.Common/Nodes/ThingSetNode.cs @@ -3,7 +3,9 @@ * * SPDX-License-Identifier: Apache-2.0 */ -namespace ThingSet.Common.Nodes; +using ThingSet.Common; + +namespace ThingSet.Server.Common.Nodes; public abstract class ThingSetNode { diff --git a/src/ThingSet.Common/Nodes/ThingSetParentNode.cs b/src/ThingSet.Server.Common/Nodes/ThingSetParentNode.cs similarity index 90% rename from src/ThingSet.Common/Nodes/ThingSetParentNode.cs rename to src/ThingSet.Server.Common/Nodes/ThingSetParentNode.cs index cb14129..2267942 100644 --- a/src/ThingSet.Common/Nodes/ThingSetParentNode.cs +++ b/src/ThingSet.Server.Common/Nodes/ThingSetParentNode.cs @@ -5,7 +5,7 @@ */ using System.Collections.Generic; -namespace ThingSet.Common.Nodes; +namespace ThingSet.Server.Common.Nodes; public abstract class ThingSetParentNode : ThingSetNode, IThingSetParentNode { diff --git a/src/ThingSet.Common/Nodes/ThingSetProperty.cs b/src/ThingSet.Server.Common/Nodes/ThingSetProperty.cs similarity index 88% rename from src/ThingSet.Common/Nodes/ThingSetProperty.cs rename to src/ThingSet.Server.Common/Nodes/ThingSetProperty.cs index ae33a8b..a7e3a8c 100644 --- a/src/ThingSet.Common/Nodes/ThingSetProperty.cs +++ b/src/ThingSet.Server.Common/Nodes/ThingSetProperty.cs @@ -3,7 +3,9 @@ * * SPDX-License-Identifier: Apache-2.0 */ -namespace ThingSet.Common.Nodes; +using ThingSet.Common; + +namespace ThingSet.Server.Common.Nodes; public class ThingSetProperty : ThingSetNode, IThingSetValue { diff --git a/src/ThingSet.Common/Nodes/ThingSetRegistry.cs b/src/ThingSet.Server.Common/Nodes/ThingSetRegistry.cs similarity index 97% rename from src/ThingSet.Common/Nodes/ThingSetRegistry.cs rename to src/ThingSet.Server.Common/Nodes/ThingSetRegistry.cs index 695723b..b8a34bd 100644 --- a/src/ThingSet.Common/Nodes/ThingSetRegistry.cs +++ b/src/ThingSet.Server.Common/Nodes/ThingSetRegistry.cs @@ -9,9 +9,10 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Json.Pointer; +using ThingSet.Common; using ThingSet.Common.Protocols; -namespace ThingSet.Common.Nodes; +namespace ThingSet.Server.Common.Nodes; public class ThingSetRegistry { diff --git a/src/ThingSet.Server.Common/ThingSet.Server.Common.csproj b/src/ThingSet.Server.Common/ThingSet.Server.Common.csproj new file mode 100644 index 0000000..dfbf1c2 --- /dev/null +++ b/src/ThingSet.Server.Common/ThingSet.Server.Common.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + disable + enable + Brill Power, Gareth Potter + Copyright (c) Brill Power 2025 + https://github.com/Brill-Power/ThingSet.Net + https://github.com/Brill-Power/ThingSet.Net.git + git + ThingSet.Server.Common + Apache-2.0 + README.md + iot + + + + + + + + + + + + + + + diff --git a/src/ThingSet.Server/ThingSet.Server.csproj b/src/ThingSet.Server/ThingSet.Server.csproj index ad6a6d0..13ae83c 100644 --- a/src/ThingSet.Server/ThingSet.Server.csproj +++ b/src/ThingSet.Server/ThingSet.Server.csproj @@ -1,6 +1,7 @@  + diff --git a/src/ThingSet.Server/ThingSetServer.cs b/src/ThingSet.Server/ThingSetServer.cs index db35c3e..de8f01d 100644 --- a/src/ThingSet.Server/ThingSetServer.cs +++ b/src/ThingSet.Server/ThingSetServer.cs @@ -12,7 +12,7 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; -using ThingSet.Common.Nodes; +using ThingSet.Server.Common.Nodes; using ThingSet.Common.Protocols; using ThingSet.Common.Protocols.Binary; using ThingSet.Common.Transports; diff --git a/test/ThingSet.Test/TestClientServer.cs b/test/ThingSet.Test/TestClientServer.cs index 8f1d3f8..662f4ea 100644 --- a/test/ThingSet.Test/TestClientServer.cs +++ b/test/ThingSet.Test/TestClientServer.cs @@ -9,7 +9,7 @@ using ThingSet.Client; using ThingSet.Client.Schema; using ThingSet.Common; -using ThingSet.Common.Nodes; +using ThingSet.Server.Common.Nodes; using ThingSet.Common.Transports.Ip; using ThingSet.Server; diff --git a/test/ThingSet.Test/TestNodes.cs b/test/ThingSet.Test/TestNodes.cs index f43670a..ad39bc8 100644 --- a/test/ThingSet.Test/TestNodes.cs +++ b/test/ThingSet.Test/TestNodes.cs @@ -5,7 +5,7 @@ */ using System; using System.Linq; -using ThingSet.Common.Nodes; +using ThingSet.Server.Common.Nodes; namespace ThingSet.Test; diff --git a/test/ThingSet.Test/ThingSet.Test.csproj b/test/ThingSet.Test/ThingSet.Test.csproj index d68a2a1..8238046 100644 --- a/test/ThingSet.Test/ThingSet.Test.csproj +++ b/test/ThingSet.Test/ThingSet.Test.csproj @@ -21,6 +21,7 @@ + From aae8b626336b8f1d7f6c28fb7b45850c0a7a45ee Mon Sep 17 00:00:00 2001 From: Gareth Potter Date: Wed, 1 Oct 2025 12:07:29 +0100 Subject: [PATCH 6/7] add rudimentary update support --- .../Nodes/IThingSetValue.cs | 3 + .../Nodes/ThingSetProperty.cs | 3 + src/ThingSet.Server/ThingSetServer.cs | 57 ++++++++++++++++++- test/ThingSet.Test/TestClientServer.cs | 3 + 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/ThingSet.Server.Common/Nodes/IThingSetValue.cs b/src/ThingSet.Server.Common/Nodes/IThingSetValue.cs index 1d99958..cf1a4fe 100644 --- a/src/ThingSet.Server.Common/Nodes/IThingSetValue.cs +++ b/src/ThingSet.Server.Common/Nodes/IThingSetValue.cs @@ -3,9 +3,12 @@ * * SPDX-License-Identifier: Apache-2.0 */ +using System; + namespace ThingSet.Server.Common.Nodes; public interface IThingSetValue { + Type ValueType { get; } object? Value { get; set; } } \ No newline at end of file diff --git a/src/ThingSet.Server.Common/Nodes/ThingSetProperty.cs b/src/ThingSet.Server.Common/Nodes/ThingSetProperty.cs index a7e3a8c..c349971 100644 --- a/src/ThingSet.Server.Common/Nodes/ThingSetProperty.cs +++ b/src/ThingSet.Server.Common/Nodes/ThingSetProperty.cs @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ +using System; using ThingSet.Common; namespace ThingSet.Server.Common.Nodes; @@ -13,6 +14,8 @@ public ThingSetProperty(ushort id, string name, ushort parentId) : base(id, name { } + public Type ValueType => typeof(TValue); + public TValue? Value { get; set; } public override ThingSetType Type => ThingSetType.GetType(typeof(TValue)); diff --git a/src/ThingSet.Server/ThingSetServer.cs b/src/ThingSet.Server/ThingSetServer.cs index de8f01d..dfe0acf 100644 --- a/src/ThingSet.Server/ThingSetServer.cs +++ b/src/ThingSet.Server/ThingSetServer.cs @@ -128,7 +128,62 @@ private int HandleExec(ThingSetRequestContextBase context, Memory response private int HandleUpdate(ThingSetRequestContextBase context, Memory response) { - throw new NotImplementedException(); + Span responseSpan = response.Span; + if (context.Endpoint is IThingSetParentNode parent) + { + CborReader reader = new CborReader(context.RequestBody, CborConformanceMode.Lax, allowMultipleRootLevelValues: true); + if (CborDeserialiser.Read(reader) is Dictionary map) + { + foreach (object key in map.Keys) + { + if (key is string path) + { + ThingSetNode? found = null; + foreach (ThingSetNode child in parent.Children) + { + if (child.Name == path) + { + found = child; + break; + } + } + + if (found == null) + { + responseSpan[0] = (byte)ThingSetStatus.NotFound; + return 1; + } + + if (found is not IThingSetValue value) + { + responseSpan[0] = (byte)ThingSetStatus.BadRequest; + return 1; + } + + try + { + value.Value = Convert.ChangeType(map[key], value.ValueType); + responseSpan[0] = (byte)ThingSetStatus.Changed; + responseSpan[1] = 0xF6; // null + responseSpan[2] = 0xF6; + return 3; + } + catch (Exception) + { + break; + } + } + else + { + responseSpan[0] = (byte)ThingSetStatus.BadRequest; + return 1; + } + } + } + } + + responseSpan[0] = (byte)ThingSetStatus.BadRequest; + return 1; } private int HandleFetch(ThingSetRequestContextBase context, Memory response) diff --git a/test/ThingSet.Test/TestClientServer.cs b/test/ThingSet.Test/TestClientServer.cs index 662f4ea..06dbc4a 100644 --- a/test/ThingSet.Test/TestClientServer.cs +++ b/test/ThingSet.Test/TestClientServer.cs @@ -40,6 +40,9 @@ public async Task TestProperty() Assert.That(result, Is.Not.Null); Assert.That(result, Is.TypeOf()); Assert.That(result, Is.EqualTo(24.0f)); + + client.Update("voltage", 25.0f); + Assert.That(voltage.Value, Is.EqualTo(25.0f)); } } From bc9943ebbf76561122817fac37c8eb00565c219c Mon Sep 17 00:00:00 2001 From: Gareth Potter Date: Wed, 1 Oct 2025 12:12:51 +0100 Subject: [PATCH 7/7] undo rename --- src/ThingSet.Client/ThingSetClient.cs | 20 +++++++++---------- .../CanClientTransport.cs | 2 +- .../IpClientTransport.cs | 2 +- ...inaryRequestType.cs => ThingSetRequest.cs} | 2 +- ...tTextRequestType.cs => ThingSetRequest.cs} | 2 +- src/ThingSet.Server/ThingSetServer.cs | 12 +++++------ 6 files changed, 20 insertions(+), 20 deletions(-) rename src/ThingSet.Common/Protocols/Binary/{ThingSetBinaryRequestType.cs => ThingSetRequest.cs} (87%) rename src/ThingSet.Common/Protocols/Text/{ThingSetTextRequestType.cs => ThingSetRequest.cs} (94%) diff --git a/src/ThingSet.Client/ThingSetClient.cs b/src/ThingSet.Client/ThingSetClient.cs index ea534d6..e3525ac 100644 --- a/src/ThingSet.Client/ThingSetClient.cs +++ b/src/ThingSet.Client/ThingSetClient.cs @@ -85,17 +85,17 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Get(uint id) { - return DoRequest(ThingSetBinaryRequestType.Get, cw => cw.WriteUInt32(id)); + return DoRequest(ThingSetRequest.Get, cw => cw.WriteUInt32(id)); } public object? Get(string path) { - return DoRequest(ThingSetBinaryRequestType.Get, cw => cw.WriteTextString(path)); + return DoRequest(ThingSetRequest.Get, cw => cw.WriteTextString(path)); } public object? Fetch(uint id, object arg) { - return DoRequest(ThingSetBinaryRequestType.Fetch, cw => + return DoRequest(ThingSetRequest.Fetch, cw => { cw.WriteUInt32(id); CborSerialiser.Write(cw, arg); @@ -104,7 +104,7 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Fetch(uint id, params object[] args) { - return DoRequest(ThingSetBinaryRequestType.Fetch, cw => + return DoRequest(ThingSetRequest.Fetch, cw => { cw.WriteUInt32(id); if (args.Length == 0) @@ -120,7 +120,7 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Fetch(string path) { - return DoRequest(ThingSetBinaryRequestType.Fetch, cw => + return DoRequest(ThingSetRequest.Fetch, cw => { cw.WriteTextString(path); cw.WriteNull(); @@ -129,7 +129,7 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Update(string fullyQualifiedName, object value) { - return DoRequest(ThingSetBinaryRequestType.Update, cw => + return DoRequest(ThingSetRequest.Update, cw => { int index = fullyQualifiedName.LastIndexOf('/'); string pathToParent = index > 0 ? fullyQualifiedName.Substring(0, index) : String.Empty; @@ -145,7 +145,7 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Exec(uint id, params object[] args) { - return DoRequest(ThingSetBinaryRequestType.Exec, cw => + return DoRequest(ThingSetRequest.Exec, cw => { cw.WriteUInt32(id); CborSerialiser.Write(cw, args); @@ -154,14 +154,14 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options public object? Exec(string path, params object[] args) { - return DoRequest(ThingSetBinaryRequestType.Exec, cw => + return DoRequest(ThingSetRequest.Exec, cw => { cw.WriteTextString(path); CborSerialiser.Write(cw, args); }); } - private object? DoRequest(ThingSetBinaryRequestType action, Action write) + private object? DoRequest(ThingSetRequest action, Action write) { byte[] buffer = new byte[4095]; Span span = buffer; @@ -169,7 +169,7 @@ public IEnumerable GetNodes(ThingSetNodeEnumerationOptions options if (TargetNodeID.HasValue) { // prefix the request with node to forward to - span[0] = (byte)ThingSetBinaryRequestType.Forward; + span[0] = (byte)ThingSetRequest.Forward; CborWriter w = new CborWriter(CborConformanceMode.Lax); w.WriteTextString($"{TargetNodeID.Value:x}"); w.Encode(span.Slice(1)); diff --git a/src/ThingSet.Common.Transports.Can/CanClientTransport.cs b/src/ThingSet.Common.Transports.Can/CanClientTransport.cs index a9aaf51..22bdf0f 100644 --- a/src/ThingSet.Common.Transports.Can/CanClientTransport.cs +++ b/src/ThingSet.Common.Transports.Can/CanClientTransport.cs @@ -197,7 +197,7 @@ private void RunSubscriptionThread() if (type == MultiFrameMessageType.Single || type == MultiFrameMessageType.Last) { ReadOnlyMemory memory = buffer.Buffer; - if (buffer.Buffer[0] == (byte)ThingSetBinaryRequestType.Report) + if (buffer.Buffer[0] == (byte)ThingSetRequest.Report) { NotifyReport(memory.Slice(1, buffer.Position - 1)); } diff --git a/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs b/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs index c64fee8..32e1dd9 100644 --- a/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs +++ b/src/ThingSet.Common.Transports.Ip/IpClientTransport.cs @@ -118,7 +118,7 @@ private async void RunSubscriptionThread() buffer.Append(result.Buffer); } if ((messageType == MessageType.Last || messageType == MessageType.Single) && - buffer.Buffer[0] == (byte)ThingSetBinaryRequestType.Report) + buffer.Buffer[0] == (byte)ThingSetRequest.Report) { ReadOnlyMemory memory = buffer.Buffer; var len = memory.Length; diff --git a/src/ThingSet.Common/Protocols/Binary/ThingSetBinaryRequestType.cs b/src/ThingSet.Common/Protocols/Binary/ThingSetRequest.cs similarity index 87% rename from src/ThingSet.Common/Protocols/Binary/ThingSetBinaryRequestType.cs rename to src/ThingSet.Common/Protocols/Binary/ThingSetRequest.cs index e7c4694..d442519 100644 --- a/src/ThingSet.Common/Protocols/Binary/ThingSetBinaryRequestType.cs +++ b/src/ThingSet.Common/Protocols/Binary/ThingSetRequest.cs @@ -5,7 +5,7 @@ */ namespace ThingSet.Common.Protocols.Binary; -public enum ThingSetBinaryRequestType : byte +public enum ThingSetRequest : byte { Get = 0x01, Exec = 0x02, diff --git a/src/ThingSet.Common/Protocols/Text/ThingSetTextRequestType.cs b/src/ThingSet.Common/Protocols/Text/ThingSetRequest.cs similarity index 94% rename from src/ThingSet.Common/Protocols/Text/ThingSetTextRequestType.cs rename to src/ThingSet.Common/Protocols/Text/ThingSetRequest.cs index 7d66434..1c6fab4 100644 --- a/src/ThingSet.Common/Protocols/Text/ThingSetTextRequestType.cs +++ b/src/ThingSet.Common/Protocols/Text/ThingSetRequest.cs @@ -5,7 +5,7 @@ */ namespace ThingSet.Common.Protocols.Text; -public enum ThingSetTextRequestType : byte +public enum ThingSetRequest : byte { GetFetch = (byte)'?', /**< Function code for GET and FETCH requests in text mode. */ Exec = (byte)'!', /**< Function code for EXEC request in text mode. */ diff --git a/src/ThingSet.Server/ThingSetServer.cs b/src/ThingSet.Server/ThingSetServer.cs index dfe0acf..bf708ff 100644 --- a/src/ThingSet.Server/ThingSetServer.cs +++ b/src/ThingSet.Server/ThingSetServer.cs @@ -328,12 +328,12 @@ protected ThingSetRequestContextBase(Memory request) internal class ThingSetBinaryRequestContext : ThingSetRequestContextBase { - private readonly ThingSetBinaryRequestType _requestType; + private readonly ThingSetRequest _requestType; private readonly CborReader _cborReader; internal ThingSetBinaryRequestContext(Memory request) : base(request) { - _requestType = (ThingSetBinaryRequestType)request.Span[0]; + _requestType = (ThingSetRequest)request.Span[0]; _cborReader = new CborReader(request.Slice(1), CborConformanceMode.Lax, allowMultipleRootLevelValues: true); CborReaderState state = _cborReader.PeekState(); @@ -370,8 +370,8 @@ internal ThingSetBinaryRequestContext(Memory request) : base(request) RequestBody = request.Slice(request.Length - _cborReader.BytesRemaining); } - public override bool IsGet => _requestType == ThingSetBinaryRequestType.Get; - public override bool IsFetch => _requestType == ThingSetBinaryRequestType.Fetch; - public override bool IsUpdate => _requestType == ThingSetBinaryRequestType.Update; - public override bool IsExec => _requestType == ThingSetBinaryRequestType.Exec; + public override bool IsGet => _requestType == ThingSetRequest.Get; + public override bool IsFetch => _requestType == ThingSetRequest.Fetch; + public override bool IsUpdate => _requestType == ThingSetRequest.Update; + public override bool IsExec => _requestType == ThingSetRequest.Exec; }