Files
kamil_adc/tty_protocol_writer.cpp
2026-04-09 17:32:34 +03:00

240 lines
6.7 KiB
C++

#include "tty_protocol_writer.h"
#include <stdexcept>
#include <utility>
#ifdef _WIN32
TtyProtocolWriter::TtyProtocolWriter(std::string path) : path_(std::move(path)) {
throw std::runtime_error("tty output is supported only on Linux/POSIX");
}
TtyProtocolWriter::~TtyProtocolWriter() = default;
TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept = default;
TtyProtocolWriter& TtyProtocolWriter::operator=(TtyProtocolWriter&& other) noexcept = default;
void TtyProtocolWriter::emit_packet_start() const {}
void TtyProtocolWriter::emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) const {
(void) index;
(void) ch1_avg;
(void) ch2_avg;
}
const std::string& TtyProtocolWriter::path() const {
return path_;
}
void TtyProtocolWriter::write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const {
(void) word0;
(void) word1;
(void) word2;
(void) word3;
}
void TtyProtocolWriter::close_fd() noexcept {}
#else
#include <array>
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <limits.h>
#include <optional>
#include <pty.h>
#include <sstream>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
namespace {
std::string io_error(const std::string& action, const std::string& path) {
std::ostringstream out;
out << action << " '" << path << "': " << std::strerror(errno);
return out.str();
}
void set_fd_raw(int fd) {
struct termios tio {};
if (::tcgetattr(fd, &tio) != 0) {
throw std::runtime_error(io_error("Cannot read tty attributes for", std::to_string(fd)));
}
::cfmakeraw(&tio);
if (::tcsetattr(fd, TCSANOW, &tio) != 0) {
throw std::runtime_error(io_error("Cannot apply raw tty attributes to", std::to_string(fd)));
}
}
bool is_character_device_path(const std::string& path) {
struct stat st {};
if (::stat(path.c_str(), &st) != 0) {
if (errno == ENOENT) {
return false;
}
throw std::runtime_error(io_error("Cannot stat tty output", path));
}
return S_ISCHR(st.st_mode);
}
std::optional<std::string> read_link_target(const std::string& path) {
std::array<char, PATH_MAX> buf {};
const ssize_t len = ::readlink(path.c_str(), buf.data(), buf.size() - 1U);
if (len < 0) {
if (errno == EINVAL || errno == ENOENT) {
return std::nullopt;
}
throw std::runtime_error(io_error("Cannot read symlink", path));
}
buf[static_cast<std::size_t>(len)] = '\0';
return std::string(buf.data());
}
} // namespace
TtyProtocolWriter::TtyProtocolWriter(std::string path) : path_(std::move(path)) {
if (is_character_device_path(path_)) {
fd_ = ::open(path_.c_str(), O_WRONLY | O_NOCTTY);
if (fd_ < 0) {
throw std::runtime_error(io_error("Cannot open tty output", path_));
}
return;
}
std::array<char, PATH_MAX> slave_name {};
if (::openpty(&fd_, &slave_fd_, slave_name.data(), nullptr, nullptr) != 0) {
throw std::runtime_error(io_error("Cannot create PTY bridge for", path_));
}
try {
slave_path_ = slave_name.data();
set_fd_raw(slave_fd_);
struct stat st {};
if (::lstat(path_.c_str(), &st) == 0) {
if (!S_ISLNK(st.st_mode) && !S_ISREG(st.st_mode)) {
throw std::runtime_error("Refusing to replace non-link path '" + path_ + "'");
}
if (::unlink(path_.c_str()) != 0) {
throw std::runtime_error(io_error("Cannot remove existing tty link", path_));
}
} else if (errno != ENOENT) {
throw std::runtime_error(io_error("Cannot inspect tty link path", path_));
}
if (::symlink(slave_path_.c_str(), path_.c_str()) != 0) {
throw std::runtime_error(io_error("Cannot create tty symlink", path_));
}
} catch (...) {
close_slave_fd();
close_fd();
throw;
}
owns_link_ = true;
}
TtyProtocolWriter::~TtyProtocolWriter() {
remove_owned_link();
close_slave_fd();
close_fd();
}
TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept
: path_(std::move(other.path_)),
fd_(other.fd_),
slave_fd_(other.slave_fd_),
slave_path_(std::move(other.slave_path_)),
owns_link_(other.owns_link_) {
other.fd_ = -1;
other.slave_fd_ = -1;
other.owns_link_ = false;
}
TtyProtocolWriter& TtyProtocolWriter::operator=(TtyProtocolWriter&& other) noexcept {
if (this != &other) {
remove_owned_link();
close_slave_fd();
close_fd();
path_ = std::move(other.path_);
fd_ = other.fd_;
slave_fd_ = other.slave_fd_;
slave_path_ = std::move(other.slave_path_);
owns_link_ = other.owns_link_;
other.fd_ = -1;
other.slave_fd_ = -1;
other.owns_link_ = false;
}
return *this;
}
void TtyProtocolWriter::emit_packet_start() const {
write_frame(0x000A, 0xFFFF, 0xFFFF, 0xFFFF);
}
void TtyProtocolWriter::emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) const {
write_frame(0x000A,
index,
static_cast<uint16_t>(ch1_avg),
static_cast<uint16_t>(ch2_avg));
}
const std::string& TtyProtocolWriter::path() const {
return path_;
}
void TtyProtocolWriter::write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const {
const uint16_t frame[4] = {word0, word1, word2, word3};
const std::uint8_t* bytes = reinterpret_cast<const std::uint8_t*>(frame);
std::size_t remaining = sizeof(frame);
while (remaining != 0U) {
const ssize_t written = ::write(fd_, bytes, remaining);
if (written < 0) {
if (errno == EINTR) {
continue;
}
throw std::runtime_error(io_error("Cannot write tty frame to", path_));
}
if (written == 0) {
throw std::runtime_error("tty write returned 0 bytes for '" + path_ + "'");
}
bytes += static_cast<std::size_t>(written);
remaining -= static_cast<std::size_t>(written);
}
}
void TtyProtocolWriter::close_fd() noexcept {
if (fd_ >= 0) {
::close(fd_);
fd_ = -1;
}
}
void TtyProtocolWriter::close_slave_fd() noexcept {
if (slave_fd_ >= 0) {
::close(slave_fd_);
slave_fd_ = -1;
}
}
void TtyProtocolWriter::remove_owned_link() noexcept {
if (!owns_link_ || path_.empty()) {
return;
}
try {
const auto target = read_link_target(path_);
if (target && (*target == slave_path_)) {
::unlink(path_.c_str());
}
} catch (...) {
}
owns_link_ = false;
}
#endif