477 lines
19 KiB
C++
477 lines
19 KiB
C++
// remote_test.cpp - test tool for the AdvancedController remote control
|
|
// (firmware r7.2.6 or later, INPUT port, RS485, 9600 8N1). Linux.
|
|
//
|
|
// Usage
|
|
// remote_test --list show serial ports
|
|
// remote_test --scan find the port with the controller
|
|
// (PING/PONG on every port)
|
|
// remote_test -p /dev/ttyUSB0 --detect controller detected / not detected
|
|
// on this port
|
|
// remote_test -p /dev/ttyUSB0 --monitor print everything the controller sends
|
|
// (diagnostics)
|
|
// remote_test -p /dev/ttyUSB0 -c PING -c STATUS one-shot commands
|
|
// remote_test -p /dev/ttyUSB0 --selftest automatic check of the whole command set
|
|
// remote_test -p /dev/ttyUSB0 --selftest --reset ... including RESET (~6 s)
|
|
// remote_test -p /dev/ttyUSB0 interactive shell (type commands, q = quit)
|
|
// Options
|
|
// -b BAUD baud rate (default 9600)
|
|
// --checksum append "*hh" checksums to every request
|
|
// -v print every line sent / received
|
|
|
|
#include "remoteclient.h"
|
|
|
|
#include <QCommandLineParser>
|
|
#include <QCoreApplication>
|
|
#include <QElapsedTimer>
|
|
#include <QTextStream>
|
|
|
|
#include <csignal>
|
|
#include <functional>
|
|
#include <utility>
|
|
|
|
using AdvCtrl::Fields;
|
|
using AdvCtrl::RemoteClient;
|
|
using AdvCtrl::Reply;
|
|
|
|
namespace {
|
|
|
|
// ------------------------------------------------------------------------ output
|
|
QTextStream &out()
|
|
{
|
|
static QTextStream stream(stdout);
|
|
return stream;
|
|
}
|
|
|
|
void say(const QString &text = QString())
|
|
{
|
|
out() << text << Qt::endl;
|
|
}
|
|
|
|
void sayNoNewline(const QString &text)
|
|
{
|
|
out() << text;
|
|
out().flush();
|
|
}
|
|
|
|
volatile std::sig_atomic_t g_stop = 0;
|
|
|
|
void onInterrupt(int)
|
|
{
|
|
g_stop = 1;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------- checker
|
|
class Checker
|
|
{
|
|
public:
|
|
bool check(const QString &name, bool condition, const QString &detail = QString())
|
|
{
|
|
if (condition) {
|
|
++m_passed;
|
|
say(QStringLiteral(" PASS ") + name);
|
|
} else {
|
|
++m_failed;
|
|
say(QStringLiteral(" FAIL %1 %2").arg(name, detail));
|
|
}
|
|
return condition;
|
|
}
|
|
|
|
bool summary()
|
|
{
|
|
say();
|
|
say(QStringLiteral("Result: %1 passed, %2 failed").arg(m_passed).arg(m_failed));
|
|
return m_failed == 0;
|
|
}
|
|
|
|
private:
|
|
int m_passed = 0;
|
|
int m_failed = 0;
|
|
};
|
|
|
|
// true when the controller answers ERR <code> to the line
|
|
std::pair<bool, QString> expectError(RemoteClient &rc, const QString &line, int code)
|
|
{
|
|
const Reply r = rc.command(line);
|
|
switch (r.status) {
|
|
case Reply::Ok:
|
|
return {false, QStringLiteral("got OK")};
|
|
case Reply::DeviceError:
|
|
return {r.code == code, QStringLiteral("got ERR %1 %2").arg(r.code).arg(r.text)};
|
|
case Reply::Timeout:
|
|
return {false, QStringLiteral("timeout")};
|
|
default:
|
|
return {false, r.text};
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------- modes
|
|
void doList()
|
|
{
|
|
const QList<AdvCtrl::PortInfo> ports = RemoteClient::listPorts();
|
|
if (ports.isEmpty()) {
|
|
say(QStringLiteral("No serial ports found."));
|
|
return;
|
|
}
|
|
for (const AdvCtrl::PortInfo &port : ports)
|
|
say(QStringLiteral(" %1 %2").arg(port.name, -14).arg(port.description));
|
|
}
|
|
|
|
int doScan(int baud)
|
|
{
|
|
const QList<AdvCtrl::PortInfo> ports = RemoteClient::listPorts();
|
|
if (ports.isEmpty()) {
|
|
say(QStringLiteral("No serial ports found."));
|
|
return 1;
|
|
}
|
|
QString found;
|
|
for (const AdvCtrl::PortInfo &port : ports) {
|
|
sayNoNewline(QStringLiteral(" %1 %2 ").arg(port.name, -14).arg(port.description.left(40), -40));
|
|
RemoteClient rc;
|
|
if (!rc.open(port.name, baud)) {
|
|
say(QStringLiteral("cannot open (%1)").arg(rc.errorString()));
|
|
continue;
|
|
}
|
|
const bool ok = rc.detect();
|
|
rc.close();
|
|
say(ok ? QStringLiteral("CONTROLLER") : QStringLiteral("-"));
|
|
if (ok && found.isEmpty())
|
|
found = port.name;
|
|
}
|
|
say();
|
|
say(found.isEmpty() ? QStringLiteral("Controller NOT found")
|
|
: QStringLiteral("Controller found on ") + found);
|
|
return found.isEmpty() ? 1 : 0;
|
|
}
|
|
|
|
int doDetect(RemoteClient &rc)
|
|
{
|
|
if (!rc.detect()) {
|
|
say(QStringLiteral("Controller NOT detected on ") + rc.portName());
|
|
return 1;
|
|
}
|
|
Fields ident;
|
|
if (!rc.identify(&ident)) {
|
|
say(QStringLiteral("Controller detected on %1, ID failed: %2").arg(rc.portName(), rc.lastReply().toString()));
|
|
return 1;
|
|
}
|
|
say(QStringLiteral("Controller detected on %1: %2 FW=%3 CAMS=%4 UID=%5")
|
|
.arg(rc.portName(), ident.value(QStringLiteral("ID")), ident.value(QStringLiteral("FW")),
|
|
ident.value(QStringLiteral("CAMS")), ident.value(QStringLiteral("UID"))));
|
|
return 0;
|
|
}
|
|
|
|
void doMonitor(RemoteClient &rc, int seconds)
|
|
{
|
|
say(QStringLiteral("Monitoring %1 - press Ctrl+C to stop%2")
|
|
.arg(rc.portName(), seconds <= 0 ? QString() : QStringLiteral(" (%1 s)").arg(seconds)));
|
|
std::signal(SIGINT, onInterrupt);
|
|
QElapsedTimer clock;
|
|
clock.start();
|
|
int count = 0;
|
|
while (!g_stop && (seconds <= 0 || clock.elapsed() < qint64(seconds) * 1000)) {
|
|
QString line;
|
|
if (rc.readLine(500, &line)) {
|
|
++count;
|
|
say(QStringLiteral("%1 %2").arg(double(clock.elapsed()) / 1000.0, 8, 'f', 2).arg(line));
|
|
} else if (rc.portFailed()) {
|
|
say(QStringLiteral("!! ") + rc.errorString());
|
|
break;
|
|
}
|
|
}
|
|
say(QStringLiteral("%1 line(s) received").arg(count));
|
|
}
|
|
|
|
bool doCommands(RemoteClient &rc, const QStringList &commands)
|
|
{
|
|
bool ok = true;
|
|
for (const QString &line : commands) {
|
|
const Reply r = rc.raw(line);
|
|
if (r.status == Reply::Ok || r.status == Reply::DeviceError) {
|
|
say(QStringLiteral("> %1\n< %2").arg(line, r.line));
|
|
} else {
|
|
ok = false;
|
|
say(QStringLiteral("> %1\n< !! %2").arg(line, r.text));
|
|
}
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
void doShell(RemoteClient &rc)
|
|
{
|
|
say(QStringLiteral("Interactive mode - type a command (PING, STATUS, BR 12, KEY UP ...), q to quit"));
|
|
QTextStream in(stdin);
|
|
for (;;) {
|
|
sayNoNewline(QStringLiteral("> "));
|
|
QString line = in.readLine();
|
|
if (line.isNull()) { // end of input
|
|
say();
|
|
break;
|
|
}
|
|
line = line.trimmed();
|
|
if (line.isEmpty())
|
|
continue;
|
|
const QString lower = line.toLower();
|
|
if (lower == QLatin1String("q") || lower == QLatin1String("quit") || lower == QLatin1String("exit"))
|
|
break;
|
|
const Reply r = rc.raw(line);
|
|
if (r.status == Reply::Ok || r.status == Reply::DeviceError)
|
|
say(QStringLiteral("< ") + r.line);
|
|
else
|
|
say(QStringLiteral("< !! ") + r.text);
|
|
}
|
|
}
|
|
|
|
bool doSelftest(RemoteClient &rc, bool withReset)
|
|
{
|
|
Checker c;
|
|
say(QStringLiteral("Self-test on ") + rc.portName());
|
|
|
|
// 1. link
|
|
const bool pong = rc.ping();
|
|
const Reply pingReply = rc.lastReply();
|
|
if (pingReply.status != Reply::Ok) {
|
|
c.check(QStringLiteral("PING"), false, pingReply.toString());
|
|
say(QStringLiteral("No link - stopping."));
|
|
return c.summary();
|
|
}
|
|
c.check(QStringLiteral("PING answered with the controller signature"), pong,
|
|
QStringLiteral("reply without 'PONG ADVCTRL'"));
|
|
|
|
Fields ident;
|
|
const bool identOk = rc.identify(&ident);
|
|
c.check(QStringLiteral("ID returns signature / FW / CAMS / UID"),
|
|
identOk && ident.value(QStringLiteral("ID")) == RemoteClient::DeviceId
|
|
&& ident.contains(QStringLiteral("FW")) && ident.contains(QStringLiteral("CAMS"))
|
|
&& ident.contains(QStringLiteral("UID")),
|
|
rc.lastReply().toString());
|
|
say(QStringLiteral(" ") + (identOk ? rc.lastReply().data : rc.lastReply().toString()));
|
|
|
|
QString firmware;
|
|
const bool verOk = rc.version(&firmware);
|
|
c.check(QStringLiteral("VER returns a version"), verOk && !firmware.isEmpty(), rc.lastReply().toString());
|
|
say(QStringLiteral(" firmware: ") + firmware);
|
|
|
|
quint32 mask = 0;
|
|
if (rc.leds(&mask)) {
|
|
c.check(QStringLiteral("LEDS returns a 24-bit mask"), mask < (1u << 24),
|
|
QStringLiteral("%1").arg(mask, 6, 16, QLatin1Char('0')).toUpper());
|
|
QStringList on;
|
|
rc.ledsOn(&on);
|
|
say(QStringLiteral(" leds on: ") + (on.isEmpty() ? QStringLiteral("none") : on.join(QStringLiteral(", "))));
|
|
} else {
|
|
c.check(QStringLiteral("LEDS returns a 24-bit mask"), false, rc.lastReply().toString());
|
|
}
|
|
|
|
c.check(QStringLiteral("IDENTIFY accepted (gear led blinks 3 s)"), rc.identifyBlink(), rc.lastReply().toString());
|
|
|
|
// 2. STATUS
|
|
Fields st;
|
|
const bool statusOk = rc.status(&st);
|
|
const QStringList keys = {"CAM", "BR", "AGC", "ZOOM", "FLIP", "MIR", "TOSD", "LED", "AUTO",
|
|
"CT", "PT", "LT", "XH1", "XH2"};
|
|
QStringList missing;
|
|
for (const QString &k : keys) {
|
|
if (!st.contains(k))
|
|
missing.append(k);
|
|
}
|
|
c.check(QStringLiteral("STATUS has all fields"), statusOk && missing.isEmpty(),
|
|
statusOk ? QStringLiteral("missing ") + missing.join(QLatin1Char(' ')) : rc.lastReply().toString());
|
|
say(QStringLiteral(" ") + (statusOk ? rc.lastReply().data : rc.lastReply().toString()));
|
|
if (!statusOk || !missing.isEmpty())
|
|
return c.summary();
|
|
auto value = [&st](const char *k) { return st.value(QLatin1String(k)).toInt(); };
|
|
|
|
// 3. error handling
|
|
const std::pair<const char *, std::pair<QString, int>> errorCases[] = {
|
|
{"unknown command -> ERR 1", {QStringLiteral("FOO"), 1}},
|
|
{"missing argument -> ERR 2", {QStringLiteral("BR"), 2}},
|
|
{"bad argument -> ERR 2", {QStringLiteral("BR x"), 2}},
|
|
{"out of range -> ERR 3", {QStringLiteral("BR 99"), 3}},
|
|
{"CAM 0 -> ERR 3", {QStringLiteral("CAM 0"), 3}},
|
|
{"extra argument -> ERR 2", {QStringLiteral("PING 1"), 2}},
|
|
{"80-char line -> ERR 4", {QString(80, QLatin1Char('X')), 4}},
|
|
};
|
|
for (const auto &e : errorCases) {
|
|
const std::pair<bool, QString> result = expectError(rc, e.second.first, e.second.second);
|
|
c.check(QLatin1String(e.first), result.first, result.second);
|
|
}
|
|
|
|
// checksum
|
|
const bool savedChecksum = rc.checksum();
|
|
rc.setChecksum(false);
|
|
const Reply bad = rc.raw(QStringLiteral("PING*00"));
|
|
const Reply good = rc.raw(QStringLiteral("PING*") + AdvCtrl::nmeaChecksum(QStringLiteral("PING"))); // raw() verifies the reply checksum
|
|
if ((bad.status == Reply::Ok || bad.status == Reply::DeviceError)
|
|
&& (good.status == Reply::Ok || good.status == Reply::DeviceError)) {
|
|
c.check(QStringLiteral("bad checksum -> ERR 5"), bad.line.toUpper().startsWith(QLatin1String("ERR 5")), bad.line);
|
|
c.check(QStringLiteral("good checksum -> OK with checksum"), good.line.toUpper().startsWith(QLatin1String("OK")), good.line);
|
|
} else {
|
|
c.check(QStringLiteral("checksum handling"), false,
|
|
bad.status == Reply::Ok || bad.status == Reply::DeviceError ? good.text : bad.text);
|
|
}
|
|
rc.setChecksum(savedChecksum);
|
|
|
|
// 4. absolute setters (value changed, read back, restored)
|
|
auto roundtrip = [&](const QString &name, const QString &field,
|
|
const std::function<bool(int)> &setter, int newValue, int oldValue) {
|
|
const bool setOk = setter(newValue);
|
|
Fields now;
|
|
const bool readOk = rc.status(&now);
|
|
const QString got = readOk ? now.value(field) : rc.lastReply().toString();
|
|
c.check(name + QStringLiteral(" set/read back"),
|
|
setOk && readOk && now.contains(field) && now.value(field).toInt() == newValue,
|
|
QStringLiteral("set %1 got %2").arg(newValue).arg(got));
|
|
setter(oldValue);
|
|
};
|
|
roundtrip(QStringLiteral("BR"), QStringLiteral("BR"), [&](int v) { return rc.brightness(v); }, (value("BR") + 1) % 21, value("BR"));
|
|
roundtrip(QStringLiteral("AGC"), QStringLiteral("AGC"), [&](int v) { return rc.agc(v); }, (value("AGC") + 1) % 21, value("AGC"));
|
|
roundtrip(QStringLiteral("ZOOM"), QStringLiteral("ZOOM"), [&](int v) { return rc.zoom(v); }, (value("ZOOM") + 1) % 17, value("ZOOM"));
|
|
roundtrip(QStringLiteral("FLIP"), QStringLiteral("FLIP"), [&](int v) { return rc.flip(v != 0); }, 1 - value("FLIP"), value("FLIP"));
|
|
roundtrip(QStringLiteral("MIR"), QStringLiteral("MIR"), [&](int v) { return rc.mirror(v != 0); }, 1 - value("MIR"), value("MIR"));
|
|
roundtrip(QStringLiteral("TEMP"), QStringLiteral("TOSD"), [&](int v) { return rc.tempOsd(v != 0); }, 1 - value("TOSD"), value("TOSD"));
|
|
roundtrip(QStringLiteral("LED"), QStringLiteral("LED"), [&](int v) { return rc.led(v); }, (value("LED") + 1) % 4, value("LED"));
|
|
|
|
auto statusValue = [&rc](const char *k) {
|
|
Fields now;
|
|
return rc.status(&now) ? now.value(QLatin1String(k)).toInt() : -1;
|
|
};
|
|
rc.ledAuto(true);
|
|
c.check(QStringLiteral("LED AUTO 1"), statusValue("AUTO") == 1);
|
|
rc.ledAuto(false);
|
|
c.check(QStringLiteral("LED AUTO 0"), statusValue("AUTO") == 0);
|
|
if (value("AUTO") == 1)
|
|
rc.ledAuto(true);
|
|
else
|
|
rc.led(value("LED")); // restore the manual level
|
|
|
|
// 5. camera selection
|
|
bool camsOk = true;
|
|
for (int n = 1; n <= 4; ++n) {
|
|
rc.cam(n);
|
|
const int got = statusValue("CAM");
|
|
if (got != n) {
|
|
camsOk = false;
|
|
say(QStringLiteral(" CAM %1 -> STATUS CAM=%2").arg(n).arg(got));
|
|
}
|
|
}
|
|
rc.cam(value("CAM"));
|
|
c.check(QStringLiteral("CAM 1..4 select"), camsOk);
|
|
|
|
// 6. virtual keys: brightness mode, up, down, mode off
|
|
int br = statusValue("BR");
|
|
if (br >= 20) {
|
|
rc.brightness(19);
|
|
br = 19;
|
|
}
|
|
rc.key(QStringLiteral("BRIGHT")); // mode on (arrow leds on the keyboard)
|
|
rc.key(QStringLiteral("UP"));
|
|
const int up = statusValue("BR");
|
|
rc.key(QStringLiteral("DOWN"));
|
|
const int down = statusValue("BR");
|
|
rc.key(QStringLiteral("BRIGHT")); // mode off
|
|
c.check(QStringLiteral("KEY BRIGHT/UP/DOWN"), up == br + 1 && down == br,
|
|
QStringLiteral("start %1 up %2 down %3").arg(br).arg(up).arg(down));
|
|
|
|
// 7. timing
|
|
const int n = 20;
|
|
QElapsedTimer clock;
|
|
clock.start();
|
|
for (int i = 0; i < n; ++i)
|
|
rc.ping();
|
|
const double dt = double(clock.elapsed()) / n;
|
|
c.check(QStringLiteral("PING round trip < 100 ms"), dt < 100.0, QStringLiteral("%1 ms").arg(dt, 0, 'f', 1));
|
|
say(QStringLiteral(" average round trip: %1 ms").arg(dt, 0, 'f', 1));
|
|
|
|
// 8. optional long command
|
|
if (withReset) {
|
|
QElapsedTimer resetClock;
|
|
resetClock.start();
|
|
if (rc.reset()) {
|
|
c.check(QStringLiteral("RESET completes"), true);
|
|
say(QStringLiteral(" RESET took %1 s").arg(double(resetClock.elapsed()) / 1000.0, 0, 'f', 1));
|
|
} else {
|
|
c.check(QStringLiteral("RESET completes"), false, rc.lastReply().toString());
|
|
}
|
|
Fields after;
|
|
c.check(QStringLiteral("STATUS after RESET"), rc.status(&after) && after.contains(QStringLiteral("CAM")));
|
|
}
|
|
|
|
return c.summary();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// -------------------------------------------------------------------------- main
|
|
int main(int argc, char *argv[])
|
|
{
|
|
QCoreApplication app(argc, argv);
|
|
QCoreApplication::setApplicationName(QStringLiteral("remote_test"));
|
|
|
|
QCommandLineParser parser;
|
|
parser.setApplicationDescription(QStringLiteral("AdvancedController remote-control test tool"));
|
|
parser.addHelpOption();
|
|
const QCommandLineOption portOption({QStringLiteral("p"), QStringLiteral("port")},
|
|
QStringLiteral("serial port, e.g. /dev/ttyUSB0"), QStringLiteral("port"));
|
|
const QCommandLineOption baudOption({QStringLiteral("b"), QStringLiteral("baud")},
|
|
QStringLiteral("baud rate (default 9600)"), QStringLiteral("baud"), QStringLiteral("9600"));
|
|
const QCommandLineOption listOption(QStringLiteral("list"), QStringLiteral("list serial ports"));
|
|
const QCommandLineOption scanOption(QStringLiteral("scan"), QStringLiteral("find the port with the controller"));
|
|
const QCommandLineOption detectOption(QStringLiteral("detect"), QStringLiteral("controller detected / not detected on -p PORT"));
|
|
const QCommandLineOption monitorOption(QStringLiteral("monitor"), QStringLiteral("print everything received"));
|
|
const QCommandLineOption secondsOption(QStringLiteral("seconds"), QStringLiteral("monitor duration (0 = until Ctrl+C)"),
|
|
QStringLiteral("seconds"), QStringLiteral("0"));
|
|
const QCommandLineOption cmdOption({QStringLiteral("c"), QStringLiteral("cmd")},
|
|
QStringLiteral("send this command (repeatable)"), QStringLiteral("command"));
|
|
const QCommandLineOption selftestOption(QStringLiteral("selftest"), QStringLiteral("run the automatic checks"));
|
|
const QCommandLineOption resetOption(QStringLiteral("reset"), QStringLiteral("include RESET in the self-test"));
|
|
const QCommandLineOption checksumOption(QStringLiteral("checksum"), QStringLiteral("use *hh checksums"));
|
|
const QCommandLineOption verboseOption({QStringLiteral("v"), QStringLiteral("verbose")}, QStringLiteral("show raw lines"));
|
|
parser.addOptions({portOption, baudOption, listOption, scanOption, detectOption, monitorOption,
|
|
secondsOption, cmdOption, selftestOption, resetOption, checksumOption, verboseOption});
|
|
parser.process(app);
|
|
|
|
bool baudOk = false;
|
|
const int baud = parser.value(baudOption).toInt(&baudOk);
|
|
if (!baudOk || baud <= 0) {
|
|
say(QStringLiteral("Wrong baud rate: ") + parser.value(baudOption));
|
|
return 1;
|
|
}
|
|
|
|
if (parser.isSet(listOption)) {
|
|
doList();
|
|
return 0;
|
|
}
|
|
if (parser.isSet(scanOption))
|
|
return doScan(baud);
|
|
|
|
const QString port = parser.value(portOption);
|
|
if (port.isEmpty()) {
|
|
sayNoNewline(parser.helpText());
|
|
say();
|
|
say(QStringLiteral("Serial ports:"));
|
|
doList();
|
|
return 1;
|
|
}
|
|
|
|
RemoteClient rc;
|
|
rc.setChecksum(parser.isSet(checksumOption));
|
|
if (parser.isSet(verboseOption))
|
|
rc.setLog([](const QString &line) { say(QStringLiteral(" ") + line); });
|
|
if (!rc.open(port, baud)) {
|
|
say(QStringLiteral("Cannot open %1: %2").arg(port, rc.errorString()));
|
|
return 1;
|
|
}
|
|
|
|
if (parser.isSet(detectOption))
|
|
return doDetect(rc);
|
|
if (parser.isSet(monitorOption)) {
|
|
doMonitor(rc, parser.value(secondsOption).toInt());
|
|
return 0;
|
|
}
|
|
if (parser.isSet(cmdOption))
|
|
return doCommands(rc, parser.values(cmdOption)) ? 0 : 1;
|
|
if (parser.isSet(selftestOption))
|
|
return doSelftest(rc, parser.isSet(resetOption)) ? 0 : 1;
|
|
doShell(rc);
|
|
return 0;
|
|
}
|