Line data Source code
1 : // SPDX-FileCopyrightText: 2022-2024 Paul Colby <git@colby.id.au>
2 : // SPDX-License-Identifier: LGPL-3.0-or-later
3 :
4 : #include "calibratecommand.h"
5 : #include "dsocommand.h"
6 : #include "flashledcommand.h"
7 : #include "infocommand.h"
8 : #include "loggerfetchcommand.h"
9 : #include "loggerstartcommand.h"
10 : #include "loggerstopcommand.h"
11 : #include "metercommand.h"
12 : #include "scancommand.h"
13 : #include "setnamecommand.h"
14 : #include "settorchcommand.h"
15 : #include "statuscommand.h"
16 :
17 : #include <QCommandLineParser>
18 : #include <QCoreApplication>
19 : #include <QLocale>
20 : #include <QLoggingCategory>
21 : #include <QTranslator>
22 :
23 : #include <iostream>
24 :
25 : #if defined(Q_OS_UNIX)
26 : #include <unistd.h>
27 : #elif defined(Q_OS_WIN)
28 : #include <Windows.h>
29 : #endif
30 :
31 64 : static Q_LOGGING_CATEGORY(lc, "dokit.cli.main", QtInfoMsg);
32 :
33 : inline bool haveConsole()
34 : {
35 : #if defined(Q_OS_UNIX)
36 20 : return isatty(STDERR_FILENO);
37 : #elif defined(Q_OS_WIN)
38 : return GetConsoleWindow();
39 : #else
40 : return false;
41 : #endif
42 : }
43 :
44 20 : void configureLogging(const QCommandLineParser &parser)
45 : {
46 : // Start with the Qt default message pattern (see qtbase:::qlogging.cpp:defaultPattern)
47 8 : QString messagePattern = QStringLiteral("%{if-category}%{category}: %{endif}%{message}");
48 :
49 28 : if (parser.isSet(QStringLiteral("debug"))) {
50 : #ifdef QT_MESSAGELOGCONTEXT
51 : // %{file}, %{line} and %{function} are only available when QT_MESSAGELOGCONTEXT is set.
52 : messagePattern.prepend(QStringLiteral("%{function} "));
53 : #endif
54 0 : messagePattern.prepend(QStringLiteral("%{time process} %{threadid} %{type} "));
55 0 : QLoggingCategory::setFilterRules(QStringLiteral("dokit.*.debug=true\npokit.*.debug=true"));
56 : }
57 :
58 28 : if (const QString color = parser.value(QStringLiteral("color"));
59 68 : (color == QStringLiteral("yes")) || (color == QStringLiteral("auto") && haveConsole())) {
60 0 : messagePattern.prepend(QStringLiteral(
61 : "%{if-debug}\x1b[37m%{endif}" // White
62 : "%{if-info}\x1b[32m%{endif}" // Green
63 : "%{if-warning}\x1b[35m%{endif}" // Magenta
64 : "%{if-critical}\x1b[31m%{endif}" // Red
65 : "%{if-fatal}\x1b[31;1m%{endif}")); // Red and bold
66 0 : messagePattern.append(QStringLiteral("\x1b[0m")); // Reset.
67 12 : }
68 :
69 20 : qSetMessagePattern(messagePattern);
70 20 : }
71 :
72 : enum class Command {
73 : None,
74 : Info,
75 : Status,
76 : Meter,
77 : DSO,
78 : LoggerStart,
79 : LoggerStop,
80 : LoggerFetch,
81 : Scan,
82 : SetName,
83 : SetTorch,
84 : FlashLed,
85 : Calibrate
86 : };
87 :
88 0 : void showCliError(const QString &errorText)
89 : {
90 : // Output the same way QCommandLineParser does (qcommandlineparser.cpp::showParserMessage).
91 0 : const QString message = QCoreApplication::applicationName() + QLatin1String(": ")
92 0 : + errorText + QLatin1Char('\n');
93 0 : std::cerr << qUtf8Printable(message);
94 0 : }
95 :
96 20 : Command getCliCommand(const QStringList &posArguments)
97 : {
98 20 : if (posArguments.isEmpty()) {
99 : return Command::None;
100 : }
101 0 : if (posArguments.size() > 1) {
102 0 : showCliError(QCoreApplication::translate("getCliCommand", "More than one command: %1")
103 0 : .arg(posArguments.join(QStringLiteral(", "))));
104 0 : ::exit(EXIT_FAILURE);
105 : }
106 :
107 : const QMap<QString, Command> supportedCommands {
108 0 : { QStringLiteral("info"), Command::Info },
109 0 : { QStringLiteral("status"), Command::Status },
110 0 : { QStringLiteral("meter"), Command::Meter },
111 0 : { QStringLiteral("dso"), Command::DSO },
112 0 : { QStringLiteral("logger-start"), Command::LoggerStart },
113 0 : { QStringLiteral("logger-stop"), Command::LoggerStop },
114 0 : { QStringLiteral("logger-fetch"), Command::LoggerFetch },
115 0 : { QStringLiteral("scan"), Command::Scan },
116 0 : { QStringLiteral("set-name"), Command::SetName },
117 0 : { QStringLiteral("set-torch"), Command::SetTorch },
118 0 : { QStringLiteral("flash-led"), Command::FlashLed },
119 0 : { QStringLiteral("calibrate"), Command::Calibrate },
120 0 : };
121 0 : const Command command = supportedCommands.value(posArguments.first().toLower(), Command::None);
122 0 : if (command == Command::None) {
123 0 : showCliError(QCoreApplication::translate("getCliCommand", "Unknown command: %1").arg(posArguments.first()));
124 0 : ::exit(EXIT_FAILURE);
125 : }
126 : return command;
127 0 : }
128 :
129 20 : Command parseCommandLine(const QStringList &appArguments, QCommandLineParser &parser)
130 : {
131 : // Setup the command line options.
132 132 : parser.addOptions({
133 32 : { QStringLiteral("color"),
134 40 : QCoreApplication::translate("parseCommandLine", "Colors the console output. Valid options "
135 : "are: yes, no and auto. The default is auto."),
136 20 : QStringLiteral("yes|no|auto"), QStringLiteral("auto")},
137 20 : {{QStringLiteral("debug")},
138 40 : QCoreApplication::translate("parseCommandLine", "Enable debug output.")},
139 8 : {{QStringLiteral("d"), QStringLiteral("device")},
140 35 : QCoreApplication::translate("parseCommandLine",
141 : "Set the name, hardware address or macOS UUID of Pokit device to use. If not specified, "
142 : "the first discovered Pokit device will be used."),
143 40 : QCoreApplication::translate("parseCommandLine", "device")},
144 : });
145 20 : parser.addHelpOption();
146 248 : parser.addOptions({
147 32 : {{QStringLiteral("interval")},
148 40 : QCoreApplication::translate("parseCommandLine", "Set the update interval for DOS, meter and "
149 : "logger modes. Suffixes such as 's' and 'ms' (for seconds and milliseconds) may be used. "
150 : "If no suffix is present, the units will be inferred from the magnitide of the given "
151 : "interval. If the option itself is not specified, a sensible default will be chosen "
152 : "according to the selected command."),
153 40 : QCoreApplication::translate("parseCommandLine", "interval")},
154 28 : {{QStringLiteral("mode")},
155 40 : QCoreApplication::translate("parseCommandLine", "Set the desired operation mode. For "
156 : "meter, dso, and logger commands, the supported modes are: AC Voltage, DC Voltage, AC Current, "
157 : "DC Current, Resistance, Diode, Continuity, and Temperature. All are case insensitive. "
158 : "Only the first four options are available for dso and logger commands; the rest are "
159 : "available in meter mode only. Temperature is also available for logger commands, but "
160 : "requires firmware v1.5 or later for Pokit devices to support it. For the set-torch command "
161 : "supported modes are On and Off."),
162 40 : QCoreApplication::translate("parseCommandLine", "mode")},
163 28 : {{QStringLiteral("new-name")},
164 40 : QCoreApplication::translate("parseCommandLine","Give the desired new name for the set-"
165 40 : "name command."), QCoreApplication::translate("parseCommandLine", "name")},
166 28 : {{QStringLiteral("output")},
167 40 : QCoreApplication::translate("parseCommandLine","Set the format for output. Supported "
168 : "formats are: CSV, JSON and Text. All are case insenstitve. The default is Text."),
169 40 : QCoreApplication::translate("parseCommandLine", "format"),
170 40 : QCoreApplication::translate("parseCommandLine", "text")},
171 20 : {{QStringLiteral("range")},
172 40 : QCoreApplication::translate("parseCommandLine","Set the desired measurement range. Pokit "
173 : "devices support specific ranges, such as 0 to 300mV. Specify the desired upper limit, "
174 : "and the best range will be selected, or use 'auto' to enable the Pokit device's auto-"
175 : "range feature. The default is 'auto'."),
176 40 : QCoreApplication::translate("parseCommandLine", "range"), QStringLiteral("auto")},
177 20 : {{QStringLiteral("samples")},
178 40 : QCoreApplication::translate("parseCommandLine","Set the number of samples to acquire."),
179 40 : QCoreApplication::translate("parseCommandLine", "count")},
180 28 : {{QStringLiteral("temperature")},
181 40 : QCoreApplication::translate("parseCommandLine","Set the current ambient temperature for "
182 40 : "the calibration command."), QCoreApplication::translate("parseCommandLine", "degrees")},
183 28 : {{QStringLiteral("timeout")},
184 40 : QCoreApplication::translate("parseCommandLine","Set the device discovery scan timeout."
185 : "Suffixes such as 's' and 'ms' (for seconds and milliseconds) may be used. "
186 : "If no suffix is present, the units will be inferred from the magnitide of the given "
187 : "interval. The default behaviour is no timeout."),
188 40 : QCoreApplication::translate("parseCommandLine","period")},
189 28 : {{QStringLiteral("timestamp")},
190 40 : QCoreApplication::translate("parseCommandLine","Set the optional starting timestamp for "
191 : "data logging. Default to 'now'."),
192 40 : QCoreApplication::translate("parseCommandLine","period")},
193 28 : {{QStringLiteral("trigger-level")},
194 40 : QCoreApplication::translate("parseCommandLine","Set the DSO trigger level."),
195 40 : QCoreApplication::translate("parseCommandLine", "level")},
196 28 : {{QStringLiteral("trigger-mode")},
197 40 : QCoreApplication::translate("parseCommandLine","Set the DSO trigger mode. Supported "
198 : "modes are: free, rising and falling. The default is free."),
199 40 : QCoreApplication::translate("parseCommandLine", "mode"), QStringLiteral("free")},
200 : });
201 20 : parser.addVersionOption();
202 :
203 : // Add supported 'commands' (as positional arguments, so they'll appear in the help text).
204 28 : parser.addPositionalArgument(QStringLiteral("info"),
205 40 : QCoreApplication::translate("parseCommandLine", "Get Pokit device information"),
206 20 : QStringLiteral(" "));
207 28 : parser.addPositionalArgument(QStringLiteral("status"),
208 40 : QCoreApplication::translate("parseCommandLine", "Get Pokit device status"),
209 20 : QStringLiteral(" "));
210 28 : parser.addPositionalArgument(QStringLiteral("meter"),
211 40 : QCoreApplication::translate("parseCommandLine", "Access Pokit device's multimeter mode"),
212 20 : QStringLiteral(" "));
213 28 : parser.addPositionalArgument(QStringLiteral("dso"),
214 40 : QCoreApplication::translate("parseCommandLine", "Access Pokit device's DSO mode"),
215 20 : QStringLiteral(" "));
216 28 : parser.addPositionalArgument(QStringLiteral("logger-start"),
217 40 : QCoreApplication::translate("parseCommandLine", "Start Pokit device's data logger mode"),
218 20 : QStringLiteral(" "));
219 28 : parser.addPositionalArgument(QStringLiteral("logger-stop"),
220 40 : QCoreApplication::translate("parseCommandLine", "Stop Pokit device's data logger mode"),
221 20 : QStringLiteral(" "));
222 28 : parser.addPositionalArgument(QStringLiteral("logger-fetch"),
223 40 : QCoreApplication::translate("parseCommandLine", "Fetch Pokit device's data logger samples"),
224 20 : QStringLiteral(" "));
225 28 : parser.addPositionalArgument(QStringLiteral("scan"),
226 40 : QCoreApplication::translate("parseCommandLine", "Scan Bluetooth for Pokit devices"),
227 20 : QStringLiteral(" "));
228 28 : parser.addPositionalArgument(QStringLiteral("set-name"),
229 40 : QCoreApplication::translate("parseCommandLine", "Set Pokit device's name"),
230 20 : QStringLiteral(" "));
231 28 : parser.addPositionalArgument(QStringLiteral("set-torch"),
232 40 : QCoreApplication::translate("parseCommandLine", "Set Pokit device's torch on or off"),
233 20 : QStringLiteral(" "));
234 28 : parser.addPositionalArgument(QStringLiteral("flash-led"),
235 40 : QCoreApplication::translate("parseCommandLine", "Flash Pokit device's LED (Pokit Meter only)"),
236 20 : QStringLiteral(" "));
237 28 : parser.addPositionalArgument(QStringLiteral("calibrate"),
238 40 : QCoreApplication::translate("parseCommandLine", "Calibrate Pokit device temperature"),
239 20 : QStringLiteral(" "));
240 :
241 : // Do the initial parse, the see if we have a command specified yet.
242 20 : parser.parse(appArguments);
243 20 : configureLogging(parser);
244 24 : qCDebug(lc).noquote() << QCoreApplication::applicationName() << QCoreApplication::applicationVersion();
245 24 : qCDebug(lc).noquote() << "Qt " QT_VERSION_STR " compile-time";
246 24 : qCDebug(lc).noquote() << "Qt" << qVersion() << "runtime";
247 20 : const Command command = getCliCommand(parser.positionalArguments());
248 :
249 : // If we have a (single, valid) command, then remove the commands list from the help text.
250 20 : if (command != Command::None) {
251 0 : parser.clearPositionalArguments();
252 : }
253 :
254 : // Handle -h|--help explicitly, so we can tweak the output to include the <command> info.
255 28 : if (parser.isSet(QStringLiteral("help"))) {
256 0 : const QString commandString = (command == Command::None) ? QStringLiteral("<command>")
257 0 : : parser.positionalArguments().constFirst();
258 0 : std::cout << qUtf8Printable(parser.helpText()
259 : .replace(QStringLiteral("[options]"), commandString + QStringLiteral(" [options]"))
260 : .replace(QStringLiteral("Arguments:"), QStringLiteral("Command:"))
261 : );
262 0 : ::exit(EXIT_SUCCESS);
263 0 : }
264 :
265 : // Process the command for real (ie throw errors for unknown options, etc).
266 20 : parser.process(appArguments);
267 0 : return command;
268 485 : }
269 :
270 0 : AbstractCommand * getCommandObject(const Command command, QObject * const parent)
271 : {
272 0 : switch (command) {
273 0 : case Command::None:
274 0 : showCliError(QCoreApplication::translate("main",
275 : "Missing argument: <command>\nSee --help for usage information."));
276 0 : return nullptr;
277 0 : case Command::Calibrate: return new CalibrateCommand(parent);
278 0 : case Command::DSO: return new DsoCommand(parent);
279 0 : case Command::FlashLed: return new FlashLedCommand(parent);
280 0 : case Command::Info: return new InfoCommand(parent);
281 0 : case Command::LoggerStart: return new LoggerStartCommand(parent);
282 0 : case Command::LoggerStop: return new LoggerStopCommand(parent);
283 0 : case Command::LoggerFetch: return new LoggerFetchCommand(parent);
284 0 : case Command::Meter: return new MeterCommand(parent);
285 0 : case Command::Scan: return new ScanCommand(parent);
286 0 : case Command::Status: return new StatusCommand(parent);
287 0 : case Command::SetName: return new SetNameCommand(parent);
288 0 : case Command::SetTorch: return new SetTorchCommand(parent);
289 : }
290 0 : showCliError(QCoreApplication::translate("main", "Unknown command (%1)").arg((int)command));
291 0 : return nullptr;
292 : }
293 :
294 20 : int main(int argc, char *argv[])
295 : {
296 : // Setup the core application.
297 20 : QCoreApplication app(argc, argv);
298 28 : QCoreApplication::setApplicationName(QStringLiteral(PROJECT_NAME));
299 20 : QCoreApplication::setApplicationVersion(QString::fromLatin1(PROJECT_VERSION
300 : #ifdef PROJECT_PRE_RELEASE
301 : "-" PROJECT_PRE_RELEASE
302 : #endif
303 : #ifdef PROJECT_BUILD_ID
304 : "+" PROJECT_BUILD_ID
305 : #endif
306 : ));
307 :
308 : // Install localised translators, if we have translations for the current locale.
309 20 : QTranslator appTranslator, libTranslator;
310 20 : if (appTranslator.load(QLocale(), QStringLiteral("cli"), QStringLiteral("/"), QStringLiteral(":/i18n"))) {
311 0 : QCoreApplication::installTranslator(&appTranslator);
312 : }
313 20 : if (libTranslator.load(QLocale(), QStringLiteral("lib"), QStringLiteral("/"), QStringLiteral(":/i18n"))) {
314 0 : QCoreApplication::installTranslator(&libTranslator);
315 : }
316 :
317 : // Parse the command line.
318 20 : const QStringList appArguments = QCoreApplication::arguments();
319 20 : QCommandLineParser parser;
320 20 : const Command commandType = parseCommandLine(appArguments, parser);
321 : #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) // QTranslator::filePath() added in Qt 5.15.
322 0 : qCDebug(lc).noquote() << "App translations:" <<
323 0 : (appTranslator.filePath().isEmpty() ? QStringLiteral("<none>") : appTranslator.filePath());
324 0 : qCDebug(lc).noquote() << "Library translations:" <<
325 0 : (libTranslator.filePath().isEmpty() ? QStringLiteral("<none>") : libTranslator.filePath());
326 : #else
327 0 : qCDebug(lc).noquote() << "App translations:" << (!appTranslator.isEmpty());
328 0 : qCDebug(lc).noquote() << "Lib translations:" << (!libTranslator.isEmpty());
329 : #endif
330 :
331 : // Handle the given command.
332 0 : AbstractCommand * const command = getCommandObject(commandType, &app);
333 0 : if (command == nullptr) {
334 : return EXIT_FAILURE; // getCommandObject will have logged the reason already.
335 : }
336 0 : const QStringList cliErrors = command->processOptions(parser);
337 0 : for (const QString &error: cliErrors) {
338 0 : showCliError(error);
339 : }
340 0 : const int result = ((cliErrors.isEmpty()) && (command->start())) ? QCoreApplication::exec() : EXIT_FAILURE;
341 0 : delete command; // We don't strictly need to do this, but it does fix QTBUG-119063, and is probably good practice.
342 : return result;
343 0 : }
|