Skip to content

Instantly share code, notes, and snippets.

@mrflip
Last active August 29, 2015 14:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrflip/9e15703934d4dac32aff to your computer and use it in GitHub Desktop.
Save mrflip/9e15703934d4dac32aff to your computer and use it in GitHub Desktop.
Spike of a Roomie Remote API

Spike of a Roomie remote API.

Can

  • Load and write json-formatted RoomieRemotes.plist (you can use plutil to go to/from Roomie's binary .plist format)
  • Load and write .XIB files for an image remote (if only those worked!)

Things this lets you do:

  • maintain your remotes in source control
  • Have a template remote (with consistent layout for volume, power, etc) and then layer on only the minimal differences for each device
  • round-trip edits on the device back into the image-remote .xib

This is a gist to show off -- will later clean up my real repo (roomie .plist files can hold naked passwords!) and push it to github.

source 'https://rubygems.org'
gem 'gorillib', "~> 0.6"
gem 'pry', "~> 0.10"
gem 'multi_json', ">= 1.1"
gem 'crack'
gem 'erubis'
module Roomie
class Base
include Gorillib::Model
def collection_key() id ; end
def set_collection_key() receive_id(id) ; end
#
def to_wire
super.tap{|hsh| hsh.delete(:_type) }
end
def handle_extra_attributes(hsh)
warn("Extra attrs: #{hsh.inspect} for #{self}") if not hsh.blank?
end
end
class Action < Roomie::Base
field :command, :string
field :repeat, :integer
field :delay, :integer
field :params, :array, of: :string
field :deviceuuid, :string, doc: 'The device this action should take place on'
field :runuuid, :string
def to_wire
hsh = super
duuid = hsh.delete(:deviceuuid)
pars = hsh.delete(:params) || []
pars.each_with_index{|param, idx| hsh["param#{idx+1}"] = param }
hsh[:deviceuuid] = duuid if duuid.present?
hsh
end
def handle_extra_attributes(hsh)
pars = []
hsh.keys.each do |key|
if key.to_s =~ /^param(\d+)$/
pars[$1.to_i - 1] = hsh.delete(key)
end
end
self.receive_params(pars) if pars.present?
super(hsh)
end
end
class Button < Roomie::Base
field :id, :string, doc: 'Identifier for this button'
field :x, :integer, doc: 'x position in pixels from top left'
field :y, :integer, doc: 'y position in pixels from top left'
field :width, :integer, doc: 'x width in pixels'
field :height, :integer, doc: 'y height in pixels'
field :type, :integer, doc: 'Button type'
field :image, :string, doc: 'Filename of an image to use'
field :name, :string, doc: 'String to show in place of an image'
field :action, :string
field :actions, :array, of: Roomie::Action
field :prefix, :string, doc: 'A popup menu of all actions that start with that prefix'
field :feed, :integer, doc: 'A display of info fed back from the device'
field :panel, :integer, doc: 'reference number for a panel'
field :param, :string
# Sugar for contexts (eg XIB file) where buttons only have one command action
def command
actions.find{|act| act.command.present? }.command
end
def to_json_dump
hsh = to_wire
hsh.delete(:_type)
hsh[:actions] = hsh[:actions].map(&:to_wire) if hsh[:actions].present?
hsh.to_json
end
end
class Image < Roomie::Base
field :id, :string, doc: 'Identifier for this image'
field :filename, :string, position: 0, doc: 'Filename of image, to be placed in the images/ directory'
field :width, :integer, position: 1, doc: 'x width in pixels -- should be less than 320'
field :height, :integer, position: 2, doc: 'y height in pixels -- 1400 is a good number'
end
# class ActionCollection < Roomie::KeyedCollection
# self.item_type = Roomie::Action
# end
# class ButtonCollection < Roomie::KeyedCollection
# self.item_type = Roomie::Button
# end
class Remote < Roomie::Base
field :id, :string, doc: 'Identifier for this remote'
field :name, :string
field :image, Roomie::Image
field :width, :integer
field :height, :integer
#
# 100,3000,1100,101,102,1000,4000,910,8100
# 1100,101,102, 910,8100
field :tbar, :boolean, ord_key: 100, doc: 'Include a top bar?'
field :j2, :boolean, ord_key: 101, doc: 'Include a ()?'
field :j3, :boolean, ord_key: 102, doc: 'Include a ()?'
field :j4, :boolean, ord_key: 1100, doc: 'Include a ()?'
field :ovol, :boolean
field :dpad, :boolean, ord_key: 1000, doc: 'Include a 2 / 5 / 2 directional pad?'
field :ppad, :boolean, ord_key: 3000, doc: 'Include a 3 / 5 / 3 positional pad?'
field :mpad, :boolean, ord_key: 99, doc: 'Include a 1 / 3 / 1 media pad?'
field :npad, :boolean, ord_key: 4000, doc: 'Include a 4 x 9 number pad?'
field :therm, :boolean, ord_key: 910, doc: 'Include a thermostat?'
field :feed, :boolean, ord_key: 8100, doc: 'Include a feed?'
#
field :landscape, :boolean, doc: 'Force orientation?'
field :fullscreen, :boolean, doc: 'Force hide of left bar?'
#
field :order, :array, of: :integer
field :buttons, :array, of: Roomie::Button
#
field :roomuuid, :string
field :deviceuuid, :string
#
field :defbrand, :string
field :deftype, :integer, doc: '??'
field :version, :integer
field :deflayout, :string
field :defcat, :string
def receive_image(val)
if val.is_a?(String)
super({ filename: val })
else
super(val)
end
end
def json_line(hsh)
hsh.compact_blank.to_json[1..-2]
end
def json_attrs_line(*attrs)
json_line attrs.hashify{|attr| self.read_attribute(attr) }
end
def to_inspectable
super.tap{|hsh| hsh[:buttons] = [buttons.count] }
end
def dump_json
str = []
str << json_attrs_line(:name)
str << json_attrs_line(:tbar, :dpad, :ppad, :mpad, :npad, :landscape, :fullscreen)
str << json_line({image: image.filename, height: height, width: width}) if attribute_set?(:image)
str << json_attrs_line(:order)
buttons_dump = []
buttons.each{|button| buttons_dump << button.to_json_dump }
str << %Q{"buttons":\[\n #{ buttons_dump.join(",\n ")}\n \]}
str << json_attrs_line(:defcat, :defbrand, :deftype, :version)
str << json_attrs_line(:roomuuid)
str << json_attrs_line(:deviceuuid)
" {\n " + str.join(",\n ") + "\n }"
end
end
Button.class_eval do
# 100,3000,1100,101,102,1000,4000,910,8100
BTYPES = {
btn: 2001,
tog: 2002,
rowend: 2004,
#
ppad_up: 1004, ppad_up: 1004,
ppad_dn: 1008, ppad_down: 1008,
ppad_lf: 1005, ppad_left: 1005,
ppad_rt: 1007, ppad_right: 1007,
ppad_go: 1006, ppad_enter: 1006,
ppad_tl: 1001, ppad_top_left: 1001,
ppad_cl: 1002, ppad_center_left: 1002,
ppad_bl: 1003, ppad_bottom_left: 1003,
ppad_tr: 1011, ppad_top_right: 1011,
ppad_cr: 1010, ppad_center_right: 1010,
ppad_br: 1009, ppad_bottom_right: 1009,
#
dpad_up: 3001, dpad_up: 3001,
dpad_dn: 3002, dpad_down: 3002,
dpad_lf: 3003, dpad_left: 3003,
dpad_rt: 3004, dpad_right: 3004,
dpad_go: 3005, dpad_enter: 3005,
dpad_tl: 3006, dpad_top_left: 3006,
dpad_bl: 3007, dpad_bottom_left: 3007,
dpad_tr: 3008, dpad_top_right: 3008,
dpad_br: 3009, dpad_bottom_right: 3009,
#
npad_1: 4001,
npad_2: 4002,
npad_3: 4003,
npad_4: 4004,
npad_5: 4005,
npad_6: 4006,
npad_7: 4007,
npad_8: 4008,
npad_9: 4009,
npad_10: 4010,
npad_11: 4011,
npad_12: 4012,
#
feed: 8100,
therm: 910,
}.freeze
BTYPE_NUMS = BTYPES.invert.freeze
end
end
#!/usr/bin/env ruby
require 'bundler'
Bundler.require(:default)
require_relative './lib/roomie'
ROOMIE_DIR = File.expand_path('~/Dropbox/Roomie')
raw_remote = {
image: { filename: 'remote-mrflip-tv.png', width: 310, height: 1438 },
buttons: [
{px: 10, py: 10, width: 80, height: 85, actions: [ {command: "CHANNEL DOWN", repeat: -1 } ]},
{px: 90, py: 10, width: 75, height: 85, actions: [ {command: "PREVIOUS CHANNEL" } ]},
{px: 165, py: 10, width: 75, height: 85, actions: [ {command: "INPUT ANTENNA 1" } ]},
{px: 240, py: 10, width: 75, height: 85, actions: [ {command: "CHANNEL UP", repeat: -1 } ]},
{px: 10, py: 130, width: 60, height: 60, actions: [ {command: "MUTE TOGGLE" } ]},
{px: 70, py: 155, width: 55, height: 50, actions: [ {command: ".VOLUME SET", param1: "50" } ]},
{px: 125, py: 155, width: 55, height: 50, actions: [ {command: ".VOLUME SET", param1: "75" } ]},
{px: 180, py: 155, width: 55, height: 50, actions: [ {command: ".VOLUME SET", param1: "88" } ]},
{px: 235, py: 130, width: 75, height: 88, actions: [ {command: "VOLUME UP", repeat: -1 } ]},
{px: 265, py: 215, width: 45, height: 30, actions: [ {command: "VOLUME UP", repeat: -1 } ]},
{px: 235, py: 275, width: 75, height: 75, actions: [ {command: "VOLUME DOWN", repeat: -1 } ]},
{px: 265, py: 245, width: 45, height: 30, actions: [ {command: "VOLUME DOWN", repeat: -1 } ]},
{px: 10, py: 215, width: 65, height: 65, actions: [ {command: "REWD" } ]},
{px: 205, py: 215, width: 65, height: 65, actions: [ {command: "FORWARD" } ]},
{px: 10, py: 280, width: 65, height: 65, actions: [ {command: "STOP" } ]},
{px: 75, py: 215, width: 130, height: 130, actions: [ {command: "PLAY" } ]},
{px: 10, py: 360, width: 105, height: 75, actions: [ {command: "MENU MAIN" } ]},
{px: 205, py: 360, width: 105, height: 75, actions: [ {command: "RETURN" } ]},
{px: 205, py: 520, width: 105, height: 75, actions: [ {command: "INFO" } ]},
{px: 10, py: 520, width: 105, height: 75, actions: [ {command: "EXIT" } ]},
{px: 10, py: 446, width: 90, height: 67, actions: [ {command: "CURSOR LEFT" } ]},
{px: 115, py: 360, width: 90, height: 67, actions: [ {command: "CURSOR UP" } ]},
{px: 115, py: 533, width: 90, height: 67, actions: [ {command: "CURSOR DOWN" } ]},
{px: 220, py: 446, width: 90, height: 67, actions: [ {command: "CURSOR RIGHT" } ]},
{px: 100, py: 425, width: 120, height: 105, actions: [ {command: "CURSOR ENTER" } ]},
{px: 10, py: 595, width: 45, height: 45, actions: [ {command: "CC" } ]},
{px: 10, py: 640, width: 45, height: 45, actions: [ {command: "CC" } ]},
{px: 50, py: 605, width: 80, height: 80, actions: [ {command: "CC" } ]},
{px: 130, py: 605, width: 60, height: 80, actions: [ {command: "CHANNEL LIST" } ]},
{px: 190, py: 605, width: 60, height: 80, actions: [ {command: "CHANNEL LIST" } ]},
{px: 250, py: 605, width: 60, height: 80, actions: [ {command: "POWER ON", delay: 500 },{command: "INPUT HDMI 1"} ]},
{px: 125, py: 613, width: 65, height: 83, actions: [ {command: "CHANNEL LIST" } ]},
{px: 10, py: 1000, width: 100, height: 90, actions: [ {command: "DIGIT 7" } ]},
{px: 110, py: 1000, width: 100, height: 90, actions: [ {command: "DIGIT 8" } ]},
{px: 210, py: 1000, width: 100, height: 90, actions: [ {command: "DIGIT 9" } ]},
{px: 10, py: 1090, width: 100, height: 90, actions: [ {command: "DIGIT 4" } ]},
{px: 110, py: 1090, width: 100, height: 90, actions: [ {command: "DIGIT 5" } ]},
{px: 210, py: 1090, width: 100, height: 90, actions: [ {command: "DIGIT 6" } ]},
{px: 10, py: 1180, width: 100, height: 90, actions: [ {command: "DIGIT 1" } ]},
{px: 110, py: 1180, width: 100, height: 90, actions: [ {command: "DIGIT 2" } ]},
{px: 210, py: 1180, width: 100, height: 90, actions: [ {command: "DIGIT 3" } ]},
{px: 10, py: 1270, width: 100, height: 90, actions: [ {command: "DIGIT SEPARATOR" } ]},
{px: 110, py: 1270, width: 100, height: 90, actions: [ {command: "DIGIT 0" } ]},
{px: 210, py: 1270, width: 100, height: 90, actions: [ {command: "CURSOR ENTER" } ]},
]
}
# remote = Roomie::Remote.receive(raw_remote)
# remotes_json = MultiJson.load(File.read('RoomieRemotes-v1.json'))
#
# remotes = remotes_json.map{|raw| Roomie::Remote.receive(raw) }
#
# puts "[\n"
# remotes.each do |remote|
# puts remote.dump_json << ",\n\n"
# end
# puts "\n]"
remote_filename = 'RoomieRemotes-v2.json'
remotes_json = MultiJson.load(File.read(remote_filename))
remotes = remotes_json.map{|raw| Roomie::Remote.receive(raw) }
remote = remotes.find{|rem| rem.name == 'Samsung TV Custom' }
require_relative 'lib/roomie/xib'
xib_filename = File.basename(remote.image.filename, '.png') + '.xib'
Roomie::Xib.save_remote(File.join(ROOMIE_DIR, 'images', xib_filename), remote)
system('plutil', '-convert', 'binary1', '-o', File.join(ROOMIE_DIR, 'RoomieRemotes.plist'), remote_filename)
# binding.pry
#!/usr/bin/env bash
plutil -convert binary1 ~/Dropbox/Roomie-Working/RoomieRemotes-v2.json -o ~/Dropbox/Roomie/RoomieRemotes.plist
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6250" systemVersion="13F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3733"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="769" userLabel="Remote">
<rect key="frame" x="0.0" y="0.0" width="<%= @remote.width %>" height="<%= @remote.height %>"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView contentMode="scaleToFill" image="<%= @remote.image.filename %>" id="11">
<rect key="frame" x="0.0" y="0.0" width="<%= @remote.image.width %>" height="<%= @remote.image.height %>"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/>
</accessibility>
</imageView>
<%- @remote.buttons.each_with_index do |button, idx| %>
<%- next unless button.x.present? && button.type.blank? %>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" id="<%= button.id || "btn-#{idx}" %>">
<rect key="frame" x="<%= button.x %>" y="<%= button.y %>" width="<%= button.width %>" height="<%= button.height %>"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="15"/>
<state key="normal">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="disabled" title="<%= button.command %>">
<color key="titleColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
</button>
<%- end %>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
</objects>
<resources>
<image name="<%= @remote.image.filename %>" width="<%= @remote.image.width %>" height="<%= @remote.image.height %>"/>
</resources>
</document>
require 'gorillib'
require 'gorillib/object/try'
class Object
def try(*a, &b)
if a.empty? && block_given?
yield self
elsif !a.empty? && !respond_to?(a.first, true)
nil
else
__send__(*a, &b)
end
end
end
require 'gorillib/model'
require 'gorillib/model/serialization'
# require 'gorillib/metaprogramming/delegation'
# require 'gorillib/metaprogramming/class_attribute'
Gorillib::Model::ClassMethods.module_eval do
def receive(attrs={}, &block)
return nil if attrs.nil?
return attrs if native?(attrs)
#
Gorillib::Model::Validate.hashlike!(attrs){ "attributes for #{self.inspect}" }
type = attrs.delete(:_type) || attrs.delete('_type')
klass = type.present? ? Gorillib::Factory(type) : self
warn "factory #{klass} is not a subcass of #{self} (factory determined by _type: #{type.inspect} in #{attrs.inspect})" unless klass <= self
#
klass.new(attrs, &block)
end
end
module Roomie
class Base
def self.collection(field_name, collection_type, opts={})
item_type = opts[:item_type] = opts.delete(:of) if opts.has_key?(:of)
opts = opts.reverse_merge(
default: ->{ collection_type.new(item_type: item_type, belongs_to: self) } )
fld = field(field_name, collection_type, opts)
define_collection_receiver(fld)
fld
end
end
end
module Enumerable
#
# Gets value of block on each element;
# constructs a hash of element-value pairs
#
# @return [Hash] hash of key-value pairs
def hashify
raise ArgumentError, 'hashify requires a block' unless block_given?
Hash[ self.map{|el| [el, yield(el)] } ]
end
end
require_relative 'roomie/keyed_collection'
require_relative 'roomie/models'
[{
"name":"Samsung TV",
"defbrand":"Samsung","deftype":23,"version":1,"defcat":"All Models","deflayout":"SamsungTV2013",
"roomuuid":"958CC605-E323-4137-B317-E1E122A0D394",
"deviceuuid":"5AFCB3B9-BF41-489B-8AD4-2FC959A573E2",
"fullscreen":true,"ovol":true,
"image":"remote-samsungtv2013","width":310,"height":1438,
"order":[100,3000,1100,101,102,1000,4000,8100,910],
"buttons":[
{"y":97,"x":116,"width":86,"height":86,"actions":[{"command":"INPUT SCROLL"}]},
{"y":97,"x":203,"width":86,"height":86,"actions":[{"command":"INTERNET"}]},
{"y":97,"width":83,"actions":[{"command":"POWER TOGGLE"}],"x":31,"height":84},
{"y":189,"x":116,"width":81,"height":69,"actions":[{"command":"TV"}]},
{"y":189,"x":29,"width":85,"height":69,"actions":[{"command":"MENU SETUP"}]},
{"y":189,"x":201,"width":82,"height":69,"actions":[{"command":"DISPLAY"}]},
{"y":878,"x":31,"width":83,"height":71,"actions":[{"command":"CURSOR LEFT"}]},
{"y":878,"x":198,"width":85,"height":71,"actions":[{"command":"CURSOR RIGHT"}]},
{"y":792,"x":119,"width":73,"height":82,"actions":[{"command":"CURSOR UP"}]},
{"y":696,"x":120,"width":73,"height":94,"actions":[{"command":"SMART HUB"}]},
{"y":713,"x":33,"width":85,"height":62,"actions":[{"command":"MENU MAIN"}]},
{"y":713,"x":199,"width":85,"height":62,"actions":[{"command":"SEARCH"}]},
{"y":802,"x":198,"width":85,"height":62,"actions":[{"command":"INFO"}]},
{"y":802,"x":32,"width":85,"height":62,"actions":[{"command":"TOOLS"}]},
{"y":956,"x":31,"width":85,"height":62,"actions":[{"command":"RETURN"}]},
{"y":1026,"x":30,"width":66,"height":52,"actions":[{"command":"FUNCTION RED"}]},
{"y":1080,"x":33,"width":81,"height":50,"actions":[{"command":"FAMILY STORY"}]},
{"y":1132,"x":33,"width":81,"height":45,"actions":[{"command":"SUPPORT"}]},
{"y":1132,"x":116,"width":81,"height":45,"actions":[{"command":"PICTURE"}]},
{"y":1182,"x":115,"width":81,"height":45,"actions":[{"command":"PAUSE"}]},
{"y":1182,"x":33,"width":81,"height":45,"actions":[{"command":"REVERSE"}]},
{"y":1182,"x":200,"width":81,"height":45,"actions":[{"command":"FORWARD"}]},
{"y":1230,"x":115,"width":81,"height":54,"actions":[{"command":"PLAY"}]},
{"y":1230,"x":200,"width":81,"height":54,"actions":[{"command":"STOP"}]},
{"y":1230,"x":33,"width":81,"height":54,"actions":[{"command":"RECORD"}]},
{"y":1132,"x":200,"width":81,"height":45,"actions":[{"command":"CC"}]},
{"y":1080,"x":116,"width":81,"height":50,"actions":[{"command":"MTS SAP"}]},
{"y":1080,"x":200,"width":81,"height":50,"actions":[{"command":"3D"}]},
{"y":1026,"x":97,"width":61,"height":52,"actions":[{"command":"FUNCTION GREEN"}]},
{"y":1026,"x":159,"width":61,"height":52,"actions":[{"command":"FUNCTION YELLOW"}]},
{"y":1026,"x":220,"width":61,"height":52,"actions":[{"command":"FUNCTION BLUE"}]},
{"y":956,"x":198,"width":85,"height":62,"actions":[{"command":"EXIT"}]},
{"y":950,"x":119,"width":73,"height":72,"actions":[{"command":"CURSOR DOWN"}]},
{"y":522,"x":125,"width":65,"height":83,"actions":[{"command":"MUTE TOGGLE"}]},
{"y":613,"x":125,"width":65,"height":83,"actions":[{"command":"CHANNEL LIST"}]},
{"y":262,"x":34,"width":80,"height":59,"actions":[{"command":"DIGIT 1"}]},
{"y":262,"x":116,"width":81,"height":59,"actions":[{"command":"DIGIT 2"}]},
{"y":262,"x":204,"width":85,"height":59,"actions":[{"command":"DIGIT 3"}]},
{"y":325,"x":34,"width":75,"height":61,"actions":[{"command":"DIGIT 4"}]},
{"y":325,"x":117,"width":79,"height":61,"actions":[{"command":"DIGIT 5"}]},
{"y":325,"x":204,"width":85,"height":60,"actions":[{"command":"DIGIT 6"}]},
{"y":389,"x":31,"width":78,"height":59,"actions":[{"command":"DIGIT 7"}]},
{"y":389,"x":117,"width":79,"height":59,"actions":[{"command":"DIGIT 8"}]},
{"y":388,"x":204,"width":85,"height":60,"actions":[{"command":"DIGIT 9"}]},
{"y":455,"x":31,"width":78,"height":59,"actions":[{"command":"DIGIT SEPARATOR"}]},
{"y":455,"x":117,"width":79,"height":59,"actions":[{"command":"DIGIT 0"}]},
{"y":454,"x":204,"width":85,"height":60,"actions":[{"command":"PREVIOUS CHANNEL"}]},
{"y":528,"x":31,"width":91,"height":87,"actions":[{"command":"VOLUME UP","repeat":-1}]},
{"y":615,"x":30,"width":92,"height":90,"actions":[{"command":"VOLUME DOWN","repeat":-1}]},
{"y":528,"x":194,"width":91,"height":87,"actions":[{"command":"CHANNEL UP"}]},
{"y":615,"x":193,"width":92,"height":90,"actions":[{"command":"CHANNEL DOWN"}]},
{"y":878,"x":116,"width":80,"height":71,"actions":[{"command":"CURSOR ENTER"}]},
{"type":5001,"image":"vr-volup","actions":[{"command":"VOLUME UP","repeat":-1}]},
{"type":5002,"image":"vr-voldown","actions":[{"command":"VOLUME DOWN","repeat":-1}]},
{"type":5003,"image":"vr-mute","actions":[{"command":"MUTE TOGGLE"}]}
]}
]
[
{
"name":"XBMC Media Player Virtual",
"dpad":true,"tbar":true,"landscape":true,
"order":[100,3000,1100,101,102,1000,4000,8100,910],
"buttons":[
{"type":2001,"image":"keyboard","action":"kb"},
{"type":2001,"image":"vr-mute","actions":[{"command":"MUTE TOGGLE"}]},
{"type":2002,"image":"vr-volup","actions":[{"command":"VOLUME UP","repeat":-1}]},
{"type":2002,"image":"vr-voldown","actions":[{"command":"VOLUME DOWN","repeat":-1}]},
{"type":2004},
{"type":3001,"image":"vr-dpad-up","actions":[{"command":"CURSOR UP","repeat":-2}]},
{"type":3002,"image":"vr-dpad-down","actions":[{"command":"CURSOR DOWN","repeat":-2}]},
{"type":3003,"image":"vr-dpad-left","actions":[{"command":"CURSOR LEFT","repeat":-2}]},
{"type":3004,"image":"vr-dpad-right","actions":[{"command":"CURSOR RIGHT","repeat":-2}]},
{"type":3005,"image":"vr-dpad-enter","actions":[{"command":"CURSOR ENTER"}]},
{"type":3006,"name":"Info","actions":[{"command":"INFO"}]},
{"type":3007,"image":"vr-home","actions":[{"command":"MENU HOME"}]},
{"type":3008,"name":"Menu","actions":[{"command":"MENU"}]},
{"type":3009,"image":"vr-back","actions":[{"command":"BACK"}]},
{"type":3000},
{"type":3006,"image":"vr-previous","actions":[{"command":"PREVIOUS"}]},
{"type":3003,"image":"vr-reverse","actions":[{"command":"REVERSE"}]},
{"type":3005,"image":"vr-play","actions":[{"command":"PLAY PAUSE TOGGLE"}]},
{"type":3004,"image":"vr-forward","actions":[{"command":"FORWARD"}]},
{"type":3008,"image":"vr-next","actions":[{"command":"NEXT"}]},
{"type":3002,"image":"vr-stop","actions":[{"command":"STOP"}]},
{"type":3007,"image":"vr-replay","actions":[{"command":"REPLAY"}]},
{"type":3009,"image":"vr-skip","actions":[{"command":"SKIP"}]},
{"type":3001,"image":"vr-gesture","action":"switch"},
{"type":2001,"name":"Audio","prefix":"AUDIO"},
{"type":2001,"name":"Codec","actions":[{"command":"CODEC"}]},
{"type":2001,"name":"Favorites","actions":[{"command":"FAVORITES"}]},
{"type":2001,"name":"Files","actions":[{"command":"FILES"}]},
{"type":2001,"name":"Fullscreen","actions":[{"command":"FULLSCREEN"}]},
{"type":2001,"name":"Library","prefix":"LIBRARY"},
{"type":2001,"name":"Movie Sets","actions":[{"command":"MOVIE SETS"}]},
{"type":2001,"name":"Movies","actions":[{"command":"MOVIES"}]},
{"type":2001,"name":"Music","prefix":"MUSIC"},
{"type":2001,"name":"OSD","actions":[{"command":"OSD"}]},
{"type":2001,"name":"Pictures","actions":[{"command":"PICTURES"}]},
{"type":2001,"name":"Programs","actions":[{"command":"PROGRAMS"}]},
{"type":2001,"name":"Quit","actions":[{"command":"QUIT"}]},
{"type":2001,"name":"Repeat","prefix":"REPEAT"},
{"type":2001,"name":"Rotate","prefix":"ROTATE"},
{"type":2001,"name":"Settings","actions":[{"command":"SETTINGS"}]},
{"type":2001,"name":"Shuffle","prefix":"SHUFFLE"},
{"type":2001,"name":"Skip","prefix":"SKIP"},
{"type":2001,"name":"Step","prefix":"STEP"},
{"type":2001,"name":"Subtitle","prefix":"SUBTITLE"},
{"type":2001,"name":"System","prefix":"SYSTEM"},
{"type":2001,"name":"TV Shows","actions":[{"command":"TV SHOWS"}]},
{"type":2001,"name":"Watched","actions":[{"command":"WATCHED"}]},
{"type":2001,"name":"Weather","actions":[{"command":"WEATHER"}]},
{"type":2001,"name":"Zoom","prefix":"ZOOM"},
{"type":7000},
{"type":7002,"image":"vr-mute","actions":[{"command":"MUTE TOGGLE"}]},
{"type":7005,"image":"vr-volup","actions":[{"command":"VOLUME UP","repeat":-1}]},
{"type":7006,"image":"vr-voldown","actions":[{"command":"VOLUME DOWN","repeat":-1}]},
{"type":7004,"image":"vr-vr","action":"switch"},
{"type":7001,"name":"Info","actions":[{"command":"INFO"}]},
{"type":7003,"image":"vr-home","actions":[{"command":"MENU HOME"}]},
{"type":7000},
{"type":7005,"image":"vr-dpad-up","actions":[{"command":"CURSOR UP","repeat":-2}]},
{"type":7006,"image":"vr-dpad-down","actions":[{"command":"CURSOR DOWN","repeat":-2}]},
{"type":7007,"image":"vr-dpad-left","actions":[{"command":"CURSOR LEFT","repeat":-2}]},
{"type":7008,"image":"vr-dpad-right","actions":[{"command":"CURSOR RIGHT","repeat":-2}]},
{"type":7009,"image":"vr-dpad-enter","actions":[{"command":"CURSOR ENTER"}]},
{"type":7002,"name":"Menu","actions":[{"command":"MENU"}]},
{"type":7001,"image":"vr-back","actions":[{"command":"BACK"}]},
{"type":7004,"image":"vr-vr","action":"switch"},
{"type":7000},
{"type":7001,"image":"vr-previous","actions":[{"command":"PREVIOUS"}]},
{"type":7007,"image":"vr-reverse","actions":[{"command":"REVERSE","repeat":-2}]},
{"type":7009,"image":"vr-play","actions":[{"command":"PLAY PAUSE TOGGLE"}]},
{"type":7008,"image":"vr-forward","actions":[{"command":"FORWARD","repeat":-2}]},
{"type":7002,"image":"vr-next","actions":[{"command":"NEXT"}]},
{"type":7006,"image":"vr-stop","actions":[{"command":"STOP"}]},
{"type":7003,"image":"vr-replay","actions":[{"command":"REPLAY","repeat":-2}]},
{"type":7005,"image":"vr-skip","actions":[{"command":"SKIP","repeat":-2}]},
{"type":7004,"image":"vr-vr","action":"switch"}
],
"defcat":"JSON","defbrand":"XBMC","deftype":13,"version":1,
"roomuuid":"958CC605-E323-4137-B317-E1E122A0D394",
"deviceuuid":"2532FFB1-C72D-4030-9797-325480D35B62"
},
{
"name":"Samsung TV Custom",
"image":"remote-mrflip-tv","width":310,"height":1438,
"ovol":true,
"order":[100,3000,1100,101,102,1000,4000,8100,910],
"buttons":[
{"x": 10,"y": 10,"width": 80,"height": 85, "actions":[{"command":"CHANNEL DOWN","repeat":-1}]},
{"x": 90,"y": 10,"width": 75,"height": 85, "actions":[{"command":"PREVIOUS CHANNEL"}]},
{"x":165,"y": 10,"width": 75,"height": 85, "actions":[{"command":"INPUT ANTENNA 1"}]},
{"x":240,"y": 10,"width": 75,"height": 85, "actions":[{"command":"CHANNEL UP","repeat":-1}]},
{"x": 10,"y": 130,"width": 60,"height": 60, "actions":[{"command":"MUTE TOGGLE"}]},
{"x": 70,"y": 155,"width": 55,"height": 50, "actions":[{"command":".VOLUME SET", "param1":"50"}]},
{"x":125,"y": 155,"width": 55,"height": 50, "actions":[{"command":".VOLUME SET", "param1":"75"}]},
{"x":180,"y": 155,"width": 55,"height": 50, "actions":[{"command":".VOLUME SET", "param1":"88"}]},
{"x":235,"y": 130,"width": 75,"height": 88, "actions":[{"command":"VOLUME UP","repeat":-1}]},
{"x":265,"y": 215,"width": 45,"height": 30, "actions":[{"command":"VOLUME UP","repeat":-1}]},
{"x":235,"y": 275,"width": 75,"height": 75, "actions":[{"command":"VOLUME DOWN","repeat":-1}]},
{"x":265,"y": 245,"width": 45,"height": 30, "actions":[{"command":"VOLUME DOWN","repeat":-1}]},
{"x": 10,"y": 215,"width": 65,"height": 65, "actions":[{"command":"REWD"}]},
{"x":205,"y": 215,"width": 65,"height": 65, "actions":[{"command":"FORWARD"}]},
{"x": 10,"y": 280,"width": 65,"height": 65, "actions":[{"command":"STOP"}]},
{"x": 75,"y": 215,"width":130,"height":130, "actions":[{"command":"PLAY"}]},
{"x": 10,"y": 360,"width":105,"height": 75, "actions":[{"command":"MENU MAIN"}]},
{"x":205,"y": 360,"width":105,"height": 75, "actions":[{"command":"RETURN"}]},
{"x":205,"y": 520,"width":105,"height": 75, "actions":[{"command":"INFO"}]},
{"x": 10,"y": 520,"width":105,"height": 75, "actions":[{"command":"EXIT"}]},
{"x": 10,"y": 446,"width": 90,"height": 67, "actions":[{"command":"CURSOR LEFT"}]},
{"x":115,"y": 360,"width": 90,"height": 67, "actions":[{"command":"CURSOR UP"}]},
{"x":115,"y": 533,"width": 90,"height": 67, "actions":[{"command":"CURSOR DOWN"}]},
{"x":220,"y": 446,"width": 90,"height": 67, "actions":[{"command":"CURSOR RIGHT"}]},
{"x":100,"y": 425,"width":120,"height":105, "actions":[{"command":"CURSOR ENTER"}]},
{"x": 10,"y": 595,"width": 45,"height": 45, "actions":[{"command":"CC"}]},
{"x": 10,"y": 640,"width": 45,"height": 45, "actions":[{"command":"CC"}]},
{"x": 50,"y": 605,"width": 80,"height": 80, "actions":[{"command":"CC"}]},
{"x":130,"y": 605,"width": 60,"height": 80, "actions":[{"command":"CHANNEL LIST"}]},
{"x":190,"y": 605,"width": 60,"height": 80, "actions":[{"command":"CHANNEL LIST"}]},
{"x":250,"y": 605,"width": 60,"height": 80, "actions":[{"command":"POWER ON","delay":500},{"command":"INPUT HDMI 1"}]},
{"x":125,"y": 613,"width": 65,"height": 83, "actions":[{"command":"CHANNEL LIST"}]},
{"x": 10,"y":1000,"width":100,"height": 90, "actions":[{"command":"DIGIT 7"}]},
{"x":110,"y":1000,"width":100,"height": 90, "actions":[{"command":"DIGIT 8"}]},
{"x":210,"y":1000,"width":100,"height": 90, "actions":[{"command":"DIGIT 9"}]},
{"x": 10,"y":1090,"width":100,"height": 90, "actions":[{"command":"DIGIT 4"}]},
{"x":110,"y":1090,"width":100,"height": 90, "actions":[{"command":"DIGIT 5"}]},
{"x":210,"y":1090,"width":100,"height": 90, "actions":[{"command":"DIGIT 6"}]},
{"x": 10,"y":1180,"width":100,"height": 90, "actions":[{"command":"DIGIT 1"}]},
{"x":110,"y":1180,"width":100,"height": 90, "actions":[{"command":"DIGIT 2"}]},
{"x":210,"y":1180,"width":100,"height": 90, "actions":[{"command":"DIGIT 3"}]},
{"x": 10,"y":1270,"width":100,"height": 90, "actions":[{"command":"DIGIT SEPARATOR"}]},
{"x":110,"y":1270,"width":100,"height": 90, "actions":[{"command":"DIGIT 0"}]},
{"x":210,"y":1270,"width":100,"height": 90, "actions":[{"command":"CURSOR ENTER"}]}
],
"defcat":"All Models","defbrand":"Samsung","deftype":23,"version":1,
"roomuuid":"958CC605-E323-4137-B317-E1E122A0D394",
"deviceuuid":"5AFCB3B9-BF41-489B-8AD4-2FC959A573E2"
}
]
module Roomie
#
# @example:
# require_relative 'lib/roomie/xib'
# remote_from_xib = Roomie::Xib.load_remote('Roomie DDK-2/Sample Remote/remote-samsungtv2013.xib')
# Roomie::Xib.save_remote('/tmp/remote-roundtrip.xib', remote_from_xib)
# puts `diff -uw 'Roomie\ DDK-2/Sample\ Remote/remote-samsungtv2013.xib' '/tmp/remote-roundtrip.xib'`
#
module Xib
extend self # call methods on this module, or include and use
def load_remote(filename)
require 'crack'
xib = File.read(filename)
hsh = Crack::XML.parse(xib)
remote = Roomie::Remote.from_xib_hsh(hsh)
return remote
end
# @example
# Roomie::Xib.save_remote('/tmp/remote-roundtrip.xib', remote_from_xib)
def save_remote(filename, remote)
context = { remote: remote }
File.open(filename, 'wb') do |outf|
outf << xib_templater.evaluate(context)
end
end
def xib_templater()
template = File.read(File.join(File.dirname(__FILE__), 'templates/remote-template.xib.erb'))
erber = Erubis::Eruby.new(template)
end
end
Button.class_eval do
def self.from_xib_hsh(hsh)
rect = hsh['rect']
id = hsh['id']
func = hsh['state'].find{|st| st['key'] == 'disabled' }
receive({
x: rect['x'],
y: rect['y'],
width: rect['width'],
height: rect['height'],
id: id,
actions: [{command: func['title']}]
})
end
end
Image.class_eval do
def self.from_xib_hsh(hsh)
rect = hsh['rect']
receive( id: hsh['id'], filename: hsh['image'], width: rect['width'], height: rect['height'] )
end
end
Remote.class_eval do
def self.from_xib_hsh(hsh)
view = hsh['document']['objects']['view']
rect = view['rect']
#
raw_image = view['subviews']['imageView']
raw_buttons = view['subviews']['button']
#
buttons = raw_buttons.map{|btn| Roomie::Button.from_xib_hsh(btn) }
image = Roomie::Image.from_xib_hsh(raw_image)
receive( id: view['id'], image: image, buttons: buttons, width: rect['width'], height: rect['height'] )
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment