/* * pog422_uartd — exclusive UART owner for POG422 fixture (Pi + ttyXRUSB*). * * ./pog422_uartd --port /dev/ttyXRUSB1 --socket /tmp/pog422_uart_4.sock * * Python connects via pog422_uart_client.py (Unix stream socket). */ #include "pog422_ipc.h" #include "pog422_inject.h" #include "pog422_uartd_hs_diag.h" #include "pog422_hw_debug.h" #include "pog422_timing_trace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef CMSPAR #error "CMSPAR required — build on Linux with POG422 wide/preciseflags UART support" #endif #define CAP_BUF_MAX (16 * 1024) #define IPC_IO_MAX (64 * 1024) #define CAP_PROMISC_IDLE_NS 2000000ull /* flush idle window after 2 ms quiet (complete pkt) */ #define CAP_INCOMPLETE_PACKET_IDLE_NS 50000000ull /* wait for rest of pkt without EOP */ #define CAP_CHAIN_FOLLOW_NS 15000000ull /* collect 81/89 follow-on after brief HS gap */ #define PENDING_RX_MAX 2048u #define PENDING_STALE_NS 30000000ull /* drop orphan pre-HS bytes after 30 ms */ /* Match Python POST_X9_INJECT_LOCAL_TID_FIFO_GRACE_MS — FIFO poll without HS. */ /* EG 3.0.1.54: restore 25 ms grace (.53 20 ms hunt caused continuous NO_X1/MRT). */ #define POST_X9_INJECT_LOCAL_GRACE_NS 25000000ull #define UART_REOPEN_INTERVAL_NS 250000000ull /* 250 ms */ #define UART_REOPEN_LOG_INTERVAL_NS 5000000000ull /* 5 s */ #define POG422_SOH 0x05u #define POG422_EOP 0x0Du /* * CMSPAR read value for the first MARK byte on Master/Slave (ttyXRUSB1). * Capture rule: discard SPACE bytes before the first MARK on the wire (wake bit). * Master/Slave MARK always reads as 0x05; OIS (ttyXRUSB0) may use another value. */ #define POG422_MARK_START_MACHINE POG422_SOH static volatile sig_atomic_t g_stop = 0; static void on_signal(int sig) { (void)sig; g_stop = 1; } static uint64_t mono_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; } static void fail_neg_errno(const char *what, int rc) { if (rc < 0) { fprintf(stderr, "%s: %s\n", what, strerror(-rc)); } else { fprintf(stderr, "%s: failed (%d)\n", what, rc); } } static int configure_port(int fd) { struct termios tio; if (tcgetattr(fd, &tio) != 0) { return -errno; } /* * Raw 57600 9N1 via Exar CS5→9-bit quirk + wide/preciseflags (hat_init). * XR wide RX: flag byte bit0 is the 9th data bit only in 9-bit mode; * in 8-bit wide mode bit0 is parity-*error* (always 0 with parity off) — * that made wide_rx.bin look like MARK was missing on native SCR. */ tio.c_cflag |= (CLOCAL | CREAD); tio.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ECHONL | ISIG | IEXTEN); tio.c_oflag &= ~(OPOST | ONLCR | OCRNL); tio.c_iflag &= ~(INLCR | IGNCR | ICRNL | IGNBRK | PARMRK | INPCK | ISTRIP); cfsetispeed(&tio, B57600); cfsetospeed(&tio, B57600); tio.c_cflag &= ~(CSIZE | CSTOPB | PARENB | PARODD | CMSPAR); tio.c_cflag |= CS5; /* driver maps CS5 → bDataBits=9 + trans9 + wide */ tio.c_cc[VMIN] = 0; tio.c_cc[VTIME] = 1; /* match pyserial timeout=0.1 → deciseconds */ if (tcsetattr(fd, TCSANOW, &tio) != 0) { return -errno; } (void)tcflush(fd, TCIFLUSH); int hat = pog422_hat_init(fd); if (hat != 0) { return hat; } return 0; } /* * EG 3.0.1.2: never block forever on a full client socket. A stuck write held * ipc_mu so GPIO7 abort ACK could not send (pos5 17:37 aborted_de=0) while the * inject worker sat in INJECT_DONE — Python timed out and barged a second 79. */ #define IPC_WRITE_DEADLINE_NS 100000000ull /* 100 ms */ static int write_all_fd_deadline(int fd, const void *buf, size_t len, uint64_t deadline_ns) { const uint8_t *p = (const uint8_t *)buf; size_t off = 0; int flags; int restore = 0; flags = fcntl(fd, F_GETFL, 0); if (flags >= 0 && (flags & O_NONBLOCK) == 0) { if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0) { restore = 1; } } while (off < len) { ssize_t n; uint64_t now = mono_ns(); if (now >= deadline_ns) { if (restore) { (void)fcntl(fd, F_SETFL, flags); } return -ETIMEDOUT; } n = write(fd, p + off, len - off); if (n > 0) { off += (size_t)n; continue; } if (n < 0 && errno == EINTR) { continue; } if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { struct pollfd pfd = {.fd = fd, .events = POLLOUT}; int rem_ms = (int)((deadline_ns - now) / 1000000ull); if (rem_ms < 1) { rem_ms = 1; } if (poll(&pfd, 1, rem_ms) <= 0) { if (restore) { (void)fcntl(fd, F_SETFL, flags); } return -ETIMEDOUT; } continue; } if (restore) { (void)fcntl(fd, F_SETFL, flags); } return n < 0 ? -errno : -EIO; } if (restore) { (void)fcntl(fd, F_SETFL, flags); } return 0; } static int write_all_fd(int fd, const void *buf, size_t len) { return write_all_fd_deadline(fd, buf, len, mono_ns() + IPC_WRITE_DEADLINE_NS); } static int send_ipc(int fd, uint32_t type, const void *payload, uint32_t payload_len) { uint32_t hdr[3] = {POG422_IPC_MAGIC, type, payload_len}; if (write_all_fd(fd, hdr, sizeof(hdr)) != 0) { return -1; } if (payload_len > 0 && payload) { if (write_all_fd(fd, payload, payload_len) != 0) { return -1; } } return 0; } static int read_exact(int fd, void *buf, size_t len) { uint8_t *p = (uint8_t *)buf; size_t off = 0; while (off < len) { ssize_t n = read(fd, p + off, len - off); if (n < 0) { if (errno == EINTR) { continue; } return -errno; } if (n == 0) { return -ECONNRESET; } off += (size_t)n; } return 0; } typedef struct { int uart_fd; int client_fd; char port_path[256]; char channel[16]; /* machine | ois — channel resolve for attach/reattach */ char first_port_hint[256]; /* --port: first-open try only */ bool first_port_tried; bool port_lost; /* detached; reopen loop active */ uint64_t last_reopen_try_ns; uint64_t last_reopen_log_ns; bool hs_active; bool cap_gpio_led; /* * Passive GPIOB1 capture: bytes stay in the XRUSB FIFO until SLV-HS falls, * then one burst read into cap_buf (RS485 tri-states when HS is inactive). * False during inject software windows — those still read incrementally. */ bool cap_burst_on_fall; bool inject_raw_capture; /* eth→UART inject: keep SPACE prefix, no MARK align */ bool dttty_rx; /* driver 1I: capture from DTtty ring, not read()/FIONREAD */ bool wide_log; /* EG 2.2.1.15: drain wide RX tee to file */ bool wide_thread_started; volatile sig_atomic_t wide_run; pthread_t wide_tid; int wide_log_fd; char wide_log_path[512]; uint8_t cap_buf[CAP_BUF_MAX]; size_t cap_len; uint64_t window_start_mono_ns; uint64_t first_byte_mono_ns; uint64_t last_byte_mono_ns; uint64_t bytes_discarded_inactive; uint8_t pending_rx[PENDING_RX_MAX]; size_t pending_len; uint64_t pending_first_mono_ns; uint64_t pending_last_mono_ns; /* After eth→UART inject ends with SLV-HS low: drain FIFO for local x1 (v16bf). */ uint64_t post_x9_grace_until_mono_ns; uint8_t post_x9_grace_soh_hold; /* 1 = lone SOH held across grace reads */ /* * EG 3.0.0.9: inject runs on a worker so main can service GPIO7_RELEASE / * GET_SLV_IN while INJECT_TX is blocked (pos5 02:51 orphan DE/HS). */ pthread_mutex_t ipc_mu; pthread_mutex_t inject_mu; pthread_cond_t inject_cv; pthread_t inject_tid; bool inject_thread_started; volatile sig_atomic_t inject_busy; volatile sig_atomic_t inject_abort; volatile sig_atomic_t inject_job_pending; uint8_t *inject_job_payload; uint32_t inject_job_len; bool inject_echo_flushed; /* early IPC_INJECT_ECHO already sent this burst */ uint32_t inject_expect_len; /* sent frame len — early-echo framed match */ /* EG 3.0.1.1: skip early ECHO IPC during auto-close (Python must not backpressure RT). */ bool inject_skip_early_echo_ipc; /* * EG 3.0.1.4: while local x9 auto-close is armed, copy local-TID x1 out of any * HS WINDOW flush (burst_on_fall) so peel still sees it — echo/post_de alone * missed Shamrock 51 that completed as uart_window (pos5 2026-07-28 MRT). */ bool ac_peel_armed; uint8_t ac_local_tid[2]; uint8_t ac_x1_stash[512]; size_t ac_x1_stash_len; uint8_t ac_x1_msg_stashed; /* * EG 3.0.1.5: after inject returns NO_X1 (or before late WINDOW), keep x9 so * main-loop HS/grace can still hard-RT synth 0D — C closes all the time. */ bool ac_pending_live; uint8_t ac_pending_x9[128]; uint32_t ac_pending_x9_len; uint8_t ac_pending_tid_tail[2]; uint64_t ac_pending_deadline_ns; /* EG 3.0.1.18: x1 already harvested but 0D waited/EBUSY — retry when HS idle. */ uint8_t ac_pending_od[14]; uint8_t ac_pending_od_len; /* 0 or 14 */ uint8_t ac_pending_x1_msg; uint8_t ac_pending_harvest; /* EG 3.0.1.6: runtime HS timing (POG422_TIMING_TRACE=1); no wire-path change. */ bool timing_on; FILE *timing_fp; char timing_path[512]; pog422_tt_rec_t timing_ring[POG422_TIMING_US_RING_MAX]; uint32_t timing_head; uint32_t timing_count; uint32_t timing_dropped; uint64_t timing_last_flush_ns; uint64_t timing_last_rtt_sample_ns; uint64_t timing_last_poll_ns; int timing_last_hs; uint32_t timing_kernel_unsupported; #if POG422_HW_DEBUG pog422_hwdbg_ring_t hwdbg; #endif } capture_state_t; static void timing_push( capture_state_t *st, uint32_t type_id, uint32_t a, uint32_t b, uint64_t c ) { uint32_t slot; if (!st || !st->timing_on) { return; } slot = st->timing_head; st->timing_ring[slot].mono_ns = mono_ns(); st->timing_ring[slot].type_id = type_id; st->timing_ring[slot].a = a; st->timing_ring[slot].b = b; st->timing_ring[slot].c = c; st->timing_head = (slot + 1u) % POG422_TIMING_US_RING_MAX; if (st->timing_count < POG422_TIMING_US_RING_MAX) { st->timing_count++; } else { st->timing_dropped++; } } static const char *timing_type_name(uint32_t type_id) { switch (type_id) { case POG422_TT_ID_GPIO_STAT_RTT: return POG422_TT_GPIO_STAT_RTT; case POG422_TT_ID_POLL_WAKE: return POG422_TT_POLL_WAKE; case POG422_TT_ID_HS_OBS: return POG422_TT_HS_OBS; case POG422_TT_ID_CAP_BEGIN: return POG422_TT_CAP_BEGIN; case POG422_TT_ID_CAP_END: return POG422_TT_CAP_END; case POG422_TT_ID_INJECT_START: return POG422_TT_INJECT_START; case POG422_TT_ID_DE_OFF: return POG422_TT_DE_OFF; case POG422_TT_ID_POST_DE: return POG422_TT_POST_DE; case POG422_TT_ID_X1_SEEN: return POG422_TT_X1_SEEN; case POG422_TT_ID_AUTO_CLOSE_START: return POG422_TT_AUTO_CLOSE_START; case POG422_TT_ID_AUTO_CLOSE_DONE: return POG422_TT_AUTO_CLOSE_DONE; case POG422_TT_ID_INJECT_DONE: return POG422_TT_INJECT_DONE; case POG422_TT_ID_KERNEL_GPIO: return POG422_TT_KERNEL_GPIO; case POG422_TT_ID_KERNEL_RX: return POG422_TT_KERNEL_RX; case POG422_TT_ID_DROP: return POG422_TT_DROP; default: return "us_unknown"; } } static void timing_flush(capture_state_t *st) { uint32_t n, start, i, dropped; if (!st || !st->timing_on || !st->timing_fp) { return; } n = st->timing_count; if (n == 0u && st->timing_dropped == 0u) { return; } start = (st->timing_head + POG422_TIMING_US_RING_MAX - st->timing_count) % POG422_TIMING_US_RING_MAX; for (i = 0; i < n; i++) { uint32_t idx = (start + i) % POG422_TIMING_US_RING_MAX; const pog422_tt_rec_t *r = &st->timing_ring[idx]; fprintf( st->timing_fp, "{\"v\":%u,\"src\":\"uartd\",\"type\":\"%s\",\"mono_ns\":%llu," "\"a\":%u,\"b\":%u,\"c\":%llu}\n", POG422_TIMING_TRACE_VERSION, timing_type_name(r->type_id), (unsigned long long)r->mono_ns, (unsigned)r->a, (unsigned)r->b, (unsigned long long)r->c ); } dropped = st->timing_dropped; st->timing_count = 0; st->timing_dropped = 0; if (dropped) { fprintf( st->timing_fp, "{\"v\":%u,\"src\":\"uartd\",\"type\":\"%s\",\"mono_ns\":%llu," "\"a\":%u,\"b\":0,\"c\":0}\n", POG422_TIMING_TRACE_VERSION, POG422_TT_DROP, (unsigned long long)mono_ns(), (unsigned)dropped ); } fflush(st->timing_fp); st->timing_last_flush_ns = mono_ns(); } static void timing_drain_kernel(capture_state_t *st) { struct xr_pog422_timing_event edges[64]; uint32_t out_n = 0; uint32_t dropped = 0; uint32_t i; int rc; if (!st || !st->timing_on || st->uart_fd < 0 || st->timing_kernel_unsupported) { return; } rc = pog422_timing_trace_drain(st->uart_fd, edges, 64u, &out_n, &dropped); if (rc == -EOPNOTSUPP || rc == -ENOTTY) { st->timing_kernel_unsupported = 1u; fprintf(stderr, "timing_trace: kernel ring unsupported (need driver 2.2.2.9+)\n"); return; } if (rc != 0) { return; } if (dropped) { timing_push(st, POG422_TT_ID_DROP, dropped, 1u, 0); } for (i = 0; i < out_n; i++) { if (edges[i].type == POG422_TIMING_EV_GPIO_INT) { timing_push( st, POG422_TT_ID_KERNEL_GPIO, edges[i].flags, ((uint32_t)edges[i].gpio_state << 16) | edges[i].gpio_int, edges[i].timestamp_ns ); } else if (edges[i].type == POG422_TIMING_EV_RX_URB) { timing_push( st, POG422_TT_ID_KERNEL_RX, edges[i].nbytes, (uint32_t)(edges[i].rx_chars & 0xffffffffu), edges[i].timestamp_ns ); } } } static int timing_env_enabled(void) { const char *e = getenv("POG422_TIMING_TRACE"); return e && e[0] && e[0] != '0' && strcasecmp(e, "false") != 0 && strcasecmp(e, "off") != 0; } static void timing_start(capture_state_t *st, const char *channel) { const char *path; if (!st || !timing_env_enabled()) { return; } path = getenv("POG422_TIMING_TRACE_PATH"); if (path && path[0]) { strncpy(st->timing_path, path, sizeof(st->timing_path) - 1u); } else { /* Land beside events.jsonl so sync_to_dropbox.sh carries it. CHANNEL=both * runs two daemons, so the channel must be in the name. */ const char *out_dir = getenv("OUTPUT_DIR"); snprintf( st->timing_path, sizeof(st->timing_path), "%s/hs_timing_%s.jsonl", (out_dir && out_dir[0]) ? out_dir : "data", (channel && channel[0]) ? channel : "machine" ); } { char dir[512]; char *slash; strncpy(dir, st->timing_path, sizeof(dir) - 1u); slash = strrchr(dir, '/'); if (slash) { *slash = '\0'; if (dir[0]) { (void)mkdir(dir, 0755); } } } st->timing_fp = fopen(st->timing_path, "a"); if (!st->timing_fp) { fprintf(stderr, "timing_trace: open %s failed: %s\n", st->timing_path, strerror(errno)); return; } st->timing_on = true; st->timing_last_hs = -1; st->timing_last_poll_ns = mono_ns(); fprintf( st->timing_fp, "{\"v\":%u,\"src\":\"uartd\",\"type\":\"%s\",\"mono_ns\":%llu," "\"eg\":\"%s\",\"note\":\"host_urb_times_not_copper\"," "\"path\":\"%s\"}\n", POG422_TIMING_TRACE_VERSION, POG422_TT_HDR, (unsigned long long)mono_ns(), POG422_EG_VERSION, st->timing_path ); fflush(st->timing_fp); fprintf(stderr, "pog422_uartd: TIMING_TRACE=1 path=%s\n", st->timing_path); } static void timing_stop(capture_state_t *st) { if (!st) { return; } if (st->timing_on) { timing_drain_kernel(st); timing_flush(st); if (st->uart_fd >= 0 && !st->timing_kernel_unsupported) { (void)pog422_timing_trace_set(st->uart_fd, 0); } } if (st->timing_fp) { fclose(st->timing_fp); st->timing_fp = NULL; } st->timing_on = false; } static void timing_on_uart_attached(capture_state_t *st) { int rc; if (!st || !st->timing_on || st->uart_fd < 0) { return; } rc = pog422_timing_trace_set(st->uart_fd, 1); if (rc == -EOPNOTSUPP || rc == -ENOTTY) { st->timing_kernel_unsupported = 1u; fprintf(stderr, "timing_trace: kernel enable failed (%d) — userspace-only\n", rc); } else if (rc != 0) { fprintf(stderr, "timing_trace: kernel enable err=%d\n", rc); } } /* Serialize all client-bound IPC (main + inject worker). */ static int send_ipc_st(capture_state_t *st, uint32_t type, const void *payload, uint32_t payload_len) { int rc; if (!st || st->client_fd < 0) { return -1; } pthread_mutex_lock(&st->ipc_mu); rc = send_ipc(st->client_fd, type, payload, payload_len); pthread_mutex_unlock(&st->ipc_mu); return rc; } static void hwdbg_harvest_driver_edges(capture_state_t *st) { #if POG422_HW_DEBUG struct xr_pog422_hwdbg_edge edges[64]; struct xr_pog422_hwdbg_drain req; uint32_t i; if (!st || st->uart_fd < 0) { return; } memset(&req, 0, sizeof(req)); req.user_buf = (uint64_t)(uintptr_t)edges; req.max_out = (uint32_t)(sizeof(edges) / sizeof(edges[0])); if (ioctl(st->uart_fd, XR_USB_SERIAL_DRAIN_HWDBG, &req) != 0) { return; /* prod driver or no edges */ } for (i = 0; i < req.out_n; i++) { uint8_t type = POG422_HWDBG_GPIO1_EDGE; if (edges[i].flags & POG422_HWDBG_F_GPIO5) { type = POG422_HWDBG_GPIO5_EDGE; } HWDBG_EMIT_AT( &st->hwdbg, type, edges[i].flags, ((uint32_t)edges[i].gpio_state << 16) | edges[i].gpio_int, edges[i].timestamp_ns ); } #else (void)st; #endif } static void flush_hwdbg_to_client(capture_state_t *st) { #if POG422_HW_DEBUG pog422_hwdbg_sample_t samples[POG422_HWDBG_RING_MAX]; uint32_t dropped = 0; uint32_t n; uint32_t plen; uint8_t *payload; pog422_ipc_hwdbg_hdr_t hdr; if (!st || st->client_fd < 0) { return; } hwdbg_harvest_driver_edges(st); n = pog422_hwdbg_ring_drain(&st->hwdbg, samples, POG422_HWDBG_RING_MAX, &dropped); if (n == 0 && dropped == 0) { return; } hdr.n = n; hdr.dropped = dropped; plen = (uint32_t)(sizeof(hdr) + n * sizeof(samples[0])); payload = malloc(plen); if (!payload) { return; } memcpy(payload, &hdr, sizeof(hdr)); if (n > 0) { memcpy(payload + sizeof(hdr), samples, n * sizeof(samples[0])); } send_ipc_st(st, POG422_IPC_HWDBG, payload, plen); free(payload); #else (void)st; #endif } static void flush_window_to_client(capture_state_t *st); static void cap_begin_window(capture_state_t *st, uint64_t now, bool gpio_led); static void cap_end_window(capture_state_t *st, uint64_t now); static void cap_abort_window(capture_state_t *st, uint64_t now); static void post_x9_grace_arm(capture_state_t *st, uint64_t now); static void post_x9_grace_clear(capture_state_t *st); static void post_x9_grace_poll(capture_state_t *st, uint64_t now); static int uart_rx_pending_count(capture_state_t *st); static int uart_rx_available(capture_state_t *st); static ssize_t uart_rx_pull(capture_state_t *st, uint8_t *tmp, size_t cap); static int cap_append(capture_state_t *st, const uint8_t *data, size_t n, uint64_t mono); static void capture_feed_uart_chunk( capture_state_t *st, const uint8_t *tmp, size_t chunk, uint64_t byte_mono, uint64_t now, int hs ); static void cap_drain_uart(capture_state_t *st, uint64_t now, int hs); static void cap_pre_flush_chain_drain(capture_state_t *st, uint64_t now); static int slv_in_read(int uart_fd); static int uart_attach(capture_state_t *st); static void uart_detach(capture_state_t *st); static void wide_log_stop(capture_state_t *st); static void uart_mark_lost(capture_state_t *st, const char *why); static void uart_try_reopen(capture_state_t *st); static int uart_initial_attach(capture_state_t *st); static int uart_fd_is_dead(int fd); static int errno_is_uart_dead(int e); static int handle_uart_gate(capture_state_t *st, uint32_t ack_type, int (*action)(capture_state_t *)); #if POG422_UARTD_HS_DIAG static uint32_t g_hs_diag_drain_bytes; static int g_rx_stats_ioctl_warned; static void hs_diag_fill_probe(const uint8_t *buf, size_t n, pog422_ipc_hs_diag_t *d) { static const uint8_t marks[] = {0x9Du, 0xF2u}; memset(d->probe, 0, sizeof(d->probe)); d->probe_off = 0; d->probe_len = 0; if (!buf || n == 0) { return; } for (size_t mi = 0; mi < sizeof(marks); mi++) { for (size_t i = 0; i < n; i++) { if (buf[i] != marks[mi] || i + 2 >= n) { continue; } d->probe_off = (uint8_t)i; d->probe_len = (uint8_t)((n - i) < 8 ? (n - i) : 8); memcpy(d->probe, buf + i, d->probe_len); return; } } d->probe_len = (uint8_t)(n < 8 ? n : 8); memcpy(d->probe, buf, d->probe_len); } static void hs_diag_emit(capture_state_t *st, uint64_t now, uint8_t phase) { pog422_ipc_hs_diag_t d; if (!st || st->client_fd < 0 || !st->cap_burst_on_fall) { return; } memset(&d, 0, sizeof(d)); d.mono_ns = now; d.phase = phase; d.cap_burst_on_fall = 1; d.slv_in = (uint8_t)(slv_in_read(st->uart_fd) ? 1 : 0); d.fifo_avail = (uint32_t)uart_rx_pending_count(st); { uint32_t hw = 0; if (pog422_hat_read_rx_fifo_count(st->uart_fd, 2, &hw) == 0) { d.hw_fifo_avail = hw; } } d.cap_len = (uint32_t)st->cap_len; d.drain_bytes = g_hs_diag_drain_bytes; { struct xr_pog422_rx_stats rxst; if (pog422_hat_read_rx_stats(st->uart_fd, &rxst) == 0) { d.driver_rx_chars = rxst.chars_inserted; } else if (!g_rx_stats_ioctl_warned) { g_rx_stats_ioctl_warned = 1; fprintf( stderr, "hs_fifo_diag: GET_RX_STATS ioctl failed — driver < 1H? " "(run: sudo bash xr-usb-serial-pog422/install_pi.sh)\n" ); } } if (phase == POG422_HS_DIAG_FALL_POST) { hs_diag_fill_probe(st->cap_buf, st->cap_len, &d); } fprintf( stderr, "hs_fifo_diag phase=%u slv_in=%u dttty_fifo=%u hw_fifo=%u cap_len=%u drain=%u drv_rx=%llu probe_off=%u probe=%02X%02X%02X%02X\n", (unsigned)d.phase, (unsigned)d.slv_in, (unsigned)d.fifo_avail, (unsigned)d.hw_fifo_avail, (unsigned)d.cap_len, (unsigned)d.drain_bytes, (unsigned long long)d.driver_rx_chars, (unsigned)d.probe_off, d.probe_len > 0 ? d.probe[0] : 0, d.probe_len > 1 ? d.probe[1] : 0, d.probe_len > 2 ? d.probe[2] : 0, d.probe_len > 3 ? d.probe[3] : 0 ); send_ipc_st(st, POG422_IPC_HS_DIAG, &d, sizeof(d)); if (phase == POG422_HS_DIAG_FALL_PRE) { g_hs_diag_drain_bytes = 0; } } #endif /* POG422_UARTD_HS_DIAG */ static void emit_discard(capture_state_t *st, uint32_t count, uint8_t first_byte) { if (count == 0 || st->client_fd < 0) { return; } pog422_ipc_discard_t ev = { .count = count, .first_byte = first_byte, }; send_ipc_st(st, POG422_IPC_DISCARD, &ev, sizeof(ev)); } static void cap_reset_window(capture_state_t *st) { st->cap_len = 0; st->window_start_mono_ns = 0; st->first_byte_mono_ns = 0; st->last_byte_mono_ns = 0; } static void cap_clear_buf(capture_state_t *st) { st->cap_len = 0; st->first_byte_mono_ns = 0; st->last_byte_mono_ns = 0; } static void pending_reset(capture_state_t *st) { st->pending_len = 0; st->pending_first_mono_ns = 0; st->pending_last_mono_ns = 0; } static size_t buf_find_mark(const uint8_t *data, size_t n) { for (size_t i = 0; i < n; i++) { if (data[i] == POG422_MARK_START_MACHINE) { return i; } } return n; } static int buf_has_eop(const uint8_t *data, size_t n) { for (size_t i = 0; i < n; i++) { if (data[i] == POG422_EOP) { return 1; } } return 0; } static int pending_append(capture_state_t *st, const uint8_t *data, size_t n, uint64_t mono) { if (n == 0) { return 0; } if (st->pending_len + n > PENDING_RX_MAX) { return -ENOSPC; } if (st->pending_len == 0) { st->pending_first_mono_ns = mono; } memcpy(st->pending_rx + st->pending_len, data, n); st->pending_len += n; st->pending_last_mono_ns = mono; return 0; } static void pending_promote_to_cap(capture_state_t *st, uint64_t now, int gpio_led) { if (st->pending_len == 0) { return; } size_t mark = buf_find_mark(st->pending_rx, st->pending_len); if (mark > 0) { emit_discard(st, (uint32_t)mark, st->pending_rx[0]); st->bytes_discarded_inactive += mark; } if (mark >= st->pending_len) { pending_reset(st); return; } if (st->cap_len == 0 && !st->hs_active) { cap_begin_window(st, now, gpio_led); } { size_t use = st->pending_len - mark; uint64_t fb = st->pending_first_mono_ns ? st->pending_first_mono_ns : now; cap_append(st, st->pending_rx + mark, use, fb); if (st->pending_last_mono_ns > st->last_byte_mono_ns) { st->last_byte_mono_ns = st->pending_last_mono_ns; } } pending_reset(st); } static void pending_flush_complete(capture_state_t *st, uint64_t now) { if (st->pending_len == 0) { return; } if (st->pending_first_mono_ns > 0 && now - st->pending_first_mono_ns > PENDING_STALE_NS) { emit_discard(st, (uint32_t)st->pending_len, st->pending_rx[0]); st->bytes_discarded_inactive += st->pending_len; pending_reset(st); return; } if (!buf_has_eop(st->pending_rx, st->pending_len)) { return; } if (st->pending_last_mono_ns == 0 || now - st->pending_last_mono_ns < CAP_PROMISC_IDLE_NS) { return; } if (uart_rx_available(st)) { return; } pending_promote_to_cap(st, now, false); if (st->cap_len > 0) { cap_end_window(st, now); } } static int cap_append(capture_state_t *st, const uint8_t *data, size_t n, uint64_t mono) { if (n == 0) { return 0; } if (st->first_byte_mono_ns == 0) { st->first_byte_mono_ns = mono; #if POG422_HW_DEBUG HWDBG_EMIT_AT( &st->hwdbg, POG422_HWDBG_MARK, data[0], (uint32_t)n, mono ); #endif } #if POG422_HW_DEBUG HWDBG_EMIT_AT(&st->hwdbg, POG422_HWDBG_RX_CHUNK, data[0], (uint32_t)n, mono); #endif if (st->cap_len + n > CAP_BUF_MAX) { return -ENOSPC; } memcpy(st->cap_buf + st->cap_len, data, n); st->cap_len += n; st->last_byte_mono_ns = mono; return 0; } static size_t cap_find_mark_start(const capture_state_t *st) { for (size_t i = 0; i < st->cap_len; i++) { if (st->cap_buf[i] == POG422_MARK_START_MACHINE) { return i; } } return st->cap_len; } static int cap_has_eop(const capture_state_t *st) { for (size_t i = 0; i < st->cap_len; i++) { if (st->cap_buf[i] == POG422_EOP) { return 1; } } return 0; } /* * Align cap_buf to the first SOH (MARK) byte. * When discard_if_no_mark is set and no SOH is present, drop the whole buffer * (mid-frame tails from post-inject echo cannot be parsed). * Returns 1 when aligned (empty or SOH at index 0), 0 when discarded as garbage. */ static int cap_align_to_mark(capture_state_t *st, int discard_if_no_mark) { size_t mark; if (st->cap_len == 0) { return 1; } mark = cap_find_mark_start(st); if (mark == 0) { return 1; } if (mark < st->cap_len) { emit_discard(st, (uint32_t)mark, st->cap_buf[0]); st->bytes_discarded_inactive += mark; memmove(st->cap_buf, st->cap_buf + mark, st->cap_len - mark); st->cap_len -= mark; return 1; } if (!discard_if_no_mark) { return 0; } emit_discard(st, (uint32_t)st->cap_len, st->cap_buf[0]); st->bytes_discarded_inactive += st->cap_len; cap_reset_window(st); return 0; } static size_t cap_skip_to_mark(const uint8_t *data, size_t n) { for (size_t i = 0; i < n; i++) { if (data[i] == POG422_MARK_START_MACHINE) { return i; } } return n; } /* Live GPIO_STAT — not GET_GPIO1_EVENT cache (UART RX can beat IRQ to userspace). */ static int slv_in_read(int uart_fd) { uint8_t active = 0; if (pog422_slv_in_live(uart_fd, &active) != 0) { return 0; } return active ? 1 : 0; } static void cap_begin_window(capture_state_t *st, uint64_t now, bool gpio_led) { cap_reset_window(st); st->window_start_mono_ns = now; st->hs_active = true; st->cap_gpio_led = gpio_led; st->cap_burst_on_fall = gpio_led; timing_push(st, POG422_TT_ID_CAP_BEGIN, gpio_led ? 1u : 0u, 0u, now); #if POG422_HW_DEBUG hwdbg_harvest_driver_edges(st); HWDBG_EMIT_AT(&st->hwdbg, POG422_HWDBG_HS_RISE, gpio_led ? 1u : 0u, 0u, now); #endif if (st->client_fd >= 0) { pog422_ipc_hs_t ev = {.mono_ns = st->window_start_mono_ns}; send_ipc_st(st, POG422_IPC_HS_ACTIVE, &ev, sizeof(ev)); } #if POG422_UARTD_HS_DIAG if (gpio_led) { hs_diag_emit(st, now, POG422_HS_DIAG_RISE); } #endif } static void cap_abort_window(capture_state_t *st, uint64_t now) { #if POG422_HW_DEBUG HWDBG_EMIT_AT(&st->hwdbg, POG422_HWDBG_HS_FALL, 1u, 0u, now); flush_hwdbg_to_client(st); #endif if (st->client_fd >= 0 && st->hs_active) { pog422_ipc_hs_t ev = {.mono_ns = now}; send_ipc_st(st, POG422_IPC_HS_INACTIVE, &ev, sizeof(ev)); } cap_reset_window(st); st->hs_active = false; st->cap_gpio_led = false; st->cap_burst_on_fall = false; } /* Forward inject-window RX (TX echo) to Python before discarding the buffer. */ static void flush_inject_echo_to_client(capture_state_t *st) { if (st->client_fd < 0 || st->cap_len == 0 || st->inject_echo_flushed) { return; } send_ipc_st(st, POG422_IPC_INJECT_ECHO, st->cap_buf, (uint32_t)st->cap_len); st->inject_echo_flushed = true; } /* * EG 3.0.0.9: send echo as soon as a framed copy of the injected frame is in * the software window — arms Python done_extend before TEMT/DONE (pos5 02:51: * copper 79 ~150 ms in, but echo waited until burst end while DE stuck). */ static void maybe_early_inject_echo(capture_state_t *st) { size_t mark; if (!st || st->inject_echo_flushed || !st->inject_raw_capture) { return; } /* Hard-RT auto-close: keep echo in cap_buf; do not block on Python IPC. */ if (st->inject_skip_early_echo_ipc) { return; } if (!st->hs_active || st->cap_burst_on_fall) { return; } if (st->inject_expect_len == 0u || st->cap_len < (size_t)st->inject_expect_len) { return; } mark = cap_find_mark_start(st); if (mark >= st->cap_len) { return; } if (st->cap_len - mark < (size_t)st->inject_expect_len) { return; } if (st->cap_buf[mark] != POG422_SOH) { return; } if (st->cap_buf[mark + st->inject_expect_len - 1u] != POG422_EOP) { return; } flush_inject_echo_to_client(st); } /* * End the inject software window. Passive GPIOB1 burst capture takes over when * SLV-HS is still active after inject (FIFO left intact until HS falls). * * harvest_while_hs (EG 3.0.1.2): keep incremental RX while HS is already high * so auto-close can peel local x1. burst_on_fall holds FIFO until fall → NO_X1. */ static void cap_end_inject_window(capture_state_t *st, uint64_t now, int harvest_while_hs) { const bool was_inject_sw = st->hs_active && !st->cap_burst_on_fall; int live = slv_in_read(st->uart_fd); /* * Inject window: drain any trailing echo only when the bus is quiet. * If SLV-HS is still active, leave the FIFO intact for the HS-fall burst * (unless harvest_while_hs — then post-DE poll peels x1 immediately). */ if (was_inject_sw && !live) { cap_drain_uart(st, now, 0); } if (was_inject_sw) { /* Was discarded — now logged as inject_echo so we can see copper TX. */ if (st->cap_len > 0) { fprintf(stderr, "dttty_sink=inject_echo len=%u first=%02X last=%02X\n", (unsigned)st->cap_len, st->cap_buf[0], st->cap_buf[st->cap_len - 1]); } /* EG 3.0.1.1: auto-close path sends ECHO after DONE (no Python backpressure). */ if (!st->inject_skip_early_echo_ipc) { flush_inject_echo_to_client(st); } st->inject_echo_flushed = false; st->inject_expect_len = 0u; cap_clear_buf(st); pending_reset(st); if (st->client_fd >= 0) { pog422_ipc_hs_t ev = {.mono_ns = now}; send_ipc_st(st, POG422_IPC_HS_INACTIVE, &ev, sizeof(ev)); } } if (live) { post_x9_grace_clear(st); if (harvest_while_hs) { /* Incremental peel window — do not arm burst_on_fall yet. */ st->window_start_mono_ns = now; st->hs_active = true; st->cap_gpio_led = true; st->cap_burst_on_fall = false; if (st->client_fd >= 0) { pog422_ipc_hs_t ev = {.mono_ns = st->window_start_mono_ns}; send_ipc_st(st, POG422_IPC_HS_ACTIVE, &ev, sizeof(ev)); } fprintf(stderr, "cap_end_inject harvest_while_hs=1\n"); return; } cap_reset_window(st); st->window_start_mono_ns = now; st->hs_active = true; st->cap_gpio_led = true; st->cap_burst_on_fall = true; if (st->client_fd >= 0) { pog422_ipc_hs_t ev = {.mono_ns = st->window_start_mono_ns}; send_ipc_st(st, POG422_IPC_HS_ACTIVE, &ev, sizeof(ev)); } return; } st->hs_active = false; st->cap_gpio_led = false; st->cap_burst_on_fall = false; /* * Quiet bus after inject: arm 25 ms FIFO grace (pyserial v16bf / local x1). * EG 2.2.2.41: handle_inject re-arms after flush_post_de so the grace wall * does not expire during the 25+15 ms post-DE poll (Class B). */ post_x9_grace_arm(st, now); } /* * After GPIO7 DE drops, XR may unmute RX. Poll into a separate buffer so * post_de_rx is distinct from inject-window echo (often empty while DE high). * * EG 3.0.1.54: restore pre-.53 peel windows. Hard 20 ms hunt (.53) mass NO_X1 * + Python 0D before Master/retry x1 could complete — continuous glass MRT. */ #define POST_DE_RX_POLL_NS 25000000ull /* 25 ms — local x1 after x9 */ #define POST_DE_RX_IDLE_SLICE_NS 200000ull #define POST_DE_RX_SECONDARY_NS 15000000ull /* 15 ms — secondary drain for x1 ACK */ /* EG 3.0.1.44: late WINDOW after NO_X1 (was 200 ms) — keep recovery open. */ #define AUTO_CLOSE_PENDING_NS 100000000ull /* 100 ms */ /* EG 3.0.1.17: first *activity* gate, then hard wall from DE-off. */ #define X9_X1_FIRST_BYTE_NS 20000000ull /* 20 ms quiet exit if no RX */ #define X9_X1_HARD_CAP_NS 45000000ull /* 45 ms wall from DE-off */ /* * EG 3.0.1.52: after x1 harvested, brief HS-idle for synth 0D (was 150). */ #define AUTO_CLOSE_0D_IDLE_WAIT_MS 25u /* DE-high echo is raw frame bytes; phase is implied by IPC type (vs POST_DE_RX). */ #define POG422_ECHO_PHASE_DE_HIGH 1u #define POG422_ECHO_PHASE_POST_DE 2u /* * EG 3.0.2.4/5: brief park while waiting for peel RX. * Do NOT block on poll(uart_fd): when dttty_rx / wide ring is in use, POLLIN on * the tty fd does not track XR DTtty fills — soak 1549 floored * peel_first_byte_us at ~20063 (full 20 ms first-byte gate) and made spoke * post_burst worse than 0223. short nanosleep + uart_rx_pull remains event- * driven on successful reads. */ static void peel_wait_rx(int fd, uint64_t until_ns) { uint64_t now; struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000}; (void)fd; now = mono_ns(); if (now >= until_ns) { return; } (void)nanosleep(&ts, NULL); } static int post_de_buf_has_eop(const uint8_t *buf, size_t len) { size_t i; for (i = 0; i < len; i++) { if (buf[i] == POG422_EOP) { return 1; } } return 0; } static int post_de_frame_complete(const uint8_t *buf, size_t len) { if (len == 0) { return 1; /* nothing pending */ } if (buf[0] != POG422_SOH) { return 1; /* not a framed start — do not stall inject */ } return post_de_buf_has_eop(buf, len); } static int post_de_eop_count(const uint8_t *buf, size_t len) { int n = 0; size_t i; for (i = 0; i < len; i++) { if (buf[i] == POG422_EOP) n++; } return n; } /* True when inject-window RX already has a complete peer frame after *sent*. */ static int inject_echo_has_peer_after_sent( const uint8_t *echo, size_t elen, const uint8_t *sent, uint32_t slen) { size_t off; size_t i; int eops = 0; if (!echo || elen == 0u) { return 0; } if (sent && slen > 0u && elen > (size_t)slen && memcmp(echo, sent, (size_t)slen) == 0) { off = (size_t)slen; while (off < elen && echo[off] != POG422_SOH) { off++; } for (i = off; i < elen; i++) { if (echo[i] == POG422_EOP) { return 1; } } return 0; } for (i = 0; i < elen; i++) { if (echo[i] == POG422_EOP) { eops++; } } return eops >= 2; } /* Forward decls — auto-close helpers sit above some capture helpers. */ static int is_gameplay_ack_msg(uint8_t msg); static void inject_capture_poll(void *ctx); static int find_local_x1_ack( const uint8_t *buf, size_t len, const uint8_t local_tid[2], uint8_t *out_msg); /* * Collect post-DE RX into *out (optional). Returns byte count. * Does not send IPC — caller decides when (after hard-RT auto-close). * When local_tid non-NULL, exit as soon as a complete local gameplay x1 is * present (EG 3.0.2.4 — no 15 ms secondary drain for that ACK). */ static size_t collect_post_de_rx( capture_state_t *st, uint8_t *out, size_t out_cap, const uint8_t *local_tid) { size_t len = 0; uint64_t deadline; uint64_t last_rx = 0; int secondary_armed = 0; uint64_t secondary_deadline = 0; int hs_at_start; uint8_t x1_msg = 0; if (!st || st->uart_fd < 0 || !out || out_cap == 0) { return 0; } hs_at_start = slv_in_read(st->uart_fd); if (hs_at_start && st->cap_burst_on_fall) { fprintf(stderr, "post_de_vs_hs_steal hs=1 cap_burst=1 t_mono=%llu\n", (unsigned long long)mono_ns()); } deadline = mono_ns() + POST_DE_RX_POLL_NS; while (mono_ns() < deadline && len < out_cap) { uint8_t tmp[512]; ssize_t n = uart_rx_pull(st, tmp, sizeof(tmp)); if (n > 0) { size_t chunk = (size_t)n; if (chunk > out_cap - len) { chunk = out_cap - len; } memcpy(out + len, tmp, chunk); len += chunk; last_rx = mono_ns(); if (local_tid && find_local_x1_ack(out, len, local_tid, &x1_msg)) { break; } continue; } if (len > 0 && last_rx != 0 && (mono_ns() - last_rx) >= POST_DE_RX_IDLE_SLICE_NS && post_de_frame_complete(out, len)) { if (local_tid && find_local_x1_ack(out, len, local_tid, &x1_msg)) { break; } if (!secondary_armed && post_de_eop_count(out, len) == 1) { secondary_armed = 1; secondary_deadline = mono_ns() + POST_DE_RX_SECONDARY_NS; if (secondary_deadline > deadline) secondary_deadline = deadline; last_rx = 0; continue; } break; } if (secondary_armed && mono_ns() >= secondary_deadline) { break; } peel_wait_rx(st->uart_fd, secondary_armed ? secondary_deadline : deadline); } if (len == 1 && out[0] == POG422_SOH) { st->post_x9_grace_soh_hold = 1; fprintf(stderr, "post_de_hold lone soh for grace\n"); len = 0; } if (len > 0) { fprintf(stderr, "dttty_sink=post_de len=%u first=%02X last=%02X hs_start=%d\n", (unsigned)len, out[0], out[len - 1], hs_at_start); } return len; } static void flush_post_de_rx_to_client(capture_state_t *st) { uint8_t buf[CAP_BUF_MAX]; size_t len; if (st->client_fd < 0) { return; } len = collect_post_de_rx(st, buf, sizeof(buf), NULL); send_ipc_st(st, POG422_IPC_POST_DE_RX, buf, (uint32_t)len); } /* CRC-16-CCITT (poly 0x1021, init 0xFFFF) — matches Python pog422_crc16_ccitt. */ static uint16_t pog422_crc16_ccitt(const uint8_t *data, size_t len) { uint16_t crc = 0xFFFFu; size_t i; int b; for (i = 0; i < len; i++) { crc ^= (uint16_t)data[i] << 8; for (b = 0; b < 8; b++) { if (crc & 0x8000u) { crc = (uint16_t)((crc << 1) ^ 0x1021u); } else { crc = (uint16_t)(crc << 1); } } } return crc; } static int build_status_0d_from_x9( const uint8_t *x9, uint32_t x9_len, const uint8_t tid_tail[2], uint8_t out[14]) { uint16_t crc; if (!x9 || x9_len < 18u || !tid_tail || !out) { return -1; } if (x9[0] != POG422_SOH || (x9[1] & 0x0Fu) != 0x09u) { return -1; } out[0] = POG422_SOH; out[1] = 0x0Du; out[2] = 0x01u; out[3] = 0x00u; out[4] = 0x00u; out[5] = x9[5]; out[6] = x9[6]; out[7] = 0x00u; out[8] = x9[17]; out[9] = tid_tail[0]; out[10] = tid_tail[1]; out[13] = POG422_EOP; crc = pog422_crc16_ccitt(out, 11u); out[11] = (uint8_t)((crc >> 8) & 0xFFu); out[12] = (uint8_t)(crc & 0xFFu); return 0; } /* * Extract local-TID gameplay ACK (x1) SOH..EOP from bus RX. * Wire TID at bytes 3-4. Returns 1 on hit; copies frame when out_frame set. */ /* * Gameplay ACK wire length (0x21/31/41/51/61/71/81). Must match * pog422_wire_len_primary() / Python COMMAND_WIRE_LEN — never first-EOP hunt: * EOP is 0x0D, so a short SOH..early-0x0D false "frame" (6 B) previously * poisoned IPC_X1_ACK_FRAME + early eth-forward (EG 3.0.1.20 / pos2 SpinBall). */ #define POG422_GAMEPLAY_ACK_WIRE_LEN 12u static int extract_local_x1_frame( const uint8_t *buf, size_t len, const uint8_t local_tid[2], uint8_t *out_msg, uint8_t *out_frame, size_t out_cap, size_t *out_frame_len) { size_t i; if (out_frame_len) { *out_frame_len = 0; } if (!buf || len < POG422_GAMEPLAY_ACK_WIRE_LEN || !local_tid || !out_msg) { return 0; } for (i = 0; i + POG422_GAMEPLAY_ACK_WIRE_LEN <= len; i++) { uint8_t msg; size_t flen; if (buf[i] != POG422_SOH) { continue; } msg = buf[i + 1u]; if (!is_gameplay_ack_msg(msg)) { continue; } if (buf[i + 3u] != local_tid[0] || buf[i + 4u] != local_tid[1]) { continue; } flen = POG422_GAMEPLAY_ACK_WIRE_LEN; if (buf[i + flen - 1u] != POG422_EOP) { continue; } *out_msg = msg; if (out_frame && out_frame_len && out_cap > 0u) { if (flen > out_cap) { flen = out_cap; } memcpy(out_frame, buf + i, flen); *out_frame_len = flen; } return 1; } return 0; } /* Find local-TID gameplay ACK (x1) in bus RX. Returns 1 and sets *out_msg. */ static int find_local_x1_ack( const uint8_t *buf, size_t len, const uint8_t local_tid[2], uint8_t *out_msg) { return extract_local_x1_frame( buf, len, local_tid, out_msg, NULL, 0, NULL); } /* * EG 3.0.1.17 / 3.0.1.54: after local auto-close x9 DE-off, peel UART RX with no * HS gate. First activity gate 20 ms when quiet; hard wall ~45 ms from DE-off. * Do not short this to 20 ms hard — field .53 continuous NO_X1/MRT. * EG 3.0.2.4: poll for RX; optional *out_first_us / *out_hit_us peel marks. */ static size_t collect_immediate_x1_peel( capture_state_t *st, const uint8_t local_tid[2], uint8_t *out, size_t out_cap, uint16_t *out_first_us, uint16_t *out_hit_us) { size_t len = 0; uint64_t t0; uint64_t first_deadline; uint64_t hard_cap; int saw_activity = 0; uint8_t x1_msg = 0; if (out_first_us) { *out_first_us = 0u; } if (out_hit_us) { *out_hit_us = 0u; } if (!st || st->uart_fd < 0 || !out || out_cap == 0u || !local_tid) { return 0; } /* DE must already be RX; force in case caller path raced. */ (void)pog422_gpio7_rx(st->uart_fd); t0 = mono_ns(); first_deadline = t0 + X9_X1_FIRST_BYTE_NS; hard_cap = t0 + X9_X1_HARD_CAP_NS; while (mono_ns() < hard_cap && len < out_cap) { uint8_t tmp[512]; ssize_t n = uart_rx_pull(st, tmp, sizeof(tmp)); uint64_t now = mono_ns(); if (n > 0) { size_t chunk = (size_t)n; if (chunk > out_cap - len) { chunk = out_cap - len; } memcpy(out + len, tmp, chunk); len += chunk; if (!saw_activity) { saw_activity = 1; if (out_first_us) { uint64_t us = (now - t0) / 1000ull; *out_first_us = us > 0xffffull ? 0xffffu : (uint16_t)us; } } if (find_local_x1_ack(out, len, local_tid, &x1_msg)) { if (out_hit_us) { uint64_t us = (now - t0) / 1000ull; *out_hit_us = us > 0xffffull ? 0xffffu : (uint16_t)us; } fprintf(stderr, "imm_x1_peel hit len=%u msg=%02X elapsed_us=%llu\n", (unsigned)len, x1_msg, (unsigned long long)((now - t0) / 1000ull)); return len; } /* Keep peeling for local x1 — non-ACK frames do not end the hunt. */ continue; } if (!saw_activity && now >= first_deadline) { break; } { uint64_t wait_until = hard_cap; if (!saw_activity && first_deadline < hard_cap) { wait_until = first_deadline; } peel_wait_rx(st->uart_fd, wait_until); } } if (len > 0) { fprintf(stderr, "imm_x1_peel miss len=%u first=%02X last=%02X activity=%d\n", (unsigned)len, out[0], out[len - 1u], saw_activity); } return len; } /* * EG 3.0.1.52: peel local x1 + build 0D only — does NOT hold inject on 0D TX. * On success: out.{x1_msg,harvest} set, od[14] filled, X1_ACK IPC sent. */ static int auto_close_harvest_x1( capture_state_t *st, const uint8_t *x9, uint32_t x9_len, const pog422_ipc_auto_close_t *ac, const uint8_t *echo, size_t echo_len, const uint8_t *post_de, size_t post_de_len, const uint8_t *imm, size_t imm_len, const uint8_t *hs_rx, size_t hs_rx_len, pog422_ipc_auto_close_done_t *out, uint8_t od_out[14]) { uint8_t local_tid[2]; uint8_t tid_tail[2]; uint8_t x1_msg = 0; uint8_t harvest = 0; uint8_t x1_frame[256]; size_t x1_frame_len = 0; memset(out, 0, sizeof(*out)); out->err = POG422_AUTO_CLOSE_NO_X1; if (!st || !x9 || !ac || !ac->enable || !od_out) { return 0; } st->ac_pending_od_len = 0u; local_tid[0] = ac->local_tid0; local_tid[1] = ac->local_tid1; tid_tail[0] = ac->tid_tail0; tid_tail[1] = ac->tid_tail1; if (extract_local_x1_frame( echo, echo_len, local_tid, &x1_msg, x1_frame, sizeof(x1_frame), &x1_frame_len)) { harvest = 1; } else if (extract_local_x1_frame( imm, imm_len, local_tid, &x1_msg, x1_frame, sizeof(x1_frame), &x1_frame_len)) { harvest = 4; /* immediate post-x9 RX peel — EG 3.0.1.17 */ } else if (extract_local_x1_frame( post_de, post_de_len, local_tid, &x1_msg, x1_frame, sizeof(x1_frame), &x1_frame_len)) { harvest = 2; } else if (extract_local_x1_frame( hs_rx, hs_rx_len, local_tid, &x1_msg, x1_frame, sizeof(x1_frame), &x1_frame_len)) { harvest = 3; /* HS burst / WINDOW / FIFO — EG 3.0.1.4 */ } else { return 0; } timing_push(st, POG422_TT_ID_X1_SEEN, x1_msg, harvest, mono_ns()); timing_push(st, POG422_TT_ID_AUTO_CLOSE_START, x1_msg, harvest, 0); if (build_status_0d_from_x9(x9, x9_len, tid_tail, od_out) != 0) { out->err = POG422_AUTO_CLOSE_BUILD_FAIL; out->x1_msg = x1_msg; out->harvest = harvest; return 0; } /* * EG 3.0.1.20: push full local x1 to Python before 0D so eth→Master can * forward while DE/UART free after INJECT_DONE (EG 3.0.1.52). */ if (x1_frame_len > 0u && st->client_fd >= 0) { if (send_ipc_st( st, POG422_IPC_X1_ACK_FRAME, x1_frame, (uint32_t)x1_frame_len) == 0) { fprintf(stderr, "x1_ack_frame_ipc len=%u msg=%02X harvest=%u\n", (unsigned)x1_frame_len, x1_msg, (unsigned)harvest); } } out->x1_msg = x1_msg; out->counter = od_out[8]; out->harvest = harvest; out->err = POG422_AUTO_CLOSE_OK; /* harvest ok; 0D may still fail async */ return 1; } /* * EG 3.0.1.52: synth 0D after INJECT_DONE — short idle wait only (x1 already owned). * Returns 1 on copper 0D success. */ static int auto_close_tx_0d( capture_state_t *st, const uint8_t od[14], pog422_ipc_auto_close_done_t *out) { pog422_inject_opts_t od_opts; pog422_inject_result_t od_res; if (!st || !od || !out) { return 0; } memset(&od_opts, 0, sizeof(od_opts)); od_opts.pre_tx_us = 0; od_opts.post_hold_us = 400; od_opts.flush_rx_before_tx = 0; od_opts.require_idle = 1; od_opts.idle_wait_ms = AUTO_CLOSE_0D_IDLE_WAIT_MS; od_opts.poll_fn = inject_capture_poll; od_opts.poll_ctx = st; st->inject_raw_capture = true; st->inject_echo_flushed = false; st->inject_expect_len = 14u; st->inject_skip_early_echo_ipc = true; cap_begin_window(st, mono_ns(), false); memset(&od_res, 0, sizeof(od_res)); pog422_inject_burst(st->uart_fd, od, 14u, &od_opts, &od_res); if (st->inject_abort) { (void)pog422_gpio7_rx(st->uart_fd); if (od_res.err == 0u) { od_res.err = (uint32_t)ECANCELED; } } inject_capture_poll(st); cap_end_inject_window(st, mono_ns(), 0); st->inject_raw_capture = false; st->inject_expect_len = 0u; st->inject_skip_early_echo_ipc = true; /* keep until outer clears */ out->od_tx_ns = od_res.tx_ns; out->od_total_ns = od_res.total_ns; timing_push( st, POG422_TT_ID_AUTO_CLOSE_DONE, (uint32_t)(od_res.err & 0xffffffffu), out->harvest, od_res.total_ns ); if (od_res.err != 0u) { out->err = (int32_t)od_res.err; if (out->err <= 0) { out->err = POG422_AUTO_CLOSE_INJECT_FAIL; } memcpy(st->ac_pending_od, od, 14u); st->ac_pending_od_len = 14u; st->ac_pending_x1_msg = out->x1_msg; st->ac_pending_harvest = out->harvest; fprintf(stderr, "auto_close_0d_fail err=%d x1=%02X harvest=%u (od stashed for HS-idle retry)\n", (int)out->err, out->x1_msg, out->harvest); return 0; } out->err = POG422_AUTO_CLOSE_OK; st->ac_pending_od_len = 0u; fprintf(stderr, "auto_close_0d_ok x1=%02X counter=%02X harvest=%u od_total_us=%llu (post_done)\n", out->x1_msg, od[8], out->harvest, (unsigned long long)(od_res.total_ns / 1000ull)); return 1; } /* Legacy combine: still used by pending-WINDOW path. */ static int try_uartd_auto_close( capture_state_t *st, const uint8_t *x9, uint32_t x9_len, const pog422_ipc_auto_close_t *ac, const uint8_t *echo, size_t echo_len, const uint8_t *post_de, size_t post_de_len, const uint8_t *imm, size_t imm_len, const uint8_t *hs_rx, size_t hs_rx_len, pog422_ipc_auto_close_done_t *out) { uint8_t od[14]; if (!auto_close_harvest_x1( st, x9, x9_len, ac, echo, echo_len, post_de, post_de_len, imm, imm_len, hs_rx, hs_rx_len, out, od)) { return 0; } if (out->err == POG422_AUTO_CLOSE_BUILD_FAIL) { return 0; } return auto_close_tx_0d(st, od, out); } static void ac_pending_clear(capture_state_t *st) { if (!st) { return; } st->ac_pending_live = false; st->ac_pending_x9_len = 0u; st->ac_pending_deadline_ns = 0u; st->ac_peel_armed = false; st->ac_x1_stash_len = 0u; st->ac_x1_msg_stashed = 0u; st->ac_pending_od_len = 0u; st->ac_pending_x1_msg = 0u; st->ac_pending_harvest = 0u; } static void ac_pending_arm( capture_state_t *st, const uint8_t *x9, uint32_t x9_len, const pog422_ipc_auto_close_t *ac) { uint32_t n; if (!st || !x9 || !ac || x9_len == 0u) { return; } n = x9_len; if (n > (uint32_t)sizeof(st->ac_pending_x9)) { n = (uint32_t)sizeof(st->ac_pending_x9); } memcpy(st->ac_pending_x9, x9, n); st->ac_pending_x9_len = n; st->ac_pending_tid_tail[0] = ac->tid_tail0; st->ac_pending_tid_tail[1] = ac->tid_tail1; st->ac_local_tid[0] = ac->local_tid0; st->ac_local_tid[1] = ac->local_tid1; st->ac_pending_deadline_ns = mono_ns() + AUTO_CLOSE_PENDING_NS; st->ac_pending_live = true; st->ac_peel_armed = true; fprintf(stderr, "auto_close_pending_arm x9_len=%u\n", (unsigned)n); } /* EG 3.0.1.5: close from WINDOW/grace after inject already returned NO_X1. */ static int try_pending_auto_close_from_buf( capture_state_t *st, const uint8_t *buf, size_t len) { pog422_ipc_auto_close_t ac; pog422_ipc_auto_close_done_t done; uint8_t x1_msg = 0; int ok; if (!st || !st->ac_pending_live || !buf || len < 6u) { return 0; } if (mono_ns() > st->ac_pending_deadline_ns) { fprintf(stderr, "auto_close_pending_expire\n"); ac_pending_clear(st); return 0; } if (!find_local_x1_ack(buf, len, st->ac_local_tid, &x1_msg)) { return 0; } memset(&ac, 0, sizeof(ac)); ac.enable = 1; ac.tid_tail0 = st->ac_pending_tid_tail[0]; ac.tid_tail1 = st->ac_pending_tid_tail[1]; ac.local_tid0 = st->ac_local_tid[0]; ac.local_tid1 = st->ac_local_tid[1]; memset(&done, 0, sizeof(done)); ok = try_uartd_auto_close( st, st->ac_pending_x9, st->ac_pending_x9_len, &ac, NULL, 0u, NULL, 0u, NULL, 0u, buf, len, &done); if (!ok) { return 0; } if (done.harvest == 0u) { done.harvest = 3u; /* pending WINDOW/grace */ } fprintf(stderr, "auto_close_pending_ok x1=%02X harvest=%u\n", done.x1_msg, done.harvest); (void)send_ipc_st(st, POG422_IPC_AUTO_CLOSE_DONE, &done, sizeof(done)); ac_pending_clear(st); return 1; } /* * EG 3.0.1.18: x1 was harvested but synth 0D hit HS-busy — retry when idle. * Does not re-peel; uses the stashed 14-byte 0D from the failed close attempt. */ static int try_pending_auto_close_0d_when_idle(capture_state_t *st) { pog422_inject_opts_t od_opts; pog422_inject_result_t od_res; pog422_ipc_auto_close_done_t done; if (!st || !st->ac_pending_live || st->ac_pending_od_len != 14u) { return 0; } if (st->inject_raw_capture || st->inject_busy) { return 0; /* do not nest inside another inject/poll */ } if (mono_ns() > st->ac_pending_deadline_ns) { fprintf(stderr, "auto_close_pending_expire\n"); ac_pending_clear(st); return 0; } if (st->uart_fd < 0 || slv_in_read(st->uart_fd)) { return 0; } memset(&od_opts, 0, sizeof(od_opts)); od_opts.pre_tx_us = 0; od_opts.post_hold_us = 400; od_opts.flush_rx_before_tx = 0; od_opts.require_idle = 1; od_opts.idle_wait_ms = 25; /* EG 3.0.1.52/54: brief idle for pending 0D */ od_opts.poll_fn = inject_capture_poll; od_opts.poll_ctx = st; st->inject_raw_capture = true; st->inject_echo_flushed = false; st->inject_expect_len = 14u; st->inject_skip_early_echo_ipc = true; cap_begin_window(st, mono_ns(), false); memset(&od_res, 0, sizeof(od_res)); pog422_inject_burst(st->uart_fd, st->ac_pending_od, 14u, &od_opts, &od_res); inject_capture_poll(st); cap_end_inject_window(st, mono_ns(), 0); st->inject_raw_capture = false; st->inject_expect_len = 0u; memset(&done, 0, sizeof(done)); done.x1_msg = st->ac_pending_x1_msg; done.counter = st->ac_pending_od[8]; done.harvest = st->ac_pending_harvest ? st->ac_pending_harvest : 4u; done.od_tx_ns = od_res.tx_ns; done.od_total_ns = od_res.total_ns; if (od_res.err != 0u) { fprintf(stderr, "auto_close_0d_idle_retry_fail err=%u x1=%02X\n", (unsigned)od_res.err, done.x1_msg); return 0; } done.err = POG422_AUTO_CLOSE_OK; fprintf(stderr, "auto_close_0d_idle_retry_ok x1=%02X harvest=%u od_total_us=%llu\n", done.x1_msg, done.harvest, (unsigned long long)(od_res.total_ns / 1000ull)); (void)send_ipc_st(st, POG422_IPC_AUTO_CLOSE_DONE, &done, sizeof(done)); ac_pending_clear(st); return 1; } static void cap_end_window(capture_state_t *st, uint64_t now) { timing_push(st, POG422_TT_ID_CAP_END, (uint32_t)st->cap_len, 0u, now); cap_drain_uart(st, now, slv_in_read(st->uart_fd)); cap_pre_flush_chain_drain(st, now); /* Never report byte times after HS fall (FIFO drain artifact). */ if (st->first_byte_mono_ns > now) { st->first_byte_mono_ns = now; } if (st->last_byte_mono_ns > now) { st->last_byte_mono_ns = now; } #if POG422_HW_DEBUG HWDBG_EMIT_AT(&st->hwdbg, POG422_HWDBG_HS_FALL, 0u, (uint32_t)st->cap_len, now); flush_hwdbg_to_client(st); #endif if (st->client_fd >= 0) { pog422_ipc_hs_t ev = {.mono_ns = now}; send_ipc_st(st, POG422_IPC_HS_INACTIVE, &ev, sizeof(ev)); } flush_window_to_client(st); st->hs_active = false; st->cap_gpio_led = false; st->cap_burst_on_fall = false; } static int uart_rx_byte_count(int uart_fd) { int avail = 0; if (ioctl(uart_fd, FIONREAD, &avail) != 0) { return 0; } return avail > 0 ? avail : 0; } static int uart_rx_pending_count(capture_state_t *st) { if (st->dttty_rx) { uint32_t avail = 0; if (pog422_dttty_avail(st->uart_fd, &avail) == 0) { return (int)avail; } return 0; } return uart_rx_byte_count(st->uart_fd); } static ssize_t uart_rx_pull(capture_state_t *st, uint8_t *tmp, size_t cap) { if (st->dttty_rx) { return pog422_dttty_read(st->uart_fd, tmp, cap); } return read(st->uart_fd, tmp, cap); } static int uart_rx_available(capture_state_t *st) { return uart_rx_pending_count(st) > 0 ? 1 : 0; } static void cap_flush_promiscuous_idle(capture_state_t *st, uint64_t now) { uint64_t idle_ns; if (st->cap_len == 0 || st->last_byte_mono_ns == 0) { return; } idle_ns = cap_has_eop(st) ? CAP_PROMISC_IDLE_NS : CAP_INCOMPLETE_PACKET_IDLE_NS; if (now - st->last_byte_mono_ns < idle_ns) { return; } if (uart_rx_available(st)) { return; } if (!cap_align_to_mark(st, 1)) { return; } cap_end_window(st, now); } /* * Wire lengths must match EtherGold/dev pog422_fixture_core.py COMMAND_WIRE_LEN * (+ COMMAND_WIRE_LEN_ALTS). Used to frame HS/capture windows without EOP-hunt. * Return 0 when opcode unknown (caller falls back to EOP scan). */ static int pog422_wire_len_primary(uint8_t cmd) { switch (cmd) { case 0x01: return 17; /* draw poker skill grant; alt 32 = STOM config */ case 0x02: case 0x03: case 0x04: /* Partial Transfer request; live 2026-07-13 @32 */ case 0x50: case 0x52: case 0x70: case 0x72: case 0x80: return 32; case 0x09: return 1170; /* live MTC 2026-07-13; alt 1094 = legacy comlengs[9] */ case 0x0E: return 598; /* live Partial reply 2026-07-13; alt 397 mid-EOP census */ case 0x0A: return 82; case 0x0B: case 0x21: case 0x31: case 0x41: case 0x51: case 0x61: case 0x71: case 0x81: return 12; case 0x0D: return 14; case 0x20: return 37; /* Touch 6 1-card; alt 48 */ case 0x29: case 0x69: return 30; case 0x30: return 80; /* Bingo T340+; alt 58 */ case 0x39: return 74; case 0x40: return 42; case 0x49: return 44; case 0x59: return 34; /* alt 23 */ case 0x5A: case 0x7A: return 24; /* alt 20 legacy */ case 0x60: return 48; case 0x79: return 42; /* alts 38,34,31 */ case 0x89: return 36; default: return 0; } } /* Longest-first candidates into out[]; returns count (0 = unknown opcode). */ static int pog422_wire_lens(uint8_t cmd, int *out, int out_cap) { int n = 0; int primary; const int *alts = NULL; int n_alts = 0; int i; int j; if (!out || out_cap <= 0) { return 0; } primary = pog422_wire_len_primary(cmd); if (primary <= 0) { return 0; } switch (cmd) { case 0x01: { static const int a[] = {32}; alts = a; n_alts = 1; break; } case 0x09: { static const int a[] = {1094}; alts = a; n_alts = 1; break; } case 0x0E: { static const int a[] = {397}; /* census mid-EOP; primary 598 */ alts = a; n_alts = 1; break; } case 0x20: { static const int a[] = {48}; alts = a; n_alts = 1; break; } case 0x30: { static const int a[] = {58}; alts = a; n_alts = 1; break; } case 0x59: { static const int a[] = {23}; alts = a; n_alts = 1; break; } case 0x5A: case 0x7A: { static const int a[] = {20}; alts = a; n_alts = 1; break; } case 0x69: { static const int a[] = {28}; alts = a; n_alts = 1; break; } case 0x79: { static const int a[] = {38, 34, 31}; alts = a; n_alts = 3; break; } default: break; } out[n++] = primary; for (i = 0; i < n_alts && n < out_cap; i++) { int dup = 0; for (j = 0; j < n; j++) { if (out[j] == alts[i]) { dup = 1; break; } } if (!dup) { out[n++] = alts[i]; } } /* longest first */ for (i = 0; i < n; i++) { for (j = i + 1; j < n; j++) { if (out[j] > out[i]) { int tmp = out[i]; out[i] = out[j]; out[j] = tmp; } } } return n; } static uint8_t cap_last_framed_cmd(const capture_state_t *st) { size_t i = 0; uint8_t last_cmd = 0; while (i < st->cap_len) { if (st->cap_buf[i] != POG422_MARK_START_MACHINE) { i++; continue; } if (i + 1 >= st->cap_len) { break; } { int lens[8]; int nlen = pog422_wire_lens(st->cap_buf[i + 1], lens, 8); int li; int framed = 0; for (li = 0; li < nlen; li++) { int wlen = lens[li]; if (wlen > 0 && i + (size_t)wlen <= st->cap_len) { const uint8_t *pkt = st->cap_buf + i; if (pkt[0] == POG422_MARK_START_MACHINE && pkt[wlen - 1] == POG422_EOP) { last_cmd = pkt[1]; i += (size_t)wlen; framed = 1; break; } } } if (framed) { continue; } } { size_t eop = i + 1; for (; eop < st->cap_len; eop++) { if (st->cap_buf[eop] == POG422_EOP) { break; } } if (eop >= st->cap_len) { break; } if (eop - i >= 2) { last_cmd = st->cap_buf[i + 1]; } i = eop + 1; } } return last_cmd; } static int cap_has_trailing_bytes(const capture_state_t *st) { size_t i = 0; size_t consumed = 0; while (i < st->cap_len) { if (st->cap_buf[i] != POG422_MARK_START_MACHINE) { i++; continue; } if (i + 1 >= st->cap_len) { return 1; } { int lens[8]; int nlen = pog422_wire_lens(st->cap_buf[i + 1], lens, 8); int li; int framed = 0; for (li = 0; li < nlen; li++) { int wlen = lens[li]; if (wlen > 0 && i + (size_t)wlen <= st->cap_len) { const uint8_t *pkt = st->cap_buf + i; if (pkt[0] == POG422_MARK_START_MACHINE && pkt[wlen - 1] == POG422_EOP) { consumed = i + (size_t)wlen; i = consumed; framed = 1; break; } } } if (framed) { continue; } } { size_t eop = i + 1; for (; eop < st->cap_len; eop++) { if (st->cap_buf[eop] == POG422_EOP) { break; } } if (eop >= st->cap_len) { return 1; } consumed = eop + 1; i = consumed; } } return consumed < st->cap_len ? 1 : 0; } static int cmd_expects_chain_follow(uint8_t cmd) { switch (cmd) { case 0x03: case 0x80: case 0x89: case 0x20: case 0x40: return 1; default: return 0; } } static void cap_drain_until_quiet( capture_state_t *st, uint64_t now, uint64_t idle_ns, uint64_t max_ns ) { uint64_t deadline = now + max_ns; uint64_t quiet_since = 0; for (;;) { uint64_t t = mono_ns(); if (t > deadline) { break; } if (uart_rx_available(st)) { cap_drain_uart(st, t, slv_in_read(st->uart_fd)); quiet_since = 0; continue; } if (st->cap_len > 0 && st->last_byte_mono_ns > 0) { if (quiet_since == 0) { quiet_since = t; } else if (t - quiet_since >= idle_ns) { break; } } else if (t - now >= idle_ns) { break; } { struct timespec ts = {0, 100000L}; nanosleep(&ts, NULL); } } } static void cap_pre_flush_chain_drain(capture_state_t *st, uint64_t now) { uint64_t max_wait = CAP_PROMISC_IDLE_NS; if (st->cap_len == 0) { pending_flush_complete(st, now); return; } if (!cap_has_eop(st) || cap_has_trailing_bytes(st)) { max_wait = CAP_INCOMPLETE_PACKET_IDLE_NS; } else { uint8_t cmd = cap_last_framed_cmd(st); if (cmd_expects_chain_follow(cmd)) { max_wait = CAP_CHAIN_FOLLOW_NS; } } cap_drain_until_quiet(st, now, CAP_PROMISC_IDLE_NS, max_wait); if (st->pending_len > 0 && buf_has_eop(st->pending_rx, st->pending_len)) { pending_promote_to_cap(st, mono_ns(), false); } } static void flush_window_to_client(capture_state_t *st) { if (!cap_align_to_mark(st, 1) || st->cap_len == 0 || st->client_fd < 0) { cap_reset_window(st); return; } pog422_ipc_window_hdr_t wh = { .window_start_mono_ns = st->window_start_mono_ns, .first_byte_mono_ns = st->first_byte_mono_ns, .last_byte_mono_ns = st->last_byte_mono_ns, .window_len = (uint32_t)st->cap_len, }; uint32_t plen = (uint32_t)(sizeof(wh) + st->cap_len); _Static_assert(sizeof(wh) == 28, "window IPC header must be 28 bytes (packed, no tail padding)"); uint8_t *payload = malloc(plen); if (!payload) { cap_reset_window(st); return; } memcpy(payload, &wh, sizeof(wh)); memcpy(payload + sizeof(wh), st->cap_buf, st->cap_len); /* * EG 3.0.1.4: stash local x1 before WINDOW IPC clears cap_buf — auto-close * peel only had echo/post_de and returned NO_X1 while Python got uart_window. */ if (st->ac_peel_armed && st->ac_x1_stash_len == 0u && st->cap_len >= 6u) { uint8_t x1_msg = 0; if (find_local_x1_ack( st->cap_buf, st->cap_len, st->ac_local_tid, &x1_msg)) { size_t n = st->cap_len; if (n > sizeof(st->ac_x1_stash)) { n = sizeof(st->ac_x1_stash); } memcpy(st->ac_x1_stash, st->cap_buf, n); st->ac_x1_stash_len = n; st->ac_x1_msg_stashed = x1_msg; fprintf(stderr, "auto_close_x1_stash msg=%02X len=%u\n", x1_msg, (unsigned)n); } } /* Snapshot for pending close — try_uartd will wipe cap via 0D inject window. */ { uint8_t peel[512]; size_t peel_len = 0; if (st->ac_pending_live && st->cap_len >= 6u) { peel_len = st->cap_len; if (peel_len > sizeof(peel)) { peel_len = sizeof(peel); } memcpy(peel, st->cap_buf, peel_len); } { size_t n = st->cap_len < 16 ? st->cap_len : 16; fprintf(stderr, "dttty_sink=window len=%u first=%02X last=%02X hex_prefix=", (unsigned)st->cap_len, st->cap_buf[0], st->cap_buf[st->cap_len - 1]); for (size_t i = 0; i < n; i++) { fprintf(stderr, "%s%02X", i ? "," : "", st->cap_buf[i]); } fputc('\n', stderr); } send_ipc_st(st, POG422_IPC_WINDOW, payload, plen); free(payload); cap_reset_window(st); if (peel_len > 0u) { (void)try_pending_auto_close_from_buf(st, peel, peel_len); } if (st->ac_pending_live && st->ac_pending_od_len == 14u) { (void)try_pending_auto_close_0d_when_idle(st); } return; } } /* * Passive GPIOB1 fell: SLV-HS inactive — RS485 tri-stated, full frame is in the * XRUSB FIFO. Burst-drain into cap_buf and flush immediately (no idle timer). */ static void cap_gpio_fell_burst(capture_state_t *st, uint64_t now) { uint8_t tmp[512]; /* * Stamp fall-drain bytes at HS-fall (`now`), never mono_ns() at read time. * Bytes in the FIFO arrived while HS was still high; post-fall read time is * a capture artifact (T340+ cannot drive with HS inactive). */ const uint64_t byte_mono = now; st->cap_gpio_led = false; #if POG422_UARTD_HS_DIAG hs_diag_emit(st, now, POG422_HS_DIAG_FALL_PRE); #endif for (;;) { ssize_t n = uart_rx_pull(st, tmp, sizeof(tmp)); if (n <= 0) { break; } #if POG422_UARTD_HS_DIAG g_hs_diag_drain_bytes += (uint32_t)n; #endif if (st->cap_len == 0) { size_t off = cap_skip_to_mark(tmp, (size_t)n); if (off >= (size_t)n) { if (pending_append(st, tmp, (size_t)n, byte_mono) == 0) { continue; } emit_discard(st, (uint32_t)n, tmp[0]); st->bytes_discarded_inactive += (size_t)n; continue; } if (off > 0) { emit_discard(st, (uint32_t)off, tmp[0]); st->bytes_discarded_inactive += off; } cap_append(st, tmp + off, (size_t)n - off, byte_mono); } else { cap_append(st, tmp, (size_t)n, byte_mono); } } #if POG422_UARTD_HS_DIAG hs_diag_emit(st, now, POG422_HS_DIAG_FALL_POST); #endif if (st->cap_len > 0) { cap_end_window(st, now); return; } if (st->hs_active) { cap_abort_window(st, now); } /* HS just fell with empty cap — still a chance to send stashed auto-close 0D. */ if (st->ac_pending_live && st->ac_pending_od_len == 14u) { (void)try_pending_auto_close_0d_when_idle(st); } } static void capture_feed_uart_chunk( capture_state_t *st, const uint8_t *tmp, size_t chunk, uint64_t byte_mono, uint64_t now, int hs ) { /* * Inject TX echo forensics: keep every byte (including leading SPACE junk). * Normal HS capture still aligns to first MARK/SOH. */ if (st->inject_raw_capture && st->hs_active && !st->cap_burst_on_fall) { cap_append(st, tmp, chunk, byte_mono); return; } if (st->cap_len == 0) { if (!hs && !st->cap_gpio_led && !st->hs_active) { uint8_t live = 0; if (pog422_slv_in_live(st->uart_fd, &live) == 0 && live) { hs = 1; if (!st->hs_active) { cap_begin_window(st, now, true); } else { st->cap_gpio_led = true; } if (st->pending_len > 0) { pending_promote_to_cap(st, now, true); } } else if (pending_append(st, tmp, chunk, byte_mono) != 0) { emit_discard(st, (uint32_t)chunk, tmp[0]); st->bytes_discarded_inactive += chunk; } return; } { size_t off = cap_skip_to_mark(tmp, chunk); if (off >= chunk) { if (pending_append(st, tmp, chunk, byte_mono) != 0) { emit_discard(st, (uint32_t)chunk, tmp[0]); st->bytes_discarded_inactive += chunk; } return; } if (off > 0) { emit_discard(st, (uint32_t)off, tmp[0]); st->bytes_discarded_inactive += off; } if (st->pending_len > 0) { pending_promote_to_cap(st, now, st->cap_gpio_led); } if (!st->hs_active) { cap_begin_window(st, now, hs != 0); } cap_append(st, tmp + off, chunk - off, byte_mono); return; } } cap_append(st, tmp, chunk, byte_mono); } static void cap_drain_uart(capture_state_t *st, uint64_t now, int hs) { uint8_t tmp[512]; for (;;) { ssize_t n = uart_rx_pull(st, tmp, sizeof(tmp)); if (n <= 0) { break; } capture_feed_uart_chunk(st, tmp, (size_t)n, mono_ns(), now, hs); } } static int is_gameplay_ack_msg(uint8_t msg) { switch (msg) { case 0x21: case 0x31: case 0x41: case 0x51: case 0x61: case 0x71: case 0x81: return 1; default: return 0; } } static void post_x9_grace_arm(capture_state_t *st, uint64_t now) { st->post_x9_grace_until_mono_ns = now + POST_X9_INJECT_LOCAL_GRACE_NS; st->post_x9_grace_soh_hold = 0; } static void post_x9_grace_clear(capture_state_t *st) { st->post_x9_grace_until_mono_ns = 0; st->post_x9_grace_soh_hold = 0; } /* * Port of pyserial post_x9_inject_grace_hold/absorb: uartd normally ignores the * UART FIFO while HS is inactive (capture_poll early-return). After inject with * quiet SLV-HS, poll briefly so a split SOH|x1 still becomes a WINDOW for * status_0d_synth — without synthesizing 0x0D before x1. */ static void post_x9_grace_poll(capture_state_t *st, uint64_t now) { uint8_t tmp[512]; uint8_t buf[513]; size_t len = 0; ssize_t n; n = uart_rx_pull(st, tmp, sizeof(tmp)); if (n <= 0) { return; } if (st->post_x9_grace_soh_hold) { buf[0] = POG422_SOH; len = 1; st->post_x9_grace_soh_hold = 0; } if ((size_t)n > sizeof(buf) - len) { n = (ssize_t)(sizeof(buf) - len); } memcpy(buf + len, tmp, (size_t)n); len += (size_t)n; if (len == 1 && buf[0] == POG422_SOH) { st->post_x9_grace_soh_hold = 1; fprintf(stderr, "post_x9_grace_hold soh\n"); return; } if (len >= 2 && buf[0] == POG422_SOH && is_gameplay_ack_msg(buf[1])) { /* Software window (not burst-on-fall); idle flush completes the packet. */ cap_begin_window(st, now, false); if (cap_append(st, buf, len, now) != 0) { cap_abort_window(st, now); post_x9_grace_clear(st); return; } post_x9_grace_clear(st); fprintf(stderr, "post_x9_grace_absorb msg=%02X len=%u\n", buf[1], (unsigned)len); fprintf(stderr, "dttty_sink=grace len=%u first=%02X last=%02X\n", (unsigned)len, buf[0], buf[len - 1]); if (cap_has_eop(st)) { cap_end_window(st, now); } return; } emit_discard(st, (uint32_t)len, buf[0]); st->bytes_discarded_inactive += len; fprintf(stderr, "post_x9_grace_skip count=%u first=%02X\n", (unsigned)len, buf[0]); fprintf(stderr, "dttty_sink=discard len=%u first=%02X\n", (unsigned)len, buf[0]); } static void capture_poll(capture_state_t *st) { if (st->uart_fd < 0) { return; } if (st->ac_pending_live && mono_ns() > st->ac_pending_deadline_ns) { fprintf(stderr, "auto_close_pending_expire\n"); ac_pending_clear(st); } /* EG 3.0.1.18: harvested x1 + stashed 0D — send when HS idle (no stomp). */ if (st->ac_pending_live && st->ac_pending_od_len == 14u) { (void)try_pending_auto_close_0d_when_idle(st); } if (uart_fd_is_dead(st->uart_fd)) { uart_mark_lost(st, "deleted_or_fstat"); return; } int hs; uint64_t now; uint64_t t0; t0 = mono_ns(); hs = slv_in_read(st->uart_fd); now = mono_ns(); if (st->timing_on) { if (st->timing_last_hs < 0 || hs != st->timing_last_hs) { timing_push(st, POG422_TT_ID_HS_OBS, (uint32_t)hs, (uint32_t)(now - t0), now); timing_push(st, POG422_TT_ID_GPIO_STAT_RTT, (uint32_t)(now - t0), (uint32_t)hs, 0); st->timing_last_hs = hs; st->timing_last_rtt_sample_ns = now; } else if (now - st->timing_last_rtt_sample_ns >= 10000000ull) { timing_push(st, POG422_TT_ID_GPIO_STAT_RTT, (uint32_t)(now - t0), (uint32_t)hs, 0); st->timing_last_rtt_sample_ns = now; } if (now - st->timing_last_flush_ns >= 250000000ull) { timing_drain_kernel(st); timing_flush(st); } } /* Passive GPIOB1 rise: open window; bytes accumulate in hardware FIFO. */ if (hs && !st->cap_gpio_led) { if (!st->hs_active) { const int held_soh = st->post_x9_grace_soh_hold; cap_begin_window(st, now, true); /* Orphan post-DE SOH -> window, then clear grace (would drop hold). */ if (held_soh) { uint8_t soh = POG422_SOH; (void)cap_append(st, &soh, 1, now); st->post_x9_grace_soh_hold = 0; fprintf(stderr, "post_de_soh_into_hs_window\n"); } post_x9_grace_clear(st); } else if (st->cap_burst_on_fall) { st->cap_gpio_led = true; } } /* Passive GPIOB1 fall: one burst read, then flush. */ if (st->cap_burst_on_fall && st->cap_gpio_led && !hs) { cap_gpio_fell_burst(st, now); return; } /* While SLV-HS is active on a passive window: * prod: leave bytes in FIFO until fall (burst timestamp). * HW_DEBUG: stream-drain so first/last byte mono are real porch edges. */ if (st->cap_burst_on_fall && hs) { st->hs_active = true; if (!st->cap_gpio_led) { st->cap_gpio_led = true; } #if POG422_HW_DEBUG cap_drain_uart(st, now, hs); flush_hwdbg_to_client(st); #endif return; } /* Inject software window / post-grace absorb window: incremental read. */ if (!st->hs_active) { if (st->post_x9_grace_until_mono_ns != 0) { if (now >= st->post_x9_grace_until_mono_ns) { if (st->post_x9_grace_soh_hold) { emit_discard(st, 1u, POG422_SOH); st->bytes_discarded_inactive += 1u; } post_x9_grace_clear(st); } else { post_x9_grace_poll(st, now); } } return; } { uint8_t tmp[512]; ssize_t n = uart_rx_pull(st, tmp, sizeof(tmp)); if (n > 0) { capture_feed_uart_chunk(st, tmp, (size_t)n, mono_ns(), now, hs); return; } if (n < 0 && errno_is_uart_dead(errno)) { uart_mark_lost(st, "rx_eio"); return; } } pending_flush_complete(st, now); cap_flush_promiscuous_idle(st, now); } static void inject_capture_poll(void *ctx) { capture_state_t *st = (capture_state_t *)ctx; capture_poll(st); maybe_early_inject_echo(st); } static int handle_gpio7(capture_state_t *st, int assert_tx) { int err; int attempt; /* * EG 3.0.0.9: RELEASE while inject worker is in INJECT_TX — drop DE now * (SET_GPIO7 bypasses the inject mutex) and mark abort for post-ioctl cleanup. * EG 3.0.1.2: RELEASE retries until ioctl sticks (pos5 17:37 aborted_de=0). */ if (!assert_tx && st->inject_busy) { st->inject_abort = 1; } err = 0; if (assert_tx) { err = pog422_gpio7_tx(st->uart_fd); } else { for (attempt = 0; attempt < 3; attempt++) { err = pog422_gpio7_rx(st->uart_fd); if (err == 0) { break; } { struct timespec ts = {.tv_sec = 0, .tv_nsec = 500000}; nanosleep(&ts, NULL); } } if (err == 0) { fprintf(stderr, "gpio7_release_ok abort=%d busy=%d\n", (int)st->inject_abort, (int)st->inject_busy); } else { fprintf(stderr, "gpio7_release_fail err=%d abort=%d busy=%d\n", err, (int)st->inject_abort, (int)st->inject_busy); } } { pog422_ipc_gpio7_ack_t ack = { .err = err != 0 ? (int32_t)(err < 0 ? -err : errno) : 0, .slv_in = (uint8_t)(slv_in_read(st->uart_fd) ? 1 : 0), .gpio7_tx = (uint8_t)(assert_tx ? 1 : 0), }; /* Best-effort ACK — DE already dropped; do not fail the release path. */ (void)send_ipc_st(st, POG422_IPC_GPIO7_ACK, &ack, sizeof(ack)); return 0; } } static int send_inject_forensics(capture_state_t *st, const pog422_inject_result_t *res, uint32_t frame_len) { pog422_ipc_inject_forensics_hdr_t hdr; uint8_t payload[sizeof(hdr) + POG422_INJECT_FORENSICS_WIDE_MAX]; uint32_t wide_len; uint32_t plen; if (!st || st->client_fd < 0 || !res) { return 0; } wide_len = res->wide_len; if (wide_len > POG422_INJECT_FORENSICS_WIDE_MAX) { wide_len = POG422_INJECT_FORENSICS_WIDE_MAX; } hdr.inject_method = res->inject_method; hdr.wide_is_wire = res->wide_is_wire; hdr.driver_errno = res->driver_errno; hdr.frame_len = frame_len; hdr.wide_len = wide_len; hdr.rx_fifo_before = res->rx_fifo_before; hdr.rx_fifo_after = res->rx_fifo_after; hdr.drain_wait_ns = res->drain_wait_ns; hdr.tx_fifo_before = res->tx_fifo_before; hdr.tx_fifo_after_bulk = res->tx_fifo_after_bulk; hdr.tx_fifo_after_drain = res->tx_fifo_after_drain; hdr.usb_actual = res->usb_actual; hdr.de_hold_us = res->de_hold_us; hdr.tx_empty_reason = res->tx_empty_reason; hdr.de_release_reason = res->de_release_reason; hdr._pad_de[0] = 0; hdr._pad_de[1] = 0; memcpy(payload, &hdr, sizeof(hdr)); plen = (uint32_t)sizeof(hdr); if (wide_len > 0u) { memcpy(payload + sizeof(hdr), res->wide_buf, wide_len); plen += wide_len; } return send_ipc_st(st, POG422_IPC_INJECT_FORENSICS, payload, plen); } static int send_inject_copper(capture_state_t *st, const pog422_inject_result_t *res) { pog422_ipc_inject_copper_hdr_t hdr; uint8_t payload[sizeof(hdr) + POG422_COPPER_PHASE_MAX * sizeof(pog422_ipc_copper_sample_t)]; uint32_t plen; uint8_t i; uint8_t n; if (!st || st->client_fd < 0 || !res || res->copper_n == 0u) { return 0; } n = res->copper_n; if (n > POG422_COPPER_PHASE_MAX) { n = (uint8_t)POG422_COPPER_PHASE_MAX; } memset(&hdr, 0, sizeof(hdr)); hdr.gpio_mode = res->gpio_mode; hdr.uart_flow = res->uart_flow; hdr.n = n; memcpy(payload, &hdr, sizeof(hdr)); plen = (uint32_t)sizeof(hdr); for (i = 0; i < n; i++) { pog422_ipc_copper_sample_t s; memset(&s, 0, sizeof(s)); s.t_us = res->copper[i].t_us; s.gpio_stat = res->copper[i].gpio_stat; s.tx_fifo = res->copper[i].tx_fifo; s.rx_fifo = res->copper[i].rx_fifo; s.phase = res->copper[i].phase; s.gpio7 = res->copper[i].gpio7; s.gpio1 = res->copper[i].gpio1; memcpy(payload + plen, &s, sizeof(s)); plen += (uint32_t)sizeof(s); } return send_ipc_st(st, POG422_IPC_INJECT_COPPER, payload, plen); } /* * EG 3.0.1.4 / 3.0.1.54: when echo+post_de miss local x1, wait for HS burst/FIFO * that Python already sees as uart_window — peel before DONE so C still closes. */ #define AUTO_CLOSE_HS_X1_WAIT_NS 40000000ull /* 40 ms */ static size_t collect_hs_window_x1_for_auto_close( capture_state_t *st, const pog422_ipc_auto_close_t *ac, uint8_t *out, size_t out_cap) { uint8_t local_tid[2]; uint8_t x1_msg = 0; uint64_t deadline; size_t len = 0; if (!st || !ac || !out || out_cap == 0u) { return 0; } local_tid[0] = ac->local_tid0; local_tid[1] = ac->local_tid1; if (st->ac_x1_stash_len > 0u) { len = st->ac_x1_stash_len; if (len > out_cap) { len = out_cap; } memcpy(out, st->ac_x1_stash, len); return len; } if (st->cap_len >= 6u && find_local_x1_ack(st->cap_buf, st->cap_len, local_tid, &x1_msg)) { len = st->cap_len; if (len > out_cap) { len = out_cap; } memcpy(out, st->cap_buf, len); /* Emit WINDOW so Python still eth-forwards the x1. */ if (st->hs_active) { cap_end_window(st, mono_ns()); } fprintf(stderr, "auto_close_cap_x1 msg=%02X len=%u\n", x1_msg, (unsigned)len); return len; } if (slv_in_read(st->uart_fd)) { uint64_t now = mono_ns(); if (!st->hs_active) { cap_begin_window(st, now, true); fprintf(stderr, "auto_close_arm_hs_burst\n"); } else if (!st->cap_burst_on_fall) { st->cap_burst_on_fall = true; st->cap_gpio_led = true; fprintf(stderr, "auto_close_promote_hs_burst\n"); } } deadline = mono_ns() + AUTO_CLOSE_HS_X1_WAIT_NS; while (mono_ns() < deadline) { int hs = slv_in_read(st->uart_fd); uint64_t now = mono_ns(); if (st->ac_x1_stash_len > 0u) { len = st->ac_x1_stash_len; if (len > out_cap) { len = out_cap; } memcpy(out, st->ac_x1_stash, len); return len; } if (hs && !st->cap_burst_on_fall) { if (!st->hs_active) { cap_begin_window(st, now, true); } else { st->cap_burst_on_fall = true; st->cap_gpio_led = true; } } if (st->cap_burst_on_fall && st->cap_gpio_led && !hs) { cap_gpio_fell_burst(st, now); if (st->ac_x1_stash_len > 0u) { len = st->ac_x1_stash_len; if (len > out_cap) { len = out_cap; } memcpy(out, st->ac_x1_stash, len); return len; } } if (!hs && !st->cap_burst_on_fall) { uint8_t tmp[512]; ssize_t n = uart_rx_pull(st, tmp, sizeof(tmp)); if (n > 0) { size_t chunk = (size_t)n; if (chunk > out_cap - len) { chunk = out_cap - len; } memcpy(out + len, tmp, chunk); len += chunk; if (find_local_x1_ack(out, len, local_tid, &x1_msg)) { fprintf(stderr, "auto_close_fifo_x1 msg=%02X len=%u\n", x1_msg, (unsigned)len); return len; } if (len >= out_cap) { break; } continue; } } peel_wait_rx(st->uart_fd, deadline); } if (st->ac_x1_stash_len > 0u) { len = st->ac_x1_stash_len; if (len > out_cap) { len = out_cap; } memcpy(out, st->ac_x1_stash, len); } return len; } static int handle_inject(capture_state_t *st, const uint8_t *payload, uint32_t len) { const pog422_ipc_inject_req_hdr_t *req; const uint8_t *frame; const pog422_ipc_auto_close_t *ac = NULL; uint8_t echo_snap[CAP_BUF_MAX]; size_t echo_len = 0; uint8_t post_de_buf[CAP_BUF_MAX]; size_t post_de_len = 0; uint8_t imm_buf[CAP_BUF_MAX]; size_t imm_len = 0; uint8_t hs_x1_buf[512]; size_t hs_x1_len = 0; int is_x9; int skip_post_de; int auto_close_ok = 0; int defer_0d = 0; uint8_t od_buf[14]; pog422_ipc_auto_close_done_t ac_done; pog422_inject_opts_t opts; pog422_inject_result_t res; pog422_ipc_inject_done_t done; /* EG 3.0.1.58 — locate residual Python inject wall outside inject_burst. */ const uint64_t job_t0 = mono_ns(); uint64_t burst_t0 = 0ull; uint64_t burst_t1 = 0ull; uint32_t uartd_pre_burst_us = 0u; uint32_t uartd_burst_wall_us = 0u; uint32_t uartd_post_burst_us = 0u; uint32_t uartd_job_to_done_us = 0u; /* EG 3.0.2.4 — peel stage marks for INJECT_DONE trailer */ uint16_t peel_first_us = 0u; uint16_t peel_hit_us = 0u; uint8_t peel_src = 0u; uint8_t peel_x1_msg = 0u; memset(od_buf, 0, sizeof(od_buf)); if (st->uart_fd < 0) { return -1; } if (len < sizeof(pog422_ipc_inject_req_hdr_t)) { return -EINVAL; } req = (const pog422_ipc_inject_req_hdr_t *)payload; frame = payload + sizeof(*req); if (len < sizeof(*req) + req->frame_len) { return -EINVAL; } if (len >= sizeof(*req) + req->frame_len + sizeof(pog422_ipc_auto_close_t)) { ac = (const pog422_ipc_auto_close_t *)(frame + req->frame_len); } /* Arm WINDOW→stash before TX so mid-inject HS x1 is not lost to peel. * EG 3.0.1.5: do not clear ac_pending_live on unrelated injects (0A/0D). */ if (ac && ac->enable) { ac_pending_clear(st); st->ac_peel_armed = true; st->ac_local_tid[0] = ac->local_tid0; st->ac_local_tid[1] = ac->local_tid1; st->ac_x1_stash_len = 0; st->ac_x1_msg_stashed = 0; } capture_poll(st); pending_flush_complete(st, mono_ns()); /* * EG 2.2.2.27: flush any pending passive-capture bytes before the new inject * wipes cap_buf via cap_begin_window → cap_reset_window. A 71 ACK from a * prior round may be sitting in the UART FIFO — without this flush it gets * swallowed into the new inject's raw echo (wrong context) and is never * emitted as a packet_complete, causing an MRT. */ if (st->cap_burst_on_fall && st->hs_active) { int hs_now = slv_in_read(st->uart_fd); cap_drain_uart(st, mono_ns(), hs_now); if (st->cap_len > 0) { cap_end_window(st, mono_ns()); } else if (st->hs_active) { cap_abort_window(st, mono_ns()); } } /* * RS485 auto: local TX may not assert SLV-IN — software window so bytes read * during inject (poll_fn) are not discarded as idle. Trunk traffic afterward * uses the normal HS capture path (no inject↔reply pairing here). * inject_raw_capture: do not strip SPACE-before-MARK (chop forensics). */ st->inject_raw_capture = true; st->inject_echo_flushed = false; st->inject_expect_len = req->frame_len; /* EG 3.0.1.1: never send ECHO/POST_DE before DONE (Python backpressure = MRT). */ st->inject_skip_early_echo_ipc = true; cap_begin_window(st, mono_ns(), false); memset(&opts, 0, sizeof(opts)); opts.pre_tx_us = req->pre_tx_us; opts.post_hold_us = req->post_hold_us; opts.flush_rx_before_tx = req->flush_rx; opts.require_idle = req->require_idle; opts.idle_wait_ms = req->idle_wait_ms; opts.poll_fn = inject_capture_poll; opts.poll_ctx = st; memset(&res, 0, sizeof(res)); timing_push( st, POG422_TT_ID_INJECT_START, req->frame_len, (req->frame_len >= 2u) ? frame[1] : 0u, 0 ); burst_t0 = mono_ns(); uartd_pre_burst_us = (uint32_t)((burst_t0 - job_t0) / 1000ull); pog422_inject_burst(st->uart_fd, frame, req->frame_len, &opts, &res); burst_t1 = mono_ns(); uartd_burst_wall_us = (uint32_t)((burst_t1 - burst_t0) / 1000ull); timing_push( st, POG422_TT_ID_DE_OFF, res.de_release_reason, (uint32_t)(res.de_hold_us & 0xffffffffu), res.tx_ns ); if (st->inject_abort) { /* Python timed out and force-dropped DE — belt-and-suspenders. */ (void)pog422_gpio7_rx(st->uart_fd); if (res.err == 0u) { res.err = (uint32_t)ECANCELED; } } if (errno_is_uart_dead(res.err) || uart_fd_is_dead(st->uart_fd)) { /* Still send DONE below so Python can requeue; mark lost after. */ st->port_lost = true; /* hint — uart_mark_lost called by handle_client */ } inject_capture_poll(st); /* Snapshot echo BEFORE cap_end clears the software window. */ if (st->cap_len > 0) { echo_len = st->cap_len; if (echo_len > sizeof(echo_snap)) { echo_len = sizeof(echo_snap); } memcpy(echo_snap, st->cap_buf, echo_len); } is_x9 = (req->frame_len >= 2u && (frame[1] & 0x0Fu) == 0x09u); skip_post_de = !is_x9 || inject_echo_has_peer_after_sent( echo_snap, echo_len, frame, req->frame_len); /* * EG 3.0.1.2: if auto-close armed, keep incremental RX while HS may already * be high so post-DE peel can see local x1 (burst_on_fall = NO_X1 miss). */ { int harvest_hs = (ac && ac->enable && is_x9) ? 1 : 0; cap_end_inject_window(st, mono_ns(), harvest_hs); } st->inject_raw_capture = false; st->inject_expect_len = 0u; /* * EG 3.0.1.17 / 3.0.1.54: primary post-x9 path for armed auto-close — * HS-agnostic immediate peel (20 ms first-byte, 45 ms hard). post_de + HS * remain backup on miss (restored after .53 continuous-MRT failure). */ if (ac && ac->enable && is_x9 && res.err == 0u && !st->inject_abort) { uint8_t peek_msg = 0; uint8_t local_tid[2]; local_tid[0] = ac->local_tid0; local_tid[1] = ac->local_tid1; if (!find_local_x1_ack( echo_snap, echo_len, local_tid, &peek_msg)) { imm_len = collect_immediate_x1_peel( st, local_tid, imm_buf, sizeof(imm_buf), &peel_first_us, &peel_hit_us); timing_push( st, POG422_TT_ID_POST_DE, (uint32_t)imm_len, 4u, 0); } else { peel_src = 1u; /* echo */ peel_x1_msg = peek_msg; peel_hit_us = 0u; } } if (!skip_post_de && !st->inject_abort) { uint8_t peek_msg = 0; uint8_t local_tid[2]; int have_x1 = 0; const uint8_t *tid_for_post = NULL; if (ac && ac->enable && is_x9) { local_tid[0] = ac->local_tid0; local_tid[1] = ac->local_tid1; tid_for_post = local_tid; have_x1 = find_local_x1_ack( echo_snap, echo_len, local_tid, &peek_msg) || find_local_x1_ack( imm_buf, imm_len, local_tid, &peek_msg); } if (!have_x1) { post_de_len = collect_post_de_rx( st, post_de_buf, sizeof(post_de_buf), tid_for_post); timing_push(st, POG422_TT_ID_POST_DE, (uint32_t)post_de_len, 0u, 0); } } /* * EG 3.0.1.52/54: harvest x1 (no 0D yet), send INJECT_DONE, then short 0D. * On miss: still run HS/FIFO wait (40 ms) before declaring NO_X1. */ memset(&ac_done, 0, sizeof(ac_done)); defer_0d = 0; if (ac && ac->enable && is_x9 && res.err == 0u && !st->inject_abort) { uint8_t peek_msg = 0; int have_x1 = find_local_x1_ack( echo_snap, echo_len, st->ac_local_tid, &peek_msg) || find_local_x1_ack( imm_buf, imm_len, st->ac_local_tid, &peek_msg) || find_local_x1_ack( post_de_buf, post_de_len, st->ac_local_tid, &peek_msg); if (!have_x1) { hs_x1_len = collect_hs_window_x1_for_auto_close( st, ac, hs_x1_buf, sizeof(hs_x1_buf)); } else if (st->ac_x1_stash_len > 0u) { hs_x1_len = st->ac_x1_stash_len; if (hs_x1_len > sizeof(hs_x1_buf)) { hs_x1_len = sizeof(hs_x1_buf); } memcpy(hs_x1_buf, st->ac_x1_stash, hs_x1_len); } auto_close_ok = auto_close_harvest_x1( st, frame, req->frame_len, ac, echo_snap, echo_len, post_de_buf, post_de_len, imm_buf, imm_len, hs_x1_buf, hs_x1_len, &ac_done, od_buf); if (auto_close_ok && ac_done.err == POG422_AUTO_CLOSE_OK) { defer_0d = 1; /* 0D after INJECT_DONE */ peel_src = ac_done.harvest; peel_x1_msg = ac_done.x1_msg; } else { if (ac_done.err == 0) { ac_done.err = POG422_AUTO_CLOSE_NO_X1; } /* Miss / build fail: tell Python immediately (before DONE scoop). */ if (send_ipc_st( st, POG422_IPC_AUTO_CLOSE_DONE, &ac_done, sizeof(ac_done)) != 0) { ac_pending_clear(st); return -1; } ac_pending_arm(st, frame, req->frame_len, ac); if (st->ac_x1_stash_len > 0u) { (void)try_pending_auto_close_from_buf( st, st->ac_x1_stash, st->ac_x1_stash_len); } } /* * EG 3.0.1.20 / 3.0.2.6: merge imm into post_de for luxury POST_DE_RX. * Kept lightweight; arm HS / grace run *after* DONE (below). */ if (imm_len > 0u) { uint8_t peek = 0; int post_has_x1 = find_local_x1_ack( post_de_buf, post_de_len, st->ac_local_tid, &peek); if (!post_has_x1) { if (post_de_len == 0u) { post_de_len = imm_len; if (post_de_len > sizeof(post_de_buf)) { post_de_len = sizeof(post_de_buf); } memcpy(post_de_buf, imm_buf, post_de_len); } else if (post_de_len + imm_len <= sizeof(post_de_buf)) { memcpy(post_de_buf + post_de_len, imm_buf, imm_len); post_de_len += imm_len; } } } } else { st->ac_peel_armed = false; st->ac_x1_stash_len = 0; } /* * EG 3.0.2.6: DONE as soon as TX + peel harvest finish — before HS arm / * post-x9 grace (those were ~ms of GPIO polls after x1 already owned). */ memset(&done, 0, sizeof(done)); done.err = res.err; done.tx_ns = res.tx_ns; done.total_ns = res.total_ns; done.air_us = res.air_us; done.slv_in_before = res.slv_in_before; done.slv_in_after_on = res.slv_in_after_on; done.slv_in_after_body = res.slv_in_after_body; done.slv_in_after_drain = res.slv_in_after_drain; done.slv_in_after_off = res.slv_in_after_off; done.gpio1_edges = res.gpio1_edges; done.gpio1_first_edge_us = res.gpio1_first_edge_us; done.gpio1_last_edge_us = res.gpio1_last_edge_us; done.idle_wait_elapsed_us = res.idle_wait_elapsed_us; done.pre_bulk_us = res.pre_bulk_us; done.bulk_wall_us = res.bulk_wall_us; done.idle_wait_budget_ms = res.idle_wait_budget_ms; done.require_idle = res.require_idle; done.idle_saw_busy = res.idle_saw_busy; { const uint64_t done_t = mono_ns(); uartd_post_burst_us = (uint32_t)((done_t - (burst_t1 ? burst_t1 : job_t0)) / 1000ull); uartd_job_to_done_us = (uint32_t)((done_t - job_t0) / 1000ull); done.uartd_pre_burst_us = uartd_pre_burst_us; done.uartd_burst_wall_us = uartd_burst_wall_us; done.uartd_post_burst_us = uartd_post_burst_us; done.uartd_job_to_done_us = uartd_job_to_done_us; } done.peel_first_byte_us = peel_first_us; done.peel_hit_us = peel_hit_us; done.peel_src = peel_src; done.peel_x1_msg = peel_x1_msg; done._pad_peel = 0u; if (send_ipc_st(st, POG422_IPC_INJECT_DONE, &done, sizeof(done)) != 0) { return -1; } timing_push( st, POG422_TT_ID_INJECT_DONE, (uint32_t)(res.err & 0xffffffffu), (uint32_t)echo_len, res.total_ns ); /* * After peel (or miss), if HS still high and we deferred burst_on_fall, * arm fall-capture for the remainder of the slave window. */ if (ac && ac->enable && is_x9 && slv_in_read(st->uart_fd) && !st->cap_burst_on_fall) { uint64_t now = mono_ns(); cap_clear_buf(st); pending_reset(st); st->window_start_mono_ns = now; st->hs_active = true; st->cap_gpio_led = true; st->cap_burst_on_fall = true; fprintf(stderr, "cap_arm_burst_after_harvest\n"); } /* * Class B: post-DE can consume the entire 25 ms grace armed at DE end. * Re-arm after post-DE returns so late local x1 still has a FIFO window. */ if (!slv_in_read(st->uart_fd)) { post_x9_grace_arm(st, mono_ns()); } /* After-the-fact logging IPC — best effort; Python already has DONE. */ if (echo_len > 0 && st->client_fd >= 0) { (void)send_ipc_st(st, POG422_IPC_INJECT_ECHO, echo_snap, (uint32_t)echo_len); } if (post_de_len > 0 && st->client_fd >= 0) { (void)send_ipc_st(st, POG422_IPC_POST_DE_RX, post_de_buf, (uint32_t)post_de_len); } (void)send_inject_forensics(st, &res, req->frame_len); (void)send_inject_copper(st, &res); if (defer_0d) { if (auto_close_tx_0d(st, od_buf, &ac_done)) { ac_pending_clear(st); } else { /* Harvest kept; 0D failed — arm pending for HS-idle retry. */ ac_pending_arm(st, frame, req->frame_len, ac); } if (send_ipc_st( st, POG422_IPC_AUTO_CLOSE_DONE, &ac_done, sizeof(ac_done)) != 0) { ac_pending_clear(st); return -1; } } st->inject_skip_early_echo_ipc = false; return 0; } static void uart_detach(capture_state_t *st) { if (st->uart_fd >= 0) { /* Best-effort DE/RX restore before close (ignore errors on dead fd). */ (void)pog422_gpio7_rx(st->uart_fd); close(st->uart_fd); st->uart_fd = -1; } } static int errno_is_uart_dead(int e) { return e == EIO || e == ENODEV || e == ENXIO || e == EBADF; } static int uart_fd_is_dead(int fd) { char linkpath[64]; char link[256]; struct stat stbuf; ssize_t n; if (fd < 0) { return 1; } if (fstat(fd, &stbuf) != 0) { return 1; } snprintf(linkpath, sizeof(linkpath), "/proc/self/fd/%d", fd); n = readlink(linkpath, link, sizeof(link) - 1); if (n > 0) { link[n] = '\0'; if (strstr(link, "(deleted)") != NULL) { return 1; } } return 0; } /* Mirror Python resolve_serial_port channel heuristic (no sticky --port). */ static int resolve_xrusb_port(const char *channel, char *out, size_t out_sz) { glob_t g; size_t i; const char *pick = NULL; int is_ois = (channel && strcmp(channel, "ois") == 0); int rc; memset(&g, 0, sizeof(g)); rc = glob("/dev/ttyXRUSB*", 0, NULL, &g); if (rc != 0 || g.gl_pathc == 0) { if (rc == 0) { globfree(&g); } return -ENOENT; } if (is_ois) { for (i = 0; i < g.gl_pathc; i++) { if (strcmp(g.gl_pathv[i], "/dev/ttyXRUSB0") == 0) { pick = g.gl_pathv[i]; break; } } if (!pick) { pick = g.gl_pathv[0]; } } else { for (i = 0; i < g.gl_pathc; i++) { if (strcmp(g.gl_pathv[i], "/dev/ttyXRUSB1") == 0) { pick = g.gl_pathv[i]; break; } } if (!pick) { pick = g.gl_pathv[g.gl_pathc - 1]; } } snprintf(out, out_sz, "%s", pick); globfree(&g); return 0; } static int send_uart_port_event(capture_state_t *st, uint32_t typ, const char *old_path, const char *new_path) { pog422_ipc_uart_port_event_t ev; if (st->client_fd < 0) { return 0; } memset(&ev, 0, sizeof(ev)); if (old_path) { strncpy(ev.old_path, old_path, sizeof(ev.old_path) - 1u); } if (new_path) { strncpy(ev.new_path, new_path, sizeof(ev.new_path) - 1u); } return send_ipc_st(st, typ, &ev, sizeof(ev)); } static void uart_mark_lost(capture_state_t *st, const char *why) { char old_path[256]; if (st->port_lost && st->uart_fd < 0) { return; } strncpy(old_path, st->port_path, sizeof(old_path) - 1u); old_path[sizeof(old_path) - 1u] = '\0'; wide_log_stop(st); uart_detach(st); st->port_lost = true; st->hs_active = false; st->cap_gpio_led = false; st->cap_burst_on_fall = false; pending_reset(st); cap_reset_window(st); fprintf(stderr, "pog422_uartd: uart_port_lost path=%s why=%s — re-resolve/reopen\n", old_path[0] ? old_path : "(none)", why ? why : "?"); (void)send_uart_port_event(st, POG422_IPC_UART_PORT_LOST, old_path, ""); } static void uart_try_reopen(capture_state_t *st) { uint64_t now = mono_ns(); char resolved[256]; char old_path[256]; int rc; if (!st->port_lost || st->uart_fd >= 0) { return; } if (st->last_reopen_try_ns != 0 && now - st->last_reopen_try_ns < UART_REOPEN_INTERVAL_NS) { return; } st->last_reopen_try_ns = now; if (resolve_xrusb_port(st->channel, resolved, sizeof(resolved)) != 0) { if (st->last_reopen_log_ns == 0 || now - st->last_reopen_log_ns >= UART_REOPEN_LOG_INTERVAL_NS) { st->last_reopen_log_ns = now; fprintf(stderr, "pog422_uartd: still waiting for ttyXRUSB* (channel=%s)\n", st->channel); } return; } strncpy(old_path, st->port_path, sizeof(old_path) - 1u); old_path[sizeof(old_path) - 1u] = '\0'; strncpy(st->port_path, resolved, sizeof(st->port_path) - 1u); rc = uart_attach(st); if (rc != 0) { if (st->last_reopen_log_ns == 0 || now - st->last_reopen_log_ns >= UART_REOPEN_LOG_INTERVAL_NS) { st->last_reopen_log_ns = now; fprintf(stderr, "pog422_uartd: reopen %s failed (%d): %s\n", resolved, rc, strerror(rc < 0 ? -rc : rc)); } return; } st->port_lost = false; st->last_reopen_log_ns = 0; fprintf(stderr, "pog422_uartd: uart_port_reopen old=%s new=%s channel=%s\n", old_path[0] ? old_path : "(none)", st->port_path, st->channel); (void)send_uart_port_event(st, POG422_IPC_UART_PORT_REOPEN, old_path, st->port_path); } /* First open: optional --port once, then channel resolve. Reattach: channel only. */ static int uart_initial_attach(capture_state_t *st) { int rc; if (!st->first_port_tried && st->first_port_hint[0]) { st->first_port_tried = true; strncpy(st->port_path, st->first_port_hint, sizeof(st->port_path) - 1u); rc = uart_attach(st); if (rc == 0) { return 0; } fprintf(stderr, "pog422_uartd: first --port %s failed (%d): %s — channel resolve\n", st->first_port_hint, rc, strerror(rc < 0 ? -rc : rc)); } if (resolve_xrusb_port(st->channel, st->port_path, sizeof(st->port_path)) != 0) { return -ENOENT; } return uart_attach(st); } static int wide_log_env_enabled(void) { const char *v = getenv("POG422_WIDE_LOG"); if (!v || !v[0]) { return 1; /* default on when launchers export it; also on if unset in uartd */ } if (v[0] == '0' && v[1] == '\0') { return 0; } if (strcasecmp(v, "off") == 0 || strcasecmp(v, "false") == 0 || strcasecmp(v, "no") == 0) { return 0; } return 1; } static void wide_log_resolve_path(capture_state_t *st) { const char *path = getenv("POG422_WIDE_LOG_PATH"); const char *outdir; if (path && path[0]) { snprintf(st->wide_log_path, sizeof(st->wide_log_path), "%s", path); return; } outdir = getenv("OUTPUT_DIR"); if (outdir && outdir[0]) { snprintf(st->wide_log_path, sizeof(st->wide_log_path), "%s/wide_rx.bin", outdir); return; } snprintf(st->wide_log_path, sizeof(st->wide_log_path), "data/wide_rx.bin"); } static void *wide_log_thread(void *arg) { capture_state_t *st = arg; uint8_t buf[4096]; while (st->wide_run && !g_stop) { int fd = st->uart_fd; ssize_t n; if (fd < 0 || st->wide_log_fd < 0) { usleep(10000); continue; } n = pog422_wide_rx_read(fd, buf, sizeof(buf)); if (n > 0) { const uint8_t *p = buf; size_t left = (size_t)n; while (left > 0) { ssize_t w = write(st->wide_log_fd, p, left); if (w < 0) { if (errno == EINTR) { continue; } break; } p += (size_t)w; left -= (size_t)w; } continue; } if (n < 0 && n != -EAGAIN && n != -EWOULDBLOCK) { /* ENOTTY during suspend/detach — brief backoff */ usleep(5000); continue; } usleep(1000); } return NULL; } static void wide_log_stop(capture_state_t *st) { if (!st->wide_log) { return; } st->wide_run = 0; if (st->wide_thread_started) { (void)pthread_join(st->wide_tid, NULL); st->wide_thread_started = false; memset(&st->wide_tid, 0, sizeof(st->wide_tid)); } if (st->uart_fd >= 0) { (void)pog422_wide_rx_enable(st->uart_fd, 0); } if (st->wide_log_fd >= 0) { close(st->wide_log_fd); st->wide_log_fd = -1; } } static int wide_log_start(capture_state_t *st) { if (!st->wide_log || st->uart_fd < 0) { return 0; } if (st->wide_thread_started) { /* already running — re-enable tee after re-attach */ if (pog422_wide_rx_enable(st->uart_fd, 1) != 0) { return -EIO; } return 0; } wide_log_resolve_path(st); { char *slash = strrchr(st->wide_log_path, '/'); if (slash && slash != st->wide_log_path) { char dir[512]; size_t n = (size_t)(slash - st->wide_log_path); if (n >= sizeof(dir)) { n = sizeof(dir) - 1; } memcpy(dir, st->wide_log_path, n); dir[n] = '\0'; (void)mkdir(dir, 0755); } } st->wide_log_fd = open(st->wide_log_path, O_WRONLY | O_CREAT | O_APPEND, 0644); if (st->wide_log_fd < 0) { fprintf(stderr, "pog422_uartd: wide log open failed %s: %s\n", st->wide_log_path, strerror(errno)); return -errno; } if (pog422_wide_rx_enable(st->uart_fd, 1) != 0) { fprintf(stderr, "pog422_uartd: SET_WIDE_RX failed (need driver 2.2.1.15+): %s\n", strerror(errno)); close(st->wide_log_fd); st->wide_log_fd = -1; return -EIO; } st->wide_run = 1; if (pthread_create(&st->wide_tid, NULL, wide_log_thread, st) != 0) { st->wide_run = 0; (void)pog422_wide_rx_enable(st->uart_fd, 0); close(st->wide_log_fd); st->wide_log_fd = -1; return -errno; } st->wide_thread_started = true; fprintf(stderr, "pog422_uartd: wide RX log -> %s (bit0=9th data, bit0x10=HS cache)\n", st->wide_log_path); return 0; } static int uart_attach(capture_state_t *st) { if (st->uart_fd >= 0) { return 0; } if (!st->port_path[0]) { return -EINVAL; } int uart_fd = open(st->port_path, O_RDWR | O_NOCTTY | O_NONBLOCK); if (uart_fd < 0) { return -errno; } int cfg = configure_port(uart_fd); if (cfg != 0) { close(uart_fd); return cfg < 0 ? cfg : -EIO; } if (pog422_dttty_enable(uart_fd, 1) != 0) { close(uart_fd); return -EIO; } st->uart_fd = uart_fd; if (st->wide_log) { int wrc = wide_log_start(st); if (wrc != 0) { fprintf(stderr, "pog422_uartd: wide log start failed (%d) — continuing\n", wrc); } } timing_on_uart_attached(st); return 0; } static int uart_suspend_action(capture_state_t *st) { /* Leave wide-log thread running; it idles while uart_fd < 0. */ uart_detach(st); return 0; } static int uart_resume_action(capture_state_t *st) { /* Channel resolve — do not stick to a stale --port path. */ if (resolve_xrusb_port(st->channel, st->port_path, sizeof(st->port_path)) != 0) { return -ENOENT; } return uart_attach(st); } static int handle_uart_gate(capture_state_t *st, uint32_t ack_type, int (*action)(capture_state_t *)) { pog422_ipc_uart_gate_ack_t ack = {.err = 0}; int rc = action(st); if (rc != 0) { ack.err = rc < 0 ? rc : -rc; } return send_ipc_st(st, ack_type, &ack, sizeof(ack)); } /* EG 3.0.0.9: inject worker — main poll keeps servicing GPIO7 / GET_SLV_IN. */ static void *inject_worker_main(void *arg) { capture_state_t *st = (capture_state_t *)arg; while (!g_stop) { uint8_t *payload = NULL; uint32_t len = 0; int rc; pthread_mutex_lock(&st->inject_mu); while (!st->inject_job_pending && !g_stop) { pthread_cond_wait(&st->inject_cv, &st->inject_mu); } if (g_stop && !st->inject_job_pending) { pthread_mutex_unlock(&st->inject_mu); break; } payload = st->inject_job_payload; len = st->inject_job_len; st->inject_job_payload = NULL; st->inject_job_len = 0; st->inject_job_pending = 0; st->inject_busy = 1; st->inject_abort = 0; pthread_mutex_unlock(&st->inject_mu); rc = handle_inject(st, payload, len); free(payload); if (st->inject_abort) { (void)pog422_gpio7_rx(st->uart_fd); } if (st->uart_fd >= 0 && (st->port_lost || uart_fd_is_dead(st->uart_fd) || (rc < 0 && errno_is_uart_dead(-rc)))) { uart_mark_lost(st, "inject_path"); } pthread_mutex_lock(&st->inject_mu); st->inject_busy = 0; st->inject_abort = 0; pthread_mutex_unlock(&st->inject_mu); } return NULL; } static int handle_client_message(capture_state_t *st) { uint32_t hdr[3]; if (read_exact(st->client_fd, hdr, sizeof(hdr)) != 0) { return -1; } if (hdr[0] != POG422_IPC_MAGIC) { return -1; } if (hdr[2] > IPC_IO_MAX) { return -1; } uint8_t *payload = NULL; if (hdr[2] > 0) { payload = malloc(hdr[2]); if (!payload) { return -1; } if (read_exact(st->client_fd, payload, hdr[2]) != 0) { free(payload); return -1; } } int rc = 0; switch (hdr[1]) { case POG422_IPC_INJECT: if (st->uart_fd < 0 || st->port_lost) { pog422_ipc_inject_done_t done = {0}; /* ENODEV: Python requeues (distinct from HS-busy EBUSY). */ done.err = ENODEV; rc = send_ipc_st(st, POG422_IPC_INJECT_DONE, &done, sizeof(done)); break; } pthread_mutex_lock(&st->inject_mu); if (st->inject_busy || st->inject_job_pending) { pthread_mutex_unlock(&st->inject_mu); { pog422_ipc_inject_done_t done = {0}; done.err = EBUSY; rc = send_ipc_st(st, POG422_IPC_INJECT_DONE, &done, sizeof(done)); } break; } /* Hand payload to worker; do not free here. */ st->inject_job_payload = payload; st->inject_job_len = hdr[2]; payload = NULL; st->inject_job_pending = 1; pthread_cond_signal(&st->inject_cv); pthread_mutex_unlock(&st->inject_mu); rc = 0; break; case POG422_IPC_GET_SLV_IN: { int active = 0; if (st->uart_fd >= 0) { active = slv_in_read(st->uart_fd) ? 1 : 0; } pog422_ipc_slv_in_t si = {.active = (uint8_t)active}; rc = send_ipc_st(st, POG422_IPC_SLV_IN, &si, sizeof(si)); break; } case POG422_IPC_GPIO7_ASSERT: rc = handle_gpio7(st, 1); break; case POG422_IPC_GPIO7_RELEASE: rc = handle_gpio7(st, 0); break; case POG422_IPC_GET_HAT_REGS: { pog422_ipc_hat_regs_t hr; uint32_t gpio_mode = 0; uint32_t uart_flow = 0; uint32_t gpio_mode_ch0 = 0; uint32_t uart_flow_ch0 = 0; int hat_rc = pog422_hat_read_regs(st->uart_fd, &gpio_mode, &uart_flow); int hat_rc0 = pog422_hat_read_regs_ch(st->uart_fd, 0, &gpio_mode_ch0, &uart_flow_ch0); memset(&hr, 0, sizeof(hr)); hr.err = hat_rc != 0 ? hat_rc : hat_rc0; if (hat_rc == 0) { hr.gpio_mode = gpio_mode; hr.uart_flow = uart_flow; } if (hat_rc0 == 0) { hr.gpio_mode_ch0 = gpio_mode_ch0; hr.uart_flow_ch0 = uart_flow_ch0; } rc = send_ipc_st(st, POG422_IPC_HAT_REGS, &hr, sizeof(hr)); break; } case POG422_IPC_UART_SUSPEND: rc = handle_uart_gate(st, POG422_IPC_UART_SUSPEND_ACK, uart_suspend_action); break; case POG422_IPC_UART_RESUME: rc = handle_uart_gate(st, POG422_IPC_UART_RESUME_ACK, uart_resume_action); break; case POG422_IPC_GET_VERSION: { pog422_ipc_version_t ver; memset(&ver, 0, sizeof(ver)); strncpy(ver.uartd, POG422_UARTD_VERSION, sizeof(ver.uartd) - 1u); strncpy(ver.inject, pog422_inject_version(), sizeof(ver.inject) - 1u); strncpy(ver.eg, POG422_EG_VERSION, sizeof(ver.eg) - 1u); ver.hw_debug = (uint8_t)HWDBG_ENABLED; rc = send_ipc_st(st, POG422_IPC_VERSION, &ver, sizeof(ver)); break; } case POG422_IPC_SHUTDOWN: g_stop = 1; pthread_mutex_lock(&st->inject_mu); pthread_cond_broadcast(&st->inject_cv); pthread_mutex_unlock(&st->inject_mu); break; default: rc = -1; break; } free(payload); return rc; } static void usage(const char *argv0) { fprintf( stderr, "Usage: %s [--port /dev/ttyXRUSB1] [--channel machine|ois] --socket /tmp/pog422_uart.sock\n" " --port first-open hint only; attach/reattach use --channel resolve\n" " --channel machine (default) or ois\n", argv0 ); } int main(int argc, char **argv) { const char *port = NULL; const char *channel = "machine"; const char *sock_path = "/tmp/pog422_uart.sock"; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) { port = argv[++i]; } else if (strcmp(argv[i], "--channel") == 0 && i + 1 < argc) { channel = argv[++i]; } else if (strcmp(argv[i], "--socket") == 0 && i + 1 < argc) { sock_path = argv[++i]; } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { usage(argv[0]); return 0; } else { fprintf(stderr, "Unknown arg: %s\n", argv[i]); usage(argv[0]); return 1; } } if (!port && strcmp(channel, "machine") != 0 && strcmp(channel, "ois") != 0) { usage(argv[0]); return 1; } if (strcmp(channel, "machine") != 0 && strcmp(channel, "ois") != 0) { fprintf(stderr, "Invalid --channel %s (use machine|ois)\n", channel); return 1; } if (!port) { /* Channel-only: resolve at attach (no first-try hint). */ port = ""; } signal(SIGINT, on_signal); signal(SIGTERM, on_signal); capture_state_t st; memset(&st, 0, sizeof(st)); st.uart_fd = -1; st.client_fd = -1; st.wide_log_fd = -1; st.dttty_rx = true; st.wide_log = wide_log_env_enabled(); pthread_mutex_init(&st.ipc_mu, NULL); pthread_mutex_init(&st.inject_mu, NULL); pthread_cond_init(&st.inject_cv, NULL); if (pthread_create(&st.inject_tid, NULL, inject_worker_main, &st) != 0) { fprintf(stderr, "pog422_uartd: inject worker pthread_create failed\n"); return 1; } st.inject_thread_started = true; strncpy(st.channel, channel, sizeof(st.channel) - 1); if (port[0]) { strncpy(st.first_port_hint, port, sizeof(st.first_port_hint) - 1); } #if POG422_HW_DEBUG pog422_hwdbg_ring_init(&st.hwdbg); fprintf(stderr, "pog422_uartd: POG422_HW_DEBUG=1 (dense HS/GPIO timeline + HS porch stream)\n"); #endif timing_start(&st, channel); int attach_rc = uart_initial_attach(&st); if (attach_rc != 0) { if (attach_rc == -ENOTTY) { fprintf(stderr, "hat/GPIO ioctl failed on %s — wrong driver or rebuild inject-shim\n", st.port_path[0] ? st.port_path : port); } fprintf(stderr, "pog422_uartd: initial attach failed (%d): %s — waiting for ttyXRUSB*\n", attach_rc, strerror(attach_rc < 0 ? -attach_rc : attach_rc)); st.port_lost = true; } fprintf(stderr, "pog422_uartd %s port=%s channel=%s socket=%s slv-in=%s dttty=1 wide_log=%d lost=%d\n", POG422_UARTD_VERSION, st.port_path[0] ? st.port_path : "(none)", st.channel, sock_path, pog422_slv_in_mode(), st.wide_log ? 1 : 0, st.port_lost ? 1 : 0); unlink(sock_path); int listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket"); uart_detach(&st); return 1; } struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { perror("bind"); close(listen_fd); uart_detach(&st); return 1; } if (listen(listen_fd, 1) != 0) { perror("listen"); close(listen_fd); uart_detach(&st); return 1; } struct pollfd pfds[2]; while (!g_stop) { if (st.client_fd < 0) { st.client_fd = accept(listen_fd, NULL, NULL); if (st.client_fd < 0) { if (errno == EINTR) { continue; } perror("accept"); usleep(100000); continue; } fprintf(stderr, "client connected\n"); { int sndbuf = 256 * 1024; setsockopt(st.client_fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); } cap_reset_window(&st); pending_reset(&st); if (st.uart_fd >= 0) { (void)tcflush(st.uart_fd, TCIFLUSH); (void)pog422_dttty_flush(st.uart_fd); if (!st.dttty_rx) { uint8_t trash[512]; while (read(st.uart_fd, trash, sizeof(trash)) > 0) { } } } } /* Only auto-reopen after unexpected loss — not intentional UART_SUSPEND. */ if (st.port_lost) { uart_try_reopen(&st); } pfds[0].fd = st.client_fd; pfds[0].events = POLLIN; pfds[1].fd = st.uart_fd; pfds[1].events = st.uart_fd >= 0 ? (POLLIN | POLLERR | POLLHUP) : 0; int pr = poll(pfds, 2, 1); if (pr < 0) { if (errno == EINTR) { continue; } perror("poll"); break; } if (st.timing_on) { uint64_t wake = mono_ns(); uint64_t gap = wake - st.timing_last_poll_ns; /* Nominal poll timeout is 1 ms — only log late wakes. */ if (gap >= 1500000ull) { timing_push(&st, POG422_TT_ID_POLL_WAKE, (uint32_t)gap, (uint32_t)pr, 0); } st.timing_last_poll_ns = wake; } if (pfds[0].revents & (POLLIN | POLLHUP | POLLERR)) { while (st.client_fd >= 0) { if (handle_client_message(&st) != 0) { close(st.client_fd); st.client_fd = -1; fprintf(stderr, "client disconnected\n"); break; } struct pollfd cp = {.fd = st.client_fd, .events = POLLIN}; if (poll(&cp, 1, 0) <= 0 || !(cp.revents & POLLIN)) { break; } } } if (st.uart_fd >= 0 && (pfds[1].revents & (POLLERR | POLLHUP | POLLNVAL))) { uart_mark_lost(&st, "poll_err"); } if (st.uart_fd >= 0 && !st.inject_busy) { capture_poll(&st); } } g_stop = 1; if (st.inject_thread_started) { pthread_mutex_lock(&st.inject_mu); pthread_cond_broadcast(&st.inject_cv); pthread_mutex_unlock(&st.inject_mu); (void)pthread_join(st.inject_tid, NULL); st.inject_thread_started = false; } free(st.inject_job_payload); st.inject_job_payload = NULL; pthread_mutex_destroy(&st.ipc_mu); pthread_mutex_destroy(&st.inject_mu); pthread_cond_destroy(&st.inject_cv); if (st.client_fd >= 0) { close(st.client_fd); } close(listen_fd); timing_stop(&st); wide_log_stop(&st); uart_detach(&st); unlink(sock_path); return 0; }