test tty writer

This commit is contained in:
awe
2026-04-09 17:32:34 +03:00
parent 8f9d8dd81a
commit 9b521641c9
3 changed files with 197 additions and 24 deletions

View File

@ -381,7 +381,7 @@ void print_help(const char* exe_name) {
<< " [internal_ref_hz:2000000]\n" << " [internal_ref_hz:2000000]\n"
<< " [di1:zero|trace|ignore]\n" << " [di1:zero|trace|ignore]\n"
<< " [duration_ms:100] [packet_limit:0] [csv:capture.csv] [svg:capture.svg]\n" << " [duration_ms:100] [packet_limit:0] [csv:capture.csv] [svg:capture.svg]\n"
<< " [live_html:live_plot.html] [live_json:live_plot.json] [tty:/dev/ttyADC_data]\n" << " [live_html:live_plot.html] [live_json:live_plot.json] [tty:/tmp/ttyADC_data]\n"
<< " [recv_block:32768] [stats_period_ms:1000] [live_update_period_ms:1000] [svg_history_packets:50] [start_wait_ms:10000]\n" << " [recv_block:32768] [stats_period_ms:1000] [live_update_period_ms:1000] [svg_history_packets:50] [start_wait_ms:10000]\n"
<< " [buffer_words:8388608] [step_words:32768]\n" << " [buffer_words:8388608] [step_words:32768]\n"
<< " [pullup_syn1] [pullup_syn2] [pulldown_conv_in] [pulldown_start_in]\n" << " [pullup_syn1] [pullup_syn2] [pulldown_conv_in] [pulldown_start_in]\n"
@ -412,7 +412,7 @@ void print_help(const char* exe_name) {
<< " buffer_words:8388608 -> input stream buffer size in 32-bit words\n" << " buffer_words:8388608 -> input stream buffer size in 32-bit words\n"
<< " step_words:32768 -> input stream transfer step in 32-bit words\n" << " step_words:32768 -> input stream transfer step in 32-bit words\n"
<< " live_html/live_json -> live graph files updated as packets arrive\n" << " live_html/live_json -> live graph files updated as packets arrive\n"
<< " tty:/dev/ttyADC_data -> write binary step frames to a Linux/POSIX tty path\n" << " tty:/tmp/ttyADC_data -> write binary step frames to a Linux/POSIX tty or PTY link path\n"
<< " If sample_clock_hz is omitted together with clock:internal, the maximum ADC speed is used\n" << " If sample_clock_hz is omitted together with clock:internal, the maximum ADC speed is used\n"
<< "\n" << "\n"
<< "Differential physical channel mapping:\n" << "Differential physical channel mapping:\n"
@ -747,6 +747,19 @@ Fn load_symbol(ModuleHandle module, const char* name) {
#endif #endif
} }
template <typename Fn>
Fn try_load_symbol(ModuleHandle module, const char* name) {
#ifdef _WIN32
const auto addr = GetProcAddress(module, name);
return reinterpret_cast<Fn>(addr);
#else
dlerror();
void* addr = dlsym(module, name);
(void) dlerror();
return reinterpret_cast<Fn>(addr);
#endif
}
struct Api { struct Api {
ModuleHandle x502_module = nullptr; ModuleHandle x502_module = nullptr;
ModuleHandle e502_module = nullptr; ModuleHandle e502_module = nullptr;
@ -755,6 +768,7 @@ struct Api {
decltype(&X502_Free) Free = nullptr; decltype(&X502_Free) Free = nullptr;
decltype(&X502_Close) Close = nullptr; decltype(&X502_Close) Close = nullptr;
decltype(&X502_GetErrorString) GetErrorString = nullptr; decltype(&X502_GetErrorString) GetErrorString = nullptr;
decltype(&X502_GetDevInfo) GetDevInfo = nullptr;
decltype(&X502_GetDevInfo2) GetDevInfo2 = nullptr; decltype(&X502_GetDevInfo2) GetDevInfo2 = nullptr;
decltype(&X502_SetMode) SetMode = nullptr; decltype(&X502_SetMode) SetMode = nullptr;
decltype(&X502_StreamsStop) StreamsStop = nullptr; decltype(&X502_StreamsStop) StreamsStop = nullptr;
@ -804,7 +818,11 @@ struct Api {
Free = load_symbol<decltype(Free)>(x502_module, "X502_Free"); Free = load_symbol<decltype(Free)>(x502_module, "X502_Free");
Close = load_symbol<decltype(Close)>(x502_module, "X502_Close"); Close = load_symbol<decltype(Close)>(x502_module, "X502_Close");
GetErrorString = load_symbol<decltype(GetErrorString)>(x502_module, "X502_GetErrorString"); GetErrorString = load_symbol<decltype(GetErrorString)>(x502_module, "X502_GetErrorString");
GetDevInfo2 = load_symbol<decltype(GetDevInfo2)>(x502_module, "X502_GetDevInfo2"); GetDevInfo = try_load_symbol<decltype(GetDevInfo)>(x502_module, "X502_GetDevInfo");
GetDevInfo2 = try_load_symbol<decltype(GetDevInfo2)>(x502_module, "X502_GetDevInfo2");
if ((GetDevInfo == nullptr) && (GetDevInfo2 == nullptr)) {
fail("Neither X502_GetDevInfo nor X502_GetDevInfo2 is available in x502 API library");
}
SetMode = load_symbol<decltype(SetMode)>(x502_module, "X502_SetMode"); SetMode = load_symbol<decltype(SetMode)>(x502_module, "X502_SetMode");
StreamsStop = load_symbol<decltype(StreamsStop)>(x502_module, "X502_StreamsStop"); StreamsStop = load_symbol<decltype(StreamsStop)>(x502_module, "X502_StreamsStop");
StreamsDisable = load_symbol<decltype(StreamsDisable)>(x502_module, "X502_StreamsDisable"); StreamsDisable = load_symbol<decltype(StreamsDisable)>(x502_module, "X502_StreamsDisable");
@ -859,6 +877,8 @@ void expect_ok(const Api& api, int32_t err, const std::string& what) {
constexpr uint32_t kE502DiSyn2Mask = constexpr uint32_t kE502DiSyn2Mask =
(static_cast<uint32_t>(1U) << 13U) | (static_cast<uint32_t>(1U) << 17U); (static_cast<uint32_t>(1U) << 13U) | (static_cast<uint32_t>(1U) << 17U);
constexpr uint32_t kE502Digital1Mask = (static_cast<uint32_t>(1U) << 0U); constexpr uint32_t kE502Digital1Mask = (static_cast<uint32_t>(1U) << 0U);
constexpr uint32_t kStreamInputAdcFlag = 0x80000000U;
constexpr uint32_t kStreamInputCalibratedAdcFlag = 0x40000000U;
using TickMs = uint64_t; using TickMs = uint64_t;
@ -1012,6 +1032,30 @@ int16_t pack_raw_code_to_int16(double avg_raw_code) {
return static_cast<int16_t>(clamped); return static_cast<int16_t>(clamped);
} }
bool try_extract_raw_adc_code(uint32_t word, double& raw_code) {
if ((word & kStreamInputAdcFlag) == 0U) {
return false;
}
int32_t value = 0;
if ((word & kStreamInputCalibratedAdcFlag) != 0U) {
const uint32_t payload = word & 0x00FFFFFFU;
value = static_cast<int32_t>(payload);
if ((payload & 0x00800000U) != 0U) {
value |= static_cast<int32_t>(0xFF000000U);
}
} else {
const uint32_t payload = word & 0x0000FFFFU;
value = static_cast<int32_t>(payload);
if ((payload & 0x00008000U) != 0U) {
value |= static_cast<int32_t>(0xFFFF0000U);
}
}
raw_code = static_cast<double>(value);
return true;
}
struct ConsoleCtrlGuard { struct ConsoleCtrlGuard {
bool installed = false; bool installed = false;
@ -1113,7 +1157,11 @@ int run(const Config& cfg) {
device.opened = true; device.opened = true;
t_x502_info info{}; t_x502_info info{};
err = api.GetDevInfo2(device.hnd, &info, sizeof(info)); if (api.GetDevInfo2 != nullptr) {
err = api.GetDevInfo2(device.hnd, &info, sizeof(info));
} else {
err = api.GetDevInfo(device.hnd, &info);
}
expect_ok(api, err, "Get device info"); expect_ok(api, err, "Get device info");
print_device_info(info); print_device_info(info);
@ -1356,7 +1404,9 @@ int run(const Config& cfg) {
return; return;
} }
if (tty_step.next_step_index >= 0xFFFFU) { if (tty_step.next_step_index >= 0xFFFFU) {
fail("TTY protocol step index overflow within packet"); std::cerr << "Warning: TTY protocol step index overflow, forcing tty packet restart\n";
tty_step.reset_packet();
tty_writer->emit_packet_start();
} }
const double ch1_avg = tty_step.sum_ch1 / static_cast<double>(tty_step.count_ch1); const double ch1_avg = tty_step.sum_ch1 / static_cast<double>(tty_step.count_ch1);
@ -1531,18 +1581,18 @@ int run(const Config& cfg) {
uint32_t raw_adc_count = 0; uint32_t raw_adc_count = 0;
if (tty_writer) { if (tty_writer) {
raw_adc_count = static_cast<uint32_t>(adc_raw_buffer.size()); for (std::size_t i = 0; i < recvd; ++i) {
const int32_t raw_process_err = api.ProcessData(device.hnd, double raw_code = 0.0;
raw.data(), if (!try_extract_raw_adc_code(raw[i], raw_code)) {
static_cast<uint32_t>(recvd), continue;
0, }
adc_raw_buffer.data(), if (raw_adc_count >= adc_raw_buffer.size()) {
&raw_adc_count, fail("Raw ADC parsing overflowed the temporary buffer");
nullptr, }
nullptr); adc_raw_buffer[raw_adc_count++] = raw_code;
expect_ok(api, raw_process_err, "Process raw ADC data"); }
if (raw_adc_count != adc_count) { if (raw_adc_count != adc_count) {
fail("Raw ADC processing returned a different sample count than voltage processing"); fail("Raw ADC parsing returned a different sample count than voltage processing");
} }
} }
@ -1624,7 +1674,14 @@ int run(const Config& cfg) {
start_packet(); start_packet();
} }
if (!packet_active && start_edge) { if (packet_active && start_edge) {
finish_packet(PacketCloseReason::ExternalStopEdge);
if ((cfg.packet_limit != 0U) && (total_completed_packets >= cfg.packet_limit)) {
stop_loop_requested = true;
continue;
}
start_packet();
} else if (!packet_active && start_edge) {
start_packet(); start_packet();
} }

View File

@ -38,11 +38,17 @@ void TtyProtocolWriter::close_fd() noexcept {}
#else #else
#include <array>
#include <cerrno> #include <cerrno>
#include <cstring> #include <cstring>
#include <fcntl.h> #include <fcntl.h>
#include <limits.h>
#include <optional>
#include <pty.h>
#include <sstream> #include <sstream>
#include <sys/stat.h>
#include <sys/types.h> #include <sys/types.h>
#include <termios.h>
#include <unistd.h> #include <unistd.h>
namespace { namespace {
@ -53,31 +59,113 @@ std::string io_error(const std::string& action, const std::string& path) {
return out.str(); return out.str();
} }
} // namespace void set_fd_raw(int fd) {
struct termios tio {};
TtyProtocolWriter::TtyProtocolWriter(std::string path) : path_(std::move(path)) { if (::tcgetattr(fd, &tio) != 0) {
fd_ = ::open(path_.c_str(), O_WRONLY | O_NOCTTY); throw std::runtime_error(io_error("Cannot read tty attributes for", std::to_string(fd)));
if (fd_ < 0) { }
throw std::runtime_error(io_error("Cannot open tty output", path_)); ::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() { TtyProtocolWriter::~TtyProtocolWriter() {
remove_owned_link();
close_slave_fd();
close_fd(); close_fd();
} }
TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept
: path_(std::move(other.path_)), : path_(std::move(other.path_)),
fd_(other.fd_) { fd_(other.fd_),
slave_fd_(other.slave_fd_),
slave_path_(std::move(other.slave_path_)),
owns_link_(other.owns_link_) {
other.fd_ = -1; other.fd_ = -1;
other.slave_fd_ = -1;
other.owns_link_ = false;
} }
TtyProtocolWriter& TtyProtocolWriter::operator=(TtyProtocolWriter&& other) noexcept { TtyProtocolWriter& TtyProtocolWriter::operator=(TtyProtocolWriter&& other) noexcept {
if (this != &other) { if (this != &other) {
remove_owned_link();
close_slave_fd();
close_fd(); close_fd();
path_ = std::move(other.path_); path_ = std::move(other.path_);
fd_ = other.fd_; fd_ = other.fd_;
slave_fd_ = other.slave_fd_;
slave_path_ = std::move(other.slave_path_);
owns_link_ = other.owns_link_;
other.fd_ = -1; other.fd_ = -1;
other.slave_fd_ = -1;
other.owns_link_ = false;
} }
return *this; return *this;
} }
@ -125,4 +213,27 @@ void TtyProtocolWriter::close_fd() noexcept {
} }
} }
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 #endif

View File

@ -21,9 +21,14 @@ public:
private: private:
void write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const; void write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const;
void close_fd() noexcept; void close_fd() noexcept;
void close_slave_fd() noexcept;
void remove_owned_link() noexcept;
std::string path_; std::string path_;
#ifndef _WIN32 #ifndef _WIN32
int fd_ = -1; int fd_ = -1;
int slave_fd_ = -1;
std::string slave_path_;
bool owns_link_ = false;
#endif #endif
}; };