This guide explains how to synchronously play raw data across four WhaleTeq devices.
Reference Code: multi_device_playback.py
To control multiple devices, you must duplicate the DLL or library file. Each DLL instance must be associated with one device.
For example, for four devices, create:
AECG100x64-1.dllAECG100x64-2.dllAECG100x64-3.dllAECG100x64-4.dllEach device requires a separate DLL instance. The following example sets a 5-second timeout for each connection:
device1 = AECG100('./AECG100x64-1.dll')
if device1.connect(-1, 5000) == False:
print('Error: device1 is not connected')
sys.exit()
device2 = AECG100('./AECG100x64-2.dll')
if device2.connect(-1, 5000) == False:
print('Error: device2 is not connected')
device1.free()
sys.exit()
device3 = AECG100('./AECG100x64-3.dll')
if device3.connect(-1, 5000) == False:
print('Error: device3 is not connected')
device1.free()
device2.free()
sys.exit()
device4 = AECG100('./AECG100x64-4.dll')
if device4.connect(-1, 5000) == False:
print('Error: device4 is not connected')
device1.free()
device2.free()
device3.free()
sys.exit()
If you know the COM port number:
# device1 is on COM9
device1 = AECG100('./SDK/windows/AECG100x64-1.dll')
if not device1.connect(9, 5000):
sys.exit("Error: Device 1 connection failed.")
Verify connection by printing serial numbers:
print(f"device1 connected ({device1.get_serial_number()} / {device1.get_ppg_serial_number()})")
Create one synchronization signal and assign it to all devices:
sync_object = device1.create_synchronization_signal()
for dev in [device1, device2, device3, device4]:
dev.set_synchronization_signal(sync_object)
Note: Only create the sync signal on one device and share it with others.
Format your raw data file as follows (recommended extension: .txt):
Line 1: Sampling rate (e.g., 1000)
Line 2: Number of data points
Line 3: Number of output channels
Line 4: Channel names, comma-separated (e.g., ECG,R,IR,Green)
Line 5~End: Raw values (comma-separated, one line per sample)
1000
5000
2
ECG,R
0.01,0.12
0.02,0.15
...
ac = []
dc = []
class ChannelSetting:
def __init__(self, dc_value=None, amplify=None):
self.dc_value = dc_value
self.amplify = amplify
ECG = ChannelSetting(dc_value=0, amplify=1)
G = ChannelSetting(dc_value=0, amplify=30)
R = ChannelSetting(dc_value=600, amplify=30)
IR = ChannelSetting(dc_value=600, amplify=30)
def load_raw_data_file(file_name):
global ac, dc
raw_data_struct = []
line_number = 0
with open(file_name, 'r') as f:
for x in f:
x = x.strip()
if not x:
continue
if line_number == 0:
sample_rate = float(x)
elif line_number == 1:
size = int(x)
elif line_number == 2:
channel_count = int(x)
start_index = len(ac)
for i in range(channel_count):
raw_data_struct.append(PLAY_RAW_DATA())
ac.append((c_double * size)())
dc.append((c_double * size)())
elif line_number == 3:
signal = x.split(',')
elif line_number >= 4:
vals = x.split(',')
if len(vals) >= channel_count:
for i, signal_name in enumerate(signal):
match signal_name:
case "ECG":
ac[start_index + i][line_number - 4] = float(vals[i]) * ECG.amplify # ECG AC
dc[start_index + i][line_number - 4] = ECG.dc_value
case "R":
ac[start_index + i][line_number - 4] = float(vals[i]) * R.amplify # RED AC
dc[start_index + i][line_number - 4] = R.dc_value
case "IR":
ac[start_index + i][line_number - 4] = float(vals[i]) * IR.amplify # IR AC
dc[start_index + i][line_number - 4] = IR.dc_value
case "Green":
ac[start_index + i][line_number - 4] = float(vals[i]) * G.amplify # Green AC
dc[start_index + i][line_number - 4] = G.dc_value
line_number += 1
for i, signal_name in enumerate(signal):
raw_data_struct[i].SampleRate = sample_rate
raw_data_struct[i].Size = size
raw_data_struct[i].AC = addressof(ac[start_index + i])
raw_data_struct[i].DC = addressof(dc[start_index + i])
raw_data_struct[i].OutputSignalCallback = OutputSignalCallback(0)
return raw_data_struct
Once the data is loaded, you'll get a list where the length depends on the number of channels that need to be output. Depending on the type of signals you want to play, use the corresponding API and pass the list items as inputs.
There are several types of API to play data from file:
def play_raw_ecg(raw_data)
def play_raw_ecg_ppg(raw_data_ecg, ppg_channel_number, raw_data_ppg)
def play_raw_ecg_ppg_ex(raw_data_ecg, raw_data_ch1, raw_data_ch2)
def play_raw_ecg_ppg3(raw_data_ecg, raw_data_r, raw_data_ir, raw_data_g)
def play_raw_ppg(ppg_channel_number, raw_data)
def play_raw_ppg_ex(raw_data_ch1, raw_data_ch2)
def play_raw_ppg3(raw_data_r, raw_data_ir, raw_data_g)
ppg_channel_number Mapping// WAP1001 channel-1: G, channel-2: NA, channel-3: NA
// WAP2001-2004 channel-1: R, channel-2: IR, channel-3: NA
// WAP2005-2008 channel-1: R, channel-2: IR, channel-3: G
// WAP2012 channel-1: R, channel-2: IR, channel-3: NA
typedef enum {
PPGChannel1 = 1, //!< channel-1
PPGChannel2 = 2, //!< channel-2
PPGChannel3 = 3 //!< channel-3
} PPGChannel;
| ECG | PPG (R) | PPG (IR) | PPG (G) | API Call |
|---|---|---|---|---|
| ✓ | play_raw_ecg |
|||
| ✓ | ✓* | ✓* | ✓* | play_raw_ecg_ppg |
| ✓ | ✓ | ✓ | play_raw_ecg_ppg_ex |
|
| ✓ | ✓ | ✓ | ✓ | play_raw_ecg_ppg3 |
| ✓* | ✓* | ✓* | play_raw_ppg |
|
| ✓ | ✓ | play_raw_ppg_ex |
||
| ✓ | ✓ | ✓ | play_raw_ppg3 |
✳ Indicates that only one PPG channel is supported at a time.
device.enable_player_loop(c_bool(True))
raw_data_list = load_raw_data_file('ecg-1hz-5mv.txt')
device1.play_raw_ecg(pointer(raw_data_list[0]))
device1.enable_player_loop(c_bool(True))
raw_data_list = load_raw_data_file('ppg_player1.txt')
device2.play_raw_ppg(1, pointer(raw_data_list[0]))
device2.enable_player_loop(c_bool(True))
raw_data_list = load_raw_data_file('sync_test_data_delay_0.00.txt')
device3.play_raw_ecg_ppg(pointer(raw_data_list[0]), 1, pointer(raw_data_list[1]))
device3.enable_player_loop(c_bool(True))
raw_data_list = load_raw_data_file('sync_test_data_delay_0.00.txt')
device4.play_raw_ecg_ppg_ex(pointer(raw_data_list[0]), pointer(raw_data_list[1]), pointer(raw_data_list[2]))
device4.enable_player_loop(c_bool(True))
Raise the sync signal to start synchronized playback:
device1.synchronization_signal_set_event(c_void_p(sync_object))