+-----------------------------------------------------------------------------+ | | | How to Make a Serial-to-TCP Adapter for Vintage Computers | | | | by D!99y Dud3 | | | +-----------------------------------------------------------------------------+ Gr33tz, and welcome to my first textphile! Today, we're going to talk a little bit about socket and serial port programming in PHP. PHP is a scripting language that was originally created for making dynamic websites. What some of you may not realize is that PHP scripts can also be run from the command line. It provides hundreds of extensions that interface with GNU libraries and programs, making it ideal for general-purpose Linux scripting. Here's what you need to get started: (1) A Raspberry Pi or other single-board computer (SBC) that has a WiFi or ethernet adapter. (2) An FTDI USB-to-serial cable. (3) A null modem cable. (4) A home computer with an RS-232C serial port or adapter. (5) Possibly a DE-9 to DB-25 adapter, depending on what connectors your serial port and cables have. Some of these adapters are built for null modem operation, in which case you won't need the null modem cable. The first thing you need to do is install PHP on your SBC. Since it's probably a Debian-based system, we'll use apt-get for that. So execute the following two commands from the console: apt-get update apt-get install php5 This will, incidentally, also install a handy-dandy Apache web server, so you'll be all set if you decide you want to be a World Wide Web phag. Once that's done, head on over to php.net and have a look at the socket functions. The PHP socket functions are very similar to the Linux system calls in C, so if you're familiar with those, you're ahead of the curve. The functions we'll be using are socket_create(), socket_connect(), socket_write(), socket_recv(), and socket_close(). We'll also want to do some error handling with socket_last_error() and socket_strerror(). The socket_create() function returns what is known in PHP as a socket resource. (PHP resources have the curious property that they can be used as integers in a script, but let's not worry too much about that right now.) It can be used to create a TCP, UDP, or Unix socket. The resource is then passed to other socket functions in our script. Here's how to create a TCP socket: The "@" symbol before a PHP function call suppresses printing errors and warnings. We do this if we want to capture the error message and handle it in some way. Notice that we check to see if the function returns false. Many PHP functions return false on error. Since some functions may also return another "empty" value that evaluates as false (e.g., null, 0, or ""), we use the triple equals sign to see if it is exactly false and not one of those other values. The socket_last_error() function returns a numeric error code. We have to pass it to socket_strerror() to get the actual error message. The die() statement simply halts script execution. If we wanted to, we could alternately cause the script to exit with the error code like this: So now that we have our socket resource, we want to connect to a remote server. That's what the aptly named socket_connect() function does: But what if we only know the server's domain name, and not the IP address? Well, there's a DNS lookup function for that, gethostbyname(). We can use regular expressions to see if the provided address matches the nnn.nnn.nnn.nnn pattern of an IPv4 address. If not, we'll look up the IP address: Note that gethostbyname() simply returns the supplied address unmodified if it's unable to resolve it to an IP address, so we checked for that. Let's move on. Now that we've created a client socket and connected to the remote host, we need to send data to and receive data from the server listening on the other end. That's where socket_write() and socket_recv() come in. Since there are a number of things we need to take into account when using these functions, we'll wrap them in custom functions of our own to make things easier to handle. First, our receive function: What's going on here? First of all, there's a bug in some PHP releases where the constant MSG_DONTWAIT isn't defined. So we fixed that. Secondly, as I said above, there are a couple of situations we need to check for: (1) The server disconnected us. In that case, socket_recv() will return 0, so we close the connection on our end and return false. (2) The server may not have anything to say to us, or it may send fewer bytes than we asked for. We use the while loop to poll the server until socket_recv() returns false, which indicates there's no more data from the server. On each pass through the loop, we append the received bytes in $buffer to our return string, $data. The MSG_DONTWAIT constant tells socket_recv() not to hang around forever waiting for the server to say something. Now, for our send() function: Well, that's certainly a lot of code just to send some text over a socket, isn't it? Let's see if we can unravel this mess. First, we may not have sent any bytes at all, in which case there was an error. So we dutifully print out the error message and return false. Secondly, the server may ingest the data in chunks rather than all at once. So we need to keep sending until the entire data string is sent. We can use the $bytesSent variable to see how much of the string was sent with each call to socket_write(), and keep chopping our original $data string down until we have no more bytes left to send, i.e., $length is 0. Finally , we'll take a quick look at shutting the socket down before we move on to the serial port programming part of our show. All we need to do here is call socket_close() at the end of our script: Let's phuck around with serial ports, shall we? There is no native PHP function for that. Fortunately, PHP does have a function that allows us to execute operating system commands. Here, we'll use the PHP exec() function to execute the stty command on a Linux system. This is the command that initializes a Unix TTY terminal. The command line arguments to stty are fairly arcane, and we're not going to spend a whole lot of time covering them all. Instead, we'll wrap the command to initialize an 8-bit connection with one stop bit and no parity in another custom function and call it good. Once the device is initialized, we can open it like a regular file handle and read from it, write to it, and close it: = $timeout) { break; } usleep(10000); } while (char === ""); return $char; } function print($serial, $data) { stream_set_blocking($serial, 1); fwrite($serial, $data); } ?> How do we know which device is the serial port we want though? Here's an easy way to find out. Before you plug the FTDI cable into the USB port on you SBC, execute this from the console: ls -l /dev/ttyUSB* If there are any FTDI cables already connected to the SBC, they'll be listed. Make a note of which ones are there. Now plug in the FTDI cable and execute it again. A new device should appear in the listing. That's the one we want. If you're still with me, your patience is about to be rewarded. It's time to put it all together. What we're going to do is write a script that will accept a host, port number, serial device, and baud as command line arguments. We're not going to do any fancy command line parsing, so you'll have to be sure you pass the arguments in the correct order. We're going to use a while loop to keep the link going until the user types "+++". Basically, anything the user types (received by the getInput() function) gets forwarded to the remote host using the send() function, and anything we receive from the remote host with our receive() function gets forwarded to the serial terminal using the print() function. You'll notice I've thrown some usleep() calls into the various while loops in this script. Those prevent the script from hogging the CPU, and give Linux a few thousand microseconds to take care of other business. Note also that $argv[0] is the name of the PHP script itself as seen by the program we're actually running (the PHP script engine), so our script arguments will start with $argv[1]. Copy the following script, paste it into a text phile, and save it as tcp_serial.php: 0) { send($socket, $input); } $data = receive($socket); if ($data !== false) { print($serial, $data); } usleep(10000); } echo "User issued disconnect command."); socket_close($socket); fclose($serial); function receive($socket) { $data = ""; while ($bytesReceived = @socket_recv($socket, $buffer, 1024, MSG_DONTWAIT)) { if ($bytesReceived === false) { break; } if ($bytesReceived === 0) { echo "The server disconnected."; @socket_close($socket); return false; } $data .= $buffer; } return $data; } function send($socket, $data) { $length = strlen($data); while (true) { $bytesSent = @socket_write($socket, $data, $length); if (($bytesSent === false) || ($bytesSent <= 0)) { echo socket_strerror(socket_last_error($socket)); return false; } if ($bytesSent < $length) { $data = substr($data, $bytesSent); $length -= $bytesSent; } else { return true; } } } function stty($device, $baud) { $command = "/bin/stty -F " . $device . " " . $baud . " sane raw cs8" . " -cstopb hupcl cread clocal -echo -onlcr"; exec($command); } function getInput($serial) { stream_set_blocking($serial, 0); $char = ""; $timeout = time() + 2; do { $char = fgetc($serial); if (time() >= $timeout) { break; } usleep(10000); } while (char === ""); return $char; } function print($serial, $data) { { stream_set_blocking($serial, 1); fwrite($serial, $data); } ?> Now we just need to connect our hardware and run the script. Plug the FTDI cable into the USB port and find out which device is it like I showed you. Then plug the null modem cable into the FTDI cable. Then plug the other end of the null modem cable into the RS-232C port on your old computer. Run the script from the command line like so: php tcp_serial.php bbs.example.com 23 /dev/ttyUSB0 2400 Fire up your terminal program on the home computer, hit the enter key, and see what happens! I have totally not linted, tested, or debugged this script, so let me know if it doesn't work! ------------------------------------------------------------------------------- This has been another riveting communique from D!99y Dud3 at the Department of Demented Development. You can find all premium D!99y Philes (TM) on these fine, scholarly BBSes. Feel free to copy and distribute this phile to all other boards with this notice intact! Agency BBS Sysop: Avon telnet://agency.bbs.nz Borderline BBS Sysop: Balzabaar telnet://borderlinebbs.dyndns.org:6400 Particles! BBS Sysop: Paradroyd telnet://particlesbbs.dyndns.org