Line data Source code
1 : // SPDX-FileCopyrightText: 2022 Paul Colby <git@colby.id.au>
2 : // SPDX-License-Identifier: LGPL-3.0-or-later
3 :
4 : #include "abstractcommand.h"
5 :
6 : #include <qtpokit/pokitdevice.h>
7 : #include <qtpokit/pokitdiscoveryagent.h>
8 :
9 : #include <QLocale>
10 :
11 : /*!
12 : * \class AbstractCommand
13 : *
14 : * The AbstractCommand class provides a consistent base for the classes that implement CLI commands.
15 : */
16 :
17 : /// \enum AbstractCommand::OutputFormat
18 : /// \brief Supported output formats.
19 :
20 : /*!
21 : * Constructs a new command with \a parent.
22 : */
23 7230 : AbstractCommand::AbstractCommand(QObject * const parent) : QObject(parent),
24 7230 : discoveryAgent(new PokitDiscoveryAgent(this)), format(OutputFormat::Text)
25 : {
26 7230 : connect(discoveryAgent, &PokitDiscoveryAgent::pokitDeviceDiscovered,
27 : this, &AbstractCommand::deviceDiscovered);
28 7230 : connect(discoveryAgent, &PokitDiscoveryAgent::finished,
29 : this, &AbstractCommand::deviceDiscoveryFinished);
30 7230 : connect(discoveryAgent,
31 : #if (QT_VERSION < QT_VERSION_CHECK(6, 2, 0))
32 : QOverload<PokitDiscoveryAgent::Error>::of(&PokitDiscoveryAgent::error),
33 : #else
34 : &PokitDiscoveryAgent::errorOccurred,
35 : #endif
36 45 : [](const PokitDiscoveryAgent::Error &error) {
37 90 : qCWarning(lc).noquote() << tr("Bluetooth controller error:") << error;
38 45 : QCoreApplication::exit(EXIT_FAILURE);
39 45 : });
40 7230 : }
41 :
42 : /*!
43 : * Returns a list of CLI option names required by this command. The main console appication may
44 : * use this list to output an eror (and exit) if any of the returned names are not found in the
45 : * parsed CLI options.
46 : *
47 : * The (already parsed) \a parser may be used adjust the returned required options depending on the
48 : * value of other options. For example, the `logger` command only requires the `--mode` option if
49 : * the `--command` option is `start`.
50 : *
51 : * This base implementation simply returns an empty list. Derived classes should override this
52 : * function to include any required options.
53 : */
54 4637 : QStringList AbstractCommand::requiredOptions(const QCommandLineParser &parser) const
55 : {
56 : Q_UNUSED(parser);
57 4637 : return QStringList();
58 : }
59 :
60 : /*!
61 : * Returns a list of CLI option names supported by this command. The main console appication may
62 : * use this list to output a warning for any parsed CLI options not included in the returned list.
63 : *
64 : * The (already parsed) \a parser may be used adjust the returned supported options depending on the
65 : * value of other options. For example, the `logger` command only supported the `--timestamp` option
66 : * if the `--command` option is `start`.
67 : *
68 : * This base implementation simply returns requiredOptions(). Derived classes should override this
69 : * function to include optional options, such as:
70 : *
71 : * ```
72 : * QStringList Derived::supportedOptions(const QCommandLineParser &parser) const
73 : * {
74 : * const QStringList list = AbstractCommand::supportedOptions(parser) + QStringList{ ... };
75 : * list.sort();
76 : * list.removeDuplicates(); // Optional, recommended.
77 : * return list;
78 : * }
79 : * ```
80 : */
81 2259 : QStringList AbstractCommand::supportedOptions(const QCommandLineParser &parser) const
82 : {
83 15813 : return requiredOptions(parser) + QStringList{
84 : QLatin1String("debug"),
85 : QLatin1String("device"), QLatin1String("d"),
86 : QLatin1String("output"),
87 : QLatin1String("timeout"),
88 13728 : };
89 : }
90 :
91 : /*!
92 : * Returns an RFC 4180 compliant version of \a field. That is, if \a field contains any of the
93 : * the below four characters, than any double quotes are escaped (by addition double-quotes), and
94 : * the string itself surrounded in double-quotes. Otherwise, \a field is returned verbatim.
95 : *
96 : * Some examples:
97 : * ```
98 : * QCOMPARE(escapeCsvField("abc"), "abc"); // Returned unchanged.
99 : * QCOMPARE(escapeCsvField("a,c"), "\"a,c\""); // Wrapped in double-quotes.
100 : * QCOMPARE(escapeCsvField("a\"c"), "\"a""c\""); // Existing double-quotes doubles, then wrapped.
101 : * ```
102 : */
103 1497 : QString AbstractCommand::escapeCsvField(const QString &field)
104 : {
105 1480 : if (field.contains(QLatin1Char(','))||field.contains(QLatin1Char('\r'))||
106 4440 : field.contains(QLatin1Char('"'))||field.contains(QLatin1Char('\n')))
107 : {
108 40 : return QString::fromLatin1("\"%1\"").arg(
109 34 : QString(field).replace(QLatin1Char('"'), QLatin1String("\"\"")));
110 : } else return field;
111 : }
112 :
113 : /*!
114 : * Returns \a value as a number of micros, such as microseconds, or microvolts. The string \a value
115 : * may end with the optional \a unit, such as `V` or `s`, which may also be preceded with a SI unit
116 : * prefix such as `m` for `milli`. If \a value contains no SI unit prefix, then the result will be
117 : * multiplied by 1,000 enough times to be greater than \a sensibleMinimum. This allows for
118 : * convenient use like:
119 : *
120 : * ```
121 : * const quin32t timeout = parseMicroValue(parser.value("window"), 's', 500*1000);
122 : * ```
123 : *
124 : * So that an unqalified period like "300" will be assumed to be 300 milliseconds, and not 300
125 : * microseconds, while a period like "1000" will be assume to be 1 second.
126 : *
127 : * If conversion fails for any reason, 0 is returned.
128 : */
129 459 : quint32 AbstractCommand::parseMicroValue(const QString &value, const QString &unit,
130 : const quint32 sensibleMinimum)
131 : {
132 : // Remove the optional (whole) unit suffix.
133 : quint32 scale = 0;
134 216 : QString number = value.trimmed();
135 459 : if ((!unit.isEmpty()) && (number.endsWith(unit, Qt::CaseInsensitive))) {
136 204 : number.chop(unit.length());
137 : scale = 1000 * 1000;
138 : }
139 :
140 : // Parse, and remove, the optional SI unit prefix.
141 459 : if (number.endsWith(QLatin1String("m"))) {
142 136 : number.chop(1);
143 : scale = 1000;
144 : }
145 :
146 : // Parse, and remove, the optional SI unit prefix.
147 459 : if (number.endsWith(QLatin1String("u"))) {
148 17 : number.chop(1);
149 : scale = 1;
150 : }
151 :
152 : // Parse the number as an (unsigned) integer.
153 756 : QLocale locale; bool ok;
154 378 : const quint32 integer = locale.toUInt(number, &ok);
155 459 : if (ok) {
156 272 : if ((scale == 0) && (integer != 0)) {
157 170 : for (scale = 1; (integer * scale) < sensibleMinimum; scale *= 1000);
158 : }
159 272 : return integer * scale;
160 : }
161 :
162 : // Parse the number as a (double) floating point number, and check that it is positive.
163 154 : const double dbl = locale.toDouble(number, &ok);
164 187 : if ((ok) && (dbl > 0)) {
165 68 : if ((scale == 0) && (dbl > 0.0)) {
166 51 : for (scale = 1; (dbl * scale) < sensibleMinimum; scale *= 1000);
167 : }
168 68 : return dbl * scale;
169 : }
170 :
171 : return 0; // Failed to parse as either integer, or float.
172 162 : }
173 :
174 : /*!
175 : * Returns \a value as a number of millis, such as milliseconds, or millivolts. The string \a value
176 : * may end with the optional \a unit, such as `V` or `s`, which may also be preceded with a SI unit
177 : * prefix such as `m` for `milli`. If \a value contains no SI unit prefix, then the result will be
178 : * multiplied by 1,000 enough times to be greater than \a sensibleMinimum. This allows for
179 : * convenient use like:
180 : *
181 : * ```
182 : * const quin32t timeout = parseMilliValue(parser.value("timeout"), 's', 600);
183 : * ```
184 : *
185 : * So that an unqalified period like "300" will be assumed to be 300 seconds, and not 300
186 : * milliseconds, while a period like "1000" will be assume to be 1 second.
187 : *
188 : * If conversion fails for any reason, 0 is returned.
189 : */
190 1222 : quint32 AbstractCommand::parseMilliValue(const QString &value, const QString &unit,
191 : const quint32 sensibleMinimum)
192 : {
193 : // Remove the optional (whole) unit suffix.
194 : quint32 scale = 0;
195 520 : QString number = value.trimmed();
196 1222 : if ((!unit.isEmpty()) && (number.endsWith(unit, Qt::CaseInsensitive))) {
197 767 : number.chop(unit.length());
198 : scale = 1000;
199 : }
200 :
201 : // Parse, and remove, the optional SI unit prefix.
202 1222 : if (number.endsWith(QLatin1String("m"))) {
203 605 : number.chop(1);
204 : scale = 1;
205 : }
206 :
207 : // Parse the number as an (unsigned) integer.
208 1976 : QLocale locale; bool ok;
209 988 : const quint32 integer = locale.toUInt(number, &ok);
210 1222 : if (ok) {
211 922 : if ((scale == 0) && (integer != 0)) {
212 283 : for (scale = 1; (integer * scale) < sensibleMinimum; scale *= 1000);
213 : }
214 922 : return integer * scale;
215 : }
216 :
217 : // Parse the number as a (double) floating point number, and check that it is positive.
218 240 : const double dbl = locale.toDouble(number, &ok);
219 300 : if ((ok) && (dbl > 0)) {
220 68 : if ((scale == 0) && (dbl > 0.0)) {
221 51 : for (scale = 1; (dbl * scale) < sensibleMinimum; scale *= 1000);
222 : }
223 68 : return dbl * scale;
224 : }
225 :
226 : return 0; // Failed to parse as either integer, or float.
227 468 : }
228 :
229 : /*!
230 : * Returns \a value as a number, with optional SI unit prefix, and optional \a unit suffix. For
231 : * example:
232 : *
233 : * ```
234 : * QCOMPARE(parseWholeValue("1.2Mohm", "ohm"), 1200000);
235 : * ```
236 : *
237 : * If conversion fails for any reason, 0 is returned.
238 : */
239 323 : quint32 AbstractCommand::parseWholeValue(const QString &value, const QString &unit)
240 : {
241 : // Remove the optional unit suffix.
242 152 : QString number = value.trimmed();
243 323 : if (number.endsWith(unit, Qt::CaseInsensitive)) {
244 170 : number.chop(unit.length());
245 : }
246 :
247 : // Parse, and remove, the optional SI unit prefix.
248 : quint32 scale = 1;
249 323 : if (number.endsWith(QLatin1String("k"), Qt::CaseInsensitive)) {
250 51 : number.chop(1);
251 : scale = 1000;
252 272 : } else if (number.endsWith(QLatin1String("M"))) {
253 51 : number.chop(1);
254 : scale = 1000 * 1000;
255 : }
256 :
257 : // Parse the number as an (unsigned) integer.
258 532 : QLocale locale; bool ok;
259 266 : const quint16 integer = locale.toUInt(number, &ok);
260 323 : if (ok) {
261 136 : return integer * scale;
262 : }
263 :
264 : // Parse the number as a (double) floating point number, and check that it is positive.
265 154 : const double dbl = locale.toDouble(number, &ok);
266 187 : if ((ok) && (dbl > 0)) {
267 51 : return dbl * scale;
268 : }
269 :
270 : return 0; // Failed to parse as either integer, or float.
271 114 : }
272 :
273 : /*!
274 : * Processes the relevant options from the command line \a parser.
275 : *
276 : * On success, returns an empty QStringList, otherwise returns a list of CLI errors that the caller
277 : * should report appropriately before exiting.
278 : *
279 : * This base implementations performs some common checks, such as ensuring that required options are
280 : * present. Derived classes should override this function to perform further processing, typically
281 : * inovking this base implementation as a first step, such as:
282 : *
283 : * ```
284 : * QStringList CustomCommand::processOptions(const QCommandLineParser &parser)
285 : * {
286 : * QStringList errors = AbstractCommand::processOptions(parser);
287 : * if (!errors.isEmpty()) {
288 : * return errors;
289 : * }
290 : *
291 : * // Do further procession of options.
292 : *
293 : * return errors;
294 : * }
295 : * ```
296 : */
297 1936 : QStringList AbstractCommand::processOptions(const QCommandLineParser &parser)
298 : {
299 : // Report any supplied options that are not supported by this command.
300 1936 : const QStringList suppliedOptions = parser.optionNames();
301 1936 : const QStringList supportedOptions = this->supportedOptions(parser);
302 5638 : for (const QString &option: suppliedOptions) {
303 3702 : if (!supportedOptions.contains(option)) {
304 37 : qCInfo(lc).noquote() << tr("Ignoring option: %1").arg(option);
305 : }
306 : }
307 360 : QStringList errors;
308 :
309 : // Parse the device (name/addr/uuid) option.
310 2296 : if (parser.isSet(QLatin1String("device"))) {
311 40 : deviceToScanFor = parser.value(QLatin1String("device"));
312 : }
313 :
314 : // Parse the output format options (if supported, and supplied).
315 4099 : if ((supportedOptions.contains(QLatin1String("output"))) && // Derived classes may have removed.
316 4193 : (parser.isSet(QLatin1String("output"))))
317 : {
318 546 : const QString output = parser.value(QLatin1String("output")).toLower();
319 221 : if (output == QLatin1String("csv")) {
320 51 : format = OutputFormat::Csv;
321 170 : } else if (output == QLatin1String("json")) {
322 51 : format = OutputFormat::Json;
323 119 : } else if (output == QLatin1String("text")) {
324 51 : format = OutputFormat::Text;
325 : } else {
326 80 : errors.append(tr("Unknown output format: %1").arg(output));
327 : }
328 78 : }
329 :
330 : // Parse the device scan timeout option.
331 2296 : if (parser.isSet(QLatin1String("timeout"))) {
332 156 : const quint32 timeout = parseMilliValue(parser.value(QLatin1String("timeout")),
333 : QLatin1String("s"), 500);
334 117 : if (timeout == 0) {
335 108 : errors.append(tr("Invalid timeout: %1").arg(parser.value(QLatin1String("timeout"))));
336 : } else {
337 63 : discoveryAgent->setLowEnergyDiscoveryTimeout(timeout);
338 63 : qCDebug(lc).noquote() << tr("Set scan timeout to %1").arg(
339 0 : discoveryAgent->lowEnergyDiscoveryTimeout());
340 : }
341 : }
342 :
343 : // Return errors for any required options that are absent.
344 1936 : const QStringList requiredOptions = this->requiredOptions(parser);
345 4569 : for (const QString &option: requiredOptions) {
346 2633 : if (!parser.isSet(option)) {
347 240 : errors.append(tr("Missing required option: %1").arg(option));
348 : }
349 : }
350 1936 : return errors;
351 : }
352 :
353 : /*!
354 : * \fn virtual bool AbstractCommand::start()
355 : *
356 : * Begins the functionality of this command, and returns `true` if begun successfully, `false`
357 : * otherwise.
358 : */
359 :
360 : /*!
361 : * \fn virtual void AbstractCommand::deviceDiscovered(const QBluetoothDeviceInfo &info) = 0
362 : *
363 : * Handles PokitDiscoveryAgent::pokitDeviceDiscovered signal. Derived classes must
364 : * implement this slot to begin whatever actions are relevant when a Pokit device has been
365 : * discovered. For example, the 'scan' command would simply output the \a info details, whereas
366 : * most other commands would begin connecting if \a info is the device they're after.
367 : */
368 :
369 : /*!
370 : * \fn virtual void AbstractCommand::deviceDiscoveryFinished() = 0
371 : *
372 : * Handles PokitDiscoveryAgent::deviceDiscoveryFinished signal. Derived classes must
373 : * implement this slot to perform whatever actions are appropraite when discovery is finished.
374 : * For example, the 'scan' command would simply exit, whereas most other commands would verify that
375 : * an appropriate device was found.
376 : */
|