A little help translating Ruby?

Kevin

Well-Known Member
Licensed User
Longtime User
I am trying to take some sample Ruby code and write the equivalent in B4A. I think I am understanding most of it, but there is a section where I am not quite sure what it is doing. This code controls an LG Smart TV over a network.

Here is the section in question:
B4X:
def reverse_2bytes hexstr
  hexstr[2..3]+hexstr[0..1]
end

def reverse_4bytes hexstr
  hexstr[6..7]+hexstr[4..5]+hexstr[2..3]+hexstr[0..1]
end

def prepare_2bytes uint16_value
  # We wrap integers in an array [] so we can perform binary conversions.
  # See Ruby's Array.pack() method for an explanation. A simple example:
  # data << [0].pack("N*").unpack("H*")
  reverse_2bytes [uint16_value].pack("n*").unpack("H*").first
end

def prepare_4bytes uint32_value
  # We wrap integers in an array [] so we can perform binary conversions.
  # See Ruby's Array.pack() method for an explanation. A simple example:
  # data << [0].pack("N*").unpack("H*")
  reverse_4bytes [uint32_value].pack("N*").unpack("H*").first
end

def craft_packet cmd0, cmd1, byte0, byte1=nil, byte2=nil, str=nil
  # UDP packets captured with wireshark.
  # Just type "udp.port == 7070" into the filter box.

  # <------------   UDP Payload 18, 22, or 26 bytes   -------------->
  # 1) Original Message
  # uint32      uint32     uint16 uint32      uint32      uint32      uint32
  # <---------> <---------> <---> <---------> <---------> <---------> <--------->
  # 00:00:00:00 54:13:43:65 02:00 08:00:00:00 00:00:00:00 04:00:00:00 04:00:00:00
  # <---------> <---------> <---> <---------> <---------> <---------> <--------->
  # Zero-pad    session     cmd1  cmd2        data1       data2*      data3*
  # 
  # * The data2 and data3 are optional extra arguments

  # Each *individual* fields are little endian (LSB first --> MSB last)
  # Its not as simple as the expected network native big endian.
  # We must reverse each individual field from the Network order.
  # So for example cmd1 "02:00" is actually (uint16)0x02
  #           and  cmd2 "08:00:00:00"   ==  (uint32)0x08

  # 2) Final Message with crc32 checksum filled in.
  #    Where "crc32" field = crc32() of zero-padded Message 1) above
  # 03:14:6b:6d 54:13:43:65 02:00 08:00:00:00 00:00:00:00 04:00:00:00
  # <---------> <---------> <---> <---------> <---------> <--------->
  # crc32       session     cmd1  cmd2        data1       data2*
  # 
  # Final UDP packet
  # "03:14:6b:6d:54:13:43:65:02:00:08:00:00:00 00:00:00:00 04:00:00:00"
  
  data = []

  data << "00000000"                                # Zero-pad
  data << prepare_4bytes( $lgtv[:session].to_i )    # session

  data << prepare_2bytes( cmd0  )                   # cmd1
  data << prepare_4bytes( cmd1  )                   # cmd2

  data << prepare_4bytes( byte0 )                   # data1
  
  if byte1 || byte2
    data << prepare_4bytes( byte1 ) if byte1        # data2
    data << prepare_4bytes( byte2 ) if byte2        # data3

  elsif str
    # For text input mode, there is no data2, data3.
    # Instead we (re-)update the whole textbox. With a variable-length ASCII string
    # f1:db:b2:5d 91:76:f6:15 09:00 0d:00:00:00 01:00:00:00 74:6f:74:6f:74:6f:74 00:00
    #                                                       t  0  t  0  t  0  t  \0 \0
    data << str.unpack("H*").first
    data << ["0000"].pack("H*") # trailing NULLs [0x00, 0x00]
  end

  # Before checksum
  # puts "data = #{data.to_s}"

  crc32 = Zlib::crc32(["#{data}"].pack('H*'))
  data[0]=prepare_4bytes( crc32                )    # crc32

  # After checksum
  # puts "data = #{data.to_s.inspect}"

  bytes = ["#{data}"].pack('H*')
  return bytes
end




Here is the full Ruby code. This is a command-line program.

B4X:
#!/usr/bin/env ruby
# 
# lgremote
#   A command line program to control for LG "Smart" TVs.
#   LV5500,LW5500,LW6500,LW7700,LW9800
#   LV550x,LW550x,LW650x,LW770x,LW980x

# Mouse settings
$mouse_move_start_incr = 15   # pixels
$mouse_incr_multiplier = 1.27 # factor
$mouse_incr_reset_thr  = 1.5  # seconds

# LgRemote config directory
$lgremote_config = "#{ENV["HOME"]}/.lgremote"

# = MIT License
# 
# Copyright (c) 2011 Dreamcat4
# 
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# 

require "rubygems"

# lgremote requires the following gems
require "dnssd"
require "patron"
require "fsdb"
require "highline/import"

require "socket"
require "zlib"
require "timeout"

class Hash
  # def + hash1, hash2
  #   hash1.merge hash2
  # end
  def + hash
    merge hash
  end
end

module LgRemote
  module ActiveSupport
    # <tt></tt>
    #   ActiveSupport::OrderedHash
    #   
    #   Copyright (c) 2005 David Hansson, 
    #   Copyright (c) 2007 Mauricio Fernandez, Sam Stephenson
    #   Copyright (c) 2008 Steve Purcell, Josh Peek
    #   Copyright (c) 2009 Christoffer Sawicki
    #   
    #   Permission is hereby granted, free of charge, to any person obtaining
    #   a copy of this software and associated documentation files (the
    #   "Software"), to deal in the Software without restriction, including
    #   without limitation the rights to use, copy, modify, merge, publish,
    #   distribute, sublicense, and/or sell copies of the Software, and to
    #   permit persons to whom the Software is furnished to do so, subject to
    #   the following conditions:
    #   
    #   The above copyright notice and this permission notice shall be
    #   included in all copies or substantial portions of the Software.
    #   
    #   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    #   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    #   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    #   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    #   LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    #   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
    #   WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    #
    class OrderedHash < Hash
      def initialize(*args, &block)
        super *args, &block
        @keys = []
      end

      def self.[](*args)
        ordered_hash = new

        if (args.length == 1 && args.first.is_a?(Array))
          args.first.each do |key_value_pair|
            next unless (key_value_pair.is_a?(Array))
            ordered_hash[key_value_pair[0]] = key_value_pair[1]
          end

          return ordered_hash
        end

        if (args.first.is_a?(Hash))
          args.each do |h|
            next unless (h.is_a?(Hash))
            h.each do |k,v|
              ordered_hash[k] = v
            end
          end
          return ordered_hash
        end

        unless (args.size % 2 == 0)
          raise ArgumentError.new("odd number of arguments for Hash")
        end

        args.each_with_index do |val, ind|
          next if (ind % 2 != 0)
          ordered_hash[val] = args[ind + 1]
        end

        ordered_hash
      end

      def initialize_copy(other)
        super
        # make a deep copy of keys
        @keys = other.keys
      end

      def store(key, value)
        @keys << key if !has_key?(key)
        super
      end

      def []=(key, value)
        @keys << key if !has_key?(key)
        super
      end

      def delete(key)
        if has_key? key
          index = @keys.index(key)
          @keys.delete_at index
        end
        super
      end

      def delete_if
        super
        sync_keys!
        self
      end

      def reject!
        super
        sync_keys!
        self
      end

      def reject(&block)
        dup.reject!(&block)
      end

      def keys
        (@keys || []).dup
      end

      def values
        @keys.collect { |key| self[key] }
      end

      def to_hash
        self
      end

      def to_a
        @keys.map { |key| [ key, self[key] ] }
      end

      def each_key
        @keys.each { |key| yield key }
      end

      def each_value
        @keys.each { |key| yield self[key]}
      end

      def each
        @keys.each {|key| yield [key, self[key]]}
      end

      alias_method :each_pair, :each

      def clear
        super
        @keys.clear
        self
      end

      def shift
        k = @keys.first
        v = delete(k)
        [k, v]
      end

      def merge!(other_hash)
        other_hash.each {|k,v| self[k] = v }
        self
      end

      def merge(other_hash)
        dup.merge!(other_hash)
      end

      # When replacing with another hash, the initial order of our keys must come from the other hash -ordered or not.
      def replace(other)
        super
        @keys = other.keys
        self
      end

      def inspect
        "#<OrderedHash #{super}>"
      end

    private

      def sync_keys!
        @keys.delete_if {|k| !has_key?(k)}
      end
    end
  end
end

module LgRemote
  if RUBY_VERSION >= '1.9'
    # Inheritance
    #   Ruby 1.9 < Hash
    #   Ruby 1.8 < ActiveSupport::OrderedHash
    class OrderedHash < ::Hash
    end
  else
    # Inheritance
    #   Ruby 1.9 < Hash
    #   Ruby 1.8 < ActiveSupport::OrderedHash
    class OrderedHash < LgRemote::ActiveSupport::OrderedHash
    end
  end
end

$menus = LgRemote::OrderedHash[
  :status_bar, 35,
  :quick_menu, 69,
  :home_menu, 67,
  :premium_menu, 89,
  :installation_menu, 207,
  :factory_advanced_menu1, 251,
  :factory_advanced_menu2, 255,
]

$power_controls = LgRemote::OrderedHash[
  :power_off, 8,
  :sleep_timer, 14,
]

$navigation = LgRemote::OrderedHash[
  :left, 7,
  :right, 6,
  :up, 64,
  :down, 65,
  :select, 68,
  :back, 40,
  :exit, 91,
  :red, 114,
  :green, 113,
  :yellow, 99,
  :blue, 97,
]

$keypad = LgRemote::OrderedHash[
  :"0", 16,
  :"1", 17,
  :"2", 18,
  :"3", 19,
  :"4", 20,
  :"5", 21,
  :"6", 22,
  :"7", 23,
  :"8", 24,
  :"9", 25,
  :underscore, 76,
]

$playback_controls = LgRemote::OrderedHash[
  :play, 176,
  :pause, 186,
  :fast_forward, 142,
  :rewind, 143,
  :stop, 177,
  :record, 189,
]

$input_controls = LgRemote::OrderedHash[
  :tv_radio, 15,
  :simplink, 126,
  :input, 11,
  :component_rgb_hdmi, 152,
  :component, 191,
  :rgb, 213,
  :hdmi, 198,
  :hdmi1, 206,
  :hdmi2, 204,
  :hdmi3, 233,
  :hdmi4, 218,
  :av1, 90,
  :av2, 208,
  :av3, 209,
  :usb, 124,
  :slideshow_usb1, 238,
  :slideshow_usb2, 168,
]

$tv_controls = LgRemote::OrderedHash[
  :channel_up, 0,
  :channel_down, 1,
  :channel_back, 26,
  :favorites, 30,
  :teletext, 32,
  :t_opt, 33,
  :channel_list, 83,
  :greyed_out_add_button?, 85,
  :guide, 169,
  :info, 170,
  :live_tv, 158,
]

$picture_controls = LgRemote::OrderedHash[
  :av_mode, 48,
  :picture_mode, 77,
  :ratio, 121,
  :ratio_4_3, 118,
  :ratio_16_9, 119,
  :energy_saving, 149,
  :cinema_zoom, 175,
  :"3d", 220,
  :factory_picture_check, 252,
]

$audio_controls = LgRemote::OrderedHash[
  :volume_up, 2,
  :volume_down, 3,
  :mute, 9,
  :audio_language, 10,
  :sound_mode, 82,
  :factory_sound_check, 253,
  :subtitle_language, 57,
  :audio_description, 145,
]

$keymap = \
  $menus + $power_controls + \
  $navigation + $keypad + $playback_controls + \
  $input_controls + $tv_controls + \
  $picture_controls + $audio_controls

$keymap_strings = LgRemote::OrderedHash[
  "Menus",              $menus,
  "Power controls",     $power_controls,
  "Navigation",         $navigation,
  "Keypad",             $keypad,
  "Playback controls",  $playback_controls,
  "Input controls",     $input_controls,
  "TV controls",        $tv_controls,
  "Picture controls",   $picture_controls,
  "Audio controls",     $audio_controls
]

labels_array = $keymap_strings.map do |s,h|
  [s.downcase.tr(" ","_").to_sym,h]
end
$keymap_labels = Hash[labels_array]

def create_session lgtv
  $sess = Patron::Session.new
  $sess.timeout  = 5.0
  $sess.base_url = "http://#{lgtv[:address]}:8080"
  $headers = {"Content-Type" => "application/atom+xml" }
end

def load_config_open_session
  unless File.exist?($lgremote_config)
    print "Config files missing. Please pair with \"lgremote pair\"\n\n"
    help
    exit 1
  end

  $db = FSDB::Database.new($lgremote_config)
  $lgtv = $db[$db["default"]]
  # puts $lgtv.inspect

  create_session $lgtv
end

def reconnect failed_resp
  # puts "error"
  # puts failed_resp.body
  # <?xml version="1.0" encoding="utf-8"?><envelope><HDCPError>401</HDCPError><HDCPErrorDetail>Unauthorized</HDCPErrorDetail></envelope>
  error_detail = failed_resp.body.gsub(/.*<HDCPErrorDetai>/,"").gsub(/<\/HDCPErrorDetai>.*/,"")
  if error_detail.downcase =~ /unauthorized/
    resp = $sess.post("/hdcp/api/auth","<?xml version=\"1.0\" encoding=\"utf-8\"?><auth><type>AuthReq</type><value>#{$lgtv[:pairing_key]}</value></auth>",$headers)
    if resp.status == 200
      # Obtain session number
      session = resp.body.gsub(/.*<session>/,"").gsub(/<\/session>.*/,"")
      if session =~ /[0-9]{9}/
        # puts "Connection re-established."
        # store information for next invocation
        $lgtv[:session] = session
        $db["#{$lgtv[:address]}"] = $lgtv # save
        # puts "Session saved."
      end
      return true
    else
      raise "Session timed out. But we failed to re-establish a connection."
    end
  else
    raise failed_resp.body
  end
end

def event name, value=nil
  if value
    resp = $sess.post("/hdcp/api/event","<?xml version=\"1.0\" encoding=\"utf-8\"?><event><session>#{$lgtv[:session]}</session><name>#{name}</name><value>#{value}</value></event>",$headers)
  else
    resp = $sess.post("/hdcp/api/event","<?xml version=\"1.0\" encoding=\"utf-8\"?><event><session>#{$lgtv[:session]}</session><name>#{name}</name></event>",$headers)
  end

  if resp.status == 200
    # puts resp.body
    # <?xml version="1.0" encoding="utf-8"?><envelope><HDCPError>200</HDCPError><HDCPErrorDetail>OK</HDCPErrorDetail><session>114859659</session></envelope>
  else
    reconnect resp
    event name, value
  end
end

def change_channel assigned_no, real_no, uhf_no
  # The 3 parameters must match and agree with whats currently stored in the memory of the TV
  # otherwise we get a blank screen. Problem is the API doesnt let us query such information.
  # Note: If you add all your channels to one of the favorites group, we could download them.
  # But information would go stale whenever the user chooses to update their channel mappings.
  # "<?xml version=\"1.0\" encoding=\"utf-8\"?><command><session>483166968</session><type>HandleChannelChange</type><major>7</major><minor>7</minor><sourceIndex>1</sourceIndex><physicalNum>34</physicalNum></command>"
  resp = $sess.post("/hdcp/api/dtv_wifirc","<?xml version=\"1.0\" encoding=\"utf-8\"?><command><session>#{$lgtv[:session]}</session><type>HandleChannelChange</type><major>#{assigned_no}</major><minor>#{real_no}</minor><sourceIndex>1</sourceIndex><physicalNum>#{uhf_no}</physicalNum></command>",$headers)
  if resp.status == 200
    puts resp.body
    # <?xml version="1.0" encoding="utf-8"?><envelope><HDCPError>200</HDCPError><HDCPErrorDetail>OK</HDCPErrorDetail><session>114859659</session></envelope>
  else
    reconnect resp
    change_channel assigned_no, real_no
  end
end

# def get_favorites
#   # Returns information about channels in the favorites groups A,B,C,D
#   # GET /hdcp/api/data?target=fav_list&session=1664204142
#   resp = $sess.get("/hdcp/api/data?target=fav_list&session=#{$lgtv[:session]}",$headers)
#   if resp.status == 200
#     resp.body
#   else
#     reconnect resp
#     get_favorites
#   end
# end

# def get_model_name
#   # GET "/hdcp/api/data?target=model_info&session="
#   resp = $sess.get("/hdcp/api/data?target=model_info&session=#{$lgtv[:session]}",$headers)
#   if resp.status == 200
#     resp.body.gsub(/.*<modelName>/,"").gsub(/<\/modelName>.*/,"")
#   else
#     reconnect resp
#     get_model_info
#   end
# end

# def get_cur_channel
#   # Gives invalid data when in menus, or external input (eg HDMI)
#   # GET "/hdcp/api/data?target=cur_channel&session="
#   resp = $sess.get("/hdcp/api/data?target=cur_channel&session=#{$lgtv[:session]}",$headers)
#   if resp.status == 200
#     resp.body
#   else
#     reconnect resp
#     get_cur_channel
#   end
# end

def cursor_show
  event "CursorVisible", true
end

def reverse_2bytes hexstr
  hexstr[2..3]+hexstr[0..1]
end

def reverse_4bytes hexstr
  hexstr[6..7]+hexstr[4..5]+hexstr[2..3]+hexstr[0..1]
end

def prepare_2bytes uint16_value
  # We wrap integers in an array [] so we can perform binary conversions.
  # See Ruby's Array.pack() method for an explanation. A simple example:
  # data << [0].pack("N*").unpack("H*")
  reverse_2bytes [uint16_value].pack("n*").unpack("H*").first
end

def prepare_4bytes uint32_value
  # We wrap integers in an array [] so we can perform binary conversions.
  # See Ruby's Array.pack() method for an explanation. A simple example:
  # data << [0].pack("N*").unpack("H*")
  reverse_4bytes [uint32_value].pack("N*").unpack("H*").first
end

def craft_packet cmd0, cmd1, byte0, byte1=nil, byte2=nil, str=nil
  # UDP packets captured with wireshark.
  # Just type "udp.port == 7070" into the filter box.

  # <------------   UDP Payload 18, 22, or 26 bytes   -------------->
  # 1) Original Message
  # uint32      uint32     uint16 uint32      uint32      uint32      uint32
  # <---------> <---------> <---> <---------> <---------> <---------> <--------->
  # 00:00:00:00 54:13:43:65 02:00 08:00:00:00 00:00:00:00 04:00:00:00 04:00:00:00
  # <---------> <---------> <---> <---------> <---------> <---------> <--------->
  # Zero-pad    session     cmd1  cmd2        data1       data2*      data3*
  # 
  # * The data2 and data3 are optional extra arguments

  # Each *individual* fields are little endian (LSB first --> MSB last)
  # Its not as simple as the expected network native big endian.
  # We must reverse each individual field from the Network order.
  # So for example cmd1 "02:00" is actually (uint16)0x02
  #           and  cmd2 "08:00:00:00"   ==  (uint32)0x08

  # 2) Final Message with crc32 checksum filled in.
  #    Where "crc32" field = crc32() of zero-padded Message 1) above
  # 03:14:6b:6d 54:13:43:65 02:00 08:00:00:00 00:00:00:00 04:00:00:00
  # <---------> <---------> <---> <---------> <---------> <--------->
  # crc32       session     cmd1  cmd2        data1       data2*
  # 
  # Final UDP packet
  # "03:14:6b:6d:54:13:43:65:02:00:08:00:00:00 00:00:00:00 04:00:00:00"
  
  data = []

  data << "00000000"                                # Zero-pad
  data << prepare_4bytes( $lgtv[:session].to_i )    # session

  data << prepare_2bytes( cmd0  )                   # cmd1
  data << prepare_4bytes( cmd1  )                   # cmd2

  data << prepare_4bytes( byte0 )                   # data1
  
  if byte1 || byte2
    data << prepare_4bytes( byte1 ) if byte1        # data2
    data << prepare_4bytes( byte2 ) if byte2        # data3

  elsif str
    # For text input mode, there is no data2, data3.
    # Instead we (re-)update the whole textbox. With a variable-length ASCII string
    # f1:db:b2:5d 91:76:f6:15 09:00 0d:00:00:00 01:00:00:00 74:6f:74:6f:74:6f:74 00:00
    #                                                       t  0  t  0  t  0  t  \0 \0
    data << str.unpack("H*").first
    data << ["0000"].pack("H*") # trailing NULLs [0x00, 0x00]
  end

  # Before checksum
  # puts "data = #{data.to_s}"

  crc32 = Zlib::crc32(["#{data}"].pack('H*'))
  data[0]=prepare_4bytes( crc32                )    # crc32

  # After checksum
  # puts "data = #{data.to_s.inspect}"

  bytes = ["#{data}"].pack('H*')
  return bytes
end

def send_packet bytes
  # puts bytes
  sock = UDPSocket.new
  sock.send(bytes, 0, $lgtv[:address], 7070)
  sock.close
end

def move_mouse px, py
  cursor_show
  cmd = [2,8] # move mouse
  bytes = craft_packet( cmd[0], cmd[1], px, py) 
  i = 0
  n = 4
  while i < n
    send_packet bytes
    i += 1
    sleep 0.1
  end
end

def click_mouse
  cursor_show
  cmd = [3,4]
  bytes = craft_packet(cmd[0],cmd[1], 0x02)
  send_packet bytes
end

def enter_text str
  # cmd = [ 9, 6 + str.size ]
  # cmd = [ 9, 8 + str.size ]
  cmd = [ 9, str.size ]
  bytes = craft_packet( cmd[0],cmd[1], 0x01, nil, nil, str )
  send_packet bytes
end

class String
  # Remove the leading spaces of the first line, and same to all lines of a multiline string.
  # This effectively shifts all the lines across to the left, until the first line hits the 
  # left margin.
  # @example
  #   def usage; <<-EOS.undent
  #     # leading indent
  #     # subsequent indent
  #        # subsequent indent + '  '
  #     EOS
  #   end
  def undent
    gsub /^.{#{slice(/^ +/).length}}/, ''
  end
end

$cmd = "$ #{File.basename $0}"

def usage
  <<-EOS.undent
  Usage:
     #{$cmd} <cmd> <args>
  
  Interactive pairing
     #{$cmd} pair
  
  Display pairing key
     #{$cmd} pair 192.168.1.2
  
  Enter pairing key
     #{$cmd} pair 192.168.1.2 AAABBB
  
  Show all buttons
     #{$cmd} press
  
  Show all buttons in group "Menus"
     #{$cmd} press menus
  
  Press button
     #{$cmd} press volume_up
     #{$cmd} press volume_down
  
  Move mouse by 1 increment
     #{$cmd} mouse up
     #{$cmd} mouse down
     #{$cmd} mouse left
     #{$cmd} mouse right
  
  Move mouse by +- {x,y} pixels
     #{$cmd} mouse -25 0
  
  Interactive text entry (tab updates)
     #{$cmd} keyboard
  
  Non-interactive text entry
     #{$cmd} keyboard text_string
    
  EOS
end

def help
  puts usage
end

def bad_arg arg
  print "Unrecognised argument #{arg.inspect}.\n\n"
  help
end

def missing_arg_after arg
  print "Missing argument after #{arg}.\n\n"
  puts usage
end

class DNSSD::Reply::Browse < DNSSD::Reply
  attr_reader :addresses
  attr_reader :address

  def resolve!
    reply = self
    @addresses = []
    resolver = DNSSD.resolve! reply.name, reply.type, 'local' do |reply|
      service = DNSSD::Service.new

      service.getaddrinfo reply.target do |addrinfo|
        @addresses << addrinfo.address
        break unless addrinfo.flags.more_coming?
      end
      break
    end
    @address = @addresses.first
  end

  def inspect
    return "#{name} #{address} (#{name}.#{type}.#{domain.chop})"
  end
end

class DNSSD::Service
  def self.find service, timeout=2.0
    browser = DNSSD::Service.new
    replies = []
    begin
      Timeout::timeout(timeout) do
        browser.browse service do |reply|
          reply.resolve!
          replies << reply
        end
      end
    rescue Timeout::Error
    rescue
    end
    return replies
  end
end

def pair_show_pairing_key lgtv
  create_session lgtv
  resp = $sess.get("/hdcp/api/data?target=version_info",$headers)
  if resp.status == 200
    resp = $sess.post("/hdcp/api/auth","<?xml version=\"1.0\" encoding=\"utf-8\"?><auth><type>AuthKeyReq</type></auth>",$headers)
    if resp.status == 200
      # puts resp.body
      # If xml contains nodes HDCPError=200 && HDCPErrorDetail=OK
      # This means the Pairing key is currently being displayed on the TV
      db = FSDB::Database.new($lgremote_config)
      db["#{lgtv[:address]}"] = lgtv
      db["default"] = lgtv[:address]
      say "Success"
      say "A 6-digit pairing key should be displayed on your TV"
      say "Session saved."
    end
  else
    raise resp.body
  end
end

def pair_with_lgtv lgtv
  create_session lgtv
  resp = $sess.post("/hdcp/api/auth","<?xml version=\"1.0\" encoding=\"utf-8\"?><auth><type>AuthReq</type><value>#{lgtv[:pairing_key]}</value></auth>",$headers)
  if resp.status == 200
    # Obtain session number
    session = resp.body.gsub(/.*<session>/,"").gsub(/<\/session>.*/,"")
    if session =~ /[0-9]{9}/
      lgtv[:session] = session
      lgtv[:mouse_last_moved] = Time.new
      say "Pairing successful"

      # store information for next invocation
      db = FSDB::Database.new($lgremote_config)
      db["#{lgtv[:address]}"] = lgtv
      db["default"] = lgtv[:address]
      say "Session saved."
    end
  else
    puts "Pairing failed."
    puts "<?xml version=\"1.0\" encoding=\"utf-8\"?><auth><type>AuthReq</type><value>#{pairing_key}</value></auth>"
    puts resp.body
  end
end

def pair_interactive
  timeout=1.0
  replies = DNSSD::Service.find("_lg_dtv_wifirc._tcp",timeout)

  # for testing multiple TVs selection list
  # replies << replies.first.dup
  # replies << replies.first.dup

  case replies.size
  when 0
    say "No LG Smart TVs were found on your network."
    say "Please check that:"
    say "TV model is actually labelled as an LG \"SMART\" TV *"
    say "TV is switched on and NOT stuck in the menu."
    say "Both computer + TV are on the same LAN segment."
    say "uPNP is enabled on the local router."
    say " * Not all of LG's DLNA capable TVs are Smart TVs."

  when 1
    puts "One TV found"
    puts replies.first.inspect

  else
    puts replies.first.inspect
    match = agree("Is this your TV?  ", true)

    unless match
      puts "#{replies.size} TVs found."

      choice = choose do |menu|
        menu.prompt = "Which TV do you wish to pair?"

        replies.each do |reply|
          menu.choice reply.inspect
        end
      end

      say "You chose:"
      say choice.inspect
      exit unless agree("Continue?", true)

      replies.delete(choice)
      replies.insert(0,choice)
    end
  end

  r = replies.first
  replies.drop(1)
  lgtv = { :name => r.name, :address => r.address }

  pair_show_pairing_key lgtv

  # Gather user input
  # Obtain the pairing key from the user
  pairing_key = nil
  pairing_key_timeout = 60.0
  begin
    Timeout::timeout(pairing_key_timeout) do
      lgtv[:pairing_key] = ask("Please enter the 6-letter pairing key, as displayed on the TV:") { |q| q.validate = /[a-zA-Z]{6}/ }.upcase
    end
  rescue Timeout::Error
    "Timeout."
    exit
  rescue
    exit
  end
  pair_with_lgtv lgtv
end

def pair args
  # pair
  # pair 192.168.1.2
  # pair 192.168.1.2 AAABBB
  case args[0]
  when nil
    # interactive bonjour
    pair_interactive
  when /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/
    # ip address given
    case args[1]
    when nil
      pair_show_pairing_key :name => "LG Smart TV", :address => args[0]
    when /[a-zA-Z]{6}/
      # ip address + pairing key given
      pair_with_lgtv  :name => "LG Smart TV", :address => args[0], :pairing_key => args[1].upcase
    else
      bad_arg args[1]
    end
  else
    bad_arg args[0]
  end
end

def press_udp key
  # eg
  # volume UP   = 0x02
  # f3:b1:cb:9e 33:a8:89:5b 01:00 04:00:00:00 02:00:00:00
  # volume DOWN = 0x03
  # 96:d6:77:26 33:a8:89:5b 01:00 04:00:00:00 03:00:00:00
  cmd = [1,4]
  bytes = craft_packet( cmd[0],cmd[1], key.to_i )
  send_packet bytes
end

def press_tcp key
  # key = lookup(key)
  resp = $sess.post("/hdcp/api/dtv_wifirc","<?xml version=\"1.0\" encoding=\"utf-8\"?><command><session>#{$lgtv[:session]}</session><type>HandleKeyInput</type><value>#{key}</value></command>",$headers)
  if resp.status == 200
    resp.body
  else
    reconnect resp
    press key
  end
end

def show_keymap label=nil
  if label
    $keymap_strings.each do |l, h|
      if label == l.downcase.tr(" ","_").to_sym
        print "#{l}:\n  "
        puts h.keys.join("\n  ")
      end
    end
  else
    $keymap_strings.each do |label, keymap|
      print "#{label}:\n  "
      puts keymap.keys.join("\n  ")
      print "\n"
    end
  end
end

def press args
  # press quick_menu
  # press mute
  case args[0]
  when nil, "help"
    # print list of available commands
    show_keymap
  else
    label = args.join("_").downcase.to_sym
    if $keymap_labels.keys.include?(label)
      show_keymap label
    else
      if $keymap.keys.include?(label)
        press_tcp $keymap[label]
      else
        bad_arg args[0]
        show_keymap
      end
    end
  end
end

def keyboard args
  # keyboard
  # keyboard "text input"
  reconnect
  if args[0]
    puts args.inspect
    enter_text args.join(" ")
  else
    require 'readline'
    Readline.basic_word_break_characters=""
    Readline.completion_proc = proc{ |s| enter_text(s); nil }
    buf = Readline.readline("Enter text:  ", true)
    enter_text buf
  end
end

$incr = 0
def calc_incr
  if (Time.new - $lgtv[:mouse_last_moved]) > $mouse_incr_reset_thr
    $incr = $mouse_move_start_incr
  else
    # This should be limited
    $incr = ($lgtv[:mouse_move_incr] * $mouse_incr_multiplier).to_i
  end
  $lgtv[:mouse_move_incr] = $incr
  $lgtv[:mouse_last_moved] = Time.new
  $db["#{$lgtv[:address]}"] = $lgtv # save
end

def mouse args
  # mouse up
  # mouse down
  # mouse left
  # mouse right
  # mouse -25 0
  case args[0]
  when nil
    missing_arg_after "mouse"
  when /^[+-]?[0-9]$/
    missing_arg_after args[0] unless args[1]
    case args[1]
    when /^[+-]?[0-9]$/
      move_mouse args[0], args[1]
    else
      bad_arg args[1]
    end
  else
    calc_incr
    case args[0].to_sym
    when :show
      move_mouse(0,0)
    when :left
      move_mouse(-$incr,0)
    when :right
      move_mouse(+$incr,0)
    when :up
      move_mouse(0,-$incr)
    when :down
      move_mouse(0,+$incr)
    when :click
      click_mouse
    else
      bad_arg args[0]
    end
  end
end

class NilClass
  def to_sym
    :nil
  end
end

def main_loop
  valid_cmds = [:pair, :press, :mouse, :keyboard]
  $args = ARGV.dup
  if $args[0]
    first_arg = $args[0].downcase.to_sym
    if valid_cmds.include?(first_arg)
      load_config_open_session unless first_arg == :pair
      send $args[0].downcase.to_sym, $args.dup.drop(1)
    else
      bad_arg $args[0]
    end
  else
    puts usage
  end
end

# Execute main loop
main_loop

If anyone has an LG TV and wishes to try working with this code or wants to know how this is is supposed to work, below is the command line usage info:
Interactive pairing
lgremote pair

Display pairing key
lgremote pair 192.168.1.2

Enter pairing key
lgremote pair 192.168.1.2 AAABBB

Show all buttons
lgremote press

Show all buttons in group "Menus"
lgremote press menus

Press button
lgremote press volume_up
lgremote press volume_down

Move mouse by 1 increment
lgremote mouse up
lgremote mouse down
lgremote mouse left
lgremote mouse right

Move mouse by +- {x,y} pixels
lgremote mouse -25 0

Interactive text entry (tab updates)
lgremote keyboard

Non-interactive text entry
lgremote keyboard text_string


Any help would be appreciated!
 
Top