Skip to content

Commit 4a0c4b6

Browse files
committed
Apple commissioning almost works
1 parent 7b05d28 commit 4a0c4b6

14 files changed

+443
-107
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ You do not need to pay anything or be a member organization.
1313

1414
CircuitMatter is currently developed in CPython 3.12, the de facto implementation written in C. It is designed with minimal dependencies so that it can also be used on CircuitPython on microcontrollers.
1515

16-
After cloning the repo, pip install `ecdsa` and `cryptography`.
16+
After cloning the repo, pip install `ecdsa`, `cryptography` and `qrcode`.
1717

1818
### Running a CircuitMatter replay
1919

@@ -73,7 +73,9 @@ Logs can be added into the chip sources to understand what is happening on the c
7373

7474
### Apple Home
7575

76-
The Apple Home app can also discover and (attempt to) commission the device. Tap Add Accessory and the CircuitMatter device will show up as a nearby Matter Accessory. Tap it and then enter the setup code `67202583`. This will start the commissioning process from Apple Home.
76+
The Apple Home app can also discover and (attempt to) commission the device. Tap Add Accessory.
77+
* By default this will pull up the camera to scan a QR Code. CircuitMatter will print the qrcode to the console to scan.
78+
* You can also use the passcode by clicking "More options" and the CircuitMatter device will show up as a nearby Matter Accessory. Tap it and then enter the setup code `67202583`. This will start the commissioning process from Apple Home.
7779

7880
## Generate a certificate declaration
7981

circuitmatter/__init__.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __init__(
3333
with open(state_filename, "r") as state_file:
3434
self.nonvolatile = json.load(state_file)
3535

36-
for key in ["descriminator", "salt", "iteration-count", "verifier"]:
36+
for key in ["discriminator", "salt", "iteration-count", "verifier"]:
3737
if key not in self.nonvolatile:
3838
raise RuntimeError(f"Missing key {key} in state file")
3939

@@ -58,6 +58,7 @@ def __init__(
5858
self._next_endpoint = 0
5959
self._descriptor = data_model.DescriptorCluster()
6060
self._descriptor.PartsList = []
61+
self._descriptor.ServerList = []
6162
self.add_cluster(0, self._descriptor)
6263
basic_info = data_model.BasicInformationCluster()
6364
basic_info.vendor_id = vendor_id
@@ -66,6 +67,11 @@ def __init__(
6667
group_keys = core.GroupKeyManagementCluster()
6768
self.add_cluster(0, group_keys)
6869
network_info = data_model.NetworkCommissioningCluster()
70+
71+
ethernet = data_model.NetworkCommissioningCluster.NetworkInfoStruct()
72+
ethernet.NetworkID = "enp13s0".encode("utf-8")
73+
ethernet.Connected = True
74+
network_info.networks = [ethernet]
6975
network_info.connect_max_time_seconds = 10
7076
self.add_cluster(0, network_info)
7177
general_commissioning = core.GeneralCommissioningCluster()
@@ -75,6 +81,9 @@ def __init__(
7581
)
7682
self.add_cluster(0, noc)
7783

84+
self.vendor_id = vendor_id
85+
self.product_id = product_id
86+
7887
self.manager = session.SessionManager(self.random, self.socket, noc)
7988

8089
print(f"Listening on UDP port {self.UDP_PORT}")
@@ -83,17 +92,21 @@ def __init__(
8392
self.start_commissioning()
8493

8594
def start_commissioning(self):
86-
descriminator = self.nonvolatile["descriminator"]
95+
discriminator = self.nonvolatile["discriminator"]
96+
passcode = self.nonvolatile["passcode"]
8797
txt_records = {
8898
"PI": "",
8999
"PH": "33",
90100
"CM": "1",
91-
"D": str(descriminator),
101+
"D": str(discriminator),
92102
"CRI": "3000",
93103
"CRA": "4000",
94104
"T": "1",
95-
"VP": "65521+32769",
105+
"VP": f"{self.vendor_id}+{self.product_id}",
96106
}
107+
from . import pase
108+
109+
pase.show_qr_code(self.vendor_id, self.product_id, discriminator, passcode)
97110
instance_name = self.random.urandom(8).hex().upper()
98111
self.mdns_server.advertise_service(
99112
"_matterc",
@@ -102,21 +115,25 @@ def start_commissioning(self):
102115
txt_records=txt_records,
103116
instance_name=instance_name,
104117
subtypes=[
105-
f"_L{descriminator}._sub._matterc._udp",
118+
f"_L{discriminator}._sub._matterc._udp",
106119
"_CM._sub._matterc._udp",
107120
],
108121
)
109122

110123
def add_cluster(self, endpoint, cluster):
111124
if endpoint not in self._endpoints:
112125
self._endpoints[endpoint] = {}
113-
self._descriptor.PartsList.append(endpoint)
126+
if endpoint > 0:
127+
self._descriptor.PartsList.append(endpoint)
114128
self._next_endpoint = max(self._next_endpoint, endpoint + 1)
129+
if endpoint == 0:
130+
self._descriptor.ServerList.append(cluster.CLUSTER_ID)
115131
self._endpoints[endpoint][cluster.CLUSTER_ID] = cluster
116132

117133
def add_device(self, device):
118134
self._endpoints[self._next_endpoint] = {}
119-
self._descriptor.PartsList.append(self._next_endpoint)
135+
if self._next_endpoint > 0:
136+
self._descriptor.PartsList.append(self._next_endpoint)
120137
self._next_endpoint += 1
121138

122139
def process_packets(self):
@@ -358,8 +375,16 @@ def process_packet(self, address, data):
358375
report = session.StatusReport()
359376
report.decode(message.application_payload)
360377
print(report)
378+
379+
# Acknowledge the message because we have no further reply.
380+
if message.exchange_flags & session.ExchangeFlags.R:
381+
exchange.send_standalone()
361382
elif protocol_opcode == SecureProtocolOpcode.ICD_CHECK_IN:
362383
print("Received ICD Check-in")
384+
elif protocol_opcode == SecureProtocolOpcode.MRP_STANDALONE_ACK:
385+
print("Received MRP Standalone Ack")
386+
else:
387+
print("Unhandled secure channel opcode", protocol_opcode)
363388
elif message.protocol_id == ProtocolId.INTERACTION_MODEL:
364389
secure_session_context = self.manager.secure_session_contexts[
365390
message.session_id
@@ -456,4 +481,6 @@ def process_packet(self, address, data):
456481
else:
457482
print(message)
458483
print("application payload", message.application_payload.hex(" "))
484+
else:
485+
print("Unknown protocol", message.protocol_id, message.protocol_opcode)
459486
print()

circuitmatter/__main__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def advertise_service(
103103
class MDNSServer(DummyMDNS):
104104
def __init__(self):
105105
self.active_services = {}
106+
self.publish_address = None
106107

107108
def advertise_service(
108109
self,
@@ -130,10 +131,20 @@ def advertise_service(
130131
]
131132
print("running avahi", command)
132133
self.active_services[service_type] = subprocess.Popen(command)
134+
if self.publish_address is None:
135+
command = [
136+
"avahi-publish-address",
137+
f"{instance_name}.local",
138+
"fe80::642:1aff:fe0c:9f2a",
139+
]
140+
print("run", command)
141+
self.publish_address = subprocess.Popen(command)
133142

134143
def __del__(self):
135144
for active_service in self.active_services.values():
136145
active_service.kill()
146+
if self.publish_address is not None:
147+
self.publish_address.kill()
137148

138149

139150
class RecordingRandom:

circuitmatter/clusters/core.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,10 @@ def add_noc(
252252
noc, _ = crypto.MatterCertificate.decode(
253253
args.NOCValue[0], memoryview(args.NOCValue)[1:]
254254
)
255-
icac, _ = crypto.MatterCertificate.decode(
256-
args.ICACValue[0], memoryview(args.ICACValue)[1:]
257-
)
255+
if args.ICACValue:
256+
icac, _ = crypto.MatterCertificate.decode(
257+
args.ICACValue[0], memoryview(args.ICACValue)[1:]
258+
)
258259

259260
response = data_model.NodeOperationalCredentialsCluster.NOCResponse()
260261

circuitmatter/data_model.py

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,29 @@ class Enum16(enum.IntEnum):
1515
pass
1616

1717

18+
class Uint16(tlv.IntMember):
19+
def __init__(self, _id=None, minimum=0):
20+
super().__init__(_id, signed=False, octets=2, minimum=minimum)
21+
22+
23+
class GroupId(Uint16):
24+
pass
25+
26+
27+
class ClusterId(Uint16):
28+
pass
29+
30+
31+
class EndpointNumber(Uint16):
32+
def __init__(self, _id=None):
33+
super().__init__(_id, minimum=1)
34+
35+
36+
# Data model "lists" are encoded as tlv arrays. 🙄
37+
class List(tlv.ArrayMember):
38+
pass
39+
40+
1841
class Attribute:
1942
def __init__(self, _id, default=None):
2043
self.id = _id
@@ -86,7 +109,12 @@ def __init__(self, _id, enum_type, default=None):
86109

87110

88111
class ListAttribute(Attribute):
89-
pass
112+
def __init__(self, _id, element_type):
113+
self.tlv_type = tlv.ArrayMember(None, element_type)
114+
super().__init__(_id)
115+
116+
def encode(self, value) -> bytes:
117+
return self.tlv_type.encode(value)
90118

91119

92120
class BoolAttribute(Attribute):
@@ -218,11 +246,10 @@ class DeviceTypeStruct(tlv.Structure):
218246
devtype_id = tlv.IntMember(0, signed=False, octets=4)
219247
revision = tlv.IntMember(1, signed=False, octets=2, minimum=1)
220248

221-
DeviceTypeList = ListAttribute(0x0000)
222-
ServerList = ListAttribute(0x0001)
223-
ClientList = ListAttribute(0x0002)
224-
PartsList = ListAttribute(0x0003)
225-
TagList = ListAttribute(0x0004)
249+
DeviceTypeList = ListAttribute(0x0000, DeviceTypeStruct)
250+
ServerList = ListAttribute(0x0001, ClusterId())
251+
ClientList = ListAttribute(0x0002, ClusterId())
252+
PartsList = ListAttribute(0x0003, EndpointNumber())
226253

227254

228255
class ProductFinish(enum.IntEnum):
@@ -323,11 +350,20 @@ class GroupKeySetStruct(tlv.Structure):
323350
class GroupKeyManagementCluster(Cluster):
324351
CLUSTER_ID = 0x3F
325352

353+
class GroupKeyMapStruct(tlv.Structure):
354+
GroupId = GroupId(1)
355+
GroupKeySetID = tlv.IntMember(2, signed=False, octets=2, minimum=1)
356+
357+
class GroupInfoMapStruct(tlv.Structure):
358+
GroupId = GroupId(1)
359+
Endpoints = List(2, EndpointNumber())
360+
GroupName = tlv.UTF8StringMember(3, max_length=16)
361+
326362
class KeySetWrite(tlv.Structure):
327363
GroupKeySet = tlv.StructMember(0, GroupKeySetStruct)
328364

329-
group_key_map = ListAttribute(0)
330-
group_table = ListAttribute(1)
365+
group_key_map = ListAttribute(0, GroupKeyMapStruct)
366+
group_table = ListAttribute(1, GroupInfoMapStruct)
331367
max_groups_per_fabric = NumberAttribute(2, signed=False, bits=16, default=0)
332368
max_group_keys_per_fabric = NumberAttribute(3, signed=False, bits=16, default=1)
333369

@@ -403,6 +439,14 @@ class FeatureBitmap(enum.IntFlag):
403439
THREAD_NETWORK_INTERFACE = 0b010
404440
ETHERNET_NETWORK_INTERFACE = 0b100
405441

442+
class WifiBandEnum(Enum8):
443+
BAND_2G4 = 0
444+
BAND_3G65 = 1
445+
BAND_5G = 2
446+
BAND_6G = 3
447+
BAND_60G = 4
448+
BAND_1G = 5
449+
406450
class NetworkCommissioningStatus(Enum8):
407451
SUCCESS = 0
408452
"""Ok, no error"""
@@ -443,15 +487,19 @@ class NetworkCommissioningStatus(Enum8):
443487
UNKNOWN_ERROR = 12
444488
"""Unknown error"""
445489

490+
class NetworkInfoStruct(tlv.Structure):
491+
NetworkID = tlv.OctetStringMember(0, min_length=1, max_length=32)
492+
Connected = tlv.BoolMember(1)
493+
446494
max_networks = NumberAttribute(0, signed=False, bits=8)
447-
networks = ListAttribute(1)
495+
networks = ListAttribute(1, NetworkInfoStruct)
448496
scan_max_time_seconds = NumberAttribute(2, signed=False, bits=8)
449497
connect_max_time_seconds = NumberAttribute(3, signed=False, bits=8)
450498
interface_enabled = BoolAttribute(4)
451499
last_network_status = EnumAttribute(5, NetworkCommissioningStatus)
452500
last_network_id = OctetStringAttribute(6, min_length=1, max_length=32)
453501
last_connect_error_value = NumberAttribute(7, signed=True, bits=32)
454-
supported_wifi_bands = ListAttribute(8)
502+
supported_wifi_bands = ListAttribute(8, WifiBandEnum)
455503
supported_thread_features = BitmapAttribute(9)
456504
thread_version = NumberAttribute(10, signed=False, bits=16)
457505

@@ -547,11 +595,11 @@ class RemoveFabric(tlv.Structure):
547595
class AddTrustedRootCertificate(tlv.Structure):
548596
RootCACertificate = tlv.OctetStringMember(0, 400)
549597

550-
nocs = ListAttribute(0)
551-
fabrics = ListAttribute(1)
598+
nocs = ListAttribute(0, NOCStruct)
599+
fabrics = ListAttribute(1, FabricDescriptorStruct)
552600
supported_fabrics = NumberAttribute(2, signed=False, bits=8)
553601
commissioned_fabrics = NumberAttribute(3, signed=False, bits=8)
554-
trusted_root_certificates = ListAttribute(4)
602+
trusted_root_certificates = ListAttribute(4, tlv.OctetStringMember(None, 400))
555603
current_fabric_index = NumberAttribute(5, signed=False, bits=8, default=0)
556604

557605
attestation_request = Command(0x00, AttestationRequest, 0x01, AttestationResponse)

circuitmatter/exchange.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ def __init__(self, session, initiator: bool, exchange_id: int, protocols):
3939
self.pending_retransmission = None
4040
"""Message that we've attempted to send but hasn't been acked"""
4141

42-
def send(self, protocol_id, protocol_opcode, application_payload=None):
42+
def send(
43+
self, protocol_id, protocol_opcode, application_payload=None, reliable=True
44+
):
4345
message = Message()
4446
message.exchange_flags = ExchangeFlags(0)
4547
if self.initiator:
@@ -49,6 +51,9 @@ def send(self, protocol_id, protocol_opcode, application_payload=None):
4951
self.send_standalone_time = None
5052
message.acknowledged_message_counter = self.pending_acknowledgement
5153
self.pending_acknowledgement = None
54+
if reliable:
55+
message.exchange_flags |= ExchangeFlags.R
56+
self.pending_retransmission = message
5257
message.source_node_id = self.session.local_node_id
5358
message.protocol_id = protocol_id
5459
message.protocol_opcode = protocol_opcode
@@ -57,36 +62,48 @@ def send(self, protocol_id, protocol_opcode, application_payload=None):
5762
self.session.send(message)
5863

5964
def send_standalone(self):
65+
if self.pending_retransmission is not None:
66+
print("resending", self.pending_retransmission)
67+
self.session.send(self.pending_retransmission)
68+
return
6069
self.send(
61-
ProtocolId.SECURE_CHANNEL, SecureProtocolOpcode.MRP_STANDALONE_ACK, None
70+
ProtocolId.SECURE_CHANNEL,
71+
SecureProtocolOpcode.MRP_STANDALONE_ACK,
72+
None,
73+
reliable=False,
6274
)
6375

6476
def receive(self, message) -> bool:
6577
"""Process the message and return if the packet should be dropped."""
66-
if message.protocol_id not in self.protocols:
67-
# Drop messages that don't match the protocols we're waiting for.
68-
return True
69-
7078
# Section 4.12.5.2.1
7179
if message.exchange_flags & ExchangeFlags.A:
7280
if message.acknowledged_message_counter is None:
7381
# Drop messages that are missing an acknowledgement counter.
7482
return True
75-
if message.acknowledged_message_counter != self.pending_acknowledgement:
83+
if (
84+
message.acknowledged_message_counter
85+
!= self.pending_retransmission.message_counter
86+
):
7687
# Drop messages that have the wrong acknowledgement counter.
7788
return True
7889
self.pending_retransmission = None
7990
self.next_retransmission_time = None
8091

92+
if message.protocol_id not in self.protocols:
93+
print("protocol mismatch")
94+
# Drop messages that don't match the protocols we're waiting for.
95+
return True
96+
8197
# Section 4.12.5.2.2
8298
# Incoming packets that are marked Reliable.
8399
if message.exchange_flags & ExchangeFlags.R:
84100
if message.duplicate:
85101
# Send a standalone acknowledgement.
102+
self.send_standalone()
86103
return True
87104
if self.pending_acknowledgement is not None:
88105
# Send a standalone acknowledgement with the message counter we're about to overwrite.
89-
pass
106+
self.send_standalone()
90107
self.pending_acknowledgement = message.message_counter
91108
self.send_standalone_time = (
92109
time.monotonic() + MRP_STANDALONE_ACK_TIMEOUT_MS / 1000

0 commit comments

Comments
 (0)