OpenGD77/firmware/source/user_interface/uiUtilities.c

2835 wiersze
77 KiB
C

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* Copyright (C)2019 Roger Clark. VK3KYY / G4KYF
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include <math.h>
#include "hardware/EEPROM.h"
#include "user_interface/menuSystem.h"
#include "user_interface/uiUtilities.h"
#include "user_interface/uiLocalisation.h"
#include "hardware/HR-C6000.h"
#include "functions/settings.h"
#include "hardware/SPI_Flash.h"
#include "functions/ticks.h"
#include "functions/trx.h"
static __attribute__((section(".data.$RAM2"))) LinkItem_t callsList[NUM_LASTHEARD_STORED];
static const int DMRID_MEMORY_STORAGE_START = 0x30000;
static const int DMRID_HEADER_LENGTH = 0x0C;
static dmrIDsCache_t dmrIDsCache;
static voicePromptItem_t voicePromptSequenceState = PROMPT_SEQUENCE_CHANNEL_NAME_OR_VFO_FREQ;
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;
static void announceChannelNameOrVFOFrequency(bool voicePromptWasPlaying, bool announceVFOName);
DECLARE_SMETER_ARRAY(rssiMeterHeaderBar, DISPLAY_SIZE_X);
// 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, struct_codeplugContact_t *contact)
{
if ((contact->reserve1 & 0x01) == 0x00)
{
tsSetManualOverride(chan, (((contact->reserve1 & 0x02) >> 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) && (HRC6000GetReceivedTgOrPcId() != 0)) &&
(getAudioAmpStatus() & AUDIO_AMP_MODE_RF)
&& checkTalkGroupFilter() &&
(((item = lastheardFindInList(rxID)) != NULL) && (item == LinkHead)))
{
return true;
}
return false;
}
int alignFrequencyToStep(int freq, int step)
{
int r = freq % step;
return (r ? freq + (step - r) : freq);
}
/*
* 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(char *str)
{
char *p = str;
while(*p != '\0')
{
if (*p == ' ')
{
return (p - str);
}
p++;
}
return -1;
}
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].locator[0] = 0;
callsList[i].time = 0;
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;
}
}
}
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;
}
static uint8_t *coordsToMaidenhead(double longitude, double latitude)
{
static uint8_t maidenhead[15];
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;
maidenhead[0 + i] = c + 'A';
l2 = c;
l -= l2;
l *= 10.0;
c = (uint8_t) l;
maidenhead[2 + i] = c + '0';
l2 = c;
l -= l2;
l *= 24.0;
c = (uint8_t) l;
maidenhead[4 + i] = c + 'A';
#if 0
if (extended)
{
l2 = c;
l -= l2;
l *= 10.0;
c = (uint8_t) l;
maidenhead[6 + i] = c + '0';
l2 = c;
l -= l2;
l *= 24.0;
c = (uint8_t) l;
maidenhead[8 + i] = c + (extended ? 'A' : 'a');
l2 = c;
l -= l2;
l *= 10.0;
c = (uint8_t) l;
maidenhead[10 + i] = c + '0';
l2 = c;
l -= l2;
l *= 24.0;
c = (uint8_t) l;
maidenhead[12 + i] = c + (extended ? 'A' : 'a');
}
#endif
l = latitude;
}
#if 0
maidenhead[extended ? 14 : 6] = '\0';
#else
maidenhead[6] = '\0';
#endif
return &maidenhead[0];
}
static uint8_t *decodeGPSPosition(uint8_t *data)
{
#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;
float longitude = 360.0F / 33554432.0F; // 360/2^25 steps
float latitude = 180.0F / 16777216.0F; // 180/2^24 steps
longitude *= (float)longitudeI;
latitude *= (float)latitudeI;
return (coordsToMaidenhead(longitude, latitude));
}
static uint8_t *decodeTA(uint8_t *TA)
{
uint8_t *b;
uint8_t c;
int8_t j;
uint8_t i, t1, t2;
static uint8_t buffer[32];
uint8_t *talkerAlias = TA;
uint8_t TAformat = (talkerAlias[0] >> 6U) & 0x03U;
uint8_t TAsize = (talkerAlias[0] >> 1U) & 0x1FU;
switch (TAformat)
{
case 0U: // 7 bit
memset(&buffer, 0, sizeof(buffer));
b = &talkerAlias[0];
t1 = 0U; t2 = 0U; c = 0U;
for (i = 0U; (i < 32U) && (t2 < TAsize); i++)
{
for (j = 7; j >= 0; j--)
{
c = (c << 1U) | (b[i] >> j);
if (++t1 == 7U)
{
if (i > 0U)
{
buffer[t2++] = c & 0x7FU;
}
t1 = 0U;
c = 0U;
}
}
}
buffer[TAsize] = 0;
break;
case 1U: // ISO 8 bit
case 2U: // UTF8
memcpy(&buffer, talkerAlias + 1U, sizeof(buffer));
break;
case 3U: // UTF16 poor man's conversion
t2=0;
memset(&buffer, 0, sizeof(buffer));
for (i = 0U; (i < 15U) && (t2 < TAsize); i++)
{
if (talkerAlias[2U * i + 1U] == 0)
{
buffer[t2++] = talkerAlias[2U * i + 2U];
}
else
{
buffer[t2++] = '?';
}
}
buffer[TAsize] = 0;
break;
}
return &buffer[0];
}
void lastHeardClearLastID(void)
{
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) == true)
{
snprintf(item->contact, 17, "%s", buffer);
}
else
{
dmrIDLookup(item->id, &currentRec);
snprintf(item->contact, 17, "%s", currentRec.text);
}
break;
case CONTACT_DISPLAY_PRIO_DB_CC_TA:
case CONTACT_DISPLAY_PRIO_TA_DB_CC:
if (dmrIDLookup(item->id, &currentRec) == true)
{
snprintf(item->contact, 17, "%s", currentRec.text);
}
else
{
if (contactIDLookup(item->id, CONTACT_CALLTYPE_PC, buffer) == true)
{
snprintf(item->contact, 17, "%s", buffer);
}
else
{
snprintf(item->contact, 17, "%s", currentRec.text);
}
}
break;
}
if (item->talkGroupOrPcId != (trxDMRID | (PC_CALL_FLAG << 24)))
{
if (contactIDLookup(item->talkGroupOrPcId & 0x00FFFFFF, CONTACT_CALLTYPE_PC, buffer) == true)
{
snprintf(item->talkgroup, 17, "%s", buffer);
}
else
{
dmrIDLookup(item->talkGroupOrPcId & 0x00FFFFFF, &currentRec);
snprintf(item->talkgroup, 17, "%s", currentRec.text);
}
}
}
else
{
// TalkGroup
if (contactIDLookup(item->talkGroupOrPcId, CONTACT_CALLTYPE_TG, buffer) == true)
{
snprintf(item->talkgroup, 17, "%s", buffer);
}
else
{
snprintf(item->talkgroup, 17, "%s %d", 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) == true)
{
snprintf(item->contact, 21, "%s", buffer);
}
else
{
dmrIDLookup((item->id & 0x00FFFFFF), &currentRec);
snprintf(item->contact, 21, "%s", currentRec.text);
}
break;
case CONTACT_DISPLAY_PRIO_DB_CC_TA:
case CONTACT_DISPLAY_PRIO_TA_DB_CC:
if (dmrIDLookup((item->id & 0x00FFFFFF), &currentRec) == true)
{
snprintf(item->contact, 21, "%s", currentRec.text);
}
else
{
if (contactIDLookup(item->id, CONTACT_CALLTYPE_PC, buffer) == true)
{
snprintf(item->contact, 21, "%s", buffer);
}
else
{
snprintf(item->contact, 21, "%s", currentRec.text);
}
}
break;
}
}
}
bool lastHeardListUpdate(uint8_t *dmrDataBuffer, bool forceOnHotspot)
{
static uint8_t bufferTA[32];
static uint8_t blocksTA = 0x00;
bool retVal = false;
uint32_t talkGroupOrPcId = (dmrDataBuffer[0] << 24) + (dmrDataBuffer[3] << 16) + (dmrDataBuffer[4] << 8) + (dmrDataBuffer[5] << 0);
static bool overrideTA = false;
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 != lastID)
{
memset(bufferTA, 0, 32);// Clear any TA data in TA buffer (used for decode)
blocksTA = 0x00;
overrideTA = false;
retVal = true;// something has changed
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 = fw_millis();
lastTG = talkGroupOrPcId;
if (item == LinkHead)
{
uiDataGlobal.displayQSOState = QSO_DISPLAY_CALLER_DATA;// flag that the display needs to update
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 = fw_millis();
item->receivedTS = (dmrMonitorCapturedTS != -1) ? dmrMonitorCapturedTS : trxGetDMRTimeSlot();
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));
memset(item->locator, 0, sizeof(item->locator));
updateLHItem(item);
if (item->talkGroupOrPcId != 0)
{
uiDataGlobal.displayQSOState = QSO_DISPLAY_CALLER_DATA;// flag that the display needs to update
}
}
}
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 = fw_millis();
}
lastTG = talkGroupOrPcId;
memset(bufferTA, 0, 32);// Clear any TA data in TA buffer (used for decode)
blocksTA = 0x00;
overrideTA = false;
retVal = true;// something has changed
}
item->receivedTS = (dmrMonitorCapturedTS != -1) ? dmrMonitorCapturedTS : trxGetDMRTimeSlot();// Always update this in case the TS changed.
}
}
else
{
// Data contains the Talker Alias Data
uint8_t blockID = (forceOnHotspot ? dmrDataBuffer[0] : DMR_frame_buffer[0]) - 4;
// ID 0x04..0x07: TA
if (blockID < 4)
{
// 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 *decodedTA;
if ((decodedTA = decodeTA(&bufferTA[0])) != NULL)
{
// TAs doesn't match, update contact and screen.
if (overrideTA || (strlen((const char *)decodedTA) > strlen((const char *)&LinkHead->talkerAlias)))
{
memcpy(&LinkHead->talkerAlias, decodedTA, 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
{
uint8_t *locator = decodeGPSPosition((uint8_t *)(forceOnHotspot ? &dmrDataBuffer[0] : &DMR_frame_buffer[0]));
if (strncmp((char *)&LinkHead->locator, (char *)locator, 7) != 0)
{
memcpy(&LinkHead->locator, locator, 7);
uiDataGlobal.displayQSOState = QSO_DISPLAY_CALLER_DATA_UPDATE;
}
}
}
}
return retVal;
}
static void dmrIDReadContactInFlash(uint32_t contactOffset, uint8_t *data, uint32_t len)
{
SPI_Flash_read((DMRID_MEMORY_STORAGE_START + DMRID_HEADER_LENGTH) + contactOffset, data, len);
}
void dmrIDCacheInit(void)
{
uint8_t headerBuf[32];
memset(&dmrIDsCache, 0, sizeof(dmrIDsCache_t));
memset(&headerBuf, 0, sizeof(headerBuf));
SPI_Flash_read(DMRID_MEMORY_STORAGE_START, headerBuf, DMRID_HEADER_LENGTH);
if ((headerBuf[0] != 'I') || (headerBuf[1] != 'D') || (headerBuf[2] != '-'))
{
return;
}
dmrIDsCache.entries = ((uint32_t)headerBuf[8] | (uint32_t)headerBuf[9] << 8 | (uint32_t)headerBuf[10] << 16 | (uint32_t)headerBuf[11] << 24);
dmrIDsCache.contactLength = (uint8_t)headerBuf[3] - 0x4a;
if (dmrIDsCache.entries > 0)
{
dmrIdDataStruct_t dmrIDContact;
// Set Min and Max IDs boundaries
// First available ID
dmrIDReadContactInFlash(0, (uint8_t *)&dmrIDContact, 4U);
dmrIDsCache.slices[0] = dmrIDContact.id;
// Last available ID
dmrIDReadContactInFlash((dmrIDsCache.contactLength * (dmrIDsCache.entries - 1)), (uint8_t *)&dmrIDContact, 4U);
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++)
{
dmrIDReadContactInFlash((dmrIDsCache.contactLength * ((dmrIDsCache.IDsPerSlice * i) + dmrIDsCache.IDsPerSlice)), (uint8_t *)&dmrIDContact, 4U);
dmrIDsCache.slices[i + 1] = dmrIDContact.id;
}
}
}
}
bool dmrIDLookup(int targetId, dmrIdDataStruct_t *foundRecord)
{
int targetIdBCD = int2bcd(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 - 4U) aren't NULL terminated,
// so clearing the whole destination array is mandatory
memset(foundRecord->text, 0, sizeof(foundRecord->text));
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];
dmrIDReadContactInFlash((dmrIDsCache.contactLength * (dmrIDsCache.IDsPerSlice * i)) + 4U, (uint8_t *)foundRecord + 4U, (dmrIDsCache.contactLength - 4U));
return true;
}
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))];
dmrIDReadContactInFlash((dmrIDsCache.contactLength * (dmrIDsCache.IDsPerSlice * (isMin ? 0 : (ID_SLICES - 1)))) + 4U, (uint8_t *)foundRecord + 4U, (dmrIDsCache.contactLength - 4U));
return true;
}
}
// Look for the ID now
while (startPos <= endPos)
{
curPos = (startPos + endPos) >> 1;
dmrIDReadContactInFlash((dmrIDsCache.contactLength * curPos), (uint8_t *)foundRecord, 4U);
if (foundRecord->id < targetIdBCD)
{
startPos = curPos + 1;
}
else
{
if (foundRecord->id > targetIdBCD)
{
endPos = curPos - 1;
}
else
{
dmrIDReadContactInFlash((dmrIDsCache.contactLength * curPos) + 4U, (uint8_t *)foundRecord + 4U, (dmrIDsCache.contactLength - 4U));
return true;
}
}
}
}
snprintf(foundRecord->text, 20, "ID:%d", targetId);
return false;
}
bool contactIDLookup(uint32_t id, int calltype, char *buffer)
{
struct_codeplugContact_t contact;
int8_t manTS = tsGetManualOverrideFromCurrentChannel();
int contactIndex = codeplugContactIndexByTGorPC((id & 0x00FFFFFF), calltype, &contact, (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)
{
codeplugUtilConvertBufToString(currentChannelData->name, buffer, 16);
}
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);
}
uiUtilityDisplayInformation(buffer, DISPLAY_INFO_ZONE, -1);
}
static void displaySplitOrSpanText(uint8_t y, char *text)
{
if (text != NULL)
{
uint8_t len = strlen(text);
if (len == 0)
{
return;
}
else if (len <= 16)
{
ucPrintCentered(y, text, FONT_SIZE_3);
}
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[43]; // 2 * 21 chars + NULL
memcpy(buffer, text, len + 1);
char *p = buffer + 20;
// Find a space backward
while ((*p != ' ') && (p > buffer))
{
p--;
}
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;
ucPrintCentered(y, chomp(buffer), FONT_SIZE_1); // 2 pixels are saved, could center
buffer[21] = c;
buffer[42] = 0;
ucPrintCentered(y + 8, chomp(buffer + 21), FONT_SIZE_1);
}
else
{
*p = 0;
ucPrintCentered(y, chomp(buffer), FONT_SIZE_1);
ucPrintCentered(y + 8, chomp(p + 1), FONT_SIZE_1);
}
}
else // One line of 21 chars max
{
ucPrintCentered(y
#if ! defined(PLATFORM_RD5R)
+ 4
#endif
, text, FONT_SIZE_1);
}
}
}
}
/*
* 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)
{
char buffer[37]; // Max: TA 27 (in 7bit format) + ' [' + 6 (Maidenhead) + ']' + NULL
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);
displayChannelNameOrRxFrequency(buffer, (sizeof(buffer) / sizeof(buffer[0])));
return;
}
if ((cpos = getFirstSpacePos(text)) != -1)
{
// Callsign found
memcpy(buffer, text, cpos);
buffer[cpos] = 0;
uiUtilityDisplayInformation(chomp(buffer), DISPLAY_INFO_CHANNEL, -1);
memcpy(buffer, text + (cpos + 1), (maxLen - (cpos + 1)));
buffer[(strlen(text) - (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);
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);
displayChannelNameOrRxFrequency(buffer, (sizeof(buffer) / sizeof(buffer[0])));
}
}
void uiUtilityDisplayInformation(const char *str, displayInformation_t line, int8_t yOverride)
{
bool inverted = false;
switch (line)
{
case DISPLAY_INFO_CONTACT_INVERTED:
#if defined(PLATFORM_RD5R)
ucFillRect(0, DISPLAY_Y_POS_CONTACT + 1, DISPLAY_SIZE_X, MENU_ENTRY_HEIGHT, false);
#else
ucClearRows(2, 4, true);
#endif
inverted = true;
case DISPLAY_INFO_CONTACT:
ucPrintCore(0, ((yOverride == -1) ? (DISPLAY_Y_POS_CONTACT + V_OFFSET) : yOverride), str, FONT_SIZE_3, TEXT_ALIGN_CENTER, inverted);
break;
case DISPLAY_INFO_CONTACT_OVERRIDE_FRAME:
ucDrawRect(0, ((yOverride == -1) ? DISPLAY_Y_POS_CONTACT : yOverride), DISPLAY_SIZE_X, OVERRIDE_FRAME_HEIGHT, true);
break;
case DISPLAY_INFO_CHANNEL:
ucPrintCentered(((yOverride == -1) ? DISPLAY_Y_POS_CHANNEL_FIRST_LINE : yOverride), str, FONT_SIZE_3);
break;
case DISPLAY_INFO_SQUELCH:
{
static const int xbar = 74; // 128 - (51 /* max squelch px */ + 3);
int sLen = (strlen(str) * 8);
// Center squelch word between col0 and bargraph, if possible.
ucPrintAt(0 + ((sLen) < xbar - 2 ? (((xbar - 2) - (sLen)) >> 1) : 0), DISPLAY_Y_POS_SQUELCH_BAR, str, FONT_SIZE_3);
int bargraph = 1 + ((currentChannelData->sql - 1) * 5) / 2;
ucDrawRect(xbar - 2, DISPLAY_Y_POS_SQUELCH_BAR, 55, SQUELCH_BAR_H + 4, true);
ucFillRect(xbar, DISPLAY_Y_POS_SQUELCH_BAR + 2, bargraph, SQUELCH_BAR_H, false);
}
break;
case DISPLAY_INFO_TONE_AND_SQUELCH:
{
char buf[24];
int pos = 0;
if (trxGetMode() == RADIO_MODE_ANALOG)
{
pos += snprintf(buf + pos, 24 - pos, "Rx:");
if (codeplugChannelToneIsCTCSS(currentChannelData->rxTone))
{
pos += snprintf(buf + pos, 24 - pos, "%d.%dHz", currentChannelData->rxTone / 10 , currentChannelData->rxTone % 10);
}
else if (codeplugChannelToneIsDCS(currentChannelData->rxTone))
{
pos += snprintDCS(buf + pos, 24 - pos, currentChannelData->rxTone & 0777, (currentChannelData->rxTone & CODEPLUG_DCS_INVERTED_MASK));
}
else
{
pos += snprintf(buf + pos, 24 - pos, "%s", currentLanguage->none);
}
pos += snprintf(buf + pos, 24 - pos, "|Tx:");
if (codeplugChannelToneIsCTCSS(currentChannelData->txTone))
{
pos += snprintf(buf + pos, 24 - pos, "%d.%dHz", currentChannelData->txTone / 10 , currentChannelData->txTone % 10);
}
else if (codeplugChannelToneIsDCS(currentChannelData->txTone))
{
pos += snprintDCS(buf + pos, 24 - pos, currentChannelData->txTone & 0777, (currentChannelData->txTone & CODEPLUG_DCS_INVERTED_MASK));
}
else
{
pos += snprintf(buf + pos, 24 - pos, "%s", currentLanguage->none);
}
ucPrintCentered(DISPLAY_Y_POS_CSS_INFO, buf, FONT_SIZE_1);
snprintf(buf, 24, "SQL:%d%%", 5 * (((currentChannelData->sql == 0) ? nonVolatileSettings.squelchDefaults[trxCurrentBand[TRX_RX_FREQ_BAND]] : currentChannelData->sql)-1));
ucPrintCentered(DISPLAY_Y_POS_SQL_INFO, buf, FONT_SIZE_1);
}
}
break;
case DISPLAY_INFO_SQUELCH_CLEAR_AREA:
#if defined(PLATFORM_RD5R)
ucFillRect(0, DISPLAY_Y_POS_SQUELCH_BAR, DISPLAY_SIZE_X, 9, true);
#else
ucClearRows(2, 4, false);
#endif
break;
case DISPLAY_INFO_TX_TIMER:
ucPrintCentered(DISPLAY_Y_POS_TX_TIMER, str, FONT_SIZE_4);
break;
case DISPLAY_INFO_ZONE:
ucPrintCentered(DISPLAY_Y_POS_ZONE, str, FONT_SIZE_1);
break;
}
}
void uiUtilityRenderQSODataAndUpdateScreen(void)
{
if (isQSODataAvailableForCurrentTalker())
{
ucClearBuf();
uiUtilityRenderHeader(false);
uiUtilityRenderQSOData();
ucRender();
}
}
void uiUtilityRenderQSOData(void)
{
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
ucPrintCentered(16, LinkHead->contact, FONT_SIZE_3);
ucPrintCentered(DISPLAY_Y_POS_CHANNEL_FIRST_LINE, currentLanguage->private_call, FONT_SIZE_3);
if (LinkHead->talkGroupOrPcId != (trxDMRID | (PC_CALL_FLAG << 24)))
{
uiUtilityDisplayInformation(LinkHead->talkgroup, DISPLAY_INFO_ZONE, -1);
ucPrintAt(1, DISPLAY_Y_POS_ZONE, "=>", FONT_SIZE_1);
}
}
else
{
// Group call
bool different = (((LinkHead->talkGroupOrPcId & 0xFFFFFF) != trxTalkGroupOrPcId ) ||
(((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);
}
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->locator[0] != 0)
{
char bufferTA[37]; // TA + ' [' + Maidenhead + ']' + NULL
memset(bufferTA, 0, sizeof(bufferTA));
snprintf(bufferTA, 37, "%s [%s]", LinkHead->talkerAlias, LinkHead->locator);
displayContactTextInfos(bufferTA, sizeof(bufferTA), 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->locator[0] != 0)
{
char bufferTA[37]; // TA + ' [' + Maidenhead + ']' + NULL
memset(bufferTA, 0, sizeof(bufferTA));
snprintf(bufferTA, 37, "%s [%s]", LinkHead->talkerAlias, LinkHead->locator);
displayContactTextInfos(bufferTA, sizeof(bufferTA), 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;
}
}
}
}
void uiUtilityRenderHeader(bool isVFODualWatchScanning)
{
const int MODE_TEXT_X_OFFSET = 1;
const int FILTER_TEXT_X_OFFSET = 25;
static const int bufferLen = 17;
char buffer[bufferLen];
static bool scanBlinkPhase = true;
static uint32_t blinkTime = 0;
int powerLevel = trxGetPowerLevel();
bool isPerChannelPower = (currentChannelData->libreDMR_Power != 0x00);
if (isVFODualWatchScanning == false)
{
if (!trxTransmissionEnabled)
{
uiUtilityDrawRSSIBarGraph();
}
else
{
if (trxGetMode() == RADIO_MODE_DIGITAL)
{
uiUtilityDrawDMRMicLevelBarGraph();
}
}
}
if (uiDataGlobal.Scan.active || uiDataGlobal.Scan.toneActive)
{
int blinkPeriod = 1000;
if (scanBlinkPhase)
{
blinkPeriod = 500;
}
if ((fw_millis() - blinkTime) > blinkPeriod)
{
blinkTime = fw_millis();
scanBlinkPhase = !scanBlinkPhase;
}
}
else
{
scanBlinkPhase = false;
}
switch(trxGetMode())
{
case RADIO_MODE_ANALOG:
if (isVFODualWatchScanning)
{
strcpy(buffer, "[DW]");
}
else
{
strcpy(buffer, "FM");
if (!trxGetBandwidthIs25kHz())
{
strcat(buffer,"N");
}
}
ucPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, buffer, ((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, scanBlinkPhase);
if ((monitorModeData.isEnabled == false) &&
((currentChannelData->txTone != CODEPLUG_CSS_NONE) || (currentChannelData->rxTone != CODEPLUG_CSS_NONE)))
{
bool cssTextInverted = (trxGetAnalogFilterLevel() == ANALOG_FILTER_NONE);//(nonVolatileSettings.analogFilterLevel == ANALOG_FILTER_NONE);
if (currentChannelData->txTone != CODEPLUG_CSS_NONE)
{
strcpy(buffer, (codeplugChannelToneIsDCS(currentChannelData->txTone) ? "DT" : "CT"));
}
else // tx and/or rx tones are enabled, no need to check for this
{
strcpy(buffer, (codeplugChannelToneIsDCS(currentChannelData->rxTone) ? "D" : "C"));
}
// There is no room to display if rxTone is CTCSS or DCS, when txTone is set.
if (currentChannelData->rxTone != CODEPLUG_CSS_NONE)
{
strcat(buffer, "R");
}
if (cssTextInverted)
{
// Inverted rectangle width is fixed size, large enough to fit 3 characters
ucFillRect((FILTER_TEXT_X_OFFSET - 2), DISPLAY_Y_POS_HEADER - 1, (18 + 3), 9, false);
}
// DCS chars are centered in their H space
ucPrintCore((FILTER_TEXT_X_OFFSET + (9 /* halt of 3 chars */)) - ((strlen(buffer) * 6) >> 1),
DISPLAY_Y_POS_HEADER, buffer, FONT_SIZE_1, TEXT_ALIGN_LEFT, cssTextInverted);
}
break;
case RADIO_MODE_DIGITAL:
if (settingsUsbMode != USB_MODE_HOTSPOT)
{
bool contactTSActive = false;
bool tsManOverride = false;
if (nonVolatileSettings.extendedInfosOnScreen & (INFO_ON_SCREEN_TS & INFO_ON_SCREEN_BOTH))
{
contactTSActive = ((nonVolatileSettings.overrideTG == 0) && ((currentContactData.reserve1 & 0x01) == 0x00));
tsManOverride = (contactTSActive ? tsIsContactHasBeenOverriddenFromCurrentChannel() : (tsGetManualOverrideFromCurrentChannel() != 0));
}
if (isVFODualWatchScanning == false)
{
if (!scanBlinkPhase && (nonVolatileSettings.dmrDestinationFilter > DMR_DESTINATION_FILTER_NONE))
{
ucFillRect(0, DISPLAY_Y_POS_HEADER - 1, 20, 9, false);
}
}
if (!scanBlinkPhase)
{
bool isInverted = isVFODualWatchScanning ? false : (scanBlinkPhase ^ (nonVolatileSettings.dmrDestinationFilter > DMR_DESTINATION_FILTER_NONE));
ucPrintCore(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 (isVFODualWatchScanning == false)
{
bool tsInverted = false;
snprintf(buffer, bufferLen, "%s%d", contactTSActive ? "cS" : currentLanguage->ts, trxGetDMRTimeSlot() + 1);
if (!(nonVolatileSettings.dmrCcTsFilter & DMR_TS_FILTER_PATTERN))
{
ucFillRect(FILTER_TEXT_X_OFFSET - 2, DISPLAY_Y_POS_HEADER - 1, 21, 9, false);
tsInverted = true;
}
ucPrintCore(FILTER_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, buffer, (tsManOverride ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, tsInverted);
}
}
break;
}
sprintf(buffer,"%s%s", POWER_LEVELS[powerLevel], POWER_LEVEL_UNITS[powerLevel]);
ucPrintCore(0, 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_CENTER, 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) && (((settingsUsbMode == USB_MODE_HOTSPOT) || (trxGetMode() == RADIO_MODE_ANALOG)) == false))
{
const int COLOR_CODE_X_POSITION = 84;
int ccode = trxGetDMRColourCode();
bool isNotFilteringCC = !(nonVolatileSettings.dmrCcTsFilter & DMR_CC_FILTER_PATTERN);
snprintf(buffer, bufferLen, "C%d", ccode);
if (isNotFilteringCC)
{
ucFillRect(COLOR_CODE_X_POSITION - 1, DISPLAY_Y_POS_HEADER - 1, 13 + ((ccode > 9) * 6), 9, false);
}
ucPrintCore(COLOR_CODE_X_POSITION, DISPLAY_Y_POS_HEADER, buffer, FONT_SIZE_1, TEXT_ALIGN_LEFT, isNotFilteringCC);
}
// Display battery percentage/voltage
if (nonVolatileSettings.bitfieldOptions & BIT_BATTERY_VOLTAGE_IN_HEADER)
{
int volts, mvolts;
int16_t xV = (DISPLAY_SIZE_X - ((3 * 6) + 3));
getBatteryVoltage(&volts, &mvolts);
snprintf(buffer, bufferLen, "%1d", volts);
ucPrintCore(xV, DISPLAY_Y_POS_HEADER, buffer, FONT_SIZE_1, TEXT_ALIGN_LEFT, false);
ucDrawRect(xV + 6, DISPLAY_Y_POS_HEADER + 5, 2, 2, true);
snprintf(buffer, bufferLen, "%1dV", mvolts);
ucPrintCore(xV + 6 + 3, DISPLAY_Y_POS_HEADER, buffer, FONT_SIZE_1, TEXT_ALIGN_LEFT, false);
}
else
{
snprintf(buffer, bufferLen, "%d%%", getBatteryPercentage());
ucPrintCore(0, DISPLAY_Y_POS_HEADER, buffer, FONT_SIZE_1, TEXT_ALIGN_RIGHT, false);// Display battery percentage at the right
}
}
void uiUtilityRedrawHeaderOnly(bool isVFODualWatchScanning)
{
#if defined(PLATFORM_RD5R)
ucClearRows(0, 1, false);
#else
ucClearRows(0, 2, false);
#endif
uiUtilityRenderHeader(isVFODualWatchScanning);
ucRenderRows(0, 2);
}
int getRSSIdBm(void)
{
int dBm = 0;
if (trxCurrentBand[TRX_RX_FREQ_BAND] == RADIO_BAND_UHF)
{
// Use fixed point maths to scale the RSSI value to dBm, based on data from VK4JWT and VK7ZJA
dBm = -151 + trxRxSignal;// Note no the RSSI value on UHF does not need to be scaled like it does on VHF
}
else
{
// VHF
// Use fixed point maths to scale the RSSI value to dBm, based on data from VK4JWT and VK7ZJA
dBm = -164 + ((trxRxSignal * 32) / 27);
}
return dBm;
}
static void drawHeaderBar(int *barWidth, int16_t barHeight)
{
*barWidth = CLAMP(*barWidth, 0, DISPLAY_SIZE_X);
if (*barWidth)
{
ucFillRect(0, DISPLAY_Y_POS_BAR, *barWidth, barHeight, false);
}
// Clear the end of the bar area, if needed
if (*barWidth < DISPLAY_SIZE_X)
{
ucFillRect(*barWidth, DISPLAY_Y_POS_BAR, (DISPLAY_SIZE_X - *barWidth), barHeight, true);
}
}
void uiUtilityDrawRSSIBarGraph(void)
{
int rssi = getRSSIdBm();
if ((rssi > SMETER_S9) && (trxGetMode() == RADIO_MODE_ANALOG))
{
// In Analog mode, the max RSSI value from the hardware is over S9+60.
// So scale this to fit in the last 30% of the display
rssi = ((rssi - SMETER_S9) / 5) + SMETER_S9;
// in Digital mode. The maximum signal is around S9+10 dB.
// So no scaling is required, as the full scale value is approximately S9+10dB
}
// Scale the entire bar by 2.
// Because above S9 the values are scaled to 1/5. This results in the signal below S9 being doubled in scale
// Signals above S9 the scales is compressed to 2/5.
rssi = (rssi - SMETER_S0) * 2;
int barWidth = ((rssi * rssiMeterHeaderBarNumUnits) / rssiMeterHeaderBarDivider);
drawHeaderBar(&barWidth, 4);
#if 0 // Commented for now, maybe an option later.
int xPos = 0;
int currentMode = trxGetMode();
for (uint8_t i = 1; ((i < 10) && (xPos <= barWidth)); i += 2)
{
if ((i <= 9) || (currentMode == RADIO_MODE_DIGITAL))
{
xPos = rssiMeterHeaderBar[i];
}
else
{
xPos = ((rssiMeterHeaderBar[i] - rssiMeterHeaderBar[9]) / 5) + rssiMeterHeaderBar[9];
}
xPos *= 2;
ucDrawFastVLine(xPos, (DISPLAY_Y_POS_BAR + 1), 2, false);
}
#endif
}
void uiUtilityDrawFMMicLevelBarGraph(void)
{
trxReadVoxAndMicStrength();
uint8_t micdB = (trxTxMic >> 1); // trxTxMic is in 0.5dB unit, displaying 50dB .. 100dB
// display from 50dB to 100dB, span over 128pix
int barWidth = ((uint16_t)(((float)DISPLAY_SIZE_X / 50.0) * ((float)micdB - 50.0)));
drawHeaderBar(&barWidth, 3);
}
void uiUtilityDrawDMRMicLevelBarGraph(void)
{
int barWidth = ((uint16_t)(sqrt(micAudioSamplesTotal) * 1.5));
drawHeaderBar(&barWidth, 3);
}
void setOverrideTGorPC(int tgOrPc, bool privateCall)
{
uiDataGlobal.tgBeforePcMode = 0;
settingsSet(nonVolatileSettings.overrideTG, (uint32_t) tgOrPc);
if (privateCall == true)
{
// 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;
}
settingsSet(nonVolatileSettings.overrideTG, (nonVolatileSettings.overrideTG | (PC_CALL_FLAG << 24)));
}
}
void uiUtilityDisplayFrequency(uint8_t y, bool isTX, bool hasFocus, uint32_t frequency, bool displayVFOChannel, bool isScanMode, uint8_t dualWatchVFO)
{
static const int bufferLen = 17;
char buffer[bufferLen];
int val_before_dp = frequency / 100000;
int val_after_dp = frequency - val_before_dp * 100000;
// Focus + direction
snprintf(buffer, bufferLen, "%c%c", ((hasFocus && !isScanMode)? '>' : ' '), (isTX ? 'T' : 'R'));
ucPrintAt(0, y, buffer, FONT_SIZE_3);
// VFO
if (displayVFOChannel)
{
ucPrintAt(16, y + VFO_LETTER_Y_OFFSET, (((dualWatchVFO == 0) && (nonVolatileSettings.currentVFONumber == 0)) || (dualWatchVFO == 1)) ? "A" : "B", FONT_SIZE_1);
}
// Frequency
snprintf(buffer, bufferLen, "%d.%05d", val_before_dp, val_after_dp);
ucPrintAt(FREQUENCY_X_POS, y, buffer, FONT_SIZE_3);
ucPrintAt(DISPLAY_SIZE_X - (3 * 8), y, "MHz", FONT_SIZE_3);
}
size_t snprintDCS(char *s, size_t n, uint16_t code, bool inverted)
{
return snprintf(s, n, "D%03o%c", code, (inverted ? 'I' : 'N'));
}
void freqEnterReset(void)
{
memset(uiDataGlobal.FreqEnter.digits, '-', FREQ_ENTER_DIGITS_MAX);
uiDataGlobal.FreqEnter.index = 0;
}
int freqEnterRead(int startDigit, int endDigit)
{
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';
}
}
}
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);
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);
return powerHasChanged;
}
ANNOUNCE_STATIC void announceRadioMode(bool voicePromptWasPlaying)
{
if (!voicePromptWasPlaying)
{
voicePromptsAppendLanguageString(&currentLanguage->mode);
}
voicePromptsAppendPrompt( (trxGetMode() == RADIO_MODE_DIGITAL) ? PROMPT_DMR : PROMPT_FM);
}
ANNOUNCE_STATIC void announceZoneName(bool voicePromptWasPlaying)
{
if (!voicePromptWasPlaying)
{
voicePromptsAppendLanguageString(&currentLanguage->zone);
}
voicePromptsAppendString(currentZone.name);
}
ANNOUNCE_STATIC void announceContactNameTgOrPc(bool voicePromptWasPlaying)
{
if (nonVolatileSettings.overrideTG == 0)
{
if (!voicePromptWasPlaying)
{
voicePromptsAppendLanguageString(&currentLanguage->contact);
}
char nameBuf[17];
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)
{
int powerLevel = trxGetPowerLevel();
if (!voicePromptWasPlaying)
{
voicePromptsAppendPrompt(PROMPT_POWER);
}
if (powerLevel < 9)
{
voicePromptsAppendString((char *)POWER_LEVELS[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);
}
}
ANNOUNCE_STATIC void announceTemperature(bool voicePromptWasPlaying)
{
char buffer[17];
int temperature = getTemperature();
if (!voicePromptWasPlaying)
{
voicePromptsAppendLanguageString(&currentLanguage->temperature);
}
snprintf(buffer, 17, "%d.%1d", (temperature / 10), (temperature % 10));
voicePromptsAppendString(buffer);
voicePromptsAppendLanguageString(&currentLanguage->celcius);
}
ANNOUNCE_STATIC void announceBatteryVoltage(void)
{
char buffer[17];
int volts, mvolts;
voicePromptsAppendLanguageString(&currentLanguage->battery);
getBatteryVoltage(&volts, &mvolts);
snprintf(buffer, 17, " %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)
{
voicePromptsAppendPrompt(PROMPT_TIMESLOT);
voicePromptsAppendInteger(trxGetDMRTimeSlot() + 1);
}
ANNOUNCE_STATIC void announceCC(void)
{
voicePromptsAppendLanguageString(&currentLanguage->colour_code);
voicePromptsAppendInteger(trxGetDMRColourCode());
}
ANNOUNCE_STATIC void announceChannelName(bool voicePromptWasPlaying)
{
char voiceBuf[17];
codeplugUtilConvertBufToString(channelScreenChannelData.name, voiceBuf, 16);
if (!voicePromptWasPlaying)
{
voicePromptsAppendPrompt(PROMPT_CHANNEL);
}
voicePromptsAppendString(voiceBuf);
}
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;
}
}
}
ANNOUNCE_STATIC void announceFrequency(void)
{
char buffer[17];
bool duplex = (currentChannelData->txFreq != currentChannelData->rxFreq);
if (duplex)
{
voicePromptsAppendPrompt(PROMPT_RECEIVE);
}
int val_before_dp = currentChannelData->rxFreq / 100000;
int val_after_dp = currentChannelData->rxFreq - val_before_dp * 100000;
snprintf(buffer, 17, "%d.%05d", val_before_dp, val_after_dp);
removeUnnecessaryZerosFromVoicePrompts(buffer);
voicePromptsAppendString(buffer);
voicePromptsAppendPrompt(PROMPT_MEGAHERTZ);
if (duplex)
{
voicePromptsAppendPrompt(PROMPT_TRANSMIT);
val_before_dp = currentChannelData->txFreq / 100000;
val_after_dp = currentChannelData->txFreq - val_before_dp * 100000;
snprintf(buffer, 17, "%d.%05d", val_before_dp, val_after_dp);
removeUnnecessaryZerosFromVoicePrompts(buffer);
voicePromptsAppendString(buffer);
voicePromptsAppendPrompt(PROMPT_MEGAHERTZ);
}
}
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[trxCurrentBand[TRX_RX_FREQ_BAND]] : currentChannelData->sql)-1));
voicePromptsAppendString(buf);
}
void announceChar(char ch)
{
if (nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_LEVEL_1)
{
return;
}
char buf[2] = {ch, 0};
voicePromptsInit();
voicePromptsAppendString(buf);
voicePromptsPlay();
}
void buildCSSCodeVoicePrompts(uint16_t code, CSSTypes_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);
switch (cssType)
{
case CSS_NONE:
voicePromptsAppendString("CSS");
voicePromptsAppendPrompt(PROMPT_SILENCE);
voicePromptsAppendLanguageString(&currentLanguage->none);
break;
case CSS_CTCSS:
if (announceType)
{
voicePromptsAppendString("CTCSS");
voicePromptsAppendPrompt(PROMPT_SILENCE);
}
snprintf(buf, BUFFER_LEN, "%d.%d", code / 10, code % 10);
voicePromptsAppendString(buf);
voicePromptsAppendPrompt(PROMPT_HERTZ);
break;
case CSS_DCS:
case CSS_DCS_INVERTED:
if (announceType)
{
voicePromptsAppendString("DCS");
voicePromptsAppendPrompt(PROMPT_SILENCE);
}
snprintf(buf, BUFFER_LEN, "D%03o%c", code & 0777, (code & CODEPLUG_DCS_INVERTED_MASK) ? 'I' : 'N');
voicePromptsAppendString(buf);
break;
}
}
void announceCSSCode(uint16_t code, CSSTypes_t cssType, Direction_t direction, bool announceType, audioPromptThreshold_t immediateAnnounceThreshold)
{
if (nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_LEVEL_1)
{
return;
}
bool voicePromptWasPlaying = voicePromptsIsPlaying();
voicePromptsInit();
buildCSSCodeVoicePrompts(code, 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();
}
}
void playNextSettingSequence(void)
{
voicePromptSequenceState++;
if (voicePromptSequenceState == NUM_PROMPT_SEQUENCES)
{
voicePromptSequenceState = 0;
}
announceItem(voicePromptSequenceState, PROMPT_THRESHOLD_3);
}
static void announceChannelNameOrVFOFrequency(bool voicePromptWasPlaying, bool announceVFOName)
{
if (menuSystemGetCurrentMenuNumber() == UI_CHANNEL_MODE)
{
announceChannelName(voicePromptWasPlaying);
}
else
{
announceVFOAndFrequency(announceVFOName);
}
}
void announceItem(voicePromptItem_t item, audioPromptThreshold_t immediateAnnounceThreshold)
{
if (nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_LEVEL_1)
{
return;
}
bool voicePromptWasPlaying = voicePromptsIsPlaying();
voicePromptSequenceState = item;
voicePromptsInit();
switch(voicePromptSequenceState)
{
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_VFO_FREQ_UPDATE:
announceChannelNameOrVFOFrequency(voicePromptWasPlaying, (voicePromptSequenceState != PROMPT_SEQUENCE_VFO_FREQ_UPDATE));
if (voicePromptSequenceState == PROMPT_SEQUENCE_CHANNEL_NAME_OR_VFO_FREQ)
{
break;
}
if (voicePromptSequenceState == PROMPT_SEQUENCE_VFO_FREQ_UPDATE)
{
announceVFOChannelName();
}
announceRadioMode(voicePromptWasPlaying);
if (voicePromptSequenceState == PROMPT_SEQUENCE_CHANNEL_NAME_OR_VFO_FREQ_AND_MODE)
{
break;
}
if (trxGetMode() == RADIO_MODE_DIGITAL)
{
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 (nonVolatileSettings.bitfieldOptions & BIT_BATTERY_VOLTAGE_IN_HEADER)
{
announceBatteryVoltage();
}
else
{
announceBatteryPercentage();
}
break;
case PROMPT_SQUENCE_SQUELCH:
announceSquelchLevel(voicePromptWasPlaying);
break;
case PROMPT_SEQUENCE_TEMPERATURE:
announceTemperature(voicePromptWasPlaying);
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 promptsPlayNotAfterTx(void)
{
if (menuSystemGetPreviouslyPushedMenuNumber() != UI_TX_SCREEN)
{
voicePromptsPlay();
}
}
void uiUtilityBuildTgOrPCDisplayName(char *nameBuf, int bufferLen)
{
int contactIndex;
struct_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)
{
snprintf(nameBuf, bufferLen, "%s %d", currentLanguage->tg, (trxTalkGroupOrPcId & 0x00FFFFFF));
}
else
{
codeplugUtilConvertBufToString(contact.name, nameBuf, 16);
}
}
else
{
contactIndex = codeplugContactIndexByTGorPC(id, CONTACT_CALLTYPE_PC, &contact, (manTS ? manTS : (trxGetDMRTimeSlot() + 1)));
if (contactIndex == -1)
{
dmrIdDataStruct_t currentRec;
if (dmrIDLookup(id, &currentRec))
{
strncpy(nameBuf, currentRec.text, bufferLen);
}
else
{
// check LastHeard for TA data.
LinkItem_t *item = lastheardFindInList(id);
if ((item != NULL) && (strlen(item->talkerAlias) != 0))
{
strncpy(nameBuf, item->talkerAlias, bufferLen);
}
else
{
snprintf(nameBuf, bufferLen, "ID:%d", id);
}
}
}
else
{
codeplugUtilConvertBufToString(contact.name, nameBuf, 16);
}
}
}
void acceptPrivateCall(int 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);
tsSetManualOverride(((menuSystemGetRootMenuNumber() == UI_CHANNEL_MODE) ? CHANNEL_CHANNEL : (CHANNEL_VFO_A + nonVolatileSettings.currentVFONumber)), (timeslot + 1));
}
#else
(void)timeslot;
#endif
announceItem(PROMPT_SEQUENCE_CONTACT_TG_OR_PC,PROMPT_THRESHOLD_3);
}
bool repeatVoicePromptOnSK1(uiEvent_t *ev)
{
if (BUTTONCHECK_SHORTUP(ev, BUTTON_SK1) && (ev->keys.key == 0))
{
if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_LEVEL_1)
{
int currentMenu = menuSystemGetCurrentMenuNumber();
if ((((currentMenu == UI_CHANNEL_MODE) && (uiDataGlobal.Scan.active && (uiDataGlobal.Scan.state != SCAN_PAUSED))) ||
((currentMenu == UI_VFO_MODE) && ((uiDataGlobal.Scan.active && (uiDataGlobal.Scan.state != SCAN_PAUSED)) || uiDataGlobal.Scan.toneActive))) == false)
{
if (!voicePromptsIsPlaying())
{
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 = 250;// Normally it seems to take about 125mS to detect DMR even if the CC is incorrect.
if (monitorModeData.isEnabled)
{
#if defined(PLATFORM_GD77S)
// PLATFORM_GD77S
if ((BUTTONCHECK_DOWN(ev, BUTTON_SK1) == false) || (BUTTONCHECK_DOWN(ev, BUTTON_SK2) == false) || BUTTONCHECK_DOWN(ev, BUTTON_ORANGE) || (ev->events & ROTARY_EVENT))
#else
if ((BUTTONCHECK_DOWN(ev, BUTTON_SK2) == false) || (ev->keys.key != 0) || BUTTONCHECK_DOWN(ev, BUTTON_SK1)
#if !defined(PLATFORM_RD5R)
|| BUTTONCHECK_DOWN(ev, BUTTON_ORANGE)
#endif
)
#endif
{
bool wasRadioModeWasChanged = (trxGetMode() != monitorModeData.savedRadioMode);
if (wasRadioModeWasChanged)
{
trxSetModeAndBandwidth(currentChannelData->chMode, ((currentChannelData->flag4 & 0x02) == 0x02));
currentChannelData->sql = monitorModeData.savedSquelch;
}
switch (monitorModeData.savedRadioMode)
{
case RADIO_MODE_ANALOG:
currentChannelData->sql = monitorModeData.savedSquelch;
trxSetRxCSS(currentChannelData->rxTone);
break;
case RADIO_MODE_DIGITAL:
nonVolatileSettings.dmrCcTsFilter = monitorModeData.savedDMRCcTsFilter;
nonVolatileSettings.dmrDestinationFilter = monitorModeData.savedDMRDestinationFilter;
trxSetDMRColourCode(monitorModeData.savedDMRCc);
trxSetDMRTimeSlot(monitorModeData.savedDMRTs);
break;
}
monitorModeData.isEnabled = false;
headerRowIsDirty = true;
return true;
}
}
else
{
#if defined(PLATFORM_GD77S)
if (BUTTONCHECK_LONGDOWN(ev, BUTTON_SK1) && BUTTONCHECK_LONGDOWN(ev, BUTTON_SK2))
{
if (voicePromptsIsPlaying())
{
voicePromptsTerminate();
}
#else
if (BUTTONCHECK_EXTRALONGDOWN(ev, BUTTON_SK2))
{
#endif
monitorModeData.savedRadioMode = trxGetMode();
monitorModeData.savedSquelch = currentChannelData->sql;
switch (monitorModeData.savedRadioMode)
{
case RADIO_MODE_ANALOG:
monitorModeData.savedSquelch = currentChannelData->sql;
currentChannelData->sql = CODEPLUG_MIN_VARIABLE_SQUELCH;
trxSetRxCSS(CODEPLUG_CSS_NONE);
break;
case RADIO_MODE_DIGITAL:
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;
break;
}
monitorModeData.isEnabled = true;
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 = (int *)MELODY_ACK_BEEP;
}
return true;
}
nextKeyBeepMelody = (int *)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)
int cssIndex(uint16_t tone, CSSTypes_t type)
{
switch (type)
{
case CSS_CTCSS:
for (int i = 0; i < TRX_NUM_CTCSS; i++)
{
if (TRX_CTCSSTones[i] >= tone)
{
return i;
}
}
break;
case CSS_DCS:
case CSS_DCS_INVERTED:
tone &= 0777;
for (int i = 0; i < TRX_NUM_DCS; i++)
{
if (TRX_DCSCodes[i] >= tone)
{
return i;
}
}
break;
case CSS_NONE:
break;
}
return 0;
}
uint16_t cssGetTone(int32_t index, CSSTypes_t type)
{
if (index >= 0)
{
switch (type)
{
case CSS_CTCSS:
if (index < TRX_NUM_CTCSS)
{
return TRX_CTCSSTones[index];
}
break;
case CSS_DCS:
if (index < TRX_NUM_DCS)
{
return (TRX_DCSCodes[index] | 0x8000);
}
break;
case CSS_DCS_INVERTED:
if (index < TRX_NUM_DCS)
{
return (TRX_DCSCodes[index] | 0xC000);
}
break;
case CSS_NONE:
break;
}
}
return TRX_CTCSSTones[0];
}
void cssIncrement(uint16_t *tone, int32_t *index, CSSTypes_t *type, bool loop, bool stayInCSSType)
{
(*index)++;
switch (*type)
{
case CSS_CTCSS:
if (*index >= TRX_NUM_CTCSS)
{
if (stayInCSSType)
{
*index = 0;
}
else
{
*type = CSS_DCS;
*index = 0;
*tone = TRX_DCSCodes[*index] | 0x8000;
return;
}
}
*tone = TRX_CTCSSTones[*index];
break;
case CSS_DCS:
if (*index >= TRX_NUM_DCS)
{
if (stayInCSSType)
{
*index = 0;
}
else
{
*type = CSS_DCS_INVERTED;
*index = 0;
*tone = TRX_DCSCodes[*index] | 0xC000;
return;
}
}
*tone = TRX_DCSCodes[*index] | 0x8000;
break;
case CSS_DCS_INVERTED:
if (*index >= TRX_NUM_DCS)
{
if (stayInCSSType)
{
*index = 0;
}
else
{
if (loop)
{
*type = CSS_CTCSS;
*index = 0;
*tone = TRX_CTCSSTones[*index];
return;
}
*index = TRX_NUM_DCS - 1;
}
}
*tone = TRX_DCSCodes[*index] | 0xC000;
break;
case CSS_NONE:
*type = CSS_CTCSS;
*index = 0;
*tone = TRX_CTCSSTones[*index];
break;
}
return;
}
void cssDecrement(uint16_t *tone, int32_t *index, CSSTypes_t *type)
{
(*index)--;
switch (*type)
{
case CSS_CTCSS:
if (*index < 0)
{
*type = CSS_NONE;
*index = 0;
*tone = CODEPLUG_CSS_NONE;
return;
}
*tone = TRX_CTCSSTones[*index];
break;
case CSS_DCS:
if (*index < 0)
{
*type = CSS_CTCSS;
*index = TRX_NUM_CTCSS - 1;
*tone = TRX_CTCSSTones[*index];
return;
}
*tone = TRX_DCSCodes[*index] | 0x8000;
break;
case CSS_DCS_INVERTED:
if (*index < 0)
{
*type = CSS_DCS;
*index = (TRX_NUM_DCS - 1);
*tone = TRX_DCSCodes[*index] | 0x8000;
return;
}
*tone = TRX_DCSCodes[*index] | 0xC000;
break;
case CSS_NONE:
*index = 0;
*tone = CODEPLUG_CSS_NONE;
break;
}
}
bool uiShowQuickKeysChoices(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);
ucDrawChoice(CHOICES_OKARROWS, true);
if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_LEVEL_1)
{
voicePromptsInit();
voicePromptsAppendLanguageString(&currentLanguage->set_quickkey);
voicePromptsAppendPrompt(PROMPT_0 + (menuDataGlobal.menuOptionsSetQuickkey - '0'));
}
}
else if (settingOption == false)
{
menuDisplayTitle(menuTitle);
}
return settingOption;
}
// --- 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 * 100) : (uiDataGlobal.DTMFContactList.durations.fstDur * 100)) + 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 * 100) : 0) + duration);
}
static void dtmfProcess(void)
{
if (uiDataGlobal.DTMFContactList.poLen == 0U)
{
return;
}
if (PITCounter > uiDataGlobal.DTMFContactList.nextPeriod)
{
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]);
trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_DTMF);
}
else
{
trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_NONE);
}
uiDataGlobal.DTMFContactList.inTone = !uiDataGlobal.DTMFContactList.inTone;
}
else
{
// Pause after last digit
if (uiDataGlobal.DTMFContactList.inTone)
{
uiDataGlobal.DTMFContactList.inTone = false;
trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_NONE);
uiDataGlobal.DTMFContactList.nextPeriod = PITCounter + ((duration + (uiDataGlobal.DTMFContactList.durations.libreDMR_Tail * 100)) * 10U);
return;
}
}
if (uiDataGlobal.DTMFContactList.inTone)
{
// Move forward in the sequence, set tone duration
uiDataGlobal.DTMFContactList.nextPeriod = PITCounter + (dtmfGetToneDuration(duration) * 10U);
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
uiDataGlobal.DTMFContactList.nextPeriod = PITCounter + (duration * 10U);
}
}
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;
}
void dtmfSequenceTick(bool popPreviousMenuOnEnding)
{
if (uiDataGlobal.DTMFContactList.isKeying)
{
if (!trxTransmissionEnabled)
{
// Start TX DTMF, prepare for ANALOG
if (trxGetMode() != RADIO_MODE_ANALOG)
{
trxSetModeAndBandwidth(RADIO_MODE_ANALOG, false);
trxSetTxCSS(CODEPLUG_CSS_NONE);
}
trxEnableTransmission();
trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_NONE);
enableAudioAmp(AUDIO_AMP_MODE_RF);
GPIO_PinWrite(GPIO_RX_audio_mux, Pin_RX_audio_mux, 1);
uiDataGlobal.DTMFContactList.inTone = false;
uiDataGlobal.DTMFContactList.nextPeriod = PITCounter + ((uiDataGlobal.DTMFContactList.durations.fstDigitDly * 100) * 10U); // Sequence preamble
}
// DTMF has been TXed, restore DIGITAL/ANALOG
if (uiDataGlobal.DTMFContactList.poLen == 0U)
{
trxDisableTransmission();
if (trxTransmissionEnabled)
{
// Stop TXing;
trxTransmissionEnabled = false;
trxSetRX();
LEDs_PinWrite(GPIO_LEDgreen, Pin_LEDgreen, 0);
trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_MIC);
disableAudioAmp(AUDIO_AMP_MODE_RF);
if (currentChannelData->chMode == RADIO_MODE_ANALOG)
{
trxSetModeAndBandwidth(currentChannelData->chMode, ((currentChannelData->flag4 & 0x02) == 0x02));
trxSetTxCSS(currentChannelData->txTone);
}
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)
{
originalNonVolatileSettings.magicNumber = 0xDEADBEEF;
}