From d35b666a7b79de39176fae254e52a7b855caf82d Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Sat, 9 Mar 2024 13:12:42 +0530 Subject: [PATCH 1/5] setup_payload: Add support for parsing setup payloads in python impl - Base38 decode impl in python - use construct to generate/parse setup payload in python - Add cli to parse and generate using click - unit tests for parsing and verification using chip-tool - removed the older script which only generated the codes - replaced the usage of older utility with newer one --- .github/workflows/build.yaml | 2 +- scripts/tools/bouffalolab/factory_qrcode.py | 4 +- .../tools/generate_esp32_chip_factory_bin.py | 2 +- .../generate_nrfconnect_chip_factory_data.py | 4 +- src/setup_payload/python/Base38.py | 23 ++ src/setup_payload/python/README.md | 17 +- src/setup_payload/python/SetupPayload.py | 220 ++++++++++++++++++ .../python/generate_setup_payload.py | 170 -------------- src/setup_payload/python/requirements.txt | 4 +- ...st.py => run_python_setup_payload_test.py} | 75 ++++-- 10 files changed, 323 insertions(+), 198 deletions(-) create mode 100755 src/setup_payload/python/SetupPayload.py delete mode 100755 src/setup_payload/python/generate_setup_payload.py rename src/setup_payload/tests/{run_python_setup_payload_gen_test.py => run_python_setup_payload_test.py} (62%) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e509aab995c682..bf0acb4d17e685 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -331,7 +331,7 @@ jobs: scripts/run_in_build_env.sh 'virtualenv pyenv' source pyenv/bin/activate pip3 install -r src/setup_payload/python/requirements.txt - python3 src/setup_payload/tests/run_python_setup_payload_gen_test.py out/chip-tool + python3 src/setup_payload/tests/run_python_setup_payload_test.py out/chip-tool build_linux_python_lighting_device: name: Build on Linux (python lighting-app) diff --git a/scripts/tools/bouffalolab/factory_qrcode.py b/scripts/tools/bouffalolab/factory_qrcode.py index 1a7f6303495775..0e12d8349d4c91 100644 --- a/scripts/tools/bouffalolab/factory_qrcode.py +++ b/scripts/tools/bouffalolab/factory_qrcode.py @@ -20,13 +20,13 @@ try: import qrcode - from generate_setup_payload import CommissioningFlow, SetupPayload + from SetupPayload import CommissioningFlow, SetupPayload except ImportError: SDK_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) sys.path.append(os.path.join(SDK_ROOT, "src/setup_payload/python")) try: import qrcode - from generate_setup_payload import CommissioningFlow, SetupPayload + from SetupPayload import CommissioningFlow, SetupPayload except ModuleNotFoundError or ImportError: no_onboarding_modules = True else: diff --git a/scripts/tools/generate_esp32_chip_factory_bin.py b/scripts/tools/generate_esp32_chip_factory_bin.py index a4ccce52c0ab77..e21ea140ceab19 100755 --- a/scripts/tools/generate_esp32_chip_factory_bin.py +++ b/scripts/tools/generate_esp32_chip_factory_bin.py @@ -33,7 +33,7 @@ sys.path.insert(0, os.path.join(CHIP_TOPDIR, 'scripts', 'tools', 'spake2p')) from spake2p import generate_verifier # noqa: E402 isort:skip sys.path.insert(0, os.path.join(CHIP_TOPDIR, 'src', 'setup_payload', 'python')) -from generate_setup_payload import CommissioningFlow, SetupPayload # noqa: E402 isort:skip +from SetupPayload import CommissioningFlow, SetupPayload # noqa: E402 isort:skip if os.getenv('IDF_PATH'): sys.path.insert(0, os.path.join(os.getenv('IDF_PATH'), diff --git a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py index 7cddb766a90cca..1d0a771e1c53da 100644 --- a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py +++ b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py @@ -31,13 +31,13 @@ try: import qrcode - from generate_setup_payload import CommissioningFlow, SetupPayload + from SetupPayload import CommissioningFlow, SetupPayload except ImportError: SDK_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) sys.path.append(os.path.join(SDK_ROOT, "src/setup_payload/python")) try: import qrcode - from generate_setup_payload import CommissioningFlow, SetupPayload + from SetupPayload import CommissioningFlow, SetupPayload except ModuleNotFoundError or ImportError: no_onboarding_modules = True else: diff --git a/src/setup_payload/python/Base38.py b/src/setup_payload/python/Base38.py index 23113f7ed25c8d..834ae9e1d520e7 100644 --- a/src/setup_payload/python/Base38.py +++ b/src/setup_payload/python/Base38.py @@ -24,6 +24,7 @@ RADIX = len(CODES) BASE38_CHARS_NEEDED_IN_CHUNK = [2, 4, 5] MAX_BYTES_IN_CHUNK = 3 +MAX_ENCODED_BYTES_IN_CHUNK = 5 def encode(bytes): @@ -47,3 +48,25 @@ def encode(bytes): base38_chars_needed -= 1 return qrcode + + +def decode(qrcode): + total_chars = len(qrcode) + decoded_bytes = bytearray() + + for i in range(0, total_chars, MAX_ENCODED_BYTES_IN_CHUNK): + if (i + MAX_ENCODED_BYTES_IN_CHUNK) > total_chars: + chars_in_chunk = total_chars - i + else: + chars_in_chunk = MAX_ENCODED_BYTES_IN_CHUNK + + value = 0 + for j in range(i + chars_in_chunk - 1, i - 1, -1): + value = value * RADIX + CODES.index(qrcode[j]) + + bytes_in_chunk = BASE38_CHARS_NEEDED_IN_CHUNK.index(chars_in_chunk) + 1 + for k in range(0, bytes_in_chunk): + decoded_bytes.append(value & 0xFF) + value = value >> 8 + + return decoded_bytes diff --git a/src/setup_payload/python/README.md b/src/setup_payload/python/README.md index 068bf553fb7e6b..a39496104300d1 100644 --- a/src/setup_payload/python/README.md +++ b/src/setup_payload/python/README.md @@ -1,19 +1,22 @@ -## Python tool to generate Matter onboarding codes +## Python tool to generate and parse Matter onboarding codes -Generates Manual Pairing Code and QR Code +Generates and parses Manual Pairing Code and QR Code #### example usage: +- Parse + ``` -./generate_setup_payload.py -h -./generate_setup_payload.py -d 3840 -p 20202021 -cf 0 -dm 2 -vid 65521 -pid 32768 +./SetupPayload.py parse MT:U9VJ0OMV172PX813210 +./SetupPayload.py parse 34970112332 ``` -- Output +- Generate ``` -Manualcode : 34970112332 -QRCode : MT:Y.K9042C00KA0648G00 +./SetupPayload.py generate --help +./SetupPayload.py generate -d 3840 -p 20202021 +./SetupPayload.py generate -d 3840 -p 20202021 --vendor-id 65521 --product-id 32768 -cf 0 -dm 2 ``` For more details please refer Matter Specification diff --git a/src/setup_payload/python/SetupPayload.py b/src/setup_payload/python/SetupPayload.py new file mode 100755 index 00000000000000..88e39601fe0c4d --- /dev/null +++ b/src/setup_payload/python/SetupPayload.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 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 enum + +import Base38 +import click +from bitarray import bitarray +from bitarray.util import int2ba, zeros +from construct import BitStruct, BitsInteger, Enum +from stdnum.verhoeff import calc_check_digit + +# Format for constructing manualcode +manualcode_format = BitStruct( + 'version' / BitsInteger(1), + 'vid_pid_present' / BitsInteger(1), + 'discriminator' / BitsInteger(4), + 'pincode_lsb' / BitsInteger(14), + 'pincode_msb' / BitsInteger(13), + 'vid' / BitsInteger(16), + 'pid' / BitsInteger(16), + 'padding' / BitsInteger(7), # this is intentional as BitStruct only takes 8-bit aligned data +) + +# Format for constructing qrcode +# qrcode bytes are packed as lsb....msb, hence the order is reversed +qrcode_format = BitStruct( + 'padding' / BitsInteger(4), + 'pincode' / BitsInteger(27), + 'discriminator' / BitsInteger(12), + 'discovery' / BitsInteger(8), + 'flow' / Enum(BitsInteger(2), + Standard=0, UserIntent=1, Custom=2), + 'pid' / BitsInteger(16), + 'vid' / BitsInteger(16), + 'version' / BitsInteger(3), +) + + +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.long_discriminator = discriminator + self.short_discriminator = discriminator >> 8 + self.pincode = pincode + self.discovery = rendezvous + self.flow = flow + self.vid = vid + self.pid = pid + + def p_print(self): + print('{:<{}} :{}'.format('Flow', 24, self.flow)) + print('{:<{}} :{}'.format('Pincode', 24, self.pincode)) + print('{:<{}} :{}'.format('Short Discriminator', 24, self.short_discriminator)) + if self.long_discriminator: + print('{:<{}} :{}'.format('Long Discriminator', 24, self.long_discriminator)) + if self.discovery: + print('{:<{}} :{}'.format('Discovery Capabilities', 24, self.discovery)) + if self.vid is not None and self.pid is not None: + print('{:<{}} :{:<{}} (0x{:04x})'.format('Vendor Id', 24, self.vid, 6, self.vid)) + print('{:<{}} :{:<{}} (0x{:04x})'.format('Product Id', 24, self.pid, 6, self.pid)) + + def qrcode_dict(self): + return { + 'version': 0, + 'vid': self.vid, + 'pid': self.pid, + 'flow': int(self.flow), + 'discovery': self.discovery, + 'discriminator': self.long_discriminator, + 'pincode': self.pincode, + 'padding': 0, + } + + def manualcode_dict(self): + return { + 'version': 0, + 'vid_pid_present': 0 if self.flow == CommissioningFlow.Standard else 1, + 'discriminator': self.short_discriminator, + 'pincode_lsb': self.pincode & 0x3FFF, # 14 ls-bits + 'pincode_msb': self.pincode >> 14, # 13 ms-bits + 'vid': 0 if self.flow == CommissioningFlow.Standard else self.vid, + 'pid': 0 if self.flow == CommissioningFlow.Standard else self.pid, + 'padding': 0, + } + + def generate_qrcode(self): + data = qrcode_format.build(self.qrcode_dict()) + b38_encoded = Base38.encode(data[::-1]) # reversing + return 'MT:{}'.format(b38_encoded) + + def generate_manualcode(self): + CHUNK1_START = 0 + CHUNK1_LEN = 4 + CHUNK2_START = CHUNK1_START + CHUNK1_LEN + CHUNK2_LEN = 16 + CHUNK3_START = CHUNK2_START + CHUNK2_LEN + CHUNK3_LEN = 13 + + bytes = manualcode_format.build(self.manualcode_dict()) + bits = bitarray() + bits.frombytes(bytes) + + chunk1 = str(int(bits[CHUNK1_START:CHUNK1_START + CHUNK1_LEN].to01(), 2)).zfill(1) + chunk2 = str(int(bits[CHUNK2_START:CHUNK2_START + CHUNK2_LEN].to01(), 2)).zfill(5) + chunk3 = str(int(bits[CHUNK3_START:CHUNK3_START + CHUNK3_LEN].to01(), 2)).zfill(4) + chunk4 = str(self.vid).zfill(5) if self.flow != CommissioningFlow.Standard else '' + chunk5 = str(self.pid).zfill(5) if self.flow != CommissioningFlow.Standard else '' + payload = '{}{}{}{}{}'.format(chunk1, chunk2, chunk3, chunk4, chunk5) + return '{}{}'.format(payload, calc_check_digit(payload)) + + @staticmethod + def from_container(container, is_qrcode): + payload = None + if is_qrcode: + payload = SetupPayload(container['discriminator'], container['pincode'], + container['discovery'], CommissioningFlow(container['flow'].__int__()), + container['vid'], container['pid']) + else: + payload = SetupPayload(discriminator=container['discriminator'], + pincode=(container['pincode_msb'] << 14) | container['pincode_lsb'], + vid=container['vid'] if container['vid_pid_present'] else None, + pid=container['pid'] if container['vid_pid_present'] else None) + payload.short_discriminator = container['discriminator'] + payload.long_discriminator = None + payload.discovery = None + payload.flow = 2 if container['vid_pid_present'] else 0 + + return payload + + @staticmethod + def parse_qrcode(payload): + payload = payload[3:] # remove 'MT:' + b38_decoded = Base38.decode(payload)[::-1] + container = qrcode_format.parse(b38_decoded) + return SetupPayload.from_container(container, is_qrcode=True) + + @staticmethod + def parse_manualcode(payload): + payload_len = len(payload) + if payload_len != 11 and payload_len != 21: + print('Invalid length') + return None + + # if first digit is greater than 7 the its not v1 + if int(str(payload)[0]) > 7: + print('incorrect first digit') + return None + + if calc_check_digit(payload[:-1]) != str(payload)[-1]: + print('check digit mismatch') + return None + + # vid_pid_present bit position + is_long = int(str(payload)[0]) & (1 << 2) + + bits = int2ba(int(payload[0]), length=4) + bits += int2ba(int(payload[1:6]), length=16) + bits += int2ba(int(payload[6:10]), length=13) + bits += int2ba(int(payload[10:15]), length=16) if is_long else zeros(16) + bits += int2ba(int(payload[15:20]), length=16) if is_long else zeros(16) + bits += zeros(7) # padding + + container = manualcode_format.parse(bits.tobytes()) + return SetupPayload.from_container(container, is_qrcode=False) + + @staticmethod + def parse(payload): + if payload.startswith('MT:'): + return SetupPayload.parse_qrcode(payload) + else: + return SetupPayload.parse_manualcode(payload) + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.argument('payload') +def parse(payload): + click.echo(f'Parsing payload: {payload}') + SetupPayload.parse(payload).p_print() + + +@cli.command() +@click.option('--discriminator', '-d', required=True, type=click.IntRange(0, 0xFFF), help='Discriminator') +@click.option('--passcode', '-p', required=True, type=click.IntRange(1, 0x5F5E0FE), help='setup pincode') +@click.option('--vendor-id', '-vid', type=click.IntRange(0, 0xFFFF), default=0, help='Vendor ID') +@click.option('--product-id', '-pid', type=click.IntRange(0, 0xFFFF), default=0, help='Product ID') +@click.option('--discovery-cap-bitmask', '-dm', type=click.IntRange(0, 7), default=4, help='Commissionable device discovery capability bitmask. 0:SoftAP, 1:BLE, 2:OnNetwork. Default: OnNetwork') +@click.option('--commissioning-flow', '-cf', type=click.IntRange(0, 2), default=0, help='Commissioning flow, 0:Standard, 1:User-Intent, 2:Custom') +def generate(passcode, discriminator, vendor_id, product_id, discovery_cap_bitmask, commissioning_flow): + payload = SetupPayload(discriminator, passcode, discovery_cap_bitmask, commissioning_flow, vendor_id, product_id) + print("Manualcode : {}".format(payload.generate_manualcode())) + print("QRCode : {}".format(payload.generate_qrcode())) + + +if __name__ == '__main__': + cli() diff --git a/src/setup_payload/python/generate_setup_payload.py b/src/setup_payload/python/generate_setup_payload.py deleted file mode 100755 index 28a834651214b1..00000000000000 --- a/src/setup_payload/python/generate_setup_payload.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/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 argparse -import enum -import sys - -import Base38 -from bitarray import bitarray -from stdnum.verhoeff import calc_check_digit - -# See section 5.1.4.1 Manual Pairing Code in the Matter specification v1.0 -MANUAL_DISCRIMINATOR_LEN = 4 -PINCODE_LEN = 27 - -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 - -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 - -MANUAL_CHUNK3_PINCODE_MSBITS_LEN = 13 -MANUAL_CHUNK3_PINCODE_MSBITS_POS = 0 -MANUAL_CHUNK3_LEN = 4 - -MANUAL_VID_LEN = 5 -MANUAL_PID_LEN = 5 - -# See section 5.1.3. QR Code in the Matter specification v1.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] - - -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.long_discriminator = discriminator - self.short_discriminator = discriminator >> 8 - self.pincode = pincode - self.rendezvous = rendezvous - self.flow = flow - self.vid = vid - self.pid = pid - - 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(MANUAL_VID_LEN) - payload += str(self.pid).zfill(MANUAL_PID_LEN) - - payload += calc_check_digit(payload) - return payload - - 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)) - - -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)) - 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() - 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.generate_manualcode() - qrcode = payloads.generate_qrcode() - - print("Manualcode : {}".format(manualcode)) - print("QRCode : {}".format(qrcode)) - - -if __name__ == '__main__': - main() diff --git a/src/setup_payload/python/requirements.txt b/src/setup_payload/python/requirements.txt index 43800d601c7736..e5c1cddf6dcf3d 100644 --- a/src/setup_payload/python/requirements.txt +++ b/src/setup_payload/python/requirements.txt @@ -1,2 +1,4 @@ -bitarray==2.6.0 +bitarray==2.8.1 +click==8.1.3 +construct==2.10.68 python_stdnum==1.18 diff --git a/src/setup_payload/tests/run_python_setup_payload_gen_test.py b/src/setup_payload/tests/run_python_setup_payload_test.py similarity index 62% rename from src/setup_payload/tests/run_python_setup_payload_gen_test.py rename to src/setup_payload/tests/run_python_setup_payload_test.py index b52000c1a6fd98..5097d61cea730a 100644 --- a/src/setup_payload/tests/run_python_setup_payload_gen_test.py +++ b/src/setup_payload/tests/run_python_setup_payload_test.py @@ -21,7 +21,7 @@ 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 CommissioningFlow, SetupPayload # noqa: E402 +from SetupPayload import CommissioningFlow, SetupPayload # noqa: E402 def payload_param_dict(): @@ -43,7 +43,7 @@ def remove_escape_sequence(data): return result -def parse_setup_payload(chip_tool, payload): +def chip_tool_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) @@ -62,6 +62,27 @@ def parse_setup_payload(chip_tool, payload): return parsed_params +def chip_tool_generate_code(chip_tool, payload_data, cmd, key, is_qrcode): + cmd_args = [chip_tool, 'payload', cmd] + cmd_args += ['--setup-pin-code', str(payload_data.pincode)] + cmd_args += ['--vendor-id', str(payload_data.vid)] if payload_data.vid else [] + cmd_args += ['--product-id', str(payload_data.pid)] if payload_data.pid else [] + cmd_args += ['--commissioning-mode', str(int(payload_data.flow))] + + if is_qrcode: + cmd_args += ['--discriminator', str(payload_data.long_discriminator)] + cmd_args += ['--rendezvous', str(payload_data.discovery)] + else: + # generate-manualcode always takes in the long discriminator, but the python parsed data only has the short one + cmd_args += ['--discriminator', str(payload_data.short_discriminator << 8)] + + data = subprocess.check_output(cmd_args).decode('utf-8') + data = remove_escape_sequence(data) + st = data.find(key) + len(key) + end = data.find('\n', st) + return data[st:end].strip() + + def generate_payloads(in_params): payloads = SetupPayload(in_params['Long discriminator'], in_params['Passcode'], in_params['Discovery Bitmask'], CommissioningFlow(in_params['Custom flow']), @@ -71,7 +92,7 @@ def generate_payloads(in_params): return manualcode, qrcode -def verify_payloads(in_params, manualcode_params, qrcode_params): +def verify_generated_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) @@ -101,7 +122,7 @@ def get_payload_params(discriminator, passcode, discovery=4, flow=0, vid=0, pid= return p -def run_tests(chip_tool): +def test_code_generation(chip_tool): test_data_set = [ get_payload_params(3840, 20202021), get_payload_params(3781, 12349876, flow=1, vid=1, pid=1), @@ -113,23 +134,49 @@ def run_tests(chip_tool): 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) + manualcode_params = chip_tool_parse_setup_payload(chip_tool, manualcode) + qrcode_params = chip_tool_parse_setup_payload(chip_tool, qrcode) + + verify_generated_payloads(test_params, manualcode_params, qrcode_params) - 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 test_manualcode_parsing(chip_tool): + test_data_set = [ + '34970112332', + '745492075300001000013', + '619156140465523329207', + '702871264504387000187', + '402104334209029041311', + '403732495800069000166', + ] + for test_payload in test_data_set: + payload = SetupPayload.parse(test_payload) + code = chip_tool_generate_code(chip_tool, payload, 'generate-manualcode', 'Manual Code:', is_qrcode=False) + assert test_payload == code + + +def test_qrcode_parsing(chip_tool): + test_data_set = [ + 'MT:U9VJ0OMV172PX813210', + 'MT:00000CQM00KA0648G00', + 'MT:A3L90ARR15G6N57Y900', + 'MT:MZWA6G6026O2XP0II00', + 'MT:KSNK4M5113-JPR4UY00', + 'MT:0A.T0P--00Y0OJ0.510', + 'MT:EPX0482F26DAVY09R10', + ] + for test_payload in test_data_set: + payload = SetupPayload.parse(test_payload) + code = chip_tool_generate_code(chip_tool, payload, 'generate-qrcode', 'QR Code:', is_qrcode=True) + assert test_payload == code def main(): if len(sys.argv) == 2: chip_tool = sys.argv[1] - run_tests(chip_tool) + test_code_generation(chip_tool) + test_manualcode_parsing(chip_tool) + test_qrcode_parsing(chip_tool) if __name__ == '__main__': From 4198a90f1f0595145a34d457fc587c337834a80c Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Sat, 9 Mar 2024 15:55:12 +0000 Subject: [PATCH 2/5] Restyled by isort --- src/setup_payload/python/SetupPayload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup_payload/python/SetupPayload.py b/src/setup_payload/python/SetupPayload.py index 88e39601fe0c4d..82a58d957eff08 100755 --- a/src/setup_payload/python/SetupPayload.py +++ b/src/setup_payload/python/SetupPayload.py @@ -21,7 +21,7 @@ import click from bitarray import bitarray from bitarray.util import int2ba, zeros -from construct import BitStruct, BitsInteger, Enum +from construct import BitsInteger, BitStruct, Enum from stdnum.verhoeff import calc_check_digit # Format for constructing manualcode From 8820fafa57c9dc667e87d17ca444938a943c9dc8 Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Tue, 2 Apr 2024 17:10:30 +0530 Subject: [PATCH 3/5] fix the requirements --- src/setup_payload/python/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/setup_payload/python/requirements.txt b/src/setup_payload/python/requirements.txt index e5c1cddf6dcf3d..112192e69051be 100644 --- a/src/setup_payload/python/requirements.txt +++ b/src/setup_payload/python/requirements.txt @@ -1,4 +1,4 @@ bitarray==2.8.1 -click==8.1.3 -construct==2.10.68 +click>=8.1.3 +construct>=2.10.68 python_stdnum==1.18 From cf31fdc4c0b216df82db2516073e51ddd67e9edd Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Mon, 20 May 2024 16:24:01 +0530 Subject: [PATCH 4/5] Added some test dataset --- .../tests/run_python_setup_payload_test.py | 130 +++++++++++------- 1 file changed, 80 insertions(+), 50 deletions(-) diff --git a/src/setup_payload/tests/run_python_setup_payload_test.py b/src/setup_payload/tests/run_python_setup_payload_test.py index 5097d61cea730a..da62a45c05fe97 100644 --- a/src/setup_payload/tests/run_python_setup_payload_test.py +++ b/src/setup_payload/tests/run_python_setup_payload_test.py @@ -62,27 +62,6 @@ def chip_tool_parse_setup_payload(chip_tool, payload): return parsed_params -def chip_tool_generate_code(chip_tool, payload_data, cmd, key, is_qrcode): - cmd_args = [chip_tool, 'payload', cmd] - cmd_args += ['--setup-pin-code', str(payload_data.pincode)] - cmd_args += ['--vendor-id', str(payload_data.vid)] if payload_data.vid else [] - cmd_args += ['--product-id', str(payload_data.pid)] if payload_data.pid else [] - cmd_args += ['--commissioning-mode', str(int(payload_data.flow))] - - if is_qrcode: - cmd_args += ['--discriminator', str(payload_data.long_discriminator)] - cmd_args += ['--rendezvous', str(payload_data.discovery)] - else: - # generate-manualcode always takes in the long discriminator, but the python parsed data only has the short one - cmd_args += ['--discriminator', str(payload_data.short_discriminator << 8)] - - data = subprocess.check_output(cmd_args).decode('utf-8') - data = remove_escape_sequence(data) - st = data.find(key) + len(key) - end = data.find('\n', st) - return data[st:end].strip() - - def generate_payloads(in_params): payloads = SetupPayload(in_params['Long discriminator'], in_params['Passcode'], in_params['Discovery Bitmask'], CommissioningFlow(in_params['Custom flow']), @@ -109,7 +88,7 @@ def verify_generated_payloads(in_params, manualcode_params, qrcode_params): 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): +def get_payload_params(discriminator, passcode, discovery=4, flow=0, vid=0, pid=0, version=0, short_discriminator=None): p = payload_param_dict() p['Version'] = version p['VendorID'] = vid @@ -117,7 +96,7 @@ def get_payload_params(discriminator, passcode, discovery=4, flow=0, vid=0, pid= p['Custom flow'] = flow p['Discovery Bitmask'] = discovery p['Long discriminator'] = discriminator - p['Short discriminator'] = discriminator >> 8 + p['Short discriminator'] = short_discriminator if short_discriminator is not None else (discriminator >> 8) p['Passcode'] = passcode return p @@ -140,43 +119,94 @@ def test_code_generation(chip_tool): verify_generated_payloads(test_params, manualcode_params, qrcode_params) -def test_manualcode_parsing(chip_tool): - test_data_set = [ - '34970112332', - '745492075300001000013', - '619156140465523329207', - '702871264504387000187', - '402104334209029041311', - '403732495800069000166', - ] - for test_payload in test_data_set: - payload = SetupPayload.parse(test_payload) - code = chip_tool_generate_code(chip_tool, payload, 'generate-manualcode', 'Manual Code:', is_qrcode=False) - assert test_payload == code - +def test_onboardingcode_parsing(): + # This test dataset is generated using `chip-tool payload parse-setup-payload ` -def test_qrcode_parsing(chip_tool): test_data_set = [ - 'MT:U9VJ0OMV172PX813210', - 'MT:00000CQM00KA0648G00', - 'MT:A3L90ARR15G6N57Y900', - 'MT:MZWA6G6026O2XP0II00', - 'MT:KSNK4M5113-JPR4UY00', - 'MT:0A.T0P--00Y0OJ0.510', - 'MT:EPX0482F26DAVY09R10', + { + 'code': '34970112332', + 'res': get_payload_params(discriminator=None, passcode=20202021, discovery=None, + flow=0, vid=None, pid=None, version=0, short_discriminator=15), + }, + { + 'code': '745492075300001000013', + 'res': get_payload_params(discriminator=None, passcode=12349876, discovery=None, + flow=2, vid=1, pid=1, version=0, short_discriminator=14), + }, + { + 'code': '619156140465523329207', + 'res': get_payload_params(discriminator=None, passcode=23005908, discovery=None, + flow=2, vid=65523, pid=32920, version=0, short_discriminator=9), + }, + { + 'code': '702871264504387000187', + 'res': get_payload_params(discriminator=None, passcode=43338551, discovery=None, + flow=2, vid=4387, pid=18, version=0, short_discriminator=12), + }, + { + 'code': '402104334209029041311', + 'res': get_payload_params(discriminator=None, passcode=54757432, discovery=None, + flow=2, vid=9029, pid=4131, version=0, short_discriminator=0), + }, + { + 'code': '403732495800069000166', + 'res': get_payload_params(discriminator=None, passcode=81235604, discovery=None, + flow=2, vid=69, pid=16, version=0, short_discriminator=0), + }, + { + 'code': 'MT:U9VJ0OMV172PX813210', + 'res': get_payload_params(discriminator=3431, passcode=49910688, discovery=2, + flow=0, vid=4891, pid=2, version=0, short_discriminator=None), + }, + { + 'code': 'MT:00000CQM00KA0648G00', + 'res': get_payload_params(discriminator=3840, passcode=20202021, discovery=4, + flow=0, vid=0, pid=0, version=0, short_discriminator=None), + }, + { + 'code': 'MT:A3L90ARR15G6N57Y900', + 'res': get_payload_params(discriminator=3781, passcode=12349876, discovery=4, + flow=1, vid=1, pid=1, version=0, short_discriminator=None), + }, + { + 'code': 'MT:MZWA6G6026O2XP0II00', + 'res': get_payload_params(discriminator=2310, passcode=23005908, discovery=4, + flow=2, vid=65523, pid=32920, version=0, short_discriminator=None), + }, + { + 'code': 'MT:KSNK4M5113-JPR4UY00', + 'res': get_payload_params(discriminator=3091, passcode=43338551, discovery=2, + flow=2, vid=4387, pid=18, version=0, short_discriminator=12), + }, + { + 'code': 'MT:0A.T0P--00Y0OJ0.510', + 'res': get_payload_params(discriminator=80, passcode=54757432, discovery=6, + flow=2, vid=9029, pid=4131, version=0, short_discriminator=None), + }, + { + 'code': 'MT:EPX0482F26DAVY09R10', + 'res': get_payload_params(discriminator=174, passcode=81235604, discovery=7, + flow=1, vid=69, pid=16, version=0, short_discriminator=None), + }, ] + for test_payload in test_data_set: - payload = SetupPayload.parse(test_payload) - code = chip_tool_generate_code(chip_tool, payload, 'generate-qrcode', 'QR Code:', is_qrcode=True) - assert test_payload == code + payload = SetupPayload.parse(test_payload['code']) + + assert payload.long_discriminator == test_payload['res']['Long discriminator'] + assert payload.short_discriminator == test_payload['res']['Short discriminator'] + assert payload.pincode == test_payload['res']['Passcode'] + assert payload.discovery == test_payload['res']['Discovery Bitmask'] + assert payload.flow == test_payload['res']['Custom flow'] + assert payload.vid == test_payload['res']['VendorID'] + assert payload.pid == test_payload['res']['ProductID'] def main(): if len(sys.argv) == 2: chip_tool = sys.argv[1] test_code_generation(chip_tool) - test_manualcode_parsing(chip_tool) - test_qrcode_parsing(chip_tool) + test_onboardingcode_parsing() if __name__ == '__main__': From 3ef07288c6ea724856dfd842050239586cb7c023 Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Wed, 22 May 2024 18:28:27 +0530 Subject: [PATCH 5/5] always use latest bitarray --- src/setup_payload/python/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup_payload/python/requirements.txt b/src/setup_payload/python/requirements.txt index 112192e69051be..1026fa98d061c5 100644 --- a/src/setup_payload/python/requirements.txt +++ b/src/setup_payload/python/requirements.txt @@ -1,4 +1,4 @@ -bitarray==2.8.1 +bitarray>=2.8.1 click>=8.1.3 construct>=2.10.68 python_stdnum==1.18