From b78fdd1d3e38777339a91d9c8283cece63bd17ac Mon Sep 17 00:00:00 2001 From: jdomnitz <380352+jdomnitz@users.noreply.github.com> Date: Sun, 12 Jan 2025 01:39:20 -0500 Subject: [PATCH] Support for bluetooth le --- ExampleConsole/ExampleConsole.csproj | 2 +- MatterDotNet/Entities/Node.cs | 32 ++- MatterDotNet/MatterDotNet.csproj | 39 +-- .../BTDiscoveryService.cs | 116 +++++++++ .../OperationalDiscovery/FabricInterface.cs | 46 ++++ ...coveryService.cs => IPDiscoveryService.cs} | 55 ++-- .../Protocol/Connection/BLEEndPoint.cs | 38 +++ .../Protocol/Connection/BTPConnection.cs | 245 ++++++++++++++++++ MatterDotNet/Protocol/Payloads/BTPFrame.cs | 136 ++++++++++ .../Protocol/Payloads/Flags/BTPFlags.cs | 26 ++ .../Payloads/OpCodes/BTPManagementOpcode.cs | 26 ++ MatterDotNet/Protocol/Payloads/TLVPayload.cs | 2 + .../Protocol/Sessions/SessionManager.cs | 24 +- .../Subprotocols/InteractionManager.cs | 2 +- README.md | 17 +- 15 files changed, 730 insertions(+), 76 deletions(-) create mode 100644 MatterDotNet/OperationalDiscovery/BTDiscoveryService.cs create mode 100644 MatterDotNet/OperationalDiscovery/FabricInterface.cs rename MatterDotNet/OperationalDiscovery/{DiscoveryService.cs => IPDiscoveryService.cs} (80%) create mode 100644 MatterDotNet/Protocol/Connection/BLEEndPoint.cs create mode 100644 MatterDotNet/Protocol/Connection/BTPConnection.cs create mode 100644 MatterDotNet/Protocol/Payloads/BTPFrame.cs create mode 100644 MatterDotNet/Protocol/Payloads/Flags/BTPFlags.cs create mode 100644 MatterDotNet/Protocol/Payloads/OpCodes/BTPManagementOpcode.cs diff --git a/ExampleConsole/ExampleConsole.csproj b/ExampleConsole/ExampleConsole.csproj index 676d59b..45212e7 100644 --- a/ExampleConsole/ExampleConsole.csproj +++ b/ExampleConsole/ExampleConsole.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net8.0-windows10.0.19041.0; net8.0 enable enable diff --git a/MatterDotNet/Entities/Node.cs b/MatterDotNet/Entities/Node.cs index 2c3c011..6635167 100644 --- a/MatterDotNet/Entities/Node.cs +++ b/MatterDotNet/Entities/Node.cs @@ -13,6 +13,7 @@ using MatterDotNet.Clusters.Utility; using MatterDotNet.OperationalDiscovery; using MatterDotNet.PKI; +using MatterDotNet.Protocol.Connection; using MatterDotNet.Protocol.Sessions; using MatterDotNet.Protocol.Subprotocols; using System.Net; @@ -56,15 +57,23 @@ private Node(ODNode connection, Fabric fabric, OperationalCertificate noc) /// public async Task GetCASESession() { - using (SessionContext session = SessionManager.GetUnsecureSession(new IPEndPoint(connection.IPAddress!, connection.Port), true)) - return await GetCASESession(session); + if (connection.IPAddress != null) + { + using (SessionContext session = SessionManager.GetUnsecureSession(new IPEndPoint(connection.IPAddress!, connection.Port), true)) + return await GetCASESession(session); + } + else + { + using (SessionContext session = SessionManager.GetUnsecureSession(new BLEEndPoint(connection.BTAddress), true)) + return await GetCASESession(session); + } } /// /// Get a secure session for the node /// /// /// - public async Task GetCASESession(SessionContext session) + internal async Task GetCASESession(SessionContext session) { CASE caseProtocol = new CASE(session); //TODO - Use OD session params @@ -74,15 +83,18 @@ public async Task GetCASESession(SessionContext session) return caseSession; } - internal static Node CreateTemp(OperationalCertificate noc, Fabric fabric, ODNode opInfo) + internal static Node CreateTemp(OperationalCertificate noc, Fabric fabric, ODNode opInfo, EndPoint rootNode) { - return new Node(opInfo, fabric, noc); + Node node = new Node(opInfo, fabric, noc); + rootNode.SetNode(node); + node.root = rootNode; + return node; } internal static async Task Enumerate(OperationalCertificate noc, Fabric fabric) { string operationalInstanceName = $"{Convert.ToHexString(fabric.CompressedFabricID)}-{noc.NodeID:X16}"; - ODNode? opInfo = await DiscoveryService.Shared.Find(operationalInstanceName); + ODNode? opInfo = await IPDiscoveryService.Shared.Find(operationalInstanceName); if (opInfo == null) return null; return await Enumerate(noc, fabric, opInfo); @@ -112,6 +124,14 @@ internal static async Task Populate(SecureSession session, Node node) await node.Root.EnumerateClusters(session); } + internal string OperationalInstanceName + { + get + { + return $"{Convert.ToHexString(fabric.CompressedFabricID)}-{noc.NodeID:X16}"; + } + } + /// public override string ToString() { diff --git a/MatterDotNet/MatterDotNet.csproj b/MatterDotNet/MatterDotNet.csproj index 9e56041..56dc384 100644 --- a/MatterDotNet/MatterDotNet.csproj +++ b/MatterDotNet/MatterDotNet.csproj @@ -1,24 +1,30 @@  + enable + enable + 0.1.0 + jdomnitz + SmartHomeOS and Contributors + AGPL-3.0-or-later + MatterDotNet + A C# implementation of the Matter 1.3 Standard (Formally known as project chip) + Copyright MatterDotNet Contributors + README.md + https://github.com/SmartHomeOS/MatterDotNet/ + matter; matter-controller; smarthome; project-chip; dotnet; + Library is not yet functional. See README for details. + logo.png + True + + + + net8.0-windows10.0.19041.0; net9.0-windows10.0.19041.0; net8.0 + + net8.0; net9.0 - enable - enable - 0.1.0 - jdomnitz - SmartHomeOS and Contributors - AGPL-3.0-or-later - MatterDotNet - A C# implementation of the Matter 1.3 Standard (Formally known as project chip) - Copyright MatterDotNet Contributors - README.md - https://github.com/SmartHomeOS/MatterDotNet/ - matter; matter-controller; smarthome; project-chip; dotnet; - Library is not yet functional. See README for details. - logo.png - True - + True True @@ -40,6 +46,7 @@ + diff --git a/MatterDotNet/OperationalDiscovery/BTDiscoveryService.cs b/MatterDotNet/OperationalDiscovery/BTDiscoveryService.cs new file mode 100644 index 0000000..ad2be8f --- /dev/null +++ b/MatterDotNet/OperationalDiscovery/BTDiscoveryService.cs @@ -0,0 +1,116 @@ +// MatterDotNet Copyright (C) 2025 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using InTheHand.Bluetooth; +using MatterDotNet.Protocol.Connection; +using System.Buffers.Binary; + +namespace MatterDotNet.OperationalDiscovery +{ + /// + /// Bluetooth LE Operational Discovery Service + /// + public static class BTDiscoveryService + { + /// + /// Generate commissionable node info from BLE advertisement + /// + /// + /// + /// + /// + private static ODNode FromAdvertisement(string id, string name, ReadOnlySpan bleAdvertisement) + { + ODNode ret = new ODNode(); + if (bleAdvertisement[0] == 0x0) + ret.CommissioningMode = CommissioningMode.Basic; + ret.Discriminator = (ushort)(BinaryPrimitives.ReadUInt16LittleEndian(bleAdvertisement.Slice(1, 2)) & 0xFFF); + ret.Vendor = BinaryPrimitives.ReadUInt16LittleEndian(bleAdvertisement.Slice(3, 2)); + ret.Product = BinaryPrimitives.ReadUInt16LittleEndian(bleAdvertisement.Slice(5, 2)); + ret.DeviceName = name; + ret.BTAddress = id; + return ret; + } + + /// + /// Discover all matter devices commissionable using Bluetooth LE + /// + /// + /// + public static async Task ScanAll() + { + Dictionary discoveredDevices = new Dictionary(); + void Bluetooth_AdvertisementReceived(object? sender, BluetoothAdvertisingEvent e) + { + if (e.ServiceData.ContainsKey(BTPConnection.MATTER_UUID) && e.Device != null && !discoveredDevices.ContainsKey(e.Device.Id)) + discoveredDevices.Add(e.Device.Id, FromAdvertisement(e.Device.Id, e.Device?.Name ?? e.Name, e.ServiceData[BTPConnection.MATTER_UUID])); + } + if (await Bluetooth.GetAvailabilityAsync() == false) + throw new PlatformNotSupportedException("No Bluetooth Adapter Found"); + BluetoothLEScanOptions opts = new BluetoothLEScanOptions(); + BluetoothLEScanFilter filter = new BluetoothLEScanFilter(); + filter.Services.Add(BTPConnection.MATTER_UUID); + opts.Filters.Add(filter); + opts.AcceptAllAdvertisements = false; + opts.KeepRepeatedDevices = false; + Bluetooth.AdvertisementReceived += Bluetooth_AdvertisementReceived; + BluetoothLEScan scan = await Bluetooth.RequestLEScanAsync(opts); + await Task.Delay(3000); + scan.Stop(); + Bluetooth.AdvertisementReceived -= Bluetooth_AdvertisementReceived; + return discoveredDevices.Values.ToArray(); + } + + /// + /// Find a Bluetooth device that matches the provided payload + /// + /// + /// + /// + public static async Task Find(CommissioningPayload payload) + { + SemaphoreSlim findLock = new SemaphoreSlim(0, 1); + ODNode? result = null; + void Bluetooth_AdvertisementReceived(object? sender, BluetoothAdvertisingEvent e) + { + if (e.ServiceData.ContainsKey(BTPConnection.MATTER_UUID)) + { + ODNode found = FromAdvertisement(e.Device.Id, e.Device?.Name ?? e.Name, e.ServiceData[BTPConnection.MATTER_UUID]); + if (found.Vendor != payload.VendorID && payload.VendorID != 0 && found.Vendor != 0) + return; + if (found.Product != payload.ProductID && payload.ProductID != 0 && found.Product != 0) + return; + if (payload.LongDiscriminator && found.Discriminator != payload.Discriminator) + return; + if (!payload.LongDiscriminator && (found.Discriminator >> 8) != payload.Discriminator) + return; + result = found; + findLock.Release(); + } + } + if (await Bluetooth.GetAvailabilityAsync() == false) + throw new InvalidOperationException("No Bluetooth"); + BluetoothLEScanOptions opts = new BluetoothLEScanOptions(); + BluetoothLEScanFilter filter = new BluetoothLEScanFilter(); + filter.Services.Add(BTPConnection.MATTER_UUID); + opts.Filters.Add(filter); + opts.AcceptAllAdvertisements = false; + opts.KeepRepeatedDevices = false; + Bluetooth.AdvertisementReceived += Bluetooth_AdvertisementReceived; + BluetoothLEScan scan = await Bluetooth.RequestLEScanAsync(opts); + await findLock.WaitAsync(10000); + scan.Stop(); + Bluetooth.AdvertisementReceived -= Bluetooth_AdvertisementReceived; + return result; + } + } +} diff --git a/MatterDotNet/OperationalDiscovery/FabricInterface.cs b/MatterDotNet/OperationalDiscovery/FabricInterface.cs new file mode 100644 index 0000000..546da4e --- /dev/null +++ b/MatterDotNet/OperationalDiscovery/FabricInterface.cs @@ -0,0 +1,46 @@ +// MatterDotNet Copyright (C) 2025 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +namespace MatterDotNet.OperationalDiscovery +{ + /// + /// Connectivity method between a node and a controller + /// + [Flags] + public enum FabricInterface + { + /// + /// No connectivity Type + /// + None = 0x0, + /// + /// WiFi Connection + /// + WiFi = 0x1, + /// + /// Thread 802.11.4 + /// + Thread = 0x2, + /// + /// Ethernet + /// + Ethernet = 0x4, + /// + /// One or more IP protocols (aka anything but bluetooth) + /// + IP = 0x8, + /// + /// Bluetooth LE + /// + Bluetooth = 0x10 + } +} diff --git a/MatterDotNet/OperationalDiscovery/DiscoveryService.cs b/MatterDotNet/OperationalDiscovery/IPDiscoveryService.cs similarity index 80% rename from MatterDotNet/OperationalDiscovery/DiscoveryService.cs rename to MatterDotNet/OperationalDiscovery/IPDiscoveryService.cs index 07a008f..b196830 100644 --- a/MatterDotNet/OperationalDiscovery/DiscoveryService.cs +++ b/MatterDotNet/OperationalDiscovery/IPDiscoveryService.cs @@ -10,7 +10,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using System.Buffers.Binary; using TinyDNS; using TinyDNS.Enums; using TinyDNS.Records; @@ -18,18 +17,18 @@ namespace MatterDotNet.OperationalDiscovery { /// - /// Operational Discovery Service + /// IP Operational Discovery Service /// - public class DiscoveryService + public class IPDiscoveryService { bool running = false; MDNS mdns = new MDNS(); - private static DiscoveryService service = new DiscoveryService(); + private static IPDiscoveryService service = new IPDiscoveryService(); /// /// Return a shared instance of the discovery service /// - public static DiscoveryService Shared + public static IPDiscoveryService Shared { get { @@ -46,41 +45,18 @@ public static DiscoveryService Shared } /// - /// Generate commissionable node info from BLE advertisement + /// Find IP commissionable nodes matching the given params /// - /// - /// - /// + /// /// - public static ODNode FromAdvertisement(string id, string name, ReadOnlySpan bleAdvertisement) + public async Task Find(CommissioningPayload commissioningPayload) { - ODNode ret = new ODNode(); - if (bleAdvertisement[0] == 0x0) - ret.CommissioningMode = CommissioningMode.Basic; - ret.Discriminator = (ushort)(BinaryPrimitives.ReadUInt16LittleEndian(bleAdvertisement.Slice(1, 2)) & 0xFFF); - ret.Vendor = BinaryPrimitives.ReadUInt16LittleEndian(bleAdvertisement.Slice(3, 2)); - ret.Product = BinaryPrimitives.ReadUInt16LittleEndian(bleAdvertisement.Slice(5, 2)); - ret.DeviceName = name; - ret.BTAddress = id; - return ret; - } - - /// - /// Find commissionable nodes matching the given params - /// - /// - /// - /// - /// - /// - public async Task Find(uint vendor, uint product, uint discriminator, bool fullLen) - { - List results = await Find(discriminator, fullLen); + List results = await Find(commissioningPayload.Discriminator, commissioningPayload.LongDiscriminator); foreach (ODNode result in results) { - if (result.Vendor != 0 && result.Vendor != vendor) + if (result.Vendor != 0 && commissioningPayload.VendorID != 0 && result.Vendor != commissioningPayload.VendorID) continue; - if (result.Product != 0 && result.Product != product) + if (result.Product != 0 && commissioningPayload.ProductID != 0 && result.Product != commissioningPayload.ProductID) continue; return result; } @@ -88,7 +64,7 @@ public static ODNode FromAdvertisement(string id, string name, ReadOnlySpan - /// Find commissionable nodes matching the given discriminator + /// Find IP commissionable nodes matching the given discriminator /// /// /// @@ -127,11 +103,14 @@ public async Task> Find(uint discriminator, bool fullLen) /// Query operational information about the provided instance /// /// + /// /// - public async Task Find(string operationalInstanceName) + public async Task Find(string operationalInstanceName, bool extendedSearch = false) { + Console.WriteLine("Looking for " + operationalInstanceName); List results; - for (int i = 0; i < 10; i++) + int length = extendedSearch ? 20 : 10; // 60 / 30 seconds + for (int i = 0; i < length; i++) { results = Parse(await mdns.ResolveServiceInstance(operationalInstanceName, "_matter._tcp", "local")); if (results.Count > 0) @@ -141,7 +120,7 @@ public async Task> Find(uint discriminator, bool fullLen) } /// - /// Find all commissionable nodes + /// Find all commissionable nodes accessible by IP /// /// public async Task FindAll() diff --git a/MatterDotNet/Protocol/Connection/BLEEndPoint.cs b/MatterDotNet/Protocol/Connection/BLEEndPoint.cs new file mode 100644 index 0000000..9830b9d --- /dev/null +++ b/MatterDotNet/Protocol/Connection/BLEEndPoint.cs @@ -0,0 +1,38 @@ +// MatterDotNet Copyright (C) 2025 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Net; + +namespace MatterDotNet.Protocol.Connection +{ + /// + /// Bluetooth End Point + /// + public class BLEEndPoint : EndPoint + { + private string address; + + /// + /// Create a Bluetooth End Point + /// + /// + public BLEEndPoint(string address) + { + this.address = address; + } + + /// + /// Get the BT Address associated with this endpoint + /// + public string Address { get { return address; } } + } +} diff --git a/MatterDotNet/Protocol/Connection/BTPConnection.cs b/MatterDotNet/Protocol/Connection/BTPConnection.cs new file mode 100644 index 0000000..679e028 --- /dev/null +++ b/MatterDotNet/Protocol/Connection/BTPConnection.cs @@ -0,0 +1,245 @@ +// MatterDotNet Copyright (C) 2025 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using InTheHand.Bluetooth; +using MatterDotNet.Protocol.Payloads; +using MatterDotNet.Protocol.Payloads.Flags; +using MatterDotNet.Protocol.Payloads.OpCodes; +using MatterDotNet.Protocol.Sessions; +using System.Threading.Channels; + +namespace MatterDotNet.Protocol.Connection +{ + internal class BTPConnection : IConnection + { + public static readonly BluetoothUuid MATTER_UUID = BluetoothUuid.FromShortId(0xFFF6); + private static readonly BluetoothUuid C1_UUID = BluetoothUuid.FromGuid(Guid.Parse("18EE2EF5-263D-4559-959F-4F9C429F9D11")); + private static readonly BluetoothUuid C2_UUID = BluetoothUuid.FromGuid(Guid.Parse("18EE2EF5-263D-4559-959F-4F9C429F9D12")); + + private static readonly TimeSpan CONN_RSP_TIMEOUT = TimeSpan.FromSeconds(5); + private static readonly TimeSpan ACK_TIME = TimeSpan.FromSeconds(6); + private static readonly TimeSpan ACK_TIMEOUT = TimeSpan.FromSeconds(15); + + CancellationTokenSource cts = new CancellationTokenSource(); + private GattCharacteristic Read; + private GattCharacteristic Write; + Channel instream = Channel.CreateBounded(10); + ushort MTU = 0; + byte ServerWindow = 0; + byte txCounter = 0; // First is 0 + byte rxCounter = 0; + byte rxAcknowledged = 255; //Ensures we acknowledge the handshake + byte txAcknowledged = 0; + Timer? AckTimer; + SemaphoreSlim WriteLock = new SemaphoreSlim(1, 1); + bool connected; + BluetoothDevice? device; + + public BTPConnection(BLEEndPoint bleDevice) + { + Connect(bleDevice.Address).Wait(); + AckTimer = new Timer(SendAck, null, ACK_TIME, ACK_TIME); + if (Read == null || Write == null) + throw new InvalidOperationException("Failed to initialize characteristics"); + } + + private async Task Connect(string deviceID) + { + device = await BluetoothDevice.FromIdAsync(deviceID); + device.GattServerDisconnected += Device_GattServerDisconnected; + await Connect(); + } + + private async Task Connect() + { + if (!device!.Gatt.IsConnected) + await device.Gatt.ConnectAsync(); + MTU = (ushort)Math.Min(device.Gatt.Mtu, 244); + GattService service = await device.Gatt.GetPrimaryServiceAsync(MATTER_UUID); + Write = await service.GetCharacteristicAsync(C1_UUID); + Read = await service.GetCharacteristicAsync(C2_UUID); + await Read.StopNotificationsAsync(); + + await SendHandshake().WaitAsync(CONN_RSP_TIMEOUT); + await Task.Factory.StartNew(Run); + } + + private void Device_GattServerDisconnected(object? sender, EventArgs e) + { + connected = false; + AckTimer?.Change(Timeout.Infinite, Timeout.Infinite); + rxAcknowledged = 255; + Console.WriteLine(DateTime.Now + "** Disconnected **"); + } + + private async Task SendHandshake() + { + try + { + Console.WriteLine("Send Handshake Request"); + BTPFrame handshake = new BTPFrame(BTPFlags.Handshake | BTPFlags.Management | BTPFlags.Beginning | BTPFlags.Ending); + handshake.OpCode = BTPManagementOpcode.Handshake; + handshake.WindowSize = 8; + handshake.ATT_MTU = MTU; + await Write.WriteValueWithResponseAsync(handshake.Serialize(9)); + Read.CharacteristicValueChanged += Read_CharacteristicValueChanged; + await Read.StartNotificationsAsync(); + + BTPFrame frame = await instream.Reader.ReadAsync(); + MTU = frame.ATT_MTU; + ServerWindow = frame.WindowSize; + if (frame.Version != BTPFrame.MATTER_BT_VERSION1) + throw new NotSupportedException($"Version {frame.Version} not supported"); + connected = true; + Console.WriteLine($"MTU: {MTU}, Window: {ServerWindow}"); + } + catch(Exception ex) + { + Console.WriteLine(ex.Message); + } + } + + private async void SendAck(object? state) + { + await WriteLock.WaitAsync(); + if (!connected) + return; + try + { + BTPFrame segment = new BTPFrame(BTPFlags.Acknowledgement); + segment.Sequence = txCounter++; + if (rxCounter != rxAcknowledged) + { + segment.Acknowledge = rxCounter; + rxAcknowledged = rxCounter; + } + Console.WriteLine("[StandaloneAck] Wrote Segment: " + segment); + await Write.WriteValueWithResponseAsync(segment.Serialize(MTU)); + } + finally + { + WriteLock.Release(); + } + } + + private void Read_CharacteristicValueChanged(object? sender, GattCharacteristicValueChangedEventArgs e) + { + if (e.Value != null) + { + BTPFrame frame = new BTPFrame(e.Value!); + Console.WriteLine("BTP Received: " + frame); + AckTimer?.Change(ACK_TIME, ACK_TIME); + if ((frame.Flags & BTPFlags.Acknowledgement) != 0) + txAcknowledged = frame.Acknowledge; + if ((frame.Flags & BTPFlags.Handshake) == 0) + rxCounter = frame.Sequence; + if ((frame.Flags & BTPFlags.Continuing) != 0 || (frame.Flags & BTPFlags.Beginning) != 0) + instream.Writer.TryWrite(frame); + } + } + + public async Task SendFrame(Exchange exchange, Frame frame, bool reliable) + { + PayloadWriter writer = new PayloadWriter(Frame.MAX_SIZE); + frame.Serialize(writer, exchange.Session); + if (!connected) + await Connect(); + await WaitForWindow(); + await WriteLock.WaitAsync(); + try + { + byte? ack = null; + if (rxCounter != rxAcknowledged) + { + ack = rxCounter; + rxAcknowledged = rxCounter; + AckTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + BTPFrame[] segments = BTPFrame.CreateSegments(frame, exchange.Session, MTU, ack); + foreach (BTPFrame segment in segments) + { + await WaitForWindow(); + segment.Sequence = txCounter++; + Console.WriteLine("Wrote Segment: " + segment); + await Write.WriteValueWithResponseAsync(segment.Serialize(MTU)); + } + } + finally + { + WriteLock.Release(); + } + } + + private async Task WaitForWindow() + { + while (txCounter - txAcknowledged > ServerWindow) + await instream.Reader.WaitToReadAsync(); + } + + public async Task Run() + { + try + { + List segments = new List(); + while (!cts.IsCancellationRequested) + { + BTPFrame segment = await instream.Reader.ReadAsync(); + Console.WriteLine("Segment Received: " + segment); + segments.Add(segment); + if ((segment.Flags & BTPFlags.Ending) == 0x0) + continue; + PayloadWriter buffer = new PayloadWriter(segments[0].Length); + foreach (BTPFrame part in segments) + buffer.Write(part.Payload); + segments.Clear(); + Frame frame = new Frame(buffer.GetPayload().Span); + if (!frame.Valid) + { + Console.WriteLine("Invalid frame received"); + continue; + } + SessionContext? session = SessionManager.GetSession(frame.SessionID); + Console.WriteLine(DateTime.Now.ToString("h:mm:ss") + " Received: " + frame.ToString()); + if (session == null) + { + Console.WriteLine("Unknown Session: " + frame.SessionID); + continue; + } + session.ProcessFrame(frame); + session.Timestamp = DateTime.Now; + session.LastActive = DateTime.Now; + } + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + } + + public Task CloseExchange(Exchange exchange) + { + // Nothing to do + return Task.CompletedTask; + } + + public bool Connected { get { return connected; } } + + /// + public void Dispose() + { + AckTimer?.Change(Timeout.Infinite, Timeout.Infinite); + Read.StopNotificationsAsync().Wait(); + cts.Cancel(); + cts.Dispose(); + } + } +} diff --git a/MatterDotNet/Protocol/Payloads/BTPFrame.cs b/MatterDotNet/Protocol/Payloads/BTPFrame.cs new file mode 100644 index 0000000..f8d1603 --- /dev/null +++ b/MatterDotNet/Protocol/Payloads/BTPFrame.cs @@ -0,0 +1,136 @@ +// MatterDotNet Copyright (C) 2025 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using MatterDotNet.Protocol.Payloads.Flags; +using MatterDotNet.Protocol.Payloads.OpCodes; +using MatterDotNet.Protocol.Sessions; +using System.Buffers.Binary; + +namespace MatterDotNet.Protocol.Payloads +{ + internal class BTPFrame + { + public const byte MATTER_BT_VERSION1 = 0x04; + + public BTPFlags Flags { get; set; } + public BTPManagementOpcode OpCode { get; set; } + public byte Acknowledge { get; set; } + public byte Sequence { get; set; } + public ushort Length { get; set; } + public Memory Payload { get; set; } + public ushort ATT_MTU { get; set; } + public byte WindowSize { get; set; } + public byte Version { get; private set; } + + private BTPFrame() { } + + public BTPFrame(BTPFlags flags) + { + Flags = flags; + } + + public BTPFrame(Memory payload) + { + int pos = 0; + Span span = payload.Span; + Flags = (BTPFlags)span[pos++]; + if ((Flags & BTPFlags.Management) != 0) + { + OpCode = (BTPManagementOpcode)span[pos++]; + Version = (byte)(span[pos++] & 0xF); + } + if ((Flags & BTPFlags.Acknowledgement) != 0) + Acknowledge = span[pos++]; + if ((Flags & BTPFlags.Handshake) == 0) + { + Sequence = span[pos++]; + if ((Flags & BTPFlags.Beginning) != 0) + { + Length = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(pos, 2)); + pos += 2; + } + Payload = payload.Slice(pos); + } + if ((Flags & BTPFlags.Handshake) != 0) + { + ATT_MTU = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(pos, 2)); + WindowSize = span[pos + 2]; + } + } + + public byte[] Serialize(int mtu) + { + PayloadWriter stream = new PayloadWriter(mtu); + stream.Write((byte)Flags); + if ((Flags & BTPFlags.Management) != 0) + stream.Write((byte)OpCode); + if ((Flags & BTPFlags.Acknowledgement) != 0) + stream.Write(Acknowledge); + if ((Flags & BTPFlags.Handshake) == 0) + { + stream.Write(Sequence); + if ((Flags & BTPFlags.Beginning) != 0) + stream.Write(Length); + stream.Write(Payload.Span); + } + else + { + stream.Write([MATTER_BT_VERSION1, 0x0, 0x0, 0x0]); + stream.Write(ATT_MTU); + stream.Write(WindowSize); + } + return stream.GetPayload().ToArray(); + } + + public override string ToString() + { + return $"Flags: {Flags}, Seq: {Sequence}, Ack: {Acknowledge}, Len: {Length}, Payload: {Payload.Length}"; + } + + internal static BTPFrame[] CreateSegments(Frame frame, SessionContext session, ushort maxSegment, byte? ack = null) + { + PayloadWriter writer = new PayloadWriter(Frame.MAX_SIZE); + frame.Serialize(writer, session); + List segments = new List(); + ushort bytesPacked = 0; + do + { + ushort header = 2; //Flags + sequence + BTPFrame segment = new BTPFrame(); + if (segments.Count == 0) + { + segment.Flags = BTPFlags.Beginning; + segment.Length = (ushort)writer.Length; + header += 2; //Length field + if (ack.HasValue) + { + header += 1; + segment.Acknowledge = ack.Value; + segment.Flags |= BTPFlags.Acknowledgement; + } + } + else + segment.Flags = BTPFlags.Continuing; + + ushort segmentSize = (ushort)Math.Min(writer.Length - bytesPacked, maxSegment - header); + if (segmentSize + bytesPacked == writer.Length) + segment.Flags |= BTPFlags.Ending; + segment.Payload = writer.GetPayload().Slice(bytesPacked, segmentSize); + + segments.Add(segment); + bytesPacked += segmentSize; + } + while (bytesPacked < writer.Length); + return segments.ToArray(); + } + } +} diff --git a/MatterDotNet/Protocol/Payloads/Flags/BTPFlags.cs b/MatterDotNet/Protocol/Payloads/Flags/BTPFlags.cs new file mode 100644 index 0000000..76ce477 --- /dev/null +++ b/MatterDotNet/Protocol/Payloads/Flags/BTPFlags.cs @@ -0,0 +1,26 @@ +// MatterDotNet Copyright (C) 2025 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +namespace MatterDotNet.Protocol.Payloads.Flags +{ + [Flags] + internal enum BTPFlags : byte + { + Beginning = 0x1, + Continuing = 0x2, + Ending = 0x4, + Acknowledgement = 0x8, + Reserved = 0x10, + Management = 0x20, + Handshake = 0x40, + } +} diff --git a/MatterDotNet/Protocol/Payloads/OpCodes/BTPManagementOpcode.cs b/MatterDotNet/Protocol/Payloads/OpCodes/BTPManagementOpcode.cs new file mode 100644 index 0000000..d882c49 --- /dev/null +++ b/MatterDotNet/Protocol/Payloads/OpCodes/BTPManagementOpcode.cs @@ -0,0 +1,26 @@ +// MatterDotNet Copyright (C) 2025 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +namespace MatterDotNet.Protocol.Payloads.OpCodes +{ + internal enum BTPManagementOpcode : byte + { + /// + /// No OpCode + /// + None = 0x0, + /// + /// Request and response for BTP session establishment + /// + Handshake = 0x6C + } +} diff --git a/MatterDotNet/Protocol/Payloads/TLVPayload.cs b/MatterDotNet/Protocol/Payloads/TLVPayload.cs index 77b9afe..c9e972b 100644 --- a/MatterDotNet/Protocol/Payloads/TLVPayload.cs +++ b/MatterDotNet/Protocol/Payloads/TLVPayload.cs @@ -36,6 +36,8 @@ internal TLVPayload(Memory data) : this(new TLVReader(data)) {} /// internal TLVPayload(TLVReader reader, long structureNumber = -1) { } + public TLVPayload(object[] fields) { } + /// /// Write the TLVs to an application payload /// diff --git a/MatterDotNet/Protocol/Sessions/SessionManager.cs b/MatterDotNet/Protocol/Sessions/SessionManager.cs index 6f42de4..93b82b3 100644 --- a/MatterDotNet/Protocol/Sessions/SessionManager.cs +++ b/MatterDotNet/Protocol/Sessions/SessionManager.cs @@ -23,10 +23,10 @@ namespace MatterDotNet.Protocol.Sessions public static class SessionManager { private static uint globalCtr; - private static ConcurrentDictionary connections = new ConcurrentDictionary(); + private static ConcurrentDictionary connections = new ConcurrentDictionary(); private static ConcurrentDictionary sessions = new ConcurrentDictionary(); - public static SessionContext GetUnsecureSession(IPEndPoint ep, bool initiator) + public static SessionContext GetUnsecureSession(EndPoint ep, bool initiator) { return GetUnsecureSession(GetConnection(ep), initiator); } @@ -40,7 +40,7 @@ internal static SessionContext GetUnsecureSession(IConnection connection, bool i return ctx; } - public static SecureSession? CreateSession(IPEndPoint ep, bool PASE, bool initiator, ushort initiatorSessionId, ushort responderSessionId, byte[] i2r, byte[] r2i, ulong localNodeId, ulong peerNodeId, byte[] sharedSecret, byte[] resumptionId, bool group, uint idleInterval, uint activeInterval, uint activeThreshold) + internal static SecureSession? CreateSession(EndPoint ep, bool PASE, bool initiator, ushort initiatorSessionId, ushort responderSessionId, byte[] i2r, byte[] r2i, ulong localNodeId, ulong peerNodeId, byte[] sharedSecret, byte[] resumptionId, bool group, uint idleInterval, uint activeInterval, uint activeThreshold) { return CreateSession(GetConnection(ep), PASE, initiator, initiatorSessionId, responderSessionId, i2r, r2i, localNodeId, peerNodeId, sharedSecret, resumptionId, group, idleInterval, activeInterval, activeThreshold); } @@ -86,13 +86,23 @@ public static uint GlobalUnencryptedCounter } } - private static IConnection GetConnection(IPEndPoint endPoint) + private static IConnection GetConnection(EndPoint endPoint) { if (connections.TryGetValue(endPoint, out IConnection? connection)) return connection; - IConnection con = new MRPConnection(endPoint); - connections.TryAdd(endPoint, con); - return con; + if (endPoint is IPEndPoint ipep) + { + IConnection con = new MRPConnection(ipep); + connections.TryAdd(endPoint, con); + return con; + } + else if (endPoint is BLEEndPoint ble) + { + IConnection con = new BTPConnection(ble); + connections.TryAdd(endPoint, con); + return con; + } + throw new ArgumentException("Invalid EndPoint"); } public static SessionParameter GetDefaultSessionParams() diff --git a/MatterDotNet/Protocol/Subprotocols/InteractionManager.cs b/MatterDotNet/Protocol/Subprotocols/InteractionManager.cs index a1db59c..bd97411 100644 --- a/MatterDotNet/Protocol/Subprotocols/InteractionManager.cs +++ b/MatterDotNet/Protocol/Subprotocols/InteractionManager.cs @@ -201,7 +201,7 @@ public static async Task ExecCommand(SecureSession secSession, Frame response = await exchange.Read(); if (response.Message.Payload is InvokeResponseMessage msg) { - if (!msg.InvokeResponses[0].Status!.CommandRef.HasValue || msg.InvokeResponses[0].Status!.CommandRef!.Value == refNum) + if (msg.InvokeResponses[0].Status == null || !msg.InvokeResponses[0].Status!.CommandRef.HasValue || msg.InvokeResponses[0].Status!.CommandRef!.Value == refNum) return msg.InvokeResponses[0]; } else if (response.Message.Payload is StatusResponseMessage status) diff --git a/README.md b/README.md index 5181625..135a507 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,25 @@ [![Build](https://github.com/SmartHomeOS/MatterDotNet/actions/workflows/dotnet.yml/badge.svg)](https://github.com/SmartHomeOS/MatterDotNet/actions/workflows/dotnet.yml) # MatterDotNet -**Coming Soon** - New implementation in progress +##### A C# implementation of the Matter 1.4 (formerly Project CHIP) standard #### What's Working -* Automatic Code Generation (directly from the specification) +* Automatic code generation (directly from the specification) +* Operational discovery over IP or Bluetooth LE * Commissioning by QR code or PIN -* Operational Discovery over IP or BT LE -* Reading/Writing Attributes +* Reading/Writing attributes * Executing cluster commands -#### What's In Progress: -* Cluster generation +#### In-Progress: +* Matter 1.4 cluster generation #### Coming Soon -* Matter 1.4 Standard Implementation * Multicast/Group Control * Subscriptions * Events +* Over the Air Software Updates + +#### Will Not Implement +* Provisional specification items (including TermsAndConditions and Joint Fabric) #### Other Projects: * Check out my other projects for [HomeKit](https://github.com/SmartHomeOS/HomeKitDotNet) and [ZWave](https://github.com/SmartHomeOS/ZWaveDotNet)