/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2020 Jon Beniston, M7RCE // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // // the Free Software Foundation as version 3 of the License, or // // (at your option) any later version. // // // // This program is distributed in the hope that it will be useful, // // but WITHOUT ANY WARRANTY; without even the implied warranty of // // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // // GNU General Public License V3 for more details. // // // // You should have received a copy of the GNU General Public License // // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// #include "rigctrl.h" #include #include #include // Length of buffers const unsigned int RigCtrl::m_CmdLength = 1024; const unsigned int RigCtrl::m_UrlLength = 1024; const unsigned int RigCtrl::m_ResponseLength = 1024; const unsigned int RigCtrl::m_DataLength = 1024; // Hamlib rigctrl error codes enum rig_errcode_e { RIG_OK = 0, /*!< No error, operation completed successfully */ RIG_EINVAL = -1, /*!< invalid parameter */ RIG_ECONF = -2, /*!< invalid configuration (serial,..) */ RIG_ENOMEM = -3, /*!< memory shortage */ RIG_ENIMPL = -4, /*!< function not implemented, but will be */ RIG_ETIMEOUT = -5, /*!< communication timed out */ RIG_EIO = -6, /*!< IO error, including open failed */ RIG_EINTERNAL = -7, /*!< Internal Hamlib error, huh! */ RIG_EPROTO = -8, /*!< Protocol error */ RIG_ERJCTED = -9, /*!< Command rejected by the rig */ RIG_ETRUNC = -10, /*!< Command performed, but arg truncated */ RIG_ENAVAIL = -11, /*!< function not available */ RIG_ENTARGET = -12, /*!< VFO not targetable */ RIG_BUSERROR = -13, /*!< Error talking on the bus */ RIG_BUSBUSY = -14, /*!< Collision on the bus */ RIG_EARG = -15, /*!< NULL RIG handle or any invalid pointer parameter in get arg */ RIG_EVFO = -16, /*!< Invalid VFO */ RIG_EDOM = -17 /*!< Argument out of domain of func */ }; // Map rigctrl mode names to SDRangel modem names const static struct mode_demod { const char *mode; const char *modem; } mode_map[] = { {"FM", "NFMDemod"}, {"WFM", "WFMDemod"}, {"AM", "AMDemod"}, {"LSB", "SSBDemod"}, {"USB", "SSBDemod"}, {nullptr, nullptr} }; // Get double value from within nested JSON object static double getSubObjectDouble(QJsonObject &json, const QString &key) { double value = -1.0; for (QJsonObject::const_iterator it = json.begin(); it != json.end(); it++) { QJsonValue jsonValue = it.value(); if (jsonValue.isObject()) { QJsonObject settings = jsonValue.toObject(); if (settings.contains(key)) { value = settings[key].toDouble(); return value; } } } return value; } // Set double value withing nested JSON object static bool setSubObjectDouble(QJsonObject &json, const QString &key, double value) { for (QJsonObject::iterator it = json.begin(); it != json.end(); it++) { QJsonValue jsonValue = it.value(); if (jsonValue.isObject()) { QJsonObject settings = jsonValue.toObject(); if (settings.contains(key)) { settings[key] = value; it.value() = settings; return true; } } } return false; } RigCtrl::RigCtrl() : m_settings(), m_state(idle), m_tcpServer(nullptr), m_clientConnection(nullptr) { m_netman = new QNetworkAccessManager(this); connect(m_netman, &QNetworkAccessManager::finished, this, &RigCtrl::processAPIResponse); setSettings(&m_settings); } RigCtrl::~RigCtrl() { if (m_clientConnection != nullptr) { m_clientConnection->close(); delete m_clientConnection; } if (m_tcpServer != nullptr) { m_tcpServer->close(); delete m_tcpServer; } delete m_netman; } void RigCtrl::getSettings(RigCtrlSettings *settings) { *settings = m_settings; } // Is there a potential for this to be called while in getCommand or processAPIResponse? void RigCtrl::setSettings(RigCtrlSettings *settings) { bool disabled = (!settings->m_enabled) && m_settings.m_enabled; bool enabled = settings->m_enabled && !m_settings.m_enabled; bool portChanged = settings->m_rigCtrlPort != m_settings.m_rigCtrlPort; if (disabled || portChanged) { if (m_clientConnection != nullptr) { m_clientConnection->close(); delete m_clientConnection; m_clientConnection = nullptr; } if (m_tcpServer != nullptr) { m_tcpServer->close(); delete m_tcpServer; m_tcpServer = nullptr; } } if (enabled || portChanged) { qDebug() << "RigCtrl enabled on port " << settings->m_rigCtrlPort; m_tcpServer = new QTcpServer(this); if(!m_tcpServer->listen(QHostAddress::Any, settings->m_rigCtrlPort)) { qDebug() << "RigCtrl failed to listen on port " << settings->m_rigCtrlPort << ". Check it is not already in use."; } else { connect(m_tcpServer, &QTcpServer::newConnection, this, &RigCtrl::acceptConnection); } } m_settings = *settings; } QByteArray RigCtrl::serialize() const { return m_settings.serialize(); } bool RigCtrl::deserialize(const QByteArray& data) { bool success = true; RigCtrlSettings settings; if (!settings.deserialize(data)) { success = false; } else { setSettings(&settings); } return success; } // Accept connection from rigctrl client void RigCtrl::acceptConnection() { m_clientConnection = m_tcpServer->nextPendingConnection(); if (!m_clientConnection) { return; } connect(m_clientConnection, &QIODevice::readyRead, this, &RigCtrl::getCommand); connect(m_clientConnection, &QAbstractSocket::disconnected, m_clientConnection, &QObject::deleteLater); } // Get rigctrl command and start processing it void RigCtrl::getCommand() { char cmd[m_CmdLength]; char url[m_UrlLength]; char response[m_ResponseLength]; qint64 len; QNetworkRequest request; char *p; int i, l; // Get rigctrld command from client len = m_clientConnection->readLine(cmd, sizeof(cmd)); if (len != -1) { //qDebug() << "RigCtrl::getCommand - " << cmd; if (!strncmp(cmd, "F ", 2) || !strncmp(cmd, "set_freq ", 9)) { // Set frequency m_targetFrequency = atof(cmd[0] == 'F' ? &cmd[2] : &cmd[9]); // Get current centre frequency sprintf(url, "%s/deviceset/%d/device/settings", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex); request.setUrl(QUrl(url)); m_netman->get(request); m_state = set_freq; } else if (!strncmp(cmd, "f", 1) || !strncmp(cmd, "get_freq", 8)) { // Get frequency - need to add centerFrequency and inputFrequencyOffset sprintf(url, "%s/deviceset/%d/device/settings", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex); request.setUrl(QUrl(url)); m_netman->get(request); m_state = get_freq_center; } else if (!strncmp(cmd, "M ?", 3) || !(strncmp(cmd, "set_mode ?", 10))) { // Return list of modes supported p = response; for (i = 0; mode_map[i].mode != nullptr; i++) { p += sprintf(p, "%s ", mode_map[i].mode); } p += sprintf(p, "\n"); m_clientConnection->write(response, strlen(response)); } else if (!strncmp(cmd, "M ", 2) || !(strncmp(cmd, "set_mode ", 9))) { // Set mode // Map rigctrl mode name to SDRangel modem name m_targetModem = nullptr; m_targetBW = -1; p = cmd[0] == 'M' ? &cmd[2] : &cmd[9]; for (i = 0; mode_map[i].mode != nullptr; i++) { l = strlen(mode_map[i].mode); if (!strncmp(p, mode_map[i].mode, l)) { m_targetModem = mode_map[i].modem; p += l; break; } } // Save bandwidth, if given while(isspace(*p)) { p++; } if (*p == ',') { p++; m_targetBW = atoi(p); } if (mode_map[i].modem != nullptr) { // Delete current modem sprintf(url, "%s/deviceset/%d/channel/%d", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex, m_settings.m_channelIndex); request.setUrl(QUrl(url)); m_netman->sendCustomRequest(request, "DELETE"); m_state = set_mode_mod; } else { sprintf(response, "RPRT %d\n", RIG_EINVAL); m_clientConnection->write(response, strlen(response)); } } else if (!strncmp(cmd, "set_powerstat 0", 15)) { // Power off radio sprintf(url, "%s/deviceset/%d/device/run", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex); request.setUrl(QUrl(url)); m_netman->sendCustomRequest(request, "DELETE"); m_state = set_power_off; } else if (!strncmp(cmd, "set_powerstat 1", 15)) { // Power on radio sprintf(url, "%s/deviceset/%d/device/run", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex); request.setUrl(QUrl(url)); m_netman->post(request, ""); m_state = set_power_on; } else if (!strncmp(cmd, "get_powerstat", 13)) { // Return if powered on or off sprintf(url, "%s/deviceset/%d/device/run", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex); request.setUrl(QUrl(url)); m_netman->get(request); m_state = get_power; } else { // Unimplemented command sprintf(response, "RPRT %d\n", RIG_ENIMPL); m_clientConnection->write(response, strlen(response)); m_state = idle; } } } // Process reply from SDRangel API server void RigCtrl::processAPIResponse(QNetworkReply *reply) { double freq; char response[m_ResponseLength]; char url[m_UrlLength]; char data[m_DataLength]; QNetworkRequest request; if (reply->error() == QNetworkReply::NoError) { QString answer = reply->readAll(); //qDebug("RigCtrl::processAPIResponse - '%s'", qUtf8Printable(answer)); QJsonDocument jsonResponse = QJsonDocument::fromJson(answer.toUtf8()); QJsonObject jsonObj = jsonResponse.object(); switch (m_state) { case get_freq_center: // Reply with current center frequency freq = getSubObjectDouble(jsonObj, "centerFrequency"); if (freq >= 0.0) { m_targetFrequency = freq; sprintf(url, "%s/deviceset/%d/channel/%d/settings", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex, m_settings.m_channelIndex); request.setUrl(QUrl(url)); m_netman->get(request); } else { sprintf(response, "RPRT %d\n", RIG_ENIMPL); // File source doesn't have centre frequency } m_state = get_freq_offset; break; case get_freq_offset: freq = m_targetFrequency + getSubObjectDouble(jsonObj, "inputFrequencyOffset"); sprintf(response, "%u\n", (unsigned)freq); m_clientConnection->write(response, strlen(response)); m_state = idle; break; case set_freq: // Check if target requency is within max offset from current center frequency freq = getSubObjectDouble(jsonObj, "centerFrequency"); if (fabs(freq - m_targetFrequency) > m_settings.m_maxFrequencyOffset) { // Update centerFrequency setSubObjectDouble(jsonObj, "centerFrequency", m_targetFrequency); sprintf(url, "%s/deviceset/%d/device/settings", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex); request.setUrl(QUrl(url)); m_netman->sendCustomRequest(request, "PATCH", QJsonDocument(jsonObj).toJson()); m_state = set_freq_center; } else { // In range, so update inputFrequencyOffset m_targetOffset = m_targetFrequency - freq; // Get settings containg inputFrequencyOffset, so we can patch them sprintf(url, "%s/deviceset/%d/channel/%d/settings", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex, m_settings.m_channelIndex); request.setUrl(QUrl(url)); m_netman->get(request); m_state = set_freq_set_offset; } break; case set_freq_no_offset: // Update centerFrequency, without trying to set offset setSubObjectDouble(jsonObj, "centerFrequency", m_targetFrequency); sprintf(url, "%s/deviceset/%d/device/settings", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex); request.setUrl(QUrl(url)); m_netman->sendCustomRequest(request, "PATCH", QJsonDocument(jsonObj).toJson()); m_state = set_freq_center_no_offset; break; case set_freq_center: // Check whether frequency was set as expected freq = getSubObjectDouble(jsonObj, "centerFrequency"); if (freq == m_targetFrequency) { // Set inputFrequencyOffset to 0 m_targetOffset = 0; sprintf(url, "%s/deviceset/%d/channel/%d/settings", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex, m_settings.m_channelIndex); request.setUrl(QUrl(url)); m_netman->get(request); m_state = set_freq_set_offset; } else { sprintf(response, "RPRT %d\n", RIG_EINVAL); m_clientConnection->write(response, strlen(response)); m_state = idle; } break; case set_freq_center_no_offset: // Check whether frequency was set as expected freq = getSubObjectDouble(jsonObj, "centerFrequency"); if (freq == m_targetFrequency) { sprintf(response, "RPRT 0\n"); } else { sprintf(response, "RPRT %d\n", RIG_EINVAL); } m_clientConnection->write(response, strlen(response)); m_state = idle; break; case set_freq_set_offset: // Patch inputFrequencyOffset if (setSubObjectDouble(jsonObj, "inputFrequencyOffset", m_targetOffset)) { sprintf(url, "%s/deviceset/%d/channel/%d/settings", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex, m_settings.m_channelIndex); request.setUrl(QUrl(url)); m_netman->sendCustomRequest(request, "PATCH", QJsonDocument(jsonObj).toJson()); m_state = set_freq_offset; } else { // No inputFrequencyOffset sprintf(response, "RPRT %d\n", RIG_EINVAL); m_clientConnection->write(response, strlen(response)); m_state = idle; } break; case set_freq_offset: freq = getSubObjectDouble(jsonObj, "inputFrequencyOffset"); if (freq == m_targetOffset) { sprintf(response, "RPRT 0\n"); } else { sprintf(response, "RPRT %d\n", RIG_EINVAL); } m_clientConnection->write(response, strlen(response)); m_state = idle; break; case set_mode_mod: // Create new modem sprintf(url, "%s/deviceset/%d/channel", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex); sprintf(data, "{ \"channelType\": \"%s\", \"direction\": 0, \"originatorDeviceSetIndex\": %d}\n", m_targetModem, m_settings.m_deviceIndex); request.setUrl(QUrl(url)); request.setHeader(QNetworkRequest::ContentTypeHeader,"application/json"); m_netman->post(request, data); if (m_targetBW >= 0) { m_state = set_mode_settings; } else { m_state = set_mode_reply; } break; case set_mode_settings: // Set modem bandwidth sprintf(url, "%s/deviceset/%d/channel/%d/settings", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex, m_settings.m_channelIndex); sprintf(data, "{ \"channelType\": \"%s\", \"%sSettings\": {\"rfBandwidth\":%d}}\n", m_targetModem, m_targetModem, m_targetBW); request.setUrl(QUrl(url)); request.setHeader(QNetworkRequest::ContentTypeHeader,"application/json"); m_netman->sendCustomRequest(request, "PATCH", data); m_state = set_mode_reply; break; case set_mode_reply: sprintf(response, "RPRT 0\n"); m_clientConnection->write(response, strlen(response)); m_state = idle; break; case get_power: if (!jsonObj["state"].toString().compare("running")) { sprintf(response, "1\n"); } else { sprintf(response, "0\n"); } m_clientConnection->write(response, strlen(response)); m_state = idle; break; case set_power_on: case set_power_off: // Reply contains previous state, not current state, so assume it worked sprintf(response, "RPRT 0\n"); m_clientConnection->write(response, strlen(response)); m_state = idle; break; } } else { //qDebug("RigCtrl::processAPIResponse - got error: '%s'", qUtf8Printable(reply->errorString())); //qDebug() << reply->readAll(); switch (m_state) { case get_freq_offset: // Probably no modem enabled on the specified channel sprintf(response, "%u\n", (unsigned)m_targetFrequency); m_clientConnection->write(response, strlen(response)); m_state = idle; break; case set_freq_set_offset: // Probably no demodulator enabled on the specified channel // Just set as center frequency sprintf(url, "%s/deviceset/%d/device/settings", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex); request.setUrl(QUrl(url)); m_netman->get(request); m_state = set_freq_no_offset; break; case set_mode_mod: // Probably no modem on channel to delete, so continue to try to create one sprintf(url, "%s/deviceset/%d/channel", qUtf8Printable(m_APIBaseURI), m_settings.m_deviceIndex); sprintf(data, "{ \"channelType\": \"%s\", \"direction\": 0, \"originatorDeviceSetIndex\": %d}\n", m_targetModem, m_settings.m_deviceIndex); request.setUrl(QUrl(url)); request.setHeader(QNetworkRequest::ContentTypeHeader,"application/json"); m_netman->post(request, data); if (m_targetBW >= 0) { m_state = set_mode_settings; } else { m_state = set_mode_reply; } break; default: sprintf(response, "RPRT %d\n", RIG_EIO); m_clientConnection->write(response, strlen(response)); break; } } reply->deleteLater(); }