232 lines
11 KiB
C++
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
|