diff --git a/adc_pin_probe.cpp b/adc_pin_probe.cpp new file mode 100644 index 0000000..4e62d08 --- /dev/null +++ b/adc_pin_probe.cpp @@ -0,0 +1,779 @@ +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include +#else + #include + #include +#endif + +namespace { + +struct Config { + std::string serial; + std::optional 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 exclude_phy = {2, 3, 18, 19}; + + 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::max())) { + fail("Invalid integer for " + field_name + ": " + text); + } + return static_cast(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 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 parse_exclude_phy(const std::string& text) { + const std::string clean = trim_copy(text); + if (clean.empty() || (clean == "none")) { + return {}; + } + + std::vector 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:2,3,18,19 -> excludes X3,Y3,X4,Y4 from current diff setup\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& 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 +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(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(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_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(x502_module, "X502_Create"); + Free = load_symbol(x502_module, "X502_Free"); + Close = load_symbol(x502_module, "X502_Close"); + GetErrorString = load_symbol(x502_module, "X502_GetErrorString"); + GetDevInfo2 = load_symbol(x502_module, "X502_GetDevInfo2"); + SetMode = load_symbol(x502_module, "X502_SetMode"); + StreamsStop = load_symbol(x502_module, "X502_StreamsStop"); + StreamsDisable = load_symbol(x502_module, "X502_StreamsDisable"); + SetSyncMode = load_symbol(x502_module, "X502_SetSyncMode"); + SetSyncStartMode = load_symbol(x502_module, "X502_SetSyncStartMode"); + SetRefFreq = load_symbol(x502_module, "X502_SetRefFreq"); + SetLChannelCount = load_symbol(x502_module, "X502_SetLChannelCount"); + SetLChannel = load_symbol(x502_module, "X502_SetLChannel"); + SetAdcFreq = load_symbol(x502_module, "X502_SetAdcFreq"); + Configure = load_symbol(x502_module, "X502_Configure"); + StreamsEnable = load_symbol(x502_module, "X502_StreamsEnable"); + StreamsStart = load_symbol(x502_module, "X502_StreamsStart"); + Recv = load_symbol(x502_module, "X502_Recv"); + ProcessData = load_symbol(x502_module, "X502_ProcessData"); + + OpenUsb = load_symbol(e502_module, "E502_OpenUsb"); + OpenByIpAddr = load_symbol(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(GetTickCount64()); +#else + using namespace std::chrono; + return static_cast( + duration_cast(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::infinity(); + double max = -std::numeric_limits::infinity(); + + void add(double value) { + ++count; + const double delta = value - mean; + mean += delta / static_cast(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 raw(cfg.recv_block_words); + std::vector adc(cfg.recv_block_words); + RunningStats stats; + + const uint64_t capture_until = tick_count_ms() + static_cast(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(adc.size()); + expect_ok(api, + api.ProcessData(device.hnd, + raw.data(), + static_cast(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(stats.count)); + const double variance = + (stats.count > 1U) ? (stats.m2 / static_cast(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(info.fpga_ver >> 8) << "." + << static_cast(info.fpga_ver & 0xFF) << "\n"; +} + +void print_ranked_table(const std::vector& 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{}; + err = api.GetDevInfo2(device.hnd, &info, sizeof(info)); + expect_ok(api, err, "Get device info"); + print_device_info(info); + + std::array 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 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 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(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; + } +}