shithub: purgatorio

ref: 3efb5bbb4061056e523858b134c555949591efe2
dir: /appl/examples/minitel/modem.b/

View raw version
#
# Copyright © 1998 Vita Nuova Limited.  All rights reserved.
#

#modem states for direct connection
MSstart, MSdialing, MSconnected, MSdisconnecting,

# special features
Ecp								# error correction
	: con (1 << iota);

Ecplen: con 17;	# error correction block length: data[15], crc, validation (=0)

Modem: adt {
	m:		ref Module;			# common attributes
	in:		chan of ref Event;

	connect:	int;					# None, Direct, Network
	state:	int;					# modem dialing state
	saved:	string;				# response, so far (direct dial)
	initstr:	string;				# softmodem init string (direct dial)
	dialstr:	string;				# softmodem dial string (direct dial)
	lastdialstr:	string;

	spec:	int;					# special features
	fd:		ref Sys->FD;			# modem data file, if != nil
	cfd:		ref Sys->FD;			# modem ctl file, if != nil (direct dial only)
	devpath:	string;				# path to the modem;
	avail:	array of byte;			# already read
	rd:		chan of array of byte;	# reader -> rd
	pid:		int;					# reader pid if != 0

	seq:		int;					# ECP block sequence number
	waitsyn:	int;					# awaiting restart SYN SYN ... sequence
	errforce:	int;
	addparity:	int;					# must add parity to outgoing data

	init:		fn(m: self ref Modem, connect: int, initstr, dialstr: string);
	reset:	fn(m: self ref Modem);
	run:		fn(m: self ref Modem);
	quit:		fn(m: self ref Modem);
	runstate:	fn(m: self ref Modem, data: array of byte);
	write:	fn(m: self ref Modem, data: array of byte):int;	# to network
	reader:	fn(m: self ref Modem, pidc: chan of int);
};

partab: array of byte;

dump(a: array of byte, n: int): string
{
	s := sys->sprint("[%d]", n);
	for(i := 0; i < n; i++)
		s += sys->sprint(" %.2x", int a[i]);
	return s;
}

Modem.init(m: self ref Modem, connect: int, initstr, dialstr: string)
{
	partab = array[128] of byte;
	for(c := 0; c < 128; c++)
		if(parity(c))
			partab[c] = byte (c | 16r80);
		else
			partab[c] = byte c;
	m.in = chan of ref Event;
	m.connect = connect;
	m.state = MSstart;
	m.initstr = initstr;
	m.dialstr = dialstr;
	m.pid = 0;
	m.spec = 0;
	m.seq = 0;
	m.waitsyn = 0;
	m.errforce = 0;
	m.addparity = 0;
	m.avail = array[0] of byte;
	m.rd = chan of array of byte;
	m.reset();
}

Modem.reset(m: self ref Modem)
{
	m.m = ref Module(Pscreen, 0);
}

Modem.run(m: self ref Modem)
{
	if(m.dialstr != nil)
		send(ref Event.Eproto(Pmodem, Mmodem, Cconnect, "", 0,0,0));
Runloop:
	for(;;){
		alt {
		ev := <- m.in =>
			pick e := ev {
			Equit =>
				break Runloop;
			Edata =>
				if(debug['m'] > 0)
					fprint(stderr, "Modem <- %s\n", e.str());
				m.write(e.data);
				if(T.state == Local || T.spec & Echo) {	# loopback
					if(e.from == Mkeyb) {
						send(ref Event.Eproto(Pscreen, Mkeyb, Ccursor, "", 0,0,0));
						send(ref Event.Edata(Pscreen, Mkeyb, e.data));
					}
				}
			Eproto =>
				case e.cmd {
				Creset =>
					m.reset();
				Cconnect =>
					if(m.pid != 0)
						break;
					m.addparity = 1;
					T.state = Connecting;
					send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));

					case m.connect {
					Direct =>
						S.msg("Appel "+m.dialstr+" ...");
						dev := "/dev/modem";
						if(openmodem(m, dev) < 0) {
							S.msg("Modem non prêt");
							T.state = Local;
							send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
							break;
						}
						m.state = MSdialing;
						m.saved = "";
						dialout(m);
						T.terminalid = TERMINALID2;
					Network =>	
						S.msg("Connexion au serveur ...");
						if(debug['m'] > 0 || debug['M'] > 0)
							sys->print("dial(%s)\n", m.dialstr);
						cx := dial->dial(m.dialstr, "");
						if (cx == nil){
							S.msg("Echec de la connexion");
							T.state = Local;
							send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
							if(debug['m'] > 0)
								sys->print("can't dial %s: %r\n", m.dialstr);
							break;
						}
						m.fd = cx.dfd;
						m.cfd = cx.cfd;
						if(len m.dialstr >= 3 && m.dialstr[0:3] == "tcp")
							m.addparity = 0;	# Internet gateway apparently doesn't require parity
						if(m.fd != nil) {
							S.msg(nil);
							m.state = MSconnected;
							T.state = Online;
							send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
						}
						T.terminalid = TERMINALID1;
					}
					if(m.fd != nil) {
						pidc := chan of int;
						spawn m.reader(pidc);
						m.pid = <-pidc;
					}
				Cdisconnect =>
					if(m.pid != 0) {
						S.msg("Déconnexion ...");
						m.state = MSdisconnecting;
					}
					if(m.connect == Direct)
						hangup(m);
					else
						nethangup(m);
				Cplay =>			# for testing
					case e.s {
					"play" =>
						replay(m);
					}
				Crequestecp =>
					if(m.spec & Ecp){	# for testing: if already active, force an error
						m.errforce = 1;
						break;
					}
					m.write(array[] of {byte SEP, byte 16r4A});
sys->print("sending request for ecp\n");
				Cstartecp =>
					m.spec |= Ecp;
					m.seq = 0;	# not in spec
					m.waitsyn = 0;	# not in spec
				Cstopecp =>
					m.spec &= ~Ecp;
				* => break;
				}
			}
		b := <- m.rd =>
			if(debug['m'] > 0){
				fprint(stderr, "Modem -> %s\n", dump(b,len b));
			}
			if(b == nil) {
				m.pid = 0;
				case m.state {
				MSdialing =>
					S.msg("Echec appel");
				MSdisconnecting =>
					S.msg(nil);
				}
				m.state = MSstart;
				T.state = Local;
				send(ref Event.Eproto(Pscreen, Mmodem, Cscreenon, "",0,0,0));
				send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
				break;
			}
			m.runstate(b);
		}
	}
	if(m.pid != 0)
		kill(m.pid);
	send(nil);	
}

Modem.quit(nil: self ref Modem)
{
}

Modem.runstate(m: self ref Modem, data: array of byte)
{
	if(debug['m']>0)
		sys->print("runstate %d %s\n", m.state, dump(data, len data));
	case m.state {
	MSstart =>	;
	MSdialing =>
		for(i:=0; i<len data; i++) {
			ch := int data[i];
			if(ch != '\n' && ch != '\r') {
				m.saved[len m.saved] = ch;
				continue;
			}
			(code, str) := seenreply(m.saved);
			case code {
			Noise or Ok =>	;
			Success =>
				S.msg(nil);
				m.state = MSconnected;
				T.state = Online;
				send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
			Failure =>
				hangup(m);
				S.msg(str);
				m.state = MSstart;
				T.state = Local;
				send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
			}
			m.saved = "";
		}
	MSconnected =>
		send(ref Event.Edata(m.m.path, Mmodem, data));
	MSdisconnecting =>	;
	}
}

Modem.write(m: self ref Modem, data: array of byte): int
{
	if(m.fd == nil)
		return -1;
	if(len data == 0)
		return 0;
	if(m.addparity){
		# unfortunately must copy data to add parity for direct modem connection
		pa := array[len data] of byte;
		for(i := 0; i<len data; i++)
			pa[i] = partab[int data[i] & 16r7F];
		data = pa;
	}
	if(debug['m']>0)
		sys->print("WRITE %s\n", dump(data, len data));
	return sys->write(m.fd, data, len data);
}

#
# minitel error correction protocol
#
# SYN, SYN, block number	start of retransmission
# NUL ignored
# DLE escapes {DLE, SYN, NACK, NUL}
# NACK, block	restart request
#

crctab: array of int;
Crcpoly: con 16r9;	# crc7 = x^7+x^3+1

# precalculate the CRC7 remainder for all bytes

mktabs()
{
	crctab = array[256] of int;
	for(c := 0; c < 256; c++){
		v := c;
		crc := 0;
		for(i := 0; i < 8; i++){
			crc <<= 1;		# align remainder's MSB with value's
			if((v^crc) & 16r80)
				crc ^= Crcpoly;
			v <<= 1;
		}
		crctab[c] = (crc<<1) & 16rFE;	# pre-align the result to save <<1 later
	}
}

# return the index of the first non-NUL character (the start of a block)

nextblock(a: array of byte, i: int, n: int): int
{
	for(; i < n; i++)
		if(a[i] != byte NUL)
			break;
	return i;
}

# return the data in the ecp block in a[0:Ecplen] (return nil for bad format)

decode(a: array of byte): array of byte
{
	if(debug['M']>0)
		sys->print("DECODE: %s\n", dump(a, Ecplen));
	badpar := 0;
	oldcrc := int a[Ecplen-2];
	crc := 0;
	op := 0;
	dle := 0;
	for(i:=0; i<Ecplen-2; i++){	# first byte is high-order byte of polynomial (MSB first)
		c := int a[i];
		nc := c & 16r7F;	# strip parity
		if((c^int partab[nc]) & 16r80)
			badpar++;
		crc = crctab[crc ^ c];
		# collapse DLE sequences
		if(!dle){
			if(nc == DLE && i+1 < Ecplen-2){
				dle = 1;
				continue;
			}
			if(nc == NUL)
				continue;	# strip non-escaped NULs
		}
		dle = 0;
		a[op++] = byte nc;
	}
	if(badpar){
		if(debug['E'] > 0)
			sys->print("bad parity\n");
		return nil;	
	}
	crc = (crc>>1)&16r7F;
	if(int partab[crc] != oldcrc){
		if(debug['E'] > 0)
			sys->print("bad crc: in %ux got %ux\n", oldcrc, int partab[crc]);
		return nil;
	}
	b := array[op] of byte;
	b[0:] = a[0:op];
	if(debug['M'] > 0)
		sys->print("OUT: %s [%x :: %x]\n", dump(b,op), crc, oldcrc);
	return b;
}

Modem.reader(m: self ref Modem, pidc: chan of int)
{
	pidc <-= sys->pctl(0, nil);
	if(crctab == nil)
		mktabs();
	a := array[Sys->ATOMICIO] of byte;
	inbuf := 0;
	while(m.fd != nil) {
		while((n := read(m.fd, a[inbuf:], len a-inbuf)) > 0){
			n += inbuf;
			inbuf = 0;
			if((m.spec & Ecp) == 0){
				b := array[n] of byte;
				for(i := 0; i<n; i++)
					b[i] = byte (int a[i] & 16r7F);	# strip parity
				m.rd <-= b;
			}else{
				#sys->print("IN: %s\n", dump(a,n));
				i := 0;
				if(m.waitsyn){
					sys->print("seeking SYN #%x\n", m.seq);
					syn := byte (SYN | 16r80);
					lim := n-3;
					for(; i <= lim; i++)
						if(a[i] == syn && a[i+1] == syn && (int a[i+2]&16r0F) == m.seq){
							i += 3;
							m.waitsyn = 0;
							sys->print("found SYN #%x@%d\n", m.seq, i-3);
							break;
						}
				}
				lim := n-Ecplen;
				for(; (i = nextblock(a, i, n)) <= lim; i += Ecplen){
					b := decode(a[i:]);
					if(m.errforce || b == nil){
						m.errforce = 0;
						b = array[2] of byte;
						b[0] = byte NACK;
						b[1] = byte (m.seq | 16r40);
						sys->print("NACK #%x\n", m.seq);
						m.write(b);
						m.waitsyn = 1;
						i = n;		# discard rest of block
						break;
					}
					m.seq = (m.seq+1) & 16rF;	# mod 16 counter
					m.rd <-= b;
				}
				if(i < n){
					a[0:] = a[i:n];
					inbuf = n-i;
				}
			}
		}
		if(n <= 0)
			break;
	}
#	m.fd = nil;
	m.rd <-= nil;
}

playfd: ref Sys->FD;
in_code, in_char: con iota;

replay(m: ref Modem)
{
	buf := array[8192] of byte;
	DMAX:	con 10;
	d := 0;
	da := array[DMAX] of byte;
	playfd = nil;
	if(playfd == nil)
		playfd = sys->open("minitel.txt", Sys->OREAD);
	if(playfd == nil)
		return;
	nl := 1;
	discard := 1;
	state := in_code;
	hs := "";
	start := 0;
mainloop:
	for(;;) {
		n := sys->read(playfd, buf, len buf);
		if(n <= 0)
			break;
		for(i:=0; i<n; i++) {
			ch := int buf[i];
			if(nl)
				case ch {
				'>' =>	discard = 0;
				'<' =>	discard = 1;
						if(start)
							sys->sleep(1000);
				'{' =>		start = 1;
				'}' =>		break mainloop;
				}
			if(ch == '\n')
				nl = 1;
			else
				nl = 0;
			if(discard)
				continue;
			if(!start)
				continue;
			if(state == in_code && ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z')))
				hs[len hs] = ch;
			else if(ch == '(') {
				state = in_char;
				(v, nil) := toint(hs, 16);
				da[d++] = byte v;
				if(d == DMAX) {
					send(ref Event.Edata(m.m.path, Mmodem, da));
					d = 0;
					da = array[DMAX] of byte;
					sys->sleep(50);
				}
				hs = "";
			}else if(ch == ')')
				state = in_code;
		}
	}
	playfd = nil;

}

kill(pid : int)
{
	prog := "#p/" + string pid + "/ctl";
	fd := sys->open(prog, Sys->OWRITE);
	if (fd != nil) {
		cmd := array of byte "kill";
		sys->write(fd, cmd, len cmd);
	}
}


# Modem stuff


# modem return codes
Ok, Success, Failure, Noise, Found: con iota;

#
#  modem return messages
#
Msg: adt {
	text: string;
	trans: string;
	code: int;
};

msgs: array of Msg = array [] of {
	("OK",			"Ok", Ok),
	("NO CARRIER",		"No carrier", Failure),
	("ERROR",			"Bad modem command", Failure),
	("NO DIALTONE",	"No dial tone", Failure),
	("BUSY",			"Busy tone", Failure),
	("NO ANSWER",		"No answer", Failure),
	("CONNECT",		"", Success),
};

msend(m: ref Modem, x: string): int
{
	a := array of byte x;
	return sys->write(m.fd, a, len a);
}

#
#  apply a string of commands to modem
#
apply(m: ref Modem, s: string): int
{
	buf := "";
	for(i := 0; i < len s; i++){
		c := s[i];
		buf[len buf] = c;	# assume no Unicode
		if(c == '\r' || i == (len s -1)){
			if(c != '\r')
				buf[len buf] = '\r';
			if(msend(m, buf) < 0)
				return Failure;
			buf = "";
		}
	}
	return Ok;
}

openmodem(m: ref Modem, dev: string): int
{
	m.fd = sys->open(dev, Sys->ORDWR);
	m.cfd = sys->open(dev+"ctl", Sys->ORDWR);
	if(m.fd == nil || m.cfd == nil)
		return -1;
#	hangup(m);
#	m.fd = sys->open(dev, Sys->ORDWR);
#	m.cfd = sys->open(dev+"ctl", Sys->ORDWR);
#	if(m.fd == nil || m.cfd == nil)
#		return -1;
	return 0;
}

hangup(m: ref Modem)
{
	sys->sleep(1020);
	msend(m, "+++");
	sys->sleep(1020);
	apply(m, "ATH0");
	m.fd = nil;
#	sys->write(m.cfd, array of byte "f", 1);
	sys->write(m.cfd, array of byte "h", 1);
	m.cfd = nil;
	# HACK: shannon softmodem "off-hook" bug fix
	sys->open("/dev/modem", Sys->OWRITE);
}

nethangup(m: ref Modem)
{
	m.fd = nil;
	sys->write(m.cfd, array of byte "hangup", 6);
	m.cfd = nil;
}


#
#  check `s' for a known reply or `substr'
#
seenreply(s: string): (int, string)
{
	for(k := 0; k < len msgs; k++)
		if(len s >= len msgs[k].text && s[0:len msgs[k].text] == msgs[k].text) {
			return (msgs[k].code, msgs[k].trans);
		}
	return (Noise, s);
}

contains(s, t: string): int
{
	if(t == nil)
		return 1;
	if(s == nil)
		return 0;
	n := len t;
	for(i := 0; i+n <= len s; i++)
		if(s[i:i+n] == t)
			return 1;
	return 0;
}

dialout(m: ref Modem)
{
	if(m.initstr != nil)
		apply(m, "AT"+m.initstr);
	if(m.dialstr != nil) {
		apply(m, "ATD"+m.dialstr);
		m.lastdialstr = m.dialstr;
		m.dialstr = nil;
	}
}