diff --git a/Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h b/Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h index 7cfde77..5a06915 100644 --- a/Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h +++ b/Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h @@ -9,6 +9,7 @@ #include "MemoryMapBroker.h" #include "ObjectBuilder.h" #include "ObjectRegistryDatabase.h" +#include "Threads.h" #include "Vec.h" // Original broker headers @@ -55,17 +56,62 @@ static Reference FindByNameRecursive(ReferenceContainer *container, */ class DebugBrokerHelper { public: + // Evaluate a break condition against the current live value of a signal. + // Returns true if the condition is met and execution should be paused. + // Only called when signal->breakOp != BREAK_OFF; reads memoryAddress directly. + static bool EvaluateBreak(const DebugSignalInfo *s) { + if (s->memoryAddress == NULL_PTR(void *)) return false; + float64 val = 0.0; + const TypeDescriptor &t = s->type; + if (t == Float64Bit) val = *static_cast(s->memoryAddress); + else if (t == Float32Bit) val = *static_cast(s->memoryAddress); + else if (t == UnsignedInteger32Bit) val = *static_cast(s->memoryAddress); + else if (t == SignedInteger32Bit) val = *static_cast(s->memoryAddress); + else if (t == UnsignedInteger64Bit) val = static_cast(*static_cast(s->memoryAddress)); + else if (t == SignedInteger64Bit) val = static_cast(*static_cast(s->memoryAddress)); + else if (t == UnsignedInteger16Bit) val = *static_cast(s->memoryAddress); + else if (t == SignedInteger16Bit) val = *static_cast(s->memoryAddress); + else if (t == UnsignedInteger8Bit) val = *static_cast(s->memoryAddress); + else if (t == SignedInteger8Bit) val = *static_cast(s->memoryAddress); + else return false; // unsupported type — skip + const float64 thr = s->breakThreshold; + switch (s->breakOp) { + case BREAK_GT: return val > thr; + case BREAK_LT: return val < thr; + case BREAK_EQ: return val == thr; + case BREAK_GEQ: return val >= thr; + case BREAK_LEQ: return val <= thr; + case BREAK_NEQ: return val != thr; + default: return false; + } + } + + // Spin-wait point for output brokers — called from Execute() AFTER Process(). + // 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; + // 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. + const char8 *tName = Threads::Name(Threads::Id()); + service->ConsumeStepIfNeeded(gamName, tName); + while (service->IsPaused()) Sleep::MSec(10); + } + static void Process(DebugService *service, DebugSignalInfo **signalInfoPointers, Vec &activeIndices, Vec &activeSizes, - FastPollingMutexSem &activeMutex) { + FastPollingMutexSem &activeMutex, + volatile bool *anyBreakFlag, + Vec *breakIndices) { if (service == NULL_PTR(DebugService *)) return; - // Re-establish break logic - while (service->IsPaused()) { - Sleep::MSec(10); - } + // NOTE: No spin here. Spinning for paused state is handled in Execute() of + // OUTPUT brokers only (see OutputPauseAndStep). Input brokers must not block + // because that prevents cross-thread EventSem posts from completing. activeMutex.FastLock(); uint32 n = activeIndices.Size(); @@ -83,6 +129,23 @@ 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 nb = breakIndices->Size(); + for (uint32 i = 0; i < nb; i++) { + uint32 idx = (*breakIndices)[i]; + DebugSignalInfo *s = signalInfoPointers[idx]; + if (s != NULL_PTR(DebugSignalInfo *) && s->breakOp != BREAK_OFF && + EvaluateBreak(s)) { + service->SetPaused(true); + break; + } + } + } + activeMutex.FastUnLock(); } @@ -93,7 +156,8 @@ public: uint32 numCopies, MemoryMapBrokerCopyTableEntry *copyTable, const char8 *functionName, SignalDirection direction, volatile bool *anyActiveFlag, Vec *activeIndices, - Vec *activeSizes, FastPollingMutexSem *activeMutex) { + Vec *activeSizes, FastPollingMutexSem *activeMutex, + volatile bool *anyBreakFlag, Vec *breakIndices) { if (numCopies > 0) { signalInfoPointers = new DebugSignalInfo *[numCopies]; for (uint32 i = 0; i < numCopies; i++) @@ -181,8 +245,12 @@ public: } // Register broker in DebugService for optimized control + bool isOutputBroker = (direction == OutputSignals); service->RegisterBroker(signalInfoPointers, numCopies, mmb, anyActiveFlag, - activeIndices, activeSizes, activeMutex); + activeIndices, activeSizes, activeMutex, + anyBreakFlag, breakIndices, + (functionName != NULL_PTR(const char8 *)) ? functionName : dsPath.Buffer(), + isOutputBroker); } } }; @@ -197,6 +265,9 @@ public: signalInfoPointers = NULL_PTR(DebugSignalInfo **); numSignals = 0; anyActive = false; + anyBreakActive = false; + isOutput = false; + gamName[0] = '\0'; } virtual ~DebugBrokerWrapper() { @@ -206,9 +277,15 @@ public: virtual bool Execute() { bool ret = BaseClass::Execute(); - if (ret && (anyActive || (service && service->IsPaused()))) { + if (ret && (anyActive || anyBreakActive)) { DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices, - activeSizes, activeMutex); + activeSizes, activeMutex, + &anyBreakActive, &breakIndices); + } + // Output brokers are the safe pause point: base Execute has already + // committed data / posted any cross-thread EventSems. + if (ret && isOutput) { + DebugBrokerHelper::OutputPauseAndStep(service, gamName); } return ret; } @@ -220,10 +297,13 @@ public: direction == InputSignals ? "In" : "Out"); if (ret) { numSignals = this->GetNumberOfCopies(); + isOutput = (direction == OutputSignals); + StringHelper::CopyN(gamName, name, 255u); DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers, numSignals, this->copyTable, name, direction, &anyActive, &activeIndices, - &activeSizes, &activeMutex); + &activeSizes, &activeMutex, + &anyBreakActive, &breakIndices); } return ret; } @@ -235,10 +315,13 @@ public: direction == InputSignals ? "In" : "Out"); if (ret) { numSignals = this->GetNumberOfCopies(); + isOutput = (direction == OutputSignals); + StringHelper::CopyN(gamName, name, 255u); DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers, numSignals, this->copyTable, name, direction, &anyActive, &activeIndices, - &activeSizes, &activeMutex); + &activeSizes, &activeMutex, + &anyBreakActive, &breakIndices); } return ret; } @@ -247,8 +330,12 @@ public: DebugSignalInfo **signalInfoPointers; uint32 numSignals; volatile bool anyActive; + volatile bool anyBreakActive; + bool isOutput; + char8 gamName[256]; Vec activeIndices; Vec activeSizes; + Vec breakIndices; FastPollingMutexSem activeMutex; }; @@ -260,6 +347,9 @@ public: signalInfoPointers = NULL_PTR(DebugSignalInfo **); numSignals = 0; anyActive = false; + anyBreakActive = false; + isOutput = false; + gamName[0] = '\0'; } virtual ~DebugBrokerWrapperNoOptim() { @@ -269,9 +359,13 @@ public: virtual bool Execute() { bool ret = BaseClass::Execute(); - if (ret && (anyActive || (service && service->IsPaused()))) { + if (ret && (anyActive || anyBreakActive)) { DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices, - activeSizes, activeMutex); + activeSizes, activeMutex, + &anyBreakActive, &breakIndices); + } + if (ret && isOutput) { + DebugBrokerHelper::OutputPauseAndStep(service, gamName); } return ret; } @@ -281,10 +375,13 @@ public: bool ret = BaseClass::Init(direction, ds, name, gamMem); if (ret) { numSignals = this->GetNumberOfCopies(); + isOutput = (direction == OutputSignals); + StringHelper::CopyN(gamName, name, 255u); DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers, numSignals, this->copyTable, name, direction, &anyActive, &activeIndices, - &activeSizes, &activeMutex); + &activeSizes, &activeMutex, + &anyBreakActive, &breakIndices); } return ret; } @@ -293,8 +390,12 @@ public: DebugSignalInfo **signalInfoPointers; uint32 numSignals; volatile bool anyActive; + volatile bool anyBreakActive; + bool isOutput; + char8 gamName[256]; Vec activeIndices; Vec activeSizes; + Vec breakIndices; FastPollingMutexSem activeMutex; }; @@ -305,6 +406,8 @@ public: signalInfoPointers = NULL_PTR(DebugSignalInfo **); numSignals = 0; anyActive = false; + anyBreakActive = false; + gamName[0] = '\0'; } virtual ~DebugMemoryMapAsyncOutputBroker() { if (signalInfoPointers) @@ -312,9 +415,14 @@ public: } virtual bool Execute() { bool ret = MemoryMapAsyncOutputBroker::Execute(); - if (ret && (anyActive || (service && service->IsPaused()))) { + if (ret && (anyActive || anyBreakActive)) { DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices, - activeSizes, activeMutex); + activeSizes, activeMutex, + &anyBreakActive, &breakIndices); + } + // Async output brokers are always output direction + if (ret) { + DebugBrokerHelper::OutputPauseAndStep(service, gamName); } return ret; } @@ -330,10 +438,11 @@ public: numberOfBuffersIn, cpuMaskIn, stackSizeIn); if (ret) { numSignals = this->GetNumberOfCopies(); + StringHelper::CopyN(gamName, (functionName != NULL_PTR(const char8 *)) ? functionName : "", 255u); DebugBrokerHelper::InitSignals( this, dataSourceIn, service, signalInfoPointers, numSignals, this->copyTable, functionName, direction, &anyActive, &activeIndices, - &activeSizes, &activeMutex); + &activeSizes, &activeMutex, &anyBreakActive, &breakIndices); } return ret; } @@ -341,8 +450,11 @@ public: DebugSignalInfo **signalInfoPointers; uint32 numSignals; volatile bool anyActive; + volatile bool anyBreakActive; + char8 gamName[256]; Vec activeIndices; Vec activeSizes; + Vec breakIndices; FastPollingMutexSem activeMutex; }; @@ -355,6 +467,8 @@ public: signalInfoPointers = NULL_PTR(DebugSignalInfo **); numSignals = 0; anyActive = false; + anyBreakActive = false; + gamName[0] = '\0'; } virtual ~DebugMemoryMapAsyncTriggerOutputBroker() { if (signalInfoPointers) @@ -362,9 +476,13 @@ public: } virtual bool Execute() { bool ret = MemoryMapAsyncTriggerOutputBroker::Execute(); - if (ret && (anyActive || (service && service->IsPaused()))) { + if (ret && (anyActive || anyBreakActive)) { DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices, - activeSizes, activeMutex); + activeSizes, activeMutex, + &anyBreakActive, &breakIndices); + } + if (ret) { + DebugBrokerHelper::OutputPauseAndStep(service, gamName); } return ret; } @@ -380,10 +498,11 @@ public: stackSizeIn); if (ret) { numSignals = this->GetNumberOfCopies(); + StringHelper::CopyN(gamName, (functionName != NULL_PTR(const char8 *)) ? functionName : "", 255u); DebugBrokerHelper::InitSignals( this, dataSourceIn, service, signalInfoPointers, numSignals, this->copyTable, functionName, direction, &anyActive, &activeIndices, - &activeSizes, &activeMutex); + &activeSizes, &activeMutex, &anyBreakActive, &breakIndices); } return ret; } @@ -391,8 +510,11 @@ public: DebugSignalInfo **signalInfoPointers; uint32 numSignals; volatile bool anyActive; + volatile bool anyBreakActive; + char8 gamName[256]; Vec activeIndices; Vec activeSizes; + Vec breakIndices; FastPollingMutexSem activeMutex; }; diff --git a/Source/Components/Interfaces/DebugService/DebugCore.h b/Source/Components/Interfaces/DebugService/DebugCore.h index 49d0529..08057ff 100644 --- a/Source/Components/Interfaces/DebugService/DebugCore.h +++ b/Source/Components/Interfaces/DebugService/DebugCore.h @@ -8,6 +8,17 @@ namespace MARTe { +// Break condition operators stored in DebugSignalInfo::breakOp +enum BreakOp { + BREAK_OFF = 0, + BREAK_GT = 1, // > + BREAK_LT = 2, // < + BREAK_EQ = 3, // == + BREAK_GEQ = 4, // >= + BREAK_LEQ = 5, // <= + BREAK_NEQ = 6 // != +}; + struct DebugSignalInfo { void* memoryAddress; TypeDescriptor type; @@ -16,10 +27,13 @@ struct DebugSignalInfo { uint32 numberOfElements; volatile bool isTracing; volatile bool isForcing; - uint8 forcedValue[1024]; + uint8 forcedValue[1024]; uint32 internalID; volatile uint32 decimationFactor; volatile uint32 decimationCounter; + // Conditional break fields (zero-cost when breakOp == BREAK_OFF) + volatile uint8 breakOp; // BreakOp enum value + float64 breakThreshold; // comparison threshold }; #pragma pack(push, 1) diff --git a/Source/Components/Interfaces/DebugService/DebugService.cpp b/Source/Components/Interfaces/DebugService/DebugService.cpp index 2d1cb55..6d28159 100644 --- a/Source/Components/Interfaces/DebugService/DebugService.cpp +++ b/Source/Components/Interfaces/DebugService/DebugService.cpp @@ -97,6 +97,7 @@ DebugService::DebugService() isServer = false; suppressTimeoutLogs = true; isPaused = false; + stepRemaining = 0u; manualConfigSet = false; activeClient = NULL_PTR(BasicTCPSocket *); streamerPacketOffset = 0u; @@ -431,6 +432,8 @@ DebugSignalInfo *DebugService::RegisterSignal(void *memoryAddress, res->internalID = sigIdx; res->decimationFactor = 1; res->decimationCounter = 0; + res->breakOp = BREAK_OFF; + res->breakThreshold = 0.0; signals.Push(res); } if (sigIdx != 0xFFFFFFFF) { @@ -474,7 +477,10 @@ void DebugService::RegisterBroker(DebugSignalInfo **signalPointers, volatile bool *anyActiveFlag, Vec *activeIndices, Vec *activeSizes, - FastPollingMutexSem *activeMutex) { + FastPollingMutexSem *activeMutex, + volatile bool *anyBreakFlag, + Vec *breakIndices, + const char8 *gamName, bool isOutput) { mutex.FastLock(); BrokerInfo b; b.signalPointers = signalPointers; @@ -484,6 +490,11 @@ void DebugService::RegisterBroker(DebugSignalInfo **signalPointers, b.activeIndices = activeIndices; b.activeSizes = activeSizes; b.activeMutex = activeMutex; + b.anyBreakFlag = anyBreakFlag; + b.breakIndices = breakIndices; + b.isOutput = isOutput; + if (gamName != NULL_PTR(const char8 *)) + b.gamName = gamName; brokers.Push(b); mutex.FastUnLock(); } @@ -525,6 +536,28 @@ void DebugService::UpdateBrokersActiveStatus() { } } +void DebugService::UpdateBrokersBreakStatus() { + for (uint32 i = 0; i < brokers.Size(); i++) { + Vec tempBreak; + uint32 count = 0; + for (uint32 j = 0; j < brokers[i].numSignals; j++) { + DebugSignalInfo *s = brokers[i].signalPointers[j]; + if (s != NULL_PTR(DebugSignalInfo *) && s->breakOp != BREAK_OFF) { + tempBreak.Push(j); + count++; + } + } + if (brokers[i].activeMutex) + brokers[i].activeMutex->FastLock(); + if (brokers[i].breakIndices) + *(brokers[i].breakIndices) = tempBreak; + if (brokers[i].anyBreakFlag) + *(brokers[i].anyBreakFlag) = (count > 0); + if (brokers[i].activeMutex) + brokers[i].activeMutex->FastUnLock(); + } +} + ErrorManagement::ErrorType DebugService::Execute(ExecutionInfo &info) { return ErrorManagement::FatalError; } @@ -731,6 +764,72 @@ void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) { (void)client->Write(resp.Buffer(), s); } } + } else if (token == "STEP") { + StreamString nStr; + uint32 n = 1u; + if (cmd.GetToken(nStr, delims, term)) { + AnyType nVal(UnsignedInteger32Bit, 0u, &n); + AnyType nS(CharString, 0u, nStr.Buffer()); + (void)TypeConvert(nVal, nS); + } + // Optional thread name: STEP [] + StreamString threadStr; + const char8 *threadArg = NULL_PTR(const char8 *); + if (cmd.GetToken(threadStr, delims, term) && threadStr.Size() > 0u) { + threadArg = threadStr.Buffer(); + } + Step(n, threadArg); + if (client) { + StreamString resp; + resp.Printf("OK STEP %u\n", n); + uint32 s = resp.Size(); + (void)client->Write(resp.Buffer(), s); + } + } else if (token == "STEP_STATUS") { + GetStepStatus(client); + } else if (token == "VALUE") { + StreamString sigName; + if (cmd.GetToken(sigName, delims, term)) { + GetSignalValue(sigName.Buffer(), client); + } else if (client) { + const char8 *errResp = "{\"Error\": \"Missing signal name\"}\nOK VALUE\n"; + uint32 s = StringHelper::Length(errResp); + (void)client->Write(errResp, s); + } + } else if (token == "BREAK") { + // BREAK — set break condition + // BREAK OFF — clear break condition + StreamString name, opStr; + if (cmd.GetToken(name, delims, term) && cmd.GetToken(opStr, delims, term)) { + uint32 count = 0; + if (opStr == "OFF") { + count = ClearBreak(name.Buffer()); + } else { + StreamString threshStr; + if (cmd.GetToken(threshStr, delims, term)) { + uint8 op = BREAK_OFF; + if (opStr == ">") op = BREAK_GT; + else if (opStr == "<") op = BREAK_LT; + else if (opStr == "==") op = BREAK_EQ; + else if (opStr == ">=") op = BREAK_GEQ; + else if (opStr == "<=") op = BREAK_LEQ; + else if (opStr == "!=") op = BREAK_NEQ; + if (op != BREAK_OFF) { + float64 threshold = 0.0; + AnyType thrVal(Float64Bit, 0u, &threshold); + AnyType thrStr(CharString, 0u, threshStr.Buffer()); + (void)TypeConvert(thrVal, thrStr); + count = SetBreak(name.Buffer(), op, threshold); + } + } + } + if (client) { + StreamString resp; + resp.Printf("OK BREAK %u\n", count); + uint32 s = resp.Size(); + (void)client->Write(resp.Buffer(), s); + } + } } else if (token == "DISCOVER") Discover(client); else if (token == "MSG") { @@ -1369,6 +1468,159 @@ uint32 DebugService::TraceSignal(const char8 *name, bool enable, return count; } +void DebugService::ConsumeStepIfNeeded(const char8 *gamName, + const char8 *threadName) { + if (stepRemaining == 0u) return; + mutex.FastLock(); + // If a thread filter is set, only the matching OS thread consumes step credits. + if (stepThreadFilter.Size() > 0u && + (threadName == NULL_PTR(const char8 *) || stepThreadFilter != threadName)) { + mutex.FastUnLock(); + return; + } + if (stepRemaining > 0u) { + stepRemaining--; + if (stepRemaining == 0u) { + isPaused = true; + pausedAtGam = (gamName != NULL_PTR(const char8 *)) ? gamName : ""; + } + } + mutex.FastUnLock(); +} + +void DebugService::Step(uint32 n, const char8 *threadName) { + mutex.FastLock(); + stepRemaining = n; + isPaused = false; + stepThreadFilter = (threadName != NULL_PTR(const char8 *)) ? threadName : ""; + mutex.FastUnLock(); +} + +void DebugService::GetStepStatus(BasicTCPSocket *client) { + if (client == NULL_PTR(BasicTCPSocket *)) return; + mutex.FastLock(); + bool paused = isPaused; + uint32 remaining = stepRemaining; + StreamString gam = pausedAtGam; + StreamString threadFilter = stepThreadFilter; + mutex.FastUnLock(); + StreamString resp; + resp.Printf("{\"Paused\": %s, \"PausedAtGam\": \"%s\", \"StepRemaining\": %u, \"StepThread\": \"%s\"}\nOK STEP_STATUS\n", + paused ? "true" : "false", + gam.Buffer(), + remaining, + threadFilter.Buffer()); + uint32 s = resp.Size(); + (void)client->Write(resp.Buffer(), s); +} + +void DebugService::GetSignalValue(const char8 *name, BasicTCPSocket *client) { + if (client == NULL_PTR(BasicTCPSocket *)) return; + mutex.FastLock(); + DebugSignalInfo *sig = NULL_PTR(DebugSignalInfo *); + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (aliases[i].name == name || + SuffixMatch(aliases[i].name.Buffer(), name)) { + sig = signals[aliases[i].signalIndex]; + break; + } + } + if (sig == NULL_PTR(DebugSignalInfo *)) { + mutex.FastUnLock(); + StreamString resp; + resp.Printf("{\"Error\": \"Signal not found: %s\"}\nOK VALUE\n", name); + uint32 s = resp.Size(); + (void)client->Write(resp.Buffer(), s); + return; + } + TypeDescriptor td = sig->type; + uint32 nElem = sig->numberOfElements; + uint32 byteSize = (td.numberOfBits > 0u) ? (td.numberOfBits / 8u) : 1u; + uint32 totalBytes = byteSize * nElem; + 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]; + memset(localBuf, 0, sizeof(localBuf)); + if (sig->memoryAddress != NULL_PTR(void *)) { + memcpy(localBuf, sig->memoryAddress, totalBytes); + } + mutex.FastUnLock(); + + // Build the value string via CharString TypeConvert — the same path used by + // EnrichWithConfig/JsonifyDatabase, which is known to work for all types. + StreamString valueStr; + if (nElem > 1u) { + // Array or matrix: produce comma-separated text + for (uint32 i = 0u; i < nElem; i++) { + char8 elemBuf[128] = {'\0'}; + AnyType srcElem(td, 0u, (void *)(localBuf + i * byteSize)); + AnyType dstStr(CharString, 0u, elemBuf); + dstStr.SetNumberOfElements(0u, 128u); + (void)TypeConvert(dstStr, srcElem); + if (i > 0u) valueStr += ", "; + valueStr += elemBuf; + } + } else { + char8 elemBuf[256] = {'\0'}; + AnyType srcElem(td, 0u, (void *)localBuf); + AnyType dstStr(CharString, 0u, elemBuf); + dstStr.SetNumberOfElements(0u, 256u); + (void)TypeConvert(dstStr, srcElem); + valueStr = elemBuf; + } + + StreamString resp; + resp += "{\"Name\": \""; + resp += name; + resp += "\", \"Value\": \""; + // Escape the value text (quotes inside a string value) + const char8 *vp = valueStr.Buffer(); + while (vp != NULL_PTR(const char8 *) && *vp != '\0') { + if (*vp == '"') resp += "\\\""; + else if (*vp == '\\') resp += "\\\\"; + else { char8 tmp[2] = { *vp, '\0' }; resp += tmp; } + vp++; + } + resp += "\", \"Elements\": "; + resp.Printf("%u}\nOK VALUE\n", nElem); + uint32 s = resp.Size(); + (void)client->Write(resp.Buffer(), s); +} + +uint32 DebugService::SetBreak(const char8 *name, uint8 op, float64 threshold) { + mutex.FastLock(); + uint32 count = 0; + for (uint32 i = 0; i < aliases.Size(); i++) { + if (aliases[i].name == name || + SuffixMatch(aliases[i].name.Buffer(), name)) { + DebugSignalInfo *s = signals[aliases[i].signalIndex]; + s->breakThreshold = threshold; + s->breakOp = op; + count++; + } + } + if (count > 0) + UpdateBrokersBreakStatus(); + mutex.FastUnLock(); + return count; +} + +uint32 DebugService::ClearBreak(const char8 *name) { + mutex.FastLock(); + uint32 count = 0; + for (uint32 i = 0; i < aliases.Size(); i++) { + if (aliases[i].name == name || + SuffixMatch(aliases[i].name.Buffer(), name)) { + signals[aliases[i].signalIndex]->breakOp = BREAK_OFF; + count++; + } + } + if (count > 0) + UpdateBrokersBreakStatus(); + mutex.FastUnLock(); + return count; +} + bool DebugService::IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable) { mutex.FastLock(); diff --git a/Source/Components/Interfaces/DebugService/DebugService.h b/Source/Components/Interfaces/DebugService/DebugService.h index fe8e8c5..e17f8fb 100644 --- a/Source/Components/Interfaces/DebugService/DebugService.h +++ b/Source/Components/Interfaces/DebugService/DebugService.h @@ -32,6 +32,12 @@ struct BrokerInfo { Vec *activeIndices; Vec *activeSizes; FastPollingMutexSem *activeMutex; + // Conditional break — mirrors activeIndices but for break-enabled signals + volatile bool *anyBreakFlag; + Vec *breakIndices; + // GAM association for step-by-GAM + StreamString gamName; + bool isOutput; }; class DebugService : public ReferenceContainer, @@ -55,7 +61,10 @@ public: void RegisterBroker(DebugSignalInfo **signalPointers, uint32 numSignals, MemoryMapBroker *broker, volatile bool *anyActiveFlag, Vec *activeIndices, Vec *activeSizes, - FastPollingMutexSem *activeMutex); + FastPollingMutexSem *activeMutex, + volatile bool *anyBreakFlag, Vec *breakIndices, + const char8 *gamName = NULL_PTR(const char8 *), + bool isOutput = false); virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info); @@ -64,11 +73,27 @@ public: 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 *)); + + 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); @@ -93,6 +118,7 @@ public: private: void HandleCommand(StreamString cmd, BasicTCPSocket *client); void UpdateBrokersActiveStatus(); + void UpdateBrokersBreakStatus(); void InjectTcpLoggerIfNeeded(); uint32 ExportTree(ReferenceContainer *container, StreamString &json, const char8 *pathPrefix); @@ -111,6 +137,9 @@ private: bool isServer; bool suppressTimeoutLogs; volatile bool isPaused; + volatile uint32 stepRemaining; + StreamString pausedAtGam; + StreamString stepThreadFilter; // empty = all threads; non-empty = only this OS thread name BasicTCPSocket tcpServer; BasicUDPSocket udpSocket; diff --git a/Test/UnitTests/UnitTests.cpp b/Test/UnitTests/UnitTests.cpp index 280d4ec..63559e4 100644 --- a/Test/UnitTests/UnitTests.cpp +++ b/Test/UnitTests/UnitTests.cpp @@ -63,14 +63,61 @@ public: Vec sizes; FastPollingMutexSem mutex; DebugSignalInfo* ptrs[1] = { service.signals[0] }; - service.RegisterBroker(ptrs, 1, NULL_PTR(MemoryMapBroker*), &active, &indices, &sizes, &mutex); + volatile bool anyBreak = false; + Vec breakIdx; + service.RegisterBroker(ptrs, 1, NULL_PTR(MemoryMapBroker*), &active, &indices, &sizes, &mutex, &anyBreak, &breakIdx); service.UpdateBrokersActiveStatus(); assert(active == true); assert(indices.Size() == 1); assert(indices[0] == 0); - + // Helper Process - DebugBrokerHelper::Process(&service, ptrs, indices, sizes, mutex); + DebugBrokerHelper::Process(&service, ptrs, indices, sizes, mutex, &anyBreak, &breakIdx); + + // 4. Step / per-thread filter + // STEP with no thread filter — all threads can consume + service.Step(2u, NULL_PTR(const char8 *)); + assert(!service.IsPaused()); + assert(service.stepRemaining == 2u); + service.ConsumeStepIfNeeded("GAM1", "ThreadA"); + assert(service.stepRemaining == 1u); + assert(!service.IsPaused()); + service.ConsumeStepIfNeeded("GAM1", "ThreadB"); + assert(service.stepRemaining == 0u); + assert(service.IsPaused()); + assert(service.pausedAtGam == "GAM1"); + + // STEP with a thread filter — only matching thread consumes + service.Step(2u, "ThreadA"); + assert(!service.IsPaused()); + assert(service.stepThreadFilter == "ThreadA"); + // ThreadB should be ignored + service.ConsumeStepIfNeeded("GAMB", "ThreadB"); + assert(service.stepRemaining == 2u); // unchanged + assert(!service.IsPaused()); + // ThreadA consumes both credits + service.ConsumeStepIfNeeded("GAMA1", "ThreadA"); + assert(service.stepRemaining == 1u); + assert(!service.IsPaused()); + service.ConsumeStepIfNeeded("GAMA2", "ThreadA"); + assert(service.stepRemaining == 0u); + assert(service.IsPaused()); + assert(service.pausedAtGam == "GAMA2"); + + // STEP with unknown thread (NULL threadName arg to ConsumeStep) is also filtered out + service.Step(1u, "ThreadA"); + service.ConsumeStepIfNeeded("X", NULL_PTR(const char8 *)); + assert(service.stepRemaining == 1u); // not consumed + service.SetPaused(false); + service.Step(0u, NULL_PTR(const char8 *)); + + // 5. VALUE command (NULL client — smoke test, no crash) + service.HandleCommand("VALUE X.Y.Z", NULL_PTR(BasicTCPSocket*)); + service.HandleCommand("VALUE NoSuchSignal", NULL_PTR(BasicTCPSocket*)); + service.HandleCommand("STEP 1 ThreadA", NULL_PTR(BasicTCPSocket*)); + assert(service.stepThreadFilter == "ThreadA"); + service.HandleCommand("STEP 3", NULL_PTR(BasicTCPSocket*)); + assert(service.stepThreadFilter.Size() == 0u); } }; @@ -86,9 +133,9 @@ void timeout_handler(int sig) { int main() { signal(SIGALRM, timeout_handler); alarm(10); - printf("--- MARTe2 Debug Suite COVERAGE V29 ---\n"); + printf("--- MARTe2 Debug Suite COVERAGE V30 ---\n"); MARTe::TestTcpLogger(); MARTe::DebugServiceTest::TestAll(); - printf("\nCOVERAGE V29 PASSED!\n"); + printf("\nCOVERAGE V30 PASSED!\n"); return 0; } diff --git a/Tools/gui_client/src/main.rs b/Tools/gui_client/src/main.rs index b586028..9a44c15 100644 --- a/Tools/gui_client/src/main.rs +++ b/Tools/gui_client/src/main.rs @@ -153,6 +153,9 @@ struct PlotInstance { plot_type: PlotType, signals: Vec, auto_bounds: bool, + max_points: usize, + follow: bool, + reset_view: bool, } #[derive(Clone, PartialEq)] @@ -198,6 +201,8 @@ enum InternalEvent { RecordingError(String, String), // SignalName, ErrorMessage TelemMatched(u32), ServiceConfig { udp_port: String, log_port: String }, + StepStatus { paused: bool, paused_at_gam: String, step_remaining: u32, step_thread: String }, + SignalValue { path: String, value_text: String, found: bool }, } // --- App State --- @@ -212,6 +217,12 @@ struct MonitorDialog { period_ms: String, } +struct BreakDialog { + signal_path: String, + op: String, // ">", "<", "==", ">=", "<=", "!=" + threshold: String, +} + struct MessageDialog { destination: String, function: String, @@ -219,6 +230,15 @@ struct MessageDialog { expect_reply: bool, } +struct InfoDialog { + path: String, + is_signal: bool, + config_text: String, + is_loading: bool, + value_text: Option, + value_loading: bool, +} + struct LogFilters { show_debug: bool, show_info: bool, @@ -253,6 +273,12 @@ struct MarteDebugApp { traced_signals: Arc>>, plots: Vec, forced_signals: HashMap, + break_conditions: HashMap, // signal -> (op, threshold) + break_dialog: Option, + step_status: Option<(bool, String, u32)>, // (paused, paused_at_gam, step_remaining) + last_step_poll: std::time::Instant, + step_thread: String, + info_dialog: Option, logs: VecDeque, log_filters: LogFilters, show_left_panel: bool, @@ -329,8 +355,17 @@ impl MarteDebugApp { plot_type: PlotType::Normal, signals: Vec::new(), auto_bounds: true, + max_points: 5000, + follow: true, + reset_view: false, }], forced_signals: HashMap::new(), + break_conditions: HashMap::new(), + break_dialog: None, + step_status: None, + last_step_poll: std::time::Instant::now(), + step_thread: String::new(), + info_dialog: None, logs: VecDeque::with_capacity(2000), log_filters: LogFilters { show_debug: true, @@ -471,6 +506,22 @@ impl MarteDebugApp { objects } + fn get_threads(&self) -> Vec { + let mut threads = Vec::new(); + if let Some(tree) = &self.app_tree { + fn collect(item: &TreeItem, out: &mut Vec) { + if item.class == "RealTimeThread" { + out.push(item.name.clone()); + } + if let Some(children) = &item.children { + for child in children { collect(child, out); } + } + } + collect(tree, &mut threads); + } + threads + } + fn render_tree(&mut self, ui: &mut egui::Ui, item: &TreeItem, path: String) { let current_path = if path.is_empty() { if item.name == "Root" { @@ -502,6 +553,14 @@ impl MarteDebugApp { { self.selected_node = current_path.clone(); let _ = self.tx_cmd.send(format!("INFO {}", current_path)); + self.info_dialog = Some(InfoDialog { + path: current_path.clone(), + is_signal: false, + config_text: String::new(), + is_loading: true, + value_text: None, + value_loading: false, + }); } } }); @@ -511,16 +570,47 @@ impl MarteDebugApp { }); } else { ui.horizontal(|ui| { - if ui - .selectable_label( - self.selected_node == current_path, - format!("{} [{}]", label, item.class), - ) - .clicked() - { + let resp = ui.selectable_label( + self.selected_node == current_path, + format!("{} [{}]", label, item.class), + ); + if resp.clicked() { self.selected_node = current_path.clone(); let _ = self.tx_cmd.send(format!("INFO {}", current_path)); } + if resp.double_clicked() { + self.selected_node = current_path.clone(); + let _ = self.tx_cmd.send(format!("INFO {}", current_path)); + let is_sig = item.class.contains("Signal"); + let mut value_loading = false; + if is_sig { + let _ = self.tx_cmd.send(format!("VALUE {}", current_path)); + value_loading = true; + } + self.info_dialog = Some(InfoDialog { + path: current_path.clone(), + is_signal: is_sig, + config_text: String::new(), + is_loading: true, + value_text: None, + value_loading, + }); + } + if !resp.double_clicked() && !resp.clicked() { + // keep the existing Info button for non-signal leaf nodes + if !item.class.contains("Signal") && ui.selectable_label(self.selected_node == current_path, "ℹ Info").clicked() { + self.selected_node = current_path.clone(); + let _ = self.tx_cmd.send(format!("INFO {}", current_path)); + self.info_dialog = Some(InfoDialog { + path: current_path.clone(), + is_signal: false, + config_text: String::new(), + is_loading: true, + value_text: None, + value_loading: false, + }); + } + } if item.class.contains("Signal") { let elements = item.elements.unwrap_or(1); if elements > 1 { @@ -572,6 +662,21 @@ impl MarteDebugApp { value: "".to_string(), }); } + if traceable { + let has_break = self.break_conditions.contains_key(¤t_path); + let btn_text = if has_break { "🔴 Break*" } else { "🔴 Break" }; + if ui.button(btn_text).clicked() { + let (op, thr) = self.break_conditions + .get(¤t_path) + .cloned() + .unwrap_or_else(|| (">".to_string(), 0.0)); + self.break_dialog = Some(BreakDialog { + signal_path: current_path.clone(), + op, + threshold: thr.to_string(), + }); + } + } } } }); @@ -740,6 +845,49 @@ fn tcp_command_worker( let _ = tx_events_inner .send(InternalEvent::ConfigResponse(text.to_string())); json_acc.clear(); + } else if trimmed.contains("OK STEP_STATUS") { + in_json = false; + let json_clean = json_acc.split("OK STEP_STATUS").next().unwrap_or("").trim(); + let paused = json_clean.contains("\"Paused\": true"); + let gam = json_clean.split("\"PausedAtGam\": \"") + .nth(1) + .and_then(|s| s.split('"').next()) + .unwrap_or("") + .to_string(); + let remaining = json_clean.split("\"StepRemaining\": ") + .nth(1) + .and_then(|s| s.split(',').next()) + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + let step_thread = json_clean.split("\"StepThread\": \"") + .nth(1) + .and_then(|s| s.split('"').next()) + .unwrap_or("") + .to_string(); + let _ = tx_events_inner.send(InternalEvent::StepStatus { + paused, + paused_at_gam: gam, + step_remaining: remaining, + step_thread, + }); + json_acc.clear(); + } else if trimmed.contains("OK VALUE") { + in_json = false; + let json_clean = json_acc.split("OK VALUE").next().unwrap_or("").trim(); + let found = !json_clean.contains("\"Error\""); + let path = json_clean.split("\"Name\": \"") + .nth(1) + .and_then(|s| s.split('"').next()) + .unwrap_or("") + .to_string(); + // Value is a quoted string: "Value": "..." + let value_text = json_clean.split("\"Value\": \"") + .nth(1) + .and_then(|s| s.split('"').next()) + .unwrap_or("") + .to_string(); + let _ = tx_events_inner.send(InternalEvent::SignalValue { path, value_text, found }); + json_acc.clear(); } } else { if trimmed.starts_with("OK SERVICE_INFO") { @@ -1158,7 +1306,18 @@ impl eframe::App for MarteDebugApp { self.app_tree = Some(tree); } InternalEvent::NodeInfo(info) => { - self.node_info = info; + self.node_info = info.clone(); + if let Some(dialog) = &mut self.info_dialog { + // Try to pretty-print as MARTe2 config if it's a JSON object + let display = if info.trim_start().starts_with('{') { + let converted = convert_config_json(&info); + if converted.is_empty() { info } else { converted } + } else { + info + }; + dialog.config_text = display; + dialog.is_loading = false; + } } InternalEvent::TraceRequested(name, is_monitored) => { let mut data_map = self.traced_signals.lock().unwrap(); @@ -1255,6 +1414,20 @@ impl eframe::App for MarteDebugApp { self.app_config_text = convert_config_json(&text); } InternalEvent::TelemMatched(_) => {} + InternalEvent::StepStatus { paused, paused_at_gam, step_remaining, step_thread: _ } => { + self.step_status = Some((paused, paused_at_gam, step_remaining)); + if paused { + self.is_breaking = true; + } + } + InternalEvent::SignalValue { path, value_text, found } => { + if let Some(dialog) = &mut self.info_dialog { + if dialog.path == path { + dialog.value_loading = false; + dialog.value_text = if found { Some(value_text) } else { None }; + } + } + } InternalEvent::RecordPathChosen(name, path) => { let mut data_map = self.traced_signals.lock().unwrap(); if let Some(entry) = data_map.get_mut(&name) { @@ -1277,6 +1450,24 @@ impl eframe::App for MarteDebugApp { } } + // Poll STEP_STATUS: fast while debugging, slow background poll to catch conditional breaks + if self.connected { + let elapsed = self.last_step_poll.elapsed(); + let interval = if self.is_breaking { + std::time::Duration::from_millis(500) + } else { + std::time::Duration::from_secs(2) + }; + if elapsed >= interval { + let _ = self.tx_cmd.send("STEP_STATUS".to_string()); + self.last_step_poll = std::time::Instant::now(); + } + if self.is_breaking { + ctx.request_repaint_after(std::time::Duration::from_millis(500)); + } + } + + if self.scope.enabled { self.apply_trigger_logic(); } @@ -1318,6 +1509,59 @@ impl eframe::App for MarteDebugApp { } } + if self.break_dialog.is_some() { + let mut dialog = self.break_dialog.take().unwrap(); + let mut close = false; + let ops = [">", "<", "==", ">=", "<=", "!="]; + egui::Window::new("Set Conditional Break").show(ctx, |ui| { + ui.label(egui::RichText::new(&dialog.signal_path).monospace().strong()); + ui.separator(); + ui.horizontal(|ui| { + ui.label("Condition:"); + egui::ComboBox::from_id_salt("break_op_combo") + .selected_text(&dialog.op) + .width(60.0) + .show_ui(ui, |ui| { + for op in &ops { + ui.selectable_value(&mut dialog.op, op.to_string(), *op); + } + }); + ui.text_edit_singleline(&mut dialog.threshold); + }); + ui.label(egui::RichText::new(format!( + "Pause RT when: signal {} {}", + dialog.op, dialog.threshold + )).small().color(egui::Color32::GRAY)); + ui.separator(); + ui.horizontal(|ui| { + if ui.button("✔ Apply").clicked() { + if let Ok(thr) = dialog.threshold.parse::() { + let _ = self.tx_cmd.send(format!( + "BREAK {} {} {}", + dialog.signal_path, dialog.op, thr + )); + self.break_conditions.insert( + dialog.signal_path.clone(), + (dialog.op.clone(), thr), + ); + } + close = true; + } + if ui.button("🗑 Clear Break").clicked() { + let _ = self.tx_cmd.send(format!("BREAK {} OFF", dialog.signal_path)); + self.break_conditions.remove(&dialog.signal_path); + close = true; + } + if ui.button("Cancel").clicked() { + close = true; + } + }); + }); + if !close { + self.break_dialog = Some(dialog); + } + } + if let Some(dialog) = &mut self.monitoring_dialog { let mut close = false; egui::Window::new("Monitor Signal").show(ctx, |ui| { @@ -1485,6 +1729,78 @@ impl eframe::App for MarteDebugApp { } } + if self.info_dialog.is_some() { + let mut close = false; + let mut send_value_cmd: Option = None; + { + let dialog = self.info_dialog.as_mut().unwrap(); + let max_h = ctx.available_rect().height() * 0.75; + let title = format!("ℹ {}", dialog.path); + egui::Window::new(title) + .resizable(true) + .default_width(480.0) + .max_height(max_h) + .show(ctx, |ui| { + // Always-visible close button at top-right + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("✖ Close").clicked() { + close = true; + } + }); + }); + ui.separator(); + if dialog.is_signal { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Value:").color(egui::Color32::GRAY)); + if dialog.value_loading { + ui.spinner(); + } else { + match &dialog.value_text { + Some(v) => { + ui.label(egui::RichText::new(v.as_str()) + .monospace().strong() + .color(egui::Color32::from_rgb(100, 220, 255))); + } + None => { + ui.label(egui::RichText::new("—").color(egui::Color32::GRAY)); + } + } + } + if ui.small_button("🔄").on_hover_text("Refresh value").clicked() { + send_value_cmd = Some(dialog.path.clone()); + dialog.value_loading = true; + } + }); + ui.separator(); + } + if dialog.is_loading { + ui.spinner(); + ui.label(egui::RichText::new("Loading config…").italics().color(egui::Color32::GRAY)); + } else if dialog.config_text.is_empty() { + ui.label(egui::RichText::new("No config available").italics().color(egui::Color32::GRAY)); + } else { + egui::ScrollArea::both() + .auto_shrink([true, true]) + .max_height(max_h - 120.0) + .show(ui, |ui| { + ui.add( + egui::Label::new( + egui::RichText::new(&dialog.config_text).monospace().small() + ).selectable(true), + ); + }); + } + }); + } + if let Some(path) = send_value_cmd { + let _ = self.tx_cmd.send(format!("VALUE {}", path)); + } + if close { + self.info_dialog = None; + } + } + egui::TopBottomPanel::top("top").show(ctx, |ui| { ui.horizontal(|ui| { ui.toggle_value(&mut self.show_left_panel, "🗂 Tree"); @@ -1498,6 +1814,9 @@ impl eframe::App for MarteDebugApp { plot_type: PlotType::Normal, signals: Vec::new(), auto_bounds: true, + max_points: 5000, + follow: true, + reset_view: false, }); } ui.separator(); @@ -1516,6 +1835,10 @@ impl eframe::App for MarteDebugApp { } else { "RESUME".to_string() }); + if !self.is_breaking { + self.step_status = None; + } + self.last_step_poll = std::time::Instant::now() - std::time::Duration::from_secs(1); } ui.separator(); ui.checkbox(&mut self.scope.enabled, "🔭 Scope"); @@ -1682,82 +2005,155 @@ impl eframe::App for MarteDebugApp { .resizable(true) .width_range(250.0..=400.0) .show(ctx, |ui| { + // Debug Controls pane — shown when app is paused / breaking + if self.is_breaking { + let paused = self.step_status.as_ref().map(|(p, _, _)| *p).unwrap_or(false); + let gam = self.step_status.as_ref().map(|(_, g, _)| g.clone()).unwrap_or_default(); + let remaining = self.step_status.as_ref().map(|(_, _, r)| *r).unwrap_or(0); + let threads = self.get_threads(); + + ui.group(|ui| { + ui.label(egui::RichText::new("⏸ Debug Controls").strong().color(egui::Color32::YELLOW)); + ui.separator(); + if paused { + if gam.is_empty() { + ui.label(egui::RichText::new("Paused (waiting for break)").color(egui::Color32::from_rgb(255, 200, 100))); + } else { + ui.label(egui::RichText::new("Paused at GAM:").color(egui::Color32::GRAY).small()); + ui.label(egui::RichText::new(&gam).monospace().strong().color(egui::Color32::from_rgb(255, 180, 80))); + } + } else { + ui.label(egui::RichText::new(format!("Running ({} steps left)", remaining)).color(egui::Color32::from_rgb(100, 220, 100))); + } + if !threads.is_empty() { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Thread:").color(egui::Color32::GRAY).small()); + let sel = if self.step_thread.is_empty() { "(all)" } else { &self.step_thread }; + egui::ComboBox::from_id_salt("step_thread_combo") + .selected_text(sel) + .width(140.0) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.step_thread, String::new(), "(all)"); + for t in &threads { + ui.selectable_value(&mut self.step_thread, t.clone(), t.as_str()); + } + }); + }); + } + ui.add_space(4.0); + ui.horizontal(|ui| { + let thread_arg = if self.step_thread.is_empty() { + String::new() + } else { + format!(" {}", self.step_thread) + }; + if ui.button("Step 1").on_hover_text("Run one output broker cycle, then pause").clicked() { + let _ = self.tx_cmd.send(format!("STEP 1{}", thread_arg)); + self.step_status = self.step_status.as_ref().map(|(_, g, _)| (false, g.clone(), 1)); + } + if ui.button("Step 5").on_hover_text("Run 5 output broker cycles, then pause").clicked() { + let _ = self.tx_cmd.send(format!("STEP 5{}", thread_arg)); + self.step_status = self.step_status.as_ref().map(|(_, g, _)| (false, g.clone(), 5)); + } + if ui.button(egui::RichText::new("▶ Resume").color(egui::Color32::GREEN)).clicked() { + let _ = self.tx_cmd.send("RESUME".to_string()); + self.is_breaking = false; + self.step_status = None; + } + }); + }); + ui.separator(); + } + ui.heading("Traced Signals"); let mut names: Vec<_> = { let data_map = self.traced_signals.lock().unwrap(); data_map.keys().cloned().collect() }; names.sort(); + let mut open_info_signal: Option<(String, f64)> = None; egui::ScrollArea::vertical() .id_salt("traced_scroll") .show(ui, |ui| { for key in names { - let mut data_map = self.traced_signals.lock().unwrap(); - if let Some(entry) = data_map.get_mut(&key) { - let last_val = entry.last_value; - let is_recording = entry.recording_tx.is_some(); - ui.horizontal(|ui| { - if is_recording { - ui.label( - egui::RichText::new("●").color(egui::Color32::RED), - ); - } - let response = ui.add( - egui::Label::new(format!("{}: {:.2}", key, last_val)) - .sense( - egui::Sense::drag().union(egui::Sense::click()), - ), - ); - if response.drag_started() { - ctx.data_mut(|d| { - d.insert_temp( - egui::Id::new("drag_signal"), - key.clone(), - ) - }); - } - response.context_menu(|ui| { - if !is_recording { - if ui.button("⏺ Record to Parquet").clicked() { - let tx = self.internal_tx.clone(); - let name_clone = key.clone(); - thread::spawn(move || { - if let Some(path) = FileDialog::new() - .add_filter("Parquet", &["parquet"]) - .save_file() - { - let _ = tx.send( - InternalEvent::RecordPathChosen( - name_clone, - path.to_string_lossy() - .to_string(), - ), - ); - } - }); - ui.close_menu(); - } - } else { - if ui.button("⏹ Stop").clicked() { - entry.recording_tx = None; - ui.close_menu(); - } - } + let (last_val, is_recording, is_monitored) = { + let dm = self.traced_signals.lock().unwrap(); + if let Some(e) = dm.get(&key) { + (e.last_value, e.recording_tx.is_some(), e.is_monitored) + } else { + continue; + } + }; + ui.horizontal(|ui| { + if is_recording { + ui.label(egui::RichText::new("●").color(egui::Color32::RED)); + } + let response = ui.add( + egui::Label::new(format!("{}: {:.2}", key, last_val)) + .sense(egui::Sense::drag().union(egui::Sense::click())), + ); + if response.drag_started() { + ctx.data_mut(|d| { + d.insert_temp(egui::Id::new("drag_signal"), key.clone()) }); - if ui.button("❌").clicked() { - if entry.is_monitored { - let _ = self.tx_cmd.send(format!("UNMONITOR SIGNAL {}", key)); - } else { - let _ = self.tx_cmd.send(format!("TRACE {} 0", key)); + } + if response.double_clicked() { + open_info_signal = Some((key.clone(), last_val)); + } + if ui.small_button("ℹ").on_hover_text("Show info / last value").clicked() { + open_info_signal = Some((key.clone(), last_val)); + } + response.context_menu(|ui| { + if !is_recording { + if ui.button("⏺ Record to Parquet").clicked() { + let tx = self.internal_tx.clone(); + let name_clone = key.clone(); + thread::spawn(move || { + if let Some(path) = FileDialog::new() + .add_filter("Parquet", &["parquet"]) + .save_file() + { + let _ = tx.send(InternalEvent::RecordPathChosen( + name_clone, + path.to_string_lossy().to_string(), + )); + } + }); + ui.close_menu(); + } + } else { + if ui.button("⏹ Stop").clicked() { + let mut dm = self.traced_signals.lock().unwrap(); + if let Some(e) = dm.get_mut(&key) { + e.recording_tx = None; + } + ui.close_menu(); } - let _ = self - .internal_tx - .send(InternalEvent::ClearTrace(key.clone())); } }); - } + if ui.button("❌").clicked() { + if is_monitored { + let _ = self.tx_cmd.send(format!("UNMONITOR SIGNAL {}", key)); + } else { + let _ = self.tx_cmd.send(format!("TRACE {} 0", key)); + } + let _ = self.internal_tx.send(InternalEvent::ClearTrace(key.clone())); + } + }); } }); + if let Some((sig_path, last_val)) = open_info_signal { + let _ = self.tx_cmd.send(format!("INFO {}", sig_path)); + let _ = self.tx_cmd.send(format!("VALUE {}", sig_path)); + self.info_dialog = Some(InfoDialog { + path: sig_path, + is_signal: true, + config_text: String::new(), + is_loading: true, + value_text: None, + value_loading: true, + }); + } ui.separator(); ui.heading("Forced Signals"); let mut to_delete = Vec::new(); @@ -1774,6 +2170,45 @@ impl eframe::App for MarteDebugApp { self.forced_signals.remove(key); } + ui.separator(); + ui.heading("Breakpoints"); + if self.break_conditions.is_empty() { + ui.label(egui::RichText::new("No active breakpoints").italics().color(egui::Color32::GRAY)); + } else { + let mut to_clear: Vec = Vec::new(); + let mut to_edit: Option = None; + let mut sorted_breaks: Vec<_> = self.break_conditions.iter().collect(); + sorted_breaks.sort_by_key(|(k, _)| k.as_str()); + for (path, (op, thr)) in &sorted_breaks { + ui.horizontal(|ui| { + ui.label( + egui::RichText::new(format!("🔴 {} {} {}", path, op, thr)) + .color(egui::Color32::from_rgb(255, 120, 120)) + .monospace() + .small(), + ); + if ui.small_button("✏").on_hover_text("Edit").clicked() { + to_edit = Some(path.to_string()); + } + if ui.small_button("❌").on_hover_text("Clear break").clicked() { + to_clear.push(path.to_string()); + } + }); + } + for path in to_clear { + let _ = self.tx_cmd.send(format!("BREAK {} OFF", path)); + self.break_conditions.remove(&path); + } + if let Some(path) = to_edit { + let (op, thr) = self.break_conditions[&path].clone(); + self.break_dialog = Some(BreakDialog { + signal_path: path, + op, + threshold: thr.to_string(), + }); + } + } + if self.show_message_history { ui.separator(); ui.horizontal(|ui| { @@ -1973,16 +2408,34 @@ impl eframe::App for MarteDebugApp { ui.group(|ui| { ui.horizontal(|ui| { ui.label(egui::RichText::new(&plot_inst.id).strong()); - ui.selectable_value( - &mut plot_inst.plot_type, - PlotType::Normal, - "Series", - ); - ui.selectable_value( - &mut plot_inst.plot_type, - PlotType::LogicAnalyzer, - "Logic", - ); + ui.selectable_value(&mut plot_inst.plot_type, PlotType::Normal, "Series"); + ui.selectable_value(&mut plot_inst.plot_type, PlotType::LogicAnalyzer, "Logic"); + ui.separator(); + let follow_text = if plot_inst.follow { + egui::RichText::new("▶ Follow").color(egui::Color32::from_rgb(100, 220, 100)) + } else { + egui::RichText::new("▶ Follow").color(egui::Color32::GRAY) + }; + if ui.toggle_value(&mut plot_inst.follow, follow_text) + .on_hover_text("Follow latest data (auto-scroll X axis)") + .changed() + { + if plot_inst.follow { + plot_inst.auto_bounds = true; + self.shared_x_range = None; + } + } + if ui.button("↺ Reset").on_hover_text("Reset view to fit all data").clicked() { + plot_inst.auto_bounds = true; + plot_inst.follow = true; + plot_inst.reset_view = true; + self.shared_x_range = None; + } + ui.separator(); + ui.label(egui::RichText::new("Pts:").color(egui::Color32::GRAY).small()); + ui.add(egui::DragValue::new(&mut plot_inst.max_points).range(100..=100000).speed(100.0)) + .on_hover_text("Max points per line (lower = faster rendering)"); + ui.separator(); if ui.button("🗑").clicked() { to_remove = Some(p_idx); } @@ -2039,7 +2492,47 @@ impl eframe::App for MarteDebugApp { } } + // Pre-compute fit bounds if a reset was requested (reset_view flag). + // Must be done before plot.show() so we can call set_plot_bounds inside. + let fit_bounds: Option = if plot_inst.reset_view { + let mut min_t = f64::INFINITY; + let mut max_t = f64::NEG_INFINITY; + let mut min_v = f64::INFINITY; + let mut max_v = f64::NEG_INFINITY; + let max_pts = plot_inst.max_points; + for (s_idx, sig_cfg) in plot_inst.signals.iter().enumerate() { + if let Some(data) = data_map.get(&sig_cfg.source_name) { + for [t, v] in data.values.iter().rev().take(max_pts) { + let mut fv = *v * sig_cfg.gain + sig_cfg.offset; + if plot_inst.plot_type == PlotType::LogicAnalyzer { + fv = (s_idx as f64 * 1.5) + (if fv > 0.5 { 1.0 } else { 0.0 }); + } + if *t < min_t { min_t = *t; } + if *t > max_t { max_t = *t; } + if fv < min_v { min_v = fv; } + if fv > max_v { max_v = fv; } + } + } + } + if min_t.is_finite() && max_t.is_finite() && min_v.is_finite() && max_v.is_finite() { + let pad_t = (max_t - min_t).abs() * 0.02 + 0.01; + let pad_v = (max_v - min_v).abs() * 0.05 + 0.01; + Some(PlotBounds::from_min_max( + [min_t - pad_t, min_v - pad_v], + [max_t + pad_t, max_v + pad_v], + )) + } else { + None + } + } else { + None + }; + let plot_resp = plot.show(ui, |plot_ui| { + if let Some(bounds) = fit_bounds { + plot_ui.set_plot_bounds(bounds); + plot_inst.reset_view = false; + } if !self.scope.enabled && !plot_inst.auto_bounds { if let Some(range) = self.shared_x_range { let bounds = plot_ui.plot_bounds(); @@ -2060,10 +2553,11 @@ impl eframe::App for MarteDebugApp { ); } + let max_pts = plot_inst.max_points; for (s_idx, sig_cfg) in plot_inst.signals.iter().enumerate() { if let Some(data) = data_map.get(&sig_cfg.source_name) { let points_iter = - data.values.iter().rev().take(5000).rev().map(|[t, v]| { + data.values.iter().rev().take(max_pts).rev().map(|[t, v]| { let mut final_v = *v * sig_cfg.gain + sig_cfg.offset; if plot_inst.plot_type == PlotType::LogicAnalyzer { final_v = (s_idx as f64 * 1.5) @@ -2110,13 +2604,20 @@ impl eframe::App for MarteDebugApp { { if plot_resp.response.hovered() { plot_inst.auto_bounds = false; + plot_inst.follow = false; let b = plot_resp.transform.bounds(); self.shared_x_range = Some([b.min()[0], b.max()[0]]); } } + // Re-apply follow on each frame when follow is active + if plot_inst.follow && !self.scope.enabled { + plot_inst.auto_bounds = true; + } plot_resp.response.context_menu(|ui| { if ui.button("🔍 Fit View").clicked() { plot_inst.auto_bounds = true; + plot_inst.follow = true; + plot_inst.reset_view = true; self.shared_x_range = None; ui.close_menu(); } @@ -2394,6 +2895,107 @@ mod tests { // ExportData() does not re-emit them after ConfigureApplication(). assert_classes_match(&expected_tree, &actual_tree, ""); } + + // ----- Unit tests for STEP_STATUS / VALUE JSON parsing ----- + // These mirror the parsing logic inside tcp_command_worker's reader thread. + + fn parse_step_status(json: &str) -> (bool, String, u32, String) { + let paused = json.contains("\"Paused\": true"); + let gam = json.split("\"PausedAtGam\": \"") + .nth(1) + .and_then(|s| s.split('"').next()) + .unwrap_or("") + .to_string(); + let remaining = json.split("\"StepRemaining\": ") + .nth(1) + .and_then(|s| s.split(',').next()) + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + let thread = json.split("\"StepThread\": \"") + .nth(1) + .and_then(|s| s.split('"').next()) + .unwrap_or("") + .to_string(); + (paused, gam, remaining, thread) + } + + fn parse_value_response(json: &str) -> (bool, String, String) { + let found = !json.contains("\"Error\""); + let path = json.split("\"Name\": \"") + .nth(1) + .and_then(|s| s.split('"').next()) + .unwrap_or("") + .to_string(); + // Value is a quoted string: "Value": "..." + let value_text = json.split("\"Value\": \"") + .nth(1) + .and_then(|s| s.split('"').next()) + .unwrap_or("") + .to_string(); + (found, path, value_text) + } + + #[test] + fn test_step_status_paused_with_thread() { + let json = r#"{"Paused": true, "PausedAtGam": "App.Functions.GAM2", "StepRemaining": 0, "StepThread": "Thread1"}"#; + let (paused, gam, remaining, thread) = parse_step_status(json); + assert!(paused); + assert_eq!(gam, "App.Functions.GAM2"); + assert_eq!(remaining, 0); + assert_eq!(thread, "Thread1"); + } + + #[test] + fn test_step_status_running_no_thread() { + let json = r#"{"Paused": false, "PausedAtGam": "", "StepRemaining": 3, "StepThread": ""}"#; + let (paused, gam, remaining, thread) = parse_step_status(json); + assert!(!paused); + assert_eq!(gam, ""); + assert_eq!(remaining, 3); + assert_eq!(thread, ""); + } + + #[test] + fn test_step_status_step_remaining_not_confused_with_thread_port() { + // "StepRemaining" must not accidentally parse digits from "StepThread" + let json = r#"{"Paused": false, "PausedAtGam": "", "StepRemaining": 7, "StepThread": "RT1"}"#; + let (_, _, remaining, thread) = parse_step_status(json); + assert_eq!(remaining, 7); + assert_eq!(thread, "RT1"); + } + + #[test] + fn test_value_response_found() { + let json = r#"{"Name": "App.Data.DDB.Counter", "Value": "42.5", "Elements": 1}"#; + let (found, path, value_text) = parse_value_response(json); + assert!(found); + assert_eq!(path, "App.Data.DDB.Counter"); + assert_eq!(value_text, "42.5"); + } + + #[test] + fn test_value_response_not_found() { + let json = r#"{"Error": "Signal not found: BadSignal"}"#; + let (found, _path, _value_text) = parse_value_response(json); + assert!(!found); + } + + #[test] + fn test_value_response_array() { + let json = r#"{"Name": "App.Data.DDB.Vec", "Value": "1, 2, 3", "Elements": 3}"#; + let (found, path, value_text) = parse_value_response(json); + assert!(found); + assert_eq!(path, "App.Data.DDB.Vec"); + assert_eq!(value_text, "1, 2, 3"); + } + + #[test] + fn test_value_response_negative() { + let json = r#"{"Name": "App.Data.DDB.Err", "Value": "-3.14159", "Elements": 1}"#; + let (found, _, value_text) = parse_value_response(json); + assert!(found); + assert_eq!(value_text, "-3.14159"); + } } fn main() -> Result<(), eframe::Error> {