summaryrefslogtreecommitdiffstats
path: root/.bash_logout
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-06-11 20:21:03 +0200
committerThomas Vanbesien <tvanbesi@proton.me>2026-06-11 21:19:10 +0200
commit29cf89ff5b26dd7443b5fcd5d9bd6f49e46b7b15 (patch)
tree793087f29e2abafdd4b84b9b522d07a5c14f101d /.bash_logout
parent8033c39d0a8ecba3ef3d9a5628c213421283cce6 (diff)
downloaddotfiles-29cf89ff5b26dd7443b5fcd5d9bd6f49e46b7b15.tar.gz
dotfiles-29cf89ff5b26dd7443b5fcd5d9bd6f49e46b7b15.zip
docs(nvim): edit comments
Diffstat (limited to '.bash_logout')
0 files changed, 0 insertions, 0 deletions
/**
 * @file   Xpl2Client.cpp
 * @brief  TCP client for the Alchemie XPL2 printhead protocol.
 */
#include "Xpl2Client.h"

#include <QDebug>
#include <QTcpSocket>

bool Xpl2Client::s_wireDebug = false;

/* ------------------------------------------------------------------ */
/*  Response dispatch table                                           */
/* ------------------------------------------------------------------ */

// clang-format off
const QHash<QByteArray, Xpl2Client::ResponseEntry>
Xpl2Client::s_responseTable = {
  /* CN_ JC success shape: controllerId, successFlag */
  { "CN_JETTING_ALL_ON",       { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jettingAllOnResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CN_JETTING_ON",           { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jettingOnResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CN_JETTING_OFF",          { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jettingOffResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CN_JC_ID_LED_ON",         { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcIdLedOnResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CN_JC_ID_LED_OFF",        { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcIdLedOffResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CN_JC_CALIBRATION",       { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcCalibrationResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CN_JC_RESET_FAULT_CODES", { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcResetFaultCodesResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CN_JC_STATUS_MESSAGING_STOP",  { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcStatusMessagingStopResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CN_PH_STATUS_MESSAGING_STOP",  { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->phStatusMessagingStopResult (p[0].toInt (), p[1].toInt () == 1); } } },

  /* CN_ PH success shape: controllerId, printheadId, successFlag */
  { "CN_PH_JETTING_ON",        { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phJettingOnResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "CN_PH_JETTING_OFF",       { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phJettingOffResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "CN_PH_ID_LED_ON",         { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phIdLedOnResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "CN_PH_ID_LED_OFF",        { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phIdLedOffResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "CN_PH_CALIBRATION",       { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phCalibrationResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "CN_PH_RESET_FAULT_CODES", { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phResetFaultCodesResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "CN_PH_NOZZLES_DISABLED",  { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phNozzlesDisabledResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },

  /* CN_ calibration data: controllerId, phId, 48 floats */
  { "CN_PH_CALIBRATION_DATA",     { ResponseShape::CalData, 50,
    [](auto *s, const auto &p) { emit s->phCalibrationDataReceived (p[0].toInt (), p[1].toInt (), p.mid (2, 48)); } } },
  { "CN_PH_CALIBRATION_RAW_DATA", { ResponseShape::CalData, 50,
    [](auto *s, const auto &p) { emit s->phCalibrationRawDataReceived (p[0].toInt (), p[1].toInt (), p.mid (2, 48)); } } },

  /* CN_ base frequency: controllerId, phId, baseFreq, activeBaseFreq */
  { "CN_PH_CALIBRATED_BASE_FREQUENCY", { ResponseShape::BaseFreq, 4,
    [](auto *s, const auto &p) { emit s->phCalibratedBaseFrequencyReceived (p[0].toInt (), p[1].toInt (), p[2].toDouble (), p[3].toDouble ()); } } },

  /* CN_ status messaging start: controllerId, level, interval, successFlag */
  { "CN_JC_STATUS_MESSAGING_START", { ResponseShape::StatusStart, 4,
    [](auto *s, const auto &p) { emit s->jcStatusMessagingStartResult (p[0].toInt (), p[1].toInt (), p[2].toInt (), p[3].toInt () == 1); } } },
  { "CN_PH_STATUS_MESSAGING_START", { ResponseShape::StatusStart, 4,
    [](auto *s, const auto &p) { emit s->phStatusMessagingStartResult (p[0].toInt (), p[1].toInt (), p[2].toInt (), p[3].toInt () == 1); } } },

  /* CF_ JC success shape: controllerId, successFlag */
  { "CF_PH_DEASSIGN_ID",                  { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->phDeassignIdResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CF_JC_SAVE_CALIBRATION",             { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcSaveCalibrationResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CF_JC_RESET_CALIBRATION",            { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcResetCalibrationResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CF_JC_SWITCH_OFF_PURGE",             { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcSwitchOffPurgeResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CF_JC_RESET_SETTINGS_ALL_PRINTHEADS", { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcResetSettingsAllPrintheadsResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CF_JC_REBOOT_ALL_PRINTHEADS",        { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcRebootAllPrintheadsResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CF_JC_RESET_CONTROLLER_SOFTWARE",    { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcResetControllerSoftwareResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CF_JC_RESTART",                      { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcRestartResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CF_JC_SHUTDOWN",                     { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcShutdownResult (p[0].toInt (), p[1].toInt () == 1); } } },
  { "CF_JC_SAVE_ALL_PRINTHEAD_SETTINGS",  { ResponseShape::JcSuccess, 2,
    [](auto *s, const auto &p) { emit s->jcSaveAllPrintheadSettingsResult (p[0].toInt (), p[1].toInt () == 1); } } },

  /* CF_ PH success shape: controllerId, phId, successFlag */
  { "CF_PH_SET_ID",              { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phSetIdResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "CF_PH_SAVE_CALIBRATION",   { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phSaveCalibrationResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "CF_PH_RESET_CALIBRATION",  { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phResetCalibrationResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "CF_PH_RESET_ALL_SETTINGS", { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phResetAllSettingsResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "CF_PH_REBOOT",             { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phRebootResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "CF_PH_SAVE_SETTINGS",      { ResponseShape::PhSuccess, 3,
    [](auto *s, const auto &p) { emit s->phSaveSettingsResult (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },

  /* CF_ purge settings: controllerId, purgeInterval, purgeTime, successFlag */
  { "CF_JC_SET_PURGE_SETTINGS", { ResponseShape::PurgeSettings, 4,
    [](auto *s, const auto &p) { emit s->jcSetPurgeSettingsResult (p[0].toInt (), p[1].toInt (), p[2].toInt (), p[3].toInt () == 1); } } },

  /* CF_ JC jetting params: cid, dutyCycle, pwmFreq, drive, nozzleDriveFreq, nozzleDriveDutyCycle, successFlag */
  { "CF_JC_SET_JETTING_PARAMS", { ResponseShape::JcJettingParams, 7,
    [](auto *s, const auto &p) { emit s->jcSetJettingParamsResult (p[0].toInt (), p[1].toInt (), p[2].toInt (), p[3].toInt (), p[4].toInt (), p[5].toInt (), static_cast<Xpl2Protocol::JettingParamsResult> (p[6].toInt ())); } } },

  /* CF_ PH jetting params: cid, phId, dutyCycle, pwmFreq, drive, nozzleDriveFreq, nozzleDriveDutyCycle, successFlag */
  { "CF_PH_SET_JETTING_PARAMS", { ResponseShape::PhJettingParams, 8,
    [](auto *s, const auto &p) { emit s->phSetJettingParamsResult (p[0].toInt (), p[1].toInt (), p[2].toInt (), p[3].toInt (), p[4].toInt (), p[5].toInt (), p[6].toInt (), static_cast<Xpl2Protocol::JettingParamsResult> (p[7].toInt ())); } } },
  { "CF_PH_GET_JETTING_PARAMS", { ResponseShape::PhJettingParams, 8,
    [](auto *s, const auto &p) { emit s->phGetJettingParamsResult (p[0].toInt (), p[1].toInt (), p[2].toInt (), p[3].toInt (), p[4].toInt (), p[5].toInt (), p[6].toInt (), static_cast<Xpl2Protocol::JettingParamsResult> (p[7].toInt ())); } } },

  /* CF_ JC setter: cid, saveNewValue, setterId, newValue, successFlag */
  { "CF_JC_SETTER", { ResponseShape::JcSetter, 5,
    [](auto *s, const auto &p) { emit s->jcSetterResult (p[0].toInt (), p[1].toInt () != 0, p[2].toInt (), p[3].toString (), static_cast<Xpl2Protocol::SetterResult> (p[4].toInt ())); } } },

  /* CF_ PH setter: cid, phId, saveNewValue, setterId, newValue, successFlag */
  { "CF_PH_SETTER", { ResponseShape::PhSetter, 6,
    [](auto *s, const auto &p) { emit s->phSetterResult (p[0].toInt (), p[1].toInt (), p[2].toInt () != 0, p[3].toInt (), p[4].toString (), static_cast<Xpl2Protocol::SetterResult> (p[5].toInt ())); } } },

  /* CF_ JC getter: cid, savedValue, getterId, currentValue, successFlag */
  { "CF_JC_GETTER", { ResponseShape::JcGetter, 5,
    [](auto *s, const auto &p) { emit s->jcGetterResult (p[0].toInt (), p[1].toInt () != 0, p[2].toInt (), p[3].toString (), p[4].toInt () == 1); } } },

  /* CF_ PH getter: cid, phId, savedValue, getterId, currentValue, successFlag */
  { "CF_PH_GETTER", { ResponseShape::PhGetter, 6,
    [](auto *s, const auto &p) { emit s->phGetterResult (p[0].toInt (), p[1].toInt (), p[2].toInt () != 0, p[3].toInt (), p[4].toString (), p[5].toInt () == 1); } } },

  /* EV_ events */
  { "EV_PH_CONNECTION_CHANGED", { ResponseShape::PhConnectionChanged, 3,
    [](auto *s, const auto &p) { emit s->phConnectionChanged (p[0].toInt (), p[1].toInt (), p[2].toInt () == 1); } } },
  { "EV_JC_ERROR_CODE", { ResponseShape::ErrorCode, 3,
    [](auto *s, const auto &p) {
      QStringList ep; for (int i = 3; i < p.size (); ++i) ep << p[i].toString ();
      emit s->jcErrorCode (p[0].toInt (), p[1].toInt (), ep); } } },
  { "EV_IMG_ERROR_CODE", { ResponseShape::ErrorCode, 3,
    [](auto *s, const auto &p) {
      QStringList ep; for (int i = 3; i < p.size (); ++i) ep << p[i].toString ();
      emit s->imgErrorCode (p[0].toInt (), p[1].toInt (), ep); } } },
  { "EV_STATUS_ERROR_CODE", { ResponseShape::ErrorCode, 3,
    [](auto *s, const auto &p) {
      QStringList ep; for (int i = 3; i < p.size (); ++i) ep << p[i].toString ();
      emit s->statusErrorCode (p[0].toInt (), p[1].toInt (), ep); } } },
  { "EV_PH_ERROR_CODE", { ResponseShape::PhErrorCode, 4,
    [](auto *s, const auto &p) {
      QStringList ep; for (int i = 4; i < p.size (); ++i) ep << p[i].toString ();
      emit s->phErrorCode (p[0].toInt (), p[1].toInt (), p[2].toInt (), ep); } } },
};
// clang-format on

Xpl2Client::Xpl2Client (QObject *parent) : QObject (parent)
{
  setupServer (m_commandServer, m_commandPort);
  setupServer (m_imagingServer, m_imagingPort);
  setupServer (m_statusServer, m_statusPort);
}

/* ------------------------------------------------------------------ */
/*  Properties                                                        */
/* ------------------------------------------------------------------ */

bool
Xpl2Client::isListening () const
{
  return m_listening;
}

bool
Xpl2Client::isConnected () const
{
  return m_connected;
}

int
Xpl2Client::controllerId () const
{
  return m_controllerId;
}

QString
Xpl2Client::firmwareVersion () const
{
  return m_firmwareVersion;
}

QString
Xpl2Client::hardwareVersion () const
{
  return m_hardwareVersion;
}

int
Xpl2Client::printheadCount () const
{
  return m_printheadCount;
}

void
Xpl2Client::enableWireDebug ()
{
  s_wireDebug = true;
}

/* ------------------------------------------------------------------ */
/*  Connection                                                        */
/* ------------------------------------------------------------------ */

void
Xpl2Client::setupServer (QTcpServer &server, quint16 port)
{
  Q_UNUSED (port)
  connect (&server, &QTcpServer::newConnection, this,
           &Xpl2Client::onNewConnection);
}

void
Xpl2Client::startListening ()
{
  if (m_listening)
    {
      emit statusMessage (QStringLiteral ("Already listening"));
      return;
    }
  if (!m_commandServer.listen (QHostAddress::Any, m_commandPort)
      || !m_imagingServer.listen (QHostAddress::Any, m_imagingPort)
      || !m_statusServer.listen (QHostAddress::Any, m_statusPort))
    {
      emit errorOccurred (QStringLiteral ("Failed to listen: %1 / %2 / %3")
                              .arg (m_commandServer.errorString ())
                              .arg (m_imagingServer.errorString ())
                              .arg (m_statusServer.errorString ()));
      m_commandServer.close ();
      m_imagingServer.close ();
      m_statusServer.close ();
      return;
    }
  m_listening = true;
  emit listeningChanged ();
  emit statusMessage (QStringLiteral ("Listening on ports %1/%2/%3…")
                          .arg (m_commandPort)
                          .arg (m_imagingPort)
                          .arg (m_statusPort));
}

void
Xpl2Client::stopListening ()
{
  m_commandServer.close ();
  m_imagingServer.close ();
  m_statusServer.close ();

  auto cleanup = [] (QTcpSocket *&sock)
    {
      if (sock)
        {
          sock->disconnectFromHost ();
          sock->deleteLater ();
          sock = nullptr;
        }
    };
  cleanup (m_commandSocket);
  cleanup (m_imagingSocket);
  cleanup (m_statusSocket);

  if (m_listening)
    {
      m_listening = false;
      emit listeningChanged ();
    }
  updateConnectedState ();
}

QTcpSocket *&
Xpl2Client::socketForServer (QTcpServer *server)
{
  if (server == &m_commandServer)
    return m_commandSocket;
  if (server == &m_imagingServer)
    return m_imagingSocket;
  return m_statusSocket;
}

void
Xpl2Client::getJcVersion ()
{
  sendCommand (m_commandSocket, "GS_JC_VERSION");
}

void
Xpl2Client::getPhVersion (int printheadId)
{
  sendCommand (m_commandSocket, "GS_PH_VERSION", { printheadId });
}

/* ------------------------------------------------------------------ */
/*  CN_ Control commands                                              */
/* ------------------------------------------------------------------ */

void
Xpl2Client::jettingAllOn ()
{
  sendCommand (m_commandSocket, "CN_JETTING_ALL_ON");
}

void
Xpl2Client::jettingOn (const QString &jettingMask)
{
  sendCommand (m_commandSocket, "CN_JETTING_ON", { jettingMask });
}

void
Xpl2Client::jettingOff ()
{
  sendCommand (m_commandSocket, "CN_JETTING_OFF");
}

void
Xpl2Client::phJettingOn (int printheadId, const QString &jettingMask)
{
  sendCommand (m_commandSocket, "CN_PH_JETTING_ON",
               { printheadId, jettingMask });
}

void
Xpl2Client::phJettingOff (int printheadId)
{
  sendCommand (m_commandSocket, "CN_PH_JETTING_OFF", { printheadId });
}

void
Xpl2Client::jcIdLedOn ()
{
  sendCommand (m_commandSocket, "CN_JC_ID_LED_ON");
}

void
Xpl2Client::jcIdLedOff ()
{
  sendCommand (m_commandSocket, "CN_JC_ID_LED_OFF");
}

void
Xpl2Client::phIdLedOn (int printheadId)
{
  sendCommand (m_commandSocket, "CN_PH_ID_LED_ON", { printheadId });
}

void
Xpl2Client::phIdLedOff (int printheadId)
{
  sendCommand (m_commandSocket, "CN_PH_ID_LED_OFF", { printheadId });
}

void
Xpl2Client::jcCalibration ()
{
  sendCommand (m_commandSocket, "CN_JC_CALIBRATION");
}

void
Xpl2Client::phCalibration (int printheadId)
{
  sendCommand (m_commandSocket, "CN_PH_CALIBRATION", { printheadId });
}

void
Xpl2Client::phCalibrationData (int printheadId)
{
  sendCommand (m_commandSocket, "CN_PH_CALIBRATION_DATA", { printheadId });
}

void
Xpl2Client::phCalibrationRawData (int printheadId)
{
  sendCommand (m_commandSocket, "CN_PH_CALIBRATION_RAW_DATA", { printheadId });
}

void
Xpl2Client::phCalibratedBaseFrequency (int printheadId)
{
  sendCommand (m_commandSocket, "CN_PH_CALIBRATED_BASE_FREQUENCY",
               { printheadId });
}

void
Xpl2Client::jcStatusMessagingStart (int statusLevel, int sendIntervalMs)
{
  sendCommand (m_statusSocket, "CN_JC_STATUS_MESSAGING_START",
               { statusLevel, sendIntervalMs });
}

void
Xpl2Client::jcStatusMessagingStop ()
{
  sendCommand (m_statusSocket, "CN_JC_STATUS_MESSAGING_STOP");
}

void
Xpl2Client::phStatusMessagingStart (int statusLevel, int sendIntervalMs)
{
  sendCommand (m_statusSocket, "CN_PH_STATUS_MESSAGING_START",
               { statusLevel, sendIntervalMs });
}

void
Xpl2Client::phStatusMessagingStop ()
{
  sendCommand (m_statusSocket, "CN_PH_STATUS_MESSAGING_STOP");
}

void
Xpl2Client::jcResetFaultCodes ()
{
  sendCommand (m_commandSocket, "CN_JC_RESET_FAULT_CODES");
}

void
Xpl2Client::phResetFaultCodes (int printheadId)
{
  sendCommand (m_commandSocket, "CN_PH_RESET_FAULT_CODES", { printheadId });
}

void
Xpl2Client::phNozzlesDisabled (int printheadId, const QString &mask)
{
  sendCommand (m_commandSocket, "CN_PH_NOZZLES_DISABLED",
               { printheadId, mask });
}

/* ------------------------------------------------------------------ */
/*  CF_ Configuration commands                                        */
/* ------------------------------------------------------------------ */

void
Xpl2Client::phSetId (int printheadId)
{
  sendCommand (m_commandSocket, "CF_PH_SET_ID", { printheadId });
}

void
Xpl2Client::phDeassignId ()
{
  sendCommand (m_commandSocket, "CF_PH_DEASSIGN_ID");
}

void
Xpl2Client::jcSetJettingParams (int dutyCycle, int pwmFreq, int drive,
                                int nozzleDriveFreq, int nozzleDriveDutyCycle)
{
  sendCommand (
      m_commandSocket, "CF_JC_SET_JETTING_PARAMS",
      { dutyCycle, pwmFreq, drive, nozzleDriveFreq, nozzleDriveDutyCycle });
}

void
Xpl2Client::phSetJettingParams (int printheadId, int dutyCycle, int pwmFreq,
                                int drive, int nozzleDriveFreq,
                                int nozzleDriveDutyCycle)
{
  sendCommand (m_commandSocket, "CF_PH_SET_JETTING_PARAMS",
               { printheadId, dutyCycle, pwmFreq, drive, nozzleDriveFreq,
                 nozzleDriveDutyCycle });
}

void
Xpl2Client::phGetJettingParams (int printheadId)
{
  sendCommand (m_commandSocket, "CF_PH_GET_JETTING_PARAMS", { printheadId });
}

void
Xpl2Client::jcSaveCalibration ()
{
  sendCommand (m_commandSocket, "CF_JC_SAVE_CALIBRATION");
}

void
Xpl2Client::phSaveCalibration (int printheadId)
{
  sendCommand (m_commandSocket, "CF_PH_SAVE_CALIBRATION", { printheadId });
}

void
Xpl2Client::jcResetCalibration ()
{
  sendCommand (m_commandSocket, "CF_JC_RESET_CALIBRATION");
}

void
Xpl2Client::phResetCalibration (int printheadId)
{
  sendCommand (m_commandSocket, "CF_PH_RESET_CALIBRATION", { printheadId });
}

void
Xpl2Client::jcSetPurgeSettings (int purgeIntervalMs, int purgeTimeMs)
{
  sendCommand (m_commandSocket, "CF_JC_SET_PURGE_SETTINGS",
               { purgeIntervalMs, purgeTimeMs });
}

void
Xpl2Client::jcSwitchOffPurge ()
{
  sendCommand (m_commandSocket, "CF_JC_SWITCH_OFF_PURGE");
}

void
Xpl2Client::jcSetter (bool saveNewValue, int setterId, const QString &newValue)
{
  sendCommand (m_commandSocket, "CF_JC_SETTER",
               { saveNewValue, setterId, newValue });
}

void
Xpl2Client::phSetter (int printheadId, bool saveNewValue, int setterId,
                      const QString &newValue)
{
  sendCommand (m_commandSocket, "CF_PH_SETTER",
               { printheadId, saveNewValue, setterId, newValue });
}

void
Xpl2Client::jcGetter (bool getSavedValue, int getterId)
{
  sendCommand (m_commandSocket, "CF_JC_GETTER", { getSavedValue, getterId });
}

void
Xpl2Client::phGetter (int printheadId, bool getSavedValue, int getterId)
{
  sendCommand (m_commandSocket, "CF_PH_GETTER",
               { printheadId, getSavedValue, getterId });
}

void
Xpl2Client::jcResetSettingsAllPrintheads ()
{
  sendCommand (m_commandSocket, "CF_JC_RESET_SETTINGS_ALL_PRINTHEADS");
}

void
Xpl2Client::phResetAllSettings (int printheadId)
{
  sendCommand (m_commandSocket, "CF_PH_RESET_ALL_SETTINGS", { printheadId });
}

void
Xpl2Client::jcRebootAllPrintheads ()
{
  sendCommand (m_commandSocket, "CF_JC_REBOOT_ALL_PRINTHEADS");
}

void
Xpl2Client::phReboot (int printheadId)
{
  sendCommand (m_commandSocket, "CF_PH_REBOOT", { printheadId });
}

void
Xpl2Client::jcResetControllerSoftware ()
{
  sendCommand (m_commandSocket, "CF_JC_RESET_CONTROLLER_SOFTWARE");
}

void
Xpl2Client::jcRestart ()
{
  sendCommand (m_commandSocket, "CF_JC_RESTART");
}

void
Xpl2Client::jcShutdown ()
{
  sendCommand (m_commandSocket, "CF_JC_SHUTDOWN");
}

void
Xpl2Client::jcSaveAllPrintheadSettings ()
{
  sendCommand (m_commandSocket, "CF_JC_SAVE_ALL_PRINTHEAD_SETTINGS");
}

void
Xpl2Client::phSaveSettings (int printheadId)
{
  sendCommand (m_commandSocket, "CF_PH_SAVE_SETTINGS", { printheadId });
}

/* ------------------------------------------------------------------ */
/*  Imaging (m/n) commands                                            */
/* ------------------------------------------------------------------ */

void
Xpl2Client::imagingStart (double speed)
{
  sendCommand (m_imagingSocket, "m2", { speed });
}

void
Xpl2Client::imagingStop ()
{
  sendCommand (m_imagingSocket, "m4");
}

void
Xpl2Client::imageMaskStart (const QString &mask)
{
  sendCommand (m_imagingSocket, "m0", { mask });
}

void
Xpl2Client::imageMaskEnd (const QString &tail)
{
  sendCommand (m_imagingSocket, "m1", { tail });
}

void
Xpl2Client::dutyCycleMaskStart (const QString &mask)
{
  sendCommand (m_imagingSocket, "m5", { mask });
}

void
Xpl2Client::dutyCycleMaskEnd (const QString &tail)
{
  sendCommand (m_imagingSocket, "m6", { tail });
}

void
Xpl2Client::imageCount ()
{
  sendCommand (m_imagingSocket, "m3");
}

/* ------------------------------------------------------------------ */
/*  Send / dispatch                                                   */
/* ------------------------------------------------------------------ */

void
Xpl2Client::sendCommand (QTcpSocket *socket, const QByteArray &command,
                         const QVariantList &params)
{
  if (!socket || socket->state () != QAbstractSocket::ConnectedState)
    {
      emit errorOccurred (QStringLiteral ("Not connected for %1")
                              .arg (QString::fromUtf8 (command)));
      return;
    }
  QByteArray data = Xpl2Protocol::buildMessage (command, params);
  socket->write (data);

  QByteArray wire;
  if (s_wireDebug)
    wire = " >> " + data.trimmed ();
  qDebug ("%s TX %s%s", qPrintable (logTag (socket)), command.constData (),
          wire.constData ());
}

void
Xpl2Client::dispatchCommandMessage (const Xpl2Protocol::ParsedMessage &msg)
{
  if (msg.command == "KA_PING")
    handleKaPing (m_commandSocket);
  else if (msg.command == "EV_SHUTTING_DOWN")
    emit shuttingDown ();
  else if (msg.command == "GS_JC_VERSION")
    handleGsJcVersion (msg.params);
  else if (msg.command == "GS_PH_VERSION")
    handleGsPhVersion (msg.params);
  else if (!dispatchResponse (msg.command, msg.params, m_commandSocket))
    qWarning ("%s Unknown command: %s", qPrintable (logTag (m_commandSocket)),
              msg.command.constData ());
}

void
Xpl2Client::dispatchImagingMessage (const Xpl2Protocol::ParsedMessage &msg)
{
  if (msg.command == "KA_PING")
    handleKaPing (m_imagingSocket);
  else if (msg.command == "EV_SHUTTING_DOWN")
    emit shuttingDown ();
  else if (msg.command == "n")
    {
      int lines = 0;
      if (!msg.params.isEmpty ())
        lines = msg.params[0].toString ().toInt (nullptr, 16);
      qDebug ("%s    n imageLines=%d", qPrintable (logTag (m_imagingSocket)),
              lines);
      emit statusMessage (QStringLiteral ("RX n: imageLines=%1").arg (lines));
      emit imagingReply (lines);
    }
  else if (msg.command == "m4")
    {
      bool ok = !msg.params.isEmpty () && msg.params[0].toInt () == 1;
      qDebug ("%s    m4 success=%d", qPrintable (logTag (m_imagingSocket)),
              ok);
      emit statusMessage (QStringLiteral ("RX m4: success=%1").arg (ok));
      emit imagingStopResult (ok);
    }
  else if (!dispatchResponse (msg.command, msg.params, m_imagingSocket))
    qWarning ("%s Unknown command: %s", qPrintable (logTag (m_imagingSocket)),
              msg.command.constData ());
}

void
Xpl2Client::dispatchStatusMessage (const Xpl2Protocol::ParsedMessage &msg)
{
  if (msg.command == "KA_PING")
    handleKaPing (m_statusSocket);
  else if (msg.command == "EV_SHUTTING_DOWN")
    emit shuttingDown ();
  else if (msg.command == "EV_STATUS_MSG_JC")
    handleEvStatusMsgJc (msg.params);
  else if (msg.command == "EV_STATUS_MSG_PH")
    handleEvStatusMsgPh (msg.params);
  else if (!dispatchResponse (msg.command, msg.params, m_statusSocket))
    qWarning ("%s Unknown command: %s", qPrintable (logTag (m_statusSocket)),
              msg.command.constData ());
}

void
Xpl2Client::handleKaPing (QTcpSocket *socket)
{
  sendCommand (socket, "KA_PING", { 1 });
}

void
Xpl2Client::handleGsJcVersion (const QVariantList &params)
{
  if (params.size () < 4)
    {
      qWarning () << "GS_JC_VERSION: expected 4 params, got" << params.size ();
      return;
    }
  m_controllerId = params[0].toInt ();
  m_firmwareVersion = params[1].toString ();
  m_hardwareVersion = params[2].toString ();
  m_printheadCount = params[3].toInt ();
  qDebug ("%s    controller=%d fw=%s hw=%s phCount=%d",
          qPrintable (logTag (m_commandSocket)), m_controllerId,
          qPrintable (m_firmwareVersion), qPrintable (m_hardwareVersion),
          m_printheadCount);
  emit statusMessage (
      QStringLiteral ("RX JC Version: controller=%1 fw=%2 hw=%3 phCount=%4")
          .arg (m_controllerId)
          .arg (m_firmwareVersion)
          .arg (m_hardwareVersion)
          .arg (m_printheadCount));
  emit jcVersionReceived ();
}

void
Xpl2Client::handleGsPhVersion (const QVariantList &params)
{
  if (params.size () < 8)
    {
      qWarning () << "GS_PH_VERSION: expected 8 params, got" << params.size ();
      return;
    }
  int cid = params[0].toInt ();
  int phId = params[1].toInt ();
  QString mcuFw = params[2].toString ();
  QString mcuHw = params[3].toString ();
  QString mcuFwVar = params[4].toString ();
  QString fpgaFw = params[5].toString ();
  QString fpgaHw = params[6].toString ();
  QString bootVer = params[7].toString ();
  qDebug ("%s    PH[%d] mcuFw=%s mcuHw=%s mcuFwVar=%s fpgaFw=%s fpgaHw=%s "
          "boot=%s",
          qPrintable (logTag (m_commandSocket)), phId, qPrintable (mcuFw),
          qPrintable (mcuHw), qPrintable (mcuFwVar), qPrintable (fpgaFw),
          qPrintable (fpgaHw), qPrintable (bootVer));
  emit statusMessage (
      QStringLiteral (
          "RX PH[%1] Version: mcuFw=%2 mcuHw=%3 fpgaFw=%4 fpgaHw=%5 boot=%6")
          .arg (phId)
          .arg (mcuFw)
          .arg (mcuHw)
          .arg (fpgaFw)
          .arg (fpgaHw)
          .arg (bootVer));
  emit phVersionReceived (cid, phId, mcuFw, mcuHw, mcuFwVar, fpgaFw, fpgaHw,
                          bootVer);
}

bool
Xpl2Client::dispatchResponse (const QByteArray &command,
                              const QVariantList &params,
                              const QTcpSocket *socket)
{
  auto it = s_responseTable.find (command);
  if (it == s_responseTable.end ())
    return false;

  const ResponseEntry &entry = *it;
  if (params.size () < entry.minParams)
    {
      qWarning ("%s %s: expected %d params, got %lld",
                qPrintable (logTag (socket)), command.constData (),
                entry.minParams, static_cast<long long> (params.size ()));
      return true;
    }

  int cid = params[0].toInt ();
  QString cmd = QString::fromUtf8 (command);
  QString logStr;

  switch (entry.shape)
    {
    case ResponseShape::JcSuccess:
      logStr = QStringLiteral ("RX %1: controller=%2 success=%3")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[1].toInt () == 1);
      break;
    case ResponseShape::PhSuccess:
      logStr = QStringLiteral ("RX %1: controller=%2 ph=%3 success=%4")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[1].toInt ())
                   .arg (params[2].toInt () == 1);
      break;
    case ResponseShape::CalData:
      logStr = QStringLiteral ("RX %1: controller=%2 ph=%3")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[1].toInt ());
      break;
    case ResponseShape::BaseFreq:
      logStr = QStringLiteral ("RX %1: controller=%2 ph=%3 base=%4 active=%5")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[1].toInt ())
                   .arg (params[2].toDouble ())
                   .arg (params[3].toDouble ());
      break;
    case ResponseShape::StatusStart:
      logStr = QStringLiteral ("RX %1: controller=%2 level=%3 interval=%4 "
                               "success=%5")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[1].toInt ())
                   .arg (params[2].toInt ())
                   .arg (params[3].toInt () == 1);
      break;
    case ResponseShape::PurgeSettings:
      logStr = QStringLiteral (
                   "RX %1: controller=%2 interval=%3 time=%4 success=%5")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[1].toInt ())
                   .arg (params[2].toInt ())
                   .arg (params[3].toInt () == 1);
      break;
    case ResponseShape::JcJettingParams:
      logStr = QStringLiteral ("RX %1: controller=%2 result=%3")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[6].toInt ());
      break;
    case ResponseShape::PhJettingParams:
      logStr = QStringLiteral ("RX %1: controller=%2 ph=%3 result=%4")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[1].toInt ())
                   .arg (params[7].toInt ());
      break;
    case ResponseShape::JcSetter:
      logStr = QStringLiteral ("RX %1: controller=%2 setter=%3 result=%4")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[2].toInt ())
                   .arg (params[4].toInt ());
      break;
    case ResponseShape::PhSetter:
      logStr
          = QStringLiteral ("RX %1: controller=%2 ph=%3 setter=%4 result=%5")
                .arg (cmd)
                .arg (cid)
                .arg (params[1].toInt ())
                .arg (params[3].toInt ())
                .arg (params[5].toInt ());
      break;
    case ResponseShape::JcGetter:
      logStr = QStringLiteral ("RX %1: controller=%2 getter=%3 value=%4 "
                               "success=%5")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[2].toInt ())
                   .arg (params[3].toString ())
                   .arg (params[4].toInt () == 1);
      break;
    case ResponseShape::PhGetter:
      logStr = QStringLiteral ("RX %1: controller=%2 ph=%3 getter=%4 "
                               "value=%5 success=%6")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[1].toInt ())
                   .arg (params[3].toInt ())
                   .arg (params[4].toString ())
                   .arg (params[5].toInt () == 1);
      break;
    case ResponseShape::PhConnectionChanged:
      logStr = QStringLiteral ("RX %1: controller=%2 ph=%3 connected=%4")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[1].toInt ())
                   .arg (params[2].toInt () == 1);
      break;
    case ResponseShape::ErrorCode:
      logStr = QStringLiteral ("RX %1: controller=%2 error=%3")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[1].toInt ());
      break;
    case ResponseShape::PhErrorCode:
      logStr = QStringLiteral ("RX %1: controller=%2 ph=%3 error=%4")
                   .arg (cmd)
                   .arg (cid)
                   .arg (params[1].toInt ())
                   .arg (params[2].toInt ());
      break;
    }

  qDebug ("%s    %s", qPrintable (logTag (socket)), qPrintable (logStr));
  emit statusMessage (logStr);
  entry.emitter (this, params);
  return true;
}

void
Xpl2Client::handleEvStatusMsgJc (const QVariantList &params)
{
  Xpl2JcStatus status = Xpl2JcStatus::fromParams (params);
  qDebug ("%s    EV_STATUS_MSG_JC controller=%d level=%d temp=%.1f cpu=%.1f%%",
          qPrintable (logTag (m_statusSocket)), status.controllerId,
          status.statusLevel, status.temperature, status.cpuPercentageBusy);
  emit statusMessage (
      QStringLiteral ("RX EV_STATUS_MSG_JC: controller=%1 level=%2")
          .arg (status.controllerId)
          .arg (status.statusLevel));
  emit jcStatusReceived (status);
}

void
Xpl2Client::handleEvStatusMsgPh (const QVariantList &params)
{
  Xpl2PhStatus status = Xpl2PhStatus::fromParams (params);
  qDebug ("%s    EV_STATUS_MSG_PH controller=%d level=%d ph=%d temp=%.1f",
          qPrintable (logTag (m_statusSocket)), status.controllerId,
          status.statusLevel, status.printheadId, status.temperature);
  emit statusMessage (
      QStringLiteral ("RX EV_STATUS_MSG_PH: controller=%1 level=%2 ph=%3")
          .arg (status.controllerId)
          .arg (status.statusLevel)
          .arg (status.printheadId));
  emit phStatusReceived (status);
}

/* ------------------------------------------------------------------ */
/*  Internal                                                          */
/* ------------------------------------------------------------------ */

void
Xpl2Client::updateConnectedState ()
{
  bool allConnected
      = m_commandSocket
        && m_commandSocket->state () == QAbstractSocket::ConnectedState
        && m_imagingSocket
        && m_imagingSocket->state () == QAbstractSocket::ConnectedState
        && m_statusSocket
        && m_statusSocket->state () == QAbstractSocket::ConnectedState;
  if (m_connected == allConnected)
    return;
  m_connected = allConnected;
  emit connectedChanged ();
}

QString
Xpl2Client::logTag (const QTcpSocket *socket) const
{
  const char *name = "Unknown";
  quint16 port = 0;
  if (socket == m_commandSocket)
    {
      name = "Command";
      port = m_commandPort;
    }
  else if (socket == m_imagingSocket)
    {
      name = "Imaging";
      port = m_imagingPort;
    }
  else if (socket == m_statusSocket)
    {
      name = "Status";
      port = m_statusPort;
    }

  return QStringLiteral ("[%1:%2]").arg (name).arg (port).leftJustified (15);
}

/* ------------------------------------------------------------------ */
/*  Socket slots                                                      */
/* ------------------------------------------------------------------ */

void
Xpl2Client::onNewConnection ()
{
  auto *server = qobject_cast<QTcpServer *> (sender ());
  QTcpSocket *&slot = socketForServer (server);

  while (auto *pending = server->nextPendingConnection ())
    {
      if (slot)
        {
          qWarning ("%s Rejected extra connection",
                    qPrintable (logTag (slot)));
          pending->deleteLater ();
          continue;
        }
      slot = pending;
      connect (slot, &QTcpSocket::readyRead, this,
               &Xpl2Client::onSocketMessageReady);
      connect (slot, &QTcpSocket::disconnected, this,
               &Xpl2Client::onSocketDisconnected);
      connect (slot, &QAbstractSocket::errorOccurred, this,
               &Xpl2Client::onSocketError);
      qInfo ("%s Controller connected", qPrintable (logTag (slot)));
      updateConnectedState ();
    }
}

void
Xpl2Client::onSocketDisconnected ()
{
  auto *socket = qobject_cast<QTcpSocket *> (sender ());
  qInfo ("%s Controller disconnected", qPrintable (logTag (socket)));

  if (socket == m_commandSocket)
    m_commandSocket = nullptr;
  else if (socket == m_imagingSocket)
    m_imagingSocket = nullptr;
  else if (socket == m_statusSocket)
    m_statusSocket = nullptr;

  socket->deleteLater ();
  updateConnectedState ();
}

void
Xpl2Client::onSocketMessageReady ()
{
  auto *socket = qobject_cast<QTcpSocket *> (sender ());
  while (socket->canReadLine ())
    {
      QByteArray raw = socket->readLine ();
      auto msg = Xpl2Protocol::parseMessage (raw);
      if (msg.valid)
        {
          QByteArray wire;
          if (s_wireDebug)
            wire = " << " + raw.trimmed ();
          qDebug ("%s RX %s%s", qPrintable (logTag (socket)),
                  msg.command.constData (), wire.constData ());

          if (socket == m_commandSocket)
            dispatchCommandMessage (msg);
          else if (socket == m_imagingSocket)
            dispatchImagingMessage (msg);
          else
            dispatchStatusMessage (msg);
        }
    }
}

void
Xpl2Client::onSocketError (QAbstractSocket::SocketError error)
{
  Q_UNUSED (error)
  auto *socket = qobject_cast<QTcpSocket *> (sender ());
  emit errorOccurred (QStringLiteral ("%1 %2")
                          .arg (logTag (socket))
                          .arg (socket->errorString ()));
}