CLLocation.altitude under CarPlay reports 0.0 with a positive verticalAccuracy, and a second altitude outlier, both reproducible in Apple's Compass app

Hello,

I'm Greg, the developer of EV Dashboard, an app for electric vehicle owners. I'm extending it with features that help drivers understand their efficiency, including elevation and grade along a drive. It works for most of my beta testers, but two behaviors have me stuck, and they raise the same question: both hand my app an altitude that is wrong by hundreds or thousands of meters while reporting a small, positive verticalAccuracy, so I have no field I can test to distinguish a measurement from a value that is not one.

Both also reproduce in Apple's Compass app on a tester's phone, with none of my code in the path.

A 35-second screen recording he sent me, at the same spot in Eagan, Minnesota, shows Compass reading 889ft (the true elevation, 271m), then 5996ft for about a second, then 889ft again, and then 0ft at the moment the "CarPlay - AirPlay Connected" banner appears. Screenshots attached.

Setup in my app: one CLLocationManager per active scene, desiredAccuracy kCLLocationAccuracyBestForNavigation, standard location updates (not significant-change), authorization When In Use or Always depending on the tester. Elevation comes from CLLocation.altitude, with CMAltimeter relative altitude used to carry a known altitude between fixes.

Case 1: accessory fixes under CarPlay report altitude 0.0 with a positive verticalAccuracy (FB24778173)

On several vehicles, every fix whose sourceInformation.isProducedByAccessory is true arrives with altitude exactly 0.0 and a verticalAccuracy that is positive and constant for the whole session (19.0 on the cars I have traces from). In the same drives, the phone's own fixes read the true altitude, around 1,345m on one tester's route. ellipsoidalAltitude does not distinguish them either: it comes back as the geoid correction applied to the 0 (-16.5 to -17.4), so both fields agree on sea level.

This is not fleet-wide, which is what makes it testable. Other vehicles deliver real altitude on accessory fixes, on the same app build, with verticalAccuracy 9.5 and values that agree with the phone within a few meters. So it appears to depend on the head unit, and an app cannot ask iOS for the phone's own fix while CarPlay is connected.

The Compass recording above is the same behavior in a first-party app: 889ft before the connection, 0ft after it.

Case 2: a fix with an altitude about 1,550m too high

The same tester's iPhone has twice delivered fixes with altitude 1823.8 where the true elevation is about 271m, an error of roughly 1,552m, with verticalAccuracy 30.0 and horizontalAccuracy 5. These fixes report no speed and no course. The same value appeared on two separate days five days apart, at the same coordinates, identical to the tenth of a meter in both altitude (1823.8) and ellipsoidalAltitude (1796.4). Both times it was the first fix after a location manager started, with the vehicle at rest. Every other fix at that spot in his logs, 76 of them across a week, reads between 269.2m and 272.6m, most with verticalAccuracy 3.0. Compass showed 5996ft (1,827.6m) at that spot in the recording, within 4m of the value my app receives.

Because the value repeats exactly across days, it does not look like a measurement.

What I'm asking

  1. Are either of these known issues?
  2. My reading of the documentation is that a positive verticalAccuracy means the altitude is valid, with that value as one standard deviation. In both cases the error is 50 times the stated accuracy or more. Is there any supported way to recognize an altitude that CoreLocation did not measure? Should an accessory that supplies no altitude produce a negative verticalAccuracy, as an invalid altitude does elsewhere in CoreLocation?
  3. Is a fix with no speed and no course a reliable signal that it is not a live GNSS solution, and are cached or non-GNSS positions expected to carry an altitude at all?
  4. More generally, is there current guidance for obtaining elevation along a drive that I may have missed, particularly how CLLocation.altitude and CMAltimeter are intended to be combined, and what to expect from accessory-produced fixes under CarPlay?

Happy to provide traces, the recording, or sysdiagnose for either case.

Thanks, Greg

Thanks for the post, this is really interesting when the iPhone is connected to CarPlay the altitude reported is 0! Thanks for filing a bug. However I think is by design as you are getting the values from an external device?

I believe, when an iPhone connects to CarPlay, iOS often defers to the vehicle's onboard GPS. However, the quality of the data sent by the head unit depends entirely on the automaker's firmware. Many head units only compute a 2D fix (latitude/longitude) and simply zero out the altitude in the NMEA/protocol payload sent to the iPhone. So this is not the data from the iPhone is the external device. verticalAccuracy > 0 is the supported way to know if an altitude is valid. When the underlying source a CarPlay head unit or a Wi-Fi database, Core Location passes that lie up to your app.

https://www.apple.com/privacy/docs/Location_Services_White_Paper_Nov_2019.pdf

https://developer.apple.com/videos/play/wwdc2018/213/

Albert  WWDR

Hi Albert,

Thank you for the reply. That makes sense -- a 2D fix with the altitude zeroed out would explain what I see. What I can't tell from my side is whether the zero originates in the vehicle's firmware or somewhere in the handoff, and I'd rather establish that than guess.

Is there a way for me to see what the vehicle actually transmits? Are there any logs that capture the location payload coming from the head unit?

I think this is a worthy exploration, because impacted testers have reported that their own vehicle's native GUI (not CarPlay) is correctly reporting altitude -- that may be a 3D fix or it could be terrain lookup approach, I don't know. I would like to rule out something on the transport side from the vehicle to CarPlay.

I have a cooperative and enthusiastic EV owner community that would be glad to send any logs that I request. 😀

One more question, separate from where the zero comes from. While CarPlay is connected, is there any supported way for an app to obtain the device's own GNSS altitude? On an affected vehicle the phone knows the right value and my app receives what CoreLocation passes along from the vehicle, with no way to ask for the other.

Greg

@gburlingame

Thank you for your inquiries. I am not sufficiently familiar with external devices, their integration with CarPlay, or the associated log collection procedures. I recommend reviewing the location logs to identify the data source. I invite engineers from the relevant teams to join this thread and provide further assistance.

Albert  WWDR

Thanks Albert!

When you say review the location logs — which capture do you mean? On the Profiles and Logs page I see Location Services, CarPlay and iAP profiles, and I can't tell from the outside which, if any, records what the vehicle sends over the CarPlay link.

If you can point me at the right one or point in the direction of who to ask, I'll get a capture from an affected car and attach it to FB24778173.

@gburlingame The sysdiagnose will contain that information. You can upload that however is not a bug and right now is filed as a bug.

Albert  WWDR

Thank you Albert. I installed the location profile on my own device, and I'm studying my own sysdiagnose logs to start with.

I can see location services using a DEM. I suppose that there is no developer framework to access that DEM? I've already been considering implementing that as a solution to this problem and having an on-device DEM would certainly be handy. 😀

Altitude data from vehicles can be notoriously wrong, due to using different reference frames and then not bothering to report which they are using. Most of the time the issue is caused by the difference between WGS-84 and MSL (Mean Sea Level), and iOS then having to guess what's what.

While there isn't a way to ignore vehicle sourced data while connected to CarPlay, there is a way to detect if the data is coming from the phone GPS or not. CLLocation.CLLocationSourceInformation will tell you if the source is external.

Once you know that, the workaround would be:

  1. If you can, store the altitude you have before CarPlay is connected
  2. Once CarPlay connects, if you notice a jump in altitude, and the values you are getting are relatively sane, you can add/subtract as needed to get the correct(ish) value.
  3. If the values are crazy, jumping up and down, then you could use CMAltimeter instead, that is based on the phone barometer and will not rely on CarPlay.

CMAltimeter is likely to lag behind, but probably useful enough if you are in a car and not in a rocket, and if you can source a DEM, also probably predictable.

It is also likely to be somewhat wrong depending on how good the door seals are in the car. One can hope that if they have good enough door seals to throw a barometer off, then they have a proper nav system that gets the altitude correct.

-- S.Beck -- 7strikes.dev

Thank you!

That workaround is in fact what the app already does. It uses CLLocationSourceInformation to tell accessory fixes from the phone's own, keeps the last altitude it trusts, and carries it with CMAltimeter between fixes. It has been with my testers for abut one week and it works well. What I have learned since is where it gets hard, in case it is useful to anyone reading this later.

The anchor is the whole problem. CMAltimeter gives relative altitude, so the carry needs an absolute starting value, and the moment you would naturally take one — "before CarPlay connects" — often does not exist. On an affected car the app is frequently launched by the CarPlay connection itself, so its first fix is already the vehicle's. Across my fleet logs, 12 of 13 drives ended before CarPlay detached, so there is no phone fix at the end either. Some drives contain no phone fix at all. I now persist the last known altitude to a file and adopt it at launch if the car has not moved more than 50 m since it was written, which covers the common case of parking and driving off again.

Detecting "crazy" values is also harder than it looks, because the failure mode is not jumpy. An affected head unit reports exactly 0.0, with a positive verticalAccuracy, every second, for the whole drive. It is stable, plausible, and wrong. What catches it is not the altitude itself but the barometer: if the vehicle's altitude holds at exactly 0.0 while the phone's barometer says we climbed five meters and the car has left the spot, the reading is not a measurement. That rule cannot be built on 0.0 alone, since 0.0 is correct for a driver at sea level.

The phone is not always the safe source either. One tester's iPhone twice handed my app 1,823.8 m at a spot that is 271 m, with verticalAccuracy 30, the same value to the tenth of a meter five days apart — and Apple's Compass app showed the same number at the same spot. So the anchor needs its own sanity check; I now refuse a weak fix that disagrees with what the barometer has been tracking.

On door seals: in practice the carry has held up better than I expected. Re-anchor drifts on real drives are typically under two meters after twenty to thirty minutes of carrying.

On the DEM suggestion — agreed, and it is the direction I am heading. Measured against real drives, a 90m model agrees with a good phone fix to about 1.5m median. A worldwide set is about 7 GB, so it cannot ship inside the app, but regional tiles are a few megabytes.

One thing I noticed while capturing logs with Apple's Location Services profile, which may interest you: iOS already queries a DEM on every fix and logs the ground altitude with an uncertainty of 2.40m, alongside the vehicle's own value. It even appears to calibrate the barometric filter from it. None of that appears to be exposed to apps, which is a shame — it would be very handy right now to solve this problem.

Two little facts that might prove useful:

  • when you instantiate a location manager and start updates, the system will usually (but unfortunately not always) first send the latest cached location so the app has a starting point while a GPS fix is pending. You can detect this by checking the CLLocation timestamp against the time your app started updates. Although it wouldn’t be guaranteed (and maybe that’s worse than nothing) it could be another data point
  • although not the ideal solution, using significant location change API would keep sending you updates in the backgroun, and along with requestLocation() you could be keeping a tally of locations and altitudes before CarPlay gets involved.

Unfortunately without access to raw data and the DEM, tricks like this seem to be the only remaining solution.

The significant-change idea is interesting.

Today I cover the common case differently: the app writes its last known altitude to a small file on every fix, and at launch it adopts that value if the first fix shows the car is still within 50m of where the file was written. Park at home, drive off next morning, and the anchor is already there before CarPlay connects — no background execution needed.

What that cannot cover is the case where the car moved while the app was not running. Then the stored altitude belongs to somewhere else, the position check rejects it, and the drive starts with no anchor at all — which on an affected vehicle means no altitude for the whole trip. That is exactly the gap significant-change would fill, so the suggestion is a good one.

Two things give me pause, and I would rather ask than assume:

First, the authorization. Significant-change delivering in the background means asking for Always, and this app runs happily on When In Use today. Asking every owner for Always so that a minority with a particular head unit get a better altitude is a tough trade, unless it can be requested only when the app has actually detected one of those vehicles.

Second, the quality of what arrives. The fixes that come from that path tend to be the coarse Wi-Fi and cell derived ones, and those are the same class that handed the app 1,823.8m at a spot that is 271m. Your requestLocation() refinement is presumably the answer to that — but in practice, after a significant-change wake, does requestLocation() reliably produce a GNSS-quality fix, or does it often hand back the same coarse position that triggered the wake? If it is the latter, the tally would be full of altitudes I would not want to anchor on.

If it is the former, then the shape I would build is: wake on significant change, request one good fix, keep the altitude only if it is better than what is stored, and use the barometer on the next drive.

Authorization needed for the significant change is indeed a bummer. But Apple does recommend to start with the minimum authorization you require, and then escalate if needed. "Your car sucks!" as an excuse would be a valid reason for a user, IMO.

Significant Change will only return WiFi/Mobile tower based locations, and won't even have altitude. Once you have an approximate location, you can either use a DEM to estimate the altitude, or use requestLocation(), which is the same function as startUpdatingLocation() but with an internal 10 second timeout, which BTW is how long your app will be running after a significant change wake. If the system can get a fix, the last location you will be sent (you can get multiple) will hopefully be accurate. As you can always end up with a stale location, checking the timestamp is always a good idea.

All of the location updates for continuous updates, significant change, and requestLocation will come through the same callback. If the CLLocation.verticalAccuracy is zero, then the location is likely from a non-gps source.

BTW, have you looked into comparing ellipsoidalAltitude vs altitude?

In case the problems with some cars is not the quality of the data but using conflicting systems (WGS84 vs MSL)?

-- S.Beck -- 7strikes.dev

The timestamp is worth checking, though it catches a different failure than mine. A stale fix is an accurate altitude for wherever the phone used to be, which is exactly what a background wake with a cold receiver is likely to produce. But the fix that handed me 1,823.8 m was 1.2 seconds old: fresh, and wrong by 1,552 m. I log the age of every fix, unfortunately, it can't tell me whether the altitude is true.

On verticalAccuracy of zero meaning a non-GPS source: I counted across every trace I have. 125,694 fixes — 113,375 from vehicles, 12,319 from the phone — and not a single zero. Nineteen accessory fixes are negative, which is the documented invalid case. So on this fleet (all E-GMP vehicles) that signal rarely fires.

On WGS-84 versus MSL — I looked into that theory too, and the data does not support it. Altitude minus ellipsoidalAltitude comes out at the local geoid undulation on both phone and vehicle fixes — about 28 m in Massachusetts, 27 in Minnesota, 21 in Nova Scotia, 17 in Utah — so iOS is applying its geoid model consistently, and it applies it to the fabricated zero too: altitude 0.0 with ellipsoidalAltitude around −27, exactly what a car genuinely at sea level would produce. A frame mismatch would show as a roughly constant offset of one undulation rather than as zero. I log both values on every fix to catch that case, and no car in my fleet has shown it.

Curiously, the conversion is not always applied: on some cars the majority of fixes come through as 0.0 / 0.0, on others essentially all of them carry the undulation, and every car shows both. Same car, same place, both behaviours — so it looks like a timing or availability thing inside the conversion rather than anything the vehicle is doing.

CLLocation.altitude under CarPlay reports 0.0 with a positive verticalAccuracy, and a second altitude outlier, both reproducible in Apple's Compass app
 
 
Q