Files
2026-08-13 18:33:10 +02:00

583 lines
18 KiB
C++
Executable File

/**
* 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 <avm_dts.h>
#include <algorithm>
#include <QJsonDocument>
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<int> exerciseId;
if (!dbData.exerciseId.isNull())
exerciseId = dbData.exerciseId.value<int>();
return TvcConfig(dbData.id,
dbData.tetraChannelId,
dbData.radioCircuit.toStdString(),
exerciseId,
dbData.recording,
dbData.info.toStdString());
}
ModifyTvcConfigRequest toRequest(const Database::AvmTvcConfig &dbData, const boost::optional<int32_t> &requestId = boost::none)
{
dds::core::optional<int> exerciseId;
if (!dbData.exerciseId.isNull())
exerciseId = dbData.exerciseId.value<int>();
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<int32_t, Database::AvmDeviceConfig> m_cache; // <TvcConfig::id, Database::DeviceConfig>
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 &currentChannel : qAsConst(m_channels))
m_dts->publish(toDds(currentChannel));
QVector<AvmTvcConfig> tvcConfigs;
if (!m_db->select(tvcConfigs))
{
ERROR_LOG(m_logger) << "Error while selecting TVC configs from db";
return false;
}
for (const AvmTvcConfig &currentConfig : 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<int> 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<int> &exerciseId, const boost::optional<int32_t> &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<int> 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<int> &exerciseId, const boost::optional<int32_t> &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<int32_t> &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<bool (const TvcConfig &)> &constraint, const boost::optional<int> &exerciseId, const boost::optional<int32_t> &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