AdvancedControllerRemoteCPP/lib/remoteclient.h

232 lines
11 KiB
C++

// 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 <code> <text>\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 <QByteArray>
#include <QDeadlineTimer>
#include <QElapsedTimer>
#include <QList>
#include <QMap>
#include <QString>
#include <QStringList>
#include <functional>
#include <memory>
class QSerialPort;
namespace AdvCtrl {
// Result of one request.
struct Reply
{
enum Status {
Ok, // "OK [data]"
DeviceError, // "ERR <code> <text>": 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 <data>", "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<QString, QString>;
struct PortInfo
{
QString name; // e.g. "/dev/ttyUSB0"
QString description; // e.g. "USB Serial Port"
};
// Meaning of the code in "ERR <code> <text>" (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<PortInfo> 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<void(const QString &)> 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<QSerialPort> 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<void(const QString &)> m_log;
};
} // namespace AdvCtrl
#endif // ADVCTRL_REMOTECLIENT_H