phidgets_native 0.2.1 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZTJhOGE5ZGUyNDM2ZjExODdjMjVjMWRhOTU0YjZjMjdhZjRlNWRiNw==
5
+ data.tar.gz: !binary |-
6
+ MDMwMDllMGFlMzQ2NzhjZTdlOWJlNzgxZWFkOTQxYzFmNTczMzhlYQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZjAzYTVkYTIyNzRjMTg3NjUyOTQyOGQwMTFmM2Y4MzZlM2I4MDcwMDJlOTU0
10
+ NTlmMTNhODM2Y2QzOWViMzYxYzA0MTI1OWRjMGU2YzFmYWRjYjhmYzBmZmI4
11
+ MTZhN2RlYmEzZTNkZGU3NGIxODQ1MjQ2ZTJlYzVmOGEwNjljYjA=
12
+ data.tar.gz: !binary |-
13
+ OGNlOGE5MTk0MDMxZWY1ZTM3Mzg2MzU5MWI5ZDEzNTVmYWEzM2E3YjJmNDY2
14
+ MDZjMTJhZDM1ODljMzFiZmQzYWEwOGVmYWJkZWE1MGI3Mzk0ZWEwNTIxMjA4
15
+ YTk5ODdjMDFhMWMyZWFkNWUzOWJkZDEwZDIwYjRiNTJmNzU2MzA=
data/Gemfile CHANGED
@@ -1,3 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gem 'rake'
4
+ gem 'rdoc'
3
5
  gem 'rake-compiler'
6
+ gem 'rspec'
data/Gemfile.lock CHANGED
@@ -1,12 +1,27 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
- rake (10.1.0)
5
- rake-compiler (0.8.3)
4
+ diff-lcs (1.2.5)
5
+ json (1.8.1)
6
+ rake (10.1.1)
7
+ rake-compiler (0.9.2)
6
8
  rake
9
+ rdoc (4.1.1)
10
+ json (~> 1.4)
11
+ rspec (2.14.1)
12
+ rspec-core (~> 2.14.0)
13
+ rspec-expectations (~> 2.14.0)
14
+ rspec-mocks (~> 2.14.0)
15
+ rspec-core (2.14.8)
16
+ rspec-expectations (2.14.5)
17
+ diff-lcs (>= 1.1.3, < 2.0)
18
+ rspec-mocks (2.14.6)
7
19
 
8
20
  PLATFORMS
9
21
  ruby
10
22
 
11
23
  DEPENDENCIES
24
+ rake
12
25
  rake-compiler
26
+ rdoc
27
+ rspec
data/README.rdoc CHANGED
@@ -175,8 +175,16 @@ Note that you can also use the log method to send your own messages to the logge
175
175
  if you think you have something you need to add.
176
176
 
177
177
  =Changelog
178
+ == Version 0.2.5
179
+ - Added orientation tracking code to the Spatial sensor via the Madgwick AHRS library
180
+ - Fixed compile issues on ARM
181
+
178
182
  == Version 0.2.1
179
183
  - Added dual-ratiometric mode features and interfaces to the InterfaceKit
180
184
 
181
185
  == Version 0.2.0
182
- - First gem release, supporting all api functions of the Spatial, GPS, InterfaceKit, & AdvancedServo phidgets
186
+ - Added support for all api functions of the AdvancedServo Phidget
187
+ - Added support for all api functions of the InterfaceKit Phidget
188
+
189
+ == Version 0.1.0
190
+ - First gem release, supporting all api functions of the Spatial & GPS Phidgets
data/Rakefile CHANGED
@@ -1,17 +1,14 @@
1
1
  # File: Rakefile
2
2
 
3
+ require 'rake'
3
4
  require 'rake/extensiontask'
4
5
  require 'rake/packagetask'
5
6
  require 'rdoc/task'
7
+ require 'rspec/core/rake_task'
6
8
 
7
9
  spec = Gem::Specification.load('phidgets_native.gemspec')
8
-
9
10
  Rake::ExtensionTask.new 'phidgets_native', spec
10
-
11
- Gem::PackageTask.new(spec) do |pkg|
12
-
13
-
14
- end
11
+ Gem::PackageTask.new spec
15
12
 
16
13
  RDOC_FILES = FileList["README.rdoc", "ext/phidgets_native/phidgets_native_ruby.c",
17
14
  "ext/phidgets_native/*_ruby.c"]
@@ -29,3 +26,7 @@ Rake::RDocTask.new(:ri) do |rd|
29
26
  rd.rdoc_files.include(RDOC_FILES)
30
27
  end
31
28
 
29
+ RSpec::Core::RakeTask.new('spec')
30
+
31
+ task :spec => :compile
32
+
data/examples/spatial.rb CHANGED
@@ -41,11 +41,12 @@ phidgets_example_for( PhidgetsNative::Spatial, additional_attribs ) do |spatial|
41
41
  'Sample Rate',
42
42
  '%-40s' % 'Accelerometer',
43
43
  '%-40s' % 'Gyroscope',
44
- '%-40s' % 'Compass'
44
+ '%-40s' % 'Compass',
45
+ '%-40s' % 'OrientationQ',
45
46
  ]).output(:header => (i == 0), :separator => false) do |columns|
46
47
  i+=1
47
48
  [ ['%d Hz' % spatial.sample_rate]+[
48
- spatial.accelerometer, spatial.gyro, spatial.compass ].collect{ |axes|
49
+ spatial.accelerometer, spatial.gyro, spatial.compass, spatial.orientation_to_quaternion ].collect{ |axes|
49
50
  (axes.kind_of? Array) ?
50
51
  '[%s]' % axes.collect{|a| '%0.6f' % a}.join(', ') : axes.inspect
51
52
  } ]
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require '%s/lib/common' % File.dirname(__FILE__)
5
+
6
+ additional_attribs = [
7
+ %w(Motor\ Count motor_count),
8
+ %w(Acceleration\ Max acceleration_max),
9
+ %w(Acceleration\ Min acceleration_min),
10
+ %w(Velocity\ Max velocity_max),
11
+ %w(Velocity\ Min velocity_min)
12
+ ]
13
+
14
+ phidgets_example_for(PhidgetsNative::AdvancedServo, additional_attribs) do |servo|
15
+ #servo.acceleration 0, servo.acceleration_max[0]
16
+ #servo.velocity_limit 0, servo.velocity_max[0]
17
+ # NOTE that these doesn't seem to be supported on all servo types
18
+ #servo.position_max 0, 220
19
+ #servo.position_min 0, 0
20
+ # /NOTE
21
+ servo.servo_type 0, :default
22
+ servo.speed_ramping 0, false
23
+ servo.engaged 0, true
24
+
25
+ #servo_range = servo.position_max(0) - servo.position_min(0)
26
+ #servo_middle = servo.position_min(0) + servo_range/2
27
+ #servo_left = servo.position_min(0) + servo_middle - servo_range / 4
28
+ #servo_right = servo.position_min(0) + servo_middle + servo_range / 4
29
+
30
+ servo.position 0, 0
31
+
32
+
33
+ puts "\nPolled Values:"
34
+ i = 0
35
+ begin
36
+ i+=1
37
+ puts "Hai #{i}"
38
+ servo.position 0, (i % 2 == 0) ? 0 : 92 # NOTE: 92 seems to be the lowest that goes
39
+ end while sleep(3)
40
+ end
@@ -1,5 +1,6 @@
1
1
  # File: extconf.rb
2
2
  require 'mkmf'
3
+ require 'rbconfig'
3
4
 
4
5
  extension_name = 'phidgets_native'
5
6
 
@@ -8,11 +9,17 @@ HEADER_DIRS = [ '/Library/Frameworks/Phidget21.framework/Headers' ]
8
9
  find_header 'phidget21.h', *HEADER_DIRS
9
10
  find_library 'libphidget21', 'CPhidget_getLibraryVersion', '/usr/lib/'
10
11
 
11
- have_framework 'Phidget21'
12
- have_library('phidget21')
12
+ case RbConfig::CONFIG['host_os']
13
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
14
+ raise StandardError, "Though possible to compile in win32, this extconf wasn't designed for it.... Fix me?"
15
+ when /darwin|mac os/
16
+ have_framework 'Phidget21'
17
+ else
18
+ have_library('phidget21')
19
+ end
13
20
 
14
21
  dir_config extension_name
15
22
 
16
- $CFLAGS << ' -Wno-unused-variable -std=gnu99'
23
+ $CFLAGS << ' -Wno-unused-variable -Wno-declaration-after-statement -std=gnu99'
17
24
 
18
25
  create_makefile extension_name
@@ -44,6 +44,29 @@ VALUE phidgetbool_array_to_rb(int *bool_array, int length) {
44
44
  return rb_ary;
45
45
  }
46
46
 
47
+ /* This converts a 3x3 array of doubles into a ruby matrix, or into
48
+ * nil for the case of an invalid dbl_array
49
+ */
50
+ VALUE double3x3_to_matrix_rb(double m3x3[][3]) {
51
+ if (!m3x3) return Qnil;
52
+
53
+ // To compose a Matrix, we need a 3x3 array of floats:
54
+ VALUE aryRet = rb_ary_new2(3);
55
+ for (unsigned int i=0; i < 3; i++) {
56
+ VALUE arRow = rb_ary_new2(3);
57
+
58
+ for (unsigned int j=0; j < 3; j++)
59
+ rb_ary_store(arRow, j, rb_float_new(m3x3[i][j]));
60
+
61
+ rb_ary_store(aryRet, i, arRow);
62
+ }
63
+
64
+ VALUE c_Matrix = rb_const_get( rb_cObject, rb_intern("Matrix") );
65
+ VALUE rbMatrixRet = rb_funcall(c_Matrix, rb_intern("rows"), 2, aryRet, Qfalse);
66
+
67
+ return rbMatrixRet;
68
+ }
69
+
47
70
  // The name is ambiguous, but the main purpose here is dry things out a bit
48
71
  // and let us do a better job of reporting errors to ruby
49
72
  int ensure(int result) {
@@ -195,3 +218,104 @@ int sample_tick(SampleRate *sample_rate, CPhidget_Timestamp *ts) {
195
218
  return 0;
196
219
  }
197
220
 
221
+ // There's a number of ways to implement this, at the moment, I'm more interested
222
+ // in accuracy... so we use the close-to-optimal method with low cost from :
223
+ // http://pizer.wordpress.com/2008/10/12/fast-inverse-square-root
224
+ // as opposed to: 1.0f / sqrtf(x)
225
+ float inv_sqrt(float x) {
226
+ unsigned int i = 0x5F1F1412 - (*(unsigned int*)&x >> 1);
227
+ float tmp = *(float*)&i;
228
+ return tmp * (1.69000231f - 0.714158168f * x * tmp * tmp);
229
+ }
230
+
231
+
232
+ // Quaternion Multiplication operator. Expects its 4-element arrays in wxyz order
233
+ void quat_mult(float a[4], float b[4], float ret[4]) {
234
+ ret[0] = (b[0] * a[0]) - (b[1] * a[1]) - (b[2] * a[2]) - (b[3] * a[3]);
235
+ ret[1] = (b[0] * a[1]) + (b[1] * a[0]) + (b[2] * a[3]) - (b[3] * a[2]);
236
+ ret[2] = (b[0] * a[2]) + (b[2] * a[0]) + (b[3] * a[1]) - (b[1] * a[3]);
237
+ ret[3] = (b[0] * a[3]) + (b[3] * a[0]) + (b[1] * a[2]) - (b[2] * a[1]);
238
+
239
+ return;
240
+ }
241
+
242
+ // Quaternion Normalization operator. Expects its 4-element arrays in wxyz order
243
+ void quat_norm(float a[4]) {
244
+ float n = a[1]*a[1] + a[2]*a[2] + a[3]*a[3] + a[0]*a[0];
245
+
246
+ if (n == 1.0f)
247
+ return ;
248
+
249
+ n = inv_sqrt(n);
250
+
251
+ a[1]*=n;
252
+ a[2]*=n;
253
+ a[3]*=n;
254
+ a[0]*=n;
255
+
256
+ return;
257
+ }
258
+
259
+ // Quaternion to direction cosine matrix. Simple enough:
260
+ void quat_to_dcm(float q[4], double dcm[][3]) {
261
+ float q_w = q[0];
262
+ float q_x = q[1];
263
+ float q_y = q[2];
264
+ float q_z = q[3];
265
+
266
+ float sqw = q_w*q_w;
267
+ float sqx = q_x*q_x;
268
+ float sqy = q_y*q_y;
269
+ float sqz = q_z*q_z;
270
+
271
+ // invs (inverse square length) is only required if quaternion is not already normalised
272
+ float invs = 1 / (sqx + sqy + sqz + sqw);
273
+ dcm[0][0] = ( sqx - sqy - sqz + sqw)*invs; // since sqw + sqx + sqy + sqz =1/invs*invs
274
+ dcm[1][1] = (-sqx + sqy - sqz + sqw)*invs;
275
+ dcm[2][2] = (-sqx - sqy + sqz + sqw)*invs;
276
+
277
+ float tmp1 = q_x*q_y;
278
+ float tmp2 = q_z*q_w;
279
+
280
+ dcm[1][0] = 2.0 * (tmp1 + tmp2)*invs;
281
+ dcm[0][1] = 2.0 * (tmp1 - tmp2)*invs;
282
+
283
+ tmp1 = q_x*q_z;
284
+ tmp2 = q_y*q_w;
285
+ dcm[2][0] = 2.0 * (tmp1 - tmp2)*invs;
286
+ dcm[0][2] = 2.0 * (tmp1 + tmp2)*invs;
287
+
288
+ tmp1 = q_y*q_z;
289
+ tmp2 = q_x*q_w;
290
+ dcm[2][1] = 2.0 * (tmp1 + tmp2)*invs;
291
+ dcm[1][2] = 2.0 * (tmp1 - tmp2)*invs;
292
+
293
+ return;
294
+ }
295
+
296
+
297
+ // Quaternion to euler:
298
+ void quat_to_euler(float q[4], float e[3]) {
299
+ float sqw = q[0]*q[0];
300
+ float sqx = q[1]*q[1];
301
+ float sqy = q[2]*q[2];
302
+ float sqz = q[3]*q[3];
303
+ e[2] = -1.0f * atan2f(2.f * (q[1]*q[2] + q[3]*q[0]), sqx - sqy - sqz + sqw);
304
+ e[1] = asinf(-2.f * (q[1]*q[3] - q[2]*q[0]));
305
+ e[0] = -1.0f * atan2f(2.f * (q[2]*q[3] + q[1]*q[0]), -sqx - sqy + sqz + sqw);
306
+
307
+ return;
308
+ }
309
+
310
+ // Given an axis, and a angle (in radians), construct a quat
311
+ // NOTE: Assumes axis is already normalised
312
+ void quat_from_axis_and_angle(double axis[3], double angle, float fRetQ[4]) {
313
+ double s = sin(angle/2);
314
+ fRetQ[0] = cos(angle/2); // w
315
+ fRetQ[1] = axis[0] * s; // x
316
+ fRetQ[2] = axis[1] * s; // y
317
+ fRetQ[3] = axis[2] * s; // z
318
+
319
+ return;
320
+ }
321
+
@@ -1,8 +1,10 @@
1
1
  #include <stdio.h>
2
2
  #include <stdbool.h>
3
3
  #include <math.h>
4
+ #include <sys/time.h>
4
5
  #include <time.h>
5
6
  #include <unistd.h>
7
+ #include <ctype.h>
6
8
 
7
9
  #include <ruby.h>
8
10
  #include <phidget21.h>
@@ -15,6 +17,8 @@ static double const MICROSECONDS_IN_SECOND = 1000000.0;
15
17
  static int const DEGREES_IN_CIRCLE = 360;
16
18
  static int const DEFAULT_SPATIAL_DATA_RATE = 16;
17
19
 
20
+ static float const SPATIAL_AHRS_BETA = 0.1f; // 2 * proportional gain (Kp)
21
+
18
22
  // Make sure one of these is non-zero and the other is 0
19
23
  static int const DEFAULT_INTERFACEKIT_DATA_RATE = 8;
20
24
  static int const DEFAULT_INTERFACEKIT_CHANGE_TRIGGER = 0;
@@ -90,9 +94,16 @@ typedef struct spatial_info {
90
94
 
91
95
  bool is_gyroscope_known;
92
96
  double *gyroscope;
97
+ double gyroscope_dcm[3][3];
93
98
 
94
99
  // This is used by the gyro:
95
100
  double last_microsecond;
101
+
102
+ // quaternion elements representing the estimated orientation
103
+ float orientation_q[4];
104
+
105
+ // This tells us whether it's our first pass
106
+ bool is_ahrs_initialized;
96
107
  } SpatialInfo;
97
108
 
98
109
  typedef struct gps_info {
@@ -212,12 +223,19 @@ VALUE double_array_to_rb(double *dbl_array, int length);
212
223
  VALUE int_array_to_rb(int *int_array, int length);
213
224
  VALUE int_array_zeronils_to_rb(int *int_array, int length);
214
225
  VALUE phidgetbool_array_to_rb(int *bool_array, int length);
226
+ VALUE double3x3_to_matrix_rb(double m3x3[][3]);
215
227
  int ensure(int result);
216
228
  int report(int result);
217
229
  SampleRate *sample_create();
218
230
  int sample_free(SampleRate *sample_rate);
219
231
  int sample_zero(SampleRate *sample_rate);
220
232
  int sample_tick(SampleRate *sample_rate, CPhidget_Timestamp *ts);
233
+ float inv_sqrt(float x);
234
+ void quat_mult(float a[4], float b[4], float ret[4]);
235
+ void quat_norm(float a[4]);
236
+ void quat_to_dcm(float q[4], double dcm[][3]);
237
+ void quat_to_euler(float q[4], float e[3]);
238
+ void quat_from_axis_and_angle(double axis[3], double angle, float fRetQ[4]);
221
239
 
222
240
  // Phidget Module
223
241
  VALUE phidget_enable_logging(int argc, VALUE *argv, VALUE class);
@@ -252,6 +270,12 @@ int CCONV spatial_on_attach(CPhidgetHandle phid, void *userptr);
252
270
  int CCONV spatial_on_detach(CPhidgetHandle phid, void *userptr);
253
271
  int CCONV spatial_on_data(CPhidgetSpatialHandle spatial, void *userptr, CPhidgetSpatial_SpatialEventDataHandle *data, int count);
254
272
  int spatial_set_compass_correction_by_array(CPhidgetSpatialHandle phid, double *correction);
273
+ void spatial_ahrs_update(SpatialInfo *spatial_info, float gx, float gy, float gz, float ax, float ay, float az, float mx, float my, float mz);
274
+ void spatial_ahrs_update_imu(SpatialInfo *spatial_info, float gx, float gy, float gz, float ax, float ay, float az);
275
+ void spatial_ahrs_init(SpatialInfo *spatial_info, double ax, double ay, double az, double mx, double my, double mz);
276
+ void spatial_orientation_from_accel_and_compass(double dRetEuler[3], double ax, double ay, double az, double mx, double my, double mz);
277
+ void spatial_madgeq_to_openglq(float *fMadgQ, float *fRetQ);
278
+
255
279
  VALUE spatial_initialize(VALUE self, VALUE serial);
256
280
  VALUE spatial_close(VALUE self);
257
281
  VALUE spatial_sample_rate(VALUE self);
@@ -280,6 +304,21 @@ VALUE spatial_data_rate_max(VALUE self);
280
304
  VALUE spatial_data_rate_set(VALUE self, VALUE data_rate);
281
305
  VALUE spatial_data_rate_get(VALUE self);
282
306
 
307
+ VALUE spatial_direction_cosine_matrix(int argc, VALUE *argv, VALUE self);
308
+ VALUE spatial_accelerometer_to_roll_and_pitch(VALUE self);
309
+ VALUE spatial_accelerometer_to_euler(VALUE self);
310
+ VALUE spatial_accelerometer_to_dcm(VALUE self);
311
+ VALUE spatial_gravity_to_roll_and_pitch(VALUE self);
312
+ VALUE spatial_compass_bearing(VALUE self);
313
+ VALUE spatial_compass_bearing_to_euler(VALUE self);
314
+ VALUE spatial_compass_bearing_to_dcm(VALUE self);
315
+ VALUE spatial_gyro_to_dcm(VALUE self);
316
+ VALUE spatial_orientation_to_quaternion(VALUE self);
317
+ VALUE spatial_orientation_to_dcm(VALUE self);
318
+ VALUE spatial_orientation_to_euler(VALUE self);
319
+
320
+ int euler_to_3x3dcm(double *mRet, double around_x, double around_y, double around_z, const char *in_order);
321
+
283
322
  // PhidgetsNative::InterfaceKit
284
323
  void interfacekit_on_free(void *type_info);
285
324
  int CCONV interfacekit_on_attach(CPhidgetHandle phid, void *userptr);
@@ -1,5 +1,7 @@
1
1
  #include "phidgets_native.h"
2
2
 
3
+ const char DEFAULT_IN_ORDER_STR[] = "xyz";
4
+
3
5
  void spatial_on_free(void *type_info) {
4
6
  SpatialInfo *spatial_info = type_info;
5
7
  if (spatial_info->acceleration)
@@ -34,6 +36,57 @@ int spatial_set_compass_correction_by_array(CPhidgetSpatialHandle phid, double *
34
36
  cc[10], cc[11], cc[12] ));
35
37
  }
36
38
 
39
+ /*
40
+ * This method will rotate the double[3][3] mRet around the axial rotations, in order of in_order )
41
+ */
42
+ int euler_to_3x3dcm(double *mRet, double around_x, double around_y, double around_z, const char *in_order) {
43
+ double mA[3][3];
44
+ double mB[3][3];
45
+ double mAccum[3][3];
46
+
47
+ memcpy(&mAccum, mRet, sizeof(mAccum));
48
+
49
+ unsigned long in_order_length = strlen((in_order) ? in_order : DEFAULT_IN_ORDER_STR);
50
+
51
+ for (unsigned long i = 0; i < in_order_length; i++) {
52
+ switch (tolower((in_order) ? in_order[i] : DEFAULT_IN_ORDER_STR[i])) {
53
+ case 'x':
54
+ mB[0][0] = 1.0; mB[0][1] = 0.0; mB[0][2] = 0.0;
55
+ mB[1][0] = 0.0; mB[1][1] = cos(around_x); mB[1][2] = sin(around_x) * (-1.0);
56
+ mB[2][0] = 0.0; mB[2][1] = sin(around_x); mB[2][2] = cos(around_x);
57
+ break;
58
+ case 'y':
59
+ mB[0][0] = cos(around_y); mB[0][1] = 0.0; mB[0][2] = sin(around_y);
60
+ mB[1][0] = 0.0; mB[1][1] = 1.0; mB[1][2] = 0.0;
61
+ mB[2][0] = sin(around_y) * (-1.0); mB[2][1] = 0.0; mB[2][2] = cos(around_y);
62
+ break;
63
+ case 'z':
64
+ mB[0][0] = cos(around_z); mB[0][1] = sin(around_z) * (-1.0); mB[0][2] = 0.0;
65
+ mB[1][0] = sin(around_z); mB[1][1] = cos(around_z); mB[1][2] = 0.0;
66
+ mB[2][0] = 0.0; mB[2][1] = 0.0; mB[2][2] = 1.0;
67
+ break;
68
+ default:
69
+ // Fatal Error - return without setting mRet
70
+ return 1;
71
+ break;
72
+ }
73
+
74
+ // Accumulate into ret, via multiplication
75
+ memcpy(&mA, &mAccum, sizeof(mAccum));
76
+ memset(&mAccum, 0, sizeof(mAccum));
77
+
78
+ for (unsigned int i = 0; i < 3; i++)
79
+ for (unsigned int j = 0; j < 3; j++)
80
+ for (unsigned int k = 0; k < 3; k++)
81
+ mAccum[i][j] += mA[i][k] * mB[k][j];
82
+ }
83
+
84
+ // Success:
85
+ memcpy(mRet, &mAccum, sizeof(mAccum));
86
+
87
+ return 0;
88
+ }
89
+
37
90
  int CCONV spatial_on_attach(CPhidgetHandle phid, void *userptr) {
38
91
  PhidgetInfo *info = userptr;
39
92
  SpatialInfo *spatial_info = info->type_info;
@@ -67,6 +120,12 @@ int CCONV spatial_on_attach(CPhidgetHandle phid, void *userptr) {
67
120
  spatial_info->gyroscope_min = ALLOC_N(double, spatial_info->gyro_axes);
68
121
  spatial_info->gyroscope_max = ALLOC_N(double, spatial_info->gyro_axes);
69
122
 
123
+ // Initialize the gyro dcm:
124
+ if (spatial_info->gyro_axes == 3) {
125
+ double identity3x3[3][3] = {{1.0,0.0,0.0}, {0.0,1.0,0.0}, {0.0,0.0,1.0}};
126
+ memcpy(&spatial_info->gyroscope_dcm, &identity3x3, sizeof(identity3x3));
127
+ }
128
+
70
129
  // Accelerometer
71
130
  for(int i=0; i < spatial_info->accelerometer_axes; i++) {
72
131
  report(CPhidgetSpatial_getAccelerationMin((CPhidgetSpatialHandle)phid, i, &spatial_info->acceleration_min[i]));
@@ -83,6 +142,9 @@ int CCONV spatial_on_attach(CPhidgetHandle phid, void *userptr) {
83
142
  report(CPhidgetSpatial_getAngularRateMax((CPhidgetSpatialHandle)phid, i, &spatial_info->gyroscope_max[i]));
84
143
  }
85
144
 
145
+ // Ahrs Values:
146
+ spatial_info->is_ahrs_initialized = false;
147
+
86
148
  // Set the data rate for the spatial events in milliseconds.
87
149
  // Note that 1000/16 = 62.5 Hz
88
150
  report(CPhidgetSpatial_setDataRate((CPhidgetSpatialHandle)phid, spatial_info->data_rate));
@@ -104,6 +166,7 @@ int CCONV spatial_on_detach(CPhidgetHandle phidget, void *userptr) {
104
166
  memset(spatial_info->acceleration, 0, sizeof(double) * spatial_info->accelerometer_axes);
105
167
  memset(spatial_info->gyroscope, 0, sizeof(double) * spatial_info->gyro_axes);
106
168
  memset(spatial_info->compass, 0, sizeof(double) * spatial_info->compass_axes);
169
+ memset(&spatial_info->gyroscope_dcm, 0, sizeof(spatial_info->gyroscope_dcm));
107
170
 
108
171
  spatial_info->last_microsecond = 0;
109
172
  spatial_info->is_acceleration_known = false;
@@ -145,24 +208,357 @@ int CCONV spatial_on_data(CPhidgetSpatialHandle spatial, void *userptr, CPhidget
145
208
  spatial_info->is_compass_known = true;
146
209
  }
147
210
 
148
- // Gyros get handled slightly different:
149
- // NOTE: Other people may have a better way to do this, but this is the method
150
- // I grabbed from the phidget sample. Maybe I should report these in radians...
211
+ // Gyro Time! these get handled slightly different...
151
212
  double timestamp = data[i]->timestamp.seconds + data[i]->timestamp.microseconds/MICROSECONDS_IN_SECOND;
152
213
 
153
- if (spatial_info->last_microsecond > 0) {
154
- double timechange = timestamp - spatial_info->last_microsecond;
155
-
156
- for(int j=0; j < spatial_info->gyro_axes; j++)
157
- spatial_info->gyroscope[j] += data[i]->angularRate[j] * timechange;
158
- }
214
+ double timechange = (spatial_info->last_microsecond > 0) ?
215
+ timestamp - spatial_info->last_microsecond : 0;
216
+
217
+ // This is kind of superceeded by the orientation_ functions, but I'm
218
+ // leaving it in for posterity:
219
+ for(int j=0; j < spatial_info->gyro_axes; j++)
220
+ spatial_info->gyroscope[j] += data[i]->angularRate[j] * timechange;
159
221
 
160
222
  spatial_info->is_gyroscope_known = true;
161
223
 
224
+ // If we're dealing with 3 dimensional sensors, we can calculate orientation:
225
+ if (spatial_info->gyro_axes == 3) {
226
+ // This is a shortcut to keep things DRY below:
227
+ double angular_rate_in_rad[3];
228
+ for(int j=0; j < 3; j++)
229
+ angular_rate_in_rad[j] = data[i]->angularRate[j] * M_PI/180;
230
+
231
+ euler_to_3x3dcm((double *) &spatial_info->gyroscope_dcm,
232
+ angular_rate_in_rad[0] * timechange,
233
+ angular_rate_in_rad[1] * timechange,
234
+ angular_rate_in_rad[2] * timechange,
235
+ NULL);
236
+
237
+ // Madgwick Orientation time - Update the ahrs:
238
+ if (!spatial_info->is_ahrs_initialized) {
239
+ if (spatial_info->is_compass_known)
240
+ spatial_ahrs_init(spatial_info,
241
+ data[i]->acceleration[0],
242
+ data[i]->acceleration[1],
243
+ data[i]->acceleration[2],
244
+ data[i]->magneticField[0],
245
+ data[i]->magneticField[1],
246
+ data[i]->magneticField[2]);
247
+ } else {
248
+ float gx = (float) angular_rate_in_rad[0];
249
+ float gy = (float) angular_rate_in_rad[1];
250
+ float gz = (float) angular_rate_in_rad[2];
251
+
252
+ float ax = (float) data[i]->acceleration[0];
253
+ float ay = (float) data[i]->acceleration[1];
254
+ float az = (float) data[i]->acceleration[2];
255
+
256
+ if (spatial_info->is_compass_known)
257
+ // We have enough info for a MARG update:
258
+ spatial_ahrs_update(spatial_info, gx, gy, gz, ax, ay, az,
259
+ (float) data[i]->magneticField[0],
260
+ (float) data[i]->magneticField[1],
261
+ (float) data[i]->magneticField[2]);
262
+ else
263
+ // We only have enough info for a IMU update:
264
+ spatial_ahrs_update_imu(spatial_info, gx, gy, gz, ax, ay, az);
265
+ }
266
+ }
267
+
162
268
  // We'll need this on the next go around:
163
269
  spatial_info->last_microsecond = timestamp;
270
+
164
271
  }
165
272
 
166
273
  return 0;
167
274
  }
168
275
 
276
+ void spatial_orientation_from_accel_and_compass(double dRetEuler[3],
277
+ double ax, double ay, double az,
278
+ double mx, double my, double mz) {
279
+
280
+ /*
281
+ * Roll Angle - about axis 0
282
+ * tan(roll) = gy/gz
283
+ * Use Atan2 so we have an output os (-180 - 180) degrees
284
+ */
285
+ double dbl_grv_roll = atan2(ay, az);
286
+
287
+ /*
288
+ * Pitch Angle - about axis 1
289
+ * tan(pitch) = -gx / (gy * sin(roll angle) * gz * cos(roll angle))
290
+ * Pitch angle range is (-90 - 90) degrees
291
+ */
292
+ double dbl_grv_pitch = atan(-1.0 * ax / (ay * sin(dbl_grv_roll) + az * cos(dbl_grv_roll)));
293
+
294
+ /*
295
+ * Yaw Angle - about axis 2
296
+ * tan(yaw) = (mz * sin(roll) – my * cos(roll)) /
297
+ * (mx * cos(pitch) + my * sin(pitch) * sin(roll) + mz * sin(pitch) * cos(roll))
298
+ * Use Atan2 to get our range in (-180 - 180)
299
+ *
300
+ * Yaw angle == 0 degrees when axis 0 is pointing at magnetic north
301
+ */
302
+ double dbl_yaw = atan2( (mz * sin(dbl_grv_roll)) - (my * cos(dbl_grv_roll)),
303
+ (mx * cos(dbl_grv_pitch)) +
304
+ (my * sin(dbl_grv_pitch) * sin(dbl_grv_roll)) +
305
+ (mz * sin(dbl_grv_pitch) * cos(dbl_grv_roll)) );
306
+
307
+ dRetEuler[0] = dbl_grv_pitch;
308
+ dRetEuler[1] = dbl_grv_roll;
309
+ dRetEuler[2] = dbl_yaw;
310
+
311
+ return;
312
+ }
313
+
314
+
315
+ // NOTE: The spatial_ahrs_* functions were largely copied from Madgwick's
316
+ // Quaternion implementation of the 'DCM filter'
317
+ // https://code.google.com/p/imumargalgorithm30042010sohm/
318
+ void spatial_ahrs_init(SpatialInfo *spatial_info,
319
+ double ax, double ay, double az,
320
+ double mx, double my, double mz) {
321
+
322
+ double xaxis[3] = {1.0, 0.0, 0.0};
323
+ double yaxis[3] = {0.0, 1.0, 0.0};
324
+ double zaxis[3] = {0.0, 0.0, 1.0};
325
+
326
+ // We'll assume the current state by way of the accelerometer and compass readings:
327
+ double dOrientEuler[3];
328
+ spatial_orientation_from_accel_and_compass((double *)&dOrientEuler, ax, ay, az, mx, my, mz);
329
+
330
+ // Now we construct a Roll & Pitch Quat from the Euler's
331
+ float fRollQ[4];
332
+ quat_from_axis_and_angle((double *) &xaxis, dOrientEuler[1], (float *) &fRollQ);
333
+
334
+ float fPitchQ[4];
335
+ quat_from_axis_and_angle((double *) &yaxis, dOrientEuler[0], (float *) &fPitchQ);
336
+
337
+ float fRollPitchQ[4];
338
+ quat_mult((float *) &fRollQ, (float *) &fPitchQ, (float *)&fRollPitchQ);
339
+
340
+ // Rotate the the Quat around our 'Z' so that we're facing North:
341
+ // NOTE, Due to the madgwick origin, -0.5*M_PI is North, and we add the
342
+ // bearing offset to this angle
343
+ double bearing_in_rad = 2*M_PI+dOrientEuler[2];
344
+
345
+ float fCompassQ[4];
346
+ quat_from_axis_and_angle((double *) &zaxis, bearing_in_rad, (float *) &fCompassQ);
347
+
348
+ // Commit this to the persisting orientation state:
349
+ quat_mult((float *) &fRollPitchQ, (float *) &fCompassQ, (float *) spatial_info->orientation_q);
350
+
351
+ spatial_info->is_ahrs_initialized = true;
352
+ }
353
+
354
+ void spatial_ahrs_update(SpatialInfo *spatial_info,
355
+ float gx, float gy, float gz,
356
+ float ax, float ay, float az,
357
+ float mx, float my, float mz) {
358
+
359
+ // Just a shorthand meant to make this a bit easier to work with, and transact
360
+ // commits (eventually)
361
+ float q0 = spatial_info->orientation_q[0];
362
+ float q1 = spatial_info->orientation_q[1];
363
+ float q2 = spatial_info->orientation_q[2];
364
+ float q3 = spatial_info->orientation_q[3];
365
+
366
+ float sample_rate_in_hz = 1000.0f / spatial_info->data_rate;
367
+
368
+ float recipNorm;
369
+ float s0, s1, s2, s3;
370
+ float qDot1, qDot2, qDot3, qDot4;
371
+ float hx, hy;
372
+ float _2q0mx, _2q0my, _2q0mz, _2q1mx, _2bx, _2bz, _4bx, _4bz, _2q0, _2q1, _2q2, _2q3, _2q0q2, _2q2q3, q0q0, q0q1, q0q2, q0q3, q1q1, q1q2, q1q3, q2q2, q2q3, q3q3;
373
+
374
+ // Rate of change of quaternion from gyroscope
375
+ qDot1 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz);
376
+ qDot2 = 0.5f * (q0 * gx + q2 * gz - q3 * gy);
377
+ qDot3 = 0.5f * (q0 * gy - q1 * gz + q3 * gx);
378
+ qDot4 = 0.5f * (q0 * gz + q1 * gy - q2 * gx);
379
+
380
+ // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation)
381
+ if(!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) {
382
+
383
+ // Normalise accelerometer measurement
384
+ recipNorm = inv_sqrt(ax * ax + ay * ay + az * az);
385
+ ax *= recipNorm;
386
+ ay *= recipNorm;
387
+ az *= recipNorm;
388
+
389
+ // Normalise magnetometer measurement
390
+ recipNorm = inv_sqrt(mx * mx + my * my + mz * mz);
391
+ mx *= recipNorm;
392
+ my *= recipNorm;
393
+ mz *= recipNorm;
394
+
395
+ // Auxiliary variables to avoid repeated arithmetic
396
+ _2q0mx = 2.0f * q0 * mx;
397
+ _2q0my = 2.0f * q0 * my;
398
+ _2q0mz = 2.0f * q0 * mz;
399
+ _2q1mx = 2.0f * q1 * mx;
400
+ _2q0 = 2.0f * q0;
401
+ _2q1 = 2.0f * q1;
402
+ _2q2 = 2.0f * q2;
403
+ _2q3 = 2.0f * q3;
404
+ _2q0q2 = 2.0f * q0 * q2;
405
+ _2q2q3 = 2.0f * q2 * q3;
406
+ q0q0 = q0 * q0;
407
+ q0q1 = q0 * q1;
408
+ q0q2 = q0 * q2;
409
+ q0q3 = q0 * q3;
410
+ q1q1 = q1 * q1;
411
+ q1q2 = q1 * q2;
412
+ q1q3 = q1 * q3;
413
+ q2q2 = q2 * q2;
414
+ q2q3 = q2 * q3;
415
+ q3q3 = q3 * q3;
416
+
417
+ // Reference direction of Earth's magnetic field
418
+ hx = mx * q0q0 - _2q0my * q3 + _2q0mz * q2 + mx * q1q1 + _2q1 * my * q2 + _2q1 * mz * q3 - mx * q2q2 - mx * q3q3;
419
+ hy = _2q0mx * q3 + my * q0q0 - _2q0mz * q1 + _2q1mx * q2 - my * q1q1 + my * q2q2 + _2q2 * mz * q3 - my * q3q3;
420
+ _2bx = sqrt(hx * hx + hy * hy);
421
+ _2bz = -_2q0mx * q2 + _2q0my * q1 + mz * q0q0 + _2q1mx * q3 - mz * q1q1 + _2q2 * my * q3 - mz * q2q2 + mz * q3q3;
422
+ _4bx = 2.0f * _2bx;
423
+ _4bz = 2.0f * _2bz;
424
+
425
+ // Gradient decent algorithm corrective step
426
+ s0 = -_2q2 * (2.0f * q1q3 - _2q0q2 - ax) + _2q1 * (2.0f * q0q1 + _2q2q3 - ay) - _2bz * q2 * (_2bx * (0.5f - q2q2 - q3q3) + _2bz * (q1q3 - q0q2) - mx) + (-_2bx * q3 + _2bz * q1) * (_2bx * (q1q2 - q0q3) + _2bz * (q0q1 + q2q3) - my) + _2bx * q2 * (_2bx * (q0q2 + q1q3) + _2bz * (0.5f - q1q1 - q2q2) - mz);
427
+ s1 = _2q3 * (2.0f * q1q3 - _2q0q2 - ax) + _2q0 * (2.0f * q0q1 + _2q2q3 - ay) - 4.0f * q1 * (1 - 2.0f * q1q1 - 2.0f * q2q2 - az) + _2bz * q3 * (_2bx * (0.5f - q2q2 - q3q3) + _2bz * (q1q3 - q0q2) - mx) + (_2bx * q2 + _2bz * q0) * (_2bx * (q1q2 - q0q3) + _2bz * (q0q1 + q2q3) - my) + (_2bx * q3 - _4bz * q1) * (_2bx * (q0q2 + q1q3) + _2bz * (0.5f - q1q1 - q2q2) - mz);
428
+ s2 = -_2q0 * (2.0f * q1q3 - _2q0q2 - ax) + _2q3 * (2.0f * q0q1 + _2q2q3 - ay) - 4.0f * q2 * (1 - 2.0f * q1q1 - 2.0f * q2q2 - az) + (-_4bx * q2 - _2bz * q0) * (_2bx * (0.5f - q2q2 - q3q3) + _2bz * (q1q3 - q0q2) - mx) + (_2bx * q1 + _2bz * q3) * (_2bx * (q1q2 - q0q3) + _2bz * (q0q1 + q2q3) - my) + (_2bx * q0 - _4bz * q2) * (_2bx * (q0q2 + q1q3) + _2bz * (0.5f - q1q1 - q2q2) - mz);
429
+ s3 = _2q1 * (2.0f * q1q3 - _2q0q2 - ax) + _2q2 * (2.0f * q0q1 + _2q2q3 - ay) + (-_4bx * q3 + _2bz * q1) * (_2bx * (0.5f - q2q2 - q3q3) + _2bz * (q1q3 - q0q2) - mx) + (-_2bx * q0 + _2bz * q2) * (_2bx * (q1q2 - q0q3) + _2bz * (q0q1 + q2q3) - my) + _2bx * q1 * (_2bx * (q0q2 + q1q3) + _2bz * (0.5f - q1q1 - q2q2) - mz);
430
+ recipNorm = inv_sqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); // normalise step magnitude
431
+ s0 *= recipNorm;
432
+ s1 *= recipNorm;
433
+ s2 *= recipNorm;
434
+ s3 *= recipNorm;
435
+
436
+ // Apply feedback step
437
+ qDot1 -= SPATIAL_AHRS_BETA * s0;
438
+ qDot2 -= SPATIAL_AHRS_BETA * s1;
439
+ qDot3 -= SPATIAL_AHRS_BETA * s2;
440
+ qDot4 -= SPATIAL_AHRS_BETA * s3;
441
+ }
442
+
443
+ // Integrate rate of change of quaternion to yield quaternion
444
+ q0 += qDot1 * (1.0f / sample_rate_in_hz);
445
+ q1 += qDot2 * (1.0f / sample_rate_in_hz);
446
+ q2 += qDot3 * (1.0f / sample_rate_in_hz);
447
+ q3 += qDot4 * (1.0f / sample_rate_in_hz);
448
+
449
+ // Normalise quaternion
450
+ recipNorm = inv_sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
451
+ q0 *= recipNorm;
452
+ q1 *= recipNorm;
453
+ q2 *= recipNorm;
454
+ q3 *= recipNorm;
455
+
456
+ spatial_info->orientation_q[0] = q0;
457
+ spatial_info->orientation_q[1] = q1;
458
+ spatial_info->orientation_q[2] = q2;
459
+ spatial_info->orientation_q[3] = q3;
460
+ }
461
+
462
+ void spatial_ahrs_update_imu(SpatialInfo *spatial_info,
463
+ float gx, float gy, float gz,
464
+ float ax, float ay, float az) {
465
+
466
+ // Just a shorthand meant to make this a bit easier to work with, and transact
467
+ // commits (eventually)
468
+ float q0 = spatial_info->orientation_q[0];
469
+ float q1 = spatial_info->orientation_q[1];
470
+ float q2 = spatial_info->orientation_q[2];
471
+ float q3 = spatial_info->orientation_q[3];
472
+
473
+ float sample_rate_in_hz = 1000.0f / spatial_info->data_rate;
474
+
475
+ float recipNorm;
476
+ float s0, s1, s2, s3;
477
+ float qDot1, qDot2, qDot3, qDot4;
478
+ float _2q0, _2q1, _2q2, _2q3, _4q0, _4q1, _4q2 ,_8q1, _8q2, q0q0, q1q1, q2q2, q3q3;
479
+
480
+ // Rate of change of quaternion from gyroscope
481
+ qDot1 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz);
482
+ qDot2 = 0.5f * (q0 * gx + q2 * gz - q3 * gy);
483
+ qDot3 = 0.5f * (q0 * gy - q1 * gz + q3 * gx);
484
+ qDot4 = 0.5f * (q0 * gz + q1 * gy - q2 * gx);
485
+
486
+ // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation)
487
+ if(!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) {
488
+
489
+ // Normalise accelerometer measurement
490
+ recipNorm = inv_sqrt(ax * ax + ay * ay + az * az);
491
+ ax *= recipNorm;
492
+ ay *= recipNorm;
493
+ az *= recipNorm;
494
+
495
+ // Auxiliary variables to avoid repeated arithmetic
496
+ _2q0 = 2.0f * q0;
497
+ _2q1 = 2.0f * q1;
498
+ _2q2 = 2.0f * q2;
499
+ _2q3 = 2.0f * q3;
500
+ _4q0 = 4.0f * q0;
501
+ _4q1 = 4.0f * q1;
502
+ _4q2 = 4.0f * q2;
503
+ _8q1 = 8.0f * q1;
504
+ _8q2 = 8.0f * q2;
505
+ q0q0 = q0 * q0;
506
+ q1q1 = q1 * q1;
507
+ q2q2 = q2 * q2;
508
+ q3q3 = q3 * q3;
509
+
510
+ // Gradient decent algorithm corrective step
511
+ s0 = _4q0 * q2q2 + _2q2 * ax + _4q0 * q1q1 - _2q1 * ay;
512
+ s1 = _4q1 * q3q3 - _2q3 * ax + 4.0f * q0q0 * q1 - _2q0 * ay - _4q1 + _8q1 * q1q1 + _8q1 * q2q2 + _4q1 * az;
513
+ s2 = 4.0f * q0q0 * q2 + _2q0 * ax + _4q2 * q3q3 - _2q3 * ay - _4q2 + _8q2 * q1q1 + _8q2 * q2q2 + _4q2 * az;
514
+ s3 = 4.0f * q1q1 * q3 - _2q1 * ax + 4.0f * q2q2 * q3 - _2q2 * ay;
515
+ recipNorm = inv_sqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); // normalise step magnitude
516
+ s0 *= recipNorm;
517
+ s1 *= recipNorm;
518
+ s2 *= recipNorm;
519
+ s3 *= recipNorm;
520
+
521
+ // Apply feedback step
522
+ qDot1 -= SPATIAL_AHRS_BETA * s0;
523
+ qDot2 -= SPATIAL_AHRS_BETA * s1;
524
+ qDot3 -= SPATIAL_AHRS_BETA * s2;
525
+ qDot4 -= SPATIAL_AHRS_BETA * s3;
526
+ }
527
+
528
+ // Integrate rate of change of quaternion to yield quaternion
529
+ q0 += qDot1 * (1.0f / sample_rate_in_hz);
530
+ q1 += qDot2 * (1.0f / sample_rate_in_hz);
531
+ q2 += qDot3 * (1.0f / sample_rate_in_hz);
532
+ q3 += qDot4 * (1.0f / sample_rate_in_hz);
533
+
534
+ // Normalise quaternion
535
+ recipNorm = inv_sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
536
+ q0 *= recipNorm;
537
+ q1 *= recipNorm;
538
+ q2 *= recipNorm;
539
+ q3 *= recipNorm;
540
+
541
+ spatial_info->orientation_q[0] = q0;
542
+ spatial_info->orientation_q[1] = q1;
543
+ spatial_info->orientation_q[2] = q2;
544
+ spatial_info->orientation_q[3] = q3;
545
+ }
546
+
547
+ /*
548
+ The Madwick algorithm :
549
+ * Uses +x to indicate North, we want +y which means we need a -90 degree rotation around z.
550
+ * Uses the following coordinate system:
551
+ http://stackoverflow.com/questions/17788043/madgwicks-sensor-fusion-algorithm-on-ios
552
+ */
553
+ void spatial_madgeq_to_openglq(float *fMadgQ, float *fRetQ) {
554
+ float fTmpQ[4];
555
+ float fXYRotationQ[4] = { sqrt(0.5), 0, 0, -1.0*sqrt(0.5) }; // wxyz
556
+
557
+ fTmpQ[0] = fMadgQ[0];
558
+ fTmpQ[1] = fMadgQ[1] * -1.0f;
559
+ fTmpQ[2] = fMadgQ[2];
560
+ fTmpQ[3] = fMadgQ[3] * -1.0f;
561
+
562
+ // And then store the Rotation into fTranslatedQ
563
+ quat_mult((float *) &fTmpQ, (float *) &fXYRotationQ, fRetQ);
564
+ }