Skip to content

Commit cbe31f3

Browse files
committed
Updated the reading and added in some groundwork for yaw estimate
1 parent 7e18a12 commit cbe31f3

File tree

2 files changed

+99
-43
lines changed

2 files changed

+99
-43
lines changed

Readme.markdown

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,41 @@ Shumai is _the_ Extended Kalman Filter (EKF) for fixed-wing aircraft. It provide
55

66
In this first release only roll and pitch are included but yaw, position and wind estimation will follow shortly.
77

8+
What you should understand very clearly about this filter is it has gimbal lock / singularity issues. UAVs aren't used for aerobatics (yet) so if you keep things gentle and smooth (±45 degrees from level in pitch and roll) you'll be just fine.
9+
810
Shumai was developed by [Tim Trueman](http://github.com/timtrueman) and [Ryan Beall](http://diydrones.com/profile/RyanBeall). It's based on a [BYU paper](http://contentdm.lib.byu.edu/ETD/image/etd1527.pdf) [PDF].
911

10-
Quickstart
12+
Installation
1113
==========
14+
15+
Mac/Linux/Unix
16+
1. Install X-Plane (this takes the longest)
17+
2. Download source code
18+
3. sudo easy_install numpy
19+
4. Start X-Plane and set the checkbox that outputs data
20+
5. run "python shumai.py" in the command line
21+
22+
Windows
1223
1. Install X-Plane (this takes the longest)
1324
2. Download source code
14-
3. Install Python
15-
4. Install numpy
16-
5. Install curses (if available)
17-
6. Start X-Plane and run "python shumai.py" in the command line
25+
3. Install the 32-bit Python 2.6 package
26+
4. Install the 32-bit numpy package
27+
5. Set the registry thingy so python works in the command line
28+
6. Start X-Plane and set the checkbox that outputs data
29+
7. run "python shumai.py" in the command line
1830

19-
Configuration
31+
Future
2032
=============
21-
Todo.
33+
Yaw estimate
34+
Position estimate
35+
Wind vector estimate
36+
Altitude estimate
37+
Airspeed estimate
38+
Tuning / noise / real hardware testing (perhaps a genetic algorithm for tuning)
39+
Porting to other languages/platforms (microcontrollers)
2240

2341
Screenshots
2442
===========
25-
I'll put up better screenshots later…
26-
2743
X-Plane + Shumai running
2844
---------
2945
![Running](http://dronedynamics.com/shumai-running.jpg)
@@ -34,8 +50,18 @@ Performance
3450

3551
FAQ
3652
===
53+
54+
Q: Why did you build this and release it as open source?<br/>
55+
A: It's coming. I promise.
56+
57+
Q: What license is it released under?<br/>
58+
A: The MIT license: http://en.wikipedia.org/wiki/MIT_License
59+
3760
Q: Something doesn't work.<br/>
3861
A: Email or IM me: [email protected]
3962

4063
Q: Where's the documentation?<br/>
41-
A: It's coming. I promise.
64+
A: It's coming. I promise.
65+
66+
Q: Why does your filter suffer from gimbal lock / singularities? I want to do aerobatics!<br/>
67+
A: The vast majority of UAVs are not going to be doing crazy maneuvers; a few common sense rules keeps things simple and allows for optimized precision. Also if you think you can solve this problem, email me or just write the code! This is open source after all.

shumai.py

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,35 @@
11
#!/usr/bin/env python
22
# encoding: utf-8
33

4+
"""
5+
Copyright (c) 2010 Tim Trueman and Ryan Beall
6+
7+
Permission is hereby granted, free of charge, to any person
8+
obtaining a copy of this software and associated documentation
9+
files (the "Software"), to deal in the Software without
10+
restriction, including without limitation the rights to use,
11+
copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
copies of the Software, and to permit persons to whom the
13+
Software is furnished to do so, subject to the following
14+
conditions:
15+
16+
The above copyright notice and this permission notice shall be
17+
included in all copies or substantial portions of the Software.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26+
OTHER DEALINGS IN THE SOFTWARE.
27+
"""
28+
29+
430
import socket
531
import sys
6-
import math
32+
from math import cos, sin, tan, isnan, pi, degrees, radians
733
from struct import unpack_from
834
from datetime import datetime
935
import logging
@@ -69,13 +95,13 @@ def get_matrix_display_size(m, precision=2, title=True):
6995

7096
def safe_tangent(x):
7197
""" This is my awful attempt at preventing NaN from returning. """
72-
tangent = math.tan(x)
73-
if math.isnan(tangent):
98+
tangent = tan(x)
99+
if isnan(tangent):
74100
logger.error("Tangent departed, input x = %f" % x)
75-
if tangent % (math.pi / 2) > 0:
76-
tangent = math.tan(x+0.000000001)
101+
if tangent % (pi / 2) > 0:
102+
tangent = tan(x+0.000000001)
77103
else:
78-
tangent = math.tan(x-0.000000001)
104+
tangent = tan(x-0.000000001)
79105
return tangent
80106

81107
class AttitudeObserver:
@@ -127,15 +153,15 @@ def __init__(self):
127153

128154
def __update_state_estimate_using_kinematic_update(self, p, q, r, dt):
129155
""" When we get new gyro readings we can update the estimate of pitch and roll with the new values. [ISEFMAV 2.20] """
130-
self.phihat = self.phi + ((p + (q * math.sin(self.phi) * safe_tangent(self.theta)) + (r * math.cos(self.phi) * safe_tangent(self.theta))) * dt)
131-
self.thetahat = self.theta + (((q * math.cos(self.phi)) - (r * math.sin(self.phi))) * dt)
156+
self.phihat = self.phi + ((p + (q * sin(self.phi) * safe_tangent(self.theta)) + (r * cos(self.phi) * safe_tangent(self.theta))) * dt)
157+
self.thetahat = self.theta + (((q * cos(self.phi)) - (r * sin(self.phi))) * dt)
132158
logger.debug("kinematic update, phihat = %f, thetahat = %f" % (self.phihat, self.thetahat))
133159

134160
def __compute_linearized_state_update_matrix(self, q, r):
135161
""" Once we've updated the state estimates for phi and theta we need to linearize it again. [ISEFMAV 2.21] """
136-
self.A[0,0] = (q * math.cos(self.phihat) * safe_tangent(self.thetahat)) - (r * math.sin(self.phihat) * safe_tangent(self.thetahat))
137-
self.A[0,1] = ((q * math.sin(self.phihat)) - (r * math.cos(self.phihat))) / math.pow(math.cos(self.thetahat), 2)
138-
self.A[1,0] = (-q * math.sin(self.phihat)) + (r * math.cos(self.phihat))
162+
self.A[0,0] = (q * cos(self.phihat) * safe_tangent(self.thetahat)) - (r * sin(self.phihat) * safe_tangent(self.thetahat))
163+
self.A[0,1] = ((q * sin(self.phihat)) - (r * cos(self.phihat))) / pow(cos(self.thetahat), 2)
164+
self.A[1,0] = (-q * sin(self.phihat)) + (r * cos(self.phihat))
139165
# self.A[1,1] = 0
140166

141167
def __propagate_covariance_matrix(self, dt):
@@ -146,15 +172,15 @@ def __propagate_covariance_matrix(self, dt):
146172

147173
def __linearized_model_output_matrix(self, p, q, r, Vair):
148174
""" Axhat, ayhat and azhat are used to compute the the expected accelerometer readings given the model (the flight dynamics of a fixed-wing aircraft). Once we have these we can look at the actual readings and adjust things accordingly. [ISEFMAV 2.26] """
149-
self.axhat = ((Vair * q * math.sin(self.thetahat))/GRAVITY) + math.sin(self.thetahat)
150-
self.ayhat = ((Vair * ((r * math.cos(self.thetahat)) - (p * math.sin(self.thetahat))))/GRAVITY) - (math.cos(self.thetahat) * math.sin(self.phihat))
151-
self.azhat = ((-Vair * q * math.cos(self.thetahat))/GRAVITY) - (math.cos(self.thetahat) * math.cos(self.phihat))
175+
self.axhat = ((Vair * q * sin(self.thetahat))/GRAVITY) + sin(self.thetahat)
176+
self.ayhat = ((Vair * ((r * cos(self.thetahat)) - (p * sin(self.thetahat))))/GRAVITY) - (cos(self.thetahat) * sin(self.phihat))
177+
self.azhat = ((-Vair * q * cos(self.thetahat))/GRAVITY) - (cos(self.thetahat) * cos(self.phihat))
152178

153179
def __gain_calculation_and_variance_update(self, p, q, r, Vair):
154180
""" Calculate linearized output equations...this is the magic of the Kalman filter; here we are figuring out how much we trust the sensors [ISEFMAV 2.27] """
155-
self.Cx = numpy.matrix([[0, ((q * Vair / GRAVITY) * math.cos(self.thetahat)) + math.cos(self.thetahat)]])
156-
self.Cy = numpy.matrix([[-math.cos(self.thetahat) * math.cos(self.phihat), ((-r * Vair / GRAVITY) * math.sin(self.thetahat)) - ((p * Vair / GRAVITY) * math.cos(self.thetahat)) + (math.sin(self.thetahat) * math.sin(self.phihat))]])
157-
self.Cz = numpy.matrix([[math.cos(self.thetahat) * math.cos(self.phihat), (((q * Vair / GRAVITY) * math.sin(self.thetahat)) + math.cos(self.phihat)) * math.sin(self.thetahat)]])
181+
self.Cx = numpy.matrix([[0, ((q * Vair / GRAVITY) * cos(self.thetahat)) + cos(self.thetahat)]])
182+
self.Cy = numpy.matrix([[-cos(self.thetahat) * cos(self.phihat), ((-r * Vair / GRAVITY) * sin(self.thetahat)) - ((p * Vair / GRAVITY) * cos(self.thetahat)) + (sin(self.thetahat) * sin(self.phihat))]])
183+
self.Cz = numpy.matrix([[cos(self.thetahat) * cos(self.phihat), (((q * Vair / GRAVITY) * sin(self.thetahat)) + cos(self.phihat)) * sin(self.thetahat)]])
158184

159185
def __get_sensor_noise_covariance_matrix(self, q):
160186
""" Sensor noise penalty, needs a better explanation of how it works. Tau is a tuning parameter which is increased to reduce the Kalman gain on x-axis accelerometer during high pitch rate maneuvers (that flight dynamic is unmodeled). I'm trying values between 10-100. [ISEFMAV 2.28] """
@@ -168,7 +194,7 @@ def __calc_kalman_gain_matrix(self):
168194
self.Lx = (self.Px * self.Cx.transpose()) / (self.rx + (self.Cx * self.Px * self.Cx.transpose()))
169195
self.Ly = (self.Py * self.Cy.transpose()) / (self.ry + (self.Cy * self.Py * self.Cy.transpose()))
170196
self.Lz = (self.Pz * self.Cz.transpose()) / (self.rz + (self.Cz * self.Pz * self.Cz.transpose()))
171-
self.acceleration_magnitude = math.sqrt(self.ax**2 + self.ay**2 + self.az**2) / GRAVITY
197+
self.acceleration_magnitude = sqrt(self.ax**2 + self.ay**2 + self.az**2) / GRAVITY
172198
self.acceleration_weight = min(max(0, (1 - 2 * abs(1 - self.acceleration_magnitude))), 1)
173199
self.Lx *= self.acceleration_weight
174200
self.Ly *= self.acceleration_weight
@@ -183,13 +209,13 @@ def __update_state_estimate(self, ax, ay, az):
183209

184210
def __check_for_divergence(self):
185211
""" Divergence is when the EKF has departed from reality is is confused. I log it so you can see when the EKF goes into a broken state. You can try resetting it and it may come back (no guarantees but it's been known to work). """
186-
if abs(math.degrees(self.phi)) > 90:
212+
if abs(degrees(self.phi)) > 90:
187213
if self.roll_is_departed == False:
188214
self.roll_is_departed = True
189215
logger.critical("Roll has departed.")
190216
else:
191217
self.roll_is_departed = False
192-
if abs(math.degrees(self.theta)) > 90:
218+
if abs(degrees(self.theta)) > 90:
193219
if self.pitch_is_departed == False:
194220
self.pitch_is_departed = True
195221
logger.critical("Pitch has departed.")
@@ -222,8 +248,8 @@ def display_state(self, screen, precision=5):
222248
"axhat":self.axhat,
223249
"ayhat":self.ayhat,
224250
"azhat":self.azhat,
225-
"Phi (deg)":math.degrees(self.phi),
226-
"Theta (deg)":math.degrees(self.theta),}
251+
"Phi (deg)":degrees(self.phi),
252+
"Theta (deg)":degrees(self.theta),}
227253
scalar_count = len(scalars)
228254
screen.addstr(0, 0, "Shumai: the Extended Kalman Filter for aircraft")
229255
i = 1
@@ -279,7 +305,7 @@ def estimate_roll_and_pitch(self, p, q, r, Vair, ax, ay, az, dt, screen=None):
279305
if screen:
280306
self.display_state(screen)
281307
else:
282-
logger.info("roll = %f, pitch = %f" % (math.degrees(self.phi), math.degrees(self.theta)))
308+
logger.info("roll = %f, pitch = %f" % (degrees(self.phi), degrees(self.theta)))
283309
return self.phi, self.theta
284310

285311
class Shumai:
@@ -311,10 +337,10 @@ def loop(self):
311337
Vair = self.differential_pressure_sensor.read_airspeed() * 0.5144444444 # kias to meters per second
312338
phi, theta = self.attitude_observer.estimate_roll_and_pitch(p, q, r, Vair, ax, ay, az, self.dt, screen=self.screen)
313339
return {
314-
"roll": math.degrees(phi),
315-
"pitch": math.degrees(theta),
340+
"roll": degrees(phi),
341+
"pitch": degrees(theta),
316342
# Coming soon:
317-
# "yaw": math.degrees(psi),
343+
# "yaw": degrees(psi),
318344
# "airspeed": Vair,
319345
# "position": (position_north, position_east),
320346
# "wind": (wind_north, wind_east),
@@ -344,12 +370,16 @@ def datagramReceived(self, data, (host, port)):
344370
self.az = 0 - unpack_from(fmt, data, 9+16+36)[0]
345371
self.ax = unpack_from(fmt, data, 9+20+36)[0]
346372
self.ay = unpack_from(fmt, data, 9+24+36)[0]
347-
self.q = math.radians(unpack_from(fmt, data, 9+108+0)[0])
348-
self.p = math.radians(unpack_from(fmt, data, 9+108+4)[0])
349-
self.r = math.radians(unpack_from(fmt, data, 9+108+8)[0])
350-
self.pitch = math.radians(unpack_from(fmt, data, 9+144+0)[0])
351-
self.roll = math.radians(unpack_from(fmt, data, 9+144+4)[0])
352-
self.heading = math.radians(unpack_from(fmt, data, 9+144+8)[0])
373+
self.q = radians(unpack_from(fmt, data, 9+108+0)[0])
374+
self.p = radians(unpack_from(fmt, data, 9+108+4)[0])
375+
self.r = radians(unpack_from(fmt, data, 9+108+8)[0])
376+
self.pitch = radians(unpack_from(fmt, data, 9+144+0)[0])
377+
self.roll = radians(unpack_from(fmt, data, 9+144+4)[0])
378+
self.heading = radians(unpack_from(fmt, data, 9+144+8)[0])
379+
phi, theta, psi = self.roll, self.pitch, self.heading
380+
self.bx = cos(theta) * cos(psi) * cos(psi) * -sin(theta)
381+
self.by = ((sin(phi) * sin(theta) * cos(psi)) - (cos(phi) * sin(psi))) * ((sin(phi) *sin(theta) * sin(psi)) + (cos(phi) * cos(psi))) * sin(phi) * cos(theta)
382+
self.bz = ((cos(phi) * sin(theta) * cos(psi)) + (sin(phi) * sin(psi))) * ((cos(phi) * sin(theta) * sin(psi)) - (sin(phi) * cos(psi))) * cos(phi) * cos(theta)
353383
logger.debug("Vair %0.1f, accelerometers (%0.2f, %0.2f, %0.2f), gyros (%0.2f, %0.2f, %0.2f)" % (self.Vair, self.ax, self.ay, self.az, self.p, self.q, self.r))
354384
current_state = self.ekf.loop()
355385
if self.screen is not None:
@@ -359,7 +389,7 @@ def datagramReceived(self, data, (host, port)):
359389
else:
360390
sys.stdout.write("%sRoll = %f, pitch = %f " % (chr(13), current_state['roll'], current_state['pitch']))
361391
sys.stdout.flush()
362-
FOUT.writerow([math.degrees(self.roll), math.degrees(self.pitch), current_state['roll'], current_state['pitch'], current_state['roll'] - math.degrees(self.roll), current_state['pitch'] - math.degrees(self.pitch)])
392+
FOUT.writerow([degrees(self.roll), degrees(self.pitch), current_state['roll'], current_state['pitch'], current_state['roll'] - degrees(self.roll), current_state['pitch'] - degrees(self.pitch)])
363393

364394
def read_gyros(self):
365395
return self.p, self.q, self.r

0 commit comments

Comments
 (0)