From 5110bcc29451ab2c733ea5627aa319ca2d4b69f5 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 16 Sep 2025 15:46:16 +0200 Subject: [PATCH 001/124] Added pipeline events messages and node --- CMakeLists.txt | 3 ++ .../pipeline/datatype/DatatypeEnum.hpp | 2 + .../pipeline/datatype/PipelineEvent.hpp | 41 +++++++++++++++ .../pipeline/datatype/PipelineState.hpp | 42 +++++++++++++++ .../node/PipelineEventAggregation.hpp | 52 +++++++++++++++++++ .../PipelineEventAggregationProperties.hpp | 16 ++++++ src/pipeline/datatype/PipelineEvent.cpp | 4 ++ src/pipeline/datatype/PipelineState.cpp | 4 ++ src/pipeline/datatype/StreamMessageParser.cpp | 8 +++ .../node/PipelineEventAggregation.cpp | 23 ++++++++ 10 files changed, 195 insertions(+) create mode 100644 include/depthai/pipeline/datatype/PipelineEvent.hpp create mode 100644 include/depthai/pipeline/datatype/PipelineState.hpp create mode 100644 include/depthai/pipeline/node/PipelineEventAggregation.hpp create mode 100644 include/depthai/properties/PipelineEventAggregationProperties.hpp create mode 100644 src/pipeline/datatype/PipelineEvent.cpp create mode 100644 src/pipeline/datatype/PipelineState.cpp create mode 100644 src/pipeline/node/PipelineEventAggregation.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d40d6ac32..4b5f62af0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -318,6 +318,7 @@ set(TARGET_CORE_SOURCES src/pipeline/node/host/RGBD.cpp src/pipeline/datatype/DatatypeEnum.cpp src/pipeline/node/PointCloud.cpp + src/pipeline/node/PipelineEventAggregation.cpp src/pipeline/datatype/Buffer.cpp src/pipeline/datatype/ImgFrame.cpp src/pipeline/datatype/ImgTransformations.cpp @@ -347,6 +348,8 @@ set(TARGET_CORE_SOURCES src/pipeline/datatype/PointCloudConfig.cpp src/pipeline/datatype/ObjectTrackerConfig.cpp src/pipeline/datatype/PointCloudData.cpp + src/pipeline/datatype/PipelineEvent.cpp + src/pipeline/datatype/PipelineState.cpp src/pipeline/datatype/RGBDData.cpp src/pipeline/datatype/MessageGroup.cpp src/pipeline/datatype/TransformData.cpp diff --git a/include/depthai/pipeline/datatype/DatatypeEnum.hpp b/include/depthai/pipeline/datatype/DatatypeEnum.hpp index d8d594263..a501ad7f4 100644 --- a/include/depthai/pipeline/datatype/DatatypeEnum.hpp +++ b/include/depthai/pipeline/datatype/DatatypeEnum.hpp @@ -43,6 +43,8 @@ enum class DatatypeEnum : std::int32_t { DynamicCalibrationResult, CalibrationQuality, CoverageData, + PipelineEvent, + PipelineState, }; bool isDatatypeSubclassOf(DatatypeEnum parent, DatatypeEnum children); diff --git a/include/depthai/pipeline/datatype/PipelineEvent.hpp b/include/depthai/pipeline/datatype/PipelineEvent.hpp new file mode 100644 index 000000000..a8c7af539 --- /dev/null +++ b/include/depthai/pipeline/datatype/PipelineEvent.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "depthai/common/Timestamp.hpp" +#include "depthai/common/optional.hpp" +#include "depthai/common/variant.hpp" +#include "depthai/pipeline/datatype/Buffer.hpp" + +namespace dai { + +/** + * Pipeline event message. + */ +class PipelineEvent : public Buffer { + public: + enum class EventType : std::int32_t { + CUSTOM = 0, + LOOP = 1, + INPUT = 2, + OUTPUT = 3, + FUNC_CALL = 4 + }; + + PipelineEvent() = default; + virtual ~PipelineEvent() = default; + + std::optional metadata; + uint64_t duration {0}; // Duration in microseconds + EventType type = EventType::CUSTOM; + std::variant> source; + + void serialize(std::vector& metadata, DatatypeEnum& datatype) const override { + metadata = utility::serialize(*this); + datatype = DatatypeEnum::PipelineEvent; + }; + + DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, metadata, duration, type, source); +}; + +} // namespace dai diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp new file mode 100644 index 000000000..465ee6beb --- /dev/null +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include "depthai/pipeline/datatype/Buffer.hpp" +#include "depthai/pipeline/datatype/PipelineEvent.hpp" + +namespace dai { + +class NodeState { + public: + struct Timing { + uint64_t averageMicros; + uint64_t stdDevMicros; + DEPTHAI_SERIALIZE(Timing, averageMicros, stdDevMicros); + }; + std::vector events; + std::unordered_map timingsByType; + std::unordered_map timingsByInstance; + + DEPTHAI_SERIALIZE(NodeState, events, timingsByType, timingsByInstance); +}; + +/** + * Pipeline event message. + */ +class PipelineState : public Buffer { + public: + PipelineState() = default; + virtual ~PipelineState() = default; + + std::unordered_map nodeStates; + + void serialize(std::vector& metadata, DatatypeEnum& datatype) const override { + metadata = utility::serialize(*this); + datatype = DatatypeEnum::PipelineState; + }; + + DEPTHAI_SERIALIZE(PipelineState, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeStates); +}; + +} // namespace dai diff --git a/include/depthai/pipeline/node/PipelineEventAggregation.hpp b/include/depthai/pipeline/node/PipelineEventAggregation.hpp new file mode 100644 index 000000000..f2636a4a9 --- /dev/null +++ b/include/depthai/pipeline/node/PipelineEventAggregation.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +// shared +#include + +namespace dai { +namespace node { + +/** + * @brief Sync node. Performs syncing between image frames + */ +class PipelineEventAggregation : public DeviceNodeCRTP, public HostRunnable { + private: + bool runOnHostVar = false; + + public: + constexpr static const char* NAME = "PipelineEventAggregation"; + using DeviceNodeCRTP::DeviceNodeCRTP; + + /** + * A map of inputs + */ + InputMap inputs{*this, "inputs", {"", DEFAULT_GROUP, false, 10, {{{DatatypeEnum::PipelineEvent, false}}}, DEFAULT_WAIT_FOR_MESSAGE}}; + + /** + * A map of outputs + */ + OutputMap outputs{*this, "outputs", {DEFAULT_NAME, DEFAULT_GROUP, {{{DatatypeEnum::PipelineEvent, false}}}}}; + + /** + * Output message of type TODO + */ + Output out{*this, {"out", DEFAULT_GROUP, {{{DatatypeEnum::Buffer, false}}}}}; + + /** + * Specify whether to run on host or device + * By default, the node will run on device. + */ + void setRunOnHost(bool runOnHost); + + /** + * Check if the node is set to run on host + */ + bool runOnHost() const override; + + void run() override; +}; + +} // namespace node +} // namespace dai diff --git a/include/depthai/properties/PipelineEventAggregationProperties.hpp b/include/depthai/properties/PipelineEventAggregationProperties.hpp new file mode 100644 index 000000000..654e10950 --- /dev/null +++ b/include/depthai/properties/PipelineEventAggregationProperties.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "depthai/properties/Properties.hpp" + +namespace dai { + +/** + * Specify properties for Sync. + */ +struct PipelineEventAggregationProperties : PropertiesSerializable { + int dummy = 0; +}; + +DEPTHAI_SERIALIZE_EXT(PipelineEventAggregationProperties, dummy); + +} // namespace dai diff --git a/src/pipeline/datatype/PipelineEvent.cpp b/src/pipeline/datatype/PipelineEvent.cpp new file mode 100644 index 000000000..4d7121336 --- /dev/null +++ b/src/pipeline/datatype/PipelineEvent.cpp @@ -0,0 +1,4 @@ +#include "depthai/pipeline/datatype/PipelineEvent.hpp" + +namespace dai { +} // namespace dai diff --git a/src/pipeline/datatype/PipelineState.cpp b/src/pipeline/datatype/PipelineState.cpp new file mode 100644 index 000000000..f45bb47e9 --- /dev/null +++ b/src/pipeline/datatype/PipelineState.cpp @@ -0,0 +1,4 @@ +#include "depthai/pipeline/datatype/PipelineState.hpp" + +namespace dai { +} // namespace dai diff --git a/src/pipeline/datatype/StreamMessageParser.cpp b/src/pipeline/datatype/StreamMessageParser.cpp index 660afa832..7ec39151d 100644 --- a/src/pipeline/datatype/StreamMessageParser.cpp +++ b/src/pipeline/datatype/StreamMessageParser.cpp @@ -15,6 +15,8 @@ #include "depthai/pipeline/datatype/BenchmarkReport.hpp" #include "depthai/pipeline/datatype/Buffer.hpp" #include "depthai/pipeline/datatype/CameraControl.hpp" +#include "depthai/pipeline/datatype/PipelineEvent.hpp" +#include "depthai/pipeline/datatype/PipelineState.hpp" #ifdef DEPTHAI_HAVE_DYNAMIC_CALIBRATION_SUPPORT #include "depthai/pipeline/datatype/DynamicCalibrationControl.hpp" #include "depthai/pipeline/datatype/DynamicCalibrationResults.hpp" @@ -238,6 +240,12 @@ std::shared_ptr StreamMessageParser::parseMessage(streamPacketDesc_t* case DatatypeEnum::PointCloudData: return parseDatatype(metadataStart, serializedObjectSize, data, fd); break; + case DatatypeEnum::PipelineEvent: + return parseDatatype(metadataStart, serializedObjectSize, data, fd); + break; + case DatatypeEnum::PipelineState: + return parseDatatype(metadataStart, serializedObjectSize, data, fd); + break; case DatatypeEnum::MessageGroup: return parseDatatype(metadataStart, serializedObjectSize, data, fd); break; diff --git a/src/pipeline/node/PipelineEventAggregation.cpp b/src/pipeline/node/PipelineEventAggregation.cpp new file mode 100644 index 000000000..5cf3b48a3 --- /dev/null +++ b/src/pipeline/node/PipelineEventAggregation.cpp @@ -0,0 +1,23 @@ +#include "depthai/pipeline/node/PipelineEventAggregation.hpp" + +namespace dai { +namespace node { + +void PipelineEventAggregation::setRunOnHost(bool runOnHost) { + runOnHostVar = runOnHost; +} + +/** + * Check if the node is set to run on host + */ +bool PipelineEventAggregation::runOnHost() const { + return runOnHostVar; +} + +void PipelineEventAggregation::run() { + while(isRunning()) { + + } +} +} // namespace node +} // namespace dai From e165806fe912218241b7f54f37c9657d69eca0ce Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 18 Sep 2025 13:48:54 +0200 Subject: [PATCH 002/124] Added the pipeline event dispatcher --- CMakeLists.txt | 1 + .../pipeline/datatype/PipelineEvent.hpp | 7 +- .../pipeline/datatype/PipelineState.hpp | 3 +- .../PipelineEventAggregationProperties.hpp | 6 +- include/depthai/utility/CircularBuffer.hpp | 53 +++++++++++ .../utility/PipelineEventDispatcher.hpp | 39 ++++++++ .../node/PipelineEventAggregation.cpp | 95 ++++++++++++++++++- src/utility/PipelineEventDispatcher.cpp | 78 +++++++++++++++ 8 files changed, 274 insertions(+), 8 deletions(-) create mode 100644 include/depthai/utility/CircularBuffer.hpp create mode 100644 include/depthai/utility/PipelineEventDispatcher.hpp create mode 100644 src/utility/PipelineEventDispatcher.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b5f62af0..bd1ac83ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -359,6 +359,7 @@ set(TARGET_CORE_SOURCES src/utility/Initialization.cpp src/utility/Resources.cpp src/utility/Platform.cpp + src/utility/PipelineEventDispatcher.cpp src/utility/RecordReplay.cpp src/utility/McapImpl.cpp src/utility/Environment.cpp diff --git a/include/depthai/pipeline/datatype/PipelineEvent.hpp b/include/depthai/pipeline/datatype/PipelineEvent.hpp index a8c7af539..9ea1b92f2 100644 --- a/include/depthai/pipeline/datatype/PipelineEvent.hpp +++ b/include/depthai/pipeline/datatype/PipelineEvent.hpp @@ -2,9 +2,7 @@ #include -#include "depthai/common/Timestamp.hpp" #include "depthai/common/optional.hpp" -#include "depthai/common/variant.hpp" #include "depthai/pipeline/datatype/Buffer.hpp" namespace dai { @@ -25,17 +23,18 @@ class PipelineEvent : public Buffer { PipelineEvent() = default; virtual ~PipelineEvent() = default; + int64_t nodeId = -1; std::optional metadata; uint64_t duration {0}; // Duration in microseconds EventType type = EventType::CUSTOM; - std::variant> source; + std::string source; void serialize(std::vector& metadata, DatatypeEnum& datatype) const override { metadata = utility::serialize(*this); datatype = DatatypeEnum::PipelineEvent; }; - DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, metadata, duration, type, source); + DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeId, metadata, duration, type, source); }; } // namespace dai diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index 465ee6beb..64ddbbf34 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -4,6 +4,7 @@ #include "depthai/pipeline/datatype/Buffer.hpp" #include "depthai/pipeline/datatype/PipelineEvent.hpp" +#include "depthai/common/optional.hpp" namespace dai { @@ -29,7 +30,7 @@ class PipelineState : public Buffer { PipelineState() = default; virtual ~PipelineState() = default; - std::unordered_map nodeStates; + std::unordered_map> nodeStates; void serialize(std::vector& metadata, DatatypeEnum& datatype) const override { metadata = utility::serialize(*this); diff --git a/include/depthai/properties/PipelineEventAggregationProperties.hpp b/include/depthai/properties/PipelineEventAggregationProperties.hpp index 654e10950..028f0bf09 100644 --- a/include/depthai/properties/PipelineEventAggregationProperties.hpp +++ b/include/depthai/properties/PipelineEventAggregationProperties.hpp @@ -8,9 +8,11 @@ namespace dai { * Specify properties for Sync. */ struct PipelineEventAggregationProperties : PropertiesSerializable { - int dummy = 0; + uint32_t aggregationWindowSize = 20; + uint32_t eventBatchSize = 10; + bool sendEvents = false; }; -DEPTHAI_SERIALIZE_EXT(PipelineEventAggregationProperties, dummy); +DEPTHAI_SERIALIZE_EXT(PipelineEventAggregationProperties, aggregationWindowSize, eventBatchSize, sendEvents); } // namespace dai diff --git a/include/depthai/utility/CircularBuffer.hpp b/include/depthai/utility/CircularBuffer.hpp new file mode 100644 index 000000000..a0f631d60 --- /dev/null +++ b/include/depthai/utility/CircularBuffer.hpp @@ -0,0 +1,53 @@ +#pragma once +#include +#include + +namespace dai { +namespace utility { + +template +class CircularBuffer { + public: + CircularBuffer(size_t size) : maxSize(size) { + buffer.reserve(size); + } + void add(int value) { + if(buffer.size() < maxSize) { + buffer.push_back(value); + } else { + buffer[index] = value; + index = (index + 1) % maxSize; + } + } + std::vector getBuffer() const { + std::vector result; + if(buffer.size() < maxSize) { + result = buffer; + } else { + result.insert(result.end(), buffer.begin() + index, buffer.end()); + result.insert(result.end(), buffer.begin(), buffer.begin() + index); + } + return result; + } + T last() const { + if(buffer.empty()) { + throw std::runtime_error("CircularBuffer is empty"); + } + if(buffer.size() < maxSize) { + return buffer.back(); + } else { + return buffer[(index + maxSize - 1) % maxSize]; + } + } + size_t size() const { + return buffer.size(); + } + + private: + std::vector buffer; + size_t maxSize; + size_t index = 0; +}; + +} // namespace utility +} // namespace dai diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp new file mode 100644 index 000000000..4dfb006a3 --- /dev/null +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "depthai/pipeline/Node.hpp" +#include "depthai/pipeline/datatype/PipelineEvent.hpp" + +namespace dai { +namespace utility { + +class PipelineEventDispatcher { + struct EventStatus { + PipelineEvent::EventType type; + std::chrono::microseconds duration; + std::chrono::time_point timestamp; + bool ongoing; + std::optional metadata; + }; + + int64_t nodeId; + std::unordered_map events; + Node::Output* out = nullptr; + + public: + PipelineEventDispatcher(int64_t nodeId, Node::Output* output) : nodeId(nodeId), out(output) {} + + void addEvent(const std::string& source, PipelineEvent::EventType type); + + void startEvent(const std::string& source, std::optional metadata = std::nullopt); // Start event with a start and an end + void endEvent(const std::string& source); // Stop event with a start and an end + void pingEvent(const std::string& source); // Event where stop and start are the same (eg. loop) +}; + +} // namespace utility +} // namespace dai diff --git a/src/pipeline/node/PipelineEventAggregation.cpp b/src/pipeline/node/PipelineEventAggregation.cpp index 5cf3b48a3..bb4be94b9 100644 --- a/src/pipeline/node/PipelineEventAggregation.cpp +++ b/src/pipeline/node/PipelineEventAggregation.cpp @@ -1,8 +1,75 @@ #include "depthai/pipeline/node/PipelineEventAggregation.hpp" +#include "depthai/pipeline/datatype/PipelineEvent.hpp" +#include "depthai/pipeline/datatype/PipelineState.hpp" +#include "depthai/utility/CircularBuffer.hpp" + namespace dai { namespace node { +class NodeEventAggregation { + int windowSize; + + public: + NodeEventAggregation(int windowSize) : windowSize(windowSize) {} + NodeState state; + std::unordered_map>> timingsBufferByType; + std::unordered_map>> timingsBufferByInstance; + + void add(PipelineEvent& event) { + // TODO optimize avg + state.events.push_back(event); + if(timingsBufferByType.find(event.type) == timingsBufferByType.end()) { + timingsBufferByType[event.type] = std::make_unique>(windowSize); + } + if(timingsBufferByInstance.find(event.source) == timingsBufferByInstance.end()) { + timingsBufferByInstance[event.source] = std::make_unique>(windowSize); + } + timingsBufferByType[event.type]->add(event.duration); + timingsBufferByInstance[event.source]->add(event.duration); + // Calculate average duration and standard deviation from buffers + state.timingsByType[event.type].averageMicros = 0; + state.timingsByType[event.type].stdDevMicros = 0; + state.timingsByInstance[event.source].averageMicros = 0; + state.timingsByInstance[event.source].stdDevMicros = 0; + auto bufferByType = timingsBufferByType[event.type]->getBuffer(); + auto bufferByInstance = timingsBufferByInstance[event.source]->getBuffer(); + if(!bufferByType.empty()) { + uint64_t sum = 0; + double variance = 0; + for(auto v : bufferByType) { + sum += v; + } + state.timingsByType[event.type].averageMicros = sum / bufferByType.size(); + // Calculate standard deviation + for(auto v : bufferByType) { + auto diff = v - state.timingsByType[event.type].averageMicros; + variance += diff * diff; + } + variance /= bufferByType.size(); + state.timingsByType[event.type].stdDevMicros = (uint64_t)(std::sqrt(variance)); + } + if(!bufferByInstance.empty()) { + uint64_t sum = 0; + double variance = 0; + for(auto v : bufferByInstance) { + sum += v; + } + state.timingsByInstance[event.source].averageMicros = sum / bufferByInstance.size(); + // Calculate standard deviation + for(auto v : bufferByInstance) { + auto diff = v - state.timingsByInstance[event.source].averageMicros; + variance += diff * diff; + } + variance /= bufferByInstance.size(); + state.timingsByInstance[event.source].stdDevMicros = (uint64_t)(std::sqrt(variance)); + } + } + void resetEvents() { + state.events.clear(); + } +}; + void PipelineEventAggregation::setRunOnHost(bool runOnHost) { runOnHostVar = runOnHost; } @@ -15,8 +82,34 @@ bool PipelineEventAggregation::runOnHost() const { } void PipelineEventAggregation::run() { + std::unordered_map nodeStates; while(isRunning()) { - + std::unordered_map> events; + for(auto& [k, v] : inputs) { + events[k.second] = v.get(); + } + for(auto& [k, event] : events) { + if(event != nullptr) { + if(nodeStates.find(event->nodeId) == nodeStates.end()) { + nodeStates.insert_or_assign(event->nodeId, NodeEventAggregation(properties.aggregationWindowSize)); + } + nodeStates.at(event->nodeId).add(*event); + } + } + auto outState = std::make_shared(); + bool shouldSend = false; + for(auto& [nodeId, nodeState] : nodeStates) { + auto numEvents = nodeState.state.events.size(); + if(numEvents >= properties.eventBatchSize) { + outState->nodeStates[nodeId] = nodeState.state; + if(!properties.sendEvents) outState->nodeStates[nodeId]->events.clear(); + nodeState.resetEvents(); + shouldSend = true; + } else { + outState->nodeStates[nodeId] = std::nullopt; + } + } + if(shouldSend) out.send(outState); } } } // namespace node diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp new file mode 100644 index 000000000..ceb19451c --- /dev/null +++ b/src/utility/PipelineEventDispatcher.cpp @@ -0,0 +1,78 @@ +#include "depthai/utility/PipelineEventDispatcher.hpp" +#include + +namespace dai { +namespace utility { + +void PipelineEventDispatcher::addEvent(const std::string& source, PipelineEvent::EventType type) { + if(events.find(source) != events.end()) { + throw std::runtime_error("Event with name " + source + " already exists"); + } + events[source] = {type, {}, {}, false, std::nullopt}; +} +void PipelineEventDispatcher::startEvent(const std::string& source, std::optional metadata) { + if(events.find(source) == events.end()) { + throw std::runtime_error("Event with name " + source + " does not exist"); + } + auto& event = events[source]; + if(event.ongoing) { + throw std::runtime_error("Event with name " + source + " is already ongoing"); + } + event.timestamp = std::chrono::steady_clock::now(); + event.ongoing = true; + event.metadata = metadata; +} +void PipelineEventDispatcher::endEvent(const std::string& source) { + auto now = std::chrono::steady_clock::now(); + + if(events.find(source) == events.end()) { + throw std::runtime_error("Event with name " + source + " does not exist"); + } + auto& event = events[source]; + if(!event.ongoing) { + throw std::runtime_error("Event with name " + source + " has not been started"); + } + event.duration = std::chrono::duration_cast(now - event.timestamp); + event.ongoing = false; + + PipelineEvent pipelineEvent; + pipelineEvent.nodeId = nodeId; + pipelineEvent.duration = event.duration.count(); + pipelineEvent.type = event.type; + pipelineEvent.source = source; + + if(out) { + out->send(std::make_shared(pipelineEvent)); + } + + event.metadata = std::nullopt; +} +void PipelineEventDispatcher::pingEvent(const std::string& source) { + auto now = std::chrono::steady_clock::now(); + + if(events.find(source) == events.end()) { + throw std::runtime_error("Event with name " + source + " does not exist"); + } + auto& event = events[source]; + if(event.ongoing) { + event.duration = std::chrono::duration_cast(now - event.timestamp); + event.timestamp = now; + + PipelineEvent pipelineEvent; + pipelineEvent.nodeId = nodeId; + pipelineEvent.duration = event.duration.count(); + pipelineEvent.type = event.type; + pipelineEvent.source = source; + pipelineEvent.metadata = std::nullopt; + + if(out) { + out->send(std::make_shared(pipelineEvent)); + } + } else { + event.timestamp = now; + event.ongoing = true; + } +} + +} // namespace utility +} // namespace dai From fbd4137199f8b8213e49aa262969432ab372aedc Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 22 Sep 2025 10:56:41 +0200 Subject: [PATCH 003/124] Add Pipeline event bindings --- bindings/python/CMakeLists.txt | 7 ++- bindings/python/src/DatatypeBindings.cpp | 8 ++- .../datatype/PipelineEventBindings.cpp | 55 ++++++++++++++++++ .../datatype/PipelineStateBindings.cpp | 57 +++++++++++++++++++ .../python/src/pipeline/node/NodeBindings.cpp | 2 + .../node/PipelineEventAggregationBindings.cpp | 38 +++++++++++++ bindings/python/src/py_bindings.cpp | 2 + .../PipelineEventDispatcherBindings.cpp | 27 +++++++++ .../PipelineEventDispatcherBindings.hpp | 8 +++ .../node/PipelineEventAggregation.hpp | 9 +-- .../utility/PipelineEventDispatcher.hpp | 1 + src/pipeline/node/host/Replay.cpp | 4 ++ src/utility/ProtoSerialize.cpp | 2 + 13 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp create mode 100644 bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp create mode 100644 bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp create mode 100644 bindings/python/src/utility/PipelineEventDispatcherBindings.cpp create mode 100644 bindings/python/src/utility/PipelineEventDispatcherBindings.hpp diff --git a/bindings/python/CMakeLists.txt b/bindings/python/CMakeLists.txt index 2a4bceee3..58209d883 100644 --- a/bindings/python/CMakeLists.txt +++ b/bindings/python/CMakeLists.txt @@ -98,7 +98,7 @@ set(SOURCE_LIST src/pipeline/node/UVCBindings.cpp src/pipeline/node/ToFBindings.cpp src/pipeline/node/PointCloudBindings.cpp - src/pipeline/node/SyncBindings.cpp + src/pipeline/node/PipelineEventAggregationBindings.cpp src/pipeline/node/MessageDemuxBindings.cpp src/pipeline/node/HostNodeBindings.cpp src/pipeline/node/RecordBindings.cpp @@ -136,6 +136,8 @@ set(SOURCE_LIST src/pipeline/datatype/PointCloudConfigBindings.cpp src/pipeline/datatype/ObjectTrackerConfigBindings.cpp src/pipeline/datatype/PointCloudDataBindings.cpp + src/pipeline/datatype/PipelineEventBindings.cpp + src/pipeline/datatype/PipelineStateBindings.cpp src/pipeline/datatype/TransformDataBindings.cpp src/pipeline/datatype/ImageAlignConfigBindings.cpp src/pipeline/datatype/ImgAnnotationsBindings.cpp @@ -149,6 +151,7 @@ set(SOURCE_LIST src/remote_connection/RemoteConnectionBindings.cpp src/utility/EventsManagerBindings.cpp + src/utility/PipelineEventDispatcherBindings.cpp ) if(DEPTHAI_MERGED_TARGET) list(APPEND SOURCE_LIST @@ -412,4 +415,4 @@ endif() ######################## if (DEPTHAI_PYTHON_ENABLE_EXAMPLES) add_subdirectory(../../examples/python ${CMAKE_BINARY_DIR}/examples/python) -endif() \ No newline at end of file +endif() diff --git a/bindings/python/src/DatatypeBindings.cpp b/bindings/python/src/DatatypeBindings.cpp index 69d1c922a..8276647fa 100644 --- a/bindings/python/src/DatatypeBindings.cpp +++ b/bindings/python/src/DatatypeBindings.cpp @@ -30,6 +30,8 @@ void bind_tracklets(pybind11::module& m, void* pCallstack); void bind_benchmarkreport(pybind11::module& m, void* pCallstack); void bind_pointcloudconfig(pybind11::module& m, void* pCallstack); void bind_pointclouddata(pybind11::module& m, void* pCallstack); +void bind_pipelineevent(pybind11::module& m, void* pCallstack); +void bind_pipelinestate(pybind11::module& m, void* pCallstack); void bind_transformdata(pybind11::module& m, void* pCallstack); void bind_rgbddata(pybind11::module& m, void* pCallstack); void bind_imagealignconfig(pybind11::module& m, void* pCallstack); @@ -71,6 +73,8 @@ void DatatypeBindings::addToCallstack(std::deque& callstack) { callstack.push_front(bind_benchmarkreport); callstack.push_front(bind_pointcloudconfig); callstack.push_front(bind_pointclouddata); + callstack.push_front(bind_pipelineevent); + callstack.push_front(bind_pipelinestate); callstack.push_front(bind_transformdata); callstack.push_front(bind_imagealignconfig); callstack.push_front(bind_imageannotations); @@ -131,5 +135,7 @@ void DatatypeBindings::bind(pybind11::module& m, void* pCallstack) { .value("PointCloudData", DatatypeEnum::PointCloudData) .value("ImageAlignConfig", DatatypeEnum::ImageAlignConfig) .value("ImgAnnotations", DatatypeEnum::ImgAnnotations) - .value("RGBDData", DatatypeEnum::RGBDData); + .value("RGBDData", DatatypeEnum::RGBDData) + .value("PipelineEvent", DatatypeEnum::PipelineEvent) + .value("PipelineState", DatatypeEnum::PipelineState); } diff --git a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp new file mode 100644 index 000000000..ea9a7552c --- /dev/null +++ b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp @@ -0,0 +1,55 @@ +#include +#include + +#include "DatatypeBindings.hpp" +#include "pipeline/CommonBindings.hpp" + +// depthai +#include "depthai/pipeline/datatype/PipelineEvent.hpp" + +// pybind +#include +#include + +// #include "spdlog/spdlog.h" + +void bind_pipelineevent(pybind11::module& m, void* pCallstack) { + using namespace dai; + + py::class_, Buffer, std::shared_ptr> pipelineEvent(m, "PipelineEvent", DOC(dai, PipelineEvent)); + py::enum_ pipelineEventType(pipelineEvent, "EventType", DOC(dai, PipelineEvent, EventType)); + + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + // Call the rest of the type defines, then perform the actual bindings + Callstack* callstack = (Callstack*)pCallstack; + auto cb = callstack->top(); + callstack->pop(); + cb(m, pCallstack); + // Actual bindings + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + + pipelineEventType.value("CUSTOM", PipelineEvent::EventType::CUSTOM) + .value("LOOP", PipelineEvent::EventType::LOOP) + .value("INPUT", PipelineEvent::EventType::INPUT) + .value("OUTPUT", PipelineEvent::EventType::OUTPUT) + .value("FUNC_CALL", PipelineEvent::EventType::FUNC_CALL); + + // Message + pipelineEvent.def(py::init<>()) + .def("__repr__", &PipelineEvent::str) + .def_readwrite("nodeId", &PipelineEvent::nodeId, DOC(dai, PipelineEvent, nodeId)) + .def_readwrite("metadata", &PipelineEvent::metadata, DOC(dai, PipelineEvent, metadata)) + .def_readwrite("duration", &PipelineEvent::duration, DOC(dai, PipelineEvent, duration)) + .def_readwrite("type", &PipelineEvent::type, DOC(dai, PipelineEvent, type)) + .def_readwrite("source", &PipelineEvent::source, DOC(dai, PipelineEvent, source)) + .def("getTimestamp", &PipelineEvent::Buffer::getTimestamp, DOC(dai, Buffer, getTimestamp)) + .def("getTimestampDevice", &PipelineEvent::Buffer::getTimestampDevice, DOC(dai, Buffer, getTimestampDevice)) + .def("getSequenceNum", &PipelineEvent::Buffer::getSequenceNum, DOC(dai, Buffer, getSequenceNum)) + .def("setTimestamp", &PipelineEvent::setTimestamp, DOC(dai, PipelineEvent, setTimestamp)) + .def("setTimestampDevice", &PipelineEvent::setTimestampDevice, DOC(dai, PipelineEvent, setTimestampDevice)) + .def("setSequenceNum", &PipelineEvent::setSequenceNum, DOC(dai, PipelineEvent, setSequenceNum)); +} diff --git a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp new file mode 100644 index 000000000..8b59cc6ea --- /dev/null +++ b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp @@ -0,0 +1,57 @@ +#include +#include + +#include "DatatypeBindings.hpp" +#include "pipeline/CommonBindings.hpp" + +// depthai +#include "depthai/pipeline/datatype/PipelineState.hpp" + +// pybind +#include +#include + +// #include "spdlog/spdlog.h" + +void bind_pipelinestate(pybind11::module& m, void* pCallstack) { + using namespace dai; + + py::class_ nodeState(m, "NodeState", DOC(dai, NodeState)); + py::class_ nodeStateTiming(nodeState, "Timing", DOC(dai, NodeState, Timing)); + py::class_, Buffer, std::shared_ptr> pipelineState(m, "PipelineState", DOC(dai, PipelineState)); + + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + // Call the rest of the type defines, then perform the actual bindings + Callstack* callstack = (Callstack*)pCallstack; + auto cb = callstack->top(); + callstack->pop(); + cb(m, pCallstack); + // Actual bindings + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + + nodeStateTiming.def(py::init<>()) + .def("__repr__", &NodeState::Timing::str) + .def_readwrite("averageMicros", &NodeState::Timing::averageMicros, DOC(dai, NodeState, Timing, averageMicros)) + .def_readwrite("stdDevMicros", &NodeState::Timing::stdDevMicros, DOC(dai, NodeState, Timing, stdDevMicros)); + + nodeState.def(py::init<>()) + .def("__repr__", &NodeState::str) + .def_readwrite("events", &NodeState::events, DOC(dai, NodeState, events)) + .def_readwrite("timingsByType", &NodeState::timingsByType, DOC(dai, NodeState, timingsByType)) + .def_readwrite("timingsByInstance", &NodeState::timingsByInstance, DOC(dai, NodeState, timingsByInstance)); + + // Message + pipelineState.def(py::init<>()) + .def("__repr__", &PipelineState::str) + .def_readwrite("nodeStates", &PipelineState::nodeStates, DOC(dai, PipelineState, nodeStates)) + .def("getTimestamp", &PipelineState::Buffer::getTimestamp, DOC(dai, Buffer, getTimestamp)) + .def("getTimestampDevice", &PipelineState::Buffer::getTimestampDevice, DOC(dai, Buffer, getTimestampDevice)) + .def("getSequenceNum", &PipelineState::Buffer::getSequenceNum, DOC(dai, Buffer, getSequenceNum)) + .def("setTimestamp", &PipelineState::setTimestamp, DOC(dai, PipelineState, setTimestamp)) + .def("setTimestampDevice", &PipelineState::setTimestampDevice, DOC(dai, PipelineState, setTimestampDevice)) + .def("setSequenceNum", &PipelineState::setSequenceNum, DOC(dai, PipelineState, setSequenceNum)); +} diff --git a/bindings/python/src/pipeline/node/NodeBindings.cpp b/bindings/python/src/pipeline/node/NodeBindings.cpp index d58ac38a9..4237ba403 100644 --- a/bindings/python/src/pipeline/node/NodeBindings.cpp +++ b/bindings/python/src/pipeline/node/NodeBindings.cpp @@ -153,6 +153,7 @@ void bind_uvc(pybind11::module& m, void* pCallstack); void bind_thermal(pybind11::module& m, void* pCallstack); void bind_tof(pybind11::module& m, void* pCallstack); void bind_pointcloud(pybind11::module& m, void* pCallstack); +void bind_pipelineeventaggregation(pybind11::module& m, void* pCallstack); void bind_sync(pybind11::module& m, void* pCallstack); void bind_messagedemux(pybind11::module& m, void* pCallstack); void bind_hostnode(pybind11::module& m, void* pCallstack); @@ -202,6 +203,7 @@ void NodeBindings::addToCallstack(std::deque& callstack) { callstack.push_front(bind_thermal); callstack.push_front(bind_tof); callstack.push_front(bind_pointcloud); + callstack.push_front(bind_pipelineeventaggregation); callstack.push_front(bind_sync); callstack.push_front(bind_messagedemux); callstack.push_front(bind_hostnode); diff --git a/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp new file mode 100644 index 000000000..d28be67f7 --- /dev/null +++ b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp @@ -0,0 +1,38 @@ +#include "Common.hpp" +#include "depthai/pipeline/node/PipelineEventAggregation.hpp" +#include "depthai/properties/PipelineEventAggregationProperties.hpp" + +void bind_pipelineeventaggregation(pybind11::module& m, void* pCallstack) { + using namespace dai; + using namespace dai::node; + + // Node and Properties declare upfront + py::class_ pipelineEventAggregationProperties( + m, "PipelineEventAggregationProperties", DOC(dai, PipelineEventAggregationProperties)); + auto pipelineEventAggregation = ADD_NODE(PipelineEventAggregation); + + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + // Call the rest of the type defines, then perform the actual bindings + Callstack* callstack = (Callstack*)pCallstack; + auto cb = callstack->top(); + callstack->pop(); + cb(m, pCallstack); + // Actual bindings + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + + // Properties + pipelineEventAggregationProperties.def_readwrite("aggregationWindowSize", &PipelineEventAggregationProperties::aggregationWindowSize) + .def_readwrite("eventBatchSize", &PipelineEventAggregationProperties::eventBatchSize) + .def_readwrite("sendEvents", &PipelineEventAggregationProperties::sendEvents); + + // Node + pipelineEventAggregation.def_readonly("out", &PipelineEventAggregation::out, DOC(dai, node, PipelineEventAggregation, out)) + .def_readonly("inputs", &PipelineEventAggregation::inputs, DOC(dai, node, PipelineEventAggregation, inputs)) + .def("setRunOnHost", &PipelineEventAggregation::setRunOnHost, py::arg("runOnHost"), DOC(dai, node, PipelineEventAggregation, setRunOnHost)) + .def("runOnHost", &PipelineEventAggregation::runOnHost, DOC(dai, node, PipelineEventAggregation, runOnHost)); + daiNodeModule.attr("PipelineEventAggregation").attr("Properties") = pipelineEventAggregationProperties; +} diff --git a/bindings/python/src/py_bindings.cpp b/bindings/python/src/py_bindings.cpp index 122d03016..cbbb4400e 100644 --- a/bindings/python/src/py_bindings.cpp +++ b/bindings/python/src/py_bindings.cpp @@ -37,6 +37,7 @@ #include "pipeline/node/NodeBindings.hpp" #include "remote_connection/RemoteConnectionBindings.hpp" #include "utility/EventsManagerBindings.hpp" +#include "utility/PipelineEventDispatcherBindings.hpp" #ifdef DEPTHAI_HAVE_OPENCV_SUPPORT #include #endif @@ -87,6 +88,7 @@ PYBIND11_MODULE(depthai, m) callstack.push_front(&CalibrationHandlerBindings::bind); callstack.push_front(&ZooBindings::bind); callstack.push_front(&EventsManagerBindings::bind); + callstack.push_front(&PipelineEventDispatcherBindings::bind); callstack.push_front(&RemoteConnectionBindings::bind); callstack.push_front(&FilterParamsBindings::bind); // end of the callstack diff --git a/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp new file mode 100644 index 000000000..42f2b995f --- /dev/null +++ b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp @@ -0,0 +1,27 @@ +#include "PipelineEventDispatcherBindings.hpp" +#include "depthai/utility/PipelineEventDispatcher.hpp" + +void PipelineEventDispatcherBindings::bind(pybind11::module& m, void* pCallstack) { + using namespace dai; + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + // Call the rest of the type defines, then perform the actual bindings + Callstack* callstack = (Callstack*)pCallstack; + auto cb = callstack->top(); + callstack->pop(); + cb(m, pCallstack); + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + + using namespace dai::utility; + auto pipelineEventDispatcher = py::class_(m, "PipelineEventDispatcher"); + + pipelineEventDispatcher + .def(py::init(), py::arg("nodeId"), py::arg("output")) + .def("addEvent", &PipelineEventDispatcher::addEvent, py::arg("source"), py::arg("type"), DOC(dai, utility, PipelineEventDispatcher, addEvent)) + .def("startEvent", &PipelineEventDispatcher::startEvent, py::arg("source"), py::arg("metadata") = std::nullopt, DOC(dai, utility, PipelineEventDispatcher, startEvent)) + .def("endEvent", &PipelineEventDispatcher::endEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, endEvent)) + .def("pingEvent", &PipelineEventDispatcher::pingEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, pingEvent)); +} diff --git a/bindings/python/src/utility/PipelineEventDispatcherBindings.hpp b/bindings/python/src/utility/PipelineEventDispatcherBindings.hpp new file mode 100644 index 000000000..36e074193 --- /dev/null +++ b/bindings/python/src/utility/PipelineEventDispatcherBindings.hpp @@ -0,0 +1,8 @@ +#pragma once + +// pybind +#include "pybind11_common.hpp" + +struct PipelineEventDispatcherBindings { + static void bind(pybind11::module& m, void* pCallstack); +}; diff --git a/include/depthai/pipeline/node/PipelineEventAggregation.hpp b/include/depthai/pipeline/node/PipelineEventAggregation.hpp index f2636a4a9..9d9910fae 100644 --- a/include/depthai/pipeline/node/PipelineEventAggregation.hpp +++ b/include/depthai/pipeline/node/PipelineEventAggregation.hpp @@ -25,14 +25,9 @@ class PipelineEventAggregation : public DeviceNodeCRTP getMessage(const std::shared_ptr getProtoMessage(utility::ByteP case DatatypeEnum::DynamicCalibrationResult: case DatatypeEnum::CalibrationQuality: case DatatypeEnum::CoverageData: + case DatatypeEnum::PipelineEvent: + case DatatypeEnum::PipelineState: throw std::runtime_error("Cannot replay message type: " + std::to_string((int)datatype)); } return {}; diff --git a/src/utility/ProtoSerialize.cpp b/src/utility/ProtoSerialize.cpp index 300600579..d94870502 100644 --- a/src/utility/ProtoSerialize.cpp +++ b/src/utility/ProtoSerialize.cpp @@ -170,6 +170,8 @@ bool deserializationSupported(DatatypeEnum datatype) { case DatatypeEnum::DynamicCalibrationResult: case DatatypeEnum::CalibrationQuality: case DatatypeEnum::CoverageData: + case DatatypeEnum::PipelineEvent: + case DatatypeEnum::PipelineState: return false; } return false; From 9876101176eaa8430120a66af54ea9c6ff691234 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 23 Sep 2025 15:44:49 +0200 Subject: [PATCH 004/124] Integrate pipeline events --- CMakeLists.txt | 2 +- .../datatype/PipelineEventBindings.cpp | 1 + .../node/PipelineEventAggregationBindings.cpp | 4 +-- .../PipelineEventDispatcherBindings.cpp | 3 +- include/depthai/pipeline/MessageQueue.hpp | 11 +++++++- include/depthai/pipeline/Node.hpp | 11 ++++++-- include/depthai/pipeline/ThreadedNode.hpp | 10 ++++++- .../pipeline/datatype/PipelineEvent.hpp | 3 +- .../pipeline/datatype/PipelineState.hpp | 2 +- .../PipelineEventAggregation.hpp | 2 +- .../PipelineEventAggregationProperties.hpp | 0 include/depthai/utility/CircularBuffer.hpp | 2 +- .../utility/PipelineEventDispatcher.hpp | 21 +++++++++----- .../PipelineEventDispatcherInterface.hpp | 19 +++++++++++++ src/pipeline/MessageQueue.cpp | 11 +++++++- src/pipeline/Node.cpp | 4 +++ src/pipeline/Pipeline.cpp | 28 +++++++++++++++++++ src/pipeline/ThreadedNode.cpp | 10 ++++++- .../PipelineEventAggregation.cpp | 23 ++++++++------- src/utility/PipelineEventDispatcher.cpp | 14 ++++++++++ 20 files changed, 148 insertions(+), 33 deletions(-) rename include/depthai/pipeline/node/{ => internal}/PipelineEventAggregation.hpp (93%) rename include/depthai/properties/{ => internal}/PipelineEventAggregationProperties.hpp (100%) create mode 100644 include/depthai/utility/PipelineEventDispatcherInterface.hpp rename src/pipeline/node/{ => internal}/PipelineEventAggregation.cpp (90%) diff --git a/CMakeLists.txt b/CMakeLists.txt index bd1ac83ab..162675b4c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -279,6 +279,7 @@ set(TARGET_CORE_SOURCES src/pipeline/ThreadedNode.cpp src/pipeline/DeviceNode.cpp src/pipeline/DeviceNodeGroup.cpp + src/pipeline/node/internal/PipelineEventAggregation.cpp src/pipeline/node/internal/XLinkIn.cpp src/pipeline/node/internal/XLinkOut.cpp src/pipeline/node/ColorCamera.cpp @@ -318,7 +319,6 @@ set(TARGET_CORE_SOURCES src/pipeline/node/host/RGBD.cpp src/pipeline/datatype/DatatypeEnum.cpp src/pipeline/node/PointCloud.cpp - src/pipeline/node/PipelineEventAggregation.cpp src/pipeline/datatype/Buffer.cpp src/pipeline/datatype/ImgFrame.cpp src/pipeline/datatype/ImgTransformations.cpp diff --git a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp index ea9a7552c..89478d816 100644 --- a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp @@ -43,6 +43,7 @@ void bind_pipelineevent(pybind11::module& m, void* pCallstack) { .def("__repr__", &PipelineEvent::str) .def_readwrite("nodeId", &PipelineEvent::nodeId, DOC(dai, PipelineEvent, nodeId)) .def_readwrite("metadata", &PipelineEvent::metadata, DOC(dai, PipelineEvent, metadata)) + .def_readwrite("timestamp", &PipelineEvent::timestamp, DOC(dai, PipelineEvent, timestamp)) .def_readwrite("duration", &PipelineEvent::duration, DOC(dai, PipelineEvent, duration)) .def_readwrite("type", &PipelineEvent::type, DOC(dai, PipelineEvent, type)) .def_readwrite("source", &PipelineEvent::source, DOC(dai, PipelineEvent, source)) diff --git a/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp index d28be67f7..c9e675f30 100644 --- a/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp +++ b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp @@ -1,6 +1,6 @@ #include "Common.hpp" -#include "depthai/pipeline/node/PipelineEventAggregation.hpp" -#include "depthai/properties/PipelineEventAggregationProperties.hpp" +#include "depthai/pipeline/node/internal/PipelineEventAggregation.hpp" +#include "depthai/properties/internal/PipelineEventAggregationProperties.hpp" void bind_pipelineeventaggregation(pybind11::module& m, void* pCallstack) { using namespace dai; diff --git a/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp index 42f2b995f..422edd72a 100644 --- a/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp +++ b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp @@ -19,7 +19,8 @@ void PipelineEventDispatcherBindings::bind(pybind11::module& m, void* pCallstack auto pipelineEventDispatcher = py::class_(m, "PipelineEventDispatcher"); pipelineEventDispatcher - .def(py::init(), py::arg("nodeId"), py::arg("output")) + .def(py::init(), py::arg("output")) + .def("setNodeId", &PipelineEventDispatcher::setNodeId, py::arg("id"), DOC(dai, utility, PipelineEventDispatcher, setNodeId)) .def("addEvent", &PipelineEventDispatcher::addEvent, py::arg("source"), py::arg("type"), DOC(dai, utility, PipelineEventDispatcher, addEvent)) .def("startEvent", &PipelineEventDispatcher::startEvent, py::arg("source"), py::arg("metadata") = std::nullopt, DOC(dai, utility, PipelineEventDispatcher, startEvent)) .def("endEvent", &PipelineEventDispatcher::endEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, endEvent)) diff --git a/include/depthai/pipeline/MessageQueue.hpp b/include/depthai/pipeline/MessageQueue.hpp index e7667e623..515c5d773 100644 --- a/include/depthai/pipeline/MessageQueue.hpp +++ b/include/depthai/pipeline/MessageQueue.hpp @@ -7,6 +7,7 @@ // project #include "depthai/pipeline/datatype/ADatatype.hpp" #include "depthai/utility/LockingQueue.hpp" +#include "depthai/utility/PipelineEventDispatcherInterface.hpp" // shared namespace dai { @@ -37,11 +38,15 @@ class MessageQueue : public std::enable_shared_from_this { private: void callCallbacks(std::shared_ptr msg); + std::shared_ptr pipelineEventDispatcher; public: // DataOutputQueue constructor explicit MessageQueue(unsigned int maxSize = 16, bool blocking = true); - explicit MessageQueue(std::string name, unsigned int maxSize = 16, bool blocking = true); + explicit MessageQueue(std::string name, + unsigned int maxSize = 16, + bool blocking = true, + std::shared_ptr pipelineEventDispatcher = nullptr); MessageQueue(const MessageQueue& c) : enable_shared_from_this(c), queue(c.queue), name(c.name), callbacks(c.callbacks), uniqueCallbackId(c.uniqueCallbackId){}; @@ -196,11 +201,13 @@ class MessageQueue : public std::enable_shared_from_this { */ template std::shared_ptr tryGet() { + if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(name); if(queue.isDestroyed()) { throw QueueException(CLOSED_QUEUE_MESSAGE); } std::shared_ptr val = nullptr; if(!queue.tryPop(val)) return nullptr; + if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name); return std::dynamic_pointer_cast(val); } @@ -220,10 +227,12 @@ class MessageQueue : public std::enable_shared_from_this { */ template std::shared_ptr get() { + if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(name); std::shared_ptr val = nullptr; if(!queue.waitAndPop(val)) { throw QueueException(CLOSED_QUEUE_MESSAGE); } + if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name); return std::dynamic_pointer_cast(val); } diff --git a/include/depthai/pipeline/Node.hpp b/include/depthai/pipeline/Node.hpp index a491bf894..aa5bdbc33 100644 --- a/include/depthai/pipeline/Node.hpp +++ b/include/depthai/pipeline/Node.hpp @@ -20,6 +20,7 @@ #include "depthai/capabilities/Capability.hpp" #include "depthai/pipeline/datatype/DatatypeEnum.hpp" #include "depthai/properties/Properties.hpp" +#include "depthai/utility/PipelineEventDispatcherInterface.hpp" // libraries #include @@ -83,6 +84,8 @@ class Node : public std::enable_shared_from_this { std::vector inputMapRefs; std::vector*> nodeRefs; + std::shared_ptr pipelineEventDispatcher; + // helpers for setting refs void setOutputRefs(std::initializer_list l); void setOutputRefs(Output* outRef); @@ -129,11 +132,12 @@ class Node : public std::enable_shared_from_this { std::vector queueConnections; Type type = Type::MSender; // Slave sender not supported yet OutputDescription desc; + std::shared_ptr pipelineEventDispatcher; public: // std::vector possibleCapabilities; - Output(Node& par, OutputDescription desc, bool ref = true) : parent(par), desc(std::move(desc)) { + Output(Node& par, OutputDescription desc, bool ref = true) : parent(par), desc(std::move(desc)), pipelineEventDispatcher(par.pipelineEventDispatcher) { // Place oneself to the parents references if(ref) { par.setOutputRefs(this); @@ -141,6 +145,9 @@ class Node : public std::enable_shared_from_this { if(getName().empty()) { setName(par.createUniqueOutputName()); } + if(pipelineEventDispatcher && getName() != "pipelineEventOutput") { + pipelineEventDispatcher->addEvent(getName(), PipelineEvent::EventType::OUTPUT); + } } Node& getParent() { @@ -344,7 +351,7 @@ class Node : public std::enable_shared_from_this { public: std::vector possibleDatatypes; explicit Input(Node& par, InputDescription desc, bool ref = true) - : MessageQueue(std::move(desc.name), desc.queueSize, desc.blocking), + : MessageQueue(std::move(desc.name), desc.queueSize, desc.blocking, par.pipelineEventDispatcher), parent(par), waitForMessage(desc.waitForMessage), possibleDatatypes(std::move(desc.types)) { diff --git a/include/depthai/pipeline/ThreadedNode.hpp b/include/depthai/pipeline/ThreadedNode.hpp index e21768b83..30c2394a8 100644 --- a/include/depthai/pipeline/ThreadedNode.hpp +++ b/include/depthai/pipeline/ThreadedNode.hpp @@ -5,14 +5,22 @@ #include "depthai/utility/AtomicBool.hpp" #include "depthai/utility/JoiningThread.hpp" #include "depthai/utility/spimpl.h" +#include "depthai/utility/PipelineEventDispatcher.hpp" namespace dai { class ThreadedNode : public Node { + friend class PipelineImpl; + private: JoiningThread thread; AtomicBool running{false}; + protected: + Output pipelineEventOutput{*this, {"pipelineEventOutput", DEFAULT_GROUP, {{{DatatypeEnum::PipelineEvent, false}}}}}; + + void initPipelineEventDispatcher(int64_t nodeId); + public: using Node::Node; ThreadedNode(); @@ -43,7 +51,7 @@ class ThreadedNode : public Node { virtual void run() = 0; // check if still running - bool isRunning() const; + bool isRunning(); /** * @brief Sets the logging severity level for this node. diff --git a/include/depthai/pipeline/datatype/PipelineEvent.hpp b/include/depthai/pipeline/datatype/PipelineEvent.hpp index 9ea1b92f2..9b2b67672 100644 --- a/include/depthai/pipeline/datatype/PipelineEvent.hpp +++ b/include/depthai/pipeline/datatype/PipelineEvent.hpp @@ -25,6 +25,7 @@ class PipelineEvent : public Buffer { int64_t nodeId = -1; std::optional metadata; + uint64_t timestamp {0}; uint64_t duration {0}; // Duration in microseconds EventType type = EventType::CUSTOM; std::string source; @@ -34,7 +35,7 @@ class PipelineEvent : public Buffer { datatype = DatatypeEnum::PipelineEvent; }; - DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeId, metadata, duration, type, source); + DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeId, metadata, timestamp, duration, type, source); }; } // namespace dai diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index 64ddbbf34..159dfdbc3 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -30,7 +30,7 @@ class PipelineState : public Buffer { PipelineState() = default; virtual ~PipelineState() = default; - std::unordered_map> nodeStates; + std::unordered_map nodeStates; void serialize(std::vector& metadata, DatatypeEnum& datatype) const override { metadata = utility::serialize(*this); diff --git a/include/depthai/pipeline/node/PipelineEventAggregation.hpp b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp similarity index 93% rename from include/depthai/pipeline/node/PipelineEventAggregation.hpp rename to include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp index 9d9910fae..3dbce6149 100644 --- a/include/depthai/pipeline/node/PipelineEventAggregation.hpp +++ b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp @@ -3,7 +3,7 @@ #include // shared -#include +#include namespace dai { namespace node { diff --git a/include/depthai/properties/PipelineEventAggregationProperties.hpp b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp similarity index 100% rename from include/depthai/properties/PipelineEventAggregationProperties.hpp rename to include/depthai/properties/internal/PipelineEventAggregationProperties.hpp diff --git a/include/depthai/utility/CircularBuffer.hpp b/include/depthai/utility/CircularBuffer.hpp index a0f631d60..6e386eb19 100644 --- a/include/depthai/utility/CircularBuffer.hpp +++ b/include/depthai/utility/CircularBuffer.hpp @@ -11,7 +11,7 @@ class CircularBuffer { CircularBuffer(size_t size) : maxSize(size) { buffer.reserve(size); } - void add(int value) { + void add(T value) { if(buffer.size() < maxSize) { buffer.push_back(value); } else { diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index a26d3546f..78a47ab8c 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -8,11 +8,12 @@ #include "depthai/pipeline/Node.hpp" #include "depthai/pipeline/datatype/PipelineEvent.hpp" +#include "PipelineEventDispatcherInterface.hpp" namespace dai { namespace utility { -class PipelineEventDispatcher { +class PipelineEventDispatcher : public PipelineEventDispatcherInterface { struct EventStatus { PipelineEvent::EventType type; std::chrono::microseconds duration; @@ -21,19 +22,25 @@ class PipelineEventDispatcher { std::optional metadata; }; - int64_t nodeId; + int64_t nodeId = -1; std::unordered_map events; Node::Output* out = nullptr; + void checkNodeId(); + + uint32_t sequenceNum = 0; + public: PipelineEventDispatcher() = delete; - PipelineEventDispatcher(int64_t nodeId, Node::Output* output) : nodeId(nodeId), out(output) {} + PipelineEventDispatcher(Node::Output* output) : out(output) {} + + void setNodeId(int64_t id) override; - void addEvent(const std::string& source, PipelineEvent::EventType type); + void addEvent(const std::string& source, PipelineEvent::EventType type) override; - void startEvent(const std::string& source, std::optional metadata = std::nullopt); // Start event with a start and an end - void endEvent(const std::string& source); // Stop event with a start and an end - void pingEvent(const std::string& source); // Event where stop and start are the same (eg. loop) + void startEvent(const std::string& source, std::optional metadata = std::nullopt) override; // Start event with a start and an end + void endEvent(const std::string& source) override; // Stop event with a start and an end + void pingEvent(const std::string& source) override; // Event where stop and start are the same (eg. loop) }; } // namespace utility diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp new file mode 100644 index 000000000..34bf526f0 --- /dev/null +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "depthai/pipeline/datatype/PipelineEvent.hpp" + +namespace dai { +namespace utility { + +class PipelineEventDispatcherInterface { +public: + virtual ~PipelineEventDispatcherInterface() = default; + virtual void setNodeId(int64_t id) = 0; + virtual void addEvent(const std::string& source, PipelineEvent::EventType type) = 0; + virtual void startEvent(const std::string& source, std::optional metadata = std::nullopt) = 0; // Start event with a start and an end + virtual void endEvent(const std::string& source) = 0; // Stop event with a start and an end + virtual void pingEvent(const std::string& source) = 0; // Event where stop and start are the same (eg. loop) +}; + +} // namespace utility +} // namespace dai diff --git a/src/pipeline/MessageQueue.cpp b/src/pipeline/MessageQueue.cpp index 28f082f16..b8459fb46 100644 --- a/src/pipeline/MessageQueue.cpp +++ b/src/pipeline/MessageQueue.cpp @@ -14,10 +14,19 @@ // Additions #include "spdlog/fmt/bin_to_hex.h" #include "spdlog/fmt/chrono.h" +#include "utility/PipelineEventDispatcherInterface.hpp" namespace dai { -MessageQueue::MessageQueue(std::string name, unsigned int maxSize, bool blocking) : queue(maxSize, blocking), name(std::move(name)) {} +MessageQueue::MessageQueue(std::string name, + unsigned int maxSize, + bool blocking, + std::shared_ptr pipelineEventDispatcher) + : queue(maxSize, blocking), name(std::move(name)), pipelineEventDispatcher(pipelineEventDispatcher) { + if(pipelineEventDispatcher) { + pipelineEventDispatcher->addEvent(name, PipelineEvent::EventType::INPUT); + } +} MessageQueue::MessageQueue(unsigned int maxSize, bool blocking) : queue(maxSize, blocking) {} diff --git a/src/pipeline/Node.cpp b/src/pipeline/Node.cpp index 0bec10444..bfc024dca 100644 --- a/src/pipeline/Node.cpp +++ b/src/pipeline/Node.cpp @@ -239,9 +239,11 @@ void Node::Output::send(const std::shared_ptr& msg) { // } // } // } + if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(getName()); for(auto& messageQueue : connectedInputs) { messageQueue->send(msg); } + if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(getName()); } bool Node::Output::trySend(const std::shared_ptr& msg) { @@ -261,9 +263,11 @@ bool Node::Output::trySend(const std::shared_ptr& msg) { // } // } // } + if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(getName()); for(auto& messageQueue : connectedInputs) { success &= messageQueue->trySend(msg); } + if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(getName()); return success; } diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 7076d2504..f92523b42 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -4,6 +4,7 @@ #include "depthai/device/CalibrationHandler.hpp" #include "depthai/pipeline/ThreadedHostNode.hpp" +#include "depthai/pipeline/node/internal/PipelineEventAggregation.hpp" #include "depthai/pipeline/node/internal/XLinkIn.hpp" #include "depthai/pipeline/node/internal/XLinkInHost.hpp" #include "depthai/pipeline/node/internal/XLinkOut.hpp" @@ -626,6 +627,33 @@ void PipelineImpl::build() { } } + // Create pipeline event aggregator node and link + // Check if any nodes are on host or device + bool hasHostNodes = false; + bool hasDeviceNodes = false; + for(const auto& node : getAllNodes()) { + if(node->runOnHost()) { + hasHostNodes = true; + } else { + hasDeviceNodes = true; + } + } + std::shared_ptr hostEventAgg = nullptr; + std::shared_ptr deviceEventAgg = nullptr; + if(hasHostNodes) hostEventAgg = parent.create(); + if(hasDeviceNodes) deviceEventAgg = parent.create(); + for(auto& node : getAllNodes()) { + auto threadedNode = std::dynamic_pointer_cast(node); + if(threadedNode) { + if(node->runOnHost() && hostEventAgg) { + threadedNode->pipelineEventOutput.link(hostEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); + } else if(!node->runOnHost() && deviceEventAgg) { + threadedNode->pipelineEventOutput.link(deviceEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); + } + } + } + // TODO: link outputs of event aggregation nodes + // Go through the build stages sequentially for(const auto& node : getAllNodes()) { node->buildStage1(); diff --git a/src/pipeline/ThreadedNode.cpp b/src/pipeline/ThreadedNode.cpp index 3f099b782..8455ed571 100644 --- a/src/pipeline/ThreadedNode.cpp +++ b/src/pipeline/ThreadedNode.cpp @@ -18,6 +18,12 @@ ThreadedNode::ThreadedNode() { level = Logging::parseLevel(envLevel); } pimpl->logger->set_level(level); + pipelineEventDispatcher = std::make_shared(&pipelineEventOutput); +} + +void ThreadedNode::initPipelineEventDispatcher(int64_t nodeId) { + pipelineEventDispatcher->setNodeId(nodeId); + pipelineEventDispatcher->addEvent("_mainLoop", PipelineEvent::EventType::LOOP); } void ThreadedNode::start() { @@ -30,6 +36,7 @@ void ThreadedNode::start() { running = true; thread = std::thread([this]() { try { + initPipelineEventDispatcher(this->id); run(); } catch(const MessageQueue::QueueException& ex) { // catch the exception and stop the node @@ -80,7 +87,8 @@ dai::LogLevel ThreadedNode::getLogLevel() const { return spdlogLevelToLogLevel(pimpl->logger->level(), LogLevel::WARN); } -bool ThreadedNode::isRunning() const { +bool ThreadedNode::isRunning() { + this->pipelineEventDispatcher->pingEvent("_mainLoop"); return running; } diff --git a/src/pipeline/node/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp similarity index 90% rename from src/pipeline/node/PipelineEventAggregation.cpp rename to src/pipeline/node/internal/PipelineEventAggregation.cpp index bb4be94b9..0f1ed3aa7 100644 --- a/src/pipeline/node/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -1,4 +1,4 @@ -#include "depthai/pipeline/node/PipelineEventAggregation.hpp" +#include "depthai/pipeline/node/internal/PipelineEventAggregation.hpp" #include "depthai/pipeline/datatype/PipelineEvent.hpp" #include "depthai/pipeline/datatype/PipelineState.hpp" @@ -11,14 +11,19 @@ class NodeEventAggregation { int windowSize; public: - NodeEventAggregation(int windowSize) : windowSize(windowSize) {} + NodeEventAggregation(int windowSize) : windowSize(windowSize), eventsBuffer(windowSize) {} NodeState state; + utility::CircularBuffer eventsBuffer; std::unordered_map>> timingsBufferByType; std::unordered_map>> timingsBufferByInstance; + uint32_t eventCount = 0; + void add(PipelineEvent& event) { // TODO optimize avg - state.events.push_back(event); + ++eventCount; + eventsBuffer.add(event); + state.events = eventsBuffer.getBuffer(); if(timingsBufferByType.find(event.type) == timingsBufferByType.end()) { timingsBufferByType[event.type] = std::make_unique>(windowSize); } @@ -65,9 +70,6 @@ class NodeEventAggregation { state.timingsByInstance[event.source].stdDevMicros = (uint64_t)(std::sqrt(variance)); } } - void resetEvents() { - state.events.clear(); - } }; void PipelineEventAggregation::setRunOnHost(bool runOnHost) { @@ -99,14 +101,11 @@ void PipelineEventAggregation::run() { auto outState = std::make_shared(); bool shouldSend = false; for(auto& [nodeId, nodeState] : nodeStates) { - auto numEvents = nodeState.state.events.size(); - if(numEvents >= properties.eventBatchSize) { + if(nodeState.eventCount >= properties.eventBatchSize) { outState->nodeStates[nodeId] = nodeState.state; - if(!properties.sendEvents) outState->nodeStates[nodeId]->events.clear(); - nodeState.resetEvents(); + if(!properties.sendEvents) outState->nodeStates[nodeId].events.clear(); shouldSend = true; - } else { - outState->nodeStates[nodeId] = std::nullopt; + nodeState.eventCount = 0; } } if(shouldSend) out.send(outState); diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index ceb19451c..b0c59ef1d 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -4,6 +4,14 @@ namespace dai { namespace utility { +void PipelineEventDispatcher::checkNodeId() { + if(nodeId == -1) { + throw std::runtime_error("Node ID not set for PipelineEventDispatcher"); + } +} +void PipelineEventDispatcher::setNodeId(int64_t id) { + nodeId = id; +} void PipelineEventDispatcher::addEvent(const std::string& source, PipelineEvent::EventType type) { if(events.find(source) != events.end()) { throw std::runtime_error("Event with name " + source + " already exists"); @@ -11,6 +19,7 @@ void PipelineEventDispatcher::addEvent(const std::string& source, PipelineEvent: events[source] = {type, {}, {}, false, std::nullopt}; } void PipelineEventDispatcher::startEvent(const std::string& source, std::optional metadata) { + checkNodeId(); if(events.find(source) == events.end()) { throw std::runtime_error("Event with name " + source + " does not exist"); } @@ -23,6 +32,7 @@ void PipelineEventDispatcher::startEvent(const std::string& source, std::optiona event.metadata = metadata; } void PipelineEventDispatcher::endEvent(const std::string& source) { + checkNodeId(); auto now = std::chrono::steady_clock::now(); if(events.find(source) == events.end()) { @@ -37,9 +47,12 @@ void PipelineEventDispatcher::endEvent(const std::string& source) { PipelineEvent pipelineEvent; pipelineEvent.nodeId = nodeId; + pipelineEvent.timestamp = std::chrono::duration_cast(event.timestamp.time_since_epoch()).count(); pipelineEvent.duration = event.duration.count(); pipelineEvent.type = event.type; pipelineEvent.source = source; + pipelineEvent.sequenceNum = sequenceNum++; + pipelineEvent.setTimestampDevice(std::chrono::steady_clock::now()); if(out) { out->send(std::make_shared(pipelineEvent)); @@ -48,6 +61,7 @@ void PipelineEventDispatcher::endEvent(const std::string& source) { event.metadata = std::nullopt; } void PipelineEventDispatcher::pingEvent(const std::string& source) { + checkNodeId(); auto now = std::chrono::steady_clock::now(); if(events.find(source) == events.end()) { From b2024f053e42b3f47fc0ac46a351d7f8c88bddd3 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 24 Sep 2025 09:09:13 +0200 Subject: [PATCH 005/124] RVC4 FW: initial pipeline debugging impl --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 2dc99bffe..14c8b244a 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -4,4 +4,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" # set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+93f7b75a885aa32f44c5e9f53b74470c49d2b1af") -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+f76251a3e31c957cd21183d6170da4f53aac475b") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+34d24327d3bf906cd73f6277fd10752a6e631608") From 75e93861ef847292b320e7318a558f0fbdebf2fc Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 24 Sep 2025 14:41:03 +0200 Subject: [PATCH 006/124] Pipeline events bugfixes, pipeline state merge node --- CMakeLists.txt | 1 + include/depthai/pipeline/Pipeline.hpp | 10 ++++ .../node/internal/PipelineStateMerge.hpp | 32 +++++++++++ src/pipeline/MessageQueue.cpp | 2 +- src/pipeline/Node.cpp | 30 +++++----- src/pipeline/Pipeline.cpp | 23 +++++++- src/pipeline/datatype/DatatypeEnum.cpp | 6 ++ .../internal/PipelineEventAggregation.cpp | 4 ++ .../node/internal/PipelineStateMerge.cpp | 55 +++++++++++++++++++ src/utility/PipelineEventDispatcher.cpp | 10 +++- 10 files changed, 153 insertions(+), 20 deletions(-) create mode 100644 include/depthai/pipeline/node/internal/PipelineStateMerge.hpp create mode 100644 src/pipeline/node/internal/PipelineStateMerge.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 162675b4c..74bb0382c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -280,6 +280,7 @@ set(TARGET_CORE_SOURCES src/pipeline/DeviceNode.cpp src/pipeline/DeviceNodeGroup.cpp src/pipeline/node/internal/PipelineEventAggregation.cpp + src/pipeline/node/internal/PipelineStateMerge.cpp src/pipeline/node/internal/XLinkIn.cpp src/pipeline/node/internal/XLinkOut.cpp src/pipeline/node/ColorCamera.cpp diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index 83457a05f..0a47639f8 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -22,6 +22,7 @@ #include "depthai/pipeline/PipelineSchema.hpp" #include "depthai/properties/GlobalProperties.hpp" #include "depthai/utility/RecordReplay.hpp" +#include "depthai/pipeline/datatype/PipelineState.hpp" namespace dai { @@ -85,6 +86,9 @@ class PipelineImpl : public std::enable_shared_from_this { bool isHostOnly() const; bool isDeviceOnly() const; + // Pipeline state getters + std::shared_ptr getPipelineState(); + // Must be incremented and unique for each node Node::Id latestId = 0; // Pipeline asset manager @@ -115,6 +119,9 @@ class PipelineImpl : public std::enable_shared_from_this { bool removeRecordReplayFiles = true; std::string defaultDeviceId; + // Pipeline events + std::shared_ptr pipelineStateOut; + // Output queues std::vector> outputQueues; @@ -506,6 +513,9 @@ class Pipeline { /// Record and Replay void enableHolisticRecord(const RecordConfig& config); void enableHolisticReplay(const std::string& pathToRecording); + + // Pipeline state getters + std::shared_ptr getPipelineState(); }; } // namespace dai diff --git a/include/depthai/pipeline/node/internal/PipelineStateMerge.hpp b/include/depthai/pipeline/node/internal/PipelineStateMerge.hpp new file mode 100644 index 000000000..dd84f2952 --- /dev/null +++ b/include/depthai/pipeline/node/internal/PipelineStateMerge.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "depthai/pipeline/ThreadedHostNode.hpp" + +namespace dai { +namespace node { + +/** + * @brief PipelineStateMerge node. Merges PipelineState messages from device and host into a single output. + */ +class PipelineStateMerge : public CustomThreadedNode { + bool hasDeviceNodes = false; + bool hasHostNodes = false; + + public: + constexpr static const char* NAME = "PipelineStateMerge"; + + Input inputDevice{*this, {"inputDevice", DEFAULT_GROUP, false, 4, {{DatatypeEnum::PipelineState, false}}}}; + Input inputHost{*this, {"inputHost", DEFAULT_GROUP, false, 4, {{DatatypeEnum::PipelineState, false}}}}; + + std::shared_ptr build(bool hasDeviceNodes, bool hasHostNodes); + + /** + * Output message of type + */ + Output out{*this, {"out", DEFAULT_GROUP, {{{DatatypeEnum::PipelineState, false}}}}}; + + void run() override; +}; + +} // namespace node +} // namespace dai diff --git a/src/pipeline/MessageQueue.cpp b/src/pipeline/MessageQueue.cpp index b8459fb46..9d2aa464e 100644 --- a/src/pipeline/MessageQueue.cpp +++ b/src/pipeline/MessageQueue.cpp @@ -24,7 +24,7 @@ MessageQueue::MessageQueue(std::string name, std::shared_ptr pipelineEventDispatcher) : queue(maxSize, blocking), name(std::move(name)), pipelineEventDispatcher(pipelineEventDispatcher) { if(pipelineEventDispatcher) { - pipelineEventDispatcher->addEvent(name, PipelineEvent::EventType::INPUT); + pipelineEventDispatcher->addEvent(this->name, PipelineEvent::EventType::INPUT); } } diff --git a/src/pipeline/Node.cpp b/src/pipeline/Node.cpp index bfc024dca..7acbd6ae8 100644 --- a/src/pipeline/Node.cpp +++ b/src/pipeline/Node.cpp @@ -318,9 +318,10 @@ Node::OutputMap::OutputMap(Node& parent, Node::OutputDescription defaultOutput, Node::Output& Node::OutputMap::operator[](const std::string& key) { if(count({name, key}) == 0) { // Create using default and rename with group and key - Output output(parent, defaultOutput, false); - output.setGroup(name); - output.setName(key); + auto desc = defaultOutput; + desc.group = name; + desc.name = key; + Output output(parent, desc, false); insert({{name, key}, output}); } // otherwise just return reference to existing @@ -329,11 +330,11 @@ Node::Output& Node::OutputMap::operator[](const std::string& key) { Node::Output& Node::OutputMap::operator[](std::pair groupKey) { if(count(groupKey) == 0) { // Create using default and rename with group and key - Output output(parent, defaultOutput, false); - + auto desc = defaultOutput; // Uses \t (tab) as a special character to parse out as subgroup name - output.setGroup(fmt::format("{}\t{}", name, groupKey.first)); - output.setName(groupKey.second); + desc.group = fmt::format("{}\t{}", name, groupKey.first); + desc.name = groupKey.second; + Output output(parent, desc, false); insert(std::make_pair(groupKey, output)); } // otherwise just return reference to existing @@ -350,9 +351,10 @@ Node::InputMap::InputMap(Node& parent, Node::InputDescription description) : Inp Node::Input& Node::InputMap::operator[](const std::string& key) { if(count({name, key}) == 0) { // Create using default and rename with group and key - Input input(parent, defaultInput, false); - input.setGroup(name); - input.setName(key); + auto desc = defaultInput; + desc.group = name; + desc.name = key; + Input input(parent, desc, false); insert({{name, key}, input}); } // otherwise just return reference to existing @@ -361,11 +363,11 @@ Node::Input& Node::InputMap::operator[](const std::string& key) { Node::Input& Node::InputMap::operator[](std::pair groupKey) { if(count(groupKey) == 0) { // Create using default and rename with group and key - Input input(parent, defaultInput, false); - + auto desc = defaultInput; // Uses \t (tab) as a special character to parse out as subgroup name - input.setGroup(fmt::format("{}\t{}", name, groupKey.first)); - input.setName(groupKey.second); + desc.group = fmt::format("{}\t{}", name, groupKey.first); + desc.name = groupKey.second; + Input input(parent, desc, false); insert(std::make_pair(groupKey, input)); } // otherwise just return reference to existing diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index f92523b42..9d05b9580 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -12,6 +12,7 @@ #include "depthai/utility/Initialization.hpp" #include "pipeline/datatype/ImgFrame.hpp" #include "pipeline/node/DetectionNetwork.hpp" +#include "pipeline/node/internal/PipelineStateMerge.hpp" #include "utility/Compression.hpp" #include "utility/Environment.hpp" #include "utility/ErrorMacros.hpp" @@ -478,6 +479,11 @@ bool PipelineImpl::isDeviceOnly() const { return deviceOnly; } +std::shared_ptr PipelineImpl::getPipelineState() { + auto pipelineState = pipelineStateOut->get(); + return pipelineState; +} + void PipelineImpl::add(std::shared_ptr node) { if(node == nullptr) { throw std::invalid_argument(fmt::format("Given node pointer is null")); @@ -537,7 +543,6 @@ bool PipelineImpl::isBuilt() const { void PipelineImpl::build() { // TODO(themarpe) - add mutex and set running up ahead if(isBuild) return; - isBuild = true; if(defaultDevice) { auto recordPath = std::filesystem::path(utility::getEnvAs("DEPTHAI_RECORD", "")); @@ -652,7 +657,16 @@ void PipelineImpl::build() { } } } - // TODO: link outputs of event aggregation nodes + auto stateMerge = parent.create()->build(hasDeviceNodes, hasHostNodes); + if(deviceEventAgg) { + deviceEventAgg->out.link(stateMerge->inputDevice); + } + if(hostEventAgg) { + hostEventAgg->out.link(stateMerge->inputHost); + } + pipelineStateOut = stateMerge->out.createOutputQueue(1, false); + + isBuild = true; // Go through the build stages sequentially for(const auto& node : getAllNodes()) { @@ -1065,4 +1079,9 @@ void Pipeline::enableHolisticReplay(const std::string& pathToRecording) { impl()->recordConfig.state = RecordConfig::RecordReplayState::REPLAY; impl()->enableHolisticRecordReplay = true; } + +std::shared_ptr Pipeline::getPipelineState() { + return impl()->getPipelineState(); +} + } // namespace dai diff --git a/src/pipeline/datatype/DatatypeEnum.cpp b/src/pipeline/datatype/DatatypeEnum.cpp index 32ec647eb..91bfb3231 100644 --- a/src/pipeline/datatype/DatatypeEnum.cpp +++ b/src/pipeline/datatype/DatatypeEnum.cpp @@ -36,6 +36,8 @@ const std::unordered_map> hierarchy = { DatatypeEnum::AprilTags, DatatypeEnum::BenchmarkReport, DatatypeEnum::MessageGroup, + DatatypeEnum::PipelineEvent, + DatatypeEnum::PipelineState, DatatypeEnum::PointCloudConfig, DatatypeEnum::PointCloudData, DatatypeEnum::RGBDData, @@ -73,6 +75,8 @@ const std::unordered_map> hierarchy = { DatatypeEnum::AprilTags, DatatypeEnum::BenchmarkReport, DatatypeEnum::MessageGroup, + DatatypeEnum::PipelineEvent, + DatatypeEnum::PipelineState, DatatypeEnum::PointCloudConfig, DatatypeEnum::PointCloudData, DatatypeEnum::RGBDData, @@ -108,6 +112,8 @@ const std::unordered_map> hierarchy = { {DatatypeEnum::AprilTags, {}}, {DatatypeEnum::BenchmarkReport, {}}, {DatatypeEnum::MessageGroup, {}}, + {DatatypeEnum::PipelineEvent, {}}, + {DatatypeEnum::PipelineState, {}}, {DatatypeEnum::PointCloudConfig, {}}, {DatatypeEnum::PointCloudData, {}}, {DatatypeEnum::RGBDData, {}}, diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 0f1ed3aa7..019452118 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -85,6 +85,7 @@ bool PipelineEventAggregation::runOnHost() const { void PipelineEventAggregation::run() { std::unordered_map nodeStates; + uint32_t sequenceNum = 0; while(isRunning()) { std::unordered_map> events; for(auto& [k, v] : inputs) { @@ -108,6 +109,9 @@ void PipelineEventAggregation::run() { nodeState.eventCount = 0; } } + outState->sequenceNum = sequenceNum++; + outState->setTimestamp(std::chrono::steady_clock::now()); + outState->tsDevice = outState->ts; if(shouldSend) out.send(outState); } } diff --git a/src/pipeline/node/internal/PipelineStateMerge.cpp b/src/pipeline/node/internal/PipelineStateMerge.cpp new file mode 100644 index 000000000..177ec3325 --- /dev/null +++ b/src/pipeline/node/internal/PipelineStateMerge.cpp @@ -0,0 +1,55 @@ +#include "depthai/pipeline/node/internal/PipelineStateMerge.hpp" + +#include "depthai/pipeline/datatype/PipelineState.hpp" + +#include "pipeline/ThreadedNodeImpl.hpp" + +namespace dai { +namespace node { + +std::shared_ptr PipelineStateMerge::build(bool hasDeviceNodes, bool hasHostNodes) { + this->hasDeviceNodes = hasDeviceNodes; + this->hasHostNodes = hasHostNodes; + return std::static_pointer_cast(shared_from_this()); +} + +void mergeStates(std::shared_ptr& outState, const std::shared_ptr& inState) { + for(const auto& [key, value] : inState->nodeStates) { + if(outState->nodeStates.find(key) != outState->nodeStates.end()) { + throw std::runtime_error("PipelineStateMerge: Duplicate node state for nodeId " + std::to_string(key)); + } else outState->nodeStates[key] = value; + } +} +void PipelineStateMerge::run() { + auto& logger = pimpl->logger; + if(!hasDeviceNodes && !hasHostNodes) { + logger->warn("PipelineStateMerge: both device and host nodes are disabled. Have you built the node?"); + } + uint32_t sequenceNum = 0; + while(isRunning()) { + auto outState = std::make_shared(); + if(hasDeviceNodes) { + auto deviceState = inputDevice.get(); + if(deviceState != nullptr) { + *outState = *deviceState; + } + } + if(hasHostNodes) { + auto hostState = inputHost.get(); + if(hostState != nullptr) { + if(hasDeviceNodes) { + // merge + mergeStates(outState, hostState); + auto minTimestamp = std::min(outState->getTimestamp(), hostState->getTimestamp()); + outState->setTimestamp(minTimestamp); + outState->sequenceNum = sequenceNum++; + } else { + *outState = *hostState; + } + } + } + out.send(outState); + } +} +} // namespace node +} // namespace dai diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index b0c59ef1d..2243f5d2a 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -1,5 +1,6 @@ #include "depthai/utility/PipelineEventDispatcher.hpp" #include +#include namespace dai { namespace utility { @@ -13,10 +14,12 @@ void PipelineEventDispatcher::setNodeId(int64_t id) { nodeId = id; } void PipelineEventDispatcher::addEvent(const std::string& source, PipelineEvent::EventType type) { - if(events.find(source) != events.end()) { - throw std::runtime_error("Event with name " + source + " already exists"); + if(!source.empty()) { + if(events.find(source) != events.end()) { + throw std::runtime_error("Event with name '" + source + "' already exists"); + } + events[source] = {type, {}, {}, false, std::nullopt}; } - events[source] = {type, {}, {}, false, std::nullopt}; } void PipelineEventDispatcher::startEvent(const std::string& source, std::optional metadata) { checkNodeId(); @@ -53,6 +56,7 @@ void PipelineEventDispatcher::endEvent(const std::string& source) { pipelineEvent.source = source; pipelineEvent.sequenceNum = sequenceNum++; pipelineEvent.setTimestampDevice(std::chrono::steady_clock::now()); + pipelineEvent.ts = pipelineEvent.tsDevice; if(out) { out->send(std::make_shared(pipelineEvent)); From 99ce452b408e7f19bcce8561b45868bbcd3ece1d Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 25 Sep 2025 18:23:55 +0200 Subject: [PATCH 007/124] Some pipeline debugging bugfixes --- .../node/PipelineEventAggregationBindings.cpp | 2 +- include/depthai/pipeline/Node.hpp | 3 ++- .../node/internal/PipelineEventAggregation.hpp | 4 +++- src/pipeline/Pipeline.cpp | 18 ++++++++++++------ .../node/internal/PipelineEventAggregation.cpp | 3 +++ 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp index c9e675f30..64d1fd539 100644 --- a/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp +++ b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp @@ -4,7 +4,7 @@ void bind_pipelineeventaggregation(pybind11::module& m, void* pCallstack) { using namespace dai; - using namespace dai::node; + using namespace dai::node::internal; // Node and Properties declare upfront py::class_ pipelineEventAggregationProperties( diff --git a/include/depthai/pipeline/Node.hpp b/include/depthai/pipeline/Node.hpp index aa5bdbc33..9792d6ec9 100644 --- a/include/depthai/pipeline/Node.hpp +++ b/include/depthai/pipeline/Node.hpp @@ -351,9 +351,10 @@ class Node : public std::enable_shared_from_this { public: std::vector possibleDatatypes; explicit Input(Node& par, InputDescription desc, bool ref = true) - : MessageQueue(std::move(desc.name), desc.queueSize, desc.blocking, par.pipelineEventDispatcher), + : MessageQueue(desc.name, desc.queueSize, desc.blocking, par.pipelineEventDispatcher), parent(par), waitForMessage(desc.waitForMessage), + group(desc.group), possibleDatatypes(std::move(desc.types)) { if(ref) { par.setInputRefs(this); diff --git a/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp index 3dbce6149..74a6a31be 100644 --- a/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp +++ b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp @@ -7,6 +7,7 @@ namespace dai { namespace node { +namespace internal { /** * @brief Sync node. Performs syncing between image frames @@ -22,7 +23,7 @@ class PipelineEventAggregation : public DeviceNodeCRTP hostEventAgg = nullptr; - std::shared_ptr deviceEventAgg = nullptr; - if(hasHostNodes) hostEventAgg = parent.create(); - if(hasDeviceNodes) deviceEventAgg = parent.create(); + std::shared_ptr hostEventAgg = nullptr; + std::shared_ptr deviceEventAgg = nullptr; + if(hasHostNodes) { + hostEventAgg = parent.create(); + hostEventAgg->setRunOnHost(true); + } + if(hasDeviceNodes) { + deviceEventAgg = parent.create(); + deviceEventAgg->setRunOnHost(false); + } for(auto& node : getAllNodes()) { auto threadedNode = std::dynamic_pointer_cast(node); if(threadedNode) { - if(node->runOnHost() && hostEventAgg) { + if(node->runOnHost() && hostEventAgg && node->id != hostEventAgg->id) { threadedNode->pipelineEventOutput.link(hostEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); - } else if(!node->runOnHost() && deviceEventAgg) { + } else if(!node->runOnHost() && deviceEventAgg && node->id != deviceEventAgg->id) { threadedNode->pipelineEventOutput.link(deviceEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); } } diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 019452118..95f5c104f 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -6,6 +6,7 @@ namespace dai { namespace node { +namespace internal { class NodeEventAggregation { int windowSize; @@ -115,5 +116,7 @@ void PipelineEventAggregation::run() { if(shouldSend) out.send(outState); } } + +} // namespace internal } // namespace node } // namespace dai From 914cbc56f2da7358f1228802bf2cc386415742ec Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 26 Sep 2025 14:21:15 +0200 Subject: [PATCH 008/124] Fix pipeline events on host --- examples/cpp/HostNodes/CMakeLists.txt | 3 ++- examples/cpp/HostNodes/image_manip_host.cpp | 13 ++++++++----- include/depthai/pipeline/MessageQueue.hpp | 5 ++++- include/depthai/pipeline/Node.hpp | 5 +---- src/pipeline/ThreadedNode.cpp | 2 +- src/utility/PipelineEventDispatcher.cpp | 2 +- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/examples/cpp/HostNodes/CMakeLists.txt b/examples/cpp/HostNodes/CMakeLists.txt index ba9f11135..f4bd4187f 100644 --- a/examples/cpp/HostNodes/CMakeLists.txt +++ b/examples/cpp/HostNodes/CMakeLists.txt @@ -17,6 +17,7 @@ dai_set_example_test_labels(host_node ondevice rvc2_all rvc4 ci) dai_add_example(image_manip_host image_manip_host.cpp ON OFF "${construction_vest}") dai_set_example_test_labels(image_manip_host ondevice rvc2_all rvc4 rvc4rgb ci) +target_compile_definitions(image_manip_host PRIVATE VIDEO_PATH="${construction_vest}") dai_add_example(image_manip_color_conversion_host image_manip_color_conversion.cpp ON OFF) dai_set_example_test_labels(image_manip_color_conversion_host ondevice rvc2_all rvc4 rvc4rgb ci) @@ -32,4 +33,4 @@ dai_set_example_test_labels(threaded_host_node ondevice rvc2_all rvc4 rvc4rgb ci dai_add_example(host_pipeline_synced_node host_pipeline_synced_node.cpp ON OFF) dai_set_example_test_labels(host_pipeline_synced_node ondevice rvc2_all rvc4 ci) -dai_add_example(host_only_camera host_only_camera.cpp OFF OFF) \ No newline at end of file +dai_add_example(host_only_camera host_only_camera.cpp OFF OFF) diff --git a/examples/cpp/HostNodes/image_manip_host.cpp b/examples/cpp/HostNodes/image_manip_host.cpp index ba1d5df8c..c43d75c37 100644 --- a/examples/cpp/HostNodes/image_manip_host.cpp +++ b/examples/cpp/HostNodes/image_manip_host.cpp @@ -5,11 +5,14 @@ #include "depthai/pipeline/node/host/Display.hpp" #include "depthai/pipeline/node/host/Replay.hpp" +#ifndef VIDEO_PATH +#define VIDEO_PATH "" +#endif + int main(int argc, char** argv) { - if(argc <= 1) { - std::cout << "Video parameter is missing" << std::endl; - std::cout << "Usage: ./image_manip_host video_path" << std::endl; - return -1; + std::string videoFile = VIDEO_PATH; + if(argc > 1) { + videoFile = argv[1]; } dai::Pipeline pipeline(false); @@ -27,7 +30,7 @@ int main(int argc, char** argv) { manip->initialConfig->addFlipVertical(); manip->initialConfig->setFrameType(dai::ImgFrame::Type::RGB888p); - replay->setReplayVideoFile(argv[1]); + replay->setReplayVideoFile(videoFile); replay->setOutFrameType(dai::ImgFrame::Type::NV12); replay->setFps(30); replay->setSize(1280, 720); diff --git a/include/depthai/pipeline/MessageQueue.hpp b/include/depthai/pipeline/MessageQueue.hpp index 515c5d773..5c39bf41e 100644 --- a/include/depthai/pipeline/MessageQueue.hpp +++ b/include/depthai/pipeline/MessageQueue.hpp @@ -206,7 +206,10 @@ class MessageQueue : public std::enable_shared_from_this { throw QueueException(CLOSED_QUEUE_MESSAGE); } std::shared_ptr val = nullptr; - if(!queue.tryPop(val)) return nullptr; + if(!queue.tryPop(val)) { + if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name); + return nullptr; + } if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name); return std::dynamic_pointer_cast(val); } diff --git a/include/depthai/pipeline/Node.hpp b/include/depthai/pipeline/Node.hpp index 9792d6ec9..c61c75498 100644 --- a/include/depthai/pipeline/Node.hpp +++ b/include/depthai/pipeline/Node.hpp @@ -351,7 +351,7 @@ class Node : public std::enable_shared_from_this { public: std::vector possibleDatatypes; explicit Input(Node& par, InputDescription desc, bool ref = true) - : MessageQueue(desc.name, desc.queueSize, desc.blocking, par.pipelineEventDispatcher), + : MessageQueue(desc.name.empty() ? par.createUniqueInputName() : desc.name, desc.queueSize, desc.blocking, par.pipelineEventDispatcher), parent(par), waitForMessage(desc.waitForMessage), group(desc.group), @@ -359,9 +359,6 @@ class Node : public std::enable_shared_from_this { if(ref) { par.setInputRefs(this); } - if(getName().empty()) { - setName(par.createUniqueInputName()); - } } /** diff --git a/src/pipeline/ThreadedNode.cpp b/src/pipeline/ThreadedNode.cpp index 8455ed571..246f7ecc8 100644 --- a/src/pipeline/ThreadedNode.cpp +++ b/src/pipeline/ThreadedNode.cpp @@ -27,6 +27,7 @@ void ThreadedNode::initPipelineEventDispatcher(int64_t nodeId) { } void ThreadedNode::start() { + initPipelineEventDispatcher(this->id); // A node should not be started if it is already running // We would be creating multiple threads for the same node DAI_CHECK_V(!isRunning(), "Node with id {} is already running. Cannot start it again. Node name: {}", id, getName()); @@ -36,7 +37,6 @@ void ThreadedNode::start() { running = true; thread = std::thread([this]() { try { - initPipelineEventDispatcher(this->id); run(); } catch(const MessageQueue::QueueException& ex) { // catch the exception and stop the node diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index 2243f5d2a..0f379e0c2 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -7,7 +7,7 @@ namespace utility { void PipelineEventDispatcher::checkNodeId() { if(nodeId == -1) { - throw std::runtime_error("Node ID not set for PipelineEventDispatcher"); + throw std::runtime_error("Node ID not set on PipelineEventDispatcher"); } } void PipelineEventDispatcher::setNodeId(int64_t id) { From d30b5fdc842a9c349fcb9c433f7076bef44c1c51 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 30 Sep 2025 09:24:52 +0200 Subject: [PATCH 009/124] Fix RVC4 pipeline events crash --- include/depthai/pipeline/Pipeline.hpp | 5 + src/pipeline/Pipeline.cpp | 216 +++++++++++++------------- 2 files changed, 116 insertions(+), 105 deletions(-) diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index 0a47639f8..df460980e 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -118,6 +118,7 @@ class PipelineImpl : public std::enable_shared_from_this { std::unordered_map recordReplayFilenames; bool removeRecordReplayFiles = true; std::string defaultDeviceId; + bool pipelineOnHost = true; // Pipeline events std::shared_ptr pipelineStateOut; @@ -484,6 +485,10 @@ class Pipeline { void build() { impl()->build(); } + void buildDevice() { + impl()->pipelineOnHost = false; + impl()->build(); + } void start() { impl()->start(); } diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 4c088ef34..b002329bd 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -544,133 +544,139 @@ void PipelineImpl::build() { // TODO(themarpe) - add mutex and set running up ahead if(isBuild) return; - if(defaultDevice) { - auto recordPath = std::filesystem::path(utility::getEnvAs("DEPTHAI_RECORD", "")); - auto replayPath = std::filesystem::path(utility::getEnvAs("DEPTHAI_REPLAY", "")); - - if(defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_2 - || defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_X - || defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_RVC4) { - try { + if(pipelineOnHost) { + if(defaultDevice) { + auto recordPath = std::filesystem::path(utility::getEnvAs("DEPTHAI_RECORD", "")); + auto replayPath = std::filesystem::path(utility::getEnvAs("DEPTHAI_REPLAY", "")); + + if(defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_2 + || defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_X + || defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_RVC4) { + try { #ifdef DEPTHAI_MERGED_TARGET - if(enableHolisticRecordReplay) { - switch(recordConfig.state) { - case RecordConfig::RecordReplayState::RECORD: - recordPath = recordConfig.outputDir; - replayPath = ""; - break; - case RecordConfig::RecordReplayState::REPLAY: - recordPath = ""; - replayPath = recordConfig.outputDir; - break; - case RecordConfig::RecordReplayState::NONE: - enableHolisticRecordReplay = false; - break; + if(enableHolisticRecordReplay) { + switch(recordConfig.state) { + case RecordConfig::RecordReplayState::RECORD: + recordPath = recordConfig.outputDir; + replayPath = ""; + break; + case RecordConfig::RecordReplayState::REPLAY: + recordPath = ""; + replayPath = recordConfig.outputDir; + break; + case RecordConfig::RecordReplayState::NONE: + enableHolisticRecordReplay = false; + break; + } } - } - defaultDeviceId = defaultDevice->getDeviceId(); - - if(!recordPath.empty() && !replayPath.empty()) { - Logging::getInstance().logger.warn("Both DEPTHAI_RECORD and DEPTHAI_REPLAY are set. Record and replay disabled."); - } else if(!recordPath.empty()) { - if(enableHolisticRecordReplay || utility::checkRecordConfig(recordPath, recordConfig)) { - if(platform::checkWritePermissions(recordPath)) { - if(utility::setupHolisticRecord(parent, - defaultDeviceId, - recordConfig, - recordReplayFilenames, - defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_2 - || defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_X)) { - recordConfig.state = RecordConfig::RecordReplayState::RECORD; - Logging::getInstance().logger.info("Record enabled."); + defaultDeviceId = defaultDevice->getDeviceId(); + + if(!recordPath.empty() && !replayPath.empty()) { + Logging::getInstance().logger.warn("Both DEPTHAI_RECORD and DEPTHAI_REPLAY are set. Record and replay disabled."); + } else if(!recordPath.empty()) { + if(enableHolisticRecordReplay || utility::checkRecordConfig(recordPath, recordConfig)) { + if(platform::checkWritePermissions(recordPath)) { + if(utility::setupHolisticRecord(parent, + defaultDeviceId, + recordConfig, + recordReplayFilenames, + defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_2 + || defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_X)) { + recordConfig.state = RecordConfig::RecordReplayState::RECORD; + Logging::getInstance().logger.info("Record enabled."); + } else { + Logging::getInstance().logger.warn("Could not set up holistic record. Record and replay disabled."); + } } else { - Logging::getInstance().logger.warn("Could not set up holistic record. Record and replay disabled."); + Logging::getInstance().logger.warn("DEPTHAI_RECORD path does not have write permissions. Record disabled."); } } else { - Logging::getInstance().logger.warn("DEPTHAI_RECORD path does not have write permissions. Record disabled."); + Logging::getInstance().logger.warn("Could not successfully parse DEPTHAI_RECORD. Record disabled."); } - } else { - Logging::getInstance().logger.warn("Could not successfully parse DEPTHAI_RECORD. Record disabled."); - } - } else if(!replayPath.empty()) { - if(platform::checkPathExists(replayPath)) { - if(platform::checkWritePermissions(replayPath)) { - if(utility::setupHolisticReplay(parent, - replayPath, - defaultDeviceId, - recordConfig, - recordReplayFilenames, - defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_2 - || defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_X)) { - recordConfig.state = RecordConfig::RecordReplayState::REPLAY; - if(platform::checkPathExists(replayPath, true)) { - removeRecordReplayFiles = false; + } else if(!replayPath.empty()) { + if(platform::checkPathExists(replayPath)) { + if(platform::checkWritePermissions(replayPath)) { + if(utility::setupHolisticReplay(parent, + replayPath, + defaultDeviceId, + recordConfig, + recordReplayFilenames, + defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_2 + || defaultDevice->getDeviceInfo().platform == XLinkPlatform_t::X_LINK_MYRIAD_X)) { + recordConfig.state = RecordConfig::RecordReplayState::REPLAY; + if(platform::checkPathExists(replayPath, true)) { + removeRecordReplayFiles = false; + } + Logging::getInstance().logger.info("Replay enabled."); + } else { + Logging::getInstance().logger.warn("Could not set up holistic replay. Record and replay disabled."); } - Logging::getInstance().logger.info("Replay enabled."); } else { - Logging::getInstance().logger.warn("Could not set up holistic replay. Record and replay disabled."); + Logging::getInstance().logger.warn("DEPTHAI_REPLAY path does not have write permissions. Replay disabled."); } } else { - Logging::getInstance().logger.warn("DEPTHAI_REPLAY path does not have write permissions. Replay disabled."); + Logging::getInstance().logger.warn("DEPTHAI_REPLAY path does not exist or is invalid. Replay disabled."); } - } else { - Logging::getInstance().logger.warn("DEPTHAI_REPLAY path does not exist or is invalid. Replay disabled."); } - } #else - recordConfig.state = RecordConfig::RecordReplayState::NONE; - if(!recordPath.empty() || !replayPath.empty()) { - Logging::getInstance().logger.warn("Merged target is required to use holistic record/replay."); - } + recordConfig.state = RecordConfig::RecordReplayState::NONE; + if(!recordPath.empty() || !replayPath.empty()) { + Logging::getInstance().logger.warn("Merged target is required to use holistic record/replay."); + } #endif - } catch(std::runtime_error& e) { - Logging::getInstance().logger.warn("Could not set up record / replay: {}", e.what()); + } catch(std::runtime_error& e) { + Logging::getInstance().logger.warn("Could not set up record / replay: {}", e.what()); + } + } else if(enableHolisticRecordReplay || !recordPath.empty() || !replayPath.empty()) { + throw std::runtime_error("Holistic record/replay is only supported on RVC2 devices for now."); } - } else if(enableHolisticRecordReplay || !recordPath.empty() || !replayPath.empty()) { - throw std::runtime_error("Holistic record/replay is only supported on RVC2 devices for now."); } - } - // Create pipeline event aggregator node and link - // Check if any nodes are on host or device - bool hasHostNodes = false; - bool hasDeviceNodes = false; - for(const auto& node : getAllNodes()) { - if(node->runOnHost()) { - hasHostNodes = true; - } else { - hasDeviceNodes = true; + // Create pipeline event aggregator node and link + // Check if any nodes are on host or device + bool hasHostNodes = false; + bool hasDeviceNodes = false; + for(const auto& node : getAllNodes()) { + if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; + + if(node->runOnHost()) { + hasHostNodes = true; + } else { + hasDeviceNodes = true; + } } - } - std::shared_ptr hostEventAgg = nullptr; - std::shared_ptr deviceEventAgg = nullptr; - if(hasHostNodes) { - hostEventAgg = parent.create(); - hostEventAgg->setRunOnHost(true); - } - if(hasDeviceNodes) { - deviceEventAgg = parent.create(); - deviceEventAgg->setRunOnHost(false); - } - for(auto& node : getAllNodes()) { - auto threadedNode = std::dynamic_pointer_cast(node); - if(threadedNode) { - if(node->runOnHost() && hostEventAgg && node->id != hostEventAgg->id) { - threadedNode->pipelineEventOutput.link(hostEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); - } else if(!node->runOnHost() && deviceEventAgg && node->id != deviceEventAgg->id) { - threadedNode->pipelineEventOutput.link(deviceEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); + std::shared_ptr hostEventAgg = nullptr; + std::shared_ptr deviceEventAgg = nullptr; + if(hasHostNodes) { + hostEventAgg = parent.create(); + hostEventAgg->setRunOnHost(true); + } + if(hasDeviceNodes) { + deviceEventAgg = parent.create(); + deviceEventAgg->setRunOnHost(false); + } + for(auto& node : getAllNodes()) { + if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; + + auto threadedNode = std::dynamic_pointer_cast(node); + if(threadedNode) { + if(node->runOnHost() && hostEventAgg && node->id != hostEventAgg->id) { + threadedNode->pipelineEventOutput.link(hostEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); + } else if(!node->runOnHost() && deviceEventAgg && node->id != deviceEventAgg->id) { + threadedNode->pipelineEventOutput.link(deviceEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); + } } } + auto stateMerge = parent.create()->build(hasDeviceNodes, hasHostNodes); + if(deviceEventAgg) { + deviceEventAgg->out.link(stateMerge->inputDevice); + } + if(hostEventAgg) { + hostEventAgg->out.link(stateMerge->inputHost); + } + pipelineStateOut = stateMerge->out.createOutputQueue(1, false); } - auto stateMerge = parent.create()->build(hasDeviceNodes, hasHostNodes); - if(deviceEventAgg) { - deviceEventAgg->out.link(stateMerge->inputDevice); - } - if(hostEventAgg) { - hostEventAgg->out.link(stateMerge->inputHost); - } - pipelineStateOut = stateMerge->out.createOutputQueue(1, false); isBuild = true; From 3a579ac2f4ea0ec4cececc6d337221b3d980c762 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 30 Sep 2025 09:26:27 +0200 Subject: [PATCH 010/124] RVC4 FW: Fix pipeline events crash --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 14c8b244a..cfcb0c77f 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -4,4 +4,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" # set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+93f7b75a885aa32f44c5e9f53b74470c49d2b1af") -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+34d24327d3bf906cd73f6277fd10752a6e631608") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+8e58c765badb38f3efa655c203a0429470600e5d") From 2e9b26dda8a341773e113bf4b7e6c896766f640c Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 1 Oct 2025 16:29:37 +0200 Subject: [PATCH 011/124] Add more stats, queue state WIP --- .../datatype/PipelineEventBindings.cpp | 18 +- .../datatype/PipelineStateBindings.cpp | 42 ++- include/depthai/pipeline/Node.hpp | 2 +- .../pipeline/datatype/PipelineEvent.hpp | 18 +- .../pipeline/datatype/PipelineState.hpp | 43 ++- .../utility/PipelineEventDispatcher.hpp | 7 +- .../PipelineEventDispatcherInterface.hpp | 4 +- src/pipeline/MessageQueue.cpp | 2 +- src/pipeline/ThreadedNode.cpp | 2 +- .../internal/PipelineEventAggregation.cpp | 246 ++++++++++++++---- src/utility/PipelineEventDispatcher.cpp | 69 ++--- 11 files changed, 333 insertions(+), 120 deletions(-) diff --git a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp index 89478d816..65f212520 100644 --- a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp @@ -17,7 +17,8 @@ void bind_pipelineevent(pybind11::module& m, void* pCallstack) { using namespace dai; py::class_, Buffer, std::shared_ptr> pipelineEvent(m, "PipelineEvent", DOC(dai, PipelineEvent)); - py::enum_ pipelineEventType(pipelineEvent, "EventType", DOC(dai, PipelineEvent, EventType)); + py::enum_ pipelineEventType(pipelineEvent, "Type", DOC(dai, PipelineEvent, Type)); + py::enum_ pipelineEventInterval(pipelineEvent, "Interval", DOC(dai, PipelineEvent, Interval)); /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// @@ -32,19 +33,20 @@ void bind_pipelineevent(pybind11::module& m, void* pCallstack) { /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// - pipelineEventType.value("CUSTOM", PipelineEvent::EventType::CUSTOM) - .value("LOOP", PipelineEvent::EventType::LOOP) - .value("INPUT", PipelineEvent::EventType::INPUT) - .value("OUTPUT", PipelineEvent::EventType::OUTPUT) - .value("FUNC_CALL", PipelineEvent::EventType::FUNC_CALL); + pipelineEventType.value("CUSTOM", PipelineEvent::Type::CUSTOM) + .value("LOOP", PipelineEvent::Type::LOOP) + .value("INPUT", PipelineEvent::Type::INPUT) + .value("OUTPUT", PipelineEvent::Type::OUTPUT); + pipelineEventInterval.value("NONE", PipelineEvent::Interval::NONE) + .value("START", PipelineEvent::Interval::START) + .value("END", PipelineEvent::Interval::END); // Message pipelineEvent.def(py::init<>()) .def("__repr__", &PipelineEvent::str) .def_readwrite("nodeId", &PipelineEvent::nodeId, DOC(dai, PipelineEvent, nodeId)) .def_readwrite("metadata", &PipelineEvent::metadata, DOC(dai, PipelineEvent, metadata)) - .def_readwrite("timestamp", &PipelineEvent::timestamp, DOC(dai, PipelineEvent, timestamp)) - .def_readwrite("duration", &PipelineEvent::duration, DOC(dai, PipelineEvent, duration)) + .def_readwrite("interval", &PipelineEvent::interval, DOC(dai, PipelineEvent, interval)) .def_readwrite("type", &PipelineEvent::type, DOC(dai, PipelineEvent, type)) .def_readwrite("source", &PipelineEvent::source, DOC(dai, PipelineEvent, source)) .def("getTimestamp", &PipelineEvent::Buffer::getTimestamp, DOC(dai, Buffer, getTimestamp)) diff --git a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp index 8b59cc6ea..eff2c7bdd 100644 --- a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp @@ -17,7 +17,10 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { using namespace dai; py::class_ nodeState(m, "NodeState", DOC(dai, NodeState)); - py::class_ nodeStateTiming(nodeState, "Timing", DOC(dai, NodeState, Timing)); + py::class_ durationEvent(nodeState, "DurationEvent", DOC(dai, NodeState, DurationEvent)); + py::class_ nodeStateTimingStats(nodeState, "TimingStats", DOC(dai, NodeState, TimingStats)); + py::class_ nodeStateQueueStats(nodeState, "QueueStats", DOC(dai, NodeState, QueueStats)); + py::class_ nodeStateQueueState(nodeState, "QueueState", DOC(dai, NodeState, QueueState)); py::class_, Buffer, std::shared_ptr> pipelineState(m, "PipelineState", DOC(dai, PipelineState)); /////////////////////////////////////////////////////////////////////// @@ -33,16 +36,43 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// - nodeStateTiming.def(py::init<>()) - .def("__repr__", &NodeState::Timing::str) - .def_readwrite("averageMicros", &NodeState::Timing::averageMicros, DOC(dai, NodeState, Timing, averageMicros)) - .def_readwrite("stdDevMicros", &NodeState::Timing::stdDevMicros, DOC(dai, NodeState, Timing, stdDevMicros)); + durationEvent.def(py::init<>()) + .def("__repr__", &NodeState::DurationEvent::str) + .def_readwrite("startEvent", &NodeState::DurationEvent::startEvent, DOC(dai, NodeState, DurationEvent, startEvent)) + .def_readwrite("durationUs", &NodeState::DurationEvent::durationUs, DOC(dai, NodeState, TimingStats, durationUs)); + + nodeStateTimingStats.def(py::init<>()) + .def("__repr__", &NodeState::TimingStats::str) + .def_readwrite("minMicros", &NodeState::TimingStats::minMicros, DOC(dai, NodeState, TimingStats, minMicros)) + .def_readwrite("maxMicros", &NodeState::TimingStats::maxMicros, DOC(dai, NodeState, TimingStats, maxMicros)) + .def_readwrite("averageMicrosRecent", &NodeState::TimingStats::averageMicrosRecent, DOC(dai, NodeState, TimingStats, averageMicrosRecent)) + .def_readwrite("stdDevMicrosRecent", &NodeState::TimingStats::stdDevMicrosRecent, DOC(dai, NodeState, TimingStats, stdDevMicrosRecent)) + .def_readwrite("minMicrosRecent", &NodeState::TimingStats::minMicrosRecent, DOC(dai, NodeState, TimingStats, minMicrosRecent)) + .def_readwrite("maxMicrosRecent", &NodeState::TimingStats::maxMicrosRecent, DOC(dai, NodeState, TimingStats, maxMicrosRecent)) + .def_readwrite("medianMicrosRecent", &NodeState::TimingStats::medianMicrosRecent, DOC(dai, NodeState, TimingStats, medianMicrosRecent)); + + nodeStateQueueStats.def(py::init<>()) + .def("__repr__", &NodeState::QueueStats::str) + .def_readwrite("maxQueued", &NodeState::QueueStats::maxQueued, DOC(dai, NodeState, QueueStats, maxQueued)) + .def_readwrite("minQueuedRecent", &NodeState::QueueStats::minQueuedRecent, DOC(dai, NodeState, QueueStats, minQueuedRecent)) + .def_readwrite("maxQueuedRecent", &NodeState::QueueStats::maxQueuedRecent, DOC(dai, NodeState, QueueStats, maxQueuedRecent)) + .def_readwrite("medianQueuedRecent", &NodeState::QueueStats::medianQueuedRecent, DOC(dai, NodeState, QueueStats, medianQueuedRecent)); + + nodeStateQueueState.def(py::init<>()) + .def("__repr__", &NodeState::QueueState::str) + .def_readwrite("waiting", &NodeState::QueueState::waiting, DOC(dai, NodeState, QueueState, waiting)) + .def_readwrite("numQueued", &NodeState::QueueState::numQueued, DOC(dai, NodeState, QueueState, numQueued)) + .def_readwrite("timingStats", &NodeState::QueueState::timingStats, DOC(dai, NodeState, QueueState, timingStats)) + .def_readwrite("queueStats", &NodeState::QueueState::queueStats, DOC(dai, NodeState, QueueState, queueStats)); nodeState.def(py::init<>()) .def("__repr__", &NodeState::str) .def_readwrite("events", &NodeState::events, DOC(dai, NodeState, events)) .def_readwrite("timingsByType", &NodeState::timingsByType, DOC(dai, NodeState, timingsByType)) - .def_readwrite("timingsByInstance", &NodeState::timingsByInstance, DOC(dai, NodeState, timingsByInstance)); + .def_readwrite("inputStates", &NodeState::inputStates, DOC(dai, NodeState, inputStates)) + .def_readwrite("outputStates", &NodeState::outputStates, DOC(dai, NodeState, outputStates)) + .def_readwrite("mainLoopStats", &NodeState::mainLoopStats, DOC(dai, NodeState, mainLoopStats)) + .def_readwrite("otherStats", &NodeState::otherStats, DOC(dai, NodeState, otherStats)); // Message pipelineState.def(py::init<>()) diff --git a/include/depthai/pipeline/Node.hpp b/include/depthai/pipeline/Node.hpp index c61c75498..4d858d1fc 100644 --- a/include/depthai/pipeline/Node.hpp +++ b/include/depthai/pipeline/Node.hpp @@ -146,7 +146,7 @@ class Node : public std::enable_shared_from_this { setName(par.createUniqueOutputName()); } if(pipelineEventDispatcher && getName() != "pipelineEventOutput") { - pipelineEventDispatcher->addEvent(getName(), PipelineEvent::EventType::OUTPUT); + pipelineEventDispatcher->addEvent(getName(), PipelineEvent::Type::OUTPUT); } } diff --git a/include/depthai/pipeline/datatype/PipelineEvent.hpp b/include/depthai/pipeline/datatype/PipelineEvent.hpp index 9b2b67672..0eae2cf6e 100644 --- a/include/depthai/pipeline/datatype/PipelineEvent.hpp +++ b/include/depthai/pipeline/datatype/PipelineEvent.hpp @@ -2,7 +2,6 @@ #include -#include "depthai/common/optional.hpp" #include "depthai/pipeline/datatype/Buffer.hpp" namespace dai { @@ -12,22 +11,25 @@ namespace dai { */ class PipelineEvent : public Buffer { public: - enum class EventType : std::int32_t { + enum class Type : std::int32_t { CUSTOM = 0, LOOP = 1, INPUT = 2, OUTPUT = 3, - FUNC_CALL = 4 + }; + enum class Interval : std::int32_t { + NONE = 0, + START = 1, + END = 2 }; PipelineEvent() = default; virtual ~PipelineEvent() = default; int64_t nodeId = -1; - std::optional metadata; - uint64_t timestamp {0}; - uint64_t duration {0}; // Duration in microseconds - EventType type = EventType::CUSTOM; + Buffer metadata; + Interval interval = Interval::NONE; + Type type = Type::CUSTOM; std::string source; void serialize(std::vector& metadata, DatatypeEnum& datatype) const override { @@ -35,7 +37,7 @@ class PipelineEvent : public Buffer { datatype = DatatypeEnum::PipelineEvent; }; - DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeId, metadata, timestamp, duration, type, source); + DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeId, metadata, interval, type, source); }; } // namespace dai diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index 159dfdbc3..ae3fc5d68 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -10,16 +10,43 @@ namespace dai { class NodeState { public: - struct Timing { - uint64_t averageMicros; - uint64_t stdDevMicros; - DEPTHAI_SERIALIZE(Timing, averageMicros, stdDevMicros); + struct DurationEvent { + PipelineEvent startEvent; + uint64_t durationUs; + DEPTHAI_SERIALIZE(DurationEvent, startEvent, durationUs); }; - std::vector events; - std::unordered_map timingsByType; - std::unordered_map timingsByInstance; + struct TimingStats { + uint64_t minMicros = -1; + uint64_t maxMicros; + uint64_t averageMicrosRecent; + uint64_t stdDevMicrosRecent; + uint64_t minMicrosRecent = -1; + uint64_t maxMicrosRecent; + uint64_t medianMicrosRecent; + DEPTHAI_SERIALIZE(TimingStats, minMicros, maxMicros, averageMicrosRecent, stdDevMicrosRecent, minMicrosRecent, maxMicrosRecent, medianMicrosRecent); + }; + struct QueueStats { + uint32_t maxQueued; + uint32_t minQueuedRecent; + uint32_t maxQueuedRecent; + uint32_t medianQueuedRecent; + DEPTHAI_SERIALIZE(QueueStats, maxQueued, minQueuedRecent, maxQueuedRecent, medianQueuedRecent); + }; + struct QueueState { + bool waiting; + uint32_t numQueued; + TimingStats timingStats; + QueueStats queueStats; + DEPTHAI_SERIALIZE(QueueState, waiting, numQueued, timingStats); + }; + std::vector events; + std::unordered_map timingsByType; + std::unordered_map inputStates; + std::unordered_map outputStates; + TimingStats mainLoopStats; + std::unordered_map otherStats; - DEPTHAI_SERIALIZE(NodeState, events, timingsByType, timingsByInstance); + DEPTHAI_SERIALIZE(NodeState, events, timingsByType, inputStates, outputStates, mainLoopStats, otherStats); }; /** diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index 78a47ab8c..26ded4550 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -15,11 +15,8 @@ namespace utility { class PipelineEventDispatcher : public PipelineEventDispatcherInterface { struct EventStatus { - PipelineEvent::EventType type; - std::chrono::microseconds duration; - std::chrono::time_point timestamp; + PipelineEvent event; bool ongoing; - std::optional metadata; }; int64_t nodeId = -1; @@ -36,7 +33,7 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void setNodeId(int64_t id) override; - void addEvent(const std::string& source, PipelineEvent::EventType type) override; + void addEvent(const std::string& source, PipelineEvent::Type type) override; void startEvent(const std::string& source, std::optional metadata = std::nullopt) override; // Start event with a start and an end void endEvent(const std::string& source) override; // Stop event with a start and an end diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index 34bf526f0..332281ecb 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "depthai/pipeline/datatype/PipelineEvent.hpp" namespace dai { @@ -9,7 +11,7 @@ class PipelineEventDispatcherInterface { public: virtual ~PipelineEventDispatcherInterface() = default; virtual void setNodeId(int64_t id) = 0; - virtual void addEvent(const std::string& source, PipelineEvent::EventType type) = 0; + virtual void addEvent(const std::string& source, PipelineEvent::Type type) = 0; virtual void startEvent(const std::string& source, std::optional metadata = std::nullopt) = 0; // Start event with a start and an end virtual void endEvent(const std::string& source) = 0; // Stop event with a start and an end virtual void pingEvent(const std::string& source) = 0; // Event where stop and start are the same (eg. loop) diff --git a/src/pipeline/MessageQueue.cpp b/src/pipeline/MessageQueue.cpp index 9d2aa464e..faee735db 100644 --- a/src/pipeline/MessageQueue.cpp +++ b/src/pipeline/MessageQueue.cpp @@ -24,7 +24,7 @@ MessageQueue::MessageQueue(std::string name, std::shared_ptr pipelineEventDispatcher) : queue(maxSize, blocking), name(std::move(name)), pipelineEventDispatcher(pipelineEventDispatcher) { if(pipelineEventDispatcher) { - pipelineEventDispatcher->addEvent(this->name, PipelineEvent::EventType::INPUT); + pipelineEventDispatcher->addEvent(this->name, PipelineEvent::Type::INPUT); } } diff --git a/src/pipeline/ThreadedNode.cpp b/src/pipeline/ThreadedNode.cpp index 246f7ecc8..228cb1a24 100644 --- a/src/pipeline/ThreadedNode.cpp +++ b/src/pipeline/ThreadedNode.cpp @@ -23,7 +23,7 @@ ThreadedNode::ThreadedNode() { void ThreadedNode::initPipelineEventDispatcher(int64_t nodeId) { pipelineEventDispatcher->setNodeId(nodeId); - pipelineEventDispatcher->addEvent("_mainLoop", PipelineEvent::EventType::LOOP); + pipelineEventDispatcher->addEvent("_mainLoop", PipelineEvent::Type::LOOP); } void ThreadedNode::start() { diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 95f5c104f..768f78239 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -3,72 +3,219 @@ #include "depthai/pipeline/datatype/PipelineEvent.hpp" #include "depthai/pipeline/datatype/PipelineState.hpp" #include "depthai/utility/CircularBuffer.hpp" +#include "pipeline/ThreadedNodeImpl.hpp" namespace dai { namespace node { namespace internal { class NodeEventAggregation { + private: + std::shared_ptr logger; + int windowSize; public: - NodeEventAggregation(int windowSize) : windowSize(windowSize), eventsBuffer(windowSize) {} + NodeEventAggregation(int windowSize, std::shared_ptr logger) : logger(logger), windowSize(windowSize), eventsBuffer(windowSize) {} NodeState state; - utility::CircularBuffer eventsBuffer; - std::unordered_map>> timingsBufferByType; - std::unordered_map>> timingsBufferByInstance; + utility::CircularBuffer eventsBuffer; + std::unordered_map>> timingsBufferByType; + std::unordered_map>> inputTimingsBuffers; + std::unordered_map>> outputTimingsBuffers; + std::unique_ptr> mainLoopTimingsBuffer; + std::unordered_map>> otherTimingsBuffers; + + std::unordered_map> ongoingInputEvents; + std::unordered_map> ongoingOutputEvents; + std::optional ongoingMainLoopEvent; + std::unordered_map> ongoingOtherEvents; uint32_t eventCount = 0; - void add(PipelineEvent& event) { - // TODO optimize avg - ++eventCount; - eventsBuffer.add(event); - state.events = eventsBuffer.getBuffer(); - if(timingsBufferByType.find(event.type) == timingsBufferByType.end()) { - timingsBufferByType[event.type] = std::make_unique>(windowSize); - } - if(timingsBufferByInstance.find(event.source) == timingsBufferByInstance.end()) { - timingsBufferByInstance[event.source] = std::make_unique>(windowSize); - } - timingsBufferByType[event.type]->add(event.duration); - timingsBufferByInstance[event.source]->add(event.duration); - // Calculate average duration and standard deviation from buffers - state.timingsByType[event.type].averageMicros = 0; - state.timingsByType[event.type].stdDevMicros = 0; - state.timingsByInstance[event.source].averageMicros = 0; - state.timingsByInstance[event.source].stdDevMicros = 0; - auto bufferByType = timingsBufferByType[event.type]->getBuffer(); - auto bufferByInstance = timingsBufferByInstance[event.source]->getBuffer(); - if(!bufferByType.empty()) { - uint64_t sum = 0; - double variance = 0; - for(auto v : bufferByType) { - sum += v; + private: + inline bool updateIntervalBuffers(PipelineEvent& event) { + using namespace std::chrono; + auto& ongoingEvent = [&]() -> std::optional& { + switch(event.type) { + case PipelineEvent::Type::LOOP: + throw std::runtime_error("LOOP event should not be an interval"); + case PipelineEvent::Type::INPUT: + return ongoingInputEvents[event.source]; + case PipelineEvent::Type::OUTPUT: + return ongoingOutputEvents[event.source]; + case PipelineEvent::Type::CUSTOM: + return ongoingOtherEvents[event.source]; + } + return ongoingMainLoopEvent; // To silence compiler warning + }(); + auto& timingsBuffer = [&]() -> std::unique_ptr>& { + switch(event.type) { + case PipelineEvent::Type::LOOP: + throw std::runtime_error("LOOP event should not be an interval"); + case PipelineEvent::Type::INPUT: + if(inputTimingsBuffers.find(event.source) == inputTimingsBuffers.end()) { + inputTimingsBuffers[event.source] = std::make_unique>(windowSize); + } + return inputTimingsBuffers[event.source]; + case PipelineEvent::Type::OUTPUT: + if(outputTimingsBuffers.find(event.source) == outputTimingsBuffers.end()) { + outputTimingsBuffers[event.source] = std::make_unique>(windowSize); + } + return outputTimingsBuffers[event.source]; + case PipelineEvent::Type::CUSTOM: + if(otherTimingsBuffers.find(event.source) == otherTimingsBuffers.end()) { + otherTimingsBuffers[event.source] = std::make_unique>(windowSize); + } + return otherTimingsBuffers[event.source]; + } + return mainLoopTimingsBuffer; // To silence compiler warning + }(); + if(ongoingEvent.has_value() && ongoingEvent->sequenceNum == event.sequenceNum && event.interval == PipelineEvent::Interval::END) { + // End event + NodeState::DurationEvent durationEvent; + durationEvent.startEvent = *ongoingEvent; + durationEvent.durationUs = duration_cast(event.getTimestamp() - ongoingEvent->getTimestamp()).count(); + eventsBuffer.add(durationEvent); + state.events = eventsBuffer.getBuffer(); + + if(timingsBufferByType.find(event.type) == timingsBufferByType.end()) { + timingsBufferByType[event.type] = std::make_unique>(windowSize); } - state.timingsByType[event.type].averageMicros = sum / bufferByType.size(); - // Calculate standard deviation - for(auto v : bufferByType) { - auto diff = v - state.timingsByType[event.type].averageMicros; - variance += diff * diff; + timingsBufferByType[event.type]->add(durationEvent.durationUs); + timingsBuffer->add(durationEvent.durationUs); + + ongoingEvent = std::nullopt; + + return true; + } else { + if(ongoingEvent.has_value()) { + logger->warn("Ongoing event not finished before new one started. Event source: {}, node {}", ongoingEvent->source, event.nodeId); + } + if(event.interval == PipelineEvent::Interval::START) { + // Start event + ongoingEvent = event; } - variance /= bufferByType.size(); - state.timingsByType[event.type].stdDevMicros = (uint64_t)(std::sqrt(variance)); + return false; } - if(!bufferByInstance.empty()) { - uint64_t sum = 0; - double variance = 0; - for(auto v : bufferByInstance) { - sum += v; + } + + inline bool updatePingBuffers(PipelineEvent& event) { + using namespace std::chrono; + auto& ongoingEvent = [&]() -> std::optional& { + switch(event.type) { + case PipelineEvent::Type::LOOP: + return ongoingMainLoopEvent; + case PipelineEvent::Type::CUSTOM: + return ongoingOtherEvents[event.source]; + case PipelineEvent::Type::INPUT: + case PipelineEvent::Type::OUTPUT: + throw std::runtime_error("INPUT and OUTPUT events should not be pings"); + } + return ongoingMainLoopEvent; // To silence compiler warning + }(); + auto& timingsBuffer = [&]() -> std::unique_ptr>& { + switch(event.type) { + case PipelineEvent::Type::LOOP: + return mainLoopTimingsBuffer; + case PipelineEvent::Type::CUSTOM: + if(otherTimingsBuffers.find(event.source) == otherTimingsBuffers.end()) { + otherTimingsBuffers[event.source] = std::make_unique>(windowSize); + } + return otherTimingsBuffers[event.source]; + case PipelineEvent::Type::INPUT: + case PipelineEvent::Type::OUTPUT: + throw std::runtime_error("INPUT and OUTPUT events should not be pings"); + } + return mainLoopTimingsBuffer; // To silence compiler warning + }(); + if(ongoingEvent.has_value() && ongoingEvent->sequenceNum == event.sequenceNum - 1) { + // End event + NodeState::DurationEvent durationEvent; + durationEvent.startEvent = *ongoingEvent; + durationEvent.durationUs = duration_cast(event.getTimestamp() - ongoingEvent->getTimestamp()).count(); + eventsBuffer.add(durationEvent); + state.events = eventsBuffer.getBuffer(); + + if(timingsBufferByType.find(event.type) == timingsBufferByType.end()) { + timingsBufferByType[event.type] = std::make_unique>(windowSize); + } + if(timingsBuffer == nullptr) { + timingsBuffer = std::make_unique>(windowSize); } - state.timingsByInstance[event.source].averageMicros = sum / bufferByInstance.size(); - // Calculate standard deviation - for(auto v : bufferByInstance) { - auto diff = v - state.timingsByInstance[event.source].averageMicros; - variance += diff * diff; + timingsBufferByType[event.type]->add(durationEvent.durationUs); + timingsBuffer->add(durationEvent.durationUs); + + // Start event + ongoingEvent = event; + + return true; + } else if(ongoingEvent.has_value()) { + logger->warn("Ongoing main loop event not finished before new one started. Event source: {}, node {}", ongoingEvent->source, event.nodeId); + } + // Start event + ongoingEvent = event; + + return false; + } + + inline void updateTimingStats(NodeState::TimingStats& stats, const utility::CircularBuffer& buffer) { + stats.minMicros = std::min(stats.minMicros, buffer.last()); + stats.maxMicros = std::max(stats.maxMicros, buffer.last()); + stats.averageMicrosRecent = 0; + stats.stdDevMicrosRecent = 0; + + auto bufferByType = buffer.getBuffer(); + uint64_t sum = 0; + double variance = 0; + for(auto v : bufferByType) { + sum += v; + } + stats.averageMicrosRecent = sum / bufferByType.size(); + // Calculate standard deviation + for(auto v : bufferByType) { + auto diff = v - stats.averageMicrosRecent; + variance += diff * diff; + } + variance /= bufferByType.size(); + stats.stdDevMicrosRecent = (uint64_t)(std::sqrt(variance)); + + std::sort(bufferByType.begin(), bufferByType.end()); + stats.minMicrosRecent = bufferByType.front(); + stats.maxMicrosRecent = bufferByType.back(); + stats.medianMicrosRecent = bufferByType[bufferByType.size() / 2]; + if(bufferByType.size() % 2 == 0) { + stats.medianMicrosRecent = (stats.medianMicrosRecent + bufferByType[bufferByType.size() / 2 - 1]) / 2; + } + } + + public: + void add(PipelineEvent& event) { + using namespace std::chrono; + ++eventCount; + bool recalculateStats = false; + if(event.interval == PipelineEvent::Interval::NONE) { + recalculateStats = updatePingBuffers(event); + } else { + recalculateStats = updateIntervalBuffers(event); + } + if(recalculateStats) { + // By type + updateTimingStats(state.timingsByType[event.type], *timingsBufferByType[event.type]); + // By instance + switch(event.type) { + case PipelineEvent::Type::CUSTOM: + updateTimingStats(state.otherStats[event.source], *otherTimingsBuffers[event.source]); + break; + case PipelineEvent::Type::LOOP: + updateTimingStats(state.mainLoopStats, *mainLoopTimingsBuffer); + break; + case PipelineEvent::Type::INPUT: + updateTimingStats(state.inputStates[event.source].timingStats, *inputTimingsBuffers[event.source]); + break; + case PipelineEvent::Type::OUTPUT: + updateTimingStats(state.outputStates[event.source].timingStats, *outputTimingsBuffers[event.source]); + break; } - variance /= bufferByInstance.size(); - state.timingsByInstance[event.source].stdDevMicros = (uint64_t)(std::sqrt(variance)); } } }; @@ -85,6 +232,7 @@ bool PipelineEventAggregation::runOnHost() const { } void PipelineEventAggregation::run() { + auto& logger = pimpl->logger; std::unordered_map nodeStates; uint32_t sequenceNum = 0; while(isRunning()) { @@ -95,7 +243,7 @@ void PipelineEventAggregation::run() { for(auto& [k, event] : events) { if(event != nullptr) { if(nodeStates.find(event->nodeId) == nodeStates.end()) { - nodeStates.insert_or_assign(event->nodeId, NodeEventAggregation(properties.aggregationWindowSize)); + nodeStates.insert_or_assign(event->nodeId, NodeEventAggregation(properties.aggregationWindowSize, logger)); } nodeStates.at(event->nodeId).add(*event); } diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index 0f379e0c2..c1d08479e 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -1,4 +1,5 @@ #include "depthai/utility/PipelineEventDispatcher.hpp" + #include #include @@ -13,12 +14,15 @@ void PipelineEventDispatcher::checkNodeId() { void PipelineEventDispatcher::setNodeId(int64_t id) { nodeId = id; } -void PipelineEventDispatcher::addEvent(const std::string& source, PipelineEvent::EventType type) { +void PipelineEventDispatcher::addEvent(const std::string& source, PipelineEvent::Type type) { if(!source.empty()) { if(events.find(source) != events.end()) { throw std::runtime_error("Event with name '" + source + "' already exists"); } - events[source] = {type, {}, {}, false, std::nullopt}; + PipelineEvent event; + event.type = type; + event.source = source; + events[source] = {event, false}; } } void PipelineEventDispatcher::startEvent(const std::string& source, std::optional metadata) { @@ -30,9 +34,18 @@ void PipelineEventDispatcher::startEvent(const std::string& source, std::optiona if(event.ongoing) { throw std::runtime_error("Event with name " + source + " is already ongoing"); } - event.timestamp = std::chrono::steady_clock::now(); + event.event.setTimestamp(std::chrono::steady_clock::now()); + event.event.tsDevice = event.event.ts; + event.event.sequenceNum = sequenceNum++; + event.event.nodeId = nodeId; + // TODO: event.event.metadata.emplace(metadata); + event.event.interval = PipelineEvent::Interval::START; + // type and source are already set event.ongoing = true; - event.metadata = metadata; + + if(out) { + out->send(std::make_shared(event.event)); + } } void PipelineEventDispatcher::endEvent(const std::string& source) { checkNodeId(); @@ -45,24 +58,20 @@ void PipelineEventDispatcher::endEvent(const std::string& source) { if(!event.ongoing) { throw std::runtime_error("Event with name " + source + " has not been started"); } - event.duration = std::chrono::duration_cast(now - event.timestamp); - event.ongoing = false; - PipelineEvent pipelineEvent; - pipelineEvent.nodeId = nodeId; - pipelineEvent.timestamp = std::chrono::duration_cast(event.timestamp.time_since_epoch()).count(); - pipelineEvent.duration = event.duration.count(); - pipelineEvent.type = event.type; - pipelineEvent.source = source; - pipelineEvent.sequenceNum = sequenceNum++; - pipelineEvent.setTimestampDevice(std::chrono::steady_clock::now()); - pipelineEvent.ts = pipelineEvent.tsDevice; + event.event.setTimestamp(now); + event.event.tsDevice = event.event.ts; + event.event.nodeId = nodeId; + // TODO: event.event.metadata.emplace(metadata); + event.event.interval = PipelineEvent::Interval::END; + // type and source are already set + event.ongoing = false; if(out) { - out->send(std::make_shared(pipelineEvent)); + out->send(std::make_shared(event.event)); } - event.metadata = std::nullopt; + // event.event.metadata = 0u; TODO } void PipelineEventDispatcher::pingEvent(const std::string& source) { checkNodeId(); @@ -73,22 +82,18 @@ void PipelineEventDispatcher::pingEvent(const std::string& source) { } auto& event = events[source]; if(event.ongoing) { - event.duration = std::chrono::duration_cast(now - event.timestamp); - event.timestamp = now; - - PipelineEvent pipelineEvent; - pipelineEvent.nodeId = nodeId; - pipelineEvent.duration = event.duration.count(); - pipelineEvent.type = event.type; - pipelineEvent.source = source; - pipelineEvent.metadata = std::nullopt; + throw std::runtime_error("Event with name " + source + " is already ongoing"); + } + event.event.setTimestamp(now); + event.event.tsDevice = event.event.ts; + event.event.sequenceNum = sequenceNum++; + event.event.nodeId = nodeId; + // TODO: event.event.metadata.emplace(metadata); + event.event.interval = PipelineEvent::Interval::NONE; + // type and source are already set - if(out) { - out->send(std::make_shared(pipelineEvent)); - } - } else { - event.timestamp = now; - event.ongoing = true; + if(out) { + out->send(std::make_shared(event.event)); } } From a5e93d83bdf41d45413e8e5c2ccdc5fec5aa3180 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 1 Oct 2025 17:07:13 +0200 Subject: [PATCH 012/124] Event instances now have independent sequence numbers --- .../node/internal/PipelineEventAggregation.hpp | 2 +- .../node/internal/PipelineEventAggregation.cpp | 12 ++++++++++-- src/utility/PipelineEventDispatcher.cpp | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp index 74a6a31be..d4a454c0a 100644 --- a/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp +++ b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp @@ -23,7 +23,7 @@ class PipelineEventAggregation : public DeviceNodeCRTPwarn("Ongoing event not finished before new one started. Event source: {}, node {}", ongoingEvent->source, event.nodeId); + logger->warn("Ongoing event (seq {}) not finished before new one (seq {}) started. Event source: {}, node {}", + ongoingEvent->sequenceNum, + event.sequenceNum, + ongoingEvent->source, + event.nodeId); } if(event.interval == PipelineEvent::Interval::START) { // Start event @@ -150,7 +154,11 @@ class NodeEventAggregation { return true; } else if(ongoingEvent.has_value()) { - logger->warn("Ongoing main loop event not finished before new one started. Event source: {}, node {}", ongoingEvent->source, event.nodeId); + logger->warn("Ongoing main loop event (seq {}) not finished before new one (seq {}) started. Event source: {}, node {}", + ongoingEvent->sequenceNum, + event.sequenceNum, + ongoingEvent->source, + event.nodeId); } // Start event ongoingEvent = event; diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index c1d08479e..e06fec993 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -36,7 +36,7 @@ void PipelineEventDispatcher::startEvent(const std::string& source, std::optiona } event.event.setTimestamp(std::chrono::steady_clock::now()); event.event.tsDevice = event.event.ts; - event.event.sequenceNum = sequenceNum++; + ++event.event.sequenceNum; event.event.nodeId = nodeId; // TODO: event.event.metadata.emplace(metadata); event.event.interval = PipelineEvent::Interval::START; @@ -86,7 +86,7 @@ void PipelineEventDispatcher::pingEvent(const std::string& source) { } event.event.setTimestamp(now); event.event.tsDevice = event.event.ts; - event.event.sequenceNum = sequenceNum++; + ++event.event.sequenceNum; event.event.nodeId = nodeId; // TODO: event.event.metadata.emplace(metadata); event.event.interval = PipelineEvent::Interval::NONE; From 38481f0ed9ca142a3f08468f3e51f07e4054e8aa Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 1 Oct 2025 17:46:13 +0200 Subject: [PATCH 013/124] Do not update stats for every event --- .../PipelineEventAggregationProperties.hpp | 4 ++-- .../internal/PipelineEventAggregation.cpp | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp index 028f0bf09..b5195a75f 100644 --- a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp +++ b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp @@ -8,8 +8,8 @@ namespace dai { * Specify properties for Sync. */ struct PipelineEventAggregationProperties : PropertiesSerializable { - uint32_t aggregationWindowSize = 20; - uint32_t eventBatchSize = 10; + uint32_t aggregationWindowSize = 100; + uint32_t eventBatchSize = 50; bool sendEvents = false; }; diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 035be9eaf..105f38ffe 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -13,10 +13,12 @@ class NodeEventAggregation { private: std::shared_ptr logger; - int windowSize; + uint32_t windowSize; + uint32_t eventBatchSize; public: - NodeEventAggregation(int windowSize, std::shared_ptr logger) : logger(logger), windowSize(windowSize), eventsBuffer(windowSize) {} + NodeEventAggregation(uint32_t windowSize, uint32_t eventBatchSize, std::shared_ptr logger) + : logger(logger), windowSize(windowSize), eventBatchSize(eventBatchSize), eventsBuffer(windowSize) {} NodeState state; utility::CircularBuffer eventsBuffer; std::unordered_map>> timingsBufferByType; @@ -30,7 +32,7 @@ class NodeEventAggregation { std::optional ongoingMainLoopEvent; std::unordered_map> ongoingOtherEvents; - uint32_t eventCount = 0; + uint32_t count = 0; private: inline bool updateIntervalBuffers(PipelineEvent& event) { @@ -199,14 +201,13 @@ class NodeEventAggregation { public: void add(PipelineEvent& event) { using namespace std::chrono; - ++eventCount; - bool recalculateStats = false; + bool addedEvent = false; if(event.interval == PipelineEvent::Interval::NONE) { - recalculateStats = updatePingBuffers(event); + addedEvent = updatePingBuffers(event); } else { - recalculateStats = updateIntervalBuffers(event); + addedEvent = updateIntervalBuffers(event); } - if(recalculateStats) { + if(addedEvent && ++count % eventBatchSize == 0) { // By type updateTimingStats(state.timingsByType[event.type], *timingsBufferByType[event.type]); // By instance @@ -251,7 +252,7 @@ void PipelineEventAggregation::run() { for(auto& [k, event] : events) { if(event != nullptr) { if(nodeStates.find(event->nodeId) == nodeStates.end()) { - nodeStates.insert_or_assign(event->nodeId, NodeEventAggregation(properties.aggregationWindowSize, logger)); + nodeStates.insert_or_assign(event->nodeId, NodeEventAggregation(properties.aggregationWindowSize, properties.eventBatchSize, logger)); } nodeStates.at(event->nodeId).add(*event); } @@ -259,11 +260,10 @@ void PipelineEventAggregation::run() { auto outState = std::make_shared(); bool shouldSend = false; for(auto& [nodeId, nodeState] : nodeStates) { - if(nodeState.eventCount >= properties.eventBatchSize) { + if(nodeState.count % properties.eventBatchSize == 0) { outState->nodeStates[nodeId] = nodeState.state; if(!properties.sendEvents) outState->nodeStates[nodeId].events.clear(); shouldSend = true; - nodeState.eventCount = 0; } } outState->sequenceNum = sequenceNum++; From 962d4afea79579a48eb30947ff1eec72afd10842 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 2 Oct 2025 13:02:47 +0200 Subject: [PATCH 014/124] Fix dropped pipeline events --- .../depthai/pipeline/node/internal/PipelineEventAggregation.hpp | 2 +- src/pipeline/node/internal/PipelineEventAggregation.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp index d4a454c0a..e16fedf23 100644 --- a/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp +++ b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp @@ -23,7 +23,7 @@ class PipelineEventAggregation : public DeviceNodeCRTP> events; for(auto& [k, v] : inputs) { - events[k.second] = v.get(); + events[k.second] = v.tryGet(); } for(auto& [k, event] : events) { if(event != nullptr) { From b79ded6c18d67673c4de9da1ffa4c312c5e327dd Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 2 Oct 2025 13:19:06 +0200 Subject: [PATCH 015/124] RVC4 FW: Fix dropped pipeline events --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index b49f8947d..d99dca2c5 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+d9ffe3ca006f85ae4d976468c1a342828739ae18") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+c104182cb9ef621c71adb2ec7a203e2f03851062") From de20f54f1c03aa6d89d4780f5935690f97a5ff9e Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 2 Oct 2025 16:39:14 +0200 Subject: [PATCH 016/124] Added queue size stats --- .../datatype/PipelineEventBindings.cpp | 1 + .../datatype/PipelineStateBindings.cpp | 2 +- .../PipelineEventDispatcherBindings.cpp | 4 +-- include/depthai/pipeline/MessageQueue.hpp | 4 +-- .../pipeline/datatype/PipelineEvent.hpp | 4 ++- .../pipeline/datatype/PipelineState.hpp | 4 +-- .../utility/PipelineEventDispatcher.hpp | 12 ++++++--- .../PipelineEventDispatcherInterface.hpp | 12 ++++++--- .../internal/PipelineEventAggregation.cpp | 25 ++++++++++++++++++- src/utility/PipelineEventDispatcher.cpp | 14 ++++++----- 10 files changed, 59 insertions(+), 23 deletions(-) diff --git a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp index 65f212520..f2d028be6 100644 --- a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp @@ -46,6 +46,7 @@ void bind_pipelineevent(pybind11::module& m, void* pCallstack) { .def("__repr__", &PipelineEvent::str) .def_readwrite("nodeId", &PipelineEvent::nodeId, DOC(dai, PipelineEvent, nodeId)) .def_readwrite("metadata", &PipelineEvent::metadata, DOC(dai, PipelineEvent, metadata)) + .def_readwrite("queueSize", &PipelineEvent::metadata, DOC(dai, PipelineEvent, metadata)) .def_readwrite("interval", &PipelineEvent::interval, DOC(dai, PipelineEvent, interval)) .def_readwrite("type", &PipelineEvent::type, DOC(dai, PipelineEvent, type)) .def_readwrite("source", &PipelineEvent::source, DOC(dai, PipelineEvent, source)) diff --git a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp index eff2c7bdd..67272b9e2 100644 --- a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp @@ -70,7 +70,7 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { .def_readwrite("events", &NodeState::events, DOC(dai, NodeState, events)) .def_readwrite("timingsByType", &NodeState::timingsByType, DOC(dai, NodeState, timingsByType)) .def_readwrite("inputStates", &NodeState::inputStates, DOC(dai, NodeState, inputStates)) - .def_readwrite("outputStates", &NodeState::outputStates, DOC(dai, NodeState, outputStates)) + .def_readwrite("outputStats", &NodeState::outputStats, DOC(dai, NodeState, outputStats)) .def_readwrite("mainLoopStats", &NodeState::mainLoopStats, DOC(dai, NodeState, mainLoopStats)) .def_readwrite("otherStats", &NodeState::otherStats, DOC(dai, NodeState, otherStats)); diff --git a/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp index 422edd72a..7ce1fd934 100644 --- a/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp +++ b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp @@ -22,7 +22,7 @@ void PipelineEventDispatcherBindings::bind(pybind11::module& m, void* pCallstack .def(py::init(), py::arg("output")) .def("setNodeId", &PipelineEventDispatcher::setNodeId, py::arg("id"), DOC(dai, utility, PipelineEventDispatcher, setNodeId)) .def("addEvent", &PipelineEventDispatcher::addEvent, py::arg("source"), py::arg("type"), DOC(dai, utility, PipelineEventDispatcher, addEvent)) - .def("startEvent", &PipelineEventDispatcher::startEvent, py::arg("source"), py::arg("metadata") = std::nullopt, DOC(dai, utility, PipelineEventDispatcher, startEvent)) - .def("endEvent", &PipelineEventDispatcher::endEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, endEvent)) + .def("startEvent", &PipelineEventDispatcher::startEvent, py::arg("source"), py::arg("queueSize") = std::nullopt, py::arg("metadata") = std::nullopt, DOC(dai, utility, PipelineEventDispatcher, startEvent)) + .def("endEvent", &PipelineEventDispatcher::endEvent, py::arg("source"), py::arg("queueSize") = std::nullopt, py::arg("metadata") = std::nullopt, DOC(dai, utility, PipelineEventDispatcher, endEvent)) .def("pingEvent", &PipelineEventDispatcher::pingEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, pingEvent)); } diff --git a/include/depthai/pipeline/MessageQueue.hpp b/include/depthai/pipeline/MessageQueue.hpp index 5c39bf41e..7969aef1d 100644 --- a/include/depthai/pipeline/MessageQueue.hpp +++ b/include/depthai/pipeline/MessageQueue.hpp @@ -230,12 +230,12 @@ class MessageQueue : public std::enable_shared_from_this { */ template std::shared_ptr get() { - if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(name); + if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(name, getSize()); std::shared_ptr val = nullptr; if(!queue.waitAndPop(val)) { throw QueueException(CLOSED_QUEUE_MESSAGE); } - if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name); + if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name, getSize()); return std::dynamic_pointer_cast(val); } diff --git a/include/depthai/pipeline/datatype/PipelineEvent.hpp b/include/depthai/pipeline/datatype/PipelineEvent.hpp index 0eae2cf6e..3158123c5 100644 --- a/include/depthai/pipeline/datatype/PipelineEvent.hpp +++ b/include/depthai/pipeline/datatype/PipelineEvent.hpp @@ -2,6 +2,7 @@ #include +#include "depthai/common/optional.hpp" #include "depthai/pipeline/datatype/Buffer.hpp" namespace dai { @@ -27,7 +28,8 @@ class PipelineEvent : public Buffer { virtual ~PipelineEvent() = default; int64_t nodeId = -1; - Buffer metadata; + std::optional metadata; + std::optional queueSize; Interval interval = Interval::NONE; Type type = Type::CUSTOM; std::string source; diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index ae3fc5d68..325d53704 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -41,12 +41,12 @@ class NodeState { }; std::vector events; std::unordered_map timingsByType; + std::unordered_map outputStats; std::unordered_map inputStates; - std::unordered_map outputStates; TimingStats mainLoopStats; std::unordered_map otherStats; - DEPTHAI_SERIALIZE(NodeState, events, timingsByType, inputStates, outputStates, mainLoopStats, otherStats); + DEPTHAI_SERIALIZE(NodeState, events, timingsByType, outputStats, inputStates, mainLoopStats, otherStats); }; /** diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index 26ded4550..318c2e809 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -6,9 +6,9 @@ #include #include +#include "PipelineEventDispatcherInterface.hpp" #include "depthai/pipeline/Node.hpp" #include "depthai/pipeline/datatype/PipelineEvent.hpp" -#include "PipelineEventDispatcherInterface.hpp" namespace dai { namespace utility { @@ -35,9 +35,13 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void addEvent(const std::string& source, PipelineEvent::Type type) override; - void startEvent(const std::string& source, std::optional metadata = std::nullopt) override; // Start event with a start and an end - void endEvent(const std::string& source) override; // Stop event with a start and an end - void pingEvent(const std::string& source) override; // Event where stop and start are the same (eg. loop) + void startEvent(const std::string& source, + std::optional queueSize = std::nullopt, + std::optional metadata = std::nullopt) override; // Start event with a start and an end + void endEvent(const std::string& source, + std::optional queueSize = std::nullopt, + std::optional metadata = std::nullopt) override; // Stop event with a start and an end + void pingEvent(const std::string& source) override; // Event where stop and start are the same (eg. loop) }; } // namespace utility diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index 332281ecb..e4f6d9858 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -8,13 +8,17 @@ namespace dai { namespace utility { class PipelineEventDispatcherInterface { -public: + public: virtual ~PipelineEventDispatcherInterface() = default; virtual void setNodeId(int64_t id) = 0; virtual void addEvent(const std::string& source, PipelineEvent::Type type) = 0; - virtual void startEvent(const std::string& source, std::optional metadata = std::nullopt) = 0; // Start event with a start and an end - virtual void endEvent(const std::string& source) = 0; // Stop event with a start and an end - virtual void pingEvent(const std::string& source) = 0; // Event where stop and start are the same (eg. loop) + virtual void startEvent(const std::string& source, + std::optional queueSize = std::nullopt, + std::optional metadata = std::nullopt) = 0; // Start event with a start and an end + virtual void endEvent(const std::string& source, + std::optional queueSize = std::nullopt, + std::optional metadata = std::nullopt) = 0; // Stop event with a start and an end + virtual void pingEvent(const std::string& source) = 0; // Event where stop and start are the same (eg. loop) }; } // namespace utility diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 4c381b60f..bd9def4d9 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -27,6 +27,8 @@ class NodeEventAggregation { std::unique_ptr> mainLoopTimingsBuffer; std::unordered_map>> otherTimingsBuffers; + std::unordered_map>> inputQueueSizesBuffers; + std::unordered_map> ongoingInputEvents; std::unordered_map> ongoingOutputEvents; std::optional ongoingMainLoopEvent; @@ -201,6 +203,14 @@ class NodeEventAggregation { public: void add(PipelineEvent& event) { using namespace std::chrono; + if(event.type == PipelineEvent::Type::INPUT && event.interval == PipelineEvent::Interval::END) { + if(event.queueSize.has_value()) { + if(inputQueueSizesBuffers.find(event.source) == inputQueueSizesBuffers.end()) { + inputQueueSizesBuffers[event.source] = std::make_unique>(windowSize); + } + inputQueueSizesBuffers[event.source]->add(*event.queueSize); + } + } bool addedEvent = false; if(event.interval == PipelineEvent::Interval::NONE) { addedEvent = updatePingBuffers(event); @@ -222,10 +232,23 @@ class NodeEventAggregation { updateTimingStats(state.inputStates[event.source].timingStats, *inputTimingsBuffers[event.source]); break; case PipelineEvent::Type::OUTPUT: - updateTimingStats(state.outputStates[event.source].timingStats, *outputTimingsBuffers[event.source]); + updateTimingStats(state.outputStats[event.source], *outputTimingsBuffers[event.source]); break; } } + if(event.type == PipelineEvent::Type::INPUT && event.interval == PipelineEvent::Interval::END && ++count % eventBatchSize == 0) { + auto& qStats = state.inputStates[event.source].queueStats; + auto& qBuffer = *inputQueueSizesBuffers[event.source]; + qStats.maxQueued = std::max(qStats.maxQueued, *event.queueSize); + auto qBufferData = qBuffer.getBuffer(); + std::sort(qBufferData.begin(), qBufferData.end()); + qStats.minQueuedRecent = qBufferData.front(); + qStats.maxQueuedRecent = qBufferData.back(); + qStats.medianQueuedRecent = qBufferData[qBufferData.size() / 2]; + if(qBufferData.size() % 2 == 0) { + qStats.medianQueuedRecent = (qStats.medianQueuedRecent + qBufferData[qBufferData.size() / 2 - 1]) / 2; + } + } } }; diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index e06fec993..1fdb32831 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -25,7 +25,7 @@ void PipelineEventDispatcher::addEvent(const std::string& source, PipelineEvent: events[source] = {event, false}; } } -void PipelineEventDispatcher::startEvent(const std::string& source, std::optional metadata) { +void PipelineEventDispatcher::startEvent(const std::string& source, std::optional queueSize, std::optional metadata) { checkNodeId(); if(events.find(source) == events.end()) { throw std::runtime_error("Event with name " + source + " does not exist"); @@ -38,7 +38,8 @@ void PipelineEventDispatcher::startEvent(const std::string& source, std::optiona event.event.tsDevice = event.event.ts; ++event.event.sequenceNum; event.event.nodeId = nodeId; - // TODO: event.event.metadata.emplace(metadata); + event.event.metadata = std::move(metadata); + event.event.queueSize = std::move(queueSize); event.event.interval = PipelineEvent::Interval::START; // type and source are already set event.ongoing = true; @@ -47,7 +48,7 @@ void PipelineEventDispatcher::startEvent(const std::string& source, std::optiona out->send(std::make_shared(event.event)); } } -void PipelineEventDispatcher::endEvent(const std::string& source) { +void PipelineEventDispatcher::endEvent(const std::string& source, std::optional queueSize, std::optional metadata) { checkNodeId(); auto now = std::chrono::steady_clock::now(); @@ -62,7 +63,8 @@ void PipelineEventDispatcher::endEvent(const std::string& source) { event.event.setTimestamp(now); event.event.tsDevice = event.event.ts; event.event.nodeId = nodeId; - // TODO: event.event.metadata.emplace(metadata); + event.event.metadata = std::move(metadata); + event.event.queueSize = std::move(queueSize); event.event.interval = PipelineEvent::Interval::END; // type and source are already set event.ongoing = false; @@ -71,7 +73,8 @@ void PipelineEventDispatcher::endEvent(const std::string& source) { out->send(std::make_shared(event.event)); } - // event.event.metadata = 0u; TODO + event.event.metadata = std::nullopt; + event.event.queueSize = std::nullopt; } void PipelineEventDispatcher::pingEvent(const std::string& source) { checkNodeId(); @@ -88,7 +91,6 @@ void PipelineEventDispatcher::pingEvent(const std::string& source) { event.event.tsDevice = event.event.ts; ++event.event.sequenceNum; event.event.nodeId = nodeId; - // TODO: event.event.metadata.emplace(metadata); event.event.interval = PipelineEvent::Interval::NONE; // type and source are already set From 485412465eb1231ce67712ad8705c8a3b1d2a90e Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 6 Oct 2025 11:32:18 +0200 Subject: [PATCH 017/124] WIP pipeline state api --- CMakeLists.txt | 1 + .../datatype/PipelineStateBindings.cpp | 3 +- .../python/src/pipeline/node/NodeBindings.cpp | 1 + .../node/PipelineEventAggregationBindings.cpp | 64 ++--- bindings/python/tests/messsage_queue_test.py | 4 +- examples/cpp/AprilTags/april_tags_replay.cpp | 4 +- examples/cpp/HostNodes/host_camera.cpp | 4 +- examples/cpp/HostNodes/threaded_host_node.cpp | 6 +- examples/cpp/RGBD/rgbd.cpp | 2 +- examples/cpp/RGBD/rgbd_pcl_processing.cpp | 2 +- examples/cpp/RVC2/VSLAM/rerun_node.hpp | 4 +- .../python/AprilTags/april_tags_replay.py | 2 +- examples/python/HostNodes/host_camera.py | 4 +- .../python/HostNodes/threaded_host_nodes.py | 10 +- examples/python/RGBD/rgbd.py | 2 +- examples/python/RGBD/rgbd_o3d.py | 2 +- examples/python/RGBD/rgbd_pcl_processing.py | 2 +- examples/python/RVC2/VSLAM/rerun_node.py | 4 +- .../python/Visualizer/visualizer_encoded.py | 2 +- include/depthai/pipeline/Pipeline.hpp | 218 +++++++++++++++++- include/depthai/pipeline/ThreadedNode.hpp | 4 +- .../pipeline/datatype/DatatypeEnum.hpp | 1 + .../PipelineEventAggregationConfig.hpp | 37 +++ .../pipeline/datatype/PipelineState.hpp | 9 +- .../internal/PipelineEventAggregation.hpp | 5 + .../node/internal/PipelineStateMerge.hpp | 12 +- include/depthai/utility/CircularBuffer.hpp | 10 + include/depthai/utility/ImageManipImpl.hpp | 2 +- src/basalt/BasaltVIO.cpp | 2 +- src/pipeline/InputQueue.cpp | 2 +- src/pipeline/Pipeline.cpp | 2 + src/pipeline/ThreadedNode.cpp | 8 +- src/pipeline/datatype/DatatypeEnum.cpp | 3 + .../PipelineEventAggregationConfig.cpp | 12 + src/pipeline/datatype/StreamMessageParser.cpp | 4 + src/pipeline/node/AprilTag.cpp | 2 +- src/pipeline/node/BenchmarkIn.cpp | 2 +- src/pipeline/node/DynamicCalibrationNode.cpp | 2 +- src/pipeline/node/ImageAlign.cpp | 4 +- src/pipeline/node/ImageFilters.cpp | 4 +- src/pipeline/node/ObjectTracker.cpp | 2 +- src/pipeline/node/Sync.cpp | 2 +- src/pipeline/node/host/Display.cpp | 2 +- src/pipeline/node/host/HostCamera.cpp | 4 +- src/pipeline/node/host/HostNode.cpp | 2 +- src/pipeline/node/host/RGBD.cpp | 2 +- src/pipeline/node/host/Record.cpp | 4 +- src/pipeline/node/host/Replay.cpp | 2 + .../internal/PipelineEventAggregation.cpp | 74 +++++- .../node/internal/PipelineStateMerge.cpp | 23 +- src/pipeline/node/internal/XLinkOutHost.cpp | 2 +- src/pipeline/node/test/MyProducer.cpp | 2 +- src/rtabmap/RTABMapSLAM.cpp | 2 +- src/rtabmap/RTABMapVIO.cpp | 2 +- src/utility/ProtoSerialize.cpp | 1 + .../input_output_naming_test.cpp | 2 +- 56 files changed, 498 insertions(+), 98 deletions(-) create mode 100644 include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp create mode 100644 src/pipeline/datatype/PipelineEventAggregationConfig.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c916c9be0..237f2c5f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -355,6 +355,7 @@ set(TARGET_CORE_SOURCES src/pipeline/datatype/PointCloudData.cpp src/pipeline/datatype/PipelineEvent.cpp src/pipeline/datatype/PipelineState.cpp + src/pipeline/datatype/PipelineEventAggregationConfig.cpp src/pipeline/datatype/RGBDData.cpp src/pipeline/datatype/MessageGroup.cpp src/pipeline/datatype/ThermalConfig.cpp diff --git a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp index 67272b9e2..d6bc03086 100644 --- a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp @@ -39,7 +39,8 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { durationEvent.def(py::init<>()) .def("__repr__", &NodeState::DurationEvent::str) .def_readwrite("startEvent", &NodeState::DurationEvent::startEvent, DOC(dai, NodeState, DurationEvent, startEvent)) - .def_readwrite("durationUs", &NodeState::DurationEvent::durationUs, DOC(dai, NodeState, TimingStats, durationUs)); + .def_readwrite("durationUs", &NodeState::DurationEvent::durationUs, DOC(dai, NodeState, TimingStats, durationUs)) + .def_readwrite("fps", &NodeState::DurationEvent::fps, DOC(dai, NodeState, TimingStats, fps)); nodeStateTimingStats.def(py::init<>()) .def("__repr__", &NodeState::TimingStats::str) diff --git a/bindings/python/src/pipeline/node/NodeBindings.cpp b/bindings/python/src/pipeline/node/NodeBindings.cpp index 4237ba403..715c689d0 100644 --- a/bindings/python/src/pipeline/node/NodeBindings.cpp +++ b/bindings/python/src/pipeline/node/NodeBindings.cpp @@ -447,6 +447,7 @@ void NodeBindings::bind(pybind11::module& m, void* pCallstack) { .def("error", [](dai::ThreadedNode& node, const std::string& msg) { node.pimpl->logger->error(msg); }) .def("critical", [](dai::ThreadedNode& node, const std::string& msg) { node.pimpl->logger->critical(msg); }) .def("isRunning", &ThreadedNode::isRunning, DOC(dai, ThreadedNode, isRunning)) + .def("mainLoop", &ThreadedNode::mainLoop, DOC(dai, ThreadedNode, mainLoop)) .def("setLogLevel", &ThreadedNode::setLogLevel, DOC(dai, ThreadedNode, setLogLevel)) .def("getLogLevel", &ThreadedNode::getLogLevel, DOC(dai, ThreadedNode, getLogLevel)); } diff --git a/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp index 64d1fd539..3fcf8a616 100644 --- a/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp +++ b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp @@ -3,36 +3,36 @@ #include "depthai/properties/internal/PipelineEventAggregationProperties.hpp" void bind_pipelineeventaggregation(pybind11::module& m, void* pCallstack) { - using namespace dai; - using namespace dai::node::internal; - - // Node and Properties declare upfront - py::class_ pipelineEventAggregationProperties( - m, "PipelineEventAggregationProperties", DOC(dai, PipelineEventAggregationProperties)); - auto pipelineEventAggregation = ADD_NODE(PipelineEventAggregation); - - /////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////// - // Call the rest of the type defines, then perform the actual bindings - Callstack* callstack = (Callstack*)pCallstack; - auto cb = callstack->top(); - callstack->pop(); - cb(m, pCallstack); - // Actual bindings - /////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////// - - // Properties - pipelineEventAggregationProperties.def_readwrite("aggregationWindowSize", &PipelineEventAggregationProperties::aggregationWindowSize) - .def_readwrite("eventBatchSize", &PipelineEventAggregationProperties::eventBatchSize) - .def_readwrite("sendEvents", &PipelineEventAggregationProperties::sendEvents); - - // Node - pipelineEventAggregation.def_readonly("out", &PipelineEventAggregation::out, DOC(dai, node, PipelineEventAggregation, out)) - .def_readonly("inputs", &PipelineEventAggregation::inputs, DOC(dai, node, PipelineEventAggregation, inputs)) - .def("setRunOnHost", &PipelineEventAggregation::setRunOnHost, py::arg("runOnHost"), DOC(dai, node, PipelineEventAggregation, setRunOnHost)) - .def("runOnHost", &PipelineEventAggregation::runOnHost, DOC(dai, node, PipelineEventAggregation, runOnHost)); - daiNodeModule.attr("PipelineEventAggregation").attr("Properties") = pipelineEventAggregationProperties; + // using namespace dai; + // using namespace dai::node::internal; + // + // // Node and Properties declare upfront + // py::class_ pipelineEventAggregationProperties( + // m, "PipelineEventAggregationProperties", DOC(dai, PipelineEventAggregationProperties)); + // auto pipelineEventAggregation = ADD_NODE(PipelineEventAggregation); + // + // /////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////// + // // Call the rest of the type defines, then perform the actual bindings + // Callstack* callstack = (Callstack*)pCallstack; + // auto cb = callstack->top(); + // callstack->pop(); + // cb(m, pCallstack); + // // Actual bindings + // /////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////// + // + // // Properties + // pipelineEventAggregationProperties.def_readwrite("aggregationWindowSize", &PipelineEventAggregationProperties::aggregationWindowSize) + // .def_readwrite("eventBatchSize", &PipelineEventAggregationProperties::eventBatchSize) + // .def_readwrite("sendEvents", &PipelineEventAggregationProperties::sendEvents); + // + // // Node + // pipelineEventAggregation.def_readonly("out", &PipelineEventAggregation::out, DOC(dai, node, PipelineEventAggregation, out)) + // .def_readonly("inputs", &PipelineEventAggregation::inputs, DOC(dai, node, PipelineEventAggregation, inputs)) + // .def("setRunOnHost", &PipelineEventAggregation::setRunOnHost, py::arg("runOnHost"), DOC(dai, node, PipelineEventAggregation, setRunOnHost)) + // .def("runOnHost", &PipelineEventAggregation::runOnHost, DOC(dai, node, PipelineEventAggregation, runOnHost)); + // daiNodeModule.attr("PipelineEventAggregation").attr("Properties") = pipelineEventAggregationProperties; } diff --git a/bindings/python/tests/messsage_queue_test.py b/bindings/python/tests/messsage_queue_test.py index 120c87b2e..0334ca52f 100644 --- a/bindings/python/tests/messsage_queue_test.py +++ b/bindings/python/tests/messsage_queue_test.py @@ -387,7 +387,7 @@ def __init__(self, name: str): self.output = self.createOutput() def run(self): - while self.isRunning(): + while self.mainLoop(): buffer = self.input.get() self.output.send(buffer) @@ -399,7 +399,7 @@ def __init__(self, name: str): self.output = self.createOutput() def run(self): - while self.isRunning(): + while self.mainLoop(): buffer = dai.Buffer() self.output.send(buffer) time.sleep(0.001) diff --git a/examples/cpp/AprilTags/april_tags_replay.cpp b/examples/cpp/AprilTags/april_tags_replay.cpp index c03958fa4..71779178d 100644 --- a/examples/cpp/AprilTags/april_tags_replay.cpp +++ b/examples/cpp/AprilTags/april_tags_replay.cpp @@ -27,7 +27,7 @@ class ImageReplay : public dai::NodeCRTP { return; } - while(isRunning()) { + while(mainLoop()) { // Read the frame from the camera cv::Mat frame; if(!cap.read(frame)) { @@ -96,4 +96,4 @@ int main() { pipeline.wait(); return 0; -} \ No newline at end of file +} diff --git a/examples/cpp/HostNodes/threaded_host_node.cpp b/examples/cpp/HostNodes/threaded_host_node.cpp index 90b95560c..60c02c008 100644 --- a/examples/cpp/HostNodes/threaded_host_node.cpp +++ b/examples/cpp/HostNodes/threaded_host_node.cpp @@ -10,7 +10,7 @@ class TestPassthrough : public dai::node::CustomThreadedNode { Output output = dai::Node::Output{*this, {}}; void run() override { - while(isRunning()) { + while(mainLoop()) { auto buffer = input.get(); if(buffer) { std::cout << "The passthrough node received a buffer!" << std::endl; @@ -25,7 +25,7 @@ class TestSink : public dai::node::CustomThreadedNode { Input input = dai::Node::Input{*this, {}}; void run() override { - while(isRunning()) { + while(mainLoop()) { auto buffer = input.get(); if(buffer) { std::cout << "The sink node received a buffer!" << std::endl; @@ -39,7 +39,7 @@ class TestSource : public dai::node::CustomThreadedNode { Output output = dai::Node::Output{*this, {}}; void run() override { - while(isRunning()) { + while(mainLoop()) { auto buffer = std::make_shared(); std::cout << "The source node is sending a buffer!" << std::endl; output.send(buffer); diff --git a/examples/cpp/RGBD/rgbd.cpp b/examples/cpp/RGBD/rgbd.cpp index 59369862a..d4f73f273 100644 --- a/examples/cpp/RGBD/rgbd.cpp +++ b/examples/cpp/RGBD/rgbd.cpp @@ -15,7 +15,7 @@ class RerunNode : public dai::NodeCRTP { const auto rec = rerun::RecordingStream("rerun"); rec.spawn().exit_on_failure(); rec.log_static("world", rerun::ViewCoordinates::RDF); - while(isRunning()) { + while(mainLoop()) { auto pclIn = inputPCL.get(); auto rgbdIn = inputRGBD.get(); if(pclIn != nullptr) { diff --git a/examples/cpp/RGBD/rgbd_pcl_processing.cpp b/examples/cpp/RGBD/rgbd_pcl_processing.cpp index 54c75e6a0..41b6636de 100644 --- a/examples/cpp/RGBD/rgbd_pcl_processing.cpp +++ b/examples/cpp/RGBD/rgbd_pcl_processing.cpp @@ -16,7 +16,7 @@ class CustomPCLProcessingNode : public dai::NodeCRTP(); auto pclOut = std::make_shared(); if(pclIn != nullptr) { diff --git a/examples/cpp/RVC2/VSLAM/rerun_node.hpp b/examples/cpp/RVC2/VSLAM/rerun_node.hpp index dcea0fd91..0c7fd1a41 100644 --- a/examples/cpp/RVC2/VSLAM/rerun_node.hpp +++ b/examples/cpp/RVC2/VSLAM/rerun_node.hpp @@ -41,7 +41,7 @@ class RerunNode : public dai::NodeCRTP { rec.spawn().exit_on_failure(); rec.log_static("world", rerun::ViewCoordinates::FLU); rec.log("world/ground", rerun::Boxes3D::from_half_sizes({{3.f, 3.f, 0.00001f}})); - while(isRunning()) { + while(mainLoop()) { std::shared_ptr transData = inputTrans.get(); auto imgFrame = inputImg.get(); if(!intrinsicsSet) { @@ -107,4 +107,4 @@ class RerunNode : public dai::NodeCRTP { float fx = 400.0; float fy = 400.0; bool intrinsicsSet = false; -}; \ No newline at end of file +}; diff --git a/examples/python/AprilTags/april_tags_replay.py b/examples/python/AprilTags/april_tags_replay.py index 01cae83a9..fe6dec7f2 100644 --- a/examples/python/AprilTags/april_tags_replay.py +++ b/examples/python/AprilTags/april_tags_replay.py @@ -24,7 +24,7 @@ def __init__(self): imgFrame.setType(dai.ImgFrame.Type.GRAY8) self.imgFrame = imgFrame def run(self): - while self.isRunning(): + while self.mainLoop(): self.output.send(self.imgFrame) time.sleep(0.03) diff --git a/examples/python/HostNodes/host_camera.py b/examples/python/HostNodes/host_camera.py index fd458b84b..83dd78089 100644 --- a/examples/python/HostNodes/host_camera.py +++ b/examples/python/HostNodes/host_camera.py @@ -13,7 +13,7 @@ def run(self): if not cap.isOpened(): p.stop() raise RuntimeError("Error: Couldn't open host camera") - while self.isRunning(): + while self.mainLoop(): # Read the frame from the camera ret, frame = cap.read() if not ret: @@ -40,4 +40,4 @@ def run(self): key = cv2.waitKey(1) if key == ord('q'): p.stop() - break \ No newline at end of file + break diff --git a/examples/python/HostNodes/threaded_host_nodes.py b/examples/python/HostNodes/threaded_host_nodes.py index 7b3c50827..4118fd788 100644 --- a/examples/python/HostNodes/threaded_host_nodes.py +++ b/examples/python/HostNodes/threaded_host_nodes.py @@ -31,7 +31,7 @@ def onStop(self): print("Goodbye from", self.name) def run(self): - while self.isRunning(): + while self.mainLoop(): buffer = self.input.get() print("The passthrough node received a buffer!") self.output.send(buffer) @@ -47,7 +47,7 @@ def onStart(self): print("Hello, this is", self.name) def run(self): - while self.isRunning(): + while self.mainLoop(): buffer = self.input.get() del buffer print(f"{self.name} node received a buffer!") @@ -59,7 +59,7 @@ def __init__(self, name: str): self.output = self.createOutput() def run(self): - while self.isRunning(): + while self.mainLoop(): buffer = dai.Buffer() print(f"{self.name} node is sending a buffer!") self.output.send(buffer) @@ -77,7 +77,7 @@ def __init__(self, name: str): self.passthrough1.output.link(self.passthrough2.input) def run(self): - while self.isRunning(): + while self.mainLoop(): buffer = self.input.get() self.output.send(buffer) @@ -105,4 +105,4 @@ def run(self): p.start() while p.isRunning(): time.sleep(1) - print("Pipeline is running...") \ No newline at end of file + print("Pipeline is running...") diff --git a/examples/python/RGBD/rgbd.py b/examples/python/RGBD/rgbd.py index 4f2202097..5e15d796d 100644 --- a/examples/python/RGBD/rgbd.py +++ b/examples/python/RGBD/rgbd.py @@ -21,7 +21,7 @@ def run(self): rr.init("", spawn=True) rr.log("world", rr.ViewCoordinates.RDF) rr.log("world/ground", rr.Boxes3D(half_sizes=[3.0, 3.0, 0.00001])) - while self.isRunning(): + while self.mainLoop(): try: inPointCloud = self.inputPCL.get() except dai.MessageQueue.QueueException: diff --git a/examples/python/RGBD/rgbd_o3d.py b/examples/python/RGBD/rgbd_o3d.py index 0f7d2787f..5ba514015 100644 --- a/examples/python/RGBD/rgbd_o3d.py +++ b/examples/python/RGBD/rgbd_o3d.py @@ -33,7 +33,7 @@ def key_callback(vis, action, mods): ) vis.add_geometry(coordinateFrame) first = True - while self.isRunning(): + while self.mainLoop(): try: inPointCloud = self.inputPCL.tryGet() except dai.MessageQueue.QueueException: diff --git a/examples/python/RGBD/rgbd_pcl_processing.py b/examples/python/RGBD/rgbd_pcl_processing.py index 804d83e77..0b1930bac 100644 --- a/examples/python/RGBD/rgbd_pcl_processing.py +++ b/examples/python/RGBD/rgbd_pcl_processing.py @@ -11,7 +11,7 @@ def __init__(self): self.thresholdDistance = 3000.0 def run(self): - while self.isRunning(): + while self.mainLoop(): try: inPointCloud = self.inputPCL.get() except dai.MessageQueue.QueueException: diff --git a/examples/python/RVC2/VSLAM/rerun_node.py b/examples/python/RVC2/VSLAM/rerun_node.py index 4211b7ef1..caefda601 100644 --- a/examples/python/RVC2/VSLAM/rerun_node.py +++ b/examples/python/RVC2/VSLAM/rerun_node.py @@ -36,7 +36,7 @@ def run(self): rr.init("", spawn=True) rr.log("world", rr.ViewCoordinates.FLU) rr.log("world/ground", rr.Boxes3D(half_sizes=[3.0, 3.0, 0.00001])) - while self.isRunning(): + while self.mainLoop(): transData = self.inputTrans.get() imgFrame = self.inputImg.get() if not self.intrinsicsSet: @@ -63,4 +63,4 @@ def run(self): points, colors = pclGrndData.getPointsRGB() rr.log("world/ground_pcl", rr.Points3D(points, colors=colors, radii=[0.01])) if mapData is not None: - rr.log("map", rr.Image(mapData.getCvFrame())) \ No newline at end of file + rr.log("map", rr.Image(mapData.getCvFrame())) diff --git a/examples/python/Visualizer/visualizer_encoded.py b/examples/python/Visualizer/visualizer_encoded.py index 9c6ab195a..78d0eca6f 100644 --- a/examples/python/Visualizer/visualizer_encoded.py +++ b/examples/python/Visualizer/visualizer_encoded.py @@ -20,7 +20,7 @@ def __init__(self): def setLabelMap(self, labelMap): self.labelMap = labelMap def run(self): - while self.isRunning(): + while self.mainLoop(): nnData = self.inputDet.get() detections = nnData.detections imgAnnt = dai.ImgAnnotations() diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index df460980e..2f686ec36 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -15,14 +15,15 @@ #include "depthai/device/CalibrationHandler.hpp" #include "depthai/device/Device.hpp" #include "depthai/openvino/OpenVINO.hpp" +#include "depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp" #include "depthai/utility/AtomicBool.hpp" // shared #include "depthai/device/BoardConfig.hpp" #include "depthai/pipeline/PipelineSchema.hpp" +#include "depthai/pipeline/datatype/PipelineState.hpp" #include "depthai/properties/GlobalProperties.hpp" #include "depthai/utility/RecordReplay.hpp" -#include "depthai/pipeline/datatype/PipelineState.hpp" namespace dai { @@ -242,6 +243,221 @@ class PipelineImpl : public std::enable_shared_from_this { std::vector loadResourceCwd(fs::path uri, fs::path cwd, bool moveAsset = false); }; +/** + * pipeline.getState().nodes({nodeId1}).summary() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).detailed() -> std::unordered_map; + * pipeline.getState().nodes(nodeId1).detailed() -> NodeState; + * pipeline.getState().nodes({nodeId1}).outputs() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).outputs({outputName1}) -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).outputs(outputName) -> TimingStats; + * pipeline.getState().nodes({nodeId1}).events(); + * pipeline.getState().nodes({nodeId1}).inputs() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).inputs({inputName1}) -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).inputs(inputName) -> QueueState; + * pipeline.getState().nodes({nodeId1}).otherStats() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).otherStats({statName1}) -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).outputs(statName) -> TimingStats; + */ +template +class NodeStateApi { + static_assert(false); +}; +template <> +class NodeStateApi> { + std::vector nodeIds; + + public: + explicit NodeStateApi(std::vector nodeIds) : nodeIds(std::move(nodeIds)) {} + std::unordered_map> summary() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.summary = true; + cfg.nodes.push_back(nodeCfg); + } + + // TODO send and get + return {}; + } + PipelineState detailed() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.summary = true; // contains main loop timing + nodeCfg.inputs = {}; // send all + nodeCfg.outputs = {}; // send all + nodeCfg.others = {}; // send all + cfg.nodes.push_back(nodeCfg); + } + + // TODO send and get + return {}; + } + std::unordered_map> outputs() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.outputs = {}; // send all + cfg.nodes.push_back(nodeCfg); + } + + // TODO send and get + return {}; + } + std::unordered_map> inputs() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.inputs = {}; // send all + cfg.nodes.push_back(nodeCfg); + } + + // TODO send and get + return {}; + } + std::unordered_map> otherStats() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.others = {}; // send all + cfg.nodes.push_back(nodeCfg); + } + + // TODO send and get + return {}; + } +}; +template <> +class NodeStateApi { + Node::Id nodeId; + + public: + explicit NodeStateApi(Node::Id nodeId) : nodeId(nodeId) {} + std::unordered_map summary() { + return NodeStateApi>({nodeId}).summary()[nodeId]; + } + NodeState detailed() { + return NodeStateApi>({nodeId}).detailed().nodeStates[nodeId]; + } + std::unordered_map outputs() { + return NodeStateApi>({nodeId}).outputs()[nodeId]; + } + std::unordered_map inputs() { + return NodeStateApi>({nodeId}).inputs()[nodeId]; + } + std::unordered_map otherStats() { + return NodeStateApi>({nodeId}).otherStats()[nodeId]; + } + std::unordered_map outputs(const std::vector& outputNames) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.outputs = outputNames; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + NodeState::TimingStats outputs(const std::string& outputName) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.outputs = {outputName}; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + std::unordered_map> events() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.events = true; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + std::unordered_map inputs(const std::vector& inputNames) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.inputs = inputNames; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + NodeState::QueueState inputs(const std::string& inputName) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.inputs = {inputName}; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + std::unordered_map otherStats(const std::vector& statNames) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.others = statNames; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + NodeState::TimingStats otherStats(const std::string& statName) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.others = {statName}; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } +}; +class PipelineStateApi { + public: + NodeStateApi> nodes(const std::vector& nodeIds) { + return NodeStateApi>(nodeIds); + } + NodeStateApi nodes(Node::Id nodeId) { + return NodeStateApi(nodeId); + } +}; + /** * @brief Represents the pipeline, set of nodes and connections between them */ diff --git a/include/depthai/pipeline/ThreadedNode.hpp b/include/depthai/pipeline/ThreadedNode.hpp index 9ef4d836a..84475b1fc 100644 --- a/include/depthai/pipeline/ThreadedNode.hpp +++ b/include/depthai/pipeline/ThreadedNode.hpp @@ -51,7 +51,9 @@ class ThreadedNode : public Node { virtual void run() = 0; // check if still running - bool isRunning(); + bool isRunning() const; + + bool mainLoop(); /** * @brief Sets the logging severity level for this node. diff --git a/include/depthai/pipeline/datatype/DatatypeEnum.hpp b/include/depthai/pipeline/datatype/DatatypeEnum.hpp index a501ad7f4..7be2fa4f7 100644 --- a/include/depthai/pipeline/datatype/DatatypeEnum.hpp +++ b/include/depthai/pipeline/datatype/DatatypeEnum.hpp @@ -45,6 +45,7 @@ enum class DatatypeEnum : std::int32_t { CoverageData, PipelineEvent, PipelineState, + PipelineEventAggregationConfig, }; bool isDatatypeSubclassOf(DatatypeEnum parent, DatatypeEnum children); diff --git a/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp b/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp new file mode 100644 index 000000000..afa804b0c --- /dev/null +++ b/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include + +#include "depthai/common/optional.hpp" +#include "depthai/pipeline/datatype/Buffer.hpp" +#include "depthai/pipeline/datatype/DatatypeEnum.hpp" + +namespace dai { + +class NodeEventAggregationConfig { + public: + int64_t nodeId = -1; + std::optional> inputs; + std::optional> outputs; + std::optional> others; + bool summary = false; + bool events = false; + + DEPTHAI_SERIALIZE(NodeEventAggregationConfig, nodeId, inputs, outputs, others, summary, events); +}; + +/// PipelineEventAggregationConfig configuration structure +class PipelineEventAggregationConfig : public Buffer { + public: + std::vector nodes; + bool repeat = false; // Keep sending the aggregated state without waiting for new config + + PipelineEventAggregationConfig() = default; + virtual ~PipelineEventAggregationConfig(); + + void serialize(std::vector& metadata, DatatypeEnum& datatype) const override; + + DEPTHAI_SERIALIZE(PipelineEventAggregationConfig, nodes, repeat); +}; + +} // namespace dai diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index 325d53704..d6665c292 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -13,7 +13,8 @@ class NodeState { struct DurationEvent { PipelineEvent startEvent; uint64_t durationUs; - DEPTHAI_SERIALIZE(DurationEvent, startEvent, durationUs); + float fps; + DEPTHAI_SERIALIZE(DurationEvent, startEvent, durationUs, fps); }; struct TimingStats { uint64_t minMicros = -1; @@ -23,7 +24,8 @@ class NodeState { uint64_t minMicrosRecent = -1; uint64_t maxMicrosRecent; uint64_t medianMicrosRecent; - DEPTHAI_SERIALIZE(TimingStats, minMicros, maxMicros, averageMicrosRecent, stdDevMicrosRecent, minMicrosRecent, maxMicrosRecent, medianMicrosRecent); + float fps; + DEPTHAI_SERIALIZE(TimingStats, minMicros, maxMicros, averageMicrosRecent, stdDevMicrosRecent, minMicrosRecent, maxMicrosRecent, medianMicrosRecent, fps); }; struct QueueStats { uint32_t maxQueued; @@ -58,13 +60,14 @@ class PipelineState : public Buffer { virtual ~PipelineState() = default; std::unordered_map nodeStates; + uint32_t configSequenceNum = 0; void serialize(std::vector& metadata, DatatypeEnum& datatype) const override { metadata = utility::serialize(*this); datatype = DatatypeEnum::PipelineState; }; - DEPTHAI_SERIALIZE(PipelineState, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeStates); + DEPTHAI_SERIALIZE(PipelineState, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeStates, configSequenceNum); }; } // namespace dai diff --git a/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp index e16fedf23..2ca556841 100644 --- a/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp +++ b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp @@ -25,6 +25,11 @@ class PipelineEventAggregation : public DeviceNodeCRTP { Input inputDevice{*this, {"inputDevice", DEFAULT_GROUP, false, 4, {{DatatypeEnum::PipelineState, false}}}}; Input inputHost{*this, {"inputHost", DEFAULT_GROUP, false, 4, {{DatatypeEnum::PipelineState, false}}}}; - std::shared_ptr build(bool hasDeviceNodes, bool hasHostNodes); + /** + * Input PipelineEventAggregationConfig message with state request parameters + */ + Input request{*this, {"request", DEFAULT_GROUP, DEFAULT_BLOCKING, DEFAULT_QUEUE_SIZE, {{{DatatypeEnum::PipelineEventAggregationConfig, false}}}, false}}; + + /** + * Output PipelineEventAggregationConfig message with state request parameters + */ + Output outRequest{*this, {"outRequest", DEFAULT_GROUP, {{{DatatypeEnum::PipelineEventAggregationConfig, false}}}}}; /** * Output message of type */ Output out{*this, {"out", DEFAULT_GROUP, {{{DatatypeEnum::PipelineState, false}}}}}; + std::shared_ptr build(bool hasDeviceNodes, bool hasHostNodes); + void run() override; }; diff --git a/include/depthai/utility/CircularBuffer.hpp b/include/depthai/utility/CircularBuffer.hpp index 6e386eb19..c4cf3c181 100644 --- a/include/depthai/utility/CircularBuffer.hpp +++ b/include/depthai/utility/CircularBuffer.hpp @@ -29,6 +29,16 @@ class CircularBuffer { } return result; } + T first() const { + if(buffer.empty()) { + throw std::runtime_error("CircularBuffer is empty"); + } + if(buffer.size() < maxSize) { + return buffer.front(); + } else { + return buffer[index]; + } + } T last() const { if(buffer.empty()) { throw std::runtime_error("CircularBuffer is empty"); diff --git a/include/depthai/utility/ImageManipImpl.hpp b/include/depthai/utility/ImageManipImpl.hpp index 505e0e09c..d3c8dea9e 100644 --- a/include/depthai/utility/ImageManipImpl.hpp +++ b/include/depthai/utility/ImageManipImpl.hpp @@ -59,7 +59,7 @@ void loop(N& node, std::shared_ptr inImage; - while(node.isRunning()) { + while(node.mainLoop()) { std::shared_ptr pConfig; bool hasConfig = false; bool needsImage = true; diff --git a/src/basalt/BasaltVIO.cpp b/src/basalt/BasaltVIO.cpp index ac2b30c8e..1eda40d08 100644 --- a/src/basalt/BasaltVIO.cpp +++ b/src/basalt/BasaltVIO.cpp @@ -53,7 +53,7 @@ void BasaltVIO::run() { Eigen::Quaterniond q(R); basalt::PoseState::SE3 opticalTransform(q, Eigen::Vector3d(0, 0, 0)); - while(isRunning()) { + while(mainLoop()) { if(!initialized) continue; pimpl->outStateQueue->pop(data); diff --git a/src/pipeline/InputQueue.cpp b/src/pipeline/InputQueue.cpp index 818f418d6..70526c9b1 100644 --- a/src/pipeline/InputQueue.cpp +++ b/src/pipeline/InputQueue.cpp @@ -14,7 +14,7 @@ InputQueue::InputQueueNode::InputQueueNode(unsigned int maxSize, bool blocking) } void InputQueue::InputQueueNode::run() { - while(isRunning()) { + while(mainLoop()) { output.send(input.get()); } } diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index b002329bd..1ee73c20a 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -671,9 +671,11 @@ void PipelineImpl::build() { auto stateMerge = parent.create()->build(hasDeviceNodes, hasHostNodes); if(deviceEventAgg) { deviceEventAgg->out.link(stateMerge->inputDevice); + stateMerge->outRequest.link(deviceEventAgg->request); } if(hostEventAgg) { hostEventAgg->out.link(stateMerge->inputHost); + stateMerge->outRequest.link(hostEventAgg->request); } pipelineStateOut = stateMerge->out.createOutputQueue(1, false); } diff --git a/src/pipeline/ThreadedNode.cpp b/src/pipeline/ThreadedNode.cpp index 3c5e79b38..242f97db4 100644 --- a/src/pipeline/ThreadedNode.cpp +++ b/src/pipeline/ThreadedNode.cpp @@ -89,9 +89,13 @@ dai::LogLevel ThreadedNode::getLogLevel() const { return spdlogLevelToLogLevel(pimpl->logger->level(), LogLevel::WARN); } -bool ThreadedNode::isRunning() { - this->pipelineEventDispatcher->pingEvent("_mainLoop"); +bool ThreadedNode::isRunning() const { return running; } +bool ThreadedNode::mainLoop() { + this->pipelineEventDispatcher->pingEvent("_mainLoop"); + return isRunning(); +} + } // namespace dai diff --git a/src/pipeline/datatype/DatatypeEnum.cpp b/src/pipeline/datatype/DatatypeEnum.cpp index 91bfb3231..2d3a3e8ff 100644 --- a/src/pipeline/datatype/DatatypeEnum.cpp +++ b/src/pipeline/datatype/DatatypeEnum.cpp @@ -38,6 +38,7 @@ const std::unordered_map> hierarchy = { DatatypeEnum::MessageGroup, DatatypeEnum::PipelineEvent, DatatypeEnum::PipelineState, + DatatypeEnum::PipelineEventAggregationConfig, DatatypeEnum::PointCloudConfig, DatatypeEnum::PointCloudData, DatatypeEnum::RGBDData, @@ -77,6 +78,7 @@ const std::unordered_map> hierarchy = { DatatypeEnum::MessageGroup, DatatypeEnum::PipelineEvent, DatatypeEnum::PipelineState, + DatatypeEnum::PipelineEventAggregationConfig, DatatypeEnum::PointCloudConfig, DatatypeEnum::PointCloudData, DatatypeEnum::RGBDData, @@ -114,6 +116,7 @@ const std::unordered_map> hierarchy = { {DatatypeEnum::MessageGroup, {}}, {DatatypeEnum::PipelineEvent, {}}, {DatatypeEnum::PipelineState, {}}, + {DatatypeEnum::PipelineEventAggregationConfig, {}}, {DatatypeEnum::PointCloudConfig, {}}, {DatatypeEnum::PointCloudData, {}}, {DatatypeEnum::RGBDData, {}}, diff --git a/src/pipeline/datatype/PipelineEventAggregationConfig.cpp b/src/pipeline/datatype/PipelineEventAggregationConfig.cpp new file mode 100644 index 000000000..8ef38e1ba --- /dev/null +++ b/src/pipeline/datatype/PipelineEventAggregationConfig.cpp @@ -0,0 +1,12 @@ +#include "depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp" + +namespace dai { + +PipelineEventAggregationConfig::~PipelineEventAggregationConfig() = default; + +void PipelineEventAggregationConfig::serialize(std::vector& metadata, DatatypeEnum& datatype) const { + metadata = utility::serialize(*this); + datatype = DatatypeEnum::PipelineEventAggregationConfig; +} + +} // namespace dai diff --git a/src/pipeline/datatype/StreamMessageParser.cpp b/src/pipeline/datatype/StreamMessageParser.cpp index 7ec39151d..f25884b02 100644 --- a/src/pipeline/datatype/StreamMessageParser.cpp +++ b/src/pipeline/datatype/StreamMessageParser.cpp @@ -16,6 +16,7 @@ #include "depthai/pipeline/datatype/Buffer.hpp" #include "depthai/pipeline/datatype/CameraControl.hpp" #include "depthai/pipeline/datatype/PipelineEvent.hpp" +#include "depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp" #include "depthai/pipeline/datatype/PipelineState.hpp" #ifdef DEPTHAI_HAVE_DYNAMIC_CALIBRATION_SUPPORT #include "depthai/pipeline/datatype/DynamicCalibrationControl.hpp" @@ -246,6 +247,9 @@ std::shared_ptr StreamMessageParser::parseMessage(streamPacketDesc_t* case DatatypeEnum::PipelineState: return parseDatatype(metadataStart, serializedObjectSize, data, fd); break; + case DatatypeEnum::PipelineEventAggregationConfig: + return parseDatatype(metadataStart, serializedObjectSize, data, fd); + break; case DatatypeEnum::MessageGroup: return parseDatatype(metadataStart, serializedObjectSize, data, fd); break; diff --git a/src/pipeline/node/AprilTag.cpp b/src/pipeline/node/AprilTag.cpp index 7fe371e2a..765af8d42 100644 --- a/src/pipeline/node/AprilTag.cpp +++ b/src/pipeline/node/AprilTag.cpp @@ -206,7 +206,7 @@ void AprilTag::run() { // Handle possible errors during configuration handleErrors(errno); - while(isRunning()) { + while(mainLoop()) { // Retrieve config from user if available if(properties.inputConfigSync) { inConfig = inputConfig.get(); diff --git a/src/pipeline/node/BenchmarkIn.cpp b/src/pipeline/node/BenchmarkIn.cpp index 539f748a4..634a0153d 100644 --- a/src/pipeline/node/BenchmarkIn.cpp +++ b/src/pipeline/node/BenchmarkIn.cpp @@ -55,7 +55,7 @@ void BenchmarkIn::run() { auto start = steady_clock::now(); - while(isRunning()) { + while(mainLoop()) { auto inMessage = input.get(); // If this is the first message of the batch, reset counters diff --git a/src/pipeline/node/DynamicCalibrationNode.cpp b/src/pipeline/node/DynamicCalibrationNode.cpp index cd32d0f91..c9da5c484 100644 --- a/src/pipeline/node/DynamicCalibrationNode.cpp +++ b/src/pipeline/node/DynamicCalibrationNode.cpp @@ -557,7 +557,7 @@ void DynamicCalibration::run() { auto previousLoadingTimeFloat = std::chrono::steady_clock::now() + std::chrono::duration(calibrationPeriod); auto previousLoadingTime = std::chrono::time_point_cast(previousLoadingTimeFloat); initializePipeline(device); - while(isRunning()) { + while(mainLoop()) { slept = false; doWork(previousLoadingTime); if(!slept) { diff --git a/src/pipeline/node/ImageAlign.cpp b/src/pipeline/node/ImageAlign.cpp index 794001de8..d1da2a960 100644 --- a/src/pipeline/node/ImageAlign.cpp +++ b/src/pipeline/node/ImageAlign.cpp @@ -353,7 +353,7 @@ void ImageAlign::run() { ImgFrame inputAlignToImgFrame; uint32_t currentEepromId = getParentPipeline().getEepromId(); - while(isRunning()) { + while(mainLoop()) { auto inputImg = input.get(); if(!initialized) { @@ -585,4 +585,4 @@ void ImageAlign::run() { #endif // DEPTHAI_HAVE_OPENCV_SUPPORT } // namespace node -} // namespace dai \ No newline at end of file +} // namespace dai diff --git a/src/pipeline/node/ImageFilters.cpp b/src/pipeline/node/ImageFilters.cpp index 5c5757201..b474d6284 100644 --- a/src/pipeline/node/ImageFilters.cpp +++ b/src/pipeline/node/ImageFilters.cpp @@ -818,7 +818,7 @@ void ImageFilters::run() { filters.size() == 0, getFilterPipelineString()); - while(isRunning()) { + while(mainLoop()) { // Set config while(inputConfig.has()) { auto configMsg = inputConfig.get(); @@ -946,7 +946,7 @@ void ToFDepthConfidenceFilter::applyDepthConfidenceFilter(std::shared_ptr(); diff --git a/src/pipeline/node/ObjectTracker.cpp b/src/pipeline/node/ObjectTracker.cpp index 1756c40e7..fd2ee507f 100644 --- a/src/pipeline/node/ObjectTracker.cpp +++ b/src/pipeline/node/ObjectTracker.cpp @@ -98,7 +98,7 @@ void ObjectTracker::run() { impl::OCSTracker tracker(properties); - while(isRunning()) { + while(mainLoop()) { std::shared_ptr inputTrackerImg; std::shared_ptr inputDetectionImg; std::shared_ptr inputImgDetections; diff --git a/src/pipeline/node/Sync.cpp b/src/pipeline/node/Sync.cpp index 5d0048969..c22f82af1 100644 --- a/src/pipeline/node/Sync.cpp +++ b/src/pipeline/node/Sync.cpp @@ -50,7 +50,7 @@ void Sync::run() { auto syncThresholdNs = properties.syncThresholdNs; logger->trace("Sync threshold: {}", syncThresholdNs); - while(isRunning()) { + while(mainLoop()) { auto tAbsoluteBeginning = steady_clock::now(); std::unordered_map> inputFrames; for(auto name : inputNames) { diff --git a/src/pipeline/node/host/Display.cpp b/src/pipeline/node/host/Display.cpp index 257a4fc8f..1e7488a88 100644 --- a/src/pipeline/node/host/Display.cpp +++ b/src/pipeline/node/host/Display.cpp @@ -37,7 +37,7 @@ Display::Display(std::string name) : name(std::move(name)) {} void Display::run() { auto fpsCounter = FPSCounter(); - while(isRunning()) { + while(mainLoop()) { std::shared_ptr imgFrame = input.get(); if(imgFrame != nullptr) { fpsCounter.update(); diff --git a/src/pipeline/node/host/HostCamera.cpp b/src/pipeline/node/host/HostCamera.cpp index 7b5cfac4c..f698f9d26 100644 --- a/src/pipeline/node/host/HostCamera.cpp +++ b/src/pipeline/node/host/HostCamera.cpp @@ -13,7 +13,7 @@ void HostCamera::run() { throw std::runtime_error("Couldn't open camera"); } int64_t seqNum = 0; - while(isRunning()) { + while(mainLoop()) { cv::Mat frame; auto success = cap.read(frame); if(frame.empty() || !success) { @@ -36,4 +36,4 @@ void HostCamera::run() { } } // namespace node -} // namespace dai \ No newline at end of file +} // namespace dai diff --git a/src/pipeline/node/host/HostNode.cpp b/src/pipeline/node/host/HostNode.cpp index 885a3a533..e5d6522b3 100644 --- a/src/pipeline/node/host/HostNode.cpp +++ b/src/pipeline/node/host/HostNode.cpp @@ -17,7 +17,7 @@ void HostNode::buildStage1() { } void HostNode::run() { - while(isRunning()) { + while(mainLoop()) { // Get input auto in = input.get(); // Create a lambda that captures the class as a shared pointer and the message diff --git a/src/pipeline/node/host/RGBD.cpp b/src/pipeline/node/host/RGBD.cpp index 92fd67d84..5f964b1b9 100644 --- a/src/pipeline/node/host/RGBD.cpp +++ b/src/pipeline/node/host/RGBD.cpp @@ -363,7 +363,7 @@ void RGBD::initialize(std::shared_ptr frames) { } void RGBD::run() { - while(isRunning()) { + while(mainLoop()) { if(!pcl.getQueueConnections().empty() || !pcl.getConnections().empty() || !rgbd.getQueueConnections().empty() || !rgbd.getConnections().empty()) { // Get the color and depth frames auto group = inSync.get(); diff --git a/src/pipeline/node/host/Record.cpp b/src/pipeline/node/host/Record.cpp index 4604d2bf7..36f6316d5 100644 --- a/src/pipeline/node/host/Record.cpp +++ b/src/pipeline/node/host/Record.cpp @@ -53,7 +53,7 @@ void RecordVideo::run() { unsigned int i = 0; auto start = std::chrono::steady_clock::now(); auto end = std::chrono::steady_clock::now(); - while(isRunning()) { + while(mainLoop()) { auto msg = input.get(); if(msg == nullptr) continue; if(streamType == DatatypeEnum::ADatatype) { @@ -150,7 +150,7 @@ void RecordMetadataOnly::run() { utility::ByteRecorder byteRecorder; DatatypeEnum streamType = DatatypeEnum::ADatatype; - while(isRunning()) { + while(mainLoop()) { auto msg = input.get(); if(msg == nullptr) continue; if(streamType == DatatypeEnum::ADatatype) { diff --git a/src/pipeline/node/host/Replay.cpp b/src/pipeline/node/host/Replay.cpp index 70e00d7fc..d995179c6 100644 --- a/src/pipeline/node/host/Replay.cpp +++ b/src/pipeline/node/host/Replay.cpp @@ -99,6 +99,7 @@ inline std::shared_ptr getMessage(const std::shared_ptr getProtoMessage(utility::ByteP case DatatypeEnum::CoverageData: case DatatypeEnum::PipelineEvent: case DatatypeEnum::PipelineState: + case DatatypeEnum::PipelineEventAggregationConfig: throw std::runtime_error("Cannot replay message type: " + std::to_string((int)datatype)); } return {}; diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index bd9def4d9..670c24083 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -1,5 +1,7 @@ #include "depthai/pipeline/node/internal/PipelineEventAggregation.hpp" +#include + #include "depthai/pipeline/datatype/PipelineEvent.hpp" #include "depthai/pipeline/datatype/PipelineState.hpp" #include "depthai/utility/CircularBuffer.hpp" @@ -11,6 +13,11 @@ namespace internal { class NodeEventAggregation { private: + struct FpsMeasurement { + std::chrono::steady_clock::time_point time; + int64_t sequenceNum; + }; + std::shared_ptr logger; uint32_t windowSize; @@ -29,6 +36,10 @@ class NodeEventAggregation { std::unordered_map>> inputQueueSizesBuffers; + std::unordered_map>> inputFpsBuffers; + std::unordered_map>> outputFpsBuffers; + std::unordered_map>> otherFpsBuffers; + std::unordered_map> ongoingInputEvents; std::unordered_map> ongoingOutputEvents; std::optional ongoingMainLoopEvent; @@ -39,6 +50,9 @@ class NodeEventAggregation { private: inline bool updateIntervalBuffers(PipelineEvent& event) { using namespace std::chrono; + std::unique_ptr> emptyIntBuffer; + std::unique_ptr> emptyTimeBuffer; + auto& ongoingEvent = [&]() -> std::optional& { switch(event.type) { case PipelineEvent::Type::LOOP: @@ -72,7 +86,29 @@ class NodeEventAggregation { } return otherTimingsBuffers[event.source]; } - return mainLoopTimingsBuffer; // To silence compiler warning + return emptyIntBuffer; // To silence compiler warning + }(); + auto& fpsBuffer = [&]() -> std::unique_ptr>& { + switch(event.type) { + case PipelineEvent::Type::LOOP: + throw std::runtime_error("LOOP event should not be an interval"); + case PipelineEvent::Type::INPUT: + if(inputFpsBuffers.find(event.source) == inputFpsBuffers.end()) { + inputFpsBuffers[event.source] = std::make_unique>(windowSize); + } + return inputFpsBuffers[event.source]; + case PipelineEvent::Type::OUTPUT: + if(outputFpsBuffers.find(event.source) == outputFpsBuffers.end()) { + outputFpsBuffers[event.source] = std::make_unique>(windowSize); + } + return outputFpsBuffers[event.source]; + case PipelineEvent::Type::CUSTOM: + if(otherFpsBuffers.find(event.source) == otherFpsBuffers.end()) { + otherFpsBuffers[event.source] = std::make_unique>(windowSize); + } + return otherFpsBuffers[event.source]; + } + return emptyTimeBuffer; // To silence compiler warning }(); if(ongoingEvent.has_value() && ongoingEvent->sequenceNum == event.sequenceNum && event.interval == PipelineEvent::Interval::END) { // End event @@ -87,6 +123,7 @@ class NodeEventAggregation { } timingsBufferByType[event.type]->add(durationEvent.durationUs); timingsBuffer->add(durationEvent.durationUs); + fpsBuffer->add({durationEvent.startEvent.getTimestamp(), durationEvent.startEvent.getSequenceNum()}); ongoingEvent = std::nullopt; @@ -109,6 +146,9 @@ class NodeEventAggregation { inline bool updatePingBuffers(PipelineEvent& event) { using namespace std::chrono; + std::unique_ptr> emptyIntBuffer; + std::unique_ptr> emptyTimeBuffer; + auto& ongoingEvent = [&]() -> std::optional& { switch(event.type) { case PipelineEvent::Type::LOOP: @@ -134,7 +174,22 @@ class NodeEventAggregation { case PipelineEvent::Type::OUTPUT: throw std::runtime_error("INPUT and OUTPUT events should not be pings"); } - return mainLoopTimingsBuffer; // To silence compiler warning + return emptyIntBuffer; // To silence compiler warning + }(); + auto& fpsBuffer = [&]() -> std::unique_ptr>& { + switch(event.type) { + case PipelineEvent::Type::INPUT: + case PipelineEvent::Type::OUTPUT: + throw std::runtime_error("INPUT and OUTPUT events should not be pings"); + case PipelineEvent::Type::LOOP: + break; + case PipelineEvent::Type::CUSTOM: + if(otherFpsBuffers.find(event.source) == otherFpsBuffers.end()) { + otherFpsBuffers[event.source] = std::make_unique>(windowSize); + } + return otherFpsBuffers[event.source]; + } + return emptyTimeBuffer; // To silence compiler warning }(); if(ongoingEvent.has_value() && ongoingEvent->sequenceNum == event.sequenceNum - 1) { // End event @@ -152,6 +207,7 @@ class NodeEventAggregation { } timingsBufferByType[event.type]->add(durationEvent.durationUs); timingsBuffer->add(durationEvent.durationUs); + if(fpsBuffer) fpsBuffer->add({durationEvent.startEvent.getTimestamp(), durationEvent.startEvent.getSequenceNum()}); // Start event ongoingEvent = event; @@ -199,6 +255,14 @@ class NodeEventAggregation { stats.medianMicrosRecent = (stats.medianMicrosRecent + bufferByType[bufferByType.size() / 2 - 1]) / 2; } } + inline void updateFpsStats(NodeState::TimingStats& stats, const utility::CircularBuffer& buffer) { + if(buffer.size() < 2) return; + auto timeDiff = std::chrono::duration_cast(buffer.last().time - buffer.first().time).count(); + auto frameDiff = buffer.last().sequenceNum - buffer.first().sequenceNum; + if(timeDiff > 0 && buffer.last().sequenceNum > buffer.first().sequenceNum) { + stats.fps = frameDiff * (1e6f / (float)timeDiff); + } + } public: void add(PipelineEvent& event) { @@ -224,15 +288,19 @@ class NodeEventAggregation { switch(event.type) { case PipelineEvent::Type::CUSTOM: updateTimingStats(state.otherStats[event.source], *otherTimingsBuffers[event.source]); + updateFpsStats(state.otherStats[event.source], *otherFpsBuffers[event.source]); break; case PipelineEvent::Type::LOOP: updateTimingStats(state.mainLoopStats, *mainLoopTimingsBuffer); + state.mainLoopStats.fps = 1e6f / (float)state.mainLoopStats.averageMicrosRecent; break; case PipelineEvent::Type::INPUT: updateTimingStats(state.inputStates[event.source].timingStats, *inputTimingsBuffers[event.source]); + updateFpsStats(state.inputStates[event.source].timingStats, *inputFpsBuffers[event.source]); break; case PipelineEvent::Type::OUTPUT: updateTimingStats(state.outputStats[event.source], *outputTimingsBuffers[event.source]); + updateFpsStats(state.outputStats[event.source], *outputFpsBuffers[event.source]); break; } } @@ -267,7 +335,7 @@ void PipelineEventAggregation::run() { auto& logger = pimpl->logger; std::unordered_map nodeStates; uint32_t sequenceNum = 0; - while(isRunning()) { + while(mainLoop()) { std::unordered_map> events; for(auto& [k, v] : inputs) { events[k.second] = v.tryGet(); diff --git a/src/pipeline/node/internal/PipelineStateMerge.cpp b/src/pipeline/node/internal/PipelineStateMerge.cpp index 177ec3325..157c6c4c1 100644 --- a/src/pipeline/node/internal/PipelineStateMerge.cpp +++ b/src/pipeline/node/internal/PipelineStateMerge.cpp @@ -1,7 +1,7 @@ #include "depthai/pipeline/node/internal/PipelineStateMerge.hpp" +#include "depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp" #include "depthai/pipeline/datatype/PipelineState.hpp" - #include "pipeline/ThreadedNodeImpl.hpp" namespace dai { @@ -17,7 +17,8 @@ void mergeStates(std::shared_ptr& outState, const std::shared_ptr for(const auto& [key, value] : inState->nodeStates) { if(outState->nodeStates.find(key) != outState->nodeStates.end()) { throw std::runtime_error("PipelineStateMerge: Duplicate node state for nodeId " + std::to_string(key)); - } else outState->nodeStates[key] = value; + } else + outState->nodeStates[key] = value; } } void PipelineStateMerge::run() { @@ -25,11 +26,27 @@ void PipelineStateMerge::run() { if(!hasDeviceNodes && !hasHostNodes) { logger->warn("PipelineStateMerge: both device and host nodes are disabled. Have you built the node?"); } + std::optional currentConfig; uint32_t sequenceNum = 0; - while(isRunning()) { + while(mainLoop()) { auto outState = std::make_shared(); + bool waitForMatch = false; + if(!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeat) || request.has()) { + auto req = request.get(); + if(req != nullptr) { + currentConfig = *req; + currentConfig->setSequenceNum(++sequenceNum); + waitForMatch = true; + } + } if(hasDeviceNodes) { auto deviceState = inputDevice.get(); + if(waitForMatch && deviceState != nullptr && currentConfig.has_value()) { + while(isRunning() && deviceState->sequenceNum != currentConfig->sequenceNum) { + deviceState = inputDevice.get(); + if(!isRunning()) break; + } + } if(deviceState != nullptr) { *outState = *deviceState; } diff --git a/src/pipeline/node/internal/XLinkOutHost.cpp b/src/pipeline/node/internal/XLinkOutHost.cpp index c7890fbce..de5d782d4 100644 --- a/src/pipeline/node/internal/XLinkOutHost.cpp +++ b/src/pipeline/node/internal/XLinkOutHost.cpp @@ -53,7 +53,7 @@ void XLinkOutHost::run() { stream = XLinkStream(this->conn, this->streamName, maxSize); currentMaxSize = maxSize; }; - while(isRunning()) { + while(mainLoop()) { try { auto outgoing = in.get(); auto metadata = StreamMessageParser::serializeMetadata(outgoing); diff --git a/src/pipeline/node/test/MyProducer.cpp b/src/pipeline/node/test/MyProducer.cpp index 260d6807f..611cc14ad 100644 --- a/src/pipeline/node/test/MyProducer.cpp +++ b/src/pipeline/node/test/MyProducer.cpp @@ -12,7 +12,7 @@ namespace test { void MyProducer::run() { std::cout << "Hello, I just woke up and I'm ready to do some work!\n"; - while(isRunning()) { + while(mainLoop()) { std::this_thread::sleep_for(std::chrono::milliseconds(1500)); auto buf = std::make_shared(); diff --git a/src/rtabmap/RTABMapSLAM.cpp b/src/rtabmap/RTABMapSLAM.cpp index 65243135b..d7a5ed0d8 100644 --- a/src/rtabmap/RTABMapSLAM.cpp +++ b/src/rtabmap/RTABMapSLAM.cpp @@ -115,7 +115,7 @@ void RTABMapSLAM::odomPoseCB(std::shared_ptr data) { void RTABMapSLAM::run() { auto& logger = pimpl->logger; - while(isRunning()) { + while(mainLoop()) { if(!initialized) { continue; } else { diff --git a/src/rtabmap/RTABMapVIO.cpp b/src/rtabmap/RTABMapVIO.cpp index da82d0924..55f1ddae5 100644 --- a/src/rtabmap/RTABMapVIO.cpp +++ b/src/rtabmap/RTABMapVIO.cpp @@ -129,7 +129,7 @@ void RTABMapVIO::syncCB(std::shared_ptr data) { } void RTABMapVIO::run() { - while(isRunning()) { + while(mainLoop()) { auto msg = inSync.get(); if(msg != nullptr) { syncCB(msg); diff --git a/src/utility/ProtoSerialize.cpp b/src/utility/ProtoSerialize.cpp index d94870502..5894fcbb7 100644 --- a/src/utility/ProtoSerialize.cpp +++ b/src/utility/ProtoSerialize.cpp @@ -172,6 +172,7 @@ bool deserializationSupported(DatatypeEnum datatype) { case DatatypeEnum::CoverageData: case DatatypeEnum::PipelineEvent: case DatatypeEnum::PipelineState: + case DatatypeEnum::PipelineEventAggregationConfig: return false; } return false; diff --git a/tests/src/ondevice_tests/input_output_naming_test.cpp b/tests/src/ondevice_tests/input_output_naming_test.cpp index b246788b2..d845ced6e 100644 --- a/tests/src/ondevice_tests/input_output_naming_test.cpp +++ b/tests/src/ondevice_tests/input_output_naming_test.cpp @@ -8,7 +8,7 @@ class TestNode : public dai::node::CustomThreadedNode { public: void run() override { - while(isRunning()) { + while(mainLoop()) { // do nothing } } From c34cc45b15f9bad1df7b92130e9aa02ff547987e Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 6 Oct 2025 12:31:38 +0200 Subject: [PATCH 018/124] Implement service like api for pipeline state --- .../internal/PipelineEventAggregation.cpp | 21 +++++++++++++++---- .../node/internal/PipelineStateMerge.cpp | 11 ++++++++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 670c24083..cc778cb2e 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -3,6 +3,7 @@ #include #include "depthai/pipeline/datatype/PipelineEvent.hpp" +#include "depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp" #include "depthai/pipeline/datatype/PipelineState.hpp" #include "depthai/utility/CircularBuffer.hpp" #include "pipeline/ThreadedNodeImpl.hpp" @@ -334,6 +335,7 @@ bool PipelineEventAggregation::runOnHost() const { void PipelineEventAggregation::run() { auto& logger = pimpl->logger; std::unordered_map nodeStates; + std::optional currentConfig; uint32_t sequenceNum = 0; while(mainLoop()) { std::unordered_map> events; @@ -357,10 +359,21 @@ void PipelineEventAggregation::run() { shouldSend = true; } } - outState->sequenceNum = sequenceNum++; - outState->setTimestamp(std::chrono::steady_clock::now()); - outState->tsDevice = outState->ts; - if(shouldSend) out.send(outState); + bool gotConfig = false; + if(!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeat) || request.has()) { + auto req = request.get(); + if(req != nullptr) { + currentConfig = *req; + gotConfig = true; + } + } + if(gotConfig || (currentConfig.has_value() && currentConfig->repeat)) { + outState->sequenceNum = sequenceNum++; + outState->configSequenceNum = currentConfig.has_value() ? currentConfig->sequenceNum : 0; + outState->setTimestamp(std::chrono::steady_clock::now()); + outState->tsDevice = outState->ts; + if(gotConfig || shouldSend) out.send(outState); + } } } diff --git a/src/pipeline/node/internal/PipelineStateMerge.cpp b/src/pipeline/node/internal/PipelineStateMerge.cpp index 157c6c4c1..d532ec161 100644 --- a/src/pipeline/node/internal/PipelineStateMerge.cpp +++ b/src/pipeline/node/internal/PipelineStateMerge.cpp @@ -28,6 +28,7 @@ void PipelineStateMerge::run() { } std::optional currentConfig; uint32_t sequenceNum = 0; + uint32_t configSequenceNum = 0; while(mainLoop()) { auto outState = std::make_shared(); bool waitForMatch = false; @@ -35,14 +36,14 @@ void PipelineStateMerge::run() { auto req = request.get(); if(req != nullptr) { currentConfig = *req; - currentConfig->setSequenceNum(++sequenceNum); + currentConfig->setSequenceNum(++configSequenceNum); waitForMatch = true; } } if(hasDeviceNodes) { auto deviceState = inputDevice.get(); if(waitForMatch && deviceState != nullptr && currentConfig.has_value()) { - while(isRunning() && deviceState->sequenceNum != currentConfig->sequenceNum) { + while(isRunning() && deviceState->configSequenceNum != currentConfig->sequenceNum) { deviceState = inputDevice.get(); if(!isRunning()) break; } @@ -53,6 +54,12 @@ void PipelineStateMerge::run() { } if(hasHostNodes) { auto hostState = inputHost.get(); + if(waitForMatch && hostState != nullptr && currentConfig.has_value()) { + while(isRunning() && hostState->configSequenceNum != currentConfig->sequenceNum) { + hostState = inputHost.get(); + if(!isRunning()) break; + } + } if(hostState != nullptr) { if(hasDeviceNodes) { // merge From fcbfe787f713d5df9d64bb06bf01026602a0d426 Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 6 Oct 2025 15:44:19 +0200 Subject: [PATCH 019/124] Remove static assert --- include/depthai/pipeline/Pipeline.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index 2f686ec36..bb6dae7c3 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -260,7 +260,6 @@ class PipelineImpl : public std::enable_shared_from_this { */ template class NodeStateApi { - static_assert(false); }; template <> class NodeStateApi> { From 28e5b93386dbbfbc02a6a2abaf73415eb3f0fa4b Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 7 Oct 2025 08:50:26 +0200 Subject: [PATCH 020/124] Bugfixes --- examples/cpp/HostNodes/image_manip_host.cpp | 7 +- include/depthai/pipeline/Pipeline.hpp | 458 +++++++++--------- src/pipeline/Pipeline.cpp | 8 +- .../internal/PipelineEventAggregation.cpp | 1 + .../node/internal/PipelineStateMerge.cpp | 1 + 5 files changed, 254 insertions(+), 221 deletions(-) diff --git a/examples/cpp/HostNodes/image_manip_host.cpp b/examples/cpp/HostNodes/image_manip_host.cpp index c43d75c37..6bf7edafa 100644 --- a/examples/cpp/HostNodes/image_manip_host.cpp +++ b/examples/cpp/HostNodes/image_manip_host.cpp @@ -40,5 +40,10 @@ int main(int argc, char** argv) { pipeline.start(); - pipeline.wait(); + while(pipeline.isRunning()) { + std::this_thread::sleep_for(std::chrono::milliseconds(5000)); + std::cout << "Pipeline state: " << pipeline.getPipelineState().nodes().detailed().str() << std::endl; + } + + pipeline.stop(); } diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index bb6dae7c3..1739a7907 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -20,6 +20,7 @@ // shared #include "depthai/device/BoardConfig.hpp" +#include "depthai/pipeline/InputQueue.hpp" #include "depthai/pipeline/PipelineSchema.hpp" #include "depthai/pipeline/datatype/PipelineState.hpp" #include "depthai/properties/GlobalProperties.hpp" @@ -29,6 +30,244 @@ namespace dai { namespace fs = std::filesystem; +/** + * pipeline.getState().nodes({nodeId1}).summary() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).detailed() -> std::unordered_map; + * pipeline.getState().nodes(nodeId1).detailed() -> NodeState; + * pipeline.getState().nodes({nodeId1}).outputs() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).outputs({outputName1}) -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).outputs(outputName) -> TimingStats; + * pipeline.getState().nodes({nodeId1}).events(); + * pipeline.getState().nodes({nodeId1}).inputs() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).inputs({inputName1}) -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).inputs(inputName) -> QueueState; + * pipeline.getState().nodes({nodeId1}).otherStats() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).otherStats({statName1}) -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).outputs(statName) -> TimingStats; + */ +template +class NodeStateApi {}; +template <> +class NodeStateApi> { + std::vector nodeIds; + + std::shared_ptr pipelineStateOut; + std::shared_ptr pipelineStateRequest; + + public: + explicit NodeStateApi(std::vector nodeIds, std::shared_ptr pipelineStateOut, std::shared_ptr pipelineStateRequest) + : nodeIds(std::move(nodeIds)), pipelineStateOut(pipelineStateOut), pipelineStateRequest(pipelineStateRequest) {} + std::unordered_map> summary() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.summary = true; + cfg.nodes.push_back(nodeCfg); + } + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + return {}; // TODO + } + PipelineState detailed() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.summary = true; // contains main loop timing + nodeCfg.inputs = {}; // send all + nodeCfg.outputs = {}; // send all + nodeCfg.others = {}; // send all + cfg.nodes.push_back(nodeCfg); + } + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + return *state; + } + std::unordered_map> outputs() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.outputs = {}; // send all + cfg.nodes.push_back(nodeCfg); + } + + // TODO send and get + return {}; + } + std::unordered_map> inputs() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.inputs = {}; // send all + cfg.nodes.push_back(nodeCfg); + } + + // TODO send and get + return {}; + } + std::unordered_map> otherStats() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.others = {}; // send all + cfg.nodes.push_back(nodeCfg); + } + + // TODO send and get + return {}; + } +}; +template <> +class NodeStateApi { + Node::Id nodeId; + + std::shared_ptr pipelineStateOut; + std::shared_ptr pipelineStateRequest; + + public: + explicit NodeStateApi(Node::Id nodeId, std::shared_ptr pipelineStateOut, std::shared_ptr pipelineStateRequest) + : nodeId(nodeId), pipelineStateOut(pipelineStateOut), pipelineStateRequest(pipelineStateRequest) {} + std::unordered_map summary() { + return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).summary()[nodeId]; + } + NodeState detailed() { + return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).detailed().nodeStates[nodeId]; + } + std::unordered_map outputs() { + return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).outputs()[nodeId]; + } + std::unordered_map inputs() { + return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).inputs()[nodeId]; + } + std::unordered_map otherStats() { + return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).otherStats()[nodeId]; + } + std::unordered_map outputs(const std::vector& outputNames) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.outputs = outputNames; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + NodeState::TimingStats outputs(const std::string& outputName) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.outputs = {outputName}; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + std::unordered_map> events() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.events = true; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + std::unordered_map inputs(const std::vector& inputNames) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.inputs = inputNames; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + NodeState::QueueState inputs(const std::string& inputName) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.inputs = {inputName}; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + std::unordered_map otherStats(const std::vector& statNames) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.others = statNames; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } + NodeState::TimingStats otherStats(const std::string& statName) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.others = {statName}; + cfg.nodes.push_back(nodeCfg); + + // TODO send and get + return {}; + } +}; +class PipelineStateApi { + std::shared_ptr pipelineStateOut; + std::shared_ptr pipelineStateRequest; + std::vector nodeIds; // empty means all nodes + + public: + PipelineStateApi(std::shared_ptr pipelineStateOut, std::shared_ptr pipelineStateRequest, const std::vector>& allNodes) + : pipelineStateOut(std::move(pipelineStateOut)), pipelineStateRequest(std::move(pipelineStateRequest)) { + for(const auto& n : allNodes) { + nodeIds.push_back(n->id); + } + } + NodeStateApi> nodes() { + return NodeStateApi>(nodeIds, pipelineStateOut, pipelineStateRequest); + } + NodeStateApi> nodes(const std::vector& nodeIds) { + return NodeStateApi>(nodeIds, pipelineStateOut, pipelineStateRequest); + } + NodeStateApi nodes(Node::Id nodeId) { + return NodeStateApi(nodeId, pipelineStateOut, pipelineStateRequest); + } +}; + class PipelineImpl : public std::enable_shared_from_this { friend class Pipeline; friend class Node; @@ -88,7 +327,7 @@ class PipelineImpl : public std::enable_shared_from_this { bool isDeviceOnly() const; // Pipeline state getters - std::shared_ptr getPipelineState(); + PipelineStateApi getPipelineState(); // Must be incremented and unique for each node Node::Id latestId = 0; @@ -123,6 +362,7 @@ class PipelineImpl : public std::enable_shared_from_this { // Pipeline events std::shared_ptr pipelineStateOut; + std::shared_ptr pipelineStateRequest; // Output queues std::vector> outputQueues; @@ -243,220 +483,6 @@ class PipelineImpl : public std::enable_shared_from_this { std::vector loadResourceCwd(fs::path uri, fs::path cwd, bool moveAsset = false); }; -/** - * pipeline.getState().nodes({nodeId1}).summary() -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).detailed() -> std::unordered_map; - * pipeline.getState().nodes(nodeId1).detailed() -> NodeState; - * pipeline.getState().nodes({nodeId1}).outputs() -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).outputs({outputName1}) -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).outputs(outputName) -> TimingStats; - * pipeline.getState().nodes({nodeId1}).events(); - * pipeline.getState().nodes({nodeId1}).inputs() -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).inputs({inputName1}) -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).inputs(inputName) -> QueueState; - * pipeline.getState().nodes({nodeId1}).otherStats() -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).otherStats({statName1}) -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).outputs(statName) -> TimingStats; - */ -template -class NodeStateApi { -}; -template <> -class NodeStateApi> { - std::vector nodeIds; - - public: - explicit NodeStateApi(std::vector nodeIds) : nodeIds(std::move(nodeIds)) {} - std::unordered_map> summary() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - for(auto id : nodeIds) { - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = id; - nodeCfg.summary = true; - cfg.nodes.push_back(nodeCfg); - } - - // TODO send and get - return {}; - } - PipelineState detailed() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - for(auto id : nodeIds) { - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = id; - nodeCfg.summary = true; // contains main loop timing - nodeCfg.inputs = {}; // send all - nodeCfg.outputs = {}; // send all - nodeCfg.others = {}; // send all - cfg.nodes.push_back(nodeCfg); - } - - // TODO send and get - return {}; - } - std::unordered_map> outputs() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - for(auto id : nodeIds) { - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = id; - nodeCfg.outputs = {}; // send all - cfg.nodes.push_back(nodeCfg); - } - - // TODO send and get - return {}; - } - std::unordered_map> inputs() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - for(auto id : nodeIds) { - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = id; - nodeCfg.inputs = {}; // send all - cfg.nodes.push_back(nodeCfg); - } - - // TODO send and get - return {}; - } - std::unordered_map> otherStats() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - for(auto id : nodeIds) { - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = id; - nodeCfg.others = {}; // send all - cfg.nodes.push_back(nodeCfg); - } - - // TODO send and get - return {}; - } -}; -template <> -class NodeStateApi { - Node::Id nodeId; - - public: - explicit NodeStateApi(Node::Id nodeId) : nodeId(nodeId) {} - std::unordered_map summary() { - return NodeStateApi>({nodeId}).summary()[nodeId]; - } - NodeState detailed() { - return NodeStateApi>({nodeId}).detailed().nodeStates[nodeId]; - } - std::unordered_map outputs() { - return NodeStateApi>({nodeId}).outputs()[nodeId]; - } - std::unordered_map inputs() { - return NodeStateApi>({nodeId}).inputs()[nodeId]; - } - std::unordered_map otherStats() { - return NodeStateApi>({nodeId}).otherStats()[nodeId]; - } - std::unordered_map outputs(const std::vector& outputNames) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.outputs = outputNames; - cfg.nodes.push_back(nodeCfg); - - // TODO send and get - return {}; - } - NodeState::TimingStats outputs(const std::string& outputName) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.outputs = {outputName}; - cfg.nodes.push_back(nodeCfg); - - // TODO send and get - return {}; - } - std::unordered_map> events() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.events = true; - cfg.nodes.push_back(nodeCfg); - - // TODO send and get - return {}; - } - std::unordered_map inputs(const std::vector& inputNames) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.inputs = inputNames; - cfg.nodes.push_back(nodeCfg); - - // TODO send and get - return {}; - } - NodeState::QueueState inputs(const std::string& inputName) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.inputs = {inputName}; - cfg.nodes.push_back(nodeCfg); - - // TODO send and get - return {}; - } - std::unordered_map otherStats(const std::vector& statNames) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.others = statNames; - cfg.nodes.push_back(nodeCfg); - - // TODO send and get - return {}; - } - NodeState::TimingStats otherStats(const std::string& statName) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.others = {statName}; - cfg.nodes.push_back(nodeCfg); - - // TODO send and get - return {}; - } -}; -class PipelineStateApi { - public: - NodeStateApi> nodes(const std::vector& nodeIds) { - return NodeStateApi>(nodeIds); - } - NodeStateApi nodes(Node::Id nodeId) { - return NodeStateApi(nodeId); - } -}; - /** * @brief Represents the pipeline, set of nodes and connections between them */ @@ -735,7 +761,7 @@ class Pipeline { void enableHolisticReplay(const std::string& pathToRecording); // Pipeline state getters - std::shared_ptr getPipelineState(); + PipelineStateApi getPipelineState(); }; } // namespace dai diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 1ee73c20a..737cea306 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -479,9 +479,8 @@ bool PipelineImpl::isDeviceOnly() const { return deviceOnly; } -std::shared_ptr PipelineImpl::getPipelineState() { - auto pipelineState = pipelineStateOut->get(); - return pipelineState; +PipelineStateApi PipelineImpl::getPipelineState() { + return PipelineStateApi(pipelineStateOut, pipelineStateRequest, getAllNodes()); } void PipelineImpl::add(std::shared_ptr node) { @@ -678,6 +677,7 @@ void PipelineImpl::build() { stateMerge->outRequest.link(hostEventAgg->request); } pipelineStateOut = stateMerge->out.createOutputQueue(1, false); + pipelineStateRequest = stateMerge->request.createInputQueue(); } isBuild = true; @@ -1094,7 +1094,7 @@ void Pipeline::enableHolisticReplay(const std::string& pathToRecording) { impl()->enableHolisticRecordReplay = true; } -std::shared_ptr Pipeline::getPipelineState() { +PipelineStateApi Pipeline::getPipelineState() { return impl()->getPipelineState(); } diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index cc778cb2e..308df35fd 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -372,6 +372,7 @@ void PipelineEventAggregation::run() { outState->configSequenceNum = currentConfig.has_value() ? currentConfig->sequenceNum : 0; outState->setTimestamp(std::chrono::steady_clock::now()); outState->tsDevice = outState->ts; + // TODO: send only requested data if(gotConfig || shouldSend) out.send(outState); } } diff --git a/src/pipeline/node/internal/PipelineStateMerge.cpp b/src/pipeline/node/internal/PipelineStateMerge.cpp index d532ec161..c3d4f7a38 100644 --- a/src/pipeline/node/internal/PipelineStateMerge.cpp +++ b/src/pipeline/node/internal/PipelineStateMerge.cpp @@ -37,6 +37,7 @@ void PipelineStateMerge::run() { if(req != nullptr) { currentConfig = *req; currentConfig->setSequenceNum(++configSequenceNum); + outRequest.send(std::make_shared(currentConfig.value())); waitForMatch = true; } } From 215119d5cca6715c5e4514ec58bf25f1ce0b6c8e Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 7 Oct 2025 11:34:04 +0200 Subject: [PATCH 021/124] Collect events in a different thread --- include/depthai/pipeline/MessageQueue.hpp | 6 +- src/pipeline/node/host/Replay.cpp | 4 +- .../internal/PipelineEventAggregation.cpp | 98 ++++++++++++++----- src/utility/PipelineEventDispatcher.cpp | 3 + 4 files changed, 84 insertions(+), 27 deletions(-) diff --git a/include/depthai/pipeline/MessageQueue.hpp b/include/depthai/pipeline/MessageQueue.hpp index 7969aef1d..c4d72a7fb 100644 --- a/include/depthai/pipeline/MessageQueue.hpp +++ b/include/depthai/pipeline/MessageQueue.hpp @@ -201,16 +201,16 @@ class MessageQueue : public std::enable_shared_from_this { */ template std::shared_ptr tryGet() { - if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(name); + if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(name, getSize()); if(queue.isDestroyed()) { throw QueueException(CLOSED_QUEUE_MESSAGE); } std::shared_ptr val = nullptr; if(!queue.tryPop(val)) { - if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name); + if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name, getSize()); return nullptr; } - if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name); + if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name, getSize()); return std::dynamic_pointer_cast(val); } diff --git a/src/pipeline/node/host/Replay.cpp b/src/pipeline/node/host/Replay.cpp index d995179c6..f35f5dfb9 100644 --- a/src/pipeline/node/host/Replay.cpp +++ b/src/pipeline/node/host/Replay.cpp @@ -214,7 +214,7 @@ void ReplayVideo::run() { uint64_t index = 0; auto loopStart = std::chrono::steady_clock::now(); auto prevMsgTs = loopStart; - while(isRunning()) { + while(mainLoop()) { std::shared_ptr metadata; std::vector frame; if(hasMetadata) { @@ -341,7 +341,7 @@ void ReplayMetadataOnly::run() { bool first = true; auto loopStart = std::chrono::steady_clock::now(); auto prevMsgTs = loopStart; - while(isRunning()) { + while(mainLoop()) { std::shared_ptr metadata; std::vector frame; if(!utility::deserializationSupported(datatype)) { diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 308df35fd..6859e939e 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -1,6 +1,7 @@ #include "depthai/pipeline/node/internal/PipelineEventAggregation.hpp" #include +#include #include "depthai/pipeline/datatype/PipelineEvent.hpp" #include "depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp" @@ -274,6 +275,8 @@ class NodeEventAggregation { inputQueueSizesBuffers[event.source] = std::make_unique>(windowSize); } inputQueueSizesBuffers[event.source]->add(*event.queueSize); + } else { + throw std::runtime_error(fmt::format("INPUT END event must have queue size set source: {}, node {}", event.source, event.nodeId)); } } bool addedEvent = false; @@ -321,6 +324,67 @@ class NodeEventAggregation { } }; +class PipelineEventHandler { + std::unordered_map nodeStates; + Node::InputMap* inputs; + uint32_t aggregationWindowSize; + uint32_t eventBatchSize; + + std::atomic running; + + std::thread thread; + + std::shared_ptr logger; + + std::shared_mutex mutex; + + public: + PipelineEventHandler( + Node::InputMap* inputs, uint32_t aggregationWindowSize, uint32_t eventBatchSize, std::shared_ptr logger) + : inputs(inputs), aggregationWindowSize(aggregationWindowSize), eventBatchSize(eventBatchSize), logger(logger) {} + void threadedRun() { + while(running) { + std::unordered_map> events; + bool gotEvents = false; + for(auto& [k, v] : *inputs) { + events[k.second] = v.tryGet(); + gotEvents = gotEvents || (events[k.second] != nullptr); + } + for(auto& [k, event] : events) { + if(event != nullptr) { + if(nodeStates.find(event->nodeId) == nodeStates.end()) { + nodeStates.insert_or_assign(event->nodeId, NodeEventAggregation(aggregationWindowSize, eventBatchSize, logger)); + } + { + std::unique_lock lock(mutex); + nodeStates.at(event->nodeId).add(*event); + } + } + } + if(!gotEvents) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + } + void run() { + running = true; + thread = std::thread(&PipelineEventHandler::threadedRun, this); + } + void stop() { + running = false; + if(thread.joinable()) thread.join(); + } + bool getState(std::shared_ptr outState) { + std::shared_lock lock(mutex); + bool updated = false; + for(auto& [nodeId, nodeState] : nodeStates) { + outState->nodeStates[nodeId] = nodeState.state; + if(nodeState.count % eventBatchSize == 0) updated = true; + } + return updated; + } +}; + void PipelineEventAggregation::setRunOnHost(bool runOnHost) { runOnHostVar = runOnHost; } @@ -334,31 +398,14 @@ bool PipelineEventAggregation::runOnHost() const { void PipelineEventAggregation::run() { auto& logger = pimpl->logger; - std::unordered_map nodeStates; + + PipelineEventHandler handler(&inputs, properties.aggregationWindowSize, properties.eventBatchSize, logger); + handler.run(); + std::optional currentConfig; uint32_t sequenceNum = 0; while(mainLoop()) { - std::unordered_map> events; - for(auto& [k, v] : inputs) { - events[k.second] = v.tryGet(); - } - for(auto& [k, event] : events) { - if(event != nullptr) { - if(nodeStates.find(event->nodeId) == nodeStates.end()) { - nodeStates.insert_or_assign(event->nodeId, NodeEventAggregation(properties.aggregationWindowSize, properties.eventBatchSize, logger)); - } - nodeStates.at(event->nodeId).add(*event); - } - } auto outState = std::make_shared(); - bool shouldSend = false; - for(auto& [nodeId, nodeState] : nodeStates) { - if(nodeState.count % properties.eventBatchSize == 0) { - outState->nodeStates[nodeId] = nodeState.state; - if(!properties.sendEvents) outState->nodeStates[nodeId].events.clear(); - shouldSend = true; - } - } bool gotConfig = false; if(!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeat) || request.has()) { auto req = request.get(); @@ -368,14 +415,21 @@ void PipelineEventAggregation::run() { } } if(gotConfig || (currentConfig.has_value() && currentConfig->repeat)) { + bool updated = handler.getState(outState); + for(auto& [nodeId, nodeState] : outState->nodeStates) { + if(!properties.sendEvents) nodeState.events.clear(); + } + outState->sequenceNum = sequenceNum++; outState->configSequenceNum = currentConfig.has_value() ? currentConfig->sequenceNum : 0; outState->setTimestamp(std::chrono::steady_clock::now()); outState->tsDevice = outState->ts; // TODO: send only requested data - if(gotConfig || shouldSend) out.send(outState); + if(gotConfig || (currentConfig.has_value() && currentConfig->repeat && updated)) out.send(outState); } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); } + handler.stop(); } } // namespace internal diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index 1fdb32831..33bff7a04 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -26,6 +26,7 @@ void PipelineEventDispatcher::addEvent(const std::string& source, PipelineEvent: } } void PipelineEventDispatcher::startEvent(const std::string& source, std::optional queueSize, std::optional metadata) { + // TODO add mutex checkNodeId(); if(events.find(source) == events.end()) { throw std::runtime_error("Event with name " + source + " does not exist"); @@ -49,6 +50,7 @@ void PipelineEventDispatcher::startEvent(const std::string& source, std::optiona } } void PipelineEventDispatcher::endEvent(const std::string& source, std::optional queueSize, std::optional metadata) { + // TODO add mutex checkNodeId(); auto now = std::chrono::steady_clock::now(); @@ -77,6 +79,7 @@ void PipelineEventDispatcher::endEvent(const std::string& source, std::optional< event.event.queueSize = std::nullopt; } void PipelineEventDispatcher::pingEvent(const std::string& source) { + // TODO add mutex checkNodeId(); auto now = std::chrono::steady_clock::now(); From e8045e473279e0ff1bd26f5c050c8e43b56205f8 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 7 Oct 2025 18:00:34 +0200 Subject: [PATCH 022/124] WIP: dispatcher + state improvements --- .../datatype/PipelineStateBindings.cpp | 3 +- include/depthai/pipeline/MessageQueue.hpp | 9 +- include/depthai/pipeline/ThreadedNode.hpp | 7 +- .../pipeline/datatype/PipelineEvent.hpp | 5 +- .../pipeline/datatype/PipelineState.hpp | 47 ++++++-- include/depthai/utility/ImageManipImpl.hpp | 74 +++++++------ .../utility/PipelineEventDispatcher.hpp | 27 +++-- .../PipelineEventDispatcherInterface.hpp | 38 +++++-- src/pipeline/MessageQueue.cpp | 6 +- src/pipeline/Node.cpp | 8 +- src/pipeline/ThreadedNode.cpp | 18 +++- .../internal/PipelineEventAggregation.cpp | 69 ++++++++---- src/utility/PipelineEventDispatcher.cpp | 100 ++++++++++++------ 13 files changed, 279 insertions(+), 132 deletions(-) diff --git a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp index d6bc03086..9f40eaf72 100644 --- a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp @@ -69,9 +69,10 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { nodeState.def(py::init<>()) .def("__repr__", &NodeState::str) .def_readwrite("events", &NodeState::events, DOC(dai, NodeState, events)) - .def_readwrite("timingsByType", &NodeState::timingsByType, DOC(dai, NodeState, timingsByType)) .def_readwrite("inputStates", &NodeState::inputStates, DOC(dai, NodeState, inputStates)) .def_readwrite("outputStats", &NodeState::outputStats, DOC(dai, NodeState, outputStats)) + .def_readwrite("inputGroupStats", &NodeState::inputGroupStats, DOC(dai, NodeState, inputGroupStats)) + .def_readwrite("outputGroupStats", &NodeState::outputGroupStats, DOC(dai, NodeState, outputGroupStats)) .def_readwrite("mainLoopStats", &NodeState::mainLoopStats, DOC(dai, NodeState, mainLoopStats)) .def_readwrite("otherStats", &NodeState::otherStats, DOC(dai, NodeState, otherStats)); diff --git a/include/depthai/pipeline/MessageQueue.hpp b/include/depthai/pipeline/MessageQueue.hpp index c4d72a7fb..43e1d31e6 100644 --- a/include/depthai/pipeline/MessageQueue.hpp +++ b/include/depthai/pipeline/MessageQueue.hpp @@ -201,16 +201,15 @@ class MessageQueue : public std::enable_shared_from_this { */ template std::shared_ptr tryGet() { - if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(name, getSize()); + if(pipelineEventDispatcher) pipelineEventDispatcher->startInputEvent(name, getSize()); if(queue.isDestroyed()) { throw QueueException(CLOSED_QUEUE_MESSAGE); } std::shared_ptr val = nullptr; if(!queue.tryPop(val)) { - if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name, getSize()); return nullptr; } - if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name, getSize()); + if(pipelineEventDispatcher) pipelineEventDispatcher->endInputEvent(name, getSize()); return std::dynamic_pointer_cast(val); } @@ -230,12 +229,12 @@ class MessageQueue : public std::enable_shared_from_this { */ template std::shared_ptr get() { - if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(name, getSize()); + if(pipelineEventDispatcher) pipelineEventDispatcher->startInputEvent(name, getSize()); std::shared_ptr val = nullptr; if(!queue.waitAndPop(val)) { throw QueueException(CLOSED_QUEUE_MESSAGE); } - if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(name, getSize()); + if(pipelineEventDispatcher) pipelineEventDispatcher->endInputEvent(name, getSize()); return std::dynamic_pointer_cast(val); } diff --git a/include/depthai/pipeline/ThreadedNode.hpp b/include/depthai/pipeline/ThreadedNode.hpp index 84475b1fc..d5a2147b0 100644 --- a/include/depthai/pipeline/ThreadedNode.hpp +++ b/include/depthai/pipeline/ThreadedNode.hpp @@ -5,7 +5,6 @@ #include "depthai/utility/AtomicBool.hpp" #include "depthai/utility/JoiningThread.hpp" #include "depthai/utility/spimpl.h" -#include "depthai/utility/PipelineEventDispatcher.hpp" namespace dai { @@ -16,7 +15,7 @@ class ThreadedNode : public Node { JoiningThread thread; AtomicBool running{false}; - protected: + protected: Output pipelineEventOutput{*this, {"pipelineEventOutput", DEFAULT_GROUP, {{{DatatypeEnum::PipelineEvent, false}}}}}; void initPipelineEventDispatcher(int64_t nodeId); @@ -69,6 +68,10 @@ class ThreadedNode : public Node { */ virtual dai::LogLevel getLogLevel() const; + utility::PipelineEventDispatcherInterface::BlockPipelineEvent inputBlockEvent(const std::string& source = "defaultInputGroup"); + utility::PipelineEventDispatcherInterface::BlockPipelineEvent outputBlockEvent(const std::string& source = "defaultOutputGroup"); + utility::PipelineEventDispatcherInterface::BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source); + class Impl; spimpl::impl_ptr pimpl; }; diff --git a/include/depthai/pipeline/datatype/PipelineEvent.hpp b/include/depthai/pipeline/datatype/PipelineEvent.hpp index 3158123c5..b0c17f215 100644 --- a/include/depthai/pipeline/datatype/PipelineEvent.hpp +++ b/include/depthai/pipeline/datatype/PipelineEvent.hpp @@ -17,6 +17,8 @@ class PipelineEvent : public Buffer { LOOP = 1, INPUT = 2, OUTPUT = 3, + INPUT_BLOCK = 4, + OUTPUT_BLOCK = 5, }; enum class Interval : std::int32_t { NONE = 0, @@ -28,7 +30,6 @@ class PipelineEvent : public Buffer { virtual ~PipelineEvent() = default; int64_t nodeId = -1; - std::optional metadata; std::optional queueSize; Interval interval = Interval::NONE; Type type = Type::CUSTOM; @@ -39,7 +40,7 @@ class PipelineEvent : public Buffer { datatype = DatatypeEnum::PipelineEvent; }; - DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeId, metadata, interval, type, source); + DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeId, interval, type, source); }; } // namespace dai diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index d6665c292..e30833c92 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -24,8 +24,12 @@ class NodeState { uint64_t minMicrosRecent = -1; uint64_t maxMicrosRecent; uint64_t medianMicrosRecent; + DEPTHAI_SERIALIZE(TimingStats, minMicros, maxMicros, averageMicrosRecent, stdDevMicrosRecent, minMicrosRecent, maxMicrosRecent, medianMicrosRecent); + }; + struct Timing { float fps; - DEPTHAI_SERIALIZE(TimingStats, minMicros, maxMicros, averageMicrosRecent, stdDevMicrosRecent, minMicrosRecent, maxMicrosRecent, medianMicrosRecent, fps); + TimingStats durationStats; + DEPTHAI_SERIALIZE(Timing, fps, durationStats); }; struct QueueStats { uint32_t maxQueued; @@ -34,21 +38,42 @@ class NodeState { uint32_t medianQueuedRecent; DEPTHAI_SERIALIZE(QueueStats, maxQueued, minQueuedRecent, maxQueuedRecent, medianQueuedRecent); }; - struct QueueState { - bool waiting; + struct InputQueueState { + enum class State : std::int32_t { + IDLE = 0, + WAITING = 1, + BLOCKED = 2 + } state = State::IDLE; uint32_t numQueued; - TimingStats timingStats; + Timing timing; QueueStats queueStats; - DEPTHAI_SERIALIZE(QueueState, waiting, numQueued, timingStats); + DEPTHAI_SERIALIZE(InputQueueState, state, numQueued, timing); + }; + struct OutputQueueState { + enum class State : std::int32_t { + IDLE = 0, + BLOCKED = 1 + } state = State::IDLE; + Timing timing; + DEPTHAI_SERIALIZE(OutputQueueState, state, timing); }; + enum class State : std::int32_t { + IDLE = 0, + GETTING_INPUTS = 1, + PROCESSING = 2, + SENDING_OUTPUTS = 3 + }; + + State state = State::IDLE; std::vector events; - std::unordered_map timingsByType; - std::unordered_map outputStats; - std::unordered_map inputStates; - TimingStats mainLoopStats; - std::unordered_map otherStats; + std::unordered_map outputStates; + std::unordered_map inputStates; + Timing inputsGetTiming; + Timing outputsSendTiming; + Timing mainLoopTiming; + std::unordered_map otherTimings; - DEPTHAI_SERIALIZE(NodeState, events, timingsByType, outputStats, inputStates, mainLoopStats, otherStats); + DEPTHAI_SERIALIZE(NodeState, events, outputStates, inputStates, inputsGetTiming, outputsSendTiming, mainLoopTiming, otherTimings); }; /** diff --git a/include/depthai/utility/ImageManipImpl.hpp b/include/depthai/utility/ImageManipImpl.hpp index d3c8dea9e..5472ffd4d 100644 --- a/include/depthai/utility/ImageManipImpl.hpp +++ b/include/depthai/utility/ImageManipImpl.hpp @@ -64,44 +64,48 @@ void loop(N& node, bool hasConfig = false; bool needsImage = true; bool skipImage = false; - if(node.inputConfig.getWaitForMessage()) { - pConfig = node.inputConfig.template get(); - hasConfig = true; - if(inImage != nullptr && hasConfig && pConfig->getReusePreviousImage()) { - needsImage = false; - } - skipImage = pConfig->getSkipCurrentImage(); - } else { - pConfig = node.inputConfig.template tryGet(); - if(pConfig != nullptr) { - hasConfig = true; - } - } + { + auto blockEvent = node.inputBlockEvent(); - if(needsImage) { - inImage = node.inputImage.template get(); - if(inImage == nullptr) { - logger->warn("No input image, skipping frame"); - continue; - } - if(!hasConfig) { - auto _pConfig = node.inputConfig.template tryGet(); - if(_pConfig != nullptr) { - pConfig = _pConfig; + if(node.inputConfig.getWaitForMessage()) { + pConfig = node.inputConfig.template get(); + hasConfig = true; + if(inImage != nullptr && hasConfig && pConfig->getReusePreviousImage()) { + needsImage = false; + } + skipImage = pConfig->getSkipCurrentImage(); + } else { + pConfig = node.inputConfig.template tryGet(); + if(pConfig != nullptr) { hasConfig = true; } } - if(skipImage) { - continue; + + if(needsImage) { + inImage = node.inputImage.template get(); + if(inImage == nullptr) { + logger->warn("No input image, skipping frame"); + continue; + } + if(!hasConfig) { + auto _pConfig = node.inputConfig.template tryGet(); + if(_pConfig != nullptr) { + pConfig = _pConfig; + hasConfig = true; + } + } + if(skipImage) { + continue; + } } - } - // if has new config, parse and check if any changes - if(hasConfig) { - config = *pConfig; - } - if(!node.inputConfig.getWaitForMessage() && config.getReusePreviousImage()) { - logger->warn("reusePreviousImage is only taken into account when inputConfig is synchronous"); + // if has new config, parse and check if any changes + if(hasConfig) { + config = *pConfig; + } + if(!node.inputConfig.getWaitForMessage() && config.getReusePreviousImage()) { + logger->warn("reusePreviousImage is only taken into account when inputConfig is synchronous"); + } } auto startP = std::chrono::steady_clock::now(); @@ -135,7 +139,11 @@ void loop(N& node, if(!success) { logger->error("Processing failed, potentially unsupported config"); } - node.out.send(outImage); + { + auto blockEvent = node.outputBlockEvent(); + + node.out.send(outImage); + } } else { logger->error( "Output image is bigger ({}B) than maximum frame size specified in properties ({}B) - skipping frame.\nPlease use the setMaxOutputFrameSize " diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index 318c2e809..eda5cb2cd 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -33,15 +32,23 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void setNodeId(int64_t id) override; - void addEvent(const std::string& source, PipelineEvent::Type type) override; - - void startEvent(const std::string& source, - std::optional queueSize = std::nullopt, - std::optional metadata = std::nullopt) override; // Start event with a start and an end - void endEvent(const std::string& source, - std::optional queueSize = std::nullopt, - std::optional metadata = std::nullopt) override; // Stop event with a start and an end - void pingEvent(const std::string& source) override; // Event where stop and start are the same (eg. loop) + void startEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize = std::nullopt) override; + void startInputEvent(const std::string& source, std::optional queueSize = std::nullopt) override; + void startOutputEvent(const std::string& source) override; + void startCustomEvent(const std::string& source) override; + void endEvent(PipelineEvent::Type type, + const std::string& source, + std::optional queueSize = std::nullopt) override; + void endInputEvent(const std::string& source, std::optional queueSize = std::nullopt) override; + void endOutputEvent(const std::string& source) override; + void endCustomEvent(const std::string& source) override; + void pingEvent(PipelineEvent::Type type, const std::string& source) override; + void pingMainLoopEvent() override; + void pingCustomEvent(const std::string& source) override; + BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source) override; + BlockPipelineEvent inputBlockEvent(const std::string& source = "defaultInputGroup") override; + BlockPipelineEvent outputBlockEvent(const std::string& source = "defaultOutputGroup") override; + BlockPipelineEvent customBlockEvent(const std::string& source) override; }; } // namespace utility diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index e4f6d9858..46c38003d 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -9,16 +9,38 @@ namespace utility { class PipelineEventDispatcherInterface { public: + class BlockPipelineEvent { + PipelineEventDispatcherInterface& dispatcher; + PipelineEvent::Type type; + std::string source; + + public: + BlockPipelineEvent(PipelineEventDispatcherInterface& dispatcher, PipelineEvent::Type type, const std::string& source) + : dispatcher(dispatcher), type(type), source(source) { + dispatcher.startEvent(type, source, std::nullopt); + } + ~BlockPipelineEvent() { + dispatcher.endEvent(type, source, std::nullopt); + } + }; + virtual ~PipelineEventDispatcherInterface() = default; virtual void setNodeId(int64_t id) = 0; - virtual void addEvent(const std::string& source, PipelineEvent::Type type) = 0; - virtual void startEvent(const std::string& source, - std::optional queueSize = std::nullopt, - std::optional metadata = std::nullopt) = 0; // Start event with a start and an end - virtual void endEvent(const std::string& source, - std::optional queueSize = std::nullopt, - std::optional metadata = std::nullopt) = 0; // Stop event with a start and an end - virtual void pingEvent(const std::string& source) = 0; // Event where stop and start are the same (eg. loop) + virtual void startEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize = std::nullopt) = 0; + virtual void startInputEvent(const std::string& source, std::optional queueSize = std::nullopt) = 0; + virtual void startOutputEvent(const std::string& source) = 0; + virtual void startCustomEvent(const std::string& source) = 0; + virtual void endEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize = std::nullopt) = 0; + virtual void endInputEvent(const std::string& source, std::optional queueSize = std::nullopt) = 0; + virtual void endOutputEvent(const std::string& source) = 0; + virtual void endCustomEvent(const std::string& source) = 0; + virtual void pingEvent(PipelineEvent::Type type, const std::string& source) = 0; + virtual void pingMainLoopEvent() = 0; + virtual void pingCustomEvent(const std::string& source) = 0; + virtual BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source) = 0; + virtual BlockPipelineEvent inputBlockEvent(const std::string& source = "defaultInputGroup") = 0; + virtual BlockPipelineEvent outputBlockEvent(const std::string& source = "defaultOutputGroup") = 0; + virtual BlockPipelineEvent customBlockEvent(const std::string& source); }; } // namespace utility diff --git a/src/pipeline/MessageQueue.cpp b/src/pipeline/MessageQueue.cpp index faee735db..eed60214e 100644 --- a/src/pipeline/MessageQueue.cpp +++ b/src/pipeline/MessageQueue.cpp @@ -22,11 +22,7 @@ MessageQueue::MessageQueue(std::string name, unsigned int maxSize, bool blocking, std::shared_ptr pipelineEventDispatcher) - : queue(maxSize, blocking), name(std::move(name)), pipelineEventDispatcher(pipelineEventDispatcher) { - if(pipelineEventDispatcher) { - pipelineEventDispatcher->addEvent(this->name, PipelineEvent::Type::INPUT); - } -} + : queue(maxSize, blocking), name(std::move(name)), pipelineEventDispatcher(pipelineEventDispatcher) {} MessageQueue::MessageQueue(unsigned int maxSize, bool blocking) : queue(maxSize, blocking) {} diff --git a/src/pipeline/Node.cpp b/src/pipeline/Node.cpp index cac6baa87..49490765d 100644 --- a/src/pipeline/Node.cpp +++ b/src/pipeline/Node.cpp @@ -241,11 +241,11 @@ void Node::Output::send(const std::shared_ptr& msg) { // } // } // } - if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(getName()); + if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getName()); for(auto& messageQueue : connectedInputs) { messageQueue->send(msg); } - if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(getName()); + if(pipelineEventDispatcher) pipelineEventDispatcher->endOutputEvent(getName()); } bool Node::Output::trySend(const std::shared_ptr& msg) { @@ -265,11 +265,11 @@ bool Node::Output::trySend(const std::shared_ptr& msg) { // } // } // } - if(pipelineEventDispatcher) pipelineEventDispatcher->startEvent(getName()); + if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getName()); for(auto& messageQueue : connectedInputs) { success &= messageQueue->trySend(msg); } - if(pipelineEventDispatcher) pipelineEventDispatcher->endEvent(getName()); + if(pipelineEventDispatcher && success) pipelineEventDispatcher->endOutputEvent(getName()); return success; } diff --git a/src/pipeline/ThreadedNode.cpp b/src/pipeline/ThreadedNode.cpp index 242f97db4..b86ad8783 100644 --- a/src/pipeline/ThreadedNode.cpp +++ b/src/pipeline/ThreadedNode.cpp @@ -2,6 +2,9 @@ #include +#include + +#include "depthai/utility/PipelineEventDispatcher.hpp" #include "pipeline/ThreadedNodeImpl.hpp" #include "utility/Environment.hpp" #include "utility/ErrorMacros.hpp" @@ -23,7 +26,6 @@ ThreadedNode::ThreadedNode() { void ThreadedNode::initPipelineEventDispatcher(int64_t nodeId) { pipelineEventDispatcher->setNodeId(nodeId); - pipelineEventDispatcher->addEvent("_mainLoop", PipelineEvent::Type::LOOP); } ThreadedNode::~ThreadedNode() = default; @@ -94,8 +96,20 @@ bool ThreadedNode::isRunning() const { } bool ThreadedNode::mainLoop() { - this->pipelineEventDispatcher->pingEvent("_mainLoop"); + this->pipelineEventDispatcher->pingMainLoopEvent(); return isRunning(); } +utility::PipelineEventDispatcherInterface::BlockPipelineEvent ThreadedNode::blockEvent(PipelineEvent::Type type, const std::string& source) { + return pipelineEventDispatcher->blockEvent(type, source); +} +utility::PipelineEventDispatcherInterface::BlockPipelineEvent ThreadedNode::inputBlockEvent(const std::string& source) { + // Just for convenience due to the default source + return blockEvent(PipelineEvent::Type::INPUT_BLOCK, source); +} +utility::PipelineEventDispatcherInterface::BlockPipelineEvent ThreadedNode::outputBlockEvent(const std::string& source) { + // Just for convenience due to the default source + return blockEvent(PipelineEvent::Type::OUTPUT_BLOCK, source); +} + } // namespace dai diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 6859e939e..749f46587 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -30,9 +30,10 @@ class NodeEventAggregation { : logger(logger), windowSize(windowSize), eventBatchSize(eventBatchSize), eventsBuffer(windowSize) {} NodeState state; utility::CircularBuffer eventsBuffer; - std::unordered_map>> timingsBufferByType; std::unordered_map>> inputTimingsBuffers; std::unordered_map>> outputTimingsBuffers; + std::unordered_map>> inputGroupTimingsBuffers; + std::unordered_map>> outputGroupTimingsBuffers; std::unique_ptr> mainLoopTimingsBuffer; std::unordered_map>> otherTimingsBuffers; @@ -40,10 +41,14 @@ class NodeEventAggregation { std::unordered_map>> inputFpsBuffers; std::unordered_map>> outputFpsBuffers; + std::unordered_map>> inputGroupFpsBuffers; + std::unordered_map>> outputGroupFpsBuffers; std::unordered_map>> otherFpsBuffers; std::unordered_map> ongoingInputEvents; std::unordered_map> ongoingOutputEvents; + std::unordered_map> ongoingInputGroupEvents; + std::unordered_map> ongoingOutputGroupEvents; std::optional ongoingMainLoopEvent; std::unordered_map> ongoingOtherEvents; @@ -65,6 +70,10 @@ class NodeEventAggregation { return ongoingOutputEvents[event.source]; case PipelineEvent::Type::CUSTOM: return ongoingOtherEvents[event.source]; + case PipelineEvent::Type::INPUT_GROUP: + return ongoingInputGroupEvents[event.source]; + case PipelineEvent::Type::OUTPUT_GROUP: + return ongoingOutputGroupEvents[event.source]; } return ongoingMainLoopEvent; // To silence compiler warning }(); @@ -87,6 +96,16 @@ class NodeEventAggregation { otherTimingsBuffers[event.source] = std::make_unique>(windowSize); } return otherTimingsBuffers[event.source]; + case PipelineEvent::Type::INPUT_GROUP: + if(inputGroupTimingsBuffers.find(event.source) == inputGroupTimingsBuffers.end()) { + inputGroupTimingsBuffers[event.source] = std::make_unique>(windowSize); + } + return inputGroupTimingsBuffers[event.source]; + case PipelineEvent::Type::OUTPUT_GROUP: + if(outputGroupTimingsBuffers.find(event.source) == outputGroupTimingsBuffers.end()) { + outputGroupTimingsBuffers[event.source] = std::make_unique>(windowSize); + } + return outputGroupTimingsBuffers[event.source]; } return emptyIntBuffer; // To silence compiler warning }(); @@ -109,6 +128,16 @@ class NodeEventAggregation { otherFpsBuffers[event.source] = std::make_unique>(windowSize); } return otherFpsBuffers[event.source]; + case PipelineEvent::Type::INPUT_GROUP: + if(inputGroupFpsBuffers.find(event.source) == inputGroupFpsBuffers.end()) { + inputGroupFpsBuffers[event.source] = std::make_unique>(windowSize); + } + return inputGroupFpsBuffers[event.source]; + case PipelineEvent::Type::OUTPUT_GROUP: + if(outputGroupFpsBuffers.find(event.source) == outputGroupFpsBuffers.end()) { + outputGroupFpsBuffers[event.source] = std::make_unique>(windowSize); + } + return outputGroupFpsBuffers[event.source]; } return emptyTimeBuffer; // To silence compiler warning }(); @@ -120,10 +149,6 @@ class NodeEventAggregation { eventsBuffer.add(durationEvent); state.events = eventsBuffer.getBuffer(); - if(timingsBufferByType.find(event.type) == timingsBufferByType.end()) { - timingsBufferByType[event.type] = std::make_unique>(windowSize); - } - timingsBufferByType[event.type]->add(durationEvent.durationUs); timingsBuffer->add(durationEvent.durationUs); fpsBuffer->add({durationEvent.startEvent.getTimestamp(), durationEvent.startEvent.getSequenceNum()}); @@ -132,6 +157,7 @@ class NodeEventAggregation { return true; } else { if(ongoingEvent.has_value()) { + // TODO: add ability to wait for multiple events (nn hailo threaded processing time) logger->warn("Ongoing event (seq {}) not finished before new one (seq {}) started. Event source: {}, node {}", ongoingEvent->sequenceNum, event.sequenceNum, @@ -159,6 +185,8 @@ class NodeEventAggregation { return ongoingOtherEvents[event.source]; case PipelineEvent::Type::INPUT: case PipelineEvent::Type::OUTPUT: + case PipelineEvent::Type::INPUT_GROUP: + case PipelineEvent::Type::OUTPUT_GROUP: throw std::runtime_error("INPUT and OUTPUT events should not be pings"); } return ongoingMainLoopEvent; // To silence compiler warning @@ -174,15 +202,14 @@ class NodeEventAggregation { return otherTimingsBuffers[event.source]; case PipelineEvent::Type::INPUT: case PipelineEvent::Type::OUTPUT: + case PipelineEvent::Type::INPUT_GROUP: + case PipelineEvent::Type::OUTPUT_GROUP: throw std::runtime_error("INPUT and OUTPUT events should not be pings"); } return emptyIntBuffer; // To silence compiler warning }(); auto& fpsBuffer = [&]() -> std::unique_ptr>& { switch(event.type) { - case PipelineEvent::Type::INPUT: - case PipelineEvent::Type::OUTPUT: - throw std::runtime_error("INPUT and OUTPUT events should not be pings"); case PipelineEvent::Type::LOOP: break; case PipelineEvent::Type::CUSTOM: @@ -190,6 +217,11 @@ class NodeEventAggregation { otherFpsBuffers[event.source] = std::make_unique>(windowSize); } return otherFpsBuffers[event.source]; + case PipelineEvent::Type::INPUT: + case PipelineEvent::Type::OUTPUT: + case PipelineEvent::Type::INPUT_GROUP: + case PipelineEvent::Type::OUTPUT_GROUP: + throw std::runtime_error("INPUT and OUTPUT events should not be pings"); } return emptyTimeBuffer; // To silence compiler warning }(); @@ -201,13 +233,9 @@ class NodeEventAggregation { eventsBuffer.add(durationEvent); state.events = eventsBuffer.getBuffer(); - if(timingsBufferByType.find(event.type) == timingsBufferByType.end()) { - timingsBufferByType[event.type] = std::make_unique>(windowSize); - } if(timingsBuffer == nullptr) { timingsBuffer = std::make_unique>(windowSize); } - timingsBufferByType[event.type]->add(durationEvent.durationUs); timingsBuffer->add(durationEvent.durationUs); if(fpsBuffer) fpsBuffer->add({durationEvent.startEvent.getTimestamp(), durationEvent.startEvent.getSequenceNum()}); @@ -285,9 +313,7 @@ class NodeEventAggregation { } else { addedEvent = updateIntervalBuffers(event); } - if(addedEvent && ++count % eventBatchSize == 0) { - // By type - updateTimingStats(state.timingsByType[event.type], *timingsBufferByType[event.type]); + if(addedEvent /* && ++count % eventBatchSize == 0 */) { // TODO // By instance switch(event.type) { case PipelineEvent::Type::CUSTOM: @@ -306,9 +332,17 @@ class NodeEventAggregation { updateTimingStats(state.outputStats[event.source], *outputTimingsBuffers[event.source]); updateFpsStats(state.outputStats[event.source], *outputFpsBuffers[event.source]); break; + case PipelineEvent::Type::INPUT_GROUP: + updateTimingStats(state.inputGroupStats[event.source], *inputGroupTimingsBuffers[event.source]); + updateFpsStats(state.inputGroupStats[event.source], *inputGroupFpsBuffers[event.source]); + break; + case PipelineEvent::Type::OUTPUT_GROUP: + updateTimingStats(state.outputGroupStats[event.source], *outputGroupTimingsBuffers[event.source]); + updateFpsStats(state.outputGroupStats[event.source], *outputGroupFpsBuffers[event.source]); + break; } } - if(event.type == PipelineEvent::Type::INPUT && event.interval == PipelineEvent::Interval::END && ++count % eventBatchSize == 0) { + if(event.type == PipelineEvent::Type::INPUT && event.interval == PipelineEvent::Interval::END /* && ++count % eventBatchSize == 0 */) { // TODO auto& qStats = state.inputStates[event.source].queueStats; auto& qBuffer = *inputQueueSizesBuffers[event.source]; qStats.maxQueued = std::max(qStats.maxQueued, *event.queueSize); @@ -339,8 +373,7 @@ class PipelineEventHandler { std::shared_mutex mutex; public: - PipelineEventHandler( - Node::InputMap* inputs, uint32_t aggregationWindowSize, uint32_t eventBatchSize, std::shared_ptr logger) + PipelineEventHandler(Node::InputMap* inputs, uint32_t aggregationWindowSize, uint32_t eventBatchSize, std::shared_ptr logger) : inputs(inputs), aggregationWindowSize(aggregationWindowSize), eventBatchSize(eventBatchSize), logger(logger) {} void threadedRun() { while(running) { diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index 33bff7a04..bc19992f8 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -1,11 +1,33 @@ #include "depthai/utility/PipelineEventDispatcher.hpp" #include -#include namespace dai { namespace utility { +std::string typeToString(PipelineEvent::Type type) { + switch(type) { + case PipelineEvent::Type::CUSTOM: + return "CUSTOM"; + case PipelineEvent::Type::LOOP: + return "LOOP"; + case PipelineEvent::Type::INPUT: + return "INPUT"; + case PipelineEvent::Type::OUTPUT: + return "OUTPUT"; + case PipelineEvent::Type::INPUT_BLOCK: + return "INPUT_BLOCK"; + case PipelineEvent::Type::OUTPUT_BLOCK: + return "OUTPUT_BLOCK"; + default: + return "UNKNOWN"; + } +} + +std::string makeKey(PipelineEvent::Type type, const std::string& source) { + return typeToString(type) + "#" + source; +} + void PipelineEventDispatcher::checkNodeId() { if(nodeId == -1) { throw std::runtime_error("Node ID not set on PipelineEventDispatcher"); @@ -14,24 +36,10 @@ void PipelineEventDispatcher::checkNodeId() { void PipelineEventDispatcher::setNodeId(int64_t id) { nodeId = id; } -void PipelineEventDispatcher::addEvent(const std::string& source, PipelineEvent::Type type) { - if(!source.empty()) { - if(events.find(source) != events.end()) { - throw std::runtime_error("Event with name '" + source + "' already exists"); - } - PipelineEvent event; - event.type = type; - event.source = source; - events[source] = {event, false}; - } -} -void PipelineEventDispatcher::startEvent(const std::string& source, std::optional queueSize, std::optional metadata) { +void PipelineEventDispatcher::startEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize) { // TODO add mutex checkNodeId(); - if(events.find(source) == events.end()) { - throw std::runtime_error("Event with name " + source + " does not exist"); - } - auto& event = events[source]; + auto& event = events[makeKey(type, source)]; if(event.ongoing) { throw std::runtime_error("Event with name " + source + " is already ongoing"); } @@ -39,25 +47,30 @@ void PipelineEventDispatcher::startEvent(const std::string& source, std::optiona event.event.tsDevice = event.event.ts; ++event.event.sequenceNum; event.event.nodeId = nodeId; - event.event.metadata = std::move(metadata); event.event.queueSize = std::move(queueSize); event.event.interval = PipelineEvent::Interval::START; // type and source are already set event.ongoing = true; if(out) { - out->send(std::make_shared(event.event)); + out->send(std::make_shared(event)); } } -void PipelineEventDispatcher::endEvent(const std::string& source, std::optional queueSize, std::optional metadata) { +void PipelineEventDispatcher::startInputEvent(const std::string& source, std::optional queueSize) { + startEvent(PipelineEvent::Type::INPUT, source, std::move(queueSize)); +} +void PipelineEventDispatcher::startOutputEvent(const std::string& source) { + startEvent(PipelineEvent::Type::OUTPUT, source, std::nullopt); +} +void PipelineEventDispatcher::startCustomEvent(const std::string& source) { + startEvent(PipelineEvent::Type::CUSTOM, source, std::nullopt); +} +void PipelineEventDispatcher::endEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize) { // TODO add mutex checkNodeId(); auto now = std::chrono::steady_clock::now(); - if(events.find(source) == events.end()) { - throw std::runtime_error("Event with name " + source + " does not exist"); - } - auto& event = events[source]; + auto& event = events[makeKey(type, source)]; if(!event.ongoing) { throw std::runtime_error("Event with name " + source + " has not been started"); } @@ -65,7 +78,6 @@ void PipelineEventDispatcher::endEvent(const std::string& source, std::optional< event.event.setTimestamp(now); event.event.tsDevice = event.event.ts; event.event.nodeId = nodeId; - event.event.metadata = std::move(metadata); event.event.queueSize = std::move(queueSize); event.event.interval = PipelineEvent::Interval::END; // type and source are already set @@ -75,18 +87,23 @@ void PipelineEventDispatcher::endEvent(const std::string& source, std::optional< out->send(std::make_shared(event.event)); } - event.event.metadata = std::nullopt; event.event.queueSize = std::nullopt; } -void PipelineEventDispatcher::pingEvent(const std::string& source) { +void PipelineEventDispatcher::endInputEvent(const std::string& source, std::optional queueSize) { + endEvent(PipelineEvent::Type::INPUT, source, std::move(queueSize)); +} +void PipelineEventDispatcher::endOutputEvent(const std::string& source) { + endEvent(PipelineEvent::Type::OUTPUT, source, std::nullopt); +} +void PipelineEventDispatcher::endCustomEvent(const std::string& source) { + endEvent(PipelineEvent::Type::CUSTOM, source, std::nullopt); +} +void PipelineEventDispatcher::pingEvent(PipelineEvent::Type type, const std::string& source) { // TODO add mutex checkNodeId(); auto now = std::chrono::steady_clock::now(); - if(events.find(source) == events.end()) { - throw std::runtime_error("Event with name " + source + " does not exist"); - } - auto& event = events[source]; + auto& event = events[makeKey(type, source)]; if(event.ongoing) { throw std::runtime_error("Event with name " + source + " is already ongoing"); } @@ -101,6 +118,27 @@ void PipelineEventDispatcher::pingEvent(const std::string& source) { out->send(std::make_shared(event.event)); } } +void PipelineEventDispatcher::pingMainLoopEvent() { + pingEvent(PipelineEvent::Type::LOOP, "_mainLoop"); +} +void PipelineEventDispatcher::pingCustomEvent(const std::string& source) { + pingEvent(PipelineEvent::Type::CUSTOM, source); +} +PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::blockEvent(PipelineEvent::Type type, const std::string& source) { + return BlockPipelineEvent(*this, type, source); +} +PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::inputBlockEvent(const std::string& source) { + // For convenience due to the default source + return blockEvent(PipelineEvent::Type::INPUT_BLOCK, source); +} +PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::outputBlockEvent(const std::string& source) { + // For convenience due to the default source + return blockEvent(PipelineEvent::Type::OUTPUT_BLOCK, source); +} +PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::customBlockEvent(const std::string& source) { + // For convenience due to the default source + return blockEvent(PipelineEvent::Type::CUSTOM, source); +} } // namespace utility } // namespace dai From 36e917aa712dae394fbb43f4a8bf42c5148badd1 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 8 Oct 2025 13:57:17 +0200 Subject: [PATCH 023/124] Do not require adding events, node / input states, other improvements --- .../datatype/PipelineEventBindings.cpp | 3 +- .../datatype/PipelineStateBindings.cpp | 52 ++++-- .../PipelineEventDispatcherBindings.cpp | 10 +- include/depthai/pipeline/Node.hpp | 3 - include/depthai/pipeline/Pipeline.hpp | 8 +- .../pipeline/datatype/PipelineState.hpp | 2 +- include/depthai/utility/LockingQueue.hpp | 28 ++- .../utility/PipelineEventDispatcher.hpp | 1 + .../PipelineEventDispatcherInterface.hpp | 3 +- src/pipeline/MessageQueue.cpp | 30 +++- .../internal/PipelineEventAggregation.cpp | 170 +++++++++--------- src/utility/PipelineEventDispatcher.cpp | 34 +++- 12 files changed, 226 insertions(+), 118 deletions(-) diff --git a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp index f2d028be6..5920810db 100644 --- a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp @@ -45,8 +45,7 @@ void bind_pipelineevent(pybind11::module& m, void* pCallstack) { pipelineEvent.def(py::init<>()) .def("__repr__", &PipelineEvent::str) .def_readwrite("nodeId", &PipelineEvent::nodeId, DOC(dai, PipelineEvent, nodeId)) - .def_readwrite("metadata", &PipelineEvent::metadata, DOC(dai, PipelineEvent, metadata)) - .def_readwrite("queueSize", &PipelineEvent::metadata, DOC(dai, PipelineEvent, metadata)) + .def_readwrite("queueSize", &PipelineEvent::queueSize, DOC(dai, PipelineEvent, queueSize)) .def_readwrite("interval", &PipelineEvent::interval, DOC(dai, PipelineEvent, interval)) .def_readwrite("type", &PipelineEvent::type, DOC(dai, PipelineEvent, type)) .def_readwrite("source", &PipelineEvent::source, DOC(dai, PipelineEvent, source)) diff --git a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp index 9f40eaf72..0a22611f4 100644 --- a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp @@ -1,8 +1,6 @@ #include -#include #include "DatatypeBindings.hpp" -#include "pipeline/CommonBindings.hpp" // depthai #include "depthai/pipeline/datatype/PipelineState.hpp" @@ -19,10 +17,25 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { py::class_ nodeState(m, "NodeState", DOC(dai, NodeState)); py::class_ durationEvent(nodeState, "DurationEvent", DOC(dai, NodeState, DurationEvent)); py::class_ nodeStateTimingStats(nodeState, "TimingStats", DOC(dai, NodeState, TimingStats)); + py::class_ nodeStateTiming(nodeState, "Timing", DOC(dai, NodeState, Timing)); py::class_ nodeStateQueueStats(nodeState, "QueueStats", DOC(dai, NodeState, QueueStats)); - py::class_ nodeStateQueueState(nodeState, "QueueState", DOC(dai, NodeState, QueueState)); + py::class_ nodeStateInputQueueState(nodeState, "InputQueueState", DOC(dai, NodeState, InputQueueState)); + py::class_ nodeStateOutputQueueState(nodeState, "OutputQueueState", DOC(dai, NodeState, OutputQueueState)); py::class_, Buffer, std::shared_ptr> pipelineState(m, "PipelineState", DOC(dai, PipelineState)); + py::enum_(nodeStateInputQueueState, "State", DOC(dai, NodeState, InputQueueState, State)) + .value("IDLE", NodeState::InputQueueState::State::IDLE) + .value("WAITING", NodeState::InputQueueState::State::WAITING) + .value("BLOCKED", NodeState::InputQueueState::State::BLOCKED); + py::enum_(nodeStateOutputQueueState, "State", DOC(dai, NodeState, OutputQueueState, State)) + .value("IDLE", NodeState::OutputQueueState::State::IDLE) + .value("SENDING", NodeState::OutputQueueState::State::SENDING); + py::enum_(nodeState, "State", DOC(dai, NodeState, State)) + .value("IDLE", NodeState::State::IDLE) + .value("GETTING_INPUTS", NodeState::State::GETTING_INPUTS) + .value("PROCESSING", NodeState::State::PROCESSING) + .value("SENDING_OUTPUTS", NodeState::State::SENDING_OUTPUTS); + /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// @@ -52,6 +65,11 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { .def_readwrite("maxMicrosRecent", &NodeState::TimingStats::maxMicrosRecent, DOC(dai, NodeState, TimingStats, maxMicrosRecent)) .def_readwrite("medianMicrosRecent", &NodeState::TimingStats::medianMicrosRecent, DOC(dai, NodeState, TimingStats, medianMicrosRecent)); + nodeStateTiming.def(py::init<>()) + .def("__repr__", &NodeState::Timing::str) + .def_readwrite("fps", &NodeState::Timing::fps, DOC(dai, NodeState, Timing, fps)) + .def_readwrite("durationStats", &NodeState::Timing::durationStats, DOC(dai, NodeState, Timing, durationStats)); + nodeStateQueueStats.def(py::init<>()) .def("__repr__", &NodeState::QueueStats::str) .def_readwrite("maxQueued", &NodeState::QueueStats::maxQueued, DOC(dai, NodeState, QueueStats, maxQueued)) @@ -59,22 +77,28 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { .def_readwrite("maxQueuedRecent", &NodeState::QueueStats::maxQueuedRecent, DOC(dai, NodeState, QueueStats, maxQueuedRecent)) .def_readwrite("medianQueuedRecent", &NodeState::QueueStats::medianQueuedRecent, DOC(dai, NodeState, QueueStats, medianQueuedRecent)); - nodeStateQueueState.def(py::init<>()) - .def("__repr__", &NodeState::QueueState::str) - .def_readwrite("waiting", &NodeState::QueueState::waiting, DOC(dai, NodeState, QueueState, waiting)) - .def_readwrite("numQueued", &NodeState::QueueState::numQueued, DOC(dai, NodeState, QueueState, numQueued)) - .def_readwrite("timingStats", &NodeState::QueueState::timingStats, DOC(dai, NodeState, QueueState, timingStats)) - .def_readwrite("queueStats", &NodeState::QueueState::queueStats, DOC(dai, NodeState, QueueState, queueStats)); + nodeStateInputQueueState.def(py::init<>()) + .def("__repr__", &NodeState::InputQueueState::str) + .def_readwrite("state", &NodeState::InputQueueState::state, DOC(dai, NodeState, InputQueueState, state)) + .def_readwrite("numQueued", &NodeState::InputQueueState::numQueued, DOC(dai, NodeState, InputQueueState, numQueued)) + .def_readwrite("timing", &NodeState::InputQueueState::timing, DOC(dai, NodeState, InputQueueState, timing)) + .def_readwrite("queueStats", &NodeState::InputQueueState::queueStats, DOC(dai, NodeState, InputQueueState, queueStats)); + + nodeStateOutputQueueState.def(py::init<>()) + .def("__repr__", &NodeState::OutputQueueState::str) + .def_readwrite("state", &NodeState::OutputQueueState::state, DOC(dai, NodeState, OutputQueueState, state)) + .def_readwrite("timing", &NodeState::OutputQueueState::timing, DOC(dai, NodeState, OutputQueueState, timing)); nodeState.def(py::init<>()) .def("__repr__", &NodeState::str) + .def_readwrite("state", &NodeState::state, DOC(dai, NodeState, state)) .def_readwrite("events", &NodeState::events, DOC(dai, NodeState, events)) .def_readwrite("inputStates", &NodeState::inputStates, DOC(dai, NodeState, inputStates)) - .def_readwrite("outputStats", &NodeState::outputStats, DOC(dai, NodeState, outputStats)) - .def_readwrite("inputGroupStats", &NodeState::inputGroupStats, DOC(dai, NodeState, inputGroupStats)) - .def_readwrite("outputGroupStats", &NodeState::outputGroupStats, DOC(dai, NodeState, outputGroupStats)) - .def_readwrite("mainLoopStats", &NodeState::mainLoopStats, DOC(dai, NodeState, mainLoopStats)) - .def_readwrite("otherStats", &NodeState::otherStats, DOC(dai, NodeState, otherStats)); + .def_readwrite("outputStates", &NodeState::outputStates, DOC(dai, NodeState, outputStates)) + .def_readwrite("inputsGetTiming", &NodeState::inputsGetTiming, DOC(dai, NodeState, inputsGetTiming)) + .def_readwrite("outputsSendTiming", &NodeState::outputsSendTiming, DOC(dai, NodeState, outputsSendTiming)) + .def_readwrite("mainLoopTiming", &NodeState::mainLoopTiming, DOC(dai, NodeState, mainLoopTiming)) + .def_readwrite("otherTimings", &NodeState::otherTimings, DOC(dai, NodeState, otherTimings)); // Message pipelineState.def(py::init<>()) diff --git a/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp index 7ce1fd934..5fccc0505 100644 --- a/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp +++ b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp @@ -21,8 +21,10 @@ void PipelineEventDispatcherBindings::bind(pybind11::module& m, void* pCallstack pipelineEventDispatcher .def(py::init(), py::arg("output")) .def("setNodeId", &PipelineEventDispatcher::setNodeId, py::arg("id"), DOC(dai, utility, PipelineEventDispatcher, setNodeId)) - .def("addEvent", &PipelineEventDispatcher::addEvent, py::arg("source"), py::arg("type"), DOC(dai, utility, PipelineEventDispatcher, addEvent)) - .def("startEvent", &PipelineEventDispatcher::startEvent, py::arg("source"), py::arg("queueSize") = std::nullopt, py::arg("metadata") = std::nullopt, DOC(dai, utility, PipelineEventDispatcher, startEvent)) - .def("endEvent", &PipelineEventDispatcher::endEvent, py::arg("source"), py::arg("queueSize") = std::nullopt, py::arg("metadata") = std::nullopt, DOC(dai, utility, PipelineEventDispatcher, endEvent)) - .def("pingEvent", &PipelineEventDispatcher::pingEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, pingEvent)); + .def("startCustomEvent", &PipelineEventDispatcher::startCustomEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, startCustomEvent)) + .def("endCustomEvent", &PipelineEventDispatcher::endCustomEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, endCustomEvent)) + .def("pingCustomEvent", &PipelineEventDispatcher::pingCustomEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, pingCustomEvent)) + .def("inputBlockEvent", &PipelineEventDispatcher::inputBlockEvent, py::arg("source") = "defaultInputGroup", DOC(dai, utility, PipelineEventDispatcher, inputBlockEvent)) + .def("outputBlockEvent", &PipelineEventDispatcher::outputBlockEvent, py::arg("source") = "defaultOutputGroup", DOC(dai, utility, PipelineEventDispatcher, outputBlockEvent)) + .def("customBlockEvent", &PipelineEventDispatcher::customBlockEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, customBlockEvent)); } diff --git a/include/depthai/pipeline/Node.hpp b/include/depthai/pipeline/Node.hpp index d8f1833ed..85d826a3f 100644 --- a/include/depthai/pipeline/Node.hpp +++ b/include/depthai/pipeline/Node.hpp @@ -145,9 +145,6 @@ class Node : public std::enable_shared_from_this { if(getName().empty()) { setName(par.createUniqueOutputName()); } - if(pipelineEventDispatcher && getName() != "pipelineEventOutput") { - pipelineEventDispatcher->addEvent(getName(), PipelineEvent::Type::OUTPUT); - } } Node& getParent() { diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index 1739a7907..30d1a209a 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -106,7 +106,7 @@ class NodeStateApi> { // TODO send and get return {}; } - std::unordered_map> inputs() { + std::unordered_map> inputs() { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); @@ -154,7 +154,7 @@ class NodeStateApi { std::unordered_map outputs() { return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).outputs()[nodeId]; } - std::unordered_map inputs() { + std::unordered_map inputs() { return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).inputs()[nodeId]; } std::unordered_map otherStats() { @@ -196,7 +196,7 @@ class NodeStateApi { // TODO send and get return {}; } - std::unordered_map inputs(const std::vector& inputNames) { + std::unordered_map inputs(const std::vector& inputNames) { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); @@ -208,7 +208,7 @@ class NodeStateApi { // TODO send and get return {}; } - NodeState::QueueState inputs(const std::string& inputName) { + NodeState::InputQueueState inputs(const std::string& inputName) { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index e30833c92..ef0d66851 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -52,7 +52,7 @@ class NodeState { struct OutputQueueState { enum class State : std::int32_t { IDLE = 0, - BLOCKED = 1 + SENDING = 1 } state = State::IDLE; Timing timing; DEPTHAI_SERIALIZE(OutputQueueState, state, timing); diff --git a/include/depthai/utility/LockingQueue.hpp b/include/depthai/utility/LockingQueue.hpp index f4a859396..54b076153 100644 --- a/include/depthai/utility/LockingQueue.hpp +++ b/include/depthai/utility/LockingQueue.hpp @@ -18,6 +18,8 @@ namespace dai { // Mutex& operator=(Mutex&&) = delete; // }; +enum class LockingQueueState { OK, BLOCKED, CANCELLED }; + template class LockingQueue { public: @@ -149,7 +151,7 @@ class LockingQueue { return true; } - bool push(T const& data) { + bool push(T const& data, std::function callback = [](LockingQueueState) {}) { { std::unique_lock lock(guard); if(maxSize == 0) { @@ -166,6 +168,9 @@ class LockingQueue { queue.pop(); } } else { + if(queue.size() >= maxSize) { + callback(LockingQueueState::BLOCKED); + } signalPop.wait(lock, [this]() { return queue.size() < maxSize || destructed; }); if(destructed) return false; } @@ -176,7 +181,7 @@ class LockingQueue { return true; } - bool push(T&& data) { + bool push(T&& data, std::function callback = [](LockingQueueState) {}) { { std::unique_lock lock(guard); if(maxSize == 0) { @@ -193,6 +198,9 @@ class LockingQueue { queue.pop(); } } else { + if(queue.size() >= maxSize) { + callback(LockingQueueState::BLOCKED); + } signalPop.wait(lock, [this]() { return queue.size() < maxSize || destructed; }); if(destructed) return false; } @@ -204,7 +212,7 @@ class LockingQueue { } template - bool tryWaitAndPush(T const& data, std::chrono::duration timeout) { + bool tryWaitAndPush(T const& data, std::chrono::duration timeout, std::function callback = [](LockingQueueState) {}) { { std::unique_lock lock(guard); if(maxSize == 0) { @@ -221,8 +229,14 @@ class LockingQueue { queue.pop(); } } else { + if(queue.size() >= maxSize) { + callback(LockingQueueState::BLOCKED); + } // First checks predicate, then waits bool pred = signalPop.wait_for(lock, timeout, [this]() { return queue.size() < maxSize || destructed; }); + if(!pred) { + callback(LockingQueueState::CANCELLED); + } if(!pred) return false; if(destructed) return false; } @@ -234,7 +248,7 @@ class LockingQueue { } template - bool tryWaitAndPush(T&& data, std::chrono::duration timeout) { + bool tryWaitAndPush(T&& data, std::chrono::duration timeout, std::function callback = [](LockingQueueState) {}) { { std::unique_lock lock(guard); if(maxSize == 0) { @@ -252,7 +266,13 @@ class LockingQueue { } } else { // First checks predicate, then waits + if(queue.size() >= maxSize) { + callback(LockingQueueState::BLOCKED); + } bool pred = signalPop.wait_for(lock, timeout, [this]() { return queue.size() < maxSize || destructed; }); + if(!pred) { + callback(LockingQueueState::CANCELLED); + } if(!pred) return false; if(destructed) return false; } diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index eda5cb2cd..1ac2fe8a9 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -45,6 +45,7 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void pingEvent(PipelineEvent::Type type, const std::string& source) override; void pingMainLoopEvent() override; void pingCustomEvent(const std::string& source) override; + void pingInputEvent(const std::string& source, std::optional queueSize = std::nullopt) override; BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source) override; BlockPipelineEvent inputBlockEvent(const std::string& source = "defaultInputGroup") override; BlockPipelineEvent outputBlockEvent(const std::string& source = "defaultOutputGroup") override; diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index 46c38003d..bf2f1d970 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -37,10 +37,11 @@ class PipelineEventDispatcherInterface { virtual void pingEvent(PipelineEvent::Type type, const std::string& source) = 0; virtual void pingMainLoopEvent() = 0; virtual void pingCustomEvent(const std::string& source) = 0; + virtual void pingInputEvent(const std::string& source, std::optional queueSize = std::nullopt) = 0; virtual BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source) = 0; virtual BlockPipelineEvent inputBlockEvent(const std::string& source = "defaultInputGroup") = 0; virtual BlockPipelineEvent outputBlockEvent(const std::string& source = "defaultOutputGroup") = 0; - virtual BlockPipelineEvent customBlockEvent(const std::string& source); + virtual BlockPipelineEvent customBlockEvent(const std::string& source) = 0; }; } // namespace utility diff --git a/src/pipeline/MessageQueue.cpp b/src/pipeline/MessageQueue.cpp index eed60214e..409f4da61 100644 --- a/src/pipeline/MessageQueue.cpp +++ b/src/pipeline/MessageQueue.cpp @@ -117,7 +117,20 @@ void MessageQueue::send(const std::shared_ptr& msg) { throw QueueException(CLOSED_QUEUE_MESSAGE); } callCallbacks(msg); - auto queueNotClosed = queue.push(msg); + auto queueNotClosed = queue.push(msg, [&](LockingQueueState state) { + if(pipelineEventDispatcher) { + switch(state) { + case LockingQueueState::BLOCKED: + pipelineEventDispatcher->pingInputEvent(name, -1); + break; + case LockingQueueState::CANCELLED: + pipelineEventDispatcher->pingInputEvent(name, -2); + break; + case LockingQueueState::OK: + break; + } + } + }); if(!queueNotClosed) throw QueueException(CLOSED_QUEUE_MESSAGE); } @@ -127,7 +140,20 @@ bool MessageQueue::send(const std::shared_ptr& msg, std::chrono::mill if(queue.isDestroyed()) { throw QueueException(CLOSED_QUEUE_MESSAGE); } - return queue.tryWaitAndPush(msg, timeout); + return queue.tryWaitAndPush(msg, timeout, [&](LockingQueueState state) { + if(pipelineEventDispatcher) { + switch(state) { + case LockingQueueState::BLOCKED: + pipelineEventDispatcher->pingInputEvent(name, -1); + break; + case LockingQueueState::CANCELLED: + pipelineEventDispatcher->pingInputEvent(name, -2); + break; + case LockingQueueState::OK: + break; + } + } + }); } bool MessageQueue::trySend(const std::shared_ptr& msg) { diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 749f46587..2372e572f 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -32,8 +32,8 @@ class NodeEventAggregation { utility::CircularBuffer eventsBuffer; std::unordered_map>> inputTimingsBuffers; std::unordered_map>> outputTimingsBuffers; - std::unordered_map>> inputGroupTimingsBuffers; - std::unordered_map>> outputGroupTimingsBuffers; + std::unique_ptr> inputsGetTimingsBuffer; + std::unique_ptr> outputsSendTimingsBuffer; std::unique_ptr> mainLoopTimingsBuffer; std::unordered_map>> otherTimingsBuffers; @@ -41,14 +41,14 @@ class NodeEventAggregation { std::unordered_map>> inputFpsBuffers; std::unordered_map>> outputFpsBuffers; - std::unordered_map>> inputGroupFpsBuffers; - std::unordered_map>> outputGroupFpsBuffers; + std::unique_ptr> inputsGetFpsBuffer; + std::unique_ptr> outputsSendFpsBuffer; std::unordered_map>> otherFpsBuffers; std::unordered_map> ongoingInputEvents; std::unordered_map> ongoingOutputEvents; - std::unordered_map> ongoingInputGroupEvents; - std::unordered_map> ongoingOutputGroupEvents; + std::optional ongoingGetInputsEvent; + std::optional ongoingSendOutputsEvent; std::optional ongoingMainLoopEvent; std::unordered_map> ongoingOtherEvents; @@ -70,10 +70,10 @@ class NodeEventAggregation { return ongoingOutputEvents[event.source]; case PipelineEvent::Type::CUSTOM: return ongoingOtherEvents[event.source]; - case PipelineEvent::Type::INPUT_GROUP: - return ongoingInputGroupEvents[event.source]; - case PipelineEvent::Type::OUTPUT_GROUP: - return ongoingOutputGroupEvents[event.source]; + case PipelineEvent::Type::INPUT_BLOCK: + return ongoingGetInputsEvent; + case PipelineEvent::Type::OUTPUT_BLOCK: + return ongoingSendOutputsEvent; } return ongoingMainLoopEvent; // To silence compiler warning }(); @@ -82,30 +82,15 @@ class NodeEventAggregation { case PipelineEvent::Type::LOOP: throw std::runtime_error("LOOP event should not be an interval"); case PipelineEvent::Type::INPUT: - if(inputTimingsBuffers.find(event.source) == inputTimingsBuffers.end()) { - inputTimingsBuffers[event.source] = std::make_unique>(windowSize); - } return inputTimingsBuffers[event.source]; case PipelineEvent::Type::OUTPUT: - if(outputTimingsBuffers.find(event.source) == outputTimingsBuffers.end()) { - outputTimingsBuffers[event.source] = std::make_unique>(windowSize); - } return outputTimingsBuffers[event.source]; case PipelineEvent::Type::CUSTOM: - if(otherTimingsBuffers.find(event.source) == otherTimingsBuffers.end()) { - otherTimingsBuffers[event.source] = std::make_unique>(windowSize); - } return otherTimingsBuffers[event.source]; - case PipelineEvent::Type::INPUT_GROUP: - if(inputGroupTimingsBuffers.find(event.source) == inputGroupTimingsBuffers.end()) { - inputGroupTimingsBuffers[event.source] = std::make_unique>(windowSize); - } - return inputGroupTimingsBuffers[event.source]; - case PipelineEvent::Type::OUTPUT_GROUP: - if(outputGroupTimingsBuffers.find(event.source) == outputGroupTimingsBuffers.end()) { - outputGroupTimingsBuffers[event.source] = std::make_unique>(windowSize); - } - return outputGroupTimingsBuffers[event.source]; + case PipelineEvent::Type::INPUT_BLOCK: + return inputsGetTimingsBuffer; + case PipelineEvent::Type::OUTPUT_BLOCK: + return outputsSendTimingsBuffer; } return emptyIntBuffer; // To silence compiler warning }(); @@ -114,33 +99,22 @@ class NodeEventAggregation { case PipelineEvent::Type::LOOP: throw std::runtime_error("LOOP event should not be an interval"); case PipelineEvent::Type::INPUT: - if(inputFpsBuffers.find(event.source) == inputFpsBuffers.end()) { - inputFpsBuffers[event.source] = std::make_unique>(windowSize); - } return inputFpsBuffers[event.source]; case PipelineEvent::Type::OUTPUT: - if(outputFpsBuffers.find(event.source) == outputFpsBuffers.end()) { - outputFpsBuffers[event.source] = std::make_unique>(windowSize); - } return outputFpsBuffers[event.source]; case PipelineEvent::Type::CUSTOM: - if(otherFpsBuffers.find(event.source) == otherFpsBuffers.end()) { - otherFpsBuffers[event.source] = std::make_unique>(windowSize); - } return otherFpsBuffers[event.source]; - case PipelineEvent::Type::INPUT_GROUP: - if(inputGroupFpsBuffers.find(event.source) == inputGroupFpsBuffers.end()) { - inputGroupFpsBuffers[event.source] = std::make_unique>(windowSize); - } - return inputGroupFpsBuffers[event.source]; - case PipelineEvent::Type::OUTPUT_GROUP: - if(outputGroupFpsBuffers.find(event.source) == outputGroupFpsBuffers.end()) { - outputGroupFpsBuffers[event.source] = std::make_unique>(windowSize); - } - return outputGroupFpsBuffers[event.source]; + case PipelineEvent::Type::INPUT_BLOCK: + return inputsGetFpsBuffer; + case PipelineEvent::Type::OUTPUT_BLOCK: + return outputsSendFpsBuffer; } return emptyTimeBuffer; // To silence compiler warning }(); + + if(timingsBuffer == nullptr) timingsBuffer = std::make_unique>(windowSize); + if(fpsBuffer == nullptr) fpsBuffer = std::make_unique>(windowSize); + if(ongoingEvent.has_value() && ongoingEvent->sequenceNum == event.sequenceNum && event.interval == PipelineEvent::Interval::END) { // End event NodeState::DurationEvent durationEvent; @@ -185,8 +159,8 @@ class NodeEventAggregation { return ongoingOtherEvents[event.source]; case PipelineEvent::Type::INPUT: case PipelineEvent::Type::OUTPUT: - case PipelineEvent::Type::INPUT_GROUP: - case PipelineEvent::Type::OUTPUT_GROUP: + case PipelineEvent::Type::INPUT_BLOCK: + case PipelineEvent::Type::OUTPUT_BLOCK: throw std::runtime_error("INPUT and OUTPUT events should not be pings"); } return ongoingMainLoopEvent; // To silence compiler warning @@ -196,14 +170,11 @@ class NodeEventAggregation { case PipelineEvent::Type::LOOP: return mainLoopTimingsBuffer; case PipelineEvent::Type::CUSTOM: - if(otherTimingsBuffers.find(event.source) == otherTimingsBuffers.end()) { - otherTimingsBuffers[event.source] = std::make_unique>(windowSize); - } return otherTimingsBuffers[event.source]; case PipelineEvent::Type::INPUT: case PipelineEvent::Type::OUTPUT: - case PipelineEvent::Type::INPUT_GROUP: - case PipelineEvent::Type::OUTPUT_GROUP: + case PipelineEvent::Type::INPUT_BLOCK: + case PipelineEvent::Type::OUTPUT_BLOCK: throw std::runtime_error("INPUT and OUTPUT events should not be pings"); } return emptyIntBuffer; // To silence compiler warning @@ -213,18 +184,19 @@ class NodeEventAggregation { case PipelineEvent::Type::LOOP: break; case PipelineEvent::Type::CUSTOM: - if(otherFpsBuffers.find(event.source) == otherFpsBuffers.end()) { - otherFpsBuffers[event.source] = std::make_unique>(windowSize); - } return otherFpsBuffers[event.source]; case PipelineEvent::Type::INPUT: case PipelineEvent::Type::OUTPUT: - case PipelineEvent::Type::INPUT_GROUP: - case PipelineEvent::Type::OUTPUT_GROUP: + case PipelineEvent::Type::INPUT_BLOCK: + case PipelineEvent::Type::OUTPUT_BLOCK: throw std::runtime_error("INPUT and OUTPUT events should not be pings"); } return emptyTimeBuffer; // To silence compiler warning }(); + + if(timingsBuffer == nullptr) timingsBuffer = std::make_unique>(windowSize); + if(fpsBuffer == nullptr) fpsBuffer = std::make_unique>(windowSize); + if(ongoingEvent.has_value() && ongoingEvent->sequenceNum == event.sequenceNum - 1) { // End event NodeState::DurationEvent durationEvent; @@ -233,9 +205,6 @@ class NodeEventAggregation { eventsBuffer.add(durationEvent); state.events = eventsBuffer.getBuffer(); - if(timingsBuffer == nullptr) { - timingsBuffer = std::make_unique>(windowSize); - } timingsBuffer->add(durationEvent.durationUs); if(fpsBuffer) fpsBuffer->add({durationEvent.startEvent.getTimestamp(), durationEvent.startEvent.getSequenceNum()}); @@ -285,12 +254,12 @@ class NodeEventAggregation { stats.medianMicrosRecent = (stats.medianMicrosRecent + bufferByType[bufferByType.size() / 2 - 1]) / 2; } } - inline void updateFpsStats(NodeState::TimingStats& stats, const utility::CircularBuffer& buffer) { + inline void updateFpsStats(NodeState::Timing& timing, const utility::CircularBuffer& buffer) { if(buffer.size() < 2) return; auto timeDiff = std::chrono::duration_cast(buffer.last().time - buffer.first().time).count(); auto frameDiff = buffer.last().sequenceNum - buffer.first().sequenceNum; if(timeDiff > 0 && buffer.last().sequenceNum > buffer.first().sequenceNum) { - stats.fps = frameDiff * (1e6f / (float)timeDiff); + timing.fps = frameDiff * (1e6f / (float)timeDiff); } } @@ -307,38 +276,77 @@ class NodeEventAggregation { throw std::runtime_error(fmt::format("INPUT END event must have queue size set source: {}, node {}", event.source, event.nodeId)); } } + // Update states + switch(event.type) { + case PipelineEvent::Type::CUSTOM: + case PipelineEvent::Type::LOOP: + break; + case PipelineEvent::Type::INPUT: + state.inputStates[event.source].numQueued = *event.queueSize; + switch(event.interval) { + case PipelineEvent::Interval::START: + state.inputStates[event.source].state = NodeState::InputQueueState::State::WAITING; + break; + case PipelineEvent::Interval::END: + state.inputStates[event.source].state = NodeState::InputQueueState::State::IDLE; + break; + case PipelineEvent::Interval::NONE: + if(event.queueSize.has_value() && (event.queueSize == -1 || event.queueSize == -2)) + state.inputStates[event.source].state = NodeState::InputQueueState::State::BLOCKED; + break; + } + break; + case PipelineEvent::Type::OUTPUT: + if(event.interval == PipelineEvent::Interval::START) + state.outputStates[event.source].state = NodeState::OutputQueueState::State::SENDING; + else if(event.interval == PipelineEvent::Interval::END) + state.outputStates[event.source].state = NodeState::OutputQueueState::State::IDLE; + break; + case PipelineEvent::Type::INPUT_BLOCK: + if(event.interval == PipelineEvent::Interval::START) + state.state = NodeState::State::GETTING_INPUTS; + else if(event.interval == PipelineEvent::Interval::END) + state.state = NodeState::State::PROCESSING; + break; + case PipelineEvent::Type::OUTPUT_BLOCK: + if(event.interval == PipelineEvent::Interval::START) + state.state = NodeState::State::SENDING_OUTPUTS; + else if(event.interval == PipelineEvent::Interval::END) + state.state = NodeState::State::PROCESSING; + break; + } bool addedEvent = false; - if(event.interval == PipelineEvent::Interval::NONE) { + if(event.interval == PipelineEvent::Interval::NONE && event.type != PipelineEvent::Type::INPUT && event.type != PipelineEvent::Type::OUTPUT) { addedEvent = updatePingBuffers(event); - } else { + } else if(event.interval != PipelineEvent::Interval::NONE) { addedEvent = updateIntervalBuffers(event); } if(addedEvent /* && ++count % eventBatchSize == 0 */) { // TODO // By instance switch(event.type) { case PipelineEvent::Type::CUSTOM: - updateTimingStats(state.otherStats[event.source], *otherTimingsBuffers[event.source]); - updateFpsStats(state.otherStats[event.source], *otherFpsBuffers[event.source]); + updateTimingStats(state.otherTimings[event.source].durationStats, *otherTimingsBuffers[event.source]); + updateFpsStats(state.otherTimings[event.source], *otherFpsBuffers[event.source]); break; case PipelineEvent::Type::LOOP: - updateTimingStats(state.mainLoopStats, *mainLoopTimingsBuffer); - state.mainLoopStats.fps = 1e6f / (float)state.mainLoopStats.averageMicrosRecent; + updateTimingStats(state.mainLoopTiming.durationStats, *mainLoopTimingsBuffer); + state.mainLoopTiming.fps = 1e6f / (float)state.mainLoopTiming.durationStats.averageMicrosRecent; break; case PipelineEvent::Type::INPUT: - updateTimingStats(state.inputStates[event.source].timingStats, *inputTimingsBuffers[event.source]); - updateFpsStats(state.inputStates[event.source].timingStats, *inputFpsBuffers[event.source]); + updateTimingStats(state.inputStates[event.source].timing.durationStats, *inputTimingsBuffers[event.source]); + updateFpsStats(state.inputStates[event.source].timing, *inputFpsBuffers[event.source]); break; case PipelineEvent::Type::OUTPUT: - updateTimingStats(state.outputStats[event.source], *outputTimingsBuffers[event.source]); - updateFpsStats(state.outputStats[event.source], *outputFpsBuffers[event.source]); + updateTimingStats(state.outputStates[event.source].timing.durationStats, *outputTimingsBuffers[event.source]); + updateFpsStats(state.outputStates[event.source].timing, *outputFpsBuffers[event.source]); break; - case PipelineEvent::Type::INPUT_GROUP: - updateTimingStats(state.inputGroupStats[event.source], *inputGroupTimingsBuffers[event.source]); - updateFpsStats(state.inputGroupStats[event.source], *inputGroupFpsBuffers[event.source]); + case PipelineEvent::Type::INPUT_BLOCK: + updateTimingStats(state.inputsGetTiming.durationStats, *inputsGetTimingsBuffer); + updateFpsStats(state.inputsGetTiming, *inputsGetFpsBuffer); break; - case PipelineEvent::Type::OUTPUT_GROUP: - updateTimingStats(state.outputGroupStats[event.source], *outputGroupTimingsBuffers[event.source]); - updateFpsStats(state.outputGroupStats[event.source], *outputGroupFpsBuffers[event.source]); + case PipelineEvent::Type::OUTPUT_BLOCK: + updateTimingStats(state.outputsSendTiming.durationStats, *outputsSendTimingsBuffer); + updateFpsStats(state.outputsSendTiming, *outputsSendFpsBuffer); break; } } diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index bc19992f8..0393c26c6 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -23,11 +23,15 @@ std::string typeToString(PipelineEvent::Type type) { return "UNKNOWN"; } } - std::string makeKey(PipelineEvent::Type type, const std::string& source) { return typeToString(type) + "#" + source; } +bool blacklist(PipelineEvent::Type type, const std::string& source) { + if(type == PipelineEvent::Type::OUTPUT && source == "pipelineEventOutput") return true; + return false; +} + void PipelineEventDispatcher::checkNodeId() { if(nodeId == -1) { throw std::runtime_error("Node ID not set on PipelineEventDispatcher"); @@ -39,6 +43,8 @@ void PipelineEventDispatcher::setNodeId(int64_t id) { void PipelineEventDispatcher::startEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize) { // TODO add mutex checkNodeId(); + if(blacklist(type, source)) return; + auto& event = events[makeKey(type, source)]; if(event.ongoing) { throw std::runtime_error("Event with name " + source + " is already ongoing"); @@ -53,7 +59,7 @@ void PipelineEventDispatcher::startEvent(PipelineEvent::Type type, const std::st event.ongoing = true; if(out) { - out->send(std::make_shared(event)); + out->send(std::make_shared(event.event)); } } void PipelineEventDispatcher::startInputEvent(const std::string& source, std::optional queueSize) { @@ -68,6 +74,8 @@ void PipelineEventDispatcher::startCustomEvent(const std::string& source) { void PipelineEventDispatcher::endEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize) { // TODO add mutex checkNodeId(); + if(blacklist(type, source)) return; + auto now = std::chrono::steady_clock::now(); auto& event = events[makeKey(type, source)]; @@ -101,6 +109,8 @@ void PipelineEventDispatcher::endCustomEvent(const std::string& source) { void PipelineEventDispatcher::pingEvent(PipelineEvent::Type type, const std::string& source) { // TODO add mutex checkNodeId(); + if(blacklist(type, source)) return; + auto now = std::chrono::steady_clock::now(); auto& event = events[makeKey(type, source)]; @@ -124,6 +134,26 @@ void PipelineEventDispatcher::pingMainLoopEvent() { void PipelineEventDispatcher::pingCustomEvent(const std::string& source) { pingEvent(PipelineEvent::Type::CUSTOM, source); } +void PipelineEventDispatcher::pingInputEvent(const std::string& source, std::optional queueSize) { + // TODO add mutex + checkNodeId(); + if(blacklist(PipelineEvent::Type::INPUT, source)) return; + + auto now = std::chrono::steady_clock::now(); + + auto& event = events[makeKey(PipelineEvent::Type::INPUT, source)]; + PipelineEvent eventCopy = event.event; + eventCopy.setTimestamp(now); + eventCopy.tsDevice = eventCopy.ts; + eventCopy.nodeId = nodeId; + eventCopy.queueSize = std::move(queueSize); + eventCopy.interval = PipelineEvent::Interval::NONE; + // type and source are already set + + if(out) { + out->send(std::make_shared(eventCopy)); + } +} PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::blockEvent(PipelineEvent::Type type, const std::string& source) { return BlockPipelineEvent(*this, type, source); } From 48c1b6a92f807932dd93b78fcd311f3529cf8585 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 8 Oct 2025 16:39:13 +0200 Subject: [PATCH 024/124] Add ability to get the entire pipeline schema, add bridge info to pipeline schema --- include/depthai/pipeline/NodeObjInfo.hpp | 3 +- include/depthai/pipeline/Pipeline.hpp | 11 +- include/depthai/pipeline/PipelineSchema.hpp | 3 +- src/pipeline/Pipeline.cpp | 197 ++++++++++-------- .../internal/PipelineEventAggregation.cpp | 2 +- 5 files changed, 128 insertions(+), 88 deletions(-) diff --git a/include/depthai/pipeline/NodeObjInfo.hpp b/include/depthai/pipeline/NodeObjInfo.hpp index da1c87a0d..c786065dd 100644 --- a/include/depthai/pipeline/NodeObjInfo.hpp +++ b/include/depthai/pipeline/NodeObjInfo.hpp @@ -15,6 +15,7 @@ struct NodeObjInfo { std::string name; std::string alias; + std::string device; std::vector properties; @@ -27,6 +28,6 @@ struct NodeObjInfo { std::unordered_map, NodeIoInfo, IoInfoKey> ioInfo; }; -DEPTHAI_SERIALIZE_EXT(NodeObjInfo, id, parentId, name, alias, properties, logLevel, ioInfo); +DEPTHAI_SERIALIZE_EXT(NodeObjInfo, id, parentId, name, alias, device, properties, logLevel, ioInfo); } // namespace dai diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index 30d1a209a..480729b7c 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -251,7 +251,9 @@ class PipelineStateApi { std::vector nodeIds; // empty means all nodes public: - PipelineStateApi(std::shared_ptr pipelineStateOut, std::shared_ptr pipelineStateRequest, const std::vector>& allNodes) + PipelineStateApi(std::shared_ptr pipelineStateOut, + std::shared_ptr pipelineStateRequest, + const std::vector>& allNodes) : pipelineStateOut(std::move(pipelineStateOut)), pipelineStateRequest(std::move(pipelineStateRequest)) { for(const auto& n : allNodes) { nodeIds.push_back(n->id); @@ -294,6 +296,7 @@ class PipelineImpl : public std::enable_shared_from_this { // Functions Node::Id getNextUniqueId(); PipelineSchema getPipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE) const; + PipelineSchema getDevicePipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE) const; Device::Config getDeviceConfig() const; void setCameraTuningBlobPath(const fs::path& path); void setXLinkChunkSize(int sizeBytes); @@ -341,6 +344,7 @@ class PipelineImpl : public std::enable_shared_from_this { // using NodeMap = std::unordered_map>; // NodeMap nodeMap; std::vector> nodes; + std::vector> xlinkBridges; // TODO(themarpe) - refactor, connections are now carried by nodes instead using NodeConnectionMap = std::unordered_map>; @@ -539,6 +543,11 @@ class Pipeline { */ PipelineSchema getPipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE) const; + /** + * @returns Device pipeline schema (without host only nodes and connections) + */ + PipelineSchema getDevicePipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE) const; + // void loadAssets(AssetManager& assetManager); void serialize(PipelineSchema& schema, Assets& assets, std::vector& assetStorage) const { impl()->serialize(schema, assets, assetStorage); diff --git a/include/depthai/pipeline/PipelineSchema.hpp b/include/depthai/pipeline/PipelineSchema.hpp index b8cd497d0..a9fee1ada 100644 --- a/include/depthai/pipeline/PipelineSchema.hpp +++ b/include/depthai/pipeline/PipelineSchema.hpp @@ -14,8 +14,9 @@ struct PipelineSchema { std::vector connections; GlobalProperties globalProperties; std::unordered_map nodes; + std::vector> bridges; }; -DEPTHAI_SERIALIZE_EXT(PipelineSchema, connections, globalProperties, nodes); +DEPTHAI_SERIALIZE_EXT(PipelineSchema, connections, globalProperties, nodes, bridges); } // namespace dai diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 737cea306..9ed64bdee 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -74,6 +74,10 @@ PipelineSchema Pipeline::getPipelineSchema(SerializationType type) const { return pimpl->getPipelineSchema(type); } +PipelineSchema Pipeline::getDevicePipelineSchema(SerializationType type) const { + return pimpl->getDevicePipelineSchema(type); +} + GlobalProperties PipelineImpl::getGlobalProperties() const { return globalProperties; } @@ -117,7 +121,7 @@ std::vector> PipelineImpl::getSourceNodes() { void PipelineImpl::serialize(PipelineSchema& schema, Assets& assets, std::vector& assetStorage, SerializationType type) const { // Set schema - schema = getPipelineSchema(type); + schema = getDevicePipelineSchema(type); // Serialize all asset managers into asset storage assetStorage.clear(); @@ -188,6 +192,7 @@ std::vector PipelineImpl::getConnections() const { PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type) const { PipelineSchema schema; schema.globalProperties = globalProperties; + schema.bridges = xlinkBridges; int latestIoId = 0; // Loop over all nodes, and add them to schema for(const auto& node : getAllNodes()) { @@ -195,94 +200,91 @@ PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type) const { if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) { continue; } - // Check if its a host node or device node - if(node->runOnHost()) { - // host node, no need to serialize to a schema - // TBD any additional changes - } else { - // Create 'node' info - NodeObjInfo info; - info.id = node->id; - info.name = node->getName(); - info.alias = node->getAlias(); - info.parentId = node->parentId; - const auto& deviceNode = std::dynamic_pointer_cast(node); - if(!deviceNode) { - throw std::invalid_argument(fmt::format("Node '{}' should subclass DeviceNode or have hostNode == true", info.name)); - } + // Create 'node' info + NodeObjInfo info; + info.id = node->id; + info.name = node->getName(); + info.alias = node->getAlias(); + info.parentId = node->parentId; + info.device = node->runOnHost() ? "host" : "device"; + const auto& deviceNode = std::dynamic_pointer_cast(node); + if(!node->runOnHost() && !deviceNode) { + throw std::invalid_argument(fmt::format("Node '{}' should subclass DeviceNode or have hostNode == true", info.name)); + } + if(deviceNode) { deviceNode->getProperties().serialize(info.properties, type); info.logLevel = deviceNode->getLogLevel(); - // Create Io information - auto inputs = node->getInputs(); - auto outputs = node->getOutputs(); - - info.ioInfo.reserve(inputs.size() + outputs.size()); - - // Add inputs - for(const auto& input : inputs) { - NodeIoInfo io; - io.id = latestIoId; - latestIoId++; - io.blocking = input.getBlocking(); - io.queueSize = input.getMaxSize(); - io.name = input.getName(); - io.group = input.getGroup(); - auto ioKey = std::make_tuple(io.group, io.name); - - io.waitForMessage = input.getWaitForMessage(); - switch(input.getType()) { - case Node::Input::Type::MReceiver: - io.type = NodeIoInfo::Type::MReceiver; - break; - case Node::Input::Type::SReceiver: - io.type = NodeIoInfo::Type::SReceiver; - break; - } + } + // Create Io information + auto inputs = node->getInputs(); + auto outputs = node->getOutputs(); + + info.ioInfo.reserve(inputs.size() + outputs.size()); + + // Add inputs + for(const auto& input : inputs) { + NodeIoInfo io; + io.id = latestIoId; + latestIoId++; + io.blocking = input.getBlocking(); + io.queueSize = input.getMaxSize(); + io.name = input.getName(); + io.group = input.getGroup(); + auto ioKey = std::make_tuple(io.group, io.name); + + io.waitForMessage = input.getWaitForMessage(); + switch(input.getType()) { + case Node::Input::Type::MReceiver: + io.type = NodeIoInfo::Type::MReceiver; + break; + case Node::Input::Type::SReceiver: + io.type = NodeIoInfo::Type::SReceiver; + break; + } - if(info.ioInfo.count(ioKey) > 0) { - if(io.group == "") { - throw std::invalid_argument(fmt::format("'{}.{}' redefined. Inputs and outputs must have unique names", info.name, io.name)); - } else { - throw std::invalid_argument( - fmt::format("'{}.{}[\"{}\"]' redefined. Inputs and outputs must have unique names", info.name, io.group, io.name)); - } + if(info.ioInfo.count(ioKey) > 0) { + if(io.group == "") { + throw std::invalid_argument(fmt::format("'{}.{}' redefined. Inputs and outputs must have unique names", info.name, io.name)); + } else { + throw std::invalid_argument( + fmt::format("'{}.{}[\"{}\"]' redefined. Inputs and outputs must have unique names", info.name, io.group, io.name)); } - info.ioInfo[ioKey] = io; } + info.ioInfo[ioKey] = io; + } - // Add outputs - for(const auto& output : outputs) { - NodeIoInfo io; - io.id = latestIoId; - latestIoId++; - io.blocking = false; - io.name = output.getName(); - io.group = output.getGroup(); - auto ioKey = std::make_tuple(io.group, io.name); - - switch(output.getType()) { - case Node::Output::Type::MSender: - io.type = NodeIoInfo::Type::MSender; - break; - case Node::Output::Type::SSender: - io.type = NodeIoInfo::Type::SSender; - break; - } + // Add outputs + for(const auto& output : outputs) { + NodeIoInfo io; + io.id = latestIoId; + latestIoId++; + io.blocking = false; + io.name = output.getName(); + io.group = output.getGroup(); + auto ioKey = std::make_tuple(io.group, io.name); + + switch(output.getType()) { + case Node::Output::Type::MSender: + io.type = NodeIoInfo::Type::MSender; + break; + case Node::Output::Type::SSender: + io.type = NodeIoInfo::Type::SSender; + break; + } - if(info.ioInfo.count(ioKey) > 0) { - if(io.group == "") { - throw std::invalid_argument(fmt::format("'{}.{}' redefined. Inputs and outputs must have unique names", info.name, io.name)); - } else { - throw std::invalid_argument( - fmt::format("'{}.{}[\"{}\"]' redefined. Inputs and outputs must have unique names", info.name, io.group, io.name)); - } + if(info.ioInfo.count(ioKey) > 0) { + if(io.group == "") { + throw std::invalid_argument(fmt::format("'{}.{}' redefined. Inputs and outputs must have unique names", info.name, io.name)); + } else { + throw std::invalid_argument( + fmt::format("'{}.{}[\"{}\"]' redefined. Inputs and outputs must have unique names", info.name, io.group, io.name)); } - info.ioInfo[ioKey] = io; } - - // At the end, add the constructed node information to the schema - schema.nodes[info.id] = info; + info.ioInfo[ioKey] = io; } + + // At the end, add the constructed node information to the schema + schema.nodes[info.id] = info; } // Create 'connections' info @@ -314,11 +316,6 @@ PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type) const { bool outputHost = outNode->runOnHost(); bool inputHost = inNode->runOnHost(); - if(outputHost && inputHost) { - // skip - connection between host nodes doesn't have to be represented to the device - continue; - } - if(outputHost && !inputHost) { throw std::invalid_argument( fmt::format("Connection from host node '{}' to device node '{}' is not allowed during serialization.", outNode->getName(), inNode->getName())); @@ -336,6 +333,35 @@ PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type) const { return schema; } +PipelineSchema PipelineImpl::getDevicePipelineSchema(SerializationType type) const { + auto schema = getPipelineSchema(type); + // Remove bridge info + schema.bridges.clear(); + // Remove host nodes + for(auto it = schema.nodes.begin(); it != schema.nodes.end();) { + if(it->second.device != "device") { + it = schema.nodes.erase(it); + } else { + ++it; + } + } + // Remove connections between host nodes (host - device connections should not exist) + schema.connections.erase(std::remove_if(schema.connections.begin(), + schema.connections.end(), + [&schema](const NodeConnectionSchema& c) { + auto node1 = schema.nodes.find(c.node1Id); + auto node2 = schema.nodes.find(c.node2Id); + if(node1 == schema.nodes.end() && node2 == schema.nodes.end()) { + return true; + } else if(node1 == schema.nodes.end() || node2 == schema.nodes.end()) { + throw std::invalid_argument("Connection from host node to device node should not exist here"); + } + return false; + }), + schema.connections.end()); + return schema; +} + Device::Config PipelineImpl::getDeviceConfig() const { Device::Config config; config.board = board; @@ -759,6 +785,9 @@ void PipelineImpl::build() { xLinkBridge.xLinkInHost->setStreamName(streamName); xLinkBridge.xLinkInHost->setConnection(defaultDevice->getConnection()); connection.out->link(xLinkBridge.xLinkOut->input); + + // Note the created bridge for serialization (for visualization) + xlinkBridges.push_back({outNode->id, inNode->id}); } auto xLinkBridge = bridgesOut[connection.out]; connection.out->unlink(*connection.in); // Unlink the connection diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 2372e572f..439694004 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -131,7 +131,7 @@ class NodeEventAggregation { return true; } else { if(ongoingEvent.has_value()) { - // TODO: add ability to wait for multiple events (nn hailo threaded processing time) + // TODO: add ability to wait for multiple events (nn hailo threaded processing time - events with custom ids for tracking) logger->warn("Ongoing event (seq {}) not finished before new one (seq {}) started. Event source: {}, node {}", ongoingEvent->sequenceNum, event.sequenceNum, From c8ec85a41417d776a3c3c57ef03d1e3a299da514 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 8 Oct 2025 16:52:56 +0200 Subject: [PATCH 025/124] Add comments to pipeline state --- .../pipeline/datatype/PipelineState.hpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index ef0d66851..658db2215 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -39,21 +39,28 @@ class NodeState { DEPTHAI_SERIALIZE(QueueStats, maxQueued, minQueuedRecent, maxQueuedRecent, medianQueuedRecent); }; struct InputQueueState { + // Current state of the input queue. enum class State : std::int32_t { IDLE = 0, - WAITING = 1, - BLOCKED = 2 + WAITING = 1, // Waiting to receive a message + BLOCKED = 2 // An output attempted to send to this input, but the input queue was full } state = State::IDLE; + // Number of messages currently queued in the input queue uint32_t numQueued; + // Timing info about this input Timing timing; + // Queue usage stats QueueStats queueStats; DEPTHAI_SERIALIZE(InputQueueState, state, numQueued, timing); }; struct OutputQueueState { + // Current state of the output queue. Send should ideally be instant. This is not the case when the input queue is full. + // In that case, the state will be SENDING until there is space in the input queue (unless trySend is used). enum class State : std::int32_t { IDLE = 0, SENDING = 1 } state = State::IDLE; + // Timing info about this output Timing timing; DEPTHAI_SERIALIZE(OutputQueueState, state, timing); }; @@ -64,13 +71,21 @@ class NodeState { SENDING_OUTPUTS = 3 }; + // Current state of the node - idle only when not running State state = State::IDLE; + // Optional list of recent events std::vector events; + // Info about each output std::unordered_map outputStates; + // Info about each input std::unordered_map inputStates; + // Time spent getting inputs in a loop Timing inputsGetTiming; + // Time spent sending outputs in a loop Timing outputsSendTiming; + // Main node loop timing (processing time + inputs get + outputs send) Timing mainLoopTiming; + // Other timings that the developer of the node decided to add std::unordered_map otherTimings; DEPTHAI_SERIALIZE(NodeState, events, outputStates, inputStates, inputsGetTiming, outputsSendTiming, mainLoopTiming, otherTimings); From 1a2b848d210590b260909563c87709534acef357 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 9 Oct 2025 09:26:08 +0200 Subject: [PATCH 026/124] Bugfix --- .../node/internal/PipelineEventAggregation.cpp | 10 ++++------ src/utility/PipelineEventDispatcher.cpp | 15 ++++++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 439694004..e88f082cd 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -129,7 +129,7 @@ class NodeEventAggregation { ongoingEvent = std::nullopt; return true; - } else { + } else if(event.interval == PipelineEvent::Interval::START) { if(ongoingEvent.has_value()) { // TODO: add ability to wait for multiple events (nn hailo threaded processing time - events with custom ids for tracking) logger->warn("Ongoing event (seq {}) not finished before new one (seq {}) started. Event source: {}, node {}", @@ -138,12 +138,10 @@ class NodeEventAggregation { ongoingEvent->source, event.nodeId); } - if(event.interval == PipelineEvent::Interval::START) { - // Start event - ongoingEvent = event; - } - return false; + // Start event + ongoingEvent = event; } + return false; } inline bool updatePingBuffers(PipelineEvent& event) { diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index 0393c26c6..502fd15ff 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -46,16 +46,14 @@ void PipelineEventDispatcher::startEvent(PipelineEvent::Type type, const std::st if(blacklist(type, source)) return; auto& event = events[makeKey(type, source)]; - if(event.ongoing) { - throw std::runtime_error("Event with name " + source + " is already ongoing"); - } event.event.setTimestamp(std::chrono::steady_clock::now()); event.event.tsDevice = event.event.ts; ++event.event.sequenceNum; event.event.nodeId = nodeId; event.event.queueSize = std::move(queueSize); event.event.interval = PipelineEvent::Interval::START; - // type and source are already set + event.event.type = type; + event.event.source = source; event.ongoing = true; if(out) { @@ -88,7 +86,8 @@ void PipelineEventDispatcher::endEvent(PipelineEvent::Type type, const std::stri event.event.nodeId = nodeId; event.event.queueSize = std::move(queueSize); event.event.interval = PipelineEvent::Interval::END; - // type and source are already set + event.event.type = type; + event.event.source = source; event.ongoing = false; if(out) { @@ -122,7 +121,8 @@ void PipelineEventDispatcher::pingEvent(PipelineEvent::Type type, const std::str ++event.event.sequenceNum; event.event.nodeId = nodeId; event.event.interval = PipelineEvent::Interval::NONE; - // type and source are already set + event.event.type = type; + event.event.source = source; if(out) { out->send(std::make_shared(event.event)); @@ -148,7 +148,8 @@ void PipelineEventDispatcher::pingInputEvent(const std::string& source, std::opt eventCopy.nodeId = nodeId; eventCopy.queueSize = std::move(queueSize); eventCopy.interval = PipelineEvent::Interval::NONE; - // type and source are already set + eventCopy.type = PipelineEvent::Type::INPUT; + eventCopy.source = source; if(out) { out->send(std::make_shared(eventCopy)); From 5a6d7652188cae7f6c3ab32d4db21d48466c70e5 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 9 Oct 2025 10:54:37 +0200 Subject: [PATCH 027/124] Update input queue size more often --- .../datatype/PipelineEventBindings.cpp | 1 + .../pipeline/datatype/PipelineEvent.hpp | 3 ++- include/depthai/utility/LockingQueue.hpp | 20 +++++++++---------- .../utility/PipelineEventDispatcher.hpp | 2 +- .../PipelineEventDispatcherInterface.hpp | 2 +- src/pipeline/MessageQueue.cpp | 12 +++++------ .../internal/PipelineEventAggregation.cpp | 5 ++--- src/utility/PipelineEventDispatcher.cpp | 3 ++- 8 files changed, 25 insertions(+), 23 deletions(-) diff --git a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp index 5920810db..f3437db59 100644 --- a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp @@ -45,6 +45,7 @@ void bind_pipelineevent(pybind11::module& m, void* pCallstack) { pipelineEvent.def(py::init<>()) .def("__repr__", &PipelineEvent::str) .def_readwrite("nodeId", &PipelineEvent::nodeId, DOC(dai, PipelineEvent, nodeId)) + .def_readwrite("status", &PipelineEvent::status, DOC(dai, PipelineEvent, status)) .def_readwrite("queueSize", &PipelineEvent::queueSize, DOC(dai, PipelineEvent, queueSize)) .def_readwrite("interval", &PipelineEvent::interval, DOC(dai, PipelineEvent, interval)) .def_readwrite("type", &PipelineEvent::type, DOC(dai, PipelineEvent, type)) diff --git a/include/depthai/pipeline/datatype/PipelineEvent.hpp b/include/depthai/pipeline/datatype/PipelineEvent.hpp index b0c17f215..1ececed60 100644 --- a/include/depthai/pipeline/datatype/PipelineEvent.hpp +++ b/include/depthai/pipeline/datatype/PipelineEvent.hpp @@ -30,6 +30,7 @@ class PipelineEvent : public Buffer { virtual ~PipelineEvent() = default; int64_t nodeId = -1; + int32_t status = 0; std::optional queueSize; Interval interval = Interval::NONE; Type type = Type::CUSTOM; @@ -40,7 +41,7 @@ class PipelineEvent : public Buffer { datatype = DatatypeEnum::PipelineEvent; }; - DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeId, interval, type, source); + DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeId, status, interval, type, source); }; } // namespace dai diff --git a/include/depthai/utility/LockingQueue.hpp b/include/depthai/utility/LockingQueue.hpp index 54b076153..40db11abf 100644 --- a/include/depthai/utility/LockingQueue.hpp +++ b/include/depthai/utility/LockingQueue.hpp @@ -151,7 +151,7 @@ class LockingQueue { return true; } - bool push(T const& data, std::function callback = [](LockingQueueState) {}) { + bool push(T const& data, std::function callback = [](LockingQueueState, size_t) {}) { { std::unique_lock lock(guard); if(maxSize == 0) { @@ -169,7 +169,7 @@ class LockingQueue { } } else { if(queue.size() >= maxSize) { - callback(LockingQueueState::BLOCKED); + callback(LockingQueueState::BLOCKED, queue.size()); } signalPop.wait(lock, [this]() { return queue.size() < maxSize || destructed; }); if(destructed) return false; @@ -181,7 +181,7 @@ class LockingQueue { return true; } - bool push(T&& data, std::function callback = [](LockingQueueState) {}) { + bool push(T&& data, std::function callback = [](LockingQueueState, size_t) {}) { { std::unique_lock lock(guard); if(maxSize == 0) { @@ -199,7 +199,7 @@ class LockingQueue { } } else { if(queue.size() >= maxSize) { - callback(LockingQueueState::BLOCKED); + callback(LockingQueueState::BLOCKED, queue.size()); } signalPop.wait(lock, [this]() { return queue.size() < maxSize || destructed; }); if(destructed) return false; @@ -212,7 +212,7 @@ class LockingQueue { } template - bool tryWaitAndPush(T const& data, std::chrono::duration timeout, std::function callback = [](LockingQueueState) {}) { + bool tryWaitAndPush(T const& data, std::chrono::duration timeout, std::function callback = [](LockingQueueState, size_t) {}) { { std::unique_lock lock(guard); if(maxSize == 0) { @@ -230,12 +230,12 @@ class LockingQueue { } } else { if(queue.size() >= maxSize) { - callback(LockingQueueState::BLOCKED); + callback(LockingQueueState::BLOCKED, queue.size()); } // First checks predicate, then waits bool pred = signalPop.wait_for(lock, timeout, [this]() { return queue.size() < maxSize || destructed; }); if(!pred) { - callback(LockingQueueState::CANCELLED); + callback(LockingQueueState::CANCELLED, queue.size()); } if(!pred) return false; if(destructed) return false; @@ -248,7 +248,7 @@ class LockingQueue { } template - bool tryWaitAndPush(T&& data, std::chrono::duration timeout, std::function callback = [](LockingQueueState) {}) { + bool tryWaitAndPush(T&& data, std::chrono::duration timeout, std::function callback = [](LockingQueueState, size_t) {}) { { std::unique_lock lock(guard); if(maxSize == 0) { @@ -267,11 +267,11 @@ class LockingQueue { } else { // First checks predicate, then waits if(queue.size() >= maxSize) { - callback(LockingQueueState::BLOCKED); + callback(LockingQueueState::BLOCKED, queue.size()); } bool pred = signalPop.wait_for(lock, timeout, [this]() { return queue.size() < maxSize || destructed; }); if(!pred) { - callback(LockingQueueState::CANCELLED); + callback(LockingQueueState::CANCELLED, queue.size()); } if(!pred) return false; if(destructed) return false; diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index 1ac2fe8a9..3590b0967 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -45,7 +45,7 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void pingEvent(PipelineEvent::Type type, const std::string& source) override; void pingMainLoopEvent() override; void pingCustomEvent(const std::string& source) override; - void pingInputEvent(const std::string& source, std::optional queueSize = std::nullopt) override; + void pingInputEvent(const std::string& source, int32_t status, std::optional queueSize = std::nullopt) override; BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source) override; BlockPipelineEvent inputBlockEvent(const std::string& source = "defaultInputGroup") override; BlockPipelineEvent outputBlockEvent(const std::string& source = "defaultOutputGroup") override; diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index bf2f1d970..9fe66b16f 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -37,7 +37,7 @@ class PipelineEventDispatcherInterface { virtual void pingEvent(PipelineEvent::Type type, const std::string& source) = 0; virtual void pingMainLoopEvent() = 0; virtual void pingCustomEvent(const std::string& source) = 0; - virtual void pingInputEvent(const std::string& source, std::optional queueSize = std::nullopt) = 0; + virtual void pingInputEvent(const std::string& source, int32_t status, std::optional queueSize = std::nullopt) = 0; virtual BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source) = 0; virtual BlockPipelineEvent inputBlockEvent(const std::string& source = "defaultInputGroup") = 0; virtual BlockPipelineEvent outputBlockEvent(const std::string& source = "defaultOutputGroup") = 0; diff --git a/src/pipeline/MessageQueue.cpp b/src/pipeline/MessageQueue.cpp index 409f4da61..4e906bb71 100644 --- a/src/pipeline/MessageQueue.cpp +++ b/src/pipeline/MessageQueue.cpp @@ -117,14 +117,14 @@ void MessageQueue::send(const std::shared_ptr& msg) { throw QueueException(CLOSED_QUEUE_MESSAGE); } callCallbacks(msg); - auto queueNotClosed = queue.push(msg, [&](LockingQueueState state) { + auto queueNotClosed = queue.push(msg, [&](LockingQueueState state, size_t size) { if(pipelineEventDispatcher) { switch(state) { case LockingQueueState::BLOCKED: - pipelineEventDispatcher->pingInputEvent(name, -1); + pipelineEventDispatcher->pingInputEvent(name, -1, size); break; case LockingQueueState::CANCELLED: - pipelineEventDispatcher->pingInputEvent(name, -2); + pipelineEventDispatcher->pingInputEvent(name, -2, size); break; case LockingQueueState::OK: break; @@ -140,14 +140,14 @@ bool MessageQueue::send(const std::shared_ptr& msg, std::chrono::mill if(queue.isDestroyed()) { throw QueueException(CLOSED_QUEUE_MESSAGE); } - return queue.tryWaitAndPush(msg, timeout, [&](LockingQueueState state) { + return queue.tryWaitAndPush(msg, timeout, [&](LockingQueueState state, size_t size) { if(pipelineEventDispatcher) { switch(state) { case LockingQueueState::BLOCKED: - pipelineEventDispatcher->pingInputEvent(name, -1); + pipelineEventDispatcher->pingInputEvent(name, -1, size); break; case LockingQueueState::CANCELLED: - pipelineEventDispatcher->pingInputEvent(name, -2); + pipelineEventDispatcher->pingInputEvent(name, -2, size); break; case LockingQueueState::OK: break; diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index e88f082cd..4b22f3a30 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -280,7 +280,7 @@ class NodeEventAggregation { case PipelineEvent::Type::LOOP: break; case PipelineEvent::Type::INPUT: - state.inputStates[event.source].numQueued = *event.queueSize; + if(event.queueSize.has_value()) state.inputStates[event.source].numQueued = *event.queueSize; switch(event.interval) { case PipelineEvent::Interval::START: state.inputStates[event.source].state = NodeState::InputQueueState::State::WAITING; @@ -289,8 +289,7 @@ class NodeEventAggregation { state.inputStates[event.source].state = NodeState::InputQueueState::State::IDLE; break; case PipelineEvent::Interval::NONE: - if(event.queueSize.has_value() && (event.queueSize == -1 || event.queueSize == -2)) - state.inputStates[event.source].state = NodeState::InputQueueState::State::BLOCKED; + if(event.status == -1 || event.status == -2) state.inputStates[event.source].state = NodeState::InputQueueState::State::BLOCKED; break; } break; diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index 502fd15ff..c76c90f48 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -134,7 +134,7 @@ void PipelineEventDispatcher::pingMainLoopEvent() { void PipelineEventDispatcher::pingCustomEvent(const std::string& source) { pingEvent(PipelineEvent::Type::CUSTOM, source); } -void PipelineEventDispatcher::pingInputEvent(const std::string& source, std::optional queueSize) { +void PipelineEventDispatcher::pingInputEvent(const std::string& source, int32_t status, std::optional queueSize) { // TODO add mutex checkNodeId(); if(blacklist(PipelineEvent::Type::INPUT, source)) return; @@ -146,6 +146,7 @@ void PipelineEventDispatcher::pingInputEvent(const std::string& source, std::opt eventCopy.setTimestamp(now); eventCopy.tsDevice = eventCopy.ts; eventCopy.nodeId = nodeId; + eventCopy.status = std::move(status); eventCopy.queueSize = std::move(queueSize); eventCopy.interval = PipelineEvent::Interval::NONE; eventCopy.type = PipelineEvent::Type::INPUT; From af86cae59186e8090a0c3cb04b282b87216a027d Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 9 Oct 2025 10:59:40 +0200 Subject: [PATCH 028/124] Add ability to disable sending pipeline events --- include/depthai/utility/PipelineEventDispatcherInterface.hpp | 2 ++ src/utility/PipelineEventDispatcher.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index 9fe66b16f..e8f35751f 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -24,6 +24,8 @@ class PipelineEventDispatcherInterface { } }; + bool sendEvents = true; + virtual ~PipelineEventDispatcherInterface() = default; virtual void setNodeId(int64_t id) = 0; virtual void startEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize = std::nullopt) = 0; diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index c76c90f48..a67504518 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -42,6 +42,7 @@ void PipelineEventDispatcher::setNodeId(int64_t id) { } void PipelineEventDispatcher::startEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize) { // TODO add mutex + if(!sendEvents) return; checkNodeId(); if(blacklist(type, source)) return; @@ -71,6 +72,7 @@ void PipelineEventDispatcher::startCustomEvent(const std::string& source) { } void PipelineEventDispatcher::endEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize) { // TODO add mutex + if(!sendEvents) return; checkNodeId(); if(blacklist(type, source)) return; @@ -107,6 +109,7 @@ void PipelineEventDispatcher::endCustomEvent(const std::string& source) { } void PipelineEventDispatcher::pingEvent(PipelineEvent::Type type, const std::string& source) { // TODO add mutex + if(!sendEvents) return; checkNodeId(); if(blacklist(type, source)) return; @@ -136,6 +139,7 @@ void PipelineEventDispatcher::pingCustomEvent(const std::string& source) { } void PipelineEventDispatcher::pingInputEvent(const std::string& source, int32_t status, std::optional queueSize) { // TODO add mutex + if(!sendEvents) return; checkNodeId(); if(blacklist(PipelineEvent::Type::INPUT, source)) return; From 0405afbbb460c6edafd204cbfea64de68c5b2e46 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 9 Oct 2025 12:30:24 +0200 Subject: [PATCH 029/124] Remove ongoing event warnings --- .../internal/PipelineEventAggregation.cpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 4b22f3a30..24a276340 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -132,11 +132,11 @@ class NodeEventAggregation { } else if(event.interval == PipelineEvent::Interval::START) { if(ongoingEvent.has_value()) { // TODO: add ability to wait for multiple events (nn hailo threaded processing time - events with custom ids for tracking) - logger->warn("Ongoing event (seq {}) not finished before new one (seq {}) started. Event source: {}, node {}", - ongoingEvent->sequenceNum, - event.sequenceNum, - ongoingEvent->source, - event.nodeId); + // logger->warn("Ongoing event (seq {}) not finished before new one (seq {}) started. Event source: {}, node {}", + // ongoingEvent->sequenceNum, + // event.sequenceNum, + // ongoingEvent->source, + // event.nodeId); } // Start event ongoingEvent = event; @@ -211,11 +211,11 @@ class NodeEventAggregation { return true; } else if(ongoingEvent.has_value()) { - logger->warn("Ongoing main loop event (seq {}) not finished before new one (seq {}) started. Event source: {}, node {}", - ongoingEvent->sequenceNum, - event.sequenceNum, - ongoingEvent->source, - event.nodeId); + // logger->warn("Ongoing main loop event (seq {}) not finished before new one (seq {}) started. Event source: {}, node {}", + // ongoingEvent->sequenceNum, + // event.sequenceNum, + // ongoingEvent->source, + // event.nodeId); } // Start event ongoingEvent = event; From af4d644bee45a31b9bdd07a7fc12fb4220153330 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 9 Oct 2025 12:38:10 +0200 Subject: [PATCH 030/124] RVC4 FW: core bump --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 1e26e9d08..c26383a21 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+cd0fc231d8c860acf11ab424109d7e42500626ae") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+51cca92012e8d745a2825d0157ea21d2c7e518aa") From 5024bc96904a88d69c41cdebf3113876973e8f96 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 9 Oct 2025 14:08:36 +0200 Subject: [PATCH 031/124] Fix getting pipeline state from device --- examples/cpp/HostNodes/image_manip_host.cpp | 3 +++ .../cpp/ObjectTracker/object_tracker_replay.cpp | 17 +++++++++++++++++ .../datatype/PipelineEventAggregationConfig.hpp | 2 +- .../depthai/pipeline/datatype/PipelineState.hpp | 1 - .../node/internal/PipelineStateMerge.cpp | 1 - 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/examples/cpp/HostNodes/image_manip_host.cpp b/examples/cpp/HostNodes/image_manip_host.cpp index 6bf7edafa..8688237e1 100644 --- a/examples/cpp/HostNodes/image_manip_host.cpp +++ b/examples/cpp/HostNodes/image_manip_host.cpp @@ -40,10 +40,13 @@ int main(int argc, char** argv) { pipeline.start(); + // TODO remove before merge while(pipeline.isRunning()) { std::this_thread::sleep_for(std::chrono::milliseconds(5000)); std::cout << "Pipeline state: " << pipeline.getPipelineState().nodes().detailed().str() << std::endl; } pipeline.stop(); + // + // pipeline.wait(); } diff --git a/examples/cpp/ObjectTracker/object_tracker_replay.cpp b/examples/cpp/ObjectTracker/object_tracker_replay.cpp index 7a5586826..6b44ae83c 100644 --- a/examples/cpp/ObjectTracker/object_tracker_replay.cpp +++ b/examples/cpp/ObjectTracker/object_tracker_replay.cpp @@ -37,16 +37,33 @@ int main() { // Start pipeline pipeline.start(); + // TODO remove before merge + nlohmann::json j; + j["pipeline"] = pipeline.getPipelineSchema(); + std::cout << "Pipeline schema: " << j.dump(2) << std::endl; + // + // FPS calculation variables auto startTime = std::chrono::steady_clock::now(); int counter = 0; float fps = 0; cv::Scalar color(255, 255, 255); + // TODO remove before merge + int index = 0; + // + while(pipeline.isRunning()) { auto imgFrame = preview->get(); auto track = tracklets->get(); + // TODO remove before merge + if(index++ % 30 == 0) { + std::cout << "----------------------------------------" << std::endl; + std::cout << "Pipeline state: " << pipeline.getPipelineState().nodes().detailed().str() << std::endl; + } + // + counter++; auto currentTime = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast(currentTime - startTime).count(); diff --git a/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp b/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp index afa804b0c..56f844cd1 100644 --- a/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp +++ b/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp @@ -31,7 +31,7 @@ class PipelineEventAggregationConfig : public Buffer { void serialize(std::vector& metadata, DatatypeEnum& datatype) const override; - DEPTHAI_SERIALIZE(PipelineEventAggregationConfig, nodes, repeat); + DEPTHAI_SERIALIZE(PipelineEventAggregationConfig, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodes, repeat); }; } // namespace dai diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index 658db2215..8e3228991 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -4,7 +4,6 @@ #include "depthai/pipeline/datatype/Buffer.hpp" #include "depthai/pipeline/datatype/PipelineEvent.hpp" -#include "depthai/common/optional.hpp" namespace dai { diff --git a/src/pipeline/node/internal/PipelineStateMerge.cpp b/src/pipeline/node/internal/PipelineStateMerge.cpp index c3d4f7a38..7e65ccd88 100644 --- a/src/pipeline/node/internal/PipelineStateMerge.cpp +++ b/src/pipeline/node/internal/PipelineStateMerge.cpp @@ -46,7 +46,6 @@ void PipelineStateMerge::run() { if(waitForMatch && deviceState != nullptr && currentConfig.has_value()) { while(isRunning() && deviceState->configSequenceNum != currentConfig->sequenceNum) { deviceState = inputDevice.get(); - if(!isRunning()) break; } } if(deviceState != nullptr) { From a5dbc4a81a0a6940fcb6eaeca0727a301a035269 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 9 Oct 2025 15:22:46 +0200 Subject: [PATCH 032/124] Bugfix, implement API for getting state (c++) --- .../node/PipelineEventAggregationBindings.cpp | 3 +- include/depthai/pipeline/Pipeline.hpp | 214 +++++++++++++----- .../pipeline/datatype/PipelineEvent.hpp | 2 +- .../PipelineEventAggregationConfig.hpp | 3 +- .../pipeline/datatype/PipelineState.hpp | 2 +- .../PipelineEventAggregationProperties.hpp | 3 +- .../internal/PipelineEventAggregation.cpp | 43 +++- 7 files changed, 199 insertions(+), 71 deletions(-) diff --git a/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp index 3fcf8a616..aa52d802a 100644 --- a/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp +++ b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp @@ -26,8 +26,7 @@ void bind_pipelineeventaggregation(pybind11::module& m, void* pCallstack) { // // // Properties // pipelineEventAggregationProperties.def_readwrite("aggregationWindowSize", &PipelineEventAggregationProperties::aggregationWindowSize) - // .def_readwrite("eventBatchSize", &PipelineEventAggregationProperties::eventBatchSize) - // .def_readwrite("sendEvents", &PipelineEventAggregationProperties::sendEvents); + // .def_readwrite("eventBatchSize", &PipelineEventAggregationProperties::eventBatchSize); // // // Node // pipelineEventAggregation.def_readonly("out", &PipelineEventAggregation::out, DOC(dai, node, PipelineEventAggregation, out)) diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index 480729b7c..8780f894a 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -45,33 +45,34 @@ namespace fs = std::filesystem; * pipeline.getState().nodes({nodeId1}).otherStats({statName1}) -> std::unordered_map; * pipeline.getState().nodes({nodeId1}).outputs(statName) -> TimingStats; */ -template -class NodeStateApi {}; -template <> -class NodeStateApi> { +// TODO move this somewhere else +class NodesStateApi { std::vector nodeIds; std::shared_ptr pipelineStateOut; std::shared_ptr pipelineStateRequest; public: - explicit NodeStateApi(std::vector nodeIds, std::shared_ptr pipelineStateOut, std::shared_ptr pipelineStateRequest) + explicit NodesStateApi(std::vector nodeIds, std::shared_ptr pipelineStateOut, std::shared_ptr pipelineStateRequest) : nodeIds(std::move(nodeIds)), pipelineStateOut(pipelineStateOut), pipelineStateRequest(pipelineStateRequest) {} - std::unordered_map> summary() { + PipelineState summary() { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); for(auto id : nodeIds) { NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = id; - nodeCfg.summary = true; + nodeCfg.events = false; + nodeCfg.inputs = {}; // Do not send any + nodeCfg.outputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any cfg.nodes.push_back(nodeCfg); } pipelineStateRequest->send(std::make_shared(cfg)); auto state = pipelineStateOut->get(); if(!state) throw std::runtime_error("Failed to get PipelineState"); - return {}; // TODO + return *state; } PipelineState detailed() { PipelineEventAggregationConfig cfg; @@ -80,10 +81,7 @@ class NodeStateApi> { for(auto id : nodeIds) { NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = id; - nodeCfg.summary = true; // contains main loop timing - nodeCfg.inputs = {}; // send all - nodeCfg.outputs = {}; // send all - nodeCfg.others = {}; // send all + nodeCfg.events = false; cfg.nodes.push_back(nodeCfg); } @@ -92,19 +90,27 @@ class NodeStateApi> { if(!state) throw std::runtime_error("Failed to get PipelineState"); return *state; } - std::unordered_map> outputs() { + std::unordered_map> outputs() { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); for(auto id : nodeIds) { NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = id; - nodeCfg.outputs = {}; // send all + nodeCfg.inputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; cfg.nodes.push_back(nodeCfg); } - // TODO send and get - return {}; + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + std::unordered_map> result; + for(auto& [nodeId, nodeState] : state->nodeStates) { + result[nodeId] = nodeState.outputStates; + } + return result; } std::unordered_map> inputs() { PipelineEventAggregationConfig cfg; @@ -113,30 +119,45 @@ class NodeStateApi> { for(auto id : nodeIds) { NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = id; - nodeCfg.inputs = {}; // send all + nodeCfg.outputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; cfg.nodes.push_back(nodeCfg); } - // TODO send and get - return {}; + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + std::unordered_map> result; + for(auto& [nodeId, nodeState] : state->nodeStates) { + result[nodeId] = nodeState.inputStates; + } + return result; } - std::unordered_map> otherStats() { + std::unordered_map> otherTimings() { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); for(auto id : nodeIds) { NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = id; - nodeCfg.others = {}; // send all + nodeCfg.inputs = {}; // Do not send any + nodeCfg.outputs = {}; // Do not send any + nodeCfg.events = false; cfg.nodes.push_back(nodeCfg); } - // TODO send and get - return {}; + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + std::unordered_map> result; + for(auto& [nodeId, nodeState] : state->nodeStates) { + result[nodeId] = nodeState.otherTimings; + } + return result; } }; -template <> -class NodeStateApi { +class NodeStateApi { Node::Id nodeId; std::shared_ptr pipelineStateOut; @@ -145,56 +166,87 @@ class NodeStateApi { public: explicit NodeStateApi(Node::Id nodeId, std::shared_ptr pipelineStateOut, std::shared_ptr pipelineStateRequest) : nodeId(nodeId), pipelineStateOut(pipelineStateOut), pipelineStateRequest(pipelineStateRequest) {} - std::unordered_map summary() { - return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).summary()[nodeId]; + NodeState summary() { + return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).summary().nodeStates[nodeId]; } NodeState detailed() { - return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).detailed().nodeStates[nodeId]; + return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).detailed().nodeStates[nodeId]; } - std::unordered_map outputs() { - return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).outputs()[nodeId]; + std::unordered_map outputs() { + return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).outputs()[nodeId]; } std::unordered_map inputs() { - return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).inputs()[nodeId]; + return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).inputs()[nodeId]; } - std::unordered_map otherStats() { - return NodeStateApi>({nodeId}, pipelineStateOut, pipelineStateRequest).otherStats()[nodeId]; + std::unordered_map otherTimings() { + return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).otherTimings()[nodeId]; } - std::unordered_map outputs(const std::vector& outputNames) { + std::unordered_map outputs(const std::vector& outputNames) { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; nodeCfg.outputs = outputNames; + nodeCfg.inputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; cfg.nodes.push_back(nodeCfg); - // TODO send and get - return {}; + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + std::unordered_map result; + for(const auto& outputName : outputNames) { + result[outputName] = state->nodeStates[nodeId].outputStates[outputName]; + } + return result; } - NodeState::TimingStats outputs(const std::string& outputName) { + NodeState::OutputQueueState outputs(const std::string& outputName) { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; nodeCfg.outputs = {outputName}; + nodeCfg.inputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; cfg.nodes.push_back(nodeCfg); - // TODO send and get - return {}; + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + if(state->nodeStates[nodeId].outputStates.find(outputName) == state->nodeStates[nodeId].outputStates.end()) { + throw std::runtime_error("Output name " + outputName + " not found in NodeState for node ID " + std::to_string(nodeId)); + } + return state->nodeStates[nodeId].outputStates[outputName]; } - std::unordered_map> events() { + std::vector events() { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; + nodeCfg.outputs = {}; // Do not send any + nodeCfg.inputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any nodeCfg.events = true; cfg.nodes.push_back(nodeCfg); - // TODO send and get - return {}; + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + return state->nodeStates[nodeId].events; } std::unordered_map inputs(const std::vector& inputNames) { PipelineEventAggregationConfig cfg; @@ -203,10 +255,22 @@ class NodeStateApi { NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; nodeCfg.inputs = inputNames; + nodeCfg.outputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; cfg.nodes.push_back(nodeCfg); - // TODO send and get - return {}; + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + std::unordered_map result; + for(const auto& inputName : inputNames) { + result[inputName] = state->nodeStates[nodeId].inputStates[inputName]; + } + return result; } NodeState::InputQueueState inputs(const std::string& inputName) { PipelineEventAggregationConfig cfg; @@ -215,34 +279,68 @@ class NodeStateApi { NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; nodeCfg.inputs = {inputName}; + nodeCfg.outputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; cfg.nodes.push_back(nodeCfg); - // TODO send and get - return {}; + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + if(state->nodeStates[nodeId].inputStates.find(inputName) == state->nodeStates[nodeId].inputStates.end()) { + throw std::runtime_error("Input name " + inputName + " not found in NodeState for node ID " + std::to_string(nodeId)); + } + return state->nodeStates[nodeId].inputStates[inputName]; } - std::unordered_map otherStats(const std::vector& statNames) { + std::unordered_map otherTimings(const std::vector& statNames) { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; nodeCfg.others = statNames; + nodeCfg.outputs = {}; // Do not send any + nodeCfg.inputs = {}; // Do not send any + nodeCfg.events = false; cfg.nodes.push_back(nodeCfg); - // TODO send and get - return {}; + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + std::unordered_map result; + for(const auto& otherName : statNames) { + result[otherName] = state->nodeStates[nodeId].otherTimings[otherName]; + } + return result; } - NodeState::TimingStats otherStats(const std::string& statName) { + NodeState::Timing otherStats(const std::string& statName) { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; nodeCfg.others = {statName}; + nodeCfg.outputs = {}; // Do not send any + nodeCfg.inputs = {}; // Do not send any + nodeCfg.events = false; cfg.nodes.push_back(nodeCfg); - // TODO send and get - return {}; + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + if(state->nodeStates[nodeId].otherTimings.find(statName) == state->nodeStates[nodeId].otherTimings.end()) { + throw std::runtime_error("Stat name " + statName + " not found in NodeState for node ID " + std::to_string(nodeId)); + } + return state->nodeStates[nodeId].otherTimings[statName]; } }; class PipelineStateApi { @@ -259,14 +357,14 @@ class PipelineStateApi { nodeIds.push_back(n->id); } } - NodeStateApi> nodes() { - return NodeStateApi>(nodeIds, pipelineStateOut, pipelineStateRequest); + NodesStateApi nodes() { + return NodesStateApi(nodeIds, pipelineStateOut, pipelineStateRequest); } - NodeStateApi> nodes(const std::vector& nodeIds) { - return NodeStateApi>(nodeIds, pipelineStateOut, pipelineStateRequest); + NodesStateApi nodes(const std::vector& nodeIds) { + return NodesStateApi(nodeIds, pipelineStateOut, pipelineStateRequest); } - NodeStateApi nodes(Node::Id nodeId) { - return NodeStateApi(nodeId, pipelineStateOut, pipelineStateRequest); + NodeStateApi nodes(Node::Id nodeId) { + return NodeStateApi(nodeId, pipelineStateOut, pipelineStateRequest); } }; diff --git a/include/depthai/pipeline/datatype/PipelineEvent.hpp b/include/depthai/pipeline/datatype/PipelineEvent.hpp index 1ececed60..03293fa97 100644 --- a/include/depthai/pipeline/datatype/PipelineEvent.hpp +++ b/include/depthai/pipeline/datatype/PipelineEvent.hpp @@ -41,7 +41,7 @@ class PipelineEvent : public Buffer { datatype = DatatypeEnum::PipelineEvent; }; - DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeId, status, interval, type, source); + DEPTHAI_SERIALIZE(PipelineEvent, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeId, status, queueSize, interval, type, source); }; } // namespace dai diff --git a/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp b/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp index 56f844cd1..e321bccea 100644 --- a/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp +++ b/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp @@ -14,10 +14,9 @@ class NodeEventAggregationConfig { std::optional> inputs; std::optional> outputs; std::optional> others; - bool summary = false; bool events = false; - DEPTHAI_SERIALIZE(NodeEventAggregationConfig, nodeId, inputs, outputs, others, summary, events); + DEPTHAI_SERIALIZE(NodeEventAggregationConfig, nodeId, inputs, outputs, others, events); }; /// PipelineEventAggregationConfig configuration structure diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index 8e3228991..261994571 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -87,7 +87,7 @@ class NodeState { // Other timings that the developer of the node decided to add std::unordered_map otherTimings; - DEPTHAI_SERIALIZE(NodeState, events, outputStates, inputStates, inputsGetTiming, outputsSendTiming, mainLoopTiming, otherTimings); + DEPTHAI_SERIALIZE(NodeState, state, events, outputStates, inputStates, inputsGetTiming, outputsSendTiming, mainLoopTiming, otherTimings); }; /** diff --git a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp index b5195a75f..04fd9c940 100644 --- a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp +++ b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp @@ -10,9 +10,8 @@ namespace dai { struct PipelineEventAggregationProperties : PropertiesSerializable { uint32_t aggregationWindowSize = 100; uint32_t eventBatchSize = 50; - bool sendEvents = false; }; -DEPTHAI_SERIALIZE_EXT(PipelineEventAggregationProperties, aggregationWindowSize, eventBatchSize, sendEvents); +DEPTHAI_SERIALIZE_EXT(PipelineEventAggregationProperties, aggregationWindowSize, eventBatchSize); } // namespace dai diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 24a276340..2bd4f0701 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -454,15 +454,48 @@ void PipelineEventAggregation::run() { } if(gotConfig || (currentConfig.has_value() && currentConfig->repeat)) { bool updated = handler.getState(outState); - for(auto& [nodeId, nodeState] : outState->nodeStates) { - if(!properties.sendEvents) nodeState.events.clear(); - } - outState->sequenceNum = sequenceNum++; outState->configSequenceNum = currentConfig.has_value() ? currentConfig->sequenceNum : 0; outState->setTimestamp(std::chrono::steady_clock::now()); outState->tsDevice = outState->ts; - // TODO: send only requested data + + for(auto it = outState->nodeStates.begin(); it != outState->nodeStates.end();) { + auto nodeConfig = std::find_if( + currentConfig->nodes.begin(), currentConfig->nodes.end(), [&](const NodeEventAggregationConfig& cfg) { return cfg.nodeId == it->first; }); + if(nodeConfig == currentConfig->nodes.end()) { + it = outState->nodeStates.erase(it); + } else { + if(nodeConfig->inputs.has_value()) { + auto inputStates = it->second.inputStates; + it->second.inputStates.clear(); + for(const auto& inputName : *nodeConfig->inputs) { + if(inputStates.find(inputName) != inputStates.end()) { + it->second.inputStates[inputName] = inputStates[inputName]; + } + } + } + if(nodeConfig->outputs.has_value()) { + auto outputStates = it->second.outputStates; + it->second.outputStates.clear(); + for(const auto& outputName : *nodeConfig->outputs) { + if(outputStates.find(outputName) != outputStates.end()) { + it->second.outputStates[outputName] = outputStates[outputName]; + } + } + } + if(nodeConfig->others.has_value()) { + auto otherTimings = it->second.otherTimings; + it->second.otherTimings.clear(); + for(const auto& otherName : *nodeConfig->others) { + if(otherTimings.find(otherName) != otherTimings.end()) { + it->second.otherTimings[otherName] = otherTimings[otherName]; + } + } + } + if(!nodeConfig->events) it->second.events.clear(); + ++it; + } + } if(gotConfig || (currentConfig.has_value() && currentConfig->repeat && updated)) out.send(outState); } std::this_thread::sleep_for(std::chrono::milliseconds(10)); From 89a168fd7e883fff70df628f7c3151dcd74220fa Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 9 Oct 2025 15:27:01 +0200 Subject: [PATCH 033/124] Fix pipeline state filtering --- .../internal/PipelineEventAggregation.cpp | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 2bd4f0701..dde5fc920 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -459,41 +459,44 @@ void PipelineEventAggregation::run() { outState->setTimestamp(std::chrono::steady_clock::now()); outState->tsDevice = outState->ts; - for(auto it = outState->nodeStates.begin(); it != outState->nodeStates.end();) { - auto nodeConfig = std::find_if( - currentConfig->nodes.begin(), currentConfig->nodes.end(), [&](const NodeEventAggregationConfig& cfg) { return cfg.nodeId == it->first; }); - if(nodeConfig == currentConfig->nodes.end()) { - it = outState->nodeStates.erase(it); - } else { - if(nodeConfig->inputs.has_value()) { - auto inputStates = it->second.inputStates; - it->second.inputStates.clear(); - for(const auto& inputName : *nodeConfig->inputs) { - if(inputStates.find(inputName) != inputStates.end()) { - it->second.inputStates[inputName] = inputStates[inputName]; + if(!currentConfig->nodes.empty()) { + for(auto it = outState->nodeStates.begin(); it != outState->nodeStates.end();) { + auto nodeConfig = std::find_if(currentConfig->nodes.begin(), currentConfig->nodes.end(), [&](const NodeEventAggregationConfig& cfg) { + return cfg.nodeId == it->first; + }); + if(nodeConfig == currentConfig->nodes.end()) { + it = outState->nodeStates.erase(it); + } else { + if(nodeConfig->inputs.has_value()) { + auto inputStates = it->second.inputStates; + it->second.inputStates.clear(); + for(const auto& inputName : *nodeConfig->inputs) { + if(inputStates.find(inputName) != inputStates.end()) { + it->second.inputStates[inputName] = inputStates[inputName]; + } } } - } - if(nodeConfig->outputs.has_value()) { - auto outputStates = it->second.outputStates; - it->second.outputStates.clear(); - for(const auto& outputName : *nodeConfig->outputs) { - if(outputStates.find(outputName) != outputStates.end()) { - it->second.outputStates[outputName] = outputStates[outputName]; + if(nodeConfig->outputs.has_value()) { + auto outputStates = it->second.outputStates; + it->second.outputStates.clear(); + for(const auto& outputName : *nodeConfig->outputs) { + if(outputStates.find(outputName) != outputStates.end()) { + it->second.outputStates[outputName] = outputStates[outputName]; + } } } - } - if(nodeConfig->others.has_value()) { - auto otherTimings = it->second.otherTimings; - it->second.otherTimings.clear(); - for(const auto& otherName : *nodeConfig->others) { - if(otherTimings.find(otherName) != otherTimings.end()) { - it->second.otherTimings[otherName] = otherTimings[otherName]; + if(nodeConfig->others.has_value()) { + auto otherTimings = it->second.otherTimings; + it->second.otherTimings.clear(); + for(const auto& otherName : *nodeConfig->others) { + if(otherTimings.find(otherName) != otherTimings.end()) { + it->second.otherTimings[otherName] = otherTimings[otherName]; + } } } + if(!nodeConfig->events) it->second.events.clear(); + ++it; } - if(!nodeConfig->events) it->second.events.clear(); - ++it; } } if(gotConfig || (currentConfig.has_value() && currentConfig->repeat && updated)) out.send(outState); From c5befed2ce627820e04bea62caa4b71fa8e5d726 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 9 Oct 2025 16:12:01 +0200 Subject: [PATCH 034/124] RVC4 FW: time main loop of some nodes --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index c26383a21..380cdd5e9 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+51cca92012e8d745a2825d0157ea21d2c7e518aa") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+e19591198e276d1580a98f77b3387c1abc881042") From c79abbf237775aaa4358f3b4b857bf1b0c1c3e04 Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 10 Oct 2025 10:07:10 +0200 Subject: [PATCH 035/124] Add python bindings for getting pipeline state --- CMakeLists.txt | 1 + .../python/src/pipeline/PipelineBindings.cpp | 56 +++ include/depthai/pipeline/Pipeline.hpp | 339 +----------------- 3 files changed, 58 insertions(+), 338 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 237f2c5f7..ff4122a05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -273,6 +273,7 @@ set(TARGET_CORE_SOURCES src/device/CalibrationHandler.cpp src/device/Version.cpp src/pipeline/Pipeline.cpp + src/pipeline/PipelineStateApi.cpp src/pipeline/AssetManager.cpp src/pipeline/MessageQueue.cpp src/pipeline/Node.cpp diff --git a/bindings/python/src/pipeline/PipelineBindings.cpp b/bindings/python/src/pipeline/PipelineBindings.cpp index 02d10b3bd..f2b1e6ee8 100644 --- a/bindings/python/src/pipeline/PipelineBindings.cpp +++ b/bindings/python/src/pipeline/PipelineBindings.cpp @@ -64,6 +64,9 @@ void PipelineBindings::bind(pybind11::module& m, void* pCallstack) { py::class_ globalProperties(m, "GlobalProperties", DOC(dai, GlobalProperties)); py::class_ recordConfig(m, "RecordConfig", DOC(dai, RecordConfig)); py::class_ recordVideoConfig(recordConfig, "VideoEncoding", DOC(dai, RecordConfig, VideoEncoding)); + py::class_ pipelineStateApi(m, "PipelineStateApi", DOC(dai, PipelineStateApi)); + py::class_ nodesStateApi(m, "NodesStateApi", DOC(dai, NodesStateApi)); + py::class_ nodeStateApi(m, "NodeStateApi", DOC(dai, NodeStateApi)); py::class_ pipeline(m, "Pipeline", DOC(dai, Pipeline, 2)); /////////////////////////////////////////////////////////////////////// @@ -102,6 +105,59 @@ void PipelineBindings::bind(pybind11::module& m, void* pCallstack) { .def_readwrite("videoEncoding", &RecordConfig::videoEncoding, DOC(dai, RecordConfig, videoEncoding)) .def_readwrite("compressionLevel", &RecordConfig::compressionLevel, DOC(dai, RecordConfig, compressionLevel)); + pipelineStateApi.def("nodes", static_cast(&PipelineStateApi::nodes), DOC(dai, PipelineStateApi, nodes)) + .def("nodes", + static_cast&)>(&PipelineStateApi::nodes), + py::arg("nodeIds"), + DOC(dai, PipelineStateApi, nodes, 2)) + .def("nodes", + static_cast(&PipelineStateApi::nodes), + py::arg("nodeId"), + DOC(dai, PipelineStateApi, nodes, 3)); + + nodesStateApi.def("summary", &NodesStateApi::summary, DOC(dai, NodesStateApi, summary)) + .def("detailed", &NodesStateApi::detailed, DOC(dai, NodesStateApi, detailed)) + .def("outputs", &NodesStateApi::outputs, DOC(dai, NodesStateApi, outputs)) + .def("inputs", &NodesStateApi::inputs, DOC(dai, NodesStateApi, inputs)) + .def("otherTimings", &NodesStateApi::otherTimings, DOC(dai, NodesStateApi, otherTimings)); + + nodeStateApi.def("summary", &NodeStateApi::summary, DOC(dai, NodeStateApi, summary)) + .def("detailed", &NodeStateApi::detailed, DOC(dai, NodeStateApi, detailed)) + .def("outputs", + static_cast (NodeStateApi::*)()>(&NodeStateApi::outputs), + DOC(dai, NodeStateApi, outputs)) + .def("outputs", + static_cast (NodeStateApi::*)(const std::vector&)>( + &NodeStateApi::outputs), + py::arg("outputNames"), + DOC(dai, NodeStateApi, outputs, 2)) + .def("outputs", + static_cast(&NodeStateApi::outputs), + py::arg("outputName"), + DOC(dai, NodeStateApi, outputs, 3)) + .def("inputs", + static_cast (NodeStateApi::*)()>(&NodeStateApi::inputs), + DOC(dai, NodeStateApi, inputs)) + .def("inputs", + static_cast (NodeStateApi::*)(const std::vector&)>(&NodeStateApi::inputs), + py::arg("inputNames"), + DOC(dai, NodeStateApi, inputs, 2)) + .def("inputs", + static_cast(&NodeStateApi::inputs), + py::arg("inputName"), + DOC(dai, NodeStateApi, inputs, 3)) + .def("otherTimings", + static_cast (NodeStateApi::*)()>(&NodeStateApi::otherTimings), + DOC(dai, NodeStateApi, otherTimings)) + .def("otherTimings", + static_cast (NodeStateApi::*)(const std::vector&)>(&NodeStateApi::otherTimings), + py::arg("statNames"), + DOC(dai, NodeStateApi, otherTimings, 2)) + .def("otherStats", + static_cast(&NodeStateApi::otherStats), + py::arg("statName"), + DOC(dai, NodeStateApi, otherStats)); + // bind pipeline pipeline .def(py::init([](bool createImplicitDevice) { diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index 8780f894a..4991b770a 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -12,6 +12,7 @@ #include "AssetManager.hpp" #include "DeviceNode.hpp" #include "Node.hpp" +#include "PipelineStateApi.hpp" #include "depthai/device/CalibrationHandler.hpp" #include "depthai/device/Device.hpp" #include "depthai/openvino/OpenVINO.hpp" @@ -30,344 +31,6 @@ namespace dai { namespace fs = std::filesystem; -/** - * pipeline.getState().nodes({nodeId1}).summary() -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).detailed() -> std::unordered_map; - * pipeline.getState().nodes(nodeId1).detailed() -> NodeState; - * pipeline.getState().nodes({nodeId1}).outputs() -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).outputs({outputName1}) -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).outputs(outputName) -> TimingStats; - * pipeline.getState().nodes({nodeId1}).events(); - * pipeline.getState().nodes({nodeId1}).inputs() -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).inputs({inputName1}) -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).inputs(inputName) -> QueueState; - * pipeline.getState().nodes({nodeId1}).otherStats() -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).otherStats({statName1}) -> std::unordered_map; - * pipeline.getState().nodes({nodeId1}).outputs(statName) -> TimingStats; - */ -// TODO move this somewhere else -class NodesStateApi { - std::vector nodeIds; - - std::shared_ptr pipelineStateOut; - std::shared_ptr pipelineStateRequest; - - public: - explicit NodesStateApi(std::vector nodeIds, std::shared_ptr pipelineStateOut, std::shared_ptr pipelineStateRequest) - : nodeIds(std::move(nodeIds)), pipelineStateOut(pipelineStateOut), pipelineStateRequest(pipelineStateRequest) {} - PipelineState summary() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - for(auto id : nodeIds) { - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = id; - nodeCfg.events = false; - nodeCfg.inputs = {}; // Do not send any - nodeCfg.outputs = {}; // Do not send any - nodeCfg.others = {}; // Do not send any - cfg.nodes.push_back(nodeCfg); - } - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - return *state; - } - PipelineState detailed() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - for(auto id : nodeIds) { - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = id; - nodeCfg.events = false; - cfg.nodes.push_back(nodeCfg); - } - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - return *state; - } - std::unordered_map> outputs() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - for(auto id : nodeIds) { - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = id; - nodeCfg.inputs = {}; // Do not send any - nodeCfg.others = {}; // Do not send any - nodeCfg.events = false; - cfg.nodes.push_back(nodeCfg); - } - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - std::unordered_map> result; - for(auto& [nodeId, nodeState] : state->nodeStates) { - result[nodeId] = nodeState.outputStates; - } - return result; - } - std::unordered_map> inputs() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - for(auto id : nodeIds) { - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = id; - nodeCfg.outputs = {}; // Do not send any - nodeCfg.others = {}; // Do not send any - nodeCfg.events = false; - cfg.nodes.push_back(nodeCfg); - } - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - std::unordered_map> result; - for(auto& [nodeId, nodeState] : state->nodeStates) { - result[nodeId] = nodeState.inputStates; - } - return result; - } - std::unordered_map> otherTimings() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - for(auto id : nodeIds) { - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = id; - nodeCfg.inputs = {}; // Do not send any - nodeCfg.outputs = {}; // Do not send any - nodeCfg.events = false; - cfg.nodes.push_back(nodeCfg); - } - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - std::unordered_map> result; - for(auto& [nodeId, nodeState] : state->nodeStates) { - result[nodeId] = nodeState.otherTimings; - } - return result; - } -}; -class NodeStateApi { - Node::Id nodeId; - - std::shared_ptr pipelineStateOut; - std::shared_ptr pipelineStateRequest; - - public: - explicit NodeStateApi(Node::Id nodeId, std::shared_ptr pipelineStateOut, std::shared_ptr pipelineStateRequest) - : nodeId(nodeId), pipelineStateOut(pipelineStateOut), pipelineStateRequest(pipelineStateRequest) {} - NodeState summary() { - return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).summary().nodeStates[nodeId]; - } - NodeState detailed() { - return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).detailed().nodeStates[nodeId]; - } - std::unordered_map outputs() { - return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).outputs()[nodeId]; - } - std::unordered_map inputs() { - return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).inputs()[nodeId]; - } - std::unordered_map otherTimings() { - return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).otherTimings()[nodeId]; - } - std::unordered_map outputs(const std::vector& outputNames) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.outputs = outputNames; - nodeCfg.inputs = {}; // Do not send any - nodeCfg.others = {}; // Do not send any - nodeCfg.events = false; - cfg.nodes.push_back(nodeCfg); - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { - throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); - } - std::unordered_map result; - for(const auto& outputName : outputNames) { - result[outputName] = state->nodeStates[nodeId].outputStates[outputName]; - } - return result; - } - NodeState::OutputQueueState outputs(const std::string& outputName) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.outputs = {outputName}; - nodeCfg.inputs = {}; // Do not send any - nodeCfg.others = {}; // Do not send any - nodeCfg.events = false; - cfg.nodes.push_back(nodeCfg); - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { - throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); - } - if(state->nodeStates[nodeId].outputStates.find(outputName) == state->nodeStates[nodeId].outputStates.end()) { - throw std::runtime_error("Output name " + outputName + " not found in NodeState for node ID " + std::to_string(nodeId)); - } - return state->nodeStates[nodeId].outputStates[outputName]; - } - std::vector events() { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.outputs = {}; // Do not send any - nodeCfg.inputs = {}; // Do not send any - nodeCfg.others = {}; // Do not send any - nodeCfg.events = true; - cfg.nodes.push_back(nodeCfg); - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { - throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); - } - return state->nodeStates[nodeId].events; - } - std::unordered_map inputs(const std::vector& inputNames) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.inputs = inputNames; - nodeCfg.outputs = {}; // Do not send any - nodeCfg.others = {}; // Do not send any - nodeCfg.events = false; - cfg.nodes.push_back(nodeCfg); - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { - throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); - } - std::unordered_map result; - for(const auto& inputName : inputNames) { - result[inputName] = state->nodeStates[nodeId].inputStates[inputName]; - } - return result; - } - NodeState::InputQueueState inputs(const std::string& inputName) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.inputs = {inputName}; - nodeCfg.outputs = {}; // Do not send any - nodeCfg.others = {}; // Do not send any - nodeCfg.events = false; - cfg.nodes.push_back(nodeCfg); - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { - throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); - } - if(state->nodeStates[nodeId].inputStates.find(inputName) == state->nodeStates[nodeId].inputStates.end()) { - throw std::runtime_error("Input name " + inputName + " not found in NodeState for node ID " + std::to_string(nodeId)); - } - return state->nodeStates[nodeId].inputStates[inputName]; - } - std::unordered_map otherTimings(const std::vector& statNames) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.others = statNames; - nodeCfg.outputs = {}; // Do not send any - nodeCfg.inputs = {}; // Do not send any - nodeCfg.events = false; - cfg.nodes.push_back(nodeCfg); - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { - throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); - } - std::unordered_map result; - for(const auto& otherName : statNames) { - result[otherName] = state->nodeStates[nodeId].otherTimings[otherName]; - } - return result; - } - NodeState::Timing otherStats(const std::string& statName) { - PipelineEventAggregationConfig cfg; - cfg.repeat = false; - cfg.setTimestamp(std::chrono::steady_clock::now()); - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = nodeId; - nodeCfg.others = {statName}; - nodeCfg.outputs = {}; // Do not send any - nodeCfg.inputs = {}; // Do not send any - nodeCfg.events = false; - cfg.nodes.push_back(nodeCfg); - - pipelineStateRequest->send(std::make_shared(cfg)); - auto state = pipelineStateOut->get(); - if(!state) throw std::runtime_error("Failed to get PipelineState"); - if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { - throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); - } - if(state->nodeStates[nodeId].otherTimings.find(statName) == state->nodeStates[nodeId].otherTimings.end()) { - throw std::runtime_error("Stat name " + statName + " not found in NodeState for node ID " + std::to_string(nodeId)); - } - return state->nodeStates[nodeId].otherTimings[statName]; - } -}; -class PipelineStateApi { - std::shared_ptr pipelineStateOut; - std::shared_ptr pipelineStateRequest; - std::vector nodeIds; // empty means all nodes - - public: - PipelineStateApi(std::shared_ptr pipelineStateOut, - std::shared_ptr pipelineStateRequest, - const std::vector>& allNodes) - : pipelineStateOut(std::move(pipelineStateOut)), pipelineStateRequest(std::move(pipelineStateRequest)) { - for(const auto& n : allNodes) { - nodeIds.push_back(n->id); - } - } - NodesStateApi nodes() { - return NodesStateApi(nodeIds, pipelineStateOut, pipelineStateRequest); - } - NodesStateApi nodes(const std::vector& nodeIds) { - return NodesStateApi(nodeIds, pipelineStateOut, pipelineStateRequest); - } - NodeStateApi nodes(Node::Id nodeId) { - return NodeStateApi(nodeId, pipelineStateOut, pipelineStateRequest); - } -}; - class PipelineImpl : public std::enable_shared_from_this { friend class Pipeline; friend class Node; From df8011bbf3e3e8203fe89c6188f8074fbf3f3760 Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 10 Oct 2025 13:35:38 +0200 Subject: [PATCH 036/124] Implement tracking events through threads --- .../datatype/PipelineStateBindings.cpp | 3 +- .../pipeline/datatype/PipelineState.hpp | 3 +- .../PipelineEventAggregationProperties.hpp | 3 +- include/depthai/utility/CircularBuffer.hpp | 108 ++++++++++++++++- .../utility/PipelineEventDispatcher.hpp | 1 + .../PipelineEventDispatcherInterface.hpp | 2 + .../internal/PipelineEventAggregation.cpp | 109 ++++++++++-------- src/utility/PipelineEventDispatcher.cpp | 20 ++++ 8 files changed, 195 insertions(+), 54 deletions(-) diff --git a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp index 0a22611f4..21e560891 100644 --- a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp @@ -52,8 +52,7 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { durationEvent.def(py::init<>()) .def("__repr__", &NodeState::DurationEvent::str) .def_readwrite("startEvent", &NodeState::DurationEvent::startEvent, DOC(dai, NodeState, DurationEvent, startEvent)) - .def_readwrite("durationUs", &NodeState::DurationEvent::durationUs, DOC(dai, NodeState, TimingStats, durationUs)) - .def_readwrite("fps", &NodeState::DurationEvent::fps, DOC(dai, NodeState, TimingStats, fps)); + .def_readwrite("durationUs", &NodeState::DurationEvent::durationUs, DOC(dai, NodeState, TimingStats, durationUs)); nodeStateTimingStats.def(py::init<>()) .def("__repr__", &NodeState::TimingStats::str) diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index 261994571..ea73c38a5 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -12,8 +12,7 @@ class NodeState { struct DurationEvent { PipelineEvent startEvent; uint64_t durationUs; - float fps; - DEPTHAI_SERIALIZE(DurationEvent, startEvent, durationUs, fps); + DEPTHAI_SERIALIZE(DurationEvent, startEvent, durationUs); }; struct TimingStats { uint64_t minMicros = -1; diff --git a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp index 04fd9c940..013dd29bf 100644 --- a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp +++ b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp @@ -10,8 +10,9 @@ namespace dai { struct PipelineEventAggregationProperties : PropertiesSerializable { uint32_t aggregationWindowSize = 100; uint32_t eventBatchSize = 50; + uint32_t eventWaitWindow = 16; }; -DEPTHAI_SERIALIZE_EXT(PipelineEventAggregationProperties, aggregationWindowSize, eventBatchSize); +DEPTHAI_SERIALIZE_EXT(PipelineEventAggregationProperties, aggregationWindowSize, eventBatchSize, eventWaitWindow); } // namespace dai diff --git a/include/depthai/utility/CircularBuffer.hpp b/include/depthai/utility/CircularBuffer.hpp index c4cf3c181..c0428924e 100644 --- a/include/depthai/utility/CircularBuffer.hpp +++ b/include/depthai/utility/CircularBuffer.hpp @@ -11,12 +11,14 @@ class CircularBuffer { CircularBuffer(size_t size) : maxSize(size) { buffer.reserve(size); } - void add(T value) { + T& add(T value) { if(buffer.size() < maxSize) { buffer.push_back(value); + return buffer.back(); } else { buffer[index] = value; index = (index + 1) % maxSize; + return buffer[(index + maxSize - 1) % maxSize]; } } std::vector getBuffer() const { @@ -53,10 +55,114 @@ class CircularBuffer { return buffer.size(); } + // =========================================================== + // Forward iterator + // =========================================================== + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = T*; + using reference = T&; + + iterator(CircularBuffer* parent, size_t pos) : parent(parent), pos(pos) {} + + reference operator*() { + return parent->buffer[(parent->start() + pos) % parent->buffer.size()]; + } + pointer operator->() { + return &(**this); + } + + iterator& operator++() { + ++pos; + return *this; + } + iterator operator++(int) { + iterator tmp = *this; + ++(*this); + return tmp; + } + + bool operator==(const iterator& other) const { + return parent == other.parent && pos == other.pos; + } + bool operator!=(const iterator& other) const { + return !(*this == other); + } + + private: + CircularBuffer* parent; + size_t pos; + }; + + iterator begin() { + return iterator(this, 0); + } + iterator end() { + return iterator(this, buffer.size()); + } + + // =========================================================== + // Reverse iterator + // =========================================================== + class reverse_iterator { + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = T*; + using reference = T&; + + reverse_iterator(CircularBuffer* parent, size_t pos) : parent(parent), pos(pos) {} + + reference operator*() { + // Map reverse position to logical order + size_t logicalIndex = (parent->start() + parent->buffer.size() - 1 - pos) % parent->buffer.size(); + return parent->buffer[logicalIndex]; + } + pointer operator->() { + return &(**this); + } + + reverse_iterator& operator++() { + ++pos; + return *this; + } + reverse_iterator operator++(int) { + reverse_iterator tmp = *this; + ++(*this); + return tmp; + } + + bool operator==(const reverse_iterator& other) const { + return parent == other.parent && pos == other.pos; + } + bool operator!=(const reverse_iterator& other) const { + return !(*this == other); + } + + private: + CircularBuffer* parent; + size_t pos; + }; + + reverse_iterator rbegin() { + return reverse_iterator(this, 0); + } + reverse_iterator rend() { + return reverse_iterator(this, buffer.size()); + } + private: std::vector buffer; size_t maxSize; size_t index = 0; + + size_t start() const { + return (buffer.size() < maxSize) ? 0 : index; + } }; } // namespace utility diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index 3590b0967..0908cead6 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -43,6 +43,7 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void endOutputEvent(const std::string& source) override; void endCustomEvent(const std::string& source) override; void pingEvent(PipelineEvent::Type type, const std::string& source) override; + void pingTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) override; void pingMainLoopEvent() override; void pingCustomEvent(const std::string& source) override; void pingInputEvent(const std::string& source, int32_t status, std::optional queueSize = std::nullopt) override; diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index e8f35751f..e50b35537 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -37,6 +37,8 @@ class PipelineEventDispatcherInterface { virtual void endOutputEvent(const std::string& source) = 0; virtual void endCustomEvent(const std::string& source) = 0; virtual void pingEvent(PipelineEvent::Type type, const std::string& source) = 0; + // The sequenceNum should be unique. Duration is calculated from sequenceNum - 1 to sequenceNum + virtual void pingTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) = 0; virtual void pingMainLoopEvent() = 0; virtual void pingCustomEvent(const std::string& source) = 0; virtual void pingInputEvent(const std::string& source, int32_t status, std::optional queueSize = std::nullopt) = 0; diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index dde5fc920..31243a82c 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -24,10 +24,11 @@ class NodeEventAggregation { uint32_t windowSize; uint32_t eventBatchSize; + uint32_t eventWaitWindow; public: - NodeEventAggregation(uint32_t windowSize, uint32_t eventBatchSize, std::shared_ptr logger) - : logger(logger), windowSize(windowSize), eventBatchSize(eventBatchSize), eventsBuffer(windowSize) {} + NodeEventAggregation(uint32_t windowSize, uint32_t eventBatchSize, uint32_t eventWaitWindow, std::shared_ptr logger) + : logger(logger), windowSize(windowSize), eventBatchSize(eventBatchSize), eventWaitWindow(eventWaitWindow), eventsBuffer(windowSize) {} NodeState state; utility::CircularBuffer eventsBuffer; std::unordered_map>> inputTimingsBuffers; @@ -45,22 +46,33 @@ class NodeEventAggregation { std::unique_ptr> outputsSendFpsBuffer; std::unordered_map>> otherFpsBuffers; - std::unordered_map> ongoingInputEvents; - std::unordered_map> ongoingOutputEvents; - std::optional ongoingGetInputsEvent; - std::optional ongoingSendOutputsEvent; - std::optional ongoingMainLoopEvent; - std::unordered_map> ongoingOtherEvents; + std::unordered_map>>> ongoingInputEvents; + std::unordered_map>>> ongoingOutputEvents; + std::unique_ptr>> ongoingGetInputsEvents; + std::unique_ptr>> ongoingSendOutputsEvents; + std::unique_ptr>> ongoingMainLoopEvents; + std::unordered_map>>> ongoingOtherEvents; uint32_t count = 0; private: + inline std::optional* findOngoingEvent(uint32_t sequenceNum, utility::CircularBuffer>& buffer) { + for(auto rit = buffer.rbegin(); rit != buffer.rend(); ++rit) { + if(rit->has_value() && rit->value().sequenceNum == sequenceNum) { + return &(*rit); + } else if(rit->has_value() && rit->value().sequenceNum < sequenceNum) { + break; + } + } + return nullptr; + } + inline bool updateIntervalBuffers(PipelineEvent& event) { using namespace std::chrono; std::unique_ptr> emptyIntBuffer; std::unique_ptr> emptyTimeBuffer; - auto& ongoingEvent = [&]() -> std::optional& { + auto& ongoingEvents = [&]() -> std::unique_ptr>>& { switch(event.type) { case PipelineEvent::Type::LOOP: throw std::runtime_error("LOOP event should not be an interval"); @@ -71,11 +83,11 @@ class NodeEventAggregation { case PipelineEvent::Type::CUSTOM: return ongoingOtherEvents[event.source]; case PipelineEvent::Type::INPUT_BLOCK: - return ongoingGetInputsEvent; + return ongoingGetInputsEvents; case PipelineEvent::Type::OUTPUT_BLOCK: - return ongoingSendOutputsEvent; + return ongoingSendOutputsEvents; } - return ongoingMainLoopEvent; // To silence compiler warning + return ongoingMainLoopEvents; // To silence compiler warning }(); auto& timingsBuffer = [&]() -> std::unique_ptr>& { switch(event.type) { @@ -112,34 +124,28 @@ class NodeEventAggregation { return emptyTimeBuffer; // To silence compiler warning }(); + if(ongoingEvents == nullptr) ongoingEvents = std::make_unique>>(eventWaitWindow); if(timingsBuffer == nullptr) timingsBuffer = std::make_unique>(windowSize); if(fpsBuffer == nullptr) fpsBuffer = std::make_unique>(windowSize); - if(ongoingEvent.has_value() && ongoingEvent->sequenceNum == event.sequenceNum && event.interval == PipelineEvent::Interval::END) { + auto* ongoingEvent = findOngoingEvent(event.sequenceNum, *ongoingEvents); + + if(ongoingEvent && ongoingEvent->has_value() && event.interval == PipelineEvent::Interval::END) { // End event NodeState::DurationEvent durationEvent; - durationEvent.startEvent = *ongoingEvent; - durationEvent.durationUs = duration_cast(event.getTimestamp() - ongoingEvent->getTimestamp()).count(); + durationEvent.startEvent = ongoingEvent->value(); + durationEvent.durationUs = duration_cast(event.getTimestamp() - ongoingEvent->value().getTimestamp()).count(); eventsBuffer.add(durationEvent); - state.events = eventsBuffer.getBuffer(); timingsBuffer->add(durationEvent.durationUs); fpsBuffer->add({durationEvent.startEvent.getTimestamp(), durationEvent.startEvent.getSequenceNum()}); - ongoingEvent = std::nullopt; + *ongoingEvent = std::nullopt; return true; } else if(event.interval == PipelineEvent::Interval::START) { - if(ongoingEvent.has_value()) { - // TODO: add ability to wait for multiple events (nn hailo threaded processing time - events with custom ids for tracking) - // logger->warn("Ongoing event (seq {}) not finished before new one (seq {}) started. Event source: {}, node {}", - // ongoingEvent->sequenceNum, - // event.sequenceNum, - // ongoingEvent->source, - // event.nodeId); - } // Start event - ongoingEvent = event; + ongoingEvents->add(event); } return false; } @@ -149,10 +155,10 @@ class NodeEventAggregation { std::unique_ptr> emptyIntBuffer; std::unique_ptr> emptyTimeBuffer; - auto& ongoingEvent = [&]() -> std::optional& { + auto& ongoingEvents = [&]() -> std::unique_ptr>>& { switch(event.type) { case PipelineEvent::Type::LOOP: - return ongoingMainLoopEvent; + return ongoingMainLoopEvents; case PipelineEvent::Type::CUSTOM: return ongoingOtherEvents[event.source]; case PipelineEvent::Type::INPUT: @@ -161,7 +167,7 @@ class NodeEventAggregation { case PipelineEvent::Type::OUTPUT_BLOCK: throw std::runtime_error("INPUT and OUTPUT events should not be pings"); } - return ongoingMainLoopEvent; // To silence compiler warning + return ongoingMainLoopEvents; // To silence compiler warning }(); auto& timingsBuffer = [&]() -> std::unique_ptr>& { switch(event.type) { @@ -192,33 +198,29 @@ class NodeEventAggregation { return emptyTimeBuffer; // To silence compiler warning }(); + if(ongoingEvents == nullptr) ongoingEvents = std::make_unique>>(eventWaitWindow); if(timingsBuffer == nullptr) timingsBuffer = std::make_unique>(windowSize); if(fpsBuffer == nullptr) fpsBuffer = std::make_unique>(windowSize); - if(ongoingEvent.has_value() && ongoingEvent->sequenceNum == event.sequenceNum - 1) { + auto* ongoingEvent = findOngoingEvent(event.sequenceNum - 1, *ongoingEvents); + + if(ongoingEvent && ongoingEvent->has_value()) { // End event NodeState::DurationEvent durationEvent; - durationEvent.startEvent = *ongoingEvent; - durationEvent.durationUs = duration_cast(event.getTimestamp() - ongoingEvent->getTimestamp()).count(); + durationEvent.startEvent = ongoingEvent->value(); + durationEvent.durationUs = duration_cast(event.getTimestamp() - ongoingEvent->value().getTimestamp()).count(); eventsBuffer.add(durationEvent); - state.events = eventsBuffer.getBuffer(); timingsBuffer->add(durationEvent.durationUs); if(fpsBuffer) fpsBuffer->add({durationEvent.startEvent.getTimestamp(), durationEvent.startEvent.getSequenceNum()}); // Start event - ongoingEvent = event; + ongoingEvents->add(event); return true; - } else if(ongoingEvent.has_value()) { - // logger->warn("Ongoing main loop event (seq {}) not finished before new one (seq {}) started. Event source: {}, node {}", - // ongoingEvent->sequenceNum, - // event.sequenceNum, - // ongoingEvent->source, - // event.nodeId); } // Start event - ongoingEvent = event; + ongoingEvents->add(event); return false; } @@ -368,6 +370,7 @@ class PipelineEventHandler { Node::InputMap* inputs; uint32_t aggregationWindowSize; uint32_t eventBatchSize; + uint32_t eventWaitWindow; std::atomic running; @@ -378,8 +381,9 @@ class PipelineEventHandler { std::shared_mutex mutex; public: - PipelineEventHandler(Node::InputMap* inputs, uint32_t aggregationWindowSize, uint32_t eventBatchSize, std::shared_ptr logger) - : inputs(inputs), aggregationWindowSize(aggregationWindowSize), eventBatchSize(eventBatchSize), logger(logger) {} + PipelineEventHandler( + Node::InputMap* inputs, uint32_t aggregationWindowSize, uint32_t eventBatchSize, uint32_t eventWaitWindow, std::shared_ptr logger) + : inputs(inputs), aggregationWindowSize(aggregationWindowSize), eventBatchSize(eventBatchSize), eventWaitWindow(eventWaitWindow), logger(logger) {} void threadedRun() { while(running) { std::unordered_map> events; @@ -391,7 +395,7 @@ class PipelineEventHandler { for(auto& [k, event] : events) { if(event != nullptr) { if(nodeStates.find(event->nodeId) == nodeStates.end()) { - nodeStates.insert_or_assign(event->nodeId, NodeEventAggregation(aggregationWindowSize, eventBatchSize, logger)); + nodeStates.insert_or_assign(event->nodeId, NodeEventAggregation(aggregationWindowSize, eventBatchSize, eventWaitWindow, logger)); } { std::unique_lock lock(mutex); @@ -412,11 +416,12 @@ class PipelineEventHandler { running = false; if(thread.joinable()) thread.join(); } - bool getState(std::shared_ptr outState) { + bool getState(std::shared_ptr outState, bool sendEvents) { std::shared_lock lock(mutex); bool updated = false; for(auto& [nodeId, nodeState] : nodeStates) { outState->nodeStates[nodeId] = nodeState.state; + if(sendEvents) outState->nodeStates[nodeId].events = nodeState.eventsBuffer.getBuffer(); if(nodeState.count % eventBatchSize == 0) updated = true; } return updated; @@ -437,7 +442,7 @@ bool PipelineEventAggregation::runOnHost() const { void PipelineEventAggregation::run() { auto& logger = pimpl->logger; - PipelineEventHandler handler(&inputs, properties.aggregationWindowSize, properties.eventBatchSize, logger); + PipelineEventHandler handler(&inputs, properties.aggregationWindowSize, properties.eventBatchSize, properties.eventWaitWindow, logger); handler.run(); std::optional currentConfig; @@ -453,7 +458,16 @@ void PipelineEventAggregation::run() { } } if(gotConfig || (currentConfig.has_value() && currentConfig->repeat)) { - bool updated = handler.getState(outState); + bool sendEvents = false; + if(currentConfig.has_value()) { + for(const auto& nodeCfg : currentConfig->nodes) { + if(nodeCfg.events) { + sendEvents = true; + break; + } + } + } + bool updated = handler.getState(outState, sendEvents); outState->sequenceNum = sequenceNum++; outState->configSequenceNum = currentConfig.has_value() ? currentConfig->sequenceNum : 0; outState->setTimestamp(std::chrono::steady_clock::now()); @@ -494,7 +508,6 @@ void PipelineEventAggregation::run() { } } } - if(!nodeConfig->events) it->second.events.clear(); ++it; } } diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index a67504518..4a16c4506 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -131,6 +131,26 @@ void PipelineEventDispatcher::pingEvent(PipelineEvent::Type type, const std::str out->send(std::make_shared(event.event)); } } +void PipelineEventDispatcher::pingTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { + if(!sendEvents) return; + checkNodeId(); + if(blacklist(type, source)) return; + + auto now = std::chrono::steady_clock::now(); + + auto event = std::make_shared(); + event->setTimestamp(now); + event->tsDevice = event->ts; + event->sequenceNum = sequenceNum; + event->nodeId = nodeId; + event->interval = PipelineEvent::Interval::NONE; + event->type = type; + event->source = source; + + if(out) { + out->send(event); + } +} void PipelineEventDispatcher::pingMainLoopEvent() { pingEvent(PipelineEvent::Type::LOOP, "_mainLoop"); } From fd27d195beeb3c29fc31d8e3ed2a4e708469ed44 Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 10 Oct 2025 13:40:42 +0200 Subject: [PATCH 037/124] Add missing files --- include/depthai/pipeline/PipelineStateApi.hpp | 96 +++++++ src/pipeline/PipelineStateApi.cpp | 271 ++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 include/depthai/pipeline/PipelineStateApi.hpp create mode 100644 src/pipeline/PipelineStateApi.cpp diff --git a/include/depthai/pipeline/PipelineStateApi.hpp b/include/depthai/pipeline/PipelineStateApi.hpp new file mode 100644 index 000000000..90f0bd98e --- /dev/null +++ b/include/depthai/pipeline/PipelineStateApi.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include "Node.hpp" +#include "depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp" +#include "depthai/pipeline/datatype/PipelineState.hpp" + +namespace dai { + +/** + * pipeline.getState().nodes({nodeId1}).summary() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).detailed() -> std::unordered_map; + * pipeline.getState().nodes(nodeId1).detailed() -> NodeState; + * pipeline.getState().nodes({nodeId1}).outputs() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).outputs({outputName1}) -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).outputs(outputName) -> TimingStats; + * pipeline.getState().nodes({nodeId1}).events(); + * pipeline.getState().nodes({nodeId1}).inputs() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).inputs({inputName1}) -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).inputs(inputName) -> QueueState; + * pipeline.getState().nodes({nodeId1}).otherStats() -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).otherStats({statName1}) -> std::unordered_map; + * pipeline.getState().nodes({nodeId1}).outputs(statName) -> TimingStats; + */ +class NodesStateApi { + std::vector nodeIds; + + std::shared_ptr pipelineStateOut; + std::shared_ptr pipelineStateRequest; + + public: + explicit NodesStateApi(std::vector nodeIds, std::shared_ptr pipelineStateOut, std::shared_ptr pipelineStateRequest) + : nodeIds(std::move(nodeIds)), pipelineStateOut(pipelineStateOut), pipelineStateRequest(pipelineStateRequest) {} + PipelineState summary(); + PipelineState detailed(); + std::unordered_map> outputs(); + std::unordered_map> inputs(); + std::unordered_map> otherTimings(); +}; +class NodeStateApi { + Node::Id nodeId; + + std::shared_ptr pipelineStateOut; + std::shared_ptr pipelineStateRequest; + + public: + explicit NodeStateApi(Node::Id nodeId, std::shared_ptr pipelineStateOut, std::shared_ptr pipelineStateRequest) + : nodeId(nodeId), pipelineStateOut(pipelineStateOut), pipelineStateRequest(pipelineStateRequest) {} + NodeState summary() { + return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).summary().nodeStates[nodeId]; + } + NodeState detailed() { + return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).detailed().nodeStates[nodeId]; + } + std::unordered_map outputs() { + return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).outputs()[nodeId]; + } + std::unordered_map inputs() { + return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).inputs()[nodeId]; + } + std::unordered_map otherTimings() { + return NodesStateApi({nodeId}, pipelineStateOut, pipelineStateRequest).otherTimings()[nodeId]; + } + std::unordered_map outputs(const std::vector& outputNames); + NodeState::OutputQueueState outputs(const std::string& outputName); + std::vector events(); + std::unordered_map inputs(const std::vector& inputNames); + NodeState::InputQueueState inputs(const std::string& inputName); + std::unordered_map otherTimings(const std::vector& statNames); + NodeState::Timing otherStats(const std::string& statName); +}; +class PipelineStateApi { + std::shared_ptr pipelineStateOut; + std::shared_ptr pipelineStateRequest; + std::vector nodeIds; // empty means all nodes + + public: + PipelineStateApi(std::shared_ptr pipelineStateOut, + std::shared_ptr pipelineStateRequest, + const std::vector>& allNodes) + : pipelineStateOut(std::move(pipelineStateOut)), pipelineStateRequest(std::move(pipelineStateRequest)) { + for(const auto& n : allNodes) { + nodeIds.push_back(n->id); + } + } + NodesStateApi nodes() { + return NodesStateApi(nodeIds, pipelineStateOut, pipelineStateRequest); + } + NodesStateApi nodes(const std::vector& nodeIds) { + return NodesStateApi(nodeIds, pipelineStateOut, pipelineStateRequest); + } + NodeStateApi nodes(Node::Id nodeId) { + return NodeStateApi(nodeId, pipelineStateOut, pipelineStateRequest); + } +}; + +} // namespace dai diff --git a/src/pipeline/PipelineStateApi.cpp b/src/pipeline/PipelineStateApi.cpp new file mode 100644 index 000000000..35beb756d --- /dev/null +++ b/src/pipeline/PipelineStateApi.cpp @@ -0,0 +1,271 @@ +#include "depthai/pipeline/PipelineStateApi.hpp" + +#include "depthai/pipeline/InputQueue.hpp" + +namespace dai { + +PipelineState NodesStateApi::summary() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.events = false; + nodeCfg.inputs = {}; // Do not send any + nodeCfg.outputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + cfg.nodes.push_back(nodeCfg); + } + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + return *state; +} +PipelineState NodesStateApi::detailed() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + } + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + return *state; +} +std::unordered_map> NodesStateApi::outputs() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.inputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + } + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + std::unordered_map> result; + for(auto& [nodeId, nodeState] : state->nodeStates) { + result[nodeId] = nodeState.outputStates; + } + return result; +} +std::unordered_map> NodesStateApi::inputs() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.outputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + } + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + std::unordered_map> result; + for(auto& [nodeId, nodeState] : state->nodeStates) { + result[nodeId] = nodeState.inputStates; + } + return result; +} +std::unordered_map> NodesStateApi::otherTimings() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.inputs = {}; // Do not send any + nodeCfg.outputs = {}; // Do not send any + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + } + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + std::unordered_map> result; + for(auto& [nodeId, nodeState] : state->nodeStates) { + result[nodeId] = nodeState.otherTimings; + } + return result; +} + +std::unordered_map NodeStateApi::outputs(const std::vector& outputNames) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.outputs = outputNames; + nodeCfg.inputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + std::unordered_map result; + for(const auto& outputName : outputNames) { + result[outputName] = state->nodeStates[nodeId].outputStates[outputName]; + } + return result; +} +NodeState::OutputQueueState NodeStateApi::outputs(const std::string& outputName) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.outputs = {outputName}; + nodeCfg.inputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + if(state->nodeStates[nodeId].outputStates.find(outputName) == state->nodeStates[nodeId].outputStates.end()) { + throw std::runtime_error("Output name " + outputName + " not found in NodeState for node ID " + std::to_string(nodeId)); + } + return state->nodeStates[nodeId].outputStates[outputName]; +} +std::vector NodeStateApi::events() { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.outputs = {}; // Do not send any + nodeCfg.inputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = true; + cfg.nodes.push_back(nodeCfg); + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + return state->nodeStates[nodeId].events; +} +std::unordered_map NodeStateApi::inputs(const std::vector& inputNames) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.inputs = inputNames; + nodeCfg.outputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + std::unordered_map result; + for(const auto& inputName : inputNames) { + result[inputName] = state->nodeStates[nodeId].inputStates[inputName]; + } + return result; +} +NodeState::InputQueueState NodeStateApi::inputs(const std::string& inputName) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.inputs = {inputName}; + nodeCfg.outputs = {}; // Do not send any + nodeCfg.others = {}; // Do not send any + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + if(state->nodeStates[nodeId].inputStates.find(inputName) == state->nodeStates[nodeId].inputStates.end()) { + throw std::runtime_error("Input name " + inputName + " not found in NodeState for node ID " + std::to_string(nodeId)); + } + return state->nodeStates[nodeId].inputStates[inputName]; +} +std::unordered_map NodeStateApi::otherTimings(const std::vector& statNames) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.others = statNames; + nodeCfg.outputs = {}; // Do not send any + nodeCfg.inputs = {}; // Do not send any + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + std::unordered_map result; + for(const auto& otherName : statNames) { + result[otherName] = state->nodeStates[nodeId].otherTimings[otherName]; + } + return result; +} +NodeState::Timing NodeStateApi::otherStats(const std::string& statName) { + PipelineEventAggregationConfig cfg; + cfg.repeat = false; + cfg.setTimestamp(std::chrono::steady_clock::now()); + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = nodeId; + nodeCfg.others = {statName}; + nodeCfg.outputs = {}; // Do not send any + nodeCfg.inputs = {}; // Do not send any + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + + pipelineStateRequest->send(std::make_shared(cfg)); + auto state = pipelineStateOut->get(); + if(!state) throw std::runtime_error("Failed to get PipelineState"); + if(state->nodeStates.find(nodeId) == state->nodeStates.end()) { + throw std::runtime_error("Node ID " + std::to_string(nodeId) + " not found in PipelineState"); + } + if(state->nodeStates[nodeId].otherTimings.find(statName) == state->nodeStates[nodeId].otherTimings.end()) { + throw std::runtime_error("Stat name " + statName + " not found in NodeState for node ID " + std::to_string(nodeId)); + } + return state->nodeStates[nodeId].otherTimings[statName]; +} + +} // namespace dai From 66e4fcdee5777edb00788927490577bb363a908c Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 10 Oct 2025 14:52:25 +0200 Subject: [PATCH 038/124] Add mutex lock to pipeline event dispatcher --- .../depthai/utility/PipelineEventDispatcher.hpp | 2 ++ src/utility/PipelineEventDispatcher.cpp | 14 ++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index 0908cead6..6a0595f6a 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -25,6 +25,8 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void checkNodeId(); uint32_t sequenceNum = 0; + + std::mutex mutex; public: PipelineEventDispatcher() = delete; diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index 4a16c4506..e0a9e79bf 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -41,11 +41,12 @@ void PipelineEventDispatcher::setNodeId(int64_t id) { nodeId = id; } void PipelineEventDispatcher::startEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize) { - // TODO add mutex if(!sendEvents) return; checkNodeId(); if(blacklist(type, source)) return; + std::lock_guard lock(mutex); + auto& event = events[makeKey(type, source)]; event.event.setTimestamp(std::chrono::steady_clock::now()); event.event.tsDevice = event.event.ts; @@ -71,11 +72,12 @@ void PipelineEventDispatcher::startCustomEvent(const std::string& source) { startEvent(PipelineEvent::Type::CUSTOM, source, std::nullopt); } void PipelineEventDispatcher::endEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize) { - // TODO add mutex if(!sendEvents) return; checkNodeId(); if(blacklist(type, source)) return; + std::lock_guard lock(mutex); + auto now = std::chrono::steady_clock::now(); auto& event = events[makeKey(type, source)]; @@ -108,11 +110,12 @@ void PipelineEventDispatcher::endCustomEvent(const std::string& source) { endEvent(PipelineEvent::Type::CUSTOM, source, std::nullopt); } void PipelineEventDispatcher::pingEvent(PipelineEvent::Type type, const std::string& source) { - // TODO add mutex if(!sendEvents) return; checkNodeId(); if(blacklist(type, source)) return; + std::lock_guard lock(mutex); + auto now = std::chrono::steady_clock::now(); auto& event = events[makeKey(type, source)]; @@ -136,6 +139,8 @@ void PipelineEventDispatcher::pingTrackedEvent(PipelineEvent::Type type, const s checkNodeId(); if(blacklist(type, source)) return; + std::lock_guard lock(mutex); + auto now = std::chrono::steady_clock::now(); auto event = std::make_shared(); @@ -158,11 +163,12 @@ void PipelineEventDispatcher::pingCustomEvent(const std::string& source) { pingEvent(PipelineEvent::Type::CUSTOM, source); } void PipelineEventDispatcher::pingInputEvent(const std::string& source, int32_t status, std::optional queueSize) { - // TODO add mutex if(!sendEvents) return; checkNodeId(); if(blacklist(PipelineEvent::Type::INPUT, source)) return; + std::lock_guard lock(mutex); + auto now = std::chrono::steady_clock::now(); auto& event = events[makeKey(PipelineEvent::Type::INPUT, source)]; From 75592f7d03a9bac5b49cb4e7650a19fcfbc7ad30 Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 13 Oct 2025 16:06:32 +0200 Subject: [PATCH 039/124] Fix docstrings errors --- .../src/pipeline/datatype/PipelineEventBindings.cpp | 6 +++--- .../src/pipeline/datatype/PipelineStateBindings.cpp | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp index f3437db59..23435044f 100644 --- a/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineEventBindings.cpp @@ -53,7 +53,7 @@ void bind_pipelineevent(pybind11::module& m, void* pCallstack) { .def("getTimestamp", &PipelineEvent::Buffer::getTimestamp, DOC(dai, Buffer, getTimestamp)) .def("getTimestampDevice", &PipelineEvent::Buffer::getTimestampDevice, DOC(dai, Buffer, getTimestampDevice)) .def("getSequenceNum", &PipelineEvent::Buffer::getSequenceNum, DOC(dai, Buffer, getSequenceNum)) - .def("setTimestamp", &PipelineEvent::setTimestamp, DOC(dai, PipelineEvent, setTimestamp)) - .def("setTimestampDevice", &PipelineEvent::setTimestampDevice, DOC(dai, PipelineEvent, setTimestampDevice)) - .def("setSequenceNum", &PipelineEvent::setSequenceNum, DOC(dai, PipelineEvent, setSequenceNum)); + .def("setTimestamp", &PipelineEvent::setTimestamp, DOC(dai, Buffer, setTimestamp)) + .def("setTimestampDevice", &PipelineEvent::setTimestampDevice, DOC(dai, Buffer, setTimestampDevice)) + .def("setSequenceNum", &PipelineEvent::setSequenceNum, DOC(dai, Buffer, setSequenceNum)); } diff --git a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp index 21e560891..bb42da24b 100644 --- a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp @@ -52,7 +52,7 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { durationEvent.def(py::init<>()) .def("__repr__", &NodeState::DurationEvent::str) .def_readwrite("startEvent", &NodeState::DurationEvent::startEvent, DOC(dai, NodeState, DurationEvent, startEvent)) - .def_readwrite("durationUs", &NodeState::DurationEvent::durationUs, DOC(dai, NodeState, TimingStats, durationUs)); + .def_readwrite("durationUs", &NodeState::DurationEvent::durationUs, DOC(dai, NodeState, DurationEvent, durationUs)); nodeStateTimingStats.def(py::init<>()) .def("__repr__", &NodeState::TimingStats::str) @@ -106,7 +106,7 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { .def("getTimestamp", &PipelineState::Buffer::getTimestamp, DOC(dai, Buffer, getTimestamp)) .def("getTimestampDevice", &PipelineState::Buffer::getTimestampDevice, DOC(dai, Buffer, getTimestampDevice)) .def("getSequenceNum", &PipelineState::Buffer::getSequenceNum, DOC(dai, Buffer, getSequenceNum)) - .def("setTimestamp", &PipelineState::setTimestamp, DOC(dai, PipelineState, setTimestamp)) - .def("setTimestampDevice", &PipelineState::setTimestampDevice, DOC(dai, PipelineState, setTimestampDevice)) - .def("setSequenceNum", &PipelineState::setSequenceNum, DOC(dai, PipelineState, setSequenceNum)); + .def("setTimestamp", &PipelineState::setTimestamp, DOC(dai, Buffer, setTimestamp)) + .def("setTimestampDevice", &PipelineState::setTimestampDevice, DOC(dai, Buffer, setTimestampDevice)) + .def("setSequenceNum", &PipelineState::setSequenceNum, DOC(dai, Buffer, setSequenceNum)); } From 4cef1e6a7231ee19a0eae6835348eb022f8da84f Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 16 Oct 2025 16:29:22 +0200 Subject: [PATCH 040/124] Disable pipeline events --- src/pipeline/Pipeline.cpp | 90 ++++++++++++++++++++----------------- src/utility/Environment.hpp | 7 +-- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 9ed64bdee..b3e1157e2 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -659,51 +659,59 @@ void PipelineImpl::build() { } // Create pipeline event aggregator node and link - // Check if any nodes are on host or device - bool hasHostNodes = false; - bool hasDeviceNodes = false; - for(const auto& node : getAllNodes()) { - if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; - - if(node->runOnHost()) { - hasHostNodes = true; - } else { - hasDeviceNodes = true; + bool enablePipelineDebugging = utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false); + if(enablePipelineDebugging) { + // Check if any nodes are on host or device + bool hasHostNodes = false; + bool hasDeviceNodes = false; + for(const auto& node : getAllNodes()) { + if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; + + if(node->runOnHost()) { + hasHostNodes = true; + } else { + hasDeviceNodes = true; + } } - } - std::shared_ptr hostEventAgg = nullptr; - std::shared_ptr deviceEventAgg = nullptr; - if(hasHostNodes) { - hostEventAgg = parent.create(); - hostEventAgg->setRunOnHost(true); - } - if(hasDeviceNodes) { - deviceEventAgg = parent.create(); - deviceEventAgg->setRunOnHost(false); - } - for(auto& node : getAllNodes()) { - if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; - - auto threadedNode = std::dynamic_pointer_cast(node); - if(threadedNode) { - if(node->runOnHost() && hostEventAgg && node->id != hostEventAgg->id) { - threadedNode->pipelineEventOutput.link(hostEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); - } else if(!node->runOnHost() && deviceEventAgg && node->id != deviceEventAgg->id) { - threadedNode->pipelineEventOutput.link(deviceEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); + std::shared_ptr hostEventAgg = nullptr; + std::shared_ptr deviceEventAgg = nullptr; + if(hasHostNodes) { + hostEventAgg = parent.create(); + hostEventAgg->setRunOnHost(true); + } + if(hasDeviceNodes) { + deviceEventAgg = parent.create(); + deviceEventAgg->setRunOnHost(false); + } + for(auto& node : getAllNodes()) { + if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; + + auto threadedNode = std::dynamic_pointer_cast(node); + if(threadedNode) { + if(node->runOnHost() && hostEventAgg && node->id != hostEventAgg->id) { + threadedNode->pipelineEventOutput.link(hostEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); + } else if(!node->runOnHost() && deviceEventAgg && node->id != deviceEventAgg->id) { + threadedNode->pipelineEventOutput.link(deviceEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); + } } } + auto stateMerge = parent.create()->build(hasDeviceNodes, hasHostNodes); + if(deviceEventAgg) { + deviceEventAgg->out.link(stateMerge->inputDevice); + stateMerge->outRequest.link(deviceEventAgg->request); + } + if(hostEventAgg) { + hostEventAgg->out.link(stateMerge->inputHost); + stateMerge->outRequest.link(hostEventAgg->request); + } + pipelineStateOut = stateMerge->out.createOutputQueue(1, false); + pipelineStateRequest = stateMerge->request.createInputQueue(); } - auto stateMerge = parent.create()->build(hasDeviceNodes, hasHostNodes); - if(deviceEventAgg) { - deviceEventAgg->out.link(stateMerge->inputDevice); - stateMerge->outRequest.link(deviceEventAgg->request); - } - if(hostEventAgg) { - hostEventAgg->out.link(stateMerge->inputHost); - stateMerge->outRequest.link(hostEventAgg->request); - } - pipelineStateOut = stateMerge->out.createOutputQueue(1, false); - pipelineStateRequest = stateMerge->request.createInputQueue(); + } + + if(std::find_if(getAllNodes().begin(), getAllNodes().end(), [](const std::shared_ptr& n) { return strcmp(n->getName(), "PipelineEventAggregation") == 0; }) + == getAllNodes().end()) { + for(auto& node : getAllNodes()) node->pipelineEventDispatcher->sendEvents = false; } isBuild = true; diff --git a/src/utility/Environment.hpp b/src/utility/Environment.hpp index 07f56ec5d..4a0e90099 100644 --- a/src/utility/Environment.hpp +++ b/src/utility/Environment.hpp @@ -97,14 +97,15 @@ T getEnvAs(const std::string& var, T defaultValue, spdlog::logger& logger, bool // bool else if constexpr(std::is_same_v) { - if(value == "1" || value == "true" || value == "TRUE" || value == "True") { + std::transform(value.begin(), value.end(), value.begin(), ::tolower); + if(value == "1" || value == "true" || value == "t" || value == "on") { returnValue = true; - } else if(value == "0" || value == "false" || value == "FALSE" || value == "False") { + } else if(value == "0" || value == "false" || value == "f" || value == "off") { returnValue = false; } else { std::ostringstream message; message << "Failed to convert environment variable " << var << " from '" << value << "' to type " << typeid(T).name(); - message << ". Possible values are '1', 'true', 'TRUE', 'True', '0', 'false', 'FALSE', 'False'"; + message << ". Possible values are '1', 'true', 'TRUE', 'True', 'T', 'ON', '0', 'false', 'FALSE', 'False', 'F', 'OFF'"; throw std::runtime_error(message.str()); } } From bb92f4f9d506a2dd28f9a85d5beb1a2c876f152e Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 17 Oct 2025 13:26:21 +0200 Subject: [PATCH 041/124] Reduce pipeline state stat calculation, add ability to disable sending pipeline debug events, bugfixes --- .../python/src/pipeline/PipelineBindings.cpp | 3 +- .../node/PipelineEventAggregationBindings.cpp | 24 ++-- .../PipelineEventAggregationProperties.hpp | 4 +- src/pipeline/Pipeline.cpp | 19 ++- src/pipeline/node/Sync.cpp | 9 +- .../internal/PipelineEventAggregation.cpp | 126 +++++++++--------- 6 files changed, 103 insertions(+), 82 deletions(-) diff --git a/bindings/python/src/pipeline/PipelineBindings.cpp b/bindings/python/src/pipeline/PipelineBindings.cpp index f2b1e6ee8..17b4472bf 100644 --- a/bindings/python/src/pipeline/PipelineBindings.cpp +++ b/bindings/python/src/pipeline/PipelineBindings.cpp @@ -311,6 +311,7 @@ void PipelineBindings::bind(pybind11::module& m, void* pCallstack) { .def("isRunning", &Pipeline::isRunning) .def("processTasks", &Pipeline::processTasks, py::arg("waitForTasks") = false, py::arg("timeoutSeconds") = -1.0) .def("enableHolisticRecord", &Pipeline::enableHolisticRecord, py::arg("recordConfig"), DOC(dai, Pipeline, enableHolisticRecord)) - .def("enableHolisticReplay", &Pipeline::enableHolisticReplay, py::arg("recordingPath"), DOC(dai, Pipeline, enableHolisticReplay)); + .def("enableHolisticReplay", &Pipeline::enableHolisticReplay, py::arg("recordingPath"), DOC(dai, Pipeline, enableHolisticReplay)) + .def("getPipelineState", &Pipeline::getPipelineState, DOC(dai, Pipeline, getPipelineState)); ; } diff --git a/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp index aa52d802a..3f8fa2747 100644 --- a/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp +++ b/bindings/python/src/pipeline/node/PipelineEventAggregationBindings.cpp @@ -11,18 +11,18 @@ void bind_pipelineeventaggregation(pybind11::module& m, void* pCallstack) { // m, "PipelineEventAggregationProperties", DOC(dai, PipelineEventAggregationProperties)); // auto pipelineEventAggregation = ADD_NODE(PipelineEventAggregation); // - // /////////////////////////////////////////////////////////////////////// - // /////////////////////////////////////////////////////////////////////// - // /////////////////////////////////////////////////////////////////////// - // // Call the rest of the type defines, then perform the actual bindings - // Callstack* callstack = (Callstack*)pCallstack; - // auto cb = callstack->top(); - // callstack->pop(); - // cb(m, pCallstack); - // // Actual bindings - // /////////////////////////////////////////////////////////////////////// - // /////////////////////////////////////////////////////////////////////// - // /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + // Call the rest of the type defines, then perform the actual bindings + Callstack* callstack = (Callstack*)pCallstack; + auto cb = callstack->top(); + callstack->pop(); + cb(m, pCallstack); + // Actual bindings + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////// // // // Properties // pipelineEventAggregationProperties.def_readwrite("aggregationWindowSize", &PipelineEventAggregationProperties::aggregationWindowSize) diff --git a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp index 013dd29bf..708c0cb3f 100644 --- a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp +++ b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp @@ -9,10 +9,10 @@ namespace dai { */ struct PipelineEventAggregationProperties : PropertiesSerializable { uint32_t aggregationWindowSize = 100; - uint32_t eventBatchSize = 50; + uint32_t statsUpdateIntervalMs = 1000; uint32_t eventWaitWindow = 16; }; -DEPTHAI_SERIALIZE_EXT(PipelineEventAggregationProperties, aggregationWindowSize, eventBatchSize, eventWaitWindow); +DEPTHAI_SERIALIZE_EXT(PipelineEventAggregationProperties, aggregationWindowSize, statsUpdateIntervalMs, eventWaitWindow); } // namespace dai diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index b3e1157e2..40ec95904 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -506,6 +506,16 @@ bool PipelineImpl::isDeviceOnly() const { } PipelineStateApi PipelineImpl::getPipelineState() { + bool hasPipelineMergeNode = false; + for(const auto& node : getAllNodes()) { + if(strcmp(node->getName(), "PipelineStateMerge") == 0) { + hasPipelineMergeNode = true; + break; + } + } + if(!hasPipelineMergeNode) { + throw std::runtime_error("Pipeline debugging disabled. Cannot get pipeline state."); + } return PipelineStateApi(pipelineStateOut, pipelineStateRequest, getAllNodes()); } @@ -709,9 +719,12 @@ void PipelineImpl::build() { } } - if(std::find_if(getAllNodes().begin(), getAllNodes().end(), [](const std::shared_ptr& n) { return strcmp(n->getName(), "PipelineEventAggregation") == 0; }) - == getAllNodes().end()) { - for(auto& node : getAllNodes()) node->pipelineEventDispatcher->sendEvents = false; + { + auto allNodes = getAllNodes(); + if(std::find_if(allNodes.begin(), allNodes.end(), [](const std::shared_ptr& n) { return strcmp(n->getName(), "PipelineEventAggregation") == 0; }) + == allNodes.end()) { + for(auto& node : allNodes) node->pipelineEventDispatcher->sendEvents = false; + } } isBuild = true; diff --git a/src/pipeline/node/Sync.cpp b/src/pipeline/node/Sync.cpp index c22f82af1..b682bec4b 100644 --- a/src/pipeline/node/Sync.cpp +++ b/src/pipeline/node/Sync.cpp @@ -78,10 +78,11 @@ void Sync::run() { } } if(attempts > properties.syncAttempts && properties.syncAttempts != -1) { - logger->warn( - "Sync node has been trying to sync for {} messages, but the messages are still not in sync. " - "The node will send the messages anyway.", - attempts); + if(properties.syncAttempts != 0) + logger->warn( + "Sync node has been trying to sync for {} messages, but the messages are still not in sync. " + "The node will send the messages anyway.", + attempts); break; } // Find a minimum timestamp diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 31243a82c..c8c881e24 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -23,12 +23,12 @@ class NodeEventAggregation { std::shared_ptr logger; uint32_t windowSize; - uint32_t eventBatchSize; + uint32_t statsUpdateIntervalMs; uint32_t eventWaitWindow; public: - NodeEventAggregation(uint32_t windowSize, uint32_t eventBatchSize, uint32_t eventWaitWindow, std::shared_ptr logger) - : logger(logger), windowSize(windowSize), eventBatchSize(eventBatchSize), eventWaitWindow(eventWaitWindow), eventsBuffer(windowSize) {} + NodeEventAggregation(uint32_t windowSize, uint32_t statsUpdateIntervalMs, uint32_t eventWaitWindow, std::shared_ptr logger) + : logger(logger), windowSize(windowSize), statsUpdateIntervalMs(statsUpdateIntervalMs), eventWaitWindow(eventWaitWindow), eventsBuffer(windowSize) {} NodeState state; utility::CircularBuffer eventsBuffer; std::unordered_map>> inputTimingsBuffers; @@ -53,7 +53,8 @@ class NodeEventAggregation { std::unique_ptr>> ongoingMainLoopEvents; std::unordered_map>>> ongoingOtherEvents; - uint32_t count = 0; + std::chrono::time_point lastUpdated; + std::atomic updated = false; private: inline std::optional* findOngoingEvent(uint32_t sequenceNum, utility::CircularBuffer>& buffer) { @@ -268,9 +269,7 @@ class NodeEventAggregation { using namespace std::chrono; if(event.type == PipelineEvent::Type::INPUT && event.interval == PipelineEvent::Interval::END) { if(event.queueSize.has_value()) { - if(inputQueueSizesBuffers.find(event.source) == inputQueueSizesBuffers.end()) { - inputQueueSizesBuffers[event.source] = std::make_unique>(windowSize); - } + inputQueueSizesBuffers.try_emplace(event.source, std::make_unique>(windowSize)); inputQueueSizesBuffers[event.source]->add(*event.queueSize); } else { throw std::runtime_error(fmt::format("INPUT END event must have queue size set source: {}, node {}", event.source, event.nodeId)); @@ -320,47 +319,51 @@ class NodeEventAggregation { } else if(event.interval != PipelineEvent::Interval::NONE) { addedEvent = updateIntervalBuffers(event); } - if(addedEvent /* && ++count % eventBatchSize == 0 */) { // TODO - // By instance - switch(event.type) { - case PipelineEvent::Type::CUSTOM: - updateTimingStats(state.otherTimings[event.source].durationStats, *otherTimingsBuffers[event.source]); - updateFpsStats(state.otherTimings[event.source], *otherFpsBuffers[event.source]); - break; - case PipelineEvent::Type::LOOP: - updateTimingStats(state.mainLoopTiming.durationStats, *mainLoopTimingsBuffer); - state.mainLoopTiming.fps = 1e6f / (float)state.mainLoopTiming.durationStats.averageMicrosRecent; - break; - case PipelineEvent::Type::INPUT: - updateTimingStats(state.inputStates[event.source].timing.durationStats, *inputTimingsBuffers[event.source]); - updateFpsStats(state.inputStates[event.source].timing, *inputFpsBuffers[event.source]); - break; - case PipelineEvent::Type::OUTPUT: - updateTimingStats(state.outputStates[event.source].timing.durationStats, *outputTimingsBuffers[event.source]); - updateFpsStats(state.outputStates[event.source].timing, *outputFpsBuffers[event.source]); - break; - case PipelineEvent::Type::INPUT_BLOCK: - updateTimingStats(state.inputsGetTiming.durationStats, *inputsGetTimingsBuffer); - updateFpsStats(state.inputsGetTiming, *inputsGetFpsBuffer); - break; - case PipelineEvent::Type::OUTPUT_BLOCK: - updateTimingStats(state.outputsSendTiming.durationStats, *outputsSendTimingsBuffer); - updateFpsStats(state.outputsSendTiming, *outputsSendFpsBuffer); - break; - } - } - if(event.type == PipelineEvent::Type::INPUT && event.interval == PipelineEvent::Interval::END /* && ++count % eventBatchSize == 0 */) { // TODO - auto& qStats = state.inputStates[event.source].queueStats; - auto& qBuffer = *inputQueueSizesBuffers[event.source]; - qStats.maxQueued = std::max(qStats.maxQueued, *event.queueSize); - auto qBufferData = qBuffer.getBuffer(); - std::sort(qBufferData.begin(), qBufferData.end()); - qStats.minQueuedRecent = qBufferData.front(); - qStats.maxQueuedRecent = qBufferData.back(); - qStats.medianQueuedRecent = qBufferData[qBufferData.size() / 2]; - if(qBufferData.size() % 2 == 0) { - qStats.medianQueuedRecent = (qStats.medianQueuedRecent + qBufferData[qBufferData.size() / 2 - 1]) / 2; + if(addedEvent && std::chrono::steady_clock::now() - lastUpdated >= std::chrono::milliseconds(statsUpdateIntervalMs)) { + lastUpdated = std::chrono::steady_clock::now(); + for(int i = (int)PipelineEvent::Type::CUSTOM; i <= (int)PipelineEvent::Type::OUTPUT_BLOCK; ++i) { + // By instance + switch(event.type) { + case PipelineEvent::Type::CUSTOM: + updateTimingStats(state.otherTimings[event.source].durationStats, *otherTimingsBuffers[event.source]); + updateFpsStats(state.otherTimings[event.source], *otherFpsBuffers[event.source]); + break; + case PipelineEvent::Type::LOOP: + updateTimingStats(state.mainLoopTiming.durationStats, *mainLoopTimingsBuffer); + state.mainLoopTiming.fps = 1e6f / (float)state.mainLoopTiming.durationStats.averageMicrosRecent; + break; + case PipelineEvent::Type::INPUT: + updateTimingStats(state.inputStates[event.source].timing.durationStats, *inputTimingsBuffers[event.source]); + updateFpsStats(state.inputStates[event.source].timing, *inputFpsBuffers[event.source]); + { + auto& qStats = state.inputStates[event.source].queueStats; + auto& qBuffer = *inputQueueSizesBuffers[event.source]; + qStats.maxQueued = std::max(qStats.maxQueued, *event.queueSize); + auto qBufferData = qBuffer.getBuffer(); + std::sort(qBufferData.begin(), qBufferData.end()); + qStats.minQueuedRecent = qBufferData.front(); + qStats.maxQueuedRecent = qBufferData.back(); + qStats.medianQueuedRecent = qBufferData[qBufferData.size() / 2]; + if(qBufferData.size() % 2 == 0) { + qStats.medianQueuedRecent = (qStats.medianQueuedRecent + qBufferData[qBufferData.size() / 2 - 1]) / 2; + } + } + break; + case PipelineEvent::Type::OUTPUT: + updateTimingStats(state.outputStates[event.source].timing.durationStats, *outputTimingsBuffers[event.source]); + updateFpsStats(state.outputStates[event.source].timing, *outputFpsBuffers[event.source]); + break; + case PipelineEvent::Type::INPUT_BLOCK: + updateTimingStats(state.inputsGetTiming.durationStats, *inputsGetTimingsBuffer); + updateFpsStats(state.inputsGetTiming, *inputsGetFpsBuffer); + break; + case PipelineEvent::Type::OUTPUT_BLOCK: + updateTimingStats(state.outputsSendTiming.durationStats, *outputsSendTimingsBuffer); + updateFpsStats(state.outputsSendTiming, *outputsSendFpsBuffer); + break; + } } + updated = true; } } }; @@ -369,7 +372,7 @@ class PipelineEventHandler { std::unordered_map nodeStates; Node::InputMap* inputs; uint32_t aggregationWindowSize; - uint32_t eventBatchSize; + uint32_t statsUpdateIntervalMs; uint32_t eventWaitWindow; std::atomic running; @@ -381,9 +384,16 @@ class PipelineEventHandler { std::shared_mutex mutex; public: - PipelineEventHandler( - Node::InputMap* inputs, uint32_t aggregationWindowSize, uint32_t eventBatchSize, uint32_t eventWaitWindow, std::shared_ptr logger) - : inputs(inputs), aggregationWindowSize(aggregationWindowSize), eventBatchSize(eventBatchSize), eventWaitWindow(eventWaitWindow), logger(logger) {} + PipelineEventHandler(Node::InputMap* inputs, + uint32_t aggregationWindowSize, + uint32_t statsUpdateIntervalMs, + uint32_t eventWaitWindow, + std::shared_ptr logger) + : inputs(inputs), + aggregationWindowSize(aggregationWindowSize), + statsUpdateIntervalMs(statsUpdateIntervalMs), + eventWaitWindow(eventWaitWindow), + logger(logger) {} void threadedRun() { while(running) { std::unordered_map> events; @@ -394,13 +404,9 @@ class PipelineEventHandler { } for(auto& [k, event] : events) { if(event != nullptr) { - if(nodeStates.find(event->nodeId) == nodeStates.end()) { - nodeStates.insert_or_assign(event->nodeId, NodeEventAggregation(aggregationWindowSize, eventBatchSize, eventWaitWindow, logger)); - } - { - std::unique_lock lock(mutex); - nodeStates.at(event->nodeId).add(*event); - } + std::unique_lock lock(mutex); + nodeStates.try_emplace(event->nodeId, aggregationWindowSize, statsUpdateIntervalMs, eventWaitWindow, logger); + nodeStates.at(event->nodeId).add(*event); } } if(!gotEvents) { @@ -422,7 +428,7 @@ class PipelineEventHandler { for(auto& [nodeId, nodeState] : nodeStates) { outState->nodeStates[nodeId] = nodeState.state; if(sendEvents) outState->nodeStates[nodeId].events = nodeState.eventsBuffer.getBuffer(); - if(nodeState.count % eventBatchSize == 0) updated = true; + if(nodeState.updated.exchange(false)) updated = true; } return updated; } @@ -442,7 +448,7 @@ bool PipelineEventAggregation::runOnHost() const { void PipelineEventAggregation::run() { auto& logger = pimpl->logger; - PipelineEventHandler handler(&inputs, properties.aggregationWindowSize, properties.eventBatchSize, properties.eventWaitWindow, logger); + PipelineEventHandler handler(&inputs, properties.aggregationWindowSize, properties.statsUpdateIntervalMs, properties.eventWaitWindow, logger); handler.run(); std::optional currentConfig; From a13c0efe8bc6167e53ecd486b9d7ecddc24a8f9a Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 17 Oct 2025 13:28:02 +0200 Subject: [PATCH 042/124] RVC4 FW: update core with pipeline debugging improvemets --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 380cdd5e9..b08d0e439 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+e19591198e276d1580a98f77b3387c1abc881042") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+bdf2d2353503a794df5db7c8937a05ef2b106de2") From b3e34dc6e52bb0945627b6f868a46ddb03b32fbb Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 20 Oct 2025 17:32:56 +0200 Subject: [PATCH 043/124] Bugfixes, add block events for host runnable nodes --- .../datatype/PipelineStateBindings.cpp | 9 +- .../PipelineEventDispatcherBindings.cpp | 4 +- include/depthai/pipeline/ThreadedNode.hpp | 4 +- .../pipeline/datatype/PipelineState.hpp | 15 +++ .../utility/PipelineEventDispatcher.hpp | 4 +- .../PipelineEventDispatcherInterface.hpp | 4 +- src/pipeline/ThreadedNode.cpp | 10 +- src/pipeline/node/AprilTag.cpp | 51 +++++--- src/pipeline/node/BenchmarkIn.cpp | 27 ++-- src/pipeline/node/BenchmarkOut.cpp | 21 ++- src/pipeline/node/DynamicCalibrationNode.cpp | 6 +- src/pipeline/node/ImageAlign.cpp | 54 ++++---- src/pipeline/node/ImageFilters.cpp | 84 +++++++----- src/pipeline/node/ObjectTracker.cpp | 68 +++++----- src/pipeline/node/Sync.cpp | 120 ++++++++++-------- src/pipeline/node/host/Display.cpp | 6 +- src/pipeline/node/host/RGBD.cpp | 35 +++-- src/pipeline/node/host/Record.cpp | 12 +- src/pipeline/node/host/Replay.cpp | 10 +- src/utility/PipelineEventDispatcher.cpp | 11 +- 20 files changed, 342 insertions(+), 213 deletions(-) diff --git a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp index bb42da24b..328fa9cb7 100644 --- a/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp +++ b/bindings/python/src/pipeline/datatype/PipelineStateBindings.cpp @@ -67,7 +67,8 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { nodeStateTiming.def(py::init<>()) .def("__repr__", &NodeState::Timing::str) .def_readwrite("fps", &NodeState::Timing::fps, DOC(dai, NodeState, Timing, fps)) - .def_readwrite("durationStats", &NodeState::Timing::durationStats, DOC(dai, NodeState, Timing, durationStats)); + .def_readwrite("durationStats", &NodeState::Timing::durationStats, DOC(dai, NodeState, Timing, durationStats)) + .def("isValid", &NodeState::Timing::isValid, DOC(dai, NodeState, Timing, isValid)); nodeStateQueueStats.def(py::init<>()) .def("__repr__", &NodeState::QueueStats::str) @@ -81,12 +82,14 @@ void bind_pipelinestate(pybind11::module& m, void* pCallstack) { .def_readwrite("state", &NodeState::InputQueueState::state, DOC(dai, NodeState, InputQueueState, state)) .def_readwrite("numQueued", &NodeState::InputQueueState::numQueued, DOC(dai, NodeState, InputQueueState, numQueued)) .def_readwrite("timing", &NodeState::InputQueueState::timing, DOC(dai, NodeState, InputQueueState, timing)) - .def_readwrite("queueStats", &NodeState::InputQueueState::queueStats, DOC(dai, NodeState, InputQueueState, queueStats)); + .def_readwrite("queueStats", &NodeState::InputQueueState::queueStats, DOC(dai, NodeState, InputQueueState, queueStats)) + .def("isValid", &NodeState::InputQueueState::isValid, DOC(dai, NodeState, InputQueueState, isValid)); nodeStateOutputQueueState.def(py::init<>()) .def("__repr__", &NodeState::OutputQueueState::str) .def_readwrite("state", &NodeState::OutputQueueState::state, DOC(dai, NodeState, OutputQueueState, state)) - .def_readwrite("timing", &NodeState::OutputQueueState::timing, DOC(dai, NodeState, OutputQueueState, timing)); + .def_readwrite("timing", &NodeState::OutputQueueState::timing, DOC(dai, NodeState, OutputQueueState, timing)) + .def("isValid", &NodeState::OutputQueueState::isValid, DOC(dai, NodeState, OutputQueueState, isValid)); nodeState.def(py::init<>()) .def("__repr__", &NodeState::str) diff --git a/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp index 5fccc0505..cc786ceaf 100644 --- a/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp +++ b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp @@ -24,7 +24,7 @@ void PipelineEventDispatcherBindings::bind(pybind11::module& m, void* pCallstack .def("startCustomEvent", &PipelineEventDispatcher::startCustomEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, startCustomEvent)) .def("endCustomEvent", &PipelineEventDispatcher::endCustomEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, endCustomEvent)) .def("pingCustomEvent", &PipelineEventDispatcher::pingCustomEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, pingCustomEvent)) - .def("inputBlockEvent", &PipelineEventDispatcher::inputBlockEvent, py::arg("source") = "defaultInputGroup", DOC(dai, utility, PipelineEventDispatcher, inputBlockEvent)) - .def("outputBlockEvent", &PipelineEventDispatcher::outputBlockEvent, py::arg("source") = "defaultOutputGroup", DOC(dai, utility, PipelineEventDispatcher, outputBlockEvent)) + .def("inputBlockEvent", &PipelineEventDispatcher::inputBlockEvent, DOC(dai, utility, PipelineEventDispatcher, inputBlockEvent)) + .def("outputBlockEvent", &PipelineEventDispatcher::outputBlockEvent, DOC(dai, utility, PipelineEventDispatcher, outputBlockEvent)) .def("customBlockEvent", &PipelineEventDispatcher::customBlockEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, customBlockEvent)); } diff --git a/include/depthai/pipeline/ThreadedNode.hpp b/include/depthai/pipeline/ThreadedNode.hpp index d5a2147b0..854f38005 100644 --- a/include/depthai/pipeline/ThreadedNode.hpp +++ b/include/depthai/pipeline/ThreadedNode.hpp @@ -68,8 +68,8 @@ class ThreadedNode : public Node { */ virtual dai::LogLevel getLogLevel() const; - utility::PipelineEventDispatcherInterface::BlockPipelineEvent inputBlockEvent(const std::string& source = "defaultInputGroup"); - utility::PipelineEventDispatcherInterface::BlockPipelineEvent outputBlockEvent(const std::string& source = "defaultOutputGroup"); + utility::PipelineEventDispatcherInterface::BlockPipelineEvent inputBlockEvent(); + utility::PipelineEventDispatcherInterface::BlockPipelineEvent outputBlockEvent(); utility::PipelineEventDispatcherInterface::BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source); class Impl; diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index ea73c38a5..924f59d9e 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -27,6 +27,11 @@ class NodeState { struct Timing { float fps; TimingStats durationStats; + + bool isValid() const { + return durationStats.minMicros <= durationStats.maxMicros; + } + DEPTHAI_SERIALIZE(Timing, fps, durationStats); }; struct QueueStats { @@ -49,6 +54,11 @@ class NodeState { Timing timing; // Queue usage stats QueueStats queueStats; + + bool isValid() const { + return timing.isValid(); + } + DEPTHAI_SERIALIZE(InputQueueState, state, numQueued, timing); }; struct OutputQueueState { @@ -60,6 +70,11 @@ class NodeState { } state = State::IDLE; // Timing info about this output Timing timing; + + bool isValid() const { + return timing.isValid(); + } + DEPTHAI_SERIALIZE(OutputQueueState, state, timing); }; enum class State : std::int32_t { diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index 6a0595f6a..e310644dc 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -50,8 +50,8 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void pingCustomEvent(const std::string& source) override; void pingInputEvent(const std::string& source, int32_t status, std::optional queueSize = std::nullopt) override; BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source) override; - BlockPipelineEvent inputBlockEvent(const std::string& source = "defaultInputGroup") override; - BlockPipelineEvent outputBlockEvent(const std::string& source = "defaultOutputGroup") override; + BlockPipelineEvent inputBlockEvent() override; + BlockPipelineEvent outputBlockEvent() override; BlockPipelineEvent customBlockEvent(const std::string& source) override; }; diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index e50b35537..0c2ad7640 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -43,8 +43,8 @@ class PipelineEventDispatcherInterface { virtual void pingCustomEvent(const std::string& source) = 0; virtual void pingInputEvent(const std::string& source, int32_t status, std::optional queueSize = std::nullopt) = 0; virtual BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source) = 0; - virtual BlockPipelineEvent inputBlockEvent(const std::string& source = "defaultInputGroup") = 0; - virtual BlockPipelineEvent outputBlockEvent(const std::string& source = "defaultOutputGroup") = 0; + virtual BlockPipelineEvent inputBlockEvent() = 0; + virtual BlockPipelineEvent outputBlockEvent() = 0; virtual BlockPipelineEvent customBlockEvent(const std::string& source) = 0; }; diff --git a/src/pipeline/ThreadedNode.cpp b/src/pipeline/ThreadedNode.cpp index b86ad8783..f960e6852 100644 --- a/src/pipeline/ThreadedNode.cpp +++ b/src/pipeline/ThreadedNode.cpp @@ -103,13 +103,11 @@ bool ThreadedNode::mainLoop() { utility::PipelineEventDispatcherInterface::BlockPipelineEvent ThreadedNode::blockEvent(PipelineEvent::Type type, const std::string& source) { return pipelineEventDispatcher->blockEvent(type, source); } -utility::PipelineEventDispatcherInterface::BlockPipelineEvent ThreadedNode::inputBlockEvent(const std::string& source) { - // Just for convenience due to the default source - return blockEvent(PipelineEvent::Type::INPUT_BLOCK, source); +utility::PipelineEventDispatcherInterface::BlockPipelineEvent ThreadedNode::inputBlockEvent() { + return pipelineEventDispatcher->inputBlockEvent(); } -utility::PipelineEventDispatcherInterface::BlockPipelineEvent ThreadedNode::outputBlockEvent(const std::string& source) { - // Just for convenience due to the default source - return blockEvent(PipelineEvent::Type::OUTPUT_BLOCK, source); +utility::PipelineEventDispatcherInterface::BlockPipelineEvent ThreadedNode::outputBlockEvent() { + return pipelineEventDispatcher->outputBlockEvent(); } } // namespace dai diff --git a/src/pipeline/node/AprilTag.cpp b/src/pipeline/node/AprilTag.cpp index 765af8d42..6bc5872c5 100644 --- a/src/pipeline/node/AprilTag.cpp +++ b/src/pipeline/node/AprilTag.cpp @@ -207,30 +207,35 @@ void AprilTag::run() { handleErrors(errno); while(mainLoop()) { - // Retrieve config from user if available - if(properties.inputConfigSync) { - inConfig = inputConfig.get(); - } else { - inConfig = inputConfig.tryGet(); - } - - // Set config if there is one and handle possible errors - if(inConfig != nullptr) { - setDetectorConfig(td.get(), tf, tfamily, *inConfig); - handleErrors(errno); - } - - // Get latest frame - inFrame = inputImage.get(); - // Preallocate data on stack for AprilTag detection int32_t width = 0; int32_t height = 0; int32_t stride = 0; uint8_t* imgbuf = nullptr; + ImgFrame::Type frameType = ImgFrame::Type::NONE; - // Prepare data for AprilTag detection based on input frame type - ImgFrame::Type frameType = inFrame->getType(); + { + auto blockEvent = this->inputBlockEvent(); + + // Retrieve config from user if available + if(properties.inputConfigSync) { + inConfig = inputConfig.get(); + } else { + inConfig = inputConfig.tryGet(); + } + + // Set config if there is one and handle possible errors + if(inConfig != nullptr) { + setDetectorConfig(td.get(), tf, tfamily, *inConfig); + handleErrors(errno); + } + + // Get latest frame + inFrame = inputImage.get(); + + // Prepare data for AprilTag detection based on input frame type + frameType = inFrame->getType(); + } if(frameType == ImgFrame::Type::GRAY8 || frameType == ImgFrame::Type::NV12) { width = static_cast(inFrame->getWidth()); @@ -307,9 +312,13 @@ void AprilTag::run() { aprilTags->setTimestamp(inFrame->getTimestamp()); aprilTags->setTimestampDevice(inFrame->getTimestampDevice()); - // Send detections and pass through input frame - out.send(aprilTags); - passthroughInputImage.send(inFrame); + { + auto blockEvent = this->outputBlockEvent(); + + // Send detections and pass through input frame + out.send(aprilTags); + passthroughInputImage.send(inFrame); + } // Logging logger->trace("Detected {} april tags", zarray_size(detections.get())); diff --git a/src/pipeline/node/BenchmarkIn.cpp b/src/pipeline/node/BenchmarkIn.cpp index 634a0153d..6b6697e10 100644 --- a/src/pipeline/node/BenchmarkIn.cpp +++ b/src/pipeline/node/BenchmarkIn.cpp @@ -56,7 +56,12 @@ void BenchmarkIn::run() { auto start = steady_clock::now(); while(mainLoop()) { - auto inMessage = input.get(); + std::shared_ptr inMessage = nullptr; + std::shared_ptr reportMessage = nullptr; + { + auto blockEvent = this->inputBlockEvent(); + inMessage = input.get(); + } // If this is the first message of the batch, reset counters if(messageCount == 0) { @@ -96,7 +101,7 @@ void BenchmarkIn::run() { auto stop = steady_clock::now(); duration durationS = stop - start; - auto reportMessage = std::make_shared(); + reportMessage = std::make_shared(); reportMessage->numMessagesReceived = numMessages; reportMessage->timeTotal = durationS.count(); reportMessage->fps = numMessages / durationS.count(); @@ -121,16 +126,22 @@ void BenchmarkIn::run() { logFunc("Messages took {} s", reportMessage->timeTotal); logFunc("Average latency: {} s", reportMessage->averageLatency); - // Send out the report - report.send(reportMessage); - logger->trace("Sent report message"); - // Reset for next batch messageCount = 0; } - // Passthrough the message - passthrough.send(inMessage); + { + auto blockEvent = this->outputBlockEvent(); + + if(reportMessage) { + // Send out the report + report.send(reportMessage); + logger->trace("Sent report message"); + } + + // Passthrough the message + passthrough.send(inMessage); + } } } diff --git a/src/pipeline/node/BenchmarkOut.cpp b/src/pipeline/node/BenchmarkOut.cpp index 62cea34b4..89423bff4 100644 --- a/src/pipeline/node/BenchmarkOut.cpp +++ b/src/pipeline/node/BenchmarkOut.cpp @@ -24,8 +24,13 @@ bool BenchmarkOut::runOnHost() const { void BenchmarkOut::run() { using namespace std::chrono; auto& logger = pimpl->logger; - logger->trace("Wait for the input message."); - auto inMessage = input.get(); + std::shared_ptr inMessage = nullptr; + { + auto blockEvent = this->inputBlockEvent(); + + logger->trace("Wait for the input message."); + inMessage = input.get(); + } bool useTiming = (properties.fps > 0); @@ -33,7 +38,7 @@ void BenchmarkOut::run() { auto frameDuration = std::chrono::duration_cast(frameDurationDouble); auto nextFrameTime = steady_clock::now(); - for(int i = 0; (i < properties.numMessages || properties.numMessages == -1) && isRunning(); i++) { + for(int i = 0; (i < properties.numMessages || properties.numMessages == -1) && mainLoop(); i++) { auto imgMessage = std::dynamic_pointer_cast(inMessage); if(imgMessage != nullptr) { logger->trace("Sending img message with id {}", i); @@ -47,10 +52,16 @@ void BenchmarkOut::run() { } else { newMessage->setTimestampDevice(steady_clock::now()); } - out.send(newMessage); + { + auto blockEvent = this->outputBlockEvent(); + out.send(newMessage); + } } else { logger->trace("Sending message with id {}", i); - out.send(inMessage); + { + auto blockEvent = this->outputBlockEvent(); + out.send(inMessage); + } } if(useTiming) { diff --git a/src/pipeline/node/DynamicCalibrationNode.cpp b/src/pipeline/node/DynamicCalibrationNode.cpp index c9da5c484..a2a3b5e36 100644 --- a/src/pipeline/node/DynamicCalibrationNode.cpp +++ b/src/pipeline/node/DynamicCalibrationNode.cpp @@ -512,7 +512,11 @@ DynamicCalibration::ErrorCode DynamicCalibration::evaluateCommand(const std::sha DynamicCalibration::ErrorCode DynamicCalibration::doWork(std::chrono::steady_clock::time_point& previousLoadingAndCalibrationTime) { auto error = ErrorCode::OK; // Expect everything is ok - auto calibrationCommand = inputControl.tryGet(); + std::shared_ptr calibrationCommand = nullptr; + { + auto blockEvent = this->inputBlockEvent(); + calibrationCommand = inputControl.tryGet(); + } if(calibrationCommand) { error = evaluateCommand(calibrationCommand); } diff --git a/src/pipeline/node/ImageAlign.cpp b/src/pipeline/node/ImageAlign.cpp index d1da2a960..55a36ee6e 100644 --- a/src/pipeline/node/ImageAlign.cpp +++ b/src/pipeline/node/ImageAlign.cpp @@ -354,36 +354,41 @@ void ImageAlign::run() { uint32_t currentEepromId = getParentPipeline().getEepromId(); while(mainLoop()) { - auto inputImg = input.get(); + std::shared_ptr inputImg = nullptr; + std::shared_ptr inConfig = nullptr; + bool hasConfig = false; + { + auto blockEvent = this->inputBlockEvent(); + + inputImg = input.get(); - if(!initialized) { - initialized = true; + if(!initialized) { + initialized = true; - auto inputAlignToImg = inputAlignTo.get(); + auto inputAlignToImg = inputAlignTo.get(); - inputAlignToImgFrame = *inputAlignToImg; + inputAlignToImgFrame = *inputAlignToImg; - inputAlignToTransform = inputAlignToImg->transformation; + inputAlignToTransform = inputAlignToImg->transformation; - alignSourceIntrinsics = inputAlignToImg->transformation.getIntrinsicMatrix(); + alignSourceIntrinsics = inputAlignToImg->transformation.getIntrinsicMatrix(); - alignTo = static_cast(inputAlignToImg->getInstanceNum()); - if(alignWidth == 0 || alignHeight == 0) { - alignWidth = inputAlignToImg->getWidth(); - alignHeight = inputAlignToImg->getHeight(); + alignTo = static_cast(inputAlignToImg->getInstanceNum()); + if(alignWidth == 0 || alignHeight == 0) { + alignWidth = inputAlignToImg->getWidth(); + alignHeight = inputAlignToImg->getHeight(); + } } - } - bool hasConfig = false; - std::shared_ptr inConfig = nullptr; - if(inputConfig.getWaitForMessage()) { - logger->trace("Receiving ImageAlign config message!"); - inConfig = inputConfig.get(); - hasConfig = true; - } else { - inConfig = inputConfig.tryGet(); - if(inConfig != nullptr) { + if(inputConfig.getWaitForMessage()) { + logger->trace("Receiving ImageAlign config message!"); + inConfig = inputConfig.get(); hasConfig = true; + } else { + inConfig = inputConfig.tryGet(); + if(inConfig != nullptr) { + hasConfig = true; + } } } @@ -577,8 +582,11 @@ void ImageAlign::run() { logger->trace("ImageAlign took {} ms", runtime); alignedImg->transformation.setDistortionCoefficients({}); - outputAligned.send(alignedImg); - passthroughInput.send(inputImg); + { + auto blockEvent = this->outputBlockEvent(); + outputAligned.send(alignedImg); + passthroughInput.send(inputImg); + } } } diff --git a/src/pipeline/node/ImageFilters.cpp b/src/pipeline/node/ImageFilters.cpp index b474d6284..acf397397 100644 --- a/src/pipeline/node/ImageFilters.cpp +++ b/src/pipeline/node/ImageFilters.cpp @@ -819,21 +819,26 @@ void ImageFilters::run() { getFilterPipelineString()); while(mainLoop()) { - // Set config - while(inputConfig.has()) { - auto configMsg = inputConfig.get(); - bool isUpdate = configMsg->filterIndices.size() > 0; - if(isUpdate) { - updateExistingFilterPipeline(*configMsg); - logger->debug("ImageFilters: Updating existing filter pipeline. New pipeline is {}", getFilterPipelineString()); - } else { - createNewFilterPipeline(*configMsg); - logger->debug("ImageFilters: Creating a new filter pipeline. New pipeline is {}", getFilterPipelineString()); + std::shared_ptr frame = nullptr; + { + auto blockEvent = this->inputBlockEvent(); + + // Set config + while(inputConfig.has()) { + auto configMsg = inputConfig.get(); + bool isUpdate = configMsg->filterIndices.size() > 0; + if(isUpdate) { + updateExistingFilterPipeline(*configMsg); + logger->debug("ImageFilters: Updating existing filter pipeline. New pipeline is {}", getFilterPipelineString()); + } else { + createNewFilterPipeline(*configMsg); + logger->debug("ImageFilters: Creating a new filter pipeline. New pipeline is {}", getFilterPipelineString()); + } } - } - // Get frame from input queue - std::shared_ptr frame = input.get(); + // Get frame from input queue + frame = input.get(); + } if(frame == nullptr) { logger->error("ImageFilters: Input frame is nullptr"); break; @@ -854,8 +859,12 @@ void ImageFilters::run() { tlast = t2; } - // Send filtered frame to the output queue - output.send(filteredFrame); + { + auto blockEvent = this->outputBlockEvent(); + + // Send filtered frame to the output queue + output.send(filteredFrame); + } } } @@ -946,25 +955,35 @@ void ToFDepthConfidenceFilter::applyDepthConfidenceFilter(std::shared_ptr depthFrame = nullptr; + std::shared_ptr amplitudeFrame = nullptr; + while(mainLoop()) { - // Update threshold dynamically - while(inputConfig.has()) { - auto configMsg = inputConfig.get(); - confidenceThreshold = configMsg->confidenceThreshold; - } + { + auto blockEvent = this->inputBlockEvent(); - // Get frames from input queue - std::shared_ptr depthFrame = depth.get(); - std::shared_ptr amplitudeFrame = amplitude.get(); - if(depthFrame == nullptr || amplitudeFrame == nullptr) { - pimpl->logger->error("DepthConfidenceFilter: Input frame is nullptr"); - break; + // Update threshold dynamically + while(inputConfig.has()) { + auto configMsg = inputConfig.get(); + confidenceThreshold = configMsg->confidenceThreshold; + } + + // Get frames from input queue + depthFrame = depth.get(); + amplitudeFrame = amplitude.get(); + if(depthFrame == nullptr || amplitudeFrame == nullptr) { + pimpl->logger->error("DepthConfidenceFilter: Input frame is nullptr"); + break; + } } // In case the confidence threshold is 0, serve as a passthrough if(confidenceThreshold == 0.0f) { - filteredDepth.send(depthFrame); - confidence.send(amplitudeFrame); + { + auto blockEvent = this->outputBlockEvent(); + filteredDepth.send(depthFrame); + confidence.send(amplitudeFrame); + } continue; } @@ -993,9 +1012,12 @@ void ToFDepthConfidenceFilter::run() { tlast = t2; } - // Send results - filteredDepth.send(filteredDepthFrame); - confidence.send(confidenceFrame); + { + auto blockEvent = this->outputBlockEvent(); + // Send results + filteredDepth.send(filteredDepthFrame); + confidence.send(confidenceFrame); + } } } diff --git a/src/pipeline/node/ObjectTracker.cpp b/src/pipeline/node/ObjectTracker.cpp index fd2ee507f..4e894eabc 100644 --- a/src/pipeline/node/ObjectTracker.cpp +++ b/src/pipeline/node/ObjectTracker.cpp @@ -107,31 +107,35 @@ void ObjectTracker::run() { bool gotDetections = false; - inputTrackerImg = inputTrackerFrame.get(); - if(inputDetections.has()) { - auto detectionsBuffer = inputDetections.get(); - inputImgDetections = std::dynamic_pointer_cast(detectionsBuffer); - inputSpatialImgDetections = std::dynamic_pointer_cast(detectionsBuffer); - if(inputImgDetections) { - gotDetections = true; - if(!inputImgDetections->transformation.has_value()) { - logger->debug("Transformation is not set for input detections, inputDetectionFrame is required"); - inputDetectionImg = inputDetectionFrame.get(); - } - } else if(inputSpatialImgDetections) { - gotDetections = true; - if(!inputSpatialImgDetections->transformation.has_value()) { - logger->debug("Transformation is not set for input detections, inputDetectionFrame is required"); - inputDetectionImg = inputDetectionFrame.get(); + { + auto blockEvent = this->inputBlockEvent(); + + inputTrackerImg = inputTrackerFrame.get(); + if(inputDetections.has()) { + auto detectionsBuffer = inputDetections.get(); + inputImgDetections = std::dynamic_pointer_cast(detectionsBuffer); + inputSpatialImgDetections = std::dynamic_pointer_cast(detectionsBuffer); + if(inputImgDetections) { + gotDetections = true; + if(!inputImgDetections->transformation.has_value()) { + logger->debug("Transformation is not set for input detections, inputDetectionFrame is required"); + inputDetectionImg = inputDetectionFrame.get(); + } + } else if(inputSpatialImgDetections) { + gotDetections = true; + if(!inputSpatialImgDetections->transformation.has_value()) { + logger->debug("Transformation is not set for input detections, inputDetectionFrame is required"); + inputDetectionImg = inputDetectionFrame.get(); + } + } else if(!inputImgDetections && !inputSpatialImgDetections) { + logger->error("Input detections is not of type ImgDetections or SpatialImgDetections, skipping tracking"); } - } else if(!inputImgDetections && !inputSpatialImgDetections) { - logger->error("Input detections is not of type ImgDetections or SpatialImgDetections, skipping tracking"); } - } - if(inputConfig.getWaitForMessage()) { - inputCfg = inputConfig.get(); - } else { - inputCfg = inputConfig.tryGet(); + if(inputConfig.getWaitForMessage()) { + inputCfg = inputConfig.get(); + } else { + inputCfg = inputConfig.tryGet(); + } } if(inputCfg) { @@ -202,13 +206,17 @@ void ObjectTracker::run() { } trackletsMsg->transformation = inputTrackerImg->transformation; - out.send(trackletsMsg); - passthroughTrackerFrame.send(inputTrackerImg); - if(gotDetections) { - passthroughDetections.send(inputImgDetections ? std::dynamic_pointer_cast(inputImgDetections) - : std::dynamic_pointer_cast(inputSpatialImgDetections)); - if(inputDetectionImg) { - passthroughDetectionFrame.send(inputDetectionImg); + { + auto blockEvent = this->outputBlockEvent(); + + out.send(trackletsMsg); + passthroughTrackerFrame.send(inputTrackerImg); + if(gotDetections) { + passthroughDetections.send(inputImgDetections ? std::dynamic_pointer_cast(inputImgDetections) + : std::dynamic_pointer_cast(inputSpatialImgDetections)); + if(inputDetectionImg) { + passthroughDetectionFrame.send(inputDetectionImg); + } } } } diff --git a/src/pipeline/node/Sync.cpp b/src/pipeline/node/Sync.cpp index b682bec4b..7a45432ef 100644 --- a/src/pipeline/node/Sync.cpp +++ b/src/pipeline/node/Sync.cpp @@ -1,4 +1,5 @@ #include "depthai/pipeline/node/Sync.hpp" +#include #include "depthai/pipeline/datatype/MessageGroup.hpp" #include "pipeline/ThreadedNodeImpl.hpp" @@ -50,73 +51,79 @@ void Sync::run() { auto syncThresholdNs = properties.syncThresholdNs; logger->trace("Sync threshold: {}", syncThresholdNs); + time_point tAfterMessageBeginning; + while(mainLoop()) { auto tAbsoluteBeginning = steady_clock::now(); std::unordered_map> inputFrames; - for(auto name : inputNames) { - logger->trace("Receiving input: {}", name); - inputFrames[name] = inputs[name].get(); - if(inputFrames[name] == nullptr) { - logger->error("Received nullptr from input {}, sync node only accepts messages inherited from Buffer on the inputs", name); - throw std::runtime_error("Received nullptr from input " + name); - } - } - // Print out the timestamps - for(const auto& frame : inputFrames) { - logger->debug( - "Starting input {} timestamp is {} ms", frame.first, static_cast(frame.second->getTimestamp().time_since_epoch().count()) / 1000000.f); - } - auto tAfterMessageBeginning = steady_clock::now(); - int attempts = 0; - while(true) { - logger->trace("There have been {} attempts to sync", attempts); - if(attempts > 50) { - logger->warn("Sync node has been trying to sync for {} messages, but the messages are still not in sync.", attempts); - for(const auto& frame : inputFrames) { - logger->warn( - "Output {} timestamp is {} ms", frame.first, static_cast(frame.second->getTimestamp().time_since_epoch().count()) / 1000000.f); + { + auto eventBlock = this->inputBlockEvent(); + + for(auto name : inputNames) { + logger->trace("Receiving input: {}", name); + inputFrames[name] = inputs[name].get(); + if(inputFrames[name] == nullptr) { + logger->error("Received nullptr from input {}, sync node only accepts messages inherited from Buffer on the inputs", name); + throw std::runtime_error("Received nullptr from input " + name); } } - if(attempts > properties.syncAttempts && properties.syncAttempts != -1) { - if(properties.syncAttempts != 0) - logger->warn( - "Sync node has been trying to sync for {} messages, but the messages are still not in sync. " - "The node will send the messages anyway.", - attempts); - break; - } - // Find a minimum timestamp - auto minTs = inputFrames.begin()->second->getTimestamp(); + // Print out the timestamps for(const auto& frame : inputFrames) { - if(frame.second->getTimestamp() < minTs) { - minTs = frame.second->getTimestamp(); - } + logger->debug( + "Starting input {} timestamp is {} ms", frame.first, static_cast(frame.second->getTimestamp().time_since_epoch().count()) / 1000000.f); } - - // Find a max timestamp - auto maxTs = inputFrames.begin()->second->getTimestamp(); - for(const auto& frame : inputFrames) { - if(frame.second->getTimestamp() > maxTs) { - maxTs = frame.second->getTimestamp(); + tAfterMessageBeginning = steady_clock::now(); + int attempts = 0; + while(true) { + logger->trace("There have been {} attempts to sync", attempts); + if(attempts > 50) { + logger->warn("Sync node has been trying to sync for {} messages, but the messages are still not in sync.", attempts); + for(const auto& frame : inputFrames) { + logger->warn( + "Output {} timestamp is {} ms", frame.first, static_cast(frame.second->getTimestamp().time_since_epoch().count()) / 1000000.f); + } + } + if(attempts > properties.syncAttempts && properties.syncAttempts != -1) { + if(properties.syncAttempts != 0) + logger->warn( + "Sync node has been trying to sync for {} messages, but the messages are still not in sync. " + "The node will send the messages anyway.", + attempts); + break; + } + // Find a minimum timestamp + auto minTs = inputFrames.begin()->second->getTimestamp(); + for(const auto& frame : inputFrames) { + if(frame.second->getTimestamp() < minTs) { + minTs = frame.second->getTimestamp(); + } } - } - logger->debug("Diff: {} ms", duration_cast(maxTs - minTs).count()); - if(duration_cast(maxTs - minTs).count() < syncThresholdNs) { - break; - } + // Find a max timestamp + auto maxTs = inputFrames.begin()->second->getTimestamp(); + for(const auto& frame : inputFrames) { + if(frame.second->getTimestamp() > maxTs) { + maxTs = frame.second->getTimestamp(); + } + } + logger->debug("Diff: {} ms", duration_cast(maxTs - minTs).count()); - // Get the message with the minimum timestamp (oldest message) - std::string minTsName; - for(const auto& frame : inputFrames) { - if(frame.second->getTimestamp() == minTs) { - minTsName = frame.first; + if(duration_cast(maxTs - minTs).count() < syncThresholdNs) { break; } + + // Get the message with the minimum timestamp (oldest message) + std::string minTsName; + for(const auto& frame : inputFrames) { + if(frame.second->getTimestamp() == minTs) { + minTsName = frame.first; + break; + } + } + logger->trace("Receiving input: {}", minTsName); + inputFrames[minTsName] = inputs[minTsName].get(); + attempts++; } - logger->trace("Receiving input: {}", minTsName); - inputFrames[minTsName] = inputs[minTsName].get(); - attempts++; } auto tBeforeSend = steady_clock::now(); auto outputGroup = std::make_shared(); @@ -133,7 +140,10 @@ void Sync::run() { outputGroup->setTimestamp(newestFrame->getTimestamp()); outputGroup->setTimestampDevice(newestFrame->getTimestampDevice()); outputGroup->setSequenceNum(newestFrame->getSequenceNum()); - out.send(outputGroup); + { + auto blockEvent = this->outputBlockEvent(); + out.send(outputGroup); + } auto tAbsoluteEnd = steady_clock::now(); logger->debug("Sync total took {}ms, processing {}ms, getting_frames {}ms, sending_frames {}ms", duration_cast(tAbsoluteEnd - tAbsoluteBeginning).count() / 1000, diff --git a/src/pipeline/node/host/Display.cpp b/src/pipeline/node/host/Display.cpp index 1e7488a88..6f28a20cb 100644 --- a/src/pipeline/node/host/Display.cpp +++ b/src/pipeline/node/host/Display.cpp @@ -38,7 +38,11 @@ Display::Display(std::string name) : name(std::move(name)) {} void Display::run() { auto fpsCounter = FPSCounter(); while(mainLoop()) { - std::shared_ptr imgFrame = input.get(); + std::shared_ptr imgFrame = nullptr; + { + auto blockEvent = this->inputBlockEvent(); + imgFrame = input.get(); + } if(imgFrame != nullptr) { fpsCounter.update(); auto fps = fpsCounter.getFPS(); diff --git a/src/pipeline/node/host/RGBD.cpp b/src/pipeline/node/host/RGBD.cpp index 5f964b1b9..c40bc0e45 100644 --- a/src/pipeline/node/host/RGBD.cpp +++ b/src/pipeline/node/host/RGBD.cpp @@ -365,8 +365,12 @@ void RGBD::initialize(std::shared_ptr frames) { void RGBD::run() { while(mainLoop()) { if(!pcl.getQueueConnections().empty() || !pcl.getConnections().empty() || !rgbd.getQueueConnections().empty() || !rgbd.getConnections().empty()) { - // Get the color and depth frames - auto group = inSync.get(); + std::shared_ptr group = nullptr; + { + auto blockEvent = this->inputBlockEvent(); + // Get the color and depth frames + group = inSync.get(); + } if(group == nullptr) continue; if(!initialized) { initialize(group); @@ -429,17 +433,22 @@ void RGBD::run() { pc->setTimestampDevice(colorFrame->getTimestampDevice()); pc->setSequenceNum(colorFrame->getSequenceNum()); pc->setInstanceNum(colorFrame->getInstanceNum()); - if(!pcl.getQueueConnections().empty() || !pcl.getConnections().empty()) { - pcl.send(pc); - } - if(!rgbd.getQueueConnections().empty() || !rgbd.getConnections().empty()) { - auto rgbdData = std::make_shared(); - rgbdData->setTimestamp(colorFrame->getTimestamp()); - rgbdData->setTimestampDevice(colorFrame->getTimestampDevice()); - rgbdData->setSequenceNum(colorFrame->getSequenceNum()); - rgbdData->setDepthFrame(depthFrame); - rgbdData->setRGBFrame(colorFrame); - rgbd.send(rgbdData); + { + auto blockEvent = this->outputBlockEvent(); + if(!pcl.getQueueConnections().empty() || !pcl.getConnections().empty()) { + { + pcl.send(pc); + } + } + if(!rgbd.getQueueConnections().empty() || !rgbd.getConnections().empty()) { + auto rgbdData = std::make_shared(); + rgbdData->setTimestamp(colorFrame->getTimestamp()); + rgbdData->setTimestampDevice(colorFrame->getTimestampDevice()); + rgbdData->setSequenceNum(colorFrame->getSequenceNum()); + rgbdData->setDepthFrame(depthFrame); + rgbdData->setRGBFrame(colorFrame); + rgbd.send(rgbdData); + } } } } diff --git a/src/pipeline/node/host/Record.cpp b/src/pipeline/node/host/Record.cpp index 36f6316d5..aee524e2e 100644 --- a/src/pipeline/node/host/Record.cpp +++ b/src/pipeline/node/host/Record.cpp @@ -54,7 +54,11 @@ void RecordVideo::run() { auto start = std::chrono::steady_clock::now(); auto end = std::chrono::steady_clock::now(); while(mainLoop()) { - auto msg = input.get(); + std::shared_ptr msg = nullptr; + { + auto blockEvent = this->inputBlockEvent(); + msg = input.get(); + } if(msg == nullptr) continue; if(streamType == DatatypeEnum::ADatatype) { if(std::dynamic_pointer_cast(msg) != nullptr) { @@ -151,7 +155,11 @@ void RecordMetadataOnly::run() { DatatypeEnum streamType = DatatypeEnum::ADatatype; while(mainLoop()) { - auto msg = input.get(); + std::shared_ptr msg = nullptr; + { + auto blockEvent = this->inputBlockEvent(); + msg = input.get(); + } if(msg == nullptr) continue; if(streamType == DatatypeEnum::ADatatype) { if(std::dynamic_pointer_cast(msg) != nullptr) { diff --git a/src/pipeline/node/host/Replay.cpp b/src/pipeline/node/host/Replay.cpp index f35f5dfb9..43fb4805a 100644 --- a/src/pipeline/node/host/Replay.cpp +++ b/src/pipeline/node/host/Replay.cpp @@ -292,7 +292,10 @@ void ReplayVideo::run() { std::this_thread::sleep_until(loopStart + (buffer->getTimestampDevice() - prevMsgTs)); } - if(buffer) out.send(buffer); + { + auto blockEvent = this->outputBlockEvent(); + if(buffer) out.send(buffer); + } if(fps.has_value() && fps.value() > 0.1f) { std::this_thread::sleep_until(loopStart + std::chrono::milliseconds((uint32_t)roundf(1000.f / fps.value()))); @@ -368,7 +371,10 @@ void ReplayMetadataOnly::run() { std::this_thread::sleep_until(loopStart + (buffer->getTimestampDevice() - prevMsgTs)); } - if(buffer) out.send(buffer); + { + auto blockEvent = this->outputBlockEvent(); + if(buffer) out.send(buffer); + } if(fps.has_value() && fps.value() > 0.1f) { std::this_thread::sleep_until(loopStart + std::chrono::milliseconds((uint32_t)roundf(1000.f / fps.value()))); diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index e0a9e79bf..011c6d3ca 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -5,6 +5,9 @@ namespace dai { namespace utility { +constexpr const char* OUTPUT_BLOCK_NAME = "getInputs"; +constexpr const char* INPUT_BLOCK_NAME = "sendOutputs"; + std::string typeToString(PipelineEvent::Type type) { switch(type) { case PipelineEvent::Type::CUSTOM: @@ -189,13 +192,13 @@ void PipelineEventDispatcher::pingInputEvent(const std::string& source, int32_t PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::blockEvent(PipelineEvent::Type type, const std::string& source) { return BlockPipelineEvent(*this, type, source); } -PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::inputBlockEvent(const std::string& source) { +PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::inputBlockEvent() { // For convenience due to the default source - return blockEvent(PipelineEvent::Type::INPUT_BLOCK, source); + return blockEvent(PipelineEvent::Type::INPUT_BLOCK, INPUT_BLOCK_NAME); } -PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::outputBlockEvent(const std::string& source) { +PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::outputBlockEvent() { // For convenience due to the default source - return blockEvent(PipelineEvent::Type::OUTPUT_BLOCK, source); + return blockEvent(PipelineEvent::Type::OUTPUT_BLOCK, OUTPUT_BLOCK_NAME); } PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::customBlockEvent(const std::string& source) { // For convenience due to the default source From 0017ab7c300994cd1ebd6235277f6963e2e40de6 Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 20 Oct 2025 17:52:09 +0200 Subject: [PATCH 044/124] RVC4 FW: Add io events to some nodes --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index b08d0e439..dd5d438e1 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+bdf2d2353503a794df5db7c8937a05ef2b106de2") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+905051e8e38f01ac02d99eb8bf55122302dc3dc9") From 55e7dd7928e9dd31fea47c363cfacc487c199d9b Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 28 Oct 2025 11:51:17 +0100 Subject: [PATCH 045/124] Use group and name for event source --- include/depthai/pipeline/MessageQueue.hpp | 2 +- src/pipeline/Node.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/depthai/pipeline/MessageQueue.hpp b/include/depthai/pipeline/MessageQueue.hpp index 43e1d31e6..6aee529be 100644 --- a/include/depthai/pipeline/MessageQueue.hpp +++ b/include/depthai/pipeline/MessageQueue.hpp @@ -209,7 +209,7 @@ class MessageQueue : public std::enable_shared_from_this { if(!queue.tryPop(val)) { return nullptr; } - if(pipelineEventDispatcher) pipelineEventDispatcher->endInputEvent(name, getSize()); + if(pipelineEventDispatcher && std::dynamic_pointer_cast(val)) pipelineEventDispatcher->endInputEvent(name, getSize()); return std::dynamic_pointer_cast(val); } diff --git a/src/pipeline/Node.cpp b/src/pipeline/Node.cpp index 49490765d..6e1e8c818 100644 --- a/src/pipeline/Node.cpp +++ b/src/pipeline/Node.cpp @@ -241,11 +241,11 @@ void Node::Output::send(const std::shared_ptr& msg) { // } // } // } - if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getName()); + if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getGroup() + "." + getName()); for(auto& messageQueue : connectedInputs) { messageQueue->send(msg); } - if(pipelineEventDispatcher) pipelineEventDispatcher->endOutputEvent(getName()); + if(pipelineEventDispatcher) pipelineEventDispatcher->endOutputEvent(getGroup() + "." + getName()); } bool Node::Output::trySend(const std::shared_ptr& msg) { @@ -265,11 +265,11 @@ bool Node::Output::trySend(const std::shared_ptr& msg) { // } // } // } - if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getName()); + if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getGroup() + "." + getName()); for(auto& messageQueue : connectedInputs) { success &= messageQueue->trySend(msg); } - if(pipelineEventDispatcher && success) pipelineEventDispatcher->endOutputEvent(getName()); + if(pipelineEventDispatcher && success) pipelineEventDispatcher->endOutputEvent(getGroup() + "." + getName()); return success; } From ef20c8be2b5664bdd0af88fe12c704d08accd5e5 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 29 Oct 2025 12:07:40 +0100 Subject: [PATCH 046/124] Add default values to pipeline state --- .../pipeline/datatype/PipelineState.hpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index 924f59d9e..7caccb395 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -16,16 +16,16 @@ class NodeState { }; struct TimingStats { uint64_t minMicros = -1; - uint64_t maxMicros; - uint64_t averageMicrosRecent; - uint64_t stdDevMicrosRecent; + uint64_t maxMicros = 0; + uint64_t averageMicrosRecent = 0; + uint64_t stdDevMicrosRecent = 0; uint64_t minMicrosRecent = -1; - uint64_t maxMicrosRecent; - uint64_t medianMicrosRecent; + uint64_t maxMicrosRecent = 0; + uint64_t medianMicrosRecent = 0; DEPTHAI_SERIALIZE(TimingStats, minMicros, maxMicros, averageMicrosRecent, stdDevMicrosRecent, minMicrosRecent, maxMicrosRecent, medianMicrosRecent); }; struct Timing { - float fps; + float fps = 0.0f; TimingStats durationStats; bool isValid() const { @@ -35,10 +35,10 @@ class NodeState { DEPTHAI_SERIALIZE(Timing, fps, durationStats); }; struct QueueStats { - uint32_t maxQueued; - uint32_t minQueuedRecent; - uint32_t maxQueuedRecent; - uint32_t medianQueuedRecent; + uint32_t maxQueued = 0; + uint32_t minQueuedRecent = 0; + uint32_t maxQueuedRecent = 0; + uint32_t medianQueuedRecent = 0; DEPTHAI_SERIALIZE(QueueStats, maxQueued, minQueuedRecent, maxQueuedRecent, medianQueuedRecent); }; struct InputQueueState { @@ -49,7 +49,7 @@ class NodeState { BLOCKED = 2 // An output attempted to send to this input, but the input queue was full } state = State::IDLE; // Number of messages currently queued in the input queue - uint32_t numQueued; + uint32_t numQueued = 0; // Timing info about this input Timing timing; // Queue usage stats From 6b58440a99dfa871b1a1a31b58eae3a335c5e15c Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 3 Nov 2025 13:27:46 +0100 Subject: [PATCH 047/124] Add pipeline debugging tests --- include/depthai/utility/LockingQueue.hpp | 8 + src/pipeline/MessageQueue.cpp | 1 + src/pipeline/Node.cpp | 8 +- src/utility/PipelineEventDispatcher.cpp | 4 +- tests/CMakeLists.txt | 4 + .../pipeline_debugging_host_test.cpp | 401 ++++++++++++++++++ 6 files changed, 420 insertions(+), 6 deletions(-) create mode 100644 tests/src/onhost_tests/pipeline_debugging_host_test.cpp diff --git a/include/depthai/utility/LockingQueue.hpp b/include/depthai/utility/LockingQueue.hpp index 40db11abf..0bbe507e3 100644 --- a/include/depthai/utility/LockingQueue.hpp +++ b/include/depthai/utility/LockingQueue.hpp @@ -176,6 +176,8 @@ class LockingQueue { } queue.push(data); + + callback(LockingQueueState::OK, queue.size()); } signalPush.notify_all(); return true; @@ -206,6 +208,8 @@ class LockingQueue { } queue.push(std::move(data)); + + callback(LockingQueueState::OK, queue.size()); } signalPush.notify_all(); return true; @@ -242,6 +246,8 @@ class LockingQueue { } queue.push(data); + + callback(LockingQueueState::OK, queue.size()); } signalPush.notify_all(); return true; @@ -278,6 +284,8 @@ class LockingQueue { } queue.push(std::move(data)); + + callback(LockingQueueState::OK, queue.size()); } signalPush.notify_all(); return true; diff --git a/src/pipeline/MessageQueue.cpp b/src/pipeline/MessageQueue.cpp index 4e906bb71..2d0dd05d9 100644 --- a/src/pipeline/MessageQueue.cpp +++ b/src/pipeline/MessageQueue.cpp @@ -127,6 +127,7 @@ void MessageQueue::send(const std::shared_ptr& msg) { pipelineEventDispatcher->pingInputEvent(name, -2, size); break; case LockingQueueState::OK: + pipelineEventDispatcher->pingInputEvent(name, 0, size); break; } } diff --git a/src/pipeline/Node.cpp b/src/pipeline/Node.cpp index 6e1e8c818..49490765d 100644 --- a/src/pipeline/Node.cpp +++ b/src/pipeline/Node.cpp @@ -241,11 +241,11 @@ void Node::Output::send(const std::shared_ptr& msg) { // } // } // } - if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getGroup() + "." + getName()); + if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getName()); for(auto& messageQueue : connectedInputs) { messageQueue->send(msg); } - if(pipelineEventDispatcher) pipelineEventDispatcher->endOutputEvent(getGroup() + "." + getName()); + if(pipelineEventDispatcher) pipelineEventDispatcher->endOutputEvent(getName()); } bool Node::Output::trySend(const std::shared_ptr& msg) { @@ -265,11 +265,11 @@ bool Node::Output::trySend(const std::shared_ptr& msg) { // } // } // } - if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getGroup() + "." + getName()); + if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getName()); for(auto& messageQueue : connectedInputs) { success &= messageQueue->trySend(msg); } - if(pipelineEventDispatcher && success) pipelineEventDispatcher->endOutputEvent(getGroup() + "." + getName()); + if(pipelineEventDispatcher && success) pipelineEventDispatcher->endOutputEvent(getName()); return success; } diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index 011c6d3ca..e551491c2 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -5,8 +5,8 @@ namespace dai { namespace utility { -constexpr const char* OUTPUT_BLOCK_NAME = "getInputs"; -constexpr const char* INPUT_BLOCK_NAME = "sendOutputs"; +constexpr const char* OUTPUT_BLOCK_NAME = "sendOutputs"; +constexpr const char* INPUT_BLOCK_NAME = "getInputs"; std::string typeToString(PipelineEvent::Type type) { switch(type) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8881ad587..198d9b11c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -352,6 +352,10 @@ dai_set_test_labels(color_camera_node_test ondevice rvc2_all ci) dai_add_test(pipeline_test src/ondevice_tests/pipeline_test.cpp) dai_set_test_labels(pipeline_test ondevice rvc2_all ci) +# Pipeline debugging tests +dai_add_test(pipeline_debugging_host_test src/onhost_tests/pipeline_debugging_host_test.cpp) +dai_set_test_labels(pipeline_debugging_host_test onhost ci) + # Device USB Speed and serialization macros test dai_add_test(device_usbspeed_test src/ondevice_tests/device_usbspeed_test.cpp) dai_set_test_labels(device_usbspeed_test ondevice rvc2 usb ci) diff --git a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp new file mode 100644 index 000000000..8fe3760b7 --- /dev/null +++ b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp @@ -0,0 +1,401 @@ +#include +#include +#include + +#include "depthai/depthai.hpp" +#include "depthai/pipeline/ThreadedHostNode.hpp" + +using namespace dai; + +/** + * 1. invalid pipeline (no state yet) + * 2. pipeline with predictable timings + * 3. stuck pipeline (check state, full queue) + */ + +class GeneratorNode : public node::CustomThreadedNode { + bool doStep = true; + int runTo = 0; + + int seqNo = 0; + + public: + Output output{*this, {"output", DEFAULT_GROUP, {{{DatatypeEnum::Buffer, true}}}}}; + + Input ping{*this, {"_ping", DEFAULT_GROUP, true, 8, {{{DatatypeEnum::Buffer, true}}}, DEFAULT_WAIT_FOR_MESSAGE}}; + Output ack{*this, {"_ack", DEFAULT_GROUP, {{{DatatypeEnum::Buffer, true}}}}}; + + void run() override { + step(0); + while(mainLoop()) { + step(1); + { + auto blockEvent = this->outputBlockEvent(); + auto msg = std::make_shared(); + msg->sequenceNum = seqNo++; + msg->setTimestamp(std::chrono::steady_clock::now()); + output.send(msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + + private: + void step(int seq = 0) { + if(doStep && (runTo == seq || runTo == 0) || ping.has()) { + // wait for ping + auto pingMsg = ping.get(); + doStep = pingMsg->sequenceNum >= 0; + runTo = pingMsg->sequenceNum; + // send ack + auto ackMsg = std::make_shared(); + ackMsg->sequenceNum = seq; + ack.send(ackMsg); + } + } +}; + +class ConsumerNode : public node::CustomThreadedNode { + bool doStep = true; + int runTo = 0; + + public: + Input input{*this, {"input", DEFAULT_GROUP, true, 4, {{{DatatypeEnum::Buffer, true}}}, DEFAULT_WAIT_FOR_MESSAGE}}; + + Input ping{*this, {"_ping", DEFAULT_GROUP, true, 8, {{{DatatypeEnum::Buffer, true}}}, DEFAULT_WAIT_FOR_MESSAGE}}; + Output ack{*this, {"_ack", DEFAULT_GROUP, {{{DatatypeEnum::Buffer, true}}}}}; + + void run() override { + step(0); + while(mainLoop()) { + std::shared_ptr msg = nullptr; + step(1); + { + auto blockEvent = this->inputBlockEvent(); + msg = input.get(); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + + private: + void step(int seq = 0) { + if(doStep && (runTo == seq || runTo == 0) || ping.has()) { + // wait for ping + auto pingMsg = ping.get(); + doStep = pingMsg->sequenceNum >= 0; + runTo = pingMsg->sequenceNum; + // send ack + auto ackMsg = std::make_shared(); + ackMsg->sequenceNum = seq; + ack.send(ackMsg); + } + } +}; + +class MapNode : public node::CustomThreadedNode { + bool doStep = true; + int runTo = 0; + + public: + InputMap inputs{*this, "inputs", {DEFAULT_NAME, DEFAULT_GROUP, false, 4, {{{DatatypeEnum::Buffer, true}}}, DEFAULT_WAIT_FOR_MESSAGE}}; + OutputMap outputs{*this, "outputs", {DEFAULT_NAME, DEFAULT_GROUP, {{{DatatypeEnum::Buffer, true}}}}}; + + Input ping{*this, {"_ping", DEFAULT_GROUP, true, 8, {{{DatatypeEnum::Buffer, true}}}, DEFAULT_WAIT_FOR_MESSAGE}}; + Output ack{*this, {"_ack", DEFAULT_GROUP, {{{DatatypeEnum::Buffer, true}}}}}; + + void run() override { + step(0); + while(mainLoop()) { + std::unordered_map> msg; + step(1); + { + auto blockEvent = this->inputBlockEvent(); + for(auto& [name, input] : inputs) { + msg[name.second] = inputs[name.second].get(); + } + } + step(2); + { + auto blockEvent = this->outputBlockEvent(); + for(auto& [name, output] : outputs) { + outputs[name].send(msg[name.second]); + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + + private: + void step(int seq = 0) { + if(doStep && (runTo == seq || runTo == 0) || ping.has()) { + // wait for ping + auto pingMsg = ping.get(); + doStep = pingMsg->sequenceNum >= 0; + runTo = pingMsg->sequenceNum; + // send ack + auto ackMsg = std::make_shared(); + ackMsg->sequenceNum = seq; + ack.send(ackMsg); + } + } +}; + +class BridgeNode : public node::CustomThreadedNode { + bool doStep = true; + int runTo = 0; + + public: + Input input{*this, {"input", DEFAULT_GROUP, true, 4, {{{DatatypeEnum::Buffer, true}}}, DEFAULT_WAIT_FOR_MESSAGE}}; + Output output{*this, {"output", DEFAULT_GROUP, {{{DatatypeEnum::Buffer, true}}}}}; + + Input ping{*this, {"_ping", DEFAULT_GROUP, true, 8, {{{DatatypeEnum::Buffer, true}}}, DEFAULT_WAIT_FOR_MESSAGE}}; + Output ack{*this, {"_ack", DEFAULT_GROUP, {{{DatatypeEnum::Buffer, true}}}}}; + + void run() override { + step(0); + while(mainLoop()) { + std::shared_ptr msg = nullptr; + step(1); + { + auto blockEvent = this->inputBlockEvent(); + msg = input.get(); + } + step(2); + { + auto blockEvent = this->outputBlockEvent(); + output.send(msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + + private: + void step(int seq = 0) { + if(doStep && (runTo == seq || runTo == 0) || ping.has()) { + // wait for ping + auto pingMsg = ping.get(); + doStep = pingMsg->sequenceNum >= 0; + runTo = pingMsg->sequenceNum; + // send ack + auto ackMsg = std::make_shared(); + ackMsg->sequenceNum = seq; + ack.send(ackMsg); + } + } +}; + +class PipelineHandler { + std::unordered_map> pingQueues; + std::unordered_map> ackQueues; + std::unordered_map nodeIds; + + public: + Pipeline pipeline; + + PipelineHandler() : pipeline(false) { + auto gen1 = pipeline.create(); + nodeIds["gen1"] = gen1->id; + auto gen2 = pipeline.create(); + nodeIds["gen2"] = gen2->id; + auto bridge1 = pipeline.create(); + nodeIds["bridge1"] = bridge1->id; + auto bridge2 = pipeline.create(); + nodeIds["bridge2"] = bridge2->id; + auto map = pipeline.create(); + nodeIds["map"] = map->id; + auto cons1 = pipeline.create(); + nodeIds["cons1"] = cons1->id; + auto cons2 = pipeline.create(); + nodeIds["cons2"] = cons2->id; + + gen1->output.link(bridge1->input); + gen2->output.link(bridge2->input); + bridge1->output.link(map->inputs["bridge1"]); + bridge2->output.link(map->inputs["bridge2"]); + map->outputs["bridge1"].link(cons1->input); + map->outputs["bridge2"].link(cons2->input); + + pingQueues["gen1"] = gen1->ping.createInputQueue(); + pingQueues["gen2"] = gen2->ping.createInputQueue(); + pingQueues["bridge1"] = bridge1->ping.createInputQueue(); + pingQueues["bridge2"] = bridge2->ping.createInputQueue(); + pingQueues["map"] = map->ping.createInputQueue(); + pingQueues["cons1"] = cons1->ping.createInputQueue(); + pingQueues["cons2"] = cons2->ping.createInputQueue(); + ackQueues["gen1"] = gen1->ack.createOutputQueue(); + ackQueues["gen2"] = gen2->ack.createOutputQueue(); + ackQueues["bridge1"] = bridge1->ack.createOutputQueue(); + ackQueues["bridge2"] = bridge2->ack.createOutputQueue(); + ackQueues["map"] = map->ack.createOutputQueue(); + ackQueues["cons1"] = cons1->ack.createOutputQueue(); + ackQueues["cons2"] = cons2->ack.createOutputQueue(); + } + + void start() { + pipeline.start(); + } + + void stop() { + pipeline.stop(); + } + + int ping(const std::string& nodeName, int seqNo) { + auto pingMsg = std::make_shared(); + pingMsg->sequenceNum = seqNo; + pingQueues[nodeName]->send(pingMsg); + auto ackMsg = ackQueues[nodeName]->get(); + return ackMsg->sequenceNum; + } + + int pingNoAck(const std::string& nodeName, int seqNo) { + auto pingMsg = std::make_shared(); + pingMsg->sequenceNum = seqNo; + pingQueues[nodeName]->send(pingMsg); + return 0; + } + + std::vector getNodeNames() const { + std::vector names; + for(const auto& [name, _] : pingQueues) { + names.push_back(name); + } + return names; + } + + int64_t getNodeId(const std::string& nodeName) const { + return nodeIds.at(nodeName); + } +}; + +TEST_CASE("Node states test") { + PipelineHandler ph; + ph.start(); + + // State of non-ping/ack ios should be invalid after first ping, node states should exist + for(const auto& nodeName : ph.getNodeNames()) { + auto ackSeq = ph.ping(nodeName, 0); + REQUIRE(ackSeq == 0); + } + { + // Nodes should now be stopped before inputs get + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + auto state = ph.pipeline.getPipelineState().nodes().detailed(); + for(const auto& nodeName : ph.getNodeNames()) { + auto nodeState = state.nodeStates.at(ph.getNodeId(nodeName)); + REQUIRE(nodeState.state == dai::NodeState::State::IDLE); + REQUIRE_FALSE(nodeState.mainLoopTiming.isValid()); + REQUIRE_FALSE(nodeState.inputsGetTiming.isValid()); + REQUIRE_FALSE(nodeState.outputsSendTiming.isValid()); + for(const auto& [inputName, inputState] : nodeState.inputStates) { + if(inputName.rfind("_ping") != std::string::npos) continue; + REQUIRE(inputState.state == dai::NodeState::InputQueueState::State::IDLE); + REQUIRE_FALSE(inputState.timing.isValid()); + } + for(const auto& [outputName, outputState] : nodeState.outputStates) { + if(outputName.rfind("_ack") != std::string::npos) continue; + REQUIRE(outputState.state == dai::NodeState::OutputQueueState::State::IDLE); + REQUIRE_FALSE(outputState.timing.isValid()); + } + for(const auto& [otherName, otherTiming] : nodeState.otherTimings) { + REQUIRE_FALSE(otherTiming.isValid()); + } + } + } + + { + // Send bridge1 to input get + auto ackSeq = ph.ping("bridge1", 0); + REQUIRE(ackSeq == 1); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + auto nodeState = ph.pipeline.getPipelineState().nodes(ph.getNodeId("bridge1")).detailed(); + REQUIRE(nodeState.state == dai::NodeState::State::GETTING_INPUTS); + REQUIRE(nodeState.inputStates["input"].numQueued == 0); + REQUIRE(nodeState.inputStates["input"].state == dai::NodeState::InputQueueState::State::WAITING); + } + { + // Send gen2 to output send + auto ackSeq = ph.ping("gen2", 0); + REQUIRE(ackSeq == 1); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + auto nodeState = ph.pipeline.getPipelineState().nodes(ph.getNodeId("bridge2")).detailed(); + REQUIRE(nodeState.inputStates["input"].numQueued == 1); + + // Fill bridge2 input queue (currently 1/4) + for(int i = 0; i < 3; ++i) { + ackSeq = ph.ping("gen2", 0); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + nodeState = ph.pipeline.getPipelineState().nodes(ph.getNodeId("bridge2")).detailed(); + REQUIRE(nodeState.inputStates["input"].numQueued == 4); + + // Try to send another + ackSeq = ph.ping("gen2", 0); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + nodeState = ph.pipeline.getPipelineState().nodes(ph.getNodeId("bridge2")).detailed(); + REQUIRE(nodeState.inputStates["input"].numQueued == 4); + REQUIRE(nodeState.inputStates["input"].state == dai::NodeState::InputQueueState::State::BLOCKED); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + nodeState = ph.pipeline.getPipelineState().nodes(ph.getNodeId("gen2")).detailed(); + REQUIRE(nodeState.state == dai::NodeState::State::SENDING_OUTPUTS); + REQUIRE(nodeState.outputStates["output"].state == dai::NodeState::OutputQueueState::State::SENDING); + + // Read 1 from bridge2 (unblock) + ph.ping("bridge2", 0); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + nodeState = ph.pipeline.getPipelineState().nodes(ph.getNodeId("bridge2")).detailed(); + REQUIRE(nodeState.inputStates["input"].numQueued == 4); + REQUIRE(nodeState.inputStates["input"].state == dai::NodeState::InputQueueState::State::IDLE); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + nodeState = ph.pipeline.getPipelineState().nodes(ph.getNodeId("gen2")).detailed(); + REQUIRE(nodeState.outputStates[".output"].state == dai::NodeState::OutputQueueState::State::IDLE); + } + + // Let nodes run + for(const auto& nodeName : ph.getNodeNames()) { + ph.pingNoAck(nodeName, -1); + } + + std::this_thread::sleep_for(std::chrono::seconds(2)); + + try { + ph.stop(); + } catch(...) { + // Ignore + } +} + +// TEST_CASE("Node timings test") { +// PipelineHandler ph; +// ph.start(); +// +// // Let nodes run +// for(const auto& nodeName : ph.getNodeNames()) { +// ph.ping(nodeName, -1); +// } +// +// std::this_thread::sleep_for(std::chrono::seconds(3)); +// + // std::this_thread::sleep_for(std::chrono::milliseconds(100)); +// auto state = ph.pipeline.getPipelineState().nodes().detailed(); +// +// for(const auto& nodeName : ph.getNodeNames()) { +// auto nodeState = state.nodeStates.at(ph.getNodeId(nodeName)); +// REQUIRE(nodeState.mainLoopTiming.isValid()); +// REQUIRE(nodeState.inputsGetTiming.isValid()); +// REQUIRE(nodeState.outputsSendTiming.isValid()); +// for(const auto& [inputName, inputState] : nodeState.inputStates) { +// if(inputName.rfind("_ping") != std::string::npos) continue; +// REQUIRE(inputState.timing.isValid()); +// } +// for(const auto& [outputName, outputState] : nodeState.outputStates) { +// if(outputName.rfind("_ack") != std::string::npos) continue; +// REQUIRE(outputState.timing.isValid()); +// } +// for(const auto& [otherName, otherTiming] : nodeState.otherTimings) { +// REQUIRE(otherTiming.isValid()); +// } +// } +// ph.stop(); +// } From 312384a8b63c49686e189e21e4f7e08157913460 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 4 Nov 2025 16:45:45 +0100 Subject: [PATCH 048/124] Pipeline debugging bugfixes --- include/depthai/utility/CircularBuffer.hpp | 17 ++--- .../internal/PipelineEventAggregation.cpp | 49 ++++++++---- .../pipeline_debugging_host_test.cpp | 76 +++++++++---------- 3 files changed, 78 insertions(+), 64 deletions(-) diff --git a/include/depthai/utility/CircularBuffer.hpp b/include/depthai/utility/CircularBuffer.hpp index c0428924e..580630b88 100644 --- a/include/depthai/utility/CircularBuffer.hpp +++ b/include/depthai/utility/CircularBuffer.hpp @@ -7,6 +7,14 @@ namespace utility { template class CircularBuffer { + std::vector buffer; + size_t maxSize; + size_t index = 0; + + size_t start() const { + return (buffer.size() < maxSize) ? 0 : index; + } + public: CircularBuffer(size_t size) : maxSize(size) { buffer.reserve(size); @@ -154,15 +162,6 @@ class CircularBuffer { reverse_iterator rend() { return reverse_iterator(this, buffer.size()); } - - private: - std::vector buffer; - size_t maxSize; - size_t index = 0; - - size_t start() const { - return (buffer.size() < maxSize) ? 0 : index; - } }; } // namespace utility diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index c8c881e24..22077df68 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -28,7 +28,14 @@ class NodeEventAggregation { public: NodeEventAggregation(uint32_t windowSize, uint32_t statsUpdateIntervalMs, uint32_t eventWaitWindow, std::shared_ptr logger) - : logger(logger), windowSize(windowSize), statsUpdateIntervalMs(statsUpdateIntervalMs), eventWaitWindow(eventWaitWindow), eventsBuffer(windowSize) {} + : logger(logger), windowSize(windowSize), statsUpdateIntervalMs(statsUpdateIntervalMs), eventWaitWindow(eventWaitWindow), eventsBuffer(windowSize) { + inputsGetTimingsBuffer = std::make_unique>(windowSize); + outputsSendTimingsBuffer = std::make_unique>(windowSize); + mainLoopTimingsBuffer = std::make_unique>(windowSize); + inputsGetFpsBuffer = std::make_unique>(windowSize); + outputsSendFpsBuffer = std::make_unique>(windowSize); + } + NodeState state; utility::CircularBuffer eventsBuffer; std::unordered_map>> inputTimingsBuffers; @@ -227,6 +234,8 @@ class NodeEventAggregation { } inline void updateTimingStats(NodeState::TimingStats& stats, const utility::CircularBuffer& buffer) { + if(buffer.size() == 0) return; + stats.minMicros = std::min(stats.minMicros, buffer.last()); stats.maxMicros = std::max(stats.maxMicros, buffer.last()); stats.averageMicrosRecent = 0; @@ -323,24 +332,26 @@ class NodeEventAggregation { lastUpdated = std::chrono::steady_clock::now(); for(int i = (int)PipelineEvent::Type::CUSTOM; i <= (int)PipelineEvent::Type::OUTPUT_BLOCK; ++i) { // By instance - switch(event.type) { + switch((PipelineEvent::Type)i) { case PipelineEvent::Type::CUSTOM: - updateTimingStats(state.otherTimings[event.source].durationStats, *otherTimingsBuffers[event.source]); - updateFpsStats(state.otherTimings[event.source], *otherFpsBuffers[event.source]); + for(auto& [source, _] : otherTimingsBuffers) { + updateTimingStats(state.otherTimings[source].durationStats, *otherTimingsBuffers[source]); + updateFpsStats(state.otherTimings[source], *otherFpsBuffers[source]); + } break; case PipelineEvent::Type::LOOP: updateTimingStats(state.mainLoopTiming.durationStats, *mainLoopTimingsBuffer); state.mainLoopTiming.fps = 1e6f / (float)state.mainLoopTiming.durationStats.averageMicrosRecent; break; case PipelineEvent::Type::INPUT: - updateTimingStats(state.inputStates[event.source].timing.durationStats, *inputTimingsBuffers[event.source]); - updateFpsStats(state.inputStates[event.source].timing, *inputFpsBuffers[event.source]); - { - auto& qStats = state.inputStates[event.source].queueStats; - auto& qBuffer = *inputQueueSizesBuffers[event.source]; - qStats.maxQueued = std::max(qStats.maxQueued, *event.queueSize); + for(auto& [source, _] : inputTimingsBuffers) { + updateTimingStats(state.inputStates[source].timing.durationStats, *inputTimingsBuffers[source]); + updateFpsStats(state.inputStates[source].timing, *inputFpsBuffers[source]); + auto& qStats = state.inputStates[source].queueStats; + auto& qBuffer = *inputQueueSizesBuffers[source]; auto qBufferData = qBuffer.getBuffer(); std::sort(qBufferData.begin(), qBufferData.end()); + qStats.maxQueued = std::max(qStats.maxQueued, qBufferData.back()); qStats.minQueuedRecent = qBufferData.front(); qStats.maxQueuedRecent = qBufferData.back(); qStats.medianQueuedRecent = qBufferData[qBufferData.size() / 2]; @@ -350,8 +361,10 @@ class NodeEventAggregation { } break; case PipelineEvent::Type::OUTPUT: - updateTimingStats(state.outputStates[event.source].timing.durationStats, *outputTimingsBuffers[event.source]); - updateFpsStats(state.outputStates[event.source].timing, *outputFpsBuffers[event.source]); + for(auto& [source, _] : outputTimingsBuffers) { + updateTimingStats(state.outputStates[source].timing.durationStats, *outputTimingsBuffers[source]); + updateFpsStats(state.outputStates[source].timing, *outputFpsBuffers[source]); + } break; case PipelineEvent::Type::INPUT_BLOCK: updateTimingStats(state.inputsGetTiming.durationStats, *inputsGetTimingsBuffer); @@ -399,8 +412,10 @@ class PipelineEventHandler { std::unordered_map> events; bool gotEvents = false; for(auto& [k, v] : *inputs) { - events[k.second] = v.tryGet(); - gotEvents = gotEvents || (events[k.second] != nullptr); + try { + events[k.second] = v.tryGet(); + gotEvents = gotEvents || (events[k.second] != nullptr); + } catch(const dai::MessageQueue::QueueException&) {} } for(auto& [k, event] : events) { if(event != nullptr) { @@ -409,7 +424,7 @@ class PipelineEventHandler { nodeStates.at(event->nodeId).add(*event); } } - if(!gotEvents) { + if(!gotEvents && running) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } @@ -432,6 +447,10 @@ class PipelineEventHandler { } return updated; } + + ~PipelineEventHandler() { + stop(); + } }; void PipelineEventAggregation::setRunOnHost(bool runOnHost) { diff --git a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp index 8fe3760b7..501d0b937 100644 --- a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp +++ b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp @@ -1,4 +1,5 @@ #include + #include #include @@ -349,7 +350,7 @@ TEST_CASE("Node states test") { REQUIRE(nodeState.inputStates["input"].state == dai::NodeState::InputQueueState::State::IDLE); std::this_thread::sleep_for(std::chrono::milliseconds(100)); nodeState = ph.pipeline.getPipelineState().nodes(ph.getNodeId("gen2")).detailed(); - REQUIRE(nodeState.outputStates[".output"].state == dai::NodeState::OutputQueueState::State::IDLE); + REQUIRE(nodeState.outputStates["output"].state == dai::NodeState::OutputQueueState::State::IDLE); } // Let nodes run @@ -357,45 +358,40 @@ TEST_CASE("Node states test") { ph.pingNoAck(nodeName, -1); } - std::this_thread::sleep_for(std::chrono::seconds(2)); + std::this_thread::sleep_for(std::chrono::seconds(1)); - try { - ph.stop(); - } catch(...) { - // Ignore - } + ph.stop(); } -// TEST_CASE("Node timings test") { -// PipelineHandler ph; -// ph.start(); -// -// // Let nodes run -// for(const auto& nodeName : ph.getNodeNames()) { -// ph.ping(nodeName, -1); -// } -// -// std::this_thread::sleep_for(std::chrono::seconds(3)); -// - // std::this_thread::sleep_for(std::chrono::milliseconds(100)); -// auto state = ph.pipeline.getPipelineState().nodes().detailed(); -// -// for(const auto& nodeName : ph.getNodeNames()) { -// auto nodeState = state.nodeStates.at(ph.getNodeId(nodeName)); -// REQUIRE(nodeState.mainLoopTiming.isValid()); -// REQUIRE(nodeState.inputsGetTiming.isValid()); -// REQUIRE(nodeState.outputsSendTiming.isValid()); -// for(const auto& [inputName, inputState] : nodeState.inputStates) { -// if(inputName.rfind("_ping") != std::string::npos) continue; -// REQUIRE(inputState.timing.isValid()); -// } -// for(const auto& [outputName, outputState] : nodeState.outputStates) { -// if(outputName.rfind("_ack") != std::string::npos) continue; -// REQUIRE(outputState.timing.isValid()); -// } -// for(const auto& [otherName, otherTiming] : nodeState.otherTimings) { -// REQUIRE(otherTiming.isValid()); -// } -// } -// ph.stop(); -// } +TEST_CASE("Node timings test") { + PipelineHandler ph; + ph.start(); + + // Let nodes run + for(const auto& nodeName : ph.getNodeNames()) { + ph.ping(nodeName, -1); + } + + std::this_thread::sleep_for(std::chrono::seconds(3)); + + auto state = ph.pipeline.getPipelineState().nodes().detailed(); + + for(const auto& nodeName : ph.getNodeNames()) { + auto nodeState = state.nodeStates.at(ph.getNodeId(nodeName)); + REQUIRE(nodeState.mainLoopTiming.isValid()); + if(nodeName.find("gen") == std::string::npos) REQUIRE(nodeState.inputsGetTiming.isValid()); + if(nodeName.find("cons") == std::string::npos) REQUIRE(nodeState.outputsSendTiming.isValid()); + for(const auto& [inputName, inputState] : nodeState.inputStates) { + if(inputName.rfind("_ping") != std::string::npos) continue; + REQUIRE(inputState.timing.isValid()); + } + for(const auto& [outputName, outputState] : nodeState.outputStates) { + if(outputName.rfind("_ack") != std::string::npos) continue; + REQUIRE(outputState.timing.isValid()); + } + for(const auto& [otherName, otherTiming] : nodeState.otherTimings) { + REQUIRE(otherTiming.isValid()); + } + } + ph.stop(); +} From 5e1847b7b5931dc2208031104dbb95ec92225875 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 5 Nov 2025 10:16:20 +0100 Subject: [PATCH 049/124] Pipeline debugging test improvements, bugfixes --- .../python/src/pipeline/PipelineBindings.cpp | 2 +- include/depthai/pipeline/Pipeline.hpp | 4 +++ src/pipeline/Pipeline.cpp | 9 +++++- .../internal/PipelineEventAggregation.cpp | 5 +-- .../pipeline_debugging_host_test.cpp | 31 +++++++++++++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/bindings/python/src/pipeline/PipelineBindings.cpp b/bindings/python/src/pipeline/PipelineBindings.cpp index 17b4472bf..5e65d6241 100644 --- a/bindings/python/src/pipeline/PipelineBindings.cpp +++ b/bindings/python/src/pipeline/PipelineBindings.cpp @@ -1,4 +1,3 @@ - #include "PipelineBindings.hpp" #include @@ -312,6 +311,7 @@ void PipelineBindings::bind(pybind11::module& m, void* pCallstack) { .def("processTasks", &Pipeline::processTasks, py::arg("waitForTasks") = false, py::arg("timeoutSeconds") = -1.0) .def("enableHolisticRecord", &Pipeline::enableHolisticRecord, py::arg("recordConfig"), DOC(dai, Pipeline, enableHolisticRecord)) .def("enableHolisticReplay", &Pipeline::enableHolisticReplay, py::arg("recordingPath"), DOC(dai, Pipeline, enableHolisticReplay)) + .def("enablePipelineDebugging", &Pipeline::enablePipelineDebugging, py::arg("enable") = true, DOC(dai, Pipeline, enablePipelineDebugging)) .def("getPipelineState", &Pipeline::getPipelineState, DOC(dai, Pipeline, getPipelineState)); ; } diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index 4991b770a..e99d23fd1 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -126,6 +126,7 @@ class PipelineImpl : public std::enable_shared_from_this { bool pipelineOnHost = true; // Pipeline events + bool enablePipelineDebugging = false; std::shared_ptr pipelineStateOut; std::shared_ptr pipelineStateRequest; @@ -530,6 +531,9 @@ class Pipeline { void enableHolisticRecord(const RecordConfig& config); void enableHolisticReplay(const std::string& pathToRecording); + /// Pipeline debugging + void enablePipelineDebugging(bool enable = true); + // Pipeline state getters PipelineStateApi getPipelineState(); }; diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 40ec95904..5b29ed306 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -669,7 +669,7 @@ void PipelineImpl::build() { } // Create pipeline event aggregator node and link - bool enablePipelineDebugging = utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false); + enablePipelineDebugging = enablePipelineDebugging || utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false); if(enablePipelineDebugging) { // Check if any nodes are on host or device bool hasHostNodes = false; @@ -1144,6 +1144,13 @@ void Pipeline::enableHolisticReplay(const std::string& pathToRecording) { impl()->enableHolisticRecordReplay = true; } +void Pipeline::enablePipelineDebugging(bool enable) { + if(this->isBuilt()) { + throw std::runtime_error("Cannot change pipeline debugging state after pipeline is built"); + } + impl()->enablePipelineDebugging = enable; +} + PipelineStateApi Pipeline::getPipelineState() { return impl()->getPipelineState(); } diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 22077df68..5d20b162a 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -276,7 +276,7 @@ class NodeEventAggregation { public: void add(PipelineEvent& event) { using namespace std::chrono; - if(event.type == PipelineEvent::Type::INPUT && event.interval == PipelineEvent::Interval::END) { + if(event.type == PipelineEvent::Type::INPUT && (event.interval == PipelineEvent::Interval::END || event.interval == PipelineEvent::Interval::NONE)) { if(event.queueSize.has_value()) { inputQueueSizesBuffers.try_emplace(event.source, std::make_unique>(windowSize)); inputQueueSizesBuffers[event.source]->add(*event.queueSize); @@ -415,7 +415,8 @@ class PipelineEventHandler { try { events[k.second] = v.tryGet(); gotEvents = gotEvents || (events[k.second] != nullptr); - } catch(const dai::MessageQueue::QueueException&) {} + } catch(const dai::MessageQueue::QueueException&) { + } } for(auto& [k, event] : events) { if(event != nullptr) { diff --git a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp index 501d0b937..0c4ca3890 100644 --- a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp +++ b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include "depthai/depthai.hpp" @@ -195,6 +196,8 @@ class PipelineHandler { Pipeline pipeline; PipelineHandler() : pipeline(false) { + pipeline.enablePipelineDebugging(); + auto gen1 = pipeline.create(); nodeIds["gen1"] = gen1->id; auto gen2 = pipeline.create(); @@ -360,6 +363,12 @@ TEST_CASE("Node states test") { std::this_thread::sleep_for(std::chrono::seconds(1)); + { + auto inputState = ph.pipeline.getPipelineState().nodes(ph.getNodeId("bridge2")).inputs("input"); + REQUIRE(inputState.isValid()); + REQUIRE(inputState.queueStats.maxQueued == 4); + } + ph.stop(); } @@ -378,16 +387,38 @@ TEST_CASE("Node timings test") { for(const auto& nodeName : ph.getNodeNames()) { auto nodeState = state.nodeStates.at(ph.getNodeId(nodeName)); + REQUIRE(nodeState.mainLoopTiming.isValid()); + REQUIRE(nodeState.mainLoopTiming.durationStats.averageMicrosRecent == Catch::Approx(100000).margin(50000)); + REQUIRE(nodeState.mainLoopTiming.durationStats.medianMicrosRecent == Catch::Approx(100000).margin(50000)); + REQUIRE(nodeState.mainLoopTiming.durationStats.minMicrosRecent == Catch::Approx(100000).margin(10000)); + REQUIRE(nodeState.mainLoopTiming.durationStats.minMicros == Catch::Approx(100000).margin(10000)); + REQUIRE(nodeState.mainLoopTiming.durationStats.maxMicrosRecent == Catch::Approx(150000).margin(50000)); + REQUIRE(nodeState.mainLoopTiming.durationStats.maxMicros == Catch::Approx(150000).margin(50000)); + if(nodeName.find("gen") == std::string::npos) REQUIRE(nodeState.inputsGetTiming.isValid()); if(nodeName.find("cons") == std::string::npos) REQUIRE(nodeState.outputsSendTiming.isValid()); for(const auto& [inputName, inputState] : nodeState.inputStates) { if(inputName.rfind("_ping") != std::string::npos) continue; REQUIRE(inputState.timing.isValid()); + REQUIRE(inputState.timing.fps == Catch::Approx(10.f).margin(5.f)); + REQUIRE(inputState.timing.durationStats.minMicros <= 0.1e6); + REQUIRE(inputState.timing.durationStats.maxMicros <= 0.2e6); + REQUIRE(inputState.timing.durationStats.averageMicrosRecent <= 0.2e6); + REQUIRE(inputState.timing.durationStats.minMicrosRecent <= 0.12e6); + REQUIRE(inputState.timing.durationStats.maxMicrosRecent <= 0.2e6); + REQUIRE(inputState.timing.durationStats.medianMicrosRecent <= 0.2e6); } for(const auto& [outputName, outputState] : nodeState.outputStates) { if(outputName.rfind("_ack") != std::string::npos) continue; REQUIRE(outputState.timing.isValid()); + REQUIRE(outputState.timing.fps == Catch::Approx(10.f).margin(5.f)); + REQUIRE(outputState.timing.durationStats.minMicros <= 0.01e6); + REQUIRE(outputState.timing.durationStats.maxMicros <= 0.01e6); + REQUIRE(outputState.timing.durationStats.averageMicrosRecent <= 0.01e6); + REQUIRE(outputState.timing.durationStats.minMicrosRecent <= 0.01e6); + REQUIRE(outputState.timing.durationStats.maxMicrosRecent <= 0.01e6); + REQUIRE(outputState.timing.durationStats.medianMicrosRecent <= 0.01e6); } for(const auto& [otherName, otherTiming] : nodeState.otherTimings) { REQUIRE(otherTiming.isValid()); From cc8638e2fc8d400c9136facd6b176ef21255d309 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 5 Nov 2025 13:57:14 +0100 Subject: [PATCH 050/124] Add pipeline debugging test for rvc4 --- .../utility/PipelineEventDispatcher.hpp | 9 ++- .../PipelineEventDispatcherInterface.hpp | 4 +- src/pipeline/node/Sync.cpp | 2 +- .../internal/PipelineEventAggregation.cpp | 44 ++++++------ src/utility/PipelineEventDispatcher.cpp | 56 ++++++++++----- tests/CMakeLists.txt | 9 ++- .../pipeline_debugging_rvc4_test.cpp | 68 +++++++++++++++++++ .../pipeline_debugging_host_test.cpp | 5 +- 8 files changed, 145 insertions(+), 52 deletions(-) create mode 100644 tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index e310644dc..df5f150ba 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -25,7 +25,7 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void checkNodeId(); uint32_t sequenceNum = 0; - + std::mutex mutex; public: @@ -38,14 +38,13 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void startInputEvent(const std::string& source, std::optional queueSize = std::nullopt) override; void startOutputEvent(const std::string& source) override; void startCustomEvent(const std::string& source) override; - void endEvent(PipelineEvent::Type type, - const std::string& source, - std::optional queueSize = std::nullopt) override; + void endEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize = std::nullopt) override; void endInputEvent(const std::string& source, std::optional queueSize = std::nullopt) override; void endOutputEvent(const std::string& source) override; void endCustomEvent(const std::string& source) override; + void startTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) override; + void endTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) override; void pingEvent(PipelineEvent::Type type, const std::string& source) override; - void pingTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) override; void pingMainLoopEvent() override; void pingCustomEvent(const std::string& source) override; void pingInputEvent(const std::string& source, int32_t status, std::optional queueSize = std::nullopt) override; diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index 0c2ad7640..0e8f6d5df 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -36,9 +36,9 @@ class PipelineEventDispatcherInterface { virtual void endInputEvent(const std::string& source, std::optional queueSize = std::nullopt) = 0; virtual void endOutputEvent(const std::string& source) = 0; virtual void endCustomEvent(const std::string& source) = 0; + virtual void startTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) = 0; + virtual void endTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) = 0; virtual void pingEvent(PipelineEvent::Type type, const std::string& source) = 0; - // The sequenceNum should be unique. Duration is calculated from sequenceNum - 1 to sequenceNum - virtual void pingTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) = 0; virtual void pingMainLoopEvent() = 0; virtual void pingCustomEvent(const std::string& source) = 0; virtual void pingInputEvent(const std::string& source, int32_t status, std::optional queueSize = std::nullopt) = 0; diff --git a/src/pipeline/node/Sync.cpp b/src/pipeline/node/Sync.cpp index 7a45432ef..4eac7768f 100644 --- a/src/pipeline/node/Sync.cpp +++ b/src/pipeline/node/Sync.cpp @@ -57,7 +57,7 @@ void Sync::run() { auto tAbsoluteBeginning = steady_clock::now(); std::unordered_map> inputFrames; { - auto eventBlock = this->inputBlockEvent(); + auto blockEvent = this->inputBlockEvent(); for(auto name : inputNames) { logger->trace("Receiving input: {}", name); diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 5d20b162a..45940807b 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -83,7 +83,7 @@ class NodeEventAggregation { auto& ongoingEvents = [&]() -> std::unique_ptr>>& { switch(event.type) { case PipelineEvent::Type::LOOP: - throw std::runtime_error("LOOP event should not be an interval"); + return ongoingMainLoopEvents; case PipelineEvent::Type::INPUT: return ongoingInputEvents[event.source]; case PipelineEvent::Type::OUTPUT: @@ -100,7 +100,7 @@ class NodeEventAggregation { auto& timingsBuffer = [&]() -> std::unique_ptr>& { switch(event.type) { case PipelineEvent::Type::LOOP: - throw std::runtime_error("LOOP event should not be an interval"); + return mainLoopTimingsBuffer; case PipelineEvent::Type::INPUT: return inputTimingsBuffers[event.source]; case PipelineEvent::Type::OUTPUT: @@ -117,7 +117,7 @@ class NodeEventAggregation { auto& fpsBuffer = [&]() -> std::unique_ptr>& { switch(event.type) { case PipelineEvent::Type::LOOP: - throw std::runtime_error("LOOP event should not be an interval"); + return emptyTimeBuffer; case PipelineEvent::Type::INPUT: return inputFpsBuffers[event.source]; case PipelineEvent::Type::OUTPUT: @@ -146,7 +146,7 @@ class NodeEventAggregation { eventsBuffer.add(durationEvent); timingsBuffer->add(durationEvent.durationUs); - fpsBuffer->add({durationEvent.startEvent.getTimestamp(), durationEvent.startEvent.getSequenceNum()}); + if(event.type != PipelineEvent::Type::LOOP) fpsBuffer->add({durationEvent.startEvent.getTimestamp(), durationEvent.startEvent.getSequenceNum()}); *ongoingEvent = std::nullopt; @@ -276,21 +276,17 @@ class NodeEventAggregation { public: void add(PipelineEvent& event) { using namespace std::chrono; - if(event.type == PipelineEvent::Type::INPUT && (event.interval == PipelineEvent::Interval::END || event.interval == PipelineEvent::Interval::NONE)) { - if(event.queueSize.has_value()) { - inputQueueSizesBuffers.try_emplace(event.source, std::make_unique>(windowSize)); - inputQueueSizesBuffers[event.source]->add(*event.queueSize); - } else { - throw std::runtime_error(fmt::format("INPUT END event must have queue size set source: {}, node {}", event.source, event.nodeId)); - } - } // Update states switch(event.type) { case PipelineEvent::Type::CUSTOM: case PipelineEvent::Type::LOOP: break; case PipelineEvent::Type::INPUT: - if(event.queueSize.has_value()) state.inputStates[event.source].numQueued = *event.queueSize; + if((event.interval == PipelineEvent::Interval::END || event.interval == PipelineEvent::Interval::NONE) && event.queueSize.has_value()) { + state.inputStates[event.source].numQueued = *event.queueSize; + inputQueueSizesBuffers.try_emplace(event.source, std::make_unique>(windowSize)); + inputQueueSizesBuffers[event.source]->add(*event.queueSize); + } switch(event.interval) { case PipelineEvent::Interval::START: state.inputStates[event.source].state = NodeState::InputQueueState::State::WAITING; @@ -347,16 +343,18 @@ class NodeEventAggregation { for(auto& [source, _] : inputTimingsBuffers) { updateTimingStats(state.inputStates[source].timing.durationStats, *inputTimingsBuffers[source]); updateFpsStats(state.inputStates[source].timing, *inputFpsBuffers[source]); - auto& qStats = state.inputStates[source].queueStats; - auto& qBuffer = *inputQueueSizesBuffers[source]; - auto qBufferData = qBuffer.getBuffer(); - std::sort(qBufferData.begin(), qBufferData.end()); - qStats.maxQueued = std::max(qStats.maxQueued, qBufferData.back()); - qStats.minQueuedRecent = qBufferData.front(); - qStats.maxQueuedRecent = qBufferData.back(); - qStats.medianQueuedRecent = qBufferData[qBufferData.size() / 2]; - if(qBufferData.size() % 2 == 0) { - qStats.medianQueuedRecent = (qStats.medianQueuedRecent + qBufferData[qBufferData.size() / 2 - 1]) / 2; + if(inputQueueSizesBuffers.find(source) != inputQueueSizesBuffers.end()) { + auto& qStats = state.inputStates[source].queueStats; + auto& qBuffer = *inputQueueSizesBuffers[source]; + auto qBufferData = qBuffer.getBuffer(); + std::sort(qBufferData.begin(), qBufferData.end()); + qStats.maxQueued = std::max(qStats.maxQueued, qBufferData.back()); + qStats.minQueuedRecent = qBufferData.front(); + qStats.maxQueuedRecent = qBufferData.back(); + qStats.medianQueuedRecent = qBufferData[qBufferData.size() / 2]; + if(qBufferData.size() % 2 == 0) { + qStats.medianQueuedRecent = (qStats.medianQueuedRecent + qBufferData[qBufferData.size() / 2 - 1]) / 2; + } } } break; diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index e551491c2..a4420bbd0 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -112,32 +112,49 @@ void PipelineEventDispatcher::endOutputEvent(const std::string& source) { void PipelineEventDispatcher::endCustomEvent(const std::string& source) { endEvent(PipelineEvent::Type::CUSTOM, source, std::nullopt); } -void PipelineEventDispatcher::pingEvent(PipelineEvent::Type type, const std::string& source) { +void PipelineEventDispatcher::startTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { if(!sendEvents) return; checkNodeId(); if(blacklist(type, source)) return; std::lock_guard lock(mutex); - auto now = std::chrono::steady_clock::now(); - auto& event = events[makeKey(type, source)]; - if(event.ongoing) { - throw std::runtime_error("Event with name " + source + " is already ongoing"); + event.event.setTimestamp(std::chrono::steady_clock::now()); + event.event.tsDevice = event.event.ts; + event.event.sequenceNum = sequenceNum; + event.event.nodeId = nodeId; + event.event.interval = PipelineEvent::Interval::START; + event.event.type = type; + event.event.source = source; + event.ongoing = false; + + if(out) { + out->send(std::make_shared(event.event)); } - event.event.setTimestamp(now); +} +void PipelineEventDispatcher::endTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { + if(!sendEvents) return; + checkNodeId(); + if(blacklist(type, source)) return; + + std::lock_guard lock(mutex); + + auto& event = events[makeKey(type, source)]; + event.event.setTimestamp(std::chrono::steady_clock::now()); event.event.tsDevice = event.event.ts; - ++event.event.sequenceNum; + event.event.sequenceNum = sequenceNum; event.event.nodeId = nodeId; - event.event.interval = PipelineEvent::Interval::NONE; + event.event.interval = PipelineEvent::Interval::END; event.event.type = type; event.event.source = source; + event.ongoing = false; if(out) { out->send(std::make_shared(event.event)); } } -void PipelineEventDispatcher::pingTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { +void PipelineEventDispatcher::pingEvent(PipelineEvent::Type type, const std::string& source) { if(!sendEvents) return; checkNodeId(); if(blacklist(type, source)) return; @@ -146,17 +163,20 @@ void PipelineEventDispatcher::pingTrackedEvent(PipelineEvent::Type type, const s auto now = std::chrono::steady_clock::now(); - auto event = std::make_shared(); - event->setTimestamp(now); - event->tsDevice = event->ts; - event->sequenceNum = sequenceNum; - event->nodeId = nodeId; - event->interval = PipelineEvent::Interval::NONE; - event->type = type; - event->source = source; + auto& event = events[makeKey(type, source)]; + if(event.ongoing) { + throw std::runtime_error("Event with name " + source + " is already ongoing"); + } + event.event.setTimestamp(now); + event.event.tsDevice = event.event.ts; + ++event.event.sequenceNum; + event.event.nodeId = nodeId; + event.event.interval = PipelineEvent::Interval::NONE; + event.event.type = type; + event.event.source = source; if(out) { - out->send(event); + out->send(std::make_shared(event.event)); } } void PipelineEventDispatcher::pingMainLoopEvent() { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 198d9b11c..21d865384 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -296,6 +296,10 @@ dai_set_test_labels(platform_test onhost ci) target_compile_definitions(platform_test PRIVATE FSLOCK_DUMMY_PATH="$") add_dependencies(platform_test fslock_dummy) +# Pipeline debugging tests +dai_add_test(pipeline_debugging_host_test src/onhost_tests/pipeline_debugging_host_test.cpp) +dai_set_test_labels(pipeline_debugging_host_test onhost ci) + # Datatype tests dai_add_test(nndata_test src/onhost_tests/pipeline/datatype/nndata_test.cpp) dai_set_test_labels(nndata_test onhost ci) @@ -353,8 +357,9 @@ dai_add_test(pipeline_test src/ondevice_tests/pipeline_test.cpp) dai_set_test_labels(pipeline_test ondevice rvc2_all ci) # Pipeline debugging tests -dai_add_test(pipeline_debugging_host_test src/onhost_tests/pipeline_debugging_host_test.cpp) -dai_set_test_labels(pipeline_debugging_host_test onhost ci) +dai_add_test(pipeline_debugging_rvc4_test src/ondevice_tests/pipeline_debugging_rvc4_test.cpp) +dai_set_test_labels(pipeline_debugging_rvc4_test rvc4 ci) +target_compile_definitions(pipeline_debugging_rvc4_test PRIVATE VIDEO_PATH="${construction_vest}") # Device USB Speed and serialization macros test dai_add_test(device_usbspeed_test src/ondevice_tests/device_usbspeed_test.cpp) diff --git a/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp b/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp new file mode 100644 index 000000000..52b3d9e3e --- /dev/null +++ b/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp @@ -0,0 +1,68 @@ +#include + +#include +#include +#include + +#include "depthai/depthai.hpp" + +#define VIDEO_DURATION_SECONDS 5 + +TEST_CASE("Object Tracker") { + // Create pipeline + dai::Pipeline pipeline; + pipeline.enablePipelineDebugging(); + + // Define sources and outputs + auto replay = pipeline.create(); + replay->setReplayVideoFile(VIDEO_PATH); + replay->setLoop(false); + + // Create spatial detection network + dai::NNModelDescription modelDescription{"yolov6-nano"}; + auto detectionNetwork = pipeline.create()->build(replay, modelDescription); + detectionNetwork->setConfidenceThreshold(0.6f); + detectionNetwork->input.setBlocking(false); + + // Create object tracker + auto objectTracker = pipeline.create(); + objectTracker->setDetectionLabelsToTrack({0}); // track only person + objectTracker->setTrackerIdAssignmentPolicy(dai::TrackerIdAssignmentPolicy::SMALLEST_ID); + + // Create output queues + auto tracklets = objectTracker->out.createOutputQueue(); + + // Link nodes + detectionNetwork->passthrough.link(objectTracker->inputTrackerFrame); + + detectionNetwork->passthrough.link(objectTracker->inputDetectionFrame); + detectionNetwork->out.link(objectTracker->inputDetections); + + // Start pipeline + pipeline.start(); + + auto start = std::chrono::steady_clock::now(); + while(pipeline.isRunning() && std::chrono::steady_clock::now() - start < std::chrono::seconds(VIDEO_DURATION_SECONDS)) { + auto track = tracklets->get(); + } + + auto state = pipeline.getPipelineState().nodes().detailed(); + + for(const auto& [nodeId, nodeState] : state.nodeStates) { + auto node = pipeline.getNode(nodeId); + REQUIRE(nodeState.mainLoopTiming.isValid()); + if(!node->getInputs().empty()) REQUIRE(nodeState.inputsGetTiming.isValid()); + if(!node->getOutputs().empty()) REQUIRE(nodeState.outputsSendTiming.isValid()); + for(const auto& [inputName, inputState] : nodeState.inputStates) { + if(std::string(node->getName()) == "ObjectTracker" && inputName == "inputConfig") continue; // This example does not use inputConfig + if(std::string(node->getName()) == "ObjectTracker" && inputName == "inputDetectionFrame") continue; // This example does not use inputDetectionFrame + REQUIRE(inputState.timing.isValid()); + } + for(const auto& [outputName, outputState] : nodeState.outputStates) { + REQUIRE(outputState.timing.isValid()); + } + for(const auto& [otherName, otherTiming] : nodeState.otherTimings) { + REQUIRE(otherTiming.isValid()); + } + } +} diff --git a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp index 0c4ca3890..bf3486046 100644 --- a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp +++ b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp @@ -69,7 +69,10 @@ class ConsumerNode : public node::CustomThreadedNode { void run() override { step(0); - while(mainLoop()) { + volatile int sequence = 0; + while(isRunning()) { + this->pipelineEventDispatcher->endTrackedEvent(PipelineEvent::Type::LOOP, "_mainLoop", sequence); + this->pipelineEventDispatcher->startTrackedEvent(PipelineEvent::Type::LOOP, "_mainLoop", ++sequence); std::shared_ptr msg = nullptr; step(1); { From 5632e033500157bc32e774752eb02c5f94afbc2c Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 5 Nov 2025 14:35:19 +0100 Subject: [PATCH 051/124] RVC4 FW: Pipeline debugging fixes, add to nodes --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 29259b083..e886e289c 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+67877eee00768bd2cd4a59daa02ace2ef5e87082") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+8aa456bd0f98b954bd386beeeb0da6b6b96afb3c") From ddef6df0dfffb82189fa09379c0f76f49492e741 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 5 Nov 2025 16:47:54 +0100 Subject: [PATCH 052/124] TMP: run tests with pipeline debugging enabled --- tests/CMakeLists.txt | 3 +++ tests/run_tests.py | 4 ++-- tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 21d865384..2b390e37b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -360,6 +360,9 @@ dai_set_test_labels(pipeline_test ondevice rvc2_all ci) dai_add_test(pipeline_debugging_rvc4_test src/ondevice_tests/pipeline_debugging_rvc4_test.cpp) dai_set_test_labels(pipeline_debugging_rvc4_test rvc4 ci) target_compile_definitions(pipeline_debugging_rvc4_test PRIVATE VIDEO_PATH="${construction_vest}") +dai_add_test(pipeline_debugging_rvc2_test src/ondevice_tests/pipeline_debugging_rvc2_test.cpp) +dai_set_test_labels(pipeline_debugging_rvc2_test rvc2 ci) +target_compile_definitions(pipeline_debugging_rvc2_test PRIVATE VIDEO_PATH="${construction_vest}") # Device USB Speed and serialization macros test dai_add_test(device_usbspeed_test src/ondevice_tests/device_usbspeed_test.cpp) diff --git a/tests/run_tests.py b/tests/run_tests.py index 9a4d291b6..fade2ee57 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -108,12 +108,12 @@ def compute_or_result(results): all_configs = [ { "name": "Host", - "env": {}, + "env": {"DEPTHAI_PIPELINE_DEBUGGING": "1"}, "labels": ["onhost"], }, { "name": "RVC4", - "env": {"DEPTHAI_PLATFORM": "rvc4"}, + "env": {"DEPTHAI_PLATFORM": "rvc4", "DEPTHAI_PIPELINE_DEBUGGING": "1"}, "labels": ["rvc4"], }, { diff --git a/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp b/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp index 52b3d9e3e..0b422fa30 100644 --- a/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp +++ b/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp @@ -8,7 +8,7 @@ #define VIDEO_DURATION_SECONDS 5 -TEST_CASE("Object Tracker") { +TEST_CASE("Object Tracker Pipeline Debugging") { // Create pipeline dai::Pipeline pipeline; pipeline.enablePipelineDebugging(); From 411134a6a01714497c7d0e89f0aa8d03e8d6d011 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 6 Nov 2025 11:49:24 +0100 Subject: [PATCH 053/124] RVC2 FW: Pipeline debugging bugfixes --- cmake/Depthai/DepthaiDeviceSideConfig.cmake | 2 +- .../pipeline_debugging_rvc2_test.cpp | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/src/ondevice_tests/pipeline_debugging_rvc2_test.cpp diff --git a/cmake/Depthai/DepthaiDeviceSideConfig.cmake b/cmake/Depthai/DepthaiDeviceSideConfig.cmake index f9a2999b1..3d8c0bc1c 100644 --- a/cmake/Depthai/DepthaiDeviceSideConfig.cmake +++ b/cmake/Depthai/DepthaiDeviceSideConfig.cmake @@ -2,7 +2,7 @@ set(DEPTHAI_DEVICE_SIDE_MATURITY "snapshot") # "full commit hash of device side binary" -set(DEPTHAI_DEVICE_SIDE_COMMIT "f81edb9c2328ee1f80a6f80c3fade0af0076a3ff") +set(DEPTHAI_DEVICE_SIDE_COMMIT "28de549d4b5cb0a6c659420f68a16a18477c147c") # "version if applicable" set(DEPTHAI_DEVICE_SIDE_VERSION "") diff --git a/tests/src/ondevice_tests/pipeline_debugging_rvc2_test.cpp b/tests/src/ondevice_tests/pipeline_debugging_rvc2_test.cpp new file mode 100644 index 000000000..00846ed80 --- /dev/null +++ b/tests/src/ondevice_tests/pipeline_debugging_rvc2_test.cpp @@ -0,0 +1,68 @@ +#include + +#include +#include +#include + +#include "depthai/depthai.hpp" + +#define VIDEO_DURATION_SECONDS 5 + +TEST_CASE("Object Tracker Pipeline Debugging") { + // Create pipeline + dai::Pipeline pipeline; + pipeline.enablePipelineDebugging(); + + // Define sources and outputs + auto replay = pipeline.create(); + replay->setReplayVideoFile(VIDEO_PATH); + replay->setLoop(false); + + // Create spatial detection network + dai::NNModelDescription modelDescription{"yolov6-nano"}; + auto detectionNetwork = pipeline.create()->build(replay, modelDescription); + detectionNetwork->setConfidenceThreshold(0.6f); + detectionNetwork->input.setBlocking(false); + + // Create object tracker + auto objectTracker = pipeline.create(); + objectTracker->setDetectionLabelsToTrack({0}); // track only person + objectTracker->setTrackerIdAssignmentPolicy(dai::TrackerIdAssignmentPolicy::SMALLEST_ID); + + // Create output queues + auto tracklets = objectTracker->out.createOutputQueue(); + + // Link nodes + detectionNetwork->passthrough.link(objectTracker->inputTrackerFrame); + + detectionNetwork->passthrough.link(objectTracker->inputDetectionFrame); + detectionNetwork->out.link(objectTracker->inputDetections); + + // Start pipeline + pipeline.start(); + + auto start = std::chrono::steady_clock::now(); + while(pipeline.isRunning() && std::chrono::steady_clock::now() - start < std::chrono::seconds(VIDEO_DURATION_SECONDS)) { + auto track = tracklets->get(); + } + + auto state = pipeline.getPipelineState().nodes().detailed(); + + for(const auto& [nodeId, nodeState] : state.nodeStates) { + auto node = pipeline.getNode(nodeId); + if(node->id == 11) continue; + // REQUIRE(nodeState.mainLoopTiming.isValid()); + // if(!node->getInputs().empty()) REQUIRE(nodeState.inputsGetTiming.isValid()); + // if(!node->getOutputs().empty()) REQUIRE(nodeState.outputsSendTiming.isValid()); + for(const auto& [inputName, inputState] : nodeState.inputStates) { + if(std::string(node->getName()) == "ObjectTracker" && inputName == "inputConfig") continue; // This example does not use inputConfig + REQUIRE(inputState.timing.isValid()); + } + for(const auto& [outputName, outputState] : nodeState.outputStates) { + REQUIRE(outputState.timing.isValid()); + } + for(const auto& [otherName, otherTiming] : nodeState.otherTimings) { + REQUIRE(otherTiming.isValid()); + } + } +} From 1dd9736894d2862379c2a981422c19ea343215ac Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 6 Nov 2025 12:15:55 +0100 Subject: [PATCH 054/124] Add DEPTHAI_PIPELINE_DEBUGGING to tests env --- tests/run_tests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/run_tests.py b/tests/run_tests.py index fade2ee57..dbb1d0090 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -45,6 +45,7 @@ def run(self): # Function to run ctest with specific environment variables and labels def run_ctest(env_vars, labels, blocking=True, name=""): env = os.environ.copy() + env_vars["DEPTHAI_PIPELINE_DEBUGGING"] = "1" env.update(env_vars) cmd = ["ctest", "--no-tests=error", "-VV", "-L", "^ci$", "--timeout", "1000", "-C", "Release"] @@ -108,12 +109,12 @@ def compute_or_result(results): all_configs = [ { "name": "Host", - "env": {"DEPTHAI_PIPELINE_DEBUGGING": "1"}, + "env": {}, "labels": ["onhost"], }, { "name": "RVC4", - "env": {"DEPTHAI_PLATFORM": "rvc4", "DEPTHAI_PIPELINE_DEBUGGING": "1"}, + "env": {"DEPTHAI_PLATFORM": "rvc4"}, "labels": ["rvc4"], }, { From 4677f08676413feb5d9c6d082e4002dbc6f4c24c Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 6 Nov 2025 12:40:32 +0100 Subject: [PATCH 055/124] PR fixes --- examples/cpp/HostNodes/image_manip_host.cpp | 10 +--------- .../cpp/ObjectTracker/object_tracker_replay.cpp | 17 ----------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/examples/cpp/HostNodes/image_manip_host.cpp b/examples/cpp/HostNodes/image_manip_host.cpp index 8688237e1..c43d75c37 100644 --- a/examples/cpp/HostNodes/image_manip_host.cpp +++ b/examples/cpp/HostNodes/image_manip_host.cpp @@ -40,13 +40,5 @@ int main(int argc, char** argv) { pipeline.start(); - // TODO remove before merge - while(pipeline.isRunning()) { - std::this_thread::sleep_for(std::chrono::milliseconds(5000)); - std::cout << "Pipeline state: " << pipeline.getPipelineState().nodes().detailed().str() << std::endl; - } - - pipeline.stop(); - // - // pipeline.wait(); + pipeline.wait(); } diff --git a/examples/cpp/ObjectTracker/object_tracker_replay.cpp b/examples/cpp/ObjectTracker/object_tracker_replay.cpp index 6b44ae83c..7a5586826 100644 --- a/examples/cpp/ObjectTracker/object_tracker_replay.cpp +++ b/examples/cpp/ObjectTracker/object_tracker_replay.cpp @@ -37,33 +37,16 @@ int main() { // Start pipeline pipeline.start(); - // TODO remove before merge - nlohmann::json j; - j["pipeline"] = pipeline.getPipelineSchema(); - std::cout << "Pipeline schema: " << j.dump(2) << std::endl; - // - // FPS calculation variables auto startTime = std::chrono::steady_clock::now(); int counter = 0; float fps = 0; cv::Scalar color(255, 255, 255); - // TODO remove before merge - int index = 0; - // - while(pipeline.isRunning()) { auto imgFrame = preview->get(); auto track = tracklets->get(); - // TODO remove before merge - if(index++ % 30 == 0) { - std::cout << "----------------------------------------" << std::endl; - std::cout << "Pipeline state: " << pipeline.getPipelineState().nodes().detailed().str() << std::endl; - } - // - counter++; auto currentTime = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast(currentTime - startTime).count(); From 9874b632890141b31ceb6354f808c9735e8ca19f Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 6 Nov 2025 13:07:16 +0100 Subject: [PATCH 056/124] Add ability go get schema without pipeline debugging stuff [no ci] --- include/depthai/pipeline/Pipeline.hpp | 8 ++++---- src/pipeline/Pipeline.cpp | 17 ++++++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index e99d23fd1..f29698e18 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -56,8 +56,8 @@ class PipelineImpl : public std::enable_shared_from_this { // Functions Node::Id getNextUniqueId(); - PipelineSchema getPipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE) const; - PipelineSchema getDevicePipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE) const; + PipelineSchema getPipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE, bool includePipelineDebugging = true) const; + PipelineSchema getDevicePipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE, bool includePipelineDebugging = true) const; Device::Config getDeviceConfig() const; void setCameraTuningBlobPath(const fs::path& path); void setXLinkChunkSize(int sizeBytes); @@ -303,12 +303,12 @@ class Pipeline { /** * @returns Pipeline schema */ - PipelineSchema getPipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE) const; + PipelineSchema getPipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE, bool includePipelineDebugging = true) const; /** * @returns Device pipeline schema (without host only nodes and connections) */ - PipelineSchema getDevicePipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE) const; + PipelineSchema getDevicePipelineSchema(SerializationType type = DEFAULT_SERIALIZATION_TYPE, bool includePipelineDebugging = true) const; // void loadAssets(AssetManager& assetManager); void serialize(PipelineSchema& schema, Assets& assets, std::vector& assetStorage) const { diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 5b29ed306..83516d79e 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -70,12 +70,12 @@ Pipeline::Pipeline(std::shared_ptr device) : pimpl(std::make_shared pimpl) : pimpl(std::move(pimpl)) {} -PipelineSchema Pipeline::getPipelineSchema(SerializationType type) const { - return pimpl->getPipelineSchema(type); +PipelineSchema Pipeline::getPipelineSchema(SerializationType type, bool includePipelineDebugging) const { + return pimpl->getPipelineSchema(type, includePipelineDebugging); } -PipelineSchema Pipeline::getDevicePipelineSchema(SerializationType type) const { - return pimpl->getDevicePipelineSchema(type); +PipelineSchema Pipeline::getDevicePipelineSchema(SerializationType type, bool includePipelineDebugging) const { + return pimpl->getDevicePipelineSchema(type, includePipelineDebugging); } GlobalProperties PipelineImpl::getGlobalProperties() const { @@ -189,7 +189,7 @@ std::vector PipelineImpl::getConnections() const { return conns; } -PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type) const { +PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type, bool includePipelineDebugging) const { PipelineSchema schema; schema.globalProperties = globalProperties; schema.bridges = xlinkBridges; @@ -200,6 +200,9 @@ PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type) const { if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) { continue; } + if(!includePipelineDebugging && (std::string(node->getName()) == "PipelineEventAggregation" || std::string(node->getName()) == "PipelineStateMerge")) { + continue; + } // Create 'node' info NodeObjInfo info; info.id = node->id; @@ -333,8 +336,8 @@ PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type) const { return schema; } -PipelineSchema PipelineImpl::getDevicePipelineSchema(SerializationType type) const { - auto schema = getPipelineSchema(type); +PipelineSchema PipelineImpl::getDevicePipelineSchema(SerializationType type, bool includePipelineDebugging) const { + auto schema = getPipelineSchema(type, includePipelineDebugging); // Remove bridge info schema.bridges.clear(); // Remove host nodes From 1b3533cda97761d83e6beb53ce674c163b0b978e Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 6 Nov 2025 13:33:34 +0100 Subject: [PATCH 057/124] clangformat --- .../utility/PipelineEventDispatcherBindings.cpp | 4 ++-- examples/cpp/HostNodes/image_manip_host.cpp | 2 +- .../depthai/pipeline/datatype/PipelineEvent.hpp | 6 +----- .../datatype/PipelineEventAggregationConfig.hpp | 4 ++-- .../depthai/pipeline/datatype/PipelineState.hpp | 16 ++++------------ .../pipeline/node/SpatialDetectionNetwork.hpp | 16 ++++------------ .../PipelineEventAggregationProperties.hpp | 2 +- include/depthai/utility/LockingQueue.hpp | 6 ++++-- src/pipeline/datatype/PipelineEvent.cpp | 3 +-- src/pipeline/datatype/PipelineState.cpp | 3 +-- src/pipeline/node/ObjectTracker.cpp | 2 +- src/pipeline/node/Sync.cpp | 11 +++++++---- .../node/spatial_location_calculator_test.cpp | 6 ++---- .../pipeline_debugging_rvc4_test.cpp | 3 ++- 14 files changed, 33 insertions(+), 51 deletions(-) diff --git a/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp index cc786ceaf..930eea849 100644 --- a/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp +++ b/bindings/python/src/utility/PipelineEventDispatcherBindings.cpp @@ -1,4 +1,5 @@ #include "PipelineEventDispatcherBindings.hpp" + #include "depthai/utility/PipelineEventDispatcher.hpp" void PipelineEventDispatcherBindings::bind(pybind11::module& m, void* pCallstack) { @@ -18,8 +19,7 @@ void PipelineEventDispatcherBindings::bind(pybind11::module& m, void* pCallstack using namespace dai::utility; auto pipelineEventDispatcher = py::class_(m, "PipelineEventDispatcher"); - pipelineEventDispatcher - .def(py::init(), py::arg("output")) + pipelineEventDispatcher.def(py::init(), py::arg("output")) .def("setNodeId", &PipelineEventDispatcher::setNodeId, py::arg("id"), DOC(dai, utility, PipelineEventDispatcher, setNodeId)) .def("startCustomEvent", &PipelineEventDispatcher::startCustomEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, startCustomEvent)) .def("endCustomEvent", &PipelineEventDispatcher::endCustomEvent, py::arg("source"), DOC(dai, utility, PipelineEventDispatcher, endCustomEvent)) diff --git a/examples/cpp/HostNodes/image_manip_host.cpp b/examples/cpp/HostNodes/image_manip_host.cpp index c43d75c37..db37c8197 100644 --- a/examples/cpp/HostNodes/image_manip_host.cpp +++ b/examples/cpp/HostNodes/image_manip_host.cpp @@ -6,7 +6,7 @@ #include "depthai/pipeline/node/host/Replay.hpp" #ifndef VIDEO_PATH -#define VIDEO_PATH "" + #define VIDEO_PATH "" #endif int main(int argc, char** argv) { diff --git a/include/depthai/pipeline/datatype/PipelineEvent.hpp b/include/depthai/pipeline/datatype/PipelineEvent.hpp index 03293fa97..5aa601ee2 100644 --- a/include/depthai/pipeline/datatype/PipelineEvent.hpp +++ b/include/depthai/pipeline/datatype/PipelineEvent.hpp @@ -20,11 +20,7 @@ class PipelineEvent : public Buffer { INPUT_BLOCK = 4, OUTPUT_BLOCK = 5, }; - enum class Interval : std::int32_t { - NONE = 0, - START = 1, - END = 2 - }; + enum class Interval : std::int32_t { NONE = 0, START = 1, END = 2 }; PipelineEvent() = default; virtual ~PipelineEvent() = default; diff --git a/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp b/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp index e321bccea..b5dd77adc 100644 --- a/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp +++ b/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp @@ -9,7 +9,7 @@ namespace dai { class NodeEventAggregationConfig { - public: + public: int64_t nodeId = -1; std::optional> inputs; std::optional> outputs; @@ -23,7 +23,7 @@ class NodeEventAggregationConfig { class PipelineEventAggregationConfig : public Buffer { public: std::vector nodes; - bool repeat = false; // Keep sending the aggregated state without waiting for new config + bool repeat = false; // Keep sending the aggregated state without waiting for new config PipelineEventAggregationConfig() = default; virtual ~PipelineEventAggregationConfig(); diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index 7caccb395..a5970ace3 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -45,8 +45,8 @@ class NodeState { // Current state of the input queue. enum class State : std::int32_t { IDLE = 0, - WAITING = 1, // Waiting to receive a message - BLOCKED = 2 // An output attempted to send to this input, but the input queue was full + WAITING = 1, // Waiting to receive a message + BLOCKED = 2 // An output attempted to send to this input, but the input queue was full } state = State::IDLE; // Number of messages currently queued in the input queue uint32_t numQueued = 0; @@ -64,10 +64,7 @@ class NodeState { struct OutputQueueState { // Current state of the output queue. Send should ideally be instant. This is not the case when the input queue is full. // In that case, the state will be SENDING until there is space in the input queue (unless trySend is used). - enum class State : std::int32_t { - IDLE = 0, - SENDING = 1 - } state = State::IDLE; + enum class State : std::int32_t { IDLE = 0, SENDING = 1 } state = State::IDLE; // Timing info about this output Timing timing; @@ -77,12 +74,7 @@ class NodeState { DEPTHAI_SERIALIZE(OutputQueueState, state, timing); }; - enum class State : std::int32_t { - IDLE = 0, - GETTING_INPUTS = 1, - PROCESSING = 2, - SENDING_OUTPUTS = 3 - }; + enum class State : std::int32_t { IDLE = 0, GETTING_INPUTS = 1, PROCESSING = 2, SENDING_OUTPUTS = 3 }; // Current state of the node - idle only when not running State state = State::IDLE; diff --git a/include/depthai/pipeline/node/SpatialDetectionNetwork.hpp b/include/depthai/pipeline/node/SpatialDetectionNetwork.hpp index 978d24bc9..35ccd09f6 100644 --- a/include/depthai/pipeline/node/SpatialDetectionNetwork.hpp +++ b/include/depthai/pipeline/node/SpatialDetectionNetwork.hpp @@ -29,9 +29,7 @@ class SpatialDetectionNetwork : public DeviceNodeCRTPinput}, outNetwork{neuralNetwork->out}, - passthrough { - neuralNetwork->passthrough - } + passthrough{neuralNetwork->passthrough} #endif { if(device) { @@ -47,9 +45,7 @@ class SpatialDetectionNetwork : public DeviceNodeCRTPinput}, outNetwork{neuralNetwork->out}, - passthrough { - neuralNetwork->passthrough - } + passthrough{neuralNetwork->passthrough} #endif { auto device = getDevice(); @@ -66,9 +62,7 @@ class SpatialDetectionNetwork : public DeviceNodeCRTPinput}, outNetwork{neuralNetwork->out}, - passthrough { - neuralNetwork->passthrough - } + passthrough{neuralNetwork->passthrough} #endif { auto device = getDevice(); @@ -86,9 +80,7 @@ class SpatialDetectionNetwork : public DeviceNodeCRTPinput}, outNetwork{neuralNetwork->out}, - passthrough { - neuralNetwork->passthrough - } + passthrough{neuralNetwork->passthrough} #endif { if(device) { diff --git a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp index 708c0cb3f..4804f6cad 100644 --- a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp +++ b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp @@ -9,7 +9,7 @@ namespace dai { */ struct PipelineEventAggregationProperties : PropertiesSerializable { uint32_t aggregationWindowSize = 100; - uint32_t statsUpdateIntervalMs = 1000; + uint32_t statsUpdateIntervalMs = 1000; uint32_t eventWaitWindow = 16; }; diff --git a/include/depthai/utility/LockingQueue.hpp b/include/depthai/utility/LockingQueue.hpp index 0bbe507e3..c5703a7e7 100644 --- a/include/depthai/utility/LockingQueue.hpp +++ b/include/depthai/utility/LockingQueue.hpp @@ -216,7 +216,8 @@ class LockingQueue { } template - bool tryWaitAndPush(T const& data, std::chrono::duration timeout, std::function callback = [](LockingQueueState, size_t) {}) { + bool tryWaitAndPush( + T const& data, std::chrono::duration timeout, std::function callback = [](LockingQueueState, size_t) {}) { { std::unique_lock lock(guard); if(maxSize == 0) { @@ -254,7 +255,8 @@ class LockingQueue { } template - bool tryWaitAndPush(T&& data, std::chrono::duration timeout, std::function callback = [](LockingQueueState, size_t) {}) { + bool tryWaitAndPush( + T&& data, std::chrono::duration timeout, std::function callback = [](LockingQueueState, size_t) {}) { { std::unique_lock lock(guard); if(maxSize == 0) { diff --git a/src/pipeline/datatype/PipelineEvent.cpp b/src/pipeline/datatype/PipelineEvent.cpp index 4d7121336..4bcb49b78 100644 --- a/src/pipeline/datatype/PipelineEvent.cpp +++ b/src/pipeline/datatype/PipelineEvent.cpp @@ -1,4 +1,3 @@ #include "depthai/pipeline/datatype/PipelineEvent.hpp" -namespace dai { -} // namespace dai +namespace dai {} // namespace dai diff --git a/src/pipeline/datatype/PipelineState.cpp b/src/pipeline/datatype/PipelineState.cpp index f45bb47e9..0ba8f8a8a 100644 --- a/src/pipeline/datatype/PipelineState.cpp +++ b/src/pipeline/datatype/PipelineState.cpp @@ -1,4 +1,3 @@ #include "depthai/pipeline/datatype/PipelineState.hpp" -namespace dai { -} // namespace dai +namespace dai {} // namespace dai diff --git a/src/pipeline/node/ObjectTracker.cpp b/src/pipeline/node/ObjectTracker.cpp index 4e894eabc..bb6bb0c0a 100644 --- a/src/pipeline/node/ObjectTracker.cpp +++ b/src/pipeline/node/ObjectTracker.cpp @@ -213,7 +213,7 @@ void ObjectTracker::run() { passthroughTrackerFrame.send(inputTrackerImg); if(gotDetections) { passthroughDetections.send(inputImgDetections ? std::dynamic_pointer_cast(inputImgDetections) - : std::dynamic_pointer_cast(inputSpatialImgDetections)); + : std::dynamic_pointer_cast(inputSpatialImgDetections)); if(inputDetectionImg) { passthroughDetectionFrame.send(inputDetectionImg); } diff --git a/src/pipeline/node/Sync.cpp b/src/pipeline/node/Sync.cpp index 4eac7768f..3171721d3 100644 --- a/src/pipeline/node/Sync.cpp +++ b/src/pipeline/node/Sync.cpp @@ -1,4 +1,5 @@ #include "depthai/pipeline/node/Sync.hpp" + #include #include "depthai/pipeline/datatype/MessageGroup.hpp" @@ -69,8 +70,9 @@ void Sync::run() { } // Print out the timestamps for(const auto& frame : inputFrames) { - logger->debug( - "Starting input {} timestamp is {} ms", frame.first, static_cast(frame.second->getTimestamp().time_since_epoch().count()) / 1000000.f); + logger->debug("Starting input {} timestamp is {} ms", + frame.first, + static_cast(frame.second->getTimestamp().time_since_epoch().count()) / 1000000.f); } tAfterMessageBeginning = steady_clock::now(); int attempts = 0; @@ -79,8 +81,9 @@ void Sync::run() { if(attempts > 50) { logger->warn("Sync node has been trying to sync for {} messages, but the messages are still not in sync.", attempts); for(const auto& frame : inputFrames) { - logger->warn( - "Output {} timestamp is {} ms", frame.first, static_cast(frame.second->getTimestamp().time_since_epoch().count()) / 1000000.f); + logger->warn("Output {} timestamp is {} ms", + frame.first, + static_cast(frame.second->getTimestamp().time_since_epoch().count()) / 1000000.f); } } if(attempts > properties.syncAttempts && properties.syncAttempts != -1) { diff --git a/tests/src/ondevice_tests/pipeline/node/spatial_location_calculator_test.cpp b/tests/src/ondevice_tests/pipeline/node/spatial_location_calculator_test.cpp index 1316e8566..0a8ca09b7 100644 --- a/tests/src/ondevice_tests/pipeline/node/spatial_location_calculator_test.cpp +++ b/tests/src/ondevice_tests/pipeline/node/spatial_location_calculator_test.cpp @@ -1,6 +1,5 @@ -#include - #include +#include #include #include #include @@ -106,7 +105,6 @@ TEST_CASE("SpatialLocationCalculator synthetic depth data test") { auto outputQueue = spatial->out.createOutputQueue(); auto passthroughQueue = spatial->passthroughDepth.createOutputQueue(); - std::vector depthPixels(width * height, 1000); auto setRegionDepth = [&](const RoiSpec& spec) { const int x0 = static_cast(spec.roi.x); @@ -120,7 +118,7 @@ TEST_CASE("SpatialLocationCalculator synthetic depth data test") { for(const auto& spec : roiSpecs) { setRegionDepth(spec); } - + // Prepare synthetic depth frame auto depthFrame = std::make_shared(); depthFrame->setType(dai::ImgFrame::Type::RAW16); diff --git a/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp b/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp index 0b422fa30..bf28224fb 100644 --- a/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp +++ b/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp @@ -55,7 +55,8 @@ TEST_CASE("Object Tracker Pipeline Debugging") { if(!node->getOutputs().empty()) REQUIRE(nodeState.outputsSendTiming.isValid()); for(const auto& [inputName, inputState] : nodeState.inputStates) { if(std::string(node->getName()) == "ObjectTracker" && inputName == "inputConfig") continue; // This example does not use inputConfig - if(std::string(node->getName()) == "ObjectTracker" && inputName == "inputDetectionFrame") continue; // This example does not use inputDetectionFrame + if(std::string(node->getName()) == "ObjectTracker" && inputName == "inputDetectionFrame") + continue; // This example does not use inputDetectionFrame REQUIRE(inputState.timing.isValid()); } for(const auto& [outputName, outputState] : nodeState.outputStates) { From ab22e6b398e66cf1c01231bbd17408acfe043acd Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 7 Nov 2025 12:25:16 +0100 Subject: [PATCH 058/124] Pipeline debugging: fix HostNode crash [no ci] --- examples/cpp/HostNodes/display.cpp | 2 +- src/pipeline/Pipeline.cpp | 9 +++++---- src/utility/PipelineEventDispatcher.cpp | 12 ++++++------ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/examples/cpp/HostNodes/display.cpp b/examples/cpp/HostNodes/display.cpp index fda3fb18f..12daaa1ec 100644 --- a/examples/cpp/HostNodes/display.cpp +++ b/examples/cpp/HostNodes/display.cpp @@ -52,4 +52,4 @@ int main() { pipeline.wait(); return 0; -} \ No newline at end of file +} diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 83516d79e..80bd38927 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -671,6 +671,11 @@ void PipelineImpl::build() { } } + // Run first build stage for all nodes + for(const auto& node : getAllNodes()) { + node->buildStage1(); + } + // Create pipeline event aggregator node and link enablePipelineDebugging = enablePipelineDebugging || utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false); if(enablePipelineDebugging) { @@ -733,10 +738,6 @@ void PipelineImpl::build() { isBuild = true; // Go through the build stages sequentially - for(const auto& node : getAllNodes()) { - node->buildStage1(); - } - for(const auto& node : getAllNodes()) { node->buildStage2(); } diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index a4420bbd0..95c3b5b69 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -45,8 +45,8 @@ void PipelineEventDispatcher::setNodeId(int64_t id) { } void PipelineEventDispatcher::startEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize) { if(!sendEvents) return; - checkNodeId(); if(blacklist(type, source)) return; + checkNodeId(); std::lock_guard lock(mutex); @@ -76,8 +76,8 @@ void PipelineEventDispatcher::startCustomEvent(const std::string& source) { } void PipelineEventDispatcher::endEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize) { if(!sendEvents) return; - checkNodeId(); if(blacklist(type, source)) return; + checkNodeId(); std::lock_guard lock(mutex); @@ -114,8 +114,8 @@ void PipelineEventDispatcher::endCustomEvent(const std::string& source) { } void PipelineEventDispatcher::startTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { if(!sendEvents) return; - checkNodeId(); if(blacklist(type, source)) return; + checkNodeId(); std::lock_guard lock(mutex); @@ -135,8 +135,8 @@ void PipelineEventDispatcher::startTrackedEvent(PipelineEvent::Type type, const } void PipelineEventDispatcher::endTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { if(!sendEvents) return; - checkNodeId(); if(blacklist(type, source)) return; + checkNodeId(); std::lock_guard lock(mutex); @@ -156,8 +156,8 @@ void PipelineEventDispatcher::endTrackedEvent(PipelineEvent::Type type, const st } void PipelineEventDispatcher::pingEvent(PipelineEvent::Type type, const std::string& source) { if(!sendEvents) return; - checkNodeId(); if(blacklist(type, source)) return; + checkNodeId(); std::lock_guard lock(mutex); @@ -187,8 +187,8 @@ void PipelineEventDispatcher::pingCustomEvent(const std::string& source) { } void PipelineEventDispatcher::pingInputEvent(const std::string& source, int32_t status, std::optional queueSize) { if(!sendEvents) return; - checkNodeId(); if(blacklist(PipelineEvent::Type::INPUT, source)) return; + checkNodeId(); std::lock_guard lock(mutex); From ae05e488320d3c24e7568986ea790bd099baa411 Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 7 Nov 2025 12:55:32 +0100 Subject: [PATCH 059/124] RVC4 FW: Added pipeline debugging info to videoencoder --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index e886e289c..aaabe16df 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+8aa456bd0f98b954bd386beeeb0da6b6b96afb3c") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+2e4740ba63848fdf4eefd24813d4aafe9900bd78") From c0aef2ab777acd57f738e65d43048c0b7a1ea39d Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 7 Nov 2025 15:43:43 +0100 Subject: [PATCH 060/124] Make block events thread safe --- include/depthai/pipeline/MessageQueue.hpp | 32 ++++++++---- .../utility/PipelineEventDispatcher.hpp | 2 + .../PipelineEventDispatcherInterface.hpp | 26 ++++++++-- src/pipeline/Node.cpp | 31 ++++++++--- src/utility/PipelineEventDispatcher.cpp | 52 ++++++++++--------- 5 files changed, 99 insertions(+), 44 deletions(-) diff --git a/include/depthai/pipeline/MessageQueue.hpp b/include/depthai/pipeline/MessageQueue.hpp index 6aee529be..72de0ca10 100644 --- a/include/depthai/pipeline/MessageQueue.hpp +++ b/include/depthai/pipeline/MessageQueue.hpp @@ -201,16 +201,23 @@ class MessageQueue : public std::enable_shared_from_this { */ template std::shared_ptr tryGet() { - if(pipelineEventDispatcher) pipelineEventDispatcher->startInputEvent(name, getSize()); if(queue.isDestroyed()) { throw QueueException(CLOSED_QUEUE_MESSAGE); } std::shared_ptr val = nullptr; - if(!queue.tryPop(val)) { - return nullptr; + auto getInput = [this, &val]() -> std::shared_ptr { + if(!queue.tryPop(val)) { + return nullptr; + } + return std::dynamic_pointer_cast(val); + }; + if(pipelineEventDispatcher) { + auto blockEvent = pipelineEventDispatcher->blockEvent(PipelineEvent::Type::INPUT, name); + blockEvent.setQueueSize(getSize()); + return getInput(); + } else { + return getInput(); } - if(pipelineEventDispatcher && std::dynamic_pointer_cast(val)) pipelineEventDispatcher->endInputEvent(name, getSize()); - return std::dynamic_pointer_cast(val); } /** @@ -229,12 +236,19 @@ class MessageQueue : public std::enable_shared_from_this { */ template std::shared_ptr get() { - if(pipelineEventDispatcher) pipelineEventDispatcher->startInputEvent(name, getSize()); std::shared_ptr val = nullptr; - if(!queue.waitAndPop(val)) { - throw QueueException(CLOSED_QUEUE_MESSAGE); + auto getInput = [this, &val]() { + if(!queue.waitAndPop(val)) { + throw QueueException(CLOSED_QUEUE_MESSAGE); + } + }; + if(pipelineEventDispatcher) { + auto blockEvent = pipelineEventDispatcher->blockEvent(PipelineEvent::Type::INPUT, name); + blockEvent.setQueueSize(getSize()); + getInput(); + } else { + getInput(); } - if(pipelineEventDispatcher) pipelineEventDispatcher->endInputEvent(name, getSize()); return std::dynamic_pointer_cast(val); } diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index df5f150ba..4f64d9850 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -42,7 +42,9 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void endInputEvent(const std::string& source, std::optional queueSize = std::nullopt) override; void endOutputEvent(const std::string& source) override; void endCustomEvent(const std::string& source) override; + void startTrackedEvent(PipelineEvent event) override; void startTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) override; + void endTrackedEvent(PipelineEvent event) override; void endTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) override; void pingEvent(PipelineEvent::Type type, const std::string& source) override; void pingMainLoopEvent() override; diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index 0e8f6d5df..e2d8a6a8e 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include "depthai/pipeline/datatype/PipelineEvent.hpp" @@ -8,19 +9,36 @@ namespace dai { namespace utility { class PipelineEventDispatcherInterface { + std::atomic sequence{0}; + public: class BlockPipelineEvent { PipelineEventDispatcherInterface& dispatcher; PipelineEvent::Type type; std::string source; + uint64_t sequence; + + bool canceled = false; + std::optional queueSize = std::nullopt; public: BlockPipelineEvent(PipelineEventDispatcherInterface& dispatcher, PipelineEvent::Type type, const std::string& source) - : dispatcher(dispatcher), type(type), source(source) { - dispatcher.startEvent(type, source, std::nullopt); + : dispatcher(dispatcher), type(type), source(source), sequence(dispatcher.sequence++) { + dispatcher.startTrackedEvent(type, source, sequence); } ~BlockPipelineEvent() { - dispatcher.endEvent(type, source, std::nullopt); + PipelineEvent event; + event.type = type; + event.source = source; + event.sequenceNum = sequence; + event.queueSize = queueSize; + if(!canceled) dispatcher.endTrackedEvent(type, source, sequence); + } + void cancel() { + canceled = true; + } + void setQueueSize(uint32_t qs) { + queueSize = qs; } }; @@ -37,7 +55,9 @@ class PipelineEventDispatcherInterface { virtual void endOutputEvent(const std::string& source) = 0; virtual void endCustomEvent(const std::string& source) = 0; virtual void startTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) = 0; + virtual void startTrackedEvent(PipelineEvent event) = 0; virtual void endTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) = 0; + virtual void endTrackedEvent(PipelineEvent event) = 0; virtual void pingEvent(PipelineEvent::Type type, const std::string& source) = 0; virtual void pingMainLoopEvent() = 0; virtual void pingCustomEvent(const std::string& source) = 0; diff --git a/src/pipeline/Node.cpp b/src/pipeline/Node.cpp index 49490765d..e1d2ff383 100644 --- a/src/pipeline/Node.cpp +++ b/src/pipeline/Node.cpp @@ -241,11 +241,17 @@ void Node::Output::send(const std::shared_ptr& msg) { // } // } // } - if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getName()); - for(auto& messageQueue : connectedInputs) { - messageQueue->send(msg); + auto sendToInputs = [this, &msg]() { + for(auto& messageQueue : connectedInputs) { + messageQueue->send(msg); + } + }; + if(pipelineEventDispatcher) { + auto blockEvent = pipelineEventDispatcher->blockEvent(PipelineEvent::Type::OUTPUT, getName()); + sendToInputs(); + } else { + sendToInputs(); } - if(pipelineEventDispatcher) pipelineEventDispatcher->endOutputEvent(getName()); } bool Node::Output::trySend(const std::shared_ptr& msg) { @@ -265,11 +271,20 @@ bool Node::Output::trySend(const std::shared_ptr& msg) { // } // } // } - if(pipelineEventDispatcher) pipelineEventDispatcher->startOutputEvent(getName()); - for(auto& messageQueue : connectedInputs) { - success &= messageQueue->trySend(msg); + auto sendToInputs = [this, &msg, &success]() { + for(auto& messageQueue : connectedInputs) { + success &= messageQueue->trySend(msg); + } + }; + if(pipelineEventDispatcher) { + auto blockEvent = pipelineEventDispatcher->blockEvent(PipelineEvent::Type::OUTPUT, getName()); + sendToInputs(); + if(!success) { + blockEvent.cancel(); + } + } else { + sendToInputs(); } - if(pipelineEventDispatcher && success) pipelineEventDispatcher->endOutputEvent(getName()); return success; } diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index 95c3b5b69..72bdaf6d2 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -112,48 +112,52 @@ void PipelineEventDispatcher::endOutputEvent(const std::string& source) { void PipelineEventDispatcher::endCustomEvent(const std::string& source) { endEvent(PipelineEvent::Type::CUSTOM, source, std::nullopt); } -void PipelineEventDispatcher::startTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { +void PipelineEventDispatcher::startTrackedEvent(PipelineEvent event) { if(!sendEvents) return; - if(blacklist(type, source)) return; + if(blacklist(event.type, event.source)) return; checkNodeId(); std::lock_guard lock(mutex); - auto& event = events[makeKey(type, source)]; - event.event.setTimestamp(std::chrono::steady_clock::now()); - event.event.tsDevice = event.event.ts; - event.event.sequenceNum = sequenceNum; - event.event.nodeId = nodeId; - event.event.interval = PipelineEvent::Interval::START; - event.event.type = type; - event.event.source = source; - event.ongoing = false; + event.setTimestamp(std::chrono::steady_clock::now()); + event.tsDevice = event.ts; + event.nodeId = nodeId; + event.interval = PipelineEvent::Interval::START; if(out) { - out->send(std::make_shared(event.event)); + out->send(std::make_shared(event)); } } -void PipelineEventDispatcher::endTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { +void PipelineEventDispatcher::startTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { + PipelineEvent event; + event.type = type; + event.source = source; + event.sequenceNum = sequenceNum; + startTrackedEvent(event); +} +void PipelineEventDispatcher::endTrackedEvent(PipelineEvent event) { if(!sendEvents) return; - if(blacklist(type, source)) return; + if(blacklist(event.type, event.source)) return; checkNodeId(); std::lock_guard lock(mutex); - auto& event = events[makeKey(type, source)]; - event.event.setTimestamp(std::chrono::steady_clock::now()); - event.event.tsDevice = event.event.ts; - event.event.sequenceNum = sequenceNum; - event.event.nodeId = nodeId; - event.event.interval = PipelineEvent::Interval::END; - event.event.type = type; - event.event.source = source; - event.ongoing = false; + event.setTimestamp(std::chrono::steady_clock::now()); + event.tsDevice = event.ts; + event.nodeId = nodeId; + event.interval = PipelineEvent::Interval::END; if(out) { - out->send(std::make_shared(event.event)); + out->send(std::make_shared(event)); } } +void PipelineEventDispatcher::endTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { + PipelineEvent event; + event.type = type; + event.source = source; + event.sequenceNum = sequenceNum; + endTrackedEvent(event); +} void PipelineEventDispatcher::pingEvent(PipelineEvent::Type type, const std::string& source) { if(!sendEvents) return; if(blacklist(type, source)) return; From 19422fb88e981d099bd8285e2cfe334d6dedf08b Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 7 Nov 2025 18:33:23 +0100 Subject: [PATCH 061/124] Bugfixes [no ci] --- .../utility/PipelineEventDispatcherInterface.hpp | 3 ++- src/pipeline/Pipeline.cpp | 10 ++++++---- .../node/internal/PipelineEventAggregation.cpp | 11 +++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index e2d8a6a8e..1e91338ec 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -27,12 +27,13 @@ class PipelineEventDispatcherInterface { dispatcher.startTrackedEvent(type, source, sequence); } ~BlockPipelineEvent() { + if(canceled || std::uncaught_exceptions() > 0) return; PipelineEvent event; event.type = type; event.source = source; event.sequenceNum = sequence; event.queueSize = queueSize; - if(!canceled) dispatcher.endTrackedEvent(type, source, sequence); + dispatcher.endTrackedEvent(type, source, sequence); } void cancel() { canceled = true; diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 80bd38927..6f1a2dd50 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -670,12 +670,14 @@ void PipelineImpl::build() { throw std::runtime_error("Holistic record/replay is only supported on RVC2 devices for now."); } } + } - // Run first build stage for all nodes - for(const auto& node : getAllNodes()) { - node->buildStage1(); - } + // Run first build stage for all nodes + for(const auto& node : getAllNodes()) { + node->buildStage1(); + } + if(pipelineOnHost) { // Create pipeline event aggregator node and link enablePipelineDebugging = enablePipelineDebugging || utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false); if(enablePipelineDebugging) { diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 45940807b..741b15291 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -17,7 +17,6 @@ class NodeEventAggregation { private: struct FpsMeasurement { std::chrono::steady_clock::time_point time; - int64_t sequenceNum; }; std::shared_ptr logger; @@ -146,7 +145,7 @@ class NodeEventAggregation { eventsBuffer.add(durationEvent); timingsBuffer->add(durationEvent.durationUs); - if(event.type != PipelineEvent::Type::LOOP) fpsBuffer->add({durationEvent.startEvent.getTimestamp(), durationEvent.startEvent.getSequenceNum()}); + if(event.type != PipelineEvent::Type::LOOP) fpsBuffer->add({durationEvent.startEvent.getTimestamp()}); *ongoingEvent = std::nullopt; @@ -220,7 +219,7 @@ class NodeEventAggregation { eventsBuffer.add(durationEvent); timingsBuffer->add(durationEvent.durationUs); - if(fpsBuffer) fpsBuffer->add({durationEvent.startEvent.getTimestamp(), durationEvent.startEvent.getSequenceNum()}); + if(fpsBuffer) fpsBuffer->add({durationEvent.startEvent.getTimestamp()}); // Start event ongoingEvents->add(event); @@ -267,9 +266,9 @@ class NodeEventAggregation { inline void updateFpsStats(NodeState::Timing& timing, const utility::CircularBuffer& buffer) { if(buffer.size() < 2) return; auto timeDiff = std::chrono::duration_cast(buffer.last().time - buffer.first().time).count(); - auto frameDiff = buffer.last().sequenceNum - buffer.first().sequenceNum; - if(timeDiff > 0 && buffer.last().sequenceNum > buffer.first().sequenceNum) { - timing.fps = frameDiff * (1e6f / (float)timeDiff); + auto numFrames = buffer.size() - 1; + if(timeDiff > 0) { + timing.fps = numFrames * (1e6f / (float)timeDiff); } } From a8cd27984f51c59915ddc4bd29416f4325a4a46f Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 7 Nov 2025 18:35:16 +0100 Subject: [PATCH 062/124] RVC4 FW: Core update --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index aaabe16df..47079cee1 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+2e4740ba63848fdf4eefd24813d4aafe9900bd78") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+f3484d9c8d6d3b67d91139824ec43a4eabdb1a86") From 4879c08df52cdec9126bf9d9e96c114d5a2c90b6 Mon Sep 17 00:00:00 2001 From: asahtik Date: Sat, 8 Nov 2025 10:56:38 +0100 Subject: [PATCH 063/124] RVC2 FW: Fix pipeline debugging fps calculation [no ci] --- cmake/Depthai/DepthaiDeviceSideConfig.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceSideConfig.cmake b/cmake/Depthai/DepthaiDeviceSideConfig.cmake index 3d8c0bc1c..7bdd6c14d 100644 --- a/cmake/Depthai/DepthaiDeviceSideConfig.cmake +++ b/cmake/Depthai/DepthaiDeviceSideConfig.cmake @@ -2,7 +2,7 @@ set(DEPTHAI_DEVICE_SIDE_MATURITY "snapshot") # "full commit hash of device side binary" -set(DEPTHAI_DEVICE_SIDE_COMMIT "28de549d4b5cb0a6c659420f68a16a18477c147c") +set(DEPTHAI_DEVICE_SIDE_COMMIT "38564345773c52550731e509bbf5f1464c9f263d") # "version if applicable" set(DEPTHAI_DEVICE_SIDE_VERSION "") From d1c961e859b5868726ad636c632e45cdf9beb9f5 Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 10 Nov 2025 11:06:51 +0100 Subject: [PATCH 064/124] Add pipeline debugging input duration test --- .../pipeline_debugging_host_test.cpp | 119 ++++++++++++------ 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp index bf3486046..9d34d9884 100644 --- a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp +++ b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "depthai/depthai.hpp" #include "depthai/pipeline/ThreadedHostNode.hpp" @@ -198,45 +199,62 @@ class PipelineHandler { public: Pipeline pipeline; - PipelineHandler() : pipeline(false) { + PipelineHandler(int idx=0) : pipeline(false) { pipeline.enablePipelineDebugging(); - auto gen1 = pipeline.create(); - nodeIds["gen1"] = gen1->id; - auto gen2 = pipeline.create(); - nodeIds["gen2"] = gen2->id; - auto bridge1 = pipeline.create(); - nodeIds["bridge1"] = bridge1->id; - auto bridge2 = pipeline.create(); - nodeIds["bridge2"] = bridge2->id; - auto map = pipeline.create(); - nodeIds["map"] = map->id; - auto cons1 = pipeline.create(); - nodeIds["cons1"] = cons1->id; - auto cons2 = pipeline.create(); - nodeIds["cons2"] = cons2->id; - - gen1->output.link(bridge1->input); - gen2->output.link(bridge2->input); - bridge1->output.link(map->inputs["bridge1"]); - bridge2->output.link(map->inputs["bridge2"]); - map->outputs["bridge1"].link(cons1->input); - map->outputs["bridge2"].link(cons2->input); - - pingQueues["gen1"] = gen1->ping.createInputQueue(); - pingQueues["gen2"] = gen2->ping.createInputQueue(); - pingQueues["bridge1"] = bridge1->ping.createInputQueue(); - pingQueues["bridge2"] = bridge2->ping.createInputQueue(); - pingQueues["map"] = map->ping.createInputQueue(); - pingQueues["cons1"] = cons1->ping.createInputQueue(); - pingQueues["cons2"] = cons2->ping.createInputQueue(); - ackQueues["gen1"] = gen1->ack.createOutputQueue(); - ackQueues["gen2"] = gen2->ack.createOutputQueue(); - ackQueues["bridge1"] = bridge1->ack.createOutputQueue(); - ackQueues["bridge2"] = bridge2->ack.createOutputQueue(); - ackQueues["map"] = map->ack.createOutputQueue(); - ackQueues["cons1"] = cons1->ack.createOutputQueue(); - ackQueues["cons2"] = cons2->ack.createOutputQueue(); + switch(idx) { + case 0: { + auto gen1 = pipeline.create(); + nodeIds["gen1"] = gen1->id; + auto gen2 = pipeline.create(); + nodeIds["gen2"] = gen2->id; + auto bridge1 = pipeline.create(); + nodeIds["bridge1"] = bridge1->id; + auto bridge2 = pipeline.create(); + nodeIds["bridge2"] = bridge2->id; + auto map = pipeline.create(); + nodeIds["map"] = map->id; + auto cons1 = pipeline.create(); + nodeIds["cons1"] = cons1->id; + auto cons2 = pipeline.create(); + nodeIds["cons2"] = cons2->id; + + gen1->output.link(bridge1->input); + gen2->output.link(bridge2->input); + bridge1->output.link(map->inputs["bridge1"]); + bridge2->output.link(map->inputs["bridge2"]); + map->outputs["bridge1"].link(cons1->input); + map->outputs["bridge2"].link(cons2->input); + + pingQueues["gen1"] = gen1->ping.createInputQueue(); + pingQueues["gen2"] = gen2->ping.createInputQueue(); + pingQueues["bridge1"] = bridge1->ping.createInputQueue(); + pingQueues["bridge2"] = bridge2->ping.createInputQueue(); + pingQueues["map"] = map->ping.createInputQueue(); + pingQueues["cons1"] = cons1->ping.createInputQueue(); + pingQueues["cons2"] = cons2->ping.createInputQueue(); + ackQueues["gen1"] = gen1->ack.createOutputQueue(); + ackQueues["gen2"] = gen2->ack.createOutputQueue(); + ackQueues["bridge1"] = bridge1->ack.createOutputQueue(); + ackQueues["bridge2"] = bridge2->ack.createOutputQueue(); + ackQueues["map"] = map->ack.createOutputQueue(); + ackQueues["cons1"] = cons1->ack.createOutputQueue(); + ackQueues["cons2"] = cons2->ack.createOutputQueue(); + } break; + case 1: { + auto gen = pipeline.create(); + nodeIds["gen"] = gen->id; + auto cons = pipeline.create(); + nodeIds["cons"] = cons->id; + + gen->output.link(cons->input); + + pingQueues["gen"] = gen->ping.createInputQueue(); + pingQueues["cons"] = cons->ping.createInputQueue(); + ackQueues["gen"] = gen->ack.createOutputQueue(); + ackQueues["cons"] = cons->ack.createOutputQueue(); + } break; + } } void start() { @@ -429,3 +447,30 @@ TEST_CASE("Node timings test") { } ph.stop(); } + +TEST_CASE("Input duration test") { + PipelineHandler ph(1); + ph.start(); + + ph.ping("gen", 0); + ph.ping("cons", 0); + + for(int i = 0; i < 10; ++i) { + ph.ping("cons", 0); // input get + std::this_thread::sleep_for(std::chrono::milliseconds(900)); + ph.ping("gen", 0); // output send + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + std::this_thread::sleep_for(std::chrono::seconds(1)); // Wait for state update + + auto inputState = ph.pipeline.getPipelineState().nodes(ph.getNodeId("cons")).inputs("input"); + REQUIRE(inputState.isValid()); + REQUIRE(inputState.timing.durationStats.averageMicrosRecent == Catch::Approx(1e6).margin(0.2e6)); + REQUIRE(inputState.timing.durationStats.medianMicrosRecent == Catch::Approx(1e6).margin(0.2e6)); + REQUIRE(inputState.timing.durationStats.maxMicrosRecent == Catch::Approx(1e6).margin(0.4e6)); + REQUIRE(inputState.timing.durationStats.minMicrosRecent == Catch::Approx(1e6).margin(0.4e6)); + REQUIRE(inputState.timing.durationStats.stdDevMicrosRecent == Catch::Approx(0).margin(0.5e6)); + REQUIRE(inputState.timing.durationStats.maxMicros == Catch::Approx(1e6).margin(0.4e6)); + REQUIRE(inputState.timing.durationStats.minMicros == Catch::Approx(1e6).margin(0.4e6)); +} From 065a97c3e8d92e5bc3f31c2a311e1e5b92ef0beb Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 10 Nov 2025 11:38:30 +0100 Subject: [PATCH 065/124] Add foxglove service for full pipeline and pipeline state + support for python 3.14 --- bindings/python/setup.py | 1 + .../remote_connection/RemoteConnection.hpp | 2 +- src/remote_connection/RemoteConnection.cpp | 4 +- .../RemoteConnectionImpl.cpp | 108 ++++++++++++++---- .../RemoteConnectionImpl.hpp | 4 +- 5 files changed, 92 insertions(+), 27 deletions(-) diff --git a/bindings/python/setup.py b/bindings/python/setup.py index bdbe65c38..2daaf0386 100644 --- a/bindings/python/setup.py +++ b/bindings/python/setup.py @@ -273,6 +273,7 @@ def build_extension(self, ext): "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: C++", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Scientific/Engineering", diff --git a/include/depthai/remote_connection/RemoteConnection.hpp b/include/depthai/remote_connection/RemoteConnection.hpp index 636a3cc9b..45332eb14 100644 --- a/include/depthai/remote_connection/RemoteConnection.hpp +++ b/include/depthai/remote_connection/RemoteConnection.hpp @@ -77,7 +77,7 @@ class RemoteConnection { * * @param pipeline The pipeline to register. */ - void registerPipeline(const Pipeline& pipeline); + void registerPipeline(Pipeline& pipeline); /** * @brief Waits for a key event. diff --git a/src/remote_connection/RemoteConnection.cpp b/src/remote_connection/RemoteConnection.cpp index d070e34db..21908cd2e 100644 --- a/src/remote_connection/RemoteConnection.cpp +++ b/src/remote_connection/RemoteConnection.cpp @@ -22,7 +22,7 @@ bool RemoteConnection::removeTopic(const std::string& topicName) { return impl->removeTopic(topicName); } -void RemoteConnection::registerPipeline(const Pipeline& pipeline) { +void RemoteConnection::registerPipeline(Pipeline& pipeline) { impl->registerPipeline(pipeline); } @@ -34,4 +34,4 @@ void RemoteConnection::registerService(const std::string& serviceName, std::func impl->registerService(serviceName, std::move(callback)); } -} // namespace dai \ No newline at end of file +} // namespace dai diff --git a/src/remote_connection/RemoteConnectionImpl.cpp b/src/remote_connection/RemoteConnectionImpl.cpp index 9a112b5ab..a99ecd8c8 100644 --- a/src/remote_connection/RemoteConnectionImpl.cpp +++ b/src/remote_connection/RemoteConnectionImpl.cpp @@ -268,7 +268,7 @@ bool RemoteConnectionImpl::removeTopic(const std::string& topicName) { return true; } -void RemoteConnectionImpl::registerPipeline(const Pipeline& pipeline) { +void RemoteConnectionImpl::registerPipeline(Pipeline& pipeline) { exposePipelineService(pipeline); } @@ -391,34 +391,98 @@ void RemoteConnectionImpl::exposeKeyPressedService() { }; } -void RemoteConnectionImpl::exposePipelineService(const Pipeline& pipeline) { +void RemoteConnectionImpl::exposePipelineService(Pipeline& pipeline) { // Make sure pipeline is built so that we can serialize it. // If not built, an error is thrown is case there are host -> device or device -> host connections. DAI_CHECK(pipeline.isBuilt(), "Pipeline is not built. Call Pipeline::build first!"); std::vector services; - auto pipelineService = foxglove::ServiceWithoutId(); - pipelineService.name = "pipelineSchema"; - auto request = foxglove::ServiceRequestDefinition(); - request.schemaName = "pipelineSchema"; - request.schema = ""; - request.encoding = "json"; - pipelineService.request = request; - pipelineService.response = request; - pipelineService.type = "json"; - services.push_back(pipelineService); + { + auto pipelineService = foxglove::ServiceWithoutId(); + pipelineService.name = "pipelineSchema"; + auto request = foxglove::ServiceRequestDefinition(); + request.schemaName = "pipelineSchema"; + request.schema = ""; + request.encoding = "json"; + pipelineService.request = request; + pipelineService.response = request; + pipelineService.type = "json"; + services.push_back(pipelineService); + } + { + auto pipelineService = foxglove::ServiceWithoutId(); + pipelineService.name = "fullPipelineSchema"; + auto request = foxglove::ServiceRequestDefinition(); + request.schemaName = "fullPipelineSchema"; + request.schema = ""; + request.encoding = "json"; + pipelineService.request = request; + pipelineService.response = request; + pipelineService.type = "json"; + services.push_back(pipelineService); + } + { + auto pipelineService = foxglove::ServiceWithoutId(); + pipelineService.name = "pipelineState"; + auto request = foxglove::ServiceRequestDefinition(); + request.schemaName = "pipelineState"; + request.schema = ""; + request.encoding = "json"; + pipelineService.request = request; + pipelineService.response = request; + pipelineService.type = "json"; + services.push_back(pipelineService); + } auto ids = server->addServices(services); - assert(ids.size() == 1); - auto id = ids[0]; + assert(ids.size() == 3); + { + // Pipeline Schema + + auto id = ids[0]; + + auto serializedPipeline = pipeline.serializeToJson(false); + auto serializedPipelineStr = serializedPipeline.dump(); + serviceMap[id] = [serializedPipelineStr](foxglove::ServiceResponse request) { + (void)request; + auto response = foxglove::ServiceResponse(); + response.data = std::vector(serializedPipelineStr.begin(), serializedPipelineStr.end()); + return response; + }; + } + { + // Full Pipeline Schema + + auto id = ids[1]; + + nlohmann::json j; + j["pipeline"] = pipeline.getPipelineSchema(SerializationType::JSON, false); + auto serializedPipelineStr = j.dump(); + serviceMap[id] = [serializedPipelineStr](foxglove::ServiceResponse request) { + (void)request; + auto response = foxglove::ServiceResponse(); + response.data = std::vector(serializedPipelineStr.begin(), serializedPipelineStr.end()); + return response; + }; + } + { + // Pipeline State - auto serializedPipeline = pipeline.serializeToJson(false); - auto serializedPipelineStr = serializedPipeline.dump(); - serviceMap[id] = [serializedPipelineStr](foxglove::ServiceResponse request) { - (void)request; - auto response = foxglove::ServiceResponse(); - response.data = std::vector(serializedPipelineStr.begin(), serializedPipelineStr.end()); - return response; - }; + auto id = ids[2]; + + serviceMap[id] = [&pipeline](foxglove::ServiceResponse request) { + (void)request; + std::string stateStr; + try { + auto state = pipeline.getPipelineState().nodes().detailed(); + stateStr = ((nlohmann::json)state).dump(); + } catch(const std::runtime_error& e) { + stateStr = R"({"error": "Pipeline debugging disabled. Cannot get pipeline state."})"; + } + auto response = foxglove::ServiceResponse(); + response.data = std::vector(stateStr.begin(), stateStr.end()); + return response; + }; + } } void RemoteConnectionImpl::registerService(const std::string& serviceName, std::function callback) { diff --git a/src/remote_connection/RemoteConnectionImpl.hpp b/src/remote_connection/RemoteConnectionImpl.hpp index ae42b791b..3b3b5a3ca 100644 --- a/src/remote_connection/RemoteConnectionImpl.hpp +++ b/src/remote_connection/RemoteConnectionImpl.hpp @@ -28,7 +28,7 @@ class RemoteConnectionImpl { std::shared_ptr addTopic( const std::string& topicName, const std::string& group, unsigned int maxSize, bool blocking, bool useVisualizationIfAvailable); bool removeTopic(const std::string& topicName); - void registerPipeline(const Pipeline& pipeline); + void registerPipeline(Pipeline& pipeline); void registerService(const std::string& serviceName, std::function callback); int waitKey(int delayMs); @@ -56,7 +56,7 @@ class RemoteConnectionImpl { bool useVisualizationIfAvailable); void exposeTopicGroupsService(); void exposeKeyPressedService(); - void exposePipelineService(const Pipeline& pipeline); + void exposePipelineService(Pipeline& pipeline); void keyPressedCallback(int key); std::mutex keyMutex; From ab48d7467d112bfcafdc16d888bd555d992b897f Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 10 Nov 2025 12:14:54 +0100 Subject: [PATCH 066/124] Change pipeline state fg json [no ci] --- src/remote_connection/RemoteConnectionImpl.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/remote_connection/RemoteConnectionImpl.cpp b/src/remote_connection/RemoteConnectionImpl.cpp index a99ecd8c8..943d5a507 100644 --- a/src/remote_connection/RemoteConnectionImpl.cpp +++ b/src/remote_connection/RemoteConnectionImpl.cpp @@ -473,8 +473,10 @@ void RemoteConnectionImpl::exposePipelineService(Pipeline& pipeline) { (void)request; std::string stateStr; try { + nlohmann::json j; auto state = pipeline.getPipelineState().nodes().detailed(); - stateStr = ((nlohmann::json)state).dump(); + j["nodeStates"] = ((nlohmann::json)state)["nodeStates"]; + stateStr = j.dump(); } catch(const std::runtime_error& e) { stateStr = R"({"error": "Pipeline debugging disabled. Cannot get pipeline state."})"; } From 0c874741312681b19a054668fedc141d59fa6b37 Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 10 Nov 2025 13:57:01 +0100 Subject: [PATCH 067/124] RVC2 FW: Fix script node issue with pipeline debugging --- cmake/Depthai/DepthaiDeviceSideConfig.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceSideConfig.cmake b/cmake/Depthai/DepthaiDeviceSideConfig.cmake index 7bdd6c14d..688044c9f 100644 --- a/cmake/Depthai/DepthaiDeviceSideConfig.cmake +++ b/cmake/Depthai/DepthaiDeviceSideConfig.cmake @@ -2,7 +2,7 @@ set(DEPTHAI_DEVICE_SIDE_MATURITY "snapshot") # "full commit hash of device side binary" -set(DEPTHAI_DEVICE_SIDE_COMMIT "38564345773c52550731e509bbf5f1464c9f263d") +set(DEPTHAI_DEVICE_SIDE_COMMIT "90b5b48739ee78ba1b9c08791e3845d6d4d2408b") # "version if applicable" set(DEPTHAI_DEVICE_SIDE_VERSION "") From 22ac8fd65da6e05f03e047f92a35de7a9635cfb8 Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 10 Nov 2025 15:12:29 +0100 Subject: [PATCH 068/124] Add toJson to PipelineState --- include/depthai/pipeline/datatype/PipelineState.hpp | 2 ++ src/pipeline/datatype/PipelineState.cpp | 8 +++++++- src/remote_connection/RemoteConnectionImpl.cpp | 4 +--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index a5970ace3..d824b978e 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -112,6 +112,8 @@ class PipelineState : public Buffer { datatype = DatatypeEnum::PipelineState; }; + nlohmann::json toJson() const; + DEPTHAI_SERIALIZE(PipelineState, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodeStates, configSequenceNum); }; diff --git a/src/pipeline/datatype/PipelineState.cpp b/src/pipeline/datatype/PipelineState.cpp index 0ba8f8a8a..d683a1356 100644 --- a/src/pipeline/datatype/PipelineState.cpp +++ b/src/pipeline/datatype/PipelineState.cpp @@ -1,3 +1,9 @@ #include "depthai/pipeline/datatype/PipelineState.hpp" -namespace dai {} // namespace dai +namespace dai { +nlohmann::json PipelineState::toJson() const { + nlohmann::json j; + j["nodeStates"] = nodeStates; + return j; +} +} // namespace dai diff --git a/src/remote_connection/RemoteConnectionImpl.cpp b/src/remote_connection/RemoteConnectionImpl.cpp index 943d5a507..89ab1143d 100644 --- a/src/remote_connection/RemoteConnectionImpl.cpp +++ b/src/remote_connection/RemoteConnectionImpl.cpp @@ -473,10 +473,8 @@ void RemoteConnectionImpl::exposePipelineService(Pipeline& pipeline) { (void)request; std::string stateStr; try { - nlohmann::json j; auto state = pipeline.getPipelineState().nodes().detailed(); - j["nodeStates"] = ((nlohmann::json)state)["nodeStates"]; - stateStr = j.dump(); + stateStr = state.toJson().dump(); } catch(const std::runtime_error& e) { stateStr = R"({"error": "Pipeline debugging disabled. Cannot get pipeline state."})"; } From fd4e6921e47b03d69592dec59cafb25d1a950b3a Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 10 Nov 2025 16:32:40 +0100 Subject: [PATCH 069/124] Add pipeline debugging examples --- examples/cpp/Misc/CMakeLists.txt | 3 +- .../cpp/Misc/PipelineDebugging/CMakeLists.txt | 7 ++ .../PipelineDebugging/get_pipeline_state.cpp | 104 ++++++++++++++++++ examples/python/CMakeLists.txt | 3 + .../PipelineDebugging/get_pipeline_state.py | 76 +++++++++++++ 5 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 examples/cpp/Misc/PipelineDebugging/CMakeLists.txt create mode 100644 examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp create mode 100644 examples/python/Misc/PipelineDebugging/get_pipeline_state.py diff --git a/examples/cpp/Misc/CMakeLists.txt b/examples/cpp/Misc/CMakeLists.txt index 9e42fa359..ba1876750 100644 --- a/examples/cpp/Misc/CMakeLists.txt +++ b/examples/cpp/Misc/CMakeLists.txt @@ -2,4 +2,5 @@ project(misc_examples) cmake_minimum_required(VERSION 3.10) add_subdirectory(AutoReconnect) -add_subdirectory(Projectors) \ No newline at end of file +add_subdirectory(Projectors) +add_subdirectory(PipelineDebugging) diff --git a/examples/cpp/Misc/PipelineDebugging/CMakeLists.txt b/examples/cpp/Misc/PipelineDebugging/CMakeLists.txt new file mode 100644 index 000000000..94e0c228b --- /dev/null +++ b/examples/cpp/Misc/PipelineDebugging/CMakeLists.txt @@ -0,0 +1,7 @@ +project(pipeline_debugging_examples) +cmake_minimum_required(VERSION 3.10) + +## function: dai_add_example(example_name example_src enable_test use_pcl) +## function: dai_set_example_test_labels(example_name ...) + +dai_add_example(get_pipeline_state get_pipeline_state.cpp ON OFF) diff --git a/examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp b/examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp new file mode 100644 index 000000000..7f033ece6 --- /dev/null +++ b/examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp @@ -0,0 +1,104 @@ +#include +#include + +#include "depthai/depthai.hpp" + +int main() { + dai::Pipeline pipeline; + pipeline.enablePipelineDebugging(); + + auto monoLeft = pipeline.create()->build(dai::CameraBoardSocket::CAM_B); + auto monoRight = pipeline.create()->build(dai::CameraBoardSocket::CAM_C); + + auto stereo = pipeline.create(); + + auto monoLeftOut = monoLeft->requestFullResolutionOutput(); + auto monoRightOut = monoRight->requestFullResolutionOutput(); + + monoLeftOut->link(stereo->left); + monoRightOut->link(stereo->right); + + stereo->setRectification(true); + stereo->setExtendedDisparity(true); + stereo->setLeftRightCheck(true); + + auto disparityQueue = stereo->disparity.createOutputQueue(); + + double maxDisparity = 1.0; + pipeline.start(); + while(true) { + auto disparity = disparityQueue->get(); + cv::Mat npDisparity = disparity->getFrame(); + + double minVal, curMax; + cv::minMaxLoc(npDisparity, &minVal, &curMax); + maxDisparity = std::max(maxDisparity, curMax); + + // Normalize the disparity image to an 8-bit scale. + cv::Mat normalized; + npDisparity.convertTo(normalized, CV_8UC1, 255.0 / maxDisparity); + + cv::Mat colorizedDisparity; + cv::applyColorMap(normalized, colorizedDisparity, cv::COLORMAP_JET); + + // Set pixels with zero disparity to black. + colorizedDisparity.setTo(cv::Scalar(0, 0, 0), normalized == 0); + + cv::imshow("disparity", colorizedDisparity); + + int key = cv::waitKey(1); + if(key == 'q') { + break; + } else if(key == 's') { + auto state = pipeline.getPipelineState().nodes().detailed(); + try { + // Assuming these APIs exist similarly in C++ + auto pipelineState = pipeline.getPipelineState().nodes().detailed(); + + for(const auto& [nodeId, nodeState] : pipelineState.nodeStates) { + std::string nodeName = pipeline.getNode(nodeId)->getName(); + + std::cout << "\n# State for node " << nodeName << " (" << nodeId << "):\n"; + + std::cout << "## State: " << (int)nodeState.state << "\n"; + + std::cout << "## mainLoopTiming: " << (nodeState.mainLoopTiming.isValid() ? "" : "invalid") << "\n"; + if(nodeState.mainLoopTiming.isValid()) { + std::cout << "-----\n" << nodeState.mainLoopTiming.str() << "\n-----\n"; + } + + std::cout << "## inputsGetTiming: " << (nodeState.inputsGetTiming.isValid() ? "" : "invalid") << "\n"; + if(nodeState.inputsGetTiming.isValid()) { + std::cout << "-----\n" << nodeState.inputsGetTiming.str() << "\n-----\n"; + } + + std::cout << "## outputsSendTiming: " << (nodeState.outputsSendTiming.isValid() ? "" : "invalid") << "\n"; + if(nodeState.outputsSendTiming.isValid()) { + std::cout << "-----\n" << nodeState.outputsSendTiming.str() << "\n-----\n"; + } + + std::cout << "## inputStates: " << (nodeState.inputStates.empty() ? "empty" : "") << "\n"; + for(const auto& [inputName, inputState] : nodeState.inputStates) { + std::cout << "### " << inputName << ":\n-----" << inputState.str() << "\n-----\n"; + } + + std::cout << "## outputStates: " << (nodeState.outputStates.empty() ? "empty" : "") << "\n"; + for(const auto& [outputName, outputState] : nodeState.outputStates) { + std::cout << "### " << outputName << ":\n-----" << outputState.str() << "\n-----\n"; + } + + std::cout << "## otherTimings: " << (nodeState.otherTimings.empty() ? "empty" : "") << "\n"; + for(const auto& [otherName, otherTiming] : nodeState.otherTimings) { + std::cout << "### " << otherName << ":\n-----" << otherTiming.str() << "\n-----\n"; + } + } + } catch(const std::runtime_error& e) { + std::cerr << "Error getting pipeline state: " << e.what() << "\n"; + } + } + } + + pipeline.stop(); + + return 0; +} diff --git a/examples/python/CMakeLists.txt b/examples/python/CMakeLists.txt index 569707305..d9d5b5697 100644 --- a/examples/python/CMakeLists.txt +++ b/examples/python/CMakeLists.txt @@ -218,6 +218,9 @@ dai_set_example_test_labels(image_manip_remap ondevice rvc2_all rvc4 rvc4rgb ci) add_python_example(reconnect_callback Misc/AutoReconnect/reconnect_callback.py) dai_set_example_test_labels(reconnect_callback ondevice rvc2_all rvc4 rvc4rgb ci) +add_python_example(get_pipeline_state Misc/PipelineDebugging/get_pipeline_state.py) +dai_set_example_test_labels(get_pipeline_state ondevice rvc2_all rvc4 rvc4rgb ci) + ## SystemLogger add_python_example(system_logger RVC2/SystemLogger/system_information.py) dai_set_example_test_labels(system_logger ondevice rvc2_all ci) diff --git a/examples/python/Misc/PipelineDebugging/get_pipeline_state.py b/examples/python/Misc/PipelineDebugging/get_pipeline_state.py new file mode 100644 index 000000000..50970b26d --- /dev/null +++ b/examples/python/Misc/PipelineDebugging/get_pipeline_state.py @@ -0,0 +1,76 @@ +import depthai as dai +import numpy as np +import cv2 + +with dai.Pipeline() as pipeline: + # Pipeline debugging is disabled by default. + # You can also enable it by setting the DEPTHAI_PIPELINE_DEBUGGING environment variable to '1' + pipeline.enablePipelineDebugging(True) + + monoLeft = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_B) + monoRight = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_C) + stereo = pipeline.create(dai.node.StereoDepth) + + # Linking + monoLeftOut = monoLeft.requestFullResolutionOutput() + monoRightOut = monoRight.requestFullResolutionOutput() + monoLeftOut.link(stereo.left) + monoRightOut.link(stereo.right) + + stereo.setRectification(True) + stereo.setExtendedDisparity(True) + stereo.setLeftRightCheck(True) + + disparityQueue = stereo.disparity.createOutputQueue() + + colorMap = cv2.applyColorMap(np.arange(256, dtype=np.uint8), cv2.COLORMAP_JET) + colorMap[0] = [0, 0, 0] # to make zero-disparity pixels black + + with pipeline: + pipeline.start() + maxDisparity = 1 + while pipeline.isRunning(): + disparity = disparityQueue.get() + assert isinstance(disparity, dai.ImgFrame) + npDisparity = disparity.getFrame() + maxDisparity = max(maxDisparity, np.max(npDisparity)) + colorizedDisparity = cv2.applyColorMap(((npDisparity / maxDisparity) * 255).astype(np.uint8), colorMap) + cv2.imshow("disparity", colorizedDisparity) + key = cv2.waitKey(1) + if key == ord('q'): + pipeline.stop() + break + elif key == ord('s'): + try: + # If pipeline debugging is disabled, this will raise an exception + pipelineState = pipeline.getPipelineState().nodes().detailed() + for nodeId, nodeState in pipelineState.nodeStates.items(): + nodeName = pipeline.getNode(nodeId).getName() + print(f"\n# State for node {pipeline.getNode(nodeId).getName()} ({nodeId}):") + print(f"## State: {nodeState.state}") + print(f"## mainLoopTiming: {'invalid' if not nodeState.mainLoopTiming.isValid() else ''}") + if(nodeState.mainLoopTiming.isValid()): + print("-----") + print(nodeState.mainLoopTiming) + print("-----") + print(f"## inputsGetTiming: {'invalid' if not nodeState.inputsGetTiming.isValid() else ''}") + if(nodeState.inputsGetTiming.isValid()): + print("-----") + print(nodeState.inputsGetTiming) + print("-----") + print(f"## outputsSendTiming: {'invalid' if not nodeState.outputsSendTiming.isValid() else ''}") + if(nodeState.outputsSendTiming.isValid()): + print("-----") + print(nodeState.outputsSendTiming) + print("-----") + print(f"## inputStates: {'empty' if not nodeState.inputStates else ''}") + for inputName, inputState in nodeState.inputStates.items(): + print(f"### {inputName}:\n-----{inputState}\n-----") + print(f"## outputStates: {'empty' if not nodeState.outputStates else ''}") + for outputName, outputState in nodeState.outputStates.items(): + print(f"### {outputName}:\n-----{outputState}\n-----") + print(f"## otherTimings: {'empty' if not nodeState.otherTimings else ''}") + for otherName, otherTiming in nodeState.otherTimings.items(): + print(f"### {otherName}:\n-----{otherTiming}\n-----") + except Exception as e: + print("Error getting pipeline state:", e) From 0f89f3e67bc69b74a6055eb14ad2dd4fd1388d69 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 11 Nov 2025 09:19:09 +0100 Subject: [PATCH 070/124] Fix try get pipeline debugging issue --- include/depthai/pipeline/MessageQueue.hpp | 6 ++- .../pipeline_debugging_host_test.cpp | 53 +++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/include/depthai/pipeline/MessageQueue.hpp b/include/depthai/pipeline/MessageQueue.hpp index 72de0ca10..517a0aa74 100644 --- a/include/depthai/pipeline/MessageQueue.hpp +++ b/include/depthai/pipeline/MessageQueue.hpp @@ -214,7 +214,11 @@ class MessageQueue : public std::enable_shared_from_this { if(pipelineEventDispatcher) { auto blockEvent = pipelineEventDispatcher->blockEvent(PipelineEvent::Type::INPUT, name); blockEvent.setQueueSize(getSize()); - return getInput(); + auto result = getInput(); + if(!result) { + blockEvent.cancel(); + } + return result; } else { return getInput(); } diff --git a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp index 9d34d9884..defd91ade 100644 --- a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp +++ b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp @@ -191,6 +191,33 @@ class BridgeNode : public node::CustomThreadedNode { } }; +class TryNode : public node::CustomThreadedNode { + bool doStep = true; + int runTo = 0; + + public: + Input input{*this, {"input", DEFAULT_GROUP, true, 4, {{{DatatypeEnum::Buffer, true}}}, DEFAULT_WAIT_FOR_MESSAGE}}; + Output output{*this, {"output", DEFAULT_GROUP, {{{DatatypeEnum::Buffer, true}}}}}; + + void run() override { + while(mainLoop()) { + std::shared_ptr msg = nullptr; + { + auto blockEvent = this->inputBlockEvent(); + msg = input.tryGet(); + } + if(msg == nullptr) { + msg = std::make_shared(); + } + { + auto blockEvent = this->outputBlockEvent(); + output.trySend(msg); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } +}; + class PipelineHandler { std::unordered_map> pingQueues; std::unordered_map> ackQueues; @@ -199,7 +226,7 @@ class PipelineHandler { public: Pipeline pipeline; - PipelineHandler(int idx=0) : pipeline(false) { + PipelineHandler(int idx = 0) : pipeline(false) { pipeline.enablePipelineDebugging(); switch(idx) { @@ -456,13 +483,13 @@ TEST_CASE("Input duration test") { ph.ping("cons", 0); for(int i = 0; i < 10; ++i) { - ph.ping("cons", 0); // input get + ph.ping("cons", 0); // input get std::this_thread::sleep_for(std::chrono::milliseconds(900)); ph.ping("gen", 0); // output send std::this_thread::sleep_for(std::chrono::milliseconds(100)); } - std::this_thread::sleep_for(std::chrono::seconds(1)); // Wait for state update + std::this_thread::sleep_for(std::chrono::seconds(1)); // Wait for state update auto inputState = ph.pipeline.getPipelineState().nodes(ph.getNodeId("cons")).inputs("input"); REQUIRE(inputState.isValid()); @@ -474,3 +501,23 @@ TEST_CASE("Input duration test") { REQUIRE(inputState.timing.durationStats.maxMicros == Catch::Approx(1e6).margin(0.4e6)); REQUIRE(inputState.timing.durationStats.minMicros == Catch::Approx(1e6).margin(0.4e6)); } + +TEST_CASE("Try I/O test") { + dai::Pipeline p(false); + p.enablePipelineDebugging(); + + auto tryNode = p.create(); + auto gen = p.create(); + auto cons = p.create(); + gen->output.link(tryNode->input); + tryNode->output.link(cons->input); + + p.start(); + + std::this_thread::sleep_for(std::chrono::seconds(3)); + + auto state = p.getPipelineState().nodes().detailed(); + REQUIRE(!state.nodeStates.at(tryNode->id).inputStates["input"].isValid()); + REQUIRE(state.nodeStates.at(tryNode->id).inputStates["input"].timing.fps == 0.f); + REQUIRE(state.nodeStates.at(tryNode->id).outputStates["output"].isValid()); +} From aeaa95a7ef04202c571d69c728d0b0475a4a476c Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 11 Nov 2025 12:28:27 +0100 Subject: [PATCH 071/124] bugfixes, pipeline event dispatcher improvements [no ci] --- .../PipelineDebugging/get_pipeline_state.cpp | 5 ++- .../PipelineDebugging/get_pipeline_state.py | 5 ++- .../utility/PipelineEventDispatcher.hpp | 20 ++++++++--- .../PipelineEventDispatcherInterface.hpp | 35 ++++++++++++++----- .../internal/PipelineEventAggregation.cpp | 10 +++--- src/utility/PipelineEventDispatcher.cpp | 34 ++++++++++++------ 6 files changed, 80 insertions(+), 29 deletions(-) diff --git a/examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp b/examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp index 7f033ece6..5d001fb1b 100644 --- a/examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp +++ b/examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp @@ -79,7 +79,10 @@ int main() { std::cout << "## inputStates: " << (nodeState.inputStates.empty() ? "empty" : "") << "\n"; for(const auto& [inputName, inputState] : nodeState.inputStates) { - std::cout << "### " << inputName << ":\n-----" << inputState.str() << "\n-----\n"; + if(inputState.isValid()) + std::cout << "### " << inputName << ":\n-----" << inputState.str() << "\n-----\n"; + else + std::cout << "### " << inputName << ": invalid\n"; } std::cout << "## outputStates: " << (nodeState.outputStates.empty() ? "empty" : "") << "\n"; diff --git a/examples/python/Misc/PipelineDebugging/get_pipeline_state.py b/examples/python/Misc/PipelineDebugging/get_pipeline_state.py index 50970b26d..8022d4942 100644 --- a/examples/python/Misc/PipelineDebugging/get_pipeline_state.py +++ b/examples/python/Misc/PipelineDebugging/get_pipeline_state.py @@ -65,7 +65,10 @@ print("-----") print(f"## inputStates: {'empty' if not nodeState.inputStates else ''}") for inputName, inputState in nodeState.inputStates.items(): - print(f"### {inputName}:\n-----{inputState}\n-----") + if inputState.isValid(): + print(f"### {inputName}:\n-----{inputState}\n-----") + else: + print(f"### {inputName}: invalid") print(f"## outputStates: {'empty' if not nodeState.outputStates else ''}") for outputName, outputState in nodeState.outputStates.items(): print(f"### {outputName}:\n-----{outputState}\n-----") diff --git a/include/depthai/utility/PipelineEventDispatcher.hpp b/include/depthai/utility/PipelineEventDispatcher.hpp index 4f64d9850..3f5d8e13d 100644 --- a/include/depthai/utility/PipelineEventDispatcher.hpp +++ b/include/depthai/utility/PipelineEventDispatcher.hpp @@ -42,15 +42,25 @@ class PipelineEventDispatcher : public PipelineEventDispatcherInterface { void endInputEvent(const std::string& source, std::optional queueSize = std::nullopt) override; void endOutputEvent(const std::string& source) override; void endCustomEvent(const std::string& source) override; - void startTrackedEvent(PipelineEvent event) override; - void startTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) override; - void endTrackedEvent(PipelineEvent event) override; - void endTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) override; + // Timestamp in event is not used. If ts is specified ts is used, else curent time is applied + void startTrackedEvent(PipelineEvent event, std::optional> ts = std::nullopt) override; + void startTrackedEvent(PipelineEvent::Type type, + const std::string& source, + int64_t sequenceNum, + std::optional> ts = std::nullopt) override; + // Timestamp in event is not used. If ts is specified ts is used, else curent time is applied + void endTrackedEvent(PipelineEvent event, std::optional> ts = std::nullopt) override; + void endTrackedEvent(PipelineEvent::Type type, + const std::string& source, + int64_t sequenceNum, + std::optional> ts = std::nullopt) override; void pingEvent(PipelineEvent::Type type, const std::string& source) override; void pingMainLoopEvent() override; void pingCustomEvent(const std::string& source) override; void pingInputEvent(const std::string& source, int32_t status, std::optional queueSize = std::nullopt) override; - BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source) override; + BlockPipelineEvent blockEvent(PipelineEvent::Type type, + const std::string& source, + std::optional> ts = std::nullopt) override; BlockPipelineEvent inputBlockEvent() override; BlockPipelineEvent outputBlockEvent() override; BlockPipelineEvent customBlockEvent(const std::string& source) override; diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index 1e91338ec..a23ca905f 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -20,11 +20,19 @@ class PipelineEventDispatcherInterface { bool canceled = false; std::optional queueSize = std::nullopt; + std::optional> endTs = std::nullopt; public: - BlockPipelineEvent(PipelineEventDispatcherInterface& dispatcher, PipelineEvent::Type type, const std::string& source) + BlockPipelineEvent(PipelineEventDispatcherInterface& dispatcher, + PipelineEvent::Type type, + const std::string& source, + std::optional> ts = std::nullopt) : dispatcher(dispatcher), type(type), source(source), sequence(dispatcher.sequence++) { - dispatcher.startTrackedEvent(type, source, sequence); + PipelineEvent event; + event.type = type; + event.source = source; + event.sequenceNum = sequence; + dispatcher.startTrackedEvent(event, ts); } ~BlockPipelineEvent() { if(canceled || std::uncaught_exceptions() > 0) return; @@ -33,7 +41,7 @@ class PipelineEventDispatcherInterface { event.source = source; event.sequenceNum = sequence; event.queueSize = queueSize; - dispatcher.endTrackedEvent(type, source, sequence); + dispatcher.endTrackedEvent(event, endTs); } void cancel() { canceled = true; @@ -41,6 +49,9 @@ class PipelineEventDispatcherInterface { void setQueueSize(uint32_t qs) { queueSize = qs; } + void setEndTimestamp(std::chrono::time_point ts) { + endTs = ts; + } }; bool sendEvents = true; @@ -55,15 +66,23 @@ class PipelineEventDispatcherInterface { virtual void endInputEvent(const std::string& source, std::optional queueSize = std::nullopt) = 0; virtual void endOutputEvent(const std::string& source) = 0; virtual void endCustomEvent(const std::string& source) = 0; - virtual void startTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) = 0; - virtual void startTrackedEvent(PipelineEvent event) = 0; - virtual void endTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) = 0; - virtual void endTrackedEvent(PipelineEvent event) = 0; + virtual void startTrackedEvent(PipelineEvent event, std::optional> ts = std::nullopt) = 0; + virtual void startTrackedEvent(PipelineEvent::Type type, + const std::string& source, + int64_t sequenceNum, + std::optional> ts = std::nullopt) = 0; + virtual void endTrackedEvent(PipelineEvent event, std::optional> ts = std::nullopt) = 0; + virtual void endTrackedEvent(PipelineEvent::Type type, + const std::string& source, + int64_t sequenceNum, + std::optional> ts = std::nullopt) = 0; virtual void pingEvent(PipelineEvent::Type type, const std::string& source) = 0; virtual void pingMainLoopEvent() = 0; virtual void pingCustomEvent(const std::string& source) = 0; virtual void pingInputEvent(const std::string& source, int32_t status, std::optional queueSize = std::nullopt) = 0; - virtual BlockPipelineEvent blockEvent(PipelineEvent::Type type, const std::string& source) = 0; + virtual BlockPipelineEvent blockEvent(PipelineEvent::Type type, + const std::string& source, + std::optional> ts = std::nullopt) = 0; virtual BlockPipelineEvent inputBlockEvent() = 0; virtual BlockPipelineEvent outputBlockEvent() = 0; virtual BlockPipelineEvent customBlockEvent(const std::string& source) = 0; diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 741b15291..405397d35 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -33,6 +33,7 @@ class NodeEventAggregation { mainLoopTimingsBuffer = std::make_unique>(windowSize); inputsGetFpsBuffer = std::make_unique>(windowSize); outputsSendFpsBuffer = std::make_unique>(windowSize); + mainLoopFpsBuffer = std::make_unique>(windowSize); } NodeState state; @@ -50,6 +51,7 @@ class NodeEventAggregation { std::unordered_map>> outputFpsBuffers; std::unique_ptr> inputsGetFpsBuffer; std::unique_ptr> outputsSendFpsBuffer; + std::unique_ptr> mainLoopFpsBuffer; std::unordered_map>> otherFpsBuffers; std::unordered_map>>> ongoingInputEvents; @@ -116,7 +118,7 @@ class NodeEventAggregation { auto& fpsBuffer = [&]() -> std::unique_ptr>& { switch(event.type) { case PipelineEvent::Type::LOOP: - return emptyTimeBuffer; + return mainLoopFpsBuffer; case PipelineEvent::Type::INPUT: return inputFpsBuffers[event.source]; case PipelineEvent::Type::OUTPUT: @@ -145,7 +147,7 @@ class NodeEventAggregation { eventsBuffer.add(durationEvent); timingsBuffer->add(durationEvent.durationUs); - if(event.type != PipelineEvent::Type::LOOP) fpsBuffer->add({durationEvent.startEvent.getTimestamp()}); + fpsBuffer->add({durationEvent.startEvent.getTimestamp()}); *ongoingEvent = std::nullopt; @@ -193,7 +195,7 @@ class NodeEventAggregation { auto& fpsBuffer = [&]() -> std::unique_ptr>& { switch(event.type) { case PipelineEvent::Type::LOOP: - break; + return mainLoopFpsBuffer; case PipelineEvent::Type::CUSTOM: return otherFpsBuffers[event.source]; case PipelineEvent::Type::INPUT: @@ -336,7 +338,7 @@ class NodeEventAggregation { break; case PipelineEvent::Type::LOOP: updateTimingStats(state.mainLoopTiming.durationStats, *mainLoopTimingsBuffer); - state.mainLoopTiming.fps = 1e6f / (float)state.mainLoopTiming.durationStats.averageMicrosRecent; + updateFpsStats(state.mainLoopTiming, *mainLoopFpsBuffer); break; case PipelineEvent::Type::INPUT: for(auto& [source, _] : inputTimingsBuffers) { diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index 72bdaf6d2..5fa3ef77a 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -112,14 +112,17 @@ void PipelineEventDispatcher::endOutputEvent(const std::string& source) { void PipelineEventDispatcher::endCustomEvent(const std::string& source) { endEvent(PipelineEvent::Type::CUSTOM, source, std::nullopt); } -void PipelineEventDispatcher::startTrackedEvent(PipelineEvent event) { +void PipelineEventDispatcher::startTrackedEvent(PipelineEvent event, std::optional> ts) { if(!sendEvents) return; if(blacklist(event.type, event.source)) return; checkNodeId(); std::lock_guard lock(mutex); - event.setTimestamp(std::chrono::steady_clock::now()); + if(!ts.has_value()) + event.setTimestamp(std::chrono::steady_clock::now()); + else + event.setTimestamp(ts.value()); event.tsDevice = event.ts; event.nodeId = nodeId; event.interval = PipelineEvent::Interval::START; @@ -128,21 +131,27 @@ void PipelineEventDispatcher::startTrackedEvent(PipelineEvent event) { out->send(std::make_shared(event)); } } -void PipelineEventDispatcher::startTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { +void PipelineEventDispatcher::startTrackedEvent(PipelineEvent::Type type, + const std::string& source, + int64_t sequenceNum, + std::optional> ts) { PipelineEvent event; event.type = type; event.source = source; event.sequenceNum = sequenceNum; - startTrackedEvent(event); + startTrackedEvent(event, ts); } -void PipelineEventDispatcher::endTrackedEvent(PipelineEvent event) { +void PipelineEventDispatcher::endTrackedEvent(PipelineEvent event, std::optional> ts) { if(!sendEvents) return; if(blacklist(event.type, event.source)) return; checkNodeId(); std::lock_guard lock(mutex); - event.setTimestamp(std::chrono::steady_clock::now()); + if(!ts.has_value()) + event.setTimestamp(std::chrono::steady_clock::now()); + else + event.setTimestamp(ts.value()); event.tsDevice = event.ts; event.nodeId = nodeId; event.interval = PipelineEvent::Interval::END; @@ -151,12 +160,15 @@ void PipelineEventDispatcher::endTrackedEvent(PipelineEvent event) { out->send(std::make_shared(event)); } } -void PipelineEventDispatcher::endTrackedEvent(PipelineEvent::Type type, const std::string& source, int64_t sequenceNum) { +void PipelineEventDispatcher::endTrackedEvent(PipelineEvent::Type type, + const std::string& source, + int64_t sequenceNum, + std::optional> ts) { PipelineEvent event; event.type = type; event.source = source; event.sequenceNum = sequenceNum; - endTrackedEvent(event); + endTrackedEvent(event, ts); } void PipelineEventDispatcher::pingEvent(PipelineEvent::Type type, const std::string& source) { if(!sendEvents) return; @@ -213,8 +225,10 @@ void PipelineEventDispatcher::pingInputEvent(const std::string& source, int32_t out->send(std::make_shared(eventCopy)); } } -PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::blockEvent(PipelineEvent::Type type, const std::string& source) { - return BlockPipelineEvent(*this, type, source); +PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::blockEvent(PipelineEvent::Type type, + const std::string& source, + std::optional> ts) { + return BlockPipelineEvent(*this, type, source, ts); } PipelineEventDispatcher::BlockPipelineEvent PipelineEventDispatcher::inputBlockEvent() { // For convenience due to the default source From 4e242b76f2233f6ec6e820c1342a5ec131e27404 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 11 Nov 2025 12:32:09 +0100 Subject: [PATCH 072/124] RVC4 FW: Add pipeline debugging to stereo depth, camera --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 47079cee1..4595a729e 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+f3484d9c8d6d3b67d91139824ec43a4eabdb1a86") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+2e622cc6b938b292e35d442bbfcb24ef0ac147ab") From d758208071d70e1878b47785d17d79327189355d Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 11 Nov 2025 13:50:37 +0100 Subject: [PATCH 073/124] Fix macos build? --- include/depthai/utility/PipelineEventDispatcherInterface.hpp | 2 +- src/utility/PipelineEventDispatcher.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/include/depthai/utility/PipelineEventDispatcherInterface.hpp b/include/depthai/utility/PipelineEventDispatcherInterface.hpp index a23ca905f..a2eed1df6 100644 --- a/include/depthai/utility/PipelineEventDispatcherInterface.hpp +++ b/include/depthai/utility/PipelineEventDispatcherInterface.hpp @@ -56,7 +56,7 @@ class PipelineEventDispatcherInterface { bool sendEvents = true; - virtual ~PipelineEventDispatcherInterface() = default; + virtual ~PipelineEventDispatcherInterface(); virtual void setNodeId(int64_t id) = 0; virtual void startEvent(PipelineEvent::Type type, const std::string& source, std::optional queueSize = std::nullopt) = 0; virtual void startInputEvent(const std::string& source, std::optional queueSize = std::nullopt) = 0; diff --git a/src/utility/PipelineEventDispatcher.cpp b/src/utility/PipelineEventDispatcher.cpp index 5fa3ef77a..7f887084c 100644 --- a/src/utility/PipelineEventDispatcher.cpp +++ b/src/utility/PipelineEventDispatcher.cpp @@ -8,6 +8,8 @@ namespace utility { constexpr const char* OUTPUT_BLOCK_NAME = "sendOutputs"; constexpr const char* INPUT_BLOCK_NAME = "getInputs"; +PipelineEventDispatcherInterface::~PipelineEventDispatcherInterface() = default; + std::string typeToString(PipelineEvent::Type type) { switch(type) { case PipelineEvent::Type::CUSTOM: From b9cda65f6fec1abf0eb5cc27c70062ab2006b55b Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 11 Nov 2025 14:53:29 +0100 Subject: [PATCH 074/124] Change device to deviceId & deviceNode in objinfo [no ci] --- include/depthai/pipeline/NodeObjInfo.hpp | 5 +++-- src/pipeline/Pipeline.cpp | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/include/depthai/pipeline/NodeObjInfo.hpp b/include/depthai/pipeline/NodeObjInfo.hpp index c786065dd..8b4f93184 100644 --- a/include/depthai/pipeline/NodeObjInfo.hpp +++ b/include/depthai/pipeline/NodeObjInfo.hpp @@ -15,7 +15,8 @@ struct NodeObjInfo { std::string name; std::string alias; - std::string device; + std::string deviceId; + bool deviceNode = true; std::vector properties; @@ -28,6 +29,6 @@ struct NodeObjInfo { std::unordered_map, NodeIoInfo, IoInfoKey> ioInfo; }; -DEPTHAI_SERIALIZE_EXT(NodeObjInfo, id, parentId, name, alias, device, properties, logLevel, ioInfo); +DEPTHAI_SERIALIZE_EXT(NodeObjInfo, id, parentId, name, alias, deviceId, deviceNode, properties, logLevel, ioInfo); } // namespace dai diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 6f1a2dd50..feef76524 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -209,7 +209,9 @@ PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type, bool incl info.name = node->getName(); info.alias = node->getAlias(); info.parentId = node->parentId; - info.device = node->runOnHost() ? "host" : "device"; + info.deviceNode = !node->runOnHost(); + if(!node->runOnHost()) info.deviceId = defaultDeviceId; + const auto& deviceNode = std::dynamic_pointer_cast(node); if(!node->runOnHost() && !deviceNode) { throw std::invalid_argument(fmt::format("Node '{}' should subclass DeviceNode or have hostNode == true", info.name)); @@ -342,7 +344,7 @@ PipelineSchema PipelineImpl::getDevicePipelineSchema(SerializationType type, boo schema.bridges.clear(); // Remove host nodes for(auto it = schema.nodes.begin(); it != schema.nodes.end();) { - if(it->second.device != "device") { + if(!it->second.deviceNode) { it = schema.nodes.erase(it); } else { ++it; From d32da8a29cc3c67606fab35791e03b00806fc6d3 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 11 Nov 2025 14:55:47 +0100 Subject: [PATCH 075/124] RVC4 FW: update core [no ci] --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 4595a729e..adb9ebb85 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+2e622cc6b938b292e35d442bbfcb24ef0ac147ab") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+88ad05a75c12d1ab35183d86a0de4c19a6b3097b") From 8589893f9b567e452103f3a178c400bd8de0f944 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 11 Nov 2025 14:59:14 +0100 Subject: [PATCH 076/124] RVC2 FW: update core --- cmake/Depthai/DepthaiDeviceSideConfig.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceSideConfig.cmake b/cmake/Depthai/DepthaiDeviceSideConfig.cmake index 688044c9f..2946c6c90 100644 --- a/cmake/Depthai/DepthaiDeviceSideConfig.cmake +++ b/cmake/Depthai/DepthaiDeviceSideConfig.cmake @@ -2,7 +2,7 @@ set(DEPTHAI_DEVICE_SIDE_MATURITY "snapshot") # "full commit hash of device side binary" -set(DEPTHAI_DEVICE_SIDE_COMMIT "90b5b48739ee78ba1b9c08791e3845d6d4d2408b") +set(DEPTHAI_DEVICE_SIDE_COMMIT "0a06b16e5f3a68be9815547c316317310c35e968") # "version if applicable" set(DEPTHAI_DEVICE_SIDE_VERSION "") From 568ad646491bca841df3471760b6e239b6a1ceb7 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 11 Nov 2025 16:25:08 +0100 Subject: [PATCH 077/124] PR fixes --- include/depthai/pipeline/Pipeline.hpp | 7 +- src/pipeline/Pipeline.cpp | 106 +++++++++++++------------- 2 files changed, 59 insertions(+), 54 deletions(-) diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index f29698e18..cc9007e12 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -123,7 +123,8 @@ class PipelineImpl : public std::enable_shared_from_this { std::unordered_map recordReplayFilenames; bool removeRecordReplayFiles = true; std::string defaultDeviceId; - bool pipelineOnHost = true; + // Is the pipeline building on host? Some steps should be skipped when building on device + bool buildingOnHost = true; // Pipeline events bool enablePipelineDebugging = false; @@ -239,6 +240,8 @@ class PipelineImpl : public std::enable_shared_from_this { void stop(); void run(); + void setupPipelineDebugging(); + // Reset connections void resetConnections(); void disconnectXLinkHosts(); @@ -498,7 +501,7 @@ class Pipeline { impl()->build(); } void buildDevice() { - impl()->pipelineOnHost = false; + impl()->buildingOnHost = false; impl()->build(); } void start() { diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index feef76524..6f3ec1ad6 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -584,7 +584,7 @@ void PipelineImpl::build() { // TODO(themarpe) - add mutex and set running up ahead if(isBuild) return; - if(pipelineOnHost) { + if(buildingOnHost) { if(defaultDevice) { auto recordPath = std::filesystem::path(utility::getEnvAs("DEPTHAI_RECORD", "")); auto replayPath = std::filesystem::path(utility::getEnvAs("DEPTHAI_REPLAY", "")); @@ -679,57 +679,7 @@ void PipelineImpl::build() { node->buildStage1(); } - if(pipelineOnHost) { - // Create pipeline event aggregator node and link - enablePipelineDebugging = enablePipelineDebugging || utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false); - if(enablePipelineDebugging) { - // Check if any nodes are on host or device - bool hasHostNodes = false; - bool hasDeviceNodes = false; - for(const auto& node : getAllNodes()) { - if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; - - if(node->runOnHost()) { - hasHostNodes = true; - } else { - hasDeviceNodes = true; - } - } - std::shared_ptr hostEventAgg = nullptr; - std::shared_ptr deviceEventAgg = nullptr; - if(hasHostNodes) { - hostEventAgg = parent.create(); - hostEventAgg->setRunOnHost(true); - } - if(hasDeviceNodes) { - deviceEventAgg = parent.create(); - deviceEventAgg->setRunOnHost(false); - } - for(auto& node : getAllNodes()) { - if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; - - auto threadedNode = std::dynamic_pointer_cast(node); - if(threadedNode) { - if(node->runOnHost() && hostEventAgg && node->id != hostEventAgg->id) { - threadedNode->pipelineEventOutput.link(hostEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); - } else if(!node->runOnHost() && deviceEventAgg && node->id != deviceEventAgg->id) { - threadedNode->pipelineEventOutput.link(deviceEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); - } - } - } - auto stateMerge = parent.create()->build(hasDeviceNodes, hasHostNodes); - if(deviceEventAgg) { - deviceEventAgg->out.link(stateMerge->inputDevice); - stateMerge->outRequest.link(deviceEventAgg->request); - } - if(hostEventAgg) { - hostEventAgg->out.link(stateMerge->inputHost); - stateMerge->outRequest.link(hostEventAgg->request); - } - pipelineStateOut = stateMerge->out.createOutputQueue(1, false); - pipelineStateRequest = stateMerge->request.createInputQueue(); - } - } + if(buildingOnHost) setupPipelineDebugging(); { auto allNodes = getAllNodes(); @@ -1121,6 +1071,58 @@ std::vector PipelineImpl::loadResourceCwd(fs::path uri, fs::path cwd, b throw std::invalid_argument(fmt::format("No handler specified for following ({}) URI", uri)); } +void PipelineImpl::setupPipelineDebugging() { + // Create pipeline event aggregator node and link + enablePipelineDebugging = enablePipelineDebugging || utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false); + if(enablePipelineDebugging) { + // Check if any nodes are on host or device + bool hasHostNodes = false; + bool hasDeviceNodes = false; + for(const auto& node : getAllNodes()) { + if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; + + if(node->runOnHost()) { + hasHostNodes = true; + } else { + hasDeviceNodes = true; + } + } + std::shared_ptr hostEventAgg = nullptr; + std::shared_ptr deviceEventAgg = nullptr; + if(hasHostNodes) { + hostEventAgg = parent.create(); + hostEventAgg->setRunOnHost(true); + } + if(hasDeviceNodes) { + deviceEventAgg = parent.create(); + deviceEventAgg->setRunOnHost(false); + } + for(auto& node : getAllNodes()) { + if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; + + auto threadedNode = std::dynamic_pointer_cast(node); + if(threadedNode) { + if(node->runOnHost() && hostEventAgg && node->id != hostEventAgg->id) { + threadedNode->pipelineEventOutput.link(hostEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); + } else if(!node->runOnHost() && deviceEventAgg && node->id != deviceEventAgg->id) { + threadedNode->pipelineEventOutput.link(deviceEventAgg->inputs[fmt::format("{} - {}", node->getName(), node->id)]); + } + } + } + auto stateMerge = parent.create()->build(hasDeviceNodes, hasHostNodes); + if(deviceEventAgg) { + deviceEventAgg->out.link(stateMerge->inputDevice); + stateMerge->outRequest.link(deviceEventAgg->request); + } + if(hostEventAgg) { + hostEventAgg->out.link(stateMerge->inputHost); + stateMerge->outRequest.link(hostEventAgg->request); + } + pipelineStateOut = stateMerge->out.createOutputQueue(1, false); + pipelineStateRequest = stateMerge->request.createInputQueue(); + } +} + // Record and Replay void Pipeline::enableHolisticRecord(const RecordConfig& config) { if(this->isRunning()) { From d0087cfa7ad12d02044138f0614fba9511e15f6d Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 12 Nov 2025 09:31:38 +0100 Subject: [PATCH 078/124] Remove pipeline debugging env var from tests --- tests/run_tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/run_tests.py b/tests/run_tests.py index dbb1d0090..9a4d291b6 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -45,7 +45,6 @@ def run(self): # Function to run ctest with specific environment variables and labels def run_ctest(env_vars, labels, blocking=True, name=""): env = os.environ.copy() - env_vars["DEPTHAI_PIPELINE_DEBUGGING"] = "1" env.update(env_vars) cmd = ["ctest", "--no-tests=error", "-VV", "-L", "^ci$", "--timeout", "1000", "-C", "Release"] From 2d0fc90ace6bf113cc593e9ac9d82137b861a7f8 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 12 Nov 2025 10:24:09 +0100 Subject: [PATCH 079/124] Add example for pipeline event subscription --- .../python/src/pipeline/node/NodeBindings.cpp | 3 +- .../cpp/Misc/PipelineDebugging/CMakeLists.txt | 1 + .../PipelineDebugging/get_pipeline_state.cpp | 2 +- .../node_pipeline_events.cpp | 64 +++++++++++++++++++ examples/python/CMakeLists.txt | 3 + .../PipelineDebugging/node_pipeline_events.py | 45 +++++++++++++ include/depthai/pipeline/ThreadedNode.hpp | 5 +- 7 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 examples/cpp/Misc/PipelineDebugging/node_pipeline_events.cpp create mode 100644 examples/python/Misc/PipelineDebugging/node_pipeline_events.py diff --git a/bindings/python/src/pipeline/node/NodeBindings.cpp b/bindings/python/src/pipeline/node/NodeBindings.cpp index 715c689d0..ca2d187ca 100644 --- a/bindings/python/src/pipeline/node/NodeBindings.cpp +++ b/bindings/python/src/pipeline/node/NodeBindings.cpp @@ -449,5 +449,6 @@ void NodeBindings::bind(pybind11::module& m, void* pCallstack) { .def("isRunning", &ThreadedNode::isRunning, DOC(dai, ThreadedNode, isRunning)) .def("mainLoop", &ThreadedNode::mainLoop, DOC(dai, ThreadedNode, mainLoop)) .def("setLogLevel", &ThreadedNode::setLogLevel, DOC(dai, ThreadedNode, setLogLevel)) - .def("getLogLevel", &ThreadedNode::getLogLevel, DOC(dai, ThreadedNode, getLogLevel)); + .def("getLogLevel", &ThreadedNode::getLogLevel, DOC(dai, ThreadedNode, getLogLevel)) + .def_readonly("pipelineEventOutput", &ThreadedNode::pipelineEventOutput, DOC(dai, ThreadedNode, pipelineEventOutput)); } diff --git a/examples/cpp/Misc/PipelineDebugging/CMakeLists.txt b/examples/cpp/Misc/PipelineDebugging/CMakeLists.txt index 94e0c228b..99af9ea6c 100644 --- a/examples/cpp/Misc/PipelineDebugging/CMakeLists.txt +++ b/examples/cpp/Misc/PipelineDebugging/CMakeLists.txt @@ -5,3 +5,4 @@ cmake_minimum_required(VERSION 3.10) ## function: dai_set_example_test_labels(example_name ...) dai_add_example(get_pipeline_state get_pipeline_state.cpp ON OFF) +dai_add_example(node_pipeline_events node_pipeline_events.cpp ON OFF) diff --git a/examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp b/examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp index 5d001fb1b..29f0df73f 100644 --- a/examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp +++ b/examples/cpp/Misc/PipelineDebugging/get_pipeline_state.cpp @@ -26,7 +26,7 @@ int main() { double maxDisparity = 1.0; pipeline.start(); - while(true) { + while(pipeline.isRunning()) { auto disparity = disparityQueue->get(); cv::Mat npDisparity = disparity->getFrame(); diff --git a/examples/cpp/Misc/PipelineDebugging/node_pipeline_events.cpp b/examples/cpp/Misc/PipelineDebugging/node_pipeline_events.cpp new file mode 100644 index 000000000..a4ca7c5c7 --- /dev/null +++ b/examples/cpp/Misc/PipelineDebugging/node_pipeline_events.cpp @@ -0,0 +1,64 @@ +#include +#include + +#include "depthai/depthai.hpp" +#include "fmt/base.h" + +int main() { + dai::Pipeline pipeline; + pipeline.enablePipelineDebugging(); + + auto monoLeft = pipeline.create()->build(dai::CameraBoardSocket::CAM_B); + auto monoRight = pipeline.create()->build(dai::CameraBoardSocket::CAM_C); + + auto stereo = pipeline.create(); + + auto monoLeftOut = monoLeft->requestFullResolutionOutput(); + auto monoRightOut = monoRight->requestFullResolutionOutput(); + + monoLeftOut->link(stereo->left); + monoRightOut->link(stereo->right); + + stereo->setRectification(true); + stereo->setExtendedDisparity(true); + stereo->setLeftRightCheck(true); + + auto disparityQueue = stereo->disparity.createOutputQueue(); + auto monoLeftEventQueue = monoLeft->pipelineEventOutput.createOutputQueue(1, false); + + double maxDisparity = 1.0; + pipeline.start(); + while(pipeline.isRunning()) { + auto disparity = disparityQueue->get(); + auto latestNodeEvent = monoLeftEventQueue->tryGet(); + + cv::Mat npDisparity = disparity->getFrame(); + + double minVal, curMax; + cv::minMaxLoc(npDisparity, &minVal, &curMax); + maxDisparity = std::max(maxDisparity, curMax); + + // Normalize the disparity image to an 8-bit scale. + cv::Mat normalized; + npDisparity.convertTo(normalized, CV_8UC1, 255.0 / maxDisparity); + + cv::Mat colorizedDisparity; + cv::applyColorMap(normalized, colorizedDisparity, cv::COLORMAP_JET); + + // Set pixels with zero disparity to black. + colorizedDisparity.setTo(cv::Scalar(0, 0, 0), normalized == 0); + + cv::imshow("disparity", colorizedDisparity); + + fmt::println("Latest event from MonoLeft camera node: {}", latestNodeEvent ? latestNodeEvent->str() : "No event"); + + int key = cv::waitKey(1); + if(key == 'q') { + break; + } + } + + pipeline.stop(); + + return 0; +} diff --git a/examples/python/CMakeLists.txt b/examples/python/CMakeLists.txt index d9d5b5697..9ab6e6745 100644 --- a/examples/python/CMakeLists.txt +++ b/examples/python/CMakeLists.txt @@ -221,6 +221,9 @@ dai_set_example_test_labels(reconnect_callback ondevice rvc2_all rvc4 rvc4rgb ci add_python_example(get_pipeline_state Misc/PipelineDebugging/get_pipeline_state.py) dai_set_example_test_labels(get_pipeline_state ondevice rvc2_all rvc4 rvc4rgb ci) +add_python_example(node_pipeline_events Misc/PipelineDebugging/node_pipeline_events.py) +dai_set_example_test_labels(node_pipeline_events ondevice rvc2_all rvc4 rvc4rgb ci) + ## SystemLogger add_python_example(system_logger RVC2/SystemLogger/system_information.py) dai_set_example_test_labels(system_logger ondevice rvc2_all ci) diff --git a/examples/python/Misc/PipelineDebugging/node_pipeline_events.py b/examples/python/Misc/PipelineDebugging/node_pipeline_events.py new file mode 100644 index 000000000..bd896fdef --- /dev/null +++ b/examples/python/Misc/PipelineDebugging/node_pipeline_events.py @@ -0,0 +1,45 @@ +import depthai as dai +import numpy as np +import cv2 + +with dai.Pipeline() as pipeline: + # Pipeline debugging is disabled by default. + # You can also enable it by setting the DEPTHAI_PIPELINE_DEBUGGING environment variable to '1' + pipeline.enablePipelineDebugging(True) + + monoLeft = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_B) + monoRight = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_C) + stereo = pipeline.create(dai.node.StereoDepth) + + # Linking + monoLeftOut = monoLeft.requestFullResolutionOutput() + monoRightOut = monoRight.requestFullResolutionOutput() + monoLeftOut.link(stereo.left) + monoRightOut.link(stereo.right) + + stereo.setRectification(True) + stereo.setExtendedDisparity(True) + stereo.setLeftRightCheck(True) + + disparityQueue = stereo.disparity.createOutputQueue() + monoLeftEventQueue = monoLeft.pipelineEventOutput.createOutputQueue() + + colorMap = cv2.applyColorMap(np.arange(256, dtype=np.uint8), cv2.COLORMAP_JET) + colorMap[0] = [0, 0, 0] # to make zero-disparity pixels black + + with pipeline: + pipeline.start() + maxDisparity = 1 + while pipeline.isRunning(): + disparity = disparityQueue.get() + latestEvent = monoLeftEventQueue.tryGet() + assert isinstance(disparity, dai.ImgFrame) + npDisparity = disparity.getFrame() + maxDisparity = max(maxDisparity, np.max(npDisparity)) + colorizedDisparity = cv2.applyColorMap(((npDisparity / maxDisparity) * 255).astype(np.uint8), colorMap) + cv2.imshow("disparity", colorizedDisparity) + print(f"Latest event from MonoLeft camera node: {latestEvent if latestEvent is not None else 'No event'}") + key = cv2.waitKey(1) + if key == ord('q'): + pipeline.stop() + break diff --git a/include/depthai/pipeline/ThreadedNode.hpp b/include/depthai/pipeline/ThreadedNode.hpp index 854f38005..09c95873d 100644 --- a/include/depthai/pipeline/ThreadedNode.hpp +++ b/include/depthai/pipeline/ThreadedNode.hpp @@ -16,12 +16,13 @@ class ThreadedNode : public Node { AtomicBool running{false}; protected: - Output pipelineEventOutput{*this, {"pipelineEventOutput", DEFAULT_GROUP, {{{DatatypeEnum::PipelineEvent, false}}}}}; - void initPipelineEventDispatcher(int64_t nodeId); public: + Output pipelineEventOutput{*this, {"pipelineEventOutput", DEFAULT_GROUP, {{{DatatypeEnum::PipelineEvent, false}}}}}; + using Node::Node; + ThreadedNode(); virtual ~ThreadedNode(); From 560d764510c4ca0e371a7aa5eb82f285015783f7 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 12 Nov 2025 10:26:23 +0100 Subject: [PATCH 080/124] Clangformat --- src/remote_connection/RemoteConnectionImpl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/remote_connection/RemoteConnectionImpl.cpp b/src/remote_connection/RemoteConnectionImpl.cpp index bb5f545aa..08b329cce 100644 --- a/src/remote_connection/RemoteConnectionImpl.cpp +++ b/src/remote_connection/RemoteConnectionImpl.cpp @@ -121,7 +121,7 @@ bool RemoteConnectionImpl::initWebsocketServer(const std::string& address, uint1 // Server options foxglove::ServerOptions serverOptions; - serverOptions.sendBufferPriorityLimitMessages = {{0, 3}, {1, 5}}; // 3 messages for low priority, 5 messages for high priority + serverOptions.sendBufferPriorityLimitMessages = {{0, 3}, {1, 5}}; // 3 messages for low priority, 5 messages for high priority serverOptions.messageDropPolicy = foxglove::MessageDropPolicy::MAX_MESSAGE_COUNT; serverOptions.capabilities.emplace_back("services"); serverOptions.supportedEncodings.emplace_back("json"); From 2c80e9254a75baf402f42a55ed37e73ac36ced48 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 12 Nov 2025 11:55:10 +0100 Subject: [PATCH 081/124] Macos build fix v2? --- include/depthai/pipeline/datatype/PipelineEvent.hpp | 2 +- include/depthai/pipeline/datatype/PipelineState.hpp | 2 +- src/pipeline/datatype/PipelineEvent.cpp | 4 +++- src/pipeline/datatype/PipelineState.cpp | 3 +++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/include/depthai/pipeline/datatype/PipelineEvent.hpp b/include/depthai/pipeline/datatype/PipelineEvent.hpp index 5aa601ee2..69168f317 100644 --- a/include/depthai/pipeline/datatype/PipelineEvent.hpp +++ b/include/depthai/pipeline/datatype/PipelineEvent.hpp @@ -23,7 +23,7 @@ class PipelineEvent : public Buffer { enum class Interval : std::int32_t { NONE = 0, START = 1, END = 2 }; PipelineEvent() = default; - virtual ~PipelineEvent() = default; + virtual ~PipelineEvent(); int64_t nodeId = -1; int32_t status = 0; diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index d824b978e..e1336eee5 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -102,7 +102,7 @@ class NodeState { class PipelineState : public Buffer { public: PipelineState() = default; - virtual ~PipelineState() = default; + virtual ~PipelineState(); std::unordered_map nodeStates; uint32_t configSequenceNum = 0; diff --git a/src/pipeline/datatype/PipelineEvent.cpp b/src/pipeline/datatype/PipelineEvent.cpp index 4bcb49b78..9c1b46fad 100644 --- a/src/pipeline/datatype/PipelineEvent.cpp +++ b/src/pipeline/datatype/PipelineEvent.cpp @@ -1,3 +1,5 @@ #include "depthai/pipeline/datatype/PipelineEvent.hpp" -namespace dai {} // namespace dai +namespace dai { +PipelineEvent::~PipelineEvent() = default; +} // namespace dai diff --git a/src/pipeline/datatype/PipelineState.cpp b/src/pipeline/datatype/PipelineState.cpp index d683a1356..7d3ae7146 100644 --- a/src/pipeline/datatype/PipelineState.cpp +++ b/src/pipeline/datatype/PipelineState.cpp @@ -1,6 +1,9 @@ #include "depthai/pipeline/datatype/PipelineState.hpp" namespace dai { + +PipelineState::~PipelineState() = default; + nlohmann::json PipelineState::toJson() const { nlohmann::json j; j["nodeStates"] = nodeStates; From c4ba9a257c489981e659043f1e51bdc253398e3f Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 12 Nov 2025 13:49:45 +0100 Subject: [PATCH 082/124] Fix windows build of example --- examples/cpp/Misc/PipelineDebugging/node_pipeline_events.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/cpp/Misc/PipelineDebugging/node_pipeline_events.cpp b/examples/cpp/Misc/PipelineDebugging/node_pipeline_events.cpp index a4ca7c5c7..e19dee459 100644 --- a/examples/cpp/Misc/PipelineDebugging/node_pipeline_events.cpp +++ b/examples/cpp/Misc/PipelineDebugging/node_pipeline_events.cpp @@ -2,7 +2,6 @@ #include #include "depthai/depthai.hpp" -#include "fmt/base.h" int main() { dai::Pipeline pipeline; @@ -50,7 +49,7 @@ int main() { cv::imshow("disparity", colorizedDisparity); - fmt::println("Latest event from MonoLeft camera node: {}", latestNodeEvent ? latestNodeEvent->str() : "No event"); + std::cout << "Latest event from MonoLeft camera node: " << (latestNodeEvent ? latestNodeEvent->str() : "No event"); int key = cv::waitKey(1); if(key == 'q') { From bcbf58420ab5aa55a10a26b99de39be18a9e461a Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 12 Nov 2025 17:07:08 +0100 Subject: [PATCH 083/124] MacOs build fix v3 [no ci] --- .../internal/PipelineEventAggregationProperties.hpp | 2 ++ src/properties/Properties.cpp | 4 +++- tests/run_tests.py | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp index 4804f6cad..9b6328fe7 100644 --- a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp +++ b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp @@ -11,6 +11,8 @@ struct PipelineEventAggregationProperties : PropertiesSerializable Date: Wed, 12 Nov 2025 17:09:29 +0100 Subject: [PATCH 084/124] RVC4 FW: merge develop [no ci] --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index adb9ebb85..448859a70 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+88ad05a75c12d1ab35183d86a0de4c19a6b3097b") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+4588ad61baadf53309b02dba26435245fbdae908") From 201bbd315b28f9a3bf1e36122d97452028ae57d6 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 12 Nov 2025 17:13:14 +0100 Subject: [PATCH 085/124] RVC2 FW: merge develop --- cmake/Depthai/DepthaiDeviceSideConfig.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceSideConfig.cmake b/cmake/Depthai/DepthaiDeviceSideConfig.cmake index 2946c6c90..576372e8d 100644 --- a/cmake/Depthai/DepthaiDeviceSideConfig.cmake +++ b/cmake/Depthai/DepthaiDeviceSideConfig.cmake @@ -2,7 +2,7 @@ set(DEPTHAI_DEVICE_SIDE_MATURITY "snapshot") # "full commit hash of device side binary" -set(DEPTHAI_DEVICE_SIDE_COMMIT "0a06b16e5f3a68be9815547c316317310c35e968") +set(DEPTHAI_DEVICE_SIDE_COMMIT "e46193612333c5f8d036ec6dd87f35e8bf2d6240") # "version if applicable" set(DEPTHAI_DEVICE_SIDE_VERSION "") From c04ed85ff6702026346ae0febea8b25df7f43313 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 13 Nov 2025 07:21:32 +0100 Subject: [PATCH 086/124] Fix include [no ci] --- src/properties/Properties.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/properties/Properties.cpp b/src/properties/Properties.cpp index a3d151fef..da46bf209 100644 --- a/src/properties/Properties.cpp +++ b/src/properties/Properties.cpp @@ -32,9 +32,9 @@ #include "depthai/properties/UVCProperties.hpp" #include "depthai/properties/VideoEncoderProperties.hpp" #include "depthai/properties/WarpProperties.hpp" +#include "depthai/properties/internal/PipelineEventAggregationProperties.hpp" #include "depthai/properties/internal/XLinkInProperties.hpp" #include "depthai/properties/internal/XLinkOutProperties.hpp" -#include "properties/internal/PipelineEventAggregationProperties.hpp" // RVC2_FW does not need these properties #ifndef RVC2_FW From 7637f2e07dd9c5d4a6f8eda23279c53d8bade357 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 13 Nov 2025 07:22:32 +0100 Subject: [PATCH 087/124] RVC2 FW: Update core [no ci] --- cmake/Depthai/DepthaiDeviceSideConfig.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceSideConfig.cmake b/cmake/Depthai/DepthaiDeviceSideConfig.cmake index 576372e8d..b740a24ea 100644 --- a/cmake/Depthai/DepthaiDeviceSideConfig.cmake +++ b/cmake/Depthai/DepthaiDeviceSideConfig.cmake @@ -2,7 +2,7 @@ set(DEPTHAI_DEVICE_SIDE_MATURITY "snapshot") # "full commit hash of device side binary" -set(DEPTHAI_DEVICE_SIDE_COMMIT "e46193612333c5f8d036ec6dd87f35e8bf2d6240") +set(DEPTHAI_DEVICE_SIDE_COMMIT "da6d60a9395e28df10c53a8868ea71f73fc7697b") # "version if applicable" set(DEPTHAI_DEVICE_SIDE_VERSION "") From f3eeb97ffa10033e57778d0cc990722f3fce21fa Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 13 Nov 2025 11:57:13 +0100 Subject: [PATCH 088/124] Added pipeline debugging docs --- PipelineDebugging.md | 212 ++++++++++++++++++ .../python/src/pipeline/PipelineBindings.cpp | 10 +- images/pipeline_debugging_graph.png | Bin 0 -> 102870 bytes include/depthai/pipeline/PipelineStateApi.hpp | 4 +- src/pipeline/PipelineStateApi.cpp | 2 +- 5 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 PipelineDebugging.md create mode 100644 images/pipeline_debugging_graph.png diff --git a/PipelineDebugging.md b/PipelineDebugging.md new file mode 100644 index 000000000..3c2ccbf47 --- /dev/null +++ b/PipelineDebugging.md @@ -0,0 +1,212 @@ +# Pipeline Debugging + +## Notes + +- Pipeline debugging on RVC2 is very limited compared to RVC4. +- States are calculated for individual nodes, not for node groups (e.g. instead of the `DetectionNetwork` state, `NeuralNetwork` and `DetectionParser` states are calculated). + +## API + +The pipeline state can be retrieved from the pipeline object by calling the `getPipelineState()` method which exposes methods for selecting which data to retrieve: + +```python +pipelineState = pipeline.getPipelineState().nodes().detailed() +``` + +The method `nodes()` takes an optional argument to select which nodes to include in the state by their IDs. If no argument or multiple IDs are provided, the following methods can be used to select the desired data: + +- `summary() -> PipelineState`: returns data pertaining the main loop and I\O blocks only. Statistics for individual inputs, outputs and other timings are not included. +- `detailed() -> PipelineState`: returns all data including statistics for individual inputs, outputs and other timings. +- `outputs() -> map(NodeId, map(str, OutputQueueState))`: returns state of all outputs per node. +- `inputs() -> map(NodeId, map(str, InputQueueState))`: returns state of all inputs per node. +- `otherTimings() -> map(NodeId, map(str, Timing))`: returns other timing statistics per node. + +Specifying a single node ID to the `nodes()` method allows further filtering of the data: + +- `summary() -> NodeState`: returns summary data for the specified node similarly to above. +- `detailed() -> NodeState`: returns detailed data for the specified node similarly to above. +- `outputs() -> map(str, OutputQueueState)`: returns states of all or specific outputs of the node. If only one output name is provided, the return type is `OutputQueueState`. +- `inputs() -> map(str, InputQueueState)`: returns states of all or specific inputs of the node. If only one input name is provided, the return type is `InputQueueState`. +- `otherTimings() -> map(str, Timing)`: returns other timing statistics of the node. If only one timing name is provided, the return type is `Timing`. + +## Operation Overview + +### Schema + +![Pipeline Debugging Graph](./images/pipeline_debugging_graph.png) + +### Description + +Each node in the pipeline has a `pipelineEventOutput` output that emits `PipelineEvent` events related to the node's operation. These events are created and sent using the `PipelineEventDispatcher` object in each node. The event output is linked to one of the `PipelineEventAggregation` nodes, depending on where the node is running (by default events do not get sent from device to host, it is however possible to subscribe to the events of a node by simply creating an output queue). + +The `PipelineEventAggregation` node collects events from the nodes running on the same device and merges them into a `PipelineState` object by calculating duration statistics, events per second, and various states. The state is then sent to the `StateMerge` node which runs on host and merges device and host states into a single `PipelineState` object. + +## Pipeline Events + +### Class + +```cpp +class PipelineEvent : public Buffer { + public: + enum class Type : std::int32_t { + CUSTOM = 0, + LOOP = 1, + INPUT = 2, + OUTPUT = 3, + INPUT_BLOCK = 4, + OUTPUT_BLOCK = 5, + }; + enum class Interval : std::int32_t { NONE = 0, START = 1, END = 2 }; + + int64_t nodeId = -1; + int32_t status = 0; + std::optional queueSize; + Interval interval = Interval::NONE; + Type type = Type::CUSTOM; + std::string source; +}; +``` + +### Description + +`PipelineEvent` can have different types depending on what kind of event is being reported: + +- `CUSTOM` events can be defined by the node developers to report and track relevant information (e.g. timing of specific operations). These need to be manually added. +- `LOOP` events track the main processing loop timings. They can only track one loop per node. These can be generated by using the `mainLoop` method in the main `while` loop for simple nodes, or by using tracked events in more complex threaded nodes. +- `INPUT` events track input queue operations and states. These are automatically added. +- `OUTPUT` events track output queue operations and states. These are automatically added. +- `INPUT_BLOCK` events track a group of input operations. Only one input block can be tracked per node. These need to be manually added. +- `OUTPUT_BLOCK` events track a group of output operations. Only one output block can be tracked per node. These need to be manually added. + +The `PipelineEvent` also contains the source node ID, the status code used to indicate success or failure of the operation (for `tryGet` and `trySend`), the queue size (if applicable - inputs only), the source or name of the event, the timestamp, the sequence number, and the interval (start, end or none). The interval is none when the end and start events are the same (e.g. simple main loop). + +Pipeline events are generated using the `PipelineEventDispatcher` of a node. They can be created manually by using the `startEvent` and `endEvent` methods or by using the `BlockPipelineEvent` helper class (created using the `blockEvent` method) to automatically create start (in constructor) and end (in destructor) events for a block of code. + +## Pipeline State + +The pipeline state contains a map of `NodeState` objects by node ID. + +### NodeState Class + +```cpp +class NodeState { + public: + struct DurationEvent { + PipelineEvent startEvent; + uint64_t durationUs; + }; + struct TimingStats { + uint64_t minMicros = -1; + uint64_t maxMicros = 0; + uint64_t averageMicrosRecent = 0; + uint64_t stdDevMicrosRecent = 0; + uint64_t minMicrosRecent = -1; + uint64_t maxMicrosRecent = 0; + uint64_t medianMicrosRecent = 0; + }; + struct Timing { + float fps = 0.0f; + TimingStats durationStats; + }; + struct QueueStats { + uint32_t maxQueued = 0; + uint32_t minQueuedRecent = 0; + uint32_t maxQueuedRecent = 0; + uint32_t medianQueuedRecent = 0; + }; + struct InputQueueState { + enum class State : std::int32_t { + IDLE = 0, + WAITING = 1, + BLOCKED = 2 + } state = State::IDLE; + uint32_t numQueued = 0; + Timing timing; + QueueStats queueStats; + }; + struct OutputQueueState { + enum class State : std::int32_t { IDLE = 0, SENDING = 1 } state = State::IDLE; + Timing timing; + }; + enum class State : std::int32_t { IDLE = 0, GETTING_INPUTS = 1, PROCESSING = 2, SENDING_OUTPUTS = 3 }; + + State state = State::IDLE; + std::vector events; + std::unordered_map outputStates; + std::unordered_map inputStates; + Timing inputsGetTiming; + Timing outputsSendTiming; + Timing mainLoopTiming; + std::unordered_map otherTimings; +}; +``` + +### NodeState Description + +The `NodeState` class contains information about the state of the node, optional list of recent events (the number of events stored is limited), output and input queue states, timings for getting inputs, sending outputs, main loop timing, and other timings added by the node developer. + +#### Node State + +The node can be in one of the following states: + +- `IDLE`: the node is not currently processing anything. This is only possible before the node has entered its main loop. +- `GETTING_INPUTS`: the node is currently trying to get inputs (in the input block). +- `PROCESSING`: the node is currently processing data (it is not in the input or the output block). +- `SENDING_OUTPUTS`: the node is currently trying to send outputs (in the output block). + +#### Duration Event + +The `DurationEvent` merges a start and end `PipelineEvent` by storing the start event and the calculated duration in microseconds. + +#### Timing + +The `Timing` struct contains the calculated frames per second and the duration statistics in microseconds: + +- `minMicros`: minimum duration recorded. +- `maxMicros`: maximum duration recorded. +- `averageMicrosRecent`: average duration over recent events. +- `stdDevMicrosRecent`: standard deviation of duration over recent events. +- `minMicrosRecent`: minimum duration over recent events. +- `maxMicrosRecent`: maximum duration over recent events. +- `medianMicrosRecent`: median duration over recent events. + +The timing is invalid if the minimum duration is larger than the maximum duration. This struct provides an `isValid()` method to check for validity. + +#### Queue Stats + +The `QueueStats` struct contains statistics about the queue sizes: + +- `maxQueued`: maximum number of input messages queued. +- `minQueuedRecent`: minimum number of input messages queued over recent events. +- `maxQueuedRecent`: maximum number of input messages queued over recent events. +- `medianQueuedRecent`: median number of input messages queued over recent events. + +#### Input Queue State + +The `InputQueueState` struct contains the current number of queued messages, the timing information, the queue statistics and the current state of the input: + +- `IDLE`: the input is waiting to get data and there are no outputs waiting to send data. +- `WAITING`: the input is waiting for a message to arrive (the input queue is empty). +- `BLOCKED`: the input queue is full and an output is attempting to send data. + +#### Output Queue State + +The `OutputQueueState` struct contains the timing information and the current state of the output: + +- `IDLE`: the output is not currently sending data. +- `SENDING`: the output is currently sending data. If the output is blocked due to a full queue on the input the state will be `SENDING`, otherwise the send should be instantaneous and the state will return to `IDLE`. + + +## Pipeline Event Aggregation + +The `PipelineEventAggregation` node collects `PipelineEvent` events in a separate thread. To improve performance, it does not update the state for every event, but rather collects events into a buffer and processes them in batches at a fixed interval. The node can be configured to adjust the processing interval and the maximum number of stored recent events per node. + +The events are processed by their type and source in pairs (start and end). Interval events (where the interval is not `NONE`) are matched by their sequence numbers. Ping events (where the interval is `NONE`) are matched by looking for the event with with the previous sequence number. + +The state is filtered before sending according to the input config. By default, the node waits for a request before sending the state. If the `repeat` flag in the config is set to true, the state is sent at regular intervals. + +## State Merge + +The `StateMerge` node collects `PipelineState` objects from multiple `PipelineEventAggregation` nodes and merges them into a single `PipelineState` object. The merging is done by combining the node states from each device into a single map of node states - it is expected that node IDs are unique. + +The node waits for the input config and forwards it to connected `PipelineEventAggregation` nodes. The returned states are matched by the config sequence number to ensure that they correspond to the same request. diff --git a/bindings/python/src/pipeline/PipelineBindings.cpp b/bindings/python/src/pipeline/PipelineBindings.cpp index 5e65d6241..e35028a02 100644 --- a/bindings/python/src/pipeline/PipelineBindings.cpp +++ b/bindings/python/src/pipeline/PipelineBindings.cpp @@ -150,12 +150,12 @@ void PipelineBindings::bind(pybind11::module& m, void* pCallstack) { DOC(dai, NodeStateApi, otherTimings)) .def("otherTimings", static_cast (NodeStateApi::*)(const std::vector&)>(&NodeStateApi::otherTimings), - py::arg("statNames"), + py::arg("timingNames"), DOC(dai, NodeStateApi, otherTimings, 2)) - .def("otherStats", - static_cast(&NodeStateApi::otherStats), - py::arg("statName"), - DOC(dai, NodeStateApi, otherStats)); + .def("otherTimings", + static_cast(&NodeStateApi::otherTimings), + py::arg("timingName"), + DOC(dai, NodeStateApi, otherTimings)); // bind pipeline pipeline diff --git a/images/pipeline_debugging_graph.png b/images/pipeline_debugging_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..a1d34c7447243aed1abe50f1f95ad44c6ee9f752 GIT binary patch literal 102870 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 UTvNp#Uj@my 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 literal 0 HcmV?d00001 diff --git a/include/depthai/pipeline/PipelineStateApi.hpp b/include/depthai/pipeline/PipelineStateApi.hpp index 90f0bd98e..d04460a59 100644 --- a/include/depthai/pipeline/PipelineStateApi.hpp +++ b/include/depthai/pipeline/PipelineStateApi.hpp @@ -65,8 +65,8 @@ class NodeStateApi { std::vector events(); std::unordered_map inputs(const std::vector& inputNames); NodeState::InputQueueState inputs(const std::string& inputName); - std::unordered_map otherTimings(const std::vector& statNames); - NodeState::Timing otherStats(const std::string& statName); + std::unordered_map otherTimings(const std::vector& timingNames); + NodeState::Timing otherTimings(const std::string& timingName); }; class PipelineStateApi { std::shared_ptr pipelineStateOut; diff --git a/src/pipeline/PipelineStateApi.cpp b/src/pipeline/PipelineStateApi.cpp index 35beb756d..506d4d0b9 100644 --- a/src/pipeline/PipelineStateApi.cpp +++ b/src/pipeline/PipelineStateApi.cpp @@ -244,7 +244,7 @@ std::unordered_map NodeStateApi::otherTimings(co } return result; } -NodeState::Timing NodeStateApi::otherStats(const std::string& statName) { +NodeState::Timing NodeStateApi::otherTimings(const std::string& statName) { PipelineEventAggregationConfig cfg; cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); From 3a14630b4c2e9c4024297e06a94727a708d2fa94 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 13 Nov 2025 12:00:12 +0100 Subject: [PATCH 089/124] Move section in PD docs --- PipelineDebugging.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/PipelineDebugging.md b/PipelineDebugging.md index 3c2ccbf47..899e88683 100644 --- a/PipelineDebugging.md +++ b/PipelineDebugging.md @@ -29,18 +29,6 @@ Specifying a single node ID to the `nodes()` method allows further filtering of - `inputs() -> map(str, InputQueueState)`: returns states of all or specific inputs of the node. If only one input name is provided, the return type is `InputQueueState`. - `otherTimings() -> map(str, Timing)`: returns other timing statistics of the node. If only one timing name is provided, the return type is `Timing`. -## Operation Overview - -### Schema - -![Pipeline Debugging Graph](./images/pipeline_debugging_graph.png) - -### Description - -Each node in the pipeline has a `pipelineEventOutput` output that emits `PipelineEvent` events related to the node's operation. These events are created and sent using the `PipelineEventDispatcher` object in each node. The event output is linked to one of the `PipelineEventAggregation` nodes, depending on where the node is running (by default events do not get sent from device to host, it is however possible to subscribe to the events of a node by simply creating an output queue). - -The `PipelineEventAggregation` node collects events from the nodes running on the same device and merges them into a `PipelineState` object by calculating duration statistics, events per second, and various states. The state is then sent to the `StateMerge` node which runs on host and merges device and host states into a single `PipelineState` object. - ## Pipeline Events ### Class @@ -196,6 +184,17 @@ The `OutputQueueState` struct contains the timing information and the current st - `IDLE`: the output is not currently sending data. - `SENDING`: the output is currently sending data. If the output is blocked due to a full queue on the input the state will be `SENDING`, otherwise the send should be instantaneous and the state will return to `IDLE`. +## Operation Overview + +### Schema + +![Pipeline Debugging Graph](./images/pipeline_debugging_graph.png) + +### Description + +Each node in the pipeline has a `pipelineEventOutput` output that emits `PipelineEvent` events related to the node's operation. These events are created and sent using the `PipelineEventDispatcher` object in each node. The event output is linked to one of the `PipelineEventAggregation` nodes, depending on where the node is running (by default events do not get sent from device to host, it is however possible to subscribe to the events of a node by simply creating an output queue). + +The `PipelineEventAggregation` node collects events from the nodes running on the same device and merges them into a `PipelineState` object by calculating duration statistics, events per second, and various states. The state is then sent to the `StateMerge` node which runs on host and merges device and host states into a single `PipelineState` object. ## Pipeline Event Aggregation From b71c87f33c6c0db597c6c8a8f98eae28cee71b5d Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 14 Nov 2025 08:57:33 +0100 Subject: [PATCH 090/124] Add background to PD doc schema --- images/pipeline_debugging_graph.png | Bin 102870 -> 98684 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/images/pipeline_debugging_graph.png b/images/pipeline_debugging_graph.png index a1d34c7447243aed1abe50f1f95ad44c6ee9f752..e4340cd99de6af7b63526bf19bb8a4d09bc2fd69 100644 GIT binary patch literal 98684 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%0A$k%S+(JLl`wMe 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%0DT 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 UTvNp#Uj@my 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 From cd4b338e20707548363bd1cd16ee1f1df7ac742f Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 14 Nov 2025 09:00:24 +0100 Subject: [PATCH 091/124] RVC2 FW: Update core --- cmake/Depthai/DepthaiDeviceSideConfig.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceSideConfig.cmake b/cmake/Depthai/DepthaiDeviceSideConfig.cmake index b740a24ea..53b9cfa8c 100644 --- a/cmake/Depthai/DepthaiDeviceSideConfig.cmake +++ b/cmake/Depthai/DepthaiDeviceSideConfig.cmake @@ -2,7 +2,7 @@ set(DEPTHAI_DEVICE_SIDE_MATURITY "snapshot") # "full commit hash of device side binary" -set(DEPTHAI_DEVICE_SIDE_COMMIT "da6d60a9395e28df10c53a8868ea71f73fc7697b") +set(DEPTHAI_DEVICE_SIDE_COMMIT "daeb6c3741caf6f1416b97d61d4f495f8324b13f") # "version if applicable" set(DEPTHAI_DEVICE_SIDE_VERSION "") From 0caa3d397c6cea0dbf49fa99567d3581f849cecf Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 14 Nov 2025 09:21:49 +0100 Subject: [PATCH 092/124] RVC4 FW: Update core --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 448859a70..4a3a414b6 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+4588ad61baadf53309b02dba26435245fbdae908") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+45a553c0a8a4679a946f01e6cd7ca4c02ceba232") From e32cbdd86edb9918b156b6b8aad0e34d30cd0aed Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 17 Nov 2025 11:42:11 +0100 Subject: [PATCH 093/124] RVC4 FW: Update core [no ci] --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 4a3a414b6..57e86a4e1 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+45a553c0a8a4679a946f01e6cd7ca4c02ceba232") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+aadf4e7260ed12dc0794db20b97e4ef8d8208b02") From fd9e3af1f4d03b2026e77b28ba32cd15e9e8b3bb Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 17 Nov 2025 12:06:39 +0100 Subject: [PATCH 094/124] RVC2 FW: Merge develop --- cmake/Depthai/DepthaiDeviceSideConfig.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceSideConfig.cmake b/cmake/Depthai/DepthaiDeviceSideConfig.cmake index 53b9cfa8c..eff33b6cd 100644 --- a/cmake/Depthai/DepthaiDeviceSideConfig.cmake +++ b/cmake/Depthai/DepthaiDeviceSideConfig.cmake @@ -2,7 +2,7 @@ set(DEPTHAI_DEVICE_SIDE_MATURITY "snapshot") # "full commit hash of device side binary" -set(DEPTHAI_DEVICE_SIDE_COMMIT "daeb6c3741caf6f1416b97d61d4f495f8324b13f") +set(DEPTHAI_DEVICE_SIDE_COMMIT "5cf315c03f05c41a11a4a2747335f4f43123d277") # "version if applicable" set(DEPTHAI_DEVICE_SIDE_VERSION "") From 87847d7d2970580230db4482fcac45e47354a39b Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 17 Nov 2025 14:07:50 +0100 Subject: [PATCH 095/124] Add flexible async get for pipeline state --- include/depthai/pipeline/PipelineStateApi.hpp | 1 + src/pipeline/PipelineStateApi.cpp | 24 +++++++ .../internal/PipelineEventAggregation.cpp | 9 ++- .../pipeline_debugging_host_test.cpp | 67 +++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) diff --git a/include/depthai/pipeline/PipelineStateApi.hpp b/include/depthai/pipeline/PipelineStateApi.hpp index d04460a59..b2d1e61bc 100644 --- a/include/depthai/pipeline/PipelineStateApi.hpp +++ b/include/depthai/pipeline/PipelineStateApi.hpp @@ -91,6 +91,7 @@ class PipelineStateApi { NodeStateApi nodes(Node::Id nodeId) { return NodeStateApi(nodeId, pipelineStateOut, pipelineStateRequest); } + void stateAsync(std::function callback, std::optional config = std::nullopt); }; } // namespace dai diff --git a/src/pipeline/PipelineStateApi.cpp b/src/pipeline/PipelineStateApi.cpp index 506d4d0b9..a0e790dc5 100644 --- a/src/pipeline/PipelineStateApi.cpp +++ b/src/pipeline/PipelineStateApi.cpp @@ -267,5 +267,29 @@ NodeState::Timing NodeStateApi::otherTimings(const std::string& statName) { } return state->nodeStates[nodeId].otherTimings[statName]; } +void PipelineStateApi::stateAsync(std::function callback, std::optional config) { + PipelineEventAggregationConfig cfg; + if(config.has_value()) { + cfg = *config; + } else { + cfg.repeat = true; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + } + } + + pipelineStateRequest->send(std::make_shared(cfg)); + + pipelineStateOut->addCallback([callback](const std::shared_ptr& data) { + if(data) { + const auto state = std::dynamic_pointer_cast(data); + if(state) callback(*state); + } + }); +} } // namespace dai diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 405397d35..8ebe6e84f 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -472,6 +472,7 @@ void PipelineEventAggregation::run() { std::optional currentConfig; uint32_t sequenceNum = 0; + std::chrono::time_point lastSentTime; while(mainLoop()) { auto outState = std::make_shared(); bool gotConfig = false; @@ -537,7 +538,13 @@ void PipelineEventAggregation::run() { } } } - if(gotConfig || (currentConfig.has_value() && currentConfig->repeat && updated)) out.send(outState); + auto now = std::chrono::steady_clock::now(); + if(gotConfig + || (currentConfig.has_value() && currentConfig->repeat && updated + && (now - lastSentTime >= std::chrono::milliseconds(properties.statsUpdateIntervalMs) / 2))) { + lastSentTime = now; + out.send(outState); + } } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } diff --git a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp index defd91ade..8f92914cb 100644 --- a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp +++ b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp @@ -521,3 +521,70 @@ TEST_CASE("Try I/O test") { REQUIRE(state.nodeStates.at(tryNode->id).inputStates["input"].timing.fps == 0.f); REQUIRE(state.nodeStates.at(tryNode->id).outputStates["output"].isValid()); } + +TEST_CASE("State callback test") { + PipelineHandler ph; + ph.start(); + + // Let nodes run + for(const auto& nodeName : ph.getNodeNames()) { + ph.ping(nodeName, -1); + } + + std::this_thread::sleep_for(std::chrono::seconds(3)); + + std::mutex mtx; + int callbackCount = 0; + ph.pipeline.getPipelineState().stateAsync([&](const PipelineState& state) { + std::lock_guard lock(mtx); + callbackCount++; + for(const auto& nodeName : ph.getNodeNames()) { + auto nodeState = state.nodeStates.at(ph.getNodeId(nodeName)); + + REQUIRE(nodeState.mainLoopTiming.isValid()); + REQUIRE(nodeState.mainLoopTiming.durationStats.averageMicrosRecent == Catch::Approx(100000).margin(50000)); + REQUIRE(nodeState.mainLoopTiming.durationStats.medianMicrosRecent == Catch::Approx(100000).margin(50000)); + REQUIRE(nodeState.mainLoopTiming.durationStats.minMicrosRecent == Catch::Approx(100000).margin(10000)); + REQUIRE(nodeState.mainLoopTiming.durationStats.minMicros == Catch::Approx(100000).margin(10000)); + REQUIRE(nodeState.mainLoopTiming.durationStats.maxMicrosRecent == Catch::Approx(150000).margin(50000)); + REQUIRE(nodeState.mainLoopTiming.durationStats.maxMicros == Catch::Approx(150000).margin(50000)); + + if(nodeName.find("gen") == std::string::npos) REQUIRE(nodeState.inputsGetTiming.isValid()); + if(nodeName.find("cons") == std::string::npos) REQUIRE(nodeState.outputsSendTiming.isValid()); + for(const auto& [inputName, inputState] : nodeState.inputStates) { + if(inputName.rfind("_ping") != std::string::npos) continue; + REQUIRE(inputState.timing.isValid()); + REQUIRE(inputState.timing.fps == Catch::Approx(10.f).margin(5.f)); + REQUIRE(inputState.timing.durationStats.minMicros <= 0.1e6); + REQUIRE(inputState.timing.durationStats.maxMicros <= 0.2e6); + REQUIRE(inputState.timing.durationStats.averageMicrosRecent <= 0.2e6); + REQUIRE(inputState.timing.durationStats.minMicrosRecent <= 0.12e6); + REQUIRE(inputState.timing.durationStats.maxMicrosRecent <= 0.2e6); + REQUIRE(inputState.timing.durationStats.medianMicrosRecent <= 0.2e6); + } + for(const auto& [outputName, outputState] : nodeState.outputStates) { + if(outputName.rfind("_ack") != std::string::npos) continue; + REQUIRE(outputState.timing.isValid()); + REQUIRE(outputState.timing.fps == Catch::Approx(10.f).margin(5.f)); + REQUIRE(outputState.timing.durationStats.minMicros <= 0.01e6); + REQUIRE(outputState.timing.durationStats.maxMicros <= 0.01e6); + REQUIRE(outputState.timing.durationStats.averageMicrosRecent <= 0.01e6); + REQUIRE(outputState.timing.durationStats.minMicrosRecent <= 0.01e6); + REQUIRE(outputState.timing.durationStats.maxMicrosRecent <= 0.01e6); + REQUIRE(outputState.timing.durationStats.medianMicrosRecent <= 0.01e6); + } + for(const auto& [otherName, otherTiming] : nodeState.otherTimings) { + REQUIRE(otherTiming.isValid()); + } + } + }); + + std::this_thread::sleep_for(std::chrono::seconds(8)); + + { + std::lock_guard lock(mtx); + REQUIRE(callbackCount >= 5); // At least 3 callbacks in 5 seconds + } + + ph.stop(); +} From b56fc44832f03bf63a1a3350e5745f7393d46f0a Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 18 Nov 2025 13:00:26 +0100 Subject: [PATCH 096/124] Improve repeat state calling, add logs [no ci] --- .../datatype/PipelineEventAggregationConfig.hpp | 4 ++-- src/pipeline/Pipeline.cpp | 8 ++++++++ src/pipeline/PipelineStateApi.cpp | 14 +------------- .../node/internal/PipelineEventAggregation.cpp | 8 ++++---- src/pipeline/node/internal/PipelineStateMerge.cpp | 2 +- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp b/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp index b5dd77adc..ede06195e 100644 --- a/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp +++ b/include/depthai/pipeline/datatype/PipelineEventAggregationConfig.hpp @@ -23,14 +23,14 @@ class NodeEventAggregationConfig { class PipelineEventAggregationConfig : public Buffer { public: std::vector nodes; - bool repeat = false; // Keep sending the aggregated state without waiting for new config + std::optional repeatIntervalSeconds = std::nullopt; // Keep sending the aggregated state without waiting for new config PipelineEventAggregationConfig() = default; virtual ~PipelineEventAggregationConfig(); void serialize(std::vector& metadata, DatatypeEnum& datatype) const override; - DEPTHAI_SERIALIZE(PipelineEventAggregationConfig, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodes, repeat); + DEPTHAI_SERIALIZE(PipelineEventAggregationConfig, Buffer::ts, Buffer::tsDevice, Buffer::sequenceNum, nodes, repeatIntervalSeconds); }; } // namespace dai diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 6f3ec1ad6..0dda817c3 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -20,6 +20,7 @@ #include "utility/Logging.hpp" #include "utility/Platform.hpp" #include "utility/RecordReplayImpl.hpp" +#include "utility/Serialization.hpp" #include "utility/spdlog-fmt.hpp" // shared @@ -859,6 +860,8 @@ void PipelineImpl::start() { // Implicitly build (if not already) build(); + Logging::getInstance().logger.debug("Full schema dump: ", ((nlohmann::json)getPipelineSchema(SerializationType::JSON, false)).dump()); + // Indicate that pipeline is running running = true; @@ -875,6 +878,11 @@ void PipelineImpl::start() { const auto weak = std::weak_ptr(shared); defaultDevice->pipelinePtr = weak; } + + if(utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false)) { + getPipelineState().stateAsync( + [](const PipelineState& state) { Logging::getInstance().logger.debug("Pipeline state update: {}", ((nlohmann::json)state).dump()); }); + } } void PipelineImpl::resetConnections() { diff --git a/src/pipeline/PipelineStateApi.cpp b/src/pipeline/PipelineStateApi.cpp index a0e790dc5..b7cc3a035 100644 --- a/src/pipeline/PipelineStateApi.cpp +++ b/src/pipeline/PipelineStateApi.cpp @@ -6,7 +6,6 @@ namespace dai { PipelineState NodesStateApi::summary() { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); for(auto id : nodeIds) { NodeEventAggregationConfig nodeCfg; @@ -25,7 +24,6 @@ PipelineState NodesStateApi::summary() { } PipelineState NodesStateApi::detailed() { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); for(auto id : nodeIds) { NodeEventAggregationConfig nodeCfg; @@ -41,7 +39,6 @@ PipelineState NodesStateApi::detailed() { } std::unordered_map> NodesStateApi::outputs() { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); for(auto id : nodeIds) { NodeEventAggregationConfig nodeCfg; @@ -63,7 +60,6 @@ std::unordered_map> NodesStateApi::inputs() { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); for(auto id : nodeIds) { NodeEventAggregationConfig nodeCfg; @@ -85,7 +81,6 @@ std::unordered_map> NodesStateApi::otherTimings() { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); for(auto id : nodeIds) { NodeEventAggregationConfig nodeCfg; @@ -108,7 +103,6 @@ std::unordered_map> std::unordered_map NodeStateApi::outputs(const std::vector& outputNames) { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; @@ -132,7 +126,6 @@ std::unordered_map NodeStateApi::outpu } NodeState::OutputQueueState NodeStateApi::outputs(const std::string& outputName) { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; @@ -155,7 +148,6 @@ NodeState::OutputQueueState NodeStateApi::outputs(const std::string& outputName) } std::vector NodeStateApi::events() { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; @@ -175,7 +167,6 @@ std::vector NodeStateApi::events() { } std::unordered_map NodeStateApi::inputs(const std::vector& inputNames) { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; @@ -199,7 +190,6 @@ std::unordered_map NodeStateApi::inputs } NodeState::InputQueueState NodeStateApi::inputs(const std::string& inputName) { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; @@ -222,7 +212,6 @@ NodeState::InputQueueState NodeStateApi::inputs(const std::string& inputName) { } std::unordered_map NodeStateApi::otherTimings(const std::vector& statNames) { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; @@ -246,7 +235,6 @@ std::unordered_map NodeStateApi::otherTimings(co } NodeState::Timing NodeStateApi::otherTimings(const std::string& statName) { PipelineEventAggregationConfig cfg; - cfg.repeat = false; cfg.setTimestamp(std::chrono::steady_clock::now()); NodeEventAggregationConfig nodeCfg; nodeCfg.nodeId = nodeId; @@ -272,7 +260,7 @@ void PipelineStateApi::stateAsync(std::function call if(config.has_value()) { cfg = *config; } else { - cfg.repeat = true; + cfg.repeatIntervalSeconds = 1; cfg.setTimestamp(std::chrono::steady_clock::now()); for(auto id : nodeIds) { NodeEventAggregationConfig nodeCfg; diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 8ebe6e84f..90dbfa4f7 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -476,14 +476,14 @@ void PipelineEventAggregation::run() { while(mainLoop()) { auto outState = std::make_shared(); bool gotConfig = false; - if(!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeat) || request.has()) { + if(!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeatIntervalSeconds.has_value()) || request.has()) { auto req = request.get(); if(req != nullptr) { currentConfig = *req; gotConfig = true; } } - if(gotConfig || (currentConfig.has_value() && currentConfig->repeat)) { + if(gotConfig || (currentConfig.has_value() && currentConfig->repeatIntervalSeconds.has_value())) { bool sendEvents = false; if(currentConfig.has_value()) { for(const auto& nodeCfg : currentConfig->nodes) { @@ -540,8 +540,8 @@ void PipelineEventAggregation::run() { } auto now = std::chrono::steady_clock::now(); if(gotConfig - || (currentConfig.has_value() && currentConfig->repeat && updated - && (now - lastSentTime >= std::chrono::milliseconds(properties.statsUpdateIntervalMs) / 2))) { + || (currentConfig.has_value() && currentConfig->repeatIntervalSeconds.has_value() && updated + && (now - lastSentTime) >= std::chrono::seconds(currentConfig->repeatIntervalSeconds.value()))) { lastSentTime = now; out.send(outState); } diff --git a/src/pipeline/node/internal/PipelineStateMerge.cpp b/src/pipeline/node/internal/PipelineStateMerge.cpp index 7e65ccd88..ab330cd72 100644 --- a/src/pipeline/node/internal/PipelineStateMerge.cpp +++ b/src/pipeline/node/internal/PipelineStateMerge.cpp @@ -32,7 +32,7 @@ void PipelineStateMerge::run() { while(mainLoop()) { auto outState = std::make_shared(); bool waitForMatch = false; - if(!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeat) || request.has()) { + if(!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeatIntervalSeconds.has_value()) || request.has()) { auto req = request.get(); if(req != nullptr) { currentConfig = *req; From 83fd99ee95a917267720025045696bd927e209b5 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 18 Nov 2025 15:44:21 +0100 Subject: [PATCH 097/124] Fix pipeline schema without pipeline debugging stuff --- src/pipeline/Pipeline.cpp | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 0dda817c3..cbe99d5b7 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -195,13 +195,37 @@ PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type, bool incl schema.globalProperties = globalProperties; schema.bridges = xlinkBridges; int latestIoId = 0; + + std::vector pipelineDebuggingNodeIds; + if(!includePipelineDebugging) { + for(const auto& node : getAllNodes()) { + if(std::string(node->getName()) == std::string("PipelineEventAggregation") || std::string(node->getName()) == std::string("PipelineStateMerge")) { + pipelineDebuggingNodeIds.push_back(node->id); + } + } + for(const auto& conn : getConnectionsInternal()) { + auto outNode = conn.outputNode.lock(); + auto inNode = conn.inputNode.lock(); + if(std::string(outNode->getName()).find("XLink") != std::string::npos || std::string(inNode->getName()).find("XLink") != std::string::npos + || std::string(outNode->getName()).find("InputQueue") != std::string::npos || std::string(inNode->getName()).find("OutputQueue") != std::string::npos) { + if(std::find(pipelineDebuggingNodeIds.begin(), pipelineDebuggingNodeIds.end(), inNode->id) != pipelineDebuggingNodeIds.end()) { + pipelineDebuggingNodeIds.push_back(outNode->id); + } + if(std::find(pipelineDebuggingNodeIds.begin(), pipelineDebuggingNodeIds.end(), outNode->id) != pipelineDebuggingNodeIds.end()) { + pipelineDebuggingNodeIds.push_back(inNode->id); + } + } + } + } + // Loop over all nodes, and add them to schema for(const auto& node : getAllNodes()) { // const auto& node = kv.second; if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) { continue; } - if(!includePipelineDebugging && (std::string(node->getName()) == "PipelineEventAggregation" || std::string(node->getName()) == "PipelineStateMerge")) { + if(!includePipelineDebugging + && std::find(pipelineDebuggingNodeIds.begin(), pipelineDebuggingNodeIds.end(), node->id) != pipelineDebuggingNodeIds.end()) { continue; } // Create 'node' info @@ -261,6 +285,7 @@ PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type, bool incl // Add outputs for(const auto& output : outputs) { + if(!includePipelineDebugging && output.getName() == "pipelineEventOutput") continue; NodeIoInfo io; io.id = latestIoId; latestIoId++; @@ -309,6 +334,7 @@ PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type, bool incl // Node::Id xLinkBridgeId = latestId; for(const auto& conn : getConnectionsInternal()) { + if(!includePipelineDebugging && conn.outputName == "pipelineEventOutput") continue; NodeConnectionSchema c; auto outNode = conn.outputNode.lock(); auto inNode = conn.inputNode.lock(); @@ -319,6 +345,12 @@ PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type, bool incl c.node2Input = conn.inputName; c.node2InputGroup = conn.inputGroup; + if(!includePipelineDebugging + && (std::find(pipelineDebuggingNodeIds.begin(), pipelineDebuggingNodeIds.end(), c.node1Id) != pipelineDebuggingNodeIds.end() + || std::find(pipelineDebuggingNodeIds.begin(), pipelineDebuggingNodeIds.end(), c.node2Id) != pipelineDebuggingNodeIds.end())) { + continue; + } + bool outputHost = outNode->runOnHost(); bool inputHost = inNode->runOnHost(); @@ -860,7 +892,7 @@ void PipelineImpl::start() { // Implicitly build (if not already) build(); - Logging::getInstance().logger.debug("Full schema dump: ", ((nlohmann::json)getPipelineSchema(SerializationType::JSON, false)).dump()); + Logging::getInstance().logger.debug("Full schema dump: {}", ((nlohmann::json)getPipelineSchema(SerializationType::JSON, false)).dump()); // Indicate that pipeline is running running = true; From c1a81b53a9b8c82f291fe509cc1ca47095f0b012 Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 18 Nov 2025 16:01:21 +0100 Subject: [PATCH 098/124] Move pipeline state update to trace --- src/pipeline/Pipeline.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index cbe99d5b7..f86d574f9 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -913,7 +913,7 @@ void PipelineImpl::start() { if(utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false)) { getPipelineState().stateAsync( - [](const PipelineState& state) { Logging::getInstance().logger.debug("Pipeline state update: {}", ((nlohmann::json)state).dump()); }); + [](const PipelineState& state) { Logging::getInstance().logger.trace("Pipeline state update: {}", ((nlohmann::json)state).dump()); }); } } From 177a275942bae08cc0c0a724ae960284f2396568 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 20 Nov 2025 10:30:37 +0100 Subject: [PATCH 099/124] Change the way bridges are stored --- src/pipeline/Pipeline.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index f86d574f9..be8bca309 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -799,7 +799,7 @@ void PipelineImpl::build() { connection.out->link(xLinkBridge.xLinkOut->input); // Note the created bridge for serialization (for visualization) - xlinkBridges.push_back({outNode->id, inNode->id}); + xlinkBridges.push_back({xLinkBridge.xLinkOut->id, xLinkBridge.xLinkInHost->id}); } auto xLinkBridge = bridgesOut[connection.out]; connection.out->unlink(*connection.in); // Unlink the connection @@ -829,6 +829,9 @@ void PipelineImpl::build() { } else { xLinkBridge.xLinkOutHost->allowStreamResize(false); } + + // Note the created bridge for serialization (for visualization) + xlinkBridges.push_back({xLinkBridge.xLinkOutHost->id, xLinkBridge.xLinkIn->id}); } auto xLinkBridge = bridgesIn[connection.in]; connection.out->unlink(*connection.in); // Unlink the original connection From cc96c844b1724861a851068c4dd49c29da83d9bd Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 20 Nov 2025 11:42:38 +0100 Subject: [PATCH 100/124] Remove version ctrl marker --- src/pipeline/node/host/Replay.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pipeline/node/host/Replay.cpp b/src/pipeline/node/host/Replay.cpp index 579abcf82..a35a11cd1 100644 --- a/src/pipeline/node/host/Replay.cpp +++ b/src/pipeline/node/host/Replay.cpp @@ -170,13 +170,10 @@ inline std::shared_ptr getProtoMessage(utility::ByteP case DatatypeEnum::DynamicCalibrationResult: case DatatypeEnum::CalibrationQuality: case DatatypeEnum::CoverageData: -<<<<<<< HEAD case DatatypeEnum::PipelineEvent: case DatatypeEnum::PipelineState: case DatatypeEnum::PipelineEventAggregationConfig: -======= case DatatypeEnum::NeuralDepthConfig: ->>>>>>> 25340b3c487832095ef5a9a28b2886bb0c8f6aa0 throw std::runtime_error("Cannot replay message type: " + std::to_string((int)datatype)); } return {}; From 6ac1286ff11491bc054fd4c088548f3d5280c3fa Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 20 Nov 2025 17:25:17 +0100 Subject: [PATCH 101/124] Bugfixes, add xlink event tracking --- .../pipeline/datatype/PipelineState.hpp | 2 +- src/pipeline/Pipeline.cpp | 43 ++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/include/depthai/pipeline/datatype/PipelineState.hpp b/include/depthai/pipeline/datatype/PipelineState.hpp index e1336eee5..8ffc2af2d 100644 --- a/include/depthai/pipeline/datatype/PipelineState.hpp +++ b/include/depthai/pipeline/datatype/PipelineState.hpp @@ -59,7 +59,7 @@ class NodeState { return timing.isValid(); } - DEPTHAI_SERIALIZE(InputQueueState, state, numQueued, timing); + DEPTHAI_SERIALIZE(InputQueueState, state, numQueued, timing, queueStats); }; struct OutputQueueState { // Current state of the output queue. Send should ideally be instant. This is not the case when the input queue is full. diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index be8bca309..2fea42186 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -206,8 +206,10 @@ PipelineSchema PipelineImpl::getPipelineSchema(SerializationType type, bool incl for(const auto& conn : getConnectionsInternal()) { auto outNode = conn.outputNode.lock(); auto inNode = conn.inputNode.lock(); + if(conn.outputName == "pipelineEventOutput") continue; if(std::string(outNode->getName()).find("XLink") != std::string::npos || std::string(inNode->getName()).find("XLink") != std::string::npos - || std::string(outNode->getName()).find("InputQueue") != std::string::npos || std::string(inNode->getName()).find("OutputQueue") != std::string::npos) { + || std::string(outNode->getName()).find("InputQueue") != std::string::npos + || std::string(inNode->getName()).find("OutputQueue") != std::string::npos) { if(std::find(pipelineDebuggingNodeIds.begin(), pipelineDebuggingNodeIds.end(), inNode->id) != pipelineDebuggingNodeIds.end()) { pipelineDebuggingNodeIds.push_back(outNode->id); } @@ -874,6 +876,39 @@ void PipelineImpl::build() { } } } + + if(buildingOnHost && enablePipelineDebugging) { + std::shared_ptr pipelineEventAggHost = nullptr; + std::shared_ptr pipelineEventAggDevice = nullptr; + for(const auto& node : getAllNodes()) { + if(strcmp(node->getName(), "PipelineEventAggregation") == 0) { + if(node->runOnHost() && !pipelineEventAggHost) { + pipelineEventAggHost = std::dynamic_pointer_cast(node); + } else if(!node->runOnHost() && !pipelineEventAggDevice) { + pipelineEventAggDevice = std::dynamic_pointer_cast(node); + } + } + if(pipelineEventAggHost && pipelineEventAggDevice) { + break; + } + } + if(!pipelineEventAggHost || !pipelineEventAggDevice) { + throw std::runtime_error("PipelineEventAggregation nodes not found for pipeline debugging setup"); + } + for(auto& bridge : bridgesOut) { + auto& nodes = bridge.second; + nodes.xLinkInHost->pipelineEventOutput.link( + pipelineEventAggHost->inputs[fmt::format("{} - {}", nodes.xLinkInHost->getName(), nodes.xLinkInHost->id)]); + nodes.xLinkOut->pipelineEventOutput.link(pipelineEventAggDevice->inputs[fmt::format("{} - {}", nodes.xLinkOut->getName(), nodes.xLinkOut->id)]); + } + for(auto& bridge : bridgesIn) { + auto& nodes = bridge.second; + nodes.xLinkIn->pipelineEventOutput.link(pipelineEventAggDevice->inputs[fmt::format("{} - {}", nodes.xLinkIn->getName(), nodes.xLinkIn->id)]); + nodes.xLinkOutHost->pipelineEventOutput.link( + pipelineEventAggHost->inputs[fmt::format("{} - {}", nodes.xLinkOutHost->getName(), nodes.xLinkOutHost->id)]); + } + } + // Build if(!isHostOnly()) { // TODO(Morato) - handle multiple devices correctly, start pipeline on all of them @@ -916,7 +951,7 @@ void PipelineImpl::start() { if(utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false)) { getPipelineState().stateAsync( - [](const PipelineState& state) { Logging::getInstance().logger.trace("Pipeline state update: {}", ((nlohmann::json)state).dump()); }); + [](const PipelineState& state) { Logging::getInstance().logger.trace("Pipeline state update: {}", state.toJson().dump()); }); } } @@ -1198,6 +1233,10 @@ void Pipeline::enableHolisticReplay(const std::string& pathToRecording) { } void Pipeline::enablePipelineDebugging(bool enable) { + if(utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false)) { + throw std::runtime_error( + "You can enable pipeline debugging either through the DEPTHAI_PIPELINE_DEBUGGING environment variable or through the Pipeline API, not both"); + } if(this->isBuilt()) { throw std::runtime_error("Cannot change pipeline debugging state after pipeline is built"); } From 285b16e5c46945fbe066dfd907ef340c0694fa64 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 20 Nov 2025 17:26:16 +0100 Subject: [PATCH 102/124] RVC4 FW: Update core --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 57e86a4e1..9d7600708 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+aadf4e7260ed12dc0794db20b97e4ef8d8208b02") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+67c8b568b306ecb5062c21b24f39ece5881d32f3") From 2280f0400b05961aa7189d14c2509f9d934301b3 Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 21 Nov 2025 13:42:34 +0100 Subject: [PATCH 103/124] Fix missing input map events --- include/depthai/pipeline/MessageQueue.hpp | 12 ++++++++++-- src/pipeline/Pipeline.cpp | 10 ++++++++++ src/pipeline/ThreadedNode.cpp | 1 - 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/include/depthai/pipeline/MessageQueue.hpp b/include/depthai/pipeline/MessageQueue.hpp index 517a0aa74..00d2ce98d 100644 --- a/include/depthai/pipeline/MessageQueue.hpp +++ b/include/depthai/pipeline/MessageQueue.hpp @@ -49,19 +49,26 @@ class MessageQueue : public std::enable_shared_from_this { std::shared_ptr pipelineEventDispatcher = nullptr); MessageQueue(const MessageQueue& c) - : enable_shared_from_this(c), queue(c.queue), name(c.name), callbacks(c.callbacks), uniqueCallbackId(c.uniqueCallbackId){}; + : enable_shared_from_this(c), + queue(c.queue), + name(c.name), + callbacks(c.callbacks), + uniqueCallbackId(c.uniqueCallbackId), + pipelineEventDispatcher(c.pipelineEventDispatcher) {}; MessageQueue(MessageQueue&& m) noexcept : enable_shared_from_this(m), queue(std::move(m.queue)), name(std::move(m.name)), callbacks(std::move(m.callbacks)), - uniqueCallbackId(m.uniqueCallbackId){}; + uniqueCallbackId(m.uniqueCallbackId), + pipelineEventDispatcher(m.pipelineEventDispatcher) {}; MessageQueue& operator=(const MessageQueue& c) { queue = c.queue; name = c.name; callbacks = c.callbacks; uniqueCallbackId = c.uniqueCallbackId; + pipelineEventDispatcher = c.pipelineEventDispatcher; return *this; } @@ -70,6 +77,7 @@ class MessageQueue : public std::enable_shared_from_this { name = std::move(m.name); callbacks = std::move(m.callbacks); uniqueCallbackId = m.uniqueCallbackId; + pipelineEventDispatcher = m.pipelineEventDispatcher; return *this; } diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index 2fea42186..e3b560a18 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -877,7 +877,9 @@ void PipelineImpl::build() { } } + // Finish setting up pipeline debugging if(buildingOnHost && enablePipelineDebugging) { + // Enable events on xlink bridges std::shared_ptr pipelineEventAggHost = nullptr; std::shared_ptr pipelineEventAggDevice = nullptr; for(const auto& node : getAllNodes()) { @@ -909,6 +911,14 @@ void PipelineImpl::build() { } } + // Initialize event dispatchers + for(const auto& node : getAllNodes()) { + auto threadedNode = std::dynamic_pointer_cast(node); + if(threadedNode) { + threadedNode->initPipelineEventDispatcher(threadedNode->id); + } + } + // Build if(!isHostOnly()) { // TODO(Morato) - handle multiple devices correctly, start pipeline on all of them diff --git a/src/pipeline/ThreadedNode.cpp b/src/pipeline/ThreadedNode.cpp index f960e6852..73ad5d2bb 100644 --- a/src/pipeline/ThreadedNode.cpp +++ b/src/pipeline/ThreadedNode.cpp @@ -31,7 +31,6 @@ void ThreadedNode::initPipelineEventDispatcher(int64_t nodeId) { ThreadedNode::~ThreadedNode() = default; void ThreadedNode::start() { - initPipelineEventDispatcher(this->id); // A node should not be started if it is already running // We would be creating multiple threads for the same node DAI_CHECK_V(!isRunning(), "Node with id {} is already running. Cannot start it again. Node name: {}", id, getName()); From 215dffd176e45f9312e1ae8e0fa8fdc99e41c882 Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 21 Nov 2025 14:12:08 +0100 Subject: [PATCH 104/124] Add pipeline events to new detection parser --- src/pipeline/node/DetectionParser.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/pipeline/node/DetectionParser.cpp b/src/pipeline/node/DetectionParser.cpp index f06447353..14fbdccc3 100644 --- a/src/pipeline/node/DetectionParser.cpp +++ b/src/pipeline/node/DetectionParser.cpp @@ -394,9 +394,13 @@ void DetectionParser::run() { logger->info("Detection parser running on host."); using namespace std::chrono; - while(isRunning()) { + while(mainLoop()) { auto tAbsoluteBeginning = steady_clock::now(); - std::shared_ptr sharedInputData = input.get(); + std::shared_ptr sharedInputData; + { + auto blockEvent = this->inputBlockEvent(); + sharedInputData = input.get(); + } auto outDetections = std::make_shared(); if(!sharedInputData) { @@ -442,8 +446,11 @@ void DetectionParser::run() { outDetections->setTimestampDevice(inputData.getTimestampDevice()); outDetections->transformation = inputData.transformation; - // Send detections - out.send(outDetections); + { + auto blockEvent = this->outputBlockEvent(); + // Send detections + out.send(outDetections); + } auto tAbsoluteEnd = steady_clock::now(); logger->debug("Detection parser total took {}ms, processing {}ms, getting_frames {}ms, sending_frames {}ms", From af02c97e2359864c081f6de63ca0d89c7c65b57c Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 21 Nov 2025 14:15:42 +0100 Subject: [PATCH 105/124] RVC4 FW: Update core --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 9d7600708..559d8462b 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+67c8b568b306ecb5062c21b24f39ece5881d32f3") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+38e2fe79196a628978baeb7915c29bfb918cf7b3") From e056852bd94d875068853988684a40d55702ff90 Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 21 Nov 2025 16:39:01 +0100 Subject: [PATCH 106/124] Fix pipeline debugging with no explicit host nodes, update RVC2 FW --- cmake/Depthai/DepthaiDeviceSideConfig.cmake | 2 +- src/pipeline/Pipeline.cpp | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/cmake/Depthai/DepthaiDeviceSideConfig.cmake b/cmake/Depthai/DepthaiDeviceSideConfig.cmake index eff33b6cd..d3470f2e8 100644 --- a/cmake/Depthai/DepthaiDeviceSideConfig.cmake +++ b/cmake/Depthai/DepthaiDeviceSideConfig.cmake @@ -2,7 +2,7 @@ set(DEPTHAI_DEVICE_SIDE_MATURITY "snapshot") # "full commit hash of device side binary" -set(DEPTHAI_DEVICE_SIDE_COMMIT "5cf315c03f05c41a11a4a2747335f4f43123d277") +set(DEPTHAI_DEVICE_SIDE_COMMIT "3535bdde4b2460b3ba80b7e4539e71aa9dc39fb9") # "version if applicable" set(DEPTHAI_DEVICE_SIDE_VERSION "") diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index e3b560a18..7e8b84589 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -1164,23 +1164,18 @@ void PipelineImpl::setupPipelineDebugging() { enablePipelineDebugging = enablePipelineDebugging || utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false); if(enablePipelineDebugging) { // Check if any nodes are on host or device - bool hasHostNodes = false; bool hasDeviceNodes = false; for(const auto& node : getAllNodes()) { if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; - if(node->runOnHost()) { - hasHostNodes = true; - } else { + if(!node->runOnHost()) { hasDeviceNodes = true; } } std::shared_ptr hostEventAgg = nullptr; std::shared_ptr deviceEventAgg = nullptr; - if(hasHostNodes) { - hostEventAgg = parent.create(); - hostEventAgg->setRunOnHost(true); - } + hostEventAgg = parent.create(); + hostEventAgg->setRunOnHost(true); if(hasDeviceNodes) { deviceEventAgg = parent.create(); deviceEventAgg->setRunOnHost(false); @@ -1197,7 +1192,7 @@ void PipelineImpl::setupPipelineDebugging() { } } } - auto stateMerge = parent.create()->build(hasDeviceNodes, hasHostNodes); + auto stateMerge = parent.create()->build(hasDeviceNodes, true); if(deviceEventAgg) { deviceEventAgg->out.link(stateMerge->inputDevice); stateMerge->outRequest.link(deviceEventAgg->request); From 8e0035fb0ac6a08570eb9c5bf1580c361cd70f7e Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 24 Nov 2025 13:54:34 +0100 Subject: [PATCH 107/124] Add pipeline events to xlink host nodes --- src/pipeline/node/internal/XLinkInHost.cpp | 15 +++-- src/pipeline/node/internal/XLinkOutHost.cpp | 71 +++++++++++---------- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/src/pipeline/node/internal/XLinkInHost.cpp b/src/pipeline/node/internal/XLinkInHost.cpp index afecbf294..6936ffd36 100644 --- a/src/pipeline/node/internal/XLinkInHost.cpp +++ b/src/pipeline/node/internal/XLinkInHost.cpp @@ -38,10 +38,14 @@ void XLinkInHost::run() { while(reconnect) { reconnect = false; XLinkStream stream(std::move(conn), streamName, 1); - while(isRunning()) { + while(mainLoop()) { try { // Blocking -- parse packet and gather timing information - auto packet = stream.readMove(); + StreamPacketDesc packet; + { + auto blockEvent = this->inputBlockEvent(); + packet = stream.readMove(); + } const auto t1Parse = std::chrono::steady_clock::now(); const auto msg = StreamMessageParser::parseMessage(std::move(packet)); if(std::dynamic_pointer_cast(msg) != nullptr) { @@ -66,7 +70,10 @@ void XLinkInHost::run() { spdlog::to_hex(metadata)); } - out.send(msg); + { + auto blockEvent = this->outputBlockEvent(); + out.send(msg); + } // // Add 'data' to queue // if(!queue.push(msg)) { // throw std::runtime_error(fmt::format("Underlying queue destructed")); @@ -110,4 +117,4 @@ void XLinkInHost::run() { } // namespace internal } // namespace node -} // namespace dai \ No newline at end of file +} // namespace dai diff --git a/src/pipeline/node/internal/XLinkOutHost.cpp b/src/pipeline/node/internal/XLinkOutHost.cpp index de5d782d4..4221d65e3 100644 --- a/src/pipeline/node/internal/XLinkOutHost.cpp +++ b/src/pipeline/node/internal/XLinkOutHost.cpp @@ -55,7 +55,11 @@ void XLinkOutHost::run() { }; while(mainLoop()) { try { - auto outgoing = in.get(); + std::shared_ptr outgoing; + { + auto blockEvent = this->inputBlockEvent(); + outgoing = in.get(); + } auto metadata = StreamMessageParser::serializeMetadata(outgoing); using namespace std::chrono; @@ -65,40 +69,43 @@ void XLinkOutHost::run() { if(outgoingDataSize > currentMaxSize - metadata.size()) { increaseBufferSize(outgoingDataSize + metadata.size()); } - if(outgoing->data->getSize() > 0) { - auto sharedMemory = std::dynamic_pointer_cast(outgoing->data); - if(sharedMemory && sharedMemory->getFd() > 0) { - stream.write(sharedMemory->getFd(), metadata); + { + auto blockEvent = this->outputBlockEvent(); + if(outgoing->data->getSize() > 0) { + auto sharedMemory = std::dynamic_pointer_cast(outgoing->data); + if(sharedMemory && sharedMemory->getFd() > 0) { + stream.write(sharedMemory->getFd(), metadata); + } else { + stream.write(outgoing->data->getData(), metadata); + } } else { - stream.write(outgoing->data->getData(), metadata); + stream.write(metadata); + } + auto t2 = steady_clock::now(); + // Log + if(spdlog::get_level() == spdlog::level::trace) { + logger::trace("Sent message to device ({}) - data size: {}, metadata: {}, sending time: {}", + stream.getStreamName(), + outgoing->data->getSize(), + spdlog::to_hex(metadata), + duration_cast(t2 - t1)); } - } else { - stream.write(metadata); - } - auto t2 = steady_clock::now(); - // Log - if(spdlog::get_level() == spdlog::level::trace) { - logger::trace("Sent message to device ({}) - data size: {}, metadata: {}, sending time: {}", - stream.getStreamName(), - outgoing->data->getSize(), - spdlog::to_hex(metadata), - duration_cast(t2 - t1)); - } - // Attempt dynamic cast to MessageGroup - if(auto msgGroupPtr = std::dynamic_pointer_cast(outgoing)) { - logger::trace("Sending group message to device with {} messages", msgGroupPtr->group.size()); - for(auto& msg : msgGroupPtr->group) { - logger::trace("Sending part of a group message: {}", msg.first); - auto metadata = StreamMessageParser::serializeMetadata(msg.second); - outgoingDataSize = msg.second->data->getSize(); - if(outgoingDataSize > currentMaxSize - metadata.size()) { - increaseBufferSize(outgoingDataSize + metadata.size()); - } - if(msg.second->data->getSize() > 0) { - stream.write(msg.second->data->getData(), metadata); - } else { - stream.write(metadata); + // Attempt dynamic cast to MessageGroup + if(auto msgGroupPtr = std::dynamic_pointer_cast(outgoing)) { + logger::trace("Sending group message to device with {} messages", msgGroupPtr->group.size()); + for(auto& msg : msgGroupPtr->group) { + logger::trace("Sending part of a group message: {}", msg.first); + auto metadata = StreamMessageParser::serializeMetadata(msg.second); + outgoingDataSize = msg.second->data->getSize(); + if(outgoingDataSize > currentMaxSize - metadata.size()) { + increaseBufferSize(outgoingDataSize + metadata.size()); + } + if(msg.second->data->getSize() > 0) { + stream.write(msg.second->data->getData(), metadata); + } else { + stream.write(metadata); + } } } } From 0e5208a3ba9a9a11f5faa59fcd57174f3aebaf33 Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 24 Nov 2025 13:59:41 +0100 Subject: [PATCH 108/124] RVC4 FW: Add pipeline events to xlink nodes --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 559d8462b..67b8892e6 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+38e2fe79196a628978baeb7915c29bfb918cf7b3") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+d70eb5c515369ca6e2eab8ad52b7391afb1e0d23") From 93318ea3f1f21137d73f07164f18db2339009f41 Mon Sep 17 00:00:00 2001 From: asahtik Date: Mon, 24 Nov 2025 14:03:37 +0100 Subject: [PATCH 109/124] RVC2 FW: Update core --- cmake/Depthai/DepthaiDeviceSideConfig.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceSideConfig.cmake b/cmake/Depthai/DepthaiDeviceSideConfig.cmake index d3470f2e8..088b10d97 100644 --- a/cmake/Depthai/DepthaiDeviceSideConfig.cmake +++ b/cmake/Depthai/DepthaiDeviceSideConfig.cmake @@ -2,7 +2,7 @@ set(DEPTHAI_DEVICE_SIDE_MATURITY "snapshot") # "full commit hash of device side binary" -set(DEPTHAI_DEVICE_SIDE_COMMIT "3535bdde4b2460b3ba80b7e4539e71aa9dc39fb9") +set(DEPTHAI_DEVICE_SIDE_COMMIT "b4d87628a16909866820da9dd7f42e5a785716dd") # "version if applicable" set(DEPTHAI_DEVICE_SIDE_VERSION "") From e8d08b59fec197701857247719c2695522888ffa Mon Sep 17 00:00:00 2001 From: asahtik Date: Tue, 25 Nov 2025 13:36:22 +0100 Subject: [PATCH 110/124] WIP: separate path for trace state messages [no ci] --- include/depthai/pipeline/Pipeline.hpp | 1 + include/depthai/pipeline/PipelineStateApi.hpp | 1 + .../internal/PipelineEventAggregation.hpp | 9 +- .../node/internal/PipelineStateMerge.hpp | 4 + .../PipelineEventAggregationProperties.hpp | 3 +- src/pipeline/Pipeline.cpp | 30 ++++- src/pipeline/PipelineStateApi.cpp | 13 ++ .../internal/PipelineEventAggregation.cpp | 124 +++++++++++------- .../node/internal/PipelineStateMerge.cpp | 7 +- 9 files changed, 139 insertions(+), 53 deletions(-) diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index cc9007e12..68fd5562a 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -130,6 +130,7 @@ class PipelineImpl : public std::enable_shared_from_this { bool enablePipelineDebugging = false; std::shared_ptr pipelineStateOut; std::shared_ptr pipelineStateRequest; + std::shared_ptr pipelineStateTraceOut; // Output queues std::vector> outputQueues; diff --git a/include/depthai/pipeline/PipelineStateApi.hpp b/include/depthai/pipeline/PipelineStateApi.hpp index b2d1e61bc..8a5c0d284 100644 --- a/include/depthai/pipeline/PipelineStateApi.hpp +++ b/include/depthai/pipeline/PipelineStateApi.hpp @@ -92,6 +92,7 @@ class PipelineStateApi { return NodeStateApi(nodeId, pipelineStateOut, pipelineStateRequest); } void stateAsync(std::function callback, std::optional config = std::nullopt); + void configureTraceOutput(uint32_t repeatIntervalSeconds); }; } // namespace dai diff --git a/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp index 2ca556841..0d1a9dfd3 100644 --- a/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp +++ b/include/depthai/pipeline/node/internal/PipelineEventAggregation.hpp @@ -31,10 +31,15 @@ class PipelineEventAggregation : public DeviceNodeCRTP { bool hasDeviceNodes = false; bool hasHostNodes = false; + bool allowConfiguration = true; + public: constexpr static const char* NAME = "PipelineStateMerge"; @@ -35,6 +37,8 @@ class PipelineStateMerge : public CustomThreadedNode { std::shared_ptr build(bool hasDeviceNodes, bool hasHostNodes); + PipelineStateMerge& setAllowConfiguration(bool allow); + void run() override; }; diff --git a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp index 9b6328fe7..7998f3aa1 100644 --- a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp +++ b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp @@ -11,10 +11,11 @@ struct PipelineEventAggregationProperties : PropertiesSerializablepipelinePtr = weak; } - if(utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false)) { - getPipelineState().stateAsync( - [](const PipelineState& state) { Logging::getInstance().logger.trace("Pipeline state update: {}", state.toJson().dump()); }); + if(buildingOnHost && utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false)) { + if(pipelineStateTraceOut) { + getPipelineState().configureTraceOutput(1); + pipelineStateTraceOut->addCallback([](const std::shared_ptr& data) { + if(data) { + auto state = std::dynamic_pointer_cast(data); + if(state) Logging::getInstance().logger.trace("Pipeline state update: {}", state->toJson().dump()); + } + }); + } } } @@ -1161,7 +1168,8 @@ std::vector PipelineImpl::loadResourceCwd(fs::path uri, fs::path cwd, b void PipelineImpl::setupPipelineDebugging() { // Create pipeline event aggregator node and link - enablePipelineDebugging = enablePipelineDebugging || utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false); + bool envPipelineDebugging = utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false); + enablePipelineDebugging = enablePipelineDebugging || envPipelineDebugging; if(enablePipelineDebugging) { // Check if any nodes are on host or device bool hasDeviceNodes = false; @@ -1176,9 +1184,11 @@ void PipelineImpl::setupPipelineDebugging() { std::shared_ptr deviceEventAgg = nullptr; hostEventAgg = parent.create(); hostEventAgg->setRunOnHost(true); + hostEventAgg->setTraceOutput(envPipelineDebugging); if(hasDeviceNodes) { deviceEventAgg = parent.create(); deviceEventAgg->setRunOnHost(false); + deviceEventAgg->setTraceOutput(envPipelineDebugging); } for(auto& node : getAllNodes()) { if(std::string(node->getName()) == std::string("NodeGroup") || std::string(node->getName()) == std::string("DeviceNodeGroup")) continue; @@ -1193,16 +1203,28 @@ void PipelineImpl::setupPipelineDebugging() { } } auto stateMerge = parent.create()->build(hasDeviceNodes, true); + std::shared_ptr traceStateMerge; + if(envPipelineDebugging) { + traceStateMerge = parent.create()->build(hasDeviceNodes, true); + traceStateMerge->setAllowConfiguration(false); + } if(deviceEventAgg) { deviceEventAgg->out.link(stateMerge->inputDevice); stateMerge->outRequest.link(deviceEventAgg->request); + if(envPipelineDebugging) { + deviceEventAgg->outTrace.link(traceStateMerge->inputDevice); + } } if(hostEventAgg) { hostEventAgg->out.link(stateMerge->inputHost); stateMerge->outRequest.link(hostEventAgg->request); + if(envPipelineDebugging) { + hostEventAgg->outTrace.link(traceStateMerge->inputHost); + } } pipelineStateOut = stateMerge->out.createOutputQueue(1, false); pipelineStateRequest = stateMerge->request.createInputQueue(); + if(envPipelineDebugging) pipelineStateTraceOut = traceStateMerge->out.createOutputQueue(1, false); } } diff --git a/src/pipeline/PipelineStateApi.cpp b/src/pipeline/PipelineStateApi.cpp index b7cc3a035..6ceff5c7b 100644 --- a/src/pipeline/PipelineStateApi.cpp +++ b/src/pipeline/PipelineStateApi.cpp @@ -279,5 +279,18 @@ void PipelineStateApi::stateAsync(std::function call } }); } +void PipelineStateApi::configureTraceOutput(uint32_t repeatIntervalSeconds) { + PipelineEventAggregationConfig cfg; + cfg.repeatIntervalSeconds = repeatIntervalSeconds; + cfg.setTimestamp(std::chrono::steady_clock::now()); + for(auto id : nodeIds) { + NodeEventAggregationConfig nodeCfg; + nodeCfg.nodeId = id; + nodeCfg.events = false; + cfg.nodes.push_back(nodeCfg); + } + + pipelineStateRequest->send(std::make_shared(cfg)); +} } // namespace dai diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 90dbfa4f7..0863479f5 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -1,6 +1,7 @@ #include "depthai/pipeline/node/internal/PipelineEventAggregation.hpp" #include +#include #include #include "depthai/pipeline/datatype/PipelineEvent.hpp" @@ -464,6 +465,60 @@ bool PipelineEventAggregation::runOnHost() const { return runOnHostVar; } +PipelineEventAggregation& PipelineEventAggregation::setTraceOutput(bool enable) { + properties.traceOutput = enable; + return *this; +} + +std::tuple, bool> makeOutputState(PipelineEventHandler& handler, std::optional& currentConfig, const uint32_t sequenceNum, bool sendEvents) { + auto outState = std::make_shared(); + bool updated = handler.getState(outState, sendEvents); + outState->sequenceNum = sequenceNum; + outState->configSequenceNum = currentConfig.has_value() ? currentConfig->sequenceNum : 0; + outState->setTimestamp(std::chrono::steady_clock::now()); + outState->tsDevice = outState->ts; + + if(!currentConfig->nodes.empty()) { + for(auto it = outState->nodeStates.begin(); it != outState->nodeStates.end();) { + auto nodeConfig = std::find_if( + currentConfig->nodes.begin(), currentConfig->nodes.end(), [&](const NodeEventAggregationConfig& cfg) { return cfg.nodeId == it->first; }); + if(nodeConfig == currentConfig->nodes.end()) { + it = outState->nodeStates.erase(it); + } else { + if(nodeConfig->inputs.has_value()) { + auto inputStates = it->second.inputStates; + it->second.inputStates.clear(); + for(const auto& inputName : *nodeConfig->inputs) { + if(inputStates.find(inputName) != inputStates.end()) { + it->second.inputStates[inputName] = inputStates[inputName]; + } + } + } + if(nodeConfig->outputs.has_value()) { + auto outputStates = it->second.outputStates; + it->second.outputStates.clear(); + for(const auto& outputName : *nodeConfig->outputs) { + if(outputStates.find(outputName) != outputStates.end()) { + it->second.outputStates[outputName] = outputStates[outputName]; + } + } + } + if(nodeConfig->others.has_value()) { + auto otherTimings = it->second.otherTimings; + it->second.otherTimings.clear(); + for(const auto& otherName : *nodeConfig->others) { + if(otherTimings.find(otherName) != otherTimings.end()) { + it->second.otherTimings[otherName] = otherTimings[otherName]; + } + } + } + ++it; + } + } + } + return {outState, updated}; +} + void PipelineEventAggregation::run() { auto& logger = pimpl->logger; @@ -471,10 +526,13 @@ void PipelineEventAggregation::run() { handler.run(); std::optional currentConfig; + + std::optional traceOutputConfig = std::nullopt; + std::thread traceOutputThread; + uint32_t sequenceNum = 0; std::chrono::time_point lastSentTime; while(mainLoop()) { - auto outState = std::make_shared(); bool gotConfig = false; if(!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeatIntervalSeconds.has_value()) || request.has()) { auto req = request.get(); @@ -483,6 +541,21 @@ void PipelineEventAggregation::run() { gotConfig = true; } } + if(properties.traceOutput && !traceOutputConfig.has_value() && gotConfig && currentConfig->repeatIntervalSeconds.has_value()) { + traceOutputConfig = currentConfig; + traceOutputThread = std::thread([this, &handler, &traceOutputConfig]() { + uint32_t traceSequenceNum = 0; + PipelineEventHandler& traceHandler = handler; + std::optional config = traceOutputConfig; + while(this->isRunning()) { + auto start = std::chrono::steady_clock::now(); + auto [outState, updated] = makeOutputState(traceHandler, config, traceSequenceNum++, false); + this->outTrace.send(outState); + auto duration = std::chrono::steady_clock::now() - start; + std::this_thread::sleep_for(std::chrono::seconds(config->repeatIntervalSeconds.value()) - duration); + } + }); + } if(gotConfig || (currentConfig.has_value() && currentConfig->repeatIntervalSeconds.has_value())) { bool sendEvents = false; if(currentConfig.has_value()) { @@ -493,51 +566,7 @@ void PipelineEventAggregation::run() { } } } - bool updated = handler.getState(outState, sendEvents); - outState->sequenceNum = sequenceNum++; - outState->configSequenceNum = currentConfig.has_value() ? currentConfig->sequenceNum : 0; - outState->setTimestamp(std::chrono::steady_clock::now()); - outState->tsDevice = outState->ts; - - if(!currentConfig->nodes.empty()) { - for(auto it = outState->nodeStates.begin(); it != outState->nodeStates.end();) { - auto nodeConfig = std::find_if(currentConfig->nodes.begin(), currentConfig->nodes.end(), [&](const NodeEventAggregationConfig& cfg) { - return cfg.nodeId == it->first; - }); - if(nodeConfig == currentConfig->nodes.end()) { - it = outState->nodeStates.erase(it); - } else { - if(nodeConfig->inputs.has_value()) { - auto inputStates = it->second.inputStates; - it->second.inputStates.clear(); - for(const auto& inputName : *nodeConfig->inputs) { - if(inputStates.find(inputName) != inputStates.end()) { - it->second.inputStates[inputName] = inputStates[inputName]; - } - } - } - if(nodeConfig->outputs.has_value()) { - auto outputStates = it->second.outputStates; - it->second.outputStates.clear(); - for(const auto& outputName : *nodeConfig->outputs) { - if(outputStates.find(outputName) != outputStates.end()) { - it->second.outputStates[outputName] = outputStates[outputName]; - } - } - } - if(nodeConfig->others.has_value()) { - auto otherTimings = it->second.otherTimings; - it->second.otherTimings.clear(); - for(const auto& otherName : *nodeConfig->others) { - if(otherTimings.find(otherName) != otherTimings.end()) { - it->second.otherTimings[otherName] = otherTimings[otherName]; - } - } - } - ++it; - } - } - } + auto [outState, updated] = makeOutputState(handler, currentConfig, sequenceNum++, sendEvents); auto now = std::chrono::steady_clock::now(); if(gotConfig || (currentConfig.has_value() && currentConfig->repeatIntervalSeconds.has_value() && updated @@ -549,6 +578,9 @@ void PipelineEventAggregation::run() { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } handler.stop(); + if(traceOutputThread.joinable()) { + traceOutputThread.join(); + } } } // namespace internal diff --git a/src/pipeline/node/internal/PipelineStateMerge.cpp b/src/pipeline/node/internal/PipelineStateMerge.cpp index ab330cd72..111ca6b81 100644 --- a/src/pipeline/node/internal/PipelineStateMerge.cpp +++ b/src/pipeline/node/internal/PipelineStateMerge.cpp @@ -13,6 +13,11 @@ std::shared_ptr PipelineStateMerge::build(bool hasDeviceNode return std::static_pointer_cast(shared_from_this()); } +PipelineStateMerge& PipelineStateMerge::setAllowConfiguration(bool allow) { + this->allowConfiguration = allow; + return *this; +} + void mergeStates(std::shared_ptr& outState, const std::shared_ptr& inState) { for(const auto& [key, value] : inState->nodeStates) { if(outState->nodeStates.find(key) != outState->nodeStates.end()) { @@ -32,7 +37,7 @@ void PipelineStateMerge::run() { while(mainLoop()) { auto outState = std::make_shared(); bool waitForMatch = false; - if(!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeatIntervalSeconds.has_value()) || request.has()) { + if(allowConfiguration && (!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeatIntervalSeconds.has_value()) || request.has())) { auto req = request.get(); if(req != nullptr) { currentConfig = *req; From 96c10aa74540ef60bd90318505be735650cfcbf8 Mon Sep 17 00:00:00 2001 From: asahtik Date: Wed, 26 Nov 2025 11:39:58 +0100 Subject: [PATCH 111/124] Pipeline debugging test fixes --- src/pipeline/Pipeline.cpp | 2 +- src/pipeline/node/internal/PipelineEventAggregation.cpp | 2 ++ src/pipeline/node/internal/PipelineStateMerge.cpp | 3 +++ tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp | 5 +++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pipeline/Pipeline.cpp b/src/pipeline/Pipeline.cpp index ea2060c59..1293c09af 100644 --- a/src/pipeline/Pipeline.cpp +++ b/src/pipeline/Pipeline.cpp @@ -894,7 +894,7 @@ void PipelineImpl::build() { break; } } - if(!pipelineEventAggHost || !pipelineEventAggDevice) { + if(!(bridgesOut.empty() && bridgesIn.empty()) && (!pipelineEventAggHost || !pipelineEventAggDevice)) { throw std::runtime_error("PipelineEventAggregation nodes not found for pipeline debugging setup"); } for(auto& bridge : bridgesOut) { diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 0863479f5..8290ffb3b 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -522,6 +522,8 @@ std::tuple, bool> makeOutputState(PipelineEventHa void PipelineEventAggregation::run() { auto& logger = pimpl->logger; + this->pipelineEventDispatcher->sendEvents = false; + PipelineEventHandler handler(&inputs, properties.aggregationWindowSize, properties.statsUpdateIntervalMs, properties.eventWaitWindow, logger); handler.run(); diff --git a/src/pipeline/node/internal/PipelineStateMerge.cpp b/src/pipeline/node/internal/PipelineStateMerge.cpp index 111ca6b81..be06f1543 100644 --- a/src/pipeline/node/internal/PipelineStateMerge.cpp +++ b/src/pipeline/node/internal/PipelineStateMerge.cpp @@ -28,6 +28,9 @@ void mergeStates(std::shared_ptr& outState, const std::shared_ptr } void PipelineStateMerge::run() { auto& logger = pimpl->logger; + + this->pipelineEventDispatcher->sendEvents = false; + if(!hasDeviceNodes && !hasHostNodes) { logger->warn("PipelineStateMerge: both device and host nodes are disabled. Have you built the node?"); } diff --git a/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp b/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp index bf28224fb..3240143b4 100644 --- a/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp +++ b/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp @@ -49,6 +49,11 @@ TEST_CASE("Object Tracker Pipeline Debugging") { auto state = pipeline.getPipelineState().nodes().detailed(); for(const auto& [nodeId, nodeState] : state.nodeStates) { + if(nodeId == 9) continue; // XLinkOutHost of merge outRequest + if(nodeId == 12) continue; // XLinkInHost of merge inputDevice + if(nodeId == 10) continue; // XLinkIn of aggregation request + if(nodeId == 11) continue; // XLinkOut of aggregation out + auto node = pipeline.getNode(nodeId); REQUIRE(nodeState.mainLoopTiming.isValid()); if(!node->getInputs().empty()) REQUIRE(nodeState.inputsGetTiming.isValid()); From 59d6b536389ca5d803f8967a918e39cf3d834102 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 27 Nov 2025 11:12:05 +0100 Subject: [PATCH 112/124] Fix issues with pipeline state trace [no ci] --- include/depthai/pipeline/Pipeline.hpp | 1 + include/depthai/pipeline/PipelineStateApi.hpp | 1 - .../node/internal/PipelineStateMerge.hpp | 2 +- .../PipelineEventAggregationProperties.hpp | 1 + src/pipeline/Pipeline.cpp | 17 +++++++++----- src/pipeline/PipelineStateApi.cpp | 13 ----------- .../internal/PipelineEventAggregation.cpp | 6 +++-- .../node/internal/PipelineStateMerge.cpp | 6 +++-- .../pipeline_debugging_rvc4_test.cpp | 23 +++++++++++++++---- .../pipeline_debugging_host_test.cpp | 3 ++- 10 files changed, 43 insertions(+), 30 deletions(-) diff --git a/include/depthai/pipeline/Pipeline.hpp b/include/depthai/pipeline/Pipeline.hpp index 68fd5562a..074df10fd 100644 --- a/include/depthai/pipeline/Pipeline.hpp +++ b/include/depthai/pipeline/Pipeline.hpp @@ -131,6 +131,7 @@ class PipelineImpl : public std::enable_shared_from_this { std::shared_ptr pipelineStateOut; std::shared_ptr pipelineStateRequest; std::shared_ptr pipelineStateTraceOut; + std::shared_ptr pipelineStateTraceRequest; // Output queues std::vector> outputQueues; diff --git a/include/depthai/pipeline/PipelineStateApi.hpp b/include/depthai/pipeline/PipelineStateApi.hpp index 8a5c0d284..b2d1e61bc 100644 --- a/include/depthai/pipeline/PipelineStateApi.hpp +++ b/include/depthai/pipeline/PipelineStateApi.hpp @@ -92,7 +92,6 @@ class PipelineStateApi { return NodeStateApi(nodeId, pipelineStateOut, pipelineStateRequest); } void stateAsync(std::function callback, std::optional config = std::nullopt); - void configureTraceOutput(uint32_t repeatIntervalSeconds); }; } // namespace dai diff --git a/include/depthai/pipeline/node/internal/PipelineStateMerge.hpp b/include/depthai/pipeline/node/internal/PipelineStateMerge.hpp index 7370f4a55..ac08bf07d 100644 --- a/include/depthai/pipeline/node/internal/PipelineStateMerge.hpp +++ b/include/depthai/pipeline/node/internal/PipelineStateMerge.hpp @@ -12,7 +12,7 @@ class PipelineStateMerge : public CustomThreadedNode { bool hasDeviceNodes = false; bool hasHostNodes = false; - bool allowConfiguration = true; + bool allowReconfiguration = true; public: constexpr static const char* NAME = "PipelineStateMerge"; diff --git a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp index 7998f3aa1..bbca333f7 100644 --- a/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp +++ b/include/depthai/properties/internal/PipelineEventAggregationProperties.hpp @@ -11,6 +11,7 @@ struct PipelineEventAggregationProperties : PropertiesSerializable("DEPTHAI_PIPELINE_DEBUGGING", false)) { if(pipelineStateTraceOut) { - getPipelineState().configureTraceOutput(1); + PipelineEventAggregationConfig cfg; + cfg.repeatIntervalSeconds = 1; + cfg.setTimestamp(std::chrono::steady_clock::now()); + pipelineStateTraceRequest->send(std::make_shared(cfg)); + pipelineStateTraceOut->addCallback([](const std::shared_ptr& data) { if(data) { auto state = std::dynamic_pointer_cast(data); @@ -1213,6 +1217,7 @@ void PipelineImpl::setupPipelineDebugging() { stateMerge->outRequest.link(deviceEventAgg->request); if(envPipelineDebugging) { deviceEventAgg->outTrace.link(traceStateMerge->inputDevice); + traceStateMerge->outRequest.link(deviceEventAgg->request); } } if(hostEventAgg) { @@ -1220,11 +1225,15 @@ void PipelineImpl::setupPipelineDebugging() { stateMerge->outRequest.link(hostEventAgg->request); if(envPipelineDebugging) { hostEventAgg->outTrace.link(traceStateMerge->inputHost); + traceStateMerge->outRequest.link(hostEventAgg->request); } } pipelineStateOut = stateMerge->out.createOutputQueue(1, false); pipelineStateRequest = stateMerge->request.createInputQueue(); - if(envPipelineDebugging) pipelineStateTraceOut = traceStateMerge->out.createOutputQueue(1, false); + if(envPipelineDebugging) { + pipelineStateTraceOut = traceStateMerge->out.createOutputQueue(1, false); + pipelineStateTraceRequest = traceStateMerge->request.createInputQueue(); + } } } @@ -1260,10 +1269,6 @@ void Pipeline::enableHolisticReplay(const std::string& pathToRecording) { } void Pipeline::enablePipelineDebugging(bool enable) { - if(utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false)) { - throw std::runtime_error( - "You can enable pipeline debugging either through the DEPTHAI_PIPELINE_DEBUGGING environment variable or through the Pipeline API, not both"); - } if(this->isBuilt()) { throw std::runtime_error("Cannot change pipeline debugging state after pipeline is built"); } diff --git a/src/pipeline/PipelineStateApi.cpp b/src/pipeline/PipelineStateApi.cpp index 6ceff5c7b..b7cc3a035 100644 --- a/src/pipeline/PipelineStateApi.cpp +++ b/src/pipeline/PipelineStateApi.cpp @@ -279,18 +279,5 @@ void PipelineStateApi::stateAsync(std::function call } }); } -void PipelineStateApi::configureTraceOutput(uint32_t repeatIntervalSeconds) { - PipelineEventAggregationConfig cfg; - cfg.repeatIntervalSeconds = repeatIntervalSeconds; - cfg.setTimestamp(std::chrono::steady_clock::now()); - for(auto id : nodeIds) { - NodeEventAggregationConfig nodeCfg; - nodeCfg.nodeId = id; - nodeCfg.events = false; - cfg.nodes.push_back(nodeCfg); - } - - pipelineStateRequest->send(std::make_shared(cfg)); -} } // namespace dai diff --git a/src/pipeline/node/internal/PipelineEventAggregation.cpp b/src/pipeline/node/internal/PipelineEventAggregation.cpp index 8290ffb3b..a901e2a3f 100644 --- a/src/pipeline/node/internal/PipelineEventAggregation.cpp +++ b/src/pipeline/node/internal/PipelineEventAggregation.cpp @@ -557,8 +557,11 @@ void PipelineEventAggregation::run() { std::this_thread::sleep_for(std::chrono::seconds(config->repeatIntervalSeconds.value()) - duration); } }); + currentConfig = std::nullopt; + continue; } - if(gotConfig || (currentConfig.has_value() && currentConfig->repeatIntervalSeconds.has_value())) { + auto now = std::chrono::steady_clock::now(); + if(gotConfig || (currentConfig.has_value() && currentConfig->repeatIntervalSeconds.has_value() && (now - lastSentTime) >= std::chrono::seconds(currentConfig->repeatIntervalSeconds.value()))) { bool sendEvents = false; if(currentConfig.has_value()) { for(const auto& nodeCfg : currentConfig->nodes) { @@ -569,7 +572,6 @@ void PipelineEventAggregation::run() { } } auto [outState, updated] = makeOutputState(handler, currentConfig, sequenceNum++, sendEvents); - auto now = std::chrono::steady_clock::now(); if(gotConfig || (currentConfig.has_value() && currentConfig->repeatIntervalSeconds.has_value() && updated && (now - lastSentTime) >= std::chrono::seconds(currentConfig->repeatIntervalSeconds.value()))) { diff --git a/src/pipeline/node/internal/PipelineStateMerge.cpp b/src/pipeline/node/internal/PipelineStateMerge.cpp index be06f1543..5257a7ae6 100644 --- a/src/pipeline/node/internal/PipelineStateMerge.cpp +++ b/src/pipeline/node/internal/PipelineStateMerge.cpp @@ -14,7 +14,7 @@ std::shared_ptr PipelineStateMerge::build(bool hasDeviceNode } PipelineStateMerge& PipelineStateMerge::setAllowConfiguration(bool allow) { - this->allowConfiguration = allow; + this->allowReconfiguration = allow; return *this; } @@ -40,7 +40,9 @@ void PipelineStateMerge::run() { while(mainLoop()) { auto outState = std::make_shared(); bool waitForMatch = false; - if(allowConfiguration && (!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeatIntervalSeconds.has_value()) || request.has())) { + if((allowReconfiguration + && (!currentConfig.has_value() || (currentConfig.has_value() && !currentConfig->repeatIntervalSeconds.has_value()) || request.has())) + || (!allowReconfiguration && !currentConfig.has_value())) { auto req = request.get(); if(req != nullptr) { currentConfig = *req; diff --git a/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp b/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp index 3240143b4..fc13dea17 100644 --- a/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp +++ b/tests/src/ondevice_tests/pipeline_debugging_rvc4_test.cpp @@ -5,10 +5,28 @@ #include #include "depthai/depthai.hpp" +#include "utility/Environment.hpp" #define VIDEO_DURATION_SECONDS 5 TEST_CASE("Object Tracker Pipeline Debugging") { + std::vector skipNodeIds; + if(!dai::utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false)) { + skipNodeIds = { + 9, // XLinkOutHost of merge outRequest + 10, // XLinkIn of aggregation request + 11, // XLinkOut of aggregation out + 12, // XLinkInHost of merge inputDevice + }; + } else { + skipNodeIds = { + 11, + 12, + 15, + 16, + }; + } + // Create pipeline dai::Pipeline pipeline; pipeline.enablePipelineDebugging(); @@ -49,10 +67,7 @@ TEST_CASE("Object Tracker Pipeline Debugging") { auto state = pipeline.getPipelineState().nodes().detailed(); for(const auto& [nodeId, nodeState] : state.nodeStates) { - if(nodeId == 9) continue; // XLinkOutHost of merge outRequest - if(nodeId == 12) continue; // XLinkInHost of merge inputDevice - if(nodeId == 10) continue; // XLinkIn of aggregation request - if(nodeId == 11) continue; // XLinkOut of aggregation out + if(std::find(skipNodeIds.begin(), skipNodeIds.end(), nodeId) != skipNodeIds.end()) continue; auto node = pipeline.getNode(nodeId); REQUIRE(nodeState.mainLoopTiming.isValid()); diff --git a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp index 8f92914cb..707291969 100644 --- a/tests/src/onhost_tests/pipeline_debugging_host_test.cpp +++ b/tests/src/onhost_tests/pipeline_debugging_host_test.cpp @@ -583,7 +583,8 @@ TEST_CASE("State callback test") { { std::lock_guard lock(mtx); - REQUIRE(callbackCount >= 5); // At least 3 callbacks in 5 seconds + REQUIRE(callbackCount >= 5); // At least 5 callbacks in 8 seconds + REQUIRE(callbackCount <= 11); // At most 11 callbacks in 8 seconds } ph.stop(); From 816c1e772ff9d9254c0023b4792751c148170f3b Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 27 Nov 2025 11:18:56 +0100 Subject: [PATCH 113/124] RVC4 FW: Merge develop --- cmake/Depthai/DepthaiDeviceRVC4Config.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake index 67b8892e6..c3dddadc9 100644 --- a/cmake/Depthai/DepthaiDeviceRVC4Config.cmake +++ b/cmake/Depthai/DepthaiDeviceRVC4Config.cmake @@ -3,4 +3,4 @@ set(DEPTHAI_DEVICE_RVC4_MATURITY "snapshot") # "version if applicable" -set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+d70eb5c515369ca6e2eab8ad52b7391afb1e0d23") +set(DEPTHAI_DEVICE_RVC4_VERSION "0.0.1+eaa6f94d045ad1ee034ce05976a68d6374ecfa8c") From 1a8c6d3db6ed5e7e1029b5f29d7fa03e8b3519ae Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 27 Nov 2025 16:40:03 +0100 Subject: [PATCH 114/124] Improve rvc2 pipeline debugging test --- .../pipeline_debugging_rvc2_test.cpp | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/src/ondevice_tests/pipeline_debugging_rvc2_test.cpp b/tests/src/ondevice_tests/pipeline_debugging_rvc2_test.cpp index 00846ed80..7d4c3f994 100644 --- a/tests/src/ondevice_tests/pipeline_debugging_rvc2_test.cpp +++ b/tests/src/ondevice_tests/pipeline_debugging_rvc2_test.cpp @@ -5,10 +5,28 @@ #include #include "depthai/depthai.hpp" +#include "utility/Environment.hpp" #define VIDEO_DURATION_SECONDS 5 TEST_CASE("Object Tracker Pipeline Debugging") { + std::vector skipNodeIds; + if(!dai::utility::getEnvAs("DEPTHAI_PIPELINE_DEBUGGING", false)) { + skipNodeIds = { + 9, // XLinkOutHost of merge outRequest + 10, // XLinkIn of aggregation request + 11, // XLinkOut of aggregation out + 12, // XLinkInHost of merge inputDevice + }; + } else { + skipNodeIds = { + 11, + 12, + 15, + 16, + }; + } + // Create pipeline dai::Pipeline pipeline; pipeline.enablePipelineDebugging(); @@ -49,8 +67,9 @@ TEST_CASE("Object Tracker Pipeline Debugging") { auto state = pipeline.getPipelineState().nodes().detailed(); for(const auto& [nodeId, nodeState] : state.nodeStates) { + if(std::find(skipNodeIds.begin(), skipNodeIds.end(), nodeId) != skipNodeIds.end()) continue; + auto node = pipeline.getNode(nodeId); - if(node->id == 11) continue; // REQUIRE(nodeState.mainLoopTiming.isValid()); // if(!node->getInputs().empty()) REQUIRE(nodeState.inputsGetTiming.isValid()); // if(!node->getOutputs().empty()) REQUIRE(nodeState.outputsSendTiming.isValid()); From 4eb828df7ce8df3bf11720cd40415bd96e2b5b46 Mon Sep 17 00:00:00 2001 From: asahtik Date: Thu, 27 Nov 2025 17:15:07 +0100 Subject: [PATCH 115/124] RVC2 FW: fix pipeline debugging with trace output enabled --- cmake/Depthai/DepthaiDeviceSideConfig.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Depthai/DepthaiDeviceSideConfig.cmake b/cmake/Depthai/DepthaiDeviceSideConfig.cmake index 088b10d97..db55ce20c 100644 --- a/cmake/Depthai/DepthaiDeviceSideConfig.cmake +++ b/cmake/Depthai/DepthaiDeviceSideConfig.cmake @@ -2,7 +2,7 @@ set(DEPTHAI_DEVICE_SIDE_MATURITY "snapshot") # "full commit hash of device side binary" -set(DEPTHAI_DEVICE_SIDE_COMMIT "b4d87628a16909866820da9dd7f42e5a785716dd") +set(DEPTHAI_DEVICE_SIDE_COMMIT "4ccbaacbb4c3f01f889c674dce537089a2cd09bd") # "version if applicable" set(DEPTHAI_DEVICE_SIDE_VERSION "") From dc78276798862221e58fcf902242f69291ef3c3b Mon Sep 17 00:00:00 2001 From: asahtik Date: Fri, 28 Nov 2025 08:34:19 +0100 Subject: [PATCH 116/124] Clangformat --- bindings/python/src/pybind11_common.hpp | 2 +- include/depthai/pipeline/Node.hpp | 4 +- include/depthai/utility/ImageManipImpl.hpp | 39 +++++++------------ include/depthai/utility/LockingQueue.hpp | 4 +- include/depthai/utility/MemoryWrappers.hpp | 2 +- .../depthai/utility/NlohmannJsonCompat.hpp | 4 +- src/device/DeviceBase.cpp | 4 +- src/pipeline/Node.cpp | 4 +- src/pipeline/node/DetectionNetwork.cpp | 7 +--- .../internal/PipelineEventAggregation.cpp | 9 ++++- src/utility/MemoryWrappers.cpp | 2 +- src/utility/ObjectTrackerImpl.cpp | 2 +- src/utility/Platform.cpp | 6 +-- tests/src/ondevice_tests/filesystem_test.cpp | 2 +- .../pipeline_debugging_rvc2_test.cpp | 8 ++-- .../pipeline_debugging_rvc4_test.cpp | 8 ++-- .../pipeline_debugging_host_test.cpp | 2 +- 17 files changed, 50 insertions(+), 59 deletions(-) diff --git a/bindings/python/src/pybind11_common.hpp b/bindings/python/src/pybind11_common.hpp index ed09b4879..e0f3db10f 100644 --- a/bindings/python/src/pybind11_common.hpp +++ b/bindings/python/src/pybind11_common.hpp @@ -1,6 +1,6 @@ #pragma once -#if(_MSC_VER >= 1910) || !defined(_MSC_VER) +#if (_MSC_VER >= 1910) || !defined(_MSC_VER) #ifndef HAVE_SNPRINTF #define HAVE_SNPRINTF #endif diff --git a/include/depthai/pipeline/Node.hpp b/include/depthai/pipeline/Node.hpp index 85d826a3f..5ef755ee7 100644 --- a/include/depthai/pipeline/Node.hpp +++ b/include/depthai/pipeline/Node.hpp @@ -66,7 +66,9 @@ class Node : public std::enable_shared_from_this { static constexpr auto DEFAULT_NAME = ""; #define DEFAULT_TYPES \ { \ - { DatatypeEnum::Buffer, true } \ + { \ + DatatypeEnum::Buffer, true \ + } \ } static constexpr auto DEFAULT_BLOCKING = true; static constexpr auto DEFAULT_QUEUE_SIZE = 3; diff --git a/include/depthai/utility/ImageManipImpl.hpp b/include/depthai/utility/ImageManipImpl.hpp index 5472ffd4d..c8cd81cbc 100644 --- a/include/depthai/utility/ImageManipImpl.hpp +++ b/include/depthai/utility/ImageManipImpl.hpp @@ -465,8 +465,7 @@ class ColorChange { template