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.
The CalibrateCommand class implements the calibrate CLI command.
The DsoCommand class implements the dso CLI command.
Definition dsocommand.h:11
The FlashLedCommand class implements the flash-led CLI command.
The InfoCommand class implements the info CLI command.
Definition infocommand.h:9
The LoggerFetchCommand class implements the logger CLI command.
The LoggerStartCommand class implements the logger CLI command.
The LoggerStopCommand class implements the logger stop CLI command.
The MeterCommand class implements the meter CLI command.
The ScanCommand class implements the scan CLI command, by scanning for nearby Pokit Bluetooth devices...
Definition scancommand.h:7
The SetNameCommand class implements the set-name CLI command.
The SetTorchCommand class implements the set-torch CLI command.
The StatusCommand class implements the status CLI command.
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
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 ...