1# Copyright 2021-2022 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16# -----------------------------------------------------------------------------
17# Imports
18# -----------------------------------------------------------------------------
19import struct
20from typing import Optional, Tuple
21
22from bumble.gatt_client import ServiceProxy, ProfileServiceProxy, CharacteristicProxy
23from bumble.gatt import (
24    GATT_DEVICE_INFORMATION_SERVICE,
25    GATT_FIRMWARE_REVISION_STRING_CHARACTERISTIC,
26    GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC,
27    GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
28    GATT_MODEL_NUMBER_STRING_CHARACTERISTIC,
29    GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC,
30    GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC,
31    GATT_SYSTEM_ID_CHARACTERISTIC,
32    GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
33    TemplateService,
34    Characteristic,
35    DelegatedCharacteristicAdapter,
36    UTF8CharacteristicAdapter,
37)
38
39
40# -----------------------------------------------------------------------------
41class DeviceInformationService(TemplateService):
42    UUID = GATT_DEVICE_INFORMATION_SERVICE
43
44    @staticmethod
45    def pack_system_id(oui, manufacturer_id):
46        return struct.pack('<Q', oui << 40 | manufacturer_id)
47
48    @staticmethod
49    def unpack_system_id(buffer):
50        system_id = struct.unpack('<Q', buffer)[0]
51        return (system_id >> 40, system_id & 0xFFFFFFFFFF)
52
53    def __init__(
54        self,
55        manufacturer_name: Optional[str] = None,
56        model_number: Optional[str] = None,
57        serial_number: Optional[str] = None,
58        hardware_revision: Optional[str] = None,
59        firmware_revision: Optional[str] = None,
60        software_revision: Optional[str] = None,
61        system_id: Optional[Tuple[int, int]] = None,  # (OUI, Manufacturer ID)
62        ieee_regulatory_certification_data_list: Optional[bytes] = None,
63        # TODO: pnp_id
64    ):
65        characteristics = [
66            Characteristic(
67                uuid, Characteristic.Properties.READ, Characteristic.READABLE, field
68            )
69            for (field, uuid) in (
70                (manufacturer_name, GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
71                (model_number, GATT_MODEL_NUMBER_STRING_CHARACTERISTIC),
72                (serial_number, GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC),
73                (hardware_revision, GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC),
74                (firmware_revision, GATT_FIRMWARE_REVISION_STRING_CHARACTERISTIC),
75                (software_revision, GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC),
76            )
77            if field is not None
78        ]
79
80        if system_id is not None:
81            characteristics.append(
82                Characteristic(
83                    GATT_SYSTEM_ID_CHARACTERISTIC,
84                    Characteristic.Properties.READ,
85                    Characteristic.READABLE,
86                    self.pack_system_id(*system_id),
87                )
88            )
89
90        if ieee_regulatory_certification_data_list is not None:
91            characteristics.append(
92                Characteristic(
93                    GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
94                    Characteristic.Properties.READ,
95                    Characteristic.READABLE,
96                    ieee_regulatory_certification_data_list,
97                )
98            )
99
100        super().__init__(characteristics)
101
102
103# -----------------------------------------------------------------------------
104class DeviceInformationServiceProxy(ProfileServiceProxy):
105    SERVICE_CLASS = DeviceInformationService
106
107    manufacturer_name: Optional[UTF8CharacteristicAdapter]
108    model_number: Optional[UTF8CharacteristicAdapter]
109    serial_number: Optional[UTF8CharacteristicAdapter]
110    hardware_revision: Optional[UTF8CharacteristicAdapter]
111    firmware_revision: Optional[UTF8CharacteristicAdapter]
112    software_revision: Optional[UTF8CharacteristicAdapter]
113    system_id: Optional[DelegatedCharacteristicAdapter]
114    ieee_regulatory_certification_data_list: Optional[CharacteristicProxy]
115
116    def __init__(self, service_proxy: ServiceProxy):
117        self.service_proxy = service_proxy
118
119        for field, uuid in (
120            ('manufacturer_name', GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
121            ('model_number', GATT_MODEL_NUMBER_STRING_CHARACTERISTIC),
122            ('serial_number', GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC),
123            ('hardware_revision', GATT_HARDWARE_REVISION_STRING_CHARACTERISTIC),
124            ('firmware_revision', GATT_FIRMWARE_REVISION_STRING_CHARACTERISTIC),
125            ('software_revision', GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC),
126        ):
127            if characteristics := service_proxy.get_characteristics_by_uuid(uuid):
128                characteristic = UTF8CharacteristicAdapter(characteristics[0])
129            else:
130                characteristic = None
131            self.__setattr__(field, characteristic)
132
133        if characteristics := service_proxy.get_characteristics_by_uuid(
134            GATT_SYSTEM_ID_CHARACTERISTIC
135        ):
136            self.system_id = DelegatedCharacteristicAdapter(
137                characteristics[0],
138                encode=lambda v: DeviceInformationService.pack_system_id(*v),
139                decode=DeviceInformationService.unpack_system_id,
140            )
141        else:
142            self.system_id = None
143
144        if characteristics := service_proxy.get_characteristics_by_uuid(
145            GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC
146        ):
147            self.ieee_regulatory_certification_data_list = characteristics[0]
148        else:
149            self.ieee_regulatory_certification_data_list = None
150