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