livoxProto1: Convert heartbeat sender into daemon coro

This commit is contained in:
2026-06-10 07:02:07 -04:00
parent facb665217
commit f9c64cf363
2 changed files with 194 additions and 130 deletions
+128 -79
View File
@@ -1,6 +1,9 @@
#include <boostAsioLinkageFix.h>
#include <sstream> #include <sstream>
#include <thread> #include <thread>
#include <chrono> #include <chrono>
#include <iostream>
#include <string> #include <string>
#include <stdexcept> #include <stdexcept>
#include <memory> #include <memory>
@@ -15,7 +18,10 @@
#include <optional> #include <optional>
#include <boost/asio/deadline_timer.hpp> #include <boost/asio/deadline_timer.hpp>
#include <boost/asio/posix/stream_descriptor.hpp> #include <boost/asio/posix/stream_descriptor.hpp>
#include <componentThread.h>
#include <adapters/boostAsio/deadlineTimerAReq.h>
#include <opts.h> #include <opts.h>
#include <spinscale/co/nonViralCompletion.h>
#include "device.h" #include "device.h"
#include "protocol.h" #include "protocol.h"
#include "core.h" #include "core.h"
@@ -100,7 +106,7 @@ componentThread(componentThread),
commandTimeoutMs(commandTimeoutMs), retryDelayMs(retryDelayMs), commandTimeoutMs(commandTimeoutMs), retryDelayMs(retryDelayMs),
smoIp(smoIp), detectedSmoListeningIp(""), smoSubnetNbits(smoSubnetNbits), smoIp(smoIp), detectedSmoListeningIp(""), smoSubnetNbits(smoSubnetNbits),
dataPort(dataPort), cmdPort(cmdPort), imuPort(imuPort), dataPort(dataPort), cmdPort(cmdPort), imuPort(imuPort),
heartbeatActive(false), heartbeatTimer(componentThread->getIoContext()),
pcloudDataActive(false) pcloudDataActive(false)
{ {
} }
@@ -112,8 +118,6 @@ Device::~Device()
if (pcloudDataActive.load()) { if (pcloudDataActive.load()) {
pcloudDataActive.store(false); pcloudDataActive.store(false);
} }
heartbeatTimer.reset();
} }
namespace { namespace {
@@ -1080,79 +1084,70 @@ static void discardHeartbeatAck(
<< std::hex << ack.ack_msg << std::dec << std::endl; << std::hex << ack.ack_msg << std::dec << std::endl;
} }
void Device::startHeartbeat() namespace {
constexpr long DEVICE_HEARTBEAT_PERIOD_MS = 1000;
long computeTimesliceResidueMs(long workDurationMs, long periodMs)
{ {
if (!componentThread || discoveredDevice.ipAddr.empty()) if (workDurationMs >= periodMs) {
{ return 0;
throw std::runtime_error(
std::string(__func__) +
": Can't start heartbeat without component thread or IP");
} }
return periodMs - workDurationMs;
// Register heartbeat ACK handler (cmd_set=0x00, cmd_id=0x03)
sscl::SpinLock::Guard lock(heartbeatActiveLock);
registerUdpCommandHandler(
0x00, 0x03, discardHeartbeatAck, discoveredDevice.ipAddr);
// Create heartbeat timer
heartbeatTimer = std::make_unique<boost::asio::deadline_timer>(
componentThread->getIoContext());
heartbeatActive.store(true);
// Send first heartbeat immediately
sendHeartbeat();
} }
void Device::stopHeartbeat() long durationMsSince(
const std::chrono::high_resolution_clock::time_point &startStamp,
const std::chrono::high_resolution_clock::time_point &endStamp)
{ {
{ const auto duration = endStamp - startStamp;
sscl::SpinLock::Guard lock(heartbeatActiveLock); return std::chrono::duration_cast<std::chrono::milliseconds>(
duration).count();
heartbeatActive.store(false);
unregisterUdpCommandHandler(0x00, 0x03, discoveredDevice.ipAddr);
}
if (heartbeatTimer) {
heartbeatTimer->cancel();
heartbeatTimer.reset();
}
} }
void Device::sendHeartbeat() void logDeviceCDaemonException(std::exception_ptr &exceptionPtr)
{ {
if (!heartbeatActive.load()) sscl::co::NonViralCompletion nvc(exceptionPtr);
{ if (!nvc.hasException()) {
std::cerr << __func__ << ": Ending heartbeat loop due to "
"heartbeatActive==false.\n";
return; return;
} }
try {
nvc.checkAndRethrowException();
} catch (const std::exception &e) {
std::cerr << "Device: deviceCDaemon: "
<< e.what() << std::endl;
}
}
} // namespace
void Device::sendHeartbeatOnce()
{
if (discoveredDevice.ipAddr.empty()) if (discoveredDevice.ipAddr.empty())
{ {
std::cerr << __func__ << ": Ending heartbeat loop due to " throw std::runtime_error(
"discoveredDevice.ipAddr.empty().\n"; std::string(__func__)
return; + ": Ending heartbeat loop due to "
"discoveredDevice.ipAddr.empty().");
} }
// Get the command endpoint from the UdpCommandDemuxer // Get the command endpoint from the UdpCommandDemuxer
auto& protoState = livoxProto1::getProtoState(); auto& protoState = livoxProto1::getProtoState();
if (!protoState.deviceManager) if (!protoState.deviceManager)
{ {
std::cerr << __func__ << ": No device manager available\n"; throw std::runtime_error(
return; std::string(__func__) + ": No device manager available");
} }
auto cmdEndpointFdDesc = protoState.deviceManager->udpCommandDemuxer auto cmdEndpointFdDesc = protoState.deviceManager->udpCommandDemuxer
.getCmdEndpointFdDesc(); .getCmdEndpointFdDesc();
if (!cmdEndpointFdDesc) if (!cmdEndpointFdDesc)
{ {
std::cerr << __func__ << ": No command endpoint available\n"; throw std::runtime_error(
return; std::string(__func__) + ": No command endpoint available");
} }
try {
comms::HeartbeatMessage heartbeatMsg; comms::HeartbeatMessage heartbeatMsg;
heartbeatMsg.swapContentsToProtocolEndianness(); heartbeatMsg.swapContentsToProtocolEndianness();
heartbeatMsg.header.setCrc16FromRawBytes(); heartbeatMsg.header.setCrc16FromRawBytes();
@@ -1175,53 +1170,107 @@ void Device::sendHeartbeat()
if (bytesSent < 0) if (bytesSent < 0)
{ {
std::cerr << "[" << __func__ << "] Failed to send heartbeat: " throw std::runtime_error(
<< strerror(errno) << std::endl; std::string("[") + __func__ + "] Failed to send heartbeat: "
return; + strerror(errno));
} }
}
sscl::co::DynamicNonViralPostingInvoker
Device::deviceCDaemon(
sscl::co::ExplicitPostTarget, std::exception_ptr &, std::function<void()>,
sscl::SyncCancelerForAsyncWork &canceler)
{
const long heartbeatPeriodMs = DEVICE_HEARTBEAT_PERIOD_MS;
while (!canceler.isCancellationRequested())
{
const auto workStartStamp =
std::chrono::high_resolution_clock::now();
try {
if (!canceler.execUncancelableSegmentOrAbort([&]() {
sendHeartbeatOnce();
})) {
break;
}
} catch (const std::exception &e) {
std::cerr << "deviceCDaemon: heartbeat failed for device "
<< discoveredDevice.deviceIdentifier << ": "
<< e.what() << std::endl;
}
const auto workEndStamp =
std::chrono::high_resolution_clock::now();
const long workDurationMs = durationMsSince(
workStartStamp, workEndStamp);
/** EXPLANATION: /** EXPLANATION:
* Schedule next heartbeat in 1 second, per the spec. * Schedule next heartbeat in 1 second, per the spec.
*/ */
heartbeatTimer->expires_from_now(boost::posix_time::seconds(1)); const long residueMs = computeTimesliceResidueMs(
heartbeatTimer->async_wait( workDurationMs, heartbeatPeriodMs);
[this](const boost::system::error_code& error) {
onHeartbeatTimer(error); // Timer was cancelled, which is expected when stopping
const bool expiredNormally = co_await
adapters::boostAsio::getDeadlineTimerAReqAwaiter(
sscl::ComponentThread::getSelf()->getIoContext(),
heartbeatTimer,
boost::posix_time::milliseconds(residueMs));
if (!expiredNormally) {
break;
} }
);
}
catch (const std::exception& e)
{
std::cerr << __func__ << ": Heartbeat send failed for device "
<< discoveredDevice.deviceIdentifier
<< ": " << e.what() << std::endl;
} }
co_return;
} }
void Device::onHeartbeatTimer(const boost::system::error_code& error) void Device::startHeartbeat()
{ {
// Timer was cancelled, heartbeat stopped if (!componentThread || discoveredDevice.ipAddr.empty())
if (error == boost::asio::error::operation_aborted) { {
throw std::runtime_error(
std::string(__func__) +
": Can't start heartbeat without component thread or IP");
}
if (daemonNursery.admissionIsOpen()) {
return; return;
} }
if (error) // Register heartbeat ACK handler (cmd_set=0x00, cmd_id=0x03)
{ registerUdpCommandHandler(
std::cerr << "[" << __func__ << "] Heartbeat timer error for device " 0x00, 0x03, discardHeartbeatAck, discoveredDevice.ipAddr);
<< discoveredDevice.deviceIdentifier
<< ": " << error.message() << std::endl;
daemonNursery.openAdmission();
// Send first heartbeat immediately
daemonNursery.launch(
[this](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
{
return deviceCDaemon(
sscl::co::ExplicitPostTarget{
componentThread->getIoContext()},
lease.getExceptionStorage(),
lease.getCallerLambda(),
lease.getSyncCanceler());
},
logDeviceCDaemonException);
}
void Device::stopHeartbeat()
{
unregisterUdpCommandHandler(0x00, 0x03, discoveredDevice.ipAddr);
if (!daemonNursery.admissionIsOpen()) {
return; return;
} }
// Send next heartbeat heartbeatTimer.cancel();
{ daemonNursery.requestCancelOnAll();
sscl::SpinLock::Guard lock(heartbeatActiveLock); daemonNursery.closeAdmission();
if (!heartbeatActive.load()) daemonNursery.syncAwaitAllSettlements(
{ return; } sscl::ComponentThread::getSelf()->getIoContext());
sendHeartbeat();
}
} }
uint32_t Device::getSubnetMaskFor(uint8_t nbits) uint32_t Device::getSubnetMaskFor(uint8_t nbits)
+22 -7
View File
@@ -1,6 +1,8 @@
#ifndef LIVOX_PROTO1_DEVICE_H #ifndef LIVOX_PROTO1_DEVICE_H
#define LIVOX_PROTO1_DEVICE_H #define LIVOX_PROTO1_DEVICE_H
#include <boostAsioLinkageFix.h>
#include <string> #include <string>
#include <cstdint> #include <cstdint>
#include <cstddef> #include <cstddef>
@@ -17,8 +19,10 @@
#include <boost/asio/deadline_timer.hpp> #include <boost/asio/deadline_timer.hpp>
#include <boost/asio/posix/stream_descriptor.hpp> #include <boost/asio/posix/stream_descriptor.hpp>
#include "protocol.h" #include "protocol.h"
#include <spinscale/co/dynamicPostingInvoker.h>
#include <spinscale/co/invokers.h> #include <spinscale/co/invokers.h>
#include <spinscale/spinLock.h> #include <spinscale/co/nonViralTaskNursery.h>
#include <spinscale/syncCancelerForAsyncWork.h>
// Custom hash function for std::pair<uint8_t, uint8_t> // Custom hash function for std::pair<uint8_t, uint8_t>
namespace std { namespace std {
@@ -86,8 +90,20 @@ private:
// Heartbeat mechanism // Heartbeat mechanism
void startHeartbeat(); void startHeartbeat();
void stopHeartbeat(); void stopHeartbeat();
void sendHeartbeat(); void sendHeartbeatOnce();
void onHeartbeatTimer(const boost::system::error_code& error);
/** EXPLANATION:
* deviceCDaemon is a dynamic posting non-viral coroutine: startHeartbeat()
* passes ExplicitPostTarget{componentThread->getIoContext()} so the daemon
* body always runs on componentThread. Extensible for future per-device
* background work beyond heartbeats.
*/
sscl::co::DynamicNonViralPostingInvoker deviceCDaemon(
sscl::co::ExplicitPostTarget postTarget,
std::exception_ptr &exceptionPtr,
std::function<void()> callback,
sscl::SyncCancelerForAsyncWork &canceler);
std::string generateClientDeviceIpFromSerialNumber( std::string generateClientDeviceIpFromSerialNumber(
const std::string& broadcastCode); const std::string& broadcastCode);
@@ -172,10 +188,9 @@ public:
uint8_t smoSubnetNbits; uint8_t smoSubnetNbits;
uint16_t dataPort, cmdPort, imuPort; uint16_t dataPort, cmdPort, imuPort;
// Heartbeat state // Heartbeat state (timer lifetime tied to Device ctor/dtor)
std::unique_ptr<boost::asio::deadline_timer> heartbeatTimer; boost::asio::deadline_timer heartbeatTimer;
std::atomic<bool> heartbeatActive; sscl::co::NonViralTaskNursery daemonNursery;
sscl::SpinLock heartbeatActiveLock;
// Point cloud data state // Point cloud data state
std::atomic<bool> pcloudDataActive; std::atomic<bool> pcloudDataActive;