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.
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