summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bluetoothgpsserver.cpp137
-rw-r--r--bluetoothgpsserver.h41
-rw-r--r--btgpsd.pro25
-rw-r--r--btgpsd.service11
-rw-r--r--main.cpp13
-rw-r--r--nmeasource.cpp619
-rw-r--r--nmeasource.h86
-rw-r--r--rpm/btgpsd.spec82
-rw-r--r--rpm/btgpsd.yaml30
9 files changed, 1044 insertions, 0 deletions
diff --git a/bluetoothgpsserver.cpp b/bluetoothgpsserver.cpp
new file mode 100644
index 0000000..efbddf0
--- /dev/null
+++ b/bluetoothgpsserver.cpp
@@ -0,0 +1,137 @@
+#include <QtCore/QDebug>
+
+#include "nmeasource.h"
+#include "bluetoothgpsserver.h"
+
+BluetoothGpsServer::BluetoothGpsServer(uint port, QObject *parent) :
+ QObject(parent), m_port(port), m_source(new NmeaSource(this)), m_server(0)
+{
+ connect(m_source, SIGNAL(dataReady(QString)), this, SLOT(sendData(QString)));
+}
+
+BluetoothGpsServer::~BluetoothGpsServer()
+{
+ stop();
+}
+
+void BluetoothGpsServer::start()
+{
+ if (m_server) {
+ return;
+ }
+
+ m_server = new QRfcommServer(this);
+ connect(m_server, SIGNAL(newConnection()), this, SLOT(acceptConnection()));
+ if (!m_server->listen(QBluetoothAddress(), m_port)) {
+ qWarning() << "Failed to start Bluetooth listener socket";
+ stop();
+ return;
+ }
+
+ quint8 serverPort = m_server->serverPort();
+
+ const QBluetoothUuid service_uuid(QLatin1String("2af0b21d-2d9d-43bd-9693-5d9235fa2033"));
+ const QBluetoothUuid gnss_profile_uuid(quint16(0x1135));
+ const QBluetoothUuid gnss_server_uuid(quint16(0x1136));
+
+ m_service.setServiceName("GPS");
+ m_service.setServiceDescription("GPS/NMEA emulator over Serial Port");
+ m_service.setServiceProvider("btgpsd");
+ m_service.setServiceUuid(service_uuid);
+
+ QBluetoothServiceInfo::Sequence classIds;
+ classIds.append(QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::SerialPort)));
+ classIds.append(QVariant::fromValue(gnss_server_uuid));
+ m_service.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classIds);
+
+ QBluetoothServiceInfo::Sequence browseGroupList;
+ browseGroupList.append(QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::PublicBrowseGroup)));
+ m_service.setAttribute(QBluetoothServiceInfo::BrowseGroupList, browseGroupList);
+
+ QBluetoothServiceInfo::Sequence protocolDescriptorList;
+ QBluetoothServiceInfo::Sequence protocol;
+
+ protocol.append(QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::L2cap)));
+ protocolDescriptorList.append(QVariant::fromValue(protocol));
+ protocol.clear();
+
+ protocol.append(QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::Rfcomm)));
+ protocol.append(QVariant::fromValue(serverPort));
+ protocolDescriptorList.append(QVariant::fromValue(protocol));
+ protocol.clear();
+
+ m_service.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList,
+ protocolDescriptorList);
+
+ QBluetoothServiceInfo::Sequence profileDescriptorList;
+ protocol.append(QVariant::fromValue(gnss_profile_uuid));
+ protocol.append(QVariant::fromValue<quint16>(0x100));
+ profileDescriptorList.append(QVariant::fromValue(protocol));
+ protocol.clear();
+
+ // Profile Descriptor list
+ m_service.setAttribute(0x0009, QVariant::fromValue(profileDescriptorList));
+
+ if (!m_service.registerService()) {
+ qWarning() << "Failed to register the Serial Port service";
+ }
+}
+
+void BluetoothGpsServer::stop()
+{
+ if (!m_server) {
+ return;
+ }
+
+ if (!m_service.unregisterService()) {
+ qWarning() << "Failed to unregister Serial Port service";
+ }
+
+ qDeleteAll(m_clients);
+ m_clients.clear();
+ m_source->stop();
+
+ delete m_server;
+ m_server = 0;
+}
+
+void BluetoothGpsServer::sendData(const QString &data)
+{
+ QByteArray text = data.toLatin1();
+ foreach (QBluetoothSocket *socket, m_clients) {
+ socket->write(text);
+ }
+}
+
+void BluetoothGpsServer::acceptConnection()
+{
+ qDebug() << "Incoming BT connection";
+ QBluetoothSocket *socket = m_server->nextPendingConnection();
+ if (!socket) {
+ qWarning() << "Actually, no incoming connection";
+ return;
+ }
+
+ connect(socket, SIGNAL(disconnected()), this, SLOT(handleDisconnection()));
+
+ m_clients.append(socket);
+ if (m_clients.size() == 1) {
+ // This was the first client; start listening to GPS
+ qDebug() << "Starting GPS for BT";
+ m_source->start();
+ }
+}
+
+void BluetoothGpsServer::handleDisconnection()
+{
+ QBluetoothSocket *socket = qobject_cast<QBluetoothSocket*>(sender());
+ Q_ASSERT(socket);
+
+ m_clients.removeOne(socket);
+ socket->deleteLater();
+
+ if (m_clients.isEmpty()) {
+ qDebug() << "Stopping GPS for BT";
+ m_source->stop();
+ }
+}
diff --git a/bluetoothgpsserver.h b/bluetoothgpsserver.h
new file mode 100644
index 0000000..db5c25b
--- /dev/null
+++ b/bluetoothgpsserver.h
@@ -0,0 +1,41 @@
+#ifndef BLUETOOTHGPSSERVER_H
+#define BLUETOOTHGPSSERVER_H
+
+#include <QtCore/QObject>
+#include <QtCore/QList>
+
+#include <QtBluetooth/QBluetoothSocket>
+#include <QtBluetooth/QBluetoothServiceInfo>
+#include <QtBluetooth/QRfcommServer>
+
+QT_USE_NAMESPACE_BLUETOOTH
+
+class NmeaSource;
+
+class BluetoothGpsServer : public QObject
+{
+ Q_OBJECT
+public:
+ explicit BluetoothGpsServer(uint m_port, QObject *parent = 0);
+ ~BluetoothGpsServer();
+
+signals:
+
+public slots:
+ void start();
+ void stop();
+
+protected slots:
+ void sendData(const QString& data);
+ void acceptConnection();
+ void handleDisconnection();
+
+private:
+ uint m_port;
+ NmeaSource* m_source;
+ QBluetoothServiceInfo m_service;
+ QRfcommServer* m_server;
+ QList<QBluetoothSocket*> m_clients;
+};
+
+#endif // BLUETOOTHGPSSERVER_H
diff --git a/btgpsd.pro b/btgpsd.pro
new file mode 100644
index 0000000..6d7bb50
--- /dev/null
+++ b/btgpsd.pro
@@ -0,0 +1,25 @@
+TEMPLATE = app
+CONFIG += console
+CONFIG -= app_bundle
+
+QT -= qml gui
+QT += bluetooth positioning
+
+SOURCES += main.cpp \
+ bluetoothgpsserver.cpp \
+ nmeasource.cpp
+
+HEADERS += \
+ bluetoothgpsserver.h \
+ nmeasource.h
+
+OTHER_FILES += \
+ rpm/btgpsd.yaml rpm/btgpsd.spec \
+ btgpsd.service
+
+target.path = /usr/sbin
+INSTALLS += target
+
+unit.path = /usr/lib/systemd/user/
+unit.files = btgpsd.service
+INSTALLS += unit
diff --git a/btgpsd.service b/btgpsd.service
new file mode 100644
index 0000000..72ca5a6
--- /dev/null
+++ b/btgpsd.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Bluetooth GPS daemon
+Requires=dbus.socket bluetooth.target
+After=dbus.socket bluetooth.target
+
+[Service]
+ExecStart=/usr/sbin/btgpsd
+Restart=always
+
+[Install]
+WantedBy=user-session.target
diff --git a/main.cpp b/main.cpp
new file mode 100644
index 0000000..d1e0194
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,13 @@
+#include <QtCore/QCoreApplication>
+
+#include "bluetoothgpsserver.h"
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication app(argc, argv);
+ QScopedPointer<BluetoothGpsServer> server(new BluetoothGpsServer(0));
+
+ server->start();
+
+ return app.exec();
+}
diff --git a/nmeasource.cpp b/nmeasource.cpp
new file mode 100644
index 0000000..e9bb222
--- /dev/null
+++ b/nmeasource.cpp
@@ -0,0 +1,619 @@
+#include <cmath>
+#include <QtCore/QDebug>
+#include <QtCore/QDir>
+#include <QtCore/QFile>
+#include <QtPositioning/QNmeaPositionInfoSource>
+
+#include "nmeasource.h"
+
+NmeaSource::NmeaSource(QObject *parent) :
+ QObject(parent)
+{
+ m_possrc = QGeoPositionInfoSource::createDefaultSource(this);
+ m_satsrc = QGeoSatelliteInfoSource::createDefaultSource(this);
+ if (m_possrc) {
+ connect(m_possrc, SIGNAL(positionUpdated(QGeoPositionInfo)),
+ this, SLOT(handlePosition(QGeoPositionInfo)));
+ } else {
+#ifdef QT_DEBUG
+ qDebug() << "Using test GPS source";
+ QNmeaPositionInfoSource* nmea = new QNmeaPositionInfoSource(QNmeaPositionInfoSource::SimulationMode, this);
+ QFile* file = new QFile(QDir::home().absoluteFilePath("nmea.txt"), this);
+ if (file->open(QIODevice::ReadOnly | QIODevice::Text)) {
+ nmea->setDevice(file);
+ m_possrc = nmea;
+ connect(m_possrc, SIGNAL(positionUpdated(QGeoPositionInfo)),
+ this, SLOT(handlePosition(QGeoPositionInfo)));
+ } else {
+ qWarning() << "Could not open test GPS source";
+ delete file;
+ delete nmea;
+ }
+#else
+ qWarning() << "No valid GPS position source!";
+#endif
+ }
+ if (m_satsrc) {
+ connect(m_satsrc, SIGNAL(satellitesInUseUpdated(QList<QGeoSatelliteInfo>)),
+ this, SLOT(handleSatsInUse(QList<QGeoSatelliteInfo>)));
+ connect(m_satsrc, SIGNAL(satellitesInViewUpdated(QList<QGeoSatelliteInfo>)),
+ this, SLOT(handleSatsInView(QList<QGeoSatelliteInfo>)));
+ }
+}
+
+NmeaSource::~NmeaSource()
+{
+}
+
+void NmeaSource::start()
+{
+ m_last_satsinview = 0;
+ if (m_possrc) {
+ m_possrc->startUpdates();
+ }
+ if (m_satsrc) {
+ m_satsrc->startUpdates();
+ }
+}
+
+void NmeaSource::stop()
+{
+ if (m_possrc) {
+ m_possrc->stopUpdates();
+ }
+ if (m_satsrc) {
+ m_satsrc->stopUpdates();
+ }
+}
+
+QString NmeaSource::formatFixMode(const QGeoCoordinate &coord)
+{
+ switch (coord.type()) {
+ case QGeoCoordinate::Coordinate3D:
+ return QString::fromLatin1("3");
+ case QGeoCoordinate::Coordinate2D:
+ return QString::fromLatin1("2");
+ default:
+ return QString::fromLatin1("1");
+ }
+}
+
+QString NmeaSource::formatTimestamp(const QTime& time)
+{
+ return time.toString("HHmmss.zzz");
+}
+
+QString NmeaSource::formatDatestamp(const QDate& date)
+{
+ return date.toString("ddMMyy");
+}
+
+QString NmeaSource::formatLatitude(const QGeoCoordinate& coord)
+{
+ if (coord.isValid()) {
+ QLatin1Char fill('0');
+ double degrees;
+ double minutes = modf(fabs(coord.latitude()), &degrees) * 60;
+ return QString::fromLatin1("%1%2")
+ .arg(static_cast<int>(degrees), 2, 10, fill)
+ .arg(minutes, 7, 'f', 4, fill);
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatLatitudeNS(const QGeoCoordinate& coord)
+{
+ if (coord.isValid()) {
+ if (coord.latitude() >= 0.0) {
+ return QLatin1String("N");
+ } else {
+ return QLatin1String("S");
+ }
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatLongitude(const QGeoCoordinate& coord)
+{
+ if (coord.isValid()) {
+ QLatin1Char fill('0');
+ double degrees;
+ double minutes = modf(fabs(coord.longitude()), &degrees) * 60;
+ return QString::fromLatin1("%1%2")
+ .arg(static_cast<int>(degrees), 3, 10, fill)
+ .arg(minutes, 7, 'f', 4, fill);
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatLongitudeEW(const QGeoCoordinate& coord)
+{
+ if (coord.isValid()) {
+ if (coord.longitude() >= 0.0) {
+ return QString::fromLatin1("E");
+ } else {
+ return QString::fromLatin1("W");
+ }
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatAltitude(const QGeoCoordinate& coord)
+{
+ if (coord.type() == QGeoCoordinate::Coordinate3D) {
+ return QString::number(coord.altitude(), 'f', 1);
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatAltitudeUnits(const QGeoCoordinate& coord)
+{
+ if (coord.type() == QGeoCoordinate::Coordinate3D) {
+ return QString::fromLatin1("M");
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatSpeedKnots(const QGeoPositionInfo& pos)
+{
+ if (pos.hasAttribute(QGeoPositionInfo::GroundSpeed)) {
+ double speed = pos.attribute(QGeoPositionInfo::GroundSpeed); // [m/s]
+ speed *= 1.943844; // [knots]
+ return QString::number(speed, 'f', 1);
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatSpeedKmH(const QGeoPositionInfo& pos)
+{
+ if (pos.hasAttribute(QGeoPositionInfo::GroundSpeed)) {
+ double speed = pos.attribute(QGeoPositionInfo::GroundSpeed); // [m/s]
+ speed *= 3.6; // [km/h]
+ return QString::number(speed, 'f', 1);
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatTrueCourse(const QGeoPositionInfo& pos)
+{
+ if (pos.hasAttribute(QGeoPositionInfo::Direction)) {
+ double course = pos.attribute(QGeoPositionInfo::Direction); // [deg]
+ return QString::number(course, 'f', 1);
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatMagCourse(const QGeoPositionInfo& pos)
+{
+ if (pos.hasAttribute(QGeoPositionInfo::Direction)
+ && pos.hasAttribute(QGeoPositionInfo::MagneticVariation)) {
+ double course = pos.attribute(QGeoPositionInfo::Direction); // [deg]
+ double magvar = pos.attribute(QGeoPositionInfo::MagneticVariation); // [+/-deg]
+ return QString::number(course + magvar, 'f', 1);
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatMagVariation(const QGeoPositionInfo& pos)
+{
+ if (pos.hasAttribute(QGeoPositionInfo::MagneticVariation)) {
+ double var = pos.attribute(QGeoPositionInfo::MagneticVariation); // [+/-deg]
+ return QString::number(fabs(var), 'f', 1);
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatMagVariationEW(const QGeoPositionInfo& pos)
+{
+ if (pos.hasAttribute(QGeoPositionInfo::MagneticVariation)) {
+ if (pos.attribute(QGeoPositionInfo::MagneticVariation) >= 0.0) {
+ return QString::fromLatin1("E");
+ } else {
+ return QString::fromLatin1("W");
+ }
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatHDOP(const QGeoPositionInfo& pos)
+{
+ if (pos.hasAttribute(QGeoPositionInfo::HorizontalAccuracy)) {
+ double eph = pos.attribute(QGeoPositionInfo::HorizontalAccuracy); // [m]
+ return QString::number(eph / H_UERE, 'f', 1);
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatVDOP(const QGeoPositionInfo& pos)
+{
+ if (pos.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) {
+ double epv = pos.attribute(QGeoPositionInfo::VerticalAccuracy); // [m]
+ return QString::number(epv / V_UERE, 'f', 1);
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatPDOP(const QGeoPositionInfo& pos)
+{
+
+ if (pos.hasAttribute(QGeoPositionInfo::HorizontalAccuracy)
+ && pos.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) {
+ double eph = pos.attribute(QGeoPositionInfo::HorizontalAccuracy); // [m]
+ double epv = pos.attribute(QGeoPositionInfo::VerticalAccuracy); // [m]
+ double epe = sqrt(eph * eph + epv * epv);
+ return QString::number(epe / P_UERE, 'f', 1);
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatError(const QGeoPositionInfo& pos,
+ PositionAccuracyDirection which)
+{
+ switch (which) {
+ case Spherical:
+ if (pos.hasAttribute(QGeoPositionInfo::HorizontalAccuracy)
+ && pos.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) {
+ double eph = pos.attribute(QGeoPositionInfo::HorizontalAccuracy); // [m]
+ double epv = pos.attribute(QGeoPositionInfo::VerticalAccuracy); // [m]
+ double epe = sqrt(eph * eph + epv * epv);
+ return QString::number(epe, 'f', 1);
+ } else {
+ return QString();
+ }
+ break;
+ case Horizontal:
+ if (pos.hasAttribute(QGeoPositionInfo::HorizontalAccuracy)) {
+ double eph = pos.attribute(QGeoPositionInfo::HorizontalAccuracy); // [m]
+ return QString::number(eph, 'f', 1);
+ } else {
+ return QString();
+ }
+ break;
+ case Vertical:
+ if (pos.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) {
+ double epv = pos.attribute(QGeoPositionInfo::VerticalAccuracy); // [m]
+ return QString::number(epv, 'f', 1);
+ } else {
+ return QString();
+ }
+ break;
+ }
+ return QString();
+}
+
+QString NmeaSource::formatErrorUnits(const QGeoPositionInfo& pos,
+ PositionAccuracyDirection which)
+{
+ switch (which) {
+ case Spherical:
+ if (pos.hasAttribute(QGeoPositionInfo::HorizontalAccuracy)
+ && pos.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) {
+ return QString::fromLatin1("M");
+ } else {
+ return QString();
+ }
+ break;
+ case Horizontal:
+ if (pos.hasAttribute(QGeoPositionInfo::HorizontalAccuracy)) {
+ return QString::fromLatin1("M");
+ } else {
+ return QString();
+ }
+ break;
+ case Vertical:
+ if (pos.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) {
+ return QString::fromLatin1("M");
+ } else {
+ return QString();
+ }
+ break;
+ }
+ return QString();
+}
+
+QString NmeaSource::formatSatPrn(const QGeoSatelliteInfo &sat)
+{
+ return QString::fromLatin1("%1").arg(sat.satelliteIdentifier(), 2, 10, QLatin1Char('0'));
+}
+
+QString NmeaSource::formatSatElev(const QGeoSatelliteInfo &sat)
+{
+ if (sat.hasAttribute(QGeoSatelliteInfo::Elevation)) {
+ int elv = lrint(sat.attribute(QGeoSatelliteInfo::Elevation));
+ return QString::fromLatin1("%1").arg(elv, 2, 10, QLatin1Char('0'));
+ } else {
+ return QString();
+ }
+}
+
+QString NmeaSource::formatSatAz(const QGeoSatelliteInfo &sat)
+{
+ if (sat.hasAttribute(QGeoSatelliteInfo::Azimuth)) {
+ int az = lrint(sat.attribute(QGeoSatelliteInfo::Azimuth));
+ return QString::fromLatin1("%1").arg(az, 3, 10, QLatin1Char('0'));
+ } else {
+ return QString();
+ }
+}
+QString NmeaSource::formatSatSnr(const QGeoSatelliteInfo &sat)
+{
+ int snr = sat.signalStrength();
+ if (snr >= 0) {
+ return QString::fromLatin1("%1").arg(snr, 2, 10, QLatin1Char('0'));
+ } else {
+ return QString();
+ }
+ return QString::fromLatin1("%1").arg(sat.signalStrength(), 2, 10, QLatin1Char('0'));
+}
+
+void NmeaSource::emitSentence(const QString &sentence)
+{
+ QByteArray data = sentence.toLatin1();
+ uint checksum = 0;
+ foreach (const char& c, data) {
+ checksum ^= c;
+ }
+
+#if 0
+ qDebug("%s", qPrintable(QString::fromLatin1("$%1*%2").arg(sentence).
+ arg(checksum, 2, 16, QLatin1Char('0'))));
+#endif
+
+ emit dataReady(QString::fromLatin1("$%1*%2\r\n")
+ .arg(sentence)
+ .arg(checksum, 2, 16, QLatin1Char('0')));
+}
+
+void NmeaSource::generateGPRMC(const QGeoPositionInfo &pos)
+{
+ QString sentence =
+ QString::fromLatin1("GPRMC,%1,%2,%3,%4,%5,%6,%7,%8,%9,%10,%11,%12")
+ // 1 = UTC of position fix
+ .arg(formatTimestamp(pos.timestamp().time()))
+ // 2 = Data status (A = Autonomous V = Warning)
+ .arg(pos.isValid() ? QLatin1Char('A') : QLatin1Char('V'))
+ // 3 = Latitude of fix
+ .arg(formatLatitude(pos.coordinate()))
+ // 4 = N or S
+ .arg(formatLatitudeNS(pos.coordinate()))
+ // 5 = Longitude of fix
+ .arg(formatLongitude(pos.coordinate()))
+ // 6 = E or W
+ .arg(formatLongitudeEW(pos.coordinate()))
+ // 7 = Speed over ground in knots
+ .arg(formatSpeedKnots(pos))
+ // 8 = True track made good in degrees
+ .arg(formatTrueCourse(pos))
+ // 9 = Date Stamp
+ .arg(formatDatestamp(pos.timestamp().date()))
+ // 10 = Magnetic variation in degrees
+ .arg(formatMagVariation(pos))
+ // 11 = E or W
+ .arg(formatMagVariationEW(pos))
+ // 12 = FAA mode indicator (A = Autonomous, N = Not valid)
+ .arg(pos.isValid() ? QLatin1Char('A') : QLatin1Char('N'))
+ ;
+ emitSentence(sentence);
+}
+
+void NmeaSource::generateGPGGA(const QGeoPositionInfo &pos)
+{
+ QString sentence =
+ QString::fromLatin1("GPGGA,%1,%2,%3,%4,%5,%6,%7,%8,%9,%10,%11,%12,%13,%14")
+ // 1 = UTC of position fix
+ .arg(formatTimestamp(pos.timestamp().time()))
+ // 2 = Latitude of fix
+ .arg(formatLatitude(pos.coordinate()))
+ // 3 = N or S
+ .arg(formatLatitudeNS(pos.coordinate()))
+ // 4 = Longitude of fix
+ .arg(formatLongitude(pos.coordinate()))
+ // 5 = E or W
+ .arg(formatLongitudeEW(pos.coordinate()))
+ // 6 = Fix quality (0 = invalid, 1 = GPS)
+ .arg(pos.isValid() ? QLatin1Char('1') : QLatin1Char('0'))
+ // 7 = Number of satellites being tracked
+ .arg(m_last_satsinview, 2, 10, QLatin1Char('0'))
+ // 8 = Horizontal dillusion of position
+ .arg(formatHDOP(pos))
+ // 9 = Altitude
+ .arg(formatAltitude(pos.coordinate()))
+ // 10 = Altitude units
+ .arg(formatAltitudeUnits(pos.coordinate()))
+ // 11 = Height of geoid (mean sea level) about WGS 84
+ .arg(QString()) // TODO We could calculate it. Usefulness debatable.
+ // 12 = Units
+ .arg(QString())
+ // 13 = Time in seconds since last DGPS update
+ .arg(QString())
+ // 14 = DGPS station ID number
+ .arg(QString())
+ ;
+ emitSentence(sentence);
+}
+
+void NmeaSource::generateGPGLL(const QGeoPositionInfo &pos)
+{
+ QString sentence =
+ QString::fromLatin1("GPGLL,%1,%2,%3,%4,%5,%6,%7")
+ // 1 = Latitude of fix
+ .arg(formatLatitude(pos.coordinate()))
+ // 2 = N or S
+ .arg(formatLatitudeNS(pos.coordinate()))
+ // 3 = Longitude of fix
+ .arg(formatLongitude(pos.coordinate()))
+ // 4 = E or W
+ .arg(formatLongitudeEW(pos.coordinate()))
+ // 5 = UTC of fix
+ .arg(formatTimestamp(pos.timestamp().time()))
+ // 6 = Data valid (A = Active, V = Void)
+ .arg(pos.isValid() ? QLatin1Char('A') : QLatin1Char('V'))
+ // 7 = Mode indicator (A = Autonomous, N = Not valid)
+ .arg(pos.isValid() ? QLatin1Char('A') : QLatin1Char('N'))
+ ;
+ emitSentence(sentence);
+}
+
+void NmeaSource::generateGPVTG(const QGeoPositionInfo &pos)
+{
+ QString sentence =
+ QString::fromLatin1("GPVTG,%1,%2,%3,%4,%5,%6,%7,%8")
+ // 1 = True track made good in degrees
+ .arg(formatTrueCourse(pos))
+ // 2 = T
+ .arg(pos.hasAttribute(QGeoPositionInfo::Direction)
+ ? QString::fromLatin1("T") : QString())
+ // 3 = Magnetic track made good
+ .arg(formatMagCourse(pos))
+ // 4 = M
+ .arg((pos.hasAttribute(QGeoPositionInfo::Direction)
+ && pos.hasAttribute(QGeoPositionInfo::MagneticVariation))
+ ? QString::fromLatin1("M") : QString())
+ // 5 = Speed over ground in knots
+ .arg(formatSpeedKnots(pos))
+ // 6 = N
+ .arg(pos.hasAttribute(QGeoPositionInfo::GroundSpeed)
+ ? QString::fromLatin1("N") : QString())
+ // 7 = Mode indicator (A = Autonomous, N = Not valid)
+ .arg(formatSpeedKmH(pos))
+ // 8 = K
+ .arg(pos.hasAttribute(QGeoPositionInfo::GroundSpeed)
+ ? QString::fromLatin1("K") : QString())
+ ;
+ emitSentence(sentence);
+}
+
+void NmeaSource::generatePGRME(const QGeoPositionInfo &pos)
+{
+ QString sentence =
+ QString::fromLatin1("PGRME,%1,%2,%3,%4,%5,%6")
+ // 1 = Horizontal error estimate
+ .arg(formatError(pos, Horizontal))
+ // 2 = Units
+ .arg(formatErrorUnits(pos, Horizontal))
+ // 3 = Vertical error estimate
+ .arg(formatError(pos, Vertical))
+ // 4 = Units
+ .arg(formatErrorUnits(pos, Vertical))
+ // 5 = Spherical error estimate
+ .arg(formatError(pos, Spherical))
+ // 6 = Units
+ .arg(formatErrorUnits(pos, Spherical))
+ ;
+ emitSentence(sentence);
+}
+
+void NmeaSource::generateGPGSA(const QGeoPositionInfo& pos, const QList<QGeoSatelliteInfo>& info)
+{
+ const int n = info.size();
+ QString sentence =
+ QString::fromLatin1("GPGSA,%1,%2,%3,%4,%5,%6,%7,%8,%9,%10,%11,%12,%13,%14,%15,%16,%17")
+ // 1 = Mode (A = Automatic)
+ .arg(QLatin1Char('A'))
+ // 2 = Fix mode
+ .arg(formatFixMode(pos.coordinate()))
+ // 3 - 14 = IDs of SVs used in position fix
+ .arg(n > 0 ? formatSatPrn(info[0]) : QString())
+ .arg(n > 1 ? formatSatPrn(info[1]) : QString())
+ .arg(n > 2 ? formatSatPrn(info[2]) : QString())
+ .arg(n > 3 ? formatSatPrn(info[3]) : QString())
+ .arg(n > 4 ? formatSatPrn(info[4]) : QString())
+ .arg(n > 5 ? formatSatPrn(info[5]) : QString())
+ .arg(n > 6 ? formatSatPrn(info[6]) : QString())
+ .arg(n > 7 ? formatSatPrn(info[7]) : QString())
+ .arg(n > 8 ? formatSatPrn(info[8]) : QString())
+ .arg(n > 9 ? formatSatPrn(info[9]) : QString())
+ .arg(n > 10 ? formatSatPrn(info[10]) : QString())
+ .arg(n > 11 ? formatSatPrn(info[11]) : QString())
+ // 15 = PDOP
+ .arg(formatPDOP(pos))
+ // 16 = HDOP
+ .arg(formatHDOP(pos))
+ // 17 = VDOP
+ .arg(formatVDOP(pos))
+ ;
+ emitSentence(sentence);
+}
+
+void NmeaSource::generateGPGSV(const QGeoPositionInfo& pos, const QList<QGeoSatelliteInfo>& info)
+{
+ const int nsats = info.size();
+ const int nmsgs = nsats / 4;
+ int i;
+
+ Q_UNUSED(pos);
+
+ for (i = 0; i < nmsgs; i++) {
+ int s = i * 4; // Start from this satellite index)
+ QString sentence =
+ QString::fromLatin1("GPGSV,%1,%2,%3,%4,%5,%6,%7,%8,%9,%10,%11,%12,%13,%14,%15,%16,%17,%18,%19")
+ // 1 = Number of sentences for full data
+ .arg(nmsgs)
+ // 2 = Sentence number
+ .arg(i)
+ // 3 = Total number of satellites in view
+ .arg(nsats)
+ // 4 - 14 = IDs of SVs used in position fix
+ .arg(nsats > s + 0 ? formatSatPrn(info[s + 0]) : QString())
+ .arg(nsats > s + 0 ? formatSatElev(info[s + 0]) : QString())
+ .arg(nsats > s + 0 ? formatSatAz(info[s + 0]) : QString())
+ .arg(nsats > s + 0 ? formatSatSnr(info[s + 0]) : QString())
+ .arg(nsats > s + 1 ? formatSatPrn(info[s + 1]) : QString())
+ .arg(nsats > s + 1 ? formatSatElev(info[s + 1]) : QString())
+ .arg(nsats > s + 1 ? formatSatAz(info[s + 1]) : QString())
+ .arg(nsats > s + 1 ? formatSatSnr(info[s + 1]) : QString())
+ .arg(nsats > s + 2 ? formatSatPrn(info[s + 2]) : QString())
+ .arg(nsats > s + 2 ? formatSatElev(info[s + 2]) : QString())
+ .arg(nsats > s + 2 ? formatSatAz(info[s + 2]) : QString())
+ .arg(nsats > s + 2 ? formatSatSnr(info[s + 2]) : QString())
+ .arg(nsats > s + 3 ? formatSatPrn(info[s + 3]) : QString())
+ .arg(nsats > s + 3 ? formatSatElev(info[s + 3]) : QString())
+ .arg(nsats > s + 3 ? formatSatAz(info[s + 3]) : QString())
+ .arg(nsats > s + 3 ? formatSatSnr(info[s + 3]) : QString())
+ ;
+ emitSentence(sentence);
+ }
+}
+
+void NmeaSource::handlePosition(const QGeoPositionInfo &pos)
+{
+ generateGPRMC(pos);
+
+ generateGPGGA(pos);
+ generateGPGLL(pos);
+ generateGPVTG(pos);
+ generatePGRME(pos); //Estimated Position Error
+
+ //PGRMZ = Altitude info, PGRMM = Map datum?
+ //generateGPRMB?
+ //generateGPBOD?
+ //generateGPRTE?
+
+ m_last_pos = pos;
+}
+
+void NmeaSource::handleSatsInUse(const QList<QGeoSatelliteInfo> &info)
+{
+ generateGPGSA(m_last_pos, info);
+}
+
+void NmeaSource::handleSatsInView(const QList<QGeoSatelliteInfo> &info)
+{
+ generateGPGSV(m_last_pos, info);
+ m_last_satsinview = info.count();
+}
diff --git a/nmeasource.h b/nmeasource.h
new file mode 100644
index 0000000..b874b7b
--- /dev/null
+++ b/nmeasource.h
@@ -0,0 +1,86 @@
+#ifndef NMEASOURCE_H
+#define NMEASOURCE_H
+
+#include <QtCore/QObject>
+#include <QtCore/QString>
+
+#include <QtPositioning/QGeoPositionInfo>
+#include <QtPositioning/QGeoPositionInfoSource>
+#include <QtPositioning/QGeoSatelliteInfoSource>
+
+class NmeaSource : public QObject
+{
+ Q_OBJECT
+public:
+ explicit NmeaSource(QObject *parent = 0);
+ ~NmeaSource();
+
+signals:
+ void dataReady(const QString& data);
+
+public slots:
+ void start();
+ void stop();
+
+protected:
+ static const double H_UERE = 15.0;
+ static const double V_UERE = 23.0;
+ static const double P_UERE = 19.0;
+
+ enum PositionAccuracyDirection {
+ Spherical,
+ Horizontal,
+ Vertical
+ };
+
+ static QString formatFixMode(const QGeoCoordinate& coord);
+ static QString formatTimestamp(const QTime& time);
+ static QString formatDatestamp(const QDate& date);
+ static QString formatLatitude(const QGeoCoordinate& coord);
+ static QString formatLatitudeNS(const QGeoCoordinate& coord);
+ static QString formatLongitude(const QGeoCoordinate& coord);
+ static QString formatLongitudeEW(const QGeoCoordinate& coord);
+ static QString formatAltitude(const QGeoCoordinate& coord);
+ static QString formatAltitudeUnits(const QGeoCoordinate& coord);
+ static QString formatSpeedKnots(const QGeoPositionInfo& pos);
+ static QString formatSpeedKmH(const QGeoPositionInfo& pos);
+ static QString formatTrueCourse(const QGeoPositionInfo& pos);
+ static QString formatMagCourse(const QGeoPositionInfo& pos);
+ static QString formatMagVariation(const QGeoPositionInfo& pos);
+ static QString formatMagVariationEW(const QGeoPositionInfo& pos);
+ static QString formatHDOP(const QGeoPositionInfo& pos);
+ static QString formatVDOP(const QGeoPositionInfo& pos);
+ static QString formatPDOP(const QGeoPositionInfo& pos);
+ static QString formatError(const QGeoPositionInfo& pos,
+ PositionAccuracyDirection which = Spherical);
+ static QString formatErrorUnits(const QGeoPositionInfo& pos,
+ PositionAccuracyDirection which = Spherical);
+ static QString formatSatPrn(const QGeoSatelliteInfo& sat);
+ static QString formatSatElev(const QGeoSatelliteInfo& sat);
+ static QString formatSatAz(const QGeoSatelliteInfo& sat);
+ static QString formatSatSnr(const QGeoSatelliteInfo& sat);
+
+ void emitSentence(const QString& sentence);
+
+ void generateGPRMC(const QGeoPositionInfo& pos);
+ void generateGPGGA(const QGeoPositionInfo& pos);
+ void generateGPGLL(const QGeoPositionInfo& pos);
+ void generateGPVTG(const QGeoPositionInfo& pos);
+ void generatePGRME(const QGeoPositionInfo& pos);
+
+ void generateGPGSA(const QGeoPositionInfo& pos, const QList<QGeoSatelliteInfo>& info);
+ void generateGPGSV(const QGeoPositionInfo& pos, const QList<QGeoSatelliteInfo>& info);
+
+protected slots:
+ void handlePosition(const QGeoPositionInfo& pos);
+ void handleSatsInUse(const QList<QGeoSatelliteInfo>& info);
+ void handleSatsInView(const QList<QGeoSatelliteInfo>& info);
+
+private:
+ QGeoPositionInfoSource *m_possrc;
+ QGeoSatelliteInfoSource *m_satsrc;
+ QGeoPositionInfo m_last_pos;
+ int m_last_satsinview;
+};
+
+#endif // NMEASOURCE_H
diff --git a/rpm/btgpsd.spec b/rpm/btgpsd.spec
new file mode 100644
index 0000000..b9ec983
--- /dev/null
+++ b/rpm/btgpsd.spec
@@ -0,0 +1,82 @@
+#
+# Do NOT Edit the Auto-generated Part!
+# Generated by: spectacle version 0.27
+#
+
+Name: btgpsd
+
+# >> macros
+# << macros
+
+%{!?qtc_qmake:%define qtc_qmake %qmake}
+%{!?qtc_qmake5:%define qtc_qmake5 %qmake5}
+%{!?qtc_make:%define qtc_make make}
+%{?qtc_builddir:%define _builddir %qtc_builddir}
+Summary: A Bluetooth GPS server daemon
+Version: 0.1
+Release: 1
+Group: Communications/Bluetooth
+License: GPL
+URL: http://example.org/
+Source0: %{name}-%{version}.tar.bz2
+Source100: btgpsd.yaml
+Requires: systemd
+Requires: systemd-user-session-targets
+BuildRequires: pkgconfig(sailfishapp) >= 1.0.2
+BuildRequires: pkgconfig(Qt5Core)
+BuildRequires: pkgconfig(Qt5Positioning)
+BuildRequires: pkgconfig(Qt5Bluetooth)
+
+%description
+btgpsd emulates a NMEA-compatible Bluetooth GPS from data obtained using
+the Qt Positioning framework
+
+
+%prep
+%setup -q -n %{name}-%{version}
+
+# >> setup
+# << setup
+
+%build
+# >> build pre
+# << build pre
+
+%qtc_qmake5
+
+%qtc_make %{?_smp_mflags}
+
+# >> build post
+# << build post
+
+%install
+rm -rf %{buildroot}
+# >> install pre
+# << install pre
+%qmake5_install
+
+# >> install post
+# << install post
+
+%post
+# >> post
+if [ "$1" -ge 1 ]; then
+systemctl-user daemon-reload || :
+systemctl-user restart btgpsd.service || :
+fi
+# << post
+
+%postun
+# >> postun
+if [ "$1" -eq 0 ]; then
+systemctl-user stop btgpsd.service || :
+systemctl-user daemon-reload || :
+fi
+# << postun
+
+%files
+%defattr(-,root,root,-)
+%{_sbindir}/btgpsd
+%{_libdir}/systemd/user/btgpsd.service
+# >> files
+# << files
diff --git a/rpm/btgpsd.yaml b/rpm/btgpsd.yaml
new file mode 100644
index 0000000..d0a4772
--- /dev/null
+++ b/rpm/btgpsd.yaml
@@ -0,0 +1,30 @@
+Name: btgpsd
+Summary: A Bluetooth GPS server daemon
+Version: 0.1
+Release: 1
+Group: Communications/Bluetooth
+URL: http://example.org/
+License: GPL
+Sources:
+- '%{name}-%{version}.tar.bz2'
+Description: |
+ btgpsd emulates a NMEA-compatible Bluetooth GPS from data obtained using
+ the Qt Positioning framework
+Configure: none
+# The qtc5 builder inserts macros to allow QtCreator to have fine
+# control over qmake/make execution
+Builder: qtc5
+
+PkgConfigBR:
+ - sailfishapp >= 1.0.2
+ - Qt5Core
+ - Qt5Positioning
+ - Qt5Bluetooth
+
+Requires:
+ - systemd
+ - systemd-user-session-targets
+
+Files:
+ - '%{_sbindir}/btgpsd'
+ - '%{_libdir}/systemd/user/btgpsd.service'