Repository: seemoo-lab/pyshimmer

Issue:
New Shimmer hardware revision Shimmer 3R
Collect the necessary changes for the new Shimmer hardware revision 3R

Hints from issue comments:

Solution patch:
diff --git a/pyshimmer/bluetooth/bt_api.py b/pyshimmer/bluetooth/bt_api.py
index 36033c2..aeb4bba 100644
--- a/pyshimmer/bluetooth/bt_api.py
+++ b/pyshimmer/bluetooth/bt_api.py
@@ -19,7 +19,7 @@ from typing import List, Tuple, Callable, Iterable, Optional
 
 from serial import Serial
 
-from pyshimmer.bluetooth.bt_commands import ShimmerCommand, GetSamplingRateCommand, GetConfigTimeCommand, \
+from pyshimmer.bluetooth.bt_commands import GetShimmerHardwareVersion, ShimmerCommand, GetSamplingRateCommand, GetConfigTimeCommand, \
     SetConfigTimeCommand, GetRealTimeClockCommand, SetRealTimeClockCommand, GetStatusCommand, \
     GetFirmwareVersionCommand, InquiryCommand, StartStreamingCommand, StopStreamingCommand, DataPacket, \
     GetEXGRegsCommand, SetEXGRegsCommand, StartLoggingCommand, StopLoggingCommand, GetExperimentIDCommand, \
@@ -29,7 +29,7 @@ from pyshimmer.bluetooth.bt_const import ACK_COMMAND_PROCESSED, DATA_PACKET, FUL
 from pyshimmer.bluetooth.bt_serial import BluetoothSerial
 from pyshimmer.dev.channels import ChDataTypeAssignment, ChannelDataType, EChannelType, ESensorGroup
 from pyshimmer.dev.exg import ExGRegister
-from pyshimmer.dev.fw_version import EFirmwareType, FirmwareVersion, FirmwareCapabilities
+from pyshimmer.dev.fw_version import EFirmwareType, FirmwareVersion, FirmwareCapabilities, HardwareVersion
 from pyshimmer.serial_base import ReadAbort
 from pyshimmer.util import fmt_hex, PeekQueue
 
@@ -283,6 +283,7 @@ class ShimmerBluetooth:
 
         self._fw_version: Optional[FirmwareVersion] = None
         self._fw_caps: Optional[FirmwareCapabilities] = None
+        self._hw_version: Optional[HardwareVersion] = None
 
     @property
     def initialized(self) -> bool:
@@ -314,6 +315,7 @@ class ShimmerBluetooth:
     def _set_fw_capabilities(self) -> None:
         fw_type, fw_ver = self.get_firmware_version()
         self._fw_caps = FirmwareCapabilities(fw_type, fw_ver)
+        self._hw_version = self.get_device_hardware_version()
 
     def initialize(self) -> None:
         """Initialize the Bluetooth connection
@@ -322,7 +324,6 @@ class ShimmerBluetooth:
         optionally disables the status acknowledgment and starts the read loop.
         """
         self._thread.start()
-
         self._set_fw_capabilities()
 
         if self.capabilities.supports_ack_disable and self._disable_ack:
@@ -503,6 +504,13 @@ class ShimmerBluetooth:
         :param dev_name: The device name to set
         """
         self._process_and_wait(SetDeviceNameCommand(dev_name))
+        
+    def get_device_hardware_version(self) -> HardwareVersion:
+        """Retrieve the device hardware version
+        
+        :return: The device hardware version as string
+        """
+        return self._process_and_wait(GetShimmerHardwareVersion())
 
     def get_experiment_id(self) -> str:
         """Retrieve the experiment id as string
diff --git a/pyshimmer/bluetooth/bt_commands.py b/pyshimmer/bluetooth/bt_commands.py
index 5813fa7..4f9d733 100644
--- a/pyshimmer/bluetooth/bt_commands.py
+++ b/pyshimmer/bluetooth/bt_commands.py
@@ -24,7 +24,7 @@ from pyshimmer.dev.base import dr2sr, sr2dr, sec2ticks, ticks2sec
 from pyshimmer.dev.channels import ChannelDataType, EChannelType, ESensorGroup, serialize_sensorlist
 from pyshimmer.dev.exg import ExGRegister
 from pyshimmer.dev.calibration import AllCalibration
-from pyshimmer.dev.fw_version import get_firmware_type
+from pyshimmer.dev.fw_version import HardwareVersion, get_firmware_type
 
 from pyshimmer.util import bit_is_set, resp_code_to_bytes, calibrate_u12_adc_value, battery_voltage_to_percent
 
@@ -503,7 +503,22 @@ class GetDeviceNameCommand(GetStringCommand):
     """
 
     def __init__(self):
-        super().__init__(GET_SHIMMERNAME_COMMAND, SHIMMERNAME_RESPONSE)
+        super().__init__(GET_SHIMMERNAME_COMMAND, SHIMMERNAME_RESPONSE)     
+
+
+class GetShimmerHardwareVersion(ResponseCommand):
+    """Get the device hardware version
+    
+    """
+    def __init__(self):
+        super().__init__(SHIMMER_VERSION_RESPONSE)
+        
+    def send(self, ser: BluetoothSerial) -> None:
+        ser.write_command(GET_SHIMMER_VERSION_COMMAND)
+        
+    def receive(self, ser: BluetoothSerial) -> any:
+        hw_version = ser.read_response(SHIMMER_VERSION_RESPONSE, arg_format='<B')
+        return HardwareVersion.from_int(hw_version)
 
 
 class SetDeviceNameCommand(SetStringCommand):
diff --git a/pyshimmer/bluetooth/bt_const.py b/pyshimmer/bluetooth/bt_const.py
index 7df4809..d9e2438 100644
--- a/pyshimmer/bluetooth/bt_const.py
+++ b/pyshimmer/bluetooth/bt_const.py
@@ -40,6 +40,9 @@ SET_SENSORS_COMMAND = 0x08
 STOP_STREAMING_COMMAND = 0x20
 # No response for command
 
+GET_SHIMMER_VERSION_COMMAND = 0x3F
+SHIMMER_VERSION_RESPONSE = 0x25
+
 GET_CONFIGTIME_COMMAND = 0x87
 CONFIGTIME_RESPONSE = 0x86
 
@@ -133,4 +136,4 @@ BtChannelsByIndex = {
     0x26: EChannelType.EXG_ADS1292R_2_CH2_16BIT,
     0x27: EChannelType.STRAIN_HIGH,
     0x28: EChannelType.STRAIN_LOW,
-}
+}
\ No newline at end of file
diff --git a/pyshimmer/dev/fw_version.py b/pyshimmer/dev/fw_version.py
index 3c3ddb9..974765e 100644
--- a/pyshimmer/dev/fw_version.py
+++ b/pyshimmer/dev/fw_version.py
@@ -13,7 +13,7 @@
 
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
-from enum import Enum, auto
+from enum import Enum, IntEnum, auto
 
 
 def ensure_firmware_version(func):
@@ -99,3 +99,24 @@ def get_firmware_type(f_type: int) -> EFirmwareType:
         raise ValueError(f'Unknown firmware type: 0x{f_type:x}')
 
     return FirmwareTypeValueAssignment[f_type]
+
+
+class HardwareVersion(IntEnum):
+    """Represents the supported Shimmer device hardware versions
+    
+    """
+    SHIMMER1 = 0
+    SHIMMER2 = 1
+    SHIMMER2R = 2
+    SHIMMER3 = 3
+    SHIMMER3R = 10
+    UNKNOWN = -1
+    
+    @classmethod
+    def from_int(cls, value: int) -> 'HardwareVersion':
+        """Converts an Integer to the corresponding HardwareVersion enum
+        
+        :param value: Integer representing device hardware version
+        :return: Corresponding HardwareVersion enum member, or UNKNOWN if unrecognised 
+        """
+        return cls._value2member_map_.get(value, cls.UNKNOWN)
\ No newline at end of file


Test patch:
diff --git a/test/bluetooth/test_bluetooth_api.py b/test/bluetooth/test_bluetooth_api.py
index dde8aa3..790b0a6 100644
--- a/test/bluetooth/test_bluetooth_api.py
+++ b/test/bluetooth/test_bluetooth_api.py
@@ -22,7 +22,7 @@ from pyshimmer.bluetooth.bt_commands import GetDeviceNameCommand, SetDeviceNameC
     GetStringCommand, ResponseCommand
 from pyshimmer.bluetooth.bt_serial import BluetoothSerial
 from pyshimmer.dev.channels import ChDataTypeAssignment, EChannelType
-from pyshimmer.dev.fw_version import FirmwareVersion, EFirmwareType
+from pyshimmer.dev.fw_version import FirmwareVersion, EFirmwareType, HardwareVersion
 from pyshimmer.test_util import PTYSerialMockCreator
 
 
@@ -333,12 +333,15 @@ class ShimmerBluetoothIntegrationTest(TestCase):
         if initialize:
             # The Bluetooth API automatically requests the firmware version upon initialization.
             # We must prepare a proper response beforehand.
-            future = self._submit_req_resp_handler(req_len=1, resp=b'\xff\x2f\x03\x00\x00\x00\x0b\x00')
+            req_future_fw = self._submit_req_resp_handler(req_len=1, resp=b'\xff\x2f\x03\x00\x00\x00\x0b\x00')
+            req_future_hw = self._submit_req_resp_handler(req_len=1, resp=b'\xff\x25\x03')
             self._sot.initialize()
 
             # Check that it properly asked for the firmware version
-            result = future.result()
+            result = req_future_fw.result()
             assert result == b'\x2E'
+            result = req_future_hw.result()
+            assert result == b'\x3F'
 
     def tearDown(self) -> None:
         self._sot.shutdown()
@@ -349,11 +352,14 @@ class ShimmerBluetoothIntegrationTest(TestCase):
 
         # The Bluetooth API automatically requests the firmware version upon initialization.
         # We must prepare a proper response beforehand.
-        req_future = self._submit_req_resp_handler(req_len=1, resp=b'\xff\x2f\x03\x00\x00\x00\x0b\x00')
+        req_future_fw = self._submit_req_resp_handler(req_len=1, resp=b'\xff\x2f\x03\x00\x00\x00\x0b\x00')
+        req_future_hw = self._submit_req_resp_handler(req_len=1, resp=b'\xff\x25\x03')
         with self._sot:
             # We check that the API properly asked for the firmware version
-            req_data = req_future.result()
-            self.assertEqual(req_data, b'\x2e')
+            req_data_fw = req_future_fw.result()
+            self.assertEqual(req_data_fw, b'\x2e')
+            req_data_hw = req_future_hw.result()
+            self.assertEqual(req_data_hw, b'\x3f')
 
             # It should now be in an initialized state
             self.assertTrue(self._sot.initialized)
@@ -440,11 +446,27 @@ class ShimmerBluetoothIntegrationTest(TestCase):
         self.assertEqual(fwtype, EFirmwareType.LogAndStream)
         self.assertEqual(fwver, FirmwareVersion(1, 2, 3))
 
+    def test_get_hardware_version(self):
+        self.do_setup()
+    
+        self._submit_req_resp_handler(1, b'\xFF\x25\x03')
+        hw_version = self._sot.get_device_hardware_version()
+        self.assertEqual(hw_version, HardwareVersion.SHIMMER3)
+ 
+        self._submit_req_resp_handler(1, b'\xFF\x25\x0A')
+        hw_version = self._sot.get_device_hardware_version()
+        self.assertEqual(hw_version, HardwareVersion.SHIMMER3R)    
+ 
+        self._submit_req_resp_handler(1, b'\xFF\x25\x04')
+        hw_version = self._sot.get_device_hardware_version()
+        self.assertEqual(hw_version, HardwareVersion.UNKNOWN)
+
     def test_status_ack_disable(self):
         self.do_setup(initialize=False)
 
         # Queue response for version command
         self._submit_req_resp_handler(1, b'\xFF\x2F\x03\x00\x00\x00\x0F\x04')
+        self._submit_req_resp_handler(1, b'\xFF\x25\x03')
         # Queue response for disabling the status acknowledgment
         req_future = self._submit_req_resp_handler(2, b'\xFF')
 
@@ -457,4 +479,5 @@ class ShimmerBluetoothIntegrationTest(TestCase):
 
         # Queue response for version command
         self._submit_req_resp_handler(1, b'\xFF\x2F\x03\x00\x00\x00\x0F\x04')
+        self._submit_req_resp_handler(1, b'\xFF\x25\x03')
         self._sot.initialize()
diff --git a/test/bluetooth/test_bt_commands.py b/test/bluetooth/test_bt_commands.py
index 1b76393..0e5f5e6 100644
--- a/test/bluetooth/test_bt_commands.py
+++ b/test/bluetooth/test_bt_commands.py
@@ -16,7 +16,7 @@
 from typing import Tuple, Union
 from unittest import TestCase
 
-from pyshimmer.bluetooth.bt_commands import ShimmerCommand, GetSamplingRateCommand, GetBatteryCommand, \
+from pyshimmer.bluetooth.bt_commands import GetShimmerHardwareVersion, ShimmerCommand, GetSamplingRateCommand, GetBatteryCommand, \
     GetConfigTimeCommand, SetConfigTimeCommand, GetRealTimeClockCommand, SetRealTimeClockCommand, GetStatusCommand, \
     GetFirmwareVersionCommand, InquiryCommand, StartStreamingCommand, StopStreamingCommand, StartLoggingCommand, \
     StopLoggingCommand, GetEXGRegsCommand, SetEXGRegsCommand, GetExperimentIDCommand, SetExperimentIDCommand, \
@@ -24,7 +24,7 @@ from pyshimmer.bluetooth.bt_commands import ShimmerCommand, GetSamplingRateComma
     SetSensorsCommand, SetSamplingRateCommand, GetAllCalibrationCommand
 from pyshimmer.bluetooth.bt_serial import BluetoothSerial
 from pyshimmer.dev.channels import ChDataTypeAssignment, EChannelType, ESensorGroup
-from pyshimmer.dev.fw_version import EFirmwareType
+from pyshimmer.dev.fw_version import EFirmwareType, HardwareVersion
 from pyshimmer.test_util import MockSerial
 
 
@@ -203,6 +203,15 @@ class BluetoothCommandsTest(TestCase):
         cmd = GetDeviceNameCommand()
         self.assert_cmd(cmd, b'\x7b', b'\x7a', b'\x7a\x05S_PPG', 'S_PPG')
 
+    def test_get_hardware_version(self):
+        cmd = GetShimmerHardwareVersion()
+        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)  
+        self.assert_cmd(cmd, b'\x3f', b'\x25', b'\x25\x03', HardwareVersion.SHIMMER3)
+        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')
         self.assert_cmd(cmd, b'\x79\x05S_PPG')