diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
new file mode 100644
index 0000000..0e22473
--- /dev/null
+++ b/.claude-plugin/marketplace.json
@@ -0,0 +1,16 @@
+{
+ "name": "scopeone",
+ "owner": {
+ "name": "ScopeOne Project"
+ },
+ "description": "Claude Code plugins for ScopeOne microscopy automation",
+ "plugins": [
+ {
+ "name": "scopeone",
+ "displayName": "ScopeOne",
+ "source": "./scopeone-skill",
+ "description": "Control ScopeOne through its bundled MCP server",
+ "homepage": "https://github.com/Experimental-Microscopy-Lab/ScopeOne"
+ }
+ ]
+}
diff --git a/.github/workflows/compile-check.yml b/.github/workflows/compile-check.yml
index 4fecd1d..b00bdeb 100644
--- a/.github/workflows/compile-check.yml
+++ b/.github/workflows/compile-check.yml
@@ -44,6 +44,6 @@ jobs:
libtiff-dev \
zlib1g-dev
- - name: Compile MMCore and ScopeOne
+ - name: Compile mmCoreAndDevices and ScopeOne
shell: bash
run: ./scripts/build-linux.sh
diff --git a/.gitignore b/.gitignore
index 6f20162..979af84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,12 @@ ScopeOneCore/python/**
!.github/
!.github/**
+!.claude-plugin/
+!.claude-plugin/**
+
+!scopeone-skill/
+!scopeone-skill/**
+
!resources/
!resources/**
diff --git a/README.md b/README.md
index addfc9b..e9e5061 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
-ScopeOne is an open-source microscopy control software for multi-camera imaging, originally developed for in-house lab use. Built with C++ and Qt, it uses a multi-process architecture where each camera runs in its own [MMCore](https://github.com/micro-manager/mmCoreAndDevices) instance, enabling simultaneous preview and acquisition across multiple cameras.
+ScopeOne is an open-source microscopy control software for multi-camera imaging, originally developed for in-house lab use. Built with C++ and Qt, it uses a native [MMCore](https://github.com/micro-manager/mmCoreAndDevices) backend for single-camera operation and one isolated camera agent per device for simultaneous multi-camera preview and acquisition.
It retains full compatibility with the [Micro-Manager](https://micro-manager.org/) device ecosystem and adds a modular real-time image processing pipeline with support for background calibration, temporal filtering, FFT analysis, and more.
@@ -154,6 +154,20 @@ The API reports which operations mutate hardware, write files, remove state, or
`ScopeOneMcpServer` is a standalone C++ MCP server included with ScopeOne. It uses MCP protocol version `2025-06-18` over standard input and output and forwards validated tool calls to the running desktop app through the Local API. It does not embed a model, open a network port, or depend on Python.
+#### Claude Code plugin
+
+Install ScopeOne first, then run these commands inside Claude Code:
+
+```text
+/plugin marketplace add Experimental-Microscopy-Lab/ScopeOne
+/plugin install scopeone@scopeone
+/reload-plugins
+```
+
+When prompted, select `ScopeOneMcpServer.exe` from the same directory as `ScopeOne.exe`. Start ScopeOne before asking Claude to inspect or control it. The plugin supplies the MCP configuration and agent operating guidance, so no Python installation or manual MCP server launch is required.
+
+#### Other MCP hosts
+
To use it:
1. Start ScopeOne. Load the device configuration manually or ask the agent to load it after confirmation.
@@ -173,7 +187,8 @@ The MCP tool set mirrors the Local API operation catalog, including system state
The current validation list is still short, but the codebase has been cleaned to remove early hard-coded device assumptions. In principle, ScopeOne should follow Micro-Manager device compatibility.
## 🖥️ Tested System Configurations
-- Windows 10, Dual Intel(R) Xeon(R) E5-2637 v3, 64 GB RAM, NVIDIA Quadro K620
-- Windows 11, Intel(R) Core(TM) Ultra 5 125U, 64 GB RAM
+- Windows 10 Version 21H2, Dual Intel(R) Xeon(R) E5-2637 v3, 64 GB RAM, NVIDIA Quadro K620
+- Windows 11 Version 25H2, Intel(R) Core(TM) Ultra 5 125U, 64 GB RAM
+- Fedora Linux 44 (Workstation Edition), Intel(R) Core(TM) i7-7700, 32 GB RAM
-Although these machines are older and weaker than many typical lab computers, ScopeOne still provides smooth real-time preview and processing on them.
+We build and test ScopeOne on the above machines, which are comparatively older and weaker than many typical optical lab computers. However, ScopeOne still provides smooth real-time preview and processing on them.
diff --git a/ScopeOneCore/CMakeLists.txt b/ScopeOneCore/CMakeLists.txt
index b2a23c2..6ee40b5 100644
--- a/ScopeOneCore/CMakeLists.txt
+++ b/ScopeOneCore/CMakeLists.txt
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
-project(ScopeOneCore VERSION 2.0.719 LANGUAGES CXX)
+project(ScopeOneCore VERSION 2.0.722 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_AUTOMOC ON)
@@ -108,7 +108,10 @@ set(CORE_SOURCES
src/DifferentialRollingModule.cpp
src/MDAManager.cpp
src/RecordingManager.cpp
- src/MultiProcessCameraManager.cpp
+ src/CameraBackend.cpp
+ src/CameraManager.cpp
+ src/NativeCameraBackend.cpp
+ src/AgentCameraBackend.cpp
src/ScopeOneCore.cpp
)
@@ -129,7 +132,8 @@ set(CORE_HEADERS
internal/DifferentialRollingModule.h
internal/MDAManager.h
internal/RecordingManager.h
- internal/MultiProcessCameraManager.h
+ internal/CameraBackend.h
+ internal/CameraManager.h
include/scopeone/ImageFrame.h
include/scopeone/SharedFrame.h
include/scopeone/scopeone_core_export.h
diff --git a/ScopeOneCore/README.md b/ScopeOneCore/README.md
index 360529f..234b8da 100644
--- a/ScopeOneCore/README.md
+++ b/ScopeOneCore/README.md
@@ -43,7 +43,7 @@ Use this placement rule:
| Namespace | Purpose | Examples |
|---|---|---|
| `scopeone::core` | Stable Core-facing types and public facades | `ScopeOneCore`, `ImageFrame`, `ExperimentDocument`, `ImageSceneModel` |
-| `scopeone::core::internal` | Core-only managers and processing implementations | `MMCoreManager`, `RecordingManager`, processing modules |
+| `scopeone::core::internal` | Core-only managers and processing implementations | `CameraManager`, `MMCoreManager`, `RecordingManager`, processing modules |
| `scopeone::core::internal::agent` | Private camera-agent protocol details | Agent request, response and frame transport types |
| `scopeone::ui` | Desktop application widgets and UI coordination outside this library | `MainWindow`, `PreviewWidget`, `InspectWidget` |
diff --git a/ScopeOneCore/include/scopeone/ScopeOneCore.h b/ScopeOneCore/include/scopeone/ScopeOneCore.h
index 70ae169..c0260ab 100644
--- a/ScopeOneCore/include/scopeone/ScopeOneCore.h
+++ b/ScopeOneCore/include/scopeone/ScopeOneCore.h
@@ -3,6 +3,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -21,6 +22,8 @@
#include "scopeone/scopeone_core_export.h"
class CMMCore;
+class QThreadPool;
+class QTimer;
namespace scopeone::core
{
@@ -620,6 +623,7 @@ namespace scopeone::core
void clearProcessedFrames();
bool getRawImageStatistics(const QString& cameraId, HistogramStats& stats) const;
bool getLayerHistogram(const QString& layerKey, HistogramStats& stats) const;
+ void setActiveHistogramLayer(const QString& layerKey);
bool autoLayerLevels(const QString& layerKey);
bool fullLayerLevels(const QString& layerKey);
bool setLayerAutoStretchEnabled(const QString& layerKey, bool enabled);
@@ -726,10 +730,12 @@ namespace scopeone::core
void deviceStateChanged();
void stagePositionChanged();
void newRawFrameReady(const ImageFrame& frame);
+ void rawFramesAcquired(const QString& cameraId, quint64 frameCount);
void previewRawFrameReady(const ImageFrame& frame);
void previewStateChanged(bool running);
void agentControlServerListening(const QString& cameraId, const QString& serverName);
void processedFrameReady(const ImageFrame& frame);
+ void processedFramesCompleted(const QString& cameraId, quint64 frameCount);
void previewProcessedFrameReady(const ImageFrame& frame);
void staticFramePublished(const QString& sourceId,
const QString& displayName,
@@ -825,6 +831,7 @@ namespace scopeone::core
struct HistogramJobState
{
bool inFlight{false};
+ bool retryScheduled{false};
qint64 lastScheduledMs{0};
quint64 activeSequence{0};
ImageFrame queuedFrame;
@@ -834,7 +841,7 @@ namespace scopeone::core
LoadConfigResult* result,
QString* errorMessage);
std::shared_ptr core() const;
- bool isAgentCamera(const QString& deviceLabel) const;
+ bool isConfiguredCamera(const QString& deviceLabel) const;
bool isNativeCamera(const QString& deviceLabel) const;
bool isPropertyPreInit(const QString& deviceLabel, const QString& name) const;
void ensureSceneLayer(const QString& layerKey,
@@ -845,13 +852,12 @@ namespace scopeone::core
void processGraphRawFrameAsync(const ImageFrame& frame);
void queuePreviewRawFrame(const ImageFrame& frame);
void queuePreviewProcessedFrame(const ImageFrame& frame);
- void flushPreviewRawFrames();
- void flushPreviewProcessedFrames();
+ void schedulePreviewFlush();
+ void flushPreviewFrames();
QString sessionFrameSourceId(const std::shared_ptr& session) const;
bool publishSessionFrameSource(const std::shared_ptr& session);
- void scheduleHistogramStats(const QString& cameraId,
- bool processed,
- const ImageFrame& frame);
+ bool histogramUpdatesEnabled(const QString& layerKey) const;
+ void scheduleHistogramStats(const QString& layerKey, const ImageFrame& frame);
void clearLayerAnalysis(const QString& layerKey);
void clearLayerAnalysisByPrefix(const QString& prefix);
void updateLineProfile(const QString& cameraId,
@@ -891,9 +897,12 @@ namespace scopeone::core
QHash m_pendingPreviewProcessedFrames;
QHash m_histogramJobStates;
QHash m_latestHistogramStats;
+ std::unique_ptr m_histogramThreadPool;
+ QString m_activeHistogramLayerKey;
quint64 m_nextHistogramSequence{0};
- bool m_previewRawFlushQueued{false};
- bool m_previewProcessedFlushQueued{false};
+ QElapsedTimer m_lineProfileUpdateTimer;
+ QElapsedTimer m_previewPublishTimer;
+ QTimer* m_previewFlushTimer{nullptr};
QSet m_sessionsSaving;
};
}
diff --git a/ScopeOneCore/include/scopeone/SharedFrame.h b/ScopeOneCore/include/scopeone/SharedFrame.h
index 770acef..eeadebb 100644
--- a/ScopeOneCore/include/scopeone/SharedFrame.h
+++ b/ScopeOneCore/include/scopeone/SharedFrame.h
@@ -76,8 +76,8 @@ namespace scopeone::core
}
}
- inline constexpr int kSharedFrameNumSlots = 12;
- inline constexpr int kSharedFrameMaxBytes = 16 * 2048 * 2048;
+ inline constexpr int kSharedFrameNumSlots = 32;
+ inline constexpr int kSharedFrameMaxBytes = 4 * 2048 * 2048;
inline constexpr int kSharedMemoryControlSize = 64;
inline constexpr int kSharedFrameHeaderSize = static_cast(sizeof(SharedFrameHeader));
inline constexpr int kSharedFrameSlotStride = kSharedFrameHeaderSize + kSharedFrameMaxBytes;
diff --git a/ScopeOneCore/internal/AgentProtocol.h b/ScopeOneCore/internal/AgentProtocol.h
index 81d8d55..ae1d07c 100644
--- a/ScopeOneCore/internal/AgentProtocol.h
+++ b/ScopeOneCore/internal/AgentProtocol.h
@@ -8,7 +8,7 @@
namespace scopeone::core::internal::agent
{
- inline constexpr quint32 kProtocolVersion = 1;
+ inline constexpr quint32 kProtocolVersion = 3;
inline constexpr quint32 kMaxControlMessageBytes = 256 * 1024;
inline const QString kEnvelopeKindField = QStringLiteral("kind");
@@ -23,6 +23,7 @@ namespace scopeone::core::internal::agent
inline const QString kCommandShutdown = QStringLiteral("Shutdown");
inline const QString kCommandStartPreview = QStringLiteral("StartPreview");
inline const QString kCommandStopPreview = QStringLiteral("StopPreview");
+ inline const QString kCommandSetFrameDeliveryMode = QStringLiteral("SetFrameDeliveryMode");
inline const QString kCommandSetExposure = QStringLiteral("SetExposure");
inline const QString kCommandListProperties = QStringLiteral("ListProperties");
inline const QString kCommandGetProperty = QStringLiteral("GetProperty");
@@ -32,11 +33,14 @@ namespace scopeone::core::internal::agent
inline const QString kCommandClearRoi = QStringLiteral("ClearROI");
inline const QString kCommandGetRoi = QStringLiteral("GetROI");
+ inline const QString kFrameDeliveryModePreviewLatest = QStringLiteral("PreviewLatest");
+ inline const QString kFrameDeliveryModeLatestOnly = QStringLiteral("LatestOnly");
+ inline const QString kFrameDeliveryModeAllFrames = QStringLiteral("AllFrames");
+
inline const QString kEventHello = QStringLiteral("Hello");
inline const QString kEventFrameAvailable = QStringLiteral("FrameAvailable");
inline const QString kEventPreviewState = QStringLiteral("PreviewState");
inline const QString kEventAgentError = QStringLiteral("AgentError");
- inline const QString kEventBufferOverflow = QStringLiteral("BufferOverflow");
inline const QString kExecutableFileName = QStringLiteral("ScopeOne_Agent.exe");
diff --git a/ScopeOneCore/internal/BackgroundCalibrationModule.h b/ScopeOneCore/internal/BackgroundCalibrationModule.h
index 15b1a02..dd9285d 100644
--- a/ScopeOneCore/internal/BackgroundCalibrationModule.h
+++ b/ScopeOneCore/internal/BackgroundCalibrationModule.h
@@ -2,6 +2,7 @@
#include "internal/ProcessingModule.h"
#include
+#include
namespace scopeone::core::internal
{
@@ -41,9 +42,12 @@ namespace scopeone::core::internal
private:
void resetCalibration();
void computeBackground();
+ ProcessingResult processRunningMean(const ImageFrame& sourceFrame,
+ const ImageFrame& workingFrame);
int m_calibrationFrames{101};
std::deque m_buffer;
+ std::vector m_runningSum;
ImageFrame m_background;
bool m_calibrated{false};
BackgroundOperation m_operation{BackgroundOperation::Subtract};
diff --git a/ScopeOneCore/internal/CameraBackend.h b/ScopeOneCore/internal/CameraBackend.h
new file mode 100644
index 0000000..5f71fea
--- /dev/null
+++ b/ScopeOneCore/internal/CameraBackend.h
@@ -0,0 +1,140 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "scopeone/ImageFrame.h"
+
+class CMMCore;
+
+namespace scopeone::core::internal
+{
+ struct CameraPropertyReadback
+ {
+ QString value;
+ QString type{QStringLiteral("Unknown")};
+ bool readOnly{true};
+ bool preInit{false};
+ QStringList allowedValues;
+ bool hasLimits{false};
+ double lowerLimit{0.0};
+ double upperLimit{0.0};
+ };
+
+ class CameraBackend : public QObject
+ {
+ Q_OBJECT
+
+ public:
+ enum class Kind
+ {
+ Native,
+ Agent
+ };
+
+ explicit CameraBackend(QObject* parent = nullptr);
+ ~CameraBackend() override;
+
+ virtual Kind kind() const = 0;
+
+ virtual bool configureNativeCamera(const std::shared_ptr& core,
+ const QString& cameraId,
+ double exposureMs);
+ virtual bool addAgentCamera(const QString& cameraId,
+ const QString& adapter,
+ const QString& device,
+ const QStringList& preInitProperties,
+ const QStringList& properties,
+ double exposureMs);
+
+ virtual bool startPreview() = 0;
+ virtual bool stopPreview() = 0;
+ virtual bool startPreviewFor(const QString& cameraId) = 0;
+ virtual bool stopPreviewFor(const QString& cameraId) = 0;
+ virtual bool isPreviewRunning(const QString& cameraId) const = 0;
+ virtual void setFrameDeliveryPaused(bool paused) = 0;
+ virtual bool captureEventFrame(const QString& cameraId,
+ scopeone::core::ImageFrame& frame,
+ int timeoutMs);
+
+ virtual bool setRecordingFrameDeliveryEnabled(bool enabled);
+ virtual bool setHighRateFrameDeliveryEnabled(bool enabled);
+
+ bool getExposure(const QString& cameraIdOrAll, double& exposureMs) const;
+ bool setExposure(const QString& cameraIdOrAll, double exposureMs);
+ QStringList listProperties(const QString& cameraId);
+ bool readPropertyDetails(const QString& cameraId,
+ const QString& name,
+ bool fromCache,
+ CameraPropertyReadback& readback);
+ bool setProperty(const QString& cameraId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage);
+ bool setROI(const QString& cameraId, int x, int y, int width, int height);
+ bool clearROI(const QString& cameraId);
+ bool getROI(const QString& cameraId, int& x, int& y, int& width, int& height);
+
+ signals:
+ void rawFrameReady(const scopeone::core::ImageFrame& frame);
+ void rawFramesAcquired(const QString& cameraId, quint64 frameCount);
+ void recordingFramesReady(const QList& frames);
+ void frameDeliveryFailed(const QString& errorMessage);
+ void previewStateChanged(bool running);
+ void agentControlServerListening(const QString& cameraId, const QString& serverName);
+
+ protected:
+ bool applyWithPreviewRestart(const QString& cameraId, const std::function& operation);
+ void notifyPreviewStarted(const QString& cameraId);
+ void notifyPreviewStopped();
+ void submitFrames(const QList& frames, quint64 acquiredFrameCount);
+ void discardPendingPreviewFrames();
+ bool recordingFrameDeliveryEnabled() const;
+ bool highRateFrameDeliveryEnabled() const;
+
+ virtual bool hasRunningCamera() const = 0;
+ virtual bool resolvePrimaryCameraId(const QString& cameraIdOrAll, QString& cameraId) const = 0;
+ virtual QStringList resolveTargetCameraIds(const QString& cameraIdOrAll) const = 0;
+ virtual bool readExposureFor(const QString& cameraId, double& exposureMs) const = 0;
+ virtual bool writeExposureFor(const QString& cameraId, double exposureMs) = 0;
+ virtual QStringList listPropertiesFor(const QString& cameraId) = 0;
+ virtual bool readPropertyDetailsFor(const QString& cameraId,
+ const QString& name,
+ bool fromCache,
+ CameraPropertyReadback& readback) = 0;
+ virtual bool setPropertyFor(const QString& cameraId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage) = 0;
+ virtual bool setROIFor(const QString& cameraId, int x, int y, int width, int height) = 0;
+ virtual bool clearROIFor(const QString& cameraId) = 0;
+ virtual bool getROIFor(const QString& cameraId,
+ int& x,
+ int& y,
+ int& width,
+ int& height) = 0;
+
+ private:
+ void flushPendingFrames();
+
+ mutable QMutex m_frameDeliveryMutex;
+ QHash m_pendingLatestFrames;
+ QHash m_pendingAcquiredFrameCounts;
+ QList m_pendingRecordingFrames;
+ qint64 m_pendingRecordingBytes{0};
+ QString m_pendingDeliveryError;
+ std::atomic_bool m_recordingFrameDeliveryEnabled{false};
+ std::atomic_bool m_highRateFrameDeliveryEnabled{false};
+ bool m_frameFlushQueued{false};
+ };
+
+ std::unique_ptr createNativeCameraBackend();
+ std::unique_ptr createAgentCameraBackend();
+}
diff --git a/ScopeOneCore/internal/CameraManager.h b/ScopeOneCore/internal/CameraManager.h
new file mode 100644
index 0000000..f04f6c0
--- /dev/null
+++ b/ScopeOneCore/internal/CameraManager.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "internal/CameraBackend.h"
+
+class CMMCore;
+
+namespace scopeone::core::internal
+{
+ class CameraManager : public QObject
+ {
+ Q_OBJECT
+
+ public:
+ explicit CameraManager(QObject* parent = nullptr);
+ ~CameraManager() override;
+
+ bool configureNativeCamera(const std::shared_ptr& core,
+ const QString& cameraId,
+ double exposureMs = 0.0);
+ bool addAgentCamera(const QString& cameraId,
+ const QString& adapter,
+ const QString& device,
+ const QStringList& preInitProperties = QStringList(),
+ const QStringList& properties = QStringList(),
+ double exposureMs = 0.0);
+ void shutdown();
+
+ bool startPreview();
+ bool stopPreview();
+ bool startPreviewFor(const QString& cameraId);
+ bool stopPreviewFor(const QString& cameraId);
+ bool isPreviewRunning(const QString& cameraId) const;
+ void setFrameDeliveryPaused(bool paused);
+ bool setRecordingFrameDeliveryEnabled(bool enabled);
+ bool setHighRateFrameDeliveryEnabled(bool enabled);
+
+ bool getExposure(const QString& cameraIdOrAll, double& exposureMs) const;
+ bool setExposure(const QString& cameraIdOrAll, double exposureMs);
+ QStringList listProperties(const QString& cameraId);
+ QString getProperty(const QString& cameraId,
+ const QString& name,
+ bool fromCache = false);
+ bool setProperty(const QString& cameraId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage = nullptr);
+ QString getPropertyType(const QString& cameraId, const QString& name);
+ bool isPropertyReadOnly(const QString& cameraId, const QString& name);
+ bool isPropertyPreInit(const QString& cameraId, const QString& name);
+ QStringList getAllowedPropertyValues(const QString& cameraId, const QString& name);
+ bool hasPropertyLimits(const QString& cameraId, const QString& name);
+ double getPropertyLowerLimit(const QString& cameraId, const QString& name);
+ double getPropertyUpperLimit(const QString& cameraId, const QString& name);
+
+ bool setROI(const QString& cameraId, int x, int y, int width, int height);
+ bool clearROI(const QString& cameraId);
+ bool getROI(const QString& cameraId, int& x, int& y, int& width, int& height);
+ bool captureEventFrame(const QString& cameraId,
+ scopeone::core::ImageFrame& frame,
+ int timeoutMs = 1500);
+
+ signals:
+ void newRawFrameReady(const scopeone::core::ImageFrame& frame);
+ void rawFramesAcquired(const QString& cameraId, quint64 frameCount);
+ void recordingFramesReady(const QList& frames);
+ void frameDeliveryFailed(const QString& errorMessage);
+ void previewStateChanged(bool running);
+ void agentControlServerListening(const QString& cameraId, const QString& serverName);
+
+ private:
+ bool activateBackend(CameraBackend::Kind kind);
+
+ std::unique_ptr m_backend;
+ bool m_recordingFrameDeliveryEnabled{false};
+ bool m_highRateFrameDeliveryEnabled{false};
+ QMap m_propertyDetailsCache;
+ };
+}
diff --git a/ScopeOneCore/internal/FrameBufferUtils.h b/ScopeOneCore/internal/FrameBufferUtils.h
index e43cf84..233944e 100644
--- a/ScopeOneCore/internal/FrameBufferUtils.h
+++ b/ScopeOneCore/internal/FrameBufferUtils.h
@@ -3,6 +3,7 @@
#include "scopeone/ImageFrame.h"
#include
+#include
#include
namespace cv
@@ -65,16 +66,34 @@ namespace scopeone::core::internal
}
template
- inline Pixel* mutableRowData(QByteArray& bytes, int width, int y)
+ inline Pixel clampPixelValue(int value, int maxValue)
{
- return reinterpret_cast(
- bytes.data() + static_cast(y) * static_cast(width) * static_cast(sizeof(Pixel)));
+ return static_cast(qBound(0, value, maxValue));
}
- template
- inline Pixel clampPixelValue(int value, int maxValue)
+ template
+ inline void parallelForRows(qint64 workItemCount, int rowCount, Handler&& handler)
{
- return static_cast(qBound(0, value, maxValue));
+ constexpr qint64 kMinimumParallelWorkItems = 1024ll * 1024ll;
+ if (rowCount <= 0 || workItemCount < kMinimumParallelWorkItems)
+ {
+ handler(0, qMax(0, rowCount));
+ return;
+ }
+
+ cv::parallel_for_(cv::Range(0, rowCount),
+ [&handler](const cv::Range& range)
+ {
+ handler(range.start, range.end);
+ });
+ }
+
+ template
+ inline void parallelForImageRows(int width, int height, Handler&& handler)
+ {
+ parallelForRows(static_cast(width) * height,
+ height,
+ std::forward(handler));
}
template
diff --git a/ScopeOneCore/internal/ImageProcessingFramework.h b/ScopeOneCore/internal/ImageProcessingFramework.h
index bb0244c..fded134 100644
--- a/ScopeOneCore/internal/ImageProcessingFramework.h
+++ b/ScopeOneCore/internal/ImageProcessingFramework.h
@@ -3,6 +3,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -67,15 +68,18 @@ namespace scopeone::core::internal
ImageFrame processFrameThrough(int endModuleIndex, const ImageFrame& frame);
signals:
- void imageProcessed(const ImageFrame& frame);
+ void imageProcessed(const ImageFrame& frame, quint64 completedFrameCount);
void processingError(const QString& error);
private:
struct CameraSlot
{
ImageFrame latestFrame;
+ quint64 latestFrameGeneration{0};
bool hasFrame{false};
bool processing{false};
+ ImageFrame latestProcessedFrame;
+ quint64 completedFrameCount{0};
};
QString getCameraKey(const ImageFrame& frame) const;
@@ -85,6 +89,7 @@ namespace scopeone::core::internal
QHash>& pipelines,
const QString& cameraKey);
void processCameraQueue(const QString& cameraKey);
+ void flushProcessedOutputs();
ImageFrame frameFromResult(ProcessingResult result);
ProcessingPipelineDefinition m_definition;
@@ -92,10 +97,11 @@ namespace scopeone::core::internal
QHash> m_offlinePipelines;
std::atomic m_realTimeEnabled;
std::atomic m_processingBitDepth{16};
- void submitFrame(const ImageFrame& frame);
mutable QMutex m_frameMutex;
QHash m_cameraSlots;
+ quint64 m_liveGeneration{1};
QThreadPool m_threadPool;
+ QTimer m_outputTimer;
};
}
diff --git a/ScopeOneCore/internal/MDAManager.h b/ScopeOneCore/internal/MDAManager.h
index ff821c7..2978e5c 100644
--- a/ScopeOneCore/internal/MDAManager.h
+++ b/ScopeOneCore/internal/MDAManager.h
@@ -15,7 +15,7 @@ class CMMCore;
namespace scopeone::core::internal
{
- class MultiProcessCameraManager;
+ class CameraManager;
struct MDAOutput
{
@@ -37,7 +37,7 @@ namespace scopeone::core::internal
bool isRunning() const { return m_running.load(); }
- void setMultiProcessCameraManager(MultiProcessCameraManager* mpcm);
+ void setCameraManager(CameraManager* cameraManager);
bool start(const QList& events, bool block = false);
void requestCancel();
@@ -58,7 +58,7 @@ namespace scopeone::core::internal
void runSequence(QList events);
std::shared_ptr m_mmcore;
- MultiProcessCameraManager* m_mpcm{nullptr};
+ CameraManager* m_cameraManager{nullptr};
QThreadPool m_threadPool;
std::atomic m_running{false};
std::atomic m_cancelRequested{false};
diff --git a/ScopeOneCore/internal/MMCoreManager.h b/ScopeOneCore/internal/MMCoreManager.h
index 6ffce9b..8a4d932 100644
--- a/ScopeOneCore/internal/MMCoreManager.h
+++ b/ScopeOneCore/internal/MMCoreManager.h
@@ -7,7 +7,7 @@
namespace scopeone::core::internal
{
- class MultiProcessCameraManager;
+ class CameraManager;
enum class DeviceType
{
@@ -52,7 +52,7 @@ namespace scopeone::core::internal
QString getDeviceTypeString(DeviceType type) const;
bool loadConfigurationAndStartCameras(const QString& configPath,
- MultiProcessCameraManager* mpcm,
+ CameraManager* cameraManager,
LoadConfigResult* result,
QString* errorMessage);
diff --git a/ScopeOneCore/internal/MultiProcessCameraManager.h b/ScopeOneCore/internal/MultiProcessCameraManager.h
deleted file mode 100644
index bdb2b98..0000000
--- a/ScopeOneCore/internal/MultiProcessCameraManager.h
+++ /dev/null
@@ -1,123 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "scopeone/ImageFrame.h"
-
-class QJsonObject;
-class CMMCore;
-
-namespace scopeone::core::internal
-{
- class MultiProcessCameraManager : public QObject
- {
- Q_OBJECT
-
- public:
- explicit MultiProcessCameraManager(QObject* parent = nullptr);
- ~MultiProcessCameraManager();
-
- void setNativeCore(const std::shared_ptr& core);
- bool startSingleCamera(const QString& cameraId, double exposureMs = 0.0);
-
- void stopAgents();
-
- bool startAgentFor(const QString& cameraId, const QString& adapter, const QString& device,
- const QStringList& preInitProperties = QStringList(),
- const QStringList& properties = QStringList(),
- double exposureMs = 0.0);
- bool stopAgentFor(const QString& cameraId);
-
- bool startPreview();
- bool stopPreview();
- void setPollingPaused(bool paused);
-
- bool getExposure(const QString& cameraIdOrAll, double& exposureMs) const;
- bool setExposure(const QString& cameraIdOrAll, double exposureMs);
- bool startPreviewFor(const QString& cameraId);
- bool stopPreviewFor(const QString& cameraId);
- bool isPreviewRunning(const QString& cameraId) const;
- QStringList listProperties(const QString& cameraId);
- QString getProperty(const QString& cameraId, const QString& name);
- bool setProperty(const QString& cameraId,
- const QString& name,
- const QString& value,
- QString* errorMessage = nullptr);
- QString getPropertyType(const QString& cameraId, const QString& name);
- bool isPropertyReadOnly(const QString& cameraId, const QString& name);
- QStringList getAllowedPropertyValues(const QString& cameraId, const QString& name);
- bool hasPropertyLimits(const QString& cameraId, const QString& name);
- double getPropertyLowerLimit(const QString& cameraId, const QString& name);
- double getPropertyUpperLimit(const QString& cameraId, const QString& name);
-
- bool setROI(const QString& cameraId, int x, int y, int width, int height);
- bool clearROI(const QString& cameraId);
- bool getROI(const QString& cameraId, int& x, int& y, int& width, int& height);
- bool captureEventFrame(const QString& cameraId,
- scopeone::core::ImageFrame& frame,
- int timeoutMs = 1500);
-
- signals:
- void newRawFrameReady(const scopeone::core::ImageFrame& frame);
- void previewStateChanged(bool running);
- void agentControlServerListening(const QString& cameraId, const QString& serverName);
-
- private:
- struct CameraBackend;
- struct DeviceControlBackend;
- struct NativeSingleCameraBackend;
- struct AgentCameraBackend;
- struct ControlSession;
- struct CameraSlot;
-
- void pollSharedMemory();
- bool consumeAgentFrames(CameraSlot& slot, bool emitFrames);
- bool hasRunningCamera() const;
- void clearPropertyCaches();
- bool waitForControlReady(CameraSlot& slot, int timeoutMs);
-
- struct CameraSlot
- {
- QString cameraId;
- QString shmKey;
- std::shared_ptr shm;
- std::shared_ptr process;
- std::shared_ptr control;
- quint64 lastFrameIndex = 0;
- bool isRunning = false;
- double exposureMs = 10.0;
- scopeone::core::ImageFrame latestFrame;
- };
-
- bool ensureSharedMemory(CameraSlot& slot);
- bool readLatestFrame(CameraSlot& slot);
- void updatePollingInterval();
- bool isSingleCamera(const QString& cameraId) const;
- void pollSingleCamera(CameraSlot& slot);
- bool sendControlCommand(const QString& cameraId,
- const QJsonObject& request,
- QJsonObject* response,
- int timeoutMs = 1200);
-
- QMap> m_cameras;
- QTimer m_pollTimer;
- bool m_pollingPaused{false};
- std::shared_ptr m_nativeCore;
- QString m_singleCameraId;
- std::unique_ptr m_runtime;
-
- QMap m_propertyTypeCache;
- QMap m_propertyReadOnlyCache;
- QMap m_propertyAllowedValuesCache;
- QMap m_propertyHasLimitsCache;
- QMap m_propertyLowerLimitCache;
- QMap m_propertyUpperLimitCache;
- };
-}
diff --git a/ScopeOneCore/internal/RecordingManager.h b/ScopeOneCore/internal/RecordingManager.h
index 471ecf7..8c9483b 100644
--- a/ScopeOneCore/internal/RecordingManager.h
+++ b/ScopeOneCore/internal/RecordingManager.h
@@ -6,6 +6,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -27,7 +29,7 @@ namespace scopeone::core::internal
using RecordingWriterPhase = scopeone::core::ScopeOneCore::RecordingWriterPhase;
using RecordingWriterStatus = scopeone::core::ScopeOneCore::RecordingWriterStatus;
- class MultiProcessCameraManager;
+ class CameraManager;
class RecordingManager : public QObject
{
@@ -37,7 +39,7 @@ namespace scopeone::core::internal
explicit RecordingManager(QObject* parent = nullptr);
~RecordingManager() override;
- void setMultiProcessCameraManager(MultiProcessCameraManager* mpcm) { m_mpcm = mpcm; }
+ void setCameraManager(CameraManager* cameraManager) { m_cameraManager = cameraManager; }
void setMMCore(const std::shared_ptr& core) { m_mmcore = core; }
void setLatestFrameFetcher(std::function fetcher)
@@ -54,7 +56,8 @@ namespace scopeone::core::internal
bool isRecording() const { return m_captureState.isRecording; }
- void onNewRawFrameReady(const ImageFrame& frame);
+ void onRawFramesReady(const QList& frames);
+ void onFrameDeliveryFailed(const QString& errorMessage);
static QString saveSessionToDisk(const std::shared_ptr& session);
@@ -77,7 +80,6 @@ namespace scopeone::core::internal
double y,
bool hasZ,
double z);
- void bufferUsageChanged(qint64 pendingWriteBytes);
void writerStatusChanged(const RecordingWriterStatus& status);
void recordingStateChanged(bool isRecording);
void recordingStopped(const std::shared_ptr& session);
@@ -187,14 +189,14 @@ namespace scopeone::core::internal
QString& errorMessage);
void appendPreviewEventRecord(const ImageFrame& frame);
void primeLastFrameIndices();
- void emitProgress();
+ void emitProgress(bool force = false);
bool startStreamingOutputs(const ExperimentPlan& plan);
void stopStreamingOutputs(bool applyOutputManifest = false);
void requestWriterStop();
void writerLoop(const std::shared_ptr& output, quint64 generation);
bool writeTask(CameraOutput& output, const WriteTask& task, QString& errorMessage);
QString formatName(RecordingFormat format) const;
- void emitBufferUsageChanged(qint64 pendingWriteBytes);
+ void markWriterStatusDirty();
void emitWriterStatus();
void setWriterStatus(RecordingWriterPhase phase, const QString& errorMessage = QString());
QString writerErrorSnapshot() const;
@@ -214,7 +216,7 @@ namespace scopeone::core::internal
bool allCamerasReachedTarget() const;
bool advanceBurstStateIfNeeded();
- MultiProcessCameraManager* m_mpcm{nullptr};
+ CameraManager* m_cameraManager{nullptr};
std::shared_ptr m_mmcore;
std::function m_latestFrameFetcher;
@@ -222,5 +224,8 @@ namespace scopeone::core::internal
WriterState m_writerState;
CaptureState m_captureState;
MdaState m_mdaState;
+ QElapsedTimer m_progressPublishTimer;
+ QTimer m_writerStatusTimer;
+ std::atomic_bool m_writerStatusDirty{false};
};
}
diff --git a/ScopeOneCore/internal/SpatiotemporalBinningModule.h b/ScopeOneCore/internal/SpatiotemporalBinningModule.h
index 4363924..c1b21d9 100644
--- a/ScopeOneCore/internal/SpatiotemporalBinningModule.h
+++ b/ScopeOneCore/internal/SpatiotemporalBinningModule.h
@@ -3,6 +3,7 @@
#include "internal/ProcessingModule.h"
#include
+#include
namespace scopeone::core::internal
{
@@ -33,5 +34,6 @@ namespace scopeone::core::internal
BinningMode m_spatialMode{BinningMode::Mean};
BinningMode m_temporalMode{BinningMode::Mean};
std::deque m_frameBuffer;
+ std::vector m_temporalSum;
};
}
diff --git a/ScopeOneCore/src/AgentCameraBackend.cpp b/ScopeOneCore/src/AgentCameraBackend.cpp
new file mode 100644
index 0000000..58d298e
--- /dev/null
+++ b/ScopeOneCore/src/AgentCameraBackend.cpp
@@ -0,0 +1,1851 @@
+#include "internal/CameraBackend.h"
+#include "internal/AgentProtocol.h"
+#include "scopeone/SharedFrame.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace scopeone::core::internal
+{
+ static_assert(std::atomic_ref::is_always_lock_free,
+ "Shared frame state requires lock-free 32-bit atomics");
+
+ using scopeone::core::ImageFrame;
+ using scopeone::core::SharedFrameHeader;
+ using scopeone::core::SharedMemoryControl;
+ using scopeone::core::SharedPixelFormat;
+ using scopeone::core::kSharedFrameHeaderSize;
+ using scopeone::core::kSharedFrameMaxBytes;
+ using scopeone::core::kSharedFrameNumSlots;
+ using scopeone::core::kSharedFrameSlotStride;
+ using scopeone::core::kSharedMemoryControlSize;
+
+ namespace
+ {
+ constexpr int kAgentControlReadyTimeoutMs = 15000;
+
+ enum class AgentFrameDeliveryMode
+ {
+ PreviewLatest,
+ LatestOnly,
+ AllFrames
+ };
+
+ // Normalizes camera ids before they become backend keys
+ QString normalizedCameraId(const QString& cameraId)
+ {
+ return cameraId.trimmed();
+ }
+
+ const QString& frameDeliveryModeName(AgentFrameDeliveryMode mode)
+ {
+ switch (mode)
+ {
+ case AgentFrameDeliveryMode::PreviewLatest:
+ return agent::kFrameDeliveryModePreviewLatest;
+ case AgentFrameDeliveryMode::LatestOnly:
+ return agent::kFrameDeliveryModeLatestOnly;
+ case AgentFrameDeliveryMode::AllFrames:
+ return agent::kFrameDeliveryModeAllFrames;
+ }
+ return agent::kFrameDeliveryModePreviewLatest;
+ }
+
+ AgentFrameDeliveryMode nonRecordingDeliveryMode(bool highRate)
+ {
+ return highRate
+ ? AgentFrameDeliveryMode::LatestOnly
+ : AgentFrameDeliveryMode::PreviewLatest;
+ }
+
+ } // namespace
+
+ struct AgentControlSession final : QObject
+ {
+ struct PendingRequest
+ {
+ QByteArray encoded;
+ QTimer* timer{nullptr};
+ std::function completion;
+ };
+
+ explicit AgentControlSession(const QString& cameraId,
+ const QString& serverName,
+ QObject* parent = nullptr)
+ : QObject(parent)
+ , m_cameraId(cameraId)
+ , m_serverName(serverName)
+ {
+ m_reconnectTimer.setSingleShot(true);
+ connect(&m_reconnectTimer, &QTimer::timeout, this, [this]() { ensureConnected(); });
+
+ connect(&m_socket, &QLocalSocket::connected, this, [this]() { flushPendingWrites(); });
+ connect(&m_socket, &QLocalSocket::disconnected, this, [this]()
+ {
+ m_readBuffer.clear();
+ setReady(false);
+ failAll(QStringLiteral("Control session disconnected"));
+ if (!m_closing)
+ {
+ m_reconnectTimer.start(100);
+ }
+ });
+ connect(&m_socket, &QLocalSocket::readyRead, this, [this]() { handleReadyRead(); });
+ connect(&m_socket, &QLocalSocket::errorOccurred, this,
+ [this](QLocalSocket::LocalSocketError)
+ {
+ if (m_closing)
+ {
+ return;
+ }
+ setReady(false);
+ failAll(QStringLiteral("Control socket error for '%1'").arg(m_cameraId));
+ if (m_socket.state() == QLocalSocket::UnconnectedState)
+ {
+ m_reconnectTimer.start(100);
+ }
+ });
+ }
+
+ void start()
+ {
+ ensureConnected();
+ }
+
+ void stop()
+ {
+ m_closing = true;
+ m_reconnectTimer.stop();
+ m_readBuffer.clear();
+ setReady(false);
+ failAll(QStringLiteral("Control session closed"));
+ if (m_socket.state() != QLocalSocket::UnconnectedState)
+ {
+ m_socket.abort();
+ }
+ }
+
+ bool isReady() const
+ {
+ return m_ready && m_socket.state() == QLocalSocket::ConnectedState;
+ }
+
+ bool waitForReady(int timeoutMs)
+ {
+ if (isReady())
+ {
+ return true;
+ }
+
+ ensureConnected();
+
+ QEventLoop loop;
+ QTimer watchdog;
+ watchdog.setSingleShot(true);
+ watchdog.setInterval((std::max)(1, timeoutMs));
+
+ const auto quitIfReady = [this, &loop]()
+ {
+ if (isReady())
+ {
+ loop.quit();
+ }
+ };
+
+ connect(&m_socket, &QLocalSocket::connected, &loop, quitIfReady);
+ connect(&m_socket, &QLocalSocket::readyRead, &loop, quitIfReady);
+ connect(&m_socket, &QLocalSocket::disconnected, &loop, quitIfReady);
+ connect(&m_socket, &QLocalSocket::errorOccurred, &loop,
+ [quitIfReady](QLocalSocket::LocalSocketError)
+ {
+ quitIfReady();
+ });
+ connect(&watchdog, &QTimer::timeout, &loop, &QEventLoop::quit);
+
+ watchdog.start();
+ if (!isReady())
+ {
+ loop.exec();
+ }
+ return isReady();
+ }
+
+ void addReadyHandler(std::function handler)
+ {
+ m_readyHandlers.push_back(std::move(handler));
+ }
+
+ void addEventHandler(std::function handler)
+ {
+ m_eventHandlers.push_back(std::move(handler));
+ }
+
+ bool sendRequest(const QJsonObject& request,
+ int timeoutMs,
+ std::function completion)
+ {
+ const QString type = request.value(agent::kMessageTypeField).toString();
+ if (type.isEmpty())
+ {
+ return false;
+ }
+
+ const quint64 requestId = m_nextRequestId++;
+ QJsonObject envelope = request;
+ envelope.insert(agent::kEnvelopeKindField, agent::kMessageKindRequest);
+ envelope.insert(agent::kEnvelopeVersionField, static_cast(agent::kProtocolVersion));
+ envelope.insert(agent::kEnvelopeRequestIdField, agent::encodeUInt64(requestId));
+
+ PendingRequest pending;
+ pending.encoded = agent::encodeMessage(envelope);
+ pending.completion = std::move(completion);
+ pending.timer = new QTimer(this);
+ pending.timer->setSingleShot(true);
+ connect(pending.timer, &QTimer::timeout, this, [this, requestId]()
+ {
+ completeRequest(requestId, false, QJsonObject{}, QStringLiteral("Control request timed out"));
+ });
+ pending.timer->start((std::max)(1, timeoutMs));
+
+ m_pending.insert(requestId, pending);
+ m_sendQueue.push_back(requestId);
+ ensureConnected();
+ flushPendingWrites();
+ return true;
+ }
+
+ private:
+ void ensureConnected()
+ {
+ if (m_closing)
+ {
+ return;
+ }
+ if (m_socket.state() == QLocalSocket::ConnectedState
+ || m_socket.state() == QLocalSocket::ConnectingState)
+ {
+ return;
+ }
+ m_socket.connectToServer(m_serverName);
+ }
+
+ void setReady(bool ready)
+ {
+ if (m_ready == ready)
+ {
+ return;
+ }
+ m_ready = ready;
+ for (const auto& handler : m_readyHandlers)
+ {
+ handler(m_ready);
+ }
+ }
+
+ void flushPendingWrites()
+ {
+ if (m_socket.state() != QLocalSocket::ConnectedState)
+ {
+ return;
+ }
+
+ while (!m_sendQueue.isEmpty())
+ {
+ const quint64 requestId = m_sendQueue.front();
+ m_sendQueue.pop_front();
+
+ auto it = m_pending.find(requestId);
+ if (it == m_pending.end())
+ {
+ continue;
+ }
+ m_socket.write(it->encoded);
+ }
+ m_socket.flush();
+ }
+
+ void handleReadyRead()
+ {
+ m_readBuffer += m_socket.readAll();
+ while (true)
+ {
+ QJsonObject message;
+ QString error;
+ const agent::DecodeResult result =
+ agent::tryDecodeMessage(m_readBuffer, message, &error);
+ if (result == agent::DecodeResult::Incomplete)
+ {
+ return;
+ }
+ if (result == agent::DecodeResult::Error)
+ {
+ resetWithError(error);
+ return;
+ }
+
+ if (message.value(agent::kEnvelopeVersionField).toInt(0)
+ != static_cast(agent::kProtocolVersion))
+ {
+ resetWithError(QStringLiteral("Control protocol version mismatch"));
+ return;
+ }
+
+ const QString kind = message.value(agent::kEnvelopeKindField).toString();
+ if (kind == agent::kMessageKindResponse)
+ {
+ const quint64 requestId =
+ agent::decodeUInt64(message.value(agent::kEnvelopeRequestIdField));
+ if (requestId == 0)
+ {
+ continue;
+ }
+ completeRequest(requestId, true, message, QString{});
+ continue;
+ }
+
+ if (kind == agent::kMessageKindEvent)
+ {
+ const QString type = message.value(agent::kMessageTypeField).toString();
+ if (type == agent::kEventHello)
+ {
+ setReady(true);
+ }
+ for (const auto& handler : m_eventHandlers)
+ {
+ handler(message);
+ }
+ }
+ }
+ }
+
+ void completeRequest(quint64 requestId,
+ bool ok,
+ const QJsonObject& response,
+ const QString& error)
+ {
+ auto it = m_pending.find(requestId);
+ if (it == m_pending.end())
+ {
+ return;
+ }
+
+ PendingRequest pending = it.value();
+ m_pending.erase(it);
+
+ if (pending.timer)
+ {
+ pending.timer->stop();
+ pending.timer->deleteLater();
+ }
+ if (pending.completion)
+ {
+ pending.completion(ok, response, error);
+ }
+ }
+
+ void failAll(const QString& error)
+ {
+ const QList requestIds = m_pending.keys();
+ for (quint64 requestId : requestIds)
+ {
+ completeRequest(requestId, false, QJsonObject{}, error);
+ }
+ m_sendQueue.clear();
+ }
+
+ void resetWithError(const QString& error)
+ {
+ m_readBuffer.clear();
+ setReady(false);
+ failAll(error);
+ if (m_socket.state() != QLocalSocket::UnconnectedState)
+ {
+ m_socket.abort();
+ }
+ if (!m_closing)
+ {
+ m_reconnectTimer.start(100);
+ }
+ }
+
+ QString m_cameraId;
+ QString m_serverName;
+ QLocalSocket m_socket;
+ QTimer m_reconnectTimer;
+ QByteArray m_readBuffer;
+ QMap m_pending;
+ QList m_sendQueue;
+ QList> m_readyHandlers;
+ QList> m_eventHandlers;
+ quint64 m_nextRequestId{1};
+ bool m_ready{false};
+ bool m_closing{false};
+ };
+
+ class AgentCameraBackend;
+
+ class AgentFrameWorker final : public QObject
+ {
+ public:
+ explicit AgentFrameWorker(AgentCameraBackend* owner)
+ : m_owner(owner)
+ {
+ }
+
+ void addCamera(const QString& cameraId, const QString& shmKey);
+ void removeCamera(const QString& cameraId);
+ void clear();
+ bool ensureSharedMemory(const QString& cameraId);
+ ImageFrame consumeFrames(const QString& cameraId);
+ void consumeFramesAsync(const QString& cameraId);
+
+ private:
+ struct ReaderSlot
+ {
+ QString cameraId;
+ QString shmKey;
+ std::unique_ptr shm;
+ quint64 lastFrameIndex{0};
+ ImageFrame latestFrame;
+ };
+
+ bool ensureSharedMemory(ReaderSlot& slot);
+ bool copyFrame(ReaderSlot& slot,
+ const SharedFrameHeader& header,
+ const uchar* pixelData,
+ QList& frames);
+ bool readLatestFrame(ReaderSlot& slot,
+ QList& frames,
+ quint64& acquiredFrameCount);
+ bool readAllFrames(ReaderSlot& slot,
+ QList& frames,
+ quint64& acquiredFrameCount);
+
+ AgentCameraBackend* const m_owner;
+ QMap> m_readers;
+ };
+
+ struct AgentCameraSlot
+ {
+ QString cameraId;
+ QString shmKey;
+ std::shared_ptr process;
+ std::shared_ptr control;
+ bool isRunning{false};
+ double exposureMs{10.0};
+ bool frameReadQueued{false};
+ bool frameReadRequested{false};
+ };
+
+ class AgentCameraBackend final : public CameraBackend
+ {
+ public:
+ AgentCameraBackend();
+ ~AgentCameraBackend() override;
+
+ Kind kind() const override { return Kind::Agent; }
+ bool addAgentCamera(const QString& cameraId,
+ const QString& adapter,
+ const QString& device,
+ const QStringList& preInitProperties,
+ const QStringList& properties,
+ double exposureMs) override;
+ void removeAgentCamera(const QString& cameraId);
+ bool isPreviewRunning(const QString& cameraId) const override
+ {
+ const auto it = m_cameras.constFind(cameraId);
+ return it != m_cameras.constEnd() && it.value() && it.value()->isRunning;
+ }
+ void setFrameDeliveryPaused(bool paused) override;
+ bool setRecordingFrameDeliveryEnabled(bool enabled) override;
+ bool setHighRateFrameDeliveryEnabled(bool enabled) override;
+ bool captureEventFrame(const QString& cameraId,
+ ImageFrame& frame,
+ int timeoutMs) override;
+
+ void shutdown();
+
+ bool startPreview() override
+ {
+ if (m_cameras.isEmpty())
+ {
+ return false;
+ }
+
+ QStringList startedCameraIds;
+ for (auto it = m_cameras.begin(); it != m_cameras.end(); ++it)
+ {
+ const QString cameraId = it.key();
+ if (it.value() && it.value()->isRunning)
+ {
+ continue;
+ }
+ if (!startPreviewFor(cameraId))
+ {
+ for (const QString& startedCameraId : startedCameraIds)
+ {
+ stopPreviewFor(startedCameraId);
+ }
+ return false;
+ }
+ startedCameraIds.append(cameraId);
+ }
+ qInfo().noquote() << "Agent preview started";
+ return true;
+ }
+
+ bool stopPreview() override
+ {
+ bool ok = true;
+ for (auto it = m_cameras.begin(); it != m_cameras.end(); ++it)
+ {
+ ok = stopPreviewFor(it.key()) && ok;
+ }
+ if (ok)
+ {
+ qInfo().noquote() << "Agent preview stopped";
+ }
+ return ok;
+ }
+
+ bool startPreviewFor(const QString& cameraId) override
+ {
+ if (!m_cameras.contains(cameraId))
+ {
+ qWarning().noquote() << QString("Camera '%1' not found").arg(cameraId);
+ return false;
+ }
+
+ AgentCameraSlot& slot = *m_cameras[cameraId];
+ if (slot.isRunning)
+ {
+ return true;
+ }
+ QJsonObject req;
+ req.insert(agent::kMessageTypeField, agent::kCommandStartPreview);
+ QJsonObject resp;
+ if (!sendControlCommand(cameraId, req, &resp, 1200))
+ {
+ qWarning().noquote() << QString("Failed to connect to camera '%1'").arg(cameraId);
+ return false;
+ }
+ if (!resp.value(QStringLiteral("ok")).toBool(false))
+ {
+ qWarning().noquote() << QString("Agent refused to start preview for '%1'").arg(cameraId);
+ return false;
+ }
+
+ bool sharedMemoryReady = false;
+ for (int attempt = 0; attempt < 20 && !sharedMemoryReady; ++attempt)
+ {
+ sharedMemoryReady = prepareFrameReader(cameraId);
+ if (!sharedMemoryReady)
+ {
+ QThread::msleep(50);
+ }
+ }
+ if (!sharedMemoryReady)
+ {
+ qWarning().noquote() << QString("Shared memory unavailable for '%1'").arg(cameraId);
+ QJsonObject stopRequest;
+ stopRequest.insert(agent::kMessageTypeField, agent::kCommandStopPreview);
+ sendControlCommand(cameraId, stopRequest, nullptr, 1200);
+ return false;
+ }
+
+ slot.isRunning = true;
+ notifyPreviewStarted(cameraId);
+ return true;
+ }
+
+ bool stopPreviewFor(const QString& cameraId) override
+ {
+ const auto it = m_cameras.find(cameraId);
+ if (it == m_cameras.end() || !it.value())
+ {
+ return false;
+ }
+ if (!it.value()->isRunning)
+ {
+ return true;
+ }
+
+ QJsonObject req;
+ req.insert(agent::kMessageTypeField, agent::kCommandStopPreview);
+ QJsonObject resp;
+ if (!sendControlCommand(cameraId, req, &resp, 1200))
+ {
+ return false;
+ }
+ if (!resp.value(QStringLiteral("ok")).toBool(false))
+ {
+ return false;
+ }
+
+ const bool wasRunning = it.value()->isRunning;
+ it.value()->isRunning = false;
+ if (wasRunning)
+ {
+ notifyPreviewStopped();
+ }
+ return true;
+ }
+
+ protected:
+ bool hasRunningCamera() const override
+ {
+ for (auto it = m_cameras.constBegin(); it != m_cameras.constEnd(); ++it)
+ {
+ if (it.value() && it.value()->isRunning)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool resolvePrimaryCameraId(const QString& cameraIdOrAll, QString& cameraId) const override
+ {
+ if (m_cameras.isEmpty())
+ {
+ return false;
+ }
+
+ const QString target =
+ cameraIdOrAll.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0
+ ? m_cameras.firstKey()
+ : cameraIdOrAll;
+ if (!m_cameras.contains(target))
+ {
+ return false;
+ }
+ cameraId = target;
+ return true;
+ }
+
+ QStringList resolveTargetCameraIds(const QString& cameraIdOrAll) const override
+ {
+ if (cameraIdOrAll.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0)
+ {
+ return m_cameras.keys();
+ }
+ return m_cameras.contains(cameraIdOrAll) ? QStringList{cameraIdOrAll} : QStringList{};
+ }
+
+ bool readExposureFor(const QString& cameraId, double& exposureMs) const override
+ {
+ const auto it = m_cameras.constFind(cameraId);
+ if (it == m_cameras.constEnd() || !it.value())
+ {
+ return false;
+ }
+ exposureMs = it.value()->exposureMs;
+ return exposureMs > 0.0;
+ }
+
+ bool writeExposureFor(const QString& cameraId, double exposureMs) override
+ {
+ QJsonObject req;
+ req.insert(agent::kMessageTypeField, agent::kCommandSetExposure);
+ req.insert(QStringLiteral("value"), exposureMs);
+ QJsonObject resp;
+ if (!sendControlCommand(cameraId, req, &resp, 1200)
+ || !resp.value(QStringLiteral("ok")).toBool(false))
+ {
+ return false;
+ }
+ const double actualExposureMs = resp.value(QStringLiteral("exposureMs")).toDouble(exposureMs);
+ m_cameras[cameraId]->exposureMs = actualExposureMs;
+ return true;
+ }
+
+ QStringList listPropertiesFor(const QString& cameraId) override
+ {
+ QStringList out;
+ QJsonObject req;
+ req.insert(agent::kMessageTypeField, agent::kCommandListProperties);
+ QJsonObject resp;
+ if (!sendControlCommand(cameraId, req, &resp, 4000))
+ {
+ return out;
+ }
+ if (!resp.value(QStringLiteral("ok")).toBool(false))
+ {
+ return out;
+ }
+ const QJsonArray properties = resp.value(QStringLiteral("properties")).toArray();
+ for (const auto& value : properties)
+ {
+ out << value.toString();
+ }
+ return out;
+ }
+
+ bool readPropertyDetailsFor(const QString& cameraId,
+ const QString& name,
+ bool fromCache,
+ CameraPropertyReadback& readback) override
+ {
+ QJsonObject req;
+ req.insert(agent::kMessageTypeField, agent::kCommandGetProperty);
+ req.insert(QStringLiteral("name"), name);
+ req.insert(QStringLiteral("fromCache"), fromCache);
+ QJsonObject resp;
+ if (!sendControlCommand(cameraId, req, &resp, 4000))
+ {
+ return false;
+ }
+ if (!resp.value(QStringLiteral("ok")).toBool(false))
+ {
+ return false;
+ }
+
+ readback.value = resp.value(QStringLiteral("value")).toString();
+ readback.type = resp.value(QStringLiteral("propertyType")).toString(QStringLiteral("Unknown"));
+ readback.readOnly = resp.value(QStringLiteral("readOnly")).toBool(true);
+ readback.preInit = resp.value(QStringLiteral("preInit")).toBool(false);
+ readback.hasLimits = resp.value(QStringLiteral("hasLimits")).toBool(false);
+ readback.lowerLimit = resp.value(QStringLiteral("lowerLimit")).toDouble(0.0);
+ readback.upperLimit = resp.value(QStringLiteral("upperLimit")).toDouble(0.0);
+
+ const QJsonArray allowedValues = resp.value(QStringLiteral("allowedValues")).toArray();
+ for (const auto& value : allowedValues)
+ {
+ readback.allowedValues << value.toString();
+ }
+ return true;
+ }
+
+ bool setPropertyFor(const QString& cameraId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage) override
+ {
+ QJsonObject req;
+ req.insert(agent::kMessageTypeField, agent::kCommandSetProperty);
+ req.insert(QStringLiteral("name"), name);
+ req.insert(QStringLiteral("value"), value);
+ QJsonObject resp;
+ if (!sendControlCommand(cameraId, req, &resp, 4000))
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("Agent control request failed");
+ }
+ return false;
+ }
+ if (!resp.value(QStringLiteral("ok")).toBool(false))
+ {
+ if (errorMessage)
+ {
+ *errorMessage = resp.value(QStringLiteral("error")).toString();
+ }
+ return false;
+ }
+ return true;
+ }
+
+ bool setROIFor(const QString& cameraId, int x, int y, int width, int height) override
+ {
+ QJsonObject req;
+ req.insert(agent::kMessageTypeField, agent::kCommandSetRoi);
+ req.insert(QStringLiteral("x"), x);
+ req.insert(QStringLiteral("y"), y);
+ req.insert(QStringLiteral("width"), width);
+ req.insert(QStringLiteral("height"), height);
+ QJsonObject resp;
+ if (!sendControlCommand(cameraId, req, &resp, 1200))
+ {
+ qWarning().noquote() << QString("Failed to connect to camera '%1'").arg(cameraId);
+ return false;
+ }
+ const bool ok = resp.value(QStringLiteral("ok")).toBool(false);
+ if (!ok)
+ {
+ qWarning().noquote() << QString("Failed to set ROI for '%1': %2")
+ .arg(cameraId, resp.value(QStringLiteral("error")).toString(QStringLiteral("Unknown error")));
+ }
+ return ok;
+ }
+
+ bool clearROIFor(const QString& cameraId) override
+ {
+ QJsonObject req;
+ req.insert(agent::kMessageTypeField, agent::kCommandClearRoi);
+ QJsonObject resp;
+ if (!sendControlCommand(cameraId, req, &resp, 1200))
+ {
+ qWarning().noquote() << QString("Failed to connect to camera '%1'").arg(cameraId);
+ return false;
+ }
+ const bool ok = resp.value(QStringLiteral("ok")).toBool(false);
+ if (!ok)
+ {
+ qWarning().noquote() << QString("Failed to clear ROI for '%1': %2")
+ .arg(cameraId, resp.value(QStringLiteral("error")).toString(QStringLiteral("Unknown error")));
+ }
+ return ok;
+ }
+
+ bool getROIFor(const QString& cameraId, int& x, int& y, int& width, int& height) override
+ {
+ QJsonObject req;
+ req.insert(agent::kMessageTypeField, agent::kCommandGetRoi);
+ QJsonObject resp;
+ if (!sendControlCommand(cameraId, req, &resp, 1200))
+ {
+ qWarning().noquote() << QString("Failed to connect to camera '%1'").arg(cameraId);
+ return false;
+ }
+ const bool ok = resp.value(QStringLiteral("ok")).toBool(false);
+ if (!ok)
+ {
+ qWarning().noquote() << QString("Failed to get ROI for '%1': %2")
+ .arg(cameraId, resp.value(QStringLiteral("error")).toString(QStringLiteral("Unknown error")));
+ return false;
+ }
+
+ x = resp.value(QStringLiteral("x")).toInt(0);
+ y = resp.value(QStringLiteral("y")).toInt(0);
+ width = resp.value(QStringLiteral("width")).toInt(0);
+ height = resp.value(QStringLiteral("height")).toInt(0);
+ return true;
+ }
+ private:
+ friend class AgentFrameWorker;
+
+ bool waitForControlReady(AgentCameraSlot& slot, int timeoutMs);
+ bool addFrameReader(const QString& cameraId, const QString& shmKey);
+ void removeFrameReader(const QString& cameraId);
+ bool prepareFrameReader(const QString& cameraId);
+ ImageFrame consumeFrameNow(const QString& cameraId);
+ void scheduleFrameRead(const QString& cameraId);
+ void completeFrameRead(const QString& cameraId);
+ bool sendControlCommand(const QString& cameraId,
+ const QJsonObject& request,
+ QJsonObject* response,
+ int timeoutMs);
+ bool sendFrameDeliveryMode(const QString& cameraId, AgentFrameDeliveryMode mode);
+
+ QMap> m_cameras;
+ std::atomic_bool m_frameDeliveryPaused{false};
+ bool m_shuttingDown{false};
+ QThread m_frameThread;
+ AgentFrameWorker* m_frameWorker{nullptr};
+ };
+
+ // Returns payload byte count from a shared frame header
+ static quint64 sharedFramePayloadSize(const SharedFrameHeader& header)
+ {
+ return static_cast(header.stride) * header.height;
+ }
+
+ // Validates one shared frame header before reading pixels
+ static bool headerLooksSane(const SharedFrameHeader& header)
+ {
+ if (header.channels != 1) return false;
+ if (header.width == 0 || header.height == 0) return false;
+ if (header.stride == 0) return false;
+ if (header.pixelFormat != static_cast(SharedPixelFormat::Mono8) &&
+ header.pixelFormat != static_cast(SharedPixelFormat::Mono16))
+ {
+ return false;
+ }
+
+ const quint32 bytesPerPixel =
+ (header.pixelFormat == static_cast(SharedPixelFormat::Mono16)) ? 2u : 1u;
+ if (header.pixelFormat == static_cast(SharedPixelFormat::Mono8)
+ && header.bitsPerSample != 8)
+ {
+ return false;
+ }
+ if (header.pixelFormat == static_cast(SharedPixelFormat::Mono16)
+ && (header.bitsPerSample == 0 || header.bitsPerSample > 16))
+ {
+ return false;
+ }
+ const quint64 minimumStride = static_cast(header.width) * bytesPerPixel;
+ if (minimumStride > static_cast((std::numeric_limits::max)())) return false;
+ if (header.width > static_cast((std::numeric_limits::max)())) return false;
+ if (header.height > static_cast((std::numeric_limits::max)())) return false;
+ if (header.stride > static_cast((std::numeric_limits::max)())) return false;
+ if (header.stride < minimumStride) return false;
+ if ((header.stride % bytesPerPixel) != 0) return false;
+
+ const quint64 rawSize = sharedFramePayloadSize(header);
+ if (rawSize == 0 || rawSize > static_cast(kSharedFrameMaxBytes)) return false;
+
+ return true;
+ }
+
+ // Claims one ready ring slot so the producer cannot overwrite it while copying
+ static bool claimFrameSlot(uchar* slotPtr, SharedFrameHeader& header)
+ {
+ auto& stateValue = *reinterpret_cast(slotPtr);
+ std::atomic_ref state(stateValue);
+ quint32 expected = 2;
+ if (!state.compare_exchange_strong(expected,
+ 3,
+ std::memory_order_acq_rel,
+ std::memory_order_acquire))
+ {
+ return false;
+ }
+
+ memcpy(&header, slotPtr, sizeof(header));
+ header.state = 2;
+ if (headerLooksSane(header))
+ {
+ return true;
+ }
+
+ state.store(2, std::memory_order_release);
+ return false;
+ }
+
+ // Releases one claimed ring slot back to the producer
+ static void releaseFrameSlot(uchar* slotPtr)
+ {
+ auto& stateValue = *reinterpret_cast(slotPtr);
+ std::atomic_ref(stateValue).store(2, std::memory_order_release);
+ }
+
+ AgentCameraBackend::AgentCameraBackend()
+ {
+ m_frameThread.setObjectName(QStringLiteral("ScopeOneAgentFrameReader"));
+ m_frameWorker = new AgentFrameWorker(this);
+ m_frameWorker->moveToThread(&m_frameThread);
+ QObject::connect(&m_frameThread, &QThread::finished,
+ m_frameWorker, &QObject::deleteLater);
+ m_frameThread.start();
+ }
+
+ AgentCameraBackend::~AgentCameraBackend()
+ {
+ shutdown();
+ m_frameThread.quit();
+ m_frameThread.wait();
+ m_frameWorker = nullptr;
+ }
+
+ void AgentCameraBackend::shutdown()
+ {
+ if (m_shuttingDown)
+ {
+ return;
+ }
+ m_shuttingDown = true;
+ CameraBackend::setRecordingFrameDeliveryEnabled(false);
+
+ const bool hadRunningCamera = hasRunningCamera();
+ for (auto& slot : m_cameras)
+ {
+ slot->isRunning = false;
+ }
+ if (m_frameWorker && m_frameThread.isRunning())
+ {
+ AgentFrameWorker* const worker = m_frameWorker;
+ QMetaObject::invokeMethod(worker,
+ [worker]() { worker->clear(); },
+ Qt::BlockingQueuedConnection);
+ }
+ for (auto& slot : m_cameras)
+ {
+ if (slot->control)
+ {
+ QJsonObject request;
+ request.insert(agent::kMessageTypeField, agent::kCommandShutdown);
+ sendControlCommand(slot->cameraId, request, nullptr, 800);
+ slot->control->stop();
+ }
+ if (slot->process)
+ {
+ slot->process->terminate();
+ if (!slot->process->waitForFinished(1500))
+ {
+ slot->process->kill();
+ slot->process->waitForFinished(1000);
+ }
+ }
+ }
+ m_cameras.clear();
+ discardPendingPreviewFrames();
+ if (hadRunningCamera)
+ {
+ notifyPreviewStopped();
+ }
+ }
+
+ void AgentFrameWorker::addCamera(const QString& cameraId, const QString& shmKey)
+ {
+ removeCamera(cameraId);
+ auto slot = std::make_shared();
+ slot->cameraId = cameraId;
+ slot->shmKey = shmKey;
+ slot->shm = std::make_unique();
+ slot->shm->setNativeKey(shmKey);
+ m_readers.insert(cameraId, std::move(slot));
+ }
+
+ void AgentFrameWorker::removeCamera(const QString& cameraId)
+ {
+ m_readers.remove(cameraId);
+ }
+
+ void AgentFrameWorker::clear()
+ {
+ m_readers.clear();
+ }
+
+ bool AgentFrameWorker::ensureSharedMemory(const QString& cameraId)
+ {
+ const auto it = m_readers.find(cameraId);
+ return it != m_readers.end() && ensureSharedMemory(*it.value());
+ }
+
+ bool AgentFrameWorker::ensureSharedMemory(ReaderSlot& slot)
+ {
+ const int expectedSize =
+ kSharedMemoryControlSize + kSharedFrameNumSlots * kSharedFrameSlotStride;
+ if (slot.shm->isAttached())
+ {
+ if (slot.shm->size() >= expectedSize)
+ {
+ return true;
+ }
+ qWarning().noquote()
+ << QString("SHM size mismatch for %1 (key=%2)").arg(slot.cameraId, slot.shmKey);
+ slot.shm->detach();
+ return false;
+ }
+ if (!slot.shm->attach(QSharedMemory::ReadWrite))
+ {
+ qWarning().noquote()
+ << QString("SHM attach failed for %1 (key=%2)").arg(slot.cameraId, slot.shmKey);
+ return false;
+ }
+ if (slot.shm->size() < expectedSize)
+ {
+ qWarning().noquote()
+ << QString("SHM layout mismatch for %1 (key=%2)").arg(slot.cameraId, slot.shmKey);
+ slot.shm->detach();
+ return false;
+ }
+ qInfo().noquote()
+ << QString("SHM attached for %1 (key=%2)").arg(slot.cameraId, slot.shmKey);
+ return true;
+ }
+
+ // Waits for one agent control channel to become ready
+ bool AgentCameraBackend::waitForControlReady(AgentCameraSlot& slot, int timeoutMs)
+ {
+ return slot.control && slot.control->waitForReady(timeoutMs);
+ }
+
+ bool AgentCameraBackend::addFrameReader(const QString& cameraId, const QString& shmKey)
+ {
+ if (!m_frameWorker || !m_frameThread.isRunning())
+ {
+ return false;
+ }
+ AgentFrameWorker* const worker = m_frameWorker;
+ return QMetaObject::invokeMethod(
+ worker,
+ [worker, cameraId, shmKey]() { worker->addCamera(cameraId, shmKey); },
+ Qt::BlockingQueuedConnection);
+ }
+
+ void AgentCameraBackend::removeFrameReader(const QString& cameraId)
+ {
+ if (m_frameWorker && m_frameThread.isRunning())
+ {
+ AgentFrameWorker* const worker = m_frameWorker;
+ QMetaObject::invokeMethod(
+ worker,
+ [worker, cameraId]() { worker->removeCamera(cameraId); },
+ Qt::BlockingQueuedConnection);
+ }
+ }
+
+ bool AgentCameraBackend::prepareFrameReader(const QString& cameraId)
+ {
+ if (!m_frameWorker || !m_frameThread.isRunning())
+ {
+ return false;
+ }
+ bool attached = false;
+ AgentFrameWorker* const worker = m_frameWorker;
+ const bool invoked = QMetaObject::invokeMethod(
+ worker,
+ [worker, cameraId, &attached]() { attached = worker->ensureSharedMemory(cameraId); },
+ Qt::BlockingQueuedConnection);
+ return invoked && attached;
+ }
+
+ // Sends one JSON command to an agent control channel
+ bool AgentCameraBackend::sendControlCommand(const QString& cameraId,
+ const QJsonObject& request,
+ QJsonObject* response,
+ int timeoutMs)
+ {
+ const QString normalizedId = normalizedCameraId(cameraId);
+ const auto it = m_cameras.constFind(normalizedId);
+ if (it == m_cameras.constEnd() || !it.value() || !it.value()->control)
+ {
+ return false;
+ }
+
+ bool completed = false;
+ bool ok = false;
+ QJsonObject capturedResponse;
+ QEventLoop loop;
+ QTimer watchdog;
+ watchdog.setSingleShot(true);
+ watchdog.setInterval((std::max)(1, timeoutMs + 250));
+ connect(&watchdog, &QTimer::timeout, &loop, [&]()
+ {
+ if (!completed)
+ {
+ completed = true;
+ ok = false;
+ }
+ loop.quit();
+ });
+
+ if (!it.value()->control->sendRequest(
+ request,
+ (std::max)(1, timeoutMs),
+ [&](bool requestOk, const QJsonObject& requestResponse, const QString&)
+ {
+ if (completed)
+ {
+ return;
+ }
+ completed = true;
+ ok = requestOk;
+ capturedResponse = requestResponse;
+ loop.quit();
+ }))
+ {
+ return false;
+ }
+
+ watchdog.start();
+ if (!completed)
+ {
+ loop.exec();
+ }
+
+ if (!ok)
+ {
+ return false;
+ }
+ if (response)
+ {
+ *response = capturedResponse;
+ }
+ return true;
+ }
+
+ // Selects the requested producer delivery policy in one agent
+ bool AgentCameraBackend::sendFrameDeliveryMode(const QString& cameraId,
+ AgentFrameDeliveryMode mode)
+ {
+ QJsonObject request;
+ request.insert(agent::kMessageTypeField, agent::kCommandSetFrameDeliveryMode);
+ request.insert(QStringLiteral("mode"), frameDeliveryModeName(mode));
+ QJsonObject response;
+ return sendControlCommand(cameraId, request, &response, 1200)
+ && response.value(QStringLiteral("ok")).toBool(false);
+ }
+
+ bool AgentFrameWorker::copyFrame(ReaderSlot& slot,
+ const SharedFrameHeader& header,
+ const uchar* pixelData,
+ QList& frames)
+ {
+ const quint64 rawSize = sharedFramePayloadSize(header);
+ QByteArray payload;
+ payload.resize(static_cast(rawSize));
+ memcpy(payload.data(), pixelData, static_cast(rawSize));
+
+ ImageFrame frame = ImageFrame::fromSharedFrame(slot.cameraId, header, payload);
+ if (!frame.isValid())
+ {
+ return false;
+ }
+ slot.latestFrame = frame;
+ frames.append(std::move(frame));
+ return true;
+ }
+
+ // Reads only the newest ready frame for responsive preview delivery
+ bool AgentFrameWorker::readLatestFrame(ReaderSlot& slot,
+ QList& frames,
+ quint64& acquiredFrameCount)
+ {
+ auto* base = static_cast(slot.shm->data());
+ if (!base)
+ {
+ return false;
+ }
+
+ const int slotStride = kSharedFrameSlotStride;
+ const int baseOffset = kSharedMemoryControlSize;
+ auto* control = reinterpret_cast(base);
+
+ const quint64 previousFrameIndex = slot.lastFrameIndex;
+ SharedFrameHeader capturedHeader{};
+ uchar* capturedSlot = nullptr;
+
+ auto claimCandidate = [&](quint32 idx) -> bool
+ {
+ if (idx >= kSharedFrameNumSlots) return false;
+ uchar* ptr = base + baseOffset + idx * slotStride;
+ SharedFrameHeader header{};
+ if (!claimFrameSlot(ptr, header)) return false;
+ if (header.frameIndex <= slot.lastFrameIndex)
+ {
+ releaseFrameSlot(ptr);
+ return false;
+ }
+ capturedHeader = header;
+ capturedSlot = ptr;
+ return true;
+ };
+
+ const quint32 latestIdx = std::atomic_ref(control->latestSlotIndex)
+ .load(std::memory_order_acquire);
+ claimCandidate(latestIdx);
+
+ if (!capturedSlot)
+ {
+ quint64 bestIndex = slot.lastFrameIndex;
+ for (int i = 0; i < kSharedFrameNumSlots; ++i)
+ {
+ uchar* ptr = base + baseOffset + i * slotStride;
+ SharedFrameHeader header{};
+ if (!claimFrameSlot(ptr, header)) continue;
+ if (header.frameIndex > bestIndex)
+ {
+ if (capturedSlot)
+ {
+ releaseFrameSlot(capturedSlot);
+ }
+ bestIndex = header.frameIndex;
+ capturedHeader = header;
+ capturedSlot = ptr;
+ }
+ else
+ {
+ releaseFrameSlot(ptr);
+ }
+ }
+ }
+
+ const bool ok = capturedSlot
+ && copyFrame(slot,
+ capturedHeader,
+ capturedSlot + kSharedFrameHeaderSize,
+ frames);
+ if (capturedSlot)
+ {
+ releaseFrameSlot(capturedSlot);
+ }
+ if (ok)
+ {
+ slot.lastFrameIndex = capturedHeader.frameIndex;
+ acquiredFrameCount = capturedHeader.frameIndex > previousFrameIndex
+ ? capturedHeader.frameIndex - previousFrameIndex
+ : 1;
+ }
+ return ok;
+ }
+
+ // Reads every retained shared memory frame in order for recording delivery
+ bool AgentFrameWorker::readAllFrames(ReaderSlot& slot,
+ QList& frames,
+ quint64& acquiredFrameCount)
+ {
+ auto* base = static_cast(slot.shm->data());
+ if (!base)
+ {
+ return false;
+ }
+
+ const int slotStride = kSharedFrameSlotStride;
+ const int baseOffset = kSharedMemoryControlSize;
+ const quint64 lastIndex = slot.lastFrameIndex;
+
+ struct ClaimedSlot
+ {
+ uchar* ptr{nullptr};
+ SharedFrameHeader header{};
+ };
+ std::vector claimedSlots;
+ claimedSlots.reserve(kSharedFrameNumSlots);
+
+ for (int i = 0; i < kSharedFrameNumSlots; ++i)
+ {
+ uchar* ptr = base + baseOffset + i * slotStride;
+ SharedFrameHeader header{};
+ if (!claimFrameSlot(ptr, header)) continue;
+ if (header.frameIndex > lastIndex)
+ {
+ claimedSlots.push_back({ptr, header});
+ }
+ else
+ {
+ releaseFrameSlot(ptr);
+ }
+ }
+
+ if (claimedSlots.empty())
+ {
+ return false;
+ }
+
+ std::sort(claimedSlots.begin(), claimedSlots.end(), [](const ClaimedSlot& a, const ClaimedSlot& b)
+ {
+ return a.header.frameIndex < b.header.frameIndex;
+ });
+
+ frames.reserve(static_cast(claimedSlots.size()));
+ quint64 maxIndex = lastIndex;
+ for (const ClaimedSlot& claimed : claimedSlots)
+ {
+ if (copyFrame(slot,
+ claimed.header,
+ claimed.ptr + kSharedFrameHeaderSize,
+ frames))
+ {
+ maxIndex = claimed.header.frameIndex;
+ }
+ releaseFrameSlot(claimed.ptr);
+ }
+ if (frames.isEmpty())
+ {
+ return false;
+ }
+
+ slot.latestFrame = frames.constLast();
+ slot.lastFrameIndex = maxIndex;
+ acquiredFrameCount = maxIndex > lastIndex
+ ? maxIndex - lastIndex
+ : static_cast(frames.size());
+ return true;
+ }
+
+ // Copies frames on the reader thread and forwards only completed images
+ ImageFrame AgentFrameWorker::consumeFrames(const QString& cameraId)
+ {
+ const auto it = m_readers.find(cameraId);
+ if (it == m_readers.end())
+ {
+ return {};
+ }
+
+ ReaderSlot& slot = *it.value();
+ QList frames;
+ quint64 acquiredFrameCount = 0;
+ if (ensureSharedMemory(slot))
+ {
+ if (m_owner->recordingFrameDeliveryEnabled()
+ || m_owner->highRateFrameDeliveryEnabled())
+ {
+ readAllFrames(slot, frames, acquiredFrameCount);
+ }
+ else
+ {
+ readLatestFrame(slot, frames, acquiredFrameCount);
+ }
+ }
+
+ if (!frames.isEmpty())
+ {
+ m_owner->submitFrames(frames, acquiredFrameCount);
+ }
+ return slot.latestFrame;
+ }
+
+ void AgentFrameWorker::consumeFramesAsync(const QString& cameraId)
+ {
+ if (!m_owner->m_frameDeliveryPaused.load(std::memory_order_relaxed))
+ {
+ consumeFrames(cameraId);
+ }
+ AgentCameraBackend* const owner = m_owner;
+ QMetaObject::invokeMethod(owner,
+ [owner, cameraId]() { owner->completeFrameRead(cameraId); },
+ Qt::QueuedConnection);
+ }
+
+ ImageFrame AgentCameraBackend::consumeFrameNow(const QString& cameraId)
+ {
+ if (!m_frameWorker || !m_frameThread.isRunning())
+ {
+ return {};
+ }
+
+ ImageFrame frame;
+ AgentFrameWorker* const worker = m_frameWorker;
+ const bool invoked = QMetaObject::invokeMethod(
+ worker,
+ [worker, cameraId, &frame]() { frame = worker->consumeFrames(cameraId); },
+ Qt::BlockingQueuedConnection);
+ return invoked ? frame : ImageFrame{};
+ }
+
+ void AgentCameraBackend::scheduleFrameRead(const QString& cameraId)
+ {
+ const auto it = m_cameras.find(cameraId);
+ if (it == m_cameras.end() || !it.value() || !m_frameWorker || m_shuttingDown)
+ {
+ return;
+ }
+ AgentCameraSlot& slot = *it.value();
+ if (!slot.isRunning || m_frameDeliveryPaused.load(std::memory_order_relaxed))
+ {
+ return;
+ }
+ if (slot.frameReadQueued)
+ {
+ slot.frameReadRequested = true;
+ return;
+ }
+
+ slot.frameReadQueued = true;
+ AgentFrameWorker* const worker = m_frameWorker;
+ if (!QMetaObject::invokeMethod(
+ worker,
+ [worker, cameraId]() { worker->consumeFramesAsync(cameraId); },
+ Qt::QueuedConnection))
+ {
+ slot.frameReadQueued = false;
+ }
+ }
+
+ void AgentCameraBackend::completeFrameRead(const QString& cameraId)
+ {
+ const auto it = m_cameras.find(cameraId);
+ if (it == m_cameras.end() || !it.value())
+ {
+ return;
+ }
+ AgentCameraSlot& slot = *it.value();
+ slot.frameReadQueued = false;
+ if (!slot.frameReadRequested)
+ {
+ return;
+ }
+
+ slot.frameReadRequested = false;
+ scheduleFrameRead(cameraId);
+ }
+
+ // Triggers one camera and waits for its event frame
+ bool AgentCameraBackend::captureEventFrame(const QString& cameraId,
+ ImageFrame& frame,
+ int timeoutMs)
+ {
+ const QString normalizedId = normalizedCameraId(cameraId);
+ const auto cameraIt = m_cameras.constFind(normalizedId);
+ if (cameraIt == m_cameras.constEnd() || !cameraIt.value()
+ || !prepareFrameReader(normalizedId))
+ {
+ return false;
+ }
+
+ const ImageFrame previousFrame = consumeFrameNow(normalizedId);
+ const quint64 previousFrameIndex = previousFrame.isValid()
+ ? previousFrame.frameIndex
+ : 0;
+
+ QJsonObject req;
+ req.insert(agent::kMessageTypeField, agent::kCommandCaptureEvent);
+ QJsonObject resp;
+ const int waitMs = (timeoutMs > 0) ? timeoutMs : 1500;
+ if (!sendControlCommand(normalizedId, req, &resp, waitMs + 1000))
+ {
+ return false;
+ }
+ if (!resp.value("ok").toBool(false))
+ {
+ return false;
+ }
+
+ const quint64 targetFrameIndex =
+ agent::decodeUInt64(resp.value(QStringLiteral("frameIndex")));
+ QElapsedTimer timer;
+ timer.start();
+
+ while (timer.elapsed() <= waitMs)
+ {
+ const ImageFrame candidate = consumeFrameNow(normalizedId);
+ if (candidate.isValid()
+ && candidate.frameIndex > previousFrameIndex
+ && (targetFrameIndex == 0 || candidate.frameIndex >= targetFrameIndex))
+ {
+ frame = candidate;
+ return true;
+ }
+ QCoreApplication::processEvents(QEventLoop::AllEvents, 5);
+ QThread::msleep(1);
+ }
+
+ return false;
+ }
+
+ // Pauses or resumes polling while another workflow controls capture
+ void AgentCameraBackend::setFrameDeliveryPaused(bool paused)
+ {
+ if (m_frameDeliveryPaused.exchange(paused, std::memory_order_relaxed) == paused)
+ {
+ return;
+ }
+ if (!paused)
+ {
+ for (auto it = m_cameras.begin(); it != m_cameras.end(); ++it)
+ {
+ if (it.value() && it.value()->isRunning)
+ {
+ scheduleFrameRead(it.key());
+ }
+ }
+ }
+ }
+
+ // Switches agent producers before changing the local recording consumer mode
+ bool AgentCameraBackend::setRecordingFrameDeliveryEnabled(bool enabled)
+ {
+ if (enabled && recordingFrameDeliveryEnabled())
+ {
+ return true;
+ }
+
+ const AgentFrameDeliveryMode previewMode =
+ nonRecordingDeliveryMode(highRateFrameDeliveryEnabled());
+ if (!enabled)
+ {
+ CameraBackend::setRecordingFrameDeliveryEnabled(false);
+ bool ok = true;
+ for (auto it = m_cameras.constBegin(); it != m_cameras.constEnd(); ++it)
+ {
+ ok = it.value() && sendFrameDeliveryMode(it.key(), previewMode) && ok;
+ }
+ return ok;
+ }
+
+ QStringList updatedCameraIds;
+ for (auto it = m_cameras.constBegin(); it != m_cameras.constEnd(); ++it)
+ {
+ if (!it.value()
+ || !sendFrameDeliveryMode(it.key(), AgentFrameDeliveryMode::AllFrames))
+ {
+ for (const QString& cameraId : updatedCameraIds)
+ {
+ sendFrameDeliveryMode(cameraId, previewMode);
+ }
+ return false;
+ }
+ updatedCameraIds.append(it.key());
+ }
+
+ for (const QString& cameraId : updatedCameraIds)
+ {
+ if (!prepareFrameReader(cameraId))
+ {
+ for (const QString& updatedCameraId : updatedCameraIds)
+ {
+ sendFrameDeliveryMode(updatedCameraId, previewMode);
+ }
+ return false;
+ }
+ consumeFrameNow(cameraId);
+ }
+ return CameraBackend::setRecordingFrameDeliveryEnabled(true);
+ }
+
+ // Switches preview producers between display-rate and processing-rate delivery
+ bool AgentCameraBackend::setHighRateFrameDeliveryEnabled(bool enabled)
+ {
+ if (highRateFrameDeliveryEnabled() == enabled)
+ {
+ return true;
+ }
+
+ if (recordingFrameDeliveryEnabled())
+ {
+ return CameraBackend::setHighRateFrameDeliveryEnabled(enabled);
+ }
+
+ const AgentFrameDeliveryMode targetMode = nonRecordingDeliveryMode(enabled);
+ const AgentFrameDeliveryMode rollbackMode =
+ nonRecordingDeliveryMode(highRateFrameDeliveryEnabled());
+ QStringList updatedCameraIds;
+ for (auto it = m_cameras.constBegin(); it != m_cameras.constEnd(); ++it)
+ {
+ if (!it.value() || !sendFrameDeliveryMode(it.key(), targetMode))
+ {
+ for (const QString& cameraId : updatedCameraIds)
+ {
+ sendFrameDeliveryMode(cameraId, rollbackMode);
+ }
+ return false;
+ }
+ updatedCameraIds.append(it.key());
+ }
+ return CameraBackend::setHighRateFrameDeliveryEnabled(enabled);
+ }
+
+ // Starts one camera agent process and connects its control channel
+ bool AgentCameraBackend::addAgentCamera(const QString& cameraId,
+ const QString& adapter,
+ const QString& device,
+ const QStringList& preInitProperties,
+ const QStringList& properties,
+ double exposureMs)
+ {
+ const QString normalizedId = normalizedCameraId(cameraId);
+ const QString adapterName = adapter.trimmed();
+ const QString deviceName = device.trimmed();
+ if (normalizedId.isEmpty() || adapterName.isEmpty() || deviceName.isEmpty())
+ {
+ return false;
+ }
+ if (m_cameras.contains(normalizedId))
+ {
+ return true;
+ }
+ auto slot = std::make_shared();
+ slot->cameraId = normalizedId;
+ slot->shmKey = agent::sharedMemoryKey(normalizedId);
+ slot->process = std::make_shared();
+ slot->control = std::make_shared(normalizedId,
+ agent::controlServerName(normalizedId));
+ slot->exposureMs = exposureMs;
+ const QString agentPath =
+ QDir(QCoreApplication::applicationDirPath()).filePath(agent::kExecutableFileName);
+ if (!QFileInfo::exists(agentPath))
+ {
+ qWarning().noquote() << QString("Agent executable not found: %1").arg(agentPath);
+ return false;
+ }
+ QStringList args;
+ args << "--cameraId" << normalizedId
+ << "--adapter" << adapterName
+ << "--device" << deviceName
+ << "--shm" << slot->shmKey;
+ if (exposureMs > 0.0)
+ {
+ args << "--exposure" << QString::number(exposureMs, 'f', 4);
+ }
+ for (const QString& encodedProperty : preInitProperties)
+ {
+ if (!encodedProperty.isEmpty())
+ {
+ args << "--preinit" << encodedProperty;
+ }
+ }
+ for (const QString& encodedProperty : properties)
+ {
+ if (!encodedProperty.isEmpty())
+ {
+ args << "--property" << encodedProperty;
+ }
+ }
+ slot->process->setProgram(agentPath);
+ slot->process->setArguments(args);
+ slot->process->setProcessChannelMode(QProcess::MergedChannels);
+
+ QProcess* const process = slot->process.get();
+ connect(process, &QProcess::readyReadStandardOutput, this, [normalizedId, process]()
+ {
+ const QByteArray output = process->readAllStandardOutput();
+ if (!output.isEmpty())
+ {
+ QStringList lines = QString::fromUtf8(output).split('\n', Qt::SkipEmptyParts);
+ for (const QString& line : lines)
+ {
+ const QString trimmed = line.trimmed();
+ qInfo().noquote() << QString("[Agent %1] %2").arg(normalizedId, trimmed);
+ }
+ }
+ });
+ connect(process, &QProcess::finished, this,
+ [this, normalizedId](int, QProcess::ExitStatus)
+ {
+ const auto it = m_cameras.find(normalizedId);
+ if (it == m_cameras.end() || !it.value())
+ {
+ return;
+ }
+ const bool wasRunning = it.value()->isRunning;
+ it.value()->isRunning = false;
+ const bool wasRecording = recordingFrameDeliveryEnabled();
+ if (wasRecording)
+ {
+ CameraBackend::setRecordingFrameDeliveryEnabled(false);
+ }
+ if (wasRunning)
+ {
+ notifyPreviewStopped();
+ }
+ if (wasRecording)
+ {
+ emit frameDeliveryFailed(
+ QStringLiteral("Camera agent exited for '%1'").arg(normalizedId));
+ }
+ });
+
+ if (slot->control)
+ {
+ slot->control->addReadyHandler([this, normalizedId](bool ready)
+ {
+ if (ready)
+ {
+ emit agentControlServerListening(normalizedId, agent::controlServerName(normalizedId));
+ }
+ });
+ slot->control->addEventHandler([this, normalizedId](const QJsonObject& event)
+ {
+ const auto it = m_cameras.find(normalizedId);
+ if (it == m_cameras.end() || !it.value())
+ {
+ return;
+ }
+ AgentCameraSlot& slot = *it.value();
+ const QString type = event.value(agent::kMessageTypeField).toString();
+ if (type == agent::kEventFrameAvailable)
+ {
+ scheduleFrameRead(normalizedId);
+ return;
+ }
+ if (type == agent::kEventPreviewState)
+ {
+ const bool wasRunning = slot.isRunning;
+ slot.isRunning = event.value(QStringLiteral("running")).toBool(slot.isRunning);
+ if (wasRunning && !slot.isRunning)
+ {
+ notifyPreviewStopped();
+ }
+ return;
+ }
+ if (type == agent::kEventAgentError)
+ {
+ const QString error = QStringLiteral("Agent '%1' error: %2")
+ .arg(slot.cameraId,
+ event.value(QStringLiteral("error")).toString());
+ qWarning().noquote() << error;
+ if (recordingFrameDeliveryEnabled())
+ {
+ CameraBackend::setRecordingFrameDeliveryEnabled(false);
+ emit frameDeliveryFailed(error);
+ }
+ return;
+ }
+ });
+ }
+
+ slot->process->start();
+ if (!slot->process->waitForStarted(3000))
+ {
+ qWarning().noquote() << QString("Failed to start agent for %1").arg(normalizedId);
+ return false;
+ }
+ m_cameras.insert(normalizedId, slot);
+ if (slot->control)
+ {
+ slot->control->start();
+ }
+ if (!waitForControlReady(*slot, kAgentControlReadyTimeoutMs))
+ {
+ qWarning().noquote()
+ << QString("Agent control session did not become ready for %1 within %2 ms")
+ .arg(normalizedId)
+ .arg(kAgentControlReadyTimeoutMs);
+ removeAgentCamera(normalizedId);
+ return false;
+ }
+ if (!addFrameReader(normalizedId, slot->shmKey))
+ {
+ removeAgentCamera(normalizedId);
+ return false;
+ }
+ const AgentFrameDeliveryMode deliveryMode = recordingFrameDeliveryEnabled()
+ ? AgentFrameDeliveryMode::AllFrames
+ : nonRecordingDeliveryMode(
+ highRateFrameDeliveryEnabled());
+ if (!sendFrameDeliveryMode(normalizedId, deliveryMode))
+ {
+ removeAgentCamera(normalizedId);
+ return false;
+ }
+ return true;
+ }
+
+ // Stops one camera agent process and releases its resources
+ void AgentCameraBackend::removeAgentCamera(const QString& cameraId)
+ {
+ const QString normalizedId = normalizedCameraId(cameraId);
+ if (!m_cameras.contains(normalizedId))
+ {
+ return;
+ }
+ const auto slot = m_cameras.value(normalizedId);
+ const bool wasRunning = slot->isRunning;
+ slot->isRunning = false;
+ removeFrameReader(normalizedId);
+ if (slot->control)
+ {
+ QJsonObject request;
+ request.insert(agent::kMessageTypeField, agent::kCommandShutdown);
+ sendControlCommand(normalizedId, request, nullptr, 800);
+ }
+ m_cameras.remove(normalizedId);
+ if (slot->control)
+ {
+ slot->control->stop();
+ }
+ if (slot->process)
+ {
+ slot->process->terminate();
+ if (!slot->process->waitForFinished(1500))
+ {
+ slot->process->kill();
+ slot->process->waitForFinished(1000);
+ }
+ }
+ if (wasRunning)
+ {
+ notifyPreviewStopped();
+ }
+ }
+
+ std::unique_ptr createAgentCameraBackend()
+ {
+ return std::make_unique();
+ }
+}
diff --git a/ScopeOneCore/src/AgentMain.cpp b/ScopeOneCore/src/AgentMain.cpp
index 6d89ce3..feeeb6a 100644
--- a/ScopeOneCore/src/AgentMain.cpp
+++ b/ScopeOneCore/src/AgentMain.cpp
@@ -14,9 +14,14 @@
#include
#include
+#include
+#include
+#include
#include
#include
#include
+#include
+#include
#include "MMCore.h"
#include "internal/AgentProtocol.h"
@@ -24,6 +29,9 @@
namespace scopeone::core::internal
{
+ static_assert(std::atomic_ref::is_always_lock_free,
+ "Shared frame state requires lock-free 32-bit atomics");
+
using scopeone::core::SharedFrameHeader;
using scopeone::core::SharedMemoryControl;
using scopeone::core::SharedPixelFormat;
@@ -34,6 +42,21 @@ namespace scopeone::core::internal
using scopeone::core::kSharedFrameSlotStride;
using scopeone::core::kSharedMemoryControlSize;
+ constexpr int kMinimumPollIntervalMs = 1;
+ constexpr int kMaximumPollIntervalMs = 50;
+ constexpr int kPreviewFrameDeliveryIntervalMs = 16;
+
+ int pollingIntervalFor(double frameIntervalMs)
+ {
+ if (!std::isfinite(frameIntervalMs) || frameIntervalMs <= 0.0)
+ {
+ return kMinimumPollIntervalMs;
+ }
+ return static_cast(std::clamp(frameIntervalMs / 4.0,
+ static_cast(kMinimumPollIntervalMs),
+ static_cast(kMaximumPollIntervalMs)));
+ }
+
// Normalize frame bit depth before publishing shared frame metadata
static quint16 normalizedSharedBitDepth(SharedPixelFormat format, int bitsPerSample)
{
@@ -89,7 +112,6 @@ namespace scopeone::core::internal
return;
}
m_socket->write(agent::encodeMessage(message));
- m_socket->flush();
}
signals:
@@ -212,7 +234,7 @@ namespace scopeone::core::internal
if (!m_timer)
{
m_timer = new QTimer(this);
- m_timer->setInterval(1);
+ m_timer->setTimerType(Qt::PreciseTimer);
connect(m_timer, &QTimer::timeout,
this, &AgentRuntime::pollAndWrite);
}
@@ -297,7 +319,19 @@ namespace scopeone::core::internal
if (m_shm->lock())
{
- memset(m_shm->data(), 0, totalBytes);
+ auto* base = static_cast(m_shm->data());
+ if (base)
+ {
+ const SharedMemoryControl control{};
+ memcpy(base, &control, sizeof(control));
+ for (int i = 0; i < kSharedFrameNumSlots; ++i)
+ {
+ const SharedFrameHeader header{};
+ uchar* slot = base + kSharedMemoryControlSize
+ + i * kSharedFrameSlotStride;
+ memcpy(slot, &header, sizeof(header));
+ }
+ }
m_shm->unlock();
}
@@ -343,6 +377,13 @@ namespace scopeone::core::internal
ShuttingDown
};
+ enum class FrameDeliveryMode
+ {
+ PreviewLatest,
+ LatestOnly,
+ AllFrames
+ };
+
// Frame layout describes one shared memory payload shape
struct FrameLayout
{
@@ -369,13 +410,15 @@ namespace scopeone::core::internal
QString* errorMessage);
void emitPreviewStateEvent();
void emitAgentErrorEvent(const QString& error);
- void emitBufferOverflowEvent(long bufferCapacity);
void emitFrameAvailableEvent(quint64 frameIndex);
bool startPreviewInternal(QString* errorMessage);
bool stopPreviewInternal(QString* errorMessage);
bool captureEventFrameInternal(quint64& frameIndex, QString* errorMessage);
- bool writeFrameToSharedMemory(const void* pixels, quint64* frameIndexOut = nullptr);
+ bool writeFrameToSharedMemory(const void* pixels,
+ quint64 frameAdvance = 1,
+ quint64* frameIndexOut = nullptr);
void pollAndWrite();
+ void updatePollingInterval(quint64 frameCount);
bool ensureFrameLayout(unsigned width, unsigned height, unsigned bytesPerPixel);
void refreshSourceRoi();
@@ -394,6 +437,9 @@ namespace scopeone::core::internal
std::unique_ptr m_mmcore;
std::unique_ptr m_shm;
QTimer* m_timer{nullptr};
+ QElapsedTimer m_frameIntervalTimer;
+ QElapsedTimer m_deliveryTimer;
+ double m_observedFrameIntervalMs{0.0};
FrameLayout m_frameLayout{};
bool m_frameLayoutValid{false};
@@ -405,7 +451,9 @@ namespace scopeone::core::internal
int m_sourceRoiHeight{0};
quint64 m_frameIndex{0};
- int m_overflowCheckCounter{0};
+ quint64 m_pendingFrameAdvance{0};
+ int m_nextWriteSlot{0};
+ FrameDeliveryMode m_frameDeliveryMode{FrameDeliveryMode::PreviewLatest};
};
// Publish the initial hello event to connected clients
@@ -514,6 +562,39 @@ namespace scopeone::core::internal
return;
}
+ if (type == agent::kCommandSetFrameDeliveryMode)
+ {
+ const QString mode = message.value(QStringLiteral("mode")).toString();
+ if (mode == agent::kFrameDeliveryModePreviewLatest)
+ {
+ m_frameDeliveryMode = FrameDeliveryMode::PreviewLatest;
+ m_deliveryTimer.invalidate();
+ }
+ else if (mode == agent::kFrameDeliveryModeLatestOnly)
+ {
+ m_frameDeliveryMode = FrameDeliveryMode::LatestOnly;
+ }
+ else if (mode == agent::kFrameDeliveryModeAllFrames)
+ {
+ m_frameDeliveryMode = FrameDeliveryMode::AllFrames;
+ m_pendingFrameAdvance = 0;
+ m_deliveryTimer.invalidate();
+ }
+ else
+ {
+ emit responseReady(connectionId,
+ makeErrorResponse(type,
+ requestId,
+ QStringLiteral("Unknown frame delivery mode")));
+ return;
+ }
+
+ QJsonObject response = makeResponse(type, requestId, true);
+ response.insert(QStringLiteral("mode"), mode);
+ emit responseReady(connectionId, response);
+ return;
+ }
+
if (type == agent::kCommandSetExposure)
{
const double exposureMs = message.value(QStringLiteral("value")).toDouble(-1.0);
@@ -586,9 +667,13 @@ namespace scopeone::core::internal
if (type == agent::kCommandGetProperty)
{
const QString name = message.value(QStringLiteral("name")).toString();
+ const bool fromCache = message.value(QStringLiteral("fromCache")).toBool(false);
+ const std::string camera = m_cameraId.toStdString();
+ const std::string property = name.toStdString();
QString value;
QString propertyType = QStringLiteral("Unknown");
bool readOnly = true;
+ bool preInit = false;
QJsonArray allowedValues;
bool hasLimits = false;
double lowerLimit = 0.0;
@@ -599,13 +684,13 @@ namespace scopeone::core::internal
try
{
value = QString::fromStdString(
- m_mmcore->getProperty(m_cameraId.toStdString().c_str(),
- name.toStdString().c_str()));
+ fromCache
+ ? m_mmcore->getPropertyFromCache(camera.c_str(), property.c_str())
+ : m_mmcore->getProperty(camera.c_str(), property.c_str()));
try
{
- switch (m_mmcore->getPropertyType(m_cameraId.toStdString().c_str(),
- name.toStdString().c_str()))
+ switch (m_mmcore->getPropertyType(camera.c_str(), property.c_str()))
{
case MM::String:
propertyType = QStringLiteral("String");
@@ -627,9 +712,15 @@ namespace scopeone::core::internal
try
{
- readOnly = m_mmcore->isPropertyReadOnly(
- m_cameraId.toStdString().c_str(),
- name.toStdString().c_str());
+ preInit = m_mmcore->isPropertyPreInit(camera.c_str(), property.c_str());
+ }
+ catch (const CMMError&)
+ {
+ }
+
+ try
+ {
+ readOnly = m_mmcore->isPropertyReadOnly(camera.c_str(), property.c_str());
}
catch (const CMMError&)
{
@@ -638,8 +729,7 @@ namespace scopeone::core::internal
try
{
const auto values =
- m_mmcore->getAllowedPropertyValues(m_cameraId.toStdString().c_str(),
- name.toStdString().c_str());
+ m_mmcore->getAllowedPropertyValues(camera.c_str(), property.c_str());
for (const auto& allowedValue : values)
{
allowedValues.append(QString::fromStdString(allowedValue));
@@ -651,16 +741,11 @@ namespace scopeone::core::internal
try
{
- hasLimits = m_mmcore->hasPropertyLimits(m_cameraId.toStdString().c_str(),
- name.toStdString().c_str());
+ hasLimits = m_mmcore->hasPropertyLimits(camera.c_str(), property.c_str());
if (hasLimits)
{
- lowerLimit = m_mmcore->getPropertyLowerLimit(
- m_cameraId.toStdString().c_str(),
- name.toStdString().c_str());
- upperLimit = m_mmcore->getPropertyUpperLimit(
- m_cameraId.toStdString().c_str(),
- name.toStdString().c_str());
+ lowerLimit = m_mmcore->getPropertyLowerLimit(camera.c_str(), property.c_str());
+ upperLimit = m_mmcore->getPropertyUpperLimit(camera.c_str(), property.c_str());
}
}
catch (const CMMError&)
@@ -680,6 +765,7 @@ namespace scopeone::core::internal
response.insert(QStringLiteral("value"), value);
response.insert(QStringLiteral("propertyType"), propertyType);
response.insert(QStringLiteral("readOnly"), readOnly);
+ response.insert(QStringLiteral("preInit"), preInit);
response.insert(QStringLiteral("allowedValues"), allowedValues);
response.insert(QStringLiteral("hasLimits"), hasLimits);
response.insert(QStringLiteral("lowerLimit"), lowerLimit);
@@ -912,14 +998,6 @@ namespace scopeone::core::internal
emit eventReady(event);
}
- // Publish a camera buffer overflow event
- void AgentRuntime::emitBufferOverflowEvent(long bufferCapacity)
- {
- QJsonObject event = makeEvent(agent::kEventBufferOverflow);
- event.insert(QStringLiteral("capacityFrames"), static_cast(bufferCapacity));
- emit eventReady(event);
- }
-
// Publish the newest shared memory frame index
void AgentRuntime::emitFrameAvailableEvent(quint64 frameIndex)
{
@@ -959,7 +1037,22 @@ namespace scopeone::core::internal
m_mmcore->popNextImage();
}
refreshSourceRoi();
+ if (!ensureFrameLayout(m_mmcore->getImageWidth(),
+ m_mmcore->getImageHeight(),
+ m_mmcore->getBytesPerPixel()))
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("Unsupported frame format");
+ }
+ return false;
+ }
m_mmcore->startContinuousSequenceAcquisition(0.0);
+ m_observedFrameIntervalMs = 0.0;
+ m_pendingFrameAdvance = 0;
+ m_frameIntervalTimer.restart();
+ m_deliveryTimer.invalidate();
+ m_timer->setInterval(pollingIntervalFor(m_exposureMs));
if (m_timer && !m_timer->isActive())
{
m_timer->start();
@@ -999,6 +1092,10 @@ namespace scopeone::core::internal
{
m_timer->stop();
}
+ m_frameIntervalTimer.invalidate();
+ m_deliveryTimer.invalidate();
+ m_observedFrameIntervalMs = 0.0;
+ m_pendingFrameAdvance = 0;
if (m_state != State::ShuttingDown && m_state != State::Error)
{
setState(State::Idle);
@@ -1066,7 +1163,9 @@ namespace scopeone::core::internal
return false;
}
- if (!writeFrameToSharedMemory(pixels, &frameIndex))
+ const quint64 frameAdvance = m_pendingFrameAdvance + 1;
+ m_pendingFrameAdvance = 0;
+ if (!writeFrameToSharedMemory(pixels, frameAdvance, &frameIndex))
{
if (errorMessage)
{
@@ -1075,6 +1174,11 @@ namespace scopeone::core::internal
return false;
}
+ if (m_frameDeliveryMode == FrameDeliveryMode::PreviewLatest)
+ {
+ m_deliveryTimer.restart();
+ }
+
emitFrameAvailableEvent(frameIndex);
return true;
}
@@ -1089,27 +1193,62 @@ namespace scopeone::core::internal
}
// Copy one camera frame into the shared memory ring buffer
- bool AgentRuntime::writeFrameToSharedMemory(const void* pixels, quint64* frameIndexOut)
+ bool AgentRuntime::writeFrameToSharedMemory(const void* pixels,
+ quint64 frameAdvance,
+ quint64* frameIndexOut)
{
if (!m_shm || !m_shm->isAttached())
{
return false;
}
- if (!m_shm->lock())
+ uchar* base = static_cast(m_shm->data());
+ if (!base)
{
return false;
}
- uchar* base = static_cast(m_shm->data());
- if (!base)
+ const quint64 nextFrameIndex = m_frameIndex + (std::max)(quint64{1}, frameAdvance);
+ const int preferredSlotIndex = m_nextWriteSlot;
+ int slotIndex = -1;
+ uchar* ptr = nullptr;
+ for (int offset = 0; offset < kSharedFrameNumSlots; ++offset)
+ {
+ const int candidate = (preferredSlotIndex + offset) % kSharedFrameNumSlots;
+ uchar* candidatePtr = base + kSharedMemoryControlSize
+ + candidate * kSharedFrameSlotStride;
+ auto& stateValue = *reinterpret_cast(candidatePtr);
+ std::atomic_ref state(stateValue);
+ quint32 expected = state.load(std::memory_order_acquire);
+ while (expected == 0 || expected == 2)
+ {
+ if (state.compare_exchange_weak(expected,
+ 1,
+ std::memory_order_acq_rel,
+ std::memory_order_acquire))
+ {
+ slotIndex = candidate;
+ ptr = candidatePtr;
+ break;
+ }
+ }
+ if (slotIndex >= 0)
+ {
+ break;
+ }
+ }
+
+ if (slotIndex < 0 || !ptr)
{
- m_shm->unlock();
+ m_frameIndex = nextFrameIndex;
+ if (frameIndexOut)
+ {
+ *frameIndexOut = m_frameIndex;
+ }
return false;
}
- const int slotIndex = static_cast(m_frameIndex % kSharedFrameNumSlots);
auto* control = reinterpret_cast(base);
- uchar* ptr = base + kSharedMemoryControlSize + slotIndex * kSharedFrameSlotStride;
+ m_nextWriteSlot = (slotIndex + 1) % kSharedFrameNumSlots;
SharedFrameHeader header{};
header.state = 1;
header.width = m_frameLayout.width;
@@ -1118,7 +1257,7 @@ namespace scopeone::core::internal
header.pixelFormat = static_cast(m_frameLayout.format);
header.bitsPerSample = m_frameLayout.bitDepth;
header.channels = m_frameLayout.channels;
- header.frameIndex = ++m_frameIndex;
+ header.frameIndex = nextFrameIndex;
header.timestampNs =
static_cast(QDateTime::currentMSecsSinceEpoch()) * 1000000ull;
setSharedFrameSourceRoi(header,
@@ -1126,20 +1265,23 @@ namespace scopeone::core::internal
m_sourceRoiY,
m_sourceRoiWidth,
m_sourceRoiHeight);
- memcpy(ptr, &header, sizeof(header));
+ memcpy(ptr + sizeof(header.state),
+ reinterpret_cast(&header) + sizeof(header.state),
+ sizeof(header) - sizeof(header.state));
uchar* dst = ptr + kSharedFrameHeaderSize;
memcpy(dst, pixels, static_cast(m_frameLayout.byteCount));
- header.state = 2;
- memcpy(ptr, &header, sizeof(header));
- control->latestSlotIndex = static_cast(slotIndex);
+ auto& stateValue = *reinterpret_cast(ptr);
+ std::atomic_ref(stateValue).store(2, std::memory_order_release);
+ std::atomic_ref(control->latestSlotIndex)
+ .store(static_cast(slotIndex), std::memory_order_release);
+ m_frameIndex = nextFrameIndex;
if (frameIndexOut)
{
*frameIndexOut = m_frameIndex;
}
- m_shm->unlock();
return true;
}
@@ -1155,49 +1297,82 @@ namespace scopeone::core::internal
{
long remaining = m_mmcore->getRemainingImageCount();
- if (++m_overflowCheckCounter >= 100 || remaining > 10)
- {
- m_overflowCheckCounter = 0;
- if (m_mmcore->isBufferOverflowed())
- {
- emitBufferOverflowEvent(m_mmcore->getBufferTotalCapacity());
- }
- }
-
if (remaining <= 0)
{
return;
}
quint64 newestFrameIndex = 0;
- while (remaining-- > 0)
- {
- const void* pixels = m_mmcore->popNextImage();
- if (!pixels)
+ quint64 acquiredFrameCount = 0;
+ if (m_frameDeliveryMode != FrameDeliveryMode::AllFrames)
+ {
+ const bool previewRateLimited =
+ m_frameDeliveryMode == FrameDeliveryMode::PreviewLatest;
+ const bool publishFrame = !previewRateLimited
+ || !m_deliveryTimer.isValid()
+ || m_deliveryTimer.elapsed() >= kPreviewFrameDeliveryIntervalMs;
+ while (remaining-- > 0)
{
- break;
+ const void* pixels = m_mmcore->popNextImage();
+ if (!pixels)
+ {
+ break;
+ }
+ ++acquiredFrameCount;
+ ++m_pendingFrameAdvance;
+ if (remaining > 0)
+ {
+ continue;
+ }
+ if (publishFrame && m_frameLayoutValid)
+ {
+ quint64 writtenFrameIndex = 0;
+ const bool written = writeFrameToSharedMemory(
+ pixels,
+ m_pendingFrameAdvance,
+ &writtenFrameIndex);
+ m_pendingFrameAdvance = 0;
+ if (previewRateLimited)
+ {
+ m_deliveryTimer.restart();
+ }
+ if (written)
+ {
+ newestFrameIndex = writtenFrameIndex;
+ }
+ }
}
-
- const unsigned width = m_mmcore->getImageWidth();
- const unsigned height = m_mmcore->getImageHeight();
- const unsigned bytesPerPixel = m_mmcore->getBytesPerPixel();
- if (!ensureFrameLayout(width, height, bytesPerPixel))
+ }
+ else
+ {
+ while (remaining-- > 0)
{
- break;
- }
+ const void* pixels = m_mmcore->popNextImage();
+ if (!pixels)
+ {
+ break;
+ }
+ ++acquiredFrameCount;
- quint64 writtenFrameIndex = 0;
- if (!writeFrameToSharedMemory(pixels, &writtenFrameIndex))
- {
- break;
+ if (!m_frameLayoutValid)
+ {
+ break;
+ }
+
+ quint64 writtenFrameIndex = 0;
+ if (!writeFrameToSharedMemory(pixels, 1, &writtenFrameIndex))
+ {
+ break;
+ }
+ newestFrameIndex = writtenFrameIndex;
}
- newestFrameIndex = writtenFrameIndex;
}
if (newestFrameIndex != 0)
{
emitFrameAvailableEvent(newestFrameIndex);
}
+ updatePollingInterval(acquiredFrameCount);
}
catch (const CMMError& error)
{
@@ -1208,6 +1383,33 @@ namespace scopeone::core::internal
}
}
+ // Adapts MMCore polling to the observed camera frame interval
+ void AgentRuntime::updatePollingInterval(quint64 frameCount)
+ {
+ if (frameCount == 0 || !m_frameIntervalTimer.isValid())
+ {
+ return;
+ }
+
+ const double elapsedMs = static_cast(m_frameIntervalTimer.nsecsElapsed()) / 1000000.0;
+ m_frameIntervalTimer.restart();
+ const double measuredIntervalMs = elapsedMs / static_cast(frameCount);
+ if (!std::isfinite(measuredIntervalMs) || measuredIntervalMs <= 0.0)
+ {
+ return;
+ }
+
+ m_observedFrameIntervalMs = m_observedFrameIntervalMs > 0.0
+ ? 0.75 * m_observedFrameIntervalMs
+ + 0.25 * measuredIntervalMs
+ : measuredIntervalMs;
+ const int intervalMs = pollingIntervalFor(m_observedFrameIntervalMs);
+ if (m_timer->interval() != intervalMs)
+ {
+ m_timer->setInterval(intervalMs);
+ }
+ }
+
// Validate and cache the current frame memory layout
bool AgentRuntime::ensureFrameLayout(unsigned width, unsigned height, unsigned bytesPerPixel)
{
diff --git a/ScopeOneCore/src/BackgroundCalibrationModule.cpp b/ScopeOneCore/src/BackgroundCalibrationModule.cpp
index 25b1de6..c88a54d 100644
--- a/ScopeOneCore/src/BackgroundCalibrationModule.cpp
+++ b/ScopeOneCore/src/BackgroundCalibrationModule.cpp
@@ -1,9 +1,58 @@
#include "internal/BackgroundCalibrationModule.h"
#include "internal/FrameBufferUtils.h"
#include
+#include
namespace scopeone::core::internal
{
+ namespace
+ {
+ // Applies one calibrated background sample to one source sample
+ int applyBackgroundOperation(int source,
+ int background,
+ int maxValue,
+ BackgroundOperation operation)
+ {
+ switch (operation)
+ {
+ case BackgroundOperation::Subtract:
+ return source - background;
+ case BackgroundOperation::Add:
+ return source + background;
+ case BackgroundOperation::Multiply:
+ return static_cast((static_cast(source) * background) / maxValue);
+ case BackgroundOperation::Divide:
+ return background > 0
+ ? static_cast((static_cast(source) * maxValue) / background)
+ : maxValue;
+ }
+ return source;
+ }
+
+ // Adds one frame to a full precision running pixel sum
+ void addFrameToRunningSum(const ImageFrame& frame, std::vector& sum)
+ {
+ dispatchFrameType(frame, [&]()
+ {
+ const char* sourceData = frame.bytes.constData();
+ qint64* sumData = sum.data();
+ parallelForImageRows(frame.width, frame.height, [&](int firstRow, int lastRow)
+ {
+ for (int y = firstRow; y < lastRow; ++y)
+ {
+ const Pixel* row = reinterpret_cast(
+ sourceData + static_cast(y) * frame.stride);
+ qint64* sumRow = sumData + static_cast(y) * frame.width;
+ for (int x = 0; x < frame.width; ++x)
+ {
+ sumRow[x] += static_cast