Dokit
Internal development documentation
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Macros Pages
scancommand.cpp
1// SPDX-FileCopyrightText: 2022-2025 Paul Colby <git@colby.id.au>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4#include "scancommand.h"
6
8
9#include <QBluetoothUuid>
10#include <QJsonArray>
11#include <QJsonDocument>
12#include <QJsonObject>
13
14#include <iostream>
15
17
18/*!
19 * \class ScanCommand
20 *
21 * The ScanCommand class implements the `scan` CLI command, by scanning for nearby Pokit Bluetooth
22 * devices. When devices are found, they are logged to stdout in the chosen format.
23 */
24
25/*!
26 * Construct a new ScanCommand object with \a parent.
27 */
29{
30 #if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) // Required signal, and Fields, added in Qt 5.12.
33 #endif
34}
35
40
46
47/// \copydoc AbstractCommand::processOptions
49{
51 if (!errors.isEmpty()) {
52 return errors;
53 }
54
55 return errors;
56}
57
58/*!
59 * Begins scanning for Pokit devices.
60 */
62{
63 Q_ASSERT(discoveryAgent);
64 qCInfo(lc).noquote() << tr("Scanning for Pokit devices...");
65 discoveryAgent->start();
66 return true;
67}
68
69/*!
70 * Handles discovered Pokit devices, writing \a info to stdout.
71 */
73{
74 switch (format) {
76 for (; showCsvHeader; showCsvHeader = false) {
77 std::cout << qUtf8Printable(tr("uuid,address,name,major_class,minor_class,signal_strength\n"));
78 }
79 std::cout << qUtf8Printable(QString::fromLatin1("%1,%2,%3,%4,%5,%6\n").arg(info.deviceUuid().toString(),
81 toString(info.majorDeviceClass(), info.minorDeviceClass())).arg(info.rssi()));
82 break;
84 std::cout << QJsonDocument(toJson(info)).toJson().toStdString();
85 break;
87 std::cout << qUtf8Printable(tr("%1 %2 %3 %4\n").arg(info.deviceUuid().toString(),
88 info.address().toString(), info.name()).arg(info.rssi()));
89 break;
90 }
91}
92
93/*!
94 * Handles updated Pokit devices, writing \a info to stdout. Currently \a updatedFields us unused.
95 */
96#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) // Required signal, and Fields, added in Qt 5.12.
98 const QBluetoothDeviceInfo::Fields updatedFields)
99{
100 Q_UNUSED(updatedFields)
101 deviceDiscovered(info);
102}
103#endif
104
105/*!
106 * Handles the completion of device discovery. In this override we simply exit, as the scan command
107 * is nothing more than logging of discovered devices.
108 */
110{
111 qCDebug(lc).noquote() << tr("Finished scanning for Pokit devices.");
113}
114
115/*!
116 * Returns \a info as a JSON object.
117 */
119{
120 if (!info.isValid()) {
121 return QJsonObject();
122 }
123 QJsonObject json{
124 { u"address"_s, info.address().toString() },
125 { u"name"_s, info.name() },
126 { u"isCached"_s, info.isCached() },
127 { u"majorDeviceClass"_s, info.majorDeviceClass() },
128 { u"majorDeviceClass"_s, toJson(info.majorDeviceClass()) },
129 { u"minorDeviceClass"_s, toJson(info.majorDeviceClass(), info.minorDeviceClass()) },
130 { u"signalStrength"_s, info.rssi() },
131 };
133 json.insert(u"coreConfiguration"_s, toJson(info.coreConfigurations()));
134 }
135 if (!info.deviceUuid().isNull()) {
136 json.insert(u"deviceUuid"_s, info.deviceUuid().toString());
137 }
138 #if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) // Added in Qt 5.12.
139 if (!info.manufacturerData().isEmpty()) {
140 json.insert(u"manufacturerData"_s, toJson(info.manufacturerData()));
141 }
142 #endif
144 json.insert(u"serviceClasses"_s, toJson(info.serviceClasses()));
145 }
146 if (!info.serviceUuids().isEmpty()) {
147 json.insert(u"serviceUuids"_s, toJson(info.serviceUuids()));
148 }
149 return json;
150}
151
152/*!
153 * Returns \a configuration as a JSON array of strings.
154 */
156{
157 QJsonArray array;
158 #define DOKIT_INTERNAL_IF_SET_THEN_APPEND(flag) \
159 if (configurations.testFlag(QBluetoothDeviceInfo::flag)) \
160 array.append(QLatin1String(#flag))
161 DOKIT_INTERNAL_IF_SET_THEN_APPEND(UnknownCoreConfiguration);
162 DOKIT_INTERNAL_IF_SET_THEN_APPEND(LowEnergyCoreConfiguration);
163 DOKIT_INTERNAL_IF_SET_THEN_APPEND(BaseRateCoreConfiguration);
164 //DOKIT_INTERNAL_IF_SET_THEN_APPEND(BaseRateAndLowEnergyCoreConfiguration); // Combination flag.
165 #undef DOKIT_INTERNAL_IF_SET_THEN_APPEND
166 return array;
167}
168
169/*!
170 * Returns \a majorClass as a JSON value. This is equivalent to toString, except that if toString
171 * does not recognise \a majorClass, then \a majorClass is returned as a JSON number (not a string).
172 *
173 * \see toString(const QBluetoothDeviceInfo::MajorDeviceClass &majorClass)
174 */
176{
177 const QString string = toString(majorClass);
178 return (string.isNull() ? QJsonValue(majorClass) : QJsonValue(string));
179}
180
181/*!
182 * Returns \a minorClass as a JSON value. This is equivalent to toString, except that if toString
183 * does not recognise \a minorClass as a sub-class of \a majorClass, then \a minorClass is returned
184 * as a JSON number (not a string).
185 *
186 * \see toString(const QBluetoothDeviceInfo::MajorDeviceClass &majorClass, const quint8 minorClass)
187 */
189{
190 const QString string = toString(majorClass, minorClass);
191 return (string.isNull() ? QJsonValue(minorClass) : QJsonValue(string));
192}
193
194/*!
195 * Returns \a classes as a JSON array of strings.
196 */
198{
199 QJsonArray array;
200 #define DOKIT_INTERNAL_IF_SET_THEN_APPEND(flag) \
201 if (classes.testFlag(QBluetoothDeviceInfo::flag)) \
202 array.append(QLatin1String(#flag))
203 DOKIT_INTERNAL_IF_SET_THEN_APPEND(PositioningService);
204 DOKIT_INTERNAL_IF_SET_THEN_APPEND(NetworkingService);
205 DOKIT_INTERNAL_IF_SET_THEN_APPEND(RenderingService);
206 DOKIT_INTERNAL_IF_SET_THEN_APPEND(CapturingService);
207 DOKIT_INTERNAL_IF_SET_THEN_APPEND(ObjectTransferService);
208 DOKIT_INTERNAL_IF_SET_THEN_APPEND(AudioService);
209 DOKIT_INTERNAL_IF_SET_THEN_APPEND(TelephonyService);
210 DOKIT_INTERNAL_IF_SET_THEN_APPEND(InformationService);
211 #undef DOKIT_INTERNAL_IF_SET_THEN_APPEND
212 return array;
213}
214
215/*!
216 * Returns \a uuids as a JSON array.
217 */
219{
220 QJsonArray array;
221 for (const QBluetoothUuid &uuid: uuids) {
222 array.append(uuid.toString());
223 }
224 return array;
225}
226
227/*!
228 * Returns Bluetooth manufacturer \a data as a JSON object that maps the manufacturer IDs (unsigned
229 * integers as strings) to arrays of one or more values.
230 */
232{
233 QJsonObject object;
234 QList<quint16> keys = data.uniqueKeys();
235 std::sort(keys.begin(), keys.end());
236 for (const quint16 key: keys) {
237 // Convert the key's values to a JSON array, reversing the order, because QMultiHash
238 // guarantees that the values are order "from the most recently inserted to the least
239 // recently inserted", which is the opposite of what we want.
240 QList<QByteArray> values = data.values(key);
241 std::reverse(values.begin(), values.end());
242 QJsonArray array;
243 for (const QByteArray &value: values) {
244 array.append(QLatin1String(value.toBase64()));
245 }
246 object.insert(QString::number(key), array);
247 }
248 return object;
249}
250
251/*!
252 * Returns \a majorClass as a human-readable string, or a null QString if \a majorClass is not
253 * recognised.
254 *
255 * For example, if \a majorClass is \c QBluetoothDeviceInfo::ToyDevice, then the string `ToyDevice`
256 * is returned.
257 */
259{
260 #define DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(value) \
261 if (majorClass == QBluetoothDeviceInfo::value) \
262 return QLatin1String(#value)
263 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(MiscellaneousDevice);
264 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ComputerDevice);
265 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(PhoneDevice);
266 #if (QT_VERSION < QT_VERSION_CHECK(5, 13, 0))
267 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(LANAccessDevice); // Deprecated since Qt 5.13.
268 #else
269 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(NetworkDevice); // Added in Qt 5.13.
270 #endif
271 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(AudioVideoDevice);
272 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(PeripheralDevice);
273 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ImagingDevice);
274 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(WearableDevice);
275 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ToyDevice);
276 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HealthDevice);
277 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(UncategorizedDevice);
278 #undef DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN
279 qCDebug(lc).noquote() << tr("Unknown major class %1.").arg(majorClass);
280 return QString(); // Null QString indicates unknown minor class.
281}
282
283/*!
284 * Returns \a minorClass as a human-readable string, or a null QString if \a minorClass is not
285 * recognised as a sub-class of \a majorClass.
286 *
287 * For example, if \a majorClass is \c QBluetoothDeviceInfo::ToyDevice, and \a minorClass is
288 * \c QBluetoothDeviceInfo::ToyRobot, then the string `ToyRobot` is returned.
289 */
291{
292 #define DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(value) \
293 if (minorClass == QBluetoothDeviceInfo::value) \
294 return QLatin1String(#value)
295 switch (majorClass) {
297 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(UncategorizedMiscellaneous);
298 break;
300 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(UncategorizedComputer);
301 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(DesktopComputer);
302 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ServerComputer);
303 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(LaptopComputer);
304 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HandheldClamShellComputer);
305 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HandheldComputer);
306 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(WearableComputer);
307 break;
309 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(UncategorizedPhone);
310 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(CellularPhone);
311 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(CordlessPhone);
312 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(SmartPhone);
313 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(WiredModemOrVoiceGatewayPhone);
314 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(CommonIsdnAccessPhone);
315 break;
316 #if (QT_VERSION < QT_VERSION_CHECK(5, 13, 0))
317 case QBluetoothDeviceInfo::LANAccessDevice: // Deprecated since Qt 5.13.
318 #else
319 case QBluetoothDeviceInfo::NetworkDevice: // Added in Qt 5.13.
320 #endif
321 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(NetworkFullService);
322 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(NetworkLoadFactorOne);
323 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(NetworkLoadFactorTwo);
324 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(NetworkLoadFactorThree);
325 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(NetworkLoadFactorFour);
326 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(NetworkLoadFactorFive);
327 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(NetworkLoadFactorSix);
328 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(NetworkNoService);
329 break;
331 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(UncategorizedAudioVideoDevice);
332 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(WearableHeadsetDevice);
333 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HandsFreeDevice);
334 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(Microphone);
335 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(Loudspeaker);
336 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(Headphones);
337 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(PortableAudioDevice);
338 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(CarAudio);
339 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(SetTopBox);
340 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HiFiAudioDevice);
341 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(Vcr);
342 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(VideoCamera);
343 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(Camcorder);
344 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(VideoMonitor);
345 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(VideoDisplayAndLoudspeaker);
346 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(VideoConferencing);
347 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(GamingDevice);
348 break;
350 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(UncategorizedPeripheral);
351 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(KeyboardPeripheral);
352 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(PointingDevicePeripheral);
353 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(KeyboardWithPointingDevicePeripheral);
354 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(JoystickPeripheral);
355 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(GamepadPeripheral);
356 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(RemoteControlPeripheral);
357 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(SensingDevicePeripheral);
358 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(DigitizerTabletPeripheral);
359 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(CardReaderPeripheral);
360 break;
362 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(UncategorizedImagingDevice);
363 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ImageDisplay);
364 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ImageCamera);
365 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ImageScanner);
366 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ImagePrinter);
367 break;
369 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(UncategorizedWearableDevice);
370 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(WearableWristWatch);
371 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(WearablePager);
372 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(WearableJacket);
373 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(WearableHelmet);
374 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(WearableGlasses);
375 break;
377 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(UncategorizedToy);
378 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ToyRobot);
379 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ToyVehicle);
380 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ToyDoll);
381 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ToyController);
382 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(ToyGame);
383 break;
385 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(UncategorizedHealthDevice);
386 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HealthBloodPressureMonitor);
387 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HealthThermometer);
388 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HealthWeightScale);
389 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HealthGlucoseMeter);
390 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HealthPulseOximeter);
391 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HealthDataDisplay);
392 DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN(HealthStepCounter);
393 break;
395 // There are no minor classes defined (in Qt) for uncategorized devices.
396 break;
397 }
398 #undef DOKIT_INTERNAL_IF_EQUAL_THEN_RETURN
399 qCDebug(lc).noquote() << tr("Unknown minor class %1 for major class %2.")
400 .arg(minorClass).arg(majorClass);
401 return QString(); // Null QString indicates unknown minor class.
402}
AbstractCommand(QObject *const parent=nullptr)
Constructs a new command with parent.
virtual QStringList supportedOptions(const QCommandLineParser &parser) const
Returns a list of CLI option names supported by this command.
PokitDiscoveryAgent * discoveryAgent
Agent for Pokit device discovery.
OutputFormat format
Selected output format.
@ Text
Plain unstructured text.
@ Csv
RFC 4180 compliant CSV text.
@ Json
RFC 8259 compliant JSON text.
virtual QStringList processOptions(const QCommandLineParser &parser)
Processes the relevant options from the command line parser.
static QString escapeCsvField(const QString &field)
Returns an RFC 4180 compliant version of field.
virtual QStringList requiredOptions(const QCommandLineParser &parser) const
Returns a list of CLI option names required by this command.
void pokitDeviceUpdated(const QBluetoothDeviceInfo &info, QBluetoothDeviceInfo::Fields updatedFields)
This signal is emitted when the Pokit device described by info is updated.
static QJsonObject toJson(const QBluetoothDeviceInfo &info)
Returns info as a JSON object.
QStringList requiredOptions(const QCommandLineParser &parser) const override
Returns a list of CLI option names required by this command.
void deviceUpdated(const QBluetoothDeviceInfo &info, const QBluetoothDeviceInfo::Fields updatedFields)
Handles updated Pokit devices, writing info to stdout.
ScanCommand(QObject *const parent=nullptr)
Construct a new ScanCommand object with parent.
static QString toString(const QBluetoothDeviceInfo::MajorDeviceClass &majorClass)
Returns majorClass as a human-readable string, or a null QString if majorClass is not recognised.
void deviceDiscoveryFinished() override
Handles the completion of device discovery.
bool start() override
Begins scanning for Pokit devices.
QStringList processOptions(const QCommandLineParser &parser) override
Processes the relevant options from the command line parser.
bool showCsvHeader
Whether or not to show a header as the first line of CSV output.
Definition scancommand.h:29
void deviceDiscovered(const QBluetoothDeviceInfo &info) override
Handles discovered Pokit devices, writing info to stdout.
QStringList supportedOptions(const QCommandLineParser &parser) const override
Returns a list of CLI option names supported by this command.
Declares the PokitDiscoveryAgent class.
QString toString() const const
QBluetoothAddress address() const const
QBluetoothDeviceInfo::CoreConfigurations coreConfigurations() const const
QBluetoothUuid deviceUuid() const const
bool isCached() const const
bool isValid() const const
QBluetoothDeviceInfo::MajorDeviceClass majorDeviceClass() const const
QByteArray manufacturerData(quint16 manufacturerId) const const
quint8 minorDeviceClass() const const
QString name() const const
qint16 rssi() const const
QBluetoothDeviceInfo::ServiceClasses serviceClasses() const const
QList< QBluetoothUuid > serviceUuids(QBluetoothDeviceInfo::DataCompleteness *completeness) const const
bool isEmpty() const const
std::string toStdString() const const
void append(const QJsonValue &value)
void insert(int i, const QJsonValue &value)
QByteArray toJson() const const
QList::iterator begin()
QList::iterator end()
bool isEmpty() const const
QList< Key > uniqueKeys() const const
QList< T > values(const Key &key) const const
QObject(QObject *parent)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject * parent() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString fromLatin1(const char *str, int size)
QString number(int n, int base)
bool isNull() const const
QString toString() const const
Declares the DOKIT_USE_STRINGLITERALS macro, and related functions.
#define DOKIT_USE_STRINGLITERALS
Internal macro for using either official Qt string literals (added in Qt 6.4), or our own equivalent ...