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