Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ The goal of this library is to provide an easy to use and feature-rich API for d
## Documentation
All documentation can be found at: [Library Documentation](https://libemg.github.io/libemg/)

## Datasets
All datasets can be found at: [Datasets](https://www.libemg.com/)

## Install
`pip install libemg`

Expand Down
17 changes: 15 additions & 2 deletions libemg/_gui/_data_collection_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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]
Expand Down Expand Up @@ -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,:]
Expand Down
76 changes: 61 additions & 15 deletions libemg/_streamers/_sifi_bridge_streamer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from multiprocessing import Process, Event
import time
import numpy as np
from collections.abc import Callable

Expand All @@ -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()].
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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()):
Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -241,69 +254,85 @@ 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)

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):
Expand All @@ -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):
Expand All @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions libemg/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
24 changes: 20 additions & 4 deletions libemg/streamers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

def sifi_biopoint_streamer(
name = "BioPoint_v1_3",
device_id = None,
shared_memory_items = None,
ecg = False,
emg = True,
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -112,6 +117,7 @@ def sifi_biopoint_streamer(

sb = SiFiBridgeStreamer(
name,
device_id,
shared_memory_items,
ecg,
emg,
Expand All @@ -124,14 +130,17 @@ def sifi_biopoint_streamer(
eda_bandpass,
eda_freq,
streaming,
mac
mac,
ble_power,
memory_mode,
)
sb.start()
return sb, shared_memory_items


def sifi_bioarmband_streamer(
name = "BioPoint_v1_1",
device_id = None,
shared_memory_items = None,
ecg = False,
emg = True,
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -226,6 +239,7 @@ def sifi_bioarmband_streamer(

sb = SiFiBridgeStreamer(
name,
device_id,
shared_memory_items,
ecg,
emg,
Expand All @@ -238,7 +252,9 @@ def sifi_bioarmband_streamer(
eda_bandpass,
eda_freq,
streaming,
mac
mac,
ble_power,
memory_mode,
)

sb.start()
Expand Down
Loading