/* * Copyright (c) 2021-2022 joshua stein * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /* * On the Macintosh Plus, the RS422 serial ports don't have the GPIn pin * connected like later models do to the Carrier Detect pin on the DCE. * This means we can't properly detect when the call drops, other than * just looking for "NO CARRIER" in the input stream. * * We do have the ability to assert and de-assert DTR so we can forcefully * hangup a call from our end and then reset the modem to put it into a * known good state in preparation for a new call. */ #include #include #include #include #include #include "logger.h" #include "serial_local.h" #include "session.h" #include "subtext.h" #include "util.h" /* more speeds defined in Inside Macintosh: Serial Driver, may not work */ #ifndef baud14400 #define baud14400 6 #endif #ifndef baud28800 #define baud28800 2 #endif #ifndef baud38400 #define baud38400 1 #endif /* * The number of seconds after an ATA that we'll allow a connection to * establish and a session to start, before we hang up and reset the * modem. */ #define SERIAL_CONNECT_TIMEOUT 45 /* The number of seconds of continuous write timeouts before we hang up */ #define SERIAL_WRITE_GIVE_UP 45 static short serial_out_refnum = 0, serial_in_refnum = 0; static char serial_input_internal_buf[1024]; static char serial_input_buf[128]; static size_t serial_input_buf_len = 0; static short serial_rings = 0; static time_t serial_last_ring = 0; static ParamBlockRec serial_write_pbr = { 0 }; enum { SERIAL_STATE_IDLE = 0, SERIAL_STATE_ANSWERED, SERIAL_STATE_CONNECTED, SERIAL_STATE_HANGUP }; struct serial_node { short state; unsigned long answered_at; unsigned long write_timeout_since; struct session *session; } the_serial_node; void serial_flush(void); void serial_printf(const char *format, ...); char * serial_get_line(bool wait); void serial_wait_for_connect(struct session *session); short serial_input(struct session *session); short serial_output(struct session *session); bool serial_write_with_timeout(long milliseconds); void serial_close(struct session *session); struct node_funcs serial_node_funcs = { serial_wait_for_connect, serial_input, serial_output, serial_close }; void serial_init(void) { Str32 in_port = "\p.AIn"; Str32 out_port = "\p.AOut"; SerShk flags = { 0 }; short baud, error, parity; char *resp; long m; if (serial_in_refnum) { SerSetBuf(serial_in_refnum, (Ptr)&serial_input_internal_buf, 0); CloseDriver(serial_in_refnum); serial_in_refnum = 0; } if (serial_out_refnum) { CloseDriver(serial_out_refnum); serial_out_refnum = 0; } if (db->config.modem_port == 0) return; if (db->config.modem_port == 2) { /* printer */ in_port[2] = 'B'; out_port[2] = 'B'; } if ((error = OpenDriver(in_port, &serial_in_refnum)) != 0) panic("OpenDriver(%s) failed: %d", PtoCstr(in_port), error); if ((error = OpenDriver(out_port, &serial_out_refnum)) != 0) panic("OpenDriver(%s) failed: %d", PtoCstr(out_port), error); switch (db->config.modem_speed) { case 300: baud = baud300; break; case 600: baud = baud600; break; case 1200: baud = baud1200; break; case 2400: baud = baud2400; break; case 4800: baud = baud4800; break; case 9600: baud = baud9600; break; case 14400: baud = baud14400; break; case 19200: baud = baud19200; break; case 28800: baud = baud28800; break; case 38400: baud = baud38400; break; case 57600: baud = baud57600; break; default: logger_printf("[modem] Unsupported port speed %ld, using 9600", db->config.modem_speed); db->config.modem_speed = 9600; baud = baud9600; } if (strcmp(db->config.modem_parity, "7E1") == 0) { parity = data7 + evenParity + stop10; } else { if (strcmp(db->config.modem_parity, "8N1") != 0) { logger_printf("[modem] Unknown parity \"%s\", using 8N1", db->config.modem_parity); snprintf(db->config.modem_parity, sizeof(db->config.modem_parity), "8N1"); } parity = data8 + noParity + stop10; } logger_printf("[modem] Initializing %s port at %ld (%s)", (db->config.modem_port == 2 ? "printer" : "modem"), db->config.modem_speed, db->config.modem_parity); if ((error = SerReset(serial_in_refnum, baud + parity)) != 0) panic("SerReset(in) failed: %d", error); if ((error = SerReset(serial_out_refnum, baud + parity)) != 0) panic("SerReset(out) failed: %d", error); SerSetBuf(serial_in_refnum, (Ptr)&serial_input_internal_buf, sizeof(serial_input_internal_buf)); flags.fXOn = true; flags.fInX = true; flags.xOn = 0x11; /* ^Q */ flags.xOff = 0x13; /* ^S */ flags.fDTR = 1; /* use Control instead of SerHShake to control fDTR */ if ((error = Control(serial_out_refnum, 14, &flags)) != 0) panic("Control failed: %d", error); /* reset */ serial_printf("ATZ\r"); Delay(TICKS_PER_SEC * 2, &m); serial_flush(); /* disable echo */ serial_printf("ATE0\r"); resp = serial_get_line(true); if (resp && resp[0] == 'A' && resp[1] == 'T' && resp[2] == 'E') /* eat echo */ resp = serial_get_line(true); if (!resp || (resp[0] != 'O' || resp[1] != 'K')) logger_printf("[modem] Bad response to ATE0: \"%s\"", resp == NULL ? "" : resp); /* initialize */ serial_printf("%s\r", db->config.modem_init); Delay(TICKS_PER_SEC, &m); resp = serial_get_line(false); if (!resp || (resp[0] != 'O' || resp[1] != 'K')) logger_printf("[modem] Bad response to init: \"%s\"", resp == NULL ? "" : resp); serial_flush(); the_serial_node.session = NULL; the_serial_node.state = SERIAL_STATE_IDLE; the_serial_node.answered_at = 0; serial_input_buf_len = 0; serial_rings = 0; serial_last_ring = 0; } bool serial_reinit(void) { if (the_serial_node.state != SERIAL_STATE_IDLE) { logger_printf("[modem] In use, not re-initializing"); return false; } logger_printf("[modem] Re-initializing"); serial_hangup(); serial_init(); return true; } void serial_hangup(void) { long m; if (serial_out_refnum == 0) return; logger_printf("[modem] Hanging up"); /* de-assert DTR */ Control(serial_out_refnum, 18, NULL); Delay(TICKS_PER_SEC * 1, &m); /* assert DTR */ Control(serial_out_refnum, 17, NULL); if (the_serial_node.session != NULL) the_serial_node.session->ending = true; } void serial_flush(void) { long len; short error; for (;;) { error = SerGetBuf(serial_in_refnum, &len); if (error != 0) { warn("SerGetBuf returned %d", error); return; } if (!len) break; if (len > sizeof(serial_input_buf)) len = sizeof(serial_input_buf); FSRead(serial_in_refnum, &len, &serial_input_buf); } memset(serial_input_buf, 0, sizeof(serial_input_buf)); serial_input_buf_len = 0; } void serial_printf(const char *format, ...) { static char serial_printf_tbuf[256]; va_list ap; size_t len; va_start(ap, format); len = vsnprintf(serial_printf_tbuf, sizeof(serial_printf_tbuf), format, ap); va_end(ap); logger_printf("[modem] Sending: %s", serial_printf_tbuf); memset(&serial_write_pbr, 0, sizeof(serial_write_pbr)); serial_write_pbr.ioParam.ioRefNum = serial_out_refnum; serial_write_pbr.ioParam.ioBuffer = (Ptr)&serial_printf_tbuf; serial_write_pbr.ioParam.ioReqCount = len; serial_write_with_timeout(1000); } bool serial_write_with_timeout(long milliseconds) { unsigned long ts = milliseconds / ((double)1000 / (double)60); unsigned long start = Ticks; unsigned long expire = start + ts; PBWrite(&serial_write_pbr, true); while (serial_write_pbr.ioParam.ioResult == 1) { if (Ticks >= expire) { if (the_serial_node.write_timeout_since == 0) logger_printf("[modem] Timed out waiting %ld milliseconds " "to write %d byte(s), aborting", milliseconds, serial_write_pbr.ioParam.ioReqCount); PBKillIO(&serial_write_pbr, false); return false; } if (Ticks - start > 30) uthread_yield(); } if (serial_write_pbr.ioParam.ioResult == 0) return true; logger_printf("[modem] Error writing to serial: %d", serial_write_pbr.ioParam.ioResult); return false; } char * serial_get_line(bool wait) { static char serial_cur_line[sizeof(serial_input_buf)]; size_t n; long len, rem; unsigned long started = Time; short error; maybe_read: /* append as much new data as we can fit */ if (serial_input_buf_len < sizeof(serial_input_buf)) { error = SerGetBuf(serial_in_refnum, &len); if (error != 0) { logger_printf("[modem] SerGetBuf returned %d", error); return NULL; } if (len) { len = MIN(len, sizeof(serial_input_buf) - serial_input_buf_len); FSRead(serial_in_refnum, &len, serial_input_buf + serial_input_buf_len); serial_input_buf[serial_input_buf_len + len] = '\0'; serial_input_buf_len += len; } } find_line: for (n = 0; n < serial_input_buf_len; n++) { if (serial_input_buf[n] == '\r' || serial_input_buf[n] == '\n') { if (n > 0) memcpy(serial_cur_line, serial_input_buf, n); serial_cur_line[n] = '\0'; /* eat any trailing newlines */ while (n + 1 < serial_input_buf_len && (serial_input_buf[n + 1] == '\r' || serial_input_buf[n + 1] == '\n')) n++; /* shift remaining data down */ rem = serial_input_buf_len - n - 1; if (rem > 0) memmove(serial_input_buf, serial_input_buf + n + 1, rem); else if (rem < 0) panic("bogus serial input remaining %ld", rem); serial_input_buf_len = rem; /* skip blank lines */ if (serial_cur_line[0] == '\0') goto find_line; logger_printf("[modem] Read: %s", serial_cur_line); return (char *)&serial_cur_line; } } if (wait) { if (serial_input_buf_len >= sizeof(serial_input_buf)) panic("serial_get_line with wait but input buffer full"); if (Time - started > 5) { logger_printf("[modem] Timed out waiting for modem response"); return NULL; } goto maybe_read; } return NULL; } void serial_atexit(void) { serial_init(); if (serial_in_refnum) { SerSetBuf(serial_in_refnum, (Ptr)&serial_input_internal_buf, 0); CloseDriver(serial_in_refnum); } if (serial_out_refnum) CloseDriver(serial_out_refnum); } void serial_wait_for_connect(struct session *session) { /* * We get here as session setup shortly after we answered the call. * Spin until serial_idle moves us to a connected state and we're * ready for the actual session to proceed, or just end the session * early if the negotiation failed. */ while (the_serial_node.state == SERIAL_STATE_ANSWERED) uthread_yield(); if (the_serial_node.state != SERIAL_STATE_CONNECTED && session != NULL) session->ending = true; } void serial_idle(void) { SerStaRec status; char *line; if (db->config.modem_port == 0) return; SerStatus(serial_in_refnum, &status); #if 0 if (status.ctsHold != 0) logger_printf("[modem] CTS set"); if (status.xOffHold != 0) logger_printf("[modem] XOFF set"); #endif if (status.cumErrs != 0) logger_printf("[modem] Reported errors: %d", status.cumErrs); switch (the_serial_node.state) { case SERIAL_STATE_IDLE: { char tty[7]; if ((line = serial_get_line(false)) == NULL) return; if (strstr(line, "RING") != NULL) { if (Time - serial_last_ring > 10) serial_rings = 0; serial_last_ring = Time; serial_rings++; if (serial_rings < db->config.modem_rings) break; sprintf(tty, "ttym%d", (db->config.modem_port == 2 ? 1 : 0)); the_serial_node.session = session_create(tty, "modem", &serial_node_funcs); if (the_serial_node.session == NULL) { logger_printf("[modem] No free nodes, can't answer call"); break; } the_serial_node.session->cookie = (void *)&the_serial_node; the_serial_node.session->vt100 = 1; the_serial_node.state = SERIAL_STATE_ANSWERED; the_serial_node.answered_at = Time; logger_printf("[modem] Answering call after %d ring%s", serial_rings, serial_rings == 1 ? "" : "s"); serial_printf("ATA\r"); } break; } case SERIAL_STATE_ANSWERED: { char *connect; long baud; /* optimistic :) */ short count; if (Time - the_serial_node.answered_at > SERIAL_CONNECT_TIMEOUT) { logger_printf("[modem] Timed out establishing a " "connection, aborting"); the_serial_node.session->ending = true; the_serial_node.state = SERIAL_STATE_HANGUP; break; } if ((line = serial_get_line(false)) == NULL) break; /* sometimes we don't get the beginning of CONNECT */ if ((connect = strstr(line, "NECT")) != NULL) { the_serial_node.state = SERIAL_STATE_CONNECTED; if (sscanf(connect, "NECT %ld/%n", &baud, &count) == 1 && count > 0) the_serial_node.session->tspeed = baud; } break; } } } short serial_input(struct session *session) { long len; short error, n; short lenwas = session->ibuflen; if (session->ending) return 0; if (serial_input_buf_len) { /* drain input buf first */ n = MIN(serial_input_buf_len, sizeof(session->ibuf) - session->ibuflen); memcpy(session->ibuf + session->ibuflen, serial_input_buf, n); session->ibuflen += n; if (serial_input_buf_len - n > 0) memmove(serial_input_buf, serial_input_buf + n, serial_input_buf_len - n); serial_input_buf_len -= n; if (session->ibuflen < lenwas) panic("serial ibuflen shrank"); if (session->ibuflen > sizeof(session->ibuf)) panic("serial ibuflen bogus"); session_check_buf_canaries(session); return n; } error = SerGetBuf(serial_in_refnum, &len); if (error != 0) { logger_printf("[modem] SerGetBuf returned %d", error); return 0; } if (!len) return 0; len = MIN(len, sizeof(session->ibuf) - session->ibuflen); error = FSRead(serial_in_refnum, &len, session->ibuf + session->ibuflen); session_check_buf_canaries(session); if (error == noErr) session->ibuflen += len; else logger_printf("[modem] Error from FSRead: %d", error); if (session->ibuflen < lenwas) panic("serial ibuflen shrank"); if (session->ibuflen > sizeof(session->ibuf)) panic("serial ibuflen bogus"); return len; } short serial_output(struct session *session) { if (session->obuflen == 0 || session->ending) return 0; memset(&serial_write_pbr, 0, sizeof(serial_write_pbr)); serial_write_pbr.ioParam.ioRefNum = serial_out_refnum; serial_write_pbr.ioParam.ioBuffer = (Ptr)&session->obuf; serial_write_pbr.ioParam.ioReqCount = session->obuflen; if (!serial_write_with_timeout(2000)) { if (the_serial_node.write_timeout_since == 0) the_serial_node.write_timeout_since = Time; if (Time - the_serial_node.write_timeout_since >= SERIAL_WRITE_GIVE_UP) { logger_printf("[modem] Reached %d seconds of write timeouts, " "hanging up", Time - the_serial_node.write_timeout_since); session->ending = true; } return 0; } the_serial_node.write_timeout_since = 0; if (serial_write_pbr.ioParam.ioReqCount > session->obuflen) warn("serial wrote more than obuflen?"); session->obuflen -= serial_write_pbr.ioParam.ioReqCount; session_check_buf_canaries(session); return serial_write_pbr.ioParam.ioReqCount; } void serial_close(struct session *session) { session_logf(session, "Closing serial session"); serial_hangup(); serial_init(); }