From 68a32b9269c83edc48a90530247631b48a67dcef Mon Sep 17 00:00:00 2001 From: jdomnitz <380352+jdomnitz@users.noreply.github.com> Date: Fri, 10 Jan 2025 01:01:33 -0500 Subject: [PATCH] Preparation for BLE --- MatterDotNet/Entities/Controller.cs | 4 +- MatterDotNet/Entities/Node.cs | 2 +- .../CommissioningPayload.cs | 8 ++-- .../OperationalDiscovery/DiscoveryService.cs | 43 +++++++++++++++---- MatterDotNet/OperationalDiscovery/ODNode.cs | 15 +++++-- .../Protocol/Connection/MRPConnection.cs | 2 +- MatterDotNet/Protocol/Payloads/Frame.cs | 11 +++-- README.md | 16 ++++--- Test/PayloadParsingTests.cs | 8 ++-- 9 files changed, 74 insertions(+), 35 deletions(-) diff --git a/MatterDotNet/Entities/Controller.cs b/MatterDotNet/Entities/Controller.cs index 5b90075..2a4dc4d 100644 --- a/MatterDotNet/Entities/Controller.cs +++ b/MatterDotNet/Entities/Controller.cs @@ -107,14 +107,14 @@ public async Task Start() throw new NotImplementedException("BLE Commissioning is not supported yet"); // Discover the Node - ODNode? commissionableNode = await DiscoveryService.Shared.Find(payload.VendorID, payload.ProductID, payload.Discriminator, payload.DiscriminatorLength == 12); + ODNode? commissionableNode = await DiscoveryService.Shared.Find(payload.VendorID, payload.ProductID, payload.Discriminator, payload.LongDiscriminator); if (commissionableNode == null) return null; try { // Establish PASE session - unsecureSession = SessionManager.GetUnsecureSession(new IPEndPoint(commissionableNode.Address!, commissionableNode.Port), true); + unsecureSession = SessionManager.GetUnsecureSession(new IPEndPoint(commissionableNode.IPAddress!, commissionableNode.Port), true); PASE pase = new PASE(unsecureSession); paseSecureSession = await pase.EstablishSecureSession(payload.Passcode); if (paseSecureSession == null) diff --git a/MatterDotNet/Entities/Node.cs b/MatterDotNet/Entities/Node.cs index 6c0f67d..2c3c011 100644 --- a/MatterDotNet/Entities/Node.cs +++ b/MatterDotNet/Entities/Node.cs @@ -56,7 +56,7 @@ private Node(ODNode connection, Fabric fabric, OperationalCertificate noc) /// public async Task GetCASESession() { - using (SessionContext session = SessionManager.GetUnsecureSession(new IPEndPoint(connection.Address!, connection.Port), true)) + using (SessionContext session = SessionManager.GetUnsecureSession(new IPEndPoint(connection.IPAddress!, connection.Port), true)) return await GetCASESession(session); } /// diff --git a/MatterDotNet/OperationalDiscovery/CommissioningPayload.cs b/MatterDotNet/OperationalDiscovery/CommissioningPayload.cs index c22a76d..8cd3936 100644 --- a/MatterDotNet/OperationalDiscovery/CommissioningPayload.cs +++ b/MatterDotNet/OperationalDiscovery/CommissioningPayload.cs @@ -92,7 +92,7 @@ public enum FlowType /// /// Length of Discriminator (bits) /// - public byte DiscriminatorLength { get; set; } + public bool LongDiscriminator { get; set; } /// /// Device Type /// @@ -116,8 +116,8 @@ private CommissioningPayload(string QRCode) Flow = (FlowType)readBits(data, 35, 2); Capabilities = (DiscoveryCapabilities)readBits(data, 37, 8); - DiscriminatorLength = 12; - Discriminator = (ushort)readBits(data, 45, DiscriminatorLength); + LongDiscriminator = true; + Discriminator = (ushort)readBits(data, 45, 12); Passcode = readBits(data, 57, 27); uint padding = readBits(data, 84, 4); if (padding != 0) @@ -163,7 +163,7 @@ public static CommissioningPayload FromPIN(string pin) ret.Passcode = (uint)(group1 & 0x3FFF); ushort group2 = ushort.Parse(pin.Substring(6, 4)); ret.Passcode |= (uint)(group2 << 14); - ret.DiscriminatorLength = 4; + ret.LongDiscriminator = false; if (vidpid) { if (pin.Length != 21) diff --git a/MatterDotNet/OperationalDiscovery/DiscoveryService.cs b/MatterDotNet/OperationalDiscovery/DiscoveryService.cs index 8fa9373..07a008f 100644 --- a/MatterDotNet/OperationalDiscovery/DiscoveryService.cs +++ b/MatterDotNet/OperationalDiscovery/DiscoveryService.cs @@ -10,6 +10,7 @@ // 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; @@ -44,6 +45,26 @@ public static DiscoveryService Shared } } + /// + /// Generate commissionable node info from BLE advertisement + /// + /// + /// + /// + /// + public 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; + } + /// /// Find commissionable nodes matching the given params /// @@ -109,9 +130,13 @@ public async Task> Find(uint discriminator, bool fullLen) /// public async Task Find(string operationalInstanceName) { - List results = Parse(await mdns.ResolveServiceInstance(operationalInstanceName, "_matter._tcp", "local")); - if (results.Count > 0) - return results[0]; + List results; + for (int i = 0; i < 10; i++) + { + results = Parse(await mdns.ResolveServiceInstance(operationalInstanceName, "_matter._tcp", "local")); + if (results.Count > 0) + return results[0]; + } return null; } @@ -155,14 +180,14 @@ private List Parse(List msgs) { if (node.Port == 0 && additional is SRVRecord service) node.Port = service.Port; - else if (node.Address == null && additional is ARecord A) - node.Address = A.Address; - else if (node.Address == null && additional is AAAARecord AAAA) - node.Address = AAAA.Address; + else if (node.IPAddress == null && additional is ARecord A) + node.IPAddress = A.Address; + else if (node.IPAddress == null && additional is AAAARecord AAAA) + node.IPAddress = AAAA.Address; else if (additional is TxtRecord txt) PopulateText(txt, ref node); } - if (node.Address == null || node.Port == 0) + if (node.IPAddress == null || node.Port == 0) continue; ret.Add(node); } @@ -196,7 +221,7 @@ private void PopulateText(TxtRecord txt, ref ODNode node) break; case "D": if (ushort.TryParse(kv[1], out ushort descriminator)) - node.Descriminator = descriminator; + node.Discriminator = descriminator; break; case "SII": if (int.TryParse(kv[1], out int sessionIdle)) diff --git a/MatterDotNet/OperationalDiscovery/ODNode.cs b/MatterDotNet/OperationalDiscovery/ODNode.cs index efbe634..a536094 100644 --- a/MatterDotNet/OperationalDiscovery/ODNode.cs +++ b/MatterDotNet/OperationalDiscovery/ODNode.cs @@ -24,12 +24,16 @@ public record ODNode /// /// Discovered IP Address /// - public IPAddress? Address { get; set; } + public IPAddress? IPAddress { get; set; } /// /// Discovered Port /// public ushort Port { get; set; } /// + /// Discovered BT LE Address + /// + public string BTAddress { get; set; } + /// /// Idle Session Interval /// public int IdleInterval { get; set; } @@ -66,12 +70,17 @@ public record ODNode /// public uint Product { get; set; } /// - /// Descriminator + /// Discriminator /// - public ushort Descriminator { get; set; } + public ushort Discriminator { get; set; } /// /// Current Commissioning Mode /// public CommissioningMode CommissioningMode { get; set; } + + public override string ToString() + { + return $"Vendor: {Vendor}, Product: {Product}, Discriminator: {Discriminator:X3}, Name: {DeviceName}, Address: {(BTAddress != null ? BTAddress : $"{IPAddress}:{Port}")}, Type: {Type}, Mode: {CommissioningMode}"; + } } } diff --git a/MatterDotNet/Protocol/Connection/MRPConnection.cs b/MatterDotNet/Protocol/Connection/MRPConnection.cs index 5513c91..55aa35d 100644 --- a/MatterDotNet/Protocol/Connection/MRPConnection.cs +++ b/MatterDotNet/Protocol/Connection/MRPConnection.cs @@ -118,7 +118,7 @@ public async Task SendAck(SessionContext? session, ushort exchange, uint counter ack.Flags |= MessageFlags.DestinationNodeID; ack.Message.AckCounter = counter; ack.Message.Protocol = Payloads.ProtocolType.SecureChannel; - PayloadWriter writer = new PayloadWriter(Frame.MAX_SIZE + 4); + PayloadWriter writer = new PayloadWriter(Frame.MAX_SIZE); ack.Serialize(writer, session!); if (AckTable.TryGetValue(exchange, out uint ctr) && ctr == counter) AckTable.TryRemove(exchange, out _); diff --git a/MatterDotNet/Protocol/Payloads/Frame.cs b/MatterDotNet/Protocol/Payloads/Frame.cs index 6442355..09a703d 100644 --- a/MatterDotNet/Protocol/Payloads/Frame.cs +++ b/MatterDotNet/Protocol/Payloads/Frame.cs @@ -24,7 +24,10 @@ namespace MatterDotNet.Protocol.Payloads /// public class Frame { - internal const int MAX_SIZE = 1280; + /// + /// Frame MTU Size + /// + public const int MAX_SIZE = 1280; internal static readonly byte[] PRIVACY_INFO = Encoding.UTF8.GetBytes("PrivacyKey"); /// @@ -68,7 +71,7 @@ public override string ToString() return $"Frame [F:{Flags}, Session: {SessionID}, S:{Security}, From: {SourceNodeID}, To: {DestinationID}, Ctr: {Counter}, Message: {Message}"; } - internal void Serialize(PayloadWriter stream, SessionContext session) + public void Serialize(PayloadWriter stream, SessionContext session) { stream.Write((byte)Flags); stream.Write(SessionID); @@ -121,13 +124,13 @@ internal void Serialize(PayloadWriter stream, SessionContext session) } } - internal Frame(IPayload? payload, byte opCode) + public Frame(IPayload? payload, byte opCode) { Valid = true; Message = new Version1Payload(payload, opCode); } - internal Frame(Span payload) + public Frame(Span payload) { Valid = true; Flags = (MessageFlags)payload[0]; diff --git a/README.md b/README.md index 8d48447..5181625 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,20 @@ **Coming Soon** - New implementation in progress #### What's Working -* Automatic Code Generator (based on the specification) -* Cryptography -* Framing & TLV Parsing / Generation -* Sessions & Exchanges -* QR Code / MDNS Payload Parsing -* Device Commissioning +* Automatic Code Generation (directly from the specification) +* Commissioning by QR code or PIN +* Operational Discovery over IP or BT LE +* Reading/Writing Attributes +* Executing cluster commands #### What's In Progress: * Cluster generation #### Coming Soon -* Matter 1.3 Standard Implementation +* Matter 1.4 Standard Implementation +* Multicast/Group Control +* Subscriptions +* Events #### Other Projects: * Check out my other projects for [HomeKit](https://github.com/SmartHomeOS/HomeKitDotNet) and [ZWave](https://github.com/SmartHomeOS/ZWaveDotNet) diff --git a/Test/PayloadParsingTests.cs b/Test/PayloadParsingTests.cs index b52b0a9..5305e2b 100644 --- a/Test/PayloadParsingTests.cs +++ b/Test/PayloadParsingTests.cs @@ -26,7 +26,7 @@ public void PIN_AllOnes() Assert.That(parser.VendorID, Is.EqualTo(65535), "Invalid Vendor ID"); Assert.That(parser.ProductID, Is.EqualTo(65535), "Invalid Product ID"); Assert.That(parser.Passcode, Is.EqualTo(0x7FFFFFF), "Invalid Passcode"); - Assert.That(parser.DiscriminatorLength, Is.EqualTo(4), "Invalid Discriminator Length"); + Assert.That(parser.LongDiscriminator, Is.EqualTo(false), "Invalid Discriminator Length"); } [Test] @@ -38,7 +38,7 @@ public void PIN_TestValuesLong() Assert.That(parser.VendorID, Is.EqualTo(1), "Invalid Vendor ID"); Assert.That(parser.ProductID, Is.EqualTo(1), "Invalid Product ID"); Assert.That(parser.Passcode, Is.EqualTo(12345679), "Invalid Passcode"); - Assert.That(parser.DiscriminatorLength, Is.EqualTo(4), "Invalid Discriminator Length"); + Assert.That(parser.LongDiscriminator, Is.EqualTo(false), "Invalid Discriminator Length"); } [Test] @@ -50,7 +50,7 @@ public void PIN_TestValuesShort() Assert.That(parser.VendorID, Is.EqualTo(0), "Vendor ID should not exist"); Assert.That(parser.ProductID, Is.EqualTo(0), "Product ID should not exist"); Assert.That(parser.Passcode, Is.EqualTo(97095205), "Invalid Passcode"); - Assert.That(parser.DiscriminatorLength, Is.EqualTo(4), "Invalid Discriminator Length"); + Assert.That(parser.LongDiscriminator, Is.EqualTo(false), "Invalid Discriminator Length"); } [Test] @@ -64,7 +64,7 @@ public void QR_Test() Assert.That(parser.Passcode, Is.EqualTo(20202021), "Invalid Passcode"); Assert.That(parser.Capabilities, Is.EqualTo(CommissioningPayload.DiscoveryCapabilities.BLE), "Invalid Capabilities"); Assert.That(parser.Flow, Is.EqualTo(CommissioningPayload.FlowType.STANDARD), "Invalid Capabilities"); - Assert.That(parser.DiscriminatorLength, Is.EqualTo(12), "Invalid Discriminator Length"); + Assert.That(parser.LongDiscriminator, Is.EqualTo(true), "Invalid Discriminator Length"); } } } \ No newline at end of file