Revision control

Copy as Markdown

// Copyright (C) 2023 basysKom GmbH, opensource@basyskom.com
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QMap>
#include <QRegularExpression>
/*
* This generator should be run if there is a new version of the NodeIds.csv or StatusCode.csv file
*
* Building
* Qt OPC UA must be configured with FEATURE_internalgenerator to enable building the generator.
*
* Usage:
* qtopcua-internalgenerator -n NodeIds.csv -s StatusCode.csv -o /path/to/qtopcua
*
* This updates qopcuanodeids.h, qopcuanodeids.cpp, qopcuatype.h, qopcuatype.cpp,
* opcuastatus_p.h and opcuastatus.cpp with the most recent status codes and node ids.
* The Qt OPC UA module must be rebuilt after running the generator.
*/
bool patchFile(const QString &path, const QString &startMarker, const QString &endMarker, const QString &newContent)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "Failed to open" << path << "for reading";
return false;
}
const auto content = QString::fromUtf8(file.readAll());
file.close();
const auto startIndex = content.indexOf(startMarker);
if (startIndex == -1) {
qWarning() << "Failed to parse" << path << "- has the structure changed?";
return false;
}
const auto endIndex = content.indexOf(endMarker, startIndex);
if (endIndex == -1) {
qWarning() << "Failed to parse" << path << "- has the structure changed?";
return false;
}
if (!file.open(QIODevice::WriteOnly | QFile::Truncate)) {
qWarning() << "Failed to open" << path << "for writing";
return false;
}
QTextStream out(&file);
out << content.left(startIndex + startMarker.length());
out << newContent;
out << content.right(content.size() - endIndex);
return true;
}
class NodeId {
public:
QString name;
QString value;
};
class StatusCode {
public:
QString name;
QString value;
QString comment;
};
QMap<QString, QString> parseExistingNodeIds(const QString &existingHeaderFilePath)
{
QMap<QString, QString> result;
QFile file(existingHeaderFilePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "Failed to open" << existingHeaderFilePath << "for reading";
return result;
}
QTextStream in(&file);
while (!in.atEnd()) {
const static QRegularExpression lineRegex(QStringLiteral("([a-zA-Z0-9_]+) = ([0-9]+),"));
const auto match = lineRegex.match(in.readLine());
if (match.hasMatch() && match.capturedLength(1) > 0 && match.capturedLength(2) > 0) {
result[match.captured(1)] = match.captured(2);
}
}
return result;
}
QList<NodeId> parseNodeIds(const QString &path)
{
QList<NodeId> result;
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "Failed to open" << path << "for reading";
return result;
}
QTextStream in(&file);
while (!in.atEnd())
{
const auto split = in.readLine().split(QChar::fromLatin1(','));
if (split.length() != 3)
continue;
result.push_back({ split.at(0), split.at(1) });
}
if (!result.isEmpty())
result.push_front({ QStringLiteral("Unknown"), QString::number(0) });
return result;
}
bool patchNodeIdsHeader(const QList<NodeId> &nodeIds, const QString &path)
{
const auto startMarker = QStringLiteral("enum class Namespace0 : quint32 {\n");
const auto endMarker = QStringLiteral(" };");
QString newContent;
QTextStream out(&newContent);
for (const auto &id : nodeIds) {
out << " " << id.name << " = " << id.value << "," << "\n";
}
return patchFile(path, startMarker, endMarker, newContent);
}
bool patchNodeIdsSource(const QList<NodeId> &nodeIds, const QString &path)
{
const auto startMarker = QStringLiteral("RootFolder)));\n \\endcode\n\n");
const auto endMarker = QStringLiteral("*/");
QString newContent;
QTextStream out(&newContent);
for (const auto &id : nodeIds) {
out << " " << "\\value " << id.name << "\n";
}
return patchFile(path, startMarker, endMarker, newContent);
}
bool generateNodeIdsMetaObject(const QList<NodeId> &nodeIds, const QString &path)
{
const auto startMarker = QStringLiteral("// Begin generated QMetaObject assembly");
const auto endMarker = QStringLiteral("// End generated QMetaObject assembly");
QString newContent;
QTextStream out(&newContent);
const QStringList additionalEntries = {
QStringLiteral("QOpcUa::NodeIds"),
QStringLiteral("Namespace0"),
};
int totalStringLength = 0;
const int offsetCount = (nodeIds.length() + additionalEntries.length()) * 2;
for (const auto &name : additionalEntries)
totalStringLength += name.length() + 1;
for (const auto &id : nodeIds)
totalStringLength += id.name.length() + 1;
out << "\nstatic constexpr CustomStringData<" << offsetCount << ", " << totalStringLength << "> customStringData {\n";
int offset = 0;
int currentLineCount = 0;
out << "{\n";
for (const auto &name : additionalEntries) {
out << "O(" << offset <<"," << name.length() << ")";
offset += name.length() + 1;
++currentLineCount;
}
for (const auto &id : nodeIds) {
out << "O(" << offset << "," << id.name.length() << ")" << (currentLineCount == 10 ? "\n" : "");
offset += id.name.length() + 1;
currentLineCount = ++currentLineCount % 11;
}
out << "},\n";
QString concat;
for (const auto &name : additionalEntries)
concat += name + QStringLiteral("\\0");
for (const auto &entry : nodeIds)
concat += entry.name + (entry.value == nodeIds.back().value ? QString() : QStringLiteral("\\0"));
out << "\"";
for (int i = 0; i < concat.size(); ++i) {
out << concat.at(i);
if (i != 0 && i % 150 == 0) {
if (concat.at(i) == QChar::fromLatin1('\\'))
out << concat.at(++i);
out << "\"\n\"";
}
}
out << "\"\n";
out << "};\n\n";
out << "static constexpr auto buildMetaObjectData()\n";
out << "{\n";
out << " const QtMocHelpers::UintData methodsData {};\n";
out << " const QtMocHelpers::UintData propertiesData {};\n";
out << " const QtMocHelpers::StringRefStorage dummyStringData {\n";
out << " \"\",\n";
out << " };\n";
out << " const QtMocHelpers::UintData enumData {\n";
out <<" QtMocHelpers::EnumData<Namespace0>(1, 1, QtMocConstants::EnumIsScoped).add({\n";
for (int i = 0; i < nodeIds.size(); ++i)
out << "E(" << i + 2 << "," << nodeIds.at(i).value << ")" << (i != 0 && i % 10 == 0 ? "\n" : "");
out << "})\n";
out << "};\n";
return patchFile(path, startMarker, endMarker, newContent);
}
QList<StatusCode> parseStatusCodes(const QString &path)
{
QList<StatusCode> result;
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "Failed to open" << path << "for reading";
return result;
}
QTextStream in(&file);
while (!in.atEnd())
{
const auto split = in.readLine().split(QChar::fromLatin1(','));
if (split.length() < 3)
continue;
QString comment = split.size() == 3 ? split.at(2) : split.mid(2).join(QStringLiteral(","));
comment = comment.remove(QChar::fromLatin1('"'));
result.push_back({ split.at(0), split.at(1), comment });
}
return result;
}
bool patchStatusCodeHeader(const QList<StatusCode> &statusCodes, const QString &path)
{
const auto startMarker = QStringLiteral("enum UaStatusCode : quint32\n{\n");
const auto endMarker = QStringLiteral("};");
QString newContent;
QTextStream out(&newContent);
for (const auto &statusCode : statusCodes) {
out << " " << statusCode.name << " = " << statusCode.value << "," << "\n";
}
return patchFile(path, startMarker, endMarker, newContent);
}
bool patchStatusCodeSource(const QList<StatusCode> &statusCodes, const QString &path)
{
const auto startMarker = QStringLiteral("Opc.Ua.StatusCodes.csv}\n\n");
const auto endMarker = QStringLiteral("*/");
QString newContent;
QTextStream out(&newContent);
for (const auto &statusCode : statusCodes) {
out << " " << "\\value " << statusCode.name << " " << statusCode.comment << "\n";
}
return patchFile(path, startMarker, endMarker, newContent);
}
bool patchQmlStatusCodeHeader(const QList<StatusCode> &statusCodes, const QString &path)
{
const auto startMarker = QStringLiteral("enum class Status {\n");
const auto endMarker = QStringLiteral(" };");
QString newContent;
QTextStream out(&newContent);
for (const auto &statusCode : statusCodes) {
out << " " << statusCode.name << " = " << statusCode.value.left(6) << "," << "\n";
}
return patchFile(path, startMarker, endMarker, newContent);
}
bool patchQmlStatusCodeSource(const QList<StatusCode> &statusCodes, const QString &path)
{
auto startMarker = QStringLiteral("Attributes of a status\n\n");
auto endMarker = QStringLiteral("*/");
QString newContent;
QTextStream out(&newContent);
for (const auto &statusCode : statusCodes) {
out << " " << "\\value Status." << statusCode.name << " " << statusCode.comment << "\n";
}
const auto success = patchFile(path, startMarker, endMarker, newContent);
if (!success)
return false;
startMarker = QStringLiteral("switch (static_cast<QOpcUa::UaStatusCode>(m_statusCode)) {\n");
endMarker = QStringLiteral(" }");
out.seek(0);
newContent.clear();
for (const auto &statusCode : statusCodes) {
out << " " << "case QOpcUa::" << statusCode.name << ":\n";
out << " " << "return Status::" << statusCode.name << ";\n";
}
return patchFile(path, startMarker, endMarker, newContent);
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QCommandLineParser parser;
parser.addHelpOption();
parser.setApplicationDescription("\nThis application applies changes from the NodeIds.csv and StatusCode.csv files distributed by the OPC Foundation.");
const QCommandLineOption nOption("n", "The NodeIds.csv file path", "input file");
const QCommandLineOption sOption("s", "The StatusCode.csv file path", "input file");
const QCommandLineOption oOption("o", "The base directory of the Qt OPC UA repository", "output base directory");
parser.addOption(nOption);
parser.addOption(sOption);
parser.addOption(oOption);
parser.process(app);
if (!parser.isSet(oOption)) {
qWarning() << "Unable to run without -o specified";
return EXIT_FAILURE;
}
if (!parser.isSet(nOption) && !parser.isSet(sOption)) {
qWarning() << "Unable to run without any of -n and -s specified";
return EXIT_FAILURE;
}
bool success = false;
if (parser.isSet(nOption)) {
const auto nodeIdsHeaderPath = QStringLiteral("%1/src/opcua/client/qopcuanodeids.h").arg(parser.value(oOption));
const auto nodeIdsSourcePath = QStringLiteral("%1/src/opcua/client/qopcuanodeids.cpp").arg(parser.value(oOption));
const auto nodeIdsMetaObjectPath = QStringLiteral("%1/src/opcua/client/qopcuanodeidsmetaobject.cpp").arg(parser.value(oOption));
const auto existingNodeIds = parseExistingNodeIds(nodeIdsHeaderPath);
const auto newNodeIds = parseNodeIds(parser.value(nOption));
if (newNodeIds.isEmpty()) {
qWarning() << "No node ids were found in the file";
return EXIT_FAILURE;
}
QList<NodeId> mergedIds;
for (const auto &entry : existingNodeIds.asKeyValueRange())
mergedIds.push_back({ entry.first, entry.second });
for (const auto &entry : newNodeIds) {
if (!existingNodeIds.contains(entry.name)) {
mergedIds.push_back(entry);
} else if (entry.value != existingNodeIds.value(entry.name)) {
qFatal() << "The value of" << entry.name << "has changed from"
<< existingNodeIds.value(entry.name) << "to" << entry.value
<< " - please check manually";
}
}
std::sort(mergedIds.begin(), mergedIds.end(), [](const NodeId &a, const NodeId &b) {
const auto idA = a.value.toInt();
const auto idB = b.value.toInt();
return idA == idB ? a.name < b.name : idA < idB;
});
success = patchNodeIdsHeader(mergedIds, nodeIdsHeaderPath);
if (success)
success = patchNodeIdsSource(mergedIds, nodeIdsSourcePath);
if (success)
success = generateNodeIdsMetaObject(mergedIds, nodeIdsMetaObjectPath);
return success ? EXIT_SUCCESS : EXIT_FAILURE;
}
if (parser.isSet(sOption)) {
const auto typeHeaderPath = QStringLiteral("%1/src/opcua/client/qopcuatype.h").arg(parser.value(oOption));
const auto typeSourcePath = QStringLiteral("%1/src/opcua/client/qopcuatype.cpp").arg(parser.value(oOption));
const auto qmlStatusHeaderPath = QStringLiteral("%1/src/declarative_opcua/opcuastatus_p.h").arg(parser.value(oOption));
const auto qmlStatusSourcePath = QStringLiteral("%1/src/declarative_opcua/opcuastatus.cpp").arg(parser.value(oOption));
const auto statusCodes = parseStatusCodes(parser.value(sOption));
if (statusCodes.isEmpty()) {
qWarning() << "No status codes were found in the file";
return EXIT_FAILURE;
}
success = patchStatusCodeHeader(statusCodes, typeHeaderPath);
if (success)
success = patchStatusCodeSource(statusCodes, typeSourcePath);
if (success)
success = patchQmlStatusCodeHeader(statusCodes, qmlStatusHeaderPath);
if (success)
success = patchQmlStatusCodeSource(statusCodes, qmlStatusSourcePath);
}
return success ? EXIT_SUCCESS : EXIT_SUCCESS;
}