// 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 #include #include #include #include #include #include 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 to the line std::pair 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 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 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> 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 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 &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; }