222 lines
7.8 KiB
C
222 lines
7.8 KiB
C
// 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 initializes. 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 <arpa/inet.h>
|
|
#include <errno.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#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);
|
|
}
|