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;
|
|||
|
}
|