From 3b5fc081c8d22fac73db0fbe63eb2058595d43f6 Mon Sep 17 00:00:00 2001 From: Javier Date: Sun, 29 Jun 2014 17:32:54 +0200 Subject: initial import --- bluetoothgpsserver.cpp | 137 +++++++++++ bluetoothgpsserver.h | 41 ++++ btgpsd.pro | 25 ++ btgpsd.service | 11 + main.cpp | 13 ++ nmeasource.cpp | 619 +++++++++++++++++++++++++++++++++++++++++++++++++ nmeasource.h | 86 +++++++ rpm/btgpsd.spec | 82 +++++++ rpm/btgpsd.yaml | 30 +++ 9 files changed, 1044 insertions(+) create mode 100644 bluetoothgpsserver.cpp create mode 100644 bluetoothgpsserver.h create mode 100644 btgpsd.pro create mode 100644 btgpsd.service create mode 100644 main.cpp create mode 100644 nmeasource.cpp create mode 100644 nmeasource.h create mode 100644 rpm/btgpsd.spec create mode 100644 rpm/btgpsd.yaml 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 + +#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(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(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 +#include + +#include +#include +#include + +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 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 + +#include "bluetoothgpsserver.h" + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + QScopedPointer 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 +#include +#include +#include +#include + +#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)), + this, SLOT(handleSatsInUse(QList))); + connect(m_satsrc, SIGNAL(satellitesInViewUpdated(QList)), + this, SLOT(handleSatsInView(QList))); + } +} + +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()), °rees) * 60; + return QString::fromLatin1("%1%2") + .arg(static_cast(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()), °rees) * 60; + return QString::fromLatin1("%1%2") + .arg(static_cast(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& 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& 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 &info) +{ + generateGPGSA(m_last_pos, info); +} + +void NmeaSource::handleSatsInView(const QList &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 +#include + +#include +#include +#include + +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& info); + void generateGPGSV(const QGeoPositionInfo& pos, const QList& info); + +protected slots: + void handlePosition(const QGeoPositionInfo& pos); + void handleSatsInUse(const QList& info); + void handleSatsInView(const QList& 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' -- cgit v1.2.3