From 6672bbeb9229bde94205aeaff061b472c09676bf Mon Sep 17 00:00:00 2001
From: tz <185176969+tzhaoo@users.noreply.github.com>
Date: Sun, 16 Aug 2026 04:10:17 +0200
Subject: [PATCH 01/37] Add hardware provider architecture
---
ScopeOneCore/CMakeLists.txt | 17 +
ScopeOneCore/README.md | 8 +-
.../include/scopeone/CameraProvider.h | 54 ++
ScopeOneCore/include/scopeone/ClockService.h | 12 +
.../include/scopeone/HardwareProvider.h | 22 +
ScopeOneCore/include/scopeone/HardwareTypes.h | 83 +++
ScopeOneCore/include/scopeone/ImageFrame.h | 8 +
ScopeOneCore/include/scopeone/ScopeOneCore.h | 8 +
.../include/scopeone/SimulatorProvider.h | 74 +++
ScopeOneCore/internal/AcquisitionEngine.h | 36 ++
ScopeOneCore/internal/AgentProtocol.h | 127 +----
ScopeOneCore/internal/CameraManager.h | 59 ++-
ScopeOneCore/internal/CameraRuntimeControl.h | 20 +
ScopeOneCore/internal/DriverHostProtocol.h | 118 +++++
ScopeOneCore/internal/FrameRouter.h | 29 ++
ScopeOneCore/internal/HardwareRuntime.h | 113 +++++
ScopeOneCore/internal/MDAManager.h | 7 +-
ScopeOneCore/internal/MMCoreManager.h | 2 +
ScopeOneCore/internal/MicroManagerProvider.h | 58 +++
ScopeOneCore/internal/RecordingManager.h | 13 +-
ScopeOneCore/src/AcquisitionEngine.cpp | 129 +++++
ScopeOneCore/src/CameraManager.cpp | 15 +-
ScopeOneCore/src/ClockService.cpp | 18 +
ScopeOneCore/src/FrameBufferUtils.cpp | 1 +
ScopeOneCore/src/FrameRouter.cpp | 22 +
ScopeOneCore/src/HardwareRuntime.cpp | 477 ++++++++++++++++++
ScopeOneCore/src/MDAManager.cpp | 14 +-
ScopeOneCore/src/MMCoreManager.cpp | 45 ++
ScopeOneCore/src/MicroManagerProvider.cpp | 164 ++++++
ScopeOneCore/src/NativeCameraBackend.cpp | 2 +
ScopeOneCore/src/RecordingManager.cpp | 30 +-
ScopeOneCore/src/ScopeOneCore.cpp | 204 ++++++--
ScopeOneCore/src/SimulatorProvider.cpp | 306 +++++++++++
33 files changed, 2078 insertions(+), 217 deletions(-)
create mode 100644 ScopeOneCore/include/scopeone/CameraProvider.h
create mode 100644 ScopeOneCore/include/scopeone/ClockService.h
create mode 100644 ScopeOneCore/include/scopeone/HardwareProvider.h
create mode 100644 ScopeOneCore/include/scopeone/HardwareTypes.h
create mode 100644 ScopeOneCore/include/scopeone/SimulatorProvider.h
create mode 100644 ScopeOneCore/internal/AcquisitionEngine.h
create mode 100644 ScopeOneCore/internal/CameraRuntimeControl.h
create mode 100644 ScopeOneCore/internal/DriverHostProtocol.h
create mode 100644 ScopeOneCore/internal/FrameRouter.h
create mode 100644 ScopeOneCore/internal/HardwareRuntime.h
create mode 100644 ScopeOneCore/internal/MicroManagerProvider.h
create mode 100644 ScopeOneCore/src/AcquisitionEngine.cpp
create mode 100644 ScopeOneCore/src/ClockService.cpp
create mode 100644 ScopeOneCore/src/FrameRouter.cpp
create mode 100644 ScopeOneCore/src/HardwareRuntime.cpp
create mode 100644 ScopeOneCore/src/MicroManagerProvider.cpp
create mode 100644 ScopeOneCore/src/SimulatorProvider.cpp
diff --git a/ScopeOneCore/CMakeLists.txt b/ScopeOneCore/CMakeLists.txt
index 38568aa..45b91fa 100644
--- a/ScopeOneCore/CMakeLists.txt
+++ b/ScopeOneCore/CMakeLists.txt
@@ -77,6 +77,12 @@ endfunction()
set(CORE_SOURCES
src/ExperimentDocument.cpp
+ src/AcquisitionEngine.cpp
+ src/ClockService.cpp
+ src/FrameRouter.cpp
+ src/HardwareRuntime.cpp
+ src/MicroManagerProvider.cpp
+ src/SimulatorProvider.cpp
src/ImageSceneModel.cpp
src/MMCoreManager.cpp
src/FrameBufferUtils.cpp
@@ -99,7 +105,17 @@ set(CORE_SOURCES
set(CORE_HEADERS
include/scopeone/ExperimentDocument.h
+ internal/AcquisitionEngine.h
+ include/scopeone/CameraProvider.h
+ internal/CameraRuntimeControl.h
+ include/scopeone/ClockService.h
+ include/scopeone/HardwareTypes.h
+ include/scopeone/HardwareProvider.h
+ include/scopeone/SimulatorProvider.h
include/scopeone/ImageSceneModel.h
+ internal/HardwareRuntime.h
+ internal/FrameRouter.h
+ internal/MicroManagerProvider.h
internal/MMCoreManager.h
internal/FrameBufferUtils.h
internal/ParticleAnalysis.h
@@ -107,6 +123,7 @@ set(CORE_HEADERS
internal/ImageProcessingFramework.h
internal/ProcessingModule.h
internal/AgentProtocol.h
+ internal/DriverHostProtocol.h
internal/SpatiotemporalBinningModule.h
internal/GaussianBlurModule.h
internal/FFTModule.h
diff --git a/ScopeOneCore/README.md b/ScopeOneCore/README.md
index f504d66..e7f6836 100644
--- a/ScopeOneCore/README.md
+++ b/ScopeOneCore/README.md
@@ -44,7 +44,8 @@ Use this placement rule:
|---|---|---|
| `scopeone::core` | Stable Core-facing types and public facades | `ScopeOneCore`, `ImageFrame`, `ExperimentDocument`, `ImageSceneModel` |
| `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::core::internal::driverhost` | Shared DriverHost message framing | Versioned request, response and event envelopes |
+| `scopeone::core::internal::agent` | Micro-Manager DriverHost commands | Camera commands and shared-memory endpoint names |
| `scopeone::ui` | Desktop application widgets and UI coordination outside this library | `MainWindow`, `PreviewWidget`, `InspectWidget` |
Code in `src` that implements a public type remains in `scopeone::core`. Code that implements an `internal` header remains in `scopeone::core::internal`. The Python package named `scopeone` is an external client package and is not an embedded form of the C++ namespace.
@@ -99,6 +100,9 @@ Outputs:
The installed headers are the source of truth for the public API:
- `ScopeOneCore.h` provides the main hardware, acquisition, processing, recording and frame-graph facade.
+- `HardwareProvider.h` and `CameraProvider.h` define provider discovery, control and frame delivery.
+- `HardwareTypes.h` defines provider-independent device identity, state, endpoint and clock metadata.
+- `SimulatorProvider.h` provides an in-process reference provider.
- `ImageFrame.h` defines the image payload and metadata exchanged across Core features.
- `ExperimentDocument.h` defines experiment plans, results, persistence and provenance.
- `ImageSceneModel.h` defines shared image-layer, display-state and markup state.
@@ -107,6 +111,8 @@ The installed headers are the source of truth for the public API:
External code should enter through these headers and `scopeone::core::ScopeOneCore`. Internal managers are implementation details and must not become alternate access paths.
+Providers use ScopeOne logical device IDs and publish `ImageFrame` objects through `CameraProvider::FrameSink`. Register them with `ScopeOneCore::registerHardwareProvider(...)`; ScopeOne owns acquisition routing, clocks and downstream frame delivery. Micro-Manager uses the same provider boundary and may run in process or through the existing DriverHost transport.
+
## Processing Data Flow
`ImageFrame` is the frame model used by preview, processing, recording, gallery and the local API. Use `processFrameThrough(...)` to stop at one pipeline stage and `processFrameFrom(...)` to continue from a later module after an edited frame is written back. Saved OME-TIFF, OME-Zarr, TIFF and binary recording outputs are read back asynchronously through `ScopeOneCore::requestRecordingSessionFrame(...)`. Live preview processing and synchronous API processing use separate runtime pipeline state so offline frame edits do not change live module buffers.
diff --git a/ScopeOneCore/include/scopeone/CameraProvider.h b/ScopeOneCore/include/scopeone/CameraProvider.h
new file mode 100644
index 0000000..b9d06e9
--- /dev/null
+++ b/ScopeOneCore/include/scopeone/CameraProvider.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+#include "scopeone/ImageFrame.h"
+
+namespace scopeone::core
+{
+ class CameraProvider
+ {
+ public:
+ using FrameSink = std::function;
+
+ virtual ~CameraProvider() = default;
+
+ virtual void setFrameSink(FrameSink sink) = 0;
+ 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 bool getExposure(const QString& cameraIdOrAll, double& exposureMs) const = 0;
+ virtual bool setExposure(const QString& cameraIdOrAll, double exposureMs) = 0;
+ virtual QStringList listProperties(const QString& cameraId) = 0;
+ virtual QString getProperty(const QString& cameraId,
+ const QString& name,
+ bool fromCache = false) = 0;
+ virtual bool setProperty(const QString& cameraId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage = nullptr) = 0;
+ virtual QString getPropertyType(const QString& cameraId, const QString& name) = 0;
+ virtual bool isPropertyReadOnly(const QString& cameraId, const QString& name) = 0;
+ virtual bool isPropertyPreInit(const QString& cameraId, const QString& name) = 0;
+ virtual QStringList getAllowedPropertyValues(const QString& cameraId,
+ const QString& name) = 0;
+ virtual bool hasPropertyLimits(const QString& cameraId, const QString& name) = 0;
+ virtual double getPropertyLowerLimit(const QString& cameraId, const QString& name) = 0;
+ virtual double getPropertyUpperLimit(const QString& cameraId, const QString& name) = 0;
+ virtual bool setROI(const QString& cameraId, int x, int y, int width, int height) = 0;
+ virtual bool clearROI(const QString& cameraId) = 0;
+ virtual bool getROI(const QString& cameraId,
+ int& x,
+ int& y,
+ int& width,
+ int& height) = 0;
+ virtual bool captureEventFrame(const QString& cameraId,
+ ImageFrame& frame,
+ int timeoutMs = 1500) = 0;
+ };
+}
diff --git a/ScopeOneCore/include/scopeone/ClockService.h b/ScopeOneCore/include/scopeone/ClockService.h
new file mode 100644
index 0000000..77dc5ac
--- /dev/null
+++ b/ScopeOneCore/include/scopeone/ClockService.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "scopeone/HardwareTypes.h"
+
+namespace scopeone::core
+{
+ class SCOPEONE_CORE_EXPORT ClockService
+ {
+ public:
+ ClockStamp now() const;
+ };
+}
diff --git a/ScopeOneCore/include/scopeone/HardwareProvider.h b/ScopeOneCore/include/scopeone/HardwareProvider.h
new file mode 100644
index 0000000..223998e
--- /dev/null
+++ b/ScopeOneCore/include/scopeone/HardwareProvider.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+#include "scopeone/HardwareTypes.h"
+
+namespace scopeone::core
+{
+ class HardwareProvider
+ {
+ public:
+ virtual ~HardwareProvider() = default;
+
+ virtual HardwareProviderDescriptor descriptor() const = 0;
+ virtual QList devices() const = 0;
+ };
+
+ using HardwareProviderPtr = std::shared_ptr;
+}
diff --git a/ScopeOneCore/include/scopeone/HardwareTypes.h b/ScopeOneCore/include/scopeone/HardwareTypes.h
new file mode 100644
index 0000000..9183df5
--- /dev/null
+++ b/ScopeOneCore/include/scopeone/HardwareTypes.h
@@ -0,0 +1,83 @@
+#pragma once
+
+#include
+#include
+#include
+
+#include
+
+#include "scopeone/scopeone_core_export.h"
+
+namespace scopeone::core
+{
+ enum class HardwareDeviceKind
+ {
+ Unknown,
+ Camera,
+ XYStage,
+ ZStage,
+ Shutter,
+ State,
+ Hub
+ };
+
+ enum class HardwareDeviceState
+ {
+ Unknown,
+ Discovered,
+ Initialized,
+ Faulted,
+ Unavailable
+ };
+
+ enum class HardwareEndpointKind
+ {
+ InProcess,
+ DriverHost
+ };
+
+ struct SCOPEONE_CORE_EXPORT HardwareProviderDescriptor
+ {
+ QString id;
+ QString name;
+ QString version;
+ };
+
+ struct SCOPEONE_CORE_EXPORT HardwareDeviceDescriptor
+ {
+ QString logicalId;
+ QString providerId;
+ QString providerDeviceId;
+ QString hardwareId;
+ QString name;
+ HardwareDeviceKind kind{HardwareDeviceKind::Unknown};
+ HardwareDeviceState state{HardwareDeviceState::Unknown};
+ HardwareEndpointKind endpoint{HardwareEndpointKind::InProcess};
+ QVariantMap properties;
+ };
+
+ struct SCOPEONE_CORE_EXPORT ClockStamp
+ {
+ std::int64_t ticks{0};
+ std::int64_t tickPeriodNumerator{1};
+ std::int64_t tickPeriodDenominator{1000000000};
+ std::int64_t hostMonotonicNs{0};
+ QString clockDomain;
+ QString source;
+
+ bool isValid() const
+ {
+ return tickPeriodNumerator > 0
+ && tickPeriodDenominator > 0
+ && !clockDomain.isEmpty()
+ && !source.isEmpty();
+ }
+ };
+}
+
+Q_DECLARE_METATYPE(scopeone::core::HardwareDeviceKind)
+Q_DECLARE_METATYPE(scopeone::core::HardwareDeviceState)
+Q_DECLARE_METATYPE(scopeone::core::HardwareEndpointKind)
+Q_DECLARE_METATYPE(scopeone::core::HardwareProviderDescriptor)
+Q_DECLARE_METATYPE(scopeone::core::HardwareDeviceDescriptor)
+Q_DECLARE_METATYPE(scopeone::core::ClockStamp)
diff --git a/ScopeOneCore/include/scopeone/ImageFrame.h b/ScopeOneCore/include/scopeone/ImageFrame.h
index c2754b5..5241cef 100644
--- a/ScopeOneCore/include/scopeone/ImageFrame.h
+++ b/ScopeOneCore/include/scopeone/ImageFrame.h
@@ -8,6 +8,7 @@
#include
#include "scopeone/SharedFrame.h"
+#include "scopeone/HardwareTypes.h"
namespace scopeone::core
{
@@ -28,6 +29,7 @@ namespace scopeone::core
ImagePixelFormat pixelFormat{ImagePixelFormat::Invalid};
quint64 frameIndex{0};
quint64 timestampNs{0};
+ ClockStamp clockStamp;
int sourceRoiX{0};
int sourceRoiY{0};
int sourceRoiWidth{0};
@@ -169,6 +171,12 @@ namespace scopeone::core
frame.bitsPerSample = static_cast(header.bitsPerSample);
frame.frameIndex = header.frameIndex;
frame.timestampNs = header.timestampNs;
+ if (frame.timestampNs > 0)
+ {
+ frame.clockStamp.ticks = static_cast(frame.timestampNs);
+ frame.clockStamp.clockDomain = QStringLiteral("provider.timestampNs");
+ frame.clockStamp.source = QStringLiteral("Driver");
+ }
if (header.pixelFormat == static_cast(SharedPixelFormat::Mono16))
{
diff --git a/ScopeOneCore/include/scopeone/ScopeOneCore.h b/ScopeOneCore/include/scopeone/ScopeOneCore.h
index 29104b5..f8675d5 100644
--- a/ScopeOneCore/include/scopeone/ScopeOneCore.h
+++ b/ScopeOneCore/include/scopeone/ScopeOneCore.h
@@ -19,6 +19,8 @@
#include
#include "scopeone/ExperimentDocument.h"
+#include "scopeone/HardwareTypes.h"
+#include "scopeone/HardwareProvider.h"
#include "scopeone/ImageFrame.h"
#include "scopeone/scopeone_core_export.h"
@@ -71,6 +73,7 @@ namespace scopeone::core
int failCount{0};
int skippedCameraCount{0};
bool foundCamera{false};
+ QList devices;
};
struct HistogramStats
@@ -527,6 +530,9 @@ namespace scopeone::core
bool setAdditionalDeviceAdapterSearchPaths(const QStringList& paths);
QStringList cameraIds() const { return m_cameraIds; }
+ QList hardwareDevices() const;
+ bool registerHardwareProvider(const HardwareProviderPtr& provider);
+ bool unregisterHardwareProvider(const QString& providerId);
QStringList runningPreviewCameraIds() const;
double cameraPixelSizeUm(const QString& cameraId) const;
bool setCameraPixelSizeUm(const QString& cameraId, double pixelSizeUm);
@@ -666,6 +672,7 @@ namespace scopeone::core
const QString& errorMessage);
void configurationUnloadFinished(bool success, const QString& errorMessage);
void hardwareConfigurationChanged();
+ void hardwareDevicesChanged();
void deviceStateChanged();
void stagePositionChanged();
void stageMoveFinished(quint64 commandId,
@@ -781,6 +788,7 @@ namespace scopeone::core
void applySystemShutdownPreset();
void applyLoadedConfiguration(const QString& configPath,
const LoadConfigResult& result);
+ void synchronizeCameraIdsFromRegistry();
void finishConfigurationLoadFailure(const LoadConfigResult& result,
const QString& errorMessage);
void clearConfigurationRuntime(bool notify, bool shutdownCameraBackend);
diff --git a/ScopeOneCore/include/scopeone/SimulatorProvider.h b/ScopeOneCore/include/scopeone/SimulatorProvider.h
new file mode 100644
index 0000000..ae63351
--- /dev/null
+++ b/ScopeOneCore/include/scopeone/SimulatorProvider.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include
+#include
+#include
+
+#include "scopeone/CameraProvider.h"
+#include "scopeone/HardwareProvider.h"
+
+namespace scopeone::core
+{
+ class SCOPEONE_CORE_EXPORT SimulatorProvider final
+ : public QObject,
+ public HardwareProvider,
+ public CameraProvider
+ {
+ public:
+ explicit SimulatorProvider(const QString& logicalCameraId = QStringLiteral("camera.simulator"),
+ int width = 512,
+ int height = 512);
+
+ HardwareProviderDescriptor descriptor() const override;
+ QList devices() const override;
+ void setFrameSink(FrameSink sink) override;
+ bool startPreview() override;
+ bool stopPreview() override;
+ bool startPreviewFor(const QString& cameraId) override;
+ bool stopPreviewFor(const QString& cameraId) override;
+ bool isPreviewRunning(const QString& cameraId) const override;
+ bool getExposure(const QString& cameraIdOrAll, double& exposureMs) const override;
+ bool setExposure(const QString& cameraIdOrAll, double exposureMs) override;
+ QStringList listProperties(const QString& cameraId) override;
+ QString getProperty(const QString& cameraId,
+ const QString& name,
+ bool fromCache) override;
+ bool setProperty(const QString& cameraId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage) override;
+ QString getPropertyType(const QString& cameraId, const QString& name) override;
+ bool isPropertyReadOnly(const QString& cameraId, const QString& name) override;
+ bool isPropertyPreInit(const QString& cameraId, const QString& name) override;
+ QStringList getAllowedPropertyValues(const QString& cameraId,
+ const QString& name) override;
+ bool hasPropertyLimits(const QString& cameraId, const QString& name) override;
+ double getPropertyLowerLimit(const QString& cameraId, const QString& name) override;
+ double getPropertyUpperLimit(const QString& cameraId, const QString& name) override;
+ bool setROI(const QString& cameraId, int x, int y, int width, int height) override;
+ bool clearROI(const QString& cameraId) override;
+ bool getROI(const QString& cameraId,
+ int& x,
+ int& y,
+ int& width,
+ int& height) override;
+ bool captureEventFrame(const QString& cameraId,
+ ImageFrame& frame,
+ int timeoutMs) override;
+
+ private:
+ bool accepts(const QString& cameraIdOrAll) const;
+ ImageFrame makeFrame();
+ void updateTimerInterval();
+
+ QString m_providerId;
+ QString m_cameraId;
+ int m_sensorWidth{512};
+ int m_sensorHeight{512};
+ QRect m_roi;
+ double m_exposureMs{10.0};
+ quint64 m_frameIndex{0};
+ FrameSink m_frameSink;
+ QTimer m_timer;
+ };
+}
diff --git a/ScopeOneCore/internal/AcquisitionEngine.h b/ScopeOneCore/internal/AcquisitionEngine.h
new file mode 100644
index 0000000..1401a95
--- /dev/null
+++ b/ScopeOneCore/internal/AcquisitionEngine.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include
+#include
+
+#include "scopeone/CameraProvider.h"
+
+namespace scopeone::core::internal
+{
+ class DeviceRegistry;
+
+ class AcquisitionEngine : public QObject
+ {
+ Q_OBJECT
+
+ public:
+ enum class State
+ {
+ Idle,
+ Prepared,
+ Running
+ };
+
+ AcquisitionEngine(DeviceRegistry* deviceRegistry, QObject* parent = nullptr);
+
+ void prepare();
+ void reset();
+ bool start(const QString& cameraIdOrAll);
+ bool stop(const QString& cameraIdOrAll);
+ State state() const { return m_state; }
+
+ private:
+ DeviceRegistry* m_deviceRegistry{nullptr};
+ State m_state{State::Idle};
+ };
+}
diff --git a/ScopeOneCore/internal/AgentProtocol.h b/ScopeOneCore/internal/AgentProtocol.h
index ae1d07c..e97ea2a 100644
--- a/ScopeOneCore/internal/AgentProtocol.h
+++ b/ScopeOneCore/internal/AgentProtocol.h
@@ -1,24 +1,25 @@
#pragma once
-#include
-#include
-#include
#include
-#include
+
+#include "internal/DriverHostProtocol.h"
namespace scopeone::core::internal::agent
{
- inline constexpr quint32 kProtocolVersion = 3;
- inline constexpr quint32 kMaxControlMessageBytes = 256 * 1024;
-
- inline const QString kEnvelopeKindField = QStringLiteral("kind");
- inline const QString kEnvelopeVersionField = QStringLiteral("version");
- inline const QString kEnvelopeRequestIdField = QStringLiteral("requestId");
- inline const QString kMessageTypeField = QStringLiteral("type");
-
- inline const QString kMessageKindRequest = QStringLiteral("Request");
- inline const QString kMessageKindResponse = QStringLiteral("Response");
- inline const QString kMessageKindEvent = QStringLiteral("Event");
+ using driverhost::kProtocolVersion;
+ using driverhost::kEnvelopeKindField;
+ using driverhost::kEnvelopeVersionField;
+ using driverhost::kEnvelopeRequestIdField;
+ using driverhost::kMessageTypeField;
+ using driverhost::kMessageKindRequest;
+ using driverhost::kMessageKindResponse;
+ using driverhost::kMessageKindEvent;
+ using driverhost::encodeUInt64;
+ using driverhost::decodeUInt64;
+ using driverhost::makeEnvelope;
+ using driverhost::encodeMessage;
+ using driverhost::DecodeResult;
+ using driverhost::tryDecodeMessage;
inline const QString kCommandShutdown = QStringLiteral("Shutdown");
inline const QString kCommandStartPreview = QStringLiteral("StartPreview");
@@ -54,100 +55,4 @@ namespace scopeone::core::internal::agent
return QStringLiteral("ScopeOne.%1.shm").arg(cameraId);
}
- inline QString encodeUInt64(quint64 value)
- {
- return QString::number(value);
- }
-
- inline quint64 decodeUInt64(const QJsonValue& value, quint64 defaultValue = 0)
- {
- if (value.isString())
- {
- bool ok = false;
- const quint64 parsed = value.toString().toULongLong(&ok);
- return ok ? parsed : defaultValue;
- }
- if (value.isDouble())
- {
- const double numeric = value.toDouble(static_cast(defaultValue));
- return (numeric >= 0.0) ? static_cast(numeric) : defaultValue;
- }
- return defaultValue;
- }
-
- inline QJsonObject makeEnvelope(const QString& kind,
- const QString& type,
- quint64 requestId = 0)
- {
- QJsonObject obj;
- obj.insert(kEnvelopeKindField, kind);
- obj.insert(kEnvelopeVersionField, static_cast(kProtocolVersion));
- obj.insert(kMessageTypeField, type);
- if (requestId != 0)
- {
- obj.insert(kEnvelopeRequestIdField, encodeUInt64(requestId));
- }
- return obj;
- }
-
- inline QByteArray encodeMessage(const QJsonObject& message)
- {
- const QByteArray payload = QJsonDocument(message).toJson(QJsonDocument::Compact);
- QByteArray framed;
- framed.resize(static_cast(sizeof(quint32)));
- qToLittleEndian(static_cast(payload.size()),
- reinterpret_cast(framed.data()));
- framed += payload;
- return framed;
- }
-
- enum class DecodeResult
- {
- Incomplete,
- Complete,
- Error
- };
-
- inline DecodeResult tryDecodeMessage(QByteArray& buffer, QJsonObject& message, QString* error = nullptr)
- {
- if (buffer.size() < static_cast(sizeof(quint32)))
- {
- return DecodeResult::Incomplete;
- }
-
- const quint32 payloadSize =
- qFromLittleEndian(reinterpret_cast(buffer.constData()));
- if (payloadSize == 0 || payloadSize > kMaxControlMessageBytes)
- {
- if (error)
- {
- *error = QStringLiteral("Invalid control message size");
- }
- buffer.clear();
- return DecodeResult::Error;
- }
-
- const int frameSize = static_cast(sizeof(quint32) + payloadSize);
- if (buffer.size() < frameSize)
- {
- return DecodeResult::Incomplete;
- }
-
- const QByteArray payload = buffer.mid(static_cast(sizeof(quint32)), static_cast(payloadSize));
- buffer.remove(0, frameSize);
-
- QJsonParseError parseError{};
- const QJsonDocument doc = QJsonDocument::fromJson(payload, &parseError);
- if (parseError.error != QJsonParseError::NoError || !doc.isObject())
- {
- if (error)
- {
- *error = QStringLiteral("Malformed control message payload");
- }
- return DecodeResult::Error;
- }
-
- message = doc.object();
- return DecodeResult::Complete;
- }
} // namespace scopeone::core::internal::agent
diff --git a/ScopeOneCore/internal/CameraManager.h b/ScopeOneCore/internal/CameraManager.h
index 9be2d48..bc44bff 100644
--- a/ScopeOneCore/internal/CameraManager.h
+++ b/ScopeOneCore/internal/CameraManager.h
@@ -9,12 +9,14 @@
#include
#include "internal/CameraBackend.h"
+#include "scopeone/CameraProvider.h"
+#include "internal/CameraRuntimeControl.h"
class CMMCore;
namespace scopeone::core::internal
{
- class CameraManager : public QObject
+ class CameraManager : public QObject, public CameraProvider, public CameraRuntimeControl
{
Q_OBJECT
@@ -22,6 +24,8 @@ namespace scopeone::core::internal
explicit CameraManager(QObject* parent = nullptr);
~CameraManager() override;
+ void setFrameSink(FrameSink sink) override;
+
bool configureNativeCamera(const std::shared_ptr& core,
const QString& cameraId,
double exposureMs = 0.0);
@@ -34,42 +38,42 @@ namespace scopeone::core::internal
void shutdownNow();
void shutdown(std::function completion);
- bool startPreview();
- bool stopPreview();
+ bool startPreview() override;
+ bool stopPreview() override;
bool usesAgentBackend() const;
- 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 isProcessingFrameTokenCurrent(const QString& cameraId, quint64 token);
- void finishProcessingFrame(const QString& cameraId, quint64 token);
+ bool startPreviewFor(const QString& cameraId) override;
+ bool stopPreviewFor(const QString& cameraId) override;
+ bool isPreviewRunning(const QString& cameraId) const override;
+ void setFrameDeliveryPaused(bool paused) override;
+ bool setRecordingFrameDeliveryEnabled(bool enabled) override;
+ bool setHighRateFrameDeliveryEnabled(bool enabled) override;
+ bool isProcessingFrameTokenCurrent(const QString& cameraId, quint64 token) override;
+ void finishProcessingFrame(const QString& cameraId, quint64 token) override;
- bool getExposure(const QString& cameraIdOrAll, double& exposureMs) const;
- bool setExposure(const QString& cameraIdOrAll, double exposureMs);
- QStringList listProperties(const QString& cameraId);
+ bool getExposure(const QString& cameraIdOrAll, double& exposureMs) const override;
+ bool setExposure(const QString& cameraIdOrAll, double exposureMs) override;
+ QStringList listProperties(const QString& cameraId) override;
QString getProperty(const QString& cameraId,
const QString& name,
- bool fromCache = false);
+ bool fromCache = false) override;
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);
+ QString* errorMessage = nullptr) override;
+ QString getPropertyType(const QString& cameraId, const QString& name) override;
+ bool isPropertyReadOnly(const QString& cameraId, const QString& name) override;
+ bool isPropertyPreInit(const QString& cameraId, const QString& name) override;
+ QStringList getAllowedPropertyValues(const QString& cameraId, const QString& name) override;
+ bool hasPropertyLimits(const QString& cameraId, const QString& name) override;
+ double getPropertyLowerLimit(const QString& cameraId, const QString& name) override;
+ double getPropertyUpperLimit(const QString& cameraId, const QString& name) override;
- 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 setROI(const QString& cameraId, int x, int y, int width, int height) override;
+ bool clearROI(const QString& cameraId) override;
+ bool getROI(const QString& cameraId, int& x, int& y, int& width, int& height) override;
bool captureEventFrame(const QString& cameraId,
scopeone::core::ImageFrame& frame,
- int timeoutMs = 1500);
+ int timeoutMs = 1500) override;
signals:
void newRawFrameReady(const scopeone::core::ImageFrame& frame);
@@ -85,6 +89,7 @@ namespace scopeone::core::internal
ProcessingFrameGate m_processingFrameGate;
std::unique_ptr m_backend;
+ FrameSink m_frameSink;
bool m_recordingFrameDeliveryEnabled{false};
bool m_highRateFrameDeliveryEnabled{false};
QMap m_propertyNamesCache;
diff --git a/ScopeOneCore/internal/CameraRuntimeControl.h b/ScopeOneCore/internal/CameraRuntimeControl.h
new file mode 100644
index 0000000..b104b2e
--- /dev/null
+++ b/ScopeOneCore/internal/CameraRuntimeControl.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include
+#include
+
+namespace scopeone::core::internal
+{
+ class CameraRuntimeControl
+ {
+ public:
+ virtual ~CameraRuntimeControl() = default;
+
+ virtual void setFrameDeliveryPaused(bool paused) = 0;
+ virtual bool setRecordingFrameDeliveryEnabled(bool enabled) = 0;
+ virtual bool setHighRateFrameDeliveryEnabled(bool enabled) = 0;
+ virtual bool isProcessingFrameTokenCurrent(const QString& cameraId,
+ quint64 token) = 0;
+ virtual void finishProcessingFrame(const QString& cameraId, quint64 token) = 0;
+ };
+}
diff --git a/ScopeOneCore/internal/DriverHostProtocol.h b/ScopeOneCore/internal/DriverHostProtocol.h
new file mode 100644
index 0000000..36793d3
--- /dev/null
+++ b/ScopeOneCore/internal/DriverHostProtocol.h
@@ -0,0 +1,118 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+namespace scopeone::core::internal::driverhost
+{
+ inline constexpr quint32 kProtocolVersion = 3;
+ inline constexpr quint32 kMaxControlMessageBytes = 256 * 1024;
+
+ inline const QString kEnvelopeKindField = QStringLiteral("kind");
+ inline const QString kEnvelopeVersionField = QStringLiteral("version");
+ inline const QString kEnvelopeRequestIdField = QStringLiteral("requestId");
+ inline const QString kMessageTypeField = QStringLiteral("type");
+
+ inline const QString kMessageKindRequest = QStringLiteral("Request");
+ inline const QString kMessageKindResponse = QStringLiteral("Response");
+ inline const QString kMessageKindEvent = QStringLiteral("Event");
+
+ inline QString encodeUInt64(quint64 value)
+ {
+ return QString::number(value);
+ }
+
+ inline quint64 decodeUInt64(const QJsonValue& value, quint64 defaultValue = 0)
+ {
+ if (value.isString())
+ {
+ bool ok = false;
+ const quint64 parsed = value.toString().toULongLong(&ok);
+ return ok ? parsed : defaultValue;
+ }
+ if (value.isDouble())
+ {
+ const double numeric = value.toDouble(static_cast(defaultValue));
+ return numeric >= 0.0 ? static_cast(numeric) : defaultValue;
+ }
+ return defaultValue;
+ }
+
+ inline QJsonObject makeEnvelope(const QString& kind,
+ const QString& type,
+ quint64 requestId = 0)
+ {
+ QJsonObject object;
+ object.insert(kEnvelopeKindField, kind);
+ object.insert(kEnvelopeVersionField, static_cast(kProtocolVersion));
+ object.insert(kMessageTypeField, type);
+ if (requestId != 0)
+ {
+ object.insert(kEnvelopeRequestIdField, encodeUInt64(requestId));
+ }
+ return object;
+ }
+
+ inline QByteArray encodeMessage(const QJsonObject& message)
+ {
+ const QByteArray payload = QJsonDocument(message).toJson(QJsonDocument::Compact);
+ QByteArray framed;
+ framed.resize(static_cast(sizeof(quint32)));
+ qToLittleEndian(static_cast(payload.size()),
+ reinterpret_cast(framed.data()));
+ framed += payload;
+ return framed;
+ }
+
+ enum class DecodeResult
+ {
+ Incomplete,
+ Complete,
+ Error
+ };
+
+ inline DecodeResult tryDecodeMessage(QByteArray& buffer,
+ QJsonObject& message,
+ QString* error = nullptr)
+ {
+ if (buffer.size() < static_cast(sizeof(quint32)))
+ {
+ return DecodeResult::Incomplete;
+ }
+ const quint32 payloadSize =
+ qFromLittleEndian(reinterpret_cast(buffer.constData()));
+ if (payloadSize == 0 || payloadSize > kMaxControlMessageBytes)
+ {
+ if (error)
+ {
+ *error = QStringLiteral("Invalid control message size");
+ }
+ buffer.clear();
+ return DecodeResult::Error;
+ }
+ const int frameSize = static_cast(sizeof(quint32) + payloadSize);
+ if (buffer.size() < frameSize)
+ {
+ return DecodeResult::Incomplete;
+ }
+
+ const QByteArray payload = buffer.mid(static_cast(sizeof(quint32)),
+ static_cast(payloadSize));
+ buffer.remove(0, frameSize);
+ QJsonParseError parseError{};
+ const QJsonDocument document = QJsonDocument::fromJson(payload, &parseError);
+ if (parseError.error != QJsonParseError::NoError || !document.isObject())
+ {
+ if (error)
+ {
+ *error = QStringLiteral("Malformed control message payload");
+ }
+ return DecodeResult::Error;
+ }
+ message = document.object();
+ return DecodeResult::Complete;
+ }
+}
diff --git a/ScopeOneCore/internal/FrameRouter.h b/ScopeOneCore/internal/FrameRouter.h
new file mode 100644
index 0000000..f7650c2
--- /dev/null
+++ b/ScopeOneCore/internal/FrameRouter.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include
+
+#include "scopeone/ImageFrame.h"
+
+namespace scopeone::core
+{
+ class ClockService;
+}
+
+namespace scopeone::core::internal
+{
+ class FrameRouter : public QObject
+ {
+ Q_OBJECT
+
+ public:
+ FrameRouter(scopeone::core::ClockService* clockService,
+ QObject* parent = nullptr);
+ void publish(const scopeone::core::ImageFrame& frame);
+
+ signals:
+ void frameReady(const scopeone::core::ImageFrame& frame);
+
+ private:
+ scopeone::core::ClockService* m_clockService{nullptr};
+ };
+}
diff --git a/ScopeOneCore/internal/HardwareRuntime.h b/ScopeOneCore/internal/HardwareRuntime.h
new file mode 100644
index 0000000..0458291
--- /dev/null
+++ b/ScopeOneCore/internal/HardwareRuntime.h
@@ -0,0 +1,113 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include "scopeone/HardwareProvider.h"
+#include "scopeone/ClockService.h"
+#include "scopeone/CameraProvider.h"
+#include "internal/AcquisitionEngine.h"
+#include "internal/FrameRouter.h"
+
+namespace scopeone::core::internal
+{
+ class DeviceRegistry : public QObject
+ {
+ Q_OBJECT
+
+ public:
+ explicit DeviceRegistry(QObject* parent = nullptr);
+
+ void clear();
+ bool registerProvider(const HardwareProviderPtr& provider);
+ void unregisterProvider(const QString& providerId);
+ void refreshProvider(const QString& providerId);
+ QList providers() const;
+ QList devices() const;
+ HardwareDeviceDescriptor device(const QString& logicalId) const;
+ HardwareProviderPtr provider(const QString& providerId) const;
+ HardwareProviderPtr providerForDevice(const QString& logicalId) const;
+
+ signals:
+ void changed();
+
+ private:
+ struct ProviderEntry
+ {
+ HardwareProviderPtr provider;
+ QList devices;
+ };
+
+ QHash m_providers;
+ };
+
+ class HardwareRuntime : public QObject, public CameraProvider
+ {
+ Q_OBJECT
+
+ public:
+ explicit HardwareRuntime(QObject* parent = nullptr);
+
+ void setFrameSink(FrameSink sink) override;
+ bool startPreview() override;
+ bool stopPreview() override;
+ bool startPreviewFor(const QString& cameraId) override;
+ bool stopPreviewFor(const QString& cameraId) override;
+ bool isPreviewRunning(const QString& cameraId) const override;
+ bool getExposure(const QString& cameraIdOrAll, double& exposureMs) const override;
+ bool setExposure(const QString& cameraIdOrAll, double exposureMs) override;
+ QStringList listProperties(const QString& cameraId) override;
+ QString getProperty(const QString& cameraId,
+ const QString& name,
+ bool fromCache) override;
+ bool setProperty(const QString& cameraId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage) override;
+ QString getPropertyType(const QString& cameraId, const QString& name) override;
+ bool isPropertyReadOnly(const QString& cameraId, const QString& name) override;
+ bool isPropertyPreInit(const QString& cameraId, const QString& name) override;
+ QStringList getAllowedPropertyValues(const QString& cameraId,
+ const QString& name) override;
+ bool hasPropertyLimits(const QString& cameraId, const QString& name) override;
+ double getPropertyLowerLimit(const QString& cameraId, const QString& name) override;
+ double getPropertyUpperLimit(const QString& cameraId, const QString& name) override;
+ bool setROI(const QString& cameraId, int x, int y, int width, int height) override;
+ bool clearROI(const QString& cameraId) override;
+ bool getROI(const QString& cameraId,
+ int& x,
+ int& y,
+ int& width,
+ int& height) override;
+ bool captureEventFrame(const QString& cameraId,
+ ImageFrame& frame,
+ int timeoutMs) override;
+
+ DeviceRegistry* deviceRegistry() { return &m_registry; }
+ const DeviceRegistry* deviceRegistry() const { return &m_registry; }
+ AcquisitionEngine* acquisitionEngine() { return &m_acquisitionEngine; }
+ ClockService* clockService() { return &m_clockService; }
+ FrameRouter* frameRouter() { return &m_frameRouter; }
+ void clear();
+ bool registerProvider(const HardwareProviderPtr& provider);
+ void unregisterProvider(const QString& providerId);
+ void refreshProvider(const QString& providerId);
+
+ signals:
+ void devicesChanged();
+
+ private:
+ CameraProvider* cameraProviderForDevice(const QString& logicalId) const;
+ QList cameraProviders() const;
+
+ DeviceRegistry m_registry;
+ ClockService m_clockService;
+ FrameRouter m_frameRouter;
+ AcquisitionEngine m_acquisitionEngine;
+ FrameSink m_frameSink;
+ };
+}
diff --git a/ScopeOneCore/internal/MDAManager.h b/ScopeOneCore/internal/MDAManager.h
index 0f24a06..eb50e47 100644
--- a/ScopeOneCore/internal/MDAManager.h
+++ b/ScopeOneCore/internal/MDAManager.h
@@ -10,13 +10,12 @@
#include
#include "scopeone/ExperimentDocument.h"
+#include "scopeone/CameraProvider.h"
class CMMCore;
namespace scopeone::core::internal
{
- class CameraManager;
-
struct MDAOutput
{
AcquisitionEvent event;
@@ -37,7 +36,7 @@ namespace scopeone::core::internal
bool isRunning() const { return m_running.load(); }
- void setCameraManager(CameraManager* cameraManager);
+ void setCameraProvider(CameraProvider* cameraProvider);
bool start(const QList& events, bool block = false);
void requestCancel();
void cancelAndWait();
@@ -59,7 +58,7 @@ namespace scopeone::core::internal
void runSequence(QList events);
std::shared_ptr m_mmcore;
- CameraManager* m_cameraManager{nullptr};
+ CameraProvider* m_cameraProvider{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 fc7f6e8..0f384c1 100644
--- a/ScopeOneCore/internal/MMCoreManager.h
+++ b/ScopeOneCore/internal/MMCoreManager.h
@@ -5,6 +5,7 @@
#include
#include
#include "MMCore.h"
+#include "scopeone/HardwareTypes.h"
namespace scopeone::core::internal
{
@@ -35,6 +36,7 @@ namespace scopeone::core::internal
int skippedCameraCount{0};
bool foundCamera{false};
bool useSingleCamera{false};
+ QList devices;
};
explicit MMCoreManager(QObject* parent = nullptr);
diff --git a/ScopeOneCore/internal/MicroManagerProvider.h b/ScopeOneCore/internal/MicroManagerProvider.h
new file mode 100644
index 0000000..423064b
--- /dev/null
+++ b/ScopeOneCore/internal/MicroManagerProvider.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include
+
+#include "scopeone/HardwareProvider.h"
+#include "scopeone/CameraProvider.h"
+
+namespace scopeone::core::internal
+{
+ class MicroManagerProvider final : public HardwareProvider, public CameraProvider
+ {
+ public:
+ explicit MicroManagerProvider(CameraProvider* cameraProvider);
+
+ HardwareProviderDescriptor descriptor() const override;
+ QList devices() const override;
+ void setDevices(const QList& devices);
+
+ void setFrameSink(FrameSink sink) override;
+ bool startPreview() override;
+ bool stopPreview() override;
+ bool startPreviewFor(const QString& cameraId) override;
+ bool stopPreviewFor(const QString& cameraId) override;
+ bool isPreviewRunning(const QString& cameraId) const override;
+ bool getExposure(const QString& cameraIdOrAll, double& exposureMs) const override;
+ bool setExposure(const QString& cameraIdOrAll, double exposureMs) override;
+ QStringList listProperties(const QString& cameraId) override;
+ QString getProperty(const QString& cameraId,
+ const QString& name,
+ bool fromCache) override;
+ bool setProperty(const QString& cameraId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage) override;
+ QString getPropertyType(const QString& cameraId, const QString& name) override;
+ bool isPropertyReadOnly(const QString& cameraId, const QString& name) override;
+ bool isPropertyPreInit(const QString& cameraId, const QString& name) override;
+ QStringList getAllowedPropertyValues(const QString& cameraId,
+ const QString& name) override;
+ bool hasPropertyLimits(const QString& cameraId, const QString& name) override;
+ double getPropertyLowerLimit(const QString& cameraId, const QString& name) override;
+ double getPropertyUpperLimit(const QString& cameraId, const QString& name) override;
+ bool setROI(const QString& cameraId, int x, int y, int width, int height) override;
+ bool clearROI(const QString& cameraId) override;
+ bool getROI(const QString& cameraId,
+ int& x,
+ int& y,
+ int& width,
+ int& height) override;
+ bool captureEventFrame(const QString& cameraId,
+ scopeone::core::ImageFrame& frame,
+ int timeoutMs) override;
+
+ private:
+ CameraProvider* m_cameraProvider{nullptr};
+ QList m_devices;
+ };
+}
diff --git a/ScopeOneCore/internal/RecordingManager.h b/ScopeOneCore/internal/RecordingManager.h
index ae99e69..4859b20 100644
--- a/ScopeOneCore/internal/RecordingManager.h
+++ b/ScopeOneCore/internal/RecordingManager.h
@@ -1,7 +1,9 @@
#pragma once
#include "scopeone/ScopeOneCore.h"
+#include "scopeone/CameraProvider.h"
#include "internal/MDAManager.h"
+#include "internal/CameraRuntimeControl.h"
#include
#include
#include
@@ -25,8 +27,6 @@ namespace scopeone::core::internal
using RecordingWriterPhase = scopeone::core::ScopeOneCore::RecordingWriterPhase;
using RecordingWriterStatus = scopeone::core::ScopeOneCore::RecordingWriterStatus;
- class CameraManager;
-
class RecordingManager : public QObject
{
Q_OBJECT
@@ -35,7 +35,11 @@ namespace scopeone::core::internal
explicit RecordingManager(QObject* parent = nullptr);
~RecordingManager() override;
- void setCameraManager(CameraManager* cameraManager) { m_cameraManager = cameraManager; }
+ void setCameraProvider(CameraProvider* cameraProvider) { m_cameraProvider = cameraProvider; }
+ void setCameraRuntimeControl(CameraRuntimeControl* cameraRuntimeControl)
+ {
+ m_cameraRuntimeControl = cameraRuntimeControl;
+ }
void setMMCore(const std::shared_ptr& core) { m_mmcore = core; }
void setLatestFrameFetcher(std::function fetcher)
@@ -203,7 +207,8 @@ namespace scopeone::core::internal
bool allCamerasReachedTarget() const;
void advanceBurstStateIfNeeded();
- CameraManager* m_cameraManager{nullptr};
+ CameraProvider* m_cameraProvider{nullptr};
+ CameraRuntimeControl* m_cameraRuntimeControl{nullptr};
std::shared_ptr m_mmcore;
std::function m_latestFrameFetcher;
std::function m_sessionPreparationCallback;
diff --git a/ScopeOneCore/src/AcquisitionEngine.cpp b/ScopeOneCore/src/AcquisitionEngine.cpp
new file mode 100644
index 0000000..7e44f9b
--- /dev/null
+++ b/ScopeOneCore/src/AcquisitionEngine.cpp
@@ -0,0 +1,129 @@
+#include "internal/AcquisitionEngine.h"
+
+#include "scopeone/CameraProvider.h"
+#include "internal/HardwareRuntime.h"
+
+#include
+
+namespace scopeone::core::internal
+{
+ AcquisitionEngine::AcquisitionEngine(DeviceRegistry* deviceRegistry, QObject* parent)
+ : QObject(parent)
+ , m_deviceRegistry(deviceRegistry)
+ {
+ }
+
+ void AcquisitionEngine::prepare()
+ {
+ m_state = State::Prepared;
+ }
+
+ void AcquisitionEngine::reset()
+ {
+ m_state = State::Idle;
+ }
+
+ bool AcquisitionEngine::start(const QString& cameraIdOrAll)
+ {
+ if (!m_deviceRegistry || m_state == State::Idle)
+ {
+ return false;
+ }
+ const QString target = cameraIdOrAll.trimmed();
+ if (target.isEmpty())
+ {
+ return false;
+ }
+ bool started = false;
+ if (target.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0)
+ {
+ QList startedProviders;
+ QSet visited;
+ started = true;
+ for (const HardwareDeviceDescriptor& device : m_deviceRegistry->devices())
+ {
+ if (device.kind != HardwareDeviceKind::Camera)
+ {
+ continue;
+ }
+ const HardwareProviderPtr provider = m_deviceRegistry->provider(device.providerId);
+ auto* cameraProvider = dynamic_cast(provider.get());
+ if (!cameraProvider || visited.contains(cameraProvider))
+ {
+ continue;
+ }
+ visited.insert(cameraProvider);
+ if (!cameraProvider->startPreview())
+ {
+ started = false;
+ for (CameraProvider* activeProvider : startedProviders)
+ {
+ activeProvider->stopPreview();
+ }
+ break;
+ }
+ startedProviders.append(cameraProvider);
+ }
+ started = started && !startedProviders.isEmpty();
+ }
+ else
+ {
+ const HardwareProviderPtr provider = m_deviceRegistry->providerForDevice(target);
+ auto* cameraProvider = dynamic_cast(provider.get());
+ started = cameraProvider && cameraProvider->startPreviewFor(target);
+ }
+ if (started)
+ {
+ m_state = State::Running;
+ }
+ return started;
+ }
+
+ bool AcquisitionEngine::stop(const QString& cameraIdOrAll)
+ {
+ if (!m_deviceRegistry || m_state == State::Idle)
+ {
+ return false;
+ }
+ const QString target = cameraIdOrAll.trimmed();
+ if (target.isEmpty())
+ {
+ return false;
+ }
+ bool stopped = false;
+ if (target.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0)
+ {
+ QSet visited;
+ stopped = true;
+ bool found = false;
+ for (const HardwareDeviceDescriptor& device : m_deviceRegistry->devices())
+ {
+ if (device.kind != HardwareDeviceKind::Camera)
+ {
+ continue;
+ }
+ const HardwareProviderPtr provider = m_deviceRegistry->provider(device.providerId);
+ auto* cameraProvider = dynamic_cast(provider.get());
+ if (!cameraProvider || visited.contains(cameraProvider))
+ {
+ continue;
+ }
+ found = true;
+ visited.insert(cameraProvider);
+ stopped = cameraProvider->stopPreview() && stopped;
+ }
+ stopped = found && stopped;
+ }
+ else
+ {
+ const HardwareProviderPtr provider = m_deviceRegistry->providerForDevice(target);
+ auto* cameraProvider = dynamic_cast(provider.get());
+ stopped = cameraProvider && cameraProvider->stopPreviewFor(target);
+ }
+ if (stopped && target.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0)
+ {
+ m_state = State::Prepared;
+ }
+ return stopped;
+ }
+}
diff --git a/ScopeOneCore/src/CameraManager.cpp b/ScopeOneCore/src/CameraManager.cpp
index 1703583..838d754 100644
--- a/ScopeOneCore/src/CameraManager.cpp
+++ b/ScopeOneCore/src/CameraManager.cpp
@@ -1,6 +1,7 @@
#include "internal/CameraManager.h"
#include
+#include
namespace scopeone::core::internal
{
@@ -28,6 +29,11 @@ namespace scopeone::core::internal
shutdownNow();
}
+ void CameraManager::setFrameSink(FrameSink sink)
+ {
+ m_frameSink = std::move(sink);
+ }
+
// Selects one camera backend and forwards its runtime signals
bool CameraManager::activateBackend(CameraBackend::Kind kind)
{
@@ -46,7 +52,14 @@ namespace scopeone::core::internal
}
connect(m_backend.get(), &CameraBackend::rawFrameReady,
- this, &CameraManager::newRawFrameReady);
+ this, [this](const scopeone::core::ImageFrame& frame)
+ {
+ if (m_frameSink)
+ {
+ m_frameSink(frame);
+ }
+ emit newRawFrameReady(frame);
+ });
// Keeps processing input on the producer thread
connect(m_backend.get(), &CameraBackend::processingFrameReady,
this, &CameraManager::processingFrameReady,
diff --git a/ScopeOneCore/src/ClockService.cpp b/ScopeOneCore/src/ClockService.cpp
new file mode 100644
index 0000000..ccd4b82
--- /dev/null
+++ b/ScopeOneCore/src/ClockService.cpp
@@ -0,0 +1,18 @@
+#include "scopeone/ClockService.h"
+
+#include
+
+namespace scopeone::core
+{
+ ClockStamp ClockService::now() const
+ {
+ const auto ticks = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch()).count();
+ ClockStamp stamp;
+ stamp.ticks = ticks;
+ stamp.hostMonotonicNs = ticks;
+ stamp.clockDomain = QStringLiteral("scopeone.host.monotonic");
+ stamp.source = QStringLiteral("HostEstimated");
+ return stamp;
+ }
+}
diff --git a/ScopeOneCore/src/FrameBufferUtils.cpp b/ScopeOneCore/src/FrameBufferUtils.cpp
index 17f7d3e..56113df 100644
--- a/ScopeOneCore/src/FrameBufferUtils.cpp
+++ b/ScopeOneCore/src/FrameBufferUtils.cpp
@@ -16,6 +16,7 @@ namespace scopeone::core::internal
dst.cameraId = src.cameraId;
dst.frameIndex = src.frameIndex;
dst.timestampNs = src.timestampNs;
+ dst.clockStamp = src.clockStamp;
dst.sourceRoiX = src.sourceRoiX;
dst.sourceRoiY = src.sourceRoiY;
dst.sourceRoiWidth = src.sourceRoiWidth;
diff --git a/ScopeOneCore/src/FrameRouter.cpp b/ScopeOneCore/src/FrameRouter.cpp
new file mode 100644
index 0000000..31b4446
--- /dev/null
+++ b/ScopeOneCore/src/FrameRouter.cpp
@@ -0,0 +1,22 @@
+#include "internal/FrameRouter.h"
+
+#include "scopeone/ClockService.h"
+
+namespace scopeone::core::internal
+{
+ FrameRouter::FrameRouter(scopeone::core::ClockService* clockService, QObject* parent)
+ : QObject(parent)
+ , m_clockService(clockService)
+ {
+ }
+
+ void FrameRouter::publish(const scopeone::core::ImageFrame& frame)
+ {
+ scopeone::core::ImageFrame routedFrame(frame);
+ if (!routedFrame.clockStamp.isValid() && m_clockService)
+ {
+ routedFrame.clockStamp = m_clockService->now();
+ }
+ emit frameReady(routedFrame);
+ }
+}
diff --git a/ScopeOneCore/src/HardwareRuntime.cpp b/ScopeOneCore/src/HardwareRuntime.cpp
new file mode 100644
index 0000000..5314467
--- /dev/null
+++ b/ScopeOneCore/src/HardwareRuntime.cpp
@@ -0,0 +1,477 @@
+#include "internal/HardwareRuntime.h"
+
+#include "scopeone/CameraProvider.h"
+
+#include
+#include
+#include
+
+namespace scopeone::core::internal
+{
+ DeviceRegistry::DeviceRegistry(QObject* parent)
+ : QObject(parent)
+ {
+ }
+
+ void DeviceRegistry::clear()
+ {
+ if (m_providers.isEmpty())
+ {
+ return;
+ }
+ m_providers.clear();
+ emit changed();
+ }
+
+ bool DeviceRegistry::registerProvider(const HardwareProviderPtr& provider)
+ {
+ if (!provider)
+ {
+ return false;
+ }
+ const HardwareProviderDescriptor descriptor = provider->descriptor();
+ const QString providerId = descriptor.id.trimmed();
+ if (providerId.isEmpty())
+ {
+ return false;
+ }
+ const QList devices = provider->devices();
+ QSet logicalIds;
+ for (const HardwareDeviceDescriptor& device : devices)
+ {
+ const QString logicalId = device.logicalId.trimmed();
+ if (logicalId.isEmpty()
+ || device.providerId.trimmed() != providerId
+ || logicalIds.contains(logicalId))
+ {
+ return false;
+ }
+ const HardwareDeviceDescriptor existing = this->device(logicalId);
+ if (!existing.logicalId.isEmpty() && existing.providerId != providerId)
+ {
+ return false;
+ }
+ logicalIds.insert(logicalId);
+ }
+ ProviderEntry entry;
+ entry.provider = provider;
+ entry.devices = devices;
+ m_providers.insert(providerId, std::move(entry));
+ emit changed();
+ return true;
+ }
+
+ void DeviceRegistry::unregisterProvider(const QString& providerId)
+ {
+ if (m_providers.remove(providerId.trimmed()) > 0)
+ {
+ emit changed();
+ }
+ }
+
+ void DeviceRegistry::refreshProvider(const QString& providerId)
+ {
+ const QString normalizedId = providerId.trimmed();
+ const auto it = m_providers.constFind(normalizedId);
+ if (it == m_providers.constEnd() || !it->provider)
+ {
+ return;
+ }
+ const HardwareProviderPtr provider = it->provider;
+ registerProvider(provider);
+ }
+
+ QList DeviceRegistry::providers() const
+ {
+ QList result;
+ result.reserve(m_providers.size());
+ for (auto it = m_providers.constBegin(); it != m_providers.constEnd(); ++it)
+ {
+ result.append(it->provider->descriptor());
+ }
+ std::sort(result.begin(), result.end(), [](const auto& lhs, const auto& rhs)
+ {
+ return lhs.id < rhs.id;
+ });
+ return result;
+ }
+
+ QList DeviceRegistry::devices() const
+ {
+ QList result;
+ for (auto it = m_providers.constBegin(); it != m_providers.constEnd(); ++it)
+ {
+ result.append(it->devices);
+ }
+ std::sort(result.begin(), result.end(), [](const auto& lhs, const auto& rhs)
+ {
+ return lhs.logicalId < rhs.logicalId;
+ });
+ return result;
+ }
+
+ HardwareDeviceDescriptor DeviceRegistry::device(const QString& logicalId) const
+ {
+ const QString normalizedId = logicalId.trimmed();
+ for (auto it = m_providers.constBegin(); it != m_providers.constEnd(); ++it)
+ {
+ for (const HardwareDeviceDescriptor& candidate : it->devices)
+ {
+ if (candidate.logicalId == normalizedId)
+ {
+ return candidate;
+ }
+ }
+ }
+ return {};
+ }
+
+ HardwareProviderPtr DeviceRegistry::provider(const QString& providerId) const
+ {
+ const auto it = m_providers.constFind(providerId.trimmed());
+ return it == m_providers.constEnd() ? HardwareProviderPtr{} : it->provider;
+ }
+
+ HardwareProviderPtr DeviceRegistry::providerForDevice(const QString& logicalId) const
+ {
+ const HardwareDeviceDescriptor descriptor = device(logicalId);
+ return descriptor.logicalId.isEmpty() ? HardwareProviderPtr{} : provider(descriptor.providerId);
+ }
+
+ HardwareRuntime::HardwareRuntime(QObject* parent)
+ : QObject(parent)
+ , m_registry(this)
+ , m_frameRouter(&m_clockService, this)
+ , m_acquisitionEngine(&m_registry, this)
+ {
+ connect(&m_registry, &DeviceRegistry::changed,
+ this, &HardwareRuntime::devicesChanged);
+ connect(&m_frameRouter, &FrameRouter::frameReady,
+ this, [this](const ImageFrame& frame)
+ {
+ if (m_frameSink)
+ {
+ m_frameSink(frame);
+ }
+ });
+ }
+
+ void HardwareRuntime::setFrameSink(FrameSink sink)
+ {
+ m_frameSink = std::move(sink);
+ }
+
+ CameraProvider* HardwareRuntime::cameraProviderForDevice(const QString& logicalId) const
+ {
+ const HardwareProviderPtr provider = m_registry.providerForDevice(logicalId);
+ return dynamic_cast(provider.get());
+ }
+
+ QList HardwareRuntime::cameraProviders() const
+ {
+ QList result;
+ QSet visited;
+ for (const HardwareDeviceDescriptor& device : m_registry.devices())
+ {
+ if (device.kind != HardwareDeviceKind::Camera)
+ {
+ continue;
+ }
+ const HardwareProviderPtr provider = m_registry.provider(device.providerId);
+ auto* cameraProvider = dynamic_cast(provider.get());
+ if (cameraProvider && !visited.contains(cameraProvider))
+ {
+ visited.insert(cameraProvider);
+ result.append(cameraProvider);
+ }
+ }
+ return result;
+ }
+
+ bool HardwareRuntime::startPreview()
+ {
+ return m_acquisitionEngine.start(QStringLiteral("All"));
+ }
+
+ bool HardwareRuntime::stopPreview()
+ {
+ return m_acquisitionEngine.stop(QStringLiteral("All"));
+ }
+
+ bool HardwareRuntime::startPreviewFor(const QString& cameraId)
+ {
+ return m_acquisitionEngine.start(cameraId);
+ }
+
+ bool HardwareRuntime::stopPreviewFor(const QString& cameraId)
+ {
+ return m_acquisitionEngine.stop(cameraId);
+ }
+
+ bool HardwareRuntime::isPreviewRunning(const QString& cameraId) const
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider && provider->isPreviewRunning(cameraId);
+ }
+
+ bool HardwareRuntime::getExposure(const QString& cameraIdOrAll, double& exposureMs) const
+ {
+ const QString target = cameraIdOrAll.trimmed();
+ if (target.compare(QStringLiteral("All"), Qt::CaseInsensitive) != 0)
+ {
+ CameraProvider* provider = cameraProviderForDevice(target);
+ return provider && provider->getExposure(target, exposureMs);
+ }
+ const QList providers = cameraProviders();
+ return !providers.isEmpty() && providers.first()->getExposure(QStringLiteral("All"), exposureMs);
+ }
+
+ bool HardwareRuntime::setExposure(const QString& cameraIdOrAll, double exposureMs)
+ {
+ const QString target = cameraIdOrAll.trimmed();
+ if (target.compare(QStringLiteral("All"), Qt::CaseInsensitive) != 0)
+ {
+ CameraProvider* provider = cameraProviderForDevice(target);
+ return provider && provider->setExposure(target, exposureMs);
+ }
+ const QList providers = cameraProviders();
+ bool ok = !providers.isEmpty();
+ for (CameraProvider* provider : providers)
+ {
+ ok = provider->setExposure(QStringLiteral("All"), exposureMs) && ok;
+ }
+ return ok;
+ }
+
+ QStringList HardwareRuntime::listProperties(const QString& cameraId)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider ? provider->listProperties(cameraId) : QStringList{};
+ }
+
+ QString HardwareRuntime::getProperty(const QString& cameraId,
+ const QString& name,
+ bool fromCache)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider ? provider->getProperty(cameraId, name, fromCache) : QString{};
+ }
+
+ bool HardwareRuntime::setProperty(const QString& cameraId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider && provider->setProperty(cameraId, name, value, errorMessage);
+ }
+
+ QString HardwareRuntime::getPropertyType(const QString& cameraId, const QString& name)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider ? provider->getPropertyType(cameraId, name) : QStringLiteral("Unknown");
+ }
+
+ bool HardwareRuntime::isPropertyReadOnly(const QString& cameraId, const QString& name)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return !provider || provider->isPropertyReadOnly(cameraId, name);
+ }
+
+ bool HardwareRuntime::isPropertyPreInit(const QString& cameraId, const QString& name)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider && provider->isPropertyPreInit(cameraId, name);
+ }
+
+ QStringList HardwareRuntime::getAllowedPropertyValues(const QString& cameraId,
+ const QString& name)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider ? provider->getAllowedPropertyValues(cameraId, name) : QStringList{};
+ }
+
+ bool HardwareRuntime::hasPropertyLimits(const QString& cameraId, const QString& name)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider && provider->hasPropertyLimits(cameraId, name);
+ }
+
+ double HardwareRuntime::getPropertyLowerLimit(const QString& cameraId, const QString& name)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider ? provider->getPropertyLowerLimit(cameraId, name) : 0.0;
+ }
+
+ double HardwareRuntime::getPropertyUpperLimit(const QString& cameraId, const QString& name)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider ? provider->getPropertyUpperLimit(cameraId, name) : 0.0;
+ }
+
+ bool HardwareRuntime::setROI(const QString& cameraId,
+ int x,
+ int y,
+ int width,
+ int height)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider && provider->setROI(cameraId, x, y, width, height);
+ }
+
+ bool HardwareRuntime::clearROI(const QString& cameraId)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider && provider->clearROI(cameraId);
+ }
+
+ bool HardwareRuntime::getROI(const QString& cameraId,
+ int& x,
+ int& y,
+ int& width,
+ int& height)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider && provider->getROI(cameraId, x, y, width, height);
+ }
+
+ bool HardwareRuntime::captureEventFrame(const QString& cameraId,
+ ImageFrame& frame,
+ int timeoutMs)
+ {
+ CameraProvider* provider = cameraProviderForDevice(cameraId);
+ return provider && provider->captureEventFrame(cameraId, frame, timeoutMs);
+ }
+
+ void HardwareRuntime::clear()
+ {
+ m_acquisitionEngine.reset();
+ for (const HardwareProviderDescriptor& descriptor : m_registry.providers())
+ {
+ const HardwareProviderPtr provider = m_registry.provider(descriptor.id);
+ if (auto* cameraProvider = dynamic_cast(provider.get()))
+ {
+ cameraProvider->stopPreview();
+ cameraProvider->setFrameSink({});
+ }
+ }
+ m_registry.clear();
+ }
+
+ bool HardwareRuntime::registerProvider(const HardwareProviderPtr& provider)
+ {
+ if (!provider || provider->descriptor().id.trimmed().isEmpty())
+ {
+ return false;
+ }
+ const HardwareProviderPtr previous = m_registry.provider(provider->descriptor().id);
+ auto* cameraProvider = dynamic_cast(provider.get());
+ if (cameraProvider)
+ {
+ cameraProvider->setFrameSink([this](const ImageFrame& frame)
+ {
+ m_frameRouter.publish(frame);
+ });
+ }
+ const QList providerDevices = provider->devices();
+ const bool providerHasCamera = std::any_of(
+ providerDevices.cbegin(),
+ providerDevices.cend(),
+ [](const HardwareDeviceDescriptor& device)
+ {
+ return device.kind == HardwareDeviceKind::Camera;
+ });
+ if (providerHasCamera)
+ {
+ m_acquisitionEngine.prepare();
+ }
+ if (!m_registry.registerProvider(provider))
+ {
+ if (cameraProvider && previous != provider)
+ {
+ cameraProvider->setFrameSink({});
+ }
+ const QList registeredDevices = m_registry.devices();
+ const bool hasRegisteredCamera = std::any_of(
+ registeredDevices.cbegin(),
+ registeredDevices.cend(),
+ [](const HardwareDeviceDescriptor& device)
+ {
+ return device.kind == HardwareDeviceKind::Camera;
+ });
+ if (!hasRegisteredCamera)
+ {
+ m_acquisitionEngine.reset();
+ }
+ return false;
+ }
+ if (previous && previous != provider)
+ {
+ if (auto* previousCameraProvider = dynamic_cast(previous.get()))
+ {
+ previousCameraProvider->stopPreview();
+ previousCameraProvider->setFrameSink({});
+ }
+ }
+ if (!providerHasCamera)
+ {
+ const QList registeredDevices = m_registry.devices();
+ const bool hasRegisteredCamera = std::any_of(
+ registeredDevices.cbegin(),
+ registeredDevices.cend(),
+ [](const HardwareDeviceDescriptor& device)
+ {
+ return device.kind == HardwareDeviceKind::Camera;
+ });
+ if (!hasRegisteredCamera)
+ {
+ m_acquisitionEngine.reset();
+ }
+ }
+ return true;
+ }
+
+ void HardwareRuntime::unregisterProvider(const QString& providerId)
+ {
+ const HardwareProviderPtr provider = m_registry.provider(providerId);
+ if (auto* cameraProvider = dynamic_cast(provider.get()))
+ {
+ cameraProvider->stopPreview();
+ cameraProvider->setFrameSink({});
+ }
+ m_registry.unregisterProvider(providerId);
+ const QList devices = m_registry.devices();
+ const bool hasCamera = std::any_of(
+ devices.cbegin(),
+ devices.cend(),
+ [](const HardwareDeviceDescriptor& device)
+ {
+ return device.kind == HardwareDeviceKind::Camera;
+ });
+ if (!hasCamera)
+ {
+ m_acquisitionEngine.reset();
+ }
+ }
+
+ void HardwareRuntime::refreshProvider(const QString& providerId)
+ {
+ m_registry.refreshProvider(providerId);
+ const QList devices = m_registry.devices();
+ const bool hasCamera = std::any_of(
+ devices.cbegin(),
+ devices.cend(),
+ [](const HardwareDeviceDescriptor& device)
+ {
+ return device.kind == HardwareDeviceKind::Camera;
+ });
+ if (hasCamera)
+ {
+ m_acquisitionEngine.prepare();
+ }
+ else
+ {
+ m_acquisitionEngine.reset();
+ }
+ }
+}
diff --git a/ScopeOneCore/src/MDAManager.cpp b/ScopeOneCore/src/MDAManager.cpp
index 0015bbc..efbd1cf 100644
--- a/ScopeOneCore/src/MDAManager.cpp
+++ b/ScopeOneCore/src/MDAManager.cpp
@@ -1,7 +1,8 @@
#include "internal/MDAManager.h"
-#include "internal/CameraManager.h"
+#include "scopeone/CameraProvider.h"
#include "MMCore.h"
+#include "scopeone/ClockService.h"
#include
#include
@@ -38,9 +39,9 @@ namespace scopeone::core::internal
}
// Connects MDA capture to the active camera backend
- void MDAManager::setCameraManager(CameraManager* cameraManager)
+ void MDAManager::setCameraProvider(CameraProvider* cameraProvider)
{
- m_cameraManager = cameraManager;
+ m_cameraProvider = cameraProvider;
}
// Starts one immutable acquisition event sequence
@@ -186,9 +187,9 @@ namespace scopeone::core::internal
{
if (event.cameraIds.size() > 1)
{
- if (!m_cameraManager)
+ if (!m_cameraProvider)
{
- if (errorMessage) *errorMessage = QStringLiteral("CameraManager not available");
+ if (errorMessage) *errorMessage = QStringLiteral("Camera provider not available");
return false;
}
return execEventMultiCamera(event, output, errorMessage);
@@ -249,6 +250,7 @@ namespace scopeone::core::internal
frame.pixelFormat,
static_cast(m_mmcore->getImageBitDepth()));
frame.timestampNs = currentTimestampNs();
+ frame.clockStamp = scopeone::core::ClockService{}.now();
frame.sourceRoiWidth = frame.width;
frame.sourceRoiHeight = frame.height;
if (!frame.cameraId.isEmpty())
@@ -303,7 +305,7 @@ namespace scopeone::core::internal
{
CaptureResult result;
result.cameraId = cameraId;
- if (!m_cameraManager->captureEventFrame(cameraId, result.frame, captureTimeoutMs))
+ if (!m_cameraProvider->captureEventFrame(cameraId, result.frame, captureTimeoutMs))
{
result.error = QStringLiteral("Failed to capture frame from camera: %1").arg(cameraId);
return result;
diff --git a/ScopeOneCore/src/MMCoreManager.cpp b/ScopeOneCore/src/MMCoreManager.cpp
index e1f30d5..6ee53f3 100644
--- a/ScopeOneCore/src/MMCoreManager.cpp
+++ b/ScopeOneCore/src/MMCoreManager.cpp
@@ -16,6 +16,27 @@ namespace scopeone::core::internal
{
namespace
{
+ scopeone::core::HardwareDeviceKind hardwareDeviceKind(MM::DeviceType type)
+ {
+ switch (type)
+ {
+ case MM::CameraDevice:
+ return scopeone::core::HardwareDeviceKind::Camera;
+ case MM::XYStageDevice:
+ return scopeone::core::HardwareDeviceKind::XYStage;
+ case MM::StageDevice:
+ return scopeone::core::HardwareDeviceKind::ZStage;
+ case MM::ShutterDevice:
+ return scopeone::core::HardwareDeviceKind::Shutter;
+ case MM::StateDevice:
+ return scopeone::core::HardwareDeviceKind::State;
+ case MM::HubDevice:
+ return scopeone::core::HardwareDeviceKind::Hub;
+ default:
+ return scopeone::core::HardwareDeviceKind::Unknown;
+ }
+ }
+
struct DevicePropertyState
{
QStringList preInitProperties;
@@ -370,8 +391,28 @@ namespace scopeone::core::internal
continue;
}
+ scopeone::core::HardwareDeviceDescriptor descriptor;
+ descriptor.logicalId = deviceName;
+ descriptor.providerId = QStringLiteral("micro-manager");
+ descriptor.providerDeviceId = deviceName;
+ descriptor.name = deviceName;
+ descriptor.kind = hardwareDeviceKind(deviceType);
+ descriptor.state = scopeone::core::HardwareDeviceState::Discovered;
+ descriptor.endpoint = deviceType == MM::CameraDevice && !useSingleCamera
+ ? scopeone::core::HardwareEndpointKind::DriverHost
+ : scopeone::core::HardwareEndpointKind::InProcess;
+ try
+ {
+ descriptor.hardwareId = QString::fromStdString(
+ m_mmcore->getDeviceName(label.c_str()));
+ }
+ catch (const CMMError&)
+ {
+ }
+
if (deviceType == MM::CameraDevice && !useSingleCamera)
{
+ result.devices.append(descriptor);
skippedCameraCount++;
continue;
}
@@ -382,11 +423,13 @@ namespace scopeone::core::internal
try
{
m_mmcore->initializeDevice(label.c_str());
+ descriptor.state = scopeone::core::HardwareDeviceState::Initialized;
successCount++;
}
catch (const CMMError& error)
{
failCount++;
+ descriptor.state = scopeone::core::HardwareDeviceState::Faulted;
result.failedDevices.append(deviceName);
qWarning().noquote()
<< QString("Failed to initialize device '%1': %2")
@@ -395,8 +438,10 @@ namespace scopeone::core::internal
}
else
{
+ descriptor.state = scopeone::core::HardwareDeviceState::Initialized;
successCount++;
}
+ result.devices.append(descriptor);
}
catch (const CMMError& error)
{
diff --git a/ScopeOneCore/src/MicroManagerProvider.cpp b/ScopeOneCore/src/MicroManagerProvider.cpp
new file mode 100644
index 0000000..390eb66
--- /dev/null
+++ b/ScopeOneCore/src/MicroManagerProvider.cpp
@@ -0,0 +1,164 @@
+#include "internal/MicroManagerProvider.h"
+
+#include
+
+namespace scopeone::core::internal
+{
+ MicroManagerProvider::MicroManagerProvider(CameraProvider* cameraProvider)
+ : m_cameraProvider(cameraProvider)
+ {
+ }
+
+ HardwareProviderDescriptor MicroManagerProvider::descriptor() const
+ {
+ return {
+ QStringLiteral("micro-manager"),
+ QStringLiteral("Micro-Manager"),
+ QStringLiteral("MMCore")
+ };
+ }
+
+ QList MicroManagerProvider::devices() const
+ {
+ return m_devices;
+ }
+
+ void MicroManagerProvider::setDevices(const QList& devices)
+ {
+ m_devices = devices;
+ }
+
+ void MicroManagerProvider::setFrameSink(FrameSink sink)
+ {
+ if (m_cameraProvider)
+ {
+ m_cameraProvider->setFrameSink(std::move(sink));
+ }
+ }
+
+ bool MicroManagerProvider::startPreview()
+ {
+ return m_cameraProvider && m_cameraProvider->startPreview();
+ }
+
+ bool MicroManagerProvider::stopPreview()
+ {
+ return m_cameraProvider && m_cameraProvider->stopPreview();
+ }
+
+ bool MicroManagerProvider::startPreviewFor(const QString& cameraId)
+ {
+ return m_cameraProvider && m_cameraProvider->startPreviewFor(cameraId);
+ }
+
+ bool MicroManagerProvider::stopPreviewFor(const QString& cameraId)
+ {
+ return m_cameraProvider && m_cameraProvider->stopPreviewFor(cameraId);
+ }
+
+ bool MicroManagerProvider::isPreviewRunning(const QString& cameraId) const
+ {
+ return m_cameraProvider && m_cameraProvider->isPreviewRunning(cameraId);
+ }
+
+ bool MicroManagerProvider::getExposure(const QString& cameraIdOrAll, double& exposureMs) const
+ {
+ return m_cameraProvider && m_cameraProvider->getExposure(cameraIdOrAll, exposureMs);
+ }
+
+ bool MicroManagerProvider::setExposure(const QString& cameraIdOrAll, double exposureMs)
+ {
+ return m_cameraProvider && m_cameraProvider->setExposure(cameraIdOrAll, exposureMs);
+ }
+
+ QStringList MicroManagerProvider::listProperties(const QString& cameraId)
+ {
+ return m_cameraProvider ? m_cameraProvider->listProperties(cameraId) : QStringList{};
+ }
+
+ QString MicroManagerProvider::getProperty(const QString& cameraId,
+ const QString& name,
+ bool fromCache)
+ {
+ return m_cameraProvider ? m_cameraProvider->getProperty(cameraId, name, fromCache) : QString{};
+ }
+
+ bool MicroManagerProvider::setProperty(const QString& cameraId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage)
+ {
+ return m_cameraProvider
+ && m_cameraProvider->setProperty(cameraId, name, value, errorMessage);
+ }
+
+ QString MicroManagerProvider::getPropertyType(const QString& cameraId, const QString& name)
+ {
+ return m_cameraProvider
+ ? m_cameraProvider->getPropertyType(cameraId, name)
+ : QStringLiteral("Unknown");
+ }
+
+ bool MicroManagerProvider::isPropertyReadOnly(const QString& cameraId, const QString& name)
+ {
+ return !m_cameraProvider || m_cameraProvider->isPropertyReadOnly(cameraId, name);
+ }
+
+ bool MicroManagerProvider::isPropertyPreInit(const QString& cameraId, const QString& name)
+ {
+ return m_cameraProvider && m_cameraProvider->isPropertyPreInit(cameraId, name);
+ }
+
+ QStringList MicroManagerProvider::getAllowedPropertyValues(const QString& cameraId,
+ const QString& name)
+ {
+ return m_cameraProvider
+ ? m_cameraProvider->getAllowedPropertyValues(cameraId, name)
+ : QStringList{};
+ }
+
+ bool MicroManagerProvider::hasPropertyLimits(const QString& cameraId, const QString& name)
+ {
+ return m_cameraProvider && m_cameraProvider->hasPropertyLimits(cameraId, name);
+ }
+
+ double MicroManagerProvider::getPropertyLowerLimit(const QString& cameraId, const QString& name)
+ {
+ return m_cameraProvider ? m_cameraProvider->getPropertyLowerLimit(cameraId, name) : 0.0;
+ }
+
+ double MicroManagerProvider::getPropertyUpperLimit(const QString& cameraId, const QString& name)
+ {
+ return m_cameraProvider ? m_cameraProvider->getPropertyUpperLimit(cameraId, name) : 0.0;
+ }
+
+ bool MicroManagerProvider::setROI(const QString& cameraId,
+ int x,
+ int y,
+ int width,
+ int height)
+ {
+ return m_cameraProvider && m_cameraProvider->setROI(cameraId, x, y, width, height);
+ }
+
+ bool MicroManagerProvider::clearROI(const QString& cameraId)
+ {
+ return m_cameraProvider && m_cameraProvider->clearROI(cameraId);
+ }
+
+ bool MicroManagerProvider::getROI(const QString& cameraId,
+ int& x,
+ int& y,
+ int& width,
+ int& height)
+ {
+ return m_cameraProvider && m_cameraProvider->getROI(cameraId, x, y, width, height);
+ }
+
+ bool MicroManagerProvider::captureEventFrame(const QString& cameraId,
+ scopeone::core::ImageFrame& frame,
+ int timeoutMs)
+ {
+ return m_cameraProvider && m_cameraProvider->captureEventFrame(cameraId, frame, timeoutMs);
+ }
+}
diff --git a/ScopeOneCore/src/NativeCameraBackend.cpp b/ScopeOneCore/src/NativeCameraBackend.cpp
index 7f28cea..2bb519d 100644
--- a/ScopeOneCore/src/NativeCameraBackend.cpp
+++ b/ScopeOneCore/src/NativeCameraBackend.cpp
@@ -1,6 +1,7 @@
#include "internal/CameraBackend.h"
#include "MMCore.h"
+#include "scopeone/ClockService.h"
#include
#include
@@ -279,6 +280,7 @@ namespace scopeone::core::internal
frame.pixelFormat = m_configuration.pixelFormat;
frame.frameIndex = frameIndex;
frame.timestampNs = static_cast(QDateTime::currentMSecsSinceEpoch()) * 1000000ull;
+ frame.clockStamp = scopeone::core::ClockService{}.now();
frame.sourceRoiX = m_configuration.sourceRoiX;
frame.sourceRoiY = m_configuration.sourceRoiY;
frame.sourceRoiWidth = m_configuration.sourceRoiWidth;
diff --git a/ScopeOneCore/src/RecordingManager.cpp b/ScopeOneCore/src/RecordingManager.cpp
index bd8b8ad..3ad1902 100644
--- a/ScopeOneCore/src/RecordingManager.cpp
+++ b/ScopeOneCore/src/RecordingManager.cpp
@@ -1,6 +1,6 @@
#include "internal/RecordingManager.h"
-#include "internal/CameraManager.h"
+#include "scopeone/CameraProvider.h"
#include "MMCore.h"
#include
@@ -833,7 +833,7 @@ namespace scopeone::core::internal
{
return false;
}
- if (!planUsesMda(plan) && !planStreamsMda(plan) && !m_cameraManager && !m_latestFrameFetcher)
+ if (!planUsesMda(plan) && !planStreamsMda(plan) && !m_cameraProvider && !m_latestFrameFetcher)
{
errorMessage = QStringLiteral("Frame source is not available for recording");
return false;
@@ -1329,8 +1329,8 @@ namespace scopeone::core::internal
if (!usesMda)
{
primeLastFrameIndices();
- if (m_cameraManager
- && !m_cameraManager->setRecordingFrameDeliveryEnabled(true))
+ if (m_cameraRuntimeControl
+ && !m_cameraRuntimeControl->setRecordingFrameDeliveryEnabled(true))
{
if (plan.streamToDisk)
{
@@ -1346,9 +1346,9 @@ namespace scopeone::core::internal
{
if (!startStreamingOutputs(plan))
{
- if (m_cameraManager)
+ if (m_cameraRuntimeControl)
{
- m_cameraManager->setRecordingFrameDeliveryEnabled(false);
+ m_cameraRuntimeControl->setRecordingFrameDeliveryEnabled(false);
}
const QString writerError = writerErrorSnapshot();
qWarning().noquote() << (writerError.isEmpty()
@@ -1414,17 +1414,17 @@ namespace scopeone::core::internal
emit recordingStateChanged(false);
emitProgress(true);
- if (m_cameraManager)
+ if (m_cameraRuntimeControl)
{
- m_cameraManager->setRecordingFrameDeliveryEnabled(false);
+ m_cameraRuntimeControl->setRecordingFrameDeliveryEnabled(false);
}
if (m_mdaState.usingMda && m_mdaState.manager && m_mdaState.manager->isRunning())
{
m_mdaState.manager->requestCancel();
}
- if (m_cameraManager)
+ if (m_cameraRuntimeControl)
{
- m_cameraManager->setFrameDeliveryPaused(false);
+ m_cameraRuntimeControl->setFrameDeliveryPaused(false);
}
qInfo().noquote() << "Recording stopped";
@@ -1975,9 +1975,9 @@ namespace scopeone::core::internal
qWarning().noquote() << message;
return false;
}
- if (m_captureState.activeCameraIds.size() > 1 && !m_cameraManager)
+ if (m_captureState.activeCameraIds.size() > 1 && !m_cameraProvider)
{
- const QString message = QStringLiteral("Multi-camera MDA requires CameraManager");
+ const QString message = QStringLiteral("Multi-camera MDA requires a camera provider");
if (errorMessage) *errorMessage = message;
qWarning().noquote() << message;
return false;
@@ -1994,11 +1994,11 @@ namespace scopeone::core::internal
qWarning().noquote() << message;
return false;
}
- m_mdaState.manager->setCameraManager(m_cameraManager);
+ m_mdaState.manager->setCameraProvider(m_cameraProvider);
- if (m_captureState.activeCameraIds.size() > 1)
+ if (m_captureState.activeCameraIds.size() > 1 && m_cameraRuntimeControl)
{
- m_cameraManager->setFrameDeliveryPaused(true);
+ m_cameraRuntimeControl->setFrameDeliveryPaused(true);
}
m_mdaState.cameraId = m_captureState.activeCameraIds.first();
diff --git a/ScopeOneCore/src/ScopeOneCore.cpp b/ScopeOneCore/src/ScopeOneCore.cpp
index 1f8c909..3b5e413 100644
--- a/ScopeOneCore/src/ScopeOneCore.cpp
+++ b/ScopeOneCore/src/ScopeOneCore.cpp
@@ -2,11 +2,15 @@
#include "scopeone/ImageSceneModel.h"
#include "internal/BackgroundCalibrationModule.h"
+#include "internal/AcquisitionEngine.h"
#include "internal/DifferentialRollingModule.h"
#include "internal/FFTModule.h"
#include "internal/GaussianBlurModule.h"
#include "internal/ImageProcessingFramework.h"
+#include "internal/FrameRouter.h"
+#include "internal/HardwareRuntime.h"
#include "internal/MMCoreManager.h"
+#include "internal/MicroManagerProvider.h"
#include "internal/CameraManager.h"
#include "internal/ParticleAnalysis.h"
#include "internal/RecordingManager.h"
@@ -364,6 +368,7 @@ namespace
facade.failCount = result.failCount;
facade.skippedCameraCount = result.skippedCameraCount;
facade.foundCamera = result.foundCamera;
+ facade.devices = result.devices;
return facade;
}
@@ -911,6 +916,10 @@ namespace scopeone::core
quint64 completedCount{0};
};
+ HardwareRuntime* hardwareRuntime{nullptr};
+ CameraProvider* cameraProvider{nullptr};
+ CameraRuntimeControl* cameraRuntimeControl{nullptr};
+ std::shared_ptr microManagerProvider;
MMCoreManager* mmcoreManager{nullptr};
CameraManager* cameraManager{nullptr};
RecordingManager* recordingManager{nullptr};
@@ -938,6 +947,48 @@ namespace scopeone::core
return QStringLiteral(SCOPEONE_CORE_VERSION_STRING);
}
+ // Return the unified device catalog owned by the hardware runtime
+ QList ScopeOneCore::hardwareDevices() const
+ {
+ return m_managers->hardwareRuntime->deviceRegistry()->devices();
+ }
+
+ bool ScopeOneCore::registerHardwareProvider(const HardwareProviderPtr& provider)
+ {
+ if (!provider
+ || provider->descriptor().id.trimmed().isEmpty()
+ || m_configurationOperationRunning
+ || isRecording())
+ {
+ return false;
+ }
+ const bool registered = m_managers->hardwareRuntime->registerProvider(provider);
+ if (registered)
+ {
+ synchronizeCameraIdsFromRegistry();
+ }
+ return registered;
+ }
+
+ bool ScopeOneCore::unregisterHardwareProvider(const QString& providerId)
+ {
+ const QString normalizedId = providerId.trimmed();
+ if (normalizedId.isEmpty()
+ || normalizedId == QStringLiteral("micro-manager")
+ || m_configurationOperationRunning
+ || isRecording())
+ {
+ return false;
+ }
+ if (!m_managers->hardwareRuntime->deviceRegistry()->provider(normalizedId))
+ {
+ return false;
+ }
+ m_managers->hardwareRuntime->unregisterProvider(normalizedId);
+ synchronizeCameraIdsFromRegistry();
+ return true;
+ }
+
// Return the linked MMCore version
QString ScopeOneCore::getMMCoreVersion()
{
@@ -1100,12 +1151,25 @@ namespace scopeone::core
this, &ScopeOneCore::syncLineProfileFromScene);
m_managers->mmcoreManager = new MMCoreManager(this);
m_managers->cameraManager = new CameraManager(this);
+ m_managers->cameraRuntimeControl = m_managers->cameraManager;
+ m_managers->microManagerProvider =
+ std::make_shared(m_managers->cameraManager);
+ m_managers->hardwareRuntime = new HardwareRuntime(this);
+ m_managers->cameraProvider = m_managers->hardwareRuntime;
+ connect(m_managers->hardwareRuntime, &HardwareRuntime::devicesChanged,
+ this, [this]()
+ {
+ synchronizeCameraIdsFromRegistry();
+ emit hardwareDevicesChanged();
+ });
+ m_managers->hardwareRuntime->registerProvider(m_managers->microManagerProvider);
m_managers->recordingManager = new RecordingManager(this);
m_managers->imageProcessingManager = new ImageProcessingManager(this);
m_managers->stageMosaicManager = new StageMosaicManager(this, this);
m_managers->recordingWriterStatus.reset(
m_managers->recordingManager->recordedMaxBytes());
- m_managers->recordingManager->setCameraManager(m_managers->cameraManager);
+ m_managers->recordingManager->setCameraProvider(m_managers->cameraProvider);
+ m_managers->recordingManager->setCameraRuntimeControl(m_managers->cameraRuntimeControl);
m_managers->recordingManager->setMMCore(m_managers->mmcoreManager->getCore());
m_managers->recordingManager->setLatestFrameFetcher(
[this](const QString& cameraId, ImageFrame& frame)
@@ -1122,8 +1186,20 @@ namespace scopeone::core
session.setPresentationState(layers, markups);
});
- connect(m_managers->cameraManager, &CameraManager::newRawFrameReady,
+ connect(m_managers->hardwareRuntime->frameRouter(), &FrameRouter::frameReady,
this, &ScopeOneCore::handleIncomingRawFrame);
+ connect(m_managers->hardwareRuntime->frameRouter(), &FrameRouter::frameReady,
+ this, [this](const ImageFrame& frame)
+ {
+ const HardwareDeviceDescriptor device =
+ m_managers->hardwareRuntime->deviceRegistry()->device(frame.cameraId);
+ if (device.providerId == QStringLiteral("micro-manager"))
+ {
+ return;
+ }
+ submitProcessingFrame(frame);
+ m_managers->recordingManager->onRawFramesReady(QList{frame});
+ });
connect(m_managers->cameraManager, &CameraManager::processingFrameReady,
this, &ScopeOneCore::submitProcessingFrame,
Qt::DirectConnection);
@@ -1152,8 +1228,13 @@ namespace scopeone::core
connect(m_managers->recordingManager, &RecordingManager::mdaRawFrameReady,
this, [this](const ImageFrame& frame)
{
- handleIncomingRawFrame(frame);
- submitProcessingFrame(frame);
+ m_managers->hardwareRuntime->frameRouter()->publish(frame);
+ const HardwareDeviceDescriptor device =
+ m_managers->hardwareRuntime->deviceRegistry()->device(frame.cameraId);
+ if (device.providerId == QStringLiteral("micro-manager"))
+ {
+ submitProcessingFrame(frame);
+ }
},
Qt::QueuedConnection);
@@ -1316,7 +1397,7 @@ namespace scopeone::core
QStringList running;
for (const QString& cameraId : m_cameraIds)
{
- if (m_managers->cameraManager->isPreviewRunning(cameraId))
+ if (m_managers->cameraProvider->isPreviewRunning(cameraId))
{
running.append(cameraId);
}
@@ -1397,23 +1478,14 @@ namespace scopeone::core
void ScopeOneCore::applyLoadedConfiguration(const QString& configPath,
const LoadConfigResult& result)
{
- m_cameraIds = result.cameraIds;
m_configurationFailedDevices = result.failedDevices;
m_configurationError.clear();
m_configurationState = result.failedDevices.isEmpty()
? ConfigurationState::Loaded
: ConfigurationState::PartiallyLoaded;
- for (const QString& cameraId : m_cameraIds)
- {
- ensureSceneLayer(rawLayerKey(cameraId),
- cameraId,
- QStringLiteral("%1 Raw").arg(cameraId),
- DocumentLayerKind::Raw);
- ensureSceneLayer(processedLayerKey(cameraId),
- cameraId,
- QStringLiteral("%1 Processed").arg(cameraId),
- DocumentLayerKind::Processed);
- }
+ m_managers->microManagerProvider->setDevices(result.devices);
+ m_managers->hardwareRuntime->refreshProvider(QStringLiteral("micro-manager"));
+ synchronizeCameraIdsFromRegistry();
const QFileInfo configFile(configPath);
m_loadedConfigPath = configPath.trimmed().isEmpty()
? QString()
@@ -1428,6 +1500,45 @@ namespace scopeone::core
emit hardwareConfigurationChanged();
}
+ void ScopeOneCore::synchronizeCameraIdsFromRegistry()
+ {
+ QStringList nextCameraIds;
+ for (const HardwareDeviceDescriptor& device
+ : m_managers->hardwareRuntime->deviceRegistry()->devices())
+ {
+ const QString logicalId = device.logicalId.trimmed();
+ if (device.kind == HardwareDeviceKind::Camera
+ && !logicalId.isEmpty()
+ && !nextCameraIds.contains(logicalId))
+ {
+ nextCameraIds.append(logicalId);
+ }
+ }
+ nextCameraIds.sort(Qt::CaseInsensitive);
+
+ for (const QString& cameraId : m_cameraIds)
+ {
+ if (!nextCameraIds.contains(cameraId))
+ {
+ clearLiveFrames(cameraId);
+ m_imageSceneModel->removeLayer(rawLayerKey(cameraId));
+ m_imageSceneModel->removeLayer(processedLayerKey(cameraId));
+ }
+ }
+ m_cameraIds = nextCameraIds;
+ for (const QString& cameraId : m_cameraIds)
+ {
+ ensureSceneLayer(rawLayerKey(cameraId),
+ cameraId,
+ QStringLiteral("%1 Raw").arg(cameraId),
+ DocumentLayerKind::Raw);
+ ensureSceneLayer(processedLayerKey(cameraId),
+ cameraId,
+ QStringLiteral("%1 Processed").arg(cameraId),
+ DocumentLayerKind::Processed);
+ }
+ }
+
// Complete a failed configuration load and publish one consistent result
void ScopeOneCore::finishConfigurationLoadFailure(const LoadConfigResult& result,
const QString& errorMessage)
@@ -1455,6 +1566,7 @@ namespace scopeone::core
catch (const CMMError&)
{
}
+ m_managers->hardwareRuntime->clear();
}
// Apply the configured hardware shutdown state before releasing devices
@@ -1487,6 +1599,7 @@ namespace scopeone::core
const bool processingWasEnabled = isRealTimeProcessingEnabled();
m_managers->imageProcessingManager->enableRealTimeProcessing(false);
const QStringList cameraIds = m_cameraIds;
+ m_managers->hardwareRuntime->acquisitionEngine()->reset();
if (shutdownCameraBackend)
{
m_managers->cameraManager->stopPreview();
@@ -1509,6 +1622,9 @@ namespace scopeone::core
m_latestHistogramStats.clear();
m_activeHistogramLayerKey.clear();
m_imageSceneModel->reset();
+ m_managers->microManagerProvider->setDevices(QList{});
+ m_managers->hardwareRuntime->refreshProvider(QStringLiteral("micro-manager"));
+ synchronizeCameraIdsFromRegistry();
if (notify && processingWasEnabled)
{
emit processingSettingsChanged();
@@ -1779,11 +1895,7 @@ namespace scopeone::core
{
return false;
}
- if (target.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0)
- {
- return m_managers->cameraManager->startPreview();
- }
- return m_managers->cameraManager->startPreviewFor(target);
+ return m_managers->hardwareRuntime->acquisitionEngine()->start(target);
}
// Stop preview for one camera or the full camera set
@@ -1794,11 +1906,7 @@ namespace scopeone::core
{
return false;
}
- if (target.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0)
- {
- return m_managers->cameraManager->stopPreview();
- }
- return m_managers->cameraManager->stopPreviewFor(target);
+ return m_managers->hardwareRuntime->acquisitionEngine()->stop(target);
}
// Submit exposure changes through the active camera manager
@@ -1809,7 +1917,7 @@ namespace scopeone::core
{
return false;
}
- const bool ok = m_managers->cameraManager->setExposure(target, exposureMs);
+ const bool ok = m_managers->cameraProvider->setExposure(target, exposureMs);
if (ok)
{
emit deviceStateChanged();
@@ -1825,7 +1933,7 @@ namespace scopeone::core
{
return false;
}
- const bool ok = m_managers->cameraManager->setROI(target, x, y, width, height);
+ const bool ok = m_managers->cameraProvider->setROI(target, x, y, width, height);
if (ok)
{
clearLiveFrames(target);
@@ -1888,7 +1996,7 @@ namespace scopeone::core
bool changed = false;
for (const QString& cameraId : targets)
{
- if (!m_managers->cameraManager->clearROI(cameraId))
+ if (!m_managers->cameraProvider->clearROI(cameraId))
{
ok = false;
continue;
@@ -1911,7 +2019,7 @@ namespace scopeone::core
{
return false;
}
- return m_managers->cameraManager->getROI(target, x, y, width, height);
+ return m_managers->cameraProvider->getROI(target, x, y, width, height);
}
// Track the active line profile request for future frames
@@ -2069,7 +2177,7 @@ namespace scopeone::core
processingToken,
[this, cameraId, processingToken]()
{
- return m_managers->cameraManager->isProcessingFrameTokenCurrent(
+ return m_managers->cameraRuntimeControl->isProcessingFrameTokenCurrent(
cameraId,
processingToken);
});
@@ -3168,7 +3276,7 @@ namespace scopeone::core
{
if (isConfiguredCamera(device))
{
- const QString value = m_managers->cameraManager->getProperty(
+ const QString value = m_managers->cameraProvider->getProperty(
device, property, false);
if (value.isNull())
{
@@ -3285,7 +3393,7 @@ namespace scopeone::core
if (isConfiguredCamera(device))
{
QString error;
- if (!m_managers->cameraManager->setProperty(
+ if (!m_managers->cameraProvider->setProperty(
device, property, value, &error))
{
failed.push_back(setting);
@@ -3383,7 +3491,7 @@ namespace scopeone::core
resolvedTarget = m_cameraIds.first();
}
if (m_cameraIds.contains(resolvedTarget)
- && m_managers->cameraManager->getExposure(resolvedTarget, exposureMs))
+ && m_managers->cameraProvider->getExposure(resolvedTarget, exposureMs))
{
return true;
}
@@ -3473,7 +3581,7 @@ namespace scopeone::core
}
if (isConfiguredCamera(device))
{
- return m_managers->cameraManager->listProperties(device);
+ return m_managers->cameraProvider->listProperties(device);
}
auto handle = core();
try
@@ -3497,7 +3605,7 @@ namespace scopeone::core
}
if (isConfiguredCamera(device))
{
- return m_managers->cameraManager->getProperty(device, property, fromCache);
+ return m_managers->cameraProvider->getProperty(device, property, fromCache);
}
auto handle = core();
try
@@ -3528,7 +3636,7 @@ namespace scopeone::core
}
if (isConfiguredCamera(device))
{
- return m_managers->cameraManager->getPropertyType(device, property);
+ return m_managers->cameraProvider->getPropertyType(device, property);
}
auto handle = core();
try
@@ -3560,7 +3668,7 @@ namespace scopeone::core
}
if (isConfiguredCamera(device))
{
- return m_managers->cameraManager->isPropertyReadOnly(device, property);
+ return m_managers->cameraProvider->isPropertyReadOnly(device, property);
}
auto handle = core();
try
@@ -3584,7 +3692,7 @@ namespace scopeone::core
}
if (isConfiguredCamera(device))
{
- return m_managers->cameraManager->isPropertyPreInit(device, property);
+ return m_managers->cameraProvider->isPropertyPreInit(device, property);
}
auto handle = core();
try
@@ -3608,7 +3716,7 @@ namespace scopeone::core
}
if (isConfiguredCamera(device))
{
- return m_managers->cameraManager->getAllowedPropertyValues(device, property);
+ return m_managers->cameraProvider->getAllowedPropertyValues(device, property);
}
auto handle = core();
try
@@ -3639,12 +3747,12 @@ namespace scopeone::core
}
if (isConfiguredCamera(device))
{
- if (!m_managers->cameraManager->hasPropertyLimits(device, property))
+ if (!m_managers->cameraProvider->hasPropertyLimits(device, property))
{
return false;
}
- lower = m_managers->cameraManager->getPropertyLowerLimit(device, property);
- upper = m_managers->cameraManager->getPropertyUpperLimit(device, property);
+ lower = m_managers->cameraProvider->getPropertyLowerLimit(device, property);
+ upper = m_managers->cameraProvider->getPropertyUpperLimit(device, property);
return true;
}
@@ -3699,7 +3807,7 @@ namespace scopeone::core
if (isConfiguredCamera(device))
{
QString cameraError;
- if (!m_managers->cameraManager->setProperty(device, property, value, &cameraError))
+ if (!m_managers->cameraProvider->setProperty(device, property, value, &cameraError))
{
if (errorMessage)
{
@@ -3770,7 +3878,7 @@ namespace scopeone::core
}
if (m_managers->imageProcessingManager->isRealTimeProcessingEnabled() == enabled)
{
- if (!m_managers->cameraManager->setHighRateFrameDeliveryEnabled(enabled))
+ if (!m_managers->cameraRuntimeControl->setHighRateFrameDeliveryEnabled(enabled))
{
return false;
}
@@ -3783,7 +3891,7 @@ namespace scopeone::core
if (enabled)
{
m_managers->imageProcessingManager->enableRealTimeProcessing(true);
- if (!m_managers->cameraManager->setHighRateFrameDeliveryEnabled(true))
+ if (!m_managers->cameraRuntimeControl->setHighRateFrameDeliveryEnabled(true))
{
m_managers->imageProcessingManager->enableRealTimeProcessing(false);
return false;
@@ -3791,7 +3899,7 @@ namespace scopeone::core
}
else
{
- if (!m_managers->cameraManager->setHighRateFrameDeliveryEnabled(false))
+ if (!m_managers->cameraRuntimeControl->setHighRateFrameDeliveryEnabled(false))
{
return false;
}
diff --git a/ScopeOneCore/src/SimulatorProvider.cpp b/ScopeOneCore/src/SimulatorProvider.cpp
new file mode 100644
index 0000000..c5f043b
--- /dev/null
+++ b/ScopeOneCore/src/SimulatorProvider.cpp
@@ -0,0 +1,306 @@
+#include "scopeone/SimulatorProvider.h"
+
+#include "scopeone/ClockService.h"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace scopeone::core
+{
+ SimulatorProvider::SimulatorProvider(const QString& logicalCameraId, int width, int height)
+ : m_providerId(QStringLiteral("simulator.%1").arg(
+ QUuid::createUuid().toString(QUuid::WithoutBraces)))
+ , m_cameraId(logicalCameraId.trimmed().isEmpty()
+ ? QStringLiteral("camera.simulator")
+ : logicalCameraId.trimmed())
+ , m_sensorWidth((std::max)(1, width))
+ , m_sensorHeight((std::max)(1, height))
+ , m_roi(0, 0, m_sensorWidth, m_sensorHeight)
+ {
+ m_timer.setTimerType(Qt::PreciseTimer);
+ updateTimerInterval();
+ connect(&m_timer, &QTimer::timeout, this, [this]()
+ {
+ if (m_frameSink)
+ {
+ m_frameSink(makeFrame());
+ }
+ });
+ }
+
+ HardwareProviderDescriptor SimulatorProvider::descriptor() const
+ {
+ return {m_providerId, QStringLiteral("ScopeOne Simulator"), QStringLiteral("1")};
+ }
+
+ QList SimulatorProvider::devices() const
+ {
+ HardwareDeviceDescriptor camera;
+ camera.logicalId = m_cameraId;
+ camera.providerId = m_providerId;
+ camera.providerDeviceId = m_cameraId;
+ camera.hardwareId = m_cameraId;
+ camera.name = QStringLiteral("Simulator Camera");
+ camera.kind = HardwareDeviceKind::Camera;
+ camera.state = HardwareDeviceState::Initialized;
+ camera.endpoint = HardwareEndpointKind::InProcess;
+ return {camera};
+ }
+
+ void SimulatorProvider::setFrameSink(FrameSink sink)
+ {
+ m_frameSink = std::move(sink);
+ }
+
+ bool SimulatorProvider::startPreview()
+ {
+ if (!m_frameSink)
+ {
+ return false;
+ }
+ m_timer.start();
+ return true;
+ }
+
+ bool SimulatorProvider::stopPreview()
+ {
+ m_timer.stop();
+ return true;
+ }
+
+ bool SimulatorProvider::startPreviewFor(const QString& cameraId)
+ {
+ return accepts(cameraId) && startPreview();
+ }
+
+ bool SimulatorProvider::stopPreviewFor(const QString& cameraId)
+ {
+ return accepts(cameraId) && stopPreview();
+ }
+
+ bool SimulatorProvider::isPreviewRunning(const QString& cameraId) const
+ {
+ return accepts(cameraId) && m_timer.isActive();
+ }
+
+ bool SimulatorProvider::getExposure(const QString& cameraIdOrAll, double& exposureMs) const
+ {
+ if (!accepts(cameraIdOrAll))
+ {
+ return false;
+ }
+ exposureMs = m_exposureMs;
+ return true;
+ }
+
+ bool SimulatorProvider::setExposure(const QString& cameraIdOrAll, double exposureMs)
+ {
+ if (!accepts(cameraIdOrAll) || !std::isfinite(exposureMs) || exposureMs <= 0.0)
+ {
+ return false;
+ }
+ m_exposureMs = exposureMs;
+ updateTimerInterval();
+ return true;
+ }
+
+ QStringList SimulatorProvider::listProperties(const QString& cameraId)
+ {
+ return accepts(cameraId)
+ ? QStringList{QStringLiteral("Exposure"),
+ QStringLiteral("SensorWidth"),
+ QStringLiteral("SensorHeight")}
+ : QStringList{};
+ }
+
+ QString SimulatorProvider::getProperty(const QString& cameraId,
+ const QString& name,
+ bool)
+ {
+ if (!accepts(cameraId))
+ {
+ return {};
+ }
+ if (name == QStringLiteral("Exposure"))
+ {
+ return QString::number(m_exposureMs, 'g', 12);
+ }
+ if (name == QStringLiteral("SensorWidth"))
+ {
+ return QString::number(m_sensorWidth);
+ }
+ if (name == QStringLiteral("SensorHeight"))
+ {
+ return QString::number(m_sensorHeight);
+ }
+ return {};
+ }
+
+ bool SimulatorProvider::setProperty(const QString& cameraId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage)
+ {
+ if (errorMessage)
+ {
+ errorMessage->clear();
+ }
+ if (!accepts(cameraId) || name != QStringLiteral("Exposure"))
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("Property is not writable");
+ }
+ return false;
+ }
+ bool ok = false;
+ const double exposureMs = value.toDouble(&ok);
+ if (!ok || !setExposure(cameraId, exposureMs))
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("Invalid exposure value");
+ }
+ return false;
+ }
+ return true;
+ }
+
+ QString SimulatorProvider::getPropertyType(const QString& cameraId, const QString& name)
+ {
+ return accepts(cameraId) && listProperties(cameraId).contains(name)
+ ? (name == QStringLiteral("Exposure")
+ ? QStringLiteral("Float")
+ : QStringLiteral("Integer"))
+ : QStringLiteral("Unknown");
+ }
+
+ bool SimulatorProvider::isPropertyReadOnly(const QString& cameraId, const QString& name)
+ {
+ return !accepts(cameraId) || name != QStringLiteral("Exposure");
+ }
+
+ bool SimulatorProvider::isPropertyPreInit(const QString&, const QString&)
+ {
+ return false;
+ }
+
+ QStringList SimulatorProvider::getAllowedPropertyValues(const QString&, const QString&)
+ {
+ return {};
+ }
+
+ bool SimulatorProvider::hasPropertyLimits(const QString& cameraId, const QString& name)
+ {
+ return accepts(cameraId) && name == QStringLiteral("Exposure");
+ }
+
+ double SimulatorProvider::getPropertyLowerLimit(const QString& cameraId, const QString& name)
+ {
+ return accepts(cameraId) && name == QStringLiteral("Exposure") ? 0.1 : 0.0;
+ }
+
+ double SimulatorProvider::getPropertyUpperLimit(const QString& cameraId, const QString& name)
+ {
+ return accepts(cameraId) && name == QStringLiteral("Exposure") ? 1000.0 : 0.0;
+ }
+
+ bool SimulatorProvider::setROI(const QString& cameraId,
+ int x,
+ int y,
+ int width,
+ int height)
+ {
+ const QRect roi(x, y, width, height);
+ const QRect sensor(0, 0, m_sensorWidth, m_sensorHeight);
+ if (!accepts(cameraId) || width <= 0 || height <= 0 || !sensor.contains(roi))
+ {
+ return false;
+ }
+ m_roi = roi;
+ return true;
+ }
+
+ bool SimulatorProvider::clearROI(const QString& cameraId)
+ {
+ if (!accepts(cameraId))
+ {
+ return false;
+ }
+ m_roi = QRect(0, 0, m_sensorWidth, m_sensorHeight);
+ return true;
+ }
+
+ bool SimulatorProvider::getROI(const QString& cameraId,
+ int& x,
+ int& y,
+ int& width,
+ int& height)
+ {
+ if (!accepts(cameraId))
+ {
+ return false;
+ }
+ x = m_roi.x();
+ y = m_roi.y();
+ width = m_roi.width();
+ height = m_roi.height();
+ return true;
+ }
+
+ bool SimulatorProvider::captureEventFrame(const QString& cameraId,
+ ImageFrame& frame,
+ int)
+ {
+ if (!accepts(cameraId))
+ {
+ return false;
+ }
+ frame = makeFrame();
+ return frame.isValid();
+ }
+
+ bool SimulatorProvider::accepts(const QString& cameraIdOrAll) const
+ {
+ const QString target = cameraIdOrAll.trimmed();
+ return target == m_cameraId
+ || target.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0;
+ }
+
+ ImageFrame SimulatorProvider::makeFrame()
+ {
+ ImageFrame frame;
+ frame.cameraId = m_cameraId;
+ frame.width = m_roi.width();
+ frame.height = m_roi.height();
+ frame.stride = frame.width;
+ frame.bitsPerSample = 8;
+ frame.pixelFormat = ImagePixelFormat::Mono8;
+ frame.frameIndex = ++m_frameIndex;
+ frame.timestampNs = static_cast(QDateTime::currentMSecsSinceEpoch()) * 1000000ull;
+ frame.clockStamp = ClockService{}.now();
+ frame.sourceRoiX = m_roi.x();
+ frame.sourceRoiY = m_roi.y();
+ frame.sourceRoiWidth = m_roi.width();
+ frame.sourceRoiHeight = m_roi.height();
+ frame.bytes.resize(static_cast(frame.width) * frame.height);
+ for (int y = 0; y < frame.height; ++y)
+ {
+ uchar* row = reinterpret_cast(frame.bytes.data())
+ + static_cast(y) * frame.stride;
+ for (int x = 0; x < frame.width; ++x)
+ {
+ row[x] = static_cast((x + y + frame.frameIndex) & 0xffu);
+ }
+ }
+ return frame;
+ }
+
+ void SimulatorProvider::updateTimerInterval()
+ {
+ m_timer.setInterval((std::max)(1, static_cast(std::ceil(m_exposureMs))));
+ }
+}
From d22e69da6d1848cb3643cc3c34f4c90223174a70 Mon Sep 17 00:00:00 2001
From: tz <185176969+tzhaoo@users.noreply.github.com>
Date: Wed, 19 Aug 2026 15:04:30 +0200
Subject: [PATCH 02/37] Complete hardware provider architecture
---
CMakeLists.txt | 31 +-
README.md | 4 +-
ScopeOneCore/CMakeLists.txt | 41 +-
ScopeOneCore/README.md | 16 +-
.../include/scopeone/CameraProvider.h | 21 +-
ScopeOneCore/include/scopeone/ClockService.h | 12 -
.../scopeone/DriverHostProviderPlugin.h | 23 +
.../include/scopeone/HardwareCapabilities.h | 102 +
.../include/scopeone/HardwareProvider.h | 4 +-
ScopeOneCore/include/scopeone/HardwareTypes.h | 32 +-
ScopeOneCore/include/scopeone/ImageFrame.h | 8 -
ScopeOneCore/include/scopeone/ScopeOneCore.h | 21 +-
.../include/scopeone/SimulatorProvider.h | 5 +-
ScopeOneCore/internal/AcquisitionEngine.h | 11 -
ScopeOneCore/internal/AgentProtocol.h | 58 -
ScopeOneCore/internal/CameraBackend.h | 8 +-
ScopeOneCore/internal/CameraManager.h | 7 +-
ScopeOneCore/internal/DriverHostProtocol.h | 89 +-
.../internal/DriverHostProviderProxy.h | 14 +
ScopeOneCore/internal/FrameRouter.h | 10 +-
ScopeOneCore/internal/HardwareRuntime.h | 75 +-
ScopeOneCore/internal/MDAManager.h | 19 +-
ScopeOneCore/internal/MicroManagerProvider.h | 68 +-
ScopeOneCore/internal/RecordingManager.h | 8 +-
ScopeOneCore/src/AcquisitionEngine.cpp | 97 +-
ScopeOneCore/src/AgentMain.cpp | 1808 ----------
ScopeOneCore/src/CameraBackend.cpp | 2 +-
ScopeOneCore/src/CameraManager.cpp | 21 +-
ScopeOneCore/src/ClockService.cpp | 18 -
...ackend.cpp => DriverHostCameraBackend.cpp} | 369 +-
ScopeOneCore/src/DriverHostMain.cpp | 3084 +++++++++++++++++
ScopeOneCore/src/DriverHostProviderProxy.cpp | 1606 +++++++++
ScopeOneCore/src/FrameBufferUtils.cpp | 1 -
ScopeOneCore/src/FrameRouter.cpp | 12 +-
ScopeOneCore/src/HardwareProvider.cpp | 14 +
ScopeOneCore/src/HardwareRuntime.cpp | 650 +++-
ScopeOneCore/src/MDAManager.cpp | 196 +-
ScopeOneCore/src/MMCoreManager.cpp | 40 +-
ScopeOneCore/src/MicroManagerProvider.cpp | 772 ++++-
ScopeOneCore/src/NativeCameraBackend.cpp | 2 -
ScopeOneCore/src/RecordingManager.cpp | 35 +-
ScopeOneCore/src/ScopeOneCore.cpp | 798 ++---
ScopeOneCore/src/SimulatorProvider.cpp | 56 +-
ScopeOneCore/src/SimulatorProviderPlugin.cpp | 49 +
44 files changed, 7164 insertions(+), 3153 deletions(-)
delete mode 100644 ScopeOneCore/include/scopeone/ClockService.h
create mode 100644 ScopeOneCore/include/scopeone/DriverHostProviderPlugin.h
create mode 100644 ScopeOneCore/include/scopeone/HardwareCapabilities.h
delete mode 100644 ScopeOneCore/internal/AgentProtocol.h
create mode 100644 ScopeOneCore/internal/DriverHostProviderProxy.h
delete mode 100644 ScopeOneCore/src/AgentMain.cpp
delete mode 100644 ScopeOneCore/src/ClockService.cpp
rename ScopeOneCore/src/{AgentCameraBackend.cpp => DriverHostCameraBackend.cpp} (82%)
create mode 100644 ScopeOneCore/src/DriverHostMain.cpp
create mode 100644 ScopeOneCore/src/DriverHostProviderProxy.cpp
create mode 100644 ScopeOneCore/src/HardwareProvider.cpp
create mode 100644 ScopeOneCore/src/SimulatorProviderPlugin.cpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b1f968b..cb66019 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -176,11 +176,20 @@ install(FILES
DESTINATION .
)
-file(GLOB SCOPEONE_CORE_RUNTIME_FILES
- LIST_DIRECTORIES false
- "${ScopeOneCore_ROOT}/${CMAKE_INSTALL_BINDIR}/*.dll"
- "${ScopeOneCore_ROOT}/${CMAKE_INSTALL_BINDIR}/ScopeOne_Agent.exe"
-)
+if (WIN32)
+ file(GLOB SCOPEONE_CORE_RUNTIME_FILES
+ LIST_DIRECTORIES false
+ "${ScopeOneCore_ROOT}/${CMAKE_INSTALL_BINDIR}/*.dll"
+ "${ScopeOneCore_ROOT}/${CMAKE_INSTALL_BINDIR}/ScopeOne_DriverHost.exe"
+ )
+else ()
+ file(GLOB SCOPEONE_CORE_RUNTIME_FILES
+ LIST_DIRECTORIES false
+ "${ScopeOneCore_ROOT}/${CMAKE_INSTALL_BINDIR}/ScopeOne_DriverHost"
+ "${ScopeOneCore_ROOT}/${CMAKE_INSTALL_LIBDIR}/libScopeOneCore.so*"
+ "${ScopeOneCore_ROOT}/${CMAKE_INSTALL_LIBDIR}/libScopeWriter.so*"
+ )
+endif ()
if (SCOPEONE_CORE_RUNTIME_FILES)
add_custom_command(TARGET ScopeOne POST_BUILD
@@ -191,6 +200,16 @@ if (SCOPEONE_CORE_RUNTIME_FILES)
)
endif ()
+set(SCOPEONE_PROVIDER_DIR "${ScopeOneCore_ROOT}/${CMAKE_INSTALL_BINDIR}/providers")
+if (EXISTS "${SCOPEONE_PROVIDER_DIR}")
+ add_custom_command(TARGET ScopeOne POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_directory
+ "${SCOPEONE_PROVIDER_DIR}"
+ "$/providers"
+ )
+ install(DIRECTORY "${SCOPEONE_PROVIDER_DIR}/" DESTINATION providers)
+endif ()
+
if (WIN32)
if (WINDEPLOYQT_EXECUTABLE)
add_custom_command(TARGET ScopeOne POST_BUILD
@@ -227,7 +246,7 @@ if (WIN32)
install(CODE "
file(GLOB _core_runtime_files
\"$/*.dll\"
- \"$/ScopeOne_Agent.exe\"
+ \"$/ScopeOne_DriverHost.exe\"
)
if (_core_runtime_files)
file(INSTALL
diff --git a/README.md b/README.md
index 2c5621c..4321523 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,8 @@
-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.
+ScopeOne is open-source microscopy control software built with C++ and Qt. Hardware is accessed through provider-independent device contracts; [Micro-Manager](https://micro-manager.org/) is the built-in provider, and isolated devices run through ScopeOne DriverHost processes.
+It retains compatibility with the Micro-Manager device ecosystem while allowing native vendor providers and adds a modular real-time image processing pipeline with support for background calibration, temporal filtering, FFT analysis, and more.

diff --git a/ScopeOneCore/CMakeLists.txt b/ScopeOneCore/CMakeLists.txt
index 45b91fa..f1db249 100644
--- a/ScopeOneCore/CMakeLists.txt
+++ b/ScopeOneCore/CMakeLists.txt
@@ -78,8 +78,9 @@ endfunction()
set(CORE_SOURCES
src/ExperimentDocument.cpp
src/AcquisitionEngine.cpp
- src/ClockService.cpp
+ src/DriverHostProviderProxy.cpp
src/FrameRouter.cpp
+ src/HardwareProvider.cpp
src/HardwareRuntime.cpp
src/MicroManagerProvider.cpp
src/SimulatorProvider.cpp
@@ -99,7 +100,7 @@ set(CORE_SOURCES
src/CameraBackend.cpp
src/CameraManager.cpp
src/NativeCameraBackend.cpp
- src/AgentCameraBackend.cpp
+ src/DriverHostCameraBackend.cpp
src/ScopeOneCore.cpp
)
@@ -107,8 +108,9 @@ set(CORE_HEADERS
include/scopeone/ExperimentDocument.h
internal/AcquisitionEngine.h
include/scopeone/CameraProvider.h
+ include/scopeone/DriverHostProviderPlugin.h
internal/CameraRuntimeControl.h
- include/scopeone/ClockService.h
+ include/scopeone/HardwareCapabilities.h
include/scopeone/HardwareTypes.h
include/scopeone/HardwareProvider.h
include/scopeone/SimulatorProvider.h
@@ -122,8 +124,8 @@ set(CORE_HEADERS
internal/StageMosaicManager.h
internal/ImageProcessingFramework.h
internal/ProcessingModule.h
- internal/AgentProtocol.h
internal/DriverHostProtocol.h
+ internal/DriverHostProviderProxy.h
internal/SpatiotemporalBinningModule.h
internal/GaussianBlurModule.h
internal/FFTModule.h
@@ -204,24 +206,31 @@ add_custom_command(TARGET ScopeOneCore POST_BUILD
$
)
-add_executable(ScopeOne_Agent
- src/AgentMain.cpp
+add_executable(ScopeOne_DriverHost
+ src/DriverHostMain.cpp
)
-target_link_libraries(ScopeOne_Agent
+target_link_libraries(ScopeOne_DriverHost
+ ScopeOneCore
MMCore
Qt::Core
Qt::Network
)
-target_compile_definitions(ScopeOne_Agent PRIVATE MMDEVICE_CLIENT_BUILD)
-target_include_directories(ScopeOne_Agent PRIVATE
+target_compile_definitions(ScopeOne_DriverHost PRIVATE MMDEVICE_CLIENT_BUILD)
+target_include_directories(ScopeOne_DriverHost PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}
${MMCORE_INCLUDE_DIR}
${MMDEVICE_INCLUDE_DIR}
)
-target_link_directories(ScopeOne_Agent PRIVATE ${MMCORE_BIN_DIR})
+target_link_directories(ScopeOne_DriverHost PRIVATE ${MMCORE_BIN_DIR})
-scopeone_add_runtime_copy_commands(ScopeOne_Agent $)
+scopeone_add_runtime_copy_commands(ScopeOne_DriverHost $)
+
+add_library(ScopeOne_SimulatorProvider MODULE
+ src/SimulatorProviderPlugin.cpp
+)
+target_link_libraries(ScopeOne_SimulatorProvider PRIVATE ScopeOneCore Qt::Core)
+set_target_properties(ScopeOne_SimulatorProvider PROPERTIES PREFIX "")
# Fast Release build
# if (MSVC)
@@ -239,10 +248,10 @@ if (MSVC)
$<$:/LTCG>
)
- target_compile_options(ScopeOne_Agent PRIVATE
+ target_compile_options(ScopeOne_DriverHost PRIVATE
$<$:/GL /fp:fast /arch:AVX2>
)
- target_link_options(ScopeOne_Agent PRIVATE
+ target_link_options(ScopeOne_DriverHost PRIVATE
$<$:/LTCG>
)
endif ()
@@ -265,9 +274,13 @@ install(TARGETS ScopeOneCore
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
)
-install(TARGETS ScopeOne_Agent
+install(TARGETS ScopeOne_DriverHost
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
)
+install(TARGETS ScopeOne_SimulatorProvider
+ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}/providers"
+ LIBRARY DESTINATION "${CMAKE_INSTALL_BINDIR}/providers"
+)
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
FILES_MATCHING
diff --git a/ScopeOneCore/README.md b/ScopeOneCore/README.md
index e7f6836..3243f13 100644
--- a/ScopeOneCore/README.md
+++ b/ScopeOneCore/README.md
@@ -25,7 +25,7 @@ ScopeOneCore/
#include
```
-`internal` contains implementation contracts between Core managers, processing modules and the camera agent. These headers are available to the `ScopeOneCore` target through a private include path, are not installed, and may change without preserving source compatibility. Code outside `ScopeOneCore` must not include them.
+`internal` contains implementation contracts between Core managers, processing modules and DriverHost. These headers are available to the `ScopeOneCore` target through a private include path, are not installed, and may change without preserving source compatibility. Code outside `ScopeOneCore` must not include them.
`src` contains implementations. A public class such as `ScopeOneCore`, `ImageSceneModel` or `ExperimentDocument` still has its `.cpp` file in `src`; being public is determined by its header location and exported API, not by the location of its implementation.
@@ -45,7 +45,6 @@ Use this placement rule:
| `scopeone::core` | Stable Core-facing types and public facades | `ScopeOneCore`, `ImageFrame`, `ExperimentDocument`, `ImageSceneModel` |
| `scopeone::core::internal` | Core-only managers and processing implementations | `CameraManager`, `MMCoreManager`, `RecordingManager`, processing modules |
| `scopeone::core::internal::driverhost` | Shared DriverHost message framing | Versioned request, response and event envelopes |
-| `scopeone::core::internal::agent` | Micro-Manager DriverHost commands | Camera commands and shared-memory endpoint names |
| `scopeone::ui` | Desktop application widgets and UI coordination outside this library | `MainWindow`, `PreviewWidget`, `InspectWidget` |
Code in `src` that implements a public type remains in `scopeone::core`. Code that implements an `internal` header remains in `scopeone::core::internal`. The Python package named `scopeone` is an external client package and is not an embedded form of the C++ namespace.
@@ -88,10 +87,12 @@ Outputs:
- `build/Release/ScopeOneCore.dll`
- `build/Release/ScopeOneCore.lib`
-- `build/Release/ScopeOne_Agent.exe`
+- `build/Release/ScopeOne_DriverHost.exe`
+- `build/Release/ScopeOne_SimulatorProvider.dll`
- `build/ScopeOneCoreConfig.cmake`
- `install/bin/ScopeOneCore.dll`
-- `install/bin/ScopeOne_Agent.exe`
+- `install/bin/ScopeOne_DriverHost.exe`
+- `install/bin/providers/ScopeOne_SimulatorProvider.dll`
- `install/lib/cmake/ScopeOneCore/ScopeOneCoreConfig.cmake`
@@ -100,8 +101,9 @@ Outputs:
The installed headers are the source of truth for the public API:
- `ScopeOneCore.h` provides the main hardware, acquisition, processing, recording and frame-graph facade.
-- `HardwareProvider.h` and `CameraProvider.h` define provider discovery, control and frame delivery.
-- `HardwareTypes.h` defines provider-independent device identity, state, endpoint and clock metadata.
+- `HardwareProvider.h`, `HardwareCapabilities.h` and `CameraProvider.h` define provider discovery, device control and frame delivery.
+- `DriverHostProviderPlugin.h` defines the module factory used to load external providers in isolated DriverHost processes.
+- `HardwareTypes.h` defines provider-independent device identity, state and endpoint metadata.
- `SimulatorProvider.h` provides an in-process reference provider.
- `ImageFrame.h` defines the image payload and metadata exchanged across Core features.
- `ExperimentDocument.h` defines experiment plans, results, persistence and provenance.
@@ -111,7 +113,7 @@ The installed headers are the source of truth for the public API:
External code should enter through these headers and `scopeone::core::ScopeOneCore`. Internal managers are implementation details and must not become alternate access paths.
-Providers use ScopeOne logical device IDs and publish `ImageFrame` objects through `CameraProvider::FrameSink`. Register them with `ScopeOneCore::registerHardwareProvider(...)`; ScopeOne owns acquisition routing, clocks and downstream frame delivery. Micro-Manager uses the same provider boundary and may run in process or through the existing DriverHost transport.
+Providers use ScopeOne logical device IDs and publish `ImageFrame` objects through `CameraProvider::FrameSink`. Register in-process providers with `ScopeOneCore::registerHardwareProvider(...)`. Register an isolated module with `ScopeOneCore::registerDriverHostProvider(providerId, modulePath, options)`. One DriverHost process owns the complete Provider and registers all of its cameras and control devices together. Micro-Manager remains the built-in provider, using the native camera path for one camera and isolated DriverHost camera processes for multiple cameras.
## Processing Data Flow
diff --git a/ScopeOneCore/include/scopeone/CameraProvider.h b/ScopeOneCore/include/scopeone/CameraProvider.h
index b9d06e9..f01ff8d 100644
--- a/ScopeOneCore/include/scopeone/CameraProvider.h
+++ b/ScopeOneCore/include/scopeone/CameraProvider.h
@@ -5,16 +5,17 @@
#include
+#include "scopeone/HardwareCapabilities.h"
#include "scopeone/ImageFrame.h"
namespace scopeone::core
{
- class CameraProvider
+ class SCOPEONE_CORE_EXPORT CameraProvider : public DevicePropertyProvider
{
public:
using FrameSink = std::function;
- virtual ~CameraProvider() = default;
+ ~CameraProvider() override;
virtual void setFrameSink(FrameSink sink) = 0;
virtual bool startPreview() = 0;
@@ -24,22 +25,6 @@ namespace scopeone::core
virtual bool isPreviewRunning(const QString& cameraId) const = 0;
virtual bool getExposure(const QString& cameraIdOrAll, double& exposureMs) const = 0;
virtual bool setExposure(const QString& cameraIdOrAll, double exposureMs) = 0;
- virtual QStringList listProperties(const QString& cameraId) = 0;
- virtual QString getProperty(const QString& cameraId,
- const QString& name,
- bool fromCache = false) = 0;
- virtual bool setProperty(const QString& cameraId,
- const QString& name,
- const QString& value,
- QString* errorMessage = nullptr) = 0;
- virtual QString getPropertyType(const QString& cameraId, const QString& name) = 0;
- virtual bool isPropertyReadOnly(const QString& cameraId, const QString& name) = 0;
- virtual bool isPropertyPreInit(const QString& cameraId, const QString& name) = 0;
- virtual QStringList getAllowedPropertyValues(const QString& cameraId,
- const QString& name) = 0;
- virtual bool hasPropertyLimits(const QString& cameraId, const QString& name) = 0;
- virtual double getPropertyLowerLimit(const QString& cameraId, const QString& name) = 0;
- virtual double getPropertyUpperLimit(const QString& cameraId, const QString& name) = 0;
virtual bool setROI(const QString& cameraId, int x, int y, int width, int height) = 0;
virtual bool clearROI(const QString& cameraId) = 0;
virtual bool getROI(const QString& cameraId,
diff --git a/ScopeOneCore/include/scopeone/ClockService.h b/ScopeOneCore/include/scopeone/ClockService.h
deleted file mode 100644
index 77dc5ac..0000000
--- a/ScopeOneCore/include/scopeone/ClockService.h
+++ /dev/null
@@ -1,12 +0,0 @@
-#pragma once
-
-#include "scopeone/HardwareTypes.h"
-
-namespace scopeone::core
-{
- class SCOPEONE_CORE_EXPORT ClockService
- {
- public:
- ClockStamp now() const;
- };
-}
diff --git a/ScopeOneCore/include/scopeone/DriverHostProviderPlugin.h b/ScopeOneCore/include/scopeone/DriverHostProviderPlugin.h
new file mode 100644
index 0000000..5e7caec
--- /dev/null
+++ b/ScopeOneCore/include/scopeone/DriverHostProviderPlugin.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include
+#include
+
+#include "scopeone/HardwareProvider.h"
+
+namespace scopeone::core
+{
+ class DriverHostProviderPlugin
+ {
+ public:
+ virtual ~DriverHostProviderPlugin() = default;
+
+ virtual QString providerId() const = 0;
+ virtual HardwareProviderPtr createProvider(const QJsonObject& options,
+ QString* errorMessage = nullptr) = 0;
+ };
+}
+
+#define ScopeOneDriverHostProviderPlugin_iid "org.scopeone.DriverHostProviderPlugin/1.0"
+Q_DECLARE_INTERFACE(scopeone::core::DriverHostProviderPlugin,
+ ScopeOneDriverHostProviderPlugin_iid)
diff --git a/ScopeOneCore/include/scopeone/HardwareCapabilities.h b/ScopeOneCore/include/scopeone/HardwareCapabilities.h
new file mode 100644
index 0000000..0ff44cd
--- /dev/null
+++ b/ScopeOneCore/include/scopeone/HardwareCapabilities.h
@@ -0,0 +1,102 @@
+#pragma once
+
+#include
+#include
+
+#include "scopeone/scopeone_core_export.h"
+
+namespace scopeone::core
+{
+ class SCOPEONE_CORE_EXPORT DevicePropertyProvider
+ {
+ public:
+ virtual ~DevicePropertyProvider();
+
+ virtual QStringList listProperties(const QString& deviceId) = 0;
+ virtual QString getProperty(const QString& deviceId,
+ const QString& name,
+ bool fromCache = false) = 0;
+ virtual bool setProperty(const QString& deviceId,
+ const QString& name,
+ const QString& value,
+ QString* errorMessage = nullptr) = 0;
+ virtual QString getPropertyType(const QString& deviceId, const QString& name) = 0;
+ virtual bool isPropertyReadOnly(const QString& deviceId, const QString& name) = 0;
+ virtual bool isPropertyPreInit(const QString& deviceId, const QString& name) = 0;
+ virtual QStringList getAllowedPropertyValues(const QString& deviceId,
+ const QString& name) = 0;
+ virtual bool hasPropertyLimits(const QString& deviceId, const QString& name) = 0;
+ virtual double getPropertyLowerLimit(const QString& deviceId, const QString& name) = 0;
+ virtual double getPropertyUpperLimit(const QString& deviceId, const QString& name) = 0;
+ };
+
+ class SCOPEONE_CORE_EXPORT StageProvider
+ {
+ public:
+ virtual ~StageProvider();
+
+ virtual QString defaultXYStage() const = 0;
+ virtual QString defaultZStage() const = 0;
+ virtual bool getXYPosition(const QString& deviceId,
+ double& x,
+ double& y,
+ QString* errorMessage = nullptr) const = 0;
+ virtual bool getZPosition(const QString& deviceId,
+ double& z,
+ QString* errorMessage = nullptr) const = 0;
+ virtual bool setRelativeXYPosition(const QString& deviceId,
+ double dx,
+ double dy,
+ QString* errorMessage = nullptr) = 0;
+ virtual bool setRelativeZPosition(const QString& deviceId,
+ double dz,
+ QString* errorMessage = nullptr) = 0;
+ virtual bool setXYPosition(const QString& deviceId,
+ double x,
+ double y,
+ QString* errorMessage = nullptr) = 0;
+ virtual bool setZPosition(const QString& deviceId,
+ double z,
+ QString* errorMessage = nullptr) = 0;
+ };
+
+ class SCOPEONE_CORE_EXPORT ShutterProvider
+ {
+ public:
+ virtual ~ShutterProvider();
+
+ virtual bool isShutterOpen(const QString& deviceId,
+ bool& open,
+ QString* errorMessage = nullptr) const = 0;
+ virtual bool setShutterOpen(const QString& deviceId,
+ bool open,
+ QString* errorMessage = nullptr) = 0;
+ };
+
+ class SCOPEONE_CORE_EXPORT StateProvider
+ {
+ public:
+ virtual ~StateProvider();
+
+ virtual bool getState(const QString& deviceId,
+ long& state,
+ QString* errorMessage = nullptr) const = 0;
+ virtual bool setState(const QString& deviceId,
+ long state,
+ QString* errorMessage = nullptr) = 0;
+ virtual QString stateLabel(const QString& deviceId, long state) const = 0;
+ };
+
+ class SCOPEONE_CORE_EXPORT ConfigurationProvider
+ {
+ public:
+ virtual ~ConfigurationProvider();
+
+ virtual QStringList availableConfigGroups() const = 0;
+ virtual QStringList availableConfigs(const QString& groupName) const = 0;
+ virtual QString currentConfig(const QString& groupName) const = 0;
+ virtual bool setConfig(const QString& groupName,
+ const QString& configName,
+ QString* errorMessage = nullptr) = 0;
+ };
+}
diff --git a/ScopeOneCore/include/scopeone/HardwareProvider.h b/ScopeOneCore/include/scopeone/HardwareProvider.h
index 223998e..d6cf26c 100644
--- a/ScopeOneCore/include/scopeone/HardwareProvider.h
+++ b/ScopeOneCore/include/scopeone/HardwareProvider.h
@@ -9,10 +9,10 @@
namespace scopeone::core
{
- class HardwareProvider
+ class SCOPEONE_CORE_EXPORT HardwareProvider
{
public:
- virtual ~HardwareProvider() = default;
+ virtual ~HardwareProvider();
virtual HardwareProviderDescriptor descriptor() const = 0;
virtual QList devices() const = 0;
diff --git a/ScopeOneCore/include/scopeone/HardwareTypes.h b/ScopeOneCore/include/scopeone/HardwareTypes.h
index 9183df5..0b0a267 100644
--- a/ScopeOneCore/include/scopeone/HardwareTypes.h
+++ b/ScopeOneCore/include/scopeone/HardwareTypes.h
@@ -4,8 +4,6 @@
#include
#include
-#include
-
#include "scopeone/scopeone_core_export.h"
namespace scopeone::core
@@ -18,7 +16,17 @@ namespace scopeone::core
ZStage,
Shutter,
State,
- Hub
+ Hub,
+ Serial,
+ Generic,
+ AutoFocus,
+ ImageProcessor,
+ SignalIO,
+ Magnifier,
+ SLM,
+ Galvo,
+ PressurePump,
+ VolumetricPump
};
enum class HardwareDeviceState
@@ -56,23 +64,6 @@ namespace scopeone::core
QVariantMap properties;
};
- struct SCOPEONE_CORE_EXPORT ClockStamp
- {
- std::int64_t ticks{0};
- std::int64_t tickPeriodNumerator{1};
- std::int64_t tickPeriodDenominator{1000000000};
- std::int64_t hostMonotonicNs{0};
- QString clockDomain;
- QString source;
-
- bool isValid() const
- {
- return tickPeriodNumerator > 0
- && tickPeriodDenominator > 0
- && !clockDomain.isEmpty()
- && !source.isEmpty();
- }
- };
}
Q_DECLARE_METATYPE(scopeone::core::HardwareDeviceKind)
@@ -80,4 +71,3 @@ Q_DECLARE_METATYPE(scopeone::core::HardwareDeviceState)
Q_DECLARE_METATYPE(scopeone::core::HardwareEndpointKind)
Q_DECLARE_METATYPE(scopeone::core::HardwareProviderDescriptor)
Q_DECLARE_METATYPE(scopeone::core::HardwareDeviceDescriptor)
-Q_DECLARE_METATYPE(scopeone::core::ClockStamp)
diff --git a/ScopeOneCore/include/scopeone/ImageFrame.h b/ScopeOneCore/include/scopeone/ImageFrame.h
index 5241cef..c2754b5 100644
--- a/ScopeOneCore/include/scopeone/ImageFrame.h
+++ b/ScopeOneCore/include/scopeone/ImageFrame.h
@@ -8,7 +8,6 @@
#include
#include "scopeone/SharedFrame.h"
-#include "scopeone/HardwareTypes.h"
namespace scopeone::core
{
@@ -29,7 +28,6 @@ namespace scopeone::core
ImagePixelFormat pixelFormat{ImagePixelFormat::Invalid};
quint64 frameIndex{0};
quint64 timestampNs{0};
- ClockStamp clockStamp;
int sourceRoiX{0};
int sourceRoiY{0};
int sourceRoiWidth{0};
@@ -171,12 +169,6 @@ namespace scopeone::core
frame.bitsPerSample = static_cast(header.bitsPerSample);
frame.frameIndex = header.frameIndex;
frame.timestampNs = header.timestampNs;
- if (frame.timestampNs > 0)
- {
- frame.clockStamp.ticks = static_cast(frame.timestampNs);
- frame.clockStamp.clockDomain = QStringLiteral("provider.timestampNs");
- frame.clockStamp.source = QStringLiteral("Driver");
- }
if (header.pixelFormat == static_cast(SharedPixelFormat::Mono16))
{
diff --git a/ScopeOneCore/include/scopeone/ScopeOneCore.h b/ScopeOneCore/include/scopeone/ScopeOneCore.h
index f8675d5..21c63b5 100644
--- a/ScopeOneCore/include/scopeone/ScopeOneCore.h
+++ b/ScopeOneCore/include/scopeone/ScopeOneCore.h
@@ -532,6 +532,10 @@ namespace scopeone::core
QStringList cameraIds() const { return m_cameraIds; }
QList hardwareDevices() const;
bool registerHardwareProvider(const HardwareProviderPtr& provider);
+ bool registerDriverHostProvider(const QString& providerId,
+ const QString& modulePath,
+ const QVariantMap& options = {},
+ QString* errorMessage = nullptr);
bool unregisterHardwareProvider(const QString& providerId);
QStringList runningPreviewCameraIds() const;
double cameraPixelSizeUm(const QString& cameraId) const;
@@ -592,6 +596,15 @@ namespace scopeone::core
quint64 moveZRelative(const QString& zStageLabel, double dz);
quint64 moveXYTo(const QString& xyStageLabel, double x, double y);
quint64 moveZTo(const QString& zStageLabel, double z);
+ bool readShutterOpen(const QString& shutterLabel, bool& open) const;
+ bool setShutterOpen(const QString& shutterLabel,
+ bool open,
+ QString* errorMessage = nullptr);
+ bool readDeviceState(const QString& deviceLabel, long& state) const;
+ bool setDeviceState(const QString& deviceLabel,
+ long state,
+ QString* errorMessage = nullptr);
+ QString deviceStateLabel(const QString& deviceLabel, long state) const;
bool readExposure(const QString& cameraIdOrAll, double& exposureMs) const;
QStringList availableConfigGroups() const;
@@ -683,7 +696,7 @@ namespace scopeone::core
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 driverHostControlServerListening(const QString& cameraId, const QString& serverName);
void processedFrameReady(const ImageFrame& frame);
void processedFramesCompleted(const QString& cameraId, quint64 frameCount);
void previewProcessedFrameReady(const ImageFrame& frame);
@@ -786,7 +799,7 @@ namespace scopeone::core
void unloadConfigurationForShutdown();
void applySystemShutdownPreset();
- void applyLoadedConfiguration(const QString& configPath,
+ bool applyLoadedConfiguration(const QString& configPath,
const LoadConfigResult& result);
void synchronizeCameraIdsFromRegistry();
void finishConfigurationLoadFailure(const LoadConfigResult& result,
@@ -798,10 +811,8 @@ namespace scopeone::core
void startConfigurationUnloadTask();
quint64 queueStageMove(
const QString& deviceLabel,
- std::function command);
+ std::function command);
std::shared_ptr core() 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,
const QString& sourceId,
diff --git a/ScopeOneCore/include/scopeone/SimulatorProvider.h b/ScopeOneCore/include/scopeone/SimulatorProvider.h
index ae63351..94ff509 100644
--- a/ScopeOneCore/include/scopeone/SimulatorProvider.h
+++ b/ScopeOneCore/include/scopeone/SimulatorProvider.h
@@ -1,6 +1,7 @@
#pragma once
#include
+#include
#include
#include
@@ -17,7 +18,8 @@ namespace scopeone::core
public:
explicit SimulatorProvider(const QString& logicalCameraId = QStringLiteral("camera.simulator"),
int width = 512,
- int height = 512);
+ int height = 512,
+ const QString& providerId = {});
HardwareProviderDescriptor descriptor() const override;
QList devices() const override;
@@ -70,5 +72,6 @@ namespace scopeone::core
quint64 m_frameIndex{0};
FrameSink m_frameSink;
QTimer m_timer;
+ mutable QMutex m_mutex;
};
}
diff --git a/ScopeOneCore/internal/AcquisitionEngine.h b/ScopeOneCore/internal/AcquisitionEngine.h
index 1401a95..2293794 100644
--- a/ScopeOneCore/internal/AcquisitionEngine.h
+++ b/ScopeOneCore/internal/AcquisitionEngine.h
@@ -14,23 +14,12 @@ namespace scopeone::core::internal
Q_OBJECT
public:
- enum class State
- {
- Idle,
- Prepared,
- Running
- };
-
AcquisitionEngine(DeviceRegistry* deviceRegistry, QObject* parent = nullptr);
- void prepare();
- void reset();
bool start(const QString& cameraIdOrAll);
bool stop(const QString& cameraIdOrAll);
- State state() const { return m_state; }
private:
DeviceRegistry* m_deviceRegistry{nullptr};
- State m_state{State::Idle};
};
}
diff --git a/ScopeOneCore/internal/AgentProtocol.h b/ScopeOneCore/internal/AgentProtocol.h
deleted file mode 100644
index e97ea2a..0000000
--- a/ScopeOneCore/internal/AgentProtocol.h
+++ /dev/null
@@ -1,58 +0,0 @@
-#pragma once
-
-#include
-
-#include "internal/DriverHostProtocol.h"
-
-namespace scopeone::core::internal::agent
-{
- using driverhost::kProtocolVersion;
- using driverhost::kEnvelopeKindField;
- using driverhost::kEnvelopeVersionField;
- using driverhost::kEnvelopeRequestIdField;
- using driverhost::kMessageTypeField;
- using driverhost::kMessageKindRequest;
- using driverhost::kMessageKindResponse;
- using driverhost::kMessageKindEvent;
- using driverhost::encodeUInt64;
- using driverhost::decodeUInt64;
- using driverhost::makeEnvelope;
- using driverhost::encodeMessage;
- using driverhost::DecodeResult;
- using driverhost::tryDecodeMessage;
-
- 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");
- inline const QString kCommandSetProperty = QStringLiteral("SetProperty");
- inline const QString kCommandCaptureEvent = QStringLiteral("CaptureEvent");
- inline const QString kCommandSetRoi = QStringLiteral("SetROI");
- 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 kExecutableFileName = QStringLiteral("ScopeOne_Agent.exe");
-
- inline QString controlServerName(const QString& cameraId)
- {
- return QStringLiteral("ScopeOne.%1.ctrl").arg(cameraId);
- }
-
- inline QString sharedMemoryKey(const QString& cameraId)
- {
- return QStringLiteral("ScopeOne.%1.shm").arg(cameraId);
- }
-
-} // namespace scopeone::core::internal::agent
diff --git a/ScopeOneCore/internal/CameraBackend.h b/ScopeOneCore/internal/CameraBackend.h
index 99e3be1..c8fd814 100644
--- a/ScopeOneCore/internal/CameraBackend.h
+++ b/ScopeOneCore/internal/CameraBackend.h
@@ -52,7 +52,7 @@ namespace scopeone::core::internal
enum class Kind
{
Native,
- Agent
+ DriverHost
};
explicit CameraBackend(ProcessingFrameGate& processingFrameGate,
@@ -71,7 +71,7 @@ namespace scopeone::core::internal
virtual bool configureNativeCamera(const std::shared_ptr& core,
const QString& cameraId,
double exposureMs);
- virtual bool addAgentCamera(const QString& cameraId,
+ virtual bool addDriverHostCamera(const QString& cameraId,
const QString& adapter,
const QString& device,
const QStringList& preInitProperties,
@@ -113,7 +113,7 @@ namespace scopeone::core::internal
void recordingFramesReady(const QList& frames);
void frameDeliveryFailed(const QString& errorMessage, quint64 droppedFrames);
void previewStateChanged(bool running);
- void agentControlServerListening(const QString& cameraId, const QString& serverName);
+ void driverHostControlServerListening(const QString& cameraId, const QString& serverName);
protected:
bool applyWithPreviewRestart(const QString& cameraId, const std::function& operation);
@@ -170,5 +170,5 @@ namespace scopeone::core::internal
};
std::unique_ptr createNativeCameraBackend(ProcessingFrameGate& processingFrameGate);
- std::unique_ptr createAgentCameraBackend(ProcessingFrameGate& processingFrameGate);
+ std::unique_ptr createDriverHostCameraBackend(ProcessingFrameGate& processingFrameGate);
}
diff --git a/ScopeOneCore/internal/CameraManager.h b/ScopeOneCore/internal/CameraManager.h
index bc44bff..7a622be 100644
--- a/ScopeOneCore/internal/CameraManager.h
+++ b/ScopeOneCore/internal/CameraManager.h
@@ -29,7 +29,7 @@ namespace scopeone::core::internal
bool configureNativeCamera(const std::shared_ptr& core,
const QString& cameraId,
double exposureMs = 0.0);
- bool addAgentCamera(const QString& cameraId,
+ bool addDriverHostCamera(const QString& cameraId,
const QString& adapter,
const QString& device,
const QStringList& preInitProperties = QStringList(),
@@ -40,7 +40,7 @@ namespace scopeone::core::internal
bool startPreview() override;
bool stopPreview() override;
- bool usesAgentBackend() const;
+ bool usesDriverHostBackend() const;
bool startPreviewFor(const QString& cameraId) override;
bool stopPreviewFor(const QString& cameraId) override;
bool isPreviewRunning(const QString& cameraId) const override;
@@ -76,13 +76,12 @@ namespace scopeone::core::internal
int timeoutMs = 1500) override;
signals:
- void newRawFrameReady(const scopeone::core::ImageFrame& frame);
void processingFrameReady(const scopeone::core::ImageFrame& frame, quint64 token);
void rawFramesAcquired(const QString& cameraId, quint64 frameCount);
void recordingFramesReady(const QList& frames);
void frameDeliveryFailed(const QString& errorMessage, quint64 droppedFrames);
void previewStateChanged(bool running);
- void agentControlServerListening(const QString& cameraId, const QString& serverName);
+ void driverHostControlServerListening(const QString& cameraId, const QString& serverName);
private:
bool activateBackend(CameraBackend::Kind kind);
diff --git a/ScopeOneCore/internal/DriverHostProtocol.h b/ScopeOneCore/internal/DriverHostProtocol.h
index 36793d3..1e660b0 100644
--- a/ScopeOneCore/internal/DriverHostProtocol.h
+++ b/ScopeOneCore/internal/DriverHostProtocol.h
@@ -2,24 +2,111 @@
#include
#include
+#include
#include
#include
+#include
+#include
#include
namespace scopeone::core::internal::driverhost
{
- inline constexpr quint32 kProtocolVersion = 3;
+ inline constexpr quint32 kProtocolVersion = 5;
inline constexpr quint32 kMaxControlMessageBytes = 256 * 1024;
inline const QString kEnvelopeKindField = QStringLiteral("kind");
inline const QString kEnvelopeVersionField = QStringLiteral("version");
inline const QString kEnvelopeRequestIdField = QStringLiteral("requestId");
inline const QString kMessageTypeField = QStringLiteral("type");
+ inline const QString kProviderIdField = QStringLiteral("providerId");
+ inline const QString kDeviceIdField = QStringLiteral("deviceId");
+ inline const QString kDeviceKindField = QStringLiteral("deviceKind");
+ inline const QString kCapabilitiesField = QStringLiteral("capabilities");
+ inline const QString kDevicesField = QStringLiteral("devices");
+ inline const QString kProviderNameField = QStringLiteral("providerName");
+ inline const QString kProviderVersionField = QStringLiteral("providerVersion");
+ inline const QString kProviderDeviceIdField = QStringLiteral("providerDeviceId");
+ inline const QString kHardwareIdField = QStringLiteral("hardwareId");
+ inline const QString kDeviceNameField = QStringLiteral("deviceName");
+ inline const QString kDeviceStateField = QStringLiteral("deviceState");
+ inline const QString kDevicePropertiesField = QStringLiteral("deviceProperties");
+ inline const QString kSharedMemoryKeyField = QStringLiteral("sharedMemoryKey");
+ inline const QString kDefaultXYStageField = QStringLiteral("defaultXYStage");
+ inline const QString kDefaultZStageField = QStringLiteral("defaultZStage");
inline const QString kMessageKindRequest = QStringLiteral("Request");
inline const QString kMessageKindResponse = QStringLiteral("Response");
inline const QString kMessageKindEvent = QStringLiteral("Event");
+ inline const QString kCommandDescribe = QStringLiteral("Describe");
+ 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 kCommandGetExposure = QStringLiteral("GetExposure");
+ inline const QString kCommandSetExposure = QStringLiteral("SetExposure");
+ inline const QString kCommandListProperties = QStringLiteral("ListProperties");
+ inline const QString kCommandGetProperty = QStringLiteral("GetProperty");
+ inline const QString kCommandSetProperty = QStringLiteral("SetProperty");
+ inline const QString kCommandCaptureEvent = QStringLiteral("CaptureEvent");
+ inline const QString kCommandSetRoi = QStringLiteral("SetROI");
+ inline const QString kCommandClearRoi = QStringLiteral("ClearROI");
+ inline const QString kCommandGetRoi = QStringLiteral("GetROI");
+ inline const QString kCommandGetXYPosition = QStringLiteral("GetXYPosition");
+ inline const QString kCommandGetZPosition = QStringLiteral("GetZPosition");
+ inline const QString kCommandSetRelativeXYPosition = QStringLiteral("SetRelativeXYPosition");
+ inline const QString kCommandSetRelativeZPosition = QStringLiteral("SetRelativeZPosition");
+ inline const QString kCommandSetXYPosition = QStringLiteral("SetXYPosition");
+ inline const QString kCommandSetZPosition = QStringLiteral("SetZPosition");
+ inline const QString kCommandGetShutterOpen = QStringLiteral("GetShutterOpen");
+ inline const QString kCommandSetShutterOpen = QStringLiteral("SetShutterOpen");
+ inline const QString kCommandGetState = QStringLiteral("GetState");
+ inline const QString kCommandSetState = QStringLiteral("SetState");
+ inline const QString kCommandGetStateLabel = QStringLiteral("GetStateLabel");
+ inline const QString kCommandListConfigGroups = QStringLiteral("ListConfigGroups");
+ inline const QString kCommandListConfigs = QStringLiteral("ListConfigs");
+ inline const QString kCommandGetCurrentConfig = QStringLiteral("GetCurrentConfig");
+ inline const QString kCommandSetConfig = QStringLiteral("SetConfig");
+
+ inline const QString kCapabilityCamera = QStringLiteral("Camera");
+ inline const QString kCapabilityProperties = QStringLiteral("Properties");
+ inline const QString kCapabilityStage = QStringLiteral("Stage");
+ inline const QString kCapabilityShutter = QStringLiteral("Shutter");
+ inline const QString kCapabilityState = QStringLiteral("State");
+ inline const QString kCapabilityConfiguration = QStringLiteral("Configuration");
+
+ 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 kEventDriverHostError = QStringLiteral("DriverHostError");
+
+#ifdef Q_OS_WIN
+ inline const QString kExecutableFileName = QStringLiteral("ScopeOne_DriverHost.exe");
+#else
+ inline const QString kExecutableFileName = QStringLiteral("ScopeOne_DriverHost");
+#endif
+
+ inline QString controlServerName(const QString& deviceId)
+ {
+ return QStringLiteral("ScopeOne.DriverHost.%1.ctrl").arg(deviceId);
+ }
+
+ inline QString sharedMemoryKey(const QString& deviceId)
+ {
+ return QStringLiteral("ScopeOne.DriverHost.%1.shm").arg(deviceId);
+ }
+
+ inline QString sharedMemoryKey(const QString& hostKey, int cameraIndex)
+ {
+ return QStringLiteral("ScopeOne.DriverHost.%1.%2.shm")
+ .arg(hostKey)
+ .arg(cameraIndex);
+ }
+
inline QString encodeUInt64(quint64 value)
{
return QString::number(value);
diff --git a/ScopeOneCore/internal/DriverHostProviderProxy.h b/ScopeOneCore/internal/DriverHostProviderProxy.h
new file mode 100644
index 0000000..d164a97
--- /dev/null
+++ b/ScopeOneCore/internal/DriverHostProviderProxy.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include
+#include
+
+#include "scopeone/HardwareProvider.h"
+
+namespace scopeone::core::internal
+{
+ HardwareProviderPtr createDriverHostProviderProxy(const QString& providerId,
+ const QString& pluginPath,
+ const QJsonObject& options,
+ QString* errorMessage = nullptr);
+}
diff --git a/ScopeOneCore/internal/FrameRouter.h b/ScopeOneCore/internal/FrameRouter.h
index f7650c2..1e9a062 100644
--- a/ScopeOneCore/internal/FrameRouter.h
+++ b/ScopeOneCore/internal/FrameRouter.h
@@ -4,11 +4,6 @@
#include "scopeone/ImageFrame.h"
-namespace scopeone::core
-{
- class ClockService;
-}
-
namespace scopeone::core::internal
{
class FrameRouter : public QObject
@@ -16,14 +11,11 @@ namespace scopeone::core::internal
Q_OBJECT
public:
- FrameRouter(scopeone::core::ClockService* clockService,
- QObject* parent = nullptr);
+ explicit FrameRouter(QObject* parent = nullptr);
void publish(const scopeone::core::ImageFrame& frame);
signals:
void frameReady(const scopeone::core::ImageFrame& frame);
- private:
- scopeone::core::ClockService* m_clockService{nullptr};
};
}
diff --git a/ScopeOneCore/internal/HardwareRuntime.h b/ScopeOneCore/internal/HardwareRuntime.h
index 0458291..f733c2a 100644
--- a/ScopeOneCore/internal/HardwareRuntime.h
+++ b/ScopeOneCore/internal/HardwareRuntime.h
@@ -3,14 +3,15 @@
#include
#include
#include
+#include
#include
#include
#include "scopeone/HardwareProvider.h"
-#include "scopeone/ClockService.h"
#include "scopeone/CameraProvider.h"
#include "internal/AcquisitionEngine.h"
+#include "internal/CameraRuntimeControl.h"
#include "internal/FrameRouter.h"
namespace scopeone::core::internal
@@ -23,9 +24,10 @@ namespace scopeone::core::internal
explicit DeviceRegistry(QObject* parent = nullptr);
void clear();
- bool registerProvider(const HardwareProviderPtr& provider);
+ bool registerProvider(const HardwareProviderPtr& provider,
+ const HardwareProviderDescriptor& descriptor,
+ const QList& devices);
void unregisterProvider(const QString& providerId);
- void refreshProvider(const QString& providerId);
QList providers() const;
QList devices() const;
HardwareDeviceDescriptor device(const QString& logicalId) const;
@@ -39,13 +41,21 @@ namespace scopeone::core::internal
struct ProviderEntry
{
HardwareProviderPtr provider;
+ HardwareProviderDescriptor descriptor;
QList devices;
};
+ mutable QReadWriteLock m_lock;
QHash m_providers;
};
- class HardwareRuntime : public QObject, public CameraProvider
+ class HardwareRuntime : public QObject,
+ public CameraProvider,
+ public StageProvider,
+ public ShutterProvider,
+ public StateProvider,
+ public ConfigurationProvider,
+ public CameraRuntimeControl
{
Q_OBJECT
@@ -86,26 +96,75 @@ namespace scopeone::core::internal
bool captureEventFrame(const QString& cameraId,
ImageFrame& frame,
int timeoutMs) override;
+ void setFrameDeliveryPaused(bool paused) override;
+ bool setRecordingFrameDeliveryEnabled(bool enabled) override;
+ bool setHighRateFrameDeliveryEnabled(bool enabled) override;
+ bool isProcessingFrameTokenCurrent(const QString& cameraId, quint64 token) override;
+ void finishProcessingFrame(const QString& cameraId, quint64 token) override;
+ QString defaultXYStage() const override;
+ QString defaultZStage() const override;
+ bool getXYPosition(const QString& deviceId,
+ double& x,
+ double& y,
+ QString* errorMessage) const override;
+ bool getZPosition(const QString& deviceId,
+ double& z,
+ QString* errorMessage) const override;
+ bool setRelativeXYPosition(const QString& deviceId,
+ double dx,
+ double dy,
+ QString* errorMessage) override;
+ bool setRelativeZPosition(const QString& deviceId,
+ double dz,
+ QString* errorMessage) override;
+ bool setXYPosition(const QString& deviceId,
+ double x,
+ double y,
+ QString* errorMessage) override;
+ bool setZPosition(const QString& deviceId,
+ double z,
+ QString* errorMessage) override;
+ bool isShutterOpen(const QString& deviceId,
+ bool& open,
+ QString* errorMessage) const override;
+ bool setShutterOpen(const QString& deviceId,
+ bool open,
+ QString* errorMessage) override;
+ bool getState(const QString& deviceId,
+ long& state,
+ QString* errorMessage) const override;
+ bool setState(const QString& deviceId,
+ long state,
+ QString* errorMessage) override;
+ QString stateLabel(const QString& deviceId, long state) const override;
+ QStringList availableConfigGroups() const override;
+ QStringList availableConfigs(const QString& groupName) const override;
+ QString currentConfig(const QString& groupName) const override;
+ bool setConfig(const QString& groupName,
+ const QString& configName,
+ QString* errorMessage) override;
DeviceRegistry* deviceRegistry() { return &m_registry; }
const DeviceRegistry* deviceRegistry() const { return &m_registry; }
- AcquisitionEngine* acquisitionEngine() { return &m_acquisitionEngine; }
- ClockService* clockService() { return &m_clockService; }
FrameRouter* frameRouter() { return &m_frameRouter; }
void clear();
bool registerProvider(const HardwareProviderPtr& provider);
void unregisterProvider(const QString& providerId);
- void refreshProvider(const QString& providerId);
+ bool refreshProvider(const QString& providerId);
signals:
void devicesChanged();
private:
CameraProvider* cameraProviderForDevice(const QString& logicalId) const;
+ DevicePropertyProvider* propertyProviderForDevice(const QString& logicalId) const;
+ StageProvider* stageProviderForDevice(const QString& logicalId) const;
+ ShutterProvider* shutterProviderForDevice(const QString& logicalId) const;
+ StateProvider* stateProviderForDevice(const QString& logicalId) const;
+ ConfigurationProvider* configurationProviderForGroup(const QString& groupName) const;
QList cameraProviders() const;
DeviceRegistry m_registry;
- ClockService m_clockService;
FrameRouter m_frameRouter;
AcquisitionEngine m_acquisitionEngine;
FrameSink m_frameSink;
diff --git a/ScopeOneCore/internal/MDAManager.h b/ScopeOneCore/internal/MDAManager.h
index eb50e47..5c2ee8f 100644
--- a/ScopeOneCore/internal/MDAManager.h
+++ b/ScopeOneCore/internal/MDAManager.h
@@ -7,12 +7,9 @@
#include
#include
#include
-#include
-
#include "scopeone/ExperimentDocument.h"
#include "scopeone/CameraProvider.h"
-
-class CMMCore;
+#include "scopeone/HardwareCapabilities.h"
namespace scopeone::core::internal
{
@@ -31,12 +28,13 @@ namespace scopeone::core::internal
Q_OBJECT
public:
- explicit MDAManager(std::shared_ptr core, QObject* parent = nullptr);
+ explicit MDAManager(QObject* parent = nullptr);
~MDAManager() override;
bool isRunning() const { return m_running.load(); }
void setCameraProvider(CameraProvider* cameraProvider);
+ void setStageProvider(StageProvider* stageProvider);
bool start(const QList& events, bool block = false);
void requestCancel();
void cancelAndWait();
@@ -50,15 +48,18 @@ namespace scopeone::core::internal
private:
bool setupEvent(const AcquisitionEvent& event, QString* errorMessage);
bool execEvent(const AcquisitionEvent& event, MDAOutput& output, QString* errorMessage);
- bool execEventSingleCamera(const AcquisitionEvent& event, MDAOutput& output, QString* errorMessage);
- bool execEventMultiCamera(const AcquisitionEvent& event, MDAOutput& output, QString* errorMessage);
- bool setExposure(double exposureMs, QString* errorMessage);
+ bool captureCameras(const AcquisitionEvent& event,
+ MDAOutput& output,
+ QString* errorMessage);
+ bool setExposure(const QStringList& cameraIds,
+ double exposureMs,
+ QString* errorMessage);
bool moveXY(double x, double y, QString* errorMessage);
bool moveZ(double z, QString* errorMessage);
void runSequence(QList events);
- std::shared_ptr m_mmcore;
CameraProvider* m_cameraProvider{nullptr};
+ StageProvider* m_stageProvider{nullptr};
QThreadPool m_threadPool;
std::atomic m_running{false};
std::atomic m_cancelRequested{false};
diff --git a/ScopeOneCore/internal/MicroManagerProvider.h b/ScopeOneCore/internal/MicroManagerProvider.h
index 423064b..7f10347 100644
--- a/ScopeOneCore/internal/MicroManagerProvider.h
+++ b/ScopeOneCore/internal/MicroManagerProvider.h
@@ -2,15 +2,28 @@
#include
+#include
+
#include "scopeone/HardwareProvider.h"
#include "scopeone/CameraProvider.h"
+#include "internal/CameraRuntimeControl.h"
+
+class CMMCore;
namespace scopeone::core::internal
{
- class MicroManagerProvider final : public HardwareProvider, public CameraProvider
+ class MicroManagerProvider final : public HardwareProvider,
+ public CameraProvider,
+ public StageProvider,
+ public ShutterProvider,
+ public StateProvider,
+ public ConfigurationProvider,
+ public CameraRuntimeControl
{
public:
- explicit MicroManagerProvider(CameraProvider* cameraProvider);
+ MicroManagerProvider(std::shared_ptr core,
+ CameraProvider* cameraProvider,
+ CameraRuntimeControl* cameraRuntimeControl);
HardwareProviderDescriptor descriptor() const override;
QList devices() const override;
@@ -50,9 +63,60 @@ namespace scopeone::core::internal
bool captureEventFrame(const QString& cameraId,
scopeone::core::ImageFrame& frame,
int timeoutMs) override;
+ void setFrameDeliveryPaused(bool paused) override;
+ bool setRecordingFrameDeliveryEnabled(bool enabled) override;
+ bool setHighRateFrameDeliveryEnabled(bool enabled) override;
+ bool isProcessingFrameTokenCurrent(const QString& cameraId, quint64 token) override;
+ void finishProcessingFrame(const QString& cameraId, quint64 token) override;
+ QString defaultXYStage() const override;
+ QString defaultZStage() const override;
+ bool getXYPosition(const QString& deviceId,
+ double& x,
+ double& y,
+ QString* errorMessage) const override;
+ bool getZPosition(const QString& deviceId,
+ double& z,
+ QString* errorMessage) const override;
+ bool setRelativeXYPosition(const QString& deviceId,
+ double dx,
+ double dy,
+ QString* errorMessage) override;
+ bool setRelativeZPosition(const QString& deviceId,
+ double dz,
+ QString* errorMessage) override;
+ bool setXYPosition(const QString& deviceId,
+ double x,
+ double y,
+ QString* errorMessage) override;
+ bool setZPosition(const QString& deviceId,
+ double z,
+ QString* errorMessage) override;
+ bool isShutterOpen(const QString& deviceId,
+ bool& open,
+ QString* errorMessage) const override;
+ bool setShutterOpen(const QString& deviceId,
+ bool open,
+ QString* errorMessage) override;
+ bool getState(const QString& deviceId,
+ long& state,
+ QString* errorMessage) const override;
+ bool setState(const QString& deviceId,
+ long state,
+ QString* errorMessage) override;
+ QString stateLabel(const QString& deviceId, long state) const override;
+ QStringList availableConfigGroups() const override;
+ QStringList availableConfigs(const QString& groupName) const override;
+ QString currentConfig(const QString& groupName) const override;
+ bool setConfig(const QString& groupName,
+ const QString& configName,
+ QString* errorMessage) override;
private:
+ bool isCamera(const QString& deviceId) const;
+
+ std::shared_ptr m_core;
CameraProvider* m_cameraProvider{nullptr};
+ CameraRuntimeControl* m_cameraRuntimeControl{nullptr};
QList m_devices;
};
}
diff --git a/ScopeOneCore/internal/RecordingManager.h b/ScopeOneCore/internal/RecordingManager.h
index 4859b20..9285da4 100644
--- a/ScopeOneCore/internal/RecordingManager.h
+++ b/ScopeOneCore/internal/RecordingManager.h
@@ -2,6 +2,7 @@
#include "scopeone/ScopeOneCore.h"
#include "scopeone/CameraProvider.h"
+#include "scopeone/HardwareCapabilities.h"
#include "internal/MDAManager.h"
#include "internal/CameraRuntimeControl.h"
#include
@@ -14,8 +15,6 @@
#include
#include
-class CMMCore;
-
namespace scopeone::core::internal
{
using scopeone::core::RecordingFormat;
@@ -36,12 +35,11 @@ namespace scopeone::core::internal
~RecordingManager() override;
void setCameraProvider(CameraProvider* cameraProvider) { m_cameraProvider = cameraProvider; }
+ void setStageProvider(StageProvider* stageProvider) { m_stageProvider = stageProvider; }
void setCameraRuntimeControl(CameraRuntimeControl* cameraRuntimeControl)
{
m_cameraRuntimeControl = cameraRuntimeControl;
}
- void setMMCore(const std::shared_ptr& core) { m_mmcore = core; }
-
void setLatestFrameFetcher(std::function fetcher)
{
m_latestFrameFetcher = std::move(fetcher);
@@ -208,8 +206,8 @@ namespace scopeone::core::internal
void advanceBurstStateIfNeeded();
CameraProvider* m_cameraProvider{nullptr};
+ StageProvider* m_stageProvider{nullptr};
CameraRuntimeControl* m_cameraRuntimeControl{nullptr};
- std::shared_ptr m_mmcore;
std::function m_latestFrameFetcher;
std::function m_sessionPreparationCallback;
diff --git a/ScopeOneCore/src/AcquisitionEngine.cpp b/ScopeOneCore/src/AcquisitionEngine.cpp
index 7e44f9b..792e022 100644
--- a/ScopeOneCore/src/AcquisitionEngine.cpp
+++ b/ScopeOneCore/src/AcquisitionEngine.cpp
@@ -3,8 +3,6 @@
#include "scopeone/CameraProvider.h"
#include "internal/HardwareRuntime.h"
-#include
-
namespace scopeone::core::internal
{
AcquisitionEngine::AcquisitionEngine(DeviceRegistry* deviceRegistry, QObject* parent)
@@ -13,19 +11,9 @@ namespace scopeone::core::internal
{
}
- void AcquisitionEngine::prepare()
- {
- m_state = State::Prepared;
- }
-
- void AcquisitionEngine::reset()
- {
- m_state = State::Idle;
- }
-
bool AcquisitionEngine::start(const QString& cameraIdOrAll)
{
- if (!m_deviceRegistry || m_state == State::Idle)
+ if (!m_deviceRegistry)
{
return false;
}
@@ -34,54 +22,56 @@ namespace scopeone::core::internal
{
return false;
}
- bool started = false;
if (target.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0)
{
- QList startedProviders;
- QSet visited;
- started = true;
+ struct StartedCamera
+ {
+ CameraProvider* provider;
+ QString cameraId;
+ };
+ QList startedCameras;
+ bool found = false;
for (const HardwareDeviceDescriptor& device : m_deviceRegistry->devices())
{
if (device.kind != HardwareDeviceKind::Camera)
{
continue;
}
+ found = true;
const HardwareProviderPtr provider = m_deviceRegistry->provider(device.providerId);
auto* cameraProvider = dynamic_cast(provider.get());
- if (!cameraProvider || visited.contains(cameraProvider))
+ if (!cameraProvider)
+ {
+ for (auto it = startedCameras.crbegin(); it != startedCameras.crend(); ++it)
+ {
+ it->provider->stopPreviewFor(it->cameraId);
+ }
+ return false;
+ }
+ if (cameraProvider->isPreviewRunning(device.logicalId))
{
continue;
}
- visited.insert(cameraProvider);
- if (!cameraProvider->startPreview())
+ if (!cameraProvider->startPreviewFor(device.logicalId))
{
- started = false;
- for (CameraProvider* activeProvider : startedProviders)
+ for (auto it = startedCameras.crbegin(); it != startedCameras.crend(); ++it)
{
- activeProvider->stopPreview();
+ it->provider->stopPreviewFor(it->cameraId);
}
- break;
+ return false;
}
- startedProviders.append(cameraProvider);
+ startedCameras.append({cameraProvider, device.logicalId});
}
- started = started && !startedProviders.isEmpty();
- }
- else
- {
- const HardwareProviderPtr provider = m_deviceRegistry->providerForDevice(target);
- auto* cameraProvider = dynamic_cast(provider.get());
- started = cameraProvider && cameraProvider->startPreviewFor(target);
+ return found;
}
- if (started)
- {
- m_state = State::Running;
- }
- return started;
+ const HardwareProviderPtr provider = m_deviceRegistry->providerForDevice(target);
+ auto* cameraProvider = dynamic_cast(provider.get());
+ return cameraProvider && cameraProvider->startPreviewFor(target);
}
bool AcquisitionEngine::stop(const QString& cameraIdOrAll)
{
- if (!m_deviceRegistry || m_state == State::Idle)
+ if (!m_deviceRegistry)
{
return false;
}
@@ -90,40 +80,33 @@ namespace scopeone::core::internal
{
return false;
}
- bool stopped = false;
if (target.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0)
{
- QSet visited;
- stopped = true;
bool found = false;
+ bool stopped = true;
for (const HardwareDeviceDescriptor& device : m_deviceRegistry->devices())
{
if (device.kind != HardwareDeviceKind::Camera)
{
continue;
}
+ found = true;
const HardwareProviderPtr provider = m_deviceRegistry->provider(device.providerId);
auto* cameraProvider = dynamic_cast(provider.get());
- if (!cameraProvider || visited.contains(cameraProvider))
+ if (!cameraProvider)
{
+ stopped = false;
continue;
}
- found = true;
- visited.insert(cameraProvider);
- stopped = cameraProvider->stopPreview() && stopped;
+ if (cameraProvider->isPreviewRunning(device.logicalId))
+ {
+ stopped = cameraProvider->stopPreviewFor(device.logicalId) && stopped;
+ }
}
- stopped = found && stopped;
- }
- else
- {
- const HardwareProviderPtr provider = m_deviceRegistry->providerForDevice(target);
- auto* cameraProvider = dynamic_cast(provider.get());
- stopped = cameraProvider && cameraProvider->stopPreviewFor(target);
- }
- if (stopped && target.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0)
- {
- m_state = State::Prepared;
+ return found && stopped;
}
- return stopped;
+ const HardwareProviderPtr provider = m_deviceRegistry->providerForDevice(target);
+ auto* cameraProvider = dynamic_cast(provider.get());
+ return cameraProvider && cameraProvider->stopPreviewFor(target);
}
}
diff --git a/ScopeOneCore/src/AgentMain.cpp b/ScopeOneCore/src/AgentMain.cpp
deleted file mode 100644
index d112669..0000000
--- a/ScopeOneCore/src/AgentMain.cpp
+++ /dev/null
@@ -1,1808 +0,0 @@
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "MMCore.h"
-#include "internal/AgentProtocol.h"
-#include "scopeone/SharedFrame.h"
-
-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;
- using scopeone::core::computeMaxFrameBytes;
- using scopeone::core::kSharedFrameHeaderSize;
- using scopeone::core::kSharedFrameMaxBytes;
- using scopeone::core::kSharedFrameNumSlots;
- 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)
- {
- if (format == SharedPixelFormat::Mono8)
- {
- return 8;
- }
- if (format == SharedPixelFormat::Mono16)
- {
- return bitsPerSample >= 1 && bitsPerSample <= 16
- ? static_cast(bitsPerSample)
- : 16;
- }
- return 0;
- }
-
- class ControlConnection final : public QObject
- {
- Q_OBJECT
-
- public:
- // Wrap one local control socket connection
- ControlConnection(quint64 connectionId, QLocalSocket* socket, QObject* parent = nullptr)
- : QObject(parent)
- , m_connectionId(connectionId)
- , m_socket(socket)
- {
- if (!socket)
- {
- qFatal("ControlConnection requires QLocalSocket");
- }
- m_socket->setParent(this);
-
- connect(m_socket, &QLocalSocket::readyRead,
- this, &ControlConnection::onReadyRead);
- connect(m_socket, &QLocalSocket::disconnected,
- this, &ControlConnection::onDisconnected);
- connect(m_socket, &QLocalSocket::errorOccurred, this,
- [this](QLocalSocket::LocalSocketError socketError)
- {
- qWarning().noquote()
- << QString("Agent control socket error (%1) on connection %2")
- .arg(static_cast(socketError))
- .arg(m_connectionId);
- });
- }
-
- // Send one encoded protocol message to the client
- void sendMessage(const QJsonObject& message)
- {
- if (m_socket->state() != QLocalSocket::ConnectedState)
- {
- return;
- }
- m_socket->write(agent::encodeMessage(message));
- }
-
- signals:
- void requestReceived(quint64 connectionId,
- quint64 requestId,
- const QString& type,
- const QJsonObject& message);
- void connectionClosed(quint64 connectionId);
-
- private slots:
- // Decode queued socket bytes into control requests
- void onReadyRead()
- {
- 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)
- {
- qWarning().noquote()
- << QString("Agent control protocol error on connection %1: %2")
- .arg(m_connectionId)
- .arg(error);
- m_socket->disconnectFromServer();
- return;
- }
-
- if (message.value(agent::kEnvelopeVersionField).toInt(0)
- != static_cast(agent::kProtocolVersion))
- {
- qWarning().noquote()
- << QString("Agent control protocol version mismatch on connection %1")
- .arg(m_connectionId);
- m_socket->disconnectFromServer();
- return;
- }
-
- if (message.value(agent::kEnvelopeKindField).toString()
- != agent::kMessageKindRequest)
- {
- qWarning().noquote()
- << QString("Ignoring non-request control message on connection %1")
- .arg(m_connectionId);
- continue;
- }
-
- const quint64 requestId =
- agent::decodeUInt64(message.value(agent::kEnvelopeRequestIdField));
- const QString type = message.value(agent::kMessageTypeField).toString();
- if (requestId == 0 || type.isEmpty())
- {
- qWarning().noquote()
- << QString("Ignoring malformed request on connection %1")
- .arg(m_connectionId);
- continue;
- }
-
- emit requestReceived(m_connectionId, requestId, type, message);
- }
- }
-
- // Notify the agent when this connection closes
- void onDisconnected()
- {
- emit connectionClosed(m_connectionId);
- deleteLater();
- }
-
- private:
- quint64 m_connectionId{0};
- QLocalSocket* m_socket{nullptr};
- QByteArray m_readBuffer;
- };
-
- class AgentRuntime final : public QObject
- {
- Q_OBJECT
-
- public:
- // Store launch settings for one camera runtime
- AgentRuntime(QString cameraId,
- QString adapter,
- QString device,
- QString shmKey,
- QStringList preInitProperties,
- QStringList properties,
- double exposureMs,
- bool autoPreview,
- QObject* parent = nullptr)
- : QObject(parent)
- , m_cameraId(std::move(cameraId))
- , m_adapter(std::move(adapter))
- , m_device(std::move(device))
- , m_shmKey(std::move(shmKey))
- , m_preInitProperties(std::move(preInitProperties))
- , m_properties(std::move(properties))
- , m_exposureMs(exposureMs)
- , m_autoPreview(autoPreview)
- {
- }
-
- QString lastError() const
- {
- return m_lastError;
- }
-
- // Initialize MMCore camera state and shared memory transport
- bool initializeRuntime()
- {
- m_lastError.clear();
- setState(State::Starting);
-
- if (!m_timer)
- {
- m_timer = new QTimer(this);
- m_timer->setTimerType(Qt::PreciseTimer);
- connect(m_timer, &QTimer::timeout,
- this, &AgentRuntime::pollAndWrite);
- }
-
- try
- {
- m_mmcore = std::make_unique();
- const std::string label = m_cameraId.toStdString();
- const std::string adapter = m_adapter.toStdString();
- const std::string device = m_device.toStdString();
-
- m_mmcore->loadDevice(label.c_str(), adapter.c_str(), device.c_str());
- QString preInitError;
- if (!applyProperties(m_preInitProperties, label, QStringLiteral("pre-init property"), &preInitError))
- {
- m_lastError = preInitError;
- setState(State::Error, m_lastError);
- return false;
- }
- m_mmcore->initializeDevice(label.c_str());
- m_mmcore->setCameraDevice(label.c_str());
-
- QString propertyError;
- if (!applyProperties(m_properties, label, QStringLiteral("property"), &propertyError))
- {
- m_lastError = propertyError;
- setState(State::Error, m_lastError);
- return false;
- }
-
- try
- {
- m_mmcore->setCircularBufferMemoryFootprint(2048);
- }
- catch (const CMMError&)
- {
- }
-
- double finalExposure = 0.0;
- if (m_exposureMs > 0.0)
- {
- m_mmcore->setExposure(m_exposureMs);
- }
- m_mmcore->waitForDevice(label.c_str());
- finalExposure = m_mmcore->getExposure();
- if (finalExposure <= 0.0)
- {
- m_lastError = QStringLiteral("Camera reported an invalid exposure");
- setState(State::Error, m_lastError);
- return false;
- }
- m_exposureMs = finalExposure;
- }
- catch (const CMMError& error)
- {
- m_lastError = QString::fromStdString(error.getMsg());
- setState(State::Error, m_lastError);
- return false;
- }
-
- m_shm = std::make_unique();
- m_shm->setNativeKey(m_shmKey);
- const int totalBytes =
- kSharedMemoryControlSize + kSharedFrameNumSlots * kSharedFrameSlotStride;
- if (!m_shm->create(totalBytes))
- {
- if (m_shm->attach())
- {
- m_shm->detach();
- }
- if (!m_shm->create(totalBytes))
- {
- m_lastError = QStringLiteral("Cannot create shared memory '%1'")
- .arg(m_shmKey);
- setState(State::Error, m_lastError);
- return false;
- }
- }
-
- if (m_shm->lock())
- {
- 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();
- }
-
- if (m_autoPreview)
- {
- QString error;
- if (!startPreviewInternal(&error))
- {
- m_lastError = error;
- setState(State::Error, m_lastError);
- return false;
- }
- }
- else
- {
- setState(State::Idle);
- }
-
- return true;
- }
-
- public slots:
- void publishHello();
- void handleRequest(quint64 connectionId,
- quint64 requestId,
- const QString& type,
- const QJsonObject& message);
- void stopForExit();
-
- signals:
- void responseReady(quint64 connectionId, const QJsonObject& response);
- void eventReady(const QJsonObject& event);
- void shutdownRequested();
-
- private:
- // Runtime state is mirrored to control clients
- enum class State
- {
- Starting,
- Idle,
- Previewing,
- Error,
- ShuttingDown
- };
-
- enum class FrameDeliveryMode
- {
- PreviewLatest,
- LatestOnly,
- AllFrames
- };
-
- // Frame layout describes one shared memory payload shape
- struct FrameLayout
- {
- unsigned width{0};
- unsigned height{0};
- unsigned bytesPerPixel{0};
- SharedPixelFormat format{SharedPixelFormat::Mono8};
- unsigned stride{0};
- quint64 byteCount{0};
- quint16 bitDepth{8};
- quint16 channels{1};
- };
-
- bool previewRunning() const;
- void setState(State state, const QString& error = QString());
- QJsonObject makeResponse(const QString& type, quint64 requestId, bool ok) const;
- QJsonObject makeErrorResponse(const QString& type,
- quint64 requestId,
- const QString& error) const;
- QJsonObject makeEvent(const QString& type) const;
- bool applyProperties(const QStringList& encodedProperties,
- const std::string& label,
- const QString& propertyKind,
- QString* errorMessage);
- void emitPreviewStateEvent();
- void emitAgentErrorEvent(const QString& error);
- void emitFrameAvailableEvent(quint64 frameIndex);
- bool startPreviewInternal(QString* errorMessage);
- bool stopPreviewInternal(QString* errorMessage);
- bool captureEventFrameInternal(quint64& frameIndex, QString* errorMessage);
- 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();
-
- QString m_cameraId;
- QString m_adapter;
- QString m_device;
- QString m_shmKey;
- QStringList m_preInitProperties;
- QStringList m_properties;
- double m_exposureMs{10.0};
- bool m_autoPreview{false};
-
- QString m_lastError;
- State m_state{State::Starting};
-
- 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};
- bool m_loggedOversizedFrame{false};
- bool m_loggedUnsupportedFormat{false};
- int m_sourceRoiX{0};
- int m_sourceRoiY{0};
- int m_sourceRoiWidth{0};
- int m_sourceRoiHeight{0};
-
- quint64 m_frameIndex{0};
- quint64 m_pendingFrameAdvance{0};
- int m_nextWriteSlot{0};
- FrameDeliveryMode m_frameDeliveryMode{FrameDeliveryMode::PreviewLatest};
- };
-
- // Publish the initial hello event to connected clients
- void AgentRuntime::publishHello()
- {
- emit eventReady(makeEvent(agent::kEventHello));
- }
-
- // Replay encoded cfg properties into the agent MMCore instance
- bool AgentRuntime::applyProperties(const QStringList& encodedProperties,
- const std::string& label,
- const QString& propertyKind,
- QString* errorMessage)
- {
- for (const QString& encodedProperty : encodedProperties)
- {
- if (encodedProperty.isEmpty())
- {
- continue;
- }
-
- QJsonParseError parseError;
- const QJsonDocument doc = QJsonDocument::fromJson(encodedProperty.toUtf8(), &parseError);
- if (parseError.error != QJsonParseError::NoError || !doc.isObject())
- {
- if (errorMessage)
- {
- *errorMessage = QStringLiteral("Invalid %1 payload for '%2'")
- .arg(propertyKind, m_cameraId);
- }
- return false;
- }
-
- const QJsonObject property = doc.object();
- const QString propertyName = property.value(QStringLiteral("name")).toString().trimmed();
- const QString propertyValue = property.value(QStringLiteral("value")).toString();
- if (propertyName.isEmpty())
- {
- if (errorMessage)
- {
- *errorMessage = QStringLiteral("Missing %1 name for '%2'")
- .arg(propertyKind, m_cameraId);
- }
- return false;
- }
-
- try
- {
- m_mmcore->setProperty(label.c_str(),
- propertyName.toStdString().c_str(),
- propertyValue.toStdString().c_str());
- }
- catch (const CMMError& mmError)
- {
- if (errorMessage)
- {
- *errorMessage = QString("Failed to apply %1 '%2': %3")
- .arg(propertyKind, propertyName, QString::fromStdString(mmError.getMsg()));
- }
- return false;
- }
- }
-
- return true;
- }
-
- // Dispatch one control request and emit a matching response
- void AgentRuntime::handleRequest(quint64 connectionId,
- quint64 requestId,
- const QString& type,
- const QJsonObject& message)
- {
- if (requestId == 0 || type.isEmpty())
- {
- return;
- }
-
- if (!m_mmcore && type != agent::kCommandShutdown)
- {
- emit responseReady(connectionId,
- makeErrorResponse(type,
- requestId,
- QStringLiteral("Agent runtime not initialized")));
- return;
- }
-
- if (type == agent::kCommandStartPreview)
- {
- QString error;
- const bool ok = startPreviewInternal(&error);
- emit responseReady(connectionId,
- ok
- ? makeResponse(type, requestId, true)
- : makeErrorResponse(type, requestId, error));
- return;
- }
-
- if (type == agent::kCommandStopPreview)
- {
- QString error;
- const bool ok = stopPreviewInternal(&error);
- emit responseReady(connectionId,
- ok
- ? makeResponse(type, requestId, true)
- : makeErrorResponse(type, requestId, error));
- 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);
- bool ok = exposureMs > 0.0;
- QString error;
- if (!ok)
- {
- error = QStringLiteral("Invalid exposure value");
- }
- else
- {
- try
- {
- m_mmcore->setExposure(exposureMs);
- m_exposureMs = m_mmcore->getExposure();
- }
- catch (const CMMError& mmError)
- {
- ok = false;
- error = QString::fromStdString(mmError.getMsg());
- }
- }
-
- if (ok)
- {
- QJsonObject response = makeResponse(type, requestId, true);
- response.insert(QStringLiteral("exposureMs"), m_exposureMs);
- emit responseReady(connectionId, response);
- }
- else
- {
- emit responseReady(connectionId, makeErrorResponse(type, requestId, error));
- }
- return;
- }
-
- if (type == agent::kCommandListProperties)
- {
- QJsonArray properties;
- QString error;
- bool ok = true;
- try
- {
- const auto names =
- m_mmcore->getDevicePropertyNames(m_cameraId.toStdString().c_str());
- for (const auto& name : names)
- {
- properties.append(QString::fromStdString(name));
- }
- }
- catch (const CMMError& mmError)
- {
- ok = false;
- error = QString::fromStdString(mmError.getMsg());
- }
-
- if (ok)
- {
- QJsonObject response = makeResponse(type, requestId, true);
- response.insert(QStringLiteral("properties"), properties);
- emit responseReady(connectionId, response);
- }
- else
- {
- emit responseReady(connectionId, makeErrorResponse(type, requestId, error));
- }
- return;
- }
-
- 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;
- double upperLimit = 0.0;
- QString error;
- bool ok = true;
-
- try
- {
- value = QString::fromStdString(
- fromCache
- ? m_mmcore->getPropertyFromCache(camera.c_str(), property.c_str())
- : m_mmcore->getProperty(camera.c_str(), property.c_str()));
-
- try
- {
- switch (m_mmcore->getPropertyType(camera.c_str(), property.c_str()))
- {
- case MM::String:
- propertyType = QStringLiteral("String");
- break;
- case MM::Float:
- propertyType = QStringLiteral("Float");
- break;
- case MM::Integer:
- propertyType = QStringLiteral("Integer");
- break;
- default:
- propertyType = QStringLiteral("Unknown");
- break;
- }
- }
- catch (const CMMError&)
- {
- }
-
- try
- {
- 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&)
- {
- }
-
- try
- {
- const auto values =
- m_mmcore->getAllowedPropertyValues(camera.c_str(), property.c_str());
- for (const auto& allowedValue : values)
- {
- allowedValues.append(QString::fromStdString(allowedValue));
- }
- }
- catch (const CMMError&)
- {
- }
-
- try
- {
- hasLimits = m_mmcore->hasPropertyLimits(camera.c_str(), property.c_str());
- if (hasLimits)
- {
- lowerLimit = m_mmcore->getPropertyLowerLimit(camera.c_str(), property.c_str());
- upperLimit = m_mmcore->getPropertyUpperLimit(camera.c_str(), property.c_str());
- }
- }
- catch (const CMMError&)
- {
- hasLimits = false;
- }
- }
- catch (const CMMError& mmError)
- {
- ok = false;
- error = QString::fromStdString(mmError.getMsg());
- }
-
- if (ok)
- {
- QJsonObject response = makeResponse(type, requestId, true);
- 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);
- response.insert(QStringLiteral("upperLimit"), upperLimit);
- emit responseReady(connectionId, response);
- }
- else
- {
- emit responseReady(connectionId, makeErrorResponse(type, requestId, error));
- }
- return;
- }
-
- if (type == agent::kCommandSetProperty)
- {
- const QString name = message.value(QStringLiteral("name")).toString();
- const QString value = message.value(QStringLiteral("value")).toString();
- QString error;
- bool ok = true;
- try
- {
- m_mmcore->setProperty(m_cameraId.toStdString().c_str(),
- name.toStdString().c_str(),
- value.toStdString().c_str());
- m_mmcore->waitForDevice(m_cameraId.toStdString().c_str());
- }
- catch (const CMMError& mmError)
- {
- ok = false;
- error = QString::fromStdString(mmError.getMsg());
- }
-
- emit responseReady(connectionId,
- ok
- ? makeResponse(type, requestId, true)
- : makeErrorResponse(type, requestId, error));
- return;
- }
-
- if (type == agent::kCommandSetRoi)
- {
- const int x = message.value(QStringLiteral("x")).toInt(0);
- const int y = message.value(QStringLiteral("y")).toInt(0);
- const int width = message.value(QStringLiteral("width")).toInt(0);
- const int height = message.value(QStringLiteral("height")).toInt(0);
- QString error;
- bool ok = true;
- try
- {
- m_mmcore->setROI(m_cameraId.toStdString().c_str(), x, y, width, height);
- m_mmcore->waitForDevice(m_cameraId.toStdString().c_str());
- refreshSourceRoi();
- }
- catch (const CMMError& mmError)
- {
- ok = false;
- error = QString::fromStdString(mmError.getMsg());
- }
-
- emit responseReady(connectionId,
- ok
- ? makeResponse(type, requestId, true)
- : makeErrorResponse(type, requestId, error));
- return;
- }
-
- if (type == agent::kCommandClearRoi)
- {
- QString error;
- bool ok = true;
- try
- {
- m_mmcore->setCameraDevice(m_cameraId.toStdString().c_str());
- m_mmcore->clearROI();
- m_mmcore->waitForDevice(m_cameraId.toStdString().c_str());
- refreshSourceRoi();
- }
- catch (const CMMError& mmError)
- {
- ok = false;
- error = QString::fromStdString(mmError.getMsg());
- }
-
- emit responseReady(connectionId,
- ok
- ? makeResponse(type, requestId, true)
- : makeErrorResponse(type, requestId, error));
- return;
- }
-
- if (type == agent::kCommandGetRoi)
- {
- int x = 0;
- int y = 0;
- int width = 0;
- int height = 0;
- QString error;
- bool ok = true;
- try
- {
- m_mmcore->getROI(m_cameraId.toStdString().c_str(), x, y, width, height);
- }
- catch (const CMMError& mmError)
- {
- ok = false;
- error = QString::fromStdString(mmError.getMsg());
- }
-
- if (ok)
- {
- QJsonObject response = makeResponse(type, requestId, true);
- response.insert(QStringLiteral("x"), x);
- response.insert(QStringLiteral("y"), y);
- response.insert(QStringLiteral("width"), width);
- response.insert(QStringLiteral("height"), height);
- emit responseReady(connectionId, response);
- }
- else
- {
- emit responseReady(connectionId, makeErrorResponse(type, requestId, error));
- }
- return;
- }
-
- if (type == agent::kCommandCaptureEvent)
- {
- QString error;
- quint64 frameIndex = 0;
- const bool ok = captureEventFrameInternal(frameIndex, &error);
- if (ok)
- {
- QJsonObject response = makeResponse(type, requestId, true);
- response.insert(QStringLiteral("frameIndex"), agent::encodeUInt64(frameIndex));
- emit responseReady(connectionId, response);
- }
- else
- {
- emit responseReady(connectionId, makeErrorResponse(type, requestId, error));
- }
- return;
- }
-
- if (type == agent::kCommandShutdown)
- {
- stopPreviewInternal(nullptr);
- setState(State::ShuttingDown);
- emit responseReady(connectionId, makeResponse(type, requestId, true));
- QTimer::singleShot(50, this, [this]() { emit shutdownRequested(); });
- return;
- }
-
- emit responseReady(connectionId,
- makeErrorResponse(type,
- requestId,
- QStringLiteral("Unknown control command")));
- }
-
- // Stop preview before the runtime thread exits
- void AgentRuntime::stopForExit()
- {
- stopPreviewInternal(nullptr);
- setState(State::ShuttingDown);
- }
-
- // Check whether the runtime is currently previewing
- bool AgentRuntime::previewRunning() const
- {
- return m_state == State::Previewing;
- }
-
- // Update runtime state and publish state changes
- void AgentRuntime::setState(State state, const QString& error)
- {
- const bool changed = (m_state != state);
- m_state = state;
- if (!error.isEmpty())
- {
- m_lastError = error;
- }
- if (changed && (state == State::Previewing
- || state == State::Idle
- || state == State::Error
- || state == State::ShuttingDown))
- {
- emitPreviewStateEvent();
- }
- if (!error.isEmpty())
- {
- emitAgentErrorEvent(error);
- }
- }
-
- // Build a protocol response envelope
- QJsonObject AgentRuntime::makeResponse(const QString& type, quint64 requestId, bool ok) const
- {
- QJsonObject response =
- agent::makeEnvelope(agent::kMessageKindResponse, type, requestId);
- response.insert(QStringLiteral("ok"), ok);
- return response;
- }
-
- // Build a protocol error response envelope
- QJsonObject AgentRuntime::makeErrorResponse(const QString& type,
- quint64 requestId,
- const QString& error) const
- {
- QJsonObject response = makeResponse(type, requestId, false);
- response.insert(QStringLiteral("error"), error);
- return response;
- }
-
- // Build a protocol event envelope
- QJsonObject AgentRuntime::makeEvent(const QString& type) const
- {
- return agent::makeEnvelope(agent::kMessageKindEvent, type);
- }
-
- // Publish current preview state to clients
- void AgentRuntime::emitPreviewStateEvent()
- {
- QJsonObject event = makeEvent(agent::kEventPreviewState);
- event.insert(QStringLiteral("running"), previewRunning());
- emit eventReady(event);
- }
-
- // Publish an agent error event to clients
- void AgentRuntime::emitAgentErrorEvent(const QString& error)
- {
- QJsonObject event = makeEvent(agent::kEventAgentError);
- event.insert(QStringLiteral("error"), error);
- emit eventReady(event);
- }
-
- // Publish the newest shared memory frame index
- void AgentRuntime::emitFrameAvailableEvent(quint64 frameIndex)
- {
- QJsonObject event = makeEvent(agent::kEventFrameAvailable);
- event.insert(QStringLiteral("frameIndex"), agent::encodeUInt64(frameIndex));
- emit eventReady(event);
- }
-
- // Start continuous acquisition and polling
- bool AgentRuntime::startPreviewInternal(QString* errorMessage)
- {
- if (!m_mmcore)
- {
- if (errorMessage)
- {
- *errorMessage = QStringLiteral("MMCore not available");
- }
- return false;
- }
- if (m_state == State::ShuttingDown || m_state == State::Error)
- {
- if (errorMessage)
- {
- *errorMessage = QStringLiteral("Agent is not in a runnable state");
- }
- return false;
- }
- if (previewRunning())
- {
- return true;
- }
-
- try
- {
- while (m_mmcore->getRemainingImageCount() > 0)
- {
- 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();
- }
- setState(State::Previewing);
- return true;
- }
- catch (const CMMError& error)
- {
- if (errorMessage)
- {
- *errorMessage = QString::fromStdString(error.getMsg());
- }
- return false;
- }
- }
-
- // Stop continuous acquisition and polling
- bool AgentRuntime::stopPreviewInternal(QString* errorMessage)
- {
- if (!m_mmcore)
- {
- if (errorMessage)
- {
- *errorMessage = QStringLiteral("MMCore not available");
- }
- return false;
- }
-
- try
- {
- if (m_mmcore->isSequenceRunning())
- {
- m_mmcore->stopSequenceAcquisition();
- }
- if (m_timer && m_timer->isActive())
- {
- 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);
- }
- return true;
- }
- catch (const CMMError& error)
- {
- if (errorMessage)
- {
- *errorMessage = QString::fromStdString(error.getMsg());
- }
- return false;
- }
- }
-
- // Capture one frame for recording or API requests
- bool AgentRuntime::captureEventFrameInternal(quint64& frameIndex, QString* errorMessage)
- {
- frameIndex = 0;
- try
- {
- const void* pixels = nullptr;
- if (previewRunning())
- {
- QElapsedTimer waitTimer;
- waitTimer.start();
- while (m_mmcore->getRemainingImageCount() <= 0 && waitTimer.elapsed() < 2000)
- {
- QThread::msleep(1);
- }
- if (m_mmcore->getRemainingImageCount() > 0)
- {
- pixels = m_mmcore->popNextImage();
- }
- else if (errorMessage)
- {
- *errorMessage = QStringLiteral("No frame available from running sequence");
- }
- }
- else
- {
- m_mmcore->snapImage();
- pixels = m_mmcore->getImage();
- }
-
- if (!pixels)
- {
- if (errorMessage && errorMessage->isEmpty())
- {
- *errorMessage = QStringLiteral("Empty image buffer");
- }
- return false;
- }
-
- const unsigned width = m_mmcore->getImageWidth();
- const unsigned height = m_mmcore->getImageHeight();
- const unsigned bytesPerPixel = m_mmcore->getBytesPerPixel();
- if (!ensureFrameLayout(width, height, bytesPerPixel))
- {
- if (errorMessage)
- {
- *errorMessage = QStringLiteral("Unsupported frame format");
- }
- return false;
- }
-
- const quint64 frameAdvance = m_pendingFrameAdvance + 1;
- m_pendingFrameAdvance = 0;
- if (!writeFrameToSharedMemory(pixels, frameAdvance, &frameIndex))
- {
- if (errorMessage)
- {
- *errorMessage = QStringLiteral("Shared memory unavailable");
- }
- return false;
- }
-
- if (m_frameDeliveryMode == FrameDeliveryMode::PreviewLatest)
- {
- m_deliveryTimer.restart();
- }
-
- emitFrameAvailableEvent(frameIndex);
- return true;
- }
- catch (const CMMError& error)
- {
- if (errorMessage)
- {
- *errorMessage = QString::fromStdString(error.getMsg());
- }
- return false;
- }
- }
-
- // Copy one camera frame into the shared memory ring buffer
- bool AgentRuntime::writeFrameToSharedMemory(const void* pixels,
- quint64 frameAdvance,
- quint64* frameIndexOut)
- {
- if (!m_shm || !m_shm->isAttached())
- {
- return false;
- }
- uchar* base = static_cast(m_shm->data());
- if (!base)
- {
- return false;
- }
-
- 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_frameIndex = nextFrameIndex;
- if (frameIndexOut)
- {
- *frameIndexOut = m_frameIndex;
- }
- return false;
- }
-
- auto* control = reinterpret_cast(base);
- m_nextWriteSlot = (slotIndex + 1) % kSharedFrameNumSlots;
- SharedFrameHeader header{};
- header.state = 1;
- header.width = m_frameLayout.width;
- header.height = m_frameLayout.height;
- header.stride = m_frameLayout.stride;
- header.pixelFormat = static_cast(m_frameLayout.format);
- header.bitsPerSample = m_frameLayout.bitDepth;
- header.channels = m_frameLayout.channels;
- header.frameIndex = nextFrameIndex;
- header.timestampNs =
- static_cast(QDateTime::currentMSecsSinceEpoch()) * 1000000ull;
- setSharedFrameSourceRoi(header,
- m_sourceRoiX,
- m_sourceRoiY,
- m_sourceRoiWidth,
- m_sourceRoiHeight);
- 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));
-
- 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;
- }
-
- return true;
- }
-
- // Drain camera frames into shared memory and publish the newest index
- void AgentRuntime::pollAndWrite()
- {
- if (!m_mmcore)
- {
- return;
- }
-
- try
- {
- long remaining = m_mmcore->getRemainingImageCount();
-
- if (remaining <= 0)
- {
- return;
- }
-
- quint64 newestFrameIndex = 0;
- 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)
- {
- 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;
- }
- }
- }
- }
- else
- {
- while (remaining-- > 0)
- {
- const void* pixels = m_mmcore->popNextImage();
- if (!pixels)
- {
- break;
- }
- ++acquiredFrameCount;
-
- if (!m_frameLayoutValid)
- {
- break;
- }
-
- quint64 writtenFrameIndex = 0;
- if (!writeFrameToSharedMemory(pixels, 1, &writtenFrameIndex))
- {
- break;
- }
- newestFrameIndex = writtenFrameIndex;
- }
- }
-
- if (newestFrameIndex != 0)
- {
- emitFrameAvailableEvent(newestFrameIndex);
- }
- updatePollingInterval(acquiredFrameCount);
- }
- catch (const CMMError& error)
- {
- const QString message =
- QStringLiteral("Agent capture error: %1")
- .arg(QString::fromStdString(error.getMsg()));
- setState(State::Error, message);
- }
- }
-
- // 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)
- {
- const auto logOversized = [this](quint64 byteCount)
- {
- if (!m_loggedOversizedFrame)
- {
- qWarning().noquote()
- << QString("[Agent %1] Frame payload (%2 bytes) exceeds shared memory slot capacity (%3 bytes)")
- .arg(m_cameraId)
- .arg(static_cast(byteCount))
- .arg(kSharedFrameMaxBytes);
- m_loggedOversizedFrame = true;
- }
- };
-
- if (bytesPerPixel != 1 && bytesPerPixel != 2)
- {
- if (!m_loggedUnsupportedFormat)
- {
- qWarning().noquote()
- << QString("[Agent %1] Unsupported bytes-per-pixel (%2). Only Mono8/Mono16 are supported.")
- .arg(m_cameraId)
- .arg(bytesPerPixel);
- m_loggedUnsupportedFormat = true;
- }
- return false;
- }
- m_loggedUnsupportedFormat = false;
-
- const bool geometryChanged =
- !m_frameLayoutValid
- || m_frameLayout.width != width
- || m_frameLayout.height != height
- || m_frameLayout.bytesPerPixel != bytesPerPixel;
-
- if (!geometryChanged)
- {
- if (m_frameLayout.byteCount == 0
- || m_frameLayout.byteCount > static_cast(kSharedFrameMaxBytes))
- {
- logOversized(m_frameLayout.byteCount);
- return false;
- }
- if (m_sourceRoiWidth <= 0 || m_sourceRoiHeight <= 0)
- {
- refreshSourceRoi();
- }
- m_loggedOversizedFrame = false;
- return true;
- }
-
- m_frameLayoutValid = false;
-
- FrameLayout updated;
- updated.width = width;
- updated.height = height;
- updated.bytesPerPixel = bytesPerPixel;
- updated.format =
- (bytesPerPixel == 2) ? SharedPixelFormat::Mono16 : SharedPixelFormat::Mono8;
- updated.channels = 1;
- const quint64 stride = static_cast(width) * bytesPerPixel;
- if (stride == 0 || stride > (std::numeric_limits::max)())
- {
- logOversized(stride);
- return false;
- }
- updated.stride = static_cast(stride);
- updated.byteCount = computeMaxFrameBytes(width, height, updated.format);
-
- if (updated.byteCount == 0
- || updated.byteCount > static_cast(kSharedFrameMaxBytes))
- {
- logOversized(updated.byteCount);
- return false;
- }
-
- quint16 bitDepth =
- (updated.format == SharedPixelFormat::Mono16) ? 16 : 8;
- try
- {
- bitDepth = normalizedSharedBitDepth(
- updated.format,
- static_cast(m_mmcore->getImageBitDepth()));
- }
- catch (const CMMError&)
- {
- bitDepth =
- (updated.format == SharedPixelFormat::Mono16) ? 16 : 8;
- }
- updated.bitDepth = normalizedSharedBitDepth(updated.format, bitDepth);
-
- m_frameLayout = updated;
- m_frameLayoutValid = true;
- refreshSourceRoi();
- m_loggedOversizedFrame = false;
- return true;
- }
-
- // Read the active camera ROI used by following frame metadata
- void AgentRuntime::refreshSourceRoi()
- {
- int x = 0;
- int y = 0;
- int width = 0;
- int height = 0;
- try
- {
- m_mmcore->getROI(m_cameraId.toStdString().c_str(), x, y, width, height);
- }
- catch (const CMMError&)
- {
- width = static_cast(m_frameLayout.width);
- height = static_cast(m_frameLayout.height);
- }
-
- if (width <= 0 || height <= 0)
- {
- width = static_cast(m_frameLayout.width);
- height = static_cast(m_frameLayout.height);
- }
-
- m_sourceRoiX = x;
- m_sourceRoiY = y;
- m_sourceRoiWidth = width;
- m_sourceRoiHeight = height;
- }
-
- class Agent final : public QObject
- {
- Q_OBJECT
-
- public:
- // Create the control server wrapper for one camera agent
- Agent(QString cameraId,
- QString adapter,
- QString device,
- QString shmKey,
- QStringList preInitProperties,
- QStringList properties,
- double exposureMs,
- bool autoPreview,
- QObject* parent = nullptr)
- : QObject(parent)
- , m_cameraId(std::move(cameraId))
- , m_adapter(std::move(adapter))
- , m_device(std::move(device))
- , m_shmKey(std::move(shmKey))
- , m_serverName(agent::controlServerName(m_cameraId))
- , m_preInitProperties(std::move(preInitProperties))
- , m_properties(std::move(properties))
- , m_exposureMs(exposureMs)
- , m_autoPreview(autoPreview)
- {
- qRegisterMetaType("QJsonObject");
- qRegisterMetaType("quint64");
- }
-
- // Stop the runtime thread before destruction
- ~Agent() override
- {
- stopRuntime();
- }
-
- bool start();
-
- private slots:
- void onNewControlConnection();
- void onRuntimeResponse(quint64 connectionId, const QJsonObject& response);
- void broadcastEvent(const QJsonObject& event);
- void onConnectionClosed(quint64 connectionId);
-
- private:
- void stopRuntime();
-
- QString m_cameraId;
- QString m_adapter;
- QString m_device;
- QString m_shmKey;
- QString m_serverName;
- QStringList m_preInitProperties;
- QStringList m_properties;
- double m_exposureMs{0.0};
- bool m_autoPreview{false};
-
- std::unique_ptr m_ctrlServer;
- QThread m_runtimeThread;
- AgentRuntime* m_runtime{nullptr};
- quint64 m_nextConnectionId{1};
- QHash m_connections;
- };
-
- // Start the runtime thread and local control server
- bool Agent::start()
- {
- m_runtime = new AgentRuntime(m_cameraId,
- m_adapter,
- m_device,
- m_shmKey,
- m_preInitProperties,
- m_properties,
- m_exposureMs,
- m_autoPreview);
- m_runtime->moveToThread(&m_runtimeThread);
- connect(&m_runtimeThread, &QThread::finished,
- m_runtime, &QObject::deleteLater);
- connect(m_runtime, &AgentRuntime::responseReady,
- this, &Agent::onRuntimeResponse);
- connect(m_runtime, &AgentRuntime::eventReady,
- this, &Agent::broadcastEvent);
- connect(m_runtime, &AgentRuntime::shutdownRequested, this,
- []() { QCoreApplication::quit(); });
-
- m_runtimeThread.start();
-
- bool initialized = false;
- QString initError;
- QMetaObject::invokeMethod(
- m_runtime,
- [this, &initialized, &initError]()
- {
- initialized = m_runtime->initializeRuntime();
- initError = m_runtime->lastError();
- },
- Qt::BlockingQueuedConnection);
-
- if (!initialized)
- {
- qCritical().noquote()
- << QString("Agent init failed for '%1': %2")
- .arg(m_cameraId, initError);
- stopRuntime();
- return false;
- }
-
- QLocalServer::removeServer(m_serverName);
- m_ctrlServer = std::make_unique(this);
- connect(m_ctrlServer.get(), &QLocalServer::newConnection,
- this, &Agent::onNewControlConnection);
- if (!m_ctrlServer->listen(m_serverName))
- {
- qCritical().noquote()
- << QString("Agent control server failed to listen on %1")
- .arg(m_serverName);
- stopRuntime();
- return false;
- }
-
- qInfo().noquote()
- << QString("Agent control server listening on %1").arg(m_serverName);
- return true;
- }
-
- // Accept pending local control connections
- void Agent::onNewControlConnection()
- {
- while (m_ctrlServer && m_ctrlServer->hasPendingConnections())
- {
- QLocalSocket* socket = m_ctrlServer->nextPendingConnection();
- if (!socket)
- {
- continue;
- }
-
- const quint64 connectionId = m_nextConnectionId++;
- auto* connection = new ControlConnection(connectionId, socket, this);
- m_connections.insert(connectionId, connection);
-
- connect(connection, &ControlConnection::requestReceived,
- m_runtime, &AgentRuntime::handleRequest,
- Qt::QueuedConnection);
- connect(connection, &ControlConnection::connectionClosed,
- this, &Agent::onConnectionClosed);
-
- QMetaObject::invokeMethod(m_runtime,
- &AgentRuntime::publishHello,
- Qt::QueuedConnection);
- }
- }
-
- // Route one runtime response back to its connection
- void Agent::onRuntimeResponse(quint64 connectionId, const QJsonObject& response)
- {
- auto it = m_connections.find(connectionId);
- if (it == m_connections.end() || !it.value())
- {
- return;
- }
- it.value()->sendMessage(response);
- }
-
- // Broadcast one runtime event to all clients
- void Agent::broadcastEvent(const QJsonObject& event)
- {
- for (auto it = m_connections.begin(); it != m_connections.end(); ++it)
- {
- if (it.value())
- {
- it.value()->sendMessage(event);
- }
- }
- }
-
- // Remove a closed control connection
- void Agent::onConnectionClosed(quint64 connectionId)
- {
- m_connections.remove(connectionId);
- }
-
- // Stop the runtime worker thread cleanly
- void Agent::stopRuntime()
- {
- if (!m_runtime)
- {
- return;
- }
-
- QMetaObject::invokeMethod(m_runtime,
- &AgentRuntime::stopForExit,
- Qt::BlockingQueuedConnection);
- m_runtimeThread.quit();
- m_runtimeThread.wait();
- m_runtime = nullptr;
- }
-} // namespace scopeone::core::internal
-
-// Launch one camera agent process from command line arguments
-int main(int argc, char* argv[])
-{
- QCoreApplication app(argc, argv);
-
- QCommandLineParser parser;
- parser.setApplicationDescription("ScopeOne Camera Agent");
- parser.addHelpOption();
-
- QCommandLineOption optCamId({QStringLiteral("c"), QStringLiteral("cameraId")},
- QStringLiteral("Camera ID"),
- QStringLiteral("id"));
- QCommandLineOption optAdapter(QStringLiteral("adapter"),
- QStringLiteral("MM adapter"),
- QStringLiteral("adapter"));
- QCommandLineOption optDevice(QStringLiteral("device"),
- QStringLiteral("MM device"),
- QStringLiteral("device"));
- QCommandLineOption optShm(QStringLiteral("shm"),
- QStringLiteral("Shared memory key"),
- QStringLiteral("key"));
- QCommandLineOption optExp(QStringLiteral("exposure"),
- QStringLiteral("Exposure ms"),
- QStringLiteral("ms"));
- QCommandLineOption optPreInit(QStringLiteral("preinit"),
- QStringLiteral("JSON-encoded pre-init property"),
- QStringLiteral("json"));
- QCommandLineOption optProperty(QStringLiteral("property"),
- QStringLiteral("JSON-encoded initialized property"),
- QStringLiteral("json"));
- QCommandLineOption optAuto(QStringLiteral("autoPreview"),
- QStringLiteral("Start preview immediately"));
-
- parser.addOption(optCamId);
- parser.addOption(optAdapter);
- parser.addOption(optDevice);
- parser.addOption(optShm);
- parser.addOption(optExp);
- parser.addOption(optPreInit);
- parser.addOption(optProperty);
- parser.addOption(optAuto);
- parser.process(app);
-
- if (!parser.isSet(optCamId)
- || !parser.isSet(optAdapter)
- || !parser.isSet(optDevice)
- || !parser.isSet(optShm))
- {
- qCritical().noquote()
- << "Missing required arguments: --cameraId, --adapter, --device, --shm";
- return 2;
- }
-
- scopeone::core::internal::Agent agent(parser.value(optCamId),
- parser.value(optAdapter),
- parser.value(optDevice),
- parser.value(optShm),
- parser.values(optPreInit),
- parser.values(optProperty),
- parser.isSet(optExp)
- ? parser.value(optExp).toDouble()
- : 0.0,
- parser.isSet(optAuto));
- if (!agent.start())
- {
- return 2;
- }
-
- return app.exec();
-}
-
-#include "AgentMain.moc"
diff --git a/ScopeOneCore/src/CameraBackend.cpp b/ScopeOneCore/src/CameraBackend.cpp
index 7cce6d0..b7b3877 100644
--- a/ScopeOneCore/src/CameraBackend.cpp
+++ b/ScopeOneCore/src/CameraBackend.cpp
@@ -85,7 +85,7 @@ namespace scopeone::core::internal
return false;
}
- bool CameraBackend::addAgentCamera(const QString&,
+ bool CameraBackend::addDriverHostCamera(const QString&,
const QString&,
const QString&,
const QStringList&,
diff --git a/ScopeOneCore/src/CameraManager.cpp b/ScopeOneCore/src/CameraManager.cpp
index 838d754..46b62d3 100644
--- a/ScopeOneCore/src/CameraManager.cpp
+++ b/ScopeOneCore/src/CameraManager.cpp
@@ -45,7 +45,7 @@ namespace scopeone::core::internal
shutdownNow();
m_backend = kind == CameraBackend::Kind::Native
? createNativeCameraBackend(m_processingFrameGate)
- : createAgentCameraBackend(m_processingFrameGate);
+ : createDriverHostCameraBackend(m_processingFrameGate);
if (!m_backend)
{
return false;
@@ -58,7 +58,6 @@ namespace scopeone::core::internal
{
m_frameSink(frame);
}
- emit newRawFrameReady(frame);
});
// Keeps processing input on the producer thread
connect(m_backend.get(), &CameraBackend::processingFrameReady,
@@ -72,8 +71,8 @@ namespace scopeone::core::internal
this, &CameraManager::frameDeliveryFailed);
connect(m_backend.get(), &CameraBackend::previewStateChanged,
this, &CameraManager::previewStateChanged);
- connect(m_backend.get(), &CameraBackend::agentControlServerListening,
- this, &CameraManager::agentControlServerListening);
+ connect(m_backend.get(), &CameraBackend::driverHostControlServerListening,
+ this, &CameraManager::driverHostControlServerListening);
if (!m_backend->setHighRateFrameDeliveryEnabled(m_highRateFrameDeliveryEnabled)
|| !m_backend->setRecordingFrameDeliveryEnabled(m_recordingFrameDeliveryEnabled))
{
@@ -101,8 +100,8 @@ namespace scopeone::core::internal
return configured;
}
- // Adds one process isolated camera to the agent backend
- bool CameraManager::addAgentCamera(const QString& cameraId,
+ // Add one process isolated camera to the DriverHost backend
+ bool CameraManager::addDriverHostCamera(const QString& cameraId,
const QString& adapter,
const QString& device,
const QStringList& preInitProperties,
@@ -111,8 +110,8 @@ namespace scopeone::core::internal
{
const QString normalizedId = normalizedCameraId(cameraId);
const bool configured = !normalizedId.isEmpty()
- && activateBackend(CameraBackend::Kind::Agent)
- && m_backend->addAgentCamera(normalizedId,
+ && activateBackend(CameraBackend::Kind::DriverHost)
+ && m_backend->addDriverHostCamera(normalizedId,
adapter,
device,
preInitProperties,
@@ -192,10 +191,10 @@ namespace scopeone::core::internal
return m_backend && m_backend->stopPreview();
}
- // Report whether cameras are isolated in agent processes
- bool CameraManager::usesAgentBackend() const
+ // Report whether cameras are isolated in DriverHost processes
+ bool CameraManager::usesDriverHostBackend() const
{
- return m_backend && m_backend->kind() == CameraBackend::Kind::Agent;
+ return m_backend && m_backend->kind() == CameraBackend::Kind::DriverHost;
}
bool CameraManager::startPreviewFor(const QString& cameraId)
diff --git a/ScopeOneCore/src/ClockService.cpp b/ScopeOneCore/src/ClockService.cpp
deleted file mode 100644
index ccd4b82..0000000
--- a/ScopeOneCore/src/ClockService.cpp
+++ /dev/null
@@ -1,18 +0,0 @@
-#include "scopeone/ClockService.h"
-
-#include
-
-namespace scopeone::core
-{
- ClockStamp ClockService::now() const
- {
- const auto ticks = std::chrono::duration_cast(
- std::chrono::steady_clock::now().time_since_epoch()).count();
- ClockStamp stamp;
- stamp.ticks = ticks;
- stamp.hostMonotonicNs = ticks;
- stamp.clockDomain = QStringLiteral("scopeone.host.monotonic");
- stamp.source = QStringLiteral("HostEstimated");
- return stamp;
- }
-}
diff --git a/ScopeOneCore/src/AgentCameraBackend.cpp b/ScopeOneCore/src/DriverHostCameraBackend.cpp
similarity index 82%
rename from ScopeOneCore/src/AgentCameraBackend.cpp
rename to ScopeOneCore/src/DriverHostCameraBackend.cpp
index 0f27673..3c8f2ab 100644
--- a/ScopeOneCore/src/AgentCameraBackend.cpp
+++ b/ScopeOneCore/src/DriverHostCameraBackend.cpp
@@ -1,5 +1,5 @@
#include "internal/CameraBackend.h"
-#include "internal/AgentProtocol.h"
+#include "internal/DriverHostProtocol.h"
#include "scopeone/SharedFrame.h"
#include
@@ -45,10 +45,10 @@ namespace scopeone::core::internal
namespace
{
- constexpr int kAgentControlReadyTimeoutMs = 15000;
+ constexpr int kDriverHostControlReadyTimeoutMs = 15000;
constexpr int kPreviewFrameDeliveryIntervalMs = 16;
- enum class AgentFrameDeliveryMode
+ enum class DriverHostFrameDeliveryMode
{
PreviewLatest,
LatestOnly,
@@ -61,30 +61,30 @@ namespace scopeone::core::internal
return cameraId.trimmed();
}
- const QString& frameDeliveryModeName(AgentFrameDeliveryMode mode)
+ const QString& frameDeliveryModeName(DriverHostFrameDeliveryMode mode)
{
switch (mode)
{
- case AgentFrameDeliveryMode::PreviewLatest:
- return agent::kFrameDeliveryModePreviewLatest;
- case AgentFrameDeliveryMode::LatestOnly:
- return agent::kFrameDeliveryModeLatestOnly;
- case AgentFrameDeliveryMode::AllFrames:
- return agent::kFrameDeliveryModeAllFrames;
+ case DriverHostFrameDeliveryMode::PreviewLatest:
+ return driverhost::kFrameDeliveryModePreviewLatest;
+ case DriverHostFrameDeliveryMode::LatestOnly:
+ return driverhost::kFrameDeliveryModeLatestOnly;
+ case DriverHostFrameDeliveryMode::AllFrames:
+ return driverhost::kFrameDeliveryModeAllFrames;
}
- return agent::kFrameDeliveryModePreviewLatest;
+ return driverhost::kFrameDeliveryModePreviewLatest;
}
- AgentFrameDeliveryMode nonRecordingDeliveryMode(bool highRate)
+ DriverHostFrameDeliveryMode nonRecordingDeliveryMode(bool highRate)
{
return highRate
- ? AgentFrameDeliveryMode::LatestOnly
- : AgentFrameDeliveryMode::PreviewLatest;
+ ? DriverHostFrameDeliveryMode::LatestOnly
+ : DriverHostFrameDeliveryMode::PreviewLatest;
}
} // namespace
- struct AgentControlSession final : QObject
+ struct DriverHostControlSession final : QObject
{
struct PendingRequest
{
@@ -93,10 +93,12 @@ namespace scopeone::core::internal
std::function completion;
};
- explicit AgentControlSession(const QString& cameraId,
- const QString& serverName,
- QObject* parent = nullptr)
+ explicit DriverHostControlSession(const QString& providerId,
+ const QString& cameraId,
+ const QString& serverName,
+ QObject* parent = nullptr)
: QObject(parent)
+ , m_providerId(providerId)
, m_cameraId(cameraId)
, m_serverName(serverName)
{
@@ -208,7 +210,7 @@ namespace scopeone::core::internal
int timeoutMs,
std::function completion)
{
- const QString type = request.value(agent::kMessageTypeField).toString();
+ const QString type = request.value(driverhost::kMessageTypeField).toString();
if (type.isEmpty())
{
return false;
@@ -216,12 +218,12 @@ namespace scopeone::core::internal
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));
+ envelope.insert(driverhost::kEnvelopeKindField, driverhost::kMessageKindRequest);
+ envelope.insert(driverhost::kEnvelopeVersionField, static_cast(driverhost::kProtocolVersion));
+ envelope.insert(driverhost::kEnvelopeRequestIdField, driverhost::encodeUInt64(requestId));
PendingRequest pending;
- pending.encoded = agent::encodeMessage(envelope);
+ pending.encoded = driverhost::encodeMessage(envelope);
pending.completion = std::move(completion);
pending.timer = new QTimer(this);
pending.timer->setSingleShot(true);
@@ -295,30 +297,30 @@ namespace scopeone::core::internal
{
QJsonObject message;
QString error;
- const agent::DecodeResult result =
- agent::tryDecodeMessage(m_readBuffer, message, &error);
- if (result == agent::DecodeResult::Incomplete)
+ const driverhost::DecodeResult result =
+ driverhost::tryDecodeMessage(m_readBuffer, message, &error);
+ if (result == driverhost::DecodeResult::Incomplete)
{
return;
}
- if (result == agent::DecodeResult::Error)
+ if (result == driverhost::DecodeResult::Error)
{
resetWithError(error);
return;
}
- if (message.value(agent::kEnvelopeVersionField).toInt(0)
- != static_cast(agent::kProtocolVersion))
+ if (message.value(driverhost::kEnvelopeVersionField).toInt(0)
+ != static_cast(driverhost::kProtocolVersion))
{
resetWithError(QStringLiteral("Control protocol version mismatch"));
return;
}
- const QString kind = message.value(agent::kEnvelopeKindField).toString();
- if (kind == agent::kMessageKindResponse)
+ const QString kind = message.value(driverhost::kEnvelopeKindField).toString();
+ if (kind == driverhost::kMessageKindResponse)
{
const quint64 requestId =
- agent::decodeUInt64(message.value(agent::kEnvelopeRequestIdField));
+ driverhost::decodeUInt64(message.value(driverhost::kEnvelopeRequestIdField));
if (requestId == 0)
{
continue;
@@ -327,11 +329,21 @@ namespace scopeone::core::internal
continue;
}
- if (kind == agent::kMessageKindEvent)
+ if (kind == driverhost::kMessageKindEvent)
{
- const QString type = message.value(agent::kMessageTypeField).toString();
- if (type == agent::kEventHello)
+ const QString type = message.value(driverhost::kMessageTypeField).toString();
+ if (type == driverhost::kEventHello)
{
+ const QJsonArray capabilities =
+ message.value(driverhost::kCapabilitiesField).toArray();
+ if (message.value(driverhost::kProviderIdField).toString()
+ != m_providerId
+ || message.value(driverhost::kDeviceIdField).toString() != m_cameraId
+ || !capabilities.contains(driverhost::kCapabilityCamera))
+ {
+ resetWithError(QStringLiteral("DriverHost identity mismatch"));
+ return;
+ }
setReady(true);
}
for (const auto& handler : m_eventHandlers)
@@ -392,6 +404,7 @@ namespace scopeone::core::internal
}
}
+ QString m_providerId;
QString m_cameraId;
QString m_serverName;
QLocalSocket m_socket;
@@ -406,12 +419,12 @@ namespace scopeone::core::internal
bool m_closing{false};
};
- class AgentCameraBackend;
+ class DriverHostCameraBackend;
- class AgentFrameWorker final : public QObject
+ class DriverHostFrameWorker final : public QObject
{
public:
- explicit AgentFrameWorker(AgentCameraBackend* owner)
+ explicit DriverHostFrameWorker(DriverHostCameraBackend* owner)
: m_owner(owner)
{
}
@@ -448,36 +461,36 @@ namespace scopeone::core::internal
QList& frames,
quint64& acquiredFrameCount);
- AgentCameraBackend* const m_owner;
+ DriverHostCameraBackend* const m_owner;
QMap> m_readers;
};
- struct AgentCameraSlot
+ struct DriverHostCameraSlot
{
QString cameraId;
QString shmKey;
std::shared_ptr process;
- std::shared_ptr control;
+ std::shared_ptr control;
bool isRunning{false};
double exposureMs{10.0};
bool frameReadQueued{false};
bool frameReadRequested{false};
};
- class AgentCameraBackend final : public CameraBackend
+ class DriverHostCameraBackend final : public CameraBackend
{
public:
- explicit AgentCameraBackend(ProcessingFrameGate& processingFrameGate);
- ~AgentCameraBackend() override;
+ explicit DriverHostCameraBackend(ProcessingFrameGate& processingFrameGate);
+ ~DriverHostCameraBackend() override;
- Kind kind() const override { return Kind::Agent; }
- bool addAgentCamera(const QString& cameraId,
+ Kind kind() const override { return Kind::DriverHost; }
+ bool addDriverHostCamera(const QString& cameraId,
const QString& adapter,
const QString& device,
const QStringList& preInitProperties,
const QStringList& properties,
double exposureMs) override;
- void removeAgentCamera(const QString& cameraId);
+ void removeDriverHostCamera(const QString& cameraId);
bool isPreviewRunning(const QString& cameraId) const override
{
const auto it = m_cameras.constFind(cameraId);
@@ -518,7 +531,7 @@ namespace scopeone::core::internal
}
startedCameraIds.append(cameraId);
}
- qInfo().noquote() << "Agent preview started";
+ qInfo().noquote() << "DriverHost preview started";
return true;
}
@@ -531,7 +544,7 @@ namespace scopeone::core::internal
}
if (ok)
{
- qInfo().noquote() << "Agent preview stopped";
+ qInfo().noquote() << "DriverHost preview stopped";
}
return ok;
}
@@ -544,18 +557,18 @@ namespace scopeone::core::internal
return false;
}
- AgentCameraSlot& slot = *m_cameras[cameraId];
+ DriverHostCameraSlot& slot = *m_cameras[cameraId];
if (slot.isRunning)
{
return true;
}
- AgentFrameWorker* const worker = m_frameWorker;
+ DriverHostFrameWorker* const worker = m_frameWorker;
QMetaObject::invokeMethod(
worker,
[worker, cameraId]() { worker->resetPreviewDeliveryState(cameraId); },
Qt::BlockingQueuedConnection);
QJsonObject req;
- req.insert(agent::kMessageTypeField, agent::kCommandStartPreview);
+ req.insert(driverhost::kMessageTypeField, driverhost::kCommandStartPreview);
QJsonObject resp;
if (!sendControlCommand(cameraId, req, &resp, 1200))
{
@@ -564,7 +577,7 @@ namespace scopeone::core::internal
}
if (!resp.value(QStringLiteral("ok")).toBool(false))
{
- qWarning().noquote() << QString("Agent refused to start preview for '%1'").arg(cameraId);
+ qWarning().noquote() << QString("DriverHost refused to start preview for '%1'").arg(cameraId);
return false;
}
@@ -581,7 +594,7 @@ namespace scopeone::core::internal
{
qWarning().noquote() << QString("Shared memory unavailable for '%1'").arg(cameraId);
QJsonObject stopRequest;
- stopRequest.insert(agent::kMessageTypeField, agent::kCommandStopPreview);
+ stopRequest.insert(driverhost::kMessageTypeField, driverhost::kCommandStopPreview);
sendControlCommand(cameraId, stopRequest, nullptr, 1200);
return false;
}
@@ -604,7 +617,7 @@ namespace scopeone::core::internal
}
QJsonObject req;
- req.insert(agent::kMessageTypeField, agent::kCommandStopPreview);
+ req.insert(driverhost::kMessageTypeField, driverhost::kCommandStopPreview);
QJsonObject resp;
if (!sendControlCommand(cameraId, req, &resp, 1200))
{
@@ -667,19 +680,22 @@ namespace scopeone::core::internal
bool readExposureFor(const QString& cameraId, double& exposureMs) const override
{
- const auto it = m_cameras.constFind(cameraId);
- if (it == m_cameras.constEnd() || !it.value())
+ QJsonObject request;
+ request.insert(driverhost::kMessageTypeField, driverhost::kCommandGetExposure);
+ QJsonObject response;
+ if (!sendControlCommand(cameraId, request, &response, 1200)
+ || !response.value(QStringLiteral("ok")).toBool(false))
{
return false;
}
- exposureMs = it.value()->exposureMs;
+ exposureMs = response.value(QStringLiteral("exposureMs")).toDouble(0.0);
return exposureMs > 0.0;
}
bool writeExposureFor(const QString& cameraId, double exposureMs) override
{
QJsonObject req;
- req.insert(agent::kMessageTypeField, agent::kCommandSetExposure);
+ req.insert(driverhost::kMessageTypeField, driverhost::kCommandSetExposure);
req.insert(QStringLiteral("value"), exposureMs);
QJsonObject resp;
if (!sendControlCommand(cameraId, req, &resp, 1200)
@@ -696,7 +712,7 @@ namespace scopeone::core::internal
{
QStringList out;
QJsonObject req;
- req.insert(agent::kMessageTypeField, agent::kCommandListProperties);
+ req.insert(driverhost::kMessageTypeField, driverhost::kCommandListProperties);
QJsonObject resp;
if (!sendControlCommand(cameraId, req, &resp, 4000))
{
@@ -720,7 +736,7 @@ namespace scopeone::core::internal
CameraPropertyReadback& readback) override
{
QJsonObject req;
- req.insert(agent::kMessageTypeField, agent::kCommandGetProperty);
+ req.insert(driverhost::kMessageTypeField, driverhost::kCommandGetProperty);
req.insert(QStringLiteral("name"), name);
req.insert(QStringLiteral("fromCache"), fromCache);
QJsonObject resp;
@@ -755,7 +771,7 @@ namespace scopeone::core::internal
QString* errorMessage) override
{
QJsonObject req;
- req.insert(agent::kMessageTypeField, agent::kCommandSetProperty);
+ req.insert(driverhost::kMessageTypeField, driverhost::kCommandSetProperty);
req.insert(QStringLiteral("name"), name);
req.insert(QStringLiteral("value"), value);
QJsonObject resp;
@@ -763,7 +779,7 @@ namespace scopeone::core::internal
{
if (errorMessage)
{
- *errorMessage = QStringLiteral("Agent control request failed");
+ *errorMessage = QStringLiteral("DriverHost control request failed");
}
return false;
}
@@ -781,7 +797,7 @@ namespace scopeone::core::internal
bool setROIFor(const QString& cameraId, int x, int y, int width, int height) override
{
QJsonObject req;
- req.insert(agent::kMessageTypeField, agent::kCommandSetRoi);
+ req.insert(driverhost::kMessageTypeField, driverhost::kCommandSetRoi);
req.insert(QStringLiteral("x"), x);
req.insert(QStringLiteral("y"), y);
req.insert(QStringLiteral("width"), width);
@@ -804,7 +820,7 @@ namespace scopeone::core::internal
bool clearROIFor(const QString& cameraId) override
{
QJsonObject req;
- req.insert(agent::kMessageTypeField, agent::kCommandClearRoi);
+ req.insert(driverhost::kMessageTypeField, driverhost::kCommandClearRoi);
QJsonObject resp;
if (!sendControlCommand(cameraId, req, &resp, 1200))
{
@@ -823,7 +839,7 @@ namespace scopeone::core::internal
bool getROIFor(const QString& cameraId, int& x, int& y, int& width, int& height) override
{
QJsonObject req;
- req.insert(agent::kMessageTypeField, agent::kCommandGetRoi);
+ req.insert(driverhost::kMessageTypeField, driverhost::kCommandGetRoi);
QJsonObject resp;
if (!sendControlCommand(cameraId, req, &resp, 1200))
{
@@ -845,9 +861,9 @@ namespace scopeone::core::internal
return true;
}
private:
- friend class AgentFrameWorker;
+ friend class DriverHostFrameWorker;
- bool waitForControlReady(AgentCameraSlot& slot, int timeoutMs);
+ bool waitForControlReady(DriverHostCameraSlot& slot, int timeoutMs);
bool addFrameReader(const QString& cameraId, const QString& shmKey);
void removeFrameReader(const QString& cameraId);
bool prepareFrameReader(const QString& cameraId);
@@ -857,13 +873,13 @@ namespace scopeone::core::internal
bool sendControlCommand(const QString& cameraId,
const QJsonObject& request,
QJsonObject* response,
- int timeoutMs);
+ int timeoutMs) const;
bool sendFrameDeliveryModes(const QStringList& cameraIds,
- AgentFrameDeliveryMode mode);
- void stopAgentProcessesAsync();
+ DriverHostFrameDeliveryMode mode);
+ void stopDriverHostProcessesAsync();
void completeAsyncShutdown();
- QMap> m_cameras;
+ QMap> m_cameras;
std::atomic_bool m_frameDeliveryPaused{false};
bool m_shuttingDown{false};
bool m_shutdownHadRunningCamera{false};
@@ -872,7 +888,7 @@ namespace scopeone::core::internal
QString m_shutdownError;
std::function m_shutdownCompletion;
QThread m_frameThread;
- AgentFrameWorker* m_frameWorker{nullptr};
+ DriverHostFrameWorker* m_frameWorker{nullptr};
};
// Returns payload byte count from a shared frame header
@@ -951,18 +967,18 @@ namespace scopeone::core::internal
std::atomic_ref(stateValue).store(2, std::memory_order_release);
}
- AgentCameraBackend::AgentCameraBackend(ProcessingFrameGate& processingFrameGate)
+ DriverHostCameraBackend::DriverHostCameraBackend(ProcessingFrameGate& processingFrameGate)
: CameraBackend(processingFrameGate)
{
- m_frameThread.setObjectName(QStringLiteral("ScopeOneAgentFrameReader"));
- m_frameWorker = new AgentFrameWorker(this);
+ m_frameThread.setObjectName(QStringLiteral("ScopeOneDriverHostFrameReader"));
+ m_frameWorker = new DriverHostFrameWorker(this);
m_frameWorker->moveToThread(&m_frameThread);
QObject::connect(&m_frameThread, &QThread::finished,
m_frameWorker, &QObject::deleteLater);
m_frameThread.start();
}
- AgentCameraBackend::~AgentCameraBackend()
+ DriverHostCameraBackend::~DriverHostCameraBackend()
{
shutdownNow();
m_frameThread.quit();
@@ -970,7 +986,7 @@ namespace scopeone::core::internal
m_frameWorker = nullptr;
}
- void AgentCameraBackend::shutdownNow()
+ void DriverHostCameraBackend::shutdownNow()
{
if (m_shuttingDown && m_cameras.isEmpty())
{
@@ -987,7 +1003,7 @@ namespace scopeone::core::internal
}
if (m_frameWorker && m_frameThread.isRunning())
{
- AgentFrameWorker* const worker = m_frameWorker;
+ DriverHostFrameWorker* const worker = m_frameWorker;
QMetaObject::invokeMethod(worker,
[worker]() { worker->clear(); },
Qt::BlockingQueuedConnection);
@@ -997,7 +1013,7 @@ namespace scopeone::core::internal
if (slot->control)
{
QJsonObject request;
- request.insert(agent::kMessageTypeField, agent::kCommandShutdown);
+ request.insert(driverhost::kMessageTypeField, driverhost::kCommandShutdown);
sendControlCommand(slot->cameraId, request, nullptr, 800);
slot->control->stop();
}
@@ -1019,8 +1035,8 @@ namespace scopeone::core::internal
}
}
- // Stops agent processes without waiting on the facade thread
- void AgentCameraBackend::shutdown(std::function completion)
+ // Stop DriverHost processes without waiting on the facade thread
+ void DriverHostCameraBackend::shutdown(std::function completion)
{
if (m_shuttingDown)
{
@@ -1038,7 +1054,7 @@ namespace scopeone::core::internal
completion(m_cameras.isEmpty()
? QString()
: m_shutdownError.isEmpty()
- ? QStringLiteral("Camera agent shutdown is already in progress")
+ ? QStringLiteral("Camera DriverHost shutdown is already in progress")
: m_shutdownError);
}
return;
@@ -1056,8 +1072,8 @@ namespace scopeone::core::internal
}
if (m_frameWorker && m_frameThread.isRunning())
{
- AgentFrameWorker* const worker = m_frameWorker;
- const QPointer guardedThis(this);
+ DriverHostFrameWorker* const worker = m_frameWorker;
+ const QPointer guardedThis(this);
const bool queued = QMetaObject::invokeMethod(
worker,
[guardedThis, worker]()
@@ -1071,7 +1087,7 @@ namespace scopeone::core::internal
{
if (guardedThis)
{
- guardedThis->stopAgentProcessesAsync();
+ guardedThis->stopDriverHostProcessesAsync();
}
},
Qt::QueuedConnection);
@@ -1084,11 +1100,11 @@ namespace scopeone::core::internal
}
}
- stopAgentProcessesAsync();
+ stopDriverHostProcessesAsync();
}
- // Requests agent exit and falls back to bounded process termination
- void AgentCameraBackend::stopAgentProcessesAsync()
+ // Request DriverHost exit and fall back to bounded process termination
+ void DriverHostCameraBackend::stopDriverHostProcessesAsync()
{
if (!m_shutdownCompletion)
{
@@ -1131,7 +1147,7 @@ namespace scopeone::core::internal
if (slot->control)
{
QJsonObject request;
- request.insert(agent::kMessageTypeField, agent::kCommandShutdown);
+ request.insert(driverhost::kMessageTypeField, driverhost::kCommandShutdown);
const QPointer guardedProcess(process);
shutdownQueued = slot->control->sendRequest(
request,
@@ -1176,8 +1192,8 @@ namespace scopeone::core::internal
});
}
- // Releases agent state and completes the owning manager callback
- void AgentCameraBackend::completeAsyncShutdown()
+ // Release DriverHost state and complete the owning manager callback
+ void DriverHostCameraBackend::completeAsyncShutdown()
{
if (!m_shutdownCompletion && m_shutdownError.isEmpty())
{
@@ -1193,7 +1209,7 @@ namespace scopeone::core::internal
}
if (m_shutdownKillPollsRemaining <= 0)
{
- m_shutdownError = QStringLiteral("Camera agent process did not stop after termination");
+ m_shutdownError = QStringLiteral("Camera DriverHost process did not stop after termination");
auto completion = std::move(m_shutdownCompletion);
completion(m_shutdownError);
return;
@@ -1222,7 +1238,7 @@ namespace scopeone::core::internal
}
// Registers one camera shared memory reader on the worker thread
- void AgentFrameWorker::addCamera(const QString& cameraId, const QString& shmKey)
+ void DriverHostFrameWorker::addCamera(const QString& cameraId, const QString& shmKey)
{
removeCamera(cameraId);
auto slot = std::make_shared();
@@ -1234,19 +1250,19 @@ namespace scopeone::core::internal
}
// Removes one camera reader and releases its shared memory mapping
- void AgentFrameWorker::removeCamera(const QString& cameraId)
+ void DriverHostFrameWorker::removeCamera(const QString& cameraId)
{
m_readers.remove(cameraId);
}
// Releases every shared memory reader owned by the worker
- void AgentFrameWorker::clear()
+ void DriverHostFrameWorker::clear()
{
m_readers.clear();
}
- // Resets preview coalescing after an agent delivery mode change
- void AgentFrameWorker::resetPreviewDeliveryState(const QString& cameraId)
+ // Reset preview coalescing after a DriverHost delivery mode change
+ void DriverHostFrameWorker::resetPreviewDeliveryState(const QString& cameraId)
{
const auto it = m_readers.find(cameraId);
if (it == m_readers.end())
@@ -1258,14 +1274,14 @@ namespace scopeone::core::internal
}
// Attaches the named camera reader when its mapping is unavailable
- bool AgentFrameWorker::ensureSharedMemory(const QString& cameraId)
+ bool DriverHostFrameWorker::ensureSharedMemory(const QString& cameraId)
{
const auto it = m_readers.find(cameraId);
return it != m_readers.end() && ensureSharedMemory(*it.value());
}
// Validates and attaches one shared memory reader slot
- bool AgentFrameWorker::ensureSharedMemory(ReaderSlot& slot)
+ bool DriverHostFrameWorker::ensureSharedMemory(ReaderSlot& slot)
{
const int expectedSize =
kSharedMemoryControlSize + kSharedFrameNumSlots * kSharedFrameSlotStride;
@@ -1298,20 +1314,20 @@ namespace scopeone::core::internal
return true;
}
- // Waits for one agent control channel to become ready
- bool AgentCameraBackend::waitForControlReady(AgentCameraSlot& slot, int timeoutMs)
+ // Wait for one DriverHost control channel to become ready
+ bool DriverHostCameraBackend::waitForControlReady(DriverHostCameraSlot& slot, int timeoutMs)
{
return slot.control && slot.control->waitForReady(timeoutMs);
}
// Creates one shared memory reader on the dedicated frame thread
- bool AgentCameraBackend::addFrameReader(const QString& cameraId, const QString& shmKey)
+ bool DriverHostCameraBackend::addFrameReader(const QString& cameraId, const QString& shmKey)
{
if (!m_frameWorker || !m_frameThread.isRunning())
{
return false;
}
- AgentFrameWorker* const worker = m_frameWorker;
+ DriverHostFrameWorker* const worker = m_frameWorker;
return QMetaObject::invokeMethod(
worker,
[worker, cameraId, shmKey]() { worker->addCamera(cameraId, shmKey); },
@@ -1319,11 +1335,11 @@ namespace scopeone::core::internal
}
// Removes one shared memory reader on the dedicated frame thread
- void AgentCameraBackend::removeFrameReader(const QString& cameraId)
+ void DriverHostCameraBackend::removeFrameReader(const QString& cameraId)
{
if (m_frameWorker && m_frameThread.isRunning())
{
- AgentFrameWorker* const worker = m_frameWorker;
+ DriverHostFrameWorker* const worker = m_frameWorker;
QMetaObject::invokeMethod(
worker,
[worker, cameraId]() { worker->removeCamera(cameraId); },
@@ -1332,14 +1348,14 @@ namespace scopeone::core::internal
}
// Attaches one reader before preview begins
- bool AgentCameraBackend::prepareFrameReader(const QString& cameraId)
+ bool DriverHostCameraBackend::prepareFrameReader(const QString& cameraId)
{
if (!m_frameWorker || !m_frameThread.isRunning())
{
return false;
}
bool attached = false;
- AgentFrameWorker* const worker = m_frameWorker;
+ DriverHostFrameWorker* const worker = m_frameWorker;
const bool invoked = QMetaObject::invokeMethod(
worker,
[worker, cameraId, &attached]() { attached = worker->ensureSharedMemory(cameraId); },
@@ -1347,11 +1363,11 @@ namespace scopeone::core::internal
return invoked && attached;
}
- // Sends one JSON command to an agent control channel
- bool AgentCameraBackend::sendControlCommand(const QString& cameraId,
+ // Send one JSON command to a DriverHost control channel
+ bool DriverHostCameraBackend::sendControlCommand(const QString& cameraId,
const QJsonObject& request,
QJsonObject* response,
- int timeoutMs)
+ int timeoutMs) const
{
const QString normalizedId = normalizedCameraId(cameraId);
const auto it = m_cameras.constFind(normalizedId);
@@ -1412,12 +1428,12 @@ namespace scopeone::core::internal
return true;
}
- // Switches multiple agent producers within one bounded wait
- bool AgentCameraBackend::sendFrameDeliveryModes(const QStringList& cameraIds,
- AgentFrameDeliveryMode mode)
+ // Switch multiple DriverHost producers within one bounded wait
+ bool DriverHostCameraBackend::sendFrameDeliveryModes(const QStringList& cameraIds,
+ DriverHostFrameDeliveryMode mode)
{
QJsonObject request;
- request.insert(agent::kMessageTypeField, agent::kCommandSetFrameDeliveryMode);
+ request.insert(driverhost::kMessageTypeField, driverhost::kCommandSetFrameDeliveryMode);
request.insert(QStringLiteral("mode"), frameDeliveryModeName(mode));
struct BatchState
@@ -1471,7 +1487,7 @@ namespace scopeone::core::internal
}
// Copies one claimed shared memory slot into an owned frame
- bool AgentFrameWorker::copyFrame(ReaderSlot& slot,
+ bool DriverHostFrameWorker::copyFrame(ReaderSlot& slot,
const SharedFrameHeader& header,
const uchar* pixelData,
QList& frames)
@@ -1492,7 +1508,7 @@ namespace scopeone::core::internal
}
// Reads only the newest ready frame for responsive preview delivery
- bool AgentFrameWorker::readLatestFrame(ReaderSlot& slot,
+ bool DriverHostFrameWorker::readLatestFrame(ReaderSlot& slot,
QList& frames,
quint64& acquiredFrameCount)
{
@@ -1575,7 +1591,7 @@ namespace scopeone::core::internal
}
// Reads every retained shared memory frame in order for recording delivery
- bool AgentFrameWorker::readAllFrames(ReaderSlot& slot,
+ bool DriverHostFrameWorker::readAllFrames(ReaderSlot& slot,
QList& frames,
quint64& acquiredFrameCount)
{
@@ -1649,7 +1665,7 @@ namespace scopeone::core::internal
}
// Reads frames on the worker thread and optionally publishes the batch
- ImageFrame AgentFrameWorker::consumeFrames(const QString& cameraId, bool publishFrames)
+ ImageFrame DriverHostFrameWorker::consumeFrames(const QString& cameraId, bool publishFrames)
{
const auto it = m_readers.find(cameraId);
if (it == m_readers.end())
@@ -1717,21 +1733,21 @@ namespace scopeone::core::internal
return slot.latestFrame;
}
- // Consumes one agent batch and reports completion on the backend thread
- void AgentFrameWorker::consumeFramesAsync(const QString& cameraId)
+ // Consume one DriverHost batch and report completion on the backend thread
+ void DriverHostFrameWorker::consumeFramesAsync(const QString& cameraId)
{
if (!m_owner->m_frameDeliveryPaused.load(std::memory_order_relaxed))
{
consumeFrames(cameraId, true);
}
- AgentCameraBackend* const owner = m_owner;
+ DriverHostCameraBackend* const owner = m_owner;
QMetaObject::invokeMethod(owner,
[owner, cameraId]() { owner->completeFrameRead(cameraId); },
Qt::QueuedConnection);
}
// Reads the newest frame synchronously for event capture
- ImageFrame AgentCameraBackend::consumeFrameNow(const QString& cameraId)
+ ImageFrame DriverHostCameraBackend::consumeFrameNow(const QString& cameraId)
{
if (!m_frameWorker || !m_frameThread.isRunning())
{
@@ -1739,7 +1755,7 @@ namespace scopeone::core::internal
}
ImageFrame frame;
- AgentFrameWorker* const worker = m_frameWorker;
+ DriverHostFrameWorker* const worker = m_frameWorker;
const bool invoked = QMetaObject::invokeMethod(
worker,
[worker, cameraId, &frame]() { frame = worker->consumeFrames(cameraId, false); },
@@ -1748,14 +1764,14 @@ namespace scopeone::core::internal
}
// Coalesces frame notifications into one worker request per camera
- void AgentCameraBackend::scheduleFrameRead(const QString& cameraId)
+ void DriverHostCameraBackend::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();
+ DriverHostCameraSlot& slot = *it.value();
if (!slot.isRunning || m_frameDeliveryPaused.load(std::memory_order_relaxed))
{
return;
@@ -1767,7 +1783,7 @@ namespace scopeone::core::internal
}
slot.frameReadQueued = true;
- AgentFrameWorker* const worker = m_frameWorker;
+ DriverHostFrameWorker* const worker = m_frameWorker;
if (!QMetaObject::invokeMethod(
worker,
[worker, cameraId]() { worker->consumeFramesAsync(cameraId); },
@@ -1778,14 +1794,14 @@ namespace scopeone::core::internal
}
// Schedules one deferred read when notifications arrived during a request
- void AgentCameraBackend::completeFrameRead(const QString& cameraId)
+ void DriverHostCameraBackend::completeFrameRead(const QString& cameraId)
{
const auto it = m_cameras.find(cameraId);
if (it == m_cameras.end() || !it.value())
{
return;
}
- AgentCameraSlot& slot = *it.value();
+ DriverHostCameraSlot& slot = *it.value();
slot.frameReadQueued = false;
if (!slot.frameReadRequested)
{
@@ -1797,7 +1813,7 @@ namespace scopeone::core::internal
}
// Triggers one camera and waits for its event frame
- bool AgentCameraBackend::captureEventFrame(const QString& cameraId,
+ bool DriverHostCameraBackend::captureEventFrame(const QString& cameraId,
ImageFrame& frame,
int timeoutMs)
{
@@ -1815,7 +1831,7 @@ namespace scopeone::core::internal
: 0;
QJsonObject req;
- req.insert(agent::kMessageTypeField, agent::kCommandCaptureEvent);
+ req.insert(driverhost::kMessageTypeField, driverhost::kCommandCaptureEvent);
QJsonObject resp;
const int waitMs = (timeoutMs > 0) ? timeoutMs : 1500;
if (!sendControlCommand(normalizedId, req, &resp, waitMs + 1000))
@@ -1828,7 +1844,7 @@ namespace scopeone::core::internal
}
const quint64 targetFrameIndex =
- agent::decodeUInt64(resp.value(QStringLiteral("frameIndex")));
+ driverhost::decodeUInt64(resp.value(QStringLiteral("frameIndex")));
QElapsedTimer timer;
timer.start();
@@ -1850,7 +1866,7 @@ namespace scopeone::core::internal
}
// Pauses or resumes polling while another workflow controls capture
- void AgentCameraBackend::setFrameDeliveryPaused(bool paused)
+ void DriverHostCameraBackend::setFrameDeliveryPaused(bool paused)
{
if (m_frameDeliveryPaused.exchange(paused, std::memory_order_relaxed) == paused)
{
@@ -1868,15 +1884,15 @@ namespace scopeone::core::internal
}
}
- // Switches agent producers before changing the local recording consumer mode
- bool AgentCameraBackend::setRecordingFrameDeliveryEnabled(bool enabled)
+ // Switch DriverHost producers before changing the local recording consumer mode
+ bool DriverHostCameraBackend::setRecordingFrameDeliveryEnabled(bool enabled)
{
if (enabled && recordingFrameDeliveryEnabled())
{
return true;
}
- const AgentFrameDeliveryMode previewMode =
+ const DriverHostFrameDeliveryMode previewMode =
nonRecordingDeliveryMode(highRateFrameDeliveryEnabled());
if (!enabled)
{
@@ -1885,7 +1901,7 @@ namespace scopeone::core::internal
}
const QStringList cameraIds = m_cameras.keys();
- if (!sendFrameDeliveryModes(cameraIds, AgentFrameDeliveryMode::AllFrames))
+ if (!sendFrameDeliveryModes(cameraIds, DriverHostFrameDeliveryMode::AllFrames))
{
sendFrameDeliveryModes(cameraIds, previewMode);
return false;
@@ -1904,7 +1920,7 @@ namespace scopeone::core::internal
}
// Switches preview producers between display-rate and processing-rate delivery
- bool AgentCameraBackend::setHighRateFrameDeliveryEnabled(bool enabled)
+ bool DriverHostCameraBackend::setHighRateFrameDeliveryEnabled(bool enabled)
{
if (highRateFrameDeliveryEnabled() == enabled)
{
@@ -1916,8 +1932,8 @@ namespace scopeone::core::internal
return CameraBackend::setHighRateFrameDeliveryEnabled(enabled);
}
- const AgentFrameDeliveryMode targetMode = nonRecordingDeliveryMode(enabled);
- const AgentFrameDeliveryMode rollbackMode =
+ const DriverHostFrameDeliveryMode targetMode = nonRecordingDeliveryMode(enabled);
+ const DriverHostFrameDeliveryMode rollbackMode =
nonRecordingDeliveryMode(highRateFrameDeliveryEnabled());
const QStringList cameraIds = m_cameras.keys();
if (!sendFrameDeliveryModes(cameraIds, targetMode))
@@ -1928,8 +1944,8 @@ namespace scopeone::core::internal
return CameraBackend::setHighRateFrameDeliveryEnabled(enabled);
}
- // Starts one camera agent process and connects its control channel
- bool AgentCameraBackend::addAgentCamera(const QString& cameraId,
+ // Start one camera DriverHost process and connect its control channel
+ bool DriverHostCameraBackend::addDriverHostCamera(const QString& cameraId,
const QString& adapter,
const QString& device,
const QStringList& preInitProperties,
@@ -1947,22 +1963,25 @@ namespace scopeone::core::internal
{
return true;
}
- auto slot = std::make_shared();
+ auto slot = std::make_shared();
slot->cameraId = normalizedId;
- slot->shmKey = agent::sharedMemoryKey(normalizedId);
+ slot->shmKey = driverhost::sharedMemoryKey(normalizedId);
slot->process = std::make_shared();
- slot->control = std::make_shared(normalizedId,
- agent::controlServerName(normalizedId));
+ slot->control = std::make_shared(
+ QStringLiteral("micro-manager"),
+ normalizedId,
+ driverhost::controlServerName(normalizedId));
slot->exposureMs = exposureMs;
- const QString agentPath =
- QDir(QCoreApplication::applicationDirPath()).filePath(agent::kExecutableFileName);
- if (!QFileInfo::exists(agentPath))
+ const QString driverHostPath =
+ QDir(QCoreApplication::applicationDirPath()).filePath(driverhost::kExecutableFileName);
+ if (!QFileInfo::exists(driverHostPath))
{
- qWarning().noquote() << QString("Agent executable not found: %1").arg(agentPath);
+ qWarning().noquote() << QString("DriverHost executable not found: %1").arg(driverHostPath);
return false;
}
QStringList args;
- args << "--cameraId" << normalizedId
+ args << "--provider" << QStringLiteral("micro-manager")
+ << "--deviceId" << normalizedId
<< "--adapter" << adapterName
<< "--device" << deviceName
<< "--shm" << slot->shmKey;
@@ -1984,7 +2003,7 @@ namespace scopeone::core::internal
args << "--property" << encodedProperty;
}
}
- slot->process->setProgram(agentPath);
+ slot->process->setProgram(driverHostPath);
slot->process->setArguments(args);
slot->process->setProcessChannelMode(QProcess::MergedChannels);
@@ -1998,7 +2017,7 @@ namespace scopeone::core::internal
for (const QString& line : lines)
{
const QString trimmed = line.trimmed();
- qInfo().noquote() << QString("[Agent %1] %2").arg(normalizedId, trimmed);
+ qInfo().noquote() << QString("[DriverHost %1] %2").arg(normalizedId, trimmed);
}
}
});
@@ -2024,7 +2043,7 @@ namespace scopeone::core::internal
if (wasRecording)
{
emit frameDeliveryFailed(
- QStringLiteral("Camera agent exited for '%1'").arg(normalizedId),
+ QStringLiteral("Camera DriverHost exited for '%1'").arg(normalizedId),
0);
}
});
@@ -2035,7 +2054,7 @@ namespace scopeone::core::internal
{
if (ready)
{
- emit agentControlServerListening(normalizedId, agent::controlServerName(normalizedId));
+ emit driverHostControlServerListening(normalizedId, driverhost::controlServerName(normalizedId));
}
});
slot->control->addEventHandler([this, normalizedId](const QJsonObject& event)
@@ -2045,14 +2064,14 @@ namespace scopeone::core::internal
{
return;
}
- AgentCameraSlot& slot = *it.value();
- const QString type = event.value(agent::kMessageTypeField).toString();
- if (type == agent::kEventFrameAvailable)
+ DriverHostCameraSlot& slot = *it.value();
+ const QString type = event.value(driverhost::kMessageTypeField).toString();
+ if (type == driverhost::kEventFrameAvailable)
{
scheduleFrameRead(normalizedId);
return;
}
- if (type == agent::kEventPreviewState)
+ if (type == driverhost::kEventPreviewState)
{
const bool wasRunning = slot.isRunning;
slot.isRunning = event.value(QStringLiteral("running")).toBool(slot.isRunning);
@@ -2062,9 +2081,9 @@ namespace scopeone::core::internal
}
return;
}
- if (type == agent::kEventAgentError)
+ if (type == driverhost::kEventDriverHostError)
{
- const QString error = QStringLiteral("Agent '%1' error: %2")
+ const QString error = QStringLiteral("DriverHost '%1' error: %2")
.arg(slot.cameraId,
event.value(QStringLiteral("error")).toString());
qWarning().noquote() << error;
@@ -2081,7 +2100,7 @@ namespace scopeone::core::internal
slot->process->start();
if (!slot->process->waitForStarted(3000))
{
- qWarning().noquote() << QString("Failed to start agent for %1").arg(normalizedId);
+ qWarning().noquote() << QString("Failed to start DriverHost for %1").arg(normalizedId);
return false;
}
m_cameras.insert(normalizedId, slot);
@@ -2089,34 +2108,34 @@ namespace scopeone::core::internal
{
slot->control->start();
}
- if (!waitForControlReady(*slot, kAgentControlReadyTimeoutMs))
+ if (!waitForControlReady(*slot, kDriverHostControlReadyTimeoutMs))
{
qWarning().noquote()
- << QString("Agent control session did not become ready for %1 within %2 ms")
+ << QString("DriverHost control session did not become ready for %1 within %2 ms")
.arg(normalizedId)
- .arg(kAgentControlReadyTimeoutMs);
- removeAgentCamera(normalizedId);
+ .arg(kDriverHostControlReadyTimeoutMs);
+ removeDriverHostCamera(normalizedId);
return false;
}
if (!addFrameReader(normalizedId, slot->shmKey))
{
- removeAgentCamera(normalizedId);
+ removeDriverHostCamera(normalizedId);
return false;
}
- const AgentFrameDeliveryMode deliveryMode = recordingFrameDeliveryEnabled()
- ? AgentFrameDeliveryMode::AllFrames
+ const DriverHostFrameDeliveryMode deliveryMode = recordingFrameDeliveryEnabled()
+ ? DriverHostFrameDeliveryMode::AllFrames
: nonRecordingDeliveryMode(
highRateFrameDeliveryEnabled());
if (!sendFrameDeliveryModes(QStringList{normalizedId}, deliveryMode))
{
- removeAgentCamera(normalizedId);
+ removeDriverHostCamera(normalizedId);
return false;
}
return true;
}
- // Stops one camera agent process and releases its resources
- void AgentCameraBackend::removeAgentCamera(const QString& cameraId)
+ // Stop one camera DriverHost process and release its resources
+ void DriverHostCameraBackend::removeDriverHostCamera(const QString& cameraId)
{
const QString normalizedId = normalizedCameraId(cameraId);
if (!m_cameras.contains(normalizedId))
@@ -2130,7 +2149,7 @@ namespace scopeone::core::internal
if (slot->control)
{
QJsonObject request;
- request.insert(agent::kMessageTypeField, agent::kCommandShutdown);
+ request.insert(driverhost::kMessageTypeField, driverhost::kCommandShutdown);
sendControlCommand(normalizedId, request, nullptr, 800);
}
m_cameras.remove(normalizedId);
@@ -2153,9 +2172,9 @@ namespace scopeone::core::internal
}
}
- std::unique_ptr createAgentCameraBackend(
+ std::unique_ptr createDriverHostCameraBackend(
ProcessingFrameGate& processingFrameGate)
{
- return std::make_unique(processingFrameGate);
+ return std::make_unique(processingFrameGate);
}
}
diff --git a/ScopeOneCore/src/DriverHostMain.cpp b/ScopeOneCore/src/DriverHostMain.cpp
new file mode 100644
index 0000000..8150ccd
--- /dev/null
+++ b/ScopeOneCore/src/DriverHostMain.cpp
@@ -0,0 +1,3084 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "MMCore.h"
+#include "internal/DriverHostProtocol.h"
+#include "scopeone/CameraProvider.h"
+#include "scopeone/DriverHostProviderPlugin.h"
+#include "scopeone/HardwareCapabilities.h"
+#include "scopeone/SharedFrame.h"
+
+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;
+ using scopeone::core::computeMaxFrameBytes;
+ using scopeone::core::kSharedFrameHeaderSize;
+ using scopeone::core::kSharedFrameMaxBytes;
+ using scopeone::core::kSharedFrameNumSlots;
+ 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)
+ {
+ if (format == SharedPixelFormat::Mono8)
+ {
+ return 8;
+ }
+ if (format == SharedPixelFormat::Mono16)
+ {
+ return bitsPerSample >= 1 && bitsPerSample <= 16
+ ? static_cast(bitsPerSample)
+ : 16;
+ }
+ return 0;
+ }
+
+ class ControlConnection final : public QObject
+ {
+ Q_OBJECT
+
+ public:
+ // Wrap one local control socket connection
+ ControlConnection(quint64 connectionId, QLocalSocket* socket, QObject* parent = nullptr)
+ : QObject(parent)
+ , m_connectionId(connectionId)
+ , m_socket(socket)
+ {
+ if (!socket)
+ {
+ qFatal("ControlConnection requires QLocalSocket");
+ }
+ m_socket->setParent(this);
+
+ connect(m_socket, &QLocalSocket::readyRead,
+ this, &ControlConnection::onReadyRead);
+ connect(m_socket, &QLocalSocket::disconnected,
+ this, &ControlConnection::onDisconnected);
+ connect(m_socket, &QLocalSocket::errorOccurred, this,
+ [this](QLocalSocket::LocalSocketError socketError)
+ {
+ qWarning().noquote()
+ << QString("DriverHost control socket error (%1) on connection %2")
+ .arg(static_cast(socketError))
+ .arg(m_connectionId);
+ });
+ }
+
+ // Send one encoded protocol message to the client
+ void sendMessage(const QJsonObject& message)
+ {
+ if (m_socket->state() != QLocalSocket::ConnectedState)
+ {
+ return;
+ }
+ m_socket->write(driverhost::encodeMessage(message));
+ }
+
+ signals:
+ void requestReceived(quint64 connectionId,
+ quint64 requestId,
+ const QString& type,
+ const QJsonObject& message);
+ void connectionClosed(quint64 connectionId);
+
+ private slots:
+ // Decode queued socket bytes into control requests
+ void onReadyRead()
+ {
+ m_readBuffer += m_socket->readAll();
+ while (true)
+ {
+ QJsonObject message;
+ QString error;
+ const driverhost::DecodeResult result =
+ driverhost::tryDecodeMessage(m_readBuffer, message, &error);
+ if (result == driverhost::DecodeResult::Incomplete)
+ {
+ return;
+ }
+ if (result == driverhost::DecodeResult::Error)
+ {
+ qWarning().noquote()
+ << QString("DriverHost control protocol error on connection %1: %2")
+ .arg(m_connectionId)
+ .arg(error);
+ m_socket->disconnectFromServer();
+ return;
+ }
+
+ if (message.value(driverhost::kEnvelopeVersionField).toInt(0)
+ != static_cast(driverhost::kProtocolVersion))
+ {
+ qWarning().noquote()
+ << QString("DriverHost control protocol version mismatch on connection %1")
+ .arg(m_connectionId);
+ m_socket->disconnectFromServer();
+ return;
+ }
+
+ if (message.value(driverhost::kEnvelopeKindField).toString()
+ != driverhost::kMessageKindRequest)
+ {
+ qWarning().noquote()
+ << QString("Ignoring non-request control message on connection %1")
+ .arg(m_connectionId);
+ continue;
+ }
+
+ const quint64 requestId =
+ driverhost::decodeUInt64(message.value(driverhost::kEnvelopeRequestIdField));
+ const QString type = message.value(driverhost::kMessageTypeField).toString();
+ if (requestId == 0 || type.isEmpty())
+ {
+ qWarning().noquote()
+ << QString("Ignoring malformed request on connection %1")
+ .arg(m_connectionId);
+ continue;
+ }
+
+ emit requestReceived(m_connectionId, requestId, type, message);
+ }
+ }
+
+ // Notify DriverHost when this connection closes
+ void onDisconnected()
+ {
+ emit connectionClosed(m_connectionId);
+ deleteLater();
+ }
+
+ private:
+ quint64 m_connectionId{0};
+ QLocalSocket* m_socket{nullptr};
+ QByteArray m_readBuffer;
+ };
+
+ class DriverHostRuntime : public QObject
+ {
+ Q_OBJECT
+
+ public:
+ using QObject::QObject;
+ virtual bool initializeRuntime() = 0;
+ virtual QString lastError() const = 0;
+ virtual void publishHello() = 0;
+ virtual void handleRequest(quint64 connectionId,
+ quint64 requestId,
+ const QString& type,
+ const QJsonObject& message) = 0;
+ virtual void stopForExit() = 0;
+
+ signals:
+ void responseReady(quint64 connectionId, const QJsonObject& response);
+ void eventReady(const QJsonObject& event);
+ void shutdownRequested();
+ };
+
+ class MicroManagerDriverRuntime final : public DriverHostRuntime
+ {
+ Q_OBJECT
+
+ public:
+ // Store launch settings for one camera runtime
+ MicroManagerDriverRuntime(QString providerId,
+ QString cameraId,
+ QString adapter,
+ QString device,
+ QString shmKey,
+ QStringList preInitProperties,
+ QStringList properties,
+ double exposureMs,
+ bool autoPreview,
+ QObject* parent = nullptr)
+ : DriverHostRuntime(parent)
+ , m_providerId(std::move(providerId))
+ , m_cameraId(std::move(cameraId))
+ , m_adapter(std::move(adapter))
+ , m_device(std::move(device))
+ , m_shmKey(std::move(shmKey))
+ , m_preInitProperties(std::move(preInitProperties))
+ , m_properties(std::move(properties))
+ , m_exposureMs(exposureMs)
+ , m_autoPreview(autoPreview)
+ {
+ }
+
+ QString lastError() const override
+ {
+ return m_lastError;
+ }
+
+ // Initialize MMCore camera state and shared memory transport
+ bool initializeRuntime() override
+ {
+ m_lastError.clear();
+ setState(State::Starting);
+
+ if (!m_timer)
+ {
+ m_timer = new QTimer(this);
+ m_timer->setTimerType(Qt::PreciseTimer);
+ connect(m_timer, &QTimer::timeout,
+ this, &MicroManagerDriverRuntime::pollAndWrite);
+ }
+
+ try
+ {
+ m_mmcore = std::make_unique();
+ const std::string label = m_cameraId.toStdString();
+ const std::string adapter = m_adapter.toStdString();
+ const std::string device = m_device.toStdString();
+
+ m_mmcore->loadDevice(label.c_str(), adapter.c_str(), device.c_str());
+ QString preInitError;
+ if (!applyProperties(m_preInitProperties, label, QStringLiteral("pre-init property"), &preInitError))
+ {
+ m_lastError = preInitError;
+ setState(State::Error, m_lastError);
+ return false;
+ }
+ m_mmcore->initializeDevice(label.c_str());
+ m_mmcore->setCameraDevice(label.c_str());
+
+ QString propertyError;
+ if (!applyProperties(m_properties, label, QStringLiteral("property"), &propertyError))
+ {
+ m_lastError = propertyError;
+ setState(State::Error, m_lastError);
+ return false;
+ }
+
+ try
+ {
+ m_mmcore->setCircularBufferMemoryFootprint(2048);
+ }
+ catch (const CMMError&)
+ {
+ }
+
+ double finalExposure = 0.0;
+ if (m_exposureMs > 0.0)
+ {
+ m_mmcore->setExposure(m_exposureMs);
+ }
+ m_mmcore->waitForDevice(label.c_str());
+ finalExposure = m_mmcore->getExposure();
+ if (finalExposure <= 0.0)
+ {
+ m_lastError = QStringLiteral("Camera reported an invalid exposure");
+ setState(State::Error, m_lastError);
+ return false;
+ }
+ m_exposureMs = finalExposure;
+ }
+ catch (const CMMError& error)
+ {
+ m_lastError = QString::fromStdString(error.getMsg());
+ setState(State::Error, m_lastError);
+ return false;
+ }
+
+ m_shm = std::make_unique();
+ m_shm->setNativeKey(m_shmKey);
+ const int totalBytes =
+ kSharedMemoryControlSize + kSharedFrameNumSlots * kSharedFrameSlotStride;
+ if (!m_shm->create(totalBytes))
+ {
+ if (m_shm->attach())
+ {
+ m_shm->detach();
+ }
+ if (!m_shm->create(totalBytes))
+ {
+ m_lastError = QStringLiteral("Cannot create shared memory '%1'")
+ .arg(m_shmKey);
+ setState(State::Error, m_lastError);
+ return false;
+ }
+ }
+
+ if (m_shm->lock())
+ {
+ 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();
+ }
+
+ if (m_autoPreview)
+ {
+ QString error;
+ if (!startPreviewInternal(&error))
+ {
+ m_lastError = error;
+ setState(State::Error, m_lastError);
+ return false;
+ }
+ }
+ else
+ {
+ setState(State::Idle);
+ }
+
+ return true;
+ }
+
+ void publishHello() override;
+ void handleRequest(quint64 connectionId,
+ quint64 requestId,
+ const QString& type,
+ const QJsonObject& message) override;
+ void stopForExit() override;
+
+ private:
+ // Runtime state is mirrored to control clients
+ enum class State
+ {
+ Starting,
+ Idle,
+ Previewing,
+ Error,
+ ShuttingDown
+ };
+
+ enum class FrameDeliveryMode
+ {
+ PreviewLatest,
+ LatestOnly,
+ AllFrames
+ };
+
+ // Frame layout describes one shared memory payload shape
+ struct FrameLayout
+ {
+ unsigned width{0};
+ unsigned height{0};
+ unsigned bytesPerPixel{0};
+ SharedPixelFormat format{SharedPixelFormat::Mono8};
+ unsigned stride{0};
+ quint64 byteCount{0};
+ quint16 bitDepth{8};
+ quint16 channels{1};
+ };
+
+ bool previewRunning() const;
+ void setState(State state, const QString& error = QString());
+ QJsonObject makeResponse(const QString& type, quint64 requestId, bool ok) const;
+ QJsonObject makeErrorResponse(const QString& type,
+ quint64 requestId,
+ const QString& error) const;
+ QJsonObject makeEvent(const QString& type) const;
+ bool applyProperties(const QStringList& encodedProperties,
+ const std::string& label,
+ const QString& propertyKind,
+ QString* errorMessage);
+ void emitPreviewStateEvent();
+ void emitDriverHostErrorEvent(const QString& error);
+ void emitFrameAvailableEvent(quint64 frameIndex);
+ bool startPreviewInternal(QString* errorMessage);
+ bool stopPreviewInternal(QString* errorMessage);
+ bool captureEventFrameInternal(quint64& frameIndex, QString* errorMessage);
+ 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();
+
+ QString m_providerId;
+ QString m_cameraId;
+ QString m_adapter;
+ QString m_device;
+ QString m_shmKey;
+ QStringList m_preInitProperties;
+ QStringList m_properties;
+ double m_exposureMs{10.0};
+ bool m_autoPreview{false};
+
+ QString m_lastError;
+ State m_state{State::Starting};
+
+ 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};
+ bool m_loggedOversizedFrame{false};
+ bool m_loggedUnsupportedFormat{false};
+ int m_sourceRoiX{0};
+ int m_sourceRoiY{0};
+ int m_sourceRoiWidth{0};
+ int m_sourceRoiHeight{0};
+
+ quint64 m_frameIndex{0};
+ quint64 m_pendingFrameAdvance{0};
+ int m_nextWriteSlot{0};
+ FrameDeliveryMode m_frameDeliveryMode{FrameDeliveryMode::PreviewLatest};
+ };
+
+ // Publish the initial hello event to connected clients
+ void MicroManagerDriverRuntime::publishHello()
+ {
+ QJsonObject event = makeEvent(driverhost::kEventHello);
+ event.insert(driverhost::kProviderIdField, m_providerId);
+ event.insert(driverhost::kDeviceIdField, m_cameraId);
+ event.insert(driverhost::kDeviceKindField, driverhost::kCapabilityCamera);
+ event.insert(driverhost::kCapabilitiesField,
+ QJsonArray{driverhost::kCapabilityCamera,
+ driverhost::kCapabilityProperties});
+ emit eventReady(event);
+ }
+
+ // Replay encoded cfg properties into the DriverHost MMCore instance
+ bool MicroManagerDriverRuntime::applyProperties(const QStringList& encodedProperties,
+ const std::string& label,
+ const QString& propertyKind,
+ QString* errorMessage)
+ {
+ for (const QString& encodedProperty : encodedProperties)
+ {
+ if (encodedProperty.isEmpty())
+ {
+ continue;
+ }
+
+ QJsonParseError parseError;
+ const QJsonDocument doc = QJsonDocument::fromJson(encodedProperty.toUtf8(), &parseError);
+ if (parseError.error != QJsonParseError::NoError || !doc.isObject())
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("Invalid %1 payload for '%2'")
+ .arg(propertyKind, m_cameraId);
+ }
+ return false;
+ }
+
+ const QJsonObject property = doc.object();
+ const QString propertyName = property.value(QStringLiteral("name")).toString().trimmed();
+ const QString propertyValue = property.value(QStringLiteral("value")).toString();
+ if (propertyName.isEmpty())
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("Missing %1 name for '%2'")
+ .arg(propertyKind, m_cameraId);
+ }
+ return false;
+ }
+
+ try
+ {
+ m_mmcore->setProperty(label.c_str(),
+ propertyName.toStdString().c_str(),
+ propertyValue.toStdString().c_str());
+ }
+ catch (const CMMError& mmError)
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QString("Failed to apply %1 '%2': %3")
+ .arg(propertyKind, propertyName, QString::fromStdString(mmError.getMsg()));
+ }
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // Dispatch one control request and emit a matching response
+ void MicroManagerDriverRuntime::handleRequest(quint64 connectionId,
+ quint64 requestId,
+ const QString& type,
+ const QJsonObject& message)
+ {
+ if (requestId == 0 || type.isEmpty())
+ {
+ return;
+ }
+
+ if (!m_mmcore && type != driverhost::kCommandShutdown)
+ {
+ emit responseReady(connectionId,
+ makeErrorResponse(type,
+ requestId,
+ QStringLiteral("DriverHost runtime not initialized")));
+ return;
+ }
+
+ if (type == driverhost::kCommandDescribe)
+ {
+ QJsonObject response = makeResponse(type, requestId, true);
+ response.insert(driverhost::kProviderIdField, m_providerId);
+ response.insert(driverhost::kDeviceIdField, m_cameraId);
+ response.insert(driverhost::kDeviceKindField, driverhost::kCapabilityCamera);
+ response.insert(driverhost::kCapabilitiesField,
+ QJsonArray{driverhost::kCapabilityCamera,
+ driverhost::kCapabilityProperties});
+ emit responseReady(connectionId, response);
+ return;
+ }
+
+ if (type == driverhost::kCommandStartPreview)
+ {
+ QString error;
+ const bool ok = startPreviewInternal(&error);
+ emit responseReady(connectionId,
+ ok
+ ? makeResponse(type, requestId, true)
+ : makeErrorResponse(type, requestId, error));
+ return;
+ }
+
+ if (type == driverhost::kCommandStopPreview)
+ {
+ QString error;
+ const bool ok = stopPreviewInternal(&error);
+ emit responseReady(connectionId,
+ ok
+ ? makeResponse(type, requestId, true)
+ : makeErrorResponse(type, requestId, error));
+ return;
+ }
+
+ if (type == driverhost::kCommandSetFrameDeliveryMode)
+ {
+ const QString mode = message.value(QStringLiteral("mode")).toString();
+ if (mode == driverhost::kFrameDeliveryModePreviewLatest)
+ {
+ m_frameDeliveryMode = FrameDeliveryMode::PreviewLatest;
+ m_deliveryTimer.invalidate();
+ }
+ else if (mode == driverhost::kFrameDeliveryModeLatestOnly)
+ {
+ m_frameDeliveryMode = FrameDeliveryMode::LatestOnly;
+ }
+ else if (mode == driverhost::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 == driverhost::kCommandGetExposure)
+ {
+ try
+ {
+ m_exposureMs = m_mmcore->getExposure();
+ QJsonObject response = makeResponse(type, requestId, true);
+ response.insert(QStringLiteral("exposureMs"), m_exposureMs);
+ emit responseReady(connectionId, response);
+ }
+ catch (const CMMError& mmError)
+ {
+ emit responseReady(
+ connectionId,
+ makeErrorResponse(type,
+ requestId,
+ QString::fromStdString(mmError.getMsg())));
+ }
+ return;
+ }
+
+ if (type == driverhost::kCommandSetExposure)
+ {
+ const double exposureMs = message.value(QStringLiteral("value")).toDouble(-1.0);
+ bool ok = exposureMs > 0.0;
+ QString error;
+ if (!ok)
+ {
+ error = QStringLiteral("Invalid exposure value");
+ }
+ else
+ {
+ try
+ {
+ m_mmcore->setExposure(exposureMs);
+ m_exposureMs = m_mmcore->getExposure();
+ }
+ catch (const CMMError& mmError)
+ {
+ ok = false;
+ error = QString::fromStdString(mmError.getMsg());
+ }
+ }
+
+ if (ok)
+ {
+ QJsonObject response = makeResponse(type, requestId, true);
+ response.insert(QStringLiteral("exposureMs"), m_exposureMs);
+ emit responseReady(connectionId, response);
+ }
+ else
+ {
+ emit responseReady(connectionId, makeErrorResponse(type, requestId, error));
+ }
+ return;
+ }
+
+ if (type == driverhost::kCommandListProperties)
+ {
+ QJsonArray properties;
+ QString error;
+ bool ok = true;
+ try
+ {
+ const auto names =
+ m_mmcore->getDevicePropertyNames(m_cameraId.toStdString().c_str());
+ for (const auto& name : names)
+ {
+ properties.append(QString::fromStdString(name));
+ }
+ }
+ catch (const CMMError& mmError)
+ {
+ ok = false;
+ error = QString::fromStdString(mmError.getMsg());
+ }
+
+ if (ok)
+ {
+ QJsonObject response = makeResponse(type, requestId, true);
+ response.insert(QStringLiteral("properties"), properties);
+ emit responseReady(connectionId, response);
+ }
+ else
+ {
+ emit responseReady(connectionId, makeErrorResponse(type, requestId, error));
+ }
+ return;
+ }
+
+ if (type == driverhost::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;
+ double upperLimit = 0.0;
+ QString error;
+ bool ok = true;
+
+ try
+ {
+ value = QString::fromStdString(
+ fromCache
+ ? m_mmcore->getPropertyFromCache(camera.c_str(), property.c_str())
+ : m_mmcore->getProperty(camera.c_str(), property.c_str()));
+
+ try
+ {
+ switch (m_mmcore->getPropertyType(camera.c_str(), property.c_str()))
+ {
+ case MM::String:
+ propertyType = QStringLiteral("String");
+ break;
+ case MM::Float:
+ propertyType = QStringLiteral("Float");
+ break;
+ case MM::Integer:
+ propertyType = QStringLiteral("Integer");
+ break;
+ default:
+ propertyType = QStringLiteral("Unknown");
+ break;
+ }
+ }
+ catch (const CMMError&)
+ {
+ }
+
+ try
+ {
+ 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&)
+ {
+ }
+
+ try
+ {
+ const auto values =
+ m_mmcore->getAllowedPropertyValues(camera.c_str(), property.c_str());
+ for (const auto& allowedValue : values)
+ {
+ allowedValues.append(QString::fromStdString(allowedValue));
+ }
+ }
+ catch (const CMMError&)
+ {
+ }
+
+ try
+ {
+ hasLimits = m_mmcore->hasPropertyLimits(camera.c_str(), property.c_str());
+ if (hasLimits)
+ {
+ lowerLimit = m_mmcore->getPropertyLowerLimit(camera.c_str(), property.c_str());
+ upperLimit = m_mmcore->getPropertyUpperLimit(camera.c_str(), property.c_str());
+ }
+ }
+ catch (const CMMError&)
+ {
+ hasLimits = false;
+ }
+ }
+ catch (const CMMError& mmError)
+ {
+ ok = false;
+ error = QString::fromStdString(mmError.getMsg());
+ }
+
+ if (ok)
+ {
+ QJsonObject response = makeResponse(type, requestId, true);
+ 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);
+ response.insert(QStringLiteral("upperLimit"), upperLimit);
+ emit responseReady(connectionId, response);
+ }
+ else
+ {
+ emit responseReady(connectionId, makeErrorResponse(type, requestId, error));
+ }
+ return;
+ }
+
+ if (type == driverhost::kCommandSetProperty)
+ {
+ const QString name = message.value(QStringLiteral("name")).toString();
+ const QString value = message.value(QStringLiteral("value")).toString();
+ QString error;
+ bool ok = true;
+ try
+ {
+ m_mmcore->setProperty(m_cameraId.toStdString().c_str(),
+ name.toStdString().c_str(),
+ value.toStdString().c_str());
+ m_mmcore->waitForDevice(m_cameraId.toStdString().c_str());
+ }
+ catch (const CMMError& mmError)
+ {
+ ok = false;
+ error = QString::fromStdString(mmError.getMsg());
+ }
+
+ emit responseReady(connectionId,
+ ok
+ ? makeResponse(type, requestId, true)
+ : makeErrorResponse(type, requestId, error));
+ return;
+ }
+
+ if (type == driverhost::kCommandSetRoi)
+ {
+ const int x = message.value(QStringLiteral("x")).toInt(0);
+ const int y = message.value(QStringLiteral("y")).toInt(0);
+ const int width = message.value(QStringLiteral("width")).toInt(0);
+ const int height = message.value(QStringLiteral("height")).toInt(0);
+ QString error;
+ bool ok = true;
+ try
+ {
+ m_mmcore->setROI(m_cameraId.toStdString().c_str(), x, y, width, height);
+ m_mmcore->waitForDevice(m_cameraId.toStdString().c_str());
+ refreshSourceRoi();
+ }
+ catch (const CMMError& mmError)
+ {
+ ok = false;
+ error = QString::fromStdString(mmError.getMsg());
+ }
+
+ emit responseReady(connectionId,
+ ok
+ ? makeResponse(type, requestId, true)
+ : makeErrorResponse(type, requestId, error));
+ return;
+ }
+
+ if (type == driverhost::kCommandClearRoi)
+ {
+ QString error;
+ bool ok = true;
+ try
+ {
+ m_mmcore->setCameraDevice(m_cameraId.toStdString().c_str());
+ m_mmcore->clearROI();
+ m_mmcore->waitForDevice(m_cameraId.toStdString().c_str());
+ refreshSourceRoi();
+ }
+ catch (const CMMError& mmError)
+ {
+ ok = false;
+ error = QString::fromStdString(mmError.getMsg());
+ }
+
+ emit responseReady(connectionId,
+ ok
+ ? makeResponse(type, requestId, true)
+ : makeErrorResponse(type, requestId, error));
+ return;
+ }
+
+ if (type == driverhost::kCommandGetRoi)
+ {
+ int x = 0;
+ int y = 0;
+ int width = 0;
+ int height = 0;
+ QString error;
+ bool ok = true;
+ try
+ {
+ m_mmcore->getROI(m_cameraId.toStdString().c_str(), x, y, width, height);
+ }
+ catch (const CMMError& mmError)
+ {
+ ok = false;
+ error = QString::fromStdString(mmError.getMsg());
+ }
+
+ if (ok)
+ {
+ QJsonObject response = makeResponse(type, requestId, true);
+ response.insert(QStringLiteral("x"), x);
+ response.insert(QStringLiteral("y"), y);
+ response.insert(QStringLiteral("width"), width);
+ response.insert(QStringLiteral("height"), height);
+ emit responseReady(connectionId, response);
+ }
+ else
+ {
+ emit responseReady(connectionId, makeErrorResponse(type, requestId, error));
+ }
+ return;
+ }
+
+ if (type == driverhost::kCommandCaptureEvent)
+ {
+ QString error;
+ quint64 frameIndex = 0;
+ const bool ok = captureEventFrameInternal(frameIndex, &error);
+ if (ok)
+ {
+ QJsonObject response = makeResponse(type, requestId, true);
+ response.insert(QStringLiteral("frameIndex"), driverhost::encodeUInt64(frameIndex));
+ emit responseReady(connectionId, response);
+ }
+ else
+ {
+ emit responseReady(connectionId, makeErrorResponse(type, requestId, error));
+ }
+ return;
+ }
+
+ if (type == driverhost::kCommandShutdown)
+ {
+ stopPreviewInternal(nullptr);
+ setState(State::ShuttingDown);
+ emit responseReady(connectionId, makeResponse(type, requestId, true));
+ QTimer::singleShot(50, this, [this]() { emit shutdownRequested(); });
+ return;
+ }
+
+ emit responseReady(connectionId,
+ makeErrorResponse(type,
+ requestId,
+ QStringLiteral("Unknown control command")));
+ }
+
+ // Stop preview before the runtime thread exits
+ void MicroManagerDriverRuntime::stopForExit()
+ {
+ stopPreviewInternal(nullptr);
+ setState(State::ShuttingDown);
+ }
+
+ // Check whether the runtime is currently previewing
+ bool MicroManagerDriverRuntime::previewRunning() const
+ {
+ return m_state == State::Previewing;
+ }
+
+ // Update runtime state and publish state changes
+ void MicroManagerDriverRuntime::setState(State state, const QString& error)
+ {
+ const bool changed = (m_state != state);
+ m_state = state;
+ if (!error.isEmpty())
+ {
+ m_lastError = error;
+ }
+ if (changed && (state == State::Previewing
+ || state == State::Idle
+ || state == State::Error
+ || state == State::ShuttingDown))
+ {
+ emitPreviewStateEvent();
+ }
+ if (!error.isEmpty())
+ {
+ emitDriverHostErrorEvent(error);
+ }
+ }
+
+ // Build a protocol response envelope
+ QJsonObject MicroManagerDriverRuntime::makeResponse(const QString& type, quint64 requestId, bool ok) const
+ {
+ QJsonObject response =
+ driverhost::makeEnvelope(driverhost::kMessageKindResponse, type, requestId);
+ response.insert(QStringLiteral("ok"), ok);
+ return response;
+ }
+
+ // Build a protocol error response envelope
+ QJsonObject MicroManagerDriverRuntime::makeErrorResponse(const QString& type,
+ quint64 requestId,
+ const QString& error) const
+ {
+ QJsonObject response = makeResponse(type, requestId, false);
+ response.insert(QStringLiteral("error"), error);
+ return response;
+ }
+
+ // Build a protocol event envelope
+ QJsonObject MicroManagerDriverRuntime::makeEvent(const QString& type) const
+ {
+ return driverhost::makeEnvelope(driverhost::kMessageKindEvent, type);
+ }
+
+ // Publish current preview state to clients
+ void MicroManagerDriverRuntime::emitPreviewStateEvent()
+ {
+ QJsonObject event = makeEvent(driverhost::kEventPreviewState);
+ event.insert(QStringLiteral("running"), previewRunning());
+ emit eventReady(event);
+ }
+
+ // Publish a DriverHost error event to clients
+ void MicroManagerDriverRuntime::emitDriverHostErrorEvent(const QString& error)
+ {
+ QJsonObject event = makeEvent(driverhost::kEventDriverHostError);
+ event.insert(QStringLiteral("error"), error);
+ emit eventReady(event);
+ }
+
+ // Publish the newest shared memory frame index
+ void MicroManagerDriverRuntime::emitFrameAvailableEvent(quint64 frameIndex)
+ {
+ QJsonObject event = makeEvent(driverhost::kEventFrameAvailable);
+ event.insert(QStringLiteral("frameIndex"), driverhost::encodeUInt64(frameIndex));
+ emit eventReady(event);
+ }
+
+ // Start continuous acquisition and polling
+ bool MicroManagerDriverRuntime::startPreviewInternal(QString* errorMessage)
+ {
+ if (!m_mmcore)
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("MMCore not available");
+ }
+ return false;
+ }
+ if (m_state == State::ShuttingDown || m_state == State::Error)
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("DriverHost is not in a runnable state");
+ }
+ return false;
+ }
+ if (previewRunning())
+ {
+ return true;
+ }
+
+ try
+ {
+ while (m_mmcore->getRemainingImageCount() > 0)
+ {
+ 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();
+ }
+ setState(State::Previewing);
+ return true;
+ }
+ catch (const CMMError& error)
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QString::fromStdString(error.getMsg());
+ }
+ return false;
+ }
+ }
+
+ // Stop continuous acquisition and polling
+ bool MicroManagerDriverRuntime::stopPreviewInternal(QString* errorMessage)
+ {
+ if (!m_mmcore)
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("MMCore not available");
+ }
+ return false;
+ }
+
+ try
+ {
+ if (m_mmcore->isSequenceRunning())
+ {
+ m_mmcore->stopSequenceAcquisition();
+ }
+ if (m_timer && m_timer->isActive())
+ {
+ 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);
+ }
+ return true;
+ }
+ catch (const CMMError& error)
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QString::fromStdString(error.getMsg());
+ }
+ return false;
+ }
+ }
+
+ // Capture one frame for recording or API requests
+ bool MicroManagerDriverRuntime::captureEventFrameInternal(quint64& frameIndex, QString* errorMessage)
+ {
+ frameIndex = 0;
+ try
+ {
+ const void* pixels = nullptr;
+ if (previewRunning())
+ {
+ QElapsedTimer waitTimer;
+ waitTimer.start();
+ while (m_mmcore->getRemainingImageCount() <= 0 && waitTimer.elapsed() < 2000)
+ {
+ QThread::msleep(1);
+ }
+ if (m_mmcore->getRemainingImageCount() > 0)
+ {
+ pixels = m_mmcore->popNextImage();
+ }
+ else if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("No frame available from running sequence");
+ }
+ }
+ else
+ {
+ m_mmcore->snapImage();
+ pixels = m_mmcore->getImage();
+ }
+
+ if (!pixels)
+ {
+ if (errorMessage && errorMessage->isEmpty())
+ {
+ *errorMessage = QStringLiteral("Empty image buffer");
+ }
+ return false;
+ }
+
+ const unsigned width = m_mmcore->getImageWidth();
+ const unsigned height = m_mmcore->getImageHeight();
+ const unsigned bytesPerPixel = m_mmcore->getBytesPerPixel();
+ if (!ensureFrameLayout(width, height, bytesPerPixel))
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("Unsupported frame format");
+ }
+ return false;
+ }
+
+ const quint64 frameAdvance = m_pendingFrameAdvance + 1;
+ m_pendingFrameAdvance = 0;
+ if (!writeFrameToSharedMemory(pixels, frameAdvance, &frameIndex))
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QStringLiteral("Shared memory unavailable");
+ }
+ return false;
+ }
+
+ if (m_frameDeliveryMode == FrameDeliveryMode::PreviewLatest)
+ {
+ m_deliveryTimer.restart();
+ }
+
+ emitFrameAvailableEvent(frameIndex);
+ return true;
+ }
+ catch (const CMMError& error)
+ {
+ if (errorMessage)
+ {
+ *errorMessage = QString::fromStdString(error.getMsg());
+ }
+ return false;
+ }
+ }
+
+ // Copy one camera frame into the shared memory ring buffer
+ bool MicroManagerDriverRuntime::writeFrameToSharedMemory(const void* pixels,
+ quint64 frameAdvance,
+ quint64* frameIndexOut)
+ {
+ if (!m_shm || !m_shm->isAttached())
+ {
+ return false;
+ }
+ uchar* base = static_cast(m_shm->data());
+ if (!base)
+ {
+ return false;
+ }
+
+ 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_frameIndex = nextFrameIndex;
+ if (frameIndexOut)
+ {
+ *frameIndexOut = m_frameIndex;
+ }
+ return false;
+ }
+
+ auto* control = reinterpret_cast(base);
+ m_nextWriteSlot = (slotIndex + 1) % kSharedFrameNumSlots;
+ SharedFrameHeader header{};
+ header.state = 1;
+ header.width = m_frameLayout.width;
+ header.height = m_frameLayout.height;
+ header.stride = m_frameLayout.stride;
+ header.pixelFormat = static_cast(m_frameLayout.format);
+ header.bitsPerSample = m_frameLayout.bitDepth;
+ header.channels = m_frameLayout.channels;
+ header.frameIndex = nextFrameIndex;
+ header.timestampNs =
+ static_cast(QDateTime::currentMSecsSinceEpoch()) * 1000000ull;
+ setSharedFrameSourceRoi(header,
+ m_sourceRoiX,
+ m_sourceRoiY,
+ m_sourceRoiWidth,
+ m_sourceRoiHeight);
+ 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));
+
+ 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;
+ }
+
+ return true;
+ }
+
+ // Drain camera frames into shared memory and publish the newest index
+ void MicroManagerDriverRuntime::pollAndWrite()
+ {
+ if (!m_mmcore)
+ {
+ return;
+ }
+
+ try
+ {
+ long remaining = m_mmcore->getRemainingImageCount();
+
+ if (remaining <= 0)
+ {
+ return;
+ }
+
+ quint64 newestFrameIndex = 0;
+ 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)
+ {
+ 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;
+ }
+ }
+ }
+ }
+ else
+ {
+ while (remaining-- > 0)
+ {
+ const void* pixels = m_mmcore->popNextImage();
+ if (!pixels)
+ {
+ break;
+ }
+ ++acquiredFrameCount;
+
+ if (!m_frameLayoutValid)
+ {
+ break;
+ }
+
+ quint64 writtenFrameIndex = 0;
+ if (!writeFrameToSharedMemory(pixels, 1, &writtenFrameIndex))
+ {
+ break;
+ }
+ newestFrameIndex = writtenFrameIndex;
+ }
+ }
+
+ if (newestFrameIndex != 0)
+ {
+ emitFrameAvailableEvent(newestFrameIndex);
+ }
+ updatePollingInterval(acquiredFrameCount);
+ }
+ catch (const CMMError& error)
+ {
+ const QString message =
+ QStringLiteral("DriverHost capture error: %1")
+ .arg(QString::fromStdString(error.getMsg()));
+ setState(State::Error, message);
+ }
+ }
+
+ // Adapts MMCore polling to the observed camera frame interval
+ void MicroManagerDriverRuntime::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 MicroManagerDriverRuntime::ensureFrameLayout(unsigned width, unsigned height, unsigned bytesPerPixel)
+ {
+ const auto logOversized = [this](quint64 byteCount)
+ {
+ if (!m_loggedOversizedFrame)
+ {
+ qWarning().noquote()
+ << QString("[DriverHost %1] Frame payload (%2 bytes) exceeds shared memory slot capacity (%3 bytes)")
+ .arg(m_cameraId)
+ .arg(static_cast(byteCount))
+ .arg(kSharedFrameMaxBytes);
+ m_loggedOversizedFrame = true;
+ }
+ };
+
+ if (bytesPerPixel != 1 && bytesPerPixel != 2)
+ {
+ if (!m_loggedUnsupportedFormat)
+ {
+ qWarning().noquote()
+ << QString("[DriverHost %1] Unsupported bytes-per-pixel (%2). Only Mono8/Mono16 are supported.")
+ .arg(m_cameraId)
+ .arg(bytesPerPixel);
+ m_loggedUnsupportedFormat = true;
+ }
+ return false;
+ }
+ m_loggedUnsupportedFormat = false;
+
+ const bool geometryChanged =
+ !m_frameLayoutValid
+ || m_frameLayout.width != width
+ || m_frameLayout.height != height
+ || m_frameLayout.bytesPerPixel != bytesPerPixel;
+
+ if (!geometryChanged)
+ {
+ if (m_frameLayout.byteCount == 0
+ || m_frameLayout.byteCount > static_cast