summaryrefslogtreecommitdiff
path: root/stacks/tncs/tnc2c-probe.c
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-07 18:25:13 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-07 18:25:13 +0000
commit04d965d67a7264a1c7c211494aebda1953df7603 (patch)
tree0ebd700a6e219f84a26a656f4bee8778bc75da7b /stacks/tncs/tnc2c-probe.c
Initial push
Diffstat (limited to 'stacks/tncs/tnc2c-probe.c')
-rw-r--r--stacks/tncs/tnc2c-probe.c535
1 files changed, 535 insertions, 0 deletions
diff --git a/stacks/tncs/tnc2c-probe.c b/stacks/tncs/tnc2c-probe.c
new file mode 100644
index 0000000..8770409
--- /dev/null
+++ b/stacks/tncs/tnc2c-probe.c
@@ -0,0 +1,535 @@
+/*
+ * tnc2c-probe — scan /dev/ttyS* and /dev/ttyUSB* for a Landolt TNC2C.
+ *
+ * TNC2C host port (per manual): 7-bit, even parity, 1 stop bit.
+ * Terminal speed set by TERM jumper (this unit: 19200; manual default often 9600).
+ * RTS/CTS handshake; DTR asserted to wake some adapters.
+ *
+ * Build: make
+ * Run: ./tnc2c-probe
+ * ./tnc2c-probe /dev/ttyS4
+ */
+
+#if defined(__linux__) || defined(__GLIBC__)
+#define _DEFAULT_SOURCE 1
+#endif
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <glob.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+
+#define PROBE_READ_MS 900
+#define PROBE_GAP_MS 120
+#define RX_BUF_SIZE 4096
+#define MAX_DEVICES 64
+
+typedef struct serial_profile {
+ const char *label;
+ speed_t speed;
+ unsigned int baud_num;
+ tcflag_t databits;
+ tcflag_t parity;
+} serial_profile_t;
+
+typedef struct probe_result {
+ char device[64];
+ char profile[32];
+ char command[32];
+ int got_data;
+ size_t len;
+ char text[RX_BUF_SIZE];
+ unsigned char raw[RX_BUF_SIZE];
+} probe_result_t;
+
+static const serial_profile_t PROFILES[] = {
+ { "19200-7E1", B19200, 19200, CS7, PARENB },
+ { "19200-8N1", B19200, 19200, CS8, 0 },
+ { "9600-7E1", B9600, 9600, CS7, PARENB },
+ { "9600-8N1", B9600, 9600, CS8, 0 },
+ { "4800-7E1", B4800, 4800, CS7, PARENB },
+ { "2400-7E1", B2400, 2400, CS7, PARENB },
+ { "1200-7E1", B1200, 1200, CS7, PARENB },
+};
+
+static const char *PROBE_COMMANDS[] = {
+ "\r",
+ "INFO\r",
+ "HELP\r",
+ "?\r",
+};
+
+static int verbose = 0;
+
+static int sleep_ms(unsigned ms)
+{
+ struct timespec ts;
+
+ ts.tv_sec = (time_t)(ms / 1000u);
+ ts.tv_nsec = (long)(ms % 1000u) * 1000000L;
+ return nanosleep(&ts, NULL);
+}
+
+static int set_modem_lines(int fd, int rts_on, int dtr_on)
+{
+#ifdef TIOCMGET
+ int flags;
+
+ if (ioctl(fd, TIOCMGET, &flags) != 0) {
+ return -1;
+ }
+ if (rts_on) {
+ flags |= TIOCM_RTS;
+ } else {
+ flags &= ~TIOCM_RTS;
+ }
+ if (dtr_on) {
+ flags |= TIOCM_DTR;
+ } else {
+ flags &= ~TIOCM_DTR;
+ }
+ return ioctl(fd, TIOCMSET, &flags);
+#else
+ (void)fd;
+ (void)rts_on;
+ (void)dtr_on;
+ return 0;
+#endif
+}
+
+static int read_modem_lines(int fd, int *rts, int *cts, int *dtr, int *dsr)
+{
+#ifdef TIOCMGET
+ int flags;
+
+ if (ioctl(fd, TIOCMGET, &flags) != 0) {
+ return -1;
+ }
+ if (rts != NULL) {
+ *rts = (flags & TIOCM_RTS) != 0;
+ }
+ if (cts != NULL) {
+ *cts = (flags & TIOCM_CTS) != 0;
+ }
+ if (dtr != NULL) {
+ *dtr = (flags & TIOCM_DTR) != 0;
+ }
+ if (dsr != NULL) {
+ *dsr = (flags & TIOCM_DSR) != 0;
+ }
+ return 0;
+#else
+ (void)fd;
+ if (rts) {
+ *rts = -1;
+ }
+ if (cts) {
+ *cts = -1;
+ }
+ if (dtr) {
+ *dtr = -1;
+ }
+ if (dsr) {
+ *dsr = -1;
+ }
+ return 0;
+#endif
+}
+
+static int configure_port(int fd, const serial_profile_t *profile)
+{
+ struct termios tty;
+
+ if (tcgetattr(fd, &tty) != 0) {
+ return -1;
+ }
+
+ cfmakeraw(&tty);
+ tty.c_cflag |= (CLOCAL | CREAD);
+ tty.c_cflag &= ~(CSIZE | PARENB | PARODD | CSTOPB);
+ tty.c_cflag |= profile->databits;
+ if (profile->parity != 0) {
+ tty.c_cflag |= profile->parity;
+ }
+ tty.c_cc[VMIN] = 0;
+ tty.c_cc[VTIME] = 1;
+
+#if defined(__linux__) || defined(__GLIBC__)
+ if (cfsetspeed(&tty, profile->speed) != 0) {
+ cfsetispeed(&tty, profile->speed);
+ cfsetospeed(&tty, profile->speed);
+ }
+#else
+ cfsetispeed(&tty, profile->speed);
+ cfsetospeed(&tty, profile->speed);
+#endif
+
+ if (tcsetattr(fd, TCSANOW, &tty) != 0) {
+ return -1;
+ }
+
+ tcflush(fd, TCIOFLUSH);
+ return 0;
+}
+
+static int is_printable_ascii(unsigned char c)
+{
+ return c == '\r' || c == '\n' || c == '\t' || (c >= 0x20 && c <= 0x7e);
+}
+
+static void sanitize_text(const unsigned char *raw, size_t len, char *out, size_t out_cap)
+{
+ size_t i;
+ size_t o = 0;
+
+ if (out_cap == 0) {
+ return;
+ }
+
+ for (i = 0; i < len && o + 1 < out_cap; i++) {
+ unsigned char c = raw[i];
+
+ if (c == '\r') {
+ if (o + 2 < out_cap) {
+ out[o++] = '\\';
+ out[o++] = 'r';
+ }
+ } else if (c == '\n') {
+ if (o + 2 < out_cap) {
+ out[o++] = '\\';
+ out[o++] = 'n';
+ }
+ } else if (is_printable_ascii(c)) {
+ out[o++] = (char)c;
+ } else {
+ int n = snprintf(out + o, out_cap - o, "\\x%02x", c);
+ if (n <= 0 || (size_t)n >= out_cap - o) {
+ break;
+ }
+ o += (size_t)n;
+ }
+ }
+ out[o] = '\0';
+}
+
+static int looks_like_tnc(const unsigned char *raw, size_t len)
+{
+ static const char *needles[] = {
+ "cmd:", "CMD:", "TNC", "tnc", "INFO", "help", "HELP",
+ "***", "connected", "DISCONNECTED", "kiss", "KISS",
+ "MYCALL", "WA8DED", "NOCALL", "?"
+ };
+ char buf[RX_BUF_SIZE];
+ size_t i;
+
+ if (len == 0) {
+ return 0;
+ }
+
+ sanitize_text(raw, len, buf, sizeof(buf));
+ for (i = 0; i < sizeof(needles) / sizeof(needles[0]); i++) {
+ if (strstr(buf, needles[i]) != NULL) {
+ return 1;
+ }
+ }
+ return len >= 3;
+}
+
+static size_t read_for_ms(int fd, unsigned char *buf, size_t cap, unsigned ms)
+{
+ size_t total = 0;
+ unsigned elapsed = 0;
+
+ while (elapsed < ms && total < cap) {
+ ssize_t n = read(fd, buf + total, cap - total);
+
+ if (n > 0) {
+ total += (size_t)n;
+ elapsed = 0;
+ continue;
+ }
+ if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
+ break;
+ }
+ sleep_ms(40);
+ elapsed += 40;
+ }
+
+ return total;
+}
+
+static int probe_once(const char *device, const serial_profile_t *profile,
+ const char *command, probe_result_t *result)
+{
+ int fd;
+ ssize_t wlen;
+
+ memset(result, 0, sizeof(*result));
+ snprintf(result->device, sizeof(result->device), "%s", device);
+ snprintf(result->profile, sizeof(result->profile), "%s", profile->label);
+ snprintf(result->command, sizeof(result->command), "%s",
+ command[0] == '\r' ? "<CR>" : command);
+
+ fd = open(device, O_RDWR | O_NOCTTY | O_NONBLOCK);
+ if (fd < 0) {
+ return -1;
+ }
+
+ if (configure_port(fd, profile) != 0) {
+ close(fd);
+ return -1;
+ }
+
+ (void)set_modem_lines(fd, 1, 1);
+ sleep_ms(80);
+ (void)read_for_ms(fd, result->raw, sizeof(result->raw), 150);
+
+ wlen = write(fd, command, strlen(command));
+ if (wlen < 0) {
+ close(fd);
+ return -1;
+ }
+
+ sleep_ms(PROBE_GAP_MS);
+ result->len = read_for_ms(fd, result->raw, sizeof(result->raw), PROBE_READ_MS);
+ close(fd);
+
+ result->got_data = result->len > 0;
+ sanitize_text(result->raw, result->len, result->text, sizeof(result->text));
+ return 0;
+}
+
+static int device_accessible(const char *path)
+{
+ return access(path, R_OK | W_OK) == 0;
+}
+
+static int collect_devices(char devices[][64], int max_devices)
+{
+ glob_t g;
+ size_t i;
+ int count = 0;
+ static const char *patterns[] = {
+ "/dev/ttyUSB[0-9]*",
+ "/dev/ttyS[0-9]*",
+ NULL
+ };
+ int p;
+
+ for (p = 0; patterns[p] != NULL; p++) {
+ if (glob(patterns[p], 0, NULL, &g) != 0) {
+ continue;
+ }
+ for (i = 0; i < g.gl_pathc && count < max_devices; i++) {
+ const char *path = g.gl_pathv[i];
+ int j;
+ int dup = 0;
+
+ for (j = 0; j < count; j++) {
+ if (strcmp(devices[j], path) == 0) {
+ dup = 1;
+ break;
+ }
+ }
+ if (!dup) {
+ snprintf(devices[count], 64, "%s", path);
+ count++;
+ }
+ }
+ globfree(&g);
+ }
+
+ return count;
+}
+
+static void print_device_header(const char *device)
+{
+ int fd;
+ int rts = -1, cts = -1, dtr = -1, dsr = -1;
+
+ printf("\n============================================================\n");
+ printf("Device: %s\n", device);
+ if (!device_accessible(device)) {
+ printf("Access: NO (need dialout group or root)\n");
+ return;
+ }
+ printf("Access: yes\n");
+
+ fd = open(device, O_RDWR | O_NOCTTY | O_NONBLOCK);
+ if (fd >= 0) {
+ if (read_modem_lines(fd, &rts, &cts, &dtr, &dsr) == 0) {
+ printf("Lines (before probe): RTS=%d CTS=%d DTR=%d DSR=%d\n",
+ rts, cts, dtr, dsr);
+ }
+ close(fd);
+ }
+}
+
+static void listen_quiet(const char *device, const serial_profile_t *profile)
+{
+ int fd;
+ unsigned char buf[512];
+ size_t n;
+
+ fd = open(device, O_RDWR | O_NOCTTY | O_NONBLOCK);
+ if (fd < 0) {
+ return;
+ }
+ if (configure_port(fd, profile) != 0) {
+ close(fd);
+ return;
+ }
+ (void)set_modem_lines(fd, 1, 1);
+ n = read_for_ms(fd, buf, sizeof(buf), 1200);
+ close(fd);
+
+ if (n > 0) {
+ char text[512];
+ sanitize_text(buf, n, text, sizeof(text));
+ printf(" Spontaneous traffic @ %s: %s\n", profile->label, text);
+ } else if (verbose) {
+ printf(" Spontaneous traffic @ %s: (none)\n", profile->label);
+ }
+}
+
+static int run_device_scan(const char *device)
+{
+ size_t pi;
+ size_t ci;
+ int hits = 0;
+ int best_score = 0;
+ char best_profile[32] = "";
+ char best_text[512] = "";
+
+ print_device_header(device);
+ if (!device_accessible(device)) {
+ return 0;
+ }
+
+ listen_quiet(device, &PROFILES[0]);
+ listen_quiet(device, &PROFILES[1]);
+
+ for (pi = 0; pi < sizeof(PROFILES) / sizeof(PROFILES[0]); pi++) {
+ for (ci = 0; ci < sizeof(PROBE_COMMANDS) / sizeof(PROBE_COMMANDS[0]); ci++) {
+ probe_result_t result;
+ int score;
+
+ if (probe_once(device, &PROFILES[pi], PROBE_COMMANDS[ci], &result) != 0) {
+ continue;
+ }
+ if (!result.got_data) {
+ if (verbose) {
+ printf(" %s + %s -> (no data)\n",
+ result.profile, result.command);
+ }
+ continue;
+ }
+
+ score = looks_like_tnc(result.raw, result.len) ? 2 : 1;
+ hits++;
+ printf(" HIT %s + %s (%zu bytes, score=%d)\n",
+ result.profile, result.command, result.len, score);
+ printf(" %s\n", result.text);
+
+ if (score > best_score) {
+ best_score = score;
+ snprintf(best_profile, sizeof(best_profile), "%s", result.profile);
+ strncpy(best_text, result.text, sizeof(best_text) - 1);
+ best_text[sizeof(best_text) - 1] = '\0';
+ }
+ }
+ }
+
+ if (hits == 0) {
+ printf(" Result: no response on any profile/command.\n");
+ printf(" Hint: check power, cable (DTE<->DCE modem cable), baud jumper.\n");
+ } else {
+ printf(" Result: %d response(s); best profile=%s (score=%d)\n",
+ hits, best_profile[0] ? best_profile : "?", best_score);
+ if (best_text[0] != '\0') {
+ printf(" Best: %s\n", best_text);
+ }
+ }
+
+ return hits;
+}
+
+static void usage(const char *argv0)
+{
+ fprintf(stderr,
+ "Usage: %s [-v] [device ...]\n"
+ "\n"
+ "Scan serial ports for a Landolt TNC2C.\n"
+ "Without device arguments, probes all /dev/ttyUSB* and /dev/ttyS*.\n"
+ "\n"
+ "Options:\n"
+ " -v verbose (show silent probes too)\n"
+ " -h this help\n",
+ argv0);
+}
+
+int main(int argc, char **argv)
+{
+ char devices[MAX_DEVICES][64];
+ int device_count = 0;
+ int argi;
+ int total_hits = 0;
+ int scanned = 0;
+
+ for (argi = 1; argi < argc; argi++) {
+ if (strcmp(argv[argi], "-h") == 0 || strcmp(argv[argi], "--help") == 0) {
+ usage(argv[0]);
+ return 0;
+ }
+ if (strcmp(argv[argi], "-v") == 0 || strcmp(argv[argi], "--verbose") == 0) {
+ verbose = 1;
+ continue;
+ }
+ if (device_count < MAX_DEVICES) {
+ snprintf(devices[device_count], 64, "%s", argv[argi]);
+ device_count++;
+ }
+ }
+
+ if (device_count == 0) {
+ device_count = collect_devices(devices, MAX_DEVICES);
+ }
+
+ printf("TNC2C serial probe\n");
+ printf("Profiles: 7E1 (manual) and 8N1 (HyBBX default) at common baud rates.\n");
+ printf("Found %d serial device(s).\n", device_count);
+
+ if (device_count == 0) {
+ fprintf(stderr, "No /dev/ttyUSB* or /dev/ttyS* devices found.\n");
+ return 1;
+ }
+
+ for (argi = 0; argi < device_count; argi++) {
+ total_hits += run_device_scan(devices[argi]);
+ scanned++;
+ }
+
+ printf("\n============================================================\n");
+ printf("Summary: scanned %d port(s), %d response(s) total.\n",
+ scanned, total_hits);
+
+ if (total_hits == 0) {
+ printf("\nCable note (TNC2C manual):\n");
+ printf(" - TNC2C DB25 is wired like a modem (DCE).\n");
+ printf(" - PC/USB-serial is DTE -> use a straight modem cable,\n");
+ printf(" not a null-modem crossover cable.\n");
+ printf(" - Some PCs need RTS-CTS bridged on the host side (pins 4+5).\n");
+ printf(" - Host settings: 7-bit, even parity (7E1), baud per TERM jumper.\n");
+ printf(" - LEDs after power-on: Power on; Status+Connected off after ~3 s.\n");
+ return 2;
+ }
+
+ return 0;
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com