#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pcloudStimulusProducer.h" #include "livoxGen1.h" namespace smo { namespace stim_buff { // Salmanoff hooks, obtained from SMO_GET_STIM_BUFF_API_DESC_FN_NAME(). const SmoCallbacks* smoHooksPtr = nullptr; static SmoThreadingModelDesc smoThreadingModelDesc; // Local collection of stimulus producers static std::vector> attachedStimulusProducers; // Check if a StimulusProducer matches the requested stim feature static bool isProducerForStimFeature( const std::shared_ptr& stimProducer, const std::string& qualeIfaceApi) { // Check if the qualeIfaceApi requires a PcloudStimulusProducer if (qualeIfaceApi == "mesh" || qualeIfaceApi == "pcloudIntensity" || qualeIfaceApi == "pcloudAmbience") { // Attempt to upcast to PcloudStimulusProducer auto pcloudProducer = std::dynamic_pointer_cast( stimProducer); return pcloudProducer != nullptr; } else if (qualeIfaceApi == "gyro" || qualeIfaceApi == "accel") { /** TODO: * Add upcast mappings for gyro and accel later when we implement * ImuStimulusProducer. */ return false; } return false; } // Get stimulus producer by device attachment spec static std::shared_ptr getStimulusProducer( const std::shared_ptr& spec ) { for (const auto& stimProducer : attachedStimulusProducers) { // Compare device selectors to find matching buffer if (livoxProto1::comms::deviceIdentifiersEqual( stimProducer->deviceAttachmentSpec->deviceSelector, spec->deviceSelector) && isProducerForStimFeature(stimProducer, spec->qualeIfaceApi)) { return stimProducer; } } return nullptr; } // Helper function to parse histbuffMs from device attachment spec static int parseHistbuffMs( const std::shared_ptr& spec) { int histbuffMs = 30000; // Default: 30000ms (30 seconds) const std::vector histbuffParamNames = { "history-buffer-duration-ms", "hist-buff-duration-ms", "histbuff-duration-ms", "histbuff-ms" }; // Loop through synonyms in reverse order; lattermost synonym wins. for (auto synIt = histbuffParamNames.rbegin(); synIt != histbuffParamNames.rend(); ++synIt) { const auto& paramName = *synIt; try { histbuffMs = smo::device::DeviceAttachmentSpec ::parseRequiredParamAsInt( spec->qualeIfaceApiParams, paramName); break; // Found and parsed successfully } catch (const std::exception&) { // Parameter not found or parse error, continue to next synonym continue; } } return histbuffMs; } // Helper function to parse n-dgrams-per-frame from stim-buff-api params static size_t parseNDgramsPerFrame( const std::shared_ptr& spec) { size_t nDgramsPerFrame = 84; // Default: 84 datagrams per frame const std::vector nDgramsPerFrameParamNames = { "n-dgrams-per-frame", "num-dgrams-per-frame" }; // Loop through synonyms in reverse order; lattermost synonym wins. for (auto synIt = nDgramsPerFrameParamNames.rbegin(); synIt != nDgramsPerFrameParamNames.rend(); ++synIt) { const auto& paramName = *synIt; try { nDgramsPerFrame = static_cast( smo::device::DeviceAttachmentSpec::parseRequiredParamAsInt( spec->stimBuffApiParams, paramName)); break; // Found and parsed successfully } catch (const std::exception&) { // Parameter not found or parse error, continue to next synonym continue; } } return nDgramsPerFrame; } // LivoxProto1DllState constructor implementation LivoxProto1DllState::LivoxProto1DllState() : dlopenHandle(nullptr, DlCloser), livoxProto1_main(nullptr), livoxProto1_exit(nullptr), livoxProto1_getOrCreateDeviceReq(nullptr), livoxProto1_destroyDeviceReq(nullptr), livoxProto1_device_enablePcloudDataReq(nullptr), livoxProto1_device_disablePcloudDataReq(nullptr), livoxProto1_device_getReturnModeReq(nullptr), livoxProto1_getPcloudDataFdDesc(nullptr) {} // LivoxProto1DllState DlCloser implementation void LivoxProto1DllState::DlCloser(void* handle) { if (handle) { dlclose(handle); } } LivoxProto1DllState livoxProto1; // Continuation classes for async operations class AttachDeviceReq : public smo::NonPostedAsynchronousContinuation { public: AttachDeviceReq( const std::shared_ptr& spec, smo::Callback cb) : smo::NonPostedAsynchronousContinuation( std::move(cb)), spec(spec) {} public: const std::shared_ptr spec; std::shared_ptr stimProducer; std::shared_ptr deviceTmp; private: std::unique_ptr delayTimer; // Helper method to ensure StimBuffer is attached // Returns true if successful, false on error bool ensureStimBufferAttached(std::shared_ptr context) { if (!context->stimProducer) { std::cerr << __func__ << ": stimProducer is null" << std::endl; return false; } // Parse histbuffMs int histbuffMs = parseHistbuffMs(context->spec); // Call getOrCreateAttachedStimulusBuffer (may throw, catch and return failure) try { context->stimProducer->getOrCreateAttachedStimulusBuffer( context->spec, histbuffMs); } catch (const std::exception& e) { std::cerr << __func__ << ": Failed to create StimBuffer: " << e.what() << ". Producer is committed, DeviceReattacher will retry." << std::endl; // Return false so DeviceReattacher can retry later return false; } return true; } public: void attachDeviceReq1( std::shared_ptr context, bool success, std::shared_ptr dev) { if (!success || !dev) { std::cerr << __func__ << ": Failed to create/find Livox device: " << context->spec->deviceSelector << std::endl; context->callOriginalCb(false, context->spec); return; } // Stash device pointer until after getReturnMode succeeds context->deviceTmp = dev; if (1 || smoHooksPtr->OptionParser_getOptions().verbose) { std::cout << __func__ << ": Successfully attached/found Livox " "device: " << context->spec->deviceSelector << " (ID: " << context->spec->deviceIdentifier << ")\n"; } /* Delay here because getOrCreate just sent HandshakeReq, so device * may not yet be ready for another command. */ context->delayedGetReturnMode(context); } void delayedGetReturnMode( std::shared_ptr context) { // Initialize timer with LivoxGen1 metadata io_service delayTimer = std::make_unique( smoThreadingModelDesc.componentThread->getIoService()); delayTimer->expires_from_now(boost::posix_time::milliseconds(5)); delayTimer->async_wait( std::bind( &AttachDeviceReq::attachDeviceReq2, context.get(), context, std::placeholders::_1)); } void attachDeviceReq2( std::shared_ptr context, const boost::system::error_code& error) { if (error) { std::cerr << __func__ << ": Timer error: " << error.message() << std::endl; context->callOriginalCb(false, context->spec); return; } (*livoxProto1.livoxProto1_device_getReturnModeReq)( context->deviceTmp, {context, std::bind( &AttachDeviceReq::attachDeviceReq3_doCreateStimProducer, context.get(), context, std::placeholders::_1, std::placeholders::_2)}); } void attachDeviceReq3_doCreateStimProducer( std::shared_ptr context, bool success, uint8_t mode) { if (!success) { std::cerr << __func__ << ": Failed to get return mode for dev " << context->spec->deviceSelector << std::endl; context->callOriginalCb(false, context->spec); return; } if (1 || smoHooksPtr->OptionParser_getOptions().verbose) { std::cout << __func__ << ": Got return mode (" << (int)mode << ") for device: " << context->spec->deviceSelector << std::endl; } /* Check if PcloudStimulusProducer already exists * (race condition or double-add) */ auto existingProducer = getStimulusProducer(context->spec); if (existingProducer) { throw std::runtime_error( std::string(__func__) + ": PcloudStimulusProducer already " "exists for device " + context->spec->deviceSelector + " " "(race condition or double-add)"); } // Create & add PcloudStimulusProducer to collection since dev now ready PcloudStimulusProducer::PcloudFormatDesc formatDesc; formatDesc.format = PcloudStimulusProducer::PcloudFormatDesc::Format ::XYZI; // Parse n-dgrams-per-frame from stim-buff-api params (default: 84) size_t nDgramsPerFrame = parseNDgramsPerFrame(context->spec); auto pcloudDataProducer = std::make_shared( context->spec, context->deviceTmp, formatDesc, nDgramsPerFrame); context->stimProducer = pcloudDataProducer; context->deviceTmp->nAttachedStimulusProducers++; if (context->deviceTmp->nAttachedStimulusProducers > 2) { throw std::runtime_error( std::string(__func__) + ": Each LivoxGen1 device can only have " "at most two StimulusProducers attached to it. Found " + std::to_string( context->deviceTmp->nAttachedStimulusProducers) + "."); } attachedStimulusProducers.push_back(pcloudDataProducer); if (false /*attachedStimulusProducers.size() >= 2*nDevicesKnownToGen1Lib */) { /** TODO: * It would be nice to add an nDevicesKnownToGen1Lib counter, and * then add a check here to ensure that * attachedStimulusProducers.size() is always less than or equal to * 2*nDevicesKnownToGen1Lib. * * (2 stim producers per device). */ #if 0 throw std::runtime_error( std::string(__func__) + ": Number of StimulusProducers attached " "to LivoxGen1 devices known to the library (" + std::to_string(attachedStimulusProducers.size()) + ") is greater than " "expected. Lib knows about " + std::to_string(nDevicesKnownToGen1Lib) + " devices, " "so there should be at most " + std::to_string(2*nDevicesKnownToGen1Lib) + " StimulusProducers attached to devices."); #endif } pcloudDataProducer->start(); // Ensure StimBuffer is attached attachDeviceReq4_doCreateStimBuff_maybeDirectlyCalled(context); } // Ensure StimBuffer is attached void attachDeviceReq4_doCreateStimBuff_maybeDirectlyCalled( std::shared_ptr context ) { // Ensure StimBuffer is attached if (!ensureStimBufferAttached(context)) { context->callOriginalCb(false, context->spec); return; } // Continue to enable pcloud data if needed attachDeviceReq5_doEnablePcloudData_maybeDirectlyCalled(context); } // Enable pcloud data if needed void attachDeviceReq5_doEnablePcloudData_maybeDirectlyCalled( std::shared_ptr context ) { if (!context->stimProducer || !context->stimProducer->device) { std::cerr << __func__ << ": stimProducer or device is null" << std::endl; context->callOriginalCb(false, context->spec); return; } /* Enable pcloud data. Don't need delay since no commands were * sent to device prior to us reaching here (or delay already handled). */ (*livoxProto1.livoxProto1_device_enablePcloudDataReq)( context->stimProducer->device, {context, std::bind( &AttachDeviceReq::attachDeviceReq6, context.get(), context, std::placeholders::_1)}); } void attachDeviceReq6( std::shared_ptr context, bool success) { if (!success) { std::cerr << __func__ << ": Failed to enable pcloud data for dev " << context->spec->deviceSelector << std::endl; context->callOriginalCb(false, context->spec); return; } if (1 || smoHooksPtr->OptionParser_getOptions().verbose) { std::cout << __func__ << ": Enabled pcloud data for device: " << context->spec->deviceSelector << std::endl; } context->callOriginalCb(success, context->spec); } }; class DetachDeviceReq : public smo::NonPostedAsynchronousContinuation { public: DetachDeviceReq( const std::shared_ptr& spec, const std::shared_ptr& stimBuffer, smo::Callback cb) : smo::NonPostedAsynchronousContinuation( std::move(cb)), spec(spec), stimBuffer(stimBuffer) {} public: const std::shared_ptr spec; std::shared_ptr stimBuffer; private: std::unique_ptr delayTimer; public: void detachDeviceReq1( std::shared_ptr context, bool success) { if (!success) { std::cerr << __func__ << ": Failed to disable pcloud data for " "stim producer " << context->spec->deviceSelector << std::endl; // Fallthrough. } // Add 5ms delay before destroying device context->delayedDestroyDevice(context); } // Helper method to delay and then call destroyDeviceReq void delayedDestroyDevice( std::shared_ptr context) { // Initialize timer with LivoxGen1 metadata io_service delayTimer = std::make_unique( smoThreadingModelDesc.componentThread->getIoService()); delayTimer->expires_from_now(boost::posix_time::milliseconds(5)); delayTimer->async_wait( std::bind( &DetachDeviceReq::detachDeviceReq1_delayed, context.get(), context, std::placeholders::_1)); } // Callback for the delayed destroyDeviceReq void detachDeviceReq1_delayed( std::shared_ptr context, const boost::system::error_code& error) { if (error) { std::cerr << __func__ << ": Timer error: " << error.message() << std::endl; // Fallthrough. } // Remove StimBuffer from collection if it exists if (!context->stimBuffer) { throw std::runtime_error(std::string(__func__) + ": stimBuffer (API: " + context->spec->stimBuffApi + ") " + "is missing in detachDeviceReq1_delayed " + "for device " + context->spec->deviceSelector); } // Get the producer from the buffer's parent auto& stimProducer = dynamic_cast( context->stimBuffer->parent); auto it = std::find( stimProducer.attachedStimulusBuffers.begin(), stimProducer.attachedStimulusBuffers.end(), context->stimBuffer); if (it != stimProducer.attachedStimulusBuffers.end()) { stimProducer.attachedStimulusBuffers.erase(it); } // Clear specialized buffer members if they match if (stimProducer.xyzStimulusBuffer == context->stimBuffer) { stimProducer.xyzStimulusBuffer.reset(); } if (stimProducer.iStimulusBuffer == context->stimBuffer) { stimProducer.iStimulusBuffer.reset(); } if (stimProducer.ambienceStimulusBuffer == context->stimBuffer) { stimProducer.ambienceStimulusBuffer.reset(); } // Check if StimProducer has other buffers if (!stimProducer.attachedStimulusBuffers.empty()) { // Other buffers exist - just remove this buffer, done context->callOriginalCb(true, context->spec); return; } // No other buffers - stop and remove StimProducer stimProducer.stop(); // Remove stimulus producer from collection before destroying device stimProducer.device->nAttachedStimulusProducers--; // Find and remove the producer from the collection by comparing device auto it2 = std::find_if( attachedStimulusProducers.begin(), attachedStimulusProducers.end(), [&stimProducer](const std::shared_ptr& p) { /** FIXME: * When we implement the ImuStimulusProducer, we need to make * sure we handle that properly here. */ auto pcloudProd = std::dynamic_pointer_cast(p); return pcloudProd && pcloudProd->device == stimProducer.device; }); if (it2 != attachedStimulusProducers.end()) { attachedStimulusProducers.erase(it2); } (*livoxProto1.livoxProto1_destroyDeviceReq)( stimProducer.device, {context, std::bind( &DetachDeviceReq::detachDeviceReq2, context.get(), context, std::placeholders::_1)}); } void detachDeviceReq2( std::shared_ptr context, bool success) { if (!success) { std::cerr << __func__ << ": Failed to destroy dev " "device " << context->spec->deviceSelector << " for stim " "producer.\n"; /** NOTE: * There's a decent argument for falling through here and still * removing the stimulus producer from attachedStimulusProducers. */ context->callOriginalCb(false, context->spec); return; } if (1 || smoHooksPtr->OptionParser_getOptions().verbose) { std::cout << __func__ << ": Successfully detached pcloud stim " "producer for device " << context->spec->deviceSelector << " and possibly also destroyed device.\n"; } context->callOriginalCb(success, context->spec); } }; // Callback function declarations extern "C" sal_mlo_initializeIndFn livoxGen1_initializeInd; extern "C" sal_mlo_finalizeIndFn livoxGen1_finalizeInd; extern "C" sal_mlo_attachDeviceReqFn livoxGen1_attachDeviceReq; extern "C" sal_mlo_detachDeviceReqFn livoxGen1_detachDeviceReq; // Stim Buff API descriptor static const StimBuffApiDesc livoxGen1ApiDesc = { .name = "livoxGen1", .exportedQualeIfaceApis = { {.name = "mesh"}, {.name = "pcloudIntensity"}, {.name = "pcloudAmbience"}, {.name = "gyro"}, {.name = "accel"} }, .sal_mgmt_libOps = { .initializeInd = livoxGen1_initializeInd, .finalizeInd = livoxGen1_finalizeInd, .attachDeviceReq = livoxGen1_attachDeviceReq, .detachDeviceReq = livoxGen1_detachDeviceReq } }; // Callback function implementations extern "C" int livoxGen1_initializeInd(void) { if (!smoHooksPtr) { throw std::runtime_error(std::string(__func__) + ": SMO hooks " "pointers not filled in."); } // Load LivoxProto1 library auto libPath = smoHooksPtr->searchForLibInSmoSearchPaths( "liblivoxProto1.so"); livoxProto1.dlopenHandle.reset(dlopen( libPath.value_or("liblivoxProto1.so").c_str(), RTLD_LAZY)); if (!livoxProto1.dlopenHandle) { throw std::runtime_error( std::string(__func__) + ": Failed to load LivoxProto1 library: " + (dlerror() ? dlerror() : "unknown error")); } // Get LivoxProto1 library functions livoxProto1.livoxProto1_main = reinterpret_cast( dlsym(livoxProto1.dlopenHandle.get(), "livoxProto1_main")); livoxProto1.livoxProto1_exit = reinterpret_cast( dlsym(livoxProto1.dlopenHandle.get(), "livoxProto1_exit")); livoxProto1.livoxProto1_getOrCreateDeviceReq = reinterpret_cast< livoxProto1_getOrCreateDeviceReqFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_getOrCreateDeviceReq")); livoxProto1.livoxProto1_destroyDeviceReq = reinterpret_cast< livoxProto1_destroyDeviceReqFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_destroyDeviceReq")); livoxProto1.livoxProto1_device_enablePcloudDataReq = reinterpret_cast< livoxProto1_device_enablePcloudDataReqFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_device_enablePcloudDataReq")); livoxProto1.livoxProto1_device_disablePcloudDataReq = reinterpret_cast< livoxProto1_device_disablePcloudDataReqFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_device_disablePcloudDataReq")); livoxProto1.livoxProto1_device_getReturnModeReq = reinterpret_cast< livoxProto1_device_getReturnModeReqFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_device_getReturnModeReq")); livoxProto1.livoxProto1_getPcloudDataFdDesc = reinterpret_cast< livoxProto1_getPcloudDataFdDescFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_getPcloudDataFdDesc")); if (!livoxProto1.livoxProto1_main || !livoxProto1.livoxProto1_exit || !livoxProto1.livoxProto1_getOrCreateDeviceReq || !livoxProto1.livoxProto1_destroyDeviceReq || !livoxProto1.livoxProto1_device_enablePcloudDataReq || !livoxProto1.livoxProto1_device_disablePcloudDataReq || !livoxProto1.livoxProto1_device_getReturnModeReq || !livoxProto1.livoxProto1_getPcloudDataFdDesc) { throw std::runtime_error( std::string(__func__) + ": Failed to get LivoxProto1 library functions"); } // Call LivoxProto1 library main function (*livoxProto1.livoxProto1_main)( smoThreadingModelDesc.componentThread, *smoHooksPtr); return 0; // Success } extern "C" int livoxGen1_finalizeInd(void) { attachedStimulusProducers.clear(); // Call LivoxProto1 library exit function if (livoxProto1.livoxProto1_exit) { (*livoxProto1.livoxProto1_exit)(); } livoxProto1.dlopenHandle.reset(nullptr); livoxProto1 = LivoxProto1DllState(); return 0; // Success } extern "C" void livoxGen1_attachDeviceReq( const std::shared_ptr& desc, const std::shared_ptr& componentThread, Callback cb ) { if (!livoxProto1.livoxProto1_getOrCreateDeviceReq) { throw std::runtime_error( std::string(__func__) + ": LivoxProto1 getOrCreateDevice function " "not available"); } auto request = std::make_shared(desc, cb); // Case 1: Check if StimBuffer already exists auto stimProducer = std::dynamic_pointer_cast( getStimulusProducer(desc)); if (stimProducer) { auto existingBuffer = stimProducer->getAttachedStimulusBuffer(desc); if (existingBuffer) { // StimBuffer exists, check if pcloud data is active if (stimProducer->device && stimProducer->device->pcloudDataActive) { // Both StimBuffer and pcloud data are active, early return with success request->callOriginalCb(true, request->spec); return; } // StimBuffer exists but pcloud data is not active, enable it request->stimProducer = stimProducer; request->attachDeviceReq5_doEnablePcloudData_maybeDirectlyCalled( request); return; } else { // StimProducer exists, StimBuffer doesn't request->stimProducer = stimProducer; // Ensure StimBuffer is attached and enable pcloud data if needed request->attachDeviceReq4_doCreateStimBuff_maybeDirectlyCalled( request); return; } } // StimProducer doesn't exist - need to create device first // Parse integer parameters from provider params with defaults /** EXPLANATION: * We may want to add a new param here called "command-delay-ms" to control * the delay we insert between commands sent to the device. 5ms has been * shown to be sufficient for the Livox Avia. */ /* The Livox Avia will generally respond to a handshake request within * 5ms. */ int commandTimeoutMs = 5; // Default: 5ms /* Based on testing on a Livox Avia, the device will generally resume * sending broadcast advertisement dgrams after about 5 seconds at most. * Generally, it will resume sending them within 1-2 seconds. */ int retryDelayMs = 5250; // Default: 5250ms uint8_t smoSubnetNbits = 24; // Default: /24 subnet uint16_t dataPort = 56000; // Default data port uint16_t cmdPort = 56001; // Default command port uint16_t imuPort = 56002; // Default IMU port // Default: empty string (will trigger IP auto-detection) std::string smoIp = ""; // Parse optional integer parameters from provider params for (const auto& param : desc->providerParams) { if (param.first == "cmd-timeout-ms") { commandTimeoutMs = smo::device::DeviceAttachmentSpec ::parseRequiredParamAsInt( desc->providerParams, "cmd-timeout-ms"); } else if (param.first == "command-timeout-ms") { commandTimeoutMs = smo::device::DeviceAttachmentSpec ::parseRequiredParamAsInt( desc->providerParams, "command-timeout-ms"); } else if (param.first == "retry-delay-ms") { retryDelayMs = smo::device::DeviceAttachmentSpec ::parseRequiredParamAsInt( desc->providerParams, "retry-delay-ms"); } else if (param.first == "smo-subnet-nbits") { smoSubnetNbits = static_cast( smo::device::DeviceAttachmentSpec ::parseRequiredParamAsInt( desc->providerParams, "smo-subnet-nbits")); } else if (param.first == "data-port") { dataPort = static_cast( smo::device::DeviceAttachmentSpec ::parseRequiredParamAsInt(desc->providerParams, "data-port")); } else if (param.first == "cmd-port") { cmdPort = static_cast( smo::device::DeviceAttachmentSpec ::parseRequiredParamAsInt(desc->providerParams, "cmd-port")); } else if (param.first == "imu-port") { imuPort = static_cast( smo::device::DeviceAttachmentSpec ::parseRequiredParamAsInt(desc->providerParams, "imu-port")); } else if (param.first == "smo-ip") { if (param.second.empty()) { throw std::runtime_error( std::string(__func__) + ": smo-ip parameter is empty"); } if (param.second.find('.') == std::string::npos || std::count(param.second.begin(), param.second.end(), '.') != 3) { throw std::runtime_error( std::string(__func__) + ": smo-ip parameter is not an " "IPv4 address"); } smoIp = param.second; } else { throw std::runtime_error( std::string(__func__) + ": Unknown provider parameter: " + param.first); } } (*livoxProto1.livoxProto1_getOrCreateDeviceReq)( desc->deviceSelector, // deviceIdentifier (broadcast code) componentThread, commandTimeoutMs, retryDelayMs, smoIp, smoSubnetNbits, dataPort, cmdPort, imuPort, {request, std::bind( &AttachDeviceReq::attachDeviceReq1, request.get(), request, std::placeholders::_1, std::placeholders::_2)}); } extern "C" void livoxGen1_detachDeviceReq( const std::shared_ptr& desc, Callback cb ) { // Case 1: Check if StimBuffer doesn't exist (early return) auto stimProducerBase = getStimulusProducer(desc); if (!stimProducerBase) { // StimProducer doesn't exist, nothing to detach - success cb.callbackFn(true, desc); return; } auto stimProducer = std::dynamic_pointer_cast( stimProducerBase); if (!stimProducer) { throw std::runtime_error(std::string(__func__) + ": Failed to cast StimulusProducer to PcloudStimulusProducer " "for device " + desc->deviceSelector); } // Check if StimBuffer exists auto stimBuffer = stimProducer->getAttachedStimulusBuffer(desc); if (!stimBuffer) { // StimBuffer doesn't exist, nothing to detach - success cb.callbackFn(true, desc); return; } // Case 2: StimBuffer exists - proceed with detach auto request = std::make_shared( desc, stimBuffer, cb); // Disable point cloud data first (*livoxProto1.livoxProto1_device_disablePcloudDataReq)( stimProducer->device, {request, std::bind( &DetachDeviceReq::detachDeviceReq1, request.get(), request, std::placeholders::_1)}); } // Exported function extern "C" smo::stim_buff::SMO_GET_STIM_BUFF_API_DESC_FN_TYPEDEF SMO_GET_STIM_BUFF_API_DESC_FN_NAME; const smo::stim_buff::StimBuffApiDesc& SMO_GET_STIM_BUFF_API_DESC_FN_NAME( const smo::stim_buff::SmoCallbacks& callbacks, const smo::stim_buff::SmoThreadingModelDesc& threadingModel) { smoHooksPtr = &callbacks; smoThreadingModelDesc = threadingModel; return livoxGen1ApiDesc; } } // namespace stim_buff } // namespace smo