Merge branch 'main' of ssh://git.radiophotonics.ru:2222/awe/kamil_adc

This commit is contained in:
kamil
2026-04-09 16:59:13 +03:00
4 changed files with 445 additions and 41 deletions

0
.codex Normal file
View File

329
main.cpp
View File

@ -15,9 +15,12 @@
#endif
#include "capture_file_writer.h"
#include "tty_protocol_writer.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
@ -26,12 +29,18 @@
#include <iomanip>
#include <iostream>
#include <limits>
#include <memory>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#ifndef _WIN32
#include <csignal>
#include <dlfcn.h>
#endif
namespace {
enum class StopMode {
@ -94,6 +103,7 @@ struct Config {
std::string svg_path = "capture.svg";
std::string live_html_path = "live_plot.html";
std::string live_json_path = "live_plot.json";
std::optional<std::string> tty_path;
};
[[noreturn]] void fail(const std::string& message) {
@ -408,6 +418,7 @@ void print_help(const char* exe_name) {
<< " buffer_words:8388608 -> input stream buffer size 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"
<< " tty:/dev/ttyADC_data -> write binary step frames to a Linux/POSIX tty path\n"
<< " If sample_clock_hz is omitted together with clock:internal, the maximum ADC speed is used\n"
<< "\n"
<< "Differential physical channel mapping:\n"
@ -598,6 +609,10 @@ Config parse_args(int argc, char** argv) {
cfg.live_json_path = arg.substr(10);
continue;
}
if (starts_with(arg, "tty:")) {
cfg.tty_path = arg.substr(4);
continue;
}
fail("Unknown argument: " + arg);
}
@ -637,6 +652,9 @@ Config parse_args(int argc, char** argv) {
if (cfg.input_buffer_words < cfg.recv_block_words) {
cfg.input_buffer_words = cfg.recv_block_words;
}
if (cfg.tty_path && (cfg.channel_count != 2U)) {
fail("tty output requires channels:2");
}
if (sync_uses_di_syn1(cfg.sync_mode) && sync_uses_di_syn1(cfg.sync_start_mode)) {
fail("clock and start cannot both use DI_SYN1; use start_in or immediate start");
}
@ -672,18 +690,87 @@ Config parse_args(int argc, char** argv) {
return cfg;
}
#ifdef _WIN32
using ModuleHandle = HMODULE;
#else
using ModuleHandle = void*;
#endif
std::string dynamic_loader_error() {
#ifdef _WIN32
return "unknown error";
#else
const char* error = dlerror();
return ((error != nullptr) && (*error != '\0')) ? std::string(error) : std::string("unknown error");
#endif
}
ModuleHandle open_library(const char* path) {
#ifdef _WIN32
return LoadLibraryA(path);
#else
dlerror();
return dlopen(path, RTLD_LAZY | RTLD_LOCAL);
#endif
}
void close_library(ModuleHandle module) {
#ifdef _WIN32
if (module != nullptr) {
FreeLibrary(module);
}
#else
if (module != nullptr) {
dlclose(module);
}
#endif
}
ModuleHandle load_library_or_fail(const std::vector<std::string>& candidates,
const std::string& description) {
std::string last_error = "no candidates provided";
for (const auto& candidate : candidates) {
ModuleHandle module = open_library(candidate.c_str());
if (module != nullptr) {
return module;
}
last_error = dynamic_loader_error();
}
std::ostringstream out;
out << "Cannot load " << description << ". Tried:";
for (const auto& candidate : candidates) {
out << " " << candidate;
}
out << ". Last error: " << last_error;
fail(out.str());
}
template <typename Fn>
Fn load_symbol(HMODULE module, const char* name) {
Fn load_symbol(ModuleHandle module, const char* name) {
#ifdef _WIN32
const auto addr = GetProcAddress(module, name);
if (addr == nullptr) {
fail(std::string("GetProcAddress failed for symbol: ") + name);
}
return reinterpret_cast<Fn>(addr);
#else
dlerror();
void* addr = dlsym(module, name);
const char* error = dlerror();
if ((addr == nullptr) || (error != nullptr)) {
std::ostringstream out;
out << "dlsym failed for symbol " << name << ": "
<< ((error != nullptr) ? error : "unknown error");
fail(out.str());
}
return reinterpret_cast<Fn>(addr);
#endif
}
struct Api {
HMODULE x502_module = nullptr;
HMODULE e502_module = nullptr;
ModuleHandle x502_module = nullptr;
ModuleHandle e502_module = nullptr;
decltype(&X502_Create) Create = nullptr;
decltype(&X502_Free) Free = nullptr;
@ -719,14 +806,20 @@ struct Api {
decltype(&E502_OpenByIpAddr) OpenByIpAddr = nullptr;
Api() {
x502_module = LoadLibraryA("x502api.dll");
if (x502_module == nullptr) {
fail("Cannot load x502api.dll");
}
e502_module = LoadLibraryA("e502api.dll");
if (e502_module == nullptr) {
fail("Cannot load e502api.dll");
}
x502_module = load_library_or_fail(
#ifdef _WIN32
{"x502api.dll"},
#else
{"libx502api.so", "x502api.so", "./libx502api.so", "./x502api.so"},
#endif
"x502 API library");
e502_module = load_library_or_fail(
#ifdef _WIN32
{"e502api.dll"},
#else
{"libe502api.so", "e502api.so", "./libe502api.so", "./e502api.so"},
#endif
"e502 API library");
Create = load_symbol<decltype(Create)>(x502_module, "X502_Create");
Free = load_symbol<decltype(Free)>(x502_module, "X502_Free");
@ -763,12 +856,8 @@ struct Api {
}
~Api() {
if (e502_module != nullptr) {
FreeLibrary(e502_module);
}
if (x502_module != nullptr) {
FreeLibrary(x502_module);
}
close_library(e502_module);
close_library(x502_module);
}
};
@ -792,6 +881,19 @@ constexpr uint32_t kE502DiSyn2Mask =
(static_cast<uint32_t>(1U) << 13U) | (static_cast<uint32_t>(1U) << 17U);
constexpr uint32_t kE502Digital1Mask = (static_cast<uint32_t>(1U) << 0U);
using TickMs = uint64_t;
TickMs tick_count_ms() {
#ifdef _WIN32
return static_cast<TickMs>(GetTickCount64());
#else
using namespace std::chrono;
return static_cast<TickMs>(
duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count());
#endif
}
#ifdef _WIN32
volatile LONG g_console_stop_requested = 0;
BOOL WINAPI console_ctrl_handler(DWORD ctrl_type) {
@ -805,6 +907,17 @@ BOOL WINAPI console_ctrl_handler(DWORD ctrl_type) {
bool console_stop_requested() {
return InterlockedCompareExchange(&g_console_stop_requested, 0, 0) != 0;
}
#else
volatile std::sig_atomic_t g_console_stop_requested = 0;
void console_ctrl_handler(int) {
g_console_stop_requested = 1;
}
bool console_stop_requested() {
return g_console_stop_requested != 0;
}
#endif
enum class PacketCloseReason {
ExternalStopEdge,
@ -878,19 +991,97 @@ struct PacketAccumulator {
}
};
struct TtyStepAccumulator {
double sum_ch1 = 0.0;
double sum_ch2 = 0.0;
uint32_t count_ch1 = 0;
uint32_t count_ch2 = 0;
uint32_t next_step_index = 1;
void clear_step() {
sum_ch1 = 0.0;
sum_ch2 = 0.0;
count_ch1 = 0;
count_ch2 = 0;
}
void reset_packet() {
clear_step();
next_step_index = 1;
}
void add_sample(uint32_t lch, double raw_code) {
if (lch == 0U) {
sum_ch1 += raw_code;
++count_ch1;
} else if (lch == 1U) {
sum_ch2 += raw_code;
++count_ch2;
}
}
bool has_complete_step() const {
return (count_ch1 != 0U) && (count_ch2 != 0U);
}
};
int16_t pack_raw_code_to_int16(double avg_raw_code) {
const double scaled =
avg_raw_code * 32767.0 / static_cast<double>(X502_ADC_SCALE_CODE_MAX);
const long long rounded = std::llround(scaled);
const long long clamped = std::clamp<long long>(rounded, -32768LL, 32767LL);
return static_cast<int16_t>(clamped);
}
struct ConsoleCtrlGuard {
bool installed = false;
#ifndef _WIN32
bool sigint_installed = false;
bool sigterm_installed = false;
bool sigabrt_installed = false;
struct sigaction old_sigint {};
struct sigaction old_sigterm {};
struct sigaction old_sigabrt {};
#endif
ConsoleCtrlGuard() {
#ifdef _WIN32
InterlockedExchange(&g_console_stop_requested, 0);
installed = SetConsoleCtrlHandler(console_ctrl_handler, TRUE) != 0;
#else
g_console_stop_requested = 0;
struct sigaction action {};
action.sa_handler = console_ctrl_handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigint_installed = sigaction(SIGINT, &action, &old_sigint) == 0;
sigterm_installed = sigaction(SIGTERM, &action, &old_sigterm) == 0;
sigabrt_installed = sigaction(SIGABRT, &action, &old_sigabrt) == 0;
installed = sigint_installed && sigterm_installed && sigabrt_installed;
#endif
}
~ConsoleCtrlGuard() {
#ifdef _WIN32
if (installed) {
SetConsoleCtrlHandler(console_ctrl_handler, FALSE);
}
InterlockedExchange(&g_console_stop_requested, 0);
#else
if (sigint_installed) {
sigaction(SIGINT, &old_sigint, nullptr);
}
if (sigterm_installed) {
sigaction(SIGTERM, &old_sigterm, nullptr);
}
if (sigabrt_installed) {
sigaction(SIGABRT, &old_sigabrt, nullptr);
}
g_console_stop_requested = 0;
#endif
}
};
@ -1060,17 +1251,12 @@ int run(const Config& cfg) {
<< " ADC range: +/-" << range_to_volts(cfg.range) << " V\n"
<< " max frames per packet per channel: " << target_frames << "\n";
CaptureFileWriter writer(cfg.csv_path,
cfg.svg_path,
cfg.live_html_path,
cfg.live_json_path,
cfg.full_res_live,
cfg.live_history_packets,
cfg.di1_group_average);
CaptureFileWriter writer(cfg.csv_path, cfg.svg_path, cfg.live_html_path, cfg.live_json_path);
writer.initialize_live_plot();
writer.initialize_csv(cfg.channel_count, cfg.di1_mode == Di1Mode::Trace);
std::cout << " live plot html: " << writer.live_html_path() << "\n"
<< " live plot data: " << writer.live_json_path() << "\n"
<< " tty step output: " << (cfg.tty_path ? *cfg.tty_path : std::string("disabled")) << "\n"
<< " recv block words: " << cfg.recv_block_words << "\n"
<< " input step words: " << cfg.input_step_words << "\n"
<< " input buffer words: " << cfg.input_buffer_words << "\n"
@ -1094,11 +1280,14 @@ int run(const Config& cfg) {
const uint32_t read_capacity_words = std::max(cfg.recv_block_words, cfg.input_step_words);
std::vector<uint32_t> raw(read_capacity_words);
std::vector<double> adc_buffer(read_capacity_words);
std::vector<double> adc_raw_buffer(read_capacity_words);
std::vector<uint32_t> din_buffer(read_capacity_words);
std::deque<double> pending_adc;
std::deque<double> pending_adc_raw;
std::deque<uint32_t> pending_din;
std::deque<CapturePacket> packets;
PacketAccumulator current_packet;
TtyStepAccumulator tty_step;
current_packet.reset(target_frames, cfg.channel_count);
std::size_t csv_global_frame_index = 0;
@ -1111,11 +1300,11 @@ int run(const Config& cfg) {
uint32_t next_lch = 0;
bool di1_initialized = false;
bool di1_prev_level = false;
const ULONGLONG start_wait_deadline = GetTickCount64() + cfg.start_wait_ms;
const ULONGLONG capture_loop_start = GetTickCount64();
ULONGLONG stats_window_start = capture_loop_start;
ULONGLONG last_stats_print = capture_loop_start;
ULONGLONG last_live_update = 0;
const TickMs start_wait_deadline = tick_count_ms() + cfg.start_wait_ms;
const TickMs capture_loop_start = tick_count_ms();
TickMs stats_window_start = capture_loop_start;
TickMs last_stats_print = capture_loop_start;
TickMs last_live_update = 0;
uint64_t total_raw_words = 0;
uint64_t total_adc_samples = 0;
@ -1133,7 +1322,7 @@ int run(const Config& cfg) {
uint64_t stats_completed_packets = 0;
auto print_stats = [&](bool final_report) {
const ULONGLONG now = GetTickCount64();
const TickMs now = tick_count_ms();
const double elapsed_s = std::max(1e-9, static_cast<double>(now - stats_window_start) / 1000.0);
const double mb_per_s = (static_cast<double>(stats_raw_words) * sizeof(uint32_t)) / elapsed_s / 1000.0 / 1000.0;
const double adc_samples_per_s = static_cast<double>(stats_adc_samples) / elapsed_s;
@ -1177,9 +1366,29 @@ int run(const Config& cfg) {
return;
}
current_packet.reset(target_frames, cfg.channel_count);
if (tty_writer) {
tty_step.reset_packet();
tty_writer->emit_packet_start();
}
packet_active = true;
};
auto emit_tty_step = [&]() {
if (!tty_writer || !tty_step.has_complete_step()) {
return;
}
if (tty_step.next_step_index >= 0xFFFFU) {
fail("TTY protocol step index overflow within packet");
}
const double ch1_avg = tty_step.sum_ch1 / static_cast<double>(tty_step.count_ch1);
const double ch2_avg = tty_step.sum_ch2 / static_cast<double>(tty_step.count_ch2);
tty_writer->emit_step(static_cast<uint16_t>(tty_step.next_step_index),
pack_raw_code_to_int16(ch1_avg),
pack_raw_code_to_int16(ch2_avg));
++tty_step.next_step_index;
};
auto finish_packet = [&](PacketCloseReason reason) {
const std::size_t frames = current_packet.frame_count(cfg.channel_count);
if (frames != 0U) {
@ -1232,12 +1441,12 @@ int run(const Config& cfg) {
}
const double elapsed_capture_s =
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0);
std::max(1e-9, static_cast<double>(tick_count_ms() - capture_loop_start) / 1000.0);
const double packets_per_second =
(elapsed_capture_s >= 0.1)
? (static_cast<double>(total_completed_packets) / elapsed_capture_s)
: 0.0;
const ULONGLONG now = GetTickCount64();
const TickMs now = tick_count_ms();
const bool should_update_live =
(cfg.live_update_period_ms == 0U) ||
(last_live_update == 0U) ||
@ -1262,6 +1471,7 @@ int run(const Config& cfg) {
packet_active = false;
current_packet.reset(target_frames, cfg.channel_count);
tty_step.clear_step();
};
while (!stop_loop_requested) {
@ -1295,7 +1505,7 @@ int run(const Config& cfg) {
stats_raw_words += static_cast<uint64_t>(recvd);
}
if (recvd == 0) {
if (!capture_started && (GetTickCount64() >= start_wait_deadline)) {
if (!capture_started && (tick_count_ms() >= start_wait_deadline)) {
std::ostringstream message;
message << "Timeout before first ADC data. start="
<< sync_mode_to_string(cfg.sync_start_mode, true)
@ -1341,7 +1551,24 @@ int run(const Config& cfg) {
}
expect_ok(api, process_err, "Process ADC+DIN data");
if ((adc_count == 0U) && (din_count == 0U)) {
uint32_t raw_adc_count = 0;
if (tty_writer) {
raw_adc_count = static_cast<uint32_t>(adc_raw_buffer.size());
const int32_t raw_process_err = api.ProcessData(device.hnd,
raw.data(),
static_cast<uint32_t>(recvd),
0,
adc_raw_buffer.data(),
&raw_adc_count,
nullptr,
nullptr);
expect_ok(api, raw_process_err, "Process raw ADC data");
if (raw_adc_count != adc_count) {
fail("Raw ADC processing returned a different sample count than voltage processing");
}
}
if ((adc_count == 0U) && (din_count == 0U) && (!tty_writer || (raw_adc_count == 0U))) {
continue;
}
@ -1353,19 +1580,31 @@ int run(const Config& cfg) {
for (uint32_t i = 0; i < adc_count; ++i) {
pending_adc.push_back(adc_buffer[i]);
}
if (tty_writer) {
for (uint32_t i = 0; i < raw_adc_count; ++i) {
pending_adc_raw.push_back(adc_raw_buffer[i]);
}
}
for (uint32_t i = 0; i < din_count; ++i) {
pending_din.push_back(din_buffer[i]);
}
if ((pending_adc.size() > 1000000U) || (pending_din.size() > 1000000U)) {
if ((pending_adc.size() > 1000000U) ||
(pending_din.size() > 1000000U) ||
(tty_writer && (pending_adc_raw.size() > 1000000U))) {
fail("Internal backlog grew too large while aligning ADC and DIN samples");
}
while (!pending_adc.empty() &&
!pending_din.empty() &&
(!tty_writer || !pending_adc_raw.empty()) &&
!stop_loop_requested) {
const double adc_value = pending_adc.front();
pending_adc.pop_front();
const double adc_raw_value = tty_writer ? pending_adc_raw.front() : 0.0;
if (tty_writer) {
pending_adc_raw.pop_front();
}
const uint32_t din_value = pending_din.front();
pending_din.pop_front();
@ -1423,6 +1662,11 @@ int run(const Config& cfg) {
continue;
}
if (tty_writer && di1_changed) {
emit_tty_step();
tty_step.clear_step();
}
const uint32_t lch = next_lch;
next_lch = (next_lch + 1U) % cfg.channel_count;
@ -1441,6 +1685,9 @@ int run(const Config& cfg) {
if (current_packet.channels[lch].size() < target_frames) {
current_packet.channels[lch].push_back(stored_value);
if (tty_writer) {
tty_step.add_sample(lch, adc_raw_value);
}
++current_packet.stored_samples;
++total_stored_adc_samples;
++stats_stored_adc_samples;
@ -1471,7 +1718,7 @@ int run(const Config& cfg) {
stop_loop_requested = true;
}
if ((cfg.stats_period_ms != 0U) && ((GetTickCount64() - last_stats_print) >= cfg.stats_period_ms)) {
if ((cfg.stats_period_ms != 0U) && ((tick_count_ms() - last_stats_print) >= cfg.stats_period_ms)) {
print_stats(false);
}
}
@ -1505,20 +1752,20 @@ int run(const Config& cfg) {
std::cout << "Average stats: "
<< "MB/s=" << std::fixed << std::setprecision(3)
<< ((static_cast<double>(total_raw_words) * sizeof(uint32_t)) /
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0) /
std::max(1e-9, static_cast<double>(tick_count_ms() - capture_loop_start) / 1000.0) /
1000.0 / 1000.0)
<< ", ADC samples/s="
<< (static_cast<double>(total_adc_samples) /
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0))
std::max(1e-9, static_cast<double>(tick_count_ms() - capture_loop_start) / 1000.0))
<< ", DIN samples/s="
<< (static_cast<double>(total_din_samples) /
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0))
std::max(1e-9, static_cast<double>(tick_count_ms() - capture_loop_start) / 1000.0))
<< ", frames/s per channel="
<< (static_cast<double>(total_completed_frames) /
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0))
std::max(1e-9, static_cast<double>(tick_count_ms() - capture_loop_start) / 1000.0))
<< ", packets/s="
<< (static_cast<double>(total_completed_packets) /
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0))
std::max(1e-9, static_cast<double>(tick_count_ms() - capture_loop_start) / 1000.0))
<< ", packets captured=" << total_completed_packets;
if (cfg.di1_mode == Di1Mode::ZeroOnChange) {
std::cout << ", zeroed on DI1 change="

128
tty_protocol_writer.cpp Normal file
View File

@ -0,0 +1,128 @@
#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 <cerrno>
#include <cstring>
#include <fcntl.h>
#include <sstream>
#include <sys/types.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();
}
} // namespace
TtyProtocolWriter::TtyProtocolWriter(std::string path) : path_(std::move(path)) {
fd_ = ::open(path_.c_str(), O_WRONLY | O_NOCTTY);
if (fd_ < 0) {
throw std::runtime_error(io_error("Cannot open tty output", path_));
}
}
TtyProtocolWriter::~TtyProtocolWriter() {
close_fd();
}
TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept
: path_(std::move(other.path_)),
fd_(other.fd_) {
other.fd_ = -1;
}
TtyProtocolWriter& TtyProtocolWriter::operator=(TtyProtocolWriter&& other) noexcept {
if (this != &other) {
close_fd();
path_ = std::move(other.path_);
fd_ = other.fd_;
other.fd_ = -1;
}
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;
}
}
#endif

29
tty_protocol_writer.h Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include <cstdint>
#include <string>
class TtyProtocolWriter {
public:
explicit TtyProtocolWriter(std::string path);
~TtyProtocolWriter();
TtyProtocolWriter(const TtyProtocolWriter&) = delete;
TtyProtocolWriter& operator=(const TtyProtocolWriter&) = delete;
TtyProtocolWriter(TtyProtocolWriter&& other) noexcept;
TtyProtocolWriter& operator=(TtyProtocolWriter&& other) noexcept;
void emit_packet_start() const;
void emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) const;
const std::string& path() const;
private:
void write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const;
void close_fd() noexcept;
std::string path_;
#ifndef _WIN32
int fd_ = -1;
#endif
};