Rework the header RSSI bar (uiUtilityDrawRSSIBarGraph) from a solid two-stage bar into discrete blocks, one per S-level, on colour displays: - 9 green blocks for S1..S9 (4 dB per S-unit) plus 6 red blocks for the S9+ region (10 dB per block, S9+10..S9+60), drawn with 2 px gaps so the header background shows between them. - Proportional fill: full blocks below the level, leading block partially filled to reflect the exact signal. - Bar height raised 4 -> 6 px; the FM/DMR mic-level bars follow the same height so nothing is left behind when switching RX <-> TX. - Scale labels below the blocks: S1/S3/S5/S7/S9 under the green zone and +20/+40/+60 under the red, centred under their blocks (font_6x8). The label row is blanked while transmitting. Monochrome displays keep the original solid bar. Existing analog S9+ RSSI compression is preserved, so calibration is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4753 lines
142 KiB
C
4753 lines
142 KiB
C
/*
|
||
* Copyright (C) 2019-2025 Roger Clark, VK3KYY / G4KYF
|
||
* Daniel Caujolle-Bert, F1RMB
|
||
*
|
||
*
|
||
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions
|
||
* are met:
|
||
*
|
||
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||
*
|
||
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
|
||
* in the documentation and/or other materials provided with the distribution.
|
||
*
|
||
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
|
||
* from this software without specific prior written permission.
|
||
*
|
||
* 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation,
|
||
* incorporation in a commercial product or incorporation into a product or project which allows commercial use.
|
||
*
|
||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
*
|
||
*/
|
||
|
||
#include <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 "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, ¤tRec);
|
||
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, ¤tRec))
|
||
{
|
||
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, ¤tRec);
|
||
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), ¤tRec);
|
||
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), ¤tRec))
|
||
{
|
||
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(¤tContactData));
|
||
|
||
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(16, LinkHead->contact, FONT_SIZE_3);
|
||
|
||
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();
|
||
}
|
||
|
||
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();
|
||
bool audioIsMuted = audioAmpIsMuted();
|
||
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
|
||
// 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);
|
||
displayPrintAtDoubleHeight(FREQUENCY_X_POS, yFont3, buffer, FONT_SIZE_3, displayFocusAndDblHeight);
|
||
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, ¤tRec))
|
||
{
|
||
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
|