| @@ -363,6 +363,7 @@ func runDSP(ctx context.Context, src sdr.Source, cfg config.Config, det *detecto | |||
| dcEnabled := cfg.DCBlock | |||
| iqEnabled := cfg.IQBalance | |||
| gotSamples := false | |||
| for { | |||
| select { | |||
| case <-ctx.Done(): | |||
| @@ -385,6 +386,10 @@ func runDSP(ctx context.Context, src sdr.Source, cfg config.Config, det *detecto | |||
| log.Printf("read IQ: %v", err) | |||
| continue | |||
| } | |||
| if !gotSamples { | |||
| log.Printf("received IQ samples") | |||
| gotSamples = true | |||
| } | |||
| if dcEnabled { | |||
| dcBlocker.Apply(iq) | |||
| } | |||
| @@ -70,6 +70,7 @@ import ( | |||
| "fmt" | |||
| "runtime/cgo" | |||
| "sync" | |||
| "time" | |||
| "unsafe" | |||
| "sdr-visual-suite/internal/sdr" | |||
| @@ -134,6 +135,11 @@ func (s *Source) configure(sampleRate int, centerHz float64, gainDb float64) err | |||
| if err := cErr(C.sdrplay_api_Init(s.dev.dev, &cb, unsafe.Pointer(uintptr(s.handle)))); err != nil { | |||
| return fmt.Errorf("sdrplay_api_Init: %w", err) | |||
| } | |||
| // Apply initial settings explicitly to ensure streaming starts. | |||
| updateReasons := C.int(C.sdrplay_api_Update_Dev_Fs | C.sdrplay_api_Update_Tuner_Frf | C.sdrplay_api_Update_Tuner_Gr | C.sdrplay_api_Update_Ctrl_Agc) | |||
| if err := cErr(C.sdrplay_update(unsafe.Pointer(s.dev.dev), updateReasons)); err != nil { | |||
| return fmt.Errorf("sdrplay_api_Update: %w", err) | |||
| } | |||
| return nil | |||
| } | |||
| @@ -200,11 +206,15 @@ func (s *Source) Stop() error { | |||
| } | |||
| func (s *Source) ReadIQ(n int) ([]complex64, error) { | |||
| buf := <-s.ch | |||
| if len(buf) >= n { | |||
| return buf[:n], nil | |||
| select { | |||
| case buf := <-s.ch: | |||
| if len(buf) >= n { | |||
| return buf[:n], nil | |||
| } | |||
| return buf, nil | |||
| case <-time.After(1500 * time.Millisecond): | |||
| return nil, errors.New("timeout waiting for IQ samples") | |||
| } | |||
| return buf, nil | |||
| } | |||
| //export goStreamCallback | |||
| @@ -16,6 +16,7 @@ const detailSpectrogram = document.getElementById('detailSpectrogram'); | |||
| const configStatusEl = document.getElementById('configStatus'); | |||
| const centerInput = document.getElementById('centerInput'); | |||
| const spanInput = document.getElementById('spanInput'); | |||
| const sampleRateSelect = document.getElementById('sampleRateSelect'); | |||
| const fftSelect = document.getElementById('fftSelect'); | |||
| const gainRange = document.getElementById('gainRange'); | |||
| const gainInput = document.getElementById('gainInput'); | |||
| @@ -87,7 +88,11 @@ function applyConfigToUI(cfg) { | |||
| if (!cfg) return; | |||
| isSyncingConfig = true; | |||
| centerInput.value = toMHz(cfg.center_hz).toFixed(6); | |||
| spanInput.value = toMHz(cfg.sample_rate).toFixed(3); | |||
| if (sampleRateSelect) { | |||
| sampleRateSelect.value = toMHz(cfg.sample_rate).toFixed(3).replace(/\.0+$/, '').replace(/\.$/, ''); | |||
| } | |||
| const spanMHz = toMHz(cfg.sample_rate / zoom); | |||
| spanInput.value = spanMHz.toFixed(3); | |||
| fftSelect.value = String(cfg.fft_size); | |||
| gainRange.value = cfg.gain_db; | |||
| gainInput.value = cfg.gain_db; | |||
| @@ -214,6 +219,9 @@ function renderSpectrum() { | |||
| const span = sample_rate / zoom; | |||
| const startHz = center_hz - span / 2 + pan * span; | |||
| const endHz = center_hz + span / 2 + pan * span; | |||
| if (!isSyncingConfig && spanInput) { | |||
| spanInput.value = (span / 1e6).toFixed(3); | |||
| } | |||
| const minDb = -120; | |||
| const maxDb = 0; | |||
| @@ -465,11 +473,22 @@ centerInput.addEventListener('change', () => { | |||
| spanInput.addEventListener('change', () => { | |||
| const mhz = parseFloat(spanInput.value); | |||
| if (Number.isFinite(mhz) && mhz > 0) { | |||
| queueConfigUpdate({ sample_rate: Math.round(fromMHz(mhz)) }); | |||
| } | |||
| if (!Number.isFinite(mhz) || mhz <= 0) return; | |||
| const baseRate = currentConfig ? currentConfig.sample_rate : (latest ? latest.sample_rate : 0); | |||
| if (!baseRate) return; | |||
| zoom = Math.max(0.25, Math.min(20, baseRate / fromMHz(mhz))); | |||
| timelineDirty = true; | |||
| }); | |||
| if (sampleRateSelect) { | |||
| sampleRateSelect.addEventListener('change', () => { | |||
| const mhz = parseFloat(sampleRateSelect.value); | |||
| if (Number.isFinite(mhz) && mhz > 0) { | |||
| queueConfigUpdate({ sample_rate: Math.round(fromMHz(mhz)) }); | |||
| } | |||
| }); | |||
| } | |||
| fftSelect.addEventListener('change', () => { | |||
| const size = parseInt(fftSelect.value, 10); | |||
| if (Number.isFinite(size)) { | |||
| @@ -30,7 +30,20 @@ | |||
| <label class="control-label" for="spanInput">Span (MHz)</label> | |||
| <div class="control-row"> | |||
| <input id="spanInput" type="number" step="0.1" min="0.1" /> | |||
| <input id="spanInput" type="number" step="0.05" min="0.05" /> | |||
| </div> | |||
| <label class="control-label" for="sampleRateSelect">Sample Rate (MHz)</label> | |||
| <div class="control-row"> | |||
| <select id="sampleRateSelect"> | |||
| <option value="0.5">0.5</option> | |||
| <option value="1.0">1.0</option> | |||
| <option value="1.536">1.536</option> | |||
| <option value="2.048">2.048</option> | |||
| <option value="2.5">2.5</option> | |||
| <option value="3.072">3.072</option> | |||
| <option value="4.096">4.096</option> | |||
| </select> | |||
| </div> | |||
| <label class="control-label" for="fftSelect">FFT Size</label> | |||