| #!/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 bitarray.util import ba2int |
| 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() |