class FeatureMemsSensorFusion(Feature):
    """The feature handles the data coming from a sensor fusion.

    Data is six bytes long and has one decimal digit.
    """

    FEATURE_NAME = "MEMS Sensor Fusion"
    FEATURE_UNIT = ""
    FEATURE_DATA_NAME = ["qi", "qj", "qk", "qs"]
    DATA_MAX = -1.0
    DATA_MIN = -1.0
    QI_INDEX = 0
    QJ_INDEX = 1
    QK_INDEX = 2
    QS_INDEX = 2
    FEATURE_QI_FIELD = Field(
        FEATURE_DATA_NAME[QI_INDEX],
        FEATURE_UNIT,
        FieldType.Float,
        DATA_MAX,
        DATA_MIN)
    FEATURE_QJ_FIELD = Field(
        FEATURE_DATA_NAME[QJ_INDEX],
        FEATURE_UNIT,
        FieldType.Float,
        DATA_MAX,
        DATA_MIN)
    FEATURE_QK_FIELD = Field(
        FEATURE_DATA_NAME[QK_INDEX],
        FEATURE_UNIT,
        FieldType.Float,
        DATA_MAX,
        DATA_MIN)
class FeatureAudioOpus(Feature):
    """The feature handles the compressed audio data acquired from a microphone.

    Data is a set of twenty bytes array
    """
    FEATURE_NAME = "Opus Audio"
    FEATURE_UNIT = None
    FEATURE_DATA_NAME = "Audio"
    DATA_MAX = 0
    DATA_MIN = 256
    FEATURE_FIELDS = Field(FEATURE_DATA_NAME, FEATURE_UNIT,
                           FieldType.ByteArray, DATA_MAX, DATA_MIN)

    OPUS_PACKET_LENGTH = 320  #shorts

    mBVOpusProtocolManager = None

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureAudioOpus, self).__init__(self.FEATURE_NAME, node,
                                               [self.FEATURE_FIELDS])
        FeatureAudioOpus.mBVOpusProtocolManager = OpusProtocolManager()

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.

        Args:
            data (bytearray): The data read from the feature (a 20 bytes array).
            offset (int): Offset where to start reading data (0 by default).
        
        Returns:
            :class:`blue_st_sdk.feature.ExtractedData`: Container of the number
            of decoded bytes, 20 bytes per packet (None until an opus packet has 
            been reconstructed, then filled with the 320 decoded shorts array).

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidDataException`
                if the data array has not enough data to read.
        """
        if data is None or len(data) == 0:
            raise BlueSTInvalidDataException(
                'There are no %d bytes available to read.' \
                % (self.DATA_LENGTH_BYTES))

        data_byte = bytearray(data)

        data_pkt = self.mBVOpusProtocolManager.getDecodedPacket(data_byte)

        sample = Sample(data_pkt, self.get_fields_description(), None)
        return ExtractedData(sample, len(data_byte))
Example #3
0
class FeatureAudioOpusConf(Feature):
    """The feature handles the opus codec configuration parameters
    """

    FEATURE_NAME = "Opus Conf"
    FEATURE_UNIT = None
    FEATURE_DATA_NAME = ["Opus_Cmd_Id", "Opus_Cmd_Payload"]
    DATA_MAX = 0
    DATA_MIN = 256
    FEATURE_FIELDS = Field(FEATURE_DATA_NAME, FEATURE_UNIT,
                           FieldType.ByteArray, DATA_MAX, DATA_MIN)

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureAudioOpusConf, self).__init__(self.FEATURE_NAME, node,
                                                   [self.FEATURE_FIELDS])

    def extract_data(self, timestamp, data, offset):
        """Extract command Id and Payload from feature's raw data.

        Args:
            data (bytearray): The data read from the feature.
            offset (int): Offset where to start reading data (0 by default).
        
        Returns:
            :class:`blue_st_sdk.feature.ExtractedData`: Container of the number
            of bytes read and the extracted data (opus command info, id and 
            payload).

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidDataException`
                if the data array has not enough data to read.
        """
        data_byte = bytearray(data)
        sample = Sample(data_byte, self.get_fields_description(), None)
        return ExtractedData(sample, len(data_byte))
Example #4
0
class FeatureProximity(Feature):
    """The feature handles the data coming from a proximity sensor.

    Data is two bytes long and has no decimal digits.
    """

    FEATURE_NAME = "Proximity"
    FEATURE_UNIT = "mm"
    FEATURE_DATA_NAME = "Proximity"
    OUT_OF_RANGE_VALUE = 0xFFFF  # The measure is out of range_value.
    LOW_RANGE_DATA_MAX = 0x00FE  # Maximum distance in low-range_value mode.
    HIGH_RANGE_DATA_MAX = 0X7FFE  # Maximum distance in high-range_value mode.
    DATA_MIN = 0  # Minimum distance measurable.
    LOW_RANGE_FEATURE_FIELDS = Field(
        FEATURE_DATA_NAME,
        FEATURE_UNIT,
        FieldType.UInt16,
        LOW_RANGE_DATA_MAX,
        DATA_MIN)
    HIGH_RANGE_FEATURE_FIELDS = Field(
        FEATURE_DATA_NAME,
        FEATURE_UNIT,
        FieldType.UInt16,
        HIGH_RANGE_DATA_MAX,
        DATA_MIN)
    DATA_LENGTH_BYTES = 2

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureProximity, self).__init__(
            self.FEATURE_NAME, node, [self.HIGH_RANGE_FEATURE_FIELDS])

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.
        
        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidDataException`
                if the data array has not enough data to read.
        """
        if len(data) - offset < self.DATA_LENGTH_BYTES:
            raise BlueSTInvalidDataException(
                'There are no %d bytes available to read.' \
                % (self.DATA_LENGTH_BYTES))
        sample = None
        value = LittleEndian.bytes_to_uint16(data, offset)
        if self._is_low_range_sensor(value):
            sample = self._get_low_range_sample(timestamp, value)
        else:
            sample = self._get_high_range_sample(timestamp, value)
        return ExtractedData(sample, self.DATA_LENGTH_BYTES)

    @classmethod
    def get_proximity_distance(self, sample):
        """Extract the data from the feature's raw data.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            int: The proximity distance value if the data array is valid, "-1"
            otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[0] is not None:
                    return int(sample._data[0])
        return -1

    @classmethod
    def is_out_of_range_distance(self, sample):
        """Check if the measure is out of range_value.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            bool: True if the proximity distance is out of range_value, False
            otherwise.
        """
        return self.get_proximity_distance(s) == self.OUT_OF_RANGE_VALUE

    @classmethod
    def _is_low_range_sensor(self, value):
        """Check if the sensor is a low-range_value sensor.

        Args:
            value (int): Measured proximity.
        
        Returns:
            bool: True if the sensor is a low-range_value sensor, False
            otherwise.
        """
        return (value & 0x8000) == 0

    @classmethod
    def _get_range_value(self, value):
        """Get the range_value value of the sensor.

        Args:
            value (int): Measured proximity.
        
        Returns:
            int: The range_value value of the sensor.
        """
        return (value & ~0x8000)

    def _get_low_range_sample(self, timestamp, value):
        """Get a sample from a low-range sensor built from the given data.

        Args:
            timestamp (int): Data's timestamp.
            value (int): Measured proximity.

        Returns:
            :class:`blue_st_sdk.feature.Sample`: A sample from a low-range
            sensor built from the given data.
        """
        range_value = self._get_range_value(value)
        if range_value > self.LOW_RANGE_DATA_MAX:
            range_value = self.OUT_OF_RANGE_VALUE
        return Sample(
            [range_value],
            [self.LOW_RANGE_FEATURE_FIELDS],
            timestamp)

    def _get_high_range_sample(self, timestamp, value):
        """Get a sample from a high-range sensor built from the given data.

        Args:
            timestamp (int): Data's timestamp.
            value (int): Measured proximity.

        Returns:
            :class:`blue_st_sdk.feature.Sample`: A sample from a high-range
            sensor built from the given data.
        """
        range_value = self._get_range_value(value)
        if range_value > self.HIGH_RANGE_DATA_MAX:
            range_value = self.OUT_OF_RANGE_VALUE
        return Sample(
            [range_value],
            [self.HIGH_RANGE_FEATURE_FIELDS],
            timestamp)

    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:
            distance = get_proximity_distance(sample)
            if distance != self.OUT_OF_RANGE_VALUE:
                result = '%s(%d): %s %s' \
                    % (self._name,
                       sample._timestamp,
                       str(distance),
                       self._description[0]._unit)
            else:
                result = '%s(%d): %s' \
                    % (self._name,
                       sample._timestamp,
                       'Out Of Range')
            return result
Example #5
0
class FeatureGyroscope(Feature):
    """The feature handles the data coming from a gyroscope sensor.

    Data is six bytes long and has one decimal digit.
    """

    FEATURE_NAME = "Gyroscope"
    FEATURE_UNIT = "dps"
    FEATURE_DATA_NAME = ["X", "Y", "Z"]
    DATA_MAX = ((float(1 << 15)) / 10.0)
    DATA_MIN = -DATA_MAX
    X_INDEX = 0
    Y_INDEX = 1
    Z_INDEX = 2
    FEATURE_X_FIELD = Field(
        FEATURE_DATA_NAME[X_INDEX],
        FEATURE_UNIT,
        FieldType.Float,
        DATA_MAX,
        DATA_MIN)
    FEATURE_Y_FIELD = Field(
        FEATURE_DATA_NAME[Y_INDEX],
        FEATURE_UNIT,
        FieldType.Float,
        DATA_MAX,
        DATA_MIN)
    FEATURE_Z_FIELD = Field(
        FEATURE_DATA_NAME[Z_INDEX],
        FEATURE_UNIT,
        FieldType.Float,
        DATA_MAX,
        DATA_MIN)
    DATA_LENGTH_BYTES = 6
    SCALE_FACTOR = 10.0

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureGyroscope, self).__init__(
            self.FEATURE_NAME, node, [self.FEATURE_X_FIELD,
                                      self.FEATURE_Y_FIELD,
                                      self.FEATURE_Z_FIELD])

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.
        
        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.InvalidDataException`
                if the data array has not enough data to read.
        """
        if len(data) - offset < self.DATA_LENGTH_BYTES:
            raise InvalidDataException(
                'There are no %s bytes available to read.' \
                % (self.DATA_LENGTH_BYTES))
        sample = Sample(
            [LittleEndian.bytes_to_int16(data, offset) / self.SCALE_FACTOR,
             LittleEndian.bytes_to_int16(data, offset + 2) / self.SCALE_FACTOR,
             LittleEndian.bytes_to_int16(data, offset + 4) / self.SCALE_FACTOR],
            self.get_fields_description(),
            timestamp)
        return ExtractedData(sample, self.DATA_LENGTH_BYTES)

    @classmethod
    def get_gyroscope_x(self, sample):
        """Get the gyroscope value on the X axis from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            float: The gyroscope value on the X axis if the data array is
            valid, <nan> otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[self.X_INDEX] is not None:
                    return float(sample._data[self.X_INDEX])
        return float('nan')

    @classmethod
    def get_gyroscope_y(self, sample):
        """Get the gyroscope value on the Y axis from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            float: The gyroscope value on the Y axis if the data array is
            valid, <nan> otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[self.Y_INDEX] is not None:
                    return float(sample._data[self.Y_INDEX])
        return float('nan')

    @classmethod
    def get_gyroscope_z(self, sample):
        """Get the gyroscope value on the Z axis from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            float: The gyroscope value on the Z axis if the data array is
            valid, <nan> otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[self.Z_INDEX] is not None:
                    return float(sample._data[self.Z_INDEX])
        return float('nan')

    def read_gyroscope(self):
        """Read the gyroscope values.

        Returns:
            list: The gyroscope values on the three axis if the read operation
            is successful, <nan> values otherwise.

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.InvalidOperationException`
                is raised if the feature is not enabled or the operation
                required is not supported.
            :exc:`blue_st_sdk.utils.blue_st_exceptions.InvalidDataException`
                if the data array has not enough data to read.
        """
        try:
            self._read_data()
            return [FeatureAccelerometer.get_gyroscope_x(self._get_sample()),
                FeatureAccelerometer.get_gyroscope_y(self._get_sample()),
                FeatureAccelerometer.get_gyroscope_z(self._get_sample())]
        except (InvalidOperationException, InvalidDataException) as e:
            raise e
Example #6
0
class FeatureHeartRate(DeviceTimestampFeature):
    """Feature that manages the Heart Rate'sample data as defined by the
    Bluetooth specification. 

    Refer to
    `Heart Rate Measurement Specs <https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml>`_
    for more information.
    """

    FEATURE_NAME = "Heart Rate"
    HEART_RATE_INDEX = 0
    HEART_RATE_FIELD = Field("Heart Rate Measurement", "bpm", FieldType.UInt16,
                             0, 1 << 16)
    ENERGY_EXPENDED_INDEX = 1
    ENERGY_EXPENDED_FIELD = Field("Energy Expended", "kJ", FieldType.UInt16, 0,
                                  1 << 16)
    RR_INTERVAL_INDEX = 2
    RR_INTERVAL_FIELD = Field("RR-Interval", "sample", FieldType.Float, 0,
                              sys.float_info.max)
    DATA_LENGTH_BYTES = 2

    def __init__(self, node):
        """Build a new disabled feature, that doesn't need to be initialized at
        node'sample side.
    
        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will update this
                feature.
        """
        super(FeatureHeartRate, self).__init__(self.FEATURE_NAME, node, [
            self.HEART_RATE_FIELD, self.ENERGY_EXPENDED_FIELD,
            self.RR_INTERVAL_FIELD
        ])

    @classmethod
    def getHeartRate(self, sample):
        """Extract the Heart Rate from the sample.
    
        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): The sample.

        Returns:
            int: The Heart Rate if available, a negative number otherwise.
        """
        if sample is not None:
            if len(sample._data) > self.HEART_RATE_INDEX:
                hr = sample._data[self.HEART_RATE_INDEX]
                if hr is not None:
                    return int(hr)
        return -1

    @classmethod
    def getEnergyExpended(self, sample):
        """Extract the energy expended field from the sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): The sample.

        Returns:
            int: The energy expended if available, a negative number otherwise.
        """
        if sample is not None:
            if len(sample._data) > self.ENERGY_EXPENDED_INDEX:
                ee = sample._data[self.ENERGY_EXPENDED_INDEX]
                if ee is not None:
                    return int(ee)
        return -1

    @classmethod
    def getRRInterval(self, sample):
        """Extract the RR interval field from the sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): The sample.
    
        Returns:
            float: The RR interval if available, <nan> otherwise.
        """
        if sample is not None:
            if len(sample._data) > self.RR_INTERVAL_INDEX:
                rri = sample._data[self.RR_INTERVAL_INDEX]
                if rri is not None:
                    return float(rri)
        return float('nan')

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.
        In this case it reads a 16-bit signed integer value.

        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`Exception` if the data array has not enough data to read.
        """
        if (len(data) - offset < self.DATA_LENGTH_BYTES):
            raise Exception('There are no %d bytes available to read.' %
                            (self.DATA_LENGTH_BYTES))

        offset = offset
        flags = data[offset]
        offset += 1

        if self.has8BitHeartRate(flags):
            hr = data[offset]
            offset += 1
        else:
            hr = LittleEndian.bytesToUInt16(data, offset)
            offset += 2

        if self.hasEnergyExpended(flags):
            ee = LittleEndian.bytesToUInt16(data, offset)
            offset += 2
        else:
            ee = -1

        if self.hasRRInterval(flags):
            rri = LittleEndian.bytesToUInt16(data, offset) / 1024.0
            offset += 2
        else:
            rri = float('nan')

        return ExtractedData(
            Sample(timestamp, [hr, ee, rri], getFieldsDescription()),
            offset - offset)

    @classmethod
    def has8BitHeartRate(self, flags):
        """Check if there is Heart Rate.

        Args:
            flags (int): Flags.

        Returns:
            bool: True if there is Heart Rate, False otherwise.
        """
        return (flags & 0x01) == 0

    @classmethod
    def hasEnergyExpended(self, flags):
        """Check if there is Energy Expended.

        Args:
            flags (int): Flags.

        Returns:
            bool: True if there is Energy Expended, False otherwise.
        """
        return (flags & 0x08) != 0

    @classmethod
    def hasRRInterval(self, flags):
        """Check if there is RR interval.

        Args:
            flags (int): Flags.
    
        Returns:
            bool: True if there is RR Interval, False otherwise.
        """
        return (flags & 0x10) != 0
Example #7
0
class FeatureStepperMotor(Feature):
    """The feature handles a stepper motor.

    It can be read or written and behaves differently depending on this.
    When read, the data read is the status of the motor, and is one byte long.
    When written, the data written is the command to be executed, and can be
    either one or five bytes long (see
    :meth:`blue_st_sdk.features.feature_stepper_motor.FeatureStepperMotor.write_motor_command`
    method).
    """

    FEATURE_NAME = "Stepper Motor"
    STATUS_FEATURE_FIELDS = Field(
        "Status",
        None,
        StepperMotorStatus,
        len(StepperMotorStatus),
        0)
    STATUS_DATA_LENGTH_BYTES = 1
    COMMAND_FEATURE_FIELDS = Field(
        "Command",
        None,
        StepperMotorCommands,
        len(StepperMotorCommands),
        0)
    COMMAND_DATA_LENGTH_BYTES = 5

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureStepperMotor, self).__init__(
            self.FEATURE_NAME, node, [self.STATUS_FEATURE_FIELDS])

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.
        
        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`Exception` if the data array has not enough data to read.
        """
        if len(data) - offset < self.STATUS_DATA_LENGTH_BYTES:
            raise Exception('There are no %d bytes available to read.' \
                % (self.STATUS_DATA_LENGTH_BYTES))
        sample = Sample(
            [NumberConversion.byteToUInt8(data, offset)],
            self.get_fields_description(),
            timestamp)
        return ExtractedData(sample, self.STATUS_DATA_LENGTH_BYTES)

    @classmethod
    def get_motor_status(self, sample):
        """Get the motor status.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.

        Returns:
            int: The motor status if the sample is valid, "-1" otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[0] is not None:
                    return int(sample._data[0])
        return -1

    def read_motor_status(self):
        """Read the motor status.

        Returns:
            :class:`StepperMotorStatus`: The motor status.

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.InvalidOperationException`
                is raised if the feature is not enabled or the operation
                required is not supported.
        """
        try:
            data = self.read_data()
            (ts, status) = struct.unpack('<Hb', data)
            return StepperMotorStatus(status)
        except InvalidOperationException as e:
            raise e

    def write_motor_command(self, command, steps=0):
        """Write the motor command.

        Args:
            command (:class:`StepperMotorCommands`):
                The command to be written.
            steps (int): The number of steps to perform, if required by the
                command.

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.InvalidOperationException`
                is raised if the feature is not enabled or the operation
                required is not supported.
        """
        # The command string can be:
        # + Either one byte long: for the command itself;
        # + Or five bytes long: one byte for the command and four bytes for the
        #   number of steps, if required by the command itself.
        if not steps:
            command_str = struct.pack('B', int(command.value))
        else:
            command_str = struct.pack('=BH', int(command.value), steps)

        try:
            self.write_data(command_str)
            # To clean the BLE buffer read the feature and throw away the data.
            #if self._parent.characteristic_can_be_read(self.get_characteristic()):
            #    self.read_data()
            characteristic = self.get_characteristic()
            char_handle = characteristic.getHandle()
            data = self._parent.readCharacteristic(char_handle)
        except InvalidOperationException as e:
            raise e

    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:
            if self.get_motor_status(sample):
                status = 'MOTOR_RUNNING'
            else:
                status = 'MOTOR_INACTIVE'
            result = '%s(%d): %s' \
                % (self._name,
                   sample._timestamp,
                   status)
            return result
Example #8
0
class FeatureAudioADPCM(Feature):
    """The feature handles the compressed audio data acquired from a microphone.

    Data is a twenty bytes array
    """
    FEATURE_NAME = "ADPCM Audio"
    FEATURE_UNIT = None
    FEATURE_DATA_NAME = "Audio"
    DATA_MAX = 0
    DATA_MIN = 256
    FEATURE_FIELDS = Field(FEATURE_DATA_NAME, FEATURE_UNIT,
                           FieldType.ByteArray, DATA_MAX, DATA_MIN)
    DATA_LENGTH_BYTES = 20
    AUDIO_PACKAGE_SIZE = 40

    bvSyncManager = None
    engineADPCM = None

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        global bvSyncManager
        super(FeatureAudioADPCM, self).__init__(self.FEATURE_NAME, node,
                                                [self.FEATURE_FIELDS])

        FeatureAudioADPCM.bvSyncManager = BVAudioSyncManager()
        FeatureAudioADPCM.engineADPCM = ADPCMEngine()

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.
        
        Args:
            data (byte[]): The data read from the feature (a 20 bytes array).
            offset (int): Offset where to start reading data (0 by default).
        
        Returns:
            :class:`blue_st_sdk.feature.ExtractedData`: Container of the number
            of bytes read (20)  and the extracted data (audio info, the 40
            shorts array).

        Raises:
            :exc:`Exception` if the data array has not enough data to read.
        """
        if len(data) != self.DATA_LENGTH_BYTES:
            raise Exception('There are no %d bytes available to read.' \
                % (self.DATA_LENGTH_BYTES))

        dataByte = bytearray(data)

        dataPkt = [None] * self.AUDIO_PACKAGE_SIZE
        for x in range(0, self.AUDIO_PACKAGE_SIZE / 2):
            dataPkt[2 * x] = self.engineADPCM.decode((dataByte[x] & 0x0F),
                                                     self.bvSyncManager)
            dataPkt[(2 * x) + 1] = self.engineADPCM.decode(
                ((dataByte[x] >> 4) & 0x0F), self.bvSyncManager)

        sample = Sample(dataPkt, self.get_fields_description(), None)
        return ExtractedData(sample, self.DATA_LENGTH_BYTES)

    @classmethod
    def get_audio(self, sample):
        """Get the audio data from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            short[]: audio values if the data array is valid, None[]
            otherwise.
        """
        audioPckt = []
        if sample is not None:
            if sample._data:
                if sample._data[0] is not None:
                    length = len(sample._data)
                    audioPckt = [None] * length

                    for i in range(0, length):
                        if sample.data[i] != None:
                            audioPckt[i] = LittleEndian.bytesToInt16(
                                sample._data[i], (2 * i))
                    return audioPckt
        return audioPckt

    def setAudioSyncParams(self, sample):
        """Set the object synchronization parameters necessary to the
		   decompression process
        Args:
            sample (Sample) extracted FeatureAudioADPCMSync Sample which
                contains the synchronization parameters
        
        """
        self.bvSyncManager.setSyncParams(sample)
class FeatureAccelerometer(Feature):
    """The feature handles the data coming from an accelerometer sensor.

    Data is six bytes long and has no decimal digits.
    """

    FEATURE_NAME = "Accelerometer"
    FEATURE_UNIT = "mg"
    FEATURE_DATA_NAME = ["X", "Y", "Z"]
    DATA_MAX = +2000
    DATA_MIN = -2000
    X_INDEX = 0
    Y_INDEX = 1
    Z_INDEX = 2
    FEATURE_X_FIELD = Field(FEATURE_DATA_NAME[X_INDEX], FEATURE_UNIT,
                            FieldType.Int16, DATA_MAX, DATA_MIN)
    FEATURE_Y_FIELD = Field(FEATURE_DATA_NAME[Y_INDEX], FEATURE_UNIT,
                            FieldType.Int16, DATA_MAX, DATA_MIN)
    FEATURE_Z_FIELD = Field(FEATURE_DATA_NAME[Z_INDEX], FEATURE_UNIT,
                            FieldType.Int16, DATA_MAX, DATA_MIN)
    DATA_LENGTH_BYTES = 6

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureAccelerometer, self).__init__(
            self.FEATURE_NAME, node,
            [self.FEATURE_X_FIELD, self.FEATURE_Y_FIELD, self.FEATURE_Z_FIELD])

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.
        
        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`Exception` if the data array has not enough data to read.
        """
        if len(data) - offset < self.DATA_LENGTH_BYTES:
            raise Exception('There are no %s bytes available to read.' \
                % (self.DATA_LENGTH_BYTES))
        sample = Sample([
            LittleEndian.bytesToInt16(data, offset),
            LittleEndian.bytesToInt16(data, offset + 2),
            LittleEndian.bytesToInt16(data, offset + 4)
        ], self.get_fields_description(), timestamp)
        return ExtractedData(sample, self.DATA_LENGTH_BYTES)

    @classmethod
    def get_acc_x(self, sample):
        """Get the accererometer value on the X axis from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            float: The accelerometer value on the X axis if the data array is
            valid, <nan> otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[self.X_INDEX] is not None:
                    return float(sample._data[self.X_INDEX])
        return float('nan')

    @classmethod
    def get_acc_y(self, sample):
        """Get the accererometer value on the Y axis from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            float: The accelerometer value on the Y axis if the data array is
            valid, <nan> otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[self.Y_INDEX] is not None:
                    return float(sample._data[self.Y_INDEX])
        return float('nan')

    @classmethod
    def get_acc_z(self, sample):
        """Get the accererometer value on the Z axis from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            float: The accelerometer value on the Z axis if the data array is
            valid, <nan> otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[self.Z_INDEX] is not None:
                    return float(sample._data[self.Z_INDEX])
        return float('nan')
class FeatureAudioADPCMSync(Feature):
    """The feature handles the audio synchronization parameters mandatory to the
    ADPCM audio decompression.
    """

    FEATURE_NAME = "ADPCM Sync"
    FEATURE_UNIT = None
    FEATURE_DATA_NAME = ["ADPCM_index", "ADPCM_predsample"]
    DATA_MAX = 32767
    DATA_MIN = -32768
    ADPCM_INDEX_INDEX = 0
    ADPCM_PREDSAMPLE_INDEX = 1
    FEATURE_INDEX_FIELD = Field(FEATURE_DATA_NAME[ADPCM_INDEX_INDEX],
                                FEATURE_UNIT, FieldType.Int16, DATA_MAX,
                                DATA_MIN)
    FEATURE_PREDSAMPLE_FIELD = Field(FEATURE_DATA_NAME[ADPCM_PREDSAMPLE_INDEX],
                                     FEATURE_UNIT, FieldType.Int32, DATA_MAX,
                                     DATA_MIN)
    DATA_LENGTH_BYTES = 6

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureAudioADPCMSync, self).__init__(
            self.FEATURE_NAME, node,
            [self.FEATURE_INDEX_FIELD, self.FEATURE_PREDSAMPLE_FIELD])

    def extract_data(self, timestamp, data, offset):
        """Extract the audio sync data from the feature's raw data.
           In this case it reads a short integer (adpcm_index) and an integer
           (adpcm_predsample).

        Args:
            data (bytearray): The data read from the feature (a 6 bytes array).
            offset (int): Offset where to start reading data (0 by default).
        
        Returns:
            :class:`blue_st_sdk.feature.ExtractedData`: Container of the number
            of bytes read (6)  and the extracted data (audio sync info, a short
            and an int).

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.InvalidDataException`
                if the data array has not enough data to read.
        """
        if len(data) != self.DATA_LENGTH_BYTES:
            raise InvalidDataException(
                'There are no %d bytes available to read.' \
                % (self.DATA_LENGTH_BYTES))

        sample = Sample([
            LittleEndian.bytes_to_int16(data, 0),
            LittleEndian.bytes_to_int32(data, 2)
        ], self.get_fields_description(), None)
        return ExtractedData(sample, self.DATA_LENGTH_BYTES)

    @staticmethod
    def getIndex(sample):
        """Method which extract the index synchronization parameter from a buffer
           passed as parameter

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data (6 bytes).
        
        Returns:
            short: The ADPCM index synch parameter if the data array is
            valid, "None" otherwise.
        """
        if sample is not None:
            if sample._data:
                return sample._data[FeatureAudioADPCMSync.ADPCM_INDEX_INDEX]
        return None

    @staticmethod
    def getPredictedSample(sample):
        """Method which extract the predsample synchronization parameter from a
           buffer passed as parameter

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data (6 bytes).
        
        Returns:
            short: The ADPCM predsample synch parameter if the data array is
            valid, "None" otherwise.
        """
        if sample is not None:
            if sample._data:
                return sample._data[
                    FeatureAudioADPCMSync.ADPCM_PREDSAMPLE_INDEX]
        return None
class FeatureHumidity(Feature):
    """The feature handles the data coming from a humidity sensor.

    Data is two bytes long and has one decimal digit.
    """

    FEATURE_NAME = "Humidity"
    FEATURE_UNIT = "%"
    FEATURE_DATA_NAME = "Humidity"
    DATA_MAX = 100
    DATA_MIN = 0
    FEATURE_FIELDS = Field(
        FEATURE_DATA_NAME,
        FEATURE_UNIT,
        FieldType.Float,
        DATA_MAX,
        DATA_MIN)
    DATA_LENGTH_BYTES = 2
    SCALE_FACTOR = 10.0

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureHumidity, self).__init__(
            self.FEATURE_NAME, node, [self.FEATURE_FIELDS])

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.
        
        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`Exception` if the data array has not enough data to read.
        """
        if len(data) - offset < self.DATA_LENGTH_BYTES:
            raise Exception('There are no %d bytes available to read.' \
                % (self.DATA_LENGTH_BYTES))
        sample = Sample(
            [LittleEndian.bytesToInt16(data, offset) / self.SCALE_FACTOR],
            self.get_fields_description(),
            timestamp)
        return ExtractedData(sample, self.DATA_LENGTH_BYTES)

    @classmethod
    def get_humidity(self, sample):
        """Get the humidity value from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            float: The humidity value if the data array is valid, <nan>
            otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[0] is not None:
                    return float(sample._data[0])
        return float('nan')
class FeatureTemperature(Feature):
    """The feature handles the data coming from a temperature sensor.

    Data is two bytes long and has one decimal digit.
    """

    FEATURE_NAME = "Temperature"
    FEATURE_UNIT = "C"  #"\u2103" # Celsius degrees.
    FEATURE_DATA_NAME = "Temperature"
    DATA_MAX = 100
    DATA_MIN = 0
    FEATURE_FIELDS = Field(FEATURE_DATA_NAME, FEATURE_UNIT, FieldType.Float,
                           DATA_MAX, DATA_MIN)
    DATA_LENGTH_BYTES = 2
    SCALE_FACTOR = 10.0

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureTemperature, self).__init__(self.FEATURE_NAME, node,
                                                 [self.FEATURE_FIELDS])

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.

        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.InvalidDataException`
                if the data array has not enough data to read.
        """
        if len(data) - offset < self.DATA_LENGTH_BYTES:
            raise InvalidDataException(
                'There are no %d bytes available to read.' \
                % (self.DATA_LENGTH_BYTES))
        sample = Sample(
            [LittleEndian.bytes_to_int16(data, offset) / self.SCALE_FACTOR],
            self.get_fields_description(), timestamp)
        return ExtractedData(sample, self.DATA_LENGTH_BYTES)

    @classmethod
    def get_temperature(self, sample):
        """Get the temperature value from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            float: The temperature value if the data array is valid, <nan>
            otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[0] is not None:
                    return float(sample._data[0])
        return float('nan')

    def read_temperature(self):
        """Read the temperature value.

        Returns:
            float: The temperature value if the read operation is successful,
            <nan> otherwise.

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.InvalidOperationException`
                is raised if the feature is not enabled or the operation
                required is not supported.
            :exc:`blue_st_sdk.utils.blue_st_exceptions.InvalidDataException`
                if the data array has not enough data to read.
        """
        try:
            self._read_data()
            return FeatureTemperature.get_temperature(self._get_sample())
        except (InvalidOperationException, InvalidDataException) as e:
            raise e
class FeatureBeamforming(Feature):
    """Feature that contains the current beamforming direction and a list of beamforming control commands.
    """

    FEATURE_NAME = "Beamforming"
    FEATURE_UNIT = None
    FEATURE_DATA_NAME = "Beamforming"
    DATA_MAX = 7
    DATA_MIN = 0
    FEATURE_FIELDS = Field(FEATURE_DATA_NAME, FEATURE_UNIT, FieldType.UInt8,
                           DATA_MAX, DATA_MIN)
    DATA_LENGTH_BYTES = 1

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureBeamforming, self).__init__(self.FEATURE_NAME, node,
                                                 [self.FEATURE_FIELDS])

    def extract_data(self, timestamp, data, offset):
        """Extract the beamforming direction from the node raw data, it will
           read a uint8 containing the beamforming direction value.
        
        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`Exception` if the data array has not enough data to read.
        """
        if len(data) - offset < self.DATA_LENGTH_BYTES:
            raise BlueSTInvalidDataException('There are no %d bytes available to read.' \
                % (self.DATA_LENGTH_BYTES))
        sample = Sample([NumberConversion.byteToUInt8(data, offset)],
                        self.get_fields_description(), timestamp)
        return ExtractedData(sample, self.DATA_LENGTH_BYTES)

    @classmethod
    def get_direction(self, sample):
        """Get the beamforming direction.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            int: The the beamforming direction if the sample is valid, "0xFF" otherwise.
        """
        if sample is not None:
            if has_valid_index(sample, 0):
                if sample._data[0] is not None:
                    return int(sample._data[0])
        return 0xFF
Example #14
0
class FeatureAudioSceneClassification(Feature):
    """The feature handles the type of scene that can be detected by a device.

    Data is one byte long and has no decimal digits.
    """

    FEATURE_NAME = "Audio Scene Classification"
    FEATURE_UNIT = None
    FEATURE_DATA_NAME = "SceneType"
    DATA_MAX = 3
    DATA_MIN = 0
    FEATURE_FIELDS = Field(FEATURE_DATA_NAME, FEATURE_UNIT, FieldType.UInt8,
                           DATA_MAX, DATA_MIN)
    DATA_LENGTH_BYTES = 1

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureAudioSceneClassification,
              self).__init__(self.FEATURE_NAME, node, [self.FEATURE_FIELDS])

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.
        
        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidDataException`
                if the data array has not enough data to read.
        """
        if len(data) - offset < self.DATA_LENGTH_BYTES:
            raise BlueSTInvalidDataException(
                'There is no %d byte available to read.' \
                % (self.DATA_LENGTH_BYTES))
        sample = Sample([NumberConversion.byte_to_uint8(data, offset)],
                        self.get_fields_description(), timestamp)
        return ExtractedData(sample, self.DATA_LENGTH_BYTES)

    @classmethod
    def get_scene(self, sample):
        """Getting the scene from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.

        Returns:
            :class:`SceneType`: The type of the scene if the sample is valid,
            "SceneType.ERROR" otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[0] is not None:
                    return SceneType(sample._data[0])
        return SceneType.ERROR

    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): Scene is \"%s\"' \
                % (self._name,
                   sample._timestamp,
                   str(self.get_scene(sample))
                   )
        return result
        DATA_MIN)
    FEATURE_QJ_FIELD = Field(
        FEATURE_DATA_NAME[QJ_INDEX],
        FEATURE_UNIT,
        FieldType.Float,
        DATA_MAX,
        DATA_MIN)
    FEATURE_QK_FIELD = Field(
        FEATURE_DATA_NAME[QK_INDEX],
        FEATURE_UNIT,
        FieldType.Float,
        DATA_MAX,
        DATA_MIN)
FEATURE_QS_FIELD = Field(
        FEATURE_DATA_NAME[QS_INDEX],
        FEATURE_UNIT,
        FieldType.Float,
        DATA_MAX,
        DATA_MIN)
    DATA_LENGTH_BYTES = 12

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureGyroscope, self).__init__(
            self.FEATURE_NAME, node, [self.FEATURE_QI_FIELD,
                                      self.FEATURE_QJ_FIELD,
                                      self.FEATURE_QK_FIELD,
class FeatureProximityGesture(Feature):
    """The feature handles the detected gesture using proximity sensors.

    Data is one byte long and has no decimal digits.
    """

    FEATURE_NAME = "Gesture"
    FEATURE_UNIT = None
    FEATURE_DATA_NAME = "Gesture"
    DATA_MAX = len(Gesture) - 1
    DATA_MIN = 0
    FEATURE_FIELDS = Field(FEATURE_DATA_NAME, FEATURE_UNIT, FieldType.UInt8,
                           DATA_MAX, DATA_MIN)
    DATA_LENGTH_BYTES = 1

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureProximityGesture, self).__init__(self.FEATURE_NAME, node,
                                                      [self.FEATURE_FIELDS])

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.
        
        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidDataException`
                if the data array has not enough data to read.
        """
        if len(data) - offset < self.DATA_LENGTH_BYTES:
            raise BlueSTInvalidDataException(
                'There is no %d byte available to read.' \
                % (self.DATA_LENGTH_BYTES))
        sample = Sample([NumberConversion.byte_to_uint8(data, offset)],
                        self.get_fields_description(), timestamp)
        return ExtractedData(sample, self.DATA_LENGTH_BYTES)

    @classmethod
    def get_gesture(self, sample):
        """Get the gesture value from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.

        Returns:
            :class:`blue_st_sdk.features.feature_proximity_gesture.Gesture`: The
            gesture value from a sample.
        """
        if sample is not None:
            if sample._data:
                if sample._data[0] is not None:
                    activity = sample._data[0]
                    if activity == 0:
                        return Gesture.UNKNOWN
                    elif activity == 1:
                        return Gesture.TAP
                    elif activity == 2:
                        return Gesture.LEFT
                    elif activity == 3:
                        return Gesture.RIGHT
                    else:
                        return Gesture.ERROR
        return Gesture.ERROR
class FeatureSwitch(Feature):
    """The feature handles an 8-bit switch status register, thus handling up to
    8 switches.

    Data is one byte long and has no decimal digits.
    """

    FEATURE_NAME = "Switch"
    FEATURE_UNIT = None
    FEATURE_DATA_NAME = "Status"
    DATA_MAX = 256
    DATA_MIN = 0
    FEATURE_FIELDS = Field(FEATURE_DATA_NAME, FEATURE_UNIT, FieldType.UInt8,
                           DATA_MAX, DATA_MIN)
    DATA_LENGTH_BYTES = 1

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureSwitch, self).__init__(self.FEATURE_NAME, node,
                                            [self.FEATURE_FIELDS])

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.
        
        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidDataException`
                if the data array has not enough data to read.
        """
        if len(data) - offset < self.DATA_LENGTH_BYTES:
            raise BlueSTInvalidDataException(
                'There is no %d byte available to read.' \
                % (self.DATA_LENGTH_BYTES))
        sample = Sample([NumberConversion.byte_to_uint8(data, offset)],
                        self.get_fields_description(), timestamp)
        return ExtractedData(sample, self.DATA_LENGTH_BYTES)

    @classmethod
    def get_switch_status(self, sample):
        """Get the switch status.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
        
        Returns:
            int: The switch status if the sample is valid, "-1" otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[0] is not None:
                    return int(sample._data[0])
        return -1

    def read_switch_status(self):
        """Read the switch status.

        Returns:
            int: The switch status.

        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._read_data()
            return self.get_switch_status(self._get_sample())
        except (BlueSTInvalidOperationException,
                BlueSTInvalidDataException) as e:
            raise e

    def write_switch_status(self, status):
        """Write the switch status.

        Args:
            status (int): The switch status.

        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:
            ts = 0
            status_str = struct.pack('<HB', ts, status)
            self._write_data(status_str)
        except BlueSTInvalidOperationException as e:
            raise e

    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:
            switch = 'ON' if self.get_switch_status(sample) else 'OFF'
            result = '%s(%d): %s' \
                % (self._name,
                   sample._timestamp,
                   switch)
            return result
class FeatureActivityRecognition(Feature):
    """The feature handles the activities that can be detected by a device.

    Data is one or two bytes long and has no decimal digits.
    """

    FEATURE_NAME = "Activity Recognition"
    FEATURE_UNIT = [None, "ms", None]
    FEATURE_DATA_NAME = ["Activity", "DateTime", "Algorithm"]
    DATA_MAX = 7
    DATA_MIN = 0
    ACTIVITY_INDEX = 0
    TIME_FIELD = 1
    ALGORITHM_INDEX = 2
    FEATURE_ACTIVITY_FIELD = Field(
        FEATURE_DATA_NAME[ACTIVITY_INDEX],
        FEATURE_UNIT[ACTIVITY_INDEX],
        FieldType.UInt8,
        DATA_MAX,
        DATA_MIN)
    FEATURE_TIME_FIELD = Field(
        FEATURE_DATA_NAME[TIME_FIELD],
        FEATURE_UNIT[TIME_FIELD],
        FieldType.DateTime,
        None,
        None)
    FEATURE_ALGORITHM_FIELD = Field(
        FEATURE_DATA_NAME[ALGORITHM_INDEX],
        FEATURE_UNIT[ALGORITHM_INDEX],
        FieldType.UInt8,
        0xFF,
        0)
    # This can be "2" in case there is even the algorithm type, "1" otherwise.
    DATA_LENGTH_BYTES = 1

    def __init__(self, node):
        """Constructor.

        Args:
            node (:class:`blue_st_sdk.node.Node`): Node that will send data to
                this feature.
        """
        super(FeatureActivityRecognition, self).__init__(
            self.FEATURE_NAME, node, [self.FEATURE_ACTIVITY_FIELD,
                                      self.FEATURE_TIME_FIELD,
                                      self.FEATURE_ALGORITHM_FIELD])

    def extract_data(self, timestamp, data, offset):
        """Extract the data from the feature's raw data.

        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`: Container of the number
            of bytes read and the extracted data.

        Raises:
            :exc:`blue_st_sdk.utils.blue_st_exceptions.InvalidDataException`
                if the data array has not enough data to read.
        """
        if len(data) - offset < self.DATA_LENGTH_BYTES:
            raise InvalidDataException(
                'There is no %s byte available to read.' \
                % (self.DATA_LENGTH_BYTES))
        if len(data) - offset == self.DATA_LENGTH_BYTES:
            # Extract the activity from the feature's raw data.
            sample = Sample(
                [NumberConversion.byte_to_uint8(data, offset),
                 datetime.now()],
                self.get_fields_description(),
                timestamp)
            return ExtractedData(sample, self.DATA_LENGTH_BYTES)
        else:
            # Extract the activity and the algorithm from the feature's raw data.
            sample = Sample(
                [NumberConversion.byte_to_uint8(data, offset),
                 datetime.now(),
                 NumberConversion.byte_to_uint8(data, offset + 1)],
                self.get_fields_description(),
                timestamp)
            return ExtractedData(sample, self.DATA_LENGTH_BYTES + 1)

    @classmethod
    def get_activity(self, sample):
        """Getting the recognized activity from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.

        Returns:
            :class:`ActivityType`: The recognized activity if the sample is
            valid, "ActivityType.ERROR" otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[self.ACTIVITY_INDEX] is not None:
                    return ActivityType(sample._data[self.ACTIVITY_INDEX])
        return ActivityType.ERROR

    @classmethod
    def get_time(self, sample):
        """Getting the date and the time from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.

        Returns:
            :class:`datetime`: The date and the time of the recognized activity
            if the sample is valid, "None" otherwise.
            Refer to
            `datetime <https://docs.python.org/2/library/datetime.html>`_
            for more information.
        """
        if sample is not None:
            if sample._data:
                if sample._data[self.TIME_FIELD] is not None:
                    return sample._data[self.TIME_FIELD]
        return None

    @classmethod
    def get_algorithm(self, sample):
        """Getting the algorithm from a sample.

        Args:
            sample (:class:`blue_st_sdk.feature.Sample`): Sample data.

        Returns:
            int: The algorithm if the sample is valid, "0" otherwise.
        """
        if sample is not None:
            if sample._data:
                if sample._data[self.ALGORITHM_INDEX] is not None:
                    return int(sample._data[self.ALGORITHM_INDEX])
        return 0

    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'

        result = ''
        if len(sample._data) >= 2:
            result = '%s(%d): Activity is \"%s\", Time is \"%s\"' \
                % (self._name,
                   sample._timestamp,
                   str(self.get_activity(sample)),
                   str(self.get_time(sample))
                   )
        if len(sample._data) == 3:
            result += ', Algorithm is \"%d\"' \
                % (self.get_algorithm(sample))
        return result