I'd like to create a python script that can help me profile the maximum rate at which I can trigger an industrial camera. The idea is kind of iterative and as follows:
1. The script triggers a number of acquisitions over tcp socket A with a specific frequency.
2. It awaits all responses and measures the time it takes to get all responses over tcp socket B and calculates the average time for the responses.
3. Based on the average time from the previous test, a new frequency is calculated and the script repeats from step 1.
4. When the highest achievable frequency where the camera can keep up is found, report this as the result.

The behavior of the camera is a bit tricky. If it receives triggers too fast, it will queue them and perform them in sequence and continue until all triggers have been processed. If the camera is pushed to the limit, the response time will increase, but if it is not running on its critical timelimit the responses appear earlier. It seems the camera needs some time to recover inbetween the triggers.

I did a similar program that worked a bit differently, but i think this new method is much better. You can use the ip address, tcp sockets and command information from the program below, but do not use any of the workflow ideas from the current implementation:

import datetime
import os
import sys
import time
import socket
import pandas as pd
from openpyxl import load_workbook


def send_trigger(s, res, interval):
    start_send = time.time_ns()
    s.sendall(b'\x02trigger\x03')
    try:
        response = res.recv(1024)
    except socket.timeout:
        res.settimeout(10)
        try:
            response = res.recv(1024)
        except:
            pass
        res.settimeout(interval)
        response = b''
    end_send = time.time_ns()
    send_duration = (end_send - start_send) / 1e9
    return send_duration, response


def trigger_camera(interval, num_measurements):
    timeout_count = 0
    time_to_responses = []
    actual_time = []
    res = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    res.connect((ip_address, 8999))
    res.settimeout(0.2)
    try:
        response = res.recv(1024)
    except socket.timeout:
        pass
    res.settimeout(interval)
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((ip_address, 6666))

        for i in range(num_measurements):
            start_time = time.time_ns()
            ttr, response = send_trigger(s, res, interval)
            time_to_responses.append(ttr)
            remaining_time = max(0, interval - ttr)
            time.sleep(remaining_time)
            stop_time = time.time_ns()
            actual_time_taken = (stop_time - start_time) / 1e6
            actual_time.append(actual_time_taken)
            sys.stdout.write(
                f'\rInterval: {interval*1000:.1f}ms Iteration: {i} Timeouts: {(timeout_count / (i+0.00000001)) * 100:.1f}%, actual_time_taken {actual_time_taken:.1f}ms TTR:{ttr*1000:.1f}ms')
            sys.stdout.flush()
            if not response:
                timeout_count += 1

    res.close()

    avg_time_to_responses = sum(time_to_responses) / num_measurements
    avg_actual_time_taken = sum(actual_time) / num_measurements
    timeout_percentage = (timeout_count / num_measurements) * 100

    return avg_actual_time_taken, avg_time_to_responses, timeout_count, timeout_percentage


def find_optimal_interval():
    global ip_address
    ip_address = input(
        f"IP Address ({'192.168.137.201'}): ") or '192.168.137.201'
    print("Overtriggering the camera to estimate response time...")
    _, avg_time_to_responses, _, _ = trigger_camera(
        0.001, 50)  # 50 iterations for overtriggering
    initial_estimate = avg_time_to_responses * 1000  # Convert to milliseconds

    print(f"\nEstimated response time: {initial_estimate:.2f}ms")

    # Prompt for user input after initial overtriggering with pre-filled defaults
    camera = input(f"Camera ({'Inspector8xx'}): ") or 'Inspector8xx'
    resolution = input(f"Resolution ({'5MP'}): ") or '5MP'
    exposure_time = input(f"Exposure time [us] ({'70'}): ") or '70'
    job = input(f"Job ({'Empty'}): ") or 'Empty'
    channels = input(f"Channels ({'mono'}): ") or 'mono'
    tool_time = input(f"Tool time [ms] ({'0'}): ") or '0'
    reported_time = input(f"Reported time [ms] ({'0'}): ") or '0'

    low_interval = max(0, initial_estimate - 1) / 1000
    best_interval = None
    best_timeout_percentage = float('inf')
    consecutive_below_2 = 0

    valid_intervals = []

    print(
        f"Evaluating around the estimated response time: starting from {low_interval*1000:.2f}ms")

    interval_ms = int(low_interval*1000)
    while True:
        interval = interval_ms / 1000
        _, _, _, timeout_percentage = trigger_camera(
            interval, 100)  # 100 iterations per step

        print(
            f"\nInterval: {interval_ms}ms, Timeout Percentage: {timeout_percentage:.2f}%")

        if timeout_percentage < 2:
            consecutive_below_2 += 1
            if consecutive_below_2 == 3:
                valid_intervals.append(interval_ms)
                # Continue searching
                consecutive_below_2 = 0
        else:
            consecutive_below_2 = 0

        interval_ms += 1

        # Exit loop if valid intervals are found
        if valid_intervals:
            break

    # If no valid intervals are found, use a default interval or handle the error
    if not valid_intervals:
        print("No valid intervals found where timeout percentage is below 2%")
        return None, None, None, None, None, camera, resolution, exposure_time, job, ip_address, channels, tool_time, reported_time

    # Find the lowest interval among valid intervals
    best_interval = min(valid_intervals)

    # Perform final measurement at the best interval
    avg_actual_time_taken, avg_time_to_responses, timeout_count, timeout_percentage = trigger_camera(
        best_interval / 1000, 400)

    return best_interval, avg_actual_time_taken, avg_time_to_responses, timeout_count, timeout_percentage, camera, resolution, exposure_time, job, ip_address, channels, tool_time, reported_time


if __name__ == "__main__":
    best_interval, avg_actual_time_taken, avg_time_to_responses, timeout_count, timeout_percentage, camera, resolution, exposure_time, job, ip_address, channels, tool_time, reported_time = find_optimal_interval()

    current_time = datetime.datetime.now()
    folder_name = current_time.strftime("%Y-%m-%d_%H-%M-%S")
    os.makedirs(folder_name)

    filename = (f"{camera}_{resolution}_{exposure_time}us_{job}_"
                f"{ip_address}_{channels}_{tool_time}ms_{reported_time}ms_"
                f"Results_{current_time.strftime('%Y%m%d_%H%M%S')}.xlsx")

    unified_filename = "Unified_Results.xlsx"

    # Prepare data to append
    data = {
        "Camera": [camera],
        "Resolution": [resolution],
        "Exposure Time (us)": [exposure_time],
        "Job": [job],
        "IP Address": [ip_address],
        "Channels": [channels],
        "Tool Time (ms)": [tool_time],
        "Reported Time (ms)": [reported_time],
        "Interval (ms)": [best_interval],
        "Avg. Actual Time Taken (ms)": [f'{avg_actual_time_taken:.2f}'],
        "Avg. Time to responses (ms)": [f'{avg_time_to_responses*1000:.2f}'],
        "Timeout Count": [timeout_count],
        "Timeout Percentage": [f"{timeout_percentage:.2f}%"]
    }

    df = pd.DataFrame(data)

    if os.path.exists(unified_filename):
        with pd.ExcelWriter(unified_filename, engine='openpyxl', mode='a', if_sheet_exists='overlay') as writer:
            df.to_excel(writer, index=False, header=False,
                        startrow=writer.sheets['Sheet1'].max_row)
    else:
        with pd.ExcelWriter(unified_filename, engine='openpyxl') as writer:
            df.to_excel(writer, index=False)

    # Save the individual results to a separate file
    df.to_excel(os.path.join(folder_name, filename), index=False)

    print(f"Results saved as {filename} in folder {folder_name}")
    print(f"Unified results saved in {unified_filename}")