Major functionality implemented (missing reconfig).
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
#include "MemoryMapBroker.h"
|
#include "MemoryMapBroker.h"
|
||||||
#include "ObjectBuilder.h"
|
#include "ObjectBuilder.h"
|
||||||
#include "ObjectRegistryDatabase.h"
|
#include "ObjectRegistryDatabase.h"
|
||||||
|
#include "Threads.h"
|
||||||
#include "Vec.h"
|
#include "Vec.h"
|
||||||
|
|
||||||
// Original broker headers
|
// Original broker headers
|
||||||
@@ -55,17 +56,62 @@ static Reference FindByNameRecursive(ReferenceContainer *container,
|
|||||||
*/
|
*/
|
||||||
class DebugBrokerHelper {
|
class DebugBrokerHelper {
|
||||||
public:
|
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<const float64 *>(s->memoryAddress);
|
||||||
|
else if (t == Float32Bit) val = *static_cast<const float32 *>(s->memoryAddress);
|
||||||
|
else if (t == UnsignedInteger32Bit) val = *static_cast<const uint32 *>(s->memoryAddress);
|
||||||
|
else if (t == SignedInteger32Bit) val = *static_cast<const int32 *>(s->memoryAddress);
|
||||||
|
else if (t == UnsignedInteger64Bit) val = static_cast<float64>(*static_cast<const uint64 *>(s->memoryAddress));
|
||||||
|
else if (t == SignedInteger64Bit) val = static_cast<float64>(*static_cast<const int64 *>(s->memoryAddress));
|
||||||
|
else if (t == UnsignedInteger16Bit) val = *static_cast<const uint16 *>(s->memoryAddress);
|
||||||
|
else if (t == SignedInteger16Bit) val = *static_cast<const int16 *>(s->memoryAddress);
|
||||||
|
else if (t == UnsignedInteger8Bit) val = *static_cast<const uint8 *>(s->memoryAddress);
|
||||||
|
else if (t == SignedInteger8Bit) val = *static_cast<const int8 *>(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,
|
static void Process(DebugService *service,
|
||||||
DebugSignalInfo **signalInfoPointers,
|
DebugSignalInfo **signalInfoPointers,
|
||||||
Vec<uint32> &activeIndices, Vec<uint32> &activeSizes,
|
Vec<uint32> &activeIndices, Vec<uint32> &activeSizes,
|
||||||
FastPollingMutexSem &activeMutex) {
|
FastPollingMutexSem &activeMutex,
|
||||||
|
volatile bool *anyBreakFlag,
|
||||||
|
Vec<uint32> *breakIndices) {
|
||||||
if (service == NULL_PTR(DebugService *))
|
if (service == NULL_PTR(DebugService *))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Re-establish break logic
|
// NOTE: No spin here. Spinning for paused state is handled in Execute() of
|
||||||
while (service->IsPaused()) {
|
// OUTPUT brokers only (see OutputPauseAndStep). Input brokers must not block
|
||||||
Sleep::MSec(10);
|
// because that prevents cross-thread EventSem posts from completing.
|
||||||
}
|
|
||||||
|
|
||||||
activeMutex.FastLock();
|
activeMutex.FastLock();
|
||||||
uint32 n = activeIndices.Size();
|
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> *)) {
|
||||||
|
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();
|
activeMutex.FastUnLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +156,8 @@ public:
|
|||||||
uint32 numCopies, MemoryMapBrokerCopyTableEntry *copyTable,
|
uint32 numCopies, MemoryMapBrokerCopyTableEntry *copyTable,
|
||||||
const char8 *functionName, SignalDirection direction,
|
const char8 *functionName, SignalDirection direction,
|
||||||
volatile bool *anyActiveFlag, Vec<uint32> *activeIndices,
|
volatile bool *anyActiveFlag, Vec<uint32> *activeIndices,
|
||||||
Vec<uint32> *activeSizes, FastPollingMutexSem *activeMutex) {
|
Vec<uint32> *activeSizes, FastPollingMutexSem *activeMutex,
|
||||||
|
volatile bool *anyBreakFlag, Vec<uint32> *breakIndices) {
|
||||||
if (numCopies > 0) {
|
if (numCopies > 0) {
|
||||||
signalInfoPointers = new DebugSignalInfo *[numCopies];
|
signalInfoPointers = new DebugSignalInfo *[numCopies];
|
||||||
for (uint32 i = 0; i < numCopies; i++)
|
for (uint32 i = 0; i < numCopies; i++)
|
||||||
@@ -181,8 +245,12 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register broker in DebugService for optimized control
|
// Register broker in DebugService for optimized control
|
||||||
|
bool isOutputBroker = (direction == OutputSignals);
|
||||||
service->RegisterBroker(signalInfoPointers, numCopies, mmb, anyActiveFlag,
|
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 **);
|
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
|
||||||
numSignals = 0;
|
numSignals = 0;
|
||||||
anyActive = false;
|
anyActive = false;
|
||||||
|
anyBreakActive = false;
|
||||||
|
isOutput = false;
|
||||||
|
gamName[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual ~DebugBrokerWrapper() {
|
virtual ~DebugBrokerWrapper() {
|
||||||
@@ -206,9 +277,15 @@ public:
|
|||||||
|
|
||||||
virtual bool Execute() {
|
virtual bool Execute() {
|
||||||
bool ret = BaseClass::Execute();
|
bool ret = BaseClass::Execute();
|
||||||
if (ret && (anyActive || (service && service->IsPaused()))) {
|
if (ret && (anyActive || anyBreakActive)) {
|
||||||
DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices,
|
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;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -220,10 +297,13 @@ public:
|
|||||||
direction == InputSignals ? "In" : "Out");
|
direction == InputSignals ? "In" : "Out");
|
||||||
if (ret) {
|
if (ret) {
|
||||||
numSignals = this->GetNumberOfCopies();
|
numSignals = this->GetNumberOfCopies();
|
||||||
|
isOutput = (direction == OutputSignals);
|
||||||
|
StringHelper::CopyN(gamName, name, 255u);
|
||||||
DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers,
|
DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers,
|
||||||
numSignals, this->copyTable, name,
|
numSignals, this->copyTable, name,
|
||||||
direction, &anyActive, &activeIndices,
|
direction, &anyActive, &activeIndices,
|
||||||
&activeSizes, &activeMutex);
|
&activeSizes, &activeMutex,
|
||||||
|
&anyBreakActive, &breakIndices);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -235,10 +315,13 @@ public:
|
|||||||
direction == InputSignals ? "In" : "Out");
|
direction == InputSignals ? "In" : "Out");
|
||||||
if (ret) {
|
if (ret) {
|
||||||
numSignals = this->GetNumberOfCopies();
|
numSignals = this->GetNumberOfCopies();
|
||||||
|
isOutput = (direction == OutputSignals);
|
||||||
|
StringHelper::CopyN(gamName, name, 255u);
|
||||||
DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers,
|
DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers,
|
||||||
numSignals, this->copyTable, name,
|
numSignals, this->copyTable, name,
|
||||||
direction, &anyActive, &activeIndices,
|
direction, &anyActive, &activeIndices,
|
||||||
&activeSizes, &activeMutex);
|
&activeSizes, &activeMutex,
|
||||||
|
&anyBreakActive, &breakIndices);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -247,8 +330,12 @@ public:
|
|||||||
DebugSignalInfo **signalInfoPointers;
|
DebugSignalInfo **signalInfoPointers;
|
||||||
uint32 numSignals;
|
uint32 numSignals;
|
||||||
volatile bool anyActive;
|
volatile bool anyActive;
|
||||||
|
volatile bool anyBreakActive;
|
||||||
|
bool isOutput;
|
||||||
|
char8 gamName[256];
|
||||||
Vec<uint32> activeIndices;
|
Vec<uint32> activeIndices;
|
||||||
Vec<uint32> activeSizes;
|
Vec<uint32> activeSizes;
|
||||||
|
Vec<uint32> breakIndices;
|
||||||
FastPollingMutexSem activeMutex;
|
FastPollingMutexSem activeMutex;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -260,6 +347,9 @@ public:
|
|||||||
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
|
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
|
||||||
numSignals = 0;
|
numSignals = 0;
|
||||||
anyActive = false;
|
anyActive = false;
|
||||||
|
anyBreakActive = false;
|
||||||
|
isOutput = false;
|
||||||
|
gamName[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual ~DebugBrokerWrapperNoOptim() {
|
virtual ~DebugBrokerWrapperNoOptim() {
|
||||||
@@ -269,9 +359,13 @@ public:
|
|||||||
|
|
||||||
virtual bool Execute() {
|
virtual bool Execute() {
|
||||||
bool ret = BaseClass::Execute();
|
bool ret = BaseClass::Execute();
|
||||||
if (ret && (anyActive || (service && service->IsPaused()))) {
|
if (ret && (anyActive || anyBreakActive)) {
|
||||||
DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices,
|
DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices,
|
||||||
activeSizes, activeMutex);
|
activeSizes, activeMutex,
|
||||||
|
&anyBreakActive, &breakIndices);
|
||||||
|
}
|
||||||
|
if (ret && isOutput) {
|
||||||
|
DebugBrokerHelper::OutputPauseAndStep(service, gamName);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -281,10 +375,13 @@ public:
|
|||||||
bool ret = BaseClass::Init(direction, ds, name, gamMem);
|
bool ret = BaseClass::Init(direction, ds, name, gamMem);
|
||||||
if (ret) {
|
if (ret) {
|
||||||
numSignals = this->GetNumberOfCopies();
|
numSignals = this->GetNumberOfCopies();
|
||||||
|
isOutput = (direction == OutputSignals);
|
||||||
|
StringHelper::CopyN(gamName, name, 255u);
|
||||||
DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers,
|
DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers,
|
||||||
numSignals, this->copyTable, name,
|
numSignals, this->copyTable, name,
|
||||||
direction, &anyActive, &activeIndices,
|
direction, &anyActive, &activeIndices,
|
||||||
&activeSizes, &activeMutex);
|
&activeSizes, &activeMutex,
|
||||||
|
&anyBreakActive, &breakIndices);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -293,8 +390,12 @@ public:
|
|||||||
DebugSignalInfo **signalInfoPointers;
|
DebugSignalInfo **signalInfoPointers;
|
||||||
uint32 numSignals;
|
uint32 numSignals;
|
||||||
volatile bool anyActive;
|
volatile bool anyActive;
|
||||||
|
volatile bool anyBreakActive;
|
||||||
|
bool isOutput;
|
||||||
|
char8 gamName[256];
|
||||||
Vec<uint32> activeIndices;
|
Vec<uint32> activeIndices;
|
||||||
Vec<uint32> activeSizes;
|
Vec<uint32> activeSizes;
|
||||||
|
Vec<uint32> breakIndices;
|
||||||
FastPollingMutexSem activeMutex;
|
FastPollingMutexSem activeMutex;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -305,6 +406,8 @@ public:
|
|||||||
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
|
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
|
||||||
numSignals = 0;
|
numSignals = 0;
|
||||||
anyActive = false;
|
anyActive = false;
|
||||||
|
anyBreakActive = false;
|
||||||
|
gamName[0] = '\0';
|
||||||
}
|
}
|
||||||
virtual ~DebugMemoryMapAsyncOutputBroker() {
|
virtual ~DebugMemoryMapAsyncOutputBroker() {
|
||||||
if (signalInfoPointers)
|
if (signalInfoPointers)
|
||||||
@@ -312,9 +415,14 @@ public:
|
|||||||
}
|
}
|
||||||
virtual bool Execute() {
|
virtual bool Execute() {
|
||||||
bool ret = MemoryMapAsyncOutputBroker::Execute();
|
bool ret = MemoryMapAsyncOutputBroker::Execute();
|
||||||
if (ret && (anyActive || (service && service->IsPaused()))) {
|
if (ret && (anyActive || anyBreakActive)) {
|
||||||
DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices,
|
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;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -330,10 +438,11 @@ public:
|
|||||||
numberOfBuffersIn, cpuMaskIn, stackSizeIn);
|
numberOfBuffersIn, cpuMaskIn, stackSizeIn);
|
||||||
if (ret) {
|
if (ret) {
|
||||||
numSignals = this->GetNumberOfCopies();
|
numSignals = this->GetNumberOfCopies();
|
||||||
|
StringHelper::CopyN(gamName, (functionName != NULL_PTR(const char8 *)) ? functionName : "", 255u);
|
||||||
DebugBrokerHelper::InitSignals(
|
DebugBrokerHelper::InitSignals(
|
||||||
this, dataSourceIn, service, signalInfoPointers, numSignals,
|
this, dataSourceIn, service, signalInfoPointers, numSignals,
|
||||||
this->copyTable, functionName, direction, &anyActive, &activeIndices,
|
this->copyTable, functionName, direction, &anyActive, &activeIndices,
|
||||||
&activeSizes, &activeMutex);
|
&activeSizes, &activeMutex, &anyBreakActive, &breakIndices);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -341,8 +450,11 @@ public:
|
|||||||
DebugSignalInfo **signalInfoPointers;
|
DebugSignalInfo **signalInfoPointers;
|
||||||
uint32 numSignals;
|
uint32 numSignals;
|
||||||
volatile bool anyActive;
|
volatile bool anyActive;
|
||||||
|
volatile bool anyBreakActive;
|
||||||
|
char8 gamName[256];
|
||||||
Vec<uint32> activeIndices;
|
Vec<uint32> activeIndices;
|
||||||
Vec<uint32> activeSizes;
|
Vec<uint32> activeSizes;
|
||||||
|
Vec<uint32> breakIndices;
|
||||||
FastPollingMutexSem activeMutex;
|
FastPollingMutexSem activeMutex;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -355,6 +467,8 @@ public:
|
|||||||
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
|
signalInfoPointers = NULL_PTR(DebugSignalInfo **);
|
||||||
numSignals = 0;
|
numSignals = 0;
|
||||||
anyActive = false;
|
anyActive = false;
|
||||||
|
anyBreakActive = false;
|
||||||
|
gamName[0] = '\0';
|
||||||
}
|
}
|
||||||
virtual ~DebugMemoryMapAsyncTriggerOutputBroker() {
|
virtual ~DebugMemoryMapAsyncTriggerOutputBroker() {
|
||||||
if (signalInfoPointers)
|
if (signalInfoPointers)
|
||||||
@@ -362,9 +476,13 @@ public:
|
|||||||
}
|
}
|
||||||
virtual bool Execute() {
|
virtual bool Execute() {
|
||||||
bool ret = MemoryMapAsyncTriggerOutputBroker::Execute();
|
bool ret = MemoryMapAsyncTriggerOutputBroker::Execute();
|
||||||
if (ret && (anyActive || (service && service->IsPaused()))) {
|
if (ret && (anyActive || anyBreakActive)) {
|
||||||
DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices,
|
DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices,
|
||||||
activeSizes, activeMutex);
|
activeSizes, activeMutex,
|
||||||
|
&anyBreakActive, &breakIndices);
|
||||||
|
}
|
||||||
|
if (ret) {
|
||||||
|
DebugBrokerHelper::OutputPauseAndStep(service, gamName);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -380,10 +498,11 @@ public:
|
|||||||
stackSizeIn);
|
stackSizeIn);
|
||||||
if (ret) {
|
if (ret) {
|
||||||
numSignals = this->GetNumberOfCopies();
|
numSignals = this->GetNumberOfCopies();
|
||||||
|
StringHelper::CopyN(gamName, (functionName != NULL_PTR(const char8 *)) ? functionName : "", 255u);
|
||||||
DebugBrokerHelper::InitSignals(
|
DebugBrokerHelper::InitSignals(
|
||||||
this, dataSourceIn, service, signalInfoPointers, numSignals,
|
this, dataSourceIn, service, signalInfoPointers, numSignals,
|
||||||
this->copyTable, functionName, direction, &anyActive, &activeIndices,
|
this->copyTable, functionName, direction, &anyActive, &activeIndices,
|
||||||
&activeSizes, &activeMutex);
|
&activeSizes, &activeMutex, &anyBreakActive, &breakIndices);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -391,8 +510,11 @@ public:
|
|||||||
DebugSignalInfo **signalInfoPointers;
|
DebugSignalInfo **signalInfoPointers;
|
||||||
uint32 numSignals;
|
uint32 numSignals;
|
||||||
volatile bool anyActive;
|
volatile bool anyActive;
|
||||||
|
volatile bool anyBreakActive;
|
||||||
|
char8 gamName[256];
|
||||||
Vec<uint32> activeIndices;
|
Vec<uint32> activeIndices;
|
||||||
Vec<uint32> activeSizes;
|
Vec<uint32> activeSizes;
|
||||||
|
Vec<uint32> breakIndices;
|
||||||
FastPollingMutexSem activeMutex;
|
FastPollingMutexSem activeMutex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,17 @@
|
|||||||
|
|
||||||
namespace MARTe {
|
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 {
|
struct DebugSignalInfo {
|
||||||
void* memoryAddress;
|
void* memoryAddress;
|
||||||
TypeDescriptor type;
|
TypeDescriptor type;
|
||||||
@@ -20,6 +31,9 @@ struct DebugSignalInfo {
|
|||||||
uint32 internalID;
|
uint32 internalID;
|
||||||
volatile uint32 decimationFactor;
|
volatile uint32 decimationFactor;
|
||||||
volatile uint32 decimationCounter;
|
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)
|
#pragma pack(push, 1)
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ DebugService::DebugService()
|
|||||||
isServer = false;
|
isServer = false;
|
||||||
suppressTimeoutLogs = true;
|
suppressTimeoutLogs = true;
|
||||||
isPaused = false;
|
isPaused = false;
|
||||||
|
stepRemaining = 0u;
|
||||||
manualConfigSet = false;
|
manualConfigSet = false;
|
||||||
activeClient = NULL_PTR(BasicTCPSocket *);
|
activeClient = NULL_PTR(BasicTCPSocket *);
|
||||||
streamerPacketOffset = 0u;
|
streamerPacketOffset = 0u;
|
||||||
@@ -431,6 +432,8 @@ DebugSignalInfo *DebugService::RegisterSignal(void *memoryAddress,
|
|||||||
res->internalID = sigIdx;
|
res->internalID = sigIdx;
|
||||||
res->decimationFactor = 1;
|
res->decimationFactor = 1;
|
||||||
res->decimationCounter = 0;
|
res->decimationCounter = 0;
|
||||||
|
res->breakOp = BREAK_OFF;
|
||||||
|
res->breakThreshold = 0.0;
|
||||||
signals.Push(res);
|
signals.Push(res);
|
||||||
}
|
}
|
||||||
if (sigIdx != 0xFFFFFFFF) {
|
if (sigIdx != 0xFFFFFFFF) {
|
||||||
@@ -474,7 +477,10 @@ void DebugService::RegisterBroker(DebugSignalInfo **signalPointers,
|
|||||||
volatile bool *anyActiveFlag,
|
volatile bool *anyActiveFlag,
|
||||||
Vec<uint32> *activeIndices,
|
Vec<uint32> *activeIndices,
|
||||||
Vec<uint32> *activeSizes,
|
Vec<uint32> *activeSizes,
|
||||||
FastPollingMutexSem *activeMutex) {
|
FastPollingMutexSem *activeMutex,
|
||||||
|
volatile bool *anyBreakFlag,
|
||||||
|
Vec<uint32> *breakIndices,
|
||||||
|
const char8 *gamName, bool isOutput) {
|
||||||
mutex.FastLock();
|
mutex.FastLock();
|
||||||
BrokerInfo b;
|
BrokerInfo b;
|
||||||
b.signalPointers = signalPointers;
|
b.signalPointers = signalPointers;
|
||||||
@@ -484,6 +490,11 @@ void DebugService::RegisterBroker(DebugSignalInfo **signalPointers,
|
|||||||
b.activeIndices = activeIndices;
|
b.activeIndices = activeIndices;
|
||||||
b.activeSizes = activeSizes;
|
b.activeSizes = activeSizes;
|
||||||
b.activeMutex = activeMutex;
|
b.activeMutex = activeMutex;
|
||||||
|
b.anyBreakFlag = anyBreakFlag;
|
||||||
|
b.breakIndices = breakIndices;
|
||||||
|
b.isOutput = isOutput;
|
||||||
|
if (gamName != NULL_PTR(const char8 *))
|
||||||
|
b.gamName = gamName;
|
||||||
brokers.Push(b);
|
brokers.Push(b);
|
||||||
mutex.FastUnLock();
|
mutex.FastUnLock();
|
||||||
}
|
}
|
||||||
@@ -525,6 +536,28 @@ void DebugService::UpdateBrokersActiveStatus() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DebugService::UpdateBrokersBreakStatus() {
|
||||||
|
for (uint32 i = 0; i < brokers.Size(); i++) {
|
||||||
|
Vec<uint32> 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) {
|
ErrorManagement::ErrorType DebugService::Execute(ExecutionInfo &info) {
|
||||||
return ErrorManagement::FatalError;
|
return ErrorManagement::FatalError;
|
||||||
}
|
}
|
||||||
@@ -731,6 +764,72 @@ void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) {
|
|||||||
(void)client->Write(resp.Buffer(), s);
|
(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 <n> [<threadName>]
|
||||||
|
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 <signal> <op> <threshold> — set break condition
|
||||||
|
// BREAK <signal> 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")
|
} else if (token == "DISCOVER")
|
||||||
Discover(client);
|
Discover(client);
|
||||||
else if (token == "MSG") {
|
else if (token == "MSG") {
|
||||||
@@ -1369,6 +1468,159 @@ uint32 DebugService::TraceSignal(const char8 *name, bool enable,
|
|||||||
return count;
|
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 DebugService::IsInstrumented(const char8 *fullPath, bool &traceable,
|
||||||
bool &forcable) {
|
bool &forcable) {
|
||||||
mutex.FastLock();
|
mutex.FastLock();
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ struct BrokerInfo {
|
|||||||
Vec<uint32> *activeIndices;
|
Vec<uint32> *activeIndices;
|
||||||
Vec<uint32> *activeSizes;
|
Vec<uint32> *activeSizes;
|
||||||
FastPollingMutexSem *activeMutex;
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
class DebugService : public ReferenceContainer,
|
class DebugService : public ReferenceContainer,
|
||||||
@@ -55,7 +61,10 @@ public:
|
|||||||
void RegisterBroker(DebugSignalInfo **signalPointers, uint32 numSignals,
|
void RegisterBroker(DebugSignalInfo **signalPointers, uint32 numSignals,
|
||||||
MemoryMapBroker *broker, volatile bool *anyActiveFlag,
|
MemoryMapBroker *broker, volatile bool *anyActiveFlag,
|
||||||
Vec<uint32> *activeIndices, Vec<uint32> *activeSizes,
|
Vec<uint32> *activeIndices, Vec<uint32> *activeSizes,
|
||||||
FastPollingMutexSem *activeMutex);
|
FastPollingMutexSem *activeMutex,
|
||||||
|
volatile bool *anyBreakFlag, Vec<uint32> *breakIndices,
|
||||||
|
const char8 *gamName = NULL_PTR(const char8 *),
|
||||||
|
bool isOutput = false);
|
||||||
|
|
||||||
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info);
|
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info);
|
||||||
|
|
||||||
@@ -64,11 +73,27 @@ public:
|
|||||||
bool IsPaused() const { return isPaused; }
|
bool IsPaused() const { return isPaused; }
|
||||||
void SetPaused(bool paused) { isPaused = paused; }
|
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);
|
static bool GetFullObjectName(const Object &obj, StreamString &fullPath);
|
||||||
|
|
||||||
uint32 ForceSignal(const char8 *name, const char8 *valueStr);
|
uint32 ForceSignal(const char8 *name, const char8 *valueStr);
|
||||||
uint32 UnforceSignal(const char8 *name);
|
uint32 UnforceSignal(const char8 *name);
|
||||||
uint32 TraceSignal(const char8 *name, bool enable, uint32 decimation = 1);
|
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);
|
bool IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable);
|
||||||
void Discover(BasicTCPSocket *client);
|
void Discover(BasicTCPSocket *client);
|
||||||
void InfoNode(const char8 *path, BasicTCPSocket *client);
|
void InfoNode(const char8 *path, BasicTCPSocket *client);
|
||||||
@@ -93,6 +118,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void HandleCommand(StreamString cmd, BasicTCPSocket *client);
|
void HandleCommand(StreamString cmd, BasicTCPSocket *client);
|
||||||
void UpdateBrokersActiveStatus();
|
void UpdateBrokersActiveStatus();
|
||||||
|
void UpdateBrokersBreakStatus();
|
||||||
void InjectTcpLoggerIfNeeded();
|
void InjectTcpLoggerIfNeeded();
|
||||||
|
|
||||||
uint32 ExportTree(ReferenceContainer *container, StreamString &json, const char8 *pathPrefix);
|
uint32 ExportTree(ReferenceContainer *container, StreamString &json, const char8 *pathPrefix);
|
||||||
@@ -111,6 +137,9 @@ private:
|
|||||||
bool isServer;
|
bool isServer;
|
||||||
bool suppressTimeoutLogs;
|
bool suppressTimeoutLogs;
|
||||||
volatile bool isPaused;
|
volatile bool isPaused;
|
||||||
|
volatile uint32 stepRemaining;
|
||||||
|
StreamString pausedAtGam;
|
||||||
|
StreamString stepThreadFilter; // empty = all threads; non-empty = only this OS thread name
|
||||||
|
|
||||||
BasicTCPSocket tcpServer;
|
BasicTCPSocket tcpServer;
|
||||||
BasicUDPSocket udpSocket;
|
BasicUDPSocket udpSocket;
|
||||||
|
|||||||
@@ -63,14 +63,61 @@ public:
|
|||||||
Vec<uint32> sizes;
|
Vec<uint32> sizes;
|
||||||
FastPollingMutexSem mutex;
|
FastPollingMutexSem mutex;
|
||||||
DebugSignalInfo* ptrs[1] = { service.signals[0] };
|
DebugSignalInfo* ptrs[1] = { service.signals[0] };
|
||||||
service.RegisterBroker(ptrs, 1, NULL_PTR(MemoryMapBroker*), &active, &indices, &sizes, &mutex);
|
volatile bool anyBreak = false;
|
||||||
|
Vec<uint32> breakIdx;
|
||||||
|
service.RegisterBroker(ptrs, 1, NULL_PTR(MemoryMapBroker*), &active, &indices, &sizes, &mutex, &anyBreak, &breakIdx);
|
||||||
service.UpdateBrokersActiveStatus();
|
service.UpdateBrokersActiveStatus();
|
||||||
assert(active == true);
|
assert(active == true);
|
||||||
assert(indices.Size() == 1);
|
assert(indices.Size() == 1);
|
||||||
assert(indices[0] == 0);
|
assert(indices[0] == 0);
|
||||||
|
|
||||||
// Helper Process
|
// 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() {
|
int main() {
|
||||||
signal(SIGALRM, timeout_handler);
|
signal(SIGALRM, timeout_handler);
|
||||||
alarm(10);
|
alarm(10);
|
||||||
printf("--- MARTe2 Debug Suite COVERAGE V29 ---\n");
|
printf("--- MARTe2 Debug Suite COVERAGE V30 ---\n");
|
||||||
MARTe::TestTcpLogger();
|
MARTe::TestTcpLogger();
|
||||||
MARTe::DebugServiceTest::TestAll();
|
MARTe::DebugServiceTest::TestAll();
|
||||||
printf("\nCOVERAGE V29 PASSED!\n");
|
printf("\nCOVERAGE V30 PASSED!\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+645
-43
@@ -153,6 +153,9 @@ struct PlotInstance {
|
|||||||
plot_type: PlotType,
|
plot_type: PlotType,
|
||||||
signals: Vec<SignalPlotConfig>,
|
signals: Vec<SignalPlotConfig>,
|
||||||
auto_bounds: bool,
|
auto_bounds: bool,
|
||||||
|
max_points: usize,
|
||||||
|
follow: bool,
|
||||||
|
reset_view: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
@@ -198,6 +201,8 @@ enum InternalEvent {
|
|||||||
RecordingError(String, String), // SignalName, ErrorMessage
|
RecordingError(String, String), // SignalName, ErrorMessage
|
||||||
TelemMatched(u32),
|
TelemMatched(u32),
|
||||||
ServiceConfig { udp_port: String, log_port: String },
|
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 ---
|
// --- App State ---
|
||||||
@@ -212,6 +217,12 @@ struct MonitorDialog {
|
|||||||
period_ms: String,
|
period_ms: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct BreakDialog {
|
||||||
|
signal_path: String,
|
||||||
|
op: String, // ">", "<", "==", ">=", "<=", "!="
|
||||||
|
threshold: String,
|
||||||
|
}
|
||||||
|
|
||||||
struct MessageDialog {
|
struct MessageDialog {
|
||||||
destination: String,
|
destination: String,
|
||||||
function: String,
|
function: String,
|
||||||
@@ -219,6 +230,15 @@ struct MessageDialog {
|
|||||||
expect_reply: bool,
|
expect_reply: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct InfoDialog {
|
||||||
|
path: String,
|
||||||
|
is_signal: bool,
|
||||||
|
config_text: String,
|
||||||
|
is_loading: bool,
|
||||||
|
value_text: Option<String>,
|
||||||
|
value_loading: bool,
|
||||||
|
}
|
||||||
|
|
||||||
struct LogFilters {
|
struct LogFilters {
|
||||||
show_debug: bool,
|
show_debug: bool,
|
||||||
show_info: bool,
|
show_info: bool,
|
||||||
@@ -253,6 +273,12 @@ struct MarteDebugApp {
|
|||||||
traced_signals: Arc<Mutex<HashMap<String, TraceData>>>,
|
traced_signals: Arc<Mutex<HashMap<String, TraceData>>>,
|
||||||
plots: Vec<PlotInstance>,
|
plots: Vec<PlotInstance>,
|
||||||
forced_signals: HashMap<String, String>,
|
forced_signals: HashMap<String, String>,
|
||||||
|
break_conditions: HashMap<String, (String, f64)>, // signal -> (op, threshold)
|
||||||
|
break_dialog: Option<BreakDialog>,
|
||||||
|
step_status: Option<(bool, String, u32)>, // (paused, paused_at_gam, step_remaining)
|
||||||
|
last_step_poll: std::time::Instant,
|
||||||
|
step_thread: String,
|
||||||
|
info_dialog: Option<InfoDialog>,
|
||||||
logs: VecDeque<LogEntry>,
|
logs: VecDeque<LogEntry>,
|
||||||
log_filters: LogFilters,
|
log_filters: LogFilters,
|
||||||
show_left_panel: bool,
|
show_left_panel: bool,
|
||||||
@@ -329,8 +355,17 @@ impl MarteDebugApp {
|
|||||||
plot_type: PlotType::Normal,
|
plot_type: PlotType::Normal,
|
||||||
signals: Vec::new(),
|
signals: Vec::new(),
|
||||||
auto_bounds: true,
|
auto_bounds: true,
|
||||||
|
max_points: 5000,
|
||||||
|
follow: true,
|
||||||
|
reset_view: false,
|
||||||
}],
|
}],
|
||||||
forced_signals: HashMap::new(),
|
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),
|
logs: VecDeque::with_capacity(2000),
|
||||||
log_filters: LogFilters {
|
log_filters: LogFilters {
|
||||||
show_debug: true,
|
show_debug: true,
|
||||||
@@ -471,6 +506,22 @@ impl MarteDebugApp {
|
|||||||
objects
|
objects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_threads(&self) -> Vec<String> {
|
||||||
|
let mut threads = Vec::new();
|
||||||
|
if let Some(tree) = &self.app_tree {
|
||||||
|
fn collect(item: &TreeItem, out: &mut Vec<String>) {
|
||||||
|
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) {
|
fn render_tree(&mut self, ui: &mut egui::Ui, item: &TreeItem, path: String) {
|
||||||
let current_path = if path.is_empty() {
|
let current_path = if path.is_empty() {
|
||||||
if item.name == "Root" {
|
if item.name == "Root" {
|
||||||
@@ -502,6 +553,14 @@ impl MarteDebugApp {
|
|||||||
{
|
{
|
||||||
self.selected_node = current_path.clone();
|
self.selected_node = current_path.clone();
|
||||||
let _ = self.tx_cmd.send(format!("INFO {}", current_path));
|
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 {
|
} else {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui
|
let resp = ui.selectable_label(
|
||||||
.selectable_label(
|
|
||||||
self.selected_node == current_path,
|
self.selected_node == current_path,
|
||||||
format!("{} [{}]", label, item.class),
|
format!("{} [{}]", label, item.class),
|
||||||
)
|
);
|
||||||
.clicked()
|
if resp.clicked() {
|
||||||
{
|
|
||||||
self.selected_node = current_path.clone();
|
self.selected_node = current_path.clone();
|
||||||
let _ = self.tx_cmd.send(format!("INFO {}", current_path));
|
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") {
|
if item.class.contains("Signal") {
|
||||||
let elements = item.elements.unwrap_or(1);
|
let elements = item.elements.unwrap_or(1);
|
||||||
if elements > 1 {
|
if elements > 1 {
|
||||||
@@ -572,6 +662,21 @@ impl MarteDebugApp {
|
|||||||
value: "".to_string(),
|
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
|
let _ = tx_events_inner
|
||||||
.send(InternalEvent::ConfigResponse(text.to_string()));
|
.send(InternalEvent::ConfigResponse(text.to_string()));
|
||||||
json_acc.clear();
|
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::<u32>().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 {
|
} else {
|
||||||
if trimmed.starts_with("OK SERVICE_INFO") {
|
if trimmed.starts_with("OK SERVICE_INFO") {
|
||||||
@@ -1158,7 +1306,18 @@ impl eframe::App for MarteDebugApp {
|
|||||||
self.app_tree = Some(tree);
|
self.app_tree = Some(tree);
|
||||||
}
|
}
|
||||||
InternalEvent::NodeInfo(info) => {
|
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) => {
|
InternalEvent::TraceRequested(name, is_monitored) => {
|
||||||
let mut data_map = self.traced_signals.lock().unwrap();
|
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);
|
self.app_config_text = convert_config_json(&text);
|
||||||
}
|
}
|
||||||
InternalEvent::TelemMatched(_) => {}
|
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) => {
|
InternalEvent::RecordPathChosen(name, path) => {
|
||||||
let mut data_map = self.traced_signals.lock().unwrap();
|
let mut data_map = self.traced_signals.lock().unwrap();
|
||||||
if let Some(entry) = data_map.get_mut(&name) {
|
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 {
|
if self.scope.enabled {
|
||||||
self.apply_trigger_logic();
|
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::<f64>() {
|
||||||
|
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 {
|
if let Some(dialog) = &mut self.monitoring_dialog {
|
||||||
let mut close = false;
|
let mut close = false;
|
||||||
egui::Window::new("Monitor Signal").show(ctx, |ui| {
|
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<String> = 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| {
|
egui::TopBottomPanel::top("top").show(ctx, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.toggle_value(&mut self.show_left_panel, "🗂 Tree");
|
ui.toggle_value(&mut self.show_left_panel, "🗂 Tree");
|
||||||
@@ -1498,6 +1814,9 @@ impl eframe::App for MarteDebugApp {
|
|||||||
plot_type: PlotType::Normal,
|
plot_type: PlotType::Normal,
|
||||||
signals: Vec::new(),
|
signals: Vec::new(),
|
||||||
auto_bounds: true,
|
auto_bounds: true,
|
||||||
|
max_points: 5000,
|
||||||
|
follow: true,
|
||||||
|
reset_view: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
@@ -1516,6 +1835,10 @@ impl eframe::App for MarteDebugApp {
|
|||||||
} else {
|
} else {
|
||||||
"RESUME".to_string()
|
"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.separator();
|
||||||
ui.checkbox(&mut self.scope.enabled, "🔭 Scope");
|
ui.checkbox(&mut self.scope.enabled, "🔭 Scope");
|
||||||
@@ -1682,40 +2005,104 @@ impl eframe::App for MarteDebugApp {
|
|||||||
.resizable(true)
|
.resizable(true)
|
||||||
.width_range(250.0..=400.0)
|
.width_range(250.0..=400.0)
|
||||||
.show(ctx, |ui| {
|
.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");
|
ui.heading("Traced Signals");
|
||||||
let mut names: Vec<_> = {
|
let mut names: Vec<_> = {
|
||||||
let data_map = self.traced_signals.lock().unwrap();
|
let data_map = self.traced_signals.lock().unwrap();
|
||||||
data_map.keys().cloned().collect()
|
data_map.keys().cloned().collect()
|
||||||
};
|
};
|
||||||
names.sort();
|
names.sort();
|
||||||
|
let mut open_info_signal: Option<(String, f64)> = None;
|
||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
.id_salt("traced_scroll")
|
.id_salt("traced_scroll")
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
for key in names {
|
for key in names {
|
||||||
let mut data_map = self.traced_signals.lock().unwrap();
|
let (last_val, is_recording, is_monitored) = {
|
||||||
if let Some(entry) = data_map.get_mut(&key) {
|
let dm = self.traced_signals.lock().unwrap();
|
||||||
let last_val = entry.last_value;
|
if let Some(e) = dm.get(&key) {
|
||||||
let is_recording = entry.recording_tx.is_some();
|
(e.last_value, e.recording_tx.is_some(), e.is_monitored)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if is_recording {
|
if is_recording {
|
||||||
ui.label(
|
ui.label(egui::RichText::new("●").color(egui::Color32::RED));
|
||||||
egui::RichText::new("●").color(egui::Color32::RED),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let response = ui.add(
|
let response = ui.add(
|
||||||
egui::Label::new(format!("{}: {:.2}", key, last_val))
|
egui::Label::new(format!("{}: {:.2}", key, last_val))
|
||||||
.sense(
|
.sense(egui::Sense::drag().union(egui::Sense::click())),
|
||||||
egui::Sense::drag().union(egui::Sense::click()),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if response.drag_started() {
|
if response.drag_started() {
|
||||||
ctx.data_mut(|d| {
|
ctx.data_mut(|d| {
|
||||||
d.insert_temp(
|
d.insert_temp(egui::Id::new("drag_signal"), key.clone())
|
||||||
egui::Id::new("drag_signal"),
|
|
||||||
key.clone(),
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
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| {
|
response.context_menu(|ui| {
|
||||||
if !is_recording {
|
if !is_recording {
|
||||||
if ui.button("⏺ Record to Parquet").clicked() {
|
if ui.button("⏺ Record to Parquet").clicked() {
|
||||||
@@ -1726,38 +2113,47 @@ impl eframe::App for MarteDebugApp {
|
|||||||
.add_filter("Parquet", &["parquet"])
|
.add_filter("Parquet", &["parquet"])
|
||||||
.save_file()
|
.save_file()
|
||||||
{
|
{
|
||||||
let _ = tx.send(
|
let _ = tx.send(InternalEvent::RecordPathChosen(
|
||||||
InternalEvent::RecordPathChosen(
|
|
||||||
name_clone,
|
name_clone,
|
||||||
path.to_string_lossy()
|
path.to_string_lossy().to_string(),
|
||||||
.to_string(),
|
));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ui.button("⏹ Stop").clicked() {
|
if ui.button("⏹ Stop").clicked() {
|
||||||
entry.recording_tx = None;
|
let mut dm = self.traced_signals.lock().unwrap();
|
||||||
|
if let Some(e) = dm.get_mut(&key) {
|
||||||
|
e.recording_tx = None;
|
||||||
|
}
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if ui.button("❌").clicked() {
|
if ui.button("❌").clicked() {
|
||||||
if entry.is_monitored {
|
if is_monitored {
|
||||||
let _ = self.tx_cmd.send(format!("UNMONITOR SIGNAL {}", key));
|
let _ = self.tx_cmd.send(format!("UNMONITOR SIGNAL {}", key));
|
||||||
} else {
|
} else {
|
||||||
let _ = self.tx_cmd.send(format!("TRACE {} 0", key));
|
let _ = self.tx_cmd.send(format!("TRACE {} 0", key));
|
||||||
}
|
}
|
||||||
let _ = self
|
let _ = self.internal_tx.send(InternalEvent::ClearTrace(key.clone()));
|
||||||
.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.separator();
|
||||||
ui.heading("Forced Signals");
|
ui.heading("Forced Signals");
|
||||||
let mut to_delete = Vec::new();
|
let mut to_delete = Vec::new();
|
||||||
@@ -1774,6 +2170,45 @@ impl eframe::App for MarteDebugApp {
|
|||||||
self.forced_signals.remove(key);
|
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<String> = Vec::new();
|
||||||
|
let mut to_edit: Option<String> = 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 {
|
if self.show_message_history {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
@@ -1973,16 +2408,34 @@ impl eframe::App for MarteDebugApp {
|
|||||||
ui.group(|ui| {
|
ui.group(|ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(egui::RichText::new(&plot_inst.id).strong());
|
ui.label(egui::RichText::new(&plot_inst.id).strong());
|
||||||
ui.selectable_value(
|
ui.selectable_value(&mut plot_inst.plot_type, PlotType::Normal, "Series");
|
||||||
&mut plot_inst.plot_type,
|
ui.selectable_value(&mut plot_inst.plot_type, PlotType::LogicAnalyzer, "Logic");
|
||||||
PlotType::Normal,
|
ui.separator();
|
||||||
"Series",
|
let follow_text = if plot_inst.follow {
|
||||||
);
|
egui::RichText::new("▶ Follow").color(egui::Color32::from_rgb(100, 220, 100))
|
||||||
ui.selectable_value(
|
} else {
|
||||||
&mut plot_inst.plot_type,
|
egui::RichText::new("▶ Follow").color(egui::Color32::GRAY)
|
||||||
PlotType::LogicAnalyzer,
|
};
|
||||||
"Logic",
|
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() {
|
if ui.button("🗑").clicked() {
|
||||||
to_remove = Some(p_idx);
|
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<PlotBounds> = 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| {
|
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 !self.scope.enabled && !plot_inst.auto_bounds {
|
||||||
if let Some(range) = self.shared_x_range {
|
if let Some(range) = self.shared_x_range {
|
||||||
let bounds = plot_ui.plot_bounds();
|
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() {
|
for (s_idx, sig_cfg) in plot_inst.signals.iter().enumerate() {
|
||||||
if let Some(data) = data_map.get(&sig_cfg.source_name) {
|
if let Some(data) = data_map.get(&sig_cfg.source_name) {
|
||||||
let points_iter =
|
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;
|
let mut final_v = *v * sig_cfg.gain + sig_cfg.offset;
|
||||||
if plot_inst.plot_type == PlotType::LogicAnalyzer {
|
if plot_inst.plot_type == PlotType::LogicAnalyzer {
|
||||||
final_v = (s_idx as f64 * 1.5)
|
final_v = (s_idx as f64 * 1.5)
|
||||||
@@ -2110,13 +2604,20 @@ impl eframe::App for MarteDebugApp {
|
|||||||
{
|
{
|
||||||
if plot_resp.response.hovered() {
|
if plot_resp.response.hovered() {
|
||||||
plot_inst.auto_bounds = false;
|
plot_inst.auto_bounds = false;
|
||||||
|
plot_inst.follow = false;
|
||||||
let b = plot_resp.transform.bounds();
|
let b = plot_resp.transform.bounds();
|
||||||
self.shared_x_range = Some([b.min()[0], b.max()[0]]);
|
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| {
|
plot_resp.response.context_menu(|ui| {
|
||||||
if ui.button("🔍 Fit View").clicked() {
|
if ui.button("🔍 Fit View").clicked() {
|
||||||
plot_inst.auto_bounds = true;
|
plot_inst.auto_bounds = true;
|
||||||
|
plot_inst.follow = true;
|
||||||
|
plot_inst.reset_view = true;
|
||||||
self.shared_x_range = None;
|
self.shared_x_range = None;
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
@@ -2394,6 +2895,107 @@ mod tests {
|
|||||||
// ExportData() does not re-emit them after ConfigureApplication().
|
// ExportData() does not re-emit them after ConfigureApplication().
|
||||||
assert_classes_match(&expected_tree, &actual_tree, "");
|
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::<u32>().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> {
|
fn main() -> Result<(), eframe::Error> {
|
||||||
|
|||||||
Reference in New Issue
Block a user