#!/usr/local/bin/ruby

# rcx.rb
#  RCX communication in Ruby (Apollo: Phi + Comm)

# author: YOSHIDA Kazuhiro
# mailto: moriq@moriq.com
# Home Page: http://www.moriq.com/

# original: talkrcx.txt
#   Talk to your RCX, in hex.
#   Written by Paul Haas, http://hamjudo.com/rcx/

require 'phi'
include Phi
require 'comm'

class String

  # Convert a string into hex for easier viewing by people.
  def hex_dump
    s = self
    ret = "" ; s.each_byte { |c| ret << sprintf("%02x ", c) }
    ret
  end
  
  # Where string is of the form "xx xx xx xx" where x is 0-9a-f
  # hex numbers are limited to 8 bits.
  def hex_pack
    s = self
    s.gsub( /([0-9a-f]{1,2})\s*/i ) { sprintf("%c", $1.hex) }
  end

end

class IR

  def initialize
    # All messages start the same way.
    @header = "55 ff 00".hex_pack
    
    # Every other message has the sequence number bit set.
    # This program makes the sequence bit the inverse of the bit
    # from the previous message.  The "1234" is just an arbitrary
    # previous message.
    @data = "1234"
  end
  
  attr :data
  
  private
  
  # If we don't like the string we get back from the RCX, 
  # complain about it.  This could use work...
  def error(s, recv)
    data = @data
    $stderr.print <<EOT
#{s}:
  sent #{data.hex_dump}
  recv #{recv.hex_dump}
EOT
    ""
  end
  
  def chr2msg(c)
    c.chr + (0xff ^ c).chr
  end
  
  # Given a pair of characters, return true if they are not a complements.
  def comp_bad(pair)
    pair[0] != (0xff ^ pair[1])
  end
  
  public
  
  # Take a request and make it into a happy packet.
  # Note that the sequence number bit should be the opposite
  # of the last packet.  We use the last packet for that
  # information.
  # Packets start with a standard 3 byte header, the data
  # and then end with a checksum.  Every byte is followed
  # by its complement.
  def post(str)
    sum = 0 # checksum
    msg = @header.dup
    seqno = 0x08 != (0x08 & @data[3])
    if seqno then str[0] |= 0x08 else str[0] &= 0xf7 end
    str.each_byte do |c|
      msg << chr2msg(c)
      sum += c
    end
    sum &= 0xff
    msg << chr2msg(sum)
    @data = msg
  end
  
  def recv(inbuff)
    response = ''
    sum = 0
    sum_good = 0
  
    # Make sure that the reply includes an accurate echo of our
    # request.
    if inbuff.index(@data) != 0
      return error("no echo", inbuff)
    end
  
    # Then it should have the standard message header.
    inbuff = inbuff[@data.length..-1]
    if inbuff.index(@header) != 0
      return error("no response header", inbuff)
    end
    inbuff = inbuff[@header.length..-1]
  
    # Now make sure each character is followed by its complement.
    # Also keep track of the checksum.
    while inbuff.length >= 2
      cpair = inbuff[0..1]
      oc = cpair[0]
      if comp_bad(cpair)
        return error(sprintf("bad complement %02x", oc), inbuff)
      end
      response << oc.chr
      sum_good = sum == oc
      sum = 0xff & (sum + oc)
      inbuff = inbuff[2..-1]
    end
  
    unless sum_good
      return error("bad checksum", inbuff)
    end
  
    response = response[0...-1] # remove checksum from response
    response[0] &= 0xf7 # remove sequence
    response
  end

end

comm = CommX.new
comm.bit_rate = 2400
comm.parity_bit = CommX::CPB_ODD
comm.time_out_receive = 300
comm.port_no = 1
comm.open

print "comm rcx: using serial port ##{comm.port_no}.\n"

ir = IR.new

form = Form.new :form1
form.caption = "rcx com#{comm.port_no}"

edit = Edit.new form, :edit1
edit.align = Phi::AL_TOP
edit.text = '10'

btn = Button.new form, :button1
btn.align = Phi::AL_BOTTOM
btn.default = true
btn.caption = 'send'

btn.on_click = proc do
  ir.post(edit.text.hex_pack)
  p comm.send(ir.data)

  max_size = 32
  inbuff = ' ' * max_size
  size = comm.receive(inbuff)
  inbuff = inbuff[0...size]
  print <<EOT
in: #{ir.recv(inbuff).hex_dump}
EOT
end

memo = Memo.new form, :memo1
memo.align = Phi::AL_CLIENT
memo.font.name = '‚l‚r ƒSƒVƒbƒN'
#memo.font.pitch = Phi::FP_FIXED
memo.scroll_bars = Phi::SS_VERTICAL
memo.clear

def $>.write(s)
  s.split(/\n/).each do |line|
    Phi::SCREEN.form1.memo1.lines.add line
  end
end

form.show
Phi.mainloop

comm.close
