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