diff --git a/main.cpp b/main.cpp index 31c4db5..8d9b7d0 100644 --- a/main.cpp +++ b/main.cpp @@ -116,6 +116,7 @@ struct Config { bool do1_toggle_per_frame = false; bool do1_noise_subtract = false; bool do1_raw_tty_marked = false; + bool do1_pair_subtract_avg = false; std::optional noise_avg_steps; }; @@ -474,7 +475,7 @@ void print_help(const char* exe_name) { << " [di1:zero|trace|ignore]\n" << " [duration_ms:100] [packet_limit:0] [csv:capture.csv] [svg:capture.svg]\n" << " [live_html:live_plot.html] [live_json:live_plot.json] [tty:/tmp/ttyADC_data] [di1_group_avg]\n" - << " [do1_toggle_per_frame] [do1_noise_subtract] [do1_raw_tty_marked] [noise_avg_steps:N]\n" + << " [do1_toggle_per_frame] [do1_noise_subtract] [do1_raw_tty_marked] [do1_pair_subtract_avg] [noise_avg_steps:N]\n" << " [recv_block:32768] [stats_period_ms:1000] [live_update_period_ms:1000] [svg_history_packets:50] [start_wait_ms:10000]\n" << " [buffer_words:8388608] [step_words:32768]\n" << " [pullup_syn1] [pullup_syn2] [pulldown_conv_in] [pulldown_start_in]\n" @@ -520,9 +521,13 @@ void print_help(const char* exe_name) { << " from both phase channels, tagged by DI1 level from synchronous DIN:\n" << " word0=0x00A3 for DO1/DI1 LOW, word0=0x00A4 for DO1/DI1 HIGH\n" << " no baseline subtraction is applied in this mode\n" + << " do1_pair_subtract_avg -> with tty + do1_toggle_per_frame + phase profile, pair adjacent CH1/CH2 ticks,\n" + << " drop boundary pairs with mismatched DI1/DI2, compute low-high by DI2 per pair,\n" + << " average within each constant DI1 run, and emit legacy 4-word tty frames (word0=0x000A)\n" << " noise_avg_steps:N -> kept for compatibility in do1_noise_subtract mode (current baseline is previous HIGH step)\n" << " (still required when do1_noise_subtract is enabled)\n" - << " tty fast stream-only modes -> di1_group_avg, do1_noise_subtract, do1_raw_tty_marked; skip CSV/SVG/live outputs\n" + << " tty fast stream-only modes -> di1_group_avg, do1_noise_subtract, do1_raw_tty_marked, do1_pair_subtract_avg;\n" + << " skip CSV/SVG/live outputs\n" << " If sample_clock_hz is omitted together with clock:internal, the maximum ADC speed is used\n" << "\n" << "Profiles:\n" @@ -553,6 +558,8 @@ void print_help(const char* exe_name) { << "DI1 from synchronous DIN is used as the actual DO1 state marker, and each LOW step subtracts previous HIGH step.\n" << "For raw loopback diagnostics, use do1_toggle_per_frame + do1_raw_tty_marked in phase profile;\n" << "steps are tagged in word0 as 0x00A3 (LOW) / 0x00A4 (HIGH) with raw per-step channel averages.\n" + << "For DI1-stable low/high DI2 subtraction with legacy tty output, use do1_toggle_per_frame + do1_pair_subtract_avg\n" + << "in phase profile; boundary CH1/CH2 pairs with mismatched DI1/DI2 are dropped before averaging.\n" << "Amplitude mode keeps the raw AD8317 voltage as captured on X1; no inversion is applied.\n" << "\n" << "Phase profile example:\n" @@ -673,6 +680,10 @@ Config parse_args(int argc, char** argv) { cfg.do1_raw_tty_marked = true; continue; } + if (arg == "do1_pair_subtract_avg") { + cfg.do1_pair_subtract_avg = true; + continue; + } if (arg == "do1_toggle_per_frame") { cfg.do1_toggle_per_frame = true; continue; @@ -792,7 +803,7 @@ Config parse_args(int argc, char** argv) { cfg.live_update_period_ms = std::max(cfg.live_update_period_ms, 1000U); } } - if (cfg.tty_path && (cfg.di1_group_average || cfg.do1_noise_subtract || cfg.do1_raw_tty_marked)) { + if (cfg.tty_path && (cfg.di1_group_average || cfg.do1_noise_subtract || cfg.do1_raw_tty_marked || cfg.do1_pair_subtract_avg)) { if (!cfg.recv_block_specified) { cfg.recv_block_words = std::max(cfg.recv_block_words, 65536U); } @@ -818,6 +829,9 @@ Config parse_args(int argc, char** argv) { if (cfg.di1_group_average && cfg.do1_raw_tty_marked) { fail("do1_raw_tty_marked is incompatible with di1_group_avg"); } + if (cfg.di1_group_average && cfg.do1_pair_subtract_avg) { + fail("do1_pair_subtract_avg is incompatible with di1_group_avg"); + } if (cfg.do1_noise_subtract) { if (!cfg.tty_path.has_value()) { fail("do1_noise_subtract requires tty:"); @@ -831,6 +845,9 @@ Config parse_args(int argc, char** argv) { if (cfg.do1_raw_tty_marked) { fail("do1_noise_subtract is incompatible with do1_raw_tty_marked"); } + if (cfg.do1_pair_subtract_avg) { + fail("do1_noise_subtract is incompatible with do1_pair_subtract_avg"); + } if (!cfg.noise_avg_steps.has_value()) { fail("do1_noise_subtract requires noise_avg_steps:N"); } @@ -847,6 +864,9 @@ Config parse_args(int argc, char** argv) { if (!cfg.do1_toggle_per_frame) { fail("do1_raw_tty_marked requires do1_toggle_per_frame"); } + if (cfg.do1_pair_subtract_avg) { + fail("do1_raw_tty_marked is incompatible with do1_pair_subtract_avg"); + } if (cfg.mode != X502_LCH_MODE_DIFF) { fail("do1_raw_tty_marked requires phase configuration: mode:diff channels:2 ch1:2 ch2:3"); } @@ -854,6 +874,26 @@ Config parse_args(int argc, char** argv) { fail("do1_raw_tty_marked requires phase configuration: mode:diff channels:2 ch1:2 ch2:3"); } } + if (cfg.do1_pair_subtract_avg) { + if (!cfg.tty_path.has_value()) { + fail("do1_pair_subtract_avg requires tty:"); + } + if (!cfg.do1_toggle_per_frame) { + fail("do1_pair_subtract_avg requires do1_toggle_per_frame"); + } + if (cfg.do1_noise_subtract) { + fail("do1_pair_subtract_avg is incompatible with do1_noise_subtract"); + } + if (cfg.do1_raw_tty_marked) { + fail("do1_pair_subtract_avg is incompatible with do1_raw_tty_marked"); + } + if (cfg.profile != CaptureProfile::Phase) { + fail("do1_pair_subtract_avg requires profile:phase"); + } + if ((cfg.mode != X502_LCH_MODE_DIFF) || (cfg.channel_count != 2U) || (cfg.ch1 != 2U) || (cfg.ch2 != 3U)) { + fail("do1_pair_subtract_avg requires phase configuration: mode:diff channels:2 ch1:2 ch2:3"); + } + } 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"); } @@ -1407,6 +1447,105 @@ struct Do1NoiseSubtractState { void finish_step() { clear_step(); } }; +struct Do1PairSubtractAverageState { + struct PairSample { + double ch1 = 0.0; + double ch2 = 0.0; + }; + + std::deque low_pairs; + std::deque high_pairs; + bool run_initialized = false; + bool run_di1_high = false; + double diff_sum_ch1 = 0.0; + double diff_sum_ch2 = 0.0; + uint32_t diff_count = 0; + + bool pending_ch1_valid = false; + double pending_ch1_raw = 0.0; + bool pending_ch1_di1_high = false; + bool pending_ch1_di2_high = false; + + uint16_t next_index = 1; + + void clear_run_pairs() { + low_pairs.clear(); + high_pairs.clear(); + diff_sum_ch1 = 0.0; + diff_sum_ch2 = 0.0; + diff_count = 0; + } + + void clear_pending_ch1() { + pending_ch1_valid = false; + pending_ch1_raw = 0.0; + pending_ch1_di1_high = false; + pending_ch1_di2_high = false; + } + + void reset_packet() { + clear_run_pairs(); + run_initialized = false; + run_di1_high = false; + clear_pending_ch1(); + next_index = 1; + } + + void start_run(bool di1_high) { + clear_run_pairs(); + run_initialized = true; + run_di1_high = di1_high; + } + + void clear_run() { + clear_run_pairs(); + run_initialized = false; + } + + void add_pending_ch1(double raw_code, bool di1_high, bool di2_high) { + pending_ch1_valid = true; + pending_ch1_raw = raw_code; + pending_ch1_di1_high = di1_high; + pending_ch1_di2_high = di2_high; + } + + void add_pair_sample(bool di2_high, double ch1_value, double ch2_value) { + const PairSample sample{ch1_value, ch2_value}; + if (di2_high) { + high_pairs.push_back(sample); + } else { + low_pairs.push_back(sample); + } + + while (!low_pairs.empty() && !high_pairs.empty()) { + const PairSample low = low_pairs.front(); + const PairSample high = high_pairs.front(); + low_pairs.pop_front(); + high_pairs.pop_front(); + + diff_sum_ch1 += (low.ch1 - high.ch1); + diff_sum_ch2 += (low.ch2 - high.ch2); + ++diff_count; + } + } + + bool has_output() const { return diff_count != 0U; } + + double avg_diff_ch1() const { + if (diff_count == 0U) { + return 0.0; + } + return diff_sum_ch1 / static_cast(diff_count); + } + + double avg_diff_ch2() const { + if (diff_count == 0U) { + return 0.0; + } + return diff_sum_ch2 / static_cast(diff_count); + } +}; + int16_t pack_raw_code_to_int16(double avg_raw_code) { const double scaled = avg_raw_code * 32767.0 / static_cast(X502_ADC_SCALE_CODE_MAX); @@ -1688,15 +1827,17 @@ int run(const Config& cfg) { const bool tty_di1_group_average = cfg.tty_path.has_value() && cfg.di1_group_average; const bool tty_do1_noise_subtract = cfg.tty_path.has_value() && cfg.do1_noise_subtract; const bool tty_do1_raw_tty_marked = cfg.tty_path.has_value() && cfg.do1_raw_tty_marked; + const bool tty_do1_pair_subtract_avg = cfg.tty_path.has_value() && cfg.do1_pair_subtract_avg; const bool fast_tty_avg_stream_mode = - tty_di1_group_average || tty_do1_noise_subtract || tty_do1_raw_tty_marked; + tty_di1_group_average || tty_do1_noise_subtract || tty_do1_raw_tty_marked || tty_do1_pair_subtract_avg; const uint16_t tty_packet_start_marker = (cfg.profile == CaptureProfile::Amplitude) ? 0x001AU : 0x000AU; const std::string fast_tty_mode_name = tty_do1_noise_subtract ? std::string("do1_noise_subtract") : (tty_do1_raw_tty_marked ? std::string("do1_raw_tty_marked") - : (tty_di1_group_average ? std::string("di1_group_avg") - : std::string("none"))); + : (tty_do1_pair_subtract_avg ? std::string("do1_pair_subtract_avg") + : (tty_di1_group_average ? std::string("di1_group_avg") + : std::string("none")))); if (cfg.tty_path) { const uint64_t typical_tty_batch_frames = (static_cast(read_capacity_words) + 1U) / 2U; const uint64_t typical_tty_batch_bytes = typical_tty_batch_frames * sizeof(uint16_t) * 4U; @@ -1707,7 +1848,8 @@ int run(const Config& cfg) { } tty_ring_capacity_bytes = static_cast(tty_ring_bytes_u64); tty_writer = std::make_unique(*cfg.tty_path, tty_ring_capacity_bytes); - if (!tty_di1_group_average && !tty_do1_noise_subtract && !tty_do1_raw_tty_marked) { + if (!tty_di1_group_average && !tty_do1_noise_subtract && !tty_do1_raw_tty_marked && + !tty_do1_pair_subtract_avg) { tty_writer->emit_packet_start(tty_packet_start_marker); } } @@ -1727,10 +1869,13 @@ int run(const Config& cfg) { << " tty di1_group_avg: " << (tty_di1_group_average ? "enabled" : "disabled") << "\n" << " tty do1_noise_subtract: " << (tty_do1_noise_subtract ? "enabled" : "disabled") << "\n" << " tty do1_raw_tty_marked: " << (tty_do1_raw_tty_marked ? "enabled" : "disabled") << "\n" + << " tty do1_pair_subtract_avg: " << (tty_do1_pair_subtract_avg ? "enabled" : "disabled") << "\n" << " do1_noise_subtract marker: " << (tty_do1_noise_subtract ? "DI1 from DIN (DO1->DI1 loopback expected)" : "n/a") << "\n" << " do1_raw_tty_marked markers: " << (tty_do1_raw_tty_marked ? "0x00A3=LOW, 0x00A4=HIGH (DI1 from DIN)" : "n/a") << "\n" + << " do1_pair_subtract_avg marker: " + << (tty_do1_pair_subtract_avg ? "word0=0x000A, low-high by DI2, averaged per DI1 run" : "n/a") << "\n" << " noise_avg_steps: " << (tty_do1_noise_subtract ? std::to_string(*cfg.noise_avg_steps) : std::string("n/a")) << "\n" << " stream-only note: ignoring csv/svg/live_html/live_json/live_update_period_ms/svg_history_packets\n"; @@ -1758,6 +1903,11 @@ int run(const Config& cfg) { : (cfg.do1_raw_tty_marked ? std::string("requested, tty disabled") : std::string("disabled"))) << "\n" + << " tty do1_pair_subtract_avg: " + << (tty_do1_pair_subtract_avg ? std::string("enabled") + : (cfg.do1_pair_subtract_avg ? std::string("requested, tty disabled") + : std::string("disabled"))) + << "\n" << " SVG history packets kept in RAM: " << ((cfg.svg_history_packets == 0U) ? std::string("all (unbounded)") : std::to_string(cfg.svg_history_packets)) << "\n"; @@ -1826,6 +1976,7 @@ int run(const Config& cfg) { TtyGroupAverageState tty_group_state; Do1RawTaggedState tty_do1_raw_state; Do1NoiseSubtractState tty_do1_noise_state; + Do1PairSubtractAverageState tty_do1_pair_state; std::vector tty_frame_words; tty_frame_words.reserve(static_cast(read_capacity_words) * 2U + 16U); if (tty_do1_noise_subtract) { @@ -2041,6 +2192,32 @@ int run(const Config& cfg) { tty_do1_noise_state.finish_step(); }; + auto append_tty_do1_pair_subtracted_avg_run = [&]() { + if (!tty_do1_pair_subtract_avg) { + return; + } + if (!tty_do1_pair_state.run_initialized) { + return; + } + + if (!tty_do1_pair_state.has_output()) { + tty_do1_pair_state.clear_run(); + return; + } + + if (tty_do1_pair_state.next_index >= 0xFFFFU) { + fail("TTY protocol step index overflow within packet"); + } + + append_tty_frame(0x000AU, + tty_do1_pair_state.next_index, + static_cast(pack_raw_code_to_int16(tty_do1_pair_state.avg_diff_ch1())), + static_cast(pack_raw_code_to_int16(tty_do1_pair_state.avg_diff_ch2()))); + ++tty_do1_pair_state.next_index; + ++packet_avg_steps; + tty_do1_pair_state.clear_run(); + }; + auto flush_tty_frames = [&]() { if (!tty_writer || tty_frame_words.empty()) { return; @@ -2077,6 +2254,9 @@ int run(const Config& cfg) { } else if (tty_do1_noise_subtract) { tty_do1_noise_state.reset_packet(); append_tty_packet_start(); + } else if (tty_do1_pair_subtract_avg) { + tty_do1_pair_state.reset_packet(); + append_tty_packet_start(); } packet_active = true; }; @@ -2100,6 +2280,9 @@ int run(const Config& cfg) { // Finalize the last DI1 run in packet, drop only incomplete channel tuples. append_tty_do1_subtracted_step(); tty_do1_noise_state.clear_step(); + } else if (tty_do1_pair_subtract_avg) { + append_tty_do1_pair_subtracted_avg_run(); + tty_do1_pair_state.reset_packet(); } const std::size_t frames = fast_tty_avg_stream_mode @@ -2222,6 +2405,8 @@ int run(const Config& cfg) { tty_do1_raw_state.clear_step(); } else if (tty_do1_noise_subtract) { tty_do1_noise_state.clear_step(); + } else if (tty_do1_pair_subtract_avg) { + tty_do1_pair_state.reset_packet(); } }; @@ -2473,7 +2658,8 @@ int run(const Config& cfg) { continue; } - if ((din_value & kE502Digital2Mask) != 0U) { + const bool di2_level = (din_value & kE502Digital2Mask) != 0U; + if (di2_level) { ++packet_di2_high_clocks; } else { ++packet_di2_low_clocks; @@ -2508,7 +2694,34 @@ int run(const Config& cfg) { if (fast_tty_avg_stream_mode) { if (fast_packet_frames < target_frames) { - if (tty_do1_noise_subtract) { + if (tty_do1_pair_subtract_avg) { + if (lch == 0U) { + tty_do1_pair_state.add_pending_ch1(adc_raw_value, di1_level, di2_level); + } else if (lch == 1U) { + if (tty_do1_pair_state.pending_ch1_valid) { + const bool pair_di1_matches = + (tty_do1_pair_state.pending_ch1_di1_high == di1_level); + const bool pair_di2_matches = + (tty_do1_pair_state.pending_ch1_di2_high == di2_level); + + if (pair_di1_matches && pair_di2_matches) { + if (!tty_do1_pair_state.run_initialized) { + tty_do1_pair_state.start_run(di1_level); + } else if (tty_do1_pair_state.run_di1_high != di1_level) { + append_tty_do1_pair_subtracted_avg_run(); + tty_do1_pair_state.start_run(di1_level); + } + + tty_do1_pair_state.add_pair_sample(di2_level, + tty_do1_pair_state.pending_ch1_raw, + adc_raw_value); + } + } + tty_do1_pair_state.clear_pending_ch1(); + } else { + tty_do1_pair_state.clear_pending_ch1(); + } + } else if (tty_do1_noise_subtract) { tty_do1_noise_state.add_sample(lch, adc_raw_value); } else if (tty_do1_raw_tty_marked) { tty_do1_raw_state.add_sample(lch, adc_raw_value); diff --git a/run_do1_pair_subtract_avg.sh b/run_do1_pair_subtract_avg.sh new file mode 100755 index 0000000..1f9e190 --- /dev/null +++ b/run_do1_pair_subtract_avg.sh @@ -0,0 +1,33 @@ +#!/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" \ + profile:phase \ + 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_pair_subtract_avg \ + "tty:${TTY_PATH}" \ + "$@"