kopia lustrzana https://github.com/open-ham/OpenGD77
2835 wiersze
77 KiB
C
2835 wiersze
77 KiB
C
/*
|
||
* 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, ¤tRec);
|
||
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, ¤tRec) == 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, ¤tRec);
|
||
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), ¤tRec);
|
||
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), ¤tRec) == 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(¤tLanguage->mode);
|
||
}
|
||
voicePromptsAppendPrompt( (trxGetMode() == RADIO_MODE_DIGITAL) ? PROMPT_DMR : PROMPT_FM);
|
||
}
|
||
|
||
ANNOUNCE_STATIC void announceZoneName(bool voicePromptWasPlaying)
|
||
{
|
||
if (!voicePromptWasPlaying)
|
||
{
|
||
voicePromptsAppendLanguageString(¤tLanguage->zone);
|
||
}
|
||
voicePromptsAppendString(currentZone.name);
|
||
}
|
||
|
||
ANNOUNCE_STATIC void announceContactNameTgOrPc(bool voicePromptWasPlaying)
|
||
{
|
||
if (nonVolatileSettings.overrideTG == 0)
|
||
{
|
||
if (!voicePromptWasPlaying)
|
||
{
|
||
voicePromptsAppendLanguageString(¤tLanguage->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(¤tLanguage->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(¤tLanguage->user_power);
|
||
}
|
||
}
|
||
|
||
ANNOUNCE_STATIC void announceTemperature(bool voicePromptWasPlaying)
|
||
{
|
||
char buffer[17];
|
||
int temperature = getTemperature();
|
||
if (!voicePromptWasPlaying)
|
||
{
|
||
voicePromptsAppendLanguageString(¤tLanguage->temperature);
|
||
}
|
||
snprintf(buffer, 17, "%d.%1d", (temperature / 10), (temperature % 10));
|
||
voicePromptsAppendString(buffer);
|
||
voicePromptsAppendLanguageString(¤tLanguage->celcius);
|
||
}
|
||
|
||
ANNOUNCE_STATIC void announceBatteryVoltage(void)
|
||
{
|
||
char buffer[17];
|
||
int volts, mvolts;
|
||
|
||
voicePromptsAppendLanguageString(¤tLanguage->battery);
|
||
getBatteryVoltage(&volts, &mvolts);
|
||
snprintf(buffer, 17, " %1d.%1d", volts, mvolts);
|
||
voicePromptsAppendString(buffer);
|
||
voicePromptsAppendPrompt(PROMPT_VOLTS);
|
||
}
|
||
|
||
ANNOUNCE_STATIC void announceBatteryPercentage(void)
|
||
{
|
||
voicePromptsAppendLanguageString(¤tLanguage->battery);
|
||
voicePromptsAppendInteger(getBatteryPercentage());
|
||
voicePromptsAppendPrompt(PROMPT_PERCENT);
|
||
}
|
||
|
||
ANNOUNCE_STATIC void announceTS(void)
|
||
{
|
||
voicePromptsAppendPrompt(PROMPT_TIMESLOT);
|
||
voicePromptsAppendInteger(trxGetDMRTimeSlot() + 1);
|
||
}
|
||
|
||
ANNOUNCE_STATIC void announceCC(void)
|
||
{
|
||
voicePromptsAppendLanguageString(¤tLanguage->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(¤tLanguage->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(¤tLanguage->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, ¤tRec))
|
||
{
|
||
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(¤tLanguage->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;
|
||
}
|