Replace the 'Power Off' / 73 message on colour displays with a shut-off animation that collapses the current screen towards the middle (vertical squeeze to a line, then horizontal collapse to a centre dot) like an old CRT. Snapshots the live framebuffer into the (idle) notification offscreen buffer so no extra RAM is needed; runs blocking with vTaskDelay between frames. Mono displays keep the existing message. Docs updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
433 lines
14 KiB
C
433 lines
14 KiB
C
/*
|
|
* Copyright (C) 2019-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.
|
|
*
|
|
*/
|
|
#include "user_interface/uiGlobals.h"
|
|
#include "user_interface/menuSystem.h"
|
|
#include "user_interface/uiLocalisation.h"
|
|
#include "user_interface/uiUtilities.h"
|
|
#include "utils.h"
|
|
|
|
|
|
#if defined(PLATFORM_RD5R)
|
|
#define YCENTER ((DISPLAY_SIZE_Y / 2) + 3)
|
|
#define INNERBOX_H (FONT_SIZE_3_HEIGHT + 7)
|
|
#define YBAR (YCENTER - ((FONT_SIZE_3_HEIGHT / 2) + 2))
|
|
#define YTEXT (YBAR + 1)
|
|
#else
|
|
#define YCENTER (DISPLAY_SIZE_Y / 2)
|
|
#define INNERBOX_H (FONT_SIZE_3_HEIGHT + 6)
|
|
#define YBAR (YCENTER - (FONT_SIZE_3_HEIGHT / 2))
|
|
#define YTEXT (YBAR - 2)
|
|
#endif
|
|
#define XBAR 69
|
|
#define YBOX (YCENTER - (INNERBOX_H / 2))
|
|
|
|
|
|
#if defined(CPU_MK22FN512VLL12) || defined(PLATFORM_MD9600)
|
|
#if defined(PLATFORM_RD5R)
|
|
#define BEARING_RADIUS 18
|
|
#define BEARING_CIRCLE_X_OFFSET 4
|
|
#define BEARING_BOX_H ((BEARING_RADIUS + 5) * 2)
|
|
#else
|
|
#define BEARING_RADIUS 18
|
|
#define BEARING_CIRCLE_X_OFFSET 4
|
|
#define BEARING_BOX_H ((BEARING_RADIUS + 5) * 2)
|
|
#endif
|
|
#else // defined(CPU_MK22FN512VLL12) || defined(PLATFORM_MD9600)
|
|
#define BEARING_RADIUS 24
|
|
#define BEARING_CIRCLE_X_OFFSET 13
|
|
#define BEARING_BOX_H ((BEARING_RADIUS + 10) * 2)
|
|
#endif
|
|
#define BEARING_CIRCLE_X_POS (BEARING_RADIUS + BEARING_CIRCLE_X_OFFSET)
|
|
#define BEARING_BOX_W (DISPLAY_SIZE_X - 4)
|
|
#define BEARING_BOX_Y ((DISPLAY_SIZE_Y - BEARING_BOX_H) / 2)
|
|
#define TOP_ARROW_OFFSET (BEARING_RADIUS - 8)
|
|
#define BASE_ARROW_OFFSET -(BEARING_RADIUS - 8)
|
|
#define BEARING_AND_DIST_X_CENTER (((DISPLAY_SIZE_X - ((BEARING_RADIUS * 2) + BEARING_CIRCLE_X_OFFSET)) / 2) + ((BEARING_RADIUS * 2) + BEARING_CIRCLE_X_OFFSET))
|
|
|
|
|
|
#if defined(PLATFORM_MD9600) || defined(PLATFORM_GD77) || defined(PLATFORM_GD77S) || defined(PLATFORM_DM1801) || defined(PLATFORM_DM1801A) || defined(PLATFORM_RD5R)
|
|
static __attribute__((section(".data.$RAM2"))) uint8_t screenNotificationBufData[((DISPLAY_SIZE_X * DISPLAY_SIZE_Y) >> 3)];
|
|
#else
|
|
static __attribute__((section(".ccmram"))) uint16_t screenNotificationBufData[DISPLAY_SIZE_X * DISPLAY_SIZE_Y];
|
|
#endif
|
|
|
|
typedef struct
|
|
{
|
|
bool visible;
|
|
ticksTimer_t hideTimer;
|
|
uiNotificationType_t type;
|
|
uiNotificationID_t id;
|
|
char message[NOTIFICATION_MESSAGE_LEN_MAX];
|
|
uiNotificationBearingInfo_t bearingInfo;
|
|
} uiNotificationData_t;
|
|
|
|
static uiNotificationData_t notificationData =
|
|
{
|
|
.visible = false,
|
|
.hideTimer = { 0, 0 },
|
|
.type = NOTIFICATION_TYPE_MAX,
|
|
.id = NOTIFICATION_ID_NONE,
|
|
.message = { 0 },
|
|
.bearingInfo = { .bearing = 0.0, .distanceX10 = -1 }
|
|
};
|
|
|
|
static void displayMessage(void);
|
|
|
|
void uiNotificationShow(uiNotificationType_t type, uiNotificationID_t id, uint32_t msTimeout, const char *message, bool immediateRender)
|
|
{
|
|
bool valid = true;
|
|
|
|
if (notificationData.visible)
|
|
{
|
|
if (notificationData.id != id)
|
|
{
|
|
notificationData.visible = false;
|
|
displayRender();
|
|
}
|
|
}
|
|
|
|
memset(notificationData.message, 0, sizeof(notificationData.message));
|
|
|
|
notificationData.type = type;
|
|
notificationData.id = id;
|
|
|
|
switch (type)
|
|
{
|
|
case NOTIFICATION_TYPE_SQUELCH:
|
|
#if defined(HAS_SOFT_VOLUME)
|
|
case NOTIFICATION_TYPE_VOLUME:
|
|
#endif
|
|
break;
|
|
|
|
case NOTIFICATION_TYPE_POWER:
|
|
break;
|
|
|
|
case NOTIFICATION_TYPE_MESSAGE:
|
|
if (message)
|
|
{
|
|
snprintf(¬ificationData.message[0], sizeof(notificationData.message), "%s", message);
|
|
}
|
|
break;
|
|
|
|
case NOTIFICATION_TYPE_BEARING:
|
|
break;
|
|
|
|
default:
|
|
valid = false;
|
|
break;
|
|
}
|
|
|
|
if (valid)
|
|
{
|
|
notificationData.visible = true;
|
|
if (immediateRender)
|
|
{
|
|
uiNotificationRefresh();
|
|
}
|
|
ticksTimerStart(¬ificationData.hideTimer, msTimeout);
|
|
}
|
|
}
|
|
|
|
void uiNotificationBearingShow(uiNotificationBearingInfo_t *bearing, uint32_t msTimeout, bool immediateRender)
|
|
{
|
|
memcpy(&(notificationData.bearingInfo), bearing, sizeof(uiNotificationBearingInfo_t));
|
|
uiNotificationShow(NOTIFICATION_TYPE_BEARING, NOTIFICATION_ID_BEARING, msTimeout, NULL, immediateRender);
|
|
}
|
|
|
|
void uiNotificationRefresh(void)
|
|
{
|
|
if (notificationData.visible)
|
|
{
|
|
// copy the primary screen content
|
|
memcpy(screenNotificationBufData, displayGetPrimaryScreenBuffer(), sizeof(screenNotificationBufData));
|
|
|
|
#if defined(PLATFORM_MD9600) || defined(PLATFORM_GD77) || defined(PLATFORM_GD77S) || defined(PLATFORM_DM1801) || defined(PLATFORM_DM1801A) || defined(PLATFORM_RD5R)
|
|
displayOverrideScreenBuffer(screenNotificationBufData);
|
|
#endif
|
|
|
|
// Draw whatever
|
|
switch (notificationData.type)
|
|
{
|
|
case NOTIFICATION_TYPE_SQUELCH:
|
|
#if defined(HAS_SOFT_VOLUME)
|
|
case NOTIFICATION_TYPE_VOLUME:
|
|
#endif
|
|
{
|
|
char buffer[SCREEN_LINE_BUFFER_SIZE];
|
|
const int16_t totalBarLength = (((DISPLAY_SIZE_X - XBAR) - 4) - 4);
|
|
double slope;
|
|
int16_t bargraph;
|
|
|
|
#if defined(HAS_SOFT_VOLUME)
|
|
if (notificationData.type == NOTIFICATION_TYPE_VOLUME)
|
|
{
|
|
uint16_t volValue = CLAMP((lastVolume + 32), 0, 62);
|
|
|
|
slope = 1.0 * (totalBarLength) / 62.0;
|
|
bargraph = slope * volValue;
|
|
|
|
strncpy(buffer, currentLanguage->volume, 9);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
slope = 1.0 * (totalBarLength) / (CODEPLUG_MAX_VARIABLE_SQUELCH - CODEPLUG_MIN_VARIABLE_SQUELCH);
|
|
bargraph = slope * (currentChannelData->sql - CODEPLUG_MIN_VARIABLE_SQUELCH);
|
|
|
|
strncpy(buffer, currentLanguage->squelch, 9);
|
|
}
|
|
buffer[8] = 0;
|
|
|
|
size_t sLen = (strlen(buffer) * 8);
|
|
|
|
displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG_NOTIFICATION);
|
|
displayDrawRoundRectWithDropShadow(1, YBOX, DISPLAY_SIZE_X - 4, INNERBOX_H, 3, true);
|
|
|
|
displayThemeApply(THEME_ITEM_FG_NOTIFICATION, THEME_ITEM_BG_NOTIFICATION);
|
|
displayPrintAt(2 + ((sLen < (XBAR - 2)) ? (((XBAR - 2) - sLen) >> 1) : 0), YTEXT, buffer, FONT_SIZE_3);
|
|
|
|
displayDrawRect((XBAR - 2), YBAR, (totalBarLength + 4), (SQUELCH_BAR_H + 4), true);
|
|
displayFillRect(XBAR, (YBAR + 2), bargraph, SQUELCH_BAR_H, false);
|
|
}
|
|
break;
|
|
|
|
case NOTIFICATION_TYPE_POWER:
|
|
{
|
|
char buffer[SCREEN_LINE_BUFFER_SIZE];
|
|
uint8_t powerLevel = trxGetPowerLevel();
|
|
|
|
displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG_NOTIFICATION);
|
|
displayDrawRoundRectWithDropShadow((DISPLAY_SIZE_X / 4), YBOX, (DISPLAY_SIZE_X / 2), INNERBOX_H, 3, true);
|
|
sprintf(buffer, "%s%s", getPowerLevel(powerLevel), getPowerLevelUnit(powerLevel));
|
|
displayThemeApply(THEME_ITEM_FG_NOTIFICATION, THEME_ITEM_BG_NOTIFICATION);
|
|
displayPrintCentered(YTEXT, buffer, FONT_SIZE_3);
|
|
}
|
|
break;
|
|
|
|
case NOTIFICATION_TYPE_MESSAGE:
|
|
displayMessage();
|
|
break;
|
|
|
|
case NOTIFICATION_TYPE_BEARING:
|
|
{
|
|
char buffer[SCREEN_LINE_BUFFER_SIZE];
|
|
int16_t x1, x2, x3, y1, y2, y3;
|
|
|
|
displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG_NOTIFICATION);
|
|
displayDrawRoundRectWithDropShadow(1, BEARING_BOX_Y, (DISPLAY_SIZE_X - 4), BEARING_BOX_H, 3, true);
|
|
displayThemeApply(THEME_ITEM_FG_NOTIFICATION, THEME_ITEM_BG_NOTIFICATION);
|
|
|
|
// Circle
|
|
for (int16_t i = 0; i < 360; i += 15)
|
|
{
|
|
x1 = (int16_t)(BEARING_CIRCLE_X_POS + (BEARING_RADIUS) * cos((double)(i * DEG_TO_RAD)));
|
|
y1 = (int16_t)(((DISPLAY_SIZE_Y >> 1) - 3) + (BEARING_RADIUS) * sin((double)(i * DEG_TO_RAD)));
|
|
|
|
x2 = (int16_t)(BEARING_CIRCLE_X_POS + (BEARING_RADIUS - ((i % 45) ? 2 : 4)) * cos((double)(i * DEG_TO_RAD)));
|
|
y2 = (int16_t)(((DISPLAY_SIZE_Y >> 1) - 3) + (BEARING_RADIUS - ((i % 45) ? 2 : 4)) * sin((double)(i * DEG_TO_RAD)));
|
|
|
|
if (i == 270)
|
|
{
|
|
displayThemeApply(THEME_ITEM_FG_GPS_COLOUR, THEME_ITEM_BG_NOTIFICATION);
|
|
displayDrawLine((x1 - 1), y1, (x2 - 1), y2, true);
|
|
}
|
|
|
|
displayDrawLine(x1, y1, x2, y2, true);
|
|
|
|
if (i == 270)
|
|
{
|
|
displayDrawLine((x1 + 1), y1, (x2 + 1), y2, true);
|
|
displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG_NOTIFICATION);
|
|
}
|
|
}
|
|
|
|
|
|
displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG_NOTIFICATION);
|
|
displayDrawCircle(BEARING_CIRCLE_X_POS, ((DISPLAY_SIZE_Y >> 1) - 3), (BEARING_RADIUS + 1), true);
|
|
displayThemeApply(THEME_ITEM_FG_GPS_COLOUR, THEME_ITEM_BG_NOTIFICATION);
|
|
|
|
// Needle
|
|
double needle = (notificationData.bearingInfo.bearing - 90.0);
|
|
|
|
if (needle > 360.0)
|
|
{
|
|
needle -= 360.0;
|
|
}
|
|
|
|
needle *= DEG_TO_RAD;
|
|
|
|
x1 = (int16_t)(BEARING_CIRCLE_X_POS + TOP_ARROW_OFFSET * cos(needle));
|
|
y1 = (int16_t)(((DISPLAY_SIZE_Y >> 1) - 3) + TOP_ARROW_OFFSET * sin(needle));
|
|
|
|
x2 = (int16_t)(BEARING_CIRCLE_X_POS + BASE_ARROW_OFFSET * cos(needle + (30 * DEG_TO_RAD)));
|
|
y2 = (int16_t)(((DISPLAY_SIZE_Y >> 1) - 3) + BASE_ARROW_OFFSET * sin(needle + (30 * DEG_TO_RAD)));
|
|
|
|
x3 = (int16_t)(BEARING_CIRCLE_X_POS + BASE_ARROW_OFFSET * cos(needle - (30 * DEG_TO_RAD)));
|
|
y3 = (int16_t)(((DISPLAY_SIZE_Y >> 1) - 3) + BASE_ARROW_OFFSET * sin(needle - (30 * DEG_TO_RAD)));
|
|
|
|
displayFillTriangle(x1, y1, x2, y2, x3, y3, true);
|
|
|
|
// Bearing and Distance values
|
|
displayThemeApply(THEME_ITEM_FG_NOTIFICATION, THEME_ITEM_BG_NOTIFICATION);
|
|
|
|
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%u%c", ((uint32_t)notificationData.bearingInfo.bearing), 176);
|
|
displayPrintAt((BEARING_AND_DIST_X_CENTER - ((strlen(buffer) * 8) >> 1)),
|
|
(((DISPLAY_SIZE_Y >> 1) - 3) - FONT_SIZE_3_HEIGHT), buffer, FONT_SIZE_3);
|
|
|
|
if (notificationData.bearingInfo.distanceX10 >= 0)
|
|
{
|
|
uint32_t intPart = (notificationData.bearingInfo.distanceX10 / 10);
|
|
uint32_t decPart = (notificationData.bearingInfo.distanceX10 - (intPart * 10));
|
|
|
|
snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%u.%u km", intPart, decPart);
|
|
displayPrintAt((BEARING_AND_DIST_X_CENTER - ((strlen(buffer) * 8) >> 1)),
|
|
(((DISPLAY_SIZE_Y >> 1) - 3) + 2), buffer, FONT_SIZE_3);
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
displayThemeResetToDefault();
|
|
displayRenderWithoutNotification();
|
|
#if defined(PLATFORM_MD9600) || defined(PLATFORM_GD77) || defined(PLATFORM_GD77S) || defined(PLATFORM_DM1801) || defined(PLATFORM_DM1801A) || defined(PLATFORM_RD5R)
|
|
displayRestorePrimaryScreenBuffer();
|
|
#else
|
|
memcpy(displayGetPrimaryScreenBuffer(), screenNotificationBufData, sizeof(screenNotificationBufData));
|
|
#endif
|
|
}
|
|
}
|
|
|
|
bool uiNotificationHasTimedOut(void)
|
|
{
|
|
return (notificationData.visible && ticksTimerHasExpired(¬ificationData.hideTimer));
|
|
}
|
|
|
|
bool uiNotificationIsVisible(void)
|
|
{
|
|
return notificationData.visible;
|
|
}
|
|
|
|
void uiNotificationHide(bool immediateRender)
|
|
{
|
|
notificationData.visible = false;
|
|
uiDataGlobal.displayQSOState = uiDataGlobal.displayQSOStatePrev;
|
|
|
|
if (immediateRender)
|
|
{
|
|
displayRender();
|
|
}
|
|
}
|
|
|
|
uiNotificationID_t uiNotificationGetId(void)
|
|
{
|
|
return notificationData.id;
|
|
}
|
|
|
|
#if defined(HAS_COLOURS)
|
|
// Expose the full-screen offscreen buffer so the shutdown animation can borrow
|
|
// it as scratch (no notification is active while powering off).
|
|
uint16_t *uiNotificationGetOffscreenBuffer(void)
|
|
{
|
|
return screenNotificationBufData;
|
|
}
|
|
#endif
|
|
|
|
static void displayMessage(void)
|
|
{
|
|
char msg[SCREEN_LINE_BUFFER_SIZE];
|
|
size_t len = strlen(notificationData.message);
|
|
char *pb = notificationData.message;
|
|
char *pe = ¬ificationData.message[0] + len;
|
|
char *p = strchr(notificationData.message, '\n');
|
|
uint16_t count;
|
|
uint16_t y = 0U;
|
|
uint8_t linesCount = 1U;
|
|
uint8_t maxLines =
|
|
#if defined(HAS_COLOURS) || defined(PLATFORM_RD5R)
|
|
4U;
|
|
#else
|
|
3U;
|
|
#endif
|
|
|
|
while (p)
|
|
{
|
|
linesCount++;
|
|
|
|
if (linesCount == maxLines)
|
|
{
|
|
break;
|
|
}
|
|
|
|
p = strchr(p + 1, '\n');
|
|
}
|
|
|
|
y = (((DISPLAY_SIZE_Y - (linesCount * FONT_SIZE_3_HEIGHT)) >> 1) - 3);
|
|
|
|
displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG_NOTIFICATION);
|
|
displayDrawRoundRectWithDropShadow(1, (y - 1), (DISPLAY_SIZE_X - 4), ((FONT_SIZE_3_HEIGHT * linesCount) + 6), 3, true);
|
|
displayThemeApply(THEME_ITEM_FG_NOTIFICATION, THEME_ITEM_BG_NOTIFICATION);
|
|
|
|
p = strchr(notificationData.message, '\n');
|
|
|
|
while (linesCount > 1U)
|
|
{
|
|
count = SAFE_MIN((uint16_t)(p - pb), 14U);
|
|
|
|
memcpy(msg, pb, count);
|
|
msg[count] = '\0';
|
|
|
|
displayPrintCentered(y, msg, FONT_SIZE_3);
|
|
y += FONT_SIZE_3_HEIGHT;
|
|
|
|
pb = p + 1;
|
|
p = strchr(pb, '\n');
|
|
|
|
linesCount--;
|
|
}
|
|
|
|
if (pb < pe)
|
|
{
|
|
count = SAFE_MIN((uint16_t)(pe - pb), 14U);
|
|
|
|
p++;
|
|
|
|
// Looking for another new line
|
|
if ((p = strchr(pb, '\n')) != NULL)
|
|
{
|
|
count = (uint16_t)(p - pb);
|
|
}
|
|
|
|
memcpy(msg, pb, count);
|
|
msg[count] = '\0';
|
|
|
|
displayPrintCentered(y, msg, FONT_SIZE_3);
|
|
}
|
|
}
|