| @@ -64,7 +64,9 @@ Edit `config.yaml`: | |||
| - `agc`: enable automatic gain control | |||
| - `dc_block`: enable DC blocking filter | |||
| - `iq_balance`: enable basic IQ imbalance correction | |||
| - `detector.threshold_db`: power threshold in dB | |||
| - `detector.threshold_db`: power threshold in dB (fallback if CFAR disabled) | |||
| - `detector.cfar_enabled`: enable OS-CFAR detection | |||
| - `detector.cfar_guard_cells`, `detector.cfar_train_cells`, `detector.cfar_rank`, `detector.cfar_scale_db`: OS-CFAR window + ordered-statistic parameters | |||
| - `detector.min_duration_ms`, `detector.hold_ms`: debounce/merge | |||
| - `recorder.*`: enable IQ/audio recording, preroll, output_dir, max_disk_mb | |||
| - `decoder.*`: external decode commands (use `{iq}`, `{audio}`, `{sr}` placeholders) | |||
| @@ -319,7 +319,12 @@ func main() { | |||
| cfg.Detector.EmaAlpha, | |||
| cfg.Detector.HysteresisDb, | |||
| cfg.Detector.MinStableFrames, | |||
| time.Duration(cfg.Detector.GapToleranceMs)*time.Millisecond) | |||
| time.Duration(cfg.Detector.GapToleranceMs)*time.Millisecond, | |||
| cfg.Detector.CFAREnabled, | |||
| cfg.Detector.CFARGuardCells, | |||
| cfg.Detector.CFARTrainCells, | |||
| cfg.Detector.CFARRank, | |||
| cfg.Detector.CFARScaleDb) | |||
| window := fftutil.Hann(cfg.FFTSize) | |||
| h := newHub() | |||
| @@ -439,6 +444,11 @@ func main() { | |||
| prev.Detector.HysteresisDb != next.Detector.HysteresisDb || | |||
| prev.Detector.MinStableFrames != next.Detector.MinStableFrames || | |||
| prev.Detector.GapToleranceMs != next.Detector.GapToleranceMs || | |||
| prev.Detector.CFAREnabled != next.Detector.CFAREnabled || | |||
| prev.Detector.CFARGuardCells != next.Detector.CFARGuardCells || | |||
| prev.Detector.CFARTrainCells != next.Detector.CFARTrainCells || | |||
| prev.Detector.CFARRank != next.Detector.CFARRank || | |||
| prev.Detector.CFARScaleDb != next.Detector.CFARScaleDb || | |||
| prev.SampleRate != next.SampleRate || | |||
| prev.FFTSize != next.FFTSize | |||
| windowChanged := prev.FFTSize != next.FFTSize | |||
| @@ -451,7 +461,12 @@ func main() { | |||
| next.Detector.EmaAlpha, | |||
| next.Detector.HysteresisDb, | |||
| next.Detector.MinStableFrames, | |||
| time.Duration(next.Detector.GapToleranceMs)*time.Millisecond) | |||
| time.Duration(next.Detector.GapToleranceMs)*time.Millisecond, | |||
| next.Detector.CFAREnabled, | |||
| next.Detector.CFARGuardCells, | |||
| next.Detector.CFARTrainCells, | |||
| next.Detector.CFARRank, | |||
| next.Detector.CFARScaleDb) | |||
| } | |||
| if windowChanged { | |||
| newWindow = fftutil.Hann(next.FFTSize) | |||
| @@ -19,6 +19,11 @@ detector: | |||
| hysteresis_db: 3 | |||
| min_stable_frames: 3 | |||
| gap_tolerance_ms: 500 | |||
| cfar_enabled: true | |||
| cfar_guard_cells: 2 | |||
| cfar_train_cells: 16 | |||
| cfar_rank: 24 | |||
| cfar_scale_db: 6 | |||
| recorder: | |||
| enabled: true | |||
| min_snr_db: 10 | |||
| @@ -1,6 +1,7 @@ | |||
| package config | |||
| import ( | |||
| "math" | |||
| "os" | |||
| "time" | |||
| @@ -21,6 +22,11 @@ type DetectorConfig struct { | |||
| HysteresisDb float64 `yaml:"hysteresis_db" json:"hysteresis_db"` | |||
| MinStableFrames int `yaml:"min_stable_frames" json:"min_stable_frames"` | |||
| GapToleranceMs int `yaml:"gap_tolerance_ms" json:"gap_tolerance_ms"` | |||
| CFAREnabled bool `yaml:"cfar_enabled" json:"cfar_enabled"` | |||
| CFARGuardCells int `yaml:"cfar_guard_cells" json:"cfar_guard_cells"` | |||
| CFARTrainCells int `yaml:"cfar_train_cells" json:"cfar_train_cells"` | |||
| CFARRank int `yaml:"cfar_rank" json:"cfar_rank"` | |||
| CFARScaleDb float64 `yaml:"cfar_scale_db" json:"cfar_scale_db"` | |||
| } | |||
| type RecorderConfig struct { | |||
| @@ -83,7 +89,7 @@ func Default() Config { | |||
| AGC: false, | |||
| DCBlock: false, | |||
| IQBalance: false, | |||
| Detector: DetectorConfig{ThresholdDb: -20, MinDurationMs: 250, HoldMs: 500, EmaAlpha: 0.2, HysteresisDb: 3, MinStableFrames: 3, GapToleranceMs: 500}, | |||
| Detector: DetectorConfig{ThresholdDb: -20, MinDurationMs: 250, HoldMs: 500, EmaAlpha: 0.2, HysteresisDb: 3, MinStableFrames: 3, GapToleranceMs: 500, CFAREnabled: true, CFARGuardCells: 2, CFARTrainCells: 16, CFARRank: 24, CFARScaleDb: 6}, | |||
| Recorder: RecorderConfig{ | |||
| Enabled: false, | |||
| MinSNRDb: 10, | |||
| @@ -128,6 +134,21 @@ func Load(path string) (Config, error) { | |||
| if cfg.Detector.GapToleranceMs <= 0 { | |||
| cfg.Detector.GapToleranceMs = cfg.Detector.HoldMs | |||
| } | |||
| if cfg.Detector.CFARGuardCells <= 0 { | |||
| cfg.Detector.CFARGuardCells = 2 | |||
| } | |||
| if cfg.Detector.CFARTrainCells <= 0 { | |||
| cfg.Detector.CFARTrainCells = 16 | |||
| } | |||
| if cfg.Detector.CFARRank <= 0 || cfg.Detector.CFARRank > 2*cfg.Detector.CFARTrainCells { | |||
| cfg.Detector.CFARRank = int(math.Round(0.75 * float64(2*cfg.Detector.CFARTrainCells))) | |||
| if cfg.Detector.CFARRank <= 0 { | |||
| cfg.Detector.CFARRank = 1 | |||
| } | |||
| } | |||
| if cfg.Detector.CFARScaleDb <= 0 { | |||
| cfg.Detector.CFARScaleDb = 6 | |||
| } | |||
| if cfg.FrameRate <= 0 { | |||
| cfg.FrameRate = 15 | |||
| } | |||
| @@ -29,6 +29,11 @@ type Detector struct { | |||
| HysteresisDb float64 | |||
| MinStableFrames int | |||
| GapTolerance time.Duration | |||
| CFAREnabled bool | |||
| CFARGuardCells int | |||
| CFARTrainCells int | |||
| CFARRank int | |||
| CFARScaleDb float64 | |||
| binWidth float64 | |||
| nbins int | |||
| @@ -63,7 +68,7 @@ type Signal struct { | |||
| Class *classifier.Classification `json:"class,omitempty"` | |||
| } | |||
| func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Duration, emaAlpha, hysteresis float64, minStable int, gapTolerance time.Duration) *Detector { | |||
| func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Duration, emaAlpha, hysteresis float64, minStable int, gapTolerance time.Duration, cfarEnabled bool, cfarGuard, cfarTrain, cfarRank int, cfarScaleDb float64) *Detector { | |||
| if minDur <= 0 { | |||
| minDur = 250 * time.Millisecond | |||
| } | |||
| @@ -82,6 +87,21 @@ func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Dur | |||
| if gapTolerance <= 0 { | |||
| gapTolerance = hold | |||
| } | |||
| if cfarGuard < 0 { | |||
| cfarGuard = 2 | |||
| } | |||
| if cfarTrain <= 0 { | |||
| cfarTrain = 16 | |||
| } | |||
| if cfarScaleDb <= 0 { | |||
| cfarScaleDb = 6 | |||
| } | |||
| if cfarRank <= 0 || cfarRank > 2*cfarTrain { | |||
| cfarRank = int(math.Round(0.75 * float64(2*cfarTrain))) | |||
| if cfarRank <= 0 { | |||
| cfarRank = 1 | |||
| } | |||
| } | |||
| return &Detector{ | |||
| ThresholdDb: thresholdDb, | |||
| MinDuration: minDur, | |||
| @@ -90,6 +110,11 @@ func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Dur | |||
| HysteresisDb: hysteresis, | |||
| MinStableFrames: minStable, | |||
| GapTolerance: gapTolerance, | |||
| CFAREnabled: cfarEnabled, | |||
| CFARGuardCells: cfarGuard, | |||
| CFARTrainCells: cfarTrain, | |||
| CFARRank: cfarRank, | |||
| CFARScaleDb: cfarScaleDb, | |||
| binWidth: float64(sampleRate) / float64(fftSize), | |||
| nbins: fftSize, | |||
| sampleRate: sampleRate, | |||
| @@ -126,9 +151,8 @@ func (d *Detector) detectSignals(spectrum []float64, centerHz float64) []Signal | |||
| return nil | |||
| } | |||
| smooth := d.smoothSpectrum(spectrum) | |||
| thresholdOn := d.ThresholdDb | |||
| thresholdOff := d.ThresholdDb - d.HysteresisDb | |||
| noise := median(smooth) | |||
| thresholds := d.cfarThresholds(smooth) | |||
| noiseGlobal := median(smooth) | |||
| var signals []Signal | |||
| in := false | |||
| start := 0 | |||
| @@ -136,6 +160,11 @@ func (d *Detector) detectSignals(spectrum []float64, centerHz float64) []Signal | |||
| peakBin := 0 | |||
| for i := 0; i < n; i++ { | |||
| v := smooth[i] | |||
| thresholdOn := d.ThresholdDb | |||
| if thresholds != nil && !math.IsNaN(thresholds[i]) { | |||
| thresholdOn = thresholds[i] | |||
| } | |||
| thresholdOff := thresholdOn - d.HysteresisDb | |||
| if v >= thresholdOn { | |||
| if !in { | |||
| in = true | |||
| @@ -147,11 +176,19 @@ func (d *Detector) detectSignals(spectrum []float64, centerHz float64) []Signal | |||
| peakBin = i | |||
| } | |||
| } else if in && v < thresholdOff { | |||
| noise := noiseGlobal | |||
| if thresholds != nil && peakBin >= 0 && peakBin < len(thresholds) && !math.IsNaN(thresholds[peakBin]) { | |||
| noise = thresholds[peakBin] - d.CFARScaleDb | |||
| } | |||
| signals = append(signals, d.makeSignal(start, i-1, peak, peakBin, noise, centerHz, smooth)) | |||
| in = false | |||
| } | |||
| } | |||
| if in { | |||
| noise := noiseGlobal | |||
| if thresholds != nil && peakBin >= 0 && peakBin < len(thresholds) && !math.IsNaN(thresholds[peakBin]) { | |||
| noise = thresholds[peakBin] - d.CFARScaleDb | |||
| } | |||
| signals = append(signals, d.makeSignal(start, n-1, peak, peakBin, noise, centerHz, smooth)) | |||
| } | |||
| return signals | |||
| @@ -172,6 +209,43 @@ func (d *Detector) makeSignal(first, last int, peak float64, peakBin int, noise | |||
| } | |||
| } | |||
| func (d *Detector) cfarThresholds(spectrum []float64) []float64 { | |||
| if !d.CFAREnabled || d.CFARTrainCells <= 0 { | |||
| return nil | |||
| } | |||
| n := len(spectrum) | |||
| train := d.CFARTrainCells | |||
| guard := d.CFARGuardCells | |||
| totalTrain := 2 * train | |||
| if totalTrain <= 0 { | |||
| return nil | |||
| } | |||
| rank := d.CFARRank | |||
| if rank <= 0 || rank > totalTrain { | |||
| rank = int(math.Round(0.75 * float64(totalTrain))) | |||
| if rank <= 0 { | |||
| rank = 1 | |||
| } | |||
| } | |||
| rankIdx := rank - 1 | |||
| thresholds := make([]float64, n) | |||
| buf := make([]float64, totalTrain) | |||
| for i := 0; i < n; i++ { | |||
| leftStart := i - guard - train | |||
| rightEnd := i + guard + train | |||
| if leftStart < 0 || rightEnd >= n { | |||
| thresholds[i] = math.NaN() | |||
| continue | |||
| } | |||
| copy(buf[:train], spectrum[leftStart:leftStart+train]) | |||
| copy(buf[train:], spectrum[i+guard+1:i+guard+1+train]) | |||
| sort.Float64s(buf) | |||
| noise := buf[rankIdx] | |||
| thresholds[i] = noise + d.CFARScaleDb | |||
| } | |||
| return thresholds | |||
| } | |||
| func (d *Detector) smoothSpectrum(spectrum []float64) []float64 { | |||
| if d.ema == nil || len(d.ema) != len(spectrum) { | |||
| d.ema = make([]float64, len(spectrum)) | |||
| @@ -6,7 +6,7 @@ import ( | |||
| ) | |||
| func TestDetectorCreatesEvent(t *testing.T) { | |||
| d := New(-10, 1000, 10, 1*time.Millisecond, 10*time.Millisecond, 0.2, 3, 1, 10*time.Millisecond) | |||
| d := New(-10, 1000, 10, 1*time.Millisecond, 10*time.Millisecond, 0.2, 3, 1, 10*time.Millisecond, false, 2, 16, 24, 6) | |||
| center := 0.0 | |||
| spectrum := []float64{-30, -30, -30, -5, -5, -30, -30, -30, -30, -30} | |||
| now := time.Now() | |||
| @@ -26,6 +26,11 @@ type DetectorUpdate struct { | |||
| HysteresisDb *float64 `json:"hysteresis_db"` | |||
| MinStableFrames *int `json:"min_stable_frames"` | |||
| GapToleranceMs *int `json:"gap_tolerance_ms"` | |||
| CFAREnabled *bool `json:"cfar_enabled"` | |||
| CFARGuardCells *int `json:"cfar_guard_cells"` | |||
| CFARTrainCells *int `json:"cfar_train_cells"` | |||
| CFARRank *int `json:"cfar_rank"` | |||
| CFARScaleDb *float64 `json:"cfar_scale_db"` | |||
| } | |||
| type SettingsUpdate struct { | |||
| @@ -137,6 +142,33 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) { | |||
| if update.Detector.GapToleranceMs != nil { | |||
| next.Detector.GapToleranceMs = *update.Detector.GapToleranceMs | |||
| } | |||
| if update.Detector.CFAREnabled != nil { | |||
| next.Detector.CFAREnabled = *update.Detector.CFAREnabled | |||
| } | |||
| if update.Detector.CFARGuardCells != nil { | |||
| if *update.Detector.CFARGuardCells < 0 { | |||
| return m.cfg, errors.New("cfar_guard_cells must be >= 0") | |||
| } | |||
| next.Detector.CFARGuardCells = *update.Detector.CFARGuardCells | |||
| } | |||
| if update.Detector.CFARTrainCells != nil { | |||
| if *update.Detector.CFARTrainCells <= 0 { | |||
| return m.cfg, errors.New("cfar_train_cells must be > 0") | |||
| } | |||
| next.Detector.CFARTrainCells = *update.Detector.CFARTrainCells | |||
| } | |||
| if update.Detector.CFARRank != nil { | |||
| if *update.Detector.CFARRank <= 0 { | |||
| return m.cfg, errors.New("cfar_rank must be > 0") | |||
| } | |||
| if next.Detector.CFARTrainCells > 0 && *update.Detector.CFARRank > 2*next.Detector.CFARTrainCells { | |||
| return m.cfg, errors.New("cfar_rank must be <= 2 * cfar_train_cells") | |||
| } | |||
| next.Detector.CFARRank = *update.Detector.CFARRank | |||
| } | |||
| if update.Detector.CFARScaleDb != nil { | |||
| next.Detector.CFARScaleDb = *update.Detector.CFARScaleDb | |||
| } | |||
| } | |||
| if update.Recorder != nil { | |||
| if update.Recorder.Enabled != nil { | |||
| @@ -15,6 +15,11 @@ func TestApplyConfigUpdate(t *testing.T) { | |||
| fftSize := 4096 | |||
| threshold := -35.0 | |||
| bw := 1536 | |||
| cfarEnabled := true | |||
| cfarGuard := 2 | |||
| cfarTrain := 12 | |||
| cfarRank := 18 | |||
| cfarScale := 5.5 | |||
| updated, err := mgr.ApplyConfig(ConfigUpdate{ | |||
| CenterHz: ¢er, | |||
| @@ -22,7 +27,12 @@ func TestApplyConfigUpdate(t *testing.T) { | |||
| FFTSize: &fftSize, | |||
| TunerBwKHz: &bw, | |||
| Detector: &DetectorUpdate{ | |||
| ThresholdDb: &threshold, | |||
| ThresholdDb: &threshold, | |||
| CFAREnabled: &cfarEnabled, | |||
| CFARGuardCells: &cfarGuard, | |||
| CFARTrainCells: &cfarTrain, | |||
| CFARRank: &cfarRank, | |||
| CFARScaleDb: &cfarScale, | |||
| }, | |||
| }) | |||
| if err != nil { | |||
| @@ -40,6 +50,21 @@ func TestApplyConfigUpdate(t *testing.T) { | |||
| if updated.Detector.ThresholdDb != threshold { | |||
| t.Fatalf("threshold: %v", updated.Detector.ThresholdDb) | |||
| } | |||
| if updated.Detector.CFAREnabled != cfarEnabled { | |||
| t.Fatalf("cfar enabled: %v", updated.Detector.CFAREnabled) | |||
| } | |||
| if updated.Detector.CFARGuardCells != cfarGuard { | |||
| t.Fatalf("cfar guard: %v", updated.Detector.CFARGuardCells) | |||
| } | |||
| if updated.Detector.CFARTrainCells != cfarTrain { | |||
| t.Fatalf("cfar train: %v", updated.Detector.CFARTrainCells) | |||
| } | |||
| if updated.Detector.CFARRank != cfarRank { | |||
| t.Fatalf("cfar rank: %v", updated.Detector.CFARRank) | |||
| } | |||
| if updated.Detector.CFARScaleDb != cfarScale { | |||
| t.Fatalf("cfar scale: %v", updated.Detector.CFARScaleDb) | |||
| } | |||
| if updated.TunerBwKHz != bw { | |||
| t.Fatalf("tuner bw: %v", updated.TunerBwKHz) | |||
| } | |||
| @@ -29,6 +29,11 @@ const gainRange = qs('gainRange'); | |||
| const gainInput = qs('gainInput'); | |||
| const thresholdRange = qs('thresholdRange'); | |||
| const thresholdInput = qs('thresholdInput'); | |||
| const cfarToggle = qs('cfarToggle'); | |||
| const cfarGuardInput = qs('cfarGuardInput'); | |||
| const cfarTrainInput = qs('cfarTrainInput'); | |||
| const cfarRankInput = qs('cfarRankInput'); | |||
| const cfarScaleInput = qs('cfarScaleInput'); | |||
| const emaAlphaInput = qs('emaAlphaInput'); | |||
| const hysteresisInput = qs('hysteresisInput'); | |||
| const stableFramesInput = qs('stableFramesInput'); | |||
| @@ -282,6 +287,11 @@ function applyConfigToUI(cfg) { | |||
| gainInput.value = uiGain; | |||
| thresholdRange.value = cfg.detector.threshold_db; | |||
| thresholdInput.value = cfg.detector.threshold_db; | |||
| if (cfarToggle) cfarToggle.checked = !!cfg.detector.cfar_enabled; | |||
| if (cfarGuardInput) cfarGuardInput.value = cfg.detector.cfar_guard_cells ?? 2; | |||
| if (cfarTrainInput) cfarTrainInput.value = cfg.detector.cfar_train_cells ?? 16; | |||
| if (cfarRankInput) cfarRankInput.value = cfg.detector.cfar_rank ?? 24; | |||
| if (cfarScaleInput) cfarScaleInput.value = cfg.detector.cfar_scale_db ?? 6; | |||
| if (minDurationInput) minDurationInput.value = cfg.detector.min_duration_ms; | |||
| if (holdInput) holdInput.value = cfg.detector.hold_ms; | |||
| if (emaAlphaInput) emaAlphaInput.value = cfg.detector.ema_alpha ?? 0.2; | |||
| @@ -1136,6 +1146,24 @@ thresholdInput.addEventListener('change', () => { | |||
| } | |||
| }); | |||
| if (cfarToggle) cfarToggle.addEventListener('change', () => queueConfigUpdate({ detector: { cfar_enabled: cfarToggle.checked } })); | |||
| if (cfarGuardInput) cfarGuardInput.addEventListener('change', () => { | |||
| const v = parseInt(cfarGuardInput.value, 10); | |||
| if (Number.isFinite(v)) queueConfigUpdate({ detector: { cfar_guard_cells: v } }); | |||
| }); | |||
| if (cfarTrainInput) cfarTrainInput.addEventListener('change', () => { | |||
| const v = parseInt(cfarTrainInput.value, 10); | |||
| if (Number.isFinite(v)) queueConfigUpdate({ detector: { cfar_train_cells: v } }); | |||
| }); | |||
| if (cfarRankInput) cfarRankInput.addEventListener('change', () => { | |||
| const v = parseInt(cfarRankInput.value, 10); | |||
| if (Number.isFinite(v)) queueConfigUpdate({ detector: { cfar_rank: v } }); | |||
| }); | |||
| if (cfarScaleInput) cfarScaleInput.addEventListener('change', () => { | |||
| const v = parseFloat(cfarScaleInput.value); | |||
| if (Number.isFinite(v)) queueConfigUpdate({ detector: { cfar_scale_db: v } }); | |||
| }); | |||
| agcToggle.addEventListener('change', () => queueSettingsUpdate({ agc: agcToggle.checked })); | |||
| dcToggle.addEventListener('change', () => queueSettingsUpdate({ dc_block: dcToggle.checked })); | |||
| iqToggle.addEventListener('change', () => queueSettingsUpdate({ iq_balance: iqToggle.checked })); | |||
| @@ -173,6 +173,10 @@ | |||
| <span>Threshold</span> | |||
| <div class="slider-row"><input id="thresholdRange" type="range" min="-120" max="0" step="1" class="range--warn" /><input id="thresholdInput" type="number" min="-120" max="0" step="1" class="slider-num" /><em>dB</em></div> | |||
| </div> | |||
| <label class="field"><span>CFAR Guard Cells</span><input id="cfarGuardInput" type="number" step="1" min="0" /></label> | |||
| <label class="field"><span>CFAR Train Cells</span><input id="cfarTrainInput" type="number" step="1" min="1" /></label> | |||
| <label class="field"><span>CFAR Rank</span><input id="cfarRankInput" type="number" step="1" min="1" /></label> | |||
| <label class="field"><span>CFAR Scale (dB)</span><input id="cfarScaleInput" type="number" step="0.5" min="0" /></label> | |||
| <label class="field"><span>Min Duration (ms)</span><input id="minDurationInput" type="number" step="50" min="50" /></label> | |||
| <label class="field"><span>Hold (ms)</span><input id="holdInput" type="number" step="50" min="50" /></label> | |||
| <label class="field"><span>Averaging</span> | |||
| @@ -186,6 +190,7 @@ | |||
| <label class="field"><span>Min Stable Frames</span><input id="stableFramesInput" type="number" step="1" min="1" /></label> | |||
| <label class="field"><span>Gap Tolerance (ms)</span><input id="gapToleranceInput" type="number" step="50" min="0" /></label> | |||
| <div class="toggle-grid"> | |||
| <label class="pill-toggle"><input id="cfarToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">CFAR</span></label> | |||
| <label class="pill-toggle"><input id="agcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">AGC</span></label> | |||
| <label class="pill-toggle"><input id="dcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">DC Block</span></label> | |||
| <label class="pill-toggle"><input id="iqToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">IQ Bal</span></label> | |||