diff --git a/configs/bridge-config.example.yml b/configs/bridge-config.example.yml index aed45381..978f9050 100644 --- a/configs/bridge-config.example.yml +++ b/configs/bridge-config.example.yml @@ -181,3 +181,13 @@ system: rtsPttPort: "/dev/ttyUSB0" # Hold-off time (ms) before clearing RTS PTT after last audio output. rtsPttHoldoffMs: 250 + + # CTS COR Configuration + # Flag indicating whether CTS-based COR detection is enabled. + ctsCorEnable: false + # Serial port device for CTS COR (e.g., /dev/ttyUSB0). Often same as RTS PTT. + ctsCorPort: "/dev/ttyUSB0" + # Flag indicating whether to invert COR logic (if true, COR LOW triggers instead of HIGH). + ctsCorInvert: false + # Hold-off time (ms) before ending call after CTS COR deasserts. + ctsCorHoldoffMs: 250 diff --git a/src/bridge/CtsCorController.cpp b/src/bridge/CtsCorController.cpp new file mode 100644 index 00000000..c00096a9 --- /dev/null +++ b/src/bridge/CtsCorController.cpp @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Bridge + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Lorenzo L. Romero, K2LLR + */ +/** + * @file CtsCorController.cpp + * @ingroup bridge + */ + +#include "Defines.h" +#include "CtsCorController.h" + +#if !defined(_WIN32) +#include +#endif + +CtsCorController::CtsCorController(const std::string& port) + : m_port(port), m_isOpen(false), m_ownsFd(true) +#if defined(_WIN32) + , m_fd(INVALID_HANDLE_VALUE) +#else + , m_fd(-1) +#endif // defined(_WIN32) +{ +} + +CtsCorController::~CtsCorController() +{ + close(); +} + +bool CtsCorController::open(int reuseFd) +{ + if (m_isOpen) + return true; + +#if defined(_WIN32) + std::string deviceName = m_port; + if (deviceName.find("\\\\.\\") == std::string::npos) { + deviceName = "\\\\." + m_port; + } + + m_fd = ::CreateFileA(deviceName.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (m_fd == INVALID_HANDLE_VALUE) { + ::LogError(LOG_HOST, "Cannot open CTS COR device - %s, err=%04lx", m_port.c_str(), ::GetLastError()); + return false; + } + + DCB dcb; + if (::GetCommState(m_fd, &dcb) == 0) { + ::LogError(LOG_HOST, "Cannot get the attributes for %s, err=%04lx", m_port.c_str(), ::GetLastError()); + ::CloseHandle(m_fd); + m_fd = INVALID_HANDLE_VALUE; + return false; + } + + dcb.BaudRate = 9600; + dcb.ByteSize = 8; + dcb.Parity = NOPARITY; + dcb.fParity = FALSE; + dcb.StopBits = ONESTOPBIT; + dcb.fInX = FALSE; + dcb.fOutX = FALSE; + dcb.fOutxCtsFlow = FALSE; + dcb.fOutxDsrFlow = FALSE; + dcb.fDsrSensitivity = FALSE; + dcb.fDtrControl = DTR_CONTROL_DISABLE; + dcb.fRtsControl = RTS_CONTROL_DISABLE; + + if (::SetCommState(m_fd, &dcb) == 0) { + ::LogError(LOG_HOST, "Cannot set the attributes for %s, err=%04lx", m_port.c_str(), ::GetLastError()); + ::CloseHandle(m_fd); + m_fd = INVALID_HANDLE_VALUE; + return false; + } + +#else + // If reusing an existing file descriptor from RTS PTT, don't open a new one + if (reuseFd >= 0) { + m_fd = reuseFd; + m_ownsFd = false; // Only COR can close file descriptor + ::LogInfo(LOG_HOST, "CTS COR Controller reusing file descriptor from RTS PTT on %s", m_port.c_str()); + m_isOpen = true; + return true; + } + + m_ownsFd = true; // COR owns the file descriptor + + // Open port if not available + m_fd = ::open(m_port.c_str(), O_RDONLY | O_NOCTTY | O_NDELAY, 0); + if (m_fd < 0) { + // Try rw if ro fails + m_fd = ::open(m_port.c_str(), O_RDWR | O_NOCTTY | O_NDELAY, 0); + if (m_fd < 0) { + ::LogError(LOG_HOST, "Cannot open CTS COR device - %s", m_port.c_str()); + return false; + } + } + + if (::isatty(m_fd) == 0) { + ::LogError(LOG_HOST, "%s is not a TTY device", m_port.c_str()); + ::close(m_fd); + m_fd = -1; + return false; + } + + // Save current RTS state before configuring termios + int savedModemState = 0; + if (::ioctl(m_fd, TIOCMGET, &savedModemState) < 0) { + ::LogError(LOG_HOST, "Cannot get the control attributes for %s", m_port.c_str()); + ::close(m_fd); + m_fd = -1; + return false; + } + bool savedRtsState = (savedModemState & TIOCM_RTS) != 0; + + if (!setTermios()) { + ::close(m_fd); + m_fd = -1; + return false; + } + + // Restore RTS to its original state + int currentModemState = 0; + if (::ioctl(m_fd, TIOCMGET, ¤tModemState) < 0) { + ::LogError(LOG_HOST, "Cannot get the control attributes for %s after termios", m_port.c_str()); + ::close(m_fd); + m_fd = -1; + return false; + } + bool currentRtsState = (currentModemState & TIOCM_RTS) != 0; + if (currentRtsState != savedRtsState) { + // Restore RTS to original state + if (savedRtsState) { + currentModemState |= TIOCM_RTS; + } else { + currentModemState &= ~TIOCM_RTS; + } + if (::ioctl(m_fd, TIOCMSET, ¤tModemState) < 0) { + ::LogError(LOG_HOST, "Cannot restore RTS state for %s", m_port.c_str()); + ::close(m_fd); + m_fd = -1; + return false; + } + ::LogDebug(LOG_HOST, "CTS COR: Restored RTS to %s on %s", savedRtsState ? "HIGH" : "LOW", m_port.c_str()); + } +#endif // defined(_WIN32) + + ::LogInfo(LOG_HOST, "CTS COR Controller opened on %s (RTS preserved)", m_port.c_str()); + m_isOpen = true; + return true; +} + +void CtsCorController::close() +{ + if (!m_isOpen) + return; + +#if defined(_WIN32) + if (m_fd != INVALID_HANDLE_VALUE) { + ::CloseHandle(m_fd); + m_fd = INVALID_HANDLE_VALUE; + } +#else + // Only close the file descriptor if we opened it ourselves + // If we're reusing a descriptor from RTS PTT, don't close it + if (m_fd != -1 && m_ownsFd) { + ::close(m_fd); + m_fd = -1; + } else if (m_fd != -1 && !m_ownsFd) { + m_fd = -1; + } +#endif // defined(_WIN32) + + m_isOpen = false; + ::LogInfo(LOG_HOST, "CTS COR Controller closed"); +} + +bool CtsCorController::isCtsAsserted() +{ + if (!m_isOpen) + return false; + +#if defined(_WIN32) + DWORD modemStat = 0; + if (::GetCommModemStatus(m_fd, &modemStat) == 0) { + ::LogError(LOG_HOST, "Cannot read modem status for %s, err=%04lx", m_port.c_str(), ::GetLastError()); + return false; + } + return (modemStat & MS_CTS_ON) != 0; +#else + int modemState = 0; + if (::ioctl(m_fd, TIOCMGET, &modemState) < 0) { + ::LogError(LOG_HOST, "Cannot get the control attributes for %s", m_port.c_str()); + return false; + } + return (modemState & TIOCM_CTS) != 0; +#endif // defined(_WIN32) +} + +bool CtsCorController::setTermios() +{ +#if !defined(_WIN32) + termios termios; + if (::tcgetattr(m_fd, &termios) < 0) { + ::LogError(LOG_HOST, "Cannot get the attributes for %s", m_port.c_str()); + return false; + } + + termios.c_iflag &= ~(IGNBRK | BRKINT | IGNPAR | PARMRK | INPCK); + termios.c_iflag &= ~(ISTRIP | INLCR | IGNCR | ICRNL); + termios.c_iflag &= ~(IXON | IXOFF | IXANY); + termios.c_oflag &= ~(OPOST); + // Important: Disable hardware flow control (CRTSCTS) to avoid affecting RTS + // We only want to read CTS, not control RTS + termios.c_cflag &= ~(CSIZE | CSTOPB | PARENB | CRTSCTS); + termios.c_cflag |= (CS8 | CLOCAL | CREAD); + termios.c_lflag &= ~(ISIG | ICANON | IEXTEN); + termios.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL); + termios.c_cc[VMIN] = 0; + termios.c_cc[VTIME] = 10; + + ::cfsetospeed(&termios, B9600); + ::cfsetispeed(&termios, B9600); + + if (::tcsetattr(m_fd, TCSANOW, &termios) < 0) { + ::LogError(LOG_HOST, "Cannot set the attributes for %s", m_port.c_str()); + return false; + } +#endif // !defined(_WIN32) + + return true; +} + + diff --git a/src/bridge/CtsCorController.h b/src/bridge/CtsCorController.h new file mode 100644 index 00000000..58f53806 --- /dev/null +++ b/src/bridge/CtsCorController.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Bridge + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Lorenzo L. Romero, K2LLR + */ +/** + * @file CtsCorController.h + * @ingroup bridge + */ +#if !defined(__CTS_COR_CONTROLLER_H__) +#define __CTS_COR_CONTROLLER_H__ + +#include "Defines.h" +#include "common/Log.h" + +#include + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#include +#include +#endif // defined(_WIN32) + +/** + * @brief This class implements CTS-based COR detection for the bridge. + * @ingroup bridge + */ +class HOST_SW_API CtsCorController { +public: + /** + * @brief Initializes a new instance of the CtsCorController class. + * @param port Serial port device (e.g., /dev/ttyUSB0). + */ + CtsCorController(const std::string& port); + /** + * @brief Finalizes a instance of the CtsCorController class. + */ + ~CtsCorController(); + + /** + * @brief Opens the serial port for CTS readback. + * @param reuseFd Optional file descriptor to reuse (when sharing port with RTS PTT). + * @returns bool True, if port was opened successfully, otherwise false. + */ + bool open(int reuseFd = -1); + /** + * @brief Closes the serial port. + */ + void close(); + + /** + * @brief Reads the current CTS signal state. + * @returns bool True if CTS is asserted (active), otherwise false. + */ + bool isCtsAsserted(); + +private: + std::string m_port; + bool m_isOpen; + bool m_ownsFd; // true if we opened the fd, false if reusing from RTS PTT +#if defined(_WIN32) + HANDLE m_fd; +#else + int m_fd; +#endif // defined(_WIN32) + + /** + * @brief Sets the termios settings on the serial port. + * @returns bool True, if settings are set, otherwise false. + */ + bool setTermios(); +}; + +#endif // __CTS_COR_CONTROLLER_H__ + + diff --git a/src/bridge/HostBridge.cpp b/src/bridge/HostBridge.cpp index 006e8703..aaf3742b 100644 --- a/src/bridge/HostBridge.cpp +++ b/src/bridge/HostBridge.cpp @@ -249,6 +249,15 @@ HostBridge::HostBridge(const std::string& confFile) : m_rtsPttPort(), m_rtsPttController(nullptr), m_rtsPttActive(false), + m_lastAudioOut(), + m_rtsPttHoldoffMs(250U), + m_ctsCorEnable(false), + m_ctsCorPort(), + m_ctsCorController(nullptr), + m_ctsCorActive(false), + m_ctsCorInvert(false), + m_ctsPadTimeout(1000U, 0U, 22U), + m_ctsCorHoldoffMs(250U), m_rtpSeqNo(0U), m_rtpTimestamp(INVALID_TS), m_usrpSeqNo(0U) @@ -411,6 +420,11 @@ int HostBridge::run() if (!ret) return EXIT_FAILURE; + // initialize CTS COR detection + ret = initializeCtsCor(); + if (!ret) + return EXIT_FAILURE; + ma_result result; if (m_localAudio) { // initialize audio devices @@ -950,6 +964,20 @@ bool HostBridge::readParams() m_rtsPttPort = systemConf["rtsPttPort"].as("/dev/ttyUSB0"); m_rtsPttHoldoffMs = (uint32_t)systemConf["rtsPttHoldoffMs"].as(m_rtsPttHoldoffMs); + // CTS COR Configuration + m_ctsCorEnable = systemConf["ctsCorEnable"].as(false); + m_ctsCorPort = systemConf["ctsCorPort"].as("/dev/ttyUSB0"); + m_ctsCorInvert = systemConf["ctsCorInvert"].as(false); + m_ctsCorHoldoffMs = (uint32_t)systemConf["ctsCorHoldoffMs"].as(m_ctsCorHoldoffMs); + + if (m_ctsCorEnable) { + LogInfo("CTS COR Configuration"); + LogInfo(" Enabled: yes"); + LogInfo(" Port: %s", m_ctsCorPort.c_str()); + LogInfo(" Invert: %s (%s triggers)", m_ctsCorInvert ? "yes" : "no", m_ctsCorInvert ? "LOW" : "HIGH"); + LogInfo(" Holdoff: %u ms", m_ctsCorHoldoffMs); + } + std::string txModeStr = "DMR"; if (m_txMode == TX_MODE_P25) txModeStr = "P25"; @@ -3054,7 +3082,23 @@ void* HostBridge::threadAudioProcess(void* arg) { std::lock_guard lock(s_audioMutex); - if (bridge->m_inputAudio.dataSize() >= AUDIO_SAMPLES_LENGTH) { + // When COR is active, we need to send frames continuously when audio data is available + // The audio callback should be continuously feeding data, so we should always have data available + bool hasAudioData = bridge->m_inputAudio.dataSize() >= AUDIO_SAMPLES_LENGTH; + + // Process if we have audio data + // When COR is active: process whenever we have data (which should be continuous) + // When COR is not active: only process when VOX detects audio (normal mode) + bool shouldProcess = false; + if (bridge->m_ctsCorActive && bridge->m_audioDetect) { + // COR is active: process whenever we have audio data (continuous transmission) + shouldProcess = hasAudioData; + } else if (!bridge->m_ctsCorActive && bridge->m_audioDetect) { + // Normal VOX mode: process when we have audio data + shouldProcess = hasAudioData; + } + + if (shouldProcess && hasAudioData) { short samples[AUDIO_SAMPLES_LENGTH]; bridge->m_inputAudio.get(samples, AUDIO_SAMPLES_LENGTH); @@ -3093,74 +3137,114 @@ void* HostBridge::threadAudioProcess(void* arg) bridge->m_detectedSampleCnt++; } - // handle Rx triggered by internal VOX - if (maxSample > sampleLevel) { - bridge->m_audioDetect = true; - if (bridge->m_txStreamId == 0U) { - bridge->m_txStreamId = 1U; // prevent further false starts -- this isn't the right way to handle this... - LogInfoEx(LOG_HOST, "%s, call start, srcId = %u, dstId = %u", trafficType.c_str(), srcId, dstId); - - if (bridge->m_grantDemand) { - switch (bridge->m_txMode) { - case TX_MODE_P25: - { - p25::lc::LC lc = p25::lc::LC(); - lc.setLCO(P25DEF::LCO::GROUP); - lc.setDstId(dstId); - lc.setSrcId(srcId); - - p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); - - uint8_t controlByte = network::NET_CTRL_GRANT_DEMAND; - if (bridge->m_tekAlgoId != P25DEF::ALGO_UNENCRYPT) - controlByte |= network::NET_CTRL_GRANT_ENCRYPT; - bridge->m_network->writeP25TDU(lc, lsd, controlByte); + // handle Rx triggered by internal VOX (unless COR is active, which takes precedence) + if (!bridge->m_ctsCorActive) { + if (maxSample > sampleLevel) { + bridge->m_audioDetect = true; + if (bridge->m_txStreamId == 0U) { + bridge->m_txStreamId = 1U; + LogInfoEx(LOG_HOST, "%s, call start, srcId = %u, dstId = %u", trafficType.c_str(), srcId, dstId); + + if (bridge->m_grantDemand) { + switch (bridge->m_txMode) { + case TX_MODE_P25: + { + p25::lc::LC lc = p25::lc::LC(); + lc.setLCO(P25DEF::LCO::GROUP); + lc.setDstId(dstId); + lc.setSrcId(srcId); + + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + + uint8_t controlByte = network::NET_CTRL_GRANT_DEMAND; + if (bridge->m_tekAlgoId != P25DEF::ALGO_UNENCRYPT) + controlByte |= network::NET_CTRL_GRANT_ENCRYPT; + bridge->m_network->writeP25TDU(lc, lsd, controlByte); + } + break; } - break; } } - } - bridge->m_localDropTime.stop(); - } else { - // if we've exceeded the audio drop timeout, then really drop the audio - if (bridge->m_localDropTime.isRunning() && bridge->m_localDropTime.hasExpired()) { - if (bridge->m_audioDetect) { - bridge->callEnd(srcId, dstId); + bridge->m_localDropTime.stop(); + } else { + // if we've exceeded the audio drop timeout, then really drop the audio + if (bridge->m_localDropTime.isRunning() && bridge->m_localDropTime.hasExpired()) { + if (bridge->m_audioDetect) { + bridge->callEnd(srcId, dstId); + } } - } - if (!bridge->m_localDropTime.isRunning()) - bridge->m_localDropTime.start(); + if (!bridge->m_localDropTime.isRunning()) + bridge->m_localDropTime.start(); + } } + // Send audio frames: either from actual audio input OR silence frames when COR is active if (bridge->m_audioDetect && !bridge->m_callInProgress) { - ma_uint32 pcmBytes = AUDIO_SAMPLES_LENGTH * ma_get_bytes_per_frame(bridge->m_maDevice.capture.format, bridge->m_maDevice.capture.channels); - DECLARE_UINT8_ARRAY(pcm, pcmBytes); + // If COR is active, always send the actual audio from the buffer (even if quiet) + // If COR is not active, only send when VOX detects audio + if (bridge->m_ctsCorActive) { + // COR is active: always encode actual audio from buffer + // The buffer should always have data from audio callback, even if it's quiet + ma_uint32 pcmBytes = AUDIO_SAMPLES_LENGTH * ma_get_bytes_per_frame(bridge->m_maDevice.capture.format, bridge->m_maDevice.capture.channels); + DECLARE_UINT8_ARRAY(pcm, pcmBytes); + + // Always encode the actual samples, even if they're quiet + // This ensures real audio is passed through, not just silence + int pcmIdx = 0; + for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) { + pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF); + pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF); + pcmIdx += 2; + } - int pcmIdx = 0; - for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) { - pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF); - pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF); - pcmIdx += 2; - } + switch (bridge->m_txMode) + { + case TX_MODE_DMR: + bridge->encodeDMRAudioFrame(pcm); + break; + case TX_MODE_P25: + bridge->encodeP25AudioFrame(pcm); + break; + case TX_MODE_ANALOG: + bridge->encodeAnalogAudioFrame(pcm); + break; + } + } else { + // COR is not active: normal VOX-based audio processing + if (maxSample > sampleLevel) { + ma_uint32 pcmBytes = AUDIO_SAMPLES_LENGTH * ma_get_bytes_per_frame(bridge->m_maDevice.capture.format, bridge->m_maDevice.capture.channels); + DECLARE_UINT8_ARRAY(pcm, pcmBytes); + + int pcmIdx = 0; + for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) { + pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF); + pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF); + pcmIdx += 2; + } - switch (bridge->m_txMode) - { - case TX_MODE_DMR: - bridge->encodeDMRAudioFrame(pcm); - break; - case TX_MODE_P25: - bridge->encodeP25AudioFrame(pcm); - break; - case TX_MODE_ANALOG: - bridge->encodeAnalogAudioFrame(pcm); - break; + switch (bridge->m_txMode) + { + case TX_MODE_DMR: + bridge->encodeDMRAudioFrame(pcm); + break; + case TX_MODE_P25: + bridge->encodeP25AudioFrame(pcm); + break; + case TX_MODE_ANALOG: + bridge->encodeAnalogAudioFrame(pcm); + break; + } + } } } } } + // When COR is active, we need to process frames continuously + // The audio callback should be feeding data, but if buffer is empty and COR is active, + // we'll send silence frames. Keep minimal sleep to ensure responsive processing. Thread::sleep(1U); } @@ -3171,6 +3255,127 @@ void* HostBridge::threadAudioProcess(void* arg) return nullptr; } +/* Entry point to CTS COR monitor thread. */ + +void* HostBridge::threadCtsCorMonitor(void* arg) +{ + thread_t* th = (thread_t*)arg; + if (th != nullptr) { +#if defined(_WIN32) + ::CloseHandle(th->thread); +#else + ::pthread_detach(th->thread); +#endif // defined(_WIN32) + + std::string threadName("bridge:cts-cor-monitor"); + HostBridge* bridge = static_cast(th->obj); + if (bridge == nullptr) { + g_killed = true; + LogError(LOG_HOST, "[FAIL] %s", threadName.c_str()); + } + + if (g_killed) { + delete th; + return nullptr; + } + + LogInfoEx(LOG_HOST, "[ OK ] %s", threadName.c_str()); +#ifdef _GNU_SOURCE + ::pthread_setname_np(th->thread, threadName.c_str()); +#endif // _GNU_SOURCE + + // Initialize lastCts to current state to avoid false trigger on startup + bool lastCts = false; + if (bridge->m_ctsCorEnable && bridge->m_ctsCorController != nullptr) { + bool ctsRawInit = bridge->m_ctsCorController->isCtsAsserted(); + lastCts = bridge->m_ctsCorInvert ? !ctsRawInit : ctsRawInit; + bridge->m_ctsCorActive = lastCts; + LogInfoEx(LOG_HOST, "CTS COR monitor initialized: initial state = %s (raw: %s)", + lastCts ? "TRIGGER" : "IDLE", ctsRawInit ? "HIGH" : "LOW"); + } + uint32_t pollCount = 0U; + + while (!g_killed) { + if (!bridge->m_running) { + Thread::sleep(10U); + continue; + } + + if (!bridge->m_ctsCorEnable) { + LogDebug(LOG_HOST, "CTS COR is disabled, waiting..."); + Thread::sleep(1000U); + continue; + } + + if (bridge->m_ctsCorController == nullptr) { + LogError(LOG_HOST, "CTS COR Controller is null!"); + Thread::sleep(1000U); + continue; + } + + bool ctsRaw = bridge->m_ctsCorController->isCtsAsserted(); + // Apply inversion: if invert is true, LOW triggers (so we invert the raw signal) + bool cts = bridge->m_ctsCorInvert ? !ctsRaw : ctsRaw; + pollCount++; + + if (cts != lastCts) { + LogInfoEx(LOG_HOST, "CTS COR state changed: %s -> %s (raw: %s)", + lastCts ? "TRIGGER" : "IDLE", cts ? "TRIGGER" : "IDLE", ctsRaw ? "HIGH" : "LOW"); + lastCts = cts; + bridge->m_ctsCorActive = cts; + if (cts) { + // Rising edge: force call start, immediately send silence frame, and start padding timer + uint32_t srcId = bridge->m_srcId; + uint32_t dstId = bridge->m_dstId; + if (!bridge->m_audioDetect) { + bridge->m_audioDetect = true; + if (bridge->m_txStreamId == 0U) { + bridge->m_txStreamId = 1U; + LogInfoEx(LOG_HOST, "%s, call start (CTS COR), srcId = %u, dstId = %u", LOCAL_CALL, srcId, dstId); + if (bridge->m_grantDemand) { + switch (bridge->m_txMode) { + case TX_MODE_P25: + { + p25::lc::LC lc = p25::lc::LC(); + lc.setLCO(P25DEF::LCO::GROUP); + lc.setDstId(dstId); + lc.setSrcId(srcId); + + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + + uint8_t controlByte = network::NET_CTRL_GRANT_DEMAND; + if (bridge->m_tekAlgoId != P25DEF::ALGO_UNENCRYPT) + controlByte |= network::NET_CTRL_GRANT_ENCRYPT; + bridge->m_network->writeP25TDU(lc, lsd, controlByte); + } + break; + } + } + } + } + // Stop drop timer while COR is activem audio is processing + bridge->m_localDropTime.stop(); + // Don't start padding timer + } + else { + // Falling edge: start hold-off timer before allowing call to end + bridge->m_ctsPadTimeout.stop(); + // Start drop timer with hold-off delay + bridge->m_localDropTime = Timer(1000U, 0U, bridge->m_ctsCorHoldoffMs); + bridge->m_localDropTime.start(); + } + } + + Thread::sleep(5U); + } + + LogInfoEx(LOG_HOST, "[STOP] %s", threadName.c_str()); + delete th; + } + + return nullptr; +} + /* Entry point to UDP audio processing thread. */ void* HostBridge::threadUDPAudioProcess(void* arg) @@ -3291,7 +3496,7 @@ void* HostBridge::threadUDPAudioProcess(void* arg) if ((!bridge->m_audioDetect && !bridge->m_callInProgress) || forceCallStart) { bridge->m_audioDetect = true; if (bridge->m_txStreamId == 0U) { - bridge->m_txStreamId = 1U; // prevent further false starts -- this isn't the right way to handle this... + bridge->m_txStreamId = 1U; if (forceCallStart) bridge->m_txStreamId = txStreamId; @@ -3775,6 +3980,9 @@ void* HostBridge::threadCallWatchdog(void* arg) } } + // When CTS COR is active, the audio processing thread handles frame transmission + // We don't use the watchdog thread for padding to avoid conflicts with actual audio frames + std::string trafficType = LOCAL_CALL; if (bridge->m_trafficFromUDP) trafficType = UDP_CALL; @@ -3797,10 +4005,13 @@ void* HostBridge::threadCallWatchdog(void* arg) } } else { - // if we've exceeded the drop timeout, then really drop the audio - if (bridge->m_localDropTime.isRunning() && (bridge->m_localDropTime.getTimer() >= dropTimeout)) { - LogInfoEx(LOG_HOST, "%s, terminating stuck call", trafficType.c_str()); - bridge->callEnd(srcId, dstId); + // Don't end call due to drop timeout if COR is still active + if (!bridge->m_ctsCorActive) { + // if we've exceeded the drop timeout, then really drop the audio + if (bridge->m_localDropTime.isRunning() && (bridge->m_localDropTime.getTimer() >= dropTimeout)) { + LogInfoEx(LOG_HOST, "%s, terminating stuck call", trafficType.c_str()); + bridge->callEnd(srcId, dstId); + } } } @@ -3838,6 +4049,56 @@ bool HostBridge::initializeRtsPtt() return true; } +/* Helper to initialize CTS COR detection. */ + +bool HostBridge::initializeCtsCor() +{ + if (!m_ctsCorEnable) + return true; + + if (m_ctsCorPort.empty()) { + ::LogError(LOG_HOST, "CTS COR port is not specified"); + return false; + } + + m_ctsCorController = new CtsCorController(m_ctsCorPort); + + // If RTS PTT and CTS COR are on the same port, reuse the file descriptor + // to avoid opening the port twice (which would affect RTS) + int reuseFd = -1; + if (m_rtsPttEnable && m_rtsPttController != nullptr && + m_rtsPttPort == m_ctsCorPort && m_rtsPttController->getFd() >= 0) { + reuseFd = m_rtsPttController->getFd(); + ::LogInfo(LOG_HOST, "CTS COR reusing RTS PTT file descriptor for %s (same port)", m_ctsCorPort.c_str()); + } + + if (!m_ctsCorController->open(reuseFd)) { + ::LogError(LOG_HOST, "Failed to open CTS COR port %s", m_ctsCorPort.c_str()); + delete m_ctsCorController; + m_ctsCorController = nullptr; + return false; + } + + // Start monitor thread + thread_t* th = new thread_t(); + th->obj = this; + if (!Thread::runAsThread(this, &HostBridge::threadCtsCorMonitor, th)) { + ::LogError(LOG_HOST, "Failed to start CTS COR monitor thread"); + return false; + } + + ::LogInfo(LOG_HOST, "CTS COR initialized on %s", m_ctsCorPort.c_str()); + + // Test read CTS state to verify it's working + bool ctsRaw = m_ctsCorController->isCtsAsserted(); + bool ctsEffective = m_ctsCorInvert ? !ctsRaw : ctsRaw; + ::LogInfo(LOG_HOST, "CTS COR initial state: raw=%s, effective=%s (%s)", + ctsRaw ? "HIGH" : "LOW", ctsEffective ? "TRIGGER" : "IDLE", + m_ctsCorInvert ? "inverted" : "normal"); + + return true; +} + /* Helper to assert RTS PTT (start transmission). */ void HostBridge::assertRtsPtt() diff --git a/src/bridge/HostBridge.h b/src/bridge/HostBridge.h index 088c30e7..7449aadc 100644 --- a/src/bridge/HostBridge.h +++ b/src/bridge/HostBridge.h @@ -34,6 +34,7 @@ #include "mdc/mdc_decode.h" #include "network/PeerNetwork.h" #include "RtsPttController.h" +#include "CtsCorController.h" #include #include @@ -265,6 +266,15 @@ class HOST_SW_API HostBridge { system_clock::hrc::hrc_t m_lastAudioOut; uint32_t m_rtsPttHoldoffMs; + // CTS COR Detection + bool m_ctsCorEnable; + std::string m_ctsCorPort; + CtsCorController* m_ctsCorController; + bool m_ctsCorActive; + bool m_ctsCorInvert; // if true, COR LOW triggers (instead of HIGH) + Timer m_ctsPadTimeout; // drives silence padding while CTS is active + uint32_t m_ctsCorHoldoffMs; // hold-off time before clearing COR after it deasserts + uint16_t m_rtpSeqNo; uint32_t m_rtpTimestamp; @@ -523,6 +533,11 @@ class HOST_SW_API HostBridge { * @returns bool True, if RTS PTT was initialized successfully, otherwise false. */ bool initializeRtsPtt(); + /** + * @brief Helper to initialize CTS COR detection. + * @returns bool True, if CTS COR was initialized successfully, otherwise false. + */ + bool initializeCtsCor(); /** * @brief Helper to assert RTS PTT (start transmission). */ @@ -573,6 +588,12 @@ class HOST_SW_API HostBridge { * @returns void* (Ignore) */ static void* threadCallWatchdog(void* arg); + /** + * @brief Entry point to CTS COR monitor thread. + * @param arg Instance of the thread_t structure. + * @returns void* (Ignore) + */ + static void* threadCtsCorMonitor(void* arg); }; #endif // __HOST_BRIDGE_H__ diff --git a/src/bridge/RtsPttController.cpp b/src/bridge/RtsPttController.cpp index e82be9c9..cb0c29d6 100644 --- a/src/bridge/RtsPttController.cpp +++ b/src/bridge/RtsPttController.cpp @@ -211,6 +211,17 @@ bool RtsPttController::clearPTT() return true; } +/* Gets the file descriptor for sharing with CTS COR controller. */ + +int RtsPttController::getFd() const +{ +#if defined(_WIN32) + return (int)(intptr_t)m_fd; +#else + return m_fd; +#endif // defined(_WIN32) +} + // --------------------------------------------------------------------------- // Private Class Members // --------------------------------------------------------------------------- diff --git a/src/bridge/RtsPttController.h b/src/bridge/RtsPttController.h index 5cc8860c..f607c40a 100644 --- a/src/bridge/RtsPttController.h +++ b/src/bridge/RtsPttController.h @@ -72,6 +72,12 @@ class HOST_SW_API RtsPttController { */ bool clearPTT(); + /** + * @brief Gets the file descriptor for sharing with CTS COR controller. + * @returns int File descriptor, or -1 if not open. + */ + int getFd() const; + private: std::string m_port; bool m_isOpen;