hacky_hal 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|