583 lines
18 KiB
C++
Executable File
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 ¤tChannel : 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 ¤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<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
|