Files
salmanoff/stimBuffApis/livoxGen1/livoxGen1.cpp
T
hayodea 5845f1a41d Bug:Boost: Use shlibs instead of header-only for call_stack::top_
This symbol is defined as a static member object inside of a
boost detail header. When boost headers are used in a project
that uses Boost in both the main binary as well as dlopen()'d
shlibs, the top_ symbol gets duplicated and the metadata gets
partitioned.

We use the Boost shlib to unify both the main binary and the
shlibs to use the same memory address for top_.

This involves marking the templated object call_stack::top_ as
"extern" and then declaring to Boost that we intend to use the
shlibs.
2025-11-03 22:59:52 -04:00

689 lines
20 KiB
C++

#include <boostAsioLinkageFix.h>
#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <map>
#include <functional>
#include <algorithm>
#include <dlfcn.h>
#include <opts.h>
#include <user/senseApiDesc.h>
#include <user/deviceAttachmentSpec.h>
#include <callback.h>
#include <livoxProto1/livoxProto1.h>
#include <livoxProto1/device.h>
#include <livoxProto1/protocol.h>
#include <asynchronousContinuation.h>
#include <boost/asio/deadline_timer.hpp>
#include "pcloudStimulusBuffer.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 buffers
static std::vector<std::shared_ptr<StimulusBuffer>> attachedStimBuffs;
// Get stimulus buffer by device attachment spec
static std::shared_ptr<StimulusBuffer>
getStimBuff(const std::shared_ptr<smo::device::DeviceAttachmentSpec>& spec)
{
for (const auto& stimBuff : attachedStimBuffs)
{
// Compare device selectors to find matching buffer
if (stimBuff->deviceAttachmentSpec.deviceSelector
== spec->deviceSelector)
{
return stimBuff;
}
}
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<sal_mlo_attachDeviceReqCbFn>
{
public:
AttachDeviceReq(
const std::shared_ptr<smo::device::DeviceAttachmentSpec>& spec,
smo::Callback<sal_mlo_attachDeviceReqCbFn> cb)
: smo::NonPostedAsynchronousContinuation<sal_mlo_attachDeviceReqCbFn>(
std::move(cb)),
spec(spec)
{}
public:
const std::shared_ptr<smo::device::DeviceAttachmentSpec> spec;
std::shared_ptr<PcloudStimulusBuffer> stimBuff;
std::shared_ptr<livoxProto1::Device> deviceTmp;
private:
std::unique_ptr<boost::asio::deadline_timer> delayTimer;
public:
void attachDeviceReq1(
std::shared_ptr<AttachDeviceReq> context,
bool success, std::shared_ptr<livoxProto1::Device> 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 || 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<AttachDeviceReq> context)
{
// Initialize timer with device's component thread
delayTimer = std::make_unique<boost::asio::deadline_timer>(
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<AttachDeviceReq> 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<AttachDeviceReq> 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)
const std::vector<std::string> 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 PcloudStimulusBuffer to collection now that device is ready
StimulusBuffer::PcloudFormatDesc formatDesc;
formatDesc.format = StimulusBuffer::PcloudFormatDesc::Format::XYZI;
auto pcloudStimBuff = std::make_shared<PcloudStimulusBuffer>(
*context->spec, context->deviceTmp, formatDesc, histbuffMs, 30);
context->stimBuff = pcloudStimBuff;
context->deviceTmp->nAttachedStimBuffs++;
attachedStimBuffs.push_back(pcloudStimBuff);
pcloudStimBuff->start();
if (1 || 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<AttachDeviceReq> context)
{
// Initialize timer with device's component thread
delayTimer = std::make_unique<boost::asio::deadline_timer>(
context->stimBuff->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<AttachDeviceReq> 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->stimBuff->device,
{context, std::bind(
&AttachDeviceReq::attachDeviceReq5,
context.get(), context,
std::placeholders::_1)});
}
void attachDeviceReq5(
std::shared_ptr<AttachDeviceReq> 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 || 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<sal_mlo_detachDeviceReqCbFn>
{
public:
DetachDeviceReq(
const std::shared_ptr<smo::device::DeviceAttachmentSpec>& spec,
const std::shared_ptr<PcloudStimulusBuffer>& stimBuff,
smo::Callback<sal_mlo_detachDeviceReqCbFn> cb)
: smo::NonPostedAsynchronousContinuation<sal_mlo_detachDeviceReqCbFn>(
std::move(cb)),
spec(spec), stimBuff(stimBuff)
{}
public:
const std::shared_ptr<smo::device::DeviceAttachmentSpec> spec;
std::shared_ptr<PcloudStimulusBuffer> stimBuff;
private:
std::unique_ptr<boost::asio::deadline_timer> delayTimer;
public:
void detachDeviceReq1(
std::shared_ptr<DetachDeviceReq> context,
bool success)
{
if (!success)
{
std::cerr << __func__ << ": Failed to disable pcloud data for "
"stimbuff " << 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<DetachDeviceReq> context)
{
// Initialize timer with device's component thread
delayTimer = std::make_unique<boost::asio::deadline_timer>(
context->stimBuff->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<DetachDeviceReq> context,
const boost::system::error_code& error)
{
if (error)
{
std::cerr << __func__ << ": Timer error: " << error.message()
<< std::endl;
// Fallthrough.
}
context->stimBuff->stop();
// Remove stimulus buffer from collection before destroying device
context->stimBuff->device->nAttachedStimBuffs--;
auto it = std::find(
attachedStimBuffs.begin(), attachedStimBuffs.end(),
context->stimBuff);
if (it != attachedStimBuffs.end())
{ attachedStimBuffs.erase(it); }
(*livoxProto1.livoxProto1_destroyDeviceReq)(
context->stimBuff->device,
{context, std::bind(
&DetachDeviceReq::detachDeviceReq2,
context.get(), context,
std::placeholders::_1)});
}
void detachDeviceReq2(
std::shared_ptr<DetachDeviceReq> context,
bool success)
{
if (!success)
{
std::cerr << __func__ << ": Failed to destroy dev "
"device " << context->spec->deviceSelector << " for stimbuff."
"\n";
/** NOTE:
* There's a decent argument for falling through here and still
* removing the stimulus buffer from attachedStimBuffs.
*/
context->callOriginalCb(false, context->spec);
return;
}
if (1 || OptionParser::getOptions().verbose)
{
std::cout << __func__ << ": Successfully detached pcloud stimbuff "
"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<livoxProto1_mainFn *>(
dlsym(livoxProto1.dlopenHandle.get(), "livoxProto1_main"));
livoxProto1.livoxProto1_exit = reinterpret_cast<livoxProto1_exitFn *>(
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)
{
attachedStimBuffs.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<smo::device::DeviceAttachmentSpec>& desc,
const std::shared_ptr<smo::ComponentThread>& componentThread,
Callback<smo::stim_buff::sal_mlo_attachDeviceReqCbFn> cb
)
{
if (!livoxProto1.livoxProto1_getOrCreateDeviceReq)
{
throw std::runtime_error(
std::string(__func__) + ": LivoxProto1 getOrCreateDevice function "
"not available");
}
auto request = std::make_shared<AttachDeviceReq>(desc, cb);
// Check if stimulus buffer already exists in the collection
auto pcloudStimBuff = std::static_pointer_cast<PcloudStimulusBuffer>(
getStimBuff(desc));
if (pcloudStimBuff)
{
request->stimBuff = pcloudStimBuff;
// Check if device's point cloud data is already active
if (pcloudStimBuff->device && pcloudStimBuff->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)(
pcloudStimBuff->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<uint8_t>(
smo::device::DeviceAttachmentSpec
::parseRequiredParamAsInt(
desc->providerParams, "smo-subnet-nbits"));
} else if (param.first == "data-port")
{
dataPort = static_cast<uint16_t>(
smo::device::DeviceAttachmentSpec
::parseRequiredParamAsInt(desc->providerParams, "data-port"));
} else if (param.first == "cmd-port")
{
cmdPort = static_cast<uint16_t>(
smo::device::DeviceAttachmentSpec
::parseRequiredParamAsInt(desc->providerParams, "cmd-port"));
} else if (param.first == "imu-port")
{
imuPort = static_cast<uint16_t>(
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<smo::device::DeviceAttachmentSpec>& desc,
Callback<smo::stim_buff::sal_mlo_detachDeviceReqCbFn> cb
)
{
// Check if stimulus buffer exists in the collection
auto stimBuff = std::static_pointer_cast<PcloudStimulusBuffer>(
getStimBuff(desc));
if (!stimBuff)
{
cb.callbackFn(false, desc);
return;
}
auto request = std::make_shared<DetachDeviceReq>(
desc, stimBuff, cb);
// Disable point cloud data first
(*livoxProto1.livoxProto1_device_disablePcloudDataReq)(
stimBuff->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