changeset 0:bef2801ac83e

Initial checkin with reference implementation of core protocol Initial checkin. Has the initial version of the protocol documentation along with a reference implementation of the core protocol.
author William Astle <lost@l-w.ca>
date Sun, 08 May 2016 12:56:39 -0600
parents
children cfc9524cca2c
files .hgignore README docs/extension-virtualports.txt docs/protocol.txt src/Makefile src/lwwire-serial.c src/lwwire.c
diffstat 7 files changed, 1413 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Sun May 08 12:56:39 2016 -0600
@@ -0,0 +1,6 @@
+syntax: glob
+*~
+*.o
+syntax: regexp
+^src/lwwire
+^src/lwwire-serial
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Sun May 08 12:56:39 2016 -0600
@@ -0,0 +1,10 @@
+The docs/ folder contains protocol documentation for lwwire.
+
+The src/ folder contains a reference protocol implementation. This is NOT
+intended for casual users. It is meant to serve as an illustration of how
+the protocol can be implemented and serves as a means to test features. The
+reference implementation does not contain a fancy GUI, configuration file
+handler, or even serial or TCP port handling. It simply operates the
+protocol on stdin and stdout and prints diagnostics to stderr. A tool like
+socat can be used to link it to a TCP port. Similar tools might be used to
+link it to a serial port.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/extension-virtualports.txt	Sun May 08 12:56:39 2016 -0600
@@ -0,0 +1,167 @@
+This document describes an extension to the LWWire protocol which provides
+virtual serial ports. The protocol comes in two parts. The first is the low
+level signalling protocol to transfer data on the ports and the second is
+the higher level "modem" protocol.
+
+A low level port is simply an eight bit clean channel that allows traffic to
+pass over it bidirectionally. The low level signalling protocol does not
+care what that data is.
+
+A port must be opened before it can be used. Once it is opened, it is
+connected with some sort of endpoint. That may be a modem simulation or some
+other endpoint specification. When communication is finished, the port is
+then closed. A port can be closed by either the client or the server.
+
+LWWire Protocol Extension
+=========================
+
+This extension is called VPORT and is assigned extension number 00. It
+defines the following operations in its extension space:
+
+00 VPORT_OPEN
+
+This request opens a virtual port. It takes the following parameters:
+
+Octet	Meaning
+-----	-------
+ 0-2	F3 00 00
+ 3	endpoint type
+
+The endpoint type is 0 for a basic virtual modem as defined by this
+extension. Any other value is an extension number which may accept VPORT
+connections. In particular, the CCHAN extension is of interest here.
+
+Octet	Meaning
+-----	-------
+0	status
+1	port number assigned
+F0-FF	reserved for private endpoint types
+
+If the status is 0, it means the open was successful and a port was
+successfully allocated. Communication over the port can now begin.
+
+Any other status is a failure and the port number value must be regarded as
+invalid. The server MUST send 0 for this port number in this instance. The
+status will be usually be one 1 if no ports are available to assign or 2 if
+ports are available but the requested endpoint type is not available.
+
+
+01 VPORT_CLOSE
+
+This call informs the server that the client is finished with the specified
+port. The call looks as follows:
+
+Octet	Meaning
+-----	-------
+0-2	F3 00 01
+3	port number
+
+The return from this call is exactly one octet, a status. If the status is
+zero, the port has been closed successfully. If it is 1, it means the port
+was not open in the first place.
+
+When a port is closed, all data currently buffered on the server is cleared.
+It may or may not be delivered to its intended destination.
+
+
+02 VPORT_POLL
+
+This call requests the server to provide information on the specified port.
+The call looks as follows:
+
+Octet	Meaning
+-----	-------
+0-2	F3 00 02
+03	the port to poll
+
+The return looks as follows:
+
+Octet	Meaning
+-----	-------
+0	status
+1	write max
+2	read max
+
+If the status is nonzero, then the port is not open and the write max and
+read max items should be ignored.
+
+Otherwise, write max will be set to the maximum number of octets the server
+is willing to handle on a write operation and read max will be the number of
+bytes the server can provide in response to a read operation. If either
+number exceeds 255, then the server will set the respective value to 255. A
+value of 0 for the read max means the server has no data to send. A value of
+0 for the write max means the server's buffer is full and it is waiting for
+the endpoint to do something with it.
+
+
+03 VPORT_POLLMULTI
+
+This operation is similar to VPORT_POLL except it allows polling multiple
+ports simultaneously. It will return 3 bytes for each port polled, organized
+as in the VPORT_POLL call. The ports are returned in the same order as
+specified in the request.
+
+The request looks as follows:
+
+Octet	Meaning
+-----	-------
+0-2	F3 00 03
+3	number of ports to query
+4-n	port nubmers to query
+
+This operation can easily have quite a large request size and quite a large
+response size (maximum request size is 260 bytes and the maximum response is
+768 bytes). This call should only be used for a small number of active
+ports.
+
+04 VPORT_READ
+
+This request reads bytes from a virtual port. The request looks as follows:
+
+Octet	Meaning
+-----	-------
+0-2	F3 00 04
+3	port number
+4	maximum size to read in octets
+
+The response is an LWWire variable length packet as follows:
+
+Octet	Meaning
+-----	-------
+0	status
+1	number of bytes returned (n)
+2-n	returned data
+
+If status is nonzero, then the read failed. This will usually be due to a
+lack of data (1) OR the port not being open (2). In this case, any
+subsequent octets in the packet can be ignored.
+
+Otherwise, the packet will contain n bytes of data. The client must be
+certain not to request more bytes than it can handle.
+
+
+05 VPORT_WRITE
+
+This request is used to send octets to a port. The request looks as follows:
+
+Octet	Meaning
+-----	-------
+0-2	F3 00 05
+3	port number
+4	number of bytes
+5-n	the data to write
+
+The response is as follows:
+
+Octet	Meaning
+-----	-------
+0	status
+1	bytes written
+
+If status is zero, than the bytes written value will be valid. If all bytes
+were written, then it will match the number in the request. If this value is
+less than the requested number, it means some bytes were not written for
+whatever reason and the client must deal with queueing or buffering as
+appropriate.
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/protocol.txt	Sun May 08 12:56:39 2016 -0600
@@ -0,0 +1,423 @@
+DRAFT 2014-12-24
+
+Introduction
+============
+
+The LWWire protocol is based on the Drivewire protocol originally created by
+Boisy Pitre and later expanded by others including Aaron Wolfe.  The
+original Drivewire protocol was very simple.  It has since acquired quite a
+few bits and bobs that have unfortunate semantics.  Further, Over the years,
+Drivewire has accumulated quite a few features which it is not possible for
+the client to reliably detect support for.  In addition, the protocol seems
+to be ever expanding and accumulating features of limited utility.
+
+The goal of LWWire is to provide a stable core protocol that returns to the
+roots of Drivewire 3. The core protocol will support extension negotiation
+which allows the client to request specific additional features. This
+negotiation must occur before the features can be used.
+
+A secondary goal is for a basic Drivewire client to be able to perform block
+I/O on virtual drives even if it does not support the advanced negotiation.
+
+The Protocol
+============
+
+Throughout the protocol discussion, the term client will refer to the device
+being served, usually a Coco an emulator of some sort. The term server will
+refer to the software running on a PC or other similar device that answers
+requests from the client.
+
+The LWWire protocol is a master-slave protocol with the client serving the
+role of the master. This seems like a reversal but it is necessary because
+the client will usually be the device with limited resources which cannot
+reliably respond to a byte appearing on the communication channel. Thus, all
+transactions are initiated by the client.
+
+All values greater than a single octet are represented in network byte
+order, which is otherwise known as big endian. The protocol is octet based.
+
+The first octet of any request is an operation code. This operation code
+completely defines the request. Further data may follow the operation code,
+depending on the specifc operation. Some operations may require further
+round trips as well.
+
+There are two possible types of response from the server. The first is a
+standard fixed length response which is defined by the particular operation. 
+However, an operation may further define that it returns a variable length
+packet.  Such a packet is prefixed by a 16 bit length followed by the actual
+data.  Where such a packet is described, the header is not included in the
+description.  The variable length return values should be avoided except
+when there is a true benefit to using them.  Such a use might be passing
+along an IP packet, for instance.
+
+When a client comes online, it must send a "DWINIT" operation. The
+driver version/identifier value is not specified by the protocol and must
+not change the behaviour of the server. The server will then respond with
+its version number. If that number is anything other 0x80, the client must
+assume the server is NOT LWWire. If it receives no response, it may continue
+with the notion that it might be a Drivewire 3 server.
+
+If the client does receive an indicator that it is talking to LWWire, and it
+wishes to use anything other than requests in the Base protocol (anyting in
+the "Base Protocol" section below), it MUST initiate a feature negotiation
+request and it MUST NOT initiate any usage of the requested feature unless
+it receives an ACK response from the server.
+
+If the delay between subsequent bytes in a request is greater 10
+milliseconds, the server MUST assume the transaction has failed and treat
+it as an unknown transaction. The client MUST implement a similar timeout
+to prevent entering into an infinite loop waiting for octets that may never
+come. The timeout on the client must be no longer than 1000 milliseconds and
+should be no shorter than 10 milliseconds. These timeouts MUST be applied to
+ALL octets that are part of the communication stream.
+
+Notwithstanding the above, some operations may specify a longer timeout
+which must be respected by that operation.
+
+If the server receives an unknown request OR it detects a timeout receiving
+a request, it MUST abort any ongoing request. It MUST then remain silent for
+at least 1100 milliseconds to force a timeout on the client. The server MAY
+choose to change it's port speed parameters if it detects that the request
+may be valid but there is a transmission speed mismatch. This behaviour MUST
+NOT be relied upon by the client.
+
+
+Protocol Operations
+===================
+
+Each operation is formatted with a header indicating the operation code in
+hexadecimal following by it's name.  Below that is the specification of the
+actual request in a table organized by offset within the request. The table
+may be absent if the request consists of nothing but the operation code.
+Below that is prose describing the operation.
+
+
+Base Protocol
+-------------
+
+The base protocol is always active. It is basically the old Drivewire 3
+protocol with no support for wirebug, which turns out to be useful mostly on
+paper rather than in real circumstances. Operations are listed in numerical
+order.
+
+00 NOOP
+
+This request is a "no-op". The server MUST ignore it and not treat it as an
+error.
+
+
+23 TIME
+
+This request instructs the server to respond with its current date and time.
+The response looks as follows:
+
+Octet	Meaning
+-----	-------
+ 0	years since 1900
+ 1	month (1-12)
+ 2	day (1-31)
+ 3	hour (0-23)
+ 4	minute (0-59)
+ 5	second (0-60)
+ 6	day of week (0-6, 0 = Sunday)
+
+This packet roughly corresponds to the return structure for the localtime()
+function in C. Note that this request is part of the original Drivewire 3
+specification. However, Drivewire 3 specifies only 0-59 for the seconds
+value. LWWire allows the value 60 for the seconds value for the rare case
+where a leap second is in effect. This is unlikely to ever be a problem in
+real deployments since leap seconds can occur at most four times per year.
+
+
+46 PRINTFLUSH
+
+This operation tells the server to flush its print buffer to its defined
+printer or analogue. If it does not support a printer or analogue, or there
+is no data to flush, this operation does nothing.
+
+
+47 GETSTAT
+
+Octet	Meaning
+-----	-------
+ 0	operation code
+ 1	drive number
+ 2	GetStat or SetStat code
+
+This operations are specified for compatibility with Drivewire. There is no
+response defined. It SHOULD be treated the same as NOOP. The server MAY
+choose to log this request but is not required to.
+
+
+49 INIT
+
+This indicates the server MUST switch to Drivewire 3 mode (if it has one)
+or, if it doesn't, switch to Base Protocol mode by disabling all extensions.
+It must also clear any statistics counters and state set by any previous
+operations.
+
+
+50 PRINT
+
+Octet	Meaning
+-----	-------
+ 0	operation code
+ 1	print data octet
+
+This request tells the server to queue the specified print data octet for
+output to a printer or some analogue supported by the server. If the server
+does not support printing, it will simply ignore this request. The server
+MAY choose to flush the print buffer to its printer or analogue at any time,
+say because it has not received additional data for some time. The precise
+mechanism to do this is not specifically defined.
+
+
+52 READ
+
+NOTE: this operation should not be used unless a buffered I/O channel of
+some kind is in use.  Because the client must read the first response byte
+and then decide whether to read 258 further bytes, it is a good idea for the
+server to introduce a short delay after the result code, say the length of a
+single octet transmission. It is strongly recommended that the READEX
+operation be used instead. This operation is supported for compatibility
+with old Drivewire implementations.
+
+Octet	Meaning
+0	operation code
+1	drive number
+2-4	LSN requested
+
+This operation requests the server to read a sector from a specified drive. 
+In the event of an error, this operation will return a nonzero error code
+(see the "Error Codes" section below).  In the event of success, the
+response will look as follows:
+
+Octet	Meaning
+0	00
+1-2	16 bit checksum (simple sum)
+3-258	sector data
+
+If the checksum of the received data does not match, the client may choose
+to retry with the REREAD operation. The server may choose to treat REREAD as
+an alias of READ or it may treat a REREAD without a previous matching READ
+as an error.
+
+
+53 SETSTAT
+
+Octet	Meaning
+-----	-------
+ 0	operation code
+ 1	drive number
+ 2	GetStat or SetStat code
+
+This operation is specified for compatibility with Drivewire. There is no
+response defined. It SHOULD be treated the same as NOOP. The server MAY
+choose to log this request but is not required to.
+
+
+54 TERM
+
+This request indicates the client is finished with the protocol. It should
+be treated the same as INIT (49). It is only specified here for
+compatibility with the old Drivewire 3 protocol. New client implementations
+should not use this operation.
+
+
+57 WRITE
+
+Octet	Meaning
+-----	-------
+0	operation code
+1	drive number
+2-4	24 bit LSN
+5-260	sector data
+261-262	16 bit checksum
+
+This operation tells the server to write the specified sector data to the
+specified LSN on the specified drive.  Before doing the write, however, the
+server will verify the checksum (simple sum of sector octets) and if it
+fails, it will not write the sector and return the appropriate error code.
+Otherwise, it will attempt the write and return an error code if
+appropriate. The error codes are listed in the "Error Codes" section below.
+
+The response to this transaction is a single octet indicating success (00)
+or failure (error code). On a checksum error, the client may choose to retry
+with the REWRITE operation. On other errors, retrying does not make sense as
+the error condition is unlikely to go away.
+
+The server may choose to treat REWRITE as an alias of WRITE. It may also
+choose to treat a REWRITE in the absence of a matching WRITE immediately
+prior as an error. The client is not obligated to use the REWRITE operation
+ever.
+
+
+5A DWINIT
+
+Octet	Meaning
+-----	-------
+ 0	5A (opcode)
+ 1	driver version
+
+The driver version above may be a value assigned by the Drivewire
+maintainer. However, LWWire does not treat any values specially.
+
+Upon receiving this operation, the server must disable any extensions and
+enter into Base Protocol mode. It must also clear any statistics counters
+and state set by any previous operations.
+
+The server will respond with the following packet:
+
+Octet	Meaning
+-----	-------
+ 0	server identifier
+
+The server identifier received will be 0x80 if the server supports the
+LWWire protocol. If it is anything other than 0x80, the client MUST assume
+that the server is NOT LWWire and take whatever action it deems appropriate,
+which may include falling back to Drivewire mode.
+
+
+72 REREAD
+
+See 52 READ.
+
+
+77 REWRITE
+
+See 57 WRITE.
+
+
+D2 READEX
+
+Octet	Meaning
+-----	-------
+ 0	operation code
+ 1	drive number
+ 2-4	24 bit LSN
+
+The READEX  operation requests the server to read the logical sector
+specified by the LSN from the specified drive.  The server will respond with
+256 bytes of data.  In the event that an error occured, it will respond with
+256 NUL bytes.  Otherwise, it will respond with the actual sector data.
+
+The client will calculate a 16 bit checksum which is a simple sum of all
+bytes received.  It will then send that checksum to the server. The server
+must permit a longer timeout waiting for the checksum than is otherwise
+expected to give the remote side long enough to actually calculate the
+checksum. It is recommended that the timeout here be at least 50ms.
+
+Upon receipt of the checksum, the server will verify that it is correct. If
+there was an error reading the sector OR the checksum does not match, the
+server will return one of the error codes in the "Error Codes" section
+below. Otherwise it will return a NUL byte.
+
+In the event of a checksum error, the client may retry the read. Other
+errors are unlikely to go away on a retry so retrying in those cases is not
+recommended.
+
+The REREADEX request follows an identical flow. The server MAY choose to
+treat a REREADEX operation without an immediately preceding READEX operation
+as an error. It may choose to avoid re-reading the sector data from the
+backing store on the server if it has already read the same LSN for a
+previous READEX operation. However, it is also acceptable to simply treat
+this as an alias for READEX.
+
+
+F0 REQUESTEXTENSION
+
+Octet	Meaning
+0	operation code
+1	extension code (8 bits)
+
+This request is used to request a specific extension, as specified by the
+extension code. The server will respond with an ACK response (0x42) or a NAK
+response (0x55). Any other response must be considered an error and the
+client MUST re-initialize its driver and perform the DWINIT handshake again.
+That includes in the case of a timeout.
+
+A NAK means the server does not support the requested extension OR it is
+unwilling to make it available for whatever reason.
+
+An ACK means the server has enabled the requested extension for this
+particular client.
+
+See the section "Extension Codes" for a list of extension codes.
+
+
+F1 DISABLEEXTENSION
+
+Octet	Meaning
+0	operation code
+1	extension code
+
+This request is used to request that the server discontinue usage of a
+specific extension.  The server will respond with an ACK (0x42) if it is
+able to discontinue the extension. This is the normal response. Some
+extensions may not be disablable (which will be specified in the extension
+specification). In this case, the server will respond with a NAK (0x55). In
+the event of a NAK, the client may choose to continue with the extension
+enabled. It may also choose to treat that as an error condition and
+re-initiate the DWINIT handshake which will forcibly disable all extensions.
+
+Requesting to disable an extension that is not enabled is not considered
+an error since it doesn't require changing the state of anything. In that
+case, the ACK response is correct.
+
+See the section "Extension Codes" for a list of extension codes.
+
+
+F2 REREADEX
+
+See D2 READEX.
+
+
+F3 EXTENSIONOP
+
+This request indicates that the request is associated with a specific
+extension. The second octet is the extension number. Everything after that
+is defined entirely by the specified extension. If the specified extension
+is not enabled, this request MUST be treated as an unknown request. The
+server MUST NOT send a response to any request for any extension that is not
+currently active.
+
+This mechanism is provided so that extensions can provide their own
+operations without having to select operation codes from the global pool.
+Codes can only be assigned in the global pool by the LWWire maintainer.
+Codes used inside this request structure can be arbitrarily defined by the
+extension without any coordination.
+
+
+F8 RESET3
+FE RESET1
+FF RESET2
+
+Either one of these operations should be treated as though the client has
+gone away or restarted. These MUST be treated exactly like the INIT (49)
+operation.
+
+
+Error Codes
+===========
+
+Code	Meaning
+----	-------
+00	No error, checksum OK
+F3	checksum error
+F4	read error (out of bounds, underlying I/O error)
+F5	write error
+F6	not ready (invalid drive, etc)
+
+
+Extension Codes
+===============
+
+Any code not otherwise listed below is reserved. If you wish to have an
+official extension code assigned, contact the maintainer of LWWire to
+request one. If you are working on something that does not need general
+distribution, or which is only experimental, consider using the private
+range listed below.
+
+00	VPORT
+E0-EF	reserved for future extension code expansion
+F0-FF	reserved for private extensions and will never be assigned
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Makefile	Sun May 08 12:56:39 2016 -0600
@@ -0,0 +1,3 @@
+CFLAGS += -Wall
+
+all: lwwire lwwire-serial
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lwwire-serial.c	Sun May 08 12:56:39 2016 -0600
@@ -0,0 +1,135 @@
+/*
+Set up a serial port and launch a lwwire process to handle it. The lwwire
+process will be called with "exec".
+
+Usage:
+
+lwwire-serial <port> [speed] [lwwire binary args...]
+
+<port> is the device name for the serial port. [speed] is the optional speed
+to set the port to with a default of 115200. [lwwire binary] is the optional
+name for the lwwire binary. Default is "./lwwire". If [lwwire binary] is
+specified, then [speed] must also be specified.
+
+*/
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+
+int main(int argc, char **argv)
+{
+	char *lwwire = "lwwire";
+	int speed = 115200;
+	char *port;
+	int i, rargc;
+	char **rargv;
+	int fd;
+	struct termios io_mod;
+
+	if (argc >= 4)
+	{
+		lwwire = argv[3];
+	}
+	if (argc >= 3)
+	{
+		speed = strtol(argv[2], NULL, 10);
+	}
+	if (argc < 2)
+	{
+		fprintf(stderr, "Usage: %s <port> [<speed> [<lwwire binary> [args]]]\n", argv[0]);
+		exit(0);
+	}
+	port = argv[1];
+	
+	fprintf(stderr, "Starting lwwire on port '%s' at speed %d\n", port, speed);
+	
+	// open serial port
+	fd = open(port, O_RDWR | O_NOCTTY);
+	if (fd < 0)
+	{
+		fprintf(stderr, "Error opening port %s: %s\n", port, strerror(errno));
+		exit(1);
+	}
+	// set up parameters
+	// this is all theoretically *mostly* posix correct except for some of the 
+	// Bxxxx constants.	
+	tcgetattr(fd, &io_mod);
+	io_mod.c_iflag &= 
+		~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON);
+	io_mod.c_oflag &= ~OPOST;
+	io_mod.c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
+	io_mod.c_cflag &= ~(CSIZE|PARENB);
+	io_mod.c_cflag |= CS8;
+	switch (speed)
+	{
+	case 38400:
+		cfsetispeed(&io_mod, B38400);
+		cfsetospeed(&io_mod, B38400);
+		break;
+	
+	case 57600:
+		cfsetispeed(&io_mod, B57600);
+		cfsetospeed(&io_mod, B57600);
+		break;
+	
+	case 115200:
+		cfsetispeed(&io_mod, B115200);
+		cfsetospeed(&io_mod, B115200);
+		break;
+	
+	case 230400:
+		cfsetispeed(&io_mod, B230400);
+		cfsetospeed(&io_mod, B230400);
+		break;
+		
+	default:
+		fprintf(stderr, "Unrecognzied speed %d on port %s; using 38400\n", speed, port);
+		cfsetispeed(&io_mod, B38400);
+		cfsetospeed(&io_mod, B38400);
+		break;	
+	}
+
+	if (tcsetattr(fd, TCSANOW, &io_mod) < 0)
+	{
+		fprintf(stderr, "Cannot set serial line mode properly: %s", strerror(errno));
+		exit(1);
+	}
+
+	// set up stdin/stdout
+	if (dup2(fd, 0) < 0)
+	{
+		fprintf(stderr, "Cannot set up stdin file descriptor: %s\n", strerror(errno));
+		exit(1);
+	}
+	if (dup2(fd, 1) < 0)
+	{
+		fprintf(stderr, "Cannot set up stdout file descriptor: %s\n", strerror(errno));
+		exit(1);
+	}
+	// close the original FD since we don't need it any more.
+	close(fd);
+
+	// execute binary
+	rargc = 1;
+	if (argc >= 5)
+	{
+		rargc += argc - 4;
+	}
+	rargv = malloc(sizeof(char *) * (rargc + 1));
+	rargv[0] = lwwire;
+	for (i = 4; i < argc; i++)
+	{
+		fprintf(stderr, "Passing argument %d of %d: %s\n", i - 3, rargc, argv[i]);
+		rargv[i - 3] = argv[i];
+	}
+	rargv[rargc] = NULL;
+	
+	execv(lwwire, rargv);
+	fprintf(stderr, "Failed to execute %s: %s\n", lwwire, strerror(errno));
+	exit(1);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lwwire.c	Sun May 08 12:56:39 2016 -0600
@@ -0,0 +1,669 @@
+/*
+This program implements the lwwire protocol. It expects STDIN and STDOUT
+to be connected to an appropriately configured communication channel.
+
+The following timeouts are specified by the protocol and are listed here
+for convenience:
+
+Between bytes in a request: 10 milliseconds
+Between bytes reading a response (client): 10ms <= timeout <= 1000ms
+
+Server receiving bad request: >= 1100 milliseconds
+
+Implementation notes:
+
+This implementation uses low level I/O calls (read()/write()) on STDIN
+and STDOUT because we MUST have fully unbuffered I/O for the protocol
+to function properly and we really do not want the stdio overhead for
+that.
+
+The complexity of the lwwire_readdata() and lwwire_writedata() functions
+is required to handle some possible corner cases that would otherwise
+completely bollix everything up.
+
+Command line options:
+
+drive=N,PATH
+
+Specify that drive #N should reference the file at PATH. Note that the
+file at PATH will be created if it doesn't exist, but only if it is actually
+accessed. N is a decimal number from 0 to 255. If N is prefixed with "C",
+the drive is treated as read-only.
+
+
+By default, no drives are associated with files. Also, it is unspecified
+what happens if multiple protocol instances access the same drive.
+
+*/
+
+// for nanosleep
+#define _POSIX_C_SOURCE 199309L
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/select.h>
+#include <time.h>
+#include <unistd.h>
+
+#define LWERR_NONE 0
+#define LWERR_CHECKSUM 0xF3
+#define LWERR_READ 0xF4
+#define LWERR_WRITE 0xF5
+#define LWERR_NOTREADY 0xF6
+
+struct lwwire_driveinfo
+{
+	char *path;
+	FILE *fp;
+	int isconst;
+};
+
+struct lwwire_driveinfo drivedata[256];
+
+void lwwire_protoerror(void);
+int lwwire_readdata(void *, int, int);
+int lwwire_writedata(void *, int);
+void lwwire_write(void *, int);
+int lwwire_read(void *, int);
+int lwwire_read2(void *, int, int);
+void lwwire_reset(void);
+int lwwire_fetch_sector(int dn, int lsn, void *);
+int lwwire_save_sector(int dn, int lsn, void *);
+int nonblock(int);
+
+void lwwire_proto_read(void);
+void lwwire_proto_write(void);
+void lwwire_proto_readex(void);
+void lwwire_proto_requestextension(void);
+void lwwire_proto_disableextension(void);
+void lwwire_proto_extensionop(void);
+
+int main(int argc, char **argv)
+{
+	unsigned char buf[32];
+	time_t curtime;
+	struct tm *tmval;
+	int rv;
+	int i;
+		
+	// make stdin and stdout non-blocking
+	if (nonblock(0) < 0)
+	{
+		fprintf(stderr, "Cannot make stdin non-blocking: %s\n", strerror(errno));
+		exit(1);
+	}
+	if (nonblock(1) < 0)
+	{
+		fprintf(stderr, "Cannot make stdout non-blocking: %s\n", strerror(errno));
+		exit(1);
+	}
+
+	memset(&drivedata, 0, sizeof(drivedata));
+
+	for (i = 1; i < argc; i++)
+	{
+		if (strncmp("drive=", argv[i], 6) == 0)
+		{
+			int dn=0;
+			int isconst = 0;
+			char *ptr;
+			ptr = argv[i] + 6;
+			if (*ptr == 'C')
+			{
+				isconst = 1;
+				ptr++;
+			}
+			while (*ptr >= '0' && *ptr <= '9')
+			{
+				dn = dn * 10 + (*ptr - '0');
+				ptr++;
+			}
+			if (*ptr != ',' || dn > 255)
+			{
+				fprintf(stderr, "Ignoring invalid drive specification: %s\n", argv[i]);
+				continue;
+			}
+			ptr++;
+			drivedata[dn].path = ptr;
+			drivedata[dn].isconst = isconst;
+		}
+	}
+
+	fprintf(stderr, "Running with the following disk images:\n");
+	for (i = 0; i < 256; i++)
+	{
+		if (drivedata[i].path)
+		{
+			fprintf(stderr, "[%d%s] %s\n", i, drivedata[i].isconst ? "C" : "", drivedata[i].path);
+		}
+	}
+
+	// main loop reading operations and dispatching
+	for (;;)
+	{
+		rv = lwwire_readdata(buf, 1, 0);
+		if (rv < 0)
+		{
+			fprintf(stderr, "Error or timeout reading operation code.\n");
+			lwwire_protoerror();
+			continue;
+		}
+		if (rv == 0)
+		{
+			fprintf(stderr, "EOF on comm channel. Exiting.\n");
+			exit(0);
+		}
+		fprintf(stderr, "Handling opcode %02X\n", buf[0]);
+		
+		// we have an opcode here
+		switch (buf[0])
+		{
+		case 0x00: // NOOP
+			break;
+
+		case 0x23: // TIME
+			curtime = time(NULL);
+			tmval = localtime(&curtime);
+			buf[0] = tmval -> tm_year;
+			buf[1] = tmval -> tm_mon;
+			buf[2] = tmval -> tm_mday;
+			buf[3] = tmval -> tm_hour;
+			buf[4] = tmval -> tm_min;
+			buf[5] = tmval -> tm_sec;
+			buf[6] = tmval -> tm_wday;
+			lwwire_write(buf, 7);
+			break;
+		
+		case 0x46: // PRINTFLUSH
+			// no printer is supported by this implemention so NO-OP
+			break;
+		
+		case 0x47: // GETSTAT (useless dw3 operation)
+		case 0x53: // SETSTAT (useless dw3 operation)
+			// burn two bytes from the client and do nothing
+			lwwire_read(buf, 2);
+			break;
+		
+		case 0x49: // INIT (old style INIT call)
+		case 0x54: // TERM (old DW3 op treated same as INIT)
+		case 0xF8: // RESET3 (junk on the line during reset)
+		case 0xFE: // RESET1 (junk on the line during reset)
+		case 0xFF: // RESET2 (junk on the line during reset)
+			lwwire_reset();
+			break;
+		
+		case 0x50: // PRINT
+			// burn a byte because we don't support any printers
+			lwwire_read(buf, 1);
+			break;
+		
+		case 0x52: // READ
+		case 0x72: // REREAD (same semantics as READ)
+			fprintf(stderr, "DWPROTO: read()\n");
+			lwwire_proto_read();
+			break;
+		
+		case 0x57: // WRITE
+		case 0x77: // REWRITE (same semantics as WRITE)
+			fprintf(stderr, "DWPROTO: write()\n");
+			lwwire_proto_write();
+			break;
+		
+		case 0x5A: // DWINIT (new style init)
+			lwwire_reset();
+			if (lwwire_read(buf, 1) < 0)
+				break;
+			fprintf(stderr, "DWINIT: client drive code %02X\n", buf[0]);
+			// tell the client we speak lwwire protocol
+			buf[0] = 0x80;
+			lwwire_write(buf, 1);
+			break;
+		
+		case 0xD2: // READEX (improved reading operation)
+		case 0xF2: // REREADEX (same semantics as READEX)
+			fprintf(stderr, "DWPROTO: readex()\n");
+			lwwire_proto_readex();
+			break;
+		
+		case 0xF0: // REQUESTEXTENSION
+			lwwire_proto_requestextension();
+			break;
+		
+		case 0xF1: // DISABLEEXTENSION
+			lwwire_proto_disableextension();
+			break;
+		
+		case 0xF3: // EXTENSIONOP
+			lwwire_proto_extensionop();
+			break;
+		
+		default:
+			fprintf(stderr, "Unrecognized operation code %02X. Doing error state.\n", buf[0]);
+			lwwire_protoerror();
+			break;
+		}
+	}
+}
+
+// protocol handling functions
+void lwwire_proto_read(void)
+{
+	unsigned char buf[259];
+	int ec;
+	int lsn;
+	int i;
+	
+	if (lwwire_read(buf, 4) < 0)
+		return;
+	
+	lsn = (buf[1] << 16) | (buf[2] << 8) | buf[3];
+	
+	ec = lwwire_fetch_sector(buf[0], lsn, buf + 1);
+	buf[0] = ec;
+	lwwire_write(buf, 1);
+	if (ec)
+		return;
+	// all this futzing around here is probably a long enough
+	// delay but testing on real hardware is needed here
+	ec = 0;
+	for (i = 1; i < 257; i++)
+		ec += buf[i];
+	buf[257] = (ec >> 8) & 0xff;
+	buf[258] = ec & 0xff;
+	lwwire_write(buf + 1, 258);
+}
+
+void lwwire_proto_write(void)
+{
+	unsigned char buf[262];
+	int lsn;
+	int ec;
+	int i;	
+	if (lwwire_read(buf, 262) < 0)
+		return;
+	
+	lsn = (buf[1] << 16) | (buf[2] << 8) | buf[3];
+	for (ec = 0, i = 4; i < 260; i++)
+		ec += buf[i];
+	if (ec != ((buf[260] << 8) | buf[261]))
+	{
+		buf[0] = LWERR_CHECKSUM;
+	}
+	else
+	{
+		ec = lwwire_save_sector(buf[0], lsn, buf + 4);
+		buf[0] = ec;
+	}
+	lwwire_write(buf, 1);
+}
+
+void lwwire_proto_readex(void)
+{
+	unsigned char buf[256];
+	int lsn;
+	int ec;
+	int i;
+	int csum;
+	if (lwwire_read(buf, 4) < 0)
+		return;
+	lsn = (buf[1] << 16) | (buf[2] << 8) | buf[3];
+	ec = lwwire_fetch_sector(buf[0], lsn, buf);
+	if (ec)
+		memset(buf, 0, 256);
+	for (i = 0, csum = 0; i < 256; i++)
+		csum += buf[i];
+	lwwire_write(buf, 256);
+	if ((i = lwwire_read2(buf, 2, 5)) < 0)
+	{
+		fprintf(stderr, "Error reading protocol bytes: %d, %s\n", i, strerror(errno));
+		return;
+	}
+	i = (buf[0] << 8) | buf[1];
+	if (i != csum)
+		ec = LWERR_CHECKSUM;
+	buf[0] = ec;
+	lwwire_write(buf, 1);
+}
+
+void lwwire_proto_requestextension(void)
+{
+	unsigned char buf[1];
+	
+	if (lwwire_read(buf, 1) < 0)
+		return;
+	// NAK the request
+	buf[1] = 0x55;
+	lwwire_write(buf, 1);
+}
+
+void lwwire_proto_disableextension(void)
+{
+	unsigned char buf[1];
+	
+	if (lwwire_read(buf, 1) < 0)
+		return;
+	// ACK disabling any unsupported extensions
+	buf[1] = 0x42;
+	lwwire_write(buf, 1);
+}
+
+void lwwire_proto_extensionop(void)
+{
+	unsigned char buf[1];
+	if (lwwire_read(buf, 1) < 0)
+		return;
+	// we don't currently support any extensions so treat as unknown
+	lwwire_protoerror();
+}
+
+// Various infrastructure things follow here.
+int nonblock(int fd)
+{
+	int flags;
+	
+	flags = fcntl(fd, F_GETFL);
+	if (flags < 0)
+		return -1;
+	flags |= O_NONBLOCK;
+	return fcntl(fd, F_SETFL, flags);
+}
+
+/*
+Read len bytes from the input. If no bytes are available after
+10 ms, return error.
+
+This *may* allow a timeout longer than 10ms. However, it will
+eventually time out. In the worse case, it is more permissive
+than the specification. It will not time out before 10ms elapses.
+
+If "itimeout" is 0, then it will wait forever for the first
+byte. Otherwise, it will time out even on the first one.
+
+*/
+int lwwire_readdata(void *buf, int len, int itimeout)
+{
+	int toread = len;
+	int rv;
+	fd_set fdset;
+	struct timeval timeout;
+	
+	if (itimeout == 0)
+	{
+		for (;;)
+		{
+			// now wait for the descriptor to be readable
+			FD_ZERO(&fdset);
+			FD_SET(0, &fdset);
+		
+			rv = select(1, &fdset, NULL, NULL, NULL);
+			if (rv < 0)
+			{
+				// this is a last ditch effort to not break completely
+				// in the face of a signal; it should occur only rarely
+				// and it is not clear what the correct behaviour should
+				// be.
+				if (errno == EINTR)
+					continue;
+				return -1;
+			}
+			// if we actually have something to read, move on
+			if (rv > 0)
+				break;
+		}
+	}
+	while (toread > 0)
+	{
+		rv = read(0, buf, toread);
+		if (rv == toread)
+			break;
+		if (rv == 0)
+		{
+			// flag EOF so the caller knows to bail
+			return 0;
+		}
+		if (rv < 0)
+		{
+			if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)
+			{
+				return -1;
+			}
+			rv = 0;
+		}
+		// now rv is the number of bytes read
+		buf += rv;
+		toread -= rv;
+		
+		// now wait for the descriptor to be readable
+		FD_ZERO(&fdset);
+		FD_SET(0, &fdset);
+		timeout.tv_sec = 0;
+		timeout.tv_usec = 10000 * itimeout;
+		
+		rv = select(1, &fdset, NULL, NULL, &timeout);
+		if (rv < 0)
+		{
+			// this is a last ditch effort to not break completely
+			// in the face of a signal; it should occur only rarely
+			// and it is not clear what the correct behaviour should
+			// be.
+			if (errno == EINTR)
+				continue;
+			return -1;
+		}
+		// timeout condition
+		if (rv == 0)
+		{
+			errno = ETIMEDOUT;
+			return -1;
+		}
+		// anything else here means we have more bytes to read
+	}
+	fprintf(stderr, "Protocol bytes read (%d):", len);
+	for (rv = 0; rv < len; rv++)
+		fprintf(stderr, " %02X ", ((char *)(buf))[rv] & 0xff);
+	fprintf(stderr, "\n");
+	return len;
+}
+
+/*
+Write data to the output. This will time out after 10 seconds. The timeout
+is only there in case the underlying communication channel goes out to lunch.
+
+It returns -1 on error or len on success.
+
+The timeout requires the file descriptor to be non-blocking.
+
+*/
+int lwwire_writedata(void *buf, int len)
+{
+	int towrite = len;
+	int rv;
+	fd_set fdset;
+	struct timeval timeout;
+	
+	while (towrite > 0)
+	{
+		rv = write(0, buf, towrite);
+		if (rv == towrite)
+			break;
+		if (rv < 0)
+		{
+			if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)
+			{
+				return -1;
+			}
+			rv = 0;
+		}
+		// now rv is the number of bytes read
+		buf += rv;
+		towrite -= rv;
+		
+		// now wait for the descriptor to be writable
+		FD_ZERO(&fdset);
+		FD_SET(1, &fdset);
+		timeout.tv_sec = 10;
+		timeout.tv_usec = 0;
+		
+		rv = select(2, NULL, &fdset, NULL, &timeout);
+		if (rv < 0)
+		{
+			// this is a last ditch effort to not break completely
+			// in the face of a signal; it should occur only rarely
+			// and it is not clear what the correct behaviour should
+			// be.
+			if (errno == EINTR)
+				continue;
+			return -1;
+		}
+		// timeout condition
+		if (rv == 0)
+		{
+			errno = ETIMEDOUT;
+			return -1;
+		}
+		// anything else here means we have more bytes to write
+	}
+	fprintf(stderr, "Protocol bytes written (%d):", len);
+	for (rv = 0; rv < len; rv++)
+		fprintf(stderr, " %02X ", ((char *)(buf))[rv] & 0xff);
+	fprintf(stderr, "\n");
+	return len;
+}
+
+// like lwwire_writedata() except it bails the program on error.
+void lwwire_write(void *buf, int len)
+{
+	if (lwwire_writedata(buf, len) < 0)
+	{
+		fprintf(stderr, "Error writing %d bytes to client: %s\n", len, strerror(errno));
+		exit(1);
+	}
+}
+
+// like lwwire_readdata() except it bails on EOF and bounces to
+// error state on errors, and always does the timeout for initial
+// bytes. It returns -1 on a timeout. It does not return on EOF.
+// It does not return on random errors.
+int lwwire_read2(void *buf, int len, int toscale)
+{
+	int rv;
+	rv = lwwire_readdata(buf, len, toscale);
+	if (rv < 0)
+	{
+		if (errno == ETIMEDOUT)
+		{
+			lwwire_protoerror();
+			return -1;
+		}
+		fprintf(stderr, "Error reading %d bytes from client: %s\n", len, strerror(errno));
+		exit(1);
+	}
+	if (rv == 0)
+	{
+		fprintf(stderr, "EOF reading %d bytes from client.", len);
+		exit(0);
+	}
+	return 0;
+}
+
+int lwwire_read(void *buf, int len)
+{
+	return lwwire_read2(buf, len, 1);
+}
+
+/*
+Handle a protocol error by maintaining radio silence for
+at least 1100 ms. The pause must be *at least* 1100ms so
+it's no problem if the messing about takes longer. Do a
+state reset after the error.
+*/
+void lwwire_protoerror(void)
+{
+	struct timespec sltime;
+	struct timespec rtime;
+	
+	sltime.tv_sec = 1;
+	sltime.tv_nsec = 100000000;
+	
+	while (nanosleep(&sltime, &rtime) < 0)
+	{
+		// anything other than EINTR indicates something seriously messed up
+		if (errno != EINTR)
+			break;
+		sltime = rtime;
+	}
+	lwwire_reset();
+}
+
+/* fetch a file pointer for the specified drive number */
+FILE *lwwire_fetch_drive_fp(int dn)
+{
+	if (drivedata[dn].path == NULL)
+		return NULL;
+	if (drivedata[dn].fp)
+	{
+		if (ferror(drivedata[dn].fp))
+			fclose(drivedata[dn].fp);
+		else
+			return drivedata[dn].fp;
+	}
+	
+	drivedata[dn].fp = fopen(drivedata[dn].path, "r+");
+	if (!drivedata[dn].fp)
+	{
+		if (errno == ENOENT && !drivedata[dn].isconst)
+		{
+			drivedata[dn].fp = fopen(drivedata[dn].path, "w+");
+		}
+	}
+	return drivedata[dn].fp;
+}
+
+/* read a sector from a disk image */
+int lwwire_fetch_sector(int dn, int lsn, void *buf)
+{
+	FILE *fp;
+	int rc;
+		
+	fp = lwwire_fetch_drive_fp(dn);
+	if (!fp)
+		return LWERR_NOTREADY;
+	
+	if (fseek(fp, lsn * 256, SEEK_SET) < 0)
+		return LWERR_READ;
+	rc = fread(buf, 1, 256, fp);
+	if (rc < 256)
+	{
+		memset(buf + rc, 0, 256 - rc);
+	}
+	return 0;
+}
+
+int lwwire_save_sector(int dn, int lsn, void *buf)
+{
+	FILE *fp;
+	int rc;
+
+	fp = lwwire_fetch_drive_fp(dn);
+	if (!fp)
+		return LWERR_NOTREADY;
+	if (drivedata[dn].isconst)
+		return LWERR_WRITE;
+	if (fseek(fp, lsn * 256, SEEK_SET) < 0)
+		return LWERR_WRITE;
+	rc = fwrite(buf, 1, 256, fp);
+	if (rc < 256)
+		return LWERR_WRITE;
+	return 0;
+}	
+
+
+/*
+Reset the protocol state to "base protocol" mode.
+*/
+void lwwire_reset(void)
+{
+}