Dokit
Internal development documentation
Loading...
Searching...
No Matches
main.cpp
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
31static Q_LOGGING_CATEGORY(lc, "dokit.cli.main", QtInfoMsg);
32
34{
35 Q_DECLARE_TR_FUNCTIONS(cli_main)
36};
37
38inline bool haveConsole()
39{
40 #if defined(Q_OS_UNIX)
41 return isatty(STDERR_FILENO);
42 #elif defined(Q_OS_WIN)
43 return GetConsoleWindow();
44 #else
45 return false;
46 #endif
47}
48
49void configureLogging(const QCommandLineParser &parser)
50{
51 // Start with the Qt default message pattern (see qtbase:::qlogging.cpp:defaultPattern)
52 QString messagePattern = QStringLiteral("%{if-category}%{category}: %{endif}%{message}");
53
54 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 messagePattern.prepend(QStringLiteral("%{time process} %{threadid} %{type} "));
60 QLoggingCategory::setFilterRules(QStringLiteral("dokit.*.debug=true\npokit.*.debug=true"));
61 }
62
63 if (const QString color = parser.value(QStringLiteral("color"));
64 (color == QStringLiteral("yes")) || (color == QStringLiteral("auto") && haveConsole())) {
65 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 messagePattern.append(QStringLiteral("\x1b[0m")); // Reset.
72 }
73
74 qSetMessagePattern(messagePattern);
75}
76
77enum 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
93void showCliError(const QString &errorText)
94{
95 // Output the same way QCommandLineParser::showMessageAndExit() does.
96 const QString message = QCoreApplication::applicationName() + QLatin1String(": ") + errorText + u'\n';
97 fputs(qUtf8Printable(message), stderr);
98}
99
100Command getCliCommand(const QStringList &posArguments)
101{
102 if (posArguments.isEmpty()) {
103 return Command::None;
104 }
105 if (posArguments.size() > 1) {
106 showCliError(Private::tr("More than one command: %1").arg(posArguments.join(QStringLiteral(", "))));
107 ::exit(EXIT_FAILURE);
108 }
109
110 const QMap<QString, Command> supportedCommands {
111 { QStringLiteral("info"), Command::Info },
112 { QStringLiteral("status"), Command::Status },
113 { QStringLiteral("meter"), Command::Meter },
114 { QStringLiteral("dso"), Command::DSO },
115 { QStringLiteral("logger-start"), Command::LoggerStart },
116 { QStringLiteral("logger-stop"), Command::LoggerStop },
117 { QStringLiteral("logger-fetch"), Command::LoggerFetch },
118 { QStringLiteral("scan"), Command::Scan },
119 { QStringLiteral("set-name"), Command::SetName },
120 { QStringLiteral("set-torch"), Command::SetTorch },
121 { QStringLiteral("flash-led"), Command::FlashLed },
122 { QStringLiteral("calibrate"), Command::Calibrate },
123 };
124 const Command command = supportedCommands.value(posArguments.first().toLower(), Command::None);
125 if (command == Command::None) {
126 showCliError(Private::tr("Unknown command: %1").arg(posArguments.first()));
127 ::exit(EXIT_FAILURE);
128 }
129 return command;
130}
131
132Command parseCommandLine(const QStringList &appArguments, QCommandLineParser &parser)
133{
134 // Setup the command line options.
135 parser.addOptions({
136 { QStringLiteral("color"),
137 Private::tr("Colors the console output. Valid options are: yes, no and auto. The default is auto."),
138 QStringLiteral("yes|no|auto"), QStringLiteral("auto")},
139 {{QStringLiteral("debug")},
140 Private::tr("Enable debug output.")},
141 {{QStringLiteral("d"), QStringLiteral("device")},
142 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 Private::tr("device")},
145 });
146 parser.addHelpOption();
147 parser.addOptions({
148 {{QStringLiteral("interval")},
149 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 Private::tr("interval")},
155 {{QStringLiteral("mode")},
156 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 Private::tr("mode")},
164 {{QStringLiteral("new-name")},
165 Private::tr("Give the desired new name for the set-name command."), Private::tr("name")},
166 {{QStringLiteral("output")},
167 Private::tr("Set the format for output. Supported "
168 "formats are: CSV, JSON and Text. All are case insenstitve. The default is Text."),
169 Private::tr("format"),
170 Private::tr("text")},
171 {{QStringLiteral("range")},
172 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 Private::tr("range"), QStringLiteral("auto")},
177 {{QStringLiteral("samples")}, Private::tr("Set the number of samples to acquire."), Private::tr("count")},
178 {{QStringLiteral("temperature")},
179 Private::tr("Set the current ambient temperature for the calibration command."), Private::tr("degrees")},
180 {{QStringLiteral("timeout")},
181 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 Private::tr("period")},
186 {{QStringLiteral("timestamp")},
187 Private::tr("Set the optional starting timestamp for data logging. Default to 'now'."),
188 Private::tr("period")},
189 {{QStringLiteral("trigger-level")}, Private::tr("Set the DSO trigger level."), Private::tr("level")},
190 {{QStringLiteral("trigger-mode")},
191 Private::tr("Set the DSO trigger mode. Supported modes are: free, rising and falling. The default is free."),
192 Private::tr("mode"), QStringLiteral("free")},
193 });
194 parser.addVersionOption();
195
196 // Add supported 'commands' (as positional arguments, so they'll appear in the help text).
197 parser.addPositionalArgument(QStringLiteral("info"),
198 Private::tr("Get Pokit device information"), QStringLiteral(" "));
199 parser.addPositionalArgument(QStringLiteral("status"),
200 Private::tr("Get Pokit device status"), QStringLiteral(" "));
201 parser.addPositionalArgument(QStringLiteral("meter"),
202 Private::tr("Access Pokit device's multimeter mode"), QStringLiteral(" "));
203 parser.addPositionalArgument(QStringLiteral("dso"),
204 Private::tr("Access Pokit device's DSO mode"), QStringLiteral(" "));
205 parser.addPositionalArgument(QStringLiteral("logger-start"),
206 Private::tr("Start Pokit device's data logger mode"), QStringLiteral(" "));
207 parser.addPositionalArgument(QStringLiteral("logger-stop"),
208 Private::tr("Stop Pokit device's data logger mode"), QStringLiteral(" "));
209 parser.addPositionalArgument(QStringLiteral("logger-fetch"),
210 Private::tr("Fetch Pokit device's data logger samples"), QStringLiteral(" "));
211 parser.addPositionalArgument(QStringLiteral("scan"),
212 Private::tr("Scan Bluetooth for Pokit devices"), QStringLiteral(" "));
213 parser.addPositionalArgument(QStringLiteral("set-name"),
214 Private::tr("Set Pokit device's name"), QStringLiteral(" "));
215 parser.addPositionalArgument(QStringLiteral("set-torch"),
216 Private::tr("Set Pokit device's torch on or off"), QStringLiteral(" "));
217 parser.addPositionalArgument(QStringLiteral("flash-led"),
218 Private::tr("Flash Pokit device's LED (Pokit Meter only)"), QStringLiteral(" "));
219 parser.addPositionalArgument(QStringLiteral("calibrate"),
220 Private::tr("Calibrate Pokit device temperature"), QStringLiteral(" "));
221
222 // Do the initial parse, the see if we have a command specified yet.
223 parser.parse(appArguments);
224 configureLogging(parser);
226 qCDebug(lc).noquote() << "Qt " QT_VERSION_STR " compile-time";
227 qCDebug(lc).noquote() << "Qt" << qVersion() << "runtime";
228 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 if (command != Command::None) {
233 }
234
235 // Handle -h|--help explicitly, so we can tweak the output to include the <command> info.
236 if (parser.isSet(QStringLiteral("help"))) {
237 const QString commandString = (command == Command::None) ? QStringLiteral("<command>")
238 : parser.positionalArguments().constFirst();
239 std::cout << qUtf8Printable(parser.helpText()
240 .replace(QStringLiteral("[options]"), commandString + QStringLiteral(" [options]"))
241 .replace(QStringLiteral("Arguments:"), QStringLiteral("Command:"))
242 );
243 ::exit(EXIT_SUCCESS);
244 }
245
246 // Process the command for real (ie throw errors for unknown options, etc).
247 parser.process(appArguments);
248 return command;
249}
250
251AbstractCommand * getCommandObject(const Command command, QObject * const parent)
252{
253 switch (command) {
254 case Command::None:
255 showCliError(Private::tr("Missing argument: <command>\nSee --help for usage information."));
256 return nullptr;
257 case Command::Calibrate: return new CalibrateCommand(parent);
258 case Command::DSO: return new DsoCommand(parent);
259 case Command::FlashLed: return new FlashLedCommand(parent);
260 case Command::Info: return new InfoCommand(parent);
261 case Command::LoggerStart: return new LoggerStartCommand(parent);
262 case Command::LoggerStop: return new LoggerStopCommand(parent);
263 case Command::LoggerFetch: return new LoggerFetchCommand(parent);
264 case Command::Meter: return new MeterCommand(parent);
265 case Command::Scan: return new ScanCommand(parent);
266 case Command::Status: return new StatusCommand(parent);
267 case Command::SetName: return new SetNameCommand(parent);
268 case Command::SetTorch: return new SetTorchCommand(parent);
269 }
270 showCliError(Private::tr("Unknown command (%1)").arg((int)command));
271 return nullptr;
272}
273
274int main(int argc, char *argv[])
275{
276 // Setup the core application.
277 QCoreApplication app(argc, argv);
278 QCoreApplication::setApplicationName(QStringLiteral(PROJECT_NAME));
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 const QString localeName = QString::fromLocal8Bit(qgetenv("LANG"));
291 if (!localeName.isEmpty()) {
292 const QLocale newLocale(localeName);
293 if (newLocale.name() != localeName.split(QLatin1Char('.')).constFirst()) {
294 qWarning() << "Could not find locale" << localeName;
295 } else {
296 QLocale::setDefault(newLocale);
297 }
298 }
299#endif
300
301 // Install localised translators, if we have translations for the current locale.
302 const QLocale locale;
303 QTranslator appTranslator, libTranslator;
304 if (appTranslator.load(locale, QStringLiteral("cli"), QStringLiteral("/"), QStringLiteral(":/i18n"))) {
306 }
307 if (libTranslator.load(locale, QStringLiteral("lib"), QStringLiteral("/"), QStringLiteral(":/i18n"))) {
309 }
310
311 // Parse the command line.
312 const QStringList appArguments = QCoreApplication::arguments();
313 QCommandLineParser parser;
314 const Command commandType = parseCommandLine(appArguments, parser);
315 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 qCDebug(lc).noquote() << "App translations:" <<
318 (appTranslator.filePath().isEmpty() ? QStringLiteral("<none>") : appTranslator.filePath());
319 qCDebug(lc).noquote() << "Library translations:" <<
320 (libTranslator.filePath().isEmpty() ? QStringLiteral("<none>") : libTranslator.filePath());
321#else
322 qCDebug(lc).noquote() << "App translations:" << (!appTranslator.isEmpty());
323 qCDebug(lc).noquote() << "Lib translations:" << (!libTranslator.isEmpty());
324#endif
325
326 // Handle the given command.
327 AbstractCommand * const command = getCommandObject(commandType, &app);
328 if (command == nullptr) {
329 return EXIT_FAILURE; // getCommandObject will have logged the reason already.
330 }
331 const QStringList cliErrors = command->processOptions(parser);
332 for (const QString &error: cliErrors) {
333 showCliError(error);
334 }
335 const int result = ((cliErrors.isEmpty()) && (command->start())) ? QCoreApplication::exec() : EXIT_FAILURE;
336 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}
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)