From 27094bc33b8ae83d31b1cdffe31ff54915831e64 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Tue, 16 Apr 2024 13:18:39 -0400 Subject: [PATCH 1/5] Refs #79. Added support for AppleWatch - SensorLogger - ProcessedMotion file format v3 --- .gitignore | 1 + python/env/requirements.txt | 14 +-- .../importers/AppleWatchImporter.py | 105 ++++++++++++------ python/resources/ui/StreamWindow.ui | 3 + 4 files changed, 83 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index ea40922..f29f730 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ python-3.10 .DS_Store *.dmg python-3.11 +build diff --git a/python/env/requirements.txt b/python/env/requirements.txt index fcc9c37..c7f5c95 100644 --- a/python/env/requirements.txt +++ b/python/env/requirements.txt @@ -1,8 +1,8 @@ pypiwin32==223; sys_platform == 'win32' -PySide6==6.6.0 -cython==3.0.5 -numpy==1.26.2 -scipy==1.11.4 -sqlalchemy==2.0.23 -alembic==1.12.1 -pyinstaller==5.13.2 +PySide6==6.7.0 +cython==3.0.10 +numpy==1.26.4 +scipy==1.13.0 +sqlalchemy==2.0.29 +alembic==1.13.1 +pyinstaller==6.6.0 diff --git a/python/libopenimu/importers/AppleWatchImporter.py b/python/libopenimu/importers/AppleWatchImporter.py index b643f8c..28ff651 100644 --- a/python/libopenimu/importers/AppleWatchImporter.py +++ b/python/libopenimu/importers/AppleWatchImporter.py @@ -5,6 +5,7 @@ @date 30/05/2018 """ +import logging from libopenimu.importers.BaseImporter import BaseImporter from libopenimu.models.sensor_types import SensorType @@ -549,7 +550,7 @@ def import_sensoria_to_database(self, settings, sample_rate, sensoria: dict): sensoria_acc_channels.append(self.add_channel_to_db(sensoria_acc_sensor, Units.GRAVITY_G, DataFormat.FLOAT32, 'Accelerometer_Z')) - sensoria_gyro_sensor = self.add_sensor_to_db(SensorType.GYROMETER, 'Gyrometer', 'Sensoria', 'Foot', + sensoria_gyro_sensor = self.add_sensor_to_db(SensorType.GYROMETER, 'Gyroscope', 'Sensoria', 'Foot', sample_rate, 1, settings) sensoria_gyro_channels = list() sensoria_gyro_channels.append(self.add_channel_to_db(sensoria_gyro_sensor, Units.DEG_PER_SEC, @@ -707,12 +708,12 @@ def import_beacons_to_database(self, settings, sample_rate, beacons: dict): count += 1 self.update_progress.emit(50 + np.floor(count / len(beacons) / 2 * 100)) - def import_motion_to_database(self, settings, sampling_rate, motion: dict): + def import_motion_to_database(self, settings, sampling_rate, motion: dict, version: int): # DL Oct. 16 2018, New import to database # Create channels and sensors accelerometer_sensor = self.add_sensor_to_db(SensorType.ACCELEROMETER, 'Accelerometer', - 'AppleWatch','Wrist', sampling_rate, 1, settings) + 'AppleWatch', 'Wrist', sampling_rate, 1, settings) accelerometer_channels = list() @@ -727,8 +728,8 @@ def import_motion_to_database(self, settings, sampling_rate, motion: dict): DataFormat.FLOAT32, 'Accelerometer_Z')) # Create sensor - gyro_sensor = self.add_sensor_to_db(SensorType.GYROMETER, 'Gyro','AppleWatch','Wrist', - sampling_rate, 1, settings) + gyro_sensor = self.add_sensor_to_db(SensorType.GYROMETER, 'Gyroscope', 'AppleWatch', + 'Wrist', sampling_rate, 1, settings) gyro_channels = list() @@ -751,6 +752,18 @@ def import_motion_to_database(self, settings, sampling_rate, motion: dict): orientation_channels.append(self.add_channel_to_db(orientation_sensor, Units.NONE, DataFormat.FLOAT32, 'q2')) orientation_channels.append(self.add_channel_to_db(orientation_sensor, Units.NONE, DataFormat.FLOAT32, 'q3')) + mag_channels = list() + magneto_sensor = None + if version >= 3: # Also includes magnetometer values + # Create sensor + magneto_sensor = self.add_sensor_to_db(SensorType.MAGNETOMETER, 'Magnetometer', 'AppleWatch', + 'Wrist', sampling_rate, 1, settings) + + # Create channels + mag_channels.append(self.add_channel_to_db(magneto_sensor, Units.UTESLA, DataFormat.FLOAT32, 'Mag_X')) + mag_channels.append(self.add_channel_to_db(magneto_sensor, Units.UTESLA, DataFormat.FLOAT32, 'Mag_Y')) + mag_channels.append(self.add_channel_to_db(magneto_sensor, Units.UTESLA, DataFormat.FLOAT32, 'Mag_Z')) + # Data is already hour-aligned iterate through hours count = 0 for timestamp in motion: @@ -794,10 +807,18 @@ def import_motion_to_database(self, settings, sampling_rate, motion: dict): self.add_sensor_data_to_db(recordset, gyro_sensor, gyro_channel, sensor_timestamps, valuesarray[:, i + 6]) + # Magneto + current_offset = 9 + if magneto_sensor: + for i, mag_channel in enumerate(mag_channels): + self.add_sensor_data_to_db(recordset, magneto_sensor, mag_channel, sensor_timestamps, + valuesarray[:, i + current_offset]) + current_offset = 12 + # Attitude for i, channel in enumerate(orientation_channels): self.add_sensor_data_to_db(recordset, orientation_sensor, channel, sensor_timestamps, - valuesarray[:, i + 9]) + valuesarray[:, i + current_offset]) count += 1 self.update_progress.emit(50 + np.floor(count / len(motion) / 2 * 100)) @@ -1136,7 +1157,7 @@ def import_to_database(self, results): sampling_rate = res['motion']['sampling_rate'] if res['motion']['timestamps']: self.import_motion_to_database(res['motion']['settings'], sampling_rate, - res['motion']['timestamps']) + res['motion']['timestamps'], res['motion']['version']) if res.__contains__('battery'): sampling_rate = res['battery']['sampling_rate'] @@ -1286,7 +1307,7 @@ def read_data_file(self, file, debug=False): # if version == 1: Nothing more to do - if version == 2: + if version >= 2: [json_data_size] = struct.unpack(" true + + false + Progress From f2a99a7ef655295698c3fb9e4d85803ed6423d3d Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Thu, 18 Apr 2024 11:20:19 -0400 Subject: [PATCH 2/5] Refs #80. Added functions for dyskinetics and tremors. Disabled for now (testing required with real data) --- .../importers/AppleWatchImporter.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/python/libopenimu/importers/AppleWatchImporter.py b/python/libopenimu/importers/AppleWatchImporter.py index 28ff651..d99fddb 100644 --- a/python/libopenimu/importers/AppleWatchImporter.py +++ b/python/libopenimu/importers/AppleWatchImporter.py @@ -55,6 +55,7 @@ class AppleWatchImporter(BaseImporter): HEADINGS_ID = 16 RAW_MAGNETO_ID = 17 TREMOR_ID = 18 + DYSKINETIC_ID = 19 def __init__(self, manager: DBManager, participant: Participant): super().__init__(manager, participant) @@ -1071,6 +1072,56 @@ def import_tremor_to_database(self, settings, tremor: dict): count += 1 self.update_progress.emit(50 + np.floor(count / len(tremor) / 2 * 100)) + def import_dyskinetic_to_database(self, settings, dysk: dict): + # Create channels and sensors + dysk_sensor = self.add_sensor_to_db(SensorType.BIOMETRICS, 'Dyskinetic Mvt', 'AppleWatch', 'Wrist', 0, 1, + settings) + + dysk_channels = list() + dysk_channels.append(self.add_channel_to_db(dysk_sensor, Units.MILLISECONDS, DataFormat.UINT64, + 'Start Timestamp')) + + dysk_channels.append(self.add_channel_to_db(dysk_sensor, Units.MILLISECONDS, DataFormat.UINT64, + 'End Timestamp')) + + dysk_channels.append(self.add_channel_to_db(dysk_sensor, Units.PERCENTAGE, DataFormat.FLOAT32, 'Likeliness')) + + # Data is already hour-aligned iterate through hours + count = 0 + for timestamp in dysk: + # Filter invalid timestamp if needed + if timestamp.year < 2000: + continue + + # Calculate recordset + recordset = self.get_recordset(timestamp.timestamp(), session_name=self.session_name) + + # Add data to database + + # Create time array as float64 + timesarray = np.asarray(dysk[timestamp]['times'], dtype=np.float64) + + if len(timesarray) == 0: + self.last_error = "Aucune données temporelles." + return + + valuesarray = np.asarray(dysk[timestamp]['values'], dtype=np.float32) + + # Create sensor timestamps first + sensor_timestamps = self.create_sensor_timestamps(timesarray, recordset) + + for i, channel in enumerate(dysk_channels): + # Start and end timestamp are larger than float32 - a cast is required (channel 0 and 1) + if i <= 1: + current_timestamp = np.uint64(valuesarray[:, i*2:i*2+1]) + self.add_sensor_data_to_db(recordset, dysk_sensor, channel, sensor_timestamps, current_timestamp) + else: + self.add_sensor_data_to_db(recordset, dysk_sensor, channel, sensor_timestamps, + valuesarray[:, i + 3]) + + count += 1 + self.update_progress.emit(50 + np.floor(count / len(dysk) / 2 * 100)) + def import_question_to_database(self, settings, question: dict): # Create base sensor(s) questions_sensor = self.add_sensor_to_db(SensorType.QUESTIONS, 'Prompts', 'AppleWatch', @@ -1393,6 +1444,9 @@ def read_data_file(self, file, debug=False): # elif sensor_id == self.TREMOR_ID: # read_data_func = self.read_tremor_data # dict_name = 'tremor' + # elif sensor_id == self.DYSKINETIC_ID: + # read_data_func = self.read_dyskinetic_data + # dict_name = 'dysk' else: self.last_error = "Unknown sensor ID:" + str(sensor_id) return None @@ -1714,6 +1768,19 @@ def read_tremor_data(file, debug=False, version=2): print('TREMOR: ', data) return data + @staticmethod + def read_dyskinetic_data(file, debug=False, version=2): + """ + Start Timestamp: Uint64 -- 8 bytes: Timestamp (Unix format) with milliseconds precision on which the measurement started + End Timestamp: Uint64 -- 8 bytes: Timestamp (Unix format) with milliseconds precision on which the measurement ended + Percent likely: Float32 -- 4 bytes: Percent likely that there is dyskinetic movements + """ + chunk = file.read(20) + data = struct.unpack("<2Q<1f", chunk) + if debug: + print('DYSKINETIC: ', data) + return data + @staticmethod def read_prompt_data(file, debug=False, version=2): """ From bde9c0f976337dad900adf0fb79270bea2936567 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Thu, 18 Apr 2024 13:48:24 -0400 Subject: [PATCH 3/5] Updated version number. --- python/resources/ui/StartDialog.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/resources/ui/StartDialog.ui b/python/resources/ui/StartDialog.ui index 780be27..883f7fb 100644 --- a/python/resources/ui/StartDialog.ui +++ b/python/resources/ui/StartDialog.ui @@ -385,7 +385,7 @@ in database - 1.1.0 + 1.1.2 From 3977bdf82ed0b5ac7748de8153e0e60ac1843772 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Thu, 18 Apr 2024 14:23:26 -0400 Subject: [PATCH 4/5] Fixed version and packaging issues (kept back some python packages for now) --- python/env/requirements.txt | 4 ++-- python/resources/ui/StartDialog.ui | 2 +- setup/setup_windows.iss | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/env/requirements.txt b/python/env/requirements.txt index c7f5c95..add6095 100644 --- a/python/env/requirements.txt +++ b/python/env/requirements.txt @@ -2,7 +2,7 @@ pypiwin32==223; sys_platform == 'win32' PySide6==6.7.0 cython==3.0.10 numpy==1.26.4 -scipy==1.13.0 +scipy==1.11.4 sqlalchemy==2.0.29 alembic==1.13.1 -pyinstaller==6.6.0 +pyinstaller==5.13.2 diff --git a/python/resources/ui/StartDialog.ui b/python/resources/ui/StartDialog.ui index 883f7fb..e57e457 100644 --- a/python/resources/ui/StartDialog.ui +++ b/python/resources/ui/StartDialog.ui @@ -385,7 +385,7 @@ in database - 1.1.2 + 1.1.1 diff --git a/setup/setup_windows.iss b/setup/setup_windows.iss index 486d770..8112078 100644 --- a/setup/setup_windows.iss +++ b/setup/setup_windows.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "OpenIMU" -#define MyAppVersion "1.1.0" +#define MyAppVersion "1.1.1" #define MyAppPublisher "INTER - CdRV - 3IT - USherbrooke" #define MyAppURL "https://github.com/introlab/OpenIMU" #define MyAppExeName "OpenIMU.exe" From b4ef7ea9a2ac5ef20f89a80537f9172e88917078 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Thu, 18 Apr 2024 15:13:56 -0400 Subject: [PATCH 5/5] Updated create_dmg.sh script --- setup/create_dmg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/create_dmg.sh b/setup/create_dmg.sh index bff7947..7b1db48 100755 --- a/setup/create_dmg.sh +++ b/setup/create_dmg.sh @@ -9,7 +9,7 @@ rm -r ${SCRIPT_PATH}/../python/dist/dmg/* cp -R "${SCRIPT_PATH}/../python/dist/OpenIMU.app" "${SCRIPT_PATH}/../python/dist/dmg" # If the DMG already exists, delete it. test -f "${SCRIPT_PATH}/../python/dist/OpenIMU.dmg" && rm "${SCRIPT_PATH}/../python/dist/OpenIMU.dmg" -/usr/local/bin/create-dmg \ +create-dmg \ --volname "OpenIMU" \ --volicon "${SCRIPT_PATH}/../python/resources/icons/OpenIMU.icns" \ --window-pos 200 120 \