|
25 | 25 | ) |
26 | 26 | from .sdr_wrappers import test_sdr, reset_sdr, get_sdr_name, get_sdr_iq_cmd, get_sdr_fm_cmd, get_power_spectrum, shutdown_sdr |
27 | 27 |
|
| 28 | +# Import async scanning for concurrent peak detection |
| 29 | +try: |
| 30 | + from .scan_async import run_async_scan |
| 31 | + ASYNC_SCAN_AVAILABLE = True |
| 32 | +except ImportError: |
| 33 | + ASYNC_SCAN_AVAILABLE = False |
| 34 | + logging.warning("Async scanning not available - falling back to sequential scanning") |
| 35 | + |
28 | 36 |
|
29 | 37 | try: |
30 | 38 | from .web import flask_emit_event |
@@ -680,7 +688,8 @@ def __init__( |
680 | 688 | temporary_block_list={}, |
681 | 689 | temporary_block_time=60, |
682 | 690 | ngp_tweak=False, |
683 | | - wideband_sondes=False |
| 691 | + wideband_sondes=False, |
| 692 | + max_async_scan_workers=4 |
684 | 693 | ): |
685 | 694 | """Initialise a Sonde Scanner Object. |
686 | 695 |
|
@@ -770,6 +779,7 @@ def __init__( |
770 | 779 | self.callback = callback |
771 | 780 | self.save_detection_audio = save_detection_audio |
772 | 781 | self.wideband_sondes = wideband_sondes |
| 782 | + self.max_async_scan_workers = max_async_scan_workers |
773 | 783 |
|
774 | 784 | # Temporary block list. |
775 | 785 | self.temporary_block_list = temporary_block_list.copy() |
@@ -1118,44 +1128,98 @@ def sonde_search(self, first_only=False): |
1118 | 1128 | ) |
1119 | 1129 |
|
1120 | 1130 | # Run rs_detect on each peak frequency, to determine if there is a sonde there. |
1121 | | - for freq in peak_frequencies: |
| 1131 | + # OPTIMIZATION: Use concurrent async scanning ONLY with KA9Q-radio |
| 1132 | + # KA9Q provides virtual SDR channels that can actually scan concurrently |
| 1133 | + if ASYNC_SCAN_AVAILABLE and self.sdr_type == "KA9Q" and len(peak_frequencies) > 1: |
| 1134 | + try: |
| 1135 | + import os |
| 1136 | + cpu_count = os.cpu_count() or 1 |
| 1137 | + |
| 1138 | + # With KA9Q, we can use multiple virtual channels concurrently |
| 1139 | + # The scanner creates temporary KA9Q channels that don't consume task slots |
| 1140 | + # Cap concurrency to min of: configured max, CPU cores, and number of peaks |
| 1141 | + max_concurrent = min( |
| 1142 | + self.max_async_scan_workers, |
| 1143 | + cpu_count, |
| 1144 | + len(peak_frequencies) |
| 1145 | + ) |
1122 | 1146 |
|
1123 | | - _freq = float(freq) |
| 1147 | + self.log_info(f"KA9Q: Using concurrent peak detection with {max_concurrent} workers") |
| 1148 | + |
| 1149 | + detections = run_async_scan( |
| 1150 | + peak_frequencies=peak_frequencies, |
| 1151 | + max_concurrent=max_concurrent, |
| 1152 | + rs_path=self.rs_path, |
| 1153 | + dwell_time=self.detect_dwell_time, |
| 1154 | + sdr_type=self.sdr_type, |
| 1155 | + sdr_hostname=self.sdr_hostname, |
| 1156 | + sdr_port=self.sdr_port, |
| 1157 | + ss_iq_path=self.ss_iq_path, |
| 1158 | + rtl_fm_path=self.rtl_fm_path, |
| 1159 | + rtl_device_idx=self.rtl_device_idx, |
| 1160 | + ppm=self.ppm, |
| 1161 | + gain=self.gain, |
| 1162 | + bias=self.bias, |
| 1163 | + save_detection_audio=self.save_detection_audio, |
| 1164 | + wideband_sondes=self.wideband_sondes |
| 1165 | + ) |
1124 | 1166 |
|
1125 | | - # Exit opportunity. |
1126 | | - if self.sonde_scanner_running == False: |
1127 | | - return [] |
| 1167 | + # Process results |
| 1168 | + for _freq, detected in detections: |
| 1169 | + if self.sonde_scanner_running == False: |
| 1170 | + return [] |
1128 | 1171 |
|
1129 | | - (detected, offset_est) = detect_sonde( |
1130 | | - _freq, |
1131 | | - sdr_type=self.sdr_type, |
1132 | | - sdr_hostname=self.sdr_hostname, |
1133 | | - sdr_port=self.sdr_port, |
1134 | | - ss_iq_path = self.ss_iq_path, |
1135 | | - rtl_fm_path=self.rtl_fm_path, |
1136 | | - rtl_device_idx=self.rtl_device_idx, |
1137 | | - ppm=self.ppm, |
1138 | | - gain=self.gain, |
1139 | | - bias=self.bias, |
1140 | | - dwell_time=self.detect_dwell_time, |
1141 | | - save_detection_audio=self.save_detection_audio, |
1142 | | - wideband_sondes=self.wideband_sondes |
1143 | | - ) |
| 1172 | + _search_results.append([_freq, detected]) |
| 1173 | + self.send_to_callback([[_freq, detected]]) |
| 1174 | + |
| 1175 | + if first_only: |
| 1176 | + return _search_results |
| 1177 | + |
| 1178 | + except Exception as e: |
| 1179 | + import traceback |
| 1180 | + self.log_error(f"Async scanning failed: {e}, falling back to sequential") |
| 1181 | + self.log_debug(f"Async scan traceback: {traceback.format_exc()}") |
| 1182 | + |
| 1183 | + # Standard sequential scanning (for RTLSDR, SpyServer, or single peaks) |
| 1184 | + else: |
| 1185 | + for freq in peak_frequencies: |
| 1186 | + |
| 1187 | + _freq = float(freq) |
| 1188 | + |
| 1189 | + # Exit opportunity. |
| 1190 | + if self.sonde_scanner_running == False: |
| 1191 | + return [] |
| 1192 | + |
| 1193 | + (detected, offset_est) = detect_sonde( |
| 1194 | + _freq, |
| 1195 | + sdr_type=self.sdr_type, |
| 1196 | + sdr_hostname=self.sdr_hostname, |
| 1197 | + sdr_port=self.sdr_port, |
| 1198 | + ss_iq_path = self.ss_iq_path, |
| 1199 | + rtl_fm_path=self.rtl_fm_path, |
| 1200 | + rtl_device_idx=self.rtl_device_idx, |
| 1201 | + ppm=self.ppm, |
| 1202 | + gain=self.gain, |
| 1203 | + bias=self.bias, |
| 1204 | + dwell_time=self.detect_dwell_time, |
| 1205 | + save_detection_audio=self.save_detection_audio, |
| 1206 | + wideband_sondes=self.wideband_sondes |
| 1207 | + ) |
1144 | 1208 |
|
1145 | | - if detected != None: |
1146 | | - # Quantize the detected frequency (with offset) to 1 kHz |
1147 | | - _freq = round((_freq + offset_est) / 1000.0) * 1000.0 |
| 1209 | + if detected != None: |
| 1210 | + # Quantize the detected frequency (with offset) to 1 kHz |
| 1211 | + _freq = round((_freq + offset_est) / 1000.0) * 1000.0 |
1148 | 1212 |
|
1149 | | - # Add a detected sonde to the output array |
1150 | | - _search_results.append([_freq, detected]) |
| 1213 | + # Add a detected sonde to the output array |
| 1214 | + _search_results.append([_freq, detected]) |
1151 | 1215 |
|
1152 | | - # Immediately send this result to the callback. |
1153 | | - self.send_to_callback([[_freq, detected]]) |
1154 | | - # If we only want the first detected sonde, then return now. |
1155 | | - if first_only: |
1156 | | - return _search_results |
| 1216 | + # Immediately send this result to the callback. |
| 1217 | + self.send_to_callback([[_freq, detected]]) |
| 1218 | + # If we only want the first detected sonde, then return now. |
| 1219 | + if first_only: |
| 1220 | + return _search_results |
1157 | 1221 |
|
1158 | | - # Otherwise, we continue.... |
| 1222 | + # Otherwise, we continue.... |
1159 | 1223 |
|
1160 | 1224 | if len(_search_results) == 0: |
1161 | 1225 | self.log_debug("No sondes detected.") |
|
0 commit comments