/** * CombatNetRadioHandler - reads CnrChannel and CnrConfig data from the database and publishes the data to dds, * reacts to CnrConfig related Function topics * $Id$ * $HeadURL$ * $Author$ * Created by Hannes Baeuerle, SLSE, 2017-08-15 */ #include "cnr_handler.h" #include "avm_dts.h" #include "dds_handler.h" #include "exercise_context.h" #include "frequency_planning_handler.h" #include "active_exercises.h" #include "av_helper.h" #include "settings.h" #include #include #include #include #include namespace AudioVideoManagement { namespace { bool isRadioStation(const InfrastructureManagement::MainStructureElement &mse) { return mse.structureElementTypeId() == InfrastructureManagement::StructureElementType::RadioStation; } QString hostnameForRadioStation(const InfrastructureManagement::MainStructureElement &mse) { LtsUtils::JsonObject properties = LtsUtils::JsonObject::fromJson(mse.properties()); return properties.valueOr("hostname", QString()); } template dds::core::optional toDdsOptional(const QVariant &variant) { dds::core::optional optional; if (!variant.isNull()) optional = variant.value(); return optional; } CnrChannel toDds(const Database::AvmCnrChannel &channel) { return CnrChannel(channel.id, channel.name.toStdString(), channel.ssrc, channel.addacChannel, channel.pttControl, static_cast(channel.frequencyBand)); } ModifyCnrConfigRequest toRequest(const Database::AvmCnrConfig &dbData, const boost::optional &requestId = boost::none) { return ModifyCnrConfigRequest(requestId.get_value_or(dbData.id), dbData.cnrChannelId, toDdsOptional(dbData.exerciseId), dbData.radioCircuit.toStdString(), dbData.recording, toDdsOptional(dbData.unitId), dbData.partyId, dbData.callSign.toStdString()); } ModifyCnrConfigRequest toRequest(const CnrConfig &cnrConfig) { return ModifyCnrConfigRequest(cnrConfig.id(), cnrConfig.cnrChannelId(), cnrConfig.exerciseId(), cnrConfig.radioCircuit(), cnrConfig.recording(), cnrConfig.unitId(), cnrConfig.partyId(), cnrConfig.callSign()); } Database::AvmCnrConfig toDb(const ModifyCnrConfigRequest &config) { Database::AvmCnrConfig result; result.id = config.id(); result.cnrChannelId = config.cnrChannelId(); result.radioCircuit = QString::fromStdString(config.radioCircuit()); result.callSign = QString::fromStdString(config.callSign()); result.partyId = config.partyId(); if (config.unitId().is_set()) result.unitId = config.unitId().get(); result.recording = config.recording(); if (config.exerciseId().is_set()) result.exerciseId = config.exerciseId().get(); return result; } } // anonymous namespace /** * Internal helper class responsible for inserting, updating and removing device_config entries. */ class CnrHandler::DeviceConfig { public: explicit DeviceConfig(CnrHandler *handler, DatabaseInterface *db, LOGGER_T logger) : m_handler(handler) , m_db(db) , m_logger(logger) { Q_ASSERT(handler); Q_ASSERT(db); } void create(const CnrConfig &config) { Q_ASSERT(!m_cache.contains(config.id())); Database::AvmDeviceConfig deviceConfig = m_handler->toDeviceConfig(config); bool status = m_db->insert(deviceConfig); if (!status) ERROR_LOG(m_logger) << "Error while inserting device config"; m_cache.insert(config.id(), deviceConfig); emit m_handler->avDbReloadNeeded(); } void update(const CnrConfig &config) { Q_ASSERT(m_cache.contains(config.id())); Database::AvmDeviceConfig oldDeviceConfig = m_cache.value(config.id()); Database::AvmDeviceConfig newDeviceConfig = m_handler->toDeviceConfig(config); newDeviceConfig.id = oldDeviceConfig.id; bool status = m_db->update(newDeviceConfig); if (!status) ERROR_LOG(m_logger) << "Error while updating device config"; m_cache[config.id()] = newDeviceConfig; emit m_handler->avDbReloadNeeded(); } void createOrUpdate(const CnrConfig &config) { if (m_cache.contains(config.id())) update(config); else create(config); } void tryRemove(int32_t configId) { if (m_cache.contains(configId)) remove(configId); } void remove(int32_t configId) { Q_ASSERT(m_cache.contains(configId)); bool status = m_db->removeAvmDeviceConfig(m_cache.value(configId).id); if (!status) ERROR_LOG(m_logger) << "Error while removing device config"; m_cache.remove(configId); emit m_handler->avDbReloadNeeded(); } private: CnrHandler *m_handler; DatabaseInterface *m_db; QHash m_cache; //!< cnrConfig-Id -> AvmDeviceConfig LOGGER_T m_logger; }; CnrHandler::CnrHandler(Interface::Dts *dts, Interface::Fts *fts, InfrastructureManagement::Interface::Dtc *ismDtc, FrequencyPlanningHandler *frequencyPlanningHandler, DatabaseInterface *db, ActiveExercises *activeExercises) : ModifyCnrConfigRequestHandler(nullptr) , RemoveCnrConfigRequestHandler(nullptr) , m_dts(dts) , m_fts(fts) , m_frequencyPlanningHandler(frequencyPlanningHandler) , m_db(db) , m_activeExercises(activeExercises) , m_logger(getLogger("CombatNetRadioHandler")) , m_deviceConfig(new DeviceConfig(this, db, m_logger)) { Q_ASSERT(dts); Q_ASSERT(fts); Q_ASSERT(ismDtc); // frequencyPlanningHandler may be null Q_ASSERT(db); Q_ASSERT(activeExercises); m_fts->setModifyCnrConfigRequestHandler(this); m_fts->setRemoveCnrConfigRequestHandler(this); connect(m_activeExercises, &ActiveExercises::contextActivated, this, &CnrHandler::onContextActivated); connect(m_activeExercises, &ActiveExercises::contextDeactivated, this, &CnrHandler::onContextDeactivated); if (m_frequencyPlanningHandler) connect(m_frequencyPlanningHandler, &FrequencyPlanningHandler::planningChanged, this, &CnrHandler::onFrequencyPlanningChanged); connect(m_db, &DatabaseInterface::notifyDeleteAvmCnrConfig, this, &CnrHandler::onDbDeleteAvmCnrConfig); connect(ismDtc, &InfrastructureManagement::Interface::Dtc::mainStructureElementChanged, this, &CnrHandler::onMainStructureElementChanged); // No need to query the DTC here as it is not yet activated. // But explicitly check that the DTC (i.e. the contained DataReader) is not enabled yet. //Q_ASSERT(!ismDtc->isMainStructureElementEnabled()); } CnrHandler::~CnrHandler() { m_fts->setModifyCnrConfigRequestHandler(nullptr); m_fts->setRemoveCnrConfigRequestHandler(nullptr); } bool CnrHandler::init() { using AvmCnrChannel = Database::AvmCnrChannel; using AvmCnrConfig = Database::AvmCnrConfig; m_channels.clear(); if (!m_db->select(m_channels)) { ERROR_LOG(m_logger) << "Error while selecting cnr channels from db"; return false; } for (const AvmCnrChannel ¤tChannel : qAsConst(m_channels)) m_dts->publish(::AudioVideoManagement::toDds(currentChannel)); // Don't use select with callback here as addConfig() uses the DB, too. QVector cnrConfigs; if (!m_db->select(cnrConfigs)) { ERROR_LOG(m_logger) << "Error while selecting CNR configs from db"; return false; } for (const AvmCnrConfig ¤tConfig : qAsConst(cnrConfigs)) { auto reply = createCnrConfig(toRequest(currentConfig, 0), currentConfig.id); if (reply.error()) { ERROR_LOG((m_logger)) << QStringLiteral("Invalid CnrConfig in DB with id %1 => removing; reason:").arg(currentConfig.id) << reply(); m_db->removeAvmCnrConfig(currentConfig.id); } } return true; } bool CnrHandler::hasConfig(int32_t configId) const { return m_cache.contains(configId); } const CnrConfig &CnrHandler::config(int32_t configId) const { auto found = m_cache.constFind(configId); Q_ASSERT(found != m_cache.constEnd()); return *found; } void CnrHandler::onRemoveCnrConfigRequest(const rti::core::SampleIdentity &cookie, const Core::RemoveElementRequest &request) { auto ret = removeCnrConfig(request); if (ret.error()) WARNING_LOG(m_logger) << ret(); m_fts->removeCnrConfigReply(cookie, ret); } void CnrHandler::onModifyCnrConfigRequest(const rti::core::SampleIdentity &cookie, const ModifyCnrConfigRequest &request) { auto ret = (request.id() < 1 ? createCnrConfig(request) : modifyCnrConfig(request)); if (ret.error()) WARNING_LOG(m_logger) << ret(); m_fts->modifyCnrConfigReply(cookie, ret); } void CnrHandler::onRadioStationAdded(int id, const QString &name, const QString &hostname) { DEBUG_LOG(m_logger) << "Adding radio station with id " << id << " (" << name << ") and hostname '" << hostname << "'"; auto it = m_cnrRadioStations.find(id); if (it != m_cnrRadioStations.end() && it.value() == hostname) return; if (hostname.isEmpty()) { WARNING_LOG(m_logger) << "Radio station with id " << id << " (" << name << ") has an empty hostname - ignoring!"; return; } m_cnrRadioStations[id] = hostname; updateDeviceConfig(); } void CnrHandler::onRadioStationRemoved(int id) { DEBUG_LOG(m_logger) << "Removing radio station with id " << id; auto it = m_cnrRadioStations.find(id); if (it == m_cnrRadioStations.end()) return; m_cnrRadioStations.erase(it); updateDeviceConfig(); } void CnrHandler::onContextActivated(const ExerciseContext &context) { Q_UNUSED(context); reevalutate(); } void CnrHandler::onContextDeactivated(const ExerciseContext &context) { Q_UNUSED(context); reevalutate(); } void CnrHandler::onFrequencyPlanningChanged() { reevalutate(); } /** * Will be called by the DB after an AvmCnrConfig entry was removed. * This can happen when * - we have just removed the entry ourselves * - it was implicitly removed by the DB as a result of a cascading delete * - someone removed the entry by hand */ void CnrHandler::onDbDeleteAvmCnrConfig(const QVariant &id) { bool couldConvert = false; int configId = id.toInt(&couldConvert); Q_ASSERT(couldConvert); Q_ASSERT(configId > 0); if (!m_cache.contains(configId)) return; // the notification was a result of our own removal of the entry DEBUG_LOG(m_logger) << "DB deleted AvmCnrConfig with id: " << configId; removeCnrConfig(Core::RemoveElementRequest(configId)); } void CnrHandler::onMainStructureElementChanged(const InfrastructureManagement::MainStructureElement &data, DataModification modType) { if (isRadioStation(data)) onRadioStationChanged(data, modType); } void CnrHandler::onRadioStationChanged(const InfrastructureManagement::MainStructureElement &data, DataModification modType) { switch (modType) { case DataModification::eAdded: case DataModification::eModified: onRadioStationAdded(data.id(), QString::fromStdString(data.name()), hostnameForRadioStation(data)); break; case DataModification::eRemoved: onRadioStationRemoved(data.id()); break; } } /** * Reevalutate which CnrConfigs are active. */ void CnrHandler::reevalutate() { DBTransaction trans(m_db->database()); // Work on a copy here as the loop might modify m_cache. const QList cacheCopy = m_cache.values(); for (const CnrConfig &cnrConfig : cacheCopy) { auto reply = modifyCnrConfig(toRequest(cnrConfig)); if (reply.error()) ERROR_LOG(m_logger) << reply(); } trans.commit(); } /** * Update the DeviceConfig-entries for the currently *active* CnrConfigs. */ void CnrHandler::updateDeviceConfig() { DBTransaction trans(m_db->database()); for (const auto &config : qAsConst(m_cache)) if (config.isActive()) m_deviceConfig->update(config); trans.commit(); } CnrConfig CnrHandler::toDds(const Database::AvmCnrConfig &config) const { CnrConfig cnrConfig(config.id, config.cnrChannelId, toDdsOptional(config.exerciseId), config.radioCircuit.toStdString(), config.partyId, toDdsOptional(config.unitId), config.recording, config.callSign.toStdString(), false); if (needDeviceConfig(cnrConfig)) cnrConfig.isActive(true); return cnrConfig; } Database::AvmDeviceConfig CnrHandler::toDeviceConfig(const CnrConfig &cnrConfig) const { static const QString recordingServer = QStringLiteral("EXCAVS01"); Database::AvmDeviceConfig deviceConfig; const Database::AvmCnrChannel channel = channelById(cnrConfig.cnrChannelId()); deviceConfig.id = 0; deviceConfig.context = exerciseList(cnrConfig); deviceConfig.type = channel.addacChannel == 0 ? QStringLiteral("CNR_GUEZ_RT_CHANNEL") : QStringLiteral("ADDAC_RT_CHANNEL"); deviceConfig.name = QString::fromStdString(cnrConfig.radioCircuit()); deviceConfig.controller = stationList(channel.addacChannel); deviceConfig.definition = QStringLiteral("mode=1;ssrc=%1").arg(channel.ssrc); //for UAE we need pttc controll to! #ifndef GUEZ_MODE if (channel.addacChannel) deviceConfig.definition += QStringLiteral(";pttcontrol=true"); #endif if (channel.addacChannel) deviceConfig.definition += QStringLiteral(";channel=%1").arg(normalizedAddacChannel(channel.addacChannel)); if (cnrConfig.recording()) deviceConfig.definition.append(QStringLiteral(";recording=ECRS@%1").arg(recordingServer)); deviceConfig.link = 0; return deviceConfig; } /** * request can be either a create- or a modify-requst. */ Core::FunctionReplyObject CnrHandler::validateRequest(const ModifyCnrConfigRequest &request) { Core::FunctionReplyObject reply; if (request.id() > 0) { reply.setId(request.id()); if (!m_cache.contains(request.id())) return reply.setStr(tr_lts("dds", "Could not modify CNR config because id %1 is unknown")).arg(request.id()); } if (!isValidChannel(request.cnrChannelId())) return reply.setStr(tr_lts("dds", "Could not create/modify CNR config because the channel with id %1 is invalid")).arg(request.cnrChannelId()); if (!isStreamNameValid(request.radioCircuit())) return reply.setStr(tr_lts("dds", "Could not create/modify CNR config because the circuit name is invalid")); if (!isNameUnique(request)) return reply.setStr(tr_lts("dds", "Could not create/modify CNR config because the circuit name is already in use")); return reply; } /** * Helper function for isFreeChannel() and isNameUnique(). */ bool CnrHandler::checkConstraint(const std::function &constraint, const boost::optional &exerciseId, const boost::optional &excludeId) const { for (const CnrConfig &config : qAsConst(m_cache)) { if (excludeId == config.id()) continue; // If both configs are local the constraint must hold if they are in the same exercise // or when both exercises are currently active. // If one is global and one local the constraint must always hold if the local exercise is active // Otherwise the constraint must always hold bool bothLocal = config.exerciseId().is_set() && exerciseId.is_initialized(); if (bothLocal) { bool sameExercise = config.exerciseId().get() == *exerciseId; bool bothActive = isActiveExercise(config.exerciseId().get()) && isActiveExercise(*exerciseId); if (!sameExercise && !bothActive) continue; } else if(config.exerciseId().is_set() && !exerciseId.is_initialized()) { if(!isActiveExercise(config.exerciseId().get())) continue; } else if(exerciseId.is_initialized() && !config.exerciseId().is_set()) { if(!isActiveExercise(*exerciseId)) continue; } if (!constraint(config)) return false; } return true; } bool CnrHandler::isValidChannel(int64_t channelId) const { auto withId = [channelId] (const Database::AvmCnrChannel &channel) { return channel.id == channelId; }; auto found = std::find_if(m_channels.cbegin(), m_channels.cend(), withId); return found != m_channels.cend(); } bool CnrHandler::isFreeChannel(const ModifyCnrConfigRequest &request) const { boost::optional exerciseId = toBoost(request.exerciseId()); if (request.id() <= 0) return isFreeChannel(request.cnrChannelId(), exerciseId); return isFreeChannel(request.cnrChannelId(), exerciseId, request.id()); } bool CnrHandler::isFreeChannel(int32_t channelId, const boost::optional &exerciseId, const boost::optional &excludeId) const { auto constraint = [&channelId] (const CnrConfig &config) { return config.cnrChannelId() != channelId; }; return checkConstraint(constraint, exerciseId, excludeId); } bool CnrHandler::isNameUnique(const ModifyCnrConfigRequest &request) { boost::optional exerciseId = toBoost(request.exerciseId()); if (request.id() <= 0) return isNameUnique(request.radioCircuit(), exerciseId); return isNameUnique(request.radioCircuit(), exerciseId, request.id()); } bool CnrHandler::isNameUnique(const std::string &name, const boost::optional &exerciseId, const boost::optional &excludeId) const { auto constraint = [&name] (const CnrConfig &config) { return config.radioCircuit() != name; }; return checkConstraint(constraint, exerciseId, excludeId); } /** * @param dbId Is set when called from init(). */ Core::FunctionReplyObject CnrHandler::createCnrConfig(const ModifyCnrConfigRequest &request, const boost::optional &dbId) { DEBUG_LOG(m_logger) << "CreateCnrConfigRequest: " << request; auto reply = validateRequest(request); if (reply.error()) return reply; Database::AvmCnrConfig dbConfigData = toDb(request); if (dbId) dbConfigData.id = *dbId; else { if (!m_db->insert(dbConfigData)) return Core::FunctionReplyObject(tr_lts("dds", "Could not insert new CNR config data to database")); } CnrConfig cnrConfig = toDds(dbConfigData); m_cache.insert(cnrConfig.id(), cnrConfig); m_dts->publish(cnrConfig); if (cnrConfig.isActive()) m_deviceConfig->create(cnrConfig); return Core::FunctionReplyObject(dbConfigData.id); } Core::FunctionReplyObject CnrHandler::modifyCnrConfig(const ModifyCnrConfigRequest &request) { DEBUG_LOG(m_logger) << "ModifyCnrConfigRequest: " << request; auto reply = validateRequest(request); if (reply.error()) return reply; auto found = m_cache.find(request.id()); Q_ASSERT(found != m_cache.end()); const CnrConfig &oldCnrConfig = *found; // We do not allow changing the CnrChannel as this could cause problems when the CnrConfig is // already referenced by a FrequencyPlan. Changing the CnrChannel is not possible through the HMI. // Hint: In a FrequencyPlanning a CnrConfig has to be assigned to an entry from the FrequencySet. // But not every entry from the FrequencySet can be assigned, e.g. a CnrConfig of (channel-)type VHF cannot // be assigned to a FrequencySet-entry of type HF. // Thus changing the CnrChannel would possibly invalidate those assignments in the FrequencyPlanning. if (request.cnrChannelId() != oldCnrConfig.cnrChannelId()) return reply.setStr(tr_lts("dds", "Changing the CNR channel is not allowed")); Database::AvmCnrConfig dbConfigData = toDb(request); CnrConfig newCnrConfig = toDds(dbConfigData); if (newCnrConfig.isActive()) m_deviceConfig->createOrUpdate(newCnrConfig); else m_deviceConfig->tryRemove(newCnrConfig.id()); if (newCnrConfig == oldCnrConfig) return reply; if (!m_db->update(dbConfigData)) return reply.setStr(tr_lts("dds", "Error updating CNR config data with id %1 in database")).arg(request.id()); m_cache[newCnrConfig.id()] = newCnrConfig; m_dts->publish(newCnrConfig); emit cnrConfigChanged(newCnrConfig); return reply; } Core::FunctionReplyObject CnrHandler::removeCnrConfig(const Core::RemoveElementRequest &request) { DEBUG_LOG(m_logger) << "RemoveCnrConfigRequest: " << request; int32_t configId = request.id(); Core::FunctionReplyObject reply(configId); auto found = m_cache.find(configId); if (found == m_cache.end()) return reply.setStr(tr_lts("dds", "Could not remove CNR config because id %1 is unknown")).arg(configId); m_cache.erase(found); m_deviceConfig->tryRemove(configId); m_dts->removeCnrConfigByKey(configId); if (!m_db->removeAvmCnrConfig(configId)) return reply.setStr(tr_lts("dds", "Could not remove CNR config data with id %1")).arg(request.id()); emit cnrConfigRemoved(configId); return reply; } bool CnrHandler::isActiveExercise(int exerciseId) const { return m_activeExercises->isActive(ExerciseContext(exerciseId)); } bool CnrHandler::needDeviceConfig(const CnrConfig &config) const { // For global, we always need the config if (!config.exerciseId().is_set()) return true; int32_t exerciseId = config.exerciseId().get(); // Not for inactive exercises if (!isActiveExercise(exerciseId)) return false; // Not for exercises without planning if (!haveActiveFrequencyPlanning()) return false; // Config needs to be referenced. (Wonder if its possible to have a non-referenced one...) if (m_frequencyPlanningHandler->isReferencedByActivePlan(exerciseId, config.id())) return true; return false; } /** * Check whether any of the active exercises has an active FrequencyPlanning. */ bool CnrHandler::haveActiveFrequencyPlanning() const { if (!m_frequencyPlanningHandler) return false; for (const auto &context : m_activeExercises->allContexts()) if (m_frequencyPlanningHandler->hasActivePlan(context.id())) return true; return false; } /** * @return The list of exercises for the given config. The returned string * is used in the definition of the device_config. */ QString CnrHandler::exerciseList(const CnrConfig &config) const { // Local if (config.exerciseId().is_set()) return ExerciseContext(config.exerciseId().get()).toString(); // Global - all active exercises, no matter if they have active plannings or not QStringList exerciseList; for (const auto &context : qAsConst(m_activeExercises->allContexts())) exerciseList.append(context.toString()); return exerciseList.join(QLatin1Char(',')); } /** * @return The list of RadioStations for the given config. The returned string * is used in the controller-column of the device_config. */ QString CnrHandler::stationList(int addacChannel) const { if (addacChannel) { const int pps = Settings::addacPortsPerServer(); const int serverNum = ((addacChannel - 1) / pps) + 1; const QString hostname = Settings::addacServerNamePrefix() + QString::asprintf(("%02d"), serverNum); return hostname; } if (m_cnrRadioStations.isEmpty()) return QStringLiteral("EXCAVS01"); // fallback when there are no dedicated radio stations const QStringList radioStations = m_cnrRadioStations.values(); return radioStations.join(QLatin1Char(',')); } int CnrHandler::normalizedAddacChannel(int addacChannel) const { if (!addacChannel) return addacChannel; const int pps = Settings::addacPortsPerServer(); const int ret = (addacChannel % pps); return ret == 0 ? pps : ret; } Database::AvmCnrChannel CnrHandler::channelById(int32_t id) const { for (const Database::AvmCnrChannel &channel : qAsConst(m_channels)) if (channel.id == id) return channel; Q_ASSERT(false); return Database::AvmCnrChannel(); } } // namespace AudioVideoManagement