From 0715bbf1d6b8b2b948b133df2b8e8ffd017c91da Mon Sep 17 00:00:00 2001 From: Sameer Puri Date: Tue, 27 Oct 2026 15:18:53 -0700 Subject: [PATCH] Fragment outbound UCI control packets in Python host Host.send_control previously serialized UCI control packets into a single frame without fragmentation. Control payloads exceeding the 255-byte UCI payload limit, such as multicast list updates containing multiple extended subsession keys, overflowed the single-byte length field and failed transmission. Segment serialized control payloads into 255-byte chunks, set the Packet Boundary Flag on intermediate frames, and add test coverage exercising multicast list updates across packet boundaries. --- py/pica/pica/__init__.py | 9 ++--- tests/ranging.py | 72 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/py/pica/pica/__init__.py b/py/pica/pica/__init__.py index 423824f..7c277c3 100644 --- a/py/pica/pica/__init__.py +++ b/py/pica/pica/__init__.py @@ -97,10 +97,11 @@ async def _recv_data(self) -> bytes: return await self.data_queue.get() def send_control(self, packet: uci.ControlPacket): - # TODO packet fragmentation. - packet = bytearray(packet.serialize()) - packet[3] = len(packet) - 4 - self.writer.write(packet) + raw = packet.serialize() + chunks = [raw[i : i + 255] for i in range(4, len(raw), 255)] or [b""] + for i, chunk in enumerate(chunks): + pbf = 0x10 if i < len(chunks) - 1 else 0 + self.writer.write(bytes([raw[0] | pbf, raw[1], raw[2], len(chunk)]) + chunk) def send_data(self, packet: uci.DataPacket): packet = bytearray(packet.serialize()) diff --git a/tests/ranging.py b/tests/ranging.py index eb96185..4db736e 100755 --- a/tests/ranging.py +++ b/tests/ranging.py @@ -531,6 +531,78 @@ async def multicast_list_ntf(host: Host, peer: Host): ) ) + host.send_control( + uci.SessionUpdateControllerMulticastListCmd( + session_token=0, + action=uci.UpdateMulticastListAction.REMOVE_CONTROLEE, + payload=uci.SessionUpdateControllerMulticastListCmdPayload( + controlees=[ + uci.Controlee( + short_address=list(peer.mac_address), + subsession_id=0, + ) + ] + ).serialize(), + ) + ) + await host.expect_control( + uci.SessionUpdateControllerMulticastListRsp( + status=uci.Status.OK, + controlee_status=[ + uci.ControleeStatus( + mac_address=list(peer.mac_address), + status=uci.MulticastUpdateStatus.OK_MULTICAST_LIST_UPDATE, + ) + ], + ) + ) + + host.send_control( + uci.SessionSetAppConfigCmd( + session_token=0, + tlvs=[ + uci.AppConfigTlv( + cfg_id=uci.AppConfigTlvType.STS_CONFIG, + v=bytes([uci.StsConfig.PROVISIONED_FOR_RESPONDER_SUB_SESSION_KEY]), + ) + ], + ) + ) + await host.expect_control( + uci.SessionSetAppConfigRsp(status=uci.Status.OK, cfg_status=[]) + ) + + # 8 * 38-byte controlees -> 310-byte payload, placing the 8th MAC address in the 2nd PBF fragment. + controlees = [ + uci.Controlee_V2_0_32_Byte_Version( + short_address=bytearray([0, i + 3]), + subsession_id=i + 1, + subsession_key=bytearray(32), + ) + for i in range(8) + ] + host.send_control( + uci.SessionUpdateControllerMulticastListCmd( + session_token=0, + action=uci.UpdateMulticastListAction.ADD_CONTROLEE_WITH_EXTENDED_SUB_SESSION_KEY, + payload=uci.SessionUpdateControllerMulticastListCmd_2_0_32_Byte_Payload( + controlees=controlees + ).serialize(), + ) + ) + await host.expect_control( + uci.SessionUpdateControllerMulticastListRsp( + status=uci.Status.OK, + controlee_status=[ + uci.ControleeStatus( + mac_address=[0, i + 3], + status=uci.MulticastUpdateStatus.OK_MULTICAST_LIST_UPDATE, + ) + for i in range(8) + ], + ) + ) + host.send_control(uci.SessionDeinitCmd(session_token=0)) await host.expect_control(uci.SessionDeinitRsp(status=uci.Status.OK))