/** * TrainingVoiceCommunicationHandler - reads TetraChannel and TvcConfig data from the database and publishes the data to dds, * reacts to TvcConfig related Function topics * $Id$ * $HeadURL$ * $Author$ * Created by Hannes Baeuerle, SLSE, 2017-07-25 */ #include "tvc_handler.h" #include "dds_handler.h" #include "exercise_context.h" #include "active_exercises.h" #include "av_helper.h" #include #include #include namespace AudioVideoManagement { namespace { TvcChannel toDds(const Database::AvmTvcChannel &dbData) { return TvcChannel(dbData.id, dbData.name.toStdString(), dbData.settings.toStdString()); } TvcConfig toDds(const Database::AvmTvcConfig &dbData) { dds::core::optional exerciseId; if (!dbData.exerciseId.isNull()) exerciseId = dbData.exerciseId.value(); return TvcConfig(dbData.id, dbData.tetraChannelId, dbData.radioCircuit.toStdString(), exerciseId, dbData.recording, dbData.info.toStdString()); } ModifyTvcConfigRequest toRequest(const Database::AvmTvcConfig &dbData, const boost::optional &requestId = boost::none) { dds::core::optional exerciseId; if (!dbData.exerciseId.isNull()) exerciseId = dbData.exerciseId.value(); return ModifyTvcConfigRequest(requestId.get_value_or(dbData.id), dbData.tetraChannelId, dbData.radioCircuit.toStdString(), dbData.recording, exerciseId, dbData.info.toStdString()); } Database::AvmTvcConfig toDb(const ModifyTvcConfigRequest &request) { Database::AvmTvcConfig result; result.id = request.id(); result.radioCircuit = QString::fromStdString(request.radioCircuit()); result.info = QString::fromStdString(request.info()); result.recording = request.recording(); result.tetraChannelId = request.tetraChannelId(); if (request.exerciseId().is_set()) result.exerciseId = request.exerciseId().get(); return result; } } // anonymous namespace /** * Internal helper class responsible for inserting, updating and removing device_config entries. */ class TvcHandler::DeviceConfig { public: explicit DeviceConfig(TvcHandler *handler, DatabaseInterface *db, LOGGER_T logger) : m_handler(handler) , m_db(db) , m_logger(logger) { Q_ASSERT(handler); Q_ASSERT(db); } void create(const TvcConfig &config) { Q_ASSERT(!m_cache.contains(config.id())); Database::AvmDeviceConfig deviceConfig = toDeviceConfig(config); if (!m_db->insert(deviceConfig)) ERROR_LOG(m_logger) << "Error while inserting device config into database"; m_cache.insert(config.id(), deviceConfig); emit m_handler->avDbReloadNeeded(); } void update(const TvcConfig &config) { Q_ASSERT(m_cache.contains(config.id())); Database::AvmDeviceConfig deviceConfig = toDeviceConfig(config); const Database::AvmDeviceConfig &oldDeviceConfig = m_cache[config.id()]; deviceConfig.id = oldDeviceConfig.id; if (!m_db->update(deviceConfig)) ERROR_LOG(m_logger) << "Error while updating device config"; m_cache[config.id()] = deviceConfig; emit m_handler->avDbReloadNeeded(); } void createOrUpdate(const TvcConfig &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) { auto found = m_cache.find(configId); Q_ASSERT(found != m_cache.end()); if (!m_db->removeAvmDeviceConfig(found->id)) ERROR_LOG(m_logger) << "Error while removing device config"; m_cache.erase(found); emit m_handler->avDbReloadNeeded(); } private: Database::AvmDeviceConfig toDeviceConfig(const TvcConfig &tvcConfig) const { return m_handler->toDeviceConfig(tvcConfig); } TvcHandler *m_handler; DatabaseInterface *m_db; QMap m_cache; // LOGGER_T m_logger; }; TvcHandler::TvcHandler(Interface::Dts *dts, Interface::Fts *fts, DatabaseInterface *db, ActiveExercises *activeExercises) : ModifyTvcConfigRequestHandler(nullptr) , RemoveTvcConfigRequestHandler(nullptr) , m_dts(dts) , m_fts(fts) , m_db(db) , m_activeExercises(activeExercises) , m_logger(getLogger("TrainingVoiceCommunicationHandler")) , m_deviceConfig(new DeviceConfig(this, m_db, m_logger)) { Q_ASSERT(dts); Q_ASSERT(fts); Q_ASSERT(db); Q_ASSERT(activeExercises); m_fts->setModifyTvcConfigRequestHandler(this); m_fts->setRemoveTvcConfigRequestHandler(this); connect(m_activeExercises, &ActiveExercises::contextActivated, this, &TvcHandler::onContextActivated); connect(m_activeExercises, &ActiveExercises::contextDeactivated, this, &TvcHandler::onContextDeactivated); connect(m_db, &DatabaseInterface::notifyDeleteAvmTvcConfig, this, &TvcHandler::onDbDeleteAvmTvcConfig); } TvcHandler::~TvcHandler() { m_fts->setModifyTvcConfigRequestHandler(nullptr); m_fts->setRemoveTvcConfigRequestHandler(nullptr); } bool TvcHandler::init() { using Database::AvmTvcChannel; using Database::AvmTvcConfig; m_channels.clear(); if (!m_db->select(m_channels)) { ERROR_LOG(m_logger) << "Error while selecting tetra channels from db"; return false; } for (const AvmTvcChannel ¤tChannel : qAsConst(m_channels)) m_dts->publish(toDds(currentChannel)); QVector tvcConfigs; if (!m_db->select(tvcConfigs)) { ERROR_LOG(m_logger) << "Error while selecting TVC configs from db"; return false; } for (const AvmTvcConfig ¤tConfig : qAsConst(tvcConfigs)) createTvcConfig(toRequest(currentConfig, 0), currentConfig.id); return true; } bool TvcHandler::hasConfig(int32_t id) const { return m_cache.contains(id); } const TvcConfig &TvcHandler::config(int32_t id) const { auto found = m_cache.constFind(id); Q_ASSERT(found != m_cache.constEnd()); return *found; } void TvcHandler::onModifyTvcConfigRequest(const rti::core::SampleIdentity &cookie, const ModifyTvcConfigRequest &request) { DEBUG_LOG(m_logger) << "onModifyTvcConfigRequest: " << request; Q_ASSERT(request.id() >= 0); auto ret = request.id() > 0 ? modifyTvcConfig(request) : createTvcConfig(request); if (ret.error()) WARNING_LOG(m_logger) << ret(); m_fts->modifyTvcConfigReply(cookie, ret); } void TvcHandler::onRemoveTvcConfigRequest(const rti::core::SampleIdentity &cookie, const Core::RemoveElementRequest &request) { DEBUG_LOG(m_logger) << "onRemoveTvcConfigRequest" << request; Core::FunctionReplyObject rep = removeTvcConfig(request); if (rep.error()) WARNING_LOG(m_logger) << rep.translated(); m_fts->removeTvcConfigReply(cookie, rep); } bool TvcHandler::isValidChannel(int64_t channelId) const { auto withId = [channelId] (const Database::AvmTvcChannel &channel) { return channel.id == channelId; }; auto found = std::find_if(m_channels.cbegin(), m_channels.cend(), withId); return found != m_channels.cend(); } // Needs to be revised. // The idea is that we want to be able to reuse the channel (resource) in a different exercise // as long as the other exercise is not active at the same time. // A channel used by a global config can only be used once. // The same applies for isNameUnique(). // Such a concept would probably require checks in two different places: // 1. Here. We can check whether those constraints are met for the currently active exercises. // 2. Somewhere else. When an exercise is about to be started we must check for those constraints. // If there are conflicts between the then-active exercises the start of the new exercise must be denied. bool TvcHandler::isFreeChannel(const ModifyTvcConfigRequest &request) const { boost::optional exerciseId = toBoost(request.exerciseId()); if (request.id() <= 0) return isFreeChannel(request.tetraChannelId(), exerciseId); return isFreeChannel(request.tetraChannelId(), exerciseId, request.id()); } bool TvcHandler::isFreeChannel(int32_t channelId, const boost::optional &exerciseId, const boost::optional &excludeId) const { auto constraint = [&channelId] (const TvcConfig &config) { return config.tetraChannelId() != channelId; }; return checkConstraint(constraint, exerciseId, excludeId); } bool TvcHandler::isNameUnique(const ModifyTvcConfigRequest &request) const { boost::optional exerciseId = toBoost(request.exerciseId()); if (request.id() <= 0) return isNameUnique(request.radioCircuit(), exerciseId); return isNameUnique(request.radioCircuit(), exerciseId, request.id()); } // Needs to be revised. // See isFreeChannel(). bool TvcHandler::isNameUnique(const std::string &name, const boost::optional &exerciseId, const boost::optional &excludeId) const { auto constraint = [&name] (const TvcConfig &config) { return config.radioCircuit() != name; }; return checkConstraint(constraint, exerciseId, excludeId); } /** * @param dbId Is set when called from init(). */ Core::FunctionReplyObject TvcHandler::createTvcConfig(const ModifyTvcConfigRequest &request, const boost::optional &dbId) { auto reply = validateRequest(request); if (reply.error()) return reply; Database::AvmTvcConfig dbConfigData = toDb(request); if (dbId) dbConfigData.id = *dbId; else { if (!m_db->insert(dbConfigData)) return reply.setStr(tr_lts("dds", "Could not insert new TVC config data to database")); } TvcConfig tvcConfig = toDds(dbConfigData); m_cache.insert(tvcConfig.id(), tvcConfig); m_dts->publish(tvcConfig); if (needDeviceConfig(tvcConfig)) m_deviceConfig->create(tvcConfig); return Core::FunctionReplyObject(tvcConfig.id()); } Core::FunctionReplyObject TvcHandler::modifyTvcConfig(const ModifyTvcConfigRequest &request) { auto reply = validateRequest(request); if (reply.error()) return reply; Database::AvmTvcConfig dbConfigData = toDb(request); if (!m_db->update(dbConfigData)) return reply.setStr(tr_lts("dds", "Could not update TVC config data in database with id %1")).arg(request.id()); TvcConfig tvcConfig = toDds(dbConfigData); m_cache[tvcConfig.id()] = tvcConfig; m_dts->publish(tvcConfig); if (needDeviceConfig(tvcConfig)) m_deviceConfig->createOrUpdate(tvcConfig); else m_deviceConfig->tryRemove(tvcConfig.id()); emit tvcConfigChanged(tvcConfig); return reply; } Core::FunctionReplyObject TvcHandler::removeTvcConfig(const Core::RemoveElementRequest &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 TVC config because id %1 is unknown")).arg(configId); m_cache.erase(found); m_deviceConfig->tryRemove(configId); m_dts->removeTvcConfigByKey(configId); if (!m_db->removeAvmTvcConfig(configId)) return reply.setStr(tr_lts("dds", "Could not remove TVC config data with id %1")).arg(request.id()); return reply; } Database::AvmDeviceConfig TvcHandler::toDeviceConfig(const TvcConfig &tvcConfig) const { Database::AvmDeviceConfig result; Database::AvmTvcChannel tetraChannel = channelById(tvcConfig.tetraChannelId()); QJsonObject settings = QJsonDocument::fromJson(tetraChannel.settings.toUtf8()).object(); QJsonObject data = settings[QStringLiteral("data")].toObject(); if (settings[QStringLiteral("type")].toString() == QStringLiteral("TVC")) { result.context = exerciseList(tvcConfig); result.type = QStringLiteral("TETRA_RT_CHANNEL"); result.name = QString::fromStdString(tvcConfig.radioCircuit()); result.controller = QStringLiteral("EXCAVS01"); result.definition = QStringLiteral("mode=1;ssrc=%1;port=%2").arg(data[QStringLiteral("ssrc")].toInt()).arg(data[QStringLiteral("port")].toInt()); if (tvcConfig.recording()) result.definition.append(QStringLiteral(";recording=ECRS@%1").arg(result.controller)); result.link = 0; } else if (settings[QStringLiteral("type")].toString() == QStringLiteral("SOTAS")) { result.context = exerciseList(tvcConfig); result.type = QStringLiteral("SOTAS_SIP_RT_CHANNEL"); result.name = QString::fromStdString(tvcConfig.radioCircuit()); result.controller = QStringLiteral("EXCAVS01"); result.definition = QStringLiteral("mode=1;sip=%1@%2").arg(data[QStringLiteral("role")].toString()).arg(data[QStringLiteral("host")].toString()); if (tvcConfig.recording()) result.definition.append(QStringLiteral(";recording=ECRS@%1").arg(result.controller)); result.link = 0; } return result; } Database::AvmTvcChannel TvcHandler::channelById(int32_t id) const { for (const Database::AvmTvcChannel &channel : qAsConst(m_channels)) if (channel.id == id) return channel; Q_ASSERT(false); return Database::AvmTvcChannel(); } void TvcHandler::onContextActivated(const ExerciseContext &context) { bool isFirstActiveExercise = m_activeExercises->count() == 1; for (const TvcConfig &tvcConfig : qAsConst(m_cache)) { bool isLocal = tvcConfig.exerciseId().is_set(); if (isLocal) { if (tvcConfig.exerciseId() == context.id()) m_deviceConfig->create(tvcConfig); } else { if (isFirstActiveExercise) m_deviceConfig->create(tvcConfig); else m_deviceConfig->update(tvcConfig); } } } void TvcHandler::onContextDeactivated(const ExerciseContext &context) { bool haveActiveExercise = m_activeExercises->count() > 0; for (const TvcConfig &tvcConfig : qAsConst(m_cache)) { bool isLocal = tvcConfig.exerciseId().is_set(); if (isLocal) { if (tvcConfig.exerciseId() == context.id()) m_deviceConfig->remove(tvcConfig.id()); } else { if (!haveActiveExercise) m_deviceConfig->remove(tvcConfig.id()); else m_deviceConfig->update(tvcConfig); } } } /** * Will be called by the DB after an AvmTvcConfig 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 TvcHandler::onDbDeleteAvmTvcConfig(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 AvmTvcConfig with id: " << configId; removeTvcConfig(Core::RemoveElementRequest(configId)); } bool TvcHandler::isActiveExercise(int exerciseId) const { return m_activeExercises->isActive(ExerciseContext(exerciseId)); } /** * request can be either a create- or a modify-requst. */ Core::FunctionReplyObject TvcHandler::validateRequest(const ModifyTvcConfigRequest &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 TVC config because id %1 is unknown")).arg(request.id()); } if (!isValidChannel(request.tetraChannelId())) return reply.setStr(tr_lts("dds", "Could not create/modify TVC config because the channel with id %1 is invalid")).arg(request.tetraChannelId()); if (!isFreeChannel(request)) return reply.setStr(tr_lts("dds", "Could not create/modify TVC config because channel id %1 is already taken")).arg(request.tetraChannelId()); if (!isStreamNameValid(request.radioCircuit())) return reply.setStr(tr_lts("dds", "Could not create/modify TVC config because the circuit name is invalid")); if (!isNameUnique(request)) return reply.setStr(tr_lts("dds", "Could not create/modify TVC config because the circuit name is already in use")); return reply; } /** * Helper function for isFreeChannel() and isNameUnique(). */ bool TvcHandler::checkConstraint(const std::function &constraint, const boost::optional &exerciseId, const boost::optional &excludeId) const { for (const TvcConfig &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. // Otherwise (one or both are global) 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; } if (!constraint(config)) return false; } return true; } /** * @return The list of exercises for the given config. The returned string * is used in the definition of the device_config. */ QString TvcHandler::exerciseList(const TvcConfig &config) const { bool isLocal = config.exerciseId().is_set(); if (isLocal) return ExerciseContext(config.exerciseId().get()).toString(); // for global config QString exerciseList; const auto &activeExercises = m_activeExercises->allContexts(); for (const auto &context : activeExercises) { exerciseList.append(context.toString()); exerciseList.append(QLatin1Char(',')); } exerciseList.resize(exerciseList.size() - 1); // cut ',' at end return exerciseList; } bool TvcHandler::needDeviceConfig(const TvcConfig &config) const { bool isGlobal = !config.exerciseId().is_set(); bool hasActiveExericse = m_activeExercises->count() > 0; return (isGlobal && hasActiveExericse) || (!isGlobal && isActiveExercise(config.exerciseId().get())); } } // namespace AudioVideoManagement