Source code for blue_st_sdk.feature

################################################################################
# COPYRIGHT(c) 2018 STMicroelectronics                                         #
#                                                                              #
# Redistribution and use in source and binary forms, with or without           #
# modification, are permitted provided that the following conditions are met:  #
#   1. Redistributions of source code must retain the above copyright notice,  #
#      this list of conditions and the following disclaimer.                   #
#   2. Redistributions in binary form must reproduce the above copyright       #
#      notice, this list of conditions and the following disclaimer in the     #
#      documentation and/or other materials provided with the distribution.    #
#   3. Neither the name of STMicroelectronics nor the names of its             #
#      contributors may be used to endorse or promote products derived from    #
#      this software without specific prior written permission.                #
#                                                                              #
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"  #
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE    #
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE   #
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE    #
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR          #
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF         #
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS     #
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN      #
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)      #
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE   #
# POSSIBILITY OF SUCH DAMAGE.                                                  #
################################################################################


"""feature

The feature module represents a feature exported by a Bluetooth Low Energy (BLE)
device.
"""


# IMPORT

from abc import ABCMeta
from abc import abstractmethod
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime

from blue_st_sdk.utils.python_utils import lock
from blue_st_sdk.utils.blue_st_exceptions import BlueSTInvalidOperationException
from blue_st_sdk.utils.blue_st_exceptions import BlueSTInvalidDataException


# CLASSES

[docs]class Feature(object): """This class contains description and data exported by a node. Adding a new sensor in a node implies extending this class and implementing the :meth:`blue_st_sdk.feature.Feature.extract_data()` method to extract the information from the raw data coming from the node. This class manages notifications and listeners' subscriptions. """ _NUMBER_OF_THREADS = 5 """Number of threads to be used to notify the listeners."""
[docs] def __init__(self, name, node, description): """Constructor. Args: name (str): Name of the feature. node (:class:`blue_st_sdk.node.Node`): Node that will update the feature. description (list): Description of the data of the feature (list of :class:`blue_st_sdk.features.field.Field` objects). """ self._name = name """Feature name.""" self._parent = node """Node that will update the feature.""" self._description = description """List of feature's fields. Fields are described by name, unit, type, and minimum/maximum values.""" self._is_enabled = False """Tells whether the feature is enabled or not.""" self._notify = False """Tells whether the feature's notifications are enabled or not.""" self._thread_pool = ThreadPoolExecutor(Feature._NUMBER_OF_THREADS) """Pool of thread used to notify the listeners.""" self._listeners = [] """List of listeners to the feature changes. It is a thread safe list, so a listener can subscribe itself through a callback.""" self._loggers = [] """List of listeners to log the received data. It is a thread safe list, so a listener can subscribe itself through a callback.""" self._last_update = None """Local time of the last update.""" self._last_sample = None """Last data received from the node.""" self._characteristic = None """Reference to the characteristic that offers the feature. Note: By design, it is the characteristic that offers more features
beyond the current one, among those offering the current one."""
[docs] def add_listener(self, listener): """Add a listener. Args: listener (:class:`blue_st_sdk.feature.FeatureListener`): Listener to be added. """ if listener is not None: with lock(self): if not listener in self._listeners:
self._listeners.append(listener)
[docs] def remove_listener(self, listener): """Remove a listener. Args: listener (:class:`blue_st_sdk.feature.FeatureListener`): Listener to be removed. """ if listener is not None: with lock(self): if listener in self._listeners:
self._listeners.remove(listener)
[docs] def add_logger(self, logger): """Add a logger. Args: logger (:class:`blue_st_sdk.feature.FeatureLogger`): Logger to be added. """ if logger is not None: with lock(self): if not logger in self._loggers:
self._loggers.append(logger)
[docs] def remove_logger(self, logger): """Remove a logger. Args: logger (:class:`blue_st_sdk.feature.FeatureLogger`): Logger to be removed. """ if logger is not None: with lock(self): if logger in self._loggers:
self._loggers.remove(logger)
[docs] def get_last_update(self): """Get the time of the last update. Returns: datetime: The time of the last update received. Refer to `datetime <https://docs.python.org/2/library/datetime.html>`_ for more information. """
return self._last_update
[docs] def get_name(self): """Get the feature name. Returns: str: The feature name. """
return self._name
[docs] def get_parent_node(self): """Get the node that updates the feature. Return: :class:`blue_st_sdk.node.Node`: The node that updates the feature. """
return self._parent
[docs] def get_characteristic(self): """Get the characteristic that offers the feature. Note: By design, it is the characteristic that offers more features beyond the current one, among those offering the current one. Returns: characteristic: The characteristic that offers the feature. Refer to `Characteristic <https://ianharvey.github.io/bluepy-doc/characteristic.html>`_ for more information. """
return self._characteristic
[docs] def get_fields_description(self): """"Get the description of the data fields of the feature. Returns: list: The description of the data fields of the feature (list of :class:`blue_st_sdk.features.field.Field` objects). """
return self._description def _get_sample(self): """Return a sample containing the last timestamp and data received from the device. Returns: :class:`blue_st_sdk.feature.Sample`: The last sample received, None if missing. """ if self._last_sample is not None: return Sample.from_sample(self._last_sample) return None
[docs] def set_enable(self, flag): """Set the enable status of the feature. Args: flag (bool): New enable status: True to enable, False otherwise. """
self._is_enabled = flag
[docs] def is_enabled(self): """Checking whether the node exports the data of the feature. A node can export a feature in the advertising data without having the equivalent characteristic. Returns: bool: True if the node exports the data of the feature, False otherwise. """
return self._is_enabled
[docs] def set_notify(self, flag): """Set the notification status of the feature. Args: flag (bool): New notification status: True to enable, False otherwise. """
self._notify = flag
[docs] def is_notifying(self): """Checking whether the notifications for the feature are enabled. Returns: bool: True if the feature is notifying, False otherwise. """
return self._notify def _notify_update(self, sample): """Notify each :class:`blue_st_sdk.feature.FeatureListener`that the feature has been updated. Each call runs in a different thread. Overwriting the method :meth:`blue_st_sdk.feature.Feature.update()` implies calling this method to notify the user about the new sample. Args: sample (:class:`blue_st_sdk.feature.Sample`): Sample data. """ for listener in self._listeners: # Calling user-defined callback. self._thread_pool.submit(listener.on_update(self, sample)) def _log_update(self, raw_data, sample): """Notify each :class:`blue_st_sdk.feature.FeatureLogger` that the feature has been updated. Each call runs in a different thread. Overwriting the method :meth:`blue_st_sdk.feature.Feature.update()` implies calling this method to log a feature's update. Args: raw_data: Raw data used to extract the feature field. It can be "None". sample (:class:`blue_st_sdk.feature.Sample`): Sample data to log. """ for logger in self._loggers: # Calling user-defined callback. self._thread_pool.submit(logger.log_update(self, raw_data, sample))
[docs] def update(self, timestamp, data, offset, notify_update=False): """Update feature's internal data through an atomic operation, and notify the registered listeners about the update, if needed. This method has to be called by a node whenever it receives new data from the feature, not by the application. When overriding this method, please remember to update the timestamp and the last-updated value, and to acquire the write-lock. Args: timestamp (int): Package's timestamp. data (list): Feature's data. offset (int): Offset position to start reading data. notify_update (bool, optional): If True all the registered listeners are notified about the new data. Returns: int: The number of bytes read. Raises: :exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidDataException` if the data array has not enough data to read. """ # Update the feature's internal data sample = None with lock(self): try: extracted_data = self.extract_data(timestamp, data, offset) except BlueSTInvalidDataException as e: raise e sample = self._last_sample = extracted_data.get_sample() read_bytes = extracted_data.get_read_bytes() self._last_update = datetime.now() if notify_update: # Notify all the registered listeners about the new data. self._notify_update(sample) # Log the new data through all the registered loggers. self._log_update(data[offset:offset + read_bytes], sample)
return read_bytes
[docs] @classmethod def has_valid_index(self, sample, index): """Check whether the sample has valid data at the index position. Args: sample (:class:`blue_st_sdk.feature.Sample`): Sample data. index (int): Position to be tested. Returns: bool: True if the sample is not null and has a non null value at the index position, False otherwise. """ return sample is not None \ and len(sample._data) > index \
and sample._data[index] is not None def _read_data(self): """Read data from the feature. Raises: :exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidOperationException` is raised if the feature is not enabled or the operation required is not supported. :exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidDataException` if the data array has not enough data to read. """ try: self._parent.read_feature(self) except (BlueSTInvalidOperationException, BlueSTInvalidDataException) as e: raise e def _write_data(self, data): """Write data to the feature. Args: data (str): Raw data to write. Raises: :exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidOperationException` is raised if the feature is not enabled or the operation required is not supported. """ try: self._parent.write_feature(self, data) except BlueSTInvalidOperationException as e: raise e
[docs] @abstractmethod def extract_data(self, timestamp, data, offset): """Extract the data from the feature's raw data. You have to parse the data inside the "data" field and skip the first "offset" byte. This method has to extract the data, create a :class:`blue_st_sdk.feature.Sample` object, and return an :class:`blue_st_sdk.feature.ExtractedData` object containing it. The method that calls this one has to manage the lock acquisition/release and to notify the user about the new sample. Args: timestamp (int): Data's timestamp. data (str): The data read from the feature. offset (int): Offset where to start reading data. Returns: :class:`blue_st_sdk.feature.ExtractedData`: An object containing the number of bytes read and the extracted data. Raises: :exc:`NotImplementedError` if the method has not been implemented. :exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidDataException` if the data array has not enough data to read. """ raise NotImplementedError(
'You must implement "extract_data()" to use the "Feature" class.') def __str__(self): """Get a string representing the last sample. Return: str: A string representing the last sample. """ with lock(self): sample = self._last_sample if sample is None: return self._name + ': Unknown' if not sample._data: return self._name + ': Unknown' if len(sample._data) == 1: result = '%s(%d): %s %s' \ % (self._name, sample._timestamp, str(sample._data[0]), self._description[0]._unit) return result # Check on timestamp (ADPCM Audio and ADPCM Sync samples don't have # the timestamp field in order to save bandwidth.) if sample._timestamp is not None: result = '%s(%d): ( ' % (self._name, sample._timestamp) i = 0 while i < len(sample._data): result += '%s: %s %s%s' \ % (self._description[i]._name, str(sample._data[i]), self._description[i]._unit, ' ' if i < len(sample._data) - 1 else ' )') i += 1 else: # Only for Audio Features. result = str(self._name) + " - " for i in range(0,len(sample._data)-1): result += str(sample._data[i]) + ", " result += str(sample._data[len(sample._data)-1])
return result
[docs]class FeatureListener(object): """Interface used by the :class:`blue_st_sdk.feature.Feature` class to notify changes of a feature's data. """ __metaclass__ = ABCMeta
[docs] @abstractmethod def on_update(self, feature, sample): """To be called whenever the feature updates its data. Args: feature (:class:`blue_st_sdk.feature.Feature`): Feature that has updated. sample (:class:`blue_st_sdk.feature.Sample`): Sample data extracted from the feature. Raises: :exc:`NotImplementedError` if the method has not been implemented. """ raise NotImplementedError(
'You must implement "on_update()" to use the "FeatureListener" class.')
[docs]class FeatureLogger(object): """Interface used by the :class:`blue_st_sdk.feature.Feature` class to log changes of a feature's data. """ __metaclass__ = ABCMeta
[docs] @abstractmethod def log_update(self, feature, raw_data, sample): """To be called to log the updates of the feature. Args: feature (:class:`blue_st_sdk.feature.Feature`): Feature that has updated. raw_data (str): Raw data used to update the feature. sample (:class:`blue_st_sdk.feature.Sample`): Sample data extracted from the feature. Raises: :exc:`NotImplementedError` if the method has not been implemented. """ raise NotImplementedError(
'You must implement "log_update()" to use the "FeatureLogger" class.')
[docs]class ExtractedData(object): """Class used to return the data and the number of bytes read after extracting data with the :meth:`blue_st_sdk.feature.Feature.extract_data()` method."""
[docs] def __init__(self, sample, read_bytes): """Constructor. Args: sample (:class:`blue_st_sdk.feature.Sample`): A sample object. read_bytes (int): The number of bytes read after extracting data. """ self._sample = sample """Data extracted from the byte stream.""" self._read_bytes = read_bytes
"""Number of bytes read."""
[docs] def get_read_bytes(self): """Get the number of bytes read. Returns: int: The number of bytes read. """
return self._read_bytes
[docs] def get_sample(self): """Get the data extracted from the byte stream. Returns: :class:`blue_st_sdk.feature.Sample`: The data extracted from the byte stream. """
return self._sample
[docs]class Sample(object): """Class that contains the last data from the node."""
[docs] def __init__(self, data, description, timestamp = 0): """Constructor. Args: data (list): Feature's data. description (list): Description of the data of the feature (list of :class:`blue_st_sdk.features.field.Field` objects). timestamp (int): Data's timestamp. """ self._data = data self._description = description self._timestamp = timestamp
self._notification_time = datetime.now()
[docs] @classmethod def from_sample(self, copy_me): """Make a copy of a sample. Args: copy_me (:class:`blue_st_sdk.feature.Sample`): A given sample. """ sample = Sample( list(copy_me._data), list(copy_me._description), copy_me._timestamp) sample._notification_time = copy_me._notification_time
return sample
[docs] def equals(self, sample): """Check the equality of the sample w.r.t. the given one. Args: sample (:class:`blue_st_sdk.feature.Sample`): A sample object. Returns: bool: True if the objects are equal (timestamp and data), False otherwise. """ if sample is None: return False if isinstance(sample, self.Sample): return sample._timestamp == self._timestamp \ and sorted(sample._data) == sorted(self._data)
return False
[docs] def get_data(self): """Get the data. Returns: The data of the sample. """
return self._data
[docs] def get_description(self): """Get the description. Returns: list: A list of :class:`blue_st_sdk.features.field.Field` describing the sample. """
return self._description
[docs] def get_timestamp(self): """Get the timestamp. Returns: int: The timestamp of the sample. """
return self._timestamp
[docs] def get_notification_time(self): """Get the notification time. Returns: int: The notification time. """
return self._notification_time def __str__(self): """Get a string representing the last sample. Return: str: A string representing the last sample. """
return "Timestamp: " + str(self._timestamp) + " Data: " + str(self._data)