FreeTRX/MDUV380_firmware/application/source/user_interface/uiUtilities.c
Marcus Kida 9d50d78dc0 Mute: also show mute indicators when volume knob is fully down
Treat the volume control at its minimum (lastVolume == -99, the firmware's
'muted' position) as effectively muted, so the red LED flash, the header M
and the built-in strike-through appear without an explicit SK2 mute.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:54:25 +02:00

4778 lines
143 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 <math.h>
#if defined(PLATFORM_GD77) || defined(PLATFORM_GD77S) || defined(PLATFORM_DM1801) || defined(PLATFORM_DM1801A) || defined(PLATFORM_RD5R)
#include "hardware/EEPROM.h"
#endif
#include "user_interface/uiGlobals.h"
#include "user_interface/menuSystem.h"
#include "user_interface/uiUtilities.h"
#include "applicationMain.h" // for lastVolume (volume-knob-at-minimum = effectively muted)
#include "user_interface/uiLocalisation.h"
#include "hardware/SPI_Flash.h"
#include "functions/trx.h"
#include "functions/rxPowerSaving.h"
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
#include "interfaces/batteryAndPowerManagement.h"
#include "hardware/radioHardwareInterface.h"
#endif
#if defined(HAS_GPS)
#if defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
#include "interfaces/gps.h"
#endif
#endif
#if !defined(SQUARE)
#define SQUARE(a) ((a) * (a))
#endif
static const uint8_t DECOMPRESS_LUT[64] = { ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '.' };
#if defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
static __attribute__((section(".ccmram")))
#else // MD9600 and MK22
static __attribute__((section(".data.$RAM2")))
#endif
LinkItem_t callsList[NUM_LASTHEARD_STORED];
static uint32_t dmrIdDataArea_1_Size;
const uint32_t DMRID_HEADER_LENGTH = 0x0C;
const uint32_t DMRID_MEMORY_LOCATION_1 = 0x30000 + FLASH_ADDRESS_OFFSET;
const uint32_t DMRID_MEMORY_LOCATION_2 = 0xB8000 + FLASH_ADDRESS_OFFSET;
uint32_t dmrIDDatabaseMemoryLocation2 = DMRID_MEMORY_LOCATION_2;
static dmrIDsCache_t dmrIDsCache;
static uint32_t lastTG = 0;
volatile uint32_t lastID = 0;// This needs to be volatile as lastHeardClearLastID() is called from an ISR
LinkItem_t *LinkHead = callsList;
DECLARE_SMETER_ARRAY(rssiMeterHeaderBar, DISPLAY_SIZE_X);
static uint32_t DMRID_IdLength = 4U;
static uint8_t bufferTA[32] = { 0 };
static uint8_t blocksTA = 0x00;
static bool overrideTA = false;
static bool contactDefinedForTA = false; // lockout TA data storage until a valid DMR ID is received.
#if defined(HAS_COLOURS) || defined(PLATFORM_MD9600)
static struct tm timeAndDate;
//#define ALTERNATES_TIME_AND_BATTERY_DISPLAY 1 // Alternates Time/Battery display
#if defined(ALTERNATES_TIME_AND_BATTERY_DISPLAY)
static ticksTimer_t batOrTimeTimer = { 0, 0 };
static bool displayTimeInHeader = false;
#endif
#endif
static void announceChannelNameOrVFOFrequency(bool voicePromptWasPlaying, bool announceVFOName);
static void dmrDbTextDecode(uint8_t *compressedBufIn, uint8_t *decompressedBufOut, int compressedSize);
// Set TS manual override
// chan: CHANNEL_VFO_A, CHANNEL_VFO_B, CHANNEL_CHANNEL
// ts: 1, 2, TS_NO_OVERRIDE
void tsSetManualOverride(Channel_t chan, int8_t ts)
{
uint16_t tsOverride = nonVolatileSettings.tsManualOverride;
// Clear TS override for given channel
tsOverride &= ~(0x03 << (2 * ((int8_t)chan)));
if (ts != TS_NO_OVERRIDE)
{
// Set TS override for given channel
tsOverride |= (ts << (2 * ((int8_t)chan)));
}
settingsSet(nonVolatileSettings.tsManualOverride, tsOverride);
}
// Set TS manual override from contact TS override
// chan: CHANNEL_VFO_A, CHANNEL_VFO_B, CHANNEL_CHANNEL
// contact: apply TS override from contact setting
void tsSetFromContactOverride(Channel_t chan, CodeplugContact_t *contact)
{
if ((contact->reserve1 & CODEPLUG_CONTACT_FLAG_NO_TS_OVERRIDE) == 0x00)
{
tsSetManualOverride(chan, (((contact->reserve1 & CODEPLUG_CONTACT_FLAG_TS_OVERRIDE_TIMESLOT_MASK) >> 1) + 1));
}
else
{
tsSetManualOverride(chan, TS_NO_OVERRIDE);
}
}
// Get TS override value
// chan: CHANNEL_VFO_A, CHANNEL_VFO_B, CHANNEL_CHANNEL
// returns (TS + 1, 0 no override)
int8_t tsGetManualOverride(Channel_t chan)
{
return (nonVolatileSettings.tsManualOverride >> (2 * (int8_t)chan)) & 0x03;
}
// Get manually overridden TS, if any, from currentChannelData
// returns (TS + 1, 0 no override)
int8_t tsGetManualOverrideFromCurrentChannel(void)
{
Channel_t chan = (((currentChannelData->NOT_IN_CODEPLUG_flag & 0x01) == 0x01) ?
(((currentChannelData->NOT_IN_CODEPLUG_flag & 0x02) == 0x02) ? CHANNEL_VFO_B : CHANNEL_VFO_A) : CHANNEL_CHANNEL);
return tsGetManualOverride(chan);
}
// Check if TS is overrode
// chan: CHANNEL_VFO_A, CHANNEL_VFO_B, CHANNEL_CHANNEL
// returns true on overrode for the specified channel
bool tsIsManualOverridden(Channel_t chan)
{
return (nonVolatileSettings.tsManualOverride & (0x03 << (2 * ((int8_t)chan))));
}
// Keep track of an override when the selected contact has a TS override set
// chan: CHANNEL_VFO_A, CHANNEL_VFO_B, CHANNEL_CHANNEL
void tsSetContactHasBeenOverriden(Channel_t chan, bool isOverriden)
{
uint16_t tsOverride = nonVolatileSettings.tsManualOverride;
if (isOverriden)
{
tsOverride |= (1 << ((3 * 2) + ((int8_t)chan)));
}
else
{
tsOverride &= ~(1 << ((3 * 2) + ((int8_t)chan)));
}
settingsSet(nonVolatileSettings.tsManualOverride, tsOverride);
}
// Get TS override status, of a selected contact which have a TS override set
// chan: CHANNEL_VFO_A, CHANNEL_VFO_B, CHANNEL_CHANNEL
bool tsIsContactHasBeenOverriddenFromCurrentChannel(void)
{
Channel_t chan = (((currentChannelData->NOT_IN_CODEPLUG_flag & 0x01) == 0x01) ?
(((currentChannelData->NOT_IN_CODEPLUG_flag & 0x02) == 0x02) ? CHANNEL_VFO_B : CHANNEL_VFO_A) : CHANNEL_CHANNEL);
return tsIsContactHasBeenOverridden(chan);
}
// Get manual TS override of the selected contact (which has a TS override set)
// chan: CHANNEL_VFO_A, CHANNEL_VFO_B, CHANNEL_CHANNEL
bool tsIsContactHasBeenOverridden(Channel_t chan)
{
return (nonVolatileSettings.tsManualOverride >> ((3 * 2) + (int8_t)chan)) & 0x01;
}
bool isQSODataAvailableForCurrentTalker(void)
{
LinkItem_t *item = NULL;
uint32_t rxID = HRC6000GetReceivedSrcId();
// We're in digital mode, RXing, and current talker is already at the top of last heard list,
// hence immediately display complete contact/TG info on screen
if ((trxTransmissionEnabled == false) && ((trxGetMode() == RADIO_MODE_DIGITAL) && (rxID != 0) && (lastID != 0) && (HRC6000GetReceivedTgOrPcId() != 0)) &&
(audioAmpGetStatus() & AUDIO_AMP_CHANNEL_RF)
&& HRC6000CheckTalkGroupFilter() &&
(((item = lastHeardFindInList(rxID)) != NULL) && (item == LinkHead)))
{
return true;
}
return false;
}
#if 0
int alignFrequencyToStep(int freq, int step)
{
int r = freq % step;
return (r ? freq + (step - r) : freq);
}
#endif
/*
* Remove space at the end of the array, and return pointer to first non space character
*/
char *chomp(char *str)
{
char *sp = str, *ep = str;
while (*ep != '\0')
{
ep++;
}
// Spaces at the end
while (ep > str)
{
if (*ep == '\0')
{
}
else if (*ep == ' ')
{
*ep = '\0';
}
else
{
break;
}
ep--;
}
// Spaces at the beginning
while (*sp == ' ')
{
sp++;
}
return sp;
}
int32_t getFirstSpacePos(const char *str)
{
char *p = (char *)str;
while(*p != '\0')
{
if (*p == ' ')
{
return (p - str);
}
p++;
}
return -1;
}
// dest has to be SCREEN_LINE_BUFFER_SIZE length, minimum.
void getCallsignOnly(char *dest, const char *src)
{
if (strlen(src) >= 5)
{
int32_t cpos;
cpos = getFirstSpacePos(src);
if ((cpos == -1) || (cpos > SCREEN_LINE_BUFFER_SIZE - 2))
{
cpos = (SCREEN_LINE_BUFFER_SIZE - 1);
}
memcpy(dest, src, cpos);
dest[cpos] = 0;
}
else
{
size_t len = strlen(src);
memcpy(dest, src, len);
dest[len] = 0;
}
}
void lastHeardInitList(void)
{
LinkHead = callsList;
for(int i = 0; i < NUM_LASTHEARD_STORED; i++)
{
callsList[i].id = 0;
callsList[i].talkGroupOrPcId = 0;
callsList[i].contact[0] = 0;
callsList[i].talkgroup[0] = 0;
callsList[i].talkerAlias[0] = 0;
callsList[i].locationLat = NAN;
callsList[i].locationLon = NAN;
callsList[i].time = 0;
callsList[i].receivedTS = 0;
callsList[i].dmrMode = DMR_MODE_AUTO;
callsList[i].rxAGCGain = 0U;
if (i == 0)
{
callsList[i].prev = NULL;
}
else
{
callsList[i].prev = &callsList[i - 1];
}
if (i < (NUM_LASTHEARD_STORED - 1))
{
callsList[i].next = &callsList[i + 1];
}
else
{
callsList[i].next = NULL;
}
}
uiDataGlobal.lastHeardCount = 0;
}
LinkItem_t *lastHeardFindInList(uint32_t id)
{
LinkItem_t *item = LinkHead;
while (item->next != NULL)
{
if (item->id == id)
{
// found it
return item;
}
item = item->next;
}
return NULL;
}
// returns pointer to maidenheadBuffer
uint8_t *coordsToMaidenhead(uint8_t *maidenheadBuffer, double latitude, double longitude)
{
double l, l2;
uint8_t c;
l = longitude;
for (uint8_t i = 0; i < 2; i++)
{
l = l / ((i == 0) ? 20.0 : 10.0) + 9.0;
c = (uint8_t) l;
maidenheadBuffer[0 + i] = c + 'A';
l2 = c;
l -= l2;
l *= 10.0;
c = (uint8_t) l;
maidenheadBuffer[2 + i] = c + '0';
l2 = c;
l -= l2;
l *= 24.0;
c = (uint8_t) l;
maidenheadBuffer[4 + i] = c + 'A';
#if 0
if (extended)
{
l2 = c;
l -= l2;
l *= 10.0;
c = (uint8_t) l;
maidenheadBuffer[6 + i] = c + '0';
l2 = c;
l -= l2;
l *= 24.0;
c = (uint8_t) l;
maidenheadBuffer[8 + i] = c + (extended ? 'A' : 'a');
l2 = c;
l -= l2;
l *= 10.0;
c = (uint8_t) l;
maidenheadBuffer[10 + i] = c + '0';
l2 = c;
l -= l2;
l *= 24.0;
c = (uint8_t) l;
maidenheadBuffer[12 + i] = c + (extended ? 'A' : 'a');
}
#endif
l = latitude;
}
#if 0
maidenheadBuffer[extended ? 14 : 6] = '\0';
#else
maidenheadBuffer[6] = '\0';
#endif
return maidenheadBuffer;
}
static void buildMaidenHead(char *maidenheadBuffer, uint32_t intPartLat, uint32_t decPartLat, bool isSouthern, uint32_t intPartLon, uint32_t decPartLon, bool isWestern)
{
double latitude = intPartLat + (decPartLat * LOCATION_DECIMAL_PART_MULIPLIER_FIXED_32_DOUBLE);
double longitude = intPartLon + (decPartLon * LOCATION_DECIMAL_PART_MULIPLIER_FIXED_32_DOUBLE);
if (isSouthern)
{
latitude *= -1;
}
if (isWestern)
{
longitude *= -1;
}
coordsToMaidenhead((uint8_t *)maidenheadBuffer, latitude, longitude);
}
void buildLocationAndMaidenheadStrings(char *locationBufferOrNull, char *maidenheadBufferOrNull, bool locIsValid)
{
if (locIsValid)
{
bool southernHemisphere = ((nonVolatileSettings.location.lat & 0x80000000) != 0);
bool westernHemisphere = ((nonVolatileSettings.location.lon & 0x80000000) != 0);
uint32_t intPartLat = (nonVolatileSettings.location.lat & 0x7FFFFFFF) >> 23;
uint32_t decPartLat = (nonVolatileSettings.location.lat & 0x7FFFFF);
uint32_t intPartLon = (nonVolatileSettings.location.lon & 0x7FFFFFFF) >> 23;
uint32_t decPartLon = (nonVolatileSettings.location.lon & 0x7FFFFF);
if (maidenheadBufferOrNull)
{
buildMaidenHead(maidenheadBufferOrNull, intPartLat, decPartLat, southernHemisphere, intPartLon, decPartLon, westernHemisphere);
}
if (locationBufferOrNull)
{
snprintf(locationBufferOrNull, LOCATION_TEXT_BUFFER_SIZE, "%02u.%04u%c %03u.%04u%c",
intPartLat, decPartLat/10, currentLanguageGetSymbol(southernHemisphere ? SYMBOLS_SOUTH : SYMBOLS_NORTH),
intPartLon, decPartLon/10, currentLanguageGetSymbol(westernHemisphere ? SYMBOLS_WEST : SYMBOLS_EAST));
}
}
else
{
if (locationBufferOrNull)
{
snprintf(locationBufferOrNull, LOCATION_TEXT_BUFFER_SIZE, "%s", "??.???? ???.????");
}
if (maidenheadBufferOrNull)
{
maidenheadBufferOrNull[0] = 0;
}
}
}
double latLonFixed24ToDouble(uint32_t fixedVal)
{
// Bit 23 is the sign
// Bits 22 to 15 (8 bits) are the integer part
// Lower 15 bits are the decimal.
double intPart = (fixedVal & 0x7FFFFF) >> 15;
double decimalPart = (fixedVal & 0x7FFF);
double result = (intPart + (decimalPart * LOCATION_DECIMAL_PART_MULIPLIER_FIXED_24_DOUBLE));
return ((fixedVal & 0x800000) ? -result : result);
}
uint32_t latLonDoubleToFixed24(double value)
{
double intPart;
int decimalPart = abs((int)(modf(value, &intPart) * LOCATION_DECIMAL_PART_MULIPLIER_FIXED_24));
uint32_t fixedVal = (abs((int)intPart) << 15) + decimalPart;
if (value < 0.0)
{
fixedVal |= 0x800000;// set MSB to indicate negative number
}
return fixedVal;
}
double latLonFixed32ToDouble(uint32_t fixedVal)
{
// MS bit is the sign
// Bits 30 to 23 (8 bits) are the integer part
// Lower 23 bits are the fixed point to the right of the point.
double intPart = (fixedVal & 0x7FFFFFFF) >> 23;
double decimalPart = (fixedVal & 0x7FFFFF);
double result = (intPart + (decimalPart * LOCATION_DECIMAL_PART_MULIPLIER_FIXED_32_DOUBLE));
return ((fixedVal & 0x80000000) ? -result : result);
}
uint32_t latLonDoubleToFixed32(double value)
{
double intPart;
int decimalPart = abs((int)(modf(value, &intPart) * LOCATION_DECIMAL_PART_MULIPLIER_FIXED_32));
uint32_t fixedVal = (abs((int)intPart) << 23) + decimalPart;
if (value < 0.0)
{
fixedVal |= 0x80000000;// set MSB to indicate negative number
}
return fixedVal;
}
double distanceToLocation(double latitude, double longitude)
{
return distanceBetweenTwoCoords(latitude, longitude, settingsLocationGetLatitude(), settingsLocationGetLongitude(), true);
}
double distanceBetweenTwoCoords(double lat1, double lon1, double lat2, double lon2, bool inKm)
{
// returns distance in meters between two positions, both specified
// as signed decimal-degrees latitude and longitude. Uses great-circle
// distance computation for hypothetical sphere of radius 6372795 meters.
// Because Earth is no exact sphere, rounding errors may be up to 0.5%.
// Courtesy of Maarten Lamers
double mToKm = (inKm ? 1E-3 : 1E0);
double delta = ((lon1 - lon2) * DEG_TO_RAD);
double sdlon = sin(delta);
double cdlon = cos(delta);
lat1 = (lat1 * DEG_TO_RAD);
lat2 = (lat2 * DEG_TO_RAD);
double slat1 = sin(lat1);
double clat1 = cos(lat1);
double slat2 = sin(lat2);
double clat2 = cos(lat2);
delta = (clat1 * slat2) - (slat1 * clat2 * cdlon);
delta = SQUARE(delta);
delta += SQUARE(clat2 * sdlon);
delta = sqrt(delta);
double denom = (slat1 * slat2) + (clat1 * clat2 * cdlon);
delta = atan2(delta, denom);
return ((delta * 6372795) * mToKm);
}
double courseTo(double lat1, double lon1, double lat2, double lon2)
{
// returns course in degrees (North=0, West=270) from position 1 to position 2,
// both specified as signed decimal-degrees latitude and longitude.
// Because Earth is no exact sphere, calculated course may be off by a tiny fraction.
// Courtesy of Maarten Lamers
double dlon = ((lon2 - lon1) * DEG_TO_RAD);
lat1 = (lat1 * DEG_TO_RAD);
lat2 = (lat2 * DEG_TO_RAD);
double a1 = sin(dlon) * cos(lat2);
double a2 = sin(lat1) * cos(lat2) * cos(dlon);
a2 = cos(lat1) * sin(lat2) - a2;
a2 = atan2(a1, a2);
if (a2 < 0.0)
{
a2 += TWO_PI;
}
return (a2 * RAD_TO_DEG);
}
static double roundPosition(double val)
{
return ((double)((int)((val * 10000) + ((val > 0 ) ? 0.5 : -0.5)))) / 10000;
}
static void decodeGPSPosition(uint8_t *data, double *latitude, double *longitude)
{
#if 0
uint8_t errorI = (data[2U] & 0x0E) >> 1U;
const char* error;
switch (errorI) {
case 0U:
error = "< 2m";
break;
case 1U:
error = "< 20m";
break;
case 2U:
error = "< 200m";
break;
case 3U:
error = "< 2km";
break;
case 4U:
error = "< 20km";
break;
case 5U:
error = "< 200km";
break;
case 6U:
error = "> 200km";
break;
default:
error = "not known";
break;
}
#endif
int32_t longitudeI = ((data[2U] & 0x01U) << 31) | (data[3U] << 23) | (data[4U] << 15) | (data[5U] << 7);
longitudeI >>= 7;
int32_t latitudeI = (data[6U] << 24) | (data[7U] << 16) | (data[8U] << 8);
latitudeI >>= 8;
double lon = ((double)longitudeI * 360.0F) / 33554432.0F;
double lat = ((double)latitudeI * 180.0F) / 16777216.0F;
*longitude = roundPosition(lon);
*latitude = roundPosition(lat);
}
static uint8_t decodeTA(uint8_t *dest, uint8_t destLen, uint8_t *talkerAlias)
{
uint8_t TAformat = (talkerAlias[0] >> 6U) & 0x03U;
uint8_t TAsize = (talkerAlias[0] >> 1U) & 0x1FU;
*dest = 0;
switch (TAformat)
{
case 0U: // 7 bit
{
uint8_t *b = &talkerAlias[0];
uint8_t t1 = 0U, t2 = 0U, c = 0U;
memset(dest, 0x00U, destLen);
for (uint8_t i = 0U; (i < 32U) && (t2 < TAsize); i++)
{
for (int8_t j = 7; j >= 0; j--)
{
c = (c << 1U) | (b[i] >> j);
if (++t1 == 7U)
{
if (i > 0U)
{
dest[t2++] = c & 0x7FU;
}
t1 = 0U;
c = 0U;
}
}
}
dest[TAsize] = 0;
}
break;
case 1U: // ISO 8 bit
case 2U: // UTF8
memcpy(dest, talkerAlias + 1U, (destLen - 1));
break;
case 3U: // UTF16 poor man's conversion
{
uint8_t t2 = 0U;
memset(dest, 0x00U, destLen);
for (uint8_t i = 0U; (i < 15U) && (t2 < TAsize); i++)
{
if (talkerAlias[2U * i + 1U] == 0)
{
dest[t2++] = talkerAlias[2U * i + 2U];
}
else
{
dest[t2++] = '?';
}
}
dest[TAsize] = 0;
}
break;
}
return (strlen((char *)dest));
}
void lastHeardClearLastID(void)
{
memset(bufferTA, 0, 32);// Clear any TA data in TA buffer (used for decode)
blocksTA = 0x00;
overrideTA = false;
contactDefinedForTA = false;
lastID = 0;
}
static void updateLHItem(LinkItem_t *item)
{
static const int bufferLen = 33; // displayChannelNameOrRxFrequency() use 6x8 font
char buffer[bufferLen];// buffer passed to the DMR ID lookup function, needs to be large enough to hold worst case text length that is returned. Currently 16+1
dmrIdDataStruct_t currentRec;
if ((item->talkGroupOrPcId >> 24) == PC_CALL_FLAG)
{
// Its a Private call
switch (nonVolatileSettings.contactDisplayPriority)
{
case CONTACT_DISPLAY_PRIO_CC_DB_TA:
case CONTACT_DISPLAY_PRIO_TA_CC_DB:
if (contactIDLookup(item->id, CONTACT_CALLTYPE_PC, buffer))
{
snprintf(item->contact, SCREEN_LINE_BUFFER_SIZE, "%s", buffer);
}
else
{
dmrIDLookup(item->id, &currentRec);
snprintf(item->contact, SCREEN_LINE_BUFFER_SIZE, "%s", currentRec.text);
}
break;
case CONTACT_DISPLAY_PRIO_DB_CC_TA:
case CONTACT_DISPLAY_PRIO_TA_DB_CC:
if (dmrIDLookup(item->id, &currentRec))
{
snprintf(item->contact, SCREEN_LINE_BUFFER_SIZE, "%s", currentRec.text);
}
else
{
if (contactIDLookup(item->id, CONTACT_CALLTYPE_PC, buffer))
{
snprintf(item->contact, SCREEN_LINE_BUFFER_SIZE, "%s", buffer);
}
else
{
snprintf(item->contact, SCREEN_LINE_BUFFER_SIZE, "%s", currentRec.text);
}
}
break;
}
if (item->talkGroupOrPcId != (trxDMRID | (PC_CALL_FLAG << 24)))
{
if (contactIDLookup(item->talkGroupOrPcId & 0x00FFFFFF, CONTACT_CALLTYPE_PC, buffer))
{
snprintf(item->talkgroup, SCREEN_LINE_BUFFER_SIZE, "%s", buffer);
}
else
{
dmrIDLookup(item->talkGroupOrPcId & 0x00FFFFFF, &currentRec);
snprintf(item->talkgroup, SCREEN_LINE_BUFFER_SIZE, "%s", currentRec.text);
}
}
}
else
{
// TalkGroup
if (contactIDLookup(item->talkGroupOrPcId, CONTACT_CALLTYPE_TG, buffer))
{
snprintf(item->talkgroup, SCREEN_LINE_BUFFER_SIZE, "%s", buffer);
}
else
{
if ((item->talkGroupOrPcId & 0x00FFFFFF) == ALL_CALL_VALUE)
{
snprintf(item->talkgroup, SCREEN_LINE_BUFFER_SIZE, "%s", currentLanguage->all_call);
}
else
{
snprintf(item->talkgroup, SCREEN_LINE_BUFFER_SIZE, "%s %u", currentLanguage->tg, (item->talkGroupOrPcId & 0x00FFFFFF));
}
}
switch (nonVolatileSettings.contactDisplayPriority)
{
case CONTACT_DISPLAY_PRIO_CC_DB_TA:
case CONTACT_DISPLAY_PRIO_TA_CC_DB:
if (contactIDLookup(item->id, CONTACT_CALLTYPE_PC, buffer))
{
snprintf(item->contact, MAX_DMR_ID_CONTACT_TEXT_LENGTH, "%s", buffer);
}
else
{
dmrIDLookup((item->id & 0x00FFFFFF), &currentRec);
snprintf(item->contact, MAX_DMR_ID_CONTACT_TEXT_LENGTH, "%s", currentRec.text);
}
break;
case CONTACT_DISPLAY_PRIO_DB_CC_TA:
case CONTACT_DISPLAY_PRIO_TA_DB_CC:
if (dmrIDLookup((item->id & 0x00FFFFFF), &currentRec))
{
snprintf(item->contact, MAX_DMR_ID_CONTACT_TEXT_LENGTH, "%s", currentRec.text);
}
else
{
if (contactIDLookup(item->id, CONTACT_CALLTYPE_PC, buffer))
{
snprintf(item->contact, MAX_DMR_ID_CONTACT_TEXT_LENGTH, "%s", buffer);
}
else
{
snprintf(item->contact, MAX_DMR_ID_CONTACT_TEXT_LENGTH, "%s", currentRec.text);
}
}
break;
}
}
}
void lastHeardClearWorkingTAData(void)
{
memset(bufferTA, 0, 32);// Clear any TA data in TA buffer (used for decode)
blocksTA = 0x00;
overrideTA = false;
contactDefinedForTA = false;
}
bool lastHeardListUpdate(uint8_t *dmrDataBuffer, bool forceOnHotspot)
{
uint32_t talkGroupOrPcId = (dmrDataBuffer[0] << 24) + (dmrDataBuffer[3] << 16) + (dmrDataBuffer[4] << 8) + (dmrDataBuffer[5] << 0);
if ((HRC6000GetReceivedTgOrPcId() != 0) || forceOnHotspot)
{
if (dmrDataBuffer[0] == TG_CALL_FLAG || dmrDataBuffer[0] == PC_CALL_FLAG)
{
uint32_t id = (dmrDataBuffer[6] << 16) + (dmrDataBuffer[7] << 8) + (dmrDataBuffer[8] << 0);
if ((id != 0) && (talkGroupOrPcId != 0))
{
if (id != lastID)
{
lastHeardClearWorkingTAData();
lastID = id;
LinkItem_t *item = lastHeardFindInList(id);
// Already in the list
if (item != NULL)
{
if (item->talkGroupOrPcId != talkGroupOrPcId)
{
item->talkGroupOrPcId = talkGroupOrPcId; // update the TG in case they changed TG
updateLHItem(item);
}
item->time = ticksGetMillis();
lastTG = talkGroupOrPcId;
dmrRxAGCrxPeakAverage = (float)item->rxAGCGain;
if (item == LinkHead)
{
uiDataGlobal.displayQSOState = QSO_DISPLAY_CALLER_DATA;// flag that the display needs to update
contactDefinedForTA = true;
return true;// already at top of the list
}
else
{
// not at top of the list
// Move this item to the top of the list
LinkItem_t *next = item->next;
LinkItem_t *prev = item->prev;
// set the previous item to skip this item and link to 'items' next item.
prev->next = next;
if (item->next != NULL)
{
// not the last in the list
next->prev = prev;// backwards link the next item to the item before us in the list.
}
item->next = LinkHead;// link our next item to the item at the head of the list
LinkHead->prev = item;// backwards link the hold head item to the item moving to the top of the list.
item->prev = NULL;// change the items prev to NULL now we are at teh top of the list
LinkHead = item;// Change the global for the head of the link to the item that is to be at the top of the list.
if (item->talkGroupOrPcId != 0)
{
uiDataGlobal.displayQSOState = QSO_DISPLAY_CALLER_DATA;// flag that the display needs to update
}
}
}
else
{
// Not in the list
item = LinkHead;// setup to traverse the list from the top.
if (uiDataGlobal.lastHeardCount < NUM_LASTHEARD_STORED)
{
uiDataGlobal.lastHeardCount++;
}
// need to use the last item in the list as the new item at the top of the list.
// find last item in the list
while(item->next != NULL)
{
item = item->next;
}
//item is now the last
(item->prev)->next = NULL;// make the previous item the last
LinkHead->prev = item;// set the current head item to back reference this item.
item->next = LinkHead;// set this items next to the current head
LinkHead = item;// Make this item the new head
item->id = id;
item->talkGroupOrPcId = talkGroupOrPcId;
item->time = ticksGetMillis();
item->receivedTS = (dmrMonitorCapturedTS != -1) ? dmrMonitorCapturedTS : trxGetDMRTimeSlot();
item->dmrMode = currentRadioDevice->trxDMRModeRx;
dmrRxAGCrxPeakAverage = DMR_RX_AGC_DEFAULT_PEAK_SAMPLES;
item->rxAGCGain = (uint16_t)dmrRxAGCrxPeakAverage;
lastTG = talkGroupOrPcId;
memset(item->contact, 0, sizeof(item->contact)); // Clear contact's datas
memset(item->talkgroup, 0, sizeof(item->talkgroup));
memset(item->talkerAlias, 0, sizeof(item->talkerAlias));
item->locationLat = NAN;
item->locationLon = NAN;
updateLHItem(item);
if (item->talkGroupOrPcId != 0)
{
uiDataGlobal.displayQSOState = QSO_DISPLAY_CALLER_DATA;// flag that the display needs to update
}
}
contactDefinedForTA = true;
}
else // update TG even if the DMRID did not change
{
LinkItem_t *item = lastHeardFindInList(id);
if (lastTG != talkGroupOrPcId)
{
if (item != NULL)
{
// Already in the list
item->talkGroupOrPcId = talkGroupOrPcId;// update the TG in case they changed TG
updateLHItem(item);
item->time = ticksGetMillis();
}
lastTG = talkGroupOrPcId;
lastHeardClearWorkingTAData();
}
item->receivedTS = (dmrMonitorCapturedTS != -1) ? dmrMonitorCapturedTS : trxGetDMRTimeSlot();// Always update this in case the TS changed.
item->dmrMode = currentRadioDevice->trxDMRModeRx;
contactDefinedForTA = true;
}
}
}
else
{
if (contactDefinedForTA)
{
// Data contains the Talker Alias Data
uint8_t blockID = (forceOnHotspot ? dmrDataBuffer[0] : DMR_frame_buffer[0]);
if (blockID >= 4)
{
blockID -= 4; // shift the blockID for maths reasons
if (blockID < 4) // ID 0x04..0x07: TA
{
// Already stored first byte in block TA Header has changed, lets clear other blocks too
if ((blockID == 0) && ((blocksTA & (1 << blockID)) != 0) &&
(bufferTA[0] != (forceOnHotspot ? dmrDataBuffer[2] : DMR_frame_buffer[2])))
{
blocksTA &= ~(1 << 0);
// Clear all other blocks if they're already stored
if ((blocksTA & (1 << 1)) != 0)
{
blocksTA &= ~(1 << 1);
memset(bufferTA + 7, 0, 7); // Clear 2nd TA block
}
if ((blocksTA & (1 << 2)) != 0)
{
blocksTA &= ~(1 << 2);
memset(bufferTA + 14, 0, 7); // Clear 3rd TA block
}
if ((blocksTA & (1 << 3)) != 0)
{
blocksTA &= ~(1 << 3);
memset(bufferTA + 21, 0, 7); // Clear 4th TA block
}
overrideTA = true;
}
// We don't already have this TA block
if ((blocksTA & (1 << blockID)) == 0)
{
static const uint8_t blockLen = 7;
uint32_t blockOffset = blockID * blockLen;
blocksTA |= (1 << blockID);
if ((blockOffset + blockLen) < sizeof(bufferTA))
{
memcpy(bufferTA + blockOffset, (void *)(forceOnHotspot ? &dmrDataBuffer[2] : &DMR_frame_buffer[2]), blockLen);
// Format and length infos are available, we can decode now
if (bufferTA[0] != 0x0)
{
uint8_t bufferTADest[32];
uint8_t taLen = 0U;
if ((taLen = decodeTA(&bufferTADest[0], sizeof(bufferTADest), &bufferTA[0])) > 0)
{
// TAs doesn't match, update contact and screen.
if (overrideTA || (taLen > strlen((const char *)&LinkHead->talkerAlias)))
{
memcpy(&LinkHead->talkerAlias, &bufferTADest[0], 31);// Brandmeister seems to send callsign as 6 chars only
if ((blocksTA & (1 << 1)) != 0) // we already received the 2nd TA block, check for 'DMR ID:'
{
char *p = NULL;
// Get rid of 'DMR ID:xxxxxxx' part of the TA, sent by BM
if (((p = strstr(&LinkHead->talkerAlias[0], "DMR ID:")) != NULL) || ((p = strstr(&LinkHead->talkerAlias[0], "DMR I")) != NULL))
{
*p = 0;
}
}
overrideTA = false;
uiDataGlobal.displayQSOState = QSO_DISPLAY_CALLER_DATA;
}
}
}
}
}
}
else if (blockID == 4) // ID 0x08: GPS
{
double longitude, latitude;
decodeGPSPosition((uint8_t *)(forceOnHotspot ? &dmrDataBuffer[0] : &DMR_frame_buffer[0]), &latitude,&longitude);
if ((LinkHead->locationLat != latitude) || (LinkHead->locationLon != longitude))
{
LinkHead->locationLat = latitude;
LinkHead->locationLon = longitude;
// If we received the location but no TA text, then insert ID:xxxxx so that the rest of the QSO display system works and displays the maindenhead
if (LinkHead->talkerAlias[0] == 0x00)
{
snprintf(LinkHead->talkerAlias, 16, "ID:%u", LinkHead->id);
}
uiDataGlobal.displayQSOState = QSO_DISPLAY_CALLER_DATA_UPDATE;
}
}
}
}
}
}
return true;
}
static bool dmrIDReadContactInFlash(uint32_t contactOffset, uint8_t *data, uint32_t len)
{
uint32_t address;
if (contactOffset >= dmrIdDataArea_1_Size)
{
address = dmrIDDatabaseMemoryLocation2 + (contactOffset - dmrIdDataArea_1_Size);
}
else
{
address = DMRID_MEMORY_LOCATION_1 + DMRID_HEADER_LENGTH + contactOffset;
}
return SPI_Flash_read(address, data, len);
}
void dmrIDCacheInit(void)
{
uint8_t headerBuf[32];
dmrIDCacheClear();
memset(&headerBuf, 0, sizeof(headerBuf));
SPI_Flash_read(DMRID_MEMORY_LOCATION_1, headerBuf, DMRID_HEADER_LENGTH);
// Break backward compatibility with old "ID{N,n} tag, as we had too
// much problems with corrupted database.
if ((headerBuf[0] != 'I') || (headerBuf[1] != 'd') )
{
return;
}
if (headerBuf[2] == 'N' || headerBuf[2] == 'n')
{
DMRID_IdLength = 3U;// default is 4
if (headerBuf[2] == 'n')
{
dmrIDDatabaseMemoryLocation2 = VOICE_PROMPTS_FLASH_HEADER_ADDRESS; // overwrite the VP
}
}
dmrIDsCache.contactLength = (uint8_t)headerBuf[3] - 0x4a;
// Check that data in DMR ID DB does not have a larger record size than the code has
if (dmrIDsCache.contactLength > sizeof(dmrIdDataStruct_t))
{
return;
}
dmrIDsCache.entries = ((uint32_t)headerBuf[8] | (uint32_t)headerBuf[9] << 8 | (uint32_t)headerBuf[10] << 16 | (uint32_t)headerBuf[11] << 24);
// Size of number of complete DMR ID records for the first storage location
dmrIdDataArea_1_Size = (dmrIDsCache.contactLength * ((0x40000 - DMRID_HEADER_LENGTH) / dmrIDsCache.contactLength));
if (dmrIDsCache.entries > 0)
{
dmrIdDataStruct_t dmrIDContact;
// Set Min and Max IDs boundaries
// First available ID
dmrIDContact.id = 0;
dmrIDReadContactInFlash(0, (uint8_t *)&dmrIDContact, DMRID_IdLength);
dmrIDsCache.slices[0] = dmrIDContact.id;
// Last available ID
dmrIDContact.id = 0;
dmrIDReadContactInFlash((dmrIDsCache.contactLength * (dmrIDsCache.entries - 1)), (uint8_t *)&dmrIDContact, DMRID_IdLength);
dmrIDsCache.slices[ID_SLICES - 1] = dmrIDContact.id;
if (dmrIDsCache.entries > MIN_ENTRIES_BEFORE_USING_SLICES)
{
dmrIDsCache.IDsPerSlice = dmrIDsCache.entries / (ID_SLICES - 1);
for (uint8_t i = 0; i < (ID_SLICES - 2); i++)
{
dmrIDContact.id = 0;
dmrIDReadContactInFlash((dmrIDsCache.contactLength * ((dmrIDsCache.IDsPerSlice * i) + dmrIDsCache.IDsPerSlice)), (uint8_t *)&dmrIDContact, DMRID_IdLength);
dmrIDsCache.slices[i + 1] = dmrIDContact.id;
}
}
}
}
void dmrIDCacheClear(void)
{
memset(&dmrIDsCache, 0, sizeof(dmrIDsCache_t));
}
uint32_t dmrIDCacheGetCount(void)
{
return dmrIDsCache.entries;
}
static void dmrDbTextDecode(uint8_t *decompressedBufOut, uint8_t *compressedBufIn, int compressedSize)
{
uint8_t *outPtr = decompressedBufOut;
uint8_t cb1, cb2, cb3;
int d = 0;
do
{
cb1 = compressedBufIn[d++];
*outPtr++ = DECOMPRESS_LUT[cb1 >> 2];//A
if (d == compressedSize)
{
break;
}
cb2 = compressedBufIn[d++];
*outPtr++ = DECOMPRESS_LUT[((cb1 & 0x03) << 4) + (cb2 >> 4)];//B
if (d == compressedSize)
{
break;
}
cb3 = compressedBufIn[d++];
*outPtr++ = DECOMPRESS_LUT[((cb2 & 0x0F) << 2) + (cb3 >> 6)];//C
*outPtr++ = DECOMPRESS_LUT[cb3 & 0x3F];//D
} while (d < compressedSize);
// algorithm can result in a extra space at the end of the decompressed string
// so trim the string
uint32_t l = (outPtr - decompressedBufOut);
if (l)
{
uint8_t *p = ((decompressedBufOut + l) - 1);
while ((p >= decompressedBufOut) && (*p == ' '))
{
*p-- = 0;
}
}
}
bool dmrIDLookup(uint32_t targetId, dmrIdDataStruct_t *foundRecord)
{
uint32_t targetIdBCD;
if (DMRID_IdLength == 4U)
{
targetIdBCD = int2bcd(targetId);
}
else
{
targetIdBCD = targetId;
}
if ((dmrIDsCache.entries > 0) && (targetIdBCD >= dmrIDsCache.slices[0]) && (targetIdBCD <= dmrIDsCache.slices[ID_SLICES - 1]))
{
uint32_t startPos = 0;
uint32_t endPos = dmrIDsCache.entries - 1;
uint32_t curPos;
// Contact's text length == (dmrIDsCache.contactLength - DMRID_IdLength) aren't NULL terminated,
// so clearing the whole destination array is mandatory
memset(foundRecord->text, 0, sizeof(foundRecord->text));
uint8_t compressedBuf[MAX_DMR_ID_CONTACT_TEXT_LENGTH];// worst case length with no compression
if (dmrIDsCache.entries > MIN_ENTRIES_BEFORE_USING_SLICES) // Use slices
{
for (uint8_t i = 0; i < ID_SLICES - 1; i++)
{
// Check if ID is in slices boundaries, with a special case for the last slice as [ID_SLICES - 1] is the last ID
if ((targetIdBCD >= dmrIDsCache.slices[i]) &&
((i == ID_SLICES - 2) ? (targetIdBCD <= dmrIDsCache.slices[i + 1]) : (targetIdBCD < dmrIDsCache.slices[i + 1])))
{
// targetID is the min slice limit, don't go further
if (targetIdBCD == dmrIDsCache.slices[i])
{
foundRecord->id = dmrIDsCache.slices[i];
if (dmrIDReadContactInFlash((dmrIDsCache.contactLength * (dmrIDsCache.IDsPerSlice * i)) + DMRID_IdLength, (uint8_t *)&compressedBuf, (dmrIDsCache.contactLength - DMRID_IdLength)))
{
if (DMRID_IdLength == 3U)
{
dmrDbTextDecode((uint8_t *)foundRecord->text, (uint8_t *)&compressedBuf, (dmrIDsCache.contactLength - DMRID_IdLength));
}
else
{
memcpy((uint8_t *)foundRecord->text, (uint8_t *)&compressedBuf, (dmrIDsCache.contactLength - DMRID_IdLength));
}
return true;
}
else
{
goto spiReadFailure;
}
}
startPos = dmrIDsCache.IDsPerSlice * i;
endPos = (i == ID_SLICES - 2) ? (dmrIDsCache.entries - 1) : dmrIDsCache.IDsPerSlice * (i + 1);
break;
}
}
}
else // Not enough contact to use slices
{
bool isMin;
// Check if targetID is equal to the first or the last in the IDs list
if ((isMin = (targetIdBCD == dmrIDsCache.slices[0])) || (targetIdBCD == dmrIDsCache.slices[ID_SLICES - 1]))
{
foundRecord->id = dmrIDsCache.slices[(isMin ? 0 : (ID_SLICES - 1))];
if (dmrIDReadContactInFlash((dmrIDsCache.contactLength * (dmrIDsCache.IDsPerSlice * (isMin ? 0 : (ID_SLICES - 1)))) + DMRID_IdLength, (uint8_t *) &compressedBuf, (dmrIDsCache.contactLength - DMRID_IdLength)))
{
if (DMRID_IdLength == 3U)
{
dmrDbTextDecode((uint8_t *)foundRecord->text, (uint8_t *)&compressedBuf, (dmrIDsCache.contactLength - DMRID_IdLength));
}
else
{
memcpy((uint8_t *)foundRecord->text, (uint8_t *)&compressedBuf, (dmrIDsCache.contactLength - DMRID_IdLength));
}
return true;
}
else
{
goto spiReadFailure;
}
}
}
// Look for the ID now
while (startPos <= endPos)
{
curPos = (startPos + endPos) >> 1;
foundRecord->id = 0;
if (dmrIDReadContactInFlash((dmrIDsCache.contactLength * curPos), (uint8_t *)foundRecord, DMRID_IdLength))
{
if (foundRecord->id < targetIdBCD)
{
startPos = curPos + 1;
}
else if (foundRecord->id > targetIdBCD)
{
endPos = curPos - 1;
}
else
{
dmrIDReadContactInFlash((dmrIDsCache.contactLength * curPos) + DMRID_IdLength, (uint8_t *)&compressedBuf, (dmrIDsCache.contactLength - DMRID_IdLength));
if (DMRID_IdLength == 3U)
{
dmrDbTextDecode((uint8_t *)foundRecord->text, (uint8_t *)&compressedBuf, (dmrIDsCache.contactLength - DMRID_IdLength));
}
else
{
memcpy((uint8_t *)foundRecord->text, (uint8_t *)&compressedBuf, (dmrIDsCache.contactLength - DMRID_IdLength));
}
return true;
}
}
else
{
goto spiReadFailure;
}
}
}
spiReadFailure:
snprintf(foundRecord->text, MAX_DMR_ID_CONTACT_TEXT_LENGTH, "ID:%d", targetId);
return false;
}
bool contactIDLookup(uint32_t id, ContactCalltype_t calltype, char *buffer)
{
CodeplugContact_t contact;
int8_t manTS = tsGetManualOverrideFromCurrentChannel();
int contactIndex = codeplugContactIndexByTGorPC((id & 0x00FFFFFF), calltype, &contact,
((monitorModeData.isEnabled && (dmrMonitorCapturedTS != -1)) ? (dmrMonitorCapturedTS + 1) :
(manTS ? manTS : (trxGetDMRTimeSlot() + 1))));
if (contactIndex != -1)
{
codeplugUtilConvertBufToString(contact.name, buffer, 16);
return true;
}
return false;
}
static void displayChannelNameOrRxFrequency(char *buffer, size_t maxLen)
{
if (menuSystemGetCurrentMenuNumber() == UI_CHANNEL_MODE)
{
#if defined(PLATFORM_MD9600)
if (codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_OUT_OF_BAND) != 0)
{
snprintf(buffer, maxLen, "%s", currentLanguage->out_of_band);
}
else
#endif
{
codeplugUtilConvertBufToString(currentChannelData->name, buffer, 16);
displayThemeApply(THEME_ITEM_FG_CHANNEL_NAME, THEME_ITEM_BG);
}
}
else
{
int val_before_dp = currentChannelData->rxFreq / 100000;
int val_after_dp = currentChannelData->rxFreq - val_before_dp * 100000;
snprintf(buffer, maxLen, "%d.%05d MHz", val_before_dp, val_after_dp);
displayThemeApply(THEME_ITEM_FG_RX_FREQ, THEME_ITEM_BG);
}
uiUtilityDisplayInformation(buffer, DISPLAY_INFO_ZONE, -1);
}
static void displaySplitOrSpanText(uint8_t y, char *text)
{
if (text != NULL)
{
uint8_t len = strlen(text);
#if defined(HAS_COLOURS)
bool dblHeight = settingsIsOptionBitSet(BIT_UI_USES_DOUBLE_HEIGHT);
#else
const bool dblHeight = false;
#endif
if (len == 0)
{
return;
}
else if (len <= 16)
{
displayPrintCenteredDoubleHeight(y, text, FONT_SIZE_3, dblHeight);
}
else
{
uint8_t nLines = len / 21 + (((len % 21) != 0) ? 1 : 0);
if (nLines > 2)
{
nLines = 2;
len = 42; // 2 lines max.
}
if (nLines > 1)
{
char buffer[MAX_DMR_ID_CONTACT_TEXT_LENGTH]; // to fit 2 * 21 chars + NULL, but needs larger
memcpy(buffer, text, len + 1);
buffer[len + 1] = 0;
char *p = buffer + 20;
// Find a space backward
while ((*p != ' ') && (p > buffer))
{
p--;
}
#if defined(PLATFORM_RD5R)
y -= 6; // Avoid to write the 2nd line off the screen
#endif
uint8_t rest = (uint8_t)((buffer + strlen(buffer)) - p) - ((*p == ' ') ? 1 : 0);
// rest is too long, just split the line in two chunks
if (rest > 21)
{
char c = buffer[21];
buffer[21] = 0;
displayPrintCenteredDoubleHeight(y, chomp(buffer), FONT_SIZE_1, dblHeight); // 2 pixels are saved, could center
buffer[21] = c;
displayPrintCenteredDoubleHeight(y + (dblHeight ? (FONT_SIZE_1_HEIGHT * 2) : FONT_SIZE_1_HEIGHT), chomp(buffer + 21), FONT_SIZE_1, dblHeight);
}
else
{
*p = 0;
displayPrintCenteredDoubleHeight(y, chomp(buffer), FONT_SIZE_1, dblHeight);
displayPrintCenteredDoubleHeight(y + (dblHeight ? (FONT_SIZE_1_HEIGHT * 2) : FONT_SIZE_1_HEIGHT), chomp(p + 1), FONT_SIZE_1, dblHeight);
}
}
else // One line of 21 chars max
{
displayPrintCenteredDoubleHeight(y
#if ! defined(PLATFORM_RD5R)
+ 4
#endif
, text, FONT_SIZE_1, dblHeight);
}
}
}
}
/*
* Try to extract callsign and extra text from TA or DMR ID data, then display that on
* two lines, if possible.
* We don't care if extra text is larger than 16 chars, ucPrint*() functions cut them.
*.
*/
static void displayContactTextInfos(char *text, size_t maxLen, bool isFromTalkerAlias)
{
// Max for TalkerAlias is 37: TA 27 (in 7bit format) + ' [' + 6 (Maidenhead) + ']' + NULL
// Max for DMRID Database is MAX_DMR_ID_CONTACT_TEXT_LENGTH (50 + NULL)
char buffer[MAX_DMR_ID_CONTACT_TEXT_LENGTH];
displayThemeApply(THEME_ITEM_FG_CHANNEL_CONTACT_INFO, THEME_ITEM_BG);
if (strlen(text) >= 5 && isFromTalkerAlias) // if it's Talker Alias and there is more text than just the callsign, split across 2 lines
{
char *pbuf;
int32_t cpos;
// User prefers to not span the TA info over two lines, check it that could fit
if ((nonVolatileSettings.splitContact == SPLIT_CONTACT_SINGLE_LINE_ONLY) ||
((nonVolatileSettings.splitContact == SPLIT_CONTACT_AUTO) && (strlen(text) <= 16)))
{
memcpy(buffer, text, 16);
buffer[16] = 0;
uiUtilityDisplayInformation(chomp(buffer), DISPLAY_INFO_CHANNEL, -1);
displayThemeApply(THEME_ITEM_FG_CHANNEL_CONTACT_INFO, THEME_ITEM_BG);
displayChannelNameOrRxFrequency(buffer, (sizeof(buffer) / sizeof(buffer[0])));
displayThemeResetToDefault();
return;
}
if ((cpos = getFirstSpacePos(text)) != -1)
{
// Callsign found
memcpy(buffer, text, cpos);
buffer[cpos] = 0;
uiUtilityDisplayInformation(chomp(buffer), DISPLAY_INFO_CHANNEL, -1);
displayThemeApply(THEME_ITEM_FG_CHANNEL_CONTACT_INFO, THEME_ITEM_BG);
memcpy(buffer, text + (cpos + 1), (maxLen - (cpos + 1)));
buffer[(maxLen - (cpos + 1))] = 0;
pbuf = chomp(buffer);
if (strlen(pbuf))
{
displaySplitOrSpanText(DISPLAY_Y_POS_CHANNEL_SECOND_LINE, pbuf);
}
else
{
displayChannelNameOrRxFrequency(buffer, (sizeof(buffer) / sizeof(buffer[0])));
}
}
else
{
// No space found, use a chainsaw
memcpy(buffer, text, 16);
buffer[16] = 0;
uiUtilityDisplayInformation(chomp(buffer), DISPLAY_INFO_CHANNEL, -1);
displayThemeApply(THEME_ITEM_FG_CHANNEL_CONTACT_INFO, THEME_ITEM_BG);
memcpy(buffer, text + 16, (maxLen - 16));
buffer[(strlen(text) - 16)] = 0;
pbuf = chomp(buffer);
if (strlen(pbuf))
{
displaySplitOrSpanText(DISPLAY_Y_POS_CHANNEL_SECOND_LINE, pbuf);
}
else
{
displayChannelNameOrRxFrequency(buffer, (sizeof(buffer) / sizeof(buffer[0])));
}
}
}
else
{
memcpy(buffer, text, 16);
buffer[16] = 0;
uiUtilityDisplayInformation(chomp(buffer), DISPLAY_INFO_CHANNEL, -1);
displayThemeApply(THEME_ITEM_FG_CHANNEL_CONTACT_INFO, THEME_ITEM_BG);
displayChannelNameOrRxFrequency(buffer, (sizeof(buffer) / sizeof(buffer[0])));
}
displayThemeResetToDefault();
}
void uiUtilityDisplayInformation(const char *str, displayInformation_t line, int8_t yOverride)
{
bool inverted = false;
#if defined(HAS_COLOURS)
bool dblHeight = settingsIsOptionBitSet(BIT_UI_USES_DOUBLE_HEIGHT);
#else
const bool dblHeight = false;
#endif
switch (line)
{
case DISPLAY_INFO_CONTACT_INVERTED:
displayThemeApply(THEME_ITEM_FG_CHANNEL_CONTACT, THEME_ITEM_BG);
#if defined(PLATFORM_RD5R) || defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
displayFillRect(0, DISPLAY_Y_POS_CONTACT
#if defined(PLATFORM_RD5R)
+ 1
#endif
, DISPLAY_SIZE_X, (MENU_ENTRY_HEIGHT * (dblHeight ? 2 : 1)), false);
#else
displayClearRows(2, 4, true);
#endif
inverted = true;
case DISPLAY_INFO_CONTACT:
displayThemeApply(THEME_ITEM_FG_CHANNEL_CONTACT, THEME_ITEM_BG);
displayPrintCoreDoubleHeight(0, ((yOverride == -1) ? (DISPLAY_Y_POS_CONTACT + V_OFFSET) : yOverride), str, FONT_SIZE_3, TEXT_ALIGN_CENTER, inverted, dblHeight);
break;
case DISPLAY_INFO_CONTACT_OVERRIDE_FRAME:
displayThemeApply(THEME_ITEM_FG_CHANNEL_CONTACT, THEME_ITEM_BG);
displayDrawRect(0, ((yOverride == -1) ? DISPLAY_Y_POS_CONTACT : yOverride), DISPLAY_SIZE_X, (OVERRIDE_FRAME_HEIGHT * (dblHeight ? 2 : 1)), true);
break;
case DISPLAY_INFO_CHANNEL_INVERTED:
displayThemeApply(THEME_ITEM_FG_CHANNEL_NAME, THEME_ITEM_BG);
inverted = true;
{
#if defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
displayFillRect(0, ((yOverride == -1) ? DISPLAY_Y_POS_CHANNEL_FIRST_LINE : yOverride), DISPLAY_SIZE_X, (OVERRIDE_FRAME_HEIGHT * (dblHeight ? 2 : 1)), false);
#else
int row = ((yOverride == -1) ? DISPLAY_Y_POS_CHANNEL_FIRST_LINE : yOverride)/8;
displayClearRows(row, row + 2, true);
#endif
}
case DISPLAY_INFO_CHANNEL:
#if defined(HAS_COLOURS)
if (! displayThemeIsForegroundColourEqualTo(THEME_ITEM_FG_CHANNEL_CONTACT_INFO)) // displayContactTextInfos() override theme color
{
displayThemeApply(THEME_ITEM_FG_CHANNEL_NAME, THEME_ITEM_BG);
}
#endif
displayPrintCoreDoubleHeight(0, ((yOverride == -1) ? DISPLAY_Y_POS_CHANNEL_FIRST_LINE : yOverride), str, FONT_SIZE_3, TEXT_ALIGN_CENTER, inverted, dblHeight);
break;
case DISPLAY_INFO_CHANNEL_DETAILS:
{
static const int bufLen = 24;
char buf[bufLen];
char *p = buf;
int radioMode = trxGetMode();
if (radioMode == RADIO_MODE_ANALOG)
{
CodeplugCSSTypes_t type = codeplugGetCSSType(currentChannelData->rxTone);
p += snprintf(p, bufLen, "Rx:");
if (type == CSS_TYPE_CTCSS)
{
p += snprintf(p, bufLen - (p - buf), "%d.%dHz", currentChannelData->rxTone / 10 , currentChannelData->rxTone % 10);
}
else if (type & CSS_TYPE_DCS)
{
p += dcsPrintf(p, bufLen - (p - buf), NULL, currentChannelData->rxTone);
}
else
{
p += snprintf(p, bufLen - (p - buf), "%s", currentLanguage->none);
}
p += snprintf(p, bufLen - (p - buf), "|Tx:");
type = codeplugGetCSSType(currentChannelData->txTone);
if (type == CSS_TYPE_CTCSS)
{
p += snprintf(p, bufLen - (p - buf), "%d.%dHz", currentChannelData->txTone / 10 , currentChannelData->txTone % 10);
}
else if (type & CSS_TYPE_DCS)
{
p += dcsPrintf(p, bufLen - (p - buf), NULL, currentChannelData->txTone);
}
else
{
p += snprintf(p, bufLen - (p - buf), "%s", currentLanguage->none);
}
displayThemeApply(THEME_ITEM_FG_CSS_SQL_VALUES, THEME_ITEM_BG);
displayPrintCenteredDoubleHeight(DISPLAY_Y_POS_CSS_INFO, buf, FONT_SIZE_1, dblHeight);
p = buf;
p += snprintf(p, bufLen, "SQL:%d%%", 5 * (((currentChannelData->sql == 0) ? nonVolatileSettings.squelchDefaults[currentRadioDevice->trxCurrentBand[TRX_RX_FREQ_BAND]] : currentChannelData->sql) - 1));
}
else
{
// Digital
uint32_t PCorTG = ((nonVolatileSettings.overrideTG != 0) ? nonVolatileSettings.overrideTG : codeplugContactGetPackedId(&currentContactData));
p = buf;
p += snprintf(p, bufLen, "%s %u",
(((PCorTG >> 24) == PC_CALL_FLAG) ? currentLanguage->pc : currentLanguage->tg),
(PCorTG & 0xFFFFFF));
}
#if (defined(CPU_MK22FN512VLL12) || defined(PLATFORM_MD9600))
displayPrintCentered(((radioMode == RADIO_MODE_DIGITAL) ? (DISPLAY_Y_POS_CSS_INFO + (FONT_SIZE_2_HEIGHT / 2)) : DISPLAY_Y_POS_SQL_INFO), buf, ((radioMode == RADIO_MODE_DIGITAL) ? FONT_SIZE_2 : FONT_SIZE_1));
#else
displayPrintCenteredDoubleHeight(((radioMode == RADIO_MODE_DIGITAL) ? DISPLAY_Y_POS_CSS_INFO : (DISPLAY_Y_POS_SQL_INFO + (dblHeight ? (FONT_SIZE_3_HEIGHT / 2): 0))), buf, ((radioMode == RADIO_MODE_DIGITAL) ? FONT_SIZE_3 : FONT_SIZE_1), dblHeight);
#endif
}
break;
case DISPLAY_INFO_TX_TIMER:
displayThemeApply(THEME_ITEM_FG_TX_COUNTER, THEME_ITEM_BG);
displayPrintCentered(((yOverride == -1) ? DISPLAY_Y_POS_TX_TIMER : yOverride), str, FONT_SIZE_4);
break;
case DISPLAY_INFO_ZONE:
{
bool distanceEnabled = (((uiDataGlobal.Scan.active == false) || (uiDataGlobal.Scan.active && (uiDataGlobal.Scan.state == SCAN_STATE_PAUSED))) &&
((nonVolatileSettings.roaming != ROAMING_OFF) && (CODEPLUG_ZONE_IS_ALLCHANNELS(currentZone) == false)));
bool displayingZone = (yOverride == -2);
if (displayingZone)
{
if (distanceEnabled)
{
displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG);
#if defined(HAS_COLOURS)
displayDrawRoundRect(5, (DISPLAY_Y_POS_ZONE - 3), (DISPLAY_SIZE_X - 10), (8 + 2 + 3), 3, true);
#else
displayFillRect(0, (DISPLAY_Y_POS_ZONE - 2), DISPLAY_SIZE_X,
(8 + 2
#if ! defined(PLATFORM_RD5R)
+ 2
#endif
), false);
#endif
}
displayThemeApply(THEME_ITEM_FG_ZONE_NAME, THEME_ITEM_BG);
}
#if defined(HAS_COLOURS)
displayPrintCentered(DISPLAY_Y_POS_ZONE, str, FONT_SIZE_1);
#else
displayPrintCore(0, DISPLAY_Y_POS_ZONE, str, FONT_SIZE_1, TEXT_ALIGN_CENTER, (displayingZone && distanceEnabled));
#endif
}
break;
}
displayThemeResetToDefault();
}
void uiUtilityRenderQSODataAndUpdateScreen(void)
{
if (isQSODataAvailableForCurrentTalker())
{
displayClearBuf();
uiUtilityRenderHeader(false, false, false);
uiUtilityRenderQSOData();
displayRender();
}
}
void uiUtilityRenderQSOData(void)
{
displayThemeApply(THEME_ITEM_FG_CHANNEL_CONTACT_INFO, THEME_ITEM_BG);
uiDataGlobal.receivedPcId = 0x00; //reset the received PcId
/*
* Note.
* When using Brandmeister reflectors. TalkGroups can be used to select reflectors e.g. TG 4009, and TG 5000 to check the connnection
* Under these conditions Brandmeister seems to respond with a message via a private call even if the command was sent as a TalkGroup,
* and this caused the Private Message acceptance system to operate.
* Brandmeister seems respond on the same ID as the keyed TG, so the code
* (LinkHead->id & 0xFFFFFF) != (trxTalkGroupOrPcId & 0xFFFFFF) is designed to stop the Private call system tiggering in these instances
*
* FYI. Brandmeister seems to respond with a TG value of the users on ID number,
* but I thought it was safer to disregard any PC's from IDs the same as the current TG
* rather than testing if the TG is the user's ID, though that may work as well.
*/
if (HRC6000GetReceivedTgOrPcId() != 0)
{
if ((LinkHead->talkGroupOrPcId >> 24) == PC_CALL_FLAG) // && (LinkHead->id & 0xFFFFFF) != (trxTalkGroupOrPcId & 0xFFFFFF))
{
// Its a Private call
displayPrintCentered(DISPLAY_Y_POS_CONTACT, LinkHead->contact, FONT_SIZE_3); // was hardcoded 16, which now collides with the S-meter labels
displayPrintCentered(DISPLAY_Y_POS_CHANNEL_FIRST_LINE, currentLanguage->private_call, FONT_SIZE_3);
if (LinkHead->talkGroupOrPcId != (trxDMRID | (PC_CALL_FLAG << 24)))
{
uiUtilityDisplayInformation((((LinkHead->talkGroupOrPcId & 0x00FFFFFF) == ALL_CALL_VALUE) ? currentLanguage->all_call : LinkHead->talkgroup), DISPLAY_INFO_ZONE, -1);
displayThemeApply(THEME_ITEM_FG_CHANNEL_CONTACT_INFO, THEME_ITEM_BG);
displayPrintAt(1, DISPLAY_Y_POS_ZONE, "=>", FONT_SIZE_1);
}
}
else
{
// Group call
bool different = (((LinkHead->talkGroupOrPcId & 0xFFFFFF) != trxTalkGroupOrPcId ) ||
(((currentRadioDevice->trxDMRModeRx != DMR_MODE_DMO) && (dmrMonitorCapturedTS != -1)) && (dmrMonitorCapturedTS != trxGetDMRTimeSlot())) ||
(trxGetDMRColourCode() != currentChannelData->txColor));
uiUtilityDisplayInformation(LinkHead->talkgroup, different ? DISPLAY_INFO_CONTACT_INVERTED : DISPLAY_INFO_CONTACT, -1);
// If voice prompt feedback is enabled. Play a short beep to indicate the inverse video display showing the TG / TS / CC does not match the current Tx config
if (different && nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_LEVEL_2)
{
soundSetMelody(MELODY_RX_TGTSCC_WARNING_BEEP);
}
displayThemeApply(THEME_ITEM_FG_CHANNEL_CONTACT_INFO, THEME_ITEM_BG);
switch (nonVolatileSettings.contactDisplayPriority)
{
case CONTACT_DISPLAY_PRIO_CC_DB_TA:
case CONTACT_DISPLAY_PRIO_DB_CC_TA:
// No contact found in codeplug and DMRIDs, use TA as fallback, if any.
if ((strncmp(LinkHead->contact, "ID:", 3) == 0) && (LinkHead->talkerAlias[0] != 0x00))
{
if (LinkHead->locationLat <= 90)
{
char tmpBufferTA[37]; // TA + ' [' + Maidenhead + ']' + NULL
memset(tmpBufferTA, 0, sizeof(tmpBufferTA));
char maidenheadBuffer[8];
coordsToMaidenhead((uint8_t *)maidenheadBuffer, LinkHead->locationLat, LinkHead->locationLon);
snprintf(tmpBufferTA, 37, "%s [%s]", LinkHead->talkerAlias, maidenheadBuffer);
displayContactTextInfos(tmpBufferTA, sizeof(tmpBufferTA), true);
}
else
{
displayContactTextInfos(LinkHead->talkerAlias, sizeof(LinkHead->talkerAlias), !(nonVolatileSettings.splitContact == SPLIT_CONTACT_SINGLE_LINE_ONLY));
}
}
else
{
displayContactTextInfos(LinkHead->contact, sizeof(LinkHead->contact), !(nonVolatileSettings.splitContact == SPLIT_CONTACT_SINGLE_LINE_ONLY));
}
break;
case CONTACT_DISPLAY_PRIO_TA_CC_DB:
case CONTACT_DISPLAY_PRIO_TA_DB_CC:
// Talker Alias have the priority here
if (LinkHead->talkerAlias[0] != 0x00)
{
if (LinkHead->locationLat <= 90)
{
char tmpBufferTA[37]; // TA + ' [' + Maidenhead + ']' + NULL
memset(tmpBufferTA, 0, sizeof(tmpBufferTA));
char maidenheadBuffer[8];
coordsToMaidenhead((uint8_t *)maidenheadBuffer, LinkHead->locationLat, LinkHead->locationLon);
snprintf(tmpBufferTA, 37, "%s [%s]", LinkHead->talkerAlias, maidenheadBuffer);
displayContactTextInfos(tmpBufferTA, sizeof(tmpBufferTA), true);
}
else
{
displayContactTextInfos(LinkHead->talkerAlias, sizeof(LinkHead->talkerAlias), !(nonVolatileSettings.splitContact == SPLIT_CONTACT_SINGLE_LINE_ONLY));
}
}
else // No TA, then use the one extracted from Codeplug or DMRIdDB
{
displayContactTextInfos(LinkHead->contact, sizeof(LinkHead->contact), !(nonVolatileSettings.splitContact == SPLIT_CONTACT_SINGLE_LINE_ONLY));
}
break;
}
}
}
displayThemeResetToDefault();
}
// Toggle the audio mute (speaker) state. The header shows a mute icon and the LED
// flashes amber (see the main loop) while muted.
void toggleAudioMute(void)
{
audioAmpMute(!audioAmpIsMuted());
headerRowIsDirty = true; // refresh the header so the mute icon appears/clears
}
void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, bool forceBatteryDisplay)
{
const int MODE_TEXT_X_OFFSET = 1;
#if defined(PLATFORM_MD9600)
const int FILTER_TEXT_X_CENTER = 35;
const int POWER_X_CENTER = 61;
const int COLOR_CODE_X_CENTER = 86;
#else
const int FILTER_TEXT_X_CENTER = 37;//34;
const int POWER_X_CENTER = 65;//63;
const int COLOR_CODE_X_CENTER = 92;
#endif
char buffer[SCREEN_LINE_BUFFER_SIZE];
static bool scanBlinkPhase = true;
static uint32_t blinkTime = 0;
int radioMode = trxGetMode();
uint8_t powerLevel = trxGetPowerLevel();
// Muted either explicitly (SK2) or by the volume knob turned fully down (-99).
bool audioIsMuted = (audioAmpIsMuted() || (lastVolume == -99));
bool isPerChannelPower = (currentChannelData->libreDMR_Power != 0x00);
bool scanIsActive = (uiDataGlobal.Scan.active || uiDataGlobal.Scan.toneActive);
bool batteryIsLow = batteryIsLowWarning();
int volts = -1, mvolts = -1;
int batteryPercentage = -1;
int16_t itemOffset = 0;
int16_t modePixelLen = 18; // DMR default length (3 * 6 pixels)
#if defined(PLATFORM_GD77S)
const bool aprsIsTXing = false;
const bool aprsIsTransmittingOnSameQRG = false;
#else
bool aprsIsTXing = aprsBeaconingIsTransmitting();
bool aprsIsTransmittingOnSameQRG = (aprsIsTXing && (aprsBeaconingOnDifferentFrequency() == false));
#endif
#if ! defined(HAS_COLOURS) || defined(PLATFORM_MD9600)
UNUSED_PARAMETER(forceBatteryDisplay);
#endif
if (settingsIsOptionBitSet(BIT_BATTERY_VOLTAGE_IN_HEADER))
{
getBatteryVoltage(&volts, &mvolts);
// It's not possible to have 2 digits voltage on HTs
#if defined(PLATFORM_MD9600)
if (volts < 10)
{
itemOffset = 2;
}
#endif
}
else
{
batteryPercentage = getBatteryPercentage();
if (batteryPercentage < 100)
{
#if defined(PLATFORM_MD9600)
itemOffset = 2;
#else
itemOffset = 1;
#endif
}
}
if (isVFODualWatchScanning == false)
{
if (isTransmittingIgnoringAPRSBeaconing() == false)
{
if (!isVFOSweepScanning)
{
uiUtilityDrawRSSIBarGraph();
}
}
else
{
if (radioMode == RADIO_MODE_DIGITAL)
{
uiUtilityDrawDMRMicLevelBarGraph();
}
else
{
uiUtilityDrawFMMicLevelBarGraph();
}
}
}
displayThemeApply(THEME_ITEM_FG_HEADER_TEXT, THEME_ITEM_BG_HEADER_TEXT);
#if defined(HAS_COLOURS)
displayFillRect(0, 0, DISPLAY_SIZE_X, DISPLAY_Y_POS_BAR, true);
#endif
if (scanIsActive || batteryIsLow)
{
if ((ticksGetMillis() - blinkTime) > (scanBlinkPhase ? 500 : 1000))
{
blinkTime = ticksGetMillis();
scanBlinkPhase = !scanBlinkPhase;
}
}
else
{
scanBlinkPhase = false;
}
if (isVFOSweepScanning)
{
int span = (VFO_SWEEP_SCAN_RANGE_SAMPLE_STEP_TABLE[uiDataGlobal.Scan.sweepStepSizeIndex] * VFO_SWEEP_NUM_SAMPLES) / (VFO_SWEEP_PIXELS_PER_STEP * 100);
sprintf(buffer, "+/-%dkHz", (span >> 1));
displayPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, buffer, FONT_SIZE_1, TEXT_ALIGN_LEFT, false);
}
switch(radioMode)
{
case RADIO_MODE_ANALOG:
if (isVFOSweepScanning == false)
{
bool isInverted = (scanIsActive ? scanBlinkPhase : false);
if (isVFODualWatchScanning)
{
strcpy(buffer, "[DW]");
}
else
{
strcpy(buffer, (trxGetBandwidthIs25kHz() ? "FM" : "FMN"));
}
modePixelLen = (strlen(buffer) * 6);
if (uiDataGlobal.talkaround)
{
isInverted = true;
displayFillRect(1, 1, modePixelLen, 8 + 1, false);
}
displayPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, buffer,
(((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) && (uiDataGlobal.dmrDisabled == false)) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, isInverted);
if (audioIsMuted)
{
displayDrawFastHLine(MODE_TEXT_X_OFFSET, (DISPLAY_Y_POS_HEADER + (FONT_SIZE_1_HEIGHT >> 1)), modePixelLen, !isInverted);
}
}
if ((aprsIsTransmittingOnSameQRG == false) && (monitorModeData.isEnabled == false) && (isVFOSweepScanning == false) && (isVFODualWatchScanning == false) &&
((currentChannelData->txTone != CODEPLUG_CSS_TONE_NONE) || (currentChannelData->rxTone != CODEPLUG_CSS_TONE_NONE)))
{
bool cssTextInverted = (trxGetAnalogFilterLevel() == ANALOG_FILTER_NONE);
if (currentChannelData->txTone != CODEPLUG_CSS_TONE_NONE)
{
strcpy(buffer, ((codeplugGetCSSType(currentChannelData->txTone) & CSS_TYPE_DCS) ? "DT" : "CT"));
}
else // tx and/or rx tones are enabled, no need to check for this
{
strcpy(buffer, ((codeplugGetCSSType(currentChannelData->rxTone) & CSS_TYPE_DCS) ? "D" : "C"));
}
// There is no room to display if rxTone is CTCSS or DCS, when txTone is set.
if (currentChannelData->rxTone != CODEPLUG_CSS_TONE_NONE)
{
strcat(buffer, "R");
}
int16_t cssPixLen = (strlen(buffer) * 6);
int16_t cssXPos = ((FILTER_TEXT_X_CENTER + (itemOffset * 1)) - (cssPixLen >> 1));
if (cssTextInverted)
{
// Inverted rectangle width is fixed size, large enough to fit 3 characters
displayFillRect((cssXPos - 1), DISPLAY_Y_POS_HEADER - 1, (cssPixLen + 1), 9, false);
}
displayPrintCore(cssXPos, DISPLAY_Y_POS_HEADER, buffer, FONT_SIZE_1, TEXT_ALIGN_LEFT, cssTextInverted);
}
break;
case RADIO_MODE_DIGITAL:
if (settingsUsbMode != USB_MODE_HOTSPOT)
{
bool contactTSActive = ((nonVolatileSettings.overrideTG == 0) && ((currentContactData.reserve1 & CODEPLUG_CONTACT_FLAG_NO_TS_OVERRIDE) == 0x00));
bool tsManOverride = false;
if (nonVolatileSettings.extendedInfosOnScreen & (INFO_ON_SCREEN_TS & INFO_ON_SCREEN_BOTH))
{
tsManOverride = (contactTSActive ? tsIsContactHasBeenOverriddenFromCurrentChannel() : (tsGetManualOverrideFromCurrentChannel() != 0));
}
if ((isVFODualWatchScanning == false) && (isVFOSweepScanning == false))
{
if ((scanIsActive ? (scanBlinkPhase == false) : true) && (nonVolatileSettings.dmrDestinationFilter > DMR_DESTINATION_FILTER_NONE))
{
displayFillRect(0, DISPLAY_Y_POS_HEADER - 1, 20, 9, false);
}
}
if (isVFOSweepScanning == false)
{
if (scanIsActive ? scanBlinkPhase == false : true)
{
bool isInverted = isVFODualWatchScanning ? false : ((scanIsActive ? scanBlinkPhase : false) ^ (nonVolatileSettings.dmrDestinationFilter > DMR_DESTINATION_FILTER_NONE));
if (uiDataGlobal.talkaround)
{
isInverted = true;
displayFillRect(1, 1, (3 * 6), 8 + 1, false);
}
displayPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, (isVFODualWatchScanning ? "[DW]" : "DMR"), ((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, isInverted);
if (audioIsMuted)
{
displayDrawFastHLine(MODE_TEXT_X_OFFSET, (DISPLAY_Y_POS_HEADER + (FONT_SIZE_1_HEIGHT >> 1)), (6 * (isVFODualWatchScanning ? 4 : 3)), !isInverted);
}
}
if (isVFODualWatchScanning == false)
{
bool tsInverted = false;
if (codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_FORCE_DMO) == 0)
{
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%s%d",
((contactTSActive && (monitorModeData.isEnabled == false)) ? "cS" : currentLanguage->ts),
((monitorModeData.isEnabled && (dmrMonitorCapturedTS != -1))? (dmrMonitorCapturedTS + 1) : trxGetDMRTimeSlot() + 1));
}
else
{
strncpy(buffer, "DMO", SCREEN_LINE_BUFFER_SIZE);
}
int16_t tsPixLen = (strlen(buffer) * 6);
int16_t tsXPos = ((FILTER_TEXT_X_CENTER + (itemOffset * 1)) - (tsPixLen >> 1));
if (!(nonVolatileSettings.dmrCcTsFilter & DMR_TS_FILTER_PATTERN))
{
displayFillRect((tsXPos - 1), DISPLAY_Y_POS_HEADER - 1, (tsPixLen + 1), 9, false);
tsInverted = true;
}
displayPrintCore(tsXPos, DISPLAY_Y_POS_HEADER, buffer, (tsManOverride ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, tsInverted);
}
}
}
break;
}
// APRS flag
if ((isVFOSweepScanning == false) && (isVFODualWatchScanning == false)
&& (currentChannelData->aprsConfigIndex != 0))
{
bool aprsSuspended = false;
themeItem_t fgItem, bgItem;
strcpy(buffer, "a");
#if ! defined(PLATFORM_GD77S)
if ((aprsBeaconingGetMode() == APRS_BEACONING_MODE_OFF) || aprsBeaconingIsSuspended())
{
aprsSuspended = true;
}
#endif
if (aprsIsTXing)
{
displayThemeGetForegroundAndBackgroundItems(&fgItem, &bgItem);
displayThemeApply(THEME_ITEM_FG_TX_COUNTER, bgItem);
}
displayPrintCore(MODE_TEXT_X_OFFSET + modePixelLen, DISPLAY_Y_POS_HEADER, buffer,
(aprsSuspended ? FONT_SIZE_1 : FONT_SIZE_1_BOLD), TEXT_ALIGN_LEFT, false);
if (aprsIsTXing)
{
displayThemeApply(fgItem, bgItem);
}
}
// Power
sprintf(buffer, "%s%s", getPowerLevel(powerLevel), getPowerLevelUnit(powerLevel));
if (isVFOSweepScanning) // Need to shift to the right due to sweep span
{
displayPrintCore((COLOR_CODE_X_CENTER - 11) - ((strlen(buffer) * 6) >> 1), DISPLAY_Y_POS_HEADER, buffer, FONT_SIZE_1, TEXT_ALIGN_LEFT, false);
}
else
{
displayPrintCore(((POWER_X_CENTER + (itemOffset * 2)) - ((strlen(buffer) * 6) >> 1)), DISPLAY_Y_POS_HEADER, buffer,
((isPerChannelPower && (nonVolatileSettings.extendedInfosOnScreen & (INFO_ON_SCREEN_PWR & INFO_ON_SCREEN_BOTH))) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, false);
}
// In hotspot mode the CC is show as part of the rest of the display and in Analog mode the CC is meaningless
if((isVFODualWatchScanning == false) && (isVFOSweepScanning == false) && (((settingsUsbMode == USB_MODE_HOTSPOT) || (radioMode == RADIO_MODE_ANALOG)) == false))
{
uint8_t ccode = trxGetDMRColourCode();
bool isNotFilteringCC = !(nonVolatileSettings.dmrCcTsFilter & DMR_CC_FILTER_PATTERN);
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "C%u", ccode);
int16_t ccPixLen = (strlen(buffer) * 6);
int16_t ccXPos = ((COLOR_CODE_X_CENTER + (itemOffset * 3)) - (ccPixLen >> 1));
if (isNotFilteringCC)
{
displayFillRect((ccXPos - 1), DISPLAY_Y_POS_HEADER - 1, (ccPixLen + 1), 9, false);
}
displayPrintCore(ccXPos, DISPLAY_Y_POS_HEADER, buffer, FONT_SIZE_1, TEXT_ALIGN_LEFT, isNotFilteringCC);
}
#if defined(HAS_COLOURS) || defined(PLATFORM_MD9600)
#if defined(ALTERNATES_TIME_AND_BATTERY_DISPLAY)
if (ticksTimerHasExpired(&batOrTimeTimer))
{
displayTimeInHeader = !displayTimeInHeader;
// Alternates Time/Battery display, at a rate of 2 minutes / 20 seconds, but
// once the battery is low, only battery is displayed, as a warning measure.
ticksTimerStart(&batOrTimeTimer, (displayTimeInHeader ? (2U * (60U * 1000U)) : (20U * 1000U)));
}
// Never display time when battery is low
bool displayTime = (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER) && displayTimeInHeader && (forceBatteryDisplay == false) && (batteryIsLow == false));
#else
bool displayTime = (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER) && (forceBatteryDisplay == false) && (batteryIsLow == false));
#endif
#endif
// Mute indicator: bold "M" just left of the battery indicator.
if (audioIsMuted)
{
displayThemeApply(THEME_ITEM_FG_HEADER_TEXT, THEME_ITEM_BG_HEADER_TEXT);
displayPrintCore((DISPLAY_SIZE_X - 40), DISPLAY_Y_POS_HEADER, "M", FONT_SIZE_1_BOLD, TEXT_ALIGN_LEFT, false);
displayThemeResetToDefault();
}
// Display battery percentage/voltage
bool apoEnabled = (nonVolatileSettings.apo > 0);
#if defined(HAS_GPS)
#if defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
if (SETTINGS_GPS_MODE_GET(nonVolatileSettings) >= GPS_MODE_ON)
{
displayPrintCore(DISPLAY_SIZE_X - (50 + (displayTime ? 2 : 0)),
DISPLAY_Y_POS_HEADER, currentLanguage->gps, ((gpsData.Status & GPS_STATUS_HAS_FIX) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, false);
}
#endif
#endif
#if defined(HAS_COLOURS) || defined(PLATFORM_MD9600)
if (displayTime)
{
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%c%02u", timeAndDate.tm_hour, (((timeAndDate.tm_sec % 2) == 0) ? ':' : ' ') ,timeAndDate.tm_min);
displayPrintCore(0, DISPLAY_Y_POS_HEADER, buffer, (apoEnabled ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_RIGHT, false);// Display battery percentage at the right
}
else
#endif
{
if (settingsIsOptionBitSet(BIT_BATTERY_VOLTAGE_IN_HEADER))
{
int16_t xV = (DISPLAY_SIZE_X - ((4 * 6) + 3));
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%2d", volts);
displayPrintCore(xV, DISPLAY_Y_POS_HEADER, buffer, (apoEnabled ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, ((batteryIsLow ? scanBlinkPhase : false)));
displayDrawRect(xV + (6 * 2), DISPLAY_Y_POS_HEADER + 5, 2, 2, ((batteryIsLow ? !scanBlinkPhase : true)));
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%1dV", mvolts);
displayPrintCore(xV + (6 * 2) + 3, DISPLAY_Y_POS_HEADER, buffer, (apoEnabled ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, ((batteryIsLow ? scanBlinkPhase : false)));
}
else
{
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%d%%", batteryPercentage);
displayPrintCore(0, DISPLAY_Y_POS_HEADER, buffer, (apoEnabled ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_RIGHT, ((batteryIsLow ? scanBlinkPhase : false)));// Display battery percentage at the right
}
}
displayThemeResetToDefault();
}
void uiUtilityRedrawHeaderOnly(bool isVFODualWatchScanning, bool isVFOSweepScanning, bool forceBatteryDisplay)
{
if (isVFOSweepScanning)
{
displayFillRect(0, 0, DISPLAY_SIZE_X, 9, true);
}
else
{
#if defined(PLATFORM_RD5R)
displayClearRows(0, 1, false);
#else
displayClearRows(0, 2, false);
#endif
}
uiUtilityRenderHeader(isVFODualWatchScanning, isVFOSweepScanning, forceBatteryDisplay);
displayRenderRows(0, 2);
}
static void drawHeaderBar(int *barWidth, int16_t barHeight)
{
*barWidth = CLAMP(*barWidth, 0, DISPLAY_SIZE_X);
displayThemeApply(THEME_ITEM_FG_RSSI_BAR, THEME_ITEM_BG_HEADER_TEXT);
if (*barWidth)
{
displayFillRect(0, DISPLAY_Y_POS_BAR, *barWidth, barHeight, false);
}
// Clear the end of the bar area, if needed
if (*barWidth < DISPLAY_SIZE_X)
{
displayFillRect(*barWidth, DISPLAY_Y_POS_BAR, (DISPLAY_SIZE_X - *barWidth), barHeight, true);
}
displayThemeResetToDefault();
}
// Segmented block S-meter (colour displays): 9 green blocks for S1..S9 (4 dB per
// S-unit) plus 6 red blocks for S9+10..S9+60 (10 dB per block), drawn with gaps so
// the header background shows between them. The leading block is partially filled to
// reflect the exact level.
#define SMETER_GREEN_BLOCKS 9
#define SMETER_RED_BLOCKS 6
#define SMETER_TOTAL_BLOCKS (SMETER_GREEN_BLOCKS + SMETER_RED_BLOCKS)
#define SMETER_BLOCK_GAP 2
#if defined(HAS_COLOURS)
#define HEADER_BAR_HEIGHT 6
#else
#define HEADER_BAR_HEIGHT 4
#endif
#if defined(HAS_COLOURS)
// S-meter scale labels drawn below the blocks. Each label is centred under the
// block whose upper edge corresponds to that level (S1..S9 in the green zone,
// +20/+40/+60 dB in the red zone).
static const struct { uint8_t block; const char *text; } smeterLabels[] = {
{ 0, "S1" }, { 2, "S3" }, { 4, "S5" }, { 6, "S7" }, { 8, "S9" },
{ 10, "+20" }, { 12, "+40" }, { 14, "+60" },
};
// The label row sits just below the bar; blank it (e.g. while transmitting, when
// the mic level replaces the S-meter and the labels no longer apply).
static void clearHeaderLabelRow(void)
{
displayThemeApply(THEME_ITEM_FG_HEADER_TEXT, THEME_ITEM_BG_HEADER_TEXT);
displayFillRect(0, (DISPLAY_Y_POS_BAR + HEADER_BAR_HEIGHT), DISPLAY_SIZE_X, FONT_SIZE_1_HEIGHT, true);
displayThemeResetToDefault();
}
#endif
void uiUtilityDrawRSSIBarGraph(void)
{
int rssi = trxGetRSSIdBm(RADIO_DEVICE_PRIMARY);
if ((rssi > SMETER_S9) && (trxGetMode() == RADIO_MODE_ANALOG))
{
// In Analog mode the hardware RSSI reads well above S9+60, so compress the
// over-S9 portion to keep the calibration consistent with the previous meter.
rssi = ((rssi - SMETER_S9) / STRONG_SIGNAL_RESCALE) + SMETER_S9;
}
#if defined(HAS_COLOURS)
// Fractional block position, in 1/100 block units (0 .. SMETER_TOTAL_BLOCKS*100).
// S0..S9 spans the 9 green blocks (4 dB per S-unit); S9..S9+60 spans the 6 red
// blocks (10 dB per block).
int blockPos;
if (rssi <= SMETER_S9)
{
blockPos = ((rssi - SMETER_S0) * 100) / 4;
}
else
{
blockPos = (SMETER_GREEN_BLOCKS * 100) + (((rssi - SMETER_S9) * 100) / 10);
}
blockPos = CLAMP(blockPos, 0, (SMETER_TOTAL_BLOCKS * 100));
// Clear the whole bar band so the gaps (and unlit blocks) show the background.
displayThemeApply(THEME_ITEM_FG_RSSI_BAR, THEME_ITEM_BG_HEADER_TEXT);
displayFillRect(0, DISPLAY_Y_POS_BAR, DISPLAY_SIZE_X, HEADER_BAR_HEIGHT, true);
for (int i = 0; i < SMETER_TOTAL_BLOCKS; i++)
{
int fill = blockPos - (i * 100); // 0..100 = how much of this block is lit
if (fill <= 0)
{
continue; // not reached yet: leave as background
}
if (fill > 100)
{
fill = 100;
}
int xStart = (i * DISPLAY_SIZE_X) / SMETER_TOTAL_BLOCKS;
int xEnd = ((i + 1) * DISPLAY_SIZE_X) / SMETER_TOTAL_BLOCKS;
int blockWidth = (xEnd - xStart) - SMETER_BLOCK_GAP;
if (blockWidth <= 0)
{
continue;
}
int fillWidth = (blockWidth * fill) / 100;
if (fillWidth <= 0)
{
continue;
}
displayThemeApply(((i < SMETER_GREEN_BLOCKS) ? THEME_ITEM_FG_RSSI_BAR : THEME_ITEM_FG_RSSI_BAR_S9P), THEME_ITEM_BG_HEADER_TEXT);
displayFillRect(xStart, DISPLAY_Y_POS_BAR, fillWidth, HEADER_BAR_HEIGHT, false);
}
// Draw the S-meter scale labels just below the blocks.
displayThemeApply(THEME_ITEM_FG_HEADER_TEXT, THEME_ITEM_BG_HEADER_TEXT);
for (int l = 0; l < (int)(sizeof(smeterLabels) / sizeof(smeterLabels[0])); l++)
{
int b = smeterLabels[l].block;
int blockCentre = (((b * DISPLAY_SIZE_X) / SMETER_TOTAL_BLOCKS) + (((b + 1) * DISPLAY_SIZE_X) / SMETER_TOTAL_BLOCKS)) / 2;
int textWidth = ((int)strlen(smeterLabels[l].text)) * 6; // FONT_SIZE_1 is 6 px wide
int textX = CLAMP((blockCentre - (textWidth / 2)), 0, (DISPLAY_SIZE_X - textWidth));
displayPrintAt(textX, (DISPLAY_Y_POS_BAR + HEADER_BAR_HEIGHT), smeterLabels[l].text, FONT_SIZE_1);
}
displayThemeResetToDefault();
#else
// Monochrome displays: keep the original solid bar.
rssi = (rssi - SMETER_S0) * 2;
int barWidth = ((rssi * rssiMeterHeaderBarNumUnits) / rssiMeterHeaderBarDivider);
drawHeaderBar(&barWidth, HEADER_BAR_HEIGHT);
#endif
}
void uiUtilityDrawFMMicLevelBarGraph(void)
{
trxReadVoxAndMicStrength();
uint16_t micdB =
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
trxTxMic + // trxTxMic is in 0.2dB unit
((nonVolatileSettings.micGainFM) * 15) // mic gain adjustment is 3dB per increment of micGainFM, so scale it to 0.2dB increments
#else
(trxTxMic >> 1) // trxTxMic is in 0.5dB unit, displaying 50dB .. 100dB
#endif
;
int barWidth = SAFE_MIN(
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
((uint16_t)(((float)DISPLAY_SIZE_X / 200.0) * ((float)micdB - 150.0))) // display from 30dB to 70dB, = 150 to 350 units and span over 160pix
#else
((uint16_t)(((float)DISPLAY_SIZE_X / 50.0) * ((float)micdB - 50.0))) // display from 50dB to 100dB, span over 128pix
#endif
, DISPLAY_SIZE_X);
drawHeaderBar(&barWidth, HEADER_BAR_HEIGHT);
#if defined(HAS_COLOURS)
clearHeaderLabelRow();
#endif
}
void uiUtilityDrawDMRMicLevelBarGraph(void)
{
int barWidth = ((uint16_t)(sqrt(micAudioSamplesTotal) * 1.5));
drawHeaderBar(&barWidth, HEADER_BAR_HEIGHT);
#if defined(HAS_COLOURS)
clearHeaderLabelRow();
#endif
}
void setOverrideTGorPC(uint32_t tgOrPc, bool privateCall)
{
uiDataGlobal.tgBeforePcMode = 0;
if (privateCall)
{
// Private Call
if ((trxTalkGroupOrPcId >> 24) != PC_CALL_FLAG)
{
// if the current Tx TG is a TalkGroup then save it so it can be restored after the end of the private call
uiDataGlobal.tgBeforePcMode = trxTalkGroupOrPcId;
}
tgOrPc |= (PC_CALL_FLAG << 24);
}
settingsSet(nonVolatileSettings.overrideTG, tgOrPc);
}
void uiUtilityDisplayFrequency(uint8_t y, bool isTX, bool hasFocus, uint32_t frequency, bool displayVFOChannel, bool isScanMode, uint8_t dualWatchVFO)
{
char buffer[SCREEN_LINE_BUFFER_SIZE];
int val_before_dp = frequency / 100000;
int val_after_dp = frequency - val_before_dp * 100000;
#if ! defined(HAS_COLOURS)
uint16_t yFont3 = y;
uint16_t yFont1 = y;
#else
bool dblHeight = settingsIsOptionBitSet(BIT_UI_USES_DOUBLE_HEIGHT);
bool isTransmitting = isTransmittingIgnoringAPRSBeaconing();
bool displayDblHeightAndRX = (dblHeight && !isTX && !isScanMode && !isTransmitting);
bool displayFocusAndDblHeight = (dblHeight && ((hasFocus || !displayVFOChannel) && !isScanMode && !isTransmitting));
bool displayFrequencyScannningAndDblHeight = (dblHeight && !isTX && (uiVFOModeSweepScanning(true) || isScanMode));
uint16_t yFont3 = (y - ((displayFocusAndDblHeight || (displayFrequencyScannningAndDblHeight && isScanMode)) ? FONT_SIZE_3_HEIGHT : (displayDblHeightAndRX ? FONT_SIZE_3_HEIGHT : 0)));
uint16_t yFont1 = (y - ((displayFocusAndDblHeight || (displayFrequencyScannningAndDblHeight && isScanMode)) ? FONT_SIZE_1_HEIGHT : (displayDblHeightAndRX ? FONT_SIZE_3_HEIGHT : 0)));
if (displayFrequencyScannningAndDblHeight)
{
displayFocusAndDblHeight = true;
if (!isScanMode) // Only apply in sweep scanning
{
yFont3 = y -
#if defined(PLATFORM_VARIANT_DM1701)
8;
#else
4;
#endif
yFont1 = yFont3 + FONT_SIZE_1_HEIGHT;
}
else if (isScanMode && displayVFOChannel && (dualWatchVFO == 2U)) /* DualWatch's VFO B */
{
yFont3 += (FONT_SIZE_3_HEIGHT / 2) +
#if defined(PLATFORM_VARIANT_DM1701)
0;
#else
6;
#endif
yFont1 += (FONT_SIZE_3_HEIGHT / 2) +
#if defined(PLATFORM_VARIANT_DM1701)
0;
#else
6;
#endif
}
}
else if (displayFocusAndDblHeight && !displayVFOChannel && isTX)
{
yFont3 += (FONT_SIZE_3_HEIGHT / 2) + 4;
yFont1 += (FONT_SIZE_3_HEIGHT / 2) + 4;
}
#endif
displayThemeApply(isTX ? THEME_ITEM_FG_TX_FREQ : THEME_ITEM_FG_RX_FREQ, THEME_ITEM_BG);
// Focus + direction
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%c%c", ((hasFocus && !isScanMode) ? '>' : ' '), (isTX ? 'T' : 'R'));
displayPrintAtDoubleHeight(0, yFont3, buffer, FONT_SIZE_3, displayFocusAndDblHeight);
// VFO
if (displayVFOChannel)
{
displayPrintAtDoubleHeight(16, (yFont1 + VFO_LETTER_Y_OFFSET), (((dualWatchVFO == 0) && (nonVolatileSettings.currentVFONumber == 0)) || (dualWatchVFO == 1)) ? "A" : "B", FONT_SIZE_1, displayFocusAndDblHeight);
}
// Frequency
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%d.%05d", val_before_dp, val_after_dp);
#if defined(HAS_COLOURS)
// 7-segment "LED" style with faint inactive segments. Digits are wider than the
// normal font (12 px cells) to fill the space up to the "MHz" label.
displayDrawFrequencySevenSeg(FREQUENCY_X_POS, yFont3, 12, (displayFocusAndDblHeight ? (FONT_SIZE_3_HEIGHT * 2) : FONT_SIZE_3_HEIGHT),
buffer, (isTX ? THEME_ITEM_FG_TX_FREQ : THEME_ITEM_FG_RX_FREQ));
#else
displayPrintAtDoubleHeight(FREQUENCY_X_POS, yFont3, buffer, FONT_SIZE_3, displayFocusAndDblHeight);
#endif
displayPrintAtDoubleHeight(DISPLAY_SIZE_X - (3 * 8), yFont3, "MHz", FONT_SIZE_3, displayFocusAndDblHeight);
displayThemeResetToDefault();
}
size_t dcsPrintf(char *dest, size_t maxLen, char *prefix, uint16_t tone)
{
return snprintf(dest, maxLen, "%sD%03X%c", (prefix ? prefix : ""), (tone & ~CSS_TYPE_DCS_MASK), ((tone & CSS_TYPE_DCS_INVERTED) ? 'I' : 'N'));
}
void freqEnterReset(void)
{
memset(uiDataGlobal.FreqEnter.digits, '-', FREQ_ENTER_DIGITS_MAX);
uiDataGlobal.FreqEnter.index = 0;
}
int freqEnterRead(int startDigit, int endDigit, bool simpleDigits)
{
int result = 0;
if (((startDigit >= 0) && (startDigit <= FREQ_ENTER_DIGITS_MAX)) && ((endDigit >= 0) && (endDigit <= FREQ_ENTER_DIGITS_MAX)))
{
for (int i = startDigit; i < endDigit; i++)
{
result = result * 10;
if ((uiDataGlobal.FreqEnter.digits[i] >= '0') && (uiDataGlobal.FreqEnter.digits[i] <= '9'))
{
result = result + uiDataGlobal.FreqEnter.digits[i] - '0';
}
else
{
if (simpleDigits) // stop here, simple numbers are wanted
{
result /= 10;
break;
}
}
}
}
return result;
}
int getBatteryPercentage(void)
{
return SAFE_MAX(0, SAFE_MIN(((int)(((averageBatteryVoltage - CUTOFF_VOLTAGE_UPPER_HYST) * 100) / (BATTERY_MAX_VOLTAGE - CUTOFF_VOLTAGE_UPPER_HYST))), 100));
}
void getBatteryVoltage(int *volts, int *mvolts)
{
*volts = (int)(averageBatteryVoltage / 10);
*mvolts = (int)(averageBatteryVoltage - (*volts * 10));
}
bool increasePowerLevel(bool allowFullPower)
{
bool powerHasChanged = false;
if (currentChannelData->libreDMR_Power != 0x00)
{
if (currentChannelData->libreDMR_Power < (MAX_POWER_SETTING_NUM - 1 + CODEPLUG_MIN_PER_CHANNEL_POWER) + (allowFullPower ? 1 : 0))
{
currentChannelData->libreDMR_Power++;
trxSetPowerFromLevel(currentChannelData->libreDMR_Power - 1);
powerHasChanged = true;
}
}
else
{
if (nonVolatileSettings.txPowerLevel < (MAX_POWER_SETTING_NUM - 1 + (allowFullPower ? 1 : 0)))
{
settingsIncrement(nonVolatileSettings.txPowerLevel, 1);
trxSetPowerFromLevel(nonVolatileSettings.txPowerLevel);
powerHasChanged = true;
}
}
announceItem(PROMPT_SEQUENCE_POWER, PROMPT_THRESHOLD_3);
uiNotificationShow(NOTIFICATION_TYPE_POWER, NOTIFICATION_ID_POWER, 1000, NULL, true);
return powerHasChanged;
}
bool decreasePowerLevel(void)
{
bool powerHasChanged = false;
if (currentChannelData->libreDMR_Power != 0x00)
{
if (currentChannelData->libreDMR_Power > CODEPLUG_MIN_PER_CHANNEL_POWER)
{
currentChannelData->libreDMR_Power--;
trxSetPowerFromLevel(currentChannelData->libreDMR_Power - 1);
powerHasChanged = true;
}
}
else
{
if (nonVolatileSettings.txPowerLevel > 0)
{
settingsDecrement(nonVolatileSettings.txPowerLevel, 1);
trxSetPowerFromLevel(nonVolatileSettings.txPowerLevel);
powerHasChanged = true;
}
}
announceItem(PROMPT_SEQUENCE_POWER, PROMPT_THRESHOLD_3);
uiNotificationShow(NOTIFICATION_TYPE_POWER, NOTIFICATION_ID_POWER, 1000, NULL, true);
return powerHasChanged;
}
ANNOUNCE_STATIC void announceRadioMode(bool voicePromptWasPlaying)
{
int radioMode = trxGetMode();
if (!voicePromptWasPlaying)
{
voicePromptsAppendLanguageString(currentLanguage->mode);
}
voicePromptsAppendPrompt(((radioMode == RADIO_MODE_DIGITAL) ? PROMPT_DMR : PROMPT_FM));
if ((radioMode == RADIO_MODE_ANALOG) && (currentChannelData->aprsConfigIndex != 0))
{
voicePromptsAppendLanguageString(currentLanguage->APRS);
}
}
ANNOUNCE_STATIC void announceZoneName(bool voicePromptWasPlaying)
{
char nameBuf[17];
if (!voicePromptWasPlaying)
{
voicePromptsAppendLanguageString(currentLanguage->zone);
}
codeplugUtilConvertBufToString(currentZone.name, nameBuf, 16);
if (strcmp(nameBuf, currentLanguage->all_channels) == 0)
{
voicePromptsAppendLanguageString(currentLanguage->all_channels);
}
else
{
voicePromptsAppendString(nameBuf);
}
}
ANNOUNCE_STATIC void announceDistance(bool voicePromptWasPlaying)
{
char distBuf[SCREEN_LINE_BUFFER_SIZE];
if ((currentChannelData->NOT_IN_CODEPLUG_CALCULATED_DISTANCE_X10 != -1) &&
(settingsIsOptionBitSet(BIT_DISPLAY_CHANNEL_DISTANCE) || ((nonVolatileSettings.roaming != ROAMING_OFF) && (CODEPLUG_ZONE_IS_ALLCHANNELS(currentZone) == false))))
{
if (!voicePromptWasPlaying)
{
voicePromptsAppendPrompt(PROMPT_DISTANCE);
}
uint32_t intPart = (currentChannelData->NOT_IN_CODEPLUG_CALCULATED_DISTANCE_X10 / 10);
uint32_t decPart = (currentChannelData->NOT_IN_CODEPLUG_CALCULATED_DISTANCE_X10 - (intPart * 10));
if (decPart != 0)
{
snprintf(distBuf, SCREEN_LINE_BUFFER_SIZE, "%u.%u", intPart, decPart);
}
else
{
snprintf(distBuf, SCREEN_LINE_BUFFER_SIZE, "%u", intPart);
}
voicePromptsAppendString(distBuf);
voicePromptsAppendPrompt(PROMPT_KILOMETERS);
}
}
ANNOUNCE_STATIC void announceContactNameTgOrPc(bool voicePromptWasPlaying)
{
if (nonVolatileSettings.overrideTG == 0)
{
char nameBuf[17];
if (!voicePromptWasPlaying)
{
voicePromptsAppendLanguageString(currentLanguage->contact);
}
codeplugUtilConvertBufToString(currentContactData.name, nameBuf, 16);
voicePromptsAppendString(nameBuf);
}
else
{
char buf[17];
itoa(nonVolatileSettings.overrideTG & 0xFFFFFF, buf, 10);
if ((nonVolatileSettings.overrideTG >> 24) == PC_CALL_FLAG)
{
if (!voicePromptWasPlaying)
{
voicePromptsAppendLanguageString(currentLanguage->private_call);
}
voicePromptsAppendString("ID");
}
else
{
voicePromptsAppendPrompt(PROMPT_TALKGROUP);
}
voicePromptsAppendString(buf);
}
}
ANNOUNCE_STATIC void announcePowerLevel(bool voicePromptWasPlaying)
{
uint8_t powerLevel = trxGetPowerLevel();
if (!voicePromptWasPlaying)
{
voicePromptsAppendPrompt(PROMPT_POWER);
}
if (powerLevel < 9)
{
voicePromptsAppendString((char *)getPowerLevel(powerLevel));
switch(powerLevel)
{
case 0://50mW
case 1://250mW
case 2://500mW
case 3://750mW
voicePromptsAppendPrompt(PROMPT_MILLIWATTS);
break;
case 4://1W
voicePromptsAppendPrompt(PROMPT_WATT);
break;
default:
voicePromptsAppendPrompt(PROMPT_WATTS);
break;
}
}
else
{
voicePromptsAppendLanguageString(currentLanguage->user_power);
}
}
#if defined(PLATFORM_GD77S)
void announceEcoLevel(bool voicePromptWasPlaying)
{
if (!voicePromptWasPlaying)
{
voicePromptsAppendLanguageString(currentLanguage->eco_level);
}
voicePromptsAppendInteger(nonVolatileSettings.ecoLevel);
}
#endif
ANNOUNCE_STATIC void announceTemperature(bool voicePromptWasPlaying)
{
char buffer[SCREEN_LINE_BUFFER_SIZE];
int temperature = getTemperature();
if (!voicePromptWasPlaying)
{
voicePromptsAppendLanguageString(currentLanguage->temperature);
}
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%d.%1d", (temperature / 10), (temperature % 10));
voicePromptsAppendString(buffer);
voicePromptsAppendLanguageString(currentLanguage->celcius);
}
ANNOUNCE_STATIC void announceBatteryVoltage(void)
{
char buffer[SCREEN_LINE_BUFFER_SIZE];
int volts, mvolts;
voicePromptsAppendLanguageString(currentLanguage->battery);
getBatteryVoltage(&volts, &mvolts);
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, " %1d.%1d", volts, mvolts);
voicePromptsAppendString(buffer);
voicePromptsAppendPrompt(PROMPT_VOLTS);
}
ANNOUNCE_STATIC void announceBatteryPercentage(void)
{
voicePromptsAppendLanguageString(currentLanguage->battery);
voicePromptsAppendInteger(getBatteryPercentage());
voicePromptsAppendPrompt(PROMPT_PERCENT);
}
ANNOUNCE_STATIC void announceTS(void)
{
if (codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_FORCE_DMO) == 0)
{
voicePromptsAppendLanguageString(currentLanguage->timeSlot);
}
else
{
voicePromptsAppendString("DMO");
}
voicePromptsAppendInteger(trxGetDMRTimeSlot() + 1);
}
ANNOUNCE_STATIC void announceCC(void)
{
voicePromptsAppendLanguageString(currentLanguage->colour_code);
voicePromptsAppendInteger(trxGetDMRColourCode());
}
ANNOUNCE_STATIC void announceChannelName(bool voicePromptWasPlaying)
{
if (!voicePromptWasPlaying)
{
voicePromptsAppendPrompt(PROMPT_CHANNEL);
}
if (currentZone.NOT_IN_CODEPLUGDATA_numChannelsInZone > 0)
{
char voiceBuf[17];
codeplugUtilConvertBufToString(channelScreenChannelData.name, voiceBuf, 16);
voicePromptsAppendString(voiceBuf);
if (uiDataGlobal.reverseRepeaterChannel)
{
voicePromptsAppendPrompt(PROMPT_SILENCE);
voicePromptsAppendPrompt(PROMPT_REVERSE_REPEATER);
}
if(uiDataGlobal.talkaround)
{
voicePromptsAppendPrompt(PROMPT_SILENCE);
voicePromptsAppendLanguageString(currentLanguage->talkaround);
}
}
else
{
voicePromptsAppendLanguageString(currentLanguage->zone_empty);
}
}
static void removeUnnecessaryZerosFromVoicePrompts(char *str)
{
const int NUM_DECIMAL_PLACES = 1;
int len = strlen(str);
for(int i = len; i > 2; i--)
{
if ((str[i - 1] != '0') || (str[i - (NUM_DECIMAL_PLACES + 1)] == '.'))
{
str[i] = 0;
return;
}
}
}
static void announceQRG(uint32_t qrg, bool unit)
{
char buffer[SCREEN_LINE_BUFFER_SIZE];
int val_before_dp = qrg / 100000;
int val_after_dp = qrg - val_before_dp * 100000;
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%d.%05d", val_before_dp, val_after_dp);
removeUnnecessaryZerosFromVoicePrompts(buffer);
voicePromptsAppendString(buffer);
if (unit)
{
voicePromptsAppendPrompt(PROMPT_MEGAHERTZ);
}
}
ANNOUNCE_STATIC void announceFrequency(void)
{
bool duplex = (currentChannelData->txFreq != currentChannelData->rxFreq);
if (duplex)
{
voicePromptsAppendPrompt(PROMPT_RECEIVE);
}
announceQRG(currentChannelData->rxFreq, true);
if (duplex)
{
voicePromptsAppendPrompt(PROMPT_TRANSMIT);
announceQRG(currentChannelData->txFreq, true);
}
}
ANNOUNCE_STATIC void announceVFOChannelName(void)
{
voicePromptsAppendPrompt(PROMPT_VFO);
voicePromptsAppendString((nonVolatileSettings.currentVFONumber == 0) ? "A" : "B");
voicePromptsAppendPrompt(PROMPT_SILENCE);
}
ANNOUNCE_STATIC void announceVFOAndFrequency(bool announceVFOName)
{
if (announceVFOName)
{
announceVFOChannelName();
}
announceFrequency();
}
ANNOUNCE_STATIC void announceSquelchLevel(bool voicePromptWasPlaying)
{
static const int BUFFER_LEN = 8;
char buf[BUFFER_LEN];
if (!voicePromptWasPlaying)
{
voicePromptsAppendLanguageString(currentLanguage->squelch);
}
snprintf(buf, BUFFER_LEN, "%d%%", 5 * (((currentChannelData->sql == 0) ? nonVolatileSettings.squelchDefaults[currentRadioDevice->trxCurrentBand[TRX_RX_FREQ_BAND]] : currentChannelData->sql)-1));
voicePromptsAppendString(buf);
}
void announceChar(char ch)
{
if (nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD)
{
return;
}
char buf[2] = { ch, 0 };
voicePromptsInit();
voicePromptsAppendString(buf);
voicePromptsPlay();
}
void buildCSSCodeVoicePrompts(uint16_t tone, CodeplugCSSTypes_t cssType, Direction_t direction, bool announceType)
{
static const int BUFFER_LEN = 6;
char buf[BUFFER_LEN];
switch(direction)
{
case DIRECTION_RECEIVE:
voicePromptsAppendString("RX");
break;
case DIRECTION_TRANSMIT:
voicePromptsAppendString("TX");
break;
default:
break;
}
voicePromptsAppendPrompt(PROMPT_SILENCE);
if (cssType == CSS_TYPE_NONE)
{
voicePromptsAppendString("CSS");
voicePromptsAppendPrompt(PROMPT_SILENCE);
voicePromptsAppendLanguageString(currentLanguage->none);
}
else if (cssType == CSS_TYPE_CTCSS)
{
if (announceType)
{
voicePromptsAppendString("CTCSS");
voicePromptsAppendPrompt(PROMPT_SILENCE);
}
snprintf(buf, BUFFER_LEN, "%d.%d", tone / 10, tone % 10);
voicePromptsAppendString(buf);
voicePromptsAppendPrompt(PROMPT_HERTZ);
}
else if (cssType & CSS_TYPE_DCS)
{
if (announceType)
{
voicePromptsAppendString("DCS");
voicePromptsAppendPrompt(PROMPT_SILENCE);
}
dcsPrintf(buf, BUFFER_LEN, NULL, tone);
voicePromptsAppendString(buf);
}
}
void announceCSSCode(uint16_t tone, CodeplugCSSTypes_t cssType, Direction_t direction, bool announceType, audioPromptThreshold_t immediateAnnounceThreshold)
{
if (nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD)
{
return;
}
bool voicePromptWasPlaying = voicePromptsIsPlaying();
voicePromptsInit();
buildCSSCodeVoicePrompts(tone, cssType, direction, announceType);
// Follow-on when voicePromptWasPlaying is enabled on voice prompt level 2 and above
// Prompts are voiced immediately on voice prompt level 3
if ((voicePromptWasPlaying && (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_LEVEL_2)) ||
(nonVolatileSettings.audioPromptMode >= immediateAnnounceThreshold))
{
voicePromptsPlay();
}
}
static void announceChannelNameOrVFOFrequency(bool voicePromptWasPlaying, bool announceVFOName)
{
if (menuSystemGetCurrentMenuNumber() == UI_CHANNEL_MODE)
{
announceChannelName(voicePromptWasPlaying);
}
else
{
announceVFOAndFrequency(announceVFOName);
}
}
void announceItemWithInit(bool init, voicePromptItem_t item, audioPromptThreshold_t immediateAnnounceThreshold)
{
if (nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD)
{
return;
}
bool voicePromptWasPlaying = voicePromptsIsPlaying();
if (init)
{
voicePromptsInit();
}
switch(item)
{
case PROMPT_SEQUENCE_CHANNEL_NAME_OR_VFO_FREQ:
case PROMPT_SEQUENCE_CHANNEL_NAME_OR_VFO_FREQ_AND_MODE:
case PROMPT_SEQUENCE_CHANNEL_NAME_AND_CONTACT_OR_VFO_FREQ_AND_MODE:
case PROMPT_SEQUENCE_ZONE_NAME_CHANNEL_NAME_AND_CONTACT_OR_VFO_FREQ_AND_MODE:
case PROMPT_SEQUENCE_ZONE_NAME_CHANNEL_NAME_AND_CONTACT_OR_VFO_FREQ_AND_MODE_AND_TS_AND_CC:
case PROMPT_SEQUENCE_CHANNEL_NAME_AND_CONTACT_OR_VFO_FREQ_AND_MODE_AND_TS_AND_CC:
case PROMPT_SEQUENCE_VFO_FREQ_UPDATE:
{
uint32_t lFreq, hFreq;
if (((nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_LEVEL_2) || (immediateAnnounceThreshold == PROMPT_THRESHOLD_NEVER_PLAY_IMMEDIATELY)) &&
((item == PROMPT_SEQUENCE_ZONE_NAME_CHANNEL_NAME_AND_CONTACT_OR_VFO_FREQ_AND_MODE) ||
(item == PROMPT_SEQUENCE_ZONE_NAME_CHANNEL_NAME_AND_CONTACT_OR_VFO_FREQ_AND_MODE_AND_TS_AND_CC)))
{
announceZoneName(voicePromptWasPlaying);
announceDistance(voicePromptWasPlaying);
}
announceChannelNameOrVFOFrequency(voicePromptWasPlaying, (item != PROMPT_SEQUENCE_VFO_FREQ_UPDATE));
if (uiVFOModeFrequencyScanningIsActiveAndEnabled(&lFreq, &hFreq))
{
voicePromptsAppendPrompt(PROMPT_SCAN_MODE);
voicePromptsAppendLanguageString(currentLanguage->low);
announceQRG(lFreq, true);
voicePromptsAppendLanguageString(currentLanguage->high);
announceQRG(hFreq, true);
}
if (item == PROMPT_SEQUENCE_CHANNEL_NAME_OR_VFO_FREQ)
{
break;
}
else if (item == PROMPT_SEQUENCE_VFO_FREQ_UPDATE)
{
announceVFOChannelName();
}
announceRadioMode(voicePromptWasPlaying);
if (item == PROMPT_SEQUENCE_CHANNEL_NAME_OR_VFO_FREQ_AND_MODE)
{
break;
}
if (trxGetMode() == RADIO_MODE_DIGITAL)
{
if ((item == PROMPT_SEQUENCE_ZONE_NAME_CHANNEL_NAME_AND_CONTACT_OR_VFO_FREQ_AND_MODE_AND_TS_AND_CC) ||
(item == PROMPT_SEQUENCE_CHANNEL_NAME_AND_CONTACT_OR_VFO_FREQ_AND_MODE_AND_TS_AND_CC))
{
announceTS();
announceCC();
}
announceContactNameTgOrPc(false);// false = force the title "Contact" to be played to always separate the Channel name announcement from the Contact name
}
}
break;
case PROMPT_SEQUENCE_ZONE:
announceZoneName(voicePromptWasPlaying);
break;
case PROMPT_SEQUENCE_MODE:
announceRadioMode(voicePromptWasPlaying);
break;
case PROMPT_SEQUENCE_CONTACT_TG_OR_PC:
announceContactNameTgOrPc(voicePromptWasPlaying);
break;
case PROMPT_SEQUENCE_TS:
announceTS();
break;
case PROMPT_SEQUENCE_CC:
announceCC();
break;
case PROMPT_SEQUENCE_POWER:
announcePowerLevel(voicePromptWasPlaying);
break;
case PROMPT_SEQUENCE_BATTERY:
if (settingsIsOptionBitSet(BIT_BATTERY_VOLTAGE_IN_HEADER))
{
announceBatteryVoltage();
}
else
{
announceBatteryPercentage();
}
break;
case PROMPT_SQUENCE_SQUELCH:
announceSquelchLevel(voicePromptWasPlaying);
break;
case PROMPT_SEQUENCE_TEMPERATURE:
announceTemperature(voicePromptWasPlaying);
break;
case PROMPT_SEQUENCE_DIRECTION_TX:
voicePromptsAppendString("TX");
break;
case PROMPT_SEQUENCE_DIRECTION_RX:
voicePromptsAppendString("RX");
break;
default:
break;
}
// Follow-on when voicePromptWasPlaying is enabled on voice prompt level 2 and above
// Prompts are voiced immediately on voice prompt level 3
if ((voicePromptWasPlaying && (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_LEVEL_2)) ||
(nonVolatileSettings.audioPromptMode >= immediateAnnounceThreshold))
{
voicePromptsPlay();
}
}
void announceItem(voicePromptItem_t item, audioPromptThreshold_t immediateAnnounceThreshold)
{
announceItemWithInit(true, item, immediateAnnounceThreshold);
}
void promptsPlayNotAfterTx(void)
{
if (menuSystemGetPreviouslyPushedMenuNumber(false) != UI_TX_SCREEN)
{
voicePromptsPlay();
}
}
void uiUtilityBuildTgOrPCDisplayName(char *nameBuf, int bufferLen)
{
int contactIndex;
CodeplugContact_t contact;
uint32_t id = (trxTalkGroupOrPcId & 0x00FFFFFF);
int8_t manTS = tsGetManualOverrideFromCurrentChannel();
if ((trxTalkGroupOrPcId >> 24) == TG_CALL_FLAG)
{
contactIndex = codeplugContactIndexByTGorPC(id, CONTACT_CALLTYPE_TG, &contact, (manTS ? manTS : (trxGetDMRTimeSlot() + 1)));
if (contactIndex == -1)
{
if (id == ALL_CALL_VALUE)
{
snprintf(nameBuf, bufferLen, "%s", currentLanguage->all_call);
}
else
{
snprintf(nameBuf, bufferLen, "%s %u", currentLanguage->tg, (trxTalkGroupOrPcId & 0x00FFFFFF));
}
}
else
{
codeplugUtilConvertBufToString(contact.name, nameBuf, 16);
}
}
else
{
contactIndex = codeplugContactIndexByTGorPC(id, CONTACT_CALLTYPE_PC, &contact, (manTS ? manTS : (trxGetDMRTimeSlot() + 1)));
if (contactIndex == -1)
{
dmrIdDataStruct_t currentRec;
if (dmrIDLookup(id, &currentRec))
{
snprintf(nameBuf, bufferLen, currentRec.text);
}
else
{
// check LastHeard for TA data.
LinkItem_t *item = lastHeardFindInList(id);
if ((item != NULL) && (strlen(item->talkerAlias) != 0))
{
snprintf(nameBuf, bufferLen, item->talkerAlias);
}
else
{
if (id == ALL_CALL_VALUE)
{
snprintf(nameBuf, bufferLen, "%s", currentLanguage->all_call);
}
else
{
snprintf(nameBuf, bufferLen, "ID:%u", id);
}
}
}
}
else
{
codeplugUtilConvertBufToString(contact.name, nameBuf, 16);
}
}
}
void acceptPrivateCall(uint32_t id, int timeslot)
{
uiDataGlobal.PrivateCall.state = PRIVATE_CALL;
uiDataGlobal.PrivateCall.lastID = (id & 0xffffff);
uiDataGlobal.receivedPcId = 0x00;
setOverrideTGorPC(uiDataGlobal.PrivateCall.lastID, true);
#if !defined(PLATFORM_GD77S)
if (timeslot != trxGetDMRTimeSlot())
{
trxSetDMRTimeSlot(timeslot, true);
tsSetManualOverride(((menuSystemGetRootMenuNumber() == UI_CHANNEL_MODE) ? CHANNEL_CHANNEL : (CHANNEL_VFO_A + nonVolatileSettings.currentVFONumber)), (timeslot + 1));
}
#else
UNUSED_PARAMETER(timeslot);
#endif
announceItem(PROMPT_SEQUENCE_CONTACT_TG_OR_PC, PROMPT_THRESHOLD_3);
}
bool rebuildVoicePromptOnExtraLongSK1(uiEvent_t *ev)
{
if (BUTTONCHECK_EXTRALONGDOWN(ev, BUTTON_SK1) && (BUTTONCHECK_DOWN(ev, BUTTON_SK2) == 0) && (BUTTONCHECK_SHORTUP(ev, BUTTON_SK2) == 0) && (ev->keys.key == 0))
{
// SK2 is still held down, but SK1 just get released (reverseRepeater in VFO only, as now the one in Channel mode doesn't use the SK1+SK2 anymore).
if (uiDataGlobal.reverseRepeaterVFO)
{
return false;
}
if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD)
{
int currentMenu = menuSystemGetCurrentMenuNumber();
if ((((currentMenu == UI_CHANNEL_MODE) && (uiDataGlobal.Scan.active && (uiDataGlobal.Scan.state != SCAN_STATE_PAUSED))) ||
((currentMenu == UI_VFO_MODE) && ((uiDataGlobal.Scan.active && (uiDataGlobal.Scan.state != SCAN_STATE_PAUSED)) || uiDataGlobal.Scan.toneActive))) == false)
{
if (voicePromptsIsPlaying())
{
voicePromptsTerminate();
}
else
{
announceItem(((currentMenu == UI_VFO_MODE) ?
PROMPT_SEQUENCE_CHANNEL_NAME_AND_CONTACT_OR_VFO_FREQ_AND_MODE_AND_TS_AND_CC : PROMPT_SEQUENCE_ZONE_NAME_CHANNEL_NAME_AND_CONTACT_OR_VFO_FREQ_AND_MODE_AND_TS_AND_CC),
PROMPT_THRESHOLD_NEVER_PLAY_IMMEDIATELY);
if (trxGetMode() == RADIO_MODE_ANALOG)
{
CodeplugCSSTypes_t type = codeplugGetCSSType(currentChannelData->rxTone);
if ((type & CSS_TYPE_NONE) == 0)
{
buildCSSCodeVoicePrompts(currentChannelData->rxTone, type, DIRECTION_RECEIVE, true);
voicePromptsAppendPrompt(PROMPT_SILENCE);
}
type = codeplugGetCSSType(currentChannelData->txTone);
if ((type & CSS_TYPE_NONE) == 0)
{
buildCSSCodeVoicePrompts(currentChannelData->txTone, type, DIRECTION_TRANSMIT, true);
voicePromptsAppendPrompt(PROMPT_SILENCE);
}
}
announceItemWithInit(false, PROMPT_SEQUENCE_POWER, PROMPT_THRESHOLD_NEVER_PLAY_IMMEDIATELY);
if (currentMenu == UI_VFO_MODE)
{
announceItemWithInit(false, (uiVFOModeIsTXFocused() ? PROMPT_SEQUENCE_DIRECTION_TX : PROMPT_SEQUENCE_DIRECTION_RX), PROMPT_THRESHOLD_NEVER_PLAY_IMMEDIATELY);
}
voicePromptsPlay();
}
return true;
}
}
}
return false;
}
bool repeatVoicePromptOnSK1(uiEvent_t *ev)
{
if (BUTTONCHECK_SHORTUP(ev, BUTTON_SK1) && (BUTTONCHECK_DOWN(ev, BUTTON_SK2) == 0) && (ev->keys.key == 0))
{
if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD)
{
int currentMenu = menuSystemGetCurrentMenuNumber();
if ((((currentMenu == UI_CHANNEL_MODE) && (uiDataGlobal.Scan.active && (uiDataGlobal.Scan.state != SCAN_STATE_PAUSED))) ||
((currentMenu == UI_VFO_MODE) && ((uiDataGlobal.Scan.active && (uiDataGlobal.Scan.state != SCAN_STATE_PAUSED)) || uiDataGlobal.Scan.toneActive))) == false)
{
if (!voicePromptsIsPlaying())
{
// The following updates the VP buffer, in VFO mode only, and if frequency scanning mode is active
if (uiVFOModeFrequencyScanningIsActiveAndEnabled(NULL, NULL) && (voicePromptsDoesItContainPrompt(PROMPT_SCAN_MODE) == false))
{
voicePromptsAppendPrompt(PROMPT_SCAN_MODE);
}
voicePromptsPlay();
}
else
{
voicePromptsTerminate();
}
}
return true;
}
}
return false;
}
bool handleMonitorMode(uiEvent_t *ev)
{
// Time by which a DMR signal should have been decoded, including locking on to a DMR signal on a different CC
const int DMR_MODE_CC_DETECT_TIME_MS = 375;// WAS:250;// Normally it seems to take about 125mS to detect DMR even if the CC is incorrect.
if (monitorModeData.isEnabled)
{
#if defined(PLATFORM_GD77S)
if ((BUTTONCHECK_DOWN(ev, BUTTON_SK2) == 0) || (BUTTONCHECK_DOWN(ev, BUTTON_SK1) == 0) || BUTTONCHECK_DOWN(ev, BUTTON_ORANGE) || (ev->events & ROTARY_EVENT))
#else
if ((BUTTONCHECK_UNIQUE_DOWN(ev, BUTTON_SK2) && (ev->keys.key == 0)) == false)
#endif
{
monitorModeData.isEnabled = false;
// Blindly set mode and BW, as maybe only FM BW has changed (MonitorMode ends to FM 25kHz if no DMR is detected)
// Anyway, trxSetModeAndBandwidth() won't do anything if the current mode and BW are identical,
trxSetModeAndBandwidth(currentChannelData->chMode, (codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_BW_25K) != 0));
currentChannelData->sql = monitorModeData.savedSquelch;
nonVolatileSettings.dmrCcTsFilter = monitorModeData.savedDMRCcTsFilter;
nonVolatileSettings.dmrDestinationFilter = monitorModeData.savedDMRDestinationFilter;
switch (monitorModeData.savedRadioMode)
{
case RADIO_MODE_ANALOG:
trxSetRxCSS(RADIO_DEVICE_PRIMARY, currentChannelData->rxTone);
break;
case RADIO_MODE_DIGITAL:
trxSetDMRColourCode(monitorModeData.savedDMRCc);
trxSetDMRTimeSlot(monitorModeData.savedDMRTs, true);
HRC6000InitDigitalDmrRx();
audioAmpDisable(AUDIO_AMP_CHANNEL_RF);
break;
default: // RADIO_MODE_NONE
break;
}
headerRowIsDirty = true;
if (uiVFOModeSweepScanning(true))
{
uiVFOSweepScanModePause(false, false);
}
else
{
uiDataGlobal.displayQSOState = QSO_DISPLAY_DEFAULT_SCREEN;
(void)addTimerCallback(uiUtilityRenderQSODataAndUpdateScreen, 2000, menuSystemGetCurrentMenuNumber(), true);
}
return true;
}
}
else
{
#if defined(PLATFORM_GD77S)
if (BUTTONCHECK_LONGDOWN(ev, BUTTON_SK1) && BUTTONCHECK_LONGDOWN(ev, BUTTON_SK2)
#else
if ((ev->keys.key == 0) && (ev->buttons == (BUTTON_SK2_EXTRA_LONG_DOWN | BUTTON_SK2))
#endif
&& (trxGetMode() != RADIO_MODE_NONE)
&& (((uiDataGlobal.Scan.toneActive == false) &&
((uiDataGlobal.Scan.active == false) || (uiDataGlobal.Scan.active && (uiDataGlobal.Scan.state == SCAN_STATE_PAUSED))))
|| uiVFOModeSweepScanning(false))
)
{
if (voicePromptsIsPlaying())
{
voicePromptsTerminate();
}
if (uiVFOModeSweepScanning(false))
{
uiVFOSweepScanModePause(true, true);
}
monitorModeData.savedRadioMode = trxGetMode();
monitorModeData.savedSquelch = currentChannelData->sql;
monitorModeData.savedDMRCcTsFilter = nonVolatileSettings.dmrCcTsFilter;
monitorModeData.savedDMRDestinationFilter = nonVolatileSettings.dmrDestinationFilter;
monitorModeData.savedDMRCc = trxGetDMRColourCode();
monitorModeData.savedDMRTs = trxGetDMRTimeSlot();
// Temporary override DMR filtering
nonVolatileSettings.dmrCcTsFilter = DMR_CCTS_FILTER_NONE;
nonVolatileSettings.dmrDestinationFilter = DMR_DESTINATION_FILTER_NONE;
monitorModeData.dmrTimeout = DMR_MODE_CC_DETECT_TIME_MS;
monitorModeData.dmrIsValid = false;
monitorModeData.qsoInfoUpdated = true;
monitorModeData.triggered = true;
monitorModeData.isEnabled = true;
// Start with DMR autodetection
if (monitorModeData.savedRadioMode != RADIO_MODE_DIGITAL)
{
trxSetModeAndBandwidth(RADIO_MODE_DIGITAL, false);
// current Channels|VFO mode has reverseRepeater set, handle this.
if ((uiDataGlobal.reverseRepeaterChannel && (menuSystemGetCurrentMenuNumber() == UI_CHANNEL_MODE)) ||
(uiDataGlobal.reverseRepeaterVFO && (menuSystemGetCurrentMenuNumber() == UI_VFO_MODE)))
{
uint32_t rxFreq = currentChannelData->txFreq;
uint32_t txFreq = (uiDataGlobal.talkaround ? rxFreq : currentChannelData->rxFreq);
trxSetFrequency(rxFreq, txFreq, DMR_MODE_DMO);
}
}
trxSetDMRColourCode(0);
trxSetDMRTimeSlot(0, true);
HRC6000ClearActiveDMRID();
HRC6000ClearColorCodeSynchronisation();
lastHeardClearLastID();
HRC6000InitDigitalDmrRx();
audioAmpDisable(AUDIO_AMP_CHANNEL_RF);
headerRowIsDirty = true;
return true;
}
}
return false;
}
// Helper function that manages the returned value from the codeplug quickkey code
static bool setQuickkeyFunctionID(char key, uint16_t functionId, bool silent)
{
if (
#if defined(PLATFORM_RD5R)
// '5' is reserved for torch on RD-5R
(key != '5') &&
#endif
codeplugSetQuickkeyFunctionID(key, functionId))
{
if (silent == false)
{
nextKeyBeepMelody = (int16_t *)MELODY_ACK_BEEP;
}
return true;
}
nextKeyBeepMelody = (int16_t *)MELODY_ERROR_BEEP;
return false;
}
void saveQuickkeyMenuIndex(char key, uint8_t menuId, uint8_t entryId, uint8_t function)
{
uint16_t functionID;
functionID = QUICKKEY_MENUVALUE(menuId, entryId, function);
if (setQuickkeyFunctionID(key, functionID, false))
{
menuDataGlobal.menuOptionsTimeout = -1;// Flag to indicate that a QuickKey has just been set.
}
}
void saveQuickkeyMenuLongValue(char key, uint8_t menuId, uint16_t entryId)
{
uint16_t functionID;
functionID = QUICKKEY_MENULONGVALUE(menuId, entryId);
setQuickkeyFunctionID(key, functionID, ((menuId == 0) && (entryId == 0)));
}
void saveQuickkeyContactIndex(char key, uint16_t contactId)
{
setQuickkeyFunctionID(key, QUICKKEY_CONTACTVALUE(contactId), false);
}
// Returns the index in either the CTCSS or DCS list of the tone (or closest match)
uint8_t cssGetToneIndex(uint16_t tone, CodeplugCSSTypes_t type)
{
uint16_t *start = NULL;
uint16_t *end = NULL;
if (type & CSS_TYPE_CTCSS)
{
start = (uint16_t *)TRX_CTCSSTones;
end = start + (TRX_NUM_CTCSS - 1);
}
else if (type & CSS_TYPE_DCS)
{
tone &= ~CSS_TYPE_DCS_MASK;
start = (uint16_t *)TRX_DCSCodes;
end = start + (TRX_NUM_DCS - 1);
}
if (start && end)
{
uint16_t *p = start;
while((p <= end) && (*p < tone))
{
p++;
}
if (p <= end)
{
return (p - start);
}
}
return 0U;
}
uint16_t cssGetToneFromIndex(uint8_t index, CodeplugCSSTypes_t type)
{
if (type & CSS_TYPE_CTCSS)
{
if (index >= TRX_NUM_CTCSS)
{
index = 0;
}
return TRX_CTCSSTones[index];
}
else if (type & CSS_TYPE_DCS)
{
if (index >= TRX_NUM_DCS)
{
index = 0;
}
return (TRX_DCSCodes[index] | type);
}
return CODEPLUG_CSS_TONE_NONE;
}
void cssIncrement(uint16_t *tone, uint8_t *index, uint8_t step, CodeplugCSSTypes_t *type, bool loop, bool stayInCSSType)
{
*index += step;
if (*type & CSS_TYPE_CTCSS)
{
if (*index >= TRX_NUM_CTCSS)
{
if (stayInCSSType)
{
*index = 0;
}
else
{
*tone = cssGetToneFromIndex((*index = 0), (*type = CSS_TYPE_DCS));
return;
}
}
*tone = TRX_CTCSSTones[*index];
}
else if (*type & CSS_TYPE_DCS)
{
if (*index >= TRX_NUM_DCS)
{
if (stayInCSSType)
{
*index = 0;
}
else
{
if (*type & CSS_TYPE_DCS_INVERTED)
{
if (loop)
{
*tone = cssGetToneFromIndex((*index = 0), (*type = CSS_TYPE_CTCSS));
return;
}
// We are at the end of whole list
*index = TRX_NUM_DCS - 1;
}
else
{
*tone = cssGetToneFromIndex((*index = 0), (*type = (CSS_TYPE_DCS | CSS_TYPE_DCS_INVERTED)));
return;
}
}
}
*tone = TRX_DCSCodes[*index] | *type;
}
else if (*type & CSS_TYPE_NONE)
{
*tone = cssGetToneFromIndex((*index = 0), (*type = CSS_TYPE_CTCSS));
}
}
void cssDecrement(uint16_t *tone, uint8_t *index, uint8_t step, CodeplugCSSTypes_t *type, bool loop, bool stayInCSSType)
{
int16_t idx = *index - step;
if (*type & CSS_TYPE_CTCSS)
{
if (idx < 0)
{
if (stayInCSSType)
{
idx = TRX_NUM_CTCSS - 1;
}
else
{
if (loop)
{
*tone = cssGetToneFromIndex((*index = (TRX_NUM_DCS - 1)), (*type = (CSS_TYPE_DCS | CSS_TYPE_DCS_INVERTED)));
}
else
{
*tone = cssGetToneFromIndex((*index = 0), (*type = CSS_TYPE_NONE));
}
return;
}
}
*tone = TRX_CTCSSTones[(*index = (uint8_t)idx)];
}
else if (*type & CSS_TYPE_DCS)
{
if (idx < 0)
{
if (stayInCSSType)
{
idx = TRX_NUM_DCS - 1;
}
else
{
if (*type & CSS_TYPE_DCS_INVERTED)
{
*tone = cssGetToneFromIndex((*index = (TRX_NUM_DCS - 1)), (*type = CSS_TYPE_DCS));
}
else
{
*tone = cssGetToneFromIndex((*index = (TRX_NUM_CTCSS - 1)), (*type = CSS_TYPE_CTCSS));
}
return;
}
}
*tone = TRX_DCSCodes[(*index = (uint8_t)idx)] | *type;
}
else if (*type & CSS_TYPE_NONE)
{
if (loop)
{
*tone = cssGetToneFromIndex((*index = (TRX_NUM_DCS - 1)), (*type = (CSS_TYPE_DCS | CSS_TYPE_DCS_INVERTED)));
}
else
{
*tone = cssGetToneFromIndex((*index = 0), (*type = CSS_TYPE_NONE));
}
}
}
bool uiQuickKeysShowChoices(char *buf, const int bufferLen, const char *menuTitle)
{
bool settingOption = (menuDataGlobal.menuOptionsSetQuickkey != 0) || (menuDataGlobal.menuOptionsTimeout > 0);
if (menuDataGlobal.menuOptionsSetQuickkey != 0)
{
snprintf(buf, bufferLen, "%s %c", currentLanguage->set_quickkey, menuDataGlobal.menuOptionsSetQuickkey);
menuDisplayTitle(buf);
displayDrawChoice(CHOICES_OKARROWS, true);
if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD)
{
voicePromptsInit();
voicePromptsAppendLanguageString(currentLanguage->set_quickkey);
voicePromptsAppendPrompt(PROMPT_0 + (menuDataGlobal.menuOptionsSetQuickkey - '0'));
}
}
else if (settingOption == false)
{
menuDisplayTitle(menuTitle);
}
return settingOption;
}
bool uiQuickKeysIsStoring(uiEvent_t *ev)
{
return ((ev->events & KEY_EVENT) && (menuDataGlobal.menuOptionsSetQuickkey != 0) && (menuDataGlobal.menuOptionsTimeout == 0));
}
void uiQuickKeysStore(uiEvent_t *ev, menuStatus_t *status)
{
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
menuDataGlobal.menuOptionsSetQuickkey = 0;
menuDataGlobal.menuOptionsTimeout = 0;
*status |= MENU_STATUS_ERROR;
}
else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
{
saveQuickkeyMenuIndex(menuDataGlobal.menuOptionsSetQuickkey, menuSystemGetCurrentMenuNumber(), menuDataGlobal.currentItemIndex, 0);
menuDataGlobal.menuOptionsSetQuickkey = 0;
}
else if (KEYCHECK_SHORTUP(ev->keys, KEY_LEFT))
{
saveQuickkeyMenuIndex(menuDataGlobal.menuOptionsSetQuickkey, menuSystemGetCurrentMenuNumber(), menuDataGlobal.currentItemIndex, FUNC_LEFT);
menuDataGlobal.menuOptionsSetQuickkey = 0;
}
else if (KEYCHECK_SHORTUP(ev->keys, KEY_RIGHT))
{
saveQuickkeyMenuIndex(menuDataGlobal.menuOptionsSetQuickkey, menuSystemGetCurrentMenuNumber(), menuDataGlobal.currentItemIndex, FUNC_RIGHT);
menuDataGlobal.menuOptionsSetQuickkey = 0;
}
}
// --- DTMF contact list playback ---
static uint32_t dtmfGetToneDuration(uint32_t duration)
{
bool starOrHash = ((uiDataGlobal.DTMFContactList.buffer[uiDataGlobal.DTMFContactList.poPtr] == 14) || (uiDataGlobal.DTMFContactList.buffer[uiDataGlobal.DTMFContactList.poPtr] == 15));
/*
* https://www.sigidwiki.com/wiki/Dual_Tone_Multi_Frequency_(DTMF):
* Standard Whelen timing is 40ms tone, 20ms space, where standard Motorola rate is 250ms tone, 250ms space.
* Federal Signal ranges from 35ms tone 5ms space to 1000ms tone 1000ms space.
* Genave Superfast rate is 20ms tone 20ms space. Genave claims their decoders can even respond to 20ms tone 5ms space.
*
*
* ETSI: https://www.etsi.org/deliver/etsi_es/201200_201299/20123502/01.01.01_60/es_20123502v010101p.pdf
* 4.2.4 Signal timing
* 4.2.4.1 Tone duration
* Where the DTMF signalling tone duration is controlled automatically by the transmitter, the duration of any individual
* DTMF tone combination sent shall not be less than 65 ms. The time shall be measured from the time when the tone
* reaches 90 % of its steady-state value, until it has dropped to 90 % of its steady-state value.
*
* NOTE: For correct operation of supplementary services such as SCWID (Spontaneous Call Waiting
* Identification) and ADSI (Analogue Display Services Interface), DTMF tone bursts should not be longer
* than 90 ms.
*
* 4.2.4.2 Pause duration
* Where the DTMF signalling pause duration is controlled automatically by the transmitter the duration of the pause
* between any individual DTMF tone combination shall not be less than 65 ms. The time shall be measured from the time
* when the tone has dropped to 10 % of its steady-state value, until it has risen to 10 % of its steady-state value.
*
* NOTE: In order to ensure correct reception of all the digits in a network address sequence, some networks may
* require a sufficient pause after the last DTMF digit signalled and before normal transmission starts.
*/
// First digit
if ((uiDataGlobal.DTMFContactList.poPtr == 0) && (uiDataGlobal.DTMFContactList.durations.fstDur > 0))
{
/*
* First digit duration:
* - Example 1 "DTMF rate" is set to 10 digits per second (duration is 50 milliseconds).
* The first digit time is set to 100 milliseconds. Thus, the actual length of the first digit duration is 150 milliseconds.
* However, if the launch starts with a "*" or "#" tone, the intercom will compare the duration with "* and #" and whichever
* is longer for both.
* - Example 2 "DTMF rate" is set to 10 digits per second (duration is 50 milliseconds).
* The first digit time is set to 100 milliseconds. "* And # tone" is set to 500 milliseconds.
* Thus, the actual length of the first "*" or "#" tone is 550 milliseconds.
*/
return ((starOrHash ? (uiDataGlobal.DTMFContactList.durations.otherDur * 10) : (uiDataGlobal.DTMFContactList.durations.fstDur * 10)) + duration);
}
/*
* '*' '#' Duration:
* - Example 1 "DTMF rate" is set to 10 digits per second (duration is 50 milliseconds).
* "* And # tone" is set to 500 milliseconds. Thus, the actual length of "* and # sounds" is 550 milliseconds.
* However, if the launch starts with * and # sounds, the intercom compares the duration of the pitch with
* the "first digit time" and uses the longer one of the two.
* - Example 2 "DTMF rate" is set to 10 digits per second (duration is 50 milliseconds).
* The first digit time is set to 100 milliseconds. "* And # tone" is set to 500 milliseconds.
* Therefore, the actual number of the first digit * or # is 550 milliseconds.
*/
return ((starOrHash ? (uiDataGlobal.DTMFContactList.durations.otherDur * 10) : 0) + duration);
}
static void dtmfStopLocalTone(bool enableMic)
{
#if defined(PLATFORM_GD77) || defined(PLATFORM_GD77S) || defined(PLATFORM_DM1801) || defined(PLATFORM_DM1801A) || defined(PLATFORM_RD5R)
trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_NONE);
#else
trxDTMFoff(enableMic);
if(soundMelodyIsPlaying())
{
soundStopMelody();
}
#endif
}
static void dtmfProcess(void)
{
if (uiDataGlobal.DTMFContactList.poLen == 0U)
{
return;
}
if (ticksTimerHasExpired(&uiDataGlobal.DTMFContactList.nextPeriodTimer))
{
uint32_t duration = (1000 / (uiDataGlobal.DTMFContactList.durations.rate * 2));
if (uiDataGlobal.DTMFContactList.buffer[uiDataGlobal.DTMFContactList.poPtr] != 0xFFU)
{
// Set voice channel (and tone), accordingly to the next inTone state
if (uiDataGlobal.DTMFContactList.inTone == false)
{
trxSetDTMF(uiDataGlobal.DTMFContactList.buffer[uiDataGlobal.DTMFContactList.poPtr]);
#if defined(PLATFORM_GD77) || defined(PLATFORM_GD77S) || defined(PLATFORM_DM1801) || defined(PLATFORM_DM1801A) || defined(PLATFORM_RD5R)
trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_DTMF);
#else
soundSetMelody(MELODY_DTMF);
#endif
}
else
{
dtmfStopLocalTone(false);
}
uiDataGlobal.DTMFContactList.inTone = !uiDataGlobal.DTMFContactList.inTone;
}
else
{
// Pause after last digit
if (uiDataGlobal.DTMFContactList.inTone)
{
uiDataGlobal.DTMFContactList.inTone = false;
dtmfStopLocalTone(true);
ticksTimerStart(&uiDataGlobal.DTMFContactList.nextPeriodTimer, (duration + (uiDataGlobal.DTMFContactList.durations.libreDMR_Tail * 100)));
return;
}
}
if (uiDataGlobal.DTMFContactList.inTone)
{
// Move forward in the sequence, set tone duration
ticksTimerStart(&uiDataGlobal.DTMFContactList.nextPeriodTimer, dtmfGetToneDuration(duration));
uiDataGlobal.DTMFContactList.poPtr++;
}
else
{
// No next character, last iteration pause has already been processed.
// Move the pointer (offset) beyond the end of the sequence (handled in the next statement)
if (uiDataGlobal.DTMFContactList.buffer[uiDataGlobal.DTMFContactList.poPtr] == 0xFFU)
{
uiDataGlobal.DTMFContactList.poPtr++;
}
else
{
// Set pause time in-between tone duration
ticksTimerStart(&uiDataGlobal.DTMFContactList.nextPeriodTimer, duration);
}
}
if (uiDataGlobal.DTMFContactList.poPtr > uiDataGlobal.DTMFContactList.poLen)
{
uiDataGlobal.DTMFContactList.poPtr = 0U;
uiDataGlobal.DTMFContactList.poLen = 0U;
}
}
}
void dtmfSequenceReset(void)
{
uiDataGlobal.DTMFContactList.poLen = 0U;
uiDataGlobal.DTMFContactList.poPtr = 0U;
uiDataGlobal.DTMFContactList.isKeying = false;
}
bool dtmfSequenceIsKeying(void)
{
return uiDataGlobal.DTMFContactList.isKeying;
}
void dtmfSequencePrepare(uint8_t *seq, bool autoStart)
{
uint8_t len = 16U;
dtmfSequenceReset();
memcpy(uiDataGlobal.DTMFContactList.buffer, seq, 16);
uiDataGlobal.DTMFContactList.buffer[16] = 0xFFU;
// non empty
if (uiDataGlobal.DTMFContactList.buffer[0] != 0xFFU)
{
// Find the sequence length
for (uint8_t i = 0; i < 16; i++)
{
if (uiDataGlobal.DTMFContactList.buffer[i] == 0xFFU)
{
len = i;
break;
}
}
uiDataGlobal.DTMFContactList.poLen = len;
uiDataGlobal.DTMFContactList.isKeying = (autoStart ? (len > 0) : false);
}
}
void dtmfSequenceStart(void)
{
if (uiDataGlobal.DTMFContactList.isKeying == false)
{
uiDataGlobal.DTMFContactList.isKeying = (uiDataGlobal.DTMFContactList.poLen > 0);
}
}
void dtmfSequenceStop(void)
{
uiDataGlobal.DTMFContactList.poLen = 0U;
dtmfStopLocalTone(true);
}
void dtmfSequenceTick(bool popPreviousMenuOnEnding)
{
if (uiDataGlobal.DTMFContactList.isKeying)
{
if (!trxTransmissionEnabled)
{
if (xmitErrorTimer > 0)
{
// Wait the voice ends, then count-down 200ms;
if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD)
{
if (voicePromptsIsPlaying())
{
xmitErrorTimer = (20 * 10U);
return;
}
}
xmitErrorTimer--;
if (xmitErrorTimer == 0)
{
dtmfSequenceReset();
menuSystemPopAllAndDisplayRootMenu();
}
return;
}
rxPowerSavingSetState(ECOPHASE_POWERSAVE_INACTIVE);
if ((codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_RX_ONLY) == 0) && ((nonVolatileSettings.txFreqLimited == BAND_LIMITS_NONE) || trxCheckFrequencyInAmateurBand(currentChannelData->txFreq)))
{
// Start TX DTMF, prepare for ANALOG
if (trxGetMode() != RADIO_MODE_ANALOG)
{
trxSetModeAndBandwidth(RADIO_MODE_ANALOG, (codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_BW_25K) != 0));
}
// Make sure Tx freq is updated before transmission is enabled
// Maybe the satellite menu was entered, but we can't use the GetPreviousMenu()
// as this one is a sub-sub menu.
trxSetFrequency(currentChannelData->rxFreq, currentChannelData->txFreq, DMR_MODE_AUTO);
//
trxSetTxCSS(currentChannelData->txTone);
trxEnableTransmission();
#if defined(PLATFORM_GD77) || defined(PLATFORM_GD77S) || defined(PLATFORM_DM1801) || defined(PLATFORM_DM1801A) || defined(PLATFORM_RD5R)
trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_NONE);
audioAmpEnable(AUDIO_AMP_CHANNEL_RF);
GPIO_PinWrite(GPIO_RX_audio_mux, Pin_RX_audio_mux, 1);
#endif
uiDataGlobal.DTMFContactList.inTone = false;
ticksTimerStart(&uiDataGlobal.DTMFContactList.nextPeriodTimer, (uiDataGlobal.DTMFContactList.durations.fstDigitDly * 100)); // Sequence preamble
}
else
{
uiEvent_t ev = { .buttons = 0, .keys = NO_KEYCODE, .rotary = 0, .function = 0, .events = NO_EVENT, .hasEvent = false, .time = 0 };
menuTxScreenHandleTxTermination(&ev, ((codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_RX_ONLY) != 0) ? TXSTOP_RX_ONLY : TXSTOP_OUT_OF_BAND));
}
}
// DTMF has been TXed, restore DIGITAL/ANALOG
if (uiDataGlobal.DTMFContactList.poLen == 0U)
{
trxDisableTransmission();
if (trxTransmissionEnabled)
{
// Stop TXing;
trxTransmissionEnabled = false;
trxSetRX();
LedWrite(LED_GREEN, 0);
#if defined(PLATFORM_GD77) || defined(PLATFORM_GD77S) || defined(PLATFORM_DM1801) || defined(PLATFORM_DM1801A) || defined(PLATFORM_RD5R)
trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_MIC);
audioAmpDisable(AUDIO_AMP_CHANNEL_RF);
#endif
if (currentChannelData->chMode == RADIO_MODE_ANALOG)
{
trxSetModeAndBandwidth(currentChannelData->chMode, (codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_BW_25K) != 0));
trxSetTxCSS(currentChannelData->txTone);
trxSetRxCSS(RADIO_DEVICE_PRIMARY, currentChannelData->rxTone);
}
else
{
trxSetModeAndBandwidth(currentChannelData->chMode, false);// bandwidth false = 12.5Khz as DMR uses 12.5kHz
trxSetDMRColourCode(currentChannelData->txColor);
}
}
uiDataGlobal.DTMFContactList.isKeying = false;
if (popPreviousMenuOnEnding)
{
menuSystemPopPreviousMenu();
}
return;
}
if (uiDataGlobal.DTMFContactList.poLen > 0U)
{
dtmfProcess();
}
}
}
void resetOriginalSettingsData(void)
{
static const uint32_t unsetValue = 0xDEADBEEF;
originalNonVolatileSettings.magicNumber = unsetValue;
#if !defined(PLATFORM_GD77S)
memcpy(((uint32_t *)&aprsSettingsCopy.smart.slowRate), &unsetValue, sizeof(uint32_t));
#endif
}
void showErrorMessage(const char *message)
{
displayClearBuf();
displayThemeApply(THEME_ITEM_FG_ERROR_NOTIFICATION, THEME_ITEM_BG);
displayPrintCentered(((DISPLAY_SIZE_Y - FONT_SIZE_3_HEIGHT) >> 1), message, FONT_SIZE_3);
displayThemeResetToDefault();
displayRender();
}
const char *getPowerLevel(uint8_t level)
{
#if defined(PLATFORM_MDUV380) && !defined(PLATFORM_VARIANT_UV380_PLUS_10W)
return POWER_LEVELS[(settingsIsOptionBitSet(BIT_FORCE_10W_RADIO) ? 1U : 0U)][level];
#else
return POWER_LEVELS[level];
#endif
}
const char *getPowerLevelUnit(uint8_t level)
{
return POWER_LEVEL_UNITS[level];
}
void uiSetUTCDateTimeInSecs(time_t_custom UTCdateTimeInSecs)
{
uiDataGlobal.dateTimeSecs = UTCdateTimeInSecs;
#if ! defined(PLATFORM_GD77S)
daytimeThemeChangeUpdate(true);
#endif
}
#ifdef USE_RTC
// Sun = 0.
uint8_t getDayOfTheWeek(uint8_t day, uint8_t month, uint8_t year)
{
return ((day += (month < 3 ? year-- : (year - 2))), (23 * month / 9 + day + 4 + year / 4 - year / 100 + year / 400)) % 7;
}
time_t getEpochTime(RTC_TimeTypeDef *rtcTime, RTC_DateTypeDef *rtcDate)
{
uint8_t hh = rtcTime->Hours;
uint8_t mm = rtcTime->Minutes;
uint8_t ss = rtcTime->Seconds;
uint8_t d = rtcDate->Date;
uint8_t m = rtcDate->Month;
uint16_t y = rtcDate->Year;
uint16_t yr = (uint16_t)(y + (2000 - 1900));
struct tm tim = { 0 };
tim.tm_year = yr;
tim.tm_mon = m - 1;
tim.tm_mday = d;
tim.tm_hour = hh;
tim.tm_min = mm;
tim.tm_sec = ss;
return mktime(&tim);
}
#endif
/*
* gmtime_r.c
* Original Author: Adapted from tzcode maintained by Arthur David Olson.
* Modifications:
* - Changed to mktm_r and added __tzcalc_limits - 04/10/02, Jeff Johnston
* - Fixed bug in mday computations - 08/12/04, Alex Mogilnikov <alx@intellectronika.ru>
* - Fixed bug in __tzcalc_limits - 08/12/04, Alex Mogilnikov <alx@intellectronika.ru>
* - Move code from _mktm_r() to gmtime_r() - 05/09/14, Freddie Chopin <freddie_chopin@op.pl>
* - Fixed bug in calculations for dates after year 2069 or before year 1901. Ideas for
* solution taken from musl's __secs_to_tm() - 07/12/2014, Freddie Chopin
* <freddie_chopin@op.pl>
* - Use faster algorithm from civil_from_days() by Howard Hinnant - 12/06/2014,
* Freddie Chopin <freddie_chopin@op.pl>
*
* Converts the calendar time pointed to by tim_p into a broken-down time
* expressed as local time. Returns a pointer to a structure containing the
* broken-down time.
*/
/* Move epoch from 01.01.1970 to 01.03.0000 (yes, Year 0) - this is the first
* day of a 400-year long "era", right after additional day of leap year.
* This adjustment is required only for date calculation, so instead of
* modifying time_t value (which would require 64-bit operations to work
* correctly) it's enough to adjust the calculated number of days since epoch.
*/
#define EPOCH_ADJUSTMENT_DAYS 719468L
/* year to which the adjustment was made */
#define ADJUSTED_EPOCH_YEAR 0
/* 1st March of year 0 is Wednesday */
#define ADJUSTED_EPOCH_WDAY 3
/* there are 97 leap years in 400-year periods. ((400 - 97) * 365 + 97 * 366) */
#define DAYS_PER_ERA 146097L
/* there are 24 leap years in 100-year periods. ((100 - 24) * 365 + 24 * 366) */
#define DAYS_PER_CENTURY 36524L
/* there is one leap year every 4 years */
#define DAYS_PER_4_YEARS (3 * 365 + 366)
/* number of days in a non-leap year */
#define DAYS_PER_YEAR 365
/* number of days in January */
#define DAYS_IN_JANUARY 31
/* number of days in non-leap February */
#define DAYS_IN_FEBRUARY 28
/* number of years per era */
#define YEARS_PER_ERA 400
#define SECSPERMIN 60L
#define MINSPERHOUR 60L
#define HOURSPERDAY 24L
#define SECSPERHOUR (SECSPERMIN * MINSPERHOUR)
#define SECSPERDAY (SECSPERHOUR * HOURSPERDAY)
#define DAYSPERWEEK 7
#define MONSPERYEAR 12
#define YEAR_BASE 1900
#define EPOCH_YEAR 1970
#define EPOCH_WDAY 4
#define EPOCH_YEARS_SINCE_LEAP 2
#define EPOCH_YEARS_SINCE_CENTURY 70
#define EPOCH_YEARS_SINCE_LEAP_CENTURY 370
#define isleap(y) ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0)
struct tm *gmtime_r_Custom(const time_t_custom *__restrict tim_p, struct tm *__restrict res)
{
long days, rem;
const time_t_custom lcltime = *tim_p;
int era, weekday, year;
unsigned erayear, yearday, month, day;
unsigned long eraday;
days = lcltime / SECSPERDAY + EPOCH_ADJUSTMENT_DAYS;
rem = lcltime % SECSPERDAY;
if (rem < 0)
{
rem += SECSPERDAY;
--days;
}
/* compute hour, min, and sec */
res->tm_hour = (int) (rem / SECSPERHOUR);
rem %= SECSPERHOUR;
res->tm_min = (int) (rem / SECSPERMIN);
res->tm_sec = (int) (rem % SECSPERMIN);
/* compute day of week */
if ((weekday = ((ADJUSTED_EPOCH_WDAY + days) % DAYSPERWEEK)) < 0)
weekday += DAYSPERWEEK;
res->tm_wday = weekday;
/* compute year, month, day & day of year */
/* for description of this algorithm see
* http://howardhinnant.github.io/date_algorithms.html#civil_from_days */
era = (days >= 0 ? days : days - (DAYS_PER_ERA - 1)) / DAYS_PER_ERA;
eraday = days - era * DAYS_PER_ERA; /* [0, 146096] */
erayear = (eraday - eraday / (DAYS_PER_4_YEARS - 1) + eraday / DAYS_PER_CENTURY -
eraday / (DAYS_PER_ERA - 1)) / 365; /* [0, 399] */
yearday = eraday - (DAYS_PER_YEAR * erayear + erayear / 4 - erayear / 100); /* [0, 365] */
month = (5 * yearday + 2) / 153; /* [0, 11] */
day = yearday - (153 * month + 2) / 5 + 1; /* [1, 31] */
month += month < 10 ? 2 : -10;
year = ADJUSTED_EPOCH_YEAR + erayear + era * YEARS_PER_ERA + (month <= 1);
res->tm_yday = yearday >= DAYS_PER_YEAR - DAYS_IN_JANUARY - DAYS_IN_FEBRUARY ?
yearday - (DAYS_PER_YEAR - DAYS_IN_JANUARY - DAYS_IN_FEBRUARY) :
yearday + DAYS_IN_JANUARY + DAYS_IN_FEBRUARY + isleap(erayear);
res->tm_year = year - YEAR_BASE;
res->tm_mon = month;
res->tm_mday = day;
res->tm_isdst = 0;
return (res);
}
time_t_custom mktime_custom(const struct tm * tb)
{
const int totalDays[] = { -1, 30, 58, 89, 119, 150, 180, 211, 242, 272, 303, 333, 364 };
time_t_custom t1;
int days;
days = totalDays[tb->tm_mon];
if (!(tb->tm_year & 3) && (tb->tm_mon > 1))
{
days++;
}
t1 = (tb->tm_year - (EPOCH_YEAR - 1900)) * DAYS_PER_YEAR + ((tb->tm_year - 1L) / 4) - 17 + days + tb->tm_mday;
t1 = (t1 * HOURSPERDAY) + tb->tm_hour;
t1 = (t1 * MINSPERHOUR) + tb->tm_min;
t1 = (t1 * SECSPERMIN) + tb->tm_sec;
return (time_t_custom) t1;
}
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
time_t_custom getRtcTime_custom(void)
{
RTC_TimeTypeDef rtcTime = { 0 };
RTC_DateTypeDef rtcDate = { 0 };
struct tm RTCDateTime;
HAL_RTC_GetTime(&hrtc, &rtcTime, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &rtcDate, RTC_FORMAT_BIN);
memset(&RTCDateTime,0x00,sizeof(struct tm)); // clear entire struct
RTCDateTime.tm_mday = rtcDate.Date; /* day of the month, 1 to 31 */
RTCDateTime.tm_mon = rtcDate.Month - 1; /* months since January, 0 to 11 */
RTCDateTime.tm_year = rtcDate.Year; /* years since 1900 */
RTCDateTime.tm_hour = rtcTime.Hours;
RTCDateTime.tm_min = rtcTime.Minutes;
RTCDateTime.tm_sec = rtcTime.Seconds;
return mktime_custom(&RTCDateTime);
}
void setRtc_custom(time_t_custom tc)
{
RTC_TimeTypeDef rtcTime = { 0 };
RTC_DateTypeDef rtcDate = { 0 };
struct tm RTCDateTime;
gmtime_r_Custom(&tc, &RTCDateTime);
rtcDate.Date = RTCDateTime.tm_mday; /* day of the month, 1 to 31 */
rtcDate.Month = RTCDateTime.tm_mon + 1 ; /* months since January, 0 to 11 */
rtcDate.Year = RTCDateTime.tm_year ; /* years since 1900 */
rtcTime.Hours = RTCDateTime.tm_hour;
rtcTime.Minutes = RTCDateTime.tm_min;
rtcTime.Seconds = RTCDateTime.tm_sec ;
rtcTime.TimeFormat = RTC_HOURFORMAT12_PM;
rtcTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
rtcTime.StoreOperation = RTC_STOREOPERATION_RESET;
HAL_RTC_SetDate(&hrtc, &rtcDate, RTC_FORMAT_BIN);
HAL_RTC_SetTime(&hrtc, &rtcTime, RTC_FORMAT_BIN);
}
#endif
#if ! defined(PLATFORM_GD77S)
ticksTimer_t daytimeThemeTimer;
/*
SUNRISET.C - computes Sun rise/set times, start/end of twilight, and
the length of the day at any date and latitude
Written as DAYLEN.C, 1989-08-16
Modified to SUNRISET.C, 1992-12-01
(c) Paul Schlyter, 1989, 1992
Released to the public domain by Paul Schlyter, December 1992
*/
/* +++Date last modified: 05-Jul-1997 */
/******************************************************************/
/* This function reduces any angle to within the first revolution */
/* by subtracting or adding even multiples of 360.0 until the */
/* result is >= 0.0 and < 360.0 */
/******************************************************************/
#define INV360 (1.0 / 360.0)
/*****************************************/
/* Reduce angle to within 0..360 degrees */
/*****************************************/
static double revolution(double x)
{
return(x - 360.0 * floor(x * INV360));
} /* revolution */
/*********************************************/
/* Reduce angle to within -180..+180 degrees */
/*********************************************/
static double rev180(double x)
{
return(x - 360.0 * floor(x * INV360 + 0.5));
} /* revolution */
/*******************************************************************/
/* This function computes GMST0, the Greenwhich Mean Sidereal Time */
/* at 0h UT (i.e. the sidereal time at the Greenwhich meridian at */
/* 0h UT). GMST is then the sidereal time at Greenwich at any */
/* time of the day. I've generelized GMST0 as well, and define it */
/* as: GMST0 = GMST - UT -- this allows GMST0 to be computed at */
/* other times than 0h UT as well. While this sounds somewhat */
/* contradictory, it is very practical: instead of computing */
/* GMST like: */
/* */
/* GMST = (GMST0) + UT * (366.2422/365.2422) */
/* */
/* where (GMST0) is the GMST last time UT was 0 hours, one simply */
/* computes: */
/* */
/* GMST = GMST0 + UT */
/* */
/* where GMST0 is the GMST "at 0h UT" but at the current moment! */
/* Defined in this way, GMST0 will increase with about 4 min a */
/* day. It also happens that GMST0 (in degrees, 1 hr = 15 degr) */
/* is equal to the Sun's mean longitude plus/minus 180 degrees! */
/* (if we neglect aberration, which amounts to 20 seconds of arc */
/* or 1.33 seconds of time) */
/* */
/*******************************************************************/
static double greenwhich_mean_sideral_time_at_0_UT(double d)
{
double sidtim0;
/* Sidtime at 0h UT = L (Sun's mean longitude) + 180.0 degr */
/* L = M + w, as defined in sunpos(). Since I'm too lazy to */
/* add these numbers, I'll let the C compiler do it for me. */
/* Any decent C compiler will add the constants at compile */
/* time, imposing no runtime or code overhead. */
sidtim0 = revolution((180.0 + 356.0470 + 282.9404) + (0.9856002585 + 4.70935E-5) * d);
return sidtim0;
}
/******************************************************/
/* Computes the Sun's ecliptic longitude and distance */
/* at an instant given in d, number of days since */
/* 2000 Jan 0.0. The Sun's ecliptic latitude is not */
/* computed, since it's always very near 0. */
/******************************************************/
static void sunpos(double d, double *lon, double *r)
{
double M, /* Mean anomaly of the Sun */
w, /* Mean longitude of perihelion */
/* Note: Sun's mean longitude = M + w */
e, /* Eccentricity of Earth's orbit */
E, /* Eccentric anomaly */
x, y, /* x, y coordinates in orbit */
v; /* True anomaly */
/* Compute mean elements */
M = revolution(356.0470 + 0.9856002585 * d);
w = 282.9404 + 4.70935E-5 * d;
e = 0.016709 - 1.151E-9 * d;
/* Compute true longitude and radius vector */
E = M + e * RADEG * SIND(M) * (1.0 + e * COSD(M));
x = COSD(E) - e;
y = sqrt(1.0 - e*e) * SIND(E);
*r = sqrt(x*x + y*y); /* Solar distance */
v = ATAN2D(y, x); /* True anomaly */
*lon = v + w; /* True solar longitude */
if (*lon >= 360.0)
{
*lon -= 360.0; /* Make it 0..360 degrees */
}
}
/******************************************************/
/* Computes the Sun's equatorial coordinates RA, Decl */
/* and also its distance, at an instant given in d, */
/* the number of days since 2000 Jan 0.0. */
/******************************************************/
static void sun_RA_dec(double d, double *RA, double *dec, double *r)
{
double lon, obl_ecl, x, y, z;
/* Compute Sun's ecliptical coordinates */
sunpos(d, &lon, r);
/* Compute ecliptic rectangular coordinates (z=0) */
x = *r * COSD(lon);
y = *r * SIND(lon);
/* Compute obliquity of ecliptic (inclination of Earth's axis) */
obl_ecl = 23.4393 - 3.563E-7 * d;
/* Convert to equatorial rectangular coordinates - x is unchanged */
z = y * SIND(obl_ecl);
y = y * COSD(obl_ecl);
/* Convert to spherical coordinates */
*RA = ATAN2D(y, x);
*dec = ATAN2D(z, sqrt(x*x + y*y));
}
/***************************************************************************/
/* Note: year,month,date = calendar date, 1801-2099 only. */
/* Eastern longitude positive, Western longitude negative */
/* Northern latitude positive, Southern latitude negative */
/* The longitude value IS critical in this function! */
/* altit = the altitude which the Sun should cross */
/* Set to -35/60 degrees for rise/set, -6 degrees */
/* for civil, -12 degrees for nautical and -18 */
/* degrees for astronomical twilight. */
/* upper_limb: non-zero -> upper limb, zero -> center */
/* Set to non-zero (e.g. 1) when computing rise/set */
/* times, and to zero when computing start/end of */
/* twilight. */
/* *rise = where to store the rise time */
/* *set = where to store the set time */
/* Both times are relative to the specified altitude, */
/* and thus this function can be used to comupte */
/* various twilight times, as well as rise/set times */
/* Return value: 0 = sun rises/sets this day, times stored at */
/* *trise and *tset. */
/* +1 = sun above the specified "horizon" 24 hours. */
/* *trise set to time when the sun is at south, */
/* minus 12 hours while *tset is set to the south */
/* time plus 12 hours. "Day" length = 24 hours */
/* -1 = sun is below the specified "horizon" 24 hours */
/* "Day" length = 0 hours, *trise and *tset are */
/* both set to the time when the sun is at south. */
/* */
/**********************************************************************/
int sunriset(int year, int month, int day, double lon, double lat, double altit, int upper_limb, double *trise, double *tset)
{
double d, /* Days since 2000 Jan 0.0 (negative before) */
sr, /* Solar distance, astronomical units */
sRA, /* Sun's Right Ascension */
sdec, /* Sun's declination */
sradius, /* Sun's apparent radius */
t, /* Diurnal arc */
tsouth, /* Time when Sun is at south */
sidtime; /* Local sidereal time */
int rc = 0; /* Return cde from function - usually 0 */
/* Compute d of 12h local mean solar time */
d = DAYS_SINCE_2000_JAN_0(year, month, day) + 0.5 - lon / 360.0;
/* Compute local sideral time of this moment */
//Greenwhich Mean Sidereal Time at 0h UT
sidtime = revolution(greenwhich_mean_sideral_time_at_0_UT(d) + 180.0 + lon);
/* Compute Sun's RA + Decl at this moment */
sun_RA_dec(d, &sRA, &sdec, &sr);
/* Compute time when Sun is at south - in hours UT */
tsouth = 12.0 - rev180(sidtime - sRA)/15.0;
/* Compute the Sun's apparent radius, degrees */
sradius = 0.2666 / sr;
/* Do correction to upper limb, if necessary */
if (upper_limb)
{
altit -= sradius;
}
/* Compute the diurnal arc that the Sun traverses to reach */
/* the specified altitide altit: */
{
double cost;
cost = (SIND(altit) - SIND(lat) * SIND(sdec)) / (COSD(lat) * COSD(sdec));
if (cost >= 1.0)
{
rc = -1; t = 0.0; /* Sun always below altit */
}
else if (cost <= -1.0)
{
rc = +1; t = 12.0; /* Sun always above altit */
}
else
{
t = ACOSD(cost)/15.0; /* The diurnal arc, hours */
}
}
/* Store rise and set times - in hours UT */
*trise = tsouth - t;
*tset = tsouth + t;
return rc;
} /* __sunriset__ */
void daytimeThemeChangeUpdate(bool startup)
{
if (settingsLocationIsValid() && settingsIsOptionBitSet(BIT_AUTO_NIGHT))
{
ticksTimerStart(&daytimeThemeTimer, (startup ? 2000 : DAYTIME_THEME_TIMER_INTERVAL));
return;
}
ticksTimerReset(&daytimeThemeTimer);
}
void daytimeThemeApply(DayTime_t daytime)
{
uiEvent_t e = { .buttons = 0, .keys = NO_KEYCODE, .rotary = 0, .function = FUNC_REDRAW, .events = FUNCTION_EVENT, .hasEvent = true, .time = ticksGetMillis() };
if (uiDataGlobal.daytimeOverridden == UNDEFINED)
{
uiDataGlobal.daytime = daytime;
}
#if defined(HAS_COLOURS)
displayThemeResetToDefault(); // Apply new theme.
#else
// Need to perform a full reset on the display to change back to non-inverted
displayInit(((daytime == NIGHT) ^ settingsIsOptionBitSet(BIT_INVERSE_VIDEO)));
#endif
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 1000, ((daytime == DAY) ? currentLanguage->daytime_theme_day : currentLanguage->daytime_theme_night), true);
menuSystemCallCurrentMenuTick(&e); // redraw the current screen.
displayLightTrigger(true);
// Don't interrupt VP or melody for this.
if ((voicePromptsIsPlaying() == false) && (soundMelodyIsPlaying() == false))
{
voicePromptsInit();
voicePromptsAppendLanguageString(((daytime == DAY) ? currentLanguage->daytime_theme_day : currentLanguage->daytime_theme_night));
voicePromptsPlay();
}
}
void daytimeThemeTick(void)
{
struct tm gmt;
time_t_custom now = uiDataGlobal.dateTimeSecs;
if (gmtime_r_Custom(&now, &gmt))
{
double trise, tset; // offsets (+/-, in hours) from midnight UTC
bool afterSunrise = false;
bool afterSunset = false;
if (sunriset((gmt.tm_year + 1900), (gmt.tm_mon + 1), gmt.tm_mday,
settingsLocationGetLongitude(), settingsLocationGetLatitude(), (-35.0 / 60.0), 1, &trise, &tset) == 0)
{
struct tm gmtOfMidnightToday = { .tm_year = gmt.tm_year, .tm_mon = gmt.tm_mon, .tm_mday = gmt.tm_mday, .tm_hour = 0, .tm_min = 0, .tm_sec = 0 };
time_t_custom timeMidnightToday = mktime_custom(&gmtOfMidnightToday);
time_t_custom timeRise = timeMidnightToday + (3600.0 * trise);
time_t_custom timeSet = timeMidnightToday + (3600.0 * tset);
// We past the current computed sunset time, then add 24 hours
// to get the next day Sunrise and Sunset times (approx).
if (now > timeSet)
{
timeRise += 86400;
timeSet += 86400;
}
if ((now >= timeRise) || (now < timeSet))
{
afterSunrise = true;
}
if ((now >= timeSet) || (now < timeRise))
{
afterSunset = true;
}
if (afterSunrise && (afterSunset == false) && (uiDataGlobal.daytime == NIGHT) && (uiDataGlobal.daytimeOverridden == UNDEFINED))
{
daytimeThemeApply(DAY);
}
else if (afterSunset && (uiDataGlobal.daytime == DAY) && (uiDataGlobal.daytimeOverridden == UNDEFINED))
{
daytimeThemeApply(NIGHT);
}
}
}
}
void uiChannelModeOrVFOModeThemeDaytimeChange(bool toggle, bool isChannelMode)
{
if (toggle)
{
if (uiDataGlobal.daytimeOverridden == UNDEFINED)
{
uiDataGlobal.daytimeOverridden = ((uiDataGlobal.daytime == DAY) ? NIGHT : DAY);
}
else
{
uiDataGlobal.daytimeOverridden = ((uiDataGlobal.daytimeOverridden == DAY) ? NIGHT : DAY);
}
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT) == false)
{
settingsSetOptionBit(BIT_AUTO_NIGHT_OVERRIDE, true);
settingsSetOptionBit(BIT_AUTO_NIGHT_DAYTIME, (bool)uiDataGlobal.daytimeOverridden);
}
}
else
{
uiDataGlobal.daytimeOverridden = UNDEFINED;
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT) == false)
{
settingsSetOptionBit(BIT_AUTO_NIGHT_OVERRIDE, false);
}
}
daytimeThemeApply(toggle ? uiDataGlobal.daytimeOverridden : uiDataGlobal.daytime);
uiDataGlobal.displayChannelSettings = false;
uiDataGlobal.displayQSOStatePrev = QSO_DISPLAY_DEFAULT_SCREEN;
uiDataGlobal.displayQSOState = QSO_DISPLAY_DEFAULT_SCREEN;
if (isChannelMode)
{
uiChannelModeUpdateScreen(0);
}
else
{
uiVFOModeUpdateScreen(0);
}
}
#endif
bool txInhibitCheckAndWarn(void)
{
if (settingsIsOptionBitSet(BIT_TX_INHIBIT))
{
if ((uiNotificationIsVisible() == false) ||
(uiNotificationIsVisible() && uiNotificationGetId() != NOTIFICATION_ID_TX_INHIBIT))
{
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_TX_INHIBIT, 1000, currentLanguage->tx_inhibit, true);
if ((nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD))
{
soundSetMelody(MELODY_ERROR_BEEP);
}
else
{
voicePromptsTerminateNoTail();
voicePromptsInit();
voicePromptsAppendLanguageString(currentLanguage->tx_inhibit);
voicePromptsPlay();
}
}
return true;
}
return false;
}
bool isTransmittingIgnoringAPRSBeaconing(void)
{
return (trxTransmissionEnabled && (aprsBeaconingIsTransmitting() == false));
}
#if defined(STM32F405xx) && ! defined(PLATFORM_MD9600)
uint32_t cpuGetSignature(void)
{
return (DBGMCU->IDCODE & 0x00000FFF);
}
uint32_t cpuGetRevision(void)
{
return ((DBGMCU->IDCODE >> 16) & 0x0000FFFF);
}
uint32_t cpuGetPackage(void)
{
return (((*(__IO uint16_t *) (0x1FFF7BF0)) & 0x0700) >> 8);
}
int32_t cpuGetFlashSize(void)
{
return (*(__IO uint16_t *) (0x1FFF7A22));
}
#endif