Exemple #1
0
class MicrophoneService(AdafruitService):  # pylint: disable=too-few-public-methods
    """Digital microphone data."""

    uuid = AdafruitService.adafruit_service_uuid(0xB00)

    sound_samples = Characteristic(
        uuid=AdafruitService.adafruit_service_uuid(0xB01),
        properties=(Characteristic.READ | Characteristic.NOTIFY),
        write_perm=Attribute.NO_ACCESS,
        max_length=512,
    )
    """
    Array of 16-bit sound samples, varying based on period.
    If num_channel == 2, the samples alternate left and right channels.
    """

    number_of_channels = Uint8Characteristic(
        uuid=AdafruitService.adafruit_service_uuid(0xB02),
        properties=Characteristic.READ,
        write_perm=Attribute.NO_ACCESS,
    )
    """1 for mono, 2 for stereo (left and right)"""

    measurement_period = AdafruitService.measurement_period_charac()
    """Initially 1000ms."""
Exemple #2
0
class MyLightClient(Service):
    uuid = VendorUUID("6BFD8F3F-A704-4111-8DCE-F571BA26B40B")
    _control = Characteristic(
        uuid=VendorUUID("6BFD8F3E-A704-4111-8DCE-F571BA26B40B"), max_length=7)
    _light_level = Uint8Characteristic(
        uuid=VendorUUID("6BFD8F3D-A704-4111-8DCE-F571BA26B40B"),
        initial_value=100,
        properties=(Characteristic.READ | Characteristic.WRITE
                    | Characteristic.WRITE_NO_RESPONSE))
    print("my light init")
Exemple #3
0
    def __init__(self,
                 characteristic: Characteristic,
                 *,
                 timeout: float = -1,
                 buffer_size: int = 64):
        """Monitor the given Characteristic. Each time a new value is written to the Characteristic
        add the newly-written bytes to a FIFO buffer.

        :param Characteristic characteristic: The Characteristic to monitor.
          It may be a local Characteristic provided by a Peripheral Service,
          or a remote Characteristic in a remote Service that a Central has connected to.
        :param int timeout:  the timeout in seconds to wait for the first character
          and between subsequent characters.
        :param int buffer_size: Size of ring buffer that stores incoming data coming from client.
          Must be >= 1."""
        self._characteristic = characteristic
        self._timeout = timeout
        self._buffer_size = buffer_size
        self._queue = queue.Queue(buffer_size)
        characteristic._add_notify_callback(self._notify_callback)
Exemple #4
0
class MagicLightService(Service):
    """Service for controlling a Magic Light RGB bulb."""

    # These UUIDs actually use the standard base UUID even though they aren't standard.
    uuid = VendorUUID("0000ffe5-0000-1000-8000-00805f9b34fb")

    _control = Characteristic(
        uuid=VendorUUID("0000ffe9-0000-1000-8000-00805f9b34fb"), max_length=7
    )

    def __init__(self, service=None):
        super().__init__(service=service)
        self._color = 0xFFFFFF
        self._buf = bytearray(7)
        self._buf[0] = 0x56
        self._buf[6] = 0xAA
        self._brightness = 1.0

    def __getitem__(self, index):
        if index > 0:
            raise IndexError()
        return self._color

    def __setitem__(self, index, value):
        if index > 0:
            raise IndexError()
        if isinstance(value, int):
            r = (value >> 16) & 0xFF
            g = (value >> 8) & 0xFF
            b = value & 0xFF
        else:
            r, g, b = value
        self._buf[1] = r
        self._buf[2] = g
        self._buf[3] = b
        self._buf[4] = 0x00
        self._buf[5] = 0xF0
        self._control = self._buf
        self._color = value

    def __len__(self):
        return 1
    def __init__(self, service, report_id, usage_page, usage, *, max_length):
        self._characteristic = Characteristic.add_to_service(
            service.bleio_service,
            self.uuid.bleio_uuid,
            max_length=max_length,
            fixed_length=True,
            properties=(Characteristic.READ | Characteristic.WRITE
                        | Characteristic.WRITE_NO_RESPONSE),
            read_perm=Attribute.ENCRYPT_NO_MITM,
            write_perm=Attribute.ENCRYPT_NO_MITM)
        self._report_id = report_id
        self.usage_page = usage_page
        self.usage = usage

        _bleio.Descriptor.add_to_characteristic(
            self._characteristic,
            _REPORT_REF_DESCR_UUID,
            read_perm=Attribute.ENCRYPT_NO_MITM,
            write_perm=Attribute.NO_ACCESS,
            initial_value=struct.pack('<BB', self._report_id,
                                      _REPORT_TYPE_OUTPUT))
Exemple #6
0
class HIDService(Service):
    """
    Provide devices for HID over BLE.

    :param str hid_descriptor: USB HID descriptor that describes the structure of the reports. Known
        as the report map in BLE HID.

    Example::

        from adafruit_ble.hid_server import HIDServer

        hid = HIDServer()
    """

    uuid = StandardUUID(0x1812)

    boot_keyboard_in = Characteristic(
        uuid=StandardUUID(0x2A22),
        properties=(Characteristic.READ | Characteristic.NOTIFY),
        read_perm=Attribute.ENCRYPT_NO_MITM,
        write_perm=Attribute.NO_ACCESS,
        max_length=8,
        fixed_length=True,
    )

    boot_keyboard_out = Characteristic(
        uuid=StandardUUID(0x2A32),
        properties=(Characteristic.READ
                    | Characteristic.WRITE
                    | Characteristic.WRITE_NO_RESPONSE),
        read_perm=Attribute.ENCRYPT_NO_MITM,
        write_perm=Attribute.ENCRYPT_NO_MITM,
        max_length=1,
        fixed_length=True,
    )

    protocol_mode = Uint8Characteristic(
        uuid=StandardUUID(0x2A4E),
        properties=(Characteristic.READ | Characteristic.WRITE_NO_RESPONSE),
        read_perm=Attribute.OPEN,
        write_perm=Attribute.OPEN,
        initial_value=1,
        max_value=1,
    )
    """Protocol mode: boot (0) or report (1)"""

    # bcdHID (version), bCountryCode (0 not localized), Flags: RemoteWake, NormallyConnectable
    # bcd1.1, country = 0, flag = normal connect
    # TODO: Make this a struct.
    hid_information = Characteristic(
        uuid=StandardUUID(0x2A4A),
        properties=Characteristic.READ,
        read_perm=Attribute.ENCRYPT_NO_MITM,
        write_perm=Attribute.NO_ACCESS,
        initial_value=b"\x01\x01\x00\x02",
    )
    """Hid information including version, country code and flags."""

    report_map = Characteristic(
        uuid=StandardUUID(0x2A4B),
        properties=Characteristic.READ,
        read_perm=Attribute.ENCRYPT_NO_MITM,
        write_perm=Attribute.NO_ACCESS,
        fixed_length=True,
    )
    """This is the USB HID descriptor (not to be confused with a BLE Descriptor). It describes
       which report characteristic are what."""

    suspended = Uint8Characteristic(
        uuid=StandardUUID(0x2A4C),
        properties=Characteristic.WRITE_NO_RESPONSE,
        read_perm=Attribute.NO_ACCESS,
        write_perm=Attribute.ENCRYPT_NO_MITM,
        max_value=1,
    )
    """Controls whether the device should be suspended (0) or not (1)."""
    def __init__(self, hid_descriptor=DEFAULT_HID_DESCRIPTOR, service=None):
        super().__init__(report_map=hid_descriptor)
        if service:
            # TODO: Add support for connecting to a remote hid server.
            pass
        self._init_devices()

    def _init_devices(self):
        # pylint: disable=too-many-branches,too-many-statements,too-many-locals
        self.devices = []
        hid_descriptor = self.report_map

        global_table = [None] * 10
        local_table = [None] * 3
        collections = []
        top_level_collections = []

        i = 0
        while i < len(hid_descriptor):
            b = hid_descriptor[i]
            tag = (b & 0xF0) >> 4
            _type = (b & 0b1100) >> 2
            size = b & 0b11
            size = 4 if size == 3 else size
            i += 1
            data = hid_descriptor[i:i + size]
            if _type == _ITEM_TYPE_GLOBAL:
                global_table[tag] = data
            elif _type == _ITEM_TYPE_MAIN:
                if tag == _MAIN_ITEM_TAG_START_COLLECTION:
                    collections.append({
                        "type": data,
                        "locals": list(local_table),
                        "globals": list(global_table),
                        "mains": [],
                    })
                elif tag == _MAIN_ITEM_TAG_END_COLLECTION:
                    collection = collections.pop()
                    # This is a top level collection if the collections list is now empty.
                    if not collections:
                        top_level_collections.append(collection)
                    else:
                        collections[-1]["mains"].append(collection)
                elif tag == _MAIN_ITEM_TAG_INPUT:
                    collections[-1]["mains"].append({
                        "tag":
                        "input",
                        "locals":
                        list(local_table),
                        "globals":
                        list(global_table),
                    })
                elif tag == _MAIN_ITEM_TAG_OUTPUT:
                    collections[-1]["mains"].append({
                        "tag":
                        "output",
                        "locals":
                        list(local_table),
                        "globals":
                        list(global_table),
                    })
                else:
                    raise RuntimeError(
                        "Unsupported main item in HID descriptor")
                local_table = [None] * 3
            else:
                local_table[tag] = data

            i += size

        def get_report_info(collection, reports):
            """ Gets info about hid reports """
            for main in collection["mains"]:
                if "type" in main:
                    get_report_info(main, reports)
                else:
                    report_size, report_id, report_count = [
                        x[0] for x in main["globals"][7:10]
                    ]
                    if report_id not in reports:
                        reports[report_id] = {
                            "input_size": 0,
                            "output_size": 0
                        }
                    if main["tag"] == "input":
                        reports[report_id][
                            "input_size"] += report_size * report_count
                    elif main["tag"] == "output":
                        reports[report_id][
                            "output_size"] += report_size * report_count

        for collection in top_level_collections:
            if collection["type"][0] != 1:
                raise NotImplementedError(
                    "Only Application top level collections supported.")
            usage_page = collection["globals"][0][0]
            usage = collection["locals"][0][0]
            reports = {}
            get_report_info(collection, reports)
            if len(reports) > 1:
                raise NotImplementedError(
                    "Only one report id per Application collection supported")

            report_id, report = list(reports.items())[0]
            output_size = report["output_size"]
            if output_size > 0:
                self.devices.append(
                    ReportOut(self,
                              report_id,
                              usage_page,
                              usage,
                              max_length=output_size // 8))

            input_size = reports[report_id]["input_size"]
            if input_size > 0:
                self.devices.append(
                    ReportIn(self,
                             report_id,
                             usage_page,
                             usage,
                             max_length=input_size // 8))
Exemple #7
0
class IBBQService(Service):
    """Service for reading from an iBBQ thermometer.
    """

    _CREDENTIALS_MSG = b"\x21\x07\x06\x05\x04\x03\x02\x01\xb8\x22\x00\x00\x00\x00\x00"
    _REALTIME_DATA_ENABLE_MSG = b"\x0B\x01\x00\x00\x00\x00"
    _UNITS_FAHRENHEIT_MSG = b"\x02\x01\x00\x00\x00\x00"
    _UNITS_CELSIUS_MSG = b"\x02\x00\x00\x00\x00\x00"
    _REQUEST_BATTERY_LEVEL_MSG = b"\x08\x24\x00\x00\x00\x00"

    def __init__(self, service=None):
        super().__init__(service=service)
        # Defer creating buffers until needed, since MTU is not known yet.
        self._settings_result_buf = None
        self._realtime_data_buf = None

    uuid = StandardUUID(0xFFF0)

    settings_result = _SettingsResult()

    account_and_verify = Characteristic(
        uuid=StandardUUID(0xFFF2),
        properties=Characteristic.WRITE,
        read_perm=Attribute.NO_ACCESS,
    )
    """Send credentials to this characteristic."""

    # Not yet understood, not clear if available.
    # history_data = Characteristic(uuid=StandardUUID(0xFFF3),
    #                               properties=Characteristic.NOTIFY,
    #                               write_perm=Attribute.NO_ACCESS)

    realtime_data = _RealtimeData()
    """Real-time temperature values."""

    settings_data = Characteristic(
        uuid=StandardUUID(0xFFF5),
        properties=Characteristic.WRITE,
        read_perm=Attribute.NO_ACCESS,
    )
    """Send control messages here."""

    def init(self):
        """Perform initial "pairing", which is not regular BLE pairing."""
        self.account_and_verify = self._CREDENTIALS_MSG
        self.settings_data = self._REALTIME_DATA_ENABLE_MSG

    def display_fahrenheit(self):
        """Display temperatures on device in degrees Fahrenheit.

        Note: This does not change the units returned by `temperatures`.
        """
        self.settings_data = self._UNITS_FAHRENHEIT_MSG

    def display_celsius(self):
        """Display temperatures on device in degrees Celsius.

        Note: This does not change the units returned by `temperatures`.
        """
        self.settings_data = self._UNITS_CELSIUS_MSG

    @property
    def temperatures(self):
        """Return a tuple of temperatures for all the possible temperature probes on the device.
        Temperatures are in degrees Celsius. Unconnected probes return 0.0.
        """
        if self._realtime_data_buf is None:
            self._realtime_data_buf = bytearray(
                self.realtime_data.packet_size  # pylint: disable=no-member
            )
        data = self._realtime_data_buf
        length = self.realtime_data.readinto(data)  # pylint: disable=no-member
        if length > 0:
            return tuple(
                struct.unpack_from("<H", data, offset=offset)[0] / 10
                for offset in range(0, length, 2)
            )
        # No data.
        return None

    @property
    def battery_level(self):
        """Get current battery level in volts as ``(current_voltage, max_voltage)``.
        Results are approximate and may differ from the
        actual battery voltage by 0.1v or so.
        """
        if self._settings_result_buf is None:
            self._settings_result_buf = bytearray(
                self.settings_result.packet_size  # pylint: disable=no-member
            )

        self.settings_data = self._REQUEST_BATTERY_LEVEL_MSG
        results = self._settings_result_buf
        length = self.settings_result.readinto(results)  # pylint: disable=no-member
        if length >= 5:
            header, current_voltage, max_voltage = struct.unpack_from("<BHH", results)
            if header == 0x24:
                # Calibration was determined empirically, by comparing
                # the returned values with actual measurements of battery voltage,
                # on one sample each of two different products.
                return (
                    current_voltage / 2000 - 0.3,
                    (6550 if max_voltage == 0 else max_voltage) / 2000,
                )
        # Unexpected response or no data.
        return None
Exemple #8
0
class CyclingSpeedAndCadenceService(Service):
    """Service for reading from a Cycling Speed and Cadence sensor."""

    # 0x180D is the standard HRM 16-bit, on top of standard base UUID
    uuid = StandardUUID(0x1816)

    # Mandatory.
    csc_measurement = _CSCMeasurement()

    csc_feature = Uint8Characteristic(uuid=StandardUUID(0x2A5C),
                                      properties=Characteristic.READ)
    sensor_location = Uint8Characteristic(uuid=StandardUUID(0x2A5D),
                                          properties=Characteristic.READ)

    sc_control_point = Characteristic(uuid=StandardUUID(0x2A39),
                                      properties=Characteristic.WRITE)

    _SENSOR_LOCATIONS = (
        "Other",
        "Top of shoe",
        "In shoe",
        "Hip",
        "Front Wheel",
        "Left Crank",
        "Right Crank",
        "Left Pedal",
        "Right Pedal",
        "Front Hub",
        "Rear Dropout",
        "Chainstay",
        "Rear Wheel",
        "Rear Hub",
        "Chest",
        "Spider",
        "Chain Ring",
    )

    def __init__(self, service=None):
        super().__init__(service=service)
        # Defer creating buffer until we're definitely connected.
        self._measurement_buf = None

    @property
    def measurement_values(self):
        """All the measurement values, returned as a CSCMeasurementValues
        namedtuple.

        Return ``None`` if no packet has been read yet.
        """
        # uint8: flags
        #  bit 0 = 1: Wheel Revolution Data is present
        #  bit 1 = 1: Crank Revolution Data is present
        #
        # The next two fields are present only if bit 0 above is 1:
        #   uint32: Cumulative Wheel Revolutions
        #   uint16: Last Wheel Event Time, in 1024ths of a second
        #
        # The next two fields are present only if bit 10 above is 1:
        #   uint16: Cumulative Crank Revolutions
        #   uint16: Last Crank Event Time, in 1024ths of a second
        #

        if self._measurement_buf is None:
            self._measurement_buf = bytearray(
                self.csc_measurement.incoming_packet_length  # pylint: disable=no-member
            )
        buf = self._measurement_buf
        packet_length = self.csc_measurement.readinto(buf)  # pylint: disable=no-member
        if packet_length == 0:
            return None
        flags = buf[0]
        next_byte = 1

        if flags & 0x1:
            wheel_revs = struct.unpack_from("<L", buf, next_byte)[0]
            wheel_time = struct.unpack_from("<H", buf, next_byte + 4)[0]
            next_byte += 6
        else:
            wheel_revs = wheel_time = None

        if flags & 0x2:
            # Note that wheel revs is uint32 and and crank revs is uint16.
            crank_revs = struct.unpack_from("<H", buf, next_byte)[0]
            crank_time = struct.unpack_from("<H", buf, next_byte + 2)[0]
        else:
            crank_revs = crank_time = None

        return CSCMeasurementValues(wheel_revs, wheel_time, crank_revs,
                                    crank_time)

    @property
    def location(self):
        """The location of the sensor on the cycle, as a string.

        Possible values are:
        "Other", "Top of shoe", "In shoe", "Hip",
        "Front Wheel", "Left Crank", "Right Crank",
        "Left Pedal", "Right Pedal", "Front Hub",
        "Rear Dropout", "Chainstay", "Rear Wheel",
        "Rear Hub", "Chest", "Spider", "Chain Ring")
        "Other", "Chest", "Wrist", "Finger", "Hand", "Ear Lobe", "Foot",
        and "InvalidLocation" (if value returned does not match the specification).
        """

        try:
            return self._SENSOR_LOCATIONS[self.sensor_location]
        except IndexError:
            return "InvalidLocation"
Exemple #9
0
class SB20Service(Service):
    """
        Service for monitoring Stages SB20 status
    """

    uuid = UUID_SB20_SERVICE
    _status = _SB20Notification()
    command = Characteristic(uuid=UUID_SB20_COMMAND,
                             properties=Characteristic.WRITE_NO_RESPONSE)

    def __init__(self, *args, **kwargs):
        super(SB20Service, self).__init__(*args, **kwargs)
        self.connection = None
        self.suspend = False
        self.model = None

    def status(self, block=True, timeout=None):
        buf = self._status.get(block, timeout)
        if buf is not None and len(buf) > 0:
            if buf[:3] == b'\x0c\01\00' and len(buf) >= 8:
                self.model.update_gears(buf[3], buf[5], buf[4], buf[7])
            return buf
        return None

    def status_message(self, msg):
        if self.model is not None:
            self.model.update_status(msg)

    def run(self):
        self.bootstrap()
        self.status_message("Service bootstrapped")
        self.suspend = False
        while not self.suspend:
            status = self.status(True, 0.1)
        self.connection.disconnect()
        self.model.update_gears(0, 0, 0, 0)
        self.status_message("Disconnected")

    def disconnect_service(self):
        self.suspend = True

    def expect(self, *values):
        if len(values) == 0:
            return True
        # TODO Reset counter on b'\xfd\04'
        for count in range(0, 5):
            status = self.status()
            if status is not None:
                for v in values:
                    if status[:len(v)] == v:
                        return True
                self.status_message("Discarding {0}".format(hex(status)))
            time.sleep(0.1)
        return False

    def challenge(self, challenge, *resp):
        self.status_message("Sending {0} expecting {1}".format(
            hex(challenge), hex(*resp)))
        self.command = challenge
        if not self.expect(*resp):
            raise Exception("Expected %s after %s" %
                            (hex(*resp), hex(challenge)))

    @classmethod
    def connect_service(cls, model):
        def update_status(msg):
            model.update_status(msg)

        # PyLint can't find BLERadio for some reason so special case it here.
        ble = adafruit_ble.BLERadio()  # pylint: disable=no-member

        sb20_connection = None

        update_status("Scanning...")
        for adv in ble.start_scan(ProvideServicesAdvertisement, timeout=5):
            if CyclingSpeedAndCadenceService in adv.services:
                update_status("Found a Cycling Speed and Cadence profile")
                sb20_connection = ble.connect(adv)
                if SB20Service not in sb20_connection:
                    update_status("Device is not a Stages SB20")
                    sb20_connection.disconnect()
                    sb20_connection = None
                else:
                    update_status("Connected")
                    break

        # Stop scanning whether or not we are connected.
        ble.stop_scan()

        if sb20_connection and sb20_connection.connected:
            if DeviceInfoService in sb20_connection:
                dis = sb20_connection[DeviceInfoService]
                try:
                    manufacturer = dis.manufacturer
                except AttributeError:
                    manufacturer = "(Manufacturer Not specified)"
                try:
                    model_number = dis.model_number
                except AttributeError:
                    model_number = "(Model number not specified)"
                update_status("Device: {0} {1}".format(manufacturer,
                                                       model_number))
            else:
                update_status("No device information")

            sb20: SB20Service = sb20_connection[SB20Service]
            sb20.connection = sb20_connection
            sb20.model = model
            return sb20
        return None

    def bootstrap(self):
        try:
            self._status.get(False)
        except queue.Empty:
            pass
        setup = [
            (b'\x08\0', b'\x08\0'),
            (b'\x0c\0\x01', b'\x0c\0\x01'),
            (b'\x0a\0\0\0', b'\x0a\x01'),
            (b'\x0d\x02', b'\x0d\x02'),
            (b'\x0d\x04', b'\x0d\x04'),
            (b'\x0e\x00', b'\x0e\x00'),
            (b'\x08\0', b'\x08\0'),
            (b'\x0c\0\x01', b'\x0c\0\x01'),
            (b'\x0b\00\04\04\02\03\03\01\01\02\03\04\01\02\03\04\00',
             b'\x0b\0', b'\x0c\01'),
            (b'\x10\0\x01', b'\x10\0\x01', b'\x0c\01'),
            (b'\x0c\00\02\05\01\xc8\00\01', b'\05\01', b'\x0c\01'),
            (b'\x0c\0\x02', b'\x0c\01'),
            (b'\x0c\02\0\0\02\x0c\x10\0\03\x0e\0', b'\x0c\02', b'\x0c\01'),
            (b'\x0c\0\x02', b'\x0c\01'),
            (b'\x0c\03\x22\x32\x21\x1c\x18\x15\x13\x11\x0f\x0e\x0d\x0c\x0b\x0a',
             ),
            (b'\x0c\x02\x0b\x00\x02\x0c\x10\x00\x03\x0e\x00', b'\x0c\x02',
             b'\x0c\x01'),
            (b'\xfd\0', b'\xfd\x01', b'\x0c\01'),
            (b'\x03\x01\x4c\x1d\0\0', b'\x03\01', b'\x0c\01'),
            (b'\x0c\0\x02', b'\x0c\01'),
            (b'\x0c\0\x02', b'\x0c\01'),
            (b'\x0c\0\x02', b'\x0c\01'),
        ]
        for s in setup:
            self.challenge(*s)