FM Detection and Debug View (Againnnnnn)

FM Receiver App – Week 6 Update

Note: This week’s focus was more on the scanner, little/No progress towards the debug view.

This week, I made progress in the following features:

  1. Enhanced and Optimized the Scanner
  2. Added some features to debug view –> not complete
  3. Added features like saving configurations, mute feature

Optimized Scanner

Last Week’s Scanner

In order to understand how much better the new implementation is, we need to discuss the old implementation first.

  1. Non-GRC Approach
    The detection logic is a Python function. The FM Scanner flowgraph would take a snapshot of the spectrum at center frequency and close, all detection logic was handled in the Python function.

  2. Sharing SDR Resources
    Using del to remove all instances of the flowgraph variable in Python in order to detach the SDR kernel before running the FM scanner flowgraph; because only one flowgraph can access the SDR resource.

New Scanner

The new scanner uses a selector block to move the SDR stream between both modes, Scan and Play. I also moved the detection logic into a Python embedded Block.

The detection block is a sink block with a pmt message done to indicate completion of scanning the current center frequency; and as long as the done flag is true, the detection logic will not use new SDR samples.

    def work(self, input_items, output_items):

        if self.done == 1: 
            return len(input_items[0])  # Done working reset the flag
        
        self.data = np.concatenate((self.data, input_items[0])) # Accumulate samples

        if len(self.data) < self.num_items:
            return len(input_items[0])
        
        data = self.data

        self.compute_candidate_freqs() 

        for i in range(0, len(data), self.fft_size):
            if i + self.fft_size > len(data):
                break
                
            data_chunk = data[i:i+self.fft_size]
            
            for j, station_bin in enumerate(self.candidate_freqs_bin):
                start_bin = int(station_bin - self.half_station_size)
                end_bin = int(station_bin + self.half_station_size)
                
                # Ensure we don't go out of bounds
                start_bin = max(0, start_bin)
                end_bin = min(len(data_chunk), end_bin)
                
                if start_bin < end_bin:
                    potential_station = np.sum(np.abs(data_chunk[start_bin:end_bin])**2)
                    self.power_per_station[j] += potential_station


        normalized_power_per_station = self.normalize(self.power_per_station)

        # Find active stations
        active_indices = np.where(normalized_power_per_station > self.threshold)[0]

        # Group adjacent active indices
        groups = []
        if len(active_indices) > 0:
            group = [active_indices[0]]
            for idx in active_indices[1:]:
                if idx == group[-1] + 1:
                    group.append(idx)
                else:
                    groups.append(group)
                    group = [idx]
            groups.append(group)  # append the last group

        # Pick max power freq in each group
        for group in groups:
            max_idx = group[np.argmax(normalized_power_per_station[group])]
  
            self.detected_stations.add(float(self.candidate_freqs[max_idx]))

        self.done = 1
        msg = pmt.cons(pmt.intern("value"), pmt.from_double(1))
        self.message_port_pub(pmt.intern("done"), msg)

        self.clean_up()
        	
        return len(input_items[0])

In order to monitor everything without halting the application, I created a QThread in src/fm_receiver/gui/scan_thread.py. This will let the application run while scanning by continuously monitoring the scanning progress.

    def run(self):
        freq = self.start_freq
        logger.info("Running scanning monitor")

        while self._is_running:
            while self.fm_receiver.get_done() == 0:
                if not self._is_running:
                    return
                QThread.msleep(10)  # Don't hog the CPU
            logger.info(f"Scanning {freq}")
            freq += 1e6
            if freq > self.end_freq:
                break

            self.progress.emit(freq)
            
            while self.fm_receiver.get_done() == 1:
                if not self._is_running:
                    return
                QThread.msleep(10)

        # Post-scan logic
        self.finished.emit(True)

Finally everything came together, after the scan is complete the main window handles the finish event by updating the station list and enabling the disabled buttons that prevented the user to listen to the audio during scanning process.

New Features

  1. Added a mute feature that multiplies the volume by 0 if mute is on.
  1. Added a volume slider that maps volume from 0 to 100 to -20 to 10 (since it was configured like that in the RDS flowgraph)

  2. Added logic to src/fm_receiver/core/config_manager.py to handle saving stations

  3. Allowed control of the cutoff frequency and transition bandwidth of the baseband FIR filter

Debug View

I am still working on the debug view, not much progress has been made since last week here. Just a change of UI widgets to allow control of flowgraph parameters along with visualizing how these parameters change different plots. In the image below you can now control the Frequency Xalting FIR, but still nothing more.

Problems

My problems are becoming more silly. The thing that drives me crazy is that when running the flowgraph, GNU Radio automatically edits the flowgraph.py to import the embedded Python block like this import block as blk so I have to manually edit the line to from . import block as blk which I know is silly but is still a problem.

Other real problems include, that I want to reuse same widgets like RDS panel and radio control buttons (next/prev/listen) on different views, but PyQt allows a widget to only have one parent. Therefore I have two options, either to copy each widget and have multiple same widgets or I move widgets around according to how the user navigates the site. I also have another option, which is to remove the home page, and make all radio controls stick to the main window regardless of which page the user is on.

What’s Next?

For next week, I’m planning to:

  • Polish frontend
  • Debug View
  • Add more features: recording, config manager

Links




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • End of My GSoC journey
  • Documentation and Soapy Sdr source
  • Multiple Recording Feature
  • SDR Device and Code Documenting
  • Record Feature