XMODEM in ruby

I’m working on adding support for XMODEM file transfers to KipperTerm, and I needed something to act as the “other end” of the XMODEM transmission. Obviously I could have used a real BBS, but that would have a pain to set up, plus the typically convoluted login/file selection process would have made the test itself quite cumbersome. So I decided to put together a simple ruby server that would accept telnet connections and just require transmission of a single character to start dishing out a file via XMODEM.

Unfortunately there didn’t seem to be any existing ruby implementations of XMODEM. So I have released a gem called “modem_protocols” – currently only XMODEM is implemented, although I may add ZMODEM support over Christmas.

The API is pretty simple – you  call “xmodem_tx” or “xmodem_rx” to send or receive a file respectively. Each function takes two IO objects – the “remote” object will typically be a TCP session or serial port connection, and the “local” object will typically be a file. The XMODEM protocol is spoken over the “remote” connection, and the file being transmitted is read from/written to the “local” connection.

Here’s the “dummy BBS” I am using. In this case, I am using “StringIO” objects rather than “File” objects on the “local” side, but it should give you the flavour.

require 'modem_protocols'

LOCAL_PORT=1000

Thread.abort_on_exception = true
BasicSocket.do_not_reverse_lookup=true
ModemProtocols::logger.outputters = Outputter.stdout

def run_server
  server = TCPServer.new( LOCAL_PORT)
  loop do 
    puts "listening"
    begin
      session = server.accept
      puts "connected"
      command=nil
      loop do
        session <<"will you (R)eceive, (S)end with standard XMODEM, or send via XMODEM-(C)RC16 (R/S/C)?\r\n"
        command=session.readchar.chr.downcase
        break if ['c','s','r'].include?(command)
        session << "invalid option #{command}\r\n"
      end
     
     
      case command
        when 'c' then
          file=StringIO.new("")
          session << "start sending now\r\n"
          ModemProtocols::xmodem_rx(session,file,{:mode=>:crc})
          session <<  "rx complete\r\n"
          file.rewind
          puts file.read

        when 's' then
          file=StringIO.new("")
          session << "start sending now\r\n"
          ModemProtocols::xmodem_rx(session,file)
          session <<  "rx complete\r\n"
          file.rewind
          puts file.read
        when 'r' then
          file=StringIO.new("this is a test\n"*100)
          session << "start receiving now\r\n"
          ModemProtocols::xmodem_tx(session,file)
          session <<  "tx complete\r\n"
          file.rewind

      end   
      session.close
    rescue Exception=>e
      puts e
      puts e.backtrace
    end
  end
end

server_thread=Thread.new {run_server}
loop do
  break unless server_thread.alive?
  sleep(0.1)  #wake up occasionally to get keyboard input, so we break on ^C
 end