Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions configs/bridge-config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
239 changes: 239 additions & 0 deletions src/bridge/CtsCorController.cpp
Original file line number Diff line number Diff line change
@@ -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 <errno.h>
#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, &currentModemState) < 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, &currentModemState) < 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;
}


83 changes: 83 additions & 0 deletions src/bridge/CtsCorController.h
Original file line number Diff line number Diff line change
@@ -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 <string>

#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#else
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>
#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__


Loading