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

756 lines
24 KiB
C++
Executable File

/**
* 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 <InfrastructureManagement/ism_dtc.h>
#include <LtsUtilsCore/json_object.h>
#include <ngldb/dbtransaction.h>
#include <QStringList>
#include <algorithm>
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<class T>
dds::core::optional<T> toDdsOptional(const QVariant &variant)
{
dds::core::optional<T> optional;
if (!variant.isNull())
optional = variant.value<T>();
return optional;
}
CnrChannel toDds(const Database::AvmCnrChannel &channel)
{
return CnrChannel(channel.id,
channel.name.toStdString(),
channel.ssrc,
channel.addacChannel,
channel.pttControl,
static_cast<FrequencyBand::inner_enum>(channel.frequencyBand));
}
ModifyCnrConfigRequest toRequest(const Database::AvmCnrConfig &dbData, const boost::optional<int32_t> &requestId = boost::none)
{
return ModifyCnrConfigRequest(requestId.get_value_or(dbData.id),
dbData.cnrChannelId,
toDdsOptional<int>(dbData.exerciseId),
dbData.radioCircuit.toStdString(),
dbData.recording,
toDdsOptional<int>(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<int32_t, Database::AvmDeviceConfig> 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 &currentChannel : qAsConst(m_channels))
m_dts->publish(::AudioVideoManagement::toDds(currentChannel));
// Don't use select with callback here as addConfig() uses the DB, too.
QVector<AvmCnrConfig> cnrConfigs;
if (!m_db->select(cnrConfigs))
{
ERROR_LOG(m_logger) << "Error while selecting CNR configs from db";
return false;
}
for (const AvmCnrConfig &currentConfig : 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<CnrConfig> 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<int>(config.exerciseId),
config.radioCircuit.toStdString(),
config.partyId,
toDdsOptional<int>(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<bool (const CnrConfig &)> &constraint, const boost::optional<int> &exerciseId, const boost::optional<int32_t> &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<int> 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<int> &exerciseId, const boost::optional<int32_t> &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<int> 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<int> &exerciseId, const boost::optional<int32_t> &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<int32_t> &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