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/bluetooth/bt_commands.py b/pyshimmer/bluetooth/bt_commands.py
index 52e8080..df78e16 100644
--- a/pyshimmer/bluetooth/bt_commands.py
+++ b/pyshimmer/bluetooth/bt_commands.py
@@ -31,6 +31,7 @@
)
from pyshimmer.dev.exg import ExGRegister
from pyshimmer.dev.fw_version import HardwareVersion, get_firmware_type
+from pyshimmer.dev.revisions import HardwareRevision
from pyshimmer.util import (
bit_is_set,
resp_code_to_bytes,
@@ -40,13 +41,20 @@
class DataPacket:
- """Parses data packets received by the Shimmer device
- :arg stream_types: List of tuples that contains each data channel contained in the
- data packet as well as the corresponding data type decoder
- """
+ def __init__(
+ self,
+ rev: HardwareRevision,
+ stream_types: list[tuple[EChannelType, ChannelDataType]],
+ ):
+ """Parses data packets received by the Shimmer device
- def __init__(self, stream_types: list[tuple[EChannelType, ChannelDataType]]):
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ :param stream_types: List of tuples that contains each data channel contained in the
+ data packet as well as the corresponding data type decoder
+ """
+ self._rev = rev
self._types = stream_types
self._values = {}
@@ -87,7 +95,14 @@ def receive(self, ser: BluetoothSerial) -> None:
class ShimmerCommand(ABC):
- """Abstract base class that represents a command sent to the Shimmer"""
+
+ def __init__(self, rev: HardwareRevision):
+ """Abstract base class that represents a command sent to the Shimmer
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ self._rev = rev
@abstractmethod
def send(self, ser: BluetoothSerial) -> None:
@@ -124,14 +139,17 @@ def receive(self, ser: BluetoothSerial) -> any:
class ResponseCommand(ShimmerCommand, ABC):
- """Abstract base class for all commands that feature a command response
- :arg rcode: The response code of the response. Can be a single int for a
- single-byte response code or a tuple of ints or a bytes instance for a
- multi-byte response code
- """
+ def __init__(self, rev: HardwareRevision, rcode: int | bytes | tuple[int, ...]):
+ """Abstract base class for all commands that feature a command response
- def __init__(self, rcode: int | bytes | tuple[int, ...]):
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ :param rcode: The response code of the response. Can be a single int for a
+ single-byte response code or a tuple of ints or a bytes instance for a
+ multi-byte response code
+ """
+ super().__init__(rev)
self._rcode = resp_code_to_bytes(rcode)
def has_response(self) -> bool:
@@ -142,12 +160,15 @@ def get_response_code(self) -> bytes:
class OneShotCommand(ShimmerCommand):
- """Class for commands that only send a command code and have no response
- :arg cmd_code: The command code to send
- """
+ def __init__(self, rev: HardwareRevision, cmd_code: int):
+ """Class for commands that only send a command code and have no response
- def __init__(self, cmd_code: int):
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ :param cmd_code: The command code to send
+ """
+ super().__init__(rev)
self._code = cmd_code
def send(self, ser: BluetoothSerial) -> None:
@@ -155,20 +176,23 @@ def send(self, ser: BluetoothSerial) -> None:
class GetStringCommand(ResponseCommand):
- """Send a command that features a variable-length string as response
-
- :arg req_code: The command code of the request
- :arg resp_code: The response code
- :arg encoding: The encoding to use when reading the response string
- """
def __init__(
self,
+ rev: HardwareRevision,
req_code: int,
resp_code: int | bytes | tuple[int],
encoding: str = "utf8",
):
- super().__init__(resp_code)
+ """Send a command that features a variable-length string as response
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ :param req_code: The command code of the request
+ :param resp_code: The response code
+ :param encoding: The encoding to use when reading the response string
+ """
+ super().__init__(rev, resp_code)
self._req_code = req_code
self._encoding = encoding
@@ -181,14 +205,23 @@ def receive(self, ser: BluetoothSerial) -> any:
class SetStringCommand(ShimmerCommand):
- """A command for sending a variable-length string to the device
- :arg req_code: The code of the command request
- :arg str_data: The data to send as part of the request
- :arg encoding: The encoding to use when writing the data to the stream
- """
+ def __init__(
+ self,
+ rev: HardwareRevision,
+ req_code: int,
+ str_data: str,
+ encoding: str = "utf8",
+ ):
+ """A command for sending a variable-length string to the device
- def __init__(self, req_code: int, str_data: str, encoding: str = "utf8"):
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ :param req_code: The code of the command request
+ :param str_data: The data to send as part of the request
+ :param encoding: The encoding to use when writing the data to the stream
+ """
+ super().__init__(rev)
self._req_code = req_code
self._str_data = str_data
self._encoding = encoding
@@ -199,10 +232,14 @@ def send(self, ser: BluetoothSerial) -> None:
class GetSamplingRateCommand(ResponseCommand):
- """Retrieve the sampling rate in samples per second"""
- def __init__(self):
- super().__init__(SAMPLING_RATE_RESPONSE)
+ def __init__(self, rev: HardwareRevision):
+ """Retrieve the sampling rate in samples per second
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, SAMPLING_RATE_RESPONSE)
def send(self, ser: BluetoothSerial) -> None:
ser.write_command(GET_SAMPLING_RATE_COMMAND)
@@ -215,7 +252,8 @@ def receive(self, ser: BluetoothSerial) -> float:
class SetSamplingRateCommand(ShimmerCommand):
- def __init__(self, sr: float):
+ def __init__(self, rev: HardwareRevision, sr: float):
+ super().__init__(rev)
self._sr = sr
def send(self, ser: BluetoothSerial) -> None:
@@ -224,10 +262,14 @@ def send(self, ser: BluetoothSerial) -> None:
class GetBatteryCommand(ResponseCommand):
- """Retrieve the battery state"""
- def __init__(self, in_percent: bool):
- super().__init__(FULL_BATTERY_RESPONSE)
+ def __init__(self, rev: HardwareRevision, in_percent: bool):
+ """Retrieve the battery state
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, FULL_BATTERY_RESPONSE)
self._in_percent = in_percent
def send(self, ser: BluetoothSerial) -> None:
@@ -247,12 +289,15 @@ def receive(self, ser: BluetoothSerial) -> any:
class GetConfigTimeCommand(ResponseCommand):
- """Retrieve the config time that is stored in the Shimmer device
- configuration file
- """
- def __init__(self):
- super().__init__(CONFIGTIME_RESPONSE)
+ def __init__(self, rev: HardwareRevision):
+ """Retrieve the config time that is stored in the Shimmer device
+ configuration file
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, CONFIGTIME_RESPONSE)
def send(self, ser: BluetoothSerial) -> None:
ser.write_command(GET_CONFIGTIME_COMMAND)
@@ -263,13 +308,16 @@ def receive(self, ser: BluetoothSerial) -> any:
class SetConfigTimeCommand(ShimmerCommand):
- """Set the config time, which will be stored in the Shimmer device configuration
- file
- :arg time: The integer value to send
- """
+ def __init__(self, rev: HardwareRevision, time: int):
+ """Set the config time, which will be stored in the Shimmer device
+ configuration file
- def __init__(self, time: int):
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ :param time: The integer value to send
+ """
+ super().__init__(rev)
self._time = time
def send(self, ser: BluetoothSerial) -> None:
@@ -280,12 +328,14 @@ def send(self, ser: BluetoothSerial) -> None:
class GetRealTimeClockCommand(ResponseCommand):
- """
- Get the real-time clock as UNIX Timestamp in seconds
- """
- def __init__(self):
- super().__init__(RWC_RESPONSE)
+ def __init__(self, rev: HardwareRevision):
+ """Get the real-time clock as UNIX Timestamp in seconds
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, RWC_RESPONSE)
def send(self, ser: BluetoothSerial) -> None:
ser.write_command(GET_RWC_COMMAND)
@@ -296,13 +346,16 @@ def receive(self, ser: BluetoothSerial) -> float:
class SetRealTimeClockCommand(ShimmerCommand):
- """
- Set the real-time clock as UNIX timestamp in seconds
- :arg ts_sec: The UNIX timestamp in seconds
- """
+ def __init__(self, rev: HardwareRevision, ts_sec: float):
+ """
+ Set the real-time clock as UNIX timestamp in seconds
- def __init__(self, ts_sec: float):
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ :param ts_sec: The UNIX timestamp in seconds
+ """
+ super().__init__(rev)
self._time = int(ts_sec)
def send(self, ser: BluetoothSerial) -> None:
@@ -311,7 +364,6 @@ def send(self, ser: BluetoothSerial) -> None:
class GetStatusCommand(ResponseCommand):
- """Retrieve the current status of the device"""
STATUS_DOCKED_BF = 1 << 0
STATUS_SENSING_BF = 1 << 1
@@ -332,8 +384,13 @@ class GetStatusCommand(ResponseCommand):
STATUS_RED_LED_BF,
)
- def __init__(self):
- super().__init__(FULL_STATUS_RESPONSE)
+ def __init__(self, rev: HardwareRevision):
+ """Retrieve the current status of the device
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, FULL_STATUS_RESPONSE)
def unpack_status_bitfields(self, val: int) -> list[bool]:
values = [bit_is_set(val, f) for f in self.STATUS_BITFIELDS]
@@ -348,10 +405,14 @@ def receive(self, ser: BluetoothSerial) -> any:
class GetFirmwareVersionCommand(ResponseCommand):
- """Retrieve the firmware type and version"""
- def __init__(self):
- super().__init__(FW_VERSION_RESPONSE)
+ def __init__(self, rev: HardwareRevision):
+ """Retrieve the firmware type and version
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, FW_VERSION_RESPONSE)
def send(self, ser: BluetoothSerial) -> None:
ser.write_command(GET_FW_VERSION_COMMAND)
@@ -365,22 +426,25 @@ def receive(self, ser: BluetoothSerial) -> any:
class GetAllCalibrationCommand(ResponseCommand):
- """Returns all the stored calibration values (84 bytes) in the following order:
- ESensorGroup.ACCEL_LN (21 bytes)
- ESensorGroup.GYRO (21 bytes)
- ESensorGroup.MAG (21 bytes)
- ESensorGroup.ACCEL_WR (21 bytes)
+ def __init__(self, rev: HardwareRevision):
+ """Returns all the stored calibration values (84 bytes) in the following order:
- The breakdown of the kinematic (accel x 2, gyro and mag) calibration values is
- as follows:
- [bytes 0- 5] offset bias values: 3 (x,y,z) 16-bit signed integers (big endian).
- [bytes 6-11] sensitivity values: 3 (x,y,z) 16-bit signed integers (big endian).
- [bytes 12-20] alignment matrix: 9 values 8-bit signed integers.
- """
+ ESensorGroup.ACCEL_LN (21 bytes)
+ ESensorGroup.GYRO (21 bytes)
+ ESensorGroup.MAG (21 bytes)
+ ESensorGroup.ACCEL_WR (21 bytes)
- def __init__(self):
- super().__init__(ALL_CALIBRATION_RESPONSE)
+ The breakdown of the kinematic (accel x 2, gyro and mag) calibration values is
+ as follows:
+ [bytes 0- 5] offset bias values: 3 (x,y,z) 16-bit signed integers (big endian).
+ [bytes 6-11] sensitivity values: 3 (x,y,z) 16-bit signed integers (big endian).
+ [bytes 12-20] alignment matrix: 9 values 8-bit signed integers.
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, ALL_CALIBRATION_RESPONSE)
self._offset = 0x0
self._rlen = 0x54 # 84 bytes
@@ -395,13 +459,15 @@ def receive(self, ser: BluetoothSerial) -> any:
class InquiryCommand(ResponseCommand):
- """
- Perform an inquiry to determine the sample rate, buffer size, and active data
- channels
- """
- def __init__(self):
- super().__init__(INQUIRY_RESPONSE)
+ def __init__(self, rev: HardwareRevision):
+ """Perform an inquiry to determine the sample rate, buffer size,
+ and active data channels
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, INQUIRY_RESPONSE)
@staticmethod
def decode_channel_types(ct_bin: bytes) -> list[EChannelType]:
@@ -425,30 +491,40 @@ def receive(self, ser: BluetoothSerial) -> any:
class StartStreamingCommand(OneShotCommand):
- """Start streaming data over the Bluetooth channel"""
- def __init__(self):
- super().__init__(START_STREAMING_COMMAND)
+ def __init__(self, rev: HardwareRevision):
+ """Start streaming data over the Bluetooth channel
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, START_STREAMING_COMMAND)
class StopStreamingCommand(OneShotCommand):
- """Stop streaming data over the Bluetooth channel"""
- def __init__(self):
- super().__init__(STOP_STREAMING_COMMAND)
+ def __init__(self, rev: HardwareRevision):
+ """Stop streaming data over the Bluetooth channel
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, STOP_STREAMING_COMMAND)
class GetEXGRegsCommand(ResponseCommand):
- """Retrieve the current state of the ExG chip register
- Queries the values of all registers of the specified chip and returns it as an
- ExGRegister instance
+ def __init__(self, rev: HardwareRevision, chip_id: int):
+ """Retrieve the current state of the ExG chip register
- :arg chip_id: The chip id, can be one of [0, 1]
- """
+ Queries the values of all registers of the specified chip and returns it as an
+ ExGRegister instance
- def __init__(self, chip_id: int):
- super().__init__(EXG_REGS_RESPONSE)
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ :param chip_id: The chip id, can be one of [0, 1]
+ """
+ super().__init__(rev, EXG_REGS_RESPONSE)
self._chip = chip_id
self._offset = 0x0
@@ -469,14 +545,17 @@ def receive(self, ser: BluetoothSerial) -> any:
class SetEXGRegsCommand(ShimmerCommand):
- """Set the binary contents of the ExG registers of a chip
- :arg chip_id: The id of the chip, can be one of [0, 1]
- :arg offset: At which offset to write the data
- :arg data: The bytes to write to the registers
- """
+ def __init__(self, rev: HardwareRevision, chip_id: int, offset: int, data: bytes):
+ """Set the binary contents of the ExG registers of a chip
- def __init__(self, chip_id: int, offset: int, data: bytes):
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ :param chip_id: The id of the chip, can be one of [0, 1]
+ :param offset: At which offset to write the data
+ :param data: The bytes to write to the registers
+ """
+ super().__init__(rev)
self._chip = chip_id
self._offset = offset
self._data = data
@@ -488,25 +567,32 @@ def send(self, ser: BluetoothSerial) -> None:
class GetExperimentIDCommand(GetStringCommand):
- """Retrieve the experiment id"""
- def __init__(self):
- super().__init__(GET_EXPID_COMMAND, EXPID_RESPONSE)
+ def __init__(self, rev: HardwareRevision):
+ """Retrieve the experiment ID
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, GET_EXPID_COMMAND, EXPID_RESPONSE)
class SetExperimentIDCommand(SetStringCommand):
- """Set the experiment id
- :arg exp_id: The experiment id as string
- """
+ def __init__(self, rev: HardwareRevision, exp_id: str):
+ """Set the experiment ID
- def __init__(self, exp_id: str):
- super().__init__(SET_EXPID_COMMAND, exp_id)
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ :param exp_id: The experiment id as string
+ """
+ super().__init__(rev, SET_EXPID_COMMAND, exp_id)
class SetSensorsCommand(ShimmerCommand):
- def __init__(self, sensors: Iterable[ESensorGroup]):
+ def __init__(self, rev: HardwareRevision, sensors: Iterable[ESensorGroup]):
+ super().__init__(rev)
self._sensors = list(sensors)
def send(self, ser: BluetoothSerial) -> None:
@@ -515,17 +601,25 @@ def send(self, ser: BluetoothSerial) -> None:
class GetDeviceNameCommand(GetStringCommand):
- """Get the device name"""
- def __init__(self):
- super().__init__(GET_SHIMMERNAME_COMMAND, SHIMMERNAME_RESPONSE)
+ def __init__(self, rev: HardwareRevision):
+ """Get the device name
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, GET_SHIMMERNAME_COMMAND, SHIMMERNAME_RESPONSE)
class GetShimmerHardwareVersion(ResponseCommand):
- """Get the device hardware version"""
- def __init__(self):
- super().__init__(SHIMMER_VERSION_RESPONSE)
+ def __init__(self, rev: HardwareRevision):
+ """Get the device hardware version
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, SHIMMER_VERSION_RESPONSE)
def send(self, ser: BluetoothSerial) -> None:
ser.write_command(GET_SHIMMER_VERSION_COMMAND)
@@ -536,18 +630,20 @@ def receive(self, ser: BluetoothSerial) -> any:
class SetDeviceNameCommand(SetStringCommand):
- """Set the device name
- :arg dev_name: The new device name as string
- """
+ def __init__(self, rev: HardwareRevision, dev_name: str):
+ """Set the device name
- def __init__(self, dev_name: str):
- super().__init__(SET_SHIMMERNAME_COMMAND, dev_name)
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ :param dev_name: The new device name as string
+ """
+ super().__init__(rev, SET_SHIMMERNAME_COMMAND, dev_name)
class SetStatusAckCommand(ShimmerCommand):
- def __init__(self, enabled: bool):
+ def __init__(self, rev: HardwareRevision, enabled: bool):
"""Command to enable/disable the ACK byte before status messages
By default, the Shimmer firmware sends an acknowledgment byte before
@@ -556,9 +652,12 @@ def __init__(self, enabled: bool):
software. This command is used by the Python API to automatically disable
the acknowledgment when connecting to a Shimmer.
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
:param enabled: If set to True, the acknowledgment is sent. If set to False,
the acknowledgment is not sent.
"""
+ super().__init__(rev)
self._enabled = enabled
def send(self, ser: BluetoothSerial) -> None:
@@ -566,23 +665,34 @@ def send(self, ser: BluetoothSerial) -> None:
class StartLoggingCommand(OneShotCommand):
- """Begin logging data to the SD card"""
- def __init__(self):
- super().__init__(START_LOGGING_COMMAND)
+ def __init__(self, rev: HardwareRevision):
+ """Begin logging data to the SD card
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, START_LOGGING_COMMAND)
class StopLoggingCommand(OneShotCommand):
- """End logging data to the SD card"""
- def __init__(self):
- super().__init__(STOP_LOGGING_COMMAND)
+ def __init__(self, rev: HardwareRevision):
+ """End logging data to the SD card
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, STOP_LOGGING_COMMAND)
class DummyCommand(OneShotCommand):
- """
- Dummy command that is only acknowledged by the Shimmer but triggers no response
- """
- def __init__(self):
- super().__init__(DUMMY_COMMAND)
+ def __init__(self, rev: HardwareRevision):
+ """Dummy command that is only acknowledged by the Shimmer but
+ triggers no response
+
+ :param rev: The hardware revision of the Shimmer device this command
+ will be sent to
+ """
+ super().__init__(rev, DUMMY_COMMAND)
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/bluetooth/test_bt_commands.py b/test/bluetooth/test_bt_commands.py
index 57b7df7..53d1093 100644
--- a/test/bluetooth/test_bt_commands.py
+++ b/test/bluetooth/test_bt_commands.py
@@ -15,7 +15,7 @@
# along with this program. If not, see .
from __future__ import annotations
-from unittest import TestCase
+import pytest
from pyshimmer.bluetooth.bt_commands import (
GetShimmerHardwareVersion,
@@ -50,10 +50,15 @@
from pyshimmer.bluetooth.bt_serial import BluetoothSerial
from pyshimmer.dev.channels import ChDataTypeAssignment, EChannelType, ESensorGroup
from pyshimmer.dev.fw_version import EFirmwareType, HardwareVersion
+from pyshimmer.dev.revisions import Shimmer3Revision, HardwareRevision
from pyshimmer.test_util import MockSerial
+TEST_REVISIONS = [
+ Shimmer3Revision(),
+]
-class BluetoothCommandsTest(TestCase):
+
+class TestBluetoothCommands:
@staticmethod
def create_mock() -> tuple[BluetoothSerial, MockSerial]:
@@ -74,182 +79,215 @@ def assert_cmd(
cmd.send(serial)
actual_req_data = mock.test_get_write_data()
- self.assertEqual(actual_req_data, req_data)
+ assert actual_req_data == req_data
if resp_code is None:
- self.assertFalse(cmd.has_response())
+ assert not cmd.has_response()
return None
- self.assertTrue(cmd.has_response())
- self.assertEqual(cmd.get_response_code(), resp_code)
+ assert cmd.has_response()
+ assert cmd.get_response_code() == resp_code
mock.test_put_read_data(resp_data)
act_result = cmd.receive(serial)
if exp_result is not None:
- self.assertEqual(act_result, exp_result)
+ assert act_result == exp_result
return act_result
def test_response_command_code_conversion(self):
class TestCommand(ResponseCommand):
def __init__(self, rcode: int | bytes | tuple[int, ...]):
- super().__init__(rcode)
+ super().__init__(Shimmer3Revision(), rcode)
def send(self, ser: BluetoothSerial) -> None:
pass
cmd = TestCommand(10)
- self.assertEqual(cmd.get_response_code(), b"\x0a")
+ assert cmd.get_response_code() == b"\x0a"
cmd = TestCommand(20)
- self.assertEqual(cmd.get_response_code(), b"\x14")
+ assert cmd.get_response_code() == b"\x14"
cmd = TestCommand((10,))
- self.assertEqual(cmd.get_response_code(), b"\x0a")
+ assert cmd.get_response_code() == b"\x0a"
cmd = TestCommand((10, 20))
- self.assertEqual(cmd.get_response_code(), b"\x0a\x14")
+ assert cmd.get_response_code() == b"\x0a\x14"
cmd = TestCommand(b"\x10")
- self.assertEqual(cmd.get_response_code(), b"\x10")
+ assert cmd.get_response_code() == b"\x10"
cmd = TestCommand(b"\x10\x20")
- self.assertEqual(cmd.get_response_code(), b"\x10\x20")
+ assert cmd.get_response_code() == b"\x10\x20"
- def test_get_sampling_rate_command(self):
- cmd = GetSamplingRateCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_sampling_rate_command(self, rev):
+ cmd = GetSamplingRateCommand(rev)
self.assert_cmd(cmd, b"\x03", b"\x04", b"\x04\x40\x00", 512.0)
- def test_set_sampling_rate_command(self):
- cmd = SetSamplingRateCommand(sr=512.0)
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_set_sampling_rate_command(self, rev: HardwareRevision):
+ cmd = SetSamplingRateCommand(rev, sr=512.0)
self.assert_cmd(cmd, b"\x05\x40\x00")
- def test_get_battery_state_command(self):
- cmd = GetBatteryCommand(in_percent=True)
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_battery_state_command(self, rev: HardwareRevision):
+ cmd = GetBatteryCommand(rev, in_percent=True)
self.assert_cmd(cmd, b"\x95", b"\x8a\x94", b"\x8a\x94\x30\x0b\x80", 100)
- cmd = GetBatteryCommand(in_percent=False)
+ cmd = GetBatteryCommand(rev, in_percent=False)
self.assert_cmd(
cmd, b"\x95", b"\x8a\x94", b"\x8a\x94\x2e\x0b\x80", 4.168246153846154
)
- def test_set_sensors_command(self):
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_set_sensors_command(self, rev: HardwareRevision):
sensors = [
ESensorGroup.GYRO,
ESensorGroup.INT_CH_A1,
ESensorGroup.PRESSURE,
]
- cmd = SetSensorsCommand(sensors)
+ cmd = SetSensorsCommand(rev, sensors)
self.assert_cmd(cmd, b"\x08\x40\x01\x04")
- def test_get_config_time_command(self):
- cmd = GetConfigTimeCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_config_time_command(self, rev: HardwareRevision):
+ cmd = GetConfigTimeCommand(rev)
self.assert_cmd(cmd, b"\x87", b"\x86", b"\x86\x02\x34\x32", 42)
- def test_set_config_time_command(self):
- cmd = SetConfigTimeCommand(43)
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_set_config_time_command(self, rev: HardwareRevision):
+ cmd = SetConfigTimeCommand(rev, 43)
self.assert_cmd(cmd, b"\x85\x02\x34\x33")
- def test_get_rtc(self):
- cmd = GetRealTimeClockCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_rtc(self, rev: HardwareRevision):
+ cmd = GetRealTimeClockCommand(rev)
r = self.assert_cmd(
cmd, b"\x91", b"\x90", b"\x90\x1f\xb1\x93\x09\x00\x00\x00\x00"
)
- self.assertAlmostEqual(r, 4903.3837585)
+ assert r == pytest.approx(4903.3837585)
- def test_set_rtc(self):
- cmd = SetRealTimeClockCommand(10)
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_set_rtc(self, rev: HardwareRevision):
+ cmd = SetRealTimeClockCommand(rev, 10)
self.assert_cmd(cmd, b"\x8f\x00\x00\x05\x00\x00\x00\x00\x00")
- def test_get_status_command(self):
- cmd = GetStatusCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_status_command(self, rev: HardwareRevision):
+ cmd = GetStatusCommand(rev)
expected_result = [True, False, True, False, False, True, False, False]
self.assert_cmd(cmd, b"\x72", b"\x8a\x71", b"\x8a\x71\x25", expected_result)
- def test_get_firmware_version_command(self):
- cmd = GetFirmwareVersionCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_firmware_version_command(self, rev: HardwareRevision):
+ cmd = GetFirmwareVersionCommand(rev)
fw_type, major, minor, patch = self.assert_cmd(
cmd, b"\x2e", b"\x2f", b"\x2f\x03\x00\x00\x00\x0b\x00"
)
- self.assertEqual(fw_type, EFirmwareType.LogAndStream)
- self.assertEqual(major, 0)
- self.assertEqual(minor, 11)
- self.assertEqual(patch, 0)
-
- def test_inquiry_command(self):
- cmd = InquiryCommand()
+ assert fw_type == EFirmwareType.LogAndStream
+ assert major == 0
+ assert minor == 11
+ assert patch == 0
+
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_inquiry_command(self, rev: HardwareRevision):
+ cmd = InquiryCommand(rev)
sr, buf_size, ctypes = self.assert_cmd(
cmd, b"\x01", b"\x02", b"\x02\x40\x00\x01\xff\x01\x09\x01\x01\x12"
)
- self.assertEqual(sr, 512.0)
- self.assertEqual(buf_size, 1)
- self.assertEqual(ctypes, [EChannelType.INTERNAL_ADC_A1])
+ assert sr == 512.0
+ assert buf_size == 1
+ assert ctypes == [EChannelType.INTERNAL_ADC_A1]
- def test_start_streaming_command(self):
- cmd = StartStreamingCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_start_streaming_command(self, rev: HardwareRevision):
+ cmd = StartStreamingCommand(rev)
self.assert_cmd(cmd, b"\x07")
- def test_stop_streaming_command(self):
- cmd = StopStreamingCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_stop_streaming_command(self, rev: HardwareRevision):
+ cmd = StopStreamingCommand(rev)
self.assert_cmd(cmd, b"\x20")
- def test_start_logging_command(self):
- cmd = StartLoggingCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_start_logging_command(self, rev: HardwareRevision):
+ cmd = StartLoggingCommand(rev)
self.assert_cmd(cmd, b"\x92")
- def test_stop_logging_command(self):
- cmd = StopLoggingCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_stop_logging_command(self, rev: HardwareRevision):
+ cmd = StopLoggingCommand(rev)
self.assert_cmd(cmd, b"\x93")
- def test_get_exg_register_command(self):
- cmd = GetEXGRegsCommand(1)
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_exg_register_command(self, rev: HardwareRevision):
+ cmd = GetEXGRegsCommand(rev, 1)
r = self.assert_cmd(
cmd,
b"\x63\x01\x00\x0a",
b"\x62",
b"\x62\x0a\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01",
)
- self.assertEqual(r.binary, b"\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01")
+ assert r.binary == b"\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01"
- def test_get_exg_reg_fail(self):
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_exg_reg_fail(self, rev: HardwareRevision):
serial, mock = self.create_mock()
- cmd = GetEXGRegsCommand(1)
+ cmd = GetEXGRegsCommand(rev, 1)
mock.test_put_read_data(b"\x62\x04\x01\x02\x03\x04")
- self.assertRaises(ValueError, cmd.receive, serial)
-
- def test_get_allcalibration_command(self):
- cmd = GetAllCalibrationCommand()
- r = self.assert_cmd(
- cmd,
- b"\x2c",
- b"\x2d",
- b"\x2d\x08\xcd\x08\xcd\x08\xcd\x00\x5c\x00\x5c\x00\x5c\x00\x9c\x00\x9c\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x00\x19\x96\x19\x96\x19\x96\x00\x9c\x00\x9c\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x87\x06\x87\x06\x87\x00\x9c\x00\x64\x00\x00\x00\x00\x9c",
+ with pytest.raises(ValueError):
+ cmd.receive(serial)
+
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_allcalibration_command(self, rev: HardwareRevision):
+ response_data = (
+ b"\x2d\x08\xcd\x08\xcd\x08\xcd\x00\x5c\x00\x5c\x00\x5c\x00\x9c\x00"
+ b"\x9c\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x00\x19\x96\x19\x96"
+ b"\x19\x96\x00\x9c\x00\x9c\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x06\x87\x06\x87\x06\x87\x00\x9c\x00\x64"
+ b"\x00\x00\x00\x00\x9c"
)
- self.assertEqual(
- r.binary,
- b"\x08\xcd\x08\xcd\x08\xcd\x00\x5c\x00\x5c\x00\x5c\x00\x9c\x00\x9c\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x00\x19\x96\x19\x96\x19\x96\x00\x9c\x00\x9c\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x87\x06\x87\x06\x87\x00\x9c\x00\x64\x00\x00\x00\x00\x9c",
+ expected_result = (
+ b"\x08\xcd\x08\xcd\x08\xcd\x00\x5c\x00\x5c\x00\x5c\x00\x9c\x00\x9c"
+ b"\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x00\x19\x96\x19\x96\x19"
+ b"\x96\x00\x9c\x00\x9c\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x06\x87\x06\x87\x06\x87\x00\x9c\x00\x64\x00"
+ b"\x00\x00\x00\x9c"
)
- def test_set_exg_register_command(self):
- cmd = SetEXGRegsCommand(1, 0x02, b"\x10\x00")
+ cmd = GetAllCalibrationCommand(rev)
+ r = self.assert_cmd(cmd, b"\x2c", b"\x2d", response_data)
+ assert r.binary == expected_result
+
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_set_exg_register_command(self, rev: HardwareRevision):
+ cmd = SetEXGRegsCommand(rev, 1, 0x02, b"\x10\x00")
self.assert_cmd(cmd, b"\x61\x01\x02\x02\x10\x00")
- def test_get_experiment_id_command(self):
- cmd = GetExperimentIDCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_experiment_id_command(self, rev: HardwareRevision):
+ cmd = GetExperimentIDCommand(rev)
self.assert_cmd(cmd, b"\x7e", b"\x7d", b"\x7d\x06a_test", "a_test")
- def test_set_experiment_id_command(self):
- cmd = SetExperimentIDCommand("A_Test")
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_set_experiment_id_command(self, rev: HardwareRevision):
+ cmd = SetExperimentIDCommand(rev, "A_Test")
self.assert_cmd(cmd, b"\x7c\x06A_Test")
- def test_get_device_name_command(self):
- cmd = GetDeviceNameCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_device_name_command(self, rev: HardwareRevision):
+ cmd = GetDeviceNameCommand(rev)
self.assert_cmd(cmd, b"\x7b", b"\x7a", b"\x7a\x05S_PPG", "S_PPG")
- def test_get_hardware_version(self):
- cmd = GetShimmerHardwareVersion()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_get_hardware_version(self, rev: HardwareRevision):
+ cmd = GetShimmerHardwareVersion(rev)
self.assert_cmd(cmd, b"\x3f", b"\x25", b"\x25\x00", HardwareVersion.SHIMMER1)
self.assert_cmd(cmd, b"\x3f", b"\x25", b"\x25\x01", HardwareVersion.SHIMMER2)
self.assert_cmd(cmd, b"\x3f", b"\x25", b"\x25\x02", HardwareVersion.SHIMMER2R)
@@ -257,34 +295,38 @@ def test_get_hardware_version(self):
self.assert_cmd(cmd, b"\x3f", b"\x25", b"\x25\x0a", HardwareVersion.SHIMMER3R)
self.assert_cmd(cmd, b"\x3f", b"\x25", b"\x25\x04", HardwareVersion.UNKNOWN)
- def test_set_device_name_command(self):
- cmd = SetDeviceNameCommand("S_PPG")
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_set_device_name_command(self, rev: HardwareRevision):
+ cmd = SetDeviceNameCommand(rev, "S_PPG")
self.assert_cmd(cmd, b"\x79\x05S_PPG")
- def test_set_status_ack_command(self):
- cmd = SetStatusAckCommand(enabled=True)
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_set_status_ack_command(self, rev: HardwareRevision):
+ cmd = SetStatusAckCommand(rev, enabled=True)
self.assert_cmd(cmd, b"\xa3\x01")
- cmd = SetStatusAckCommand(enabled=False)
+ cmd = SetStatusAckCommand(rev, enabled=False)
self.assert_cmd(cmd, b"\xa3\x00")
- def test_dummy_command(self):
- cmd = DummyCommand()
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_dummy_command(self, rev: HardwareRevision):
+ cmd = DummyCommand(rev)
self.assert_cmd(cmd, b"\x96")
- def test_data_packet(self):
+ @pytest.mark.parametrize("rev", TEST_REVISIONS)
+ def test_data_packet(self, rev: HardwareRevision):
serial, mock = self.create_mock()
channels = [EChannelType.TIMESTAMP, EChannelType.INTERNAL_ADC_A1]
data_types = [ChDataTypeAssignment[c] for c in channels]
ch_and_types = list(zip(channels, data_types))
- pkt = DataPacket(ch_and_types)
- self.assertEqual(pkt.channels, channels)
- self.assertEqual(pkt.channel_types, data_types)
+ pkt = DataPacket(rev, ch_and_types)
+ assert pkt.channels == channels
+ assert pkt.channel_types == data_types
mock.test_put_read_data(b"\x00\xde\xd0\xb2\x26\x07")
pkt.receive(serial)
- self.assertEqual(pkt[EChannelType.TIMESTAMP], 0xB2D0DE)
- self.assertEqual(pkt[EChannelType.INTERNAL_ADC_A1], 0x0726)
+ assert pkt[EChannelType.TIMESTAMP] == 0xB2D0DE
+ assert pkt[EChannelType.INTERNAL_ADC_A1] == 0x0726
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():