1
0
Fork 0
mirror of https://github.com/iNavFlight/inav.git synced 2025-07-13 03:19:58 +03:00
inav/src/main/io/osd_canvas.c
2025-03-07 07:31:04 +00:00

816 lines
30 KiB
C

/*
* This file is part of INAV.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Alternatively, the contents of this file may be used under the terms
* of the GNU General Public License Version 3, as described below:
*
* This file is free software: you may copy, redistribute 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 file 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/.
*
* @author Alberto Garcia Hierro <alberto@garciahierro.com>
*/
#include <math.h>
#include "platform.h"
#if defined(USE_CANVAS)
#define AHI_MIN_DRAW_INTERVAL_MS 50
#define AHI_MAX_DRAW_INTERVAL_MS 1000
#define AHI_CROSSHAIR_MARGIN 6
#define SIDEBAR_REDRAW_INTERVAL_MS 100
#define WIDGET_SIDEBAR_LEFT_INSTANCE 0
#define WIDGET_SIDEBAR_RIGHT_INSTANCE 1
#include "common/constants.h"
#include "common/log.h"
#include "common/maths.h"
#include "common/typeconversion.h"
#include "common/utils.h"
#include "drivers/display.h"
#include "drivers/display_canvas.h"
#include "drivers/display_widgets.h"
#include "drivers/osd.h"
#include "drivers/osd_symbols.h"
#include "drivers/time.h"
#include "io/osd_common.h"
#include "io/osd.h"
#include "navigation/navigation.h"
#define OSD_CANVAS_VARIO_ARROWS_PER_SLOT 2.0f
static void osdCanvasVarioRect(int *y, int *h, displayCanvas_t *canvas, int midY, float zvel)
{
int maxHeight = ceilf(OSD_VARIO_HEIGHT_ROWS /OSD_CANVAS_VARIO_ARROWS_PER_SLOT) * canvas->gridElementHeight;
// We use font characters with 2 arrows per slot
int height = MIN(fabsf(zvel) / (OSD_VARIO_CM_S_PER_ARROW * OSD_CANVAS_VARIO_ARROWS_PER_SLOT) * canvas->gridElementHeight, maxHeight);
if (zvel >= 0) {
*y = midY - height;
} else {
*y = midY;
}
*h = height;
}
void osdCanvasDrawThrottleGauge(displayPort_t *display, displayCanvas_t *canvas, const osdDrawPoint_t *p, uint8_t thrPos)
{
UNUSED(display);
static uint8_t prevThr = 0;
if (ABS(prevThr - thrPos) < 10) {
return;
}
// Make sure we clear the grid buffer
uint8_t gx;
uint8_t gy;
osdDrawPointGetGrid(&gx, &gy, display, canvas, p);
osdGridBufferClearGridRect(gx, gy, 1, OSD_THROTTLE_GAUGE_HEIGHT_ROWS);
int x = gx * canvas->gridElementWidth;
int w = canvas->gridElementWidth;
int y = gy * canvas->gridElementHeight;
int h = OSD_THROTTLE_GAUGE_HEIGHT_ROWS * canvas->gridElementHeight;
displayCanvasClearRect(canvas, x, y, w, h);
if (thrPos >= 100)
displayCanvasDrawCharacter(canvas, x, y + (canvas->gridElementHeight * 4), SYM_THR_GAUGE_FULL, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
else if (thrPos >= 90)
displayCanvasDrawCharacter(canvas, x, y + (canvas->gridElementHeight * 4), SYM_THR_GAUGE_HALF, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
else
displayCanvasDrawCharacter(canvas, x, y + (canvas->gridElementHeight * 4), SYM_THR_GAUGE_EMPTY, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
if (thrPos >= 80)
displayCanvasDrawCharacter(canvas, x, y + (canvas->gridElementHeight * 3), SYM_THR_GAUGE_FULL, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
else if (thrPos >= 70)
displayCanvasDrawCharacter(canvas, x, y + (canvas->gridElementHeight * 3), SYM_THR_GAUGE_HALF, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
else
displayCanvasDrawCharacter(canvas, x, y + (canvas->gridElementHeight * 3), SYM_THR_GAUGE_EMPTY, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
if (thrPos >= 60)
displayCanvasDrawCharacter(canvas, x, y + (canvas->gridElementHeight * 2), SYM_THR_GAUGE_FULL, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
else if (thrPos >= 50)
displayCanvasDrawCharacter(canvas, x, y + (canvas->gridElementHeight * 2), SYM_THR_GAUGE_HALF, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
else
displayCanvasDrawCharacter(canvas, x, y + (canvas->gridElementHeight * 2), SYM_THR_GAUGE_EMPTY, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
if (thrPos >= 40)
displayCanvasDrawCharacter(canvas, x, y + canvas->gridElementHeight, SYM_THR_GAUGE_FULL, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
else if (thrPos >= 30)
displayCanvasDrawCharacter(canvas, x, y + canvas->gridElementHeight, SYM_THR_GAUGE_HALF, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
else
displayCanvasDrawCharacter(canvas, x, y + canvas->gridElementHeight, SYM_THR_GAUGE_EMPTY, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
if (thrPos >= 20)
displayCanvasDrawCharacter(canvas, x, y, SYM_THR_GAUGE_FULL, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
else if (thrPos >= 10)
displayCanvasDrawCharacter(canvas, x, y, SYM_THR_GAUGE_HALF, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
else
displayCanvasDrawCharacter(canvas, x, y, SYM_THR_GAUGE_EMPTY, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
prevThr = thrPos;
}
void osdCanvasDrawVario(displayPort_t *display, displayCanvas_t *canvas, const osdDrawPoint_t *p, float zvel)
{
UNUSED(display);
static float prev = 0;
if (fabsf(prev - zvel) < (OSD_VARIO_CM_S_PER_ARROW / (OSD_CANVAS_VARIO_ARROWS_PER_SLOT * 10))) {
return;
}
// Make sure we clear the grid buffer
uint8_t gx;
uint8_t gy;
osdDrawPointGetGrid(&gx, &gy, display, canvas, p);
osdGridBufferClearGridRect(gx, gy, 1, OSD_VARIO_HEIGHT_ROWS);
int midY = gy * canvas->gridElementHeight + (OSD_VARIO_HEIGHT_ROWS * canvas->gridElementHeight) / 2;
// Make sure we're aligned with the center-ish of the grid based variant
midY -= canvas->gridElementHeight;
int x = gx * canvas->gridElementWidth;
int w = canvas->gridElementWidth;
int y;
int h;
osdCanvasVarioRect(&y, &h, canvas, midY, zvel);
if (signbit(prev) != signbit(zvel) || fabsf(prev) > fabsf(zvel)) {
// New rectangle doesn't overwrite the old one, we need to erase
int py;
int ph;
osdCanvasVarioRect(&py, &ph, canvas, midY, prev);
if (ph != h) {
displayCanvasClearRect(canvas, x, py, w, ph);
}
}
displayCanvasClipToRect(canvas, x, y, w, h);
if (zvel > 0) {
for (int yy = midY - canvas->gridElementHeight; yy > midY - h - canvas->gridElementHeight; yy -= canvas->gridElementHeight) {
displayCanvasDrawCharacter(canvas, x, yy, SYM_VARIO_UP_2A, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
}
} else {
for (int yy = midY; yy < midY + h; yy += canvas->gridElementHeight) {
displayCanvasDrawCharacter(canvas, x, yy, SYM_VARIO_DOWN_2A, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
}
}
prev = zvel;
}
void osdCanvasDrawDirArrow(displayPort_t *display, displayCanvas_t *canvas, const osdDrawPoint_t *p, float degrees)
{
UNUSED(display);
const int top = 6;
const int topInset = -2;
const int bottom = -6;
const int width = 5;
// Since grid slots are not square, when we rotate the arrow
// it overflows horizontally a bit. Making a square-ish arrow
// doesn't look good, so it's better to overstep the grid slot
// boundaries a bit and then clean after ourselves.
const int overflow = 3;
int px;
int py;
osdDrawPointGetPixels(&px, &py, display, canvas, p);
displayCanvasClearRect(canvas, px - overflow, py, canvas->gridElementWidth + overflow * 2, canvas->gridElementHeight);
displayCanvasSetFillColor(canvas, DISPLAY_CANVAS_COLOR_WHITE);
displayCanvasSetStrokeColor(canvas, DISPLAY_CANVAS_COLOR_BLACK);
displayCanvasCtmRotate(canvas, -DEGREES_TO_RADIANS(180 + degrees));
displayCanvasCtmTranslate(canvas, px + canvas->gridElementWidth / 2, py + canvas->gridElementHeight / 2);
// Main triangle
displayCanvasFillStrokeTriangle(canvas, 0, top, width, bottom, -width, bottom);
// Inset triangle
displayCanvasSetFillColor(canvas, DISPLAY_CANVAS_COLOR_TRANSPARENT);
displayCanvasFillTriangle(canvas, 0, topInset, width + 1, bottom - 1, -width, bottom - 1);
// Draw bottom strokes of the triangle
displayCanvasMoveToPoint(canvas, -width, bottom - 1);
displayCanvasStrokeLineToPoint(canvas, 0, topInset);
displayCanvasStrokeLineToPoint(canvas, width, bottom - 1);
}
static void osdDrawArtificialHorizonLevelLine(displayCanvas_t *canvas, int width, int pos, int margin)
{
displayCanvasSetLineOutlineType(canvas, DISPLAY_CANVAS_OUTLINE_TYPE_BOTTOM);
int yoff = pos >= 0 ? 10 : -10;
int yc = -pos - 1;
int sz = width / 2;
// Horizontal strokes
displayCanvasMoveToPoint(canvas, -sz, yc);
displayCanvasStrokeLineToPoint(canvas, -margin, yc);
displayCanvasMoveToPoint(canvas, sz, yc);
displayCanvasStrokeLineToPoint(canvas, margin, yc);
// Vertical strokes
displayCanvasSetLineOutlineType(canvas, DISPLAY_CANVAS_OUTLINE_TYPE_LEFT);
displayCanvasMoveToPoint(canvas, -sz, yc);
displayCanvasStrokeLineToPoint(canvas, -sz, yc + yoff);
displayCanvasSetLineOutlineType(canvas, DISPLAY_CANVAS_OUTLINE_TYPE_RIGHT);
displayCanvasMoveToPoint(canvas, sz, yc);
displayCanvasStrokeLineToPoint(canvas, sz, yc + yoff);
}
static void osdArtificialHorizonRect(displayCanvas_t *canvas, int *lx, int *ty, int *w, int *h)
{
*w = (OSD_AHI_WIDTH + 1) * canvas->gridElementWidth;
*h = OSD_AHI_HEIGHT * canvas->gridElementHeight;
*lx = (canvas->width - *w) / 2;
*ty = (canvas->height - *h) / 2;
}
static void osdDrawArtificialHorizonShapes(displayCanvas_t *canvas, float pitchAngle, float rollAngle)
{
int barWidth = (OSD_AHI_WIDTH - 1) * canvas->gridElementWidth;
int levelBarWidth = barWidth * (3.0/4);
float pixelsPerDegreeLevel = 3.5f;
int borderSize = 3;
char buf[12];
int lx;
int ty;
int maxWidth;
int maxHeight;
osdArtificialHorizonRect(canvas, &lx, &ty, &maxWidth, &maxHeight);
displayCanvasContextPush(canvas);
int rx = lx + maxWidth;
int by = ty + maxHeight;
displayCanvasSetStrokeColor(canvas, DISPLAY_CANVAS_COLOR_BLACK);
displayCanvasMoveToPoint(canvas, lx, ty + borderSize);
displayCanvasStrokeLineToPoint(canvas, lx, ty);
displayCanvasStrokeLineToPoint(canvas, lx + borderSize, ty);
displayCanvasMoveToPoint(canvas, rx, ty + borderSize);
displayCanvasStrokeLineToPoint(canvas, rx, ty);
displayCanvasStrokeLineToPoint(canvas, rx - borderSize, ty);
displayCanvasMoveToPoint(canvas,lx, by - borderSize);
displayCanvasStrokeLineToPoint(canvas, lx, by);
displayCanvasStrokeLineToPoint(canvas, lx + borderSize, by);
displayCanvasMoveToPoint(canvas, rx, by - borderSize);
displayCanvasStrokeLineToPoint(canvas, rx, by);
displayCanvasStrokeLineToPoint(canvas, rx - borderSize, by);
displayCanvasClipToRect(canvas, lx + 1, ty + 1, maxWidth - 2, maxHeight - 2);
osdGridBufferClearPixelRect(canvas, lx, ty, maxWidth, maxHeight);
displayCanvasSetStrokeColor(canvas, DISPLAY_CANVAS_COLOR_WHITE);
displayCanvasSetLineOutlineColor(canvas, DISPLAY_CANVAS_COLOR_BLACK);
// The draw just the 5 bars closest to the current pitch level
float pitchDegrees = RADIANS_TO_DEGREES(pitchAngle);
float pitchCenter = roundf(pitchDegrees / 10.0f);
float pitchOffset = -pitchDegrees * pixelsPerDegreeLevel;
float translateX = canvas->width / 2;
float translateY = canvas->height / 2;
displayCanvasCtmTranslate(canvas, 0, pitchOffset);
displayCanvasContextPush(canvas);
displayCanvasCtmRotate(canvas, rollAngle);
displayCanvasCtmTranslate(canvas, translateX, translateY);
for (int ii = pitchCenter - 2; ii <= pitchCenter + 2; ii++) {
if (ii == 0) {
displayCanvasSetLineOutlineType(canvas, DISPLAY_CANVAS_OUTLINE_TYPE_BOTTOM);
displayCanvasMoveToPoint(canvas, -barWidth / 2, 0);
displayCanvasStrokeLineToPoint(canvas, -AHI_CROSSHAIR_MARGIN, 0);
displayCanvasMoveToPoint(canvas, barWidth / 2, 0);
displayCanvasStrokeLineToPoint(canvas, AHI_CROSSHAIR_MARGIN, 0);
continue;
}
int pos = ii * 10 * pixelsPerDegreeLevel;
int margin = (ii > 9 || ii < -9) ? 9 : 6;
osdDrawArtificialHorizonLevelLine(canvas, levelBarWidth, -pos, margin);
}
displayCanvasContextPop(canvas);
displayCanvasCtmTranslate(canvas, translateX, translateY);
displayCanvasCtmScale(canvas, 0.5f, 0.5f);
// Draw line labels
float sx = sin_approx(rollAngle);
float sy = cos_approx(-rollAngle);
for (int ii = pitchCenter - 2; ii <= pitchCenter + 2; ii++) {
if (ii == 0) {
continue;
}
int level = ii * 10;
int absLevel = ABS(level);
itoa(absLevel, buf, 10);
int pos = level * pixelsPerDegreeLevel;
int charY = 9 - pos * 2;
int cx = (absLevel >= 100 ? -1.5f : -1.0f) * canvas->gridElementWidth;
int px = cx + (pitchOffset + pos) * sx * 2;
int py = -charY - (pitchOffset + pos) * (1 - sy) * 2;
displayCanvasDrawString(canvas, px, py, buf, 0);
}
displayCanvasContextPop(canvas);
}
void osdDrawArtificialHorizonLine(displayCanvas_t *canvas, float pitchAngle, float rollAngle, bool erase)
{
int barWidth = (OSD_AHI_WIDTH - 1) * canvas->gridElementWidth;
int maxHeight = canvas->height;
float pixelsPerDegreeLevel = 1.5f;
int maxWidth = (OSD_AHI_WIDTH + 1) * canvas->gridElementWidth;
int lx = (canvas->width - maxWidth) / 2;
displayCanvasContextPush(canvas);
displayCanvasClipToRect(canvas, lx, 0, maxWidth, maxHeight);
osdGridBufferClearPixelRect(canvas, lx, 0, maxWidth, maxHeight);
if (erase) {
displayCanvasSetStrokeColor(canvas, DISPLAY_CANVAS_COLOR_TRANSPARENT);
displayCanvasSetLineOutlineColor(canvas, DISPLAY_CANVAS_COLOR_TRANSPARENT);
} else {
displayCanvasSetStrokeColor(canvas, DISPLAY_CANVAS_COLOR_WHITE);
displayCanvasSetLineOutlineColor(canvas, DISPLAY_CANVAS_COLOR_BLACK);
}
float pitchDegrees = RADIANS_TO_DEGREES(pitchAngle);
float pitchOffset = -pitchDegrees * pixelsPerDegreeLevel;
float translateX = canvas->width / 2;
float translateY = canvas->height / 2;
displayCanvasCtmTranslate(canvas, 0, pitchOffset);
displayCanvasCtmRotate(canvas, rollAngle);
displayCanvasCtmTranslate(canvas, translateX, translateY);
displayCanvasSetStrokeWidth(canvas, 2);
displayCanvasSetLineOutlineType(canvas, DISPLAY_CANVAS_OUTLINE_TYPE_BOTTOM);
displayCanvasMoveToPoint(canvas, -barWidth / 2, 0);
displayCanvasStrokeLineToPoint(canvas, -AHI_CROSSHAIR_MARGIN, 0);
displayCanvasMoveToPoint(canvas, barWidth / 2, 0);
displayCanvasStrokeLineToPoint(canvas, AHI_CROSSHAIR_MARGIN, 0);
displayCanvasContextPop(canvas);
}
static bool osdCanvasDrawArtificialHorizonWidget(displayPort_t *display, displayCanvas_t *canvas, const osdDrawPoint_t *p, float pitchAngle, float rollAngle)
{
UNUSED(display);
UNUSED(p);
const int instance = 0;
static int iterations = 0;
static bool configured = false;
displayWidgets_t widgets;
if (displayCanvasGetWidgets(&widgets, canvas) &&
displayWidgetsSupportedInstances(&widgets, DISPLAY_WIDGET_TYPE_AHI) >= instance) {
int ahiWidth = osdConfig()->ahi_width;
int ahiX = (canvas->width - ahiWidth) / 2;
int ahiHeight = osdConfig()->ahi_height;
int ahiY = ((canvas->height - ahiHeight) / 2);
ahiY += osdConfig()->ahi_vertical_offset;
if (ahiY < 0) {
ahiY = 0;
}
if (ahiY + ahiHeight >= canvas->height) {
ahiY = canvas->height - ahiHeight - 1;
}
if (!configured) {
widgetAHIStyle_e ahiStyle = 0;
switch ((osd_ahi_style_e)osdConfig()->ahi_style) {
case OSD_AHI_STYLE_DEFAULT:
ahiStyle = DISPLAY_WIDGET_AHI_STYLE_STAIRCASE;
break;
case OSD_AHI_STYLE_LINE:
ahiStyle = DISPLAY_WIDGET_AHI_STYLE_LINE;
break;
}
widgetAHIConfiguration_t config = {
.rect.x = ahiX,
.rect.y = ahiY,
.rect.w = ahiWidth,
.rect.h = ahiHeight,
.style = ahiStyle,
.options = osdConfig()->ahi_bordered ? DISPLAY_WIDGET_AHI_OPTION_SHOW_CORNERS : 0,
.crosshairMargin = AHI_CROSSHAIR_MARGIN,
.strokeWidth = 1,
};
if (!displayWidgetsConfigureAHI(&widgets, instance, &config)) {
return false;
}
configured = true;
}
// The widget displays 270degs before fixing the bar at the top/bottom
// so that's 135degs each direction. Map that to the configured limit.
const float halfRange = 135.0f;
const float limit = halfRange / 180.0f * M_PIf;
float multiplier = osdConfig()->ahi_style == OSD_AHI_STYLE_DEFAULT ? 1.0f : halfRange / osdConfig()->ahi_max_pitch;
widgetAHIData_t data = {
.pitch = constrainf(pitchAngle * multiplier, -limit, limit),
.roll = rollAngle,
};
if (displayWidgetsDrawAHI(&widgets, instance, &data)) {
if (++iterations == 10) {
iterations = 0;
osdGridBufferClearPixelRect(canvas, ahiX, ahiY, ahiWidth, ahiHeight);
}
return true;
}
}
return false;
}
void osdCanvasDrawArtificialHorizon(displayPort_t *display, displayCanvas_t *canvas, const osdDrawPoint_t *p, float pitchAngle, float rollAngle)
{
UNUSED(display);
UNUSED(p);
static float prevPitchAngle = 9999;
static float prevRollAngle = 9999;
static timeMs_t nextDrawMaxMs = 0;
static timeMs_t nextDrawMinMs = 0;
timeMs_t now = millis();
float totalError = fabsf(prevPitchAngle - pitchAngle) + fabsf(prevRollAngle - rollAngle);
if ((now > nextDrawMinMs && totalError > 0.05f)|| now > nextDrawMaxMs) {
if (!osdCanvasDrawArtificialHorizonWidget(display, canvas, p, pitchAngle, rollAngle)) {
switch ((osd_ahi_style_e)osdConfig()->ahi_style) {
case OSD_AHI_STYLE_DEFAULT:
{
int x, y, w, h;
osdArtificialHorizonRect(canvas, &x, &y, &w, &h);
displayCanvasClearRect(canvas, x, y, w, h);
osdDrawArtificialHorizonShapes(canvas, pitchAngle, rollAngle);
break;
}
case OSD_AHI_STYLE_LINE:
osdDrawArtificialHorizonLine(canvas, prevPitchAngle, prevRollAngle, true);
osdDrawArtificialHorizonLine(canvas, pitchAngle, rollAngle, false);
break;
}
}
prevPitchAngle = pitchAngle;
prevRollAngle = rollAngle;
nextDrawMinMs = now + AHI_MIN_DRAW_INTERVAL_MS;
nextDrawMaxMs = now + AHI_MAX_DRAW_INTERVAL_MS;
}
}
void osdCanvasDrawHeadingGraph(displayPort_t *display, displayCanvas_t *canvas, const osdDrawPoint_t *p, int heading)
{
static const uint8_t graph[] = {
SYM_HEADING_W,
SYM_HEADING_LINE,
SYM_HEADING_DIVIDED_LINE,
SYM_HEADING_LINE,
SYM_HEADING_N,
SYM_HEADING_LINE,
SYM_HEADING_DIVIDED_LINE,
SYM_HEADING_LINE,
SYM_HEADING_E,
SYM_HEADING_LINE,
SYM_HEADING_DIVIDED_LINE,
SYM_HEADING_LINE,
SYM_HEADING_S,
SYM_HEADING_LINE,
SYM_HEADING_DIVIDED_LINE,
SYM_HEADING_LINE,
SYM_HEADING_W,
SYM_HEADING_LINE,
SYM_HEADING_DIVIDED_LINE,
SYM_HEADING_LINE,
SYM_HEADING_N,
SYM_HEADING_LINE,
SYM_HEADING_DIVIDED_LINE,
SYM_HEADING_LINE,
SYM_HEADING_E,
SYM_HEADING_LINE,
};
STATIC_ASSERT(sizeof(graph) > (3599 / OSD_HEADING_GRAPH_DECIDEGREES_PER_CHAR) + OSD_HEADING_GRAPH_WIDTH + 1, graph_is_too_short);
char buf[OSD_HEADING_GRAPH_WIDTH + 1];
int px;
int py;
osdDrawPointGetPixels(&px, &py, display, canvas, p);
int rw = OSD_HEADING_GRAPH_WIDTH * canvas->gridElementWidth;
int rh = canvas->gridElementHeight;
displayCanvasClipToRect(canvas, px, py, rw, rh);
int idx = heading / OSD_HEADING_GRAPH_DECIDEGREES_PER_CHAR;
int offset = ((heading % OSD_HEADING_GRAPH_DECIDEGREES_PER_CHAR) * canvas->gridElementWidth) / OSD_HEADING_GRAPH_DECIDEGREES_PER_CHAR;
memcpy_fn(buf, graph + idx, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
// We need a +1 because characters are 12px wide, so
// they can't have a 1px arrow centered. All existing fonts
// place the arrow at 5px, hence there's a 1px offset.
// TODO: Put this in font metadata and read it back.
displayCanvasDrawString(canvas, px - offset + 1, py, buf, DISPLAY_CANVAS_BITMAP_OPT_ERASE_TRANSPARENT);
displayCanvasSetStrokeColor(canvas, DISPLAY_CANVAS_COLOR_BLACK);
displayCanvasSetFillColor(canvas, DISPLAY_CANVAS_COLOR_WHITE);
int rmx = px + rw / 2;
displayCanvasFillStrokeTriangle(canvas, rmx - 2, py - 1, rmx + 2, py - 1, rmx, py + 1);
}
static int32_t osdCanvasSidebarGetValue(osd_sidebar_scroll_e scroll)
{
switch (scroll) {
case OSD_SIDEBAR_SCROLL_NONE:
break;
case OSD_SIDEBAR_SCROLL_ALTITUDE:
switch ((osd_unit_e)osdConfig()->units) {
case OSD_UNIT_UK:
FALLTHROUGH;
case OSD_UNIT_GA:
FALLTHROUGH;
case OSD_UNIT_IMPERIAL:
return CENTIMETERS_TO_CENTIFEET(osdGetAltitude());
case OSD_UNIT_METRIC_MPH:
FALLTHROUGH;
case OSD_UNIT_METRIC:
return osdGetAltitude();
}
break;
case OSD_SIDEBAR_SCROLL_SPEED:
{
#if defined(USE_GPS)
int16_t speed = osdGetSpeedFromSelectedSource();
switch ((osd_unit_e)osdConfig()->units) {
case OSD_UNIT_UK:
FALLTHROUGH;
case OSD_UNIT_METRIC_MPH:
FALLTHROUGH;
case OSD_UNIT_IMPERIAL:
// cms/s to (mi/h) * 100
return speed * 224 / 100;
case OSD_UNIT_GA:
// cm/s to Knots * 100
return (int)(speed * 0.019438444924406) * 100;
case OSD_UNIT_METRIC:
// cm/s to (km/h) * 100
return speed * 36 / 10;
}
#endif
break;
}
case OSD_SIDEBAR_SCROLL_HOME_DISTANCE:
#if defined(USE_GPS)
switch ((osd_unit_e)osdConfig()->units) {
case OSD_UNIT_UK:
FALLTHROUGH;
case OSD_UNIT_GA:
FALLTHROUGH;
case OSD_UNIT_IMPERIAL:
return CENTIMETERS_TO_CENTIFEET(GPS_distanceToHome * 100);
case OSD_UNIT_METRIC_MPH:
FALLTHROUGH;
case OSD_UNIT_METRIC:
return GPS_distanceToHome * 100;
#endif
}
break;
}
return 0;
}
static uint8_t osdCanvasSidebarGetOptions(int *width, osd_sidebar_scroll_e scroll)
{
switch (scroll) {
case OSD_SIDEBAR_SCROLL_NONE:
break;
case OSD_SIDEBAR_SCROLL_ALTITUDE:
FALLTHROUGH;
case OSD_SIDEBAR_SCROLL_SPEED:
FALLTHROUGH;
case OSD_SIDEBAR_SCROLL_HOME_DISTANCE:
*width = OSD_CHAR_WIDTH * 5; // 4 numbers + unit
return 0;
}
*width = OSD_CHAR_WIDTH;
return DISPLAY_WIDGET_SIDEBAR_OPTION_STATIC | DISPLAY_WIDGET_SIDEBAR_OPTION_UNLABELED;
}
static void osdCanvasSidebarGetUnit(osdUnit_t *unit, uint16_t *countsPerStep, osd_sidebar_scroll_e scroll)
{
// We always count in 1/100 units, converting to
// "centifeet" when displaying imperial units
unit->scale = 100;
switch (scroll) {
case OSD_SIDEBAR_SCROLL_NONE:
unit->symbol = 0;
unit->divisor = 0;
unit->divided_symbol = 0;
*countsPerStep = 1;
break;
case OSD_SIDEBAR_SCROLL_ALTITUDE:
switch ((osd_unit_e)osdConfig()->units) {
case OSD_UNIT_UK:
FALLTHROUGH;
case OSD_UNIT_GA:
FALLTHROUGH;
case OSD_UNIT_IMPERIAL:
unit->symbol = SYM_ALT_FT;
unit->divisor = FEET_PER_KILOFEET;
unit->divided_symbol = SYM_ALT_KFT;
*countsPerStep = 50;
break;
case OSD_UNIT_METRIC_MPH:
FALLTHROUGH;
case OSD_UNIT_METRIC:
unit->symbol = SYM_ALT_M;
unit->divisor = METERS_PER_KILOMETER;
unit->divided_symbol = SYM_ALT_KM;
*countsPerStep = 20;
break;
}
break;
case OSD_SIDEBAR_SCROLL_SPEED:
switch ((osd_unit_e)osdConfig()->units) {
case OSD_UNIT_UK:
FALLTHROUGH;
case OSD_UNIT_METRIC_MPH:
FALLTHROUGH;
case OSD_UNIT_IMPERIAL:
unit->symbol = SYM_MPH;
unit->divisor = 0;
unit->divided_symbol = 0;
*countsPerStep = 5;
break;
case OSD_UNIT_GA:
unit->symbol = SYM_KT;
unit->divisor = 0;
unit->divided_symbol = 0;
*countsPerStep = 5;
break;
case OSD_UNIT_METRIC:
unit->symbol = SYM_KMH;
unit->divisor = 0;
unit->divided_symbol = 0;
*countsPerStep = 10;
break;
}
break;
case OSD_SIDEBAR_SCROLL_HOME_DISTANCE:
switch ((osd_unit_e)osdConfig()->units) {
case OSD_UNIT_UK:
FALLTHROUGH;
case OSD_UNIT_IMPERIAL:
unit->symbol = SYM_FT;
unit->divisor = FEET_PER_MILE;
unit->divided_symbol = SYM_MI;
*countsPerStep = 300;
break;
case OSD_UNIT_GA:
unit->symbol = SYM_FT;
unit->divisor = (int)FEET_PER_NAUTICALMILE;
unit->divided_symbol = SYM_NM;
*countsPerStep = 300;
break;
case OSD_UNIT_METRIC_MPH:
FALLTHROUGH;
case OSD_UNIT_METRIC:
unit->symbol = SYM_M;
unit->divisor = METERS_PER_KILOMETER;
unit->divided_symbol = SYM_KM;
*countsPerStep = 100;
break;
}
break;
}
}
static bool osdCanvasDrawSidebar(uint32_t *configured, displayWidgets_t *widgets,
displayCanvas_t *canvas,
int instance,
osd_sidebar_scroll_e scroll, unsigned scrollStep)
{
STATIC_ASSERT(OSD_SIDEBAR_SCROLL_MAX <= 3, adjust_scroll_shift);
STATIC_ASSERT(OSD_UNIT_MAX <= 5, adjust_units_shift);
// Configuration
uint32_t configuration = scrollStep << 16 | (unsigned)osdConfig()->sidebar_horizontal_offset << 8 | scroll << 6 | osdConfig()->units << 4;
if (configuration != *configured) {
int width;
uint8_t options = osdCanvasSidebarGetOptions(&width, scroll);
uint8_t ex;
uint8_t ey;
osdCrosshairPosition(&ex, &ey);
const int height = 2 * OSD_AH_SIDEBAR_HEIGHT_POS * OSD_CHAR_HEIGHT;
const int y = (ey - OSD_AH_SIDEBAR_HEIGHT_POS) * OSD_CHAR_HEIGHT;
widgetSidebarConfiguration_t config = {
.rect.y = y,
.rect.w = width,
.rect.h = height,
.options = options,
.divisions = OSD_AH_SIDEBAR_HEIGHT_POS * 2,
};
uint16_t countsPerStep = 0;
osdCanvasSidebarGetUnit(&config.unit, &countsPerStep, scroll);
int center = ex * OSD_CHAR_WIDTH;
int horizontalOffset = OSD_AH_SIDEBAR_WIDTH_POS * OSD_CHAR_WIDTH + osdConfig()->sidebar_horizontal_offset;
if (instance == WIDGET_SIDEBAR_LEFT_INSTANCE) {
config.rect.x = MAX(center - horizontalOffset - width, 0);
config.options |= DISPLAY_WIDGET_SIDEBAR_OPTION_LEFT;
} else {
config.rect.x = MIN(center + horizontalOffset, canvas->width - width - 1);
}
if (scrollStep > 0) {
countsPerStep = scrollStep;
}
config.counts_per_step = config.unit.scale * countsPerStep;
if (!displayWidgetsConfigureSidebar(widgets, instance, &config)) {
return false;
}
*configured = configuration;
}
// Actual drawing
int32_t data = osdCanvasSidebarGetValue(scroll);
return displayWidgetsDrawSidebar(widgets, instance, data);
}
bool osdCanvasDrawSidebars(displayPort_t *display, displayCanvas_t *canvas)
{
UNUSED(display);
static uint32_t leftConfigured = UINT32_MAX;
static uint32_t rightConfigured = UINT32_MAX;
static timeMs_t nextRedraw = 0;
timeMs_t now = millis();
if (now < nextRedraw) {
return true;
}
displayWidgets_t widgets;
if (displayCanvasGetWidgets(&widgets, canvas)) {
if (!osdCanvasDrawSidebar(&leftConfigured, &widgets, canvas,
WIDGET_SIDEBAR_LEFT_INSTANCE,
osdConfig()->left_sidebar_scroll,
osdConfig()->left_sidebar_scroll_step)) {
return false;
}
if (!osdCanvasDrawSidebar(&rightConfigured, &widgets, canvas,
WIDGET_SIDEBAR_RIGHT_INSTANCE,
osdConfig()->right_sidebar_scroll,
osdConfig()->right_sidebar_scroll_step)) {
return false;
}
nextRedraw = now + SIDEBAR_REDRAW_INTERVAL_MS;
return true;
}
return false;
}
#endif