#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; // 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)) { return stimProducer; } } return nullptr; } // 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; 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 device's component thread delayTimer = std::make_unique( context->deviceTmp->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, context.get(), context, std::placeholders::_1, std::placeholders::_2)}); } void attachDeviceReq3( 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; } // Parse history buffer duration from quale-iface-api-params int histbuffMs = 30000; // Default: 30000ms (30 seconds) (void)histbuffMs; 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( context->spec->qualeIfaceApiParams, paramName); break; // Found and parsed successfully } catch (const std::exception&) { // Parameter not found or parse error, continue to next synonym continue; } } // Create and add PcloudStimulusProducer to collection now that device is ready PcloudStimulusProducer::PcloudFormatDesc formatDesc; formatDesc.format = PcloudStimulusProducer::PcloudFormatDesc::Format ::XYZI; auto pcloudDataProducer = std::make_shared( context->spec, context->deviceTmp, formatDesc, 30); context->stimProducer = pcloudDataProducer; context->deviceTmp->nAttachedStimBuffs++; attachedStimulusProducers.push_back(pcloudDataProducer); pcloudDataProducer->start(); if (1 || smoHooksPtr->OptionParser_getOptions().verbose) { std::cout << __func__ << ": Got return mode (" << (int)mode << ") for device: " << context->spec->deviceSelector << std::endl; } context->delayedEnablePcloudData(context); } // Helper method to delay and then call enablePcloudDataReq void delayedEnablePcloudData( std::shared_ptr context) { // Initialize timer with device's component thread delayTimer = std::make_unique( context->stimProducer->device->componentThread->getIoService()); delayTimer->expires_from_now(boost::posix_time::milliseconds(5)); delayTimer->async_wait( std::bind( &AttachDeviceReq::attachDeviceReq4, context.get(), context, std::placeholders::_1)); } void attachDeviceReq4( 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_enablePcloudDataReq)( context->stimProducer->device, {context, std::bind( &AttachDeviceReq::attachDeviceReq5, context.get(), context, std::placeholders::_1)}); } void attachDeviceReq5( 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& stimProducer, smo::Callback cb) : smo::NonPostedAsynchronousContinuation( std::move(cb)), spec(spec), stimProducer(stimProducer) {} public: const std::shared_ptr spec; std::shared_ptr stimProducer; 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 device's component thread delayTimer = std::make_unique( context->stimProducer->device->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. } context->stimProducer->stop(); // Remove stimulus producer from collection before destroying device context->stimProducer->device->nAttachedStimBuffs--; auto it = std::find( attachedStimulusProducers.begin(), attachedStimulusProducers.end(), context->stimProducer); if (it != attachedStimulusProducers.end()) { attachedStimulusProducers.erase(it); } (*livoxProto1.livoxProto1_destroyDeviceReq)( context->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 = "pcloud"}, {.name = "pcloudIntensity"}, {.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); // Check if stimulus producer already exists in the collection auto pcloudDataProducer = std::static_pointer_cast( getStimulusProducer(desc)); if (pcloudDataProducer) { request->stimProducer = pcloudDataProducer; // Check if device's point cloud data is already active if (pcloudDataProducer->device && pcloudDataProducer->device->pcloudDataActive) { // Point cloud data is already active, call success callback request->callOriginalCb(true, request->spec); return; } /* Enable pcloud data first. Don't need delay since no commands were * sent to device prior to us reaching here. */ (*livoxProto1.livoxProto1_device_enablePcloudDataReq)( pcloudDataProducer->device, {request, std::bind( &AttachDeviceReq::attachDeviceReq5, request.get(), request, std::placeholders::_1)}); return; } // 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 ) { // Check if stimulus producer exists in the collection auto stimProducer = std::static_pointer_cast( getStimulusProducer(desc)); if (!stimProducer) { cb.callbackFn(false, desc); return; } auto request = std::make_shared( desc, stimProducer, 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