hacky_hal 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile +12 -0
- data/LICENSE +20 -0
- data/README.markdown +57 -0
- data/hacky_hal.gemspec +20 -0
- data/lib/hacky_hal/device_controllers/base.rb +14 -0
- data/lib/hacky_hal/device_controllers/epson_projector.rb +180 -0
- data/lib/hacky_hal/device_controllers/generic_serial_port.rb +108 -0
- data/lib/hacky_hal/device_controllers/generic_ssh.rb +74 -0
- data/lib/hacky_hal/device_controllers/io_gear_avior_hdmi_switch.rb +58 -0
- data/lib/hacky_hal/device_controllers/linux_computer.rb +36 -0
- data/lib/hacky_hal/device_controllers/osx_computer.rb +19 -0
- data/lib/hacky_hal/device_controllers/roku.rb +121 -0
- data/lib/hacky_hal/device_controllers/yamaha_av_receiver.rb +202 -0
- data/lib/hacky_hal/device_resolvers/base.rb +9 -0
- data/lib/hacky_hal/device_resolvers/ssdp.rb +36 -0
- data/lib/hacky_hal/device_resolvers/static_uri.rb +17 -0
- data/lib/hacky_hal/log.rb +33 -0
- data/lib/hacky_hal/options.rb +21 -0
- data/lib/hacky_hal/registry.rb +22 -0
- data/lib/hacky_hal/util.rb +23 -0
- data/lib/hacky_hal.rb +17 -0
- data/spec/hacky_hal/device_controllers/base_spec.rb +16 -0
- data/spec/hacky_hal/device_controllers/generic_serial_port_spec.rb +167 -0
- data/spec/hacky_hal/device_controllers/generic_ssh_spec.rb +141 -0
- data/spec/hacky_hal/device_controllers/roku_spec.rb +30 -0
- data/spec/hacky_hal/device_controllers/yamaha_av_receiver_spec.rb +29 -0
- data/spec/hacky_hal/device_resolvers/base_spec.rb +8 -0
- data/spec/hacky_hal/device_resolvers/ssdp_spec.rb +49 -0
- data/spec/hacky_hal/device_resolvers/static_uri_spec.rb +16 -0
- data/spec/hacky_hal/log_spec.rb +26 -0
- data/spec/hacky_hal/options_spec.rb +22 -0
- data/spec/hacky_hal/registry_spec.rb +30 -0
- data/spec/hacky_hal/util_spec.rb +27 -0
- data/spec/spec_helper.rb +11 -0
- data/support/hal-9000-small.png +0 -0
- data/support/hal-9000.png +0 -0
- metadata +144 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "uri"
|
3
|
+
require "cgi"
|
4
|
+
require "rexml/document"
|
5
|
+
require_relative "base"
|
6
|
+
require_relative "../util"
|
7
|
+
|
8
|
+
# Docs: http://sdkdocs.roku.com/display/sdkdoc/External+Control+Guide
|
9
|
+
|
10
|
+
module HackyHAL
|
11
|
+
module DeviceControllers
|
12
|
+
class Roku < Base
|
13
|
+
KEY_HOME = "Home"
|
14
|
+
KEY_REVERSE = "Rev"
|
15
|
+
KEY_FORWARD = "Fwd"
|
16
|
+
KEY_PLAY = "Play"
|
17
|
+
KEY_SELECT = "Select"
|
18
|
+
KEY_LEFT = "Left"
|
19
|
+
KEY_RIGHT = "Right"
|
20
|
+
KEY_DOWN = "Down"
|
21
|
+
KEY_UP = "Up"
|
22
|
+
KEY_BACK = "Back"
|
23
|
+
KEY_INSTANT_REPLAY = "InstantReplay"
|
24
|
+
KEY_INFO = "Info"
|
25
|
+
KEY_BACKSPACE = "Backspace"
|
26
|
+
KEY_SEARCH = "Search"
|
27
|
+
KEY_ENTER = "Enter"
|
28
|
+
|
29
|
+
NAMED_KEYS = Set.new([
|
30
|
+
KEY_HOME, KEY_REVERSE, KEY_FORWARD, KEY_PLAY,
|
31
|
+
KEY_SELECT, KEY_LEFT, KEY_RIGHT, KEY_DOWN,
|
32
|
+
KEY_UP, KEY_BACK, KEY_INSTANT_REPLAY, KEY_INFO,
|
33
|
+
KEY_BACKSPACE, KEY_SEARCH, KEY_ENTER
|
34
|
+
])
|
35
|
+
|
36
|
+
attr_reader :host_uri
|
37
|
+
|
38
|
+
def initialize(options)
|
39
|
+
super(options)
|
40
|
+
ensure_option(:device_resolver)
|
41
|
+
|
42
|
+
resolver = Util.object_from_hash(options[:device_resolver], DeviceResolvers)
|
43
|
+
@host_uri = resolver.uri
|
44
|
+
|
45
|
+
log("Host found at: #{@host_uri.to_s}", :debug)
|
46
|
+
end
|
47
|
+
|
48
|
+
def channel_list
|
49
|
+
response = get_request("/query/apps")
|
50
|
+
response_document = REXML::Document.new(response.body)
|
51
|
+
|
52
|
+
response_document.elements.to_a("apps/app").map do |app|
|
53
|
+
id = app.attributes["id"]
|
54
|
+
version = app.attributes["version"]
|
55
|
+
name = app.text
|
56
|
+
|
57
|
+
{id: id, version: version, name: name}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def launch(channel_id, query_params = nil)
|
62
|
+
post_request("/launch/#{channel_id}", query_params)
|
63
|
+
end
|
64
|
+
|
65
|
+
def icon(channel_id)
|
66
|
+
response = get_request("/query/icon/#{channel_id}")
|
67
|
+
{type: response["content-type"], body: response.body}
|
68
|
+
end
|
69
|
+
|
70
|
+
def key_down(key)
|
71
|
+
post_request("/keydown/#{key_code(key)}")
|
72
|
+
end
|
73
|
+
|
74
|
+
def key_up(key)
|
75
|
+
post_request("/keyup/#{key_code(key)}")
|
76
|
+
end
|
77
|
+
|
78
|
+
def key_press(key)
|
79
|
+
post_request("/keypress/#{key_code(key)}")
|
80
|
+
end
|
81
|
+
|
82
|
+
def key_press_string(string)
|
83
|
+
string.chars.each do |char|
|
84
|
+
key_press(char)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def key_code(key)
|
91
|
+
if NAMED_KEYS.include?(key)
|
92
|
+
key
|
93
|
+
else
|
94
|
+
"Lit_#{CGI::escape(key)}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def get_request(path, query_params = nil)
|
99
|
+
http_request(Net::HTTP::Get, path, query_params)
|
100
|
+
end
|
101
|
+
|
102
|
+
def post_request(path, query_params = nil)
|
103
|
+
http_request(Net::HTTP::Post, path, query_params)
|
104
|
+
end
|
105
|
+
|
106
|
+
def request_uri(path, query_params)
|
107
|
+
uri = @host_uri.dup
|
108
|
+
uri.path = path
|
109
|
+
uri.query = URI.encode_www_form(query_params) if query_params
|
110
|
+
uri
|
111
|
+
end
|
112
|
+
|
113
|
+
def http_request(request_type, path, query_params)
|
114
|
+
uri = request_uri(path, query_params)
|
115
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
116
|
+
request = request_type.new(uri.request_uri)
|
117
|
+
http.request(request)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "rexml/document"
|
3
|
+
require_relative "base"
|
4
|
+
require_relative "../util"
|
5
|
+
|
6
|
+
module HackyHAL
|
7
|
+
module DeviceControllers
|
8
|
+
class YamahaAvReceiver < Base
|
9
|
+
CONTROL_PATH = "/YamahaRemoteControl/ctrl"
|
10
|
+
CONTROL_PORT = 80
|
11
|
+
|
12
|
+
attr_reader :host_uri
|
13
|
+
|
14
|
+
def initialize(options)
|
15
|
+
super(options)
|
16
|
+
ensure_option(:device_resolver)
|
17
|
+
|
18
|
+
resolver = Util.object_from_hash(options[:device_resolver], DeviceResolvers)
|
19
|
+
|
20
|
+
@host_uri = resolver.uri
|
21
|
+
@host_uri.path = CONTROL_PATH
|
22
|
+
@host_uri.port = CONTROL_PORT
|
23
|
+
|
24
|
+
log("Host found at: #{@host_uri.to_s}", :debug)
|
25
|
+
end
|
26
|
+
|
27
|
+
def basic_status
|
28
|
+
response = get_request('<Main_Zone><Basic_Status>GetParam</Basic_Status></Main_Zone>')
|
29
|
+
|
30
|
+
basic_settings = response.elements["YAMAHA_AV/Main_Zone/Basic_Status"]
|
31
|
+
power_settings = basic_settings.elements["Power_Control"]
|
32
|
+
volume_settings = basic_settings.elements["Volume"]
|
33
|
+
input_settings = basic_settings.elements["Input/Input_Sel_Item_Info"]
|
34
|
+
sound_video_settings = basic_settings.elements["Sound_Video"]
|
35
|
+
hdmi_settings = sound_video_settings.elements["HDMI"]
|
36
|
+
tone_settings = sound_video_settings.elements["Tone"]
|
37
|
+
dialog_adjust_settings = sound_video_settings.elements["Dialogue_Adjust"]
|
38
|
+
surround_settings = basic_settings.elements["Surround/Program_Sel/Current"]
|
39
|
+
|
40
|
+
hdmi_output_values = []
|
41
|
+
hdmi_settings.elements.each("Output") do |output_element|
|
42
|
+
if output_element.name =~ /^OUT_(\d)$/
|
43
|
+
hdmi_output_values << {
|
44
|
+
name: output_element.name,
|
45
|
+
enabled: element_on?(output_element)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
{
|
51
|
+
power: element_on?(power_settings.elements["Power"]),
|
52
|
+
sleep: element_on?(power_settings.elements["Sleep"]),
|
53
|
+
mute: element_on?(volume_settings.elements["Mute"]),
|
54
|
+
volume: element_volume_value(volume_settings.elements["Lvl/Val"]),
|
55
|
+
subwoofer_trim: element_volume_value(volume_settings.elements["Subwoofer_Trim/Val"]),
|
56
|
+
tone: {
|
57
|
+
base: element_volume_value(tone_settings.elements["Bass/Val"]),
|
58
|
+
treble: element_volume_value(tone_settings.elements["Treble/Val"])
|
59
|
+
},
|
60
|
+
input: {
|
61
|
+
name: input_settings.elements["Param"].text,
|
62
|
+
title: input_settings.elements["Title"].text
|
63
|
+
},
|
64
|
+
surround: {
|
65
|
+
straight: element_on?(surround_settings.elements["Straight"]),
|
66
|
+
enhancer: element_on?(surround_settings.elements["Enhancer"]),
|
67
|
+
sound_program: surround_settings.elements["Sound_Program"].text,
|
68
|
+
cinema_dsp_3d_mode: element_on?(basic_settings.elements["Surround/_3D_Cinema_DSP"])
|
69
|
+
},
|
70
|
+
hdmi: {
|
71
|
+
standby_through: element_on?(hdmi_settings.elements["Standby_Through_Info"]),
|
72
|
+
outputs: hdmi_output_values
|
73
|
+
},
|
74
|
+
party_mode: element_on?(basic_settings.elements["Party_Info"]),
|
75
|
+
pure_direct_mode: element_on?(sound_video_settings.elements["Pure_Direct/Mode"]),
|
76
|
+
adaptive_drc: element_on?(sound_video_settings.elements["Adaptive_DRC"]),
|
77
|
+
dialog_adjust: {
|
78
|
+
level: dialog_adjust_settings.elements["Dialogue_Lvl"].text.to_i,
|
79
|
+
lift: dialog_adjust_settings.elements["Dialogue_Lift"].text.to_i
|
80
|
+
}
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def inputs
|
85
|
+
response = get_request('<Main_Zone><Input><Input_Sel_Item>GetParam</Input_Sel_Item></Input></Main_Zone>')
|
86
|
+
|
87
|
+
inputs = []
|
88
|
+
response.elements["YAMAHA_AV/Main_Zone/Input/Input_Sel_Item"].each do |input_element|
|
89
|
+
if input_element.name =~ /^Item_\d+$/
|
90
|
+
inputs << {
|
91
|
+
name: input_element.elements["Param"].text,
|
92
|
+
title: input_element.elements["Title"].text,
|
93
|
+
source_name: input_element.elements["Src_Name"].text,
|
94
|
+
source_number: input_element.elements["Src_Number"].text.to_i,
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
inputs
|
100
|
+
end
|
101
|
+
|
102
|
+
def input
|
103
|
+
response = get_request('<Main_Zone><Input><Input_Sel>GetParam</Input_Sel></Input></Main_Zone>')
|
104
|
+
response.elements["YAMAHA_AV/Main_Zone/Input/Input_Sel"].text
|
105
|
+
end
|
106
|
+
|
107
|
+
def input=(input_name)
|
108
|
+
put_request(%|<Main_Zone><Input><Input_Sel>#{input_name}</Input_Sel></Input></Main_Zone>|)
|
109
|
+
end
|
110
|
+
|
111
|
+
def hdmi_output(output_name)
|
112
|
+
response = get_request(%|<System><Sound_Video><HDMI><Output><#{output_name}>GetParam</#{output_name}></Output></HDMI></Sound_Video></System>|)
|
113
|
+
element_on?(response.elements["YAMAHA_AV/System/Sound_Video/HDMI/Output/#{output_name}"])
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_hdmi_output(output_name, enabled)
|
117
|
+
value = enabled ? "On" : "Off"
|
118
|
+
put_request(%|<System><Sound_Video><HDMI><Output><#{output_name}>#{value}</#{output_name}></Output></HDMI></Sound_Video></System>|)
|
119
|
+
end
|
120
|
+
|
121
|
+
def on
|
122
|
+
response = get_request("<Main_Zone><Power_Control><Power>GetParam</Power></Power_Control></Main_Zone>")
|
123
|
+
response.elements["YAMAHA_AV/Main_Zone/Power_Control/Power"].text == "On"
|
124
|
+
end
|
125
|
+
|
126
|
+
def on=(value)
|
127
|
+
value = value ? "On" : "Standby"
|
128
|
+
put_request("<Main_Zone><Power_Control><Power>#{value}</Power></Power_Control></Main_Zone>")
|
129
|
+
end
|
130
|
+
|
131
|
+
def volume
|
132
|
+
response = get_request("<Main_Zone><Volume><Lvl>GetParam</Lvl></Volume></Main_Zone>")
|
133
|
+
element_volume_value(response.elements["YAMAHA_AV/Main_Zone/Volume/Lvl/Val"])
|
134
|
+
end
|
135
|
+
|
136
|
+
def volume=(value)
|
137
|
+
value = (value * 10.0).to_i.to_s
|
138
|
+
response = put_request("<Main_Zone><Volume><Lvl><Val>#{value}</Val><Exp>1</Exp><Unit>dB</Unit></Lvl></Volume></Main_Zone>")
|
139
|
+
end
|
140
|
+
|
141
|
+
def mute
|
142
|
+
response = get_request("<Main_Zone><Volume><Mute>GetParam</Mute></Volume></Main_Zone>")
|
143
|
+
element_on?(response.elements["YAMAHA_AV/Main_Zone/Volume/Mute"])
|
144
|
+
end
|
145
|
+
|
146
|
+
def mute=(value)
|
147
|
+
value = case value
|
148
|
+
when true then "On"
|
149
|
+
when false then "Off"
|
150
|
+
else value
|
151
|
+
end
|
152
|
+
|
153
|
+
put_request("<Main_Zone><Volume><Mute>#{value}</Mute></Volume></Main_Zone>")
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def request(body)
|
159
|
+
http = Net::HTTP.new(@host_uri.host, @host_uri.port)
|
160
|
+
|
161
|
+
request = Net::HTTP::Post.new(@host_uri.request_uri)
|
162
|
+
request["Content-Type"] = "text/xml"
|
163
|
+
request.body = body
|
164
|
+
|
165
|
+
response = http.request(request)
|
166
|
+
log("Response: #{response.body}", :debug)
|
167
|
+
|
168
|
+
response_xml = REXML::Document.new(response.body)
|
169
|
+
|
170
|
+
if response_xml.root.attributes["RC"] == "0"
|
171
|
+
response_xml
|
172
|
+
else
|
173
|
+
false
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def get_request(body)
|
178
|
+
command = %|<YAMAHA_AV cmd="GET">#{body}</YAMAHA_AV>|
|
179
|
+
log("GET Request: #{command.inspect}", :debug)
|
180
|
+
request(command)
|
181
|
+
end
|
182
|
+
|
183
|
+
def put_request(body)
|
184
|
+
command = %|<YAMAHA_AV cmd="PUT">#{body}</YAMAHA_AV>|
|
185
|
+
log("POST Request: #{command.inspect}", :debug)
|
186
|
+
request(command)
|
187
|
+
end
|
188
|
+
|
189
|
+
def element_on?(element)
|
190
|
+
case element.text
|
191
|
+
when "On" then true
|
192
|
+
when "Off" then false
|
193
|
+
else element.text
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def element_volume_value(element)
|
198
|
+
element.text.to_f / 10.0
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
require "uri"
|
3
|
+
require "upnp/ssdp"
|
4
|
+
|
5
|
+
module HackyHAL
|
6
|
+
module DeviceResolvers
|
7
|
+
class SsdpUnresolvedDevice < Exception; end
|
8
|
+
|
9
|
+
DEFAULT_OPTIONS = {
|
10
|
+
search: "upnp:rootdevice"
|
11
|
+
}
|
12
|
+
|
13
|
+
class SSDP < Base
|
14
|
+
def initialize(options)
|
15
|
+
super(DEFAULT_OPTIONS.merge(options))
|
16
|
+
ensure_option(:usn)
|
17
|
+
end
|
18
|
+
|
19
|
+
def uri
|
20
|
+
@uri ||= (
|
21
|
+
old_upnp_log_value = UPnP.log?
|
22
|
+
UPnP.log = false
|
23
|
+
|
24
|
+
device = UPnP::SSDP.search(options[:search]).find do |device|
|
25
|
+
device[:usn] == options[:usn]
|
26
|
+
end
|
27
|
+
|
28
|
+
UPnP.log = old_upnp_log_value
|
29
|
+
|
30
|
+
raise SsdpUnresolvedDevice unless device
|
31
|
+
URI.parse(device[:location])
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
module HackyHAL
|
5
|
+
module DeviceResolvers
|
6
|
+
class StaticURI < Base
|
7
|
+
attr_reader :uri
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
super(options)
|
11
|
+
ensure_option(:uri)
|
12
|
+
|
13
|
+
@uri = URI.parse(options[:uri])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "singleton"
|
3
|
+
|
4
|
+
module HackyHAL
|
5
|
+
class Log < Logger
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
attr_accessor :enabled
|
9
|
+
|
10
|
+
class Formatter
|
11
|
+
def call(severity, time, progname, message)
|
12
|
+
time = time.strftime("%d/%b/%Y %H:%M:%S")
|
13
|
+
"[#{time}] #{severity}: #{message}\n"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
super($stdout)
|
19
|
+
self.enabled = true
|
20
|
+
self.formatter = Formatter.new
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method :add_without_enabled_switch, :add
|
24
|
+
def add(severity, message = nil, progname = nil, &block)
|
25
|
+
add_without_enabled_switch(severity, message, progname, &block) if enabled
|
26
|
+
end
|
27
|
+
|
28
|
+
# for compatibility with Rack logger
|
29
|
+
def write(string)
|
30
|
+
self << string
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module HackyHAL
|
2
|
+
module Options
|
3
|
+
attr_reader :options
|
4
|
+
|
5
|
+
def initialize(options)
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](value)
|
10
|
+
@options[value]
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def ensure_option(option_name)
|
16
|
+
unless options[option_name]
|
17
|
+
raise ArgumentError, "#{self.class.name} must set #{option_name} option."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "singleton"
|
2
|
+
require_relative "util"
|
3
|
+
|
4
|
+
module HackyHAL
|
5
|
+
class Registry
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
attr_reader :devices
|
9
|
+
|
10
|
+
def load_yaml_file(path)
|
11
|
+
devices = YAML.load(File.read(File.expand_path(path)))
|
12
|
+
devices = Util.symbolize_keys_deep(devices)
|
13
|
+
|
14
|
+
@devices = {}
|
15
|
+
devices.each do |name, config|
|
16
|
+
config = config.dup
|
17
|
+
config[:name] = name
|
18
|
+
@devices[name] = Util.object_from_hash(config, HackyHAL::DeviceControllers)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module HackyHAL
|
2
|
+
module Util
|
3
|
+
def self.object_from_hash(hash, context_module)
|
4
|
+
hash = hash.dup
|
5
|
+
type = hash.delete(:type)
|
6
|
+
|
7
|
+
unless type
|
8
|
+
raise ArgumentError, "Must specify type to build object from hash. Given: #{hash}"
|
9
|
+
end
|
10
|
+
|
11
|
+
context_module.const_get(type).new(hash)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.symbolize_keys_deep(h)
|
15
|
+
h.keys.each do |k|
|
16
|
+
ks = k.respond_to?(:to_sym) ? k.to_sym : k
|
17
|
+
h[ks] = h.delete(k)
|
18
|
+
symbolize_keys_deep(h[ks]) if h[ks].kind_of?(Hash)
|
19
|
+
end
|
20
|
+
h
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/hacky_hal.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative "hacky_hal/registry"
|
2
|
+
require_relative "hacky_hal/log"
|
3
|
+
|
4
|
+
module HackyHAL
|
5
|
+
DEVICE_CONTROLLERS_DIR = "hacky_hal/device_controllers"
|
6
|
+
DEVICE_RESOLVERS_DIR = "hacky_hal/device_resolvers"
|
7
|
+
|
8
|
+
controllers_dir = File.expand_path(DEVICE_CONTROLLERS_DIR, File.dirname(__FILE__))
|
9
|
+
Dir["#{controllers_dir}/*"].each do |file|
|
10
|
+
require_relative file
|
11
|
+
end
|
12
|
+
|
13
|
+
resolvers_dir = File.expand_path(DEVICE_RESOLVERS_DIR, File.dirname(__FILE__))
|
14
|
+
Dir["#{resolvers_dir}/*"].each do |file|
|
15
|
+
require_relative file
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "hacky_hal/device_controllers/base"
|
3
|
+
|
4
|
+
describe HackyHAL::DeviceControllers::Base do
|
5
|
+
it "should include Options" do
|
6
|
+
described_class.ancestors.should include(HackyHAL::Options)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "#log" do
|
10
|
+
it "should log to HackyHAL::Log with device name" do
|
11
|
+
controller = described_class.new(name: "dummy device")
|
12
|
+
HackyHAL::Log.instance.should_receive(:info).with("dummy device: dummy message")
|
13
|
+
controller.log("dummy message", :info)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "hacky_hal/device_controllers/generic_serial_port"
|
3
|
+
|
4
|
+
describe HackyHAL::DeviceControllers::GenericSerialPort do
|
5
|
+
before(:each) do
|
6
|
+
@serial_port = double("serial port")
|
7
|
+
@serial_port.stub(:read_timeout=)
|
8
|
+
@serial_port.stub(:baud=)
|
9
|
+
@serial_port.stub(:data_bits=)
|
10
|
+
@serial_port.stub(:stop_bits=)
|
11
|
+
@serial_port.stub(:parity=)
|
12
|
+
@serial_port.stub(:flow_control=)
|
13
|
+
|
14
|
+
@serial_device_path = "/dev/foo0"
|
15
|
+
@generic_serial_port = described_class.new(serial_device_path: @serial_device_path)
|
16
|
+
|
17
|
+
SerialPort.stub(:new).and_return(@serial_port)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#write_command" do
|
21
|
+
before(:each) do
|
22
|
+
@serial_port.stub(:flush)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should write to the serial port" do
|
26
|
+
@serial_port.should_receive(:write).with("foobar\r\n")
|
27
|
+
@generic_serial_port.write_command("foobar")
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should return true on successful run" do
|
31
|
+
@serial_port.should_receive(:write)
|
32
|
+
@generic_serial_port.write_command("foobar").should be_true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should retry after rescuing from an Errno::EIO" do
|
36
|
+
@generic_serial_port.should_receive(:disconnect)
|
37
|
+
|
38
|
+
retries = 0
|
39
|
+
@serial_port.stub(:write) do
|
40
|
+
if retries < HackyHAL::DeviceControllers::GenericSerialPort::MAX_READ_WRITE_RETRIES
|
41
|
+
retries += 1
|
42
|
+
raise Errno::EIO
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@generic_serial_port.write_command("foobar").should be_true
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should raise Errno::EIO if retry failed" do
|
50
|
+
@serial_port.stub(:write) { raise Errno::EIO }
|
51
|
+
@serial_port.stub(:close)
|
52
|
+
|
53
|
+
expect {
|
54
|
+
@generic_serial_port.write_command("foobar")
|
55
|
+
}.to raise_error(Errno::EIO)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should log debug message" do
|
59
|
+
@serial_port.stub(:write)
|
60
|
+
@generic_serial_port.should_receive(:log).with("Wrote: \"foobar\\r\\n\"", :debug)
|
61
|
+
@generic_serial_port.write_command("foobar")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#read_command" do
|
66
|
+
it "should return read string on successful run" do
|
67
|
+
@serial_port.stub(:readline).and_return("a return string")
|
68
|
+
@generic_serial_port.read_command.should == "a return string"
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should set the serial port read timeout to given value" do
|
72
|
+
@serial_port.stub(:readline)
|
73
|
+
@serial_port.should_receive(:read_timeout=).with(12000)
|
74
|
+
@generic_serial_port.read_command(12)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should call handle_error if error_response?(response) is true" do
|
78
|
+
@serial_port.stub(:readline)
|
79
|
+
@generic_serial_port.stub(:error_response?).and_return(true)
|
80
|
+
@generic_serial_port.should_receive(:handle_error)
|
81
|
+
@generic_serial_port.read_command
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should rescue EOFError and return nil" do
|
85
|
+
@serial_port.stub(:readline) { raise EOFError }
|
86
|
+
@generic_serial_port.read_command.should be_nil
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should rescue EOFError and log warn message" do
|
90
|
+
@serial_port.stub(:readline) { raise EOFError }
|
91
|
+
@generic_serial_port.should_receive(:log).with("Read EOFError", :warn)
|
92
|
+
@generic_serial_port.read_command
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should retry after rescuing from an Errno::EIO" do
|
96
|
+
@generic_serial_port.should_receive(:disconnect)
|
97
|
+
|
98
|
+
retries = 0
|
99
|
+
@serial_port.stub(:readline) do
|
100
|
+
if retries < HackyHAL::DeviceControllers::GenericSerialPort::MAX_READ_WRITE_RETRIES
|
101
|
+
retries += 1
|
102
|
+
raise Errno::EIO
|
103
|
+
else
|
104
|
+
"a return string"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
@generic_serial_port.read_command.should == "a return string"
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should raise Errno::EIO if retry failed" do
|
112
|
+
@serial_port.stub(:readline) { raise Errno::EIO }
|
113
|
+
@serial_port.stub(:close)
|
114
|
+
|
115
|
+
expect {
|
116
|
+
@generic_serial_port.read_command
|
117
|
+
}.to raise_error(Errno::EIO)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should log debug message" do
|
121
|
+
@serial_port.stub(:readline).and_return("response")
|
122
|
+
@generic_serial_port.should_receive(:log).with("Read: \"response\"", :debug)
|
123
|
+
@generic_serial_port.read_command
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "#disconnect" do
|
128
|
+
it "should close serial port if serial port is set" do
|
129
|
+
@generic_serial_port.serial_port # make sure serial port is set
|
130
|
+
@serial_port.should_receive(:close)
|
131
|
+
@generic_serial_port.disconnect
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should not attempt to close serial port if serial port is not set" do
|
135
|
+
@generic_serial_port.stub(:serial_port).and_return(nil)
|
136
|
+
@generic_serial_port.disconnect
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should set serial_port to nil" do
|
140
|
+
@generic_serial_port.serial_port # make sure serial port is set
|
141
|
+
@serial_port.should_receive(:close)
|
142
|
+
@generic_serial_port.disconnect
|
143
|
+
SerialPort.should_receive(:new)
|
144
|
+
@generic_serial_port.serial_port
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "#serial_port" do
|
149
|
+
it "should create new SerialPort" do
|
150
|
+
SerialPort.should_receive(:new).with(@serial_device_path).and_return(@serial_port)
|
151
|
+
@generic_serial_port.stub(:serial_options).and_return(
|
152
|
+
baud_rate: 50,
|
153
|
+
data_bits: 5,
|
154
|
+
stop_bits: 2,
|
155
|
+
parity: SerialPort::EVEN,
|
156
|
+
flow_control: SerialPort::HARD
|
157
|
+
)
|
158
|
+
@serial_port.should_receive(:baud=).with(50)
|
159
|
+
@serial_port.should_receive(:data_bits=).with(5)
|
160
|
+
@serial_port.should_receive(:stop_bits=).with(2)
|
161
|
+
@serial_port.should_receive(:parity=).with(SerialPort::EVEN)
|
162
|
+
@serial_port.should_receive(:flow_control=).with(SerialPort::HARD)
|
163
|
+
|
164
|
+
@generic_serial_port.serial_port.should == @serial_port
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|