From 22746f6eb1e245a4a166ec4b21bc89f73cc43ef3 Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Sat, 24 Dec 2022 12:06:02 +0530 Subject: [PATCH 1/7] SetupPayload: Python implementation to generate manual and qrcode Added requirements.txt --- src/setup_payload/python/Base38.py | 49 +++++ .../python/GenerateSetupPayload.py | 174 ++++++++++++++++++ src/setup_payload/python/README.md | 24 +++ src/setup_payload/python/requirements.txt | 2 + 4 files changed, 249 insertions(+) create mode 100644 src/setup_payload/python/Base38.py create mode 100755 src/setup_payload/python/GenerateSetupPayload.py create mode 100644 src/setup_payload/python/README.md create mode 100644 src/setup_payload/python/requirements.txt diff --git a/src/setup_payload/python/Base38.py b/src/setup_payload/python/Base38.py new file mode 100644 index 00000000000000..7a3611f4c5f37b --- /dev/null +++ b/src/setup_payload/python/Base38.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# TODO: Implement the Decode method + +kCodes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', '-', '.'] +kRadix = len(kCodes) +kBase38CharactersNeededInNBytesChunk = [2, 4, 5] +kMaxBytesInChunk = 3 + + +def Encode(bytes): + totalBytes = len(bytes) + qrcode = '' + + for i in range(0, totalBytes, kMaxBytesInChunk): + if (i + kMaxBytesInChunk) > totalBytes: + bytesInChunk = totalBytes - i + else: + bytesInChunk = kMaxBytesInChunk + + value = 0 + for j in range(i, i + bytesInChunk): + value = value + (bytes[j] << (8 * (j - i))) + + base38CharNeeded = kBase38CharactersNeededInNBytesChunk[bytesInChunk - 1] + while base38CharNeeded > 0: + qrcode += kCodes[int(value % kRadix)] + value = int(value / kRadix) + base38CharNeeded -= 1 + + return qrcode diff --git a/src/setup_payload/python/GenerateSetupPayload.py b/src/setup_payload/python/GenerateSetupPayload.py new file mode 100755 index 00000000000000..5c8e815bd9585a --- /dev/null +++ b/src/setup_payload/python/GenerateSetupPayload.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import sys +import enum +import argparse +from bitarray import bitarray +from bitarray.util import ba2int +from stdnum.verhoeff import calc_check_digit +import Base38 + +# See section 5.1.4.1 Manual Pairing Code in the Matter specification v1.0 +kManualDiscriminatorLength = 4 +kPincodeLength = 27 + +kManualChunk1DiscriminatorMsbitsLength = 2 +kManualChunk1DiscriminatorMsbitsPos = 0 +kManualChunk1VidPidPresentBitPos = kManualChunk1DiscriminatorMsbitsPos + kManualChunk1DiscriminatorMsbitsLength +kManualChunk1Length = 1 + +kManualChunk2DiscriminatorLsbitsLength = 2 +kManualChunk2PincodeLsbitsLength = 14 +kManualChunk2PincodeLsbitsPos = 0 +kManualChunk2DiscriminatorLsbitsPos = kManualChunk2PincodeLsbitsPos + kManualChunk2PincodeLsbitsLength +kManualChunk2Length = 5 + +kManualChunk3PincodeMsbitsLength = 13 +kManualChunk3PincodeMsbitsPos = 0 +kManualChunk3Length = 4 + +kManualVendorIdLength = 5 +kManualProductIdLength = 5 + +# See section 5.1.3. QR Code in the Matter specification v1.0 +kQRCodeVersionLength = 3 +kQRCodeDiscriminatorLength = 12 +kQRCodeVendorIdLength = 16 +kQRCodeProductIdLength = 16 +kQRCodeCommissioningFlowLength = 2 +kQRCodeDiscoveryCapBitmaskLength = 8 +kQRCodePaddingLength = 4 +kQRCodeVersion = 0 +kQRCodePadding = 0 + +INVALID_PASSCODES = [00000000, 11111111, 22222222, 33333333, 44444444, 55555555, + 66666666, 77777777, 88888888, 99999999, 12345678, 87654321] + + +class CommissioningFlow(enum.IntEnum): + Standard = 0, + UserIntent = 1, + Custom = 2 + + +class SetupPayload: + def __init__(self, discriminator, pincode, rendezvous=4, flow=CommissioningFlow.Standard, vid=0, pid=0): + self.longDiscriminator = discriminator + self.shortDiscriminator = self.GetShortDiscriminator(discriminator) + self.pincode = pincode + self.rendezvous = rendezvous + self.flow = flow + self.vid = vid + self.pid = pid + + # 4 Most-significant bits of the 12-bits Discriminator + def GetShortDiscriminator(self, discriminator): + return (discriminator >> 8) + + def ManualChunk1(self): + discriminatorShift = (kManualDiscriminatorLength - kManualChunk1DiscriminatorMsbitsLength) + discriminatorMask = (1 << kManualChunk1DiscriminatorMsbitsLength) - 1 + discriminatorChunk = (self.shortDiscriminator >> discriminatorShift) & discriminatorMask + vidPidPresentFlag = 0 if self.flow == CommissioningFlow.Standard else 1 + return (discriminatorChunk << kManualChunk1DiscriminatorMsbitsPos) | (vidPidPresentFlag << kManualChunk1VidPidPresentBitPos) + + def ManualChunk2(self): + discriminatorMask = (1 << kManualChunk2DiscriminatorLsbitsLength) - 1 + pincodeMask = (1 << kManualChunk2PincodeLsbitsLength) - 1 + discriminatorChunk = self.shortDiscriminator & discriminatorMask + return ((self.pincode & pincodeMask) << kManualChunk2PincodeLsbitsPos) | (discriminatorChunk << kManualChunk2DiscriminatorLsbitsPos) + + def ManualChunk3(self): + pincodeShift = kPincodeLength - kManualChunk3PincodeMsbitsLength + pincodeMask = (1 << kManualChunk3PincodeMsbitsLength) - 1 + return ((self.pincode >> pincodeShift) & pincodeMask) << kManualChunk3PincodeMsbitsPos + + def GenerateManualCode(self): + payload = str(self.ManualChunk1()).zfill(kManualChunk1Length) + payload += str(self.ManualChunk2()).zfill(kManualChunk2Length) + payload += str(self.ManualChunk3()).zfill(kManualChunk3Length) + + if self.flow != CommissioningFlow.Standard: + payload += str(self.vid).zfill(kManualVendorIdLength) + payload += str(self.pid).zfill(kManualProductIdLength) + + payload += calc_check_digit(payload) + return payload + + def GenerateQRCode(self): + qrcodeBitString = '{0:b}'.format(kQRCodePadding).zfill(kQRCodePaddingLength) + qrcodeBitString += '{0:b}'.format(self.pincode).zfill(kPincodeLength) + qrcodeBitString += '{0:b}'.format(self.longDiscriminator).zfill(kQRCodeDiscriminatorLength) + qrcodeBitString += '{0:b}'.format(self.rendezvous).zfill(kQRCodeDiscoveryCapBitmaskLength) + qrcodeBitString += '{0:b}'.format(int(self.flow)).zfill(kQRCodeCommissioningFlowLength) + qrcodeBitString += '{0:b}'.format(self.pid).zfill(kQRCodeProductIdLength) + qrcodeBitString += '{0:b}'.format(self.vid).zfill(kQRCodeVendorIdLength) + qrcodeBitString += '{0:b}'.format(kQRCodeVersion).zfill(kQRCodeVersionLength) + + qrcodeBits = bitarray(qrcodeBitString) + bytes = list(qrcodeBits.tobytes()) + bytes.reverse() + return 'MT:{}'.format(Base38.Encode(bytes)) + + +def ValidateArgs(args): + def check_int_range(value, min_value, max_value, name): + if value and ((value < min_value) or (value > max_value)): + print('{} is out of range, should be in range from {} to {}'.format(name, min_value, max_value)) + sys.exit(1) + + if args.passcode is not None: + if ((args.passcode < 0x0000001 and args.passcode > 0x5F5E0FE) or (args.passcode in INVALID_PASSCODES)): + print('Invalid passcode:' + str(args.passcode)) + sys.exit(1) + + check_int_range(args.discriminator, 0x0000, 0x0FFF, 'Discriminator') + check_int_range(args.product_id, 0x0000, 0xFFFF, 'Product id') + check_int_range(args.vendor_id, 0x0000, 0xFFFF, 'Vendor id') + check_int_range(args.discovery_cap_bitmask, 0x0001, 0x0007, 'Discovery Capability Mask') + + +def main(): + def any_base_int(s): return int(s, 0) + parser = argparse.ArgumentParser(description='Matter Manual and QRCode Setup Payload Generator Tool') + parser.add_argument('-d', '--discriminator', type=any_base_int, required=True, + help='The discriminator for pairing, range: 0x00-0x0FFF') + parser.add_argument('-p', '--passcode', type=any_base_int, required=True, + help='The setup passcode for pairing, range: 0x01-0x5F5E0FE') + parser.add_argument('-vid', '--vendor-id', type=any_base_int, default=0, help='Vendor id') + parser.add_argument('-pid', '--product-id', type=any_base_int, default=0, help='Product id') + parser.add_argument('-cf', '--commissioning-flow', type=any_base_int, default=0, + help='Device commissioning flow, 0:Standard, 1:User-Intent, 2:Custom. \ + Default is 0.', choices=[0, 1, 2]) + parser.add_argument('-dm', '--discovery-cap-bitmask', type=any_base_int, default=4, + help='Commissionable device discovery capability bitmask. \ + 0:SoftAP, 1:BLE, 2:OnNetwork. Default: OnNetwork') + args = parser.parse_args() + ValidateArgs(args) + + payloads = SetupPayload(args.discriminator, args.passcode, args.discovery_cap_bitmask, + CommissioningFlow(args.commissioning_flow), args.vendor_id, args.product_id) + manualcode = payloads.GenerateManualCode() + qrcode = payloads.GenerateQRCode() + + print("Manualcode : {}".format(manualcode)) + print("QRCode : {}".format(qrcode)) + + +if __name__ == '__main__': + main() diff --git a/src/setup_payload/python/README.md b/src/setup_payload/python/README.md new file mode 100644 index 00000000000000..dbd44b237b9c93 --- /dev/null +++ b/src/setup_payload/python/README.md @@ -0,0 +1,24 @@ +## Python tool to generate Matter onboarding codes + +Generates Manual Pairing Code and QR Code + +#### example usage: + +``` +./GenerateSetupPayload.py -h +./GenerateSetupPayload.py -d 3840 -p 20202021 -cf 0 -dm 2 -vid 65521 -pid 32768 +``` + +- Output + +``` +Manualcode : 34970112332 +QRCode : MT:Y.K9042C00KA0648G00 +``` + +For more details please refer Matter Specification + +--- + +NOTE: This tool is only capable of generating the payloads and no support to +parse the payloads. diff --git a/src/setup_payload/python/requirements.txt b/src/setup_payload/python/requirements.txt new file mode 100644 index 00000000000000..43800d601c7736 --- /dev/null +++ b/src/setup_payload/python/requirements.txt @@ -0,0 +1,2 @@ +bitarray==2.6.0 +python_stdnum==1.18 From 72ac0bc9e916e50b7d24af38ec3eba327618a5c7 Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Tue, 3 Jan 2023 20:24:00 +0530 Subject: [PATCH 2/7] Converted to pep8 styling --- src/setup_payload/python/Base38.py | 40 ++--- src/setup_payload/python/README.md | 4 +- ...upPayload.py => generate_setup_payload.py} | 138 +++++++++--------- 3 files changed, 91 insertions(+), 91 deletions(-) rename src/setup_payload/python/{GenerateSetupPayload.py => generate_setup_payload.py} (53%) diff --git a/src/setup_payload/python/Base38.py b/src/setup_payload/python/Base38.py index 7a3611f4c5f37b..23113f7ed25c8d 100644 --- a/src/setup_payload/python/Base38.py +++ b/src/setup_payload/python/Base38.py @@ -15,35 +15,35 @@ # See the License for the specific language governing permissions and # limitations under the License. -# TODO: Implement the Decode method +# TODO: Implement the decode method -kCodes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', - 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', - 'U', 'V', 'W', 'X', 'Y', 'Z', '-', '.'] -kRadix = len(kCodes) -kBase38CharactersNeededInNBytesChunk = [2, 4, 5] -kMaxBytesInChunk = 3 +CODES = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', '-', '.'] +RADIX = len(CODES) +BASE38_CHARS_NEEDED_IN_CHUNK = [2, 4, 5] +MAX_BYTES_IN_CHUNK = 3 -def Encode(bytes): - totalBytes = len(bytes) +def encode(bytes): + total_bytes = len(bytes) qrcode = '' - for i in range(0, totalBytes, kMaxBytesInChunk): - if (i + kMaxBytesInChunk) > totalBytes: - bytesInChunk = totalBytes - i + for i in range(0, total_bytes, MAX_BYTES_IN_CHUNK): + if (i + MAX_BYTES_IN_CHUNK) > total_bytes: + bytes_in_chunk = total_bytes - i else: - bytesInChunk = kMaxBytesInChunk + bytes_in_chunk = MAX_BYTES_IN_CHUNK value = 0 - for j in range(i, i + bytesInChunk): + for j in range(i, i + bytes_in_chunk): value = value + (bytes[j] << (8 * (j - i))) - base38CharNeeded = kBase38CharactersNeededInNBytesChunk[bytesInChunk - 1] - while base38CharNeeded > 0: - qrcode += kCodes[int(value % kRadix)] - value = int(value / kRadix) - base38CharNeeded -= 1 + base38_chars_needed = BASE38_CHARS_NEEDED_IN_CHUNK[bytes_in_chunk - 1] + while base38_chars_needed > 0: + qrcode += CODES[int(value % RADIX)] + value = int(value / RADIX) + base38_chars_needed -= 1 return qrcode diff --git a/src/setup_payload/python/README.md b/src/setup_payload/python/README.md index dbd44b237b9c93..068bf553fb7e6b 100644 --- a/src/setup_payload/python/README.md +++ b/src/setup_payload/python/README.md @@ -5,8 +5,8 @@ Generates Manual Pairing Code and QR Code #### example usage: ``` -./GenerateSetupPayload.py -h -./GenerateSetupPayload.py -d 3840 -p 20202021 -cf 0 -dm 2 -vid 65521 -pid 32768 +./generate_setup_payload.py -h +./generate_setup_payload.py -d 3840 -p 20202021 -cf 0 -dm 2 -vid 65521 -pid 32768 ``` - Output diff --git a/src/setup_payload/python/GenerateSetupPayload.py b/src/setup_payload/python/generate_setup_payload.py similarity index 53% rename from src/setup_payload/python/GenerateSetupPayload.py rename to src/setup_payload/python/generate_setup_payload.py index 5c8e815bd9585a..3b816629f0f212 100755 --- a/src/setup_payload/python/GenerateSetupPayload.py +++ b/src/setup_payload/python/generate_setup_payload.py @@ -24,37 +24,37 @@ import Base38 # See section 5.1.4.1 Manual Pairing Code in the Matter specification v1.0 -kManualDiscriminatorLength = 4 -kPincodeLength = 27 +MANUAL_DISCRIMINATOR_LEN = 4 +PINCODE_LEN = 27 -kManualChunk1DiscriminatorMsbitsLength = 2 -kManualChunk1DiscriminatorMsbitsPos = 0 -kManualChunk1VidPidPresentBitPos = kManualChunk1DiscriminatorMsbitsPos + kManualChunk1DiscriminatorMsbitsLength -kManualChunk1Length = 1 +MANUAL_CHUNK1_DISCRIMINATOR_MSBITS_LEN = 2 +MANUAL_CHUNK1_DISCRIMINATOR_MSBITS_POS = 0 +MANUAL_CHUNK1_VID_PID_PRESENT_BIT_POS = MANUAL_CHUNK1_DISCRIMINATOR_MSBITS_POS + MANUAL_CHUNK1_DISCRIMINATOR_MSBITS_LEN +MANUAL_CHUNK1_LEN = 1 -kManualChunk2DiscriminatorLsbitsLength = 2 -kManualChunk2PincodeLsbitsLength = 14 -kManualChunk2PincodeLsbitsPos = 0 -kManualChunk2DiscriminatorLsbitsPos = kManualChunk2PincodeLsbitsPos + kManualChunk2PincodeLsbitsLength -kManualChunk2Length = 5 +MANUAL_CHUNK2_DISCRIMINATOR_LSBITS_LEN = 2 +MANUAL_CHUNK2_PINCODE_LSBITS_LEN = 14 +MANUAL_CHUNK2_PINCODE_LSBITS_POS = 0 +MANUAL_CHUNK2_DISCRIMINATOR_LSBITS_POS = MANUAL_CHUNK2_PINCODE_LSBITS_POS + MANUAL_CHUNK2_PINCODE_LSBITS_LEN +MANUAL_CHUNK2_LEN = 5 -kManualChunk3PincodeMsbitsLength = 13 -kManualChunk3PincodeMsbitsPos = 0 -kManualChunk3Length = 4 +MANUAL_CHUNK3_PINCODE_MSBITS_LEN = 13 +MANUAL_CHUNK3_PINCODE_MSBITS_POS = 0 +MANUAL_CHUNK3_LEN = 4 -kManualVendorIdLength = 5 -kManualProductIdLength = 5 +MANUAL_VID_LEN = 5 +MANUAL_PID_LEN = 5 # See section 5.1.3. QR Code in the Matter specification v1.0 -kQRCodeVersionLength = 3 -kQRCodeDiscriminatorLength = 12 -kQRCodeVendorIdLength = 16 -kQRCodeProductIdLength = 16 -kQRCodeCommissioningFlowLength = 2 -kQRCodeDiscoveryCapBitmaskLength = 8 -kQRCodePaddingLength = 4 -kQRCodeVersion = 0 -kQRCodePadding = 0 +QRCODE_VERSION_LEN = 3 +QRCODE_DISCRIMINATOR_LEN = 12 +QRCODE_VID_LEN = 16 +QRCODE_PID_LEN = 16 +QRCODE_COMMISSIONING_FLOW_LEN = 2 +QRCODE_DISCOVERY_CAP_BITMASK_LEN = 8 +QRCODE_PADDING_LEN = 4 +QRCODE_VERSION = 0 +QRCODE_PADDING = 0 INVALID_PASSCODES = [00000000, 11111111, 22222222, 33333333, 44444444, 55555555, 66666666, 77777777, 88888888, 99999999, 12345678, 87654321] @@ -68,8 +68,8 @@ class CommissioningFlow(enum.IntEnum): class SetupPayload: def __init__(self, discriminator, pincode, rendezvous=4, flow=CommissioningFlow.Standard, vid=0, pid=0): - self.longDiscriminator = discriminator - self.shortDiscriminator = self.GetShortDiscriminator(discriminator) + self.long_discriminator = discriminator + self.short_discriminator = self.get_short_discriminator(discriminator) self.pincode = pincode self.rendezvous = rendezvous self.flow = flow @@ -77,56 +77,56 @@ def __init__(self, discriminator, pincode, rendezvous=4, flow=CommissioningFlow. self.pid = pid # 4 Most-significant bits of the 12-bits Discriminator - def GetShortDiscriminator(self, discriminator): + def get_short_discriminator(self, discriminator): return (discriminator >> 8) - def ManualChunk1(self): - discriminatorShift = (kManualDiscriminatorLength - kManualChunk1DiscriminatorMsbitsLength) - discriminatorMask = (1 << kManualChunk1DiscriminatorMsbitsLength) - 1 - discriminatorChunk = (self.shortDiscriminator >> discriminatorShift) & discriminatorMask - vidPidPresentFlag = 0 if self.flow == CommissioningFlow.Standard else 1 - return (discriminatorChunk << kManualChunk1DiscriminatorMsbitsPos) | (vidPidPresentFlag << kManualChunk1VidPidPresentBitPos) - - def ManualChunk2(self): - discriminatorMask = (1 << kManualChunk2DiscriminatorLsbitsLength) - 1 - pincodeMask = (1 << kManualChunk2PincodeLsbitsLength) - 1 - discriminatorChunk = self.shortDiscriminator & discriminatorMask - return ((self.pincode & pincodeMask) << kManualChunk2PincodeLsbitsPos) | (discriminatorChunk << kManualChunk2DiscriminatorLsbitsPos) - - def ManualChunk3(self): - pincodeShift = kPincodeLength - kManualChunk3PincodeMsbitsLength - pincodeMask = (1 << kManualChunk3PincodeMsbitsLength) - 1 - return ((self.pincode >> pincodeShift) & pincodeMask) << kManualChunk3PincodeMsbitsPos - - def GenerateManualCode(self): - payload = str(self.ManualChunk1()).zfill(kManualChunk1Length) - payload += str(self.ManualChunk2()).zfill(kManualChunk2Length) - payload += str(self.ManualChunk3()).zfill(kManualChunk3Length) + def manual_chunk1(self): + discriminator_shift = (MANUAL_DISCRIMINATOR_LEN - MANUAL_CHUNK1_DISCRIMINATOR_MSBITS_LEN) + discriminator_mask = (1 << MANUAL_CHUNK1_DISCRIMINATOR_MSBITS_LEN) - 1 + discriminator_chunk = (self.short_discriminator >> discriminator_shift) & discriminator_mask + vid_pid_present_flag = 0 if self.flow == CommissioningFlow.Standard else 1 + return (discriminator_chunk << MANUAL_CHUNK1_DISCRIMINATOR_MSBITS_POS) | (vid_pid_present_flag << MANUAL_CHUNK1_VID_PID_PRESENT_BIT_POS) + + def manual_chunk2(self): + discriminator_mask = (1 << MANUAL_CHUNK2_DISCRIMINATOR_LSBITS_LEN) - 1 + pincode_mask = (1 << MANUAL_CHUNK2_PINCODE_LSBITS_LEN) - 1 + discriminator_chunk = self.short_discriminator & discriminator_mask + return ((self.pincode & pincode_mask) << MANUAL_CHUNK2_PINCODE_LSBITS_POS) | (discriminator_chunk << MANUAL_CHUNK2_DISCRIMINATOR_LSBITS_POS) + + def manual_chunk3(self): + pincode_shift = PINCODE_LEN - MANUAL_CHUNK3_PINCODE_MSBITS_LEN + pincode_mask = (1 << MANUAL_CHUNK3_PINCODE_MSBITS_LEN) - 1 + return ((self.pincode >> pincode_shift) & pincode_mask) << MANUAL_CHUNK3_PINCODE_MSBITS_POS + + def generate_manualcode(self): + payload = str(self.manual_chunk1()).zfill(MANUAL_CHUNK1_LEN) + payload += str(self.manual_chunk2()).zfill(MANUAL_CHUNK2_LEN) + payload += str(self.manual_chunk3()).zfill(MANUAL_CHUNK3_LEN) if self.flow != CommissioningFlow.Standard: - payload += str(self.vid).zfill(kManualVendorIdLength) - payload += str(self.pid).zfill(kManualProductIdLength) + payload += str(self.vid).zfill(MANUAL_VID_LEN) + payload += str(self.pid).zfill(MANUAL_PID_LEN) payload += calc_check_digit(payload) return payload - def GenerateQRCode(self): - qrcodeBitString = '{0:b}'.format(kQRCodePadding).zfill(kQRCodePaddingLength) - qrcodeBitString += '{0:b}'.format(self.pincode).zfill(kPincodeLength) - qrcodeBitString += '{0:b}'.format(self.longDiscriminator).zfill(kQRCodeDiscriminatorLength) - qrcodeBitString += '{0:b}'.format(self.rendezvous).zfill(kQRCodeDiscoveryCapBitmaskLength) - qrcodeBitString += '{0:b}'.format(int(self.flow)).zfill(kQRCodeCommissioningFlowLength) - qrcodeBitString += '{0:b}'.format(self.pid).zfill(kQRCodeProductIdLength) - qrcodeBitString += '{0:b}'.format(self.vid).zfill(kQRCodeVendorIdLength) - qrcodeBitString += '{0:b}'.format(kQRCodeVersion).zfill(kQRCodeVersionLength) - - qrcodeBits = bitarray(qrcodeBitString) - bytes = list(qrcodeBits.tobytes()) + def generate_qrcode(self): + qrcode_bit_string = '{0:b}'.format(QRCODE_PADDING).zfill(QRCODE_PADDING_LEN) + qrcode_bit_string += '{0:b}'.format(self.pincode).zfill(PINCODE_LEN) + qrcode_bit_string += '{0:b}'.format(self.long_discriminator).zfill(QRCODE_DISCRIMINATOR_LEN) + qrcode_bit_string += '{0:b}'.format(self.rendezvous).zfill(QRCODE_DISCOVERY_CAP_BITMASK_LEN) + qrcode_bit_string += '{0:b}'.format(int(self.flow)).zfill(QRCODE_COMMISSIONING_FLOW_LEN) + qrcode_bit_string += '{0:b}'.format(self.pid).zfill(QRCODE_PID_LEN) + qrcode_bit_string += '{0:b}'.format(self.vid).zfill(QRCODE_VID_LEN) + qrcode_bit_string += '{0:b}'.format(QRCODE_VERSION).zfill(QRCODE_VERSION_LEN) + + qrcode_bits = bitarray(qrcode_bit_string) + bytes = list(qrcode_bits.tobytes()) bytes.reverse() - return 'MT:{}'.format(Base38.Encode(bytes)) + return 'MT:{}'.format(Base38.encode(bytes)) -def ValidateArgs(args): +def validate_args(args): def check_int_range(value, min_value, max_value, name): if value and ((value < min_value) or (value > max_value)): print('{} is out of range, should be in range from {} to {}'.format(name, min_value, max_value)) @@ -159,12 +159,12 @@ def any_base_int(s): return int(s, 0) help='Commissionable device discovery capability bitmask. \ 0:SoftAP, 1:BLE, 2:OnNetwork. Default: OnNetwork') args = parser.parse_args() - ValidateArgs(args) + validate_args(args) payloads = SetupPayload(args.discriminator, args.passcode, args.discovery_cap_bitmask, CommissioningFlow(args.commissioning_flow), args.vendor_id, args.product_id) - manualcode = payloads.GenerateManualCode() - qrcode = payloads.GenerateQRCode() + manualcode = payloads.generate_manualcode() + qrcode = payloads.generate_qrcode() print("Manualcode : {}".format(manualcode)) print("QRCode : {}".format(qrcode)) From c3fe6ced438e3da58e39eb8aa2193cbcd27806fa Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Tue, 3 Jan 2023 20:31:16 +0530 Subject: [PATCH 3/7] removed get_short_discriminator method which seemed to be confusing --- src/setup_payload/python/generate_setup_payload.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/setup_payload/python/generate_setup_payload.py b/src/setup_payload/python/generate_setup_payload.py index 3b816629f0f212..c5a1385c372b48 100755 --- a/src/setup_payload/python/generate_setup_payload.py +++ b/src/setup_payload/python/generate_setup_payload.py @@ -69,17 +69,13 @@ class CommissioningFlow(enum.IntEnum): class SetupPayload: def __init__(self, discriminator, pincode, rendezvous=4, flow=CommissioningFlow.Standard, vid=0, pid=0): self.long_discriminator = discriminator - self.short_discriminator = self.get_short_discriminator(discriminator) + self.short_discriminator = discriminator >> 8 self.pincode = pincode self.rendezvous = rendezvous self.flow = flow self.vid = vid self.pid = pid - # 4 Most-significant bits of the 12-bits Discriminator - def get_short_discriminator(self, discriminator): - return (discriminator >> 8) - def manual_chunk1(self): discriminator_shift = (MANUAL_DISCRIMINATOR_LEN - MANUAL_CHUNK1_DISCRIMINATOR_MSBITS_LEN) discriminator_mask = (1 << MANUAL_CHUNK1_DISCRIMINATOR_MSBITS_LEN) - 1 From 487c82ed790e89250d585baf2806184e1d41d01b Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Tue, 10 Jan 2023 12:53:34 +0530 Subject: [PATCH 4/7] Unit tests for python setup payload generator --- .github/workflows/build.yaml | 7 + .../run_python_setup_payload_gen_test.py | 129 ++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 src/setup_payload/tests/run_python_setup_payload_gen_test.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 88c8d1f93b38b9..b98a39c9e49224 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -350,6 +350,13 @@ jobs: scripts/run_in_build_env.sh 'pip3 install ./out/controller/python/chip_repl-0.0-py3-none-any.whl' scripts/run_in_build_env.sh '(cd src/controller/python/test/unit_tests/ && python3 -m unittest -v)' + - name: Run Python Setup Payload Generator Test + timeout-minutes: 10 + run: | + scripts/run_in_build_env.sh 'scripts/examples/gn_build_example.sh examples/chip-tool out/' + scripts/run_in_build_env.sh 'pip3 install -r src/setup_payload/python/requirements.txt' + scripts/tests/run_python_setup_payload_generator_test.sh out/chip-tool + build_darwin: name: Build on Darwin (clang, python_lib, simulated) timeout-minutes: 200 diff --git a/src/setup_payload/tests/run_python_setup_payload_gen_test.py b/src/setup_payload/tests/run_python_setup_payload_gen_test.py new file mode 100644 index 00000000000000..c59eeb24d707de --- /dev/null +++ b/src/setup_payload/tests/run_python_setup_payload_gen_test.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2023 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import sys +import random +import subprocess + +CHIP_TOPDIR = os.path.dirname(os.path.realpath(__file__))[:-len(os.path.join('src', 'setup_payload', 'tests'))] +sys.path.insert(0, os.path.join(CHIP_TOPDIR, 'src', 'setup_payload', 'python')) +from generate_setup_payload import SetupPayload, CommissioningFlow, INVALID_PASSCODES # noqa: E402 + + +def payload_param_dict(): + return { + 'Version': None, + 'VendorID': None, + 'ProductID': None, + 'Custom flow': None, + 'Discovery Bitmask': None, + 'Short discriminator': None, + 'Long discriminator': None, + 'Passcode': None + } + + +def remove_escape_sequence(data): + ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + result = ansi_escape.sub('', data) + return result + + +def parse_setup_payload(chip_tool, payload): + cmd_args = [chip_tool, 'payload', 'parse-setup-payload', payload] + data = subprocess.check_output(cmd_args).decode('utf-8') + data = remove_escape_sequence(data) + parsed_params = payload_param_dict() + for key in parsed_params: + k_st = data.find(key) + if k_st == -1: + continue + + # 1 is for ":" + k_end = k_st + len(key) + 1 + + k_nl = data.find('\n', k_end) + parsed_params[key] = data[k_end:k_nl].split()[0] + + return parsed_params + + +def generate_payload_params(): + payload_params = payload_param_dict() + payload_params['Version'] = 0 + payload_params['VendorID'] = random.randint(0, 0xFFFF) + payload_params['ProductID'] = random.randint(0, 0xFFFF) + payload_params['Custom flow'] = random.randint(0, 2) + payload_params['Discovery Bitmask'] = random.randint(0, 7) + payload_params['Long discriminator'] = random.randint(0, 0x0FFF) + payload_params['Short discriminator'] = (payload_params['Long discriminator'] >> 8) + payload_params['Passcode'] = random.randint(1, 0x5F5E0FE) + + # Rather than regenerating, just subtracting 5 + if payload_params['Passcode'] in INVALID_PASSCODES: + payload_params['Passcode'] -= 5 + + return payload_params + + +def generate_payloads(in_params): + payloads = SetupPayload(in_params['Long discriminator'], in_params['Passcode'], + in_params['Discovery Bitmask'], CommissioningFlow(in_params['Custom flow']), + in_params['VendorID'], in_params['ProductID']) + manualcode = payloads.generate_manualcode() + qrcode = payloads.generate_qrcode() + return manualcode, qrcode + + +def verify_payloads(chip_tool): + in_params = generate_payload_params() + manualcode, qrcode = generate_payloads(in_params) + manualcode_params = parse_setup_payload(chip_tool, manualcode) + qrcode_params = parse_setup_payload(chip_tool, qrcode) + + print("Input parameters:", in_params) + print("Manualcode:", manualcode) + print("QRCode:", qrcode) + print("Manualcode parsed by chip-tool:", manualcode_params) + print("QRCode parsed by chip-tool:", qrcode_params) + + assert in_params['Version'] == int(manualcode_params['Version'], 0) + assert in_params['Passcode'] == int(manualcode_params['Passcode'], 0) + assert in_params['Short discriminator'] == int(manualcode_params['Short discriminator'], 0) + if in_params['Custom flow'] != 0: + assert in_params['VendorID'] == int(manualcode_params['VendorID'], 0) + assert in_params['ProductID'] == int(manualcode_params['ProductID'], 0) + + assert in_params['Version'] == int(qrcode_params['Version'], 0) + assert in_params['VendorID'] == int(qrcode_params['VendorID'], 0) + assert in_params['ProductID'] == int(qrcode_params['ProductID'], 0) + assert in_params['Custom flow'] == int(qrcode_params['Custom flow'], 0) + assert in_params['Discovery Bitmask'] == int(qrcode_params['Discovery Bitmask'], 0) + assert in_params['Passcode'] == int(qrcode_params['Passcode'], 0) + assert in_params['Long discriminator'] == int(qrcode_params['Long discriminator'], 0) + + +def main(): + if len(sys.argv) == 2: + chip_tool = sys.argv[1] + for i in range(0, 10): + verify_payloads(chip_tool) + + +if __name__ == '__main__': + main() From b7a8807b24cdaafd9d61d0eff4dea2e9d1db4faa Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Tue, 10 Jan 2023 13:01:57 +0530 Subject: [PATCH 5/7] restyled --- src/setup_payload/python/generate_setup_payload.py | 7 ++++--- .../tests/run_python_setup_payload_gen_test.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/setup_payload/python/generate_setup_payload.py b/src/setup_payload/python/generate_setup_payload.py index c5a1385c372b48..967cc778d2550b 100755 --- a/src/setup_payload/python/generate_setup_payload.py +++ b/src/setup_payload/python/generate_setup_payload.py @@ -15,13 +15,14 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import sys -import enum import argparse +import enum +import sys + +import Base38 from bitarray import bitarray from bitarray.util import ba2int from stdnum.verhoeff import calc_check_digit -import Base38 # See section 5.1.4.1 Manual Pairing Code in the Matter specification v1.0 MANUAL_DISCRIMINATOR_LEN = 4 diff --git a/src/setup_payload/tests/run_python_setup_payload_gen_test.py b/src/setup_payload/tests/run_python_setup_payload_gen_test.py index c59eeb24d707de..68903460a30e3f 100644 --- a/src/setup_payload/tests/run_python_setup_payload_gen_test.py +++ b/src/setup_payload/tests/run_python_setup_payload_gen_test.py @@ -15,14 +15,14 @@ # limitations under the License. import os -import re -import sys import random +import re import subprocess +import sys CHIP_TOPDIR = os.path.dirname(os.path.realpath(__file__))[:-len(os.path.join('src', 'setup_payload', 'tests'))] sys.path.insert(0, os.path.join(CHIP_TOPDIR, 'src', 'setup_payload', 'python')) -from generate_setup_payload import SetupPayload, CommissioningFlow, INVALID_PASSCODES # noqa: E402 +from generate_setup_payload import INVALID_PASSCODES, CommissioningFlow, SetupPayload # noqa: E402 def payload_param_dict(): From 820e895e442d24fdb2ba78d07a5f65d21aac0cce Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Tue, 10 Jan 2023 13:31:01 +0530 Subject: [PATCH 6/7] use the correct script --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b98a39c9e49224..a5a3977e0bfa83 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -355,7 +355,7 @@ jobs: run: | scripts/run_in_build_env.sh 'scripts/examples/gn_build_example.sh examples/chip-tool out/' scripts/run_in_build_env.sh 'pip3 install -r src/setup_payload/python/requirements.txt' - scripts/tests/run_python_setup_payload_generator_test.sh out/chip-tool + scripts/run_in_build_env.sh 'python3 src/setup_payload/tests/run_python_setup_payload_gen_test.py out/chip-tool' build_darwin: name: Build on Darwin (clang, python_lib, simulated) From 46053f87ab461fb972a47d1853470edb52f27f94 Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Thu, 12 Jan 2023 20:29:08 +0530 Subject: [PATCH 7/7] Not using random dataset but the predefined one --- .../run_python_setup_payload_gen_test.py | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/src/setup_payload/tests/run_python_setup_payload_gen_test.py b/src/setup_payload/tests/run_python_setup_payload_gen_test.py index 68903460a30e3f..ae8b3a14e53ca3 100644 --- a/src/setup_payload/tests/run_python_setup_payload_gen_test.py +++ b/src/setup_payload/tests/run_python_setup_payload_gen_test.py @@ -63,24 +63,6 @@ def parse_setup_payload(chip_tool, payload): return parsed_params -def generate_payload_params(): - payload_params = payload_param_dict() - payload_params['Version'] = 0 - payload_params['VendorID'] = random.randint(0, 0xFFFF) - payload_params['ProductID'] = random.randint(0, 0xFFFF) - payload_params['Custom flow'] = random.randint(0, 2) - payload_params['Discovery Bitmask'] = random.randint(0, 7) - payload_params['Long discriminator'] = random.randint(0, 0x0FFF) - payload_params['Short discriminator'] = (payload_params['Long discriminator'] >> 8) - payload_params['Passcode'] = random.randint(1, 0x5F5E0FE) - - # Rather than regenerating, just subtracting 5 - if payload_params['Passcode'] in INVALID_PASSCODES: - payload_params['Passcode'] -= 5 - - return payload_params - - def generate_payloads(in_params): payloads = SetupPayload(in_params['Long discriminator'], in_params['Passcode'], in_params['Discovery Bitmask'], CommissioningFlow(in_params['Custom flow']), @@ -90,18 +72,7 @@ def generate_payloads(in_params): return manualcode, qrcode -def verify_payloads(chip_tool): - in_params = generate_payload_params() - manualcode, qrcode = generate_payloads(in_params) - manualcode_params = parse_setup_payload(chip_tool, manualcode) - qrcode_params = parse_setup_payload(chip_tool, qrcode) - - print("Input parameters:", in_params) - print("Manualcode:", manualcode) - print("QRCode:", qrcode) - print("Manualcode parsed by chip-tool:", manualcode_params) - print("QRCode parsed by chip-tool:", qrcode_params) - +def verify_payloads(in_params, manualcode_params, qrcode_params): assert in_params['Version'] == int(manualcode_params['Version'], 0) assert in_params['Passcode'] == int(manualcode_params['Passcode'], 0) assert in_params['Short discriminator'] == int(manualcode_params['Short discriminator'], 0) @@ -118,11 +89,48 @@ def verify_payloads(chip_tool): assert in_params['Long discriminator'] == int(qrcode_params['Long discriminator'], 0) +def get_payload_params(discriminator, passcode, discovery=4, flow=0, vid=0, pid=0, version=0): + p = payload_param_dict() + p['Version'] = version + p['VendorID'] = vid + p['ProductID'] = pid + p['Custom flow'] = flow + p['Discovery Bitmask'] = discovery + p['Long discriminator'] = discriminator + p['Short discriminator'] = discriminator >> 8 + p['Passcode'] = passcode + return p + + +def run_tests(chip_tool): + test_data_set = [ + get_payload_params(3840, 20202021), + get_payload_params(3781, 12349876, flow=1, vid=1, pid=1), + get_payload_params(2310, 23005908, flow=2, vid=0xFFF3, pid=0x8098), + get_payload_params(3091, 43338551, discovery=2, flow=2, vid=0x1123, pid=0x0012), + get_payload_params(80, 54757432, discovery=6, flow=2, vid=0x2345, pid=0x1023), + get_payload_params(174, 81235604, discovery=7, flow=1, vid=0x45, pid=0x10), + ] + + for test_params in test_data_set: + manualcode, qrcode = generate_payloads(test_params) + manualcode_params = parse_setup_payload(chip_tool, manualcode) + qrcode_params = parse_setup_payload(chip_tool, qrcode) + + print("Input parameters:", test_params) + print("Manualcode:", manualcode) + print("QRCode:", qrcode) + print("Manualcode parsed by chip-tool:", manualcode_params) + print("QRCode parsed by chip-tool:", qrcode_params) + print("") + + verify_payloads(test_params, manualcode_params, qrcode_params) + + def main(): if len(sys.argv) == 2: chip_tool = sys.argv[1] - for i in range(0, 10): - verify_payloads(chip_tool) + run_tests(chip_tool) if __name__ == '__main__':