Implemented history writer

This commit is contained in:
Martino Ferrari
2026-06-14 14:09:51 +02:00
parent f25bd7f08e
commit dd7dd22cb0
27 changed files with 1537 additions and 104 deletions
@@ -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 -1
View File
@@ -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 = ../../..
+150
View File
@@ -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_;