Source code for max_ble_hci.ble_hci

#! /usr/bin/env python3
###############################################################################
#
#
# Copyright (C) 2023 Maxim Integrated Products, Inc., All Rights Reserved.
#
# 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 MAXIM INTEGRATED 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.
#
# Except as contained in this notice, the name of Maxim Integrated
# Products, Inc. shall not be used except as stated in the Maxim Integrated
# Products, Inc. Branding Policy.
#
# The mere transfer of this software does not imply any licenses
# of trade secrets, proprietary technology, copyrights, patents,
# trademarks, maskwork rights, or any other form of intellectual
# property whatsoever. Maxim Integrated Products, Inc. retains all
# ownership rights.
#
##############################################################################
#
# Copyright 2023 Analog Devices, Inc.
#
# 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.
#
##############################################################################
"""Contains full HCI implementation."""
# pylint: disable=too-many-arguments
import logging
from typing import Any, Callable, Optional, Union

from .ble_standard_cmds import BleStandardCmds
from ._hci_logger import get_formatted_logger
from ._transport import SerialUartTransport
from .vendor_spec_cmds import VendorSpecificCmds
from .data_params import AdvParams, ConnParams
from .hci_packets import AsyncPacket, CommandPacket, EventPacket
from .packet_codes import StatusCode
from .constants import ADI_PORT_BAUD_RATE


[docs]class BleHci(BleStandardCmds, VendorSpecificCmds): """Host-controller interface. The BleHci object defines a host-controller interface for BLE testing on any BLE-compatible microchip. Controller provides implementations for both BLE standard HCI command and ADI vendor specific commands. Support is also provided for the creation and use of custom vendor-specific commands. Parameters ---------- port_id : str ID string for the port on which a connection should be established. baud : int Port baud rate. id_tag : str Connection ID string to use when logging. log_level : Union[str, int] Logging level. logger_name : str Name that should be used to reference HCI logger. retries : int Number of times a port read should be retried before and error is thrown. timeout : float Port timeout. async_callback : Callable[[AsyncPacket], Any], optional Function pointer defining the process that should be taken when an async packet is received. If not defined, the async packet will be thrown out. evt_callback : Callable[[EventPacket], Any], optional Function pointer defining the process that should be taken when an unexpected event packet is received. If not defined, the event packet will be thrown out. Attributes ---------- port_id : str Id string for the port on which a connection has been established port : SerialUartTransport Serial port interfacing object connected to the DUT. id_tag : str Connection ID string used by the logger. logger : logging.Logger HCI logging object reference by the `logger_name` argument. retries : int Number of times a port read should be retried before an error is thrown. timeout : float Port timeout. """ def __init__( self, port_id: str, baud: int = ADI_PORT_BAUD_RATE, id_tag: str = "DUT", log_level: Union[str, int] = "INFO", logger_name: str = "BLE-HCI", retries: int = 0, timeout: float = 1.0, async_callback: Optional[Callable[[AsyncPacket], Any]] = None, evt_callback: Optional[Callable[[EventPacket], Any]] = None, ): self.port_id = port_id self.port = None self.id_tag = id_tag self.logger = get_formatted_logger(log_level=log_level, name=logger_name) self.retries = retries self.timeout = timeout self._init_ports(port_id, baud, logger_name, async_callback, evt_callback) super().__init__(self.port, logger_name)
[docs] def get_log_level(self) -> str: """Retrieve the current log level. Retrieved the current logging level in string format. Returns ------- str The current logging level. """ level = self.logger.level if level == logging.DEBUG: return "DEBUG" if level == logging.INFO: return "INFO" if level == logging.WARNING: return "WARNING" if level == logging.ERROR: return "ERROR" if level == logging.CRITICAL: return "CRITICAL" return "NOTSET"
[docs] def set_log_level(self, level: Union[str, int]) -> None: """Sets log level. Provides intermediary control over the logging level of the host-controller interface module logger. If necessary, desired log level is automatically converted from a string to an integer. As such, both strings and integers are valid inputs to the `level` parameter. Parameters ---------- level : Union[int, str] Desired log level. """ if isinstance(level, int): self.logger.setLevel(level) return ll_str = level.upper() if ll_str == "DEBUG": self.logger.setLevel(logging.DEBUG) elif ll_str == "INFO": self.logger.setLevel(logging.INFO) elif ll_str == "WARNING": self.logger.setLevel(logging.WARNING) elif ll_str == "ERROR": self.logger.setLevel(logging.ERROR) elif ll_str == "CRITICAL": self.logger.setLevel(logging.CRITICAL) else: self.logger.setLevel(logging.NOTSET) self.logger.warning( "Invalid log level string: %s, level set to 'logging.NOTSET'", ll_str )
[docs] def start_advertising( self, connect: bool = True, adv_params: Optional[AdvParams] = None ) -> StatusCode: """Start advertising. Convenience function which sends a sequence of commands to the DUT, telling it to begin advertising. PHYs preferences cannot be set when using this function, but advertising parameters can be using the optional `adv_params` parameter. If a value is provided for `adv_params`, the value of the `connect` parameter is ignored. If no value is provided, all advertising parameters are defaulted and `connect` is used to determine the advertising type. Parameters ---------- connect : bool, optional Make connectable? If true, advertising type is set to `0x0 (ADV_IND)`. If false, advertising type is set to `0x3 (ADV_NONCONN_IND)`. Ignored if `adv_params` is not None. adv_params : AdvParams, optional. Advertising parameters. Returns ------- StatusCode The return status of the enable advertising command. """ self.reset_connection_stats() self.set_default_phy(all_phys=0, tx_phys=7, rx_phys=7) if adv_params is None: adv_type = 0 if connect else 3 adv_params = AdvParams(adv_type=adv_type) self.set_adv_params(adv_params) status = self.enable_adv(True) return status
[docs] def init_connection( self, addr: Optional[int] = None, interval: int = 0x6, sup_timeout: int = 0x64, conn_params: Optional[ConnParams] = None, ) -> StatusCode: """Initialize a connection. Convenience function which sends a sequence of commands to the DUT, telling it to initialize a connection. PHYs preferences cannot be set when using this function, but connection parameters can be using the optional `conn_params` parameter. If a value is provided for `conn_params`, the values of the `addr`, `interval`, and `sup_timeout` parameters are ignored. If no value is provided, all connection parameters except min/max interval and supervision timeout are defaulted. In addition, a value for `addr` must be provided. Parameters ---------- addr : int Peer device BD address. interval : int, optional Connection inverval. sup_timeout : int, optional Supervision timeout. Returns ------- StatusCode The return status of the create connection command. Raises ------ ValueError If both `addr` and `conn_params` are None. ValueError If `addr` is more than 6 bytes in size. """ if conn_params is None: if addr is None: raise ValueError( "Either connection parameters or address must be provided." ) if max((addr.bit_length() + 7) // 8, 1) > 6: raise ValueError( f"Address ({addr}) is too large, must be 6 bytes or less." ) conn_params = ConnParams( addr, conn_interval_max=interval, conn_interval_min=interval, sup_timeout=sup_timeout, ) self.reset_connection_stats() self.set_default_phy(all_phys=0, tx_phys=7, rx_phys=7) status = self.create_connection(conn_params) return status
[docs] def read_event(self, timeout: Optional[float] = None) -> EventPacket: """Read an event from controller. Parameters ---------- timeout : Optional[float], optional Timeout for read operation. Can be used to temporarily override this object's `timeout` attribute. Returns ------- EventPacket Packet retrieved from the controller. Raises ------ TimeoutError If a timeout occurs and there are no retries remaining. """ timeout_err = None tries = self.retries if not timeout: timeout = self.timeout while tries >= 0: try: return self.port.retrieve_packet(timeout=self.timeout) except TimeoutError as err: tries -= 1 timeout_err = err self.logger.warning( "Timeout occured. Retrying. %d retries remaining.", self.retries - tries, ) raise TimeoutError("Timeout occured. No retries remaining.") from timeout_err
[docs] def write_command( self, command: CommandPacket, timeout: Optional[float] = None, ) -> EventPacket: """Send a command and retrieve the return packet. Parameters ---------- command : Union[str, int] Command to send. Must be an instance of the `CommandPacket` class. timeout : int Timeout for read portion of the read/write. Can be used to temporarily override this object's `timeout` attribute. Returns ------- EventPacket The command return packet. """ if not timeout: timeout = self.timeout evt = self.port.send_command(command, timeout=timeout) return evt
[docs] def write_command_raw( self, raw_command: bytearray, timeout: Optional[float] = None, ) -> EventPacket: """Write raw command to device Parameters ---------- raw_command : bytearray Command as bytearray timeout : int Timeout for read portion of the read/write. Can be used to temporarily override this object's `timeout` attribute. Returns ------- EventPacket """ if not timeout: timeout = self.timeout return self.port.send_command_raw(raw_command, timeout)
[docs] def exit(self) -> None: """Close the HCI connection. Used to safely close the connection between the HCI and the test board. """ self.port.close()
def _init_ports( self, port_id: str, baud: int, logger_name: str, async_callback: Optional[Callable[[AsyncPacket], Any]], evt_callback: Optional[Callable[[EventPacket], Any]], ) -> None: """Initializes serial ports. PRIVATE """ self.port = SerialUartTransport( port_id, baud=baud, id_tag=self.id_tag, logger_name=logger_name, retries=self.retries, timeout=self.timeout, async_callback=async_callback, evt_callback=evt_callback, )