Skip to content

Standard properties for device adapters #584

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions DeviceAdapters/DemoCamera/DemoCamera.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,16 @@ int CDemoCamera::Initialize()
else
LogMessage(NoHubError);

// Example of how to create standard properties
// CPropertyAction *pActsp = new CPropertyAction (this, &CDemoCamera::OnTestStandardProperty);
// int nRett = CreateTestStandardProperty("123", pActsp);
// assert(nRett == DEVICE_OK);

// CPropertyAction *pActsp2 = new CPropertyAction (this, &CDemoCamera::OnTestWithValuesStandardProperty);
// int nRettt = CreateTestWithValuesStandardProperty("value1", pActsp2);
// assert(nRettt == DEVICE_OK);


// set property list
// -----------------

Expand Down Expand Up @@ -1384,6 +1394,39 @@ int CDemoCamera::OnBinning(MM::PropertyBase* pProp, MM::ActionType eAct)
return ret;
}

int CDemoCamera::OnTestStandardProperty(MM::PropertyBase* pProp, MM::ActionType eAct)
{
if (eAct == MM::BeforeGet)
{
pProp->Set("test");
return DEVICE_OK;
}
else if (eAct == MM::AfterSet)
{
std::string val;
pProp->Get(val);
return DEVICE_OK;
}
return DEVICE_OK;
}

int CDemoCamera::OnTestWithValuesStandardProperty(MM::PropertyBase* pProp, MM::ActionType eAct)
{
if (eAct == MM::BeforeGet)
{
pProp->Set("value1");
return DEVICE_OK;
}
else if (eAct == MM::AfterSet)
{
std::string val;
pProp->Get(val);
return DEVICE_OK;
}
return DEVICE_OK;
}


/**
* Handles "PixelType" property.
*/
Expand Down
2 changes: 2 additions & 0 deletions DeviceAdapters/DemoCamera/DemoCamera.h
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ class CDemoCamera : public CCameraBase<CDemoCamera>
// ----------------
int OnMaxExposure(MM::PropertyBase* pProp, MM::ActionType eAct);
int OnTestProperty(MM::PropertyBase* pProp, MM::ActionType eAct, long);
int OnTestStandardProperty(MM::PropertyBase* pProp, MM::ActionType eAct);
int OnTestWithValuesStandardProperty(MM::PropertyBase* pProp, MM::ActionType eAct);
int OnAsyncFollower(MM::PropertyBase* pProp, MM::ActionType eAct);
int OnAsyncLeader(MM::PropertyBase* pProp, MM::ActionType eAct);
void SlowPropUpdate(std::string leaderValue);
Expand Down
10 changes: 10 additions & 0 deletions MMCore/Devices/DeviceInstance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,16 @@ DeviceInstance::Initialize()
ThrowError("Device already initialized (or initialization already attempted)");
initializeCalled_ = true;
ThrowIfError(pImpl_->Initialize());

// Check for that all required standard properties implemented
char failedProperty[MM::MaxStrLength];
if (!pImpl_->ImplementsOrSkipsStandardProperties(failedProperty)) {
// shutdown the device
Shutdown();
ThrowError("Device " + GetLabel() +
" does not implement required standard property: " +
std::string(failedProperty));
}
initialized_ = true;
}

Expand Down
212 changes: 212 additions & 0 deletions MMDevice/DeviceBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
#include <iomanip>
#include <map>
#include <sstream>
#include <type_traits>
#include <set>

// common error messages
const char* const g_Msg_ERR = "Unknown error in the device";
Expand Down Expand Up @@ -117,6 +119,27 @@ class CDeviceBase : public T
CDeviceUtils::CopyLimitedString(name, moduleName_.c_str());
}

//// Standard properties are created using only these dedicated functions
// Such functions should all be defined here, and which device types they apply
//
// to is handled in MMDevice.h using the MM_INTERNAL_LINK_STANDARD_PROP_TO_DEVICE_TYPE macro
// int CreateTestStandardProperty(const char* value, MM::ActionFunctor* pAct = 0) {
// return CreateStandardProperty<MM::g_TestStandardProperty>(value, pAct);
// }

// int CreateTestWithValuesStandardProperty(const char* value, MM::ActionFunctor* pAct = 0) {
// // just make the values the required ones here. Also option to add
// // additional ones in real situations
// return CreateStandardProperty<MM::g_TestWithValuesStandardProperty>(value, pAct,
// MM::g_TestWithValuesStandardProperty.requiredValues);
// }

// Every standard property must either be created or explicitly skipped using
// a method like this
// void SkipTestStandardProperty() {
// SkipStandardProperty<MM::g_TestStandardProperty>();
// }

/**
* Assigns description string for a device (for use only by the calling code).
*/
Expand Down Expand Up @@ -534,6 +557,14 @@ class CDeviceBase : public T
return false;
}

virtual bool HasStandardProperty(const char* name) const
{
// prepend standard property prefix to name
std::string fullName = MM::g_KeywordStandardPropertyPrefix;
fullName += name;
return HasProperty(fullName.c_str());
}

/**
* Returns the number of allowed property values.
* If the set of property values is not defined, not bounded,
Expand Down Expand Up @@ -569,6 +600,40 @@ class CDeviceBase : public T
return true;
}

bool ImplementsOrSkipsStandardProperties(char* failedProperty) const {
// Get the device type
MM::DeviceType deviceType = this->GetType();

// Look up properties for this device type
auto it = MM::internal::GetDeviceTypeStandardPropertiesMap().find(deviceType);
if (it != MM::internal::GetDeviceTypeStandardPropertiesMap().end()) {
// Iterate through all properties for this device type
const auto& properties = it->second;
for (const auto& prop : properties) {
// Construct the full property name with prefix
std::string fullName = MM::g_KeywordStandardPropertyPrefix;
fullName += prop.name;

// Skip checking if this property is in the skipped list
if (skippedStandardProperties_.find(fullName) != skippedStandardProperties_.end()) {
continue;
}

// Check if the device has implemented it
if (!HasProperty(fullName.c_str())) {
// If not, copy in the name of the property and return false
CDeviceUtils::CopyLimitedString(failedProperty, fullName.c_str());
return false;
}
}
}

// All required properties are implemented or explicitly skipped
return true;
}



/**
* Creates a new property for the device.
* @param name - property name
Expand Down Expand Up @@ -596,6 +661,7 @@ class CDeviceBase : public T
*/
int CreatePropertyWithHandler(const char* name, const char* value, MM::PropertyType eType, bool readOnly,
int(U::*memberFunction)(MM::PropertyBase* pProp, MM::ActionType eAct), bool isPreInitProperty=false) {
// Check for reserved delimiter (handled in CreateProperty)
CPropertyAction* pAct = new CPropertyAction((U*) this, memberFunction);
return CreateProperty(name, value, eType, readOnly, pAct, isPreInitProperty);
}
Expand Down Expand Up @@ -1219,6 +1285,148 @@ class CDeviceBase : public T
}

private:

/**
* Low-level implementation for creating standard properties.
*
* This template method uses SFINAE (Substitution Failure Is Not An Error) to ensure
* that standard properties can only be created for device types they're valid for.
* The IsStandardPropertyValid template specializations determine which properties
* are valid for which device types at compile time.
*
* Note: This is a private implementation method. Device implementations should use
* the specific convenience methods like CreateStandardBinningProperty() instead.
*
* @param PropPtr - Pointer to the standard property definition
* @param value - Initial value for the property
* @param pAct - Optional action functor to handle property changes
* @return DEVICE_OK if successful, error code otherwise
*/
template <const MM::StandardProperty& PropRef>
typename std::enable_if<MM::internal::IsStandardPropertyValid<T::Type, PropRef>::value, int>::type
CreateStandardProperty(const char* value, MM::ActionFunctor* pAct = 0, const std::vector<std::string>& values = {}) {

// Create the full property name with prefix
std::string fullName = MM::g_KeywordStandardPropertyPrefix;
fullName += PropRef.name;

// Create the property with all appropriate fields
int ret = properties_.CreateProperty(fullName.c_str(), value, PropRef.type,
PropRef.isReadOnly, pAct, PropRef.isPreInit, true);
if (ret != DEVICE_OK)
return ret;

// Set limits if they exist
if (PropRef.hasLimits()) {
ret = SetPropertyLimits(fullName.c_str(), PropRef.lowerLimit, PropRef.upperLimit);
if (ret != DEVICE_OK)
return ret;
}

// Ensure the initial value is allowed if the property has predefined allowed values
if (!PropRef.allowedValues.empty()) {
if (std::find(PropRef.allowedValues.begin(), PropRef.allowedValues.end(), value) == PropRef.allowedValues.end()) {
return DEVICE_INVALID_PROPERTY_VALUE;
}
}

// Set the allowed values using the existing SetStandardPropertyValues function
if (!values.empty() || !PropRef.requiredValues.empty()) {
ret = SetStandardPropertyValues<PropRef>(values);
if (ret != DEVICE_OK)
return ret;
}

// Remove from skipped properties if it was previously marked as skipped
skippedStandardProperties_.erase(fullName);

return DEVICE_OK;
}

/**
* Sets allowed values for a standard property, clearing any existing values first.
* Performs the same validation as when creating the property.
*
* @param PropRef - Reference to the standard property definition
* @param values - Vector of values to set as allowed values
* @return DEVICE_OK if successful, error code otherwise
*/
template <const MM::StandardProperty& PropRef>
typename std::enable_if<MM::internal::IsStandardPropertyValid<T::Type, PropRef>::value, int>::type
SetStandardPropertyValues(const std::vector<std::string>& values) {
// Create the full property name with prefix
std::string fullName = MM::g_KeywordStandardPropertyPrefix;
fullName += PropRef.name;

// Check if the property exists
if (!HasProperty(fullName.c_str())) {
return DEVICE_INVALID_PROPERTY;
}

// Ensure all supplied values are allowed if the property has predefined allowed values
if (!PropRef.allowedValues.empty()) {
for (const std::string& val : values) {
if (std::find(PropRef.allowedValues.begin(), PropRef.allowedValues.end(), val) == PropRef.allowedValues.end()) {
return DEVICE_INVALID_PROPERTY_VALUE;
}
}
}

// Check if all required values are present
if (!PropRef.requiredValues.empty()) {
for (const std::string& val : PropRef.requiredValues) {
if (std::find(values.begin(), values.end(), val) == values.end()) {
return DEVICE_INVALID_PROPERTY_VALUE;
}
}
}

// Clear existing values
int ret = properties_.ClearAllowedValues(fullName.c_str(), true);
if (ret != DEVICE_OK)
return ret;

// Add the new values
for (const std::string& val : values) {
ret = properties_.AddAllowedValue(fullName.c_str(), val.c_str(), true);
if (ret != DEVICE_OK)
return ret;
}

return DEVICE_OK;
}

// This one is purely for providing better error messages at compile time
// When an function for setting an invalid standard property is called,
// this function will be called and will cause a compilation error.
template <const MM::StandardProperty& PropRef>
typename std::enable_if<!MM::internal::IsStandardPropertyValid<T::Type, PropRef>::value, int>::type
CreateStandardProperty(const char* /*value*/, MM::ActionFunctor* /*pAct*/ = 0,
const std::vector<std::string>& /*values*/ = std::vector<std::string>()) {
static_assert(MM::internal::IsStandardPropertyValid<T::Type, PropRef>::value,
"This standard property is not valid for this device type. Check the MM_INTERNAL_LINK_STANDARD_PROP_TO_DEVICE_TYPE definitions in MMDevice.h");
return DEVICE_UNSUPPORTED_COMMAND; // This line will never execute due to the static_assert
}

// Helper method to mark a required standard property as skipped
template <const MM::StandardProperty& PropRef>
void SkipStandardProperty() {
// Only allow skipping properties that are valid for this device type
if (MM::internal::IsStandardPropertyValid<T::Type, PropRef>::value) {
std::string fullName = MM::g_KeywordStandardPropertyPrefix;
fullName += PropRef.name;
skippedStandardProperties_.insert(fullName);
// Check if the property already exists. If so, delete it.
// This is needed because standard properties may be created dynamically not during
// initialization. For example, if they depend on the value of another property,
// and this is not known other than by setting that value on the device. In this case,
// the standard property will be created and destroyed as the values change.
if (HasProperty(fullName.c_str())) {
properties_.Delete(fullName.c_str());
}
}
}

bool PropertyDefined(const char* propName) const
{
return properties_.Find(propName) != 0;
Expand Down Expand Up @@ -1261,6 +1469,10 @@ class CDeviceBase : public T
// specific information about the errant property, etc.
mutable std::string morePropertyErrorInfo_;
std::string parentID_;

// Set to track which standard properties are explicitly skipped
std::set<std::string> skippedStandardProperties_;

};

// Forbid instantiation of CDeviceBase<MM::Device, U>
Expand Down
3 changes: 1 addition & 2 deletions MMDevice/MMDevice-SharedRuntime.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
<ClCompile Include="Debayer.cpp" />
<ClCompile Include="DeviceUtils.cpp" />
<ClCompile Include="ImgBuffer.cpp" />
<ClCompile Include="MMDevice.cpp" />
<ClCompile Include="ModuleInterface.cpp" />
<ClCompile Include="Property.cpp" />
</ItemGroup>
Expand Down Expand Up @@ -88,4 +87,4 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>
5 changes: 1 addition & 4 deletions MMDevice/MMDevice-SharedRuntime.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@
<ClCompile Include="ImgBuffer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MMDevice.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ModuleInterface.cpp">
<Filter>Source Files</Filter>
</ClCompile>
Expand Down Expand Up @@ -62,4 +59,4 @@
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>
</Project>
3 changes: 1 addition & 2 deletions MMDevice/MMDevice-StaticRuntime.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
<ClCompile Include="Debayer.cpp" />
<ClCompile Include="DeviceUtils.cpp" />
<ClCompile Include="ImgBuffer.cpp" />
<ClCompile Include="MMDevice.cpp" />
<ClCompile Include="ModuleInterface.cpp" />
<ClCompile Include="Property.cpp" />
</ItemGroup>
Expand Down Expand Up @@ -90,4 +89,4 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>
Loading
Loading