diff --git a/OpenScanDeviceLib/include/OpenScanDeviceLib.h b/OpenScanDeviceLib/include/OpenScanDeviceLib.h index b9ee7d9..6e47b9f 100644 --- a/OpenScanDeviceLib/include/OpenScanDeviceLib.h +++ b/OpenScanDeviceLib/include/OpenScanDeviceLib.h @@ -111,6 +111,20 @@ typedef struct OScDev_SettingImpl OScDev_SettingImpl; // functions. typedef struct OScInternal_Device OScDev_Device; typedef struct OScInternal_Setting OScDev_Setting; + +/// Handle to an acquisition, for use by one participating device. +/** + * Each device participating in an acquisition receives its own distinct + * handle to the same acquisition, in the call to its `Arm` function. + * + * The handle is owned by the host application. The device module may store + * it and use it from any of its threads, but only until the module indicates + * that the acquisition is finished on this device; that is, until it returns + * from a `Stop` call with the device idle, returns from `Wait`, or lets + * `IsRunning` report false. From that instant on, the host is allowed to + * free the handle, so no code in the module (including background threads) + * may access it. + */ typedef struct OScInternal_AcquisitionForDevice OScDev_Acquisition; typedef struct RERR_Error OScDev_RichError; #define OScDev_RichError_OK ((OScDev_RichError *)NULL) @@ -556,6 +570,13 @@ struct OScDev_DeviceImpl { */ OScDev_Error (*Open)(OScDev_Device *device); + /// Close the connection to the device. + /** + * If an acquisition is armed or running on this device, this function + * must stop it and wait for all acquisition activity (including any + * module threads that use the `OScDev_Acquisition` handle) to complete + * before returning, exactly as if `Stop` had been called. + */ OScDev_Error (*Close)(OScDev_Device *device); /// Return true if this device can provide the clock for scanning and @@ -736,6 +757,15 @@ struct OScDev_DeviceImpl { * and detector may not be clear. The implementation should behave _as if_ * the three components were separate and behaved as specified above. * + * This function is called at most once per device per acquisition, even + * when the device serves multiple roles. The handle `acq` may be stored + * and used for the duration of the acquisition, subject to the validity + * window documented at `OScDev_Acquisition`. + * + * If the device is already armed or running a previous acquisition, this + * function must fail with an error, leaving the previous acquisition + * undisturbed. OpenScanLib does not currently prevent such calls. + * * \todo Currently, state change after the device is armed is handled in a * variable way depending on what triggers the state change (especially * stop of acquisition). We should replace this with a simpler interface @@ -782,6 +812,19 @@ struct OScDev_DeviceImpl { * and return only when the device is ready to be armed again with a new * acquisition. * + * This function may be called multiple times in succession: OpenScanLib + * calls it once for each role (clock, scanner, detector) that the device + * serves in the acquisition, and also calls it to clean up when arming + * another device fails. + * + * This function must also handle the case where the device is armed but + * the acquisition was never started, by disarming and returning; it must + * not block waiting for a start trigger that will never arrive. + * + * Once this function returns with the device idle, the host may free the + * acquisition handle (see `OScDev_Acquisition`), so all use of the + * handle by the module must have ceased by that point. + * * \todo This function should be split into two separate functions, * `SoftwareTriggerStop()` and `Disarm()`, with very different roles. * The former should merely be an asynchronous signal that the device @@ -806,7 +849,11 @@ struct OScDev_DeviceImpl { * * In all cases, this function should not return `false` until all aspects * of the acquisition have completed and the device is ready to arm for a - * new acquisition. + * new acquisition. In particular, reporting false is a release point for + * the acquisition handle: once this function returns `false`, the host + * may free the handle (see `OScDev_Acquisition`), so the module must not + * report false while any of its threads may still use the handle (for + * example, to deliver frame callbacks). * * \todo This function should be replaced with a callback-based interface * to avoid polling and simplify device module implementation. @@ -821,6 +868,10 @@ struct OScDev_DeviceImpl { * This function should return immediately if `IsRunning()` is already * false. * + * After this function returns, the host may free the acquisition handle + * (see `OScDev_Acquisition`); no module thread may use the handle or + * deliver further frame callbacks. + * * \todo This function should be removed, because it leads to reinvention * of condition variable code in each device module. Better to report state * changes using a callback interface. @@ -1286,12 +1337,18 @@ OScDev_API void OScDev_Acquisition_GetROI(OScDev_Acquisition *acq, * This function must be called during an acquisition by the device that owns * the detector for the acquisition. * + * It may be called from any thread of the device module, but only while the + * acquisition handle is valid (see `OScDev_Acquisition`). + * * The data pointed to by `pixels` must not change during the call to this - * function. + * function. The call blocks while the application processes the frame data. * * \param[in] acq the acquisition that was given when arming this device * \param[in] channel the channel index * \param[in] pixels the raw pixel data for the channel + * \return `true` normally, or `false` if the application requests + * cancellation of the acquisition, in which case the module should stop the + * acquisition as soon as practical */ OScDev_API bool OScDev_Acquisition_CallFrameCallback(OScDev_Acquisition *acq, uint32_t channel, diff --git a/OpenScanLib/include/OpenScanLib.h b/OpenScanLib/include/OpenScanLib.h index 923bd48..4b40f47 100644 --- a/OpenScanLib/include/OpenScanLib.h +++ b/OpenScanLib/include/OpenScanLib.h @@ -212,6 +212,20 @@ typedef struct OScInternal_AcqTemplate OSc_AcqTemplate; /** * \brief An acquisition object, managing the state and data transfer for * data acquisition. + * + * An acquisition is created from an OSc_AcqTemplate by + * OSc_Acquisition_Create(). The only supported sequence of operations is + * OSc_Acquisition_Arm(), then OSc_Acquisition_Start(), then (once the + * acquisition has been stopped with OSc_Acquisition_Stop() or has completed + * on its own) OSc_Acquisition_Wait(), and finally OSc_Acquisition_Destroy(). + * + * At most one acquisition may be live (armed or running) at any given time + * for a given device, and therefore in practice for a given LSM. OpenScanLib + * does not currently enforce this. + * + * The acquisition holds non-owning references to the LSM's devices, so the + * devices (and the LSM) must remain valid for the lifetime of the + * acquisition object. */ typedef struct OScInternal_Acquisition OSc_Acquisition; @@ -231,6 +245,19 @@ typedef void (*OSc_SettingInvalidateFunc)(OSc_Setting *setting, void *data); /** * \brief Pointer to function that receives acquired frame data. + * + * The callback is invoked on device-module-internal threads, potentially + * concurrently with application calls into OpenScanLib and (when multiple + * detector devices or channels are involved) concurrently with itself. It + * must copy the pixel data before returning. + * + * The callback must not call OSc_Acquisition_Stop(), OSc_Acquisition_Wait(), + * or OSc_Acquisition_Destroy(); doing so may deadlock. To cancel the + * acquisition from the callback, return `false`. + * + * For a given acquisition, no calls to the callback occur after + * OSc_Acquisition_Stop() or OSc_Acquisition_Wait() has returned. + * * \sa OSc_Acquisition_SetFrameCallback() * \param acq the acquisition * \param channel the channel number (zero-based) @@ -361,6 +388,11 @@ OSc_API void OSc_SetDeviceModuleSearchPaths(const char **paths); OSc_API OSc_RichError *OSc_LSM_Create(OSc_LSM **lsm); /** + * \brief Destroy an LSM, closing all devices associated with it. + * + * The same precondition as OSc_Device_Close() applies: no device associated + * with the LSM may be participating in an armed or running acquisition. + * * \todo Return value should be `void`. */ OSc_API OSc_RichError *OSc_LSM_Destroy(OSc_LSM *lsm); @@ -449,6 +481,16 @@ OSc_API OSc_RichError *OSc_Device_GetDisplayName(OSc_Device *device, OSc_API OSc_RichError *OSc_Device_Open(OSc_Device *device, OSc_LSM *lsm); /** + * \brief Close a device. + * + * The device must not be participating in an armed or running acquisition; + * stop the acquisition (and have OSc_Acquisition_Stop() or + * OSc_Acquisition_Wait() return) before closing. As a safety net, device + * modules are required to stop and wait for any live acquisition when + * closing, but an OSc_Acquisition object that outlives this call then + * references a closed device, and the only operation that remains valid on + * it is OSc_Acquisition_Destroy(). + * * \todo Return value should be `void`. */ OSc_API OSc_RichError *OSc_Device_Close(OSc_Device *device); @@ -595,8 +637,42 @@ OSc_API OSc_RichError * OSc_AcqTemplate_GetBytesPerSample(OSc_AcqTemplate *tmpl, uint32_t *bytesPerSample); +/** + * \brief Create a new acquisition from the current settings of a template. + * + * The acquisition takes a snapshot of the template's settings; subsequent + * changes to the template do not affect it. + * + * The acquisition holds non-owning references to the LSM's clock and scanner + * devices and its enabled detector devices. These devices, and the LSM, must + * remain valid (open and not destroyed) for the lifetime of the acquisition + * object. + * + * The caller owns the returned acquisition and must destroy it with + * OSc_Acquisition_Destroy(). + * + * \param[out] acq location where the pointer to the new acquisition will be + * written + * \param tmpl the acquisition template + */ OSc_API OSc_RichError *OSc_Acquisition_Create(OSc_Acquisition **acq, OSc_AcqTemplate *tmpl); + +/** + * \brief Destroy an acquisition. + * + * This frees the acquisition object, including the per-device handles that + * were passed to the participating device modules. + * + * The acquisition must not be armed or running: a call to + * OSc_Acquisition_Stop() (or, if the acquisition completed on its own, + * OSc_Acquisition_Wait()) must have returned before this function is called. + * Destroying an armed or running acquisition results in undefined behavior, + * because device modules may still access the acquisition from their + * internal threads. OpenScanLib does not currently detect this misuse. + * + * The participating devices are not affected by this function. + */ OSc_API OSc_RichError *OSc_Acquisition_Destroy(OSc_Acquisition *acq); OSc_API OSc_RichError * OSc_Acquisition_SetNumberOfFrames(OSc_Acquisition *acq, @@ -621,9 +697,71 @@ OSc_Acquisition_GetNumberOfChannels(OSc_Acquisition *acq, OSc_API OSc_RichError * OSc_Acquisition_GetBytesPerSample(OSc_Acquisition *acq, uint32_t *bytesPerSample); +/** + * \brief Arm an acquisition, preparing all participating devices. + * + * Each device participating in the acquisition (clock, scanner, and + * detectors) is armed exactly once, even if it serves multiple roles. This + * function must be called, and succeed, before OSc_Acquisition_Start(). + * + * If arming any device fails, the devices already armed for this acquisition + * are stopped before the error is returned, leaving all of the devices + * disarmed. + * + * No participating device may be armed or running for another acquisition. + * OpenScanLib does not currently detect violations of this precondition, and + * behavior in that case is undefined (dependent on the device modules). + */ OSc_API OSc_RichError *OSc_Acquisition_Arm(OSc_Acquisition *acq); + +/** + * \brief Start an armed acquisition. + * + * This delivers the software start trigger to the device providing the + * clock. Call it exactly once per acquisition, after OSc_Acquisition_Arm() + * has succeeded. OpenScanLib does not currently check this ordering; + * starting an acquisition that is not armed is rejected only by some device + * modules. + */ OSc_API OSc_RichError *OSc_Acquisition_Start(OSc_Acquisition *acq); + +/** + * \brief Stop an acquisition. + * + * This requests every participating device to stop, and blocks until all of + * them have fully stopped and are ready to be armed again. Once this + * function returns, no further frame callbacks occur and the acquisition may + * be destroyed. + * + * This function is idempotent, and is safe to call on an acquisition that + * was never armed. + * + * This function must not be called from within the frame callback (see + * OSc_FrameCallback). + * + * \attention Stopping acts on the participating devices, not on this + * acquisition object specifically. Calling this function on an acquisition + * whose devices are participating in a different live acquisition will stop + * that acquisition. + */ OSc_API OSc_RichError *OSc_Acquisition_Stop(OSc_Acquisition *acq); + +/** + * \brief Wait for an acquisition to finish. + * + * This blocks until the acquisition has finished on all participating + * devices, whether by a call to OSc_Acquisition_Stop() or by completing the + * requested number of frames; it returns immediately if the acquisition is + * not armed or running. Once this function returns, no further frame + * callbacks occur and the acquisition may be destroyed. + * + * This function does not itself request a stop, so waiting for an + * acquisition that does not finish on its own will not return until + * OSc_Acquisition_Stop() is called from another thread. + * + * This function must not be called from within the frame callback (see + * OSc_FrameCallback). + */ OSc_API OSc_RichError *OSc_Acquisition_Wait(OSc_Acquisition *acq); /** @} */ // addtogroup api