FreeTRX/MDUV380_firmware/application/source/user_interface/menuRadioInfos.c
2026-07-06 08:45:33 +02:00

1523 lines
48 KiB
C

/*
* 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();
}
}
}