Skip to content
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

Standard properties for device adapters #584

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

henrypinkard
Copy link
Member

@henrypinkard henrypinkard commented Feb 28, 2025

Standard Properties for Device Adapters

This PR introduces a standard properties system to Micro-Manager device adapters, addressing #258

Motivation

Currently, standardized functionality across devices is only expressed through methods in different device types. However, many features are more naturally expressed as properties (eg camera triggering in the new camera API #243). Furthermore, many device adapters have common functionality that is accessed through properties with identical or nearly identical names (e.g. Gain in a camera). This PR allows certain properties to be made part of the API.

To the higher-level application, standard properties function just like regular device properties, only with an api// prefix, signaling that they have consistent meaning across different device adapters. This is thus for now a fully backwards-compatible, opt-in mechanism. Regular properties without the api// prefix will continue to work indefinitely

For device adapter developers, each standard property has a dedicated creation method (e.g., CreateTriggerTypeStandardProperty) that can only be called in device types to which it is applicable

Two dummy test standard properties are implemented in this PR, added to the demo camera

How to Create a New Standard Property

  1. Add specification of the standard property in MMDevice.h:
static const std::vector<std::string> allowedValues = {"Internal", "External"};
static const MM::StandardProperty g_TriggerTypeStandardProperty(
   "TriggerType",                // name
   String,                   // type
   false,                    // isReadOnly
   false,                    // isPreInit
   allowedValues,                       // allowedValues 
   {},                       // requiredValues (empty vector)
   PropertyLimitUndefined,   // lowerLimit
   PropertyLimitUndefined,   // upperLimit
   false                     // required
);
  1. Link it with a particular device type using this macro in MMDevice.h:
LINK_STANDARD_PROP_TO_DEVICE_TYPE(CameraDevice, g_TriggerTypeStandardProperty)
  1. Add a dedicated creation method in CDeviceBase:
int CreateTriggerTypeStandardProperty(const char* value, MM::ActionFunctor* pAct = 0)

Technical notes/questions

@marktsuchida

  • Compile-time checking of standard property methods using SFINAE patterns ensures they can only be used by appropriate device types. The linking macro enables the same standard properties to be used across multiple device types while throwing compile-time errors if they're used with incompatible device types. In order to implement these compile-time checks, I had to move the DeviceType from the class definition to the declaration and make it a constexpr. These was a note about compiler warnings related to this in MMDevice.cpp, but these didn't seems to happen. Perhaps because of the use of constexpr? or perhaps because this comment is 11 years old and no longer applicable). @marktsuchida you think this is okay?
  • Runtime checks of required values are performed in the initialization of device instances in the core. It would have been nice to do this directly in the device layer, but I didn't see any way to do this given that there was no common superclass initialization called

TODO

@marktsuchida
Copy link
Member

  • Compile-time checking of standard property methods using SFINAE patterns ensures they can only be used by appropriate device types. The linking macro enables the same standard properties to be used across multiple device types while throwing compile-time errors if they're used with incompatible device types. In order to implement these compile-time checks, I had to move the DeviceType from the class definition to the declaration and make it a constexpr. These was a note about compiler warnings related to this in MMDevice.cpp, but these didn't seems to happen. Perhaps because of the use of constexpr? or perhaps because this comment is 11 years old and no longer applicable). @marktsuchida you think this is okay?

Those comments described the requirement prior to C++11, so yes, it's fine.

  • Runtime checks of required values are performed in the initialization of device instances in the core. It would have been nice to do this directly in the device layer, but I didn't see any way to do this given that there was no common superclass initialization called

I think it is fine to do it in MMCore -- in fact it gives me more confidence because it can't be messed with by the device adapter.

Copy link
Member

@marktsuchida marktsuchida left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments.

I think the major issue that needs to be solved is how we support future migration upon adding new standard properties. If we have a YES/NO distinction between implementing or not implementing all required standard properties, it will be very difficult to ever add any new required ones; this then calls into question the point of having required standard properties in the first place.

There may certainly be some properties that require others to also exist, but that kind of complex check is probably best limited to runtime.

@henrypinkard
Copy link
Member Author

I think the major issue that needs to be solved is how we support future migration upon adding new standard properties. If we have a YES/NO distinction between implementing or not implementing all required standard properties, it will be very difficult to ever add any new required ones; this then calls into question the point of having required standard properties in the first place.

I view the requirement as useful for when making new device types/APIs. I think that using the CCameraBase etc classes, we can add default implementations of standard properties. When making new base classes (either for the same device type, like your suggestion the camera API, or entirely new types of devices, we can ensure that devices developers have implemented all required properties. So in general a way to enforce that all devices of a particular type implement required properties going forward is to mark the current base class legacy, and add standard props to the device type that are unimplemented in the new base class.

No need to merge this now. I will use it when developing the camera API and see if I get further insights about how best to structure it

@henrypinkard
Copy link
Member Author

In implementing the new camera API, I've gained more clarity on how to effectively implement standard properties:

I've removed the required field entirely. All standard properties for a device type now default to being required, but this requirement can be bypassed by calling a SkipXXXXStandardProperty method in the device adapter. Failing to either implement or skip causes an initialization error. This requirement can be managed at the base class level (e.g., in CCameraBase).

The design allows declaring a set of standard properties for a device type (e.g., MM::Camera) with different behavior across subclasses. For example:

  • The old camera API base class automatically skips all standard properties, preserving backward compatibility while allowing specific adapters to opt-in if desired
  • The new camera API base class does not skip the standard properties, requiring adapter developers to either implement them or explicitly skip them

This approach accomplishes several goals:

  • Preserves backward compatibility for existing devices
  • Ensures developers using newer APIs are aware of standard properties while maintaining flexibility if a particular device doesn't implement them
  • Provides a migration path where both standard and non-standard properties can coexist. In many cases, moving to a standard property may just require adding an existing property callback to the standard property creation function. The old non-standard and new standard properties (which control the same functionality) can then both exist
  • Simplifies the camera API by implementing GenICam conventions without requiring numerous getter/setter methods

For application layer code, checking for standard property support is straightforward: core.hasProperty(device, 'api//prop_name')

Finally, I added the ability to delete a standard property, because I noticed on the Basler camera that the existence of properties often depended on the values of others.

Nothing else to do on this on my end. Ready to merge, or happy to make further refinements if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants