2025-09-06 20:06:38 -04:00
|
|
|
#ifndef LIVOX_PROTO1_DEVICE_H
|
|
|
|
|
#define LIVOX_PROTO1_DEVICE_H
|
|
|
|
|
|
2026-06-10 07:02:07 -04:00
|
|
|
#include <boostAsioLinkageFix.h>
|
|
|
|
|
|
2025-09-06 20:06:38 -04:00
|
|
|
#include <string>
|
|
|
|
|
#include <cstdint>
|
2025-11-16 12:37:25 -04:00
|
|
|
#include <cstddef>
|
2025-09-06 20:06:38 -04:00
|
|
|
#include <memory>
|
|
|
|
|
#include <atomic>
|
|
|
|
|
#include <optional>
|
2025-09-09 12:07:49 -04:00
|
|
|
#include <functional>
|
2025-10-22 07:28:00 -04:00
|
|
|
#include <unordered_map>
|
2025-11-16 12:37:25 -04:00
|
|
|
#include <stdexcept>
|
2025-09-07 07:27:14 -04:00
|
|
|
#include <sys/socket.h>
|
|
|
|
|
#include <netinet/in.h>
|
|
|
|
|
#include <arpa/inet.h>
|
|
|
|
|
#include <unistd.h>
|
2025-10-16 01:00:48 -04:00
|
|
|
#include <boost/asio/deadline_timer.hpp>
|
2025-10-22 00:54:28 -04:00
|
|
|
#include <boost/asio/posix/stream_descriptor.hpp>
|
2025-09-06 20:06:38 -04:00
|
|
|
#include "protocol.h"
|
2026-06-10 07:02:07 -04:00
|
|
|
#include <spinscale/co/dynamicPostingInvoker.h>
|
2026-05-28 20:13:12 -04:00
|
|
|
#include <spinscale/co/invokers.h>
|
2026-06-10 07:02:07 -04:00
|
|
|
#include <spinscale/co/nonViralTaskNursery.h>
|
|
|
|
|
#include <spinscale/syncCancelerForAsyncWork.h>
|
2025-09-06 20:06:38 -04:00
|
|
|
|
2025-10-22 07:28:00 -04:00
|
|
|
// Custom hash function for std::pair<uint8_t, uint8_t>
|
|
|
|
|
namespace std {
|
|
|
|
|
template<>
|
|
|
|
|
struct hash<std::pair<uint8_t, uint8_t>> {
|
|
|
|
|
size_t operator()(const std::pair<uint8_t, uint8_t>& p) const noexcept {
|
|
|
|
|
return (static_cast<size_t>(p.first) << 8) | static_cast<size_t>(p.second);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-06 20:06:38 -04:00
|
|
|
// Forward declaration
|
|
|
|
|
namespace smo {
|
|
|
|
|
class ComponentThread;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace livoxProto1 {
|
|
|
|
|
namespace comms {
|
|
|
|
|
|
|
|
|
|
/** EXPLANATION:
|
|
|
|
|
* This class represents a discovered device. It is used to store the
|
|
|
|
|
* device identifier and IP address of a discovered device.
|
|
|
|
|
*/
|
|
|
|
|
class DiscoveredDevice
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
DiscoveredDevice(
|
|
|
|
|
const std::string &deviceIdentifier,
|
|
|
|
|
DeviceType deviceType,
|
|
|
|
|
const std::string &ipAddr);
|
|
|
|
|
|
|
|
|
|
// "Conversion" constructor from BroadcastMessage
|
|
|
|
|
DiscoveredDevice(const BroadcastMessage &msg, const std::string &ipAddr);
|
|
|
|
|
|
|
|
|
|
~DiscoveredDevice() = default;
|
|
|
|
|
|
|
|
|
|
bool operator==(const DiscoveredDevice &other) const
|
|
|
|
|
{
|
|
|
|
|
return comms::deviceIdentifiersEqual(
|
|
|
|
|
deviceIdentifier, other.deviceIdentifier);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string stringify(void) const;
|
|
|
|
|
std::string getDeviceTypeName(void) const;
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
std::string deviceIdentifier;
|
|
|
|
|
DeviceType deviceType;
|
|
|
|
|
std::string ipAddr;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} // namespace comms
|
|
|
|
|
|
|
|
|
|
class Device
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
Device(const std::string &deviceIdentifier,
|
2025-12-27 16:21:22 -04:00
|
|
|
const std::shared_ptr<sscl::ComponentThread>& componentThread,
|
2025-11-01 02:45:24 -04:00
|
|
|
int commandTimeoutMs, int retryDelayMs,
|
2025-09-06 20:06:38 -04:00
|
|
|
const std::string& smoIp, uint8_t smoSubnetNbits,
|
|
|
|
|
uint16_t dataPort, uint16_t cmdPort, uint16_t imuPort);
|
|
|
|
|
~Device();
|
|
|
|
|
|
|
|
|
|
private:
|
2025-09-09 12:07:49 -04:00
|
|
|
// Heartbeat mechanism
|
|
|
|
|
void startHeartbeat();
|
2025-10-23 00:24:23 -04:00
|
|
|
void stopHeartbeat();
|
2026-06-10 07:02:07 -04:00
|
|
|
void sendHeartbeatOnce();
|
|
|
|
|
|
|
|
|
|
/** 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);
|
|
|
|
|
|
2025-09-06 20:06:38 -04:00
|
|
|
std::string generateClientDeviceIpFromSerialNumber(
|
|
|
|
|
const std::string& broadcastCode);
|
|
|
|
|
|
|
|
|
|
// IP detection methods
|
2025-09-06 20:44:28 -04:00
|
|
|
std::optional<std::string> detectSmoIp(const std::string& deviceIP);
|
2025-09-06 20:06:38 -04:00
|
|
|
uint32_t getSubnetMaskFor(uint8_t nbits);
|
|
|
|
|
|
2025-09-09 12:07:49 -04:00
|
|
|
public:
|
2025-10-30 11:49:54 -04:00
|
|
|
enum class ReturnMode : uint8_t
|
|
|
|
|
{
|
|
|
|
|
SingleFirst = 0x00,
|
|
|
|
|
SingleStrongest = 0x01,
|
|
|
|
|
Dual = 0x02,
|
|
|
|
|
Triple = 0x03
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-16 12:37:25 -04:00
|
|
|
/**
|
|
|
|
|
* Get the number of points per datagram based on return mode
|
|
|
|
|
* @param returnMode The return mode (0=SingleFirst, 1=SingleStrongest, 2=Dual, 3=Triple)
|
|
|
|
|
* @return Number of points per datagram
|
|
|
|
|
*/
|
|
|
|
|
static inline size_t getNPointsPerDgram(int returnMode)
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* Map modes to points per datagram based on Livox docs
|
|
|
|
|
* 1: first, 2: strongest -> 96 samples => 96 points
|
|
|
|
|
* 3: dual -> 48 samples * 2 points = 96
|
|
|
|
|
* 4: triple -> 30 samples * 3 points = 90
|
|
|
|
|
*/
|
|
|
|
|
switch (returnMode)
|
|
|
|
|
{
|
|
|
|
|
case static_cast<int>(ReturnMode::SingleFirst):
|
|
|
|
|
case static_cast<int>(ReturnMode::SingleStrongest):
|
|
|
|
|
case static_cast<int>(ReturnMode::Dual):
|
|
|
|
|
return 96u;
|
|
|
|
|
case static_cast<int>(ReturnMode::Triple):
|
|
|
|
|
return 90u;
|
|
|
|
|
default:
|
|
|
|
|
throw std::runtime_error(
|
|
|
|
|
std::string(__func__) + ": Unknown returnMode "
|
|
|
|
|
+ std::to_string(returnMode));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-09 12:07:49 -04:00
|
|
|
// Utility methods
|
2025-09-09 19:54:14 -04:00
|
|
|
std::optional<std::string> getSmoIp(const std::string& deviceIP);
|
2025-09-09 12:07:49 -04:00
|
|
|
|
2026-05-28 20:13:12 -04:00
|
|
|
struct ConnectIpResult
|
|
|
|
|
{
|
|
|
|
|
bool success = false;
|
|
|
|
|
std::string ipAddr;
|
|
|
|
|
};
|
2025-09-09 12:07:49 -04:00
|
|
|
|
|
|
|
|
// Async connection methods
|
2026-05-28 20:13:12 -04:00
|
|
|
sscl::co::ViralNonPostingInvoker<bool> connectCReq();
|
|
|
|
|
sscl::co::ViralNonPostingInvoker<ConnectIpResult> connectToKnownDeviceCReq();
|
|
|
|
|
sscl::co::ViralNonPostingInvoker<ConnectIpResult> connectByDeviceIdentifierCReq();
|
|
|
|
|
sscl::co::ViralNonPostingInvoker<bool> executeHandshakeCReq(
|
|
|
|
|
const std::string& deviceIP);
|
|
|
|
|
sscl::co::ViralNonPostingInvoker<bool> disconnectCReq();
|
|
|
|
|
sscl::co::ViralNonPostingInvoker<bool> enablePcloudDataCReq();
|
|
|
|
|
sscl::co::ViralNonPostingInvoker<bool> disablePcloudDataCReq();
|
|
|
|
|
|
|
|
|
|
struct GetReturnModeResult
|
|
|
|
|
{
|
|
|
|
|
bool success = false;
|
|
|
|
|
uint8_t returnMode = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
sscl::co::ViralNonPostingInvoker<bool> setReturnModeCReq(uint8_t returnMode);
|
|
|
|
|
sscl::co::ViralNonPostingInvoker<GetReturnModeResult> getReturnModeCReq();
|
2025-09-06 20:06:38 -04:00
|
|
|
|
2025-10-25 14:43:51 -04:00
|
|
|
public:
|
|
|
|
|
comms::DiscoveredDevice discoveredDevice;
|
2025-11-15 00:56:20 -04:00
|
|
|
std::atomic<size_t> nAttachedStimulusProducers;
|
2025-10-25 14:43:51 -04:00
|
|
|
|
|
|
|
|
// Configuration
|
2025-12-27 16:21:22 -04:00
|
|
|
std::shared_ptr<sscl::ComponentThread> componentThread;
|
2025-11-01 02:45:24 -04:00
|
|
|
int commandTimeoutMs, retryDelayMs;
|
2025-10-25 14:43:51 -04:00
|
|
|
std::string smoIp;
|
|
|
|
|
std::string detectedSmoListeningIp;
|
|
|
|
|
uint8_t smoSubnetNbits;
|
|
|
|
|
uint16_t dataPort, cmdPort, imuPort;
|
|
|
|
|
|
2026-06-10 07:02:07 -04:00
|
|
|
// Heartbeat state (timer lifetime tied to Device ctor/dtor)
|
|
|
|
|
boost::asio::deadline_timer heartbeatTimer;
|
|
|
|
|
sscl::co::NonViralTaskNursery daemonNursery;
|
2025-10-22 00:54:28 -04:00
|
|
|
|
|
|
|
|
// Point cloud data state
|
|
|
|
|
std::atomic<bool> pcloudDataActive;
|
|
|
|
|
|
2025-10-30 11:49:54 -04:00
|
|
|
// Cached last-known return mode for this device
|
|
|
|
|
ReturnMode currentReturnMode = ReturnMode::SingleFirst;
|
|
|
|
|
|
2025-10-22 06:17:42 -04:00
|
|
|
public:
|
|
|
|
|
// UDP datagram handling
|
|
|
|
|
void handleUdpDgram(
|
|
|
|
|
const uint8_t* data, ssize_t bytesReceived,
|
|
|
|
|
const struct sockaddr_in& senderAddr);
|
|
|
|
|
|
2025-10-22 07:28:00 -04:00
|
|
|
// Command handler registration
|
|
|
|
|
void registerUdpCommandHandler(
|
|
|
|
|
uint8_t cmd_set, uint8_t cmd_id,
|
|
|
|
|
std::function<void(
|
|
|
|
|
const uint8_t* data, ssize_t bytesReceived,
|
2025-10-22 22:13:38 -04:00
|
|
|
const struct sockaddr_in& senderAddr)> handler,
|
|
|
|
|
const std::string& deviceIP = "");
|
|
|
|
|
|
|
|
|
|
void unregisterUdpCommandHandler(
|
|
|
|
|
uint8_t cmd_set, uint8_t cmd_id, const std::string& deviceIP = "");
|
2025-10-22 07:28:00 -04:00
|
|
|
|
2025-10-22 00:54:28 -04:00
|
|
|
private:
|
|
|
|
|
// Point cloud data setup
|
|
|
|
|
void cleanupPcloudDataSocket();
|
2025-10-22 07:28:00 -04:00
|
|
|
|
2025-10-24 03:09:17 -04:00
|
|
|
/** EXPLANATION:
|
|
|
|
|
* This is the "straightforward" map of command set and command id to
|
|
|
|
|
* handlers. This is useful for any commands which are guaranteed to be
|
|
|
|
|
* issued to the device *AFTER* the device has successfully been added
|
|
|
|
|
* to the DeviceManager's list of devices.
|
|
|
|
|
*
|
|
|
|
|
* I.e: it cannot be used for commands which are issued to the device before
|
|
|
|
|
* getOrCreateDevice() has added the device to the DeviceManager's list of
|
|
|
|
|
* devices.
|
|
|
|
|
*/
|
2025-10-22 07:28:00 -04:00
|
|
|
// Command handler map
|
|
|
|
|
std::unordered_map<
|
|
|
|
|
std::pair<uint8_t, uint8_t>,
|
|
|
|
|
std::function<void(
|
|
|
|
|
const uint8_t* data, ssize_t bytesReceived,
|
|
|
|
|
const struct sockaddr_in& senderAddr)>> udpCommandHandlers;
|
2025-10-24 03:09:17 -04:00
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
/** EXPLANATION:
|
|
|
|
|
* This is the "temporary" map of command set and command id to
|
|
|
|
|
* handlers. This is useful for any commands which are issued to the device
|
|
|
|
|
* while it is being constructed.
|
|
|
|
|
*
|
|
|
|
|
* I.e: it shouldn't be used for cmds which are issued to the device after
|
|
|
|
|
* getOrCreateDevice() has added the device to the DeviceManager's list of
|
|
|
|
|
* devices. It will work for such commands, but we'd kind of prefer to use
|
|
|
|
|
* the "straightforward" map above for such commands.
|
|
|
|
|
*
|
|
|
|
|
* NOTE:
|
|
|
|
|
* There's a strong argument to be made for just getting rid of the
|
|
|
|
|
* "straightforward" map above and just using this one, tho.
|
|
|
|
|
*/
|
|
|
|
|
struct CommandHandler {
|
|
|
|
|
uint8_t cmd_set;
|
|
|
|
|
uint8_t cmd_id;
|
|
|
|
|
std::function<void(
|
|
|
|
|
const uint8_t* data, ssize_t bytesReceived,
|
|
|
|
|
const struct sockaddr_in& senderAddr)> handler;
|
|
|
|
|
};
|
|
|
|
|
static std::unordered_map<std::string, std::vector<CommandHandler>>
|
|
|
|
|
devicesUnderConstruction;
|
2025-09-06 20:06:38 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} // namespace livoxProto1
|
|
|
|
|
|
|
|
|
|
#endif // LIVOX_PROTO1_DEVICE_H
|