Compare commits
8 Commits
main
...
codex/di1-
| Author | SHA1 | Date | |
|---|---|---|---|
| f1097055ed | |||
| 392709127f | |||
| 25ac280617 | |||
| 53c23bfdf4 | |||
| dfbbba055e | |||
| 9a0d87007d | |||
| 634ba6cc8e | |||
| 545b2f365f |
22
.gitignore
vendored
22
.gitignore
vendored
@ -1,16 +1,14 @@
|
|||||||
# Build outputs
|
# Build artifacts
|
||||||
capture
|
|
||||||
*.exe
|
*.exe
|
||||||
*.obj
|
*.obj
|
||||||
|
*.pdb
|
||||||
|
*.ilk
|
||||||
|
*.idb
|
||||||
|
|
||||||
# Large capture spools and generated data dumps
|
# Generated capture outputs
|
||||||
*.capture_spool.bin
|
*.capture_spool.bin
|
||||||
capture.csv
|
*.csv
|
||||||
capture.svg
|
*.svg
|
||||||
packets.csv
|
*.json
|
||||||
packets.svg
|
*.html
|
||||||
test.csv
|
live_*.js
|
||||||
test.svg
|
|
||||||
internal_default.csv
|
|
||||||
internal_default.svg
|
|
||||||
*_test.*
|
|
||||||
|
|||||||
20
Makefile
Normal file
20
Makefile
Normal 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
806
adc_pin_probe.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
|
||||||
@ -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"
|
|
||||||
@ -15,6 +15,9 @@
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr std::size_t kLivePlotMaxColumns = 800;
|
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 kCsvSpoolMagic = 0x4C564353U; // "SVCL"
|
||||||
constexpr uint32_t kCsvSpoolVersion = 2U;
|
constexpr uint32_t kCsvSpoolVersion = 2U;
|
||||||
|
|
||||||
@ -37,6 +40,14 @@ struct PlotPoint {
|
|||||||
double value = 0.0;
|
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) {
|
std::size_t packet_channel_count(const CapturePacket& packet) {
|
||||||
return (packet.channel_count <= 1U) ? 1U : 2U;
|
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();
|
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 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();
|
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)) {
|
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;
|
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> build_digital_step_trace(const std::vector<uint8_t>& data) {
|
||||||
std::vector<PlotPoint> result;
|
std::vector<PlotPoint> result;
|
||||||
if (data.empty()) {
|
if (data.empty()) {
|
||||||
@ -184,6 +279,24 @@ std::string json_points(const std::vector<PlotPoint>& trace, double frame_freq_h
|
|||||||
return out.str();
|
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);
|
[[noreturn]] void fail_write(const std::string& message);
|
||||||
|
|
||||||
std::string replace_extension(const std::string& path, const std::string& new_extension) {
|
std::string replace_extension(const std::string& path, const std::string& new_extension) {
|
||||||
@ -296,22 +409,25 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
|||||||
<< " <span id=\"updateInfo\">Auto-refresh every 500 ms</span>\n"
|
<< " <span id=\"updateInfo\">Auto-refresh every 500 ms</span>\n"
|
||||||
<< " </div>\n"
|
<< " </div>\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"
|
<< " <div class=\"controls\">\n"
|
||||||
<< " <button type=\"button\" id=\"zoomXIn\">X+</button>\n"
|
<< " <button type=\"button\" id=\"zoomXIn\">X+</button>\n"
|
||||||
<< " <button type=\"button\" id=\"zoomXOut\">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=\"zoomYIn\">Y+</button>\n"
|
||||||
<< " <button type=\"button\" id=\"zoomYOut\">Y-</button>\n"
|
<< " <button type=\"button\" id=\"zoomYOut\">Y-</button>\n"
|
||||||
<< " <button type=\"button\" id=\"resetZoom\">Reset</button>\n"
|
<< " <button type=\"button\" id=\"resetZoom\">Reset</button>\n"
|
||||||
<< " <label><input type=\"checkbox\" id=\"autoZoom\" checked/>Auto view</label>\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"
|
<< " </div>\n"
|
||||||
<< " <canvas id=\"plot\" width=\"1200\" height=\"620\"></canvas>\n"
|
<< " <canvas id=\"plot\" width=\"1200\" height=\"620\"></canvas>\n"
|
||||||
<< " <div class=\"legend\">\n"
|
<< " <div class=\"legend\">\n"
|
||||||
<< " <span id=\"legendCh1\"><span class=\"sw\" style=\"background:#005bbb\"></span>CH1</span>\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=\"legendCh2\"><span class=\"sw\" style=\"background:#d62828\"></span>CH2</span>\n"
|
||||||
<< " <span id=\"legendDi1\"><span class=\"sw\" style=\"background:#1b8f3a\"></span>DI1</span>\n"
|
<< " <span id=\"legendRss\"><span class=\"sw\" style=\"background:#ee8a12\"></span>sqrt(CH1^2 + CH2^2)</span>\n"
|
||||||
<< " </div>\n"
|
<< " <span id=\"legendDi1\"><span class=\"sw\" style=\"background:#1b8f3a\"></span>DI1</span>\n"
|
||||||
|
<< " </div>\n"
|
||||||
<< " </div>\n"
|
<< " </div>\n"
|
||||||
<< " </div>\n"
|
<< " </div>\n"
|
||||||
<< " <script>\n"
|
<< " <script>\n"
|
||||||
@ -323,12 +439,15 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
|||||||
<< " const timingInfo = document.getElementById('timingInfo');\n"
|
<< " const timingInfo = document.getElementById('timingInfo');\n"
|
||||||
<< " const zeroInfo = document.getElementById('zeroInfo');\n"
|
<< " const zeroInfo = document.getElementById('zeroInfo');\n"
|
||||||
<< " const zoomInfo = document.getElementById('zoomInfo');\n"
|
<< " const zoomInfo = document.getElementById('zoomInfo');\n"
|
||||||
<< " const packetRateInfo = document.getElementById('packetRateInfo');\n"
|
<< " const packetRateInfo = document.getElementById('packetRateInfo');\n"
|
||||||
<< " const updateInfo = document.getElementById('updateInfo');\n"
|
<< " const updateInfo = document.getElementById('updateInfo');\n"
|
||||||
<< " const legendCh2 = document.getElementById('legendCh2');\n"
|
<< " const legendCh2 = document.getElementById('legendCh2');\n"
|
||||||
<< " const legendDi1 = document.getElementById('legendDi1');\n"
|
<< " const legendRss = document.getElementById('legendRss');\n"
|
||||||
|
<< " const legendDi1 = document.getElementById('legendDi1');\n"
|
||||||
<< " const zoomXIn = document.getElementById('zoomXIn');\n"
|
<< " const zoomXIn = document.getElementById('zoomXIn');\n"
|
||||||
<< " const zoomXOut = document.getElementById('zoomXOut');\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 zoomYIn = document.getElementById('zoomYIn');\n"
|
||||||
<< " const zoomYOut = document.getElementById('zoomYOut');\n"
|
<< " const zoomYOut = document.getElementById('zoomYOut');\n"
|
||||||
<< " const resetZoom = document.getElementById('resetZoom');\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"
|
<< " refreshAutoCheckbox();\n"
|
||||||
<< " renderPacket(latestPacket);\n"
|
<< " renderPacket(latestPacket);\n"
|
||||||
<< " }\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"
|
<< " function resetView() {\n"
|
||||||
<< " viewState = defaultViewState();\n"
|
<< " viewState = defaultViewState();\n"
|
||||||
<< " saveViewState();\n"
|
<< " saveViewState();\n"
|
||||||
@ -476,14 +619,41 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
|||||||
<< " });\n"
|
<< " });\n"
|
||||||
<< " ctx.strokeStyle = '#1b8f3a'; ctx.lineWidth = 1.4; ctx.stroke();\n"
|
<< " ctx.strokeStyle = '#1b8f3a'; ctx.lineWidth = 1.4; ctx.stroke();\n"
|
||||||
<< " }\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"
|
<< " function renderWaiting(message) {\n"
|
||||||
<< " packetInfo.textContent = 'Waiting for packets...';\n"
|
<< " packetInfo.textContent = 'Waiting for packets...';\n"
|
||||||
<< " timingInfo.textContent = '';\n"
|
<< " timingInfo.textContent = '';\n"
|
||||||
<< " zeroInfo.textContent = '';\n"
|
<< " zeroInfo.textContent = '';\n"
|
||||||
<< " zoomInfo.textContent = 'View: auto';\n"
|
<< " zoomInfo.textContent = 'View: auto';\n"
|
||||||
<< " packetRateInfo.textContent = '';\n"
|
<< " packetRateInfo.textContent = '';\n"
|
||||||
<< " legendCh2.style.display = '';\n"
|
<< " legendCh2.style.display = '';\n"
|
||||||
<< " legendDi1.style.display = 'none';\n"
|
<< " legendRss.style.display = 'none';\n"
|
||||||
|
<< " legendDi1.style.display = 'none';\n"
|
||||||
<< " refreshAutoCheckbox();\n"
|
<< " refreshAutoCheckbox();\n"
|
||||||
<< " updateInfo.textContent = 'Polling every 500 ms';\n"
|
<< " updateInfo.textContent = 'Polling every 500 ms';\n"
|
||||||
<< " statusText.textContent = message;\n"
|
<< " statusText.textContent = message;\n"
|
||||||
@ -493,14 +663,31 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
|||||||
<< " }\n"
|
<< " }\n"
|
||||||
<< " function renderPacket(data) {\n"
|
<< " function renderPacket(data) {\n"
|
||||||
<< " latestPacket = data;\n"
|
<< " latestPacket = data;\n"
|
||||||
<< " const ch1 = data.ch1 || [];\n"
|
<< " const ch1 = data.ch1 || [];\n"
|
||||||
<< " const ch2 = data.ch2 || [];\n"
|
<< " const ch1Mid = data.ch1Mid || [];\n"
|
||||||
<< " const di1 = data.di1 || [];\n"
|
<< " const ch1High = data.ch1High || [];\n"
|
||||||
<< " const hasCh2 = (data.channelCount || 2) > 1 && ch2.length > 0;\n"
|
<< " const ch2 = data.ch2 || [];\n"
|
||||||
<< " const hasDi1Trace = !!data.hasDi1Trace && di1.length > 0;\n"
|
<< " const ch2Mid = data.ch2Mid || [];\n"
|
||||||
<< " const values = [];\n"
|
<< " const ch2High = data.ch2High || [];\n"
|
||||||
<< " ch1.forEach(p => values.push(p[1]));\n"
|
<< " const rss = data.rss || [];\n"
|
||||||
<< " if (hasCh2) ch2.forEach(p => values.push(p[1]));\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"
|
||||||
|
<< " 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 minY = Math.min(...values);\n"
|
||||||
<< " let maxY = Math.max(...values);\n"
|
<< " let maxY = Math.max(...values);\n"
|
||||||
<< " if (!Number.isFinite(minY) || !Number.isFinite(maxY) || minY === maxY) {\n"
|
<< " if (!Number.isFinite(minY) || !Number.isFinite(maxY) || minY === maxY) {\n"
|
||||||
@ -509,25 +696,33 @@ void write_live_html_document(const std::string& path, const std::string& data_s
|
|||||||
<< " const pad = Math.max(0.05, (maxY - minY) * 0.08);\n"
|
<< " const pad = Math.max(0.05, (maxY - minY) * 0.08);\n"
|
||||||
<< " minY -= pad; maxY += pad;\n"
|
<< " minY -= pad; maxY += pad;\n"
|
||||||
<< " }\n"
|
<< " }\n"
|
||||||
<< " const maxT = Math.max(data.durationMs / 1000.0, 1e-9);\n"
|
<< " const maxT = Math.max(data.durationMs / 1000.0, 1e-9);\n"
|
||||||
<< " latestAutoBounds = { minY, maxY, maxT };\n"
|
<< " latestAutoBounds = { minY, maxY, maxT };\n"
|
||||||
<< " const view = currentViewBounds(latestAutoBounds);\n"
|
<< " const view = currentViewBounds(latestAutoBounds);\n"
|
||||||
<< " const box = drawAxes(view.yMin, view.yMax, view.xMin, view.xMax);\n"
|
<< " const plotCh1 = di1Grouped ? ch1Grouped : pickTrace(ch1, ch1Mid, ch1High, maxT, view);\n"
|
||||||
<< " drawTrace(ch1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);\n"
|
<< " const plotCh2 = hasCh2 ? (di1Grouped ? ch2Grouped : pickTrace(ch2, ch2Mid, ch2High, maxT, view)) : [];\n"
|
||||||
<< " if (hasCh2) drawTrace(ch2, '#d62828', box, view.yMin, view.yMax, view.xMin, view.xMax);\n"
|
<< " const plotRss = hasRss ? (di1Grouped ? rssGrouped : pickTrace(rss, rssMid, rssHigh, maxT, view)) : [];\n"
|
||||||
<< " if (hasDi1Trace) drawDigitalTrace(di1, box, view.xMin, view.xMax);\n"
|
<< " const box = drawAxes(view.yMin, view.yMax, view.xMin, view.xMax);\n"
|
||||||
<< " legendCh2.style.display = hasCh2 ? '' : 'none';\n"
|
<< " drawPacketMarkers(packetMarkers, box, view.xMin, view.xMax);\n"
|
||||||
<< " legendDi1.style.display = hasDi1Trace ? '' : 'none';\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"
|
<< " refreshAutoCheckbox();\n"
|
||||||
<< " packetInfo.textContent = 'Packet #' + data.packetIndex + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);\n"
|
<< " const firstPacket = data.firstPacketIndex || data.packetIndex;\n"
|
||||||
<< " timingInfo.textContent = 'Frames/ch: ' + data.framesPerChannel + ', duration: ' + data.durationMs.toFixed(3) + ' ms';\n"
|
<< " const lastPacket = data.lastPacketIndex || data.packetIndex;\n"
|
||||||
<< " packetRateInfo.textContent = 'Packets/s: ' + data.packetsPerSecond.toFixed(3);\n"
|
<< " packetInfo.textContent = 'Packets #' + firstPacket + '..' + lastPacket + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);\n"
|
||||||
<< " zeroInfo.textContent = hasDi1Trace\n"
|
<< " timingInfo.textContent = 'Window frames/ch: ' + data.framesPerChannel + ', window: ' + data.durationMs.toFixed(3) + ' ms, last packet: ' + data.latestPacketDurationMs.toFixed(3) + ' ms';\n"
|
||||||
<< " ? ('DI1 trace: ' + data.di1Frames + ' frame samples')\n"
|
<< " packetRateInfo.textContent = 'Packets/s: ' + data.packetsPerSecond.toFixed(3);\n"
|
||||||
<< " : ('Zeroed on DI1 change: ' + data.zeroedPercent.toFixed(3) + '% (' + data.zeroedSamples + '/' + data.storedSamples + ')');\n"
|
<< " zeroInfo.textContent = hasDi1Trace\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"
|
<< " ? ('DI1 trace: ' + data.di1Frames + ' frame samples')\n"
|
||||||
<< " updateInfo.textContent = 'Snapshot: ' + data.updatedAt;\n"
|
<< " : ('Zeroed on DI1 change: ' + data.zeroedPercent.toFixed(3) + '% (' + data.zeroedSamples + '/' + data.storedSamples + ')');\n"
|
||||||
<< " statusText.textContent = 'Close reason: ' + data.closeReason + '. This page refreshes its data every 500 ms.';\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 + (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"
|
<< " }\n"
|
||||||
<< " function applyLiveData(data) {\n"
|
<< " function applyLiveData(data) {\n"
|
||||||
<< " if (!data || data.status === 'waiting') {\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"
|
<< " }\n"
|
||||||
<< " zoomXIn.addEventListener('click', () => applyZoomX(0.5));\n"
|
<< " zoomXIn.addEventListener('click', () => applyZoomX(0.5));\n"
|
||||||
<< " zoomXOut.addEventListener('click', () => applyZoomX(2.0));\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"
|
<< " zoomYIn.addEventListener('click', () => applyZoomY(0.5));\n"
|
||||||
<< " zoomYOut.addEventListener('click', () => applyZoomY(2.0));\n"
|
<< " zoomYOut.addEventListener('click', () => applyZoomY(2.0));\n"
|
||||||
<< " resetZoom.addEventListener('click', () => resetView());\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"
|
<< " }\n"
|
||||||
<< " }, { passive: false });\n"
|
<< " }, { passive: false });\n"
|
||||||
<< " canvas.addEventListener('dblclick', () => resetView());\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"
|
<< " renderWaiting('Waiting for first packet...');\n"
|
||||||
<< " loadLatestData();\n"
|
<< " loadLatestData();\n"
|
||||||
<< " window.setInterval(loadLatestData, 500);\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,
|
CaptureFileWriter::CaptureFileWriter(std::string csv_path,
|
||||||
std::string svg_path,
|
std::string svg_path,
|
||||||
std::string live_html_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)),
|
: csv_path_(std::move(csv_path)),
|
||||||
svg_path_(std::move(svg_path)),
|
svg_path_(std::move(svg_path)),
|
||||||
live_html_path_(std::move(live_html_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 {
|
const std::string& CaptureFileWriter::live_html_path() const {
|
||||||
return live_html_path_;
|
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,
|
std::size_t packets_seen,
|
||||||
double packets_per_second,
|
double packets_per_second,
|
||||||
double frame_freq_hz,
|
double frame_freq_hz,
|
||||||
const std::string& close_reason,
|
const std::string& close_reason,
|
||||||
std::size_t zeroed_samples,
|
std::size_t zeroed_samples,
|
||||||
std::size_t stored_samples) const {
|
std::size_t stored_samples) const {
|
||||||
const std::size_t frames = packet_frame_count(packet);
|
if (packets.empty()) {
|
||||||
const double duration_ms = (frames == 0U) ? 0.0 : (1000.0 * static_cast<double>(frames) / frame_freq_hz);
|
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)
|
const double zeroed_percent = (stored_samples == 0U)
|
||||||
? 0.0
|
? 0.0
|
||||||
: (100.0 * static_cast<double>(zeroed_samples) / static_cast<double>(stored_samples));
|
: (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);
|
std::size_t total_frames = 0;
|
||||||
const auto di1_trace = build_digital_step_trace(packet.di1);
|
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;
|
std::ostringstream json;
|
||||||
json << std::fixed << std::setprecision(9);
|
json << std::fixed << std::setprecision(9);
|
||||||
json << "{\n"
|
json << "{\n"
|
||||||
<< " \"status\": \"packet\",\n"
|
<< " \"status\": \"packet\",\n"
|
||||||
<< " \"packetIndex\": " << packet.packet_index << ",\n"
|
<< " \"packetIndex\": " << latest_packet.packet_index << ",\n"
|
||||||
<< " \"channelCount\": " << packet_channel_count(packet) << ",\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"
|
<< " \"packetsSeen\": " << packets_seen << ",\n"
|
||||||
<< " \"packetsPerSecond\": " << packets_per_second << ",\n"
|
<< " \"packetsPerSecond\": " << packets_per_second << ",\n"
|
||||||
<< " \"framesPerChannel\": " << frames << ",\n"
|
<< " \"framesPerChannel\": " << total_frames << ",\n"
|
||||||
|
<< " \"latestPacketFrames\": " << latest_frames << ",\n"
|
||||||
<< " \"frameFreqHz\": " << frame_freq_hz << ",\n"
|
<< " \"frameFreqHz\": " << frame_freq_hz << ",\n"
|
||||||
<< " \"durationMs\": " << duration_ms << ",\n"
|
<< " \"durationMs\": " << duration_ms << ",\n"
|
||||||
|
<< " \"latestPacketDurationMs\": " << latest_duration_ms << ",\n"
|
||||||
<< " \"closeReason\": \"" << close_reason << "\",\n"
|
<< " \"closeReason\": \"" << close_reason << "\",\n"
|
||||||
<< " \"zeroedSamples\": " << zeroed_samples << ",\n"
|
<< " \"zeroedSamples\": " << zeroed_samples << ",\n"
|
||||||
<< " \"storedSamples\": " << stored_samples << ",\n"
|
<< " \"storedSamples\": " << stored_samples << ",\n"
|
||||||
<< " \"zeroedPercent\": " << zeroed_percent << ",\n"
|
<< " \"zeroedPercent\": " << zeroed_percent << ",\n"
|
||||||
<< " \"hasDi1Trace\": " << (packet_has_di1_trace(packet) ? "true" : "false") << ",\n"
|
<< " \"fullResLive\": " << (full_res_live_ ? "true" : "false") << ",\n"
|
||||||
<< " \"di1Frames\": " << packet.di1.size() << ",\n"
|
<< " \"di1Grouped\": " << (di1_grouped ? "true" : "false") << ",\n"
|
||||||
<< " \"updatedAt\": \"packet " << packet.packet_index << "\",\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"
|
<< " \"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"
|
<< " \"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";
|
<< "}\n";
|
||||||
|
|
||||||
const std::string json_text = json.str();
|
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) {
|
if (!header_written) {
|
||||||
csv << "packet_index,frame_index,global_frame_index,time_s,packet_time_s,ch1_v";
|
csv << "packet_index,frame_index,global_frame_index,time_s,packet_time_s,ch1_v";
|
||||||
if (channel_count >= 2U) {
|
if (channel_count >= 2U) {
|
||||||
csv << ",ch2_v";
|
csv << ",ch2_v,rss_v";
|
||||||
}
|
}
|
||||||
if (has_di1_trace || (file_header.has_di1_trace != 0U)) {
|
if (has_di1_trace || (file_header.has_di1_trace != 0U)) {
|
||||||
csv << ",di1";
|
csv << ",di1";
|
||||||
@ -816,7 +1128,8 @@ void CaptureFileWriter::finalize_csv_from_spool(double frame_freq_hz) const {
|
|||||||
out << packet_index << "," << i << "," << global_frame_index << ","
|
out << packet_index << "," << i << "," << global_frame_index << ","
|
||||||
<< time_s << "," << packet_time_s << "," << ch1[i];
|
<< time_s << "," << packet_time_s << "," << ch1[i];
|
||||||
if (channel_count >= 2U) {
|
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)) {
|
if (has_di1_trace || (file_header.has_di1_trace != 0U)) {
|
||||||
out << "," << (has_di1_trace ? static_cast<unsigned>(di1[i]) : 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) {
|
if (!header_written) {
|
||||||
csv << "packet_index,frame_index,global_frame_index,time_s,packet_time_s,ch1_v";
|
csv << "packet_index,frame_index,global_frame_index,time_s,packet_time_s,ch1_v";
|
||||||
if (file_header.channel_count >= 2U) {
|
if (file_header.channel_count >= 2U) {
|
||||||
csv << ",ch2_v";
|
csv << ",ch2_v,rss_v";
|
||||||
}
|
}
|
||||||
if (file_header.has_di1_trace != 0U) {
|
if (file_header.has_di1_trace != 0U) {
|
||||||
csv << ",di1";
|
csv << ",di1";
|
||||||
@ -866,6 +1179,9 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
|
|||||||
if (packet_has_ch2(packet)) {
|
if (packet_has_ch2(packet)) {
|
||||||
min_y = std::min(min_y, packet.ch2[i]);
|
min_y = std::min(min_y, packet.ch2[i]);
|
||||||
max_y = std::max(max_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 trace1 = build_min_max_trace(packet.ch1, 1200);
|
||||||
const auto trace2 = build_min_max_trace(packet.ch2, 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 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=\""
|
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";
|
<< "\"/>\n";
|
||||||
if (packet_has_ch2(packet)) {
|
if (packet_has_ch2(packet)) {
|
||||||
file << " <polyline fill=\"none\" stroke=\"#d62828\" stroke-width=\"1.2\" points=\""
|
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";
|
<< "\"/>\n";
|
||||||
}
|
}
|
||||||
if (packet_has_di1_trace(packet)) {
|
if (packet_has_di1_trace(packet)) {
|
||||||
@ -974,6 +1302,9 @@ void CaptureFileWriter::write_svg(const std::vector<CapturePacket>& packets,
|
|||||||
if (has_di1_trace) {
|
if (has_di1_trace) {
|
||||||
file << ", DI1 trace";
|
file << ", DI1 trace";
|
||||||
}
|
}
|
||||||
|
if (di1_group_average_ && has_di1_trace) {
|
||||||
|
file << ", averaged by DI1 runs";
|
||||||
|
}
|
||||||
file << "</text>\n";
|
file << "</text>\n";
|
||||||
file << " <text x=\"" << left << "\" y=\"" << (height - 22)
|
file << " <text x=\"" << left << "\" y=\"" << (height - 22)
|
||||||
<< "\" font-size=\"16\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#425466\">time, s</text>\n";
|
<< "\" 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";
|
<< "\" stroke=\"#d62828\" stroke-width=\"3\"/>\n";
|
||||||
file << " <text x=\"" << (width - 110) << "\" y=\"" << (legend_y + 4)
|
file << " <text x=\"" << (width - 110) << "\" y=\"" << (legend_y + 4)
|
||||||
<< "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">CH2</text>\n";
|
<< "\" 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) {
|
if (has_di1_trace) {
|
||||||
file << " <line x1=\"" << (width - 80) << "\" y1=\"" << legend_y
|
file << " <line x1=\"" << (width - 80) << "\" y1=\"" << legend_y
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@ -19,7 +20,10 @@ public:
|
|||||||
CaptureFileWriter(std::string csv_path,
|
CaptureFileWriter(std::string csv_path,
|
||||||
std::string svg_path,
|
std::string svg_path,
|
||||||
std::string live_html_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,
|
void write(const std::vector<CapturePacket>& packets,
|
||||||
double frame_freq_hz,
|
double frame_freq_hz,
|
||||||
@ -31,7 +35,7 @@ public:
|
|||||||
double frame_freq_hz,
|
double frame_freq_hz,
|
||||||
std::size_t& global_frame_index) const;
|
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,
|
std::size_t packets_seen,
|
||||||
double packets_per_second,
|
double packets_per_second,
|
||||||
double frame_freq_hz,
|
double frame_freq_hz,
|
||||||
@ -52,4 +56,7 @@ private:
|
|||||||
std::string svg_path_;
|
std::string svg_path_;
|
||||||
std::string live_html_path_;
|
std::string live_html_path_;
|
||||||
std::string live_json_path_;
|
std::string live_json_path_;
|
||||||
|
bool full_res_live_ = false;
|
||||||
|
std::size_t live_history_packets_ = 8;
|
||||||
|
bool di1_group_average_ = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
114
live_plot.html
114
live_plot.html
@ -42,20 +42,23 @@
|
|||||||
<span id="updateInfo">Auto-refresh every 500 ms</span>
|
<span id="updateInfo">Auto-refresh every 500 ms</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="controls">
|
||||||
<button type="button" id="zoomXIn">X+</button>
|
<button type="button" id="zoomXIn">X+</button>
|
||||||
<button type="button" id="zoomXOut">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="zoomYIn">Y+</button>
|
||||||
<button type="button" id="zoomYOut">Y-</button>
|
<button type="button" id="zoomYOut">Y-</button>
|
||||||
<button type="button" id="resetZoom">Reset</button>
|
<button type="button" id="resetZoom">Reset</button>
|
||||||
<label><input type="checkbox" id="autoZoom" checked/>Auto view</label>
|
<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>
|
</div>
|
||||||
<canvas id="plot" width="1200" height="620"></canvas>
|
<canvas id="plot" width="1200" height="620"></canvas>
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<span id="legendCh1"><span class="sw" style="background:#005bbb"></span>CH1</span>
|
<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="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>
|
<span id="legendDi1"><span class="sw" style="background:#1b8f3a"></span>DI1</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -72,9 +75,12 @@
|
|||||||
const packetRateInfo = document.getElementById('packetRateInfo');
|
const packetRateInfo = document.getElementById('packetRateInfo');
|
||||||
const updateInfo = document.getElementById('updateInfo');
|
const updateInfo = document.getElementById('updateInfo');
|
||||||
const legendCh2 = document.getElementById('legendCh2');
|
const legendCh2 = document.getElementById('legendCh2');
|
||||||
|
const legendRss = document.getElementById('legendRss');
|
||||||
const legendDi1 = document.getElementById('legendDi1');
|
const legendDi1 = document.getElementById('legendDi1');
|
||||||
const zoomXIn = document.getElementById('zoomXIn');
|
const zoomXIn = document.getElementById('zoomXIn');
|
||||||
const zoomXOut = document.getElementById('zoomXOut');
|
const zoomXOut = document.getElementById('zoomXOut');
|
||||||
|
const panLeft = document.getElementById('panLeft');
|
||||||
|
const panRight = document.getElementById('panRight');
|
||||||
const zoomYIn = document.getElementById('zoomYIn');
|
const zoomYIn = document.getElementById('zoomYIn');
|
||||||
const zoomYOut = document.getElementById('zoomYOut');
|
const zoomYOut = document.getElementById('zoomYOut');
|
||||||
const resetZoom = document.getElementById('resetZoom');
|
const resetZoom = document.getElementById('resetZoom');
|
||||||
@ -165,6 +171,30 @@
|
|||||||
refreshAutoCheckbox();
|
refreshAutoCheckbox();
|
||||||
renderPacket(latestPacket);
|
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() {
|
function resetView() {
|
||||||
viewState = defaultViewState();
|
viewState = defaultViewState();
|
||||||
saveViewState();
|
saveViewState();
|
||||||
@ -222,6 +252,32 @@
|
|||||||
});
|
});
|
||||||
ctx.strokeStyle = '#1b8f3a'; ctx.lineWidth = 1.4; ctx.stroke();
|
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) {
|
function renderWaiting(message) {
|
||||||
packetInfo.textContent = 'Waiting for packets...';
|
packetInfo.textContent = 'Waiting for packets...';
|
||||||
timingInfo.textContent = '';
|
timingInfo.textContent = '';
|
||||||
@ -229,6 +285,7 @@
|
|||||||
zoomInfo.textContent = 'View: auto';
|
zoomInfo.textContent = 'View: auto';
|
||||||
packetRateInfo.textContent = '';
|
packetRateInfo.textContent = '';
|
||||||
legendCh2.style.display = '';
|
legendCh2.style.display = '';
|
||||||
|
legendRss.style.display = 'none';
|
||||||
legendDi1.style.display = 'none';
|
legendDi1.style.display = 'none';
|
||||||
refreshAutoCheckbox();
|
refreshAutoCheckbox();
|
||||||
updateInfo.textContent = 'Polling every 500 ms';
|
updateInfo.textContent = 'Polling every 500 ms';
|
||||||
@ -240,13 +297,30 @@
|
|||||||
function renderPacket(data) {
|
function renderPacket(data) {
|
||||||
latestPacket = data;
|
latestPacket = data;
|
||||||
const ch1 = data.ch1 || [];
|
const ch1 = data.ch1 || [];
|
||||||
|
const ch1Mid = data.ch1Mid || [];
|
||||||
|
const ch1High = data.ch1High || [];
|
||||||
const ch2 = data.ch2 || [];
|
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 di1 = data.di1 || [];
|
||||||
|
const packetMarkers = data.packetMarkers || [];
|
||||||
|
const di1Grouped = !!data.di1Grouped;
|
||||||
const hasCh2 = (data.channelCount || 2) > 1 && ch2.length > 0;
|
const hasCh2 = (data.channelCount || 2) > 1 && ch2.length > 0;
|
||||||
|
const hasRss = hasCh2 && rss.length > 0;
|
||||||
const hasDi1Trace = !!data.hasDi1Trace && di1.length > 0;
|
const hasDi1Trace = !!data.hasDi1Trace && di1.length > 0;
|
||||||
const values = [];
|
const values = [];
|
||||||
ch1.forEach(p => values.push(p[1]));
|
const yCh1 = di1Grouped && ch1Grouped.length > 0 ? ch1Grouped : ch1;
|
||||||
if (hasCh2) ch2.forEach(p => values.push(p[1]));
|
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 minY = Math.min(...values);
|
||||||
let maxY = Math.max(...values);
|
let maxY = Math.max(...values);
|
||||||
if (!Number.isFinite(minY) || !Number.isFinite(maxY) || minY === maxY) {
|
if (!Number.isFinite(minY) || !Number.isFinite(maxY) || minY === maxY) {
|
||||||
@ -258,22 +332,30 @@
|
|||||||
const maxT = Math.max(data.durationMs / 1000.0, 1e-9);
|
const maxT = Math.max(data.durationMs / 1000.0, 1e-9);
|
||||||
latestAutoBounds = { minY, maxY, maxT };
|
latestAutoBounds = { minY, maxY, maxT };
|
||||||
const view = currentViewBounds(latestAutoBounds);
|
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);
|
const box = drawAxes(view.yMin, view.yMax, view.xMin, view.xMax);
|
||||||
drawTrace(ch1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);
|
drawPacketMarkers(packetMarkers, box, view.xMin, view.xMax);
|
||||||
if (hasCh2) drawTrace(ch2, '#d62828', box, view.yMin, view.yMax, 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);
|
if (hasDi1Trace) drawDigitalTrace(di1, box, view.xMin, view.xMax);
|
||||||
legendCh2.style.display = hasCh2 ? '' : 'none';
|
legendCh2.style.display = hasCh2 ? '' : 'none';
|
||||||
|
legendRss.style.display = hasRss ? '' : 'none';
|
||||||
legendDi1.style.display = hasDi1Trace ? '' : 'none';
|
legendDi1.style.display = hasDi1Trace ? '' : 'none';
|
||||||
refreshAutoCheckbox();
|
refreshAutoCheckbox();
|
||||||
packetInfo.textContent = 'Packet #' + data.packetIndex + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);
|
const firstPacket = data.firstPacketIndex || data.packetIndex;
|
||||||
timingInfo.textContent = 'Frames/ch: ' + data.framesPerChannel + ', duration: ' + data.durationMs.toFixed(3) + ' ms';
|
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);
|
packetRateInfo.textContent = 'Packets/s: ' + data.packetsPerSecond.toFixed(3);
|
||||||
zeroInfo.textContent = hasDi1Trace
|
zeroInfo.textContent = hasDi1Trace
|
||||||
? ('DI1 trace: ' + data.di1Frames + ' frame samples')
|
? ('DI1 trace: ' + data.di1Frames + ' frame samples')
|
||||||
: ('Zeroed on DI1 change: ' + data.zeroedPercent.toFixed(3) + '% (' + data.zeroedSamples + '/' + data.storedSamples + ')');
|
: ('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');
|
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;
|
updateInfo.textContent = 'Snapshot: ' + data.updatedAt + (data.fullResLive ? ' | adaptive live resolution on' : '') + (di1Grouped ? (' | DI1-group avg: ' + (data.groupedPointCount || 0) + ' pts') : '');
|
||||||
statusText.textContent = 'Close reason: ' + data.closeReason + '. This page refreshes its data every 500 ms.';
|
statusText.textContent = 'Latest packet close reason: ' + data.closeReason + '. This page refreshes its rolling packet window every 500 ms.';
|
||||||
}
|
}
|
||||||
function applyLiveData(data) {
|
function applyLiveData(data) {
|
||||||
if (!data || data.status === 'waiting') {
|
if (!data || data.status === 'waiting') {
|
||||||
@ -300,6 +382,8 @@
|
|||||||
}
|
}
|
||||||
zoomXIn.addEventListener('click', () => applyZoomX(0.5));
|
zoomXIn.addEventListener('click', () => applyZoomX(0.5));
|
||||||
zoomXOut.addEventListener('click', () => applyZoomX(2.0));
|
zoomXOut.addEventListener('click', () => applyZoomX(2.0));
|
||||||
|
panLeft.addEventListener('click', () => applyPanX(-1.0));
|
||||||
|
panRight.addEventListener('click', () => applyPanX(1.0));
|
||||||
zoomYIn.addEventListener('click', () => applyZoomY(0.5));
|
zoomYIn.addEventListener('click', () => applyZoomY(0.5));
|
||||||
zoomYOut.addEventListener('click', () => applyZoomY(2.0));
|
zoomYOut.addEventListener('click', () => applyZoomY(2.0));
|
||||||
resetZoom.addEventListener('click', () => resetView());
|
resetZoom.addEventListener('click', () => resetView());
|
||||||
@ -324,6 +408,16 @@
|
|||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
canvas.addEventListener('dblclick', () => resetView());
|
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...');
|
renderWaiting('Waiting for first packet...');
|
||||||
loadLatestData();
|
loadLatestData();
|
||||||
window.setInterval(loadLatestData, 500);
|
window.setInterval(loadLatestData, 500);
|
||||||
|
|||||||
41
live_plot.js
41
live_plot.js
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
run exmple.txt
Normal file
1
run exmple.txt
Normal 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
|
||||||
@ -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}" \
|
|
||||||
"$@"
|
|
||||||
@ -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}" \
|
|
||||||
"$@"
|
|
||||||
@ -1,443 +1,127 @@
|
|||||||
#include "tty_protocol_writer.h"
|
#include "tty_protocol_writer.h"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstring>
|
|
||||||
#include <exception>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <thread>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
||||||
struct TtyProtocolWriter::Impl {};
|
TtyProtocolWriter::TtyProtocolWriter(std::string path) : path_(std::move(path)) {
|
||||||
|
|
||||||
TtyProtocolWriter::TtyProtocolWriter(std::string path, std::size_t ring_capacity_bytes)
|
|
||||||
: path_(std::move(path)) {
|
|
||||||
(void) ring_capacity_bytes;
|
|
||||||
throw std::runtime_error("tty output is supported only on Linux/POSIX");
|
throw std::runtime_error("tty output is supported only on Linux/POSIX");
|
||||||
}
|
}
|
||||||
|
|
||||||
TtyProtocolWriter::~TtyProtocolWriter() = default;
|
TtyProtocolWriter::~TtyProtocolWriter() = default;
|
||||||
|
|
||||||
void TtyProtocolWriter::emit_packet_start(uint16_t marker) {
|
TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept = default;
|
||||||
(void) marker;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) index;
|
||||||
(void) ch1_avg;
|
(void) ch1_avg;
|
||||||
(void) ch2_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 {
|
const std::string& TtyProtocolWriter::path() const {
|
||||||
return path_;
|
return path_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TtyProtocolWriter::throw_if_failed() const {}
|
void TtyProtocolWriter::write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const {
|
||||||
|
|
||||||
void TtyProtocolWriter::shutdown() {}
|
|
||||||
|
|
||||||
void TtyProtocolWriter::enqueue_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) {
|
|
||||||
(void) word0;
|
(void) word0;
|
||||||
(void) word1;
|
(void) word1;
|
||||||
(void) word2;
|
(void) word2;
|
||||||
(void) word3;
|
(void) word3;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TtyProtocolWriter::worker_loop() {}
|
void TtyProtocolWriter::close_fd() noexcept {}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <condition_variable>
|
#include <cstring>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <limits.h>
|
|
||||||
#include <mutex>
|
|
||||||
#include <optional>
|
|
||||||
#include <pty.h>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <termios.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
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::string io_error(const std::string& action, const std::string& path) {
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << action << " '" << path << "': " << std::strerror(errno);
|
out << action << " '" << path << "': " << std::strerror(errno);
|
||||||
return out.str();
|
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
|
} // namespace
|
||||||
|
|
||||||
struct TtyProtocolWriter::Impl {
|
TtyProtocolWriter::TtyProtocolWriter(std::string path) : path_(std::move(path)) {
|
||||||
explicit Impl(std::size_t ring_capacity_bytes)
|
fd_ = ::open(path_.c_str(), O_WRONLY | O_NOCTTY);
|
||||||
: capacity_frames(std::max<std::size_t>(1U, ring_capacity_bytes / kFrameByteCount)),
|
if (fd_ < 0) {
|
||||||
ring(capacity_frames) {}
|
throw std::runtime_error(io_error("Cannot open tty output", path_));
|
||||||
|
|
||||||
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) {
|
|
||||||
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() {
|
TtyProtocolWriter::~TtyProtocolWriter() {
|
||||||
try {
|
close_fd();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TtyProtocolWriter::emit_packet_start(uint16_t marker) {
|
TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept
|
||||||
enqueue_frame(marker, 0xFFFF, 0xFFFF, 0xFFFF);
|
: 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) {
|
TtyProtocolWriter& TtyProtocolWriter::operator=(TtyProtocolWriter&& other) noexcept {
|
||||||
enqueue_frame(0x000A,
|
if (this != &other) {
|
||||||
index,
|
close_fd();
|
||||||
static_cast<uint16_t>(ch1_avg),
|
path_ = std::move(other.path_);
|
||||||
static_cast<uint16_t>(ch2_avg));
|
fd_ = other.fd_;
|
||||||
|
other.fd_ = -1;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TtyProtocolWriter::enqueue_encoded_frames(const uint16_t* words, std::size_t frame_count) {
|
void TtyProtocolWriter::emit_packet_start() const {
|
||||||
if ((frame_count == 0U) || (words == nullptr)) {
|
write_frame(0x000A, 0xFFFF, 0xFFFF, 0xFFFF);
|
||||||
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 {
|
void TtyProtocolWriter::emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) const {
|
||||||
if (!impl_) {
|
write_frame(0x000A,
|
||||||
return {};
|
index,
|
||||||
}
|
static_cast<uint16_t>(ch1_avg),
|
||||||
|
static_cast<uint16_t>(ch2_avg));
|
||||||
std::lock_guard<std::mutex> lock(impl_->mutex);
|
|
||||||
return impl_->stats;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string& TtyProtocolWriter::path() const {
|
const std::string& TtyProtocolWriter::path() const {
|
||||||
return path_;
|
return path_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TtyProtocolWriter::throw_if_failed() const {
|
void TtyProtocolWriter::write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const {
|
||||||
if (!impl_) {
|
const uint16_t frame[4] = {word0, word1, word2, word3};
|
||||||
return;
|
const std::uint8_t* bytes = reinterpret_cast<const std::uint8_t*>(frame);
|
||||||
}
|
std::size_t remaining = sizeof(frame);
|
||||||
|
|
||||||
std::exception_ptr failure;
|
while (remaining != 0U) {
|
||||||
{
|
const ssize_t written = ::write(fd_, bytes, remaining);
|
||||||
std::lock_guard<std::mutex> lock(impl_->mutex);
|
if (written < 0) {
|
||||||
failure = impl_->failure;
|
if (errno == EINTR) {
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
if (failure) {
|
throw std::runtime_error(io_error("Cannot write tty frame to", path_));
|
||||||
std::rethrow_exception(failure);
|
}
|
||||||
|
if (written == 0) {
|
||||||
|
throw std::runtime_error("tty write returned 0 bytes for '" + path_ + "'");
|
||||||
|
}
|
||||||
|
bytes += static_cast<std::size_t>(written);
|
||||||
|
remaining -= static_cast<std::size_t>(written);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TtyProtocolWriter::shutdown() {
|
void TtyProtocolWriter::close_fd() noexcept {
|
||||||
if (!impl_) {
|
if (fd_ >= 0) {
|
||||||
return;
|
::close(fd_);
|
||||||
}
|
fd_ = -1;
|
||||||
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
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_ + "'"));
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,41 +1,29 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
class TtyProtocolWriter {
|
class TtyProtocolWriter {
|
||||||
public:
|
public:
|
||||||
struct StatsSnapshot {
|
explicit TtyProtocolWriter(std::string path);
|
||||||
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);
|
|
||||||
~TtyProtocolWriter();
|
~TtyProtocolWriter();
|
||||||
|
|
||||||
TtyProtocolWriter(const TtyProtocolWriter&) = delete;
|
TtyProtocolWriter(const TtyProtocolWriter&) = delete;
|
||||||
TtyProtocolWriter& operator=(const TtyProtocolWriter&) = delete;
|
TtyProtocolWriter& operator=(const TtyProtocolWriter&) = delete;
|
||||||
TtyProtocolWriter(TtyProtocolWriter&& other) noexcept = delete;
|
TtyProtocolWriter(TtyProtocolWriter&& other) noexcept;
|
||||||
TtyProtocolWriter& operator=(TtyProtocolWriter&& other) noexcept = delete;
|
TtyProtocolWriter& operator=(TtyProtocolWriter&& other) noexcept;
|
||||||
|
|
||||||
void emit_packet_start(uint16_t marker = 0x000A);
|
void emit_packet_start() const;
|
||||||
void emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg);
|
void emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) const;
|
||||||
void enqueue_encoded_frames(const uint16_t* words, std::size_t frame_count);
|
|
||||||
StatsSnapshot stats() const;
|
|
||||||
|
|
||||||
const std::string& path() const;
|
const std::string& path() const;
|
||||||
void throw_if_failed() const;
|
|
||||||
void shutdown();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void enqueue_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3);
|
void write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const;
|
||||||
void worker_loop();
|
void close_fd() noexcept;
|
||||||
|
|
||||||
std::string path_;
|
std::string path_;
|
||||||
|
#ifndef _WIN32
|
||||||
struct Impl;
|
int fd_ = -1;
|
||||||
std::unique_ptr<Impl> impl_;
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user