crubyflie 0.1.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 (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