This commit is contained in:
awe
2026-04-15 16:45:45 +03:00
parent ff2add9504
commit 4af1324e3b

158
main.cpp
View File

@ -55,10 +55,17 @@ enum class Di1Mode {
Ignore
};
enum class CaptureProfile {
Manual,
Phase,
Amplitude
};
struct Config {
std::string serial;
std::optional<uint32_t> ip_addr;
CaptureProfile profile = CaptureProfile::Manual;
uint32_t channel_count = 2;
uint32_t mode = X502_LCH_MODE_DIFF;
uint32_t range = X502_ADC_RANGE_02;
@ -90,6 +97,10 @@ struct Config {
bool input_buffer_specified = false;
bool input_step_specified = false;
bool live_update_specified = false;
bool mode_specified = false;
bool channel_count_specified = false;
bool ch1_specified = false;
bool ch2_specified = false;
bool pullup_syn1 = false;
bool pullup_syn2 = false;
@ -222,6 +233,81 @@ uint32_t parse_mode(const std::string& text) {
fail("Unsupported input mode: " + text);
}
CaptureProfile parse_profile(const std::string& text) {
const std::string value = trim_copy(text);
if (value == "phase") {
return CaptureProfile::Phase;
}
if (value == "amplitude") {
return CaptureProfile::Amplitude;
}
fail("Unsupported profile: " + text + ". Use phase or amplitude");
}
std::string profile_to_string(CaptureProfile profile) {
switch (profile) {
case CaptureProfile::Manual:
return "manual";
case CaptureProfile::Phase:
return "phase";
case CaptureProfile::Amplitude:
return "amplitude";
default:
return "unknown";
}
}
struct ProfileSettings {
uint32_t channel_count = 2;
uint32_t mode = X502_LCH_MODE_DIFF;
uint32_t ch1 = 2;
uint32_t ch2 = 3;
};
ProfileSettings profile_settings(CaptureProfile profile) {
switch (profile) {
case CaptureProfile::Phase:
return {2U, X502_LCH_MODE_DIFF, 2U, 3U};
case CaptureProfile::Amplitude:
return {1U, X502_LCH_MODE_COMM, 0U, 0U};
case CaptureProfile::Manual:
default:
fail("Internal error: no settings for manual profile");
}
}
void apply_profile(Config& cfg) {
if (cfg.profile == CaptureProfile::Manual) {
return;
}
const ProfileSettings settings = profile_settings(cfg.profile);
const std::string profile_name = profile_to_string(cfg.profile);
const std::string profile_arg = "profile:" + profile_name;
if (cfg.mode_specified && (cfg.mode != settings.mode)) {
fail(profile_arg + " conflicts with explicit mode");
}
if (cfg.channel_count_specified && (cfg.channel_count != settings.channel_count)) {
fail(profile_arg + " conflicts with explicit channels");
}
if (cfg.ch1_specified && (cfg.ch1 != settings.ch1)) {
fail(profile_arg + " conflicts with explicit ch1");
}
if (settings.channel_count <= 1U) {
if (cfg.ch2_specified) {
fail(profile_arg + " does not allow ch2");
}
} else if (cfg.ch2_specified && (cfg.ch2 != settings.ch2)) {
fail(profile_arg + " conflicts with explicit ch2");
}
cfg.mode = settings.mode;
cfg.channel_count = settings.channel_count;
cfg.ch1 = settings.ch1;
cfg.ch2 = settings.ch2;
}
uint32_t parse_internal_ref_freq(const std::string& text) {
const std::string value = trim_copy(text);
if ((value == "2000000") || (value == "2000khz") || (value == "2mhz")) {
@ -376,7 +462,8 @@ std::string phy_channel_name(uint32_t mode, uint32_t phy_ch) {
void print_help(const char* exe_name) {
std::cout
<< "Usage:\n"
<< " " << exe_name << " [serial:SN] [ip:192.168.0.10] [channels:2] [ch1:2] [ch2:3]\n"
<< " " << exe_name << " [serial:SN] [ip:192.168.0.10] [profile:phase|amplitude]\n"
<< " [channels:2] [ch1:2] [ch2:3]\n"
<< " [mode:diff|comm] [range:0.2] [clock:di_syn1_rise]\n"
<< " [start:di_syn2_rise] [stop:di_syn2_fall] [sample_clock_hz:125000|max]\n"
<< " [internal_ref_hz:2000000]\n"
@ -415,11 +502,16 @@ void print_help(const char* exe_name) {
<< " buffer_words:8388608 -> input stream buffer size in 32-bit words\n"
<< " step_words:32768 -> input stream transfer step in 32-bit words\n"
<< " live_html/live_json -> live graph files updated as packets arrive outside tty+di1_group_avg fast mode\n"
<< " tty:/tmp/ttyADC_data -> write a continuous legacy 4-word CH1/CH2 stream to a Linux/POSIX tty or PTY link path\n"
<< " di1_group_avg -> with tty + di1:trace, emit one averaged CH1/CH2 step per constant DI1 run\n"
<< " tty:/tmp/ttyADC_data -> write a continuous legacy 4-word CH1/CH2 stream; with channels:1, CH2 is 0\n"
<< " di1_group_avg -> with tty + di1:trace, emit one averaged 4-word step per constant DI1 run\n"
<< " tty+di1_group_avg -> fast stream-only mode: skip CSV/SVG/live outputs and send only averaged tty data\n"
<< " If sample_clock_hz is omitted together with clock:internal, the maximum ADC speed is used\n"
<< "\n"
<< "Profiles:\n"
<< " profile:phase -> mode:diff channels:2 ch1:2 ch2:3 (X3-Y3 and X4-Y4)\n"
<< " profile:amplitude -> mode:comm channels:1 ch1:0 (X1, raw AD8317 voltage)\n"
<< " profile:* is strict -> conflicting mode/channels/ch1/ch2 arguments are rejected\n"
<< "\n"
<< "Differential physical channel mapping:\n"
<< " 0..15 -> X1-Y1 .. X16-Y16\n"
<< "\n"
@ -439,20 +531,21 @@ void print_help(const char* exe_name) {
<< "The live HTML supports X/Y zoom buttons, mouse-wheel zoom, and reset.\n"
<< "For tty output, use di1_group_avg together with di1:trace to emit one averaged 4-word step\n"
<< "per constant DI1 run while keeping the current ring-buffered binary frame format.\n"
<< "Amplitude mode keeps the raw AD8317 voltage as captured on X1; no inversion is applied.\n"
<< "\n"
<< "Recommended working example:\n"
<< "Phase profile example:\n"
<< " " << exe_name
<< " clock:di_syn1_rise start:di_syn2_rise stop:di_syn2_fall sample_clock_hz:125000"
<< " profile:phase clock:di_syn1_rise start:di_syn2_rise stop:di_syn2_fall sample_clock_hz:125000"
<< " duration_ms:100 packet_limit:10 csv:chirp.csv svg:chirp.svg\n"
<< "\n"
<< "Internal-clock example:\n"
<< "Amplitude profile example:\n"
<< " " << exe_name
<< " clock:internal internal_ref_hz:2000000 start:di_syn2_rise stop:di_syn2_fall"
<< " sample_clock_hz:max duration_ms:100 packet_limit:0 csv:chirp.csv svg:chirp.svg\n"
<< " profile:amplitude clock:internal internal_ref_hz:2000000 start:di_syn2_rise stop:di_syn2_fall"
<< " sample_clock_hz:max duration_ms:100 packet_limit:0 csv:amplitude.csv svg:amplitude.svg\n"
<< "\n"
<< "TTY DI1-group average example:\n"
<< "Amplitude TTY DI1-group average example:\n"
<< " " << exe_name
<< " clock:internal internal_ref_hz:2000000 start:di_syn2_rise stop:di_syn2_fall"
<< " profile:amplitude 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 duration_ms:100 packet_limit:0"
<< " tty:/tmp/ttyADC_data\n";
}
@ -490,12 +583,18 @@ Config parse_args(int argc, char** argv) {
cfg.ip_addr = parse_ipv4(arg.substr(3));
continue;
}
if (starts_with(arg, "profile:")) {
cfg.profile = parse_profile(arg.substr(8));
continue;
}
if (starts_with(arg, "mode:")) {
cfg.mode = parse_mode(arg.substr(5));
cfg.mode_specified = true;
continue;
}
if (starts_with(arg, "channels:")) {
cfg.channel_count = parse_channel_count(arg.substr(9));
cfg.channel_count_specified = true;
continue;
}
if (starts_with(arg, "range:")) {
@ -504,10 +603,12 @@ Config parse_args(int argc, char** argv) {
}
if (starts_with(arg, "ch1:")) {
cfg.ch1 = parse_u32(arg.substr(4), "ch1");
cfg.ch1_specified = true;
continue;
}
if (starts_with(arg, "ch2:")) {
cfg.ch2 = parse_u32(arg.substr(4), "ch2");
cfg.ch2_specified = true;
continue;
}
if (starts_with(arg, "clock:")) {
@ -612,6 +713,8 @@ Config parse_args(int argc, char** argv) {
fail("Unknown argument: " + arg);
}
apply_profile(cfg);
if (cfg.duration_ms <= 0.0) {
fail("duration_ms must be > 0");
}
@ -662,12 +765,6 @@ Config parse_args(int argc, char** argv) {
if (cfg.di1_group_average && (cfg.di1_mode != Di1Mode::Trace)) {
fail("di1_group_avg requires di1:trace");
}
if (cfg.tty_path && cfg.di1_group_average && (cfg.channel_count != 2U)) {
fail("tty di1_group_avg requires channels:2");
}
if (cfg.tty_path && (cfg.channel_count != 2U)) {
fail("tty output requires channels:2");
}
if (sync_uses_di_syn1(cfg.sync_mode) && sync_uses_di_syn1(cfg.sync_start_mode)) {
fail("clock and start cannot both use DI_SYN1; use start_in or immediate start");
}
@ -1059,8 +1156,8 @@ struct TtyGroupAverageState {
}
}
bool has_complete_step() const {
return (count_ch1 != 0U) && (count_ch2 != 0U);
bool has_complete_step(uint32_t channel_count) const {
return (count_ch1 != 0U) && ((channel_count <= 1U) || (count_ch2 != 0U));
}
};
@ -1494,7 +1591,7 @@ int run(const Config& cfg) {
};
auto append_tty_group_step = [&]() {
if (!tty_di1_group_average || !tty_group_state.has_complete_step()) {
if (!tty_di1_group_average || !tty_group_state.has_complete_step(cfg.channel_count)) {
return;
}
if (tty_group_state.next_index >= 0xFFFFU) {
@ -1502,7 +1599,10 @@ int run(const Config& cfg) {
}
const double ch1_avg = tty_group_state.sum_ch1 / static_cast<double>(tty_group_state.count_ch1);
const double ch2_avg = tty_group_state.sum_ch2 / static_cast<double>(tty_group_state.count_ch2);
const double ch2_avg =
(cfg.channel_count <= 1U)
? 0.0
: (tty_group_state.sum_ch2 / static_cast<double>(tty_group_state.count_ch2));
append_tty_frame(0x000A,
static_cast<uint16_t>(tty_group_state.next_index),
static_cast<uint16_t>(pack_raw_code_to_int16(ch1_avg)),
@ -1760,9 +1860,25 @@ int run(const Config& cfg) {
if (!tty_di1_group_average) {
tty_frame_words.clear();
tty_frame_words.reserve(((static_cast<std::size_t>(raw_adc_count) + 1U) / 2U) * 4U);
const std::size_t tty_frame_count =
(cfg.channel_count <= 1U)
? static_cast<std::size_t>(raw_adc_count)
: ((static_cast<std::size_t>(raw_adc_count) + 1U) / 2U);
tty_frame_words.reserve(tty_frame_count * 4U);
for (uint32_t i = 0; i < raw_adc_count; ++i) {
const int16_t sample = pack_raw_code_to_int16(adc_raw_buffer[i]);
if (cfg.channel_count <= 1U) {
append_tty_frame(0x000A,
tty_state.next_index,
static_cast<uint16_t>(sample),
0U);
if (tty_state.next_index >= 0xFFFEU) {
tty_state.next_index = 1U;
} else {
++tty_state.next_index;
}
continue;
}
if (!tty_state.pending_ch1_valid) {
tty_state.pending_ch1 = sample;
tty_state.pending_ch1_valid = true;