#include #include #include #include #include #include #include #include #include #include "livoxGen1Internal.h" namespace smo::stim_buff { constexpr int LIVOX_GEN1_DEVICE_COMMAND_DELAY_MS = 5; // Salmanoff hooks, obtained from SMO_GET_STIM_BUFF_API_DESC_FN_NAME(). const SmoCallbacks *smoHooksPtr = nullptr; SmoThreadingModelDesc smoThreadingModelDesc; // Local collection of stimulus producers std::vector> managedStimulusProducers; LivoxProto1DllState::LivoxProto1DllState() : dlopenHandle(nullptr, DlCloser), livoxProto1_main(nullptr), livoxProto1_exit(nullptr), livoxProto1_getOrCreateDeviceCReq(nullptr), livoxProto1_destroyDeviceCReq(nullptr), livoxProto1_device_enablePcloudDataCReq(nullptr), livoxProto1_device_disablePcloudDataCReq(nullptr), livoxProto1_device_getReturnModeCReq(nullptr), livoxProto1_getPcloudDataFdDesc(nullptr) {} void LivoxProto1DllState::DlCloser(void *handle) { if (handle) { dlclose(handle); } } LivoxProto1DllState livoxProto1; std::shared_ptr getStimulusProducer( const std::shared_ptr &spec) { for (const auto &stimProducer : managedStimulusProducers) { if (livoxProto1::comms::deviceIdentifiersEqual( stimProducer->deviceAttachmentSpec->deviceSelector, spec->deviceSelector) && stimProducer->exportsQualeIfaceApi(spec->qualeIfaceApi)) { return stimProducer; } } return nullptr; } size_t parseNDgramsPerFrame( const std::shared_ptr &spec) { const std::vector nDgramsPerFrameParamNames = { "n-dgrams-per-frame", "num-dgrams-per-frame" }; return static_cast( device::DeviceAttachmentSpec::parseOptionalParamAsIntWithSynonyms( spec->stimBuffApiParams, nDgramsPerFrameParamNames, 84)); } LivoxProviderParams parseLivoxProviderParams( const std::shared_ptr &desc) { using device::DeviceAttachmentSpec; const auto& providerParams = desc->providerParams; static const std::vector cmdTimeoutSynonyms = { "cmd-timeout-ms", "command-timeout-ms", }; static const std::vector retryDelaySynonyms = { "retry-delay-ms", }; static const std::vector smoSubnetSynonyms = { "smo-subnet-nbits", }; static const std::vector dataPortSynonyms = { "data-port", }; static const std::vector cmdPortSynonyms = { "cmd-port", }; static const std::vector imuPortSynonyms = { "imu-port", }; static const std::vector smoIpSynonyms = { "smo-ip", }; DeviceAttachmentSpec::rejectUnknownParams( providerParams, std::string(__func__) + ": Unknown provider parameter: ", cmdTimeoutSynonyms, retryDelaySynonyms, smoSubnetSynonyms, dataPortSynonyms, cmdPortSynonyms, imuPortSynonyms, smoIpSynonyms); LivoxProviderParams params; if (const auto matchedParam = DeviceAttachmentSpec::findOptionalParamWithSynonyms( providerParams, cmdTimeoutSynonyms)) { params.commandTimeoutMs = DeviceAttachmentSpec::parseParamValueAsInt( matchedParam->first, matchedParam->second); } if (const auto matchedParam = DeviceAttachmentSpec::findOptionalParamWithSynonyms( providerParams, retryDelaySynonyms)) { params.retryDelayMs = DeviceAttachmentSpec::parseParamValueAsInt( matchedParam->first, matchedParam->second); } if (const auto matchedParam = DeviceAttachmentSpec::findOptionalParamWithSynonyms( providerParams, smoSubnetSynonyms)) { params.smoSubnetNbits = static_cast( DeviceAttachmentSpec::parseParamValueAsInt( matchedParam->first, matchedParam->second)); } if (const auto matchedParam = DeviceAttachmentSpec::findOptionalParamWithSynonyms( providerParams, dataPortSynonyms)) { params.dataPort = static_cast( DeviceAttachmentSpec::parseParamValueAsInt( matchedParam->first, matchedParam->second)); } if (const auto matchedParam = DeviceAttachmentSpec::findOptionalParamWithSynonyms( providerParams, cmdPortSynonyms)) { params.cmdPort = static_cast( DeviceAttachmentSpec::parseParamValueAsInt( matchedParam->first, matchedParam->second)); } if (const auto matchedParam = DeviceAttachmentSpec::findOptionalParamWithSynonyms( providerParams, imuPortSynonyms)) { params.imuPort = static_cast( DeviceAttachmentSpec::parseParamValueAsInt( matchedParam->first, matchedParam->second)); } if (const auto matchedParam = DeviceAttachmentSpec::findOptionalParamWithSynonyms( providerParams, smoIpSynonyms)) { if (matchedParam->second.empty()) { throw std::runtime_error( std::string(__func__) + ": smo-ip parameter is empty"); } if (matchedParam->second.find('.') == std::string::npos || std::count( matchedParam->second.begin(), matchedParam->second.end(), '.') != 3) { throw std::runtime_error( std::string(__func__) + ": smo-ip parameter is not an IPv4 address"); } params.smoIp = matchedParam->second; } return params; } constexpr size_t MAX_STIM_PRODUCERS_PER_DEVICE = 2; // Helper method to ensure StimBuffer is attached // Returns true if successful, false on error bool ensureStimBufferAttachedWithoutDuplicates( const std::shared_ptr &stimProducer, const std::shared_ptr &spec) { if (!stimProducer) { std::cerr << __func__ << ": stimProducer is null" << std::endl; return false; } // Check for duplicate qualeIfaceApi const std::string &qualeIfaceApi = spec->qualeIfaceApi; if (stimProducer->hasBufferWithQualeIfaceApi(qualeIfaceApi)) { std::cerr << __func__ << ": Buffer with qualeIfaceApi '" << qualeIfaceApi << "' already exists for this producer. " "Each producer can only have one buffer per qualeIfaceApi." << std::endl; return false; } // Call getOrCreateAttachedStimulusBuffer (may throw, catch and return failure) try { stimProducer->getOrCreateAttachedStimulusBuffer(spec); } 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 caller can handle error callback return false; } return true; } void addManagedStimulusProducer( const std::shared_ptr &producer, const std::shared_ptr &device) { if (!producer || !device) { throw std::runtime_error( std::string(__func__) + ": producer and device must be non-null"); } device->nAttachedStimulusProducers++; if (device->nAttachedStimulusProducers > MAX_STIM_PRODUCERS_PER_DEVICE) { device->nAttachedStimulusProducers--; throw std::runtime_error( std::string(__func__) + ": Each LivoxGen1 device can only have " "at most two StimulusProducers attached to it. Found " + std::to_string(device->nAttachedStimulusProducers + 1) + "."); } managedStimulusProducers.push_back(producer); if (false /*managedStimulusProducers.size() >= 2*nDevicesKnownToGen1Lib */) { /** TODO: * It would be nice to add an nDevicesKnownToGen1Lib counter, and * then add a check here to ensure that * managedStimulusProducers.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(managedStimulusProducers.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 } producer->start(); } sscl::co::ViralNonPostingInvoker removeManagedStimulusProducerIfUnused( const std::shared_ptr &producer, const std::shared_ptr &/*componentThread*/) { if (!producer) { co_return true; } if (!producer->attachedStimulusBuffers.empty()) { co_return true; } // No other buffers - stop and remove StimProducer producer->stop(); auto it = std::find_if( managedStimulusProducers.begin(), managedStimulusProducers.end(), [&producer](const std::shared_ptr &candidate) { /** FIXME: * When we implement the ImuStimulusProducer, we need to make * sure we handle that properly here. */ auto pcloudProducer = std::dynamic_pointer_cast(candidate); return pcloudProducer && pcloudProducer == producer; }); if (it != managedStimulusProducers.end()) { managedStimulusProducers.erase(it); } if (!producer->device) { co_return true; } if (producer->device->nAttachedStimulusProducers > 0) { producer->device->nAttachedStimulusProducers--; } if (producer->device->nAttachedStimulusProducers > 0) { co_return true; } const bool destroyed = co_await (*livoxProto1.livoxProto1_destroyDeviceCReq)( producer->device); if (!destroyed) { std::cerr << __func__ << ": Failed to destroy dev " "device " << producer->deviceAttachmentSpec->deviceSelector << " for stim producer.\n"; co_return false; } co_return true; } namespace { void stopAllManagedProducersBeforeFinalize() { for (const auto &producer : managedStimulusProducers) { if (!producer) { continue; } producer->stop(); } } bool validateAttachRequest( const std::shared_ptr &desc) { // Validate qualeIfaceApi const std::string &qualeIfaceApi = desc->qualeIfaceApi; if (qualeIfaceApi == "gyro" || qualeIfaceApi == "accel") { // These are for ImuStimulusProducer (not yet implemented) std::cerr << __func__ << ": qualeIfaceApi '" << qualeIfaceApi << "' requires ImuStimulusProducer which is not yet implemented" << std::endl; return false; } try { smo::intrin::validateNoIntrinParamsOnQualeIface( desc->qualeIfaceApi, desc->qualeIfaceApiParams); } catch (const std::runtime_error &e) { std::cerr << __func__ << ": " << e.what() << std::endl; return false; } if (!PcloudStimulusProducer::supportsQualeIfaceApi(qualeIfaceApi)) { // Unknown qualeIfaceApi std::cerr << __func__ << ": Unsupported qualeIfaceApi '" << qualeIfaceApi << "' for LivoxGen1. " "Supported values: mesh, pcloudIntensity, " "pcloudLightAmbience, pcloudDarkAmbience" << std::endl; return false; } return true; } sscl::co::ViralNonPostingInvoker enablePcloudDataForAttach( const std::shared_ptr &desc, const std::shared_ptr &/*componentThread*/, const std::shared_ptr &device) { /* Enable pcloud data. Don't need delay since no commands were * sent to device prior to us reaching here (or delay already handled). */ const bool enabled = co_await (*livoxProto1.livoxProto1_device_enablePcloudDataCReq)( device); if (!enabled) { std::cerr << __func__ << ": Failed to enable pcloud data for dev " << desc->deviceSelector << std::endl; co_return StimBuffDeviceOpResult{false, desc}; } if (smoHooksPtr->OptionParser_getOptions().verbose) { std::cout << __func__ << ": Enabled pcloud data for device: " << desc->deviceSelector << std::endl; } co_return StimBuffDeviceOpResult{true, desc}; } sscl::co::ViralNonPostingInvoker attachBufferAndEnablePcloud( const std::shared_ptr &desc, const std::shared_ptr &componentThread, const std::shared_ptr &stimProducer) { // Ensure StimBuffer is attached if (!ensureStimBufferAttachedWithoutDuplicates(stimProducer, desc)) { co_return StimBuffDeviceOpResult{false, desc}; } // Continue to enable pcloud data if needed co_return co_await enablePcloudDataForAttach( desc, componentThread, stimProducer->device); } sscl::co::ViralNonPostingInvoker attachToExistingProducer( const std::shared_ptr &desc, const std::shared_ptr &componentThread, const std::shared_ptr &stimProducer) { // Case 1: Check if StimBuffer already exists 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 co_return StimBuffDeviceOpResult{true, desc}; } // StimBuffer exists but pcloud data is not active, enable it co_return co_await enablePcloudDataForAttach( desc, componentThread, stimProducer->device); } /** EXPLANATION: * StimProducer exists, StimBuffer doesn't (DASpec doesn't match) * Check if producer already has a buffer with the requested * qualeIfaceApi but different DASpec - this is not allowed. */ if (stimProducer->hasBufferWithQualeIfaceApi(desc->qualeIfaceApi)) { std::cerr << __func__ << ": Producer already has a buffer with " "qualeIfaceApi '" << desc->qualeIfaceApi << "' but with a different DeviceAttachmentSpec. " "A single LivoxGen1 device cannot support multiple DASpecs " "with the same qualeIfaceApi." << std::endl; co_return StimBuffDeviceOpResult{false, desc}; } // Ensure StimBuffer is attached and enable pcloud data if needed co_return co_await attachBufferAndEnablePcloud( desc, componentThread, stimProducer); } sscl::co::ViralNonPostingInvoker attachByCreatingProducer( const std::shared_ptr &desc, const std::shared_ptr &componentThread) { // 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. */ /* 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. */ const LivoxProviderParams params = parseLivoxProviderParams(desc); LivoxProto1GetOrCreateDeviceResult deviceResult = co_await (*livoxProto1.livoxProto1_getOrCreateDeviceCReq)( desc->deviceSelector, componentThread, params.commandTimeoutMs, params.retryDelayMs, params.smoIp, params.smoSubnetNbits, params.dataPort, params.cmdPort, params.imuPort); if (!deviceResult.success || !deviceResult.device) { std::cerr << __func__ << ": Failed to create/find Livox device: " << desc->deviceSelector << std::endl; co_return StimBuffDeviceOpResult{false, desc}; } // Stash device pointer until after getReturnMode succeeds if (smoHooksPtr->OptionParser_getOptions().verbose) { std::cout << __func__ << ": Successfully attached/found Livox " "device: " << desc->deviceSelector << " (ID: " << desc->deviceIdentifier << ")\n"; } /* Delay here because getOrCreate just sent HandshakeReq, so device * may not yet be ready for another command. */ // Initialize timer with LivoxGen1 metadata io_context boost::asio::deadline_timer commandDelayTimer( componentThread->getIoContext()); const bool delayOk = co_await adapters::boostAsio::getDeadlineTimerAReqAwaiter( componentThread->getIoContext(), commandDelayTimer, boost::posix_time::milliseconds(LIVOX_GEN1_DEVICE_COMMAND_DELAY_MS)); if (!delayOk) { std::cerr << __func__ << ": Delay timer failed before getReturnMode\n"; co_return StimBuffDeviceOpResult{false, desc}; } LivoxProto1GetReturnModeResult returnModeResult = co_await (*livoxProto1.livoxProto1_device_getReturnModeCReq)( deviceResult.device); if (!returnModeResult.success) { std::cerr << __func__ << ": Failed to get return mode for dev " << desc->deviceSelector << std::endl; co_return StimBuffDeviceOpResult{false, desc}; } if (smoHooksPtr->OptionParser_getOptions().verbose) { std::cout << __func__ << ": Got return mode (" << static_cast(returnModeResult.returnMode) << ") for device: " << desc->deviceSelector << std::endl; } /* Check if PcloudStimulusProducer already exists * (race condition or double-add) */ auto existingProducer = getStimulusProducer(desc); if (existingProducer) { throw std::runtime_error( std::string(__func__) + ": PcloudStimulusProducer already " "exists for device " + desc->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) const size_t nDgramsPerFrame = parseNDgramsPerFrame(desc); auto pcloudDataProducer = std::make_shared( desc, deviceResult.device, formatDesc, nDgramsPerFrame); addManagedStimulusProducer(pcloudDataProducer, deviceResult.device); // Ensure StimBuffer is attached co_return co_await attachBufferAndEnablePcloud( desc, componentThread, pcloudDataProducer); } } // namespace sscl::co::DynamicViralPostingInvoker livoxGen1_attachDeviceCReq( [[maybe_unused]] sscl::co::ExplicitPostTarget postTarget, const std::shared_ptr &desc, const std::shared_ptr &componentThread) { (void)postTarget; if (!livoxProto1.livoxProto1_getOrCreateDeviceCReq) { throw std::runtime_error( std::string(__func__) + ": LivoxProto1 getOrCreateDevice function " "not available"); } if (!validateAttachRequest(desc)) { co_return StimBuffDeviceOpResult{false, desc}; } auto stimProducer = std::dynamic_pointer_cast( getStimulusProducer(desc)); if (stimProducer) { co_return co_await attachToExistingProducer( desc, componentThread, stimProducer); } co_return co_await attachByCreatingProducer(desc, componentThread); } sscl::co::DynamicViralPostingInvoker livoxGen1_detachDeviceCReq( [[maybe_unused]] sscl::co::ExplicitPostTarget postTarget, const std::shared_ptr &desc) { (void)postTarget; // Case 1: Check if StimBuffer doesn't exist (early return) auto stimProducerBase = getStimulusProducer(desc); if (!stimProducerBase) { // StimProducer doesn't exist, nothing to detach - success co_return StimBuffDeviceOpResult{true, desc}; } 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 co_return StimBuffDeviceOpResult{true, desc}; } // Case 2: StimBuffer exists - proceed with detach auto requestComponentThread = stimProducer->device->componentThread; // Remove StimBuffer from collection if it exists stimProducer->destroyAttachedStimulusBuffer(stimBuffer); // Check if StimProducer has other buffers if (!stimProducer->attachedStimulusBuffers.empty()) { // Other buffers exist - just remove this buffer, done co_return StimBuffDeviceOpResult{true, desc}; } // Last buffer on producer: disable pcloud before tearing down device // Disable point cloud data first const bool disabled = co_await (*livoxProto1.livoxProto1_device_disablePcloudDataCReq)( stimProducer->device); if (!disabled) { std::cerr << __func__ << ": Failed to disable pcloud data for " "stim producer " << desc->deviceSelector << std::endl; // Fallthrough. } // Add 5ms delay before destroying device // Helper method to delay and then call destroyDeviceReq // Initialize timer with LivoxGen1 metadata io_context boost::asio::deadline_timer commandDelayTimer( requestComponentThread->getIoContext()); co_await adapters::boostAsio::getDeadlineTimerAReqAwaiter( requestComponentThread->getIoContext(), commandDelayTimer, boost::posix_time::milliseconds(LIVOX_GEN1_DEVICE_COMMAND_DELAY_MS)); const bool removedOk = co_await removeManagedStimulusProducerIfUnused( stimProducer, requestComponentThread); if (!removedOk) { co_return StimBuffDeviceOpResult{false, desc}; } if (smoHooksPtr->OptionParser_getOptions().verbose) { std::cout << __func__ << ": Successfully detached pcloud stim " "producer for device " << desc->deviceSelector << " and possibly also destroyed device.\n"; } co_return StimBuffDeviceOpResult{true, desc}; } sscl::co::ViralNonPostingInvoker livoxGen1_initializeCInd() { if (!smoHooksPtr) { throw std::runtime_error(std::string(__func__) + ": SMO hooks " "pointers not filled in."); } // Load LivoxProto1 library auto libPath = smoHooksPtr->searchForLibInSmoSearchPaths("liblivoxProto1.so.0"); livoxProto1.dlopenHandle.reset(dlopen( libPath.value_or("liblivoxProto1.so.0").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_getOrCreateDeviceCReq = reinterpret_cast< livoxProto1_getOrCreateDeviceCReqFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_getOrCreateDeviceCReq")); livoxProto1.livoxProto1_destroyDeviceCReq = reinterpret_cast< livoxProto1_destroyDeviceCReqFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_destroyDeviceCReq")); livoxProto1.livoxProto1_device_enablePcloudDataCReq = reinterpret_cast< livoxProto1_device_enablePcloudDataCReqFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_device_enablePcloudDataCReq")); livoxProto1.livoxProto1_device_disablePcloudDataCReq = reinterpret_cast< livoxProto1_device_disablePcloudDataCReqFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_device_disablePcloudDataCReq")); livoxProto1.livoxProto1_device_getReturnModeCReq = reinterpret_cast< livoxProto1_device_getReturnModeCReqFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_device_getReturnModeCReq")); livoxProto1.livoxProto1_getPcloudDataFdDesc = reinterpret_cast< livoxProto1_getPcloudDataFdDescFn *>( dlsym( livoxProto1.dlopenHandle.get(), "livoxProto1_getPcloudDataFdDesc")); if (!livoxProto1.livoxProto1_main || !livoxProto1.livoxProto1_exit || !livoxProto1.livoxProto1_getOrCreateDeviceCReq || !livoxProto1.livoxProto1_destroyDeviceCReq || !livoxProto1.livoxProto1_device_enablePcloudDataCReq || !livoxProto1.livoxProto1_device_disablePcloudDataCReq || !livoxProto1.livoxProto1_device_getReturnModeCReq || !livoxProto1.livoxProto1_getPcloudDataFdDesc) { throw std::runtime_error( std::string(__func__) + ": Failed to get LivoxProto1 library functions"); } (*livoxProto1.livoxProto1_main)( smoThreadingModelDesc.componentThread, *smoHooksPtr); co_return 0; } sscl::co::ViralNonPostingInvoker livoxGen1_finalizeCInd() { stopAllManagedProducersBeforeFinalize(); managedStimulusProducers.clear(); if (livoxProto1.livoxProto1_exit) { (*livoxProto1.livoxProto1_exit)(); } livoxProto1.dlopenHandle.reset(nullptr); livoxProto1 = LivoxProto1DllState(); co_return 0; } static const StimBuffApiDesc livoxGen1ApiDesc = { .name = "livoxGen1", .exportedQualeIfaceApis = { {.name = "mesh"}, {.name = "pcloudIntensity"}, {.name = "pcloudLightAmbience"}, {.name = "pcloudDarkAmbience"}, {.name = "gyro"}, {.name = "accel"} }, .sal_mgmt_libOps = { .initializeCInd = livoxGen1_initializeCInd, .finalizeCInd = livoxGen1_finalizeCInd, .attachDeviceCReq = livoxGen1_attachDeviceCReq, .detachDeviceCReq = livoxGen1_detachDeviceCReq } }; extern "C" SMO_GET_STIM_BUFF_API_DESC_FN_TYPEDEF SMO_GET_STIM_BUFF_API_DESC_FN_NAME; const StimBuffApiDesc& SMO_GET_STIM_BUFF_API_DESC_FN_NAME( const SmoCallbacks& callbacks, const SmoThreadingModelDesc& threadingModel) { smoHooksPtr = &callbacks; smoThreadingModelDesc = threadingModel; return livoxGen1ApiDesc; } } // namespace smo::stim_buff