[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:
Piotr Jasinski
2024-02-07 13:13:12 -08:00
committed by GitHub
co-authored by Przemyslaw Bida
parent 869c2ded9c
commit 905a22e0c7
21 changed files with 2202 additions and 0 deletions
@@ -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
```
+56
View File
@@ -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.
+13
View File
@@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIICCDCCAa2gAwIBAgIJAIKxygBXoH+5MAoGCCqGSM49BAMCMG8xCzAJBgNVBAYT
AlhYMRAwDgYDVQQIEwdNeVN0YXRlMQ8wDQYDVQQHEwZNeUNpdHkxDzANBgNVBAsT
Bk15VW5pdDERMA8GA1UEChMITXlWZW5kb3IxGTAXBgNVBAMTEHd3dy5teXZlbmRv
ci5jb20wHhcNMjMxMDE2MTAzMzE1WhcNMjYxMDE2MTAzMzE1WjBvMQswCQYDVQQG
EwJYWDEQMA4GA1UECBMHTXlTdGF0ZTEPMA0GA1UEBxMGTXlDaXR5MQ8wDQYDVQQL
EwZNeVVuaXQxETAPBgNVBAoTCE15VmVuZG9yMRkwFwYDVQQDExB3d3cubXl2ZW5k
b3IuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWdyzPAXGKeZY94OhHAWX
HzJfQIjGSyaOzlgL9OEFw2SoUDncLKPGwfPAUSfuMyEkzszNDM0HHkBsDLqu4n25
/6MyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU4EynoSw9eDKZEVPkums2
IWLAJCowCgYIKoZIzj0EAwIDSQAwRgIhAMYGGL9xShyE6P9wEU+MAYF6W3CzdrwV
kuerX1encIH2AiEA5rq490NUobM1Au43roxJq1T6Z43LscPVbGZfULD1Jq0=
-----END CERTIFICATE-----
+5
View File
@@ -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-----
+112
View File
@@ -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'
+52
View File
@@ -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
+93
View File
@@ -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
+165
View File
@@ -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
+97
View File
@@ -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)
+101
View File
@@ -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()
+64
View File
@@ -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
+318
View File
@@ -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"
+16
View File
@@ -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]
+79
View File
@@ -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])
+45
View File
@@ -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])