Implemented history writer
This commit is contained in:
@@ -0,0 +1,540 @@
|
||||
/**
|
||||
* @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 */
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* @file HistoryWriter.h
|
||||
* @brief Disk-backed circular history storage for StreamHub signals.
|
||||
*
|
||||
* Each signal is stored in a separate binary file (.shist) with a fixed header
|
||||
* and a pre-allocated circular data region of (float64 time, float64 value) pairs.
|
||||
* The file size is determined at creation and never grows.
|
||||
*
|
||||
* File layout (per signal):
|
||||
* Offset Size Field
|
||||
* 0 4 Magic: "SHR1"
|
||||
* 4 4 uint32 version (1)
|
||||
* 8 4 uint32 capacity (max pairs)
|
||||
* 12 4 uint32 head (next write position, 0-based, wraps)
|
||||
* 16 4 uint32 count (valid entries, <= capacity)
|
||||
* 20 4 uint32 decimation
|
||||
* 24 8 float64 tOldest
|
||||
* 32 8 float64 tNewest
|
||||
* 40 24 reserved (pad to 64 bytes)
|
||||
* 64 ... data: capacity × 16 bytes (8 time + 8 value)
|
||||
*
|
||||
* Thread safety: WriteTick() is called from the push loop only.
|
||||
* ReadRange() uses pread() and is safe for concurrent reads from WS threads.
|
||||
*/
|
||||
|
||||
#ifndef STREAMHUB_HISTORY_WRITER_H_
|
||||
#define STREAMHUB_HISTORY_WRITER_H_
|
||||
|
||||
#include "CompilerTypes.h"
|
||||
#include "StreamString.h"
|
||||
#include "StructuredDataI.h"
|
||||
#include "UDPSourceSession.h"
|
||||
|
||||
namespace StreamHub {
|
||||
|
||||
using MARTe::uint8;
|
||||
using MARTe::uint16;
|
||||
using MARTe::uint32;
|
||||
using MARTe::uint64;
|
||||
using MARTe::float64;
|
||||
using MARTe::StreamString;
|
||||
using MARTe::StructuredDataI;
|
||||
|
||||
/** Maximum number of sessions (matches kMaxSessions). */
|
||||
static const uint32 kHistMaxSessions = 32u;
|
||||
|
||||
/** Header size in bytes. */
|
||||
static const uint32 kHistHeaderSize = 64u;
|
||||
|
||||
/** Magic bytes. */
|
||||
static const char kHistMagic[4] = {'S','H','R','1'};
|
||||
|
||||
class HistoryWriter {
|
||||
public:
|
||||
|
||||
HistoryWriter();
|
||||
~HistoryWriter();
|
||||
|
||||
/**
|
||||
* @brief Parse +History block from StreamHub config.
|
||||
*
|
||||
* Expected keys inside a +History node:
|
||||
* Directory (string, required)
|
||||
* DurationHours (float64, default 1)
|
||||
* Decimation (uint32, default 1)
|
||||
* FlushIntervalSec (uint32, default 5)
|
||||
* MinDiskFreeMB (uint32, default 500)
|
||||
*/
|
||||
bool Initialise(StructuredDataI &cfg);
|
||||
|
||||
/** @return true if history storage is enabled and initialised. */
|
||||
bool IsEnabled() const { return enabled_; }
|
||||
|
||||
/**
|
||||
* @brief Called when a source becomes configured.
|
||||
* Creates / reopens per-signal .shist files.
|
||||
*/
|
||||
void OnSourceConfigured(uint32 sessionIdx, const char *sourceId,
|
||||
UDPSourceSession &sess);
|
||||
|
||||
/**
|
||||
* @brief Called from the push loop to write new samples for one session.
|
||||
* Uses per-signal ReadSince cursors. Applies decimation.
|
||||
*/
|
||||
void WriteTick(uint32 sessionIdx, UDPSourceSession &sess);
|
||||
|
||||
/**
|
||||
* @brief Flush in-memory header state to disk (periodic).
|
||||
*/
|
||||
void FlushHeaders();
|
||||
|
||||
/**
|
||||
* @brief Read a time range for one signal from disk.
|
||||
* @return Number of (t,v) pairs written to tOut/vOut.
|
||||
*/
|
||||
uint32 ReadRange(const char *sourceId, const char *signalName,
|
||||
float64 t0, float64 t1,
|
||||
float64 *tOut, float64 *vOut, uint32 maxOut) const;
|
||||
|
||||
/**
|
||||
* @brief Get the time range stored on disk for a signal.
|
||||
* @return true if the signal has history data.
|
||||
*/
|
||||
bool GetTimeRange(const char *sourceId, const char *signalName,
|
||||
float64 &t0, float64 &t1) const;
|
||||
|
||||
/**
|
||||
* @brief Build a JSON fragment for historyInfo response.
|
||||
* Writes into buf at offset, growing via the provided append function.
|
||||
*/
|
||||
void AppendInfoJSON(char *&buf, uint32 &off, uint32 &cap) const;
|
||||
|
||||
/** @brief Check available disk space. */
|
||||
bool HasSufficientDisk() const;
|
||||
|
||||
/** @brief Duration in hours. */
|
||||
float64 DurationHours() const { return durationHours_; }
|
||||
|
||||
/** @brief Decimation factor. */
|
||||
uint32 Decimation() const { return decimation_; }
|
||||
|
||||
private:
|
||||
|
||||
struct SignalFile {
|
||||
int fd; /**< File descriptor (-1 = not open) */
|
||||
uint32 capacity; /**< Total capacity in pairs */
|
||||
uint32 head; /**< Next write position */
|
||||
uint32 count; /**< Valid entries */
|
||||
float64 tOldest;
|
||||
float64 tNewest;
|
||||
uint64 readCursor; /**< For ReadSince from ring buffer */
|
||||
uint64 decimCounter; /**< Decimation sample counter */
|
||||
bool headerDirty; /**< True if header needs flushing */
|
||||
char sourceId[64]; /**< Source ID for lookup */
|
||||
char signalName[64]; /**< Signal name for lookup */
|
||||
};
|
||||
|
||||
/** Per-signal scratch for reading from rings. */
|
||||
static const uint32 kReadScratch = 65536u;
|
||||
float64 *scratchT_;
|
||||
float64 *scratchV_;
|
||||
|
||||
/** Maximum signals tracked per session for history. */
|
||||
static const uint32 kHistMaxSignals = 64u;
|
||||
|
||||
/** Heap-allocated to keep StreamHub object size manageable. */
|
||||
SignalFile (*files_)[kHistMaxSignals]; /**< [kHistMaxSessions][kHistMaxSignals] */
|
||||
bool (*fileActive_)[kHistMaxSignals]; /**< [kHistMaxSessions][kHistMaxSignals] */
|
||||
|
||||
bool enabled_;
|
||||
char directory_[512];
|
||||
float64 durationHours_;
|
||||
uint32 decimation_;
|
||||
uint32 flushIntervalSec_;
|
||||
uint32 minDiskFreeMB_;
|
||||
bool diskLow_; /**< True when disk space is below threshold */
|
||||
|
||||
/** Create or reopen a .shist file for one signal. */
|
||||
bool OpenSignalFile(uint32 sessionIdx, uint32 sigIdx,
|
||||
const char *sourceId, const char *signalName,
|
||||
float64 estimatedRateHz);
|
||||
|
||||
/** Write a batch of (t,v) pairs to a signal file. */
|
||||
void WritePairs(SignalFile &sf, const float64 *t, const float64 *v,
|
||||
uint32 n);
|
||||
|
||||
/** Flush one signal file header to disk. */
|
||||
void FlushHeader(SignalFile &sf);
|
||||
|
||||
/** Find a signal file by sourceId:signalName. */
|
||||
const SignalFile* FindFile(const char *sourceId, const char *signalName) const;
|
||||
};
|
||||
|
||||
} /* namespace StreamHub */
|
||||
|
||||
#endif /* STREAMHUB_HISTORY_WRITER_H_ */
|
||||
@@ -1,4 +1,4 @@
|
||||
OBJSX = StreamHub.x UDPSourceSession.x WSServer.x TriggerEngine.x
|
||||
OBJSX = StreamHub.x UDPSourceSession.x WSServer.x TriggerEngine.x HistoryWriter.x
|
||||
|
||||
PACKAGE =
|
||||
ROOT_DIR = ../../..
|
||||
|
||||
@@ -130,6 +130,13 @@ bool StreamHub::Initialise(StructuredDataI &cfg) {
|
||||
StreamString sf;
|
||||
if (cfg.Read("SourcesFile", sf)) { sourcesFile_ = sf; }
|
||||
|
||||
/* Parse +History block (optional).
|
||||
* StandardParser stores the '+' prefix in the node name, so we try both. */
|
||||
if (cfg.MoveRelative("+History") || cfg.MoveRelative("History")) {
|
||||
history_.Initialise(cfg);
|
||||
cfg.MoveToAncestor(1u);
|
||||
}
|
||||
|
||||
/* Allocate scratch buffers */
|
||||
pushBuf_ = new uint8[kPushBufSize];
|
||||
lttbT_ = new float64[maxPushPoints_];
|
||||
@@ -248,6 +255,16 @@ bool StreamHub::Run() {
|
||||
PushStats();
|
||||
}
|
||||
|
||||
/* History: flush headers periodically */
|
||||
if (history_.IsEnabled()) {
|
||||
uint32 histFlushDiv = history_.Decimation() > 0u
|
||||
? pushRateHz_ * 5u : pushRateHz_ * 5u; /* every 5s */
|
||||
if (histFlushDiv == 0u) { histFlushDiv = 1u; }
|
||||
if ((tickCount_ % histFlushDiv) == 0u) {
|
||||
history_.FlushHeaders();
|
||||
}
|
||||
}
|
||||
|
||||
tickCount_++;
|
||||
|
||||
/* Sleep for remainder of period */
|
||||
@@ -305,6 +322,12 @@ void StreamHub::PushData() {
|
||||
}
|
||||
BroadcastSources();
|
||||
BroadcastConfig(i);
|
||||
|
||||
/* Open history files for this source */
|
||||
if (history_.IsEnabled()) {
|
||||
StreamString sid = sessions_[i].GetId();
|
||||
history_.OnSourceConfigured(i, sid.Buffer(), sessions_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Always serialize (advancing the per-signal cursors), even with no
|
||||
@@ -313,6 +336,11 @@ void StreamHub::PushData() {
|
||||
if (frameLen > 0u) {
|
||||
wsServer_.BroadcastBinary(pushBuf_, frameLen);
|
||||
}
|
||||
|
||||
/* Write to disk history (separate read cursors, own decimation) */
|
||||
if (history_.IsEnabled()) {
|
||||
history_.WriteTick(i, sessions_[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,6 +635,19 @@ void StreamHub::OnWSClientConnected() {
|
||||
|
||||
/* Let the new client render the current trigger badge/buttons. */
|
||||
BroadcastTriggerState();
|
||||
|
||||
/* Inform the new client about history availability. */
|
||||
if (history_.IsEnabled()) {
|
||||
/* Broadcast to all (simple; no unicast-on-connect path for broadcast). */
|
||||
uint32 cap2 = 8192u;
|
||||
char *hbuf = new char[cap2];
|
||||
uint32 hoff = 0u;
|
||||
JsonAppendf(hbuf, hoff, cap2, "{\"type\":\"historyInfo\",");
|
||||
history_.AppendInfoJSON(hbuf, hoff, cap2);
|
||||
JsonAppendf(hbuf, hoff, cap2, "}");
|
||||
wsServer_.BroadcastText(hbuf, hoff);
|
||||
delete[] hbuf;
|
||||
}
|
||||
}
|
||||
|
||||
void StreamHub::OnWSClientDisconnected() {
|
||||
@@ -631,6 +672,8 @@ void StreamHub::OnWSCommand(const char *json, uint32 /*len*/, uint32 slotIdx) {
|
||||
else if (strcmp(type, "trigStop") == 0) { HandleTrigStop(json); }
|
||||
else if (strcmp(type, "setTrigger") == 0) { HandleSetTrigger(json); }
|
||||
else if (strcmp(type, "zoom") == 0) { HandleZoom(json, slotIdx); }
|
||||
else if (strcmp(type, "historyZoom") == 0) { HandleHistoryZoom(json, slotIdx); }
|
||||
else if (strcmp(type, "historyInfo") == 0) { HandleHistoryInfo(slotIdx); }
|
||||
else if (strcmp(type, "setMaxPoints") == 0) { HandleSetMaxPoints(json); }
|
||||
else if (strcmp(type, "ping") == 0) { HandlePing(slotIdx); }
|
||||
}
|
||||
@@ -1225,6 +1268,113 @@ void StreamHub::HandleZoom(const char *json, uint32 slotIdx) {
|
||||
delete[] tDec; delete[] vDec;
|
||||
}
|
||||
|
||||
void StreamHub::HandleHistoryZoom(const char *json, uint32 slotIdx) {
|
||||
if (!history_.IsEnabled()) {
|
||||
const char *msg = "{\"type\":\"historyZoom\",\"error\":\"history not enabled\"}";
|
||||
wsServer_.SendText(slotIdx, msg, static_cast<uint32>(strlen(msg)));
|
||||
return;
|
||||
}
|
||||
|
||||
float64 t0 = 0.0, t1 = 0.0;
|
||||
float64 nF = 2400.0;
|
||||
float64 reqIdF = 0.0;
|
||||
|
||||
JsonGetFloat(json, "t0", t0);
|
||||
JsonGetFloat(json, "t1", t1);
|
||||
JsonGetFloat(json, "reqId", reqIdF);
|
||||
const bool haveN = JsonGetFloat(json, "n", nF);
|
||||
|
||||
uint32 maxOut = 2400u;
|
||||
if (haveN) {
|
||||
if (nF <= 0.0) { maxOut = 0u; }
|
||||
else if (nF < 10.0) { maxOut = 2400u; }
|
||||
else { maxOut = static_cast<uint32>(nF); }
|
||||
}
|
||||
|
||||
/* Parse comma-separated full keys "src:sig" */
|
||||
static const uint32 kKeysBuf = 8192u;
|
||||
char *keys = new char[kKeysBuf];
|
||||
keys[0] = '\0';
|
||||
JsonGetString(json, "signals", keys, kKeysBuf);
|
||||
|
||||
/* Allocate scratch for the largest possible read */
|
||||
const uint32 scratchCap = 2000000u; /* 2M pts should be enough */
|
||||
float64 *tRaw = new float64[scratchCap];
|
||||
float64 *vRaw = new float64[scratchCap];
|
||||
float64 *tDec = (maxOut > 0u) ? new float64[maxOut] : static_cast<float64 *>(0);
|
||||
float64 *vDec = (maxOut > 0u) ? new float64[maxOut] : static_cast<float64 *>(0);
|
||||
|
||||
uint32 cap = 65536u;
|
||||
char *buf = new char[cap];
|
||||
uint32 off = 0u;
|
||||
|
||||
JsonAppendf(buf, off, cap, "{\"type\":\"historyZoom\",\"reqId\":%u,\"signals\":{",
|
||||
static_cast<uint32>(reqIdF));
|
||||
|
||||
bool first = true;
|
||||
char *tok = keys;
|
||||
while ((tok != static_cast<char *>(0)) && (*tok != '\0') && (t1 > t0)) {
|
||||
char *next = strchr(tok, ',');
|
||||
if (next != static_cast<char *>(0)) { *next = '\0'; next++; }
|
||||
while (*tok == ' ') { tok++; }
|
||||
|
||||
char *colon = strchr(tok, ':');
|
||||
if (colon == static_cast<char *>(0)) { tok = next; continue; }
|
||||
*colon = '\0';
|
||||
const char *srcId = tok;
|
||||
const char *sigName = colon + 1;
|
||||
|
||||
uint32 nRaw = history_.ReadRange(srcId, sigName, t0, t1,
|
||||
tRaw, vRaw, scratchCap);
|
||||
if (nRaw > 0u) {
|
||||
const float64 *tOut = tRaw;
|
||||
const float64 *vOut = vRaw;
|
||||
uint32 nOut = nRaw;
|
||||
if ((maxOut > 0u) && (nRaw > maxOut)) {
|
||||
nOut = LTTBDecimate(tRaw, vRaw, nRaw, tDec, vDec, maxOut);
|
||||
tOut = tDec;
|
||||
vOut = vDec;
|
||||
}
|
||||
|
||||
JsonAppendf(buf, off, cap, "%s\"%s:%s\":{\"t\":[",
|
||||
(first ? "" : ","), srcId, sigName);
|
||||
first = false;
|
||||
for (uint32 p = 0u; p < nOut; p++) {
|
||||
JsonAppendf(buf, off, cap, "%s%.17g",
|
||||
(p > 0u ? "," : ""), tOut[p]);
|
||||
}
|
||||
JsonAppendf(buf, off, cap, "],\"v\":[");
|
||||
for (uint32 p = 0u; p < nOut; p++) {
|
||||
JsonAppendf(buf, off, cap, "%s%.9g",
|
||||
(p > 0u ? "," : ""), vOut[p]);
|
||||
}
|
||||
JsonAppendf(buf, off, cap, "]}");
|
||||
}
|
||||
tok = next;
|
||||
}
|
||||
|
||||
JsonAppendf(buf, off, cap, "}}");
|
||||
wsServer_.SendText(slotIdx, buf, off);
|
||||
|
||||
delete[] buf;
|
||||
delete[] keys;
|
||||
delete[] tRaw; delete[] vRaw;
|
||||
delete[] tDec; delete[] vDec;
|
||||
}
|
||||
|
||||
void StreamHub::HandleHistoryInfo(uint32 slotIdx) {
|
||||
uint32 cap = 8192u;
|
||||
char *buf = new char[cap];
|
||||
uint32 off = 0u;
|
||||
|
||||
JsonAppendf(buf, off, cap, "{\"type\":\"historyInfo\",");
|
||||
history_.AppendInfoJSON(buf, off, cap);
|
||||
JsonAppendf(buf, off, cap, "}");
|
||||
|
||||
wsServer_.SendText(slotIdx, buf, off);
|
||||
delete[] buf;
|
||||
}
|
||||
|
||||
void StreamHub::HandleSetMaxPoints(const char *json) {
|
||||
float64 mpF = static_cast<float64>(maxPoints_);
|
||||
JsonGetFloat(json, "maxPoints", mpF);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "UDPSourceSession.h"
|
||||
#include "WSServer.h"
|
||||
#include "TriggerEngine.h"
|
||||
#include "HistoryWriter.h"
|
||||
|
||||
namespace StreamHub {
|
||||
|
||||
@@ -140,6 +141,8 @@ private:
|
||||
void HandleTrigStop(const char *json);
|
||||
void HandleSetTrigger(const char *json);
|
||||
void HandleZoom(const char *json, uint32 slotIdx);
|
||||
void HandleHistoryZoom(const char *json, uint32 slotIdx);
|
||||
void HandleHistoryInfo(uint32 slotIdx);
|
||||
void HandleSetMaxPoints(const char *json);
|
||||
void HandlePing(uint32 slotIdx);
|
||||
|
||||
@@ -186,6 +189,7 @@ private:
|
||||
|
||||
WSServer wsServer_;
|
||||
TriggerEngine trigger_;
|
||||
HistoryWriter history_;
|
||||
|
||||
/* Configuration */
|
||||
uint16 wsPort_;
|
||||
|
||||
Reference in New Issue
Block a user