crubyflie 0.1.0

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