Synchronized Raw Data Playback on Multiple WhaleTeq Devices

This guide explains how to synchronously play raw data across four WhaleTeq devices.

Reference Code: multi_device_playback.py

1. Duplicate the DLL File

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:

2. Declare AECG100 Instances and Connect Devices

Each 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()})")

3. Set Up Synchronization

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.

4. Load Raw Data File

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)

Example Raw Data File

1000
5000
2
ECG,R
0.01,0.12
0.02,0.15
...

Python Function to Load File

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

5. Play Raw Data

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:

Available Playback APIs

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;

API Usage Table

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.

Enable Loop Playback

device.enable_player_loop(c_bool(True))

Sample Playback Code

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))

6. Start Synchronized Playback

Raise the sync signal to start synchronized playback:

device1.synchronization_signal_set_event(c_void_p(sync_object))