pax_global_header00006660000000000000000000000064146315214240014514gustar00rootroot0000000000000052 comment=e1f8d0c5d3166295d91ab1f1e9c0ad7a0559ca3b python-btsocket-0.3.0/000077500000000000000000000000001463152142400146515ustar00rootroot00000000000000python-btsocket-0.3.0/.github/000077500000000000000000000000001463152142400162115ustar00rootroot00000000000000python-btsocket-0.3.0/.github/workflows/000077500000000000000000000000001463152142400202465ustar00rootroot00000000000000python-btsocket-0.3.0/.github/workflows/pypi-publish.yml000066400000000000000000000015261463152142400234220ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: PyPIUpload on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | python setup.py sdist bdist_wheel twine upload dist/* python-btsocket-0.3.0/.github/workflows/python-app.yml000066400000000000000000000020271463152142400230710ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: btsocket-tests on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python 3.10 uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install python dependencies run: | python3 -m pip install --upgrade pip python3 -m pip install setuptools python3 -m pip install .[test] if [ -f requirements.txt ]; then /usr/bin/python3 -m pip install -r requirements.txt; fi - name: Lint with pycodestyle run: | python3 -m pycodestyle -v btsocket python3 -m pycodestyle -v examples - name: Test with unittest run: | coverage run -m unittest discover -v tests coverage report python-btsocket-0.3.0/.gitignore000066400000000000000000000017651463152142400166520ustar00rootroot00000000000000# Created by .ignore support plugin (hsz.mobi) ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # PyCharm .idea/ # local experiments experiments/ # web bluetooth for testing web_bluetooth/ # Python virtual environments venv/ /.vs/VSWorkspaceState.json /.vs/slnx.sqlite /.vs/ProjectSettings.json /.vs/python-bluezero/v15/.suo python-btsocket-0.3.0/LICENSE000066400000000000000000000020551463152142400156600ustar00rootroot00000000000000MIT License Copyright (c) 2021 Barry Byford Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-btsocket-0.3.0/README.rst000066400000000000000000000125211463152142400163410ustar00rootroot00000000000000============================== BlueZ Bluetooth Management API ============================== A Python library to interact with Bluez Bluetooth Management API on Linux. At this time it should be seen as a very early stage proof of concept. If you are new to Bluetooth this might not be the best library to start with Overview -------- This library aims to offer assistance to accessing the `BlueZ Bluetooth Management API `_ using Python. With the `mgmt` API there are no commands for reading and writing data on a connected device. This library has to have root privilege to access most things. Three Levels ------------ This library has tried to split things in to the how, what and when. The aim is by keeping the transport, protocol, and programming paradigm separate a plug and play approach can be taken. For example, if the Python bug in sockets gets fixed, just that part can be updated without too much disruption. It should also be possible to use different programming paradigms (models) for how commands and responses are handled and still use the socket and protocol pieces. Socket (How) ############ This library came into being because of a Bug in Python as documented at: https://bugs.python.org/issue36132 Python currently does not allow any way to initialize hci_channel, so you cannot use a `user channel` socket and so instead `btmgmt_socket` in **btsocket/btmgmt_socket.py** accesses the `user channel` by using the underlying libc socket. Protocol (What) ############### The file **btsocket/btmgmt_protocol.py** is to assist in encoding and decoding the binary format that is used to communicate This module assists in encoding and decoding the binary data Programming Paradigm (When) ########################### Handling communication with the sockets can be done a number of different ways and there are trade-offs for each of them. Initially this library is supporting two types. A procedural approach with **btsocket/btmgmt_sync.py** and a callback (or event-driven) approach with **btsocket/btmgmt_callback.py**. For actions like turning the controller on and off then these can be done with either methodology. For listening for async events like the discovery of devices, then only the callback model is practical. Commands -------- For the vast majority of the commands, the process of creating the mgmt socket is required to have the CAP_NET_ADMIN capability (e.g. root/sudo would have this). The documentation for commands is at: https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/mgmt-api.txt That documentation has been used to auto-generate parts of `btmgmt_protocol.py` To take one command as an example; powered command: :: Set Powered Command =================== Command Code: 0x0005 Controller Index: Command Parameters: Powered (1 Octet) Return Parameters: Current_Settings (4 Octets) To power-on adapter at index zero, the following command would be sent with the synchronous API .. code-block:: python from btsocket import btmgmt_sync response = btmgmt_sync.send('SetPowered', 0, 1) The format of the `send` command is : `response = send(, , )` The command name is taken from the heading in the documentation with the spaces and the word "Command" removed. A typical response is given below: :: Response( header=< event_code=CommandCompleteEvent, controller_idx=0, param_len=7>, event_frame=< command_opcode=SetPowered, status=Success>, cmd_response_frame=< current_settings=2752>) An example of the Python to access the values in the response is: .. code-block:: Python print(response.event_frame.command_opcode, response.event_frame.status) Callbacks on Events ------------------- The structure for running with callbacks on events is below. Getting the event loop and running until complete should be familiar to regular users of asyncio. `mgmt = btmgmt_callback.Mgmt()` sets up the sockets and the readers and writers to the sockets. `mgmt.add_event_callback` takes two arguments, the first is the btmgmt event and the second is the callback function to use when that event is detected. `mgmt.send` is how to send commands and is similar to the synchronous API except it doesn't get a response. You will have to add an event callback to access the response. The command(s) are not sent until `mgmt.start()` as this is what starts the writers and readers of the sockets. .. code-block:: Python from btsocket import btmgmt_callback from btsocket import btmgmt_protocol def device_found(response, mgmt_obj): print('New device found', response.event_frame.address) # To exit set running to False mgmt_obj.stop() def app(): mgmt = btmgmt_callback.Mgmt() mgmt.add_event_callback(btmgmt_protocol.Events.DeviceFoundEvent, device_found) mgmt.send('StartDiscovery', 0, [btmgmt_protocol.AddressType.LEPublic, btmgmt_protocol.AddressType.LERandom, btmgmt_protocol.AddressType.BREDR]) mgmt.start() if __name__ == '__main__': app() There are more examples in the examples folder python-btsocket-0.3.0/btsocket/000077500000000000000000000000001463152142400164675ustar00rootroot00000000000000python-btsocket-0.3.0/btsocket/__init__.py000066400000000000000000000000001463152142400205660ustar00rootroot00000000000000python-btsocket-0.3.0/btsocket/btmgmt_callback.py000066400000000000000000000077141463152142400221600ustar00rootroot00000000000000""" Use callback-based programming style to read and write to BlueZ Management API """ import asyncio from collections import deque from btsocket import btmgmt_socket from btsocket import btmgmt_protocol from btsocket import tools logger = tools.create_module_logger(__name__) class Mgmt: def __init__(self): # Setup read and write sockets self.sock = btmgmt_socket.open() self.loop = asyncio.get_event_loop() # Store for event callbacks self._event_callbacks = dict() # Queue for commands to be written to BlueZ socket self.cmd_queue = deque() self.running = False def add_event_callback(self, event, callback): """ Assign a callback to be called when a specific event happens. The callback should take two arguments. 1) The response packet 2) the AsyncMgmt() class instance object :param event: An entry from the enum btmgmt.Events :param callback: A callback function """ self._event_callbacks[event] = callback def reader(self): """ Read callback is called when data available on Bluetooth socket. Processes packet and hands-off to event callbacks that have subscribed to events. """ logger.debug('Reader callback') data = self.sock.recv(100) pkt = btmgmt_protocol.reader(data) logger.info('pkt: [%s]', pkt) if pkt.header.event_code in self._event_callbacks: self._event_callbacks[pkt.header.event_code](pkt, self) if not self.running: self.stop() def writer(self): """ Write callback when Bluetooth socket is available for writing. Takes commands that are on the cmd_queue and sends. """ logger.debug('Writer callback') if len(self.cmd_queue) > 0: this_cmd = self.cmd_queue.popleft() logger.info('sending pkt [%s]', tools.format_pkt(this_cmd)) self.sock.send(this_cmd) if not self.running and len(self.cmd_queue) == 0: self.loop.stop() # Do one more read to get the response from the last command self.reader() @staticmethod def _as_packet(pkt_objs): """Pack bytes together for sending""" full_pkt = b'' for frame in pkt_objs: if frame: full_pkt += frame.octets return full_pkt def send(self, cmd, ctrl_idx, *params): """ Add commands onto the queue ready to be sent. Basic structure of the command send(, , ) :param cmd: A value from btmgmt.Commands :param ctrl_idx: The index of the controller [0xFFFF is non-controller] :param params: 0 or more input parameters for command """ pkt_objs = btmgmt_protocol.command(cmd, ctrl_idx, *params) cmd_pkt = self._as_packet(pkt_objs) logger.debug('Queue command: %s', tools.format_pkt(cmd_pkt)) self.cmd_queue.append(cmd_pkt) def stop(self): """ Once all commands have been sent, exit the event loop """ self.running = False self.loop.remove_writer(self.sock) self.loop.remove_reader(self.sock) self.loop.stop() def close(self): """ Stop the event loop and close sockets etc. """ btmgmt_socket.close(self.sock) # Stop the event loop self.loop.close() def start(self): self.running = True # Setup reader and writer for socket streams self.loop.add_reader(self.sock, self.reader) self.loop.add_writer(self.sock, self.writer) logger.debug('Starting event loop...') try: # Run the event loop self.loop.run_forever() except KeyboardInterrupt: self.loop.stop() finally: # We are done. Close sockets and the event loop. self.close() python-btsocket-0.3.0/btsocket/btmgmt_protocol.py000066400000000000000000001336061463152142400222650ustar00rootroot00000000000000""" Hand the BlueZ Bluetooth Management (mgmt) API """ import abc from collections import namedtuple from enum import Enum import sys from btsocket import tools logger = tools.create_module_logger(__name__) current_module = sys.modules[__name__] Parameter = namedtuple('Parameter', ('name', 'width', 'repeat', 'bt_type'), defaults=(1, 'IntUL')) Response = namedtuple('Response', ('header', 'event_frame', 'cmd_response_frame'), defaults=(None,)) Command = namedtuple('Command', ('header', 'cmd_params_frame'), defaults=(None,)) class DataField(metaclass=abc.ABCMeta): def __init__(self): self.octets = b'' self.value = None def __repr__(self): return f'{self.value}' @abc.abstractmethod def decode(self, data): pass @abc.abstractmethod def encode(self, value, width): pass class Address(DataField): def decode(self, data): self.value = (f'{data[5]:02X}:{data[4]:02X}:{data[3]:02X}:' f'{data[2]:02X}:{data[1]:02X}:{data[0]:02X}') self.octets = data def encode(self, value, width): parts = value.split(':') for idx in range(5, -1, -1): self.octets += int(parts[idx], 16).to_bytes(1, byteorder='little') self.value = value class AddressTypeField(DataField): def decode(self, data): addr_types = [] as_int = int.from_bytes(data, byteorder='little', signed=False) for i in range(len(AddressType)): if (as_int >> i) & 1: addr_types.append(AddressType(i)) self.value = addr_types self.octets = data def encode(self, value, width): self.value = value bits = 0 for i in value: bits = bits | (1 << i.value) self.octets = int(bits).to_bytes(1, byteorder='little', signed=False) class IntUL(DataField): def decode(self, data): self.value = int.from_bytes(data, byteorder='little', signed=False) self.octets = data def encode(self, value, width): self.octets = int(value).to_bytes(width, byteorder='little', signed=False) self.value = value class HexStr(DataField): def decode(self, data): self.value = data.hex() self.octets = data def encode(self, value, width): self.octets = bytes.fromhex(value) self.value = value class CmdCode(DataField): def decode(self, data): self.value = Commands(int.from_bytes(data, byteorder='little')) self.octets = data def encode(self, value, width): cmd_code = Commands[value] self.octets = int(cmd_code.value).to_bytes(width, byteorder='little', signed=False) self.value = cmd_code class EvtCode(DataField): def decode(self, data): self.value = Events(int.from_bytes(data, byteorder='little')) self.octets = data def encode(self, value, width): evt_code = Events[value] self.octets = int(evt_code.value).to_bytes(width, byteorder='little', signed=False) self.value = evt_code class Status(DataField): def decode(self, data): self.value = ErrorCodes(int.from_bytes(data, byteorder='little')) self.octets = data def encode(self, value, width): status_code = ErrorCodes[value] self.octets = int(status_code.value).to_bytes(width, byteorder='little', signed=False) self.value = status_code class Controller(DataField): def decode(self, data): self.value = int.from_bytes(data, byteorder='little', signed=False) self.octets = data def encode(self, value, width): if value is None: value = 0xffff self.octets = int(value).to_bytes(width, byteorder='little', signed=False) self.value = value class ParamLen(DataField): def decode(self, data): self.value = int.from_bytes(data, byteorder='little', signed=False) self.octets = data def encode(self, value, width): self.value = value try: len_bytes = len(value) except IndexError: len_bytes = 0 self.octets = len_bytes.to_bytes(width, byteorder='little', signed=False) class Name(DataField): def decode(self, data): self.value = data.rstrip(b'\x00') self.octets = data def encode(self, value, width): self.value = value self.octets = value.ljust(width, b'\x00') class CurrentSettings(DataField): def decode(self, data): self.value = dict() as_int = int.from_bytes(data, byteorder='little', signed=False) for i in range(len(SupportedSettings)): self.value[SupportedSettings(i)] = bool((as_int >> i) & 1) self.octets = data def encode(self, value, width): raise NotImplementedError class EIRData(DataField): def decode(self, data): self.value = dict() pointer = 0 while pointer < len(data): len_data = data[pointer] data_type = data[pointer + 1] data_start = pointer + 2 data_end = data_start + len_data - 1 self.value[ADType(data_type)] = data[data_start:data_end] pointer += data[pointer] + 1 def encode(self, value, width): raise NotImplementedError class Packet: def __init__(self, shape): # e.g. # shape = (Parameter('opcode', 2), Parameter('status', 1)) self.shape = shape for param in shape: self.__setattr__(param.name, None) # params = b'\x01\x00\x00\x01\x0e\x00' self.octets = b'' def __repr__(self): key_values = ', '.join([f'{x.name}={self.__getattribute__(x.name)}' for x in self.shape]) return f'<{key_values}>' def _add_to_value(self, param, p_value): if param.repeat != 1: self.__getattribute__(param.name).append(p_value) else: self.__setattr__(param.name, p_value) def decode(self, pkt): self.octets = pkt pointer = 0 for param in self.shape: logger.debug('Decoding %s as type %s', param.name, param.bt_type) if param.repeat != 1: repeated = self.__getattribute__(param.repeat) self.__setattr__(param.name, list()) else: repeated = param.repeat for index in range(repeated): class_ = getattr(current_module, param.bt_type) data_type = class_() data_type.decode(pkt[pointer:pointer + param.width]) self._add_to_value(param, data_type.value) pointer += param.width if pointer < len(pkt): # self.value['parameters'] = pkt[pointer:] return pkt[pointer:] return None def encode(self, *args): self.octets = b'' cmd_args = args[0] for entry in range(len(self.shape)): param = self.shape[entry] logger.debug('Encoding %s as type %s', param.name, param.bt_type) class_ = getattr(current_module, param.bt_type) data_type = class_() if param.bt_type == 'ParamLen': try: data_type.encode(cmd_args[entry], param.width) except IndexError: data_type.encode(b'', param.width) else: data_type.encode(cmd_args[entry], param.width) self.octets += data_type.octets self._add_to_value(param, data_type.value) return cmd_args[2:] class EventHeader(Packet): def __init__(self): super().__init__([Parameter(name='event_code', width=2, bt_type='EvtCode'), Parameter(name='controller_idx', width=2, bt_type='Controller'), Parameter(name='param_len', width=2, bt_type='ParamLen')]) class CmdHeader(Packet): def __init__(self): super().__init__([Parameter(name='cmd_code', width=2, bt_type='CmdCode'), Parameter(name='controller_idx', width=2, bt_type='Controller'), Parameter(name='param_len', width=2, bt_type='ParamLen')]) class AddressType(Enum): def __str__(self): return str(self.name) def __repr__(self): return str(self.name) # Possible values for the Address_Type parameter are a bit-wise OR of # the following bits BREDR = 0x00 LEPublic = 0x01 LERandom = 0x02 class SupportedSettings(Enum): """ 0 Powered 1 Connectable 2 Fast Connectable 3 Discoverable 4 Bondable 5 Link Level Security (Sec. mode 3) 6 Secure Simple Pairing 7 Basic Rate/Enhanced Data Rate 8 High Speed 9 Low Energy 10 Advertising 11 Secure Connections 12 Debug Keys 13 Privacy 14 Controller Configuration 15 Static Address 16 PHY Configuration 17 Wideband Speech """ def __str__(self): return str(self.name) def __repr__(self): return str(self.name) Powered = 0x00 Connectable = 0x01 FastConnectable = 0x02 Discoverable = 0x03 Bondable = 0x04 LinkLevelSecurity = 0x05 SecureSimplePairing = 0x06 BREDR = 0x07 HighSpeed = 0x08 LowEnergy = 0x09 Advertising = 0x0A SecureConnections = 0x0B DebugKeys = 0x0C Privacy = 0x0D ControllerConfiguration = 0x0E StaticAddress = 0x0F PHYConfiguration = 0x10 WidebandSpeech = 0x11 class ADType(Enum): def __str__(self): return str(self.name) def __repr__(self): return str(self.name) Flags = 0x01 IncompleteUUID16ServiceList = 0x02 CompleteUUID16ServiceList = 0x03 CompleteUUID32ServiceList = 0x04 IncompleteUUID32ServiceList = 0x05 IncompleteUUID128ServiceList = 0x06 CompleteUUID128ServiceList = 0x07 ShortName = 0x08 CompleteName = 0x09 TXPower = 0x0a DeviceClass = 0x0d SimplePairingHashC192 = 0x0e SimplePairingRandomizer192 = 0x0f SecurityManagerTKValue = 0x10 SecurityManagerOOBFlags = 0x11 ConnectionIntervalRange = 0x12 SolicitUUID16ServiceList = 0x14 SolicitUUID128ServiceList = 0x15 ServiceDataUUID16 = 0x16 PublicTargetAddress = 0x17 RandomTargetAddress = 0x18 Appearance = 0x19 AdvertisingInterval = 0x1a LEDeviceAddress = 0x1b LERole = 0x1c SimplePairingHashC256 = 0x1d SimplePairingRandomizer256 = 0x1e SolicitUUID32ServiceList = 0x1f ServiceDataUUID32 = 0x20 ServiceDataUUID128 = 0x21 LESecureConnectionsConfirmationValue = 0x22 LESecureConnectionsRandomValue = 0x23 URI = 0x24 IndoorPositioning = 0x25 TransportDiscoverData = 0x26 LESupportedFeatures = 0x27 ChannelMapUpdateIndication = 0x28 PBADV = 0x29 MeshMessage = 0x2a MeshBeacon = 0x2b BIGInfo = 0x2c BroadcastCode = 0x2d InformationData3d = 0x3d ManufacturerData = 0xff class ErrorCodes(Enum): def __str__(self): return str(self.name) def __repr__(self): return str(self.name) Success = 0x00 UnknownCommand = 0x01 NotConnected = 0x02 Failed = 0x03 ConnectFailed = 0x04 AuthenticationFailed = 0x05 NotPaired = 0x06 NoResources = 0x07 Timeout = 0x08 AlreadyConnected = 0x09 Busy = 0x0A Rejected = 0x0B NotSupported = 0x0C InvalidParameters = 0x0D Disconnected = 0x0E NotPowered = 0x0F Cancelled = 0x10 InvalidIndex = 0x11 RFKilled = 0x12 AlreadyPaired = 0x13 PermissionDenied = 0x14 class Commands(Enum): def __str__(self): return str(self.name) def __repr__(self): return str(self.name) ReadManagementVersionInformation = 0x0001 ReadManagementSupportedCommands = 0x0002 ReadControllerIndexList = 0x0003 ReadControllerInformation = 0x0004 SetPowered = 0x0005 SetDiscoverable = 0x0006 SetConnectable = 0x0007 SetFastConnectable = 0x0008 SetBondable = 0x0009 SetLinkSecurity = 0x000A SetSecureSimplePairing = 0x000B SetHighSpeed = 0x000C SetLowEnergy = 0x000D SetDeviceClass = 0x000E SetLocalName = 0x000F AddUUID = 0x0010 RemoveUUID = 0x0011 LoadLinkKeys = 0x0012 LoadLongTermKeys = 0x0013 Disconnect = 0x0014 GetConnections = 0x0015 PINCodeReply = 0x0016 PINCodeNegativeReply = 0x0017 SetIOCapability = 0x0018 PairDevice = 0x0019 CancelPairDevice = 0x001A UnpairDevice = 0x001B UserConfirmationReply = 0x001C UserConfirmationNegativeReply = 0x001D UserPasskeyReply = 0x001E UserPasskeyNegativeReply = 0x001F ReadLocalOutOfBandData = 0x0020 AddRemoteOutOfBandData = 0x0021 RemoveRemoteOutOfBandData = 0x0022 StartDiscovery = 0x0023 StopDiscovery = 0x0024 ConfirmName = 0x0025 BlockDevice = 0x0026 UnblockDevice = 0x0027 SetDeviceID = 0x0028 SetAdvertising = 0x0029 SetBREDR = 0x002A SetStaticAddress = 0x002B SetScanParameters = 0x002C SetSecureConnections = 0x002D SetDebugKeys = 0x002E SetPrivacy = 0x002F LoadIdentityResolvingKeys = 0x0030 GetConnectionInformation = 0x0031 GetClockInformation = 0x0032 AddDevice = 0x0033 RemoveDevice = 0x0034 LoadConnectionParameters = 0x0035 ReadUnconfiguredControllerIndexList = 0x0036 ReadControllerConfigurationInformation = 0x0037 SetExternalConfiguration = 0x0038 SetPublicAddress = 0x0039 StartServiceDiscovery = 0x003a ReadLocalOutOfBandExtendedData = 0x003b ReadExtendedControllerIndexList = 0x003c ReadAdvertisingFeatures = 0x003d AddAdvertising = 0x003e RemoveAdvertising = 0x003f GetAdvertisingSizeInformation = 0x0040 StartLimitedDiscovery = 0x0041 ReadExtendedControllerInformation = 0x0042 SetAppearance = 0x0043 GetPHYConfiguration = 0x0044 SetPHYConfiguration = 0x0045 LoadBlockedKeys = 0x0046 SetWidebandSpeech = 0x0047 class Events(Enum): def __str__(self): return str(self.name) def __repr__(self): return str(self.name) CommandCompleteEvent = 0x0001 CommandStatusEvent = 0x0002 ControllerErrorEvent = 0x0003 IndexAddedEvent = 0x0004 IndexRemovedEvent = 0x0005 NewSettingsEvent = 0x0006 ClassOfDeviceChangedEvent = 0x0007 LocalNameChangedEvent = 0x0008 NewLinkKeyEvent = 0x0009 NewLongTermKeyEvent = 0x000A DeviceConnectedEvent = 0x000B DeviceDisconnectedEvent = 0x000C ConnectFailedEvent = 0x000D PINCodeRequestEvent = 0x000E UserConfirmationRequestEvent = 0x000F UserPasskeyRequestEvent = 0x0010 AuthenticationFailedEvent = 0x0011 DeviceFoundEvent = 0x0012 DiscoveringEvent = 0x0013 DeviceBlockedEvent = 0x0014 DeviceUnblockedEvent = 0x0015 DeviceUnpairedEvent = 0x0016 PasskeyNotifyEvent = 0x0017 NewIdentityResolvingKeyEvent = 0x0018 NewSignatureResolvingKeyEvent = 0x0019 DeviceAddedEvent = 0x001a DeviceRemovedEvent = 0x001b NewConnectionParameterEvent = 0x001c UnconfiguredIndexAddedEvent = 0x001d UnconfiguredIndexRemovedEvent = 0x001e NewConfigurationOptionsEvent = 0x001f ExtendedIndexAddedEvent = 0x0020 ExtendedIndexRemovedEvent = 0x0021 LocalOutOfBandExtendedDataUpdatedEvent = 0x0022 AdvertisingAddedEvent = 0x0023 AdvertisingRemovedEvent = 0x0024 ExtendedControllerInformationChangedEvent = 0x0025 PHYConfigurationChangedEvent = 0x0026 cmds = { 0x0005: Packet([Parameter(name='powered', width=1)]), 0x0006: Packet([Parameter(name='discoverable', width=1), Parameter(name='timeout', width=2)]), 0x0007: Packet([Parameter(name='connectable', width=1)]), 0x0008: Packet([Parameter(name='enable', width=1)]), 0x0009: Packet([Parameter(name='bondable', width=1)]), 0x000A: Packet([Parameter(name='link_security', width=1)]), 0x000B: Packet([Parameter(name='secure_simple_pairing', width=1)]), 0x000C: Packet([Parameter(name='high_speed', width=1)]), 0x000D: Packet([Parameter(name='low_energy', width=1)]), 0x000E: Packet([Parameter(name='major_class', width=1), Parameter(name='minor_class', width=1)]), 0x000F: Packet([Parameter(name='name', width=249, bt_type='Name'), Parameter(name='short_name', width=11)]), 0x0010: Packet([Parameter(name='uuid', width=16), Parameter(name='svc_hint', width=1)]), 0x0011: Packet([Parameter(name='uuid', width=16)]), 0x0012: Packet([Parameter(name='debug_keys', width=1), Parameter(name='key_count', width=2), Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='key_type', width=1), Parameter(name='value', width=16), Parameter(name='pin_length', width=1)]), 0x0013: Packet([Parameter(name='key_count', width=2), Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='key_type', width=1), Parameter(name='master', width=1), Parameter(name='encryption_size', width=1), Parameter(name='encryption_diversifier', width=2), Parameter(name='random_number', width=8), Parameter(name='value', width=16)]), 0x0014: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0016: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='pin_length', width=1), Parameter(name='pin_code', width=16)]), 0x0017: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0018: Packet([Parameter(name='io_capability', width=1)]), 0x0019: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='io_capability', width=1)]), 0x001A: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x001B: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='disconnect', width=1)]), 0x001C: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x001D: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x001E: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='passkey', width=4)]), 0x001F: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0021: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='hash_192', width=16), Parameter(name='randomizer_192', width=16), Parameter(name='hash_256', width=16), Parameter(name='randomizer_256', width=16)]), 0x0022: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0023: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0024: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0025: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='name_known', width=1)]), 0x0026: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0027: Packet([Parameter(name='address', width=6), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0028: Packet([Parameter(name='source', width=2), Parameter(name='vendor', width=2), Parameter(name='product', width=2), Parameter(name='version', width=2)]), 0x0029: Packet([Parameter(name='advertising', width=1)]), 0x002A: Packet([Parameter(name='br/edr', width=1)]), 0x002B: Packet([Parameter(name='address', width=6, bt_type='Address')]), 0x002C: Packet([Parameter(name='interval', width=2), Parameter(name='window', width=2)]), 0x002D: Packet([Parameter(name='secure_connections', width=1)]), 0x002E: Packet([Parameter(name='debug_keys', width=1)]), 0x002F: Packet([Parameter(name='privacy', width=1), Parameter(name='identity_resolving_key', width=16)]), 0x0030: Packet([Parameter(name='key_count', width=2), Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='value', width=16)]), 0x0031: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0032: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0033: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='action', width=1)]), 0x0034: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0035: Packet([Parameter(name='param_count', width=2), Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='min_connection_interval', width=2), Parameter(name='max_connection_interval', width=2), Parameter(name='connection_latency', width=2), Parameter(name='supervision_timeout', width=2)]), 0x0038: Packet([Parameter(name='configuration', width=1)]), 0x0039: Packet([Parameter(name='address', width=6, bt_type='Address')]), 0x003a: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='rssi_threshold', width=1), Parameter(name='uuid_count', width=2), Parameter(name='uuid[i]', width=16, repeat='uuid_count')]), 0x003b: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x003e: Packet( [ Parameter(name='instance', width=1), Parameter(name='flags', width=4), Parameter(name='duration', width=2), Parameter(name='timeout', width=2), Parameter(name='adv_data_len', width=1), Parameter(name='scan_rsp_len', width=1), Parameter( name='adv_data', width=None, repeat=1, bt_type='HexStr'), Parameter( name='scan_rsp', width=None, repeat=1, bt_type='HexStr') ] ), 0x003f: Packet([Parameter(name='instance', width=1)]), 0x0040: Packet([Parameter(name='instance', width=1), Parameter(name='flags', width=4)]), 0x0041: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0043: Packet([Parameter(name='appearance', width=2)]), 0x0045: Packet([Parameter(name='selected_phys', width=4)]), 0x0046: Packet([Parameter(name='key_count', width=2), Parameter(name='key_type', width=1), Parameter(name='value', width=16)]), 0x0047: Packet([Parameter(name='wideband_speech', width=1)]), } events = { 0x0001: Packet([Parameter(name='command_opcode', width=2, bt_type='CmdCode'), Parameter(name='status', width=1, bt_type='Status')]), 0x0002: Packet([Parameter(name='command_opcode', width=2, bt_type='CmdCode'), Parameter(name='status', width=1, bt_type='Status')]), 0x0003: Packet([Parameter(name='error_code', width=1)]), 0x0006: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x0007: Packet([Parameter(name='class_of_device', width=3)]), 0x0008: Packet([Parameter(name='name', width=249, bt_type='Name'), Parameter(name='short_name', width=11)]), 0x0009: Packet([Parameter(name='store_hint', width=1), Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='key_type', width=1), Parameter(name='value', width=16), Parameter(name='pin_length', width=1)]), 0x000A: Packet([Parameter(name='store_hint', width=1), Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='key_type', width=1), Parameter(name='master', width=1), Parameter(name='size', width=1), Parameter(name='diversifier', width=2), Parameter(name='number', width=8), Parameter(name='value', width=16)]), 0x000B: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='flags', width=4), Parameter(name='eir_data_length', width=2), Parameter(name='eir_data', width=65535, bt_type='EIRData')]), 0x000C: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='reason', width=1)]), 0x000D: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='status', width=1, bt_type='Status')]), 0x000E: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='secure', width=1)]), 0x000F: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='confirm_hint', width=1), Parameter(name='value', width=4)]), 0x0010: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0011: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='status', width=1, bt_type='Status')]), 0x0012: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='rssi', width=1), Parameter(name='flags', width=4), Parameter(name='eir_data_length', width=2), Parameter(name='eir_data', width=65535, bt_type='EIRData')]), 0x0013: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='discovering', width=1)]), 0x0014: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0015: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0016: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0017: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='passkey', width=4), Parameter(name='entered', width=1)]), 0x0018: Packet([Parameter(name='store_hint', width=1), Parameter(name='random_address', width=6), Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='value', width=16)]), 0x0019: Packet([Parameter(name='store_hint', width=1), Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='type', width=1), Parameter(name='value', width=16)]), 0x001a: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='action', width=1)]), 0x001b: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x001c: Packet([Parameter(name='store_hint', width=1), Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='min_connection_interval', width=2), Parameter(name='max_connection_interval', width=2), Parameter(name='connection_latency', width=2), Parameter(name='supervision_timeout', width=2)]), 0x001f: Packet([Parameter(name='missing_options', width=4)]), 0x0020: Packet([Parameter(name='controller_type', width=1), Parameter(name='controller_bus', width=1)]), 0x0021: Packet([Parameter(name='controller_type', width=1), Parameter(name='controller_bus', width=1)]), 0x0022: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='eir_data_length', width=2), Parameter(name='eir_data', width=65535, bt_type='EIRData')]), 0x0023: Packet([Parameter(name='instance', width=1)]), 0x0024: Packet([Parameter(name='instance', width=1)]), 0x0025: Packet([Parameter(name='eir_data_length', width=2), Parameter(name='eir_data', width=65535, bt_type='EIRData')]), 0x0026: Packet([Parameter(name='selected_phys', width=4)]), } cmd_response = { 0x0001: Packet([Parameter(name='version', width=1), Parameter(name='revision', width=2)]), 0x0002: Packet([Parameter(name='num_of_commands', width=2), Parameter(name='num_of_events', width=2), Parameter(name='command', width=2, repeat='num_of_commands'), Parameter(name='event', width=2, repeat='num_of_events')]), 0x0003: Packet([Parameter(name='num_controllers', width=2), Parameter(name='controller_index[i]', width=2, repeat='num_controllers')]), 0x0004: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='bluetooth_version', width=1), Parameter(name='manufacturer', width=2), Parameter(name='supported_settings', width=4), Parameter(name='current_settings', width=4, bt_type='CurrentSettings'), Parameter(name='class_of_device', width=3), Parameter(name='name', width=249, bt_type='Name'), Parameter(name='short_name', width=11)]), 0x0005: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x0006: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x0007: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x0008: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x0009: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x000A: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x000B: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x000C: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x000D: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x000E: Packet([Parameter(name='class_of_device', width=3)]), 0x000F: Packet([Parameter(name='name', width=249, bt_type='Name'), Parameter(name='short_name', width=11)]), 0x0010: Packet([Parameter(name='class_of_device', width=3)]), 0x0011: Packet([Parameter(name='class_of_device', width=3)]), 0x0014: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0015: Packet([Parameter(name='connection_count', width=2), Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0016: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0017: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0019: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x001A: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x001B: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x001C: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x001D: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x001E: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x001F: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0020: Packet([Parameter(name='hash_192', width=16), Parameter(name='randomizer_192', width=16), Parameter(name='hash_256', width=16), Parameter(name='randomizer_256', width=16)]), 0x0021: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0022: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0023: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0024: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0025: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0026: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0027: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0029: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x002A: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x002B: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x002D: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x002E: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x002F: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), 0x0031: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='rssi', width=1), Parameter(name='tx_power', width=1), Parameter(name='max_tx_power', width=1)]), 0x0032: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='local_clock', width=4), Parameter(name='piconet_clock', width=4), Parameter(name='accuracy', width=2)]), 0x0033: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0034: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0036: Packet([Parameter(name='num_controllers', width=2), Parameter(name='controller_index[i]', width=2)]), 0x0037: Packet([Parameter(name='manufacturer', width=2), Parameter(name='supported_options', width=4), Parameter(name='missing_options', width=4)]), 0x0038: Packet([Parameter(name='missing_options', width=4)]), 0x0039: Packet([Parameter(name='missing_options', width=4)]), 0x003a: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x003b: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField'), Parameter(name='eir_data_length', width=2), Parameter(name='eir_data', width=65535, bt_type='EIRData')]), 0x003c: Packet([Parameter(name='num_controllers', width=2), Parameter(name='controller_index', width=2, repeat='num_controllers'), Parameter(name='controller_type', width=1, repeat='num_controllers'), Parameter(name='controller_bus', width=1, repeat='num_controllers')]), 0x003d: Packet([Parameter(name='supported_flags', width=4), Parameter(name='max_adv_data_len', width=1), Parameter(name='max_scan_rsp_len', width=1), Parameter(name='max_instances', width=1), Parameter(name='num_instances', width=1), Parameter(name='instance[i]', width=1, repeat='num_instances')]), 0x003e: Packet([Parameter(name='instance', width=1)]), 0x003f: Packet([Parameter(name='instance', width=1)]), 0x0040: Packet([Parameter(name='instance', width=1), Parameter(name='flags', width=4), Parameter(name='max_adv_data_len', width=1), Parameter(name='max_scan_rsp_len', width=1)]), 0x0041: Packet([Parameter(name='address_type', width=1, bt_type='AddressTypeField')]), 0x0042: Packet([Parameter(name='address', width=6, bt_type='Address'), Parameter(name='bluetooth_version', width=1), Parameter(name='manufacturer', width=2), Parameter(name='supported_settings', width=4), Parameter(name='current_settings', width=4, bt_type='CurrentSettings'), Parameter(name='eir_data_length', width=2), Parameter(name='eir_data', width=65535, bt_type='EIRData')]), 0x0044: Packet([Parameter(name='supported_phys', width=4), Parameter(name='configurable_phys', width=4), Parameter(name='selected_phys', width=4)]), 0x0047: Packet([Parameter(name='current_settings', width=4, bt_type='CurrentSettings')]), } def reader(pckt): # <- Response packet -> # <- event_header ->|<- event_frame -> # <- event_header ->|<- event_frame ->|<- cmd_response_frame -> # # <- Command Packet -> # <- cmd_header ->|<- cmd_frame -> header = EventHeader() evt_params = header.decode(pckt) event_frame = events.get(header.event_code.value) cmd_params = event_frame.decode(evt_params) if cmd_params: cmd_response_frame = cmd_response.get(event_frame.command_opcode.value) cmd_response_frame.decode(cmd_params) logger.debug('Socket Read: %s %s %s', header, event_frame, cmd_response_frame) return Response(header, event_frame, cmd_response_frame) logger.debug('Socket read %s %s', header, event_frame) return Response(header, event_frame) def command(*args): header = CmdHeader() if len(args) == 2: header.encode(args) return Command(header) cmd_frame = cmds.get(Commands[args[0]].value) cmd_frame.encode(args[2:]) header.encode((args[0], args[1], cmd_frame.octets)) return Command(header, cmd_frame) python-btsocket-0.3.0/btsocket/btmgmt_socket.py000066400000000000000000000057001463152142400217050ustar00rootroot00000000000000import asyncio import ctypes import socket import sys AF_BLUETOOTH = 31 PF_BLUETOOTH = AF_BLUETOOTH SOCK_RAW = 3 BTPROTO_HCI = 1 SOCK_CLOEXEC = 524288 SOCK_NONBLOCK = 2048 HCI_CHANNEL_CONTROL = 3 HCI_DEV_NONE = 0xffff class BluetoothSocketError(BaseException): pass class BluetoothCommandError(BaseException): pass class SocketAddr(ctypes.Structure): _fields_ = [ ("hci_family", ctypes.c_ushort), ("hci_dev", ctypes.c_ushort), ("hci_channel", ctypes.c_ushort), ] def open(): """ Because of the following issue with Python the Bluetooth User socket on linux needs to be done with lower level calls. https://bugs.python.org/issue36132 Based on mgmt socket at: https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/mgmt-api.txt """ sockaddr_hcip = ctypes.POINTER(SocketAddr) ctypes.cdll.LoadLibrary("libc.so.6") libc = ctypes.CDLL("libc.so.6") libc_socket = libc.socket libc_socket.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.c_int) libc_socket.restype = ctypes.c_int bind = libc.bind bind.argtypes = (ctypes.c_int, ctypes.POINTER(SocketAddr), ctypes.c_int) bind.restype = ctypes.c_int # fd = libc_socket(PF_BLUETOOTH, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, # BTPROTO_HCI) fd = libc_socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI) if fd < 0: raise BluetoothSocketError("Unable to open PF_BLUETOOTH socket") addr = SocketAddr() addr.hci_family = AF_BLUETOOTH # AF_BLUETOOTH addr.hci_dev = HCI_DEV_NONE # adapter index addr.hci_channel = HCI_CHANNEL_CONTROL # HCI_USER_CHANNEL r = bind(fd, sockaddr_hcip(addr), ctypes.sizeof(addr)) if r < 0: raise BluetoothSocketError("Unable to bind %s", r) sock_fd = socket.socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI, fileno=fd) return sock_fd def close(bt_socket): """Close the open socket""" fd = bt_socket.detach() socket.close(fd) def test_asyncio_usage(): sock = open() if sys.version_info < (3, 10): loop = asyncio.get_event_loop() else: try: loop = asyncio.get_running_loop() except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) def reader(): data = sock.recv(100) print("Received:", data) # We are done: unregister the file descriptor loop.remove_reader(sock) # Stop the event loop loop.stop() # Register the file descriptor for read event loop.add_reader(sock, reader) # Write a command to the socket # Read Management Version Information Command # b'\x01\x00\xff\xff\x00\x00' loop.call_soon(sock.send, b'\x01\x00\xff\xff\x00\x00') try: # Run the event loop loop.run_forever() finally: # We are done. Close sockets and the event loop. close(sock) loop.close() if __name__ == '__main__': test_asyncio_usage() python-btsocket-0.3.0/btsocket/btmgmt_sync.py000066400000000000000000000022671463152142400213760ustar00rootroot00000000000000from btsocket import btmgmt_protocol from btsocket import btmgmt_socket from btsocket import tools logger = tools.create_module_logger(__name__) def _as_packet(pkt_objs): full_pkt = b'' for frame in pkt_objs: if frame: full_pkt += frame.octets return full_pkt def send(*args): response_recvd = False pkt_objs = btmgmt_protocol.command(*args) logger.debug('Sending btmgmt frames %s', pkt_objs) cmd_pkt = _as_packet(pkt_objs) # print('cmd pkt', [f'{octets:x}' for octets in cmd_pkt]) sock = btmgmt_socket.open() logger.debug('Sending bytes: %s', cmd_pkt) sock.send(cmd_pkt) while not response_recvd: raw_data = sock.recv(100) logger.debug('Received: %s', raw_data) data = btmgmt_protocol.reader(raw_data) logger.debug('Received btmgmt frames: %s', data) if data.cmd_response_frame: response_recvd = True btmgmt_socket.close(sock) if data.event_frame.status != btmgmt_protocol.ErrorCodes.Success: raise NameError(f'btmgmt Error: ' f'{data.event_frame.command_opcode} ' f'{data.event_frame.status.name}') return data python-btsocket-0.3.0/btsocket/tools.py000066400000000000000000000011121463152142400201740ustar00rootroot00000000000000"""Location for tools/functions to be used across various files""" import logging def create_module_logger(module_name): """helper function to create logger in modules""" logger = logging.getLogger(module_name) strm_hndlr = logging.StreamHandler() formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s') strm_hndlr.setFormatter(formatter) logger.addHandler(strm_hndlr) return logger def format_pkt(data): """Put data packets in a human readable format""" return ', '.join([f'{bite:#04x}' for bite in data]) python-btsocket-0.3.0/examples/000077500000000000000000000000001463152142400164675ustar00rootroot00000000000000python-btsocket-0.3.0/examples/callback_connect.py000066400000000000000000000032251463152142400223100ustar00rootroot00000000000000import logging from btsocket import btmgmt_callback from btsocket import btmgmt_protocol device_addr = 'E5:10:5E:37:11:2D' # btmgmt_callback.logger.setLevel(logging.INFO) # btmgmt_protocol.logger.setLevel(logging.DEBUG) def connect(mgmt_class): mgmt_class.send('ReadManagementVersionInformation', 0xffff) mgmt_class.send('AddDevice', 0, device_addr, [btmgmt_protocol.AddressType.LEPublic], 2) mgmt_class.send('StartDiscovery', 0, [btmgmt_protocol.AddressType.LEPublic]) def disconnect(mgmt): mgmt.send('RemoveDevice', 0, device_addr, [btmgmt_protocol.AddressType.LEPublic]) mgmt.send('Disconnect', 0, device_addr, [btmgmt_protocol.AddressType.LEPublic]) def on_connected(data, mgmt_class): if data.event_frame.address == device_addr: print('Connected...') mgmt_class.send('StopDiscovery', 0, [btmgmt_protocol.AddressType.LEPublic]) mgmt_class.loop.call_later(10, disconnect, mgmt_class) def on_disconnect(data, mgmt_class): if data.event_frame.command_opcode == btmgmt_protocol.Commands.Disconnect: print('Disconnected...') mgmt_class.stop() print('Exiting...') def app(): mgmt = btmgmt_callback.Mgmt() mgmt.add_event_callback(btmgmt_protocol.Events.DeviceConnectedEvent, on_connected) mgmt.add_event_callback(btmgmt_protocol.Events.CommandCompleteEvent, on_disconnect) connect(mgmt) try: mgmt.start() except KeyboardInterrupt: mgmt.stop() finally: mgmt.close() if __name__ == '__main__': app() python-btsocket-0.3.0/examples/callback_get_beacon_data.py000066400000000000000000000050111463152142400237310ustar00rootroot00000000000000import logging from btsocket import btmgmt_callback from btsocket import btmgmt_protocol scan_loop = 0 max_scans = 4 beacon_address = 'DC:76:F7:E1:62:E0' logger = logging.getLogger('btsocket.btmgmt_protocol') logger.setLevel(logging.INFO) def print_sensor_data(data): """ Function to print sensor beacon data in pretty format """ if len(data) == 14: battery_lvl = data[1] inst_temp = int.from_bytes(data[6:8], byteorder='big', signed=True) / 10 humidity = int.from_bytes(data[8:10], byteorder='big', signed=True) / 10 print(f'\tTemperature: {inst_temp}\u00B0C') print(f'\tHumidity: {humidity}%') print(f'\tBattery Level: {battery_lvl}%') def close_loop(data, mgmt_class): """ Callback for when discovering event has happened. In this example check to see how many times discovery has timed out. If it is `max_scans then exit :param data: discovering event packet :param mgmt_class: Mgmt class object to access enums etc """ global scan_loop if data.event_frame.discovering == 0: print(f'Discovering loop {scan_loop} ended') scan_loop += 1 if scan_loop < max_scans: mgmt_class.send('StartDiscovery', 0, [btmgmt_protocol.AddressType.LEPublic, btmgmt_protocol.AddressType.LERandom]) else: mgmt_class.stop() def device_found(data, mgmt_class): """ Callback for when device is found :param data: Device found event packet :param mgmt_class: Mgmt class object to access enums etc """ if data.event_frame.address == beacon_address: eir_data = data.event_frame.eir_data manuf_data = eir_data.get(btmgmt_protocol.ADType.ManufacturerData) if all((manuf_data, manuf_data.startswith(b'\x33\01'), len(manuf_data) == 16)): print_sensor_data(manuf_data[2:]) mgmt_class.stop() def app(): mgmt = btmgmt_callback.Mgmt() mgmt.add_event_callback(btmgmt_protocol.Events.DiscoveringEvent, close_loop) mgmt.add_event_callback(btmgmt_protocol.Events.DeviceFoundEvent, device_found) mgmt.send('StartDiscovery', 0, [btmgmt_protocol.AddressType.LEPublic, btmgmt_protocol.AddressType.LERandom]) print('Starting search...') mgmt.start() if __name__ == '__main__': app() python-btsocket-0.3.0/examples/create_advertisement_btmgmt_socket.py000066400000000000000000000052751463152142400261710ustar00rootroot00000000000000import asyncio import enum import sys from btsocket import btmgmt_socket from btsocket import btmgmt_protocol class Flags(enum.IntEnum): CONNECTABLE = enum.auto() GENERAL_DISCOVERABLE = enum.auto() LIMITED_DISCOVERABLE = enum.auto() FLAGS_IN_ADV_DATA = enum.auto() TX_IN_ADV_DATA = enum.auto() APPEARANCE_IN_ADV_DATA = enum.auto() LOCAL_NAME_IN_ADV_DATA = enum.auto() PHY_LE_1M = enum.auto() PHY_LE_2M = enum.auto() PHY_LE_CODED = enum.auto() def little_bytes(value, size_of): return int(value).to_bytes(size_of, byteorder='little') def advert_command(instance_id, flags, duration, timeout, adv_data, scan_rsp): cmd = little_bytes(0x003e, 2) ctrl_idx = little_bytes(0x00, 2) instance = little_bytes(instance_id, 1) # (1 Octet) flags = little_bytes(flags, 4) # (4 Octets) duration = little_bytes(duration, 2) # (2 Octets) timeout = little_bytes(timeout, 2) # (2 Octets) adv_data = bytes.fromhex(adv_data) # (0-255 Octets) adv_data_len = little_bytes(len(adv_data), 1) # (1 Octet) scan_rsp = bytes.fromhex(scan_rsp) # (0-255 Octets) scan_rsp_len = little_bytes(len(scan_rsp), 1) # (1 Octet) params = (instance + flags + duration + timeout + adv_data_len + scan_rsp_len + adv_data + scan_rsp) param_len = little_bytes(len(params), 2) return cmd + ctrl_idx + param_len + params def test_asyncio_usage(): sock = btmgmt_socket.open() if sys.version_info < (3, 10): loop = asyncio.get_event_loop() else: try: loop = asyncio.get_running_loop() except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) def reader(): raw_data = sock.recv(100) data = btmgmt_protocol.reader(raw_data) print("Received:", data.event_frame.command_opcode, data.event_frame.status) # We are done: unregister the file descriptor loop.remove_reader(sock) # Stop the event loop loop.stop() # Register the file descriptor for read event loop.add_reader(sock, reader) # Write a command to the socket loop.call_soon(sock.send, advert_command( instance_id=1, flags=Flags.GENERAL_DISCOVERABLE, duration=0x00, # zero means use default timeout=0x00, # zero means use default adv_data='1bfff0ff6DB643CF7E8F471188665938D17AAA26495E131415161718', scan_rsp='', )) try: # Run the event loop loop.run_forever() finally: # We are done. Close sockets and the event loop. btmgmt_socket.close(sock) loop.close() if __name__ == '__main__': test_asyncio_usage() python-btsocket-0.3.0/examples/create_advertisement_callback.py000066400000000000000000000027561463152142400250640ustar00rootroot00000000000000import enum import logging from btsocket import btmgmt_callback from btsocket import btmgmt_protocol # logger = logging.getLogger("btsocket.btmgmt_callback") # logger.setLevel(logging.DEBUG) logger_prot = logging.getLogger('btsocket.btmgmt_protocol') logger_prot.setLevel(logging.INFO) class Flags(enum.IntEnum): CONNECTABLE = enum.auto() GENERAL_DISCOVERABLE = enum.auto() LIMITED_DISCOVERABLE = enum.auto() FLAGS_IN_ADV_DATA = enum.auto() TX_IN_ADV_DATA = enum.auto() APPEARANCE_IN_ADV_DATA = enum.auto() LOCAL_NAME_IN_ADV_DATA = enum.auto() PHY_LE_1M = enum.auto() PHY_LE_2M = enum.auto() PHY_LE_CODED = enum.auto() ctrl_idx = 0 # hci0 instance = 1 # Arbitrary value flags = Flags.GENERAL_DISCOVERABLE duration = 0x00 # 0 means use default timeout = 0x0 # 0 means use default adv_data = "1bfff0ff6DB643CF7E8F471188665938D17AAA26495E131415161718" adv_data_len = len(bytes.fromhex(adv_data)) scan_rsp = "" # (0-255 Octets) scan_rsp_len = len(bytes.fromhex(scan_rsp)) def show_result(pkt, mgmt_class): print(pkt) def main(): mgmt = btmgmt_callback.Mgmt() mgmt.add_event_callback( btmgmt_protocol.Events.CommandCompleteEvent, show_result) mgmt.send('AddAdvertising', ctrl_idx, instance, flags, duration, timeout, adv_data_len, scan_rsp_len, adv_data, scan_rsp) mgmt.start() if __name__ == '__main__': main() python-btsocket-0.3.0/examples/create_advertisement_sync.py000066400000000000000000000025261463152142400242770ustar00rootroot00000000000000import enum import logging from btsocket import btmgmt_sync logger = logging.getLogger("btsocket.btmgmt_protocol") logger.setLevel(logging.INFO) class Flags(enum.IntEnum): CONNECTABLE = enum.auto() GENERAL_DISCOVERABLE = enum.auto() LIMITED_DISCOVERABLE = enum.auto() FLAGS_IN_ADV_DATA = enum.auto() TX_IN_ADV_DATA = enum.auto() APPEARANCE_IN_ADV_DATA = enum.auto() LOCAL_NAME_IN_ADV_DATA = enum.auto() PHY_LE_1M = enum.auto() PHY_LE_2M = enum.auto() PHY_LE_CODED = enum.auto() ctrl_idx = 0 # hci0 instance = 1 # Arbitrary value flags = Flags.GENERAL_DISCOVERABLE duration = 0x00 # 0 means use default timeout = 0x0 # 0 means use default adv_data = "1bfff0ff6DB643CF7E8F471188665938D17AAA26495E131415161718" adv_data_len = len(bytes.fromhex(adv_data)) scan_rsp = "" # (0-255 Octets) scan_rsp_len = len(bytes.fromhex(scan_rsp)) def show_result(data): print(data.event_frame.command_opcode, data.event_frame.status) show_result( btmgmt_sync.send( "AddAdvertising", ctrl_idx, instance, flags, duration, timeout, adv_data_len, scan_rsp_len, adv_data, scan_rsp, ) ) input("Press key to end advertisement") show_result( btmgmt_sync.send( "RemoveAdvertising", ctrl_idx, instance, ) ) python-btsocket-0.3.0/examples/sync_connect_device.py000066400000000000000000000025331463152142400230500ustar00rootroot00000000000000import logging from time import sleep from btsocket import btmgmt_sync from btsocket import btmgmt_protocol btmgmt_sync.logger.setLevel(logging.INFO) def show_result(data): print(data.event_frame.command_opcode, data.event_frame.status) show_result(btmgmt_sync.send('AddDevice', 0, 'E5:10:5E:37:11:2D', [btmgmt_protocol.AddressType.LEPublic], 2)) show_result(btmgmt_sync.send('StartDiscovery', 0, [btmgmt_protocol.AddressType.BREDR, btmgmt_protocol.AddressType.LERandom, btmgmt_protocol.AddressType.LEPublic])) sleep(4) success = False while not success: try: show_result(btmgmt_sync.send('StopDiscovery', 0, [btmgmt_protocol.AddressType.BREDR, btmgmt_protocol.AddressType.LERandom, btmgmt_protocol.AddressType.LEPublic])) except NameError: print('Attempt stop discovery again...') finally: success = True sleep(1) show_result(btmgmt_sync.send('RemoveDevice', 0, 'E5:10:5E:37:11:2D', [btmgmt_protocol.AddressType.LEPublic])) show_result(btmgmt_sync.send('Disconnect', 0, 'E5:10:5E:37:11:2D', [btmgmt_protocol.AddressType.LEPublic])) python-btsocket-0.3.0/examples/sync_list_commands.py000066400000000000000000000011411463152142400227260ustar00rootroot00000000000000from btsocket import btmgmt_sync data = btmgmt_sync.send('ReadManagementSupportedCommands', None) print(f'Raw response: {data}') print('Command Code | Command Name | Command Parameters') print('=' * 65) for cmd in data.cmd_response_frame.command: try: params = btmgmt_sync.btmgmt_protocol.cmds[cmd] required_params = [param.name for param in params.shape] except KeyError: required_params = None try: print(f'{cmd:<12} | {btmgmt_sync.btmgmt_protocol.Commands(cmd):30} | ' f'{required_params}') except ValueError: pass python-btsocket-0.3.0/examples/sync_power_on_off.py000066400000000000000000000013721463152142400225620ustar00rootroot00000000000000import logging from btsocket import btmgmt_sync from btsocket import tools def show_result(response): from btsocket import btmgmt_protocol print(response.event_frame.command_opcode, '-', response.cmd_response_frame.current_settings.get( btmgmt_protocol.SupportedSettings.Powered), '-', response.event_frame.status) logger = logging.getLogger('btsocket.btmgmt_sync') logger.setLevel(logging.INFO) data = btmgmt_sync.send('ReadManagementVersionInformation', None) print(f'Bluez mgmt Version: ' f'{data.cmd_response_frame.version}.{data.cmd_response_frame.revision}') data = btmgmt_sync.send('SetPowered', 0, 0) show_result(data) data = btmgmt_sync.send('SetPowered', 0, 1) show_result(data) python-btsocket-0.3.0/run_local_tests.sh000077500000000000000000000007101463152142400204060ustar00rootroot00000000000000#!/usr/bin/env bash # Ensure Python virtual environment is loaded . venv/bin/activate pycodestyle -v btsocket lint_btsocket=$? pycodestyle -v examples lint_examples=$? coverage run -m unittest discover -v tests tests_result=$? coverage report coverage html echo file://`pwd`/htmlcov/index.html if [ $((lint_btsocket + lint_examples + tests_result)) -ne 0 ]; then echo -e "\n\n### A test has failed!! ###\n" else echo -e "\n\nSuccess!!!\n" fi python-btsocket-0.3.0/setup.cfg000066400000000000000000000003251463152142400164720ustar00rootroot00000000000000[bumpversion] current_version = 0.3.0 tag = True tag_name = v{new_version} [bdist_wheel] universal = 1 [bumpversion:file:setup.py] [coverage:run] source = btsocket, examples [metadata] license_files = LICENSE python-btsocket-0.3.0/setup.py000066400000000000000000000103611463152142400163640ustar00rootroot00000000000000""" A setuptools based setup module. Based on the example at: https://packaging.python.org/en/latest/distributing.html https://github.com/pypa/sampleproject """ # Always prefer setuptools over distutils from setuptools import setup # To use a consistent encoding from codecs import open from os import path here = path.abspath(path.dirname(__file__)) required_packages = [] extras_rel = ['bumpversion', 'twine'] extras_doc = ['sphinx', 'sphinx_rtd_theme', 'pygments'] extras_test = ['coverage', 'pycodestyle'] extras_dev = extras_rel + extras_doc + extras_test # Get the long description from the README file with open(path.join(here, 'README.rst'), encoding='utf-8') as f: long_description = f.read() setup( name='btsocket', # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html version='0.3.0', description='Python library for BlueZ Bluetooth Management API', long_description=long_description, long_description_content_type='text/x-rst', # The project's main homepage. url='https://github.com/ukBaz/python-btsocket', # Author details author='Barry Byford', author_email='barry_byford@yahoo.co.uk', maintainer='Barry Byford', maintainer_email='barry_byford@yahoo.co.uk', # Choose your license license='MIT', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ # How mature is this project? Common values are # 3 - Alpha # 4 - Beta # 5 - Production/Stable 'Development Status :: 3 - Alpha', # Indicate who your project is intended for 'Intended Audience :: Developers', 'Intended Audience :: Education', 'Topic :: System :: Hardware', 'Topic :: Software Development :: Embedded Systems', 'Topic :: Home Automation', 'Topic :: Education', # Pick your license as you wish (should match "license" above) 'License :: OSI Approved :: MIT License', # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python :: 3 :: Only' ], # What does your project relate to? keywords='BlueZ Bluetooth Management MGMT API', # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). packages=['btsocket'], # Alternatively, if you want to distribute just a my_module.py, uncomment # this: # py_modules=["my_module"], # List run-time dependencies here. These will be installed by pip when # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html install_requires=required_packages, # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax, # for example: # $ pip install -e .[dev,test] extras_require={ 'rel': extras_rel, 'docs': extras_doc, 'test': extras_test, 'dev': extras_dev, }, test_suite='tests', # If there are data files included in your packages that need to be # installed, specify them here. If using Python 2.6 or less, then these # have to be included in MANIFEST.in as well. # package_data={ # 'sample': ['package_data.dat'], # }, # Although 'package_data' is the preferred approach, in some case you may # need to place data files outside of your packages. See: # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa # In this case, 'data_file' will be installed into '/my_data' # data_files=[('my_data', ['data/data_file'])], # To provide executable scripts, use entry points in preference to the # "scripts" keyword. Entry points provide cross-platform support and allow # pip to create the appropriate form of executable for the target platform. # entry_points={ # 'console_scripts': [ # 'sample=sample:main', # ], # }, ) python-btsocket-0.3.0/tests/000077500000000000000000000000001463152142400160135ustar00rootroot00000000000000python-btsocket-0.3.0/tests/test_cmds.py000066400000000000000000000027401463152142400203550ustar00rootroot00000000000000import unittest from btsocket import btmgmt_protocol class TestCmdReponse(unittest.TestCase): def test_version_cmd(self): expected = b'\x01\x00\xff\xff\x00\x00' pkt = btmgmt_protocol.command('ReadManagementVersionInformation', None) self.assertEqual(expected, pkt.header.octets) def test_disconnect_cmd(self): expected = b'\x14\x00\x00\x00\x07\x00\x2d\x11\x37\x5e\x10\xe5\x01' pkt = btmgmt_protocol.command('Disconnect', 0, 'E5:10:5E:37:11:2D', [btmgmt_protocol.AddressType.BREDR]) self.assertEqual(expected, pkt.header.octets + pkt.cmd_params_frame.octets) def test_power_on_cmd(self): expected = b'\x05\x00\x00\x00\x01\x00\x01' pkt = btmgmt_protocol.command('SetPowered', 0, 1) self.assertEqual(expected, pkt.header.octets + pkt.cmd_params_frame.octets) def test_add_adv(self): expected = bytes.fromhex('3e00000027000102000000000000001c001bfff0ff6db643cf7e8f4711886659' '38d17aaa26495e131415161718') pkt = btmgmt_protocol.command('AddAdvertising', 0, 1, 2, 0, 0, 0x1c, 0, "1bfff0ff6DB643CF7E8F471188665938D17AAA26495E131415161718", '') self.assertEqual(expected, pkt.header.octets + pkt.cmd_params_frame.octets) if __name__ == '__main__': unittest.main() python-btsocket-0.3.0/tests/test_response.py000066400000000000000000000141311463152142400212620ustar00rootroot00000000000000import unittest from btsocket import btmgmt_protocol class TestCmdReponse(unittest.TestCase): def test_version_cmd(self): expected = ('Response(header=, ' 'event_frame=, ' 'cmd_response_frame=)') recvd = b'\x01\x00\xff\xff\x06\x00\x01\x00\x00\x01\x0e\x00' pkt = btmgmt_protocol.reader(recvd) self.assertEqual(expected, str(pkt)) self.assertEqual(1, pkt.cmd_response_frame.version) self.assertEqual(14, pkt.cmd_response_frame.revision) def test_commands(self): expected = ('Response(header=, ' 'event_frame=, ' 'cmd_response_frame=)') recvd = (b'\x01\x00\xff\xff)\x00\x02\x00\x00\x06\x00\x0b\x00\x03\x00' b'\x04\x006\x007\x00<\x00B\x00\x04\x00\x05\x00\x06\x00\x07' b'\x00\x08\x00\x1d\x00\x1e\x00\x1f\x00 \x00!\x00%\x00') pkt = btmgmt_protocol.reader(recvd) self.assertEqual(expected, str(pkt)) def test_controllers(self): expected = ("Response(header=, " "event_frame=," " cmd_response_frame=)") recvd = (b'\x01\x00\x00\x00\x1b\x01\x04\x00\x00\xa4\x0c\x8f\xae\xf8' b'\xfc\x06\x02\x00\xff\xfe\x01\x00\xc1\n\x00\x00\x0c\x01' b'\x0cthinkabit1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00') pkt = btmgmt_protocol.reader(recvd) self.assertEqual(expected, str(pkt)) def test_info(self): expected = ('Response(header=, event_frame=, ' 'cmd_response_frame=None)') recvd = b'\x02\x00\x00\x00\x03\x00\x05\x00\x14' pkt = btmgmt_protocol.reader(recvd) self.assertEqual(expected, str(pkt)) def test_power_off_fail(self): expected = ('Response(header=, event_frame=, ' 'cmd_response_frame=None)') recvd = b'\x02\x00\x00\x00\x03\x00\x07\x00\x14' pkt = btmgmt_protocol.reader(recvd) self.assertEqual(expected, str(pkt)) def test_support_cmd_names(self): expected = ('Response(header=, ' 'event_frame=, ' 'cmd_response_frame=)') recvd = (b'\x01\x00\xff\xff)\x00\x02\x00\x00\x06\x00\x0b\x00\x03\x00' b'\x04\x006\x007\x00<\x00B\x00\x04\x00\x05\x00\x06\x00\x07' b'\x00\x08\x00\x1d\x00\x1e\x00\x1f\x00 \x00!\x00%\x00') pkt = btmgmt_protocol.reader(recvd) self.assertEqual(expected, str(pkt)) if __name__ == '__main__': unittest.main() python-btsocket-0.3.0/tests/test_utils.py000066400000000000000000000120761463152142400205720ustar00rootroot00000000000000import unittest from btsocket import btmgmt_protocol class TestAddress(unittest.TestCase): def test_address_decode(self): addr = btmgmt_protocol.Address() addr.decode(b'\xa4\x0c\x8f\xae\xf8\xfc') self.assertEqual('FC:F8:AE:8F:0C:A4', addr.value) def test_address_evcode(self): addr = btmgmt_protocol.Address() addr.encode('FC:F8:AE:8F:0C:A4', 6) self.assertEqual(b'\xa4\x0c\x8f\xae\xf8\xfc', addr.octets) class TestCurrentSettings(unittest.TestCase): def test_settings_decode(self): expected = {btmgmt_protocol.SupportedSettings.Advertising: False, btmgmt_protocol.SupportedSettings.Powered: True, btmgmt_protocol.SupportedSettings.Connectable: False, btmgmt_protocol.SupportedSettings.FastConnectable: False, btmgmt_protocol.SupportedSettings.Discoverable: False, btmgmt_protocol.SupportedSettings.Bondable: False, btmgmt_protocol.SupportedSettings.LinkLevelSecurity: False, btmgmt_protocol.SupportedSettings.LowEnergy: True, btmgmt_protocol.SupportedSettings.SecureSimplePairing: True, btmgmt_protocol.SupportedSettings.BREDR: True, btmgmt_protocol.SupportedSettings.HighSpeed: False, btmgmt_protocol.SupportedSettings.WidebandSpeech: False, btmgmt_protocol.SupportedSettings.SecureConnections: True, btmgmt_protocol.SupportedSettings.DebugKeys: False, btmgmt_protocol.SupportedSettings.Privacy: False, btmgmt_protocol.SupportedSettings.ControllerConfiguration: False, btmgmt_protocol.SupportedSettings.StaticAddress: False, btmgmt_protocol.SupportedSettings.PHYConfiguration: False} settings = btmgmt_protocol.CurrentSettings() settings.decode(b'\xc1\n\x00\x00') self.assertEqual(expected, settings.value) class TestEirData(unittest.TestCase): def test_eir_data_decode_sd(self): expected = {btmgmt_protocol.ADType.Flags: b'\x1a', btmgmt_protocol.ADType.CompleteUUID16ServiceList: b'o\xfd', btmgmt_protocol.ADType.ServiceDataUUID16: b'o\xfds\xc6\xde\xa5\xac>=\x8b\x1b\xe5\xe5\xac\x8f\xd0\xea7%\xa4\xe7\xcd'} data = b'\x02\x01\x1a\x03\x03o\xfd\x17\x16o\xfds\xc6\xde\xa5\xac>=\x8b\x1b\xe5\xe5\xac\x8f\xd0\xea7%\xa4\xe7\xcd' eir_data = btmgmt_protocol.EIRData() eir_data.decode(data) self.assertDictEqual(expected, eir_data.value) def test_eir_data_decode_md(self): expected = {btmgmt_protocol.ADType.Flags: b'\x06', btmgmt_protocol.ADType.CompleteName: b'DC76F7E1', btmgmt_protocol.ADType.ManufacturerData: b'3\x01(\xa7(\x96(\x8c\x00\x00\x00\xf4\x02\x03\x00\xcb\x01\xcf\x00\xe5\x01\xe9\x00\x00\x00\x00\x00'} data = b'\x02\x01\x06\x11\xff3\x01\x1bd\x0e\x10\x0bC\x00\xf0\x01\xf3(\x89\x01\x00\t\tDC76F7E1\x1c\xff3\x01(' \ b'\xa7(\x96(\x8c\x00\x00\x00\xf4\x02\x03\x00\xcb\x01\xcf\x00\xe5\x01\xe9\x00\x00\x00\x00\x00' eir_data = btmgmt_protocol.EIRData() eir_data.decode(data) self.assertEqual(expected, eir_data.value) class TestAddressTypes(unittest.TestCase): def test_address_type_decode_all(self): expected = [btmgmt_protocol.AddressType.BREDR, btmgmt_protocol.AddressType.LEPublic, btmgmt_protocol.AddressType.LERandom] addr_type = btmgmt_protocol.AddressTypeField() addr_type.decode(b'\x07') self.assertEqual(expected, addr_type.value) def test_address_type_decode_le(self): expected = [btmgmt_protocol.AddressType.LEPublic, btmgmt_protocol.AddressType.LERandom] addr_type = btmgmt_protocol.AddressTypeField() addr_type.decode(b'\x06') self.assertEqual(expected, addr_type.value) def test_address_type_decode_bdedr(self): expected = [btmgmt_protocol.AddressType.BREDR] addr_type = btmgmt_protocol.AddressTypeField() addr_type.decode(b'\x01') self.assertEqual(expected, addr_type.value) def test_address_type_encode_all(self): addr_type = btmgmt_protocol.AddressTypeField() addr_type.encode([btmgmt_protocol.AddressType.BREDR, btmgmt_protocol.AddressType.LEPublic, btmgmt_protocol.AddressType.LERandom], 1) self.assertEqual(b'\x07', addr_type.octets) def test_address_type_encode_ble(self): addr_type = btmgmt_protocol.AddressTypeField() addr_type.encode([btmgmt_protocol.AddressType.LEPublic, btmgmt_protocol.AddressType.LERandom], 1) self.assertEqual(b'\x06', addr_type.octets) def test_address_type_encode_bredr(self): addr_type = btmgmt_protocol.AddressTypeField() addr_type.encode([btmgmt_protocol.AddressType.BREDR], 1) self.assertEqual(b'\x01', addr_type.octets) if __name__ == '__main__': unittest.main()