Here I am, still trying to figure out how to simple I/O on a serial port under Windows.
I've just perused Microsoft's lengthy official article entitled "Serial Communications in Win32," and I remain no wiser. Oh, except for this gem:
"The Win32 API does not provide any mechanism for determining what ports exist on a system."
Yikes! I'd sort of assumed there would be such a mechanism, and I just didn't have a clue what it was. No mechanism???? [But see Update 2.]
I guess the article might be enlightening if I were already a Windows programming guru. To someone coming from another world, whether *nix or programming on the bare metal, it's a mixture of baby talk and arcane Windows-specific jargon. Kind of like a VMS manual.
(Oh, and those existing libraries that are supposed to enable Ruby to talk to Windows serial ports? I guess I could compile one of them. If I had a copy of VC++. Which costs... SEVEN HUNDRED BUCKS??? I don't think so. See why hackers like *nix? Shipping a *nix system without development tools is nigh-well unthinkable. Unless you're SCO, of course.)
When I first met UNIX, back in the 1970s, tty devices were treated like files. You'd open /dev/tty3 or whatever, use ioctl to set it the way you wanted it, and then use read and write to receive and send data. Simple. It worked.
MS-DOS also let you open a serial port as a file, using the magic name COM1: or similar. Unfortunately, you couldn't actually use the port this way, which is why every DOS application that did meaningful serial I/O talked directly to the hardware.
...Which is why, in turn, every general-purpose UART made since that time has had to emulate the register set of the NS8250. Which is why today's super-advanced UARTS are such a pain to use, being as how they're stuck with a crippled interface, multiple registers hidden at shared I/O addresses, and so forth.
(Microsoft was seriously Unclear On The Concept of device drivers. Back in the DOS days, I invented a new kind of mouse. Writing a device driver for a mouse ought to be easy, right? Just listen to the serial port, update internal data structures, and report movement to the operating system, no? No. The official Microsoft definition for a mouse driver called for the mouse driver, among its other duties, to draw the cursor on the screen. Which meant that a mouse driver had to know the details not only of the mouse and the port to which it connected, but also of every video card that ever had been or ever would be.)
The non-handling of serial ports was inherited by 16-bit Windows, and persisted at least up through Windows for Warehouses. I have a vague recollection that Win95 programs also commonly needed to talk to the hardware directly (though I may be conflating two projects here).
Anyway, it seems that the latest and greatest Windows still doesn't correctly handle serial-ports-as-files, and it's necessary to get intimate with the Windows API to use such ports meaningfully. The article seems to show the port being opened as a file, maybe with an extra flag set... and then the actual I/O descends into mystery. And even if I could make heads or tails of it, I still wouldn't know how to translate it into Ruby (especially as the various Win32 API modules for Ruby seem to be rather undocumented, but that's a topic for another rant).
Update: Oh, this just gets cuter and cuter! I decide to see what I can do with a serial port as a file, so I write a simple little-bitty Ruby script to open COM7, and loop reading and printing characters (the serial port being used only for input).
Then I run it. It wedges the interpreter, requiring application of the Task Manager to stop it.
OK. Is the port itself working? Launch TeraTerm, set it for COM7, set parameters, test function. Yup. Exit TeraTerm. Run program again.
Now the program buzzes around receiving EOFs on the serial port. When I send actual data from the other computer, it receives it. Hey! I can live with that!
Now I'm recalling what I saw with an earlier program that tried to read a serial port in Scheme. Sometimes it worked (EOF if the port was idle, character if there was one), and sometimes it didn't (interpreter wedged). Maybe the difference was whether or not some other program had previously used the port.
Unplugging and replugging the USB dongle that is COM7 returns it to wedgie mode.
Checking status before and after use of TeraTerm reveals that TeraTerm is leaving timeouts enabled. That makes sense, but didn't I try that already? Unplug, replug, set parameters (with timeouts enabled) with MODE command. Wedgie mode. Run TeraTerm. Mode COM7:/STATUS shows the same as before TeraTerm was run, and yet it works now.
So: programs that know how to use the serial port set some sticky property that's unknown to the MODE command. Isn't that just the cutest thing you ever heard of?
Update 2: Hmmm, the API may not know what COM ports exist, but it turns out the MODE command, sans parameters, does. This leaves only the problem of piping its output back into the application, and doing a bit of pattern searching. Oh, and setting the mysterious "make it start working" bit.
Update 3: Mayhap I've found the mysterious sticky attribute. According to an article I found at aspfree.com, there's a five-DWORD timeout array associated with each serial port, and it's persistent between applications. This seems to match what's going on. Now I just need to sort out how to make the necessary Win32API calls from Ruby, oh joy.
Update 4: Following much puzzlement, I've successfully retrieved and changed Windows serial port attributes with a Ruby script. Part of the trouble was lack of documentation, and part was a bonehead error in my test program.
I'll publish real live working code in a few days (got a busy weekend ahead, so probably no time to polish it now). Here's a snippet:
require 'dl/win32'
get_osfhandle = Win32API.new("msvcrt","_get_osfhandle", %w{i}, 'I')
setCommTimeouts = Win32API.new("kernel32", "SetCommTimeouts", %w{i p}, 'N')
getCommTimeouts = Win32API.new("kernel32", "GetCommTimeouts", %w{i p}, 'V')
getCommState = Win32API.new("kernel32", "GetCommState", %w{i p}, 'N')
setCommState = Win32API.new("kernel32", "SetCommState", %w{i p}, 'N')
$sp = File.open("\\\\.\\COM7","rb+")
$fn = $sp.fileno
$fh = get_osfhandle.call($fn)
# Get DCB; change any parameters that need fiddling; set DCB
dcb = "\0"*80
getCommState.call($fh,dcb)
dcb_u = dcb.unpack("L2B4B2B6B2BB17S3C8S")
printf("DCB:")
dcb_u.each {|v| printf(" %d",v)}
printf("\n")
dcb_u[1] = 9600 # Baud rate
dcb = dcb_u.pack("L2B4B2B6B2BB17S3C8S")
setCommState.call($fh,dcb)
# Set the timeout array to the values TeraTerm uses
params = [-1,0,0,0,0x1A4].pack('LLLLL')
setCommTimeouts.Call($fh,params)
No, this isn't very good Ruby, what with the global variables and everything. When I have time, I'll wrap it in a class that extends File, and also have a POSIX version (require the module, and get whichever applies to your system), and have proper methods for converting comm parameter structures to and from hashes with symbolic keys. I'll probably also use the (undocumented) struct mechanism that seems to be part of the dl module, instead of cryptically-packed arrays.
[Missing links filled in from various places on the web, and in particular an unfortunately truncated comment by Derek Hans at Ruby Inside.]
Recent Comments