#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "udpCommandDemuxer.h" #include "protocol.h" #include "core.h" #include "device.h" namespace livoxProto1 { namespace comms { UdpCommandDemuxer::UdpCommandDemuxer( const std::shared_ptr &componentThread, DeviceManager &deviceManager, uint16_t commandPort, uint16_t dataPort ) : componentThread(componentThread), deviceManager(deviceManager), commandPort(commandPort), dataPort(dataPort), senderAddrLen(sizeof(senderAddr)) { } UdpCommandDemuxer::~UdpCommandDemuxer() { stop(); } void UdpCommandDemuxer::start() { if (isActive.load()) { std::cerr << __func__ << ": Demuxer is already running" << std::endl; return; } try { { sscl::SpinLock::Guard lock(isActiveAndShouldStopLock); setupSockets(); isActive.store(true); shouldStop.store(false); } // Start the async receive loop startAsyncReceive(); std::cout << __func__ << ": UDP Command Demuxer started on port " << commandPort << std::endl; } catch (const std::exception &e) { std::cerr << __func__ << ": Failed to start demuxer: " << e.what() << std::endl; isActive.store(false); throw; } } void UdpCommandDemuxer::stop() { { sscl::SpinLock::Guard lock(isActiveAndShouldStopLock); if (!isActive.load()) { return; } shouldStop.store(true); } // Close socket and cleanup if (cmdEndpointFdDesc) { cmdEndpointFdDesc->cancel(); cmdEndpointFdDesc.reset(); } if (pcloudDataFdDesc) { pcloudDataFdDesc->cancel(); pcloudDataFdDesc.reset(); } isActive.store(false); std::cout << __func__ << ": UDP Command Demuxer stopped" << std::endl; } void UdpCommandDemuxer::setupSockets() { setupCommandSocket(); setupPcloudDataSocket(); } void UdpCommandDemuxer::setupCommandSocket() { // RAII class to manage socket file descriptor struct SocketRAII { int fd; SocketRAII(int socketFd) : fd(socketFd) {} ~SocketRAII() { if (fd >= 0) close(fd); } void commit() { fd = -1; } // Transfer ownership, prevent close int getFd() const { return fd; } bool isValid() const { return fd >= 0; } }; // Create UDP socket SocketRAII socketGuard(socket(AF_INET, SOCK_DGRAM, 0)); if (!socketGuard.isValid()) { throw std::runtime_error( std::string(__func__) + ": Failed to create socket: " + strerror(errno)); } // Set SO_REUSEADDR to allow binding even if port is in TIME_WAIT int reuse = 1; if (setsockopt( socketGuard.getFd(), SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) { throw std::runtime_error( std::string(__func__) + ": Failed to set SO_REUSEADDR: " + strerror(errno)); } // Set socket to non-blocking mode int flags = fcntl(socketGuard.getFd(), F_GETFL, 0); if (flags < 0 || fcntl( socketGuard.getFd(), F_SETFL, flags | O_NONBLOCK) < 0) { throw std::runtime_error( std::string(__func__) + ": Failed to set non-blocking mode: " + strerror(errno)); } /** EXPLANATION: * Bind to command port. * * WSL2 NAT PORT TRANSLATION ISSUE: * On Windows 10, WSL2 uses NAT that translates UDP source ports when * forwarding packets from WSL to the physical network. A socket bound to * port 56001 in WSL may send from port 52511 on the wire. * * The device should use the cmd_port from the handshake message (56001), * not the translated source port, so this may not break functionality. * However, Windows NAT behavior can be unpredictable. * * Solutions: * - Windows 11 22H2+: Use WSL2 mirror networking mode (.wslconfig) * - Run natively on Linux * - Accept the limitation (may work if device uses cmd_port from handshake) */ struct sockaddr_in localAddr; memset(&localAddr, 0, sizeof(localAddr)); localAddr.sin_family = AF_INET; localAddr.sin_addr.s_addr = INADDR_ANY; localAddr.sin_port = htons(commandPort); if (bind( socketGuard.getFd(), (struct sockaddr *)&localAddr, sizeof(localAddr)) < 0) { throw std::runtime_error( std::string(__func__) + ": Failed to bind to port " + std::to_string(commandPort) + ": " + strerror(errno)); } /* Verify the socket is actually bound to the expected port * This helps catch WSL/Windows networking issues. */ struct sockaddr_in boundAddr; socklen_t boundAddrLen = sizeof(boundAddr); if (getsockname( socketGuard.getFd(), (struct sockaddr *)&boundAddr, &boundAddrLen) == 0) { uint16_t boundPort = ntohs(boundAddr.sin_port); if (boundPort != commandPort) { std::cerr << __func__ << ": WARNING: Socket bound to port " << boundPort << " instead of expected port " << commandPort << std::endl; } #if 1 else { std::cout << __func__ << ": Successfully bound command socket " "to port " << boundPort << std::endl; } #endif } // Create boost wrapper for async operations cmdEndpointFdDesc = std::make_shared( componentThread->getIoContext(), socketGuard.getFd()); // Transfer ownership, prevent auto-close socketGuard.commit(); } void UdpCommandDemuxer::setupPcloudDataSocket() { // RAII class to manage socket file descriptor struct SocketRAII { int fd; SocketRAII(int socketFd) : fd(socketFd) {} ~SocketRAII() { if (fd >= 0) close(fd); } void commit() { fd = -1; } // Transfer ownership, prevent close int getFd() const { return fd; } bool isValid() const { return fd >= 0; } }; // Create UDP socket for point cloud data reception SocketRAII socketGuard(socket(AF_INET, SOCK_DGRAM, 0)); if (!socketGuard.isValid()) { throw std::runtime_error( std::string(__func__) + ": Failed to create socket: " + strerror(errno)); } // Set socket to non-blocking mode int flags = fcntl(socketGuard.getFd(), F_GETFL, 0); if (flags < 0 || fcntl(socketGuard.getFd(), F_SETFL, flags | O_NONBLOCK) < 0) { throw std::runtime_error( std::string(__func__) + ": Failed to set non-blocking mode: " + strerror(errno)); } // Bind to the data port struct sockaddr_in localAddr; memset(&localAddr, 0, sizeof(localAddr)); localAddr.sin_family = AF_INET; localAddr.sin_addr.s_addr = INADDR_ANY; localAddr.sin_port = htons(dataPort); if (bind( socketGuard.getFd(), (struct sockaddr *)&localAddr, sizeof(localAddr)) < 0) { throw std::runtime_error( std::string(__func__) + ": Failed to bind to data port: " + std::to_string(dataPort) + ": " + strerror(errno)); } // Create boost wrapper for async operations pcloudDataFdDesc = std::make_shared( componentThread->getIoContext(), socketGuard.getFd()); // Transfer ownership, prevent auto-close socketGuard.commit(); } void UdpCommandDemuxer::startAsyncReceive() { if (!isActive.load() || shouldStop.load()) { return; } cmdEndpointFdDesc->async_wait( boost::asio::posix::stream_descriptor::wait_read, std::bind( &UdpCommandDemuxer::onDataReady, this, std::placeholders::_1)); } void UdpCommandDemuxer::onDataReady(const boost::system::error_code &error) { if (error) { if (error != boost::asio::error::operation_aborted) { std::cerr << __func__ << ": Socket error: " << error.message() << std::endl; } return; } sscl::SpinLock::Guard lock(isActiveAndShouldStopLock); if (!isActive.load() || shouldStop.load()) { return; } // Read the data bytesReceived = recvfrom( cmdEndpointFdDesc->native_handle(), receiveBuffer, sizeof(receiveBuffer), 0, (struct sockaddr *)&senderAddr, &senderAddrLen); if (bytesReceived > 0) { processIncomingData(); } else if (bytesReceived < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { std::cerr << __func__ << ": recvfrom error: " << strerror(errno) << std::endl; } } // Continue listening for more data startAsyncReceive(); } void UdpCommandDemuxer::processIncomingData() { if (bytesReceived < 2) { // Too small to contain any meaningful data return; } // Extract source IP address char sourceIP[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &senderAddr.sin_addr, sourceIP, INET_ADDRSTRLEN); if (bytesReceived >= static_cast( sizeof(Header) + sizeof(Command))) { const uint8_t cmdSet = receiveBuffer[sizeof(Header)]; const uint8_t cmdId = receiveBuffer[sizeof(Header) + 1]; if (tryCompletePendingCommandWait( sourceIP, cmdSet, cmdId, receiveBuffer, bytesReceived)) { return; } } // First, find device with matching IP address in DeviceManager collection for (const auto &device : deviceManager.devices) { if (device->discoveredDevice.ipAddr != sourceIP) { continue; } // Found matching device, route the datagram to it try { device->handleUdpDgram( receiveBuffer, bytesReceived, senderAddr); } catch (const std::exception &e) { std::cerr << __func__ << ": Device handler exception for IP " << sourceIP << ": " << e.what() << std::endl; } return; } // If not found in DeviceManager, check temporary collection (devices under construction) auto tempIt = livoxProto1::Device::devicesUnderConstruction.find(sourceIP); if (tempIt != livoxProto1::Device::devicesUnderConstruction.end()) { // Extract command set and command ID from the datagram if (bytesReceived >= static_cast( sizeof(livoxProto1::comms::Header) + sizeof(livoxProto1::comms::Command))) { uint8_t cmd_set = receiveBuffer[ sizeof(livoxProto1::comms::Header)]; uint8_t cmd_id = receiveBuffer[ sizeof(livoxProto1::comms::Header) + 1]; // Found matching dev in temp collection, invoke matching handlers for (const auto& cmdHandler : tempIt->second) { if (cmdHandler.cmd_set != cmd_set || cmdHandler.cmd_id != cmd_id) { continue; } try { cmdHandler.handler( receiveBuffer, bytesReceived, senderAddr); } catch (const std::exception &e) { std::cerr << __func__ << ": Temporary device handler " "exception for IP " << sourceIP << ": " << e.what() << std::endl; } } } return; } // No device found with matching IP in either collection, discard the data std::cerr << __func__ << ": No device found for source IP " << sourceIP << ", discarding datagram" << std::endl; } struct UdpCommandDemuxer::PendingCommandWaitDesc { CommandWaitKey key; boost::asio::io_context &resumeIoContext; std::atomic settled{false}; UdpCommandResponseResult result{}; std::coroutine_handle<> callerSchedHandle; PendingCommandWaitDesc( CommandWaitKey keyIn, boost::asio::io_context &resumeIoContextIn) : key(std::move(keyIn)), resumeIoContext(resumeIoContextIn) {} }; void UdpCommandDemuxer::settlePendingCommandWait( const std::shared_ptr &wait, UdpCommandResponseResult::Outcome outcome, const uint8_t *data, ssize_t bytesReceived) { if (wait->settled.exchange(true)) { return; } wait->result.outcome = outcome; wait->result.bytesReceived = bytesReceived; if (outcome == UdpCommandResponseResult::Outcome::Response && data != nullptr && bytesReceived > 0 && bytesReceived <= static_cast(sizeof(wait->result.buffer))) { memcpy(wait->result.buffer, data, bytesReceived); } std::coroutine_handle<> handle = wait->callerSchedHandle; if (!handle) { return; } boost::asio::post(wait->resumeIoContext, handle); } std::shared_ptr UdpCommandDemuxer::findAndRemovePendingCommandWait(const CommandWaitKey &key) { sscl::SpinLock::Guard guard(pendingWaits.lock); const auto iterator = pendingWaits.rsrc.pendingWaits.find(key); if (iterator == pendingWaits.rsrc.pendingWaits.end()) { return nullptr; } std::shared_ptr wait = iterator->second; pendingWaits.rsrc.pendingWaits.erase(iterator); return wait; } void UdpCommandDemuxer::cancelPendingCommandWait( uint8_t cmdSet, uint8_t cmdId, const std::string &deviceIp) { std::shared_ptr wait = findAndRemovePendingCommandWait( {deviceIp, cmdSet, cmdId}); if (!wait) { return; } settlePendingCommandWait( wait, UdpCommandResponseResult::Outcome::Timeout, nullptr, -1); } bool UdpCommandDemuxer::tryCompletePendingCommandWait( const char *sourceIp, uint8_t cmdSet, uint8_t cmdId, const uint8_t *data, ssize_t bytesReceived) { std::shared_ptr wait = findAndRemovePendingCommandWait( {sourceIp, cmdSet, cmdId}); if (!wait) { return false; } const UdpCommandResponseResult::Outcome outcome = (bytesReceived > 0 && bytesReceived <= static_cast(sizeof(wait->result.buffer))) ? UdpCommandResponseResult::Outcome::Response : UdpCommandResponseResult::Outcome::RecvError; settlePendingCommandWait(wait, outcome, data, bytesReceived); return true; } sscl::co::ViralNonPostingInvoker UdpCommandDemuxer::waitForCommandResponseCReq( uint8_t cmdSet, uint8_t cmdId, const std::string &deviceIp) { const CommandWaitKey key{deviceIp, cmdSet, cmdId}; auto wait = std::make_shared( key, componentThread->getIoContext()); { sscl::SpinLock::Guard guard(pendingWaits.lock); pendingWaits.rsrc.pendingWaits[key] = wait; } struct PendingCommandWaitDescAwaiter { std::shared_ptr wait; bool await_ready() const noexcept { return wait->settled.load(std::memory_order_acquire); } bool await_suspend(std::coroutine_handle<> caller) noexcept { if (wait->settled.load(std::memory_order_acquire)) { return false; } wait->callerSchedHandle = caller; return true; } UdpCommandResponseResult await_resume() const noexcept { return wait->result; } }; const UdpCommandResponseResult result = co_await PendingCommandWaitDescAwaiter{wait}; if (findAndRemovePendingCommandWait(key)) { std::cerr << __func__ << ": pending wait still registered after " "settle for device " << deviceIp << " (cmd_set=" << static_cast(cmdSet) << ", cmd_id=" << static_cast(cmdId) << "); program error" << std::endl; } co_return result; } sscl::co::ViralNonPostingInvoker UdpCommandDemuxer::waitForCommandResponseCReq( uint8_t cmdSet, uint8_t cmdId, const std::string &deviceIp, int timeoutMs) { /** EXPLANATION: * We setup an async timer event to detect timeout, and register a UDP * command handler to wait for the device to respond to the incoming command * request. If the device does not respond within the timeout period, * we will consider the command to have failed. */ boost::asio::io_context &ioContext = componentThread->getIoContext(); boost::asio::deadline_timer raceTimer(ioContext); auto timerAwaiter = adapters::boostAsio::getDeadlineTimerAReqAwaiter( ioContext, raceTimer, boost::posix_time::milliseconds(timeoutMs)); auto responseInvoker = waitForCommandResponseCReq(cmdSet, cmdId, deviceIp); static constexpr int timerMemberSettlementIndex = 0; sscl::co::Group group; group.add(timerAwaiter); group.add(responseInvoker); co_await group.getAwaitFirstSettlementInvoker(); group.checkForAndReThrowGroupExceptions(); const bool timerWonFirst = group.s.rsrc.firstSettledInvokerIdx == timerMemberSettlementIndex; if (timerWonFirst) { cancelPendingCommandWait(cmdSet, cmdId, deviceIp); } else { raceTimer.cancel(); } /** Group member adapter coros are fire-and-forget; keep group alive until * both members settle so the loser adapter does not touch freed state. */ co_await group.getAwaitAllSettlementsInvoker(); group.checkForAndReThrowGroupExceptions(); if (timerWonFirst) { UdpCommandResponseResult timeoutResult; timeoutResult.outcome = UdpCommandResponseResult::Outcome::Timeout; co_return timeoutResult; } co_return responseInvoker.completedReturnValues().myReturnValue; } } // namespace comms } // namespace livoxProto1