Skip to content

Commit 4a208b1

Browse files
Retrieve stored calibration data (#17)
* Added new commands * WIP * wip * - Moved examples to examples folder - Tests added for getAllCalibration class - Changes reformatted using PEP8
1 parent 4085bc7 commit 4a208b1

File tree

8 files changed

+328
-15
lines changed

8 files changed

+328
-15
lines changed

examples/bt_api_example.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import time
2+
3+
from serial import Serial
4+
5+
from pyshimmer import ShimmerBluetooth, DEFAULT_BAUDRATE, DataPacket
6+
7+
8+
def stream_cb(pkt: DataPacket) -> None:
9+
print(f'Received new data packet: ')
10+
for chan in pkt.channels:
11+
print(f'channel: ' + str(chan))
12+
print(f'value: ' + str(pkt[chan]))
13+
print('')
14+
15+
def main(args=None):
16+
serial = Serial('/dev/rfcomm42', DEFAULT_BAUDRATE)
17+
shim_dev = ShimmerBluetooth(serial)
18+
19+
shim_dev.initialize()
20+
21+
dev_name = shim_dev.get_device_name()
22+
print(f'My name is: {dev_name}')
23+
24+
info = shim_dev.get_firmware_version()
25+
print("- firmware: [" + str(info[0]) + "]")
26+
print("- version: [" + str(info[1].major) + "." + str(info[1].minor) + "." + str(info[1].rel) + "]")
27+
28+
shim_dev.add_stream_callback(stream_cb)
29+
30+
shim_dev.start_streaming()
31+
time.sleep(5.0)
32+
shim_dev.stop_streaming()
33+
34+
shim_dev.shutdown()
35+
36+
37+
if __name__ == '__main__':
38+
main()

examples/dock_example.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from serial import Serial
2+
3+
from pyshimmer import ShimmerDock, DEFAULT_BAUDRATE, fmt_hex
4+
5+
6+
def main(args=None):
7+
serial = Serial('/dev/ttyECGdev', DEFAULT_BAUDRATE)
8+
9+
print(f'Connecting docker')
10+
shim_dock = ShimmerDock(serial)
11+
12+
mac = shim_dock.get_mac_address()
13+
print(f'Device MAC: {fmt_hex(mac)}')
14+
15+
shim_dock.close()
16+
17+
18+
if __name__ == '__main__':
19+
main()

pyshimmer/bluetooth/bt_api.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
GetFirmwareVersionCommand, InquiryCommand, StartStreamingCommand, StopStreamingCommand, DataPacket, \
2525
GetEXGRegsCommand, SetEXGRegsCommand, StartLoggingCommand, StopLoggingCommand, GetExperimentIDCommand, \
2626
SetExperimentIDCommand, GetDeviceNameCommand, SetDeviceNameCommand, DummyCommand, GetBatteryCommand, \
27-
SetSamplingRateCommand, SetSensorsCommand, SetStatusAckCommand
27+
SetSamplingRateCommand, SetSensorsCommand, SetStatusAckCommand, AllCalibration, GetAllCalibrationCommand
2828
from pyshimmer.bluetooth.bt_const import ACK_COMMAND_PROCESSED, DATA_PACKET, FULL_STATUS_RESPONSE, INSTREAM_CMD_RESPONSE
2929
from pyshimmer.bluetooth.bt_serial import BluetoothSerial
3030
from pyshimmer.dev.channels import ChDataTypeAssignment, ChannelDataType, EChannelType, ESensorGroup
@@ -475,6 +475,12 @@ def get_exg_register(self, chip_id: int) -> ExGRegister:
475475
"""
476476
return self._process_and_wait(GetEXGRegsCommand(chip_id))
477477

478+
def get_all_calibration(self) -> AllCalibration:
479+
"""Gets all calibration data from sensor
480+
:return: An AllCalibration object that presents the calibration contents in an easily processable manner
481+
"""
482+
return self._process_and_wait(GetAllCalibrationCommand())
483+
478484
def set_exg_register(self, chip_id: int, offset: int, data: bytes) -> None:
479485
"""Configure part of the memory of the ExG registers
480486

pyshimmer/bluetooth/bt_commands.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from pyshimmer.dev.base import dr2sr, sr2dr, sec2ticks, ticks2sec
2424
from pyshimmer.dev.channels import ChannelDataType, EChannelType, ESensorGroup, serialize_sensorlist
2525
from pyshimmer.dev.exg import ExGRegister
26+
from pyshimmer.dev.calibration import AllCalibration
2627
from pyshimmer.dev.fw_version import get_firmware_type
2728

2829
from pyshimmer.util import bit_is_set, resp_code_to_bytes, calibrate_u12_adc_value, battery_voltage_to_percent
@@ -221,7 +222,7 @@ def send(self, ser: BluetoothSerial) -> None:
221222

222223
def receive(self, ser: BluetoothSerial) -> any:
223224
batt = ser.read_response(self.get_response_code(), arg_format='BBB')
224-
# Calculation see:
225+
# Calculation see:
225226
# http://shimmersensing.com/wp-content/docs/support/documentation/LogAndStream_for_Shimmer3_Firmware_User_Manual_rev0.11a.pdf (Page 17)
226227
# https://shimmersensing.com/wp-content/docs/support/documentation/Shimmer_User_Manual_rev3p.pdf (Page 53)
227228
raw_values = batt[1] * 256 + batt[0]
@@ -337,11 +338,41 @@ def send(self, ser: BluetoothSerial) -> None:
337338
ser.write_command(GET_FW_VERSION_COMMAND)
338339

339340
def receive(self, ser: BluetoothSerial) -> any:
340-
fw_type_bin, major, minor, rel = ser.read_response(FW_VERSION_RESPONSE, arg_format='<HHBB')
341+
fw_type_bin, major, minor, rel = ser.read_response(
342+
FW_VERSION_RESPONSE, arg_format='<HHBB')
341343
fw_type = get_firmware_type(fw_type_bin)
342344
return fw_type, major, minor, rel
343345

344346

347+
class GetAllCalibrationCommand(ResponseCommand):
348+
""" Returns all the stored calibration values (84 bytes) in the following order:
349+
350+
ESensorGroup.ACCEL_LN (21 bytes)
351+
ESensorGroup.GYRO (21 bytes)
352+
ESensorGroup.MAG (21 bytes)
353+
ESensorGroup.ACCEL_WR (21 bytes)
354+
355+
The breakdown of the kinematic (accel x 2, gyro and mag) calibration values is as follows:
356+
[bytes 0- 5] offset bias values: 3 (x,y,z) 16-bit signed integers (big endian).
357+
[bytes 6-11] sensitivity values: 3 (x,y,z) 16-bit signed integers (big endian).
358+
[bytes 12-20] alignment matrix: 9 values 8-bit signed integers.
359+
"""
360+
361+
def __init__(self):
362+
super().__init__(ALL_CALIBRATION_RESPONSE)
363+
364+
self._offset = 0x0
365+
self._rlen = 0x54 # 84 bytes
366+
367+
def send(self, ser: BluetoothSerial) -> None:
368+
ser.write_command(GET_ALL_CALIBRATION_COMMAND)
369+
370+
def receive(self, ser: BluetoothSerial) -> any:
371+
ser.read_response(ALL_CALIBRATION_RESPONSE)
372+
reg_data = ser.read(self._rlen)
373+
return AllCalibration(reg_data)
374+
375+
345376
class InquiryCommand(ResponseCommand):
346377
"""Perform an inquiry to determine the sample rate, buffer size, and active data channels
347378
@@ -360,7 +391,8 @@ def send(self, ser: BluetoothSerial) -> None:
360391
ser.write_command(INQUIRY_COMMAND)
361392

362393
def receive(self, ser: BluetoothSerial) -> any:
363-
sr_val, _, n_ch, buf_size = ser.read_response(INQUIRY_RESPONSE, arg_format='<HIBB')
394+
sr_val, _, n_ch, buf_size = ser.read_response(
395+
INQUIRY_RESPONSE, arg_format='<HIBB')
364396
channel_conf = ser.read(n_ch)
365397

366398
sr = dr2sr(sr_val)
@@ -403,12 +435,14 @@ def __init__(self, chip_id: int):
403435
self._rlen = 0xA
404436

405437
def send(self, ser: BluetoothSerial) -> None:
406-
ser.write_command(GET_EXG_REGS_COMMAND, 'BBB', self._chip, self._offset, self._rlen)
438+
ser.write_command(GET_EXG_REGS_COMMAND, 'BBB',
439+
self._chip, self._offset, self._rlen)
407440

408441
def receive(self, ser: BluetoothSerial) -> any:
409442
rlen = ser.read_response(EXG_REGS_RESPONSE, arg_format='B')
410443
if not rlen == self._rlen:
411-
raise ValueError('Response does not contain required amount of bytes')
444+
raise ValueError(
445+
'Response does not contain required amount of bytes')
412446

413447
reg_data = ser.read(rlen)
414448
return ExGRegister(reg_data)
@@ -429,7 +463,8 @@ def __init__(self, chip_id: int, offset: int, data: bytes):
429463

430464
def send(self, ser: BluetoothSerial) -> None:
431465
dlen = len(self._data)
432-
ser.write_command(SET_EXG_REGS_COMMAND, 'BBB', self._chip, self._offset, dlen)
466+
ser.write_command(SET_EXG_REGS_COMMAND, 'BBB',
467+
self._chip, self._offset, dlen)
433468
ser.write(self._data)
434469

435470

pyshimmer/bluetooth/bt_const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@
8484

8585
ENABLE_STATUS_ACK_COMMAND = 0xA3
8686

87+
GET_ALL_CALIBRATION_COMMAND = 0x2C
88+
ALL_CALIBRATION_RESPONSE = 0x2D
89+
8790
"""
8891
The Bluetooth LogAndStream API assigns a numerical index to each channel type. This dictionary maps each index to the
8992
corresponding channel type.

pyshimmer/dev/calibration.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 <https://www.gnu.org/licenses/>.
16+
17+
import struct
18+
from typing import List
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(
32+
f'All calibration data must have length {self._num_bytes}')
33+
34+
self._reg_bin = reg_bin
35+
36+
def __str__(self) -> str:
37+
def print_sensor(sens_num: int) -> str:
38+
return 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+
obj_str = f''
44+
for i in range(0, self._num_sensors):
45+
obj_str += print_sensor(i)
46+
47+
reg_bin_str = fmt_hex(self._reg_bin)
48+
obj_str += f'Binary: {reg_bin_str}\n'
49+
50+
return obj_str
51+
52+
@property
53+
def binary(self):
54+
return self._reg_bin
55+
56+
def __eq__(self, other: "AllCalibration") -> bool:
57+
return self._reg_bin == other._reg_bin
58+
59+
def _check_sens_num(self, sens_num: int) -> None:
60+
if not 0 <= sens_num < (self._num_sensors):
61+
raise ValueError(f'Sensor num must be 0 to {self._num_sensors-1}')
62+
63+
def get_offset_bias(self, sens_num: int) -> List[int]:
64+
self._check_sens_num(sens_num)
65+
start_offset = sens_num * self._sensor_bytes
66+
end_offset = start_offset + 6
67+
ans = list(struct.unpack(
68+
'>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(
76+
'>hhh', self._reg_bin[start_offset:end_offset]))
77+
return ans
78+
79+
def get_ali_mat(self, sens_num: int) -> List[int]:
80+
self._check_sens_num(sens_num)
81+
start_offset = sens_num * self._sensor_bytes + 12
82+
end_offset = start_offset + 9
83+
ans = list(struct.unpack(
84+
'>bbbbbbbbb', self._reg_bin[start_offset:end_offset]))
85+
return ans

test/bluetooth/test_bt_commands.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
GetFirmwareVersionCommand, InquiryCommand, StartStreamingCommand, StopStreamingCommand, StartLoggingCommand, \
2222
StopLoggingCommand, GetEXGRegsCommand, SetEXGRegsCommand, GetExperimentIDCommand, SetExperimentIDCommand, \
2323
GetDeviceNameCommand, SetDeviceNameCommand, DummyCommand, DataPacket, ResponseCommand, SetStatusAckCommand, \
24-
SetSensorsCommand, SetSamplingRateCommand
24+
SetSensorsCommand, SetSamplingRateCommand, GetAllCalibrationCommand
2525
from pyshimmer.bluetooth.bt_serial import BluetoothSerial
2626
from pyshimmer.dev.channels import ChDataTypeAssignment, EChannelType, ESensorGroup
2727
from pyshimmer.dev.fw_version import EFirmwareType
@@ -95,10 +95,12 @@ def test_set_sampling_rate_command(self):
9595

9696
def test_get_battery_state_command(self):
9797
cmd = GetBatteryCommand(in_percent=True)
98-
self.assert_cmd(cmd, b'\x95', b'\x8a\x94', b'\x8a\x94\x30\x0b\x80', 100)
98+
self.assert_cmd(cmd, b'\x95', b'\x8a\x94',
99+
b'\x8a\x94\x30\x0b\x80', 100)
99100

100101
cmd = GetBatteryCommand(in_percent=False)
101-
self.assert_cmd(cmd, b'\x95', b'\x8a\x94', b'\x8a\x94\x2e\x0b\x80', 4.168246153846154)
102+
self.assert_cmd(cmd, b'\x95', b'\x8a\x94',
103+
b'\x8a\x94\x2e\x0b\x80', 4.168246153846154)
102104

103105
def test_set_sensors_command(self):
104106
sensors = [
@@ -119,7 +121,8 @@ def test_set_config_time_command(self):
119121

120122
def test_get_rtc(self):
121123
cmd = GetRealTimeClockCommand()
122-
r = self.assert_cmd(cmd, b'\x91', b'\x90', b'\x90\x1f\xb1\x93\x09\x00\x00\x00\x00')
124+
r = self.assert_cmd(cmd, b'\x91', b'\x90',
125+
b'\x90\x1f\xb1\x93\x09\x00\x00\x00\x00')
123126
self.assertAlmostEqual(r, 4903.3837585)
124127

125128
def test_set_rtc(self):
@@ -129,19 +132,22 @@ def test_set_rtc(self):
129132
def test_get_status_command(self):
130133
cmd = GetStatusCommand()
131134
expected_result = [True, False, True, False, False, True, False, False]
132-
self.assert_cmd(cmd, b'\x72', b'\x8a\x71', b'\x8a\x71\x25', expected_result)
135+
self.assert_cmd(cmd, b'\x72', b'\x8a\x71',
136+
b'\x8a\x71\x25', expected_result)
133137

134138
def test_get_firmware_version_command(self):
135139
cmd = GetFirmwareVersionCommand()
136-
fw_type, major, minor, patch = self.assert_cmd(cmd, b'\x2e', b'\x2f', b'\x2f\x03\x00\x00\x00\x0b\x00')
140+
fw_type, major, minor, patch = self.assert_cmd(
141+
cmd, b'\x2e', b'\x2f', b'\x2f\x03\x00\x00\x00\x0b\x00')
137142
self.assertEqual(fw_type, EFirmwareType.LogAndStream)
138143
self.assertEqual(major, 0)
139144
self.assertEqual(minor, 11)
140145
self.assertEqual(patch, 0)
141146

142147
def test_inquiry_command(self):
143148
cmd = InquiryCommand()
144-
sr, buf_size, ctypes = self.assert_cmd(cmd, b'\x01', b'\x02', b'\x02\x40\x00\x01\xff\x01\x09\x01\x01\x12')
149+
sr, buf_size, ctypes = self.assert_cmd(
150+
cmd, b'\x01', b'\x02', b'\x02\x40\x00\x01\xff\x01\x09\x01\x01\x12')
145151

146152
self.assertEqual(sr, 512.0)
147153
self.assertEqual(buf_size, 1)
@@ -165,7 +171,8 @@ def test_stop_logging_command(self):
165171

166172
def test_get_exg_register_command(self):
167173
cmd = GetEXGRegsCommand(1)
168-
r = self.assert_cmd(cmd, b'\x63\x01\x00\x0a', b'\x62', b'\x62\x0a\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01')
174+
r = self.assert_cmd(cmd, b'\x63\x01\x00\x0a', b'\x62',
175+
b'\x62\x0a\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01')
169176
self.assertEqual(r.binary, b'\x00\x80\x10\x00\x00\x00\x00\x00\x02\x01')
170177

171178
def test_get_exg_reg_fail(self):
@@ -175,6 +182,11 @@ def test_get_exg_reg_fail(self):
175182
mock.test_put_read_data(b'\x62\x04\x01\x02\x03\x04')
176183
self.assertRaises(ValueError, cmd.receive, serial)
177184

185+
def test_get_allcalibration_command(self):
186+
cmd = GetAllCalibrationCommand()
187+
r = self.assert_cmd(cmd, b'\x2c', b'\x2d', b'\x2d\x08\xcd\x08\xcd\x08\xcd\x00\x5c\x00\x5c\x00\x5c\x00\x9c\x00\x9c\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x00\x19\x96\x19\x96\x19\x96\x00\x9c\x00\x9c\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x87\x06\x87\x06\x87\x00\x9c\x00\x64\x00\x00\x00\x00\x9c')
188+
self.assertEqual(r.binary, b'\x08\xcd\x08\xcd\x08\xcd\x00\x5c\x00\x5c\x00\x5c\x00\x9c\x00\x9c\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x00\x19\x96\x19\x96\x19\x96\x00\x9c\x00\x9c\x00\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x87\x06\x87\x06\x87\x00\x9c\x00\x64\x00\x00\x00\x00\x9c')
189+
178190
def test_set_exg_register_command(self):
179191
cmd = SetEXGRegsCommand(1, 0x02, b'\x10\x00')
180192
self.assert_cmd(cmd, b'\x61\x01\x02\x02\x10\x00')

0 commit comments

Comments
 (0)