# 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:

- `AECG100x64-1.dll`
- `AECG100x64-2.dll`
- `AECG100x64-3.dll`
- `AECG100x64-4.dll`

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