diff --git a/main.cpp b/main.cpp index 68a428a..7c1e869 100644 --- a/main.cpp +++ b/main.cpp @@ -55,10 +55,17 @@ enum class Di1Mode { Ignore }; +enum class CaptureProfile { + Manual, + Phase, + Amplitude +}; + struct Config { std::string serial; std::optional 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(tty_group_state.count_ch1); - const double ch2_avg = tty_group_state.sum_ch2 / static_cast(tty_group_state.count_ch2); + const double ch2_avg = + (cfg.channel_count <= 1U) + ? 0.0 + : (tty_group_state.sum_ch2 / static_cast(tty_group_state.count_ch2)); append_tty_frame(0x000A, static_cast(tty_group_state.next_index), static_cast(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(raw_adc_count) + 1U) / 2U) * 4U); + const std::size_t tty_frame_count = + (cfg.channel_count <= 1U) + ? static_cast(raw_adc_count) + : ((static_cast(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(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;