/* * 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 "user_interface/menuSystem.h" #include "user_interface/uiLocalisation.h" #include "user_interface/uiUtilities.h" #include "utils.h" #include "interfaces/pit.h" #include "functions/satellite.h" #if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017) #include "interfaces/batteryAndPowerManagement.h" #include "semphr.h" #include "interfaces/gps.h" #endif static SemaphoreHandle_t battSemaphore = NULL; #define DOUBLE_ARROW_HEIGHT 6 #define VOLTAGE_BUFFER_LEN 128 static const float BATTERY_CRITICAL_VOLTAGE = 66.7f; static const int TEMPERATURE_CRITICAL = 500; // 50°C static const uint8_t daysPerMonth[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 }; static float prevAverageBatteryVoltage = 0.0f; static int prevTemperature = 0; static char keypadInputDigits[17]; // HHMMSS + terminator (Displayed as HH:MM:SS, or YYYY:MM:DD or LAT.LATIT,LON.LONGI) static int keypadInputDigitsLength = 0; static menuStatus_t menuRadioInfosExitCode = MENU_STATUS_SUCCESS; //static int32_t timeClockPITOffset = 0; // Time difference between PITCounter and real time. #define VP_TIMEBUF_SIZE 9 static uint32_t hours; static uint32_t minutes; static uint32_t seconds; static struct tm timeAndDate; bool latLonIsSouthernHemisphere = false; bool latLonIsWesternHemisphere = false; typedef struct { int32_t buffer[VOLTAGE_BUFFER_LEN]; int32_t *head; int32_t *tail; int32_t *end; bool modified; } voltageCircularBuffer_t; #if defined(PLATFORM_MD9600) || defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017) __attribute__((section(".ccmram"))) #else __attribute__((section(".data.$RAM2"))) #endif voltageCircularBuffer_t batteryVoltageHistory; enum { GRAPH_FILL = 0, GRAPH_LINE }; static int displayMode = RADIO_INFOS_BATTERY_LEVEL; static bool pureBatteryLevel = false; static int graphStyle = GRAPH_FILL; static int battery_stack_iter = 0; static const int BATTERY_ITER_PUSHBACK = 20; static void updateScreen(uiEvent_t *ev, bool forceRedraw); static void handleEvent(uiEvent_t *ev); static void updateVoicePrompts(bool spellIt, bool firstRun); static uint32_t menuRadioInfosNextUpdateTime = 0; static void circularBufferInit(voltageCircularBuffer_t *cb) { cb->end = &cb->buffer[VOLTAGE_BUFFER_LEN - 1]; cb->head = cb->buffer; cb->tail = cb->buffer; cb->modified = false; } static void circularBufferPushBack(voltageCircularBuffer_t *cb, const int32_t item) { cb->modified = true; *cb->head = item; cb->head++; if(cb->head == cb->end) { cb->head = cb->buffer; } if (cb->tail == cb->head) { cb->tail++; if(cb->tail == cb->end) { cb->tail = cb->buffer; } } } static size_t circularBufferGetData(voltageCircularBuffer_t *cb, int32_t *data, size_t dataLen) { size_t count = 0; int32_t *p = cb->tail; while ((p != cb->head) && (count < dataLen)) { *(data + count) = *p; p++; count++; if (p == cb->end) { p = cb->buffer; } } return count; } static uint32_t inputDigitsLonToFixed_10_5(void) { uint32_t inPart = ((keypadInputDigits[6] - '0') * 100) + ((keypadInputDigits[7] - '0') * 10) + ((keypadInputDigits[8] - '0') * 1); uint32_t decimalPart = ((keypadInputDigits[9] - '0') * 10000) + ((keypadInputDigits[10] - '0') * 1000) + ((keypadInputDigits[11] - '0') * 100) + ((keypadInputDigits[12] - '0') * 10); uint32_t fixedVal = (inPart << 23) + decimalPart; if (latLonIsWesternHemisphere) { fixedVal |= 0x80000000;// set MSB to indicate negative number / southern hemisphere } return fixedVal; } static uint32_t inputDigitsLatToFixed_10_5(void) { uint32_t inPart = ((keypadInputDigits[0] - '0') * 10) + ((keypadInputDigits[1] - '0') * 1); uint32_t decimalPart = ((keypadInputDigits[2] - '0') * 10000) + ((keypadInputDigits[3] - '0') * 1000) + ((keypadInputDigits[4] - '0') * 100)+ ((keypadInputDigits[5] - '0') * 10); uint32_t fixedVal = (inPart << 23) + decimalPart; if (latLonIsSouthernHemisphere) { fixedVal |= 0x80000000;// set MSB to indicate negative number / southrern hemisphere } return fixedVal; } menuStatus_t menuRadioInfos(uiEvent_t *ev, bool isFirstRun) { if (isFirstRun) { keypadInputDigits[0] = 0; keypadInputDigitsLength = 0; menuDataGlobal.numItems = NUM_RADIO_INFOS_MENU_ITEMS; displayClearBuf(); menuDisplayTitle(currentLanguage->radio_info); displayRenderRows(0, 2); if (settingsLocationIsValid()) { if (nonVolatileSettings.location.lat & 0x80000000) { latLonIsSouthernHemisphere = true; } if (nonVolatileSettings.location.lon & 0x80000000) { latLonIsWesternHemisphere = true; } } updateScreen(ev, true); updateVoicePrompts(true, true); } else { if (ev->time > menuRadioInfosNextUpdateTime) { menuRadioInfosNextUpdateTime = ev->time + 500; updateScreen(ev, false);// update the screen each 500ms to show any changes to the battery voltage or low battery } menuRadioInfosExitCode = MENU_STATUS_SUCCESS; if (ev->hasEvent) { handleEvent(ev); } } return menuRadioInfosExitCode; } static void updateScreen(uiEvent_t *ev, bool forceRedraw) { static bool blink = false; bool renderArrowOnly = true; char buffer[SCREEN_LINE_BUFFER_SIZE]; #if defined(HAS_COLOURS) bool dblHeight = settingsIsOptionBitSet(BIT_UI_USES_DOUBLE_HEIGHT); #else const bool dblHeight = false; #endif switch (displayMode) { case RADIO_INFOS_BATTERY_LEVEL: { if ((prevAverageBatteryVoltage != averageBatteryVoltage) || (averageBatteryVoltage < BATTERY_CRITICAL_VOLTAGE) || pureBatteryLevel || forceRedraw) { #if defined(PLATFORM_MD9600) int volts, mvolts; const int x = 72; const int battLevelHeight = 28; prevAverageBatteryVoltage = averageBatteryVoltage; renderArrowOnly = false; if (forceRedraw) { displayClearBuf(); menuDisplayTitle(currentLanguage->battery); // Draw... // Inner body frame displayDrawRoundRect(x + 1, 23, 49, 34, 3, true); // Outer body frame displayDrawRoundRect(x, 22, 51, 36, 3, true); // Positive poles frame displayFillRoundRect(x + 5, 18, 9, 6, 2, true); displayFillRoundRect(x + 37, 18, 9, 6, 2, true); } else { // Clear voltage area displayFillRect(0, (((DISPLAY_SIZE_Y - (14 + FONT_SIZE_3_HEIGHT)) >> 1) + 14), x - 1, 16, true); // Clear level area displayFillRoundRect(x + 4, 26, 43, battLevelHeight, 2, false); } // Want to display instant battery voltage, not the averaged value. if (pureBatteryLevel) { volts = (int)(batteryVoltage / 10); mvolts = (int)(batteryVoltage - (volts * 10)); } else { getBatteryVoltage(&volts, &mvolts); } snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%2d.%1dV", volts, mvolts); displayPrintAt(((x - (5 * 8)) >> 1), (((DISPLAY_SIZE_Y - (14 + FONT_SIZE_3_HEIGHT)) >> 1) + 14), buffer, FONT_SIZE_3); if (pureBatteryLevel == false) { uint32_t h = (uint32_t)(((averageBatteryVoltage - CUTOFF_VOLTAGE_UPPER_HYST) * battLevelHeight) / (BATTERY_MAX_VOLTAGE - CUTOFF_VOLTAGE_UPPER_HYST)); if (h > battLevelHeight) { h = battLevelHeight; } // Draw Level displayFillRoundRect(x + 4, 26 + battLevelHeight - h , 43, h, 2, (averageBatteryVoltage < BATTERY_CRITICAL_VOLTAGE) ? blink : true); } else { displayPrintCore(x + (20 / 2), 26 + ((battLevelHeight - FONT_SIZE_3_HEIGHT) >> 1), "?", FONT_SIZE_3, TEXT_ALIGN_LEFT, blink); } if (voicePromptsIsPlaying() == false) { updateVoicePrompts(false, false); } #else int volts, mvolts; const int x = 88; const int battLevelHeight = (DISPLAY_SIZE_Y - 28); prevAverageBatteryVoltage = averageBatteryVoltage; renderArrowOnly = false; if (forceRedraw) { displayClearBuf(); menuDisplayTitle(currentLanguage->battery); displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Draw... // Inner body frame displayDrawRoundRect(x + 1, 20, 26 + DISPLAY_H_EXTRA_PIXELS, DISPLAY_SIZE_Y - 22, 3, true); // Outer body frame displayDrawRoundRect(x, 19, 28 + DISPLAY_H_EXTRA_PIXELS, DISPLAY_SIZE_Y - 20, 3, true); // Positive pole frame displayFillRoundRect(x + 9 + DISPLAY_H_OFFSET, 15, 10, 6, 2, true); } else { displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Clear voltage area displayFillRect(((x - (4 * 8)) >> 1), 19 + 1, (4 * 8), (DISPLAY_SIZE_Y - 20) - 4, true); // Clear level area displayFillRoundRect(x + 4, 23, 20 + DISPLAY_H_EXTRA_PIXELS, battLevelHeight, 2, false); } displayThemeResetToDefault(); // Want to display instant battery voltage, not the averaged value. if (pureBatteryLevel) { volts = (int)(batteryVoltage / 10); mvolts = (int)(batteryVoltage - (volts * 10)); } else { getBatteryVoltage(&volts, &mvolts); } snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%1d.%1dV", volts, mvolts); displayPrintAtDoubleHeight(((x - (4 * 8)) >> 1), 19 + 1, buffer, FONT_SIZE_3, dblHeight); if (pureBatteryLevel == false) { snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%d%%", getBatteryPercentage()); displayPrintAtDoubleHeight(((x - (strlen(buffer) * 8)) >> 1), (DISPLAY_SIZE_Y - 20 - (dblHeight ? FONT_SIZE_3_HEIGHT : 0)) #if defined(PLATFORM_RD5R) + 7 #endif , buffer, FONT_SIZE_3, dblHeight); uint32_t h = (uint32_t)(((averageBatteryVoltage - CUTOFF_VOLTAGE_UPPER_HYST) * battLevelHeight) / (BATTERY_MAX_VOLTAGE - CUTOFF_VOLTAGE_UPPER_HYST)); if (h > battLevelHeight) { h = battLevelHeight; } displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Draw Level displayFillRoundRect(x + 4, 23 + battLevelHeight - h , 20 + DISPLAY_H_EXTRA_PIXELS, h, 2, (averageBatteryVoltage < BATTERY_CRITICAL_VOLTAGE) ? blink : true); } else { displayPrintCoreDoubleHeight(x + ((20 + DISPLAY_H_EXTRA_PIXELS) / 2), 23 + ((battLevelHeight - (FONT_SIZE_3_HEIGHT * (dblHeight ? 2 : 1))) >> 1), "?", FONT_SIZE_3, TEXT_ALIGN_LEFT, blink, dblHeight); } if (voicePromptsIsPlaying() == false) { updateVoicePrompts(false, false); } #endif } displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Low blinking arrow displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 1), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 5), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 5), blink); } break; case RADIO_INFOS_CURRENT_TIME: { displayClearBuf(); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%s%s", currentLanguage->time, ((nonVolatileSettings.timezone & 0x80) ? "" : " UTC")); menuDisplayTitle(buffer); if (keypadInputDigitsLength == 0) { time_t_custom t = uiDataGlobal.dateTimeSecs + ((nonVolatileSettings.timezone & 0x80) ? ((nonVolatileSettings.timezone & 0x7F) - 64) * (15 * 60) : 0); gmtime_r_Custom(&t, &timeAndDate); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%02u:%02u:%02u", timeAndDate.tm_hour, timeAndDate.tm_min, timeAndDate.tm_sec); if (voicePromptsIsPlaying() == false) { updateVoicePrompts(false, false); } } else { strcpy(buffer,"__:__:__"); int bufPos = 0; for (int i = 0; i < keypadInputDigitsLength; i++) { buffer[bufPos++] = keypadInputDigits[i]; if ((bufPos == 2) || (bufPos == 5)) { bufPos++; } } displayThemeApply(THEME_ITEM_FG_TEXT_INPUT, THEME_ITEM_BG); } displayPrintCenteredDoubleHeight((((DISPLAY_SIZE_Y >> 1) + MENU_HEADER_HEIGHT) - (dblHeight ? FONT_SIZE_4_HEIGHT : (FONT_SIZE_4_HEIGHT >> 1)) - DOUBLE_ARROW_HEIGHT), buffer, FONT_SIZE_4, dblHeight); renderArrowOnly = false; displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Up/Down blinking arrow displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 1), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 5), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); } break; case RADIO_INFOS_DATE: displayClearBuf(); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%s %s", currentLanguage->date, ((nonVolatileSettings.timezone & 0x80) ? "" : "UTC")); menuDisplayTitle(buffer); if (keypadInputDigitsLength == 0) { time_t_custom t = uiDataGlobal.dateTimeSecs + ((nonVolatileSettings.timezone & 0x80) ? ((nonVolatileSettings.timezone & 0x7F) - 64) * (15 * 60) : 0); gmtime_r_Custom(&t, &timeAndDate); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%04u-%02u-%02u", (timeAndDate.tm_year + 1900), (timeAndDate.tm_mon + 1), timeAndDate.tm_mday); if (voicePromptsIsPlaying() == false) { updateVoicePrompts(false, false); } } else { strcpy(buffer, "____-__-__"); int bufPos = 0; for(int i = 0; i < keypadInputDigitsLength; i++) { buffer[bufPos++] = keypadInputDigits[i]; if ((bufPos == 4) || (bufPos == 7)) { bufPos++; } } displayThemeApply(THEME_ITEM_FG_TEXT_INPUT, THEME_ITEM_BG); } displayPrintCenteredDoubleHeight((((DISPLAY_SIZE_Y >> 1) + MENU_HEADER_HEIGHT) - (dblHeight ? FONT_SIZE_3_HEIGHT : (FONT_SIZE_3_HEIGHT >> 1)) - DOUBLE_ARROW_HEIGHT), buffer, FONT_SIZE_3, dblHeight); renderArrowOnly = false; displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Up/Down blinking arrow displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 1), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 5), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); break; case RADIO_INFOS_LOCATION: displayClearBuf(); menuDisplayTitle(currentLanguage->location); { char maidenheadBuf[7]; if (keypadInputDigitsLength == 0) { bool locIsValid = settingsLocationIsValid(); buildLocationAndMaidenheadStrings(buffer, maidenheadBuf, locIsValid); if (locIsValid == false) { displayPrintCenteredDoubleHeight(((((DISPLAY_SIZE_Y / 3) * 2) + MENU_HEADER_HEIGHT) - (dblHeight ? FONT_SIZE_3_HEIGHT : (FONT_SIZE_3_HEIGHT >> 1)) - DOUBLE_ARROW_HEIGHT) #if defined(PLATFORM_RD5R) - 4 #endif , currentLanguage->not_set, FONT_SIZE_3, dblHeight); } if (voicePromptsIsPlaying() == false) { updateVoicePrompts(false, false); } } else { int bufPos = 0; sprintf(buffer, "__.____%c ___.____%c", currentLanguageGetSymbol(latLonIsSouthernHemisphere ? SYMBOLS_SOUTH : SYMBOLS_NORTH), currentLanguageGetSymbol(latLonIsWesternHemisphere ? SYMBOLS_WEST : SYMBOLS_EAST)); for(int i = 0; i < keypadInputDigitsLength; i++) { buffer[bufPos++] = keypadInputDigits[i]; if ((bufPos == 2) || (bufPos == 12)) { bufPos++; } else if (bufPos == 7) { bufPos +=2; } } maidenheadBuf[0] = 0; displayThemeApply(THEME_ITEM_FG_TEXT_INPUT, THEME_ITEM_BG); } #if defined(HAS_COLOURS) ucFont_t fontSize = FONT_SIZE_3; uint16_t fontHeight = FONT_SIZE_3_HEIGHT; #else ucFont_t fontSize = FONT_SIZE_1; uint16_t fontHeight = FONT_SIZE_1_HEIGHT; #endif displayPrintCenteredDoubleHeight((((DISPLAY_SIZE_Y / 3) + MENU_HEADER_HEIGHT) - (dblHeight ? fontHeight : (fontHeight >> 1)) - DOUBLE_ARROW_HEIGHT), buffer, fontSize, dblHeight); displayPrintCenteredDoubleHeight(((((DISPLAY_SIZE_Y / 3) * 2) + MENU_HEADER_HEIGHT) - (dblHeight ? FONT_SIZE_3_HEIGHT : (FONT_SIZE_3_HEIGHT >> 1)) - DOUBLE_ARROW_HEIGHT) #if defined(PLATFORM_RD5R) - 4 #endif , maidenheadBuf, FONT_SIZE_3, dblHeight); } renderArrowOnly = false; displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Up/Down blinking arrow displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 1), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 5), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); break; case RADIO_INFOS_TEMPERATURE_LEVEL: { int temperature = getTemperature(); if ((prevTemperature != temperature) || (temperature > TEMPERATURE_CRITICAL) || forceRedraw) { const int x = 102 + DISPLAY_H_OFFSET; #if defined(PLATFORM_RD5R) const int temperatureHeight = (DISPLAY_SIZE_Y - 34); const int tankVCenter = (DISPLAY_SIZE_Y - 10); const int tankRadius = 9; #else const int temperatureHeight = (DISPLAY_SIZE_Y - 40); const int tankVCenter = (DISPLAY_SIZE_Y - 14); const int tankRadius = 11; #endif prevTemperature = temperature; renderArrowOnly = false; if (forceRedraw) { displayClearBuf(); menuDisplayTitle(currentLanguage->temperature); displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Body frame displayDrawCircleHelper(x, 20 + 2, 7, (1 | 2), true); displayDrawCircleHelper(x, 20 + 2, 6, (1 | 2), true); displaySetPixel(x, (20 + 2) - 7, true); displayDrawFastVLine(x - 7, 20 + 2, temperatureHeight - 1, true); displayDrawFastVLine(x - 6, 20 + 2, temperatureHeight - 2, true); displayDrawFastVLine(x + 6, 20 + 2, temperatureHeight - 2, true); displayDrawFastVLine(x + 7, 20 + 2, temperatureHeight - 1, true); displayDrawCircle(x, tankVCenter, tankRadius, true); displayDrawCircle(x, tankVCenter, tankRadius - 1, true); displayFillCircle(x, tankVCenter, tankRadius - 3, ((temperature > TEMPERATURE_CRITICAL) ? !blink : true)); displayFillRect(x - 5, 20, 11, temperatureHeight, true); // H lines, min/max markers displayDrawFastHLine(x - (7 + 5), 20, 5, true); // MAX: 70°C displayDrawFastHLine(x - (7 + 5), (temperatureHeight + 20) - 1, 5, true); // MIN: 10°C } else { displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Clear temperature text area displayFillRect((((x - (7 + 5)) - (7 * 8)) >> 1), 20, (7 * 8), (DISPLAY_SIZE_Y - 20) - 4, true); // Clear thermo area displayFillCircle(x, tankVCenter, tankRadius - 3, ((temperature > TEMPERATURE_CRITICAL) ? !blink : true)); displayFillRect(x - 4, 20, 9, temperatureHeight, true); } displayThemeResetToDefault(); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%3d.%1d%s", (temperature / 10), abs(temperature % 10), currentLanguage->celcius); displayPrintAtDoubleHeight((((x - (7 + 5)) - (7 * 8)) >> 1), (((DISPLAY_SIZE_Y >> 1) + MENU_HEADER_HEIGHT) - (dblHeight ? FONT_SIZE_3_HEIGHT : (FONT_SIZE_3_HEIGHT >> 1)) - DOUBLE_ARROW_HEIGHT), buffer, FONT_SIZE_3, dblHeight); uint32_t t = (uint32_t)((((CLAMP(temperature, 100, 700)) - 100) * temperatureHeight) / (700 - 100)); // clamp to 10..70 °C, then scale // Draw Level if (t) { displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); displayFillRect(x - 4, 20 + temperatureHeight - t , 9, t, (temperature > TEMPERATURE_CRITICAL) ? blink : false); } if (voicePromptsIsPlaying() == false) { updateVoicePrompts(false, false); } } displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Up/Down blinking arrow displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 1), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 5), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); } break; case RADIO_INFOS_BATTERY_GRAPH: { #define CHART_WIDTH 104 static int32_t hist[CHART_WIDTH]; static size_t histLen = 0; bool newHistAvailable = false; // Grab history values. // There is a 10 ticks timeout, if it kicks in, history length will be 0, then // redraw will be done on the next run if (xSemaphoreTake(battSemaphore, (TickType_t)10) == pdTRUE) { if ((newHistAvailable = batteryVoltageHistory.modified) == true) { histLen = circularBufferGetData(&batteryVoltageHistory, hist, (sizeof(hist) / sizeof(hist[0]))); batteryVoltageHistory.modified = false; } xSemaphoreGive(battSemaphore); } if (newHistAvailable || forceRedraw) { static const uint8_t chartX = 2 + (2 * 6) + 3 + 2 + DISPLAY_H_OFFSET; static const uint8_t chartY = 14 + 1 + 2; const int chartHeight = (DISPLAY_SIZE_Y - 26); #if defined(PLATFORM_MD9600) // Min is 10V, Max is 15V // Pick: MIN @ 11V, MAX @ 14V uint32_t minVH = (uint32_t)(((110 - CUTOFF_VOLTAGE_UPPER_HYST) * chartHeight) / (BATTERY_MAX_VOLTAGE - CUTOFF_VOLTAGE_UPPER_HYST)); uint32_t maxVH = (uint32_t)(((140 - CUTOFF_VOLTAGE_UPPER_HYST) * chartHeight) / (BATTERY_MAX_VOLTAGE - CUTOFF_VOLTAGE_UPPER_HYST)); #else // Min is 6.4V, Max is 8.2V // Pick: MIN @ 7V, MAX @ 8V uint32_t minVH = (uint32_t)(((70 - CUTOFF_VOLTAGE_UPPER_HYST) * chartHeight) / (BATTERY_MAX_VOLTAGE - CUTOFF_VOLTAGE_UPPER_HYST)); uint32_t maxVH = (uint32_t)(((80 - CUTOFF_VOLTAGE_UPPER_HYST) * chartHeight) / (BATTERY_MAX_VOLTAGE - CUTOFF_VOLTAGE_UPPER_HYST)); #endif renderArrowOnly = false; // Redraw chart's axes, ticks and so on if (forceRedraw) { displayClearBuf(); menuDisplayTitle(currentLanguage->battery); displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // 2 axis chart displayDrawFastVLine(chartX - 3, chartY - 2, chartHeight + 2 + 3, true); displayDrawFastVLine(chartX - 2, chartY - 2, chartHeight + 2 + 2, true); displayDrawFastHLine(chartX - 3, chartY + chartHeight + 2, CHART_WIDTH + 3 + 3, true); displayDrawFastHLine(chartX - 2, chartY + chartHeight + 1, CHART_WIDTH + 3 + 2, true); // Min/Max Voltage ticks and values displayDrawFastHLine(chartX - 6, (chartY + chartHeight) - minVH, 3, true); #if defined(PLATFORM_MD9600) displayPrintAt(DISPLAY_H_OFFSET, ((chartY + chartHeight) - minVH) - 3, "11V", FONT_SIZE_1); displayDrawFastHLine(chartX - 6, (chartY + chartHeight) - maxVH, 3, true); displayPrintAt(DISPLAY_H_OFFSET, ((chartY + chartHeight) - maxVH) - 3, "14V", FONT_SIZE_1); #elif defined(PLATFORM_GD77) || defined(PLATFORM_GD77S) || defined(PLATFORM_DM1801) || defined(PLATFORM_DM1801A) || defined(PLATFORM_RD5R) displayPrintAt(chartX - 3 - 12 - 3, ((chartY + chartHeight) - minVH) - 3, "7V", FONT_SIZE_1); displayDrawFastHLine(chartX - 6, (chartY + chartHeight) - maxVH, 3, true); displayPrintAt(chartX - 3 - 12 - 3, ((chartY + chartHeight) - maxVH) - 3, "8V", FONT_SIZE_1); #else // Other STM32 platforms displayPrintAtDoubleHeight(DISPLAY_H_OFFSET, ((chartY + chartHeight) - minVH) - (3 * (dblHeight ? 2 : 1)), "7V", FONT_SIZE_1, dblHeight); displayDrawFastHLine(chartX - 6, (chartY + chartHeight) - maxVH, 3, true); displayPrintAtDoubleHeight(DISPLAY_H_OFFSET, ((chartY + chartHeight) - maxVH) - (3 * (dblHeight ? 2 : 1)), "8V", FONT_SIZE_1, dblHeight); #endif // Time ticks for (uint8_t i = 0; i < CHART_WIDTH + 2; i += 22 /* ~ 15 minutes */) { displaySetPixel(chartX + i, (chartY + chartHeight) + 3, true); } } else { displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); displayFillRect(chartX, chartY, CHART_WIDTH, chartHeight, true); } // Draw chart values, according to style for (size_t i = 0; i < histLen; i++) { uint32_t y = (uint32_t)((((CLAMP(hist[i], CUTOFF_VOLTAGE_UPPER_HYST, BATTERY_MAX_VOLTAGE)) - CUTOFF_VOLTAGE_UPPER_HYST) * chartHeight) / (BATTERY_MAX_VOLTAGE - CUTOFF_VOLTAGE_UPPER_HYST)); if (graphStyle == GRAPH_FILL) { displayDrawFastVLine(chartX + i, ((chartY + chartHeight) - y), y, true); } else { displaySetPixel(chartX + i, ((chartY + chartHeight) - y), true); } } // Min/Max dot lines for (uint8_t i = 0; i < CHART_WIDTH + 2; i++) { displaySetPixel(chartX + i, ((chartY + chartHeight) - minVH), (i % 2) ? false : true); displaySetPixel(chartX + i, ((chartY + chartHeight) - maxVH), (i % 2) ? false : true); } } else { displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); } // Up blinking arrow displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 5), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); if (voicePromptsIsPlaying() == false) { updateVoicePrompts(false, false); } } break; case RADIO_INFOS_UP_TIME: { displayClearBuf(); menuDisplayTitle(currentLanguage->uptime); uint32_t timeInSeconds = 0;// (ev->time + uiDataGlobal.timeClockPITOffset) / 1000; hours = timeInSeconds / (60 * 60); minutes = (timeInSeconds % (60 * 60)) / 60; snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%u %s", hours, currentLanguage->hours); displayPrintCentered((DISPLAY_SIZE_Y / 2) - 8, buffer, FONT_SIZE_3); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%u %s", minutes, currentLanguage->minutes); displayPrintCentered(((DISPLAY_SIZE_Y * 3) / 4) - 8, buffer, FONT_SIZE_3); if (voicePromptsIsPlaying() == false) { updateVoicePrompts(false, false); } renderArrowOnly = false; displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Up/Down blinking arrow displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 1), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 5), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); } break; case RADIO_INFOS_TIME_ALARM: { displayClearBuf(); menuDisplayTitle(currentLanguage->alarm_time); if (keypadInputDigitsLength == 0) { hours = (uiDataGlobal.SatelliteAndAlarmData.alarmTime % (60 * 60 * 24))/ (60 * 60); minutes = (uiDataGlobal.SatelliteAndAlarmData.alarmTime % (60 * 60)) / 60; seconds = (uiDataGlobal.SatelliteAndAlarmData.alarmTime % 60); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%02u:%02u:%02u", hours, minutes, seconds); if (voicePromptsIsPlaying() == false) { updateVoicePrompts(false, false); } } else { strcpy(buffer,"__:__:__"); int bufPos = 0; for(int i = 0; i < keypadInputDigitsLength; i++) { buffer[bufPos++] = keypadInputDigits[i]; if ((bufPos == 2) || (bufPos == 5)) { bufPos++; } } displayThemeApply(THEME_ITEM_FG_TEXT_INPUT, THEME_ITEM_BG); } displayPrintCentered((DISPLAY_SIZE_Y / 2) - 8, buffer, FONT_SIZE_4); renderArrowOnly = false; displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG); // Up/Down blinking arrow displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 1), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); displayFillTriangle(63 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 5), 59 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), 67 + DISPLAY_H_OFFSET, (DISPLAY_SIZE_Y - 3), blink); } break; } blink = !blink; displayThemeResetToDefault(); displayRenderRows((renderArrowOnly ? (DISPLAY_NUMBER_OF_ROWS - 1) : 0), DISPLAY_NUMBER_OF_ROWS); } static void handleEvent(uiEvent_t *ev) { if (ev->events & FUNCTION_EVENT) { if (ev->function == FUNC_REDRAW) { updateScreen(ev, true); return; } else if (QUICKKEY_TYPE(ev->function) == QUICKKEY_MENU) { displayMode = QUICKKEY_ENTRYID(ev->function); updateScreen(ev, true); updateVoicePrompts(true, false); return; } } if (ev->events & BUTTON_EVENT) { if (repeatVoicePromptOnSK1(ev)) { return; } if ((pureBatteryLevel == false) && BUTTONCHECK_EXTRALONGDOWN(ev, BUTTON_SK2)) { pureBatteryLevel = true; updateScreen(ev, true); } else if (pureBatteryLevel && (BUTTONCHECK_EXTRALONGDOWN(ev, BUTTON_SK2) == 0)) { pureBatteryLevel = false; updateScreen(ev, true); } } if ((KEYCHECK_SHORTUP(ev->keys, KEY_GREEN) && BUTTONCHECK_DOWN(ev, BUTTON_SK2)) #if defined(PLATFORM_MD9600) // Let the operator to set the GPS power status to GPS_MODE_ON only, on longpress GREEN (A/B mic button also). || (KEYCHECK_LONGDOWN(ev->keys, KEY_GREEN) && (SETTINGS_GPS_MODE_GET(nonVolatileSettings) > GPS_NOT_DETECTED) && (SETTINGS_GPS_MODE_GET(nonVolatileSettings) < GPS_MODE_ON)) #endif ) { if (displayMode == RADIO_INFOS_LOCATION) { #if defined(PLATFORM_MD9600) || defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017) gpsOnUsingQuickKey(true); #endif } return; } if ((ev->keys.event & (KEY_MOD_UP | KEY_MOD_LONG)) == KEY_MOD_UP) { switch(ev->keys.key) { case KEY_GREEN: if (keypadInputDigitsLength == 0) { menuSystemPopPreviousMenu(); } else { switch(displayMode) { case RADIO_INFOS_DATE: { // get the current date time because the time is constantly changing time_t_custom t = uiDataGlobal.dateTimeSecs + ((nonVolatileSettings.timezone & 0x80) ? ((nonVolatileSettings.timezone & 0x7F) - 64) * (15 * 60) : 0); gmtime_r_Custom(&t, &timeAndDate);// get the current date time as the date may have changed since starting to enter the time. timeAndDate.tm_mon = (((keypadInputDigits[4] - '0') * 10) + (keypadInputDigits[5] - '0')) - 1; timeAndDate.tm_mday = ((keypadInputDigits[6] - '0') * 10) + (keypadInputDigits[7] - '0'); timeAndDate.tm_year = (((keypadInputDigits[0] - '0') * 1000) + ((keypadInputDigits[1] - '0') * 100) + ((keypadInputDigits[2] - '0') * 10) + ((keypadInputDigits[3] - '0'))) - 1900; uiSetUTCDateTimeInSecs(mktime_custom(&timeAndDate) - ((nonVolatileSettings.timezone & 0x80)?((nonVolatileSettings.timezone & 0x7F) - 64) * (15 * 60):0)); #if defined(PLATFORM_MD9600) || defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017) setRtc_custom(uiDataGlobal.dateTimeSecs); #endif keypadInputDigits[0] = 0;// clear digits keypadInputDigitsLength = 0; menuSatelliteScreenClearPredictions(false); nextKeyBeepMelody = (int16_t *)MELODY_ACK_BEEP; updateScreen(ev, true); } break; case RADIO_INFOS_LOCATION: if (keypadInputDigitsLength == 13) { settingsSet(nonVolatileSettings.location.lat, inputDigitsLatToFixed_10_5()); settingsSet(nonVolatileSettings.location.lon, inputDigitsLonToFixed_10_5()); keypadInputDigits[0] = 0;// clear digits keypadInputDigitsLength = 0; menuSatelliteScreenClearPredictions(false); aprsBeaconingInvalidateFixedPosition(); aprsBeaconingSetCurrentLocation(settingsLocationGetLatitude(), settingsLocationGetLongitude()); // Roaming is enabled, and root menu is Channel mode => trigger distance sorting. if ((nonVolatileSettings.roaming != ROAMING_OFF) && (menuSystemGetRootMenuNumber() == UI_CHANNEL_MODE) && (CODEPLUG_ZONE_IS_ALLCHANNELS(currentZone) == false)) { currentChannelData->rxFreq = 0U; } updateScreen(ev, true); nextKeyBeepMelody = (int16_t *)MELODY_ACK_BEEP; } else { nextKeyBeepMelody = (int16_t *)MELODY_NACK_BEEP; } break; default: { const int multipliers[6] = { 36000, 3600, 600, 60, 10, 1 }; if (keypadInputDigitsLength == 6) { int newTime = 0; for(int i = 0; i < 6; i++) { newTime += (keypadInputDigits[i] - '0') * multipliers[i]; } if (displayMode == RADIO_INFOS_CURRENT_TIME) { PIT2SecondsCounter = 0;// Stop PIT2SecondsCounter rolling over during the next operations time_t_custom t = uiDataGlobal.dateTimeSecs + ((nonVolatileSettings.timezone & 0x80)?((nonVolatileSettings.timezone & 0x7F) - 64) * (15 * 60):0); gmtime_r_Custom(&t, &timeAndDate);// get the current date time as the date may have changed since starting to enter the time. timeAndDate.tm_hour = ((keypadInputDigits[0] - '0') * 10) + (keypadInputDigits[1] - '0'); timeAndDate.tm_min = ((keypadInputDigits[2] - '0') * 10) + (keypadInputDigits[3] - '0'); timeAndDate.tm_sec = ((keypadInputDigits[4] - '0') * 10) + (keypadInputDigits[5] - '0'); PIT2SecondsCounter = 0;//Synchronise PIT2SecondsCounter uiSetUTCDateTimeInSecs(mktime_custom(&timeAndDate) - ((nonVolatileSettings.timezone & 0x80)?((nonVolatileSettings.timezone & 0x7F) - 64) * (15 * 60):0)); #if defined(PLATFORM_MD9600) || defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017) setRtc_custom(uiDataGlobal.dateTimeSecs); #endif menuSatelliteScreenClearPredictions(false); } else { // alarm uiDataGlobal.SatelliteAndAlarmData.alarmTime = newTime; if (BUTTONCHECK_DOWN(ev, BUTTON_SK2)) { uiDataGlobal.SatelliteAndAlarmData.alarmType = ALARM_TYPE_CLOCK; powerOffFinalStage(true, false); } } keypadInputDigits[0] = 0;// clear digits keypadInputDigitsLength = 0; updateScreen(ev, true); nextKeyBeepMelody = (int16_t *)MELODY_ACK_BEEP; } break; } } } return; break; case KEY_RED: if ((displayMode == RADIO_INFOS_LOCATION) && BUTTONCHECK_DOWN(ev, BUTTON_SK2)) { #if defined(PLATFORM_MD9600) || defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017) gpsOnUsingQuickKey(false); #endif return; } else if (((displayMode == RADIO_INFOS_CURRENT_TIME) || (displayMode == RADIO_INFOS_DATE) || (displayMode == RADIO_INFOS_LOCATION)) && (keypadInputDigitsLength != 0)) { keypadInputDigits[0] = 0; keypadInputDigitsLength = 0; updateScreen(ev, true); } else { menuSystemPopPreviousMenu(); } return; break; } } if (ev->keys.event & KEY_MOD_PRESS) { switch(ev->keys.key) { case KEY_DOWN: if (keypadInputDigitsLength != 0) { if (displayMode == RADIO_INFOS_LOCATION) { char buf[2] = { 0, 0 }; if (BUTTONCHECK_DOWN(ev, BUTTON_SK2)) { latLonIsWesternHemisphere = true; buf[0] = currentLanguageGetSymbol(latLonIsWesternHemisphere ? SYMBOLS_WEST : SYMBOLS_EAST); } else { latLonIsSouthernHemisphere = true; buf[0] = currentLanguageGetSymbol(latLonIsSouthernHemisphere ? SYMBOLS_SOUTH : SYMBOLS_NORTH); } voicePromptsInit(); voicePromptsAppendString(buf); voicePromptsPlay(); updateScreen(ev, false); } return; } if (displayMode < (NUM_RADIO_INFOS_MENU_ITEMS - 1)) { displayMode++; updateScreen(ev, true); updateVoicePrompts(true, false); } break; case KEY_UP: if (keypadInputDigitsLength != 0) { if (displayMode == RADIO_INFOS_LOCATION) { char buf[2] = { 0, 0 }; if (BUTTONCHECK_DOWN(ev, BUTTON_SK2)) { latLonIsWesternHemisphere = false; buf[0] = currentLanguageGetSymbol(latLonIsWesternHemisphere ? SYMBOLS_WEST : SYMBOLS_EAST); } else { latLonIsSouthernHemisphere = false; buf[0] = currentLanguageGetSymbol(latLonIsSouthernHemisphere ? SYMBOLS_SOUTH : SYMBOLS_NORTH); } voicePromptsInit(); voicePromptsAppendString(buf); voicePromptsPlay(); updateScreen(ev, false); } return; } if (displayMode > RADIO_INFOS_BATTERY_LEVEL) { displayMode--; updateScreen(ev, true); updateVoicePrompts(true, false); } break; case KEY_LEFT: #if defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017) case KEY_ROTARY_DECREMENT: #endif switch(displayMode) { case RADIO_INFOS_BATTERY_GRAPH: if (graphStyle == GRAPH_LINE) { graphStyle = GRAPH_FILL; updateScreen(ev, true); } break; case RADIO_INFOS_CURRENT_TIME: case RADIO_INFOS_TIME_ALARM: case RADIO_INFOS_DATE: case RADIO_INFOS_LOCATION: { if (keypadInputDigitsLength > 0) { keypadInputDigits[keypadInputDigitsLength - 1] = 0; keypadInputDigitsLength--; } updateScreen(ev, true); } break; } break; case KEY_RIGHT: #if defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017) case KEY_ROTARY_INCREMENT: #endif switch(displayMode) { case RADIO_INFOS_BATTERY_GRAPH: if (graphStyle == GRAPH_FILL) { graphStyle = GRAPH_LINE; updateScreen(ev, true); } break; } break; default: if (BUTTONCHECK_DOWN(ev, BUTTON_SK2) == 0) // Filtering for QuickKey { int keyval = menuGetKeypadKeyValue(ev, true); if (keyval != 99) { int maxDigitsOnThisScreen; switch(displayMode) { case RADIO_INFOS_DATE: maxDigitsOnThisScreen = 8; break; case RADIO_INFOS_LOCATION: maxDigitsOnThisScreen = 13; break; case RADIO_INFOS_CURRENT_TIME: maxDigitsOnThisScreen = 6; break; default: return;// No other screens have key entry break; } if (keypadInputDigitsLength < maxDigitsOnThisScreen) { const uint8_t MAX_DIGIT_VALUE[3][9] = { { 2, 9, 5, 9, 5, 9, 0, 0, 0 }, { 2, 9, 5, 9, 5, 9, 0, 0, 0 }, { 2, 9, 5, 9, 5, 9, 0, 0, 0 } }; const uint8_t MAX_YEAR_DIGIT_VALUES[5] = { 2, 0, 4, 9, 1 }; int maxDigValue = 9; int minDigValue = 0; switch(displayMode) { case RADIO_INFOS_DATE: switch(keypadInputDigitsLength) { case 0: case 1: case 2: case 3: case 4: // Year digits, and first digit of the month maxDigValue = MAX_YEAR_DIGIT_VALUES[keypadInputDigitsLength]; break; case 5: // second digit of the month maxDigValue = (keypadInputDigits[4] == '1') ? 2 : 9; if (keypadInputDigits[4] == '0') { minDigValue = 1; } break; case 6: // first digit of the day maxDigValue = daysPerMonth[(((keypadInputDigits[4] - '0') * 10) + (keypadInputDigits[5] - '0')) - 1] / 10; break; case 7: { // second digit of the day uint32_t month = (((keypadInputDigits[4] - '0') * 10) + (keypadInputDigits[5] - '0')); if (month == 2) { // handle leap years uint32_t year = 0; uint32_t multiplier = 1000; for(int i = 0; i < 4; i++) { year += (keypadInputDigits[i] - '0') * multiplier; multiplier /= 10; } if ((keypadInputDigits[6] - '0') == 2) // 2_: Feb, 28 or 29 days (leap year checking) { if ((year % 4) == 0) { maxDigValue = 9; } else { maxDigValue = 8; } } } else { if ((keypadInputDigits[6] - '0') < 3) { maxDigValue = 9; } else { maxDigValue = daysPerMonth[month - 1] % 10; } } if (keypadInputDigits[6] == '0') { minDigValue = 1; } } break; } break; case RADIO_INFOS_LOCATION: maxDigValue = 9;// default value if no override is applied below switch(keypadInputDigitsLength) { case 1: case 2: case 3: case 4: case 5: if (keypadInputDigits[0] == '9') { maxDigValue = 0; } break; case 6: maxDigValue = 1; break; case 7: if (keypadInputDigits[6] == '1') { maxDigValue = 8; } break; case 8: case 9: case 10: case 11: case 12: if ((keypadInputDigits[6] == '1') && (keypadInputDigits[7] == '8')) { maxDigValue = 0; } break; } break; case RADIO_INFOS_CURRENT_TIME: if (keypadInputDigitsLength == 1 && (keypadInputDigits[0] == '2')) { maxDigValue = 3; } else { maxDigValue = MAX_DIGIT_VALUE[0][keypadInputDigitsLength]; } break; } if ((keyval <= maxDigValue) && (keyval >= minDigValue)) { char c[2] = {0, 0}; c[0] = keyval + '0'; if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD) { voicePromptsInit(); switch(displayMode) { case RADIO_INFOS_LOCATION: switch(keypadInputDigitsLength) { case 0: if (latLonIsSouthernHemisphere) { voicePromptsAppendPrompt(PROMPT_MINUS); } break; case 5: if (latLonIsWesternHemisphere) { voicePromptsAppendPrompt(PROMPT_MINUS); } break; } break; } voicePromptsAppendString(c); switch(displayMode) { case RADIO_INFOS_LOCATION: if ((keypadInputDigitsLength == 1) || (keypadInputDigitsLength == 8)) { voicePromptsAppendPrompt(PROMPT_POINT); } else if (keypadInputDigitsLength == 5) { char buf[2] = { 0, 0 }; buf[0] = currentLanguageGetSymbol(latLonIsSouthernHemisphere ? SYMBOLS_SOUTH : SYMBOLS_NORTH); voicePromptsAppendString(buf); } else if (keypadInputDigitsLength == 12) { char buf[2] = { 0, 0 }; buf[0] = currentLanguageGetSymbol(latLonIsWesternHemisphere ? SYMBOLS_WEST : SYMBOLS_EAST); voicePromptsAppendString(buf); } break; } voicePromptsPlay(); } strcat(keypadInputDigits, c); keypadInputDigitsLength++; updateScreen(ev, true); nextKeyBeepMelody = (int16_t *)MELODY_KEY_BEEP; } else { if (keypadInputDigitsLength != 0) { nextKeyBeepMelody = (int16_t *)MELODY_NACK_BEEP; } } } else { nextKeyBeepMelody = (int16_t *)MELODY_NACK_BEEP; } } } break; } } if (KEYCHECK_SHORTUP_NUMBER(ev->keys) && (BUTTONCHECK_DOWN(ev, BUTTON_SK2))) { saveQuickkeyMenuIndex(ev->keys.key, menuSystemGetCurrentMenuNumber(), displayMode, 0); } } void menuRadioInfosInit(void) { battSemaphore = xSemaphoreCreateMutex(); if (battSemaphore == NULL) { while(true); // Something better maybe ? } circularBufferInit(&batteryVoltageHistory); } // called every 2000 ticks void menuRadioInfosPushBackVoltage(int32_t voltage) { // Store value each 40k ticks if ((battery_stack_iter == 0) || (battery_stack_iter > BATTERY_ITER_PUSHBACK)) { if (xSemaphoreTake(battSemaphore, (TickType_t)10) == pdTRUE) { circularBufferPushBack(&batteryVoltageHistory, voltage); xSemaphoreGive(battSemaphore); } battery_stack_iter = 0; } battery_stack_iter++; } static void updateVoicePrompts(bool spellIt, bool firstRun) { if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD) { char buffer[LOCATION_TEXT_BUFFER_SIZE]; voicePromptsInit(); if (firstRun) { voicePromptsAppendPrompt(PROMPT_SILENCE); voicePromptsAppendLanguageString(currentLanguage->radio_info); voicePromptsAppendLanguageString(currentLanguage->menu); voicePromptsAppendPrompt(PROMPT_SILENCE); } switch (displayMode) { case RADIO_INFOS_BATTERY_LEVEL: case RADIO_INFOS_BATTERY_GRAPH: { int volts, mvolts; voicePromptsAppendLanguageString(currentLanguage->battery); getBatteryVoltage(&volts, &mvolts); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, " %1d.%1d", volts, mvolts); voicePromptsAppendString(buffer); voicePromptsAppendPrompt(PROMPT_VOLTS); voicePromptsAppendInteger(getBatteryPercentage()); voicePromptsAppendPrompt(PROMPT_PERCENT); } break; case RADIO_INFOS_TEMPERATURE_LEVEL: { int temperature = getTemperature(); voicePromptsAppendLanguageString(currentLanguage->temperature); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%d.%1d", (temperature / 10), (temperature % 10)); voicePromptsAppendString(buffer); voicePromptsAppendLanguageString(currentLanguage->celcius); } break; case RADIO_INFOS_CURRENT_TIME: voicePromptsAppendLanguageString(currentLanguage->time); if (!(nonVolatileSettings.timezone & 0x80)) { voicePromptsAppendString("UTC"); } snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%02u %02u%02u", timeAndDate.tm_hour, timeAndDate.tm_min, timeAndDate.tm_sec); voicePromptsAppendString(buffer); break; case RADIO_INFOS_LOCATION: voicePromptsAppendLanguageString(currentLanguage->location); if (settingsLocationIsValid()) { char maidenheadBuf[7]; buildLocationAndMaidenheadStrings(buffer, maidenheadBuf, true); voicePromptsAppendString(buffer); voicePromptsAppendPrompt(PROMPT_SILENCE); voicePromptsAppendPrompt(PROMPT_SILENCE); voicePromptsAppendPrompt(PROMPT_SILENCE); voicePromptsAppendString(maidenheadBuf); } else { voicePromptsAppendLanguageString(currentLanguage->not_set); } break; case RADIO_INFOS_DATE: voicePromptsAppendLanguageString(currentLanguage->date); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%04u %02u %02u", (timeAndDate.tm_year + 1900),(timeAndDate.tm_mon + 1),timeAndDate.tm_mday); voicePromptsAppendString(buffer); break; case RADIO_INFOS_UP_TIME: voicePromptsAppendLanguageString(currentLanguage->uptime); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%u", hours); voicePromptsAppendString(buffer); voicePromptsAppendLanguageString(currentLanguage->hours); snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%u", minutes); voicePromptsAppendString(buffer); voicePromptsAppendLanguageString(currentLanguage->minutes); break; } if (spellIt) { promptsPlayNotAfterTx(); } } }