Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "GpsdLocationProvider.h"
#include <errno.h>
#include <gps.h>
#include "MLSFallback.h"
#include "mozilla/Atomics.h"
#include "mozilla/FloatingPoint.h"
#include "mozilla/LazyIdleThread.h"
#include "mozilla/dom/GeolocationPositionErrorBinding.h"
#include "GeolocationPosition.h"
#include "nsProxyRelease.h"
#include "nsThreadUtils.h"
#include "prtime.h"
namespace mozilla {
namespace dom {
//
// MLSGeolocationUpdate
//
/**
* |MLSGeolocationUpdate| provides a fallback if gpsd is not supported.
*/
class GpsdLocationProvider::MLSGeolocationUpdate final
: public nsIGeolocationUpdate {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIGEOLOCATIONUPDATE
explicit MLSGeolocationUpdate(nsIGeolocationUpdate* aCallback);
protected:
~MLSGeolocationUpdate() = default;
private:
nsCOMPtr<nsIGeolocationUpdate> mCallback;
};
GpsdLocationProvider::MLSGeolocationUpdate::MLSGeolocationUpdate(
nsIGeolocationUpdate* aCallback)
: mCallback(aCallback) {
MOZ_ASSERT(mCallback);
}
// nsISupports
//
NS_IMPL_ISUPPORTS(GpsdLocationProvider::MLSGeolocationUpdate,
nsIGeolocationUpdate);
// nsIGeolocationUpdate
//
NS_IMETHODIMP
GpsdLocationProvider::MLSGeolocationUpdate::Update(
nsIDOMGeoPosition* aPosition) {
nsCOMPtr<nsIDOMGeoPositionCoords> coords;
aPosition->GetCoords(getter_AddRefs(coords));
if (!coords) {
return NS_ERROR_FAILURE;
}
return mCallback->Update(aPosition);
}
NS_IMETHODIMP
GpsdLocationProvider::MLSGeolocationUpdate::NotifyError(uint16_t aError) {
return mCallback->NotifyError(aError);
}
//
// UpdateRunnable
//
class GpsdLocationProvider::UpdateRunnable final : public Runnable {
public:
UpdateRunnable(
const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider,
nsIDOMGeoPosition* aPosition)
: Runnable("GpsdU"),
mLocationProvider(aLocationProvider),
mPosition(aPosition) {
MOZ_ASSERT(mLocationProvider);
MOZ_ASSERT(mPosition);
}
// nsIRunnable
//
NS_IMETHOD Run() override {
mLocationProvider->Update(mPosition);
return NS_OK;
}
private:
nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider;
RefPtr<nsIDOMGeoPosition> mPosition;
};
//
// NotifyErrorRunnable
//
class GpsdLocationProvider::NotifyErrorRunnable final : public Runnable {
public:
NotifyErrorRunnable(
const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider,
int aError)
: Runnable("GpsdNE"),
mLocationProvider(aLocationProvider),
mError(aError) {
MOZ_ASSERT(mLocationProvider);
}
// nsIRunnable
//
NS_IMETHOD Run() override {
mLocationProvider->NotifyError(mError);
return NS_OK;
}
private:
nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider;
int mError;
};
//
// PollRunnable
//
/**
* |PollRunnable| does the main work of processing GPS data received
* from gpsd. libgps blocks while polling, so this runnable has to be
* executed on it's own thread. To cancel the poll runnable, invoke
* |StopRunning| and |PollRunnable| will stop within a reasonable time
* frame.
*/
class GpsdLocationProvider::PollRunnable final : public Runnable {
public:
PollRunnable(
const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider)
: Runnable("GpsdP"),
mLocationProvider(aLocationProvider),
mRunning(true) {
MOZ_ASSERT(mLocationProvider);
}
static bool IsSupported() {
return GPSD_API_MAJOR_VERSION >= 5 && GPSD_API_MAJOR_VERSION <= 12;
}
bool IsRunning() const { return mRunning; }
void StopRunning() { mRunning = false; }
// nsIRunnable
//
NS_IMETHOD Run() override {
int err;
switch (GPSD_API_MAJOR_VERSION) {
case 5 ... 12:
err = PollLoop5();
break;
default:
err = GeolocationPositionError_Binding::POSITION_UNAVAILABLE;
break;
}
if (err) {
NS_DispatchToMainThread(
MakeAndAddRef<NotifyErrorRunnable>(mLocationProvider, err));
}
mLocationProvider = nullptr;
return NS_OK;
}
protected:
int PollLoop5() {
#if GPSD_API_MAJOR_VERSION >= 5 && GPSD_API_MAJOR_VERSION <= 12
static const int GPSD_WAIT_TIMEOUT_US =
1000000; /* us to wait for GPS data */
struct gps_data_t gpsData;
auto res = gps_open(nullptr, nullptr, &gpsData);
if (res < 0) {
return ErrnoToError(errno);
}
gps_stream(&gpsData, WATCH_ENABLE | WATCH_JSON, NULL);
int err = 0;
// nsGeoPositionCoords will convert NaNs to null for optional properties of
// the JavaScript Coordinates object.
double lat = 0;
double lon = 0;
double alt = UnspecifiedNaN<double>();
double hError = 0;
double vError = UnspecifiedNaN<double>();
double heading = UnspecifiedNaN<double>();
double speed = UnspecifiedNaN<double>();
while (IsRunning()) {
errno = 0;
auto hasGpsData = gps_waiting(&gpsData, GPSD_WAIT_TIMEOUT_US);
if (errno) {
err = ErrnoToError(errno);
break;
}
if (!hasGpsData) {
continue; /* woke up from timeout */
}
# if GPSD_API_MAJOR_VERSION >= 7
res = gps_read(&gpsData, nullptr, 0);
# else
res = gps_read(&gpsData);
# endif
if (res < 0) {
err = ErrnoToError(errno);
break;
} else if (!res) {
continue; /* no data available */
}
# if GPSD_API_MAJOR_VERSION < 10
if (gpsData.status == STATUS_NO_FIX) {
continue;
}
# endif
switch (gpsData.fix.mode) {
case MODE_3D:
double galt;
# if GPSD_API_MAJOR_VERSION >= 9
galt = gpsData.fix.altMSL;
# else
galt = gpsData.fix.altitude;
# endif
if (!std::isnan(galt)) {
alt = galt;
}
[[fallthrough]];
case MODE_2D:
if (!std::isnan(gpsData.fix.latitude)) {
lat = gpsData.fix.latitude;
}
if (!std::isnan(gpsData.fix.longitude)) {
lon = gpsData.fix.longitude;
}
if (!std::isnan(gpsData.fix.epx) && !std::isnan(gpsData.fix.epy)) {
hError = std::max(gpsData.fix.epx, gpsData.fix.epy);
} else if (!std::isnan(gpsData.fix.epx)) {
hError = gpsData.fix.epx;
} else if (!std::isnan(gpsData.fix.epy)) {
hError = gpsData.fix.epy;
}
if (!std::isnan(gpsData.fix.epv)) {
vError = gpsData.fix.epv;
}
if (!std::isnan(gpsData.fix.track)) {
heading = gpsData.fix.track;
}
if (!std::isnan(gpsData.fix.speed)) {
speed = gpsData.fix.speed;
}
break;
default:
continue; // There's no useful data in this fix; continue.
}
NS_DispatchToMainThread(MakeAndAddRef<UpdateRunnable>(
mLocationProvider,
new nsGeoPosition(lat, lon, alt, hError, vError, heading, speed,
PR_Now() / PR_USEC_PER_MSEC)));
}
gps_stream(&gpsData, WATCH_DISABLE, NULL);
gps_close(&gpsData);
return err;
#else
return GeolocationPositionError_Binding::POSITION_UNAVAILABLE;
#endif // GPSD_MAJOR_API_VERSION
}
static int ErrnoToError(int aErrno) {
switch (aErrno) {
case EACCES:
[[fallthrough]];
case EPERM:
[[fallthrough]];
case EROFS:
return GeolocationPositionError_Binding::PERMISSION_DENIED;
case ETIME:
[[fallthrough]];
case ETIMEDOUT:
return GeolocationPositionError_Binding::TIMEOUT;
default:
return GeolocationPositionError_Binding::POSITION_UNAVAILABLE;
}
}
private:
nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider;
Atomic<bool> mRunning;
};
//
// GpsdLocationProvider
//
const uint32_t GpsdLocationProvider::GPSD_POLL_THREAD_TIMEOUT_MS = 5000;
GpsdLocationProvider::GpsdLocationProvider() {}
GpsdLocationProvider::~GpsdLocationProvider() {}
void GpsdLocationProvider::Update(nsIDOMGeoPosition* aPosition) {
if (!mCallback || !mPollRunnable) {
return; // not initialized or already shut down
}
if (mMLSProvider) {
/* We got a location from gpsd, so let's cancel our MLS fallback. */
mMLSProvider->Shutdown();
mMLSProvider = nullptr;
}
mCallback->Update(aPosition);
}
void GpsdLocationProvider::NotifyError(int aError) {
if (!mCallback) {
return; // not initialized or already shut down
}
if (!mMLSProvider) {
/* With gpsd failed, we restart MLS. It will be canceled once we
* get another location from gpsd.
*/
mMLSProvider = MakeAndAddRef<MLSFallback>();
mMLSProvider->Startup(new MLSGeolocationUpdate(mCallback));
}
mCallback->NotifyError(aError);
}
// nsISupports
//
NS_IMPL_ISUPPORTS(GpsdLocationProvider, nsIGeolocationProvider)
// nsIGeolocationProvider
//
NS_IMETHODIMP
GpsdLocationProvider::Startup() {
if (!PollRunnable::IsSupported()) {
return NS_OK; // We'll fall back to MLS.
}
if (mPollRunnable) {
return NS_OK; // already running
}
RefPtr<PollRunnable> pollRunnable =
MakeAndAddRef<PollRunnable>(nsMainThreadPtrHandle<GpsdLocationProvider>(
new nsMainThreadPtrHolder<GpsdLocationProvider>("GpsdLP", this)));
// Use existing poll thread...
RefPtr<LazyIdleThread> pollThread = mPollThread;
// ... or create a new one.
if (!pollThread) {
pollThread = MakeAndAddRef<LazyIdleThread>(GPSD_POLL_THREAD_TIMEOUT_MS,
"Gpsd poll thread",
LazyIdleThread::ManualShutdown);
}
auto rv = pollThread->Dispatch(pollRunnable, NS_DISPATCH_NORMAL);
if (NS_FAILED(rv)) {
return rv;
}
mPollRunnable = pollRunnable.forget();
mPollThread = pollThread.forget();
return NS_OK;
}
NS_IMETHODIMP
GpsdLocationProvider::Watch(nsIGeolocationUpdate* aCallback) {
mCallback = aCallback;
/* The MLS fallback will kick in after a few seconds if gpsd
* doesn't provide location information within time. Once we
* see the first message from gpsd, the fallback will be
* disabled in |Update|.
*/
mMLSProvider = MakeAndAddRef<MLSFallback>();
mMLSProvider->Startup(new MLSGeolocationUpdate(aCallback));
return NS_OK;
}
NS_IMETHODIMP
GpsdLocationProvider::Shutdown() {
if (mMLSProvider) {
mMLSProvider->Shutdown();
mMLSProvider = nullptr;
}
if (!mPollRunnable) {
return NS_OK; // not running
}
mPollRunnable->StopRunning();
mPollRunnable = nullptr;
return NS_OK;
}
NS_IMETHODIMP
GpsdLocationProvider::SetHighAccuracy(bool aHigh) { return NS_OK; }
} // namespace dom
} // namespace mozilla