# HG changeset patch # User William Astle # Date 1462733799 21600 # Node ID bef2801ac83e9fe98fce8888b146b7e39be7ba33 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. diff -r 000000000000 -r bef2801ac83e .hgignore --- /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 diff -r 000000000000 -r bef2801ac83e README --- /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. diff -r 000000000000 -r bef2801ac83e docs/extension-virtualports.txt --- /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. + + diff -r 000000000000 -r bef2801ac83e docs/protocol.txt --- /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 diff -r 000000000000 -r bef2801ac83e src/Makefile --- /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 diff -r 000000000000 -r bef2801ac83e src/lwwire-serial.c --- /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 [speed] [lwwire binary args...] + + 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 +#include +#include +#include +#include +#include +#include + +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 [ [ [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); +} diff -r 000000000000 -r bef2801ac83e src/lwwire.c --- /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 +#include +#include +#include +#include +#include +#include +#include + +#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) +{ +}