Wednesday, March 28, 2012

Example: Simple Banner-Grabbing Modem Dialer Class Object (Using GNU Screen Wrapped in Ruby Goodness)


Here's an example of a class that can be leveraged to interact with modems in an effort to retrieve banners:

class AtHayesScreenDialer
  def initialize(screen_session_name, tty_dev_path, at_init_str="AT S7=45 S0=0 L1 V1 X4 &c1 E1 Q0")
    @screen_session_name = "#{screen_session_name}_#{rand(36**8).to_s(36)}"
    @screen_session_rc_file = "/tmp/screenrc_#{@screen_session_name}"
    @screen_session_log_file = "/tmp/screen_#{@screen_session_name}.log"
    @press_enter = sprintf("\r")
    @screen_cmd = "screen -p 0 -S #{@screen_session_name} -X stuff"

    begin
       # Setup GNU screen session's flushing of logfile buffer to be real-time
      File.open(@screen_session_rc_file, 'w') do |f|
        f.puts "logfile '#{@screen_session_log_file}'"
        f.puts "logfile flush 0"
      end
      # Custom rc file, log, and start in deteached mode with session name
      `screen -c #{@screen_session_rc_file} -L -d -m -S #{@screen_session_name} #{tty_dev_path}`
      sleep 3 # Let's wait a bit to ensure our screen session is ready...
      puts "\nIntializing Screen Session: #{@screen_session_name}"
      print "Sent '#{at_init_str}' Command..."
      `#{@screen_cmd} "#{at_init_str}#{@press_enter}"`
      sleep 1
      puts self.check_response
      return 0
    rescue => e
      puts "ERROR!!! #{e.class} #{e} #{e.backtrace}"
      self.close
      return 1
    end
  end

  def dump
    begin
      return File.read(@screen_session_log_file, :encoding=>"BINARY")
    rescue => e
      puts "ERROR!!! #{e.class} #{e} #{e.backtrace}"
      self.close
      return 1
    end
  end

  def close
    begin
      `screen -S #{@screen_session_name} -X quit`
      File.unlink(@screen_session_rc_file)
      File.unlink(@screen_session_log_file)
      return 0
    rescue => e
      puts "ERROR!!! #{e.class} #{e} #{e.backtrace}"
      return 1
    end
  end

  # Pass in 0 as param for key_delay_seconds if no key delay is desired...
  def press_key(key,times=1,key_delay_seconds=1)
    (1..times).each do
      printf("%s", key)
      sleep key_delay_seconds
    end
    return
  end

  def check_response
    serial_output = File.read(@screen_session_log_file, :encoding=>"BINARY")
    if serial_output.nil?
      response = nil
    else 
      response = serial_output.split(/\r\n/)[-1]
      unless response.nil?
        response = response.strip.chomp
      end
    end
    case response
      when "OK"
        return :OK
      when /CONNECT (9600|38400|115200)/
        return response
      when "ERROR"
        return :ERROR
      when /ATDT[0-9],[0-9]/
        return 0
      when "NO CARRIER"
        return :NO_CARRIER
      when "BUSY"
        return :BUSY
      when "VOICE"
        return :VOICE
    end
  end

  def command_mode
    begin
      `#{@screen_cmd} "+++"`
      sleep 1
      return 0
    rescue => e
      puts "ERROR!!! #{e.class} #{e} #{e.backtrace}"
      self.close
      return 1
    end
  end

  def data_mode
    begin
      `#{@screen_cmd} "ATO#{@press_enter}"`
      sleep 1
      return 0
    rescue => e
      puts "ERROR!!! #{e.class} #{e} #{e.backtrace}"
      self.close
      return 1
    end
  end

  def hangup
    begin
      print "\nHanging Up.  Sending 'ATH' Command..."
      `#{@screen_cmd} "ATH#{@press_enter}"`
      sleep 1
      puts self.check_response
      return 0
    rescue => e
      puts "ERROR!!! #{e.class} #{e} #{e.backtrace}"
      self.close
      return 1
    end
  end

  def reset
    begin
      print "\nHanging Up.  Sending 'ATH' Command..."
      `#{@screen_cmd} "ATZ#{@press_enter}"`
      sleep 1
      puts self.check_response
      return 0
    rescue => e
      puts "ERROR!!! #{e.class} #{e} #{e.backtrace}"
      self.close
      return 1
    end
  end

  def dial(phone_num, call_length_seconds, dial_prefix=nil, banner_grap=true)
    begin
      call_started = Time.now
      call_length_status = Time.now - call_started
      unless dial_prefix.nil?
        print "Dialing #{dial_prefix},#{phone_num} for a duration of #{call_length_seconds} seconds..."
        `#{@screen_cmd} "ATDT#{dial_prefix},#{phone_num}#{@press_enter}"`
      else
        print "Dialing #{phone_num} for a duration of #{call_length_seconds} seconds"
        `#{@screen_cmd} "ATDT#{phone_num}#{@press_enter}"`
      end
      until call_length_status > call_length_seconds
        current_response = self.check_response
        # If we get a CONNECT response, pass a few carriage returns to get a banner response...
        if current_response =~ /CONNECT (9600|38400|115200)/
          puts current_response
          `#{@screen_cmd} #{@press_enter}`
          sleep 1
          `#{@screen_cmd} #{@press_enter}`
          sleep 1
          `#{@screen_cmd} #{@press_enter}`
          `#{@screen_cmd} #{@press_enter}`
          `#{@screen_cmd} #{@press_enter}`
          sleep 3
          if banner_grap == true
            self.command_mode
            self.hangup 
            self.reset
            return 0
          #else
            #jack with the remote session
          end
        end
        print "."
        call_length_status = Time.now - call_started
        sleep 1
      end
      puts "CALL LENGTH MET (#{call_length_seconds} seconds)."
      self.command_mode
      self.hangup
      puts self.check_response
      return 0
    rescue => e
      puts "ERROR!!! #{e.class} #{e} #{e.backtrace}"
      self.close
      return 1
    end
  end
end