// remoteclient.h - client for the AdvancedController remote-control protocol // (firmware r7.2.6 or later; INPUT port, RS485, 9600 8N1). Qt 6.5+, modules Core and SerialPort. // // Take the lib folder into your application: add_subdirectory(lib) and link // AdvancedControllerRemote, or add the four files to your own target. // // Protocol summary (full description: docs/REMOTE_API_EN.txt) // request : "CMD [ARG ...]\r\n" case-insensitive // reply : "OK[ data]\r\n" or "ERR \r\n" // checksum: optional NMEA style "*hh" (XOR of all characters before '*'); // when the request carries one, the reply carries one too // rule : stop-and-wait - never send the next command before the reply // // RemoteClient is BLOCKING: every call returns after the reply or the timeout. // Qt: "A blocking serial port should only be used in non-GUI threads, to avoid freezing // the user interface." In a GUI application use ControllerLink (controllerlink.h): it runs // a RemoteClient in its own thread and reports the results as signals. // // Example (console program or worker thread) // AdvCtrl::RemoteClient rc; // if (!rc.open("/dev/ttyUSB0")) { // qWarning() << rc.errorString(); // return 1; // } // QString firmware; // rc.version(&firmware); // rc.cam(2); // rc.brightness(12); // AdvCtrl::Fields st; // if (rc.status(&st)) // qDebug() << "BR =" << st.value("BR").toInt(); // if (!rc.brightness(99)) // the controller answers ERR 3 RANGE // qDebug() << rc.lastReply().toString(); #ifndef ADVCTRL_REMOTECLIENT_H #define ADVCTRL_REMOTECLIENT_H #include #include #include #include #include #include #include #include #include class QSerialPort; namespace AdvCtrl { // Result of one request. struct Reply { enum Status { Ok, // "OK [data]" DeviceError, // "ERR ": the controller refused the command (not a link failure) Timeout, // no reply within the timeout (also after the automatic retries) BadReply, // a reply with a wrong checksum or of an unexpected shape PortError // the serial port is not open or failed (e.g. the USB adapter was unplugged) }; Status status = PortError; QString line; // the reply line as received, without CR/LF and checksum ("OK r7.2.6") QString data; // Ok: the part after "OK" ("" if none) int code = 0; // DeviceError: 1..5, see errorText() QString text; // DeviceError: the error text of the controller; other failures: what went wrong bool ok() const { return status == Ok; } QString toString() const; // "OK ", "ERR 3 RANGE" or the description of the failure }; // "KEY=VALUE" fields of the STATUS and ID replies, e.g. fields.value("BR").toInt() using Fields = QMap; struct PortInfo { QString name; // e.g. "/dev/ttyUSB0" QString description; // e.g. "USB Serial Port" }; // Meaning of the code in "ERR " (1 "unknown command" ... 5 "checksum mismatch"). QString errorText(int code); // NMEA checksum: XOR of all characters as two upper-case hex digits ("PING" -> "10", "OK" -> "04"). QString nmeaChecksum(const QString &text); class RemoteClient { public: static constexpr int DefaultBaudRate = 9600; static constexpr int DefaultTimeoutMs = 500; // ordinary commands static constexpr int StaleDrainMs = 300; // after a timeout: quiet time that swallows a late reply // Pause between the end of a reply and the next request: the controller switches its RS485 // transceiver back to receiving about 2 ms after the end of its reply; bytes sent earlier // are lost (the controller answers ERR 1 UNKNOWN or ERR 5 CHECKSUM). static constexpr int TurnaroundMs = 5; static inline const QString DeviceId = QStringLiteral("ADVCTRL"); // "PING" -> "OK PONG ADVCTRL" // Bit order of the LEDS reply, index = bit number. static inline const QStringList LedNames = { QStringLiteral("TEMP"), QStringLiteral("XH2"), QStringLiteral("XH1"), QStringLiteral("SAVE2"), QStringLiteral("SAVE1"), QStringLiteral("LOAD2"), QStringLiteral("LOAD1"), QStringLiteral("CAMMENU"), QStringLiteral("FOCUS"), QStringLiteral("FLIP"), QStringLiteral("MIRROR"), QStringLiteral("ZOOM"), QStringLiteral("CAM1SEL"), QStringLiteral("UP"), QStringLiteral("DOWN"), QStringLiteral("RIGHT"), QStringLiteral("LEFT"), QStringLiteral("ENTER"), QStringLiteral("BRIGHT"), QStringLiteral("AGC"), QStringLiteral("LEDAUTO"), QStringLiteral("LEDON"), QStringLiteral("SYSRESET"), QStringLiteral("CAM2SEL") }; RemoteClient(); ~RemoteClient(); RemoteClient(const RemoteClient &) = delete; RemoteClient &operator=(const RemoteClient &) = delete; // ------------------------------------------------------------------ port // The serial ports present, sorted naturally by name (/dev/ttyUSB2 before /dev/ttyUSB10). static QList listPorts(); // Scans the serial ports (all present ones, or the given names) and returns the first // port where the controller answers PING with its signature; "" when it is not found. // Busy ports are skipped. static QString findController(const QStringList &ports = QStringList(), int baudRate = DefaultBaudRate); bool open(const QString &portName, int baudRate = DefaultBaudRate); void close(); bool isOpen() const; QString portName() const; QString errorString() const; // why open() failed or why the port stopped working bool portFailed() const; // the port failed after opening (e.g. adapter unplugged): close it // --------------------------------------------------------------- options void setTimeout(int ms); // reply timeout of ordinary commands, default 500 ms int timeout() const; void setChecksum(bool on); // append "*hh" to every request and verify it in the reply bool checksum() const; void setRetries(int count); // repetitions of an idempotent command after a timeout, default 1 int retries() const; // Receives every line: "> sent", "< received", "~ late reply dropped". void setLog(std::function log); // ------------------------------------------------------------- detection // Controller detection right after opening the port: PING with a short timeout, a couple // of attempts. true = our controller answers on this port ("OK PONG ADVCTRL"); // false = another device or nothing. bool detect(int attempts = 2, int timeoutMs = 300); // -------------------------------------------------------------- requests // One request, no retries. Lines that are not replies (they do not start with OK/ERR) are // skipped. timeoutMs < 0: timeoutFor(line). Reply raw(const QString &line, int timeoutMs = -1); // As raw(), but idempotent commands are repeated on timeout (setRetries()). Reply command(const QString &line, int timeoutMs = -1); // Reply timeout for a request line (long commands need more). int timeoutFor(const QString &line) const; // One received line (without CR/LF), e.g. for a monitor. false = nothing within the // timeout or the port failed (portFailed()). bool readLine(int timeoutMs, QString *line); // Drop everything that may still sit in the input buffer. void flush(); // The controller answers every request exactly once, also the ones the PC stopped waiting // for (timeout). Such a late reply would be taken for the reply to the next request. // Swallow whatever arrives during a short quiet time. raw() does it by itself after a timeout. void drainLate(int quietMs = StaleDrainMs); // -------------------------------------------------------------- commands // true = the controller answered "OK". Otherwise lastReply() tells why // (Timeout, DeviceError with code and text, ...). const Reply &lastReply() const; bool ping(); // true = the answer carries the controller signature bool identify(Fields *id); // ID: "ID" (signature), "FW", "CAMS", "UID" (24 hex) bool version(QString *firmware); // VER, e.g. "r7.2.6" bool status(Fields *fields); // CAM BR AGC ZOOM FLIP MIR TOSD LED AUTO CT PT LT XH1 XH2 CT2 bool leds(quint32 *mask); // state of the 24 keyboard leds, bit order = LedNames bool ledsOn(QStringList *names); // names of the leds that are on bool identifyBlink(); // IDENTIFY: the gear led blinks fast for 3 s // Virtual button: ENTER LONGENTER UP DOWN LEFT RIGHT FLIP MIRROR ZOOM BRIGHT XH1 XH1LONG // XH2 XH2LONG CAMRESET TEMP LEDON LEDAUTO AGC LOAD1 LOAD2 SAVE1 SAVE2 SYSRESET CAMSEL FOCUS. // Returns after the key has been executed. bool key(const QString &name); bool cam(int number); // 1..4 bool brightness(int value); // 0..20 bool agc(int value); // 0..20 bool zoom(int value); // 0..16 bool flip(bool on); bool mirror(bool on); bool tempOsd(bool on); // temperature on the camera OSD bool led(int level); // backlight level 0..3 of the current camera (ends AUTO mode) bool ledAuto(bool on); bool presetLoad(int number); // 1 or 2 bool presetSave(int number); // 1 or 2 bool reset(); // CAM_RESET: default settings to all cameras, ~6 s bool focus(); // FOCUS: crosshair initialisation of the current camera, ~5 s private: enum class ReadResult { Line, Timeout, PortFailure, Interrupted }; ReadResult readLineUntil(const QDeadlineTimer &deadline, QString *line); void waitTurnaround(); bool writeLine(const QByteArray &bytes); bool checkPort(); void log(const QString &text) const; Reply finish(const Reply &reply); Reply failure(Reply::Status status, const QString &text); bool run(const QString &line); bool rejectReply(const QString &text); std::unique_ptr m_port; QByteArray m_rx; // received, not yet taken as a line QElapsedTimer m_lastRx; // since the last received bytes (TurnaroundMs) QString m_portName; QString m_errorString; bool m_failed = false; int m_timeoutMs = DefaultTimeoutMs; bool m_checksum = false; int m_retries = 1; int m_stale = 0; // requests without a reply so far: their replies may still arrive Reply m_last; std::function m_log; }; } // namespace AdvCtrl #endif // ADVCTRL_REMOTECLIENT_H