diff --git a/pyshimmer/__init__.py b/pyshimmer/__init__.py index 91b208a..891c1f2 100644 --- a/pyshimmer/__init__.py +++ b/pyshimmer/__init__.py @@ -13,12 +13,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . + from .bluetooth.bt_api import ShimmerBluetooth from .bluetooth.bt_commands import DataPacket from .dev.base import DEFAULT_BAUDRATE from .dev.channels import ChannelDataType, EChannelType from .dev.exg import ExGMux, ExGRLDLead, ERLDRef, ExGRegister from .dev.fw_version import EFirmwareType +from .dev.revisions import HardwareRevision, Shimmer3Revision from .reader.binary_reader import ShimmerBinaryReader from .reader.shimmer_reader import ShimmerReader from .uart.dock_api import ShimmerDock diff --git a/pyshimmer/dev/channels.py b/pyshimmer/dev/channels.py index 3f31564..877d94a 100644 --- a/pyshimmer/dev/channels.py +++ b/pyshimmer/dev/channels.py @@ -482,6 +482,7 @@ class ESensorGroup(Enum): ESensorGroup.EXG1_16BIT: 19, ESensorGroup.EXG2_24BIT: 20, ESensorGroup.EXG2_16BIT: 21, + ESensorGroup.TEMP: 22, } ENABLED_SENSORS_LEN = 0x03 diff --git a/pyshimmer/dev/revisions/__init__.py b/pyshimmer/dev/revisions/__init__.py new file mode 100644 index 0000000..1cebed6 --- /dev/null +++ b/pyshimmer/dev/revisions/__init__.py @@ -0,0 +1,17 @@ +# pyshimmer - API for Shimmer sensor devices +# Copyright (C) 2025 Lukas Magel + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from .revision import HardwareRevision +from .shimmer3 import Shimmer3Revision diff --git a/pyshimmer/dev/revisions/revision.py b/pyshimmer/dev/revisions/revision.py new file mode 100644 index 0000000..89dfc0a --- /dev/null +++ b/pyshimmer/dev/revisions/revision.py @@ -0,0 +1,266 @@ +# pyshimmer - API for Shimmer sensor devices +# Copyright (C) 2025 Lukas Magel + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from __future__ import annotations + +import operator +from abc import ABC, abstractmethod +from collections.abc import Iterable +from functools import reduce +from typing import overload + +import numpy as np + +from ..channels import EChannelType, ChannelDataType, ESensorGroup +from pyshimmer.util import bit_is_set, flatten_list + + +class HardwareRevision(ABC): + + @abstractmethod + def sr2dr(self, sr: float) -> int: + """Calculate equivalent device-specific rate for a sample rate in Hz + + Device-specific sample rates are given in absolute clock ticks per unit of time. + This function can be used to calculate such a rate for the Shimmer3. + + :param sr: The sampling rate in Hz + :return: An integer which represents the equivalent device-specific sampling rate + """ + pass + + @abstractmethod + def dr2sr(self, dr: int) -> float: + """Calculate equivalent sampling rate for a given device-specific rate + + Device-specific sample rates are given in absolute clock ticks per unit of time. + This function can be used to calculate a regular sampling rate in Hz from such a + rate. + + :param dr: The absolute device rate as integer + :return: A floating-point number that represents the sampling rate in Hz + """ + pass + + @overload + def sec2ticks(self, t_sec: float) -> int: ... + + @overload + def sec2ticks(self, t_sec: np.ndarray) -> np.ndarray: ... + + @abstractmethod + def sec2ticks(self, t_sec: float | np.ndarray) -> int | np.ndarray: + """Calculate equivalent device clock ticks for a time in seconds + + Args: + t_sec: A time in seconds + Returns: + An integer which represents the equivalent number of clock ticks + """ + pass + + @overload + def ticks2sec(self, t_ticks: int) -> float: ... + + @overload + def ticks2sec(self, t_ticks: np.ndarray) -> np.ndarray: ... + + @abstractmethod + def ticks2sec(self, t_ticks: int | np.ndarray) -> float | np.ndarray: + """Calculate the time in seconds equivalent to a device clock ticks count + + Args: + t_ticks: A clock tick counter for which to calculate the time in seconds + Returns: + A floating point time in seconds that is equivalent to the number of clock ticks + """ + pass + + @abstractmethod + def get_channel_dtypes( + self, channels: Iterable[EChannelType] + ) -> list[ChannelDataType]: + """Return the channel data types for a set of channels + + :param channels: A list of channels + :return: A list of channel data types with the same order + """ + pass + + @abstractmethod + def get_enabled_channels( + self, sensors: Iterable[ESensorGroup] + ) -> list[EChannelType]: + """Determine the set of data channels for a set of enabled sensors + + There exists a one-to-many mapping between enabled sensors and their corresponding + data channels. This function determines the set of necessary channels for a given + set of enabled sensors. + + :param sensors: A list of sensors that are enabled on a Shimmer + :return: A list of channels in the corresponding order + """ + pass + + @property + @abstractmethod + def sensorlist_size(self) -> int: + pass + + @abstractmethod + def sensors2bitfield(self, sensors: Iterable[ESensorGroup]) -> int: + """Convert an iterable of sensors into the corresponding bitfield transmitted to + the Shimmer + + :param sensors: A list of active sensors + :return: A bitfield that conveys the set of active sensors to the Shimmer + """ + pass + + @abstractmethod + def bitfield2sensors(self, bitfield: int) -> list[ESensorGroup]: + """Decode a bitfield returned from the Shimmer to a list of active sensors + + :param bitfield: The bitfield received from the Shimmer encoding the active sensors + :return: The corresponding list of active sensors + """ + pass + + @abstractmethod + def serialize_sensorlist(self, sensors: Iterable[ESensorGroup]) -> bytes: + """Serialize a list of sensors to the three-byte bitfield accepted by the Shimmer + + :param sensors: The list of sensors + :return: A byte string with length 3 that encodes the sensors + """ + pass + + @abstractmethod + def deserialize_sensorlist(self, bitfield_bin: bytes) -> list[ESensorGroup]: + """Deserialize the list of active sensors from the three-byte input received from + the Shimmer + + :param bitfield_bin: The input bitfield as byte string with length 3 + :return: The list of active sensors + """ + pass + + @abstractmethod + def sort_sensors(self, sensors: Iterable[ESensorGroup]) -> list[ESensorGroup]: + """Sorts the sensors in the list according to the sensor order + + This function is useful to determine the order in which sensor data will appear in + a data file by ordering the list of sensors according to their order in the file. + + :param sensors: An unsorted list of sensors + :return: A list with the same sensors as content but sorted according to their + appearance order in the data file + """ + pass + + +class BaseRevision(HardwareRevision): + + def __init__( + self, + dev_clock_rate: float, + sensor_list_dtype: ChannelDataType, + channel_data_types: dict[EChannelType, ChannelDataType], + sensor_channel_assignment: dict[ESensorGroup, list[EChannelType]], + sensor_bit_assignment: dict[ESensorGroup, int], + sensor_order: dict[ESensorGroup, int], + ): + self._dev_clock_rate = dev_clock_rate + self._sensor_list_dtype = sensor_list_dtype + self._channel_data_types = channel_data_types + self._sensor_channel_assignment = sensor_channel_assignment + self._sensor_bit_assignment = sensor_bit_assignment + self._sensor_order = sensor_order + + def sr2dr(self, sr: float) -> int: + dr_dec = self._dev_clock_rate / sr + return round(dr_dec) + + def dr2sr(self, dr: int) -> float: + return self._dev_clock_rate / dr + + @overload + def sec2ticks(self, t_sec: float) -> int: ... + + @overload + def sec2ticks(self, t_sec: np.ndarray) -> np.ndarray: ... + + def sec2ticks(self, t_sec: float | np.ndarray) -> int | np.ndarray: + t_ticks = t_sec * self._dev_clock_rate + if isinstance(t_ticks, np.ndarray): + return np.round(t_ticks, decimals=0) + + return round(t_ticks) + + @overload + def ticks2sec(self, t_ticks: int) -> float: ... + + @overload + def ticks2sec(self, t_ticks: np.ndarray) -> np.ndarray: ... + + def ticks2sec(self, t_ticks: int | np.ndarray) -> float | np.ndarray: + return t_ticks / self._dev_clock_rate + + def get_channel_dtypes( + self, channels: Iterable[EChannelType] + ) -> list[ChannelDataType]: + dtypes = [self._channel_data_types[ch] for ch in channels] + return dtypes + + def get_enabled_channels( + self, sensors: Iterable[ESensorGroup] + ) -> list[EChannelType]: + channels = [self._sensor_channel_assignment[e] for e in sensors] + return flatten_list(channels) + + @property + def sensorlist_size(self) -> int: + return self._sensor_list_dtype.size + + def sensors2bitfield(self, sensors: Iterable[ESensorGroup]) -> int: + if len(sensors) == 0: + return 0x0 + + bit_values = [1 << self._sensor_bit_assignment[g] for g in sensors] + return reduce(operator.or_, bit_values) + + def bitfield2sensors(self, bitfield: int) -> list[ESensorGroup]: + enabled_sensors = [] + for sensor in ESensorGroup: + bit_mask = 1 << self._sensor_bit_assignment[sensor] + if bit_is_set(bitfield, bit_mask): + enabled_sensors += [sensor] + + return self.sort_sensors(enabled_sensors) + + def serialize_sensorlist(self, sensors: Iterable[ESensorGroup]) -> bytes: + bitfield = self.sensors2bitfield(sensors) + return self._sensor_list_dtype.encode(bitfield) + + def deserialize_sensorlist(self, bitfield_bin: bytes) -> list[ESensorGroup]: + bitfield = self._sensor_list_dtype.decode(bitfield_bin) + return self.bitfield2sensors(bitfield) + + def sort_sensors(self, sensors: Iterable[ESensorGroup]) -> list[ESensorGroup]: + def sort_key_fn(x): + return self._sensor_order[x] + + sensors_sorted = sorted(sensors, key=sort_key_fn) + return sensors_sorted diff --git a/pyshimmer/dev/revisions/shimmer3.py b/pyshimmer/dev/revisions/shimmer3.py new file mode 100644 index 0000000..3c06b9e --- /dev/null +++ b/pyshimmer/dev/revisions/shimmer3.py @@ -0,0 +1,201 @@ +# pyshimmer - API for Shimmer sensor devices +# Copyright (C) 2025 Lukas Magel + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from __future__ import annotations + +from .revision import BaseRevision +from ..channels import EChannelType, ChannelDataType, ESensorGroup + + +class Shimmer3Revision(BaseRevision): + + # Device clock rate in ticks per second + DEV_CLOCK_RATE: float = 32768.0 + ENABLED_SENSORS_LEN = 0x03 + SENSOR_DTYPE = ChannelDataType(size=ENABLED_SENSORS_LEN, signed=False, le=True) + + CH_DTYPE_ASSIGNMENT: dict[EChannelType, ChannelDataType] = { + EChannelType.ACCEL_LN_X: ChannelDataType(2, signed=True, le=True), + EChannelType.ACCEL_LN_Y: ChannelDataType(2, signed=True, le=True), + EChannelType.ACCEL_LN_Z: ChannelDataType(2, signed=True, le=True), + EChannelType.VBATT: ChannelDataType(2, signed=True, le=True), + EChannelType.ACCEL_WR_X: ChannelDataType(2, signed=True, le=True), + EChannelType.ACCEL_WR_Y: ChannelDataType(2, signed=True, le=True), + EChannelType.ACCEL_WR_Z: ChannelDataType(2, signed=True, le=True), + EChannelType.MAG_REG_X: ChannelDataType(2, signed=True, le=True), + EChannelType.MAG_REG_Y: ChannelDataType(2, signed=True, le=True), + EChannelType.MAG_REG_Z: ChannelDataType(2, signed=True, le=True), + EChannelType.GYRO_X: ChannelDataType(2, signed=True, le=False), + EChannelType.GYRO_Y: ChannelDataType(2, signed=True, le=False), + EChannelType.GYRO_Z: ChannelDataType(2, signed=True, le=False), + EChannelType.EXTERNAL_ADC_A0: ChannelDataType(2, signed=False, le=True), + EChannelType.EXTERNAL_ADC_A1: ChannelDataType(2, signed=False, le=True), + EChannelType.EXTERNAL_ADC_A2: ChannelDataType(2, signed=False, le=True), + EChannelType.INTERNAL_ADC_A3: ChannelDataType(2, signed=False, le=True), + EChannelType.INTERNAL_ADC_A0: ChannelDataType(2, signed=False, le=True), + EChannelType.INTERNAL_ADC_A1: ChannelDataType(2, signed=False, le=True), + EChannelType.INTERNAL_ADC_A2: ChannelDataType(2, signed=False, le=True), + EChannelType.ACCEL_HG_X: None, + EChannelType.ACCEL_HG_Y: None, + EChannelType.ACCEL_HG_Z: None, + EChannelType.MAG_WR_X: None, + EChannelType.MAG_WR_Y: None, + EChannelType.MAG_WR_Z: None, + EChannelType.TEMPERATURE: ChannelDataType(2, signed=False, le=False), + EChannelType.PRESSURE: ChannelDataType(3, signed=False, le=False), + EChannelType.GSR_RAW: ChannelDataType(2, signed=False, le=True), + EChannelType.EXG1_STATUS: ChannelDataType(1, signed=False, le=True), + EChannelType.EXG1_CH1_24BIT: ChannelDataType(3, signed=True, le=False), + EChannelType.EXG1_CH2_24BIT: ChannelDataType(3, signed=True, le=False), + EChannelType.EXG2_STATUS: ChannelDataType(1, signed=False, le=True), + EChannelType.EXG2_CH1_24BIT: ChannelDataType(3, signed=True, le=False), + EChannelType.EXG2_CH2_24BIT: ChannelDataType(3, signed=True, le=False), + EChannelType.EXG1_CH1_16BIT: ChannelDataType(2, signed=True, le=False), + EChannelType.EXG1_CH2_16BIT: ChannelDataType(2, signed=True, le=False), + EChannelType.EXG2_CH1_16BIT: ChannelDataType(2, signed=True, le=False), + EChannelType.EXG2_CH2_16BIT: ChannelDataType(2, signed=True, le=False), + EChannelType.STRAIN_HIGH: ChannelDataType(2, signed=False, le=True), + EChannelType.STRAIN_LOW: ChannelDataType(2, signed=False, le=True), + EChannelType.TIMESTAMP: ChannelDataType(3, signed=False, le=True), + } + + SENSOR_CHANNEL_ASSIGNMENT: dict[ESensorGroup, list[EChannelType]] = { + ESensorGroup.ACCEL_LN: [ + EChannelType.ACCEL_LN_X, + EChannelType.ACCEL_LN_Y, + EChannelType.ACCEL_LN_Z, + ], + ESensorGroup.BATTERY: [EChannelType.VBATT], + ESensorGroup.EXT_CH_A0: [EChannelType.EXTERNAL_ADC_A0], + ESensorGroup.EXT_CH_A1: [EChannelType.EXTERNAL_ADC_A1], + ESensorGroup.EXT_CH_A2: [EChannelType.EXTERNAL_ADC_A2], + ESensorGroup.INT_CH_A0: [EChannelType.INTERNAL_ADC_A0], + ESensorGroup.INT_CH_A1: [EChannelType.INTERNAL_ADC_A1], + ESensorGroup.INT_CH_A2: [EChannelType.INTERNAL_ADC_A2], + ESensorGroup.STRAIN: [EChannelType.STRAIN_HIGH, EChannelType.STRAIN_LOW], + ESensorGroup.INT_CH_A3: [EChannelType.INTERNAL_ADC_A3], + ESensorGroup.GSR: [EChannelType.GSR_RAW], + ESensorGroup.GYRO: [ + EChannelType.GYRO_X, + EChannelType.GYRO_Y, + EChannelType.GYRO_Z, + ], + ESensorGroup.ACCEL_WR: [ + EChannelType.ACCEL_WR_X, + EChannelType.ACCEL_WR_Y, + EChannelType.ACCEL_WR_Z, + ], + ESensorGroup.MAG_REG: [ + EChannelType.MAG_REG_X, + EChannelType.MAG_REG_Y, + EChannelType.MAG_REG_Z, + ], + ESensorGroup.ACCEL_HG: [ + EChannelType.ACCEL_HG_X, + EChannelType.ACCEL_HG_Y, + EChannelType.ACCEL_HG_Z, + ], + ESensorGroup.MAG_WR: [ + EChannelType.MAG_WR_X, + EChannelType.MAG_WR_Y, + EChannelType.MAG_WR_Z, + ], + ESensorGroup.PRESSURE: [EChannelType.TEMPERATURE, EChannelType.PRESSURE], + ESensorGroup.EXG1_24BIT: [ + EChannelType.EXG1_STATUS, + EChannelType.EXG1_CH1_24BIT, + EChannelType.EXG1_CH2_24BIT, + ], + ESensorGroup.EXG1_16BIT: [ + EChannelType.EXG1_STATUS, + EChannelType.EXG1_CH1_16BIT, + EChannelType.EXG1_CH2_16BIT, + ], + ESensorGroup.EXG2_24BIT: [ + EChannelType.EXG2_STATUS, + EChannelType.EXG2_CH1_24BIT, + EChannelType.EXG2_CH2_24BIT, + ], + ESensorGroup.EXG2_16BIT: [ + EChannelType.EXG2_STATUS, + EChannelType.EXG2_CH1_16BIT, + EChannelType.EXG2_CH2_16BIT, + ], + # The MPU9150 Temp sensor is not yet available as a channel in the LogAndStream + # firmware + ESensorGroup.TEMP: [], + } + + SENSOR_BIT_ASSIGNMENT: dict[ESensorGroup, int] = { + ESensorGroup.EXT_CH_A1: 0, + ESensorGroup.EXT_CH_A0: 1, + ESensorGroup.GSR: 2, + ESensorGroup.EXG2_24BIT: 3, + ESensorGroup.EXG1_24BIT: 4, + ESensorGroup.MAG_REG: 5, + ESensorGroup.GYRO: 6, + ESensorGroup.ACCEL_LN: 7, + ESensorGroup.INT_CH_A1: 8, + ESensorGroup.INT_CH_A0: 9, + ESensorGroup.INT_CH_A3: 10, + ESensorGroup.EXT_CH_A2: 11, + ESensorGroup.ACCEL_WR: 12, + ESensorGroup.BATTERY: 13, + # No assignment 14 + ESensorGroup.STRAIN: 15, + # No assignment 16 + ESensorGroup.TEMP: 17, + ESensorGroup.PRESSURE: 18, + ESensorGroup.EXG2_16BIT: 19, + ESensorGroup.EXG1_16BIT: 20, + ESensorGroup.MAG_WR: 21, + ESensorGroup.ACCEL_HG: 22, + ESensorGroup.INT_CH_A2: 23, + } + + SENSOR_ORDER: dict[ESensorGroup, int] = { + ESensorGroup.ACCEL_LN: 1, + ESensorGroup.BATTERY: 2, + ESensorGroup.EXT_CH_A0: 3, + ESensorGroup.EXT_CH_A1: 4, + ESensorGroup.EXT_CH_A2: 5, + ESensorGroup.INT_CH_A0: 6, + ESensorGroup.INT_CH_A1: 7, + ESensorGroup.INT_CH_A2: 8, + ESensorGroup.STRAIN: 9, + ESensorGroup.INT_CH_A3: 10, + ESensorGroup.GSR: 11, + ESensorGroup.GYRO: 12, + ESensorGroup.ACCEL_WR: 13, + ESensorGroup.MAG_REG: 14, + ESensorGroup.ACCEL_HG: 15, + ESensorGroup.MAG_WR: 16, + ESensorGroup.PRESSURE: 17, + ESensorGroup.EXG1_24BIT: 18, + ESensorGroup.EXG1_16BIT: 19, + ESensorGroup.EXG2_24BIT: 20, + ESensorGroup.EXG2_16BIT: 21, + ESensorGroup.TEMP: 22, + } + + def __init__(self): + super().__init__( + self.DEV_CLOCK_RATE, + self.SENSOR_DTYPE, + self.CH_DTYPE_ASSIGNMENT, + self.SENSOR_CHANNEL_ASSIGNMENT, + self.SENSOR_BIT_ASSIGNMENT, + self.SENSOR_ORDER, + ) diff --git a/test/dev/revision/__init__.py b/test/dev/revision/__init__.py new file mode 100644 index 0000000..6e42188 --- /dev/null +++ b/test/dev/revision/__init__.py @@ -0,0 +1,15 @@ +# pyshimmer - API for Shimmer sensor devices +# Copyright (C) 2025 Lukas Magel + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/test/dev/revision/test_shimmer3_revision.py b/test/dev/revision/test_shimmer3_revision.py new file mode 100644 index 0000000..a155a24 --- /dev/null +++ b/test/dev/revision/test_shimmer3_revision.py @@ -0,0 +1,200 @@ +# pyshimmer - API for Shimmer sensor devices +# Copyright (C) 2025 Lukas Magel + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from __future__ import annotations + +import itertools + +import numpy as np +import pytest + +from pyshimmer import Shimmer3Revision, EChannelType +from pyshimmer.dev.channels import ESensorGroup + + +class TestShimmer3Revision: + + @pytest.fixture + def revision(self) -> Shimmer3Revision: + return Shimmer3Revision() + + def test_sr2dr(self, revision: Shimmer3Revision): + r = revision.sr2dr(1024.0) + assert r == 32 + + r = revision.sr2dr(500.0) + assert r == 66 + + def test_dr2sr(self, revision: Shimmer3Revision): + r = revision.dr2sr(65) + assert r == pytest.approx(504, abs=0.5) + + r = revision.dr2sr(32) + assert r == 1024.0 + + r = revision.dr2sr(64) + assert r == 512.0 + + def test_sec2ticks(self, revision: Shimmer3Revision): + r = revision.sec2ticks(1.0) + assert r == 32768 + + r = revision.sec2ticks(np.array([1.0, 2.0])) + np.testing.assert_array_equal(r, np.array([32768, 65536])) + + def test_ticks2sec(self, revision: Shimmer3Revision): + r = revision.ticks2sec(32768) + assert r == 1.0 + + r = revision.ticks2sec(65536) + assert r == 2.0 + + i = np.array([32768, 65536]) + r = revision.ticks2sec(i) + np.testing.assert_array_equal(r, np.array([1.0, 2.0])) + + def test_get_channel_dtypes(self, revision: Shimmer3Revision): + r = revision.get_channel_dtypes([]) + assert r == [] + + r = revision.get_channel_dtypes(()) + assert r == [] + + r = revision.get_channel_dtypes( + [EChannelType.INTERNAL_ADC_A0, EChannelType.INTERNAL_ADC_A0] + ) + assert len(r) == 2 + assert r[0] == r[1] + + channels = [EChannelType.INTERNAL_ADC_A1, EChannelType.GYRO_Y] + r = revision.get_channel_dtypes(channels) + + assert len(r) == 2 + first, second = r + + assert first.size == 2 + assert first.little_endian is True + assert first.signed is False + + assert second.size == 2 + assert second.little_endian is False + assert second.signed is True + + def test_channel_dtype_assignment(self, revision: Shimmer3Revision): + for channel in EChannelType: + r = revision.get_channel_dtypes([channel]) + assert len(r) > 0 + + def test_get_enabled_channels(self, revision: Shimmer3Revision): + r = revision.get_enabled_channels([]) + assert r == [] + + r = revision.get_enabled_channels(()) + assert r == [] + + r = revision.get_enabled_channels( + [ESensorGroup.PRESSURE, ESensorGroup.ACCEL_LN] + ) + + assert r == [ + EChannelType.TEMPERATURE, + EChannelType.PRESSURE, + EChannelType.ACCEL_LN_X, + EChannelType.ACCEL_LN_Y, + EChannelType.ACCEL_LN_Z, + ] + + def test_sensor_group_assignment(self, revision: Shimmer3Revision): + for group in ESensorGroup: + r = revision.get_enabled_channels([group]) + + if group != ESensorGroup.TEMP: + assert len(r) > 0 + + def test_sensor_list_to_bitfield(self, revision: Shimmer3Revision): + r = revision.sensors2bitfield((ESensorGroup.ACCEL_LN, ESensorGroup.EXT_CH_A1)) + assert r == 0x81 + + r = revision.sensors2bitfield((ESensorGroup.STRAIN, ESensorGroup.INT_CH_A1)) + assert r == 0x8100 + + r = revision.sensors2bitfield((ESensorGroup.INT_CH_A2, ESensorGroup.TEMP)) + assert r == 0x820000 + + def test_bitfield_to_sensors(self, revision: Shimmer3Revision): + r = revision.bitfield2sensors(0x81) + assert r == [ESensorGroup.ACCEL_LN, ESensorGroup.EXT_CH_A1] + + r = revision.bitfield2sensors(0x8100) + assert r == [ESensorGroup.INT_CH_A1, ESensorGroup.STRAIN] + + r = revision.bitfield2sensors(0x820000) + assert r == [ + ESensorGroup.INT_CH_A2, + ESensorGroup.TEMP, + ] + + def test_sensor_bit_assignment_uniqueness(self, revision: Shimmer3Revision): + for group1, group2 in itertools.product(ESensorGroup, ESensorGroup): + if group1 == group2: + continue + + bitfield1 = revision.sensors2bitfield([group1]) + bitfield2 = revision.sensors2bitfield([group2]) + assert bitfield1 != bitfield2 + + def test_serialize_sensorlist(self, revision: Shimmer3Revision): + r = revision.serialize_sensorlist([]) + assert r == b"\x00\x00\x00" + + r = revision.serialize_sensorlist([ESensorGroup.GSR, ESensorGroup.BATTERY]) + assert r == b"\x04\x20\x00" + + def test_deserialize_sensorlist(self, revision: Shimmer3Revision): + r = revision.deserialize_sensorlist(b"\x00\x00\x00") + assert r == [] + + r = revision.deserialize_sensorlist(b"\x01\x80\x01") + assert r == [ + ESensorGroup.EXT_CH_A1, + ESensorGroup.STRAIN, + ] + + def test_serialize_deserialize(self, revision: Shimmer3Revision): + for group in ESensorGroup: + bitfield = revision.serialize_sensorlist([group]) + group_deserialized = revision.deserialize_sensorlist(bitfield) + assert [group] == group_deserialized + + def test_sort_sensors(self, revision: Shimmer3Revision): + sensors = [ESensorGroup.BATTERY, ESensorGroup.ACCEL_LN] + expected = [ESensorGroup.ACCEL_LN, ESensorGroup.BATTERY] + r = revision.sort_sensors(sensors) + assert r == expected + + sensors = [ + ESensorGroup.EXT_CH_A2, + ESensorGroup.MAG_WR, + ESensorGroup.ACCEL_LN, + ESensorGroup.EXT_CH_A2, + ] + expected = [ + ESensorGroup.ACCEL_LN, + ESensorGroup.EXT_CH_A2, + ESensorGroup.EXT_CH_A2, + ESensorGroup.MAG_WR, + ] + r = revision.sort_sensors(sensors) + assert r == expected diff --git a/test/dev/test_device_channels.py b/test/dev/test_device_channels.py index f0bea02..ebdf200 100644 --- a/test/dev/test_device_channels.py +++ b/test/dev/test_device_channels.py @@ -28,6 +28,8 @@ EChannelType, ESensorGroup, sort_sensors, + sensors2bitfield, + bitfield2sensors, ) @@ -61,6 +63,16 @@ def test_channel_type_enum_for_id(self): EChannelType.enum_for_id(0x100) +class ESensorGroupTest: + + def test_sensor_group_uniqueness(self): + try: + # The exception will trigger upon import if the enum values are not unique + from pyshimmer.dev.channels import ESensorGroup + except ValueError as e: + pytest.fail(f"Enum not unique: {e}") + + class ChannelDataTypeTest(TestCase): def test_ch_dtype_byte_order(self): @@ -147,13 +159,6 @@ def test_get_ch_dtypes(self): self.assertEqual(second.little_endian, False) self.assertEqual(second.signed, True) - def test_sensor_group_uniqueness(self): - try: - # The exception will trigger upon import if the enum values are not unique - from pyshimmer.dev.channels import ESensorGroup - except ValueError as e: - self.fail(f"Enum not unique: {e}") - def test_datatype_assignments(self): from pyshimmer.dev.channels import EChannelType @@ -168,6 +173,19 @@ def test_sensor_channel_assignments(self): if sensor not in SensorChannelAssignment: self.fail(f"No channels assigned to sensor type: {sensor}") + def test_sensor_list_to_bitfield(self): + assert sensors2bitfield((ESensorGroup.ACCEL_LN, ESensorGroup.EXT_CH_A1)) == 0x81 + assert sensors2bitfield((ESensorGroup.STRAIN, ESensorGroup.INT_CH_A1)) == 0x8100 + assert sensors2bitfield((ESensorGroup.INT_CH_A2, ESensorGroup.TEMP)) == 0x820000 + + def test_bitfield_to_sensors(self): + assert bitfield2sensors(0x81) == [ESensorGroup.ACCEL_LN, ESensorGroup.EXT_CH_A1] + assert bitfield2sensors(0x8100) == [ESensorGroup.INT_CH_A1, ESensorGroup.STRAIN] + assert bitfield2sensors(0x820000) == [ + ESensorGroup.INT_CH_A2, + ESensorGroup.TEMP, + ] + def test_sensor_bit_assignments_uniqueness(self): for s1 in SensorBitAssignments.keys(): for s2 in SensorBitAssignments.keys():