interface added and web ui created

This commit is contained in:
Martino Ferrari
2026-05-07 10:49:24 +02:00
parent 340b024ae4
commit 774808a054
21 changed files with 4986 additions and 362 deletions
@@ -3,7 +3,7 @@
#include "BrokerI.h"
#include "DataSourceI.h"
#include "DebugService.h"
#include "DebugServiceI.h"
#include "FastPollingMutexSem.h"
#include "HighResolutionTimer.h"
#include "MemoryMapBroker.h"
@@ -90,8 +90,11 @@ public:
// Spins while paused, then consumes one step counter tick (if stepping).
// Input brokers must NOT call this — they complete normally to avoid blocking
// cross-thread EventSem posts (RealTimeThreadSynchBroker would time out).
static void OutputPauseAndStep(DebugService *service, const char8 *gamName) {
if (service == NULL_PTR(DebugService *)) return;
static void OutputPauseAndStep(DebugServiceI *service, const char8 *gamName) {
if (service == NULL_PTR(DebugServiceI *)) return;
// Fast path: nothing to do when neither paused nor stepping.
// Avoids Threads::Name() lookup (and its mutex) on every 1000 Hz cycle.
if (!service->IsPaused() && !service->IsStepPending()) return;
// Wait if already paused (manual PAUSE or breakpoint from a previous cycle)
while (service->IsPaused()) Sleep::MSec(10);
// Pass the OS thread name so per-thread step filtering works.
@@ -100,13 +103,13 @@ public:
while (service->IsPaused()) Sleep::MSec(10);
}
static void Process(DebugService *service,
static void Process(DebugServiceI *service,
DebugSignalInfo **signalInfoPointers,
Vec<uint32> &activeIndices, Vec<uint32> &activeSizes,
FastPollingMutexSem &activeMutex,
volatile bool *anyBreakFlag,
Vec<uint32> *breakIndices) {
if (service == NULL_PTR(DebugService *))
if (service == NULL_PTR(DebugServiceI *))
return;
// NOTE: No spin here. Spinning for paused state is handled in Execute() of
@@ -118,7 +121,7 @@ public:
if (n > 0 && signalInfoPointers != NULL_PTR(DebugSignalInfo **)) {
// Capture timestamp ONCE per broker cycle for lowest impact
uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() *
HighResolutionTimer::Period() * 1000000.0);
HighResolutionTimer::Period() * 1.0e9);
for (uint32 i = 0; i < n; i++) {
uint32 idx = activeIndices[i];
@@ -130,13 +133,33 @@ public:
}
}
// Conditional break check — zero cost when anyBreakFlag is false
// (single volatile read; the RT loop never pays for this when no break is set).
if (*anyBreakFlag && !service->IsPaused() &&
breakIndices != NULL_PTR(Vec<uint32> *)) {
uint32 nb = breakIndices->Size();
// FIX #3: copy break indices under lock into a local stack array, then
// release activeMutex BEFORE evaluating. EvaluateBreak reads signal
// memory which can stall (cache miss); holding activeMutex during that
// stall would block UpdateBrokersBreakStatus() in the Server thread and
// cause unnecessary priority inversion on the RT path.
bool shouldCheckBreak = (*anyBreakFlag && !service->IsPaused() &&
breakIndices != NULL_PTR(Vec<uint32> *) &&
signalInfoPointers != NULL_PTR(DebugSignalInfo **));
static const uint32 MAX_BREAK_INDICES = 64u;
uint32 localBreakIdx[MAX_BREAK_INDICES];
uint32 nb = 0u;
if (shouldCheckBreak) {
nb = breakIndices->Size();
if (nb > MAX_BREAK_INDICES) nb = MAX_BREAK_INDICES;
for (uint32 i = 0; i < nb; i++) {
uint32 idx = (*breakIndices)[i];
localBreakIdx[i] = (*breakIndices)[i];
}
}
activeMutex.FastUnLock();
// Evaluate break conditions outside the lock — safe because
// EvaluateBreak only reads signalInfoPointers[idx]->memoryAddress,
// which is RT data-bus memory and is never freed during the RT cycle.
if (shouldCheckBreak && nb > 0u) {
for (uint32 i = 0; i < nb; i++) {
uint32 idx = localBreakIdx[i];
DebugSignalInfo *s = signalInfoPointers[idx];
if (s != NULL_PTR(DebugSignalInfo *) && s->breakOp != BREAK_OFF &&
EvaluateBreak(s)) {
@@ -145,14 +168,12 @@ public:
}
}
}
activeMutex.FastUnLock();
}
// Pass numCopies explicitly so we can mock it
static void
InitSignals(BrokerI *broker, DataSourceI &dataSourceIn,
DebugService *&service, DebugSignalInfo **&signalInfoPointers,
DebugServiceI *&service, DebugSignalInfo **&signalInfoPointers,
uint32 numCopies, MemoryMapBrokerCopyTableEntry *copyTable,
const char8 *functionName, SignalDirection direction,
volatile bool *anyActiveFlag, Vec<uint32> *activeIndices,
@@ -164,16 +185,14 @@ public:
signalInfoPointers[i] = NULL_PTR(DebugSignalInfo *);
}
ReferenceContainer *root = ObjectRegistryDatabase::Instance();
Reference serviceRef = root->Find("DebugService");
if (serviceRef.IsValid()) {
service = dynamic_cast<DebugService *>(serviceRef.operator->());
}
// Use the singleton registered by DebugService::Initialise() — no ORD
// search on the Init path, and no dependency on a concrete type.
service = DebugServiceI::GetInstance();
if (service && (copyTable != NULL_PTR(MemoryMapBrokerCopyTableEntry *))) {
StreamString dsPath;
DebugService::GetFullObjectName(dataSourceIn, dsPath);
DebugServiceI::GetFullObjectName(dataSourceIn, dsPath);
fprintf(stderr, ">> %s broker for %s [%d]\n",
direction == InputSignals ? "Input" : "Output", dsPath.Buffer(),
numCopies);
@@ -225,7 +244,7 @@ public:
if (gamRef.IsValid()) {
StreamString absGamPath;
DebugService::GetFullObjectName(*(gamRef.operator->()), absGamPath);
DebugServiceI::GetFullObjectName(*(gamRef.operator->()), absGamPath);
// Register short path (In/Out) for GUI compatibility
gamFullName.Printf("%s.%s.%s", absGamPath.Buffer(), dirStrShort,
signalName.Buffer());
@@ -261,7 +280,7 @@ public:
template <typename BaseClass> class DebugBrokerWrapper : public BaseClass {
public:
DebugBrokerWrapper() : BaseClass() {
service = NULL_PTR(DebugService *);
service = NULL_PTR(DebugServiceI *);
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
numSignals = 0;
anyActive = false;
@@ -326,7 +345,7 @@ public:
return ret;
}
DebugService *service;
DebugServiceI *service;
DebugSignalInfo **signalInfoPointers;
uint32 numSignals;
volatile bool anyActive;
@@ -343,7 +362,7 @@ template <typename BaseClass>
class DebugBrokerWrapperNoOptim : public BaseClass {
public:
DebugBrokerWrapperNoOptim() : BaseClass() {
service = NULL_PTR(DebugService *);
service = NULL_PTR(DebugServiceI *);
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
numSignals = 0;
anyActive = false;
@@ -386,7 +405,7 @@ public:
return ret;
}
DebugService *service;
DebugServiceI *service;
DebugSignalInfo **signalInfoPointers;
uint32 numSignals;
volatile bool anyActive;
@@ -402,7 +421,7 @@ public:
class DebugMemoryMapAsyncOutputBroker : public MemoryMapAsyncOutputBroker {
public:
DebugMemoryMapAsyncOutputBroker() : MemoryMapAsyncOutputBroker() {
service = NULL_PTR(DebugService *);
service = NULL_PTR(DebugServiceI *);
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
numSignals = 0;
anyActive = false;
@@ -446,7 +465,7 @@ public:
}
return ret;
}
DebugService *service;
DebugServiceI *service;
DebugSignalInfo **signalInfoPointers;
uint32 numSignals;
volatile bool anyActive;
@@ -463,7 +482,7 @@ class DebugMemoryMapAsyncTriggerOutputBroker
public:
DebugMemoryMapAsyncTriggerOutputBroker()
: MemoryMapAsyncTriggerOutputBroker() {
service = NULL_PTR(DebugService *);
service = NULL_PTR(DebugServiceI *);
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
numSignals = 0;
anyActive = false;
@@ -506,7 +525,7 @@ public:
}
return ret;
}
DebugService *service;
DebugServiceI *service;
DebugSignalInfo **signalInfoPointers;
uint32 numSignals;
volatile bool anyActive;
@@ -114,7 +114,19 @@ public:
ReadFromBuffer(&tempRead, &tempSize, 4);
if (tempSize > maxSize) {
readIndex = write;
// FIX #5: Skip only the current entry rather than discarding the
// entire ring buffer. tempRead is already past the 16-byte header;
// advancing by tempSize lands at the start of the next entry.
//
// Safety fallback: if tempSize >= bufferSize the stored size field
// is corrupt (it can never be that large). In that case we cannot
// locate the next entry safely, so fall back to discarding everything
// to avoid reading garbage as sample headers on future Pop() calls.
if (tempSize >= bufferSize) {
readIndex = write; // corrupt ring — discard all
} else {
readIndex = (tempRead + tempSize) % bufferSize;
}
return false;
}
@@ -1,3 +1,4 @@
#include "Atomic.h"
#include "BasicTCPSocket.h"
#include "ClassRegistryItem.h"
#include "ConfigurationDatabase.h"
@@ -21,7 +22,8 @@
namespace MARTe {
DebugService *DebugService::instance = (DebugService *)0;
// DebugServiceI static members — defined here so no extra .cpp is needed.
DebugServiceI *DebugServiceI::instance = NULL_PTR(DebugServiceI *);
static void EscapeJson(const char8 *src, StreamString &dst) {
if (src == NULL_PTR(const char8 *))
@@ -85,6 +87,15 @@ static bool FindPathInContainer(ReferenceContainer *container,
CLASS_REGISTER(DebugService, "1.0")
// Out-of-class definitions required by C++98 when the constants are odr-used
// (i.e. their address is taken or they appear in a context that needs linkage).
const uint32 DebugService::STREAMER_MTU;
const uint32 DebugService::STREAMER_BUFFER_SIZE;
const uint32 DebugService::CMD_RATE_LIMIT;
const uint32 DebugService::CLIENT_IDLE_TIMEOUT_MS;
const uint32 DebugService::GET_VALUE_MAX_ELEMENTS;
const uint32 DebugService::INPUT_BUFFER_MAX;
DebugService::DebugService()
: ReferenceContainer(), EmbeddedServiceMethodBinderI(),
binderServer(this, ServiceBinder::ServerType),
@@ -102,11 +113,15 @@ DebugService::DebugService()
activeClient = NULL_PTR(BasicTCPSocket *);
streamerPacketOffset = 0u;
streamerSequenceNumber = 0u;
cmdCountInWindow = 0u;
cmdWindowStartMs = 0u;
lastDataTimeMs = 0u;
inputBuffer = ""; // FIX #10: initialise carry-over buffer
}
DebugService::~DebugService() {
if (instance == this) {
instance = NULL_PTR(DebugService *);
if (DebugServiceI::GetInstance() == this) {
DebugServiceI::SetInstance(NULL_PTR(DebugServiceI *));
}
threadService.Stop();
streamerService.Stop();
@@ -135,7 +150,7 @@ bool DebugService::Initialise(StructuredDataI &data) {
if (controlPort > 0) {
isServer = true;
instance = this;
DebugServiceI::SetInstance(this);
}
port = 8081;
@@ -463,12 +478,28 @@ void DebugService::ProcessSignal(DebugSignalInfo *signalInfo, uint32 size,
memcpy(signalInfo->memoryAddress, signalInfo->forcedValue, size);
}
if (signalInfo->isTracing) {
if (signalInfo->decimationCounter == 0) {
traceBuffer.Push(signalInfo->internalID, timestamp,
(uint8 *)signalInfo->memoryAddress, size);
// FIX #4: decimationCounter read-modify-write is a data race when multiple
// RT threads call ProcessSignal() concurrently for signals with the same
// DebugSignalInfo (e.g. an output signal read by two GAMs).
//
// Pattern: atomically increment, then claim the "push token" by exchanging
// back to zero only when the counter reaches the decimation factor.
// Only the thread that observes old >= decimationFactor actually pushes;
// all others just increment and return. No separate lock is needed for
// the counter itself.
//
// tracePushMutex still serialises the Push() call (FIX #2) because
// TraceRingBuffer is SPSC and multiple RT threads can be pushing concurrently.
Atomic::Add((volatile int32 *)&signalInfo->decimationCounter, 1);
if ((uint32)signalInfo->decimationCounter >= signalInfo->decimationFactor) {
int32 old = Atomic::Exchange((volatile int32 *)&signalInfo->decimationCounter, 0);
if ((uint32)old >= signalInfo->decimationFactor) {
tracePushMutex.FastLock();
traceBuffer.Push(signalInfo->internalID, timestamp,
(uint8 *)signalInfo->memoryAddress, size);
tracePushMutex.FastUnLock();
}
}
signalInfo->decimationCounter =
(signalInfo->decimationCounter + 1) % signalInfo->decimationFactor;
}
}
@@ -501,6 +532,11 @@ void DebugService::RegisterBroker(DebugSignalInfo **signalPointers,
void DebugService::UpdateBrokersActiveStatus() {
for (uint32 i = 0; i < brokers.Size(); i++) {
// FIX #3: signalPointers may be NULL when numSignals > 0 if the broker was
// registered with a null array (e.g. from unit tests or misconfigured brokers).
// Dereferencing NULL would be UB; skip the broker entirely.
if (brokers[i].signalPointers == NULL_PTR(DebugSignalInfo **)) continue;
uint32 count = 0;
for (uint32 j = 0; j < brokers[i].numSignals; j++) {
DebugSignalInfo *s = brokers[i].signalPointers[j];
@@ -524,20 +560,28 @@ void DebugService::UpdateBrokersActiveStatus() {
if (brokers[i].activeMutex)
brokers[i].activeMutex->FastLock();
// FIX #2: Use O(1) Swap instead of operator= (heap alloc + copy) inside
// the critical section. The old arrays are carried out in tempInd/tempSizes
// and freed after the lock is released.
if (brokers[i].activeIndices)
*(brokers[i].activeIndices) = tempInd;
brokers[i].activeIndices->Swap(tempInd);
if (brokers[i].activeSizes)
*(brokers[i].activeSizes) = tempSizes;
brokers[i].activeSizes->Swap(tempSizes);
if (brokers[i].anyActiveFlag)
*(brokers[i].anyActiveFlag) = (count > 0);
if (brokers[i].activeMutex)
brokers[i].activeMutex->FastUnLock();
// tempInd and tempSizes now hold the old arrays and are freed here,
// outside the lock.
}
}
void DebugService::UpdateBrokersBreakStatus() {
for (uint32 i = 0; i < brokers.Size(); i++) {
// FIX #3: same null guard as UpdateBrokersActiveStatus.
if (brokers[i].signalPointers == NULL_PTR(DebugSignalInfo **)) continue;
Vec<uint32> tempBreak;
uint32 count = 0;
for (uint32 j = 0; j < brokers[i].numSignals; j++) {
@@ -549,12 +593,14 @@ void DebugService::UpdateBrokersBreakStatus() {
}
if (brokers[i].activeMutex)
brokers[i].activeMutex->FastLock();
// FIX #2: O(1) Swap — old array freed after the lock is released.
if (brokers[i].breakIndices)
*(brokers[i].breakIndices) = tempBreak;
brokers[i].breakIndices->Swap(tempBreak);
if (brokers[i].anyBreakFlag)
*(brokers[i].anyBreakFlag) = (count > 0);
if (brokers[i].activeMutex)
brokers[i].activeMutex->FastUnLock();
// tempBreak freed here, outside the lock.
}
}
@@ -582,50 +628,143 @@ ErrorManagement::ErrorType DebugService::Server(ExecutionInfo &info) {
// The MARTe2 framework calls Execute() in a loop; each call should do
// one unit of work and return so the framework can check for Stop().
// This replaces the old internal infinite-while pattern.
//
// FIX #2: activeClient is guarded by clientMutex.
// Rule: lock clientMutex only when ASSIGNING the pointer (including to NULL).
// Do NOT hold it across blocking I/O (Read/Write) — that would stall any future
// thread trying to grab the lock to check whether there is a live client.
// The command handler (HandleCommand) receives the pointer by value so it is
// safe to call without holding the lock.
// Helper: current time in milliseconds (used for rate limiting and idle timeout)
uint64 nowMs = (uint64)((float64)HighResolutionTimer::Counter() *
HighResolutionTimer::Period() * 1000.0);
if (activeClient == NULL_PTR(BasicTCPSocket *)) {
// Wait briefly for a new connection; return so the framework loop can
// check if Stop() was requested between calls.
BasicTCPSocket *newClient = tcpServer.WaitConnection(TimeoutType(100));
if (newClient != NULL_PTR(BasicTCPSocket *)) {
activeClient = newClient;
clientMutex.FastLock();
activeClient = newClient; // publish pointer — visible to other threads after unlock
clientMutex.FastUnLock();
// FIX #6/#8: reset per-connection state when a new client connects
cmdCountInWindow = 0u;
cmdWindowStartMs = nowMs;
lastDataTimeMs = nowMs;
}
} else {
// Check if client is still connected
if (!activeClient->IsConnected()) {
// FIX #8: idle-timeout check — runs every Execute() invocation even when
// no data arrives, so a client that half-sends a command and goes silent
// cannot hold the slot open indefinitely.
if (nowMs - lastDataTimeMs > CLIENT_IDLE_TIMEOUT_MS) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"Server: TCP client idle for >%u ms — closing connection.", CLIENT_IDLE_TIMEOUT_MS);
inputBuffer = ""; // FIX #10: discard carry-over on disconnect
clientMutex.FastLock();
activeClient->Close();
delete activeClient;
activeClient = NULL_PTR(BasicTCPSocket *);
clientMutex.FastUnLock();
cmdCountInWindow = 0u;
} else if (!activeClient->IsConnected()) {
// Check if client is still connected
inputBuffer = ""; // FIX #10
clientMutex.FastLock();
activeClient->Close();
delete activeClient;
activeClient = NULL_PTR(BasicTCPSocket *);
clientMutex.FastUnLock();
} else {
char buffer[1024];
uint32 size = 1024;
if (activeClient->Read(buffer, size)) {
if (size > 0) {
// Process each line separately
char *ptr = buffer;
char *end = buffer + size;
while (ptr < end) {
char *newline = (char *)memchr(ptr, '\n', end - ptr);
if (!newline) {
break;
lastDataTimeMs = nowMs; // FIX #8: refresh idle timestamp
// FIX #6: slide rate-limit window
if (nowMs - cmdWindowStartMs >= 1000u) {
cmdWindowStartMs = nowMs;
cmdCountInWindow = 0u;
}
// FIX #10: guard against a client that never sends a newline,
// which would grow inputBuffer without bound.
if (inputBuffer.Size() + (uint32)size > INPUT_BUFFER_MAX) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"Server: input buffer overflow (>%u bytes without newline) "
"— disconnecting.", INPUT_BUFFER_MAX);
inputBuffer = "";
clientMutex.FastLock();
activeClient->Close();
delete activeClient;
activeClient = NULL_PTR(BasicTCPSocket *);
clientMutex.FastUnLock();
cmdCountInWindow = 0u;
} else {
// FIX #10: accumulate data; parse only complete (newline-terminated)
// commands so that commands split across TCP segments are assembled
// correctly before dispatch.
(void)inputBuffer.Seek(inputBuffer.Size());
uint32 writeSize = (uint32)size;
inputBuffer.Write(buffer, writeSize);
const char8 *raw = inputBuffer.Buffer();
uint32 total = (uint32)inputBuffer.Size();
uint32 lineStart = 0u;
bool rateLimitExceeded = false;
for (uint32 pos = 0u; pos < total && !rateLimitExceeded; pos++) {
if (raw[pos] != '\n') continue;
uint32 len = pos - lineStart;
// Trim trailing CR
if (len > 0u && raw[lineStart + len - 1u] == '\r') len--;
if (len > 0u) {
cmdCountInWindow++;
if (cmdCountInWindow > CMD_RATE_LIMIT) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"Server: client exceeded rate limit (%u cmd/s) — disconnecting.",
CMD_RATE_LIMIT);
rateLimitExceeded = true;
break;
}
StreamString command;
uint32 cmdLen = len;
command.Write(raw + lineStart, cmdLen);
HandleCommand(command, activeClient);
}
lineStart = pos + 1u;
}
*newline = '\0';
// Skip carriage return if present
if (newline > ptr && *(newline - 1) == '\r')
*(newline - 1) = '\0';
StreamString command;
uint32 len = (uint32)(newline - ptr);
command.Write(ptr, len);
if (command.Size() > 0) {
HandleCommand(command, activeClient);
if (rateLimitExceeded) {
inputBuffer = "";
clientMutex.FastLock();
activeClient->Close();
delete activeClient;
activeClient = NULL_PTR(BasicTCPSocket *);
clientMutex.FastUnLock();
cmdCountInWindow = 0u;
} else {
// Save the incomplete line (bytes after the last '\n') for the
// next Read() — they form the start of the next command.
StreamString newInputBuffer;
if (lineStart < total) {
uint32 remLen = total - lineStart;
newInputBuffer.Write(raw + lineStart, remLen);
}
inputBuffer = newInputBuffer;
}
ptr = newline + 1;
}
}
} else {
// Read failed (client disconnected or error), clean up
inputBuffer = ""; // FIX #10
clientMutex.FastLock();
activeClient->Close();
delete activeClient;
activeClient = NULL_PTR(BasicTCPSocket *);
clientMutex.FastUnLock();
}
}
}
@@ -654,19 +793,63 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) {
HighResolutionTimer::Period() * 1000000.0);
void *address = NULL_PTR(void *);
if (monitoredSignals[i].dataSource->GetSignalMemoryBuffer(monitoredSignals[i].signalIdx, 0, address)) {
// FIX #11: ProcessSignal() (called from RT broker threads) serialises
// its traceBuffer.Push() under tracePushMutex. The Streamer's monitored-
// signal path previously skipped that lock, creating a multi-producer race
// between the RT threads and the Streamer thread.
//
// Lock order: mutex (always held here) → tracePushMutex.
// RT broker threads only ever acquire tracePushMutex (never mutex), so
// this ordering introduces no deadlock risk.
tracePushMutex.FastLock();
traceBuffer.Push(monitoredSignals[i].internalID, ts, (uint8 *)address, monitoredSignals[i].size);
tracePushMutex.FastUnLock();
}
}
}
mutex.FastUnLock();
// Drain ring buffer into UDP packet(s)
// Drain ring buffer into UDP packet(s).
//
// FIX #1: Two-level bounds checking to prevent buffer overflow.
//
// Level 1 — per-sample maximum size:
// sampleData is SAMPLE_BUF_SIZE bytes. Pop() enforces this via maxSize so
// the local buffer can never be overrun by Pop() itself.
//
// Level 2 — assembly-buffer hard limit:
// After a flush, streamerPacketOffset resets to sizeof(TraceHeader). The
// next sample occupies (sizeof(TraceHeader) + 16 + size) bytes. With
// SAMPLE_BUF_SIZE = 1024 that is at most ~1060 bytes — well within
// STREAMER_BUFFER_SIZE = 4096. The explicit guard below catches the case
// where SAMPLE_BUF_SIZE or STREAMER_MTU are later changed carelessly so
// that a single sample could still overflow the buffer.
static const uint32 SAMPLE_BUF_SIZE = 1024u;
// Compile-time sanity: one full header + per-sample header + max sample must fit
// inside the assembly buffer. If this fires, raise STREAMER_BUFFER_SIZE or
// lower SAMPLE_BUF_SIZE.
// (C++98-compatible static assert via sizeof a negative-size array)
typedef char StaticAssert_StreamerBufferTooSmall[
(sizeof(TraceHeader) + 16u + SAMPLE_BUF_SIZE <= STREAMER_BUFFER_SIZE) ? 1 : -1];
(void)sizeof(StaticAssert_StreamerBufferTooSmall); // suppress unused-typedef warning
uint32 id, size;
uint64 ts;
uint8 sampleData[1024];
uint8 sampleData[SAMPLE_BUF_SIZE];
bool hasData = false;
while (traceBuffer.Pop(id, ts, sampleData, size, 1024)) {
while (traceBuffer.Pop(id, ts, sampleData, size, SAMPLE_BUF_SIZE)) {
hasData = true;
// Level 2 guard — should never fire given the static assert above, but
// defends against future changes that widen SAMPLE_BUF_SIZE or shrink
// STREAMER_BUFFER_SIZE without updating the other.
if (size > SAMPLE_BUF_SIZE || streamerPacketOffset + 16u + size > STREAMER_BUFFER_SIZE) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"Streamer: sample size %u would overflow assembly buffer (%u bytes used of %u) "
"— sample dropped.", size, streamerPacketOffset, STREAMER_BUFFER_SIZE);
continue;
}
if (streamerPacketOffset == 0u) {
TraceHeader header;
header.magic = 0xDA7A57AD;
@@ -676,7 +859,8 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) {
memcpy(streamerPacketBuffer, &header, sizeof(TraceHeader));
streamerPacketOffset = sizeof(TraceHeader);
}
if (streamerPacketOffset + 16u + size > 1400u) {
// Flush the current packet when adding this sample would exceed the MTU.
if (streamerPacketOffset + 16u + size > STREAMER_MTU) {
uint32 toWrite = streamerPacketOffset;
(void)udpSocket.Write((char8 *)streamerPacketBuffer, toWrite);
TraceHeader header;
@@ -705,8 +889,8 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) {
return ErrorManagement::NoError;
}
bool DebugService::GetFullObjectName(const Object &obj,
StreamString &fullPath) {
bool DebugServiceI::GetFullObjectName(const Object &obj,
StreamString &fullPath) {
fullPath = "";
if (FindPathInContainer(ObjectRegistryDatabase::Instance(), &obj, fullPath)) {
return true;
@@ -893,15 +1077,42 @@ void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) {
const char8 *eq = StringHelper::SearchChar(line.Buffer(), '=');
if (eq != NULL_PTR(const char8 *)) {
uint32 eqPos = (uint32)(eq - line.Buffer());
(void)line.Seek(0u);
char8 keyBuf[256] = {'\0'};
// FIX #7: Enforce buffer bounds before reading key/value.
// A key longer than keyBuf would be silently truncated by
// Read(), producing a mismatched key (e.g. "LongKe" instead
// of "LongKey") that causes CDB.Write() to inject a garbled
// parameter. Skip the entire line if the key overflows —
// the CDB write would be nonsensical anyway.
// Values are safely capped: the worst case is a truncated
// value, which the target GAM can detect and reject.
static const uint32 KEY_BUF_SIZE = 256u;
static const uint32 VAL_BUF_SIZE = 1024u;
if (eqPos >= KEY_BUF_SIZE) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"MSG: key length %u exceeds buffer (%u) — line skipped.",
eqPos, KEY_BUF_SIZE);
line = "";
continue;
}
(void)line.Seek(0u);
char8 keyBuf[KEY_BUF_SIZE];
MemoryOperationsHelper::Set(keyBuf, '\0', KEY_BUF_SIZE);
uint32 keyReadSize = eqPos;
(void)line.Read(keyBuf, keyReadSize);
(void)line.Seek(eqPos + 1u);
uint32 valLen = (uint32)(line.Size() - eqPos - 1u);
char8 valBuf[1024] = {'\0'};
// Truncate silently to VAL_BUF_SIZE - 1 (leave room for '\0')
if (valLen >= VAL_BUF_SIZE) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"MSG: value length %u truncated to %u for key '%s'.",
valLen, VAL_BUF_SIZE - 1u, keyBuf);
valLen = VAL_BUF_SIZE - 1u;
}
char8 valBuf[VAL_BUF_SIZE];
MemoryOperationsHelper::Set(valBuf, '\0', VAL_BUF_SIZE);
(void)line.Read(valBuf, valLen);
// Trim trailing whitespace from value
@@ -1418,6 +1629,25 @@ uint32 DebugService::ForceSignal(const char8 *name, const char8 *valueStr) {
SuffixMatch(aliases[i].name.Buffer(), name)) {
DebugSignalInfo *s = signals[aliases[i].signalIndex];
// FIX #4: Guard against writing past the end of forcedValue[1024].
// TypeConvert writes (byteSize * numberOfElements) bytes into forcedValue.
// For large arrays (e.g. 512 float64s = 4096 bytes) this would silently
// overflow the 1024-byte buffer and corrupt adjacent struct members.
//
// TypeDescriptor::numberOfBits is the element size in bits; integer divide
// by 8 gives bytes. For structured/opaque types numberOfBits may be 0 —
// those are also skipped because we cannot determine the layout safely.
uint32 elemBytes = (uint32)(s->type.numberOfBits) / 8u;
uint32 totalBytes = elemBytes * s->numberOfElements;
if (elemBytes == 0u || totalBytes > (uint32)sizeof(s->forcedValue)) {
REPORT_ERROR_STATIC(ErrorManagement::Warning,
"ForceSignal: signal '%s' requires %u bytes but forcedValue buffer is "
"only %u bytes — force request ignored.",
s->name.Buffer(), totalBytes, (uint32)sizeof(s->forcedValue));
continue;
}
s->isForcing = true;
AnyType dest(s->type, 0u, s->forcedValue);
AnyType source(CharString, 0u, valueStr);
@@ -1536,7 +1766,18 @@ void DebugService::GetSignalValue(const char8 *name, BasicTCPSocket *client) {
TypeDescriptor td = sig->type;
uint32 nElem = sig->numberOfElements;
uint32 byteSize = (td.numberOfBits > 0u) ? (td.numberOfBits / 8u) : 1u;
// FIX #9: cap element count before computing byte totals.
// Without a cap, a 1M-element uint8 signal would produce a multi-MB
// comma-separated response, exhausting heap and blocking the Server thread.
// GET_VALUE_MAX_ELEMENTS = 256 gives a ~4 KB worst-case JSON string.
bool truncated = (nElem > GET_VALUE_MAX_ELEMENTS);
if (truncated) {
nElem = GET_VALUE_MAX_ELEMENTS;
}
uint32 totalBytes = byteSize * nElem;
// Secondary cap: even after the element limit, ensure we never read more
// than 1024 bytes from the RT memory region.
if (totalBytes > 1024u) { totalBytes = 1024u; nElem = totalBytes / byteSize; }
// Copy bytes while holding the mutex to avoid data races with the RT thread
uint8 localBuf[1024];
@@ -1582,7 +1823,10 @@ void DebugService::GetSignalValue(const char8 *name, BasicTCPSocket *client) {
vp++;
}
resp += "\", \"Elements\": ";
resp.Printf("%u}\nOK VALUE\n", nElem);
// Include the capped element count and a Truncated flag so the client can
// distinguish "this is the full signal" from "there are more elements".
resp.Printf("%u, \"Truncated\": %s}\nOK VALUE\n",
nElem, truncated ? "true" : "false");
uint32 s = resp.Size();
(void)client->Write(resp.Buffer(), s);
}
@@ -4,45 +4,30 @@
#include "BasicTCPSocket.h"
#include "BasicUDPSocket.h"
#include "ConfigurationDatabase.h"
#include "DebugCore.h"
#include "DebugServiceI.h"
#include "EmbeddedServiceMethodBinderI.h"
#include "MessageI.h"
#include "Object.h"
#include "ReferenceContainer.h"
#include "ReferenceT.h"
#include "SingleThreadService.h"
#include "StreamString.h"
#include "Vec.h"
namespace MARTe {
class MemoryMapBroker;
class DataSourceI;
struct SignalAlias {
StreamString name;
uint32 signalIndex;
};
struct BrokerInfo {
DebugSignalInfo **signalPointers;
uint32 numSignals;
MemoryMapBroker *broker;
volatile bool *anyActiveFlag;
Vec<uint32> *activeIndices;
Vec<uint32> *activeSizes;
FastPollingMutexSem *activeMutex;
// Conditional break — mirrors activeIndices but for break-enabled signals
volatile bool *anyBreakFlag;
Vec<uint32> *breakIndices;
// GAM association for step-by-GAM
StreamString gamName;
bool isOutput;
};
/**
* @brief TCP/UDP implementation of DebugServiceI.
*
* Provides signal tracing (UDP), forced-value injection, break conditions,
* and a text command channel (TCP) with a log-forwarding sidecar (TcpLogger).
* Alternative transports (TTY, WebSocket, …) can be added by implementing
* DebugServiceI without touching this class or any broker wrapper code.
*/
class DebugService : public ReferenceContainer,
public MessageI,
public EmbeddedServiceMethodBinderI {
public EmbeddedServiceMethodBinderI,
public DebugServiceI {
public:
friend class DebugServiceTest;
CLASS_REGISTER_DECLARATION()
@@ -52,49 +37,43 @@ public:
virtual bool Initialise(StructuredDataI &data);
DebugSignalInfo *RegisterSignal(void *memoryAddress, TypeDescriptor type,
const char8 *name, uint8 numberOfDimensions = 0,
uint32 numberOfElements = 1);
void ProcessSignal(DebugSignalInfo *signalInfo, uint32 size,
uint64 timestamp);
// DebugServiceI RT-path overrides
virtual DebugSignalInfo *RegisterSignal(void *memoryAddress, TypeDescriptor type,
const char8 *name,
uint8 numberOfDimensions = 0,
uint32 numberOfElements = 1);
virtual void ProcessSignal(DebugSignalInfo *signalInfo, uint32 size,
uint64 timestamp);
virtual void RegisterBroker(DebugSignalInfo **signalPointers, uint32 numSignals,
MemoryMapBroker *broker, volatile bool *anyActiveFlag,
Vec<uint32> *activeIndices, Vec<uint32> *activeSizes,
FastPollingMutexSem *activeMutex,
volatile bool *anyBreakFlag, Vec<uint32> *breakIndices,
const char8 *gamName = NULL_PTR(const char8 *),
bool isOutput = false);
virtual bool IsPaused() const { return isPaused; }
virtual void SetPaused(bool paused) { isPaused = paused; }
virtual bool IsStepPending() const { return stepRemaining > 0u; }
virtual void ConsumeStepIfNeeded(const char8 *gamName,
const char8 *threadName = NULL_PTR(const char8 *));
void RegisterBroker(DebugSignalInfo **signalPointers, uint32 numSignals,
MemoryMapBroker *broker, volatile bool *anyActiveFlag,
Vec<uint32> *activeIndices, Vec<uint32> *activeSizes,
FastPollingMutexSem *activeMutex,
volatile bool *anyBreakFlag, Vec<uint32> *breakIndices,
const char8 *gamName = NULL_PTR(const char8 *),
bool isOutput = false);
// DebugServiceI control-path overrides
virtual uint32 ForceSignal (const char8 *name, const char8 *valueStr);
virtual uint32 UnforceSignal(const char8 *name);
virtual uint32 TraceSignal (const char8 *name, bool enable, uint32 decimation = 1);
virtual uint32 SetBreak (const char8 *name, uint8 op, float64 threshold);
virtual uint32 ClearBreak (const char8 *name);
virtual bool IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable);
virtual uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs);
virtual uint32 UnmonitorSignal (const char8 *path);
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info);
virtual ErrorManagement::ErrorType HandleMessage(ReferenceT<Message> &data);
bool IsPaused() const { return isPaused; }
void SetPaused(bool paused) { isPaused = paused; }
// Step-by-GAM / per-thread: called by each output broker.
// threadName is the OS thread name (from Threads::Name(Threads::Id())).
// Decrements stepRemaining only when stepThreadFilter is empty or matches;
// when stepRemaining reaches 0, sets isPaused = true and records gamName.
void ConsumeStepIfNeeded(const char8 *gamName,
const char8 *threadName = NULL_PTR(const char8 *));
// TCP-transport-specific methods (not part of DebugServiceI)
void GetStepStatus(BasicTCPSocket *client);
// Step n cycles; if threadName is non-null/non-empty, only that thread advances.
void Step(uint32 n, const char8 *threadName = NULL_PTR(const char8 *));
// Read the current raw value of a signal from its memory address.
void GetSignalValue(const char8 *name, BasicTCPSocket *client);
static bool GetFullObjectName(const Object &obj, StreamString &fullPath);
uint32 ForceSignal(const char8 *name, const char8 *valueStr);
uint32 UnforceSignal(const char8 *name);
uint32 TraceSignal(const char8 *name, bool enable, uint32 decimation = 1);
uint32 SetBreak(const char8 *name, uint8 op, float64 threshold);
uint32 ClearBreak(const char8 *name);
bool IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable);
void Discover(BasicTCPSocket *client);
void InfoNode(const char8 *path, BasicTCPSocket *client);
void ListNodes(const char8 *path, BasicTCPSocket *client);
@@ -112,9 +91,6 @@ public:
StreamString path;
};
uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs);
uint32 UnmonitorSignal(const char8 *path);
private:
void HandleCommand(StreamString cmd, BasicTCPSocket *client);
void UpdateBrokersActiveStatus();
@@ -178,17 +154,140 @@ private:
FastPollingMutexSem mutex;
TraceRingBuffer traceBuffer;
/**
* @brief Mutex protecting traceBuffer.Push() from concurrent RT broker threads.
*
* TraceRingBuffer is designed for single-producer / single-consumer access.
* In practice, multiple RT threads (one per RealTimeThread) call ProcessSignal()
* concurrently, making it a multi-producer scenario. Without a lock they can
* both pass the free-space check, both pick the same write index, and corrupt
* each other's samples. This mutex serialises the Push path.
*
* The Streamer thread (sole consumer) never holds this lock, so the lock is
* only contended between RT threads — not between RT and Streamer.
*
* FIX #2 — TraceRingBuffer multi-producer race.
*/
FastPollingMutexSem tracePushMutex;
/**
* @brief Mutex protecting the activeClient pointer.
*
* activeClient is currently owned exclusively by the Server thread:
* it is created, read, and destroyed only inside Server(). The mutex
* enforces this invariant and makes future cross-thread access safe.
*
* Rules:
* - Hold clientMutex whenever assigning (including to NULL) activeClient.
* - Do NOT hold clientMutex across blocking I/O calls (Read / Write)
* to avoid starving threads that only need the pointer.
* - Any future code that needs to send data to the active client from a
* non-Server thread must lock clientMutex, copy the pointer, release
* the lock, then do the Write.
*
* FIX #2 — activeClient cross-thread safety.
*/
FastPollingMutexSem clientMutex;
BasicTCPSocket *activeClient;
/**
* Maximum payload bytes in a single UDP datagram (leave room for IP+UDP headers
* on a standard 1500-byte Ethernet MTU). Used in the Streamer to decide when
* to flush the current packet and start a new one.
*
* FIX #1 — named constant to replace the magic number 1400.
*/
static const uint32 STREAMER_MTU = 1400u;
/**
* Size of the streamer assembly buffer. Must be >= STREAMER_MTU so at least
* one full UDP payload can be staged before flushing. A sample that on its own
* would exceed (STREAMER_BUFFER_SIZE - sizeof(TraceHeader) - 16) bytes is
* silently dropped and logged — it can never fit in any packet.
*
* FIX #1 — named constant; prevents silent integer arithmetic relying on
* the array bound being "obviously 4096".
*/
static const uint32 STREAMER_BUFFER_SIZE = 4096u;
// Streamer state persisted across Execute() calls (framework loops Execute)
uint8 streamerPacketBuffer[4096];
uint8 streamerPacketBuffer[STREAMER_BUFFER_SIZE];
uint32 streamerPacketOffset;
uint32 streamerSequenceNumber;
/**
* @brief Maximum commands allowed per 1-second sliding window per client.
*
* A client sending more than this many commands in one second is disconnected.
* This prevents a tight command loop from monopolising the Server thread and
* starving normal usage. The limit is intentionally generous (100/s) so that
* legitimate scripted tooling is unaffected.
*
* FIX #6 — TCP command rate limiting.
*/
static const uint32 CMD_RATE_LIMIT = 100u;
/**
* @brief Close the active TCP client if no data is received for this many ms.
*
* A client that sends a partial command and never completes it would otherwise
* hold the single active-client slot open indefinitely, blocking all other
* connections. After CLIENT_IDLE_TIMEOUT_MS of silence the connection is
* closed and the slot freed.
*
* FIX #8 — active-client idle timeout.
*/
static const uint32 CLIENT_IDLE_TIMEOUT_MS = 30000u;
/**
* @brief Maximum array elements returned by GetSignalValue / VALUE command.
*
* Without a cap, a 1-million-element uint8 array would produce a multi-MB
* response string, exhausting heap and blocking the Server thread. Elements
* beyond this limit are omitted; the response includes a "Truncated" flag.
*
* FIX #9 — GetSignalValue unbounded array output.
*/
static const uint32 GET_VALUE_MAX_ELEMENTS = 256u;
// Rate-limiter state (per active TCP connection, reset on each new connection)
uint32 cmdCountInWindow; // commands received in the current 1-second window
uint64 cmdWindowStartMs; // start of the current window, in milliseconds
// Idle-timeout state
uint64 lastDataTimeMs; // last time the active client sent any data (ms)
/**
* @brief Carry-over buffer for multi-segment TCP commands.
*
* A single TCP Read() call may return only part of a command if the OS
* delivers it in multiple segments. Bytes after the last '\n' in the
* current receive window are saved here and prepended to the next Read().
* This ensures that "FORCE Signal " arriving in one segment and "123\n"
* arriving in the next are assembled into the single command
* "FORCE Signal 123" before dispatch.
*
* The buffer is bounded by INPUT_BUFFER_MAX (8 KiB). If a client sends
* more than that without a newline it is disconnected.
*
* FIX #10 — incomplete input buffer handling for multi-line commands.
*/
StreamString inputBuffer;
/**
* @brief Maximum carry-over bytes allowed before a client is disconnected.
*
* A client that sends a very long line without a newline would otherwise
* grow inputBuffer without bound. At 8 KiB the limit is generous enough
* for any legitimate command while still protecting against memory exhaustion.
*
* FIX #10 — DoS guard for the input carry-over buffer.
*/
static const uint32 INPUT_BUFFER_MAX = 8192u;
ConfigurationDatabase fullConfig;
bool manualConfigSet;
static DebugService *instance;
};
} // namespace MARTe
@@ -0,0 +1,189 @@
#ifndef DEBUGSERVICEI_H
#define DEBUGSERVICEI_H
/**
* @file DebugServiceI.h
* @brief Abstract interface for the MARTe2 debug/instrumentation service.
*
* All broker wrappers (DebugBrokerWrapper) depend solely on this interface,
* decoupling them from the concrete TCP/UDP implementation. Alternative
* transports — TTY, WebSocket, shared-memory ring, etc. — can be plugged in
* by providing a new concrete implementation without touching any broker or
* application code.
*
* The interface is split into two logical groups:
*
* RT-path API
* Called from broker threads on every RT cycle. Implementations must
* keep these methods as cheap as possible; the only permissible
* synchronisation primitive is a fast spinlock (FastPollingMutexSem).
*
* Control-path API
* Called from server / command-handler threads (TCP, TTY, Web …).
* Latency here is acceptable; correctness and thread-safety matter.
*
* Concrete implementations register themselves during Initialise() via
* SetInstance(). Broker wrappers retrieve the active instance via
* GetInstance(); there is no ORD search on the hot path.
*/
#include "DebugCore.h"
#include "FastPollingMutexSem.h"
#include "Object.h"
#include "StreamString.h"
#include "TypeDescriptor.h"
#include "Vec.h"
namespace MARTe {
// Forward declarations — concrete types are only needed in the implementation.
class MemoryMapBroker;
struct SignalAlias {
StreamString name;
uint32 signalIndex;
};
struct BrokerInfo {
DebugSignalInfo **signalPointers;
uint32 numSignals;
MemoryMapBroker *broker;
volatile bool *anyActiveFlag;
Vec<uint32> *activeIndices;
Vec<uint32> *activeSizes;
FastPollingMutexSem *activeMutex;
volatile bool *anyBreakFlag;
Vec<uint32> *breakIndices;
StreamString gamName;
bool isOutput;
};
/**
* @brief Abstract debug-service interface.
*/
class DebugServiceI {
public:
// -------------------------------------------------------------------------
// Static instance registry
// -------------------------------------------------------------------------
/**
* @brief Return the currently registered debug-service instance, or NULL.
*
* Called on every broker Init() path and from OutpautPauseAndStep().
* Returns NULL when no debug service has been initialised, in which case
* all instrumentation is a no-op.
*/
static DebugServiceI *GetInstance() { return instance; }
/**
* @brief Register @p inst as the global debug-service.
*
* Concrete implementations call this from their Initialise() method.
* Passing NULL deregisters the current instance (called from the
* destructor so dangling pointers are never visible to broker threads).
*/
static void SetInstance(DebugServiceI *inst) { instance = inst; }
virtual ~DebugServiceI() {}
// =========================================================================
// RT-path API (called from broker execute threads every cycle)
// =========================================================================
/**
* @brief Register a signal memory region with the debug service.
*
* Called once per signal during broker Init(). Returns a pointer to the
* internal DebugSignalInfo that the broker caches for use on the hot path.
* Thread-safe; must not be called after the RT loop has started.
*/
virtual DebugSignalInfo *RegisterSignal(void *memoryAddress,
TypeDescriptor type,
const char8 *name,
uint8 numberOfDimensions = 0,
uint32 numberOfElements = 1) = 0;
/**
* @brief Process one signal on the RT path.
*
* Applies forced values (memcpy into signal memory) and, when tracing is
* enabled and the decimation counter fires, pushes a sample to the trace
* ring buffer. Called under the broker's activeMutex; implementations
* must not acquire any lock that is also held by the Server thread.
*/
virtual void ProcessSignal(DebugSignalInfo *signalInfo,
uint32 size,
uint64 timestamp) = 0;
/**
* @brief Register a broker so the service can push active/break index
* updates to it without iterating every signal.
*/
virtual void RegisterBroker(DebugSignalInfo **signalPointers,
uint32 numSignals,
MemoryMapBroker *broker,
volatile bool *anyActiveFlag,
Vec<uint32> *activeIndices,
Vec<uint32> *activeSizes,
FastPollingMutexSem *activeMutex,
volatile bool *anyBreakFlag,
Vec<uint32> *breakIndices,
const char8 *gamName = NULL_PTR(const char8 *),
bool isOutput = false) = 0;
/** @brief Return true if the RT loop is currently held at a pause/breakpoint. */
virtual bool IsPaused() const = 0;
/** @brief Set or clear the paused state (called by break-condition logic). */
virtual void SetPaused(bool paused) = 0;
/** @brief Return true if a step count is pending (stepRemaining > 0). */
virtual bool IsStepPending() const = 0;
/**
* @brief Consume one step credit for the current output-broker cycle.
*
* Called by every output broker after Execute(). No-op when stepRemaining
* is zero (the common case); only acquires a mutex when stepping is active.
*/
virtual void ConsumeStepIfNeeded(
const char8 *gamName,
const char8 *threadName = NULL_PTR(const char8 *)) = 0;
// =========================================================================
// Control-path API (called from server / command-handler threads)
// =========================================================================
virtual uint32 ForceSignal (const char8 *name, const char8 *valueStr) = 0;
virtual uint32 UnforceSignal(const char8 *name) = 0;
virtual uint32 TraceSignal (const char8 *name, bool enable, uint32 decimation = 1) = 0;
virtual uint32 SetBreak (const char8 *name, uint8 op, float64 threshold) = 0;
virtual uint32 ClearBreak (const char8 *name) = 0;
virtual bool IsInstrumented(const char8 *fullPath,
bool &traceable, bool &forcable) = 0;
virtual uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs) = 0;
virtual uint32 UnmonitorSignal (const char8 *path) = 0;
// =========================================================================
// Utility (implementation-agnostic, defined in DebugService.cpp)
// =========================================================================
/**
* @brief Resolve the fully-qualified ORD path of @p obj into @p fullPath.
*
* Static so it can be called without a service instance. All concrete
* implementations (and InitSignals in DebugBrokerWrapper.h) use this
* to build canonical signal names. The definition lives in
* DebugService.cpp alongside FindPathInContainer().
*/
static bool GetFullObjectName(const Object &obj, StreamString &fullPath);
protected:
/** Pointer to the single active debug-service instance. */
static DebugServiceI *instance;
};
} // namespace MARTe
#endif // DEBUGSERVICEI_H
+1 -1
View File
@@ -24,7 +24,7 @@
OBJSX=
SPB = TCPLogger.x DebugService.x
SPB = TCPLogger.x DebugService.x WebDebugService.x
ROOT_DIR=../../..
@@ -0,0 +1,3 @@
export TARGET=x86-linux
include Makefile.inc
@@ -0,0 +1,34 @@
OBJSX=WebDebugService.x
PACKAGE=Components/Interfaces
ROOT_DIR=../../../../
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
INCLUDES += -I$(ROOT_DIR)/Source/Core/Types/Result
INCLUDES += -I$(ROOT_DIR)/Source/Core/Types/Vec
INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/TCPLogger
INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/DebugService
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Logger
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4LoggerService
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L5GAMs
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams
all: $(OBJS) $(SUBPROJ) \
$(BUILD_DIR)/WebDebugService$(LIBEXT) \
$(BUILD_DIR)/WebDebugService$(DLLEXT)
echo $(OBJS)
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,205 @@
#ifndef WEBDEBUGSERVICE_H
#define WEBDEBUGSERVICE_H
#include "BasicTCPSocket.h"
#include "ConfigurationDatabase.h"
#include "DataSourceI.h"
#include "DebugServiceI.h"
#include "EmbeddedServiceMethodBinderI.h"
#include "ReferenceContainer.h"
#include "ReferenceT.h"
#include "SingleThreadService.h"
namespace MARTe {
/**
* @brief HTTP/SSE implementation of DebugServiceI.
*
* Serves a single-page web UI on httpPort (default 8090).
* Endpoints:
* GET / — embedded HTML application
* POST /api/command — execute a debug command; returns text response
* GET /api/events — Server-Sent Events stream for real-time telemetry
*
* SSE event types (JSON payload on each "data:" line):
* {"type":"trace", "name":"…","ts":<ns>,"value":<f64>}
* {"type":"monitor", "name":"…","ts":<ns>,"value":<f64>}
* {"type":"log", "level":"…","msg":"…"}
* {"type":"status", "paused":<bool>,"gam":"…","remaining":<u32>}
* {"type":"discover","signals":[…]}
* {"type":"tree", "tree":{…}}
*
* This is an alternative to DebugService (TCP/UDP) and registers itself as the
* global DebugServiceI singleton on Initialise().
*/
class WebDebugService : public ReferenceContainer,
public EmbeddedServiceMethodBinderI,
public DebugServiceI {
public:
friend class WebDebugServiceTest;
CLASS_REGISTER_DECLARATION()
WebDebugService();
virtual ~WebDebugService();
virtual bool Initialise(StructuredDataI &data);
// -------------------------------------------------------------------------
// DebugServiceI RT-path overrides
// -------------------------------------------------------------------------
virtual DebugSignalInfo *RegisterSignal(void *memoryAddress,
TypeDescriptor type,
const char8 *name,
uint8 numberOfDimensions = 0,
uint32 numberOfElements = 1);
virtual void ProcessSignal(DebugSignalInfo *signalInfo, uint32 size,
uint64 timestamp);
virtual void RegisterBroker(DebugSignalInfo **signalPointers,
uint32 numSignals, MemoryMapBroker *broker,
volatile bool *anyActiveFlag,
Vec<uint32> *activeIndices,
Vec<uint32> *activeSizes,
FastPollingMutexSem *activeMutex,
volatile bool *anyBreakFlag,
Vec<uint32> *breakIndices,
const char8 *gamName = NULL_PTR(const char8 *),
bool isOutput = false);
virtual bool IsPaused() const { return isPaused; }
virtual void SetPaused(bool paused) { isPaused = paused; }
virtual bool IsStepPending() const { return stepRemaining > 0u; }
virtual void ConsumeStepIfNeeded(const char8 *gamName,
const char8 *threadName = NULL_PTR(const char8 *));
// -------------------------------------------------------------------------
// DebugServiceI control-path overrides
// -------------------------------------------------------------------------
virtual uint32 ForceSignal (const char8 *name, const char8 *valueStr);
virtual uint32 UnforceSignal (const char8 *name);
virtual uint32 TraceSignal (const char8 *name, bool enable, uint32 decimation = 1);
virtual uint32 SetBreak (const char8 *name, uint8 op, float64 threshold);
virtual uint32 ClearBreak (const char8 *name);
virtual bool IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable);
virtual uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs);
virtual uint32 UnmonitorSignal (const char8 *path);
// EmbeddedServiceMethodBinderI (framework dispatches to sub-binders)
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info);
// Inject the full config CDB (call after ConfigureApplication)
void SetFullConfig(ConfigurationDatabase &config);
void RebuildConfigFromRegistry();
struct MonitoredSignal {
ReferenceT<DataSourceI> dataSource;
uint32 signalIdx;
uint32 internalID;
uint32 periodMs;
uint64 lastPollTime;
uint32 size;
StreamString path;
};
private:
// HTTP server helpers
bool ParseRequest(BasicTCPSocket *client, StreamString &method,
StreamString &path, StreamString &body);
void SendHttp(BasicTCPSocket *client, uint32 code, const char8 *ct,
StreamString &body);
void HandleRequest(BasicTCPSocket *client, const StreamString &method,
const StreamString &path, const StreamString &body);
void UpgradeToSse(BasicTCPSocket *client);
// Command handler — writes response to StreamString (not a socket)
void HandleCommand(const StreamString &cmd, StreamString &out);
void Discover (StreamString &out);
void InfoNode (const char8 *path, StreamString &out);
void ListNodes (const char8 *path, StreamString &out);
void GetSignalValue(const char8 *name, StreamString &out);
void ServeConfig (StreamString &out);
void GetStepStatus(StreamString &out);
void Step(uint32 n, const char8 *threadName = NULL_PTR(const char8 *));
// Tree / config helpers
uint32 ExportTree(ReferenceContainer *container, StreamString &json,
const char8 *pathPrefix);
void EnrichWithConfig(const char8 *path, StreamString &json);
static void JsonifyDatabase(ConfigurationDatabase &db, StreamString &json);
// Registry patching (same as DebugService)
void PatchRegistry();
// Broker index maintenance
void UpdateBrokersActiveStatus();
void UpdateBrokersBreakStatus();
// SSE broadcast
void BroadcastSse(const char8 *eventType, const StreamString &data);
void RemoveDeadSseClients();
// Service thread entry points
ErrorManagement::ErrorType HttpServer(ExecutionInfo &info);
ErrorManagement::ErrorType Streamer (ExecutionInfo &info);
// Sub-binder so the framework can dispatch to two separate threads
class WdsBinder : public EmbeddedServiceMethodBinderI {
public:
enum ServiceType { Http, Stream };
WdsBinder(WebDebugService *p, ServiceType t) : parent(p), stype(t) {}
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info) {
return (stype == Stream) ? parent->Streamer(info)
: parent->HttpServer(info);
}
private:
WebDebugService *parent;
ServiceType stype;
};
uint16 httpPort;
volatile bool isPaused;
volatile uint32 stepRemaining;
StreamString pausedAtGam;
StreamString stepThreadFilter;
BasicTCPSocket tcpServer;
WdsBinder binderHttp;
WdsBinder binderStream;
SingleThreadService httpService;
SingleThreadService streamerService;
Vec<DebugSignalInfo *> signals;
Vec<SignalAlias> aliases;
Vec<BrokerInfo> brokers;
Vec<MonitoredSignal> monitoredSignals;
FastPollingMutexSem mutex;
FastPollingMutexSem tracePushMutex;
TraceRingBuffer traceBuffer;
// SSE client list
static const uint32 MAX_SSE_CLIENTS = 8u;
BasicTCPSocket *sseClients[MAX_SSE_CLIENTS];
FastPollingMutexSem sseMutex;
// Streamer packet state
uint32 streamerSeq;
uint64 streamerT0Ns; // nanosecond origin for relative timestamps sent over SSE
ConfigurationDatabase fullConfig;
bool manualConfigSet;
static const uint32 GET_VALUE_MAX_ELEMENTS = 256u;
// Log history buffer — replayed to newly connecting SSE clients
static const uint32 LOG_HISTORY_SIZE = 64u;
static const uint32 LOG_ENTRY_MAX_SIZE = 512u;
char8 logHistory[64][512];
uint32 logHistoryWriteIdx;
uint32 logHistoryFill;
FastPollingMutexSem logHistoryMutex;
};
} // namespace MARTe
#endif // WEBDEBUGSERVICE_H
@@ -0,0 +1,611 @@
#ifndef WEBUI_H
#define WEBUI_H
#include "CompilerTypes.h"
namespace MARTe {
// clang-format off
static const char8 *const WEB_UI_HTML =
"<!DOCTYPE html>\n"
"<html lang='en'>\n"
"<head>\n"
"<meta charset='UTF-8'>\n"
"<meta name='viewport' content='width=device-width,initial-scale=1'>\n"
"<title>MARTe2 Debug</title>\n"
"<script src='https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js'></script>\n"
"<style>\n"
":root{--base:#1e1e2e;--mantle:#181825;--crust:#11111b;--text:#cdd6f4;--sub:#a6adc8;\n"
" --ov:#6c7086;--s0:#313244;--s1:#45475a;--s2:#585b70;--blue:#89b4fa;\n"
" --green:#a6e3a1;--yellow:#f9e2af;--red:#f38ba8;--mauve:#cba6f7;--peach:#fab387;\n"
" --teal:#94e2d5;--sky:#89dceb;}\n"
"*{box-sizing:border-box;margin:0;padding:0;}\n"
"body{background:var(--base);color:var(--text);font-family:'JetBrains Mono','Fira Code',monospace;\n"
" font-size:12px;height:100vh;display:flex;flex-direction:column;overflow:hidden;}\n"
"#toolbar{background:var(--mantle);border-bottom:1px solid var(--s0);padding:5px 8px;\n"
" display:flex;align-items:center;gap:6px;flex-shrink:0;flex-wrap:wrap;}\n"
".dot{width:9px;height:9px;border-radius:50%;background:var(--red);flex-shrink:0;}\n"
".dot.ok{background:var(--green);}\n"
".dot.pause{background:var(--yellow);animation:blink 1s infinite;}\n"
"@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}\n"
"button{background:var(--s0);color:var(--text);border:1px solid var(--s1);border-radius:3px;\n"
" padding:2px 7px;cursor:pointer;font-size:11px;font-family:inherit;}\n"
"button:hover{background:var(--s1);}\n"
"button.act{background:var(--blue);color:var(--base);border-color:var(--blue);}\n"
"button.danger{background:var(--red);color:var(--base);border-color:var(--red);}\n"
"input[type=text],input[type=number]{background:var(--s0);color:var(--text);\n"
" border:1px solid var(--s1);border-radius:3px;padding:2px 5px;font-size:11px;font-family:inherit;}\n"
"input:focus{outline:none;border-color:var(--blue);}\n"
"label{color:var(--sub);font-size:11px;}\n"
"select{background:var(--s0);color:var(--text);border:1px solid var(--s1);border-radius:3px;\n"
" padding:2px 5px;font-size:11px;font-family:inherit;}\n"
".sep{width:1px;height:18px;background:var(--s1);flex-shrink:0;}\n"
"#main{display:flex;flex:1;overflow:hidden;}\n"
"#left{width:255px;background:var(--mantle);border-right:1px solid var(--s0);\n"
" display:flex;flex-direction:column;overflow:hidden;flex-shrink:0;}\n"
"#center{flex:1;display:flex;flex-direction:column;overflow:hidden;}\n"
"#right{width:215px;background:var(--mantle);border-left:1px solid var(--s0);\n"
" display:flex;flex-direction:column;overflow:hidden;flex-shrink:0;}\n"
"#bottom{height:135px;background:var(--mantle);border-top:1px solid var(--s0);\n"
" display:flex;flex-direction:column;flex-shrink:0;}\n"
".ph{background:var(--crust);padding:4px 7px;font-size:11px;color:var(--sub);\n"
" display:flex;align-items:center;justify-content:space-between;flex-shrink:0;\n"
" border-bottom:1px solid var(--s0);}\n"
".ph span{font-weight:bold;color:var(--blue);}\n"
"#tree{flex:1;overflow-y:auto;padding:3px;}\n"
".tr{display:flex;align-items:center;padding:2px 3px;border-radius:3px;gap:2px;cursor:default;}\n"
".tr:hover{background:var(--s0);}\n"
".tog{width:13px;text-align:center;cursor:pointer;color:var(--ov);font-size:10px;flex-shrink:0;}\n"
".ico{flex-shrink:0;font-size:10px;}\n"
".tn{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;}\n"
".sig{color:var(--green);}\n"
".tbtns{display:none;gap:2px;}\n"
".tr:hover .tbtns{display:flex;}\n"
".tb{background:none;border:none;padding:1px 3px;font-size:10px;cursor:pointer;\n"
" border-radius:2px;color:var(--sub);}\n"
".tb:hover{background:var(--s1);color:var(--text);}\n"
".tch{padding-left:13px;}\n"
"#ctrls{padding:4px 7px;display:flex;align-items:center;gap:5px;background:var(--crust);\n"
" border-bottom:1px solid var(--s0);flex-shrink:0;flex-wrap:wrap;}\n"
"#ca{flex:1;padding:6px;overflow:hidden;position:relative;}\n"
"#mc{display:block;width:100%;height:100%;}\n"
"#traced{flex:1;overflow-y:auto;padding:3px;}\n"
".ti{display:flex;align-items:center;gap:3px;padding:2px 4px;border-radius:3px;margin-bottom:1px;}\n"
".ti:hover{background:var(--s0);}\n"
".tc{width:7px;height:7px;border-radius:2px;flex-shrink:0;}\n"
".tnm{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;}\n"
".tv{font-size:10px;color:var(--yellow);min-width:55px;text-align:right;flex-shrink:0;}\n"
".titbs{display:none;gap:2px;}\n"
".ti:hover .titbs{display:flex;}\n"
"#ltb{padding:3px 5px;display:flex;gap:6px;align-items:center;background:var(--crust);\n"
" border-bottom:1px solid var(--s0);flex-shrink:0;}\n"
"#lc{flex:1;overflow-y:auto;padding:2px 5px;}\n"
".le{font-size:10px;line-height:1.4;font-family:monospace;white-space:pre-wrap;word-break:break-all;}\n"
".lDEBUG{color:var(--ov);}\n"
".lINFO{color:var(--text);}\n"
".lWARNING{color:var(--yellow);}\n"
".lERROR{color:var(--red);}\n"
".mo{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);\n"
" z-index:100;align-items:center;justify-content:center;}\n"
".mo.show{display:flex;}\n"
".mb{background:var(--mantle);border:1px solid var(--s1);border-radius:7px;padding:15px;min-width:290px;}\n"
".mb h3{color:var(--blue);margin-bottom:11px;font-size:13px;}\n"
".fr{display:flex;align-items:center;gap:7px;margin-bottom:7px;}\n"
".fr label{min-width:75px;color:var(--sub);}\n"
".fr input,.fr select,.fr textarea{flex:1;}\n"
"textarea{background:var(--s0);color:var(--text);border:1px solid var(--s1);border-radius:3px;\n"
" padding:2px 5px;font-size:11px;font-family:inherit;resize:vertical;}\n"
"textarea:focus{outline:none;border-color:var(--blue);}\n"
".mbtns{display:flex;gap:7px;justify-content:flex-end;margin-top:11px;}\n"
"::-webkit-scrollbar{width:5px;height:5px;}\n"
"::-webkit-scrollbar-track{background:var(--mantle);}\n"
"::-webkit-scrollbar-thumb{background:var(--s1);border-radius:3px;}\n"
"</style>\n"
"</head>\n"
"<body>\n"
"<div id='toolbar'>\n"
" <div id='sdot' class='dot'></div>\n"
" <span id='stxt' style='color:var(--sub);font-size:11px'>Connecting\xe2\x80\xa6</span>\n"
" <div class='sep'></div>\n"
" <button onclick='cmd(\"DISCOVER\")'>Discover</button>\n"
" <button onclick='cmd(\"TREE\")'>Tree</button>\n"
" <div class='sep'></div>\n"
" <button id='pbtn' onclick='togglePause()'>Pause</button>\n"
" <div class='sep'></div>\n"
" <label>Step:</label>\n"
" <input type='number' id='sn' value='1' min='1' style='width:45px'>\n"
" <input type='text' id='sthr' list='thrnodes' placeholder='thread' style='width:90px'>\n"
" <datalist id='thrnodes'></datalist>\n"
" <button onclick='doStep()'>Step</button>\n"
" <button onclick='openMsg()'>Msg</button>\n"
" <div class='sep'></div>\n"
" <label>Win:</label>\n"
" <input type='number' id='winms' value='5000' min='100' style='width:60px' onchange='winMs=+this.value'>\n"
" <button onclick='clearChart()'>Clear</button>\n"
" <span id='ss' style='color:var(--yellow);font-size:11px'></span>\n"
"</div>\n"
"<div id='main'>\n"
" <div id='left'>\n"
" <div class='ph'><span>App Tree</span><button onclick='cmd(\"TREE\")' style='font-size:10px'>&#8635;</button></div>\n"
" <div id='tree'><div style='color:var(--ov);padding:7px;font-size:11px'>Click Tree or Discover\xe2\x80\xa6</div></div>\n"
" </div>\n"
" <div id='center'>\n"
" <div id='ctrls'>\n"
" <label>Signal:</label>\n"
" <select id='sigsel' style='width:150px'><option value=''>-- pick --</option></select>\n"
" <button onclick='addToChart()'>+ Plot</button>\n"
" <button onclick='toggleFollow()' id='fbtn' class='act'>Follow</button>\n"
" </div>\n"
" <div id='ca' ondragover='event.preventDefault()' ondrop='dropOnChart(event)'><canvas id='mc'></canvas></div>\n"
" </div>\n"
" <div id='right'>\n"
" <div class='ph'><span>Traced</span></div>\n"
" <div id='traced'></div>\n"
" </div>\n"
"</div>\n"
"<div id='bottom'>\n"
" <div class='ph'>\n"
" <span>Log</span>\n"
" <div style='display:flex;gap:5px;align-items:center'>\n"
" <label><input type='checkbox' id='fdbg' checked> D</label>\n"
" <label><input type='checkbox' id='finf' checked> I</label>\n"
" <label><input type='checkbox' id='fwrn' checked> W</label>\n"
" <label><input type='checkbox' id='ferr' checked> E</label>\n"
" <button onclick='document.getElementById(\"lc\").innerHTML=\"\"'>Clr</button>\n"
" </div>\n"
" </div>\n"
" <div id='lc'></div>\n"
"</div>\n"
"<!-- Modals -->\n"
"<div class='mo' id='frcm'>\n"
" <div class='mb'>\n"
" <h3>Force Signal</h3>\n"
" <div class='fr'><label>Signal:</label><span id='frcn' style='color:var(--green);font-size:11px;flex:1;overflow:hidden;text-overflow:ellipsis'></span></div>\n"
" <div class='fr'><label>Value:</label><input type='text' id='frcv'></div>\n"
" <div class='mbtns'>\n"
" <button onclick='closeM(\"frcm\")'>Cancel</button>\n"
" <button onclick='doUnforce()'>Unforce</button>\n"
" <button class='act' onclick='doForce()'>Force</button>\n"
" </div>\n"
" </div>\n"
"</div>\n"
"<div class='mo' id='brkm'>\n"
" <div class='mb'>\n"
" <h3>Break Condition</h3>\n"
" <div class='fr'><label>Signal:</label><span id='brkn' style='color:var(--green);font-size:11px;flex:1;overflow:hidden;text-overflow:ellipsis'></span></div>\n"
" <div class='fr'><label>Op:</label><select id='brkop'><option>></option><option><</option><option>==</option><option>>=</option><option><=</option><option>!=</option></select></div>\n"
" <div class='fr'><label>Threshold:</label><input type='number' id='brkthr' step='any' value='0'></div>\n"
" <div class='mbtns'>\n"
" <button onclick='closeM(\"brkm\")'>Cancel</button>\n"
" <button onclick='doClrBreak()'>Clear</button>\n"
" <button class='act' onclick='doBreak()'>Set</button>\n"
" </div>\n"
" </div>\n"
"</div>\n"
"<div class='mo' id='monm'>\n"
" <div class='mb'>\n"
" <h3>Monitor Signal</h3>\n"
" <div class='fr'><label>Signal:</label><span id='monn' style='color:var(--green);font-size:11px;flex:1;overflow:hidden;text-overflow:ellipsis'></span></div>\n"
" <div class='fr'><label>Period ms:</label><input type='number' id='monp' value='100' min='10'></div>\n"
" <div class='mbtns'>\n"
" <button onclick='closeM(\"monm\")'>Cancel</button>\n"
" <button onclick='doUnmonitor()'>Unmonitor</button>\n"
" <button class='act' onclick='doMonitor()'>Monitor</button>\n"
" </div>\n"
" </div>\n"
"</div>\n"
"<div class='mo' id='msgm'>\n"
" <div class='mb'>\n"
" <h3>Send Message</h3>\n"
" <div class='fr'><label>Dest:</label><input type='text' id='msgd' list='msgnodes' autocomplete='off'></div>\n"
" <datalist id='msgnodes'></datalist>\n"
" <div class='fr'><label>Function:</label><input type='text' id='msgf'></div>\n"
" <div class='fr'><label>Payload:</label><textarea id='msgp' rows='3' placeholder='key=val&#10;key2=val2'></textarea></div>\n"
" <div class='mbtns'>\n"
" <button onclick='closeM(\"msgm\")'>Cancel</button>\n"
" <button class='act' onclick='doMsg()'>Send</button>\n"
" </div>\n"
" </div>\n"
"</div>\n"
"<script>\n"
"var sigs=[],td={},isPaused=false,chart=null,cds={},winMs=5000,follow=true,curSig='',tnodes=[],threads=[];\n"
"var lc=document.getElementById('lc');\n"
"var COLS=['#89b4fa','#a6e3a1','#fab387','#cba6f7','#f38ba8','#94e2d5','#f9e2af','#89dceb'];\n"
"var ci=0;\n"
"function nc(){return COLS[ci++%COLS.length];}\n"
"\n"
"// --- SSE ---\n"
"function connect(){\n"
" var es=new EventSource('/api/events');\n"
" es.onopen=function(){\n"
" document.getElementById('sdot').className='dot ok';\n"
" document.getElementById('stxt').textContent='Connected';\n"
" cmd('TREE');cmd('DISCOVER');\n"
" };\n"
" es.onerror=function(){\n"
" document.getElementById('sdot').className='dot';\n"
" document.getElementById('stxt').textContent='Reconnecting\xe2\x80\xa6';\n"
" es.close();setTimeout(connect,3000);\n"
" };\n"
" es.onmessage=function(e){\n"
" try{handle(JSON.parse(e.data));}catch(x){}\n"
" };\n"
"}\n"
"\n"
"function handle(ev){\n"
" if(ev.type==='trace'||ev.type==='monitor'){\n"
" var n=ev.name,ts=ev.ts,v=ev.value;\n"
" var isNew=!td[n];\n"
" if(isNew){td[n]={pts:[],last:0,col:nc(),inC:false,tracing:false,forced:false};addOpt(n);updTraced();}\n"
" td[n].pts.push({x:ts,y:v});\n"
" if(td[n].pts.length>10000)td[n].pts.shift();\n"
" td[n].last=v;\n"
" } else if(ev.type==='log'){\n"
" appLog(ev.level,ev.msg);\n"
" } else if(ev.type==='status'){\n"
" isPaused=ev.paused;\n"
" updPause();\n"
" var ss=document.getElementById('ss');\n"
" if(ev.paused){\n"
" ss.textContent='PAUSED'+(ev.gam?' at '+ev.gam:'')+(ev.remaining?' ('+ev.remaining+'step)':'');\n"
" document.getElementById('sdot').className='dot pause';\n"
" } else {\n"
" ss.textContent='';\n"
" document.getElementById('sdot').className='dot ok';\n"
" }\n"
" } else if(ev.type==='discover'){\n"
" sigs=ev.signals||[];\n"
" buildSel();\n"
" } else if(ev.type==='tree'){\n"
" renderTree(ev.tree);\n"
" }\n"
"}\n"
"\n"
"// --- Chart ---\n"
"function initChart(){\n"
" var ctx=document.getElementById('mc').getContext('2d');\n"
" chart=new Chart(ctx,{\n"
" type:'line',\n"
" data:{datasets:[]},\n"
" options:{\n"
" animation:false,responsive:true,maintainAspectRatio:false,parsing:false,\n"
" plugins:{legend:{display:true,position:'bottom',\n"
" labels:{color:'#a6adc8',boxWidth:12,font:{size:10},\n"
" generateLabels:function(ch){return ch.data.datasets.map(function(ds,i){\n"
" return {text:ds.label.split('.').pop(),fillStyle:ds.borderColor,\n"
" strokeStyle:ds.borderColor,lineWidth:0,datasetIndex:i};});}},\n"
" onClick:function(e,li){remFromChart(chart.data.datasets[li.datasetIndex].label);}\n"
" }},\n"
" scales:{\n"
" x:{type:'linear',ticks:{color:'#6c7086',maxTicksLimit:8,\n"
" callback:function(v){return (v/1000).toFixed(1)+'s';}},\n"
" grid:{color:'#313244'}},\n"
" y:{ticks:{color:'#6c7086'},grid:{color:'#313244'}}\n"
" }\n"
" }\n"
" });\n"
" resizeChart();\n"
" window.addEventListener('resize',resizeChart);\n"
"}\n"
"\n"
"function resizeChart(){\n"
" var a=document.getElementById('ca');\n"
" var c=document.getElementById('mc');\n"
" c.width=a.clientWidth;c.height=a.clientHeight;\n"
" if(chart)chart.resize();\n"
"}\n"
"\n"
"function updChart(){\n"
" if(!chart)return;\n"
" var keys=Object.keys(cds);\n"
" if(keys.length===0)return;\n"
" // Find the latest timestamp across all charted signals for follow mode\n"
" var latestTs=0;\n"
" for(var i=0;i<keys.length;i++){\n"
" var t=td[keys[i]];\n"
" if(t&&t.pts.length>0){var lt=t.pts[t.pts.length-1].x;if(lt>latestTs)latestTs=lt;}\n"
" }\n"
" for(var i=0;i<keys.length;i++){\n"
" var n=keys[i],d=cds[n],t=td[n];\n"
" if(!t)continue;\n"
" if(follow){\n"
" var pts=[],minT=latestTs-winMs;\n"
" for(var j=0;j<t.pts.length;j++){if(t.pts[j].x>=minT)pts.push(t.pts[j]);}\n"
" chart.data.datasets[d].data=pts;\n"
" } else {\n"
" chart.data.datasets[d].data=t.pts.slice();\n"
" }\n"
" }\n"
" chart.update('none');\n"
"}\n"
"setInterval(function(){if(Object.keys(cds).length>0)updChart();tickValues();},100);\n"
"\n"
"function addToChart(){\n"
" var n=document.getElementById('sigsel').value;\n"
" if(!n||cds.hasOwnProperty(n))return;\n"
" if(!td[n])td[n]={pts:[],last:0,col:nc(),inC:false,tracing:false,forced:false};\n"
" var col=td[n].col,idx=chart.data.datasets.length;\n"
" cds[n]=idx;td[n].inC=true;td[n].tracing=true;\n"
" chart.data.datasets.push({label:n,data:[],borderColor:col,\n"
" backgroundColor:col+'20',borderWidth:1.5,pointRadius:0,tension:0});\n"
" chart.update('none');updTraced();\n"
" cmd('TRACE '+n+' 1');\n"
"}\n"
"\n"
"function addByName(n){\n"
" if(!n||cds.hasOwnProperty(n))return;\n"
" if(!td[n])td[n]={pts:[],last:0,col:nc(),inC:false,tracing:false,forced:false};\n"
" var col=td[n].col,idx=chart.data.datasets.length;\n"
" cds[n]=idx;td[n].inC=true;td[n].tracing=true;\n"
" chart.data.datasets.push({label:n,data:[],borderColor:col,\n"
" backgroundColor:col+'20',borderWidth:1.5,pointRadius:0,tension:0});\n"
" chart.update('none');updTraced();\n"
" cmd('TRACE '+n+' 1');\n"
"}\n"
"\n"
"function dragStart(ev,n){ev.dataTransfer.setData('text',n);}\n"
"function dropOnChart(ev){ev.preventDefault();var n=ev.dataTransfer.getData('text');if(n)addByName(n);}\n"
"\n"
"function remFromChart(n){\n"
" if(!cds.hasOwnProperty(n))return;\n"
" var idx=cds[n];\n"
" chart.data.datasets.splice(idx,1);\n"
" delete cds[n];\n"
" var ks=Object.keys(cds);\n"
" for(var i=0;i<ks.length;i++){if(cds[ks[i]]>idx)cds[ks[i]]--;}\n"
" if(td[n])td[n].inC=false;\n"
" chart.update('none');updTraced();\n"
"}\n"
"\n"
"function clearChart(){\n"
" var ks=Object.keys(cds);\n"
" for(var i=0;i<ks.length;i++){if(td[ks[i]])td[ks[i]].inC=false;}\n"
" chart.data.datasets=[];cds={};\n"
" chart.update('none');updTraced();\n"
"}\n"
"\n"
"function toggleFollow(){\n"
" follow=!follow;\n"
" document.getElementById('fbtn').className=follow?'act':'';\n"
"}\n"
"\n"
"// --- Traced list ---\n"
"function escId(s){return String(s).replace(/[^a-zA-Z0-9]/g,'_');}\n"
"function updTraced(){\n"
" var h='',ks=Object.keys(td);\n"
" for(var i=0;i<ks.length;i++){\n"
" var n=ks[i],t=td[n];\n"
" var v=typeof t.last==='number'?t.last.toPrecision(5):String(t.last);\n"
" var sn=n.split('.').pop();\n"
" h+='<div class=\"ti\" draggable=\"true\" ondragstart=\"dragStart(event,\\''+escJ(n)+'\\')\">'\n"
" h+='<div class=\"tc\" style=\"background:'+t.col+'\"></div>';\n"
" h+='<div class=\"tnm\" title=\"'+escH(n)+'\">'+escH(sn)+'</div>';\n"
" h+='<div class=\"tv\" id=\"tv_'+escId(n)+'\">'+escH(v)+'</div>';\n"
" h+='<div class=\"titbs\">';\n"
" if(t.inC)h+='<button class=\"tb\" onclick=\"remFromChart(\\''+escJ(n)+'\\')\">&#10005;</button>';\n"
" else h+='<button class=\"tb\" onclick=\"addByName(\\''+escJ(n)+'\\')\">&#9654;</button>';\n"
" if(t.tracing)h+='<button class=\"tb\" title=\"Untrace\" onclick=\"untraceN(\\''+escJ(n)+'\\')\">T&#215;</button>';\n"
" if(t.forced) h+='<button class=\"tb\" title=\"Unforce\" onclick=\"doUnforceN(\\''+escJ(n)+'\\')\">F&#215;</button>';\n"
" else h+='<button class=\"tb\" onclick=\"openFrc(\\''+escJ(n)+'\\')\">F</button>';\n"
" h+='</div></div>';\n"
" }\n"
" document.getElementById('traced').innerHTML=h;\n"
"}\n"
"function untraceN(n){cmd('TRACE '+n+' 0');if(td[n])td[n].tracing=false;updTraced();}\n"
"function doUnforceN(n){cmd('UNFORCE '+n);if(td[n])td[n].forced=false;updTraced();}\n"
"function tickValues(){\n"
" var ks=Object.keys(td);\n"
" for(var i=0;i<ks.length;i++){\n"
" var n=ks[i],t=td[n];\n"
" var el=document.getElementById('tv_'+escId(n));\n"
" if(el){var v=typeof t.last==='number'?t.last.toPrecision(5):String(t.last);el.textContent=v;}\n"
" }\n"
"}\n"
"\n"
"// --- Signal select ---\n"
"function buildSel(){\n"
" var s=document.getElementById('sigsel');\n"
" s.innerHTML='<option value=\"\">-- pick --</option>';\n"
" for(var i=0;i<sigs.length;i++){\n"
" s.innerHTML+='<option value=\"'+escH(sigs[i].name)+'\">'+escH(sigs[i].name)+'</option>';\n"
" }\n"
"}\n"
"function addOpt(n){\n"
" var s=document.getElementById('sigsel');\n"
" for(var i=0;i<s.options.length;i++){if(s.options[i].value===n)return;}\n"
" s.innerHTML+='<option value=\"'+escH(n)+'\">'+escH(n)+'</option>';\n"
"}\n"
"\n"
"// --- Log ---\n"
"function appLog(lv,msg){\n"
" var f={DEBUG:document.getElementById('fdbg').checked,\n"
" INFO:document.getElementById('finf').checked,\n"
" WARNING:document.getElementById('fwrn').checked,\n"
" ERROR:document.getElementById('ferr').checked};\n"
" if(lv==='Information')lv='INFO';\n"
" else if(lv==='Warning')lv='WARNING';\n"
" else if(lv==='Debug')lv='DEBUG';\n"
" else if(lv==='FatalError'||lv==='Error'||lv==='WARN')lv='ERROR';\n"
" if(!f[lv])return;\n"
" var d=document.createElement('div');\n"
" d.className='le l'+lv;\n"
" var now=new Date();\n"
" var ts=now.toTimeString().split(' ')[0]+'.'+String(now.getMilliseconds()).padStart(3,'0');\n"
" d.textContent='['+ts+']['+lv.substring(0,4)+'] '+msg;\n"
" lc.appendChild(d);\n"
" if(lc.children.length>500)lc.removeChild(lc.firstChild);\n"
" lc.scrollTop=lc.scrollHeight;\n"
"}\n"
"\n"
"// --- Tree ---\n"
"function renderTree(node){\n"
" tnodes=[];threads=[];\n"
" var c=document.getElementById('tree');\n"
" c.innerHTML='';\n"
" if(!node)return;\n"
" renderNode(node,c,0,'');\n"
" var dl=document.getElementById('thrnodes');\n"
" dl.innerHTML='';\n"
" for(var i=0;i<threads.length;i++){\n"
" var op=document.createElement('option');op.value=threads[i];dl.appendChild(op);\n"
" }\n"
"}\n"
"function renderNode(node,parent,depth,pathPfx){\n"
" var fullPath=pathPfx?(pathPfx+'.'+node.Name):node.Name;\n"
" var isRoot=(node.Class==='ObjectRegistryDatabase');\n"
" if(isRoot)fullPath='';\n"
" var isSig=node.IsTraceable||node.IsForcable;\n"
" if(!isRoot&&fullPath&&!isSig)tnodes.push(fullPath);\n"
" if(node.Class==='RealTimeThread'&&node.Name)threads.push(node.Name);\n"
" var div=document.createElement('div');\n"
" div.className='tn-node';\n"
" var row=document.createElement('div');\n"
" row.className='tr';\n"
" row.style.paddingLeft=(3+depth*13)+'px';\n"
" var hasCh=node.Children&&node.Children.length>0;\n"
" var tog=document.createElement('span');\n"
" tog.className='tog';\n"
" if(hasCh){\n"
" tog.textContent='\\u25b8';\n"
" tog.onclick=function(){\n"
" var ch=div.querySelector('.tch');\n"
" if(ch){ch.style.display=ch.style.display==='none'?'':'none';\n"
" tog.textContent=ch.style.display==='none'?'\\u25b8':'\\u25be';}\n"
" };\n"
" }\n"
" var ico=document.createElement('span');\n"
" ico.className='ico';\n"
" ico.textContent=isSig?'\\u25c6':(hasCh?'\\u25c9':'\\u25cb');\n"
" ico.style.color=isSig?'var(--green)':(hasCh?'var(--blue)':'var(--ov)');\n"
" var nm=document.createElement('span');\n"
" nm.className='tn'+(isSig?' sig':'');\n"
" nm.textContent=node.Name;\n"
" nm.title=(node.Class||'')+(node.Type?' ['+node.Type+']':'');\n"
" row.appendChild(tog);row.appendChild(ico);row.appendChild(nm);\n"
" if(isSig){\n"
" var btns=document.createElement('span');\n"
" btns.className='tbtns';\n"
" var fp=fullPath;\n"
" if(node.IsTraceable){\n"
" addBtn(btns,'T','Trace',function(){if(!td[fp])td[fp]={pts:[],last:0,col:nc(),inC:false,tracing:false,forced:false};td[fp].tracing=true;cmd('TRACE '+fp+' 1');updTraced();});\n"
" addBtn(btns,'\\u25b6','Plot',function(){addByName(fp);});\n"
" }\n"
" if(node.IsForcable){\n"
" addBtn(btns,'F','Force',function(){openFrc(fp);});\n"
" addBtn(btns,'B','Break',function(){openBrk(fp);});\n"
" }\n"
" addBtn(btns,'M','Monitor',function(){openMon(fp);});\n"
" row.appendChild(btns);\n"
" }\n"
" div.appendChild(row);\n"
" if(hasCh){\n"
" var chDiv=document.createElement('div');\n"
" chDiv.className='tch';\n"
" for(var i=0;i<node.Children.length;i++){\n"
" renderNode(node.Children[i],chDiv,depth+1,isRoot?'':fullPath);\n"
" }\n"
" div.appendChild(chDiv);\n"
" }\n"
" parent.appendChild(div);\n"
"}\n"
"function addBtn(parent,lbl,title,fn){\n"
" var b=document.createElement('button');\n"
" b.className='tb';b.textContent=lbl;b.title=title;\n"
" b.onclick=fn;parent.appendChild(b);\n"
"}\n"
"\n"
"// --- Commands ---\n"
"function cmd(c){\n"
" fetch('/api/command',{method:'POST',headers:{'Content-Type':'text/plain'},body:c})\n"
" .then(function(r){return r.text();})\n"
" .then(function(txt){\n"
" if(c.indexOf('DISCOVER')===0){\n"
" try{\n"
" var clean=txt.replace(/\\nOK DISCOVER\\n?$/,'');\n"
" var r=JSON.parse(clean);\n"
" if(r.Signals){sigs=r.Signals;buildSel();}\n"
" }catch(e){}\n"
" } else if(c.indexOf('TREE')===0){\n"
" try{renderTree(JSON.parse(txt.replace(/\\nOK TREE\\n?$/,'')));}\n"
" catch(e){appLog('ERROR','Tree parse: '+e);}\n"
" } else if(c.indexOf('STEP_STATUS')===0){\n"
" try{\n"
" var clean=txt.replace(/\\nOK STEP_STATUS\\n?$/,'');\n"
" var st=JSON.parse(clean);\n"
" var ev={type:'status',paused:st.Paused,gam:st.PausedAtGam,remaining:st.StepRemaining};\n"
" handle(ev);\n"
" }catch(e){}\n"
" }\n"
" })\n"
" .catch(function(e){appLog('ERROR','cmd failed: '+e);});\n"
"}\n"
"\n"
"function togglePause(){\n"
" cmd(isPaused?'RESUME':'PAUSE');\n"
"}\n"
"function updPause(){\n"
" var b=document.getElementById('pbtn');\n"
" b.textContent=isPaused?'Resume':'Pause';\n"
" b.className=isPaused?'danger':'';\n"
"}\n"
"function doStep(){\n"
" var n=document.getElementById('sn').value||'1';\n"
" var t=document.getElementById('sthr').value;\n"
" var c='STEP '+n;if(t)c+=' '+t;\n"
" cmd(c);\n"
"}\n"
"\n"
"// --- Modals ---\n"
"function openFrc(n){curSig=n;document.getElementById('frcn').textContent=n;\n"
" document.getElementById('frcv').value='';showM('frcm');}\n"
"function doForce(){var v=document.getElementById('frcv').value;\n"
" if(v!==''){cmd('FORCE '+curSig+' '+v);if(td[curSig])td[curSig].forced=true;}\n"
" closeM('frcm');updTraced();}\n"
"function doUnforce(){cmd('UNFORCE '+curSig);if(td[curSig])td[curSig].forced=false;closeM('frcm');updTraced();}\n"
"\n"
"function openBrk(n){curSig=n;document.getElementById('brkn').textContent=n;showM('brkm');}\n"
"function doBreak(){var op=document.getElementById('brkop').value;\n"
" var t=document.getElementById('brkthr').value;\n"
" cmd('BREAK '+curSig+' '+op+' '+t);closeM('brkm');}\n"
"function doClrBreak(){cmd('BREAK '+curSig+' OFF');closeM('brkm');}\n"
"\n"
"function openMon(n){curSig=n;document.getElementById('monn').textContent=n;showM('monm');}\n"
"function doMonitor(){var p=document.getElementById('monp').value||'100';\n"
" cmd('MONITOR SIGNAL '+curSig+' '+p);closeM('monm');}\n"
"function doUnmonitor(){cmd('UNMONITOR SIGNAL '+curSig);closeM('monm');}\n"
"\n"
"function openMsg(){\n"
" var dl=document.getElementById('msgnodes');\n"
" dl.innerHTML='';\n"
" for(var i=0;i<tnodes.length;i++){\n"
" var op=document.createElement('option');op.value=tnodes[i];dl.appendChild(op);\n"
" }\n"
" showM('msgm');\n"
"}\n"
"function doMsg(){var d=document.getElementById('msgd').value;\n"
" var f=document.getElementById('msgf').value;\n"
" var p=document.getElementById('msgp').value;\n"
" var c='MSG '+d+' '+f+' 0';if(p)c+=' '+p.replace(/\\n/g,' ');\n"
" cmd(c);closeM('msgm');}\n"
"\n"
"function showM(id){document.getElementById(id).className='mo show';}\n"
"function closeM(id){document.getElementById(id).className='mo';}\n"
"document.querySelectorAll('.mo').forEach(function(el){\n"
" el.addEventListener('click',function(e){if(e.target===el)closeM(el.id);});\n"
"});\n"
"\n"
"// --- Utils ---\n"
"function escH(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');}\n"
"function escJ(s){return String(s).replace(/\\\\/g,'\\\\\\\\').replace(/'/g,\"\\\\'\");}\n"
"\n"
"// Init\n"
"initChart();\n"
"connect();\n"
"</script>\n"
"</body>\n"
"</html>\n";
// clang-format on
} // namespace MARTe
#endif // WEBUI_H