Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qgeopositioninfosourcefactory_nmea.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
6#include <QtPositioning/QNmeaPositionInfoSource>
7#include <QtPositioning/QNmeaSatelliteInfoSource>
8#include <QtNetwork/QTcpSocket>
9#include <QLoggingCategory>
10#include <QSet>
11#include <QUrl>
12#include <QFile>
13#include <QSharedPointer>
14#include "qiopipe_p.h"
15
16#ifdef QT_NMEA_PLUGIN_HAS_SERIALPORT
17# include <QtSerialPort/QSerialPort>
18# include <QtSerialPort/QSerialPortInfo>
19#endif
20
21
23
24Q_STATIC_LOGGING_CATEGORY(lcNmea, "qt.positioning.nmea")
25
26static const auto sourceParameterName = QStringLiteral("nmea.source");
27static const auto socketScheme = QStringLiteral("socket:");
28static const auto serialScheme = QStringLiteral("serial:");
29
30static const auto baudRateParameterName = QStringLiteral("nmea.baudrate");
31static constexpr auto defaultBaudRate = 4800;
32
33#ifdef QT_NMEA_PLUGIN_HAS_SERIALPORT
34
35// This class is used only for SerialPort devices, because we can't open the
36// same serial port twice.
37// In case of files and sockets it's easier to explicitly create a QIODevice for
38// each new instance of Nmea*InfoSource.
39// Also QFile can't be directly used with QIOPipe, because QFile is not a
40// sequential device.
41// TcpSocket could be used with QIOPipe, but it complicates error handling
42// dramatically, as we would need to somehow forward socket errors through
43// QIOPipes to the clients.
44class IODeviceContainer
45{
46public:
47 IODeviceContainer() {}
48 IODeviceContainer(IODeviceContainer const&) = delete;
49 void operator=(IODeviceContainer const&) = delete;
50
51 QSharedPointer<QIOPipe> serial(const QString &portName, qint32 baudRate)
52 {
53 if (m_serialPorts.contains(portName)) {
54 m_serialPorts[portName].refs++;
55 QIOPipe *endPipe = new QIOPipe(m_serialPorts[portName].proxy);
56 m_serialPorts[portName].proxy->addChildPipe(endPipe);
57 return QSharedPointer<QIOPipe>(endPipe);
58 }
59 IODevice device;
60 QSerialPort *port = new QSerialPort(portName);
61 port->setBaudRate(baudRate);
62 qCDebug(lcNmea) << "Opening serial port" << portName << "with baudrate" << baudRate;
63 if (!port->open(QIODevice::ReadOnly)) {
64 qWarning("nmea: Failed to open %s", qPrintable(portName));
65 delete port;
66 return {};
67 }
68 qCDebug(lcNmea) << "Opened successfully";
69 device.device = port;
70 device.refs = 1;
71 device.proxy = new QIOPipe(port, QIOPipe::ProxyPipe);
72 m_serialPorts[portName] = device;
73 QIOPipe *endPipe = new QIOPipe(device.proxy);
74 device.proxy->addChildPipe(endPipe);
75 return QSharedPointer<QIOPipe>(endPipe);
76 }
77
78 void releaseSerial(const QString &portName, QSharedPointer<QIOPipe> &pipe)
79 {
80 if (!m_serialPorts.contains(portName))
81 return;
82
83 pipe.clear(); // make sure to release the pipe returned by getSerial, or else, if there are still refs, data will be leaked through it
84 IODevice &device = m_serialPorts[portName];
85 if (device.refs > 1) {
86 device.refs--;
87 return;
88 }
89
90 IODevice taken = m_serialPorts.take(portName);
91 taken.device->deleteLater();
92 }
93
94private:
95
96 struct IODevice {
97 QIODevice *device = nullptr;
98 QIOPipe *proxy = nullptr; // adding client pipes as children of proxy
99 // allows to dynamically add clients to one device.
100 unsigned int refs = 1;
101 };
102
103 QMap<QString, IODevice> m_serialPorts;
104};
105
106Q_GLOBAL_STATIC(IODeviceContainer, deviceContainer)
107
108#endif // QT_NMEA_PLUGIN_HAS_SERIALPORT
109
111{
112 explicit NmeaParameters(const QVariantMap &parameters);
113
116};
117
118NmeaParameters::NmeaParameters(const QVariantMap &parameters)
119{
120 source = parameters.value(sourceParameterName).toString();
121 bool ok = false;
122 const auto br = parameters.value(baudRateParameterName).toInt(&ok);
123 // According to QSerialPort::setBaudRate() documentation, we can pick any
124 // positive number as a baud rate.
125 if (ok && br > 0)
126 baudRate = br;
127}
128
129// We use a string prefix to distinguish between the different data sources.
130// "socket:" means that we use a socket connection
131// "serial:" means that we use a serial port connection
132// "file:///", "qrc:///" and just plain strings mean that we try to use local
133// file.
134// Note: if we do not specify anything, or specify "serial:" without specifying
135// the port name, then we will try to search for a well-known serial port
136// device.
138{
140public:
142 NmeaSource(QObject *parent, const QString &fileName);
144 bool isValid() const
145 {
146 return !m_dataSource.isNull() || !m_fileSource.isNull() || !m_socket.isNull();
147 }
148
149private slots:
151
152private:
153 void processParameters(const NmeaParameters &parameters);
154 void addSerialDevice(const QString &requestedPort, quint32 baudRate);
155 void setFileName(const QString &fileName);
156 void connectSocket(const QString &source);
157
158 QSharedPointer<QIOPipe> m_dataSource;
159 QScopedPointer<QFile> m_fileSource;
160 QScopedPointer<QTcpSocket> m_socket;
161 QString m_sourceName;
162};
163
164NmeaSource::NmeaSource(QObject *parent, const QVariantMap &parameters)
166{
167 processParameters(NmeaParameters(parameters));
168}
169
170NmeaSource::NmeaSource(QObject *parent, const QString &fileName)
172{
173 setFileName(fileName);
174}
175
177{
178#ifdef QT_NMEA_PLUGIN_HAS_SERIALPORT
179 if (deviceContainer.exists())
180 deviceContainer->releaseSerial(m_sourceName, m_dataSource);
181#endif
182}
183
184void NmeaSource::onSocketError(QAbstractSocket::SocketError error)
185{
186 m_socket->close();
187
188 switch (error) {
189 case QAbstractSocket::UnknownSocketError:
190 setError(QGeoPositionInfoSource::UnknownSourceError);
191 break;
192 case QAbstractSocket::SocketAccessError:
193 setError(QGeoPositionInfoSource::AccessError);
194 break;
195 case QAbstractSocket::RemoteHostClosedError:
196 setError(QGeoPositionInfoSource::ClosedError);
197 break;
198 default:
199 qWarning() << "Connection failed! QAbstractSocket::SocketError" << error;
200 // TODO - introduce new type of error. TransportError?
201 setError(QGeoPositionInfoSource::UnknownSourceError);
202 break;
203 }
204}
205
206void NmeaSource::processParameters(const NmeaParameters &parameters)
207{
208 if (parameters.source.startsWith(socketScheme)) {
209 // This is a socket
210 connectSocket(parameters.source);
211 } else {
212 // Last chance - this can be serial device.
213 // Note: File is handled in a separate case.
214 addSerialDevice(parameters.source, parameters.baudRate);
215 }
216}
217
218#ifdef QT_NMEA_PLUGIN_HAS_SERIALPORT
219static QString tryFindSerialDevice(const QString &requestedPort)
220{
221 QString portName;
222 if (requestedPort.isEmpty()) {
223 const QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts();
224 qCDebug(lcNmea) << "Found" << ports.size() << "serial ports";
225 if (ports.isEmpty()) {
226 qWarning("nmea: No serial ports found");
227 return portName;
228 }
229
230 // Try to find a well-known device.
231 QSet<int> supportedDevices;
232 supportedDevices << 0x67b; // GlobalSat (BU-353S4 and probably others)
233 supportedDevices << 0xe8d; // Qstarz MTK II
234 for (const QSerialPortInfo& port : ports) {
235 if (port.hasVendorIdentifier() && supportedDevices.contains(port.vendorIdentifier())) {
236 portName = port.portName();
237 break;
238 }
239 }
240
241 if (portName.isEmpty()) {
242 qWarning("nmea: No known GPS device found.");
243 }
244 } else {
245 portName = requestedPort;
246 if (portName.startsWith(serialScheme))
247 portName.remove(0, 7);
248 }
249 return portName;
250}
251#endif // QT_NMEA_PLUGIN_HAS_SERIALPORT
252
253void NmeaSource::addSerialDevice(const QString &requestedPort, quint32 baudRate)
254{
255#ifdef QT_NMEA_PLUGIN_HAS_SERIALPORT
256 m_sourceName = tryFindSerialDevice(requestedPort);
257 if (m_sourceName.isEmpty())
258 return;
259
260 m_dataSource = deviceContainer->serial(m_sourceName, baudRate);
261 if (!m_dataSource)
262 return;
263
264 setDevice(m_dataSource.data());
265#else
266 Q_UNUSED(baudRate);
267 // As we are not calling setDevice(), the source will be invalid, so
268 // the factory methods will return nullptr.
269 qWarning() << "Plugin was built without serialport support!"
270 << requestedPort << "cannot be used!";
271#endif
272}
273
274void NmeaSource::setFileName(const QString &fileName)
275{
276 m_sourceName = fileName;
277
278 m_fileSource.reset(new QFile(fileName));
279 qCDebug(lcNmea) << "Opening file" << fileName;
280 if (!m_fileSource->open(QIODevice::ReadOnly)) {
281 qWarning("nmea: failed to open file %s", qPrintable(fileName));
282 m_fileSource.reset();
283 }
284
285 if (!m_fileSource)
286 return;
287
288 qCDebug(lcNmea) << "Opened successfully";
289
290 setDevice(m_fileSource.data());
291}
292
293void NmeaSource::connectSocket(const QString &source)
294{
295 const QUrl url(source);
296 const QString host = url.host();
297 const int port = url.port();
298 if (!host.isEmpty() && (port > 0)) {
299 m_socket.reset(new QTcpSocket);
300 // no need to explicitly connect to connected() signal
301 connect(m_socket.get(), &QTcpSocket::errorOccurred, this, &NmeaSource::onSocketError);
302 m_socket->connectToHost(host, port, QTcpSocket::ReadOnly);
303 m_sourceName = source;
304
305 setDevice(m_socket.data());
306 } else {
307 qWarning("nmea: incorrect socket parameters %s:%d", qPrintable(host), port);
308 }
309}
310
312{
314public:
316 NmeaSatelliteSource(QObject *parent, const QString &fileName, const QVariantMap &parameters);
318
319 bool isValid() const { return !m_port.isNull() || !m_file.isNull() || !m_socket.isNull(); }
320
321private slots:
323
324private:
325 void processRealtimeParameters(const NmeaParameters &parameters);
326 void parseSimulationSource(const QString &localFileName);
327
328 QSharedPointer<QIOPipe> m_port;
329 QScopedPointer<QFile> m_file;
330 QScopedPointer<QTcpSocket> m_socket;
331 QString m_sourceName;
332};
333
334NmeaSatelliteSource::NmeaSatelliteSource(QObject *parent, const QVariantMap &parameters)
335 : QNmeaSatelliteInfoSource(QNmeaSatelliteInfoSource::UpdateMode::RealTimeMode, parent)
336{
337 processRealtimeParameters(NmeaParameters(parameters));
338}
339
340// We can use a QNmeaSatelliteInfoSource::SimulationUpdateInterval parameter to
341// set the file read frequency in simulation mode. We use setBackendProperty()
342// for it. The value can't be smaller than minimumUpdateInterval().
343// This check is done on the QNmeaSatelliteInfoSource level
344NmeaSatelliteSource::NmeaSatelliteSource(QObject *parent, const QString &fileName,
345 const QVariantMap &parameters)
347{
348 bool ok = false;
349 const int interval =
350 parameters.value(QNmeaSatelliteInfoSource::SimulationUpdateInterval).toInt(&ok);
351 if (ok)
352 setBackendProperty(QNmeaSatelliteInfoSource::SimulationUpdateInterval, interval);
353 parseSimulationSource(fileName);
354}
355
357{
358#ifdef QT_NMEA_PLUGIN_HAS_SERIALPORT
359 if (deviceContainer.exists())
360 deviceContainer->releaseSerial(m_sourceName, m_port);
361#endif
362}
363
364void NmeaSatelliteSource::onSocketError(QAbstractSocket::SocketError error)
365{
366 m_socket->close();
367
368 switch (error) {
369 case QAbstractSocket::UnknownSocketError:
370 setError(QGeoSatelliteInfoSource::UnknownSourceError);
371 break;
372 case QAbstractSocket::SocketAccessError:
373 setError(QGeoSatelliteInfoSource::AccessError);
374 break;
375 case QAbstractSocket::RemoteHostClosedError:
376 setError(QGeoSatelliteInfoSource::ClosedError);
377 break;
378 default:
379 qWarning() << "Connection failed! QAbstractSocket::SocketError" << error;
380 // TODO - introduce new type of error. TransportError?
381 setError(QGeoSatelliteInfoSource::UnknownSourceError);
382 break;
383 }
384}
385
386void NmeaSatelliteSource::processRealtimeParameters(const NmeaParameters &parameters)
387{
388 const QString source = parameters.source;
389 if (source.startsWith(socketScheme)) {
390 // This is a socket.
391 const QUrl url(source);
392 const QString host = url.host();
393 const int port = url.port();
394 if (!host.isEmpty() && (port > 0)) {
395 m_socket.reset(new QTcpSocket);
396 // no need to explicitly connect to connected() signal
397 connect(m_socket.get(), &QTcpSocket::errorOccurred,
398 this, &NmeaSatelliteSource::onSocketError);
399 m_socket->connectToHost(host, port, QTcpSocket::ReadOnly);
400 m_sourceName = source;
401
402 setDevice(m_socket.data());
403 } else {
404 qWarning("nmea: incorrect socket parameters %s:%d", qPrintable(host), port);
405 }
406 } else {
407#ifdef QT_NMEA_PLUGIN_HAS_SERIALPORT
408 // Last chance - this can be serial device.
409 m_sourceName = tryFindSerialDevice(source);
410 if (m_sourceName.isEmpty())
411 return;
412
413 m_port = deviceContainer->serial(m_sourceName, parameters.baudRate);
414 if (!m_port)
415 return;
416
417 setDevice(m_port.data());
418#else
419 // As we are not calling setDevice(), the source will be invalid, so
420 // the factory methods will return nullptr.
421 qWarning() << "Plugin was built without serialport support!"
422 << source << "cannot be used!";
423#endif // QT_NMEA_PLUGIN_HAS_SERIALPORT
424 }
425}
426
427void NmeaSatelliteSource::parseSimulationSource(const QString &localFileName)
428{
429 // This is a text file.
430 m_sourceName = localFileName;
431
432 qCDebug(lcNmea) << "Opening file" << localFileName;
433 m_file.reset(new QFile(localFileName));
434 if (!m_file->open(QIODevice::ReadOnly)) {
435 qWarning("nmea: failed to open file %s", qPrintable(localFileName));
436 m_file.reset();
437 return;
438 }
439 qCDebug(lcNmea) << "Opened successfully";
440
441 setDevice(m_file.data());
442}
443
444/*!
445 \internal
446 Returns a local file name if \a source represents it.
447 The returned value can be different from \a source, as the method tries to
448 modify the path
449*/
450static QString checkSourceIsFile(const QString &source)
451{
452 if (source.isEmpty())
453 return QString();
454
455 QString localFileName = source;
456
457 if (!QFile::exists(localFileName)) {
458 if (localFileName.startsWith(QStringLiteral("qrc:///")))
459 localFileName.remove(0, 7);
460 else if (localFileName.startsWith(QStringLiteral("file:///")))
461 localFileName.remove(0, 7);
462 else if (localFileName.startsWith(QStringLiteral("qrc:/")))
463 localFileName.remove(0, 5);
464
465 if (!QFile::exists(localFileName) && localFileName.startsWith(QLatin1Char('/')))
466 localFileName.remove(0, 1);
467 }
468 if (!QFile::exists(localFileName))
469 localFileName.prepend(QLatin1Char(':'));
470
471 const bool isLocalFile = QFile::exists(localFileName);
472 return isLocalFile ? localFileName : QString();
473}
474
475/*!
476 \internal
477 Returns a local file name if file exists, or an empty string otherwise
478*/
479static QString extractLocalFileName(const QVariantMap &parameters)
480{
481 QString localFileName = parameters.value(sourceParameterName).toString();
482 return checkSourceIsFile(localFileName);
483}
484
485QGeoPositionInfoSource *QGeoPositionInfoSourceFactoryNmea::positionInfoSource(QObject *parent, const QVariantMap &parameters)
486{
487 std::unique_ptr<NmeaSource> src = nullptr;
488
489 const QString localFileName = extractLocalFileName(parameters);
490 if (localFileName.isEmpty())
491 src = std::make_unique<NmeaSource>(parent, parameters); // use RealTimeMode
492 else
493 src = std::make_unique<NmeaSource>(parent, localFileName); // use SimulationMode
494
495 return (src && src->isValid()) ? src.release() : nullptr;
496}
497
498QGeoSatelliteInfoSource *QGeoPositionInfoSourceFactoryNmea::satelliteInfoSource(QObject *parent, const QVariantMap &parameters)
499{
500 std::unique_ptr<NmeaSatelliteSource> src = nullptr;
501
502 const QString localFileName = extractLocalFileName(parameters);
503 if (localFileName.isEmpty()) {
504 // use RealTimeMode
505 src = std::make_unique<NmeaSatelliteSource>(parent, parameters);
506 } else {
507 // use SimulationMode
508 src = std::make_unique<NmeaSatelliteSource>(parent, localFileName, parameters);
509 }
510 return (src && src->isValid()) ? src.release() : nullptr;
511}
512
513QGeoAreaMonitorSource *QGeoPositionInfoSourceFactoryNmea::areaMonitor(QObject *parent, const QVariantMap &parameters)
514{
515 Q_UNUSED(parent);
516 Q_UNUSED(parameters);
517 return nullptr;
518}
519
520QT_END_NAMESPACE
521
522#include "moc_qgeopositioninfosourcefactory_nmea.cpp"
523#include "qgeopositioninfosourcefactory_nmea.moc"
NmeaSatelliteSource(QObject *parent, const QString &fileName, const QVariantMap &parameters)
NmeaSource(QObject *parent, const QString &fileName)
static QString checkSourceIsFile(const QString &source)
static constexpr auto defaultBaudRate
static const auto socketScheme
static const auto serialScheme
static const auto baudRateParameterName
static QString extractLocalFileName(const QVariantMap &parameters)
NmeaParameters(const QVariantMap &parameters)