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