Dokit
Internal development documentation
Loading...
Searching...
No Matches
main.cpp
1// SPDX-FileCopyrightText: 2022-2026 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
33
34static Q_LOGGING_CATEGORY(lc, "dokit.cli.main", QtInfoMsg);
35
37{
38 Q_DECLARE_TR_FUNCTIONS(cli_main)
39};
40
41inline bool haveConsole()
42{
43 #if defined(Q_OS_UNIX)
44 return isatty(STDERR_FILENO);
45 #elif defined(Q_OS_WIN)
46 return GetConsoleWindow();
47 #else
48 return false;
49 #endif
50}
51
52void 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 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 messagePattern.prepend(u"%{time process} %{threadid} %{type} "_s);
63 QLoggingCategory::setFilterRules(u"dokit.*.debug=true\npokit.*.debug=true"_s);
64 }
65
66 if (const QString color = parser.value(u"color"_s);
67 (color == u"yes"_s) || (color == u"auto"_s && haveConsole())) {
68 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 messagePattern.append(u"\x1b[0m"_s); // Reset.
75 }
76
77 qSetMessagePattern(messagePattern);
78}
79
80enum 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
96void showCliError(const QString &errorText)
97{
98 // Output the same way QCommandLineParser::showMessageAndExit() does.
99 const QString message = QCoreApplication::applicationName() + u": "_s + errorText + u'\n';
100 fputs(qUtf8Printable(message), stderr);
101}
102
103Command getCliCommand(const QStringList &posArguments)
104{
105 if (posArguments.isEmpty()) {
106 return Command::None;
107 }
108 if (posArguments.size() > 1) {
109 showCliError(Private::tr("More than one command: %1").arg(posArguments.join(u", "_s)));
110 ::exit(EXIT_FAILURE);
111 }
112
113 const QMap<QString, Command> supportedCommands {
114 { u"info"_s, Command::Info },
115 { u"status"_s, Command::Status },
116 { u"meter"_s, Command::Meter },
117 { u"dso"_s, Command::DSO },
118 { u"logger-start"_s, Command::LoggerStart },
119 { u"logger-stop"_s, Command::LoggerStop },
120 { u"logger-fetch"_s, Command::LoggerFetch },
121 { u"scan"_s, Command::Scan },
122 { u"set-name"_s, Command::SetName },
123 { u"set-torch"_s, Command::SetTorch },
124 { u"flash-led"_s, Command::FlashLed },
125 { u"calibrate"_s, Command::Calibrate },
126 };
127 const Command command = supportedCommands.value(posArguments.first().toLower(), Command::None);
128 if (command == Command::None) {
129 showCliError(Private::tr("Unknown command: %1").arg(posArguments.first()));
130 ::exit(EXIT_FAILURE);
131 }
132 return command;
133}
134
135Command parseCommandLine(const QStringList &appArguments, QCommandLineParser &parser)
136{
137 // Setup the command line options.
138 parser.addOptions({
139 { u"color"_s,
140 Private::tr("Colors the console output. Valid options are: yes, no and auto. The default is auto."),
141 u"yes|no|auto"_s, u"auto"_s},
142 {{u"debug"_s},
143 Private::tr("Enable debug output.")},
144 {{u"d"_s, u"device"_s},
145 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 Private::tr("device")},
148 });
149 parser.addHelpOption();
150 parser.addOptions({
151 {{u"interval"_s},
152 Private::tr("Set the update interval for DSO, 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 magnitude 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 Private::tr("interval")},
158 {{u"mode"_s},
159 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 Private::tr("mode")},
167 {{u"new-name"_s},
168 Private::tr("Give the desired new name for the set-name command."), Private::tr("name")},
169 {{u"output"_s},
170 Private::tr("Set the format for output. Supported "
171 "formats are: CSV, JSON and Text. All are case insensitive. The default is Text."),
172 Private::tr("format"),
173 Private::tr("text")},
174 {{u"range"_s},
175 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 Private::tr("range"), u"auto"_s},
180 {{u"samples"_s}, Private::tr("Set the number of samples to acquire."), Private::tr("count")},
181 {{u"sample-rate"_s}, Private::tr("Set the DSO sample rate. Suffixes such as 'k', 'M' and 'MHz' may be used. "
182 "If no suffix is present, the scale will be inferred from the magnitude of the given rate. Rate must be "
183 "between 1Hz and 1MHz."),
184 Private::tr("rate")},
185 {{u"temperature"_s},
186 Private::tr("Set the current ambient temperature for the calibration command."), Private::tr("degrees")},
187 {{u"timeout"_s},
188 Private::tr("Set the device discovery scan timeout. "
189 "Suffixes such as 's' and 'ms' (for seconds and milliseconds) may be used. "
190 "If no suffix is present, the units will be inferred from the magnitude of the given "
191 "interval. The default behaviour is no timeout."),
192 Private::tr("period")},
193 {{u"timestamp"_s},
194 Private::tr("Set the optional starting timestamp for data logging. Default to 'now'."),
195 Private::tr("period")},
196 {{u"trigger-level"_s}, Private::tr("Set the DSO trigger level."), Private::tr("level")},
197 {{u"trigger-mode"_s},
198 Private::tr("Set the DSO trigger mode. Supported modes are: free, rising and falling. The default is free."),
199 Private::tr("mode"), u"free"_s},
200 {{u"window-size"_s}, Private::tr("Set the DSO window size. If not specified, a sensible value will be chosen"
201 "according to the connected Pokit device's capabilities. Note, the limit is Pokit device dependant."),
202 Private::tr("samples")},
203 });
204 parser.addVersionOption();
205
206 // Add supported 'commands' (as positional arguments, so they'll appear in the help text).
207 parser.addPositionalArgument(u"info"_s, Private::tr("Get Pokit device information"), u" "_s);
208 parser.addPositionalArgument(u"status"_s, Private::tr("Get Pokit device status"), u" "_s);
209 parser.addPositionalArgument(u"meter"_s, Private::tr("Access Pokit device's multimeter mode"), u" "_s);
210 parser.addPositionalArgument(u"dso"_s, Private::tr("Access Pokit device's DSO mode"), u" "_s);
211 parser.addPositionalArgument(u"logger-start"_s, Private::tr("Start Pokit device's data logger mode"), u" "_s);
212 parser.addPositionalArgument(u"logger-stop"_s, Private::tr("Stop Pokit device's data logger mode"), u" "_s);
213 parser.addPositionalArgument(u"logger-fetch"_s, Private::tr("Fetch Pokit device's data logger samples"), u" "_s);
214 parser.addPositionalArgument(u"scan"_s, Private::tr("Scan Bluetooth for Pokit devices"), u" "_s);
215 parser.addPositionalArgument(u"set-name"_s, Private::tr("Set Pokit device's name"), u" "_s);
216 parser.addPositionalArgument(u"set-torch"_s, Private::tr("Set Pokit device's torch on or off"), u" "_s);
217 parser.addPositionalArgument(u"flash-led"_s, Private::tr("Flash Pokit device's LED (Pokit Meter only)"), u" "_s);
218 parser.addPositionalArgument(u"calibrate"_s, Private::tr("Calibrate Pokit device temperature"), u" "_s);
219
220 // Do the initial parse, the see if we have a command specified yet.
221 parser.parse(appArguments);
222 configureLogging(parser);
224 qCDebug(lc).noquote() << "Qt " QT_VERSION_STR " compile-time";
225 qCDebug(lc).noquote() << "Qt" << qVersion() << "runtime";
226 const Command command = getCliCommand(parser.positionalArguments());
227
228 // If we have a (single, valid) command, then remove the commands list from the help text.
229 if (command != Command::None) {
231 }
232
233 // Handle -h|--help explicitly, so we can tweak the output to include the <command> info.
234 if (parser.isSet(u"help"_s)) {
235 const QString commandString = (command == Command::None) ? u"<command>"_s
236 : parser.positionalArguments().constFirst();
237 std::cout << qUtf8Printable(parser.helpText()
238 .replace(u"[options]"_s, commandString + u" [options]"_s)
239 .replace(u"Arguments:"_s, u"Command:"_s)
240 );
241 ::exit(EXIT_SUCCESS);
242 }
243
244 // Process the command for real (ie throw errors for unknown options, etc).
245 parser.process(appArguments);
246 return command;
247}
248
249AbstractCommand * getCommandObject(const Command command, QObject * const parent)
250{
251 switch (command) {
252 case Command::None:
253 showCliError(Private::tr("Missing argument: <command>\nSee --help for usage information."));
254 return nullptr;
255 case Command::Calibrate: return new CalibrateCommand(parent);
256 case Command::DSO: return new DsoCommand(parent);
257 case Command::FlashLed: return new FlashLedCommand(parent);
258 case Command::Info: return new InfoCommand(parent);
259 case Command::LoggerStart: return new LoggerStartCommand(parent);
260 case Command::LoggerStop: return new LoggerStopCommand(parent);
261 case Command::LoggerFetch: return new LoggerFetchCommand(parent);
262 case Command::Meter: return new MeterCommand(parent);
263 case Command::Scan: return new ScanCommand(parent);
264 case Command::Status: return new StatusCommand(parent);
265 case Command::SetName: return new SetNameCommand(parent);
266 case Command::SetTorch: return new SetTorchCommand(parent);
267 }
268 showCliError(Private::tr("Unknown command (%1)").arg((int)command));
269 return nullptr;
270}
271
272int main(int argc, char *argv[])
273{
274 // Setup the core application.
275 QCoreApplication app(argc, argv);
276 QCoreApplication::setApplicationName(QStringLiteral(PROJECT_NAME));
278 #ifdef PROJECT_PRE_RELEASE
279 "-" PROJECT_PRE_RELEASE
280 #endif
281 #ifdef PROJECT_BUILD_ID
282 "+" PROJECT_BUILD_ID
283 #endif
284 ));
285
286#if defined(Q_OS_MACOS) || defined(Q_CC_MINGW)
287 // Qt ignores shell locale overrides on macOS (QTBUG-51386), so mimic Qt's handling of LANG on *nixes.
288 // Qt also applies LANG inconsistently with MinGW (QTBUG-139433), so mimic Qt's *nix handline there too.
289 const QString localeName = QString::fromLocal8Bit(qgetenv("LANG"));
290 if (!localeName.isEmpty()) {
291 const QLocale newLocale(localeName);
292 if (newLocale.name() != localeName.split('.'_L1).constFirst()) {
293 qCWarning(lc) << "Could not find locale" << localeName;
294 } else {
295 QLocale::setDefault(newLocale);
296 }
297 }
298#endif
299
300 // Install localised translators, if we have translations for the current locale.
301 const QLocale locale;
302 QTranslator appTranslator, libTranslator;
303 if (appTranslator.load(locale, u"cli"_s, u"/"_s, u":/i18n"_s)) {
305 }
306 if (libTranslator.load(locale, u"lib"_s, u"/"_s, u":/i18n"_s)) {
308 }
309
310 // Parse the command line.
311 const QStringList appArguments = QCoreApplication::arguments();
312 QCommandLineParser parser;
313 const Command commandType = parseCommandLine(appArguments, parser);
314 qCDebug(lc).noquote() << "Locale:" << locale << locale.uiLanguages();
315#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) // QTranslator::filePath() added in Qt 5.15.
316 qCDebug(lc).noquote() << "App translations:" <<
317 (appTranslator.filePath().isEmpty() ? u"<none>"_s : appTranslator.filePath());
318 qCDebug(lc).noquote() << "Library translations:" <<
319 (libTranslator.filePath().isEmpty() ? u"<none>"_s : libTranslator.filePath());
320#else
321 qCDebug(lc).noquote() << "App translations:" << (!appTranslator.isEmpty());
322 qCDebug(lc).noquote() << "Lib translations:" << (!libTranslator.isEmpty());
323#endif
324
325 // Handle the given command.
326 AbstractCommand * const command = getCommandObject(commandType, &app);
327 if (command == nullptr) {
328 return EXIT_FAILURE; // getCommandObject will have logged the reason already.
329 }
330 const QStringList cliErrors = command->processOptions(parser);
331 for (const QString &error: cliErrors) {
332 showCliError(error);
333 }
334 const int result = ((cliErrors.isEmpty()) && (command->start())) ? QCoreApplication::exec() : EXIT_FAILURE;
335 delete command; // We don't strictly need to do this, but it does fix QTBUG-119063, and is probably good practice.
336 return result;
337}
The AbstractCommand class provides a consistent base for the classes that implement CLI commands.
virtual bool start()=0
Begins the functionality of this command, and returns true if begun successfully, false otherwise.
virtual QStringList processOptions(const QCommandLineParser &parser)
Processes the relevant options from the command line parser.
CalibrateCommand(QObject *const parent=nullptr)
Construct a new CalibrateCommand object with parent.
DsoCommand(QObject *const parent=nullptr)
Construct a new DsoCommand object with parent.
FlashLedCommand(QObject *const parent=nullptr)
Construct a new FlashLedCommand object with parent.
InfoCommand(QObject *const parent=nullptr)
Construct a new InfoCommand object with parent.
LoggerFetchCommand(QObject *const parent=nullptr)
Construct a new LoggerFetchCommand object with parent.
LoggerStartCommand(QObject *const parent=nullptr)
Construct a new LoggerStartCommand object with parent.
LoggerStopCommand(QObject *const parent=nullptr)
Construct a new LoggerStopCommand object with parent.
MeterCommand(QObject *const parent=nullptr)
Construct a new MeterCommand object with parent.
ScanCommand(QObject *const parent=nullptr)
Construct a new ScanCommand object with parent.
SetNameCommand(QObject *const parent=nullptr)
Construct a new SetNameCommand object with parent.
SetTorchCommand(QObject *const parent=nullptr)
Construct a new SetTorchCommand object with parent.
StatusCommand(QObject *const parent=nullptr)
Construct a new StatusCommand object with parent.
QCommandLineOption addHelpOption()
bool addOptions(const QList< QCommandLineOption > &options)
void addPositionalArgument(const QString &name, const QString &description, const QString &syntax)
QCommandLineOption addVersionOption()
void clearPositionalArguments()
QString helpText() const const
bool isSet(const QString &name) const const
bool parse(const QStringList &arguments)
QStringList positionalArguments() const const
void process(const QStringList &arguments)
QString value(const QString &optionName) const const
QString applicationName
QString applicationVersion
QStringList arguments()
bool installTranslator(QTranslator *translationFile)
const T & constFirst() const const
T & first()
bool isEmpty() const const
int size() const const
void setDefault(const QLocale &locale)
QStringList uiLanguages() const const
void setFilterRules(const QString &rules)
const T value(const Key &key, const T &defaultValue) const const
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString & append(QChar ch)
QString fromLatin1(const char *str, int size)
QString fromLocal8Bit(const char *str, int size)
bool isEmpty() const const
QString & prepend(QChar ch)
QString & replace(int position, int n, QChar after)
QString join(const QString &separator) const const
QString filePath() const const
virtual bool isEmpty() const const
bool load(const QString &filename, const QString &directory, const QString &search_delimiters, const QString &suffix)
Declares the DOKIT_USE_STRINGLITERALS macro, and related functions.
#define DOKIT_USE_STRINGLITERALS
Internal macro for using either official Qt string literals (added in Qt 6.4), or our own equivalent ...