diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..49a589a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.claude/ +*.o diff --git a/Makefile b/Makefile index 0f3b1191..815daac2 100644 --- a/Makefile +++ b/Makefile @@ -202,7 +202,7 @@ CENTRAL_MENU_ALL4 := $(FOLDER_CENTRAL_MENU)/menu_vehicle_data_link.o $(FOLDER_CE CENTRAL_MENU_ALL5 := $(FOLDER_CENTRAL_MENU)/menu_system_dev_stats.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_radio_link.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_radio_interface.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_management_plugins.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_peripherals.o $(FOLDER_CENTRAL_MENU)/menu_confirmation_delete_logs.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_radio.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_cpu_oipc.o $(FOLDER_CENTRAL_MENU)/menu_confirmation_vehicle_board.o CENTRAL_MENU_ALL6 := $(FOLDER_CENTRAL_MENU)/menu_controller_radio.o $(FOLDER_CENTRAL_MENU)/menu_confirmation_video_rate.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_simplesetup.o $(FOLDER_CENTRAL_MENU)/menu_confirmation_sdcard_update.o $(FOLDER_CENTRAL_MENU)/menu_controller_update.o $(FOLDER_CENTRAL_MENU)/menu_controller_update_net.o $(FOLDER_CENTRAL_MENU)/menu_quick_menu.o $(FOLDER_CENTRAL_MENU)/menu_quick_menu_settings.o CENTRAL_MENU_RC := $(FOLDER_CENTRAL_MENU)/menu_vehicle_rc.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_rc_failsafe.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_rc_channels.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_rc_expo.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_rc_camera.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_rc_input.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_functions.o -CENTRAL_MENU_RADIO := $(FOLDER_CENTRAL_MENU)/menu_controller_radio_interface_sik.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_radio_link_sik.o $(FOLDER_CENTRAL_MENU)/menu_diagnose_radio_link.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_radio_link_elrs.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_radio_pit.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_radio_rt_capab.o +CENTRAL_MENU_RADIO := $(FOLDER_CENTRAL_MENU)/menu_controller_radio_interface_sik.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_radio_link_sik.o $(FOLDER_CENTRAL_MENU)/menu_diagnose_radio_link.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_radio_link_elrs.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_radio_pit.o $(FOLDER_CENTRAL_MENU)/menu_vehicle_radio_rt_capab.o $(FOLDER_CENTRAL_MENU)/menu_rf_scan.o CENTRAL_POPUP_ALL := $(FOLDER_CENTRAL)/popup.o $(FOLDER_CENTRAL)/popup_log.o $(FOLDER_CENTRAL)/popup_commands.o $(FOLDER_CENTRAL)/popup_camera_params.o $(FOLDER_CENTRAL)/popup_radio_int.o CENTRAL_RENDER_ALL := $(FOLDER_CENTRAL)/colors.o $(FOLDER_CENTRAL)/render_commands.o $(FOLDER_CENTRAL)/render_joysticks.o $(FOLDER_CENTRAL)/process_router_messages.o $(FOLDER_CENTRAL)/video_playback.o CENTRAL_OSD_ALL := $(FOLDER_CENTRAL_OSD)/osd_common.o $(FOLDER_CENTRAL_OSD)/osd.o $(FOLDER_CENTRAL_OSD)/osd_stats.o $(FOLDER_CENTRAL_OSD)/osd_debug_stats.o $(FOLDER_CENTRAL_OSD)/osd_ahi.o $(FOLDER_CENTRAL_OSD)/osd_lean.o $(FOLDER_CENTRAL_OSD)/osd_warnings.o $(FOLDER_CENTRAL_OSD)/osd_gauges.o $(FOLDER_CENTRAL_OSD)/osd_plugins.o $(FOLDER_CENTRAL_OSD)/osd_stats_dev.o $(FOLDER_CENTRAL_OSD)/osd_stats_video_bitrate.o $(FOLDER_CENTRAL_OSD)/osd_links.o $(FOLDER_CENTRAL_OSD)/osd_stats_radio.o $(FOLDER_CENTRAL_OSD)/osd_widgets.o $(FOLDER_CENTRAL_OSD)/osd_widgets_builtin.o $(FOLDER_BASE)/vehicle_rt_info.o @@ -278,7 +278,7 @@ ruby_rx_telemetry: $(FOLDER_STATION)/ruby_rx_telemetry.o $(MODULE_BASE) $(MODULE ruby_tx_rc: $(FOLDER_STATION)/ruby_tx_rc.o $(MODULE_BASE) $(MODULE_BASE2) $(MODULE_COMMON) $(MODULE_RADIO) $(MODULE_MODELS) $(MODULE_STATION) $(FOLDER_BASE)/shared_mem_i2c.o $(FOLDER_BASE)/hardware_audio.o $(FOLDER_BASE)/wiringPiI2C_radxa.o $(CXX) $(_CPPFLAGS) -o $@ $^ $(_LDFLAGS) -ruby_rt_station: $(FOLDER_STATION)/ruby_rt_station.o $(MODULE_BASE) $(MODULE_BASE2) $(MODULE_COMMON) $(MODULE_RADIO) $(MODULE_MODELS) $(MODULE_STATION) $(FOLDER_STATION)/packets_utils.o $(FOLDER_STATION)/process_local_packets.o $(FOLDER_STATION)/process_radio_in_packets.o $(FOLDER_STATION)/process_radio_out_packets.o $(FOLDER_STATION)/periodic_loop.o $(FOLDER_STATION)/processor_rx_audio.o $(FOLDER_STATION)/processor_rx_video.o $(FOLDER_STATION)/video_rx_buffers.o $(FOLDER_STATION)/radio_links.o $(FOLDER_STATION)/relay_rx.o $(FOLDER_STATION)/test_link_params.o $(FOLDER_STATION)/process_video_packets.o $(FOLDER_STATION)/rx_video_output.o $(FOLDER_STATION)/rx_video_recording.o $(FOLDER_STATION)/rx_video_recording_data.o $(FOLDER_BASE)/shared_mem_controller_only.o $(FOLDER_COMMON)/models_connect_frequencies.o $(FOLDER_BASE)/parse_fc_telemetry.o $(FOLDER_BASE)/parse_fc_telemetry_ltm.o $(FOLDER_STATION)/radio_links_sik.o $(FOLDER_BASE)/radio_utils.o $(FOLDER_BASE)/core_plugins_settings.o $(FOLDER_BASE)/camera_utils.o \ +ruby_rt_station: $(FOLDER_STATION)/ruby_rt_station.o $(MODULE_BASE) $(MODULE_BASE2) $(MODULE_COMMON) $(MODULE_RADIO) $(MODULE_MODELS) $(MODULE_STATION) $(FOLDER_STATION)/packets_utils.o $(FOLDER_STATION)/process_local_packets.o $(FOLDER_STATION)/process_radio_in_packets.o $(FOLDER_STATION)/process_radio_out_packets.o $(FOLDER_STATION)/periodic_loop.o $(FOLDER_STATION)/rf_scan.o $(FOLDER_STATION)/processor_rx_audio.o $(FOLDER_STATION)/processor_rx_video.o $(FOLDER_STATION)/video_rx_buffers.o $(FOLDER_STATION)/radio_links.o $(FOLDER_STATION)/relay_rx.o $(FOLDER_STATION)/test_link_params.o $(FOLDER_STATION)/process_video_packets.o $(FOLDER_STATION)/rx_video_output.o $(FOLDER_STATION)/rx_video_recording.o $(FOLDER_STATION)/rx_video_recording_data.o $(FOLDER_BASE)/shared_mem_controller_only.o $(FOLDER_COMMON)/models_connect_frequencies.o $(FOLDER_BASE)/parse_fc_telemetry.o $(FOLDER_BASE)/parse_fc_telemetry_ltm.o $(FOLDER_STATION)/radio_links_sik.o $(FOLDER_BASE)/radio_utils.o $(FOLDER_BASE)/core_plugins_settings.o $(FOLDER_BASE)/camera_utils.o \ $(FOLDER_BASE)/parser_h264.o $(FOLDER_BASE)/tx_powers.o $(FOLDER_UTILS)/utils_controller.o $(FOLDER_UTILS)/utils_vehicle.o $(FOLDER_STATION)/generic_rx_ecbuffers.o $(FOLDER_BASE)/hardware_audio.o $(FOLDER_BASE)/wiringPiI2C_radxa.o $(FOLDER_BASE)/msp.o $(CXX) $(_CPPFLAGS) -o $@ $^ $(_LDFLAGS) -ldl diff --git a/code/r_central/menu/menu_objects.h b/code/r_central/menu/menu_objects.h index 56a3662a..96f0c2ef 100644 --- a/code/r_central/menu/menu_objects.h +++ b/code/r_central/menu/menu_objects.h @@ -124,6 +124,7 @@ #define MENU_ID_VEHICLE_VIDEO_COMPARE 137 #define MENU_ID_QUICK_MENU_SETTINGS 138 #define MENU_ID_QUICK_MENU 139 +#define MENU_ID_RF_SCAN 140 #define MAX_MENU_ITEMS 150 diff --git a/code/r_central/menu/menu_rf_scan.cpp b/code/r_central/menu/menu_rf_scan.cpp new file mode 100644 index 00000000..c86062ee --- /dev/null +++ b/code/r_central/menu/menu_rf_scan.cpp @@ -0,0 +1,464 @@ +/* + Ruby Licence + Copyright (c) 2020-2025 Petru Soroaga petrusoroaga@yahoo.com + All rights reserved. + + Redistribution and/or use in source and/or binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions and/or use of the source code (partially or complete) must retain + the above copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the distribution. + * Redistributions in binary form (partially or complete) must reproduce + the above copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the distribution. + * Copyright info and developer info must be preserved as is in the user + interface, additions could be made to that info. + * Neither the name of the organization nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + * Military use is not permitted. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE AUTHOR (PETRU SOROAGA) BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "menu.h" +#include "menu_rf_scan.h" +#include "menu_item_select.h" +#include "../../base/config_radio.h" +#include "../../base/hardware_radio.h" +#include "../../common/string_utils.h" + +static MenuRFScan* s_pLastRFScanMenu = NULL; + +MenuRFScan::MenuRFScan(int iVehicleRadioLink, u32 uFreqKhz) +:Menu(MENU_ID_RF_SCAN, "RF Spectrum Scan", NULL) +{ + m_Width = 0.92f; + m_xPos = 0.04f; + m_yPos = 0.04f; + + m_iVehicleRadioLink = iVehicleRadioLink; + m_uCurrentFreqKhz = uFreqKhz; + m_uBandFlags = getBand(uFreqKhz); + + m_iScanChannelsCount = 0; + m_iChannelsScanned = 0; + m_uBestFreqKhz = 0; + m_iBestChannelIndex = -1; + m_bScanInProgress = false; + m_bScanComplete = false; + m_bNoSurveyData = false; + + for ( int i = 0; i < RF_SCAN_MAX_CHANNELS; i++ ) + m_iBusyPct[i] = RF_SCAN_BUSY_NOT_SCANNED; + + // Build channel list for this band + u32* pChannels = NULL; + int nCount = 0; + if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_58 ) { pChannels = getChannels58(); nCount = getChannels58Count(); } + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_24 ) { pChannels = getChannels24(); nCount = getChannels24Count(); } + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_23 ) { pChannels = getChannels23(); nCount = getChannels23Count(); } + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_25 ) { pChannels = getChannels25(); nCount = getChannels25Count(); } + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_433 ) { pChannels = getChannels433(); nCount = getChannels433Count(); } + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_868 ) { pChannels = getChannels868(); nCount = getChannels868Count(); } + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_915 ) { pChannels = getChannels915(); nCount = getChannels915Count(); } + + if ( pChannels && nCount > 0 ) + { + if ( nCount > RF_SCAN_MAX_CHANNELS ) + nCount = RF_SCAN_MAX_CHANNELS; + for ( int i = 0; i < nCount; i++ ) + m_uScanChannels[i] = pChannels[i]; + m_iScanChannelsCount = nCount; + } + + // Reserve space for the spectrum chart at the bottom + float height_text = g_pRenderEngine->textHeight(g_idFontMenu); + addExtraHeightAtEnd(height_text * 12.0f); + + m_IndexRescan = addMenuItem(new MenuItem("Start Scan", "Scan all channels in this band and measure how busy each one is. This interrupts the radio link (video, telemetry and RC) for the duration of the scan.")); + m_IndexUseRecommended = addMenuItem(new MenuItem("Use Recommended Channel", "Apply the cleanest channel to this radio link.")); + m_IndexCancel = addMenuItem(new MenuItem("Cancel", "Close without changing the frequency.")); + + enableMenuItem(m_IndexUseRecommended, false); + + char szTitle[128]; + const char* szBand = "Unknown"; + if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_58 ) szBand = "5.8 GHz"; + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_24 ) szBand = "2.4 GHz"; + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_23 ) szBand = "2.3 GHz"; + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_25 ) szBand = "2.5 GHz"; + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_433 ) szBand = "433 MHz"; + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_868 ) szBand = "868 MHz"; + else if ( m_uBandFlags == RADIO_HW_SUPPORTED_BAND_915 ) szBand = "915 MHz"; + snprintf(szTitle, sizeof(szTitle), "RF Spectrum Scan - %s - %d channels", szBand, m_iScanChannelsCount); + setTitle(szTitle); + + s_pLastRFScanMenu = this; +} + +MenuRFScan::~MenuRFScan() +{ + // Safety net: if the menu is destroyed while a scan is running, tell the + // router to abort and restore the radio links. + if ( m_bScanInProgress ) + _stopScan(); + if ( s_pLastRFScanMenu == this ) + s_pLastRFScanMenu = NULL; +} + +void MenuRFScan::onShow() +{ + Menu::onShow(); + // Do NOT auto-start the scan: scanning drops the link to the vehicle + // (video + telemetry + RC) for several seconds. The user must explicitly + // press "Start Scan". +} + +void MenuRFScan::_startScan() +{ + for ( int i = 0; i < m_iScanChannelsCount; i++ ) + m_iBusyPct[i] = RF_SCAN_BUSY_NOT_SCANNED; + m_iChannelsScanned = 0; + m_uBestFreqKhz = 0; + m_iBestChannelIndex = -1; + m_bScanInProgress = true; + m_bScanComplete = false; + m_bNoSurveyData = false; + + enableMenuItem(m_IndexUseRecommended, false); + m_pMenuItems[m_IndexUseRecommended]->setTitle("Use Recommended Channel"); + + send_control_message_to_router(PACKET_TYPE_LOCAL_CONTROLLER_RF_SCAN_START, (u32)m_uBandFlags); + log_line("MenuRFScan: sent scan start for band flags %d, %d channels.", m_uBandFlags, m_iScanChannelsCount); +} + +void MenuRFScan::_stopScan() +{ + m_bScanInProgress = false; + send_control_message_to_router(PACKET_TYPE_LOCAL_CONTROLLER_RF_SCAN_STOP, 0); + log_line("MenuRFScan: sent scan stop."); +} + +bool MenuRFScan::periodicLoop() +{ + if ( m_bScanInProgress ) + _readResultsFile(); + return false; +} + +void MenuRFScan::_readResultsFile() +{ + FILE* f = fopen(RF_SCAN_RESULTS_FILE, "r"); + if ( NULL == f ) + return; + + u32 uFreqKhz = 0; + int iBusy = 0; + while ( fscanf(f, "%u %d", &uFreqKhz, &iBusy) == 2 ) + { + for ( int i = 0; i < m_iScanChannelsCount; i++ ) + { + if ( m_uScanChannels[i] == uFreqKhz ) + { + m_iBusyPct[i] = iBusy; + break; + } + } + } + fclose(f); + + m_iChannelsScanned = 0; + for ( int i = 0; i < m_iScanChannelsCount; i++ ) + if ( m_iBusyPct[i] != RF_SCAN_BUSY_NOT_SCANNED ) + m_iChannelsScanned++; + + if ( m_iChannelsScanned >= m_iScanChannelsCount && m_iScanChannelsCount > 0 ) + { + m_bScanInProgress = false; + m_bScanComplete = true; + + int bestBusy = 0; + for ( int i = 0; i < m_iScanChannelsCount; i++ ) + { + if ( m_iBusyPct[i] < 0 ) // not scanned or no usable survey data + continue; + if ( m_iBestChannelIndex < 0 || m_iBusyPct[i] < bestBusy ) + { + bestBusy = m_iBusyPct[i]; + m_iBestChannelIndex = i; + m_uBestFreqKhz = m_uScanChannels[i]; + } + } + + if ( m_iBestChannelIndex >= 0 ) + { + enableMenuItem(m_IndexUseRecommended, true); + char szLabel[128]; + snprintf(szLabel, sizeof(szLabel), "Use Recommended: %s", str_format_frequency(m_uBestFreqKhz)); + m_pMenuItems[m_IndexUseRecommended]->setTitle(szLabel); + } + else + { + // The radio card did not report channel-survey data for any channel. + m_bNoSurveyData = true; + } + } +} + +void MenuRFScan::Render() +{ + RenderPrepare(); + float yTop = RenderFrameAndTitle(); + float y = yTop; + for ( int i = 0; i < m_ItemsCount; i++ ) + y += RenderItem(i, y); + + float height_text_s = g_pRenderEngine->textHeight(g_idFontMenuSmall); + + // Status line + char szStatus[256]; + if ( m_bScanInProgress ) + snprintf(szStatus, sizeof(szStatus), "Scanning... %d / %d channels", m_iChannelsScanned, m_iScanChannelsCount); + else if ( m_bNoSurveyData ) + snprintf(szStatus, sizeof(szStatus), "Scan complete, but this radio card does not report channel survey data. No recommendation available."); + else if ( m_bScanComplete ) + snprintf(szStatus, sizeof(szStatus), "Scan complete | %d channels | Cleanest: %s", + m_iScanChannelsCount, m_uBestFreqKhz ? str_format_frequency(m_uBestFreqKhz) : "N/A"); + else + snprintf(szStatus, sizeof(szStatus), "Press Start Scan to begin | %d channels in band", m_iScanChannelsCount); + + g_pRenderEngine->setColors(get_Color_MenuText()); + g_pRenderEngine->drawText(m_RenderXPos + m_sfMenuPaddingX, y, g_idFontMenuSmall, szStatus); + y += height_text_s * 1.5f; + + // Spectrum chart + float fChartH = m_RenderYPos + m_RenderTotalHeight - y - m_sfMenuPaddingY; + if ( fChartH > 0.05f ) + _renderSpectrumChart(m_RenderXPos + m_sfMenuPaddingX, y, + m_RenderWidth - 2.0f * m_sfMenuPaddingX, fChartH); + + RenderEnd(yTop); +} + +void MenuRFScan::_renderSpectrumChart(float xPos, float yPos, float fWidth, float fHeight) +{ + if ( m_iScanChannelsCount == 0 ) + return; + + float wPx = g_pRenderEngine->getPixelWidth(); + float hPx = g_pRenderEngine->getPixelHeight(); + float hTs = g_pRenderEngine->textHeight(g_idFontMenuSmall); + + // Reserve left margin for Y-axis labels and bottom for freq labels + float fLabelW = g_pRenderEngine->textWidth(g_idFontMenuSmall, "100%"); + float fGrX = xPos + fLabelW + wPx * 4; + float fGrW = fWidth - fLabelW - wPx * 4; + float fGrY = yPos; + float fGrH = fHeight - hTs * 1.6f; + float fGrB = fGrY + fGrH; + + // Y range: channel busy ratio, 0% (bottom, clean) to 100% (top, congested). + // Shorter bar = less busy = cleaner channel. + + // Grid lines + Y labels + int yTicks[] = { 100, 80, 60, 40, 20, 0 }; + for ( int t = 0; t < 6; t++ ) + { + float frac = (float)yTicks[t] / 100.0f; + float gy = fGrB - frac * fGrH; + + g_pRenderEngine->setStroke(0.22f, 0.25f, 0.35f, 1.0f); + g_pRenderEngine->setStrokeSize(1); + g_pRenderEngine->drawLine(fGrX, gy, fGrX + fGrW, gy); + + char szLbl[16]; + snprintf(szLbl, sizeof(szLbl), "%d%%", yTicks[t]); + g_pRenderEngine->setColors(get_Color_MenuItemDisabledText()); + g_pRenderEngine->drawTextLeft(fGrX - wPx * 3, gy - hTs * 0.5f, g_idFontMenuSmall, szLbl); + } + + // Axes + g_pRenderEngine->setStroke(0.35f, 0.38f, 0.52f, 1.0f); + g_pRenderEngine->setStrokeSize(1); + g_pRenderEngine->drawLine(fGrX, fGrY, fGrX, fGrB); + g_pRenderEngine->drawLine(fGrX, fGrB, fGrX + fGrW, fGrB); + + // Bars + float barW = fGrW / (float)m_iScanChannelsCount; + float gap = barW * 0.10f; + if ( gap < wPx ) gap = wPx; + + for ( int i = 0; i < m_iScanChannelsCount; i++ ) + { + float bx0 = fGrX + i * barW + gap; + float bx1 = fGrX + (i + 1) * barW - gap; + float bw = bx1 - bx0; + + if ( m_iBusyPct[i] == RF_SCAN_BUSY_NOT_SCANNED ) + { + // Not yet scanned: dim outline + g_pRenderEngine->setFill(0, 0, 0, 0); + g_pRenderEngine->setStroke(0.18f, 0.20f, 0.28f, 0.6f); + g_pRenderEngine->setStrokeSize(1); + g_pRenderEngine->drawRect(bx0, fGrY, bw, fGrH); + continue; + } + + if ( m_iBusyPct[i] == RF_SCAN_BUSY_INVALID ) + { + // Measured, but no usable survey data for this channel: greyed outline. + g_pRenderEngine->setFill(0.20f, 0.20f, 0.22f, 0.35f); + g_pRenderEngine->setStroke(0.30f, 0.30f, 0.34f, 0.7f); + g_pRenderEngine->setStrokeSize(1); + g_pRenderEngine->drawRect(bx0, fGrY, bw, fGrH); + continue; + } + + float frac = (float)m_iBusyPct[i] / 100.0f; + if ( frac < 0.0f ) frac = 0.0f; + if ( frac > 1.0f ) frac = 1.0f; + float barTop = fGrB - frac * fGrH; + + // Colour by busy ratio (spectrum style: shorter/lower bar = cleaner) + float cr, cg, cb; + if ( m_iBusyPct[i] < 20 ) { cr = 0.20f; cg = 0.78f; cb = 0.38f; } // green + else if ( m_iBusyPct[i] < 50 ) { cr = 0.94f; cg = 0.75f; cb = 0.18f; } // yellow + else { cr = 0.85f; cg = 0.25f; cb = 0.22f; } // red + + // Highlight best channel + if ( i == m_iBestChannelIndex ) + { + g_pRenderEngine->setFill(0.10f, 0.32f, 0.16f, 0.35f); + g_pRenderEngine->setStroke(0.18f, 0.86f, 0.46f, 1.0f); + g_pRenderEngine->setStrokeSize(2); + g_pRenderEngine->drawRect(bx0 - wPx * 2, fGrY, bw + wPx * 4, fGrH); + cr = 0.18f; cg = 0.86f; cb = 0.46f; + } + + g_pRenderEngine->setFill(cr, cg, cb, 1.0f); + g_pRenderEngine->setStrokeSize(0); + g_pRenderEngine->drawRect(bx0, barTop, bw, fGrB - barTop); + + // Freq label for every 4th channel, first, last, and best + bool bLabel = ( i == 0 ) || ( i == m_iScanChannelsCount - 1 ) || + ( i == m_iBestChannelIndex ) || ( i % 4 == 0 ); + if ( bLabel ) + { + char szF[16]; + snprintf(szF, sizeof(szF), "%u", m_uScanChannels[i] / 1000); + float lw = g_pRenderEngine->textWidth(g_idFontMenuSmall, szF); + float cx = bx0 + bw * 0.5f; + g_pRenderEngine->setColors(( i == m_iBestChannelIndex ) ? get_Color_IconSucces() : get_Color_MenuItemDisabledText()); + g_pRenderEngine->drawText(cx - lw * 0.5f, fGrB + hPx * 4, g_idFontMenuSmall, szF); + } + + // Annotation bubble for recommended channel + if ( i == m_iBestChannelIndex && m_bScanComplete ) + { + char szLn1[32], szLn2[32]; + snprintf(szLn1, sizeof(szLn1), "%u MHz", m_uScanChannels[i] / 1000); + snprintf(szLn2, sizeof(szLn2), "%d%% busy", m_iBusyPct[i]); + + float tw1 = g_pRenderEngine->textWidth(g_idFontMenu, szLn1); + float tw2 = g_pRenderEngine->textWidth(g_idFontMenuSmall, szLn2); + float bubW = (tw1 > tw2 ? tw1 : tw2) + wPx * 16; + float bubH = hTs + g_pRenderEngine->textHeight(g_idFontMenu) + hPx * 10; + float cx = bx0 + bw * 0.5f; + float bubX = cx - bubW * 0.5f; + if ( bubX < fGrX ) bubX = fGrX; + if ( bubX + bubW > fGrX + fGrW ) bubX = fGrX + fGrW - bubW; + float bubY = barTop - bubH - hPx * 14; + if ( bubY < fGrY + hPx * 2 ) bubY = fGrY + hPx * 2; + + // Arrow (simple V shape) + g_pRenderEngine->setStroke(0.18f, 0.86f, 0.46f, 1.0f); + g_pRenderEngine->setStrokeSize(2); + float aY = barTop - hPx * 2; + float aW = wPx * 7; + float aH = hPx * 10; + g_pRenderEngine->drawLine(cx, aY, cx - aW, aY - aH); + g_pRenderEngine->drawLine(cx, aY, cx + aW, aY - aH); + g_pRenderEngine->drawLine(cx - aW, aY - aH, cx + aW, aY - aH); + + // Bubble background + g_pRenderEngine->setFill(0.10f, 0.26f, 0.16f, 0.95f); + g_pRenderEngine->setStroke(0.18f, 0.86f, 0.46f, 1.0f); + g_pRenderEngine->setStrokeSize(2); + g_pRenderEngine->drawRoundRect(bubX, bubY, bubW, bubH, 0.003f); + + // Text + float th1 = g_pRenderEngine->textHeight(g_idFontMenu); + g_pRenderEngine->setColors(get_Color_IconSucces()); + g_pRenderEngine->drawText(bubX + wPx * 8, bubY + hPx * 5, g_idFontMenu, szLn1); + g_pRenderEngine->setColors(get_Color_MenuText()); + g_pRenderEngine->drawText(bubX + wPx * 8, bubY + th1 + hPx * 7, g_idFontMenuSmall, szLn2); + } + } + + // Highlight the channel currently being scanned + if ( m_bScanInProgress && m_iChannelsScanned < m_iScanChannelsCount ) + { + int curCh = m_iChannelsScanned; + float bx0 = fGrX + curCh * barW + gap; + float bx1 = fGrX + (curCh + 1) * barW - gap; + g_pRenderEngine->setFill(0.28f, 0.48f, 0.88f, 0.25f); + g_pRenderEngine->setStroke(0.28f, 0.48f, 0.88f, 0.75f); + g_pRenderEngine->setStrokeSize(2); + g_pRenderEngine->drawRect(bx0 - wPx, fGrY, (bx1 - bx0) + wPx * 2, fGrH); + } +} + +void MenuRFScan::onSelectItem() +{ + Menu::onSelectItem(); + if ( m_SelectedIndex < 0 || m_pMenuItems[m_SelectedIndex]->isEditing() ) + return; + + if ( m_SelectedIndex == m_IndexRescan ) + { + _startScan(); + return; + } + + if ( m_SelectedIndex == m_IndexUseRecommended ) + { + if ( m_uBestFreqKhz == 0 ) + return; + if ( m_bScanInProgress ) + _stopScan(); + menu_stack_pop(1); + return; + } + + if ( m_SelectedIndex == m_IndexCancel ) + { + if ( m_bScanInProgress ) + _stopScan(); + menu_stack_pop(0); + return; + } +} + +int MenuRFScan::onBack() +{ + if ( m_bScanInProgress ) + _stopScan(); + return Menu::onBack(); +} + +u32 menu_rf_scan_get_best_freq_khz() +{ + if ( s_pLastRFScanMenu ) + return s_pLastRFScanMenu->getBestFreqKhz(); + return 0; +} diff --git a/code/r_central/menu/menu_rf_scan.h b/code/r_central/menu/menu_rf_scan.h new file mode 100644 index 00000000..8d15047c --- /dev/null +++ b/code/r_central/menu/menu_rf_scan.h @@ -0,0 +1,51 @@ +#pragma once +#include "menu_objects.h" +#include "menu_item_text.h" + +#define RF_SCAN_MAX_CHANNELS 128 +#define RF_SCAN_RESULTS_FILE "/tmp/ruby_rf_scan_results.txt" + +// Per-channel busy ratio sentinels (must match the router side in rf_scan.cpp). +#define RF_SCAN_BUSY_NOT_SCANNED (-1000) // channel not measured yet +#define RF_SCAN_BUSY_INVALID (-1) // measured, but the card reported no usable survey data + +class MenuRFScan : public Menu +{ + public: + MenuRFScan(int iVehicleRadioLink, u32 uFreqKhz); + virtual ~MenuRFScan(); + virtual void onShow(); + virtual void Render(); + virtual bool periodicLoop(); + virtual void onSelectItem(); + virtual int onBack(); + + u32 getBestFreqKhz() const { return m_uBestFreqKhz; } + + private: + void _startScan(); + void _stopScan(); + void _readResultsFile(); + void _renderSpectrumChart(float xPos, float yPos, float fWidth, float fHeight); + + int m_iVehicleRadioLink; + u32 m_uCurrentFreqKhz; + int m_uBandFlags; + + u32 m_uScanChannels[RF_SCAN_MAX_CHANNELS]; + int m_iScanChannelsCount; + int m_iBusyPct[RF_SCAN_MAX_CHANNELS]; + int m_iChannelsScanned; + u32 m_uBestFreqKhz; + int m_iBestChannelIndex; + + bool m_bScanInProgress; + bool m_bScanComplete; + bool m_bNoSurveyData; + + int m_IndexRescan; + int m_IndexUseRecommended; + int m_IndexCancel; +}; + +u32 menu_rf_scan_get_best_freq_khz(); diff --git a/code/r_central/menu/menu_vehicle_radio_link.cpp b/code/r_central/menu/menu_vehicle_radio_link.cpp index 7234a239..fe07d669 100644 --- a/code/r_central/menu/menu_vehicle_radio_link.cpp +++ b/code/r_central/menu/menu_vehicle_radio_link.cpp @@ -37,6 +37,9 @@ #include "menu_item_text.h" #include "menu_tx_raw_power.h" #include "menu_confirmation.h" +#include "menu_rf_scan.h" +#include "../../base/config_radio.h" +#include "../../base/hardware_radio.h" #include "../../common/models_connect_frequencies.h" #include "../launchers_controller.h" #include "../link_watch.h" @@ -141,7 +144,8 @@ void MenuVehicleRadioLink::addMenuItems() m_IndexLDPC = -1; m_IndexSGI = -1; m_IndexSTBC = -1; - m_IndexReset = -1; + m_IndexReset = -1; + m_IndexScanFreq = -1; log_line("MenuVehicleRadioLink: Add items: radio data rates for link %d: vid: %d, data-down: %d, data-up: %d", m_iVehicleRadioLink+1, g_pCurrentModel->radioLinksParams.downlink_datarate_video_bps[m_iVehicleRadioLink], @@ -158,6 +162,14 @@ void MenuVehicleRadioLink::addMenuItems() sprintf(szBuff, "Vehicle radio interface used for this radio link: %s", str_get_radio_card_model_string(g_pCurrentModel->radioInterfacesParams.interface_card_model[m_iVehicleRadioInterface])); addMenuItem(new MenuItemText(szBuff, false, 0.0)); addMenuItemFrequencies(); + + // Add scan option for WiFi bands only (not serial radios / SiK) + u32 uCurFreq = g_pCurrentModel->radioLinksParams.link_frequency_khz[m_iVehicleRadioLink]; + int iBand = getBand(uCurFreq); + if ( iBand == RADIO_HW_SUPPORTED_BAND_24 || iBand == RADIO_HW_SUPPORTED_BAND_23 || + iBand == RADIO_HW_SUPPORTED_BAND_25 || iBand == RADIO_HW_SUPPORTED_BAND_58 ) + m_IndexScanFreq = addMenuItem(new MenuItem("Scan for Clean Channel", "Scan all channels in the current band and find the one with the lowest interference.")); + addMenuItemsCapabilities(); addMenuItemsDataRates(); @@ -1126,6 +1138,22 @@ void MenuVehicleRadioLink::onReturnFromChild(int iChildMenuId, int returnValue) sendRadioLinkConfigParams(&m_RadioLinksParamsToApply, false); return; } + + // MenuRFScan is created with m_MenuId = MENU_ID_RF_SCAN, so match the full id + // here (not iChildMenuId/1000, which is used for confirmation-style children). + if ( MENU_ID_RF_SCAN == iChildMenuId ) + { + if ( 1 == returnValue ) + { + u32 uBestFreq = menu_rf_scan_get_best_freq_khz(); + if ( uBestFreq > 0 ) + { + log_line("MenuVehicleRadioLink: applying RF scan recommended frequency %s", str_format_frequency(uBestFreq)); + sendNewRadioLinkFrequency(m_iVehicleRadioLink, uBestFreq); + } + } + return; + } } int MenuVehicleRadioLink::onBack() @@ -1152,6 +1180,23 @@ void MenuVehicleRadioLink::onSelectItem() return; } + if ( m_SelectedIndex == m_IndexScanFreq ) + { + // Safety: scanning hops the ground station across the whole band, which + // drops the link to the vehicle (video, telemetry AND RC control) for the + // duration of the scan. Refuse to start while the vehicle is armed. + t_structure_vehicle_info* pRTInfo = get_vehicle_runtime_info_for_vehicle_id(g_pCurrentModel->uVehicleId); + if ( (NULL != pRTInfo) && pRTInfo->bGotRubyTelemetryInfo && pRTInfo->bIsArmed ) + { + add_menu_to_stack(new MenuConfirmation("Cannot Scan While Armed", + "The vehicle is armed. Scanning interrupts the radio link (video, telemetry and RC control) for several seconds. Disarm the vehicle before scanning for a clean channel.", 0, true)); + return; + } + u32 uFreq = g_pCurrentModel->radioLinksParams.link_frequency_khz[m_iVehicleRadioLink]; + add_menu_to_stack(new MenuRFScan(m_iVehicleRadioLink, uFreq)); + return; + } + char szBuff[256]; log_line("MenuVehicleRadioLink: Current vehicle radio interface assigned to vehicle radio link %d: %d", m_iVehicleRadioLink+1, m_iVehicleRadioInterface+1); diff --git a/code/r_central/menu/menu_vehicle_radio_link.h b/code/r_central/menu/menu_vehicle_radio_link.h index 6f655d22..7dabcd6e 100644 --- a/code/r_central/menu/menu_vehicle_radio_link.h +++ b/code/r_central/menu/menu_vehicle_radio_link.h @@ -55,6 +55,7 @@ class MenuVehicleRadioLink: public Menu int m_IndexSGI; int m_IndexSTBC; int m_IndexReset; + int m_IndexScanFreq; bool m_bWaitingConfirmationFromUser; bool m_bWaitingVideoChangeConfirmationFromVehicle; diff --git a/code/r_station/periodic_loop.cpp b/code/r_station/periodic_loop.cpp index 30ba6efa..b81995a6 100644 --- a/code/r_station/periodic_loop.cpp +++ b/code/r_station/periodic_loop.cpp @@ -55,6 +55,7 @@ #include "adaptive_video.h" #include "test_link_params.h" #include "packets_utils.h" +#include "rf_scan.h" #include "rx_video_output.h" #include "processor_rx_audio.h" @@ -566,6 +567,8 @@ void _check_retransmissions_state() void router_periodic_loop() { + rf_scan_periodic_loop(); + radio_links_check_reinit_sik_interfaces(); if ( test_link_is_in_progress() ) diff --git a/code/r_station/process_local_packets.cpp b/code/r_station/process_local_packets.cpp index 4c6b85a9..f0c8a251 100644 --- a/code/r_station/process_local_packets.cpp +++ b/code/r_station/process_local_packets.cpp @@ -68,6 +68,7 @@ #include "adaptive_video.h" #include "radio_links_sik.h" #include "test_link_params.h" +#include "rf_scan.h" bool _switch_to_vehicle_radio_link(int iVehicleRadioLinkId) @@ -882,6 +883,21 @@ void process_local_control_packet(u8* pPacketBuffer) return; } + if ( pPH->packet_type == PACKET_TYPE_LOCAL_CONTROLLER_RF_SCAN_START ) + { + u32 uBandFlags = pPH->vehicle_id_dest; + log_line("RFScan: received start request for band flags %u.", uBandFlags); + rf_scan_start(uBandFlags); + return; + } + + if ( pPH->packet_type == PACKET_TYPE_LOCAL_CONTROLLER_RF_SCAN_STOP ) + { + log_line("RFScan: received stop request."); + rf_scan_stop(); + return; + } + if ( pPH->packet_type == PACKET_TYPE_LOCAL_CONTROL_LINK_FREQUENCY_CHANGED ) { u32* pI = (u32*)(((u8*)(pPH))+sizeof(t_packet_header)); diff --git a/code/r_station/rf_scan.cpp b/code/r_station/rf_scan.cpp new file mode 100644 index 00000000..09af1fdb --- /dev/null +++ b/code/r_station/rf_scan.cpp @@ -0,0 +1,292 @@ +/* + Ruby Licence + Copyright (c) 2020-2025 Petru Soroaga petrusoroaga@yahoo.com + All rights reserved. + + Redistribution and/or use in source and/or binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions and/or use of the source code (partially or complete) must retain + the above copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the distribution. + * Redistributions in binary form (partially or complete) must reproduce + the above copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the distribution. + * Copyright info and developer info must be preserved as is in the user + interface, additions could be made to that info. + * Neither the name of the organization nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + * Military use is not permitted. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE AUTHOR (PETRU SOROAGA) BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include + +#include "../base/base.h" +#include "../base/config.h" +#include "../base/config_radio.h" +#include "../base/hardware.h" +#include "../base/hardware_radio.h" +#include "../base/hardware_procs.h" +#include "../radio/radiopackets2.h" +#include "../radio/radio_rx.h" +#include "shared_vars.h" +#include "timers.h" +#include "radio_links.h" +#include "rf_scan.h" + +#define RF_SCAN_MAX_CHANNELS 128 +#define RF_SCAN_DWELL_MS 280 +#define RF_SCAN_RESULTS_FILE "/tmp/ruby_rf_scan_results.txt" + +// Channel busy ratio in percent (0..100). Lower = cleaner channel. +// Written to the results file as the per-channel metric. A negative value +// means the radio card did not report usable survey data for that channel. +#define RF_SCAN_BUSY_INVALID (-1) + +static bool s_bRFScanInProgress = false; +static u32 s_uRFScanChannels[RF_SCAN_MAX_CHANNELS]; +static int s_iRFScanChannelsCount = 0; +static int s_iRFScanCurrentChannel = 0; +static u32 s_uRFScanDwellStartTime = 0; +static FILE* s_pRFScanFile = NULL; +static u32 s_uRFScanBestFreqKhz = 0; +static int s_iRFScanBestBusyPct = 0; + +// Reads the channel busy ratio (in percent) for the currently-tuned channel +// from "iw dev survey dump". Returns RF_SCAN_BUSY_INVALID when the +// driver does not expose survey data (many monitor-mode RTL drivers do not), +// so the caller can surface "not supported" instead of fabricating a value. +static int _rf_scan_read_busy_pct(u32 uFreqKhz) +{ + char szIfaceName[64]; + szIfaceName[0] = 0; + + for ( int i = 0; i < hardware_get_radio_interfaces_count(); i++ ) + { + radio_hw_info_t* pInfo = hardware_get_radio_info(i); + if ( NULL == pInfo ) + continue; + if ( ! hardware_radio_is_wifi_radio(pInfo) ) + continue; + if ( ! pInfo->isConfigurable ) + continue; + if ( 0 == hardware_radio_supports_frequency(pInfo, uFreqKhz) ) + continue; + strncpy(szIfaceName, pInfo->szName, sizeof(szIfaceName) - 1); + break; + } + + if ( szIfaceName[0] == 0 ) + return RF_SCAN_BUSY_INVALID; + + char szCmd[256]; + char szOut[8192]; + szOut[0] = 0; + snprintf(szCmd, sizeof(szCmd), "iw dev %s survey dump 2>/dev/null", szIfaceName); + hw_execute_bash_command_silent(szCmd, szOut); + + int freq_mhz = (int)(uFreqKhz / 1000); + char* pLine = szOut; + bool bFoundFreq = false; + long long llActive = -1; + long long llBusy = -1; + + while ( *pLine ) + { + char* pNext = strchr(pLine, '\n'); + if ( pNext ) + *pNext = '\0'; + + if ( !bFoundFreq ) + { + int parsedMhz = 0; + if ( sscanf(pLine, " frequency: %d MHz", &parsedMhz) == 1 && parsedMhz == freq_mhz ) + bFoundFreq = true; + } + else + { + // We are inside the survey block for the target frequency. + long long llVal = 0; + if ( sscanf(pLine, " channel active time: %lld ms", &llVal) == 1 ) + llActive = llVal; + else if ( sscanf(pLine, " channel busy time: %lld ms", &llVal) == 1 ) + llBusy = llVal; + // Reached the next survey block — stop. + else if ( strstr(pLine, "frequency:") ) + { + if ( pNext ) + *pNext = '\n'; + break; + } + } + + if ( pNext ) + { + *pNext = '\n'; + pLine = pNext + 1; + } + else + break; + } + + if ( (llActive <= 0) || (llBusy < 0) ) + return RF_SCAN_BUSY_INVALID; + + long long llPct = (llBusy * 100) / llActive; + if ( llPct < 0 ) llPct = 0; + if ( llPct > 100 ) llPct = 100; + return (int)llPct; +} + +void rf_scan_start(u32 uBandFlags) +{ + if ( s_bRFScanInProgress ) + rf_scan_stop(); + + s_iRFScanChannelsCount = 0; + s_iRFScanCurrentChannel = 0; + s_uRFScanDwellStartTime = 0; + s_uRFScanBestFreqKhz = 0; + s_iRFScanBestBusyPct = 0; + + u32* pChannels = NULL; + int nCount = 0; + + if ( uBandFlags == RADIO_HW_SUPPORTED_BAND_58 ) { pChannels = getChannels58(); nCount = getChannels58Count(); } + else if ( uBandFlags == RADIO_HW_SUPPORTED_BAND_24 ) { pChannels = getChannels24(); nCount = getChannels24Count(); } + else if ( uBandFlags == RADIO_HW_SUPPORTED_BAND_23 ) { pChannels = getChannels23(); nCount = getChannels23Count(); } + else if ( uBandFlags == RADIO_HW_SUPPORTED_BAND_25 ) { pChannels = getChannels25(); nCount = getChannels25Count(); } + else if ( uBandFlags == RADIO_HW_SUPPORTED_BAND_433 ) { pChannels = getChannels433(); nCount = getChannels433Count(); } + else if ( uBandFlags == RADIO_HW_SUPPORTED_BAND_868 ) { pChannels = getChannels868(); nCount = getChannels868Count(); } + else if ( uBandFlags == RADIO_HW_SUPPORTED_BAND_915 ) { pChannels = getChannels915(); nCount = getChannels915Count(); } + + if ( NULL == pChannels || nCount <= 0 ) + { + log_softerror_and_alarm("RFScan: unknown or empty band flags %u, cannot scan.", uBandFlags); + return; + } + + if ( nCount > RF_SCAN_MAX_CHANNELS ) + nCount = RF_SCAN_MAX_CHANNELS; + + for ( int i = 0; i < nCount; i++ ) + s_uRFScanChannels[i] = pChannels[i]; + s_iRFScanChannelsCount = nCount; + + s_pRFScanFile = fopen(RF_SCAN_RESULTS_FILE, "w"); + if ( NULL == s_pRFScanFile ) + { + log_softerror_and_alarm("RFScan: failed to open results file %s.", RF_SCAN_RESULTS_FILE); + return; + } + + s_bRFScanInProgress = true; + log_line("RFScan: started scan for band flags %u, %d channels.", uBandFlags, s_iRFScanChannelsCount); +} + +void rf_scan_periodic_loop() +{ + if ( !s_bRFScanInProgress ) + return; + + if ( s_iRFScanCurrentChannel >= s_iRFScanChannelsCount ) + { + // All channels done + if ( s_pRFScanFile ) + { + fclose(s_pRFScanFile); + s_pRFScanFile = NULL; + } + // Restore original frequencies + if ( NULL != g_pCurrentModel ) + radio_links_set_cards_frequencies_and_params(-1); + for ( int i = 0; i < hardware_get_radio_interfaces_count(); i++ ) + radio_rx_resume_interface(i); + + s_bRFScanInProgress = false; + if ( s_uRFScanBestFreqKhz != 0 ) + log_line("RFScan: scan complete. Cleanest channel: %u kHz (%d%% busy).", s_uRFScanBestFreqKhz, s_iRFScanBestBusyPct); + else + log_line("RFScan: scan complete, but no usable survey data was reported by the radio card."); + return; + } + + u32 uFreqKhz = s_uRFScanChannels[s_iRFScanCurrentChannel]; + + // Step 1: set frequency and start dwell + if ( s_uRFScanDwellStartTime == 0 ) + { + for ( int i = 0; i < hardware_get_radio_interfaces_count(); i++ ) + radio_rx_pause_interface(i, "RF scan channel change"); + radio_links_set_cards_frequencies_for_search(uFreqKhz, false, -1, -1, -1, -1); + for ( int i = 0; i < hardware_get_radio_interfaces_count(); i++ ) + radio_rx_resume_interface(i); + s_uRFScanDwellStartTime = g_TimeNow; + return; + } + + // Step 2: wait for dwell time + if ( g_TimeNow < s_uRFScanDwellStartTime + RF_SCAN_DWELL_MS ) + return; + + // Step 3: read channel busy ratio and record + int iBusyPct = _rf_scan_read_busy_pct(uFreqKhz); + + if ( s_pRFScanFile ) + { + fprintf(s_pRFScanFile, "%u %d\n", uFreqKhz, iBusyPct); + fflush(s_pRFScanFile); + } + + if ( iBusyPct >= 0 ) + { + if ( s_uRFScanBestFreqKhz == 0 || iBusyPct < s_iRFScanBestBusyPct ) + { + s_iRFScanBestBusyPct = iBusyPct; + s_uRFScanBestFreqKhz = uFreqKhz; + } + } + + log_line("RFScan: channel %d/%d %u kHz busy=%d%%.", + s_iRFScanCurrentChannel + 1, s_iRFScanChannelsCount, uFreqKhz, iBusyPct); + + s_iRFScanCurrentChannel++; + s_uRFScanDwellStartTime = 0; +} + +void rf_scan_stop() +{ + if ( !s_bRFScanInProgress ) + return; + + if ( s_pRFScanFile ) + { + fclose(s_pRFScanFile); + s_pRFScanFile = NULL; + } + if ( NULL != g_pCurrentModel ) + radio_links_set_cards_frequencies_and_params(-1); + for ( int i = 0; i < hardware_get_radio_interfaces_count(); i++ ) + radio_rx_resume_interface(i); + + s_bRFScanInProgress = false; + log_line("RFScan: scan stopped."); +} + +bool rf_scan_is_in_progress() +{ + return s_bRFScanInProgress; +} diff --git a/code/r_station/rf_scan.h b/code/r_station/rf_scan.h new file mode 100644 index 00000000..06b4a884 --- /dev/null +++ b/code/r_station/rf_scan.h @@ -0,0 +1,7 @@ +#pragma once +#include "../base/base.h" + +void rf_scan_start(u32 uBandFlags); +void rf_scan_periodic_loop(); +void rf_scan_stop(); +bool rf_scan_is_in_progress(); diff --git a/code/radio/local_packets.h b/code/radio/local_packets.h index 4a988edd..70a7d55b 100644 --- a/code/radio/local_packets.h +++ b/code/radio/local_packets.h @@ -70,3 +70,6 @@ #define PACKET_TYPE_LOCAL_CONTROL_VEHICLE_SEND_MODEL_SETTINGS 222 #define PACKET_TYPE_APPLY_SIK_PARAMS 230 + +#define PACKET_TYPE_LOCAL_CONTROLLER_RF_SCAN_START 231 // vehicle_id_dest: band flag (RADIO_HW_SUPPORTED_BAND_*) +#define PACKET_TYPE_LOCAL_CONTROLLER_RF_SCAN_STOP 232 // abort an in-progress scan and restore the radio links