/*
 * Copyright (C) 2021-2025 Roger Clark, VK3KYY / G4KYF
 *                         Colin Durbridge, G4EML
 *                         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 <hardware/HX8353E.h>
#include <stdbool.h>
#include <memory.h>
#include <malloc.h>
#include <stdio.h>

#include "interfaces/adc.h"
#include "interfaces/batteryRAM.h"
#include "hardware/SPI_Flash.h"
#include "io/LEDs.h"
#include "interfaces/adc.h"
#include "hardware/radioHardwareInterface.h"
#include "hardware/HR-C6000.h" // slotState, for restoring the green RX LED after unmute

#include "io/buttons.h"
#include "usb/usb_com.h"
#include "usb_device.h"
#include "user_interface/menuSystem.h"
#include "user_interface/uiUtilities.h"
#include "user_interface/uiLocalisation.h"
#include "functions/ticks.h"
#include "interfaces/batteryAndPowerManagement.h"
#include "interfaces/gps.h"
#include "interfaces/settingsStorage.h"
#include "interfaces/adc.h"
#include "functions/rxPowerSaving.h"
#include "functions/smsCore.h"
#include "functions/smsStorage.h"

#if defined(USING_EXTERNAL_DEBUGGER)
#include "SeggerRTT/RTT/SEGGER_RTT.h"
#endif

#define VOLUME_UPDATE_TIMEOUT      5U

// Couldn't declare into aprs.h, due to header cross-dependence.
void aprsBeaconingTick(uiEvent_t *ev);

static bool smsResendTimeoutCallback(void)
{
	if (uiDataGlobal.MessageBox.keyPressed == KEY_GREEN)
	{
		smsResendLast();
	}

	return true;
}

static void smsHandleEvents(void)
{
	for (smsEvent_t smsEvent = smsPopEvent(); smsEvent != SMS_EVENT_NONE; smsEvent = smsPopEvent())
	{
		switch (smsEvent)
		{
			case SMS_EVENT_TX_SENDING:
				uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, currentLanguage->sms_sending, true);
				break;

			case SMS_EVENT_TX_SENT:
				uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, currentLanguage->sms_sent, true);
				break;

			case SMS_EVENT_TX_DELIVERED:
				uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, currentLanguage->sms_delivered, true);

				if (melody_play == NULL)
				{
					soundSetMelody(MELODY_ACK_BEEP);
				}
				break;

			case SMS_EVENT_TX_CHANNEL_BUSY:
				uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, currentLanguage->sms_channel_busy, true);

				if (melody_play == NULL)
				{
					soundSetMelody(MELODY_NACK_BEEP);
				}
				break;

			case SMS_EVENT_TX_TIMEOUT:
				if (smsCanResendLast())
				{
					snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%s\n%s",
							currentLanguage->sms_no_ack, currentLanguage->sms_resend);
					uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
					uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_YESNO;
					uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
					uiDataGlobal.MessageBox.validatorCallback = smsResendTimeoutCallback;
					menuSystemPushNewMenu(UI_MESSAGE_BOX);
				}
				break;

			case SMS_EVENT_RX_MESSAGE: // the channel/VFO screens open the popup
			default:
				break;
		}
	}
}

volatile bool mainIsRunning = true;
static bool updateMessageOnScreen = false;
int8_t lastVolume = 0;
static bool volumeIsStillChanging = false;
static int8_t lastDisplayedVolume = 62;
bool isSuspended = false;
char globalFailureMessage[SCREEN_LINE_BUFFER_SIZE] = { 0 };
bool spiFlashInitHasFailed = false;

#if ! defined(PLATFORM_GD77S)
ticksTimer_t autolockTimer;
#endif


#if (__NVIC_PRIO_BITS != 3)
#error Files need to be manually after regernation using the STM32Cube configuration tool to support the non-genuine CPU

/*
 * To support the Non-genuine CPU in newer radios, the following changes must be made
 *
 * stm32f405xx.h line 49 needs to be changed to
 * #define __NVIC_PRIO_BITS          3U       // VK3KYY changed this to 3 to fix problem with clone CPU. Genuine STM32F4XX uses 4 Bits for the Priority Levels
 *
 * FreeRTOS
 *
 * FreeRTOSConfig.h
 *
 * line 132
 *
 *  #define configPRIO_BITS         3
 *
 *  port.c
 *
 *  line 346
 *
 * 	configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) >= configPRIO_BITS );
 *
 * Also     HAL_NVIC_SetPriority(PendSV_IRQn, 15, 0);  has to be changed to HAL_NVIC_SetPriority(PendSV_IRQn, 7, 0); in stm32f4xx_hal_msp.h
 *
 */

#endif

#if false
static void hexDump(uint8_t *ptr, int len)
{
	char buf[8];
	char msg[128];

	msg[0] = 0;
	for (int i = 0; i < len; i++)
	{
		if ((i != 0) && (i%16 == 0))
		{
			strcat(msg,"\n");
			CDC_Transmit_FS((uint8_t *)msg,strlen(msg));
			msg[0] = 0;
		}
		else
		{
			sprintf(buf,"%02x ",*ptr);
			strcat(msg,buf);
		}
		ptr++;
		osDelay(1);
	}
}
#endif


static void keyBeepHandler(uiEvent_t *ev, bool ptttoggleddown)
{
	bool isScanning = (uiVFOModeIsScanning() || uiChannelModeIsScanning()) && !uiVFOModeSweepScanning(false);

	// Do not send any beep while scanning, otherwise enabling the AMP will be handled as a valid signal detection.
	if (((ev->keys.event & (KEY_MOD_LONG | KEY_MOD_PRESS)) == (KEY_MOD_LONG | KEY_MOD_PRESS)) ||
			((ev->keys.event & KEY_MOD_UP) == KEY_MOD_UP))
	{

		if ((ptttoggleddown == false) && (isScanning == false))
		{
			if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_BEEP)
			{
				soundSetMelody(nextKeyBeepMelody);
			}
			else
			{
				soundSetMelody(MELODY_KEY_BEEP);
			}
			nextKeyBeepMelody = (int16_t *)MELODY_KEY_BEEP;// set back to the default beep
		}
		else
		{ 	// Reset the beep sound if we are scanning, otherwise the AudioAssist
			// beep will be played instead of the normal one.

			if (isScanning)
			{
				if (melody_play != NULL)
				{
					soundStopMelody();
				}
			}
			else
			{
				soundSetMelody(MELODY_KEY_BEEP);
			}
		}
	}
	else
	{
		if ((ev->keys.event & (KEY_MOD_LONG | KEY_MOD_DOWN)) == (KEY_MOD_LONG | KEY_MOD_DOWN))
		{
			if ((ptttoggleddown == false) && (isScanning == false))
			{
				if (nextKeyBeepMelody != MELODY_KEY_BEEP)
				{
					soundSetMelody(nextKeyBeepMelody);
					nextKeyBeepMelody = (int16_t *)MELODY_KEY_BEEP;
				}
				else
				{
					soundSetMelody(MELODY_KEY_LONG_BEEP);
				}
			}
		}
	}
}

static bool rxBeepsMustPlayMelody(uint8_t rxBitOption)
{
	return ((nonVolatileSettings.beepOptions & rxBitOption) && (settingsUsbMode != USB_MODE_HOTSPOT) &&
			((uiDataGlobal.Scan.active == false) || (uiDataGlobal.Scan.active && (uiDataGlobal.Scan.state == SCAN_STATE_PAUSED))) &&
			(nonVolatileSettings.audioPromptMode != AUDIO_PROMPT_MODE_SILENT) && (voicePromptsIsPlaying() == false));
}

static void rxBeepsCarrierEndCallback(void)
{
	soundSetMelody(MELODY_RX_BEEP_END_BEEP);
}

static bool rxBeepsHandler(void)
{
	if (uiDataGlobal.rxBeepState & RX_BEEP_CARRIER_HAS_STARTED_EXEC)
	{
		// Don't clear the RF start beep when scanning, it has to be played, so wait the scan has paused.
		if ((uiDataGlobal.Scan.active &&
				((uiDataGlobal.Scan.state == SCAN_STATE_SCANNING) || (uiDataGlobal.Scan.state == SCAN_STATE_SHORT_PAUSED))) == false)
		{
			uiDataGlobal.rxBeepState &= ~RX_BEEP_CARRIER_HAS_STARTED_EXEC;
		}

		if (rxBeepsMustPlayMelody(BEEP_RX_CARRIER))
		{
			// Cancel queued rx end beep, if any. In this case, there is no
			// need to play another start beep, as the previous one has already
			// been heard.
			if (cancelTimerCallback(rxBeepsCarrierEndCallback, MENU_ANY) == false)
			{
				soundSetMelody(MELODY_RX_BEEP_BEGIN_BEEP);
			}

			return true;
		}
	}
	else if (uiDataGlobal.rxBeepState & RX_BEEP_TALKER_HAS_STARTED_EXEC)
	{
		uiDataGlobal.rxBeepState &= ~(RX_BEEP_TALKER_HAS_STARTED_EXEC | RX_BEEP_TALKER_HAS_ENDED | RX_BEEP_TALKER_HAS_ENDED_EXEC);

		if ((nonVolatileSettings.beepOptions & BEEP_RX_TALKER_BEGIN) && rxBeepsMustPlayMelody(BEEP_RX_TALKER))
		{
			soundSetMelody(MELODY_RX_BEEP_CALLER_BEGIN_BEEP);

			return true;
		}
	}
	else if (uiDataGlobal.rxBeepState & RX_BEEP_TALKER_HAS_ENDED_EXEC)
	{
		// FM: Replace Carrier beeps with Talker beeps if Caller beep option is selected.
		if ((trxGetMode() == RADIO_MODE_ANALOG) && ((nonVolatileSettings.beepOptions & BEEP_RX_CARRIER) == 0) && (nonVolatileSettings.beepOptions & BEEP_RX_TALKER))
		{
			uiDataGlobal.rxBeepState = RX_BEEP_UNSET;
		}
		else
		{
			uiDataGlobal.rxBeepState &= ~(RX_BEEP_TALKER_HAS_ENDED_EXEC | RX_BEEP_TALKER_HAS_STARTED | RX_BEEP_TALKER_HAS_STARTED_EXEC | RX_BEEP_TALKER_IDENTIFIED);
		}

		if (rxBeepsMustPlayMelody(BEEP_RX_TALKER))
		{
			soundSetMelody(MELODY_RX_BEEP_CALLER_END_BEEP);

			return true;
		}
	}
	else if (uiDataGlobal.rxBeepState & RX_BEEP_CARRIER_HAS_ENDED)
	{
		uiDataGlobal.rxBeepState = RX_BEEP_UNSET;

		if (rxBeepsMustPlayMelody(BEEP_RX_CARRIER))
		{
			// Delays end beep tone by 100ms, as it could played immediately after CALLER_END_BEEP on some conditions
			(void)addTimerCallback(rxBeepsCarrierEndCallback, 150, MENU_ANY, false);

			return true;
		}
	}

	return false;
}

static bool validateUpdateCallback(void)
{
	if (uiDataGlobal.MessageBox.keyPressed == KEY_GREEN)
	{
		updateMessageOnScreen = false;
		settingsSetOptionBit(BIT_SETTINGS_UPDATED, false);
		return true;
	}

	return false;
}

static void settingsUpdateAudioAlert(void)
{
	if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD)
	{
		voicePromptsInit();
		voicePromptsAppendPrompt(PROMPT_SETTINGS_UPDATE);
		voicePromptsPlay();
	}
	else
	{
		soundSetMelody(MELODY_ACK_BEEP);
	}
}

static void updateVolumeGain(int menuScreen)
{
	HRC6000SetDmrRxGain(lastVolume);

#if defined(HAS_SOFT_VOLUME)
	if (settingsIsOptionBitSet(BIT_VISUAL_VOLUME) &&
			(menuScreen != UI_SPLASH_SCREEN) && (menuScreen != UI_MESSAGE_BOX) && (menuScreen != UI_POWER_OFF))
	{
		lastDisplayedVolume = lastVolume;

		displayLightTrigger(true);
		uiNotificationShow(NOTIFICATION_TYPE_VOLUME, NOTIFICATION_ID_VOLUME, 1000, NULL, true);
	}
#endif
}

void applicationMainTask(void)
{
	keyboardCode_t keys;
	int key_event = EVENT_KEY_NONE;
	//  int keyFunction = 0;
	uint32_t buttons = 0;
	int button_event = EVENT_BUTTON_NONE;
	uint32_t rotary = 0;
	int rotary_event = EVENT_ROTARY_NONE;
	int function_event = 0;
	bool keyOrButtonChanged = false;
	int16_t *quickkeyPushedMenuMelody = NULL;
	int keyFunction;
	bool wasRestoringDefaultsettings = false;
	bool hasSignal = false;
	uiEvent_t ev = { .buttons = 0, .keys = NO_KEYCODE, .rotary = 0, .function = 0, .events = NO_EVENT, .hasEvent = false, .time = ticksGetMillis() };
	ticksTimer_t volumeControlTimer = { 0, 0 };
	ticksTimer_t updateClockTimer = { 0, 0 };
	bool safeBootMode = false;
	bool forceSafeBootMode = false;

	HAL_GPIO_WritePin(PWR_SW_GPIO_Port, PWR_SW_Pin, GPIO_PIN_SET);// keep the power on
	//batteryRAM_Init(); // Unused, save power

	adcStartDMA();

	//osDelay(500);
	//USB_DEBUG_printf("FW started\n");

#if defined(PLATFORM_RT84_DM1701)
	/* DM-1701
	 * 		displayLCD_Type:
	 * 			- 0: "Normal" mode
	 * 			- 1: 180° rotation
	 * 			- 2: Vertically flipped
	 * 			- 3: "Normal" mode (BGR)
	 */
	displayLCD_Type = SPI_Flash_readSingleSecurityRegister(0x301D);
	displayLCD_Type &= 0x03;

	if (displayLCD_Type == 2) // Normal 3 + RGB
	{
		displayLCD_Type = (3 | DIPLAYLCD_TYPE_RGB);
	}
#elif defined(PLATFORM_MDUV380)
	/* MD-UV380
	 * 		displayLCD_Type
	 * 			- 0: 180° rotation
	 * 			- 1: "Normal" mode
	 * 			- 2: Horizontally flipped
	 * 			- 3: 180° rotation
	 */
	displayLCD_Type = SPI_Flash_readSingleSecurityRegister(0x301D);
	displayLCD_Type &= 0x03;
	displayLCD_Type |= DIPLAYLCD_TYPE_RGB;

	/* MD-UV390 Plus 10W: byte at 0x301C == 0xF4
	 */
	//bool IsMDUV390_Plus_10W = (SPI_Flash_readSingleSecurityRegister(0x301C) == 0xF4);
#else // MD-2017 (ATM unknown)
	displayLCD_Type = (3 | DIPLAYLCD_TYPE_RGB);
#endif

	spiFlashInitHasFailed = !SPI_Flash_init();
	if (spiFlashInitHasFailed)
	{
		safeBootBranching:

		if (spiFlashInitHasFailed)
		{
			nonVolatileSettings.batteryCalibration = (0x05) + (0x07 << 4); // needed to get a correct default voltage reading
		}

		displayInit(settingsIsOptionBitSet(BIT_INVERSE_VIDEO), false);
		gpioInitDisplay();

		// Theme has't been read, init to default values
		themeInitToDefaultValues(DAY, false);

		// Since the settings weren't read from the Flash, set backlight percentage
		nonVolatileSettings.displayBacklightPercentage[DAY] = 100;
		displayEnableBacklight(true, -1);
		gpioSetDisplayBacklightIntensityPercentage(100);

		menuDataGlobal.controlData.stack[0] = MENU_EMPTY;
		menuDataGlobal.currentItemIndex = 0;

		snprintf(globalFailureMessage, SCREEN_LINE_BUFFER_SIZE, "%s", (safeBootMode ? "SAFE BOOT" : "FLASH MEM ERROR"));
		showErrorMessage(globalFailureMessage);

		die(true, false, false, safeBootMode);
	}


#if defined(USING_EXTERNAL_DEBUGGER)
	SEGGER_RTT_ConfigUpBuffer(0, NULL, NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_TRIM);
	SEGGER_RTT_printf(0,"Segger RTT initialised\n");
#endif

	buttonsInit();
	keyboardInit();

	// Operator asked for settings reset.
	// With the modified frontpanel button Col/Row, it's not
	// possible to use anything else than P3 if the transceiver was
	// powered off using the power button.


	buttonsCheckButtonsEvent(&buttons, &button_event, false);

	wasRestoringDefaultsettings = settingsLoadSettings(((buttons & BUTTON_SK2) != 0));

	displayInit(settingsIsOptionBitSet(BIT_INVERSE_VIDEO), true);
	gpioInitDisplay();

	radioPowerOn();

	uiDataGlobal.dmrDisabled = !codecIsAvailable();  // Check if DMR codec is available

	radioInit();

	calibrationInit();

	HRC6000Init();

	radioPostinit();

	HAL_ADC_Start_IT(&hadc1);

	HRC6000InitTask();

	voxInit();

	menuRadioInfosInit();
	batteryUpdate();

	soundInitBeepTask();

	// Clear boot melody and image
#if defined(PLATFORM_MD9600)
	if ((buttonsFrontPanelRead() & (FRONT_KEY_P3 | FRONT_KEY_DOWN)) == (FRONT_KEY_P3 | FRONT_KEY_DOWN))
#else
	if ((buttons & BUTTON_SK2) && (keyboardRead() == KEY_FRONT_DOWN))
#endif
	{
		settingsEraseCustomContent();
#if !defined(PLATFORM_MD9600)
		themeInit(true);
#endif
	}

	// Safe boot mode
#if defined(PLATFORM_MD9600)
	if (forceSafeBootMode || (((buttonsFrontPanelRead() & FRONT_KEY_P3) == FRONT_KEY_P3) && (micButtonsRead() == KEY_0)))
#else
	if (forceSafeBootMode || ((buttons & BUTTON_SK1) && (keyboardRead() == KEY_0)))
#endif
	{
#if !defined(PLATFORM_MD9600)
		themeInit(false);
#endif
		safeBootMode = true;

		goto safeBootBranching;
	}

	lastHeardInitList();
	codeplugInitCaches();
	dmrIDCacheInit();
	voicePromptsCacheInit();

	if (wasRestoringDefaultsettings || (keyboardRead() == KEY_HASH))
	{
		enableVoicePromptsIfLoaded((keyboardRead() == KEY_HASH));
	}

	// Need to take care if the user has already been fully notified about the settings update
	wasRestoringDefaultsettings = settingsIsOptionBitSet(BIT_SETTINGS_UPDATED);

	// Should be initialized before the splash screen, as we don't want melodies when VOX is enabled
	voxSetParameters(nonVolatileSettings.voxThreshold, nonVolatileSettings.voxTailUnits);

	codeplugGetSignallingDTMFDurations(&uiDataGlobal.DTMFContactList.durations);

#if !defined(PLATFORM_MD9600)
	if (settingsIsOptionBitSet(BIT_SAFE_POWER_ON) && ((buttons & BUTTON_SK1) != BUTTON_SK1))
	{
		powerOffFinalStage(false, true);
	}
#endif

	smsStorageInit();
	smsCoreInit();

	menuSystemInit(getRtcTime_custom());

#if defined(HAS_GPS)
	gpsInit();

#if defined(LOG_GPS_DATA)
	if (((buttons & BUTTON_SK1) == BUTTON_SK1) && (keyboardRead() == KEY_5))
	{
		uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 1500, "NMEA Clearing", true);
		gpsLoggingClear();
		settingsSet(nonVolatileSettings.gpsLogMemOffset, 0U);
	}
#endif

	if (SETTINGS_GPS_MODE_GET(nonVolatileSettings) != GPS_MODE_OFF)
	{
		gpsOn();
#if defined(LOG_GPS_DATA)
		gpsLoggingStart();
#endif
	}

#endif

	ticksTimerStart(&apoTimer, ((nonVolatileSettings.apo * 30) * 60000U));
	ticksTimerStart(&autolockTimer, (nonVolatileSettings.autolockTimer * 30000U));

	if ((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_MANUAL) ||
			(nonVolatileSettings.backlightMode == BACKLIGHT_MODE_BUTTONS) ||
			(nonVolatileSettings.backlightMode == BACKLIGHT_MODE_SQUELCH))
	{
		displayEnableBacklight(true, -1);
	}

	aprsBeaconingInit();
	aprsBeaconingStart();

	/* Infinite loop */
	for(;;)
	{
		uint32_t startTime = ticksGetMillis();
		bool syntheticEvent = false; // used to not trigger the backlight on faked key/button events

		mainIsRunning = true;
		keyOrButtonChanged = false;

		tick_com_request();
		handleTimerCallbacks();
		batteryUpdate();

		// Ignore any input when APRS is TXing (avoiding changing the channel in the middle of a packet).
		if ((currentChannelData->aprsConfigIndex == 0) ||
				((currentChannelData->aprsConfigIndex != 0) && (aprsBeaconingIsTransmitting() == false)))
		{
			keyboardCheckKeyEvent(&keys, &key_event); // Read keyboard state and event

			buttonsCheckButtonsEvent(&buttons, &button_event, (keys.key != 0)); // Read button state and event
		}
		else
		{
			if (aprsTxProgress == APRS_TX_FINAL_STAGE)
			{
				// Force the UI to handle the button release.
				button_event = EVENT_BUTTON_CHANGE;
				key_event = EVENT_KEY_CHANGE;
				keyboardReset();
				syntheticEvent = true;
			}
			else
			{
				button_event = EVENT_BUTTON_NONE;
				key_event = EVENT_KEY_NONE;
				keyboardReset();
			}
		}

		// Clear PTT button event when TX Inhibit is turned on.
		if (settingsIsOptionBitSet(BIT_TX_INHIBIT) && (buttons & BUTTON_PTT))
		{
			if (buttons == BUTTON_PTT)
			{
				button_event = EVENT_BUTTON_NONE;
			}

			buttons &= ~BUTTON_PTT;

			(void) txInhibitCheckAndWarn();
		}

		// hack to allow SK1 + Up / Down to be Left / Right
#if ! (defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)) // top side button IS orange
		if (buttons & BUTTON_SK1)
		{
			bool clearSK1 = false;
/*
			if (keys.key == KEY_UP)
			{
				keys.key = KEY_LEFT;
				clearSK1 = true;
			}
			else if (keys.key == KEY_DOWN)
			{
				keys.key = KEY_RIGHT;
				clearSK1 = true;
			}
*/
			if ((keys.key == KEY_GREEN)
#if defined(HAS_COLOURS) // SK1 + GREEN is used in the theme menu to reset theme to default.
					&& (menuSystemGetCurrentMenuNumber() != MENU_THEME_ITEMS_BROWSER)
#endif
#if defined(PLATFORM_MDUV380)
					&& (menuSystemGetCurrentMenuNumber() != MENU_CALIBRATION)
#endif
			)
			{
				// Translate keyboard SK1+GREEN to ORANGE button events

				buttons |= BUTTON_ORANGE;
				button_event = EVENT_BUTTON_CHANGE;
				clearSK1 = true;

				if ((keys.event & (KEY_MOD_PRESS | KEY_MOD_LONG)) == (KEY_MOD_PRESS | KEY_MOD_LONG))
				{
					buttons |= BUTTON_ORANGE_EXTRA_LONG_DOWN;
				}
				else if ((keys.event & (KEY_MOD_DOWN | KEY_MOD_LONG)) == (KEY_MOD_DOWN | KEY_MOD_LONG))
				{
					buttons |= BUTTON_ORANGE_LONG_DOWN;
				}
				else if ((keys.event & (KEY_MOD_UP | KEY_MOD_LONG)) == KEY_MOD_UP)
				{
					buttons |= BUTTON_ORANGE_SHORT_UP;
				}
				else if (keys.event & KEY_MOD_UP)
				{
					buttons = EVENT_BUTTON_NONE;
				}

				keys.event = EVENT_KEY_NONE;
				keys.key = 0;
			}

			if (clearSK1)
			{
				// Clear all SK1 flags
				buttons &= ~(BUTTON_SK1 | BUTTON_SK1_SHORT_UP | BUTTON_SK1_LONG_DOWN | BUTTON_SK1_EXTRA_LONG_DOWN);

				if (buttons == BUTTON_NONE)
				{
					button_event = EVENT_BUTTON_NONE;
				}
			}
		}
#endif

		if (((key_event != EVENT_KEY_NONE) && (keys.key != 0)) || (button_event != EVENT_BUTTON_NONE) || (rotary_event != EVENT_ROTARY_NONE))
		{
			keyOrButtonChanged = true;
		}

		// Settings reset information screen
		if (wasRestoringDefaultsettings && (menuSystemGetRootMenuNumber() != UI_SPLASH_SCREEN))
		{
			wasRestoringDefaultsettings = false;
			updateMessageOnScreen = true;

			menuSystemPushNewMenu(MENU_LANGUAGE);// As language menu is now in Options, present the operator with the Language menu after the "Settings updated" message has been displayed.

			snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%s", "Settings\nUpdated");
			uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
			uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
			uiDataGlobal.MessageBox.buttons =
#if defined(PLATFORM_MD9600)
					MESSAGEBOX_BUTTONS_ENT;
#else
					MESSAGEBOX_BUTTONS_OK;
#endif
			uiDataGlobal.MessageBox.validatorCallback = validateUpdateCallback;
			menuSystemPushNewMenu(UI_MESSAGE_BOX);

			(void)addTimerCallback(settingsUpdateAudioAlert, 100, UI_MESSAGE_BOX, false);// Need to delay playing this for a while, because otherwise it may get played before the volume is turned up enough to hear it.
		}

		// VOX Checking
		if (voxIsEnabled())
		{
			// if a key/button event happen, reset the VOX.
			if ((key_event == EVENT_KEY_CHANGE) || (button_event == EVENT_BUTTON_CHANGE) || (keys.key != 0) || (buttons != BUTTON_NONE))
			{
				voxReset();
			}
			else
			{
				if (!trxTransmissionEnabled && voxIsTriggered() && ((buttons & BUTTON_PTT) == 0))
				{
					button_event = EVENT_BUTTON_CHANGE;
					buttons |= BUTTON_PTT;
				}
				else if (trxTransmissionEnabled && ((voxIsTriggered() == false) || (keys.event & KEY_MOD_PRESS)))
				{
					button_event = EVENT_BUTTON_CHANGE;
					buttons &= ~BUTTON_PTT;
				}
				else if (trxTransmissionEnabled && voxIsTriggered())
				{
					// Any key/button event reset the vox
					if ((button_event != EVENT_BUTTON_NONE) || (keys.event != EVENT_KEY_NONE))
					{
						voxReset();
						button_event = EVENT_BUTTON_CHANGE;
						buttons &= ~BUTTON_PTT;
					}
					else
					{
						buttons |= BUTTON_PTT;
					}
				}
			}
		}

		// If the settings update message is still on screen, don't permit to start xmitting.
		if (updateMessageOnScreen && (buttons & BUTTON_PTT))
		{
			button_event = EVENT_BUTTON_CHANGE;
			buttons &= ~BUTTON_PTT;
		}

		if (headerRowIsDirty)
		{
			int currentMenu = menuSystemGetCurrentMenuNumber();

			if ((currentMenu == UI_CHANNEL_MODE) || (currentMenu == UI_VFO_MODE) ||
					((currentMenu == MENU_SATELLITE) && menuSatelliteIsDisplayingHeader()))
			{
				bool sweeping;

				if ((sweeping = uiVFOModeSweepScanning(true)))
				{
					displayFillRect(0, 0, DISPLAY_SIZE_X, 9, true);
				}
				else
				{
#if defined(PLATFORM_RD5R)
					displayFillRect(0, 0, DISPLAY_SIZE_X, 9, true);
#else
					displayFillRect(0, 0, DISPLAY_SIZE_X, 14, true); // 2 rows are two much (16 pixels), switched to FillRect.
#endif
				}

				uiUtilityRenderHeader(uiVFOModeDualWatchIsScanning(), sweeping, ((buttons & BUTTON_SK1) == BUTTON_SK1));

				if (uiNotificationIsVisible())
				{
					displayRender();
				}
				else
				{
					displayRenderRows(0, 2);
				}
			}

			headerRowIsDirty = false;
		}

		if (keypadLocked || PTTLocked)
		{
			if (keypadLocked && ((buttons & BUTTON_PTT) == 0))
			{
				if ((key_event == EVENT_KEY_CHANGE) && (syntheticEvent == false))
				{
					bool continueToFilterKeys = true;

					// A key is pressed, but a message box is currently displayed (probably a private call notification)
					if (menuSystemGetCurrentMenuNumber() == UI_MESSAGE_BOX)
					{
						// Clear any key but RED and GREEN
						if ((keys.key == KEY_RED) || (keys.key == KEY_GREEN))
						{
							continueToFilterKeys = false;
						}
					}

					if (continueToFilterKeys)
					{
						if ((PTTToggledDown == false) && (menuSystemGetCurrentMenuNumber() != UI_LOCK_SCREEN))
						{
							menuSystemPushNewMenu(UI_LOCK_SCREEN);
						}

						key_event = EVENT_KEY_NONE;
					}

					if (settingsIsOptionBitSet(BIT_PTT_LATCH) && PTTToggledDown)
					{
						PTTToggledDown = false;
					}
				}

				// Lockout ORANGE AND BLUE (BLACK stay active regardless lock status, useful to trigger backlight)
				if ((button_event == EVENT_BUTTON_CHANGE) && ((buttons & BUTTON_ORANGE) || (buttons & BUTTON_SK2)))
				{
					if ((PTTToggledDown == false) && (menuSystemGetCurrentMenuNumber() != UI_LOCK_SCREEN))
					{
						menuSystemPushNewMenu(UI_LOCK_SCREEN);
					}

					button_event = EVENT_BUTTON_NONE;

					if (settingsIsOptionBitSet(BIT_PTT_LATCH) && PTTToggledDown)
					{
						PTTToggledDown = false;
					}
				}
			}
			else if (PTTLocked)
			{
				if ((buttons & BUTTON_PTT) && (button_event == EVENT_BUTTON_CHANGE))
				{
					// PTT button is pressed, but a message box is currently displayed, and PC allowance is set to PTT,
					// hence it's probably a private call accept, so let the PTT button being handled later in the code
					if (((menuSystemGetCurrentMenuNumber() == UI_MESSAGE_BOX) && (nonVolatileSettings.privateCalls == ALLOW_PRIVATE_CALLS_PTT)) == false)
					{
						soundSetMelody(MELODY_ERROR_BEEP);

						if (menuSystemGetCurrentMenuNumber() != UI_LOCK_SCREEN)
						{
							menuSystemPushNewMenu(UI_LOCK_SCREEN);
						}

						button_event = EVENT_BUTTON_NONE;
						// Clear PTT button
						buttons &= ~BUTTON_PTT;
					}
				}
				else if ((buttons & BUTTON_SK2) && KEYCHECK_DOWN(keys, KEY_STAR))
				{
					if (menuSystemGetCurrentMenuNumber() != UI_LOCK_SCREEN)
					{
						menuSystemPushNewMenu(UI_LOCK_SCREEN);
					}
				}
			}
		}

		int trxMode = trxGetMode();

		// Long press RED
		if ((key_event == EVENT_KEY_CHANGE) && ((buttons & BUTTON_PTT) == 0) && (keys.key != 0))
		{
			int currentMenu = menuSystemGetCurrentMenuNumber();

			// Longpress RED send back to root menu, it's only available from
			// any menu but VFO, Channel, UI_MESSAGE_BOX and CPS
			if (((currentMenu != UI_CHANNEL_MODE) && (currentMenu != UI_VFO_MODE) && (currentMenu != UI_CPS) && (currentMenu != UI_MESSAGE_BOX)) &&
					KEYCHECK_LONGDOWN(keys, KEY_RED) && (uiVFOModeIsScanning() == false) && (uiChannelModeIsScanning() == false))
			{
				uiDataGlobal.currentSelectedContactIndex = 0;
				menuSystemPopAllAndDisplayRootMenu();
				soundSetMelody(MELODY_KEY_BEEP);

				if (currentMenu != MENU_SATELLITE)
				{
					// If an option menu was currently running, ensure the
					// settings copy is reset.
					resetOriginalSettingsData();
				}

				// Clear button/key event/state.
				buttons = BUTTON_NONE;
				rotary = 0;
				key_event = EVENT_KEY_NONE;
				button_event = EVENT_BUTTON_NONE;
				rotary_event = EVENT_ROTARY_NONE;
				keys.key = 0;
				keys.event = 0;
			}
		}
		//
		// PTT toggle feature
		//
		// PTT is locked down, but any button, except SK1 or SK2(1750Hz in FM) or DTMF Key in Analog,
		// or Up/Down with LH on screen in Digital, is pressed, virtually release PTT
		if ((settingsIsOptionBitSet(BIT_PTT_LATCH) && PTTToggledDown) &&
				(((button_event & EVENT_BUTTON_CHANGE) && (
#if ! defined(PLATFORM_RD5R)
						(buttons & BUTTON_ORANGE) ||
#endif
						((trxMode != RADIO_MODE_ANALOG) && (buttons & BUTTON_SK2)))) ||
						((keys.key != 0) && (keys.event & KEY_MOD_UP) &&
								(((trxMode == RADIO_MODE_ANALOG) && keyboardKeyIsDTMFKey(keys.key)) == false) &&
								(((trxMode == RADIO_MODE_DIGITAL) && menuTxScreenDisplaysLastHeard() && ((keys.key == KEY_UP) || (keys.key == KEY_DOWN))) == false))))
		{
			PTTToggledDown = false;
			button_event = EVENT_BUTTON_CHANGE;
			buttons = BUTTON_NONE;
			key_event = EVENT_KEY_NONE;
			keys.key = 0;
		}

		// PTT toggle action
		if (settingsIsOptionBitSet(BIT_PTT_LATCH))
		{
			if (button_event == EVENT_BUTTON_CHANGE)
			{
				if (buttons & BUTTON_PTT)
				{
					if (PTTToggledDown == false)
					{
						// PTT toggle works only if a TOT value is defined.
						if (currentChannelData->tot != 0)
						{
							PTTToggledDown = true;
						}
					}
					else
					{
						PTTToggledDown = false;
					}
				}
			}

			if (PTTToggledDown && ((buttons & BUTTON_PTT) == 0))
			{
				buttons |= BUTTON_PTT;
			}
		}
		else
		{
			if (PTTToggledDown)
			{
				PTTToggledDown = false;
			}
		}

		hasSignal = false;

		if(trxGetMode() == RADIO_MODE_ANALOG)
		{
			hasSignal = trxCheckAnalogSquelch();
		}
		else
		{
			if (slotState == DMR_STATE_IDLE)
			{
				trxReadRSSIAndNoise(false);

				hasSignal = trxCheckDigitalSquelch(RADIO_DEVICE_PRIMARY);
			}
			else
			{
				if (ticksTimerHasExpired((ticksTimer_t *)&readDMRRSSITimer))
				{
					trxReadRSSIAndNoise(false);
					ticksTimerStart((ticksTimer_t *)&readDMRRSSITimer, 10000); // hold of for a very long time
				}
				hasSignal = true;
			}
		}

		// Screen dimming: an incoming signal counts as activity, so keep the backlight
		// at full brightness while receiving (Auto backlight mode + dimming enabled).
		if (hasSignal && (nonVolatileSettings.backlightMode == BACKLIGHT_MODE_AUTO) && (settingsGetScreenDimLevel() != SCREEN_DIM_OFF))
		{
			displayLightTrigger(false);
		}

		if (!trxTransmissionEnabled && updateLastHeard)
		{
			lastHeardListUpdate((uint8_t *)DMR_frame_buffer, false);
			updateLastHeard = false;
		}

		if ((nonVolatileSettings.hotspotType == HOTSPOT_TYPE_OFF) ||
				((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) && (settingsUsbMode != USB_MODE_HOTSPOT))) // Do not filter anything in HS mode.
		{
			if ((uiDataGlobal.PrivateCall.state == PRIVATE_CALL_DECLINED) &&
					(slotState == DMR_STATE_IDLE))
			{
				menuPrivateCallClear();
			}

			if ((trxTransmissionEnabled == false) && (trxIsTransmitting == false) &&
					(uiDataGlobal.displayQSOState == QSO_DISPLAY_CALLER_DATA) && (nonVolatileSettings.privateCalls > ALLOW_PRIVATE_CALLS_OFF))
			{
				if (HRC6000GetReceivedTgOrPcId() == (trxDMRID | (PC_CALL_FLAG << 24)))
				{
					int receivedSrcId = HRC6000GetReceivedSrcId();

					if ((uiDataGlobal.PrivateCall.state == PRIVATE_CALL_NOT_IN_CALL) &&
							(trxTalkGroupOrPcId != (receivedSrcId | (PC_CALL_FLAG << 24))) &&
							(receivedSrcId != uiDataGlobal.PrivateCall.lastID))
					{
						if ((receivedSrcId & 0xFFFFFF) >= 1000000)
						{
							menuSystemPushNewMenu(UI_PRIVATE_CALL);
						}
					}
				}
			}
		}

		if (button_event == EVENT_BUTTON_CHANGE)
		{
			// Toggle backlight
			if (nonVolatileSettings.backlightMode == BACKLIGHT_MODE_MANUAL)
			{
				if (buttons == BUTTON_SK1) // SK1 alone
				{
					displayEnableBacklight(! displayIsBacklightLit(), nonVolatileSettings.displayBacklightPercentageOff);
				}
			}
			else
			{
				if (syntheticEvent == false)
				{
					displayLightTrigger(true);
				}
			}

			if ((buttons & BUTTON_PTT) != 0)
			{
				int currentMenu = menuSystemGetCurrentMenuNumber();

				if ((trxGetMode() != RADIO_MODE_NONE) &&
						(settingsUsbMode != USB_MODE_HOTSPOT) &&
						(HRC6000IsSendingSMS() == false) && // no voice TX while a message is being sent
						(currentMenu != UI_POWER_OFF) &&
						(currentMenu != UI_SPLASH_SCREEN) &&
						(currentMenu != UI_TX_SCREEN)&&
						(currentMenu != MENU_CALIBRATION))
				{
					bool wasScanning = false;

					if (uiDataGlobal.Scan.active || uiDataGlobal.Scan.toneActive)
					{
						if (currentMenu == UI_VFO_MODE)
						{
							uiVFOModeStopScanning();
						}
						else
						{
							uiChannelModeStopScanning();
						}
						wasScanning = true;
					}
					else
					{
						if (currentMenu == UI_LOCK_SCREEN)
						{
							menuLockScreenPop();
						}
					}

					currentMenu = menuSystemGetCurrentMenuNumber();

					if (wasScanning)
					{
						// Mode was blinking, hence it needs to be redrawn as it could be in its hidden phase.
						uiUtilityRedrawHeaderOnly(false, false, ((buttons & BUTTON_SK1) == BUTTON_SK1));
					}
					else
					{
						if (((currentMenu == UI_MESSAGE_BOX) && (menuSystemGetPreviousMenuNumber() == UI_PRIVATE_CALL))
								&& (nonVolatileSettings.privateCalls == ALLOW_PRIVATE_CALLS_PTT))
						{
							acceptPrivateCall(uiDataGlobal.receivedPcId, uiDataGlobal.receivedPcTS);
							menuPrivateCallDismiss();
						}
						else if (((currentMenu == MENU_CONTACT_LIST_SUBMENU) || (currentMenu == MENU_CONTACT_QUICKLIST)) && dtmfSequenceIsKeying())
						{
							dtmfSequenceReset();
						}

						// Need to call menuSystemGetCurrentMenuNumber() again, as something has probably
						// changed since last above calls
						if (menuSystemGetCurrentMenuNumber() != UI_MESSAGE_BOX)
						{
							if (currentChannelData->txFreq != 0)
							{
								rxPowerSavingSetState(ECOPHASE_POWERSAVE_INACTIVE);

								menuSystemPushNewMenu(UI_TX_SCREEN);
							}
							else
							{
								soundSetMelody(MELODY_NACK_BEEP);
							}
						}
						else
						{
							button_event = EVENT_BUTTON_NONE;
							buttons &= ~BUTTON_PTT;
						}
					}
				}
			}
		}

		ev.function = 0;
		function_event = NO_EVENT;
		keyFunction = NO_EVENT;
		int currentMenu = menuSystemGetCurrentMenuNumber();
		if (KEYCHECK_SHORTUP_NUMBER(keys) && (buttons & BUTTON_SK2) && ((currentMenu == UI_VFO_MODE) || (currentMenu == UI_CHANNEL_MODE)))
		{
			keyFunction = codeplugGetQuickkeyFunctionID(keys.key);
			int menuFunction = QUICKKEY_MENUID(keyFunction);

#if 0 // For demo screen
			if (keys.key == '0')
			{
				static uint8_t demo = 90;
				keyFunction = (UI_HOTSPOT_MODE << 8) | demo; // Hotspot demo mode (able to take screengrabs)

				if (++demo > 99)
				{
					demo = 90;
				}
			}
#endif

#if defined(PLATFORM_RD5R)
			if (keys.key == '5')
			{
				menuFunction = 0;
				keyFunction = FUNC_TOGGLE_TORCH;
				keyboardReset();
			}
			else
			{
#endif
				if ((keyFunction != 0) &&
						((currentMenu == UI_CHANNEL_MODE) || (currentMenu == UI_VFO_MODE) || (currentMenu == menuFunction)))
				{
					if (QUICKKEY_TYPE(keyFunction) == QUICKKEY_MENU)
					{
						bool inChannelMenu;
						bool qkIsValid = true;

						//
						// QuickMenu special cases:
						//
						//   It's permited to share filter quickkeys between Channels and VFO screen.
						//   For this, the itemIndex value needs to be tweaked.
						//
						//   Other QuickMenu entries will simply be ignored if the current Channel/VFO screen doesn't
						//   match the QuickKey menuId.
						//
						if ((inChannelMenu = (currentMenu == UI_CHANNEL_MODE)) || (currentMenu == UI_VFO_MODE))
						{
							// The current QuickKey menu destination doesn't match the current menu (Channel or VFO)
							if (menuFunction == (inChannelMenu ? UI_VFO_QUICK_MENU : UI_CHANNEL_QUICK_MENU))
							{
								int entryId = QUICKKEY_ENTRYID(keyFunction);

								// Convert filters positions
								if ((entryId >= (inChannelMenu ? VFO_SCREEN_QUICK_MENU_FILTER_FM : CH_SCREEN_QUICK_MENU_FILTER_FM))
										&& (entryId <= (inChannelMenu ? VFO_SCREEN_QUICK_MENU_FILTER_DMR_TS : CH_SCREEN_QUICK_MENU_FILTER_DMR_TS)))
								{
									// Apply entry offset to match the filter position on the opposite screen
									if (inChannelMenu)
									{
#if defined(PLATFORM_DM1801)
										entryId -= 1;
#else
										entryId -= 2;
#endif
									}
									else
									{
#if defined(PLATFORM_DM1801)
										entryId += 1;
#else
										entryId += 2;
#endif
									}

									int kf = QUICKKEY_MENUVALUE((inChannelMenu ? UI_CHANNEL_QUICK_MENU : UI_VFO_QUICK_MENU), entryId, QUICKKEY_FUNCTIONID(keyFunction));
									keyFunction = kf;
									menuFunction = (inChannelMenu ? UI_CHANNEL_QUICK_MENU : UI_VFO_QUICK_MENU);
								}
								else
								{
									// We can't use other VFO/Channel QuickMenu entry in a mismatching screen.
									qkIsValid = false;
									keyFunction = NO_EVENT;
								}
							}
						}

						if (qkIsValid)
						{
							if ((menuFunction > 0) && (menuFunction < NUM_MENU_ENTRIES))
							{
								if (currentMenu != menuFunction)
								{
									menuSystemPushNewMenu(menuFunction);

									// Store the beep build by the new menu status. It will be restored after
									// the call of menuSystemCallCurrentMenuTick(), below
									quickkeyPushedMenuMelody = nextKeyBeepMelody;
								}
							}
							ev.function = keyFunction;
							buttons = BUTTON_NONE;
							rotary = 0;
							key_event = EVENT_KEY_NONE;
							button_event = EVENT_BUTTON_NONE;
							rotary_event = EVENT_ROTARY_NONE;
							keys.key = 0;
							keys.event = 0;
							function_event = FUNCTION_EVENT;
						}
						else
						{
							menuFunction = 0;
						}
					}
					else if ((QUICKKEY_TYPE(keyFunction) == QUICKKEY_CONTACT) && (currentMenu != menuFunction))
					{
						int contactIndex = QUICKKEY_CONTACTVALUE(keyFunction);

						if ((contactIndex >= CODEPLUG_CONTACTS_MIN) && (contactIndex <= CODEPLUG_CONTACTS_MAX))
						{
							if (codeplugContactGetDataForIndex(contactIndex, &currentContactData))
							{
								// Use quickkey contact as overrides (contact and its TS, if any)
								menuPrivateCallClear();
								setOverrideTGorPC(currentContactData.tgNumber, (currentContactData.callType == CONTACT_CALLTYPE_PC));

								trxTalkGroupOrPcId = currentContactData.tgNumber;
								if (currentContactData.callType == CONTACT_CALLTYPE_PC)
								{
									trxTalkGroupOrPcId |= (PC_CALL_FLAG << 24);
								}

								// Contact has a TS override set
								if ((currentContactData.reserve1 & CODEPLUG_CONTACT_FLAG_NO_TS_OVERRIDE) == 0x00)
								{
									int ts = ((currentContactData.reserve1 & CODEPLUG_CONTACT_FLAG_TS_OVERRIDE_TIMESLOT_MASK) >> 1);
									trxSetDMRTimeSlot(ts, true);
									tsSetManualOverride(((menuSystemGetRootMenuNumber() == UI_CHANNEL_MODE) ? CHANNEL_CHANNEL : (CHANNEL_VFO_A + nonVolatileSettings.currentVFONumber)), (ts + 1));
								}
								ev.function = FUNC_REDRAW;
								function_event = FUNCTION_EVENT;
							}
						}
					}
				}
				keyboardReset();
#if defined(PLATFORM_RD5R)
			}
#endif
		}
		ev.buttons = buttons;
		ev.keys = keys;
		ev.rotary = rotary;
		ev.events = function_event | (button_event << 1) | (rotary_event << 3) | key_event | (syntheticEvent ? SYNTHETIC_EVENT : 0);
		ev.hasEvent = keyOrButtonChanged || (function_event != NO_EVENT);
		ev.time = ticksGetMillis();


		// Clear the Quickkey slot on SK2 + longdown 0..9 KEY
		if (KEYCHECK_LONGDOWN_NUMBER(ev.keys) && BUTTONCHECK_DOWN(&ev, BUTTON_SK2))
		{
			// Only allow quick keys to be cleared on the 2 main screens
			if (currentMenu == UI_CHANNEL_MODE || currentMenu == UI_VFO_MODE)
			{
				saveQuickkeyMenuLongValue(ev.keys.key, 0, 0);
				soundSetMelody(MELODY_QUICKKEYS_CLEAR_ACK_BEEP);
			}
			else
			{
				soundSetMelody(MELODY_NACK_BEEP);
			}

			// Reset keyboard and event, as this keyboard event HAVE to
			// be ignore by the current menu.
			keyboardReset();
			ev.buttons = BUTTON_NONE;
			ev.keys.event = 0;
			ev.keys.key = 0;
			ev.rotary = 0;
			ev.events = NO_EVENT;
			ev.hasEvent = false;
		}

		menuSystemCallCurrentMenuTick(&ev);

		// Restore the beep built when a menu was pushed by the quickkey above.
		if (quickkeyPushedMenuMelody)
		{
			nextKeyBeepMelody = quickkeyPushedMenuMelody;
			quickkeyPushedMenuMelody = NULL;
			ev.keys.event = KEY_MOD_UP; // Trick to force keyBeepHandler() to set that beep
		}

		// Beep sounds aren't allowed in these modes.
		if (((nonVolatileSettings.audioPromptMode == AUDIO_PROMPT_MODE_SILENT) || voicePromptsIsPlaying()) /*|| (nonVolatileSettings.audioPromptMode == AUDIO_PROMPT_MODE_VOICE)*/)
		{
			if (melody_play != NULL)
			{
				soundStopMelody();
			}

			(void)rxBeepsHandler(); // It will remain silent, only clearing the rxToneState bits.
		}
		else
		{
			if (rxBeepsHandler() == false)
			{
				if ((menuSystemGetCurrentMenuNumber() != UI_SPLASH_SCREEN) &&
						((((key_event == EVENT_KEY_CHANGE) || (button_event == EVENT_BUTTON_CHANGE))
								&& ((buttons & BUTTON_PTT) == 0) && (ev.keys.key != 0))
								|| (function_event == FUNCTION_EVENT)))
				{
					keyBeepHandler(&ev, PTTToggledDown);
				}
			}
		}

		if (uiNotificationHasTimedOut())
		{
			uiNotificationHide(true);
		}

		batteryChecking(&ev);

#if !defined(PLATFORM_GD77S)
		// APO checkings
		apoTick((keyOrButtonChanged || (function_event != NO_EVENT) ||
				(settingsIsOptionBitSet(BIT_APO_WITH_RF) ? (audioAmpGetStatus() & AUDIO_AMP_CHANNEL_RF) : false)));

		// Autolock trigger/reset
		if (ticksTimerIsEnabled(&autolockTimer))
		{
			if (((keyOrButtonChanged || (function_event != NO_EVENT)) && (syntheticEvent == false)) || // key event resets the timer
					((currentMenu != UI_CHANNEL_MODE) && (currentMenu != UI_VFO_MODE))) // and could only auto locks while in Channel/VFO menus
			{
				ticksTimerStart(&autolockTimer, (nonVolatileSettings.autolockTimer * 30000U));
			}
			else
			{
				if (ticksTimerHasExpired(&autolockTimer))
				{
					keypadLocked = PTTLocked = true;
					ticksTimerReset(&autolockTimer);
					uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 1000, currentLanguage->auto_lock, true);
				}
			}
		}
#endif

		if (((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_AUTO)
				|| (nonVolatileSettings.backlightMode == BACKLIGHT_MODE_BUTTONS)
				|| (nonVolatileSettings.backlightMode == BACKLIGHT_MODE_SQUELCH)) && (menuDataGlobal.lightTimer > 0))
		{
			// Countdown only in (AUTO), (BUTTONS) or (SQUELCH + no audio)
			if ((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_AUTO) || (nonVolatileSettings.backlightMode == BACKLIGHT_MODE_BUTTONS) ||
					((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_SQUELCH) && ((audioAmpGetStatus() & AUDIO_AMP_CHANNEL_RF) == 0)))
			{
				menuDataGlobal.lightTimer--;
			}

			if (menuDataGlobal.lightTimer == 0)
			{
				displayEnableBacklight(false, displayGetIdleBacklightPercentage());
			}
		}

		voicePromptsTick();
		soundTickMelody();
		voxTick();
		gpsTick();
		aprsBeaconingTick(&ev);
		smsCoreTick();
		smsStorageTick();
		smsHandleEvents();
		settingsSaveIfNeeded(false);

		if (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER))
		{
			if (ticksTimerHasExpired(&updateClockTimer))
			{
				headerRowIsDirty = true;
				ticksTimerStart(&updateClockTimer, 500U);
			}
		}

		if (((trxTransmissionEnabled || trxIsTransmitting) == false))
		{
#if ! defined(PLATFORM_GD77S)
			// We got a position fix now, start daytime timer
			if (currentMenu != MENU_DISPLAY)
			{
				if (settingsIsOptionBitSet(BIT_AUTO_NIGHT) &&
						(ticksTimerIsEnabled(&daytimeThemeTimer) == false) && settingsLocationIsValid())
				{
					daytimeThemeChangeUpdate(false);
				}

				if (ticksTimerIsEnabled(&daytimeThemeTimer) && ticksTimerHasExpired(&daytimeThemeTimer))
				{
					daytimeThemeTick();

					ticksTimerStart(&daytimeThemeTimer, DAYTIME_THEME_TIMER_INTERVAL);
				}
			}
#endif

			rxPowerSavingTick(&ev, hasSignal);
		}

		int8_t latestVolume = getVolumeControl();
		if (latestVolume != lastVolume)
		{
			volumeIsStillChanging = true;

			lastVolume = latestVolume;

			ticksTimerStart(&volumeControlTimer, VOLUME_UPDATE_TIMEOUT);

			if (abs(lastDisplayedVolume - lastVolume) >= 6)
			{
				updateVolumeGain(currentMenu);
			}
		}
		else if (volumeIsStillChanging && ticksTimerHasExpired(&volumeControlTimer))
		{
			volumeIsStillChanging = false;
			updateVolumeGain(currentMenu);
		}

		// Mute indicator: while muted, flash the RED LED for 0.5s every 2s.
		// This overrides the normal RX/TX LED while muted.
		{
			static bool muteFlashActive = false;

			if (audioAmpIsMuted() || (lastVolume == -99)) // explicit mute or volume knob fully down
			{
				if (!muteFlashActive)
				{
					// Hold the green LED off for the whole mute, so the DMR RX handler
					// (which re-asserts green as soon as it reads it off) can't make it
					// flicker green between the red flashes.
					LedGreenSuppress(true);
					muteFlashActive = true;
				}

				bool flashOn = ((ticksGetMillis() % 2000) < 500);
				LedWrite(LED_RED, (flashOn ? 1 : 0));
			}
			else if (muteFlashActive)
			{
				// Just unmuted: release the green hold, stop the red flash and restore
				// the green LED to the current RX state, so a QSO in progress lights
				// green again instead of staying dark. Green is normally lit for an
				// active DMR slot (matching our filter) or a received FM/digital signal;
				// if nothing is being received it stays off. The normal RX code then
				// keeps it correct.
				LedGreenSuppress(false);
				bool rxGreen = (slotState == DMR_STATE_RX_1) || (slotState == DMR_STATE_RX_2) ||
						currentRadioDevice->analogSignalReceived || currentRadioDevice->digitalSignalReceived;
				LedWrite(LED_RED, 0);
				LedWrite(LED_GREEN, (rxGreen ? 1 : 0));
				muteFlashActive = false;
			}
		}

		// Power-on indicator: when the display doesn't stay lit, flash the GREEN LED for
		// 100ms every 3s, so it's still obvious the radio is switched on while the screen
		// is dark. Only while nothing else owns the LEDs: a LED lit by RX/TX already shows
		// the radio is alive, and muting flashes the RED one.
		{
			static bool heartbeatFlashOn = false;

			bool rxGreen = (slotState == DMR_STATE_RX_1) || (slotState == DMR_STATE_RX_2) ||
					currentRadioDevice->analogSignalReceived || currentRadioDevice->digitalSignalReceived;
			bool ledsAreFree = (rxGreen == false) && ((trxTransmissionEnabled || trxIsTransmitting) == false) &&
					(audioAmpIsMuted() == false) && (lastVolume != -99);

			if ((displayIsBacklightLit() == false) && ledsAreFree)
			{
				bool flashOn = ((ticksGetMillis() % 3000) < 100);

				if (flashOn != heartbeatFlashOn)
				{
					heartbeatFlashOn = flashOn;
					LedWrite(LED_GREEN, (flashOn ? 1 : 0));
				}
			}
			else if (heartbeatFlashOn)
			{
				// The backlight came on, or RX/TX/mute took the LEDs over: end our flash and
				// restore the green LED to whatever the current RX state requires.
				heartbeatFlashOn = false;
				LedWrite(LED_GREEN, (rxGreen ? 1 : 0));
			}
		}

		while(ticksGetMillis() < startTime + 1)				// ensure this Task runs at 1ms intervals. Regardless of clock speed.
		{
			vTaskDelay(0U);
		}
	}
}
