AdvancedController - remote control over RS485. Protocol description and integration
=====================================================================================
Firmware version: r7.2.6. Russian version: REMOTE_API_RU.txt.


1. PHYSICAL LAYER
-----------------
Controller port: INPUT (RS485, the connector next to the power connector).
Inside: USART2, PD5 TX, PD6 RX, PD7 = transceiver direction.
Line settings: 9600 baud, 8 data bits, no parity, 1 stop bit (9600 8N1).
Half duplex, two-wire A/B line. On the PC side: a USB-RS485 adapter with
automatic direction control (virtual COM port). 120 ohm termination at the
ends of a long line as usual for RS485.
Only the PC must be on this bus: the controller never transmits on its own,
it only answers commands.


2. FRAME AND EXCHANGE RULES
---------------------------
Request from the PC:   CMD [ARG1 [ARG2]]<CR><LF>
  - ASCII text, case-insensitive (br 12 = BR 12);
  - arguments separated by a space; terminator CR, LF or CR+LF;
  - at most 64 characters without the terminator.
Reply of the controller to EVERY request:
  OK<CR><LF>                    command executed
  OK <data><CR><LF>             command executed, data follows (VER, STATUS)
  ERR <code> <text><CR><LF>     command not executed (section 5)

Stop-and-wait: the PC sends the next command only after the reply arrived (or
the timeout expired). The controller handles one line per pass of its main
loop; lines that arrive earlier wait in a 256-byte buffer.

Line turnaround: after a reply wait at least 5 ms before 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 and the controller
answers ERR 1 UNKNOWN or ERR 5 CHECKSUM to the damaged line. RemoteClient
(lib/remoteclient.cpp) keeps this pause by itself.

Timeouts on the PC side (recommendation):
  ordinary commands                        0.5 s
  PRESET LOAD/SAVE, KEY LOAD*/SAVE*        3 s
  KEY XH1/XH2, KEY UP/DOWN/LEFT/RIGHT while a crosshair is being edited  2-3 s
  RESET, FOCUS, KEY CAMRESET, KEY FOCUS    10 s
The reply to a long command comes AFTER it has completed (RESET ~6 s, FOCUS
~5 s). While a long command runs the controller does not parse new lines;
they are not lost and are handled afterwards (replies arrive late).
Late reply: when the PC gave up waiting (timeout) the reply still comes later
and would be taken for the reply to the next request. Drop such lines before
the next request (RemoteClient does it by itself: after a timeout it waits
for 0.3 s of silence and clears the receive buffer).

Retries: when no reply arrives, repeat only idempotent commands (PING, VER,
STATUS, CAM, BR, AGC, ZOOM, FLIP, MIR, TEMP, LED). Never repeat KEY, RESET,
FOCUS or PRESET automatically - a repetition repeats the action.

Resynchronisation: a pause of more than 100 ms between the bytes of a line
drops the partial line without a reply. A line longer than 64 characters is
answered with ERR 4 after its terminator.

Checksum (optional): a request may end with "*hh", hh being the XOR of all
characters before '*' in hexadecimal (NMEA style):
  PING*10<CR><LF>   ->   OK PONG ADVCTRL*48<CR><LF>
When the request carries a checksum the controller verifies it (mismatch ->
ERR 5) and appends a checksum to the reply. Without '*' there is no checksum
in either direction.


3. COMMANDS
-----------
All commands with values act on the CURRENT camera (see CAM and STATUS).

  Command              Arguments          Action / reply
  -------------------  -----------------  -------------------------------------------
  PING                 -                  OK PONG ADVCTRL. Link check and controller
                                          detection (signature "PONG ADVCTRL").
  VER                  -                  OK r7.2.6. Firmware version.
  ID                   -                  OK ADVCTRL FW=r7.2.6 CAMS=4 UID=<24 hex>.
                                          Identification: firmware, number of
                                          cameras, unique MCU id.
  STATUS               -                  OK CAM=.. BR=.. ... (section 4).
  LEDS                 -                  OK <6 hex>. State of the 24 keyboard
                                          leds as the device shows them
                                          (section 4a).
  CAM n                1..4               Select camera n (like the CAM SELECT
                                          button, the selection leds follow).
  BR n                 0..20              Camera brightness.
  AGC n                0..20              Camera AGC.
  ZOOM n               0..16              Camera digital zoom.
  FLIP 0|1             -                  Image flip off/on.
  MIR 0|1              -                  Mirror off/on.
  TEMP 0|1             -                  Temperature on the camera OSD off/on.
  LED n                0..3               Backlight level: 0 off, 1 = 25 %,
                                          2 = 75 %, 3 = 100 %. Ends AUTO mode.
  LED AUTO 0|1         -                  Automatic backlight from the 0-10 V
                                          input off/on (all channels).
  PRESET LOAD 1|2      -                  Load a user preset of the camera.
  PRESET SAVE 1|2      -                  Save the camera settings to a preset.
  RESET                -                  Default settings to all cameras
                                          (CAM RESET button, ~6 s).
  FOCUS                -                  Crosshair initialisation of the current
                                          camera (FOCUS button, ~5 s).
  KEY <name>           section 6          Virtual keyboard button.
  IDENTIFY             -                  The gear led blinks fast for 3 s - to
                                          see which controller the PC talks to.

Example session (terminal, 9600 8N1, CR+LF):
  > PING            < OK PONG ADVCTRL
  > VER             < OK r7.2.6
  > ID              < OK ADVCTRL FW=r7.2.6 CAMS=4 UID=3A0032000E51383433353839
  > CAM 2           < OK
  > BR 12           < OK
  > STATUS          < OK CAM=2 BR=12 AGC=4 ZOOM=0 FLIP=0 MIR=0 TOSD=0 LED=3 AUTO=0 CT=41 PT=36 LT=G XH1=0 XH2=0 CT2=40
  > BR 99           < ERR 3 RANGE
  > HELLO           < ERR 1 UNKNOWN


4. STATUS FIELDS
----------------
  CAM   current camera 1..4
  BR    brightness 0..20         AGC   0..20         ZOOM  0..16
  FLIP  0/1                      MIR   0/1           TOSD  temperature on OSD 0/1
  LED   backlight level 0..3 (manual)     AUTO  automatic backlight 0/1
  CT    temperature of camera 1, C (sent by the camera; 25 until the first message)
  CT2   temperature of camera 2, C (last field of the STATUS line)
  PT    temperature of the controller CPU, C (internal sensor, approximate)
  LT    light tower: G - normal (green), R - camera overheated (red)
  XH1, XH2   crosshair 1 / 2 shown 0/1
BR/AGC/ZOOM/FLIP/MIR/TOSD/LED are stored in the controller per camera and
reflect both PC commands and keyboard presses. The camera's own values after
PRESET LOAD are unknown to the controller (camera limitation): after loading a
preset set the wanted values explicitly with BR/AGC/ZOOM.


4a. LEDS - LED BITS
-------------------
The reply is a 24-bit mask as 6 hexadecimal digits (bit = 1: led on).
   0 TEMP      1 XH2       2 XH1       3 SAVE2     4 SAVE1     5 LOAD2
   6 LOAD1     7 CAMMENU   8 FOCUS     9 FLIP     10 MIRROR   11 ZOOM
  12 CAM1SEL  13 UP       14 DOWN     15 RIGHT    16 LEFT     17 ENTER
  18 BRIGHT   19 AGC      20 LEDAUTO  21 LEDON    22 SYSRESET 23 CAM2SEL
The virtual panel (bin/AdvancedControllerPanel) polls LEDS 5 times a
second and mirrors the device indication. CAM1SEL/CAM2SEL are the two leds of
the camera-select button: camera 1 = 1/0, 2 = 0/1, 3 = 1/1, 4 = 0/0.


5. ERROR CODES
--------------
  ERR 1 UNKNOWN    unknown command
  ERR 2 ARG        wrong number or format of arguments
  ERR 3 RANGE      argument out of range
  ERR 4 TOOLONG    line longer than 64 characters
  ERR 5 CHECKSUM   checksum mismatch


6. VIRTUAL BUTTONS (KEY)
------------------------
KEY reproduces a press of the membrane keyboard and goes through the same
handler as the real button: same modes, leds and interlocks. OK is returned
after the press has been executed.

  ENTER     LONGENTER (long press - open the camera OSD menu)
  UP  DOWN  LEFT  RIGHT      arrows (act in the active mode)
  FLIP  MIRROR               toggle flip / mirror
  ZOOM  BRIGHT  AGC          enter/leave the arrow-adjust mode
  XH1  XH2                   show/hide crosshair 1 / 2
  XH1LONG  XH2LONG           crosshair edit mode (arrows move the lines)
  CAMRESET                   reset of all cameras (= RESET); the "three
                             sliders" button, its led blinks during the reset
  TEMP                       temperature on the camera OSD on/off
  LEDON                      manual backlight mode on/off (arrows change the level)
  LEDAUTO                    automatic backlight on/off
  LOAD1 LOAD2 SAVE1 SAVE2    camera presets
  SYSRESET                   leave all active adjust modes; the gear button
                             (its led = remote-control sign, section 7)
  CAMSEL                     next camera (1->2->3->4->1)
  FOCUS                      crosshair initialisation (= FOCUS)

Modality: ZOOM, BRIGHT, AGC, LEDON, XH1LONG, XH2LONG and LONGENTER enter a
mode in which UP/DOWN/LEFT/RIGHT change the corresponding parameter. Only one
adjust mode is active at a time. PC software should prefer the direct
commands (BR, AGC, ZOOM, LED, CAM) and use KEY for what has no direct command
(crosshairs, OSD menu).


7. BEHAVIOUR AND LIMITS
-----------------------
- The controller keyboard keeps working next to the PC. A real press wins:
  a virtual press is executed in the next loop pass.
- Changes made with the buttons are visible to the PC through STATUS.
- Camera 4 is connected to the controller by its backlight (PWM) only;
  camera commands with camera 4 selected go nowhere (the INPUT port is the PC's).
- During RESET/FOCUS the controller is busy for up to 6 s; the watchdog does
  not trip.
- The controller sends nothing on its own.
  Poll STATUS, e.g. once per second, to display the state.
- Operator indication: while commands keep coming from the PC
  the gear-button led on the device blinks twice with a 2 s pause; it goes
  off 5 s after the last command. The firmware does this; the PC software
  does nothing for it (bit 22 SYSRESET in LEDS = the gear led; bit 7 CAMMENU
  is the led of the "three sliders" button).
  The IDENTIFY command makes the same led blink fast for 3 s (100 ms on /
  100 ms off) - that is how one finds out which of several controllers is
  connected to a given port; the normal indication resumes afterwards.


8. INTEGRATION GUIDELINES FOR CLIENT SOFTWARE
---------------------------------------------
1. One thread (or one queue) per port: strictly stop-and-wait, at least 5 ms
   between the end of a reply and the next request (section 2).
2. After opening the port: wait 100 ms, clear the input buffer, then
   CONTROLLER DETECTION: PING with a 0.3 s timeout, up to 2 attempts. A reply
   starting with "OK PONG ADVCTRL" means "controller detected"; any other
   reply or silence means "controller not detected" (another device or
   nothing on that port). After the detection: ID (firmware version, number
   of cameras, MCU UID - it tells several controllers apart).
   Finding the right port among many: run the same procedure on every COM
   port (RemoteClient::detect(), RemoteClient::findController();
   remote_test --scan / --detect).
3. Wait for each command up to the timeout of section 2; ignore lines that do
   not start with OK/ERR (link test, garbage at power-on).
4. Repeat only idempotent commands on timeout (list in section 2); drop the
   late reply before the repeat (section 2).
5. ERR is not a link failure: show it to the user and continue.
6. Synchronise the PC user interface from STATUS (once per second or after
   each command), not from an assumed state.
7. No reply to 3 consecutive PINGs = link lost: reopen the port.
8. Enable the checksum in industrial environments (long line, noise).

C++ / Qt 6 - blocking client (lib/remoteclient.h), for a console program or a
worker thread:
  #include "remoteclient.h"
  AdvCtrl::RemoteClient rc;
  if (!rc.open("/dev/ttyUSB0")) {
      qWarning() << rc.errorString();
      return;
  }
  if (rc.detect()) {
      AdvCtrl::Fields id;
      if (rc.identify(&id))
          qDebug() << id.value("FW") << id.value("UID");
  }
  rc.cam(2);
  rc.brightness(12);
  rc.led(3);
  AdvCtrl::Fields st;
  if (rc.status(&st))
      qDebug() << "BR =" << st.value("BR").toInt();
  rc.key("XH1");                                // show crosshair 1
  if (!rc.brightness(99))                       // the controller answers ERR 3 RANGE
      qDebug() << "controller said" << rc.lastReply().code << rc.lastReply().text;

C++ / Qt 6 - GUI application (lib/controllerlink.h): the client runs in its own
thread, the results arrive as signals in the GUI thread:
  #include "controllerlink.h"
  auto *link = new AdvCtrl::ControllerLink(this);
  connect(link, &AdvCtrl::ControllerLink::stateChanged, this,
          [](AdvCtrl::ControllerLink::State s) { /* Closed, NoLink, Link */ });
  connect(link, &AdvCtrl::ControllerLink::reply, this,
          [](const QString &command, const QString &result) { /* "OK", "ERR 3 RANGE", "!! ..." */ });
  connect(link, &AdvCtrl::ControllerLink::leds, this, [](quint32 mask) { /* 24 led bits */ });
  link->open("/dev/ttyUSB0");
  link->send("CAM 2");
  link->send("BR 12");
The virtual panel (panel/deviceview.cpp) is a complete example of ControllerLink.
Build: in your CMake project find Qt (find_package(Qt6 REQUIRED COMPONENTS Core
SerialPort) and qt_standard_project_setup()), then add_subdirectory(<path>/lib)
and target_link_libraries(<your target> PRIVATE AdvancedControllerRemote) - or
add the four files of lib/ to your target.

C# example (System.IO.Ports.SerialPort):
  var sp = new SerialPort("/dev/ttyUSB0", 9600, Parity.None, 8, StopBits.One);
  sp.NewLine = "\r\n"; sp.ReadTimeout = 500; sp.Open();
  Thread.Sleep(100); sp.DiscardInBuffer();
  string Cmd(string line, int timeoutMs = 500) {
      Thread.Sleep(5);                        // line turnaround (section 2)
      sp.ReadTimeout = timeoutMs; sp.DiscardInBuffer();
      sp.WriteLine(line);
      while (true) {                          // skip lines that are not replies
          string r = sp.ReadLine().Trim();
          if (r.StartsWith("OK") || r.StartsWith("ERR")) return r;
      }
  }
  Console.WriteLine(Cmd("VER"));
  Cmd("BR 12");
  Console.WriteLine(Cmd("STATUS"));
  Console.WriteLine(Cmd("RESET", 10000));

Pseudo code for C / embedded hosts:
  wait_ms(5);                                  /* line turnaround after the previous reply */
  send("BR 12\r\n"); line = read_line(timeout);
  if (starts_with(line, "OK")) ... else if (starts_with(line, "ERR")) code = atoi(line+4);


9. FILES IN THIS FOLDER
-----------------------
  build.sh              builds the project and puts binaries into bin/
  run.sh                starts the virtual front panel (bin/AdvancedControllerPanel)
  CMakeLists.txt        CMake project: lib, panel, tools
  lib/                  client library to embed into your software:
                          remoteclient.h/.cpp    RemoteClient - blocking, one port
                          controllerlink.h/.cpp  ControllerLink - RemoteClient in its own
                                                 thread, results as Qt signals (GUI)
  panel/                virtual front panel (Qt Widgets): the device buttons, the leds
                        mirror the real ones (LEDS)
  tools/remote_test.cpp test tool (bin/remote_test): --list, --scan, --detect,
                        --monitor, -c CMD, --selftest, interactive
  docs/                 this document (RU/EN) and the panel button guide (PANEL_HELP.txt)
  The panel works with three controllers at once: tabs CONTROLLER 1..3, each
  with its own port, OPEN/CLOSE and indicator (grey - port closed, red - port
  open but no answer, green - connected).
Requirements: Linux, Qt 6.5 or later with Qt Serial Port, GCC or Clang (C++17),
CMake 3.16 or later. Ports like /dev/ttyUSB0; the user must be in the dialout
group (sudo usermod -a -G dialout $USER).
