__init__.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2019 Scott Shawcroft for Adafruit Industries
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # The above copyright notice and this permission notice shall be included in
 13  # all copies or substantial portions of the Software.
 14  #
 15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 21  # THE SOFTWARE.
 22  """
 23  
 24  This module provides core BLE characteristic classes that are used within Services.
 25  
 26  """
 27  
 28  import struct
 29  import _bleio
 30  
 31  from ..attributes import Attribute
 32  
 33  __version__ = "0.0.0-auto.0"
 34  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git"
 35  
 36  
 37  class Characteristic:
 38      """
 39      Top level Characteristic class that does basic binding.
 40  
 41      :param UUID uuid: The uuid of the characteristic
 42      :param int properties: The properties of the characteristic,
 43         specified as a bitmask of these values bitwise-or'd together:
 44         `BROADCAST`, `INDICATE`, `NOTIFY`, `READ`, `WRITE`, `WRITE_NO_RESPONSE`.
 45      :param int read_perm: Specifies whether the characteristic can be read by a client,
 46         and if so, which security mode is required.
 47         Must be one of the integer values `Attribute.NO_ACCESS`, `Attribute.OPEN`,
 48         `Attribute.ENCRYPT_NO_MITM`, `Attribute.ENCRYPT_WITH_MITM`,
 49         `Attribute.LESC_ENCRYPT_WITH_MITM`,
 50         `Attribute.SIGNED_NO_MITM`, or `Attribute.SIGNED_WITH_MITM`.
 51      :param int write_perm: Specifies whether the characteristic can be written by a client,
 52         and if so, which security mode is required. Values allowed are the same as ``read_perm``.
 53      :param int max_length: Maximum length in bytes of the characteristic value. The maximum allowed
 54         by the BLE specification is 512. On nRF, if ``fixed_length`` is ``True``, the maximum
 55         is 510. The default value is 20, which is the maximum
 56         number of data bytes that fit in a single BLE 4.x ATT packet.
 57      :param bool fixed_length: True if the characteristic value is of fixed length.
 58      :param buf initial_value: The initial value for this characteristic. If not given, will be
 59         filled with zeros.
 60  
 61      .. data:: BROADCAST
 62  
 63         property: allowed in advertising packets
 64  
 65      .. data:: INDICATE
 66  
 67           property: server will indicate to the client when the value is set and wait for a response
 68  
 69      .. data:: NOTIFY
 70  
 71         property: server will notify the client when the value is set
 72  
 73      .. data:: READ
 74  
 75         property: clients may read this characteristic
 76  
 77      .. data:: WRITE
 78  
 79         property: clients may write this characteristic; a response will be sent back
 80  
 81      .. data:: WRITE_NO_RESPONSE
 82  
 83         property: clients may write this characteristic; no response will be sent back
 84  """
 85  
 86      BROADCAST = _bleio.Characteristic.BROADCAST
 87      INDICATE = _bleio.Characteristic.INDICATE
 88      NOTIFY = _bleio.Characteristic.NOTIFY
 89      READ = _bleio.Characteristic.READ
 90      WRITE = _bleio.Characteristic.WRITE
 91      WRITE_NO_RESPONSE = _bleio.Characteristic.WRITE_NO_RESPONSE
 92  
 93      def __init__(
 94          self,
 95          *,
 96          uuid=None,
 97          properties=0,
 98          read_perm=Attribute.OPEN,
 99          write_perm=Attribute.OPEN,
100          max_length=None,
101          fixed_length=False,
102          initial_value=None
103      ):
104          self.field_name = None  # Set by Service during basic binding
105  
106          if uuid:
107              self.uuid = uuid
108          self.properties = properties
109          self.read_perm = read_perm
110          self.write_perm = write_perm
111          self.max_length = max_length
112          self.fixed_length = fixed_length
113          self.initial_value = initial_value
114  
115      def _ensure_bound(self, service, initial_value=None):
116          """Binds the characteristic to the local Service or remote Characteristic object given."""
117          if self.field_name in service.bleio_characteristics:
118              return
119          if service.remote:
120              for characteristic in service.bleio_service.characteristics:
121                  if characteristic.uuid == self.uuid.bleio_uuid:
122                      bleio_characteristic = characteristic
123                      break
124              else:
125                  raise AttributeError("Characteristic not available on remote service")
126          else:
127              bleio_characteristic = self.__bind_locally(service, initial_value)
128  
129          service.bleio_characteristics[self.field_name] = bleio_characteristic
130  
131      def __bind_locally(self, service, initial_value):
132          if initial_value is None:
133              initial_value = self.initial_value
134          if initial_value is None and self.max_length:
135              initial_value = bytes(self.max_length)
136          max_length = self.max_length
137          if max_length is None and initial_value is None:
138              max_length = 0
139              initial_value = b""
140          elif max_length is None:
141              max_length = len(initial_value)
142          return _bleio.Characteristic.add_to_service(
143              service.bleio_service,
144              self.uuid.bleio_uuid,
145              initial_value=initial_value,
146              max_length=max_length,
147              fixed_length=self.fixed_length,
148              properties=self.properties,
149              read_perm=self.read_perm,
150              write_perm=self.write_perm,
151          )
152  
153      def __get__(self, service, cls=None):
154          # CircuitPython doesn't invoke descriptor protocol on obj's class,
155          # but CPython does. In the CPython case, pretend that it doesn't.
156          if service is None:
157              return self
158          self._ensure_bound(service)
159          bleio_characteristic = service.bleio_characteristics[self.field_name]
160          return bleio_characteristic.value
161  
162      def __set__(self, service, value):
163          self._ensure_bound(service, value)
164          if value is None:
165              value = b""
166          bleio_characteristic = service.bleio_characteristics[self.field_name]
167          bleio_characteristic.value = value
168  
169  
170  class ComplexCharacteristic:
171      """
172      Characteristic class that does complex binding where the subclass returns a full object for
173      interacting with the characteristic data. The Characteristic itself will be shadowed once it
174      has been bound to the corresponding instance attribute.
175      """
176  
177      def __init__(
178          self,
179          *,
180          uuid=None,
181          properties=0,
182          read_perm=Attribute.OPEN,
183          write_perm=Attribute.OPEN,
184          max_length=20,
185          fixed_length=False,
186          initial_value=None
187      ):
188          self.field_name = None  # Set by Service during basic binding
189  
190          if uuid:
191              self.uuid = uuid
192          self.properties = properties
193          self.read_perm = read_perm
194          self.write_perm = write_perm
195          self.max_length = max_length
196          self.fixed_length = fixed_length
197          self.initial_value = initial_value
198  
199      def bind(self, service):
200          """Binds the characteristic to the local Service or remote Characteristic object given."""
201          if service.remote:
202              for characteristic in service.bleio_service.characteristics:
203                  if characteristic.uuid == self.uuid.bleio_uuid:
204                      return characteristic
205              raise AttributeError("Characteristic not available on remote service")
206          return _bleio.Characteristic.add_to_service(
207              service.bleio_service,
208              self.uuid.bleio_uuid,
209              initial_value=self.initial_value,
210              max_length=self.max_length,
211              properties=self.properties,
212              read_perm=self.read_perm,
213              write_perm=self.write_perm,
214          )
215  
216      def __get__(self, service, cls=None):
217          if service is None:
218              return self
219          bound_object = self.bind(service)
220          setattr(service, self.field_name, bound_object)
221          return bound_object
222  
223  
224  class StructCharacteristic(Characteristic):
225      """
226      Data descriptor for a structure with a fixed format.
227  
228      :param struct_format: a `struct` format string describing how to pack multiple values
229        into the characteristic bytestring
230      :param UUID uuid: The uuid of the characteristic
231      :param int properties: see `Characteristic`
232      :param int read_perm: see `Characteristic`
233      :param int write_perm: see `Characteristic`
234      :param buf initial_value: see `Characteristic`
235      """
236  
237      def __init__(
238          self,
239          struct_format,
240          *,
241          uuid=None,
242          properties=0,
243          read_perm=Attribute.OPEN,
244          write_perm=Attribute.OPEN,
245          initial_value=None
246      ):
247          self._struct_format = struct_format
248          self._expected_size = struct.calcsize(struct_format)
249          if initial_value is not None:
250              initial_value = struct.pack(self._struct_format, *initial_value)
251          super().__init__(
252              uuid=uuid,
253              initial_value=initial_value,
254              max_length=self._expected_size,
255              fixed_length=True,
256              properties=properties,
257              read_perm=read_perm,
258              write_perm=write_perm,
259          )
260  
261      def __get__(self, obj, cls=None):
262          if obj is None:
263              return self
264          raw_data = super().__get__(obj, cls)
265          if len(raw_data) < self._expected_size:
266              return None
267          return struct.unpack(self._struct_format, raw_data)
268  
269      def __set__(self, obj, value):
270          encoded = struct.pack(self._struct_format, *value)
271          super().__set__(obj, encoded)