blob: 82a58d957eff080a96353d44c1a6fb99178fcb89 [file] [log] [blame]
#!/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()