/* * Copyright (C) 2023-2025 Roger Clark, VK3KYY / G4KYF * Daniel Caujolle-Bert, F1RMB * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation, * incorporation in a commercial product or incorporation into a product or project which allows commercial use. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ #if !defined(PLATFORM_GD77S) #include "main.h" #include "functions/settings.h" #include "user_interface/uiLocalisation.h" #include "user_interface/uiUtilities.h" #include "interfaces/clockManager.h" #include "functions/rxPowerSaving.h" #if defined(CPU_MK22FN512VLL12) #include "fsl_ftm.h" #else // CPU_MK22FN512VLL12 #if !defined(PLATFORM_MD9600) #include "hardware/AT1846S.h" #endif #endif // CPU_MK22FN512VLL12 #include "functions/aprs.h" #include "hardware/HR-C6000.h" #include "functions/satellite.h" #if defined(HAS_GPS) #include "interfaces/gps.h" #endif #include "user_interface/menuSystem.h" #define AX25_PACKET_BUFFER_SIZE 256U #define SMART_BEACONING_SPEED_MIN 54U // more than 1km/h (0.5399568034557235 kn == 1km/h) #define APRS_DESTINATION "APOG77" // MAX 6 char (excluding terminator) #define APRS_CONFIG_SATELLITE 0U #define APRS_CONFIG_CHANNEL 1U #define APRS_ANTI_COLLISION_MS_MAX 2000U #define APRS_USE_COURSETO_FOR_BEARING 1 #if defined(CPU_MK22FN512VLL12) // On MK22, isnan macro has a bug, circumvent it #undef isnan #define isnan(x) (__builtin_isnan(x)) // On MK22, round() doesn't exist, __builtin_round() is also not functionnal // from newlib-cygwin: // https://sourceware.org/git/gitweb.cgi?p=newlib-cygwin.git;a=blob;f=newlib/libm/common/s_round.c;hb=master typedef union { double value; struct { uint32_t lsw; uint32_t msw; } parts; } ieee_double_shape_type; #define EXTRACT_WORDS(ix0,ix1,d) \ do { \ ieee_double_shape_type ew_u; \ ew_u.value = (d); \ (ix0) = ew_u.parts.msw; \ (ix1) = ew_u.parts.lsw; \ } while (0) #define INSERT_WORDS(d,ix0,ix1) \ do { \ ieee_double_shape_type iw_u; \ iw_u.parts.msw = (ix0); \ iw_u.parts.lsw = (ix1); \ (d) = iw_u.value; \ } while (0) double round(double x) { /* Most significant word, least significant word. */ int32_t msw, exponent_less_1023; uint32_t lsw; EXTRACT_WORDS(msw, lsw, x); /* Extract exponent field. */ exponent_less_1023 = ((msw & 0x7ff00000) >> 20) - 1023; if (exponent_less_1023 < 20) { if (exponent_less_1023 < 0) { msw &= 0x80000000; if (exponent_less_1023 == -1) /* Result is +1.0 or -1.0. */ msw |= ((int32_t)1023 << 20); lsw = 0; } else { uint32_t exponent_mask = 0x000fffff >> exponent_less_1023; if ((msw & exponent_mask) == 0 && lsw == 0) /* x in an integral value. */ return x; msw += 0x00080000 >> exponent_less_1023; msw &= ~exponent_mask; lsw = 0; } } else if (exponent_less_1023 > 51) { if (exponent_less_1023 == 1024) /* x is NaN or infinite. */ return x + x; else return x; } else { uint32_t exponent_mask = 0xffffffff >> (exponent_less_1023 - 20); uint32_t tmp; if ((lsw & exponent_mask) == 0) /* x is an integral value. */ return x; tmp = lsw + (1 << (51 - exponent_less_1023)); if (tmp < lsw) msw += 1; lsw = tmp; lsw &= ~exponent_mask; } INSERT_WORDS(x, msw, lsw); return x; } #endif typedef struct { uint8_t packetBuffer[AX25_PACKET_BUFFER_SIZE]; uint16_t packetBufferBitPosition; uint16_t bitStuffingCounter; uint16_t crc; bool currentBitNRZI; bool baudIs300; } AX25Encoder_t; // Beaconing typedef struct { double latitude; double longitude; } aprsBeaconingCoordinates_t; typedef struct { uint32_t time; // ms uint16_t bearing; // hundredth degree uint16_t speed; // hundredth of knots aprsBeaconingCoordinates_t coords; } aprsBeaconingLocation_t; typedef struct { ticksTimer_t checkTimer; ticksTimer_t nextBeaconTimer; #if defined(APRS_USE_COURSETO_FOR_BEARING) aprsBeaconingCoordinates_t previousBearingPosition; uint16_t currentCourse; #endif aprsBeaconingLocation_t currentLocation; aprsBeaconingLocation_t previousLocation; CodeplugAPRSConfig_t aprsConfig[APRS_CONFIG_CHANNEL + 1]; double fixedLocationLat; double fixedLocationLon; #if defined(RATE_MESSAGE_FEATURE) uint8_t rateMessageCount; #endif bool triggerBeaconing; uint8_t decayMult; // 1 to 32 aprsBeaconingSettings_t settings; bool hasDoneQSY; bool forcedManualBeaconing; uint8_t beaconingFailureCounter; } aprsBeaconingData_t; typedef struct { uint32_t rxFreq; uint32_t txFreq; uint16_t rxTone; uint8_t chMode; bool wideBW; } aprsChannelSettingsInUse_t; aprsBeaconingData_t aprsBcnData = { .checkTimer = { 0U, 0U }, .nextBeaconTimer = { 0U, 0U }, #if defined(APRS_USE_COURSETO_FOR_BEARING) .previousBearingPosition = { 0.0, 0.0 }, .currentCourse = 0U, #endif .currentLocation = { .time = 0U, .coords = { .latitude = 0.0, .longitude = 0.0 }, .bearing = UINT16_MAX, .speed = 0U }, .previousLocation = { .time = 0U, .coords = { .latitude = 0.0, .longitude = 0.0 }, .bearing = UINT16_MAX, .speed = 0U }, .fixedLocationLat = NAN, .fixedLocationLon = NAN, #if defined(RATE_MESSAGE_FEATURE) .rateMessageCount = 0U, #endif .triggerBeaconing = false, .decayMult = APRS_BEACON_DECAY_MULT_MIN, .settings = { .state = (APRS_BEACONING_STATE_LOCATION_FROM_CHANNEL | APRS_BEACONING_STATE_DECAY_ALGO_ENABLED | APRS_BEACONING_STATE_COMPRESSED_FORMAT), .mode = APRS_BEACONING_MODE_OFF, .initialInterval = APRS_BEACON_INITIAL_INTERVAL_DEFAULT, // Offset in initialIntervals[] #if defined(RATE_MESSAGE_FEATURE) .messageInterval = APRS_BEACON_MESSAGE_INTERVAL_DEFAULT, #endif .power = 0U, .smart = { .slowRate = APRS_SMART_BEACON_SLOW_RATE_DEFAULT, .fastRate = APRS_SMART_BEACON_FAST_RATE_DEFAULT, .lowSpeed = APRS_SMART_BEACON_LOW_SPEED_DEFAULT, .highSpeed = APRS_SMART_BEACON_HIGH_SPEED_DEFAULT, .turnAngle = APRS_SMART_BEACON_TURN_ANGLE_DEFAULT, .turnSlope = APRS_SMART_BEACON_TURN_SLOPE_DEFAULT, .turnTime = APRS_SMART_BEACON_TURN_TIME_DEFAULT, } }, .hasDoneQSY = false, .forcedManualBeaconing = false, .beaconingFailureCounter = 0U }; const uint16_t initialIntervalsInSecs[APRS_BEACON_INITIAL_INTERVAL_MAX + 1] = { 12U, 30U, 60U, 120U, 180U, 300U, 600U, 1200U, 1800U, 3600U }; static char myCall[16]; static int lenBytes = 0; static volatile uint32_t lastTone; static volatile int bytePos = 0; static volatile int bitPos = 0; static volatile uint8_t dataByte; static AX25Encoder_t encoderData; static CodeplugAPRSConfig_t *aprsConfig; volatile aprsSendProgress_t aprsTxProgress = APRS_TX_IDLE; // used in the ISR static bool aprsBeaconingStateEnabled(aprsBeaconingStates_t s); static bool aprsBeaconingLocationIsValid(aprsBeaconingLocation_t *location); static void enqueueCharNrzi(AX25Encoder_t *encoderData, uint8_t data, bool useBitStuffing); static void enqueueBit(AX25Encoder_t *encoderData, bool data) { if (data) { encoderData->packetBuffer[encoderData->packetBufferBitPosition / 8U] |= 0x01 << (encoderData->packetBufferBitPosition % 8U); } encoderData->packetBufferBitPosition++; } static void updateCRC(AX25Encoder_t *encoderData, bool dataBit) { uint16_t crcXorDataBit = (encoderData->crc ^ dataBit); encoderData->crc >>= 1; if (crcXorDataBit & 0x01) { encoderData->crc ^= 0x8408; } } static void enqueueCRC(AX25Encoder_t *encoderData) { uint8_t crc_lo = (encoderData->crc ^ 0xff); uint8_t crc_hi = ((encoderData->crc >> 8) ^ 0xff); enqueueCharNrzi(encoderData, crc_lo, true); enqueueCharNrzi(encoderData, crc_hi, true); } static void enqueuePadOfLength(AX25Encoder_t *encoderData, uint8_t len) { for (uint8_t j = 0; j < len; j++) { enqueueCharNrzi(encoderData, (' ' << 1), true); } } static void enqueueCharNrzi(AX25Encoder_t *encoderData, uint8_t data, bool useBitStuffing) { bool currentBit; for (uint8_t i = 0; i < 8U; i++) { currentBit = (data & 0x01); updateCRC(encoderData, currentBit); if (currentBit) { enqueueBit(encoderData, encoderData->currentBitNRZI); encoderData->bitStuffingCounter++; if (useBitStuffing && (encoderData->bitStuffingCounter == 5)) { encoderData->currentBitNRZI ^= 1; enqueueBit(encoderData, encoderData->currentBitNRZI); encoderData->bitStuffingCounter = 0U; } } else { encoderData->currentBitNRZI ^= 1; enqueueBit(encoderData, encoderData->currentBitNRZI); encoderData->bitStuffingCounter = 0U; } data >>= 1; } } static void enqueueString(AX25Encoder_t *encoderData, const char *str) { uint8_t i = 0; while (str[i] != 0) { enqueueCharNrzi(encoderData, str[i], true); i++; }; } static void enqueueFlagOfLength(AX25Encoder_t *encoderData, uint8_t len) { for (uint8_t i = 0; i < len; i++) { enqueueCharNrzi(encoderData, 0x7E, false); // 0x7E flag } } static void enqueueHeader(AX25Encoder_t *encoderData) { //int len = MIN(strlen(APRS_DESTINATION), 6U); for (uint32_t i = 0; i < strlen(APRS_DESTINATION); i++) { enqueueCharNrzi(encoderData, (APRS_DESTINATION[i] << 1), true); } //if (len < 6U) //{ // enqueuePadOfLength(encoderData, (6U - len)); //} enqueueCharNrzi(encoderData, ('0' << 1), true); uint8_t len = MIN(strlen(myCall), 6U); for (uint8_t i = 0; i < len; i++) { enqueueCharNrzi(encoderData, (myCall[i] << 1), true); } if (len < 6U) { enqueuePadOfLength(encoderData, (6U - len)); } enqueueCharNrzi(encoderData, ((aprsConfig->senderSSID + '0') << 1), true); uint8_t numPaths = ((strlen(aprsConfig->paths[1].name) == 0) ? 1U : 2U); for (uint8_t p = 0; p < numPaths; p++) { len = MIN(strlen(aprsConfig->paths[p].name), 6U); for (uint8_t i = 0; i < len; i++) { enqueueCharNrzi(encoderData, (aprsConfig->paths[p].name[i] << 1), true); } if (len < 6U) { enqueuePadOfLength(encoderData, (6U - len)); } uint8_t isEnd = (p == (numPaths - 1)) ? 1U : 0U; enqueueCharNrzi(encoderData, (((aprsConfig->paths[p].SSID + '0') << 1) + isEnd), true); } enqueueCharNrzi(encoderData, 0x03, true); enqueueCharNrzi(encoderData, 0xF0, true); } static void enqueuePayload(AX25Encoder_t *encoderData, const char *latStr, const char *lonStr, const char *courseAndSpeed, aprsChannelSettingsInUse_t *channelSettings, bool fromSatScreen) { static const uint8_t DT_POS = '!'; uint8_t symbol = (aprsConfig->iconIndex + '!'); //'+'; // + = cross symbol. Y = yacht etc uint8_t symTable = ((aprsConfig->iconTable == 0) ? '/' : '\\'); //' = secondary table bool hasComment = (aprsConfig->comment[0] != 0); enqueueCharNrzi(encoderData, DT_POS, true); if (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_COMPRESSED_FORMAT)) { enqueueCharNrzi(encoderData, symTable, true); enqueueString(encoderData, latStr); enqueueString(encoderData, lonStr); enqueueCharNrzi(encoderData, symbol, true); enqueueString(encoderData, (courseAndSpeed ? courseAndSpeed : " ")); enqueueCharNrzi(encoderData, (courseAndSpeed ? (/*0x26 (Other)*/ 0x3E /* (RMC)*/ + '!') : '!'), true); } else { enqueueString(encoderData, latStr); enqueueCharNrzi(encoderData, symTable, true); enqueueString(encoderData, lonStr); enqueueCharNrzi(encoderData, symbol, true); if (courseAndSpeed != NULL) { enqueueString(encoderData, courseAndSpeed); } } if ((fromSatScreen == false) && (aprsConfig->flags & CODEPLUG_APRS_CONFIG_FLAG_TRANSMIT_QSY_INFO) && (aprsConfig->txFrequency != 0U)) { uint32_t rxFrequency = channelSettings->rxFreq / 100U; uint32_t txFrequency = channelSettings->txFreq / 100U; int32_t offsetValue = (txFrequency - rxFrequency); bool cssEnabled = ((channelSettings->chMode == RADIO_MODE_ANALOG) && (channelSettings->rxTone != CODEPLUG_CSS_TONE_NONE)); bool wideBW = channelSettings->wideBW; uint32_t fMHz = (uint32_t)(rxFrequency * 1E-3); uint32_t fKHz = (rxFrequency - (fMHz * 1000U)); char cssStr[6U] = { ' ', (wideBW ? 'T' : 't'), 'o', 'f', 'f', 0 }; // " Toff" char freqStr[23U]; // "FFF.FFFMHz Toff +000 " // There is a limit of 9.90MHz, as the document I found stated (why no 9.99MHz, no idea). if (abs(offsetValue) > 9900) { offsetValue = 0; } if (cssEnabled) { CodeplugCSSTypes_t RxCSSType = codeplugGetCSSType(channelSettings->rxTone); if (RxCSSType == CSS_TYPE_CTCSS) { snprintf(cssStr, sizeof(cssStr), " %c%03u", (wideBW ? 'C' : 'c'), (channelSettings->rxTone / 10U)); } else if (RxCSSType & CSS_TYPE_DCS) { snprintf(cssStr, sizeof(cssStr), " %c%03X", (wideBW ? 'D' : 'd'), (channelSettings->rxTone & ~CSS_TYPE_DCS_MASK)); } } // Even if the offset is equal to zero, and no CSS is set, // we send that information, which permits to give the bandwidth information snprintf(freqStr, sizeof(freqStr), "%03u.%03uMHz%s %c%03d%c", fMHz, fKHz, cssStr, (offsetValue < 0 ? '-' : '+'), abs((offsetValue / 10)), (hasComment ? ' ' : 0)); enqueueString(encoderData, freqStr); } if (hasComment) { enqueueString(encoderData, aprsConfig->comment); } } static void aprsTxEnded(void) { #if defined(CPU_MK22FN512VLL12) trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_MIC); audioAmpDisable(AUDIO_AMP_CHANNEL_RF); #else // CPU_MK22FN512VLL12 #if defined(PLATFORM_MD9600) trxDTMFoff(true); trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_MIC); #else // PLATFORM_MD9600 AT1846sWriteTone1Reg(0); audioAmpDisable(AUDIO_AMP_CHANNEL_RF); HAL_GPIO_WritePin(RX_AUDIO_MUX_GPIO_Port, RX_AUDIO_MUX_Pin, GPIO_PIN_SET); radioWriteReg2byte(0x44, 0x06, 0xCC); // set back to FM default trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_MIC); #endif // PLATFORM_MD9600 HRC6000SetMic(true); #endif // CPU_MK22FN512VLL12 } static void aprsConvertAndSplitCoord(double coord, bool isAmbiguous, uint32_t *degrees, uint32_t *minutesIntegral, uint32_t *minutesFractional) { *degrees = (uint32_t)coord; double minutes = ((coord - (double)*degrees) * 60.0); double minutesIntegralDouble; if (isAmbiguous) { minutes += 0.000000001; } *minutesFractional = (uint32_t)abs((int)(modf(minutes, &minutesIntegralDouble) * 100)); *minutesIntegral = (uint32_t)(minutesIntegralDouble); } static void aprsCompressLatitude(double lat, char *str) { int latRes; int lat0, lat1, lat2, lat3; latRes = (int)round(380926.0 * (90.0 - lat)); lat0 = (latRes / (91 * 91 * 91)); latRes -= (lat0 * (91 * 91 * 91)); lat1 = (latRes / (91 * 91)); latRes -= (lat1 * (91 * 91)); lat2 = (latRes / 91); latRes -= (lat2 * 91); lat3 = latRes; str[0] = (lat0 + '!'); str[1] = (lat1 + '!'); str[2] = (lat2 + '!'); str[3] = (lat3 + '!'); str[4] = 0; } static void aprsCompressLongitude(double lon, char *str) { int lonRes; int lon0, lon1, lon2, lon3; lonRes = (int)round(190463.0 * (180.0 + lon)); lon0 = (lonRes / (91 * 91 * 91)); lonRes -= (lon0 * (91 * 91 * 91)); lon1 = (lonRes / (91 * 91)); lonRes -= (lon1 * (91 * 91)); lon2 = (lonRes / 91); lonRes -= (lon2 * 91); lon3 = lonRes; str[0] = (lon0 + '!'); str[1] = (lon1 + '!'); str[2] = (lon2 + '!'); str[3] = (lon3 + '!'); str[4] = 0; } static bool aprsSendPacket(CodeplugAPRSConfig_t *config, aprsBeaconingLocation_t *previousLocation, aprsBeaconingLocation_t *currentLocation, aprsChannelSettingsInUse_t *channelSettings, bool fromSatScreen) { double lat; double lon; char latStr[16]; char lonStr[16]; char courseSpeedStr[16]; bool courseAndSpeed = false; bool gpsAndLocationAreValid = (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_LOCATION_FROM_GPS) && aprsBeaconingLocationIsValid(previousLocation) && aprsBeaconingLocationIsValid(currentLocation)); #if ! defined(PLATFORM_MD9600) bool beaconingSilently = (aprsConfig->flags & CODEPLUG_APRS_CONFIG_FLAG_BEACONING_SILENTLY); #endif aprsConfig = config; encoderData.bitStuffingCounter = 0; encoderData.currentBitNRZI = false; // clear memset(encoderData.packetBuffer, 0, AX25_PACKET_BUFFER_SIZE); encoderData.packetBufferBitPosition = 0; codeplugGetRadioName(myCall); myCall[6] = 0; //truncate to 6 chars max bool usePosition = (aprsConfig->flags & CODEPLUG_APRS_CONFIG_FLAG_USE_POSITION); if (usePosition) { uint32_t lat24bits, lon24bits; lat24bits = ((aprsConfig->latitude[2] << 16) | ( aprsConfig->latitude[1] << 8) | aprsConfig->latitude[0]); lat = latLonFixed24ToDouble(lat24bits); lon24bits = ((aprsConfig->longitude[2] << 16) | (aprsConfig->longitude[1] << 8) | aprsConfig->longitude[0]); lon = latLonFixed24ToDouble(lon24bits); } else { lat = currentLocation->coords.latitude; lon = currentLocation->coords.longitude; } uint8_t ambiguity = (aprsConfig->flags & CODEPLUG_APRS_CONFIG_FLAG_AMBIGUITY_MASK) >> 5; if (ambiguity != 0U) { int tmp; double ambCoeff; switch (ambiguity) { case 1U: //0.0005 deg ambCoeff = 5000.0; break; case 2U: //0.001 deg ambCoeff = 1000.0; break; case 3U: //0.005 deg ambCoeff = 500.0; break; case 4U: //0.01 deg ambCoeff = 100.0; break; case 5U: //0.05 deg ambCoeff = 50.0; break; case 6U: //0.1 deg ambCoeff = 10.0; break; case 7U: //0.5 deg ambCoeff = 5.0; break; default: ambCoeff = 1; break; } tmp = lat * ambCoeff; lat = tmp / ambCoeff; tmp = lon * ambCoeff; lon = tmp / ambCoeff; } if (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_COMPRESSED_FORMAT)) { aprsCompressLatitude(lat, latStr); aprsCompressLongitude(lon, lonStr); if (gpsAndLocationAreValid) { // Altitude ? if (currentLocation->speed > 0) { int iSpeed = (int)(currentLocation->speed * 1E-2); int iCourse = (int)(currentLocation->bearing * 1E-2); int c; // speed int s; // course // Course c = (iCourse / 4); if (c < 0) { c += 90; } if (c >= 90) { c -= 90; } c += '!'; // Speed s = (int)round(log(iSpeed + 1.0) / log(1.08)); s += '!'; courseSpeedStr[0] = c; courseSpeedStr[1] = s; courseSpeedStr[2] = 0; courseAndSpeed = true; } } } else { uint32_t latDeg, lonDeg; uint32_t latMinsInt, lonMinsInt; uint32_t latMinsDecimal, lonMinsDecimal; bool latHemisphere = (lat >= 0.0); bool lonHemisphere = (lon >= 0.0); lat = fabs(lat); lon = fabs(lon); aprsConvertAndSplitCoord(lat, (ambiguity > 3), &latDeg, &latMinsInt, &latMinsDecimal); aprsConvertAndSplitCoord(lon, (ambiguity > 3), &lonDeg, &lonMinsInt, &lonMinsDecimal); snprintf(latStr, 16, "%02u%02u.%02u%c", latDeg, latMinsInt, latMinsDecimal, latHemisphere ? 'N' : 'S'); snprintf(lonStr, 16, "%03u%02u.%02u%c", lonDeg, lonMinsInt, lonMinsDecimal, lonHemisphere ? 'E' : 'W'); if (gpsAndLocationAreValid) { // Altitude ? if (currentLocation->speed > 0) { snprintf(courseSpeedStr, 16, "%03u/%03u", (uint16_t)(currentLocation->bearing * 1E-2), (uint16_t)(currentLocation->speed * 1E-2)); courseAndSpeed = true; } } } enqueueFlagOfLength(&encoderData, 16U); encoderData.crc = 0xFFFF; // Initialise CRC now, after data has been sent as CRC does is only for data bytes enqueueHeader(&encoderData); enqueuePayload(&encoderData, latStr, lonStr, (courseAndSpeed ? courseSpeedStr : NULL), channelSettings, fromSatScreen); enqueueCRC(&encoderData); enqueueFlagOfLength(&encoderData, 3U); lenBytes = encoderData.packetBufferBitPosition / 8; bytePos = 0; bitPos = 0; lastTone = 0xFFFFFFFF; #if defined(PLATFORM_MD9600) encoderData.baudIs300 = false; #else // PLATFORM_MD9600 encoderData.baudIs300 = ((aprsConfig->flags & CODEPLUG_APRS_CONFIG_FLAG_300_BAUDS) != 0); #endif // PLATFORM_MD9600 #if defined(CPU_MK22FN512VLL12) ftm_config_t ftmInfo; FTM_GetDefaultConfig(&ftmInfo); ftmInfo.prescale = kFTM_Prescale_Divide_4; FTM_Init(FTM1, &ftmInfo); FTM_SetTimerPeriod(FTM1, USEC_TO_COUNT((!encoderData.baudIs300 ? 833U : 3333U), (CLOCK_GetFreq(kCLOCK_BusClk) / 4)));//use 2087U instead of 833 if in HS_RUN clock speed mode FTM_EnableInterrupts(FTM1, kFTM_TimeOverflowInterruptEnable); GPIO_PinWrite(GPIO_RX_audio_mux, Pin_RX_audio_mux, 1); if (beaconingSilently) { audioAmpDisable(AUDIO_AMP_CHANNEL_RF); } else { audioAmpEnable(AUDIO_AMP_CHANNEL_RF); } AT1846sWriteTone1Reg(lastTone); trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_TONE1); aprsTxProgress = APRS_TX_IN_PROGRESS; EnableIRQ(FTM1_IRQn); FTM_StartTimer(FTM1, kFTM_SystemClock); #else // CPU_MK22FN512VLL12 htim6.Instance = TIM6; htim6.Init.Prescaler = encoderData.baudIs300 ? 7 : 1; // 7 gives 300 baud. 1 gives 1200 baud htim6.Init.CounterMode = TIM_COUNTERMODE_UP; #if defined(PLATFORM_MD9600) htim6.Init.Period = 15000; #else // PLATFORM_MD9600 htim6.Init.Period = 30000; #endif // PLATFORM_MD9600 htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_Base_Init(&htim6) != HAL_OK) { Error_Handler(); } TIM_MasterConfigTypeDef sMasterConfig = { 0 }; sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK) { Error_Handler(); } HRC6000SetMic(false); #if defined(PLATFORM_MD9600) uint32_t lastTone = 1; trxSetTone1(lastTone); #else // PLATFORM_MD9600 uint32_t lastTone = 0; AT1846sWriteTone1Reg(lastTone); trxSelectVoiceChannel(AT1846_VOICE_CHANNEL_TONE1); int volume = getVolumeControl() + 30; if (volume >= 0) { radioWriteReg2byte(0x44, 0x06, volume); HAL_GPIO_WritePin(RX_AUDIO_MUX_GPIO_Port, RX_AUDIO_MUX_Pin, GPIO_PIN_RESET); if (beaconingSilently) { audioAmpDisable(AUDIO_AMP_CHANNEL_RF); } else { audioAmpEnable(AUDIO_AMP_CHANNEL_RF); } } #endif // PLATFORM_MD9600 aprsTxProgress = APRS_TX_IN_PROGRESS; HAL_TIM_Base_Start_IT(&htim6); #endif // CPU_MK22FN512VLL12 return true; } void aprsBitStreamSender(void) { uint32_t newTone; if (aprsTxProgress != APRS_TX_IN_PROGRESS) { return; } if (bitPos % 8 == 0) { dataByte = encoderData.packetBuffer[bytePos]; bytePos++; if (bytePos == lenBytes) { // AT1846SWriteTone1Reg(0); // just stop the ISR and flag that the data has been sent. #if defined(CPU_MK22FN512VLL12) FTM_StopTimer(FTM1); DisableIRQ(FTM1_IRQn); #else // CPU_MK22FN512VLL12 HAL_TIM_Base_Stop_IT(&htim6); #endif // CPU_MK22FN512VLL12 aprsTxProgress = APRS_TX_FINISHED; // Tell the foreground we've finished, so it can do the speaker and other stuff return; } } #if defined(CPU_MK22FN512VLL12) newTone = (encoderData.baudIs300 ? 16000 : 12000) + ((dataByte & 0x01) ? (encoderData.baudIs300 ? 2000 : 10000) : 0); if (newTone != lastTone) { AT1846sWriteTone1Reg(newTone); } #else // CPU_MK22FN512VLL12 #if defined(PLATFORM_MD9600) newTone = 1200 + ((dataByte & 0x01) ? 1000 : 0); if (newTone != lastTone) { int tval = (newTone * 65536) / 32000; //calculate the value required to generate this tone uint8_t tH = (tval >> 8) & 0xFF; uint8_t tL = tval & 0xFF; SPI0WritePageRegByteExtended(0x01, 0x11B, tH);// Set DTMF tone osc 1 to frequency of the required tone SPI0WritePageRegByteExtended(0x01, 0x11A, tL); SPI0WritePageRegByteExtended(0x01, 0x123, tH);// Set DTMF tone osc 2 to frequency of the required tone SPI0WritePageRegByteExtended(0x01, 0x122, tL); lastTone = newTone; } #else // PLATFORM_MD9600 newTone = (encoderData.baudIs300 ? 16000 : 12000) + ((dataByte & 0x01) ? (encoderData.baudIs300 ? 2000 : 10000) : 0); if (newTone != lastTone) { AT1846sWriteTone1Reg(newTone); lastTone = newTone; } #endif // PLATFORM_MD9600 #endif // CPU_MK22FN512VLL12 dataByte >>= 1; bitPos++; } #if defined(CPU_MK22FN512VLL12) void FTM1_IRQHandler(void) { /* Clear interrupt flag.*/ FTM_ClearStatusFlags(FTM1, kFTM_TimeOverflowFlag); aprsBitStreamSender(); __DSB(); } #endif // CPU_MK22FN512VLL12 // // ***************** // *** Beaconing *** // ***************** // // Aknowlegments: // - SmartBeaconing™ is a beaconing algorithm invented by Tony Arnerich KD7TA and Steve Bragg KA9MVA. // - largely inspired by https://github.com/erstec/APRS-ESP/blob/master/src/smartBeaconing.cpp from Ernest Stepanov LY3PH. // static bool aprsBeaconingStateEnabled(aprsBeaconingStates_t s) { return ((aprsBcnData.settings.state & s) == s); } // Returns true if the bit value has changed. static bool aprsBeaconingStateSetEnable(aprsBeaconingStates_t s, bool enabled) { if ((s & (APRS_BEACONING_STATE_LOCATION_FROM_CHANNEL | APRS_BEACONING_STATE_LOCATION_FROM_GPS)) != 0) { return false; } bool ret = (((aprsBcnData.settings.state & s) != 0) != enabled); if (enabled) { aprsBcnData.settings.state |= s; } else { aprsBcnData.settings.state &= ~s; } return ret; } static bool gpsPVTIsValid(void) { #if defined(HAS_GPS) if (SETTINGS_GPS_MODE_GET(nonVolatileSettings) > GPS_MODE_OFF) { return ((gpsData.Status & (GPS_STATUS_HAS_FIX | GPS_STATUS_HAS_POSITION | GPS_STATUS_HAS_SPEED | GPS_STATUS_HAS_TIME)) == (GPS_STATUS_HAS_FIX | GPS_STATUS_HAS_POSITION | GPS_STATUS_HAS_SPEED | GPS_STATUS_HAS_TIME)); } #endif return false; } static bool gpsFixIsValid(void) { #if defined(HAS_GPS) if (SETTINGS_GPS_MODE_GET(nonVolatileSettings) > GPS_MODE_OFF) { return ((gpsData.Status & GPS_STATUS_HAS_FIX) == GPS_STATUS_HAS_FIX); } #endif return false; } static void aprsBeaconingInvalidateLocation(aprsBeaconingLocation_t *location) { location->bearing = UINT16_MAX; location->coords.latitude = location->coords.longitude = 0.0; location->speed = 0U; location->time = 0U; } static bool aprsBeaconingLocationIsValid(aprsBeaconingLocation_t *location) { return (location->bearing != UINT16_MAX); } static bool aprsBeaconingCurrentPositionIsValid(void) { return ((aprsBeaconingStateEnabled(APRS_BEACONING_STATE_LOCATION_FROM_GPS) && settingsLocationIsValid()) || (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_LOCATION_FROM_CHANNEL) && ((isnan(aprsBcnData.fixedLocationLat) == 0) && (isnan(aprsBcnData.fixedLocationLon) == 0)))); } static double aprsGetFixedPositionLatitude(void) { double lat = 0.0; if (isnan(aprsBcnData.fixedLocationLat) == 0) { return aprsBcnData.fixedLocationLat; } else { if (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_CONFIG)) { if (aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].flags & CODEPLUG_APRS_CONFIG_FLAG_USE_POSITION) { uint32_t tLat = aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].latitude[2]; tLat = (tLat << 8) + aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].latitude[1]; tLat = (tLat << 8) + aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].latitude[0]; aprsBcnData.fixedLocationLat = latLonFixed24ToDouble(tLat); return aprsBcnData.fixedLocationLat; } } // Returns NAN if no position is valid, not even the one stored in the settings. if (settingsLocationIsValid() == false) { return NAN; } else { aprsBcnData.fixedLocationLat = lat = settingsLocationGetLatitude(); } } return lat; } static double aprsGetFixedPositionLongitude(void) { double lon = 0.0; if (isnan(aprsBcnData.fixedLocationLon) == 0) { return aprsBcnData.fixedLocationLon; } else { if (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_CONFIG)) { if (aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].flags & CODEPLUG_APRS_CONFIG_FLAG_USE_POSITION) { uint32_t tLon = aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].longitude[2]; tLon = (tLon << 8) + aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].longitude[1]; tLon = (tLon << 8) + aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].longitude[0]; aprsBcnData.fixedLocationLon = latLonFixed24ToDouble(tLon); return aprsBcnData.fixedLocationLon; } } if (settingsLocationIsValid() == false) { return NAN; } else { aprsBcnData.fixedLocationLon = lon = settingsLocationGetLongitude(); } } return lon; } static double aprsGetBearingAngleDiff(double a, double b) { double delta = fmod(fabs(a - b), 360.0); return (delta <= 180.0 ? delta : (360.0 - delta)); } #if defined(APRS_USE_COURSETO_FOR_BEARING) uint16_t aprsBeaconingGetBearing(void) { if (distanceBetweenTwoCoords(aprsBcnData.previousBearingPosition.latitude, aprsBcnData.previousBearingPosition.longitude, aprsBcnData.currentLocation.coords.latitude, aprsBcnData.currentLocation.coords.longitude, false) > 2.0) { aprsBcnData.currentCourse = (uint16_t)(courseTo(aprsBcnData.previousBearingPosition.latitude, aprsBcnData.previousBearingPosition.longitude, aprsBcnData.currentLocation.coords.latitude, aprsBcnData.currentLocation.coords.longitude) * 1E2); memcpy(&aprsBcnData.previousBearingPosition, &aprsBcnData.currentLocation.coords, sizeof(aprsBeaconingCoordinates_t)); } return aprsBcnData.currentCourse; } #endif // APRS_USE_COURSETO_FOR_BEARING static bool aprsSmartBeaconingCornerPegging(double *currentSpeedMPS, uint32_t *timeDiff) { *currentSpeedMPS = ((aprsBcnData.currentLocation.speed * 1E-2) * MPS_PER_KNOT); *timeDiff = (aprsBcnData.currentLocation.time - aprsBcnData.previousLocation.time); if (aprsBcnData.currentLocation.speed > SMART_BEACONING_SPEED_MIN) { if (aprsBeaconingLocationIsValid(&aprsBcnData.currentLocation) == false) { return false; } // if last bearing unknown, deploy turn_time if (aprsBeaconingLocationIsValid(&aprsBcnData.previousLocation) == false) { return ((*timeDiff / MILLISECS_PER_SEC) >= aprsBcnData.settings.smart.turnTime); } double turnDiff = aprsGetBearingAngleDiff((aprsBcnData.currentLocation.bearing * 1E-2), (aprsBcnData.previousLocation.bearing * 1E-2)); double turnThreshold = MIN(120.0, ((aprsBcnData.settings.smart.turnAngle * 1.0) + ((aprsBcnData.settings.smart.turnSlope * 10.0) / (*currentSpeedMPS * MPS_TO_MPH)))); return (((*timeDiff / MILLISECS_PER_SEC) >= ((uint32_t)aprsBcnData.settings.smart.turnTime)) && (turnDiff > turnThreshold)); } return false; } static double aprsSmartBeaconingGetMaxSpeed(double currentSpeedMPS, uint32_t timeDiff) { double dist = distanceBetweenTwoCoords(aprsBcnData.currentLocation.coords.latitude, aprsBcnData.currentLocation.coords.longitude, aprsBcnData.previousLocation.coords.latitude, aprsBcnData.previousLocation.coords.longitude, false); return MAX(MAX((dist / (timeDiff / MILLISECS_PER_SEC)), currentSpeedMPS), ((aprsBcnData.previousLocation.speed * 1E-2) * MPS_PER_KNOT)); } static uint32_t aprsSmartBeaconingSpeedRate(double speedMPS) { int32_t slowRate = (aprsBcnData.settings.smart.slowRate * 60); // min => s int32_t fastRate = aprsBcnData.settings.smart.fastRate; // s double lowSpeed = (aprsBcnData.settings.smart.lowSpeed * MPS_PER_KMPH); // => m/s double highSpeed = (aprsBcnData.settings.smart.highSpeed * MPS_PER_KMPH); // => m/s if (speedMPS <= lowSpeed) { return slowRate; } else if (speedMPS >= highSpeed) { return fastRate; } return (uint32_t)(fastRate + (slowRate - fastRate) * (highSpeed - speedMPS) / (highSpeed - lowSpeed)); } static bool aprsSmartBeaconingCheck(void) { double currentSpeedMPS; uint32_t timeDiff; if ((aprsBeaconingLocationIsValid(&aprsBcnData.previousLocation) == false) || aprsSmartBeaconingCornerPegging(¤tSpeedMPS, &timeDiff)) { return true; } uint32_t speedRate = aprsSmartBeaconingSpeedRate(aprsSmartBeaconingGetMaxSpeed(currentSpeedMPS, timeDiff)); // timediff in seconds. if ((timeDiff / MILLISECS_PER_SEC) >= speedRate) { return true; } return false; } static void aprsBeaconingDecayTick(bool reset) { if ((aprsBcnData.settings.mode != APRS_BEACONING_MODE_SMART_BEACONING) && aprsBeaconingStateEnabled(APRS_BEACONING_STATE_DECAY_ALGO_ENABLED)) { if (reset) { aprsBcnData.decayMult = APRS_BEACON_DECAY_MULT_MIN; } else if (aprsBcnData.decayMult < APRS_BEACON_DECAY_MULT_MAX) { aprsBcnData.decayMult *= APRS_BEACON_DECAY_MULT_STEP; } } } static void aprsBeaconingTxStateTick(uiEvent_t *ev) { switch(aprsTxProgress) { case APRS_TX_IDLE: case APRS_TX_IN_PROGRESS: break; case APRS_TX_FINISHED: aprsTxEnded();// foreground parts of APRS send finished // deliberate fall through case APRS_TX_TERMINATE: trxTransmissionEnabled = false; // In analog mode. Stop transmitting immediately LedWrite(LED_RED, 0); #if defined(HAS_GPS) if (SETTINGS_GPS_MODE_GET(nonVolatileSettings) > GPS_MODE_OFF) { gpsDataInputStartStop(true); } #endif // Need to wrap this in Task Critical to avoid bus contention on the I2C bus. trxSetRxCSS(RADIO_DEVICE_PRIMARY, currentChannelData->rxTone); trxActivateRx(true); trxIsTransmitting = false; PTTToggledDown = false; aprsTxProgress = APRS_TX_WAIT_PTT_OFF; break; case APRS_TX_WAIT_PTT_OFF: case APRS_TX_FAILED_WAITING_TO_BEACON: if ((ev->buttons & BUTTON_PTT) == 0) { aprsTxProgress = APRS_TX_FINAL_STAGE; if (aprsBcnData.forcedManualBeaconing || (aprsBcnData.settings.mode == APRS_BEACONING_MODE_MANUAL)) { aprsBcnData.forcedManualBeaconing = false; } } break; case APRS_TX_FINAL_STAGE: // Needs to restore frequency/CSS/Mode after an APRS's config QSY // That never happen in APRS_BEACONING_MODE_PTT or Satellite modes if (aprsBcnData.hasDoneQSY) { radioSetTRxDevice(RADIO_DEVICE_PRIMARY); if (menuSystemGetRootMenuNumber() == UI_CHANNEL_MODE) { uint32_t rxFreq = (uiDataGlobal.reverseRepeaterChannel ? currentChannelData->txFreq : currentChannelData->rxFreq); uint32_t txFreq = (uiDataGlobal.talkaround ? rxFreq : (uiDataGlobal.reverseRepeaterChannel ? currentChannelData->rxFreq : currentChannelData->txFreq)); trxSetFrequency(rxFreq, txFreq, (((currentChannelData->chMode == RADIO_MODE_DIGITAL) && (uiDataGlobal.reverseRepeaterChannel || codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_FORCE_DMO))) ? DMR_MODE_DMO : DMR_MODE_AUTO)); } else { uint32_t rxFreq = (uiDataGlobal.reverseRepeaterVFO ? currentChannelData->txFreq : currentChannelData->rxFreq); uint32_t txFreq = (uiDataGlobal.reverseRepeaterVFO ? currentChannelData->rxFreq : currentChannelData->txFreq); trxSetFrequency(rxFreq, txFreq, (((currentChannelData->chMode == RADIO_MODE_DIGITAL) && codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_FORCE_DMO)) ? DMR_MODE_DMO : DMR_MODE_AUTO)); } trxSetModeAndBandwidth(currentChannelData->chMode, (codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_BW_25K) != 0)); if (currentChannelData->chMode == RADIO_MODE_ANALOG) { trxSetRxCSS(RADIO_DEVICE_PRIMARY, currentChannelData->rxTone); } vTaskDelay((40U / portTICK_PERIOD_MS)); if (currentChannelData->chMode == RADIO_MODE_DIGITAL) { HRC6000ResetTimeSlotDetection(); HRC6000ClearActiveDMRID(); } aprsBcnData.hasDoneQSY = false; } uiDataGlobal.displayQSOState = QSO_DISPLAY_DEFAULT_SCREEN; aprsTxProgress = APRS_TX_IDLE; break; } } void aprsBeaconingInit(void) { aprsBcnData.decayMult = APRS_BEACON_DECAY_MULT_MIN; aprsBeaconingStateSetEnable(APRS_BEACONING_STATE_ENABLED, false); aprsBeaconingStateSetEnable(APRS_BEACONING_STATE_HAS_GPS_FIX, false); } void aprsBeaconingStart(void) { if (aprsBcnData.settings.mode != APRS_BEACONING_MODE_OFF) { aprsBeaconingResetTimers(); aprsBeaconingInvalidateLocation(&aprsBcnData.currentLocation); aprsBeaconingStateSetEnable(APRS_BEACONING_STATE_ENABLED, true); aprsBeaconingStateSetEnable(APRS_BEACONING_STATE_HAS_GPS_FIX, false); } } void aprsBeaconingStop(void) { if (aprsBcnData.settings.mode != APRS_BEACONING_MODE_OFF) { if (aprsTxProgress != APRS_TX_IDLE) { #if defined(PLATFORM_MD9600) uint16_t frontPanelButtons; uiEvent_t ev = { .buttons = buttonsRead(&frontPanelButtons, uiDataGlobal.sk2latched), .keys = NO_KEYCODE, .rotary = 0, .function = 0, .events = NO_EVENT, .hasEvent = false, .time = ticksGetMillis() }; #else uiEvent_t ev = { .buttons = buttonsRead(), .keys = NO_KEYCODE, .rotary = 0, .function = 0, .events = NO_EVENT, .hasEvent = false, .time = ticksGetMillis() }; #endif aprsTxProgress = APRS_TX_FINISHED; aprsBeaconingTxStateTick(&ev); } aprsBeaconingStateSetEnable(APRS_BEACONING_STATE_ENABLED, false); ticksTimerReset(&aprsBcnData.checkTimer); } } void aprsBeaconingResetTimers(void) { if ((uiDataGlobal.Scan.active == false) && (aprsBcnData.settings.mode != APRS_BEACONING_MODE_OFF)) { uint8_t APRSConfigIndex = currentChannelData->aprsConfigIndex; bool hasAPRSCfg = false; #if defined(RATE_MESSAGE_FEATURE) aprsBcnData.rateMessageCount = 0U; #endif aprsBcnData.decayMult = APRS_BEACON_DECAY_MULT_MIN; aprsBeaconingInvalidateLocation(&aprsBcnData.previousLocation); aprsBeaconingInvalidateFixedPosition(); ticksTimerStart(&aprsBcnData.checkTimer, MILLISECS_PER_SEC); // 1s timer ticksTimerReset(&aprsBcnData.nextBeaconTimer); if (APRSConfigIndex != 0) { if (codeplugAPRSGetDataForIndex(APRSConfigIndex, &aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL])) { hasAPRSCfg = true; } } aprsBeaconingStateSetEnable(APRS_BEACONING_STATE_HAS_APRS_CONFIG, hasAPRSCfg); } if (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_SATELLITE_CONFIG)) { aprsBeaconingStateSetEnable(APRS_BEACONING_STATE_HAS_APRS_SATELLITE_CONFIG, false); } } void aprsBeaconingPrepareSatelliteConfig(void) { bool hasAprsConfig = false; int APRSConfigIndex = codeplugAPRSGetIndexOfName(currentActiveSatellite->name); if (APRSConfigIndex != 0) { if (codeplugAPRSGetDataForIndex(APRSConfigIndex, &aprsBcnData.aprsConfig[APRS_CONFIG_SATELLITE])) { hasAprsConfig = true; } } else { if (satelliteDataNative[uiDataGlobal.SatelliteAndAlarmData.currentSatellite].AdditionalData[0] != 0) { aprsBcnData.aprsConfig[APRS_CONFIG_SATELLITE].senderSSID = 7U; aprsBcnData.aprsConfig[APRS_CONFIG_SATELLITE].comment[0] = 0U; aprsBcnData.aprsConfig[APRS_CONFIG_SATELLITE].iconTable = 0U; aprsBcnData.aprsConfig[APRS_CONFIG_SATELLITE].iconIndex = ('0' - '!'); memcpy(aprsBcnData.aprsConfig[APRS_CONFIG_SATELLITE].paths[0].name, &satelliteDataNative[uiDataGlobal.SatelliteAndAlarmData.currentSatellite].AdditionalData[0], 6U); aprsBcnData.aprsConfig[APRS_CONFIG_SATELLITE].paths[0].SSID = satelliteDataNative[uiDataGlobal.SatelliteAndAlarmData.currentSatellite].AdditionalData[6] - '0'; memcpy(aprsBcnData.aprsConfig[APRS_CONFIG_SATELLITE].paths[1].name, &satelliteDataNative[uiDataGlobal.SatelliteAndAlarmData.currentSatellite].AdditionalData[7], 6U); aprsBcnData.aprsConfig[APRS_CONFIG_SATELLITE].paths[1].SSID = satelliteDataNative[uiDataGlobal.SatelliteAndAlarmData.currentSatellite].AdditionalData[13] - '0'; hasAprsConfig = true; } } aprsBeaconingStateSetEnable(APRS_BEACONING_STATE_HAS_APRS_SATELLITE_CONFIG, hasAprsConfig); aprsBeaconingSetCurrentLocation(settingsLocationGetLatitude(), settingsLocationGetLongitude()); // Ensure to get fixed position updated, when entering the Satellite // screen, as aprsBeaconingTick() could invalidate it when Satellite screen // isn't running. if (hasAprsConfig) { (void)aprsGetFixedPositionLatitude(); (void)aprsGetFixedPositionLongitude(); } } void aprsBeaconingSetCurrentLocation(double lat, double lon) { aprsBcnData.currentLocation.coords.latitude = lat; aprsBcnData.currentLocation.coords.longitude = lon; } void aprsBeaconingInvalidateFixedPosition(void) { aprsBcnData.fixedLocationLat = aprsBcnData.fixedLocationLon = NAN; } bool aprsBeaconingIsSuspended(void) { return ((aprsBcnData.settings.mode != APRS_BEACONING_MODE_OFF) && (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_ENABLED) == false)); } void aprsBeaconingSetSuspend(bool suspend) { if (aprsBcnData.settings.mode != APRS_BEACONING_MODE_OFF) { if (suspend) { aprsBeaconingStateSetEnable(APRS_BEACONING_STATE_ENABLED, false); } else { if (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_ENABLED) == false) { //aprsBcnData.decayMult = APRS_BEACON_DECAY_MULT_MIN; aprsBcnData.beaconingFailureCounter = 0U; ticksTimerStart(&aprsBcnData.checkTimer, MILLISECS_PER_SEC); // 1s timer aprsBeaconingStateSetEnable(APRS_BEACONING_STATE_ENABLED, true); } } } } void aprsBeaconingToggles(void) { if (aprsBcnData.settings.mode != APRS_BEACONING_MODE_OFF) { char buf[SCREEN_LINE_BUFFER_SIZE]; bool running = aprsBeaconingStateEnabled(APRS_BEACONING_STATE_ENABLED); aprsBeaconingSetSuspend(running); snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "Beaconing: %s", (running ? currentLanguage->off : currentLanguage->on)); uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 1000, buf, false); } } aprsBeaconingMode_t aprsBeaconingGetMode(void) { return aprsBcnData.settings.mode; } // Returns true if the beacon has been sent. bool aprsBeaconingSendBeacon(bool fromSatScreen, bool forcedManualBeaconing) { aprsChannelSettingsInUse_t channelSettings; if (uiDataGlobal.Scan.active || (trxGetMode() == RADIO_MODE_NONE) || ((trxGetMode() == RADIO_MODE_DIGITAL) && (aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].txFrequency == 0U)) || settingsIsOptionBitSet(BIT_TX_INHIBIT) || (fromSatScreen && (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_SATELLITE_CONFIG) == false)) || ((fromSatScreen == false) && ((aprsBcnData.settings.mode == APRS_BEACONING_MODE_OFF) || (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_ENABLED) == false) || (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_CONFIG) == false) ) ) || (aprsTxProgress != APRS_TX_IDLE)) { return false; } // Ignore PTT rate when in satellite mode. if ((fromSatScreen == false) && (aprsBcnData.settings.mode == APRS_BEACONING_MODE_PTT)) { if ((ticksTimerHasExpired(&aprsBcnData.nextBeaconTimer) == false) || (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_CONFIG) == false)) { return false; } } // Copy currentChannelData settings, as it could change because of QSY channelSettings.chMode = currentChannelData->chMode; channelSettings.rxFreq = currentChannelData->rxFreq; channelSettings.txFreq = currentChannelData->txFreq; channelSettings.rxTone = currentChannelData->rxTone; channelSettings.wideBW = trxGetBandwidthIs25kHz(); /// Build and Send beacon Here #if defined(RATE_MESSAGE_FEATURE) if (((aprsBcnData.rateMessageCount % aprsBcnData.settings.messageInterval) == 0)) { } #endif bool anyManualMode = (fromSatScreen || forcedManualBeaconing || (aprsBcnData.settings.mode == APRS_BEACONING_MODE_PTT) || (aprsBcnData.settings.mode == APRS_BEACONING_MODE_MANUAL)); // check config validity if (anyManualMode) { char buffer[SCREEN_LINE_BUFFER_SIZE]; bool configIsValid = ((fromSatScreen && aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_SATELLITE_CONFIG)) || ((fromSatScreen == false) && aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_CONFIG))); bool positionIsValid = (fromSatScreen ? false : aprsBeaconingCurrentPositionIsValid()); aprsBcnData.forcedManualBeaconing = forcedManualBeaconing; if (fromSatScreen && (positionIsValid == false)) { // Use current position (from settings and/or GPS) if (settingsLocationIsValid()) { aprsBeaconingSetCurrentLocation(settingsLocationGetLatitude(), settingsLocationGetLongitude()); positionIsValid = true; } } snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%s%c", ((configIsValid && positionIsValid) ? "APRS Tx" : (positionIsValid ? currentLanguage->APRS : currentLanguage->location)), ((configIsValid && positionIsValid) ? '\0' : '?')); if (configIsValid && positionIsValid) { int16_t yPos = (fromSatScreen ? #if defined(HAS_COLOURS) (DISPLAY_SIZE_Y / 4) #else DISPLAY_Y_POS_CONTACT #endif : DISPLAY_Y_POS_TX_TIMER); ucFont_t font = (fromSatScreen ? FONT_SIZE_3 : FONT_SIZE_4); int16_t fontHeight = (fromSatScreen ? FONT_SIZE_3_HEIGHT : FONT_SIZE_4_HEIGHT); #if ! defined(HAS_COLOURS) int16_t tBorder = (fromSatScreen ? 0 : 6); int16_t bBorder = (fromSatScreen ? -1 : 12); #endif displayThemeApply(THEME_ITEM_FG_TX_COUNTER, THEME_ITEM_BG); #if defined(HAS_COLOURS) displayFillRect(0, yPos, DISPLAY_SIZE_X, fontHeight, true); #else displayFillRect(0, (yPos + tBorder), DISPLAY_SIZE_X, (fontHeight - bBorder), true); #endif displayPrintCentered(yPos, buffer, font); displayThemeResetToDefault(); displayRender(); } else { uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 1500, buffer, true); soundSetMelody(MELODY_ERROR_BEEP); return false; } } if (fromSatScreen == false) { bool aprsConfigHasFrequencyQSY = (aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].txFrequency != 0U); uint32_t txFrequency = (aprsConfigHasFrequencyQSY ? aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].txFrequency : currentChannelData->txFreq); // toggle xmit switch (aprsBcnData.settings.mode) { case APRS_BEACONING_MODE_MANUAL: case APRS_BEACONING_MODE_AUTO: case APRS_BEACONING_MODE_SMART_BEACONING: if ((txFrequency != 0U) && (aprsConfigHasFrequencyQSY ? true : (codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_RX_ONLY) == 0)) && ((nonVolatileSettings.txFreqLimited == BAND_LIMITS_NONE) || trxCheckFrequencyInAmateurBand(txFrequency) #if defined(PLATFORM_MD9600) || (aprsConfigHasFrequencyQSY ? true : (codeplugChannelGetFlag(currentChannelData, CHANNEL_FLAG_OUT_OF_BAND) != 0)) #endif ) && aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_CONFIG)) { rxPowerSavingSetState(ECOPHASE_POWERSAVE_INACTIVE); #if defined(HAS_GPS) if (SETTINGS_GPS_MODE_GET(nonVolatileSettings) > GPS_MODE_OFF) { gpsDataInputStartStop(false); } #endif watchdogRun(false); // Needs to QSY ? // The current channel configuration will be restored // once the frame is sent (see aprsBeaconingTick()) if (aprsConfigHasFrequencyQSY) { radioSetTRxDevice(RADIO_DEVICE_PRIMARY); trxSetFrequency(txFrequency, txFrequency, DMR_MODE_AUTO); trxSetModeAndBandwidth(RADIO_MODE_ANALOG, false); trxSetRxCSS(RADIO_DEVICE_PRIMARY, CODEPLUG_CSS_TONE_NONE); trxSetTxCSS(CODEPLUG_CSS_TONE_NONE); aprsBcnData.hasDoneQSY = true; vTaskDelay((40U / portTICK_PERIOD_MS)); } // Avoid collision. uint32_t waitingForXmit = (APRS_ANTI_COLLISION_MS_MAX / RSSI_NOISE_SAMPLE_PERIOD_PIT); while((waitingForXmit > 0) && (LedRead(LED_GREEN) || ((audioAmpGetStatus() & AUDIO_AMP_CHANNEL_RF) != 0))) { vTaskDelay((RSSI_NOISE_SAMPLE_PERIOD_PIT / portTICK_PERIOD_MS)); waitingForXmit--; trxCheckAnalogSquelch(); } watchdogRun(true); if (waitingForXmit == 0U) { #if defined(HAS_GPS) if (SETTINGS_GPS_MODE_GET(nonVolatileSettings) > GPS_MODE_OFF) { gpsDataInputStartStop(true); } #endif aprsTxProgress = APRS_TX_FAILED_WAITING_TO_BEACON; return false; } // Temporary change the TX power output, if needed, and only if // the QSY is enabled in the APRS config. // The power will be set back to the "normal" value when // restoring the frequency. if (aprsBcnData.hasDoneQSY && (aprsBcnData.settings.power != 0U)) { trxSetPowerFromLevel((aprsBcnData.settings.power - 1U)); } LedWrite(LED_GREEN, 0); LedWrite(LED_RED, 1); HRC6000ClearIsWakingState(); trxSetTX(); // TX Delay; uint32_t m = ticksGetMillis(); while((ticksGetMillis() - m) < APRS_XMIT_TX_DELAY) { vTaskDelay((1U / portTICK_PERIOD_MS)); } } else { return false; } break; default: break; } } aprsSendPacket(&aprsBcnData.aprsConfig[(fromSatScreen ? APRS_CONFIG_SATELLITE : APRS_CONFIG_CHANNEL)], &aprsBcnData.previousLocation, &aprsBcnData.currentLocation, &channelSettings, fromSatScreen); #if defined(RATE_MESSAGE_FEATURE) aprsBcnData.rateMessageCount = (aprsBcnData.rateMessageCount + 1) % aprsBcnData.settings.messageInterval; #endif if (anyManualMode) { if (aprsBcnData.settings.mode == APRS_BEACONING_MODE_PTT) { ticksTimerStart(&aprsBcnData.nextBeaconTimer, (initialIntervalsInSecs[aprsBcnData.settings.initialInterval] * MILLISECS_PER_SEC)); } memcpy(&aprsBcnData.previousLocation, &aprsBcnData.currentLocation, sizeof(aprsBeaconingLocation_t)); } return true; } bool aprsBeaconingIsTransmitting(void) { return (((aprsTxProgress == APRS_TX_IDLE) && (aprsBcnData.hasDoneQSY == false)) == false); } bool aprsBeaconingForcedManualBeaconingTriggered(void) { return aprsBcnData.forcedManualBeaconing; } bool aprsBeaconingOnDifferentFrequency(void) { return ((settingsIsOptionBitSet(BIT_TX_INHIBIT) == false) && (aprsBcnData.settings.mode != APRS_BEACONING_MODE_OFF) && aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_CONFIG) && (aprsBcnData.aprsConfig[APRS_CONFIG_CHANNEL].txFrequency != 0U)); } bool aprsBeaconingHasSatelliteConfiguration(void) { return (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_SATELLITE_CONFIG)); } void aprsBeaconingUpdateConfigurationFromSystemSettings(void) { // Unpack aprsBcnData.settings.mode = ((nonVolatileSettings.aprsBeaconingSettingsPart1[0] >> 29) & 0x7); aprsBcnData.settings.smart.lowSpeed = ((nonVolatileSettings.aprsBeaconingSettingsPart1[0] >> 24) & 0x1F); aprsBcnData.settings.smart.fastRate = ((nonVolatileSettings.aprsBeaconingSettingsPart1[0] >> 16) & 0xFF); aprsBcnData.settings.smart.turnSlope = ((nonVolatileSettings.aprsBeaconingSettingsPart1[0] >> 8) & 0xFF); aprsBcnData.settings.smart.turnTime = (nonVolatileSettings.aprsBeaconingSettingsPart1[0] & 0xFF); uint8_t messageInterval = 0; messageInterval = (((nonVolatileSettings.aprsBeaconingSettingsPart1[1] >> 21) & 0x1F) + APRS_BEACON_MESSAGE_INTERVAL_MIN); aprsBcnData.settings.smart.slowRate = ((nonVolatileSettings.aprsBeaconingSettingsPart1[1] >> 14) & 0x7F); aprsBcnData.settings.smart.highSpeed = ((nonVolatileSettings.aprsBeaconingSettingsPart1[1] >> 7) & 0x7F); aprsBcnData.settings.smart.turnAngle = (nonVolatileSettings.aprsBeaconingSettingsPart1[1] & 0x7F); #if defined(RATE_MESSAGE_FEATURE) aprsBcnData.settings.messageInterval = messageInterval; #else (void)messageInterval; #endif aprsBcnData.settings.power = ((nonVolatileSettings.aprsBeaconingSettingsPart2 >> 12) & 0x0F); aprsBcnData.settings.state = ((nonVolatileSettings.aprsBeaconingSettingsPart2 >> 4) & 0xFF); aprsBcnData.settings.initialInterval = (nonVolatileSettings.aprsBeaconingSettingsPart2 & 0x0F); } void aprsBeaconingUpdateSystemSettingsFromConfiguration(void) { uint8_t messageInterval = APRS_BEACON_MESSAGE_INTERVAL_MIN; uint16_t v16; uint32_t v32; // Pack v32 = (((aprsBcnData.settings.mode & 0x7) << 29) | ((aprsBcnData.settings.smart.lowSpeed & 0x1F) << 24) | (aprsBcnData.settings.smart.fastRate << 16) | (aprsBcnData.settings.smart.turnSlope << 8) | aprsBcnData.settings.smart.turnTime); settingsSet(nonVolatileSettings.aprsBeaconingSettingsPart1[0], v32); #if defined(RATE_MESSAGE_FEATURE) messageInterval = (aprsBcnData.settings.messageInterval - APRS_BEACON_MESSAGE_INTERVAL_MIN); #endif v32 = (((messageInterval & 0x1F) << 21) | ((aprsBcnData.settings.smart.slowRate & 0x7F) << 14) | ((aprsBcnData.settings.smart.highSpeed & 0x7F) << 7) | (aprsBcnData.settings.smart.turnAngle & 0x7F)); settingsSet(nonVolatileSettings.aprsBeaconingSettingsPart1[1], v32); v16 = (((aprsBcnData.settings.power & 0x0F) << 12) | ((aprsBcnData.settings.state & 0xFF) << 4) | (aprsBcnData.settings.initialInterval & 0x0F)); settingsSet(nonVolatileSettings.aprsBeaconingSettingsPart2, v16); } void aprsBeaconingGetSettings(aprsBeaconingSettings_t *dest) { memcpy(dest, &aprsBcnData.settings, sizeof(aprsBeaconingSettings_t)); } void aprsBeaconingSetSettings(aprsBeaconingSettings_t *src) { if (memcmp(src, &aprsBcnData.settings, sizeof(aprsBeaconingSettings_t)) != 0) { aprsBeaconingStop(); memcpy(&aprsBcnData.settings, src, sizeof(aprsBeaconingSettings_t)); aprsBeaconingUpdateSystemSettingsFromConfiguration(); aprsBeaconingStart(); } } void aprsBeaconingTick(uiEvent_t *ev) { if (aprsTxProgress != APRS_TX_IDLE) { aprsBeaconingTxStateTick(ev); return; } int currentMenu = menuSystemGetCurrentMenuNumber(); // Do not automatically beaconing while in Satellite or TX screens if ((currentMenu == MENU_SATELLITE) || (currentMenu == UI_TX_SCREEN) || (currentMenu == UI_HOTSPOT_MODE) || (currentMenu == UI_CPS)) { return; } if (((uiDataGlobal.Scan.active == false) && (aprsBcnData.settings.mode != APRS_BEACONING_MODE_OFF) && aprsBeaconingStateEnabled(APRS_BEACONING_STATE_ENABLED)) && aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_APRS_CONFIG) && (settingsIsOptionBitSet(BIT_TX_INHIBIT) == false)) { if (ticksTimerHasExpired(&aprsBcnData.checkTimer)) { bool locFromGPS = aprsBeaconingStateEnabled(APRS_BEACONING_STATE_LOCATION_FROM_GPS); bool gpsHasPVT = (locFromGPS ? gpsPVTIsValid() : false); bool forceEntering = (locFromGPS ? ((isnan(aprsBcnData.fixedLocationLat) == 0) ? false : true) : true); // every second check when GPS is OFF uint32_t rearmTimerMilliseconds = MILLISECS_PER_SEC; if (gpsHasPVT || forceEntering) { bool gpsFix = (locFromGPS ? gpsFixIsValid() : true); bool checkForBeaconing = (locFromGPS ? ((isnan(aprsBcnData.fixedLocationLat) == 0) ? gpsFix : true) : true); // Reset stored location when the Fix is lost or just acquired if (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_LOCATION_FROM_GPS)) { if (((gpsFix == false) && aprsBeaconingLocationIsValid(&aprsBcnData.previousLocation)) || (gpsFix && (aprsBeaconingStateEnabled(APRS_BEACONING_STATE_HAS_GPS_FIX) == false))) { aprsBeaconingInvalidateLocation(&aprsBcnData.previousLocation); aprsBeaconingStateSetEnable(APRS_BEACONING_STATE_HAS_GPS_FIX, gpsFix); } } if (checkForBeaconing) { aprsBcnData.currentLocation.time = ticksGetMillis(); if (locFromGPS) { aprsBcnData.currentLocation.coords.latitude = #if defined(HAS_GPS) gpsFix ? gpsData.LatitudeHiRes : #endif aprsGetFixedPositionLatitude(); aprsBcnData.currentLocation.coords.longitude = #if defined(HAS_GPS) gpsFix ? gpsData.LongitudeHiRes : #endif aprsGetFixedPositionLongitude(); aprsBcnData.currentLocation.bearing = #if defined(HAS_GPS) #if defined(APRS_USE_COURSETO_FOR_BEARING) (gpsFix && (gpsData.SpeedInHundredthKn > SMART_BEACONING_SPEED_MIN)) ? aprsBeaconingGetBearing() : #else (gpsFix && (gpsData.SpeedInHundredthKn > SMART_BEACONING_SPEED_MIN)) ? gpsData.CourseInHundredthDeg : #endif #endif 0U; aprsBcnData.currentLocation.speed = #if defined(HAS_GPS) (gpsFix && (gpsData.SpeedInHundredthKn > GPS_SPEED_THRESHOLD_MIN)) ? gpsData.SpeedInHundredthKn : #endif 0U; } else { aprsBcnData.currentLocation.coords.latitude = aprsGetFixedPositionLatitude(); aprsBcnData.currentLocation.coords.longitude = aprsGetFixedPositionLongitude(); aprsBcnData.currentLocation.bearing = 0U; aprsBcnData.currentLocation.speed = 0U; // Fixed position } // No position is usable (GPS/Channel and settings ones), don't do anything for now. if (isnan(aprsBcnData.currentLocation.coords.latitude) != 0) { aprsBeaconingInvalidateLocation(&aprsBcnData.currentLocation); aprsBeaconingInvalidateLocation(&aprsBcnData.previousLocation); aprsBeaconingInvalidateFixedPosition(); goto exitRearmTimer; } if (aprsBcnData.triggerBeaconing == false) { if (aprsBcnData.settings.mode == APRS_BEACONING_MODE_SMART_BEACONING) { if (aprsSmartBeaconingCheck()) { aprsBcnData.triggerBeaconing = true; } } else if ((aprsBcnData.settings.mode == APRS_BEACONING_MODE_AUTO) && ticksTimerHasExpired(&aprsBcnData.nextBeaconTimer)) { aprsBcnData.triggerBeaconing = true; } } } } if (aprsBcnData.triggerBeaconing) { bool beaconSent = false; // in MANUAL and PTT modes, we just need the updated position (even on fixed one, as aprsBeaconingResetTimers() also invalidate positions). if ((aprsBcnData.settings.mode == APRS_BEACONING_MODE_AUTO) || (aprsBcnData.settings.mode == APRS_BEACONING_MODE_SMART_BEACONING)) { beaconSent = aprsBeaconingSendBeacon(false, false); if (beaconSent && (aprsBcnData.settings.mode != APRS_BEACONING_MODE_SMART_BEACONING)) // Pure time driven { double dist = 0.0; if (aprsBeaconingLocationIsValid(&aprsBcnData.currentLocation) && aprsBeaconingLocationIsValid(&aprsBcnData.previousLocation)) { dist = distanceBetweenTwoCoords(aprsBcnData.currentLocation.coords.latitude, aprsBcnData.currentLocation.coords.longitude, aprsBcnData.previousLocation.coords.latitude, aprsBcnData.previousLocation.coords.longitude, false); } ticksTimerStart(&aprsBcnData.nextBeaconTimer, ((initialIntervalsInSecs[aprsBcnData.settings.initialInterval] * MILLISECS_PER_SEC) * aprsBcnData.decayMult)); // Reset the Decay mult if we moved outside a radius of APRS_BEACON_DECAY_RESET_DISTANCE_MIN meters. aprsBeaconingDecayTick((dist > APRS_BEACON_DECAY_RESET_DISTANCE_MIN)); } } // store current position as previous one for the next run if (beaconSent) { aprsBcnData.triggerBeaconing = false; memcpy(&aprsBcnData.previousLocation, &aprsBcnData.currentLocation, sizeof(aprsBeaconingLocation_t)); aprsBcnData.beaconingFailureCounter = 0U; } else { if (aprsTxProgress == APRS_TX_FAILED_WAITING_TO_BEACON) { aprsBcnData.beaconingFailureCounter = SAFE_MIN((aprsBcnData.beaconingFailureCounter + 1), APRS_BEACONING_FAILURE_MAX); rearmTimerMilliseconds = (MILLISECS_PER_SEC * (aprsBcnData.beaconingFailureCounter * 2)); } } } exitRearmTimer: ticksTimerStart(&aprsBcnData.checkTimer, rearmTimerMilliseconds); // 1s timer } } } #endif // PLATFORM_GD77S