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