hybridgroup-crubyflie 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +5 -0
  5. data/LICENSE.txt +674 -0
  6. data/README.md +127 -0
  7. data/Rakefile +15 -0
  8. data/bin/crubyflie +94 -0
  9. data/configs/joystick_default.yaml +106 -0
  10. data/crubyflie.gemspec +50 -0
  11. data/examples/params_and_logging.rb +87 -0
  12. data/lib/crubyflie/crazyflie/commander.rb +54 -0
  13. data/lib/crubyflie/crazyflie/console.rb +67 -0
  14. data/lib/crubyflie/crazyflie/log.rb +383 -0
  15. data/lib/crubyflie/crazyflie/log_conf.rb +57 -0
  16. data/lib/crubyflie/crazyflie/param.rb +220 -0
  17. data/lib/crubyflie/crazyflie/toc.rb +239 -0
  18. data/lib/crubyflie/crazyflie/toc_cache.rb +87 -0
  19. data/lib/crubyflie/crazyflie.rb +282 -0
  20. data/lib/crubyflie/crazyradio/crazyradio.rb +301 -0
  21. data/lib/crubyflie/crazyradio/radio_ack.rb +48 -0
  22. data/lib/crubyflie/crubyflie_logger.rb +74 -0
  23. data/lib/crubyflie/driver/crtp_packet.rb +146 -0
  24. data/lib/crubyflie/driver/radio_driver.rb +363 -0
  25. data/lib/crubyflie/exceptions.rb +36 -0
  26. data/lib/crubyflie/input/input_reader.rb +190 -0
  27. data/lib/crubyflie/input/joystick_input_reader.rb +328 -0
  28. data/lib/crubyflie/version.rb +22 -0
  29. data/lib/crubyflie.rb +36 -0
  30. data/spec/commander_spec.rb +67 -0
  31. data/spec/console_spec.rb +76 -0
  32. data/spec/crazyflie_spec.rb +176 -0
  33. data/spec/crazyradio_spec.rb +228 -0
  34. data/spec/crtp_packet_spec.rb +79 -0
  35. data/spec/crubyflie_logger_spec.rb +39 -0
  36. data/spec/crubyflie_spec.rb +21 -0
  37. data/spec/input_reader_spec.rb +136 -0
  38. data/spec/joystick_cfg.yaml +44 -0
  39. data/spec/joystick_input_reader_spec.rb +323 -0
  40. data/spec/log_spec.rb +266 -0
  41. data/spec/param_spec.rb +166 -0
  42. data/spec/radio_ack_spec.rb +43 -0
  43. data/spec/radio_driver_spec.rb +227 -0
  44. data/spec/spec_helper.rb +53 -0
  45. data/spec/toc_cache_spec.rb +87 -0
  46. data/spec/toc_spec.rb +187 -0
  47. data/tools/sdl-joystick-axis.rb +69 -0
  48. metadata +225 -0
@@ -0,0 +1,363 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) 2013 Hector Sanjuan
3
+
4
+ # This file is part of Crubyflie.
5
+
6
+ # Crubyflie is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+
11
+ # Crubyflie is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with Crubyflie. If not, see <http://www.gnu.org/licenses/>
18
+
19
+
20
+ require 'thread'
21
+
22
+ require 'exceptions'
23
+ require 'driver/crtp_packet'
24
+ require 'crazyradio/crazyradio'
25
+
26
+
27
+
28
+ module Crubyflie
29
+ # Small URI class since Ruby URI < 1.9.3 gives problems parsing
30
+ # Crazyflie URIs
31
+ class CrubyflieURI
32
+ attr_reader :scheme, :dongle, :channel, :rate
33
+ # Initialize an URI
34
+ # @param uri_str [String] the URI
35
+ def initialize(uri_str)
36
+ @uri_str = uri_str
37
+ @scheme, @dongle, @channel, @rate = split()
38
+ if @scheme.nil? || @dongle.nil? || @channel.nil? || @rate.nil? ||
39
+ @scheme != 'radio'
40
+ raise InvalidURIException.new('Bad URI')
41
+ end
42
+ end
43
+
44
+ # Return URI as string
45
+ # @return [String] a string representation of the URI
46
+ def to_s
47
+ @uri_str
48
+ end
49
+
50
+ # Quick, dirty uri split
51
+ def split
52
+ @uri_str.sub(':', '').sub('//','/').split('/')
53
+ end
54
+ private :split
55
+ end
56
+
57
+
58
+ # This layer takes care of connecting to the crazyradio and
59
+ # managing the incoming and outgoing queues. This is done
60
+ # by spawing a thread.
61
+ # It also provides the interface to scan for available crazyflies
62
+ # to which connect
63
+ class RadioDriver
64
+ # Currently used callbacks that can be passed to connect()
65
+ CALLBACKS = [:link_quality_cb,
66
+ :link_error_cb]
67
+ # Default size for the outgoing queue
68
+ OUT_QUEUE_MAX_SIZE = 50
69
+ # Default number of retries before disconnecting
70
+ RETRIES_BEFORE_DISCONNECT = 20
71
+
72
+ attr_reader :uri
73
+ attr_reader :retries_before_disconnect, :out_queue_max_size
74
+
75
+ # Initialize the driver. Creates new empty queues.
76
+ def initialize()
77
+ @uri = nil
78
+ @in_queue = Queue.new()
79
+ @out_queue = Queue.new()
80
+ Thread.abort_on_exception = true
81
+ @radio_thread = nil
82
+ @callbacks = {}
83
+ @crazyradio = nil
84
+ @out_queue_max_size = nil
85
+ @retries_before_disconnect = nil
86
+ @shutdown_thread = false
87
+ end
88
+
89
+ # Connect to a Crazyflie in the specified URI
90
+ # @param uri_s [String] a radio uri like radio://<dongle>/<ch>/<rate>
91
+ # @param callbacks [Hash] blocks to call (see CALLBACKS contant values)
92
+ # @param opts [Hash] options. Currently supported
93
+ # :retries_before_disconnect (defaults to 20) and
94
+ # :out_queue_max_size (defaults to 50)
95
+ # @raise [CallbackMissing] when a necessary callback is not provided
96
+ # (see CALLBACKS constant values)
97
+ # @raise [InvalidURIException] when the URI is not a valid radio URI
98
+ # @raise [OpenLink] when a link is already open
99
+ def connect(uri_s, callbacks={}, opts={})
100
+ # Check if apparently there is an open link
101
+
102
+ if @crazyradio
103
+ m = "Active link to #{@uri.to_s}. Disconnect first"
104
+ raise OpenLink.new(m)
105
+ end
106
+
107
+ # Parse URI to initialize Crazyradio
108
+ # @todo: better control input. It defaults to 0
109
+ @uri = CrubyflieURI.new(uri_s)
110
+ dongle_number = @uri.dongle.to_i
111
+ channel = @uri.channel.to_i
112
+ rate = @uri.rate
113
+
114
+ # @todo this should be taken care of in crazyradio
115
+ case rate
116
+ when "250K"
117
+ rate = CrazyradioConstants::DR_250KPS
118
+ when "1M"
119
+ rate = CrazyradioConstants::DR_1MPS
120
+ when "2M"
121
+ rate = CrazyradioConstants::DR_2MPS
122
+ else
123
+ raise InvalidURIException.new("Bad radio rate")
124
+ end
125
+
126
+ # Fill in the callbacks Hash
127
+ CALLBACKS.each do |cb|
128
+ if passed_cb = callbacks[cb]
129
+ @callbacks[cb] = passed_cb
130
+ else
131
+ raise CallbackMissing.new("Callback #{cb} mandatory")
132
+ end
133
+ end
134
+
135
+ @retries_before_disconnect = opts[:retries_before_disconnect] ||
136
+ RETRIES_BEFORE_DISCONNECT
137
+ @out_queue_max_size = opts[:out_queue_max_size] ||
138
+ OUT_QUEUE_MAX_SIZE
139
+
140
+ # Initialize Crazyradio and run thread
141
+ cradio_opts = {
142
+ :channel => channel,
143
+ :data_rate => rate
144
+ }
145
+ @crazyradio = Crazyradio.factory(cradio_opts)
146
+ start_radio_thread()
147
+
148
+ end
149
+
150
+ # Disconnects from the crazyradio
151
+ # @param force [TrueClass, FalseClass]. Kill the thread right away, or
152
+ # wait for out_queue to empty
153
+ def disconnect(force=nil)
154
+ kill_radio_thread(force)
155
+ @in_queue.clear()
156
+ @out_queue.clear()
157
+
158
+ return if !@crazyradio
159
+ @crazyradio.close()
160
+ @crazyradio = nil
161
+ end
162
+
163
+ # Place a packet in the outgoing queue
164
+ # When not connected it will do nothing
165
+ # @param packet [CRTPPacket] The packet to send
166
+ def send_packet(packet)
167
+ return if !@crazyradio
168
+ if (s = @out_queue.size) >= @out_queue_max_size
169
+ m = "Reached #{s} elements in outgoing queue"
170
+ @callbacks[:link_error_cb].call(m)
171
+ disconnect()
172
+ end
173
+
174
+ @out_queue << packet if !@shutdown_thread
175
+ end
176
+
177
+ # Fetch a packet from the incoming queue
178
+ # @return [CRTPPacket,nil] a packet from the queue,
179
+ # or nil when there is none
180
+ def receive_packet(non_block=true)
181
+ begin
182
+ return @in_queue.pop(non_block)
183
+ rescue ThreadError
184
+ return nil
185
+ end
186
+ end
187
+
188
+ # List available Crazyflies in the provided channels
189
+ # @param start [Integer] channel to start
190
+ # @param stop [Intenger] channel to stop
191
+ # @return [Array] list of channels where a Crazyflie was found
192
+ def scan_radio_channels(start = 0, stop = 125)
193
+ return @crazyradio.scan_channels(start, stop)
194
+ end
195
+ private :scan_radio_channels
196
+
197
+ # List available Crazyflies
198
+ # @return [Array] List of radio URIs where a crazyflie was found
199
+ # @raise [OpenLink] if the Crazyradio is connected already
200
+ def scan_interface
201
+ raise OpenLink.new("Cannot scan when link is open") if @crazyradio
202
+ begin
203
+ @crazyradio = Crazyradio.factory()
204
+ results = {}
205
+ @crazyradio[:arc] = 1
206
+ @crazyradio[:data_rate] = Crazyradio::DR_250KPS
207
+ results["250K"] = scan_radio_channels()
208
+ @crazyradio[:data_rate] = Crazyradio::DR_1MPS
209
+ results["1M"] = scan_radio_channels()
210
+ @crazyradio[:data_rate] = Crazyradio::DR_2MPS
211
+ results["2M"] = scan_radio_channels()
212
+
213
+ uris = []
214
+ results.each do |rate, channels|
215
+ channels.each do |ch|
216
+ uris << "radio://0/#{ch}/#{rate}"
217
+ end
218
+ end
219
+ return uris
220
+ rescue USBDongleException
221
+ raise
222
+ rescue Exception
223
+ retries ||= 0
224
+ logger.error("Unknown error scanning interface: #{$!}")
225
+ @crazyradio.reopen()
226
+ retries += 1
227
+ if retries < 2
228
+ logger.error("Retrying")
229
+ sleep 0.5
230
+ retry
231
+ end
232
+ return []
233
+ ensure
234
+ @crazyradio.close() if @crazyradio
235
+ @crazyradio = nil
236
+ end
237
+ end
238
+
239
+
240
+ # Get status from the crazyradio. @see Crazyradio#status
241
+ def get_status
242
+ return Crazyradio.status()
243
+ end
244
+
245
+
246
+ # Privates
247
+ # The body of the communication thread
248
+ # Sends packets and tries to read the ACK
249
+ # @todo it is long and ugly
250
+ # @todo why the heck do we care here if we need to wait? Should the
251
+ # crazyradio do the waiting?
252
+ def start_radio_thread
253
+ @radio_thread = Thread.new do
254
+ Thread.current.priority = 5
255
+ out_data = [0xFF]
256
+ retries = @retries_before_disconnect
257
+ should_sleep = 0
258
+ error = "Unknown"
259
+ while true do
260
+ begin
261
+ ack = @crazyradio.send_packet(out_data)
262
+ # possible outcomes
263
+ # -exception - no usb dongle?
264
+ # -nil - bad comm
265
+ # -AckStatus class
266
+ rescue Exception
267
+ error = "Error talking to Crazyradio: #{$!.to_s}"
268
+ break
269
+ end
270
+
271
+ if ack.nil?
272
+ error = "Dongle communication error (ack is nil)"
273
+ break
274
+ end
275
+
276
+ # Set this in function of the retries
277
+ quality = (10 - ack.retry_count) * 10
278
+ @callbacks[:link_quality_cb].call(quality)
279
+
280
+ # Retry if we have not reached the limit
281
+ if !ack.ack
282
+ retries -= 1
283
+ next if retries > 0
284
+ error = "Too many packets lost"
285
+ break
286
+ else
287
+ retries = @retries_before_disconnect
288
+ end
289
+
290
+ # If there is data we queue it in incoming
291
+ # Otherwise we increase should_sleep
292
+ # If there is no data for more than 10 times
293
+ # we will sleep 0.01s when our outgoing queue
294
+ # is empty. Otherwise, we just send what we have
295
+ # of the 0xFF packet
296
+ data = ack.data
297
+ if data.length > 0
298
+ @in_queue << CRTPPacket.unpack(data)
299
+ should_sleep = 0
300
+ else
301
+ should_sleep += 1
302
+ end
303
+
304
+ break if @shutdown_thread && @out_queue.empty?()
305
+
306
+ begin
307
+ out_packet = @out_queue.pop(true) # non-block
308
+ should_sleep += 1
309
+ rescue ThreadError
310
+ out_packet = CRTPPacket.new(0xFF)
311
+ sleep 0.01 if should_sleep >= 10
312
+ end
313
+
314
+ out_data = out_packet.pack
315
+ end
316
+ if !@shutdown_thread
317
+ # If we reach here it means we are dying because of
318
+ # an error. The callback will likely call disconnect, which
319
+ # tries to kills us, but cannot because we are running the
320
+ # callback. Therefore we set @radio_thread to nil and then
321
+ # run the callback.
322
+ @radio_thread = nil
323
+ @callbacks[:link_error_cb].call(error)
324
+ end
325
+ end
326
+ end
327
+ private :start_radio_thread
328
+
329
+ def kill_radio_thread(force=false)
330
+ if @radio_thread
331
+ if force
332
+ @radio_thread.kill()
333
+ else
334
+ @shutdown_thread = true
335
+ @radio_thread.join()
336
+ end
337
+ @radio_thread = nil
338
+ @shutdown_thread = false
339
+ end
340
+ end
341
+ private :kill_radio_thread
342
+
343
+
344
+
345
+ # def pause_radio_thread
346
+ # @radio_thread.stop if @radio_thread
347
+ # end
348
+ # private :pause_radio_thread
349
+
350
+
351
+ # def resume_radio_thread
352
+ # @radio_thread.run if @radio_thread
353
+ # end
354
+ # private :resume_radio_thread
355
+
356
+
357
+ # def restart_radio_thread
358
+ # kill_radio_thread()
359
+ # start_radio_thread()
360
+ # end
361
+ # private :restart_radio_thread
362
+ end
363
+ end
@@ -0,0 +1,36 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) 2013 Hector Sanjuan
3
+
4
+ # This file is part of Crubyflie.
5
+
6
+ # Crubyflie is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+
11
+ # Crubyflie is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with Crubyflie. If not, see <http://www.gnu.org/licenses/>
18
+
19
+ module Crubyflie
20
+ # Raised when the radio URI is invalid
21
+ class InvalidURIException < Exception; end
22
+ # Raised when an radio link is already open
23
+ class OpenLink < Exception; end
24
+ # Raised when a radio driver callback parameter is missing
25
+ class CallbackMissing < Exception; end
26
+ # Raised when no USB dongle can be found
27
+ class NoDongleFound < Exception; end
28
+ # Raised when a problem occurs with the USB dongle
29
+ class USBDongleException < Exception; end
30
+ # Raised when a problem happens in the radio driver communications thread
31
+ class RadioThreadException < Exception; end
32
+ # Expected a package but it took to long to get it
33
+ class WaitTimeoutException < Exception; end
34
+ # Raised when there is a problem initializing a joystick
35
+ class JoystickException < Exception; end
36
+ end
@@ -0,0 +1,190 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) 2013 Hector Sanjuan
3
+
4
+ # This file is part of Crubyflie.
5
+
6
+ # Crubyflie is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+
11
+ # Crubyflie is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with Crubyflie. If not, see <http://www.gnu.org/licenses/>
18
+
19
+ module Crubyflie
20
+
21
+ # This class provides functionality basic to all controllers.
22
+ # Specific controller classes inherit from here.
23
+ #
24
+ # To read an input we must declare axis and buttons.
25
+ # The axis are analog float readings (range decided by the
26
+ # controller) while the buttons are integer where <= 0 means not pressed
27
+ # and > 0 means pressed.
28
+ #
29
+ # The reading of the values is implemented by children classes.
30
+ #
31
+ # The InputReader will also apply the #INPUT_ACTIONS to a given
32
+ # Crazyflie. In order to do that it will go through all the
33
+ # read values and perform actions associated to them, like sending
34
+ # a setpoint, shutting down the connection or altering the calibration.
35
+ class InputReader
36
+
37
+ # List of current recognized actions that controllers can declare
38
+ INPUT_ACTIONS = [:roll, :pitch, :yaw, :thrust,
39
+ :roll_inc_cal, :roll_dec_cal,
40
+ :pitch_inc_cal, :pitch_dec_cal,
41
+ :switch_scaled_output_mode,
42
+ :switch_xmode, :close_link]
43
+
44
+ attr_reader :axis, :buttons, :axis_readings, :button_readings
45
+ attr_accessor :xmode
46
+ # An input is composed by several necessary axis and buttons.
47
+ # @param axis [Hash] A hash of keys identifying axis IDs
48
+ # (the controller should know to what the
49
+ # ID maps), and values from #INPUT_ACTIONS
50
+ # @param buttons [Hash] A hash of keys identifying button IDs (the
51
+ # controller should know to what the ID maps),
52
+ # and values from #INPUT_ACTIONS
53
+ def initialize(axis, buttons)
54
+ @axis = axis
55
+ @buttons = buttons
56
+ @calibrations = {}
57
+ @xmode = false
58
+ @output_scale = 0 # off
59
+
60
+ # Calibrate defaults to 0
61
+ INPUT_ACTIONS.each do |action|
62
+ @calibrations[action] = 0
63
+ end
64
+
65
+ @axis_readings = {}
66
+ @button_readings = {}
67
+ end
68
+
69
+ # Read inputs will call read_axis() on all the declared axis
70
+ # and read_button() on all the declared buttons.
71
+ # After obtaining the reading, it will apply calibrations to
72
+ # the result. Apply the read values with #apply_input
73
+ def read_input
74
+ poll() # In case we need to poll the device
75
+ actions_to_axis = @axis.invert()
76
+ actions_to_axis.each do |action, axis_id|
77
+ if !INPUT_ACTIONS.include?(action)
78
+ logger.error("Unknown action #{action}. Skipping")
79
+ next
80
+ end
81
+ @axis_readings[action] = read_axis(axis_id)
82
+ @axis_readings[action] += @calibrations[action]
83
+ end
84
+
85
+ actions_to_buttons = @buttons.invert()
86
+ actions_to_buttons.each do |action, button_id|
87
+ if !INPUT_ACTIONS.include?(action)
88
+ logger.error("Unknown action #{action}. Skipping")
89
+ next
90
+ end
91
+ @button_readings[action] = read_button(button_id)
92
+ @button_readings[action] += @calibrations[action]
93
+ end
94
+ end
95
+
96
+ # This will act on current axis readings (by sendint a setpoint to
97
+ # the crazyflie) and on button readings (by, for example, shutting
98
+ # down the link or modifying the calibrations).
99
+ # If the link to the crazyflie is down, it will not send anything.
100
+ # @param crazyflie [Crazyflie] A crazyflie instance to send the
101
+ # setpoint to.
102
+ def apply_input(crazyflie)
103
+ return if !crazyflie.active?
104
+ setpoint = {
105
+ :roll => nil,
106
+ :pitch => nil,
107
+ :yaw => nil,
108
+ :thrust => nil
109
+ }
110
+
111
+ @button_readings.each do |action, value|
112
+ case action
113
+ when :roll
114
+ setpoint[:roll] = value
115
+ when :pitch
116
+ setpoint[:pitch] = value
117
+ when :yaw
118
+ setpoint[:yaw] = value
119
+ when :thrust
120
+ setpoint[:thrust] = value
121
+ when :roll_inc_cal
122
+ @calibrations[:roll] += 1
123
+ when :roll_dec_cal
124
+ @calibrations[:roll] -= 1
125
+ when :pitch_inc_cal
126
+ @calibrations[:pitch] += 1
127
+ when :pitch_dec_cal
128
+ @calibrations[:pitch] -= 1
129
+ when :switch_xmode
130
+ @xmode = !@xmode if value > 0
131
+ logger.info("Xmode is #{@xmode}") if value > 0
132
+ when :switch_scaled_output_mode
133
+ if value > 0 && @output_scale == 0
134
+ logger.info("Scaling output: x#{value}")
135
+ @output_scale = value.to_f
136
+ elsif value > 0 && @output_scale > 0
137
+ logger.info("Scaling output disabled")
138
+ @output_scale = 0
139
+ end
140
+ when :close_link
141
+ crazyflie.close_link() if value > 0
142
+ end
143
+ end
144
+
145
+ return if !crazyflie.active?
146
+
147
+ @axis_readings.each do |action, value|
148
+ case action
149
+ when :roll
150
+ setpoint[:roll] = value
151
+ when :pitch
152
+ setpoint[:pitch] = value
153
+ when :yaw
154
+ setpoint[:yaw] = value
155
+ when :thrust
156
+ setpoint[:thrust] = value
157
+ end
158
+ end
159
+
160
+ setpoint.keys().each do |k|
161
+ next if k == :thrust
162
+ setpoint[k] *= @output_scale
163
+ end if @output_scale > 0
164
+
165
+ pitch = setpoint[:pitch]
166
+ roll = setpoint[:roll]
167
+ yaw = setpoint[:yaw]
168
+ thrust = setpoint[:thrust]
169
+
170
+ if pitch && roll && yaw && thrust
171
+ m = "Sending R: #{roll} P: #{pitch} Y: #{yaw} T: #{thrust}"
172
+ logger.debug(m)
173
+ crazyflie.commander.send_setpoint(roll, pitch, yaw, thrust,
174
+ @xmode)
175
+ end
176
+ end
177
+
178
+ private
179
+ def read_axis(axis_id)
180
+ raise Exception.new("Not implemented!")
181
+ end
182
+
183
+ def read_button(button_id)
184
+ raise Exception.new("Not implemented!")
185
+ end
186
+
187
+ def poll
188
+ end
189
+ end
190
+ end