commeownder/simulator/test_latency.c

486 lines
17 KiB
C

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* simulator/test_latency.c
*
* Long-running BLE latency benchmark for commeownder.
*
* Opens 13 physical device serial ports, spawns the BLE simulator as a
* background 4th peer, then measures advertisement propagation latency for
* every directed pair of physical devices. Reports per-pair, per-device-as-
* source, per-device-as-receiver, and overall statistics.
*
* Life values used for measurement are globally unique per iteration so that
* a PEER_RX on the destination can only have come from the source's fresh SET.
*
* Usage:
* ./test_latency --port1 /dev/ttyUSB0 --port2 /dev/ttyUSB1 --port3 /dev/ttyUSB2
* [--sim PATH] [--game-id ID] [--iterations N] [--timeout S]
*/
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <math.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#ifndef SIM_BIN
# define SIM_BIN "./simulator"
#endif
#define MAX_DEVS 3
#define LBUF 512
#define QSIZE 256
#define DEFAULT_ITERS 30
#define DEFAULT_TIMEOUT_S 5
#define DEFAULT_GAME_ID "4242"
#define SETTLE_S 3 /* settle time after setup before measuring */
#define LIFE_BASE 50 /* first life value used for measurement */
/* ── Line queue ─────────────────────────────────────────────────────────── */
typedef struct {
char data[QSIZE][LBUF];
int head, tail, count;
pthread_mutex_t mtx;
pthread_cond_t cond;
} lq_t;
static void lq_init(lq_t *q)
{
memset(q, 0, sizeof *q);
pthread_mutex_init(&q->mtx, NULL);
pthread_condattr_t attr;
pthread_condattr_init(&attr);
pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
pthread_cond_init(&q->cond, &attr);
pthread_condattr_destroy(&attr);
}
static void lq_push(lq_t *q, const char *line)
{
pthread_mutex_lock(&q->mtx);
if (q->count < QSIZE) {
strncpy(q->data[q->tail], line, LBUF - 1);
q->data[q->tail][LBUF - 1] = '\0';
q->tail = (q->tail + 1) % QSIZE;
q->count++;
pthread_cond_signal(&q->cond);
}
pthread_mutex_unlock(&q->mtx);
}
static void lq_drain(lq_t *q)
{
pthread_mutex_lock(&q->mtx);
q->head = q->tail = q->count = 0;
pthread_mutex_unlock(&q->mtx);
}
static int lq_wait(lq_t *q, const char **pats, int secs, char *out)
{
struct timespec dl;
clock_gettime(CLOCK_MONOTONIC, &dl);
dl.tv_sec += secs;
pthread_mutex_lock(&q->mtx);
for (;;) {
while (q->count > 0) {
char *line = q->data[q->head];
q->head = (q->head + 1) % QSIZE;
q->count--;
int ok = 1;
for (int i = 0; pats[i]; i++)
if (!strstr(line, pats[i])) { ok = 0; break; }
if (ok) {
if (out) strncpy(out, line, LBUF - 1);
pthread_mutex_unlock(&q->mtx);
return 1;
}
}
if (pthread_cond_timedwait(&q->cond, &q->mtx, &dl) == ETIMEDOUT) {
pthread_mutex_unlock(&q->mtx);
return 0;
}
}
}
/* ── Device ──────────────────────────────────────────────────────────────── */
typedef struct {
const char *port;
int fd;
lq_t q;
pthread_t thr;
} device_t;
static device_t g_devs[MAX_DEVS];
static int g_ndevs = 0;
static volatile int g_stop = 0;
static void *serial_reader(void *arg)
{
device_t *dev = (device_t *)arg;
char line[LBUF];
int pos = 0;
while (!g_stop) {
char c;
ssize_t n = read(dev->fd, &c, 1);
if (n == 0) continue;
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) { usleep(1000); continue; }
break;
}
if (c == '\n' || c == '\r') {
if (pos > 0) {
line[pos] = '\0';
if (strncmp(line, "DBG ", 4) == 0)
lq_push(&dev->q, line);
pos = 0;
}
} else if (pos < LBUF - 1) {
line[pos++] = c;
}
}
return NULL;
}
static int open_serial(const char *port)
{
int fd = open(port, O_RDWR | O_NOCTTY);
if (fd < 0) { perror("open"); return -1; }
struct termios tty;
memset(&tty, 0, sizeof tty);
tty.c_cflag = CS8 | CREAD | CLOCAL;
cfsetispeed(&tty, B115200);
cfsetospeed(&tty, B115200);
tty.c_oflag = 0;
tty.c_lflag = 0;
tty.c_cc[VMIN] = 0;
tty.c_cc[VTIME] = 1;
if (tcsetattr(fd, TCSANOW, &tty) < 0) { perror("tcsetattr"); close(fd); return -1; }
return fd;
}
static void dev_send(device_t *dev, const char *cmd)
{
if (write(dev->fd, cmd, strlen(cmd)) < 0) perror("write");
}
/* Short label from port path, e.g. "/dev/ttyUSB1" → "USB1". */
static const char *port_label(const char *port)
{
const char *p = strstr(port, "tty");
return p ? p + 3 : port;
}
/* ── Simulator ───────────────────────────────────────────────────────────── */
static pid_t g_sim_pid = -1;
static const char *g_sim_bin = SIM_BIN;
static const char *g_game_id = DEFAULT_GAME_ID;
static pid_t sim_start(void)
{
pid_t pid = fork();
if (pid < 0) { perror("fork"); return -1; }
if (pid == 0) {
for (int i = 0; i < g_ndevs; i++) close(g_devs[i].fd);
const char *args[] = {
g_sim_bin,
"--name", "LATSIM",
"--life", "40",
"--poison", "0",
"--game-id", g_game_id,
NULL
};
execv(g_sim_bin, (char *const *)args);
_exit(1);
}
return pid;
}
static void sig_cleanup(int sig)
{
(void)sig;
if (g_sim_pid > 0) { kill(g_sim_pid, SIGTERM); waitpid(g_sim_pid, NULL, WNOHANG); }
_exit(1);
}
/* ── Statistics ──────────────────────────────────────────────────────────── */
typedef struct {
long min, max;
double sum, sum_sq;
int count, timeouts;
} stats_t;
static void stats_init(stats_t *s)
{
memset(s, 0, sizeof *s);
s->min = LONG_MAX;
s->max = 0;
}
static void stats_record(stats_t *s, long ms)
{
s->count++;
s->sum += ms;
s->sum_sq += (double)ms * ms;
if (ms < s->min) s->min = ms;
if (ms > s->max) s->max = ms;
}
static void stats_merge(stats_t *dst, const stats_t *src)
{
if (src->count == 0 && src->timeouts == 0) return;
dst->count += src->count;
dst->sum += src->sum;
dst->sum_sq += src->sum_sq;
dst->timeouts += src->timeouts;
if (src->count > 0) {
if (src->min < dst->min) dst->min = src->min;
if (src->max > dst->max) dst->max = src->max;
}
}
static void stats_print(const stats_t *s, const char *label)
{
printf(" %-28s", label);
if (s->count == 0) {
printf(" no samples");
} else {
double avg = s->sum / s->count;
double var = s->sum_sq / s->count - avg * avg;
double sd = var > 0.0 ? sqrt(var) : 0.0;
printf(" avg=%4.0fms min=%4ldms max=%4ldms sd=%3.0fms n=%d",
avg, s->min, s->max, sd, s->count);
}
if (s->timeouts) printf(" timeouts=%d", s->timeouts);
printf("\n");
}
/* ── Device setup ────────────────────────────────────────────────────────── */
static int setup_device(device_t *dev)
{
const char *ps[] = {"DBG STATE", NULL};
int ready = 0;
for (int i = 0; i < 10 && !ready; i++) {
dev_send(dev, "STATE\n");
if (lq_wait(&dev->q, ps, 3, NULL)) ready = 1;
}
if (!ready) return 0;
lq_drain(&dev->q);
dev_send(dev, "CLEARNVS\n");
if (!lq_wait(&dev->q, ps, 10, NULL)) return 0;
dev_send(dev, "SET ble=1\n");
if (!lq_wait(&dev->q, ps, 5, NULL)) return 0;
lq_drain(&dev->q);
return 1;
}
/* ── Measurement ─────────────────────────────────────────────────────────── */
static int g_iterations = DEFAULT_ITERS;
static int g_timeout_s = DEFAULT_TIMEOUT_S;
static int g_life_seq = 0; /* global counter for unique life values */
/*
* SET life=<val> on src, wait for dst to log PEER_RX carrying that value.
* Returns elapsed milliseconds, or -1 on timeout.
* The life value is chosen globally unique to avoid false matches from other
* devices that may be advertising stale values.
*/
static long measure_once(device_t *src, device_t *dst)
{
int val = LIFE_BASE + g_life_seq++;
char pat[32], cmd[32];
snprintf(pat, sizeof pat, "life=%d", val);
snprintf(cmd, sizeof cmd, "SET life=%d\n", val);
lq_drain(&dst->q);
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
dev_send(src, cmd);
const char *pats[] = {"DBG PEER_RX", pat, NULL};
int found = lq_wait(&dst->q, pats, g_timeout_s, NULL);
clock_gettime(CLOCK_MONOTONIC, &t1);
if (!found) return -1;
return (t1.tv_sec - t0.tv_sec) * 1000L + (t1.tv_nsec - t0.tv_nsec) / 1000000L;
}
/* ── Entry point ─────────────────────────────────────────────────────────── */
int main(int argc, char *argv[])
{
const char *ports[MAX_DEVS] = {NULL};
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--port1") && i + 1 < argc) ports[0] = argv[++i];
else if (!strcmp(argv[i], "--port2") && i + 1 < argc) ports[1] = argv[++i];
else if (!strcmp(argv[i], "--port3") && i + 1 < argc) ports[2] = argv[++i];
else if (!strcmp(argv[i], "--sim") && i + 1 < argc) g_sim_bin = argv[++i];
else if (!strcmp(argv[i], "--game-id") && i + 1 < argc) g_game_id = argv[++i];
else if (!strcmp(argv[i], "--iterations") && i + 1 < argc) g_iterations = atoi(argv[++i]);
else if (!strcmp(argv[i], "--timeout") && i + 1 < argc) g_timeout_s = atoi(argv[++i]);
else {
fprintf(stderr,
"usage: %s --port1 DEV [--port2 DEV] [--port3 DEV]\n"
" [--sim PATH] [--game-id ID]\n"
" [--iterations N] [--timeout S]\n", argv[0]);
return 1;
}
}
if (!ports[0]) {
fprintf(stderr, "error: --port1 is required\n");
return 1;
}
signal(SIGINT, sig_cleanup);
signal(SIGTERM, sig_cleanup);
/* ── Open devices ──────────────────────────────────────────────────── */
for (int i = 0; i < MAX_DEVS; i++) {
if (!ports[i]) break;
g_devs[g_ndevs].port = ports[i];
lq_init(&g_devs[g_ndevs].q);
g_devs[g_ndevs].fd = open_serial(ports[i]);
if (g_devs[g_ndevs].fd < 0) {
fprintf(stderr, "cannot open %s\n", ports[i]);
return 1;
}
pthread_create(&g_devs[g_ndevs].thr, NULL, serial_reader, &g_devs[g_ndevs]);
g_ndevs++;
}
if (g_ndevs < 2) {
fprintf(stderr, "error: need at least 2 devices (--port1 and --port2)\n");
return 1;
}
/* ── Print header ──────────────────────────────────────────────────── */
printf("\ncommeownder BLE latency benchmark\n");
printf("devices:");
for (int i = 0; i < g_ndevs; i++) printf(" %s", g_devs[i].port);
printf("\n");
printf("simulator: %s game-id: %s\n", g_sim_bin, g_game_id);
printf("iterations: %d per pair timeout: %ds\n\n", g_iterations, g_timeout_s);
/* ── Set up devices ────────────────────────────────────────────────── */
printf("Setting up devices:\n");
for (int i = 0; i < g_ndevs; i++) {
printf(" %s ... ", g_devs[i].port); fflush(stdout);
if (!setup_device(&g_devs[i])) {
printf("FAIL\n");
return 1;
}
printf("OK\n");
}
/* ── Start simulator ───────────────────────────────────────────────── */
printf("Starting simulator (LATSIM, life=40) ... ");
fflush(stdout);
g_sim_pid = sim_start();
if (g_sim_pid < 0) { printf("FAIL\n"); return 1; }
printf("OK (pid %d)\n", g_sim_pid);
/* ── Settle ────────────────────────────────────────────────────────── */
printf("Settling (%ds) ...\n\n", SETTLE_S);
sleep(SETTLE_S);
/* Drain all queues after settle so old PEER_RX lines don't skew results. */
for (int i = 0; i < g_ndevs; i++) lq_drain(&g_devs[i].q);
/* ── Run measurements ──────────────────────────────────────────────── */
int npairs = g_ndevs * (g_ndevs - 1);
stats_t pair_stats[MAX_DEVS][MAX_DEVS];
stats_t src_stats[MAX_DEVS];
stats_t dst_stats[MAX_DEVS];
stats_t overall;
for (int i = 0; i < MAX_DEVS; i++) {
for (int j = 0; j < MAX_DEVS; j++) stats_init(&pair_stats[i][j]);
stats_init(&src_stats[i]);
stats_init(&dst_stats[i]);
}
stats_init(&overall);
printf("Running measurements (%d pairs × %d iterations):\n", npairs, g_iterations);
for (int si = 0; si < g_ndevs; si++) {
for (int di = 0; di < g_ndevs; di++) {
if (si == di) continue;
printf(" %s → %s [", port_label(g_devs[si].port), port_label(g_devs[di].port));
fflush(stdout);
for (int iter = 0; iter < g_iterations; iter++) {
long ms = measure_once(&g_devs[si], &g_devs[di]);
if (ms < 0) {
pair_stats[si][di].timeouts++;
printf("T"); fflush(stdout);
} else {
stats_record(&pair_stats[si][di], ms);
printf("."); fflush(stdout);
}
}
printf("]\n");
stats_merge(&src_stats[si], &pair_stats[si][di]);
stats_merge(&dst_stats[di], &pair_stats[si][di]);
stats_merge(&overall, &pair_stats[si][di]);
}
}
/* ── Stop simulator ────────────────────────────────────────────────── */
if (g_sim_pid > 0) {
kill(g_sim_pid, SIGTERM);
waitpid(g_sim_pid, NULL, 0);
g_sim_pid = -1;
}
/* ── Report ────────────────────────────────────────────────────────── */
printf("\n────────────────────────────────────────────────────────────────\n");
printf("Pair statistics:\n");
for (int si = 0; si < g_ndevs; si++) {
for (int di = 0; di < g_ndevs; di++) {
if (si == di) continue;
char label[64];
snprintf(label, sizeof label, "%s → %s",
port_label(g_devs[si].port), port_label(g_devs[di].port));
stats_print(&pair_stats[si][di], label);
}
}
printf("\nPer-device as source:\n");
for (int i = 0; i < g_ndevs; i++) {
char label[64];
snprintf(label, sizeof label, "%s", port_label(g_devs[i].port));
stats_print(&src_stats[i], label);
}
printf("\nPer-device as receiver:\n");
for (int i = 0; i < g_ndevs; i++) {
char label[64];
snprintf(label, sizeof label, "%s", port_label(g_devs[i].port));
stats_print(&dst_stats[i], label);
}
printf("\nOverall (%d samples):\n", overall.count);
stats_print(&overall, "all pairs");
printf("────────────────────────────────────────────────────────────────\n\n");
g_stop = 1;
for (int i = 0; i < g_ndevs; i++) {
pthread_join(g_devs[i].thr, NULL);
close(g_devs[i].fd);
}
return (overall.count == 0) ? 1 : 0;
}