datactl/tsyncfile.cpp

datactl/tsyncfile.cpp

datactl/tsyncfile.cpp

Namespaces

Name
Syntalos

Functions

Name
template <class T >
T
csReadValue(QDataStream & in, XXH3_state_t * state)
QByteArraycsReadValue(QDataStream & in, XXH3_state_t * state)

Defines

Name
TSYNC_FILE_MAGIC
TSYNC_FILE_VERSION_MAJOR
TSYNC_FILE_VERSION_MINOR
TSYNC_FILE_BLOCK_TERM

Functions Documentation

function csReadValue

template <class T >
inline T csReadValue(
    QDataStream & in,
    XXH3_state_t * state
)

function csReadValue

inline QByteArray csReadValue(
    QDataStream & in,
    XXH3_state_t * state
)

Macros Documentation

define TSYNC_FILE_MAGIC

#define TSYNC_FILE_MAGIC 0xF223434E5953548A

define TSYNC_FILE_VERSION_MAJOR

#define TSYNC_FILE_VERSION_MAJOR 1

define TSYNC_FILE_VERSION_MINOR

#define TSYNC_FILE_VERSION_MINOR 2

define TSYNC_FILE_BLOCK_TERM

#define TSYNC_FILE_BLOCK_TERM 0x1126000000000000

Source code

/*
 * Copyright (C) 2019-2024 Matthias Klumpp <matthias@tenstral.net>
 *
 * Licensed under the GNU Lesser General Public License Version 3
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the license, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "tsyncfile.h"

#include <QDateTime>
#include <QDebug>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>

#include "utils/misc.h"

namespace Syntalos
{
Q_LOGGING_CATEGORY(logTSyncFile, "tsyncfile")
}

using namespace Syntalos;

// TSYNC file magic number (saved as LE): 8A T S Y N C ⏲
#define TSYNC_FILE_MAGIC 0xF223434E5953548A

#define TSYNC_FILE_VERSION_MAJOR 1
#define TSYNC_FILE_VERSION_MINOR 2

#define TSYNC_FILE_BLOCK_TERM 0x1126000000000000

QString Syntalos::tsyncFileTimeUnitToString(const TSyncFileTimeUnit &tsftunit)
{
    switch (tsftunit) {
    case TSyncFileTimeUnit::INDEX:
        return QStringLiteral("index");
    case TSyncFileTimeUnit::MICROSECONDS:
        return QStringLiteral("µs");
    case TSyncFileTimeUnit::MILLISECONDS:
        return QStringLiteral("ms");
    case TSyncFileTimeUnit::SECONDS:
        return QStringLiteral("sec");
    default:
        return QStringLiteral("?");
    }
}

QString Syntalos::tsyncFileDataTypeToString(const TSyncFileDataType &dtype)
{
    switch (dtype) {
    case TSyncFileDataType::INT16:
        return QStringLiteral("int16");
    case TSyncFileDataType::INT32:
        return QStringLiteral("int32");
    case TSyncFileDataType::INT64:
        return QStringLiteral("int64");
    case TSyncFileDataType::UINT16:
        return QStringLiteral("uint16");
    case TSyncFileDataType::UINT32:
        return QStringLiteral("uint32");
    case TSyncFileDataType::UINT64:
        return QStringLiteral("uint64");
    default:
        return QStringLiteral("INVALID");
    }
}

QString Syntalos::tsyncFileModeToString(const TSyncFileMode &mode)
{
    switch (mode) {
    case TSyncFileMode::CONTINUOUS:
        return QStringLiteral("continuous");
    case TSyncFileMode::SYNCPOINTS:
        return QStringLiteral("syncpoints");
    default:
        return QStringLiteral("INVALID");
    }
}

// ------------------
// TimeSyncFileWriter
// ------------------

TimeSyncFileWriter::TimeSyncFileWriter()
    : m_file(new QFile()),
      m_bIndex(0)
{
    m_xxh3State = XXH3_createState();

    m_timeNames = qMakePair(QStringLiteral("device-time"), QStringLiteral("master-time"));
    m_timeUnits = qMakePair(TSyncFileTimeUnit::MICROSECONDS, TSyncFileTimeUnit::MICROSECONDS);
    m_time1DType = TSyncFileDataType::UINT32;
    m_time2DType = TSyncFileDataType::UINT32;
    m_tsMode = TSyncFileMode::CONTINUOUS;
    m_blockSize = 2800;

    m_stream.setVersion(QDataStream::Qt_5_12);
    m_stream.setByteOrder(QDataStream::LittleEndian);
}

TimeSyncFileWriter::~TimeSyncFileWriter()
{
    this->close();
    delete m_file;
    XXH3_freeState(m_xxh3State);
}

QString TimeSyncFileWriter::lastError() const
{
    return m_lastError;
}

void TimeSyncFileWriter::setTimeNames(const QString &time1Name, const QString &time2Name)
{
    m_timeNames = qMakePair(time1Name, time2Name);
}

void TimeSyncFileWriter::setTimeUnits(TSyncFileTimeUnit time1Unit, TSyncFileTimeUnit time2Unit)
{
    m_timeUnits = qMakePair(time1Unit, time2Unit);
}

void TimeSyncFileWriter::setTimeDataTypes(TSyncFileDataType time1DType, TSyncFileDataType time2DType)
{
    m_time1DType = time1DType;
    m_time2DType = time2DType;
}

void TimeSyncFileWriter::setFileName(const QString &fname)
{
    if (m_file->isOpen())
        m_file->close();

    auto tsyncFname = fname;
    if (!tsyncFname.endsWith(QStringLiteral(".tsync")))
        tsyncFname = tsyncFname + QStringLiteral(".tsync");
    m_file->setFileName(tsyncFname);
}

QString TimeSyncFileWriter::fileName() const
{
    if (!m_file->isOpen())
        return QString();
    return m_file->fileName();
}

void TimeSyncFileWriter::setSyncMode(TSyncFileMode mode)
{
    m_tsMode = mode;
}

void TimeSyncFileWriter::setChunkSize(int size)
{
    m_blockSize = size;
}

void TimeSyncFileWriter::setCreationTimeOverride(const QDateTime &dt)
{
    m_creationTimeOverride = dt;
}

template<class T>
void TimeSyncFileWriter::csWriteValue(const T &data)
{
    static_assert(std::is_arithmetic<T>::value, "T must be an arithmetic type.");

    auto d = static_cast<T>(data);
    m_stream << d;
    XXH3_64bits_update(m_xxh3State, (const uint8_t *)&d, sizeof(d));
}

template<>
void TimeSyncFileWriter::csWriteValue<QByteArray>(const QByteArray &data)
{
    m_stream << data;
    XXH3_64bits_update(m_xxh3State, (const uint8_t *)data.constData(), sizeof(char) * data.size());
}

bool TimeSyncFileWriter::open(const QString &modName, const QUuid &collectionId, const QVariantHash &userData)
{
    if (m_file->isOpen())
        m_file->close();

    if (!m_file->open(QIODevice::WriteOnly)) {
        m_lastError = m_file->errorString();
        return false;
    }

    // ensure block size is not extremely small
    if ((m_blockSize >= 0) && (m_blockSize < 128))
        m_blockSize = 128;

    m_bIndex = 0;
    XXH3_64bits_reset(m_xxh3State);
    m_stream.setDevice(m_file);

    // user-defined metadata
    QJsonDocument jdoc(QJsonObject::fromVariantHash(userData));
    const QString userDataJson(jdoc.toJson(QJsonDocument::Compact));

    // write file header
    QDateTime currentTime = m_creationTimeOverride;
    if (!currentTime.isValid())
        currentTime = QDateTime::currentDateTime();
    m_creationTimeOverride = QDateTime();

    m_stream << (quint64)TSYNC_FILE_MAGIC;

    csWriteValue<quint16>(TSYNC_FILE_VERSION_MAJOR);
    csWriteValue<quint16>(TSYNC_FILE_VERSION_MINOR);

    csWriteValue<qint64>(currentTime.toTime_t());

    csWriteValue(modName.toUtf8());
    csWriteValue(collectionId.toString(QUuid::WithoutBraces).toUtf8());
    csWriteValue(userDataJson.toUtf8()); // custom JSON values

    csWriteValue<quint16>((quint16)m_tsMode);
    csWriteValue<qint32>(m_blockSize);

    csWriteValue(m_timeNames.first.toUtf8());
    csWriteValue<quint16>((quint16)m_timeUnits.first);
    csWriteValue<quint16>((quint16)m_time1DType);

    csWriteValue(m_timeNames.second.toUtf8());
    csWriteValue<quint16>((quint16)m_timeUnits.second);
    csWriteValue<quint16>((quint16)m_time2DType);

    m_file->flush();
    const auto headerBytes = m_file->size();
    if (headerBytes <= 0)
        qFatal("Could not determine amount of bytes written for tsync file header.");
    const int padding = (headerBytes * -1) & (8 - 1); // 8-byte align header
    for (int i = 0; i < padding; i++)
        csWriteValue<quint8>(0);

    // write end of header and header CRC-32
    writeBlockTerminator(false);

    m_file->flush();
    return true;
}

bool TimeSyncFileWriter::open(
    const QString &modName,
    const QUuid &collectionId,
    const microseconds_t &tolerance,
    const QVariantHash &userData)
{
    QVariantHash udata = userData;
    udata["tolerance_us"] = QVariant::fromValue(tolerance.count());
    return open(modName, collectionId, udata);
}

void TimeSyncFileWriter::flush()
{
    if (m_file->isOpen())
        m_file->flush();
}

void TimeSyncFileWriter::close()
{
    if (m_file->isOpen()) {
        // terminate the last open block, if we have one
        writeBlockTerminator();

        // finish writing file to disk
        m_file->flush();
        m_file->close();
    }
}

void TimeSyncFileWriter::writeTimes(const microseconds_t &deviceTime, const microseconds_t &masterTime)
{
    writeTimeEntry(deviceTime.count(), masterTime.count());
}

void TimeSyncFileWriter::writeTimes(const long long &timeIndex, const microseconds_t &masterTime)
{
    writeTimeEntry(timeIndex, masterTime.count());
}

void TimeSyncFileWriter::writeTimes(const long &time1, const long &time2)
{
    writeTimeEntry(time1, time2);
}

void TimeSyncFileWriter::writeTimes(const uint64_t &time1, const uint64_t &time2)
{
    writeTimeEntry(time1, time2);
}

void TimeSyncFileWriter::writeBlockTerminator(bool check)
{
    if (check && (m_bIndex == 0))
        return;
    m_stream << (quint64)TSYNC_FILE_BLOCK_TERM;
    m_stream << (quint64)XXH3_64bits_digest(m_xxh3State);
    XXH3_64bits_reset(m_xxh3State);
    m_bIndex = 0;
}

template<class T1, class T2>
void TimeSyncFileWriter::writeTimeEntry(const T1 &time1, const T2 &time2)
{
    static_assert(std::is_arithmetic<T1>::value, "T1 must be an arithmetic type.");
    static_assert(std::is_arithmetic<T2>::value, "T2 must be an arithmetic type.");

    switch (m_time1DType) {
    case TSyncFileDataType::INT16:
        csWriteValue<qint16>(time1);
        break;
    case TSyncFileDataType::INT32:
        csWriteValue<qint32>(time1);
        break;
    case TSyncFileDataType::INT64:
        csWriteValue<qint64>(time1);
        break;
    case TSyncFileDataType::UINT16:
        csWriteValue<quint16>(time1);
        break;
    case TSyncFileDataType::UINT32:
        csWriteValue<quint32>(time1);
        break;
    case TSyncFileDataType::UINT64:
        csWriteValue<quint64>(time1);
        break;
    default:
        qFatal("Tried to write unknown datatype to timesync file for time1: %i", (int)m_time1DType);
        break;
    }

    switch (m_time2DType) {
    case TSyncFileDataType::INT16:
        csWriteValue<qint16>(time2);
        break;
    case TSyncFileDataType::INT32:
        csWriteValue<qint32>(time2);
        break;
    case TSyncFileDataType::INT64:
        csWriteValue<qint64>(time2);
        break;
    case TSyncFileDataType::UINT16:
        csWriteValue<quint16>(time2);
        break;
    case TSyncFileDataType::UINT32:
        csWriteValue<quint32>(time2);
        break;
    case TSyncFileDataType::UINT64:
        csWriteValue<quint64>(time2);
        break;
    default:
        qFatal("Tried to write unknown datatype to timesync file for time2: %i", (int)m_time1DType);
        break;
    }

    m_bIndex++;
    if (m_bIndex >= m_blockSize)
        writeBlockTerminator();
}

TimeSyncFileReader::TimeSyncFileReader()
    : m_lastError(QString())
{
}

template<class T>
inline T csReadValue(QDataStream &in, XXH3_state_t *state)
{
    static_assert(std::is_arithmetic<T>::value, "T must be an arithmetic type.");

    T value;
    in >> value;
    XXH3_64bits_update(state, (const uint8_t *)&value, sizeof(value));
    return value;
}

template<>
inline QByteArray csReadValue(QDataStream &in, XXH3_state_t *state)
{
    QByteArray value;
    in >> value;
    XXH3_64bits_update(state, (const uint8_t *)value.constData(), sizeof(char) * value.size());
    return value;
}

bool TimeSyncFileReader::open(const QString &fname)
{
    QFile file(fname);
    if (!file.open(QIODevice::ReadOnly)) {
        m_lastError = file.errorString();
        return false;
    }
    QDataStream in(&file);
    in.setVersion(QDataStream::Qt_5_12);
    in.setByteOrder(QDataStream::LittleEndian);

    // read the header
    quint64 magic;
    in >> magic;
    if (magic != TSYNC_FILE_MAGIC) {
        m_lastError = QStringLiteral("Unable to read data: This file is not a valid timesync metadata file.");
        return false;
    }

    XXH3_state_t *csState = XXH3_createState();
    XXH3_64bits_reset(csState);

    const auto formatVMajor = csReadValue<quint16>(in, csState);
    const auto formatVMinor = csReadValue<quint16>(in, csState);
    if ((formatVMajor != TSYNC_FILE_VERSION_MAJOR) || (formatVMinor < TSYNC_FILE_VERSION_MINOR)) {
        m_lastError = QStringLiteral(
                          "Unable to read data: This file is using an incompatible (probably newer) version of the "
                          "format which we can not read (%1.%2 vs %3.%4).")
                          .arg(formatVMajor)
                          .arg(formatVMinor)
                          .arg(TSYNC_FILE_VERSION_MAJOR)
                          .arg(TSYNC_FILE_VERSION_MINOR);
        XXH3_freeState(csState);
        return false;
    }

    m_creationTime = csReadValue<qint64>(in, csState);

    const auto modNameUtf8 = csReadValue<QByteArray>(in, csState);
    const auto collectionIdUtf8 = csReadValue<QByteArray>(in, csState);
    const auto userJsonUtf8 = csReadValue<QByteArray>(in, csState);

    m_moduleName = QString::fromUtf8(modNameUtf8);
    m_collectionId = QUuid(QString::fromUtf8(collectionIdUtf8));

    QJsonDocument jdoc = QJsonDocument::fromJson(userJsonUtf8);
    m_userData = QVariantHash();
    if (jdoc.isObject())
        m_userData = jdoc.object().toVariantHash();

    // file storage mode
    m_tsMode = static_cast<TSyncFileMode>(csReadValue<quint16>(in, csState));

    // block size
    m_blockSize = csReadValue<qint32>(in, csState);

    // time info
    const auto timeName1Utf8 = csReadValue<QByteArray>(in, csState);
    const auto timeUnit1_i = csReadValue<quint16>(in, csState);
    const auto timeDType1_i = csReadValue<quint16>(in, csState);

    const auto timeName2Utf8 = csReadValue<QByteArray>(in, csState);
    const auto timeUnit2_i = csReadValue<quint16>(in, csState);
    const auto timeDType2_i = csReadValue<quint16>(in, csState);

    m_timeNames.first = QString::fromUtf8(timeName1Utf8);
    m_timeNames.second = QString::fromUtf8(timeName2Utf8);
    m_tolerance = microseconds_t(m_userData.value("tolerance_us").toLongLong());
    m_timeUnits.first = static_cast<TSyncFileTimeUnit>(timeUnit1_i);
    m_timeUnits.second = static_cast<TSyncFileTimeUnit>(timeUnit2_i);

    const auto timeDType1 = static_cast<TSyncFileDataType>(timeDType1_i);
    const auto timeDType2 = static_cast<TSyncFileDataType>(timeDType2_i);
    m_timeDTypes = qMakePair(timeDType1, timeDType2);

    // skip potential alignment bytes
    const int padding = (file.pos() * -1) & (8 - 1); // files use 8-byte alignment
    for (int i = 0; i < padding; i++)
        csReadValue<quint8>(in, csState);

    // check header CRC
    quint64 expectedHeaderCRC;
    quint64 blockTerm;
    in >> blockTerm >> expectedHeaderCRC;
    if (blockTerm != TSYNC_FILE_BLOCK_TERM) {
        m_lastError = QStringLiteral(
            "Header block terminator not found: The file is either invalid or its header block was damaged.");
        XXH3_freeState(csState);
        return false;
    }
    if (expectedHeaderCRC != XXH3_64bits_digest(csState)) {
        m_lastError = QStringLiteral(
            "Header checksum mismatch: The file is either invalid or its header block was damaged.");
        XXH3_freeState(csState);
        return false;
    }

    // read the time data
    m_times.clear();
    int bIndex = 0;
    XXH3_64bits_reset(csState);
    const auto data_sec_end = file.size() - 16;
    while (!in.atEnd()) {
        if (file.pos() == data_sec_end) {
            // read last 16 bytes, which *must* be the block terminator of the final block, otherwise
            // our file was truncated or corrupted.
            quint64 expectedCRC;
            in >> blockTerm >> expectedCRC;

            if (blockTerm != TSYNC_FILE_BLOCK_TERM) {
                m_lastError = QStringLiteral(
                    "Unable to read all tsync data: File was likely truncated (its last block is not complete).");
                XXH3_freeState(csState);
                return false;
            }
            if (expectedCRC != XXH3_64bits_digest(csState))
                qCWarning(logTSyncFile).noquote()
                    << "CRC check failed for last tsync data block: Data is likely corrupted.";
            break;
        }

        long long timeVal1;
        long long timeVal2;

        switch (timeDType1) {
        case TSyncFileDataType::INT16:
            timeVal1 = csReadValue<qint16>(in, csState);
            break;
        case TSyncFileDataType::INT32:
            timeVal1 = csReadValue<qint32>(in, csState);
            break;
        case TSyncFileDataType::INT64:
            timeVal1 = csReadValue<qint64>(in, csState);
            break;
        case TSyncFileDataType::UINT16:
            timeVal1 = csReadValue<quint16>(in, csState);
            break;
        case TSyncFileDataType::UINT32:
            timeVal1 = csReadValue<quint32>(in, csState);
            break;
        case TSyncFileDataType::UINT64:
            timeVal1 = csReadValue<quint64>(in, csState);
            break;
        default:
            qFatal("Tried to read unknown datatype from timesync file for time1: %i", (int)timeDType1);
            break;
        }

        switch (timeDType2) {
        case TSyncFileDataType::INT16:
            timeVal2 = csReadValue<qint16>(in, csState);
            break;
        case TSyncFileDataType::INT32:
            timeVal2 = csReadValue<qint32>(in, csState);
            break;
        case TSyncFileDataType::INT64:
            timeVal2 = csReadValue<qint64>(in, csState);
            break;
        case TSyncFileDataType::UINT16:
            timeVal2 = csReadValue<quint16>(in, csState);
            break;
        case TSyncFileDataType::UINT32:
            timeVal2 = csReadValue<quint32>(in, csState);
            break;
        case TSyncFileDataType::UINT64:
            timeVal2 = csReadValue<quint64>(in, csState);
            break;
        default:
            qFatal("Tried to read unknown datatype from timesync file for time2: %i", (int)timeDType2);
            break;
        }

        m_times.push_back(std::make_pair(timeVal1, timeVal2));

        bIndex++;
        if (bIndex == m_blockSize) {
            quint64 expectedCRC;
            in >> blockTerm >> expectedCRC;

            if (blockTerm != TSYNC_FILE_BLOCK_TERM) {
                m_lastError = QStringLiteral("Unable to read all tsync data: Block separator was invalid.");
                XXH3_freeState(csState);
                return false;
            }
            if (expectedCRC != XXH3_64bits_digest(csState))
                qCWarning(logTSyncFile).noquote() << "CRC check failed for tsync data block: Data is likely corrupted.";

            XXH3_64bits_reset(csState);
            bIndex = 0;
        }
    }

    XXH3_freeState(csState);
    return true;
}

QString TimeSyncFileReader::lastError() const
{
    return m_lastError;
}

QString TimeSyncFileReader::moduleName() const
{
    return m_moduleName;
}

QUuid TimeSyncFileReader::collectionId() const
{
    return m_collectionId;
}

time_t TimeSyncFileReader::creationTime() const
{
    return m_creationTime;
}

TSyncFileMode TimeSyncFileReader::syncMode() const
{
    return m_tsMode;
}

QVariantHash TimeSyncFileReader::userData() const
{
    return m_userData;
}

microseconds_t TimeSyncFileReader::tolerance() const
{
    return m_tolerance;
}

QPair<QString, QString> TimeSyncFileReader::timeNames() const
{
    return m_timeNames;
}

QPair<TSyncFileTimeUnit, TSyncFileTimeUnit> TimeSyncFileReader::timeUnits() const
{
    return m_timeUnits;
}

QPair<TSyncFileDataType, TSyncFileDataType> TimeSyncFileReader::timeDTypes() const
{
    return m_timeDTypes;
}

std::vector<std::pair<long long, long long>> TimeSyncFileReader::times() const
{
    return m_times;
}

Updated on 2024-11-06 at 17:10:29 +0000