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