hacky_hal 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/.gitignore +3 -0
  2. data/Gemfile +12 -0
  3. data/LICENSE +20 -0
  4. data/README.markdown +57 -0
  5. data/hacky_hal.gemspec +20 -0
  6. data/lib/hacky_hal/device_controllers/base.rb +14 -0
  7. data/lib/hacky_hal/device_controllers/epson_projector.rb +180 -0
  8. data/lib/hacky_hal/device_controllers/generic_serial_port.rb +108 -0
  9. data/lib/hacky_hal/device_controllers/generic_ssh.rb +74 -0
  10. data/lib/hacky_hal/device_controllers/io_gear_avior_hdmi_switch.rb +58 -0
  11. data/lib/hacky_hal/device_controllers/linux_computer.rb +36 -0
  12. data/lib/hacky_hal/device_controllers/osx_computer.rb +19 -0
  13. data/lib/hacky_hal/device_controllers/roku.rb +121 -0
  14. data/lib/hacky_hal/device_controllers/yamaha_av_receiver.rb +202 -0
  15. data/lib/hacky_hal/device_resolvers/base.rb +9 -0
  16. data/lib/hacky_hal/device_resolvers/ssdp.rb +36 -0
  17. data/lib/hacky_hal/device_resolvers/static_uri.rb +17 -0
  18. data/lib/hacky_hal/log.rb +33 -0
  19. data/lib/hacky_hal/options.rb +21 -0
  20. data/lib/hacky_hal/registry.rb +22 -0
  21. data/lib/hacky_hal/util.rb +23 -0
  22. data/lib/hacky_hal.rb +17 -0
  23. data/spec/hacky_hal/device_controllers/base_spec.rb +16 -0
  24. data/spec/hacky_hal/device_controllers/generic_serial_port_spec.rb +167 -0
  25. data/spec/hacky_hal/device_controllers/generic_ssh_spec.rb +141 -0
  26. data/spec/hacky_hal/device_controllers/roku_spec.rb +30 -0
  27. data/spec/hacky_hal/device_controllers/yamaha_av_receiver_spec.rb +29 -0
  28. data/spec/hacky_hal/device_resolvers/base_spec.rb +8 -0
  29. data/spec/hacky_hal/device_resolvers/ssdp_spec.rb +49 -0
  30. data/spec/hacky_hal/device_resolvers/static_uri_spec.rb +16 -0
  31. data/spec/hacky_hal/log_spec.rb +26 -0
  32. data/spec/hacky_hal/options_spec.rb +22 -0
  33. data/spec/hacky_hal/registry_spec.rb +30 -0
  34. data/spec/hacky_hal/util_spec.rb +27 -0
  35. data/spec/spec_helper.rb +11 -0
  36. data/support/hal-9000-small.png +0 -0
  37. data/support/hal-9000.png +0 -0
  38. 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,9 @@
1
+ require_relative "../options"
2
+
3
+ module HackyHAL
4
+ module DeviceResolvers
5
+ class Base
6
+ include Options
7
+ end
8
+ end
9
+ 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