8 Commits

17 changed files with 1712 additions and 2565 deletions

22
.gitignore vendored
View File

@ -1,16 +1,14 @@
# Build outputs
capture
# Build artifacts
*.exe
*.obj
*.pdb
*.ilk
*.idb
# Large capture spools and generated data dumps
# Generated capture outputs
*.capture_spool.bin
capture.csv
capture.svg
packets.csv
packets.svg
test.csv
test.svg
internal_default.csv
internal_default.svg
*_test.*
*.csv
*.svg
*.json
*.html
live_*.js

20
Makefile Normal file
View File

@ -0,0 +1,20 @@
TARGET = main.exe
BUILDDIR = build
SOURCES = main.cpp capture_file_writer.cpp tty_protocol_writer.cpp
OBJS = $(BUILDDIR)\main.obj $(BUILDDIR)\capture_file_writer.obj $(BUILDDIR)\tty_protocol_writer.obj
CXXFLAGS = /nologo /std:c++17 /EHsc /O2 /I.
all: $(TARGET)
$(TARGET): $(SOURCES)
if not exist $(BUILDDIR) mkdir $(BUILDDIR)
cl $(CXXFLAGS) /Fo$(BUILDDIR)\ /Fd$(BUILDDIR)\ /Fe$(TARGET) $(SOURCES)
rebuild: clean all
clean:
-del /Q $(TARGET) 2>nul
-if exist $(BUILDDIR) rmdir /S /Q $(BUILDDIR)
run-help: $(TARGET)
$(TARGET) help

806
adc_pin_probe.cpp Normal file
View File

@ -0,0 +1,806 @@
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#endif
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable: 4995)
#endif
#include "x502api.h"
#include "e502api.h"
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#else
#include <chrono>
#include <dlfcn.h>
#endif
namespace {
struct Config {
std::string serial;
std::optional<uint32_t> ip_addr;
double sample_clock_hz = 50000.0;
double capture_ms = 120.0;
uint32_t range = X502_ADC_RANGE_5;
uint32_t top = 8;
std::vector<uint32_t> exclude_phy;
uint32_t recv_block_words = 8192;
uint32_t recv_timeout_ms = 50;
};
[[noreturn]] void fail(const std::string& message) {
throw std::runtime_error(message);
}
std::string trim_copy(const std::string& text) {
const auto first = text.find_first_not_of(" \t\r\n");
if (first == std::string::npos) {
return {};
}
const auto last = text.find_last_not_of(" \t\r\n");
return text.substr(first, last - first + 1);
}
bool starts_with(const std::string& value, const std::string& prefix) {
return value.rfind(prefix, 0) == 0;
}
uint32_t parse_u32(const std::string& text, const std::string& field_name) {
const std::string clean = trim_copy(text);
if (clean.empty()) {
fail("Missing integer value for " + field_name);
}
char* end = nullptr;
const auto value = std::strtoull(clean.c_str(), &end, 0);
if ((end == clean.c_str()) || (*end != '\0') || (value > std::numeric_limits<uint32_t>::max())) {
fail("Invalid integer for " + field_name + ": " + text);
}
return static_cast<uint32_t>(value);
}
double parse_double(const std::string& text, const std::string& field_name) {
const std::string clean = trim_copy(text);
if (clean.empty()) {
fail("Missing floating point value for " + field_name);
}
char* end = nullptr;
const double value = std::strtod(clean.c_str(), &end);
if ((end == clean.c_str()) || (*end != '\0') || !std::isfinite(value)) {
fail("Invalid floating point value for " + field_name + ": " + text);
}
return value;
}
uint32_t parse_ipv4(const std::string& text) {
std::array<uint32_t, 4> parts{};
std::stringstream ss(text);
std::string token;
for (std::size_t i = 0; i < parts.size(); ++i) {
if (!std::getline(ss, token, '.')) {
fail("Invalid IPv4 address: " + text);
}
parts[i] = parse_u32(token, "ip");
if (parts[i] > 255U) {
fail("IPv4 byte out of range: " + token);
}
}
if (std::getline(ss, token, '.')) {
fail("Invalid IPv4 address: " + text);
}
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
}
std::string ipv4_to_string(uint32_t ip_addr) {
std::ostringstream out;
out << ((ip_addr >> 24) & 0xFF) << '.'
<< ((ip_addr >> 16) & 0xFF) << '.'
<< ((ip_addr >> 8) & 0xFF) << '.'
<< (ip_addr & 0xFF);
return out.str();
}
uint32_t parse_range(const std::string& text) {
const double value = parse_double(text, "range");
if (std::fabs(value - 10.0) < 1e-9) {
return X502_ADC_RANGE_10;
}
if (std::fabs(value - 5.0) < 1e-9) {
return X502_ADC_RANGE_5;
}
if (std::fabs(value - 2.0) < 1e-9) {
return X502_ADC_RANGE_2;
}
if (std::fabs(value - 1.0) < 1e-9) {
return X502_ADC_RANGE_1;
}
if (std::fabs(value - 0.5) < 1e-9) {
return X502_ADC_RANGE_05;
}
if (std::fabs(value - 0.2) < 1e-9) {
return X502_ADC_RANGE_02;
}
fail("Unsupported E-502 range: " + text);
}
double range_to_volts(uint32_t range) {
switch (range) {
case X502_ADC_RANGE_10:
return 10.0;
case X502_ADC_RANGE_5:
return 5.0;
case X502_ADC_RANGE_2:
return 2.0;
case X502_ADC_RANGE_1:
return 1.0;
case X502_ADC_RANGE_05:
return 0.5;
case X502_ADC_RANGE_02:
return 0.2;
default:
fail("Unknown ADC range enum");
}
}
std::vector<uint32_t> parse_exclude_phy(const std::string& text) {
const std::string clean = trim_copy(text);
if (clean.empty() || (clean == "none")) {
return {};
}
std::vector<uint32_t> result;
std::stringstream ss(clean);
std::string token;
while (std::getline(ss, token, ',')) {
const std::string item = trim_copy(token);
if (item.empty()) {
continue;
}
const uint32_t phy = parse_u32(item, "exclude_phy");
if (phy >= X502_ADC_COMM_CH_CNT) {
fail("exclude_phy has out-of-range channel " + std::to_string(phy) + " (allowed 0..31)");
}
if (std::find(result.begin(), result.end(), phy) == result.end()) {
result.push_back(phy);
}
}
return result;
}
std::string phy_channel_name(uint32_t phy_ch) {
if (phy_ch < 16U) {
return "X" + std::to_string(phy_ch + 1U);
}
return "Y" + std::to_string((phy_ch - 16U) + 1U);
}
void print_help(const char* exe_name) {
std::cout
<< "Usage:\n"
<< " " << exe_name << " [serial:SN] [ip:192.168.0.10]\n"
<< " [sample_clock_hz:50000] [capture_ms:120] [range:5]\n"
<< " [exclude_phy:2,3,18,19|none] [top:8]\n"
<< " [recv_block:8192] [recv_timeout_ms:50]\n"
<< "\n"
<< "Purpose:\n"
<< " Scan single-ended analog inputs (mode:comm, phy 0..31) and rank channels\n"
<< " by activity to locate where the ADC signal actually arrives.\n"
<< "\n"
<< "Defaults:\n"
<< " sample_clock_hz:50000\n"
<< " capture_ms:120\n"
<< " range:5 -> +/-5 V\n"
<< " top:8\n"
<< " exclude_phy:none -> scan all channels 0..31 (including 2,3,18,19)\n"
<< "\n"
<< "Single-ended physical channel mapping:\n"
<< " 0..15 -> X1..X16\n"
<< " 16..31 -> Y1..Y16\n"
<< "\n"
<< "Scoring:\n"
<< " ranking priority: stddev first, then peak-to-peak (p2p)\n"
<< "\n"
<< "Example:\n"
<< " " << exe_name
<< " sample_clock_hz:50000 capture_ms:120 range:5 exclude_phy:2,3,18,19 top:8\n";
}
Config parse_args(int argc, char** argv) {
Config cfg;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if ((arg == "help") || (arg == "--help") || (arg == "-h")) {
print_help(argv[0]);
std::exit(0);
}
if (starts_with(arg, "serial:")) {
cfg.serial = arg.substr(7);
continue;
}
if (starts_with(arg, "ip:")) {
cfg.ip_addr = parse_ipv4(arg.substr(3));
continue;
}
if (starts_with(arg, "sample_clock_hz:")) {
cfg.sample_clock_hz = parse_double(arg.substr(16), "sample_clock_hz");
continue;
}
if (starts_with(arg, "capture_ms:")) {
cfg.capture_ms = parse_double(arg.substr(11), "capture_ms");
continue;
}
if (starts_with(arg, "range:")) {
cfg.range = parse_range(arg.substr(6));
continue;
}
if (starts_with(arg, "exclude_phy:")) {
cfg.exclude_phy = parse_exclude_phy(arg.substr(12));
continue;
}
if (starts_with(arg, "top:")) {
cfg.top = parse_u32(arg.substr(4), "top");
continue;
}
if (starts_with(arg, "recv_block:")) {
cfg.recv_block_words = parse_u32(arg.substr(11), "recv_block");
continue;
}
if (starts_with(arg, "recv_timeout_ms:")) {
cfg.recv_timeout_ms = parse_u32(arg.substr(16), "recv_timeout_ms");
continue;
}
fail("Unknown argument: " + arg);
}
if (cfg.sample_clock_hz <= 0.0) {
fail("sample_clock_hz must be > 0");
}
if (cfg.capture_ms <= 0.0) {
fail("capture_ms must be > 0");
}
if (cfg.top == 0U) {
fail("top must be > 0");
}
if (cfg.recv_block_words == 0U) {
fail("recv_block must be > 0");
}
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(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
}
template <typename Fn>
Fn load_symbol_optional(ModuleHandle module, const char* name) {
#ifdef _WIN32
const auto addr = GetProcAddress(module, name);
return (addr != nullptr) ? reinterpret_cast<Fn>(addr) : nullptr;
#else
dlerror();
void* addr = dlsym(module, name);
const char* error = dlerror();
if ((addr == nullptr) || (error != nullptr)) {
return nullptr;
}
return reinterpret_cast<Fn>(addr);
#endif
}
struct Api {
using GetDevInfoFn = int32_t (*)(t_x502_hnd, t_x502_info*);
ModuleHandle x502_module = nullptr;
ModuleHandle e502_module = nullptr;
decltype(&X502_Create) Create = nullptr;
decltype(&X502_Free) Free = nullptr;
decltype(&X502_Close) Close = nullptr;
decltype(&X502_GetErrorString) GetErrorString = nullptr;
GetDevInfoFn GetDevInfo = nullptr;
decltype(&X502_GetDevInfo2) GetDevInfo2 = nullptr;
decltype(&X502_SetMode) SetMode = nullptr;
decltype(&X502_StreamsStop) StreamsStop = nullptr;
decltype(&X502_StreamsDisable) StreamsDisable = nullptr;
decltype(&X502_SetSyncMode) SetSyncMode = nullptr;
decltype(&X502_SetSyncStartMode) SetSyncStartMode = nullptr;
decltype(&X502_SetRefFreq) SetRefFreq = nullptr;
decltype(&X502_SetLChannelCount) SetLChannelCount = nullptr;
decltype(&X502_SetLChannel) SetLChannel = nullptr;
decltype(&X502_SetAdcFreq) SetAdcFreq = nullptr;
decltype(&X502_Configure) Configure = nullptr;
decltype(&X502_StreamsEnable) StreamsEnable = nullptr;
decltype(&X502_StreamsStart) StreamsStart = nullptr;
decltype(&X502_Recv) Recv = nullptr;
decltype(&X502_ProcessData) ProcessData = nullptr;
decltype(&E502_OpenUsb) OpenUsb = nullptr;
decltype(&E502_OpenByIpAddr) OpenByIpAddr = nullptr;
Api() {
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");
Close = load_symbol<decltype(Close)>(x502_module, "X502_Close");
GetErrorString = load_symbol<decltype(GetErrorString)>(x502_module, "X502_GetErrorString");
GetDevInfo = load_symbol_optional<GetDevInfoFn>(x502_module, "X502_GetDevInfo");
GetDevInfo2 = load_symbol_optional<decltype(GetDevInfo2)>(x502_module, "X502_GetDevInfo2");
if ((GetDevInfo == nullptr) && (GetDevInfo2 == nullptr)) {
fail("Neither X502_GetDevInfo2 nor X502_GetDevInfo is available in x502 API library");
}
SetMode = load_symbol<decltype(SetMode)>(x502_module, "X502_SetMode");
StreamsStop = load_symbol<decltype(StreamsStop)>(x502_module, "X502_StreamsStop");
StreamsDisable = load_symbol<decltype(StreamsDisable)>(x502_module, "X502_StreamsDisable");
SetSyncMode = load_symbol<decltype(SetSyncMode)>(x502_module, "X502_SetSyncMode");
SetSyncStartMode = load_symbol<decltype(SetSyncStartMode)>(x502_module, "X502_SetSyncStartMode");
SetRefFreq = load_symbol<decltype(SetRefFreq)>(x502_module, "X502_SetRefFreq");
SetLChannelCount = load_symbol<decltype(SetLChannelCount)>(x502_module, "X502_SetLChannelCount");
SetLChannel = load_symbol<decltype(SetLChannel)>(x502_module, "X502_SetLChannel");
SetAdcFreq = load_symbol<decltype(SetAdcFreq)>(x502_module, "X502_SetAdcFreq");
Configure = load_symbol<decltype(Configure)>(x502_module, "X502_Configure");
StreamsEnable = load_symbol<decltype(StreamsEnable)>(x502_module, "X502_StreamsEnable");
StreamsStart = load_symbol<decltype(StreamsStart)>(x502_module, "X502_StreamsStart");
Recv = load_symbol<decltype(Recv)>(x502_module, "X502_Recv");
ProcessData = load_symbol<decltype(ProcessData)>(x502_module, "X502_ProcessData");
OpenUsb = load_symbol<decltype(OpenUsb)>(e502_module, "E502_OpenUsb");
OpenByIpAddr = load_symbol<decltype(OpenByIpAddr)>(e502_module, "E502_OpenByIpAddr");
}
~Api() {
close_library(e502_module);
close_library(x502_module);
}
};
std::string x502_error(const Api& api, int32_t err) {
const char* text = api.GetErrorString ? api.GetErrorString(err) : nullptr;
std::ostringstream out;
out << "err=" << err;
if ((text != nullptr) && (*text != '\0')) {
out << " (" << text << ")";
}
return out.str();
}
void expect_ok(const Api& api, int32_t err, const std::string& what) {
if (err != X502_ERR_OK) {
fail(what + ": " + x502_error(api, err));
}
}
struct DeviceHandle {
const Api& api;
t_x502_hnd hnd = nullptr;
bool opened = false;
bool streams_started = false;
explicit DeviceHandle(const Api& api_ref) : api(api_ref), hnd(api.Create()) {
if (hnd == nullptr) {
fail("X502_Create failed");
}
}
~DeviceHandle() {
if (hnd != nullptr) {
if (streams_started) {
api.StreamsStop(hnd);
}
if (opened) {
api.Close(hnd);
}
api.Free(hnd);
}
}
};
uint64_t tick_count_ms() {
#ifdef _WIN32
return static_cast<uint64_t>(GetTickCount64());
#else
using namespace std::chrono;
return static_cast<uint64_t>(
duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count());
#endif
}
struct RunningStats {
std::size_t count = 0;
double mean = 0.0;
double m2 = 0.0;
double sum_sq = 0.0;
double min = std::numeric_limits<double>::infinity();
double max = -std::numeric_limits<double>::infinity();
void add(double value) {
++count;
const double delta = value - mean;
mean += delta / static_cast<double>(count);
const double delta2 = value - mean;
m2 += delta * delta2;
sum_sq += value * value;
if (value < min) {
min = value;
}
if (value > max) {
max = value;
}
}
};
struct ChannelResult {
uint32_t phy_ch = 0;
std::string pin_name;
std::size_t sample_count = 0;
double score = -1.0;
double stddev = 0.0;
double peak_to_peak = 0.0;
double rms = 0.0;
double mean = 0.0;
double min = 0.0;
double max = 0.0;
};
void configure_base_capture(const Api& api, DeviceHandle& device, const Config& cfg) {
expect_ok(api, api.SetMode(device.hnd, X502_MODE_FPGA), "Set FPGA mode");
api.StreamsStop(device.hnd);
device.streams_started = false;
api.StreamsDisable(device.hnd, X502_STREAM_ALL_IN | X502_STREAM_ALL_OUT);
expect_ok(api, api.SetSyncMode(device.hnd, X502_SYNC_INTERNAL), "Set sync mode");
expect_ok(api, api.SetSyncStartMode(device.hnd, X502_SYNC_INTERNAL), "Set sync start mode");
expect_ok(api, api.SetRefFreq(device.hnd, X502_REF_FREQ_2000KHZ), "Set internal reference frequency");
double adc_freq = cfg.sample_clock_hz;
double frame_freq = 0.0;
expect_ok(api, api.SetAdcFreq(device.hnd, &adc_freq, &frame_freq), "Set ADC frequency");
}
ChannelResult scan_phy_channel(const Api& api, DeviceHandle& device, const Config& cfg, uint32_t phy_ch) {
ChannelResult result;
result.phy_ch = phy_ch;
result.pin_name = phy_channel_name(phy_ch);
configure_base_capture(api, device, cfg);
expect_ok(api, api.SetLChannelCount(device.hnd, 1), "Set logical channel count");
expect_ok(api,
api.SetLChannel(device.hnd, 0, phy_ch, X502_LCH_MODE_COMM, cfg.range, 1),
"Set logical channel");
expect_ok(api, api.Configure(device.hnd, 0), "Configure device");
expect_ok(api, api.StreamsEnable(device.hnd, X502_STREAM_ADC), "Enable ADC stream");
expect_ok(api, api.StreamsStart(device.hnd), "Start streams");
device.streams_started = true;
std::vector<uint32_t> raw(cfg.recv_block_words);
std::vector<double> adc(cfg.recv_block_words);
RunningStats stats;
const uint64_t capture_until = tick_count_ms() + static_cast<uint64_t>(std::llround(cfg.capture_ms));
while (tick_count_ms() < capture_until) {
const int32_t recvd = api.Recv(device.hnd, raw.data(), cfg.recv_block_words, cfg.recv_timeout_ms);
if (recvd < 0) {
fail("X502_Recv failed while scanning " + result.pin_name + ": " + x502_error(api, recvd));
}
if (recvd == 0) {
continue;
}
uint32_t adc_count = static_cast<uint32_t>(adc.size());
expect_ok(api,
api.ProcessData(device.hnd,
raw.data(),
static_cast<uint32_t>(recvd),
X502_PROC_FLAGS_VOLT,
adc.data(),
&adc_count,
nullptr,
nullptr),
"ProcessData");
for (uint32_t i = 0; i < adc_count; ++i) {
stats.add(adc[i]);
}
}
api.StreamsStop(device.hnd);
device.streams_started = false;
api.StreamsDisable(device.hnd, X502_STREAM_ALL_IN | X502_STREAM_ALL_OUT);
result.sample_count = stats.count;
if (stats.count == 0U) {
return result;
}
result.mean = stats.mean;
result.min = stats.min;
result.max = stats.max;
result.peak_to_peak = stats.max - stats.min;
result.rms = std::sqrt(stats.sum_sq / static_cast<double>(stats.count));
const double variance =
(stats.count > 1U) ? (stats.m2 / static_cast<double>(stats.count - 1U)) : 0.0;
result.stddev = std::sqrt(std::max(0.0, variance));
// Stddev is the dominant rank metric, p2p is a secondary tie-breaker.
result.score = result.stddev * 1000000.0 + result.peak_to_peak;
return result;
}
void print_device_info(const t_x502_info& info) {
std::cout << "Device: " << info.name << "\n"
<< "Serial: " << info.serial << "\n"
<< "FPGA version: " << static_cast<unsigned>(info.fpga_ver >> 8) << "."
<< static_cast<unsigned>(info.fpga_ver & 0xFF) << "\n";
}
void print_ranked_table(const std::vector<ChannelResult>& sorted) {
std::cout << "\nRanked channels (sorted by stddev, then p2p):\n";
std::cout << std::left
<< std::setw(5) << "Rank"
<< std::setw(6) << "phy"
<< std::setw(6) << "pin"
<< std::setw(12) << "samples"
<< std::setw(14) << "score"
<< std::setw(14) << "stddev[V]"
<< std::setw(14) << "p2p[V]"
<< std::setw(14) << "rms[V]"
<< std::setw(14) << "mean[V]"
<< std::setw(14) << "min[V]"
<< std::setw(14) << "max[V]" << "\n";
std::cout << std::fixed << std::setprecision(6);
std::size_t rank = 1;
for (const auto& item : sorted) {
std::cout << std::left
<< std::setw(5) << rank
<< std::setw(6) << item.phy_ch
<< std::setw(6) << item.pin_name
<< std::setw(12) << item.sample_count
<< std::setw(14) << item.score
<< std::setw(14) << item.stddev
<< std::setw(14) << item.peak_to_peak
<< std::setw(14) << item.rms
<< std::setw(14) << item.mean
<< std::setw(14) << item.min
<< std::setw(14) << item.max
<< "\n";
++rank;
}
}
int run(const Config& cfg) {
Api api;
DeviceHandle device(api);
int32_t err = X502_ERR_OK;
if (cfg.ip_addr.has_value()) {
err = api.OpenByIpAddr(device.hnd, *cfg.ip_addr, 0, 5000);
} else {
err = api.OpenUsb(device.hnd, cfg.serial.empty() ? nullptr : cfg.serial.c_str());
}
expect_ok(api, err, "Open device");
device.opened = true;
t_x502_info 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");
print_device_info(info);
std::array<bool, X502_ADC_COMM_CH_CNT> excluded{};
for (uint32_t phy : cfg.exclude_phy) {
if (phy >= X502_ADC_COMM_CH_CNT) {
fail("exclude_phy has out-of-range channel: " + std::to_string(phy));
}
excluded[phy] = true;
}
std::vector<uint32_t> scan_channels;
scan_channels.reserve(X502_ADC_COMM_CH_CNT);
for (uint32_t phy = 0; phy < X502_ADC_COMM_CH_CNT; ++phy) {
if (!excluded[phy]) {
scan_channels.push_back(phy);
}
}
if (scan_channels.empty()) {
fail("No channels left to scan after exclude_phy filtering");
}
std::cout << "Probe settings:\n"
<< " source: "
<< (cfg.ip_addr ? ("ip:" + ipv4_to_string(*cfg.ip_addr))
: (cfg.serial.empty() ? std::string("usb:auto") : ("serial:" + cfg.serial)))
<< "\n"
<< " mode: comm (single-ended), channel_count=1\n"
<< " sample_clock_hz: " << cfg.sample_clock_hz << "\n"
<< " capture_ms per channel: " << cfg.capture_ms << "\n"
<< " ADC range: +/-" << range_to_volts(cfg.range) << " V\n"
<< " top: " << cfg.top << "\n"
<< " excluded channels: ";
if (cfg.exclude_phy.empty()) {
std::cout << "none";
} else {
for (std::size_t i = 0; i < cfg.exclude_phy.size(); ++i) {
if (i != 0U) {
std::cout << ",";
}
std::cout << cfg.exclude_phy[i] << "(" << phy_channel_name(cfg.exclude_phy[i]) << ")";
}
}
std::cout << "\n";
std::vector<ChannelResult> results;
results.reserve(scan_channels.size());
for (std::size_t i = 0; i < scan_channels.size(); ++i) {
const uint32_t phy = scan_channels[i];
std::cout << "Scanning [" << (i + 1U) << "/" << scan_channels.size() << "] "
<< "phy=" << phy << " (" << phy_channel_name(phy) << ")...\n";
results.push_back(scan_phy_channel(api, device, cfg, phy));
}
api.StreamsStop(device.hnd);
device.streams_started = false;
api.StreamsDisable(device.hnd, X502_STREAM_ALL_IN | X502_STREAM_ALL_OUT);
std::sort(results.begin(), results.end(), [](const ChannelResult& a, const ChannelResult& b) {
const bool a_valid = a.sample_count != 0U;
const bool b_valid = b.sample_count != 0U;
if (a_valid != b_valid) {
return a_valid;
}
if (a.stddev != b.stddev) {
return a.stddev > b.stddev;
}
if (a.peak_to_peak != b.peak_to_peak) {
return a.peak_to_peak > b.peak_to_peak;
}
return a.phy_ch < b.phy_ch;
});
print_ranked_table(results);
const std::size_t top_count = std::min<std::size_t>(cfg.top, results.size());
std::cout << "\nTop " << top_count << " candidate(s):\n";
for (std::size_t i = 0; i < top_count; ++i) {
const auto& item = results[i];
std::cout << " #" << (i + 1U)
<< " phy=" << item.phy_ch
<< " pin=" << item.pin_name
<< " stddev=" << std::fixed << std::setprecision(6) << item.stddev << " V"
<< " p2p=" << item.peak_to_peak << " V"
<< " score=" << item.score
<< " samples=" << item.sample_count
<< "\n";
}
if (!results.empty() && (results.front().sample_count == 0U)) {
std::cout << "\nWarning: no ADC samples were decoded. Check device connection and sample clock setup.\n";
}
return 0;
}
} // namespace
int main(int argc, char** argv) {
try {
const Config cfg = parse_args(argc, argv);
return run(cfg);
} catch (const std::exception& ex) {
std::cerr << "Error: " << ex.what() << "\n";
return 1;
}
}

View File

@ -1,28 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
CXX="${CXX:-g++}"
OUT="${OUT:-lchm_clock_counter.exe}"
CXXFLAGS=(
-std=c++17
-O2
-Wall
-Wextra
-pedantic
)
SOURCES=(
lchm_clock_counter.cpp
)
LDFLAGS=(
-ldl
)
"$CXX" "${CXXFLAGS[@]}" "${SOURCES[@]}" "${LDFLAGS[@]}" -o "$OUT"
echo "Built $OUT"

View File

@ -1,32 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
CXX="${CXX:-g++}"
OUT="${OUT:-main.exe}"
CXXFLAGS=(
-std=c++17
-O2
-Wall
-Wextra
-pedantic
)
SOURCES=(
main.cpp
capture_file_writer.cpp
tty_protocol_writer.cpp
)
LDFLAGS=(
-ldl
-lutil
-lpthread
)
"$CXX" "${CXXFLAGS[@]}" "${SOURCES[@]}" "${LDFLAGS[@]}" -o "$OUT"
echo "Built $OUT"

View File

@ -15,6 +15,9 @@
namespace {
constexpr std::size_t kLivePlotMaxColumns = 800;
constexpr std::size_t kLivePlotMidColumns = 4096;
constexpr std::size_t kLivePlotHighColumns = 16384;
constexpr std::size_t kLivePlotRawFrameLimit = 50000;
constexpr uint32_t kCsvSpoolMagic = 0x4C564353U; // "SVCL"
constexpr uint32_t kCsvSpoolVersion = 2U;
@ -37,6 +40,14 @@ struct PlotPoint {
double value = 0.0;
};
struct Di1GroupedTraces {
std::vector<PlotPoint> ch1;
std::vector<PlotPoint> ch2;
std::vector<PlotPoint> rss;
};
std::size_t packet_frame_count(const CapturePacket& packet);
std::size_t packet_channel_count(const CapturePacket& packet) {
return (packet.channel_count <= 1U) ? 1U : 2U;
}
@ -49,6 +60,22 @@ bool packet_has_di1_trace(const CapturePacket& packet) {
return packet.has_di1_trace && !packet.di1.empty();
}
std::vector<double> build_rss_values(const CapturePacket& packet) {
const std::size_t frames = packet_frame_count(packet);
std::vector<double> rss;
if (!packet_has_ch2(packet) || (frames == 0U)) {
return rss;
}
rss.reserve(frames);
for (std::size_t i = 0; i < frames; ++i) {
const double ch1 = packet.ch1[i];
const double ch2 = packet.ch2[i];
rss.push_back(std::sqrt((ch1 * ch1) + (ch2 * ch2)));
}
return rss;
}
std::size_t packet_frame_count(const CapturePacket& packet) {
std::size_t frames = packet_has_ch2(packet) ? std::min(packet.ch1.size(), packet.ch2.size()) : packet.ch1.size();
if (packet_has_di1_trace(packet)) {
@ -97,6 +124,74 @@ std::vector<PlotPoint> build_min_max_trace(const std::vector<double>& data, std:
return result;
}
std::vector<PlotPoint> build_full_trace(const std::vector<double>& data) {
std::vector<PlotPoint> result;
result.reserve(data.size());
for (std::size_t i = 0; i < data.size(); ++i) {
result.push_back({i, data[i]});
}
return result;
}
void append_offset_trace(std::vector<PlotPoint>& dst,
const std::vector<PlotPoint>& src,
std::size_t sample_offset) {
dst.reserve(dst.size() + src.size());
for (const auto& point : src) {
dst.push_back({sample_offset + point.sample_index, point.value});
}
}
Di1GroupedTraces build_di1_grouped_traces(const std::vector<double>& ch1,
const std::vector<double>& ch2,
const std::vector<uint8_t>& di1,
bool has_ch2) {
Di1GroupedTraces traces;
const std::size_t frames = std::min(ch1.size(), di1.size());
if (frames == 0U) {
return traces;
}
traces.ch1.reserve(frames / 2U + 1U);
if (has_ch2) {
traces.ch2.reserve(frames / 2U + 1U);
traces.rss.reserve(frames / 2U + 1U);
}
std::size_t begin = 0U;
while (begin < frames) {
const uint8_t level = di1[begin];
std::size_t end = begin + 1U;
while ((end < frames) && (di1[end] == level)) {
++end;
}
double sum1 = 0.0;
double sum2 = 0.0;
for (std::size_t i = begin; i < end; ++i) {
sum1 += ch1[i];
if (has_ch2 && (i < ch2.size())) {
sum2 += ch2[i];
}
}
const double count = static_cast<double>(end - begin);
const std::size_t mid = begin + ((end - begin - 1U) / 2U);
const double avg1 = sum1 / count;
traces.ch1.push_back({mid, avg1});
if (has_ch2) {
const double avg2 = sum2 / count;
traces.ch2.push_back({mid, avg2});
traces.rss.push_back({mid, std::sqrt((avg1 * avg1) + (avg2 * avg2))});
}
begin = end;
}
return traces;
}
std::vector<PlotPoint> build_digital_step_trace(const std::vector<uint8_t>& data) {
std::vector<PlotPoint> result;
if (data.empty()) {
@ -184,6 +279,24 @@ std::string json_points(const std::vector<PlotPoint>& trace, double frame_freq_h
return out.str();
}
std::string json_packet_markers(const std::vector<std::pair<std::size_t, std::size_t>>& markers,
double frame_freq_hz) {
std::ostringstream out;
out << std::fixed << std::setprecision(9);
out << "[";
bool first = true;
for (const auto& marker : markers) {
if (!first) {
out << ",";
}
first = false;
out << "{\"t\":" << (static_cast<double>(marker.first) / frame_freq_hz)
<< ",\"label\":\"P" << marker.second << "\"}";
}
out << "]";
return out.str();
}
[[noreturn]] void fail_write(const std::string& message);
std::string replace_extension(const std::string& path, const std::string& new_extension) {
@ -296,20 +409,23 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " <span id=\"updateInfo\">Auto-refresh every 500 ms</span>\n"
<< " </div>\n"
<< " </div>\n"
<< " <div class=\"status\" id=\"statusText\">Open this page once and leave it open. It refreshes its data script to pick up new packets.</div>\n"
<< " <div class=\"status\" id=\"statusText\">Open this page once and leave it open. It refreshes its data script to pick up a rolling continuous packet window.</div>\n"
<< " <div class=\"controls\">\n"
<< " <button type=\"button\" id=\"zoomXIn\">X+</button>\n"
<< " <button type=\"button\" id=\"zoomXOut\">X-</button>\n"
<< " <button type=\"button\" id=\"panLeft\">Left</button>\n"
<< " <button type=\"button\" id=\"panRight\">Right</button>\n"
<< " <button type=\"button\" id=\"zoomYIn\">Y+</button>\n"
<< " <button type=\"button\" id=\"zoomYOut\">Y-</button>\n"
<< " <button type=\"button\" id=\"resetZoom\">Reset</button>\n"
<< " <label><input type=\"checkbox\" id=\"autoZoom\" checked/>Auto view</label>\n"
<< " <span class=\"hint\">Wheel: X zoom, Shift+wheel: Y zoom, double-click: reset</span>\n"
<< " <span class=\"hint\">Wheel: X zoom, Shift+wheel: Y zoom, Left/Right buttons or arrow keys: pan X, double-click: reset. The live view shows a rolling continuous packet window; CSV stores all samples.</span>\n"
<< " </div>\n"
<< " <canvas id=\"plot\" width=\"1200\" height=\"620\"></canvas>\n"
<< " <div class=\"legend\">\n"
<< " <span id=\"legendCh1\"><span class=\"sw\" style=\"background:#005bbb\"></span>CH1</span>\n"
<< " <span id=\"legendCh2\"><span class=\"sw\" style=\"background:#d62828\"></span>CH2</span>\n"
<< " <span id=\"legendRss\"><span class=\"sw\" style=\"background:#ee8a12\"></span>sqrt(CH1^2 + CH2^2)</span>\n"
<< " <span id=\"legendDi1\"><span class=\"sw\" style=\"background:#1b8f3a\"></span>DI1</span>\n"
<< " </div>\n"
<< " </div>\n"
@ -326,9 +442,12 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " const packetRateInfo = document.getElementById('packetRateInfo');\n"
<< " const updateInfo = document.getElementById('updateInfo');\n"
<< " const legendCh2 = document.getElementById('legendCh2');\n"
<< " const legendRss = document.getElementById('legendRss');\n"
<< " const legendDi1 = document.getElementById('legendDi1');\n"
<< " const zoomXIn = document.getElementById('zoomXIn');\n"
<< " const zoomXOut = document.getElementById('zoomXOut');\n"
<< " const panLeft = document.getElementById('panLeft');\n"
<< " const panRight = document.getElementById('panRight');\n"
<< " const zoomYIn = document.getElementById('zoomYIn');\n"
<< " const zoomYOut = document.getElementById('zoomYOut');\n"
<< " const resetZoom = document.getElementById('resetZoom');\n"
@ -419,6 +538,30 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " refreshAutoCheckbox();\n"
<< " renderPacket(latestPacket);\n"
<< " }\n"
<< " function applyPanX(direction) {\n"
<< " if (!latestAutoBounds || !latestPacket) return;\n"
<< " const current = currentViewBounds(latestAutoBounds);\n"
<< " const totalX = Math.max(1e-9, latestAutoBounds.maxT);\n"
<< " const span = Math.max(1e-9, current.xMax - current.xMin);\n"
<< " if (span >= totalX) return;\n"
<< " const shift = span * 0.25 * direction;\n"
<< " let newMin = current.xMin + shift;\n"
<< " let newMax = current.xMax + shift;\n"
<< " if (newMin < 0) {\n"
<< " newMax -= newMin;\n"
<< " newMin = 0;\n"
<< " }\n"
<< " if (newMax > totalX) {\n"
<< " newMin -= (newMax - totalX);\n"
<< " newMax = totalX;\n"
<< " }\n"
<< " newMin = Math.max(0, newMin);\n"
<< " newMax = Math.min(totalX, newMax);\n"
<< " viewState = { auto: false, xMin: newMin, xMax: newMax, yMin: current.yMin, yMax: current.yMax };\n"
<< " saveViewState();\n"
<< " refreshAutoCheckbox();\n"
<< " renderPacket(latestPacket);\n"
<< " }\n"
<< " function resetView() {\n"
<< " viewState = defaultViewState();\n"
<< " saveViewState();\n"
@ -476,6 +619,32 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " });\n"
<< " ctx.strokeStyle = '#1b8f3a'; ctx.lineWidth = 1.4; ctx.stroke();\n"
<< " }\n"
<< " function drawPacketMarkers(markers, box, xMin, xMax) {\n"
<< " if (!Array.isArray(markers) || markers.length === 0) return;\n"
<< " const spanT = Math.max(1e-9, xMax - xMin);\n"
<< " ctx.save();\n"
<< " ctx.setLineDash([4, 4]);\n"
<< " ctx.strokeStyle = '#9db0c2';\n"
<< " ctx.lineWidth = 1.0;\n"
<< " ctx.fillStyle = '#5c6f82';\n"
<< " ctx.font = '11px Segoe UI';\n"
<< " markers.forEach((m) => {\n"
<< " if (!m || !Number.isFinite(m.t) || (m.t < xMin) || (m.t > xMax)) return;\n"
<< " const x = box.left + ((m.t - xMin) / spanT) * box.width;\n"
<< " ctx.beginPath(); ctx.moveTo(x, box.top); ctx.lineTo(x, box.top + box.height); ctx.stroke();\n"
<< " if (m.label) ctx.fillText(m.label, x + 4, box.top + 16);\n"
<< " });\n"
<< " ctx.restore();\n"
<< " }\n"
<< " function pickTrace(base, mid, high, maxT, view) {\n"
<< " if (!Array.isArray(base) || base.length === 0) return [];\n"
<< " const totalT = Math.max(1e-9, maxT || 0);\n"
<< " const spanT = Math.max(1e-9, view.xMax - view.xMin);\n"
<< " const ratio = spanT / totalT;\n"
<< " if (ratio <= 0.10 && Array.isArray(high) && high.length > 0) return high;\n"
<< " if (ratio <= 0.35 && Array.isArray(mid) && mid.length > 0) return mid;\n"
<< " return base;\n"
<< " }\n"
<< " function renderWaiting(message) {\n"
<< " packetInfo.textContent = 'Waiting for packets...';\n"
<< " timingInfo.textContent = '';\n"
@ -483,6 +652,7 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " zoomInfo.textContent = 'View: auto';\n"
<< " packetRateInfo.textContent = '';\n"
<< " legendCh2.style.display = '';\n"
<< " legendRss.style.display = 'none';\n"
<< " legendDi1.style.display = 'none';\n"
<< " refreshAutoCheckbox();\n"
<< " updateInfo.textContent = 'Polling every 500 ms';\n"
@ -494,13 +664,30 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " function renderPacket(data) {\n"
<< " latestPacket = data;\n"
<< " const ch1 = data.ch1 || [];\n"
<< " const ch1Mid = data.ch1Mid || [];\n"
<< " const ch1High = data.ch1High || [];\n"
<< " const ch2 = data.ch2 || [];\n"
<< " const ch2Mid = data.ch2Mid || [];\n"
<< " const ch2High = data.ch2High || [];\n"
<< " const rss = data.rss || [];\n"
<< " const rssMid = data.rssMid || [];\n"
<< " const rssHigh = data.rssHigh || [];\n"
<< " const ch1Grouped = data.ch1Grouped || [];\n"
<< " const ch2Grouped = data.ch2Grouped || [];\n"
<< " const rssGrouped = data.rssGrouped || [];\n"
<< " const di1 = data.di1 || [];\n"
<< " const packetMarkers = data.packetMarkers || [];\n"
<< " const di1Grouped = !!data.di1Grouped;\n"
<< " const hasCh2 = (data.channelCount || 2) > 1 && ch2.length > 0;\n"
<< " const hasRss = hasCh2 && rss.length > 0;\n"
<< " const hasDi1Trace = !!data.hasDi1Trace && di1.length > 0;\n"
<< " const values = [];\n"
<< " ch1.forEach(p => values.push(p[1]));\n"
<< " if (hasCh2) ch2.forEach(p => values.push(p[1]));\n"
<< " const yCh1 = di1Grouped && ch1Grouped.length > 0 ? ch1Grouped : ch1;\n"
<< " const yCh2 = di1Grouped && ch2Grouped.length > 0 ? ch2Grouped : ch2;\n"
<< " const yRss = di1Grouped && rssGrouped.length > 0 ? rssGrouped : rss;\n"
<< " yCh1.forEach(p => values.push(p[1]));\n"
<< " if (hasCh2) yCh2.forEach(p => values.push(p[1]));\n"
<< " if (hasRss) yRss.forEach(p => values.push(p[1]));\n"
<< " let minY = Math.min(...values);\n"
<< " let maxY = Math.max(...values);\n"
<< " if (!Number.isFinite(minY) || !Number.isFinite(maxY) || minY === maxY) {\n"
@ -512,22 +699,30 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " const maxT = Math.max(data.durationMs / 1000.0, 1e-9);\n"
<< " latestAutoBounds = { minY, maxY, maxT };\n"
<< " const view = currentViewBounds(latestAutoBounds);\n"
<< " const plotCh1 = di1Grouped ? ch1Grouped : pickTrace(ch1, ch1Mid, ch1High, maxT, view);\n"
<< " const plotCh2 = hasCh2 ? (di1Grouped ? ch2Grouped : pickTrace(ch2, ch2Mid, ch2High, maxT, view)) : [];\n"
<< " const plotRss = hasRss ? (di1Grouped ? rssGrouped : pickTrace(rss, rssMid, rssHigh, maxT, view)) : [];\n"
<< " const box = drawAxes(view.yMin, view.yMax, view.xMin, view.xMax);\n"
<< " drawTrace(ch1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);\n"
<< " if (hasCh2) drawTrace(ch2, '#d62828', box, view.yMin, view.yMax, view.xMin, view.xMax);\n"
<< " drawPacketMarkers(packetMarkers, box, view.xMin, view.xMax);\n"
<< " drawTrace(plotCh1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);\n"
<< " if (hasCh2) drawTrace(plotCh2, '#d62828', box, view.yMin, view.yMax, view.xMin, view.xMax);\n"
<< " if (hasRss) drawTrace(plotRss, '#ee8a12', box, view.yMin, view.yMax, view.xMin, view.xMax);\n"
<< " if (hasDi1Trace) drawDigitalTrace(di1, box, view.xMin, view.xMax);\n"
<< " legendCh2.style.display = hasCh2 ? '' : 'none';\n"
<< " legendRss.style.display = hasRss ? '' : 'none';\n"
<< " legendDi1.style.display = hasDi1Trace ? '' : 'none';\n"
<< " refreshAutoCheckbox();\n"
<< " packetInfo.textContent = 'Packet #' + data.packetIndex + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);\n"
<< " timingInfo.textContent = 'Frames/ch: ' + data.framesPerChannel + ', duration: ' + data.durationMs.toFixed(3) + ' ms';\n"
<< " const firstPacket = data.firstPacketIndex || data.packetIndex;\n"
<< " const lastPacket = data.lastPacketIndex || data.packetIndex;\n"
<< " packetInfo.textContent = 'Packets #' + firstPacket + '..' + lastPacket + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);\n"
<< " timingInfo.textContent = 'Window frames/ch: ' + data.framesPerChannel + ', window: ' + data.durationMs.toFixed(3) + ' ms, last packet: ' + data.latestPacketDurationMs.toFixed(3) + ' ms';\n"
<< " packetRateInfo.textContent = 'Packets/s: ' + data.packetsPerSecond.toFixed(3);\n"
<< " zeroInfo.textContent = hasDi1Trace\n"
<< " ? ('DI1 trace: ' + data.di1Frames + ' frame samples')\n"
<< " : ('Zeroed on DI1 change: ' + data.zeroedPercent.toFixed(3) + '% (' + data.zeroedSamples + '/' + data.storedSamples + ')');\n"
<< " zoomInfo.textContent = viewState.auto ? 'View: auto' : ('View: X ' + view.xMin.toFixed(6) + '..' + view.xMax.toFixed(6) + ' s, Y ' + view.yMin.toFixed(3) + '..' + view.yMax.toFixed(3) + ' V');\n"
<< " updateInfo.textContent = 'Snapshot: ' + data.updatedAt;\n"
<< " statusText.textContent = 'Close reason: ' + data.closeReason + '. This page refreshes its data every 500 ms.';\n"
<< " updateInfo.textContent = 'Snapshot: ' + data.updatedAt + (data.fullResLive ? ' | adaptive live resolution on' : '') + (di1Grouped ? (' | DI1-group avg: ' + (data.groupedPointCount || 0) + ' pts') : '');\n"
<< " statusText.textContent = 'Latest packet close reason: ' + data.closeReason + '. This page refreshes its rolling packet window every 500 ms.';\n"
<< " }\n"
<< " function applyLiveData(data) {\n"
<< " if (!data || data.status === 'waiting') {\n"
@ -554,6 +749,8 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " }\n"
<< " zoomXIn.addEventListener('click', () => applyZoomX(0.5));\n"
<< " zoomXOut.addEventListener('click', () => applyZoomX(2.0));\n"
<< " panLeft.addEventListener('click', () => applyPanX(-1.0));\n"
<< " panRight.addEventListener('click', () => applyPanX(1.0));\n"
<< " zoomYIn.addEventListener('click', () => applyZoomY(0.5));\n"
<< " zoomYOut.addEventListener('click', () => applyZoomY(2.0));\n"
<< " resetZoom.addEventListener('click', () => resetView());\n"
@ -578,6 +775,16 @@ void write_live_html_document(const std::string& path, const std::string& data_s
<< " }\n"
<< " }, { passive: false });\n"
<< " canvas.addEventListener('dblclick', () => resetView());\n"
<< " window.addEventListener('keydown', (event) => {\n"
<< " if (!latestPacket) return;\n"
<< " if (event.key === 'ArrowLeft') {\n"
<< " event.preventDefault();\n"
<< " applyPanX(-1.0);\n"
<< " } else if (event.key === 'ArrowRight') {\n"
<< " event.preventDefault();\n"
<< " applyPanX(1.0);\n"
<< " }\n"
<< " });\n"
<< " renderWaiting('Waiting for first packet...');\n"
<< " loadLatestData();\n"
<< " window.setInterval(loadLatestData, 500);\n"
@ -595,11 +802,17 @@ void write_live_html_document(const std::string& path, const std::string& data_s
CaptureFileWriter::CaptureFileWriter(std::string csv_path,
std::string svg_path,
std::string live_html_path,
std::string live_json_path)
std::string live_json_path,
bool full_res_live,
std::size_t live_history_packets,
bool di1_group_average)
: csv_path_(std::move(csv_path)),
svg_path_(std::move(svg_path)),
live_html_path_(std::move(live_html_path)),
live_json_path_(std::move(live_json_path)) {}
live_json_path_(std::move(live_json_path)),
full_res_live_(full_res_live),
live_history_packets_(live_history_packets),
di1_group_average_(di1_group_average) {}
const std::string& CaptureFileWriter::live_html_path() const {
return live_html_path_;
@ -694,43 +907,142 @@ void CaptureFileWriter::append_csv_packet(const CapturePacket& packet,
}
}
void CaptureFileWriter::update_live_plot(const CapturePacket& packet,
void CaptureFileWriter::update_live_plot(const std::deque<CapturePacket>& packets,
std::size_t packets_seen,
double packets_per_second,
double frame_freq_hz,
const std::string& close_reason,
std::size_t zeroed_samples,
std::size_t stored_samples) const {
const std::size_t frames = packet_frame_count(packet);
const double duration_ms = (frames == 0U) ? 0.0 : (1000.0 * static_cast<double>(frames) / frame_freq_hz);
if (packets.empty()) {
return;
}
const std::size_t begin_index =
((live_history_packets_ != 0U) && (packets.size() > live_history_packets_))
? (packets.size() - live_history_packets_)
: 0U;
const CapturePacket& latest_packet = packets.back();
const std::size_t latest_frames = packet_frame_count(latest_packet);
const double latest_duration_ms =
(latest_frames == 0U) ? 0.0 : (1000.0 * static_cast<double>(latest_frames) / frame_freq_hz);
const double zeroed_percent = (stored_samples == 0U)
? 0.0
: (100.0 * static_cast<double>(zeroed_samples) / static_cast<double>(stored_samples));
const auto trace1 = build_min_max_trace(packet.ch1, kLivePlotMaxColumns);
const auto trace2 = build_min_max_trace(packet.ch2, kLivePlotMaxColumns);
const auto di1_trace = build_digital_step_trace(packet.di1);
std::size_t total_frames = 0;
std::vector<std::pair<std::size_t, std::size_t>> packet_markers;
std::vector<double> ch1_values;
std::vector<double> ch2_values;
std::vector<uint8_t> di1_values;
for (std::size_t i = begin_index; i < packets.size(); ++i) {
total_frames += packet_frame_count(packets[i]);
}
ch1_values.reserve(total_frames);
if (packet_has_ch2(latest_packet)) {
ch2_values.reserve(total_frames);
}
if (packet_has_di1_trace(latest_packet)) {
di1_values.reserve(total_frames);
}
std::size_t frame_offset = 0;
bool first_packet = true;
for (std::size_t i = begin_index; i < packets.size(); ++i) {
const CapturePacket& packet = packets[i];
const std::size_t frames = packet_frame_count(packet);
if (frames == 0U) {
continue;
}
if (!first_packet) {
packet_markers.push_back({frame_offset, packet.packet_index});
}
first_packet = false;
ch1_values.insert(ch1_values.end(), packet.ch1.begin(), packet.ch1.begin() + static_cast<std::ptrdiff_t>(frames));
if (packet_has_ch2(packet)) {
ch2_values.insert(ch2_values.end(), packet.ch2.begin(), packet.ch2.begin() + static_cast<std::ptrdiff_t>(frames));
}
if (packet_has_di1_trace(packet)) {
di1_values.insert(di1_values.end(), packet.di1.begin(), packet.di1.begin() + static_cast<std::ptrdiff_t>(frames));
}
frame_offset += frames;
}
const double duration_ms = (total_frames == 0U) ? 0.0 : (1000.0 * static_cast<double>(total_frames) / frame_freq_hz);
std::vector<double> rss_values;
if (packet_has_ch2(latest_packet)) {
rss_values.reserve(total_frames);
for (std::size_t i = 0; i < total_frames; ++i) {
const double ch1 = ch1_values[i];
const double ch2 = ch2_values[i];
rss_values.push_back(std::sqrt((ch1 * ch1) + (ch2 * ch2)));
}
}
const auto trace1 = build_min_max_trace(ch1_values, kLivePlotMaxColumns);
const auto trace2 = build_min_max_trace(ch2_values, kLivePlotMaxColumns);
const auto rss_trace = build_min_max_trace(rss_values, kLivePlotMaxColumns);
const auto di1_trace = build_digital_step_trace(di1_values);
const bool di1_grouped = di1_group_average_ && !di1_values.empty();
const auto grouped_traces = di1_grouped
? build_di1_grouped_traces(ch1_values, ch2_values, di1_values, packet_has_ch2(latest_packet))
: Di1GroupedTraces{};
const auto trace1_mid = full_res_live_ ? build_min_max_trace(ch1_values, kLivePlotMidColumns) : std::vector<PlotPoint>{};
const auto trace2_mid = full_res_live_ ? build_min_max_trace(ch2_values, kLivePlotMidColumns) : std::vector<PlotPoint>{};
const auto rss_trace_mid = full_res_live_ ? build_min_max_trace(rss_values, kLivePlotMidColumns) : std::vector<PlotPoint>{};
const auto trace1_high = full_res_live_
? ((total_frames <= kLivePlotRawFrameLimit) ? build_full_trace(ch1_values) : build_min_max_trace(ch1_values, kLivePlotHighColumns))
: std::vector<PlotPoint>{};
const auto trace2_high = full_res_live_
? ((total_frames <= kLivePlotRawFrameLimit) ? build_full_trace(ch2_values) : build_min_max_trace(ch2_values, kLivePlotHighColumns))
: std::vector<PlotPoint>{};
const auto rss_trace_high = full_res_live_
? ((total_frames <= kLivePlotRawFrameLimit) ? build_full_trace(rss_values) : build_min_max_trace(rss_values, kLivePlotHighColumns))
: std::vector<PlotPoint>{};
std::ostringstream json;
json << std::fixed << std::setprecision(9);
json << "{\n"
<< " \"status\": \"packet\",\n"
<< " \"packetIndex\": " << packet.packet_index << ",\n"
<< " \"channelCount\": " << packet_channel_count(packet) << ",\n"
<< " \"packetIndex\": " << latest_packet.packet_index << ",\n"
<< " \"firstPacketIndex\": " << packets[begin_index].packet_index << ",\n"
<< " \"lastPacketIndex\": " << latest_packet.packet_index << ",\n"
<< " \"windowPacketCount\": " << (packets.size() - begin_index) << ",\n"
<< " \"channelCount\": " << packet_channel_count(latest_packet) << ",\n"
<< " \"packetsSeen\": " << packets_seen << ",\n"
<< " \"packetsPerSecond\": " << packets_per_second << ",\n"
<< " \"framesPerChannel\": " << frames << ",\n"
<< " \"framesPerChannel\": " << total_frames << ",\n"
<< " \"latestPacketFrames\": " << latest_frames << ",\n"
<< " \"frameFreqHz\": " << frame_freq_hz << ",\n"
<< " \"durationMs\": " << duration_ms << ",\n"
<< " \"latestPacketDurationMs\": " << latest_duration_ms << ",\n"
<< " \"closeReason\": \"" << close_reason << "\",\n"
<< " \"zeroedSamples\": " << zeroed_samples << ",\n"
<< " \"storedSamples\": " << stored_samples << ",\n"
<< " \"zeroedPercent\": " << zeroed_percent << ",\n"
<< " \"hasDi1Trace\": " << (packet_has_di1_trace(packet) ? "true" : "false") << ",\n"
<< " \"di1Frames\": " << packet.di1.size() << ",\n"
<< " \"updatedAt\": \"packet " << packet.packet_index << "\",\n"
<< " \"fullResLive\": " << (full_res_live_ ? "true" : "false") << ",\n"
<< " \"di1Grouped\": " << (di1_grouped ? "true" : "false") << ",\n"
<< " \"groupedPointCount\": " << grouped_traces.ch1.size() << ",\n"
<< " \"hasDi1Trace\": " << (packet_has_di1_trace(latest_packet) ? "true" : "false") << ",\n"
<< " \"di1Frames\": " << di1_values.size() << ",\n"
<< " \"updatedAt\": \"packet " << latest_packet.packet_index << "\",\n"
<< " \"ch1\": " << json_points(trace1, frame_freq_hz) << ",\n"
<< " \"ch1Mid\": " << json_points(trace1_mid, frame_freq_hz) << ",\n"
<< " \"ch1High\": " << json_points(trace1_high, frame_freq_hz) << ",\n"
<< " \"ch2\": " << json_points(trace2, frame_freq_hz) << ",\n"
<< " \"di1\": " << json_points(di1_trace, frame_freq_hz) << "\n"
<< " \"ch2Mid\": " << json_points(trace2_mid, frame_freq_hz) << ",\n"
<< " \"ch2High\": " << json_points(trace2_high, frame_freq_hz) << ",\n"
<< " \"rss\": " << json_points(rss_trace, frame_freq_hz) << ",\n"
<< " \"rssMid\": " << json_points(rss_trace_mid, frame_freq_hz) << ",\n"
<< " \"rssHigh\": " << json_points(rss_trace_high, frame_freq_hz) << ",\n"
<< " \"ch1Grouped\": " << json_points(grouped_traces.ch1, frame_freq_hz) << ",\n"
<< " \"ch2Grouped\": " << json_points(grouped_traces.ch2, frame_freq_hz) << ",\n"
<< " \"rssGrouped\": " << json_points(grouped_traces.rss, frame_freq_hz) << ",\n"
<< " \"di1\": " << json_points(di1_trace, frame_freq_hz) << ",\n"
<< " \"packetMarkers\": " << json_packet_markers(packet_markers, frame_freq_hz) << "\n"
<< "}\n";
const std::string json_text = json.str();
@ -777,7 +1089,7 @@ void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const {
if (!header_written) {
csv << "packet_index,frame_index,global_frame_index,time_s,packet_time_s,ch1_v";
if (channel_count >= 2U) {
csv << ",ch2_v";
csv << ",ch2_v,rss_v";
}
if (has_di1_trace || (file_header.has_di1_trace != 0U)) {
csv << ",di1";
@ -816,7 +1128,8 @@ void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const {
out << packet_index << "," << i << "," << global_frame_index << ","
<< time_s << "," << packet_time_s << "," << ch1[i];
if (channel_count >= 2U) {
out << "," << ch2[i];
const double rss = std::sqrt((ch1[i] * ch1[i]) + (ch2[i] * ch2[i]));
out << "," << ch2[i] << "," << rss;
}
if (has_di1_trace || (file_header.has_di1_trace != 0U)) {
out << "," << (has_di1_trace ? static_cast<unsigned>(di1[i]) : 0U);
@ -830,7 +1143,7 @@ void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const {
if (!header_written) {
csv << "packet_index,frame_index,global_frame_index,time_s,packet_time_s,ch1_v";
if (file_header.channel_count >= 2U) {
csv << ",ch2_v";
csv << ",ch2_v,rss_v";
}
if (file_header.has_di1_trace != 0U) {
csv << ",di1";
@ -866,6 +1179,9 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
if (packet_has_ch2(packet)) {
min_y = std::min(min_y, packet.ch2[i]);
max_y = std::max(max_y, packet.ch2[i]);
const double rss = std::sqrt((packet.ch1[i] * packet.ch1[i]) + (packet.ch2[i] * packet.ch2[i]));
min_y = std::min(min_y, rss);
max_y = std::max(max_y, rss);
}
}
}
@ -944,13 +1260,25 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
const auto trace1 = build_min_max_trace(packet.ch1, 1200);
const auto trace2 = build_min_max_trace(packet.ch2, 1200);
const auto rss_values = build_rss_values(packet);
const auto trace_rss = build_min_max_trace(rss_values, 1200);
const bool di1_grouped = di1_group_average_ && packet_has_di1_trace(packet);
const auto grouped_traces = di1_grouped
? build_di1_grouped_traces(packet.ch1, packet.ch2, packet.di1, packet_has_ch2(packet))
: Di1GroupedTraces{};
const auto trace_di1 = build_digital_step_trace(packet.di1);
const auto& plot_trace1 = di1_grouped ? grouped_traces.ch1 : trace1;
const auto& plot_trace2 = di1_grouped ? grouped_traces.ch2 : trace2;
const auto& plot_trace_rss = di1_grouped ? grouped_traces.rss : trace_rss;
file << " <polyline fill=\"none\" stroke=\"#005bbb\" stroke-width=\"1.2\" points=\""
<< polyline_points(trace1, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
<< polyline_points(plot_trace1, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
<< "\"/>\n";
if (packet_has_ch2(packet)) {
file << " <polyline fill=\"none\" stroke=\"#d62828\" stroke-width=\"1.2\" points=\""
<< polyline_points(trace2, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
<< polyline_points(plot_trace2, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
<< "\"/>\n";
file << " <polyline fill=\"none\" stroke=\"#ee8a12\" stroke-width=\"1.2\" points=\""
<< polyline_points(plot_trace_rss, frame_offset, total_frames, min_y, max_y, left, top, plot_w, plot_h)
<< "\"/>\n";
}
if (packet_has_di1_trace(packet)) {
@ -974,6 +1302,9 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
if (has_di1_trace) {
file << ", DI1 trace";
}
if (di1_group_average_ && has_di1_trace) {
file << ", averaged by DI1 runs";
}
file << "</text>\n";
file << " <text x=\"" << left << "\" y=\"" << (height - 22)
<< "\" font-size=\"16\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#425466\">time, s</text>\n";
@ -1007,6 +1338,11 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
<< "\" stroke=\"#d62828\" stroke-width=\"3\"/>\n";
file << " <text x=\"" << (width - 110) << "\" y=\"" << (legend_y + 4)
<< "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">CH2</text>\n";
file << " <line x1=\"" << (width - 360) << "\" y1=\"" << legend_y
<< "\" x2=\"" << (width - 320) << "\" y2=\"" << legend_y
<< "\" stroke=\"#ee8a12\" stroke-width=\"3\"/>\n";
file << " <text x=\"" << (width - 310) << "\" y=\"" << (legend_y + 4)
<< "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">sqrt(CH1^2+CH2^2)</text>\n";
}
if (has_di1_trace) {
file << " <line x1=\"" << (width - 80) << "\" y1=\"" << legend_y

View File

@ -2,6 +2,7 @@
#include <cstddef>
#include <cstdint>
#include <deque>
#include <string>
#include <vector>
@ -19,7 +20,10 @@ public:
CaptureFileWriter(std::string csv_path,
std::string svg_path,
std::string live_html_path,
std::string live_json_path);
std::string live_json_path,
bool full_res_live,
std::size_t live_history_packets,
bool di1_group_average);
void write(const std::vector<CapturePacket>& packets,
double frame_freq_hz,
@ -31,7 +35,7 @@ public:
double frame_freq_hz,
std::size_t& global_frame_index) const;
void update_live_plot(const CapturePacket& packet,
void update_live_plot(const std::deque<CapturePacket>& packets,
std::size_t packets_seen,
double packets_per_second,
double frame_freq_hz,
@ -52,4 +56,7 @@ private:
std::string svg_path_;
std::string live_html_path_;
std::string live_json_path_;
bool full_res_live_ = false;
std::size_t live_history_packets_ = 8;
bool di1_group_average_ = false;
};

View File

@ -1,978 +0,0 @@
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <csignal>
#include <dlfcn.h>
#endif
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable: 4995)
#endif
#include "x502api.h"
#include "e502api.h"
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <limits>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
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);
constexpr uint32_t kE502Digital2Mask = (static_cast<uint32_t>(1U) << 1U);
constexpr uint32_t kInternalRefSetting = X502_REF_FREQ_2000KHZ;
constexpr uint32_t kInternalRefHz = 2000000U;
struct Config {
std::string serial;
std::optional<uint32_t> ip_addr;
double clock_hz = 2000000.0;
double duration_ms = 100.0; // Legacy-only option retained for backward compatibility warnings.
uint32_t windows = 1000;
uint32_t recv_block_words = 8192;
uint32_t recv_timeout_ms = 50;
uint32_t clock_wait_ms = 5000;
uint32_t lchm_wait_ms = 5000;
uint32_t input_buffer_words = 262144;
uint32_t input_step_words = 8192;
bool pullup_syn2 = false;
bool legacy_clock_arg_used = false;
std::string legacy_clock_arg;
bool legacy_pullup_syn1 = false;
bool legacy_clean_start = false;
bool legacy_duration_ms = false;
};
struct LchmClockCount {
uint64_t clocks = 0;
uint64_t di2_high_clocks = 0;
uint64_t di2_low_clocks = 0;
void clear() {
clocks = 0;
di2_high_clocks = 0;
di2_low_clocks = 0;
}
void add(bool di2_high) {
++clocks;
if (di2_high) {
++di2_high_clocks;
} else {
++di2_low_clocks;
}
}
};
struct Di1StepClockCount {
bool di1_level = false;
LchmClockCount count;
};
struct RunningStats {
uint64_t count = 0;
uint64_t clocks_min = std::numeric_limits<uint64_t>::max();
uint64_t clocks_max = 0;
uint64_t clocks_sum = 0;
uint64_t high_min = std::numeric_limits<uint64_t>::max();
uint64_t high_max = 0;
uint64_t high_sum = 0;
uint64_t low_min = std::numeric_limits<uint64_t>::max();
uint64_t low_max = 0;
uint64_t low_sum = 0;
void add(const LchmClockCount& value) {
++count;
clocks_min = std::min(clocks_min, value.clocks);
clocks_max = std::max(clocks_max, value.clocks);
clocks_sum += value.clocks;
high_min = std::min(high_min, value.di2_high_clocks);
high_max = std::max(high_max, value.di2_high_clocks);
high_sum += value.di2_high_clocks;
low_min = std::min(low_min, value.di2_low_clocks);
low_max = std::max(low_max, value.di2_low_clocks);
low_sum += value.di2_low_clocks;
}
};
[[noreturn]] void fail(const std::string& message) {
throw std::runtime_error(message);
}
std::string trim_copy(const std::string& text) {
const auto first = text.find_first_not_of(" \t\r\n");
if (first == std::string::npos) {
return {};
}
const auto last = text.find_last_not_of(" \t\r\n");
return text.substr(first, last - first + 1);
}
bool starts_with(const std::string& value, const std::string& prefix) {
return value.rfind(prefix, 0) == 0;
}
uint32_t parse_u32(const std::string& text, const std::string& field_name) {
const std::string clean = trim_copy(text);
char* end = nullptr;
const auto value = std::strtoull(clean.c_str(), &end, 0);
if ((end == clean.c_str()) || (*end != '\0') || (value > std::numeric_limits<uint32_t>::max())) {
fail("Invalid integer for " + field_name + ": " + text);
}
return static_cast<uint32_t>(value);
}
double parse_double(const std::string& text, const std::string& field_name) {
const std::string clean = trim_copy(text);
char* end = nullptr;
const double value = std::strtod(clean.c_str(), &end);
if ((end == clean.c_str()) || (*end != '\0') || !std::isfinite(value)) {
fail("Invalid floating point value for " + field_name + ": " + text);
}
return value;
}
uint32_t parse_ipv4(const std::string& text) {
std::array<uint32_t, 4> parts{};
std::stringstream ss(text);
std::string token;
for (std::size_t i = 0; i < parts.size(); ++i) {
if (!std::getline(ss, token, '.')) {
fail("Invalid IPv4 address: " + text);
}
parts[i] = parse_u32(token, "ip");
if (parts[i] > 255U) {
fail("IPv4 byte out of range: " + token);
}
}
if (std::getline(ss, token, '.')) {
fail("Invalid IPv4 address: " + text);
}
return (parts[0] << 24U) | (parts[1] << 16U) | (parts[2] << 8U) | parts[3];
}
std::string ipv4_to_string(uint32_t ip_addr) {
std::ostringstream out;
out << ((ip_addr >> 24U) & 0xFFU) << '.'
<< ((ip_addr >> 16U) & 0xFFU) << '.'
<< ((ip_addr >> 8U) & 0xFFU) << '.'
<< (ip_addr & 0xFFU);
return out.str();
}
std::string parse_legacy_clock_mode(const std::string& text) {
const std::string value = trim_copy(text);
if ((value == "di_syn1_rise") || (value == "di_syn1_fall")) {
return value;
}
fail("Unsupported legacy clock mode: " + text + ". Use di_syn1_rise or di_syn1_fall.");
}
void print_help(const char* exe_name) {
std::cout
<< "Usage:\n"
<< " " << exe_name << " [serial:SN] [ip:192.168.0.10]\n"
<< " [clock_hz:2000000] [windows:1000]\n"
<< " [recv_block:8192] [recv_timeout_ms:50]\n"
<< " [clock_wait_ms:5000] [lchm_wait_ms:5000]\n"
<< " [buffer_words:262144] [step_words:8192]\n"
<< " [pullup_syn2]\n"
<< "\n"
<< "Fixed counting scheme:\n"
<< " clock -> internal only (fixed reference 2000000 Hz)\n"
<< " DI_SYN2 -> strict LCHM window; low->high starts, high->low stops\n"
<< " DI1 -> step delimiter; both DI1 edges split LCHM into steps\n"
<< " DI2 -> high/low clocks counted per step and per full LCHM\n"
<< "\n"
<< "Legacy arguments are accepted but ignored with warning:\n"
<< " clock:di_syn1_rise|di_syn1_fall, pullup_syn1, clean_start, duration_ms:...\n"
<< "\n"
<< "Example:\n"
<< " " << exe_name << " clock_hz:2000000 windows:1000 pullup_syn2\n";
}
Config parse_args(int argc, char** argv) {
Config cfg;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if ((arg == "help") || (arg == "--help") || (arg == "-h")) {
print_help(argv[0]);
std::exit(0);
}
if (arg == "pullup_syn1") {
cfg.legacy_pullup_syn1 = true;
continue;
}
if (arg == "pullup_syn2") {
cfg.pullup_syn2 = true;
continue;
}
if (arg == "clean_start") {
cfg.legacy_clean_start = true;
continue;
}
if (starts_with(arg, "serial:")) {
cfg.serial = arg.substr(7);
continue;
}
if (starts_with(arg, "ip:")) {
cfg.ip_addr = parse_ipv4(arg.substr(3));
continue;
}
if (starts_with(arg, "clock:")) {
cfg.legacy_clock_arg_used = true;
cfg.legacy_clock_arg = parse_legacy_clock_mode(arg.substr(6));
continue;
}
if (starts_with(arg, "clock_hz:")) {
cfg.clock_hz = parse_double(arg.substr(9), "clock_hz");
continue;
}
if (starts_with(arg, "sample_clock_hz:")) {
cfg.clock_hz = parse_double(arg.substr(16), "sample_clock_hz");
continue;
}
if (starts_with(arg, "duration_ms:")) {
cfg.duration_ms = parse_double(arg.substr(12), "duration_ms");
cfg.legacy_duration_ms = true;
continue;
}
if (starts_with(arg, "windows:")) {
cfg.windows = parse_u32(arg.substr(8), "windows");
continue;
}
if (starts_with(arg, "recv_block:")) {
cfg.recv_block_words = parse_u32(arg.substr(11), "recv_block");
continue;
}
if (starts_with(arg, "recv_timeout_ms:")) {
cfg.recv_timeout_ms = parse_u32(arg.substr(16), "recv_timeout_ms");
continue;
}
if (starts_with(arg, "clock_wait_ms:")) {
cfg.clock_wait_ms = parse_u32(arg.substr(14), "clock_wait_ms");
continue;
}
if (starts_with(arg, "lchm_wait_ms:")) {
cfg.lchm_wait_ms = parse_u32(arg.substr(13), "lchm_wait_ms");
continue;
}
if (starts_with(arg, "window_wait_ms:")) {
cfg.lchm_wait_ms = parse_u32(arg.substr(15), "window_wait_ms");
continue;
}
if (starts_with(arg, "buffer_words:")) {
cfg.input_buffer_words = parse_u32(arg.substr(13), "buffer_words");
continue;
}
if (starts_with(arg, "step_words:")) {
cfg.input_step_words = parse_u32(arg.substr(11), "step_words");
continue;
}
fail("Unknown argument: " + arg);
}
if (cfg.clock_hz <= 0.0) {
fail("clock_hz must be > 0");
}
if (cfg.duration_ms < 0.0) {
fail("duration_ms must be >= 0");
}
if (cfg.windows == 0U) {
fail("windows must be > 0");
}
if (cfg.recv_block_words == 0U) {
fail("recv_block must be > 0");
}
if (cfg.input_step_words == 0U) {
cfg.input_step_words = cfg.recv_block_words;
}
if (cfg.input_buffer_words < cfg.recv_block_words) {
cfg.input_buffer_words = cfg.recv_block_words;
}
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(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
}
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 {
ModuleHandle x502_module = nullptr;
ModuleHandle e502_module = nullptr;
decltype(&X502_Create) Create = nullptr;
decltype(&X502_Free) Free = nullptr;
decltype(&X502_Close) Close = nullptr;
decltype(&X502_GetErrorString) GetErrorString = nullptr;
decltype(&X502_GetDevInfo) GetDevInfo = nullptr;
decltype(&X502_GetDevInfo2) GetDevInfo2 = nullptr;
decltype(&X502_SetMode) SetMode = nullptr;
decltype(&X502_StreamsStop) StreamsStop = nullptr;
decltype(&X502_StreamsDisable) StreamsDisable = nullptr;
decltype(&X502_SetSyncMode) SetSyncMode = nullptr;
decltype(&X502_SetSyncStartMode) SetSyncStartMode = nullptr;
decltype(&X502_SetRefFreq) SetRefFreq = nullptr;
decltype(&X502_SetDinFreq) SetDinFreq = nullptr;
decltype(&X502_SetStreamBufSize) SetStreamBufSize = nullptr;
decltype(&X502_SetStreamStep) SetStreamStep = nullptr;
decltype(&X502_SetDigInPullup) SetDigInPullup = nullptr;
decltype(&X502_Configure) Configure = nullptr;
decltype(&X502_StreamsEnable) StreamsEnable = nullptr;
decltype(&X502_StreamsStart) StreamsStart = nullptr;
decltype(&X502_GetRecvReadyCount) GetRecvReadyCount = nullptr;
decltype(&X502_Recv) Recv = nullptr;
decltype(&X502_ProcessData) ProcessData = nullptr;
decltype(&E502_OpenUsb) OpenUsb = nullptr;
decltype(&E502_OpenByIpAddr) OpenByIpAddr = nullptr;
Api() {
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");
Close = load_symbol<decltype(Close)>(x502_module, "X502_Close");
GetErrorString = load_symbol<decltype(GetErrorString)>(x502_module, "X502_GetErrorString");
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");
StreamsStop = load_symbol<decltype(StreamsStop)>(x502_module, "X502_StreamsStop");
StreamsDisable = load_symbol<decltype(StreamsDisable)>(x502_module, "X502_StreamsDisable");
SetSyncMode = load_symbol<decltype(SetSyncMode)>(x502_module, "X502_SetSyncMode");
SetSyncStartMode = load_symbol<decltype(SetSyncStartMode)>(x502_module, "X502_SetSyncStartMode");
SetRefFreq = load_symbol<decltype(SetRefFreq)>(x502_module, "X502_SetRefFreq");
SetDinFreq = load_symbol<decltype(SetDinFreq)>(x502_module, "X502_SetDinFreq");
SetStreamBufSize = load_symbol<decltype(SetStreamBufSize)>(x502_module, "X502_SetStreamBufSize");
SetStreamStep = load_symbol<decltype(SetStreamStep)>(x502_module, "X502_SetStreamStep");
SetDigInPullup = load_symbol<decltype(SetDigInPullup)>(x502_module, "X502_SetDigInPullup");
Configure = load_symbol<decltype(Configure)>(x502_module, "X502_Configure");
StreamsEnable = load_symbol<decltype(StreamsEnable)>(x502_module, "X502_StreamsEnable");
StreamsStart = load_symbol<decltype(StreamsStart)>(x502_module, "X502_StreamsStart");
GetRecvReadyCount = load_symbol<decltype(GetRecvReadyCount)>(x502_module, "X502_GetRecvReadyCount");
Recv = load_symbol<decltype(Recv)>(x502_module, "X502_Recv");
ProcessData = load_symbol<decltype(ProcessData)>(x502_module, "X502_ProcessData");
OpenUsb = load_symbol<decltype(OpenUsb)>(e502_module, "E502_OpenUsb");
OpenByIpAddr = load_symbol<decltype(OpenByIpAddr)>(e502_module, "E502_OpenByIpAddr");
}
~Api() {
close_library(e502_module);
close_library(x502_module);
}
};
std::string x502_error(const Api& api, int32_t err) {
const char* text = api.GetErrorString ? api.GetErrorString(err) : nullptr;
std::ostringstream out;
out << "err=" << err;
if ((text != nullptr) && (*text != '\0')) {
out << " (" << text << ")";
}
return out.str();
}
void expect_ok(const Api& api, int32_t err, const std::string& what) {
if (err != X502_ERR_OK) {
fail(what + ": " + x502_error(api, err));
}
}
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
}
TickMs elapsed_ms(TickMs now, TickMs start) {
return (now >= start) ? (now - start) : 0U;
}
#ifdef _WIN32
volatile LONG g_console_stop_requested = 0;
BOOL WINAPI console_ctrl_handler(DWORD ctrl_type) {
if ((ctrl_type == CTRL_C_EVENT) || (ctrl_type == CTRL_BREAK_EVENT) || (ctrl_type == CTRL_CLOSE_EVENT)) {
InterlockedExchange(&g_console_stop_requested, 1);
return TRUE;
}
return FALSE;
}
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
struct ConsoleCtrlGuard {
bool installed = false;
ConsoleCtrlGuard() {
#ifdef _WIN32
installed = SetConsoleCtrlHandler(console_ctrl_handler, TRUE) != 0;
#else
struct sigaction action {};
action.sa_handler = console_ctrl_handler;
sigemptyset(&action.sa_mask);
installed = (sigaction(SIGINT, &action, nullptr) == 0);
#endif
}
~ConsoleCtrlGuard() {
#ifdef _WIN32
if (installed) {
SetConsoleCtrlHandler(console_ctrl_handler, FALSE);
}
#else
if (installed) {
struct sigaction action {};
action.sa_handler = SIG_DFL;
sigemptyset(&action.sa_mask);
sigaction(SIGINT, &action, nullptr);
}
#endif
}
};
struct DeviceHandle {
const Api& api;
t_x502_hnd hnd = nullptr;
bool opened = false;
bool streams_started = false;
explicit DeviceHandle(const Api& api_ref) : api(api_ref), hnd(api.Create()) {
if (hnd == nullptr) {
fail("X502_Create failed");
}
}
~DeviceHandle() {
if (hnd != nullptr) {
if (streams_started) {
api.StreamsStop(hnd);
}
if (opened) {
api.Close(hnd);
}
api.Free(hnd);
}
}
};
void print_device_info(const t_x502_info& info) {
std::cout << "Device: " << info.name << "\n"
<< "Serial: " << info.serial << "\n"
<< "FPGA version: " << static_cast<unsigned>(info.fpga_ver >> 8U) << "."
<< static_cast<unsigned>(info.fpga_ver & 0xFFU) << "\n"
<< "PLDA version: " << static_cast<unsigned>(info.plda_ver) << "\n"
<< "Board revision: " << static_cast<unsigned>(info.board_rev) << "\n"
<< "MCU firmware: " << info.mcu_firmware_ver << "\n";
}
void print_summary(const RunningStats& stats) {
if (stats.count == 0U) {
std::cout << "No complete LCHM windows captured\n";
return;
}
const auto avg = [count = stats.count](uint64_t sum) {
return static_cast<double>(sum) / static_cast<double>(count);
};
std::cout << std::fixed << std::setprecision(3)
<< "Summary: windows=" << stats.count << "\n"
<< " clocks: min=" << stats.clocks_min
<< ", avg=" << avg(stats.clocks_sum)
<< ", max=" << stats.clocks_max << "\n"
<< " di2_high_clocks: min=" << stats.high_min
<< ", avg=" << avg(stats.high_sum)
<< ", max=" << stats.high_max << "\n"
<< " di2_low_clocks: min=" << stats.low_min
<< ", avg=" << avg(stats.low_sum)
<< ", max=" << stats.low_max << "\n";
}
int run(const Config& cfg) {
if (cfg.legacy_clock_arg_used) {
std::cerr << "Warning: legacy argument clock:" << cfg.legacy_clock_arg
<< " is ignored. lchm_clock_counter uses internal clock only.\n";
}
if (cfg.legacy_pullup_syn1) {
std::cerr << "Warning: legacy argument pullup_syn1 is ignored in internal-only mode.\n";
}
if (cfg.legacy_clean_start) {
std::cerr << "Warning: legacy argument clean_start is ignored. "
<< "LCHM starts strictly on DI_SYN2 low->high.\n";
}
if (cfg.legacy_duration_ms) {
std::cerr << "Warning: legacy argument duration_ms:" << cfg.duration_ms
<< " is ignored. LCHM closes only on DI_SYN2 high->low.\n";
}
Api api;
DeviceHandle device(api);
int32_t open_err = X502_ERR_OK;
if (cfg.ip_addr.has_value()) {
open_err = api.OpenByIpAddr(device.hnd, *cfg.ip_addr, 0, 5000);
} else {
open_err = api.OpenUsb(device.hnd, cfg.serial.empty() ? nullptr : cfg.serial.c_str());
}
expect_ok(api, open_err, cfg.ip_addr.has_value()
? ("Open device by IP " + ipv4_to_string(*cfg.ip_addr))
: std::string("Open device over USB"));
device.opened = true;
t_x502_info info {};
int32_t info_err = X502_ERR_OK;
if (api.GetDevInfo2 != nullptr) {
info_err = api.GetDevInfo2(device.hnd, &info, sizeof(info));
} else {
info_err = api.GetDevInfo(device.hnd, &info);
}
expect_ok(api, info_err, "Get device info");
print_device_info(info);
expect_ok(api, api.SetMode(device.hnd, X502_MODE_FPGA), "Set FPGA mode");
api.StreamsStop(device.hnd);
api.StreamsDisable(device.hnd, X502_STREAM_ALL_IN | X502_STREAM_ALL_OUT);
expect_ok(api, api.SetSyncMode(device.hnd, X502_SYNC_INTERNAL), "Set internal sync mode");
expect_ok(api, api.SetSyncStartMode(device.hnd, X502_SYNC_INTERNAL), "Set immediate stream start");
expect_ok(api, api.SetRefFreq(device.hnd, kInternalRefSetting), "Set internal reference frequency");
double actual_din_freq_hz = cfg.clock_hz;
expect_ok(api, api.SetDinFreq(device.hnd, &actual_din_freq_hz), "Set DIN frequency");
expect_ok(api, api.SetStreamBufSize(device.hnd, X502_STREAM_CH_IN, cfg.input_buffer_words),
"Set input buffer size");
expect_ok(api, api.SetStreamStep(device.hnd, X502_STREAM_CH_IN, cfg.input_step_words),
"Set input stream step");
uint32_t pullups = 0;
if (cfg.pullup_syn2) {
pullups |= X502_PULLUPS_DI_SYN2;
}
expect_ok(api, api.SetDigInPullup(device.hnd, pullups), "Set digital input pullups");
expect_ok(api, api.Configure(device.hnd, 0), "Configure device");
expect_ok(api, api.StreamsEnable(device.hnd, X502_STREAM_DIN), "Enable DIN stream");
std::cout << "LCHM clock counter settings:\n"
<< " clock source: internal\n"
<< " internal ref: " << kInternalRefHz << " Hz\n"
<< " requested DIN clock: " << cfg.clock_hz << " Hz\n"
<< " effective DIN clock: " << actual_din_freq_hz << " Hz\n"
<< " LCHM gate: strict DI_SYN2 low->high start, high->low stop\n"
<< " DI1 step segmentation: both edges\n"
<< " DI2 split: enabled per step and per full LCHM\n"
<< " duration limit: disabled (legacy duration_ms ignored)"
<< "\n"
<< " target windows: " << cfg.windows << "\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";
if (std::fabs(actual_din_freq_hz - cfg.clock_hz) > std::max(0.5, cfg.clock_hz * 1e-6)) {
std::cerr << "Warning: effective DIN clock differs from requested value: requested="
<< cfg.clock_hz << " Hz, effective=" << actual_din_freq_hz << " Hz\n";
}
ConsoleCtrlGuard console_guard;
if (!console_guard.installed) {
std::cerr << "Warning: Ctrl+C handler could not be installed; stop may be abrupt.\n";
}
expect_ok(api, api.StreamsStart(device.hnd), "Start streams");
device.streams_started = true;
std::vector<uint32_t> raw(cfg.recv_block_words);
std::vector<uint32_t> din_buffer(cfg.recv_block_words);
bool gate_initialized = false;
bool last_gate = false;
bool di1_initialized = false;
bool last_di1_level = false;
bool in_lchm = false;
LchmClockCount current;
LchmClockCount current_step;
bool current_step_level = false;
std::vector<Di1StepClockCount> current_steps;
RunningStats stats;
const TickMs session_start = tick_count_ms();
TickMs last_stream_activity = session_start;
TickMs last_lchm_complete = session_start;
TickMs last_din_activity = session_start;
TickMs last_gate_edge = session_start;
uint64_t total_raw_words = 0;
uint64_t total_din_words = 0;
uint64_t total_gate_edges = 0;
auto start_lchm = [&](bool di1_level) {
in_lchm = true;
current.clear();
current_step.clear();
current_step_level = di1_level;
current_steps.clear();
};
auto finalize_current_step = [&]() {
if (current_step.clocks == 0U) {
return;
}
Di1StepClockCount step;
step.di1_level = current_step_level;
step.count = current_step;
current_steps.push_back(step);
current_step.clear();
};
auto finalize_lchm = [&](const char* close_reason, TickMs now) {
finalize_current_step();
if (current.clocks != (current.di2_high_clocks + current.di2_low_clocks)) {
std::ostringstream message;
message << "DI2 clock split invariant failed: clocks=" << current.clocks
<< ", high=" << current.di2_high_clocks
<< ", low=" << current.di2_low_clocks;
fail(message.str());
}
uint64_t step_sum_clocks = 0;
uint64_t step_sum_high = 0;
uint64_t step_sum_low = 0;
for (const auto& step : current_steps) {
step_sum_clocks += step.count.clocks;
step_sum_high += step.count.di2_high_clocks;
step_sum_low += step.count.di2_low_clocks;
}
if ((step_sum_clocks != current.clocks) ||
(step_sum_high != current.di2_high_clocks) ||
(step_sum_low != current.di2_low_clocks)) {
std::ostringstream message;
message << "DI1 step split invariant failed: total clocks/high/low="
<< current.clocks << "/" << current.di2_high_clocks << "/" << current.di2_low_clocks
<< ", step sum clocks/high/low="
<< step_sum_clocks << "/" << step_sum_high << "/" << step_sum_low;
fail(message.str());
}
if (current.clocks != 0U) {
const uint64_t lchm_index = stats.count + 1U;
stats.add(current);
std::cout << "LCHM " << lchm_index
<< ": clocks=" << current.clocks
<< ", di2_high_clocks=" << current.di2_high_clocks
<< ", di2_low_clocks=" << current.di2_low_clocks
<< ", close_reason=" << close_reason
<< "\n";
for (std::size_t step_index = 0; step_index < current_steps.size(); ++step_index) {
const auto& step = current_steps[step_index];
std::cout << " step " << (step_index + 1U)
<< ": di1_level=" << (step.di1_level ? "HIGH" : "LOW")
<< ", clocks=" << step.count.clocks
<< ", di2_high_clocks=" << step.count.di2_high_clocks
<< ", di2_low_clocks=" << step.count.di2_low_clocks
<< "\n";
}
last_lchm_complete = now;
}
current.clear();
current_step.clear();
current_steps.clear();
};
auto fail_waiting_for_lchm = [&](TickMs now) {
std::ostringstream message;
message << "ADC/DIN clock is present, but no complete DI_SYN2 LCHM window was captured within "
<< cfg.lchm_wait_ms << " ms. "
<< "DIN samples=" << total_din_words
<< ", gate edges=" << total_gate_edges << ". ";
if (total_din_words == 0U) {
message << "No synchronous DIN words were decoded. ";
} else if (!gate_initialized) {
message << "DIN data is present, but DI_SYN2 state was not initialized yet. ";
} else if (total_gate_edges == 0U) {
message << "DI_SYN2 appears stuck " << (last_gate ? "HIGH" : "LOW") << ". ";
} else if (in_lchm) {
message << "DI_SYN2 produced a rising edge, but no matching falling edge closed the LCHM. ";
} else {
message << "DI_SYN2 toggled, but no complete low->high->low LCHM was accepted. ";
}
message << "Check DI_SYN2 wiring, common DGND, and signal level around 0/3.3 V. "
<< "Progress: last DIN activity " << elapsed_ms(now, last_din_activity)
<< " ms ago, last DI_SYN2 edge " << elapsed_ms(now, last_gate_edge) << " ms ago.";
fail(message.str());
};
while ((stats.count < cfg.windows) && !console_stop_requested()) {
uint32_t recv_request_words = cfg.recv_block_words;
uint32_t recv_timeout_ms = cfg.recv_timeout_ms;
uint32_t ready_words = 0;
const int32_t ready_err = api.GetRecvReadyCount(device.hnd, &ready_words);
if ((ready_err == X502_ERR_OK) && (ready_words != 0U)) {
recv_request_words = std::min<uint32_t>(ready_words, cfg.recv_block_words);
recv_timeout_ms = 0;
}
const int32_t recvd = api.Recv(device.hnd, raw.data(), recv_request_words, recv_timeout_ms);
if (recvd < 0) {
fail("X502_Recv failed: " + x502_error(api, recvd));
}
const TickMs now = tick_count_ms();
if (recvd == 0) {
if (elapsed_ms(now, last_stream_activity) >= cfg.clock_wait_ms) {
fail("Timeout waiting for DIN stream data in internal clock mode. "
"Check device state and DIN stream configuration.");
}
if (elapsed_ms(now, last_lchm_complete) >= cfg.lchm_wait_ms) {
fail_waiting_for_lchm(now);
}
continue;
}
last_stream_activity = now;
total_raw_words += static_cast<uint64_t>(recvd);
uint32_t din_count = static_cast<uint32_t>(din_buffer.size());
expect_ok(api,
api.ProcessData(device.hnd,
raw.data(),
static_cast<uint32_t>(recvd),
0U,
nullptr,
nullptr,
din_buffer.data(),
&din_count),
"Process DIN data");
if (din_count != 0U) {
total_din_words += din_count;
last_din_activity = now;
}
for (uint32_t i = 0; (i < din_count) && (stats.count < cfg.windows); ++i) {
const uint32_t din_value = din_buffer[i];
const bool gate = (din_value & kE502DiSyn2Mask) != 0U;
const bool di1_level = (din_value & kE502Digital1Mask) != 0U;
const bool di2_high = (din_value & kE502Digital2Mask) != 0U;
bool di1_changed = false;
if (!di1_initialized) {
di1_initialized = true;
last_di1_level = di1_level;
} else if (di1_level != last_di1_level) {
di1_changed = true;
last_di1_level = di1_level;
}
if (!gate_initialized) {
gate_initialized = true;
last_gate = gate;
last_gate_edge = now;
}
if (gate_initialized && (gate != last_gate)) {
++total_gate_edges;
last_gate_edge = now;
}
if (!in_lchm && gate_initialized && !last_gate && gate) {
start_lchm(di1_level);
}
if (in_lchm && last_gate && !gate) {
finalize_lchm("di_syn2_fall", now);
in_lchm = false;
}
if (in_lchm && gate) {
if (di1_changed && (current_step.clocks != 0U)) {
finalize_current_step();
current_step_level = di1_level;
}
current.add(di2_high);
current_step.add(di2_high);
}
last_gate = gate;
}
const TickMs now_after_block = tick_count_ms();
if ((stats.count < cfg.windows) && (elapsed_ms(now_after_block, last_lchm_complete) >= cfg.lchm_wait_ms)) {
fail_waiting_for_lchm(now_after_block);
}
}
if (console_stop_requested() && in_lchm) {
finalize_lchm("user_stop", tick_count_ms());
}
expect_ok(api, api.StreamsStop(device.hnd), "Stop streams");
device.streams_started = false;
std::cout << "Raw words read: " << total_raw_words
<< ", DIN samples: " << total_din_words
<< ", DI_SYN2 edges: " << total_gate_edges << "\n";
print_summary(stats);
return 0;
}
} // namespace
int main(int argc, char** argv) {
try {
const Config cfg = parse_args(argc, argv);
return run(cfg);
} catch (const std::exception& ex) {
std::cerr << "Error: " << ex.what() << "\n";
return 1;
}
}

View File

@ -42,20 +42,23 @@
<span id="updateInfo">Auto-refresh every 500 ms</span>
</div>
</div>
<div class="status" id="statusText">Open this page once and leave it open. It refreshes its data script to pick up new packets.</div>
<div class="status" id="statusText">Open this page once and leave it open. It refreshes its data script to pick up a rolling continuous packet window.</div>
<div class="controls">
<button type="button" id="zoomXIn">X+</button>
<button type="button" id="zoomXOut">X-</button>
<button type="button" id="panLeft">Left</button>
<button type="button" id="panRight">Right</button>
<button type="button" id="zoomYIn">Y+</button>
<button type="button" id="zoomYOut">Y-</button>
<button type="button" id="resetZoom">Reset</button>
<label><input type="checkbox" id="autoZoom" checked/>Auto view</label>
<span class="hint">Wheel: X zoom, Shift+wheel: Y zoom, double-click: reset</span>
<span class="hint">Wheel: X zoom, Shift+wheel: Y zoom, Left/Right buttons or arrow keys: pan X, double-click: reset. The live view shows a rolling continuous packet window; CSV stores all samples.</span>
</div>
<canvas id="plot" width="1200" height="620"></canvas>
<div class="legend">
<span id="legendCh1"><span class="sw" style="background:#005bbb"></span>CH1</span>
<span id="legendCh2"><span class="sw" style="background:#d62828"></span>CH2</span>
<span id="legendRss"><span class="sw" style="background:#ee8a12"></span>sqrt(CH1^2 + CH2^2)</span>
<span id="legendDi1"><span class="sw" style="background:#1b8f3a"></span>DI1</span>
</div>
</div>
@ -72,9 +75,12 @@
const packetRateInfo = document.getElementById('packetRateInfo');
const updateInfo = document.getElementById('updateInfo');
const legendCh2 = document.getElementById('legendCh2');
const legendRss = document.getElementById('legendRss');
const legendDi1 = document.getElementById('legendDi1');
const zoomXIn = document.getElementById('zoomXIn');
const zoomXOut = document.getElementById('zoomXOut');
const panLeft = document.getElementById('panLeft');
const panRight = document.getElementById('panRight');
const zoomYIn = document.getElementById('zoomYIn');
const zoomYOut = document.getElementById('zoomYOut');
const resetZoom = document.getElementById('resetZoom');
@ -165,6 +171,30 @@
refreshAutoCheckbox();
renderPacket(latestPacket);
}
function applyPanX(direction) {
if (!latestAutoBounds || !latestPacket) return;
const current = currentViewBounds(latestAutoBounds);
const totalX = Math.max(1e-9, latestAutoBounds.maxT);
const span = Math.max(1e-9, current.xMax - current.xMin);
if (span >= totalX) return;
const shift = span * 0.25 * direction;
let newMin = current.xMin + shift;
let newMax = current.xMax + shift;
if (newMin < 0) {
newMax -= newMin;
newMin = 0;
}
if (newMax > totalX) {
newMin -= (newMax - totalX);
newMax = totalX;
}
newMin = Math.max(0, newMin);
newMax = Math.min(totalX, newMax);
viewState = { auto: false, xMin: newMin, xMax: newMax, yMin: current.yMin, yMax: current.yMax };
saveViewState();
refreshAutoCheckbox();
renderPacket(latestPacket);
}
function resetView() {
viewState = defaultViewState();
saveViewState();
@ -222,6 +252,32 @@
});
ctx.strokeStyle = '#1b8f3a'; ctx.lineWidth = 1.4; ctx.stroke();
}
function drawPacketMarkers(markers, box, xMin, xMax) {
if (!Array.isArray(markers) || markers.length === 0) return;
const spanT = Math.max(1e-9, xMax - xMin);
ctx.save();
ctx.setLineDash([4, 4]);
ctx.strokeStyle = '#9db0c2';
ctx.lineWidth = 1.0;
ctx.fillStyle = '#5c6f82';
ctx.font = '11px Segoe UI';
markers.forEach((m) => {
if (!m || !Number.isFinite(m.t) || (m.t < xMin) || (m.t > xMax)) return;
const x = box.left + ((m.t - xMin) / spanT) * box.width;
ctx.beginPath(); ctx.moveTo(x, box.top); ctx.lineTo(x, box.top + box.height); ctx.stroke();
if (m.label) ctx.fillText(m.label, x + 4, box.top + 16);
});
ctx.restore();
}
function pickTrace(base, mid, high, maxT, view) {
if (!Array.isArray(base) || base.length === 0) return [];
const totalT = Math.max(1e-9, maxT || 0);
const spanT = Math.max(1e-9, view.xMax - view.xMin);
const ratio = spanT / totalT;
if (ratio <= 0.10 && Array.isArray(high) && high.length > 0) return high;
if (ratio <= 0.35 && Array.isArray(mid) && mid.length > 0) return mid;
return base;
}
function renderWaiting(message) {
packetInfo.textContent = 'Waiting for packets...';
timingInfo.textContent = '';
@ -229,6 +285,7 @@
zoomInfo.textContent = 'View: auto';
packetRateInfo.textContent = '';
legendCh2.style.display = '';
legendRss.style.display = 'none';
legendDi1.style.display = 'none';
refreshAutoCheckbox();
updateInfo.textContent = 'Polling every 500 ms';
@ -240,13 +297,30 @@
function renderPacket(data) {
latestPacket = data;
const ch1 = data.ch1 || [];
const ch1Mid = data.ch1Mid || [];
const ch1High = data.ch1High || [];
const ch2 = data.ch2 || [];
const ch2Mid = data.ch2Mid || [];
const ch2High = data.ch2High || [];
const rss = data.rss || [];
const rssMid = data.rssMid || [];
const rssHigh = data.rssHigh || [];
const ch1Grouped = data.ch1Grouped || [];
const ch2Grouped = data.ch2Grouped || [];
const rssGrouped = data.rssGrouped || [];
const di1 = data.di1 || [];
const packetMarkers = data.packetMarkers || [];
const di1Grouped = !!data.di1Grouped;
const hasCh2 = (data.channelCount || 2) > 1 && ch2.length > 0;
const hasRss = hasCh2 && rss.length > 0;
const hasDi1Trace = !!data.hasDi1Trace && di1.length > 0;
const values = [];
ch1.forEach(p => values.push(p[1]));
if (hasCh2) ch2.forEach(p => values.push(p[1]));
const yCh1 = di1Grouped && ch1Grouped.length > 0 ? ch1Grouped : ch1;
const yCh2 = di1Grouped && ch2Grouped.length > 0 ? ch2Grouped : ch2;
const yRss = di1Grouped && rssGrouped.length > 0 ? rssGrouped : rss;
yCh1.forEach(p => values.push(p[1]));
if (hasCh2) yCh2.forEach(p => values.push(p[1]));
if (hasRss) yRss.forEach(p => values.push(p[1]));
let minY = Math.min(...values);
let maxY = Math.max(...values);
if (!Number.isFinite(minY) || !Number.isFinite(maxY) || minY === maxY) {
@ -258,22 +332,30 @@
const maxT = Math.max(data.durationMs / 1000.0, 1e-9);
latestAutoBounds = { minY, maxY, maxT };
const view = currentViewBounds(latestAutoBounds);
const plotCh1 = di1Grouped ? ch1Grouped : pickTrace(ch1, ch1Mid, ch1High, maxT, view);
const plotCh2 = hasCh2 ? (di1Grouped ? ch2Grouped : pickTrace(ch2, ch2Mid, ch2High, maxT, view)) : [];
const plotRss = hasRss ? (di1Grouped ? rssGrouped : pickTrace(rss, rssMid, rssHigh, maxT, view)) : [];
const box = drawAxes(view.yMin, view.yMax, view.xMin, view.xMax);
drawTrace(ch1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);
if (hasCh2) drawTrace(ch2, '#d62828', box, view.yMin, view.yMax, view.xMin, view.xMax);
drawPacketMarkers(packetMarkers, box, view.xMin, view.xMax);
drawTrace(plotCh1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);
if (hasCh2) drawTrace(plotCh2, '#d62828', box, view.yMin, view.yMax, view.xMin, view.xMax);
if (hasRss) drawTrace(plotRss, '#ee8a12', box, view.yMin, view.yMax, view.xMin, view.xMax);
if (hasDi1Trace) drawDigitalTrace(di1, box, view.xMin, view.xMax);
legendCh2.style.display = hasCh2 ? '' : 'none';
legendRss.style.display = hasRss ? '' : 'none';
legendDi1.style.display = hasDi1Trace ? '' : 'none';
refreshAutoCheckbox();
packetInfo.textContent = 'Packet #' + data.packetIndex + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);
timingInfo.textContent = 'Frames/ch: ' + data.framesPerChannel + ', duration: ' + data.durationMs.toFixed(3) + ' ms';
const firstPacket = data.firstPacketIndex || data.packetIndex;
const lastPacket = data.lastPacketIndex || data.packetIndex;
packetInfo.textContent = 'Packets #' + firstPacket + '..' + lastPacket + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);
timingInfo.textContent = 'Window frames/ch: ' + data.framesPerChannel + ', window: ' + data.durationMs.toFixed(3) + ' ms, last packet: ' + data.latestPacketDurationMs.toFixed(3) + ' ms';
packetRateInfo.textContent = 'Packets/s: ' + data.packetsPerSecond.toFixed(3);
zeroInfo.textContent = hasDi1Trace
? ('DI1 trace: ' + data.di1Frames + ' frame samples')
: ('Zeroed on DI1 change: ' + data.zeroedPercent.toFixed(3) + '% (' + data.zeroedSamples + '/' + data.storedSamples + ')');
zoomInfo.textContent = viewState.auto ? 'View: auto' : ('View: X ' + view.xMin.toFixed(6) + '..' + view.xMax.toFixed(6) + ' s, Y ' + view.yMin.toFixed(3) + '..' + view.yMax.toFixed(3) + ' V');
updateInfo.textContent = 'Snapshot: ' + data.updatedAt;
statusText.textContent = 'Close reason: ' + data.closeReason + '. This page refreshes its data every 500 ms.';
updateInfo.textContent = 'Snapshot: ' + data.updatedAt + (data.fullResLive ? ' | adaptive live resolution on' : '') + (di1Grouped ? (' | DI1-group avg: ' + (data.groupedPointCount || 0) + ' pts') : '');
statusText.textContent = 'Latest packet close reason: ' + data.closeReason + '. This page refreshes its rolling packet window every 500 ms.';
}
function applyLiveData(data) {
if (!data || data.status === 'waiting') {
@ -300,6 +382,8 @@
}
zoomXIn.addEventListener('click', () => applyZoomX(0.5));
zoomXOut.addEventListener('click', () => applyZoomX(2.0));
panLeft.addEventListener('click', () => applyPanX(-1.0));
panRight.addEventListener('click', () => applyPanX(1.0));
zoomYIn.addEventListener('click', () => applyZoomY(0.5));
zoomYOut.addEventListener('click', () => applyZoomY(2.0));
resetZoom.addEventListener('click', () => resetView());
@ -324,6 +408,16 @@
}
}, { passive: false });
canvas.addEventListener('dblclick', () => resetView());
window.addEventListener('keydown', (event) => {
if (!latestPacket) return;
if (event.key === 'ArrowLeft') {
event.preventDefault();
applyPanX(-1.0);
} else if (event.key === 'ArrowRight') {
event.preventDefault();
applyPanX(1.0);
}
});
renderWaiting('Waiting for first packet...');
loadLatestData();
window.setInterval(loadLatestData, 500);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1000
main.cpp

File diff suppressed because it is too large Load Diff

1
run exmple.txt Normal file
View File

@ -0,0 +1 @@
.\main.exe clock:internal internal_ref_hz:2000000 start:di_syn2_rise stop:di_syn2_fall sample_clock_hz:max range:0.2 di1:trace di1_group_avg full_res_live live_history_packets:8 duration_ms:100 packet_limit:0 csv:capture.csv svg:capture.svg

View File

@ -1,34 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
BIN="${BIN:-./main.exe}"
TTY_PATH="${TTY_PATH:-/tmp/ttyADC_data}"
LIB_DIR="${LIB_DIR:-$HOME/.local/lib}"
if [[ ! -x "$BIN" ]]; then
echo "Binary '$BIN' not found or not executable. Run ./build_main.sh first." >&2
exit 1
fi
if [[ -d "$LIB_DIR" ]]; then
export LD_LIBRARY_PATH="${LIB_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
fi
exec "$BIN" \
clock:internal \
internal_ref_hz:2000000 \
start:di_syn2_rise \
stop:di_syn2_fall \
sample_clock_hz:max \
range:5\
di1:trace \
di1_group_avg \
duration_ms:100 \
packet_limit:0 \
do1_toggle_per_frame \
profile:amplitude \
"tty:${TTY_PATH}" \
"$@"

View File

@ -1,45 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
BIN="${BIN:-./main.exe}"
TTY_PATH="${TTY_PATH:-/tmp/ttyADC_data}"
LIB_DIR="${LIB_DIR:-$HOME/.local/lib}"
NOISE_AVG_STEPS="${NOISE_AVG_STEPS:-}"
if [[ ! -x "$BIN" ]]; then
echo "Binary '$BIN' not found or not executable. Run ./build_main.sh first." >&2
exit 1
fi
if [[ -z "$NOISE_AVG_STEPS" ]]; then
echo "Set NOISE_AVG_STEPS to a positive integer, for example: NOISE_AVG_STEPS=16" >&2
exit 1
fi
if ! [[ "$NOISE_AVG_STEPS" =~ ^[0-9]+$ ]] || [[ "$NOISE_AVG_STEPS" -eq 0 ]]; then
echo "NOISE_AVG_STEPS must be a positive integer, got '$NOISE_AVG_STEPS'" >&2
exit 1
fi
if [[ -d "$LIB_DIR" ]]; then
export LD_LIBRARY_PATH="${LIB_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
fi
exec "$BIN" \
clock:internal \
internal_ref_hz:2000000 \
start:di_syn2_rise \
stop:di_syn2_fall \
sample_clock_hz:max \
range:5 \
duration_ms:100 \
packet_limit:0 \
do1_toggle_per_frame \
do1_noise_subtract \
"noise_avg_steps:${NOISE_AVG_STEPS}" \
profile:phase \
"tty:${TTY_PATH}" \
"$@"

View File

@ -1,443 +1,127 @@
#include "tty_protocol_writer.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstring>
#include <exception>
#include <stdexcept>
#include <thread>
#include <utility>
#include <vector>
#ifdef _WIN32
struct TtyProtocolWriter::Impl {};
TtyProtocolWriter::TtyProtocolWriter(std::string path, std::size_t ring_capacity_bytes)
: path_(std::move(path)) {
(void) ring_capacity_bytes;
TtyProtocolWriter::TtyProtocolWriter(std::string path) : path_(std::move(path)) {
throw std::runtime_error("tty output is supported only on Linux/POSIX");
}
TtyProtocolWriter::~TtyProtocolWriter() = default;
void TtyProtocolWriter::emit_packet_start(uint16_t marker) {
(void) marker;
}
TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept = default;
void TtyProtocolWriter::emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) {
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;
}
void TtyProtocolWriter::enqueue_encoded_frames(const uint16_t* words, std::size_t frame_count) {
(void) words;
(void) frame_count;
}
TtyProtocolWriter::StatsSnapshot TtyProtocolWriter::stats() const {
return {};
}
const std::string& TtyProtocolWriter::path() const {
return path_;
}
void TtyProtocolWriter::throw_if_failed() const {}
void TtyProtocolWriter::shutdown() {}
void TtyProtocolWriter::enqueue_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) {
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::worker_loop() {}
void TtyProtocolWriter::close_fd() noexcept {}
#else
#include <cerrno>
#include <condition_variable>
#include <cstring>
#include <fcntl.h>
#include <limits.h>
#include <mutex>
#include <optional>
#include <pty.h>
#include <sstream>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
namespace {
constexpr std::size_t kFrameWordCount = 4U;
constexpr std::size_t kFrameByteCount = kFrameWordCount * sizeof(uint16_t);
using EncodedFrame = std::array<std::uint8_t, kFrameByteCount>;
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 close_fd_if_open(int& fd) noexcept {
if (fd >= 0) {
::close(fd);
fd = -1;
}
}
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);
tio.c_cc[VINTR] = _POSIX_VDISABLE;
tio.c_cc[VQUIT] = _POSIX_VDISABLE;
tio.c_cc[VERASE] = _POSIX_VDISABLE;
tio.c_cc[VKILL] = _POSIX_VDISABLE;
tio.c_cc[VEOF] = _POSIX_VDISABLE;
tio.c_cc[VTIME] = 0;
tio.c_cc[VMIN] = 1;
#ifdef VSWTC
tio.c_cc[VSWTC] = _POSIX_VDISABLE;
#endif
tio.c_cc[VSTART] = _POSIX_VDISABLE;
tio.c_cc[VSTOP] = _POSIX_VDISABLE;
tio.c_cc[VSUSP] = _POSIX_VDISABLE;
#ifdef VEOL
tio.c_cc[VEOL] = _POSIX_VDISABLE;
#endif
#ifdef VREPRINT
tio.c_cc[VREPRINT] = _POSIX_VDISABLE;
#endif
#ifdef VDISCARD
tio.c_cc[VDISCARD] = _POSIX_VDISABLE;
#endif
#ifdef VWERASE
tio.c_cc[VWERASE] = _POSIX_VDISABLE;
#endif
#ifdef VLNEXT
tio.c_cc[VLNEXT] = _POSIX_VDISABLE;
#endif
#ifdef VEOL2
tio.c_cc[VEOL2] = _POSIX_VDISABLE;
#endif
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());
}
EncodedFrame encode_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) {
const uint16_t words[kFrameWordCount] = {word0, word1, word2, word3};
EncodedFrame frame {};
std::memcpy(frame.data(), words, sizeof(words));
return frame;
}
} // namespace
struct TtyProtocolWriter::Impl {
explicit Impl(std::size_t ring_capacity_bytes)
: capacity_frames(std::max<std::size_t>(1U, ring_capacity_bytes / kFrameByteCount)),
ring(capacity_frames) {}
int fd = -1;
int slave_fd = -1;
std::string slave_path;
bool owns_link = false;
const std::size_t capacity_frames;
std::vector<EncodedFrame> ring;
std::size_t head = 0;
std::size_t size = 0;
mutable std::mutex mutex;
std::condition_variable data_ready_cv;
std::thread worker;
bool stop_requested = false;
std::exception_ptr failure;
StatsSnapshot stats;
};
TtyProtocolWriter::TtyProtocolWriter(std::string path, std::size_t ring_capacity_bytes)
: path_(std::move(path)),
impl_(std::make_unique<Impl>(ring_capacity_bytes)) {
if (is_character_device_path(path_)) {
impl_->fd = ::open(path_.c_str(), O_WRONLY | O_NOCTTY);
if (impl_->fd < 0) {
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_));
}
} else {
std::array<char, PATH_MAX> slave_name {};
if (::openpty(&impl_->fd, &impl_->slave_fd, slave_name.data(), nullptr, nullptr) != 0) {
throw std::runtime_error(io_error("Cannot create PTY bridge for", path_));
}
try {
impl_->slave_path = slave_name.data();
set_fd_raw(impl_->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(impl_->slave_path.c_str(), path_.c_str()) != 0) {
throw std::runtime_error(io_error("Cannot create tty symlink", path_));
}
impl_->owns_link = true;
} catch (...) {
close_fd_if_open(impl_->slave_fd);
close_fd_if_open(impl_->fd);
throw;
}
}
impl_->worker = std::thread([this]() { worker_loop(); });
}
TtyProtocolWriter::~TtyProtocolWriter() {
try {
shutdown();
} catch (...) {
}
if (!impl_) {
return;
}
if (impl_->owns_link && !path_.empty()) {
try {
const auto target = read_link_target(path_);
if (target && (*target == impl_->slave_path)) {
::unlink(path_.c_str());
}
} catch (...) {
}
impl_->owns_link = false;
}
close_fd_if_open(impl_->slave_fd);
close_fd_if_open(impl_->fd);
close_fd();
}
void TtyProtocolWriter::emit_packet_start(uint16_t marker) {
enqueue_frame(marker, 0xFFFF, 0xFFFF, 0xFFFF);
TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept
: path_(std::move(other.path_)),
fd_(other.fd_) {
other.fd_ = -1;
}
void TtyProtocolWriter::emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) {
enqueue_frame(0x000A,
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));
}
void TtyProtocolWriter::enqueue_encoded_frames(const uint16_t* words, std::size_t frame_count) {
if ((frame_count == 0U) || (words == nullptr)) {
return;
}
throw_if_failed();
std::lock_guard<std::mutex> lock(impl_->mutex);
if (impl_->failure) {
std::rethrow_exception(impl_->failure);
}
if (impl_->stop_requested) {
throw std::runtime_error("tty writer is already shut down");
}
std::size_t start_frame = 0;
std::size_t frames_to_copy = frame_count;
std::size_t dropped_frames = 0;
if (frame_count >= impl_->capacity_frames) {
start_frame = frame_count - impl_->capacity_frames;
frames_to_copy = impl_->capacity_frames;
dropped_frames = impl_->size + start_frame;
impl_->head = 0;
impl_->size = 0;
} else {
const std::size_t available_frames = impl_->capacity_frames - impl_->size;
if (frame_count > available_frames) {
dropped_frames = frame_count - available_frames;
impl_->head = (impl_->head + dropped_frames) % impl_->capacity_frames;
impl_->size -= dropped_frames;
}
}
if (dropped_frames != 0U) {
impl_->stats.frames_dropped += static_cast<std::uint64_t>(dropped_frames);
++impl_->stats.ring_overflows;
}
for (std::size_t i = 0; i < frames_to_copy; ++i) {
const std::size_t src_index = (start_frame + i) * kFrameWordCount;
const std::size_t dst_index = (impl_->head + impl_->size) % impl_->capacity_frames;
impl_->ring[dst_index] = encode_frame(words[src_index + 0U],
words[src_index + 1U],
words[src_index + 2U],
words[src_index + 3U]);
++impl_->size;
}
impl_->data_ready_cv.notify_one();
}
TtyProtocolWriter::StatsSnapshot TtyProtocolWriter::stats() const {
if (!impl_) {
return {};
}
std::lock_guard<std::mutex> lock(impl_->mutex);
return impl_->stats;
}
const std::string& TtyProtocolWriter::path() const {
return path_;
}
void TtyProtocolWriter::throw_if_failed() const {
if (!impl_) {
return;
}
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);
std::exception_ptr failure;
{
std::lock_guard<std::mutex> lock(impl_->mutex);
failure = impl_->failure;
}
if (failure) {
std::rethrow_exception(failure);
}
}
void TtyProtocolWriter::shutdown() {
if (!impl_) {
return;
}
{
std::lock_guard<std::mutex> lock(impl_->mutex);
impl_->stop_requested = true;
}
close_fd_if_open(impl_->fd);
impl_->data_ready_cv.notify_all();
if (impl_->worker.joinable()) {
impl_->worker.join();
}
}
void TtyProtocolWriter::enqueue_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) {
const uint16_t words[kFrameWordCount] = {word0, word1, word2, word3};
enqueue_encoded_frames(words, 1U);
}
void TtyProtocolWriter::worker_loop() {
for (;;) {
EncodedFrame frame {};
{
std::unique_lock<std::mutex> lock(impl_->mutex);
impl_->data_ready_cv.wait(lock, [this]() {
return impl_->stop_requested || impl_->failure || (impl_->size != 0U);
});
if (impl_->failure || impl_->stop_requested) {
return;
}
frame = impl_->ring[impl_->head];
impl_->head = (impl_->head + 1U) % impl_->capacity_frames;
--impl_->size;
}
const std::uint8_t* bytes = frame.data();
std::size_t remaining = frame.size();
while (remaining != 0U) {
const ssize_t written = ::write(impl_->fd, bytes, remaining);
const ssize_t written = ::write(fd_, bytes, remaining);
if (written < 0) {
if (errno == EINTR) {
continue;
}
if ((errno == EAGAIN) || (errno == EWOULDBLOCK) || (errno == EIO)) {
{
std::lock_guard<std::mutex> lock(impl_->mutex);
if (impl_->stop_requested) {
return;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(5));
continue;
}
std::lock_guard<std::mutex> lock(impl_->mutex);
if (!impl_->stop_requested) {
impl_->failure = std::make_exception_ptr(
std::runtime_error(io_error("Cannot write tty frame to", path_)));
}
impl_->data_ready_cv.notify_all();
return;
throw std::runtime_error(io_error("Cannot write tty frame to", path_));
}
if (written == 0) {
std::lock_guard<std::mutex> lock(impl_->mutex);
if (!impl_->stop_requested) {
impl_->failure = std::make_exception_ptr(
std::runtime_error("tty write returned 0 bytes for '" + path_ + "'"));
throw std::runtime_error("tty write returned 0 bytes for '" + path_ + "'");
}
impl_->data_ready_cv.notify_all();
return;
}
bytes += static_cast<std::size_t>(written);
remaining -= static_cast<std::size_t>(written);
}
}
{
std::lock_guard<std::mutex> lock(impl_->mutex);
++impl_->stats.frames_written;
}
void TtyProtocolWriter::close_fd() noexcept {
if (fd_ >= 0) {
::close(fd_);
fd_ = -1;
}
}

View File

@ -1,41 +1,29 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
class TtyProtocolWriter {
public:
struct StatsSnapshot {
std::uint64_t frames_written = 0;
std::uint64_t frames_dropped = 0;
std::uint64_t ring_overflows = 0;
};
TtyProtocolWriter(std::string path, std::size_t ring_capacity_bytes);
explicit TtyProtocolWriter(std::string path);
~TtyProtocolWriter();
TtyProtocolWriter(const TtyProtocolWriter&) = delete;
TtyProtocolWriter& operator=(const TtyProtocolWriter&) = delete;
TtyProtocolWriter(TtyProtocolWriter&& other) noexcept = delete;
TtyProtocolWriter& operator=(TtyProtocolWriter&& other) noexcept = delete;
TtyProtocolWriter(TtyProtocolWriter&& other) noexcept;
TtyProtocolWriter& operator=(TtyProtocolWriter&& other) noexcept;
void emit_packet_start(uint16_t marker = 0x000A);
void emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg);
void enqueue_encoded_frames(const uint16_t* words, std::size_t frame_count);
StatsSnapshot stats() const;
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;
void throw_if_failed() const;
void shutdown();
private:
void enqueue_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3);
void worker_loop();
void write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const;
void close_fd() noexcept;
std::string path_;
struct Impl;
std::unique_ptr<Impl> impl_;
#ifndef _WIN32
int fd_ = -1;
#endif
};