/* * Copyright (C) 2019-2025 Roger Clark, VK3KYY / G4KYF * Daniel Caujolle-Bert, F1RMB * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form 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. * * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation, * incorporation in a commercial product or incorporation into a product or project which allows commercial use. * * 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 COPYRIGHT * HOLDER OR CONTRIBUTORS 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 "user_interface/uiGlobals.h" #include "functions/calibration.h" #include "functions/trx.h" #include "user_interface/menuSystem.h" #include "user_interface/uiUtilities.h" #include "user_interface/uiLocalisation.h" #include "hardware/radioHardwareInterface.h" #if defined(PLATFORM_MD2017) #define SECONDARY_DISPLAY_OFFSET 50 #endif static menuStatus_t menuRSSIExitCode = MENU_STATUS_SUCCESS; //static calibrationRSSIMeter_t rssiCalibration; // UNUSED static void updateScreen(bool forceRedraw, bool firstRun); static void handleEvent(uiEvent_t *ev); static void updateVoicePrompts(bool flushIt, bool spellIt); static int dBm[RADIO_DEVICE_MAX] = { 0 #if defined(PLATFORM_MD2017) , 0 #endif }; static uint8_t rawSignal[RADIO_DEVICE_MAX] = { 0 #if defined(PLATFORM_MD2017) , 0 #endif }; static uint8_t rawNoise[RADIO_DEVICE_MAX] = { 0 #if defined(PLATFORM_MD2017) , 0 #endif }; static bool displayRawValues = false; static const int barX = 9; DECLARE_SMETER_ARRAY(rssiMeterBar, (DISPLAY_SIZE_X - (barX - 1))); menuStatus_t menuRSSIScreen(uiEvent_t *ev, bool isFirstRun) { static uint32_t m = 0; if (isFirstRun) { //calibrationGetRSSIMeterParams(&rssiCalibration); // UNUSED menuDataGlobal.numItems = 0; displayClearBuf(); menuDisplayTitle(currentLanguage->rssi); displayRenderRows(0, 2); updateScreen(true, true); } else { menuRSSIExitCode = MENU_STATUS_SUCCESS; if (ev->hasEvent) { handleEvent(ev); } if((ev->time - m) > RSSI_UPDATE_COUNTER_RELOAD) { m = ev->time; updateScreen(false, false); } } return menuRSSIExitCode; } // Returns S-Unit 0..9..10(S9+10dB)..15(S9+60) static int32_t getSignalStrength(int dbm) { for (int8_t i = 15; i >= 0; i--) { if (dbm >= DBM_LEVELS[i]) { return i; } } return 0; } static void drawMeterGraticule(int16_t vOffset) { // Draw S-Meter outer frame displayDrawRect((barX - 2), (vOffset - 2), (DISPLAY_SIZE_X - (barX - 2)), (8 + 4), true); // Clear the right V line of the frame displayDrawFastVLine((DISPLAY_SIZE_X - 1), (vOffset - 1), (8 + 2), false); // S9+xx H Dots for (int16_t i = ((barX - 2) + (rssiMeterBar[9] * 2) + 1); i < DISPLAY_SIZE_X; i += 4) { displayDrawFastHLine(i, (vOffset - 2), 2, false); } // +10..60dB displayFillRect(((barX - 2) + (rssiMeterBar[9] * 2) + 2), (vOffset + 8) + 2, (DISPLAY_SIZE_X - ((barX - 2) + (rssiMeterBar[9] * 2) + 2)), 2, false); // Draw S, Numbers and ticks displayPrintAt(1, vOffset, "S", FONT_SIZE_1_BOLD); int xPos; int currentMode = trxGetMode(); for (uint8_t i = 0; i < 10; i++) { // Scale the bar graph so values S0 - S9 take 70% of the scale width, and signals above S9 take the last 30% // On DMR the max signal is S9+10, so teh entire bar can be the sale scale // ON FM signals above S9, the scale is compressed to 2/STRONG_SIGNAL_RESCALE if ((i <= 9) || (currentMode == RADIO_MODE_DIGITAL)) { xPos = rssiMeterBar[i]; } else { xPos = ((rssiMeterBar[i] - rssiMeterBar[9]) / STRONG_SIGNAL_RESCALE) + rssiMeterBar[9]; } xPos *= 2; // V ticks displayDrawFastVLine(((barX - 2) + xPos), (vOffset + 8) + 2, ((i % 2) ? 3 : 1), ((i < 10) ? true : false)); if ((i % 2) && (i < 10)) { char buf[2]; sprintf(buf, "%d", i); displayPrintAt(((((barX - 2) + xPos) - 2) - 1)/* FONT_2 H offset */, vOffset + 15 #if defined(PLATFORM_RD5R) -1 #endif , buf, FONT_SIZE_2); } } } static void updateScreen(bool forceRedraw, bool isFirstRun) { char buffer[LOCATION_TEXT_BUFFER_SIZE]; int barWidth; int rssi[RADIO_DEVICE_MAX]; int16_t yValuePos, yBarPos; for(RadioDevice_t device = RADIO_DEVICE_PRIMARY; device < RADIO_DEVICE_MAX; device++) { rssi[device] = dBm[device] = trxGetRSSIdBm(device); rawSignal[device] = trxGetSignalRaw(device); rawNoise[device] = trxGetNoiseRaw(device); } if (isFirstRun && (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD)) { voicePromptsInit(); voicePromptsAppendPrompt(PROMPT_SILENCE); voicePromptsAppendLanguageString(currentLanguage->rssi); voicePromptsAppendLanguageString(currentLanguage->menu); voicePromptsAppendPrompt(PROMPT_SILENCE); updateVoicePrompts(false, true); } if (forceRedraw) { yBarPos = DISPLAY_Y_POS_RSSI_BAR; displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Clear whole drawing region displayFillRect(0, 14, DISPLAY_SIZE_X, DISPLAY_SIZE_Y - 14, true); for(RadioDevice_t device = RADIO_DEVICE_PRIMARY; device < RADIO_DEVICE_MAX; device++) { drawMeterGraticule(yBarPos); #if defined(PLATFORM_MD2017) yBarPos += SECONDARY_DISPLAY_OFFSET; #endif } displayThemeResetToDefault(); } else { yValuePos = DISPLAY_Y_POS_RSSI_VALUE; for(RadioDevice_t device = RADIO_DEVICE_PRIMARY; device < RADIO_DEVICE_MAX; device++) { // Clear dBm region value displayFillRect((displayRawValues ? 0 : ((DISPLAY_SIZE_X - (7 * 8)) >> 1)), yValuePos, (displayRawValues ? DISPLAY_SIZE_X : (7 * 8)), FONT_SIZE_3_HEIGHT, true); #if defined(PLATFORM_MD2017) yValuePos += SECONDARY_DISPLAY_OFFSET; #endif } } yValuePos = DISPLAY_Y_POS_RSSI_VALUE; yBarPos = DISPLAY_Y_POS_RSSI_BAR; for(RadioDevice_t device = RADIO_DEVICE_PRIMARY; device < RADIO_DEVICE_MAX; device++) { if (displayRawValues) { snprintf(buffer, LOCATION_TEXT_BUFFER_SIZE, "%d%s [%u %u]", dBm[device], "dBm", rawSignal[device], rawNoise[device]); } else { snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%d%s", dBm[device], "dBm"); } displayPrintCentered(yValuePos, buffer, FONT_SIZE_3); #if 0 // DEBUG sprintf(buffer, "%d", currentRadioDevice->trxRxSignal); displayFillRect((DISPLAY_SIZE_X - (4 * 8)), yValuePos, (4 * 8), 8, true); ucPrintCore((DISPLAY_SIZE_X - ((strlen(buffer) + 1) * 8)), yValuePos, buffer, FONT_SIZE_2, TEXT_ALIGN_RIGHT, false); #endif if ((rssi[device] > SMETER_S9) && (trxGetMode() == RADIO_MODE_ANALOG)) { // In Analog mode, the max RSSI value from the hardware is over S9+60. // So scale this to fit in the last 30% of the display rssi[device] = ((rssi[device] - SMETER_S9) / STRONG_SIGNAL_RESCALE) + SMETER_S9; } // Scale the entire bar by 2. // Because above S9 the values are scaled to 1/5. // This can result in the signal below S9 being doubled in scale (depending on STRONG_SIGNAL_RESCALE) // Signals above S9 the scales is compressed to 2/STRONG_SIGNAL_RESCALE. rssi[device] = (rssi[device] - SMETER_S0) * 2; barWidth = ((rssi[device] * rssiMeterBarNumUnits) / rssiMeterBarDivider); barWidth = CLAMP((barWidth - 1), 0, (DISPLAY_SIZE_X - barX)); if (barWidth) { displayThemeApply(THEME_ITEM_FG_RSSI_BAR, THEME_ITEM_BG); displayFillRect(barX, yBarPos, barWidth, 8, false); displayThemeResetToDefault(); } // Clear the end of the bar area, if needed if (barWidth < (DISPLAY_SIZE_X - barX)) { displayFillRect(barX + barWidth, yBarPos, (DISPLAY_SIZE_X - barX) - barWidth, 8, true); } #if defined(HAS_COLOURS) if (rssi[device] > SMETER_S9) { int xPos; xPos = (rssiMeterBar[9] * 2); if (barWidth > xPos) { displayThemeApply(THEME_ITEM_FG_RSSI_BAR_S9P, THEME_ITEM_BG); displayFillRect((barX + xPos), yBarPos, (barWidth - xPos), 8, false); displayThemeResetToDefault(); } } #endif #if defined(PLATFORM_MD2017) yValuePos += SECONDARY_DISPLAY_OFFSET; yBarPos += SECONDARY_DISPLAY_OFFSET; #endif } if (forceRedraw || uiNotificationIsVisible()) { displayRender(); } else { int16_t yStartValuePos = DISPLAY_Y_POS_RSSI_VALUE; int16_t yStartBarPos = DISPLAY_Y_POS_RSSI_BAR; // Y end positions yValuePos = DISPLAY_Y_POS_RSSI_VALUE + FONT_SIZE_3_HEIGHT; yBarPos = DISPLAY_Y_POS_RSSI_BAR + 8; #if defined(PLATFORM_RD5R) #warning CHECK ME (end pos) displayRenderRows((DISPLAY_Y_POS_RSSI_VALUE / 8), (yValuePos / 8) + 1); #else for(RadioDevice_t device = RADIO_DEVICE_PRIMARY; device < RADIO_DEVICE_MAX; device++) { displayRenderRows((yStartValuePos / 8), (yValuePos / 8) + 1); displayRenderRows((yStartBarPos / 8), (yBarPos / 8) + 1); #if defined(PLATFORM_MD2017) yStartValuePos += SECONDARY_DISPLAY_OFFSET; yStartBarPos += SECONDARY_DISPLAY_OFFSET; yValuePos += SECONDARY_DISPLAY_OFFSET; yBarPos += SECONDARY_DISPLAY_OFFSET; #endif } #endif } } static void handleEvent(uiEvent_t *ev) { if ((ev->events & FUNCTION_EVENT) && (ev->function == FUNC_REDRAW)) { updateScreen(true, false); return; } if (ev->events & BUTTON_EVENT) { bool wasPlaying = false; if (BUTTONCHECK_SHORTUP(ev, BUTTON_SK1) && (ev->keys.key == 0)) { // Stop playback or update signal strength if ((wasPlaying = voicePromptsIsPlaying()) == false) { updateVoicePrompts(true, false); } } if (repeatVoicePromptOnSK1(ev)) { if (wasPlaying && voicePromptsIsPlaying()) { voicePromptsTerminate(); } return; } } if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN) || KEYCHECK_SHORTUP(ev->keys, KEY_RED)) { menuSystemPopPreviousMenu(); } else if (KEYCHECK_SHORTUP(ev->keys, KEY_STAR)) { displayRawValues = !displayRawValues; updateScreen(true, false); } else if (KEYCHECK_SHORTUP_NUMBER(ev->keys) && (BUTTONCHECK_DOWN(ev, BUTTON_SK2))) { saveQuickkeyMenuIndex(ev->keys.key, menuSystemGetCurrentMenuNumber(), 0, 0); } } static void updateVoicePrompts(bool flushIt, bool spellIt) { if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD) { uint8_t S = getSignalStrength(dBm[RADIO_DEVICE_PRIMARY]); if (flushIt) { voicePromptsInit(); } voicePromptsAppendPrompt(PROMPT_S); voicePromptsAppendPrompt(PROMPT_SILENCE); if (S > 9) { voicePromptsAppendPrompt(PROMPT_9); voicePromptsAppendPrompt(PROMPT_PLUS); voicePromptsAppendInteger(10 * ((S - 10) + 1)); } else { voicePromptsAppendPrompt(PROMPT_0 + S); } if (spellIt) { promptsPlayNotAfterTx(); } } }