kopia lustrzana https://github.com/OpenRTX/OpenRTX
1106 wiersze
29 KiB
C
1106 wiersze
29 KiB
C
/***************************************************************************
|
|
* Copyright (C) 2022 - 2023 by Federico Amedeo Izzo IU2NUO, *
|
|
* Niccolò Izzo IU2KIN, *
|
|
* Silvano Seva IU2KWO *
|
|
* Joseph Stephen VK7JS *
|
|
* *
|
|
* 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 3 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, see <http://www.gnu.org/licenses/> *
|
|
***************************************************************************/
|
|
|
|
/**
|
|
* This file contains functions for announcing radio functions using the
|
|
* building blocks in voicePrompts.h/c.
|
|
*/
|
|
|
|
#include "core/voicePromptUtils.h"
|
|
|
|
#include <state.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <utils.h>
|
|
#include <inttypes.h>
|
|
#include <ui/ui_default.h>
|
|
#include <beeps.h>
|
|
#include "interfaces/cps_io.h"
|
|
|
|
const uint16_t BOOT_MELODY[] = {400, 3, 600, 3, 800, 3, 0, 0};
|
|
|
|
static void clearCurrPromptIfNeeded(const vpQueueFlags_t flags)
|
|
{
|
|
if (flags & vpqInit)
|
|
vp_flush();
|
|
}
|
|
|
|
static void playIfNeeded(const vpQueueFlags_t flags)
|
|
{
|
|
uint8_t vpLevel = state.settings.vpLevel;
|
|
|
|
if ((flags & vpqPlayImmediately) ||
|
|
((flags & vpqPlayImmediatelyAtMediumOrHigher) && (vpLevel >= vpMedium)))
|
|
vp_play();
|
|
}
|
|
|
|
static void addSilenceIfNeeded(const vpQueueFlags_t flags)
|
|
{
|
|
if ((flags & vpqAddSeparatingSilence) == 0)
|
|
return;
|
|
|
|
vp_queuePrompt(PROMPT_SILENCE);
|
|
vp_queuePrompt(PROMPT_SILENCE);
|
|
}
|
|
|
|
|
|
|
|
void vp_announceChannelName(const channel_t* channel,
|
|
const uint16_t channelNumber,
|
|
const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_CHANNEL);
|
|
}
|
|
|
|
vp_queueInteger(channelNumber);
|
|
|
|
// Only queue the name if it is not the same as the raw number.
|
|
// Otherwise the radio will repeat channel 1 channel 1 for channel 1.
|
|
char numAsStr[16] = "\0";
|
|
sniprintf(numAsStr, 16, "Channel%d", channelNumber);
|
|
|
|
if (strcmp(numAsStr, channel->name) != 0)
|
|
{
|
|
vp_queueString(channel->name, vpAnnounceCommonSymbols);
|
|
}
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_queueFrequency(const freq_t freq)
|
|
{
|
|
char buffer[16];
|
|
int MHz = (freq / 1000000);
|
|
int kHz = ((freq % 1000000) / 100);
|
|
|
|
sniprintf(buffer, 16, "%d.%05d", MHz, kHz);
|
|
|
|
stripTrailingZeroes(buffer);
|
|
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols);
|
|
vp_queuePrompt(PROMPT_MEGAHERTZ);
|
|
}
|
|
|
|
void vp_announceFrequencies(const freq_t rx, const freq_t tx,
|
|
const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
// If rx and tx frequencies differ, announce both, otherwise just one
|
|
if (rx == tx)
|
|
{
|
|
vp_queueFrequency(rx);
|
|
}
|
|
else
|
|
{
|
|
vp_queuePrompt(PROMPT_RECEIVE);
|
|
vp_queueFrequency(rx);
|
|
vp_queuePrompt(PROMPT_TRANSMIT);
|
|
vp_queueFrequency(tx);
|
|
}
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceRadioMode(const uint8_t mode, const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_MODE);
|
|
}
|
|
|
|
switch (mode)
|
|
{
|
|
case OPMODE_DMR:
|
|
vp_queueStringTableEntry(¤tLanguage->dmr);
|
|
break;
|
|
|
|
case OPMODE_FM:
|
|
vp_queueStringTableEntry(¤tLanguage->fm);
|
|
break;
|
|
|
|
case OPMODE_M17:
|
|
vp_queueStringTableEntry(¤tLanguage->m17);
|
|
break;
|
|
}
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceBandwidth(const uint8_t bandwidth, const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_BANDWIDTH);
|
|
}
|
|
|
|
char* bandwidths[] = {"12.5", "25"};
|
|
vp_queueString(bandwidths[bandwidth], vpAnnounceCommonSymbols);
|
|
vp_queuePrompt(PROMPT_KILOHERTZ);
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announcePower(const uint32_t power, const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_POWER);
|
|
}
|
|
|
|
// Compute x.y format avoiding to pull in floating point math.
|
|
// Remember that power is expressed in mW!
|
|
char buffer[16] = "\0";
|
|
sniprintf(buffer, 16, "%lu.%lu", (power / 1000lu), (power % 1000lu) / 100lu);
|
|
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols);
|
|
vp_queuePrompt(PROMPT_WATTS);
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceChannelSummary(const channel_t* channel,
|
|
const uint16_t channelNumber, const uint16_t bank,
|
|
const vpSummaryInfoFlags_t infoFlags)
|
|
{
|
|
if (channel == NULL)
|
|
return;
|
|
|
|
vp_flush();
|
|
|
|
vpQueueFlags_t localFlags = vpqAddSeparatingSilence;
|
|
|
|
// Force on the descriptions for level 3.
|
|
if (state.settings.vpLevel == vpHigh)
|
|
{
|
|
localFlags |= vpqIncludeDescriptions;
|
|
}
|
|
|
|
// If VFO mode, announce VFO.
|
|
// channelNumber will be 0 if called from VFO mode.
|
|
if ((infoFlags & vpChannelNameOrVFO) != 0)
|
|
{
|
|
if (channelNumber == 0)
|
|
{
|
|
vp_queuePrompt(PROMPT_VFO);
|
|
}
|
|
else
|
|
{
|
|
vp_announceChannelName(channel, channelNumber, localFlags);
|
|
}
|
|
addSilenceIfNeeded(localFlags);
|
|
}
|
|
|
|
if ((infoFlags & vpFrequencies) != 0)
|
|
vp_announceFrequencies(channel->rx_frequency, channel->tx_frequency,
|
|
localFlags);
|
|
|
|
if ((infoFlags & vpRadioMode) != 0)
|
|
{
|
|
vp_announceRadioMode(channel->mode, localFlags);
|
|
addSilenceIfNeeded(localFlags);
|
|
}
|
|
|
|
if ((infoFlags & vpModeSpecificInfo) != 0)
|
|
{
|
|
switch(channel->mode)
|
|
{
|
|
case OPMODE_FM:
|
|
{
|
|
vp_announceBandwidth(channel->bandwidth, localFlags);
|
|
addSilenceIfNeeded(localFlags);
|
|
|
|
if (channel->fm.rxToneEn || channel->fm.txToneEn)
|
|
{
|
|
vp_announceCTCSS(channel->fm.rxToneEn, channel->fm.rxTone,
|
|
channel->fm.txToneEn, channel->fm.txTone,
|
|
localFlags);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case OPMODE_M17:
|
|
vp_announceM17Info(channel, false, localFlags);
|
|
break;
|
|
|
|
case OPMODE_DMR:
|
|
{
|
|
vp_announceContactWithIndex(channel->dmr.contact_index,
|
|
localFlags);
|
|
|
|
// Force announcement of the words timeslot and colorcode to avoid
|
|
// ambiguity.
|
|
vp_announceTimeslot(channel->dmr.dmr_timeslot,
|
|
(localFlags | vpqIncludeDescriptions));
|
|
vp_announceColorCode(channel->dmr.rxColorCode,
|
|
channel->dmr.txColorCode,
|
|
(localFlags | vpqIncludeDescriptions));
|
|
}
|
|
break;
|
|
}
|
|
|
|
addSilenceIfNeeded(localFlags);
|
|
}
|
|
|
|
if ((infoFlags & vpPower) != 0)
|
|
{
|
|
vp_announcePower(channel->power, localFlags);
|
|
addSilenceIfNeeded(localFlags);
|
|
}
|
|
|
|
if (((infoFlags & vpBankNameOrAllChannels) != 0) && (channelNumber > 0)) // i.e. not called from VFO.
|
|
{
|
|
vp_announceBank(bank, localFlags);
|
|
}
|
|
|
|
vp_play();
|
|
}
|
|
|
|
void vp_announceInputChar(const char ch)
|
|
{
|
|
char buf[2] = "\0";
|
|
buf[0] = ch;
|
|
|
|
vp_flush();
|
|
|
|
uint8_t flags = vpAnnounceSpace
|
|
| vpAnnounceCommonSymbols
|
|
| vpAnnounceLessCommonSymbols;
|
|
|
|
vp_queueString(buf, flags);
|
|
vp_play();
|
|
}
|
|
|
|
void vp_announceInputReceiveOrTransmit(const bool tx, const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (tx)
|
|
vp_queuePrompt(PROMPT_TRANSMIT);
|
|
else
|
|
vp_queuePrompt(PROMPT_RECEIVE);
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_replayLastPrompt()
|
|
{
|
|
if (vp_isPlaying())
|
|
vp_stop();
|
|
else
|
|
vp_play();
|
|
}
|
|
|
|
void vp_announceError(const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
vp_queueStringTableEntry(¤tLanguage->error);
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceText(const char* text, const vpQueueFlags_t flags)
|
|
{
|
|
if ((text == NULL) || (*text == '\0'))
|
|
return;
|
|
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
// See if we have a prompt for this string.
|
|
int offset = GetEnglishStringTableOffset(text);
|
|
|
|
if (offset != -1)
|
|
vp_queueStringTableEntry(
|
|
(const char* const*)(¤tLanguage->languageName + offset));
|
|
else // Just spell it out
|
|
vp_queueString(text, vpAnnounceCommonSymbols);
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceCTCSS(const bool rxToneEnabled, const uint8_t rxTone,
|
|
const bool txToneEnabled, const uint8_t txTone,
|
|
const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if ((rxToneEnabled == false) && (txToneEnabled == false))
|
|
{
|
|
if (flags & vpqIncludeDescriptions)
|
|
vp_queuePrompt(PROMPT_TONE);
|
|
|
|
vp_queueStringTableEntry(¤tLanguage->off);
|
|
playIfNeeded(flags);
|
|
return;
|
|
}
|
|
|
|
char buffer[16] = "\0";
|
|
|
|
// If the rx and tx tones are the same and both are enabled, just say Tone.
|
|
if ((rxToneEnabled && txToneEnabled) && (rxTone == txTone))
|
|
{
|
|
if (flags & vpqIncludeDescriptions)
|
|
vp_queuePrompt(PROMPT_TONE);
|
|
|
|
uint16_t tone = ctcss_tone[rxTone];
|
|
sniprintf(buffer, sizeof(buffer), "%d.%d", (tone / 10), (tone % 10));
|
|
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols);
|
|
vp_queuePrompt(PROMPT_HERTZ);
|
|
playIfNeeded(flags);
|
|
|
|
return;
|
|
}
|
|
|
|
// Speak the individual rx and tx tones.
|
|
if (rxToneEnabled)
|
|
{
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_RECEIVE);
|
|
vp_queuePrompt(PROMPT_TONE);
|
|
}
|
|
|
|
uint16_t tone = ctcss_tone[rxTone];
|
|
sniprintf(buffer, sizeof(buffer), "%d.%d", (tone / 10), (tone % 10));
|
|
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols);
|
|
vp_queuePrompt(PROMPT_HERTZ);
|
|
}
|
|
if (txToneEnabled)
|
|
{
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_TRANSMIT);
|
|
vp_queuePrompt(PROMPT_TONE);
|
|
}
|
|
|
|
uint16_t tone = ctcss_tone[txTone];
|
|
sniprintf(buffer, sizeof(buffer), "%d.%d", (tone / 10), (tone % 10));
|
|
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols);
|
|
vp_queuePrompt(PROMPT_HERTZ);
|
|
}
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceSquelch(const uint8_t squelch, const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_SQUELCH);
|
|
}
|
|
|
|
vp_queueInteger(squelch);
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceContact(const contact_t* contact, const vpQueueFlags_t flags)
|
|
{
|
|
if (contact == NULL)
|
|
return;
|
|
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_CONTACT);
|
|
}
|
|
|
|
if (contact->name[0] != '\0')
|
|
{
|
|
vp_queueString(contact->name, vpAnnounceCommonSymbols);
|
|
}
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
bool vp_announceContactWithIndex(const uint16_t index, const vpQueueFlags_t flags)
|
|
{
|
|
if (index == 0)
|
|
return false;
|
|
|
|
contact_t contact;
|
|
if (cps_readContact(&contact, index) == -1)
|
|
return false;
|
|
|
|
vp_announceContact(&contact, flags);
|
|
|
|
return true;
|
|
}
|
|
|
|
void vp_announceTimeslot(const uint8_t timeslot, const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_TIMESLOT);
|
|
}
|
|
|
|
vp_queueInteger(timeslot);
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceColorCode(const uint8_t rxColorCode, const uint8_t txColorCode,
|
|
const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_COLORCODE);
|
|
}
|
|
|
|
if (rxColorCode == txColorCode)
|
|
{
|
|
vp_queueInteger(rxColorCode);
|
|
}
|
|
else
|
|
{
|
|
vp_queuePrompt(PROMPT_RECEIVE);
|
|
vp_queueInteger(rxColorCode);
|
|
vp_queuePrompt(PROMPT_TRANSMIT);
|
|
vp_queueInteger(txColorCode);
|
|
}
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceBank(const uint16_t bank, const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (state.bank_enabled)
|
|
{
|
|
bankHdr_t bank_hdr = {0};
|
|
cps_readBankHeader(&bank_hdr, bank);
|
|
vp_queueString(bank_hdr.name, vpAnnounceCommonSymbols);
|
|
}
|
|
else
|
|
{
|
|
vp_queueStringTableEntry(¤tLanguage->allChannels);
|
|
}
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceM17Info(const channel_t* channel, bool isEditing,
|
|
const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_DEST_ID);
|
|
}
|
|
|
|
if (isEditing)
|
|
{
|
|
vp_queuePrompt(PROMPT_EDIT);
|
|
}
|
|
else if (state.settings.m17_dest[0] != '\0')
|
|
{
|
|
vp_queueString(state.settings.m17_dest, vpAnnounceCommonSymbols);
|
|
}
|
|
else if ((channel != NULL) && (channel->m17.contact_index != 0))
|
|
{
|
|
if (!vp_announceContactWithIndex(channel->m17.contact_index, flags))
|
|
vp_queueStringTableEntry(¤tLanguage->broadcast);
|
|
}
|
|
else
|
|
{
|
|
vp_queueStringTableEntry(¤tLanguage->broadcast);
|
|
}
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
#ifdef CONFIG_GPS
|
|
// cardinal point plus or minus this value is still considered cardinal point.
|
|
#define margin 3
|
|
|
|
static bool IsCompassCloseEnoughToCardinalPoint()
|
|
{
|
|
float tmg_true = state.gps_data.tmg_true;
|
|
|
|
return (tmg_true < (0 + margin) || tmg_true > (360 - margin)) || // north
|
|
(tmg_true > (90 - margin) && tmg_true < (90 + margin)) || // east
|
|
(tmg_true > (180 - margin) && tmg_true < (180 + margin)) || // south
|
|
(tmg_true > (270 - margin) && tmg_true < (270 + margin)) || // west
|
|
(tmg_true > (45 - margin) && tmg_true < (45 + margin)) || // n.w.
|
|
(tmg_true > (135 - margin) && tmg_true < (135 + margin)) || // s.e.
|
|
(tmg_true > (225 - margin) && tmg_true < (225 + margin)) || // s.w.
|
|
(tmg_true > (315 - margin) && tmg_true < (315 + margin)); // n.w.
|
|
}
|
|
|
|
void vp_announceGPSInfo(vpGPSInfoFlags_t gpsInfoFlags)
|
|
{
|
|
vp_flush();
|
|
vpQueueFlags_t flags = vpqIncludeDescriptions
|
|
| vpqAddSeparatingSilence;
|
|
|
|
if (gpsInfoFlags & vpGPSIntro)
|
|
{
|
|
vp_queueStringTableEntry(¤tLanguage->gps);
|
|
if (!state.settings.gps_enabled)
|
|
{
|
|
vp_queueStringTableEntry(¤tLanguage->off);
|
|
vp_play();
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (gpsInfoFlags & vpGPSFixQuality)
|
|
{
|
|
switch (state.gps_data.fix_quality)
|
|
{
|
|
case 0:
|
|
vp_queueStringTableEntry(¤tLanguage->noFix);
|
|
vp_play();
|
|
return;
|
|
case 1:
|
|
vp_queueString("SPS", vpAnnounceCommonSymbols);
|
|
break;
|
|
|
|
case 2:
|
|
vp_queueString("DGPS", vpAnnounceCommonSymbols);
|
|
break;
|
|
|
|
case 3:
|
|
vp_queueString("PPS", vpAnnounceCommonSymbols);
|
|
break;
|
|
|
|
case 6:
|
|
vp_queueStringTableEntry(¤tLanguage->fixLost);
|
|
break;
|
|
|
|
default:
|
|
vp_queueStringTableEntry(¤tLanguage->error);
|
|
|
|
vp_play();
|
|
|
|
return;
|
|
}
|
|
|
|
addSilenceIfNeeded(flags);
|
|
}
|
|
|
|
if (gpsInfoFlags & vpGPSFixType)
|
|
{
|
|
switch (state.gps_data.fix_type)
|
|
{
|
|
case 2:
|
|
vp_queueString("2D", vpAnnounceCommonSymbols);
|
|
break;
|
|
|
|
case 3:
|
|
vp_queueString("3D", vpAnnounceCommonSymbols);
|
|
break;
|
|
}
|
|
|
|
addSilenceIfNeeded(flags);
|
|
}
|
|
|
|
char buffer[17] = "\0";
|
|
|
|
if (gpsInfoFlags & vpGPSDirection)
|
|
{
|
|
vp_queuePrompt(PROMPT_COMPASS);
|
|
if (!IsCompassCloseEnoughToCardinalPoint())
|
|
{
|
|
sniprintf(buffer, 16, "%3.1f", state.gps_data.tmg_true);
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols);
|
|
vp_queuePrompt(PROMPT_DEGREES);
|
|
}
|
|
|
|
if ((state.gps_data.tmg_true < (45 + margin)) ||
|
|
(state.gps_data.tmg_true > (315 - margin)))
|
|
{
|
|
vp_queuePrompt(PROMPT_NORTH);
|
|
}
|
|
|
|
if ((state.gps_data.tmg_true > (45 - margin)) &&
|
|
(state.gps_data.tmg_true < (135 + margin)))
|
|
{
|
|
vp_queuePrompt(PROMPT_EAST);
|
|
}
|
|
|
|
if ((state.gps_data.tmg_true > (135 - margin)) &&
|
|
(state.gps_data.tmg_true < (225 + margin)))
|
|
{
|
|
vp_queuePrompt(PROMPT_SOUTH);
|
|
}
|
|
|
|
if ((state.gps_data.tmg_true > (225 - margin)) &&
|
|
(state.gps_data.tmg_true < (315 + margin)))
|
|
{
|
|
vp_queuePrompt(PROMPT_WEST);
|
|
}
|
|
|
|
addSilenceIfNeeded(flags);
|
|
}
|
|
|
|
if ((gpsInfoFlags & vpGPSSpeed) != 0)
|
|
{
|
|
// speed/altitude:
|
|
sniprintf(buffer, 16, "%dkm/h", state.gps_data.speed);
|
|
vp_queuePrompt(PROMPT_SPEED);
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols |
|
|
vpAnnounceLessCommonSymbols);
|
|
}
|
|
|
|
if ((gpsInfoFlags & vpGPSAltitude) != 0)
|
|
{
|
|
vp_queuePrompt(PROMPT_ALTITUDE);
|
|
|
|
sniprintf(buffer, 16, "%dm", state.gps_data.altitude);
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols);
|
|
addSilenceIfNeeded(flags);
|
|
}
|
|
|
|
if ((gpsInfoFlags & vpGPSLatitude) != 0)
|
|
{
|
|
// Convert from signed longitude, to unsigned + direction
|
|
int32_t latitude = abs(state.gps_data.latitude);
|
|
uint8_t latitude_int = latitude / 1000000;
|
|
int32_t latitude_dec = latitude % 1000000;
|
|
voicePrompt_t direction = (state.gps_data.latitude < 0) ? PROMPT_SOUTH : PROMPT_NORTH;
|
|
sniprintf(buffer, 16, "%d.%06"PRId32, latitude_int, latitude_dec);
|
|
stripTrailingZeroes(buffer);
|
|
vp_queuePrompt(PROMPT_LATITUDE);
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols);
|
|
vp_queuePrompt(direction);
|
|
}
|
|
|
|
if ((gpsInfoFlags & vpGPSLongitude) != 0)
|
|
{
|
|
// Convert from signed longitude, to unsigned + direction
|
|
int32_t longitude = abs(state.gps_data.longitude);
|
|
uint8_t longitude_int = longitude / 1000000;
|
|
int32_t longitude_dec = longitude % 1000000;
|
|
voicePrompt_t direction = (state.gps_data.longitude < 0) ? PROMPT_WEST : PROMPT_EAST;
|
|
sniprintf(buffer, 16, "%d.%06"PRId32, longitude_int, longitude_dec);
|
|
stripTrailingZeroes(buffer);
|
|
|
|
vp_queuePrompt(PROMPT_LONGITUDE);
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols);
|
|
vp_queuePrompt(direction);
|
|
addSilenceIfNeeded(flags);
|
|
}
|
|
|
|
if ((gpsInfoFlags & vpGPSSatCount) != 0)
|
|
{
|
|
vp_queuePrompt(PROMPT_SATELLITES);
|
|
vp_queueInteger(state.gps_data.satellites_in_view);
|
|
}
|
|
|
|
vp_play();
|
|
}
|
|
#endif // CONFIG_GPS
|
|
|
|
void vp_announceAboutScreen()
|
|
{
|
|
vp_flush();
|
|
|
|
vp_queueStringTableEntry(¤tLanguage->openRTX);
|
|
|
|
vp_queueStringTableEntry(¤tLanguage->Niccolo);
|
|
vp_queueStringTableEntry(¤tLanguage->Silvano);
|
|
vp_queueStringTableEntry(¤tLanguage->Federico);
|
|
vp_queueStringTableEntry(¤tLanguage->Fred);
|
|
vp_queueStringTableEntry(¤tLanguage->Joseph);
|
|
|
|
vp_play();
|
|
}
|
|
|
|
void vp_announceBackupScreen()
|
|
{
|
|
vp_flush();
|
|
|
|
vp_queueStringTableEntry(¤tLanguage->flashBackup);
|
|
|
|
vp_queueStringTableEntry(¤tLanguage->connectToRTXTool);
|
|
vp_queueStringTableEntry(¤tLanguage->toBackupFlashAnd);
|
|
vp_queueStringTableEntry(¤tLanguage->pressPTTToStart);
|
|
vp_queuePrompt(PROMPT_VP_UNAVAILABLE);
|
|
|
|
vp_play();
|
|
}
|
|
|
|
void vp_announceRestoreScreen()
|
|
{
|
|
vp_flush();
|
|
|
|
vp_queueStringTableEntry(¤tLanguage->flashRestore);
|
|
|
|
vp_queueStringTableEntry(¤tLanguage->connectToRTXTool);
|
|
vp_queueStringTableEntry(¤tLanguage->toRestoreFlashAnd);
|
|
vp_queueStringTableEntry(¤tLanguage->pressPTTToStart);
|
|
vp_queuePrompt(PROMPT_VP_UNAVAILABLE);
|
|
|
|
vp_play();
|
|
}
|
|
|
|
#ifdef CONFIG_RTC
|
|
void vp_announceSettingsTimeDate()
|
|
{
|
|
vp_flush();
|
|
|
|
vp_queueStringTableEntry(¤tLanguage->timeAndDate);
|
|
|
|
datetime_t local_time = utcToLocalTime(state.time,
|
|
state.settings.utc_timezone);
|
|
|
|
char buffer[16] = "\0";
|
|
sniprintf(buffer, 16, "%02d/%02d/%02d", local_time.date, local_time.month,
|
|
local_time.year);
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols |
|
|
vpAnnounceLessCommonSymbols);
|
|
|
|
vp_queuePrompt(PROMPT_SILENCE);
|
|
vp_queuePrompt(PROMPT_SILENCE);
|
|
|
|
sniprintf(buffer, 16, "%02d:%02d:%02d", local_time.hour, local_time.minute,
|
|
local_time.second);
|
|
vp_queueString(buffer, vpAnnounceCommonSymbols |
|
|
vpAnnounceLessCommonSymbols);
|
|
|
|
vp_play();
|
|
}
|
|
#endif // CONFIG_RTC
|
|
|
|
void vp_announceSettingsVoiceLevel(const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
switch (state.settings.vpLevel)
|
|
{
|
|
case vpNone:
|
|
vp_queueStringTableEntry(¤tLanguage->off);
|
|
break;
|
|
|
|
case vpBeep:
|
|
vp_queueStringTableEntry(¤tLanguage->beep);
|
|
break;
|
|
|
|
default:
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queuePrompt(PROMPT_VOICE_NAME);
|
|
vp_queueStringTableEntry(¤tLanguage->level);
|
|
}
|
|
|
|
vp_queueInteger(state.settings.vpLevel-vpBeep);
|
|
break;
|
|
}
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceSettingsOnOffToggle(const char* const* stringTableStringPtr,
|
|
const vpQueueFlags_t flags, bool val)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
vp_queueStringTableEntry(stringTableStringPtr);
|
|
|
|
vp_queueStringTableEntry(val ? ¤tLanguage->on : ¤tLanguage->off);
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceSettingsInt(const char* const* stringTableStringPtr,
|
|
const vpQueueFlags_t flags,
|
|
int val)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
vp_queueStringTableEntry(stringTableStringPtr);
|
|
|
|
vp_queueInteger(val);
|
|
|
|
playIfNeeded(flags);
|
|
}
|
|
|
|
void vp_announceScreen(uint8_t ui_screen)
|
|
{
|
|
const vpSummaryInfoFlags_t infoFlags = vpChannelNameOrVFO
|
|
| vpFrequencies
|
|
| vpRadioMode;
|
|
|
|
switch (ui_screen)
|
|
{
|
|
case MAIN_VFO:
|
|
vp_announceChannelSummary(&state.channel, 0, state.bank, infoFlags);
|
|
break;
|
|
|
|
case MAIN_MEM:
|
|
vp_announceChannelSummary(&state.channel, state.channel_index+1,
|
|
state.bank, infoFlags);
|
|
break;
|
|
|
|
#ifdef CONFIG_GPS
|
|
case MENU_GPS:
|
|
vp_announceGPSInfo(vpGPSAll);
|
|
break;
|
|
#endif
|
|
|
|
case MENU_BACKUP:
|
|
vp_announceBackupScreen();
|
|
break;
|
|
|
|
case MENU_RESTORE:
|
|
vp_announceRestoreScreen();
|
|
break;
|
|
|
|
case MENU_ABOUT:
|
|
vp_announceAboutScreen();
|
|
break;
|
|
|
|
#ifdef CONFIG_RTC
|
|
case SETTINGS_TIMEDATE:
|
|
vp_announceSettingsTimeDate();
|
|
break;
|
|
#endif
|
|
|
|
case SETTINGS_M17:
|
|
vp_announceBuffer(¤tLanguage->callsign,
|
|
false, true, state.settings.callsign);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void vp_announceBuffer(const char* const* stringTableStringPtr,
|
|
bool editMode, bool callsign,
|
|
const char* buffer)
|
|
{
|
|
bool isPlaying = vp_isPlaying();
|
|
|
|
vp_flush();
|
|
|
|
if (!isPlaying)
|
|
{
|
|
vp_queueStringTableEntry(stringTableStringPtr);
|
|
|
|
if (editMode)
|
|
vp_queuePrompt(PROMPT_EDIT);
|
|
}
|
|
|
|
vpFlags_t flags = vpAnnounceCommonSymbols;
|
|
// add edit mode flags to adjust what is spoken.
|
|
// extra symbols not relevant when entering callsign.
|
|
if ((editMode == true) && (callsign == false))
|
|
flags |= vpAnnounceLessCommonSymbols
|
|
| vpAnnounceSpace
|
|
| vpAnnounceASCIIValueForUnknownChars;
|
|
|
|
vp_queueString(buffer, flags);
|
|
|
|
vp_play();
|
|
}
|
|
|
|
void vp_announceDisplayTimer()
|
|
{
|
|
bool isPlaying = vp_isPlaying();
|
|
|
|
vp_flush();
|
|
|
|
if (isPlaying == false)
|
|
vp_queueStringTableEntry(¤tLanguage->timer);
|
|
|
|
uint8_t seconds = 0;
|
|
uint8_t minutes = 0;
|
|
|
|
switch (state.settings.display_timer)
|
|
{
|
|
case TIMER_OFF:
|
|
seconds = 0;
|
|
break;
|
|
|
|
case TIMER_5S:
|
|
case TIMER_10S:
|
|
case TIMER_15S:
|
|
case TIMER_20S:
|
|
case TIMER_25S:
|
|
case TIMER_30S:
|
|
seconds = state.settings.display_timer * 5;
|
|
break;
|
|
|
|
case TIMER_1M:
|
|
case TIMER_2M:
|
|
case TIMER_3M:
|
|
case TIMER_4M:
|
|
case TIMER_5M:
|
|
minutes = (state.settings.display_timer - (TIMER_1M - 1));
|
|
break;
|
|
|
|
case TIMER_15M:
|
|
case TIMER_30M:
|
|
case TIMER_45M:
|
|
minutes = 15 * (state.settings.display_timer - (TIMER_15M - 1));
|
|
break;
|
|
|
|
case TIMER_1H:
|
|
minutes = 60;
|
|
break;
|
|
}
|
|
|
|
if ((seconds == 0) && (minutes == 0))
|
|
{
|
|
vp_queueStringTableEntry(¤tLanguage->off);
|
|
}
|
|
else if (seconds > 0)
|
|
{
|
|
vp_queueInteger(seconds);
|
|
vp_queuePrompt(PROMPT_SECONDS);
|
|
}
|
|
else if (minutes > 0)
|
|
{
|
|
vp_queueInteger(minutes);
|
|
vp_queuePrompt(PROMPT_MINUTES);
|
|
}
|
|
|
|
vp_play();
|
|
}
|
|
|
|
vpQueueFlags_t vp_getVoiceLevelQueueFlags()
|
|
{
|
|
uint8_t vpLevel = state.settings.vpLevel;
|
|
vpQueueFlags_t flags = vpqInit
|
|
| vpqAddSeparatingSilence;
|
|
|
|
switch (vpLevel)
|
|
{
|
|
case vpNone:
|
|
case vpBeep:
|
|
return vpqDefault;
|
|
|
|
case vpLow:
|
|
// Play some immediately, other things on demand.
|
|
flags |= vpqPlayImmediatelyAtMediumOrHigher;
|
|
break;
|
|
|
|
case vpMedium:
|
|
// Play all immediately but without extra descriptions
|
|
flags |= vpqPlayImmediately;
|
|
break;
|
|
|
|
case vpHigh:
|
|
// Play immediately with descriptions unless speech is in progress.
|
|
flags |= vpqPlayImmediately;
|
|
if (!vp_isPlaying())
|
|
flags |= vpqIncludeDescriptions;
|
|
break;
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
void vp_playMenuBeepIfNeeded(bool firstItem)
|
|
{
|
|
// Since menus talk at levels above beep, there's no need to run this or you'll
|
|
// get an unwanted click.
|
|
if (state.settings.vpLevel != vpBeep)
|
|
return;
|
|
if (firstItem)
|
|
vp_beep(BEEP_MENU_FIRST_ITEM, SHORT_BEEP);
|
|
else
|
|
vp_beep(BEEP_MENU_ITEM, SHORT_BEEP);
|
|
}
|
|
|
|
void vp_announceSplashScreen()
|
|
{
|
|
if (state.settings.vpLevel < vpBeep)
|
|
return;
|
|
|
|
vp_flush();
|
|
|
|
if (state.settings.vpLevel == vpBeep)
|
|
{
|
|
vp_beepSeries(BOOT_MELODY);
|
|
return;
|
|
}
|
|
|
|
vpQueueFlags_t localFlags = vpqAddSeparatingSilence;
|
|
|
|
// Force on the descriptions for level 3.
|
|
if (state.settings.vpLevel == vpHigh)
|
|
{
|
|
localFlags |= vpqIncludeDescriptions;
|
|
}
|
|
|
|
vp_queueStringTableEntry(¤tLanguage->openRTX);
|
|
vp_queuePrompt(PROMPT_VFO);
|
|
vp_announceFrequencies(state.channel.rx_frequency,
|
|
state.channel.tx_frequency,
|
|
localFlags);
|
|
vp_announceRadioMode(state.channel.mode, localFlags);
|
|
vp_play();
|
|
}
|
|
|
|
void vp_announceTimeZone(const int8_t timeZone, const vpQueueFlags_t flags)
|
|
{
|
|
clearCurrPromptIfNeeded(flags);
|
|
|
|
if (flags & vpqIncludeDescriptions)
|
|
{
|
|
vp_queueStringTableEntry(¤tLanguage->UTCTimeZone);
|
|
}
|
|
|
|
int wholeHours = timeZone / 2;
|
|
int halfHour = timeZone % 2;
|
|
|
|
// While vp_queueInteeger handles negative numbers, we want to explicitly
|
|
// say the sign even when positive.
|
|
if (timeZone > 0)
|
|
{
|
|
vp_queuePrompt(PROMPT_PLUS);
|
|
}
|
|
else if (timeZone < 0)
|
|
{
|
|
vp_queuePrompt(PROMPT_MINUS);
|
|
wholeHours *= -1;
|
|
}
|
|
|
|
vp_queueInteger(wholeHours);
|
|
|
|
if (halfHour != 0)
|
|
{
|
|
vp_queuePrompt(PROMPT_POINT);
|
|
vp_queueInteger(5);
|
|
}
|
|
|
|
playIfNeeded(flags);
|
|
}
|