diff --git a/include/RegisterCatalogue.h b/include/RegisterCatalogue.h index a29cb83..2661cf1 100644 --- a/include/RegisterCatalogue.h +++ b/include/RegisterCatalogue.h @@ -20,6 +20,8 @@ namespace DeviceAccessPython { static py::list items(ChimeraTK::RegisterCatalogue& self); static py::list hiddenRegisters(ChimeraTK::RegisterCatalogue& self); + + static void bind(py::module& m); }; /*****************************************************************************************************************/ @@ -33,6 +35,9 @@ namespace DeviceAccessPython { // convert return type form ChimeraTK::AccessModeFlags to Python list static py::list getSupportedAccessModes(ChimeraTK::RegisterInfo& self); + + static void bind(py::module& m); + static void bindBackendRegisterInfoBase(py::module& m); }; /*****************************************************************************************************************/ @@ -41,6 +46,8 @@ namespace DeviceAccessPython { public: // Translate return type from RegisterPath to string static ChimeraTK::DataDescriptor::FundamentalType fundamentalType(ChimeraTK::DataDescriptor& self); + + static void bind(py::module& m); }; /*****************************************************************************************************************/ diff --git a/src/PyDataType.cc b/src/PyDataType.cc index 9cd821a..56bfb04 100644 --- a/src/PyDataType.cc +++ b/src/PyDataType.cc @@ -12,17 +12,37 @@ namespace ChimeraTK { /********************************************************************************************************************/ void ChimeraTK::PyDataType::bind(py::module& mod) { - py::class_ mDataType(mod, "DataType"); + py::class_ mDataType(mod, "DataType", + R"(The actual enum representing the data type. + It is a plain enum so the data type class can be used like a class enum, + i.e. types are identified for instance as DataType::int32.)"); mDataType.def(py::init()) .def("__str__", &ChimeraTK::DataType::getAsString) .def("__repr__", [](const ChimeraTK::DataType& type) { return "DataType." + type.getAsString(); }) - .def("isNumeric", &ChimeraTK::DataType::isNumeric) - .def("getAsString", &ChimeraTK::DataType::getAsString) - .def("isIntegral", &ChimeraTK::DataType::isIntegral) - .def("isSigned", &ChimeraTK::DataType::isSigned); + .def("isNumeric", &ChimeraTK::DataType::isNumeric, + R"(Returns whether the data type is numeric. Type 'none' returns false. + + :return: True if the data type is numeric, false otherwise. + :rtype: bool)") + .def("getAsString", &ChimeraTK::DataType::getAsString, + R"(Get the data type as string. + + :return: Data type as string. + :rtype: str)") + .def("isIntegral", &ChimeraTK::DataType::isIntegral, + R"(Return whether the raw data type is an integer. False is also returned for non-numerical types and 'none'. + + :return: True if the data type is an integer, false otherwise. + :rtype: bool)") + .def("isSigned", &ChimeraTK::DataType::isSigned, + R"(Return whether the raw data type is signed. True for signed integers and floating point types (currently only signed implementations). False otherwise (also for non-numerical types and 'none'). + + :return: True if the data type is signed, false otherwise. + :rtype: bool)"); py::enum_(mDataType, "TheType") - .value("none", ChimeraTK::DataType::none) + .value("none", ChimeraTK::DataType::none, + "The data type/concept does not exist, e.g. there is no raw transfer (do not confuse with Void)") .value("int8", ChimeraTK::DataType::int8) .value("uint8", ChimeraTK::DataType::uint8) .value("int16", ChimeraTK::DataType::int16) @@ -45,4 +65,4 @@ namespace ChimeraTK { /********************************************************************************************************************/ -} // namespace ChimeraTK \ No newline at end of file +} // namespace ChimeraTK diff --git a/src/PyDevice.cc b/src/PyDevice.cc index a972b51..ca0416c 100644 --- a/src/PyDevice.cc +++ b/src/PyDevice.cc @@ -207,37 +207,222 @@ namespace ChimeraTK { /*****************************************************************************************************************/ void PyDevice::bind(py::module& mod) { - py::class_ dev(mod, "Device"); - dev.def(py::init()) - .def(py::init()) - .def("open", py::overload_cast(&PyDevice::open), py::arg("aliasName")) - .def("open", py::overload_cast<>(&PyDevice::open)) - .def("close", &PyDevice::close) + py::class_ dev(mod, "Device", + R"(Class to access a ChimeraTK device. + + The device can be opened and closed, and provides methods to obtain register accessors. Additionally, + convenience methods to read and write registers directly are provided. + The class also offers methods to check the device state, obtain the register catalogue, Metadata and to set exception conditions.)"); + + dev.def(py::init(), py::arg("aliasName"), + R"(Initialize device and associate a backend. + + Note: + The device is not opened after initialization. + + :param aliasName: The ChimeraTK device descriptor for the device. + :type aliasName: str)") + .def(py::init(), + R"(Create device instance without associating a backend yet. + + A backend has to be explicitly associated using open method which + has the alias or CDD as argument.)") + .def("open", py::overload_cast(&PyDevice::open), py::arg("aliasName"), + R"(Open a device by the given alias name from the DMAP file. + + :param aliasName: The device alias name from the DMAP file. + :type aliasName: str)") + .def("open", py::overload_cast<>(&PyDevice::open), + R"((Re-)Open the device. + + Can only be called when the device was constructed with a given aliasName.)") + .def("close", &PyDevice::close, + R"(Close the device. + + The connection with the alias name is kept so the device can be re-opened + using the open() function without argument.)") .def("getVoidRegisterAccessor", &PyDevice::getVoidRegisterAccessor, py::arg("registerPathName"), - py::arg("accessModeFlags") = py::list(py::list())) + py::arg("accessModeFlags") = py::list(), + R"(Get a VoidRegisterAccessor object for the given register. + + :param registerPathName: Full path name of the register. + :type registerPathName: str + :param accessModeFlags: Optional flags to control register access details. + :type accessModeFlags: list[AccessMode] + :return: VoidRegisterAccessor for the specified register. + :rtype: VoidRegisterAccessor)") .def("getScalarRegisterAccessor", &PyDevice::getScalarRegisterAccessor, py::arg("userType"), - py::arg("registerPathName"), py::arg("elementsOffset") = 0, py::arg("accessModeFlags") = py::list()) + py::arg("registerPathName"), py::arg("elementsOffset") = 0, py::arg("accessModeFlags") = py::list(), + R"(Get a ScalarRegisterAccessor object for the given register. + + The ScalarRegisterAccessor allows to read and write registers transparently + by using the accessor object like a variable of the type UserType. + + :param userType: The data type for register access (numpy dtype). + :type userType: dtype + :param registerPathName: Full path name of the register. + :type registerPathName: str + :param elementsOffset: Word offset in register to access another but the first word. + :type elementsOffset: int, optional + :param accessModeFlags: Optional flags to control register access details. + :type accessModeFlags: list[AccessMode], optional + :return: ScalarRegisterAccessor for the specified register. + :rtype: ScalarRegisterAccessor)") .def("getOneDRegisterAccessor", &PyDevice::getOneDRegisterAccessor, py::arg("userType"), py::arg("registerPathName"), py::arg("numberOfElements") = 0, py::arg("elementsOffset") = 0, - py::arg("accessModeFlags") = py::list()) + py::arg("accessModeFlags") = py::list(), + R"(Get a OneDRegisterAccessor object for the given register. + + The OneDRegisterAccessor allows to read and write registers transparently + by using the accessor object like a vector of the type UserType. + + :param userType: The data type for register access (numpy dtype). + :type userType: dtype + :param registerPathName: Full path name of the register. + :type registerPathName: str + :param numberOfElements: Number of elements to access (0 for entire register). + :type numberOfElements: int, optional + :param elementsOffset: Word offset in register to skip initial elements. + :type elementsOffset: int, optional + :param accessModeFlags: Optional flags to control register access details. + :type accessModeFlags: list[AccessMode], optional + :return: OneDRegisterAccessor for the specified register. + :rtype: OneDRegisterAccessor)") .def("getTwoDRegisterAccessor", &PyDevice::getTwoDRegisterAccessor, py::arg("userType"), py::arg("registerPathName"), py::arg("numberOfElements") = 0, py::arg("elementsOffset") = 0, - py::arg("accessModeFlags") = py::list()) - .def("activateAsyncRead", &PyDevice::activateAsyncRead) - .def("getRegisterCatalogue", &PyDevice::getRegisterCatalogue) + py::arg("accessModeFlags") = py::list(), + R"(Get a TwoDRegisterAccessor object for the given register. + + This allows to read and write transparently 2-dimensional registers. + + :param userType: The data type for register access (numpy dtype). + :type userType: dtype + :param registerPathName: Full path name of the register. + :type registerPathName: str + :param numberOfElements: Number of elements per channel (0 for all). + :type numberOfElements: int, optional + :param elementsOffset: First element index for each channel to read. + :type elementsOffset: int, optional + :param accessModeFlags: Optional flags to control register access details. + :type accessModeFlags: list[AccessMode], optional + :return: TwoDRegisterAccessor for the specified register. + :rtype: TwoDRegisterAccessor)") + .def("activateAsyncRead", &PyDevice::activateAsyncRead, + R"(Activate asynchronous read for all transfer elements with wait_for_new_data flag. + + If called while the device is not opened or has an error, this call has no effect. + When this function returns, it is not guaranteed that all initial values have been + received already.)") + .def("getRegisterCatalogue", &PyDevice::getRegisterCatalogue, + R"(Return the register catalogue with detailed information on all registers. + + :return: Register catalogue containing all register information. + :rtype: RegisterCatalogue)") .def("read", &PyDevice::read, py::arg("registerPath"), py::arg("dtype") = py::dtype::of(), - py::arg("numberOfWords") = 0, py::arg("wordOffsetInRegister") = 0, py::arg("accessModeFlags") = py::list()) + py::arg("numberOfWords") = 0, py::arg("wordOffsetInRegister") = 0, py::arg("accessModeFlags") = py::list(), + R"(Convenience function to read a register without obtaining an accessor. + + Warning: + This function is inefficient as it creates and discards a register accessor + in each call. For better performance, use register accessors instead. + + :param registerPath: Full path name of the register. + :type registerPath: str + :param dtype: Data type for the read operation (default: float64). + :type dtype: dtype, optional + :param numberOfWords: Number of elements to read (0 for scalar or entire register). + :type numberOfWords: int, optional + :param wordOffsetInRegister: Word offset in register to skip initial elements. + :type wordOffsetInRegister: int, optional + :param accessModeFlags: Optional flags to control register access details. + :type accessModeFlags: list[AccessMode], optional + :return: Register value (scalar, 1D array, or 2D array depending on register type). + :rtype: scalar, ndarray, or list[list])") .def("write", &PyDevice::write2D, py::arg("registerPath"), py::arg("dataToWrite"), - py::arg("wordOffsetInRegister") = 0, py::arg("accessModeFlags") = py::list(), py::arg("dtype") = py::none()) + py::arg("wordOffsetInRegister") = 0, py::arg("accessModeFlags") = py::list(), py::arg("dtype") = py::none(), + R"(Convenience function to write a 2D register without obtaining an accessor. + + Warning: + This function is inefficient as it creates and discards a register accessor + in each call. For better performance, use register accessors instead. + + :param registerPath: Full path name of the register. + :type registerPath: str + :param dataToWrite: 2D array data to write to the register. + :type dataToWrite: list[list] + :param wordOffsetInRegister: Word offset in register to skip initial elements. + :type wordOffsetInRegister: int, optional + :param accessModeFlags: Optional flags to control register access details. + :type accessModeFlags: list[AccessMode], optional + :param dtype: Optional data type override (default: inferred from data). + :type dtype: dtype or None)") .def("write", &PyDevice::write1D, py::arg("registerPath"), py::arg("dataToWrite"), - py::arg("wordOffsetInRegister") = 0, py::arg("accessModeFlags") = py::list(), py::arg("dtype") = py::none()) + py::arg("wordOffsetInRegister") = 0, py::arg("accessModeFlags") = py::list(), py::arg("dtype") = py::none(), + R"(Convenience function to write a 1D register without obtaining an accessor. + + Warning: + This function is inefficient as it creates and discards a register accessor + in each call. For better performance, use register accessors instead. + + :param registerPath: Full path name of the register. + :type registerPath: str + :param dataToWrite: 1D array data to write to the register. + :type dataToWrite: list or ndarray + :param wordOffsetInRegister: Word offset in register to skip initial elements. + :type wordOffsetInRegister: int, optional + :param accessModeFlags: Optional flags to control register access details. + :type accessModeFlags: list[AccessMode], optional + :param dtype: Optional data type override (default: inferred from data). + :type dtype: dtype or None)") .def("write", &PyDevice::writeScalar, py::arg("registerPath"), py::arg("dataToWrite"), - py::arg("wordOffsetInRegister") = 0, py::arg("accessModeFlags") = py::list(), py::arg("dtype") = py::none()) - .def("isOpened", [](PyDevice& self) { return self._device.isOpened(); }) + py::arg("wordOffsetInRegister") = 0, py::arg("accessModeFlags") = py::list(), py::arg("dtype") = py::none(), + R"(Convenience function to write a scalar register without obtaining an accessor. + + Warning: + This function is inefficient as it creates and discards a register accessor + in each call. For better performance, use register accessors instead. + + :param registerPath: Full path name of the register. + :type registerPath: str + :param dataToWrite: Scalar value to write to the register. + :type dataToWrite: int, float, or str + :param wordOffsetInRegister: Word offset in register (for multi-word registers). + :type wordOffsetInRegister: int, optional + :param accessModeFlags: Optional flags to control register access details. + :type accessModeFlags: list[AccessMode], optional + :param dtype: Optional data type override (default: inferred from data). + :type dtype: dtype or None)") + .def( + "isOpened", [](PyDevice& self) { return self._device.isOpened(); }, + R"(Check if the device is currently opened. + + :return: True if device is opened, false otherwise. + :rtype: bool)") .def( "setException", [](PyDevice& self, const std::string& msg) { return self._device.setException(msg); }, - py::arg("message")) - .def("isFunctional", [](PyDevice& self) { return self._device.isFunctional(); }) + py::arg("message"), + R"(Set the device into an exception state. + + All asynchronous reads will be deactivated and all operations will see exceptions + until open() has successfully been called again. + + :param message: Exception message describing the error condition. + :type message: str)") + .def( + "isFunctional", [](PyDevice& self) { return self._device.isFunctional(); }, + R"(Check whether the device is working as intended. + + Usually this means it is opened and does not have any errors. + + :return: True if device is functional, false otherwise. + :rtype: bool)") + .def("getCatalogueMetadata", &PyDevice::getCatalogueMetadata, py::arg("metaTag"), + R"(Get metadata from the device catalogue. + + :param metaTag: The metadata parameter name to retrieve. + :type metaTag: str + :return: The metadata value. + :rtype: str)") .def("__enter__", [](PyDevice& self) { self.open(); @@ -248,8 +433,7 @@ namespace ChimeraTK { [[maybe_unused]] py::object exc_traceback) { self.close(); return false; - }) - .def("getCatalogueMetadata", &PyDevice::getCatalogueMetadata, py::arg("metaTag")); + }); } } // namespace ChimeraTK diff --git a/src/PyOneDRegisterAccessor.cc b/src/PyOneDRegisterAccessor.cc index c29d2bd..963f3e0 100644 --- a/src/PyOneDRegisterAccessor.cc +++ b/src/PyOneDRegisterAccessor.cc @@ -170,85 +170,208 @@ namespace ChimeraTK { /********************************************************************************************************************/ void PyOneDRegisterAccessor::bind(py::module& m) { - py::class_ arrayacc( - m, "OneDRegisterAccessor", py::buffer_protocol()); + py::class_ arrayacc(m, "OneDRegisterAccessor", + R"(Accessor class to read and write registers transparently by using the accessor object like a numpy array. + + Conversion to and from the UserType will be handled by a data converter matching the register + description in the map (if applicable). + + Note: + Transfers between the device and the internal buffer need to be triggered using the read() and + write() functions before reading from resp. after writing to the buffer.)", + py::buffer_protocol()); + arrayacc.def(py::init<>()) .def_buffer(&PyOneDRegisterAccessor::getBufferInfo) .def("read", &PyOneDRegisterAccessor::read, - "Read the data from the device.\n\nIf AccessMode::wait_for_new_data was set, this function will block " - "until new data has arrived. Otherwise it still might block for a short time until the data transfer was " - "complete.") + R"(Read the data from the device. + + If AccessMode.wait_for_new_data was set, this function will block until new data has arrived. + Otherwise it still might block for a short time until the data transfer was complete.)") .def("readNonBlocking", &PyOneDRegisterAccessor::readNonBlocking, - "Read the next value, if available in the input buffer.\n\nIf AccessMode::wait_for_new_data was set, this " - "function returns immediately and the return value indicated if a new value was available (true) or not " - "(false).\n\nIf AccessMode::wait_for_new_data was not set, this function is identical to read(), which " - "will still return quickly. Depending on the actual transfer implementation, the backend might need to " - "transfer data to obtain the current value before returning. Also this function is not guaranteed to be " - "lock free. The return value will be always true in this mode.") + R"(Read the next value, if available in the input buffer. + + If AccessMode.wait_for_new_data was set, this function returns immediately and the return value + indicates if a new value was available (true) or not (false). + + If AccessMode.wait_for_new_data was not set, this function is identical to read(), which will + still return quickly. Depending on the actual transfer implementation, the backend might need to + transfer data to obtain the current value before returning. Also this function is not guaranteed + to be lock free. The return value will be always true in this mode. + + :return: True if new data was available, false otherwise. + :rtype: bool)") .def("readLatest", &PyOneDRegisterAccessor::readLatest, - "Read the latest value, discarding any other update since the last read if present.\n\nOtherwise this " - "function is identical to readNonBlocking(), i.e. it will never wait for new values and it will return " - "whether a new value was available if AccessMode::wait_for_new_data is set.") - .def("write", &PyOneDRegisterAccessor::write, - "Write the data to device.\n\nThe return value is true, old data was lost on the write transfer (e.g. due " - "to an buffer overflow). In case of an unbuffered write transfer, the return value will always be false.", - py::arg("versionNumber") = PyVersionNumber::getNullVersion()) + R"(Read the latest value, discarding any other update since the last read if present. + + Otherwise this function is identical to readNonBlocking(), i.e. it will never wait for new values + and it will return whether a new value was available if AccessMode.wait_for_new_data is set. + + :return: True if new data was available, false otherwise. + :rtype: bool)") + .def("write", &PyOneDRegisterAccessor::write, py::arg("versionNumber") = PyVersionNumber::getNullVersion(), + R"(Write the data to device. + + The return value is true if old data was lost on the write transfer (e.g. due to a buffer overflow). + In case of an unbuffered write transfer, the return value will always be false. + + :param versionNumber: Version number to use for this write operation. If not specified, a new version number is generated. + :type versionNumber: VersionNumber + :return: True if data was lost, false otherwise. + :rtype: bool)") .def("writeDestructively", &PyOneDRegisterAccessor::writeDestructively, - "Just like write(), but allows the implementation to destroy the content of the user buffer in the " - "process.\n\nThis is an optional optimisation, hence there is a default implementation which just calls " - "the normal doWriteTransfer(). In any case, the application must expect the user buffer of the " - "TransferElement to contain undefined data after calling this function.", - py::arg("versionNumber") = PyVersionNumber::getNullVersion()) + py::arg("versionNumber") = PyVersionNumber::getNullVersion(), + R"(Just like write(), but allows the implementation to destroy the content of the user buffer in the process. + + This is an optional optimisation, hence there is a default implementation which just calls the normal + write(). In any case, the application must expect the user buffer of the accessor to contain + undefined data after calling this function. + + :param versionNumber: Version number to use for this write operation. If not specified, a new version number is generated. + :type versionNumber: VersionNumber + :return: True if data was lost, false otherwise. + :rtype: bool)") .def("interrupt", &PyOneDRegisterAccessor::interrupt, - "Return from a blocking read immediately and throw the ThreadInterrupted exception.") - .def("getName", &PyOneDRegisterAccessor::getName, "Returns the name that identifies the process variable.") + R"(Interrupt a blocking read operation. + + This will cause a blocking read to return immediately and throw an InterruptedException.)") + .def("getName", &PyOneDRegisterAccessor::getName, + R"(Returns the name that identifies the process variable. + + :return: The register name. + :rtype: str)") .def("getUnit", &PyOneDRegisterAccessor::getUnit, - "Returns the engineering unit.\n\nIf none was specified, it will default to ' n./ a.'") + R"(Returns the engineering unit. + + If none was specified, it will default to 'n./a.'. + + :return: The engineering unit string. + :rtype: str)") .def("getDescription", &PyOneDRegisterAccessor::getDescription, - "Returns the description of this variable/register.") + R"(Returns the description of this variable/register. + + :return: The description string. + :rtype: str)") .def("getValueType", &PyOneDRegisterAccessor::getValueType, - "Returns the std::type_info for the value type of this transfer element.\n\nThis can be used to determine " - "the type at runtime.") + R"(Returns the numpy dtype for the value type of this accessor. + + This can be used to determine the type at runtime. + + :return: Type information object. + :rtype: numpy.dtype)") .def("getVersionNumber", &PyOneDRegisterAccessor::getVersionNumber, - "Returns the version number that is associated with the last transfer (i.e. last read or write)") + R"(Returns the version number that is associated with the last transfer. + + This refers to the last read or write operation. + + :return: The version number of the last transfer. + :rtype: VersionNumber)") .def("isReadOnly", &PyOneDRegisterAccessor::isReadOnly, - "Check if transfer element is read only, i.e. it is readable but not writeable.") - .def("isReadable", &PyOneDRegisterAccessor::isReadable, "Check if transfer element is readable.") - .def("isWriteable", &PyOneDRegisterAccessor::isWriteable, "Check if transfer element is writeable.") + R"(Check if accessor is read only. + + This means it is readable but not writeable. + + :return: True if read only, false otherwise. + :rtype: bool)") + .def("isReadable", &PyOneDRegisterAccessor::isReadable, + R"(Check if accessor is readable. + + :return: True if readable, false otherwise. + :rtype: bool)") + .def("isWriteable", &PyOneDRegisterAccessor::isWriteable, + R"(Check if accessor is writeable. + + :return: True if writeable, false otherwise. + :rtype: bool)") .def("getId", &PyOneDRegisterAccessor::getId, - "Obtain unique ID for the actual implementation of this TransferElement.\n\nThis means that e.g. two " - "instances of ScalarRegisterAccessor created by the same call to Device::getScalarRegisterAccessor() (e.g. " - "by copying the accessor to another using NDRegisterAccessorBridge::replace()) will have the same ID, " - "while two instances obtained by to difference calls to Device::getScalarRegisterAccessor() will have a " - "different ID even when accessing the very same register.") + R"(Obtain unique ID for the actual implementation of this accessor. + + This means that e.g. two instances of OneDRegisterAccessor created by the same call to + Device.getOneDRegisterAccessor() will have the same ID, while two instances obtained by two + different calls to Device.getOneDRegisterAccessor() will have a different ID even when accessing + the very same register. + + :return: The unique accessor ID. + :rtype: TransferElementID)") .def("dataValidity", &PyOneDRegisterAccessor::dataValidity, - "Return current validity of the data.\n\nWill always return DataValidity.ok if the backend does not " - "support it") - .def( - "getNElements", &PyOneDRegisterAccessor::getNElements, "Return number of elements/samples in the register.") - .def("get", &PyOneDRegisterAccessor::get, "Return an array of UserType (without a previous read).") - .def("set", &PyOneDRegisterAccessor::set, "Set the values of the array of UserType.", py::arg("newValue")) - .def("setAndWrite", &PyOneDRegisterAccessor::setAndWrite, - "Convenience function to set and write new value.\n\nThe given version number. If versionNumber == {}, a" - "new version number is generated.", - py::arg("newValue"), py::arg("versionNumber") = PyVersionNumber::getNullVersion()) - .def("getAsCooked", &PyOneDRegisterAccessor::getAsCooked, - "Get the cooked values in case the accessor is a raw accessor (which does not do data conversion). This " - "returns the converted data from the use buffer. It does not do any read or write transfer.", - py::arg("element")) - .def("setAsCooked", &PyOneDRegisterAccessor::setAsCooked, - "Set the cooked values in case the accessor is a raw accessor (which does not do data conversion). This " - "converts to raw and writes the data to the user buffer. It does not do any read or write transfer.", - py::arg("element"), py::arg("value")) - .def("isInitialised", &PyOneDRegisterAccessor::isInitialised, "Check if the transfer element is initialised.") - .def("setDataValidity", &PyOneDRegisterAccessor::setDataValidity, - "Set the data validity of the transfer element.") + R"(Return current validity of the data. + + Will always return DataValidity.ok if the backend does not support it. + + :return: The current data validity state. + :rtype: DataValidity)") + .def("getNElements", &PyOneDRegisterAccessor::getNElements, + R"(Return number of elements/samples in the register. + + :return: Number of elements in the register. + :rtype: int)") + .def("get", &PyOneDRegisterAccessor::get, + R"(Return the register data as an array (without a previous read). + + :return: Array containing the register data. + :rtype: ndarray)") + .def("set", &PyOneDRegisterAccessor::set, py::arg("newValue"), + R"(Set the values of the array. + + :param newValue: New values to set in the buffer. + :type newValue: list or ndarray)") + .def("setAndWrite", &PyOneDRegisterAccessor::setAndWrite, py::arg("newValue"), + py::arg("versionNumber") = PyVersionNumber::getNullVersion(), + R"(Convenience function to set and write new value. + + If versionNumber is not specified, a new version number is generated. + + :param newValue: New values to set and write. + :type newValue: list or ndarray + :param versionNumber: Optional version number for the write operation. + :type versionNumber: VersionNumber)") + .def("getAsCooked", &PyOneDRegisterAccessor::getAsCooked, py::arg("element"), + R"(Get the cooked values in case the accessor is a raw accessor (which does not do data conversion). + + This returns the converted data from the user buffer. It does not do any read or write transfer. + + :param element: Element index to read. + :type element: int + :return: The cooked value at the specified element. + :rtype: int)") + .def("setAsCooked", &PyOneDRegisterAccessor::setAsCooked, py::arg("element"), py::arg("value"), + R"(Set the cooked values in case the accessor is a raw accessor (which does not do data conversion). + + This converts to raw and writes the data to the user buffer. It does not do any read or write transfer. + + :param element: Element index to write. + :type element: int + :param value: The cooked value to set. + :type value: float)") + .def("isInitialised", &PyOneDRegisterAccessor::isInitialised, + R"(Check if the accessor is initialised. + + :return: True if initialised, false otherwise. + :rtype: bool)") + .def("setDataValidity", &PyOneDRegisterAccessor::setDataValidity, py::arg("validity"), + R"(Set the data validity of the accessor. + + :param validity: The data validity state to set. + :type validity: DataValidity)") .def("getAccessModeFlags", &PyOneDRegisterAccessor::getAccessModeFlags, - "Return the access mode flags that were used to create this TransferElement.\n\nThis can be used to " - "determine the setting of the `raw` and the `wait_for_new_data` flags") + R"(Return the access mode flags that were used to create this accessor. + + This can be used to determine the setting of the raw and the wait_for_new_data flags. + + :return: List of access mode flags. + :rtype: list[AccessMode])") .def("readAndGet", &PyOneDRegisterAccessor::readAndGet, - "Convenience function to read and return an array of UserType.") - .def("__getitem__", &PyOneDRegisterAccessor::getitem, "Get an element from the array by index.") + R"(Convenience function to read and return the register data. + + :return: Array containing the register data after reading. + :rtype: ndarray)") + .def("__getitem__", &PyOneDRegisterAccessor::getitem, py::arg("index"), + R"(Get an element from the array by index. + + :param index: The element index. + :type index: int + :return: The value at the specified index. + :rtype: scalar)") .def("__getattr__", &PyOneDRegisterAccessor::getattr); for(const auto& fn : PyTransferElementBase::specialFunctionsToEmulateNumeric) { diff --git a/src/PyScalarRegisterAccessor.cc b/src/PyScalarRegisterAccessor.cc index 1b885a6..84027e4 100644 --- a/src/PyScalarRegisterAccessor.cc +++ b/src/PyScalarRegisterAccessor.cc @@ -233,106 +233,219 @@ namespace ChimeraTK { /********************************************************************************************************************/ void PyScalarRegisterAccessor::bind(py::module& m) { - py::class_ scalaracc( - m, "ScalarRegisterAccessor", py::buffer_protocol()); + py::class_ scalaracc(m, "ScalarRegisterAccessor", + R"(Accessor class to read and write scalar registers transparently by using the accessor object like a variable. + + Conversion to and from the UserType will be handled by a data converter matching the register + description in the map (if applicable). + + Note: + Transfers between the device and the internal buffer need to be triggered using the read() and + write() functions before reading from resp. after writing to the buffer.)", + py::buffer_protocol()); + scalaracc.def(py::init<>()) .def("read", &PyScalarRegisterAccessor::read, - "Read the data from the device.\n\nIf AccessMode::wait_for_new_data was set, this function will block " - "until new data has arrived. Otherwise it still might block for a short time until the data transfer was " - "complete.") + R"(Read the data from the device. + + If AccessMode.wait_for_new_data was set, this function will block until new data has arrived. + Otherwise it still might block for a short time until the data transfer was complete.)") .def("readNonBlocking", &PyScalarRegisterAccessor::readNonBlocking, - "Read the next value, if available in the input buffer.\n\nIf AccessMode::wait_for_new_data was set, this " - "function returns immediately and the return value indicated if a new value was available (true) or not " - "(false).\n\nIf AccessMode::wait_for_new_data was not set, this function is identical to read(), which " - "will still return quickly. Depending on the actual transfer implementation, the backend might need to " - "transfer data to obtain the current value before returning. Also this function is not guaranteed to be " - "lock free. The return value will be always true in this mode.") + R"(Read the next value, if available in the input buffer. + + If AccessMode.wait_for_new_data was set, this function returns immediately and the return value + indicates if a new value was available (true) or not (false). + + If AccessMode.wait_for_new_data was not set, this function is identical to read(), which will + still return quickly. Depending on the actual transfer implementation, the backend might need to + transfer data to obtain the current value before returning. Also this function is not guaranteed + to be lock free. The return value will be always true in this mode. + + :return: True if new data was available, false otherwise. + :rtype: bool)") .def("readLatest", &PyScalarRegisterAccessor::readLatest, - "Read the latest value, discarding any other update since the last read if present.\n\nOtherwise this " - "function is identical to readNonBlocking(), i.e. it will never wait for new values and it will return " - "whether a new value was available if AccessMode::wait_for_new_data is set.") - .def("write", &PyScalarRegisterAccessor::write, - "Write the data to device.\n\nThe return value is true, old data was lost on the write transfer (e.g. due " - "to an buffer overflow). In case of an unbuffered write transfer, the return value will always be false.", - py::arg("versionNumber") = PyVersionNumber::getNullVersion()) + R"(Read the latest value, discarding any other update since the last read if present. + + Otherwise this function is identical to readNonBlocking(), i.e. it will never wait for new values + and it will return whether a new value was available if AccessMode.wait_for_new_data is set. + + :return: True if new data was available, false otherwise. + :rtype: bool)") + .def("write", &PyScalarRegisterAccessor::write, py::arg("versionNumber") = PyVersionNumber::getNullVersion(), + R"(Write the data to device. + + The return value is true if old data was lost on the write transfer (e.g. due to a buffer overflow). + In case of an unbuffered write transfer, the return value will always be false. + + :param versionNumber: Version number to use for this write operation. If not specified, a new version number is generated. + :type versionNumber: VersionNumber)") .def("writeDestructively", &PyScalarRegisterAccessor::writeDestructively, - "Just like write(), but allows the implementation to destroy the content of the user buffer in the " - "process.\n\nThis is an optional optimisation, hence there is a default implementation which just calls " - "the normal doWriteTransfer(). In any case, the application must expect the user buffer of the " - "TransferElement to contain undefined data after calling this function.", - py::arg("versionNumber") = PyVersionNumber::getNullVersion()) - .def("getName", &PyScalarRegisterAccessor::getName, "Returns the name that identifies the process variable.") + py::arg("versionNumber") = PyVersionNumber::getNullVersion(), + R"(Just like write(), but allows the implementation to destroy the content of the user buffer in the process. + + This is an optional optimisation, hence there is a default implementation which just calls the normal + write(). In any case, the application must expect the user buffer of the accessor to contain + undefined data after calling this function. + + :param versionNumber: Version number to use for this write operation. If not specified, a new version number is generated. + :type versionNumber: VersionNumberl)") + .def("getName", &PyScalarRegisterAccessor::getName, + R"(Returns the name that identifies the process variable. + + :return: The register name. + :rtype: str)") .def("getUnit", &PyScalarRegisterAccessor::getUnit, - "Returns the engineering unit.\n\nIf none was specified, it will default to ' n./ a.'") + R"(Returns the engineering unit. + + If none was specified, it will default to 'n./a.'. + + :return: The engineering unit string. + :rtype: str)") .def("getDescription", &PyScalarRegisterAccessor::getDescription, - "Returns the description of this variable/register.") + R"(Returns the description of this variable/register. + + :return: The description string. + :rtype: str)") .def("getValueType", &PyScalarRegisterAccessor::getValueType, - "Returns the std::type_info for the value type of this transfer element.\n\nThis can be used to determine " - "the type at runtime.") + R"(Returns the type_info for the value type of this accessor. + + This can be used to determine the type at runtime. + + :return: Type information object. + :rtype: type)") .def("getVersionNumber", &PyScalarRegisterAccessor::getVersionNumber, - "Returns the version number that is associated with the last transfer (i.e. last read or write)") + R"(Returns the version number that is associated with the last transfer. + + This refers to the last read or write operation. + + :return: The version number of the last transfer. + :rtype: VersionNumber)") .def("isReadOnly", &PyScalarRegisterAccessor::isReadOnly, - "Check if transfer element is read only, i.e. it is readable but not writeable.") - .def("isReadable", &PyScalarRegisterAccessor::isReadable, "Check if transfer element is readable.") - .def("isWriteable", &PyScalarRegisterAccessor::isWriteable, "Check if transfer element is writeable.") + R"(Check if accessor is read only. + + This means it is readable but not writeable. + + :return: True if read only, false otherwise. + :rtype: bool)") + .def("isReadable", &PyScalarRegisterAccessor::isReadable, + R"(Check if accessor is readable. + + :return: True if readable, false otherwise. + :rtype: bool)") + .def("isWriteable", &PyScalarRegisterAccessor::isWriteable, + R"(Check if accessor is writeable. + + :return: True if writeable, false otherwise. + :rtype: bool)") .def("getId", &PyScalarRegisterAccessor::getId, - "Obtain unique ID for the actual implementation of this TransferElement.\n\nThis means that e.g. two " - "instances of ScalarRegisterAccessor created by the same call to Device::getScalarRegisterAccessor() (e.g. " - "by copying the accessor to another using NDRegisterAccessorBridge::replace()) will have the same ID, " - "while two instances obtained by to difference calls to Device::getScalarRegisterAccessor() will have a " - "different ID even when accessing the very same register.") + R"(Obtain unique ID for the actual implementation of this accessor. + + This means that e.g. two instances of ScalarRegisterAccessor created by the same call to + Device.getScalarRegisterAccessor() will have the same ID, while two instances obtained by two + different calls to Device.getScalarRegisterAccessor() will have a different ID even when accessing + the very same register. + + :return: The unique transfer element ID. + :rtype: TransferElementID)") .def("dataValidity", &PyScalarRegisterAccessor::dataValidity, - "Return current validity of the data.\n\nWill always return DataValidity.ok if the backend does not " - "support it") - .def("get", &PyScalarRegisterAccessor::get, "Return a value of UserType (without a previous read).") + R"(Return current validity of the data. + + Will always return DataValidity.ok if the backend does not support it. + + :return: The current data validity state. + :rtype: DataValidity)") + .def("get", &PyScalarRegisterAccessor::get, + R"(Return the scalar value (without a previous read). + + :return: The current value in the buffer. + :rtype: scalar)") .def("readAndGet", &PyScalarRegisterAccessor::readAndGet, - "Convenience function to read and return a value of UserType.") + R"(Convenience function to read and return the scalar value. + + :return: The value after reading from device. + :rtype: scalar)") .def( "set", [](PyScalarRegisterAccessor& self, const UserTypeVariantNoVoid& val) { self.set(val); }, - "Set the value of UserType.", py::arg("val")) + py::arg("val"), + R"(Set the scalar value. + + :param val: New value to set in the buffer. + :type val: int, float, bool, or str)") .def( - "set", [](PyScalarRegisterAccessor& self, const py::list& val) { self.setList(val); }, - "Set the value of UserType.", py::arg("val")) + "set", [](PyScalarRegisterAccessor& self, const py::list& val) { self.setList(val); }, py::arg("val"), + R"(Set the scalar value from a list. + + :param val: List containing a single value to set. + :type val: list)") .def( - "set", [](PyScalarRegisterAccessor& self, const py::array& val) { self.setArray(val); }, - "Set the value of UserType.", py::arg("val")) - .def("write", &PyScalarRegisterAccessor::write, - "Write the data to device.\n\nThe return value is true, old data was lost on the write transfer (e.g. due " - "to an buffer overflow). In case of an unbuffered write transfer, the return value will always be false.", - py::arg("versionNumber") = PyVersionNumber::getNullVersion()) - .def("writeDestructively", &PyScalarRegisterAccessor::writeDestructively, - "Just like write(), but allows the implementation to destroy the content of the user buffer in the " - "process.\n\nThis is an optional optimisation, hence there is a default implementation which just calls " - "the normal doWriteTransfer(). In any case, the application must expect the user buffer of the " - "TransferElement to contain undefined data after calling this function.", - py::arg("versionNumber") = PyVersionNumber::getNullVersion()) - .def("writeIfDifferent", &PyScalarRegisterAccessor::writeIfDifferent, - "Convenience function to set and write new value if it differes from the current value.\n\nThe given " - "version number is only used in case the value differs. If versionNumber == {nullptr}, a new version " - "number is generated only if the write actually takes place.", - py::arg("newValue"), py::arg("versionNumber") = PyVersionNumber::getNullVersion()) - .def("setAndWrite", &PyScalarRegisterAccessor::setAndWrite, - "Convenience function to set and write new value.\n\nThe given version number. If versionNumber == {}, a " - "new version number is generated.", - py::arg("newValue"), py::arg("versionNumber") = PyVersionNumber::getNullVersion()) + "set", [](PyScalarRegisterAccessor& self, const py::array& val) { self.setArray(val); }, py::arg("val"), + R"(Set the scalar value from a numpy array. + + :param val: Array containing a single value to set. + :type val: ndarray)") + .def("writeIfDifferent", &PyScalarRegisterAccessor::writeIfDifferent, py::arg("newValue"), + py::arg("versionNumber") = PyVersionNumber::getNullVersion(), + R"(Convenience function to set and write new value if it differs from the current value. + + The given version number is only used in case the value differs. If versionNumber is not specified, + a new version number is generated only if the write actually takes place. + + :param newValue: New value to compare and potentially write. + :type newValue: int, float, bool, or str + :param versionNumber: Optional version number for the write operation. + :type versionNumber: VersionNumber)") + .def("setAndWrite", &PyScalarRegisterAccessor::setAndWrite, py::arg("newValue"), + py::arg("versionNumber") = PyVersionNumber::getNullVersion(), + R"(Convenience function to set and write new value. + + If versionNumber is not specified, a new version number is generated. + + :param newValue: New value to set and write. + :type newValue: int, float, bool, or str + :param versionNumber: Optional version number for the write operation. + :type versionNumber: VersionNumber)") .def("getAsCooked", &PyScalarRegisterAccessor::getAsCooked, - "Get the cooked values in case the accessor is a raw accessor (which does not do data conversion). This " - "returns the converted data from the use buffer. It does not do any read or write transfer.") - .def("setAsCooked", &PyScalarRegisterAccessor::setAsCooked, - "Set the cooked values in case the accessor is a raw accessor (which does not do data conversion). This " - "converts to raw and writes the data to the user buffer. It does not do any read or write transfer.", - py::arg("value")) + R"(Get the cooked values in case the accessor is a raw accessor (which does not do data conversion). + + This returns the converted data from the user buffer. It does not do any read or write transfers. + + :return: The cooked value. + :rtype: float)") + .def("setAsCooked", &PyScalarRegisterAccessor::setAsCooked, py::arg("value"), + R"(Set the cooked values in case the accessor is a raw accessor (which does not do data conversion). + + This converts to raw and writes the data to the user buffer. It does not do any read or write transfers. + + :param value: The cooked value to set. + :type value: float)") .def("interrupt", &PyScalarRegisterAccessor::interrupt, - "Return from a blocking read immediately and throw the ThreadInterrupted exception.") + R"(Interrupt a blocking read operation. + + This will cause a blocking read to return immediately and throw an InterruptedException.)") .def("getAccessModeFlags", &PyScalarRegisterAccessor::getAccessModeFlags, - "Return the access mode flags that were used to create this TransferElement.\n\nThis can be used to " - "determine the setting of the `raw` and the `wait_for_new_data` flags") - .def("isInitialised", &PyScalarRegisterAccessor::isInitialised, "Check if the transfer element is initialised.") - .def("setDataValidity", &PyScalarRegisterAccessor::setDataValidity, - "Set the data validity of the transfer element.") + R"(Return the access mode flags that were used to create this accessor. + + This can be used to determine the setting of the raw and the wait_for_new_data flags. + + :return: List of access mode flags. + :rtype: list[AccessMode])") + .def("isInitialised", &PyScalarRegisterAccessor::isInitialised, + R"(Check if the accessor is initialised. + + :return: True if initialised, false otherwise. + :rtype: bool)") + .def("setDataValidity", &PyScalarRegisterAccessor::setDataValidity, py::arg("validity"), + R"(Set the data validity of the accessor. + + :param validity: The data validity state to set. + :type validity: DataValidity)") .def_property_readonly("dtype", &PyScalarRegisterAccessor::getValueType, - "Return the dtype of the value type of this TransferElement.\n\nThis can be used to determine the type at " - "runtime.") + R"(Return the dtype of the value type of this accessor. + + This can be used to determine the type at runtime. + + :return: Type information object. + :rtype: dtype)") .def( "__getitem__", [](PyScalarRegisterAccessor& self, const size_t& index) { @@ -341,18 +454,29 @@ namespace ChimeraTK { } return self.get(); }, - "Get the value of the scalar register accessor (same as get()).", py::arg("index")) + py::arg("index"), + R"(Get the value of the scalar register accessor (same as get()). + + :param index: Must be 0 for scalar accessors. + :type index: int + :return: The current value. + :rtype: scalar)") .def( "__setitem__", [](PyScalarRegisterAccessor& self, const size_t& index, const UserTypeVariantNoVoid& value) { if(index != 0) { - throw ChimeraTK::logic_error("PyScalarRegisterAccessor::__getitem__: Index out of range"); + throw ChimeraTK::logic_error("PyScalarRegisterAccessor::__setitem__: Index out of range"); } return self.set(value); }, - "Set the value of the scalar register accessor (same as set()).", py::arg("index"), py::arg("value")) - .def("__repr__", &PyScalarRegisterAccessor::repr); + py::arg("index"), py::arg("value"), + R"(Set the value of the scalar register accessor (same as set()). + :param index: Must be 0 for scalar accessors. + :type index: int + :param value: New value to set. + :type value: int, float, bool, or str)") + .def("__repr__", &PyScalarRegisterAccessor::repr); for(const auto& fn : PyTransferElementBase::specialFunctionsToEmulateNumeric) { scalaracc.def(fn.c_str(), [fn](PyScalarRegisterAccessor& acc, PyScalarRegisterAccessor& other) { return acc.get().attr(fn.c_str())(other.get()); diff --git a/src/RegisterCatalogue.cc b/src/RegisterCatalogue.cc index 5d371ac..aa08fd4 100644 --- a/src/RegisterCatalogue.cc +++ b/src/RegisterCatalogue.cc @@ -32,6 +32,44 @@ namespace DeviceAccessPython { /*******************************************************************************************************************/ + void RegisterCatalogue::bind(py::module& m) { + py::class_(m, "RegisterCatalogue") + .def(py::init(), "Catalogue of register information.") + .def( + "__iter__", [](const ChimeraTK::RegisterCatalogue& s) { return py::make_iterator(s.begin(), s.end()); }, + py::keep_alive<0, 1>()) + .def("_items", DeviceAccessPython::RegisterCatalogue::items) + .def("hiddenRegisters", DeviceAccessPython::RegisterCatalogue::hiddenRegisters, + R"(Returns list of all hidden registers in the catalogue + + :return: a list of hidden :class:`deviceaccess.RegisterInfo` objects. + :rtype: list[deviceaccess.RegisterInfo])") + .def("hasRegister", &ChimeraTK::RegisterCatalogue::hasRegister, + R"(Check if register with the given path name exists. + + :param registerPathName: Full path name of the register. + :type registerPathName: str + + :return: True if register exists in the catalogue, false otherwise. + :rtype: bool)") + .def("getNumberOfRegisters", &ChimeraTK::RegisterCatalogue::getNumberOfRegisters, + R"(Get number of registers in the catalogue. + + :return: Number of registers in the catalogue. + :rtype: int)") + .def("getRegister", &ChimeraTK::RegisterCatalogue::getRegister, py::arg("registerPathName"), + R"(Get register information for a given full path name. + + :param registerPathName: Full path name of the register. + :type registerPathName: str + + :return: Register information. + :rtype: RegisterInfo + :raises ChimeraTK::logic_error: if register does not exist in the catalogue.)"); + } + + /*******************************************************************************************************************/ + ChimeraTK::DataDescriptor RegisterInfo::getDataDescriptor(ChimeraTK::RegisterInfo& self) { return self.getDataDescriptor(); } @@ -53,6 +91,135 @@ namespace DeviceAccessPython { return python_flags; } + void RegisterInfo::bind(py::module& m) { + py::class_(m, "RegisterInfo") + .def(py::init(), "Catalogue of register information.") + .def("getDataDescriptor", DeviceAccessPython::RegisterInfo::getDataDescriptor, + R"(Return description of the actual payload data for this register. + + :return: :class:`deviceaccess.DataDescriptor` object containing information about the data format. + :rtype: DataDescriptor)") + .def("isReadable", &ChimeraTK::RegisterInfo::isReadable, + R"(Check whether the register is readable. + + :return: True if the register is readable, false otherwise. + :rtype: bool)") + .def("isValid", &ChimeraTK::RegisterInfo::isValid, + R"(Check whether the RegisterInfo object is valid. + + :return: True if the object contains a valid implementation, false otherwise. + :rtype: bool)") + .def("isWriteable", &ChimeraTK::RegisterInfo::isWriteable, + R"(Check whether the register is writeable. + + :return: True if the register is writeable, false otherwise. + :rtype: bool)") + .def("getRegisterName", DeviceAccessPython::RegisterInfo::getRegisterName, + R"(Return the full path name of the register. + + :return: Full path name of the register (including modules). + :rtype: RegisterPath)") + .def("getSupportedAccessModes", DeviceAccessPython::RegisterInfo::getSupportedAccessModes, + R"(Return all supported AccessModes for this register. + + :return: Flags indicating supported access modes. + :rtype: list[AccessMode])") + .def("getNumberOfElements", &ChimeraTK::RegisterInfo::getNumberOfElements, + R"(Return the number of elements per channel. + + :return: Number of elements per channel. + :rtype: int)") + .def("getNumberOfDimensions", &ChimeraTK::RegisterInfo::getNumberOfDimensions, + R"(Return the number of dimensions of this register. + + :return: Number of dimensions (0=scalar, 1=1D array, 2=2D array). + :rtype: int)") + .def("getNumberOfChannels", &ChimeraTK::RegisterInfo::getNumberOfChannels, + R"(Return the number of channels in the register. + + :return: Number of channels. + :rtype: int)") + .def("getQualifiedAsyncId", &ChimeraTK::RegisterInfo::getQualifiedAsyncId, + R"(Get the fully qualified async::SubDomain ID. + + If the register does not support wait_for_new_data it will be empty. + Note: At the moment using async::Domain and async::SubDomain is not mandatory yet, so the ID might be empty even if the register supports wait_for_new_data. + + :return: List of IDs forming the fully qualified async::SubDomain ID. + :rtype: list[int])") + .def("getTags", &ChimeraTK::RegisterInfo::getTags, + R"(Get the list of tags that are associated with this register. + + :return: Set of tags associated with this register. + :rtype: set[str])"); + } + /*******************************************************************************************************************/ + + void RegisterInfo::bindBackendRegisterInfoBase(py::module& m) { + py::class_(m, "BackendRegisterInfoBase") + .def("getDataDescriptor", &ChimeraTK::BackendRegisterInfoBase::getDataDescriptor, + R"(Return description of the actual payload data for this register. + + :return: :class:`deviceaccess.DataDescriptor` object containing information about the data format. + :rtype: DataDescriptor)") + .def("isReadable", &ChimeraTK::BackendRegisterInfoBase::isReadable, + R"(Return whether the register is readable. + + :return: True if the register is readable, false otherwise. + :rtype: bool)") + .def("isWriteable", &ChimeraTK::BackendRegisterInfoBase::isWriteable, + R"(Return whether the register is writeable. + + :return: True if the register is writeable, false otherwise. + :rtype: bool)") + .def("getRegisterName", &ChimeraTK::BackendRegisterInfoBase::getRegisterName, + R"(Return full path name of the register. + + :return: Full path name of the register (including modules). + :rtype: RegisterPath)") + .def("getSupportedAccessModes", &ChimeraTK::BackendRegisterInfoBase::getSupportedAccessModes, + R"(Return all supported AccessModes for this register. + + :return: Flags indicating supported access modes. + :rtype: list[AccessMode])") + .def("getNumberOfElements", &ChimeraTK::BackendRegisterInfoBase::getNumberOfElements, + R"(Return number of elements per channel. + + :return: Number of elements per channel. + :rtype: int)") + .def("getNumberOfDimensions", &ChimeraTK::BackendRegisterInfoBase::getNumberOfDimensions, + R"(Return number of dimensions of this register. + + :return: Number of dimensions (0=scalar, 1=1D array, 2=2D array). + :rtype: int)") + .def("getNumberOfChannels", &ChimeraTK::BackendRegisterInfoBase::getNumberOfChannels, + R"(Return number of channels in register. + + :return: Number of channels. + :rtype: int)") + .def("getQualifiedAsyncId", &ChimeraTK::BackendRegisterInfoBase::getQualifiedAsyncId, + R"(Return the fully qualified async::SubDomain ID. + + The default implementation returns an empty vector. + + :return: List of IDs forming the fully qualified async::SubDomain ID. + :rtype: list[int])") + .def("getTags", &ChimeraTK::BackendRegisterInfoBase::getTags, + R"(Get the list of tags associated with this register. + + The default implementation returns an empty set. + + :return: Set of tags associated with this register. + :rtype: set[str])") + .def("isHidden", &ChimeraTK::BackendRegisterInfoBase::isHidden, + R"(Return whether the register is "hidden". + + Hidden registers won't be listed when iterating the catalogue, but can be explicitly iterated. + + :return: True if the register is hidden, false otherwise. + :rtype: bool)"); + } + /*******************************************************************************************************************/ ChimeraTK::DataDescriptor::FundamentalType DataDescriptor::fundamentalType(ChimeraTK::DataDescriptor& self) { @@ -61,4 +228,106 @@ namespace DeviceAccessPython { /*******************************************************************************************************************/ + void DataDescriptor::bind(py::module& m) { + py::class_(m, "DataDescriptor") + .def(py::init()) + .def(py::init(), + R"(Construct a DataDescriptor from a DataType object. + + The DataDescriptor will describe the passed DataType with no raw type. + + :param type: The data type to describe. + :type type: DataType)") + .def(py::init<>(), + R"(Default constructor. + + Initializes the DataDescriptor with fundamental type set to "undefined".)") + .def("fundamentalType", DeviceAccessPython::DataDescriptor::fundamentalType, + R"(Get the fundamental data type. + + :return: The fundamental data type. + :rtype: FundamentalType)") + .def("isSigned", &ChimeraTK::DataDescriptor::isSigned, + R"(Return whether the data is signed or not. + + Only valid for numeric data types. + + :return: True if the data is signed, false otherwise. + :rtype: bool)") + .def("isIntegral", &ChimeraTK::DataDescriptor::isIntegral, + R"(Return whether the data is integral or not. + + May only be called for numeric data types. Examples: int or. float. + + :return: True if the data is integral, false otherwise. + :rtype: bool)") + .def("nDigits", &ChimeraTK::DataDescriptor::nDigits, + R"(Return the approximate maximum number of digits needed to represent the value. + + This includes a decimal dot (if not an integral data type) and the sign. + May only be called for numeric data types. + + Note: This number should only be used for displaying purposes. For some data types + this might be a large number (e.g. 300), which indicates that a different representation + than plain decimal numbers should be chosen. + + :return: Approximate maximum number of digits (base 10). + :rtype: int)") + .def("nFractionalDigits", &ChimeraTK::DataDescriptor::nFractionalDigits, + R"(Return the approximate maximum number of digits after the decimal dot. + + This is expressed in base 10 and excludes the decimal dot itself. + May only be called for non-integral numeric data types. + + Note: This number should only be used for displaying purposes. There is no guarantee + that the full precision can be displayed with the given number of digits. + + :return: Approximate maximum number of fractional digits (base 10). + :rtype: int)") + .def("rawDataType", &ChimeraTK::DataDescriptor::rawDataType, + R"(Get the raw data type. + + This describes the data conversion from 'cooked' to raw data type on the device. + The conversion does not change the shape of the data but describes the data type of + a single data point. + + Most backends will have type 'none' (no raw data conversion available). + + :return: The raw data type. + :rtype: DataType)") + .def("setRawDataType", &ChimeraTK::DataDescriptor::setRawDataType, py::arg("rawDataType"), + R"(Set the raw data type. + + This is useful e.g. when a decorated register should no longer allow raw access, + in which case you should set DataType.none. + + :param rawDataType: The raw data type to set. + :type rawDataType: DataType)") + .def("transportLayerDataType", &ChimeraTK::DataDescriptor::transportLayerDataType, + R"(Get the data type on the transport layer. + + This is always a 1D array of the specific data type. The raw transfer might contain + data for more than one register. + + Examples: + - The multiplexed data of a 2D array + - A text string containing data for multiple scalars mapped to different registers + - The byte sequence of a "struct" with data for multiple registers of different types + + Note: Currently all implementations return 'none'. There is no public API to access + the transport layer data yet. + + :return: The transport layer data type. + :rtype: DataType)") + .def("minimumDataType", &ChimeraTK::DataDescriptor::minimumDataType, + R"(Get the minimum data type required to represent the described data type. + + This is the minimum data type needed in the host CPU to represent the value. + + :return: The minimum required data type. + :rtype: DataType)"); + } + + /*******************************************************************************************************************/ + } /* namespace DeviceAccessPython*/ diff --git a/src/deviceaccessPython.cc b/src/deviceaccessPython.cc index 8254219..8215c48 100644 --- a/src/deviceaccessPython.cc +++ b/src/deviceaccessPython.cc @@ -34,59 +34,36 @@ PYBIND11_MODULE(deviceaccess, m) { ChimeraTK::PyOneDRegisterAccessor::bind(m); ChimeraTK::PyVoidRegisterAccessor::bind(m); ChimeraTK::PyDataType::bind(m); + DeviceAccessPython::RegisterCatalogue::bind(m); + DeviceAccessPython::RegisterInfo::bind(m); + DeviceAccessPython::RegisterInfo::bindBackendRegisterInfoBase(m); + DeviceAccessPython::DataDescriptor::bind(m); - m.def("setDMapFilePath", ChimeraTK::setDMapFilePath); - m.def("getDMapFilePath", ChimeraTK::getDMapFilePath); - - py::class_(m, "RegisterCatalogue") - .def(py::init()) - .def( - "__iter__", [](const ChimeraTK::RegisterCatalogue& s) { return py::make_iterator(s.begin(), s.end()); }, - py::keep_alive<0, 1>()) - .def("_items", DeviceAccessPython::RegisterCatalogue::items) - .def("hiddenRegisters", DeviceAccessPython::RegisterCatalogue::hiddenRegisters) - .def("hasRegister", &ChimeraTK::RegisterCatalogue::hasRegister) - .def("getRegister", &ChimeraTK::RegisterCatalogue::getRegister); - - py::class_(m, "RegisterInfo") - .def(py::init()) - .def("getDataDescriptor", DeviceAccessPython::RegisterInfo::getDataDescriptor) - .def("isReadable", &ChimeraTK::RegisterInfo::isReadable) - .def("isValid", &ChimeraTK::RegisterInfo::isValid) - .def("isWriteable", &ChimeraTK::RegisterInfo::isWriteable) - .def("getRegisterName", DeviceAccessPython::RegisterInfo::getRegisterName) - .def("getSupportedAccessModes", DeviceAccessPython::RegisterInfo::getSupportedAccessModes) - .def("getNumberOfElements", &ChimeraTK::RegisterInfo::getNumberOfElements) - .def("getNumberOfDimensions", &ChimeraTK::RegisterInfo::getNumberOfDimensions) - .def("getNumberOfChannels", &ChimeraTK::RegisterInfo::getNumberOfChannels); - - py::class_(m, "BackendRegisterInfoBase") - .def("getDataDescriptor", &ChimeraTK::BackendRegisterInfoBase::getDataDescriptor) - .def("isReadable", &ChimeraTK::BackendRegisterInfoBase::isReadable) - .def("isWriteable", &ChimeraTK::BackendRegisterInfoBase::isWriteable) - .def("getRegisterName", &ChimeraTK::BackendRegisterInfoBase::getRegisterName) - .def("getSupportedAccessModes", &ChimeraTK::BackendRegisterInfoBase::getSupportedAccessModes) - .def("getNumberOfElements", &ChimeraTK::BackendRegisterInfoBase::getNumberOfElements) - .def("getNumberOfDimensions", &ChimeraTK::BackendRegisterInfoBase::getNumberOfDimensions) - .def("getNumberOfChannels", &ChimeraTK::BackendRegisterInfoBase::getNumberOfChannels); - - py::class_(m, "DataDescriptor") - .def(py::init()) - .def("rawDataType", &ChimeraTK::DataDescriptor::rawDataType) - .def("transportLayerDataType", &ChimeraTK::DataDescriptor::transportLayerDataType) - .def("minimumDataType", &ChimeraTK::DataDescriptor::minimumDataType) - .def("isSigned", &ChimeraTK::DataDescriptor::isSigned) - .def("isIntegral", &ChimeraTK::DataDescriptor::isIntegral) - .def("nDigits", &ChimeraTK::DataDescriptor::nDigits) - .def("nFractionalDigits", &ChimeraTK::DataDescriptor::nFractionalDigits) - .def("fundamentalType", DeviceAccessPython::DataDescriptor::fundamentalType); - - py::enum_(m, "AccessMode") - .value("raw", ChimeraTK::AccessMode::raw) - .value("wait_for_new_data", ChimeraTK::AccessMode::wait_for_new_data) + m.def("setDMapFilePath", ChimeraTK::setDMapFilePath, py::arg("dmapFilePath"), + R"(Set the location of the dmap file. + + :param dmapFilePath: Relative or absolute path of the dmap file (directory and file name). + :type dmapFilePath: str)"); + + m.def("getDMapFilePath", ChimeraTK::getDMapFilePath, + R"(Returns the dmap file name which the library currently uses for looking up device(alias) names. + + :return: Path of the dmap file (directory and file name). + :rtype: str)"); + + py::enum_(m, "AccessMode", + R"(Access mode flags for register access. + + Note: + Using the raw flag will make your code intrinsically dependent on the backend type, since the actual raw data type must be known.)") + .value("raw", ChimeraTK::AccessMode::raw, + R"(This access mode disables any possible conversion from the original hardware data type into the given UserType. Obtaining the accessor with a UserType unequal to the actual raw data type will fail and throw an exception.)") + .value("wait_for_new_data", ChimeraTK::AccessMode::wait_for_new_data, + R"(This access mode makes any read blocking until new data has arrived since the last read. This flag may not be supported by all registers (and backends), in which case an exception will be thrown.)") .export_values(); - py::enum_(m, "FundamentalType") + py::enum_(m, "FundamentalType", + "This is only used inside the DataDescriptor class; defined outside to prevent too long fully qualified names.") .value("numeric", ChimeraTK::DataDescriptor::FundamentalType::numeric) .value("string", ChimeraTK::DataDescriptor::FundamentalType::string) .value("boolean", ChimeraTK::DataDescriptor::FundamentalType::boolean) @@ -94,29 +71,68 @@ PYBIND11_MODULE(deviceaccess, m) { .value("undefined", ChimeraTK::DataDescriptor::FundamentalType::undefined) .export_values(); - py::enum_(m, "DataValidity") - .value("ok", ChimeraTK::DataValidity::ok) - .value("faulty", ChimeraTK::DataValidity::faulty) + py::enum_(m, "DataValidity", + R"(The current state of the data. + + Note: + This is a flag to describe the validity of the data. It should be used to signalize + whether or not to trust the data currently. It MUST NOT be used to signalize any + communication errors with a device, rather to signalize the consumer after such an + error that the data is currently not trustable, because we are performing calculations + with the last known valid data, for example.)") + .value("ok", ChimeraTK::DataValidity::ok, "The data is considered valid") + .value("faulty", ChimeraTK::DataValidity::faulty, "The data is not considered valid") .export_values(); py::class_(m, "TransferElementID") - .def("isValid", &ChimeraTK::TransferElementID::isValid) + .def("isValid", &ChimeraTK::TransferElementID::isValid, "Check whether the ID is valid.") .def("__ne__", &ChimeraTK::TransferElementID::operator!=) .def("__eq__", &ChimeraTK::TransferElementID::operator==); - py::class_(m, "RegisterPath") + py::class_(m, "RegisterPath", + R"a(Class to store a register path name. Elements of the path are separated by a "/" character, but an separation character (e.g. ".") can optionally be specified as well. Different equivalent notations will be converted into a standardised notation automatically.)a") .def(py::init()) - .def(py::init(), py::arg("s")) + .def(py::init(), py::arg("path")) .def("__str__", &ChimeraTK::RegisterPath::operator std::string) - .def("setAltSeparator", &ChimeraTK::RegisterPath::setAltSeparator) - .def("getWithAltSeparator", &ChimeraTK::RegisterPath::getWithAltSeparator) - .def("__itruediv__", &ChimeraTK::RegisterPath::operator/=) - .def("__iadd__", &ChimeraTK::RegisterPath::operator+=) + .def("setAltSeparator", &ChimeraTK::RegisterPath::setAltSeparator, py::arg("altSeparator"), + R"(Set alternative separator. + + :param altSeparator: Alternative separator character to use instead of "/". Use an empty string to reset to default. + :type altSeparator: str)") + .def("getWithAltSeparator", &ChimeraTK::RegisterPath::getWithAltSeparator, + R"(Obtain path with alternative separator character instead of "/". The leading separator will be omitted. + + :return: Register path with alternative separator. + :rtype: str)") + .def("__itruediv__", &ChimeraTK::RegisterPath::operator/=, py::arg("rightHandSide"), + R"(Modify this object by adding a new element to this path. + + :param rightHandSide: New element to add to the path. + :type rightHandSide: str + + :return: Modified RegisterPath object. + :rtype: RegisterPath)") + .def("__iadd__", &ChimeraTK::RegisterPath::operator+=, py::arg("rightHandSide"), + R"(Modify this object by concatenating the given string to the path. + + :param rightHandSide: String to concatenate to the path. + :type rightHandSide: str + + :return: Modified RegisterPath object. + :rtype: RegisterPath)") .def("__lt__", &ChimeraTK::RegisterPath::operator<) - .def("length", &ChimeraTK::RegisterPath::length) + .def("length", &ChimeraTK::RegisterPath::length, + R"(Get the length of the path (including leading slash). + + :return: Length of the register path. + :rtype: int)") .def("startsWith", &ChimeraTK::RegisterPath::startsWith) .def("endsWith", &ChimeraTK::RegisterPath::endsWith) - .def("getComponents", &ChimeraTK::RegisterPath::getComponents) + .def("getComponents", &ChimeraTK::RegisterPath::getComponents, + R"(Split path into components. + + :return: list of path components. + :rtype: list[str])") .def("__ne__", [](const ChimeraTK::RegisterPath& self, const ChimeraTK::RegisterPath& other) { return self != other; }) .def("__ne__", [](const ChimeraTK::RegisterPath& self, const std::string& other) { return self != other; })