Files
MARTe-Integrated-Components/Source/Applications/StreamHub/HistoryWriter.cpp
T
2026-06-14 14:09:51 +02:00

541 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file HistoryWriter.cpp
* @brief Disk-backed circular history storage implementation.
*/
#include "HistoryWriter.h"
#include "AdvancedErrorManagement.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <math.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <fcntl.h>
#include <unistd.h>
namespace StreamHub {
/*---------------------------------------------------------------------------*/
/* Constructor / Destructor */
/*---------------------------------------------------------------------------*/
HistoryWriter::HistoryWriter()
: scratchT_(static_cast<float64 *>(0)),
scratchV_(static_cast<float64 *>(0)),
files_(static_cast<SignalFile (*)[kHistMaxSignals]>(0)),
fileActive_(static_cast<bool (*)[kHistMaxSignals]>(0)),
enabled_(false),
durationHours_(1.0),
decimation_(1u),
flushIntervalSec_(5u),
minDiskFreeMB_(500u),
diskLow_(false) {
directory_[0] = '\0';
files_ = new SignalFile[kHistMaxSessions][kHistMaxSignals];
fileActive_ = new bool[kHistMaxSessions][kHistMaxSignals];
for (uint32 i = 0u; i < kHistMaxSessions; i++) {
for (uint32 s = 0u; s < kHistMaxSignals; s++) {
files_[i][s].fd = -1;
files_[i][s].capacity = 0u;
files_[i][s].head = 0u;
files_[i][s].count = 0u;
files_[i][s].tOldest = 0.0;
files_[i][s].tNewest = 0.0;
files_[i][s].readCursor = 0u;
files_[i][s].decimCounter = 0u;
files_[i][s].headerDirty = false;
files_[i][s].sourceId[0] = '\0';
files_[i][s].signalName[0] = '\0';
fileActive_[i][s] = false;
}
}
}
HistoryWriter::~HistoryWriter() {
/* Flush and close all open files */
for (uint32 i = 0u; i < kHistMaxSessions; i++) {
for (uint32 s = 0u; s < kHistMaxSignals; s++) {
if (files_[i][s].fd >= 0) {
FlushHeader(files_[i][s]);
(void) close(files_[i][s].fd);
files_[i][s].fd = -1;
}
}
}
delete[] scratchT_;
delete[] scratchV_;
delete[] files_;
delete[] fileActive_;
}
/*---------------------------------------------------------------------------*/
/* Initialise */
/*---------------------------------------------------------------------------*/
bool HistoryWriter::Initialise(StructuredDataI &cfg) {
StreamString dir;
if (!cfg.Read("Directory", dir) || dir.Size() == 0u) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"HistoryWriter: no Directory specified, history disabled.");
return false;
}
strncpy(directory_, dir.Buffer(), sizeof(directory_) - 1u);
directory_[sizeof(directory_) - 1u] = '\0';
/* Remove trailing slash */
size_t dLen = strlen(directory_);
if (dLen > 1u && directory_[dLen - 1u] == '/') {
directory_[dLen - 1u] = '\0';
}
float64 durF = 1.0;
if (cfg.Read("DurationHours", durF)) {
durationHours_ = (durF > 0.0) ? durF : 1.0;
}
uint32 tmp = 0u;
if (cfg.Read("Decimation", tmp)) { decimation_ = (tmp > 0u) ? tmp : 1u; }
if (cfg.Read("FlushIntervalSec", tmp)) { flushIntervalSec_ = (tmp > 0u) ? tmp : 5u; }
if (cfg.Read("MinDiskFreeMB", tmp)) { minDiskFreeMB_ = tmp; }
/* Create root directory */
(void) mkdir(directory_, 0755);
/* Allocate scratch buffers */
scratchT_ = new float64[kReadScratch];
scratchV_ = new float64[kReadScratch];
enabled_ = true;
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"HistoryWriter: enabled, dir=%s, duration=%.1fh, decimation=%u, flush=%us, minDisk=%uMB.",
directory_, durationHours_, decimation_, flushIntervalSec_, minDiskFreeMB_);
return true;
}
/*---------------------------------------------------------------------------*/
/* OnSourceConfigured */
/*---------------------------------------------------------------------------*/
void HistoryWriter::OnSourceConfigured(uint32 sessionIdx, const char *sourceId,
UDPSourceSession &sess) {
if (!enabled_ || sessionIdx >= kHistMaxSessions) { return; }
const uint32 numSigs = sess.GetNumSignals();
for (uint32 s = 0u; s < numSigs && s < kHistMaxSignals; s++) {
MARTe::UDPSSignalDescriptor desc;
if (!sess.GetSignalDescriptor(s, desc)) { continue; }
/* Skip time-only signals (type uint64, typically TimeArray) */
if (desc.typeCode == 13u) { continue; } /* uint64 = 13 in UDPS */
float64 estRate = static_cast<float64>(desc.samplingRate);
if (estRate <= 0.0) { estRate = 1000.0; } /* fallback */
if (OpenSignalFile(sessionIdx, s, sourceId, desc.name, estRate)) {
fileActive_[sessionIdx][s] = true;
}
}
}
/*---------------------------------------------------------------------------*/
/* OpenSignalFile */
/*---------------------------------------------------------------------------*/
bool HistoryWriter::OpenSignalFile(uint32 sessionIdx, uint32 sigIdx,
const char *sourceId, const char *signalName,
float64 estimatedRateHz) {
/* Create source subdirectory */
char srcDir[640];
snprintf(srcDir, sizeof(srcDir), "%s/%s", directory_, sourceId);
(void) mkdir(srcDir, 0755);
char filePath[768];
snprintf(filePath, sizeof(filePath), "%s/%s.shist", srcDir, signalName);
/* Calculate capacity: duration × rate / decimation */
float64 totalSamples = durationHours_ * 3600.0 * estimatedRateHz
/ static_cast<float64>(decimation_);
uint32 capacity = static_cast<uint32>(ceil(totalSamples));
if (capacity < 1000u) { capacity = 1000u; }
SignalFile &sf = files_[sessionIdx][sigIdx];
/* Try to reopen an existing file */
int fd = open(filePath, O_RDWR);
if (fd >= 0) {
uint8 hdr[kHistHeaderSize];
ssize_t nr = pread(fd, hdr, kHistHeaderSize, 0);
if (nr == static_cast<ssize_t>(kHistHeaderSize) &&
memcmp(hdr, kHistMagic, 4) == 0) {
/* Read existing header */
uint32 ver = 0u, fileCap = 0u;
memcpy(&ver, &hdr[4], 4);
memcpy(&fileCap, &hdr[8], 4);
if (ver == 1u && fileCap == capacity) {
/* Reuse existing file */
memcpy(&sf.head, &hdr[12], 4);
memcpy(&sf.count, &hdr[16], 4);
memcpy(&sf.tOldest, &hdr[24], 8);
memcpy(&sf.tNewest, &hdr[32], 8);
sf.fd = fd;
sf.capacity = fileCap;
sf.decimCounter = 0u;
sf.readCursor = 0u;
sf.headerDirty = false;
strncpy(sf.sourceId, sourceId, sizeof(sf.sourceId) - 1u);
sf.sourceId[sizeof(sf.sourceId) - 1u] = '\0';
strncpy(sf.signalName, signalName, sizeof(sf.signalName) - 1u);
sf.signalName[sizeof(sf.signalName) - 1u] = '\0';
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"HistoryWriter: reopened %s (cap=%u, count=%u).",
filePath, fileCap, sf.count);
return true;
}
}
/* Capacity changed or corrupt — recreate */
(void) close(fd);
fd = -1;
}
/* Create new file */
fd = open(filePath, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"HistoryWriter: cannot create %s: %s", filePath, strerror(errno));
return false;
}
/* Pre-allocate: header + capacity × 16 bytes */
off_t fileSize = static_cast<off_t>(kHistHeaderSize)
+ static_cast<off_t>(capacity) * 16;
if (ftruncate(fd, fileSize) != 0) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"HistoryWriter: ftruncate %s failed: %s", filePath, strerror(errno));
(void) close(fd);
return false;
}
/* Write header */
uint8 hdr[kHistHeaderSize];
memset(hdr, 0, kHistHeaderSize);
memcpy(&hdr[0], kHistMagic, 4);
uint32 ver = 1u;
memcpy(&hdr[4], &ver, 4);
memcpy(&hdr[8], &capacity, 4);
/* head=0, count=0, decimation, tOldest=0, tNewest=0 */
memcpy(&hdr[20], &decimation_, 4);
(void) pwrite(fd, hdr, kHistHeaderSize, 0);
sf.fd = fd;
sf.capacity = capacity;
sf.head = 0u;
sf.count = 0u;
sf.tOldest = 0.0;
sf.tNewest = 0.0;
sf.readCursor = 0u;
sf.decimCounter = 0u;
sf.headerDirty = false;
strncpy(sf.sourceId, sourceId, sizeof(sf.sourceId) - 1u);
sf.sourceId[sizeof(sf.sourceId) - 1u] = '\0';
strncpy(sf.signalName, signalName, sizeof(sf.signalName) - 1u);
sf.signalName[sizeof(sf.signalName) - 1u] = '\0';
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"HistoryWriter: created %s (cap=%u, %.1f MB).",
filePath, capacity,
static_cast<double>(fileSize) / (1024.0 * 1024.0));
return true;
}
/*---------------------------------------------------------------------------*/
/* WriteTick */
/*---------------------------------------------------------------------------*/
void HistoryWriter::WriteTick(uint32 sessionIdx, UDPSourceSession &sess) {
if (!enabled_ || sessionIdx >= kHistMaxSessions || diskLow_) { return; }
const uint32 numSigs = sess.GetNumSignals();
for (uint32 s = 0u; s < numSigs && s < kHistMaxSignals; s++) {
if (!fileActive_[sessionIdx][s]) { continue; }
SignalFile &sf = files_[sessionIdx][s];
if (sf.fd < 0) { continue; }
/* Read new samples from the in-memory ring since last tick */
uint32 nRaw = sess.ReadSignalSince(s, sf.readCursor,
scratchT_, scratchV_, kReadScratch);
if (nRaw == 0u) { continue; }
if (decimation_ <= 1u) {
WritePairs(sf, scratchT_, scratchV_, nRaw);
} else {
/* Apply decimation: take every Nth sample */
uint32 nOut = 0u;
for (uint32 i = 0u; i < nRaw; i++) {
sf.decimCounter++;
if (sf.decimCounter >= static_cast<uint64>(decimation_)) {
scratchT_[nOut] = scratchT_[i];
scratchV_[nOut] = scratchV_[i];
nOut++;
sf.decimCounter = 0u;
}
}
if (nOut > 0u) {
WritePairs(sf, scratchT_, scratchV_, nOut);
}
}
}
}
/*---------------------------------------------------------------------------*/
/* WritePairs */
/*---------------------------------------------------------------------------*/
void HistoryWriter::WritePairs(SignalFile &sf, const float64 *t,
const float64 *v, uint32 n) {
for (uint32 i = 0u; i < n; i++) {
/* Write one (t,v) pair at the current head position */
off_t offset = static_cast<off_t>(kHistHeaderSize)
+ static_cast<off_t>(sf.head) * 16;
float64 pair[2] = { t[i], v[i] };
(void) pwrite(sf.fd, pair, 16, offset);
sf.head = (sf.head + 1u) % sf.capacity;
if (sf.count < sf.capacity) {
sf.count++;
}
}
/* Update time bounds */
sf.tNewest = t[n - 1u];
if (sf.count <= n) {
sf.tOldest = t[0];
} else {
/* Read the oldest entry's time from disk */
uint32 oldestIdx = (sf.head + sf.capacity - sf.count) % sf.capacity;
off_t offset = static_cast<off_t>(kHistHeaderSize)
+ static_cast<off_t>(oldestIdx) * 16;
float64 oldestT = 0.0;
(void) pread(sf.fd, &oldestT, 8, offset);
sf.tOldest = oldestT;
}
sf.headerDirty = true;
}
/*---------------------------------------------------------------------------*/
/* FlushHeaders */
/*---------------------------------------------------------------------------*/
void HistoryWriter::FlushHeaders() {
if (!enabled_) { return; }
/* Periodic disk space check */
diskLow_ = !HasSufficientDisk();
if (diskLow_) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"HistoryWriter: disk space below %u MB, writing paused.", minDiskFreeMB_);
}
for (uint32 i = 0u; i < kHistMaxSessions; i++) {
for (uint32 s = 0u; s < kHistMaxSignals; s++) {
if (!fileActive_[i][s]) { continue; }
SignalFile &sf = files_[i][s];
if (sf.fd >= 0 && sf.headerDirty) {
FlushHeader(sf);
}
}
}
}
void HistoryWriter::FlushHeader(SignalFile &sf) {
uint8 hdr[kHistHeaderSize];
memset(hdr, 0, kHistHeaderSize);
memcpy(&hdr[0], kHistMagic, 4);
uint32 ver = 1u;
memcpy(&hdr[4], &ver, 4);
memcpy(&hdr[8], &sf.capacity, 4);
memcpy(&hdr[12], &sf.head, 4);
memcpy(&hdr[16], &sf.count, 4);
memcpy(&hdr[20], &decimation_, 4);
memcpy(&hdr[24], &sf.tOldest, 8);
memcpy(&hdr[32], &sf.tNewest, 8);
(void) pwrite(sf.fd, hdr, kHistHeaderSize, 0);
(void) fdatasync(sf.fd);
sf.headerDirty = false;
}
/*---------------------------------------------------------------------------*/
/* ReadRange */
/*---------------------------------------------------------------------------*/
uint32 HistoryWriter::ReadRange(const char *sourceId, const char *signalName,
float64 t0, float64 t1,
float64 *tOut, float64 *vOut,
uint32 maxOut) const {
const SignalFile *sf = FindFile(sourceId, signalName);
if (sf == static_cast<const SignalFile *>(0) || sf->fd < 0 ||
sf->count == 0u || t1 < t0) {
return 0u;
}
const uint32 fileCap = sf->capacity;
const uint32 cnt = sf->count;
const uint32 oldest = (sf->head + fileCap - cnt) % fileCap;
const int fd = sf->fd;
/* Binary search over the logical order (same algorithm as SignalRingBuffer).
* We read individual timestamps from disk using pread. */
/* lo = first logical index with t >= t0 */
uint32 lo = 0u;
{
uint32 a = 0u, b = cnt;
while (a < b) {
const uint32 mid = a + ((b - a) >> 1);
const uint32 phys = (oldest + mid) % fileCap;
float64 tv = 0.0;
(void) pread(fd, &tv, 8,
static_cast<off_t>(kHistHeaderSize) + static_cast<off_t>(phys) * 16);
if (tv < t0) { a = mid + 1u; } else { b = mid; }
}
lo = a;
}
/* hi = first logical index with t > t1 */
uint32 hi = lo;
{
uint32 a = lo, b = cnt;
while (a < b) {
const uint32 mid = a + ((b - a) >> 1);
const uint32 phys = (oldest + mid) % fileCap;
float64 tv = 0.0;
(void) pread(fd, &tv, 8,
static_cast<off_t>(kHistHeaderSize) + static_cast<off_t>(phys) * 16);
if (tv <= t1) { a = mid + 1u; } else { b = mid; }
}
hi = a;
}
uint32 nOut = hi - lo;
if (nOut > maxOut) { nOut = maxOut; }
/* Read pairs from disk */
for (uint32 i = 0u; i < nOut; i++) {
uint32 physIdx = (oldest + lo + i) % fileCap;
off_t fileOff = static_cast<off_t>(kHistHeaderSize)
+ static_cast<off_t>(physIdx) * 16;
float64 pair[2];
(void) pread(fd, pair, 16, fileOff);
tOut[i] = pair[0];
vOut[i] = pair[1];
}
return nOut;
}
/*---------------------------------------------------------------------------*/
/* GetTimeRange */
/*---------------------------------------------------------------------------*/
bool HistoryWriter::GetTimeRange(const char *sourceId, const char *signalName,
float64 &t0, float64 &t1) const {
const SignalFile *sf = FindFile(sourceId, signalName);
if (sf == static_cast<const SignalFile *>(0) || sf->count == 0u) {
return false;
}
t0 = sf->tOldest;
t1 = sf->tNewest;
return true;
}
/*---------------------------------------------------------------------------*/
/* AppendInfoJSON */
/*---------------------------------------------------------------------------*/
/* Forward-declare JsonAppendf from StreamHub.cpp — we use the same pattern */
static bool HistJsonAppendf(char *&buf, uint32 &len, uint32 &cap,
const char *fmt, ...) {
for (;;) {
va_list ap;
va_start(ap, fmt);
const int wrote = vsnprintf(buf + len, cap - len, fmt, ap);
va_end(ap);
if (wrote < 0) { return false; }
if (static_cast<uint32>(wrote) < (cap - len)) {
len += static_cast<uint32>(wrote);
return true;
}
uint32 newCap = cap * 2u;
while ((newCap - len) <= static_cast<uint32>(wrote)) {
newCap *= 2u;
}
char *nb = new char[newCap];
memcpy(nb, buf, len);
delete[] buf;
buf = nb;
cap = newCap;
}
}
void HistoryWriter::AppendInfoJSON(char *&buf, uint32 &off, uint32 &cap) const {
HistJsonAppendf(buf, off, cap,
"\"enabled\":%s,\"durationHours\":%.2f,\"decimation\":%u,\"signals\":{",
enabled_ ? "true" : "false", durationHours_, decimation_);
bool first = true;
for (uint32 i = 0u; i < kHistMaxSessions; i++) {
for (uint32 s = 0u; s < kHistMaxSignals; s++) {
if (!fileActive_[i][s]) { continue; }
const SignalFile &sf = files_[i][s];
if (sf.count == 0u) { continue; }
HistJsonAppendf(buf, off, cap,
"%s\"%s:%s\":{\"t0\":%.17g,\"t1\":%.17g,\"count\":%u,\"capacity\":%u}",
(first ? "" : ","),
sf.sourceId, sf.signalName,
sf.tOldest, sf.tNewest, sf.count, sf.capacity);
first = false;
}
}
HistJsonAppendf(buf, off, cap, "}");
}
/*---------------------------------------------------------------------------*/
/* HasSufficientDisk */
/*---------------------------------------------------------------------------*/
bool HistoryWriter::HasSufficientDisk() const {
if (minDiskFreeMB_ == 0u) { return true; }
struct statvfs st;
if (statvfs(directory_, &st) != 0) { return true; /* assume OK */ }
uint64 freeMB = (static_cast<uint64>(st.f_bavail) * st.f_frsize)
/ (1024u * 1024u);
return freeMB >= static_cast<uint64>(minDiskFreeMB_);
}
/*---------------------------------------------------------------------------*/
/* FindFile */
/*---------------------------------------------------------------------------*/
const HistoryWriter::SignalFile* HistoryWriter::FindFile(
const char *sourceId, const char *signalName) const {
for (uint32 i = 0u; i < kHistMaxSessions; i++) {
for (uint32 s = 0u; s < kHistMaxSignals; s++) {
if (!fileActive_[i][s]) { continue; }
const SignalFile &sf = files_[i][s];
if (strcmp(sf.sourceId, sourceId) == 0 &&
strcmp(sf.signalName, signalName) == 0) {
return &sf;
}
}
}
return static_cast<const SignalFile *>(0);
}
} /* namespace StreamHub */