| #!/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 BitsInteger, BitStruct, 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() |