Line data Source code
1 : // SPDX-FileCopyrightText: 2022-2026 Paul Colby <git@colby.id.au>
2 : // SPDX-License-Identifier: LGPL-3.0-or-later
3 :
4 : #include "abstractcommand.h"
5 : #include "../stringliterals_p.h"
6 :
7 : #include <qtpokit/pokitdevice.h>
8 : #include <qtpokit/pokitdiscoveryagent.h>
9 :
10 : #include <QLocale>
11 : #include <QTimer>
12 : #include <QtMath>
13 :
14 : #include <cmath>
15 : #include <ratio>
16 :
17 : DOKIT_USE_STRINGLITERALS
18 :
19 : /*!
20 : * \class AbstractCommand
21 : *
22 : * The AbstractCommand class provides a consistent base for the classes that implement CLI commands.
23 : */
24 :
25 : /*!
26 : * Constructs a new command with \a parent.
27 : */
28 62781 : AbstractCommand::AbstractCommand(QObject * const parent) : QObject(parent),
29 62781 : discoveryAgent(new PokitDiscoveryAgent(this))
30 32451 : {
31 66729 : connect(discoveryAgent, &PokitDiscoveryAgent::pokitDeviceDiscovered,
32 47211 : this, &AbstractCommand::deviceDiscovered);
33 66729 : connect(discoveryAgent, &PokitDiscoveryAgent::finished,
34 47211 : this, &AbstractCommand::deviceDiscoveryFinished);
35 66729 : connect(discoveryAgent,
36 12237 : #if (QT_VERSION < QT_VERSION_CHECK(6, 2, 0))
37 12237 : QOverload<PokitDiscoveryAgent::Error>::of(&PokitDiscoveryAgent::error),
38 : #else
39 20214 : &PokitDiscoveryAgent::errorOccurred,
40 20214 : #endif
41 32592 : this, [](const PokitDiscoveryAgent::Error &error) {
42 699 : qCWarning(lc).noquote() << tr("Bluetooth discovery error:") << error;
43 168 : QTimer::singleShot(0, QCoreApplication::instance(), [](){
44 0 : QCoreApplication::exit(EXIT_FAILURE);
45 0 : });
46 288 : });
47 66729 : }
48 :
49 : /*!
50 : * Returns a list of CLI option names required by this command. The main console appication may
51 : * use this list to output an error (and exit) if any of the returned names are not found in the
52 : * parsed CLI options.
53 : *
54 : * The (already parsed) \a parser may be used adjust the returned required options depending on the
55 : * value of other options. For example, the `logger` command only requires the `--mode` option if
56 : * the `--command` option is `start`.
57 : *
58 : * This base implementation simply returns an empty list. Derived classes should override this
59 : * function to include any required options.
60 : */
61 29680 : QStringList AbstractCommand::requiredOptions(const QCommandLineParser &parser) const
62 32784 : {
63 32784 : Q_UNUSED(parser)
64 62464 : return QStringList();
65 32784 : }
66 :
67 : /*!
68 : * Returns a list of CLI option names supported by this command. The main console appication may
69 : * use this list to output a warning for any parsed CLI options not included in the returned list.
70 : *
71 : * The (already parsed) \a parser may be used adjust the returned supported options depending on the
72 : * value of other options. For example, the `logger` command only supported the `--timestamp` option
73 : * if the `--command` option is `start`.
74 : *
75 : * This base implementation simply returns requiredOptions(). Derived classes should override this
76 : * function to include optional options, such as:
77 : *
78 : * ```
79 : * QStringList Derived::supportedOptions(const QCommandLineParser &parser) const
80 : * {
81 : * const QStringList list = AbstractCommand::supportedOptions(parser) + QStringList{ ... };
82 : * list.sort();
83 : * list.removeDuplicates(); // Optional, recommended.
84 : * return list;
85 : * }
86 : * ```
87 : */
88 14560 : QStringList AbstractCommand::supportedOptions(const QCommandLineParser &parser) const
89 15948 : {
90 125564 : return requiredOptions(parser) + QStringList{
91 15948 : u"debug"_s,
92 15948 : u"device"_s, u"d"_s,
93 15948 : u"output"_s,
94 15948 : u"timeout"_s,
95 110796 : };
96 15948 : }
97 :
98 : /*!
99 : * Returns a human-readable version of \a value, with up-to \a precision decimal digits, and SI unit prefix when
100 : * appropiate.
101 : *
102 : * \note Only supports positive values. Tip: use appendSiPrefix(qAbs(...)) for now.
103 : */
104 1991 : QString AbstractCommand::appendSiPrefix(const double value, int precision) {
105 : // Scale the number to an appropriate SI prefix.
106 4257 : QStringList siPrefixes{ u""_s, u"m"_s, u"μ"_s };
107 1221 : int index = 0;
108 1991 : if (value != 0.)
109 4402 : for (index=0; (index < siPrefixes.length()-1) && (qAbs(value) * qPow(1000.0, (double)index) < 1); ++index);
110 1991 : QString number = QString::number(value * qPow(1000.0, (double)index), 'f', precision);
111 :
112 : // Trim trailing zeros, and decimal indicator if no decimals remain.
113 1991 : if (const DOKIT_STRING_INDEX_TYPE decimalPos = number.indexOf(u'.'); decimalPos > 0) {
114 1221 : DOKIT_STRING_INDEX_TYPE nonZeroPos;
115 13937 : for (nonZeroPos = number.length()-1; (nonZeroPos > decimalPos) && (number.at(nonZeroPos) == u'0'); --nonZeroPos);
116 1991 : number.truncate((nonZeroPos == decimalPos) ? nonZeroPos : nonZeroPos+1);
117 1221 : }
118 3267 : return QString(u"%1%2"_s).arg(number, siPrefixes.at(index));
119 1485 : }
120 :
121 : /*!
122 : * Returns an RFC 4180 compliant version of \a field. That is, if \a field contains any of the
123 : * the below four characters, than any double quotes are escaped (by addition double-quotes), and
124 : * the string itself surrounded in double-quotes. Otherwise, \a field is returned verbatim.
125 : *
126 : * Some examples:
127 : * ```
128 : * QCOMPARE(escapeCsvField("abc"), "abc"); // Returned unchanged.
129 : * QCOMPARE(escapeCsvField("a,c"), R"("a,c")"); // Wrapped in double-quotes.
130 : * QCOMPARE(escapeCsvField(R"(a"c)"), R("("a""c")"); // Existing double-quotes doubled, then wrapped.
131 : * ```
132 : */
133 8170 : QString AbstractCommand::escapeCsvField(const QString &field)
134 5688 : {
135 33888 : if (field.contains(','_L1) || field.contains('\r'_L1) || field.contains('"'_L1) || field.contains('\n'_L1)) {
136 392 : return uR"("%1")"_s.arg(QString(field).replace('"'_L1, uR"("")"_s));
137 5466 : } else return field;
138 5688 : }
139 :
140 : /*!
141 : * \internal
142 : * A (run-time) class approximately equivalent to the compile-time std::ratio template.
143 : */
144 : struct Ratio {
145 : std::intmax_t num { 0 }; ///< Numerator.
146 : std::intmax_t den { 0 }; ///< Denominator.
147 : //! Returns \a true if both #num and #den are non-zero.
148 34744 : bool isValid() const { return (num != 0) && (den != 0); }
149 : };
150 :
151 : /*!
152 : * \internal
153 : * Returns a (run-time) Ratio representation of (compile-time) ratio \a R.
154 : */
155 33622 : template<typename R> constexpr Ratio makeRatio() { return Ratio{ R::num, R::den }; }
156 :
157 : /*!
158 : * Returns \a value as an integer multiple of the ratio \a R, as number of type \a T. The string \a value
159 : * may end with the optional \a unit, such as `V` or `s`, which may also be preceded with a SI unit
160 : * prefix such as `m` for `milli`. If \a value contains no SI unit prefix, then the result will be
161 : * multiplied by 1,000 enough times to be greater than \a sensibleMinimum. This allows for
162 : * convenient use like:
163 : *
164 : * ```
165 : * const quint32 timeout = parseNumber<std::milli>(parser.value("window"), 's', (quint32)500'000);
166 : * ```
167 : *
168 : * So that an unqualified period like "300" will be assumed to be 300 milliseconds, and not 300
169 : * microseconds, while a period like "1000" will be assume to be 1 second.
170 : *
171 : * If conversion fails for any reason, quiet-NaN (if supported by \a T) or 0 is returned.
172 : */
173 : template<typename R, typename T>
174 23380 : T AbstractCommand::parseNumber(const QString &value, const QString &unit, const T sensibleMinimum)
175 18714 : {
176 42289 : static const QMap<QChar, Ratio> unitPrefixScaleMap {
177 18714 : { 'E'_L1, makeRatio<std::exa>() },
178 18714 : { 'P'_L1, makeRatio<std::peta>() },
179 18714 : { 'T'_L1, makeRatio<std::tera>() },
180 18714 : { 'G'_L1, makeRatio<std::giga>() },
181 18714 : { 'M'_L1, makeRatio<std::mega>() },
182 18714 : { 'K'_L1, makeRatio<std::kilo>() }, // Not official SI unit prefix, but commonly used.
183 18714 : { 'k'_L1, makeRatio<std::kilo>() },
184 18714 : { 'h'_L1, makeRatio<std::hecto>() },
185 18714 : { 'd'_L1, makeRatio<std::deci>() },
186 18714 : { 'c'_L1, makeRatio<std::centi>() },
187 18714 : { 'm'_L1, makeRatio<std::milli>() },
188 18714 : { 'u'_L1, makeRatio<std::micro>() }, // Not official SI unit prefix, but commonly used.
189 18714 : { QChar(0x00B5), makeRatio<std::micro>() }, // Unicode micro symbol (µ).
190 18714 : { QChar(0x03BC), makeRatio<std::micro>() }, // Unicode lower mu (μ).
191 18714 : { 'n'_L1, makeRatio<std::nano>() },
192 18714 : { 'p'_L1, makeRatio<std::pico>() },
193 18714 : { 'f'_L1, makeRatio<std::femto>() },
194 18714 : { 'a'_L1, makeRatio<std::atto>() },
195 18714 : };
196 :
197 : // Remove the optional (whole) unit suffix.
198 18714 : Ratio ratio;
199 18714 : QString number = value.trimmed();
200 42094 : if ((!unit.isEmpty()) && (number.endsWith(unit, Qt::CaseInsensitive))) {
201 24728 : number.chop(unit.length());
202 10168 : ratio = makeRatio<std::ratio<1>>();
203 10168 : }
204 :
205 : // Parse, and remove, the optional SI unit prefix.
206 42094 : if (!number.isEmpty()) {
207 17685 : #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
208 33003 : const QChar siPrefix = number.back(); // QString::back() introduced in Qt 5.10.
209 : #else
210 1584 : const QChar siPrefix = number.at(number.size() - 1);
211 918 : #endif
212 18603 : const auto iter = unitPrefixScaleMap.constFind(siPrefix);
213 41913 : if (iter != unitPrefixScaleMap.constEnd()) {
214 10584 : Q_ASSERT(iter->isValid());
215 26264 : ratio = *iter;
216 26264 : number.chop(1);
217 10584 : }
218 18603 : }
219 :
220 : // Parse the number as an (unsigned) integer.
221 42094 : QLocale locale; bool ok;
222 42094 : if (const qulonglong integer = locale.toULongLong(number, &ok); (ok)) {
223 32199 : if (integer == 0) {
224 111 : return static_cast<T>(integer); // Otherwise the next for loop would be be infinite.
225 111 : }
226 18068 : #define DOKIT_RESULT(var) (static_cast<T>(var) * ratio.num * R::den / ratio.den / R::num)
227 13258 : if (!ratio.isValid()) {
228 11810 : for (ratio = makeRatio<R>(); DOKIT_RESULT(integer) < sensibleMinimum; ratio.num *= 1000);
229 2934 : }
230 32018 : return DOKIT_RESULT(integer);
231 13369 : #undef DOKIT_RESULT
232 13369 : }
233 :
234 : // Parse the number as a (double) floating point number, and check that it is positive.
235 9895 : if (const double dbl = locale.toDouble(number, &ok); (ok) && (dbl > 0.0)) {
236 1958 : if (dbl == 0.) {
237 0 : return static_cast<T>(dbl); // Otherwise the next for loop would be be infinite.
238 0 : }
239 4100 : #define DOKIT_RESULT(var) (var * ratio.num * R::den / ratio.den / R::num)
240 1606 : if (!ratio.isValid()) {
241 1448 : for (ratio = makeRatio<R>(); DOKIT_RESULT(dbl) < sensibleMinimum; ratio.num *= 1000);
242 666 : }
243 2726 : return (std::is_integral_v<T>) ? static_cast<T>(std::llround(DOKIT_RESULT(dbl))) : DOKIT_RESULT(dbl);
244 1606 : #undef DOKIT_RESULT
245 1606 : }
246 :
247 : // Failed to parse as either integer, or float.
248 3739 : return (std::numeric_limits<T>::has_quiet_NaN) ? std::numeric_limits<T>::quiet_NaN() : 0;
249 28725 : }
250 :
251 : #define DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(Ratio, Type) template \
252 : Type AbstractCommand::parseNumber<Ratio>(const QString &value, const QString &unit, const Type sensibleMinimum)
253 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::exa, quint32);
254 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::peta, quint32);
255 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::tera, quint32);
256 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::giga, quint32);
257 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::mega, quint32);
258 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::kilo, quint32);
259 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::hecto, quint32);
260 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::deca, quint32);
261 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::ratio<1>, quint32);
262 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::ratio<1>, float);
263 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::deci, quint32);
264 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::centi, quint32);
265 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::milli, quint32);
266 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::micro, quint32);
267 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::nano, quint32);
268 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::pico, quint32);
269 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::femto, quint32);
270 : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::atto, quint32);
271 : #undef DOKIT_INSTANTIATE_TEMPLATE_FUNCTION
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 : * invoking 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 13090 : QStringList AbstractCommand::processOptions(const QCommandLineParser &parser)
298 13617 : {
299 : // Report any supplied options that are not supported by this command.
300 26707 : const QStringList suppliedOptionNames = parser.optionNames();
301 26707 : const QStringList supportedOptionNames = supportedOptions(parser);
302 74388 : for (const QString &option: suppliedOptionNames) {
303 61298 : if (!supportedOptionNames.contains(option)) {
304 331 : qCInfo(lc).noquote() << tr("Ignoring option: %1").arg(option);
305 111 : }
306 27138 : }
307 22219 : QStringList errors;
308 :
309 : // Parse the device (name/addr/uuid) option.
310 35309 : if (parser.isSet(u"device"_s)) {
311 454 : deviceToScanFor = parser.value(u"device"_s);
312 222 : }
313 :
314 : // Parse the output format options (if supported, and supplied).
315 43911 : if ((supportedOptionNames.contains(u"output"_s)) && // Derived classes may have removed.
316 39254 : (parser.isSet(u"output"_s)))
317 1443 : {
318 3263 : const QString output = parser.value(u"output"_s).toLower();
319 2353 : if (output == u"csv"_s) {
320 543 : format = OutputFormat::Csv;
321 1810 : } else if (output == u"json"_s) {
322 543 : format = OutputFormat::Json;
323 1267 : } else if (output == u"text"_s) {
324 543 : format = OutputFormat::Text;
325 444 : } else {
326 908 : errors.append(tr("Unknown output format: %1").arg(output));
327 444 : }
328 1755 : }
329 :
330 : // Parse the device scan timeout option.
331 35309 : if (parser.isSet(u"timeout"_s)) {
332 3081 : const quint32 timeout = parseNumber<std::milli>(parser.value(u"timeout"_s), u"s"_s, 500);
333 2353 : if (timeout == 0) {
334 1614 : errors.append(tr("Invalid timeout: %1").arg(parser.value(u"timeout"_s)));
335 1267 : } else if (discoveryAgent->lowEnergyDiscoveryTimeout() == -1) {
336 833 : qCWarning(lc).noquote() << tr("Platform does not support Bluetooth scan timeout");
337 777 : } else {
338 924 : discoveryAgent->setLowEnergyDiscoveryTimeout(timeout);
339 1064 : qCDebug(lc).noquote() << tr("Set scan timeout to %1").arg(
340 0 : discoveryAgent->lowEnergyDiscoveryTimeout());
341 714 : }
342 1443 : }
343 :
344 : // Return errors for any required options that are absent.
345 26707 : const QStringList requiredOptionNames = this->requiredOptions(parser);
346 47477 : for (const QString &option: requiredOptionNames) {
347 34047 : if (!parser.isSet(option)) {
348 2725 : errors.append(tr("Missing required option: %1").arg(option));
349 985 : }
350 15357 : }
351 26707 : return errors;
352 13617 : }
353 :
354 : /*!
355 : * \fn virtual bool AbstractCommand::start()
356 : *
357 : * Begins the functionality of this command, and returns `true` if begun successfully, `false`
358 : * otherwise.
359 : */
360 :
361 : /*!
362 : * \fn virtual void AbstractCommand::deviceDiscovered(const QBluetoothDeviceInfo &info) = 0
363 : *
364 : * Handles PokitDiscoveryAgent::pokitDeviceDiscovered signal. Derived classes must
365 : * implement this slot to begin whatever actions are relevant when a Pokit device has been
366 : * discovered. For example, the 'scan' command would simply output the \a info details, whereas
367 : * most other commands would begin connecting if \a info is the device they're after.
368 : */
369 :
370 : /*!
371 : * \fn virtual void AbstractCommand::deviceDiscoveryFinished() = 0
372 : *
373 : * Handles PokitDiscoveryAgent::deviceDiscoveryFinished signal. Derived classes must
374 : * implement this slot to perform whatever actions are appropriate when discovery is finished.
375 : * For example, the 'scan' command would simply exit, whereas most other commands would verify that
376 : * an appropriate device was found.
377 : */
|