├── .coveragerc ├── .git-blame-ignore-revs ├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── conf └── udev │ └── 10-shimmer.rules.example ├── examples ├── bt_api_example.py └── dock_example.py ├── pyproject.toml ├── pyshimmer ├── __init__.py ├── bluetooth │ ├── __init__.py │ ├── bt_api.py │ ├── bt_commands.py │ ├── bt_const.py │ └── bt_serial.py ├── dev │ ├── __init__.py │ ├── base.py │ ├── calibration.py │ ├── channels.py │ ├── exg.py │ └── fw_version.py ├── reader │ ├── __init__.py │ ├── binary_reader.py │ ├── reader_const.py │ └── shimmer_reader.py ├── serial_base.py ├── test_util.py ├── uart │ ├── __init__.py │ ├── dock_api.py │ ├── dock_const.py │ └── dock_serial.py └── util.py └── test ├── __init__.py ├── bluetooth ├── __init__.py ├── test_bluetooth_api.py ├── test_bt_commands.py └── test_bt_serial.py ├── dev ├── __init__.py ├── test_device_base.py ├── test_device_calibration.py ├── test_device_channels.py ├── test_device_exg.py └── test_device_fw_version.py ├── reader ├── __init__.py ├── reader_test_util.py ├── resources │ ├── ecg.bin │ ├── ecg_calibrated.csv.gz │ ├── ecg_uncalibrated.csv.gz │ ├── pair_consensys.csv │ ├── pair_raw.bin │ ├── sdlog_sync_slave.bin │ ├── sdlog_sync_slave.csv.gz │ ├── single_sample.bin │ ├── triaxcal_calibrated.csv.gz │ ├── triaxcal_sample.bin │ └── triaxcal_uncalibrated.csv.gz ├── test_binary_reader.py ├── test_shimmer_reader.py └── test_util.py ├── test_serial_base.py ├── test_util.py └── uart ├── __init__.py ├── test_dock_api.py └── test_dock_serial.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | pyshimmer 4 | omit = 5 | test/* 6 | setup.py 7 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Format code base with black 2 | fb8acf7dd53f79156b72a59b2d8df225d7401bb7 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Python Test | Build | Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | release: 9 | types: [published] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | test: 16 | name: Run Tests 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install .[test] 33 | - name: Test with pytest 34 | run: | 35 | pytest 36 | 37 | build: 38 | name: Build Python Package 39 | runs-on: ubuntu-latest 40 | needs: test 41 | steps: 42 | - uses: actions/checkout@v4 43 | with: 44 | persist-credentials: false 45 | - uses: actions/setup-python@v5 46 | with: 47 | python-version: '3.12' 48 | - name: Install build dependencies 49 | run: | 50 | pip install build twine 51 | - name: Build Package 52 | run: | 53 | python -m build 54 | - name: Check Package 55 | run: | 56 | twine check --strict dist/* 57 | - name: Save Artifacts 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: release-artifacts 61 | path: dist/ 62 | if-no-files-found: error 63 | 64 | publish: 65 | name: Publish to PyPi 66 | runs-on: ubuntu-latest 67 | 68 | needs: build 69 | environment: publish 70 | permissions: 71 | id-token: write 72 | 73 | if: github.event.release && github.event.action == 'published' 74 | steps: 75 | - name: Dowload Artifacts 76 | uses: actions/download-artifact@v4 77 | with: 78 | path: dist/ 79 | merge-multiple: true 80 | - name: Publish to PyPi 81 | uses: pypa/gh-action-pypi-publish@release/v1 82 | 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ 3 | build 4 | dist 5 | .eggs 6 | pyshimmer.egg-info 7 | .coverage 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This changelog tries to follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 4 | The project uses semantic versioning. 5 | 6 | ## Next Release 7 | 8 | ### Changed 9 | - Format code base with black 10 | - Wrap long lines to 90 characters 11 | - Replace types from typing with built-in ones 12 | - Raise required Python version to 3.9 since PEP 604 is used in the code 13 | 14 | ## 0.7.0 - 2025-01-18 15 | 16 | First release with Changelog 17 | 18 | ### Added 19 | - This Changelog :) 20 | 21 | ### Changed 22 | - The CI workflow now builds and deploys the artifacts 23 | 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Since we use setuptools_scm, all files tracked by git are included in the 2 | # sdist archive. In this file, we specify exceptions to this list. 3 | prune .* 4 | exclude .* 5 | exclude MANIFEST.in 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyshimmer: Unofficial Python API for Shimmer Sensor devices 2 | =========================================================== 3 | 4 | .. image:: https://github.com/seemoo-lab/pyshimmer/actions/workflows/build.yml/badge.svg 5 | :target: https://github.com/seemoo-lab/pyshimmer 6 | 7 | .. contents:: 8 | 9 | General Information 10 | ------------------- 11 | 12 | pyshimmer provides a Python API to work with the wearable sensor devices produced by Shimmer_. The API is divided into 13 | three major components: 14 | 15 | * The Bluetooth API: An interface to communicate with the Shimmer LogAndStream firmware via Bluetooth 16 | * The Dock API: An interface to communicate with the Shimmer while they are placed in a dock 17 | * The Reader API: An interface to read the binary files produced by the Shimmer devices 18 | 19 | .. _Shimmer: http://www.shimmersensing.com/ 20 | 21 | Please note that the following README does not provide a general introduction to the Shimmer devices. For this, please 22 | consult the corresponding `documentation page `_ 23 | of the vendor and take a closer look at: 24 | 25 | * The Shimmer User Manual 26 | * The LogAndStream Manual 27 | * The SDLog Manual 28 | 29 | Contributing 30 | ------------ 31 | All code in this repository was produced as part of my Master thesis. This means that the API is not 32 | complete. Especially the Bluetooth and Dock API do not feature all calls supported by the devices. However, the code 33 | provides a solid foundation to extend it where necessary. Please feel free to make contributions in case the code is 34 | missing required calls. 35 | 36 | Please submit pull requests against the develop branch. When a new version is released, the current state of the 37 | develop branch is merged into master to produce the new version. 38 | 39 | Installation 40 | ------------ 41 | 42 | The targeted plattform for this library is **Linux**. It has not been tested on other operating systems. In order to 43 | use all aspects of the library, you need to install the package itself, set up the Bluetooth interface, and possibly 44 | configure udev rules to ensure that the device names are consistent. 45 | 46 | pyshimmer Package 47 | ^^^^^^^^^^^^^^^^^ 48 | The easiest and quickest way is to install the latest release with pip: 49 | 50 | .. code-block:: 51 | 52 | pip install pyshimmer 53 | 54 | From Source with Tests 55 | ^^^^^^^^^^^^^^^^^^^^^^ 56 | 57 | You can also install from source in case you would like to run the tests: 58 | 59 | .. code-block:: 60 | 61 | git clone https://github.com/seemoo-lab/pyshimmer.git 62 | cd pyshimmer 63 | pip install .[test] 64 | 65 | You can then run the tests from the repository root by simply issuing: 66 | 67 | .. code-block:: 68 | 69 | pytest 70 | 71 | Shimmer Firmware 72 | ^^^^^^^^^^^^^^^^ 73 | 74 | As of version v0.15.4 of the `Shimmer3 firmware `_ the Python API is 75 | fully compatible to the firmware. Older versions of the vanilla firmware exhibit several bugs and are incompatible. 76 | If you intend to use a firmware version older than 0.15.4, you will need to compile and run a custom patched version of 77 | the firmware. In the following table, the firmware versions and their compatibility are listed. 78 | 79 | Compatibility Table 80 | """"""""""""""""""" 81 | 82 | ============= ========= ============= ====================================================================== 83 | Firmware Type Version Compatibility Issues 84 | ============= ========= ============= ====================================================================== 85 | LogAndStream v0.15.4 Compatible You will need to use pyshimmer v0.4 or newer 86 | to v0.4.0 87 | LogAndStream v0.11.0 Incompatible - `Issue 7 `_ 88 | - `Issue 10 `_ 89 | SDLog v0.21.0 Compatible Untested 90 | SDLog v0.19.0 Compatible 91 | ============= ========= ============= ====================================================================== 92 | 93 | It is recommended to use the newest *LogAndStream* firmware that is compatible to the API. If you want to use an older 94 | version with the pyshimmer library, you will need to compile and program a patched version of the firmware. We provide 95 | a forked repository which features the necessary fixes `here `_. 96 | It also contains instructions on how to compile and program the firmware. 97 | 98 | Notes on Firmware Version 0.15.4 99 | """""""""""""""""""""""""""""""" 100 | Starting with Firmware version v0.15.4, 101 | `the race condition issue `_ in the Bluetooth stack has been 102 | fixed. Additionally, the Shimmer3 now supports an additional command to disable the unsolicited status acknowledgment 103 | byte (see `Issue 10 `_). The pyshimmer Bluetooth API tries to 104 | automatically detect if the Shimmer3 runs a firmware newer or equal to v0.15.4 and automatically issues the command 105 | to disable the unsolicited status acknowledgment at startup. You can optionally disable this feature in the constructor. 106 | With this new command, the state machine in the Bluetooth API of pyshimmer is compatible to the vanilla firmware 107 | version. 108 | 109 | Creating udev rules for persistent device filenames 110 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 111 | 112 | When plugging a Shimmer dock into the host, Linux will detect two new serial interfaces and a block device representing 113 | the internal SD card of the Shimmer: 114 | 115 | * :code:`/dev/ttyUSB0` for the serial interface to the bootloader, 116 | * :code:`/dev/tty/USB1` for the serial interface to the device itself, 117 | * :code:`/dev/sdX` for the block device. 118 | 119 | When working with multiple docks and devices, keeping track of the names of the serial interfaces can be quite 120 | cumbersome, since udev simply names the devices in the order they are plugged in to the system. You can use udev rules 121 | to assign persistent names to the device files. Note that the rules do not actually match the Shimmer but the dock that 122 | it is located in. **This means that you should always place the Shimmer in the same dock**. 123 | 124 | The following section provides an example of how to handle two Shimmer docks, one of which holds an ECG and the other a 125 | PPG device: 126 | 127 | Distinguishing the Shimmer booloader and device interfaces based on their udev attributes is somewhat difficult because 128 | the distinguishing attributes are spread across multiple devices in the USB device tree. The rules first check the 129 | bInterfaceNumber of the tty device that is being processed. If the device is the bootloader device, its bInterfaceNumber 130 | is equal to 00. If the device is the interface to the Shimmer itself, bInterfaceNumber is equal to 01. 131 | In a second step, the rule set differentiates between the ECG dock and the PPG dock based on the serial number of 132 | the device. The entire udev ruleset is shown in the following code snippet: 133 | 134 | .. code-block:: 135 | 136 | SUBSYSTEMS=="usb" ATTRS{bInterfaceNumber}!="00" GOTO="is_secondary_interface" 137 | SUBSYSTEM=="tty" ATTRS{idVendor}=="" ATTRS{idProduct}=="" ATTRS{serial}=="" SYMLINK+="ttyPPGbl" 138 | SUBSYSTEM=="tty" ATTRS{idVendor}=="" ATTRS{idProduct}=="" ATTRS{serial}=="" SYMLINK+="ttyECGbl" 139 | GOTO="end" 140 | 141 | LABEL="is_secondary_interface" 142 | SUBSYSTEM=="tty" ATTRS{idVendor}=="" ATTRS{idProduct}=="" ATTRS{serial}=="" SYMLINK+="ttyPPGdev" 143 | SUBSYSTEM=="tty" ATTRS{idVendor}=="" ATTRS{idProduct}=="" ATTRS{serial}=="" SYMLINK+="ttyECGdev" 144 | GOTO="end" 145 | 146 | LABEL="end" 147 | 148 | You can also find the example file in :code:`conf/udev/10-shimmer.rules.example`. 149 | 150 | In order to create a custom ruleset for your devices, create a new udev rule file 151 | :code:`/etc/udev/rules.d/10-shimmer.rules` and add the above contents. In the file, you need to replace the 152 | :code:``, :code:``, and :code:`` of the first device, and the :code:``, 153 | :code:``, and :code:`` of the second device. You can find the values by scanning the 154 | :code:`dmesg` command after plugging in a Shimmer device. Here is an example: 155 | 156 | .. code-block:: 157 | 158 | [144366.290357] usb 1-4.3: new full-speed USB device number 34 using xhci_hcd 159 | [144366.386661] usb 1-4.3: New USB device found, idVendor=, idProduct=, bcdDevice= 5.00 160 | [144366.386668] usb 1-4.3: New USB device strings: Mfr=1, Product=2, SerialNumber=3 161 | [144366.386674] usb 1-4.3: Product: SHIMMER DOCK 162 | [144366.386679] usb 1-4.3: Manufacturer: FTDI 163 | [144366.386684] usb 1-4.3: SerialNumber: 164 | 165 | Save the file and reload the rules for them to take effect: 166 | 167 | .. code-block:: 168 | 169 | udevadm control --reload-rules && udevadm trigger 170 | 171 | You should now have two named device files for each Shimmer dock: 172 | 173 | * :code:`/dev/ttyPPGbl` and :code:`/dev/ttyPPGdev` for the PPG Shimmer bootloader and device interfaces, 174 | * :code:`/dev/ttyECGbl` and :code:`/dev/ttyECGdev` for the ECG Shimmer bootloader and device interfaces. 175 | 176 | Configuring the Bluetooth Interface 177 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 178 | The library uses a :code:`tty` serial interface to communicate with the Shimmer over Bluetooth. Before you can use the 179 | library, you need to set up the serial channel appropriately. This has only been tested under Arch Linux, but other 180 | Linux distributions should work as well. 181 | 182 | Requirements: 183 | 184 | * Functioning Bluetooth stack 185 | * The :code:`rfcomm` commandline tool. For Arch Linux, use the `bluez-rfcomm AUR `_ package 186 | * The :code:`hcitool` commandline tool. For Arch Linux, use the `bluez-hcitool AUR `_ package 187 | * A Shimmer device with :code:`LogAndStream` firmware 188 | 189 | Scan for the device to find out its MAC address: 190 | 191 | .. code-block:: 192 | 193 | hcitool scan 194 | 195 | The MAC address of the listed Shimmer device should end with the *BT Radio ID* imprinted on the back of the device. 196 | Next, you can try and ping the device: 197 | 198 | .. code-block:: 199 | 200 | hcitool name 201 | 202 | The command should complete with the name listed previously during the scan. Now you can pair the device as follows: 203 | 204 | .. code-block:: 205 | 206 | rfcomm 207 | 208 | where :code:`` is an arbitrary integer of your choosing. The command will create a new serial interface node 209 | with the following name: :code:`/dev/rfcomm`. 210 | The file acts as a regular serial device and allows you to communicate with the Shimmer. The file is also used by the 211 | library. 212 | 213 | Using the API 214 | ------------- 215 | 216 | Using the Bluetooth interface 217 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 218 | 219 | If you want to connect to the Bluetooth interface, use the :code:`ShimmerBluetooth` class. The API only offers blocking 220 | calls. 221 | 222 | .. code-block:: python 223 | 224 | import time 225 | 226 | from serial import Serial 227 | 228 | from pyshimmer import ShimmerBluetooth, DEFAULT_BAUDRATE, DataPacket, EChannelType 229 | 230 | 231 | def handler(pkt: DataPacket) -> None: 232 | cur_value = pkt[EChannelType.INTERNAL_ADC_13] 233 | print(f'Received new data point: {cur_value}') 234 | 235 | 236 | if __name__ == '__main__': 237 | serial = Serial('/dev/rfcomm42', DEFAULT_BAUDRATE) 238 | shim_dev = ShimmerBluetooth(serial) 239 | 240 | shim_dev.initialize() 241 | 242 | dev_name = shim_dev.get_device_name() 243 | print(f'My name is: {dev_name}') 244 | 245 | shim_dev.add_stream_callback(handler) 246 | 247 | shim_dev.start_streaming() 248 | time.sleep(5.0) 249 | shim_dev.stop_streaming() 250 | 251 | shim_dev.shutdown() 252 | 253 | The example shows how to make simple calls and how to use the Bluetooth streaming capabilities of the device. 254 | 255 | Using the Dock API 256 | ^^^^^^^^^^^^^^^^^^ 257 | 258 | .. code-block:: python 259 | 260 | from serial import Serial 261 | 262 | from pyshimmer import ShimmerDock, DEFAULT_BAUDRATE, fmt_hex 263 | 264 | if __name__ == '__main__': 265 | serial = Serial('/dev/ttyPPGdev', DEFAULT_BAUDRATE) 266 | shim_dock = ShimmerDock(serial) 267 | 268 | mac = shim_dock.get_mac_address() 269 | print(f'Device MAC: {fmt_hex(mac)}') 270 | 271 | shim_dock.close() 272 | 273 | Using the Dock API works very similar to the Bluetooth API. However, it does not require a separate initialization call 274 | because it does not use a background thread to decode incoming messages. 275 | 276 | Using the Reader API 277 | ^^^^^^^^^^^^^^^^^^^^ 278 | 279 | .. code-block:: python 280 | 281 | from pyshimmer import ShimmerReader, EChannelType 282 | 283 | if __name__ == '__main__': 284 | 285 | with open('test/reader/resources/ecg.bin', 'rb') as f: 286 | reader = ShimmerReader(f) 287 | 288 | # Read the file contents into memory 289 | reader.load_file_data() 290 | 291 | print(f'Available data channels: {reader.channels}') 292 | print(f'Sampling rate: {reader.sample_rate} Hz') 293 | print() 294 | 295 | ts = reader[EChannelType.TIMESTAMP] 296 | ecg_ch1 = reader[EChannelType.EXG_ADS1292R_1_CH1_24BIT] 297 | assert len(ts) == len(ecg_ch1) 298 | 299 | print(f'Timestamp: {ts.shape}') 300 | print(f'ECG Channel: {ecg_ch1.shape}') 301 | print() 302 | 303 | exg_reg = reader.exg_reg1 304 | print(f'ECG Chip Sampling Rate: {exg_reg.data_rate} Hz') 305 | print(f'ECG Chip Gain: {exg_reg.ch1_gain}') 306 | 307 | If the data was recorded using the :code:`SDLog` firmware and features synchronization information, the API 308 | automatically interpolates the data to the common timestamp information of the master. 309 | 310 | **Note**: Please be aware that although you have configured a sampling frequency f for your measurements, it can happen that observations are missing. 311 | Usually the observed time difference is a multiple of the sampling period 1 / f. 312 | However, this is not the case for the time difference between the first two observations. 313 | Please take this caveat into consideration when you design your code. 314 | -------------------------------------------------------------------------------- /conf/udev/10-shimmer.rules.example: -------------------------------------------------------------------------------- 1 | # Distinguishing both Shimmer Docks and tty interfaces is somewhat difficult because the distinguishing attributes 2 | # are spread across multiple devices in the USB device tree. The main distinguishing attribute is the serial ID 3 | # of the dock. This allows to distinguish between the ECG and the PPG dock. The second step is to check if the 4 | # bInterfaceNumber of the tty device is 00 --> bootloader or 01 --> device UART. 5 | # 6 | # Unfortunately, it is not possible to check attributes from different parents in a single rule and we need to use 7 | # the Goto action to create an if clause around the bInterfaceNumber. The actual rules then check against the serial 8 | # ID to identifiy if it is the ECG or the PPG dock. Idea taken from an answer by Arnout on Stackexchange: 9 | # https://unix.stackexchange.com/questions/204829/attributes-from-various-parent-devices-in-a-udev-rule 10 | SUBSYSTEMS=="usb" ATTRS{bInterfaceNumber}!="00" GOTO="is_secondary_interface" 11 | SUBSYSTEM=="tty" ATTRS{idVendor}=="" ATTRS{idProduct}=="" ATTRS{serial}=="" SYMLINK+="ttyPPGbl" 12 | SUBSYSTEM=="tty" ATTRS{idVendor}=="" ATTRS{idProduct}=="" ATTRS{serial}=="" SYMLINK+="ttyECGbl" 13 | GOTO="end" 14 | 15 | LABEL="is_secondary_interface" 16 | SUBSYSTEM=="tty" ATTRS{idVendor}=="" ATTRS{idProduct}=="" ATTRS{serial}=="" SYMLINK+="ttyPPGdev" 17 | SUBSYSTEM=="tty" ATTRS{idVendor}=="" ATTRS{idProduct}=="" ATTRS{serial}=="" SYMLINK+="ttyECGdev" 18 | GOTO="end" 19 | 20 | LABEL="end" 21 | -------------------------------------------------------------------------------- /examples/bt_api_example.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import time 4 | 5 | from serial import Serial 6 | 7 | from pyshimmer import ShimmerBluetooth, DEFAULT_BAUDRATE, DataPacket 8 | 9 | 10 | def stream_cb(pkt: DataPacket) -> None: 11 | print(f"Received new data packet: ") 12 | for chan in pkt.channels: 13 | print(f"channel: " + str(chan)) 14 | print(f"value: " + str(pkt[chan])) 15 | print("") 16 | 17 | 18 | def main(args=None): 19 | serial = Serial("/dev/rfcomm42", DEFAULT_BAUDRATE) 20 | shim_dev = ShimmerBluetooth(serial) 21 | 22 | shim_dev.initialize() 23 | 24 | dev_name = shim_dev.get_device_name() 25 | print(f"My name is: {dev_name}") 26 | 27 | info = shim_dev.get_firmware_version() 28 | print("- firmware: [" + str(info[0]) + "]") 29 | print( 30 | "- version: [" 31 | + str(info[1].major) 32 | + "." 33 | + str(info[1].minor) 34 | + "." 35 | + str(info[1].rel) 36 | + "]" 37 | ) 38 | 39 | shim_dev.add_stream_callback(stream_cb) 40 | 41 | shim_dev.start_streaming() 42 | time.sleep(5.0) 43 | shim_dev.stop_streaming() 44 | 45 | shim_dev.shutdown() 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /examples/dock_example.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from serial import Serial 4 | 5 | from pyshimmer import ShimmerDock, DEFAULT_BAUDRATE, fmt_hex 6 | 7 | 8 | def main(args=None): 9 | serial = Serial("/dev/ttyECGdev", DEFAULT_BAUDRATE) 10 | 11 | print(f"Connecting docker") 12 | shim_dock = ShimmerDock(serial) 13 | 14 | mac = shim_dock.get_mac_address() 15 | print(f"Device MAC: {fmt_hex(mac)}") 16 | 17 | shim_dock.close() 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | [build-system] 18 | requires = ["setuptools >= 42", "wheel", "setuptools_scm >= 3.4"] 19 | build-backend = "setuptools.build_meta" 20 | 21 | [project] 22 | name = "pyshimmer" 23 | authors = [ 24 | { name = "Lukas Magel" }, 25 | ] 26 | description = "API for Shimmer sensor devices" 27 | readme = "README.rst" 28 | urls = { "Homepage" = "https://github.com/seemoo-lab/pyshimmer" } 29 | license = { file = "LICENSE" } 30 | classifiers = [ 31 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 32 | "Programming Language :: Python :: 3", 33 | "Development Status :: 4 - Beta", 34 | ] 35 | keywords = [ 36 | "shimmer", 37 | "sensor", 38 | "streaming" 39 | ] 40 | dynamic = ["version"] 41 | requires-python = ">= 3.9" 42 | dependencies = [ 43 | "pyserial>=3.4", 44 | "numpy>=1.15", 45 | "pandas>=1.1.5", 46 | ] 47 | 48 | [project.optional-dependencies] 49 | test = [ 50 | "pytest", 51 | "pytest-cov", 52 | ] 53 | 54 | [tool.setuptools.packages.find] 55 | include = ["pyshimmer*"] 56 | 57 | [tool.setuptools_scm] 58 | -------------------------------------------------------------------------------- /pyshimmer/__init__.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - Python API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from .bluetooth.bt_api import ShimmerBluetooth 17 | from .bluetooth.bt_commands import DataPacket 18 | from .dev.base import DEFAULT_BAUDRATE 19 | from .dev.channels import ChannelDataType, EChannelType 20 | from .dev.exg import ExGMux, ExGRLDLead, ERLDRef, ExGRegister 21 | from .dev.fw_version import EFirmwareType 22 | from .reader.binary_reader import ShimmerBinaryReader 23 | from .reader.shimmer_reader import ShimmerReader 24 | from .uart.dock_api import ShimmerDock 25 | from .util import fmt_hex 26 | -------------------------------------------------------------------------------- /pyshimmer/bluetooth/__init__.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /pyshimmer/bluetooth/bt_const.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from pyshimmer.dev.channels import EChannelType 19 | 20 | ACK_COMMAND_PROCESSED = 0xFF 21 | INSTREAM_CMD_RESPONSE = 0x8A 22 | DATA_PACKET = 0x00 23 | 24 | INQUIRY_COMMAND = 0x01 25 | INQUIRY_RESPONSE = 0x02 26 | 27 | GET_SAMPLING_RATE_COMMAND = 0x03 28 | SAMPLING_RATE_RESPONSE = 0x04 29 | 30 | SET_SAMPLING_RATE_COMMAND = 0x05 31 | 32 | GET_BATTERY_COMMAND = 0x95 33 | BATTERY_RESPONSE = 0x94 34 | FULL_BATTERY_RESPONSE = bytes((INSTREAM_CMD_RESPONSE, BATTERY_RESPONSE)) 35 | 36 | START_STREAMING_COMMAND = 0x07 37 | # No response for command 38 | 39 | SET_SENSORS_COMMAND = 0x08 40 | # No response for command 41 | 42 | STOP_STREAMING_COMMAND = 0x20 43 | # No response for command 44 | 45 | GET_SHIMMER_VERSION_COMMAND = 0x3F 46 | SHIMMER_VERSION_RESPONSE = 0x25 47 | 48 | GET_CONFIGTIME_COMMAND = 0x87 49 | CONFIGTIME_RESPONSE = 0x86 50 | 51 | SET_CONFIGTIME_COMMAND = 0x85 52 | # No response for set command 53 | 54 | GET_RWC_COMMAND = 0x91 55 | RWC_RESPONSE = 0x90 56 | 57 | SET_RWC_COMMAND = 0x8F 58 | # No response for set command 59 | 60 | GET_STATUS_COMMAND = 0x72 61 | STATUS_RESPONSE = 0x71 62 | FULL_STATUS_RESPONSE = bytes((INSTREAM_CMD_RESPONSE, STATUS_RESPONSE)) 63 | 64 | GET_FW_VERSION_COMMAND = 0x2E 65 | FW_VERSION_RESPONSE = 0x2F 66 | 67 | GET_EXG_REGS_COMMAND = 0x63 68 | EXG_REGS_RESPONSE = 0x62 69 | 70 | SET_EXG_REGS_COMMAND = 0x61 71 | # No response for set command 72 | 73 | GET_EXPID_COMMAND = 0x7E 74 | EXPID_RESPONSE = 0x7D 75 | 76 | SET_EXPID_COMMAND = 0x7C 77 | # No response for set command 78 | 79 | GET_SHIMMERNAME_COMMAND = 0x7B 80 | SHIMMERNAME_RESPONSE = 0x7A 81 | 82 | SET_SHIMMERNAME_COMMAND = 0x79 83 | # No response for set command 84 | 85 | DUMMY_COMMAND = 0x96 86 | 87 | START_LOGGING_COMMAND = 0x92 88 | STOP_LOGGING_COMMAND = 0x93 89 | 90 | ENABLE_STATUS_ACK_COMMAND = 0xA3 91 | 92 | GET_ALL_CALIBRATION_COMMAND = 0x2C 93 | ALL_CALIBRATION_RESPONSE = 0x2D 94 | 95 | """ 96 | The Bluetooth LogAndStream API assigns a numerical index to each channel type. This 97 | dictionary maps each index to the corresponding channel type. 98 | """ 99 | BtChannelsByIndex = { 100 | 0x00: EChannelType.ACCEL_LN_X, 101 | 0x01: EChannelType.ACCEL_LN_Y, 102 | 0x02: EChannelType.ACCEL_LN_Z, 103 | 0x03: EChannelType.VBATT, 104 | 0x04: EChannelType.ACCEL_LSM303DLHC_X, 105 | 0x05: EChannelType.ACCEL_LSM303DLHC_Y, 106 | 0x06: EChannelType.ACCEL_LSM303DLHC_Z, 107 | 0x07: EChannelType.MAG_LSM303DLHC_X, 108 | 0x08: EChannelType.MAG_LSM303DLHC_Y, 109 | 0x09: EChannelType.MAG_LSM303DLHC_Z, 110 | 0x0A: EChannelType.GYRO_MPU9150_X, 111 | 0x0B: EChannelType.GYRO_MPU9150_Y, 112 | 0x0C: EChannelType.GYRO_MPU9150_Z, 113 | 0x0D: EChannelType.EXTERNAL_ADC_7, 114 | 0x0E: EChannelType.EXTERNAL_ADC_6, 115 | 0x0F: EChannelType.EXTERNAL_ADC_15, 116 | 0x10: EChannelType.INTERNAL_ADC_1, 117 | 0x11: EChannelType.INTERNAL_ADC_12, 118 | 0x12: EChannelType.INTERNAL_ADC_13, 119 | 0x13: EChannelType.INTERNAL_ADC_14, 120 | 0x14: EChannelType.ACCEL_MPU9150_X, 121 | 0x15: EChannelType.ACCEL_MPU9150_Y, 122 | 0x16: EChannelType.ACCEL_MPU9150_Z, 123 | 0x17: EChannelType.MAG_MPU9150_X, 124 | 0x18: EChannelType.MAG_MPU9150_Y, 125 | 0x19: EChannelType.MAG_MPU9150_Z, 126 | 0x1A: EChannelType.TEMP_BMPX80, 127 | 0x1B: EChannelType.PRESSURE_BMPX80, 128 | 0x1C: EChannelType.GSR_RAW, 129 | 0x1D: EChannelType.EXG_ADS1292R_1_STATUS, 130 | 0x1E: EChannelType.EXG_ADS1292R_1_CH1_24BIT, 131 | 0x1F: EChannelType.EXG_ADS1292R_1_CH2_24BIT, 132 | 0x20: EChannelType.EXG_ADS1292R_2_STATUS, 133 | 0x21: EChannelType.EXG_ADS1292R_2_CH1_24BIT, 134 | 0x22: EChannelType.EXG_ADS1292R_2_CH2_24BIT, 135 | 0x23: EChannelType.EXG_ADS1292R_1_CH1_16BIT, 136 | 0x24: EChannelType.EXG_ADS1292R_1_CH2_16BIT, 137 | 0x25: EChannelType.EXG_ADS1292R_2_CH1_16BIT, 138 | 0x26: EChannelType.EXG_ADS1292R_2_CH2_16BIT, 139 | 0x27: EChannelType.STRAIN_HIGH, 140 | 0x28: EChannelType.STRAIN_LOW, 141 | } 142 | -------------------------------------------------------------------------------- /pyshimmer/bluetooth/bt_serial.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from serial import Serial 19 | 20 | from pyshimmer.bluetooth.bt_const import ACK_COMMAND_PROCESSED 21 | from pyshimmer.serial_base import SerialBase 22 | from pyshimmer.util import fmt_hex, resp_code_to_bytes 23 | 24 | 25 | class BluetoothSerial(SerialBase): 26 | """Another wrapper layer around the Serial interface 27 | 28 | :arg serial: The :class:`serial.Serial` instance to wrap 29 | """ 30 | 31 | def __init__(self, serial: Serial): 32 | super().__init__(serial) 33 | 34 | def read_varlen(self) -> bytes: 35 | """Read the number of bytes specified by the first byte read 36 | 37 | The function reads a single byte that it interprets as a length field with value 38 | L and then reads the specified number of L bytes from the stream. 39 | 40 | :return: A variable number of bytes 41 | """ 42 | arg_len = self.read_byte() 43 | arg = self.read(arg_len) 44 | return arg 45 | 46 | def write_varlen(self, arg: bytes) -> None: 47 | """Write a variable number of bytes by prepending the data with their length 48 | 49 | The function first writes the length of the data as a single byte, then writes 50 | the data itself. 51 | 52 | :param arg: The data to write 53 | :raises ValueError: If the number of bytes exceeds the maximum length of 256 54 | """ 55 | arg_len = len(arg) 56 | if arg_len > 255: 57 | raise ValueError(f"Variable-length argument is too long: {arg_len:d}") 58 | 59 | self.write_byte(arg_len) 60 | self.write(arg) 61 | 62 | def write_command(self, ccode: int, arg_format: str = None, *args) -> None: 63 | """Write a Bluetooth command to the stream 64 | 65 | :param ccode: The code of the command 66 | :param arg_format: The argument format, can be a :func:`struct.pack` string or 67 | `varlen` for a variable-length argument 68 | :param args: The arguments to write along with the command, must meet the 69 | requirements of the format string 70 | """ 71 | self.write_byte(ccode) 72 | 73 | if arg_format is not None: 74 | if arg_format == "varlen": 75 | self.write_varlen(args[0]) 76 | else: 77 | self.write_packed(arg_format, *args) 78 | 79 | def read_ack(self) -> None: 80 | """Read and assert that the next byte in the stream is an acknowledgment 81 | 82 | :raises ValueError: If the byte is not an acknowledgment 83 | 84 | """ 85 | r = self.read_byte() 86 | if r != ACK_COMMAND_PROCESSED: 87 | raise ValueError("Byte received is no acknowledgment") 88 | 89 | def read_response( 90 | self, rcode: int | bytes | tuple[int, ...], arg_format: str = None 91 | ) -> any: 92 | """Read a Bluetooth command response from the stream 93 | 94 | :param rcode: The expected response code. Can be an int for a single-byte 95 | response code, or a tuple of ints or a bytes instance for a multi-byte 96 | response code 97 | :param arg_format: The format string to use when decoding the response 98 | arguments. Can be None, a :func:`struct.unpack` string or `varlen`. If 99 | None, no arguments will be read. 100 | :raises RuntimeError: If the response code is incorrect 101 | :return: The arguments of the response or () if the response has no arguments 102 | """ 103 | rcode = resp_code_to_bytes(rcode) 104 | 105 | actual_rcode = self.read(len(rcode)) 106 | if rcode != actual_rcode: 107 | raise RuntimeError( 108 | f"Received incorrect response code: " 109 | f"{fmt_hex(rcode)} != {fmt_hex(actual_rcode)}" 110 | ) 111 | 112 | if arg_format is not None: 113 | if arg_format == "varlen": 114 | return self.read_varlen() 115 | else: 116 | return self.read_packed(arg_format) 117 | return () 118 | -------------------------------------------------------------------------------- /pyshimmer/dev/__init__.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2023 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /pyshimmer/dev/base.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2023 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from typing import overload 19 | 20 | import numpy as np 21 | 22 | # Device clock rate in ticks per second 23 | DEV_CLOCK_RATE: float = 32768.0 24 | 25 | DEFAULT_BAUDRATE = 115200 26 | 27 | 28 | def sr2dr(sr: float) -> int: 29 | """Calculate equivalent device-specific rate for a sample rate in Hz 30 | 31 | Device-specific sample rates are given in absolute clock ticks per unit of time. 32 | This function can be used to calculate such a rate for the Shimmer3. 33 | 34 | Args: 35 | sr(float): The sampling rate in Hz 36 | 37 | Returns: 38 | An integer which represents the equivalent device-specific sampling rate 39 | """ 40 | dr_dec = DEV_CLOCK_RATE / sr 41 | return round(dr_dec) 42 | 43 | 44 | def dr2sr(dr: int): 45 | """Calculate equivalent sampling rate for a given device-specific rate 46 | 47 | Device-specific sample rates are given in absolute clock ticks per unit of time. 48 | This function can be used to calculate a regular sampling rate in Hz from such a 49 | rate. 50 | 51 | Args: 52 | dr(int): The absolute device rate as int 53 | 54 | Returns: 55 | A floating-point number that represents the sampling rate in Hz 56 | """ 57 | return DEV_CLOCK_RATE / dr 58 | 59 | 60 | @overload 61 | def sec2ticks(t_sec: float) -> int: ... 62 | 63 | 64 | @overload 65 | def sec2ticks(t_sec: np.ndarray) -> np.ndarray: ... 66 | 67 | 68 | def sec2ticks(t_sec: float | np.ndarray) -> int | np.ndarray: 69 | """Calculate equivalent device clock ticks for a time in seconds 70 | 71 | Args: 72 | t_sec: A time in seconds 73 | Returns: 74 | An integer which represents the equivalent number of clock ticks 75 | """ 76 | return round(t_sec * DEV_CLOCK_RATE) 77 | 78 | 79 | @overload 80 | def ticks2sec(t_ticks: int) -> float: ... 81 | 82 | 83 | @overload 84 | def ticks2sec(t_ticks: np.ndarray) -> np.ndarray: ... 85 | 86 | 87 | def ticks2sec(t_ticks: int | np.ndarray) -> float | np.ndarray: 88 | """Calculate the time in seconds equivalent to a device clock ticks count 89 | 90 | Args: 91 | t_ticks: A clock tick counter for which to calculate the time in seconds 92 | Returns: 93 | A floating point time in seconds that is equivalent to the number of clock ticks 94 | """ 95 | return t_ticks / DEV_CLOCK_RATE 96 | -------------------------------------------------------------------------------- /pyshimmer/dev/calibration.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2023 Lukas Magel, Manuel Fernandez-Carmona 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | import struct 19 | 20 | from pyshimmer.util import fmt_hex 21 | 22 | 23 | class AllCalibration: 24 | 25 | def __init__(self, reg_bin: bytes): 26 | self._num_bytes = 84 27 | self._sensor_bytes = 21 28 | self._num_sensors = 4 29 | 30 | if len(reg_bin) < self._num_bytes: 31 | raise ValueError(f"All calibration data must have length {self._num_bytes}") 32 | 33 | self._reg_bin = reg_bin 34 | 35 | def __str__(self) -> str: 36 | def print_sensor(sens_num: int) -> str: 37 | return ( 38 | f"Sensor {sens_num + 1:2d}\n" 39 | + f"\tOffset bias: {self.get_offset_bias(sens_num)}\n" 40 | + f"\tSensitivity: {self.get_sensitivity(sens_num)}\n" 41 | + f"\tAlignment Matrix: {self.get_ali_mat(sens_num)}\n" 42 | ) 43 | 44 | obj_str = f"" 45 | for i in range(0, self._num_sensors): 46 | obj_str += print_sensor(i) 47 | 48 | reg_bin_str = fmt_hex(self._reg_bin) 49 | obj_str += f"Binary: {reg_bin_str}\n" 50 | 51 | return obj_str 52 | 53 | @property 54 | def binary(self): 55 | return self._reg_bin 56 | 57 | def __eq__(self, other: AllCalibration) -> bool: 58 | return self._reg_bin == other._reg_bin 59 | 60 | def _check_sens_num(self, sens_num: int) -> None: 61 | if not 0 <= sens_num < self._num_sensors: 62 | raise ValueError(f"Sensor num must be 0 to {self._num_sensors-1}") 63 | 64 | def get_offset_bias(self, sens_num: int) -> list[int]: 65 | self._check_sens_num(sens_num) 66 | start_offset = sens_num * self._sensor_bytes 67 | end_offset = start_offset + 6 68 | ans = list(struct.unpack(">hhh", self._reg_bin[start_offset:end_offset])) 69 | return ans 70 | 71 | def get_sensitivity(self, sens_num: int) -> list[int]: 72 | self._check_sens_num(sens_num) 73 | start_offset = sens_num * self._sensor_bytes + 6 74 | end_offset = start_offset + 6 75 | ans = list(struct.unpack(">hhh", self._reg_bin[start_offset:end_offset])) 76 | return ans 77 | 78 | def get_ali_mat(self, sens_num: int) -> list[int]: 79 | self._check_sens_num(sens_num) 80 | start_offset = sens_num * self._sensor_bytes + 12 81 | end_offset = start_offset + 9 82 | ans = list(struct.unpack(">bbbbbbbbb", self._reg_bin[start_offset:end_offset])) 83 | return ans 84 | -------------------------------------------------------------------------------- /pyshimmer/dev/exg.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2023 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | import re 19 | from enum import Enum 20 | 21 | from pyshimmer.util import bit_is_set, fmt_hex 22 | from .channels import EChannelType 23 | 24 | 25 | class ExGMux(Enum): 26 | NORMAL = 0x00 27 | SHORTED = 0x01 28 | RLD_MEASURE = 0x02 29 | MVDD = 0x03 30 | TEMP_SENSOR = 0x04 31 | TEST_SIGNAL = 0x05 32 | RLD_DRP = 0x06 33 | RLD_DRM = 0x07 34 | RLD_DRPM = 0x08 35 | INPUT3 = 0x09 36 | RESERVED = 0x0A 37 | 38 | 39 | class ExGRLDLead(Enum): 40 | RLD1P = 0x01 << 0 41 | RLD1N = 0x01 << 1 42 | RLD2P = 0x01 << 2 43 | RLD2N = 0x01 << 3 44 | 45 | 46 | class ERLDRef(Enum): 47 | EXTERNAL = 0x00 48 | INTERNAL = 0x01 49 | 50 | 51 | class ExGRegister: 52 | GAIN_MAP = { 53 | 0: 6, 54 | 1: 1, 55 | 2: 2, 56 | 3: 3, 57 | 4: 4, 58 | 5: 8, 59 | 6: 12, 60 | } 61 | DATA_RATE_MAP = {0: 125, 1: 250, 2: 500, 3: 1000, 4: 2000, 5: 4000, 6: 8000, 7: -1} 62 | PD_BIT = 0x01 << 7 63 | RLD_PD_BIT = 0x01 << 5 64 | 65 | def __init__(self, reg_bin: bytes): 66 | if len(reg_bin) < 10: 67 | raise ValueError("Binary register content must have length 10") 68 | 69 | self._reg_bin = reg_bin 70 | 71 | def __str__(self) -> str: 72 | def print_ch(ch_id: int) -> str: 73 | return ( 74 | f"Channel {ch_id + 1:2d}\n" 75 | + f"\tPowerdown: {self.is_ch_powerdown(ch_id)}\n" 76 | + f"\tGain: {self.get_ch_gain(ch_id):2d}\n" 77 | + f"\tMultiplexer: {self.get_ch_mux(ch_id).name} " 78 | + f"({self.get_ch_mux_bin(ch_id):#06b})\n" 79 | ) 80 | 81 | def fmt_rld_channels(ch_names) -> str: 82 | ch_names = [ch.name for ch in ch_names] if len(ch_names) > 0 else ["None"] 83 | return ", ".join(ch_names) 84 | 85 | reg_bin_str = fmt_hex(self._reg_bin) 86 | obj_str = ( 87 | f"ExGRegister:\n" 88 | + f"Data Rate: {self.data_rate:4d}\n" 89 | + f"RLD Powerdown: {self.rld_powerdown}\n" 90 | + f"RLD Channels: {fmt_rld_channels(self.rld_channels)}\n" 91 | + f"RLD Reference: {self.rld_ref.name}\n" 92 | + f"Binary: {reg_bin_str}\n" 93 | ) 94 | obj_str += print_ch(0) 95 | obj_str += print_ch(1) 96 | return obj_str 97 | 98 | def __eq__(self, other: ExGRegister) -> bool: 99 | return self._reg_bin == other._reg_bin 100 | 101 | @staticmethod 102 | def check_ch_id(ch_id: int) -> None: 103 | if not 0 <= ch_id <= 1: 104 | raise ValueError("Channel ID must be 0 or 1") 105 | 106 | def _get_ch_byte(self, ch_id: int) -> int: 107 | ch_offset = 3 + ch_id 108 | return self._reg_bin[ch_offset] 109 | 110 | def _get_rld_byte(self) -> int: 111 | return self._reg_bin[0x05] 112 | 113 | def get_ch_gain(self, ch_id: int) -> int: 114 | self.check_ch_id(ch_id) 115 | 116 | ch_byte = self._get_ch_byte(ch_id) 117 | gain_bin = (ch_byte & 0x70) >> 4 118 | 119 | return self.GAIN_MAP[gain_bin] 120 | 121 | def get_ch_mux_bin(self, ch_id: int) -> int: 122 | self.check_ch_id(ch_id) 123 | 124 | ch_byte = self._get_ch_byte(ch_id) 125 | return ch_byte & 0x0F 126 | 127 | def get_ch_mux(self, ch_id: int) -> ExGMux: 128 | return ExGMux(self.get_ch_mux_bin(ch_id)) 129 | 130 | def is_ch_powerdown(self, ch_id: int) -> bool: 131 | self.check_ch_id(ch_id) 132 | 133 | ch_byte = self._get_ch_byte(ch_id) 134 | return bit_is_set(ch_byte, self.PD_BIT) 135 | 136 | @property 137 | def binary(self): 138 | return self._reg_bin 139 | 140 | @property 141 | def ch1_gain(self) -> int: 142 | return self.get_ch_gain(0) 143 | 144 | @property 145 | def ch2_gain(self) -> int: 146 | return self.get_ch_gain(1) 147 | 148 | @property 149 | def ch1_mux(self) -> ExGMux: 150 | return self.get_ch_mux(0) 151 | 152 | @property 153 | def ch2_mux(self) -> ExGMux: 154 | return self.get_ch_mux(1) 155 | 156 | @property 157 | def ch1_powerdown(self) -> bool: 158 | return self.is_ch_powerdown(0) 159 | 160 | @property 161 | def ch2_powerdown(self) -> bool: 162 | return self.is_ch_powerdown(1) 163 | 164 | @property 165 | def data_rate(self) -> int: 166 | dr_bin = self._reg_bin[0] & 0x07 167 | return self.DATA_RATE_MAP[dr_bin] 168 | 169 | @property 170 | def rld_powerdown(self) -> bool: 171 | rld_byte = self._get_rld_byte() 172 | return not bit_is_set(rld_byte, self.RLD_PD_BIT) 173 | 174 | @property 175 | def rld_channels(self) -> list[ExGRLDLead]: 176 | rld_byte = self._get_rld_byte() 177 | return [ch for ch in ExGRLDLead if bit_is_set(rld_byte, ch.value)] 178 | 179 | @property 180 | def rld_ref(self) -> ERLDRef: 181 | ref_byte = self._reg_bin[9] 182 | rld_ref = (ref_byte >> 1) & 0x01 183 | return ERLDRef(rld_ref) 184 | 185 | 186 | ExG_ChType_Chip_Assignment: dict[EChannelType, tuple[int, int]] = { 187 | EChannelType.EXG_ADS1292R_1_CH1_24BIT: (0, 0), 188 | EChannelType.EXG_ADS1292R_1_CH1_16BIT: (0, 0), 189 | EChannelType.EXG_ADS1292R_1_CH2_24BIT: (0, 1), 190 | EChannelType.EXG_ADS1292R_1_CH2_16BIT: (0, 1), 191 | EChannelType.EXG_ADS1292R_2_CH1_24BIT: (1, 0), 192 | EChannelType.EXG_ADS1292R_2_CH1_16BIT: (1, 0), 193 | EChannelType.EXG_ADS1292R_2_CH2_24BIT: (1, 1), 194 | EChannelType.EXG_ADS1292R_2_CH2_16BIT: (1, 1), 195 | } 196 | 197 | 198 | def is_exg_ch(ch_type: EChannelType) -> bool: 199 | """ 200 | Returns true if the signal that this channel type describes was recorded by a ExG 201 | chip 202 | 203 | Args: 204 | ch_type: The EChannelType of the signal 205 | 206 | Returns: 207 | True if the channel type belongs to the ExG chips, otherwise False 208 | """ 209 | # This is hacky but protected by unit tests 210 | regex = re.compile(r"EXG_ADS1292R_\d_CH\d_\d{2}BIT") 211 | return regex.match(ch_type.name) is not None 212 | 213 | 214 | def get_exg_ch(ch_type: EChannelType) -> tuple[int, int]: 215 | """ 216 | Each ExG Chip EChannelType originates from a specific ExG chip and channel. This 217 | function returns a tuple that specifices which chip and channel a certain signal 218 | EChannelType was recorded with. 219 | 220 | Args: 221 | ch_type: The EChannelType of the signal 222 | 223 | Returns: 224 | A tuple of ints which represents the chip id {0, 1} and the channel id {0, 1}. 225 | 226 | """ 227 | return ExG_ChType_Chip_Assignment[ch_type] 228 | -------------------------------------------------------------------------------- /pyshimmer/dev/fw_version.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2023 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from enum import Enum, IntEnum, auto 19 | 20 | 21 | def ensure_firmware_version(func): 22 | def wrapper(self, other): 23 | if not isinstance(other, FirmwareVersion): 24 | return False 25 | 26 | return func(self, other) 27 | 28 | return wrapper 29 | 30 | 31 | class EFirmwareType(Enum): 32 | BtStream = auto() 33 | SDLog = auto() 34 | LogAndStream = auto() 35 | 36 | 37 | class FirmwareVersion: 38 | 39 | def __init__(self, major: int, minor: int, rel: int): 40 | """Represents the version of the Shimmer firmware 41 | 42 | :param major: Major version 43 | :param minor: Minor version 44 | :param rel: Patch level 45 | """ 46 | self.major = major 47 | self.minor = minor 48 | self.rel = rel 49 | self._key = (major, minor, rel) 50 | 51 | @ensure_firmware_version 52 | def __eq__(self, other: FirmwareVersion) -> bool: 53 | return self._key == other._key 54 | 55 | @ensure_firmware_version 56 | def __gt__(self, other: FirmwareVersion) -> bool: 57 | return self._key > other._key 58 | 59 | @ensure_firmware_version 60 | def __ge__(self, other: FirmwareVersion) -> bool: 61 | return self._key >= other._key 62 | 63 | @ensure_firmware_version 64 | def __lt__(self, other: FirmwareVersion) -> bool: 65 | return self._key < other._key 66 | 67 | @ensure_firmware_version 68 | def __le__(self, other: FirmwareVersion) -> bool: 69 | return self._key <= other._key 70 | 71 | 72 | class FirmwareCapabilities: 73 | 74 | def __init__(self, fw_type: EFirmwareType, version: FirmwareVersion): 75 | self._fw_type = fw_type 76 | self._version = version 77 | 78 | @property 79 | def fw_type(self) -> EFirmwareType: 80 | return self._fw_type 81 | 82 | @property 83 | def version(self) -> FirmwareVersion: 84 | return self._version 85 | 86 | @property 87 | def supports_ack_disable(self) -> bool: 88 | return ( 89 | self._fw_type == EFirmwareType.LogAndStream 90 | and self._version >= FirmwareVersion(major=0, minor=15, rel=4) 91 | ) 92 | 93 | 94 | FirmwareTypeValueAssignment = { 95 | 0x01: EFirmwareType.BtStream, 96 | 0x02: EFirmwareType.SDLog, 97 | 0x03: EFirmwareType.LogAndStream, 98 | } 99 | 100 | 101 | def get_firmware_type(f_type: int) -> EFirmwareType: 102 | if f_type not in FirmwareTypeValueAssignment: 103 | raise ValueError(f"Unknown firmware type: 0x{f_type:x}") 104 | 105 | return FirmwareTypeValueAssignment[f_type] 106 | 107 | 108 | class HardwareVersion(IntEnum): 109 | """Represents the supported Shimmer device hardware versions""" 110 | 111 | SHIMMER1 = 0 112 | SHIMMER2 = 1 113 | SHIMMER2R = 2 114 | SHIMMER3 = 3 115 | SHIMMER3R = 10 116 | UNKNOWN = -1 117 | 118 | @classmethod 119 | def from_int(cls, value: int) -> HardwareVersion: 120 | """Converts an Integer to the corresponding HardwareVersion enum 121 | 122 | :param value: Integer representing device hardware version 123 | :return: Corresponding HardwareVersion enum member, or UNKNOWN if unrecognised 124 | """ 125 | return cls._value2member_map_.get(value, cls.UNKNOWN) 126 | -------------------------------------------------------------------------------- /pyshimmer/reader/__init__.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /pyshimmer/reader/binary_reader.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | import struct 19 | from typing import BinaryIO 20 | 21 | import numpy as np 22 | 23 | from pyshimmer.dev.channels import ( 24 | ESensorGroup, 25 | get_ch_dtypes, 26 | get_enabled_channels, 27 | EChannelType, 28 | ENABLED_SENSORS_LEN, 29 | deserialize_sensors, 30 | ) 31 | from pyshimmer.dev.exg import ExGRegister 32 | from pyshimmer.util import FileIOBase, unpack, bit_is_set 33 | from .reader_const import ( 34 | RTC_CLOCK_DIFF_OFFSET, 35 | ENABLED_SENSORS_OFFSET, 36 | SR_OFFSET, 37 | START_TS_OFFSET, 38 | START_TS_LEN, 39 | TRIAL_CONFIG_OFFSET, 40 | TRIAL_CONFIG_MASTER, 41 | TRIAL_CONFIG_SYNC, 42 | BLOCK_LEN, 43 | DATA_LOG_OFFSET, 44 | EXG_REG_OFFSET, 45 | EXG_REG_LEN, 46 | TRIAXCAL_FILE_OFFSET, 47 | TRIAXCAL_OFFSET_SCALING, 48 | TRIAXCAL_GAIN_SCALING, 49 | TRIAXCAL_ALIGNMENT_SCALING, 50 | ) 51 | 52 | 53 | class ShimmerBinaryReader(FileIOBase): 54 | 55 | def __init__(self, fp: BinaryIO): 56 | super().__init__(fp) 57 | 58 | self._sensors = [] 59 | self._channels = [] 60 | self._sr = 0 61 | self._rtc_diff = 0 62 | self._start_ts = 0 63 | self._trial_config = 0 64 | 65 | self._read_header() 66 | 67 | @staticmethod 68 | def get_data_channels(sensors): 69 | channels = get_enabled_channels(sensors) 70 | channels_with_ts = [EChannelType.TIMESTAMP] + channels 71 | return channels_with_ts 72 | 73 | def _read_header(self) -> None: 74 | self._sr = self._read_sample_rate() 75 | self._sensors = self._read_enabled_sensors() 76 | self._channels = self.get_data_channels(self._sensors) 77 | self._channel_dtypes = get_ch_dtypes(self._channels) 78 | self._rtc_diff = self._read_rtc_clock_diff() 79 | self._start_ts = self._read_start_time() 80 | self._trial_config = self._read_trial_config() 81 | self._exg_regs = self._read_exg_regs() 82 | 83 | self._samples_per_block, self._block_size = self._calculate_block_size() 84 | 85 | def _read_sample_rate(self) -> int: 86 | self._seek(SR_OFFSET) 87 | return self._read_packed(" list[ESensorGroup]: 90 | self._seek(ENABLED_SENSORS_OFFSET) 91 | sensor_bitfield = self._read(ENABLED_SENSORS_LEN) 92 | enabled_sensors = deserialize_sensors(sensor_bitfield) 93 | 94 | return enabled_sensors 95 | 96 | def _read_rtc_clock_diff(self) -> int: 97 | self._seek(RTC_CLOCK_DIFF_OFFSET) 98 | rtc_diff_ticks = self._read_packed(">Q") 99 | return rtc_diff_ticks 100 | 101 | def _read_start_time(self) -> int: 102 | self._seek(START_TS_OFFSET) 103 | ts_bin = self._read(START_TS_LEN) 104 | 105 | # The timestamp is 5 byte long in little endian byte order, but has its MSB at 106 | # offset 0 instead of 4. Due to this, we need to move the last byte back to the 107 | # end, pad it to 8 bytes and parse it as 64bit value. 108 | ts_bin_flipped = ts_bin[1:] + ts_bin[0:1] 109 | ts_bin_padded = ts_bin_flipped + b"\x00" * 3 110 | 111 | ts_ticks = struct.unpack(" int: 115 | self._seek(TRIAL_CONFIG_OFFSET) 116 | return self._read_packed(" int | None: 128 | # For this read operation we assume that every synchronization offset is 129 | # immediately followed by a timestamp as it is described in the manuals. We 130 | # need to pair every sync offset with a timestamp for interpolation at a later 131 | # point in time. 132 | offset_sign_bool = self._read_packed("B") 133 | offset_sign = 1 - 2 * offset_sign_bool 134 | offset_mag = self._read_packed(" list: 144 | ch_values = [] 145 | 146 | for ch, dtype in zip(self._channels, self._channel_dtypes): 147 | val_bin = self._read(dtype.size) 148 | ch_values.append(dtype.decode(val_bin)) 149 | 150 | return ch_values 151 | 152 | def _read_data_block(self) -> tuple[list[list], int]: 153 | sync_tuple = None 154 | samples = [] 155 | 156 | try: 157 | if self.has_sync: 158 | sync_tuple = self._read_sync_offset() 159 | 160 | for i in range(self._samples_per_block): 161 | sample = self._read_sample() 162 | samples += [sample] 163 | except IOError: 164 | pass 165 | 166 | return samples, sync_tuple 167 | 168 | def _read_contents(self) -> tuple[list, list[tuple[int, int]]]: 169 | sync_offsets = [] 170 | samples = [] 171 | sample_ctr = 0 172 | 173 | self._seek(DATA_LOG_OFFSET) 174 | while True: 175 | block_samples, sync_offset = self._read_data_block() 176 | 177 | if sync_offset is not None: 178 | sync_offsets += [(sample_ctr, sync_offset)] 179 | 180 | samples += block_samples 181 | sample_ctr += len(block_samples) 182 | 183 | if len(block_samples) < self.samples_per_block: 184 | # We have reached EOF 185 | break 186 | 187 | return samples, sync_offsets 188 | 189 | def _read_exg_regs(self) -> tuple[bytes, bytes]: 190 | self._seek(EXG_REG_OFFSET) 191 | 192 | reg1 = self._read(EXG_REG_LEN) 193 | reg2 = self._read(EXG_REG_LEN) 194 | return reg1, reg2 195 | 196 | def _read_triaxcal_params( 197 | self, offset: int 198 | ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: 199 | fmt = ">" + 6 * "h" + 9 * "b" 200 | 201 | self._seek(offset) 202 | calib_param_bytes = self._read(struct.calcsize(fmt)) 203 | params_raw = struct.unpack(fmt, calib_param_bytes) 204 | 205 | offset = np.array(params_raw[:3]) 206 | gain = np.diag(params_raw[3:6]) 207 | alignment = np.array(params_raw[6:]).reshape((3, 3)) 208 | 209 | return offset, gain, alignment 210 | 211 | def read_data(self): 212 | samples, sync_offsets = self._read_contents() 213 | 214 | samples_per_ch = list(zip(*samples)) 215 | arr_per_ch = [np.array(s) for s in samples_per_ch] 216 | samples_dict = dict(zip(self._channels, arr_per_ch)) 217 | 218 | if self.has_sync and len(sync_offsets) > 0: 219 | off_index, offset = list(zip(*sync_offsets)) 220 | off_index_arr = np.array(off_index) 221 | offset_arr = np.array(offset) 222 | sync_data = (off_index_arr, offset_arr) 223 | else: 224 | sync_data = ((), ()) 225 | 226 | return samples_dict, sync_data 227 | 228 | def get_exg_reg(self, chip_id: int) -> ExGRegister: 229 | reg_content = self._exg_regs[chip_id] 230 | return ExGRegister(reg_content) 231 | 232 | def get_triaxcal_params( 233 | self, sensor: ESensorGroup 234 | ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: 235 | offset = TRIAXCAL_FILE_OFFSET[sensor] 236 | sc_offset = TRIAXCAL_OFFSET_SCALING[sensor] 237 | sc_gain = TRIAXCAL_GAIN_SCALING[sensor] 238 | sc_alignment = TRIAXCAL_ALIGNMENT_SCALING[sensor] 239 | 240 | offset, gain, alignment = self._read_triaxcal_params(offset) 241 | return offset * sc_offset, gain * sc_gain, alignment * sc_alignment 242 | 243 | @property 244 | def sample_rate(self) -> int: 245 | return self._sr 246 | 247 | @property 248 | def block_size(self) -> int: 249 | return self._block_size 250 | 251 | @property 252 | def samples_per_block(self) -> int: 253 | return self._samples_per_block 254 | 255 | @property 256 | def enabled_sensors(self) -> list[ESensorGroup]: 257 | return self._sensors 258 | 259 | @property 260 | def enabled_channels(self) -> list[EChannelType]: 261 | return self._channels 262 | 263 | @property 264 | def has_global_clock(self) -> bool: 265 | return self._rtc_diff != 0x0 266 | 267 | @property 268 | def global_clock_diff(self) -> int: 269 | return self._rtc_diff 270 | 271 | @property 272 | def start_timestamp(self) -> int: 273 | return self._start_ts 274 | 275 | @property 276 | def has_sync(self) -> bool: 277 | return bit_is_set(self._trial_config, TRIAL_CONFIG_SYNC) 278 | 279 | @property 280 | def is_sync_master(self) -> bool: 281 | return bit_is_set(self._trial_config, TRIAL_CONFIG_MASTER) 282 | 283 | @property 284 | def exg_reg1(self) -> ExGRegister: 285 | return self.get_exg_reg(0) 286 | 287 | @property 288 | def exg_reg2(self) -> ExGRegister: 289 | return self.get_exg_reg(1) 290 | -------------------------------------------------------------------------------- /pyshimmer/reader/reader_const.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from pyshimmer.dev.channels import ESensorGroup 19 | 20 | SR_OFFSET = 0x00 21 | 22 | ENABLED_SENSORS_OFFSET = 0x03 23 | 24 | RTC_CLOCK_DIFF_OFFSET = 0x2C 25 | 26 | START_TS_OFFSET = 0xFB 27 | START_TS_LEN = 0x5 28 | 29 | TRIAL_CONFIG_OFFSET = 0x10 30 | 31 | DATA_LOG_OFFSET = 0x100 32 | BLOCK_LEN = 0x200 33 | 34 | TRIAL_CONFIG_SYNC = 0x04 << 8 * 0 35 | TRIAL_CONFIG_MASTER = 0x02 << 8 * 0 36 | 37 | EXG_REG_OFFSET = 0x38 38 | EXG_REG_LEN = 0x0A 39 | 40 | # The file offsets at which the calibration parameters of the respective sensor can be 41 | # found 42 | TRIAXCAL_FILE_OFFSET = { 43 | ESensorGroup.ACCEL_LN: 0x8B, 44 | ESensorGroup.ACCEL_WR: 0x4C, 45 | ESensorGroup.GYRO: 0x61, 46 | ESensorGroup.MAG: 0x76, 47 | } 48 | 49 | # Scaling value by which the calibration offset will be scaled upon deserialization 50 | TRIAXCAL_OFFSET_SCALING = { 51 | ESensorGroup.ACCEL_LN: 1.0, 52 | ESensorGroup.ACCEL_WR: 1.0, 53 | ESensorGroup.GYRO: 1.0, 54 | ESensorGroup.MAG: 1.0, 55 | } 56 | 57 | # Scaling value by which the calibration gain will be scaled upon deserialization 58 | TRIAXCAL_GAIN_SCALING = { 59 | ESensorGroup.ACCEL_LN: 1.0, 60 | ESensorGroup.ACCEL_WR: 1.0, 61 | ESensorGroup.GYRO: 1.0 / 100.0, 62 | ESensorGroup.MAG: 1.0, 63 | } 64 | 65 | # Scaling value by which the calibration alignment matrix will be scaled upon 66 | # deserialization 67 | TRIAXCAL_ALIGNMENT_SCALING = { 68 | ESensorGroup.ACCEL_LN: 1.0 / 100.0, 69 | ESensorGroup.ACCEL_WR: 1.0 / 100.0, 70 | ESensorGroup.GYRO: 1.0 / 100.0, 71 | ESensorGroup.MAG: 1.0 / 100.0, 72 | } 73 | 74 | TRIAXCAL_SENSORS = list(TRIAXCAL_FILE_OFFSET.keys()) 75 | 76 | EXG_ADC_OFFSET = 0.0 77 | EXG_ADC_REF_VOLT = 2.42 # Volts 78 | -------------------------------------------------------------------------------- /pyshimmer/reader/shimmer_reader.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from abc import ABC, abstractmethod 19 | from typing import BinaryIO 20 | 21 | import numpy as np 22 | 23 | from pyshimmer.dev.base import ticks2sec, dr2sr 24 | from pyshimmer.dev.channels import ( 25 | ChDataTypeAssignment, 26 | get_enabled_channels, 27 | EChannelType, 28 | ) 29 | from pyshimmer.dev.exg import is_exg_ch, get_exg_ch, ExGRegister 30 | from pyshimmer.reader.binary_reader import ShimmerBinaryReader 31 | from pyshimmer.reader.reader_const import ( 32 | EXG_ADC_REF_VOLT, 33 | EXG_ADC_OFFSET, 34 | TRIAXCAL_SENSORS, 35 | ) 36 | from pyshimmer.util import unwrap 37 | 38 | 39 | def unwrap_device_timestamps(ts_dev: np.ndarray) -> np.ndarray: 40 | ts_dtype = ChDataTypeAssignment[EChannelType.TIMESTAMP] 41 | return unwrap(ts_dev, 2 ** (8 * ts_dtype.size)) 42 | 43 | 44 | def fit_linear_1d(xp, fp, x): 45 | fit_coef = np.polyfit(xp, fp, 1) 46 | fn = np.poly1d(fit_coef) 47 | return fn(x) 48 | 49 | 50 | class ChannelPostProcessor(ABC): 51 | 52 | @abstractmethod 53 | def process( 54 | self, channels: dict[EChannelType, np.ndarray], reader: ShimmerBinaryReader 55 | ) -> dict[EChannelType, np.ndarray]: 56 | pass 57 | 58 | 59 | class SingleChannelProcessor(ChannelPostProcessor, ABC): 60 | 61 | def __init__(self, ch_types: list[EChannelType] = None): 62 | super().__init__() 63 | self._ch_types = ch_types 64 | 65 | def process( 66 | self, channels: dict[EChannelType, np.ndarray], reader: ShimmerBinaryReader 67 | ) -> dict[EChannelType, np.ndarray]: 68 | 69 | if self._ch_types is None: 70 | ch_types = list(channels.keys()) 71 | else: 72 | ch_types = [t for t in self._ch_types if t in channels] 73 | 74 | result = channels.copy() 75 | for ch_type in ch_types: 76 | result[ch_type] = self.process_channel(ch_type, channels[ch_type], reader) 77 | 78 | return result 79 | 80 | @abstractmethod 81 | def process_channel( 82 | self, ch_type: EChannelType, y: np.ndarray, reader: ShimmerBinaryReader 83 | ) -> np.ndarray: 84 | pass 85 | 86 | 87 | class ExGProcessor(SingleChannelProcessor): 88 | 89 | def __init__(self): 90 | exg_channels = [t for t in EChannelType if is_exg_ch(t)] 91 | super().__init__(exg_channels) 92 | 93 | def process_channel( 94 | self, ch_type: EChannelType, y: np.ndarray, reader: ShimmerBinaryReader 95 | ) -> np.ndarray: 96 | chip_id, ch_id = get_exg_ch(ch_type) 97 | exg_reg = reader.get_exg_reg(chip_id) 98 | gain = exg_reg.get_ch_gain(ch_id) 99 | 100 | ch_dtype = ChDataTypeAssignment[ch_type] 101 | resolution = 8 * ch_dtype.size 102 | sensitivity = EXG_ADC_REF_VOLT / (2 ** (resolution - 1) - 1) 103 | 104 | # According to formula in Shimmer ECG User Guide 105 | y_volt = (y - EXG_ADC_OFFSET) * sensitivity / gain 106 | return y_volt 107 | 108 | 109 | class PPGProcessor(SingleChannelProcessor): 110 | 111 | def __init__(self): 112 | super().__init__([EChannelType.INTERNAL_ADC_13]) 113 | 114 | def process_channel( 115 | self, ch_type: EChannelType, y: np.ndarray, reader: ShimmerBinaryReader 116 | ) -> np.ndarray: 117 | # Convert from mV to V 118 | return y / 1000.0 119 | 120 | 121 | class TriAxCalProcessor(ChannelPostProcessor): 122 | 123 | def process( 124 | self, channels: dict[EChannelType, np.ndarray], reader: ShimmerBinaryReader 125 | ) -> dict[EChannelType, np.ndarray]: 126 | result = channels.copy() 127 | 128 | active_sensors = [s for s in reader.enabled_sensors if s in TRIAXCAL_SENSORS] 129 | for sensor in active_sensors: 130 | sensor_channels = get_enabled_channels([sensor]) 131 | channel_data = np.stack([channels[c] for c in sensor_channels]) 132 | o, g, a = reader.get_triaxcal_params(sensor) 133 | 134 | g_a = np.matmul(g, a) 135 | r = np.linalg.solve(g_a, channel_data - o[..., None]) 136 | 137 | for i, ch in enumerate(sensor_channels): 138 | result[ch] = r[i] 139 | 140 | return result 141 | 142 | 143 | class ShimmerReader: 144 | 145 | def __init__( 146 | self, 147 | fp: BinaryIO = None, 148 | bin_reader: ShimmerBinaryReader = None, 149 | sync: bool = True, 150 | post_process: bool = True, 151 | processors: list[ChannelPostProcessor] = None, 152 | ): 153 | if fp is not None: 154 | self._bin_reader = ShimmerBinaryReader(fp) 155 | elif bin_reader is not None: 156 | self._bin_reader = bin_reader 157 | else: 158 | raise ValueError( 159 | "Need to provide file object or binary reader as parameter." 160 | ) 161 | 162 | self._ts = None 163 | self._ch_samples = {} 164 | self._sync = sync 165 | 166 | self._pp = post_process 167 | if processors is not None: 168 | self._processors = processors 169 | else: 170 | self._processors = [ 171 | ExGProcessor(), 172 | PPGProcessor(), 173 | TriAxCalProcessor(), 174 | ] 175 | 176 | @staticmethod 177 | def _apply_synchronization( 178 | data_ts: np.ndarray, offset_index: np.ndarray, offsets: np.ndarray 179 | ): 180 | # We discard all synchronization offsets for which we do not possess timestamps. 181 | index_safe = offset_index[offset_index < len(data_ts)] 182 | 183 | offsets_ts = data_ts[index_safe] 184 | data_offsets = fit_linear_1d(offsets_ts, offsets, data_ts) 185 | 186 | aligned_ts = data_ts - data_offsets 187 | return aligned_ts 188 | 189 | def _apply_clock_offsets(self, ts: np.ndarray): 190 | # First, we need calculate absolute timestamps relative to the boot-up time of 191 | # the Shimmer. In order to do so, we use the 40bit initial timestamp to 192 | # calculate an offset to apply to each timestamp. 193 | boot_offset = self._bin_reader.start_timestamp - ts[0] 194 | ts_boot = ts + boot_offset 195 | 196 | if self._bin_reader.has_global_clock: 197 | return ts_boot + self._bin_reader.global_clock_diff 198 | else: 199 | return ts_boot 200 | 201 | def _process_signals( 202 | self, channels: dict[EChannelType, np.ndarray] 203 | ) -> dict[EChannelType, np.ndarray]: 204 | result = channels.copy() 205 | 206 | for processor in self._processors: 207 | result = processor.process(result, self._bin_reader) 208 | 209 | return result 210 | 211 | def load_file_data(self): 212 | samples, sync_offsets = self._bin_reader.read_data() 213 | ts_raw = samples.pop(EChannelType.TIMESTAMP) 214 | 215 | ts_unwrapped = unwrap_device_timestamps(ts_raw) 216 | ts_sane = self._apply_clock_offsets(ts_unwrapped) 217 | 218 | if self._sync and self._bin_reader.has_sync: 219 | ts_sane = self._apply_synchronization(ts_sane, *sync_offsets) 220 | 221 | if self._pp: 222 | self._ch_samples = self._process_signals(samples) 223 | else: 224 | self._ch_samples = samples 225 | 226 | self._ts = ticks2sec(ts_sane) 227 | 228 | def get_exg_reg(self, chip_id: int) -> ExGRegister: 229 | return self._bin_reader.get_exg_reg(chip_id) 230 | 231 | def __getitem__(self, item: EChannelType) -> np.ndarray: 232 | if item == EChannelType.TIMESTAMP: 233 | return self.timestamp 234 | 235 | return self._ch_samples[item] 236 | 237 | @property 238 | def timestamp(self) -> np.ndarray: 239 | return self._ts 240 | 241 | @property 242 | def channels(self) -> list[EChannelType]: 243 | # We return all but the first channel which are the timestamps 244 | return self._bin_reader.enabled_channels[1:] 245 | 246 | @property 247 | def sample_rate(self) -> float: 248 | return dr2sr(self._bin_reader.sample_rate) 249 | 250 | @property 251 | def exg_reg1(self) -> ExGRegister: 252 | return self.get_exg_reg(0) 253 | 254 | @property 255 | def exg_reg2(self) -> ExGRegister: 256 | return self.get_exg_reg(1) 257 | -------------------------------------------------------------------------------- /pyshimmer/serial_base.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | import struct 19 | from collections.abc import Callable 20 | from io import RawIOBase 21 | 22 | from serial import Serial 23 | 24 | from pyshimmer.util import unpack 25 | 26 | 27 | class ReadAbort(Exception): 28 | """ 29 | Raised by ShimmerSerial when a read operation was cancelled by the user. 30 | """ 31 | 32 | pass 33 | 34 | 35 | class BufferedReader: 36 | """ 37 | Wraps around a RawIOBase object to enable buffered reads and peeks while only 38 | requesting as many bytes as needed for the current request from the underlying 39 | stream. 40 | 41 | :param io_obj: The binary stream from which to read data 42 | """ 43 | 44 | def __init__(self, io_obj: RawIOBase): 45 | self._io_obj = io_obj 46 | self._buf = bytearray() 47 | 48 | def _do_read_or_throw(self, read_len: int) -> bytes: 49 | result = self._io_obj.read(read_len) 50 | if len(result) < read_len: 51 | raise ReadAbort("Read operation returned prematurely. Read was cancelled.") 52 | return result 53 | 54 | def _fill_buffer(self, n: int) -> None: 55 | avail = len(self._buf) 56 | needed = n - avail 57 | 58 | if needed > 0: 59 | self._buf += self._do_read_or_throw(needed) 60 | 61 | def _get_from_buf(self, n: int) -> bytes: 62 | self._fill_buffer(n) 63 | return bytes(self._buf[:n]) 64 | 65 | def _take_from_buf(self, n: int) -> bytes: 66 | data = self._get_from_buf(n) 67 | self._buf = self._buf[n:] 68 | return data 69 | 70 | def read(self, n: int) -> bytes: 71 | """Read n bytes from the underlying stream 72 | 73 | :param n: The number of bytes to read 74 | :raises ReadAbort: If the read operation on the underlying object has been 75 | cancelled and the stream returns less bytes than requested. 76 | :return: The requested bytes 77 | """ 78 | return self._take_from_buf(n) 79 | 80 | def peek(self, n: int) -> bytes: 81 | """Peek n bytes into the stream, but do not discard the data 82 | 83 | Takes n bytes from the stream and returns them but also keeps them in an 84 | internal buffer, such that they can be returned by a later call to :meth:`read`. 85 | 86 | :param n: The number of bytes to peek 87 | :raises ReadAbort: If the read operation on the underlying object has been 88 | cancelled and the stream returns less bytes than requested. 89 | :return: The requested bytes 90 | """ 91 | return self._get_from_buf(n) 92 | 93 | def reset(self) -> None: 94 | """Clear the internal buffer of the reader""" 95 | self._buf = bytearray() 96 | 97 | 98 | class SerialBase: 99 | """Wrapper around :class:`serial.Serial` which provides multiple convenience 100 | methods. 101 | 102 | :param serial: The serial instance to wrap 103 | """ 104 | 105 | def __init__(self, serial: Serial): 106 | if serial.timeout is not None: 107 | print( 108 | "Warning: Serial Read timeout != None. " 109 | "This will interfere with the detection cancelled reads." 110 | ) 111 | 112 | self._serial = serial 113 | self._reader = BufferedReader(serial) 114 | 115 | @staticmethod 116 | def _retrieve_packed(fn_read: Callable[[int], bytes], rformat: str) -> any: 117 | read_len = struct.calcsize(rformat) 118 | 119 | r = fn_read(read_len) 120 | args_unpacked = struct.unpack(rformat, r) 121 | return unpack(args_unpacked) 122 | 123 | def flush_input_buffer(self): 124 | """ 125 | Flush the input buffer and remove any data that has been received but not yet 126 | read 127 | """ 128 | self._serial.reset_input_buffer() 129 | self._reader.reset() 130 | 131 | def write(self, data: bytes) -> int: 132 | """Write the data to the underlying serial stream 133 | 134 | The call blocks until the data has been written. 135 | 136 | :param data: The data to write 137 | :return: The number of bytes written, is equal to the length of data 138 | """ 139 | return self._serial.write(data) 140 | 141 | def write_byte(self, arg: int) -> int: 142 | """Write a single byte to the stream 143 | 144 | :param arg: The byte to write 145 | :return: The number of bytes written 146 | """ 147 | return self.write_packed("B", arg) 148 | 149 | def write_packed(self, wformat: str, *args) -> int: 150 | """Pack a number of arguments using :mod:`struct` and write them to the stream 151 | 152 | :param wformat: The format to use, see :func:`struct.pack` 153 | :param args: The arguments for the format string 154 | :return: The number of bytes written to the stream 155 | """ 156 | args_packed = struct.pack(wformat, *args) 157 | return self.write(args_packed) 158 | 159 | def read(self, read_len: int) -> bytes: 160 | """Read the requested number of bytes from the stream 161 | 162 | The call blocks until sufficient data is available. 163 | 164 | :param read_len: The number of bytes to read 165 | :return: The data as bytes instance 166 | """ 167 | return self._reader.read(read_len) 168 | 169 | def read_packed(self, rformat: str) -> any: 170 | """Read the data requested by the format string and unpack it 171 | 172 | The function evaluates the :mod:`struct` format string, reads the required 173 | number of bytes, and returns the unpacked arguments. 174 | 175 | :param rformat: The string format 176 | :return: A variable number of arguments, depending on the format string 177 | """ 178 | return self._retrieve_packed(self.read, rformat) 179 | 180 | def read_byte(self) -> int: 181 | """Read a single byte from the stream 182 | 183 | :return: The byte that was read 184 | """ 185 | r = self.read_packed("B") 186 | return r 187 | 188 | def cancel_read(self) -> None: 189 | """Cancel ongoing read operation 190 | 191 | If another thread is blocked in a read operation, cancel the operation by 192 | calling this method. 193 | """ 194 | self._serial.cancel_read() 195 | 196 | def peek(self, n: int) -> bytes: 197 | """Peek the requested number of bytes into the stream 198 | 199 | :param n: The number of bytes to return 200 | :return: The requested bytes 201 | """ 202 | return self._reader.peek(n) 203 | 204 | def peek_packed(self, rformat: str) -> any: 205 | """Peek into the stream and return the requested packed arguments 206 | 207 | :param rformat: The format of the data to peek 208 | :return: The peeked data, can be a variable number of arguments, depending on 209 | the format string 210 | """ 211 | return self._retrieve_packed(self.peek, rformat) 212 | 213 | def close(self) -> None: 214 | """Close the underlying serial stream""" 215 | self._serial.close() 216 | -------------------------------------------------------------------------------- /pyshimmer/test_util.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | import os 19 | import pty 20 | from io import BytesIO, RawIOBase, SEEK_END, SEEK_SET 21 | from typing import BinaryIO 22 | 23 | from serial import Serial 24 | 25 | 26 | class MockSerial(RawIOBase): 27 | 28 | def __init__(self, timeout=None): 29 | self._read_buf = BytesIO() 30 | self._write_buf = BytesIO() 31 | 32 | self.timeout = timeout 33 | 34 | self.test_closed = False 35 | self.test_input_flushed = False 36 | self.test_read_cancelled = False 37 | 38 | def readable(self) -> bool: 39 | return True 40 | 41 | def writable(self) -> bool: 42 | return True 43 | 44 | def close(self) -> None: 45 | super().close() 46 | 47 | self.test_closed = True 48 | 49 | def readinto(self, b: bytearray) -> int | None: 50 | return self._read_buf.readinto(b) 51 | 52 | def write(self, b: bytes | bytearray) -> int | None: 53 | return self._write_buf.write(b) 54 | 55 | def reset_input_buffer(self): 56 | self.test_input_flushed = True 57 | 58 | self._read_buf.seek(0, SEEK_END) 59 | 60 | def cancel_read(self): 61 | self.test_read_cancelled = True 62 | 63 | def test_put_read_data(self, data: bytes) -> None: 64 | cur_pos = self._read_buf.tell() 65 | 66 | self._read_buf.seek(0, SEEK_END) 67 | self._read_buf.write(data) 68 | self._read_buf.seek(cur_pos, SEEK_SET) 69 | 70 | def test_get_remaining_read_data(self) -> bytes: 71 | return self._read_buf.read() 72 | 73 | def test_clear_read_buffer(self) -> None: 74 | self._read_buf = BytesIO() 75 | 76 | def test_get_write_data(self) -> bytes: 77 | self._write_buf.seek(0, SEEK_SET) 78 | data = self._write_buf.read() 79 | 80 | self._write_buf = BytesIO() 81 | return data 82 | 83 | 84 | class PTYSerialMockCreator: 85 | 86 | def __init__(self): 87 | self._master_fobj = None 88 | self._slave_fobj = None 89 | 90 | self._slave_serial = None 91 | 92 | @staticmethod 93 | def _create_fobj(fd: int) -> BinaryIO: 94 | # https://bugs.python.org/issue20074 95 | fobj = os.fdopen(fd, "r+b", 0) 96 | assert fobj.fileno() == fd 97 | 98 | return fobj 99 | 100 | def create_mock(self) -> tuple[Serial, BinaryIO]: 101 | master_fd, slave_fd = pty.openpty() 102 | 103 | self._master_fobj = self._create_fobj(master_fd) 104 | self._slave_fobj = self._create_fobj(slave_fd) 105 | 106 | # Serial Baud rate is ignored by the driver and can be set to any value 107 | slave_path = os.ttyname(slave_fd) 108 | self._slave_serial = Serial(slave_path, 115200) 109 | 110 | return self._slave_serial, self._master_fobj 111 | 112 | def close(self): 113 | self._slave_serial.close() 114 | 115 | self._master_fobj.close() 116 | self._slave_fobj.close() 117 | -------------------------------------------------------------------------------- /pyshimmer/uart/__init__.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /pyshimmer/uart/dock_api.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | import struct 19 | 20 | from serial import Serial 21 | 22 | from pyshimmer.dev.base import sec2ticks, ticks2sec 23 | from pyshimmer.dev.exg import ExGRegister 24 | from pyshimmer.dev.fw_version import get_firmware_type, EFirmwareType 25 | from pyshimmer.uart.dock_const import * 26 | from pyshimmer.uart.dock_serial import DockSerial 27 | from pyshimmer.util import unpack 28 | 29 | 30 | class ShimmerDock: 31 | """Main API to communicate with the Shimmer over the Dock UART 32 | 33 | :arg ser: The serial interface to use for communication 34 | 35 | """ 36 | 37 | def __init__(self, ser: Serial, flush_before_req=True): 38 | self._serial = DockSerial(ser) 39 | self._flush_before_req = flush_before_req 40 | 41 | def __enter__(self): 42 | return self 43 | 44 | def __exit__(self, exc_type, exc_value, exc_traceback): 45 | self.close() 46 | 47 | def _read_resp_type_or_throw(self, expected: int) -> int: 48 | r = self._serial.read_byte() 49 | if r != START_CHAR: 50 | raise IOError(f"Unknown start character encountered: {r:x}") 51 | 52 | cmd = self._serial.read_byte() 53 | if cmd == UART_BAD_ARG_RESPONSE: 54 | raise IOError("Command failed: Bad argument") 55 | elif cmd == UART_BAD_CMD_RESPONSE: 56 | raise IOError("Command failed: Unknown command") 57 | elif cmd == UART_BAD_CRC_RESPONSE: 58 | raise IOError("Command failed: CRC Error") 59 | elif cmd != expected: 60 | raise IOError(f"Wrong response type: {expected:x} != {cmd:x}") 61 | 62 | return cmd 63 | 64 | def _write_packet( 65 | self, cmd: int, comp: int, prop: int, data: bytes = bytes() 66 | ) -> None: 67 | if self._flush_before_req: 68 | self._serial.flush_input_buffer() 69 | self._serial.start_write_crc() 70 | 71 | pkt_len = 2 + len(data) 72 | self._serial.write_packed(" None: 80 | data = struct.pack(fmt, *args) 81 | self._write_packet(cmd, comp, prop, data) 82 | 83 | def _read_response(self) -> tuple[int, int, bytes]: 84 | self._serial.start_read_crc_verify() 85 | 86 | self._read_resp_type_or_throw(UART_RESPONSE) 87 | pkt_len, comp, prop = self._serial.read_packed("BBB") 88 | 89 | data_len = pkt_len - 2 90 | data = self._serial.read(data_len) 91 | 92 | self._serial.end_read_crc_verify() 93 | return comp, prop, data 94 | 95 | def _read_response_verify(self, exp_comp: int, exp_prop: int) -> bytes: 96 | comp, prop, data = self._read_response() 97 | 98 | if exp_comp != comp: 99 | raise IOError( 100 | f"Encountered unexpected component type in response: " 101 | f"{exp_comp:x} != {comp:x}" 102 | ) 103 | elif exp_prop != prop: 104 | raise IOError( 105 | f"Encountered unexpected property type in response: " 106 | f"{exp_prop:x} != {prop:x}" 107 | ) 108 | 109 | return data 110 | 111 | def _read_response_wformat_verify( 112 | self, exp_comp: int, exp_prop: int, fmt: str 113 | ) -> any: 114 | data_packed = self._read_response_verify(exp_comp, exp_prop) 115 | data = struct.unpack(fmt, data_packed) 116 | return unpack(data) 117 | 118 | def _read_ack(self) -> None: 119 | self._serial.start_read_crc_verify() 120 | self._read_resp_type_or_throw(UART_ACK_RESPONSE) 121 | self._serial.end_read_crc_verify() 122 | 123 | def close(self) -> None: 124 | """Close the underlying serial interface and release all resources""" 125 | self._serial.close() 126 | 127 | def get_mac_address(self) -> tuple[int, ...]: 128 | """Retrieve the Bluetooth MAC address of the device 129 | 130 | :return: A tuple containing six integer values, each representing a single byte 131 | of the address 132 | """ 133 | self._write_packet(UART_GET, UART_COMP_SHIMMER, UART_PROP_MAC) 134 | 135 | mac = self._read_response_wformat_verify( 136 | UART_COMP_SHIMMER, UART_PROP_MAC, "BBBBBB" 137 | ) 138 | return mac 139 | 140 | def set_rtc(self, ts_sec: float) -> None: 141 | """Set the real-time clock of the device 142 | 143 | Specify the UNIX timestamp in seconds as new value for the real-time clock of 144 | the device 145 | 146 | :param ts_sec: The UNIX timestamp in seconds 147 | """ 148 | ticks = sec2ticks(ts_sec) 149 | self._write_packet_wformat( 150 | UART_SET, UART_COMP_SHIMMER, UART_PROP_RWC_CFG_TIME, " float: 155 | """Retrieve the current value of the real-time clock 156 | 157 | :return: A floating-point value representing the current value of the real-time 158 | clock as UNIX timestamp in seconds 159 | """ 160 | self._write_packet(UART_GET, UART_COMP_SHIMMER, UART_PROP_CURR_LOCAL_TIME) 161 | ticks = self._read_response_wformat_verify( 162 | UART_COMP_SHIMMER, UART_PROP_CURR_LOCAL_TIME, " float: 167 | """Get the value that was last set for the real-time clock 168 | 169 | Example: 170 | 171 | The real-time clock is set to a value of 42s. Subsequent calls to 172 | :meth:`get_rtc` will return v > 42s, 173 | while :meth:`get_config_rtc` will return 42s. 174 | 175 | :return: A floating-point value representing the last configured value for the 176 | real-time clock as UNIX timestamp in seconds 177 | """ 178 | self._write_packet(UART_GET, UART_COMP_SHIMMER, UART_PROP_RWC_CFG_TIME) 179 | ticks = self._read_response_wformat_verify( 180 | UART_COMP_SHIMMER, UART_PROP_RWC_CFG_TIME, " tuple[int, EFirmwareType, int, int, int]: 185 | """Retrieve the firmware version of the device 186 | 187 | :return: A tuple containing the following values: 188 | - The hardware version, should be 3 for Shimmer3 189 | - The firmware type: LogAndStream or SDLog 190 | - The major release version 191 | - The minor release version 192 | - The patch level 193 | """ 194 | self._write_packet(UART_GET, UART_COMP_SHIMMER, UART_PROP_VER) 195 | hw_ver, fw_type_bin, major, minor, rel = self._read_response_wformat_verify( 196 | UART_COMP_SHIMMER, UART_PROP_VER, " EFirmwareType: 202 | """Retrieve the active firmware type 203 | 204 | :return: The firmware type: LogAndStream or SDLog 205 | """ 206 | _, fw_type, _, _, _ = self.get_firmware_version() 207 | return fw_type 208 | 209 | def get_infomem(self, addr: int, dlen: int) -> bytes: 210 | """Access the infomem memory and retrieve the specified range 211 | 212 | :param addr: The start address 213 | :param dlen: The length of the memory block that will be retrieved 214 | :return: The bytes of the memory block 215 | """ 216 | # Due to a bug in the firmware code, we must manually set a variable in the 217 | # firmware to a specific value using a different command before we can read the 218 | # InfoMem. 219 | self._write_packet_wformat( 220 | UART_GET, UART_COMP_DAUGHTER_CARD, UART_PROP_CARD_ID, " ExGRegister: 230 | if not 0 <= chip_id <= 1: 231 | raise ValueError("Parameter chip_id must be 0 or 1") 232 | 233 | offset = UART_INFOMEM_EXG_OFFSET + chip_id * 0x0A 234 | dlen = 0x0A 235 | 236 | reg_data = self.get_infomem(offset, dlen) 237 | return ExGRegister(reg_data) 238 | -------------------------------------------------------------------------------- /pyshimmer/uart/dock_const.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | UART_SET = 0x01 19 | UART_GET = 0x03 20 | 21 | UART_RESPONSE = 0x02 22 | UART_ACK_RESPONSE = 0xFF 23 | 24 | UART_BAD_CMD_RESPONSE = 0xFC 25 | UART_BAD_ARG_RESPONSE = 0xFD 26 | UART_BAD_CRC_RESPONSE = 0xFE 27 | 28 | CRC_INIT = 0xB0CA 29 | START_CHAR = 0x24 30 | 31 | UART_COMP_SHIMMER = 0x01 32 | UART_COMP_BAT = 0x02 33 | UART_COMP_DAUGHTER_CARD = 0x03 34 | UART_COMP_D_ACCEL = 0x04 35 | UART_COMP_GSR = 0x05 36 | 37 | UART_PROP_ENABLE = 0x00 38 | UART_PROP_SAMPLE_RATE = 0x01 39 | UART_PROP_MAC = 0x02 40 | UART_PROP_VER = 0x03 41 | UART_PROP_RWC_CFG_TIME = 0x04 42 | UART_PROP_CURR_LOCAL_TIME = 0x05 43 | UART_PROP_INFOMEM = 0x06 44 | UART_PROP_CARD_ID = 0x02 45 | 46 | UART_INFOMEM_EXG_OFFSET = 0x0A 47 | -------------------------------------------------------------------------------- /pyshimmer/uart/dock_serial.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | import binascii 19 | import struct 20 | 21 | from serial import Serial 22 | 23 | from pyshimmer.serial_base import SerialBase 24 | from pyshimmer.uart.dock_const import CRC_INIT 25 | 26 | 27 | def generate_crc(msg: bytes, crc_init: int) -> bytes: 28 | if len(msg) % 2 != 0: 29 | msg += b"\x00" 30 | 31 | crc = binascii.crc_hqx(msg, crc_init) 32 | crc_bin = struct.pack(" bytes: 49 | return generate_crc(msg, self._crc_init) 50 | 51 | def read(self, read_len: int) -> bytes: 52 | data = super().read(read_len) 53 | 54 | if self._record_read: 55 | self._read_crc_buf += data 56 | 57 | return data 58 | 59 | def write(self, data: bytes) -> int: 60 | if self._record_write: 61 | self._write_crc_buf += data 62 | 63 | return super().write(data) 64 | 65 | def start_read_crc_verify(self) -> None: 66 | self._record_read = True 67 | self._read_crc_buf = bytearray() 68 | 69 | def end_read_crc_verify(self) -> None: 70 | self._record_read = False 71 | 72 | exp_crc = self._create_crc(self._read_crc_buf) 73 | act_crc = super().read(len(exp_crc)) 74 | if not exp_crc == act_crc: 75 | raise IOError("CRC check failed: Received data is invalid") 76 | 77 | def start_write_crc(self) -> None: 78 | self._record_write = True 79 | self._write_crc_buf = bytearray() 80 | 81 | def end_write_crc(self) -> None: 82 | self._record_write = False 83 | 84 | crc = self._create_crc(self._write_crc_buf) 85 | super().write(crc) 86 | -------------------------------------------------------------------------------- /pyshimmer/util.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | import struct 19 | from io import SEEK_SET, SEEK_CUR 20 | from queue import Queue 21 | from typing import BinaryIO 22 | 23 | import numpy as np 24 | 25 | 26 | def bit_is_set(bitfield: int, mask: int) -> bool: 27 | """Check if the bit set in the mask is also set in the bitfield 28 | 29 | :param bitfield: The bitfield stored as an integer of arbitrary length 30 | :param mask: The mask where only a single bit is set 31 | :return: True if the bit in the mask is set in the bitfield, else False 32 | """ 33 | return bitfield & mask == mask 34 | 35 | 36 | def raise_to_next_pow(x: int) -> int: 37 | """Raise the argument to the next power of 2 38 | 39 | Example: 40 | - 1 --> 1 41 | - 2 --> 2 42 | - 3 --> 4 43 | - 5 --> 8 44 | 45 | :param x: The value to raise to the next power 46 | :return: The raised value 47 | """ 48 | if x <= 0: 49 | return 1 50 | 51 | return 1 << (x - 1).bit_length() 52 | 53 | 54 | def flatten_list(lst: list | tuple) -> list: 55 | """Flatten the supplied list by one level 56 | 57 | Assumes that the supplied argument consists of lists itself. All elements are taken 58 | from the sublists and added to a fresh copy. 59 | 60 | :param lst: A list of lists 61 | :return: A list with the contents of the sublists 62 | """ 63 | lst_flat = [val for sublist in lst for val in sublist] 64 | return lst_flat 65 | 66 | 67 | def fmt_hex(val: bytes) -> str: 68 | """Format the supplied array of bytes as str 69 | 70 | :param val: The binary array to format 71 | :return: The resulting string 72 | """ 73 | return " ".join("{:02x}".format(i) for i in val) 74 | 75 | 76 | def unpack(args: list | tuple) -> list | tuple | any: 77 | """Extract the first object if the list has length 1 78 | 79 | If the supplied list or tuple only features a single element, the element is 80 | retrieved and returned. If the list or tuple is longer, the entire list or tuple is 81 | returned. 82 | 83 | :param args: The list or tuple to unpack 84 | :return: The list or tuple itself or the single element if the argument has a 85 | length of 1 86 | """ 87 | if len(args) == 1: 88 | return args[0] 89 | return args 90 | 91 | 92 | def unwrap(x: np.ndarray, shift: int) -> np.ndarray: 93 | """Detect overflows in the data and unwrap them 94 | 95 | The function tries to detect overflows in the input array x, with shape (N, ). It 96 | is assumed that x is monotonically increasing everywhere but at the overflows. An 97 | overflow occurs if for two consecutive points x_i and x_i+1 in the series 98 | x_i > x_i+1. For every such point, the function will add the value of the shift 99 | parameter to all following samples, i.e. x_k' = x_k + shift, for every k > i. 100 | 101 | :param x: The array to unwrap 102 | :param shift: The value which to add to the series after each overflow point 103 | :return: An array of equal length that has been unwrapped 104 | """ 105 | x_diff = np.diff(x) 106 | wrap_points = np.argwhere(x_diff < 0) 107 | 108 | for i in wrap_points.flatten(): 109 | x += (np.arange(len(x)) > i) * shift 110 | 111 | return x 112 | 113 | 114 | def resp_code_to_bytes(code: int | bytes | tuple[int, ...]) -> bytes: 115 | """Convert the supplied response code to bytes 116 | 117 | :param code: The code, can be an int, a tuple of ints, or bytes 118 | :return: The supplied code as byte array 119 | """ 120 | if isinstance(code, int): 121 | code = (code,) 122 | if isinstance(code, tuple): 123 | code = bytes(code) 124 | 125 | return code 126 | 127 | 128 | def calibrate_u12_adc_value(uncalibratedData, offset, vRefP, gain): 129 | """Convert the uncalibrated data to calibrated data 130 | 131 | :param uncalibratedData: Raw voltage measurement from device 132 | :param offset: Voltage offset in measured data 133 | :param vRefP: Voltage reference signal in Volt 134 | :param gain: gain factor 135 | :return: Calibrated voltage in Volt 136 | """ 137 | return (uncalibratedData - offset) * ((vRefP / gain) / 4095) 138 | 139 | 140 | def battery_voltage_to_percent(battery_voltage): 141 | """Convert battery voltage to percent 142 | 143 | :param battery_voltage: Battery voltage in Volt 144 | :return: approximated battery state in percent based on manual 145 | """ 146 | # reference values from: https://shimmersensing.com/wp-content/docs/support/documentation/Shimmer_User_Manual_rev3p.pdf (Page 53) 147 | reference_data_voltages = [ 148 | 3.2, 149 | 3.627, 150 | 3.645, 151 | 3.663, 152 | 3.681, 153 | 3.699, 154 | 3.717, 155 | 3.7314, 156 | 3.735, 157 | 3.7386, 158 | 3.7566, 159 | 3.771, 160 | 3.789, 161 | 3.8034, 162 | 3.8106, 163 | 3.8394, 164 | 3.861, 165 | 3.8826, 166 | 3.9078, 167 | 3.933, 168 | 3.969, 169 | 4.0086, 170 | 4.041, 171 | 4.0734, 172 | 4.113, 173 | 4.167, 174 | ] 175 | reference_data_percentages = [ 176 | 0, 177 | 5.9, 178 | 9.8, 179 | 13.8, 180 | 17.7, 181 | 21.6, 182 | 25.6, 183 | 29.5, 184 | 33.4, 185 | 37.4, 186 | 41.3, 187 | 45.2, 188 | 49.2, 189 | 53.1, 190 | 57, 191 | 61, 192 | 64.9, 193 | 68.9, 194 | 72.8, 195 | 76.7, 196 | 80.7, 197 | 84.6, 198 | 88.5, 199 | 92.5, 200 | 96.4, 201 | 100, 202 | ] 203 | 204 | battery_percent = np.interp( 205 | battery_voltage, reference_data_voltages, reference_data_percentages 206 | ) 207 | 208 | battery_percent = min(battery_percent, 100) 209 | battery_percent = max(battery_percent, 0) 210 | 211 | return battery_percent 212 | 213 | 214 | class PeekQueue(Queue): 215 | """A thread-safe queue implementation that allows peeking at the first element in 216 | the queue. 217 | 218 | Based on a suggestion on StackOverflow: 219 | https://stackoverflow.com/questions/1293966/best-way-to-obtain-indexed-access-to-a-python-queue-thread-safe 220 | """ 221 | 222 | def peek(self): 223 | """Peek at the element that will be removed next. 224 | 225 | :return: The next entry in the queue to be removed or None if the queue is empty 226 | """ 227 | # noinspection PyUnresolvedReferences 228 | with self.mutex: 229 | if self._qsize() > 0: 230 | return self.queue[0] 231 | 232 | return None 233 | 234 | 235 | class FileIOBase: 236 | """Convenience wrapper around a BinaryIO file object 237 | 238 | Serves as an (abstract) base class for IO operations 239 | 240 | :arg fp: The file to wrap 241 | """ 242 | 243 | def __init__(self, fp: BinaryIO): 244 | if not fp.seekable(): 245 | raise ValueError("IO object must be seekable") 246 | 247 | self._fp = fp 248 | 249 | def _tell(self) -> int: 250 | return self._fp.tell() 251 | 252 | def _read(self, s: int) -> bytes: 253 | r = self._fp.read(s) 254 | if len(r) < s: 255 | raise IOError("Read beyond EOF") 256 | 257 | return r 258 | 259 | def _seek(self, off: int = 0) -> None: 260 | self._fp.seek(off, SEEK_SET) 261 | 262 | def _seek_relative(self, off: int = 0) -> None: 263 | self._fp.seek(off, SEEK_CUR) 264 | 265 | def _read_packed(self, fmt: str) -> any: 266 | s = struct.calcsize(fmt) 267 | val_bin = self._read(s) 268 | 269 | args = struct.unpack(fmt, val_bin) 270 | return unpack(args) 271 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /test/bluetooth/__init__.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /test/bluetooth/test_bt_commands.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from unittest import TestCase 19 | 20 | from pyshimmer.bluetooth.bt_commands import ( 21 | GetShimmerHardwareVersion, 22 | ShimmerCommand, 23 | GetSamplingRateCommand, 24 | GetBatteryCommand, 25 | GetConfigTimeCommand, 26 | SetConfigTimeCommand, 27 | GetRealTimeClockCommand, 28 | SetRealTimeClockCommand, 29 | GetStatusCommand, 30 | GetFirmwareVersionCommand, 31 | InquiryCommand, 32 | StartStreamingCommand, 33 | StopStreamingCommand, 34 | StartLoggingCommand, 35 | StopLoggingCommand, 36 | GetEXGRegsCommand, 37 | SetEXGRegsCommand, 38 | GetExperimentIDCommand, 39 | SetExperimentIDCommand, 40 | GetDeviceNameCommand, 41 | SetDeviceNameCommand, 42 | DummyCommand, 43 | DataPacket, 44 | ResponseCommand, 45 | SetStatusAckCommand, 46 | SetSensorsCommand, 47 | SetSamplingRateCommand, 48 | GetAllCalibrationCommand, 49 | ) 50 | from pyshimmer.bluetooth.bt_serial import BluetoothSerial 51 | from pyshimmer.dev.channels import ChDataTypeAssignment, EChannelType, ESensorGroup 52 | from pyshimmer.dev.fw_version import EFirmwareType, HardwareVersion 53 | from pyshimmer.test_util import MockSerial 54 | 55 | 56 | class BluetoothCommandsTest(TestCase): 57 | 58 | @staticmethod 59 | def create_mock() -> tuple[BluetoothSerial, MockSerial]: 60 | mock = MockSerial() 61 | # noinspection PyTypeChecker 62 | serial = BluetoothSerial(mock) 63 | return serial, mock 64 | 65 | def assert_cmd( 66 | self, 67 | cmd: ShimmerCommand, 68 | req_data: bytes, 69 | resp_code: bytes = None, 70 | resp_data: bytes = None, 71 | exp_result: any = None, 72 | ) -> any: 73 | serial, mock = self.create_mock() 74 | 75 | cmd.send(serial) 76 | actual_req_data = mock.test_get_write_data() 77 | self.assertEqual(actual_req_data, req_data) 78 | 79 | if resp_code is None: 80 | self.assertFalse(cmd.has_response()) 81 | return None 82 | 83 | self.assertTrue(cmd.has_response()) 84 | self.assertEqual(cmd.get_response_code(), resp_code) 85 | 86 | mock.test_put_read_data(resp_data) 87 | 88 | act_result = cmd.receive(serial) 89 | if exp_result is not None: 90 | self.assertEqual(act_result, exp_result) 91 | return act_result 92 | 93 | def test_response_command_code_conversion(self): 94 | class TestCommand(ResponseCommand): 95 | def __init__(self, rcode: int | bytes | tuple[int, ...]): 96 | super().__init__(rcode) 97 | 98 | def send(self, ser: BluetoothSerial) -> None: 99 | pass 100 | 101 | cmd = TestCommand(10) 102 | self.assertEqual(cmd.get_response_code(), b"\x0a") 103 | 104 | cmd = TestCommand(20) 105 | self.assertEqual(cmd.get_response_code(), b"\x14") 106 | 107 | cmd = TestCommand((10,)) 108 | self.assertEqual(cmd.get_response_code(), b"\x0a") 109 | 110 | cmd = TestCommand((10, 20)) 111 | self.assertEqual(cmd.get_response_code(), b"\x0a\x14") 112 | 113 | cmd = TestCommand(b"\x10") 114 | self.assertEqual(cmd.get_response_code(), b"\x10") 115 | 116 | cmd = TestCommand(b"\x10\x20") 117 | self.assertEqual(cmd.get_response_code(), b"\x10\x20") 118 | 119 | def test_get_sampling_rate_command(self): 120 | cmd = GetSamplingRateCommand() 121 | self.assert_cmd(cmd, b"\x03", b"\x04", b"\x04\x40\x00", 512.0) 122 | 123 | def test_set_sampling_rate_command(self): 124 | cmd = SetSamplingRateCommand(sr=512.0) 125 | self.assert_cmd(cmd, b"\x05\x40\x00") 126 | 127 | def test_get_battery_state_command(self): 128 | cmd = GetBatteryCommand(in_percent=True) 129 | self.assert_cmd(cmd, b"\x95", b"\x8a\x94", b"\x8a\x94\x30\x0b\x80", 100) 130 | 131 | cmd = GetBatteryCommand(in_percent=False) 132 | self.assert_cmd( 133 | cmd, b"\x95", b"\x8a\x94", b"\x8a\x94\x2e\x0b\x80", 4.168246153846154 134 | ) 135 | 136 | def test_set_sensors_command(self): 137 | sensors = [ 138 | ESensorGroup.GYRO, 139 | ESensorGroup.CH_A13, 140 | ESensorGroup.PRESSURE, 141 | ] 142 | cmd = SetSensorsCommand(sensors) 143 | self.assert_cmd(cmd, b"\x08\x40\x01\x04") 144 | 145 | def test_get_config_time_command(self): 146 | cmd = GetConfigTimeCommand() 147 | self.assert_cmd(cmd, b"\x87", b"\x86", b"\x86\x02\x34\x32", 42) 148 | 149 | def test_set_config_time_command(self): 150 | cmd = SetConfigTimeCommand(43) 151 | self.assert_cmd(cmd, b"\x85\x02\x34\x33") 152 | 153 | def test_get_rtc(self): 154 | cmd = GetRealTimeClockCommand() 155 | r = self.assert_cmd( 156 | cmd, b"\x91", b"\x90", b"\x90\x1f\xb1\x93\x09\x00\x00\x00\x00" 157 | ) 158 | self.assertAlmostEqual(r, 4903.3837585) 159 | 160 | def test_set_rtc(self): 161 | cmd = SetRealTimeClockCommand(10) 162 | self.assert_cmd(cmd, b"\x8f\x00\x00\x05\x00\x00\x00\x00\x00") 163 | 164 | def test_get_status_command(self): 165 | cmd = GetStatusCommand() 166 | expected_result = [True, False, True, False, False, True, False, False] 167 | self.assert_cmd(cmd, b"\x72", b"\x8a\x71", b"\x8a\x71\x25", expected_result) 168 | 169 | def test_get_firmware_version_command(self): 170 | cmd = GetFirmwareVersionCommand() 171 | fw_type, major, minor, patch = self.assert_cmd( 172 | cmd, b"\x2e", b"\x2f", b"\x2f\x03\x00\x00\x00\x0b\x00" 173 | ) 174 | self.assertEqual(fw_type, EFirmwareType.LogAndStream) 175 | self.assertEqual(major, 0) 176 | self.assertEqual(minor, 11) 177 | self.assertEqual(patch, 0) 178 | 179 | def test_inquiry_command(self): 180 | cmd = InquiryCommand() 181 | sr, buf_size, ctypes = self.assert_cmd( 182 | cmd, b"\x01", b"\x02", b"\x02\x40\x00\x01\xff\x01\x09\x01\x01\x12" 183 | ) 184 | 185 | self.assertEqual(sr, 512.0) 186 | self.assertEqual(buf_size, 1) 187 | self.assertEqual(ctypes, [EChannelType.INTERNAL_ADC_13]) 188 | 189 | def test_start_streaming_command(self): 190 | cmd = StartStreamingCommand() 191 | self.assert_cmd(cmd, b"\x07") 192 | 193 | def test_stop_streaming_command(self): 194 | cmd = StopStreamingCommand() 195 | self.assert_cmd(cmd, b"\x20") 196 | 197 | def test_start_logging_command(self): 198 | cmd = StartLoggingCommand() 199 | self.assert_cmd(cmd, b"\x92") 200 | 201 | def test_stop_logging_command(self): 202 | cmd = StopLoggingCommand() 203 | self.assert_cmd(cmd, b"\x93") 204 | 205 | def test_get_exg_register_command(self): 206 | cmd = GetEXGRegsCommand(1) 207 | r = self.assert_cmd( 208 | cmd, 209 | b"\x63\x01\x00\x0a", 210 | b"\x62", 211 | b"\x62\x0a\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01", 212 | ) 213 | self.assertEqual(r.binary, b"\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01") 214 | 215 | def test_get_exg_reg_fail(self): 216 | serial, mock = self.create_mock() 217 | cmd = GetEXGRegsCommand(1) 218 | 219 | mock.test_put_read_data(b"\x62\x04\x01\x02\x03\x04") 220 | self.assertRaises(ValueError, cmd.receive, serial) 221 | 222 | def test_get_allcalibration_command(self): 223 | cmd = GetAllCalibrationCommand() 224 | r = self.assert_cmd( 225 | cmd, 226 | b"\x2c", 227 | b"\x2d", 228 | 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", 229 | ) 230 | self.assertEqual( 231 | r.binary, 232 | 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", 233 | ) 234 | 235 | def test_set_exg_register_command(self): 236 | cmd = SetEXGRegsCommand(1, 0x02, b"\x10\x00") 237 | self.assert_cmd(cmd, b"\x61\x01\x02\x02\x10\x00") 238 | 239 | def test_get_experiment_id_command(self): 240 | cmd = GetExperimentIDCommand() 241 | self.assert_cmd(cmd, b"\x7e", b"\x7d", b"\x7d\x06a_test", "a_test") 242 | 243 | def test_set_experiment_id_command(self): 244 | cmd = SetExperimentIDCommand("A_Test") 245 | self.assert_cmd(cmd, b"\x7c\x06A_Test") 246 | 247 | def test_get_device_name_command(self): 248 | cmd = GetDeviceNameCommand() 249 | self.assert_cmd(cmd, b"\x7b", b"\x7a", b"\x7a\x05S_PPG", "S_PPG") 250 | 251 | def test_get_hardware_version(self): 252 | cmd = GetShimmerHardwareVersion() 253 | self.assert_cmd(cmd, b"\x3f", b"\x25", b"\x25\x00", HardwareVersion.SHIMMER1) 254 | self.assert_cmd(cmd, b"\x3f", b"\x25", b"\x25\x01", HardwareVersion.SHIMMER2) 255 | self.assert_cmd(cmd, b"\x3f", b"\x25", b"\x25\x02", HardwareVersion.SHIMMER2R) 256 | self.assert_cmd(cmd, b"\x3f", b"\x25", b"\x25\x03", HardwareVersion.SHIMMER3) 257 | self.assert_cmd(cmd, b"\x3f", b"\x25", b"\x25\x0a", HardwareVersion.SHIMMER3R) 258 | self.assert_cmd(cmd, b"\x3f", b"\x25", b"\x25\x04", HardwareVersion.UNKNOWN) 259 | 260 | def test_set_device_name_command(self): 261 | cmd = SetDeviceNameCommand("S_PPG") 262 | self.assert_cmd(cmd, b"\x79\x05S_PPG") 263 | 264 | def test_set_status_ack_command(self): 265 | cmd = SetStatusAckCommand(enabled=True) 266 | self.assert_cmd(cmd, b"\xa3\x01") 267 | 268 | cmd = SetStatusAckCommand(enabled=False) 269 | self.assert_cmd(cmd, b"\xa3\x00") 270 | 271 | def test_dummy_command(self): 272 | cmd = DummyCommand() 273 | self.assert_cmd(cmd, b"\x96") 274 | 275 | def test_data_packet(self): 276 | serial, mock = self.create_mock() 277 | 278 | channels = [EChannelType.TIMESTAMP, EChannelType.INTERNAL_ADC_13] 279 | data_types = [ChDataTypeAssignment[c] for c in channels] 280 | ch_and_types = list(zip(channels, data_types)) 281 | 282 | pkt = DataPacket(ch_and_types) 283 | self.assertEqual(pkt.channels, channels) 284 | self.assertEqual(pkt.channel_types, data_types) 285 | 286 | mock.test_put_read_data(b"\x00\xde\xd0\xb2\x26\x07") 287 | pkt.receive(serial) 288 | 289 | self.assertEqual(pkt[EChannelType.TIMESTAMP], 0xB2D0DE) 290 | self.assertEqual(pkt[EChannelType.INTERNAL_ADC_13], 0x0726) 291 | -------------------------------------------------------------------------------- /test/bluetooth/test_bt_serial.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from unittest import TestCase 19 | 20 | from pyshimmer.bluetooth.bt_serial import BluetoothSerial 21 | from pyshimmer.test_util import MockSerial 22 | 23 | 24 | class BluetoothSerialTest(TestCase): 25 | 26 | @staticmethod 27 | def create_sot() -> tuple[MockSerial, BluetoothSerial]: 28 | mock = MockSerial() 29 | # noinspection PyTypeChecker 30 | serial = BluetoothSerial(mock) 31 | return mock, serial 32 | 33 | def test_read_varlen(self): 34 | mock, sot = self.create_sot() 35 | 36 | mock.test_put_read_data(b"\x00\x04\x01\x02\x03\x04") 37 | r = sot.read_varlen() 38 | self.assertEqual(r, b"") 39 | 40 | r = sot.read_varlen() 41 | self.assertEqual(r, b"\x01\x02\x03\x04") 42 | 43 | def test_write_varlen(self): 44 | mock, sot = self.create_sot() 45 | 46 | sot.write_varlen(b"thisisatest") 47 | r = mock.test_get_write_data() 48 | 49 | self.assertEqual(r[0], 11) 50 | self.assertEqual(r[1:], b"thisisatest") 51 | 52 | self.assertRaises(ValueError, sot.write_varlen, b"A" * 300) 53 | 54 | def test_write_command(self): 55 | mock, sot = self.create_sot() 56 | 57 | sot.write_command(0x10, ">BH", 0x42, 0x10) 58 | r = mock.test_get_write_data() 59 | self.assertEqual(r, b"\x10\x42\x00\x10") 60 | 61 | sot.write_command(0x11, "varlen", b"hello") 62 | r = mock.test_get_write_data() 63 | self.assertEqual(r, b"\x11\x05hello") 64 | 65 | def test_read_ack(self): 66 | mock, sot = self.create_sot() 67 | mock.test_put_read_data(b"\xff\x00") 68 | 69 | sot.read_ack() 70 | self.assertRaises(ValueError, sot.read_ack) 71 | 72 | def test_read_response(self): 73 | mock, sot = self.create_sot() 74 | 75 | # int response code 76 | mock.test_put_read_data(b"\x42") 77 | r = sot.read_response(0x42) 78 | self.assertEqual(r, ()) 79 | 80 | mock.test_clear_read_buffer() 81 | mock.test_put_read_data(b"\x42") 82 | self.assertRaises(RuntimeError, sot.read_response, 0x43) 83 | 84 | # Single-byte tuple response code 85 | mock.test_clear_read_buffer() 86 | mock.test_put_read_data(b"\x42") 87 | r = sot.read_response((0x42,)) 88 | self.assertEqual(r, ()) 89 | 90 | mock.test_clear_read_buffer() 91 | mock.test_put_read_data(b"\x42") 92 | self.assertRaises(RuntimeError, sot.read_response, (0x43,)) 93 | 94 | # Multi-byte tuple response code 95 | mock.test_clear_read_buffer() 96 | mock.test_put_read_data(b"\x42\x43") 97 | r = sot.read_response((0x42, 0x43)) 98 | self.assertEqual(r, ()) 99 | 100 | mock.test_clear_read_buffer() 101 | mock.test_put_read_data(b"\x43\x42") 102 | self.assertRaises(RuntimeError, sot.read_response, (0x43, 0x44)) 103 | 104 | # bytes response code 105 | mock.test_clear_read_buffer() 106 | mock.test_put_read_data(b"\x42\x43") 107 | r = sot.read_response(b"\x42\x43") 108 | self.assertEqual(r, ()) 109 | 110 | mock.test_clear_read_buffer() 111 | mock.test_put_read_data(b"\x42\x44") 112 | self.assertRaises(RuntimeError, sot.read_response, b"\x43\x44") 113 | 114 | mock.test_clear_read_buffer() 115 | mock.test_put_read_data(b"\x50\x03\x00\x01\x02") 116 | r = sot.read_response(0x50, arg_format="varlen") 117 | self.assertEqual(r, b"\x00\x01\x02") 118 | 119 | mock.test_clear_read_buffer() 120 | mock.test_put_read_data(b"\x50\x03\x00\x01\x02") 121 | r = sot.read_response(0x50, arg_format=". 16 | -------------------------------------------------------------------------------- /test/dev/test_device_base.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2023 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from unittest import TestCase 19 | 20 | from pyshimmer.dev.base import sr2dr, dr2sr, sec2ticks, ticks2sec 21 | 22 | 23 | class DeviceBaseTest(TestCase): 24 | 25 | def test_sr2dr(self): 26 | r = sr2dr(1024.0) 27 | self.assertEqual(r, 32) 28 | 29 | r = sr2dr(500.0) 30 | self.assertEqual(r, 66) 31 | 32 | def test_dr2sr(self): 33 | r = dr2sr(65) 34 | self.assertEqual(round(r), 504) 35 | 36 | r = dr2sr(32) 37 | self.assertEqual(r, 1024.0) 38 | 39 | r = dr2sr(64) 40 | self.assertEqual(r, 512.0) 41 | 42 | def test_sec2ticks(self): 43 | r = sec2ticks(1.0) 44 | self.assertEqual(r, 32768) 45 | 46 | def test_ticks2sec(self): 47 | r = ticks2sec(32768) 48 | self.assertEqual(r, 1.0) 49 | 50 | r = ticks2sec(65536) 51 | self.assertEqual(r, 2.0) 52 | -------------------------------------------------------------------------------- /test/dev/test_device_calibration.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2023 Lukas Magel, Manuel Fernandez-Carmona 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | import random 19 | from unittest import TestCase 20 | 21 | from pyshimmer.dev.calibration import AllCalibration 22 | 23 | 24 | def randbytes(k: int) -> bytes: 25 | population = list(range(256)) 26 | seq = random.choices(population, k=k) 27 | return bytes(seq) 28 | 29 | 30 | class AllCalibrationTest(TestCase): 31 | 32 | def test_equality_operator(self): 33 | def do_assert(a: bytes, b: bytes, result: bool) -> None: 34 | self.assertEqual(AllCalibration(a) == AllCalibration(b), result) 35 | 36 | x = randbytes(84) 37 | y = randbytes(84) 38 | 39 | do_assert(x, y, False) 40 | do_assert(x, x, True) 41 | do_assert(y, y, True) 42 | 43 | for i in range(len(x)): 44 | y = bytearray(x) 45 | y[i] = random.randrange(0, 256) 46 | do_assert(x, y, False) 47 | 48 | def setUp(self) -> None: 49 | random.seed(0x42) 50 | 51 | def test_allcalibration_fail(self): 52 | self.assertRaises(ValueError, AllCalibration, bytes()) 53 | 54 | def test_allcalibration(self): 55 | bin_reg1 = bytes( 56 | [ 57 | 0x08, 58 | 0xCD, 59 | 0x08, 60 | 0xCD, 61 | 0x08, 62 | 0xCD, 63 | 0x00, 64 | 0x5C, 65 | 0x00, 66 | 0x5C, 67 | 0x00, 68 | 0x5C, 69 | 0x00, 70 | 0x9C, 71 | 0x00, 72 | 0x9C, 73 | 0x00, 74 | 0x00, 75 | 0x00, 76 | 0x00, 77 | 0x9C, 78 | 0x00, 79 | 0x00, 80 | 0x00, 81 | 0x00, 82 | 0x00, 83 | 0x00, 84 | 0x19, 85 | 0x96, 86 | 0x19, 87 | 0x96, 88 | 0x19, 89 | 0x96, 90 | 0x00, 91 | 0x9C, 92 | 0x00, 93 | 0x9C, 94 | 0x00, 95 | 0x00, 96 | 0x00, 97 | 0x00, 98 | 0x9C, 99 | 0x00, 100 | 0x00, 101 | 0x00, 102 | 0x00, 103 | 0x00, 104 | 0x00, 105 | 0x02, 106 | 0x9B, 107 | 0x02, 108 | 0x9B, 109 | 0x02, 110 | 0x9B, 111 | 0x00, 112 | 0x9C, 113 | 0x00, 114 | 0x64, 115 | 0x00, 116 | 0x00, 117 | 0x00, 118 | 0x00, 119 | 0x9C, 120 | 0x00, 121 | 0x00, 122 | 0x00, 123 | 0x00, 124 | 0x00, 125 | 0x00, 126 | 0x06, 127 | 0x87, 128 | 0x06, 129 | 0x87, 130 | 0x06, 131 | 0x87, 132 | 0x00, 133 | 0x9C, 134 | 0x00, 135 | 0x64, 136 | 0x00, 137 | 0x00, 138 | 0x00, 139 | 0x00, 140 | 0x9C, 141 | ] 142 | ) 143 | 144 | bin_reg2 = bytes( 145 | [ 146 | 0x08, 147 | 0xCD, 148 | 0x08, 149 | 0xCD, 150 | 0x08, 151 | 0xCD, 152 | 0x00, 153 | 0x5C, 154 | 0x00, 155 | 0x5C, 156 | 0x00, 157 | 0x5C, 158 | 0x00, 159 | 0x9C, 160 | 0x00, 161 | 0x9C, 162 | 0x00, 163 | 0x00, 164 | 0x00, 165 | 0x00, 166 | 0x9C, 167 | 0x00, 168 | 0x00, 169 | 0x00, 170 | 0x00, 171 | 0x00, 172 | 0x00, 173 | 0x19, 174 | 0x96, 175 | 0x19, 176 | 0x96, 177 | 0x19, 178 | 0x96, 179 | 0x00, 180 | 0x9C, 181 | 0x00, 182 | 0x9C, 183 | 0x00, 184 | 0x00, 185 | 0x00, 186 | 0x00, 187 | 0x9C, 188 | 0x00, 189 | 0x00, 190 | 0x00, 191 | 0x00, 192 | 0x00, 193 | 0x00, 194 | 0x00, 195 | 0x00, 196 | 0x00, 197 | 0x00, 198 | 0x00, 199 | 0x00, 200 | 0x00, 201 | 0x00, 202 | 0x00, 203 | 0x00, 204 | 0x00, 205 | 0x00, 206 | 0x00, 207 | 0x00, 208 | 0x00, 209 | 0x00, 210 | 0x00, 211 | 0x00, 212 | 0x00, 213 | 0x00, 214 | 0x00, 215 | 0x06, 216 | 0x87, 217 | 0x06, 218 | 0x87, 219 | 0x06, 220 | 0x87, 221 | 0x00, 222 | 0x9C, 223 | 0x00, 224 | 0x64, 225 | 0x00, 226 | 0x00, 227 | 0x00, 228 | 0x00, 229 | 0x9C, 230 | ] 231 | ) 232 | 233 | allcalib1 = AllCalibration(bin_reg1) 234 | allcalib2 = AllCalibration(bin_reg2) 235 | self.assertEqual(allcalib1.get_offset_bias(0), [2253, 2253, 2253]) 236 | self.assertEqual(allcalib1.get_sensitivity(0), [92, 92, 92]) 237 | self.assertEqual(allcalib1.get_ali_mat(0), [0, -100, 0, -100, 0, 0, 0, 0, -100]) 238 | self.assertEqual(allcalib1.get_offset_bias(1), [0, 0, 0]) 239 | self.assertEqual(allcalib1.get_sensitivity(1), [6550, 6550, 6550]) 240 | self.assertEqual(allcalib1.get_ali_mat(1), [0, -100, 0, -100, 0, 0, 0, 0, -100]) 241 | self.assertEqual(allcalib1.get_offset_bias(2), [0, 0, 0]) 242 | self.assertEqual(allcalib1.get_sensitivity(2), [667, 667, 667]) 243 | self.assertEqual(allcalib1.get_ali_mat(2), [0, -100, 0, 100, 0, 0, 0, 0, -100]) 244 | self.assertEqual(allcalib1.get_offset_bias(3), [0, 0, 0]) 245 | self.assertEqual(allcalib1.get_sensitivity(3), [1671, 1671, 1671]) 246 | self.assertEqual(allcalib1.get_ali_mat(3), [0, -100, 0, 100, 0, 0, 0, 0, -100]) 247 | 248 | self.assertEqual(allcalib2.get_offset_bias(0), [2253, 2253, 2253]) 249 | self.assertEqual(allcalib2.get_sensitivity(0), [92, 92, 92]) 250 | self.assertEqual(allcalib2.get_ali_mat(0), [0, -100, 0, -100, 0, 0, 0, 0, -100]) 251 | self.assertEqual(allcalib2.get_offset_bias(1), [0, 0, 0]) 252 | self.assertEqual(allcalib2.get_sensitivity(1), [6550, 6550, 6550]) 253 | self.assertEqual(allcalib2.get_ali_mat(1), [0, -100, 0, -100, 0, 0, 0, 0, -100]) 254 | self.assertEqual(allcalib2.get_offset_bias(2), [0, 0, 0]) 255 | self.assertEqual(allcalib2.get_sensitivity(2), [0, 0, 0]) 256 | self.assertEqual(allcalib2.get_ali_mat(2), [0, 0, 0, 0, 0, 0, 0, 0, 0]) 257 | self.assertEqual(allcalib2.get_offset_bias(3), [0, 0, 0]) 258 | self.assertEqual(allcalib2.get_sensitivity(3), [1671, 1671, 1671]) 259 | self.assertEqual(allcalib2.get_ali_mat(3), [0, -100, 0, 100, 0, 0, 0, 0, -100]) 260 | 261 | def test_exg_register_print(self): 262 | bin_reg = bytes( 263 | [ 264 | 0x08, 265 | 0xCD, 266 | 0x08, 267 | 0xCD, 268 | 0x08, 269 | 0xCD, 270 | 0x00, 271 | 0x5C, 272 | 0x00, 273 | 0x5C, 274 | 0x00, 275 | 0x5C, 276 | 0x00, 277 | 0x9C, 278 | 0x00, 279 | 0x9C, 280 | 0x00, 281 | 0x00, 282 | 0x00, 283 | 0x00, 284 | 0x9C, 285 | 0x00, 286 | 0x00, 287 | 0x00, 288 | 0x00, 289 | 0x00, 290 | 0x00, 291 | 0x19, 292 | 0x96, 293 | 0x19, 294 | 0x96, 295 | 0x19, 296 | 0x96, 297 | 0x00, 298 | 0x9C, 299 | 0x00, 300 | 0x9C, 301 | 0x00, 302 | 0x00, 303 | 0x00, 304 | 0x00, 305 | 0x9C, 306 | 0x00, 307 | 0x00, 308 | 0x00, 309 | 0x00, 310 | 0x00, 311 | 0x00, 312 | 0x00, 313 | 0x00, 314 | 0x00, 315 | 0x00, 316 | 0x00, 317 | 0x00, 318 | 0x00, 319 | 0x00, 320 | 0x00, 321 | 0x00, 322 | 0x00, 323 | 0x00, 324 | 0x00, 325 | 0x00, 326 | 0x00, 327 | 0x00, 328 | 0x00, 329 | 0x00, 330 | 0x00, 331 | 0x00, 332 | 0x00, 333 | 0x06, 334 | 0x87, 335 | 0x06, 336 | 0x87, 337 | 0x06, 338 | 0x87, 339 | 0x00, 340 | 0x9C, 341 | 0x00, 342 | 0x64, 343 | 0x00, 344 | 0x00, 345 | 0x00, 346 | 0x00, 347 | 0x9C, 348 | ] 349 | ) 350 | 351 | allcalib = AllCalibration(bin_reg) 352 | 353 | str_repr = str(allcalib) 354 | self.assertTrue("Offset bias: [0, 0, 0]" in str_repr) 355 | self.assertTrue("Sensitivity: [1671," in str_repr) 356 | -------------------------------------------------------------------------------- /test/dev/test_device_channels.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2023 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from unittest import TestCase 19 | 20 | from pyshimmer.dev.channels import ( 21 | ChDataTypeAssignment, 22 | get_ch_dtypes, 23 | SensorChannelAssignment, 24 | SensorBitAssignments, 25 | ChannelDataType, 26 | EChannelType, 27 | ESensorGroup, 28 | sort_sensors, 29 | ) 30 | 31 | 32 | class DeviceChannelsTest(TestCase): 33 | 34 | def test_channel_enum_uniqueness(self): 35 | try: 36 | # The exception will trigger upon import if the enum values are not unique 37 | from pyshimmer.dev.channels import EChannelType 38 | except ValueError as e: 39 | self.fail(f"Enum not unique: {e}") 40 | 41 | def test_channel_data_type_decoding(self): 42 | def test_both_endianess(byte_val_le: bytes, expected: int, signed: bool): 43 | blen = len(byte_val_le) 44 | dt_le = ChannelDataType(blen, signed=signed, le=True) 45 | dt_be = ChannelDataType(blen, signed=signed, le=False) 46 | 47 | self.assertEqual(expected, dt_le.decode(byte_val_le)) 48 | self.assertEqual(expected, dt_be.decode(byte_val_le[::-1])) 49 | 50 | # Test the property getters 51 | dt = ChannelDataType(3, signed=False, le=True) 52 | self.assertEqual(dt.little_endian, True) 53 | self.assertEqual(dt.big_endian, False) 54 | self.assertEqual(dt.signed, False) 55 | self.assertEqual(dt.size, 3) 56 | 57 | # Test the property getters 58 | dt = ChannelDataType(3, signed=False, le=False) 59 | self.assertEqual(dt.little_endian, False) 60 | self.assertEqual(dt.big_endian, True) 61 | 62 | # Test unsigned decodation for 3 byte data 63 | test_both_endianess(b"\x00\x00\x00", 0x000000, signed=False) 64 | test_both_endianess(b"\x10\x00\x00", 0x000010, signed=False) 65 | test_both_endianess(b"\x00\x00\xff", 0xFF0000, signed=False) 66 | test_both_endianess(b"\xff\xff\xff", 0xFFFFFF, signed=False) 67 | 68 | # Test signed decodation for 3 byte data 69 | test_both_endianess(b"\xff\xff\xff", -1, signed=True) 70 | test_both_endianess(b"\x00\x00\x80", -(2**23), signed=True) 71 | test_both_endianess(b"\xff\xff\x7f", 2**23 - 1, signed=True) 72 | test_both_endianess(b"\xff\x00\x00", 255, signed=True) 73 | 74 | # Test unsigned decodation for 2 byte data 75 | test_both_endianess(b"\x00\x00", 0x0000, signed=False) 76 | test_both_endianess(b"\x10\x00", 0x0010, signed=False) 77 | test_both_endianess(b"\x00\xff", 0xFF00, signed=False) 78 | test_both_endianess(b"\xff\xff", 0xFFFF, signed=False) 79 | 80 | # Test signed decodation for 2 byte data 81 | test_both_endianess(b"\xff\xff", -1, signed=True) 82 | test_both_endianess(b"\x00\x80", -(2**15), signed=True) 83 | test_both_endianess(b"\xff\x7f", 2**15 - 1, signed=True) 84 | test_both_endianess(b"\xff\x00", 255, signed=True) 85 | 86 | def test_channel_data_type_encoding(self): 87 | def test_both_endianess(val: int, val_len: int, expected: bytes, signed: bool): 88 | dt_le = ChannelDataType(val_len, signed=signed, le=True) 89 | dt_be = ChannelDataType(val_len, signed=signed, le=False) 90 | 91 | self.assertEqual(expected, dt_le.encode(val)) 92 | self.assertEqual(expected[::-1], dt_be.encode(val)) 93 | 94 | test_both_endianess(0x1234, 2, b"\x34\x12", signed=False) 95 | test_both_endianess(-0x10, 2, b"\xf0\xff", signed=True) 96 | 97 | test_both_endianess(0x12345, 3, b"\x45\x23\x01", signed=False) 98 | test_both_endianess(-0x12345, 3, b"\xbb\xdc\xfe", signed=True) 99 | 100 | def test_get_ch_dtypes(self): 101 | channels = [EChannelType.INTERNAL_ADC_13, EChannelType.GYRO_MPU9150_Y] 102 | r = get_ch_dtypes(channels) 103 | 104 | self.assertEqual(len(r), 2) 105 | first, second = r 106 | 107 | self.assertEqual(first.size, 2) 108 | self.assertEqual(first.little_endian, True) 109 | self.assertEqual(first.signed, False) 110 | 111 | self.assertEqual(second.size, 2) 112 | self.assertEqual(second.little_endian, False) 113 | self.assertEqual(second.signed, True) 114 | 115 | def test_sensor_group_uniqueness(self): 116 | try: 117 | # The exception will trigger upon import if the enum values are not unique 118 | from pyshimmer.dev.channels import ESensorGroup 119 | except ValueError as e: 120 | self.fail(f"Enum not unique: {e}") 121 | 122 | def test_datatype_assignments(self): 123 | from pyshimmer.dev.channels import EChannelType 124 | 125 | for ch_type in EChannelType: 126 | if ch_type not in ChDataTypeAssignment: 127 | self.fail(f"No data type assigned to channel type: {ch_type}") 128 | 129 | def test_sensor_channel_assignments(self): 130 | from pyshimmer.dev.channels import ESensorGroup 131 | 132 | for sensor in ESensorGroup: 133 | if sensor not in SensorChannelAssignment: 134 | self.fail(f"No channels assigned to sensor type: {sensor}") 135 | 136 | def test_sensor_bit_assignments_uniqueness(self): 137 | for s1 in SensorBitAssignments.keys(): 138 | for s2 in SensorBitAssignments.keys(): 139 | if s1 != s2 and SensorBitAssignments[s1] == SensorBitAssignments[s2]: 140 | self.fail( 141 | f"Colliding bitfield assignments for sensor {s1} and {s2}" 142 | ) 143 | 144 | def test_sort_sensors(self): 145 | sensors = [ESensorGroup.BATTERY, ESensorGroup.ACCEL_LN] 146 | expected = [ESensorGroup.ACCEL_LN, ESensorGroup.BATTERY] 147 | r = sort_sensors(sensors) 148 | self.assertEqual(r, expected) 149 | 150 | sensors = [ 151 | ESensorGroup.CH_A15, 152 | ESensorGroup.MAG_MPU, 153 | ESensorGroup.ACCEL_LN, 154 | ESensorGroup.CH_A15, 155 | ] 156 | expected = [ 157 | ESensorGroup.ACCEL_LN, 158 | ESensorGroup.CH_A15, 159 | ESensorGroup.CH_A15, 160 | ESensorGroup.MAG_MPU, 161 | ] 162 | r = sort_sensors(sensors) 163 | self.assertEqual(r, expected) 164 | -------------------------------------------------------------------------------- /test/dev/test_device_exg.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2023 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | import random 19 | from unittest import TestCase 20 | 21 | from pyshimmer.dev.channels import EChannelType 22 | from pyshimmer.dev.exg import ( 23 | is_exg_ch, 24 | get_exg_ch, 25 | ExGMux, 26 | ExGRLDLead, 27 | ERLDRef, 28 | ExGRegister, 29 | ) 30 | 31 | 32 | def randbytes(k: int) -> bytes: 33 | population = list(range(256)) 34 | seq = random.choices(population, k=k) 35 | return bytes(seq) 36 | 37 | 38 | class DeviceExGTest(TestCase): 39 | 40 | def test_get_exg_ch(self): 41 | self.assertEqual(get_exg_ch(EChannelType.EXG_ADS1292R_1_CH1_24BIT), (0, 0)) 42 | self.assertEqual(get_exg_ch(EChannelType.EXG_ADS1292R_1_CH2_24BIT), (0, 1)) 43 | self.assertEqual(get_exg_ch(EChannelType.EXG_ADS1292R_2_CH1_24BIT), (1, 0)) 44 | self.assertEqual(get_exg_ch(EChannelType.EXG_ADS1292R_2_CH2_24BIT), (1, 1)) 45 | 46 | def test_is_exg_ch(self): 47 | from itertools import product 48 | 49 | valid_ch = [ 50 | EChannelType[f"EXG_ADS1292R_{i}_CH{j}_{k}BIT"] 51 | for i, j, k in product([1, 2], [1, 2], [16, 24]) 52 | ] 53 | 54 | for ch in EChannelType: 55 | self.assertEqual(is_exg_ch(ch), ch in valid_ch) 56 | 57 | 58 | class ExGRegisterTest(TestCase): 59 | 60 | def setUp(self) -> None: 61 | random.seed(0x42) 62 | 63 | def test_exg_register_fail(self): 64 | self.assertRaises(ValueError, ExGRegister, bytes()) 65 | 66 | def test_exg_register(self): 67 | reg1 = bytes([3, 160, 16, 64, 71, 0, 0, 0, 2, 1]) 68 | reg2 = bytes([0, 171, 16, 21, 21, 0, 0, 0, 2, 1]) 69 | 70 | exg_reg1 = ExGRegister(reg1) 71 | exg_reg2 = ExGRegister(reg2) 72 | 73 | self.assertEqual(exg_reg1.ch1_gain, 4) 74 | self.assertEqual(exg_reg1.ch2_gain, 4) 75 | self.assertEqual(exg_reg1.ch1_mux, ExGMux.NORMAL) 76 | self.assertEqual(exg_reg1.get_ch_mux_bin(0), 0b0000) 77 | self.assertEqual(exg_reg1.ch2_mux, ExGMux.RLD_DRM) 78 | self.assertEqual(exg_reg1.get_ch_mux_bin(1), 0b0111) 79 | self.assertEqual(exg_reg1.ch1_powerdown, False) 80 | self.assertEqual(exg_reg1.ch2_powerdown, False) 81 | self.assertEqual(exg_reg1.data_rate, 1000) 82 | self.assertEqual(exg_reg1.binary, reg1) 83 | 84 | self.assertEqual(exg_reg2.ch1_gain, 1) 85 | self.assertEqual(exg_reg2.ch2_gain, 1) 86 | self.assertEqual(exg_reg2.ch1_mux, ExGMux.TEST_SIGNAL) 87 | self.assertEqual(exg_reg2.ch2_mux, ExGMux.TEST_SIGNAL) 88 | self.assertEqual(exg_reg2.ch1_powerdown, False) 89 | self.assertEqual(exg_reg2.ch2_powerdown, False) 90 | self.assertEqual(exg_reg2.data_rate, 125) 91 | self.assertEqual(exg_reg2.binary, reg2) 92 | 93 | self.assertRaises(ValueError, exg_reg1.get_ch_mux, 2) 94 | self.assertRaises(ValueError, exg_reg1.get_ch_mux, -1) 95 | 96 | def test_exg_register_powerdown(self): 97 | pd = 0x1 << 7 98 | reg_bin = bytes([3, 160, 16, pd, pd, 0, 0, 0, 2, 1]) 99 | reg = ExGRegister(reg_bin) 100 | 101 | self.assertEqual(reg.ch1_powerdown, True) 102 | self.assertEqual(reg.ch2_powerdown, True) 103 | 104 | def test_exg_register_rld_powerdown(self): 105 | pd = 0x01 << 5 106 | reg_bin = bytes([0, 0, 0, 0, 0, pd, 0, 0, 0, 0]) 107 | reg = ExGRegister(reg_bin) 108 | 109 | self.assertEqual(reg.rld_powerdown, False) 110 | 111 | def test_exg_register_rld_channels(self): 112 | reg_bin = bytes([0x03, 0xA8, 0x10, 0x40, 0x40, 0x2D, 0x00, 0x00, 0x02, 0x03]) 113 | reg = ExGRegister(reg_bin) 114 | self.assertEqual( 115 | reg.rld_channels, [ExGRLDLead.RLD1P, ExGRLDLead.RLD2P, ExGRLDLead.RLD2N] 116 | ) 117 | 118 | reg_bin = bytes([0x03, 0xA8, 0x10, 0x40, 0x40, 0x00, 0x00, 0x00, 0x02, 0x03]) 119 | reg = ExGRegister(reg_bin) 120 | self.assertEqual(reg.rld_channels, []) 121 | 122 | def test_exg_register_rld_ref(self): 123 | reg_bin = bytes([0x03, 0xA8, 0x10, 0x40, 0x40, 0x2D, 0x00, 0x00, 0x02, 0x03]) 124 | reg = ExGRegister(reg_bin) 125 | self.assertEqual(reg.rld_ref, ERLDRef.INTERNAL) 126 | 127 | reg_bin = bytes([0x03, 0xA8, 0x10, 0x40, 0x40, 0x2D, 0x00, 0x00, 0x02, 0x01]) 128 | reg = ExGRegister(reg_bin) 129 | self.assertEqual(reg.rld_ref, ERLDRef.EXTERNAL) 130 | 131 | def test_exg_register_print(self): 132 | reg_bin = bytes([0x03, 0xA8, 0x10, 0x40, 0x40, 0x2D, 0x00, 0x00, 0x02, 0x03]) 133 | reg = ExGRegister(reg_bin) 134 | 135 | str_repr = str(reg) 136 | self.assertTrue("Data Rate: 1000" in str_repr) 137 | self.assertTrue("RLD Powerdown: False" in str_repr) 138 | 139 | def test_equality_operator(self): 140 | def do_assert(a: bytes, b: bytes, result: bool) -> None: 141 | self.assertEqual(ExGRegister(a) == ExGRegister(b), result) 142 | 143 | x = randbytes(10) 144 | y = randbytes(10) 145 | 146 | do_assert(x, y, False) 147 | do_assert(x, x, True) 148 | do_assert(y, y, True) 149 | 150 | for i in range(len(x)): 151 | y = bytearray(x) 152 | y[i] = random.randrange(0, 256) 153 | do_assert(x, y, False) 154 | -------------------------------------------------------------------------------- /test/dev/test_device_fw_version.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2023 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from unittest import TestCase 19 | 20 | from pyshimmer.dev.fw_version import ( 21 | FirmwareVersion, 22 | get_firmware_type, 23 | EFirmwareType, 24 | FirmwareCapabilities, 25 | ) 26 | 27 | 28 | class DeviceFirmwareVersionTest(TestCase): 29 | 30 | def test_get_firmware_type(self): 31 | r = get_firmware_type(0x01) 32 | self.assertEqual(r, EFirmwareType.BtStream) 33 | r = get_firmware_type(0x02) 34 | self.assertEqual(r, EFirmwareType.SDLog) 35 | r = get_firmware_type(0x03) 36 | self.assertEqual(r, EFirmwareType.LogAndStream) 37 | 38 | self.assertRaises(ValueError, get_firmware_type, 0xFF) 39 | 40 | 41 | class FirmwareCapabilitiesTest(TestCase): 42 | 43 | def test_capabilities(self): 44 | cap = FirmwareCapabilities( 45 | EFirmwareType.LogAndStream, version=FirmwareVersion(1, 2, 3) 46 | ) 47 | self.assertTrue(cap.supports_ack_disable) 48 | self.assertEqual(cap.version, FirmwareVersion(1, 2, 3)) 49 | self.assertEqual(cap.fw_type, EFirmwareType.LogAndStream) 50 | 51 | 52 | class FirmwareVersionTest(TestCase): 53 | 54 | def test_version_equality(self): 55 | a = FirmwareVersion(1, 2, 3) 56 | b = FirmwareVersion(1, 2, 3) 57 | c = FirmwareVersion(3, 2, 1) 58 | 59 | self.assertEqual(a, a) 60 | self.assertEqual(a, b) 61 | 62 | self.assertNotEqual(a, None) 63 | self.assertNotEqual(a, False) 64 | self.assertNotEqual(a, 10) 65 | self.assertNotEqual(a, c) 66 | 67 | def test_attributes(self): 68 | ver = FirmwareVersion(1, 2, 3) 69 | self.assertEqual(ver.major, 1) 70 | self.assertEqual(ver.minor, 2) 71 | self.assertEqual(ver.rel, 3) 72 | 73 | def test_greater_less(self): 74 | a = FirmwareVersion(3, 2, 1) 75 | 76 | b = FirmwareVersion(3, 2, 1) 77 | self.assertFalse(b > a) 78 | self.assertTrue(b >= a) 79 | self.assertFalse(b < a) 80 | self.assertTrue(b <= a) 81 | 82 | b = FirmwareVersion(2, 2, 1) 83 | self.assertFalse(b > a) 84 | self.assertFalse(b >= a) 85 | self.assertTrue(b < a) 86 | self.assertTrue(b <= a) 87 | 88 | b = FirmwareVersion(3, 1, 1) 89 | self.assertFalse(b > a) 90 | self.assertFalse(b >= a) 91 | self.assertTrue(b < a) 92 | self.assertTrue(b <= a) 93 | 94 | b = FirmwareVersion(3, 2, 0) 95 | self.assertFalse(b > a) 96 | self.assertFalse(b >= a) 97 | self.assertTrue(b < a) 98 | self.assertTrue(b <= a) 99 | -------------------------------------------------------------------------------- /test/reader/__init__.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /test/reader/reader_test_util.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from pathlib import Path 19 | 20 | _res_folder_name = "resources" 21 | _single_sample_name = "single_sample.bin" 22 | _synced_pair_bin_name = "sdlog_sync_slave.bin" 23 | _synced_pair_csv_name = "sdlog_sync_slave.csv.gz" 24 | _pair_raw_name = "pair_raw.bin" 25 | _pair_csv_name = "pair_consensys.csv" 26 | 27 | _acc_gyro_sample_name = "triaxcal_sample.bin" 28 | _acc_gyro_uncal_name = "triaxcal_uncalibrated.csv.gz" 29 | _acc_gyro_cal_name = "triaxcal_calibrated.csv.gz" 30 | 31 | _ecg_sample_bin = "ecg.bin" 32 | _ecg_sample_uncal = "ecg_uncalibrated.csv.gz" 33 | _ecg_sample_cal = "ecg_calibrated.csv.gz" 34 | 35 | 36 | def get_resources_dir(): 37 | my_dir = Path(__file__).parent 38 | res_dir = my_dir / _res_folder_name 39 | return res_dir 40 | 41 | 42 | def get_binary_sample_fpath(): 43 | return get_resources_dir() / _single_sample_name 44 | 45 | 46 | def get_bin_vs_consensys_pair_fpath(): 47 | res_dir = get_resources_dir() 48 | return res_dir / _pair_raw_name, res_dir / _pair_csv_name 49 | 50 | 51 | def get_synced_bin_vs_consensys_pair_fpath(): 52 | res_dir = get_resources_dir() 53 | return res_dir / _synced_pair_bin_name, res_dir / _synced_pair_csv_name 54 | 55 | 56 | def get_ecg_sample(): 57 | res_dir = get_resources_dir() 58 | return ( 59 | res_dir / _ecg_sample_bin, 60 | res_dir / _ecg_sample_uncal, 61 | res_dir / _ecg_sample_cal, 62 | ) 63 | 64 | 65 | def get_triaxcal_sample(): 66 | res_dir = get_resources_dir() 67 | return ( 68 | res_dir / _acc_gyro_sample_name, 69 | res_dir / _acc_gyro_uncal_name, 70 | res_dir / _acc_gyro_cal_name, 71 | ) 72 | -------------------------------------------------------------------------------- /test/reader/resources/ecg.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seemoo-lab/pyshimmer/d3df5bb597380f0a16ce1aeb05938039e336bc86/test/reader/resources/ecg.bin -------------------------------------------------------------------------------- /test/reader/resources/ecg_calibrated.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seemoo-lab/pyshimmer/d3df5bb597380f0a16ce1aeb05938039e336bc86/test/reader/resources/ecg_calibrated.csv.gz -------------------------------------------------------------------------------- /test/reader/resources/ecg_uncalibrated.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seemoo-lab/pyshimmer/d3df5bb597380f0a16ce1aeb05938039e336bc86/test/reader/resources/ecg_uncalibrated.csv.gz -------------------------------------------------------------------------------- /test/reader/resources/pair_raw.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seemoo-lab/pyshimmer/d3df5bb597380f0a16ce1aeb05938039e336bc86/test/reader/resources/pair_raw.bin -------------------------------------------------------------------------------- /test/reader/resources/sdlog_sync_slave.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seemoo-lab/pyshimmer/d3df5bb597380f0a16ce1aeb05938039e336bc86/test/reader/resources/sdlog_sync_slave.bin -------------------------------------------------------------------------------- /test/reader/resources/sdlog_sync_slave.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seemoo-lab/pyshimmer/d3df5bb597380f0a16ce1aeb05938039e336bc86/test/reader/resources/sdlog_sync_slave.csv.gz -------------------------------------------------------------------------------- /test/reader/resources/single_sample.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seemoo-lab/pyshimmer/d3df5bb597380f0a16ce1aeb05938039e336bc86/test/reader/resources/single_sample.bin -------------------------------------------------------------------------------- /test/reader/resources/triaxcal_calibrated.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seemoo-lab/pyshimmer/d3df5bb597380f0a16ce1aeb05938039e336bc86/test/reader/resources/triaxcal_calibrated.csv.gz -------------------------------------------------------------------------------- /test/reader/resources/triaxcal_sample.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seemoo-lab/pyshimmer/d3df5bb597380f0a16ce1aeb05938039e336bc86/test/reader/resources/triaxcal_sample.bin -------------------------------------------------------------------------------- /test/reader/resources/triaxcal_uncalibrated.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seemoo-lab/pyshimmer/d3df5bb597380f0a16ce1aeb05938039e336bc86/test/reader/resources/triaxcal_uncalibrated.csv.gz -------------------------------------------------------------------------------- /test/reader/test_binary_reader.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from unittest import TestCase 19 | 20 | import numpy as np 21 | 22 | from pyshimmer import EChannelType, ExGRegister 23 | from pyshimmer.dev.channels import ESensorGroup, get_ch_dtypes 24 | from pyshimmer.reader.shimmer_reader import ShimmerBinaryReader 25 | from .reader_test_util import ( 26 | get_binary_sample_fpath, 27 | get_synced_bin_vs_consensys_pair_fpath, 28 | get_ecg_sample, 29 | get_triaxcal_sample, 30 | ) 31 | 32 | 33 | class ShimmerReaderTest(TestCase): 34 | 35 | def test_parsing_wo_sync(self): 36 | fpath = get_binary_sample_fpath() 37 | with open(fpath, "rb") as f: 38 | reader = ShimmerBinaryReader(f) 39 | 40 | exp_dr = 65 41 | exp_sensors = [ 42 | ESensorGroup.ACCEL_LN, 43 | ESensorGroup.BATTERY, 44 | ESensorGroup.CH_A13, 45 | ] 46 | exp_channels = [ 47 | EChannelType.TIMESTAMP, 48 | EChannelType.ACCEL_LN_X, 49 | EChannelType.ACCEL_LN_Y, 50 | EChannelType.ACCEL_LN_Z, 51 | EChannelType.VBATT, 52 | EChannelType.INTERNAL_ADC_13, 53 | ] 54 | 55 | sample_size = sum([dt.size for dt in get_ch_dtypes(exp_channels)]) 56 | samples_per_block = int(512 / sample_size) 57 | block_size = samples_per_block * sample_size 58 | 59 | self.assertEqual(reader.enabled_sensors, exp_sensors) 60 | self.assertEqual(reader.enabled_channels, exp_channels) 61 | self.assertEqual(reader.sample_rate, exp_dr) 62 | self.assertEqual(reader.has_global_clock, True) 63 | self.assertEqual(reader.has_sync, False) 64 | self.assertEqual(reader.is_sync_master, False) 65 | self.assertEqual(reader.samples_per_block, samples_per_block) 66 | self.assertEqual(reader.start_timestamp, 31291951) 67 | self.assertEqual(reader.block_size, block_size) 68 | self.assertEqual( 69 | reader.exg_reg1.binary, b"\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01" 70 | ) 71 | self.assertEqual( 72 | reader.exg_reg2.binary, b"\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01" 73 | ) 74 | 75 | data, _ = reader.read_data() 76 | ts = data[EChannelType.TIMESTAMP] 77 | 78 | # Sanity check on the timestamps: they should all be spaced equally apart 79 | # with a stride that is equal to the sampling rate. 80 | ts_diff = np.diff(ts) 81 | correct_diff = np.sum(ts_diff == exp_dr) 82 | self.assertTrue(correct_diff / len(ts_diff) > 0.98) 83 | 84 | def test_parsing_w_sync(self): 85 | fpath, _ = get_synced_bin_vs_consensys_pair_fpath() 86 | with open(fpath, "rb") as f: 87 | reader = ShimmerBinaryReader(f) 88 | 89 | exp_dr = 64 90 | exp_sensors = [ESensorGroup.CH_A13] 91 | exp_channels = [EChannelType.TIMESTAMP, EChannelType.INTERNAL_ADC_13] 92 | exp_offsets = np.array([372, 362, 364, 351]) 93 | exp_sync_ts = np.array([3725366, 4071094, 4397558, 4724022]) 94 | exp_exg_reg1 = ExGRegister(b"\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01") 95 | exp_exg_reg2 = ExGRegister(b"\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01") 96 | 97 | sample_size = sum([dt.size for dt in get_ch_dtypes(exp_channels)]) 98 | samples_per_block = int((512 - 9) / sample_size) 99 | block_size = samples_per_block * sample_size + 9 100 | 101 | self.assertEqual(reader.has_global_clock, True) 102 | self.assertEqual(reader.has_sync, True) 103 | self.assertEqual(reader.is_sync_master, False) 104 | self.assertEqual(reader.enabled_sensors, exp_sensors) 105 | self.assertEqual(reader.enabled_channels, exp_channels) 106 | self.assertEqual(reader.sample_rate, exp_dr) 107 | self.assertEqual(reader.samples_per_block, samples_per_block) 108 | self.assertEqual(reader.start_timestamp, 3085110) 109 | self.assertEqual(reader.block_size, block_size) 110 | 111 | self.assertEqual(reader.get_exg_reg(0), exp_exg_reg1) 112 | self.assertEqual(reader.get_exg_reg(1), exp_exg_reg2) 113 | self.assertEqual(reader.exg_reg1, exp_exg_reg1) 114 | self.assertEqual(reader.exg_reg2, exp_exg_reg2) 115 | 116 | samples, (off_index, sync_off) = reader.read_data() 117 | ts = samples[EChannelType.TIMESTAMP] 118 | 119 | np.testing.assert_equal(ts[off_index], exp_sync_ts) 120 | np.testing.assert_equal(sync_off, exp_offsets) 121 | 122 | # Sanity check on the timestamps: they should all be spaced equally apart 123 | # with a stride that is equal # to the sampling rate. 124 | ts = samples[EChannelType.TIMESTAMP] 125 | ts_diff = np.diff(ts) 126 | correct_diff = np.sum(ts_diff == exp_dr) 127 | self.assertTrue(correct_diff / len(ts_diff) > 0.98) 128 | 129 | def test_ecg_registers(self): 130 | fpath, _, _ = get_ecg_sample() 131 | with open(fpath, "rb") as f: 132 | reader = ShimmerBinaryReader(f) 133 | self.assertEqual( 134 | reader.exg_reg1.binary, b"\x03\xa8\x10\x49\x40\x23\x00\x00\x02\x03" 135 | ) 136 | self.assertEqual( 137 | reader.exg_reg2.binary, b"\x03\xa0\x10\xc1\xc1\x00\x00\x00\x02\x01" 138 | ) 139 | 140 | # noinspection PyMethodMayBeStatic 141 | def test_accel_ln_calib_data(self): 142 | exp_params = { 143 | ESensorGroup.ACCEL_LN: ( 144 | np.array([2045, 2071, 2033]), 145 | np.diag([83, 83, 83]), 146 | np.array([[0.0, 1.0, 0.0], [1.0, 0.0, 0.02], [0.02, -0.01, -1.0]]), 147 | ), 148 | ESensorGroup.GYRO: ( 149 | np.array([-123, -29, -35]), 150 | np.diag([56.68, 57.91, 59.21]), 151 | np.array([[0.0, 1.0, -0.02], [1.0, 0.0, 0.03], [-0.25, 0.01, -0.97]]), 152 | ), 153 | } 154 | 155 | fpath, _, _ = get_triaxcal_sample() 156 | with open(fpath, "rb") as f: 157 | reader = ShimmerBinaryReader(f) 158 | 159 | for sensor, (exp_offset, exp_gain, exp_alignment) in exp_params.items(): 160 | offset, gain, alignment = reader.get_triaxcal_params(sensor) 161 | np.testing.assert_almost_equal(offset, exp_offset, decimal=10) 162 | np.testing.assert_almost_equal(gain, exp_gain, decimal=10) 163 | np.testing.assert_almost_equal(alignment, exp_alignment, decimal=10) 164 | -------------------------------------------------------------------------------- /test/reader/test_shimmer_reader.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from unittest import TestCase 19 | from unittest.mock import Mock, PropertyMock 20 | 21 | import numpy as np 22 | import pandas as pd 23 | 24 | from pyshimmer import EChannelType, ExGRegister 25 | from pyshimmer.dev.base import ticks2sec 26 | from pyshimmer.dev.channels import ESensorGroup, get_enabled_channels 27 | from pyshimmer.dev.exg import get_exg_ch 28 | from pyshimmer.reader.binary_reader import ShimmerBinaryReader 29 | from pyshimmer.reader.shimmer_reader import ( 30 | ShimmerReader, 31 | SingleChannelProcessor, 32 | PPGProcessor, 33 | TriAxCalProcessor, 34 | ) 35 | from .reader_test_util import ( 36 | get_bin_vs_consensys_pair_fpath, 37 | get_synced_bin_vs_consensys_pair_fpath, 38 | get_ecg_sample, 39 | get_triaxcal_sample, 40 | ) 41 | 42 | 43 | class ShimmerReaderTest(TestCase): 44 | 45 | def test_reader_timestep_interpolation(self): 46 | sr = 5 47 | ts_dev = np.array([0, 5, 10, 15, 21, 25, 29, 35]) 48 | ts = ticks2sec(ts_dev) 49 | vbatt = np.array([93, 85, 78, 74, 71, 68, 65, 64]) 50 | samples = { 51 | EChannelType.TIMESTAMP: ts_dev, 52 | EChannelType.VBATT: vbatt, 53 | } 54 | 55 | m_br = Mock(spec=ShimmerBinaryReader) 56 | m_br.read_data.return_value = (samples, []) 57 | type(m_br).sample_rate = PropertyMock(return_value=sr) 58 | type(m_br).enabled_sensors = PropertyMock(return_value=[]) 59 | type(m_br).has_sync = PropertyMock(return_value=False) 60 | type(m_br).has_global_clock = PropertyMock(return_value=False) 61 | type(m_br).start_timestamp = PropertyMock(return_value=0) 62 | 63 | reader = ShimmerReader(bin_reader=m_br) 64 | reader.load_file_data() 65 | 66 | ts_aligned = reader.timestamp 67 | self.assertEqual(len(ts_aligned), len(ts)) 68 | 69 | vbatt_aligned = reader[EChannelType.VBATT] 70 | np.testing.assert_equal( 71 | vbatt[ts_aligned == ts], vbatt_aligned[ts_aligned == ts] 72 | ) 73 | 74 | def test_timestamp_unwrapping(self): 75 | sr = 65 76 | ts_dev = np.arange(0, 4 * (2**24), sr) 77 | ts_dev_wrapped = ts_dev % 2**24 78 | 79 | ts = ticks2sec(ts_dev) 80 | vbatt = np.random.randint(0, 100 + 1, len(ts_dev)) 81 | 82 | m_br = Mock(spec=ShimmerBinaryReader) 83 | type(m_br).has_sync = PropertyMock(return_value=False) 84 | type(m_br).enabled_sensors = PropertyMock(return_value=[]) 85 | type(m_br).sample_rate = PropertyMock(return_value=sr) 86 | type(m_br).has_global_clock = PropertyMock(return_value=False) 87 | type(m_br).start_timestamp = PropertyMock(return_value=0) 88 | 89 | samples = { 90 | EChannelType.VBATT: vbatt, 91 | EChannelType.TIMESTAMP: ts_dev_wrapped, 92 | } 93 | m_br.read_data.return_value = (samples, []) 94 | 95 | reader = ShimmerReader(bin_reader=m_br) 96 | reader.load_file_data() 97 | 98 | ts_actual = reader.timestamp 99 | self.assertEqual(len(ts_actual), len(ts)) 100 | np.testing.assert_almost_equal(ts_actual, ts) 101 | np.testing.assert_equal(reader[EChannelType.VBATT], vbatt) 102 | np.testing.assert_equal(reader.timestamp, reader[EChannelType.TIMESTAMP]) 103 | 104 | def test_timestamp_synchronization(self): 105 | sr = 5 106 | ts = np.array([0, 5, 10, 15, 20, 25, 30, 35, 40, 45]) 107 | vbatt = np.array([93, 85, 78, 74, 71, 68, 65, 64, 10, 24]) 108 | samples = { 109 | EChannelType.TIMESTAMP: ts, 110 | EChannelType.VBATT: vbatt, 111 | } 112 | 113 | sync_index = np.array([0, len(ts) - 1]) 114 | sync_offset = np.array([1, 0]) 115 | 116 | m_br = Mock(spec=ShimmerBinaryReader) 117 | m_br.read_data.return_value = (samples, [sync_index, sync_offset]) 118 | type(m_br).sample_rate = PropertyMock(return_value=sr) 119 | type(m_br).enabled_sensors = PropertyMock(return_value=[]) 120 | type(m_br).has_sync = PropertyMock(return_value=True) 121 | type(m_br).has_global_clock = PropertyMock(return_value=False) 122 | type(m_br).start_timestamp = PropertyMock(return_value=0) 123 | 124 | reader = ShimmerReader(bin_reader=m_br) 125 | reader.load_file_data() 126 | 127 | ts_sync_dev = ts - np.linspace(1, 0, len(ts)) 128 | exp_ts = ticks2sec(ts_sync_dev) 129 | act_ts = reader.timestamp 130 | 131 | self.assertEqual(len(exp_ts), len(act_ts)) 132 | np.testing.assert_almost_equal(act_ts, exp_ts) 133 | np.testing.assert_equal(vbatt, reader[EChannelType.VBATT]) 134 | 135 | # noinspection PyMethodMayBeStatic 136 | def test_compare_ppg_processiong_to_consensys(self): 137 | raw_file, csv_file = get_bin_vs_consensys_pair_fpath() 138 | 139 | exp_sr = 504.12 140 | exp_channels = [ 141 | EChannelType.ACCEL_LN_X, 142 | EChannelType.ACCEL_LN_Y, 143 | EChannelType.ACCEL_LN_Z, 144 | EChannelType.VBATT, 145 | EChannelType.INTERNAL_ADC_13, 146 | ] 147 | 148 | with open(raw_file, "rb") as f: 149 | reader = ShimmerReader(f) 150 | reader.load_file_data() 151 | 152 | self.assertEqual(exp_channels, reader.channels) 153 | self.assertAlmostEqual(exp_sr, reader.sample_rate, 2) 154 | 155 | r = np.loadtxt(csv_file, delimiter="\t", skiprows=3, usecols=(0, 1)) 156 | expected_ts = r[:, 0] 157 | expected_ppg = r[:, 1] 158 | 159 | actual_ts = reader.timestamp * 1000 # needs to be in ms 160 | actual_ppg = reader[EChannelType.INTERNAL_ADC_13] * 1000.0 # needs to be in mV 161 | 162 | np.testing.assert_almost_equal(actual_ts.flatten(), expected_ts.flatten()) 163 | np.testing.assert_almost_equal(actual_ppg, expected_ppg) 164 | 165 | def test_compare_sync_processing_to_consensys(self): 166 | bin_path, csv_path = get_synced_bin_vs_consensys_pair_fpath() 167 | 168 | exp_sr = 512.0 169 | exp_channels = [EChannelType.INTERNAL_ADC_13] 170 | 171 | with open(bin_path, "rb") as f: 172 | reader = ShimmerReader(f, sync=True) 173 | reader.load_file_data() 174 | 175 | csv_data = np.loadtxt(csv_path, delimiter="\t", skiprows=3, usecols=(0, 1)) 176 | expected_ts = csv_data[:, 0] 177 | expected_ppg = csv_data[:, 1] 178 | 179 | actual_ts = reader.timestamp * 1000 180 | actual_ppg = reader[EChannelType.INTERNAL_ADC_13] * 1000.0 # needs to be in mV 181 | 182 | self.assertEqual(exp_channels, reader.channels) 183 | self.assertEqual(exp_sr, reader.sample_rate) 184 | np.testing.assert_almost_equal(actual_ts.flatten(), expected_ts.flatten()) 185 | np.testing.assert_almost_equal(actual_ppg.flatten(), expected_ppg.flatten()) 186 | 187 | def test_reader_exg_register(self): 188 | exp_reg1_content = bytes(range(10)) 189 | exp_reg1 = ExGRegister(exp_reg1_content) 190 | exp_reg2_content = bytes(range(10, 0, -1)) 191 | exp_reg2 = ExGRegister(exp_reg2_content) 192 | exp_regs = [exp_reg1, exp_reg2] 193 | 194 | m_br = Mock(spec=ShimmerBinaryReader) 195 | m_br.get_exg_reg.side_effect = lambda x: exp_regs[x] 196 | type(m_br).exg_reg1 = PropertyMock(return_value=exp_reg1) 197 | type(m_br).exg_reg2 = PropertyMock(return_value=exp_reg2) 198 | reader = ShimmerReader(bin_reader=m_br) 199 | 200 | for i in range(2): 201 | self.assertEqual(reader.get_exg_reg(i), exp_regs[i]) 202 | 203 | actual_reg1 = reader.exg_reg1 204 | actual_reg2 = reader.exg_reg2 205 | self.assertEqual(actual_reg1, exp_reg1) 206 | self.assertEqual(actual_reg2, exp_reg2) 207 | 208 | # noinspection PyMethodMayBeStatic 209 | def test_post_process_exg_signal(self): 210 | exg_reg1 = ExGRegister(b"\x03\x80\x10\x40\x40\x00\x00\x00\x02\x01") 211 | exg1_gain = 4 212 | exg_reg2 = ExGRegister(b"\x03\x80\x10\x20\x20\x00\x00\x00\x02\x01") 213 | exg2_gain = 2 214 | 215 | chip_gain = { 216 | 0: exg1_gain, 217 | 1: exg2_gain, 218 | } 219 | 220 | samples = { 221 | EChannelType.EXG_ADS1292R_1_CH1_24BIT: np.random.randn(1000), 222 | EChannelType.EXG_ADS1292R_2_CH2_24BIT: np.random.randn(1000), 223 | EChannelType.EXG_ADS1292R_1_CH1_16BIT: np.random.randn(1000), 224 | EChannelType.EXG_ADS1292R_2_CH2_16BIT: np.random.randn(1000), 225 | } 226 | 227 | samples_w_ts = {**samples, EChannelType.TIMESTAMP: np.arange(1000)} 228 | 229 | m_br = Mock(spec=ShimmerBinaryReader) 230 | m_br.get_exg_reg.side_effect = lambda x: exg_reg1 if x == 0 else exg_reg2 231 | m_br.read_data.side_effect = lambda: (dict(samples_w_ts), ((), ())) 232 | type(m_br).sample_rate = PropertyMock(return_value=1) 233 | type(m_br).enabled_sensors = PropertyMock(return_value=[]) 234 | type(m_br).has_sync = PropertyMock(return_value=False) 235 | type(m_br).has_global_clock = PropertyMock(return_value=False) 236 | type(m_br).start_timestamp = PropertyMock(return_value=0) 237 | type(m_br).exg_reg1 = PropertyMock(return_value=exg_reg1) 238 | type(m_br).exg_reg2 = PropertyMock(return_value=exg_reg2) 239 | 240 | reader = ShimmerReader(bin_reader=m_br, post_process=False) 241 | reader.load_file_data() 242 | for ch_type in samples: 243 | np.testing.assert_equal(samples[ch_type], reader[ch_type]) 244 | 245 | reader = ShimmerReader(bin_reader=m_br, post_process=True) 246 | reader.load_file_data() 247 | 248 | for ch in samples: 249 | bit = 16 if "16" in ch.name else 24 250 | gain = chip_gain[get_exg_ch(ch)[0]] 251 | expected = (samples[ch] - 0) * 2.420 / (2 ** (bit - 1) - 1) / gain 252 | actual = reader[ch] 253 | np.testing.assert_almost_equal(actual, expected) 254 | 255 | # noinspection PyMethodMayBeStatic 256 | def test_compare_exg_processing_to_consensys(self): 257 | bin_path, uncal_path, cal_path = get_ecg_sample() 258 | 259 | def verify(bin_file_path, expected, post_process): 260 | with open(bin_file_path, "rb") as f: 261 | reader = ShimmerReader(f, post_process=post_process, sync=False) 262 | reader.load_file_data() 263 | 264 | actual = reader[EChannelType.EXG_ADS1292R_1_CH1_24BIT] 265 | np.testing.assert_almost_equal(actual, expected[1]) 266 | 267 | actual = reader[EChannelType.EXG_ADS1292R_1_CH2_24BIT] 268 | np.testing.assert_almost_equal(actual, expected[2]) 269 | 270 | expected_uncal = np.loadtxt( 271 | uncal_path, delimiter="\t", skiprows=3, usecols=(0, 1, 2) 272 | ).T 273 | expected_cal = ( 274 | np.loadtxt(cal_path, delimiter="\t", skiprows=3, usecols=(0, 1, 2)).T 275 | / 1000.0 276 | ) # Volt 277 | 278 | verify(bin_path, expected_uncal, post_process=False) 279 | verify(bin_path, expected_cal, post_process=True) 280 | 281 | # noinspection PyMethodMayBeStatic 282 | def test_compare_triaxcal_to_consensys(self): 283 | bin_path, uncal_path, cal_path = get_triaxcal_sample() 284 | 285 | consensys_csv = pd.read_csv( 286 | cal_path, sep=",", skiprows=(0, 2), usecols=list(range(14)) 287 | ) 288 | col_mapping = { 289 | EChannelType.ACCEL_LN_X: "Shimmer_952D_Accel_LN_X_CAL", 290 | EChannelType.ACCEL_LN_Y: "Shimmer_952D_Accel_LN_Y_CAL", 291 | EChannelType.ACCEL_LN_Z: "Shimmer_952D_Accel_LN_Z_CAL", 292 | EChannelType.ACCEL_LSM303DLHC_X: "Shimmer_952D_Accel_WR_X_CAL", 293 | EChannelType.ACCEL_LSM303DLHC_Y: "Shimmer_952D_Accel_WR_Y_CAL", 294 | EChannelType.ACCEL_LSM303DLHC_Z: "Shimmer_952D_Accel_WR_Z_CAL", 295 | EChannelType.GYRO_MPU9150_X: "Shimmer_952D_Gyro_X_CAL", 296 | EChannelType.GYRO_MPU9150_Y: "Shimmer_952D_Gyro_Y_CAL", 297 | EChannelType.GYRO_MPU9150_Z: "Shimmer_952D_Gyro_Z_CAL", 298 | EChannelType.MAG_LSM303DLHC_X: "Shimmer_952D_Mag_X_CAL", 299 | EChannelType.MAG_LSM303DLHC_Y: "Shimmer_952D_Mag_Y_CAL", 300 | EChannelType.MAG_LSM303DLHC_Z: "Shimmer_952D_Mag_Z_CAL", 301 | } 302 | 303 | with open(bin_path, "rb") as f: 304 | reader = ShimmerReader(f) 305 | reader.load_file_data() 306 | 307 | for rdr_col, csv_col in col_mapping.items(): 308 | rdr_channel = reader[rdr_col] 309 | csv_channel = consensys_csv[csv_col] 310 | np.testing.assert_almost_equal(rdr_channel, csv_channel.to_numpy()) 311 | 312 | 313 | class SignalPostProcessorTest(TestCase): 314 | 315 | # noinspection PyTypeChecker 316 | def test_single_channel_processor(self): 317 | class TestProcessor(SingleChannelProcessor): 318 | 319 | def __init__(self, channels: list[EChannelType] = None): 320 | super().__init__(channels) 321 | 322 | self._seen = [] 323 | 324 | def process_channel( 325 | self, ch_type: EChannelType, y: np.ndarray, reader: ShimmerBinaryReader 326 | ) -> np.ndarray: 327 | self._seen.append(ch_type) 328 | return y 329 | 330 | @property 331 | def seen(self) -> set[EChannelType]: 332 | return set(self._seen) 333 | 334 | ch_data = { 335 | EChannelType.TIMESTAMP: np.random.randn(10), 336 | EChannelType.VBATT: np.random.randn(10), 337 | EChannelType.INTERNAL_ADC_13: np.random.randn(10), 338 | EChannelType.ACCEL_LN_X: np.random.randn(10), 339 | } 340 | ch_types = set(ch_data.keys()) 341 | 342 | proc = TestProcessor() 343 | proc.process(ch_data, None) 344 | self.assertEqual(proc.seen, ch_types) 345 | 346 | proc = TestProcessor([EChannelType.VBATT]) 347 | proc.process(ch_data, None) 348 | self.assertEqual(proc.seen, {EChannelType.VBATT}) 349 | 350 | proc = TestProcessor([EChannelType.VBATT, EChannelType.ACCEL_LN_Y]) 351 | proc.process(ch_data, None) 352 | self.assertEqual(proc.seen, {EChannelType.VBATT}) 353 | 354 | proc = TestProcessor([EChannelType.VBATT, EChannelType.ACCEL_LN_X]) 355 | proc.process(ch_data, None) 356 | self.assertEqual(proc.seen, {EChannelType.VBATT, EChannelType.ACCEL_LN_X}) 357 | 358 | # noinspection PyMethodMayBeStatic 359 | def test_ppg_processor(self): 360 | ppg_data = np.random.randn(10) 361 | ch_data = { 362 | EChannelType.TIMESTAMP: np.random.randn(10), 363 | EChannelType.VBATT: np.random.randn(10), 364 | EChannelType.INTERNAL_ADC_13: ppg_data, 365 | EChannelType.ACCEL_LN_X: np.random.randn(10), 366 | } 367 | 368 | proc = PPGProcessor() 369 | # noinspection PyTypeChecker 370 | output = proc.process(ch_data, None) 371 | 372 | for ch, y in output.items(): 373 | if ch != EChannelType.INTERNAL_ADC_13: 374 | np.testing.assert_equal(y, ch_data[ch]) 375 | else: 376 | np.testing.assert_equal(y, ppg_data / 1000.0) 377 | 378 | # noinspection PyMethodMayBeStatic 379 | def test_triaxcal_processor(self): 380 | o, g, a = np.array([1, 2, 3]), np.diag([4, 5, 6]), np.diag([7, 8, 9]) 381 | params = {ESensorGroup.ACCEL_LN: (o, g, a)} 382 | 383 | ch_types = get_enabled_channels(list(params.keys())) 384 | 385 | data_arr = np.random.randn(3, 100) 386 | data_dict = {c: data_arr[i] for i, c in enumerate(ch_types)} 387 | 388 | mock_reader = Mock(spec=ShimmerBinaryReader) 389 | mock_reader.get_triaxcal_params.side_effect = lambda x: params[x] 390 | type(mock_reader).enabled_sensors = PropertyMock( 391 | return_value=list(params.keys()) 392 | ) 393 | 394 | proc = TriAxCalProcessor() 395 | actual_dict = proc.process(data_dict, mock_reader) 396 | actual_arr = np.stack([actual_dict[c] for c in ch_types]) 397 | 398 | k = np.matmul(np.linalg.inv(a), np.linalg.inv(g)) 399 | exp_arr = np.matmul(k, data_arr - o[..., None]) 400 | 401 | np.testing.assert_almost_equal(actual_arr, exp_arr) 402 | -------------------------------------------------------------------------------- /test/reader/test_util.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from unittest import TestCase 19 | 20 | import numpy as np 21 | 22 | from pyshimmer.reader.shimmer_reader import unwrap_device_timestamps, fit_linear_1d 23 | 24 | 25 | # noinspection PyMethodMayBeStatic 26 | class UtilTest(TestCase): 27 | 28 | def test_unwrap_device_timestamps(self): 29 | ts_wrapped = np.array([0, 1, 2, 2**24 - 1, 0, 2**24]) 30 | expected = np.array([0, 1, 2, 2**24 - 1, 2**24, 2 * 2**24]) 31 | actual = unwrap_device_timestamps(ts_wrapped) 32 | np.testing.assert_equal(actual, expected) 33 | 34 | ts_wrapped = np.array([0, 10, 20, 30, 5, 15, 25, 35]) 35 | expected = np.array( 36 | [0, 10, 20, 30, 5 + 2**24, 15 + 2**24, 25 + 2**24, 35 + 2**24] 37 | ) 38 | actual = unwrap_device_timestamps(ts_wrapped) 39 | np.testing.assert_equal(actual, expected) 40 | 41 | def test_fit_linear_1d(self): 42 | x = np.array([0, 1]) 43 | y = np.array([0, 10]) 44 | xi = np.array([0, 0.25, 0.5, 0.75, 1]) 45 | 46 | yi_expected = np.array([0, 2.5, 5, 7.5, 10]) 47 | yi_actual = fit_linear_1d(x, y, xi) 48 | np.testing.assert_almost_equal(yi_actual, yi_expected) 49 | 50 | xi = 0.1 51 | yi_expected = 1.0 52 | yi_actual = fit_linear_1d(x, y, xi) 53 | np.testing.assert_almost_equal(yi_actual, yi_expected) 54 | -------------------------------------------------------------------------------- /test/test_serial_base.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from io import BytesIO 19 | from unittest import TestCase 20 | 21 | from pyshimmer.serial_base import SerialBase, BufferedReader, ReadAbort 22 | from pyshimmer.test_util import MockSerial 23 | 24 | 25 | class BufferedReaderTest(TestCase): 26 | 27 | def test_read(self): 28 | stream = BytesIO(b"thisisatest") 29 | 30 | # noinspection PyTypeChecker 31 | reader = BufferedReader(stream) 32 | 33 | r = reader.read(0) 34 | self.assertEqual(r, b"") 35 | 36 | r = reader.read(1) 37 | self.assertEqual(r, b"t") 38 | 39 | r = reader.read(3) 40 | self.assertEqual(r, b"his") 41 | 42 | r = reader.read(0) 43 | self.assertEqual(r, b"") 44 | 45 | r = reader.read(7) 46 | self.assertEqual(r, b"isatest") 47 | 48 | self.assertRaises(ReadAbort, reader.read, 1) 49 | 50 | def test_peek(self): 51 | stream = BytesIO(b"thisisatest") 52 | 53 | # noinspection PyTypeChecker 54 | reader = BufferedReader(stream) 55 | 56 | r = reader.peek(0) 57 | self.assertEqual(r, b"") 58 | 59 | for i in range(2): 60 | r = reader.peek(4) 61 | self.assertEqual(r, b"this") 62 | 63 | r = reader.peek(11) 64 | self.assertEqual(r, b"thisisatest") 65 | 66 | self.assertRaises(ReadAbort, reader.peek, 20) 67 | 68 | def test_peek_read(self): 69 | stream = BytesIO(b"thisisatest") 70 | 71 | # noinspection PyTypeChecker 72 | reader = BufferedReader(stream) 73 | 74 | r = reader.peek(4) 75 | self.assertEqual(r, b"this") 76 | 77 | r = reader.read(2) 78 | self.assertEqual(r, b"th") 79 | 80 | r = reader.peek(6) 81 | self.assertEqual(r, b"isisat") 82 | 83 | r = reader.read(3) 84 | self.assertEqual(r, b"isi") 85 | 86 | def test_reset(self): 87 | stream = BytesIO(b"thisisatest") 88 | 89 | # noinspection PyTypeChecker 90 | reader = BufferedReader(stream) 91 | 92 | r = reader.read(2) 93 | self.assertEqual(r, b"th") 94 | 95 | r = reader.peek(4) 96 | self.assertEqual(r, b"isis") 97 | 98 | reader.reset() 99 | r = reader.read(3) 100 | self.assertEqual(r, b"ate") 101 | 102 | reader.reset() 103 | r = reader.peek(2) 104 | self.assertEqual(r, b"st") 105 | 106 | 107 | class SerialBaseTest(TestCase): 108 | 109 | @staticmethod 110 | def create_sot() -> tuple[MockSerial, SerialBase]: 111 | mock = MockSerial() 112 | 113 | # noinspection PyTypeChecker 114 | sot = SerialBase(mock) 115 | 116 | return mock, sot 117 | 118 | def test_flush_input_buf(self): 119 | mock, sot = self.create_sot() 120 | 121 | mock.test_put_read_data(b"test") 122 | r = sot.read(2) 123 | self.assertEqual(r, b"te") 124 | 125 | sot.flush_input_buffer() 126 | self.assertTrue(mock.test_input_flushed) 127 | self.assertRaises(ReadAbort, sot.read, 1) 128 | 129 | def test_write(self): 130 | mock, sot = self.create_sot() 131 | 132 | i = sot.write(b"this") 133 | self.assertEqual(i, 4) 134 | i = sot.write(b"") 135 | self.assertEqual(i, 0) 136 | i = sot.write(b"is") 137 | self.assertEqual(i, 2) 138 | 139 | r = mock.test_get_write_data() 140 | self.assertEqual(r, b"thisis") 141 | 142 | i = sot.write_byte(10) 143 | self.assertEqual(i, 1) 144 | i = sot.write_byte(16) 145 | self.assertEqual(i, 1) 146 | 147 | r = mock.test_get_write_data() 148 | self.assertEqual(r, b"\x0a\x10") 149 | 150 | i = sot.write_packed(". 16 | from __future__ import annotations 17 | 18 | from io import BytesIO 19 | from unittest import TestCase 20 | from unittest.mock import Mock 21 | 22 | import numpy as np 23 | 24 | from pyshimmer.util import PeekQueue 25 | from pyshimmer.util import ( 26 | bit_is_set, 27 | raise_to_next_pow, 28 | flatten_list, 29 | fmt_hex, 30 | unpack, 31 | unwrap, 32 | calibrate_u12_adc_value, 33 | battery_voltage_to_percent, 34 | FileIOBase, 35 | ) 36 | 37 | 38 | class UtilTest(TestCase): 39 | 40 | def test_bit_is_set(self): 41 | r = bit_is_set(0x10, 0x01) 42 | self.assertEqual(r, False) 43 | 44 | r = bit_is_set(0x10, 0x10) 45 | self.assertEqual(r, True) 46 | 47 | r = bit_is_set(0x05, 0x01) 48 | self.assertEqual(r, True) 49 | 50 | r = bit_is_set(0x05, 0x02) 51 | self.assertEqual(r, False) 52 | 53 | r = bit_is_set(0x05, 0x04) 54 | self.assertEqual(r, True) 55 | 56 | def test_raise_to_next_pow(self): 57 | r = raise_to_next_pow(0) 58 | self.assertEqual(r, 1) 59 | 60 | r = raise_to_next_pow(1) 61 | self.assertEqual(r, 1) 62 | 63 | r = raise_to_next_pow(2) 64 | self.assertEqual(r, 2) 65 | 66 | r = raise_to_next_pow(3) 67 | self.assertEqual(r, 4) 68 | 69 | r = raise_to_next_pow(4) 70 | self.assertEqual(r, 4) 71 | 72 | r = raise_to_next_pow(6) 73 | self.assertEqual(r, 8) 74 | 75 | r = raise_to_next_pow(14) 76 | self.assertEqual(r, 16) 77 | 78 | def test_flatten_list(self): 79 | r = flatten_list([[10], [20]]) 80 | self.assertEqual(r, [10, 20]) 81 | 82 | r = flatten_list(((10,), (20,))) 83 | self.assertEqual(r, [10, 20]) 84 | 85 | r = flatten_list([[10]]) 86 | self.assertEqual(r, [10]) 87 | 88 | def test_fmt_hex(self): 89 | r = fmt_hex(b"\x01") 90 | self.assertEqual(r, "01") 91 | 92 | r = fmt_hex(b"\x01\x02") 93 | self.assertEqual(r, "01 02") 94 | 95 | def test_unpack(self): 96 | r = unpack([10]) 97 | 98 | self.assertEqual(r, 10) 99 | 100 | r = unpack([10, 20]) 101 | self.assertEqual(r, [10, 20]) 102 | 103 | r = unpack([]) 104 | self.assertEqual(r, []) 105 | 106 | r = unpack(()) 107 | self.assertEqual(r, ()) 108 | 109 | r = unpack((10,)) 110 | self.assertEqual(r, 10) 111 | 112 | r = unpack((10, 20)) 113 | self.assertEqual(r, (10, 20)) 114 | 115 | # noinspection PyMethodMayBeStatic 116 | def test_unwrap(self): 117 | shift = 10 118 | x = np.array([0, 1, 5, 8, 0, 2, 5, 10, 3, 7, 9]) 119 | e = np.array([0, 1, 5, 8, 10, 12, 15, 20, 23, 27, 29]) 120 | 121 | r = unwrap(x, shift) 122 | np.testing.assert_equal(r, e) 123 | 124 | x = np.array([0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2]) 125 | e = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) 126 | 127 | r = unwrap(x, 4) 128 | np.testing.assert_equal(r, e) 129 | 130 | e = np.arange(0, 2000, 45) 131 | x = e % 250 132 | 133 | r = unwrap(x, 250) 134 | np.testing.assert_equal(r, e) 135 | 136 | e = np.arange(0, 4 * (2**24), 65) 137 | x = e % (2**24) 138 | 139 | r = unwrap(x, 2**24) 140 | np.testing.assert_equal(r, e) 141 | 142 | def test_calibrate_u12_adc_value(self): 143 | uncalibratedData = 2863 144 | offset = 0 145 | vRefP = 3.0 146 | gain = 1.0 147 | 148 | actual = calibrate_u12_adc_value(uncalibratedData, offset, vRefP, gain) 149 | 150 | desired = 2.0974358974358975 151 | np.testing.assert_almost_equal(actual, desired) 152 | 153 | def test_battery_voltage_to_percent(self): 154 | voltage = 3.9078 155 | desired = 72.8 156 | 157 | actual = battery_voltage_to_percent(voltage) 158 | np.testing.assert_equal(actual, desired) 159 | 160 | def test_peek_queue(self): 161 | queue = PeekQueue() 162 | 163 | queue.put(1) 164 | queue.put(2) 165 | queue.put(3) 166 | 167 | self.assertEqual(queue.peek(), 1) 168 | queue.get() 169 | 170 | self.assertEqual(queue.peek(), 2) 171 | queue.get() 172 | 173 | self.assertEqual(queue.peek(), 3) 174 | queue.get() 175 | 176 | self.assertEqual(queue.peek(), None) 177 | 178 | def test_file_io_base(self): 179 | input_bin = bytes(range(255)) 180 | io_obj = BytesIO(input_bin) 181 | 182 | sut = FileIOBase(io_obj) 183 | self.assertEqual(sut._tell(), 0) 184 | 185 | sut._seek(10) 186 | self.assertEqual(sut._tell(), 10) 187 | 188 | sut._seek_relative(-2) 189 | self.assertEqual(sut._tell(), 8) 190 | 191 | r = sut._read(2) 192 | self.assertEqual(r, b"\x08\x09") 193 | 194 | r = sut._read_packed(". 16 | -------------------------------------------------------------------------------- /test/uart/test_dock_api.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from unittest import TestCase 4 | 5 | from pyshimmer import EFirmwareType, ShimmerDock 6 | from pyshimmer.test_util import MockSerial 7 | 8 | 9 | class DockAPITest(TestCase): 10 | 11 | @staticmethod 12 | def create_sot(flush: bool = False) -> tuple[ShimmerDock, MockSerial]: 13 | mock = MockSerial() 14 | # noinspection PyTypeChecker 15 | dock = ShimmerDock(mock, flush_before_req=flush) 16 | 17 | return dock, mock 18 | 19 | def test_context_manager(self): 20 | dock, mock = self.create_sot() 21 | 22 | self.assertFalse(mock.test_closed) 23 | 24 | with dock: 25 | pass 26 | 27 | self.assertTrue(mock.test_closed) 28 | 29 | def test_unknown_start_char(self): 30 | dock, mock = self.create_sot() 31 | 32 | mock.test_put_read_data(b"\x25") 33 | self.assertRaises(IOError, dock.get_firmware_version) 34 | 35 | def test_bad_arg_response(self): 36 | dock, mock = self.create_sot() 37 | 38 | mock.test_put_read_data(b"\x24\xfd") 39 | self.assertRaises(IOError, dock.get_firmware_version) 40 | 41 | def test_bad_cmd_response(self): 42 | dock, mock = self.create_sot() 43 | 44 | mock.test_put_read_data(b"\x24\xfc") 45 | self.assertRaises(IOError, dock.get_firmware_version) 46 | 47 | def test_bad_crc_response(self): 48 | dock, mock = self.create_sot() 49 | 50 | mock.test_put_read_data(b"\x24\xfe") 51 | self.assertRaises(IOError, dock.get_firmware_version) 52 | 53 | def test_unexpected_cmd_response(self): 54 | dock, mock = self.create_sot() 55 | 56 | mock.test_put_read_data(b"\x24\x03") 57 | self.assertRaises(IOError, dock.get_firmware_version) 58 | 59 | def test_unexpected_component(self): 60 | dock, mock = self.create_sot() 61 | 62 | mock.test_put_read_data(b"\x24\x02\x02\x02\x00\x98z") 63 | self.assertRaises(IOError, dock.get_firmware_version) 64 | 65 | def test_unexpected_property(self): 66 | dock, mock = self.create_sot() 67 | 68 | mock.test_put_read_data(b"\x24\x02\x02\x01\x02\xaaE") 69 | self.assertRaises(IOError, dock.get_firmware_version) 70 | 71 | def test_get_mac_address(self): 72 | dock, mock = self.create_sot() 73 | 74 | mock.test_put_read_data(b"\x24\x02\x08\x01\x02\x01\x02\x03\x04\x05\x06N\x87") 75 | r = dock.get_mac_address() 76 | 77 | self.assertEqual(r, (0x01, 0x02, 0x03, 0x04, 0x05, 0x06)) 78 | self.assertEqual(mock.test_get_write_data(), b"\x24\x03\x02\x01\x02\xfb\xef") 79 | 80 | def test_get_firmware_version(self): 81 | dock, mock = self.create_sot() 82 | 83 | mock.test_put_read_data( 84 | b"\x24\x02\x09\x01\x03\x03\x03\x00\x00\x00\x0b\x00\x14\x33" 85 | ) 86 | hw_ver, fw_type, major, minor, patch = dock.get_firmware_version() 87 | 88 | self.assertEqual(mock.test_get_write_data(), b"\x24\x03\x02\x01\x03\xca\xdc") 89 | 90 | self.assertEqual(hw_ver, 3) 91 | self.assertEqual(fw_type, EFirmwareType.LogAndStream) 92 | self.assertEqual(major, 0) 93 | self.assertEqual(minor, 11) 94 | self.assertEqual(patch, 0) 95 | 96 | def test_set_rtc(self): 97 | dock, mock = self.create_sot() 98 | 99 | mock.test_put_read_data(b"\x24\xff\xd9\xb2") 100 | dock.set_rtc(1.0) 101 | 102 | wd = mock.test_get_write_data() 103 | self.assertEqual( 104 | wd, b"\x24\x01\x0a\x01\x04\x00\x80\x00\x00\x00\x00\x00\x00\x1c\xd2" 105 | ) 106 | 107 | def test_get_rtc(self): 108 | dock, mock = self.create_sot() 109 | 110 | mock.test_put_read_data( 111 | b"\x24\x02\x0a\x01\x05\x9d\x3d\x0d\x00\x00\x00\x00\x00\xb0\xc7" 112 | ) 113 | r = dock.get_rtc() 114 | self.assertAlmostEqual(r, 26.481353759765625) 115 | 116 | def test_get_config_rtc(self): 117 | dock, mock = self.create_sot() 118 | 119 | mock.test_put_read_data( 120 | b"\x24\x02\x0a\x01\x04\x00\x00\x15\x00\x00\x00\x00\x00\xe4\xae" 121 | ) 122 | r = dock.get_config_rtc() 123 | self.assertEqual(r, 42.0) 124 | 125 | wd = mock.test_get_write_data() 126 | self.assertEqual(wd, b"\x24\x03\x02\x01\x04\x5d\x45") 127 | 128 | def test_get_exg_register(self): 129 | dock, mock = self.create_sot() 130 | 131 | # Due to the firmware bug, we first need to emulate the call to set the 132 | # DAUGHTER_CARD CARD_ID 133 | mock.test_put_read_data(b"\x24\x02\x02\x03\x02\xca\x2b") 134 | exp_send_data1 = b"\x24\x03\x05\x03\x02\x00\x00\x00\x3a\xd2" 135 | 136 | # Then the actual call to retrieve the infomem data 137 | mock.test_put_read_data( 138 | b"\x24\x02\x0c\x01\x06\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01\xff\x40" 139 | ) 140 | exp_send_data2 = b"\x24\x03\x05\x01\x06\x0a\x0a\x00\x42\x74" 141 | 142 | r = dock.get_exg_register(0) 143 | 144 | wd = mock.test_get_write_data() 145 | self.assertEqual(wd, exp_send_data1 + exp_send_data2) 146 | 147 | self.assertEqual(r.binary, b"\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01") 148 | 149 | def test_get_exg_register_fail(self): 150 | dock, mock = self.create_sot() 151 | 152 | self.assertRaises(ValueError, dock.get_exg_register, -1) 153 | -------------------------------------------------------------------------------- /test/uart/test_dock_serial.py: -------------------------------------------------------------------------------- 1 | # pyshimmer - API for Shimmer sensor devices 2 | # Copyright (C) 2020 Lukas Magel 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | 18 | from unittest import TestCase 19 | 20 | from pyshimmer.test_util import MockSerial 21 | from pyshimmer.uart.dock_serial import DockSerial, generate_crc 22 | 23 | 24 | class CRCCalculationTest(TestCase): 25 | 26 | def test_generate_crc_uneven(self): 27 | crc_init = 0xB0CA 28 | msg = b"\x24\x03\x02\x01\x03" 29 | exp_crc = b"\xca\xdc" 30 | 31 | act_crc = generate_crc(msg, crc_init) 32 | self.assertEqual(act_crc, exp_crc) 33 | 34 | def test_generate_crc_even(self): 35 | crc_init = 0xB0CA 36 | msg = b"\x24\x03\x02\x01" 37 | exp_crc = b"\x4b\xc2" 38 | 39 | act_crc = generate_crc(msg, crc_init) 40 | print(act_crc) 41 | self.assertEqual(act_crc, exp_crc) 42 | 43 | 44 | class DockSerialTest(TestCase): 45 | 46 | @staticmethod 47 | def create_sot(crc_init: int = 10) -> tuple[DockSerial, MockSerial]: 48 | mock = MockSerial() 49 | # noinspection PyTypeChecker 50 | serial = DockSerial(mock, crc_init=crc_init) 51 | return serial, mock 52 | 53 | def test_read(self): 54 | crc_init = 42 55 | serial, mock = self.create_sot(crc_init) 56 | 57 | data_no_verify = b"abcd" 58 | data = b"\x01\x02\x03\x04" 59 | crc = generate_crc(data, crc_init) 60 | mock.test_put_read_data(data_no_verify + data + crc) 61 | 62 | r = serial.read(4) 63 | self.assertEqual(data_no_verify, r) 64 | 65 | serial.start_read_crc_verify() 66 | r = serial.read(4) 67 | serial.end_read_crc_verify() 68 | 69 | self.assertEqual(data, r) 70 | self.assertEqual(mock.test_get_remaining_read_data(), b"") 71 | 72 | mock.reset_input_buffer() 73 | mock.test_put_read_data(data + b"\x00\x01") 74 | 75 | serial.start_read_crc_verify() 76 | r = serial.read(4) 77 | self.assertEqual(r, data) 78 | self.assertRaises(IOError, serial.end_read_crc_verify) 79 | 80 | def test_write(self): 81 | crc_init = 42 82 | serial, mock = self.create_sot(crc_init) 83 | 84 | data_no_verify = b"1234" 85 | data = b"another test" 86 | crc = generate_crc(data, crc_init) 87 | 88 | serial.write(data_no_verify) 89 | 90 | serial.start_write_crc() 91 | serial.write(data) 92 | serial.end_write_crc() 93 | 94 | r = mock.test_get_write_data() 95 | self.assertEqual(len(r), 18) 96 | self.assertEqual(r[:4], b"1234") 97 | self.assertEqual(r[4:16], b"another test") 98 | self.assertEqual(r[-2:], crc) 99 | --------------------------------------------------------------------------------