diff --git a/libemg/_gui/_data_collection_panel.py b/libemg/_gui/_data_collection_panel.py index 2adf0e56..a2dca22d 100644 --- a/libemg/_gui/_data_collection_panel.py +++ b/libemg/_gui/_data_collection_panel.py @@ -9,7 +9,7 @@ import json from datetime import datetime from ._utils import Media, set_texture, init_matplotlib_canvas, matplotlib_to_numpy - +import libemg.utils import threading import matplotlib.pyplot as plt @@ -234,7 +234,8 @@ def run_sgt(self, media_list): self.play_collection_visual(media_list[self.i], active=False) media_list[self.i][0].reset() self.gui.online_data_handler.reset() - + + libemg.utils.log_timestamp(self.output_folder, tag=f"C_" + str(media_list[self.i][2]) + "_R_" + str(media_list[self.i][3]) + " ACQUISITION START", append=True, print_timestamp=False) self.play_collection_visual(media_list[self.i], active=True) output_path = Path(self.output_folder, "C_" + str(media_list[self.i][2]) + "_R_" + str(media_list[self.i][3]) + ".csv").absolute().as_posix() @@ -265,6 +266,7 @@ def run_sgt(self, media_list): dpg.configure_app(manual_callback_management=False) if not is_final_media: dpg.set_value('__dc_rep', value=f"Rep {media_list[self.i][3] + 1} of {self.num_reps}") + def redo_collection_callback(self): if self.auto_advance: @@ -274,12 +276,18 @@ def redo_collection_callback(self): dpg.hide_item(item="__dc_redo_button") dpg.hide_item(item="__dc_continue_button") self.advance = True + + current_rep = (self.i // self.num_motions) + libemg.utils.log_timestamp(self.output_folder, tag=f"R_{current_rep} REDO", append=True, print_timestamp=False) def continue_collection_callback(self): dpg.hide_item(item="__dc_redo_button") dpg.hide_item(item="__dc_continue_button") self.advance = True + current_rep = (self.i // self.num_motions) - 1 + libemg.utils.log_timestamp(self.output_folder, tag=f"R_{current_rep} CONTINUE", append=True, print_timestamp=False) + def play_collection_visual(self, media, active=True): if active: timer_duration = media[-1] @@ -316,6 +324,11 @@ def play_collection_visual(self, media, active=True): def save_data(self, filename): file_parts = filename.split('.') + folder = os.path.dirname(filename) + base_name = os.path.basename(filename) + tag = f"{base_name.split('.')[0]} SAVE" + libemg.utils.log_timestamp(folder, tag=tag, append=True, print_timestamp=False) + for mod in self.rep_buffer: filename = file_parts[0] + "_" + mod + "." + file_parts[1] data = np.vstack(self.rep_buffer[mod])[::-1,:] diff --git a/libemg/_streamers/_myo_streamer.py b/libemg/_streamers/_myo_streamer.py index 0e8b1237..f69c9f8b 100644 --- a/libemg/_streamers/_myo_streamer.py +++ b/libemg/_streamers/_myo_streamer.py @@ -516,7 +516,7 @@ def on_battery(self, battery_level): import numpy as np from multiprocessing import Process, Event class MyoStreamer(Process): - def __init__(self, filtered, emg, imu, shared_memory_items=[]): + def __init__(self, filtered, emg, imu, shared_memory_items=[], addr=None): Process.__init__(self, daemon=True) self.filtered = filtered self.emg = emg @@ -524,6 +524,7 @@ def __init__(self, filtered, emg, imu, shared_memory_items=[]): self.smm = SharedMemoryManager() self.shared_memory_items = shared_memory_items self.signal = Event() + self.addr = addr def run(self): for item in self.shared_memory_items: @@ -533,7 +534,7 @@ def run(self): if not self.filtered: mode = emg_mode.RAW self.m = Myo(mode=mode) - self.m.connect() + self.m.connect(addr=self.addr) if self.emg: def write_emg(emg): diff --git a/libemg/_streamers/_sifi_bridge_streamer.py b/libemg/_streamers/_sifi_bridge_streamer.py index 2bed67ea..90fff680 100644 --- a/libemg/_streamers/_sifi_bridge_streamer.py +++ b/libemg/_streamers/_sifi_bridge_streamer.py @@ -1,4 +1,5 @@ from multiprocessing import Process, Event +import time import numpy as np from collections.abc import Callable @@ -18,6 +19,8 @@ class SiFiBridgeStreamer(Process): ---------- name : str The name of the devie (eg BioArmband, BioPoint_v1_2, BioPoint_v1_3, etc.). None to auto-connect to any device. + device_id : int | None + If multiple devices are connected, this is the device ID to differentiate them. None by default (just one device). shared_memory_items : list Shared memory configuration parameters for the streamer in format: ["tag", (size), datatype, Lock()]. @@ -51,6 +54,7 @@ class SiFiBridgeStreamer(Process): def __init__( self, name: str | None = None, + device_id: int | None = None, shared_memory_items: list = [], ecg: bool = False, emg: bool = True, @@ -80,9 +84,11 @@ def __init__( self.eda_handlers = [] self.ecg_handlers = [] self.ppg_handlers = [] + self.temp_handlers = [] self.name = name + self.device_id = device_id if device_id is not None else "" self.ecg = ecg self.emg = emg self.eda = eda @@ -130,7 +136,7 @@ def connect(self): print(f"Could not connect to {self.handle}. Retrying.") self.connected = True - print("Connected to Sifi device.") + print(f"Connected to Sifi device{self.device_id}.") self.sb.stop() self.sb.start() @@ -149,6 +155,9 @@ def add_ecg_handler(self, closure: Callable): def add_eda_handler(self, closure: Callable): self.eda_handlers.append(closure) + + def add_temperature_handler(self, closure: Callable): + self.temp_handlers.append(closure) def process_packet(self, data: dict): if "data" in list(data.keys()): @@ -212,6 +221,10 @@ def process_packet(self, data: dict): self.old_ppg_packet = None for h in self.ppg_handlers: h(ppg) + if "temperature" in list(data["data"].keys()): + temperature = np.stack((data["data"]["temperature"],)).T + for h in self.temp_handlers: + h(temperature) def run(self): # process is started beyond this point! @@ -241,20 +254,20 @@ def run(self): def write_emg(emg): # update the samples in "emg" self.smm.modify_variable( - "emg", lambda x: np.vstack((np.flip(emg, 0), x))[: x.shape[0], :] + f"emg{self.device_id}", lambda x: np.vstack((np.flip(emg, 0), x))[: x.shape[0], :] ) # update the number of samples retrieved - self.smm.modify_variable("emg_count", lambda x: x + emg.shape[0]) + self.smm.modify_variable(f"emg{self.device_id}_count", lambda x: x + emg.shape[0]) self.add_emg_handler(write_emg) def write_imu(imu): # update the samples in "imu" self.smm.modify_variable( - "imu", lambda x: np.vstack((np.flip(imu, 0), x))[: x.shape[0], :] + f"imu{self.device_id}", lambda x: np.vstack((np.flip(imu, 0), x))[: x.shape[0], :] ) # update the number of samples retrieved - self.smm.modify_variable("imu_count", lambda x: x + imu.shape[0]) + self.smm.modify_variable(f"imu{self.device_id}_count", lambda x: x + imu.shape[0]) # sock.sendto(data_arr, (self.ip, self.port)) self.add_imu_handler(write_imu) @@ -262,48 +275,64 @@ def write_imu(imu): def write_eda(eda): # update the samples in "eda" self.smm.modify_variable( - "eda", lambda x: np.vstack((np.flip(eda, 0), x))[: x.shape[0], :] + f"eda{self.device_id}", lambda x: np.vstack((np.flip(eda, 0), x))[: x.shape[0], :] ) # update the number of samples retrieved - self.smm.modify_variable("eda_count", lambda x: x + eda.shape[0]) + self.smm.modify_variable(f"eda{self.device_id}_count", lambda x: x + eda.shape[0]) self.add_eda_handler(write_eda) def write_ppg(ppg): # update the samples in "ppg" self.smm.modify_variable( - "ppg", lambda x: np.vstack((np.flip(ppg, 0), x))[: x.shape[0], :] + f"ppg{self.device_id}", lambda x: np.vstack((np.flip(ppg, 0), x))[: x.shape[0], :] ) # update the number of samples retrieved - self.smm.modify_variable("ppg_count", lambda x: x + ppg.shape[0]) + self.smm.modify_variable(f"ppg{self.device_id}_count", lambda x: x + ppg.shape[0]) self.add_ppg_handler(write_ppg) def write_ecg(ecg): # update the samples in "ecg" self.smm.modify_variable( - "ecg", lambda x: np.vstack((np.flip(ecg, 0), x))[: x.shape[0], :] + f"ecg{self.device_id}", lambda x: np.vstack((np.flip(ecg, 0), x))[: x.shape[0], :] ) # update the number of samples retrieved - self.smm.modify_variable("ecg_count", lambda x: x + ecg.shape[0]) + self.smm.modify_variable(f"ecg{self.device_id}_count", lambda x: x + ecg.shape[0]) self.add_ecg_handler(write_ecg) + + def write_temperature(temperature): + # update the samples in "temperature" + self.smm.modify_variable( + f"temp{self.device_id}", lambda x: np.vstack((np.flip(temperature, 0), x))[: x.shape[0], :] + ) + # update the number of samples retrieved + self.smm.modify_variable(f"temp{self.device_id}_count", lambda x: x + temperature.shape[0]) + self.add_temperature_handler(write_temperature) + self.connect() self.old_ppg_packet = ( None # required for now since ppg sends non-uniform packet length ) + + print("LibEMG -> SiFiBridgeStreamer (process started).") while True: try: new_packet = self.sb.get_data() self.process_packet(new_packet) except Exception as e: - print("Error Occurred: " + str(e)) + print("Error Occurred maybe: ", e, "type : ", type(e), "representation : ", repr(e), " -> continuing.") + import traceback + traceback.print_exc() continue if self.signal.is_set(): + print("LibEMG -> SiFiBridgeStreamer (signal received).") self.cleanup() break + print("LibEMG -> SiFiBridgeStreamer (process ended).") def stop_sampling(self): @@ -315,7 +344,17 @@ def turnoff(self): return def disconnect(self): - self.connected = self.sb.disconnect()["connected"] + try: + result = self.sb.disconnect() + # Handle both dictionary and boolean return types + if isinstance(result, dict) and "connected" in result: + self.connected = result["connected"] + else: + # If it returns a boolean directly, assume False means disconnected + self.connected = not result if isinstance(result, bool) else False + except Exception as e: + print(f"Error during disconnect: {e}") + self.connected = False return self.connected def deep_sleep(self): @@ -324,8 +363,15 @@ def deep_sleep(self): def cleanup(self): self.stop_sampling() # stop sampling print("LibEMG -> SiFiBridgeStreamer (sampling stopped).") - self.deep_sleep() # stops status packets - print("LibEMG -> SiFiBridgeStreamer (device sleeped).") + time.sleep(1) + if self.mac == "CE:9B:59:A6:BD:EC" or self.mac == "DD:67:FD:19:06:03": + self.turnoff() + print("LibEMG -> SiFiBridgeStreamer (device turned off).") + else: + self.deep_sleep() # stops status packets + print("LibEMG -> SiFiBridgeStreamer (device sleeped).") + + time.sleep(1) self.disconnect() # disconnect print("LibEMG -> SiFiBridgeStreamer (device disconnected).") self.sb._bridge.kill() diff --git a/libemg/gui.py b/libemg/gui.py index 45b4d020..0eb0934a 100644 --- a/libemg/gui.py +++ b/libemg/gui.py @@ -141,6 +141,11 @@ def _on_window_close(self): print("Window is closing. Performing clean-up...") if 'streamer' in self.args.keys(): self.args['streamer'].signal.set() + print("Streamer stopped in window closed.") + if 'streamers' in self.args.keys(): + for streamer in self.args['streamers']: + streamer.signal.set() + print(f"Streamer stopped in window closed.") time.sleep(3) def download_gestures(self, gesture_ids, folder, download_imgs=True, download_gifs=False, redownload=False): diff --git a/libemg/streamers.py b/libemg/streamers.py index 69882d56..4a56065e 100644 --- a/libemg/streamers.py +++ b/libemg/streamers.py @@ -19,6 +19,7 @@ def sifi_biopoint_streamer( name = "BioPoint_v1_3", + device_id = None, shared_memory_items = None, ecg = False, emg = True, @@ -31,7 +32,9 @@ def sifi_biopoint_streamer( eda_bandpass = (0,5), eda_freq = 0, streaming=False, - mac= None + mac= None, + ble_power="high", + memory_mode="both" ): """ The streamer for the SiFi BioPoint. @@ -48,6 +51,8 @@ def sifi_biopoint_streamer( device: string, default = BioPoint_v1_3 The name or MAC of the device. + device_id : int | None + If multiple devices are connected, this is the device ID to differentiate them. None by default (just one device). shared_memory_items, default = [] The key, size, datatype, and multiprocessing Lock for all data to be shared between processes. ecg, default = False @@ -112,6 +117,7 @@ def sifi_biopoint_streamer( sb = SiFiBridgeStreamer( name, + device_id, shared_memory_items, ecg, emg, @@ -124,7 +130,9 @@ def sifi_biopoint_streamer( eda_bandpass, eda_freq, streaming, - mac + mac, + ble_power, + memory_mode, ) sb.start() return sb, shared_memory_items @@ -132,6 +140,7 @@ def sifi_biopoint_streamer( def sifi_bioarmband_streamer( name = "BioPoint_v1_1", + device_id = None, shared_memory_items = None, ecg = False, emg = True, @@ -144,7 +153,9 @@ def sifi_bioarmband_streamer( eda_bandpass = (0,5), eda_freq = 0, streaming = False, - mac = None + mac = None, + ble_power = "high", + memory_mode = "both" ): """ The streamer for the SiFi BioArmband. @@ -161,6 +172,8 @@ def sifi_bioarmband_streamer( name: string, default = BioArmband The name of the Sifi Device. For example: BioArmband, BioPoint_v1_3, etc. + device_id : int | None + If multiple devices are connected, this is the device ID to differentiate them. None by default (just one device). shared_memory_items, default = [] The key, size, datatype, and multiprocessing Lock for all data to be shared between processes. ecg, default = False @@ -226,6 +239,7 @@ def sifi_bioarmband_streamer( sb = SiFiBridgeStreamer( name, + device_id, shared_memory_items, ecg, emg, @@ -238,7 +252,9 @@ def sifi_bioarmband_streamer( eda_bandpass, eda_freq, streaming, - mac + mac, + ble_power, + memory_mode, ) sb.start() @@ -248,7 +264,8 @@ def myo_streamer( shared_memory_items : list | None = None, emg : bool = True, imu : bool = False, - filtered : bool=True): + filtered : bool=True, + addr : list | None = None): """The streamer for the myo armband. This function connects to the Myo. It leverages the PyoMyo @@ -265,6 +282,9 @@ def myo_streamer( Specifies whether IMU data should be forwarded to shared memory. filtered : bool (optional), default=True If True, the data is the filtered data. Otherwise it is the raw unfiltered data. + addr : list (optional) + The MAC address of the Myo armband to connect to. Addr is the MAC address in format: [93, 41, 55, 245, 82, 194] + If None, it will connect to the first Myo it finds. Returns ---------- Object: streamer @@ -286,7 +306,7 @@ def myo_streamer( for item in shared_memory_items: item.append(Lock()) - myo = MyoStreamer(filtered, emg, imu, shared_memory_items) + myo = MyoStreamer(filtered, emg, imu, shared_memory_items, addr) myo.start() return myo, shared_memory_items diff --git a/libemg/utils.py b/libemg/utils.py index 8422dd5a..f0c335da 100644 --- a/libemg/utils.py +++ b/libemg/utils.py @@ -5,6 +5,7 @@ import matplotlib.pyplot as plt from matplotlib.backends.backend_agg import FigureCanvasAgg from matplotlib.patches import Circle +from datetime import datetime def get_windows(data, window_size, window_increment): @@ -100,3 +101,29 @@ def make_regex(left_bound, right_bound, values = None): right_bound_str = "(?=" + right_bound +")" return left_bound_str + mid_str + right_bound_str + +def log_timestamp(path, file_name="timestamp_log.txt", tag="", append=True, print_timestamp=True): + """Logs the current timestamp to a file. + + Parameters + ---------- + path: str + The path to the directory where the log file will be saved. + file_name: str + The name of the log file. + tag: str + An optional tag to include in the log entry. + append: bool + Whether to append to the log file or overwrite it. + print_timestamp: bool + Whether to print the timestamp to the console. + """ + if not os.path.exists(path): + os.makedirs(path) + + log_file = os.path.join(path, file_name) + with open(log_file, "a" if append else "w") as f: + timestamp = datetime.now().isoformat() + f.write(f"{timestamp} - {tag}\n") + if print_timestamp: + print(f"Logged timestamp: {timestamp} - {tag}")