From d8ef848494878e1370172eca15a7faf47b374929 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 20:58:38 +0000 Subject: [PATCH 1/4] chore: add .gitignore to exclude Claude Code worktree directory Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01JiQhEG8VApL39xVyJLXJ79 --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4c5f2067 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.claude/ From c809ceb2d1aac1bb2e75bc67fbb8443efd909576 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 21:05:05 +0000 Subject: [PATCH 2/4] feat: add RF spectrum scanner for clean frequency selection Adds a new "Scan for Clean Channel" menu item to the vehicle radio link settings. Opens a full-width spectrum bar chart showing noise floor per channel (shorter bar = less noise = cleaner, spectrum-analyser style). Router-side (rf_scan.cpp): hops through all band channels at 280 ms/ch, reads per-channel noise floor via `iw dev survey dump`, writes live results to /tmp/ruby_rf_scan_results.txt, signals completion via IPC. UI-side (menu_rf_scan.cpp): reads results file in periodicLoop, draws bar chart with colour-coding (green/yellow/red) and an annotation bubble over the cleanest channel. "Use Recommended" applies the winner via the existing sendNewRadioLinkFrequency() path. Only shown for 2.3/2.4/2.5/5.8 GHz links (not SiK/serial). Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01JiQhEG8VApL39xVyJLXJ79 --- Makefile | 4 +- code/r_central/menu/menu_objects.h | 1 + code/r_central/menu/menu_rf_scan.cpp | 425 ++++++++++++++++++ code/r_central/menu/menu_rf_scan.h | 44 ++ .../menu/menu_vehicle_radio_link.cpp | 35 +- code/r_central/menu/menu_vehicle_radio_link.h | 1 + code/r_station/periodic_loop.cpp | 3 + code/r_station/process_local_packets.cpp | 9 + code/r_station/rf_scan.cpp | 288 ++++++++++++ code/r_station/rf_scan.h | 8 + code/radio/local_packets.h | 3 + 11 files changed, 818 insertions(+), 3 deletions(-) create mode 100644 code/r_central/menu/menu_rf_scan.cpp create mode 100644 code/r_central/menu/menu_rf_scan.h create mode 100644 code/r_station/rf_scan.cpp create mode 100644 code/r_station/rf_scan.h 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..c743fccc --- /dev/null +++ b/code/r_central/menu/menu_rf_scan.cpp @@ -0,0 +1,425 @@ +/* + 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; + + memset(m_iNoiseDbm, 0, sizeof(m_iNoiseDbm)); + + // 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_iNoiseDbm[i] = -999; + } + 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 noise floor.")); + 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() +{ + if ( s_pLastRFScanMenu == this ) + s_pLastRFScanMenu = NULL; +} + +void MenuRFScan::onShow() +{ + Menu::onShow(); + _startScan(); +} + +void MenuRFScan::_startScan() +{ + for ( int i = 0; i < m_iScanChannelsCount; i++ ) + m_iNoiseDbm[i] = -999; + m_iChannelsScanned = 0; + m_uBestFreqKhz = 0; + m_iBestChannelIndex = -1; + m_bScanInProgress = true; + m_bScanComplete = 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); +} + +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 iNoise = 0; + while ( fscanf(f, "%u %d", &uFreqKhz, &iNoise) == 2 ) + { + for ( int i = 0; i < m_iScanChannelsCount; i++ ) + { + if ( m_uScanChannels[i] == uFreqKhz ) + { + m_iNoiseDbm[i] = iNoise; + break; + } + } + } + fclose(f); + + m_iChannelsScanned = 0; + for ( int i = 0; i < m_iScanChannelsCount; i++ ) + if ( m_iNoiseDbm[i] != -999 ) + m_iChannelsScanned++; + + if ( m_iChannelsScanned >= m_iScanChannelsCount && m_iScanChannelsCount > 0 ) + { + m_bScanInProgress = false; + m_bScanComplete = true; + + int bestNoise = 0; + for ( int i = 0; i < m_iScanChannelsCount; i++ ) + { + if ( m_iNoiseDbm[i] == -999 ) + continue; + if ( m_iBestChannelIndex < 0 || m_iNoiseDbm[i] < bestNoise ) + { + bestNoise = m_iNoiseDbm[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); + } + } +} + +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_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: -50 (top) to -100 (bottom) — lower = quieter = better + const float kNoiseTop = -50.0f; + const float kNoiseBt = -100.0f; + + // Grid lines + Y labels + int yTicks[] = { -50, -60, -70, -80, -90, -100 }; + for ( int t = 0; t < 6; t++ ) + { + float dbm = (float)yTicks[t]; + float frac = (dbm - kNoiseTop) / (kNoiseBt - kNoiseTop); + float gy = fGrY + 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_iNoiseDbm[i] == -999 ) + { + // 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; + } + + float frac = ((float)m_iNoiseDbm[i] - kNoiseTop) / (kNoiseBt - kNoiseTop); + if ( frac < 0.0f ) frac = 0.0f; + if ( frac > 1.0f ) frac = 1.0f; + float barTop = fGrY + frac * fGrH; + + // Colour by noise level (spectrum style: shorter/lower bar = cleaner) + float cr, cg, cb; + if ( m_iNoiseDbm[i] <= -88 ) { cr = 0.20f; cg = 0.78f; cb = 0.38f; } // green + else if ( m_iNoiseDbm[i] <= -76 ) { 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 dBm", m_iNoiseDbm[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; + menu_stack_pop(1); + return; + } + + if ( m_SelectedIndex == m_IndexCancel ) + { + menu_stack_pop(0); + return; + } +} + +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..5740b8ad --- /dev/null +++ b/code/r_central/menu/menu_rf_scan.h @@ -0,0 +1,44 @@ +#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" + +class MenuRFScan : public Menu +{ + public: + MenuRFScan(int iVehicleRadioLink, u32 uFreqKhz); + virtual ~MenuRFScan(); + virtual void onShow(); + virtual void Render(); + virtual bool periodicLoop(); + virtual void onSelectItem(); + + u32 getBestFreqKhz() const { return m_uBestFreqKhz; } + + private: + void _startScan(); + 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_iNoiseDbm[RF_SCAN_MAX_CHANNELS]; + int m_iChannelsScanned; + u32 m_uBestFreqKhz; + int m_iBestChannelIndex; + + bool m_bScanInProgress; + bool m_bScanComplete; + + 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..c0570d7b 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,20 @@ void MenuVehicleRadioLink::onReturnFromChild(int iChildMenuId, int returnValue) sendRadioLinkConfigParams(&m_RadioLinksParamsToApply, false); return; } + + if ( MENU_ID_RF_SCAN == iChildMenuId/1000 ) + { + 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 +1178,13 @@ void MenuVehicleRadioLink::onSelectItem() return; } + if ( m_SelectedIndex == m_IndexScanFreq ) + { + 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..b3e9fe9a 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,14 @@ 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, g_fIPCToCentral); + 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..5c53a8f7 --- /dev/null +++ b/code/r_station/rf_scan.cpp @@ -0,0 +1,288 @@ +/* + 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 "../base/ruby_ipc.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" + +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 int s_iFDToCentral = -1; +static FILE* s_pRFScanFile = NULL; +static u32 s_uRFScanBestFreqKhz = 0; +static int s_iRFScanBestNoise = 0; + +static int _rf_scan_read_noise_dbm(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 -75; + + 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; + + 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 + { + int noiseDbm = 0; + if ( sscanf(pLine, " noise: %d dBm", &noiseDbm) == 1 ) + { + if ( pNext ) + *pNext = '\n'; + return noiseDbm; + } + // Hit next survey block — stop + if ( strstr(pLine, "frequency:") ) + break; + } + + if ( pNext ) + { + *pNext = '\n'; + pLine = pNext + 1; + } + else + break; + } + return -75; +} + +static void _rf_scan_send_result_to_central() +{ + if ( s_iFDToCentral < 0 ) + return; + + t_packet_header PH; + radio_packet_init(&PH, PACKET_COMPONENT_LOCAL_CONTROL, PACKET_TYPE_LOCAL_CONTROLLER_RF_SCAN_RESULT, STREAM_ID_DATA); + PH.vehicle_id_src = 0; + PH.vehicle_id_dest = s_uRFScanBestFreqKhz; + PH.total_length = sizeof(t_packet_header); + radio_packet_compute_crc((u8*)&PH, sizeof(t_packet_header)); + ruby_ipc_channel_send_message(s_iFDToCentral, (u8*)&PH, sizeof(t_packet_header)); +} + +void rf_scan_start(u32 uBandFlags, int iFDToCentral) +{ + if ( s_bRFScanInProgress ) + rf_scan_stop(); + + s_iFDToCentral = iFDToCentral; + s_iRFScanChannelsCount = 0; + s_iRFScanCurrentChannel = 0; + s_uRFScanDwellStartTime = 0; + s_uRFScanBestFreqKhz = 0; + s_iRFScanBestNoise = 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; + log_line("RFScan: scan complete. Best channel: %u kHz (%d dBm).", s_uRFScanBestFreqKhz, s_iRFScanBestNoise); + _rf_scan_send_result_to_central(); + 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 noise and record + int iNoise = _rf_scan_read_noise_dbm(uFreqKhz); + + if ( s_pRFScanFile ) + { + fprintf(s_pRFScanFile, "%u %d\n", uFreqKhz, iNoise); + fflush(s_pRFScanFile); + } + + if ( s_uRFScanBestFreqKhz == 0 || iNoise < s_iRFScanBestNoise ) + { + s_iRFScanBestNoise = iNoise; + s_uRFScanBestFreqKhz = uFreqKhz; + } + + log_line("RFScan: channel %d/%d %u kHz noise=%d dBm.", + s_iRFScanCurrentChannel + 1, s_iRFScanChannelsCount, uFreqKhz, iNoise); + + 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; +} + +u32 rf_scan_get_best_freq_khz() +{ + return s_uRFScanBestFreqKhz; +} diff --git a/code/r_station/rf_scan.h b/code/r_station/rf_scan.h new file mode 100644 index 00000000..f2ef4085 --- /dev/null +++ b/code/r_station/rf_scan.h @@ -0,0 +1,8 @@ +#pragma once +#include "../base/base.h" + +void rf_scan_start(u32 uBandFlags, int iFDToCentral); +void rf_scan_periodic_loop(); +void rf_scan_stop(); +bool rf_scan_is_in_progress(); +u32 rf_scan_get_best_freq_khz(); diff --git a/code/radio/local_packets.h b/code/radio/local_packets.h index 4a988edd..1d4cbe9e 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_RESULT 232 // vehicle_id_dest: best freq kHz (0 = scan failed) From 9d1a6512c616e10624578599e52eebaa1b0b7438 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 21:06:04 +0000 Subject: [PATCH 3/4] chore: ignore compiled object files in .gitignore Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01JiQhEG8VApL39xVyJLXJ79 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4c5f2067..49a589a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .claude/ +*.o From 6cf7e0eca662e9a9df355050e9bdc064f1c2a742 Mon Sep 17 00:00:00 2001 From: wkumik Date: Wed, 17 Jun 2026 10:05:27 +0200 Subject: [PATCH 4/4] fix(rf-scan): safety guard, real busy-time metric, abortable scan Addresses review feedback on the RF spectrum scanner: 1. Safety: do not scan while flying. The scan hops the GS across the whole band, dropping video/telemetry/RC to the vehicle for several seconds. - Block opening the scan while the vehicle is armed (informational dialog). - Stop auto-starting the scan on menu open; require explicit "Start Scan". - Warn about the link interruption in the menu item description. 2. No fabricated data: the noise read returned a hard-coded -75 dBm when the card reported no survey data, silently recommending channel 0. Now an unreadable channel is marked invalid and excluded; if no channel yields data, the UI states the card does not support channel survey. 3. Better metric: replace noise floor (near-constant across a band) with the channel busy ratio (busy/active time) from `iw survey dump`. Chart now plots 0-100% busy; lower = cleaner. Recommendation = lowest busy %. 4. Remove dead RESULT packet path (no central handler existed); the UI reads the results file directly. Drops the unused IPC-to-central plumbing. 5. Abort on exit: send RF_SCAN_STOP on Cancel / Back / menu destruction so an in-progress scan restores the radio links immediately instead of running to completion after the user leaves. Also fix the apply path: MenuRFScan's id was matched with iChildMenuId/1000 (=> 140==0, never true), so "Use Recommended" never applied the frequency. Match the full child menu id instead. GS-only change (local control packets 231/232); no vehicle rebuild needed. Co-Authored-By: Claude Opus 4.8 (1M context) --- code/r_central/menu/menu_rf_scan.cpp | 105 ++++++++++++------ code/r_central/menu/menu_rf_scan.h | 9 +- .../menu/menu_vehicle_radio_link.cpp | 14 ++- code/r_station/process_local_packets.cpp | 9 +- code/r_station/rf_scan.cpp | 88 ++++++++------- code/r_station/rf_scan.h | 3 +- code/radio/local_packets.h | 2 +- 7 files changed, 149 insertions(+), 81 deletions(-) diff --git a/code/r_central/menu/menu_rf_scan.cpp b/code/r_central/menu/menu_rf_scan.cpp index c743fccc..c86062ee 100644 --- a/code/r_central/menu/menu_rf_scan.cpp +++ b/code/r_central/menu/menu_rf_scan.cpp @@ -56,8 +56,10 @@ MenuRFScan::MenuRFScan(int iVehicleRadioLink, u32 uFreqKhz) m_iBestChannelIndex = -1; m_bScanInProgress = false; m_bScanComplete = false; + m_bNoSurveyData = false; - memset(m_iNoiseDbm, 0, sizeof(m_iNoiseDbm)); + 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; @@ -75,10 +77,7 @@ MenuRFScan::MenuRFScan(int iVehicleRadioLink, u32 uFreqKhz) if ( nCount > RF_SCAN_MAX_CHANNELS ) nCount = RF_SCAN_MAX_CHANNELS; for ( int i = 0; i < nCount; i++ ) - { m_uScanChannels[i] = pChannels[i]; - m_iNoiseDbm[i] = -999; - } m_iScanChannelsCount = nCount; } @@ -86,7 +85,7 @@ MenuRFScan::MenuRFScan(int iVehicleRadioLink, u32 uFreqKhz) 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 noise floor.")); + 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.")); @@ -101,7 +100,7 @@ MenuRFScan::MenuRFScan(int iVehicleRadioLink, u32 uFreqKhz) 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); + snprintf(szTitle, sizeof(szTitle), "RF Spectrum Scan - %s - %d channels", szBand, m_iScanChannelsCount); setTitle(szTitle); s_pLastRFScanMenu = this; @@ -109,6 +108,10 @@ MenuRFScan::MenuRFScan(int iVehicleRadioLink, u32 uFreqKhz) 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; } @@ -116,18 +119,21 @@ MenuRFScan::~MenuRFScan() void MenuRFScan::onShow() { Menu::onShow(); - _startScan(); + // 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_iNoiseDbm[i] = -999; + 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"); @@ -136,6 +142,13 @@ void MenuRFScan::_startScan() 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 ) @@ -150,14 +163,14 @@ void MenuRFScan::_readResultsFile() return; u32 uFreqKhz = 0; - int iNoise = 0; - while ( fscanf(f, "%u %d", &uFreqKhz, &iNoise) == 2 ) + 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_iNoiseDbm[i] = iNoise; + m_iBusyPct[i] = iBusy; break; } } @@ -166,7 +179,7 @@ void MenuRFScan::_readResultsFile() m_iChannelsScanned = 0; for ( int i = 0; i < m_iScanChannelsCount; i++ ) - if ( m_iNoiseDbm[i] != -999 ) + if ( m_iBusyPct[i] != RF_SCAN_BUSY_NOT_SCANNED ) m_iChannelsScanned++; if ( m_iChannelsScanned >= m_iScanChannelsCount && m_iScanChannelsCount > 0 ) @@ -174,14 +187,14 @@ void MenuRFScan::_readResultsFile() m_bScanInProgress = false; m_bScanComplete = true; - int bestNoise = 0; + int bestBusy = 0; for ( int i = 0; i < m_iScanChannelsCount; i++ ) { - if ( m_iNoiseDbm[i] == -999 ) + if ( m_iBusyPct[i] < 0 ) // not scanned or no usable survey data continue; - if ( m_iBestChannelIndex < 0 || m_iNoiseDbm[i] < bestNoise ) + if ( m_iBestChannelIndex < 0 || m_iBusyPct[i] < bestBusy ) { - bestNoise = m_iNoiseDbm[i]; + bestBusy = m_iBusyPct[i]; m_iBestChannelIndex = i; m_uBestFreqKhz = m_uScanChannels[i]; } @@ -194,6 +207,11 @@ void MenuRFScan::_readResultsFile() 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; + } } } @@ -211,6 +229,8 @@ void MenuRFScan::Render() 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"); @@ -240,31 +260,29 @@ void MenuRFScan::_renderSpectrumChart(float xPos, float yPos, float fWidth, floa 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 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: -50 (top) to -100 (bottom) — lower = quieter = better - const float kNoiseTop = -50.0f; - const float kNoiseBt = -100.0f; + // Y range: channel busy ratio, 0% (bottom, clean) to 100% (top, congested). + // Shorter bar = less busy = cleaner channel. // Grid lines + Y labels - int yTicks[] = { -50, -60, -70, -80, -90, -100 }; + int yTicks[] = { 100, 80, 60, 40, 20, 0 }; for ( int t = 0; t < 6; t++ ) { - float dbm = (float)yTicks[t]; - float frac = (dbm - kNoiseTop) / (kNoiseBt - kNoiseTop); - float gy = fGrY + frac * fGrH; + 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]); + 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); } @@ -286,7 +304,7 @@ void MenuRFScan::_renderSpectrumChart(float xPos, float yPos, float fWidth, floa float bx1 = fGrX + (i + 1) * barW - gap; float bw = bx1 - bx0; - if ( m_iNoiseDbm[i] == -999 ) + if ( m_iBusyPct[i] == RF_SCAN_BUSY_NOT_SCANNED ) { // Not yet scanned: dim outline g_pRenderEngine->setFill(0, 0, 0, 0); @@ -296,16 +314,26 @@ void MenuRFScan::_renderSpectrumChart(float xPos, float yPos, float fWidth, floa continue; } - float frac = ((float)m_iNoiseDbm[i] - kNoiseTop) / (kNoiseBt - kNoiseTop); + 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 = fGrY + frac * fGrH; + float barTop = fGrB - frac * fGrH; - // Colour by noise level (spectrum style: shorter/lower bar = cleaner) + // Colour by busy ratio (spectrum style: shorter/lower bar = cleaner) float cr, cg, cb; - if ( m_iNoiseDbm[i] <= -88 ) { cr = 0.20f; cg = 0.78f; cb = 0.38f; } // green - else if ( m_iNoiseDbm[i] <= -76 ) { cr = 0.94f; cg = 0.75f; cb = 0.18f; } // yellow - else { cr = 0.85f; cg = 0.25f; cb = 0.22f; } // red + 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 ) @@ -339,7 +367,7 @@ void MenuRFScan::_renderSpectrumChart(float xPos, float yPos, float fWidth, floa { char szLn1[32], szLn2[32]; snprintf(szLn1, sizeof(szLn1), "%u MHz", m_uScanChannels[i] / 1000); - snprintf(szLn2, sizeof(szLn2), "%d dBm", m_iNoiseDbm[i]); + 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); @@ -406,17 +434,28 @@ void MenuRFScan::onSelectItem() { 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 ) diff --git a/code/r_central/menu/menu_rf_scan.h b/code/r_central/menu/menu_rf_scan.h index 5740b8ad..8d15047c 100644 --- a/code/r_central/menu/menu_rf_scan.h +++ b/code/r_central/menu/menu_rf_scan.h @@ -5,6 +5,10 @@ #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: @@ -14,11 +18,13 @@ class MenuRFScan : public Menu 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); @@ -28,13 +34,14 @@ class MenuRFScan : public Menu u32 m_uScanChannels[RF_SCAN_MAX_CHANNELS]; int m_iScanChannelsCount; - int m_iNoiseDbm[RF_SCAN_MAX_CHANNELS]; + 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; diff --git a/code/r_central/menu/menu_vehicle_radio_link.cpp b/code/r_central/menu/menu_vehicle_radio_link.cpp index c0570d7b..fe07d669 100644 --- a/code/r_central/menu/menu_vehicle_radio_link.cpp +++ b/code/r_central/menu/menu_vehicle_radio_link.cpp @@ -1139,7 +1139,9 @@ void MenuVehicleRadioLink::onReturnFromChild(int iChildMenuId, int returnValue) return; } - if ( MENU_ID_RF_SCAN == iChildMenuId/1000 ) + // 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 ) { @@ -1180,6 +1182,16 @@ void MenuVehicleRadioLink::onSelectItem() 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; diff --git a/code/r_station/process_local_packets.cpp b/code/r_station/process_local_packets.cpp index b3e9fe9a..f0c8a251 100644 --- a/code/r_station/process_local_packets.cpp +++ b/code/r_station/process_local_packets.cpp @@ -887,7 +887,14 @@ void process_local_control_packet(u8* pPacketBuffer) { u32 uBandFlags = pPH->vehicle_id_dest; log_line("RFScan: received start request for band flags %u.", uBandFlags); - rf_scan_start(uBandFlags, g_fIPCToCentral); + 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; } diff --git a/code/r_station/rf_scan.cpp b/code/r_station/rf_scan.cpp index 5c53a8f7..09af1fdb 100644 --- a/code/r_station/rf_scan.cpp +++ b/code/r_station/rf_scan.cpp @@ -40,7 +40,6 @@ #include "../base/hardware.h" #include "../base/hardware_radio.h" #include "../base/hardware_procs.h" -#include "../base/ruby_ipc.h" #include "../radio/radiopackets2.h" #include "../radio/radio_rx.h" #include "shared_vars.h" @@ -52,17 +51,25 @@ #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 int s_iFDToCentral = -1; static FILE* s_pRFScanFile = NULL; static u32 s_uRFScanBestFreqKhz = 0; -static int s_iRFScanBestNoise = 0; +static int s_iRFScanBestBusyPct = 0; -static int _rf_scan_read_noise_dbm(u32 uFreqKhz) +// 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; @@ -83,7 +90,7 @@ static int _rf_scan_read_noise_dbm(u32 uFreqKhz) } if ( szIfaceName[0] == 0 ) - return -75; + return RF_SCAN_BUSY_INVALID; char szCmd[256]; char szOut[8192]; @@ -94,6 +101,8 @@ static int _rf_scan_read_noise_dbm(u32 uFreqKhz) int freq_mhz = (int)(uFreqKhz / 1000); char* pLine = szOut; bool bFoundFreq = false; + long long llActive = -1; + long long llBusy = -1; while ( *pLine ) { @@ -109,16 +118,19 @@ static int _rf_scan_read_noise_dbm(u32 uFreqKhz) } else { - int noiseDbm = 0; - if ( sscanf(pLine, " noise: %d dBm", &noiseDbm) == 1 ) + // 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'; - return noiseDbm; - } - // Hit next survey block — stop - if ( strstr(pLine, "frequency:") ) break; + } } if ( pNext ) @@ -129,34 +141,26 @@ static int _rf_scan_read_noise_dbm(u32 uFreqKhz) else break; } - return -75; -} -static void _rf_scan_send_result_to_central() -{ - if ( s_iFDToCentral < 0 ) - return; + if ( (llActive <= 0) || (llBusy < 0) ) + return RF_SCAN_BUSY_INVALID; - t_packet_header PH; - radio_packet_init(&PH, PACKET_COMPONENT_LOCAL_CONTROL, PACKET_TYPE_LOCAL_CONTROLLER_RF_SCAN_RESULT, STREAM_ID_DATA); - PH.vehicle_id_src = 0; - PH.vehicle_id_dest = s_uRFScanBestFreqKhz; - PH.total_length = sizeof(t_packet_header); - radio_packet_compute_crc((u8*)&PH, sizeof(t_packet_header)); - ruby_ipc_channel_send_message(s_iFDToCentral, (u8*)&PH, sizeof(t_packet_header)); + 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, int iFDToCentral) +void rf_scan_start(u32 uBandFlags) { if ( s_bRFScanInProgress ) rf_scan_stop(); - s_iFDToCentral = iFDToCentral; s_iRFScanChannelsCount = 0; s_iRFScanCurrentChannel = 0; s_uRFScanDwellStartTime = 0; s_uRFScanBestFreqKhz = 0; - s_iRFScanBestNoise = 0; + s_iRFScanBestBusyPct = 0; u32* pChannels = NULL; int nCount = 0; @@ -213,8 +217,10 @@ void rf_scan_periodic_loop() radio_rx_resume_interface(i); s_bRFScanInProgress = false; - log_line("RFScan: scan complete. Best channel: %u kHz (%d dBm).", s_uRFScanBestFreqKhz, s_iRFScanBestNoise); - _rf_scan_send_result_to_central(); + 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; } @@ -236,23 +242,26 @@ void rf_scan_periodic_loop() if ( g_TimeNow < s_uRFScanDwellStartTime + RF_SCAN_DWELL_MS ) return; - // Step 3: read noise and record - int iNoise = _rf_scan_read_noise_dbm(uFreqKhz); + // 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, iNoise); + fprintf(s_pRFScanFile, "%u %d\n", uFreqKhz, iBusyPct); fflush(s_pRFScanFile); } - if ( s_uRFScanBestFreqKhz == 0 || iNoise < s_iRFScanBestNoise ) + if ( iBusyPct >= 0 ) { - s_iRFScanBestNoise = iNoise; - s_uRFScanBestFreqKhz = uFreqKhz; + if ( s_uRFScanBestFreqKhz == 0 || iBusyPct < s_iRFScanBestBusyPct ) + { + s_iRFScanBestBusyPct = iBusyPct; + s_uRFScanBestFreqKhz = uFreqKhz; + } } - log_line("RFScan: channel %d/%d %u kHz noise=%d dBm.", - s_iRFScanCurrentChannel + 1, s_iRFScanChannelsCount, uFreqKhz, iNoise); + log_line("RFScan: channel %d/%d %u kHz busy=%d%%.", + s_iRFScanCurrentChannel + 1, s_iRFScanChannelsCount, uFreqKhz, iBusyPct); s_iRFScanCurrentChannel++; s_uRFScanDwellStartTime = 0; @@ -281,8 +290,3 @@ bool rf_scan_is_in_progress() { return s_bRFScanInProgress; } - -u32 rf_scan_get_best_freq_khz() -{ - return s_uRFScanBestFreqKhz; -} diff --git a/code/r_station/rf_scan.h b/code/r_station/rf_scan.h index f2ef4085..06b4a884 100644 --- a/code/r_station/rf_scan.h +++ b/code/r_station/rf_scan.h @@ -1,8 +1,7 @@ #pragma once #include "../base/base.h" -void rf_scan_start(u32 uBandFlags, int iFDToCentral); +void rf_scan_start(u32 uBandFlags); void rf_scan_periodic_loop(); void rf_scan_stop(); bool rf_scan_is_in_progress(); -u32 rf_scan_get_best_freq_khz(); diff --git a/code/radio/local_packets.h b/code/radio/local_packets.h index 1d4cbe9e..70a7d55b 100644 --- a/code/radio/local_packets.h +++ b/code/radio/local_packets.h @@ -72,4 +72,4 @@ #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_RESULT 232 // vehicle_id_dest: best freq kHz (0 = scan failed) +#define PACKET_TYPE_LOCAL_CONTROLLER_RF_SCAN_STOP 232 // abort an in-progress scan and restore the radio links