// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // x68kSerialPeer.c - the host end of the X68000 serial gate. // // x68kSerialPeer [--port N] [--probe TEXT] [--timeout SECONDS] // [--until TEXT] [--listen-only] // // MAME's null_modem socket form CONNECTS outward, so this side listens (on // 127.0.0.1:N, default 6800). Once the guest's SERIAL.X is up it echoes // every byte it receives, so sending a probe string (default JOEYLIB-X68K) // and reading it back proves both directions of the link. // // The guest takes a while to appear: Human68k boots, then COMMAND.X runs // AUTOEXEC.BAT, then SERIAL.X initialises. Rather than guess, this // retransmits the probe every RESEND_SECONDS until the echo comes back or // the deadline (--timeout, default 420 seconds) passes. // // --listen-only never transmits: it isolates the guest's transmit path // (keyboard -> jlSerialWrite) from the echo. It passes once --until's text // has arrived, or at the deadline when any bytes did. // // Each option takes its value as the next argument or after "=" (--port=N); // -h or --help prints the usage. Prints an X68SER line for each step and // each chunk received (bytes in double quotes, escaped where they are not // printable), then one X68SER PASS, COLLECTED, or FAIL line; exit 0 on // pass, 1 on fail, 2 on bad arguments. #define _DEFAULT_SOURCE #include #include #include #include #include #include "gateArg.h" #include "gateNet.h" #include "gateText.h" #include "toolBuf.h" #define TOOL_NAME "x68kSerialPeer" #define HELP_EXIT 0 #define DEFAULT_PORT 6800u #define DEFAULT_PROBE "JOEYLIB-X68K" #define DEFAULT_TIMEOUT 420.0 // The probe's resend interval, and how long one read waits. #define RESEND_SECONDS 5.0 #define READ_SECONDS 2.0 #define READ_SIZE 256u #define SHOWN_BYTES 64u // What the command line asked for. typedef struct ArgsT { uint16_t port; const char *probe; double timeout; const char *until; bool listenOnly; } ArgsT; static void argsRead(ArgsT *args, int argc, char **argv); static const char *optionValue(int argc, char **argv, int *i, const char *name); static int peer(const ArgsT *args); static void usage(FILE *out, int status) __attribute__((noreturn)); static void argsRead(ArgsT *args, int argc, char **argv) { const char *value; int i; args->port = DEFAULT_PORT; args->probe = DEFAULT_PROBE; args->timeout = DEFAULT_TIMEOUT; args->until = ""; args->listenOnly = false; for (i = 1; i < argc; i++) { if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { usage(stdout, HELP_EXIT); } if (strcmp(argv[i], "--listen-only") == 0) { args->listenOnly = true; } else if ((value = optionValue(argc, argv, &i, "--port")) != NULL) { args->port = gateArgPort(value); } else if ((value = optionValue(argc, argv, &i, "--probe")) != NULL) { args->probe = value; } else if ((value = optionValue(argc, argv, &i, "--timeout")) != NULL) { args->timeout = gateArgDouble("timeout", value); } else if ((value = optionValue(argc, argv, &i, "--until")) != NULL) { args->until = value; } else { usage(stderr, GATE_USAGE_EXIT); } } } // The value of option name when argv[*i] is it: "--name VALUE" (the next // argument, taken) or "--name=VALUE". NULL when argv[*i] is not name; a // name without its value is a usage error. static const char *optionValue(int argc, char **argv, int *i, const char *name) { const char *arg; size_t len; arg = argv[*i]; len = strlen(name); if (strncmp(arg, name, len) != 0) { return NULL; } if (arg[len] == '=') { return arg + len + 1u; } if (arg[len] != '\0') { return NULL; } if (*i + 1 >= argc) { usage(stderr, GATE_USAGE_EXIT); } *i += 1; return argv[*i]; } // Listen, then send the probe until it comes back (or, listen-only, collect // what the guest sends) until the deadline. static int peer(const ArgsT *args) { struct sockaddr_in from; ToolBufT received; uint8_t chunk[READ_SIZE]; size_t probeLen; size_t untilLen; size_t sentTotal; double deadline; double lastSend; bool sentOnce; int32_t got; int listener; int conn; probeLen = strlen(args->probe); untilLen = strlen(args->until); deadline = gateNetNow() + args->timeout; listener = gateNetListen(args->port, GATE_NET_ONE_PEER); printf("X68SER listening on %s:%u\n", GATE_NET_HOST, (unsigned)args->port); conn = gateNetAccept(listener, args->timeout, &from); close(listener); if (conn < 0) { printf("X68SER FAIL: MAME never connected\n"); return 1; } printf("X68SER connected from %s:%u\n", inet_ntoa(from.sin_addr), (unsigned)ntohs(from.sin_port)); memset(&received, 0, sizeof(received)); sentTotal = 0u; lastSend = 0.0; sentOnce = false; while (gateNetNow() < deadline) { // Resend: the guest is not listening until SERIAL.X has booted and // opened the port, and nothing here can see when that happens. if (!args->listenOnly && (!sentOnce || gateNetNow() - lastSend > RESEND_SECONDS)) { if (!gateNetSendAll(conn, args->probe, probeLen)) { printf("X68SER FAIL: send error: %s\n", strerror(errno)); return 1; } sentTotal += probeLen; lastSend = gateNetNow(); sentOnce = true; } got = gateNetRecv(conn, chunk, sizeof(chunk), READ_SECONDS); if (got == GATE_NET_IDLE) { continue; } if (got == GATE_NET_CLOSED) { if (errno != 0) { printf("X68SER FAIL: recv error: %s\n", strerror(errno)); } else { printf("X68SER FAIL: link closed by MAME\n"); } return 1; } toolBufAdd(&received, chunk, (size_t)got); printf("X68SER rx %d bytes: ", got); gateNetShowBytes(stdout, chunk, (size_t)got); printf("\n"); if (args->listenOnly && untilLen > 0u && gateTextFind(received.data, received.len, 0u, args->until, untilLen) != GATE_TEXT_NOT_FOUND) { printf("X68SER PASS: guest transmitted %zu bytes unprompted: ", received.len); gateNetShowBytes(stdout, received.data, received.len); printf("\n"); return 0; } if (gateTextFind(received.data, received.len, 0u, args->probe, probeLen) != GATE_TEXT_NOT_FOUND) { printf("X68SER PASS: probe echoed back after %.1fs (sent %zu bytes, received %zu)\n", args->timeout - (deadline - gateNetNow()), sentTotal, received.len); return 0; } } if (args->listenOnly && received.len > 0u) { printf("X68SER COLLECTED %zu bytes: ", received.len); gateNetShowBytes(stdout, received.data, received.len); printf("\n"); return 0; } printf("X68SER FAIL: timeout. sent=%zu received=%zu bytes: ", sentTotal, received.len); gateNetShowBytes(stdout, received.data, received.len < SHOWN_BYTES ? received.len : SHOWN_BYTES); printf("\n"); return 1; } static void usage(FILE *out, int status) { fprintf(out, "usage: %s [--port N] [--probe TEXT] [--timeout SECONDS] [--until TEXT] [--listen-only]\n", TOOL_NAME); exit(status); } int main(int argc, char **argv) { ArgsT args; gateNetStart(TOOL_NAME); argsRead(&args, argc, argv); return peer(&args); }