StreamHub: fix calibration trim, UTF-8 unit truncation, and sort order
Finding 1: Add TrimInPlace helper; apply trim→strip-[i]→empty-check order in SetCalibrationEntry (matching Go Normalise()), so whitespace-padded source/signal from WS clients normalise identically to config-file loads. Finding 2: Trim unit before truncating to kMaxUnitLen, then walk back continuation bytes (0x80-0xBF) to avoid leaving a partial UTF-8 rune, matching Go's utf8.DecodeLastRuneInString loop. Finding 3: BroadcastCalibration and HandleSaveSources now emit entries sorted by source then signal (insertion sort over an index array, no STL), producing byte-identical calibration frames and config files to the Go hub. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
cdafb877a3
commit
957be793ae
@@ -703,22 +703,88 @@ void StreamHub::BroadcastConfig(uint32 idx) {
|
|||||||
/* Calibration store */
|
/* Calibration store */
|
||||||
/*---------------------------------------------------------------------------*/
|
/*---------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/* In-place trim of leading and trailing ASCII whitespace in a char buffer.
|
||||||
|
* Returns a pointer to the first non-space character (which remains in buf). */
|
||||||
|
static void TrimInPlace(char *buf) {
|
||||||
|
if (buf == static_cast<char *>(0)) { return; }
|
||||||
|
/* Trim leading */
|
||||||
|
char *p = buf;
|
||||||
|
while ((*p == ' ') || (*p == '\t') || (*p == '\n') || (*p == '\r')) { p++; }
|
||||||
|
if (p != buf) {
|
||||||
|
uint32 i = 0u;
|
||||||
|
while (p[i] != '\0') { buf[i] = p[i]; i++; }
|
||||||
|
buf[i] = '\0';
|
||||||
|
}
|
||||||
|
/* Trim trailing */
|
||||||
|
uint32 len = static_cast<uint32>(strlen(buf));
|
||||||
|
while (len > 0u) {
|
||||||
|
char c = buf[len - 1u];
|
||||||
|
if ((c == ' ') || (c == '\t') || (c == '\n') || (c == '\r')) {
|
||||||
|
buf[--len] = '\0';
|
||||||
|
}
|
||||||
|
else { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool StreamHub::SetCalibrationEntry(const char *source, const char *signal,
|
bool StreamHub::SetCalibrationEntry(const char *source, const char *signal,
|
||||||
float64 scale, float64 offset,
|
float64 scale, float64 offset,
|
||||||
const char *unit) {
|
const char *unit) {
|
||||||
if ((source == static_cast<const char *>(0)) || (source[0] == '\0')) { return false; }
|
if (source == static_cast<const char *>(0)) { return false; }
|
||||||
if ((signal == static_cast<const char *>(0)) || (signal[0] == '\0')) { return false; }
|
if (signal == static_cast<const char *>(0)) { return false; }
|
||||||
|
|
||||||
|
/* Go Normalise() order: trim → strip trailing "[digits]" → reject if empty. */
|
||||||
|
char src[128];
|
||||||
|
char sig[128];
|
||||||
|
strncpy(src, source, sizeof(src) - 1u);
|
||||||
|
src[sizeof(src) - 1u] = '\0';
|
||||||
|
strncpy(sig, signal, sizeof(sig) - 1u);
|
||||||
|
sig[sizeof(sig) - 1u] = '\0';
|
||||||
|
|
||||||
|
TrimInPlace(src);
|
||||||
|
TrimInPlace(sig);
|
||||||
|
|
||||||
|
/* Strip trailing "[i]" array-element suffix from signal, matching Go and JS. */
|
||||||
|
char *br = strchr(sig, '[');
|
||||||
|
if (br != static_cast<char *>(0)) { *br = '\0'; }
|
||||||
|
|
||||||
|
if (src[0] == '\0') { return false; }
|
||||||
|
if (sig[0] == '\0') { return false; }
|
||||||
|
|
||||||
/* A zero or non-finite scale makes the calibration non-invertible, which
|
/* A zero or non-finite scale makes the calibration non-invertible, which
|
||||||
* the SPA's trigger-threshold conversion depends on. */
|
* the SPA's trigger-threshold conversion depends on. */
|
||||||
if (!JsonIsFinite(scale) || (scale == 0.0)) { return false; }
|
if (!JsonIsFinite(scale) || (scale == 0.0)) { return false; }
|
||||||
if (!JsonIsFinite(offset)) { return false; }
|
if (!JsonIsFinite(offset)) { return false; }
|
||||||
|
|
||||||
|
/* Trim unit, then truncate to kMaxUnitLen bytes, walking back any
|
||||||
|
* partial UTF-8 rune to keep the stored bytes valid UTF-8 (Go parity). */
|
||||||
char u[kMaxUnitLen + 1u];
|
char u[kMaxUnitLen + 1u];
|
||||||
u[0] = '\0';
|
u[0] = '\0';
|
||||||
if (unit != static_cast<const char *>(0)) {
|
if (unit != static_cast<const char *>(0)) {
|
||||||
strncpy(u, unit, kMaxUnitLen);
|
strncpy(u, unit, sizeof(u) - 1u);
|
||||||
|
u[sizeof(u) - 1u] = '\0';
|
||||||
|
TrimInPlace(u);
|
||||||
|
/* Truncate to kMaxUnitLen bytes */
|
||||||
|
if (strlen(u) > kMaxUnitLen) {
|
||||||
u[kMaxUnitLen] = '\0';
|
u[kMaxUnitLen] = '\0';
|
||||||
}
|
}
|
||||||
|
/* Walk back any trailing partial UTF-8 rune. A byte b is a
|
||||||
|
* continuation byte (10xxxxxx) iff (b & 0xC0) == 0x80. A truncation
|
||||||
|
* may leave a sequence starter with fewer continuation bytes than it
|
||||||
|
* expects; drop bytes from the end while the last byte is a lone
|
||||||
|
* continuation byte that decodes as an invalid (RuneError, 1) pair.
|
||||||
|
* Concrete: if the last byte is 0x80-0xBF (continuation), remove it,
|
||||||
|
* then repeat — this matches Go's utf8.DecodeLastRuneInString loop. */
|
||||||
|
uint32 ulen = static_cast<uint32>(strlen(u));
|
||||||
|
while (ulen > 0u) {
|
||||||
|
const unsigned char last = static_cast<unsigned char>(u[ulen - 1u]);
|
||||||
|
/* Is it a UTF-8 continuation byte (10xxxxxx)? */
|
||||||
|
if ((last & 0xC0u) == 0x80u) {
|
||||||
|
u[--ulen] = '\0';
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* An identity entry carries no information: drop it rather than store and
|
/* An identity entry carries no information: drop it rather than store and
|
||||||
* persist it. */
|
* persist it. */
|
||||||
@@ -727,8 +793,8 @@ bool StreamHub::SetCalibrationEntry(const char *source, const char *signal,
|
|||||||
(void) calibrationMutex_.FastLock();
|
(void) calibrationMutex_.FastLock();
|
||||||
uint32 found = kMaxCalibration;
|
uint32 found = kMaxCalibration;
|
||||||
for (uint32 i = 0u; i < numCalibration_; i++) {
|
for (uint32 i = 0u; i < numCalibration_; i++) {
|
||||||
if ((strcmp(calibration_[i].source, source) == 0) &&
|
if ((strcmp(calibration_[i].source, src) == 0) &&
|
||||||
(strcmp(calibration_[i].signal, signal) == 0)) {
|
(strcmp(calibration_[i].signal, sig) == 0)) {
|
||||||
found = i;
|
found = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -750,9 +816,9 @@ bool StreamHub::SetCalibrationEntry(const char *source, const char *signal,
|
|||||||
found = numCalibration_;
|
found = numCalibration_;
|
||||||
numCalibration_++;
|
numCalibration_++;
|
||||||
}
|
}
|
||||||
strncpy(calibration_[found].source, source, sizeof(calibration_[found].source) - 1u);
|
strncpy(calibration_[found].source, src, sizeof(calibration_[found].source) - 1u);
|
||||||
calibration_[found].source[sizeof(calibration_[found].source) - 1u] = '\0';
|
calibration_[found].source[sizeof(calibration_[found].source) - 1u] = '\0';
|
||||||
strncpy(calibration_[found].signal, signal, sizeof(calibration_[found].signal) - 1u);
|
strncpy(calibration_[found].signal, sig, sizeof(calibration_[found].signal) - 1u);
|
||||||
calibration_[found].signal[sizeof(calibration_[found].signal) - 1u] = '\0';
|
calibration_[found].signal[sizeof(calibration_[found].signal) - 1u] = '\0';
|
||||||
strncpy(calibration_[found].unit, u, sizeof(calibration_[found].unit) - 1u);
|
strncpy(calibration_[found].unit, u, sizeof(calibration_[found].unit) - 1u);
|
||||||
calibration_[found].unit[sizeof(calibration_[found].unit) - 1u] = '\0';
|
calibration_[found].unit[sizeof(calibration_[found].unit) - 1u] = '\0';
|
||||||
@@ -776,19 +842,48 @@ void StreamHub::BroadcastCalibration() {
|
|||||||
uint32 off = 0u;
|
uint32 off = 0u;
|
||||||
JsonAppendf(buf, off, cap, "{\"type\":\"calibration\",\"cal\":[");
|
JsonAppendf(buf, off, cap, "{\"type\":\"calibration\",\"cal\":[");
|
||||||
|
|
||||||
|
/* Snapshot the calibration table, then release the mutex before building
|
||||||
|
* JSON (Go parity: emit sorted by source then signal). */
|
||||||
(void) calibrationMutex_.FastLock();
|
(void) calibrationMutex_.FastLock();
|
||||||
for (uint32 i = 0u; i < numCalibration_; i++) {
|
const uint32 n = numCalibration_;
|
||||||
|
/* Build a sorted index array (insertion sort — no STL). */
|
||||||
|
uint32 *idx = new uint32[n];
|
||||||
|
for (uint32 i = 0u; i < n; i++) { idx[i] = i; }
|
||||||
|
for (uint32 i = 1u; i < n; i++) {
|
||||||
|
const uint32 key = idx[i];
|
||||||
|
MARTe::int32 j = static_cast<MARTe::int32>(i) - 1;
|
||||||
|
while (j >= 0) {
|
||||||
|
const uint32 cur = idx[static_cast<uint32>(j)];
|
||||||
|
const int cmpSrc = strcmp(calibration_[cur].source,
|
||||||
|
calibration_[key].source);
|
||||||
|
const bool before = (cmpSrc > 0) ||
|
||||||
|
((cmpSrc == 0) &&
|
||||||
|
(strcmp(calibration_[cur].signal,
|
||||||
|
calibration_[key].signal) > 0));
|
||||||
|
if (!before) { break; }
|
||||||
|
idx[static_cast<uint32>(j) + 1u] = cur;
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
idx[static_cast<uint32>(j) + 1u] = key;
|
||||||
|
}
|
||||||
|
/* Snapshot entries in sorted order so we can release lock before BroadcastText. */
|
||||||
|
CalibrationEntry *snap = new CalibrationEntry[n];
|
||||||
|
for (uint32 i = 0u; i < n; i++) { snap[i] = calibration_[idx[i]]; }
|
||||||
|
calibrationMutex_.FastUnLock();
|
||||||
|
delete[] idx;
|
||||||
|
|
||||||
|
for (uint32 i = 0u; i < n; i++) {
|
||||||
JsonAppendf(buf, off, cap,
|
JsonAppendf(buf, off, cap,
|
||||||
"%s{\"source\":\"%s\",\"signal\":\"%s\","
|
"%s{\"source\":\"%s\",\"signal\":\"%s\","
|
||||||
"\"scale\":%.17g,\"offset\":%.17g,\"unit\":\"%s\"}",
|
"\"scale\":%.17g,\"offset\":%.17g,\"unit\":\"%s\"}",
|
||||||
(i > 0u) ? "," : "",
|
(i > 0u) ? "," : "",
|
||||||
calibration_[i].source,
|
snap[i].source,
|
||||||
calibration_[i].signal,
|
snap[i].signal,
|
||||||
calibration_[i].scale,
|
snap[i].scale,
|
||||||
calibration_[i].offset,
|
snap[i].offset,
|
||||||
calibration_[i].unit);
|
snap[i].unit);
|
||||||
}
|
}
|
||||||
calibrationMutex_.FastUnLock();
|
delete[] snap;
|
||||||
|
|
||||||
JsonAppendf(buf, off, cap, "]}");
|
JsonAppendf(buf, off, cap, "]}");
|
||||||
wsServer_.BroadcastText(buf, off);
|
wsServer_.BroadcastText(buf, off);
|
||||||
@@ -1121,26 +1216,52 @@ void StreamHub::HandleSaveSources() {
|
|||||||
nSaved++;
|
nSaved++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Calibration entries are further elements of the SAME flat array. */
|
/* Calibration entries are further elements of the SAME flat array.
|
||||||
|
* Emit sorted by source then signal to match Go's encodeConfigFile output. */
|
||||||
uint32 nCal = 0u;
|
uint32 nCal = 0u;
|
||||||
(void) calibrationMutex_.FastLock();
|
(void) calibrationMutex_.FastLock();
|
||||||
for (uint32 i = 0u; i < numCalibration_; i++) {
|
const uint32 nCalTotal = numCalibration_;
|
||||||
|
uint32 *cidx = new uint32[nCalTotal];
|
||||||
|
for (uint32 i = 0u; i < nCalTotal; i++) { cidx[i] = i; }
|
||||||
|
for (uint32 i = 1u; i < nCalTotal; i++) {
|
||||||
|
const uint32 key = cidx[i];
|
||||||
|
MARTe::int32 j = static_cast<MARTe::int32>(i) - 1;
|
||||||
|
while (j >= 0) {
|
||||||
|
const uint32 cur = cidx[static_cast<uint32>(j)];
|
||||||
|
const int cmpSrc = strcmp(calibration_[cur].source,
|
||||||
|
calibration_[key].source);
|
||||||
|
const bool before = (cmpSrc > 0) ||
|
||||||
|
((cmpSrc == 0) &&
|
||||||
|
(strcmp(calibration_[cur].signal,
|
||||||
|
calibration_[key].signal) > 0));
|
||||||
|
if (!before) { break; }
|
||||||
|
cidx[static_cast<uint32>(j) + 1u] = cur;
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
cidx[static_cast<uint32>(j) + 1u] = key;
|
||||||
|
}
|
||||||
|
CalibrationEntry *csnap = new CalibrationEntry[nCalTotal];
|
||||||
|
for (uint32 i = 0u; i < nCalTotal; i++) { csnap[i] = calibration_[cidx[i]]; }
|
||||||
|
calibrationMutex_.FastUnLock();
|
||||||
|
delete[] cidx;
|
||||||
|
|
||||||
|
for (uint32 i = 0u; i < nCalTotal; i++) {
|
||||||
(void) fprintf(f,
|
(void) fprintf(f,
|
||||||
"%s {\n \"source\": \"%s\",\n \"signal\": \"%s\",\n"
|
"%s {\n \"source\": \"%s\",\n \"signal\": \"%s\",\n"
|
||||||
" \"scale\": %.17g,\n \"offset\": %.17g",
|
" \"scale\": %.17g,\n \"offset\": %.17g",
|
||||||
((nSaved + nCal) > 0u) ? ",\n" : "",
|
((nSaved + nCal) > 0u) ? ",\n" : "",
|
||||||
calibration_[i].source,
|
csnap[i].source,
|
||||||
calibration_[i].signal,
|
csnap[i].signal,
|
||||||
calibration_[i].scale,
|
csnap[i].scale,
|
||||||
calibration_[i].offset);
|
csnap[i].offset);
|
||||||
if (calibration_[i].unit[0] != '\0') {
|
if (csnap[i].unit[0] != '\0') {
|
||||||
(void) fprintf(f, ",\n \"unit\": \"%s\"",
|
(void) fprintf(f, ",\n \"unit\": \"%s\"",
|
||||||
calibration_[i].unit);
|
csnap[i].unit);
|
||||||
}
|
}
|
||||||
(void) fprintf(f, "\n }");
|
(void) fprintf(f, "\n }");
|
||||||
nCal++;
|
nCal++;
|
||||||
}
|
}
|
||||||
calibrationMutex_.FastUnLock();
|
delete[] csnap;
|
||||||
|
|
||||||
(void) fprintf(f, "\n]\n");
|
(void) fprintf(f, "\n]\n");
|
||||||
(void) fclose(f);
|
(void) fclose(f);
|
||||||
|
|||||||
Reference in New Issue
Block a user