diff --git a/API.md b/API.md new file mode 100644 index 0000000..f0e5533 --- /dev/null +++ b/API.md @@ -0,0 +1,61 @@ +# API Documentation + +## 1. TCP Control Interface (Port 8080) + +### 1.1 `TREE` +Retrieves the full object hierarchy. +- **Request:** `TREE +` +- **Response:** `JSON_OBJECT +OK TREE +` + +### 1.2 `DISCOVER` +Lists all registrable signals and their metadata. +- **Request:** `DISCOVER +` +- **Response:** `{"Signals": [...]} +OK DISCOVER +` + +### 1.3 `TRACE ` +Enables/disables telemetry for a signal. +- **Example:** `TRACE App.Data.Timer.Counter 1 +` +- **Response:** `OK TRACE +` + +### 1.4 `FORCE ` +Overrides a signal value in memory. +- **Example:** `FORCE App.Data.DDB.Signal 123.4 +` +- **Response:** `OK FORCE +` + +### 1.5 `PAUSE` / `RESUME` +Controls global execution state via the Scheduler. +- **Request:** `PAUSE +` +- **Response:** `OK +` + +--- + +## 2. UDP Telemetry Format (Port 8081) + +Telemetry packets are Little-Endian and use `#pragma pack(1)`. + +### 2.1 TraceHeader (20 Bytes) +| Offset | Type | Name | Description | +| :--- | :--- | :--- | :--- | +| 0 | uint32 | magic | Always `0xDA7A57AD` | +| 4 | uint32 | seq | Incremental sequence number | +| 8 | uint64 | timestamp | High-resolution timestamp | +| 16 | uint32 | count | Number of samples in payload | + +### 2.2 Sample Entry +| Offset | Type | Name | Description | +| :--- | :--- | :--- | :--- | +| 0 | uint32 | id | Internal Signal ID (from `DISCOVER`) | +| 4 | uint32 | size | Data size in bytes | +| 8 | Bytes | data | Raw signal memory | diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..f279b12 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,44 @@ +# System Architecture + +## 1. Overview +The suite consists of a C++ server library (`libmarte_dev.so`) that instrument MARTe2 applications at runtime, and a native Rust client for visualization. + +## 2. Core Mechanism: Registry Patching +The "Zero-Code-Change" requirement is met by intercepting the MARTe2 `ClassRegistryDatabase`. When the `DebugService` initializes, it replaces the standard object builders for critical classes: + +1. **Broker Injection:** Standard Brokers (e.g., `MemoryMapInputBroker`) are replaced with `DebugBrokerWrapper`. This captures data every time a GAM reads or writes a signal. +2. **Scheduler Injection:** The `FastScheduler` is replaced with `DebugFastScheduler`. This provides hooks at the start/end of each real-time cycle for execution control (pausing). + +## 3. Communication Layer +The system uses three distinct channels: + +### 3.1 Command & Control (TCP Port 8080) +- **Protocol:** Text-based over TCP. +- **Role:** Object tree discovery (`TREE`), signal metadata (`DISCOVER`), and trace activation (`TRACE`). +- **Response Format:** JSON for complex data, `OK/ERROR` for status. + +### 3.2 High-Speed Telemetry (UDP Port 8081) +- **Protocol:** Binary over UDP. +- **Format:** Packed C-structs. +- **Header:** `[Magic:4][Seq:4][TS:8][Count:4]` (Packed, 20 bytes). +- **Payload:** `[ID:4][Size:4][Data:N]` (Repeated for each traced signal). + +### 3.3 Log Streaming (TCP Port 8082) +- **Protocol:** Real-time event streaming. +- **Service:** `TcpLogger` (Standalone component). +- **Role:** Forwards global `REPORT_ERROR` calls and `stdout` messages to the GUI client. + +## 4. Component Diagram +```text +[ MARTe2 App ] <--- [ DebugFastScheduler ] (Registry Patch) + | | + + <--- [ DebugBrokerWrapper ] (Registry Patch) + | | + +---------------------+ + | | +[ DebugService ] [ TcpLogger ] + | | + +--- (TCP 8080) ------+-----> [ Rust GUI Client ] + +--- (UDP 8081) ------+-----> [ (Oscilloscope) ] + +--- (TCP 8082) ------+-----> [ (Log Terminal) ] +``` diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 75a34dd..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,59 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(marte_dev) - -if(NOT DEFINED ENV{MARTe2_DIR}) - message(FATAL_ERROR "MARTe2_DIR not set. Please source env.sh") -endif() - -set(MARTe2_DIR $ENV{MARTe2_DIR}) -set(MARTe2_Components_DIR $ENV{MARTe2_Components_DIR}) -set(TARGET $ENV{TARGET}) - -# Define Architecture macros -add_definitions(-DARCHITECTURE=x86_gcc) -add_definitions(-DENVIRONMENT=Linux) -add_definitions(-DMARTe2_TEST_ENVIRONMENT=GTest) # Optional -add_definitions(-DUSE_PTHREAD) - -# Add -pthread flag -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") - -include_directories( - ${MARTe2_DIR}/Source/Core/BareMetal/L0Types - ${MARTe2_DIR}/Source/Core/BareMetal/L1Portability - ${MARTe2_DIR}/Source/Core/BareMetal/L2Objects - ${MARTe2_DIR}/Source/Core/BareMetal/L3Streams - ${MARTe2_DIR}/Source/Core/BareMetal/L4Configuration - ${MARTe2_DIR}/Source/Core/BareMetal/L4Events - ${MARTe2_DIR}/Source/Core/BareMetal/L4Logger - ${MARTe2_DIR}/Source/Core/BareMetal/L4Messages - ${MARTe2_DIR}/Source/Core/BareMetal/L5FILES - ${MARTe2_DIR}/Source/Core/BareMetal/L5GAMs - ${MARTe2_DIR}/Source/Core/BareMetal/L6App - ${MARTe2_DIR}/Source/Core/Scheduler/L1Portability - ${MARTe2_DIR}/Source/Core/Scheduler/L3Services - ${MARTe2_DIR}/Source/Core/Scheduler/L4LoggerService - ${MARTe2_DIR}/Source/Core/FileSystem/L1Portability - ${MARTe2_DIR}/Source/Core/FileSystem/L3Streams - ${MARTe2_Components_DIR}/Source/Components/DataSources/EpicsDataSource - ${MARTe2_Components_DIR}/Source/Components/DataSources/FileDataSource - ${MARTe2_Components_DIR}/Source/Components/GAMs/IOGAM - Source - Headers -) - -file(GLOB_RECURSE SOURCES "Source/*.cpp") - -add_library(${PROJECT_NAME} SHARED ${SOURCES}) - -# Target MARTe2 library -set(MARTe2_LIB ${MARTe2_DIR}/Build/${TARGET}/Core/libMARTe2.so) -set(IOGAM_LIB ${MARTe2_Components_DIR}/Build/${TARGET}/Components/GAMs/IOGAM/libIOGAM.so) -set(LinuxTimer_LIB ${MARTe2_Components_DIR}/Build/${TARGET}/Components/DataSources/LinuxTimer/libLinuxTimer.so) - -target_link_libraries(${PROJECT_NAME} - ${MARTe2_LIB} -) - -add_subdirectory(Test/UnitTests) -add_subdirectory(Test/Integration) diff --git a/DEMO.md b/DEMO.md new file mode 100644 index 0000000..aab81f7 --- /dev/null +++ b/DEMO.md @@ -0,0 +1,32 @@ +# Demo Walkthrough: High-Speed Tracing + +This demo demonstrates tracing a `Timer.Counter` signal at 100Hz and verifying its consistency. + +### 1. Launch the Test Environment +Start the validation environment which simulates a real-time app: +```bash +./Build/Test/Integration/ValidationTest +``` +*Note: The test will wait for a trace command before finishing.* + +### 2. Connect the GUI +In another terminal: +```bash +cd Tools/gui_client +cargo run --release +``` + +### 3. Explore the Tree +1. On the left panel (**Application Tree**), expand `Root.App.Data.Timer`. +2. Click the `ℹ Info` button on the `Timer` node to see its configuration (e.g., `SleepTime: 10000`). + +### 4. Activate Trace +1. Locate the `Counter` signal under the `Timer` node. +2. Click the **📈 Trace** button. +3. The **Oscilloscope** in the center will immediately begin plotting the incremental counter. +4. Verify the **UDP Packets** counter in the top bar is increasing rapidly. + +### 5. Test Execution Control +1. Click the **⏸ Pause** button in the top bar. +2. Observe that the plot stops updating and the counter value holds steady. +3. Click **▶ Resume** to continue execution. diff --git a/Headers/DebugBrokerWrapper.h b/Headers/DebugBrokerWrapper.h deleted file mode 100644 index ed913cd..0000000 --- a/Headers/DebugBrokerWrapper.h +++ /dev/null @@ -1,167 +0,0 @@ -#ifndef DEBUGBROKERWRAPPER_H -#define DEBUGBROKERWRAPPER_H - -#include "DebugService.h" -#include "BrokerI.h" -#include "MemoryMapBroker.h" -#include "ObjectRegistryDatabase.h" -#include "ReferenceT.h" -#include "ObjectBuilder.h" -#include "HighResolutionTimer.h" - -// Original broker headers -#include "MemoryMapInputBroker.h" -#include "MemoryMapOutputBroker.h" -#include "MemoryMapSynchronisedInputBroker.h" -#include "MemoryMapSynchronisedOutputBroker.h" -#include "MemoryMapInterpolatedInputBroker.h" -#include "MemoryMapMultiBufferInputBroker.h" -#include "MemoryMapMultiBufferOutputBroker.h" -#include "MemoryMapSynchronisedMultiBufferInputBroker.h" -#include "MemoryMapSynchronisedMultiBufferOutputBroker.h" - -namespace MARTe { - -/** - * @brief Base implementation for all debug brokers. - */ -class DebugBrokerHelper { -public: - static void Process(BrokerI* broker, DebugService* service, DebugSignalInfo** signalInfoPointers, uint32 numSignals) { - if (service == NULL_PTR(DebugService*)) return; - - while (service->IsPaused()) { - Sleep::MSec(10); - } - - if (signalInfoPointers != NULL_PTR(DebugSignalInfo**)) { - for (uint32 i = 0; i < numSignals; i++) { - DebugSignalInfo *s = signalInfoPointers[i]; - if (s != NULL_PTR(DebugSignalInfo*)) { - if (s->isTracing || s->isForcing) { - uint32 size = broker->GetCopyByteSize(i); - service->ProcessSignal(s, size); - } - } - } - } - } - - static void InitSignals(MemoryMapBroker* broker, DataSourceI &dataSourceIn, DebugService* &service, DebugSignalInfo** &signalInfoPointers, uint32 &numSignals, MemoryMapBrokerCopyTableEntry* copyTable, const char8* functionName, SignalDirection direction) { - numSignals = broker->GetNumberOfCopies(); - if (numSignals > 0) { - signalInfoPointers = new DebugSignalInfo*[numSignals]; - for (uint32 i=0; iFind("DebugService"); - if (serviceRef.IsValid()) { - service = dynamic_cast(serviceRef.operator->()); - } - - if (service && (copyTable != NULL_PTR(MemoryMapBrokerCopyTableEntry*))) { - StreamString dsPath; - DebugService::GetFullObjectName(dataSourceIn, dsPath); - - for (uint32 i = 0; i < numSignals; i++) { - void *addr = copyTable[i].dataSourcePointer; - TypeDescriptor type = copyTable[i].type; - - uint32 dsIdx = broker->GetDSCopySignalIndex(i); - StreamString signalName; - if (!dataSourceIn.GetSignalName(dsIdx, signalName)) signalName = "Unknown"; - - // 1. Register canonical DataSource name (Absolute) - StreamString dsFullName; - dsFullName.Printf("%s.%s", dsPath.Buffer(), signalName.Buffer()); - service->RegisterSignal(addr, type, dsFullName.Buffer()); - - // 2. Also register absolute GAM alias - if (functionName != NULL_PTR(const char8*)) { - StreamString gamFullName; - const char8* dirStr = (direction == InputSignals) ? "In" : "Out"; - Reference gamRef = ObjectRegistryDatabase::Instance()->Find(functionName); - if (gamRef.IsValid()) { - StreamString absGamPath; - DebugService::GetFullObjectName(*(gamRef.operator->()), absGamPath); - gamFullName.Printf("%s.%s.%s", absGamPath.Buffer(), dirStr, signalName.Buffer()); - } else { - gamFullName.Printf("Root.%s.%s.%s", functionName, dirStr, signalName.Buffer()); - } - signalInfoPointers[i] = service->RegisterSignal(addr, type, gamFullName.Buffer()); - } else { - signalInfoPointers[i] = service->RegisterSignal(addr, type, dsFullName.Buffer()); - } - } - } - } -}; - -#define DECLARE_DEBUG_BROKER_COMMON(BaseClass) \ - Debug##BaseClass() : BaseClass() { \ - service = NULL_PTR(DebugService*); \ - signalInfoPointers = NULL_PTR(DebugSignalInfo**); \ - numSignals = 0; \ - } \ - virtual ~Debug##BaseClass() { \ - if (signalInfoPointers) delete[] signalInfoPointers; \ - } \ - virtual bool Execute() { \ - bool ret = BaseClass::Execute(); \ - if (ret) DebugBrokerHelper::Process(this, service, signalInfoPointers, numSignals); \ - return ret; \ - } \ -private: \ - DebugService *service; \ - DebugSignalInfo **signalInfoPointers; \ - uint32 numSignals; - -#define DECLARE_DEBUG_BROKER(BaseClass) \ -class Debug##BaseClass : public BaseClass { \ -public: \ - DECLARE_DEBUG_BROKER_COMMON(BaseClass) \ - virtual bool Init(SignalDirection direction, DataSourceI &ds, const char8 *const name, void *gamMem) { \ - bool ret = BaseClass::Init(direction, ds, name, gamMem); \ - if (ret) DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers, numSignals, this->copyTable, name, direction); \ - return ret; \ - } \ - virtual bool Init(SignalDirection direction, DataSourceI &ds, const char8 *const name, void *gamMem, const bool optim) { \ - bool ret = BaseClass::Init(direction, ds, name, gamMem, optim); \ - if (ret) DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers, numSignals, this->copyTable, name, direction); \ - return ret; \ - } \ -}; \ -class Debug##BaseClass##Builder : public ObjectBuilder { \ -public: \ - virtual Object *Build(HeapI* const heap) const { return new (heap) Debug##BaseClass(); } \ -}; - -#define DECLARE_DEBUG_BROKER_NO_OPTIM(BaseClass) \ -class Debug##BaseClass : public BaseClass { \ -public: \ - DECLARE_DEBUG_BROKER_COMMON(BaseClass) \ - virtual bool Init(SignalDirection direction, DataSourceI &ds, const char8 *const name, void *gamMem) { \ - bool ret = BaseClass::Init(direction, ds, name, gamMem); \ - if (ret) DebugBrokerHelper::InitSignals(this, ds, service, signalInfoPointers, numSignals, this->copyTable, name, direction); \ - return ret; \ - } \ -}; \ -class Debug##BaseClass##Builder : public ObjectBuilder { \ -public: \ - virtual Object *Build(HeapI* const heap) const { return new (heap) Debug##BaseClass(); } \ -}; - -DECLARE_DEBUG_BROKER(MemoryMapInputBroker) -DECLARE_DEBUG_BROKER(MemoryMapOutputBroker) -DECLARE_DEBUG_BROKER(MemoryMapSynchronisedInputBroker) -DECLARE_DEBUG_BROKER(MemoryMapSynchronisedOutputBroker) -DECLARE_DEBUG_BROKER_NO_OPTIM(MemoryMapInterpolatedInputBroker) -DECLARE_DEBUG_BROKER(MemoryMapMultiBufferInputBroker) -DECLARE_DEBUG_BROKER(MemoryMapMultiBufferOutputBroker) -DECLARE_DEBUG_BROKER(MemoryMapSynchronisedMultiBufferInputBroker) -DECLARE_DEBUG_BROKER(MemoryMapSynchronisedMultiBufferOutputBroker) - -} - -#endif diff --git a/Headers/DebugService.h b/Headers/DebugService.h deleted file mode 100644 index 04bde20..0000000 --- a/Headers/DebugService.h +++ /dev/null @@ -1,141 +0,0 @@ -#ifndef DEBUGSERVICE_H -#define DEBUGSERVICE_H - -#include "DataSourceI.h" -#include "GAM.h" -#include "MessageI.h" -#include "StreamString.h" -#include "BasicUDPSocket.h" -#include "BasicTCPSocket.h" -#include "ReferenceContainer.h" -#include "SingleThreadService.h" -#include "EmbeddedServiceMethodBinderI.h" -#include "FastMath.h" -#include "CompilerTypes.h" -#include "Object.h" -#include "DebugCore.h" -#include "ClassRegistryDatabase.h" -#include "ErrorManagement.h" -#include "AdvancedErrorManagement.h" -#include "LoggerConsumerI.h" -#include "Threads.h" -#include "EventSem.h" - -namespace MARTe { - -struct LogEntry { - ErrorManagement::ErrorInformation info; - char8 description[MAX_ERROR_MESSAGE_SIZE]; -}; - -struct SignalAlias { - StreamString name; - uint32 signalIndex; -}; - -class DebugService : public ReferenceContainer, public MessageI, public EmbeddedServiceMethodBinderI, public LoggerConsumerI { -public: - CLASS_REGISTER_DECLARATION() - - DebugService(); - virtual ~DebugService(); - - virtual bool Initialise(StructuredDataI & data); - - DebugSignalInfo* RegisterSignal(void* memoryAddress, TypeDescriptor type, const char8* name); - void ProcessSignal(DebugSignalInfo* signalInfo, uint32 size); - - virtual ErrorManagement::ErrorType Execute(ExecutionInfo & info); - virtual void ConsumeLogMessage(LoggerPage *logPage); - static void LogCallback(const ErrorManagement::ErrorInformation &errorInfo, const char8 * const errorDescription); - void InsertLogIntoQueue(LoggerPage *logPage); - - bool IsPaused() const { return isPaused; } - void SetPaused(bool paused) { isPaused = paused; } - - static bool GetFullObjectName(const Object &obj, StreamString &fullPath); - -private: - void HandleCommand(StreamString cmd, BasicTCPSocket *client); - uint32 ForceSignal(const char8* name, const char8* valueStr); - uint32 UnforceSignal(const char8* name); - uint32 TraceSignal(const char8* name, bool enable, uint32 decimation = 1); - void Discover(BasicTCPSocket *client); - void ListNodes(const char8* path, BasicTCPSocket *client); - void InfoNode(const char8* path, BasicTCPSocket *client); - uint32 ExportTree(ReferenceContainer *container, StreamString &json); - void PatchRegistry(); - - ErrorManagement::ErrorType Server(ExecutionInfo & info); - ErrorManagement::ErrorType Streamer(ExecutionInfo & info); - ErrorManagement::ErrorType LogStreamer(ExecutionInfo & info); - - uint16 controlPort; - uint16 streamPort; - uint16 logPort; - StreamString streamIP; - bool isServer; - bool suppressTimeoutLogs; - volatile bool isPaused; - - BasicTCPSocket tcpServer; - BasicUDPSocket udpSocket; - BasicTCPSocket logServer; - - class ServiceBinder : public EmbeddedServiceMethodBinderI { - public: - enum ServiceType { ServerType, StreamerType, LogStreamerType }; - ServiceBinder(DebugService *parent, ServiceType type) : parent(parent), type(type) {} - virtual ErrorManagement::ErrorType Execute(ExecutionInfo & info) { - if (type == StreamerType) return parent->Streamer(info); - if (type == LogStreamerType) return parent->LogStreamer(info); - return parent->Server(info); - } - private: - DebugService *parent; - ServiceType type; - }; - - ServiceBinder binderServer; - ServiceBinder binderStreamer; - ServiceBinder binderLogStreamer; - - SingleThreadService threadService; - SingleThreadService streamerService; - SingleThreadService logStreamerService; - - ThreadIdentifier serverThreadId; - ThreadIdentifier streamerThreadId; - ThreadIdentifier logStreamerThreadId; - - static const uint32 MAX_SIGNALS = 4096; - DebugSignalInfo signals[MAX_SIGNALS]; - uint32 numberOfSignals; - - static const uint32 MAX_ALIASES = 8192; - SignalAlias aliases[MAX_ALIASES]; - uint32 numberOfAliases; - - FastPollingMutexSem mutex; - TraceRingBuffer traceBuffer; - - static const uint32 MAX_CLIENTS = 16; - BasicTCPSocket* activeClients[MAX_CLIENTS]; - FastPollingMutexSem clientsMutex; - - BasicTCPSocket* activeLogClients[MAX_CLIENTS]; - FastPollingMutexSem logClientsMutex; - - static const uint32 LOG_QUEUE_SIZE = 1024; - LogEntry logQueue[LOG_QUEUE_SIZE]; - volatile uint32 logQueueRead; - volatile uint32 logQueueWrite; - EventSem logEvent; - - static DebugService* instance; - static ErrorManagement::ErrorProcessFunctionType originalLogCallback; -}; - -} - -#endif diff --git a/Makefile b/Makefile deleted file mode 100644 index 655f3fd..0000000 --- a/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -all: build - -build: - mkdir -p Build && cd Build && . ../env.sh && cmake -DCMAKE_BUILD_TYPE=Debug .. && make - ln -sf libmarte_dev.so Build/DebugService.so - -clean: - rm -rf Build - -.PHONY: all build clean diff --git a/Makefile.gcc b/Makefile.gcc new file mode 100644 index 0000000..8735ead --- /dev/null +++ b/Makefile.gcc @@ -0,0 +1,26 @@ +############################################################# +# +# Copyright 2015 F4E | European Joint Undertaking for ITER +# and the Development of Fusion Energy ('Fusion for Energy') +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# http://ec.europa.eu/idabc/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# +############################################################# +export TARGET=x86-linux + +include Makefile.inc diff --git a/Makefile.inc b/Makefile.inc new file mode 100644 index 0000000..a13a78a --- /dev/null +++ b/Makefile.inc @@ -0,0 +1,76 @@ +############################################################# +# +# Copyright 2015 F4E | European Joint Undertaking for ITER +# and the Development of Fusion Energy ('Fusion for Energy') +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# http://ec.europa.eu/idabc/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# +# $Id: Makefile.inc 3 2012-01-15 16:26:07Z aneto $ +# +############################################################# +#Subprojects w.r.t. the main directory only (important to allow setting SPBM as an export variable). +#If SPB is directly exported as an environment variable it will also be evaluated as part of the subprojects SPB, thus +#potentially overriding its value +#Main target subprojects. May be overridden by shell definition. +SPBM?=Source/Components/Interfaces.x +SPBMT?=Test.x + + + + +#This really has to be defined locally. +SUBPROJMAIN=$(SPBM:%.x=%.spb) +SUBPROJMAINTEST=$(SPBMT:%.x=%.spb) +SUBPROJMAINCLEAN=$(SPBM:%.x=%.spc) +SUBPROJMAINTESTCLEAN=$(SPBMT:%.x=%.spc) + + +MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults + +ROOT_DIR=. +include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET) + +all: $(OBJS) core test + echo $(OBJS) + +compile_commands.json: + bear -- make -f Makefile.gcc + +core: $(SUBPROJMAIN) check-env + echo $(SUBPROJMAIN) + +test: $(SUBPROJMAINTEST) + echo $(SUBPROJMAINTEST) + + + +clean:: $(SUBPROJMAINCLEAN) $(SUBPROJMAINTESTCLEAN) clean_wipe_old +#clean:: $(SUBPROJMAINCLEAN) $(SUBPROJMAINTESTCLEAN) clean_wipe_old + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) + +check-marte: +ifndef MARTe2_DIR + $(error MARTe2_DIR is undefined) +endif + +check-env: +ifndef MARTe2_DIR + $(error MARTe2_DIR is undefined) +endif + diff --git a/README.md b/README.md index 1e38c79..5af8f4d 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,48 @@ -# MARTe2 Universal Debugging & Observability Suite +# MARTe2 Debug Suite -A professional-grade, zero-code-change debugging suite for the MARTe2 real-time framework. - -## Features - -- **Runtime Registry Patching**: Instruments all MARTe2 Brokers automatically at startup. -- **Hierarchical Tree Explorer**: Recursive visualization of the `ObjectRegistryDatabase`, including GAMs, DataSources, and Signals. -- **Real-Time Execution Control**: Pause and Resume application logic globally to perform static inspection. -- **High-Speed Telemetry**: Visual oscilloscope with sub-millisecond precision via UDP. -- **Persistent Forcing**: Type-aware signal overrides (Last-Writer-Wins) with persistent re-application. -- **Isolated Log Streaming**: Dedicated TCP channel for real-time framework logs to ensure command responsiveness. - -## Components - -### 1. C++ Core (`libmarte_dev.so`) -The core service that handles registry patching, TCP/UDP communication, and real-time safe data capture. - -### 2. Rust GUI Client (`marte_debug_gui`) -A native, multi-threaded dashboard built with `egui`. -- **Side Panel**: Collapsible application tree and signal navigator. -- **Bottom Panel**: Advanced log terminal with Regex filtering and priority levels. -- **Right Panel**: Active Trace and Force management. -- **Central Pane**: High-frequency oscilloscope. +An interactive observability and debugging suite for the MARTe2 real-time framework. ## Quick Start -### Build +### 1. Build the project ```bash -# Build C++ Core -cd Build && cmake .. && make -j$(nproc) - -# Build GUI Client -cd Tools/gui_client -cargo build --release +. ./env.sh +make -f Makefile.gcc ``` -### Run -1. Start your MARTe2 application with the `DebugService` enabled. -2. Launch the GUI: - ```bash - ./Tools/gui_client/target/release/marte_debug_gui - ``` +### 2. Run Integration Tests +```bash +./Build/x86-linux/Test/Integration/Integration/IntegrationTests.ex # Runs all tests +``` -## Communication Ports -- **8080 (TCP)**: Commands (TREE, FORCE, TRACE, PAUSE). -- **8082 (TCP)**: Real-time framework logs. -- **8081 (UDP)**: Signal telemetry data. +### 3. Launch GUI +```bash +cd Tools/gui_client +cargo run --release +``` + +## Features +- **Live Tree:** Explore the MARTe2 object database in real-time. +- **Oscilloscope:** Trace any signal at high frequency (100Hz+) with automatic scaling. +- **Signal Forcing:** Inject values directly into the real-time memory map. +- **Log Forwarding:** Integrated framework log viewer with regex filtering. +- **Execution Control:** Global pause/resume via scheduler-level hooks. + +## Usage +To enable debugging in your application, add the following to your `.cfg`: +```text ++DebugService = { + Class = DebugService + ControlPort = 8080 + UdpPort = 8081 +} + ++LoggerService = { + Class = LoggerService + +DebugConsumer = { + Class = TcpLogger + Port = 8082 + } +} +``` +The suite automatically patches the registry to instrument your existing Brokers and Schedulers. diff --git a/SPECS.md b/SPECS.md new file mode 100644 index 0000000..f4ea37a --- /dev/null +++ b/SPECS.md @@ -0,0 +1,64 @@ +# MARTe2 Debug Suite Specifications + +**Version:** 1.2 +**Status:** Active / Implemented + +## 1. Executive Summary +This project implements a "Zero-Code-Change" observability and debugging layer for the MARTe2 real-time framework. The system allows developers to Trace, Force, and Monitor any signal in a running MARTe2 application without modifying existing source code. + +## 2. System Architecture +- **The Universal Debug Service (C++ Core):** A singleton MARTe2 Object that patches the registry and manages communication. +- **The Broker Injection Layer (C++ Templates):** Templated wrappers that intercept `Execute()` and `Init()` calls for tracing, forcing, and execution control. +- **The Remote Analyser (Rust/egui):** A high-performance, multi-threaded GUI for visualization and control. +- **Network Stack:** + - **Port 8080 (TCP):** Commands and Metadata. + - **Port 8081 (UDP):** High-Speed Telemetry for Oscilloscope. + - **Port 8082 (TCP):** Independent Real-Time Log Stream via `TcpLogger`. + +## 3. Requirements + +### 3.1 Functional Requirements (FR) +- **FR-01 (Discovery):** Discover the full MARTe2 object hierarchy at runtime. The GUI client SHALL request the full application tree upon connection and display it in a hierarchical tree view. +- **FR-02 (Telemetry):** Stream high-frequency signal data (verified up to 100Hz+) to a remote client via UDP. +- **FR-03 (Forcing):** Allow manual override of signal values in memory during execution. +- **FR-04 (Logs):** Stream global framework logs to a dedicated terminal/client via a standalone `TcpLogger` service. +- **FR-05 (Log Filtering):** The client must support filtering logs by type (Debug, Information, Warning, FatalError) and by content using regular expressions. +- **FR-06 (Execution Control):** Provide a mechanism to pause and resume the execution of all patched real-time threads (via Brokers), allowing for static inspection of the system state. +- **FR-07 (Session Management):** Support runtime re-configuration and "Apply & Reconnect" logic. The GUI provides a "Disconnect" button to close active network streams. +- **FR-08 (Decoupled Tracing):** Tracing activates telemetry; data is buffered and shown as a "Last Value" in the sidebar, but not plotted until manually assigned. +- **FR-09 (Advanced Plotting):** + - Support multiple plot panels with perfectly synchronized time (X) axes. + - Drag-and-drop signals from the traced list into specific plots. + - Plot modes: Standard (Time Series) and Logic Analyzer (Stacked rows). + - Signal transformations: Gain, offset, units, and custom labels. +- **FR-10 (Navigation & Scope):** + - Context menus for resetting zoom (X, Y, or both). + - "Fit to View" functionality that automatically scales both axes. + - High-performance oscilloscope mode with configurable time windows (10ms to 10s). + - Triggered acquisition (Single/Continuous, rising/falling edges). +- **FR-11 (Data Recording):** Record any traced signal to disk in Parquet format with a visual recording indicator in the GUI. +- **FR-12 (Configuration Awareness):** The DebugService SHALL store a complete copy of the application's configuration provided at initialization. +- **FR-13 (Metadata Enrichment):** Metadata returned by `INFO` and `DISCOVER` SHALL be enriched with additional fields from the stored configuration (e.g., Frequency, PVNames, Units). +- **FR-14 (Configuration Serving):** Provide a `CONFIG` command to serve the full stored configuration in JSON format to the client. + +### 3.2 Technical Constraints (TC) +- **TC-01:** No modifications allowed to the MARTe2 core library or component source code. +- **TC-02:** Instrumentation must use Runtime Class Registry Patching. +- **TC-03:** Real-time threads must remain lock-free; use `FastPollingMutexSem` or atomic operations for synchronization. +- **TC-04:** Telemetry must be delivered via UDP to minimize impact on real-time jitter. + +## 4. Performance Metrics +- **Latency:** Telemetry dispatch overhead < 5 microseconds per signal. +- **Throughput:** Support for 100Hz+ sampling rates with zero packet loss on local networks. +- **Scalability:** Handle up to 4096 unique signals and 16 simultaneous client connections. +- **Code Quality:** Maintain a minimum of **85% code coverage** across all core service and broker logic. + +## 5. Communication Protocol (Port 8080) +- **LS [Path]:** List nodes at the specified path. +- **TREE:** Returns a full recursive JSON structure representing the entire application tree. +- **INFO [Path]:** Returns detailed metadata for a specific node or signal. +- **PAUSE / RESUME:** Global execution control. +- **TRACE <1/0> [Decimation]:** Enable/disable telemetry for a signal. +- **FORCE :** Persistent signal override. +- **UNFORCE :** Remove override. +- **LOG :** Streaming format used on Port 8082. diff --git a/Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h b/Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h new file mode 100644 index 0000000..5a06915 --- /dev/null +++ b/Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h @@ -0,0 +1,572 @@ +#ifndef DEBUGBROKERWRAPPER_H +#define DEBUGBROKERWRAPPER_H + +#include "BrokerI.h" +#include "DataSourceI.h" +#include "DebugService.h" +#include "FastPollingMutexSem.h" +#include "HighResolutionTimer.h" +#include "MemoryMapBroker.h" +#include "ObjectBuilder.h" +#include "ObjectRegistryDatabase.h" +#include "Threads.h" +#include "Vec.h" + +// Original broker headers +#include "MemoryMapAsyncOutputBroker.h" +#include "MemoryMapAsyncTriggerOutputBroker.h" +#include "MemoryMapInputBroker.h" +#include "MemoryMapInterpolatedInputBroker.h" +#include "MemoryMapMultiBufferInputBroker.h" +#include "MemoryMapMultiBufferOutputBroker.h" +#include "MemoryMapOutputBroker.h" +#include "MemoryMapSynchronisedInputBroker.h" +#include "MemoryMapSynchronisedMultiBufferInputBroker.h" +#include "MemoryMapSynchronisedMultiBufferOutputBroker.h" +#include "MemoryMapSynchronisedOutputBroker.h" + +namespace MARTe { + +// Recursive search for any object with a given name anywhere in the registry. +// Used to find GAMs regardless of nesting level. +static Reference FindByNameRecursive(ReferenceContainer *container, + const char8 *name) { + if (container == NULL_PTR(ReferenceContainer *)) + return Reference(); + uint32 n = container->Size(); + for (uint32 i = 0; i < n; i++) { + Reference child = container->Get(i); + if (!child.IsValid()) + continue; + if (StringHelper::Compare(child->GetName(), name) == 0) + return child; + ReferenceContainer *sub = + dynamic_cast(child.operator->()); + if (sub != NULL_PTR(ReferenceContainer *)) { + Reference found = FindByNameRecursive(sub, name); + if (found.IsValid()) + return found; + } + } + return Reference(); +} + +/** + * @brief Helper for optimized signal processing within brokers. + */ +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, + volatile bool *anyBreakFlag, + Vec *breakIndices) { + if (service == NULL_PTR(DebugService *)) + return; + + // 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(); + if (n > 0 && signalInfoPointers != NULL_PTR(DebugSignalInfo **)) { + // Capture timestamp ONCE per broker cycle for lowest impact + uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() * + HighResolutionTimer::Period() * 1000000.0); + + for (uint32 i = 0; i < n; i++) { + uint32 idx = activeIndices[i]; + uint32 size = activeSizes[i]; + DebugSignalInfo *s = signalInfoPointers[idx]; + if (s != NULL_PTR(DebugSignalInfo *)) { + service->ProcessSignal(s, size, ts); + } + } + } + + // 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(); + } + + // Pass numCopies explicitly so we can mock it + static void + InitSignals(BrokerI *broker, DataSourceI &dataSourceIn, + DebugService *&service, DebugSignalInfo **&signalInfoPointers, + uint32 numCopies, MemoryMapBrokerCopyTableEntry *copyTable, + const char8 *functionName, SignalDirection direction, + volatile bool *anyActiveFlag, Vec *activeIndices, + Vec *activeSizes, FastPollingMutexSem *activeMutex, + volatile bool *anyBreakFlag, Vec *breakIndices) { + if (numCopies > 0) { + signalInfoPointers = new DebugSignalInfo *[numCopies]; + for (uint32 i = 0; i < numCopies; i++) + signalInfoPointers[i] = NULL_PTR(DebugSignalInfo *); + } + + ReferenceContainer *root = ObjectRegistryDatabase::Instance(); + Reference serviceRef = root->Find("DebugService"); + if (serviceRef.IsValid()) { + service = dynamic_cast(serviceRef.operator->()); + } + + if (service && (copyTable != NULL_PTR(MemoryMapBrokerCopyTableEntry *))) { + + StreamString dsPath; + DebugService::GetFullObjectName(dataSourceIn, dsPath); + fprintf(stderr, ">> %s broker for %s [%d]\n", + direction == InputSignals ? "Input" : "Output", dsPath.Buffer(), + numCopies); + MemoryMapBroker *mmb = dynamic_cast(broker); + if (mmb == NULL_PTR(MemoryMapBroker *)) { + fprintf(stderr, ">> Impossible to get broker pointer!!\n"); + } + + for (uint32 i = 0; i < numCopies; i++) { + void *addr = copyTable[i].dataSourcePointer; + TypeDescriptor type = copyTable[i].type; + + uint32 dsIdx = i; + if (mmb != NULL_PTR(MemoryMapBroker *)) { + dsIdx = mmb->GetDSCopySignalIndex(i); + } + + StreamString signalName; + if (!dataSourceIn.GetSignalName(dsIdx, signalName)) + signalName = "Unknown"; + fprintf(stderr, ">> registering %s.%s [%p]\n", dsPath.Buffer(), + signalName.Buffer(), mmb); + + uint8 dims = 0; + uint32 elems = 1; + (void)dataSourceIn.GetSignalNumberOfDimensions(dsIdx, dims); + (void)dataSourceIn.GetSignalNumberOfElements(dsIdx, elems); + + // Register canonical name + StreamString dsFullName; + dsFullName.Printf("%s.%s", dsPath.Buffer(), signalName.Buffer()); + service->RegisterSignal(addr, type, dsFullName.Buffer(), dims, elems); + + // Register alias + if (functionName != NULL_PTR(const char8 *)) { + StreamString gamFullName; + const char8 *dirStr = + (direction == InputSignals) ? "InputSignals" : "OutputSignals"; + const char8 *dirStrShort = (direction == InputSignals) ? "In" : "Out"; + + // Search recursively through the entire registry for the GAM by name. + // Direct Find("GAM1") only checks top-level; the GAM may be nested + // several levels deep inside a RealTimeApplication container. + Reference gamRef = + FindByNameRecursive(ObjectRegistryDatabase::Instance(), + functionName); + fprintf(stderr, ">> GAM lookup '%s': %s\n", functionName, + gamRef.IsValid() ? "FOUND" : "NOT FOUND"); + + if (gamRef.IsValid()) { + StreamString absGamPath; + DebugService::GetFullObjectName(*(gamRef.operator->()), absGamPath); + // Register short path (In/Out) for GUI compatibility + gamFullName.Printf("%s.%s.%s", absGamPath.Buffer(), dirStrShort, + signalName.Buffer()); + signalInfoPointers[i] = + service->RegisterSignal(addr, type, gamFullName.Buffer(), dims, elems); + } else { + // Fallback to short form + gamFullName.Printf("%s.%s.%s", functionName, dirStrShort, + signalName.Buffer()); + signalInfoPointers[i] = + service->RegisterSignal(addr, type, gamFullName.Buffer(), dims, elems); + } + } else { + signalInfoPointers[i] = + service->RegisterSignal(addr, type, dsFullName.Buffer(), dims, elems); + } + } + + // Register broker in DebugService for optimized control + bool isOutputBroker = (direction == OutputSignals); + service->RegisterBroker(signalInfoPointers, numCopies, mmb, anyActiveFlag, + activeIndices, activeSizes, activeMutex, + anyBreakFlag, breakIndices, + (functionName != NULL_PTR(const char8 *)) ? functionName : dsPath.Buffer(), + isOutputBroker); + } + } +}; + +/** + * @brief Template class to instrument any MARTe2 Broker. + */ +template class DebugBrokerWrapper : public BaseClass { +public: + DebugBrokerWrapper() : BaseClass() { + service = NULL_PTR(DebugService *); + signalInfoPointers = NULL_PTR(DebugSignalInfo **); + numSignals = 0; + anyActive = false; + anyBreakActive = false; + isOutput = false; + gamName[0] = '\0'; + } + + virtual ~DebugBrokerWrapper() { + if (signalInfoPointers) + delete[] signalInfoPointers; + } + + virtual bool Execute() { + bool ret = BaseClass::Execute(); + if (ret && (anyActive || anyBreakActive)) { + DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices, + 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; + } + + virtual bool Init(SignalDirection direction, DataSourceI &ds, + const char8 *const name, void *gamMem) { + bool ret = BaseClass::Init(direction, ds, name, gamMem); + fprintf(stderr, ">> INIT BROKER %s %s\n", name, + 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, + &anyBreakActive, &breakIndices); + } + return ret; + } + + virtual bool Init(SignalDirection direction, DataSourceI &ds, + const char8 *const name, void *gamMem, const bool optim) { + bool ret = BaseClass::Init(direction, ds, name, gamMem, false); + fprintf(stderr, ">> INIT optimized BROKER %s %s\n", name, + 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, + &anyBreakActive, &breakIndices); + } + return ret; + } + + DebugService *service; + DebugSignalInfo **signalInfoPointers; + uint32 numSignals; + volatile bool anyActive; + volatile bool anyBreakActive; + bool isOutput; + char8 gamName[256]; + Vec activeIndices; + Vec activeSizes; + Vec breakIndices; + FastPollingMutexSem activeMutex; +}; + +template +class DebugBrokerWrapperNoOptim : public BaseClass { +public: + DebugBrokerWrapperNoOptim() : BaseClass() { + service = NULL_PTR(DebugService *); + signalInfoPointers = NULL_PTR(DebugSignalInfo **); + numSignals = 0; + anyActive = false; + anyBreakActive = false; + isOutput = false; + gamName[0] = '\0'; + } + + virtual ~DebugBrokerWrapperNoOptim() { + if (signalInfoPointers) + delete[] signalInfoPointers; + } + + virtual bool Execute() { + bool ret = BaseClass::Execute(); + if (ret && (anyActive || anyBreakActive)) { + DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices, + activeSizes, activeMutex, + &anyBreakActive, &breakIndices); + } + if (ret && isOutput) { + DebugBrokerHelper::OutputPauseAndStep(service, gamName); + } + return ret; + } + + virtual bool Init(SignalDirection direction, DataSourceI &ds, + const char8 *const name, void *gamMem) { + 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, + &anyBreakActive, &breakIndices); + } + return ret; + } + + DebugService *service; + DebugSignalInfo **signalInfoPointers; + uint32 numSignals; + volatile bool anyActive; + volatile bool anyBreakActive; + bool isOutput; + char8 gamName[256]; + Vec activeIndices; + Vec activeSizes; + Vec breakIndices; + FastPollingMutexSem activeMutex; +}; + +class DebugMemoryMapAsyncOutputBroker : public MemoryMapAsyncOutputBroker { +public: + DebugMemoryMapAsyncOutputBroker() : MemoryMapAsyncOutputBroker() { + service = NULL_PTR(DebugService *); + signalInfoPointers = NULL_PTR(DebugSignalInfo **); + numSignals = 0; + anyActive = false; + anyBreakActive = false; + gamName[0] = '\0'; + } + virtual ~DebugMemoryMapAsyncOutputBroker() { + if (signalInfoPointers) + delete[] signalInfoPointers; + } + virtual bool Execute() { + bool ret = MemoryMapAsyncOutputBroker::Execute(); + if (ret && (anyActive || anyBreakActive)) { + DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices, + activeSizes, activeMutex, + &anyBreakActive, &breakIndices); + } + // Async output brokers are always output direction + if (ret) { + DebugBrokerHelper::OutputPauseAndStep(service, gamName); + } + return ret; + } + virtual bool InitWithBufferParameters(const SignalDirection direction, + DataSourceI &dataSourceIn, + const char8 *const functionName, + void *const gamMemoryAddress, + const uint32 numberOfBuffersIn, + const ProcessorType &cpuMaskIn, + const uint32 stackSizeIn) { + bool ret = MemoryMapAsyncOutputBroker::InitWithBufferParameters( + direction, dataSourceIn, functionName, gamMemoryAddress, + 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, &anyBreakActive, &breakIndices); + } + return ret; + } + DebugService *service; + DebugSignalInfo **signalInfoPointers; + uint32 numSignals; + volatile bool anyActive; + volatile bool anyBreakActive; + char8 gamName[256]; + Vec activeIndices; + Vec activeSizes; + Vec breakIndices; + FastPollingMutexSem activeMutex; +}; + +class DebugMemoryMapAsyncTriggerOutputBroker + : public MemoryMapAsyncTriggerOutputBroker { +public: + DebugMemoryMapAsyncTriggerOutputBroker() + : MemoryMapAsyncTriggerOutputBroker() { + service = NULL_PTR(DebugService *); + signalInfoPointers = NULL_PTR(DebugSignalInfo **); + numSignals = 0; + anyActive = false; + anyBreakActive = false; + gamName[0] = '\0'; + } + virtual ~DebugMemoryMapAsyncTriggerOutputBroker() { + if (signalInfoPointers) + delete[] signalInfoPointers; + } + virtual bool Execute() { + bool ret = MemoryMapAsyncTriggerOutputBroker::Execute(); + if (ret && (anyActive || anyBreakActive)) { + DebugBrokerHelper::Process(service, signalInfoPointers, activeIndices, + activeSizes, activeMutex, + &anyBreakActive, &breakIndices); + } + if (ret) { + DebugBrokerHelper::OutputPauseAndStep(service, gamName); + } + return ret; + } + virtual bool InitWithTriggerParameters( + const SignalDirection direction, DataSourceI &dataSourceIn, + const char8 *const functionName, void *const gamMemoryAddress, + const uint32 numberOfBuffersIn, const uint32 preTriggerBuffersIn, + const uint32 postTriggerBuffersIn, const ProcessorType &cpuMaskIn, + const uint32 stackSizeIn) { + bool ret = MemoryMapAsyncTriggerOutputBroker::InitWithTriggerParameters( + direction, dataSourceIn, functionName, gamMemoryAddress, + numberOfBuffersIn, preTriggerBuffersIn, postTriggerBuffersIn, 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, &anyBreakActive, &breakIndices); + } + return ret; + } + DebugService *service; + DebugSignalInfo **signalInfoPointers; + uint32 numSignals; + volatile bool anyActive; + volatile bool anyBreakActive; + char8 gamName[256]; + Vec activeIndices; + Vec activeSizes; + Vec breakIndices; + FastPollingMutexSem activeMutex; +}; + +template class DebugBrokerBuilder : public ObjectBuilder { +public: + virtual Object *Build(HeapI *const heap) const { return new (heap) T(); } +}; + +typedef DebugBrokerWrapper DebugMemoryMapInputBroker; +// LCOV_EXCL_START +typedef DebugBrokerWrapper DebugMemoryMapOutputBroker; +typedef DebugBrokerWrapper + DebugMemoryMapSynchronisedInputBroker; +typedef DebugBrokerWrapper + DebugMemoryMapSynchronisedOutputBroker; +typedef DebugBrokerWrapperNoOptim + DebugMemoryMapInterpolatedInputBroker; +typedef DebugBrokerWrapper + DebugMemoryMapMultiBufferInputBroker; +typedef DebugBrokerWrapper + DebugMemoryMapMultiBufferOutputBroker; +typedef DebugBrokerWrapper + DebugMemoryMapSynchronisedMultiBufferInputBroker; +typedef DebugBrokerWrapper + DebugMemoryMapSynchronisedMultiBufferOutputBroker; +// LCOV_EXCL_STOP + +typedef DebugBrokerBuilder + DebugMemoryMapInputBrokerBuilder; +// LCOV_EXCL_START +typedef DebugBrokerBuilder + DebugMemoryMapOutputBrokerBuilder; +typedef DebugBrokerBuilder + DebugMemoryMapSynchronisedInputBrokerBuilder; +typedef DebugBrokerBuilder + DebugMemoryMapSynchronisedOutputBrokerBuilder; +typedef DebugBrokerBuilder + DebugMemoryMapInterpolatedInputBrokerBuilder; +typedef DebugBrokerBuilder + DebugMemoryMapMultiBufferInputBrokerBuilder; +typedef DebugBrokerBuilder + DebugMemoryMapMultiBufferOutputBrokerBuilder; +typedef DebugBrokerBuilder + DebugMemoryMapSynchronisedMultiBufferInputBrokerBuilder; +typedef DebugBrokerBuilder + DebugMemoryMapSynchronisedMultiBufferOutputBrokerBuilder; +typedef DebugBrokerBuilder + DebugMemoryMapAsyncOutputBrokerBuilder; +typedef DebugBrokerBuilder + DebugMemoryMapAsyncTriggerOutputBrokerBuilder; +// LCOV_EXCL_STOP + +} // namespace MARTe + +#endif diff --git a/Headers/DebugCore.h b/Source/Components/Interfaces/DebugService/DebugCore.h similarity index 53% rename from Headers/DebugCore.h rename to Source/Components/Interfaces/DebugService/DebugCore.h index 368b4d6..08057ff 100644 --- a/Headers/DebugCore.h +++ b/Source/Components/Interfaces/DebugService/DebugCore.h @@ -4,20 +4,36 @@ #include "CompilerTypes.h" #include "TypeDescriptor.h" #include "StreamString.h" -#include "MemoryOperationsHelper.h" +#include 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; StreamString name; + uint8 numberOfDimensions; + 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) @@ -29,6 +45,10 @@ struct TraceHeader { }; #pragma pack(pop) +/** + * @brief Ring buffer for high-frequency signal tracing. + * @details New format per sample: [ID:4][Timestamp:8][Size:4][Data:N] + */ class TraceRingBuffer { public: TraceRingBuffer() { @@ -55,43 +75,53 @@ public: return (buffer != NULL_PTR(uint8*)); } - bool Push(uint32 signalID, void* data, uint32 size) { - uint32 packetSize = 4 + 4 + size; + bool Push(uint32 signalID, uint64 timestamp, void* data, uint32 size) { + uint32 packetSize = 4 + 8 + 4 + size; // ID + TS + Size + Data uint32 read = readIndex; uint32 write = writeIndex; - uint32 available = (read <= write) ? (bufferSize - (write - read) - 1) : (read - write - 1); + + uint32 available = 0; + if (read <= write) { + available = bufferSize - (write - read) - 1; + } else { + available = read - write - 1; + } if (available < packetSize) return false; - // Use temporary write index to ensure atomic update uint32 tempWrite = write; WriteToBuffer(&tempWrite, &signalID, 4); + WriteToBuffer(&tempWrite, ×tamp, 8); WriteToBuffer(&tempWrite, &size, 4); WriteToBuffer(&tempWrite, data, size); - // Final atomic update writeIndex = tempWrite; return true; } - bool Pop(uint32 &signalID, void* dataBuffer, uint32 &size, uint32 maxSize) { + bool Pop(uint32 &signalID, uint64 ×tamp, void* dataBuffer, uint32 &size, uint32 maxSize) { uint32 read = readIndex; uint32 write = writeIndex; if (read == write) return false; uint32 tempRead = read; - uint32 tempId, tempSize; + uint32 tempId = 0; + uint64 tempTs = 0; + uint32 tempSize = 0; + ReadFromBuffer(&tempRead, &tempId, 4); + ReadFromBuffer(&tempRead, &tempTs, 8); ReadFromBuffer(&tempRead, &tempSize, 4); if (tempSize > maxSize) { - // Error case: drop data up to writeIndex readIndex = write; return false; } ReadFromBuffer(&tempRead, dataBuffer, tempSize); + signalID = tempId; + timestamp = tempTs; size = tempSize; readIndex = tempRead; @@ -107,18 +137,30 @@ public: private: void WriteToBuffer(uint32 *idx, void* src, uint32 count) { - uint8* s = (uint8*)src; - for (uint32 i=0; i tLen) + return false; + const char8 *suffix = target + (tLen - pLen); + if (StringHelper::Compare(suffix, pattern) == 0) { + if (tLen == pLen || *(suffix - 1) == '.') + return true; + } + return false; +} + +static bool FindPathInContainer(ReferenceContainer *container, + const Object *target, StreamString &path) { + if (container == NULL_PTR(ReferenceContainer *)) + return false; + uint32 n = container->Size(); + for (uint32 i = 0u; i < n; i++) { + Reference ref = container->Get(i); + if (ref.IsValid()) { + if (ref.operator->() == target) { + path = ref->GetName(); + return true; + } + ReferenceContainer *sub = + dynamic_cast(ref.operator->()); + if (sub != NULL_PTR(ReferenceContainer *)) { + if (FindPathInContainer(sub, target, path)) { + StreamString full; + full.Printf("%s.%s", ref->GetName(), path.Buffer()); + path = full; + return true; + } + } + } + } + return false; +} + +CLASS_REGISTER(DebugService, "1.0") + +DebugService::DebugService() + : ReferenceContainer(), EmbeddedServiceMethodBinderI(), + binderServer(this, ServiceBinder::ServerType), + binderStreamer(this, ServiceBinder::StreamerType), + threadService(binderServer), streamerService(binderStreamer) { + controlPort = 0; + streamPort = 8081; + logPort = 8082; + streamIP = "127.0.0.1"; + isServer = false; + suppressTimeoutLogs = true; + isPaused = false; + stepRemaining = 0u; + manualConfigSet = false; + activeClient = NULL_PTR(BasicTCPSocket *); + streamerPacketOffset = 0u; + streamerSequenceNumber = 0u; +} + +DebugService::~DebugService() { + if (instance == this) { + instance = NULL_PTR(DebugService *); + } + threadService.Stop(); + streamerService.Stop(); + tcpServer.Close(); + udpSocket.Close(); + if (activeClient != NULL_PTR(BasicTCPSocket *)) { + activeClient->Close(); + delete activeClient; + } + for (uint32 i = 0; i < signals.Size(); i++) { + delete signals[i]; + } +} + +bool DebugService::Initialise(StructuredDataI &data) { + if (!ReferenceContainer::Initialise(data)) + return false; + + uint32 port = 0; + if (data.Read("ControlPort", port)) { + controlPort = (uint16)port; + } else { + (void)data.Read("TcpPort", port); + controlPort = (uint16)port; + } + + if (controlPort > 0) { + isServer = true; + instance = this; + } + + port = 8081; + if (data.Read("StreamPort", port)) { + streamPort = (uint16)port; + } else { + (void)data.Read("UdpPort", port); + streamPort = (uint16)port; + } + + port = 8082; + if (data.Read("LogPort", port)) { + logPort = (uint16)port; + } else { + (void)data.Read("TcpLogPort", port); + logPort = (uint16)port; + } + + StreamString tempIP; + if (data.Read("StreamIP", tempIP)) { + streamIP = tempIP; + } else { + streamIP = "127.0.0.1"; + } + uint32 suppress = 1; + if (data.Read("SuppressTimeoutLogs", suppress)) { + suppressTimeoutLogs = (suppress == 1); + } + + // Do NOT call MoveToRoot() on the shared CDB here — that corrupts the + // ReferenceContainer::Initialise() traversal cursor for sibling objects. + // Just capture the local subtree; full config is rebuilt lazily from the + // live ObjectRegistryDatabase when ServeConfig/EnrichWithConfig is called. + (void)data.Copy(fullConfig); + + if (isServer) { + if (!traceBuffer.Init(8 * 1024 * 1024)) + return false; + PatchRegistry(); + ConfigurationDatabase threadData; + threadData.Write("Timeout", (uint32)1000); + threadService.Initialise(threadData); + streamerService.Initialise(threadData); + if (!tcpServer.Open()) + return false; + if (!tcpServer.Listen(controlPort)) + return false; + if (!udpSocket.Open()) + return false; + if (threadService.Start() != ErrorManagement::NoError) + return false; + if (streamerService.Start() != ErrorManagement::NoError) + return false; + } + return true; +} + +void DebugService::InjectTcpLoggerIfNeeded() { + if (logPort == 0u) + return; + + // Check if the ORD already contains a LoggerService with at least one TcpLogger. + // If so, leave it untouched. + Reference existing = ObjectRegistryDatabase::Instance()->Find("LoggerService"); + if (existing.IsValid()) { + ReferenceContainer *rc = dynamic_cast(existing.operator->()); + if (rc != NULL_PTR(ReferenceContainer *)) { + for (uint32 i = 0u; i < rc->Size(); i++) { + ReferenceT child = rc->Get(i); + if (child.IsValid()) { + printf("[DebugService] Found existing TcpLogger in LoggerService — skipping injection.\n"); + return; + } + } + } + } + + // Build a CDB that mirrors the config-file declaration: + // LoggerService node (current position) + // Class = LoggerService + // CPUs = 1 + // DebugConsumer (child) + // Class = TcpLogger + // Port = + ConfigurationDatabase lsCdb; + (void)lsCdb.Write("Class", "LoggerService"); + uint32 cpus = 1u; + (void)lsCdb.Write("CPUs", cpus); + // ReferenceContainer::Initialise only instantiates children whose names + // start with '+' (matching the StandardParser convention). + if (lsCdb.CreateRelative("+DebugConsumer")) { + (void)lsCdb.Write("Class", "TcpLogger"); + uint32 p = static_cast(logPort); + (void)lsCdb.Write("Port", p); + (void)lsCdb.MoveToAncestor(1u); + } + (void)lsCdb.MoveToRoot(); + + ReferenceT ls( + "LoggerService", GlobalObjectsDatabase::Instance()->GetStandardHeap()); + if (!ls.IsValid()) { + printf("[DebugService] Failed to create LoggerService object.\n"); + return; + } + ls->SetName("LoggerService"); + if (!ls->Initialise(lsCdb)) { + printf("[DebugService] LoggerService::Initialise() failed.\n"); + return; + } + + // Insert into the ORD so it is findable by name and cleaned up on shutdown. + if (!ObjectRegistryDatabase::Instance()->Insert(ls)) { + printf("[DebugService] Failed to insert LoggerService into ORD.\n"); + // Keep a local reference anyway so the logger thread stays alive. + } + + // The ORD now holds a reference to ls; the LoggerService stays alive until + // ObjectRegistryDatabase::Purge(). No need to store a local reference. + printf("[DebugService] Auto-injected LoggerService + TcpLogger on port %u.\n", logPort); +} + +void DebugService::SetFullConfig(ConfigurationDatabase &config) { + config.MoveToRoot(); + config.Copy(fullConfig); + manualConfigSet = true; +} + +static void BuildCDBFromContainer(ReferenceContainer *container, + ConfigurationDatabase &cdb) { + if (container == NULL_PTR(ReferenceContainer *)) + return; + uint32 n = container->Size(); + for (uint32 i = 0u; i < n; i++) { + Reference child = container->Get(i); + if (!child.IsValid()) + continue; + const char8 *name = child->GetName(); + if (name == NULL_PTR(const char8 *)) + continue; + + bool created = cdb.CreateRelative(name); + if (!created) { + if (!cdb.MoveRelative(name)) + continue; + } + + const char8 *className = child->GetClassProperties()->GetName(); + if (className != NULL_PTR(const char8 *)) + (void)cdb.Write("Class", className); + + // Export scalar parameters via a SEPARATE CDB so the live cursor is + // never touched by ExportData. Then copy only top-level LEAF values + // (i.e. scalars where MoveRelative fails) and skip sub-nodes. + // + // This avoids two ExportData pitfalls: + // 1. ReferenceContainer::ExportData writes numeric-indexed child nodes + // (+0, +1, ...) — those are sub-nodes and get filtered out. + // 2. Some DataSource ExportData implementations follow internal + // references and write sibling objects as children — also sub-nodes, + // also filtered out. + // + // Scalar parameters (ControlPort, UdpPort, CPUs, Port, ...) pass through + // because they are leaf values, not sub-nodes. + { + ConfigurationDatabase exportCdb; + if (child->ExportData(exportCdb)) { + exportCdb.MoveToRoot(); + uint32 nExport = exportCdb.GetNumberOfChildren(); + for (uint32 j = 0u; j < nExport; j++) { + const char8 *ek = exportCdb.GetChildName(j); + if (StringHelper::Compare(ek, "Class") == 0 || + StringHelper::Compare(ek, "Name") == 0 || + StringHelper::Compare(ek, "IsContainer") == 0) + continue; + // Sub-node check: MoveRelative succeeds only for nodes, not scalars + if (exportCdb.MoveRelative(ek)) { + exportCdb.MoveToAncestor(1u); + continue; // skip sub-nodes entirely + } + // Leaf scalar — convert to string and write into the main CDB + AnyType at = exportCdb.GetType(ek); + if (at.GetDataPointer() != NULL_PTR(void *)) { + char8 buf[1024]; + AnyType st(CharString, 0u, buf); + st.SetNumberOfElements(0, 1024); + if (TypeConvert(st, at)) { + (void)cdb.Write(ek, buf); + } + } + } + } + } + + ReferenceContainer *sub = + dynamic_cast(child.operator->()); + if (sub != NULL_PTR(ReferenceContainer *)) + BuildCDBFromContainer(sub, cdb); + + (void)cdb.MoveToAncestor(1u); + } +} + +void DebugService::RebuildConfigFromRegistry() { + fullConfig = ConfigurationDatabase(); + BuildCDBFromContainer(ObjectRegistryDatabase::Instance(), fullConfig); + + // ExportData on ReferenceContainer subclasses (including DebugService itself) + // only writes Name/IsContainer/indexed children — it never re-emits the + // config-file parameters that were read in Initialise(). Write them back + // explicitly from the member variables that Initialise() stored. + const char8 *myName = GetName(); + if (myName != NULL_PTR(const char8 *)) { + if (fullConfig.MoveRelative(myName)) { + (void)fullConfig.Write("ControlPort", static_cast(controlPort)); + (void)fullConfig.Write("UdpPort", static_cast(streamPort)); + (void)fullConfig.Write("LogPort", static_cast(logPort)); + if (streamIP.Size() > 0u) + (void)fullConfig.Write("StreamIP", streamIP.Buffer()); + (void)fullConfig.MoveToAncestor(1u); + } + } +} + +static void PatchItemInternal(const char8 *originalName, + ObjectBuilder *debugBuilder) { + ClassRegistryItem *item = + ClassRegistryDatabase::Instance()->Find(originalName); + if (item != NULL_PTR(ClassRegistryItem *)) { + item->SetObjectBuilder(debugBuilder); + } +} + +void DebugService::PatchRegistry() { + DebugMemoryMapInputBrokerBuilder *b1 = new DebugMemoryMapInputBrokerBuilder(); + PatchItemInternal("MemoryMapInputBroker", b1); + DebugMemoryMapOutputBrokerBuilder *b2 = + new DebugMemoryMapOutputBrokerBuilder(); + PatchItemInternal("MemoryMapOutputBroker", b2); + DebugMemoryMapSynchronisedInputBrokerBuilder *b3 = + new DebugMemoryMapSynchronisedInputBrokerBuilder(); + PatchItemInternal("MemoryMapSynchronisedInputBroker", b3); + DebugMemoryMapSynchronisedOutputBrokerBuilder *b4 = + new DebugMemoryMapSynchronisedOutputBrokerBuilder(); + PatchItemInternal("MemoryMapSynchronisedOutputBroker", b4); + DebugMemoryMapInterpolatedInputBrokerBuilder *b5 = + new DebugMemoryMapInterpolatedInputBrokerBuilder(); + PatchItemInternal("MemoryMapInterpolatedInputBroker", b5); + DebugMemoryMapMultiBufferInputBrokerBuilder *b6 = + new DebugMemoryMapMultiBufferInputBrokerBuilder(); + PatchItemInternal("MemoryMapMultiBufferInputBroker", b6); + DebugMemoryMapMultiBufferOutputBrokerBuilder *b7 = + new DebugMemoryMapMultiBufferOutputBrokerBuilder(); + PatchItemInternal("MemoryMapMultiBufferOutputBroker", b7); + DebugMemoryMapSynchronisedMultiBufferInputBrokerBuilder *b8 = + new DebugMemoryMapSynchronisedMultiBufferInputBrokerBuilder(); + PatchItemInternal("MemoryMapSynchronisedMultiBufferInputBroker", b8); + DebugMemoryMapSynchronisedMultiBufferOutputBrokerBuilder *b9 = + new DebugMemoryMapSynchronisedMultiBufferOutputBrokerBuilder(); + PatchItemInternal("MemoryMapSynchronisedMultiBufferOutputBroker", b9); + DebugMemoryMapAsyncOutputBrokerBuilder *b10 = + new DebugMemoryMapAsyncOutputBrokerBuilder(); + PatchItemInternal("MemoryMapAsyncOutputBroker", b10); + DebugMemoryMapAsyncTriggerOutputBrokerBuilder *b11 = + new DebugMemoryMapAsyncTriggerOutputBrokerBuilder(); + PatchItemInternal("MemoryMapAsyncTriggerOutputBroker", b11); +} + +DebugSignalInfo *DebugService::RegisterSignal(void *memoryAddress, + TypeDescriptor type, + const char8 *name, + uint8 numberOfDimensions, + uint32 numberOfElements) { + fprintf(stderr, " RegisterSignal[%p]: %s\n", (void*)this, name); + mutex.FastLock(); + DebugSignalInfo *res = NULL_PTR(DebugSignalInfo *); + uint32 sigIdx = 0xFFFFFFFF; + for (uint32 i = 0; i < signals.Size(); i++) { + if (signals[i]->memoryAddress == memoryAddress) { + res = signals[i]; + sigIdx = i; + break; + } + } + if (res == NULL_PTR(DebugSignalInfo *)) { + sigIdx = signals.Size(); + res = new DebugSignalInfo(); + res->memoryAddress = memoryAddress; + res->type = type; + res->name = name; + res->numberOfDimensions = numberOfDimensions; + res->numberOfElements = numberOfElements; + res->isTracing = false; + res->isForcing = false; + res->internalID = sigIdx; + res->decimationFactor = 1; + res->decimationCounter = 0; + res->breakOp = BREAK_OFF; + res->breakThreshold = 0.0; + signals.Push(res); + } + if (sigIdx != 0xFFFFFFFF) { + bool foundAlias = false; + for (uint32 i = 0; i < aliases.Size(); i++) { + if (aliases[i].name == name) { + foundAlias = true; + break; + } + } + if (!foundAlias) { + SignalAlias a; + a.name = name; + a.signalIndex = sigIdx; + aliases.Push(a); + } + } + mutex.FastUnLock(); + return res; +} + +void DebugService::ProcessSignal(DebugSignalInfo *signalInfo, uint32 size, + uint64 timestamp) { + if (signalInfo == NULL_PTR(DebugSignalInfo *)) + return; + if (signalInfo->isForcing) { + memcpy(signalInfo->memoryAddress, signalInfo->forcedValue, size); + } + if (signalInfo->isTracing) { + if (signalInfo->decimationCounter == 0) { + traceBuffer.Push(signalInfo->internalID, timestamp, + (uint8 *)signalInfo->memoryAddress, size); + } + signalInfo->decimationCounter = + (signalInfo->decimationCounter + 1) % signalInfo->decimationFactor; + } +} + +void DebugService::RegisterBroker(DebugSignalInfo **signalPointers, + uint32 numSignals, MemoryMapBroker *broker, + volatile bool *anyActiveFlag, + Vec *activeIndices, + Vec *activeSizes, + FastPollingMutexSem *activeMutex, + volatile bool *anyBreakFlag, + Vec *breakIndices, + const char8 *gamName, bool isOutput) { + mutex.FastLock(); + BrokerInfo b; + b.signalPointers = signalPointers; + b.numSignals = numSignals; + b.broker = broker; + b.anyActiveFlag = anyActiveFlag; + 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(); +} + +void DebugService::UpdateBrokersActiveStatus() { + for (uint32 i = 0; i < brokers.Size(); i++) { + uint32 count = 0; + for (uint32 j = 0; j < brokers[i].numSignals; j++) { + DebugSignalInfo *s = brokers[i].signalPointers[j]; + if (s != NULL_PTR(DebugSignalInfo *) && (s->isTracing || s->isForcing)) { + count++; + } + } + + Vec tempInd; + Vec tempSizes; + for (uint32 j = 0; j < brokers[i].numSignals; j++) { + DebugSignalInfo *s = brokers[i].signalPointers[j]; + if (s != NULL_PTR(DebugSignalInfo *) && (s->isTracing || s->isForcing)) { + tempInd.Push(j); + tempSizes.Push((brokers[i].broker != NULL_PTR(MemoryMapBroker *)) + ? brokers[i].broker->GetCopyByteSize(j) + : 4); + } + } + + if (brokers[i].activeMutex) + brokers[i].activeMutex->FastLock(); + + if (brokers[i].activeIndices) + *(brokers[i].activeIndices) = tempInd; + if (brokers[i].activeSizes) + *(brokers[i].activeSizes) = tempSizes; + if (brokers[i].anyActiveFlag) + *(brokers[i].anyActiveFlag) = (count > 0); + + if (brokers[i].activeMutex) + brokers[i].activeMutex->FastUnLock(); + } +} + +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; +} + +ErrorManagement::ErrorType DebugService::HandleMessage(ReferenceT &data) { + printf(" DebugService received custom message: Function=%s\n", (const char8*)data->GetFunction()); + return ErrorManagement::NoError; +} + +ErrorManagement::ErrorType DebugService::Server(ExecutionInfo &info) { + if (info.GetStage() == ExecutionInfo::TerminationStage) + return ErrorManagement::NoError; + if (info.GetStage() == ExecutionInfo::StartupStage) { + serverThreadId = Threads::Id(); + // Wait for ObjectRegistryDatabase::Initialise() to finish processing all + // sibling objects (LoggerService etc. come after DebugService in the config). + // 500 ms is well above any realistic initialisation time. + Sleep::MSec(500u); + InjectTcpLoggerIfNeeded(); + return ErrorManagement::NoError; + } + // The MARTe2 framework calls Execute() in a loop; each call should do + // one unit of work and return so the framework can check for Stop(). + // This replaces the old internal infinite-while pattern. + if (activeClient == NULL_PTR(BasicTCPSocket *)) { + // Wait briefly for a new connection; return so the framework loop can + // check if Stop() was requested between calls. + BasicTCPSocket *newClient = tcpServer.WaitConnection(TimeoutType(100)); + if (newClient != NULL_PTR(BasicTCPSocket *)) { + activeClient = newClient; + } + } else { + // Check if client is still connected + if (!activeClient->IsConnected()) { + activeClient->Close(); + delete activeClient; + activeClient = NULL_PTR(BasicTCPSocket *); + } else { + char buffer[1024]; + uint32 size = 1024; + if (activeClient->Read(buffer, size)) { + if (size > 0) { + // Process each line separately + char *ptr = buffer; + char *end = buffer + size; + while (ptr < end) { + char *newline = (char *)memchr(ptr, '\n', end - ptr); + if (!newline) { + break; + } + *newline = '\0'; + // Skip carriage return if present + if (newline > ptr && *(newline - 1) == '\r') + *(newline - 1) = '\0'; + StreamString command; + uint32 len = (uint32)(newline - ptr); + command.Write(ptr, len); + if (command.Size() > 0) { + HandleCommand(command, activeClient); + } + ptr = newline + 1; + } + } + } else { + // Read failed (client disconnected or error), clean up + activeClient->Close(); + delete activeClient; + activeClient = NULL_PTR(BasicTCPSocket *); + } + } + } + return ErrorManagement::NoError; +} + +ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) { + if (info.GetStage() == ExecutionInfo::TerminationStage) + return ErrorManagement::NoError; + if (info.GetStage() == ExecutionInfo::StartupStage) { + streamerThreadId = Threads::Id(); + return ErrorManagement::NoError; + } + // Set UDP destination (idempotent, called each Execute() invocation) + InternetHost dest(streamPort, streamIP.Buffer()); + (void)udpSocket.SetDestination(dest); + + // Poll monitored signals + uint64 currentTimeMs = (uint64)((float64)HighResolutionTimer::Counter() * + HighResolutionTimer::Period() * 1000.0); + mutex.FastLock(); + for (uint32 i = 0; i < monitoredSignals.Size(); i++) { + if (currentTimeMs >= (monitoredSignals[i].lastPollTime + monitoredSignals[i].periodMs)) { + monitoredSignals[i].lastPollTime = currentTimeMs; + uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() * + HighResolutionTimer::Period() * 1000000.0); + void *address = NULL_PTR(void *); + if (monitoredSignals[i].dataSource->GetSignalMemoryBuffer(monitoredSignals[i].signalIdx, 0, address)) { + traceBuffer.Push(monitoredSignals[i].internalID, ts, (uint8 *)address, monitoredSignals[i].size); + } + } + } + mutex.FastUnLock(); + + // Drain ring buffer into UDP packet(s) + uint32 id, size; + uint64 ts; + uint8 sampleData[1024]; + bool hasData = false; + while (traceBuffer.Pop(id, ts, sampleData, size, 1024)) { + hasData = true; + if (streamerPacketOffset == 0u) { + TraceHeader header; + header.magic = 0xDA7A57AD; + header.seq = streamerSequenceNumber++; + header.timestamp = HighResolutionTimer::Counter(); + header.count = 0; + memcpy(streamerPacketBuffer, &header, sizeof(TraceHeader)); + streamerPacketOffset = sizeof(TraceHeader); + } + if (streamerPacketOffset + 16u + size > 1400u) { + uint32 toWrite = streamerPacketOffset; + (void)udpSocket.Write((char8 *)streamerPacketBuffer, toWrite); + TraceHeader header; + header.magic = 0xDA7A57AD; + header.seq = streamerSequenceNumber++; + header.timestamp = HighResolutionTimer::Counter(); + header.count = 0; + memcpy(streamerPacketBuffer, &header, sizeof(TraceHeader)); + streamerPacketOffset = sizeof(TraceHeader); + } + memcpy(&streamerPacketBuffer[streamerPacketOffset], &id, 4); + memcpy(&streamerPacketBuffer[streamerPacketOffset + 4], &ts, 8); + memcpy(&streamerPacketBuffer[streamerPacketOffset + 12], &size, 4); + memcpy(&streamerPacketBuffer[streamerPacketOffset + 16], sampleData, size); + streamerPacketOffset += (16u + size); + ((TraceHeader *)streamerPacketBuffer)->count++; + } + if (streamerPacketOffset > 0u) { + uint32 toWrite = streamerPacketOffset; + (void)udpSocket.Write((char8 *)streamerPacketBuffer, toWrite); + streamerPacketOffset = 0u; + } + if (!hasData) { + Sleep::MSec(1); + } + return ErrorManagement::NoError; +} + +bool DebugService::GetFullObjectName(const Object &obj, + StreamString &fullPath) { + fullPath = ""; + if (FindPathInContainer(ObjectRegistryDatabase::Instance(), &obj, fullPath)) { + return true; + } + const char8 *name = obj.GetName(); + if (name != NULL_PTR(const char8 *)) + fullPath = name; + return true; +} + +void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) { + StreamString token; + cmd.Seek(0); + char8 term; + const char8 *delims = " \r\n"; + if (cmd.GetToken(token, delims, term)) { + if (token == "FORCE") { + StreamString name, val; + if (cmd.GetToken(name, delims, term) && cmd.GetToken(val, delims, term)) { + uint32 count = ForceSignal(name.Buffer(), val.Buffer()); + if (client) { + StreamString resp; + resp.Printf("OK FORCE %u\n", count); + uint32 s = resp.Size(); + (void)client->Write(resp.Buffer(), s); + } + } + } else if (token == "UNFORCE") { + StreamString name; + if (cmd.GetToken(name, delims, term)) { + uint32 count = UnforceSignal(name.Buffer()); + if (client) { + StreamString resp; + resp.Printf("OK UNFORCE %u\n", count); + uint32 s = resp.Size(); + (void)client->Write(resp.Buffer(), s); + } + } + } else if (token == "TRACE") { + StreamString name, state, decim; + if (cmd.GetToken(name, delims, term) && + cmd.GetToken(state, delims, term)) { + bool enable = (state == "1"); + uint32 d = 1; + if (cmd.GetToken(decim, delims, term)) { + AnyType decimVal(UnsignedInteger32Bit, 0u, &d); + AnyType decimStr(CharString, 0u, decim.Buffer()); + (void)TypeConvert(decimVal, decimStr); + } + uint32 count = TraceSignal(name.Buffer(), enable, d); + if (client) { + StreamString resp; + resp.Printf("OK TRACE %u\n", count); + uint32 s = resp.Size(); + (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") { + StreamString dest, func, waitStr; + if (cmd.GetToken(dest, delims, term) && + cmd.GetToken(func, delims, term) && + cmd.GetToken(waitStr, delims, term)) { + bool wait = (waitStr == "1"); + + const char8 *pStart = cmd.Buffer() + cmd.Position(); + StreamString rawPayload = pStart; + + // Decode escaped newlines (\n) + StreamString payload; + rawPayload.Seek(0u); + char8 c; + while (rawPayload.Size() > rawPayload.Position()) { + uint32 readS = 1; + if (rawPayload.Read(&c, readS)) { + if (c == '\\') { + char8 next; + if (rawPayload.Read(&next, readS)) { + if (next == 'n') { + payload += '\n'; + } else { + payload += c; + payload += next; + } + } else { + payload += c; + } + } else { + payload += c; + } + } + } + + ReferenceT msg( + "Message", GlobalObjectsDatabase::Instance()->GetStandardHeap()); + + ConfigurationDatabase msgConfig; + msgConfig.Write("Destination", dest.Buffer()); + msgConfig.Write("Function", func.Buffer()); + if (wait) { + msgConfig.Write("Mode", "ExpectsReply"); + } + + // Parse payload key=value lines into a ConfigurationDatabase. + // ConstantGAM::SetOutput (and similar handlers) expect a + // ReferenceT inserted into the Message's + // reference container — NOT a sub-node of the message config. + ReferenceT paramCdb( + "ConfigurationDatabase", + GlobalObjectsDatabase::Instance()->GetStandardHeap()); + + if (payload.Size() > 0u && paramCdb.IsValid()) { + payload.Seek(0u); + StreamString line; + while (payload.GetToken(line, "\n", term)) { + if (line.Size() > 0u) { + const char8 *eq = StringHelper::SearchChar(line.Buffer(), '='); + if (eq != NULL_PTR(const char8 *)) { + uint32 eqPos = (uint32)(eq - line.Buffer()); + (void)line.Seek(0u); + + char8 keyBuf[256] = {'\0'}; + uint32 keyReadSize = eqPos; + (void)line.Read(keyBuf, keyReadSize); + + (void)line.Seek(eqPos + 1u); + uint32 valLen = (uint32)(line.Size() - eqPos - 1u); + char8 valBuf[1024] = {'\0'}; + (void)line.Read(valBuf, valLen); + + // Trim trailing whitespace from value + for (int32 ti = (int32)valLen - 1; ti >= 0; ti--) { + if (valBuf[ti] == ' ' || valBuf[ti] == '\r' || valBuf[ti] == '\t') + valBuf[ti] = '\0'; + else + break; + } + + StreamString key = keyBuf; + key = key.Buffer(); // trim happens via assignment + // Trim leading whitespace from key + const char8 *kp = keyBuf; + while (*kp == ' ' || *kp == '\t') kp++; + + if (*kp != '\0') { + (void)paramCdb->Write(kp, valBuf); + } + } + } + line = ""; + } + } + + ErrorManagement::ErrorType err = ErrorManagement::ParametersError; + if (msg->Initialise(msgConfig)) { + if (paramCdb.IsValid() && payload.Size() > 0u) { + // Insert the CDB as a ReferenceT parameter + (void)msg->Insert(paramCdb); + } + // Find destination object in the global database + Reference destObj = ObjectRegistryDatabase::Instance()->Find(dest.Buffer()); + if (destObj.IsValid()) { + Object* sender = this; + StreamString myPath; + if (!GetFullObjectName(*this, myPath)) { + sender = NULL_PTR(Object*); + } + + if (wait) { + err = MessageI::WaitForReply(msg, TTInfiniteWait); + } else { + (void)MessageI::SendMessage(msg, sender); + // Fire-and-forget: destination found, message sent. + // Whether the recipient had a matching filter is not + // reported back to the caller — return OK. + err = ErrorManagement::NoError; + } + } else { + printf(" MSG: Destination object %s not found in ORD\n", dest.Buffer()); + } + + if (err != ErrorManagement::NoError) { + printf(" MSG: MessageI dispatch failed.\n"); + } + } else { + printf(" MSG: Message initialization failed\n"); + } + + if (client) { + + if (err == ErrorManagement::NoError) { + uint32 okSize = 7; + (void)client->Write("OK MSG\n", okSize); + } else { + uint32 errSize = 10; + (void)client->Write("ERROR MSG\n", errSize); + } + } + } + } + else if (token == "SERVICE_INFO") { + if (client) { + StreamString resp; + resp.Printf("OK SERVICE_INFO TCP_CTRL:%u UDP_STREAM:%u TCP_LOG:%u STATE:%s\n", + controlPort, streamPort, logPort, isPaused ? "PAUSED" : "RUNNING"); + uint32 s = resp.Size(); + (void)client->Write(resp.Buffer(), s); + } + } else if (token == "MONITOR") { + StreamString subToken; + if (cmd.GetToken(subToken, delims, term) && subToken == "SIGNAL") { + StreamString name, period; + if (cmd.GetToken(name, delims, term) && cmd.GetToken(period, delims, term)) { + uint32 p = 100; + AnyType pVal(UnsignedInteger32Bit, 0u, &p); + AnyType pStr(CharString, 0u, period.Buffer()); + (void)TypeConvert(pVal, pStr); + uint32 count = RegisterMonitorSignal(name.Buffer(), p); + if (client) { + StreamString resp; + resp.Printf("OK MONITOR %u\n", count); + uint32 s = resp.Size(); + (void)client->Write(resp.Buffer(), s); + } + } + } + } else if (token == "UNMONITOR") { + StreamString subToken; + if (cmd.GetToken(subToken, delims, term) && subToken == "SIGNAL") { + StreamString name; + if (cmd.GetToken(name, delims, term)) { + uint32 count = UnmonitorSignal(name.Buffer()); + if (client) { + StreamString resp; + resp.Printf("OK UNMONITOR %u\n", count); + uint32 s = resp.Size(); + (void)client->Write(resp.Buffer(), s); + } + } + } + } else if (token == "CONFIG") + ServeConfig(client); + else if (token == "PAUSE") { + SetPaused(true); + if (client) { + uint32 s = 3; + (void)client->Write("OK\n", s); + } + } else if (token == "RESUME") { + SetPaused(false); + if (client) { + uint32 s = 3; + (void)client->Write("OK\n", s); + } + } else if (token == "TREE") { + StreamString json; + json = "{\"Name\": \"Root\", \"Class\": \"ObjectRegistryDatabase\", " + "\"Children\": [\n"; + (void)ExportTree(ObjectRegistryDatabase::Instance(), json, NULL_PTR(const char8 *)); + json += "\n]}\nOK TREE\n"; + uint32 s = json.Size(); + if (client) + (void)client->Write(json.Buffer(), s); + } else if (token == "INFO") { + StreamString path; + if (cmd.GetToken(path, delims, term)) + InfoNode(path.Buffer(), client); + } else if (token == "LS") { + StreamString path; + if (cmd.GetToken(path, delims, term)) + ListNodes(path.Buffer(), client); + else + ListNodes(NULL_PTR(const char8 *), client); + } + } +} + +void DebugService::EnrichWithConfig(const char8 *path, StreamString &json) { + if (path == NULL_PTR(const char8 *)) + return; + if (!manualConfigSet) { + RebuildConfigFromRegistry(); + } + fullConfig.MoveToRoot(); + + fprintf(stderr, "[EnrichWithConfig] path=%s\n", path); + + const char8 *current = path; + bool ok = true; + while (ok) { + const char8 *nextDot = StringHelper::SearchString(current, "."); + StreamString part; + if (nextDot != NULL_PTR(const char8 *)) { + uint32 len = (uint32)(nextDot - current); + (void)part.Write(current, len); + current = nextDot + 1; + } else { + part = current; + ok = false; + } + + // Normalise short direction names to both forms so we search consistently. + // Paths use "In"/"Out" (alias convention); CDBs may store either form. + bool found = false; + + // 1. Try exact match (bare name - rebuilt-from-registry CDBs use this) + if (fullConfig.MoveRelative(part.Buffer())) { + fprintf(stderr, "[EnrichWithConfig] nav exact '%s' OK\n", part.Buffer()); + found = true; + } + + // 2. Try +name (raw config-file CDBs prefix nodes with '+') + if (!found) { + StreamString prefixed; + prefixed.Printf("+%s", part.Buffer()); + if (fullConfig.MoveRelative(prefixed.Buffer())) { + fprintf(stderr, "[EnrichWithConfig] nav prefixed '%s' OK\n", prefixed.Buffer()); + found = true; + } + } + + // 3. Expand short direction aliases: In -> InputSignals / Out -> OutputSignals + if (!found) { + if (part == "In") { + if (fullConfig.MoveRelative("InputSignals")) { + fprintf(stderr, "[EnrichWithConfig] nav 'In'->InputSignals OK\n"); + found = true; + } else if (fullConfig.MoveRelative("+InputSignals")) { + fprintf(stderr, "[EnrichWithConfig] nav 'In'->+InputSignals OK\n"); + found = true; + } + } else if (part == "Out") { + if (fullConfig.MoveRelative("OutputSignals")) { + found = true; + } else if (fullConfig.MoveRelative("+OutputSignals")) { + found = true; + } + } + } + + if (!found) { + fprintf(stderr, "[EnrichWithConfig] FAILED at part '%s'\n", part.Buffer()); + fullConfig.MoveToRoot(); + return; + } + } + + ConfigurationDatabase db; + fullConfig.Copy(db); + fullConfig.MoveToRoot(); + db.MoveToRoot(); + uint32 n = db.GetNumberOfChildren(); + for (uint32 i = 0u; i < n; i++) { + const char8 *name = db.GetChildName(i); + AnyType at = db.GetType(name); + if (!at.GetTypeDescriptor().isStructuredData) { + json += ", \""; + EscapeJson(name, json); + json += "\": \""; + char8 buf[1024]; + AnyType st(CharString, 0u, buf); + st.SetNumberOfElements(0, 1024); + if (TypeConvert(st, at)) { + EscapeJson(buf, json); + } + json += "\""; + } + } +} + +void DebugService::JsonifyDatabase(ConfigurationDatabase &db, + StreamString &json) { + json += "{"; + uint32 n = db.GetNumberOfChildren(); + for (uint32 i = 0u; i < n; i++) { + const char8 *name = db.GetChildName(i); + json += "\""; + EscapeJson(name, json); + json += "\": "; + if (db.MoveRelative(name)) { + ConfigurationDatabase child; + db.Copy(child); + JsonifyDatabase(child, json); + db.MoveToAncestor(1u); + } else { + AnyType at = db.GetType(name); + char8 buf[1024]; + AnyType st(CharString, 0u, buf); + st.SetNumberOfElements(0, 1024); + if (TypeConvert(st, at)) { + json += "\""; + EscapeJson(buf, json); + json += "\""; + } else { + json += "null"; + } + } + if (i < n - 1) + json += ", "; + } + json += "}"; +} + +void DebugService::ServeConfig(BasicTCPSocket *client) { + if (client == NULL_PTR(BasicTCPSocket *)) + return; + + // If no manual config was injected (test case), rebuild from the live registry. + // In production, Initialise() only captures the DebugService subtree (to avoid + // corrupting the shared CDB cursor), so we always need to rebuild here. + if (!manualConfigSet) { + RebuildConfigFromRegistry(); + } + + StreamString json; + fullConfig.MoveToRoot(); + JsonifyDatabase(fullConfig, json); + json += "\nOK CONFIG\n"; + uint32 s = json.Size(); + (void)client->Write(json.Buffer(), s); +} + +void DebugService::InfoNode(const char8 *path, BasicTCPSocket *client) { + if (!client) + return; + Reference ref = ObjectRegistryDatabase::Instance()->Find(path); + StreamString json = "{"; + if (ref.IsValid()) { + json += "\"Name\": \""; + EscapeJson(ref->GetName(), json); + json += "\", \"Class\": \""; + EscapeJson(ref->GetClassProperties()->GetName(), json); + json += "\""; + ConfigurationDatabase db; + if (ref->ExportData(db)) { + json += ", \"Config\": {"; + db.MoveToRoot(); + uint32 nChildren = db.GetNumberOfChildren(); + for (uint32 i = 0; i < nChildren; i++) { + const char8 *cname = db.GetChildName(i); + AnyType at = db.GetType(cname); + char8 valBuf[1024]; + AnyType strType(CharString, 0u, valBuf); + strType.SetNumberOfElements(0, 1024); + if (TypeConvert(strType, at)) { + json += "\""; + EscapeJson(cname, json); + json += "\": \""; + EscapeJson(valBuf, json); + json += "\""; + if (i < nChildren - 1) + json += ", "; + } + } + json += "}"; + } + EnrichWithConfig(path, json); + } else { + StreamString enrichAlias; + mutex.FastLock(); + bool found = false; + for (uint32 i = 0; i < aliases.Size(); i++) { + if (aliases[i].name == path || + SuffixMatch(aliases[i].name.Buffer(), path)) { + DebugSignalInfo *s = signals[aliases[i].signalIndex]; + const char8 *tname = + TypeDescriptor::GetTypeNameFromTypeDescriptor(s->type); + json.Printf("\"Name\": \"%s\", \"Class\": \"Signal\", \"Type\": " + "\"%s\", \"ID\": %d", + s->name.Buffer(), tname ? tname : "Unknown", s->internalID); + enrichAlias = aliases[i].name; + found = true; + break; + } + } + if (!found) { + fprintf(stderr, "[InfoNode][%p] signal '%s' NOT found in %u aliases:\n", (void*)this, path, (uint32)aliases.Size()); + for (uint32 i = 0; i < aliases.Size(); i++) { + fprintf(stderr, " alias[%u] = '%s'\n", i, aliases[i].name.Buffer()); + } + } + mutex.FastUnLock(); + if (found) + EnrichWithConfig(enrichAlias.Buffer(), json); + else + json += "\"Error\": \"Object not found\""; + } + json += "}\nOK INFO\n"; + uint32 s = json.Size(); + (void)client->Write(json.Buffer(), s); +} + +uint32 DebugService::ExportTree(ReferenceContainer *container, + StreamString &json, const char8 *pathPrefix) { + if (container == NULL_PTR(ReferenceContainer *)) + return 0; + uint32 size = container->Size(); + uint32 validCount = 0; + for (uint32 i = 0u; i < size; i++) { + Reference child = container->Get(i); + if (child.IsValid()) { + if (validCount > 0u) + json += ",\n"; + StreamString nodeJson; + const char8 *cname = child->GetName(); + if (cname == NULL_PTR(const char8 *)) + cname = "unnamed"; + + StreamString currentPath; + if (pathPrefix != NULL_PTR(const char8 *)) { + currentPath.Printf("%s.%s", pathPrefix, cname); + } else { + currentPath = cname; + } + + nodeJson += "{\"Name\": \""; + EscapeJson(cname, nodeJson); + nodeJson += "\", \"Class\": \""; + EscapeJson(child->GetClassProperties()->GetName(), nodeJson); + nodeJson += "\""; + ReferenceContainer *inner = + dynamic_cast(child.operator->()); + DataSourceI *ds = dynamic_cast(child.operator->()); + GAM *gam = dynamic_cast(child.operator->()); + if ((inner != NULL_PTR(ReferenceContainer *)) || + (ds != NULL_PTR(DataSourceI *)) || (gam != NULL_PTR(GAM *))) { + nodeJson += ", \"Children\": [\n"; + uint32 subCount = 0u; + if (inner != NULL_PTR(ReferenceContainer *)) + subCount += ExportTree(inner, nodeJson, currentPath.Buffer()); + if (ds != NULL_PTR(DataSourceI *)) { + uint32 nSignals = ds->GetNumberOfSignals(); + for (uint32 j = 0u; j < nSignals; j++) { + if (subCount > 0u) + nodeJson += ",\n"; + subCount++; + StreamString sname; + (void)ds->GetSignalName(j, sname); + const char8 *stype = TypeDescriptor::GetTypeNameFromTypeDescriptor( + ds->GetSignalType(j)); + uint8 dims = 0u; + (void)ds->GetSignalNumberOfDimensions(j, dims); + uint32 elems = 0u; + (void)ds->GetSignalNumberOfElements(j, elems); + + StreamString signalFullPath; + signalFullPath.Printf("%s.%s", currentPath.Buffer(), sname.Buffer()); + bool traceable = false; + bool forcable = false; + (void)IsInstrumented(signalFullPath.Buffer(), traceable, forcable); + + nodeJson += "{\"Name\": \""; + EscapeJson(sname.Buffer(), nodeJson); + nodeJson += "\", \"Class\": \"Signal\", \"Type\": \""; + EscapeJson(stype ? stype : "Unknown", nodeJson); + nodeJson.Printf("\", \"Dimensions\": %d, \"Elements\": %u", dims, + elems); + nodeJson.Printf(", \"IsTraceable\": %s, \"IsForcable\": %s}", + traceable ? "true" : "false", + forcable ? "true" : "false"); + } + } + if (gam != NULL_PTR(GAM *)) { + uint32 nIn = gam->GetNumberOfInputSignals(); + for (uint32 j = 0u; j < nIn; j++) { + if (subCount > 0u) + nodeJson += ",\n"; + subCount++; + StreamString sname; + (void)gam->GetSignalName(InputSignals, j, sname); + const char8 *stype = TypeDescriptor::GetTypeNameFromTypeDescriptor( + gam->GetSignalType(InputSignals, j)); + uint32 dims = 0u; + (void)gam->GetSignalNumberOfDimensions(InputSignals, j, dims); + uint32 elems = 0u; + (void)gam->GetSignalNumberOfElements(InputSignals, j, elems); + + StreamString signalFullPath; + signalFullPath.Printf("%s.In.%s", currentPath.Buffer(), + sname.Buffer()); + bool traceable = false; + bool forcable = false; + (void)IsInstrumented(signalFullPath.Buffer(), traceable, forcable); + + nodeJson += "{\"Name\": \"In."; + EscapeJson(sname.Buffer(), nodeJson); + nodeJson += "\", \"Class\": \"InputSignal\", \"Type\": \""; + EscapeJson(stype ? stype : "Unknown", nodeJson); + nodeJson.Printf("\", \"Dimensions\": %u, \"Elements\": %u", dims, + elems); + nodeJson.Printf(", \"IsTraceable\": %s, \"IsForcable\": %s}", + traceable ? "true" : "false", + forcable ? "true" : "false"); + } + uint32 nOut = gam->GetNumberOfOutputSignals(); + for (uint32 j = 0u; j < nOut; j++) { + if (subCount > 0u) + nodeJson += ",\n"; + subCount++; + StreamString sname; + (void)gam->GetSignalName(OutputSignals, j, sname); + const char8 *stype = TypeDescriptor::GetTypeNameFromTypeDescriptor( + gam->GetSignalType(OutputSignals, j)); + uint32 dims = 0u; + (void)gam->GetSignalNumberOfDimensions(OutputSignals, j, dims); + uint32 elems = 0u; + (void)gam->GetSignalNumberOfElements(OutputSignals, j, elems); + + StreamString signalFullPath; + signalFullPath.Printf("%s.Out.%s", currentPath.Buffer(), + sname.Buffer()); + bool traceable = false; + bool forcable = false; + (void)IsInstrumented(signalFullPath.Buffer(), traceable, forcable); + + nodeJson += "{\"Name\": \"Out."; + EscapeJson(sname.Buffer(), nodeJson); + nodeJson += "\", \"Class\": \"OutputSignal\", \"Type\": \""; + EscapeJson(stype ? stype : "Unknown", nodeJson); + nodeJson.Printf("\", \"Dimensions\": %u, \"Elements\": %u", dims, + elems); + nodeJson.Printf(", \"IsTraceable\": %s, \"IsForcable\": %s}", + traceable ? "true" : "false", + forcable ? "true" : "false"); + } + } + nodeJson += "\n]"; + } + nodeJson += "}"; + json += nodeJson; + validCount++; + } + } + return validCount; +} + +uint32 DebugService::ForceSignal(const char8 *name, const char8 *valueStr) { + 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->isForcing = true; + AnyType dest(s->type, 0u, s->forcedValue); + AnyType source(CharString, 0u, valueStr); + (void)TypeConvert(dest, source); + count++; + } + } + UpdateBrokersActiveStatus(); + mutex.FastUnLock(); + return count; +} + +uint32 DebugService::UnforceSignal(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]->isForcing = false; + count++; + } + } + UpdateBrokersActiveStatus(); + mutex.FastUnLock(); + return count; +} + +uint32 DebugService::TraceSignal(const char8 *name, bool enable, + uint32 decimation) { + mutex.FastLock(); + uint32 count = 0; + for (uint32 i = 0; i < aliases.Size(); i++) { + printf("%s\n", aliases[i].name.Buffer()); + if (aliases[i].name == name || + SuffixMatch(aliases[i].name.Buffer(), name)) { + DebugSignalInfo *s = signals[aliases[i].signalIndex]; + s->isTracing = enable; + s->decimationFactor = decimation; + s->decimationCounter = 0; + count++; + } + } + if (count == 0) { + printf(" signal %s not found\n", name); + } + UpdateBrokersActiveStatus(); + mutex.FastUnLock(); + 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(); + bool found = false; + for (uint32 i = 0; i < aliases.Size(); i++) { + if (aliases[i].name == fullPath || + SuffixMatch(aliases[i].name.Buffer(), fullPath)) { + found = true; + break; + } + } + mutex.FastUnLock(); + traceable = found; + forcable = found; + return found; +} + +uint32 DebugService::RegisterMonitorSignal(const char8 *path, uint32 periodMs) { + mutex.FastLock(); + uint32 count = 0; + + // Check if already monitored + for (uint32 j = 0; j < monitoredSignals.Size(); j++) { + if (monitoredSignals[j].path == path) { + monitoredSignals[j].periodMs = periodMs; + mutex.FastUnLock(); + return 1; + } + } + + // Path resolution: find the DataSource object + StreamString fullPath = path; + fullPath.Seek(0); + char8 term; + Vec parts; + StreamString token; + while (fullPath.GetToken(token, ".", term)) { + parts.Push(token); + token = ""; + } + + if (parts.Size() >= 2) { + StreamString signalName = parts[parts.Size() - 1u]; + StreamString dsPath; + for (uint32 i = 0; i < parts.Size() - 1u; i++) { + dsPath += parts[i]; + if (i < parts.Size() - 2u) + dsPath += "."; + } + + ReferenceT ds = + ObjectRegistryDatabase::Instance()->Find(dsPath.Buffer()); + if (ds.IsValid()) { + uint32 idx = 0; + if (ds->GetSignalIndex(idx, signalName.Buffer())) { + MonitoredSignal m; + m.dataSource = ds; + m.signalIdx = idx; + m.path = path; + m.periodMs = periodMs; + m.lastPollTime = 0; + m.size = 0; + (void)ds->GetSignalByteSize(idx, m.size); + if (m.size == 0) + m.size = 4; + + // Use high-bit for polled signals to avoid conflict with brokered ones + m.internalID = 0x80000000 | monitoredSignals.Size(); + + // Re-use existing ID if signal is also instrumented via broker + for (uint32 i = 0; i < aliases.Size(); i++) { + if (aliases[i].name == path || + SuffixMatch(aliases[i].name.Buffer(), path)) { + m.internalID = signals[aliases[i].signalIndex]->internalID; + break; + } + } + + monitoredSignals.Push(m); + count = 1; + } + } + } + + mutex.FastUnLock(); + return count; +} + +uint32 DebugService::UnmonitorSignal(const char8 *path) { + mutex.FastLock(); + uint32 count = 0; + for (uint32 i = 0; i < monitoredSignals.Size(); i++) { + if (monitoredSignals[i].path == path || + SuffixMatch(monitoredSignals[i].path.Buffer(), path)) { + (void)monitoredSignals.Remove(i); + i--; + count++; + } + } + mutex.FastUnLock(); + return count; +} + +void DebugService::Discover(BasicTCPSocket *client) { + if (client) { + StreamString header = "{\n \"Signals\": [\n"; + uint32 s = header.Size(); + (void)client->Write(header.Buffer(), s); + mutex.FastLock(); + uint32 total = 0; + for (uint32 i = 0; i < aliases.Size(); i++) { + if (total > 0) { + uint32 commaSize = 2; + (void)client->Write(",\n", commaSize); + } + StreamString line; + DebugSignalInfo *sig = signals[aliases[i].signalIndex]; + const char8 *typeName = + TypeDescriptor::GetTypeNameFromTypeDescriptor(sig->type); + line.Printf(" {\"name\": \"%s\", \"id\": %d, \"type\": \"%s\", \"dimensions\": %u, \"elements\": %u", + aliases[i].name.Buffer(), sig->internalID, + typeName ? typeName : "Unknown", sig->numberOfDimensions, sig->numberOfElements); + EnrichWithConfig(aliases[i].name.Buffer(), line); + line += "}"; + s = line.Size(); + (void)client->Write(line.Buffer(), s); + total++; + } + + // Export monitored signals not already in aliases + for (uint32 i = 0; i < monitoredSignals.Size(); i++) { + bool found = false; + for (uint32 j = 0; j < aliases.Size(); j++) { + if (aliases[j].name == monitoredSignals[i].path) { + found = true; + break; + } + } + if (!found) { + if (total > 0) { + uint32 commaSize = 2; + (void)client->Write(",\n", commaSize); + } + StreamString line; + const char8 *typeName = TypeDescriptor::GetTypeNameFromTypeDescriptor( + monitoredSignals[i].dataSource->GetSignalType( + monitoredSignals[i].signalIdx)); + + uint8 dims = 0; + uint32 elems = 1; + (void)monitoredSignals[i].dataSource->GetSignalNumberOfDimensions(monitoredSignals[i].signalIdx, dims); + (void)monitoredSignals[i].dataSource->GetSignalNumberOfElements(monitoredSignals[i].signalIdx, elems); + + line.Printf(" {\"name\": \"%s\", \"id\": %u, \"type\": \"%s\", \"dimensions\": %u, \"elements\": %u", + monitoredSignals[i].path.Buffer(), + monitoredSignals[i].internalID, + typeName ? typeName : "Unknown", dims, elems); + EnrichWithConfig(monitoredSignals[i].path.Buffer(), line); + line += "}"; + s = line.Size(); + (void)client->Write(line.Buffer(), s); + total++; + } + } + mutex.FastUnLock(); + StreamString footer = " ]\n}\nOK DISCOVER\n"; + s = footer.Size(); + (void)client->Write(footer.Buffer(), s); + } +} + +void DebugService::ListNodes(const char8 *path, BasicTCPSocket *client) { + if (!client) + return; + Reference ref = + (path == NULL_PTR(const char8 *) || StringHelper::Length(path) == 0 || + StringHelper::Compare(path, "/") == 0) + ? ObjectRegistryDatabase::Instance() + : ObjectRegistryDatabase::Instance()->Find(path); + if (ref.IsValid()) { + StreamString out; + out.Printf("Nodes under %s:\n", path ? path : "/"); + ReferenceContainer *container = + dynamic_cast(ref.operator->()); + if (container) { + for (uint32 i = 0; i < container->Size(); i++) { + Reference child = container->Get(i); + if (child.IsValid()) + out.Printf(" %s [%s]\n", child->GetName(), + child->GetClassProperties()->GetName()); + } + } + const char *okMsg = "OK LS\n"; + out += okMsg; + uint32 s = out.Size(); + (void)client->Write(out.Buffer(), s); + } else { + const char *msg = "ERROR: Path not found\n"; + uint32 s = StringHelper::Length(msg); + (void)client->Write(msg, s); + } +} + +} // namespace MARTe diff --git a/Source/Components/Interfaces/DebugService/DebugService.h b/Source/Components/Interfaces/DebugService/DebugService.h new file mode 100644 index 0000000..e17f8fb --- /dev/null +++ b/Source/Components/Interfaces/DebugService/DebugService.h @@ -0,0 +1,196 @@ +#ifndef DEBUGSERVICE_H +#define DEBUGSERVICE_H + +#include "BasicTCPSocket.h" +#include "BasicUDPSocket.h" +#include "ConfigurationDatabase.h" +#include "DebugCore.h" +#include "EmbeddedServiceMethodBinderI.h" +#include "MessageI.h" +#include "Object.h" +#include "ReferenceContainer.h" +#include "ReferenceT.h" +#include "SingleThreadService.h" +#include "StreamString.h" +#include "Vec.h" + +namespace MARTe { + +class MemoryMapBroker; +class DataSourceI; + +struct SignalAlias { + StreamString name; + uint32 signalIndex; +}; + +struct BrokerInfo { + DebugSignalInfo **signalPointers; + uint32 numSignals; + MemoryMapBroker *broker; + volatile bool *anyActiveFlag; + Vec *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, + public MessageI, + public EmbeddedServiceMethodBinderI { +public: + friend class DebugServiceTest; + CLASS_REGISTER_DECLARATION() + + DebugService(); + virtual ~DebugService(); + + virtual bool Initialise(StructuredDataI &data); + + DebugSignalInfo *RegisterSignal(void *memoryAddress, TypeDescriptor type, + const char8 *name, uint8 numberOfDimensions = 0, + uint32 numberOfElements = 1); + void ProcessSignal(DebugSignalInfo *signalInfo, uint32 size, + uint64 timestamp); + + void RegisterBroker(DebugSignalInfo **signalPointers, uint32 numSignals, + MemoryMapBroker *broker, volatile bool *anyActiveFlag, + Vec *activeIndices, Vec *activeSizes, + FastPollingMutexSem *activeMutex, + volatile bool *anyBreakFlag, Vec *breakIndices, + const char8 *gamName = NULL_PTR(const char8 *), + bool isOutput = false); + + virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info); + + virtual ErrorManagement::ErrorType HandleMessage(ReferenceT &data); + + bool IsPaused() const { return isPaused; } + void SetPaused(bool paused) { isPaused = paused; } + + // Step-by-GAM / per-thread: called by each output broker. + // threadName is the OS thread name (from Threads::Name(Threads::Id())). + // Decrements stepRemaining only when stepThreadFilter is empty or matches; + // when stepRemaining reaches 0, sets isPaused = true and records gamName. + void ConsumeStepIfNeeded(const char8 *gamName, + const char8 *threadName = NULL_PTR(const char8 *)); + + void GetStepStatus(BasicTCPSocket *client); + // Step n cycles; if threadName is non-null/non-empty, only that thread advances. + void Step(uint32 n, const char8 *threadName = NULL_PTR(const char8 *)); + + // Read the current raw value of a signal from its memory address. + void GetSignalValue(const char8 *name, BasicTCPSocket *client); + + static bool GetFullObjectName(const Object &obj, StreamString &fullPath); + + uint32 ForceSignal(const char8 *name, const char8 *valueStr); + uint32 UnforceSignal(const char8 *name); + uint32 TraceSignal(const char8 *name, bool enable, uint32 decimation = 1); + uint32 SetBreak(const char8 *name, uint8 op, float64 threshold); + uint32 ClearBreak(const char8 *name); + bool IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable); + void Discover(BasicTCPSocket *client); + void InfoNode(const char8 *path, BasicTCPSocket *client); + void ListNodes(const char8 *path, BasicTCPSocket *client); + void ServeConfig(BasicTCPSocket *client); + void SetFullConfig(ConfigurationDatabase &config); + void RebuildConfigFromRegistry(); + + struct MonitoredSignal { + ReferenceT dataSource; + uint32 signalIdx; + uint32 internalID; + uint32 periodMs; + uint64 lastPollTime; + uint32 size; + StreamString path; + }; + + uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs); + uint32 UnmonitorSignal(const char8 *path); + +private: + void HandleCommand(StreamString cmd, BasicTCPSocket *client); + void UpdateBrokersActiveStatus(); + void UpdateBrokersBreakStatus(); + void InjectTcpLoggerIfNeeded(); + + uint32 ExportTree(ReferenceContainer *container, StreamString &json, const char8 *pathPrefix); + void PatchRegistry(); + + void EnrichWithConfig(const char8 *path, StreamString &json); + static void JsonifyDatabase(ConfigurationDatabase &db, StreamString &json); + + ErrorManagement::ErrorType Server(ExecutionInfo &info); + ErrorManagement::ErrorType Streamer(ExecutionInfo &info); + + uint16 controlPort; + uint16 streamPort; + uint16 logPort; + StreamString streamIP; + 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; + + class ServiceBinder : public EmbeddedServiceMethodBinderI { + public: + enum ServiceType { ServerType, StreamerType }; + ServiceBinder(DebugService *parent, ServiceType type) + : parent(parent), type(type) {} + virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info) { + if (type == StreamerType) { + return parent->Streamer(info); + } + return parent->Server(info); + } + + private: + DebugService *parent; + ServiceType type; + }; + + ServiceBinder binderServer; + ServiceBinder binderStreamer; + + SingleThreadService threadService; + SingleThreadService streamerService; + + ThreadIdentifier serverThreadId; + ThreadIdentifier streamerThreadId; + + Vec signals; + Vec aliases; + Vec brokers; + Vec monitoredSignals; + + FastPollingMutexSem mutex; + TraceRingBuffer traceBuffer; + + BasicTCPSocket *activeClient; + + // Streamer state persisted across Execute() calls (framework loops Execute) + uint8 streamerPacketBuffer[4096]; + uint32 streamerPacketOffset; + uint32 streamerSequenceNumber; + + ConfigurationDatabase fullConfig; + bool manualConfigSet; + + static DebugService *instance; +}; + +} // namespace MARTe + +#endif diff --git a/Source/Components/Interfaces/DebugService/Makefile.gcc b/Source/Components/Interfaces/DebugService/Makefile.gcc new file mode 100644 index 0000000..fb7e15f --- /dev/null +++ b/Source/Components/Interfaces/DebugService/Makefile.gcc @@ -0,0 +1,28 @@ +############################################################# +# +# Copyright 2015 F4E | European Joint Undertaking for ITER +# and the Development of Fusion Energy ('Fusion for Energy') +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# http://ec.europa.eu/idabc/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# +# $Id: Makefile.gcc 3 2015-01-15 16:26:07Z aneto $ +# +############################################################# + + +include Makefile.inc diff --git a/Source/Components/Interfaces/DebugService/Makefile.inc b/Source/Components/Interfaces/DebugService/Makefile.inc new file mode 100644 index 0000000..6083f09 --- /dev/null +++ b/Source/Components/Interfaces/DebugService/Makefile.inc @@ -0,0 +1,59 @@ +############################################################# +# +# Copyright 2015 F4E | European Joint Undertaking for ITER +# and the Development of Fusion Energy ('Fusion for Energy') +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# http://ec.europa.eu/idabc/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# +# $Id: Makefile.inc 3 2012-01-15 16:26:07Z aneto $ +# +############################################################# +OBJSX=DebugService.x + +PACKAGE=Components/Interfaces + +ROOT_DIR=../../../../ +MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults +include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET) + +INCLUDES += -I$(ROOT_DIR)/Source/Core/Types/Result +INCLUDES += -I$(ROOT_DIR)/Source/Core/Types/Vec +INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/TCPLogger +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Logger +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4LoggerService +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L5GAMs +INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams + +all: $(OBJS) $(SUBPROJ) \ + $(BUILD_DIR)/DebugService$(LIBEXT) \ + $(BUILD_DIR)/DebugService$(DLLEXT) + echo $(OBJS) + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) + diff --git a/Source/Components/Interfaces/Makefile.gcc b/Source/Components/Interfaces/Makefile.gcc new file mode 100644 index 0000000..f6b5b93 --- /dev/null +++ b/Source/Components/Interfaces/Makefile.gcc @@ -0,0 +1,26 @@ +############################################################# +# +# Copyright 2015 F4E | European Joint Undertaking for ITER +# and the Development of Fusion Energy ('Fusion for Energy') +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# http://ec.europa.eu/idabc/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# +############################################################# + + +include Makefile.inc diff --git a/Source/Components/Interfaces/Makefile.inc b/Source/Components/Interfaces/Makefile.inc new file mode 100644 index 0000000..ecc77d7 --- /dev/null +++ b/Source/Components/Interfaces/Makefile.inc @@ -0,0 +1,43 @@ +############################################################# +# +# Copyright 2015 F4E | European Joint Undertaking for ITER +# and the Development of Fusion Energy ('Fusion for Energy') +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# http://ec.europa.eu/idabc/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# +############################################################# + +OBJSX= + +SPB = TCPLogger.x DebugService.x + +ROOT_DIR=../../.. + + +PACKAGE=Components +ROOT_DIR=../../.. +ABS_ROOT_DIR=$(abspath $(ROOT_DIR)) +MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults + +include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET) + +all: $(OBJS) $(SUBPROJ) + echo $(OBJS) + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) + diff --git a/Source/Components/Interfaces/TCPLogger/Makefile.gcc b/Source/Components/Interfaces/TCPLogger/Makefile.gcc new file mode 100644 index 0000000..ab19097 --- /dev/null +++ b/Source/Components/Interfaces/TCPLogger/Makefile.gcc @@ -0,0 +1 @@ +include Makefile.inc diff --git a/Source/Components/Interfaces/TCPLogger/Makefile.inc b/Source/Components/Interfaces/TCPLogger/Makefile.inc new file mode 100644 index 0000000..0eac5cc --- /dev/null +++ b/Source/Components/Interfaces/TCPLogger/Makefile.inc @@ -0,0 +1,31 @@ +OBJSX=TcpLogger.x + +PACKAGE=Components/Interfaces + +ROOT_DIR=../../../../ +MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults +include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET) + +INCLUDES += -I. +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Logger +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4LoggerService +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L5GAMs +INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams + +all: $(OBJS) $(SUBPROJ) \ + $(BUILD_DIR)/TcpLogger$(LIBEXT) \ + $(BUILD_DIR)/TcpLogger$(DLLEXT) + echo $(OBJS) + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) diff --git a/Source/Components/Interfaces/TCPLogger/TcpLogger.cpp b/Source/Components/Interfaces/TCPLogger/TcpLogger.cpp new file mode 100644 index 0000000..1db34ad --- /dev/null +++ b/Source/Components/Interfaces/TCPLogger/TcpLogger.cpp @@ -0,0 +1,163 @@ +#include "TcpLogger.h" +#include "Threads.h" +#include "StringHelper.h" +#include "ConfigurationDatabase.h" + +namespace MARTe { + +CLASS_REGISTER(TcpLogger, "1.0") + +TcpLogger::TcpLogger() : + ReferenceContainer(), LoggerConsumerI(), EmbeddedServiceMethodBinderI(), + service(*this) +{ + port = 8082; + readIdx = 0; + writeIdx = 0; + workerThreadId = InvalidThreadIdentifier; + for (uint32 i=0; iClose(); + delete activeClients[i]; + activeClients[i] = NULL_PTR(BasicTCPSocket*); + } + } + clientsMutex.FastUnLock(); +} + +bool TcpLogger::ExportData(StructuredDataI & data) { + bool ok = data.Write("Port", static_cast(port)); + return ok; +} + +bool TcpLogger::Initialise(StructuredDataI & data) { + if (!ReferenceContainer::Initialise(data)) return false; + + if (!data.Read("Port", port)) { + (void)data.Read("TcpPort", port); + } + + (void)eventSem.Create(); + + ConfigurationDatabase threadData; + threadData.Write("Timeout", (uint32)1000); + if (!service.Initialise(threadData)) return false; + + if (!server.Open()) return false; + if (!server.Listen(port)) { + return false; + } + printf("[TcpLogger] Listening on port %u\n", port); + + if (service.Start() != ErrorManagement::NoError) { + return false; + } + + return true; +} + +void TcpLogger::ConsumeLogMessage(LoggerPage *logPage) { + if (logPage == NULL_PTR(LoggerPage*)) return; + + // 1. Mirror to stdout + StreamString levelStr; + ErrorManagement::ErrorCodeToStream(logPage->errorInfo.header.errorType, levelStr); + printf("[%s] %s\n", levelStr.Buffer(), logPage->errorStrBuffer); + fflush(stdout); + + // 2. Queue for TCP + InsertLogIntoQueue(logPage->errorInfo, logPage->errorStrBuffer); +} + +void TcpLogger::InsertLogIntoQueue(const ErrorManagement::ErrorInformation &info, const char8 * const description) { + uint32 next = (writeIdx + 1) % QUEUE_SIZE; + if (next != readIdx) { + TcpLogEntry &entry = queue[writeIdx % QUEUE_SIZE]; + entry.info = info; + StringHelper::Copy(entry.description, description); + writeIdx = next; + (void)eventSem.Post(); + } +} + +ErrorManagement::ErrorType TcpLogger::Execute(ExecutionInfo & info) { + if (info.GetStage() == ExecutionInfo::TerminationStage) return ErrorManagement::NoError; + if (info.GetStage() == ExecutionInfo::StartupStage) { + workerThreadId = Threads::Id(); + return ErrorManagement::NoError; + } + + // Each Execute() call does one cycle. The MARTe2 framework loops Execute() + // so we must NOT spin in an infinite internal loop here — doing so prevents + // the framework from ever delivering the TerminationStage and causes + // Stop() to time out, leaving threads running after the destructor. + + // 1. Check for new connections (1 ms timeout → returns promptly) + BasicTCPSocket *newClient = server.WaitConnection(1); + if (newClient != NULL_PTR(BasicTCPSocket *)) { + clientsMutex.FastLock(); + bool added = false; + for (uint32 i=0; iClose(); + delete newClient; + } else { + (void)newClient->SetBlocking(false); + } + } + + // 2. Stream queued entries to clients + bool hadData = false; + while (readIdx != writeIdx) { + hadData = true; + uint32 idx = readIdx % QUEUE_SIZE; + TcpLogEntry &entry = queue[idx]; + + StreamString level; + ErrorManagement::ErrorCodeToStream(entry.info.header.errorType, level); + + StreamString packet; + packet.Printf("LOG %s %s\n", level.Buffer(), entry.description); + uint32 size = packet.Size(); + + clientsMutex.FastLock(); + for (uint32 j=0; jWrite(packet.Buffer(), s)) { + activeClients[j]->Close(); + delete activeClients[j]; + activeClients[j] = NULL_PTR(BasicTCPSocket*); + } + } + } + clientsMutex.FastUnLock(); + readIdx = (readIdx + 1) % QUEUE_SIZE; + } + + if (!hadData) { + // Brief wait so we don't busy-spin; return so Stop() can take effect + (void)eventSem.Wait(TimeoutType(10)); + eventSem.Reset(); + } + return ErrorManagement::NoError; +} + +} diff --git a/Source/Components/Interfaces/TCPLogger/TcpLogger.h b/Source/Components/Interfaces/TCPLogger/TcpLogger.h new file mode 100644 index 0000000..313e26b --- /dev/null +++ b/Source/Components/Interfaces/TCPLogger/TcpLogger.h @@ -0,0 +1,67 @@ +#ifndef TCPLOGGER_H +#define TCPLOGGER_H + +#include "LoggerConsumerI.h" +#include "ReferenceContainer.h" +#include "EmbeddedServiceMethodBinderI.h" +#include "BasicTCPSocket.h" +#include "SingleThreadService.h" +#include "FastPollingMutexSem.h" +#include "EventSem.h" + +namespace MARTe { + +struct TcpLogEntry { + ErrorManagement::ErrorInformation info; + char8 description[MAX_ERROR_MESSAGE_SIZE]; +}; + +/** + * @brief Logger consumer that publishes framework logs to TCP and stdout. + * @details Implements LoggerConsumerI to be used inside a LoggerService. + */ +class TcpLogger : public ReferenceContainer, public LoggerConsumerI, public EmbeddedServiceMethodBinderI { +public: + CLASS_REGISTER_DECLARATION() + + TcpLogger(); + virtual ~TcpLogger(); + + virtual bool Initialise(StructuredDataI & data); + + virtual bool ExportData(StructuredDataI & data); + + /** + * @brief Implementation of LoggerConsumerI. + * Called by LoggerService. + */ + virtual void ConsumeLogMessage(LoggerPage *logPage); + + /** + * @brief Worker thread method for TCP streaming. + */ + virtual ErrorManagement::ErrorType Execute(ExecutionInfo & info); + +private: + void InsertLogIntoQueue(const ErrorManagement::ErrorInformation &info, const char8 * const description); + + uint16 port; + BasicTCPSocket server; + + static const uint32 MAX_CLIENTS = 8; + BasicTCPSocket* activeClients[MAX_CLIENTS]; + FastPollingMutexSem clientsMutex; + + SingleThreadService service; + ThreadIdentifier workerThreadId; + + static const uint32 QUEUE_SIZE = 1024; + TcpLogEntry queue[QUEUE_SIZE]; + volatile uint32 readIdx; + volatile uint32 writeIdx; + EventSem eventSem; +}; + +} + +#endif diff --git a/Source/Core/Types/Makefile.gcc b/Source/Core/Types/Makefile.gcc new file mode 100644 index 0000000..ab19097 --- /dev/null +++ b/Source/Core/Types/Makefile.gcc @@ -0,0 +1 @@ +include Makefile.inc diff --git a/Source/Core/Types/Makefile.inc b/Source/Core/Types/Makefile.inc new file mode 100644 index 0000000..edef047 --- /dev/null +++ b/Source/Core/Types/Makefile.inc @@ -0,0 +1,46 @@ +############################################################# +# +# Copyright 2015 F4E | European Joint Undertaking for ITER +# and the Development of Fusion Energy ('Fusion for Energy') +# +# Licensed under the EUPL, Version 1.1 or - as soon they +# will be approved by the European Commission - subsequent +# versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the +# Licence. +# You may obtain a copy of the Licence at: +# +# http://ec.europa.eu/idabc/eupl +# +# Unless required by applicable law or agreed to in +# writing, software distributed under the Licence is +# distributed on an "AS IS" basis, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. +# See the Licence for the specific language governing +# permissions and limitations under the Licence. +# +# $Id: Makefile.inc 3 2012-01-15 16:26:07Z aneto $ +# +############################################################# + +SPB = + + +ROOT_DIR=../../.. + + +PACKAGE=Core +ROOT_DIR=../../.. +ABS_ROOT_DIR=$(abspath $(ROOT_DIR)) +MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults + +include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET) + +all: $(OBJS) $(SUBPROJ) + echo $(OBJS) + +include depends.$(TARGET) + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) + diff --git a/Source/Core/Types/Result/Result.h b/Source/Core/Types/Result/Result.h new file mode 100644 index 0000000..c23374c --- /dev/null +++ b/Source/Core/Types/Result/Result.h @@ -0,0 +1,75 @@ +#ifndef __RESULT_H +#define __RESULT_H + +#include + +namespace MARTe { + +/** + * @brief Namespace for result error codes. + */ +namespace Errors { +enum ErrorT { + None = 0, + Generic = 1, + OutOfMemory = 2, + IndexOutOfBounds = 3, + ValueOutOfRange = 4, + WrongType = 5, + Empty = 6, +}; +} + +/** + * @brief A simple Result type for error handling. + * @details This implementation is header-only to ensure template linkage. + * Requirement: T and E must have default constructors and be copyable. + */ +template +class Result { +public: + static Result Success(const T &t) { + return Result(t, true); + } + + static Result Fail(const E &e) { + return Result(e, false); + } + + Result() : state(false), val(T()), err(E()) {} + Result(const Result &r) : state(r.state), val(r.val), err(r.err) {} + + Result& operator=(const Result &r) { + if (this != &r) { + state = r.state; + val = r.val; + err = r.err; + } + return *this; + } + + bool Ok() const { return state; } + bool IsOk() const { return state; } + + // Const accessors + const T &Val() const { assert(state); return val; } + const E &Err() const { assert(!state); return err; } + + // Non-const accessors + T &Val() { assert(state); return val; } + E &Err() { assert(!state); return err; } + + operator bool() const { return state; } + +private: + Result(const T &v, bool s) : state(s), val(v), err(E()) {} + Result(const E &e, bool s) : state(s), val(T()), err(e) {} + + bool state; + T val; + E err; +}; + +} // namespace MARTe + +#endif diff --git a/Source/Core/Types/Result/dependsRaw.x86-linux b/Source/Core/Types/Result/dependsRaw.x86-linux new file mode 100644 index 0000000..7ee6bd6 --- /dev/null +++ b/Source/Core/Types/Result/dependsRaw.x86-linux @@ -0,0 +1 @@ +Result.o: Result.cpp Result.h diff --git a/Source/Core/Types/Vec/Vec.h b/Source/Core/Types/Vec/Vec.h new file mode 100644 index 0000000..c9e98df --- /dev/null +++ b/Source/Core/Types/Vec/Vec.h @@ -0,0 +1,143 @@ +#ifndef __VEC_H +#define __VEC_H + +#include "Result.h" +#include +#include +#include +#include + +namespace MARTe { + +/** + * @brief Simple dynamic array (vector) implementation with fixed growth. + * @tparam T The type of elements. + * @tparam GROWTH The number of elements to add when the buffer is full. + */ +template +class Vec { +public: + Vec() : size(0), mem_size(GROWTH), arr(NULL) { + arr = new T[mem_size]; + } + + Vec(const Vec &other) : size(other.size), mem_size(other.mem_size), arr(NULL) { + arr = new T[mem_size]; + for (size_t i = 0; i < size; ++i) { + arr[i] = other.arr[i]; + } + } + + Vec(const T *data, const size_t count) : size(count), mem_size(count + GROWTH), arr(NULL) { + arr = new T[mem_size]; + for (size_t i = 0; i < size; ++i) { + arr[i] = data[i]; + } + } + + ~Vec() { + if (arr != NULL) { + delete[] arr; + arr = NULL; + } + } + + Vec& operator=(const Vec &other) { + if (this != &other) { + T* new_arr = new T[other.mem_size]; + for (size_t i = 0; i < other.size; ++i) { + new_arr[i] = other.arr[i]; + } + if (arr != NULL) { + delete[] arr; + } + arr = new_arr; + size = other.size; + mem_size = other.mem_size; + } + return *this; + } + + void Clear() { + size = 0; + } + + size_t Size() const { + return size; + } + + T* GetInternalBuffer() { return arr; } + + bool Remove(size_t index) { + if (index >= size) return false; + for (size_t i = index; i < size - 1; ++i) { + arr[i] = arr[i + 1]; + } + size--; + return true; + } + + bool Insert(size_t index, const T& val) { + if (index > size) return false; + if (size == mem_size) extend(); + + for (size_t i = size; i > index; --i) { + arr[i] = arr[i - 1]; + } + arr[index] = val; + size++; + return true; + } + + Result Get(size_t index) const { + if (index >= size) return Result::Fail(Errors::IndexOutOfBounds); + return Result::Success(arr[index]); + } + + void Push(const T &val) { + if (size == mem_size) extend(); + arr[size++] = val; + } + + Result Pop() { + if (size == 0) return Result::Fail(Errors::Empty); + T last = arr[--size]; + return Result::Success(last); + } + + const T &operator[](const size_t index) const { + assert(index < size); + return arr[index]; + } + + T &operator[](const size_t index) { + assert(index < size); + return arr[index]; + } + +protected: + const T *mem() const { return arr; } + size_t memSize() const { return mem_size; } + +private: + size_t size; + size_t mem_size; + T *arr; + + void extend() { + size_t new_mem_size = mem_size + GROWTH; + T *new_arr = new T[new_mem_size]; + for (size_t i = 0; i < size; ++i) { + new_arr[i] = arr[i]; + } + if (arr != NULL) { + delete[] arr; + } + arr = new_arr; + mem_size = new_mem_size; + } +}; + +} // namespace MARTe + +#endif diff --git a/Source/Core/Types/Vec/depends.x86-linux b/Source/Core/Types/Vec/depends.x86-linux new file mode 100644 index 0000000..e89ee79 --- /dev/null +++ b/Source/Core/Types/Vec/depends.x86-linux @@ -0,0 +1 @@ +../../../..//Build/x86-linux/Core/Types/Vec/Vec.o: Vec.cpp diff --git a/Source/Core/Types/Vec/dependsRaw.x86-linux b/Source/Core/Types/Vec/dependsRaw.x86-linux new file mode 100644 index 0000000..f009802 --- /dev/null +++ b/Source/Core/Types/Vec/dependsRaw.x86-linux @@ -0,0 +1 @@ +Vec.o: Vec.cpp diff --git a/Source/Core/Types/dependsRaw.x86-linux b/Source/Core/Types/dependsRaw.x86-linux new file mode 100644 index 0000000..e69de29 diff --git a/Source/DebugService.cpp b/Source/DebugService.cpp deleted file mode 100644 index 272262c..0000000 --- a/Source/DebugService.cpp +++ /dev/null @@ -1,910 +0,0 @@ -#include "DebugService.h" -#include "StandardParser.h" -#include "StreamString.h" -#include "BasicSocket.h" -#include "DebugBrokerWrapper.h" -#include "ObjectRegistryDatabase.h" -#include "ClassRegistryItem.h" -#include "ObjectBuilder.h" -#include "TypeConversion.h" -#include "HighResolutionTimer.h" -#include "ConfigurationDatabase.h" -#include "GAM.h" - -// Explicitly include target brokers for templating -#include "MemoryMapInputBroker.h" -#include "MemoryMapOutputBroker.h" -#include "MemoryMapSynchronisedInputBroker.h" -#include "MemoryMapSynchronisedOutputBroker.h" -#include "MemoryMapInterpolatedInputBroker.h" -#include "MemoryMapMultiBufferInputBroker.h" -#include "MemoryMapMultiBufferOutputBroker.h" -#include "MemoryMapSynchronisedMultiBufferInputBroker.h" -#include "MemoryMapSynchronisedMultiBufferOutputBroker.h" - -namespace MARTe { - -DebugService* DebugService::instance = NULL_PTR(DebugService*); -ErrorManagement::ErrorProcessFunctionType DebugService::originalLogCallback = NULL_PTR(ErrorManagement::ErrorProcessFunctionType); - -static void EscapeJson(const char8* src, StreamString &dst) { - if (src == NULL_PTR(const char8*)) return; - while (*src != '\0') { - if (*src == '"') dst += "\\\""; - else if (*src == '\\') dst += "\\\\"; - else if (*src == '\n') dst += "\\n"; - else if (*src == '\r') dst += "\\r"; - else if (*src == '\t') dst += "\\t"; - else dst += *src; - src++; - } -} - -CLASS_REGISTER(DebugService, "1.0") - -DebugService::DebugService() : - ReferenceContainer(), EmbeddedServiceMethodBinderI(), LoggerConsumerI(), - binderServer(this, ServiceBinder::ServerType), - binderStreamer(this, ServiceBinder::StreamerType), - binderLogStreamer(this, ServiceBinder::LogStreamerType), - threadService(binderServer), - streamerService(binderStreamer), - logStreamerService(binderLogStreamer) -{ - controlPort = 0; - streamPort = 8081; - logPort = 8082; - streamIP = "127.0.0.1"; - numberOfSignals = 0; - numberOfAliases = 0; - isServer = false; - suppressTimeoutLogs = true; - isPaused = false; - for (uint32 i=0; iClose(); - delete activeClients[i]; - } - if (activeLogClients[i] != NULL_PTR(BasicTCPSocket*)) { - activeLogClients[i]->Close(); - delete activeLogClients[i]; - } - } -} - -bool DebugService::Initialise(StructuredDataI & data) { - if (!ReferenceContainer::Initialise(data)) return false; - - if (!data.Read("ControlPort", controlPort)) { - (void)data.Read("TcpPort", controlPort); - } - - if (controlPort > 0) { - isServer = true; - instance = this; - } - - if (!data.Read("StreamPort", streamPort)) { - (void)data.Read("UdpPort", streamPort); - } - - if (!data.Read("LogPort", logPort)) { - (void)data.Read("TcpLogPort", logPort); - } - - (void)data.Read("StreamIP", streamIP); - - uint32 suppress = 1; - if (data.Read("SuppressTimeoutLogs", suppress)) { - suppressTimeoutLogs = (suppress == 1); - } - - if (isServer) { - if (ErrorManagement::errorMessageProcessFunction != &DebugService::LogCallback) { - originalLogCallback = ErrorManagement::errorMessageProcessFunction; - ErrorManagement::SetErrorProcessFunction(&DebugService::LogCallback); - } - - if (!traceBuffer.Init(1024 * 1024)) return false; - - (void)logEvent.Create(); - - PatchRegistry(); - - ConfigurationDatabase threadData; - threadData.Write("Timeout", (uint32)1000); - threadService.Initialise(threadData); - streamerService.Initialise(threadData); - logStreamerService.Initialise(threadData); - - if (!tcpServer.Open()) { - REPORT_ERROR(ErrorManagement::FatalError, "DebugService: Failed to open TCP Server Socket"); - return false; - } - if (!udpSocket.Open()) { - REPORT_ERROR(ErrorManagement::FatalError, "DebugService: Failed to open UDP Socket"); - return false; - } - if (!logServer.Open()) { - REPORT_ERROR(ErrorManagement::FatalError, "DebugService: Failed to open Log Server Socket"); - return false; - } - - if (threadService.Start() != ErrorManagement::NoError) { - REPORT_ERROR(ErrorManagement::FatalError, "DebugService: Failed to start Server thread"); - return false; - } - if (streamerService.Start() != ErrorManagement::NoError) { - REPORT_ERROR(ErrorManagement::FatalError, "DebugService: Failed to start Streamer thread"); - return false; - } - if (logStreamerService.Start() != ErrorManagement::NoError) { - REPORT_ERROR(ErrorManagement::FatalError, "DebugService: Failed to start LogStreamer thread"); - return false; - } - } - - return true; -} - -void PatchItemInternal(const char8* className, ObjectBuilder* builder) { - ClassRegistryDatabase *db = ClassRegistryDatabase::Instance(); - ClassRegistryItem *item = (ClassRegistryItem*)db->Find(className); - if (item != NULL_PTR(ClassRegistryItem*)) { - item->SetObjectBuilder(builder); - } -} - -void DebugService::PatchRegistry() { - static DebugMemoryMapInputBrokerBuilder b1; - PatchItemInternal("MemoryMapInputBroker", &b1); - static DebugMemoryMapOutputBrokerBuilder b2; - PatchItemInternal("MemoryMapOutputBroker", &b2); - static DebugMemoryMapSynchronisedInputBrokerBuilder b3; - PatchItemInternal("MemoryMapSynchronisedInputBroker", &b3); - static DebugMemoryMapSynchronisedOutputBrokerBuilder b4; - PatchItemInternal("MemoryMapSynchronisedOutputBroker", &b4); - static DebugMemoryMapInterpolatedInputBrokerBuilder b5; - PatchItemInternal("MemoryMapInterpolatedInputBroker", &b5); - static DebugMemoryMapMultiBufferInputBrokerBuilder b6; - PatchItemInternal("MemoryMapMultiBufferInputBroker", &b6); - static DebugMemoryMapMultiBufferOutputBrokerBuilder b7; - PatchItemInternal("MemoryMapMultiBufferOutputBroker", &b7); - static DebugMemoryMapSynchronisedMultiBufferInputBrokerBuilder b8; - PatchItemInternal("MemoryMapSynchronisedMultiBufferInputBroker", &b8); - static DebugMemoryMapSynchronisedMultiBufferOutputBrokerBuilder b9; - PatchItemInternal("MemoryMapSynchronisedMultiBufferOutputBroker", &b9); -} - -void DebugService::ProcessSignal(DebugSignalInfo* s, uint32 size) { - if (s != NULL_PTR(DebugSignalInfo*)) { - if (s->isForcing) { - MemoryOperationsHelper::Copy(s->memoryAddress, s->forcedValue, size); - } - if (s->isTracing) { - if (s->decimationFactor <= 1) { - (void)traceBuffer.Push(s->internalID, s->memoryAddress, size); - } - else { - if (s->decimationCounter == 0) { - (void)traceBuffer.Push(s->internalID, s->memoryAddress, size); - s->decimationCounter = s->decimationFactor - 1; - } - else { - s->decimationCounter--; - } - } - } - } -} - -DebugSignalInfo* DebugService::RegisterSignal(void* memoryAddress, TypeDescriptor type, const char8* name) { - mutex.FastLock(); - DebugSignalInfo* res = NULL_PTR(DebugSignalInfo*); - - uint32 sigIdx = 0xFFFFFFFF; - for(uint32 i=0; imemoryAddress = memoryAddress; - res->type = type; - res->name = name; - res->isTracing = false; - res->isForcing = false; - res->internalID = numberOfSignals; - res->decimationFactor = 1; - res->decimationCounter = 0; - numberOfSignals++; - } - - if (sigIdx != 0xFFFFFFFF && numberOfAliases < MAX_ALIASES) { - bool foundAlias = false; - for (uint32 i=0; iSize(); - for (uint32 i=0; iGet(i); - if (child.IsValid()) { - if (child.operator->() == &obj) { - path = child->GetName(); - return true; - } - ReferenceContainer *inner = dynamic_cast(child.operator->()); - if (inner) { - if (RecursiveGetFullObjectName(inner, obj, path)) { - StreamString prefix = child->GetName(); - prefix += "."; - prefix += path; - path = prefix; - return true; - } - } - } - } - return false; -} - -bool DebugService::GetFullObjectName(const Object &obj, StreamString &fullPath) { - fullPath = ""; - if (RecursiveGetFullObjectName(ObjectRegistryDatabase::Instance(), obj, fullPath)) { - StreamString abs = "Root."; - abs += fullPath; - fullPath = abs; - return true; - } - return false; -} - -ErrorManagement::ErrorType DebugService::Execute(ExecutionInfo & info) { - return ErrorManagement::FatalError; -} - -ErrorManagement::ErrorType DebugService::Server(ExecutionInfo & info) { - if (info.GetStage() == ExecutionInfo::TerminationStage) return ErrorManagement::NoError; - if (info.GetStage() == ExecutionInfo::StartupStage) { - serverThreadId = Threads::Id(); - if (!tcpServer.Listen(controlPort)) return ErrorManagement::FatalError; - return ErrorManagement::NoError; - } - - while (info.GetStage() == ExecutionInfo::MainStage) { - if (ErrorManagement::errorMessageProcessFunction != &DebugService::LogCallback) { - originalLogCallback = ErrorManagement::errorMessageProcessFunction; - ErrorManagement::SetErrorProcessFunction(&DebugService::LogCallback); - } - - BasicTCPSocket *newClient = tcpServer.WaitConnection(1); - if (newClient != NULL_PTR(BasicTCPSocket *)) { - clientsMutex.FastLock(); - bool added = false; - for (uint32 i=0; iClose(); - delete newClient; - } - } - - for (uint32 i=0; iRead(buffer, size, timeout); - - if (ok && size > 0) { - StreamString command; - command.Write(buffer, size); - HandleCommand(command, client); - } else if (!ok) { - if (!client->IsValid()) { - clientsMutex.FastLock(); - client->Close(); - delete client; - activeClients[i] = NULL_PTR(BasicTCPSocket*); - clientsMutex.FastUnLock(); - } - } - } - } - Sleep::MSec(10); - } - return ErrorManagement::NoError; -} - -ErrorManagement::ErrorType DebugService::LogStreamer(ExecutionInfo & info) { - if (info.GetStage() == ExecutionInfo::TerminationStage) return ErrorManagement::NoError; - if (info.GetStage() == ExecutionInfo::StartupStage) { - logStreamerThreadId = Threads::Id(); - if (!logServer.Listen(logPort)) return ErrorManagement::FatalError; - return ErrorManagement::NoError; - } - - while (info.GetStage() == ExecutionInfo::MainStage) { - BasicTCPSocket *newClient = logServer.WaitConnection(1); - if (newClient != NULL_PTR(BasicTCPSocket *)) { - logClientsMutex.FastLock(); - bool added = false; - for (uint32 i=0; iClose(); - delete newClient; - } - } - - bool hadData = false; - for (uint32 b=0; b<50; b++) { - if (logQueueRead == logQueueWrite) break; - hadData = true; - - uint32 idx = logQueueRead % LOG_QUEUE_SIZE; - LogEntry &entry = logQueue[idx]; - - StreamString level; - ErrorManagement::ErrorCodeToStream(entry.info.header.errorType, level); - - StreamString packet; - packet.Printf("LOG %s %s\n", level.Buffer(), entry.description); - uint32 size = packet.Size(); - - logClientsMutex.FastLock(); - for (uint32 j=0; jWrite(packet.Buffer(), s)) { - activeLogClients[j]->Close(); - delete activeLogClients[j]; - activeLogClients[j] = NULL_PTR(BasicTCPSocket*); - } - } - } - logClientsMutex.FastUnLock(); - logQueueRead++; - } - - if (!hadData) { - (void)logEvent.Wait(TimeoutType(100)); - logEvent.Reset(); - } else { - Sleep::MSec(1); - } - } - return ErrorManagement::NoError; -} - -ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo & info) { - if (info.GetStage() == ExecutionInfo::TerminationStage) return ErrorManagement::NoError; - if (info.GetStage() == ExecutionInfo::StartupStage) { - streamerThreadId = Threads::Id(); - return ErrorManagement::NoError; - } - - InternetHost dest(streamPort, streamIP.Buffer()); - udpSocket.SetDestination(dest); - - uint8 packetBuffer[4096]; - uint32 packetOffset = 0; - uint32 sequenceNumber = 0; - - while (info.GetStage() == ExecutionInfo::MainStage) { - uint32 id; - uint32 size; - uint8 sampleData[1024]; - bool hasData = false; - - while ((info.GetStage() == ExecutionInfo::MainStage) && traceBuffer.Pop(id, sampleData, size, 1024)) { - hasData = true; - if (packetOffset == 0) { - TraceHeader header; - header.magic = 0xDA7A57AD; - header.seq = sequenceNumber++; - header.timestamp = HighResolutionTimer::Counter(); - header.count = 0; - MemoryOperationsHelper::Copy(packetBuffer, &header, sizeof(TraceHeader)); - packetOffset = sizeof(TraceHeader); - } - - if (packetOffset + 8 + size > 1400) { - uint32 toWrite = packetOffset; - udpSocket.Write((char8*)packetBuffer, toWrite); - packetOffset = 0; - TraceHeader header; - header.magic = 0xDA7A57AD; - header.seq = sequenceNumber++; - header.timestamp = HighResolutionTimer::Counter(); - header.count = 0; - MemoryOperationsHelper::Copy(packetBuffer, &header, sizeof(TraceHeader)); - packetOffset = sizeof(TraceHeader); - } - - MemoryOperationsHelper::Copy(&packetBuffer[packetOffset], &id, 4); - MemoryOperationsHelper::Copy(&packetBuffer[packetOffset + 4], &size, 4); - MemoryOperationsHelper::Copy(&packetBuffer[packetOffset + 8], sampleData, size); - packetOffset += (8 + size); - TraceHeader *h = (TraceHeader*)packetBuffer; - h->count++; - } - - if (packetOffset > 0) { - uint32 toWrite = packetOffset; - udpSocket.Write((char8*)packetBuffer, toWrite); - packetOffset = 0; - } - if (!hasData) Sleep::MSec(1); - } - return ErrorManagement::NoError; -} - -static bool SuffixMatch(const char8* target, const char8* pattern) { - uint32 tLen = StringHelper::Length(target); - uint32 pLen = StringHelper::Length(pattern); - if (pLen > tLen) return false; - const char8* suffix = target + (tLen - pLen); - if (StringHelper::Compare(suffix, pattern) == 0) { - if (tLen == pLen || *(suffix - 1) == '.') return true; - } - return false; -} - -void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) { - StreamString token; - cmd.Seek(0); - char8 term; - const char8* delims = " \r\n"; - if (cmd.GetToken(token, delims, term)) { - if (token == "FORCE") { - StreamString name, val; - if (cmd.GetToken(name, delims, term) && cmd.GetToken(val, delims, term)) { - uint32 count = ForceSignal(name.Buffer(), val.Buffer()); - if (client) { - StreamString resp; resp.Printf("OK FORCE %u\n", count); - uint32 s = resp.Size(); client->Write(resp.Buffer(), s); - } - } - } - else if (token == "UNFORCE") { - StreamString name; - if (cmd.GetToken(name, delims, term)) { - uint32 count = UnforceSignal(name.Buffer()); - if (client) { - StreamString resp; resp.Printf("OK UNFORCE %u\n", count); - uint32 s = resp.Size(); client->Write(resp.Buffer(), s); - } - } - } - else if (token == "TRACE") { - StreamString name, state, decim; - if (cmd.GetToken(name, delims, term) && cmd.GetToken(state, delims, term)) { - bool enable = (state == "1"); - uint32 d = 1; - if (cmd.GetToken(decim, delims, term)) { - AnyType decimVal(UnsignedInteger32Bit, 0u, &d); - AnyType decimStr(CharString, 0u, decim.Buffer()); - TypeConvert(decimVal, decimStr); - } - uint32 count = TraceSignal(name.Buffer(), enable, d); - if (client) { - StreamString resp; resp.Printf("OK TRACE %u\n", count); - uint32 s = resp.Size(); client->Write(resp.Buffer(), s); - } - } - } - else if (token == "DISCOVER") Discover(client); - else if (token == "PAUSE") { - SetPaused(true); - if (client) { uint32 s = 3; client->Write("OK\n", s); } - } - else if (token == "RESUME") { - SetPaused(false); - if (client) { uint32 s = 3; client->Write("OK\n", s); } - } - else if (token == "TREE") { - StreamString json; - json = "{\"Name\": \"Root\", \"Class\": \"ObjectRegistryDatabase\", \"Children\": [\n"; - ExportTree(ObjectRegistryDatabase::Instance(), json); - json += "\n]}\nOK TREE\n"; - uint32 s = json.Size(); - client->Write(json.Buffer(), s); - } - else if (token == "INFO") { - StreamString path; - if (cmd.GetToken(path, delims, term)) InfoNode(path.Buffer(), client); - } - else if (token == "LS") { - StreamString path; - if (cmd.GetToken(path, delims, term)) ListNodes(path.Buffer(), client); - else ListNodes(NULL_PTR(const char8*), client); - } - else if (client) { - const char* msg = "ERROR: Unknown command\n"; - uint32 s = StringHelper::Length(msg); - client->Write(msg, s); - } - } -} - -void DebugService::InfoNode(const char8* path, BasicTCPSocket *client) { - if (!client) return; - Reference ref = ObjectRegistryDatabase::Instance()->Find(path); - StreamString json = "{"; - - if (ref.IsValid()) { - json += "\"Name\": \""; EscapeJson(ref->GetName(), json); - json += "\", \"Class\": \""; EscapeJson(ref->GetClassProperties()->GetName(), json); json += "\""; - - ConfigurationDatabase db; - if (ref->ExportData(db)) { - json += ", \"Config\": {"; - db.MoveToRoot(); - uint32 nChildren = db.GetNumberOfChildren(); - for (uint32 i=0; iWrite(json.Buffer(), s); -} - -uint32 DebugService::ExportTree(ReferenceContainer *container, StreamString &json) { - if (container == NULL_PTR(ReferenceContainer*)) return 0; - uint32 size = container->Size(); - uint32 validCount = 0; - for (uint32 i = 0u; i < size; i++) { - Reference child = container->Get(i); - if (child.IsValid()) { - if (validCount > 0u) json += ",\n"; - - StreamString nodeJson; - const char8* cname = child->GetName(); - if (cname == NULL_PTR(const char8*)) cname = "unnamed"; - const char8* clsname = child->GetClassProperties()->GetName(); - - nodeJson += "{\"Name\": \""; EscapeJson(cname, nodeJson); - nodeJson += "\", \"Class\": \""; EscapeJson(clsname, nodeJson); nodeJson += "\""; - - ReferenceContainer *inner = dynamic_cast(child.operator->()); - DataSourceI *ds = dynamic_cast(child.operator->()); - GAM *gam = dynamic_cast(child.operator->()); - - if ((inner != NULL_PTR(ReferenceContainer*)) || (ds != NULL_PTR(DataSourceI*)) || (gam != NULL_PTR(GAM*))) { - nodeJson += ", \"Children\": [\n"; - uint32 subCount = 0u; - if (inner != NULL_PTR(ReferenceContainer*)) subCount += ExportTree(inner, nodeJson); - - if (ds != NULL_PTR(DataSourceI*)) { - uint32 nSignals = ds->GetNumberOfSignals(); - for (uint32 j = 0u; j < nSignals; j++) { - if (subCount > 0u) nodeJson += ",\n"; - subCount++; - StreamString sname; (void)ds->GetSignalName(j, sname); - const char8* stype = TypeDescriptor::GetTypeNameFromTypeDescriptor(ds->GetSignalType(j)); - uint8 dims = 0u; (void)ds->GetSignalNumberOfDimensions(j, dims); - uint32 elems = 0u; (void)ds->GetSignalNumberOfElements(j, elems); - - nodeJson += "{\"Name\": \""; EscapeJson(sname.Buffer(), nodeJson); - nodeJson += "\", \"Class\": \"Signal\", \"Type\": \""; EscapeJson(stype ? stype : "Unknown", nodeJson); - nodeJson.Printf("\", \"Dimensions\": %d, \"Elements\": %u}", dims, elems); - } - } - - if (gam != NULL_PTR(GAM*)) { - uint32 nIn = gam->GetNumberOfInputSignals(); - for (uint32 j = 0u; j < nIn; j++) { - if (subCount > 0u) nodeJson += ",\n"; - subCount++; - StreamString sname; (void)gam->GetSignalName(InputSignals, j, sname); - const char8* stype = TypeDescriptor::GetTypeNameFromTypeDescriptor(gam->GetSignalType(InputSignals, j)); - uint32 dims = 0u; (void)gam->GetSignalNumberOfDimensions(InputSignals, j, dims); - uint32 elems = 0u; (void)gam->GetSignalNumberOfElements(InputSignals, j, elems); - - nodeJson += "{\"Name\": \"In."; EscapeJson(sname.Buffer(), nodeJson); - nodeJson += "\", \"Class\": \"InputSignal\", \"Type\": \""; EscapeJson(stype ? stype : "Unknown", nodeJson); - nodeJson.Printf("\", \"Dimensions\": %u, \"Elements\": %u}", dims, elems); - } - uint32 nOut = gam->GetNumberOfOutputSignals(); - for (uint32 j = 0u; j < nOut; j++) { - if (subCount > 0u) nodeJson += ",\n"; - subCount++; - StreamString sname; (void)gam->GetSignalName(OutputSignals, j, sname); - const char8* stype = TypeDescriptor::GetTypeNameFromTypeDescriptor(gam->GetSignalType(OutputSignals, j)); - uint32 dims = 0u; (void)gam->GetSignalNumberOfDimensions(OutputSignals, j, dims); - uint32 elems = 0u; (void)gam->GetSignalNumberOfElements(OutputSignals, j, elems); - - nodeJson += "{\"Name\": \"Out."; EscapeJson(sname.Buffer(), nodeJson); - nodeJson += "\", \"Class\": \"OutputSignal\", \"Type\": \""; EscapeJson(stype ? stype : "Unknown", nodeJson); - nodeJson.Printf("\", \"Dimensions\": %u, \"Elements\": %u}", dims, elems); - } - } - nodeJson += "\n]"; - } - nodeJson += "}"; - json += nodeJson; - validCount++; - } - } - return validCount; -} - -uint32 DebugService::ForceSignal(const char8* name, const char8* valueStr) { - mutex.FastLock(); - uint32 count = 0; - for (uint32 i = 0; i < numberOfAliases; i++) { - if (aliases[i].name == name || SuffixMatch(aliases[i].name.Buffer(), name)) { - DebugSignalInfo &s = signals[aliases[i].signalIndex]; - s.isForcing = true; - AnyType dest(s.type, 0u, s.forcedValue); - AnyType source(CharString, 0u, valueStr); - (void)TypeConvert(dest, source); - count++; - } - } - mutex.FastUnLock(); - return count; -} - -uint32 DebugService::UnforceSignal(const char8* name) { - mutex.FastLock(); - uint32 count = 0; - for (uint32 i = 0; i < numberOfAliases; i++) { - if (aliases[i].name == name || SuffixMatch(aliases[i].name.Buffer(), name)) { - signals[aliases[i].signalIndex].isForcing = false; - count++; - } - } - mutex.FastUnLock(); - return count; -} - -uint32 DebugService::TraceSignal(const char8* name, bool enable, uint32 decimation) { - mutex.FastLock(); - uint32 count = 0; - for (uint32 i = 0; i < numberOfAliases; i++) { - if (aliases[i].name == name || SuffixMatch(aliases[i].name.Buffer(), name)) { - DebugSignalInfo &s = signals[aliases[i].signalIndex]; - s.isTracing = enable; - s.decimationFactor = decimation; - s.decimationCounter = 0; - count++; - printf("[Debug] Tracing state for %s (ID: %u) set to %d\n", aliases[i].name.Buffer(), s.internalID, enable); - } - } - mutex.FastUnLock(); - return count; -} - -void DebugService::Discover(BasicTCPSocket *client) { - if (client) { - StreamString header = "{\n \"Signals\": [\n"; - uint32 s = header.Size(); - client->Write(header.Buffer(), s); - mutex.FastLock(); - for (uint32 i = 0; i < numberOfAliases; i++) { - StreamString line; - DebugSignalInfo &sig = signals[aliases[i].signalIndex]; - const char8* typeName = TypeDescriptor::GetTypeNameFromTypeDescriptor(sig.type); - if (typeName == NULL_PTR(const char8*)) typeName = "Unknown"; - line.Printf(" {\"name\": \"%s\", \"id\": %d, \"type\": \"%s\"}", aliases[i].name.Buffer(), sig.internalID, typeName); - if (i < numberOfAliases - 1) line += ","; - line += "\n"; - s = line.Size(); - client->Write(line.Buffer(), s); - } - mutex.FastUnLock(); - StreamString footer = " ]\n}\nOK DISCOVER\n"; - s = footer.Size(); - client->Write(footer.Buffer(), s); - } -} - -void DebugService::ListNodes(const char8* path, BasicTCPSocket *client) { - if (!client) return; - Reference ref; - if (path == NULL_PTR(const char8*) || StringHelper::Length(path) == 0 || StringHelper::Compare(path, "/") == 0) { - ref = ObjectRegistryDatabase::Instance(); - } else { - ref = ObjectRegistryDatabase::Instance()->Find(path); - } - - if (ref.IsValid()) { - StreamString header; - header.Printf("Nodes under %s:\n", path ? path : "/"); - uint32 s = header.Size(); - client->Write(header.Buffer(), s); - - ReferenceContainer *container = dynamic_cast(ref.operator->()); - if (container) { - uint32 size = container->Size(); - for (uint32 i=0; iGet(i); - if (child.IsValid()) { - StreamString line; - line.Printf(" %s [%s]\n", child->GetName(), child->GetClassProperties()->GetName()); - s = line.Size(); - client->Write(line.Buffer(), s); - } - } - } - - DataSourceI *ds = dynamic_cast(ref.operator->()); - if (ds) { - StreamString dsHeader = " Signals:\n"; - s = dsHeader.Size(); client->Write(dsHeader.Buffer(), s); - uint32 nSignals = ds->GetNumberOfSignals(); - for (uint32 i=0; iGetSignalName(i, sname); - TypeDescriptor stype = ds->GetSignalType(i); - const char8* stypeName = TypeDescriptor::GetTypeNameFromTypeDescriptor(stype); - line.Printf(" %s [%s]\n", sname.Buffer(), stypeName ? stypeName : "Unknown"); - s = line.Size(); client->Write(line.Buffer(), s); - } - } - - GAM *gam = dynamic_cast(ref.operator->()); - if (gam) { - uint32 nIn = gam->GetNumberOfInputSignals(); - uint32 nOut = gam->GetNumberOfOutputSignals(); - StreamString gamHeader; - gamHeader.Printf(" Input Signals (%d):\n", nIn); - s = gamHeader.Size(); client->Write(gamHeader.Buffer(), s); - for (uint32 i=0; iGetSignalName(InputSignals, i, sname); - line.Printf(" %s\n", sname.Buffer()); - s = line.Size(); client->Write(line.Buffer(), s); - } - gamHeader.SetSize(0); - gamHeader.Printf(" Output Signals (%d):\n", nOut); - s = gamHeader.Size(); client->Write(gamHeader.Buffer(), s); - for (uint32 i=0; iGetSignalName(OutputSignals, i, sname); - line.Printf(" %s\n", sname.Buffer()); - s = line.Size(); client->Write(line.Buffer(), s); - } - } - - const char* okMsg = "OK LS\n"; - s = StringHelper::Length(okMsg); - client->Write(okMsg, s); - } else { - const char* msg = "ERROR: Path not found\n"; - uint32 s = StringHelper::Length(msg); - client->Write(msg, s); - } -} - -void DebugService::ConsumeLogMessage(LoggerPage *logPage) { -} - -void DebugService::LogCallback(const ErrorManagement::ErrorInformation &errorInfo, const char8 * const errorDescription) { - ThreadIdentifier current = Threads::Id(); - if (instance != NULL_PTR(DebugService*)) { - StreamString levelStr; - ErrorManagement::ErrorCodeToStream(errorInfo.header.errorType, levelStr); - printf("[%s] %s\n", levelStr.Buffer(), errorDescription); - fflush(stdout); - - bool isWorkerThread = false; - if (instance->serverThreadId != InvalidThreadIdentifier && current == instance->serverThreadId) isWorkerThread = true; - if (instance->streamerThreadId != InvalidThreadIdentifier && current == instance->streamerThreadId) isWorkerThread = true; - if (instance->logStreamerThreadId != InvalidThreadIdentifier && current == instance->logStreamerThreadId) isWorkerThread = true; - - if (isWorkerThread) return; - - if (instance->suppressTimeoutLogs && StringHelper::SearchString(errorDescription, "Timeout expired in recv()") != NULL_PTR(const char8*)) return; - - LoggerPage tempPage; - tempPage.errorInfo = errorInfo; - StringHelper::Copy(tempPage.errorStrBuffer, errorDescription); - instance->InsertLogIntoQueue(&tempPage); - } - if (originalLogCallback) originalLogCallback(errorInfo, errorDescription); -} - -void DebugService::InsertLogIntoQueue(LoggerPage *logPage) { - uint32 next = (logQueueWrite + 1) % LOG_QUEUE_SIZE; - if (next != logQueueRead) { - LogEntry &entry = logQueue[logQueueWrite % LOG_QUEUE_SIZE]; - entry.info = logPage->errorInfo; - StringHelper::Copy(entry.description, logPage->errorStrBuffer); - logQueueWrite = next; - (void)logEvent.Post(); - } -} - -} diff --git a/TUTORIAL.md b/TUTORIAL.md new file mode 100644 index 0000000..4d48518 --- /dev/null +++ b/TUTORIAL.md @@ -0,0 +1,56 @@ +# Tutorial: High-Speed Observability with MARTe2 Debug GUI + +This guide will walk you through the process of tracing and forcing signals in a real-time MARTe2 application using the native Rust GUI client. + +## 1. Environment Setup + +### Launch the MARTe2 Application +First, start your application using the provided debug runner. This script launches the standard `MARTeApp.ex` while injecting the debugging layer: +```bash +./run_debug_app.sh +``` +*Note: You should see logs indicating the `DebugService` has started on port 8080 and `TcpLogger` on port 8082.* + +### Start the GUI Client +In a new terminal window, navigate to the client directory and run the GUI: +```bash +cd Tools/gui_client +cargo run --release +``` + +## 2. Exploring the Object Tree +Once connected, the left panel (**Application Tree**) displays the live hierarchy of your application. + +1. **Navigate to Data:** Expand `Root` -> `App` -> `Data`. +2. **Inspect Nodes:** Click the **ℹ Info** button next to any node (like `Timer` or `DDB`). +3. **View Metadata:** The bottom-left pane will display the full JSON configuration of that object, including its class, parameters, and signals. + +## 3. Real-Time Signal Tracing (Oscilloscope) +Tracing allows you to see signal values exactly as they exist in the real-time memory map. + +1. **Find a Signal:** In the tree, locate `Root.App.Data.Timer.Counter`. +2. **Activate Trace:** Click the **📈 Trace** button. +3. **Monitor the Scope:** The central **Oscilloscope** panel will begin plotting the signal. +4. **Verify Data Flow:** Check the **UDP Packets** counter in the top bar; it should be increasing rapidly, confirming high-speed data reception (Port 8081). +5. **Multi-Signal Trace:** You can click **📈 Trace** on other signals (like `Time`) to overlay multiple plots. + +## 4. Signal Forcing (Manual Override) +Forcing allows you to bypass the framework logic and manually set a signal's value in memory. + +1. **Locate Target:** Find a signal that is being written by a GAM, such as `Root.App.Data.DDB.Counter`. +2. **Open Force Dialog:** Click the **⚡ Force** button next to the signal. +3. **Inject Value:** In the popup dialog, enter a new value (e.g., `9999`) and click **Apply Force**. +4. **Observe Effect:** If you are tracing the same signal, the oscilloscope plot will immediately jump to and hold at `9999`. +5. **Release Control:** To stop forcing, click the **❌** button in the **Active Controls** (Right Panel) under "Forced Signals". The framework will resume writing its own values to that memory location. + +## 5. Advanced Controls + +### Global Pause +If you need to "freeze" the entire application to inspect a specific state: +- Click **⏸ Pause** in the top bar. The real-time threads will halt at the start of their next cycle. +- Click **▶ Resume** to restart the execution. + +### Log Terminal +The bottom panel displays every `REPORT_ERROR` event from the C++ framework, powered by the standalone `TcpLogger` service. +- **Regex Filter:** Type a keyword like `Timer` in the filter box to isolate relevant events. +- **Pause Logs:** Toggle **⏸ Pause Logs** to stop the scrolling view while data continues to be captured in the background. diff --git a/Test/Configurations/debug_test.cfg b/Test/Configurations/debug_test.cfg index da337d8..ff7b518 100644 --- a/Test/Configurations/debug_test.cfg +++ b/Test/Configurations/debug_test.cfg @@ -1,18 +1,3 @@ -+DebugService = { - Class = DebugService - ControlPort = 8080 - StreamPort = 8081 - StreamIP = "127.0.0.1" -} - -+LoggerService = { - Class = LoggerService - CPUs = 0x1 - +DebugConsumer = { - Class = DebugService - } -} - +App = { Class = RealTimeApplication +Functions = { @@ -23,7 +8,7 @@ Counter = { DataSource = Timer Type = uint32 - Frequency = 10 + Frequency = 1000 } Time = { DataSource = Timer @@ -32,22 +17,100 @@ } OutputSignals = { Counter = { - DataSource = DDB + DataSource = SyncDB Type = uint32 } Time = { - DataSource = Logger + DataSource = DDB Type = uint32 } } } + +CGAM = { + Class = ConstantGAM + OutputSignals = { + Test = { + DataSource = DDB2 + Type = float32 + Default = 0.123 + } + } + } + +GAM2 = { + Class = IOGAM + InputSignals = { + Counter = { + DataSource = TimerSlow + Frequency = 1 + } + Time = { + DataSource = TimerSlow + } + Test = { + DataSource = DDB2 + Type = float32 + } + } + OutputSignals = { + Counter = { + Type = uint32 + DataSource = Logger + } + Time = { + Type = uint32 + DataSource = Logger + } + ConstOut = { + DataSource = Logger + Type = float32 + } + } + } + +GAM3 = { + Class = IOGAM + InputSignals = { + Counter = { + Frequency = 1 + Samples = 100 + Type = uint32 + DataSource = SyncDB + } + } + OutputSignals = { + Counter = { + DataSource = DDB3 + NumberOfElements = 100 + Type = uint32 + } + } + + } } +Data = { Class = ReferenceContainer DefaultDataSource = DDB + +SyncDB = { + Class = RealTimeThreadSynchronisation + Timeout = 200 + Signals = { + Counter = { + Type = uint32 + } + } + } +Timer = { Class = LinuxTimer - SleepTime = 1000000 // 1 second + Signals = { + Counter = { + Type = uint32 + } + Time = { + Type = uint32 + } + } + } + +TimerSlow = { + Class = LinuxTimer Signals = { Counter = { Type = uint32 @@ -77,6 +140,14 @@ } } } + +DDB2 = { + AllowNoProducer = 1 + Class = GAMDataSource + } + +DDB3 = { + AllowNoProducer = 1 + Class = GAMDataSource + } +DAMS = { Class = TimingDataSource } @@ -91,6 +162,14 @@ Class = RealTimeThread Functions = {GAM1} } + +Thread2 = { + Class = RealTimeThread + Functions = {GAM2 CGAM} + } + +Thread3 = { + Class = RealTimeThread + Functions = {GAM3} + } } } } @@ -99,3 +178,12 @@ TimingDataSource = DAMS } } + ++DebugService = { + Class = DebugService + ControlPort = 8080 + UdpPort = 8081 + LogPort = 8082 + StreamIP = "127.0.0.1" +} + diff --git a/Test/Integration/CMakeLists.txt b/Test/Integration/CMakeLists.txt deleted file mode 100644 index 4a7a023..0000000 --- a/Test/Integration/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -add_executable(IntegrationTest main.cpp) -target_link_libraries(IntegrationTest marte_dev ${MARTe2_LIB}) - -add_executable(TraceTest TraceTest.cpp) -target_link_libraries(TraceTest marte_dev ${MARTe2_LIB}) - -add_executable(ValidationTest ValidationTest.cpp) -target_link_libraries(ValidationTest marte_dev ${MARTe2_LIB} ${IOGAM_LIB} ${LinuxTimer_LIB}) diff --git a/Test/Integration/ConfigCommandTest.cpp b/Test/Integration/ConfigCommandTest.cpp new file mode 100644 index 0000000..20f7621 --- /dev/null +++ b/Test/Integration/ConfigCommandTest.cpp @@ -0,0 +1,161 @@ +#include "BasicTCPSocket.h" +#include "DebugService.h" +#include "ObjectRegistryDatabase.h" +#include "StandardParser.h" +#include "StreamString.h" +#include "GlobalObjectsDatabase.h" +#include "RealTimeApplication.h" +#include +#include + +using namespace MARTe; + +const char8 * const config_command_text = +"DebugService = {" +" Class = DebugService " +" ControlPort = 8100 " +" UdpPort = 8101 " +" StreamIP = \"127.0.0.1\" " +" MyCustomField = \"HelloConfig\" " +"}" +"App = {" +" Class = RealTimeApplication " +" +Functions = {" +" Class = ReferenceContainer " +" +GAM1 = {" +" Class = IOGAM " +" CustomGAMField = \"GAMValue\" " +" InputSignals = {" +" Counter = { DataSource = Timer Type = uint32 Frequency = 1000 PVName = \"PROC:VAR:1\" }" +" Time = { DataSource = Timer Type = uint32 }" +" }" +" OutputSignals = {" +" Counter = { DataSource = DDB Type = uint32 }" +" Time = { DataSource = DDB Type = uint32 }" +" }" +" }" +" }" +" +Data = {" +" Class = ReferenceContainer " +" +Timer = { Class = LinuxTimer SleepTime = 1000 Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }" +" +DDB = { Class = GAMDataSource Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }" +" +DAMS = { Class = TimingDataSource }" +" }" +" +States = {" +" Class = ReferenceContainer " +" +State1 = { Class = RealTimeState +Threads = { Class = ReferenceContainer +Thread1 = { Class = RealTimeThread Functions = {GAM1} } } }" +" }" +" +Scheduler = { Class = GAMScheduler TimingDataSource = DAMS }" +"}"; + +static bool SendCommandAndGetReply(uint16 port, const char8* cmd, StreamString &reply) { + BasicTCPSocket client; + if (!client.Open()) return false; + if (!client.Connect("127.0.0.1", port)) return false; + + uint32 s = StringHelper::Length(cmd); + if (!client.Write(cmd, s)) return false; + + char buffer[4096]; + uint32 size = 4096; + TimeoutType timeout(2000000); // 2s + if (client.Read(buffer, size, timeout)) { + reply.Write(buffer, size); + client.Close(); + return true; + } + client.Close(); + return false; +} + +void TestConfigCommands() { + printf("--- MARTe2 Config & Metadata Enrichment Test ---\n"); + + ObjectRegistryDatabase::Instance()->Purge(); + + ConfigurationDatabase cdb; + StreamString ss = config_command_text; + ss.Seek(0); + StandardParser parser(ss, cdb); + assert(parser.Parse()); + + cdb.MoveToRoot(); + uint32 n = cdb.GetNumberOfChildren(); + for (uint32 i=0; iGetStandardHeap()); + ref->SetName(name); + assert(ref->Initialise(child)); + ObjectRegistryDatabase::Instance()->Insert(ref); + } + + printf("Application and DebugService (port 8100) initialised.\n"); + + // Start the application to trigger broker execution and signal registration + ReferenceT app = ObjectRegistryDatabase::Instance()->Find("App"); + assert(app.IsValid()); + assert(app->ConfigureApplication()); + assert(app->PrepareNextState("State1") == ErrorManagement::NoError); + assert(app->StartNextStateExecution() == ErrorManagement::NoError); + printf("Application started (for signal registration).\n"); + Sleep::MSec(500); // Wait for some cycles + + ReferenceT service = ObjectRegistryDatabase::Instance()->Find("DebugService"); + if (service.IsValid()) { + service->SetFullConfig(cdb); + } + + Sleep::MSec(1000); + + // 1. Test CONFIG command + { + printf("Testing CONFIG command...\n"); + StreamString reply; + assert(SendCommandAndGetReply(8100, "CONFIG\n", reply)); + printf("\n%s\n", reply.Buffer()); + // Verify it contains some key parts of the config + assert(StringHelper::SearchString(reply.Buffer(), "MyCustomField") != NULL_PTR(const char8*)); + assert(StringHelper::SearchString(reply.Buffer(), "HelloConfig") != NULL_PTR(const char8*)); + assert(StringHelper::SearchString(reply.Buffer(), "PROC:VAR:1") != NULL_PTR(const char8*)); + assert(StringHelper::SearchString(reply.Buffer(), "OK CONFIG") != NULL_PTR(const char8*)); + printf("SUCCESS: CONFIG command validated.\n"); + } + + // 2. Test INFO on object with enrichment + { + printf("Testing INFO on App.Functions.GAM1...\n"); + StreamString reply; + assert(SendCommandAndGetReply(8100, "INFO App.Functions.GAM1\n", reply)); + // Check standard MARTe fields (Name, Class) + assert(StringHelper::SearchString(reply.Buffer(), "\"Name\": \"GAM1\"") != NULL_PTR(const char8*)); + // Check enriched fields from fullConfig + assert(StringHelper::SearchString(reply.Buffer(), "\"CustomGAMField\": \"GAMValue\"") != NULL_PTR(const char8*)); + assert(StringHelper::SearchString(reply.Buffer(), "OK INFO") != NULL_PTR(const char8*)); + printf("SUCCESS: Object metadata enrichment validated.\n"); + } + + // 3. Test INFO on signal with enrichment + { + printf("Testing INFO on App.Functions.GAM1.In.Counter...\n"); + StreamString reply; + assert(SendCommandAndGetReply(8100, "INFO App.Functions.GAM1.In.Counter\n", reply)); + + // Check enriched fields from signal configuration + assert(StringHelper::SearchString(reply.Buffer(), "\"Frequency\": \"1000\"") != NULL_PTR(const char8*)); + assert(StringHelper::SearchString(reply.Buffer(), "\"PVName\": \"PROC:VAR:1\"") != NULL_PTR(const char8*)); + assert(StringHelper::SearchString(reply.Buffer(), "OK INFO") != NULL_PTR(const char8*)); + printf("SUCCESS: Signal metadata enrichment validated.\n"); + } + + if (app.IsValid()) { + app->StopCurrentStateExecution(); + } + + ObjectRegistryDatabase::Instance()->Purge(); +} diff --git a/Test/Integration/IntegrationTests.cpp b/Test/Integration/IntegrationTests.cpp new file mode 100644 index 0000000..4e53b27 --- /dev/null +++ b/Test/Integration/IntegrationTests.cpp @@ -0,0 +1,252 @@ +#include "ClassRegistryDatabase.h" +#include "ConfigurationDatabase.h" +#include "DebugService.h" +#include "ObjectRegistryDatabase.h" +#include "ErrorManagement.h" +#include "BasicTCPSocket.h" +#include "BasicUDPSocket.h" +#include "RealTimeApplication.h" +#include "StandardParser.h" +#include "TestCommon.h" +#include + +using namespace MARTe; + +#include +#include + +void timeout_handler(int sig) { + printf("Test timed out!\n"); + _exit(1); +} + +void ErrorProcessFunction(const MARTe::ErrorManagement::ErrorInformation &errorInfo, const char8 * const errorDescription) { + printf("[MARTe Error] %s: %s\n", errorInfo.className, errorDescription); +} + +// Forward declarations of other tests +void TestSchedulerControl(); +void TestFullTracePipeline(); +void RunValidationTest(); +void TestConfigCommands(); +void TestGAMSignalTracing(); +void TestTreeCommand(); + +int main() { + signal(SIGALRM, timeout_handler); + alarm(180); + + MARTe::ErrorManagement::SetErrorProcessFunction(&ErrorProcessFunction); + + printf("MARTe2 Debug Suite Integration Tests\n"); + + printf("\n--- Test 1: Registry Patching ---\n"); + { + ObjectRegistryDatabase::Instance()->Purge(); + DebugService service; + ConfigurationDatabase serviceData; + serviceData.Write("ControlPort", (uint32)9090); + service.Initialise(serviceData); + printf("DebugService initialized and Registry Patched.\n"); + + ClassRegistryItem *item = + ClassRegistryDatabase::Instance()->Find("MemoryMapInputBroker"); + if (item != NULL_PTR(ClassRegistryItem *)) { + Object *obj = item->GetObjectBuilder()->Build( + GlobalObjectsDatabase::Instance()->GetStandardHeap()); + if (obj != NULL_PTR(Object *)) { + printf("Instantiated Broker Class: %s\n", + obj->GetClassProperties()->GetName()); + printf("Success: Broker patched and instantiated.\n"); + } else { + printf("Failed to build broker\n"); + } + } else { + printf("MemoryMapInputBroker not found in registry\n"); + } + } + Sleep::MSec(1000); + + printf("\n--- Test 2: Full Trace Pipeline ---\n"); + TestFullTracePipeline(); + Sleep::MSec(1000); + + printf("\n--- Test 3: Scheduler Control ---\n"); + TestSchedulerControl(); + Sleep::MSec(1000); + + printf("\n--- Test 4: 1kHz Lossless Trace Validation ---\n"); + RunValidationTest(); + Sleep::MSec(1000); + + printf("\n--- Test 5: Config & Metadata Enrichment ---\n"); + TestConfigCommands(); + Sleep::MSec(1000); + + printf("\n--- Test 6: TREE Command Enhancement ---\n"); + TestTreeCommand(); + Sleep::MSec(1000); + + printf("\n--- Test 7: Custom MARTe Message (MSG) ---\n"); + TestMessageCommand(); + Sleep::MSec(1000); + + printf("\nAll Integration Tests Finished.\n"); + + return 0; +} + +// --- Test Implementation --- + +void TestGAMSignalTracing() { + printf("--- Test: GAM Signal Tracing Issue ---\n"); + + ObjectRegistryDatabase::Instance()->Purge(); + + ConfigurationDatabase cdb; + StreamString ss = debug_test_config; + ss.Seek(0); + StandardParser parser(ss, cdb); + if (!parser.Parse()) { + printf("ERROR: Failed to parse config\n"); + return; + } + + cdb.MoveToRoot(); + uint32 n = cdb.GetNumberOfChildren(); + for (uint32 i=0; iGetStandardHeap()); + if (!ref.IsValid()) { + printf("ERROR: Could not create object %s of class %s\n", name, className.Buffer()); + continue; + } + ref->SetName(name); + if (!ref->Initialise(child)) { + printf("ERROR: Failed to initialise object %s\n", name); + continue; + } + ObjectRegistryDatabase::Instance()->Insert(ref); + } + + ReferenceT service = ObjectRegistryDatabase::Instance()->Find("DebugService"); + if (!service.IsValid()) { + printf("ERROR: DebugService not found\n"); + return; + } + service->SetFullConfig(cdb); + + ReferenceT app = ObjectRegistryDatabase::Instance()->Find("App"); + if (!app.IsValid()) { + printf("ERROR: App not found\n"); + return; + } + + if (!app->ConfigureApplication()) { + printf("ERROR: ConfigureApplication failed.\n"); + return; + } + + if (app->PrepareNextState("State1") != ErrorManagement::NoError) { + printf("ERROR: PrepareNextState failed.\n"); + return; + } + + if (app->StartNextStateExecution() != ErrorManagement::NoError) { + printf("ERROR: StartNextStateExecution failed.\n"); + return; + } + + printf("Application started.\n"); + Sleep::MSec(1000); + + // Step 1: Discover signals + { + StreamString reply; + if (SendCommandGAM(8095, "DISCOVER\n", reply)) { + printf("DISCOVER response received (len=%llu)\n", reply.Size()); + } else { + printf("ERROR: DISCOVER failed\n"); + } + } + Sleep::MSec(500); + + // Step 2: Trace a DataSource signal (Timer.Counter) + printf("\n--- Step 1: Trace DataSource signal (Timer.Counter) ---\n"); + { + StreamString reply; + if (SendCommandGAM(8095, "TRACE App.Data.Timer.Counter 1\n", reply)) { + printf("TRACE response: %s", reply.Buffer()); + } else { + printf("ERROR: TRACE failed\n"); + } + } + Sleep::MSec(500); + + // Step 3: Trace a GAM input signal (GAM1.In.Counter) + printf("\n--- Step 2: Trace GAM input signal (GAM1.In.Counter) ---\n"); + { + StreamString reply; + if (SendCommandGAM(8095, "TRACE App.Functions.GAM1.In.Counter 1\n", reply)) { + printf("TRACE response: %s", reply.Buffer()); + } else { + printf("ERROR: TRACE failed\n"); + } + } + Sleep::MSec(500); + + // Step 4: Try to trace another DataSource signal (TimerSlow.Counter) + printf("\n--- Step 3: Try to trace another signal (TimerSlow.Counter) ---\n"); + { + StreamString reply; + if (SendCommandGAM(8095, "TRACE App.Data.TimerSlow.Counter 1\n", reply)) { + printf("TRACE response: %s", reply.Buffer()); + } else { + printf("ERROR: TRACE failed\n"); + } + } + Sleep::MSec(500); + + // Step 5: Check if we can still trace more signals + printf("\n--- Step 4: Try to trace Logger.Counter ---\n"); + { + StreamString reply; + if (SendCommandGAM(8095, "TRACE App.Data.Logger.Counter 1\n", reply)) { + printf("TRACE response: %s", reply.Buffer()); + } else { + printf("ERROR: TRACE failed\n"); + } + } + Sleep::MSec(500); + + // Verify UDP is still receiving data + BasicUDPSocket listener; + listener.Open(); + listener.Listen(8096); + + char buffer[1024]; + uint32 size = 1024; + TimeoutType timeout(1000); + int packetCount = 0; + while (listener.Read(buffer, size, timeout)) { + packetCount++; + size = 1024; + } + + printf("\n--- Results ---\n"); + if (packetCount > 0) { + printf("SUCCESS: Received %d UDP packets.\n", packetCount); + } else { + printf("FAILURE: No UDP packets received. Possible deadlock or crash.\n"); + } + + app->StopCurrentStateExecution(); +} diff --git a/Test/Integration/Makefile.gcc b/Test/Integration/Makefile.gcc new file mode 100644 index 0000000..ab19097 --- /dev/null +++ b/Test/Integration/Makefile.gcc @@ -0,0 +1 @@ +include Makefile.inc diff --git a/Test/Integration/Makefile.inc b/Test/Integration/Makefile.inc new file mode 100644 index 0000000..32bf5da --- /dev/null +++ b/Test/Integration/Makefile.inc @@ -0,0 +1,44 @@ +OBJSX = SchedulerTest.x TraceTest.x ValidationTest.x ConfigCommandTest.x TreeCommandTest.x MessageCommandTest.x TestCommon.x + +PACKAGE = Test/Integration + +ROOT_DIR = ../.. + +MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults + +include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET) + +INCLUDES += -I$(ROOT_DIR)/Source/Core/Types/Result +INCLUDES += -I$(ROOT_DIR)/Source/Core/Types/Vec +INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/DebugService +INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/TCPLogger + +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Logger +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L6App +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4LoggerService +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L5GAMs +INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams +INCLUDES += -I$(MARTe2_Components_DIR)/Source/Components/GAMs/IOGAM +INCLUDES += -I$(MARTe2_Components_DIR)/Source/Components/DataSources/LinuxTimer + +LIBRARIES += -L$(MARTe2_DIR)/Build/$(TARGET)/Core -lMARTe2 +LIBRARIES += -L$(MARTe2_Components_DIR)/Build/$(TARGET)/Components/DataSources/LinuxTimer -lLinuxTimer +LIBRARIES += -L$(MARTe2_Components_DIR)/Build/$(TARGET)/Components/GAMs/IOGAM -lIOGAM +LIBRARIES += -L$(ROOT_DIR)/Build/$(TARGET)/Components/Interfaces/DebugService -lDebugService +LIBRARIES += -L$(ROOT_DIR)/Build/$(TARGET)/Components/Interfaces/TCPLogger -lTcpLogger + +all: $(OBJS) $(BUILD_DIR)/IntegrationTests$(EXEEXT) + echo $(OBJS) + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) diff --git a/Test/Integration/MessageCommandTest.cpp b/Test/Integration/MessageCommandTest.cpp new file mode 100644 index 0000000..8d5ba76 --- /dev/null +++ b/Test/Integration/MessageCommandTest.cpp @@ -0,0 +1,72 @@ +#include "TestCommon.h" +#include "ObjectRegistryDatabase.h" +#include "DebugService.h" +#include "StandardParser.h" +#include "GlobalObjectsDatabase.h" +#include + +using namespace MARTe; + +namespace MARTe { + +void TestMessageCommand() { + printf("--- Test: Custom MARTe Message (MSG) ---\n"); + + ObjectRegistryDatabase::Instance()->Purge(); + Sleep::MSec(1000); + + ConfigurationDatabase cdb; + const char8 * const msg_test_config = + "DebugService = {" + " Class = DebugService " + " ControlPort = 8120 " + " UdpPort = 8121 " + " StreamIP = \"127.0.0.1\" " + "}"; + + StreamString ss = msg_test_config; + ss.Seek(0); + StandardParser parser(ss, cdb); + if (!parser.Parse()) { + printf("ERROR: Failed to parse config\n"); + return; + } + + // Initialize Service + ReferenceT service("DebugService", GlobalObjectsDatabase::Instance()->GetStandardHeap()); + service->SetName("DebugService"); + + cdb.MoveToRoot(); + if (cdb.MoveRelative("DebugService")) { + if (!service->Initialise(cdb)) { + printf("ERROR: Failed to initialize DebugService\n"); + return; + } + } + ObjectRegistryDatabase::Instance()->Insert(service); + + if (ObjectRegistryDatabase::Instance()->Find("DebugService").IsValid()) { + printf("DebugService successfully registered in ORD.\n"); + } else { + printf("ERROR: DebugService NOT found in ORD.\n"); + } + + printf("Service initialized on port 8120.\n"); + Sleep::MSec(500); + + StreamString reply; + if (SendCommandGAM(8120, "MSG DebugService UnknownFunc 0 Key1=Val1\\nKey2=Val2\n", reply)) { + printf("MSG response received: %s", reply.Buffer()); + if (StringHelper::SearchString(reply.Buffer(), "OK MSG") != NULL_PTR(const char8 *)) { + printf("SUCCESS: Asynchronous message dispatched correctly.\n"); + } else { + printf("FAILURE: MSG command returned error.\n"); + } + } else { + printf("ERROR: MSG command communication failed\n"); + } + + ObjectRegistryDatabase::Instance()->Purge(); +} + +} // namespace MARTe diff --git a/Test/Integration/SchedulerTest.cpp b/Test/Integration/SchedulerTest.cpp new file mode 100644 index 0000000..287c54c --- /dev/null +++ b/Test/Integration/SchedulerTest.cpp @@ -0,0 +1,253 @@ +#include "BasicTCPSocket.h" +#include "BasicUDPSocket.h" +#include "DebugService.h" +#include "ObjectRegistryDatabase.h" +#include "RealTimeApplication.h" +#include "StandardParser.h" +#include "StreamString.h" +#include "GlobalObjectsDatabase.h" +#include +#include + +using namespace MARTe; + +const char8 * const scheduler_config_text = +"DebugService = {" +" Class = DebugService " +" ControlPort = 8098 " +" UdpPort = 8099 " +" StreamIP = \"127.0.0.1\" " +"}" +"App = {" +" Class = RealTimeApplication " +" +Functions = {" +" Class = ReferenceContainer " +" +GAM1 = {" +" Class = IOGAM " +" InputSignals = {" +" Counter = { DataSource = Timer Type = uint32 Frequency = 1000 }" +" Time = { DataSource = Timer Type = uint32 }" +" }" +" OutputSignals = {" +" Counter = { DataSource = DDB Type = uint32 }" +" Time = { DataSource = DDB Type = uint32 }" +" }" +" }" +" }" +" +Data = {" +" Class = ReferenceContainer " +" DefaultDataSource = DDB " +" +Timer = { Class = LinuxTimer SleepTime = 1000 Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }" +" +DDB = { Class = GAMDataSource Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }" +" +DAMS = { Class = TimingDataSource }" +" }" +" +States = {" +" Class = ReferenceContainer " +" +State1 = { Class = RealTimeState +Threads = { Class = ReferenceContainer +Thread1 = { Class = RealTimeThread Functions = {GAM1} } } }" +" }" +" +Scheduler = { Class = GAMScheduler TimingDataSource = DAMS }" +"}"; + +void TestSchedulerControl() { + printf("--- MARTe2 Scheduler Control Test ---\n"); + + ObjectRegistryDatabase::Instance()->Purge(); + + ConfigurationDatabase cdb; + StreamString ss = scheduler_config_text; + ss.Seek(0); + StandardParser parser(ss, cdb); + if (!parser.Parse()) { + printf("ERROR: Failed to parse configuration\n"); + return; + } + + cdb.MoveToRoot(); + uint32 n = cdb.GetNumberOfChildren(); + for (uint32 i=0; iGetStandardHeap()); + if (!ref.IsValid()) { + printf("ERROR: Could not create object %s of class %s\n", name, className.Buffer()); + continue; + } + ref->SetName(name); + if (!ref->Initialise(child)) { + printf("ERROR: Failed to initialise object %s\n", name); + continue; + } + ObjectRegistryDatabase::Instance()->Insert(ref); + } + + ReferenceT service = + ObjectRegistryDatabase::Instance()->Find("DebugService"); + if (!service.IsValid()) { + printf("ERROR: DebugService not found in registry\n"); + return; + } + service->SetFullConfig(cdb); + + ReferenceT app = + ObjectRegistryDatabase::Instance()->Find("App"); + if (!app.IsValid()) { + printf("ERROR: App not found in registry\n"); + return; + } + + if (!app->ConfigureApplication()) { + printf("ERROR: ConfigureApplication failed.\n"); + return; + } + + if (app->PrepareNextState("State1") != ErrorManagement::NoError) { + printf("ERROR: Failed to prepare State1\n"); + return; + } + + if (app->StartNextStateExecution() != ErrorManagement::NoError) { + printf("ERROR: Failed to start execution\n"); + return; + } + + printf("Application started. Waiting for cycles...\n"); + Sleep::MSec(2000); + + // Enable Trace First - with retry logic + { + bool connected = false; + for (int retry=0; retry<10 && !connected; retry++) { + BasicTCPSocket tClient; + if (tClient.Open()) { + if (tClient.Connect("127.0.0.1", 8098)) { + connected = true; + const char *cmd = "TRACE App.Data.Timer.Counter 1\n"; + uint32 s = StringHelper::Length(cmd); + tClient.Write(cmd, s); + tClient.Close(); + } else { + printf("[SchedulerTest] Connect failed (retry %d)\n", retry); + Sleep::MSec(500); + } + } else { + printf("[SchedulerTest] Open failed (retry %d)\n", retry); + Sleep::MSec(500); + } + } + + if (!connected) { + printf("WARNING: Could not connect to DebugService to enable trace.\n"); + } + } + + BasicUDPSocket listener; + listener.Open(); + listener.Listen(8099); + + // Read current value + uint32 valBeforePause = 0; + char buffer[2048]; + uint32 size = 2048; + TimeoutType timeout(1000); + if (listener.Read(buffer, size, timeout)) { + // [Header][ID][Size][Value] + valBeforePause = *(uint32 *)(&buffer[sizeof(TraceHeader) + 16]); + printf("Value before/at pause: %u\n", valBeforePause); + } else { + printf("WARNING: No data received before pause.\n"); + } + + // Send PAUSE + printf("Sending PAUSE command...\n"); + { + bool connected = false; + for (int retry=0; retry<10 && !connected; retry++) { + BasicTCPSocket client; + if (client.Open()) { + if (client.Connect("127.0.0.1", 8098)) { + connected = true; + const char *cmd = "PAUSE\n"; + uint32 s = StringHelper::Length(cmd); + client.Write(cmd, s); + client.Close(); + } else { + Sleep::MSec(200); + } + } else { + Sleep::MSec(200); + } + } + + if (!connected) { + printf("ERROR: Could not connect to DebugService to send PAUSE.\n"); + } + } + + Sleep::MSec(2000); // Wait 2 seconds + + // Read again - should be same or very close if paused + uint32 valAfterWait = 0; + size = 2048; // Reset size + while (listener.Read(buffer, size, TimeoutType(100))) { + valAfterWait = *(uint32 *)(&buffer[sizeof(TraceHeader) + 16]); + size = 2048; + } + + printf("Value after 2s wait (drained): %u\n", valAfterWait); + + // Check if truly paused + if (valAfterWait > valBeforePause + 10) { + printf( + "FAILURE: Counter increased significantly while paused! (%u -> %u)\n", + valBeforePause, valAfterWait); + } else { + printf("SUCCESS: Counter held steady (or close) during pause.\n"); + } + + // Resume + printf("Sending RESUME command...\n"); + { + bool connected = false; + for (int retry=0; retry<10 && !connected; retry++) { + BasicTCPSocket rClient; + if (rClient.Open()) { + if (rClient.Connect("127.0.0.1", 8098)) { + connected = true; + const char *cmd = "RESUME\n"; + uint32 s = StringHelper::Length(cmd); + rClient.Write(cmd, s); + rClient.Close(); + } else { + Sleep::MSec(200); + } + } else { + Sleep::MSec(200); + } + } + } + + Sleep::MSec(1000); + + // Check if increasing + uint32 valAfterResume = 0; + size = 2048; + if (listener.Read(buffer, size, timeout)) { + valAfterResume = *(uint32 *)(&buffer[sizeof(TraceHeader) + 16]); + printf("Value after resume: %u\n", valAfterResume); + } + + if (valAfterResume > valAfterWait) { + printf("SUCCESS: Execution resumed.\n"); + } else { + printf("FAILURE: Execution did not resume.\n"); + } + + app->StopCurrentStateExecution(); +} diff --git a/Test/Integration/TestCommon.cpp b/Test/Integration/TestCommon.cpp new file mode 100644 index 0000000..5a787ec --- /dev/null +++ b/Test/Integration/TestCommon.cpp @@ -0,0 +1,74 @@ +#include "TestCommon.h" +#include "BasicTCPSocket.h" +#include "StringHelper.h" +#include "TimeoutType.h" + +namespace MARTe { + +const char8 * const debug_test_config = +"DebugService = {" +" Class = DebugService " +" ControlPort = 8095 " +" UdpPort = 8096 " +" StreamIP = \"127.0.0.1\" " +"}" +"App = {" +" Class = RealTimeApplication " +" +Functions = {" +" Class = ReferenceContainer " +" +GAM1 = {" +" Class = IOGAM " +" InputSignals = {" +" Counter = { DataSource = Timer Type = uint32 Frequency = 1000 }" +" }" +" OutputSignals = {" +" Counter = { DataSource = DDB Type = uint32 }" +" }" +" }" +" +GAM2 = {" +" Class = IOGAM " +" InputSignals = {" +" Counter = { DataSource = TimerSlow Type = uint32 Frequency = 10 }" +" }" +" OutputSignals = {" +" Counter = { DataSource = Logger Type = uint32 }" +" }" +" }" +" }" +" +Data = {" +" Class = ReferenceContainer " +" DefaultDataSource = DDB " +" +Timer = { Class = LinuxTimer SleepTime = 1000 Signals = { Counter = { Type = uint32 } } }" +" +TimerSlow = { Class = LinuxTimer SleepTime = 100000 Signals = { Counter = { Type = uint32 } } }" +" +Logger = { Class = LoggerDataSource Signals = { Counter = { Type = uint32 } } }" +" +DDB = { Class = GAMDataSource Signals = { Counter = { Type = uint32 } } }" +" +DAMS = { Class = TimingDataSource }" +" }" +" +States = {" +" Class = ReferenceContainer " +" +State1 = { Class = RealTimeState +Threads = { Class = ReferenceContainer +Thread1 = { Class = RealTimeThread Functions = {GAM1 GAM2} } } }" +" }" +" +Scheduler = { Class = GAMScheduler TimingDataSource = DAMS }" +"}"; + +bool SendCommandGAM(uint16 port, const char8* cmd, StreamString &reply) { + BasicTCPSocket client; + if (!client.Open()) return false; + if (!client.Connect("127.0.0.1", port)) return false; + + uint32 s = StringHelper::Length(cmd); + if (!client.Write(cmd, s)) return false; + + char buffer[16384]; + uint32 size = 16384; + TimeoutType timeout(5000); + if (client.Read(buffer, size, timeout)) { + reply.Write(buffer, size); + client.Close(); + return true; + } + client.Close(); + return false; +} + +} diff --git a/Test/Integration/TestCommon.h b/Test/Integration/TestCommon.h new file mode 100644 index 0000000..0077bfa --- /dev/null +++ b/Test/Integration/TestCommon.h @@ -0,0 +1,13 @@ +#ifndef TESTCOMMON_H +#define TESTCOMMON_H + +#include "CompilerTypes.h" +#include "StreamString.h" + +namespace MARTe { + extern const char8 * const debug_test_config; + bool SendCommandGAM(uint16 port, const char8* cmd, StreamString &reply); + void TestMessageCommand(); +} + +#endif diff --git a/Test/Integration/TraceTest.cpp b/Test/Integration/TraceTest.cpp index 8706369..c81fa2c 100644 --- a/Test/Integration/TraceTest.cpp +++ b/Test/Integration/TraceTest.cpp @@ -1,9 +1,10 @@ +#include "BasicTCPSocket.h" +#include "BasicUDPSocket.h" #include "DebugService.h" -#include "DebugCore.h" #include "ObjectRegistryDatabase.h" #include "StandardParser.h" #include "StreamString.h" -#include "BasicUDPSocket.h" +#include "HighResolutionTimer.h" #include #include @@ -11,20 +12,22 @@ using namespace MARTe; void TestFullTracePipeline() { printf("Starting Full Trace Pipeline Test...\n"); - printf("sizeof(TraceHeader) = %lu\n", sizeof(TraceHeader)); + + ObjectRegistryDatabase::Instance()->Purge(); // 1. Setup Service DebugService service; ConfigurationDatabase config; - config.Write("ControlPort", (uint16)8080); - config.Write("StreamPort", (uint16)8081); - config.Write("LogPort", (uint16)8082); + config.Write("ControlPort", (uint16)8082); + config.Write("StreamPort", (uint16)8083); + config.Write("LogPort", (uint16)8084); config.Write("StreamIP", "127.0.0.1"); assert(service.Initialise(config)); + Sleep::MSec(500); // 2. Register a mock signal uint32 mockValue = 0; - DebugSignalInfo* sig = service.RegisterSignal(&mockValue, UnsignedInteger32Bit, "Test.Signal"); + DebugSignalInfo* sig = service.RegisterSignal(&mockValue, UnsignedInteger32Bit, "TraceTest.Signal", 0, 1); assert(sig != NULL_PTR(DebugSignalInfo*)); printf("Signal registered with ID: %u\n", sig->internalID); @@ -35,13 +38,14 @@ void TestFullTracePipeline() { // 4. Setup a local UDP listener BasicUDPSocket listener; assert(listener.Open()); - assert(listener.Listen(8081)); + assert(listener.Listen(8083)); // 5. Simulate cycles printf("Simulating cycles...\n"); for (int i=0; i<50; i++) { mockValue = 1000 + i; - service.ProcessSignal(sig, sizeof(uint32)); + uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() * HighResolutionTimer::Period() * 1000000.0); + service.ProcessSignal(sig, sizeof(uint32), ts); Sleep::MSec(10); } @@ -51,24 +55,19 @@ void TestFullTracePipeline() { TimeoutType timeout(1000); // 1s if (listener.Read(buffer, size, timeout)) { printf("SUCCESS: Received %u bytes over UDP!\n", size); - for(uint32 i=0; imagic, h->count, h->seq); uint32 offset = sizeof(TraceHeader); - if (size >= offset + 8) { + if (size >= offset + 16) { uint32 recId = *(uint32*)(&buffer[offset]); - uint32 recSize = *(uint32*)(&buffer[offset + 4]); - printf("Data: ID=%u, Size=%u\n", recId, recSize); - if (size >= offset + 8 + recSize) { + uint64 recTs = *(uint64*)(&buffer[offset + 4]); + uint32 recSize = *(uint32*)(&buffer[offset + 12]); + printf("Data: ID=%u, TS=%llu, Size=%u\n", recId, (unsigned long long)recTs, recSize); + if (size >= offset + 16 + recSize) { if (recSize == 4) { - uint32 recVal = *(uint32*)(&buffer[offset + 8]); + uint32 recVal = *(uint32*)(&buffer[offset + 16]); printf("Value=%u\n", recVal); } } @@ -79,8 +78,3 @@ void TestFullTracePipeline() { listener.Close(); } - -int main() { - TestFullTracePipeline(); - return 0; -} diff --git a/Test/Integration/TreeCommandTest.cpp b/Test/Integration/TreeCommandTest.cpp new file mode 100644 index 0000000..758b66c --- /dev/null +++ b/Test/Integration/TreeCommandTest.cpp @@ -0,0 +1,178 @@ +#include "BasicTCPSocket.h" +#include "ConfigurationDatabase.h" +#include "DebugService.h" +#include "GlobalObjectsDatabase.h" +#include "ObjectRegistryDatabase.h" +#include "RealTimeApplication.h" +#include "StandardParser.h" +#include "StreamString.h" +#include "TestCommon.h" +#include "IOGAM.h" +#include "LinuxTimer.h" +#include "GAMDataSource.h" +#include "TimingDataSource.h" +#include "GAMScheduler.h" +#include + +using namespace MARTe; + +void TestTreeCommand() { + printf("--- Test: TREE Command Enhancement ---\n"); + + ObjectRegistryDatabase::Instance()->Purge(); + Sleep::MSec(2000); // Wait for sockets from previous tests to clear + + ConfigurationDatabase cdb; + // Use unique ports to avoid conflict with other tests + const char8 * const tree_test_config = + "DebugService = {" + " Class = DebugService " + " ControlPort = 8110 " + " UdpPort = 8111 " + " StreamIP = \"127.0.0.1\" " + "}" + "App = {" + " Class = RealTimeApplication " + " +Functions = {" + " Class = ReferenceContainer " + " +GAM1 = {" + " Class = IOGAM " + " InputSignals = {" + " Counter = { DataSource = Timer Type = uint32 Frequency = 1000 }" + " Time = { DataSource = Timer Type = uint32 }" + " }" + " OutputSignals = {" + " Counter = { DataSource = DDB Type = uint32 }" + " Time = { DataSource = DDB Type = uint32 }" + " }" + " }" + " }" + " +Data = {" + " Class = ReferenceContainer " + " DefaultDataSource = DDB " + " +Timer = { Class = LinuxTimer SleepTime = 1000 Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }" + " +DDB = { Class = GAMDataSource Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }" + " +DAMS = { Class = TimingDataSource }" + " }" + " +States = {" + " Class = ReferenceContainer " + " +State1 = { Class = RealTimeState +Threads = { Class = ReferenceContainer +Thread1 = { Class = RealTimeThread Functions = {GAM1} } } }" + " }" + " +Scheduler = { Class = GAMScheduler TimingDataSource = DAMS }" + "}"; + + StreamString ss = tree_test_config; + ss.Seek(0); + StandardParser parser(ss, cdb); + if (!parser.Parse()) { + printf("ERROR: Failed to parse config\n"); + return; + } + + cdb.MoveToRoot(); + uint32 n = cdb.GetNumberOfChildren(); + for (uint32 i = 0; i < n; i++) { + const char8 *name = cdb.GetChildName(i); + ConfigurationDatabase child; + cdb.MoveRelative(name); + cdb.Copy(child); + cdb.MoveToAncestor(1u); + + StreamString className; + child.Read("Class", className); + + Reference ref(className.Buffer(), + GlobalObjectsDatabase::Instance()->GetStandardHeap()); + if (!ref.IsValid()) { + printf("ERROR: Could not create object %s of class %s\n", name, + className.Buffer()); + continue; + } + ref->SetName(name); + if (!ref->Initialise(child)) { + printf("ERROR: Failed to initialise object %s\n", name); + continue; + } + ObjectRegistryDatabase::Instance()->Insert(ref); + } + + ReferenceT service = + ObjectRegistryDatabase::Instance()->Find("DebugService"); + if (!service.IsValid()) { + printf("ERROR: DebugService not found\n"); + return; + } + service->SetFullConfig(cdb); + + ReferenceT app = + ObjectRegistryDatabase::Instance()->Find("App"); + if (!app.IsValid()) { + printf("ERROR: App not found\n"); + return; + } + + if (!app->ConfigureApplication()) { + printf("ERROR: ConfigureApplication failed.\n"); + return; + } + + if (app->PrepareNextState("State1") != ErrorManagement::NoError) { + printf("ERROR: PrepareNextState failed.\n"); + return; + } + + if (app->StartNextStateExecution() != ErrorManagement::NoError) { + printf("ERROR: StartNextStateExecution failed.\n"); + return; + } + + printf("Application started.\n"); + Sleep::MSec(1000); + + // Step 1: Request TREE + StreamString reply; + if (SendCommandGAM(8110, "TREE\n", reply)) { + printf("TREE response received (len=%llu)\n", reply.Size()); + // ... + } + + // Step 2: SERVICE_INFO + printf("\n--- Step 2: SERVICE_INFO ---\n"); + reply = ""; + if (SendCommandGAM(8110, "SERVICE_INFO\n", reply)) { + printf("SERVICE_INFO response: %s", reply.Buffer()); + if (StringHelper::SearchString(reply.Buffer(), "TCP_CTRL:8110") != NULL_PTR(const char8 *) && + StringHelper::SearchString(reply.Buffer(), "UDP_STREAM:8111") != NULL_PTR(const char8 *)) { + printf("SUCCESS: SERVICE_INFO returned correct ports.\n"); + } else { + printf("FAILURE: SERVICE_INFO returned incorrect data.\n"); + } + } + + // Step 3: MONITOR + printf("\n--- Step 3: MONITOR SIGNAL ---\n"); + reply = ""; + if (SendCommandGAM(8110, "MONITOR SIGNAL App.Data.Timer.Counter 10\n", reply)) { + printf("MONITOR response: %s", reply.Buffer()); + if (StringHelper::SearchString(reply.Buffer(), "OK MONITOR 1") != NULL_PTR(const char8 *)) { + printf("SUCCESS: Signal monitored.\n"); + } else { + printf("FAILURE: Could not monitor signal.\n"); + } + } + + // Step 4: UNMONITOR + printf("\n--- Step 4: UNMONITOR SIGNAL ---\n"); + reply = ""; + if (SendCommandGAM(8110, "UNMONITOR SIGNAL App.Data.Timer.Counter\n", reply)) { + printf("UNMONITOR response: %s", reply.Buffer()); + if (StringHelper::SearchString(reply.Buffer(), "OK UNMONITOR 1") != NULL_PTR(const char8 *)) { + printf("SUCCESS: Signal unmonitored.\n"); + } else { + printf("FAILURE: Could not unmonitor signal.\n"); + } + } + + app->StopCurrentStateExecution(); + ObjectRegistryDatabase::Instance()->Purge(); +} diff --git a/Test/Integration/ValidationTest.cpp b/Test/Integration/ValidationTest.cpp index 71ee510..700bd73 100644 --- a/Test/Integration/ValidationTest.cpp +++ b/Test/Integration/ValidationTest.cpp @@ -1,202 +1,161 @@ +#include "BasicTCPSocket.h" +#include "BasicUDPSocket.h" #include "DebugService.h" -#include "DebugCore.h" #include "ObjectRegistryDatabase.h" +#include "RealTimeApplication.h" #include "StandardParser.h" #include "StreamString.h" -#include "BasicUDPSocket.h" -#include "BasicTCPSocket.h" -#include "RealTimeApplication.h" +#include "GlobalObjectsDatabase.h" #include #include using namespace MARTe; -const char8 * const config_text = -"+DebugService = {" +const char8 * const validation_config = +"DebugService = {" " Class = DebugService " -" ControlPort = 8080 " -" UdpPort = 8081 " +" ControlPort = 8085 " +" UdpPort = 8086 " " StreamIP = \"127.0.0.1\" " "}" -"+App = {" +"App = {" " Class = RealTimeApplication " " +Functions = {" " Class = ReferenceContainer " " +GAM1 = {" " Class = IOGAM " " InputSignals = {" -" Counter = {" -" DataSource = Timer " -" Type = uint32 " -" }" +" Counter = { DataSource = Timer Type = uint32 Frequency = 1000 }" +" Time = { DataSource = Timer Type = uint32 }" " }" " OutputSignals = {" -" Counter = {" -" DataSource = DDB " -" Type = uint32 " -" }" +" Counter = { DataSource = DDB Type = uint32 }" +" Time = { DataSource = DDB Type = uint32 }" " }" " }" " }" " +Data = {" " Class = ReferenceContainer " " DefaultDataSource = DDB " -" +Timer = {" -" Class = LinuxTimer " -" SleepTime = 10000 " -" Signals = {" -" Counter = { Type = uint32 }" -" }" -" }" -" +DDB = {" -" Class = GAMDataSource " -" Signals = { Counter = { Type = uint32 } }" -" }" +" +Timer = { Class = LinuxTimer SleepTime = 1000 Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }" +" +DDB = { Class = GAMDataSource Signals = { Counter = { Type = uint32 } Time = { Type = uint32 } } }" " +DAMS = { Class = TimingDataSource }" " }" " +States = {" " Class = ReferenceContainer " -" +State1 = {" -" Class = RealTimeState " -" +Threads = {" -" Class = ReferenceContainer " -" +Thread1 = {" -" Class = RealTimeThread " -" Functions = {GAM1} " -" }" -" }" -" }" -" }" -" +Scheduler = {" -" Class = GAMScheduler " -" TimingDataSource = DAMS " +" +State1 = { Class = RealTimeState +Threads = { Class = ReferenceContainer +Thread1 = { Class = RealTimeThread Functions = {GAM1} } } }" " }" +" +Scheduler = { Class = GAMScheduler TimingDataSource = DAMS }" "}"; void RunValidationTest() { - printf("--- MARTe2 100Hz Trace Validation Test ---\n"); + printf("--- MARTe2 1kHz Lossless Trace Validation Test ---\n"); + + ObjectRegistryDatabase::Instance()->Purge(); - // 1. Load Configuration ConfigurationDatabase cdb; - StreamString ss = config_text; + StreamString ss = validation_config; ss.Seek(0); StandardParser parser(ss, cdb); - if (!parser.Parse()) { - printf("ERROR: Failed to parse configuration\n"); - return; - } + assert(parser.Parse()); - if (!ObjectRegistryDatabase::Instance()->Initialise(cdb)) { - printf("ERROR: Failed to initialise ObjectRegistryDatabase.\n"); + cdb.MoveToRoot(); + uint32 n = cdb.GetNumberOfChildren(); + for (uint32 i=0; iGetStandardHeap()); + ref->SetName(name); + assert(ref->Initialise(child)); + ObjectRegistryDatabase::Instance()->Insert(ref); + } + + Reference serviceGeneric = ObjectRegistryDatabase::Instance()->Find("DebugService"); + Reference appGeneric = ObjectRegistryDatabase::Instance()->Find("App"); + + if (!serviceGeneric.IsValid() || !appGeneric.IsValid()) { + printf("ERROR: Objects NOT FOUND in ValidationTest\n"); return; } - // 2. Start Application - ReferenceT app = ObjectRegistryDatabase::Instance()->Find("App"); - if (!app.IsValid()) { - printf("ERROR: App not found\n"); + DebugService *service = dynamic_cast(serviceGeneric.operator->()); + RealTimeApplication *app = dynamic_cast(appGeneric.operator->()); + + assert(service); + assert(app); + + service->SetFullConfig(cdb); + + if (!app->ConfigureApplication()) { + printf("ERROR: ConfigureApplication failed in ValidationTest.\n"); return; } - // We try to use State1 directly as many MARTe2 apps start in the first defined state if no transition is needed - if (app->PrepareNextState("State1") != ErrorManagement::NoError) { - printf("ERROR: Failed to prepare state State1\n"); - // We will try to investigate why, but for now we continue - } - - if (app->StartNextStateExecution() != ErrorManagement::NoError) { - printf("ERROR: Failed to start execution. Maybe it needs an explicit state?\n"); - // return; - } + assert(app->PrepareNextState("State1") == ErrorManagement::NoError); + assert(app->StartNextStateExecution() == ErrorManagement::NoError); - printf("Application started at 100Hz.\n"); + printf("Application started at 1kHz. Enabling Traces...\n"); Sleep::MSec(1000); - // 3. Enable Trace via TCP (Simulating GUI) - BasicTCPSocket client; - if (client.Connect("127.0.0.1", 8080)) { - const char* cmd = "TRACE Root.App.Data.Timer.Counter 1\n"; - uint32 s = StringHelper::Length(cmd); - client.Write(cmd, s); - - char resp[1024]; s = 1024; - TimeoutType timeout(1000); - if (client.Read(resp, s, timeout)) { - resp[s] = '\0'; - printf("Server Response: %s", resp); - } else { - printf("WARNING: No response from server to TRACE command.\n"); - } - client.Close(); - } else { - printf("ERROR: Failed to connect to DebugService on 8080\n"); - // continue anyway to see if it's already working + if (service->TraceSignal("App.Data.Timer.Counter", true, 1) == 0) { + printf("ERROR: Failed to enable trace for App.Data.Timer.Counter\n"); } - // 4. Setup UDP Listener BasicUDPSocket listener; - if (!listener.Open()) { printf("ERROR: Failed to open UDP socket\n"); return; } - if (!listener.Listen(8081)) { printf("ERROR: Failed to listen on UDP 8081\n"); return; } + listener.Open(); + listener.Listen(8086); - // 5. Validate for 30 seconds - printf("Validating telemetry for 30 seconds...\n"); - uint32 lastVal = 0; - bool first = true; - uint32 packetCount = 0; - uint32 discontinuityCount = 0; - - float64 startTime = HighResolutionTimer::Counter() * HighResolutionTimer::Period(); - - while ((HighResolutionTimer::Counter() * HighResolutionTimer::Period() - startTime) < 30.0) { + printf("Validating for 10 seconds...\n"); + uint32 totalPackets = 0; + uint32 totalSamples = 0; + uint32 discontinuities = 0; + uint32 lastValue = 0xFFFFFFFF; + + uint64 start = HighResolutionTimer::Counter(); + float64 elapsed = 0; + while (elapsed < 10.0) { char buffer[2048]; uint32 size = 2048; - TimeoutType timeout(500); - - if (listener.Read(buffer, size, timeout)) { + if (listener.Read(buffer, size, TimeoutType(100))) { + totalPackets++; TraceHeader *h = (TraceHeader*)buffer; - if (h->magic == 0xDA7A57AD && h->count > 0) { - uint32 offset = sizeof(TraceHeader); - // Packet format: [Header][ID:4][Size:4][Value:N] - uint32 val = *(uint32*)(&buffer[offset + 8]); - - if (!first) { - if (val != lastVal + 1) { - discontinuityCount++; + uint32 offset = sizeof(TraceHeader); + for (uint32 i=0; icount; i++) { + uint32 recId = *(uint32*)(&buffer[offset]); + uint32 recSize = *(uint32*)(&buffer[offset + 12]); + if (recSize == 4) { + uint32 val = *(uint32*)(&buffer[offset + 16]); + totalSamples++; + if (lastValue != 0xFFFFFFFF && val != lastValue + 1) { + discontinuities++; } + lastValue = val; } - lastVal = val; - first = false; - packetCount++; - - if (packetCount % 500 == 0) { - printf("Received %u packets... Current Value: %u\n", packetCount, val); - } + offset += (16 + recSize); } } + elapsed = (float64)(HighResolutionTimer::Counter() - start) * HighResolutionTimer::Period(); } - printf("Test Finished.\n"); - printf("Total Packets Received: %u (Expected ~3000)\n", packetCount); - printf("Discontinuities: %u\n", discontinuityCount); - - float64 actualFreq = (float64)packetCount / 30.0; - printf("Average Frequency: %.2f Hz\n", actualFreq); + printf("\n--- Test Results ---\n"); + printf("Total UDP Packets: %u\n", totalPackets); + printf("Total Counter Samples: %u\n", totalSamples); + printf("Counter Discontinuities: %u\n", discontinuities); - if (packetCount < 100) { - printf("FAILURE: Almost no packets received. Telemetry is broken.\n"); - } else if (packetCount < 2500) { - printf("WARNING: Too few packets received (Expected 3000, Got %u).\n", packetCount); - } else if (discontinuityCount > 100) { - printf("FAILURE: Too many discontinuities (%u).\n", discontinuityCount); + if (totalSamples < 9000) { + printf("FAILURE: Underflow - samples missing (%u).\n", totalSamples); + } else if (discontinuities > 50) { + printf("FAILURE: Excessive discontinuities detected! (%u)\n", discontinuities); } else { - printf("VALIDATION SUCCESSFUL!\n"); + printf("VALIDATION SUCCESSFUL: 1kHz Lossless Tracing Verified.\n"); } app->StopCurrentStateExecution(); - listener.Close(); -} - -int main() { - RunValidationTest(); - return 0; } diff --git a/Test/Integration/main.cpp b/Test/Integration/main.cpp deleted file mode 100644 index 3879c2c..0000000 --- a/Test/Integration/main.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include -#include "DebugService.h" -#include "MemoryMapInputBroker.h" -#include "ConfigurationDatabase.h" -#include "ObjectRegistryDatabase.h" -#include "ClassRegistryDatabase.h" - -using namespace MARTe; - -#include -#include - -void timeout_handler(int sig) { - printf("Test timed out!\n"); - _exit(1); -} - -int main() { - signal(SIGALRM, timeout_handler); - alarm(5); // 5 seconds timeout - printf("MARTe2 Debug Suite Integration Test\n"); - - { - // 1. Manually trigger Registry Patching - DebugService service; - ConfigurationDatabase serviceData; - serviceData.Write("ControlPort", (uint16)9090); - service.Initialise(serviceData); - - printf("DebugService initialized and Registry Patched.\n"); - - // 2. Try to create a MemoryMapInputBroker - ClassRegistryItem *item = ClassRegistryDatabase::Instance()->Find("MemoryMapInputBroker"); - if (item != NULL_PTR(ClassRegistryItem *)) { - Object *obj = item->GetObjectBuilder()->Build(GlobalObjectsDatabase::Instance()->GetStandardHeap()); - if (obj != NULL_PTR(Object *)) { - printf("Instantiated Broker Class: %s\n", obj->GetClassProperties()->GetName()); - printf("Success: Broker patched and instantiated.\n"); - // delete obj; - } else { - printf("Failed to build broker\n"); - } - } else { - printf("MemoryMapInputBroker not found in registry\n"); - } - } - printf("DebugService scope finished.\n"); - - return 0; -} diff --git a/Test/Makefile.gcc b/Test/Makefile.gcc new file mode 100644 index 0000000..ab19097 --- /dev/null +++ b/Test/Makefile.gcc @@ -0,0 +1 @@ +include Makefile.inc diff --git a/Test/Makefile.inc b/Test/Makefile.inc new file mode 100644 index 0000000..95cc4cb --- /dev/null +++ b/Test/Makefile.inc @@ -0,0 +1,14 @@ +SPB = UnitTests.x Integration.x + +PACKAGE = Test + +ROOT_DIR = .. + +MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults + +include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET) + +all: $(SUBPROJ) + echo $(SUBPROJ) + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) diff --git a/Test/UnitTests/CMakeLists.txt b/Test/UnitTests/CMakeLists.txt deleted file mode 100644 index 63c15c3..0000000 --- a/Test/UnitTests/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(marte_dev_tests) - -include_directories( - ${MARTe2_DIR}/Source/Core/BareMetal/L0Types - ${MARTe2_DIR}/Source/Core/BareMetal/L1Portability - # ... more ... - ../../Source - ../../Headers -) - -file(GLOB SOURCES "*.cpp") - -add_executable(UnitTests ${SOURCES}) - -target_link_libraries(UnitTests - marte_dev - ${MARTe2_DIR}/Build/${TARGET}/Core/libMARTe2.so -) diff --git a/Test/UnitTests/Makefile.gcc b/Test/UnitTests/Makefile.gcc new file mode 100644 index 0000000..ab19097 --- /dev/null +++ b/Test/UnitTests/Makefile.gcc @@ -0,0 +1 @@ +include Makefile.inc diff --git a/Test/UnitTests/Makefile.inc b/Test/UnitTests/Makefile.inc new file mode 100644 index 0000000..845fc78 --- /dev/null +++ b/Test/UnitTests/Makefile.inc @@ -0,0 +1,39 @@ +OBJSX = + +PACKAGE = Test/UnitTests + +ROOT_DIR = ../.. + +MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults + +include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET) + +INCLUDES += -I$(ROOT_DIR)/Source/Core/Types/Result +INCLUDES += -I$(ROOT_DIR)/Source/Core/Types/Vec +INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/DebugService +INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/TCPLogger + +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Logger +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4LoggerService +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L5GAMs +INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams + +LIBRARIES += -L$(MARTe2_DIR)/Build/$(TARGET)/Core -lMARTe2 +LIBRARIES += -L$(ROOT_DIR)/Build/$(TARGET)/Components/Interfaces/DebugService -lDebugService +LIBRARIES += -L$(ROOT_DIR)/Build/$(TARGET)/Components/Interfaces/TCPLogger -lTcpLogger + +all: $(OBJS) $(BUILD_DIR)/UnitTests$(EXEEXT) + echo $(OBJS) + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) diff --git a/Test/UnitTests/UnitTests.cpp b/Test/UnitTests/UnitTests.cpp new file mode 100644 index 0000000..63559e4 --- /dev/null +++ b/Test/UnitTests/UnitTests.cpp @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include +#include "DebugCore.h" +#include "DebugService.h" +#include "DebugBrokerWrapper.h" +#include "TcpLogger.h" +#include "ConfigurationDatabase.h" +#include "ObjectRegistryDatabase.h" +#include "GlobalObjectsDatabase.h" + +namespace MARTe { + +void TestTcpLogger() { + printf("Stability Logger Tests...\n"); + TcpLogger logger; + ConfigurationDatabase config; + config.Write("Port", (uint32)0); // Random port + assert(logger.Initialise(config)); +} + +class DebugServiceTest { +public: + static void TestAll() { + printf("Stability Logic Tests...\n"); + ObjectRegistryDatabase::Instance()->Purge(); + + DebugService service; + assert(service.traceBuffer.Init(1024 * 1024)); + + ConfigurationDatabase cfg; + cfg.Write("ControlPort", (uint32)0); + cfg.Write("StreamPort", (uint32)0); + cfg.Write("SuppressTimeoutLogs", (uint32)1); + assert(service.Initialise(cfg)); + + // 1. Signal logic + uint32 val = 0; + service.RegisterSignal(&val, UnsignedInteger32Bit, "X.Y.Z", 0, 1); + assert(service.TraceSignal("Z", true) == 1); + assert(service.ForceSignal("Z", "123") == 1); + + uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() * HighResolutionTimer::Period() * 1000000.0); + service.ProcessSignal(service.signals[0], 4, ts); + assert(val == 123); + service.UnforceSignal("Z"); + + // 2. Commands + service.HandleCommand("TREE", NULL_PTR(BasicTCPSocket*)); + service.HandleCommand("DISCOVER", NULL_PTR(BasicTCPSocket*)); + service.HandleCommand("CONFIG", NULL_PTR(BasicTCPSocket*)); + service.HandleCommand("PAUSE", NULL_PTR(BasicTCPSocket*)); + service.HandleCommand("RESUME", NULL_PTR(BasicTCPSocket*)); + service.HandleCommand("LS /", NULL_PTR(BasicTCPSocket*)); + service.HandleCommand("INFO X.Y.Z", NULL_PTR(BasicTCPSocket*)); + service.HandleCommand("MSG DebugService DummyFunc 0 K=V", NULL_PTR(BasicTCPSocket*)); + + // 3. Broker Active Status + volatile bool active = false; + Vec indices; + Vec sizes; + FastPollingMutexSem mutex; + DebugSignalInfo* ptrs[1] = { service.signals[0] }; + 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, &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); + } +}; + +} + +#include + +void timeout_handler(int sig) { + printf("Test timed out!\n"); + _exit(1); +} + +int main() { + signal(SIGALRM, timeout_handler); + alarm(10); + printf("--- MARTe2 Debug Suite COVERAGE V30 ---\n"); + MARTe::TestTcpLogger(); + MARTe::DebugServiceTest::TestAll(); + printf("\nCOVERAGE V30 PASSED!\n"); + return 0; +} diff --git a/Test/UnitTests/main.cpp b/Test/UnitTests/main.cpp deleted file mode 100644 index 7f53f35..0000000 --- a/Test/UnitTests/main.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include -#include -#include "DebugCore.h" - -using namespace MARTe; - -void TestRingBuffer() { - printf("Testing TraceRingBuffer...\n"); - TraceRingBuffer rb; - assert(rb.Init(1024)); - - uint32 id = 42; - uint32 val = 12345678; - uint32 size = 4; - - assert(rb.Push(id, &val, size)); - assert(rb.Count() > 0); - - uint32 poppedId = 0; - uint32 poppedVal = 0; - uint32 poppedSize = 0; - - assert(rb.Pop(poppedId, &poppedVal, poppedSize, 4)); - assert(poppedId == 42); - assert(poppedVal == 12345678); - assert(poppedSize == 4); - - printf("TraceRingBuffer test passed.\n"); -} - -int main(int argc, char **argv) { - printf("Running MARTe2 Component Tests...\n"); - TestRingBuffer(); - return 0; -} diff --git a/Tools/gui_client/Cargo.lock b/Tools/gui_client/Cargo.lock index 25590af..88146cc 100644 --- a/Tools/gui_client/Cargo.lock +++ b/Tools/gui_client/Cargo.lock @@ -35,7 +35,7 @@ dependencies = [ "atspi-common", "serde", "thiserror 1.0.69", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -78,7 +78,7 @@ dependencies = [ "futures-lite", "futures-util", "serde", - "zbus", + "zbus 4.4.0", ] [[package]] @@ -123,6 +123,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "const-random", "getrandom 0.3.4", "once_cell", "version_check", @@ -138,6 +139,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -165,6 +181,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -212,6 +234,220 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "arrow" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a3ec4fe573f9d1f59d99c085197ef669b00b088ba1d7bb75224732d9357a74" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-csv", + "arrow-data", + "arrow-ipc", + "arrow-json", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dcf19f07792d8c7f91086c67b574a79301e367029b17fcf63fb854332246a10" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "num", +] + +[[package]] +name = "arrow-array" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7845c32b41f7053e37a075b3c2f29c6f5ea1b3ca6e5df7a2d325ee6e1b4a63cf" +dependencies = [ + "ahash", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown 0.15.5", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5c681a99606f3316f2a99d9c8b6fa3aad0b1d34d8f6d7a1b471893940219d8" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-cast" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365f8527d4f87b133eeb862f9b8093c009d41a210b8f101f91aa2392f61daac" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-csv" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30dac4d23ac769300349197b845e0fd18c7f9f15d260d4659ae6b5a9ca06f586" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-schema", + "chrono", + "csv", + "csv-core", + "lazy_static", + "lexical-core", + "regex", +] + +[[package]] +name = "arrow-data" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd962fc3bf7f60705b25bcaa8eb3318b2545aa1d528656525ebdd6a17a6cd6fb" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num", +] + +[[package]] +name = "arrow-ipc" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3527365b24372f9c948f16e53738eb098720eea2093ae73c7af04ac5e30a39b" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-schema", + "flatbuffers", +] + +[[package]] +name = "arrow-json" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdec0024749fc0d95e025c0b0266d78613727b3b3a5d4cf8ea47eb6d38afdd1" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "indexmap", + "lexical-core", + "num", + "serde", + "serde_json", +] + +[[package]] +name = "arrow-ord" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79af2db0e62a508d34ddf4f76bfd6109b6ecc845257c9cba6f939653668f89ac" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "half", + "num", +] + +[[package]] +name = "arrow-row" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da30e9d10e9c52f09ea0cf15086d6d785c11ae8dcc3ea5f16d402221b6ac7735" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b0f9c0c3582dd55db0f136d3b44bfa0189df07adcf7dc7f2f2e74db0f52eb8" + +[[package]] +name = "arrow-select" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fc337f01635218493c23da81a364daf38c694b05fc20569c3193c11c561984" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num", +] + +[[package]] +name = "arrow-string" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d596a9fc25dae556672d5069b090331aca8acb93cae426d8b7dcdf1c558fa0ce" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num", + "regex", + "regex-syntax", +] + [[package]] name = "as-raw-xcb-connection" version = "1.0.1" @@ -227,6 +463,28 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus 5.14.0", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -305,6 +563,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.5.0" @@ -369,6 +638,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -395,11 +673,11 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus", + "zbus 4.4.0", "zbus-lockstep", "zbus-lockstep-macros", - "zbus_names", - "zvariant", + "zbus_names 3.0.0", + "zvariant 4.2.0", ] [[package]] @@ -411,7 +689,7 @@ dependencies = [ "atspi-common", "atspi-proxies", "futures-lite", - "zbus", + "zbus 4.4.0", ] [[package]] @@ -422,8 +700,8 @@ checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" dependencies = [ "atspi-common", "serde", - "zbus", - "zvariant", + "zbus 4.4.0", + "zvariant 4.2.0", ] [[package]] @@ -432,6 +710,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.8.0" @@ -486,6 +770,15 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.3", +] + [[package]] name = "blocking" version = "1.6.2" @@ -499,6 +792,27 @@ dependencies = [ "piper", ] +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -635,15 +949,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-targets 0.52.6", ] [[package]] @@ -684,6 +999,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -783,6 +1118,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "cursor-icon" version = "1.2.0" @@ -812,6 +1168,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.11.0", + "block2 0.6.2", + "libc", "objc2 0.6.3", ] @@ -1131,6 +1489,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flatbuffers" +version = "24.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096" +dependencies = [ + "bitflags 1.3.2", + "rustc_version", +] + [[package]] name = "flate2" version = "1.1.9" @@ -1183,6 +1551,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.32" @@ -1439,6 +1816,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "num-traits", "zerocopy", ] @@ -1648,6 +2026,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "integer-encoding" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" + [[package]] name = "itoa" version = "1.0.17" @@ -1713,12 +2097,75 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + [[package]] name = "libc" version = "0.2.182" @@ -1735,6 +2182,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.12" @@ -1785,6 +2238,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +dependencies = [ + "twox-hash 2.1.2", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1798,16 +2260,18 @@ dependencies = [ name = "marte_debug_gui" version = "0.1.0" dependencies = [ - "byteorder", + "arrow", "chrono", "crossbeam-channel", "eframe", - "egui", "egui_plot", + "once_cell", + "parquet", "regex", + "rfd", "serde", "serde_json", - "tokio", + "socket2", ] [[package]] @@ -1859,17 +2323,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - [[package]] name = "moxcms" version = "0.7.11" @@ -1960,6 +2413,70 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1967,6 +2484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2032,7 +2550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "libc", "objc2 0.5.2", "objc2-core-data", @@ -2048,6 +2566,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.11.0", + "block2 0.6.2", "objc2 0.6.3", "objc2-core-foundation", "objc2-core-graphics", @@ -2061,7 +2580,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -2073,7 +2592,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2085,7 +2604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2120,7 +2639,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -2132,7 +2651,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", "objc2-foundation 0.2.2", @@ -2151,7 +2670,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "dispatch", "libc", "objc2 0.5.2", @@ -2185,7 +2704,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -2198,7 +2717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2210,7 +2729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -2233,7 +2752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", @@ -2253,7 +2772,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2265,7 +2784,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -2287,6 +2806,15 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-float" version = "4.6.0" @@ -2344,6 +2872,39 @@ dependencies = [ "windows-link", ] +[[package]] +name = "parquet" +version = "53.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8cf58b29782a7add991f655ff42929e31a7859f5319e53db9e39a714cb113c" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ipc", + "arrow-schema", + "arrow-select", + "base64", + "brotli", + "bytes", + "chrono", + "flate2", + "half", + "hashbrown 0.15.5", + "lz4_flex", + "num", + "num-bigint", + "paste", + "seq-macro", + "snap", + "thrift", + "twox-hash 1.6.3", + "zstd", + "zstd-sys", +] + [[package]] name = "paste" version = "1.0.15" @@ -2426,6 +2987,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "potential_utf" version = "0.1.4" @@ -2534,8 +3101,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -2545,7 +3122,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -2557,6 +3144,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2625,6 +3221,30 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.2", + "dispatch2", + "js-sys", + "log", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2637,6 +3257,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -2669,6 +3298,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -2709,6 +3344,12 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + [[package]] name = "serde" version = "1.0.228" @@ -2890,13 +3531,19 @@ dependencies = [ ] [[package]] -name = "socket2" -version = "0.6.2" +name = "snap" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -3032,6 +3679,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thrift" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e54bc85fc7faa8bc175c4bab5b92ba8d9a3ce893d0e9f42cc455c8ab16a9e09" +dependencies = [ + "byteorder", + "integer-encoding", + "ordered-float 2.10.1", +] + [[package]] name = "tiff" version = "0.10.3" @@ -3046,6 +3704,15 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -3081,34 +3748,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -3177,6 +3816,22 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + [[package]] name = "type-map" version = "0.5.1" @@ -3237,14 +3892,32 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" @@ -3632,7 +4305,7 @@ dependencies = [ "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", - "ordered-float", + "ordered-float 4.6.0", "parking_lot", "profiling", "raw-window-handle", @@ -4053,7 +4726,7 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.11.0", - "block2", + "block2 0.5.1", "bytemuck", "calloop 0.13.0", "cfg_aliases", @@ -4318,7 +4991,7 @@ dependencies = [ "hex", "nix", "ordered-stream", - "rand", + "rand 0.8.5", "serde", "serde_repr", "sha1", @@ -4327,9 +5000,44 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.3", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros 5.14.0", + "zbus_names 4.3.1", + "zvariant 5.10.0", ] [[package]] @@ -4339,7 +5047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" dependencies = [ "zbus_xml", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -4353,7 +5061,7 @@ dependencies = [ "syn", "zbus-lockstep", "zbus_xml", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -4366,7 +5074,22 @@ dependencies = [ "proc-macro2", "quote", "syn", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names 4.3.1", + "zvariant 5.10.0", + "zvariant_utils 3.3.0", ] [[package]] @@ -4377,7 +5100,18 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow", + "zvariant 5.10.0", ] [[package]] @@ -4389,8 +5123,8 @@ dependencies = [ "quick-xml 0.30.0", "serde", "static_assertions", - "zbus_names", - "zvariant", + "zbus_names 3.0.0", + "zvariant 4.2.0", ] [[package]] @@ -4473,6 +5207,34 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.4.12" @@ -4498,7 +5260,22 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow", + "zvariant_derive 5.10.0", + "zvariant_utils 3.3.0", ] [[package]] @@ -4511,7 +5288,20 @@ dependencies = [ "proc-macro2", "quote", "syn", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils 3.3.0", ] [[package]] @@ -4524,3 +5314,16 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow", +] diff --git a/Tools/gui_client/Cargo.toml b/Tools/gui_client/Cargo.toml index 7005519..a1e1e84 100644 --- a/Tools/gui_client/Cargo.toml +++ b/Tools/gui_client/Cargo.toml @@ -5,12 +5,14 @@ edition = "2021" [dependencies] eframe = "0.31.0" -egui = "0.31.0" egui_plot = "0.31.0" -tokio = { version = "1.0", features = ["full"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -byteorder = "1.4" chrono = "0.4" crossbeam-channel = "0.5" -regex = "1.12.3" +regex = "1.10" +socket2 = { version = "0.5", features = ["all"] } +once_cell = "1.21" +rfd = "0.15" +parquet = { version = "53.0", features = ["arrow"] } +arrow = "53.0" diff --git a/Tools/gui_client/src/main.rs b/Tools/gui_client/src/main.rs index 433eb21..9a44c15 100644 --- a/Tools/gui_client/src/main.rs +++ b/Tools/gui_client/src/main.rs @@ -1,14 +1,25 @@ -use eframe::egui; -use egui_plot::{Line, Plot, PlotPoints}; -use std::collections::{HashMap, VecDeque}; -use std::net::{TcpStream, UdpSocket}; -use std::io::{Write, BufReader, BufRead}; -use std::sync::{Arc, Mutex}; -use std::thread; -use serde::{Deserialize, Serialize}; +use arrow::array::Float64Array; +use arrow::datatypes::{DataType, Field, Schema}; +use arrow::record_batch::RecordBatch; use chrono::Local; use crossbeam_channel::{unbounded, Receiver, Sender}; +use eframe::egui; +use egui_plot::{Line, LineStyle, MarkerShape, Plot, PlotBounds, PlotPoints, VLine}; +use once_cell::sync::Lazy; +use parquet::arrow::arrow_writer::ArrowWriter; +use parquet::file::properties::WriterProperties; use regex::Regex; +use rfd::FileDialog; +use serde::{Deserialize, Serialize}; +use socket2::{Domain, Protocol, Socket, Type}; +use std::collections::{HashMap, VecDeque}; +use std::fs::File; +use std::io::{BufRead, BufReader, Write}; +use std::net::{TcpStream, UdpSocket}; +use std::sync::{Arc, Mutex}; +use std::thread; + +static BASE_TELEM_TS: Lazy>> = Lazy::new(|| Mutex::new(None)); // --- Models --- @@ -18,8 +29,14 @@ struct Signal { id: u32, #[serde(rename = "type")] sig_type: String, + #[serde(default)] + dimensions: u8, + #[serde(default = "default_elements")] + elements: u32, } +fn default_elements() -> u32 { 1 } + #[derive(Deserialize)] struct DiscoverResponse { #[serde(rename = "Signals")] @@ -40,6 +57,10 @@ struct TreeItem { dimensions: Option, #[serde(rename = "Elements")] elements: Option, + #[serde(rename = "IsTraceable")] + is_traceable: Option, + #[serde(rename = "IsForcable")] + is_forcable: Option, } #[derive(Clone)] @@ -50,12 +71,116 @@ struct LogEntry { } struct TraceData { - values: VecDeque<[f64; 2]>, + values: VecDeque<[f64; 2]>, + last_value: f64, + recording_tx: Option>, + recording_path: Option, + is_monitored: bool, } struct SignalMetadata { names: Vec, sig_type: String, + dimensions: u8, + elements: u32, +} + +#[derive(Clone)] +struct ConnectionConfig { + ip: String, + tcp_port: String, + udp_port: String, + log_port: String, + version: u64, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum PlotType { + Normal, + LogicAnalyzer, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum AcquisitionMode { + FreeRun, + Triggered, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum TriggerEdge { + Rising, + Falling, + Both, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum TriggerType { + Single, + Continuous, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum MarkerType { + None, + Circle, + Square, +} + +impl MarkerType { + fn to_shape(&self) -> Option { + match self { + MarkerType::None => None, + MarkerType::Circle => Some(MarkerShape::Circle), + MarkerType::Square => Some(MarkerShape::Square), + } + } +} + +#[derive(Clone)] +struct SignalPlotConfig { + source_name: String, + label: String, + unit: String, + color: egui::Color32, + line_style: LineStyle, + marker_type: MarkerType, + gain: f64, + offset: f64, +} + +struct PlotInstance { + id: String, + plot_type: PlotType, + signals: Vec, + auto_bounds: bool, + max_points: usize, + follow: bool, + reset_view: bool, +} + +#[derive(Clone, PartialEq)] +enum MsgStatus { + Unknown, + Success, + Failure, +} + +#[derive(Clone)] +struct MessageHistoryEntry { + time: String, + destination: String, + function: String, + payload: String, + wait_reply: bool, + raw_cmd: String, + response: String, + status: MsgStatus, +} + +#[derive(Clone, PartialEq)] +enum MainTab { + Plots, + Config, } enum InternalEvent { @@ -64,65 +189,119 @@ enum InternalEvent { Tree(TreeItem), CommandResponse(String), NodeInfo(String), + ConfigResponse(String), Connected, + Disconnected, InternalLog(String), - TraceRequested(String), + TraceRequested(String, bool), // Name, IsMonitored ClearTrace(String), - UdpStats(u64), + UdpStats(u64), + UdpDropped(u32), + RecordPathChosen(String, String), // SignalName, FilePath + 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 --- struct ForcingDialog { signal_path: String, - sig_type: String, - dims: u8, - elems: u32, value: String, } +struct MonitorDialog { + signal_path: String, + period_ms: String, +} + +struct BreakDialog { + signal_path: String, + op: String, // ">", "<", "==", ">=", "<=", "!=" + threshold: String, +} + +struct MessageDialog { + destination: String, + function: String, + payload: String, + expect_reply: bool, +} + +struct InfoDialog { + path: String, + is_signal: bool, + config_text: String, + is_loading: bool, + value_text: Option, + value_loading: bool, +} + struct LogFilters { - content_regex: String, show_debug: bool, show_info: bool, show_warning: bool, show_error: bool, paused: bool, + content_regex: String, +} + +struct ScopeSettings { + enabled: bool, + window_ms: f64, + mode: AcquisitionMode, + paused: bool, + trigger_type: TriggerType, + trigger_source: String, + trigger_edge: TriggerEdge, + trigger_threshold: f64, + pre_trigger_percent: f64, + trigger_active: bool, + last_trigger_time: f64, + is_armed: bool, } struct MarteDebugApp { - #[allow(dead_code)] connected: bool, - tcp_addr: String, - log_addr: String, - - signals: Vec, + is_breaking: bool, + config: ConnectionConfig, + shared_config: Arc>, app_tree: Option, - - traced_signals: Arc>>, id_to_meta: Arc>>, - + traced_signals: Arc>>, + plots: Vec, forced_signals: HashMap, - is_paused: bool, - + 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, - - // UI Panels show_left_panel: bool, show_right_panel: bool, show_bottom_panel: bool, - selected_node: String, node_info: String, - udp_packets: u64, - + udp_dropped: u64, forcing_dialog: Option, - + monitoring_dialog: Option, + message_dialog: Option, + style_editor: Option<(usize, usize)>, tx_cmd: Sender, rx_events: Receiver, internal_tx: Sender, + shared_x_range: Option<[f64; 2]>, + scope: ScopeSettings, + active_main_tab: MainTab, + app_config_text: String, + message_history: Vec, + show_message_history: bool, + pending_msg_idx: Option, } impl MarteDebugApp { @@ -130,49 +309,71 @@ impl MarteDebugApp { let (tx_cmd, rx_cmd_internal) = unbounded::(); let (tx_events, rx_events) = unbounded::(); let internal_tx = tx_events.clone(); - - let tcp_addr = "127.0.0.1:8080".to_string(); - let log_addr = "127.0.0.1:8082".to_string(); - + let config = ConnectionConfig { + ip: "127.0.0.1".to_string(), + tcp_port: "8080".to_string(), + udp_port: "8081".to_string(), + log_port: "8082".to_string(), + version: 0, + }; + let shared_config = Arc::new(Mutex::new(config.clone())); let id_to_meta = Arc::new(Mutex::new(HashMap::new())); let traced_signals = Arc::new(Mutex::new(HashMap::new())); - let id_to_meta_clone = id_to_meta.clone(); let traced_signals_clone = traced_signals.clone(); - + let shared_config_cmd = shared_config.clone(); + let shared_config_log = shared_config.clone(); + let shared_config_udp = shared_config.clone(); let tx_events_c = tx_events.clone(); thread::spawn(move || { - tcp_command_worker(tcp_addr, rx_cmd_internal, tx_events_c); + tcp_command_worker(shared_config_cmd, rx_cmd_internal, tx_events_c); }); - let tx_events_log = tx_events.clone(); thread::spawn(move || { - tcp_log_worker(log_addr, tx_events_log); + tcp_log_worker(shared_config_log, tx_events_log); }); - let tx_events_udp = tx_events.clone(); thread::spawn(move || { - udp_worker(8081, id_to_meta_clone, traced_signals_clone, tx_events_udp); + udp_worker( + shared_config_udp, + id_to_meta_clone, + traced_signals_clone, + tx_events_udp, + ); }); Self { connected: false, - tcp_addr: "127.0.0.1:8080".to_string(), - log_addr: "127.0.0.1:8082".to_string(), - signals: Vec::new(), + is_breaking: false, + config, + shared_config, app_tree: None, id_to_meta, traced_signals, + plots: vec![PlotInstance { + id: "Plot 1".to_string(), + plot_type: PlotType::Normal, + signals: Vec::new(), + auto_bounds: true, + max_points: 5000, + follow: true, + reset_view: false, + }], forced_signals: HashMap::new(), - is_paused: false, + 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 { - content_regex: "".to_string(), show_debug: true, show_info: true, show_warning: true, show_error: true, paused: false, + content_regex: "".to_string(), }, show_left_panel: true, show_right_panel: true, @@ -180,31 +381,187 @@ impl MarteDebugApp { selected_node: "".to_string(), node_info: "".to_string(), udp_packets: 0, + udp_dropped: 0, forcing_dialog: None, + monitoring_dialog: None, + message_dialog: None, + style_editor: None, tx_cmd, rx_events, internal_tx, + shared_x_range: None, + scope: ScopeSettings { + enabled: false, + window_ms: 1000.0, + mode: AcquisitionMode::FreeRun, + paused: false, + trigger_type: TriggerType::Continuous, + trigger_source: "".to_string(), + trigger_edge: TriggerEdge::Rising, + trigger_threshold: 0.0, + pre_trigger_percent: 25.0, + trigger_active: false, + last_trigger_time: 0.0, + is_armed: true, + }, + active_main_tab: MainTab::Plots, + app_config_text: String::new(), + message_history: Vec::new(), + show_message_history: true, + pending_msg_idx: None, } } + fn next_color(idx: usize) -> egui::Color32 { + let colors = [ + egui::Color32::from_rgb(100, 200, 255), + egui::Color32::from_rgb(255, 100, 100), + egui::Color32::from_rgb(100, 255, 100), + egui::Color32::from_rgb(255, 200, 100), + egui::Color32::from_rgb(255, 100, 255), + egui::Color32::from_rgb(100, 255, 255), + egui::Color32::from_rgb(200, 255, 100), + egui::Color32::WHITE, + ]; + colors[idx % colors.len()] + } + + fn apply_trigger_logic(&mut self) { + if self.scope.mode != AcquisitionMode::Triggered || !self.scope.is_armed { + return; + } + if self.scope.trigger_source.is_empty() { + return; + } + let data_map = self.traced_signals.lock().unwrap(); + if let Some(data) = data_map.get(&self.scope.trigger_source) { + if data.values.len() < 2 { + return; + } + let start_idx = if data.values.len() > 100 { + data.values.len() - 100 + } else { + 0 + }; + for i in (start_idx + 1..data.values.len()).rev() { + let v_prev = data.values[i - 1][1]; + let v_curr = data.values[i][1]; + let t_curr = data.values[i][0]; + if t_curr <= self.scope.last_trigger_time { + continue; + } + let triggered = match self.scope.trigger_edge { + TriggerEdge::Rising => { + v_prev < self.scope.trigger_threshold + && v_curr >= self.scope.trigger_threshold + } + TriggerEdge::Falling => { + v_prev > self.scope.trigger_threshold + && v_curr <= self.scope.trigger_threshold + } + TriggerEdge::Both => { + (v_prev < self.scope.trigger_threshold + && v_curr >= self.scope.trigger_threshold) + || (v_prev > self.scope.trigger_threshold + && v_curr <= self.scope.trigger_threshold) + } + }; + if triggered { + self.scope.last_trigger_time = t_curr; + self.scope.trigger_active = true; + if self.scope.trigger_type == TriggerType::Single { + self.scope.is_armed = false; + } + break; + } + } + } + } + + fn get_all_objects(&self) -> Vec { + let mut objects = Vec::new(); + if let Some(tree) = &self.app_tree { + fn collect(item: &TreeItem, path: String, objects: &mut Vec) { + let current_path = if path.is_empty() { + if item.name == "Root" { + "".to_string() + } else { + item.name.clone() + } + } else { + format!("{}.{}", path, item.name) + }; + if !current_path.is_empty() && !item.class.contains("Signal") { + objects.push(current_path.clone()); + } + if let Some(children) = &item.children { + for child in children { + collect(child, current_path.clone(), objects); + } + } + } + collect(tree, "".to_string(), &mut objects); + } + objects.sort(); + 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() { - item.name.clone() - } else if path == "Root" { - item.name.clone() - } else { - format!("{}.{}", path, item.name) + let current_path = if path.is_empty() { + if item.name == "Root" { + "".to_string() + } else { + item.name.clone() + } + } else { + if path.is_empty() { + item.name.clone() + } else { + format!("{}.{}", path, item.name) + } + }; + let label = if item.class == "Signal" { + format!("📈 {}", item.name) + } else { + item.name.clone() }; - if let Some(children) = &item.children { - let header = egui::CollapsingHeader::new(format!("{} [{}]", item.name, item.class)) + let header = egui::CollapsingHeader::new(format!("{} [{}]", label, item.class)) .id_salt(¤t_path); - header.show(ui, |ui| { ui.horizontal(|ui| { - if 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)); + if !current_path.is_empty() { + if 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, + }); + } } }); for child in children { @@ -213,47 +570,230 @@ impl MarteDebugApp { }); } else { ui.horizontal(|ui| { - if ui.selectable_label(self.selected_node == current_path, format!("{} [{}]", item.name, 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 item.class.contains("Signal") { - if ui.button("📈 Trace").clicked() { - let _ = self.tx_cmd.send(format!("TRACE {} 1", current_path)); - let _ = self.internal_tx.send(InternalEvent::TraceRequested(current_path.clone())); + 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; } - if ui.button("⚡ Force").clicked() { - self.forcing_dialog = Some(ForcingDialog { - signal_path: current_path.clone(), - sig_type: item.sig_type.clone().unwrap_or_else(|| "Unknown".to_string()), - dims: item.dimensions.unwrap_or(0), - elems: item.elements.unwrap_or(1), - value: "".to_string(), + 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 { + let header = egui::CollapsingHeader::new(format!("{} [{}] ({} elems)", label, item.class, elements)) + .id_salt(¤t_path); + header.show(ui, |ui| { + for i in 0..elements { + let elem_path = format!("{}[{}]", current_path, i); + ui.horizontal(|ui| { + ui.label(format!("{}[{}]", item.name, i)); + if ui.button("Trace").clicked() { + let _ = self.tx_cmd.send(format!("TRACE {} 1", current_path)); + let _ = self.internal_tx.send(InternalEvent::TraceRequested(elem_path.clone(), false)); + } + if item.class == "Signal" { + if ui.button("Monitor").clicked() { + self.monitoring_dialog = Some(MonitorDialog { + signal_path: current_path.clone(), + period_ms: "100".to_string(), + }); + // Note: internal monitoring logic will handle individual elements via naming convention + let _ = self.internal_tx.send(InternalEvent::TraceRequested(elem_path.clone(), true)); + } + } + }); + } + }); + } else { + let traceable = item.is_traceable.unwrap_or(false); + let forcable = item.is_forcable.unwrap_or(false); + + if traceable && ui.button("Trace").clicked() { + let _ = self.tx_cmd.send(format!("TRACE {} 1", current_path)); + let _ = self + .internal_tx + .send(InternalEvent::TraceRequested(current_path.clone(), false)); + } + if item.class == "Signal" { + if ui.button("Monitor").clicked() { + self.monitoring_dialog = Some(MonitorDialog { + signal_path: current_path.clone(), + period_ms: "100".to_string(), + }); + } + } + if forcable && ui.button("⚡ Force").clicked() { + self.forcing_dialog = Some(ForcingDialog { + signal_path: current_path.clone(), + 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(), + }); + } + } + } + } }); } } } -fn tcp_command_worker(addr: String, rx_cmd: Receiver, tx_events: Sender) { +/// Emit the body of a JSON object as MARTe2 config lines at the given indent level. +/// `Class` is always emitted first; child objects with a `Class` key get `+` prefix. +fn json_node_to_marte(map: &serde_json::Map, indent: usize) -> String { + let pad = " ".repeat(indent); + let mut out = String::new(); + + // Class first + if let Some(serde_json::Value::String(cls)) = map.get("Class") { + out.push_str(&format!("{}Class = {}\n", pad, cls)); + } + + // Collect and sort remaining keys so output is deterministic + let mut keys: Vec<&String> = map.keys().filter(|k| k.as_str() != "Class").collect(); + keys.sort(); + + for key in keys { + let val = &map[key]; + match val { + serde_json::Value::Object(child_map) => { + let prefix = if child_map.contains_key("Class") { "+" } else { "" }; + out.push_str(&format!("{}{}{} = {{\n", pad, prefix, key)); + out.push_str(&json_node_to_marte(child_map, indent + 1)); + out.push_str(&format!("{}}}\n", pad)); + } + serde_json::Value::String(s) => { + out.push_str(&format!("{}{} = {}\n", pad, key, s)); + } + serde_json::Value::Null => { + out.push_str(&format!("{}{} =\n", pad, key)); + } + other => { + out.push_str(&format!("{}{} = {}\n", pad, key, other)); + } + } + } + out +} + +fn convert_config_json(json_text: &str) -> String { + let root = match serde_json::from_str::(json_text) { + Ok(v) => v, + Err(_) => return json_text.to_string(), + }; + let map = match root.as_object() { + Some(m) => m, + None => return json_text.to_string(), + }; + let mut out = String::new(); + let mut keys: Vec<&String> = map.keys().collect(); + keys.sort(); + for key in keys { + let val = &map[key]; + match val { + serde_json::Value::Object(child_map) => { + // Top-level nodes always get + (they are named MARTe2 objects) + out.push_str(&format!("+{} = {{\n", key)); + out.push_str(&json_node_to_marte(child_map, 1)); + out.push_str("}\n\n"); + } + serde_json::Value::String(s) => { + out.push_str(&format!("{} = {}\n", key, s)); + } + other => { + out.push_str(&format!("{} = {}\n", key, other)); + } + } + } + out +} + +fn tcp_command_worker( + shared_config: Arc>, + rx_cmd: Receiver, + tx_events: Sender, +) { + let mut current_version = 0; + let mut current_addr = String::new(); loop { - if let Ok(mut stream) = TcpStream::connect(&addr) { + { + let config = shared_config.lock().unwrap(); + if config.version != current_version { + current_version = config.version; + current_addr = format!("{}:{}", config.ip, config.tcp_port); + } + } + if current_addr.is_empty() || current_addr.starts_with(":") { + thread::sleep(std::time::Duration::from_secs(1)); + continue; + } + if let Ok(mut stream) = TcpStream::connect(¤t_addr) { let _ = stream.set_nodelay(true); let mut reader = BufReader::new(stream.try_clone().unwrap()); let _ = tx_events.send(InternalEvent::Connected); - + let stop_flag = Arc::new(Mutex::new(false)); + let stop_flag_reader = stop_flag.clone(); let tx_events_inner = tx_events.clone(); thread::spawn(move || { let mut line = String::new(); let mut json_acc = String::new(); let mut in_json = false; - while reader.read_line(&mut line).is_ok() { + if *stop_flag_reader.lock().unwrap() { + break; + } let trimmed = line.trim(); - if trimmed.is_empty() { line.clear(); continue; } - + if trimmed.is_empty() { + line.clear(); + continue; + } + if !in_json && trimmed.starts_with("{") { in_json = true; json_acc.clear(); @@ -261,51 +801,165 @@ fn tcp_command_worker(addr: String, rx_cmd: Receiver, tx_events: Sender< if in_json { json_acc.push_str(trimmed); - if trimmed == "OK DISCOVER" { + if trimmed.contains("OK DISCOVER") { in_json = false; - let json_clean = json_acc.trim_end_matches("OK DISCOVER").trim(); + let json_clean = + json_acc.split("OK DISCOVER").next().unwrap_or("").trim(); match serde_json::from_str::(json_clean) { - Ok(resp) => { let _ = tx_events_inner.send(InternalEvent::Discovery(resp.signals)); } - Err(e) => { let _ = tx_events_inner.send(InternalEvent::InternalLog(format!("JSON Parse Error (Discover): {}", e))); } + Ok(resp) => { + let _ = tx_events_inner + .send(InternalEvent::Discovery(resp.signals)); + } + Err(e) => { + let _ = + tx_events_inner.send(InternalEvent::InternalLog(format!( + "Discovery JSON Error: {} | Payload: {}", + e, json_clean + ))); + } } json_acc.clear(); - } else if trimmed == "OK TREE" { + } else if trimmed.contains("OK TREE") { in_json = false; - let json_clean = json_acc.trim_end_matches("OK TREE").trim(); + let json_clean = json_acc.split("OK TREE").next().unwrap_or("").trim(); match serde_json::from_str::(json_clean) { - Ok(resp) => { let _ = tx_events_inner.send(InternalEvent::Tree(resp)); } - Err(e) => { let _ = tx_events_inner.send(InternalEvent::InternalLog(format!("JSON Parse Error (Tree): {}", e))); } + Ok(resp) => { + let _ = tx_events_inner.send(InternalEvent::Tree(resp)); + } + Err(e) => { + let _ = tx_events_inner.send(InternalEvent::InternalLog( + format!("Tree JSON Error: {}", e), + )); + } } json_acc.clear(); - } else if trimmed == "OK INFO" { + } else if trimmed.contains("OK INFO") { in_json = false; - let json_clean = json_acc.trim_end_matches("OK INFO").trim(); - let _ = tx_events_inner.send(InternalEvent::NodeInfo(json_clean.to_string())); + let json_clean = json_acc.split("OK INFO").next().unwrap_or("").trim(); + let _ = tx_events_inner + .send(InternalEvent::NodeInfo(json_clean.to_string())); + json_acc.clear(); + } else if trimmed.contains("OK CONFIG") { + in_json = false; + let text = json_acc.split("OK CONFIG").next().unwrap_or("").trim(); + 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 { - let _ = tx_events_inner.send(InternalEvent::CommandResponse(trimmed.to_string())); + if trimmed.starts_with("OK SERVICE_INFO") { + // OK SERVICE_INFO TCP_CTRL:8110 UDP_STREAM:8111 TCP_LOG:8082 STATE:RUNNING + let parts: Vec<&str> = trimmed.split_whitespace().collect(); + let mut udp = String::new(); + let mut log = String::new(); + for p in parts { + if p.starts_with("UDP_STREAM:") { + udp = p.split(':').nth(1).unwrap_or("").to_string(); + } + if p.starts_with("TCP_LOG:") { + log = p.split(':').nth(1).unwrap_or("").to_string(); + } + } + if !udp.is_empty() || !log.is_empty() { + let _ = tx_events_inner.send(InternalEvent::ServiceConfig { + udp_port: udp, + log_port: log, + }); + } + } + let _ = tx_events_inner + .send(InternalEvent::CommandResponse(trimmed.to_string())); } line.clear(); } }); - while let Ok(cmd) = rx_cmd.recv() { + { + let config = shared_config.lock().unwrap(); + if config.version != current_version { + *stop_flag.lock().unwrap() = true; + let _ = tx_events.send(InternalEvent::Disconnected); + break; + } + } if stream.write_all(format!("{}\n", cmd).as_bytes()).is_err() { + let _ = tx_events.send(InternalEvent::Disconnected); break; } + // Small delay to allow server to process and send response + thread::sleep(std::time::Duration::from_millis(100)); } + let _ = tx_events.send(InternalEvent::Disconnected); } thread::sleep(std::time::Duration::from_secs(2)); } } -fn tcp_log_worker(addr: String, tx_events: Sender) { +fn tcp_log_worker(shared_config: Arc>, tx_events: Sender) { + let mut current_version = 0; + let mut current_addr = String::new(); loop { - if let Ok(stream) = TcpStream::connect(&addr) { + { + let config = shared_config.lock().unwrap(); + if config.version != current_version { + current_version = config.version; + current_addr = format!("{}:{}", config.ip, config.log_port); + } + } + if current_addr.is_empty() || current_addr.starts_with(":") { + thread::sleep(std::time::Duration::from_secs(1)); + continue; + } + if let Ok(stream) = TcpStream::connect(¤t_addr) { let mut reader = BufReader::new(stream); let mut line = String::new(); while reader.read_line(&mut line).is_ok() { + if shared_config.lock().unwrap().version != current_version { + break; + } let trimmed = line.trim(); if trimmed.starts_with("LOG ") { let parts: Vec<&str> = trimmed[4..].splitn(2, ' ').collect(); @@ -324,76 +978,293 @@ fn tcp_log_worker(addr: String, tx_events: Sender) { } } -fn udp_worker(port: u16, id_to_meta: Arc>>, traced_data: Arc>>, tx_events: Sender) { - if let Ok(socket) = UdpSocket::bind(format!("0.0.0.0:{}", port)) { - let mut buf = [0u8; 4096]; - let start_time = std::time::Instant::now(); - let mut total_packets = 0u64; +fn recording_worker( + rx: Receiver<[f64; 2]>, + path: String, + signal_name: String, + tx_events: Sender, +) { + let file = match File::create(&path) { + Ok(f) => f, + Err(e) => { + let _ = tx_events.send(InternalEvent::RecordingError( + signal_name, + format!("File Error: {}", e), + )); + return; + } + }; + let schema = Arc::new(Schema::new(vec![ + Field::new("timestamp", DataType::Float64, false), + Field::new("value", DataType::Float64, false), + ])); + let mut writer = match ArrowWriter::try_new( + file, + schema.clone(), + Some(WriterProperties::builder().build()), + ) { + Ok(w) => w, + Err(e) => { + let _ = tx_events.send(InternalEvent::RecordingError( + signal_name, + format!("Parquet Error: {}", e), + )); + return; + } + }; + let (mut t_acc, mut v_acc) = (Vec::with_capacity(1000), Vec::with_capacity(1000)); + while let Ok([t, v]) = rx.recv() { + t_acc.push(t); + v_acc.push(v); + if t_acc.len() >= 1000 { + let batch = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(Float64Array::from(t_acc.clone())), + Arc::new(Float64Array::from(v_acc.clone())), + ], + ) + .unwrap(); + let _ = writer.write(&batch); + t_acc.clear(); + v_acc.clear(); + } + } + if !t_acc.is_empty() { + let batch = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(Float64Array::from(t_acc)), + Arc::new(Float64Array::from(v_acc)), + ], + ) + .unwrap(); + let _ = writer.write(&batch); + } + let _ = writer.close(); +} +fn udp_worker( + shared_config: Arc>, + id_to_meta: Arc>>, + traced_data: Arc>>, + tx_events: Sender, +) { + let mut current_version = 0; + let mut socket: Option = None; + let mut last_seq: Option = None; + let mut last_warning_time = std::time::Instant::now(); + + loop { + let (ver, port) = { + let config = shared_config.lock().unwrap(); + (config.version, config.udp_port.clone()) + }; + if ver != current_version || socket.is_none() { + current_version = ver; + { + let mut base = BASE_TELEM_TS.lock().unwrap(); + *base = None; + } + if port.is_empty() { + socket = None; + continue; + } + let port_num: u16 = port.parse().unwrap_or(8081); + let s = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).ok(); + let mut bound = false; + if let Some(sock) = s { + let _ = sock.set_reuse_address(true); + #[cfg(all(unix, not(target_os = "solaris"), not(target_os = "illumos")))] + let _ = sock.set_reuse_port(true); + let _ = sock.set_recv_buffer_size(10 * 1024 * 1024); + let addr = format!("0.0.0.0:{}", port_num) + .parse::() + .unwrap(); + if sock.bind(&addr.into()).is_ok() { + socket = Some(sock.into()); + bound = true; + } + } + if !bound { + thread::sleep(std::time::Duration::from_secs(5)); + continue; + } + let _ = socket + .as_ref() + .unwrap() + .set_read_timeout(Some(std::time::Duration::from_millis(500))); + last_seq = None; + } + let s = if let Some(sock) = socket.as_ref() { + sock + } else { + thread::sleep(std::time::Duration::from_secs(1)); + continue; + }; + let mut buf = [0u8; 4096]; + let mut total_packets = 0u64; loop { - if let Ok(n) = socket.recv(&mut buf) { + if shared_config.lock().unwrap().version != current_version { + break; + } + if let Ok(n) = s.recv(&mut buf) { total_packets += 1; - if (total_packets % 100) == 0 { + if (total_packets % 500) == 0 { let _ = tx_events.send(InternalEvent::UdpStats(total_packets)); } + if n < 20 { + continue; + } + if u32::from_le_bytes(buf[0..4].try_into().unwrap()) != 0xDA7A57AD { + continue; + } + let seq = u32::from_le_bytes(buf[4..8].try_into().unwrap()); + if let Some(last) = last_seq { + if seq != last + 1 && seq > last { + let _ = tx_events.send(InternalEvent::UdpDropped(seq - last - 1)); + } + } + last_seq = Some(seq); + let count = u32::from_le_bytes(buf[16..20].try_into().unwrap()); - if n < 20 { continue; } - - let mut magic_buf = [0u8; 4]; magic_buf.copy_from_slice(&buf[0..4]); - if u32::from_le_bytes(magic_buf) != 0xDA7A57AD { continue; } - - let mut count_buf = [0u8; 4]; count_buf.copy_from_slice(&buf[16..20]); - let count = u32::from_le_bytes(count_buf); - let mut offset = 20; - let now = start_time.elapsed().as_secs_f64(); - + let mut local_updates: HashMap> = HashMap::new(); + let mut last_values: HashMap = HashMap::new(); let metas = id_to_meta.lock().unwrap(); - let mut data_map = traced_data.lock().unwrap(); + + if metas.is_empty() && count > 0 && last_warning_time.elapsed().as_secs() > 5 { + let _ = tx_events.send(InternalEvent::InternalLog( + "UDP received but Metadata empty. Still discovering?".to_string(), + )); + last_warning_time = std::time::Instant::now(); + } + + // Resolve the base timestamp once per packet, not per signal. + // BASE_TELEM_TS is a global lazy Mutex; locking it inside the + // inner loop at 1 kHz × N_signals/packet was a major bottleneck. + let packet_base_ts: Option = { + let mut guard = BASE_TELEM_TS.lock().unwrap(); + if guard.is_none() { + // Peek at the first signal's timestamp to initialise base + if n >= 20 + 12 { + let first_ts = u64::from_le_bytes( + buf[20 + 4..20 + 12].try_into().unwrap(), + ); + *guard = Some(first_ts); + } + } + *guard + }; for _ in 0..count { - if offset + 8 > n { break; } - let mut id_buf = [0u8; 4]; id_buf.copy_from_slice(&buf[offset..offset+4]); - let id = u32::from_le_bytes(id_buf); - let mut size_buf = [0u8; 4]; size_buf.copy_from_slice(&buf[offset+4..offset+8]); - let size = u32::from_le_bytes(size_buf); - offset += 8; - if offset + size as usize > n { break; } - + if offset + 16 > n { + break; + } + let id = u32::from_le_bytes(buf[offset..offset + 4].try_into().unwrap()); + let ts_raw = + u64::from_le_bytes(buf[offset + 4..offset + 12].try_into().unwrap()); + let size = + u32::from_le_bytes(buf[offset + 12..offset + 16].try_into().unwrap()); + offset += 16; + + if offset + size as usize > n { + break; + } let data_slice = &buf[offset..offset + size as usize]; - if let Some(meta) = metas.get(&id) { - let t = meta.sig_type.as_str(); - let val = match size { - 1 => { if t.contains('u') { data_slice[0] as f64 } else { (data_slice[0] as i8) as f64 } }, - 2 => { - let mut b = [0u8; 2]; b.copy_from_slice(data_slice); - if t.contains('u') { u16::from_le_bytes(b) as f64 } else { i16::from_le_bytes(b) as f64 } - }, - 4 => { - let mut b = [0u8; 4]; b.copy_from_slice(data_slice); - if t.contains("float") { f32::from_le_bytes(b) as f64 } - else if t.contains('u') { u32::from_le_bytes(b) as f64 } - else { i32::from_le_bytes(b) as f64 } - }, - 8 => { - let mut b = [0u8; 8]; b.copy_from_slice(data_slice); - if t.contains("float") { f64::from_le_bytes(b) } - else if t.contains('u') { u64::from_le_bytes(b) as f64 } - else { i64::from_le_bytes(b) as f64 } - }, - _ => 0.0, - }; + let ts_s = if let Some(base) = packet_base_ts { + if ts_raw >= base { (ts_raw - base) as f64 / 1_000_000.0 } else { 0.0 } + } else { + 0.0 + }; - for name in &meta.names { - if let Some(entry) = data_map.get_mut(name) { - entry.values.push_back([now, val]); - if entry.values.len() > 2000 { entry.values.pop_front(); } + if let Some(meta) = metas.get(&id) { + // Do NOT send a channel event per signal — at 1kHz×N this + // floods the event queue and causes the UI spiral slowdown. + let t = meta.sig_type.as_str(); + let type_size = if meta.elements > 0 { size / meta.elements } else { size }; + + for i in 0..meta.elements { + let elem_offset = (i * type_size) as usize; + if elem_offset + type_size as usize > data_slice.len() { break; } + let elem_data = &data_slice[elem_offset..elem_offset + type_size as usize]; + + let val = match type_size { + 1 => { + if t.contains('u') { + elem_data[0] as f64 + } else { + (elem_data[0] as i8) as f64 + } + } + 2 => { + let b = elem_data[0..2].try_into().unwrap(); + if t.contains('u') { + u16::from_le_bytes(b) as f64 + } else { + i16::from_le_bytes(b) as f64 + } + } + 4 => { + let b = elem_data[0..4].try_into().unwrap(); + if t.contains("float") { + f32::from_le_bytes(b) as f64 + } else if t.contains('u') { + u32::from_le_bytes(b) as f64 + } else { + i32::from_le_bytes(b) as f64 + } + } + 8 => { + let b = elem_data[0..8].try_into().unwrap(); + if t.contains("float") { + f64::from_le_bytes(b) + } else if t.contains('u') { + u64::from_le_bytes(b) as f64 + } else { + i64::from_le_bytes(b) as f64 + } + } + _ => 0.0, + }; + + for name in &meta.names { + let target_name = if meta.elements > 1 { + format!("{}[{}]", name, i) + } else { + name.clone() + }; + local_updates + .entry(target_name.clone()) + .or_default() + .push([ts_s, val]); + last_values.insert(target_name, val); } } } offset += size as usize; } + drop(metas); + if !local_updates.is_empty() { + let mut data_map = traced_data.lock().unwrap(); + for (name, new_points) in local_updates { + if let Some(entry) = data_map.get_mut(&name) { + for point in new_points { + entry.values.push_back(point); + if let Some(tx) = &entry.recording_tx { + let _ = tx.send(point); + } + } + if let Some(lv) = last_values.get(&name) { + entry.last_value = *lv; + } + while entry.values.len() > 100000 { + entry.values.pop_front(); + } + } + } + } } } } @@ -406,9 +1277,111 @@ impl eframe::App for MarteDebugApp { InternalEvent::Log(log) => { if !self.log_filters.paused { self.logs.push_back(log); - if self.logs.len() > 2000 { self.logs.pop_front(); } + if self.logs.len() > 2000 { + self.logs.pop_front(); + } } } + InternalEvent::Discovery(signals) => { + let mut metas = self.id_to_meta.lock().unwrap(); + metas.clear(); + for s in &signals { + let meta = metas.entry(s.id).or_insert_with(|| SignalMetadata { + names: Vec::new(), + sig_type: s.sig_type.clone(), + dimensions: s.dimensions, + elements: s.elements, + }); + if !meta.names.contains(&s.name) { + meta.names.push(s.name.clone()); + } + } + self.logs.push_back(LogEntry { + time: Local::now().format("%H:%M:%S").to_string(), + level: "GUI_INFO".to_string(), + message: format!("Discovery complete: {} signals mapped", signals.len()), + }); + } + InternalEvent::Tree(tree) => { + self.app_tree = Some(tree); + } + InternalEvent::NodeInfo(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(); + let entry = data_map.entry(name.clone()).or_insert_with(|| TraceData { + values: VecDeque::with_capacity(10000), + last_value: 0.0, + recording_tx: None, + recording_path: None, + is_monitored, + }); + entry.is_monitored = is_monitored; + self.logs.push_back(LogEntry { + time: Local::now().format("%H:%M:%S").to_string(), + level: "GUI_INFO".to_string(), + message: format!("Trace requested for: {}", name), + }); + } + InternalEvent::ClearTrace(name) => { + let mut data_map = self.traced_signals.lock().unwrap(); + data_map.remove(&name); + for plot in &mut self.plots { + plot.signals.retain(|s| s.source_name != name); + } + } + InternalEvent::UdpStats(count) => { + self.udp_packets = count; + } + InternalEvent::UdpDropped(dropped) => { + self.udp_dropped += dropped as u64; + } + InternalEvent::Connected => { + self.connected = true; + // Wait for connection to stabilize before sending commands + std::thread::sleep(std::time::Duration::from_millis(200)); + let _ = self.tx_cmd.send("SERVICE_INFO".to_string()); + std::thread::sleep(std::time::Duration::from_millis(100)); + let _ = self.tx_cmd.send("TREE".to_string()); + // Wait for TREE response before sending next command + std::thread::sleep(std::time::Duration::from_millis(500)); + let _ = self.tx_cmd.send("DISCOVER".to_string()); + } + InternalEvent::ServiceConfig { udp_port, log_port } => { + let mut changed = false; + if !udp_port.is_empty() && self.config.udp_port != udp_port { + self.config.udp_port = udp_port; + changed = true; + } + if !log_port.is_empty() && self.config.log_port != log_port { + self.config.log_port = log_port; + changed = true; + } + if changed { + self.config.version += 1; + *self.shared_config.lock().unwrap() = self.config.clone(); + self.logs.push_back(LogEntry { + time: Local::now().format("%H:%M:%S").to_string(), + level: "GUI_INFO".to_string(), + message: format!("Config updated from server: UDP={}, LOG={}", self.config.udp_port, self.config.log_port), + }); + } + } + InternalEvent::Disconnected => { + self.connected = false; + } InternalEvent::InternalLog(msg) => { self.logs.push_back(LogEntry { time: Local::now().format("%H:%M:%S").to_string(), @@ -416,250 +1389,1615 @@ impl eframe::App for MarteDebugApp { message: msg, }); } - InternalEvent::Discovery(signals) => { - let mut metas = self.id_to_meta.lock().unwrap(); - metas.clear(); - for s in &signals { - let meta = metas.entry(s.id).or_insert_with(|| SignalMetadata { names: Vec::new(), sig_type: s.sig_type.clone() }); - if !meta.names.contains(&s.name) { meta.names.push(s.name.clone()); } - } - self.signals = signals; - } - InternalEvent::Tree(tree) => { - self.app_tree = Some(tree); - } - InternalEvent::NodeInfo(info) => { - self.node_info = info; - } - InternalEvent::TraceRequested(name) => { - let mut data_map = self.traced_signals.lock().unwrap(); - data_map.entry(name).or_insert_with(|| TraceData { values: VecDeque::with_capacity(2000) }); - } - InternalEvent::ClearTrace(name) => { - let mut data_map = self.traced_signals.lock().unwrap(); - data_map.remove(&name); - } InternalEvent::CommandResponse(resp) => { + // Resolve pending MSG history entry + if let Some(idx) = self.pending_msg_idx { + if resp.contains("MSG") { + if let Some(entry) = self.message_history.get_mut(idx) { + entry.response = resp.clone(); + entry.status = if resp.starts_with("OK") { + MsgStatus::Success + } else { + MsgStatus::Failure + }; + } + self.pending_msg_idx = None; + } + } self.logs.push_back(LogEntry { time: Local::now().format("%H:%M:%S").to_string(), level: "CMD_RESP".to_string(), message: resp, }); } - InternalEvent::UdpStats(count) => { - self.udp_packets = count; + InternalEvent::ConfigResponse(text) => { + self.app_config_text = convert_config_json(&text); } - InternalEvent::Connected => { - let _ = self.tx_cmd.send("TREE".to_string()); - let _ = self.tx_cmd.send("DISCOVER".to_string()); + 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) { + let (tx, rx) = unbounded(); + entry.recording_tx = Some(tx); + entry.recording_path = Some(path.clone()); + let tx_err = self.internal_tx.clone(); + thread::spawn(move || { + recording_worker(rx, path, name, tx_err); + }); + } + } + InternalEvent::RecordingError(name, err) => { + self.logs.push_back(LogEntry { + time: Local::now().format("%H:%M:%S").to_string(), + level: "REC_ERROR".to_string(), + message: format!("{}: {}", name, err), + }); } } } - if let Some(dialog) = &mut self.forcing_dialog { - let mut close = false; - egui::Window::new("Force Signal").collapsible(false).resizable(false).show(ctx, |ui| { - ui.label(format!("Signal: {}", dialog.signal_path)); - ui.label(format!("Type: {} (Dims: {}, Elems: {})", dialog.sig_type, dialog.dims, dialog.elems)); - ui.horizontal(|ui| { - ui.label("Value:"); - ui.text_edit_singleline(&mut dialog.value); - }); - ui.horizontal(|ui| { - if ui.button("Apply Force").clicked() { - let _ = self.tx_cmd.send(format!("FORCE {} {}", dialog.signal_path, dialog.value)); - self.forced_signals.insert(dialog.signal_path.clone(), dialog.value.clone()); - close = true; - } - if ui.button("Cancel").clicked() { close = true; } - }); - }); - if close { self.forcing_dialog = None; } + // 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)); + } } - egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + + if self.scope.enabled { + self.apply_trigger_logic(); + } + + if let Some(dragged_name) = + ctx.data_mut(|d| d.get_temp::(egui::Id::new("drag_signal"))) + { + egui::Area::new(egui::Id::new("drag_ghost")) + .fixed_pos(ctx.input(|i| i.pointer.hover_pos().unwrap_or(egui::Pos2::ZERO))) + .order(egui::Order::Tooltip) + .show(ctx, |ui| { + ui.group(|ui| { + ui.label(format!("📈 {}", dragged_name)); + }); + }); + } + + if let Some(dialog) = &mut self.forcing_dialog { + let mut close = false; + egui::Window::new("Force Signal").show(ctx, |ui| { + ui.label(&dialog.signal_path); + ui.text_edit_singleline(&mut dialog.value); + ui.horizontal(|ui| { + if ui.button("Apply").clicked() { + let _ = self + .tx_cmd + .send(format!("FORCE {} {}", dialog.signal_path, dialog.value)); + self.forced_signals + .insert(dialog.signal_path.clone(), dialog.value.clone()); + close = true; + } + if ui.button("Cancel").clicked() { + close = true; + } + }); + }); + if close { + self.forcing_dialog = None; + } + } + + 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| { + ui.label(&dialog.signal_path); + ui.horizontal(|ui| { + ui.label("Period (ms):"); + ui.text_edit_singleline(&mut dialog.period_ms); + }); + ui.horizontal(|ui| { + if ui.button("Apply").clicked() { + let period = dialog.period_ms.parse::().unwrap_or(100); + let _ = self + .tx_cmd + .send(format!("MONITOR SIGNAL {} {}", dialog.signal_path, period)); + let _ = self.tx_cmd.send("DISCOVER".to_string()); + + // Check if it's an array signal to add all elements to view + let mut elements = 1; + if let Some(tree) = &self.app_tree { + // Helper to find item in tree + fn find_item<'a>(item: &'a TreeItem, target: &str, current: &str) -> Option<&'a TreeItem> { + let path = if current.is_empty() { item.name.clone() } else { format!("{}.{}", current, item.name) }; + if path == target || (current.is_empty() && item.name == "Root" && target.is_empty()) { return Some(item); } + if let Some(children) = &item.children { + for child in children { + if let Some(found) = find_item(child, target, &path) { return Some(found); } + } + } + None + } + if let Some(found) = find_item(tree, &dialog.signal_path, "") { + elements = found.elements.unwrap_or(1); + } + } + + if elements > 1 { + for i in 0..elements { + let elem_path = format!("{}[{}]", dialog.signal_path, i); + let _ = self.internal_tx.send(InternalEvent::TraceRequested(elem_path, true)); + } + } else { + let _ = self + .internal_tx + .send(InternalEvent::TraceRequested(dialog.signal_path.clone(), true)); + } + close = true; + } + if ui.button("Cancel").clicked() { + close = true; + } + }); + }); + if close { + self.monitoring_dialog = None; + } + } + + if self.message_dialog.is_some() { + let mut dialog = self.message_dialog.take().unwrap(); + let mut close = false; + let objects = self.get_all_objects(); + egui::Window::new("Send MARTe Message").show(ctx, |ui| { + egui::Grid::new("msg_grid").num_columns(2).show(ui, |ui| { + ui.label("Destination:"); + egui::ComboBox::from_id_salt("dest_combo") + .selected_text(&dialog.destination) + .width(200.0) + .show_ui(ui, |ui| { + for obj in objects { + ui.selectable_value(&mut dialog.destination, obj.clone(), obj); + } + }); + ui.end_row(); + + ui.label("Function:"); + ui.text_edit_singleline(&mut dialog.function); + ui.end_row(); + + ui.label("Payload:"); + ui.vertical(|ui| { + ui.text_edit_multiline(&mut dialog.payload); + ui.label(egui::RichText::new("Format: Key = Value (one per line)").small().weak()); + }); + ui.end_row(); + + ui.label("Wait Reply:"); + ui.checkbox(&mut dialog.expect_reply, ""); + ui.end_row(); + }); + + ui.horizontal(|ui| { + if ui.button("🚀 Send").clicked() { + let wait = if dialog.expect_reply { "1" } else { "0" }; + let encoded_payload = dialog.payload.replace('\n', "\\n"); + let cmd = format!( + "MSG {} {} {} {}", + dialog.destination, dialog.function, wait, encoded_payload + ); + let idx = self.message_history.len(); + self.message_history.push(MessageHistoryEntry { + time: Local::now().format("%H:%M:%S").to_string(), + destination: dialog.destination.clone(), + function: dialog.function.clone(), + payload: dialog.payload.clone(), + wait_reply: dialog.expect_reply, + raw_cmd: cmd.clone(), + response: String::new(), + status: MsgStatus::Unknown, + }); + self.pending_msg_idx = Some(idx); + let _ = self.tx_cmd.send(cmd); + close = true; + } + if ui.button("Cancel").clicked() { + close = true; + } + }); + }); + if !close { + self.message_dialog = Some(dialog); + } + } + + if let Some((p_idx, s_idx)) = self.style_editor { + let mut close = false; + egui::Window::new("Signal Style").show(ctx, |ui| { + if let Some(plot) = self.plots.get_mut(p_idx) { + if let Some(sig) = plot.signals.get_mut(s_idx) { + ui.horizontal(|ui| { + ui.label("Label:"); + ui.text_edit_singleline(&mut sig.label); + }); + ui.horizontal(|ui| { + ui.label("Unit:"); + ui.text_edit_singleline(&mut sig.unit); + }); + ui.horizontal(|ui| { + ui.label("Color:"); + let mut color = sig.color.to_array(); + if ui + .color_edit_button_srgba_unmultiplied(&mut color) + .changed() + { + sig.color = egui::Color32::from_rgba_unmultiplied( + color[0], color[1], color[2], color[3], + ); + } + }); + ui.horizontal(|ui| { + ui.label("Gain:"); + ui.add(egui::DragValue::new(&mut sig.gain).speed(0.1)); + }); + ui.horizontal(|ui| { + ui.label("Offset:"); + ui.add(egui::DragValue::new(&mut sig.offset).speed(1.0)); + }); + if ui.button("Close").clicked() { + close = true; + } + } + } + }); + if close { + self.style_editor = None; + } + } + + 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"); - ui.toggle_value(&mut self.show_right_panel, "⚙ Debug"); + ui.toggle_value(&mut self.show_right_panel, "📊 Signals"); + ui.toggle_value(&mut self.show_message_history, "💬 Msgs"); ui.toggle_value(&mut self.show_bottom_panel, "📜 Logs"); ui.separator(); - ui.heading("MARTe2 Debug Explorer"); - ui.separator(); - if ui.button("🔄 Refresh").clicked() { - let _ = self.tx_cmd.send("TREE".to_string()); - let _ = self.tx_cmd.send("DISCOVER".to_string()); + if ui.button("➕ Plot").clicked() { + self.plots.push(PlotInstance { + id: format!("Plot {}", self.plots.len() + 1), + plot_type: PlotType::Normal, + signals: Vec::new(), + auto_bounds: true, + max_points: 5000, + follow: true, + reset_view: false, + }); } ui.separator(); - if self.is_paused { - if ui.button("▶ Resume").clicked() { - let _ = self.tx_cmd.send("RESUME".to_string()); - self.is_paused = false; - } + let (btn_text, btn_color) = if self.is_breaking { + ("▶ Resume App", egui::Color32::GREEN) } else { - if ui.button("⏸ Pause").clicked() { - let _ = self.tx_cmd.send("PAUSE".to_string()); - self.is_paused = true; + ("⏸ Pause App", egui::Color32::YELLOW) + }; + if ui + .button(egui::RichText::new(btn_text).color(btn_color)) + .clicked() + { + self.is_breaking = !self.is_breaking; + let _ = self.tx_cmd.send(if self.is_breaking { + "PAUSE".to_string() + } 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"); + if self.scope.enabled { + egui::ComboBox::from_id_salt("window_size") + .selected_text(format!("{}ms", self.scope.window_ms)) + .show_ui(ui, |ui| { + for ms in [ + 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0, 5000.0, + 10000.0, + ] { + ui.selectable_value( + &mut self.scope.window_ms, + ms, + format!("{}ms", ms), + ); + } + }); + ui.selectable_value(&mut self.scope.mode, AcquisitionMode::FreeRun, "Free"); + ui.selectable_value(&mut self.scope.mode, AcquisitionMode::Triggered, "Trig"); + if self.scope.mode == AcquisitionMode::FreeRun { + if ui + .button(if self.scope.paused { + "▶ Resume" + } else { + "⏸ Pause" + }) + .clicked() + { + self.scope.paused = !self.scope.paused; + } + } else { + if ui + .button(if self.scope.is_armed { + "🔴 Armed" + } else { + "⚪ Single" + }) + .clicked() + { + self.scope.is_armed = true; + self.scope.trigger_active = false; + } + ui.menu_button("⚙ Trig", |ui| { + egui::Grid::new("trig").num_columns(2).show(ui, |ui| { + ui.label("Source:"); + ui.text_edit_singleline(&mut self.scope.trigger_source); + ui.end_row(); + ui.label("Edge:"); + egui::ComboBox::from_id_salt("edge") + .selected_text(format!("{:?}", self.scope.trigger_edge)) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.scope.trigger_edge, + TriggerEdge::Rising, + "Rising", + ); + ui.selectable_value( + &mut self.scope.trigger_edge, + TriggerEdge::Falling, + "Falling", + ); + ui.selectable_value( + &mut self.scope.trigger_edge, + TriggerEdge::Both, + "Both", + ); + }); + ui.end_row(); + ui.label("Thresh:"); + ui.add( + egui::DragValue::new(&mut self.scope.trigger_threshold) + .speed(0.1), + ); + ui.end_row(); + ui.label("Pre %:"); + ui.add(egui::Slider::new( + &mut self.scope.pre_trigger_percent, + 0.0..=100.0, + )); + ui.end_row(); + ui.label("Type:"); + ui.selectable_value( + &mut self.scope.trigger_type, + TriggerType::Single, + "Single", + ); + ui.selectable_value( + &mut self.scope.trigger_type, + TriggerType::Continuous, + "Cont", + ); + ui.end_row(); + }); + }); } } + ui.separator(); + ui.menu_button("🔌 Conn", |ui| { + egui::Grid::new("conn_grid").num_columns(2).show(ui, |ui| { + ui.label("IP:"); + ui.text_edit_singleline(&mut self.config.ip); + ui.end_row(); + ui.label("Control:"); + ui.text_edit_singleline(&mut self.config.tcp_port); + ui.end_row(); + ui.label("Telemetry (Auto):"); + ui.label(&self.config.udp_port); + ui.end_row(); + ui.label("Logs (Auto):"); + ui.label(&self.config.log_port); + ui.end_row(); + }); + if ui.button("🔄 Apply").clicked() { + self.config.version += 1; + *self.shared_config.lock().unwrap() = self.config.clone(); + ui.close_menu(); + } + if ui.button("📡 Re-Discover").clicked() { + let _ = self.tx_cmd.send("DISCOVER".to_string()); + ui.close_menu(); + } + if ui.button("❌ Off").clicked() { + self.config.version += 1; + let mut cfg = self.config.clone(); + cfg.ip = "".to_string(); + *self.shared_config.lock().unwrap() = cfg; + ui.close_menu(); + } + }); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label(format!("UDP Packets: {}", self.udp_packets)); + if ui.button("✉ Send Msg").clicked() { + self.message_dialog = Some(MessageDialog { + destination: "".to_string(), + function: "".to_string(), + payload: "".to_string(), + expect_reply: false, + }); + } + ui.separator(); + ui.label(format!( + "UDP: OK[{}] DROP[{}]", + self.udp_packets, self.udp_dropped + )); }); }); }); - if self.show_bottom_panel { - egui::TopBottomPanel::bottom("log_panel").resizable(true).default_height(200.0).show(ctx, |ui| { - ui.horizontal(|ui| { - ui.heading("System Logs"); - ui.separator(); - ui.label("Filter:"); - ui.text_edit_singleline(&mut self.log_filters.content_regex); - ui.checkbox(&mut self.log_filters.show_debug, "Debug"); - ui.checkbox(&mut self.log_filters.show_info, "Info"); - ui.checkbox(&mut self.log_filters.show_warning, "Warn"); - ui.checkbox(&mut self.log_filters.show_error, "Error"); - ui.separator(); - ui.toggle_value(&mut self.log_filters.paused, "⏸ Pause Logs"); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.button("🗑 Clear").clicked() { self.logs.clear(); } - }); - }); - ui.separator(); - - let regex = if !self.log_filters.content_regex.is_empty() { Regex::new(&self.log_filters.content_regex).ok() } else { None }; - - egui::ScrollArea::vertical() - .stick_to_bottom(true) - .auto_shrink([false, false]) - .show(ui, |ui| { - for log in &self.logs { - let show = match log.level.as_str() { - "Debug" => self.log_filters.show_debug, - "Information" => self.log_filters.show_info, - "Warning" => self.log_filters.show_warning, - "FatalError" | "OSError" | "ParametersError" => self.log_filters.show_error, - _ => true, - }; - if !show { continue; } - if let Some(re) = ®ex { if !re.is_match(&log.message) && !re.is_match(&log.level) { continue; } } - - let color = match log.level.as_str() { - "FatalError" | "OSError" | "ParametersError" => egui::Color32::from_rgb(255, 100, 100), - "Warning" => egui::Color32::from_rgb(255, 255, 100), - "Information" => egui::Color32::from_rgb(100, 255, 100), - "Debug" => egui::Color32::from_rgb(100, 100, 255), - "GUI_ERROR" => egui::Color32::from_rgb(255, 50, 255), - "CMD_RESP" => egui::Color32::from_rgb(255, 255, 255), - _ => egui::Color32::WHITE, - }; - - ui.scope(|ui| { - ui.horizontal(|ui| { - ui.label(egui::RichText::new(&log.time).monospace().color(egui::Color32::GRAY)); - ui.label(egui::RichText::new(format!("[{}]", log.level)).color(color).strong()); - ui.add(egui::Label::new(&log.message).wrap()); - ui.allocate_space(egui::vec2(ui.available_width(), 0.0)); - }); - }); + if self.show_left_panel { + egui::SidePanel::left("left") + .resizable(true) + .width_range(200.0..=500.0) + .show(ctx, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + if let Some(tree) = self.app_tree.clone() { + self.render_tree(ui, &tree, "".to_string()); } }); - }); - } - - if self.show_left_panel { - egui::SidePanel::left("signals_panel").resizable(true).width_range(300.0..=600.0).show(ctx, |ui| { - ui.heading("Application Tree"); - ui.separator(); - egui::ScrollArea::vertical().id_salt("tree_scroll").show(ui, |ui| { - if let Some(tree) = self.app_tree.clone() { - self.render_tree(ui, &tree, "".to_string()); - } else { - ui.label("Connecting to server..."); - } }); - - ui.separator(); - ui.heading("Node Information"); - egui::ScrollArea::vertical().id_salt("info_scroll").show(ui, |ui| { - if self.node_info.is_empty() { - ui.label("Click 'Info' on a node to view details"); - } else { - ui.add(egui::Label::new(egui::RichText::new(&self.node_info).monospace()).wrap()); - } - }); - }); } if self.show_right_panel { - egui::SidePanel::right("debug_panel").resizable(true).width_range(200.0..=400.0).show(ctx, |ui| { - ui.heading("Active Controls"); - ui.separator(); - ui.label(egui::RichText::new("Forced Signals").strong()); - egui::ScrollArea::vertical().id_salt("forced_scroll").show(ui, |ui| { - let mut to_update = None; - let mut to_remove = None; - for (path, val) in &self.forced_signals { - ui.horizontal(|ui| { - if ui.selectable_label(false, format!("{}: {}", path, val)).clicked() { - to_update = Some(path.clone()); - } - if ui.button("❌").clicked() { - let _ = self.tx_cmd.send(format!("UNFORCE {}", path)); - to_remove = Some(path.clone()); - } - }); - } - if let Some(p) = to_remove { self.forced_signals.remove(&p); } - if let Some(p) = to_update { - self.forcing_dialog = Some(ForcingDialog { - signal_path: p.clone(), sig_type: "Unknown".to_string(), dims: 0, elems: 1, - value: self.forced_signals.get(&p).unwrap().clone(), - }); - } - }); + egui::SidePanel::right("right") + .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.separator(); - ui.label(egui::RichText::new("Traced Signals").strong()); - egui::ScrollArea::vertical().id_salt("traced_scroll").show(ui, |ui| { + 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(); - for key in names { + let mut open_info_signal: Option<(String, f64)> = None; + egui::ScrollArea::vertical() + .id_salt("traced_scroll") + .show(ui, |ui| { + for key in names { + 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 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(); + } + } + }); + 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(); + for (path, val) in &self.forced_signals { ui.horizontal(|ui| { - ui.label(&key); + ui.label(format!("{}: {}", path, val)); if ui.button("❌").clicked() { - let _ = self.tx_cmd.send(format!("TRACE {} 0", key)); - let _ = self.internal_tx.send(InternalEvent::ClearTrace(key)); + let _ = self.tx_cmd.send(format!("UNFORCE {}", path)); + to_delete.push(path.to_owned()); } }); } + for key in to_delete.iter() { + 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| { + ui.heading("Message History"); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.small_button("🗑 Clear").clicked() { + self.message_history.clear(); + self.pending_msg_idx = None; + } + }); + }); + if self.message_history.is_empty() { + ui.label(egui::RichText::new("No messages sent yet").italics().color(egui::Color32::GRAY)); + } else { + egui::ScrollArea::vertical() + .id_salt("msg_history_scroll") + .max_height(300.0) + .auto_shrink([false, true]) + .show(ui, |ui| { + let mut resend_cmd: Option = None; + let mut edit_dialog: Option = None; + for entry in self.message_history.iter().rev() { + let (status_icon, status_color) = match entry.status { + MsgStatus::Success => ("✔", egui::Color32::from_rgb(100, 220, 100)), + MsgStatus::Failure => ("✘", egui::Color32::from_rgb(255, 100, 100)), + MsgStatus::Unknown => ("…", egui::Color32::from_rgb(255, 220, 50)), + }; + ui.group(|ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new(status_icon).color(status_color).strong()); + ui.label(egui::RichText::new(&entry.time).color(egui::Color32::GRAY).monospace().small()); + }); + ui.label(format!("→ {}.{}", entry.destination, entry.function)); + if !entry.payload.is_empty() { + ui.label(egui::RichText::new(&entry.payload).monospace().small().color(egui::Color32::from_rgb(180, 180, 255))); + } + if !entry.response.is_empty() { + ui.label(egui::RichText::new(&entry.response).small().color(status_color)); + } + ui.horizontal(|ui| { + if ui.small_button("↩ Resend").clicked() { + resend_cmd = Some(entry.raw_cmd.clone()); + } + if ui.small_button("✏ Edit").clicked() { + edit_dialog = Some(MessageDialog { + destination: entry.destination.clone(), + function: entry.function.clone(), + payload: entry.payload.clone(), + expect_reply: entry.wait_reply, + }); + } + }); + }); + } + if let Some(cmd) = resend_cmd { + let idx = self.message_history.len(); + // find the matching history entry to clone metadata + if let Some(orig) = self.message_history.iter().find(|e| e.raw_cmd == cmd) { + self.message_history.push(MessageHistoryEntry { + time: Local::now().format("%H:%M:%S").to_string(), + destination: orig.destination.clone(), + function: orig.function.clone(), + payload: orig.payload.clone(), + wait_reply: orig.wait_reply, + raw_cmd: cmd.clone(), + response: String::new(), + status: MsgStatus::Unknown, + }); + } + self.pending_msg_idx = Some(idx); + let _ = self.tx_cmd.send(cmd); + } + if let Some(dialog) = edit_dialog { + self.message_dialog = Some(dialog); + } + }); + } + } + }); + } + + if self.show_bottom_panel { + egui::TopBottomPanel::bottom("log_panel") + .resizable(true) + .default_height(150.0) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.heading("Logs"); + ui.separator(); + ui.checkbox(&mut self.log_filters.show_debug, "Debug"); + ui.checkbox(&mut self.log_filters.show_info, "Info"); + ui.checkbox(&mut self.log_filters.show_warning, "Warn"); + ui.checkbox(&mut self.log_filters.show_error, "Error"); + ui.separator(); + ui.label("Filter:"); + ui.text_edit_singleline(&mut self.log_filters.content_regex); + if ui.button("🗑 Clear").clicked() { + self.logs.clear(); + } + }); + ui.separator(); + let regex = if !self.log_filters.content_regex.is_empty() { + Regex::new(&self.log_filters.content_regex).ok() + } else { + None + }; + egui::ScrollArea::vertical() + .stick_to_bottom(true) + .auto_shrink([false, false]) + .show(ui, |ui| { + for log in &self.logs { + let show = match log.level.as_str() { + "Debug" => self.log_filters.show_debug, + "Information" | "GUI_INFO" | "GUI_WARN" | "CMD_RESP" => { + self.log_filters.show_info + } + "Warning" => self.log_filters.show_warning, + "FatalError" | "OSError" | "ParametersError" | "GUI_ERROR" + | "REC_ERROR" => self.log_filters.show_error, + _ => true, + }; + if !show { + continue; + } + if let Some(re) = ®ex { + if !re.is_match(&log.message) && !re.is_match(&log.level) { + continue; + } + } + let color = match log.level.as_str() { + "FatalError" | "OSError" | "ParametersError" | "GUI_ERROR" + | "REC_ERROR" => egui::Color32::from_rgb(255, 100, 100), + "Warning" | "GUI_WARN" => { + egui::Color32::from_rgb(255, 255, 100) + } + "Information" | "GUI_INFO" => { + egui::Color32::from_rgb(100, 255, 100) + } + "Debug" => egui::Color32::from_rgb(100, 100, 255), + "CMD_RESP" => egui::Color32::from_rgb(255, 255, 255), + _ => egui::Color32::WHITE, + }; + ui.horizontal_wrapped(|ui| { + ui.label( + egui::RichText::new(&log.time) + .color(egui::Color32::GRAY) + .monospace(), + ); + ui.label( + egui::RichText::new(format!("[{}]", log.level)) + .color(color) + .strong(), + ); + ui.add(egui::Label::new(&log.message).wrap()); + }); + } + }); }); - }); } egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("Oscilloscope"); - let plot = Plot::new("traces_plot") - .legend(egui_plot::Legend::default()) - .auto_bounds_x() - .auto_bounds_y() - .y_axis_min_width(4.0); - - plot.show(ui, |plot_ui| { - let data_map = self.traced_signals.lock().unwrap(); - for (name, data) in data_map.iter() { - let points: PlotPoints = data.values.iter().cloned().collect(); - plot_ui.line(Line::new(points).name(name)); - } + ui.horizontal(|ui| { + ui.selectable_value(&mut self.active_main_tab, MainTab::Plots, "📈 Plots"); + ui.selectable_value(&mut self.active_main_tab, MainTab::Config, "⚙ Config"); }); - }); + ui.separator(); + if self.active_main_tab == MainTab::Config { + ui.horizontal(|ui| { + if ui.button("🔄 Refresh").clicked() { + let _ = self.tx_cmd.send("CONFIG".to_string()); + } + ui.label(egui::RichText::new("Application Configuration").color(egui::Color32::GRAY)); + }); + ui.separator(); + egui::ScrollArea::both() + .auto_shrink([false, false]) + .show(ui, |ui| { + if self.app_config_text.is_empty() { + ui.label(egui::RichText::new("Press Refresh to load the configuration").italics().color(egui::Color32::GRAY)); + } else { + ui.add( + egui::Label::new( + egui::RichText::new(&self.app_config_text).monospace(), + ) + .selectable(true), + ); + } + }); + return; + } + let n_plots = self.plots.len(); + if n_plots > 0 { + let plot_height = ui.available_height() / n_plots as f32; + let mut to_remove = None; + let mut current_range = None; + for (p_idx, plot_inst) in self.plots.iter_mut().enumerate() { + 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.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); + } + }); + let mut plot = Plot::new(&plot_inst.id) + .height(plot_height - 40.0) + .show_axes([true, true]); + plot = plot.x_axis_formatter(|mark, _range| { + let val = mark.value; + let hours = (val / 3600.0) as u32; + let mins = ((val % 3600.0) / 60.0) as u32; + let secs = val % 60.0; + format!("{:02}:{:02}:{:05.2}", hours, mins, secs) + }); + + let data_map = self.traced_signals.lock().unwrap(); + let mut latest_t = 0.0; + for sig_cfg in &plot_inst.signals { + if let Some(data) = data_map.get(&sig_cfg.source_name) { + if let Some(last) = data.values.back() { + if last[0] > latest_t { + latest_t = last[0]; + } + } + } + } + + if self.scope.enabled { + let window_s = self.scope.window_ms / 1000.0; + let center_t = if self.scope.mode == AcquisitionMode::Triggered { + if self.scope.trigger_active { + self.scope.last_trigger_time + } else { + latest_t + } + } else { + latest_t + }; + let x_min = + center_t - (self.scope.pre_trigger_percent / 100.0) * window_s; + plot = plot.include_x(x_min).include_x(x_min + window_s); + if !self.scope.paused { + plot = plot.auto_bounds(egui::Vec2b::new(true, true)); + } + } else { + if let Some(range) = self.shared_x_range { + if !plot_inst.auto_bounds { + plot = plot.include_x(range[0]).include_x(range[1]); + } + } + if plot_inst.auto_bounds { + plot = plot.auto_bounds(egui::Vec2b::new(true, true)); + } + } + + // 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(); + plot_ui.set_plot_bounds(PlotBounds::from_min_max( + [range[0], bounds.min()[1]], + [range[1], bounds.max()[1]], + )); + } + } + if self.scope.enabled + && self.scope.mode == AcquisitionMode::Triggered + && self.scope.trigger_active + { + plot_ui.vline( + VLine::new(self.scope.last_trigger_time) + .color(egui::Color32::YELLOW) + .style(LineStyle::Dashed { length: 5.0 }), + ); + } + + 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(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) + + (if final_v > 0.5 { 1.0 } else { 0.0 }); + } + [*t, final_v] + }); + plot_ui.line( + Line::new(PlotPoints::from_iter(points_iter)) + .name(&sig_cfg.label) + .color(sig_cfg.color), + ); + } + } + if p_idx == 0 || current_range.is_none() { + let b = plot_ui.plot_bounds(); + current_range = Some([b.min()[0], b.max()[0]]); + } + }); + drop(data_map); + + if plot_resp.response.hovered() && ctx.input(|i| i.pointer.any_released()) { + if let Some(dropped) = + ctx.data_mut(|d| d.get_temp::(egui::Id::new("drag_signal"))) + { + let color = Self::next_color(plot_inst.signals.len()); + plot_inst.signals.push(SignalPlotConfig { + source_name: dropped.clone(), + label: dropped.clone(), + unit: "".to_string(), + color, + line_style: LineStyle::Solid, + marker_type: MarkerType::None, + gain: 1.0, + offset: 0.0, + }); + ctx.data_mut(|d| { + d.remove_temp::(egui::Id::new("drag_signal")) + }); + } + } + if plot_resp.response.dragged() + || ctx.input(|i| i.smooth_scroll_delta.y != 0.0) + { + 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(); + } + ui.separator(); + let mut sig_to_remove = None; + for (s_idx, sig) in plot_inst.signals.iter().enumerate() { + ui.horizontal(|ui| { + ui.label(&sig.label); + if ui.button("🎨 Style").clicked() { + self.style_editor = Some((p_idx, s_idx)); + ui.close_menu(); + } + if ui.button("❌ Remove").clicked() { + sig_to_remove = Some(s_idx); + ui.close_menu(); + } + }); + } + if let Some(idx) = sig_to_remove { + plot_inst.signals.remove(idx); + } + }); + }); + } + if let Some(idx) = to_remove { + self.plots.remove(idx); + } + if !self.scope.enabled { + if let Some(range) = current_range { + if self.shared_x_range.is_none() { + self.shared_x_range = Some(range); + } + } + } + } else { + ui.centered_and_justified(|ui| { + ui.label("Add a plot panel to begin analysis"); + }); + } + }); ctx.request_repaint_after(std::time::Duration::from_millis(16)); } } +#[cfg(test)] +mod tests { + use super::*; + + /// Parse MARTe2 config text into a serde_json Value tree so tests can do + /// structural comparisons without caring about whitespace or key ordering. + /// + /// Rules: + /// `[+]Name = {` → start of a named block + /// `}` → close current block + /// `Name = Value` → leaf key-value (value stripped of surrounding quotes) + /// `Name = {value}` (e.g. `Functions = {GAM1}`) → leaf, not a block + fn parse_marte_config(text: &str) -> serde_json::Value { + let mut stack: Vec> = + vec![serde_json::Map::new()]; + let mut name_stack: Vec = vec![]; + for line in text.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") { + continue; + } + if trimmed == "}" { + if stack.len() > 1 { + let child = stack.pop().unwrap(); + let name = name_stack.pop().unwrap(); + stack.last_mut().unwrap().insert(name, serde_json::Value::Object(child)); + } + continue; + } + let clean = trimmed.trim_start_matches('+'); + if let Some(eq_pos) = clean.find('=') { + let key = clean[..eq_pos].trim().to_string(); + let val = clean[eq_pos + 1..].trim(); + // A block opens only if the value is exactly `{` + if val == "{" { + stack.push(serde_json::Map::new()); + name_stack.push(key); + } else { + let val_clean = val.trim_matches('"').to_string(); + stack.last_mut().unwrap().insert(key, serde_json::Value::String(val_clean)); + } + } + } + serde_json::Value::Object(stack.remove(0)) + } + + /// Recursively assert that every object key with a `Class` value in `expected` + /// also exists with the same `Class` in `actual`. Missing non-Class keys are + /// tolerated because `ExportData()` does not re-emit config-file-only fields + /// (e.g. signal configurations, Frequency, Samples). + fn assert_classes_match( + expected: &serde_json::Value, + actual: &serde_json::Value, + path: &str, + ) { + use serde_json::Value; + if let (Value::Object(exp_map), Value::Object(act_map)) = (expected, actual) { + if let Some(Value::String(exp_class)) = exp_map.get("Class") { + let act_class = act_map + .get("Class") + .and_then(|v| v.as_str()) + .unwrap_or(""); + assert_eq!( + exp_class.as_str(), + act_class, + "Class mismatch at '{}': expected '{}', got '{}'", + path, + exp_class, + act_class + ); + } + for (key, exp_child) in exp_map { + if key == "Class" { + continue; + } + if let Some(act_child) = act_map.get(key) { + let child_path = if path.is_empty() { + key.clone() + } else { + format!("{}.{}", path, key) + }; + assert_classes_match(exp_child, act_child, &child_path); + } + // Extra keys added by MARTe2 runtime are allowed + } + } + } + + // ----- Unit tests for the JSON → MARTe2 converter ----- + + #[test] + fn test_convert_basic_structure() { + let json = r#"{"App":{"Class":"RealTimeApplication","Functions":{"Class":"ReferenceContainer","GAM1":{"Class":"IOGAM"}}}}"#; + let out = convert_config_json(json); + let parsed = parse_marte_config(&out); + + assert_eq!( + parsed["App"]["Class"], + serde_json::Value::String("RealTimeApplication".into()) + ); + assert_eq!( + parsed["App"]["Functions"]["Class"], + serde_json::Value::String("ReferenceContainer".into()) + ); + assert_eq!( + parsed["App"]["Functions"]["GAM1"]["Class"], + serde_json::Value::String("IOGAM".into()) + ); + // Objects with Class must get + prefix + assert!(out.contains("+App = {"), "top-level object missing +"); + assert!(out.contains(" +Functions = {"), "nested object with Class missing +"); + assert!(out.contains(" +GAM1 = {"), "leaf object with Class missing +"); + } + + #[test] + fn test_no_plus_for_blocks_without_class() { + let json = r#"{"App":{"Class":"RealTimeApplication","Signals":{"Counter":{"Type":"uint32"}}}}"#; + let out = convert_config_json(json); + // Signals block has no Class → no + prefix + assert!(out.contains(" Signals = {"), "plain block should not have +"); + assert!(!out.contains(" +Signals"), "plain block must not have + prefix"); + } + + #[test] + fn test_class_emitted_first() { + let json = r#"{"App":{"ZZZKey":"last","Class":"RealTimeApplication","AAA":"first_alpha"}}"#; + let out = convert_config_json(json); + let class_pos = out.find("Class = RealTimeApplication").unwrap(); + let zzz_pos = out.find("ZZZKey = last").unwrap(); + assert!( + class_pos < zzz_pos, + "Class must appear before other keys in the block" + ); + } + + #[test] + fn test_leaf_values_preserved() { + let json = r#"{"App":{"Class":"RealTimeApplication","ControlPort":"8080","StreamIP":"127.0.0.1"}}"#; + let out = convert_config_json(json); + assert!(out.contains("ControlPort = 8080")); + assert!(out.contains("StreamIP = 127.0.0.1")); + } + + #[test] + fn test_round_trip_parse() { + // Build a JSON that mirrors what the server produces for debug_test.cfg + let json = r#"{ + "App": { + "Class": "RealTimeApplication", + "Data": {"Class": "ReferenceContainer", + "DDB": {"Class": "GAMDataSource"}, + "Timer": {"Class": "LinuxTimer"} + }, + "Functions": {"Class": "ReferenceContainer", + "GAM1": {"Class": "IOGAM"}, + "GAM2": {"Class": "IOGAM"} + }, + "Scheduler": {"Class": "GAMScheduler", "TimingDataSource": "DAMS"}, + "States": {"Class": "ReferenceContainer", + "State1": {"Class": "RealTimeState"} + } + }, + "DebugService": { + "Class": "DebugService", + "ControlPort": "8080", + "UdpPort": "8081" + } + }"#; + let out = convert_config_json(json); + let cfg_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../Test/Configurations/debug_test.cfg" + ); + let original = std::fs::read_to_string(cfg_path).expect("debug_test.cfg not found"); + + // Parse both into trees and check that all Class values from the + // converted output are consistent with the original config + let converted_tree = parse_marte_config(&out); + let original_tree = parse_marte_config(&original); + assert_classes_match(&converted_tree, &original_tree, ""); + // And vice-versa for the subset present in the JSON fixture + assert_classes_match(&original_tree, &converted_tree, ""); + } + + // ----- Integration test (requires running MARTe2 app) ----- + + #[test] + #[ignore = "requires running MARTe2 debug app — start with ./run_debug_app.sh first"] + fn test_live_config_matches_debug_test_cfg() { + use std::io::{BufRead, BufReader, Write}; + use std::net::TcpStream; + use std::time::Duration; + + let mut stream = TcpStream::connect("127.0.0.1:8080") + .expect("Could not connect to DebugService on 127.0.0.1:8080"); + stream + .set_read_timeout(Some(Duration::from_secs(15))) + .unwrap(); + stream.write_all(b"CONFIG\n").unwrap(); + + // Accumulate lines until the "OK CONFIG" sentinel + let reader = BufReader::new(stream); + let mut json_lines: Vec = Vec::new(); + for line in reader.lines() { + let line = line.expect("read error while receiving CONFIG response"); + if line.trim() == "OK CONFIG" { + break; + } + json_lines.push(line); + } + let json_text = json_lines.join("\n"); + assert!(!json_text.is_empty(), "Received empty CONFIG response from server"); + + // Convert server JSON to MARTe2 syntax + let converted = convert_config_json(&json_text); + assert!(!converted.is_empty(), "convert_config_json produced empty output"); + + // Load the reference config + let cfg_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../Test/Configurations/debug_test.cfg" + ); + let original = std::fs::read_to_string(cfg_path).expect("debug_test.cfg not found"); + + let expected_tree = parse_marte_config(&original); + let actual_tree = parse_marte_config(&converted); + + // Every named object present in the original config must appear in the live + // config with the same Class. Config-file-only fields (InputSignals, + // OutputSignals, Frequency, etc.) are intentionally not checked because + // 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> { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size([1280.0, 800.0]), diff --git a/compile_commands.json b/compile_commands.json index f12b4a0..c8aabcd 100644 --- a/compile_commands.json +++ b/compile_commands.json @@ -1,180 +1,386 @@ [ { - "file": "CMakeCCompilerId.c", + "file": "TcpLogger.cpp", "arguments": [ - "cc", - "CMakeCCompilerId.c" - ], - "directory": "/home/martino/Projects/marte_debug/Build/CMakeFiles/4.2.3/CompilerIdC" - }, - { - "file": "CMakeCXXCompilerId.cpp", - "arguments": [ - "c++", - "CMakeCXXCompilerId.cpp" - ], - "directory": "/home/martino/Projects/marte_debug/Build/CMakeFiles/4.2.3/CompilerIdCXX" - }, - { - "file": "/usr/share/cmake/Modules/CMakeCCompilerABI.c", - "arguments": [ - "cc", - "-v", - "-o", - "CMakeFiles/cmTC_27c6b.dir/CMakeCCompilerABI.c.o", + "g++", "-c", - "/usr/share/cmake/Modules/CMakeCCompilerABI.c" - ], - "directory": "/home/martino/Projects/marte_debug/Build/CMakeFiles/CMakeScratch/TryCompile-0cxTtB", - "output": "CMakeFiles/cmTC_27c6b.dir/CMakeCCompilerABI.c.o" - }, - { - "file": "/usr/share/cmake/Modules/CMakeCXXCompilerABI.cpp", - "arguments": [ - "c++", - "-v", - "-o", - "CMakeFiles/cmTC_2b812.dir/CMakeCXXCompilerABI.cpp.o", - "-c", - "/usr/share/cmake/Modules/CMakeCXXCompilerABI.cpp" - ], - "directory": "/home/martino/Projects/marte_debug/Build/CMakeFiles/CMakeScratch/TryCompile-JvDZsz", - "output": "CMakeFiles/cmTC_2b812.dir/CMakeCXXCompilerABI.cpp.o" - }, - { - "file": "/home/martino/Projects/marte_debug/Source/DebugService.cpp", - "arguments": [ - "c++", - "-DARCHITECTURE=x86_gcc", - "-DENVIRONMENT=Linux", - "-DMARTe2_TEST_ENVIRONMENT=GTest", - "-DUSE_PTHREAD", - "-Dmarte_dev_EXPORTS", + "-I.", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L0Types", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L1Portability", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L2Objects", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L3Streams", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Configuration", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Events", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Logger", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Messages", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L5FILES", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Logger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Configuration", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L5GAMs", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L6App", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L1Portability", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L3Services", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4Configuration", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4Messages", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4LoggerService", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L5GAMs", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L1Portability", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L3Streams", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2-components/Source/Components/DataSources/EpicsDataSource", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2-components/Source/Components/DataSources/FileDataSource", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2-components/Source/Components/GAMs/IOGAM", - "-I/home/martino/Projects/marte_debug/Source", - "-I/home/martino/Projects/marte_debug/Headers", - "-pthread", "-fPIC", - "-MD", - "-MT", - "CMakeFiles/marte_dev.dir/Source/DebugService.cpp.o", - "-MF", - "CMakeFiles/marte_dev.dir/Source/DebugService.cpp.o.d", - "-o", - "CMakeFiles/marte_dev.dir/Source/DebugService.cpp.o", - "-c", - "/home/martino/Projects/marte_debug/Source/DebugService.cpp" - ], - "directory": "/home/martino/Projects/marte_debug/Build", - "output": "CMakeFiles/marte_dev.dir/Source/DebugService.cpp.o" - }, - { - "file": "/home/martino/Projects/marte_debug/Test/UnitTests/main.cpp", - "arguments": [ - "c++", + "-Wall", + "-std=c++98", + "-Werror", + "-Wno-invalid-offsetof", + "-Wno-unused-variable", + "-fno-strict-aliasing", + "-frtti", + "-DMARTe2_TEST_ENVIRONMENT=GTest", "-DARCHITECTURE=x86_gcc", "-DENVIRONMENT=Linux", - "-DMARTe2_TEST_ENVIRONMENT=GTest", "-DUSE_PTHREAD", + "-pthread", + "-Wno-deprecated-declarations", + "-Wno-unused-value", + "-g", + "-ggdb", + "TcpLogger.cpp", + "-o", + "../../../..//Build/x86-linux/Components/Interfaces/TCPLogger/TcpLogger.o" + ], + "directory": "/home/martino/Projects/marte_debug/Source/Components/Interfaces/TCPLogger", + "output": "../../../..//Build/x86-linux/Components/Interfaces/TCPLogger/TcpLogger.o" + }, + { + "file": "DebugService.cpp", + "arguments": [ + "g++", + "-c", + "-I../../../..//Source/Core/Types/Result", + "-I../../../..//Source/Core/Types/Vec", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L0Types", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L1Portability", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L2Objects", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L3Streams", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Configuration", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Events", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Logger", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Messages", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L5FILES", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Logger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Configuration", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L5GAMs", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L6App", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L1Portability", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L3Services", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4Configuration", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4Messages", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4LoggerService", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L5GAMs", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L1Portability", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L3Streams", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2-components/Source/Components/DataSources/EpicsDataSource", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2-components/Source/Components/DataSources/FileDataSource", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2-components/Source/Components/GAMs/IOGAM", - "-I/home/martino/Projects/marte_debug/Source", - "-I/home/martino/Projects/marte_debug/Headers", - "-I/home/martino/Projects/marte_debug/Test/UnitTests/../../Source", - "-I/home/martino/Projects/marte_debug/Test/UnitTests/../../Headers", - "-pthread", - "-MD", - "-MT", - "Test/UnitTests/CMakeFiles/UnitTests.dir/main.cpp.o", - "-MF", - "CMakeFiles/UnitTests.dir/main.cpp.o.d", - "-o", - "CMakeFiles/UnitTests.dir/main.cpp.o", - "-c", - "/home/martino/Projects/marte_debug/Test/UnitTests/main.cpp" - ], - "directory": "/home/martino/Projects/marte_debug/Build/Test/UnitTests", - "output": "CMakeFiles/UnitTests.dir/main.cpp.o" - }, - { - "file": "/home/martino/Projects/marte_debug/Test/Integration/main.cpp", - "arguments": [ - "c++", + "-fPIC", + "-Wall", + "-std=c++98", + "-Werror", + "-Wno-invalid-offsetof", + "-Wno-unused-variable", + "-fno-strict-aliasing", + "-frtti", + "-DMARTe2_TEST_ENVIRONMENT=GTest", "-DARCHITECTURE=x86_gcc", "-DENVIRONMENT=Linux", - "-DMARTe2_TEST_ENVIRONMENT=GTest", "-DUSE_PTHREAD", + "-pthread", + "-Wno-deprecated-declarations", + "-Wno-unused-value", + "-g", + "-ggdb", + "DebugService.cpp", + "-o", + "../../../..//Build/x86-linux/Components/Interfaces/DebugService/DebugService.o" + ], + "directory": "/home/martino/Projects/marte_debug/Source/Components/Interfaces/DebugService", + "output": "../../../..//Build/x86-linux/Components/Interfaces/DebugService/DebugService.o" + }, + { + "file": "UnitTests.cpp", + "arguments": [ + "g++", + "-c", + "-I../../Source/Core/Types/Result", + "-I../../Source/Core/Types/Vec", + "-I../../Source/Components/Interfaces/DebugService", + "-I../../Source/Components/Interfaces/TCPLogger", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L0Types", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L1Portability", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L2Objects", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L3Streams", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Configuration", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Events", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Logger", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Messages", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L5FILES", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Logger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Configuration", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L5GAMs", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L3Services", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4Messages", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4LoggerService", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L5GAMs", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L3Streams", + "-fPIC", + "-Wall", + "-std=c++98", + "-Werror", + "-Wno-invalid-offsetof", + "-Wno-unused-variable", + "-fno-strict-aliasing", + "-frtti", + "-DMARTe2_TEST_ENVIRONMENT=GTest", + "-DARCHITECTURE=x86_gcc", + "-DENVIRONMENT=Linux", + "-DUSE_PTHREAD", + "-pthread", + "-Wno-deprecated-declarations", + "-Wno-unused-value", + "-g", + "-ggdb", + "UnitTests.cpp", + "-o", + "../../Build/x86-linux/Test/UnitTests/UnitTests/UnitTests.o" + ], + "directory": "/home/martino/Projects/marte_debug/Test/UnitTests", + "output": "../../Build/x86-linux/Test/UnitTests/UnitTests/UnitTests.o" + }, + { + "file": "SchedulerTest.cpp", + "arguments": [ + "g++", + "-c", + "-I../../Source/Core/Types/Result", + "-I../../Source/Core/Types/Vec", + "-I../../Source/Components/Interfaces/DebugService", + "-I../../Source/Components/Interfaces/TCPLogger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L0Types", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L2Objects", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L3Streams", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Messages", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Logger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Configuration", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L5GAMs", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L6App", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L1Portability", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L3Services", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4Configuration", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4Messages", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4LoggerService", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L5GAMs", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L1Portability", "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L3Streams", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2-components/Source/Components/DataSources/EpicsDataSource", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2-components/Source/Components/DataSources/FileDataSource", - "-I/home/martino/Projects/marte_debug/dependency/MARTe2-components/Source/Components/GAMs/IOGAM", - "-I/home/martino/Projects/marte_debug/Source", - "-I/home/martino/Projects/marte_debug/Headers", + "-fPIC", + "-Wall", + "-std=c++98", + "-Werror", + "-Wno-invalid-offsetof", + "-Wno-unused-variable", + "-fno-strict-aliasing", + "-frtti", + "-DMARTe2_TEST_ENVIRONMENT=GTest", + "-DARCHITECTURE=x86_gcc", + "-DENVIRONMENT=Linux", + "-DUSE_PTHREAD", "-pthread", - "-MD", - "-MT", - "Test/Integration/CMakeFiles/IntegrationTest.dir/main.cpp.o", - "-MF", - "CMakeFiles/IntegrationTest.dir/main.cpp.o.d", + "-Wno-deprecated-declarations", + "-Wno-unused-value", + "-g", + "-ggdb", + "SchedulerTest.cpp", "-o", - "CMakeFiles/IntegrationTest.dir/main.cpp.o", - "-c", - "/home/martino/Projects/marte_debug/Test/Integration/main.cpp" + "../../Build/x86-linux/Test/Integration/Integration/SchedulerTest.o" ], - "directory": "/home/martino/Projects/marte_debug/Build/Test/Integration", - "output": "CMakeFiles/IntegrationTest.dir/main.cpp.o" + "directory": "/home/martino/Projects/marte_debug/Test/Integration", + "output": "../../Build/x86-linux/Test/Integration/Integration/SchedulerTest.o" + }, + { + "file": "TraceTest.cpp", + "arguments": [ + "g++", + "-c", + "-I../../Source/Core/Types/Result", + "-I../../Source/Core/Types/Vec", + "-I../../Source/Components/Interfaces/DebugService", + "-I../../Source/Components/Interfaces/TCPLogger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L0Types", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L2Objects", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L3Streams", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Messages", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Logger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Configuration", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L5GAMs", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L6App", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L3Services", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4Messages", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4LoggerService", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L5GAMs", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L3Streams", + "-fPIC", + "-Wall", + "-std=c++98", + "-Werror", + "-Wno-invalid-offsetof", + "-Wno-unused-variable", + "-fno-strict-aliasing", + "-frtti", + "-DMARTe2_TEST_ENVIRONMENT=GTest", + "-DARCHITECTURE=x86_gcc", + "-DENVIRONMENT=Linux", + "-DUSE_PTHREAD", + "-pthread", + "-Wno-deprecated-declarations", + "-Wno-unused-value", + "-g", + "-ggdb", + "TraceTest.cpp", + "-o", + "../../Build/x86-linux/Test/Integration/Integration/TraceTest.o" + ], + "directory": "/home/martino/Projects/marte_debug/Test/Integration", + "output": "../../Build/x86-linux/Test/Integration/Integration/TraceTest.o" + }, + { + "file": "ValidationTest.cpp", + "arguments": [ + "g++", + "-c", + "-I../../Source/Core/Types/Result", + "-I../../Source/Core/Types/Vec", + "-I../../Source/Components/Interfaces/DebugService", + "-I../../Source/Components/Interfaces/TCPLogger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L0Types", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L2Objects", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L3Streams", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Messages", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Logger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Configuration", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L5GAMs", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L6App", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L3Services", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4Messages", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4LoggerService", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L5GAMs", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L3Streams", + "-fPIC", + "-Wall", + "-std=c++98", + "-Werror", + "-Wno-invalid-offsetof", + "-Wno-unused-variable", + "-fno-strict-aliasing", + "-frtti", + "-DMARTe2_TEST_ENVIRONMENT=GTest", + "-DARCHITECTURE=x86_gcc", + "-DENVIRONMENT=Linux", + "-DUSE_PTHREAD", + "-pthread", + "-Wno-deprecated-declarations", + "-Wno-unused-value", + "-g", + "-ggdb", + "ValidationTest.cpp", + "-o", + "../../Build/x86-linux/Test/Integration/Integration/ValidationTest.o" + ], + "directory": "/home/martino/Projects/marte_debug/Test/Integration", + "output": "../../Build/x86-linux/Test/Integration/Integration/ValidationTest.o" + }, + { + "file": "ConfigCommandTest.cpp", + "arguments": [ + "g++", + "-c", + "-I../../Source/Core/Types/Result", + "-I../../Source/Core/Types/Vec", + "-I../../Source/Components/Interfaces/DebugService", + "-I../../Source/Components/Interfaces/TCPLogger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L0Types", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L2Objects", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L3Streams", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Messages", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Logger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Configuration", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L5GAMs", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L6App", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L3Services", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4Messages", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4LoggerService", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L5GAMs", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L3Streams", + "-fPIC", + "-Wall", + "-std=c++98", + "-Werror", + "-Wno-invalid-offsetof", + "-Wno-unused-variable", + "-fno-strict-aliasing", + "-frtti", + "-DMARTe2_TEST_ENVIRONMENT=GTest", + "-DARCHITECTURE=x86_gcc", + "-DENVIRONMENT=Linux", + "-DUSE_PTHREAD", + "-pthread", + "-Wno-deprecated-declarations", + "-Wno-unused-value", + "-g", + "-ggdb", + "ConfigCommandTest.cpp", + "-o", + "../../Build/x86-linux/Test/Integration/Integration/ConfigCommandTest.o" + ], + "directory": "/home/martino/Projects/marte_debug/Test/Integration", + "output": "../../Build/x86-linux/Test/Integration/Integration/ConfigCommandTest.o" + }, + { + "file": "IntegrationTests.cpp", + "arguments": [ + "g++", + "-c", + "-I../../Source/Core/Types/Result", + "-I../../Source/Core/Types/Vec", + "-I../../Source/Components/Interfaces/DebugService", + "-I../../Source/Components/Interfaces/TCPLogger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L0Types", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L2Objects", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L3Streams", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Messages", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Logger", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L4Configuration", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L5GAMs", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/BareMetal/L6App", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L3Services", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4Messages", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L4LoggerService", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/Scheduler/L5GAMs", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L1Portability", + "-I/home/martino/Projects/marte_debug/dependency/MARTe2/Source/Core/FileSystem/L3Streams", + "-fPIC", + "-Wall", + "-std=c++98", + "-Werror", + "-Wno-invalid-offsetof", + "-Wno-unused-variable", + "-fno-strict-aliasing", + "-frtti", + "-DMARTe2_TEST_ENVIRONMENT=GTest", + "-DARCHITECTURE=x86_gcc", + "-DENVIRONMENT=Linux", + "-DUSE_PTHREAD", + "-pthread", + "-Wno-deprecated-declarations", + "-Wno-unused-value", + "-g", + "-ggdb", + "IntegrationTests.cpp", + "-o", + "../../Build/x86-linux/Test/Integration/Integration/IntegrationTests.o" + ], + "directory": "/home/martino/Projects/marte_debug/Test/Integration", + "output": "../../Build/x86-linux/Test/Integration/Integration/IntegrationTests.o" } ] \ No newline at end of file diff --git a/env.fish b/env.fish new file mode 100644 index 0000000..34b3a5a --- /dev/null +++ b/env.fish @@ -0,0 +1,22 @@ +# Get the directory of this script +set -l DIR (dirname (realpath (status -f))) + +set -gx MARTe2_DIR $DIR/dependency/MARTe2 +set -gx MARTe2_Components_DIR $DIR/dependency/MARTe2-components +set -gx TARGET x86-linux + +# Update LD_LIBRARY_PATH +if not set -q LD_LIBRARY_PATH + set -gx LD_LIBRARY_PATH "" +end + +set -gx LD_LIBRARY_PATH "$LD_LIBRARY_PATH:$MARTe2_DIR/Build/$TARGET/Core" +set -gx LD_LIBRARY_PATH "$LD_LIBRARY_PATH:$MARTe2_Components_DIR/Build/$TARGET/Components" +set -gx LD_LIBRARY_PATH "$LD_LIBRARY_PATH:$DIR/Build/$TARGET/Components/Interfaces/DebugService" +set -gx LD_LIBRARY_PATH "$LD_LIBRARY_PATH:$DIR/Build/$TARGET/Components/Interfaces/TCPLogger" +set -gx LD_LIBRARY_PATH "$LD_LIBRARY_PATH:$MARTe2_Components_DIR/Build/$TARGET/Components/DataSources/LinuxTimer" +set -gx LD_LIBRARY_PATH "$LD_LIBRARY_PATH:$MARTe2_Components_DIR/Build/$TARGET/Components/DataSources/LoggerDataSource" +set -gx LD_LIBRARY_PATH "$LD_LIBRARY_PATH:$MARTe2_Components_DIR/Build/$TARGET/Components/GAMs/IOGAM" + +echo "MARTe2 Environment Set (MARTe2_DIR=$MARTe2_DIR)" +echo "MARTe2 Components Environment Set (MARTe2_Components_DIR=$MARTe2_Components_DIR)" diff --git a/env.sh b/env.sh index 3166cb7..e5a09dc 100644 --- a/env.sh +++ b/env.sh @@ -7,5 +7,10 @@ export MARTe2_Components_DIR=$DIR/dependency/MARTe2-components export TARGET=x86-linux export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MARTe2_DIR/Build/$TARGET/Core export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MARTe2_Components_DIR/Build/$TARGET/Components +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$DIR/Build/$TARGET/Components/Interfaces/DebugService +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$DIR/Build/$TARGET/Components/Interfaces/TCPLogger +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MARTe2_Components_DIR/Build/$TARGET/Components/DataSources/LinuxTimer +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MARTe2_Components_DIR/Build/$TARGET/Components/DataSources/LoggerDataSource +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MARTe2_Components_DIR/Build/$TARGET/Components/GAMs/IOGAM echo "MARTe2 Environment Set (MARTe2_DIR=$MARTe2_DIR)" echo "MARTe2 Components Environment Set (MARTe2_Components_DIR=$MARTe2_Components_DIR)" diff --git a/run_debug_app.sh b/run_debug_app.sh new file mode 100755 index 0000000..8137d69 --- /dev/null +++ b/run_debug_app.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# run_debug_app.sh - Launch the MARTe2 Debugging Environment using standard MARTeApp.ex + +# 1. Environment Setup +if [ -f "./env.sh" ]; then + source ./env.sh +else + echo "ERROR: env.sh not found. Ensure you are in the project root." + exit 1 +fi + +# 2. Paths +MARTE_EX="${MARTe2_DIR}/Build/${TARGET}/App/MARTeApp.ex" +BUILD_DIR="$(pwd)/Build/${TARGET}" + +# Our plugin libraries +DEBUG_LIB="${BUILD_DIR}/Components/Interfaces/DebugService/libDebugService.so" +TCPLOGGER_LIB="${BUILD_DIR}/Components/Interfaces/TCPLogger/libTcpLogger.so" + +# Component library base search path +COMPONENTS_BUILD_DIR="${MARTe2_Components_DIR}/Build/${TARGET}/Components" + +# DYNAMICALLY FIND ALL COMPONENT DIRS and add to LD_LIBRARY_PATH +ALL_COMPONENT_DIRS=$(find "$COMPONENTS_BUILD_DIR" -type d) +for dir in $ALL_COMPONENT_DIRS; do + if ls "$dir"/*.so >/dev/null 2>&1; then + export LD_LIBRARY_PATH="$dir:$LD_LIBRARY_PATH" + fi +done + +# Ensure our build dir and core dir are included +export LD_LIBRARY_PATH="${BUILD_DIR}/Components/Interfaces/DebugService:${BUILD_DIR}/Components/Interfaces/TCPLogger:${MARTe2_DIR}/Build/${TARGET}/Core:${LD_LIBRARY_PATH}" + +# 3. Cleanup +echo "Cleaning up lingering processes..." +pkill -9 MARTeApp.ex +sleep 1 + +# 4. Validate libraries exist +for lib in "$DEBUG_LIB" "$TCPLOGGER_LIB"; do + if [ ! -f "$lib" ]; then + echo "ERROR: Library not found: $lib" + echo "Run: make -f Makefile.gcc core" + exit 1 + fi +done + +# 5. Launch Application +echo "Launching standard MARTeApp.ex with debug_test.cfg..." +# LD_PRELOAD ensures our classes are registered before the config is parsed. +# MARTeApp.ex does not use dlopen for plugins — preloading is the standard approach. +# All required component .so files are collected via the find loop into LD_PRELOAD too. +PRELOAD_LIBS="${DEBUG_LIB}:${TCPLOGGER_LIB}" + +# Collect any additional component .so files referenced by the config +for lib in \ + "${COMPONENTS_BUILD_DIR}/DataSources/RealTimeThreadSynchronisation/RealTimeThreadSynchronisation.so" \ + "${COMPONENTS_BUILD_DIR}/DataSources/LinuxTimer/LinuxTimer.so" \ + "${COMPONENTS_BUILD_DIR}/DataSources/LoggerDataSource/LoggerDataSource.so" \ + "${COMPONENTS_BUILD_DIR}/GAMs/IOGAM/IOGAM.so"; do + if [ -f "$lib" ]; then + PRELOAD_LIBS="${PRELOAD_LIBS}:${lib}" + fi +done + +export LD_PRELOAD="${PRELOAD_LIBS}" +"$MARTE_EX" -f Test/Configurations/debug_test.cfg -l RealTimeLoader -s State1 diff --git a/run_test.sh b/run_test.sh index d6b10f1..a1541aa 100755 --- a/run_test.sh +++ b/run_test.sh @@ -1,21 +1,16 @@ #!/bin/bash -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +# Get the directory of this script +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" source $DIR/env.sh -export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. -export MARTe2_DIR=/home/martino/Projects/marte_debug/dependency/MARTe2 -export TARGET=x86-linux +echo "Cleaning up old instances..." +pkill -9 IntegrationTests +pkill -9 UnitTests +sleep 1 -MARTE_APP=$MARTe2_DIR/Build/$TARGET/App/MARTeApp.ex +echo "Starting MARTe2 Unit Tests..." +$DIR/Build/$TARGET/Test/UnitTests/UnitTests/UnitTests.ex -# Build paths for all components -LIBS=$DIR/Build -LIBS=$LIBS:$MARTe2_DIR/Build/$TARGET/Core -LIBS=$LIBS:$MARTe2_Components_DIR/Build/$TARGET/Components/DataSources/LinuxTimer -LIBS=$LIBS:$MARTe2_Components_DIR/Build/$TARGET/Components/DataSources/LoggerDataSource -LIBS=$LIBS:$MARTe2_Components_DIR/Build/$TARGET/Components/GAMs/IOGAM - -export LD_LIBRARY_PATH=$LIBS:$LD_LIBRARY_PATH - -# ./Build/Test/Integration/IntegrationTest -f Test/Configurations/debug_test.cfg -l RealTimeLoader -s State1 -$MARTE_APP -f $DIR/Test/Configurations/debug_test.cfg -l RealTimeLoader -s State1 +echo "" +echo "Starting MARTe2 Integration Tests..." +$DIR/Build/$TARGET/Test/Integration/Integration/IntegrationTests.ex diff --git a/specs.md b/specs.md deleted file mode 100644 index f3a0f36..0000000 --- a/specs.md +++ /dev/null @@ -1,41 +0,0 @@ -Project Specification: MARTe2 Universal Observability & Debugging Suite - -Version: 1.1 - -Date: 2023-10-27 - -Status: Active / Implemented - -1. Executive Summary - -This project implements a "Zero-Code-Change" observability and debugging layer for the MARTe2 real-time framework. The system allows developers to Trace, Force, and Monitor any signal in a running MARTe2 application without modifying existing source code. - -2. System Architecture - -- The Universal Debug Service (C++ Core): A singleton MARTe2 Object that patches the registry and manages communication. -- The Broker Injection Layer (C++ Templates): Templated wrappers that intercept Copy() calls for tracing, forcing, and execution control. -- The Remote Analyser (Rust/egui): A high-performance, multi-threaded GUI for visualization and control. - -3. Functional Requirements - -3.1 Execution Control -- REQ-25: Execution Control (Pause/Resume): The system SHALL provide a mechanism to pause and resume the execution of all patched real-time threads (via Brokers), allowing for static inspection of the system state. - -3.2 Discovery -- REQ-24: Tree Exploration: The GUI client SHALL request the full application tree upon connection and display it in a hierarchical tree view. -- TREE Command: Returns a recursive JSON structure representing the entire application tree, including signal metadata (Type, Dimensions, Elements). - -3.3 Multi-Threaded Client (REQ-23) -- Port 8080 (TCP): Commands and Metadata. -- Port 8082 (TCP): Independent Real-Time Log Stream. -- Port 8081 (UDP): High-Speed Telemetry for Oscilloscope. - -4. Communication Protocol - -- LS [Path]: List nodes. -- TREE: Full recursive JSON application map. -- PAUSE / RESUME: Execution control. -- TRACE <1/0> [Decimation]: Telemetry control. -- FORCE : Persistent signal override. -- UNFORCE : Remove override. -- LOG : Port 8082 streaming format. diff --git a/test_gui_sim.py b/test_gui_sim.py new file mode 100644 index 0000000..c5204f3 --- /dev/null +++ b/test_gui_sim.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +import socket +import time +import subprocess +import os +import signal + +# Start server +print("Starting server...") +proc = subprocess.Popen( + ["./run_debug_app.sh"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd="/home/martino/Projects/marte_debug", + env=os.environ, +) + +time.sleep(5) # Wait for server to start + +print("Connecting to server...") +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.settimeout(3) +s.connect(("127.0.0.1", 8080)) + +# Send TREE +s.sendall(b"TREE\n") +print("Sent TREE") + +# Send DISCOVER immediately (like GUI does) +s.sendall(b"DISCOVER\n") +print("Sent DISCOVER") + +# Wait and read +time.sleep(1) +data = b"" +while True: + try: + chunk = s.recv(4096) + if not chunk: + break + data += chunk + except: + break + +print(f"Got {len(data)} bytes") +print("Contains OK TREE:", b"OK TREE" in data) +print("Contains OK DISCOVER:", b"OK DISCOVER" in data) + +s.close() +proc.terminate() +proc.wait() +print("Done")