/************************************************************************/ /* modem.c */ /* */ /* modem code for Citadel bulletin board system */ /* NB: this code is rather machine-dependent: it will typically */ /* need some twiddling for each new installation. */ /* 82Nov05 CrT */ /************************************************************************/ /************************************************************************/ /* history */ /* */ /* 83Mar01 CrT FastIn() ignores LFs etc -- CRLF folks won't be trapped.*/ /* 83Feb25 CrT Possible fix for backspace-in-message-entry problem. */ /* 83Feb18 CrT fastIn() upload mode cutting in on people. Fixed. */ /* 82Dec16 dvm modemInit revised for FDC-1, with kludge for use with */ /* Big Board development system */ /* 82Dec06 CrT 2.00 release. */ /* 82Nov15 CrT readfile() & sendfile() borrowed from TelEdit.c */ /* 82Nov05 CrT Individual history file established */ /************************************************************************/ #include "b:210ctdl.h" /************************************************************************/ /* Contents */ /* */ /* BBSCharReady() returns true if user input is ready */ /* # fastIn() kludge code compiling other stuff inline*/ /* getCh() bottom-level console-input filter */ /* # getMod() bottom-level modem-input filter */ /* interpret() interprets a configuration routine */ /* KBReady() returns TRUE if a console char is ready */ /* getCh() returns a console char */ /* iChar() top-level user-input function */ /* interact() chat mode */ /* modIn() returns a user char */ /* modemInit() top-level initialize-all-modem-stuff */ /* mOReady() returns true if modem can accept a char */ /* oChar() top-level user-output function */ /* # outMod() bottom-level modem output */ /* pause() pauses for N/100 seconds */ /* putChar() */ /* recieve() read modem char or time out */ /* readFile() accept a file using WC protocol */ /* ringSysop() signal chat-mode request */ /* sendWCChar() send file with WC-protocol handshaking */ /* */ /* # == routines you should certainly check when porting system */ /************************************************************************/ /************************************************************************/ /* The principal dependencies: */ /* */ /* iChar modIn outMod */ /* modIn getMod getCh mIReady kBReady outMod carrDetect */ /* getMod */ /* getCh */ /* mIReady */ /* kBReady */ /* carrDetect */ /* */ /* oChar outMod */ /* outMod mOReady */ /************************************************************************/ /************************************************************************/ /* BBSCharReady() returns TRUE if char is available from user */ /* NB: user may be on modem, or may be sysop in CONSOLE mode */ /************************************************************************/ char BBSCharReady() { char KBReady(); return ((haveCarrier && interpret(pMIReady)) || (whichIO==CONSOLE && KBReady() ) ); } /************************************************************************/ /* fastIn() is a special kludge to read in text from the modem */ /* as quickly as possible, to allow message upload without */ /* handshaking. Hence we hand-compile some other routines inline */ /* Called only from getText(), when whichIO==MODEM. */ /* Externals: see below */ /* */ /* This code is probably overkill for 300 baud, but it may handle */ /* 1200 baud as well. */ /* */ /* code being speed-optimized would normally be: */ /* */ /* while ( */ /* !( (c=iChar()) == NEWLINE && buf[i-1] == NEWLINE ) */ /* && i < lim */ /* && (haveCarrier || whichIO == CONSOLE) */ /* ) { */ /* if (c != BACKSPACE) buf[i++] = c; */ /* else { */ /* / handle deletes: / */ /* oChar(' '); */ /* oChar(BACKSPACE); */ /* if (i>0 && buf[i-1] != NEWLINE) i--; */ /* else oChar(BELL); */ /* } */ /* } */ /************************************************************************/ #define cursor fpc1 /* external char * */ #define BUFEND fpc2 /* external char * */ #define tryCount fi1 /* external int */ #define isFast fi2 /* external int */ #define ch fc1 /* external char */ #define lastWasNL fc2 /* external char */ #define slow fc3 /* external char */ fastIn(continuing) char continuing; /* TRUE if we are continuing a message */ { char cache, notFinished; int tryStart, shortTime; isFast = 0; tryStart = 500*megaHz; shortTime = 480*megaHz; cursor = &msgBuf.mbtext[0 ]; BUFEND = &msgBuf.mbtext[MAXTEXT-1]; if (continuing) while (*cursor) ++cursor; /* find where we were */ notFinished = TRUE; lastWasNL = FALSE; /* put newline at start of buffer to simplify BACKSPACE check: */ cache = msgBuf.mbtext[-1]; msgBuf.mbtext[-1] = NEWLINE; while (notFinished && cursor < BUFEND) { tryCount = tryStart; /* try to be waiting for each char */ while (--tryCount && !interpret(pMIReady)); if (tryCount>shortTime) isFast++; else isFast--; if (!tryCount) { /* no modem char -- take break to check other stuff */ if (KBReady()) { if (getCh() == SPECIAL) { mprintf("\n system is now in CONSOLE mode.\n "); whichIO = CONSOLE; notFinished = FALSE; setUp(FALSE); } } if (!interpret(pCarrDetect)) { modIn(); /* let modIn() announce it etc */ notFinished = FALSE; } } else { /* time to read modem char: */ switch (ch = filter[ inp(mData) & 0x7F ]) { case NEWLINE: if (lastWasNL) { notFinished = FALSE; } else { lastWasNL = TRUE; *cursor++ = ch; if (isFast>0 || !termLF) ch = '\r'; else { oChar('\r'); while (!interpret(pMOReady)); /* for outp() */ } } isFast = 0; /* figure speed of each line independently */ break; case BACKSPACE: /* people don't upload backspaces -- one hopes! -- */ /* so we don't worry about speed so much here: */ if (*--cursor == NEWLINE) { /* trying to erase to previous line -- disallow */ /* because we have no upline: */ ch = BELL; cursor++; } else { oChar(BACKSPACE); oChar(' '); /* erase last char */ } while (!interpret(pMOReady)); /* for outp() */ break; case '\0': /* ignore unwanted chars completely: */ break; default: lastWasNL = FALSE; *cursor++ = ch; break; } /* echo to console--expendable but nice: */ if (thisRoom!=1 || whichIO==CONSOLE) putCh(ch); while (!interpret(pMOReady)); /* for outp() */ outp(mData, ch); /* (was:)assume port is ready by now */ } } *cursor = '\0'; /* tie off message */ if (cursor == BUFEND) { mprintf("\n \7BUFFER OVERFLOW\n "); return; } msgBuf.mbtext[-1] = cache; /* return borrowed space */ } /************************************************************************/ /* getCh() reads a console char */ /* In CONSOLE mode, CRs are changed to newlines */ /* Rubouts are changed to backspaces */ /* Returns: resulting char */ /************************************************************************/ char getCh() { char c; char bios(); return bios(3); } /************************************************************************/ /* getMod() is bottom-level modem-input routine */ /* kills any parity bit */ /* rubout -> backspace */ /* CR -> newline */ /* other nonprinting chars -> blank */ /* Returns: result */ /************************************************************************/ char getMod() { char inp(); return inp(mData) & 0x7F; } /************************************************************************/ /* iChar() is the top-level user-input function -- this is the */ /* function the rest of Citadel uses to obtain user input */ /************************************************************************/ char iChar() { char modIn(); char c; if (justLostCarrier) return 0; /* ugly patch */ c = filter[modIn()]; switch (echo) { case BOTH: if (haveCarrier) { if (c == '\n') doCR(); else outMod(c); } if (thisRoom!=1 || whichIO==CONSOLE) putCh(c); break; case CALLER: if (whichIO == MODEM) { if (c == '\n') doCR(); else outMod(c); } else { putCh(c); } break; } return(c); } /************************************************************************/ /* interact() allows the sysop to interact directly with */ /* whatever is on the modem. dvm 9-82 */ /************************************************************************/ interact() { char c, lineEcho, lineFeeds, localEcho; char getMod(), getCh(), interpret(); printf(" Direct modem-interaction mode\n"); lineEcho = getYesNo("Echo to modem" ); localEcho = getYesNo("Echo keyboard" ); lineFeeds = getYesNo("Echo CR as CRLF" ); printf(" to exit\n"); /* incredibly ugly code. Rethink sometime: */ while (c!=SPECIAL) { c = 0; if (c=interpret(pMIReady) ) { c = inp(mData) & 0x7F; if (c != '\r') c = filter[c]; if (c != '\r') { if (lineEcho) outMod(c); putCh(c); } else { if (!lineFeeds) { if (lineEcho) outMod('\r'); putCh('\r'); } else { if (lineEcho) { outMod('\r'); outMod('\n'); } putCh('\r'); putCh('\n'); } } } if (KBReady()) { c = filter[getCh()]; if (c != NEWLINE) { if (localEcho) putCh(c); outMod(c); } else { if (!lineFeeds) { if (localEcho) putCh('\r'); outMod('\r'); } else { if (lineEcho) { putCh('\r'); putCh('\n'); } outMod('\r'); outMod('\n'); } } } } } /************************************************************************/ /* interpret() interprets a configuration routine */ /* Returns byte value computed */ /************************************************************************/ char interpret(instr) union { char **pp; int *pi; char *pc; } instr; { char inp(); char accum; /* our sole accumulator */ char *prompt; int lowLim, topLim; while (TRUE) { switch (*instr.pc++) { case RET: return accum; break; case ANDI: accum &= *instr.pc++; break; case INP: accum = inp(*instr.pc++); break; case XORI: accum ^= *instr.pc++; break; case LOAD: accum = *(*instr.pp++); break; case LOADI: accum = *instr.pc++; break; case LOADX: accum = scratch[*instr.pc++]; break; case ORI: accum |= *instr.pc++; break; case OUTP: outp(*instr.pc++, accum); break; case PAUSEI: pause(*instr.pc++); break; case STORE: *(*instr.pp++) = accum; break; case STOREX: scratch[*instr.pc++] = accum; break; case OPRNUMBER: prompt = instr.pc; while(*instr.pc++); /* step over prompt */ lowLim = *instr.pc++; topLim = *instr.pc++; accum = getNumber(prompt, lowLim, topLim); break; case OUTSTRING: while(*instr.pc) { pause(5); /* SmartModem can't handle 300 baud */ outMod(*instr.pc++); /* output string */ } instr.pc++; /* skip null */ break; default: printf("intrp-no opcod%d", *(instr.pc-1)); break; } } } /************************************************************************/ /* KBReady() returns TRUE if a console char is ready */ /************************************************************************/ char KBReady() { char bios(); return bios(2); } /************************************************************************/ /* modemInit() is responsible for all modem-related initialization */ /* at system startup */ /* Globals modified: haveCarrier visibleMode */ /* whichIO modStat */ /* exitToCpm justLostCarrier */ /* modified 82Dec10 to set FDC-1 SIO-B clock speed at */ /* 300 baud -dvm */ /************************************************************************/ modemInit() { char c; newCarrier = FALSE; visibleMode = FALSE; exitToCpm = FALSE; justLostCarrier = FALSE; whichIO = CONSOLE; #ifdef FDC-1 #define MONBASE 0xF800 #define STSSP MONBASE+0x045 call(STSSP, 0, 0, 0x0B, 0x05); /* 300 baud on SIO-B */ #endif #ifdef VFC-2 /* dummy call (to CONSTAT) to make code size equal for both systems */ call(0xF006, 0, 0, 0x0B, 0x05); #endif if (!rcpm) { interpret(pInitPort); interpret(pHangUp); } haveCarrier = modStat = interpret(pCarrDetect); } /************************************************************************/ /* modIn() toplevel modem-input function */ /* If DCD status has changed since the last access, reports */ /* carrier present or absent and sets flags as appropriate. */ /* In case of a carrier loss, waits 20 ticks and rechecks */ /* carrier to make sure it was not a temporary glitch. */ /* If carrier is newly received, returns newCarrier = TRUE; if */ /* carrier lost returns 0. If carrier is present and state */ /* has not changed, gets a character if present and */ /* returns it. If a character is typed at the console, */ /* checks to see if it is keyboard interrupt character. If */ /* so, prints short-form console menu and awaits next */ /* keyboard character. */ /* Globals modified: carrierDetect modStat haveCarrier */ /* justLostCarrier whichIO exitToCpm */ /* visibleMode */ /* Returns: modem or console input character, */ /* or above special values */ /************************************************************************/ char modIn() { char getMod(), getCh(), interpret(), KBReady(); char c; unsigned hi, lo; hi = (HITIMEOUT * megaHz); lo = 0xFF; while (TRUE) { if ((whichIO==MODEM) && (c=interpret(pCarrDetect)) != modStat) { /* carrier changed */ if (c) { /* carrier present */ printf("Carr-detect\n"); haveCarrier = TRUE; pause(200); modStat = c; newCarrier = TRUE; return(0); } else { pause(200); /* confirm it's not a glitch */ if (!interpret(pCarrDetect)) { /* check again */ printf("Carr-loss\n"); haveCarrier = FALSE; modStat = FALSE; justLostCarrier = TRUE; /* while(interpret(pMIReady)) getMod(); eat garbage */ return(0); } } } if (interpret(pMIReady)) { if (haveCarrier) { c = getMod(); if (whichIO == MODEM) return c; } } if (KBReady()) { c = getCh(); if (whichIO == CONSOLE) return(c); else { if (c == SPECIAL) { printf("CONSOLE mode\n "); whichIO = CONSOLE; setUp(FALSE); return 0; } } } /* check for no input. (Short-circuit evaluation, remember!) */ if (whichIO==MODEM && haveCarrier && !--lo && !--hi) { mprintf("Sleeping? Call again :-)"); interpret(pHangUp); } } } /************************************************************************/ /* oChar() is the top-level user-output function */ /* sends to modem port and console both */ /* does conversion to upper-case etc as necessary */ /* in "debug" mode, converts control chars to uppercase letters */ /* Globals modified: prevChar */ /************************************************************************/ oChar(c) char c; { prevChar = c; /* for end-of-paragraph code */ if (outFlag) return; /* s(kip) mode */ if (termUpper) c = toupper(c); if (debug) c = visible(c); if (c == NEWLINE) c = ' '; /* doCR() handles real newlines */ /* show on console */ if (whichIO==CONSOLE || (thisRoom!=1 && echo!=CALLER)) putCh(c); if (haveCarrier) { if (!usingWCprotocol) { outMod(c); /* show on modem */ } else { sendWCChar(c); } } else { if (usingWCprotocol) { sendWCChar(c); } } } /************************************************************************/ /* outMod stuffs a char out the modem port */ /************************************************************************/ outMod(c) char c; { while(!interpret(pMOReady)); outp(mData, c); } /************************************************************************/ /* pause() busy-waits N/100 seconds */ /************************************************************************/ pause(i) int i; { int j; #define SECONDSFACTOR 55 for (; i; i--) { for (j=(SECONDSFACTOR*megaHz); j; j--); } } /************************************************************************/ /* putChar() */ /************************************************************************/ putChar(c) char c; { if (thisRoom!=1 || whichIO==CONSOLE) putCh(c); } /************************************************************************/ /* receive() gets a modem character, or times out ... */ /* Returns: char on success else ERROR */ /************************************************************************/ int receive(seconds) int seconds; { unsigned count; count = seconds * 100; while (!interpret(pMIReady) && --count) pause(1); if (count) return inp(mData); return(ERROR); } /************************************************************************/ /* readFile() accepts a file from modem using Ward Christensen's */ /* protocol. (ie, compatable with xModem, modem7, yam, modem2...) */ /* Returns: TRUE on successful transfer, else FALSE */ /************************************************************************/ char readFile(pc) int (*pc)(); /* pc will accept the file one character at a time. */ /* returns ERROR on any problem, and closes the file */ /* when handed ERROR as an argument. */ { int i, firstchar, lastSector, thisSector, thisComplement, tries; int toterr, checksum; char badSector, writeError; char sectBuf[SECTSIZE]; char *nextChar; if (!getYesNo("Ready for WC transfer")) return FALSE; lastSector = 0; tries = 0; toterr = 0; writeError = FALSE; while (interpret(pMIReady)) inp(mData); /* clear garbage */ printf("Awaiting #0 (Try=0, Errs=0) \r"); do { badSector = FALSE; /* get synchronized: */ do { firstchar = receive(10); } while ( firstchar != SOH && firstchar != EOT && firstchar != ERROR ); if (firstchar == ERROR) badSector = TRUE; if (firstchar == SOH) { /* found StartOfHeader -- read sector# in: */ thisSector = receive (1); thisComplement = receive (1); /* 1's comp of thisSector */ if ((thisSector + thisComplement) != 0xFF) badSector = TRUE; else { if (thisSect == lastSector +1) { /* right sector... let's read it in */ checksum = 0; nextChar = sectBuf; for (i=SECTSIZE; i; i--) { *nextChar = receive (1); checksum = (checksum + *nextChar++) & 0xFF; } if (checksum != receive (1)) badSector = TRUE; else { tries = 0; lastSector = thisSector; printf("Awaiting #%d (Try=0, Errs=%d) \r", thisSector, toterr ); if (tries && toterr) putchar('\n'); /* write sector to where-ever: */ nextChar = sectBuf; for (i=SECTSIZE; i; i--) { writeError &= (*pc)(*nextChar++) == ERROR; } if (!writeError) outMod(ACK); } } else { /* not expected sector... */ if (thisSector != lastSector) badSector = TRUE; else { /* aha -- sender missed an ACK and resent last: */ do; while (receive(1) != ERROR); /* eat it */ outMod(ACK); /* back in synch! */ } } } /* end of "if (thisSector + thisComplement == 255" */ } /* end of "if (firstChar == SOH)" */ if (badSector) { tries++; if (lastSector != 0) toterr++; while (receive (1) != ERROR); printf("Awaiting #%d (Try=%d, Errs=%d) \r", lastSector, tries, toterr ); if (tries && toterr) putchar('\n'); outMod(NAK); } } while ( firstchar != EOT && tries < ERRORMAX && !writeError ); if (firstchar != EOT || tries >= ERRORMAX) { printf("\naborting\n"); return FALSE; } else { outMod(ACK); printf("\nfile reception complete.\n"); return TRUE; } } /************************************************************************/ /* ringSysop() signals a chat mode request. Exits on input from */ /* modem or keyboard. */ /************************************************************************/ ringSysop() { char BBSCharReady(); int i; mprintf("\n Ringing sysop.\n "); for (i=0; !BBSCharReady() && !KBReady(); i = ++i % 7) { /* play shave-and-a-haircut/two bits... as best we can: */ oChar(BELL); pause(shave[i]); } if (KBReady()) { getCh(); whichIO = CONSOLE; interact(); whichIO = MODEM; } } /************************************************************************/ /* sendWCChar() sends a file using Ward Christensen's protocol. */ /* (i.e., compatable with xModem, modem7, modem2, YAM, ... ) */ /* */ /* C is an old, tired language which does not support coroutines */ /* adequately. SendWCChar() has suffered as a result. SendWCChar() */ /* and the disk-read routines pass control back and forth during */ /* transmission. Being smaller, sendWCChar() is the one which gets to */ /* stash all its variables as globals and play at subroutine. The */ /* ugliest part is finding our place again each time we are called. */ /* SFRunning is TRUE normally and FALSE while we're finding our place. */ /* The usual indentation rules are deliberately suppressed in the case */ /* of SFRunning, to leave the "real" program structure as untouched as */ /* possible. Ignore the righthand SFRunning stuff and it will look ok. */ /* Returns: FALSE for another char, TRUE on success, ERROR on abort */ /************************************************************************/ int sendWCChar(c) int c; /* character to output to MODEM, or ERROR on EOF */ { /* Declared globally: */ /* char SFcheckSum, SFeofSeen, SFRunning; */ /* int SFi, SFthisChar, SFthisSector, SFerrorCount, SFtries; */ if (outFlag) return ERROR; if (SFRunning) { while (interpret(pMIReady)) inp(mData); /* clear garbage off line */ SFtries = 0; SFerrorCount= 0; printf("\n0Sending #0 (Try=0 Errs=0) \n"); while ((receive (10) != NAK) && (SFtries < RETRYMAX)) { SFtries++; printf("\n1Sending #0 (Try=%d Errs=0) \r", SFtries); } if (SFtries >= RETRYMAX) { printf("\nTimed out awaiting initial NAK\n"); outFlag = OUTSKIP; return ERROR; } SFtries = 0; SFthisSector= 1; SFthisChar = c; SFeofSeen = (c == ERROR); /* send the file: */ } /* SFRunning block */ while (!SFeofSeen) { if (SFRunning) { SFtries = 0; } /* SFRunning block */ /* send sector and look for ACK: */ do { if (SFRunning) { printf("\n2Sending #%d (Try=%d Errs=%d) \n", SFthisSector, SFtries, SFerrorCount ); if (SFtries && SFerrorCount) putchar('\n'); /* send one sector: */ /* maybe read next sector into SFBuf: */ SFi =SECTSIZE; } /* SFRunning block */ if (!SFtries) { for (; SFRunning ? SFi-- : TRUE; ) { if (SFRunning) { SFRunning = FALSE; /* SFthisChar = nextIn(); */ return FALSE; } SFthisChar = c; SFRunning = TRUE; if (SFthisChar == ERROR) { SFeofSeen = TRUE; SFthisChar = CPMEOF; /* if EOF is first char in block, don't send it: */ if (SFi == (SECTSIZE-1)) break; } SFBuf[SFi] = SFthisChar; } } if (SFi == (SECTSIZE-1)) { /* don't send empty sector just for EOF: */ break; } else { /* mail sector out: */ outMod(SOH ); outMod( SFthisSector); outMod(~SFthisSector); for (SFi=SECTSIZE, SFcheckSum=0; SFi--; ) { SFcheckSum = (SFcheckSum + SFBuf[SFi]) & 0xFF; outMod( SFBuf[SFi] ); } outMod(SFcheckSum); while (interpret(pMIReady)) inp(mData); /* clear off line */ SFtries++; SFerrorCount++; } } while ((receive(10) != ACK) && (SFtries < RETRYMAX)); SFthisSector++; SFerrorCount--; if (SFtries >= RETRYMAX) break; } /* file sent, or not, as the case may be: */ if (SFtries >= RETRYMAX) { printf("\nNo ACK on sector, aborting\n"); outFlag = OUTSKIP; return ERROR; } else { /* tell recipient we're all finished: */ SFtries = 0; do { outMod(EOT); while (interpret(pMIReady)) inp(mData); /* clear off line */ SFtries++; } while ((receive(10) != ACK) && (SFtries < RETRYMAX)); if (SFtries < RETRYMAX) { return TRUE; } else { printf("\nNo ACK on EOT, aborting\n"); outFlag = OUTSKIP; return ERROR; } } } .