mirror of
https://github.com/espressif/openthread.git
synced 2026-07-29 15:17:47 +00:00
[tools] add tcat ble client (#9739)
Adds TCAT client implementation for BLE transport. Signed-off-by: Piotr Jasinski <[email protected]> Co-authored-by: Przemyslaw Bida <[email protected]>
This commit is contained in:
co-authored by
Przemyslaw Bida
parent
869c2ded9c
commit
905a22e0c7
@@ -0,0 +1,91 @@
|
||||
# BBTC X.509 certificates generation
|
||||
|
||||
---
|
||||
|
||||
TCAT uses X.509 Certificate Extensions to provide permissions with certificates.
|
||||
|
||||
## Extensions
|
||||
|
||||
Extensions were introduced in version 3 of the X.509 standard for certificates. They allow certificates to be customised to applications by supporting the addition of arbitrary fields in the certificate. Each extension, identified by its OID (Object Identifier), is marked as "Critical" or "Non-Critical", and includes the extension-specific data.
|
||||
|
||||
## Certificates generation
|
||||
|
||||
Thread uses Elliptic Curve Cryptography (ECC), so we use the `ecparam` `openssl` argument to generate the keys.
|
||||
|
||||
### Root certificate
|
||||
|
||||
1. Generate the private key:
|
||||
|
||||
```
|
||||
openssl ecparam -genkey -name prime256v1 -out ca_key.pem
|
||||
```
|
||||
|
||||
1. We can then generate the **.csr** (certificate signing request) file, which will contain all the parameters of our final certificate:
|
||||
|
||||
```
|
||||
openssl req -new -sha256 -key ca_key.pem -out ca.csr
|
||||
```
|
||||
|
||||
1. Finally, we can generate the certificate itself:
|
||||
|
||||
```
|
||||
openssl req -x509 -sha256 -days 365 -key ca_key.pem -in ca.csr -out ca_cert.pem
|
||||
```
|
||||
|
||||
1. See the generated certificate using
|
||||
|
||||
```
|
||||
openssl x509 -in ca_cert.pem -text -noout
|
||||
```
|
||||
|
||||
### Commissioner (client) certificate
|
||||
|
||||
1. Generate the key:
|
||||
|
||||
```
|
||||
openssl ecparam -genkey -name prime256v1 -out commissioner_key.pem
|
||||
```
|
||||
|
||||
1. Specify additional extensions when generating the .csr (see [sample configuration](#Configurations)):
|
||||
|
||||
```
|
||||
openssl req -new -sha256 -key commissioner_key.pem -out commissioner.csr -config commissioner.cnf
|
||||
```
|
||||
|
||||
1. Generate the certificate:
|
||||
|
||||
```
|
||||
openssl x509 -req -in commissioner.csr -CA ca_cert.pem -CAkey ca_key.pem -out commissioner_cert.pem -days 365 -sha256 -copy_extensions copy
|
||||
```
|
||||
|
||||
1. View the generated certificate using:
|
||||
|
||||
```
|
||||
openssl x509 -in commissioner_cert.pem -text -noout
|
||||
```
|
||||
|
||||
1. View parsed certificate extensions using:
|
||||
|
||||
```
|
||||
openssl asn1parse -inform PEM -in commissioner_cert.pem
|
||||
```
|
||||
|
||||
## Configurations
|
||||
|
||||
file: `commissioner.cnf` (line `1.3.6.1.4.1.44970.3 = DER:21:01:01:01:01` specifies permissions (all))
|
||||
|
||||
```
|
||||
[ req ]
|
||||
default_bits = 2048
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
req_extensions = v3_req
|
||||
|
||||
[ req_distinguished_name ]
|
||||
CN = Commissioner
|
||||
|
||||
[v3_req]
|
||||
1.3.6.1.4.1.44970.3 = DER:21:01:01:01:01
|
||||
authorityKeyIdentifier = none
|
||||
subjectKeyIdentifier = none
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
# BBTC Client
|
||||
|
||||
## Overview
|
||||
|
||||
This is a Python implementation of Bluetooth-Based Thread Commissioning client, based on Thread's TCAT (Thread Commissioning over Authenticated TLS) functionality.
|
||||
|
||||
## Installation
|
||||
|
||||
If you don't have the poetry module installed (check with `poetry --version`), install it first using:
|
||||
|
||||
```bash
|
||||
python3 -m pip install poetry
|
||||
```
|
||||
|
||||
Thread uses Elliptic Curve Cryptography (ECC), so we use the `ecparam` `openssl` argument to generate the keys.
|
||||
|
||||
```
|
||||
poetry install
|
||||
```
|
||||
|
||||
This will install all the required modules to a virtual environment, which can be used by calling `poetry run <COMMAND>` from the project directory.
|
||||
|
||||
## Usage
|
||||
|
||||
In order to connect to a TCAT device, enter the project directory and run:
|
||||
|
||||
```bash
|
||||
poetry run python3 bbtc.py {<device specifier> | --scan}
|
||||
```
|
||||
|
||||
where `<device specifier>` can be:
|
||||
|
||||
- `--name <NAME>` - name advertised by the device
|
||||
- `--mac <ADDRESS>` - physical address of the device's Bluetooth interface
|
||||
|
||||
Using the `--scan` option will scan for every TCAT device and display them in a list, to allow selection of the target.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
poetry run python3 bbtc.py --name 'Thread BLE'
|
||||
```
|
||||
|
||||
The application will connect to the first matching device discovered and set up a secure TLS channel. The user is then presented with the CLI.
|
||||
|
||||
## Commands
|
||||
|
||||
The application supports the following interactive CLI commands:
|
||||
|
||||
- `help` - Display available commands.
|
||||
- `commission` - Commission the device with current dataset.
|
||||
- `thread start` - Enable Thread interface.
|
||||
- `thread stop` - Disable Thread interface.
|
||||
- `hello` - Send "hello world" application data and read the response.
|
||||
- `exit` - Close the connection and exit.
|
||||
- `dataset` - View and manipulate current dataset. See `dataset help` for more information.
|
||||
@@ -0,0 +1,13 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICCDCCAa2gAwIBAgIJAIKxygBXoH+5MAoGCCqGSM49BAMCMG8xCzAJBgNVBAYT
|
||||
AlhYMRAwDgYDVQQIEwdNeVN0YXRlMQ8wDQYDVQQHEwZNeUNpdHkxDzANBgNVBAsT
|
||||
Bk15VW5pdDERMA8GA1UEChMITXlWZW5kb3IxGTAXBgNVBAMTEHd3dy5teXZlbmRv
|
||||
ci5jb20wHhcNMjMxMDE2MTAzMzE1WhcNMjYxMDE2MTAzMzE1WjBvMQswCQYDVQQG
|
||||
EwJYWDEQMA4GA1UECBMHTXlTdGF0ZTEPMA0GA1UEBxMGTXlDaXR5MQ8wDQYDVQQL
|
||||
EwZNeVVuaXQxETAPBgNVBAoTCE15VmVuZG9yMRkwFwYDVQQDExB3d3cubXl2ZW5k
|
||||
b3IuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWdyzPAXGKeZY94OhHAWX
|
||||
HzJfQIjGSyaOzlgL9OEFw2SoUDncLKPGwfPAUSfuMyEkzszNDM0HHkBsDLqu4n25
|
||||
/6MyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU4EynoSw9eDKZEVPkums2
|
||||
IWLAJCowCgYIKoZIzj0EAwIDSQAwRgIhAMYGGL9xShyE6P9wEU+MAYF6W3CzdrwV
|
||||
kuerX1encIH2AiEA5rq490NUobM1Au43roxJq1T6Z43LscPVbGZfULD1Jq0=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEII/jxkoUczPvlYM3ayhneif63B64lan3SubrXiVEbhoVoAoGCCqGSM49
|
||||
AwEHoUQDQgAEWdyzPAXGKeZY94OhHAWXHzJfQIjGSyaOzlgL9OEFw2SoUDncLKPG
|
||||
wfPAUSfuMyEkzszNDM0HHkBsDLqu4n25/w==
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBnzCCAUSgAwIBAgIUQ5RUJMc95ssHQybR6pcx7LXSBzcwCgYIKoZIzj0EAwIw
|
||||
bzELMAkGA1UEBhMCWFgxEDAOBgNVBAgTB015U3RhdGUxDzANBgNVBAcTBk15Q2l0
|
||||
eTEPMA0GA1UECxMGTXlVbml0MREwDwYDVQQKEwhNeVZlbmRvcjEZMBcGA1UEAxMQ
|
||||
d3d3Lm15dmVuZG9yLmNvbTAeFw0yMzEwMTgxNTIyMTZaFw0zMzEwMTUxNTIyMTZa
|
||||
MBcxFTATBgNVBAMMDENvbW1pc3Npb25lcjBZMBMGByqGSM49AgEGCCqGSM49AwEH
|
||||
A0IABDU90Qpae5c+5Diou072S6MMHNv9+Ah9Kmo+mZTT6gQUyRScFey3+6wE08o7
|
||||
wl6/8EKRgS8TFSihK4mYGxYoN06jFjAUMBIGCSsGAQQBgt8qAwQFIQEBAQEwCgYI
|
||||
KoZIzj0EAwIDSQAwRgIhAOgQH8wFSe3JtGSmEFLy4fbMhOg+5Mfhqoq95vu2ML/u
|
||||
AiEAt4BjuFo7GTxQxXl1e8TvMGESPGBKnR7cIT/BCnn2fto=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN EC PARAMETERS-----
|
||||
BggqhkjOPQMBBw==
|
||||
-----END EC PARAMETERS-----
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIFOGszqvbs62fRwd3Rnd79wf6fpWxkXLO5YzhEuJ9EV1oAoGCCqGSM49
|
||||
AwEHoUQDQgAENT3RClp7lz7kOKi7TvZLowwc2/34CH0qaj6ZlNPqBBTJFJwV7Lf7
|
||||
rATTyjvCXr/wQpGBLxMVKKEriZgbFig3Tg==
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Copyright (c) 2024, The OpenThread Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import argparse
|
||||
from os import path
|
||||
import logging
|
||||
|
||||
from ble.ble_connection_constants import BBTC_SERVICE_UUID, BBTC_TX_CHAR_UUID, \
|
||||
BBTC_RX_CHAR_UUID, SERVER_COMMON_NAME
|
||||
from ble.ble_stream import BleStream
|
||||
from ble.ble_stream_secure import BleStreamSecure
|
||||
from ble import ble_scanner
|
||||
from cli.cli import CLI
|
||||
from dataset.dataset import ThreadDataset
|
||||
from cli.command import CommandResult
|
||||
from utils import select_device_by_user_input
|
||||
|
||||
|
||||
async def main():
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
|
||||
parser = argparse.ArgumentParser(description='Device parameters')
|
||||
parser.add_argument('--debug', help='Enable debug logs', action='store_true')
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--mac', type=str, help='Device MAC address', action='store')
|
||||
group.add_argument('--name', type=str, help='Device name', action='store')
|
||||
group.add_argument('--scan', help='Scan all available devices', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
logging.getLogger('ble_stream').setLevel(logging.DEBUG)
|
||||
logging.getLogger('ble_stream_secure').setLevel(logging.DEBUG)
|
||||
|
||||
device = await get_device_by_args(args)
|
||||
|
||||
ble_sstream = None
|
||||
|
||||
if not (device is None):
|
||||
print(f'Connecting to {device}')
|
||||
ble_stream = await BleStream.create(device.address, BBTC_SERVICE_UUID, BBTC_TX_CHAR_UUID, BBTC_RX_CHAR_UUID)
|
||||
ble_sstream = BleStreamSecure(ble_stream)
|
||||
ble_sstream.load_cert(
|
||||
certfile=path.join('auth', 'commissioner_cert.pem'),
|
||||
keyfile=path.join('auth', 'commissioner_key.pem'),
|
||||
cafile=path.join('auth', 'ca_cert.pem'),
|
||||
)
|
||||
|
||||
print('Setting up secure channel...')
|
||||
await ble_sstream.do_handshake(hostname=SERVER_COMMON_NAME)
|
||||
print('Done')
|
||||
|
||||
ds = ThreadDataset()
|
||||
cli = CLI(ds, ble_sstream)
|
||||
loop = asyncio.get_running_loop()
|
||||
print('Enter \'help\' to see available commands' ' or \'exit\' to exit the application.')
|
||||
while True:
|
||||
user_input = await loop.run_in_executor(None, lambda: input('> '))
|
||||
if user_input.lower() == 'exit':
|
||||
print('Disconnecting...')
|
||||
break
|
||||
try:
|
||||
result: CommandResult = await cli.evaluate_input(user_input)
|
||||
if result:
|
||||
result.pretty_print()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
async def get_device_by_args(args):
|
||||
device = None
|
||||
if args.mac:
|
||||
device = await ble_scanner.find_first_by_mac(args.mac)
|
||||
elif args.name:
|
||||
device = await ble_scanner.find_first_by_name(args.name)
|
||||
elif args.scan:
|
||||
tcat_devices = await ble_scanner.scan_tcat_devices()
|
||||
device = select_device_by_user_input(tcat_devices)
|
||||
|
||||
return device
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except asyncio.CancelledError:
|
||||
pass # device disconnected
|
||||
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Copyright (c) 2024, The OpenThread Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
BBTC_SERVICE_UUID = 'FFFB'
|
||||
BBTC_RX_CHAR_UUID = '6BD10D8B-85A7-4E5A-BA2D-C83558A5F220'
|
||||
BBTC_TX_CHAR_UUID = '7FDDF61F-280A-4773-B448-BA1B8FE0DD69'
|
||||
SERVER_COMMON_NAME = 'myvendor.com/tcat/mydev'
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Copyright (c) 2024, The OpenThread Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
from bleak import BleakScanner
|
||||
from bbtc import BBTC_SERVICE_UUID
|
||||
|
||||
|
||||
async def find_first_by_name(name):
|
||||
match_name = lambda dev, adv_data: name == dev.name
|
||||
device = await BleakScanner.find_device_by_filter(match_name)
|
||||
return device
|
||||
|
||||
|
||||
async def find_first_by_mac(mac):
|
||||
match_mac = lambda dev, adv_data: mac.upper() == dev.address
|
||||
device = await BleakScanner.find_device_by_filter(match_mac)
|
||||
return device
|
||||
|
||||
|
||||
async def scan_tcat_devices():
|
||||
scanner = BleakScanner()
|
||||
tcat_devices = []
|
||||
devices_dict = await scanner.discover(return_adv=True, service_uuids=[BBTC_SERVICE_UUID.lower()])
|
||||
for _, (device, _) in devices_dict.items():
|
||||
tcat_devices.append(device)
|
||||
|
||||
return tcat_devices
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Copyright (c) 2024, The OpenThread Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
from itertools import count, takewhile
|
||||
from typing import Iterator
|
||||
import logging
|
||||
import time
|
||||
from asyncio import sleep
|
||||
|
||||
from bleak import BleakClient
|
||||
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BleStream:
|
||||
|
||||
def __init__(self, client, service_uuid, tx_char_uuid, rx_char_uuid):
|
||||
self.__receive_buffer = b''
|
||||
self.__last_recv_time = None
|
||||
self.client = client
|
||||
self.service_uuid = service_uuid
|
||||
self.tx_char_uuid = tx_char_uuid
|
||||
self.rx_char_uuid = rx_char_uuid
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
if self.client.is_connected:
|
||||
await self.client.disconnect()
|
||||
|
||||
def __handle_rx(self, _: BleakGATTCharacteristic, data: bytearray):
|
||||
logger.debug(f'received {len(data)} bytes')
|
||||
self.__receive_buffer += data
|
||||
self.__last_recv_time = time.time()
|
||||
|
||||
@staticmethod
|
||||
def __sliced(data: bytes, n: int) -> Iterator[bytes]:
|
||||
return takewhile(len, (data[i:i + n] for i in count(0, n)))
|
||||
|
||||
@classmethod
|
||||
async def create(cls, address, service_uuid, tx_char_uuid, rx_char_uuid):
|
||||
client = BleakClient(address)
|
||||
await client.connect()
|
||||
self = cls(client, service_uuid, tx_char_uuid, rx_char_uuid)
|
||||
await client.start_notify(self.tx_char_uuid, self.__handle_rx)
|
||||
return self
|
||||
|
||||
async def send(self, data):
|
||||
logger.debug(f'sending {data}')
|
||||
services = self.client.services.get_service(self.service_uuid)
|
||||
rx_char = services.get_characteristic(self.rx_char_uuid)
|
||||
for s in BleStream.__sliced(data, rx_char.max_write_without_response_size):
|
||||
await self.client.write_gatt_char(rx_char, s)
|
||||
return len(data)
|
||||
|
||||
async def recv(self, bufsize, recv_timeout=0.2):
|
||||
if not self.__receive_buffer:
|
||||
return b''
|
||||
|
||||
while time.time() - self.__last_recv_time <= recv_timeout:
|
||||
await sleep(0.1)
|
||||
|
||||
message = self.__receive_buffer[:bufsize]
|
||||
self.__receive_buffer = self.__receive_buffer[bufsize:]
|
||||
logger.debug(f'retrieved {message}')
|
||||
return message
|
||||
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Copyright (c) 2024, The OpenThread Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import ssl
|
||||
import logging
|
||||
|
||||
from .ble_stream import BleStream
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BleStreamSecure:
|
||||
|
||||
def __init__(self, ble_stream: BleStream):
|
||||
self.ble_stream = ble_stream
|
||||
self.ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
||||
self.incoming = ssl.MemoryBIO()
|
||||
self.outgoing = ssl.MemoryBIO()
|
||||
self.ssl_object = None
|
||||
|
||||
def load_cert(self, certfile='', keyfile='', cafile=''):
|
||||
if certfile and keyfile:
|
||||
self.ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile)
|
||||
elif certfile:
|
||||
self.ssl_context.load_cert_chain(certfile=certfile)
|
||||
|
||||
if cafile:
|
||||
self.ssl_context.load_verify_locations(cafile=cafile)
|
||||
|
||||
async def do_handshake(self, hostname):
|
||||
self.ssl_object = self.ssl_context.wrap_bio(
|
||||
incoming=self.incoming,
|
||||
outgoing=self.outgoing,
|
||||
server_side=False,
|
||||
server_hostname=hostname,
|
||||
)
|
||||
while True:
|
||||
try:
|
||||
self.ssl_object.do_handshake()
|
||||
break
|
||||
# SSLWantWrite means ssl wants to send data over the link,
|
||||
# but might need a receive first
|
||||
except ssl.SSLWantWriteError:
|
||||
output = await self.ble_stream.recv(4096)
|
||||
if output:
|
||||
self.incoming.write(output)
|
||||
data = self.outgoing.read()
|
||||
if data:
|
||||
await self.ble_stream.send(data)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# SSLWantRead means ssl wants to receive data from the link,
|
||||
# but might need to send first
|
||||
except ssl.SSLWantReadError:
|
||||
data = self.outgoing.read()
|
||||
if data:
|
||||
await self.ble_stream.send(data)
|
||||
output = await self.ble_stream.recv(4096)
|
||||
if output:
|
||||
self.incoming.write(output)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def send(self, bytes):
|
||||
self.ssl_object.write(bytes)
|
||||
encode = self.outgoing.read(4096)
|
||||
await self.ble_stream.send(encode)
|
||||
|
||||
async def recv(self, buffersize, timeout=1):
|
||||
end_time = asyncio.get_event_loop().time() + timeout
|
||||
data = await self.ble_stream.recv(buffersize)
|
||||
while not data and asyncio.get_event_loop().time() < end_time:
|
||||
await asyncio.sleep(0.1)
|
||||
data = await self.ble_stream.recv(buffersize)
|
||||
if not data:
|
||||
logger.warning('No response when response expected.')
|
||||
return b''
|
||||
|
||||
self.incoming.write(data)
|
||||
while True:
|
||||
try:
|
||||
decode = self.ssl_object.read(4096)
|
||||
break
|
||||
# if recv called before entire message was received from the link
|
||||
except ssl.SSLWantReadError:
|
||||
more = await self.ble_stream.recv(buffersize)
|
||||
while not more:
|
||||
await asyncio.sleep(0.1)
|
||||
more = await self.ble_stream.recv(buffersize)
|
||||
self.incoming.write(more)
|
||||
return decode
|
||||
|
||||
async def send_with_resp(self, bytes):
|
||||
await self.send(bytes)
|
||||
res = await self.recv(buffersize=4096, timeout=5)
|
||||
return res
|
||||
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Copyright (c) 2024, The OpenThread Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
from ble.ble_connection_constants import BBTC_SERVICE_UUID, BBTC_TX_CHAR_UUID, \
|
||||
BBTC_RX_CHAR_UUID, SERVER_COMMON_NAME
|
||||
from ble.ble_stream import BleStream
|
||||
from ble.ble_stream_secure import BleStreamSecure
|
||||
from ble import ble_scanner
|
||||
from tlv.tlv import TLV
|
||||
from tlv.tcat_tlv import TcatTLVType
|
||||
from cli.command import Command, CommandResultNone, CommandResultTLV
|
||||
from dataset.dataset import ThreadDataset
|
||||
from utils import select_device_by_user_input
|
||||
from os import path
|
||||
|
||||
|
||||
class HelpCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'Display help and return.'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
commands = context['commands']
|
||||
for name, command in commands.items():
|
||||
print(f'{name}')
|
||||
command.print_help(indent=1)
|
||||
return CommandResultNone()
|
||||
|
||||
|
||||
class HelloCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'Send round trip "Hello world!" message.'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
bless: BleStreamSecure = context['ble_sstream']
|
||||
print('Sending hello world...')
|
||||
data = TLV(TcatTLVType.APPLICATION.value, bytes('Hello world!', 'ascii')).to_bytes()
|
||||
response = await bless.send_with_resp(data)
|
||||
if not response:
|
||||
return
|
||||
tlv_response = TLV.from_bytes(response)
|
||||
return CommandResultTLV(tlv_response)
|
||||
|
||||
|
||||
class CommissionCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'Update the connected device with current dataset.'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
bless: BleStreamSecure = context['ble_sstream']
|
||||
dataset: ThreadDataset = context['dataset']
|
||||
|
||||
print('Commissioning...')
|
||||
dataset_bytes = dataset.to_bytes()
|
||||
data = TLV(TcatTLVType.ACTIVE_DATASET.value, dataset_bytes).to_bytes()
|
||||
response = await bless.send_with_resp(data)
|
||||
if not response:
|
||||
return
|
||||
tlv_response = TLV.from_bytes(response)
|
||||
return CommandResultTLV(tlv_response)
|
||||
|
||||
|
||||
class ThreadStartCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'Enable thread interface.'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
bless: BleStreamSecure = context['ble_sstream']
|
||||
|
||||
print('Enabling Thread...')
|
||||
data = TLV(TcatTLVType.THREAD_START.value, bytes()).to_bytes()
|
||||
response = await bless.send_with_resp(data)
|
||||
if not response:
|
||||
return
|
||||
tlv_response = TLV.from_bytes(response)
|
||||
return CommandResultTLV(tlv_response)
|
||||
|
||||
|
||||
class ThreadStopCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'Disable thread interface.'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
bless: BleStreamSecure = context['ble_sstream']
|
||||
print('Disabling Thread...')
|
||||
data = TLV(TcatTLVType.THREAD_STOP.value, bytes()).to_bytes()
|
||||
response = await bless.send_with_resp(data)
|
||||
if not response:
|
||||
return
|
||||
tlv_response = TLV.from_bytes(response)
|
||||
return CommandResultTLV(tlv_response)
|
||||
|
||||
|
||||
class ThreadStateCommand(Command):
|
||||
|
||||
def __init__(self):
|
||||
self._subcommands = {'start': ThreadStartCommand(), 'stop': ThreadStopCommand()}
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'Manipulate state of the Thread interface of the connected device.'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
print('Invalid usage. Provide a subcommand.')
|
||||
return CommandResultNone()
|
||||
|
||||
|
||||
class ScanCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'Perform scan for TCAT devices.'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
if not (context['ble_sstream'] is None):
|
||||
del context['ble_sstream']
|
||||
|
||||
tcat_devices = await ble_scanner.scan_tcat_devices()
|
||||
device = select_device_by_user_input(tcat_devices)
|
||||
|
||||
if device is None:
|
||||
return CommandResultNone()
|
||||
|
||||
ble_sstream = None
|
||||
|
||||
print(f'Connecting to {device}')
|
||||
ble_stream = await BleStream.create(device.address, BBTC_SERVICE_UUID, BBTC_TX_CHAR_UUID, BBTC_RX_CHAR_UUID)
|
||||
ble_sstream = BleStreamSecure(ble_stream)
|
||||
ble_sstream.load_cert(
|
||||
certfile=path.join('auth', 'commissioner_cert.pem'),
|
||||
keyfile=path.join('auth', 'commissioner_key.pem'),
|
||||
cafile=path.join('auth', 'ca_cert.pem'),
|
||||
)
|
||||
|
||||
print('Setting up secure channel...')
|
||||
await ble_sstream.do_handshake(hostname=SERVER_COMMON_NAME)
|
||||
print('Done')
|
||||
context['ble_sstream'] = ble_sstream
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Copyright (c) 2024, The OpenThread Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
import readline
|
||||
import shlex
|
||||
from ble.ble_stream_secure import BleStreamSecure
|
||||
from cli.base_commands import (HelpCommand, HelloCommand, CommissionCommand, ThreadStateCommand, ScanCommand)
|
||||
from cli.dataset_commands import (DatasetCommand)
|
||||
from dataset.dataset import ThreadDataset
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class CLI:
|
||||
|
||||
def __init__(self, dataset: ThreadDataset, ble_sstream: Optional[BleStreamSecure] = None):
|
||||
self._commands = {
|
||||
'help': HelpCommand(),
|
||||
'hello': HelloCommand(),
|
||||
'commission': CommissionCommand(),
|
||||
'dataset': DatasetCommand(),
|
||||
'thread': ThreadStateCommand(),
|
||||
'scan': ScanCommand(),
|
||||
}
|
||||
self._context = {'ble_sstream': ble_sstream, 'dataset': dataset, 'commands': self._commands}
|
||||
readline.set_completer(self.completer)
|
||||
readline.parse_and_bind('tab: complete')
|
||||
|
||||
def completer(self, text, state):
|
||||
command_pool = self._commands.keys()
|
||||
full_line = readline.get_line_buffer().lstrip()
|
||||
words = full_line.split()
|
||||
|
||||
should_suggest_subcommands = len(words) > 1 or (len(words) == 1 and full_line[-1].isspace())
|
||||
if should_suggest_subcommands:
|
||||
if words[0] not in self._commands.keys():
|
||||
return None
|
||||
|
||||
current_command = self._commands[words[0]]
|
||||
if full_line[-1].isspace():
|
||||
subcommands = words[1:]
|
||||
else:
|
||||
subcommands = words[1:-1]
|
||||
for nextarg in subcommands:
|
||||
if nextarg in current_command._subcommands.keys():
|
||||
current_command = current_command._subcommands[nextarg]
|
||||
else:
|
||||
return None
|
||||
|
||||
if len(current_command._subcommands) == 0:
|
||||
return None
|
||||
|
||||
command_pool = current_command._subcommands.keys()
|
||||
|
||||
options = [c for c in command_pool if c.startswith(text)]
|
||||
if state < len(options):
|
||||
return options[state]
|
||||
else:
|
||||
return None
|
||||
|
||||
async def evaluate_input(self, user_input):
|
||||
# do not parse empty commands
|
||||
if not user_input.strip():
|
||||
return
|
||||
|
||||
command_parts = shlex.split(user_input)
|
||||
command = command_parts[0]
|
||||
args = command_parts[1:]
|
||||
|
||||
if command not in self._commands.keys():
|
||||
raise Exception('Invalid command: {}'.format(command))
|
||||
|
||||
return await self._commands[command].execute(args, self._context)
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Copyright (c) 2024, The OpenThread Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
from tlv.tlv import TLV
|
||||
from tlv.tcat_tlv import TcatTLVType
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class CommandResult(ABC):
|
||||
|
||||
def __init__(self, value=None):
|
||||
self.value = value
|
||||
|
||||
@abstractmethod
|
||||
def pretty_print(self):
|
||||
pass
|
||||
|
||||
|
||||
class Command(ABC):
|
||||
|
||||
def __init__(self):
|
||||
self._subcommands = {}
|
||||
|
||||
async def execute(self, args, context) -> CommandResult:
|
||||
if len(args) > 0 and args[0] in self._subcommands.keys():
|
||||
return await self.execute_subcommand(args, context)
|
||||
|
||||
return await self.execute_default(args, context)
|
||||
|
||||
async def execute_subcommand(self, args, context) -> CommandResult:
|
||||
return await self._subcommands[args[0]].execute(args[1:], context)
|
||||
|
||||
@abstractmethod
|
||||
async def execute_default(self, args, context) -> CommandResult:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_help_string(self) -> str:
|
||||
pass
|
||||
|
||||
def print_help(self, indent=0):
|
||||
indent_width = 4
|
||||
indentation = ' ' * indent_width * indent
|
||||
print(f'{indentation}{self.get_help_string()}')
|
||||
|
||||
if 'help' in self._subcommands.keys():
|
||||
print(f'{indentation}"help" command available.')
|
||||
elif len(self._subcommands) != 0:
|
||||
print(f'{indentation}Subcommands:')
|
||||
for name, sc in self._subcommands.items():
|
||||
print(f'{indentation}{" " * indent_width}{name}\t- ', end='')
|
||||
sc.print_help()
|
||||
|
||||
|
||||
class CommandResultTLV(CommandResult):
|
||||
|
||||
def pretty_print(self):
|
||||
tlv: TLV = self.value
|
||||
tlv_type = TcatTLVType.from_value(tlv.type)
|
||||
print('Result: TLV:')
|
||||
if tlv_type is not None:
|
||||
print(f'\tTYPE:\t{TcatTLVType.from_value(tlv.type).name}')
|
||||
else:
|
||||
print(f'\tTYPE:\tunknown: {hex(tlv.type)} ({tlv.type})')
|
||||
print(f'\tLEN:\t{len(tlv.value)}')
|
||||
if tlv_type == TcatTLVType.APPLICATION:
|
||||
print(f'\tVALUE:\t{tlv.value.decode("ascii")}')
|
||||
else:
|
||||
print(f'\tVALUE:\t0x{tlv.value.hex()}')
|
||||
|
||||
|
||||
class CommandResultNone(CommandResult):
|
||||
|
||||
def pretty_print(self):
|
||||
pass
|
||||
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
Copyright (c) 2024, The OpenThread Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
from cli.command import Command, CommandResultNone
|
||||
from dataset.dataset import ThreadDataset, initial_dataset
|
||||
from tlv.dataset_tlv import MeshcopTlvType
|
||||
|
||||
|
||||
def handle_dataset_entry_command(type: MeshcopTlvType, args, context):
|
||||
ds: ThreadDataset = context['dataset']
|
||||
if len(args) == 0:
|
||||
ds.get_entry(type).print_content()
|
||||
return CommandResultNone()
|
||||
|
||||
ds.set_entry(type, args)
|
||||
print('Done.')
|
||||
return CommandResultNone()
|
||||
|
||||
|
||||
class DatasetHelpCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'Display help message and return.'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
indent_width = 4
|
||||
indentation = ' ' * indent_width
|
||||
commands: ThreadDataset = context['commands']
|
||||
ds_command: Command = commands['dataset']
|
||||
print(ds_command.get_help_string())
|
||||
print('Subcommands:')
|
||||
for name, subcommand in ds_command._subcommands.items():
|
||||
print(f'{indentation}{name}')
|
||||
print(f'{indentation}{" " * indent_width}{subcommand.get_help_string()}')
|
||||
return CommandResultNone()
|
||||
|
||||
|
||||
class PrintDatasetHexCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'Print current dataset as a hexadecimal string.'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
ds: ThreadDataset = context['dataset']
|
||||
print(ds.to_bytes().hex())
|
||||
return CommandResultNone()
|
||||
|
||||
|
||||
class ReloadDatasetCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'Reset dataset to the initial value.'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
context['dataset'].set_from_bytes(initial_dataset)
|
||||
return CommandResultNone()
|
||||
|
||||
|
||||
class ActiveTimestampCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set ActiveTimestamp seconds. Arguments: [seconds (int)]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.ACTIVETIMESTAMP, args, context)
|
||||
|
||||
|
||||
class PendingTimestampCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set PendingTimestamp seconds. Arguments: [seconds (int)]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.PENDINGTIMESTAMP, args, context)
|
||||
|
||||
|
||||
class NetworkKeyCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set NetworkKey. Arguments: [nk (hexstring, len=32)]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.NETWORKKEY, args, context)
|
||||
|
||||
|
||||
class NetworkNameCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set NetworkName. Arguments: [nn (string, maxlen=16)]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.NETWORKNAME, args, context)
|
||||
|
||||
|
||||
class ExtPanIDCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set ExtPanID. Arguments: [extpanid (hexstring, len=16)]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.EXTPANID, args, context)
|
||||
|
||||
|
||||
class MeshLocalPrefixCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set MeshLocalPrefix. Arguments: [mlp (hexstring, len=16)]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.MESHLOCALPREFIX, args, context)
|
||||
|
||||
|
||||
class DelayTimerCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set DelayTimer delay. Arguments: [delay (int)]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.DELAYTIMER, args, context)
|
||||
|
||||
|
||||
class PanIDCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set PanID. Arguments: [panid (hexstring, len=4)]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.PANID, args, context)
|
||||
|
||||
|
||||
class ChannelCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set Channel. Arguments: [channel (int)]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.CHANNEL, args, context)
|
||||
|
||||
|
||||
class ChannelMaskCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set ChannelMask. Arguments: [mask (hexstring)]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.CHANNELMASK, args, context)
|
||||
|
||||
|
||||
class PskcCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set Pskc. Arguments: [pskc (hexstring, maxlen=32)]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.PSKC, args, context)
|
||||
|
||||
|
||||
class SecurityPolicyCommand(Command):
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and set SecurityPolicy. Arguments: '\
|
||||
'[<rotation_time (int)> [flags (string)] [version_threshold (int)]]'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
return handle_dataset_entry_command(MeshcopTlvType.SECURITYPOLICY, args, context)
|
||||
|
||||
|
||||
class DatasetCommand(Command):
|
||||
|
||||
def __init__(self):
|
||||
self._subcommands = {
|
||||
'help': DatasetHelpCommand(),
|
||||
'hex': PrintDatasetHexCommand(),
|
||||
'reload': ReloadDatasetCommand(),
|
||||
'activetimestamp': ActiveTimestampCommand(),
|
||||
'pendingtimestamp': PendingTimestampCommand(),
|
||||
'networkkey': NetworkKeyCommand(),
|
||||
'networkname': NetworkNameCommand(),
|
||||
'extpanid': ExtPanIDCommand(),
|
||||
'meshlocalprefix': MeshLocalPrefixCommand(),
|
||||
'delay': DelayTimerCommand(),
|
||||
'panid': PanIDCommand(),
|
||||
'channel': ChannelCommand(),
|
||||
'channelmask': ChannelMaskCommand(),
|
||||
'pskc': PskcCommand(),
|
||||
'securitypolicy': SecurityPolicyCommand()
|
||||
}
|
||||
|
||||
def get_help_string(self) -> str:
|
||||
return 'View and manipulate current dataset. ' \
|
||||
'Call without parameters to show current dataset.'
|
||||
|
||||
async def execute_default(self, args, context):
|
||||
ds: ThreadDataset = context['dataset']
|
||||
ds.print_content()
|
||||
return CommandResultNone()
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Copyright (c) 2023 Nordic Semiconductor ASA
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from tlv.tlv import TLV
|
||||
from tlv.dataset_tlv import MeshcopTlvType
|
||||
from dataset.dataset_entries import DatasetEntry, create_dataset_entry
|
||||
|
||||
initial_dataset = bytes([
|
||||
0x0E, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x12, 0x35, 0x06, 0x00, 0x04,
|
||||
0x00, 0x1F, 0xFF, 0xE0, 0x02, 0x08, 0xEF, 0x13, 0x98, 0xC2, 0xFD, 0x50, 0x4B, 0x67, 0x07, 0x08, 0xFD, 0x35, 0x34,
|
||||
0x41, 0x33, 0xD1, 0xD7, 0x3E, 0x05, 0x10, 0xFD, 0xA7, 0xC7, 0x71, 0xA2, 0x72, 0x02, 0xE2, 0x32, 0xEC, 0xD0, 0x4C,
|
||||
0xF9, 0x34, 0xF4, 0x76, 0x03, 0x0F, 0x4F, 0x70, 0x65, 0x6E, 0x54, 0x68, 0x72, 0x65, 0x61, 0x64, 0x2D, 0x63, 0x36,
|
||||
0x34, 0x65, 0x01, 0x02, 0xC6, 0x4E, 0x04, 0x10, 0x5E, 0x9B, 0x9B, 0x36, 0x0F, 0x80, 0xB8, 0x8B, 0xE2, 0x60, 0x3F,
|
||||
0xB0, 0x13, 0x5C, 0x8D, 0x65, 0x0C, 0x04, 0x02, 0xA0, 0xF7, 0xF8
|
||||
])
|
||||
|
||||
|
||||
class ThreadDataset:
|
||||
|
||||
def __init__(self):
|
||||
self.entries: Dict[MeshcopTlvType, DatasetEntry] = {}
|
||||
self.set_from_bytes(initial_dataset)
|
||||
|
||||
def print_content(self):
|
||||
for type, entry in self.entries.items():
|
||||
print(f'{type.name}:')
|
||||
entry.print_content(indent=1)
|
||||
print()
|
||||
|
||||
def set_from_bytes(self, bytes):
|
||||
for tlv in TLV.parse_tlvs(bytes):
|
||||
type = MeshcopTlvType.from_value(tlv.type)
|
||||
self.entries[type] = create_dataset_entry(type)
|
||||
self.entries[type].set_from_tlv(tlv)
|
||||
|
||||
def to_bytes(self):
|
||||
res = bytes()
|
||||
for entry in self.entries.values():
|
||||
res += entry.to_tlv().to_bytes()
|
||||
return res
|
||||
|
||||
def get_entry(self, type: MeshcopTlvType):
|
||||
return self.entries[type]
|
||||
|
||||
def set_entry(self, type: MeshcopTlvType, args: List[str]):
|
||||
if type in self.entries:
|
||||
self.entries[type].set(args)
|
||||
return
|
||||
raise KeyError(f'Key {type} not available in the dataset.')
|
||||
@@ -0,0 +1,502 @@
|
||||
"""
|
||||
Copyright (c) 2023 Nordic Semiconductor ASA
|
||||
|
||||
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 struct
|
||||
import inspect
|
||||
from typing import List
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from tlv.dataset_tlv import MeshcopTlvType
|
||||
from tlv.tlv import TLV
|
||||
|
||||
|
||||
class DatasetEntry(ABC):
|
||||
|
||||
def __init__(self, type: MeshcopTlvType):
|
||||
self.type = type
|
||||
self.length = None
|
||||
self.maxlen = None
|
||||
|
||||
def print_content(self, indent: int = 0, excluded_fields: List[str] = []):
|
||||
excluded_fields += ['length', 'maxlen', 'type']
|
||||
indentation = " " * 4 * indent
|
||||
for attr_name in dir(self):
|
||||
if not attr_name.startswith('_') and attr_name not in excluded_fields:
|
||||
value = getattr(self, attr_name)
|
||||
if not inspect.ismethod(value):
|
||||
if isinstance(value, bytes):
|
||||
value = value.hex()
|
||||
print(f'{indentation}{attr_name}: {value}')
|
||||
|
||||
@abstractmethod
|
||||
def to_tlv(self) -> TLV:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set(self, args: List[str]):
|
||||
pass
|
||||
|
||||
|
||||
class ActiveTimestamp(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.ACTIVETIMESTAMP)
|
||||
self.length = 8 # spec defined
|
||||
self.seconds = 0
|
||||
self.ubit = 0
|
||||
self.ticks = 0
|
||||
|
||||
def set(self, args: List[str]):
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for ActiveTimestamp')
|
||||
self._seconds = int(args[0])
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
(value,) = struct.unpack('>Q', tlv.value)
|
||||
self.ubit = value & 0x1
|
||||
self.ticks = (value >> 1) & 0x7FFF
|
||||
self.seconds = (value >> 16) & 0xFFFF
|
||||
|
||||
def to_tlv(self):
|
||||
value = (self.seconds << 16) | (self.ticks << 1) | self.ubit
|
||||
tlv = struct.pack('>BBQ', self.type.value, self.length, value)
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
class PendingTimestamp(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.PENDINGTIMESTAMP)
|
||||
self.length = 8 # spec defined
|
||||
self.seconds = 0
|
||||
self.ubit = 0
|
||||
self.ticks = 0
|
||||
|
||||
def set(self, args: List[str]):
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for PendingTimestamp')
|
||||
self._seconds = int(args[0])
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
(value,) = struct.unpack('>Q', tlv.value)
|
||||
self.ubit = value & 0x1
|
||||
self.ticks = (value >> 1) & 0x7FFF
|
||||
self.seconds = (value >> 16) & 0xFFFF
|
||||
|
||||
def to_tlv(self):
|
||||
value = (self.seconds << 16) | (self.ticks << 1) | self.ubit
|
||||
tlv = struct.pack('>BBQ', self.type.value, self.length, value)
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
class NetworkKey(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.NETWORKKEY)
|
||||
self.length = 16 # spec defined
|
||||
self.data: str = ''
|
||||
|
||||
def set(self, args: List[str]):
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for NetworkKey')
|
||||
if args[0].startswith('0x'):
|
||||
args[0] = args[0][2:]
|
||||
nk = args[0]
|
||||
if len(nk) != self.length * 2: # need length * 2 hex characters
|
||||
raise ValueError('Invalid length of NetworkKey')
|
||||
self.data = nk
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
self.data = tlv.value.hex()
|
||||
|
||||
def to_tlv(self):
|
||||
if len(self.data) != self.length * 2: # need length * 2 hex characters
|
||||
raise ValueError('Invalid length of NetworkKey')
|
||||
value = bytes.fromhex(self.data)
|
||||
tlv = struct.pack('>BB', self.type.value, self.length) + value
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
class NetworkName(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.NETWORKNAME)
|
||||
self.maxlen = 16
|
||||
self.data: str = ''
|
||||
|
||||
def set(self, args: List[str]):
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for NetworkName')
|
||||
nn = args[0]
|
||||
if len(nn) > self.maxlen:
|
||||
raise ValueError('Invalid length of NetworkName')
|
||||
self.data = nn
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
self.data = tlv.value.decode('utf-8')
|
||||
|
||||
def to_tlv(self):
|
||||
length_value = len(self.data)
|
||||
value = self.data.encode('utf-8')
|
||||
tlv = struct.pack('>BB', self.type.value, length_value) + value
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
class ExtPanID(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.EXTPANID)
|
||||
self.length = 8 # spec defined
|
||||
self.data: str = ''
|
||||
|
||||
def set(self, args: List[str]):
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for ExtPanID')
|
||||
if args[0].startswith('0x'):
|
||||
args[0] = args[0][2:]
|
||||
epid = args[0]
|
||||
if len(epid) != self.length * 2: # need length*2 hex characters
|
||||
raise ValueError('Invalid length of ExtPanID')
|
||||
self.data = epid
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
self.data = tlv.value.hex()
|
||||
|
||||
def to_tlv(self):
|
||||
if len(self.data) != self.length * 2: # need length*2 hex characters
|
||||
raise ValueError('Invalid length of ExtPanID')
|
||||
|
||||
value = bytes.fromhex(self.data)
|
||||
tlv = struct.pack('>BB', self.type.value, self.length) + value
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
class MeshLocalPrefix(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.MESHLOCALPREFIX)
|
||||
self.length = 8 # spec defined
|
||||
self.data = ''
|
||||
|
||||
def set(self, args: List[str]):
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for MeshLocalPrefix')
|
||||
if args[0].startswith('0x'):
|
||||
args[0] = args[0][2:]
|
||||
mlp = args[0]
|
||||
if len(mlp) != self.length * 2: # need length*2 hex characters
|
||||
raise ValueError('Invalid length of MeshLocalPrefix')
|
||||
self.data = mlp
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
self.data = tlv.value.hex()
|
||||
|
||||
def to_tlv(self):
|
||||
if len(self.data) != self.length * 2: # need length*2 hex characters
|
||||
raise ValueError('Invalid length of MeshLocalPrefix')
|
||||
|
||||
value = bytes.fromhex(self.data)
|
||||
tlv = struct.pack('>BB', self.type.value, self.length) + value
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
class DelayTimer(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.DELAYTIMER)
|
||||
self.length = 4 # spec defined
|
||||
self.time_remaining = 0
|
||||
|
||||
def set(self, args: List[str]):
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for DelayTimer')
|
||||
dt = int(args[0])
|
||||
self.time_remaining = dt
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
self.time_remaining = tlv.value
|
||||
|
||||
def to_tlv(self):
|
||||
value = self.time_remaining
|
||||
tlv = struct.pack('>BBI', self.type.value, self.length, value)
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
class PanID(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.PANID)
|
||||
self.length = 2 # spec defined
|
||||
self.data: str = ''
|
||||
|
||||
def set(self, args: List[str]):
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for PanID')
|
||||
if args[0].startswith('0x'):
|
||||
args[0] = args[0][2:]
|
||||
pid = args[0]
|
||||
if len(pid) != self.length * 2: # need length*2 hex characters
|
||||
raise ValueError('Invalid length of PanID')
|
||||
self.data = pid
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
self.data = tlv.value.hex()
|
||||
|
||||
def to_tlv(self):
|
||||
if len(self.data) != self.length * 2: # need length*2 hex characters
|
||||
raise ValueError('Invalid length of PanID')
|
||||
|
||||
value = bytes.fromhex(self.data)
|
||||
tlv = struct.pack('>BB', self.type.value, self.length) + value
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
class Channel(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.CHANNEL)
|
||||
self.length = 3 # spec defined
|
||||
self.channel_page = 0
|
||||
self.channel = 0
|
||||
|
||||
def set(self, args: List[str]):
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for Channel')
|
||||
channel = int(args[0])
|
||||
self.channel = channel
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
self.channel = int.from_bytes(tlv.value[1:3], byteorder='big')
|
||||
self.channel_page = tlv.value[0]
|
||||
|
||||
def to_tlv(self):
|
||||
tlv = struct.pack('>BBB', self.type.value, self.length, self.channel_page)
|
||||
tlv += struct.pack('>H', self.channel)
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
class Pskc(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.PSKC)
|
||||
self.maxlen = 16
|
||||
self.data = ''
|
||||
|
||||
def set(self, args: List[str]):
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for Pskc')
|
||||
if args[0].startswith('0x'):
|
||||
args[0] = args[0][2:]
|
||||
pskc = args[0]
|
||||
if (len(pskc) > self.maxlen * 2):
|
||||
raise ValueError('Invalid length of Pskc. Can be max ' f'{self.length * 2} hex characters.')
|
||||
self.data = pskc
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
self.data = tlv.value.hex()
|
||||
|
||||
def to_tlv(self):
|
||||
# should not exceed max length*2 hex characters
|
||||
if (len(self.data) > self.maxlen * 2):
|
||||
raise ValueError('Invalid length of Pskc')
|
||||
|
||||
length_value = len(self.data) // 2
|
||||
value = bytes.fromhex(self.data)
|
||||
tlv = struct.pack('>BB', self.type.value, length_value) + value
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
class SecurityPolicy(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.SECURITYPOLICY)
|
||||
self.length = 4 # spec defined
|
||||
self.rotation_time = 0
|
||||
self.out_of_band = 0 # o
|
||||
self.native = 0 # n
|
||||
self.routers_1_2 = 0 # r
|
||||
self.external_commissioners = 0 # c
|
||||
self.reserved = 0
|
||||
self.commercial_commissioning_off = 0 # C
|
||||
self.autonomous_enrollment_off = 0 # e
|
||||
self.networkkey_provisioning_off = 0 # p
|
||||
self.thread_over_ble = 0
|
||||
self.non_ccm_routers_off = 0 # R
|
||||
self.rsv = 0b111
|
||||
self.version_threshold = 0
|
||||
|
||||
def set(self, args: List[str]):
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for SecurityPolicy')
|
||||
rotation_time, flags, version_threshold = args + [None] * (3 - len(args))
|
||||
self.rotation_time = int(rotation_time) & 0xffff
|
||||
|
||||
if flags:
|
||||
self.out_of_band = 1 if 'o' in flags else 0
|
||||
self.native = 1 if 'n' in flags else 0
|
||||
self.routers_1_2 = 1 if 'r' in flags else 0
|
||||
self.external_commissioners = 1 if 'c' in flags else 0
|
||||
self.commercial_commissioning_off = 0 if 'C' in flags else 1
|
||||
self.autonomous_enrollment_off = 0 if 'e' in flags else 1
|
||||
self.networkkey_provisioning_off = 0 if 'p' in flags else 1
|
||||
self.non_ccm_routers_off = 0 if 'R' in flags else 1
|
||||
|
||||
if version_threshold:
|
||||
self.version_threshold = int(version_threshold) & 0b111
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
value = int.from_bytes(tlv.value, byteorder='big')
|
||||
|
||||
self.rotation_time = (value >> 16) & 0xFFFF
|
||||
self.out_of_band = (value >> 15) & 0x1
|
||||
self.native = (value >> 14) & 0x1
|
||||
self.routers_1_2 = (value >> 13) & 0x1
|
||||
self.external_commissioners = (value >> 12) & 0x1
|
||||
self.reserved = (value >> 11) & 0x1
|
||||
self.commercial_commissioning_off = (value >> 10) & 0x1
|
||||
self.autonomous_enrollment_off = (value >> 9) & 0x1
|
||||
self.networkkey_provisioning_off = (value >> 8) & 0x1
|
||||
self.thread_over_ble = (value >> 7) & 0x1
|
||||
self.non_ccm_routers_off = (value >> 6) & 0x1
|
||||
self.rsv = (value >> 3) & 0x7
|
||||
self.version_threshold = value & 0x7
|
||||
|
||||
def to_tlv(self):
|
||||
value = self.rotation_time << 16
|
||||
value |= self.out_of_band << 15
|
||||
value |= self.native << 14
|
||||
value |= self.routers_1_2 << 13
|
||||
value |= self.external_commissioners << 12
|
||||
value |= self.reserved << 11
|
||||
value |= self.commercial_commissioning_off << 10
|
||||
value |= self.autonomous_enrollment_off << 9
|
||||
value |= self.networkkey_provisioning_off << 8
|
||||
value |= self.thread_over_ble << 7
|
||||
value |= self.non_ccm_routers_off << 6
|
||||
value |= self.rsv << 3
|
||||
value |= self.version_threshold
|
||||
tlv = struct.pack('>BBI', self.type.value, self.length, value)
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
def print_content(self, indent: int = 0):
|
||||
flags = ''
|
||||
if self.out_of_band:
|
||||
flags += 'o'
|
||||
if self.native:
|
||||
flags += 'n'
|
||||
if self.routers_1_2:
|
||||
flags += 'r'
|
||||
if self.external_commissioners:
|
||||
flags += 'c'
|
||||
if not self.commercial_commissioning_off:
|
||||
flags += 'C'
|
||||
if not self.autonomous_enrollment_off:
|
||||
flags += 'e'
|
||||
if not self.networkkey_provisioning_off:
|
||||
flags += 'p'
|
||||
if not self.non_ccm_routers_off:
|
||||
flags += 'R'
|
||||
indentation = " " * 4 * indent
|
||||
print(f'{indentation}rotation_time: {self.rotation_time}')
|
||||
print(f'{indentation}flags: {flags}')
|
||||
print(f'{indentation}version_threshold: {self.version_threshold}')
|
||||
|
||||
|
||||
class ChannelMask(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(MeshcopTlvType.CHANNELMASK)
|
||||
self.entries: List[ChannelMaskEntry] = []
|
||||
|
||||
def set(self, args: List[str]):
|
||||
# to remain consistent with the OpenThread CLI API,
|
||||
# provided hex string is value of the first channel mask entry
|
||||
if len(args) == 0:
|
||||
raise ValueError('No argument for ChannelMask')
|
||||
if args[0].startswith('0x'):
|
||||
args[0] = args[0][2:]
|
||||
channelmsk = bytes.fromhex(args[0])
|
||||
self.entries = [ChannelMaskEntry()]
|
||||
self.entries[0].channel_mask = channelmsk
|
||||
|
||||
def print_content(self, indent: int = 0):
|
||||
super().print_content(indent=indent, excluded_fields=['entries'])
|
||||
indentation = " " * 4 * indent
|
||||
for i, entry in enumerate(self.entries):
|
||||
print(f'{indentation}ChannelMaskEntry {i}')
|
||||
entry.print_content(indent=indent + 1)
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
self.entries = []
|
||||
for mask_entry_tlv in TLV.parse_tlvs(tlv.value):
|
||||
new_entry = ChannelMaskEntry()
|
||||
new_entry.set_from_tlv(mask_entry_tlv)
|
||||
self.entries.append(new_entry)
|
||||
|
||||
def to_tlv(self):
|
||||
tlv_value = b''.join(mask_entry.to_tlv().to_bytes() for mask_entry in self.entries)
|
||||
tlv = struct.pack('>BB', self.type.value, len(tlv_value)) + tlv_value
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
class ChannelMaskEntry(DatasetEntry):
|
||||
|
||||
def __init__(self):
|
||||
self.channel_page = 0
|
||||
self.channel_mask: bytes = None
|
||||
|
||||
def set(self, args: List[str]):
|
||||
pass
|
||||
|
||||
def set_from_tlv(self, tlv: TLV):
|
||||
self.channel_page = tlv.type
|
||||
self.mask_length = len(tlv.value)
|
||||
self.channel_mask = tlv.value
|
||||
|
||||
def to_tlv(self):
|
||||
mask_len = len(self.channel_mask)
|
||||
tlv = struct.pack('>BB', self.channel_page, mask_len) + self.channel_mask
|
||||
return TLV.from_bytes(tlv)
|
||||
|
||||
|
||||
def create_dataset_entry(type: MeshcopTlvType, args=None):
|
||||
entry_classes = {
|
||||
MeshcopTlvType.ACTIVETIMESTAMP: ActiveTimestamp,
|
||||
MeshcopTlvType.PENDINGTIMESTAMP: PendingTimestamp,
|
||||
MeshcopTlvType.NETWORKKEY: NetworkKey,
|
||||
MeshcopTlvType.NETWORKNAME: NetworkName,
|
||||
MeshcopTlvType.EXTPANID: ExtPanID,
|
||||
MeshcopTlvType.MESHLOCALPREFIX: MeshLocalPrefix,
|
||||
MeshcopTlvType.DELAYTIMER: DelayTimer,
|
||||
MeshcopTlvType.PANID: PanID,
|
||||
MeshcopTlvType.CHANNEL: Channel,
|
||||
MeshcopTlvType.PSKC: Pskc,
|
||||
MeshcopTlvType.SECURITYPOLICY: SecurityPolicy,
|
||||
MeshcopTlvType.CHANNELMASK: ChannelMask
|
||||
}
|
||||
|
||||
entry_class = entry_classes.get(type)
|
||||
if not entry_class:
|
||||
raise ValueError(f"Invalid configuration type: {type}")
|
||||
|
||||
res = entry_class()
|
||||
if args:
|
||||
res.set(args)
|
||||
return res
|
||||
Generated
+318
@@ -0,0 +1,318 @@
|
||||
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "4.0.2"
|
||||
description = "Timeout context manager for asyncio programs"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
|
||||
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "bleak"
|
||||
version = "0.20.2"
|
||||
description = "Bluetooth Low Energy platform Agnostic Klient"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "bleak-0.20.2-py3-none-any.whl", hash = "sha256:ce3106b7258212d92bb77be06f9301774f51f5bbc9f7cd50976ad794e9514dba"},
|
||||
{file = "bleak-0.20.2.tar.gz", hash = "sha256:6c92a47abe34e6dea8ffc5cea9457cbff6e1be966854839dbc25cddb36b79ee4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
async-timeout = {version = ">=3.0.0,<5", markers = "python_version < \"3.11\""}
|
||||
bleak-winrt = {version = ">=1.2.0,<2.0.0", markers = "platform_system == \"Windows\""}
|
||||
dbus-fast = {version = ">=1.83.0,<2.0.0", markers = "platform_system == \"Linux\""}
|
||||
pyobjc-core = {version = ">=9.0.1,<10.0.0", markers = "platform_system == \"Darwin\""}
|
||||
pyobjc-framework-CoreBluetooth = {version = ">=9.0.1,<10.0.0", markers = "platform_system == \"Darwin\""}
|
||||
pyobjc-framework-libdispatch = {version = ">=9.0.1,<10.0.0", markers = "platform_system == \"Darwin\""}
|
||||
typing-extensions = {version = ">=4.2.0,<5.0.0", markers = "python_version < \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "bleak-winrt"
|
||||
version = "1.2.0"
|
||||
description = "Python WinRT bindings for Bleak"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "bleak-winrt-1.2.0.tar.gz", hash = "sha256:0577d070251b9354fc6c45ffac57e39341ebb08ead014b1bdbd43e211d2ce1d6"},
|
||||
{file = "bleak_winrt-1.2.0-cp310-cp310-win32.whl", hash = "sha256:a2ae3054d6843ae0cfd3b94c83293a1dfd5804393977dd69bde91cb5099fc47c"},
|
||||
{file = "bleak_winrt-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:677df51dc825c6657b3ae94f00bd09b8ab88422b40d6a7bdbf7972a63bc44e9a"},
|
||||
{file = "bleak_winrt-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9449cdb942f22c9892bc1ada99e2ccce9bea8a8af1493e81fefb6de2cb3a7b80"},
|
||||
{file = "bleak_winrt-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:98c1b5a6a6c431ac7f76aa4285b752fe14a1c626bd8a1dfa56f66173ff120bee"},
|
||||
{file = "bleak_winrt-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:623ac511696e1f58d83cb9c431e32f613395f2199b3db7f125a3d872cab968a4"},
|
||||
{file = "bleak_winrt-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:13ab06dec55469cf51a2c187be7b630a7a2922e1ea9ac1998135974a7239b1e3"},
|
||||
{file = "bleak_winrt-1.2.0-cp38-cp38-win32.whl", hash = "sha256:5a36ff8cd53068c01a795a75d2c13054ddc5f99ce6de62c1a97cd343fc4d0727"},
|
||||
{file = "bleak_winrt-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:810c00726653a962256b7acd8edf81ab9e4a3c66e936a342ce4aec7dbd3a7263"},
|
||||
{file = "bleak_winrt-1.2.0-cp39-cp39-win32.whl", hash = "sha256:dd740047a08925bde54bec357391fcee595d7b8ca0c74c87170a5cbc3f97aa0a"},
|
||||
{file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus-fast"
|
||||
version = "1.90.1"
|
||||
description = "A faster version of dbus-next"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "dbus_fast-1.90.1-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:657f9f292f770b50c755bc9cc3607ec0901a607d6d6e31c67aa953b73b31d66a"},
|
||||
{file = "dbus_fast-1.90.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:229cb2dc0942dfe36ce4f5a49cacb7f39ae05527c6ccec66b9a670ca7a02129a"},
|
||||
{file = "dbus_fast-1.90.1-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:4978d6dca49b778c426f279f014554e29ece6bfc7530fa8ab9d258f068e5954e"},
|
||||
{file = "dbus_fast-1.90.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2161851a1f90a1c2fe064d1870b04bdca0033b42fdc97cc7d5132637227ac915"},
|
||||
{file = "dbus_fast-1.90.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0012799b154fb6b066ff6948f5edd0c6bf8655fca6f3578fc78598334f9f978b"},
|
||||
{file = "dbus_fast-1.90.1-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:0b97748928e3e56bc98f292be894d1d3c2a4acc58555795a3aa3b74769b96542"},
|
||||
{file = "dbus_fast-1.90.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2baa2e1053ded0a5ccdefc651ee8fcd09d6f4f864b9f301363dbf0545f95a89"},
|
||||
{file = "dbus_fast-1.90.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0262ab1d3d07ac892645d4cec54daabd3ba4a096ca4c4b2e9f09abd1819ca663"},
|
||||
{file = "dbus_fast-1.90.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c39a352a893923d255031d15fa005ac5f5df2d1195729f206fc79a95b219daed"},
|
||||
{file = "dbus_fast-1.90.1-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:bd012a0ed7e6479bcc5b0efe91a45c3abb3e0e4e371a28c0f3c347cb9baffe8a"},
|
||||
{file = "dbus_fast-1.90.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3e895adfa89a6c08d23fd22707bd5ea8a301579f3a6ff8bd33df1e94ea16e8a"},
|
||||
{file = "dbus_fast-1.90.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a94806e9990b6a7fd4896ee2a979c20af4e5ba76bfddda55a7a79f4da268b51f"},
|
||||
{file = "dbus_fast-1.90.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae0b95a08db0a7e38452926c8d5964d41e5f20d8c89fd00b8913ec4f1908f7bd"},
|
||||
{file = "dbus_fast-1.90.1-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:3ce5153accbbb7fc2aeab055f46d5611c4c57978d43feba9257fba53c338ebbe"},
|
||||
{file = "dbus_fast-1.90.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3b05b68a7be76df3e4833c72d9fadf1b934359ed3fabd69ed205eaa18d132a6"},
|
||||
{file = "dbus_fast-1.90.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d407eb9a3581ee5cc047a664d3d15e9846e5a1c0b3565922c72ffe15fea590f2"},
|
||||
{file = "dbus_fast-1.90.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f06e4cbf560e682930f0d23ea93b1887c40db00b98ee4d6a207aa2a616851e"},
|
||||
{file = "dbus_fast-1.90.1-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:28a4ee863b4a42351afb38ce9432850922dc2f0d9d9863f98fcb47e23b710572"},
|
||||
{file = "dbus_fast-1.90.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd6090c794ca3404702b59cb4dc92f674e26a15bb79a9d5ae0236658c10cac5"},
|
||||
{file = "dbus_fast-1.90.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9e7be16e96bcad521f2c045561799fc486047b6e1ce071c35a5cea36a9ed19f4"},
|
||||
{file = "dbus_fast-1.90.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:01f582d9c3f24e1721f3dd9a62c7a558c7c8406752eb042738623b9d89f454e9"},
|
||||
{file = "dbus_fast-1.90.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:085fd80c7ea3e41a2ac32e419611036a042593778fecb4d92526a22fd2be2c0b"},
|
||||
{file = "dbus_fast-1.90.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e0dbffd42875d31d0c7e2407eff685d5cb2dfa7c0448079c96ef2cec0afe8be"},
|
||||
{file = "dbus_fast-1.90.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:04eaaea336059909b5cb51c897fc343e038e80f698a049b1bf3002d162d4fec7"},
|
||||
{file = "dbus_fast-1.90.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fc215dbccb798df07ee6104b984ae09c159ed3a633df915c3c6dd9df97af753"},
|
||||
{file = "dbus_fast-1.90.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:74f7afaf780fbbc7f39cc8167ec585f8e7ce140d214768f5c296c2b03d23e571"},
|
||||
{file = "dbus_fast-1.90.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f12c7bcec416e28ba1f741e43ad2eef7eb3c677838208cf6a801ced711b508c"},
|
||||
{file = "dbus_fast-1.90.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c0811b7bdfdce40072fd7c29f0c7e0145982b981bb068efd79a5e43ef14150e5"},
|
||||
{file = "dbus_fast-1.90.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1f00197e08c7c5837861624fd1afb8e1d8331d4e28e8d2ff01dc17ac2305ae2"},
|
||||
{file = "dbus_fast-1.90.1.tar.gz", hash = "sha256:eff98b45443681bd8876bbb1444b35112d62e8d12157f004d88ebe5f0481d5b7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.1.2"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"},
|
||||
{file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "6.7.0"
|
||||
description = "Read metadata from Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"},
|
||||
{file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
perf = ["ipython"]
|
||||
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.1"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
|
||||
{file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.2.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"},
|
||||
{file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-core"
|
||||
version = "9.2"
|
||||
description = "Python<->ObjC Interoperability Module"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyobjc-core-9.2.tar.gz", hash = "sha256:d734b9291fec91ff4e3ae38b9c6839debf02b79c07314476e87da8e90b2c68c3"},
|
||||
{file = "pyobjc_core-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fa674a39949f5cde8e5c7bbcd24496446bfc67592b028aedbec7f81dc5fc4daa"},
|
||||
{file = "pyobjc_core-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bbc8de304ee322a1ee530b4d2daca135a49b4a49aa3cedc6b2c26c43885f4842"},
|
||||
{file = "pyobjc_core-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0fa950f092673883b8bd28bc18397415cabb457bf410920762109b411789ade9"},
|
||||
{file = "pyobjc_core-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:586e4cae966282eaa61b21cae66ccdcee9d69c036979def26eebdc08ddebe20f"},
|
||||
{file = "pyobjc_core-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41189c2c680931c0395a55691763c481fc681f454f21bb4f1644f98c24a45954"},
|
||||
{file = "pyobjc_core-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:2d23ee539f2ba5e9f5653d75a13f575c7e36586fc0086792739e69e4c2617eda"},
|
||||
{file = "pyobjc_core-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b9809cf96678797acb72a758f34932fe8e2602d5ab7abec15c5ac68ddb481720"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-cocoa"
|
||||
version = "9.2"
|
||||
description = "Wrappers for the Cocoa frameworks on macOS"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyobjc-framework-Cocoa-9.2.tar.gz", hash = "sha256:efd78080872d8c8de6c2b97e0e4eac99d6203a5d1637aa135d071d464eb2db53"},
|
||||
{file = "pyobjc_framework_Cocoa-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9e02d8a7cc4eb7685377c50ba4f17345701acf4c05b1e7480d421bff9e2f62a4"},
|
||||
{file = "pyobjc_framework_Cocoa-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3b1e6287b3149e4c6679cdbccd8e9ef6557a4e492a892e80a77df143f40026d2"},
|
||||
{file = "pyobjc_framework_Cocoa-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:312977ce2e3989073c6b324c69ba24283de206fe7acd6dbbbaf3e29238a22537"},
|
||||
{file = "pyobjc_framework_Cocoa-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aae7841cf40c26dd915f4dd828f91c6616e6b7998630b72e704750c09e00f334"},
|
||||
{file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:739a421e14382a46cbeb9a883f192dceff368ad28ec34d895c48c0ad34cf2c1d"},
|
||||
{file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:32d9ac1033fac1b821ddee8c68f972a7074ad8c50bec0bea9a719034c1c2fb94"},
|
||||
{file = "pyobjc_framework_Cocoa-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b236bb965e41aeb2e215d4e98a5a230d4b63252c6d26e00924ea2e69540a59d6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyobjc-core = ">=9.2"
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-corebluetooth"
|
||||
version = "9.2"
|
||||
description = "Wrappers for the framework CoreBluetooth on macOS"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyobjc-framework-CoreBluetooth-9.2.tar.gz", hash = "sha256:cb2481b1dfe211ae9ce55f36537dc8155dbf0dc8ff26e0bc2e13f7afb0a291d1"},
|
||||
{file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:53d888742119d0f0c725d0b0c2389f68e8f21f0cba6d6aec288c53260a0196b6"},
|
||||
{file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:179532882126526e38fe716a50fb0ee8f440e0b838d290252c515e622b5d0e49"},
|
||||
{file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:256a5031ea9d8a7406541fa1b0dfac549b1de93deae8284605f9355b13fb58be"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyobjc-core = ">=9.2"
|
||||
pyobjc-framework-Cocoa = ">=9.2"
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-libdispatch"
|
||||
version = "9.2"
|
||||
description = "Wrappers for libdispatch on macOS"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyobjc-framework-libdispatch-9.2.tar.gz", hash = "sha256:542e7f7c2b041939db5ed6f3119c1d67d73ec14a996278b92485f8513039c168"},
|
||||
{file = "pyobjc_framework_libdispatch-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88d4091d4bcb5702783d6e86b4107db973425a17d1de491543f56bd348909b60"},
|
||||
{file = "pyobjc_framework_libdispatch-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a67b007113328538b57893cc7829a722270764cdbeae6d5e1460a1d911314df"},
|
||||
{file = "pyobjc_framework_libdispatch-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6fccea1a57436cf1ac50d9ebc6e3e725bcf77f829ba6b118e62e6ed7866d359d"},
|
||||
{file = "pyobjc_framework_libdispatch-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6eba747b7ad91b0463265a7aee59235bb051fb97687f35ca2233690369b5e4e4"},
|
||||
{file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2e835495860d04f63c2d2f73ae3dd79da4222864c107096dc0f99e8382700026"},
|
||||
{file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1b107e5c3580b09553030961ea6b17abad4a5132101eab1af3ad2cb36d0f08bb"},
|
||||
{file = "pyobjc_framework_libdispatch-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:83cdb672acf722717b5ecf004768f215f02ac02d7f7f2a9703da6e921ab02222"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyobjc-core = ">=9.2"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.4.0"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"},
|
||||
{file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.7.1"
|
||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
|
||||
{file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.15.0"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
|
||||
{file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "488001f322904181b097ee8c0dd489ad81af39c4378d76f5945de9832077a315"
|
||||
@@ -0,0 +1,16 @@
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "bbtc client"
|
||||
version = "0.1.0"
|
||||
description = "BBTC client for TCAT devices"
|
||||
authors = ["Piotr Jasinski <[email protected]>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
bleak = "^0.20.2"
|
||||
pytest ="^7.1.2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Copyright (c) 2024, The OpenThread Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MeshcopTlvType(Enum):
|
||||
CHANNEL = 0
|
||||
PANID = 1
|
||||
EXTPANID = 2
|
||||
NETWORKNAME = 3
|
||||
PSKC = 4
|
||||
NETWORKKEY = 5
|
||||
NETWORK_KEY_SEQUENCE = 6
|
||||
MESHLOCALPREFIX = 7
|
||||
STEERING_DATA = 8
|
||||
BORDER_AGENT_RLOC = 9
|
||||
COMMISSIONER_ID = 10
|
||||
COMM_SESSION_ID = 11
|
||||
SECURITYPOLICY = 12
|
||||
GET = 13
|
||||
ACTIVETIMESTAMP = 14
|
||||
COMMISSIONER_UDP_PORT = 15
|
||||
STATE = 16
|
||||
JOINER_DTLS = 17
|
||||
JOINER_UDP_PORT = 18
|
||||
JOINER_IID = 19
|
||||
JOINER_RLOC = 20
|
||||
JOINER_ROUTER_KEK = 21
|
||||
PROVISIONING_URL = 32
|
||||
VENDOR_NAME_TLV = 33
|
||||
VENDOR_MODEL_TLV = 34
|
||||
VENDOR_SW_VERSION_TLV = 35
|
||||
VENDOR_DATA_TLV = 36
|
||||
VENDOR_STACK_VERSION_TLV = 37
|
||||
UDP_ENCAPSULATION_TLV = 48
|
||||
IPV6_ADDRESS_TLV = 49
|
||||
PENDINGTIMESTAMP = 51
|
||||
DELAYTIMER = 52
|
||||
CHANNELMASK = 53
|
||||
COUNT = 54
|
||||
PERIOD = 55
|
||||
SCAN_DURATION = 56
|
||||
ENERGY_LIST = 57
|
||||
DISCOVERYREQUEST = 128
|
||||
DISCOVERYRESPONSE = 129
|
||||
JOINERADVERTISEMENT = 241
|
||||
|
||||
@classmethod
|
||||
def from_value(cls, value: int):
|
||||
return cls._value2member_map_.get(value)
|
||||
|
||||
def to_bytes(self):
|
||||
return bytes([self.value])
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Copyright (c) 2024, The OpenThread Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class TcatTLVType(Enum):
|
||||
RESPONSE_W_STATUS = 0x01
|
||||
RESPONSE_W_PAYLOAD = 0x02
|
||||
ACTIVE_DATASET = 0x20
|
||||
DECOMMISSION = 0x60
|
||||
APPLICATION = 0x82
|
||||
THREAD_START = 0x27
|
||||
THREAD_STOP = 0x28
|
||||
|
||||
@classmethod
|
||||
def from_value(cls, value: int):
|
||||
return cls._value2member_map_.get(value)
|
||||
|
||||
def to_bytes(self):
|
||||
return bytes([self.value])
|
||||
Reference in New Issue
Block a user