Inertial Navigation System

From FlightGear wiki
Jump to navigation Jump to search
Note  Whenever possible, please refrain from modeling complex systems, like an FDM, autopilot or Route Manager with Nasal. This is primarily to help reduce Nasal overhead (especially GC overhead). It will also help to unify duplicated code. The FlightGear/SimGear code base already contains fairly generic and efficient systems and helpers, implemented in C++, that merely need to be better generalized and exposed to Nasal so that they can be used elsewhere. For example, this would enable Scripted AI Objects to use full FDM implementations and/or built-in route manager systems.

Technically, this is also the correct approach, as it allows us to easily reuse existing code that is known to be stable and working correctly, .

For details on exposing these C++ systems to Nasal, please refer to Nasal/CppBind. If in doubt, please get in touch via the mailing list or the forum first.


The inertial navigation system is a system of aircraft navigation still used today in many large aircraft as the primary navigation system. Instead of using radio navigation aids or satellites, this system is entirely self-contained within the aircraft, computing the aircraft's position by sensing its acceleration and orientation. It, in essence, is a real accurate dead-reckoning computer.

There are several names for it, some with slightly different uses:

  • Inertial Navigation System (INS)
  • Inertial Reference System (IRS)
  • Inertial Navigation Unit (INU)
  • Inertial Reference Unit (IRU)
  • Inertial Measurement Unit (IMU, the name used by NASA in many cases -- look at the Pheonix lander now on Mars.)

Dead-reckoning is a navigation technique where you know where you started, what direction you flew, how fast you flew, and how long you flew. You can then, on a map, trace your position.[1] The inertial navigation system (abbreviated "INS") uses accelerometers to find your velocity and direction, and computers compile in the time and an entered start position to calculate your current position.

Inertial navigation systems are entirely self-contained in the aircraft and can be used anywhere -- regardless of the presence of radio navigation aids or GPS. However, it accumulates error over time (an accurate one is accurate to about 0.6 nm after one hour.) To combat that, most INS systems can update their position and velocity using radio NAVAIDS and/or GPS. In fact, one of the most powerful navigation systems is when GPS data is used to update the INS, called GPS/INS navigation. GPS doesn't drift, but it only updates once a second and isn't as consistend. However, the INS will "fill in the gaps" in the GPS signal, producing a reliable, accurate, and real time signal for the location, orientation, and velocity of the aircraft. Additionally, an INS can be used as part of CAT 3 ILS equipment.

Inertial navigation systems usually have to be aligned on the ground, with the aircraft in a completely stationary position. The pilots (or GPS) gives them their initial system. Alignment for strapdown systems (see below for the types) usually takes 5-10 minutes, and is longer at higher latitudes -- sometimes systems won't align above certain latitudes, but you don't find international airports there. Gimballed systems align by powering up and leveling the platform (with motors.) Strapdown systems, however, sense gravity to get the attitude and sense the earth's rotation for true heading (they get magnetic heading by correcting with a database of magnetic variation) and to verify the entered latitude is approximately correct. However, there are some systems (like Honeywell's align-in-motion[2]) that can align the system while in flight, often using GPS and compass readings. These aren't very common in current airliners. Additionally, many INS systems have an attitude-only mode where they can be aligned in flight to only give the plane's attitude, as only a full align on those systems can give navigational capability. Some can re-align at a ground stop quickly since most of the alignment has already been done -- however, they have to have not been turned off.

Two Types

There are two primary types of inertial navigation systems, gimballed and strapdown.[3]

Gimballed

Gimballed systems have a platform in the device that is mounted in gimbals. This device has 2 or more mechanical gyroscopes (not likely there are more than 3) that keep this platform level. On the platform, in addition to the gyroscopes, are usually three accelerometers, one in each direction. This was the earlier type of INS. It does not need accurate gyroscope orientation sensing, they only need mechanical gyroscopes to keep a platform level -- a much less demanding task for the gyroscopes. Additionally, since the accelerometers are already oriented (usually north/south, east/west, and up/down) the actual integration to obtain velocity and then position can be done by simpler, analog electronics.

However, they do have their disadvantages. One is reliability -- the spinning gyros and gimbals all move, so you have wear and tear and they can fail or lose their accuracy. However, a more prominant issue is with "gimbal lock."[4] This is when two of those three gimbals align. Since they both function about one axis, and the other only does one axis too, you only have two axes -- any rotation about the last axis cannot occur, so the platform is swung and misaligned with rotation about that axis. There are two main solutions: navigate around it or a fourth gimbal. On Apollo 11 there were only three gimbals, so they planned their maneuvers around gimbal lock. Their computers told them where not to go to keep two gimbals from aligning. The second, but more complex, solution is a fourth gimbal. This gimbal is motorized to keep it always oriented away from the other gimbals, so you keep three independent axes at all times. However, this is mechanically more complex. The fourth-gimbal system is used more in more recent gimballed inertial navigation systems.

Strapdown

Strapdown systems have all their sensors mounted on a platform that changes orientation like the plane. Instead of mechanical gyros to hold it level, it has three more accurate gyros that sense the orientation of the system. Additionally, it has the same three acceleration sensors.

Whereas the gimballed system just senses the orientation of the platform to get the aircraft's attitude, the strapdown systems have three gyroscopes that sense the rate of roll, pitch, and yaw. It integrates them to get the orientation, then calculates the acceleration in each of the same axes as the gimballed system.

Due to the sensing of the rate of rotation, rather than just holding a platform level, very accurate and sensitive gyroscopes are needed. There are several sensitive gyroscope types available, but the most commonly used on is the ring laser gyro. This basically is a circular path (actually, usually triangular) that laser light travels around. This light goes both ways. When it is rotated, one direction appears to go faster than the other -- and when they get around and meet, they interfere. This creates a pattern that is picked up by sensors. These gyroscopes are so accurate the alignment for strapdown systems consists of finding true north by sensing the earth's rotation.

Strapdown systems have fewer moving parts, so they are more reliable and simpler than other systems. However, they do need more accurate gyroscopes and better computers, so they are a more recent development.

FlightGear's implementation

There is a simulation of an INS being developed for FlightGear. It is discussed in the thread "INS Systems (from Unlimited Wishlist)."

The implementation is still in development, but is now ready for use in an aircraft.

Done

  • Basic class layout and code is working
  • First XML support (will be replaced with PropertyList format)
  • Lat/lon integration, very basic work
  • Most properties for input/output supported
  • Converted to PropertyList XML format
  • Multiple unit mixing
  • Fake attitude-only mode
  • Fake alignment (using a timer)

Todo

  1. Convert integration over to quaternions, add orientation integration.
  2. Fake a drift model (a better fake than the alignment...)
  3. Add a faked radio updating system
  4. More realistic alignment simulation (gimballed/strapdown differences, attitude-only mode (in air align) etc...)

Separately from the above implementation, there is a plan to model a Delco Carousel INS device (commonly found on many early jet airliners) using a combination of Nasal and the FMS code.

System 1: How to add to aircraft

Make a ins.xml file like in the example below. Then put it in your aircraft root folder.

Do not add it in the set file.

The left, center and right you can change name of and/or add/subtract more like them.

Result will be in property folder /ins

<?xml version="1.0"?>
<PropertyList>
   <left>
      <drift>

         <drift-rate type="double">.5</drift-rate>
         <pitcherr type="double">0</pitcherr>
         <rollerr type="double">0</rollerr>
      </drift>
      <nolatlonin type="string">true</nolatlonin>
      <lat type="double">0</lat>
      <long type="double">0</long>
      <trutrack type="double">0</trutrack>
      <magtrack type="double">0</magtrack>
      <north-velocity-fps type="double">0</north-velocity-fps>
      <east-velocity-fps type="double">0</east-velocity-fps>
      <ground-speed type="double">0</ground-speed>
      <vspeed type="double">0</vspeed>
      <truheading type="double">0</truheading>
      <magheading type="double">0</magheading>
      <heading-rate type="double">0</heading-rate>
      <pitch type="double">0</pitch>
      <pitch-rate type="double">0</pitch-rate>
      <roll-angle type="double">0</roll-angle>
      <roll-rate type="double">0</roll-rate>
      <navwork type="string">false</navwork>
      <attwork type="string">false</attwork>
      <mode type="string">off</mode>

   </left>
   

   <center>

      <drift>

         <drift-rate type="double">.5</drift-rate>
         <pitcherr type="double">0</pitcherr>
         <rollerr type="double">0</rollerr>
      </drift>
      <nolatlonin type="string">true</nolatlonin>
      <lat type="double">0</lat>
      <long type="double">0</long>
      <trutrack type="double">0</trutrack>
      <magtrack type="double">0</magtrack>
      <north-velocity-fps type="double">0</north-velocity-fps>
      <east-velocity-fps type="double">0</east-velocity-fps>
      <ground-speed type="double">0</ground-speed>
      <vspeed type="double">0</vspeed>
      <truheading type="double">0</truheading>
      <magheading type="double">0</magheading>
      <heading-rate type="double">0</heading-rate>
      <pitch type="double">0</pitch>
      <pitch-rate type="double">0</pitch-rate>
      <roll-angle type="double">0</roll-angle>
      <roll-rate type="double">0</roll-rate>
      <navwork type="string">false</navwork>
      <attwork type="string">false</attwork>
      <mode type="string">off</mode>

   </center>
   

   <right>

      <drift>

         <drift-rate type="double">.5</drift-rate>
         <pitcherr type="double">0</pitcherr>
         <rollerr type="double">0</rollerr>
      </drift>
      <nolatlonin type="string">true</nolatlonin>
      <lat type="double">0</lat>
      <long type="double">0</long>
      <trutrack type="double">0</trutrack>
      <magtrack type="double">0</magtrack>
      <north-velocity-fps type="double">0</north-velocity-fps>
      <east-velocity-fps type="double">0</east-velocity-fps>
      <ground-speed type="double">0</ground-speed>
      <vspeed type="double">0</vspeed>
      <truheading type="double">0</truheading>
      <magheading type="double">0</magheading>
      <heading-rate type="double">0</heading-rate>
      <pitch type="double">0</pitch>
      <pitch-rate type="double">0</pitch-rate>
      <roll-angle type="double">0</roll-angle>
      <roll-rate type="double">0</roll-rate>
      <navwork type="string">false</navwork>
      <attwork type="string">false</attwork>
      <mode type="string">off</mode>

   </right>
</PropertyList>

Add this nasal file ins.nas to the aircraft and set file:

var inses = [];
var limit = func (n,l,h) {
	if (n<l) n=l;
	if (n>h) n=h;
	return n;
}
var errmdl = {          # drift and radio update model
        node: nil,
        delete: nil,
};
errmdl.new = func (node="/ins/drift") {
        var m = { parents: [errmdl], };
        m.node = node;
        setprop(node~"/pitcherr", rand()-0.5);
        setprop(node~"/rollerr", rand()-0.5);
        settimer(func m.run(), 0);
        return m;
}
errmdl.del = func (funct = "true") {
        me.delete = funct;
}
errmdl.run = func {
        if (me.delete == nil) settimer(func me.run(), 1);
	else {
		if (me.delete != "true") call(me.delete, nil, var err = []);
	}
        interpolate(me.node~"/pitcherr", getprop(me.node~"/pitcherr")*(0.9+0.1*rand()), 1);
        interpolate(me.node~"/rollerr", getprop(me.node~"/rollerr")*(0.9+0.1*rand()), 1);
}
errmdl.reset = func {
	setprop(me.node~"/pitcherr", rand()-0.5);
        setprop(me.node~"/rollerr", rand()-0.5);
}
var ins = {
	dtime: .1,
	latfttodeg: 0,
	nvelfps: 0,
	evelfps: 0,
	track: 0,
	magtrack: 0,
	vspeed: 0,
	gndspd: 0,
	hdg: 0,
	maghdg: 0,
	dhdg: 0,
	pitch: 0,
	dpitch: 0,
	roll: 0,
	droll: 0,
	delete: nil,
	driftrate: 0,
	mode: "off",
	alignst: 0,
};
ins.new = func (name = "ins") {
	var m = { parents: [ins], };
	m.name = name;
	m.errmdl = errmdl.new("/ins/"~name~"/drift");
	m.lat = 0;
	m.long = 0;
	m.time = getprop("/sim/time/elapsed-sec");
	setprop("/ins/"~name~"/mode", "off");
	setprop("/ins/"~name~"/lat", getprop("/position/latitude-deg"));
	setprop("/ins/"~name~"/long", getprop("/position/longitude-deg"));
	settimer(func m.run(), 0);
	return m;
}
ins.del = func (funct = "true") {
	me.delete = funct;
}
ins.nav = func {
	# Integration
	me.nvelfps = getprop("/velocities/speed-north-fps");
	me.evelfps = getprop("/velocities/speed-east-fps");
	me.vspeed = 60*getprop("/velocities/vertical-speed-fps");
	me.gndspd = math.sqrt(me.nvelfps*me.nvelfps + me.evelfps*me.evelfps) * .592483801295896;
	me.track = math.atan2(me.evelfps,me.nvelfps) * 180 / math.pi;
	me.track = me.track < 0 ? me.track+360 : me.track;
	me.magtrack = me.track-getprop("/environment/magnetic-variation-deg");
	me.magtrack = me.magtrack < 0 ? me.magtrack + 360 : me.magtrack;
	me.magtrack = me.magtrack > 360 ? me.magtrack - 360 : me.magtrack;
	me.latfttodeg = 180/((getprop("/position/sea-level-radius-ft")+getprop("/position/altitude-ft"))*math.pi);
	me.lat += getprop("/velocities/speed-north-fps")*me.dtime*me.latfttodeg;
	me.long += getprop("/velocities/speed-east-fps")*me.dtime*me.latfttodeg/math.sin((90-me.lat)*math.pi/180);
	if (me.long > 180) me.long += -360;
	if (me.long < -180) me.long += 360;             # is this right?
	me.hdg = getprop("/orientation/heading-deg");
	me.maghdg = me.hdg - getprop("/environment/magnetic-variation-deg");
	me.dhdg = getprop("/orientation/yaw-rate-degps");
	me.pitch = getprop("/orientation/pitch-deg") + getprop("/ins/"~me.name~"/drift/pitcherr");
	me.dpitch = getprop("/orientation/pitch-rate-degps");
	me.roll = getprop("/orientation/roll-deg") + getprop("/ins/"~me.name~"/drift/rollerr");
	me.droll = getprop("/orientation/roll-rate-degps");

	setprop("/ins/"~me.name~"/lat", me.lat);
	setprop("/ins/"~me.name~"/long", me.long);
	setprop("/ins/"~me.name~"/trutrack", me.track);
	setprop("/ins/"~me.name~"/magtrack", me.magtrack);
	setprop("/ins/"~me.name~"/north-velocity-fps", me.nvelfps);
	setprop("/ins/"~me.name~"/east-velocity-fps", me.evelfps);
	setprop("/ins/"~me.name~"/ground-speed", me.gndspd);
	setprop("/ins/"~me.name~"/vspeed", me.vspeed);
	setprop("/ins/"~me.name~"/truheading", me.hdg);
	setprop("/ins/"~me.name~"/magheading", me.maghdg);
	setprop("/ins/"~me.name~"/heading-rate", me.dhdg);
	setprop("/ins/"~me.name~"/pitch", me.pitch);
	setprop("/ins/"~me.name~"/pitch-rate", me.dpitch);
	setprop("/ins/"~me.name~"/roll-angle", me.roll);
	setprop("/ins/"~me.name~"/roll-rate", me.droll);
}
ins.att = func {
	var elaptm = getprop("/sim/time/elapsed-sec") - me.alignst;
	me.dhdg = getprop("/orientation/yaw-rate-degps");
	me.pitch = getprop("/orientation/pitch-deg") + getprop("/ins/"~me.name~"/drift/pitcherr");
	me.dpitch = getprop("/orientation/pitch-rate-degps");
	me.roll = getprop("/orientation/roll-deg") + getprop("/ins/"~me.name~"/drift/rollerr");
	me.droll = getprop("/orientation/roll-rate-degps");
	setprop("/ins/"~me.name~"/heading-rate", me.dhdg);
	setprop("/ins/"~me.name~"/pitch", me.pitch);
	setprop("/ins/"~me.name~"/pitch-rate", me.dpitch);
	setprop("/ins/"~me.name~"/roll-angle", me.roll);
	setprop("/ins/"~me.name~"/roll-rate", me.droll);
}
ins.align = func {
	var alignt = me.time - me.alignst;	# time since alignment started
	if (getprop("/ins/"~me.name~"/nolatlonin") != "true") {	# TODO: make this only happen once, when data is entered.
		me.lat = getprop("/ins/"~me.name~"/lat");
		me.long = getprop("/ins/"~me.name~"/long");
	} else {
		me.lat = getprop("/position/latitude-deg");
		me.long = getprop("/position/longitude-deg");
	}
	me.nvelfps = 0;
	me.evelfps = 0;
	me.vspeed = 0;
	setprop("/ins/"~me.name~"/north-velocity-fps", 0);
	setprop("/ins/"~me.name~"/east-velocity-fps", 0);
	setprop("/ins/"~me.name~"/ground-speed", 0);
	setprop("/ins/"~me.name~"/vspeed", 0);
	if (alignt > 500) {	# you don't have navigation working until after 500 seconds
		setprop("/ins/"~me.name~"/navwork", "true");
		me.hdg = getprop("/orientation/heading-deg");
		me.maghdg = me.hdg - getprop("/environment/magnetic-variation-deg");
		setprop("/ins/"~me.name~"/truheading", me.hdg);
		setprop("/ins/"~me.name~"/magheading", me.maghdg);
		setprop("/ins/"~me.name~"/done-aligning", "true");
	} else {
		setprop("/ins/"~me.name~"/done-aligning", "false");
		setprop("/ins/"~me.name~"/navwork", "false");
	}
	if (alignt > 10) {	# you don't have attitude working until after 10 seconds
		setprop("/ins/"~me.name~"/attwork", "true");
		me.pitch = getprop("/orientation/pitch-deg") + getprop("/ins/"~me.name~"/drift/pitcherr");
		me.roll = getprop("/orientation/roll-deg") + getprop("/ins/"~me.name~"/drift/rollerr");
		setprop("/ins/"~me.name~"/pitch", me.pitch);
		setprop("/ins/"~me.name~"/roll-angle", me.roll);
	} else setprop("/ins/"~me.name~"/attwork", "false");
	setprop("/ins/"~me.name~"/heading-rate", me.dhdg);
	setprop("/ins/"~me.name~"/pitch-rate", me.dpitch);
	setprop("/ins/"~me.name~"/roll-rate", me.droll);

}
ins.run = func {
	if (me.delete == nil) settimer(func me.run(), 0);
	else {
		if (me.delete != "true") call(me.delete, nil, var err = []);
	}
	if (me.mode != getprop("/ins/"~me.name~"/mode")) {
		if (me.mode == "off") {
			me.errmdl.reset();
		}
		me.mode = getprop("/ins/"~me.name~"/mode");
		if (me.mode == "align" or me.mode == "align-then-nav") {
			me.alignst = me.time;	# you have just entered alignment
		}
		if (me.mode != "nav" and me.mode != "att" and me.mode != "align" and me.mode != "align-then-nav") {
			me.mode = "off";
			setprop("/ins/"~me.name~"/mode", "off");
		}
	}
	me.dtime = getprop("/sim/time/elapsed-sec") - me.time;
	me.time += me.dtime;
	if (getprop("/ins/"~me.name~"/mode") == "nav") me.nav();
	elsif (getprop("/ins/"~me.name~"/mode") == "att") me.att();
	elsif (getprop("/ins/"~me.name~"/mode") == "align") me.align();
	elsif (getprop("/ins/"~me.name~"/mode") == "align-then-nav") {
		if (getprop("/ins/"~me.name~"/done-aligning") == "true") me.nav();
		else me.align();
	}
}
var mix = {
#       lat: 0,     # waiting until quaternions are used
#       long: 0,
	nvelfps: 0,
	evelfps: 0,
	track: 0,
	magtrack: 0,
	vspeed: 0,
	gndspd: 0,
	hdg: 0,
	maghdg: 0,
	hdgx: 0,
	hdgy: 0,
	dhdg: 0,
	pitch: 0,
	dpitch: 0,
	roll: 0,
	rollx: 0,
	rolly: 0,
	droll: 0,
};
var run = func () {
	settimer(run, 0);
	mix.nvelfps = 0;
	mix.evelfps = 0;
	mix.vspeed = 0;
	mix.gndspd = 0;
	mix.hdgx = 0;
	mix.hdgy = 0;
	mix.dhdg = 0;
	mix.pitch = 0;
	mix.dpitch = 0;
	mix.roll = 0;
	mix.rollx = 0;
	mix.rolly = 0;
	mix.droll = 0;
	for(var i=0; i<size(inses); i+=1) {
		mix.nvelfps += inses[i].nvelfps;
		mix.evelfps += inses[i].evelfps;
		mix.vspeed += inses[i].vspeed;
		mix.gndspd += inses[i].gndspd;
		mix.hdgx += math.sin(inses[i].hdg*math.pi/180);
		mix.hdgy += math.cos(inses[i].hdg*math.pi/180);
		mix.dhdg += inses[i].dhdg;
		mix.pitch += inses[i].pitch;
		mix.dpitch += inses[i].dpitch;
		mix.rollx += math.sin(inses[i].roll*math.pi/180);
		mix.rolly += math.cos(inses[i].roll*math.pi/180);
		mix.droll += inses[i].droll;
	}
	mix.nvelfps *= 1/i;
	mix.evelfps *= 1/i;
	mix.track = math.atan2(mix.evelfps, mix.nvelfps)*180/math.pi;
	mix.track = mix.track < 0 ? mix.track+360 : mix.track;
	mix.magtrack = mix.track-getprop("/environment/magnetic-variation-deg");
	mix.magtrack = mix.magtrack < 0 ? mix.magtrack + 360 : mix.magtrack;
	mix.magtrack = mix.magtrack > 360 ? mix.magtrack - 360 : mix.magtrack;
	mix.vspeed *= 1/i;
	mix.gndspd *= 1/i;
	mix.hdg = math.atan2(mix.hdgx, mix.hdgy)*180/math.pi;
	mix.hdg = mix.hdg < 0 ? mix.hdg+360 : mix.hdg;
	mix.maghdg = mix.hdg-getprop("/environment/magnetic-variation-deg");
	mix.maghdg = mix.maghdg < 0 ? mix.maghdg + 360 : mix.maghdg;
	mix.maghdg = mix.maghdg > 360 ? mix.maghdg - 360 : mix.maghdg;
	mix.dhdg *= 1/i;
	mix.pitch *= 1/i;
	mix.dpitch *= 1/i;
	mix.roll = math.atan2(mix.rollx, mix.rolly)*180/math.pi;
	mix.droll *= 1/i;
#	setprop("/ins/lat", mix.lat);
#	setprop("/ins/long", mix.long);
	setprop("/ins/trutrack", mix.track);
	setprop("/ins/magtrack", mix.magtrack);
	setprop("/ins/north-velocity-fps", mix.nvelfps);
	setprop("/ins/east-velocity-fps", mix.evelfps);
	setprop("/ins/ground-speed", mix.gndspd);
	setprop("/ins/vspeed", mix.vspeed);
	setprop("/ins/truheading", mix.hdg);
	setprop("/ins/magheading", mix.maghdg);
	setprop("/ins/heading-rate", mix.dhdg);
	setprop("/ins/pitch", mix.pitch);
	setprop("/ins/pitch-rate", mix.dpitch);
	setprop("/ins/roll-angle", mix.roll);
	setprop("/ins/roll-rate", mix.droll);
}
var main = func () {
	var path = getprop("/sim/aircraft-dir") ~ "/ins.xml";
	io.read_properties(path, "/ins");
	var inschildren = props.globals.getChild("ins").getChildren();
	for(var i=0; i<size(inschildren); i+=1) {
		inses = append(inses, ins.new(inschildren[i].getName()));
	}
	settimer(run, 0);
}
setlistener("/sim/signals/fdm-initialized", main);

System 2

The Shuttle has a pretty detailed inertial navigation system simulation, including IMU alignment procedures, redundancy management and fault detection and Kalman filtering.

It's a hybrid Nasal/JSBSim system (since drift is slow) - it has an instantaneous part which computes the values used for guidance, and it has a slow Nasal simulation of the error evolution (drift, alignment, filtering, redundancy management...) which feeds into the instantaneous part.

References

(Sources linked to above)

  1. A good reference on aircraft navigation
  2. Information on Honeywell's Align-in-motion
  3. Detailed overview of INS systems
  4. Information on the IMU on Apollo 11 and gimbal lock

Additional:

  • [5] Wikipedia article on inertial navigation systems
  • [6] everything2.com has some more information
  • [7] Another source of good INS information and the sources of drift.
  • [8] Some information about the F20's INS
  • [9] Some information about INS, GPS, and a system for taxiing awareness using GPS (probably INS/GPS, actually.)

Also see