keskiviikko 23. huhtikuuta 2014

SailfishOS: Accessing GPS

I suspect there are many others that have wanted to use GPS on their Jolla phone application but have not been successful in their attempts. In that case this should help you a bit.

If you are in hurry, just skip to the end, DBus code is there in all its ugliness. Everything in between is mostly background.

At the moment SailfishOS' Qt is something between versions 5.1 and 5.2. For GPS (or more accurately, the positioning information in general) this is unfortunate as old QtLocation subsystem no longer exists and new QtPositioning isn't there yet. After numerous web seaches I came in conclusion that following options are available right now:
  • Older QtLocation (possibly with custom import module). Reading between the lines I got the impression that if you wish to publish your app on Jolla's official shop this is very much frowned upon there.
  • Import custom module based on QtPositioning and use it. Slightly better, but this may later bite you back when phone is updated and official (yet possibly slightly different, as API is not yet completely stable) system interferes with your custom solution. So this also is frowned upon.
  • Hack to the DBUS and obtain location from there. Also frowned upon, but apparently least of these options. Of course no one bothered to mention how to do it...
Well, I went for the last option anyway, planning to upgrade to QtPositioning when it becomes properly available. It's way easier anyway than all this DBus mess.

Now, if you have read what I have written about my experience on working on Qt (previously zilch but at least I had browsed quickly some docs in the past), QML (maybe I had heard of it somewhere, not sure) or DBus (at least I had heard of it) you may already have guess how easy my progress to the depths of DBus was.

Well, actually, it wasn't as bad as I first expected, browsing the lower level documentation. As I had map drawing system running already, so I was getting familiar with "the Qt way". And since Qt appears to have everything and kitchen sink included (for the better or the worse, depending on your view), I figured there might be DBus too... And what do you know, there is.

Now, with that hurdle passed, proceed to the next. SailfishOS uses actually older implementation of Geoclue, references found here. The older version is very badly documented and examples are just about eually good - unfortunately even those are using C API (not available here, at least without more custom modules) that doesn't translate direcly to Qt/DBus combination. And Qt's DBus subsystem is very picky about defining call signatures (damn you Geoclue for using almost completely unnecessary structs to pass data I don't even care about - but Qt does, just enough to not to co-operate with my efforts). So good luck there, especially since Qt documentation mentions structs, arrays and dictionaries in passing - but completely neglects to mention how they should be used.

Did I remember to  tell you how frustrating some things were?

Even Google isn't very helpful when trying to find something this obscure. But eventually, after hundreds of searches (no, I am not kidding there) with varying search terms I managed to scrape together enough information to write my own code. Other search providers were even less helpful.

Eventually I scraped together enough information to work on. So, I implemented everything according to Geoclue and... nothing. No position information. Based on all the examples I had I should have updates coming, but no. Que debugging information, and when trying to send "GetPosition" I get result in tune of "Can't be done as there are no location providers" (I don't remember the exact phrase used). Wut? But this Maps app here has position coming in... Lightbulb moment...

At this point I had managed to gain enough knowledge of DBus in general and Qt's abilities so I could started to get creative. On terminal you can use dbus-monitor to, well, monitor what's going on dbus. So I started logging everything (if you want to do this too, log in file - there will be lots of events across all subsystems), started Maps app, and then quit soon after starting and started reading the logs.

And what do you know, the Maps app actually doesn't seem to use the Geoclue.Position interface; it accesses directly the Hybris (that's the GPS - and GPS only - provider) for location information (the interface for that is org.freedesktop.Geoclue.Providers.Hybris - so far I haven't found any relevant interface documentation about it or even Providers in general). So insert a short (and very very boring) montage of software hacking in all its glory here and soon the GPS position updates started coming in. Whew!

So, now for the actual implementation. As my project is in C++ (except the UI), the GPS interface is too. It shouldn't be too difficult to build quick adapter to access this from QML either, but I won't go to details of that here.

So, first your project. To use DBus you must have line
QT += dbus
In your .pro file.

It is best to implement your receiver as a separate class that provides its own interface for rest of your app, making it easy to switch to QtPositioning later when it becomes available. So the header (vastly simplified, relevant includes are left as exercise for the reader, that shouldn't be too difficult)

class PositionSource : public QObject {
    Q_OBJECT
public:
    PositionSource(QObject *parent);
public slots:
    void positionChangedDBUS(QDBusMessage);
private:
    QDBusInterface *masterInterface, *clientInterface, *hybrisInterface, *hereInterface;
};

And Cpp (again, I'm skipping everything but the most interesting parts). Also the Blogger's editor doesn't seem to like <> tags, so it may have messed something up during edits.

PositionSource::PositionSource(QObject *parent) :
    QObject(parent)
{
    masterInterface = new QDBusInterface("org.freedesktop.Geoclue.Master",
                                         "/org/freedesktop/Geoclue/Master",
                                         "",
                                         QDBusConnection::sessionBus() );
    QDBusReply<qdbusobjectpath> createReply = masterInterface->call("Create");
    qDebug() << "create:" << createReply.value().path();

    if (createReply.value().path().length() < 5) { // just quick-and-dirty check 
        qDebug() << "Could not create geoclue client";
        return;
    }

    clientInterface = new QDBusInterface( "org.freedesktop.Geoclue.Master",
                                          createReply.value().path(),
                                          "",
                                          QDBusConnection::sessionBus() );

    QDBusMessage msg;

     // GeoClue has "SetRequirements" function where you can specify your
     // app's requirements. Note that if they are too strict (like you
     // ask for precision no position provider can do), GeoClue doesn't
     // seem to fall back to "lesser" provider but will fail to give you
     // position information at all.
     // I am not sure if that was reason for original problems with
     // Geoclue.Position interface (found out about that only later, by
     // reading actual geoclue sources) but proceed with caution if you
     // want to try that.

     // SetRequirements: 
     //   accuracy level: GeoclueAccuracyLevel-enum
     //   minimum update time
     //   require updates
     //   resources, another enum again; (1<<2) should be GPS, (1<<10)-1 "anyting"

    clientinterface->call("SetRequirements", 1,0, true, 1023);


     // This most likely isn't very relevant when using Hybris directly, but I kept it anyway.
    clientInterface->call("PositionStart");


    // Then, register Hybris provider.
    hybrisInterface = new QDBusInterface("org.freedesktop.Geoclue.Providers.Hybris",
                                        "/org/freedesktop/Geoclue/Providers/Hybris",
                                        "",
                                        QDBusConnection::sessionBus() );


    msg = hybrisInterface->call("AddReference");
    if (msg.type() != QDBusMessage::ReplyMessage) { // quick and dirty check
        qDebug() << "Couldn't access Hybris : " << msg;
        status = POS_UNKNOWN;
        return;
    }

    // SetOptions parameter appears to be: (I'm guessing this implies auto-detect)
    // I have no idea on how to actually create the providerOpts so Qt would accept it
    // (and Qt documentation is *wonderfully* vague about that), but I'm leaving this
    // here in case someone has good ideas)
    // This call doesn't seem to be necessary anyway since position is obtained anyway,
    // but on different hardware this might have different results.
    //   array [
    //      dict: "gps-baudrate", 0;
    //      dict: "gps-device",   "";
    //   ]

    /*
    msg = hybrisInterface->call("SetOptions", providerOpts);
    if (msg.type() != QDBusMessage::ReplyMessage) {
        qDebug() << "Couldn't set hybris options : " << msg;
    }
    */

     // These are just for general information
    msg = hybrisInterface->call("GetProviderInfo");
    qDebug() << "  Hybris provider info: " << msg;

    msg = hybrisInterface->call("GetStatus");
    qDebug() << "  Hybris status: " << msg;

     // Now, if you really wanted to, you could do same for org.freedesktop.Geoclue.Providers.Here,
     // to get information from it. But then again, why bother? (especially if you are concerned about
     // privacy).

     // Then connect the PositionChanged signal. I'm connecting also the Geoclue.Position, but
     // so far it has never been triggered.
     // Note that after cold-start it may take a long while before you start getting position fix,
     // especially if you are indoors.
    QDBusConnection::sessionBus().connect("org.freedesktop.Geoclue.Position",
                                          "/org/freedesktop/Geoclue/Position",
                                          "",
                                          "PositionChanged", this, SLOT(positionChangedDBUS(QDBusMessage))); 
    QDBusConnection::sessionBus().connect("org.freedesktop.Geoclue.Providers.Hybris",
                                          "/org/freedesktop/Geoclue/Providers/Hybris",
                                          "",
                                          "PositionChanged", this, SLOT(positionChangedDBUS(QDBusMessage)));
}

// The message slot. This only spits out the data, parsing is up to you.
void PositionSource::positionChangedDBUS(QDBusMessage msg)
{
     // expecing int,int,double,double,double - flags (which data is valid), time, lat, lon, altitude

    QList<QVariant> list = msg.arguments();

    qDebug() << "PositionChanged:" << msg;
}

If you monitor the Geoclue.Position interface, there seem to be messages being sent but they certainly aren't being received by this code. No idea why.

Also note that you should also call RemoveReference for Providers (and possibly for client) when you're done (so in your adapter destructor). Unfortunately there is no way to explicitly tell GeoClue (as in client or provider) that you're done and it should shut down (at least no documented way on DBus interfaces) - RemoveReference only tells it that it may shut down if it pleases it to do so. On phone it appears that even after asking interface to close the GPS stays active even when no one is using it, eating battery. Kinda stupid interface design if you ask me. 

The code above should be runnable on the phone, but may not be in emulator (I didn't actually try, oh well). Since this is untested copy-paste from my code there may be some errors, sorry about those in advance.

Now, looking back at that mess I can make an educated guess why Jolla folks haven't been exactly forthcoming with this information, as this pretty much is accessing very very unofficial and possibly undocumented subsystems for location information. In this context this means that it may vanish without warning, but until QtPositioning rolls in, I unfortunately don't really see another (good) way to do this.

Update:
It seems that you can actually skip GeoClue master/clients completely (everything up to (down to?) "PositionStart" call in above code) and only create/connect to HybrisInterface. This seems to make position information lock slower (may take several minutes, up from 20 seconds or so I experience earlier while inside), but then RemoveReference on shutdown will actually stop GPS (well, make the icon go away at least) and hopefully also stop excessive battery drain after shutdown.
 

2 kommenttia:

  1. Thank you very much for sharing your experience with GPS in Salfish OS. It has been helping me a lot, though I have not yet achieved what I am aiming at.

    Would you mind me asking you two questions:

    1. In the code you provide, it seems to be no link between clientInterface and hybrisInterface, but their parallel connection to the PositionChanged signal. Why are those two interfaces needed eventually? Can't hybrisInterface do the job alone?

    2. I am confused between the GetPosition method and the PositionChanged signal. My understanding would be that the PositionChanged signal is returned following a call of the GetPosition method. Am I wrong here?

    Thanks in advance for your reply,
    Laurent Schumacher

    VastaaPoista
    Vastaukset
    1. 1. Normally you'd only need to use ClientInterface and it would take care of all the nasty details, like choosing which provider (hybris, here, etc) to use and just give you position data. For some reason that didn't work properly so I just used hybris (GPS) directly, and code related to client/master interfaces are actually unnecessary - it seems you can actually skip them.

      2. GetPosition is called directly to get current position anytime, like polling. PositionChanged is sent by GPS whenever position actually changes so you application doesn't need to constantly poll location (and thus use battery needlessly). Consequently I'd suggest always using PositionChanged unless you absolutely need to use GetPosition for some reason. AFAIK you should not need to call GetPosition to start updates, but it is useful to get initial position fix data (if there is any).

      So again, ideally you'd only need to register PositionChanged signal to the ClientInterface. Problem was that it never actually sent any updates, so I added the Hybris, which actually did deliver send updates, and ClientInterface was kind of leftover from earlier attempts (note that this was two or three system updates back, I haven't checked situation since - if ClientInterface has since started working you could drop Hybris and use ClientInterface like it was intended to be used).

      Poista