@@ -26,11 +26,24 @@ enum AxisIndex {
2626 AXIS_RY ,
2727 AXIS_RZ
2828};
29+
30+ // Precomputed geometric constants for the equilateral sensor triangle.
31+ const float kOneThird = 1 .0f / 3 .0f ;
32+ const float kSqrt3 = 1 .7320508f ; // sqrt(3)
33+ const float kSqrt3Over6 = 0 .28867513f ; // sqrt(3)/6
34+ const float kSqrt3Over3 = 0 .57735027f ; // sqrt(3)/3
35+ const float kMag2PosX = -0 .5f ;
36+ const float kMag2PosY = kSqrt3Over6 ;
37+ const float kMag3PosX = 0 .5f ;
38+ const float kMag3PosY = kSqrt3Over6 ;
39+ const float kMag1PosX = 0 .0f ;
40+ const float kMag1PosY = -kSqrt3Over3 ;
2941} // namespace
3042
3143void MotionController::reset () {
3244 for (int i = 0 ; i < 6 ; i++) {
33- filt_[i] = 0.0 ;
45+ kalmanX_[i] = 0 .0f ;
46+ kalmanP_[i] = 1 .0f ;
3447 }
3548 motionActive_ = false ;
3649}
@@ -41,20 +54,42 @@ float MotionController::clampf(float v, float lo, float hi) {
4154 return v;
4255}
4356
44- float MotionController::hardZero (float v, float thr) {
45- return (fabs (v) < thr) ? 0.0 : v;
46- }
57+ float MotionController::kalmanStep (int axis, float measurement) {
58+ // Predict step: uncertainty grows by process noise
59+ kalmanP_[axis] += Config::KALMAN_Q ;
60+
61+ // Update step: compute Kalman gain
62+ const float K = kalmanP_[axis] / (kalmanP_[axis] + Config::KALMAN_R );
4763
48- float MotionController::lowpass (float prev, float x, float dt, float tau) {
49- if (tau <= 0.0 ) return x;
50- const float a = dt / (tau + dt);
51- return prev + a * (x - prev);
64+ // Correct estimate with measurement
65+ kalmanX_[axis] += K * (measurement - kalmanX_[axis]);
66+
67+ // Update uncertainty
68+ kalmanP_[axis] *= (1 .0f - K);
69+
70+ return kalmanX_[axis];
5271}
5372
5473float MotionController::axisBaseDead (int i) {
5574 return (i < 3 ) ? Config::DEAD_T : Config::DEAD_R ;
5675}
5776
77+ float MotionController::sensitivityCurve (float value, float dead, float limit) {
78+ // Map the post-dead-zone range [dead, limit] onto [0, limit] with a power curve.
79+ // This gives fine control at small deflections and fast motion at large ones.
80+ const float sign = (value >= 0 .0f ) ? 1 .0f : -1 .0f ;
81+ const float abs_val = fabs (value);
82+ if (abs_val <= dead) return 0 .0f ;
83+
84+ // Normalize to 0..1 within the active range
85+ const float range = limit - dead;
86+ if (range <= 0 .0f ) return 0 .0f ;
87+ const float normalized = clampf ((abs_val - dead) / range, 0 .0f , 1 .0f );
88+
89+ // Apply power curve and scale back to output range
90+ return sign * powf (normalized, Config::SENSITIVITY_EXP ) * limit;
91+ }
92+
5893void MotionController::compute (const float raw[9 ], const float * baseline, float dt,
5994 float out[6 ]) {
6095 // Baseline subtraction converts magnetic deltas around the calibrated rest pose.
@@ -68,43 +103,21 @@ void MotionController::compute(const float raw[9], const float* baseline, float
68103 const float mag3y = raw[RAW_MAG3_Y ] - baseline[RAW_MAG3_Y ];
69104 const float mag3z = raw[RAW_MAG3_Z ] - baseline[RAW_MAG3_Z ];
70105
71- // Translation:
72- // Tx = (mag1x + mag2x + mag3x) / 3
73- // Ty = (mag1y + mag2y + mag3y) / 3
74- // Tz = (mag1z + mag2z + mag3z) / 3
75- const float tx = (mag1x + mag2x + mag3x) / 3.0 ;
76- const float ty = (mag1y + mag2y + mag3y) / 3.0 ;
77- const float tz = (mag1z + mag2z + mag3z) / 3.0 ;
78-
79- // Physical PCB layout:
80- // MAG2 = top left, MAG3 = top right, MAG1 = bottom.
81- const float mag2PosX = -0.5 ;
82- const float mag2PosY = sqrt (3.0 ) / 6.0 ;
83-
84- const float mag3PosX = 0.5 ;
85- const float mag3PosY = sqrt (3.0 ) / 6.0 ;
86-
87- const float mag1PosX = 0.0 ;
88- const float mag1PosY = -sqrt (3.0 ) / 3.0 ;
89-
90- // Rotation estimates:
91- // Ry = mag3z - mag2z
92- // right sensor minus left sensor
93- // -> side to side tilt across the top edge
94- //
95- // Rx = sqrt(3) * (mag2z + mag3z - 2 * mag1z) / 3
96- // top pair minus bottom sensor
97- // -> front/back tilt of the triangle
98- const float rx = (sqrt (3.0 ) * (mag2z + mag3z - 2.0 * mag1z)) / 3.0 ;
99- const float ry = (mag3z - mag2z);
106+ // Translation: average of all three sensors.
107+ const float tx = (mag1x + mag2x + mag3x) * kOneThird ;
108+ const float ty = (mag1y + mag2y + mag3y) * kOneThird ;
109+ const float tz = (mag1z + mag2z + mag3z) * kOneThird ;
100110
101- // Rz = sum_i (posXi * magYi - posYi * magXi)
102- // Each sensor contributes according to its x/y position in the triangle.
103- const float swirlNum =
104- (mag2PosX * mag2y - mag2PosY * mag2x) +
105- (mag3PosX * mag3y - mag3PosY * mag3x) +
106- (mag1PosX * mag1y - mag1PosY * mag1x);
107- const float rz = swirlNum;
111+ // Rotation estimates from sensor triangle geometry.
112+ // Ry: side-to-side tilt (right sensor minus left)
113+ // Rx: front/back tilt (top pair minus bottom)
114+ // Rz: twist (cross-product per sensor position)
115+ const float rx = (kSqrt3 * (mag2z + mag3z - 2 .0f * mag1z)) * kOneThird ;
116+ const float ry = (mag3z - mag2z);
117+ const float rz =
118+ (kMag2PosX * mag2y - kMag2PosY * mag2x) +
119+ (kMag3PosX * mag3y - kMag3PosY * mag3x) +
120+ (kMag1PosX * mag1y - kMag1PosY * mag1x);
108121
109122 // Apply sign fixes and gains
110123 float y[6 ];
@@ -115,21 +128,23 @@ void MotionController::compute(const float raw[9], const float* baseline, float
115128 y[AXIS_RY ] = Config::SIGN_AXIS [AXIS_RY ] * ry * Config::GAIN_R [AXIS_RY - 3 ];
116129 y[AXIS_RZ ] = Config::SIGN_AXIS [AXIS_RZ ] * rz * Config::GAIN_R [AXIS_RZ - 3 ];
117130
118- // Filter, clamp to range and dead zones.
131+ // Kalman filter, sensitivity curve, dead zones, and clamp .
119132 motionActive_ = false ;
120133 for (int i = 0 ; i < 6 ; i++) {
121134 const float dead = axisBaseDead (i);
122135
123136 if (fabs (y[i]) < dead) {
124- filt_[i] = 0.0 ;
137+ // Below dead zone: decay Kalman estimate toward zero gradually.
138+ // Preserve covariance so the filter doesn't jitter at the boundary.
139+ kalmanX_[i] *= 0 .8f ;
140+ kalmanP_[i] = fmin (kalmanP_[i] + Config::KALMAN_Q * 0 .1f , 1 .0f );
125141 } else {
126- filt_[i] = lowpass (filt_[i] , y[i], dt, Config:: SMOOTH_TAU_S );
142+ kalmanStep (i , y[i]);
127143 }
128144
129- const float limited =
130- clampf (filt_[i], -Config::AXIS_LIMIT , Config::AXIS_LIMIT );
131- out[i] = hardZero (limited, dead);
132- if (out[i] != 0.0 ) {
145+ // Apply sensitivity curve to filtered output
146+ out[i] = sensitivityCurve (kalmanX_[i], dead, Config::AXIS_LIMIT );
147+ if (out[i] != 0 .0f ) {
133148 motionActive_ = true ;
134149 }
135150 }
0 commit comments