diff options
Diffstat (limited to 'dom/system/linux/GpsdLocationProvider.cpp')
-rw-r--r-- | dom/system/linux/GpsdLocationProvider.cpp | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/dom/system/linux/GpsdLocationProvider.cpp b/dom/system/linux/GpsdLocationProvider.cpp new file mode 100644 index 0000000000..34cd23c453 --- /dev/null +++ b/dom/system/linux/GpsdLocationProvider.cpp @@ -0,0 +1,446 @@ +/* -*- 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 |