mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-04 03:06:12 +00:00
feat(compass): replace dial with a horizontal heading tape
The shrunken ring left only a few-pixel needle in the OLED's short vertical space. Switch to a linear heading tape: a fixed travel-direction pointer at centre over a scrolling N..E..S..W scale (30° ticks, N/E/S/W labelled), with the large degrees+cardinal readout below. The tape is purely linear (no trig) and uses the available width, so it stays readable on the small panel. Update the Compass docs to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -154,9 +154,9 @@ Periodically broadcasts a 0-hop advert with your GPS position. Configurable inte
|
|||||||
|
|
||||||
## Compass
|
## Compass
|
||||||
|
|
||||||
A heads-up GPS compass. The L1 has no magnetometer, so the heading is the **course over ground** — derived from how your GPS position moves over the last few seconds. The direction you are travelling is the fixed index at the **top** of the ring, and the compass card (the North needle plus the N/E/S/W letters) rotates underneath it as you turn. A large numeric readout below shows your course in degrees and cardinal (e.g. `145° SE`).
|
A heads-up GPS compass. The L1 has no magnetometer, so the heading is the **course over ground** — derived from how your GPS position moves over the last few seconds. The display is a horizontal **heading tape**: a fixed travel-direction pointer sits at the centre and the N..E..S..W scale scrolls underneath it as you turn, so whatever is under the pointer is your current course. A large numeric readout below shows that course in degrees and cardinal (e.g. `145° SE`).
|
||||||
|
|
||||||
Because the heading comes from movement, it only updates while you are actually moving: standing still shows *move to set heading* (and navigation's **Hdg** line reads `--`). Gross GPS jumps are rejected so a single bad fix can't swing the needle. The heading source runs whenever there's a GPS fix — recording a trail is **not** required.
|
Because the heading comes from movement, it only updates while you are actually moving: standing still shows *move to set heading* (and navigation's **Hdg** line reads `--`). Gross GPS jumps are rejected so a single bad fix can't swing the heading. The heading source runs whenever there's a GPS fix — recording a trail is **not** required.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// Tools › Compass. A standalone heading view: shows the device's course over
|
// Tools › Compass. A standalone heading view: shows the device's course over
|
||||||
// ground (derived from GPS movement — there is no magnetometer) as a rotating
|
// ground (derived from GPS movement — there is no magnetometer) as a horizontal
|
||||||
// arrow plus a numeric degrees + cardinal readout. When standing still the
|
// scrolling tape (N..E..S..W) under a fixed travel-direction marker, plus a
|
||||||
// heading is undefined, so it shows a hint to move.
|
// large numeric degrees + cardinal readout. When standing still the heading is
|
||||||
|
// undefined, so it shows a hint to move.
|
||||||
|
//
|
||||||
|
// A linear tape (no trig) reads well in the short vertical space of the OLED,
|
||||||
|
// where a circular dial leaves only a few-pixel needle.
|
||||||
//
|
//
|
||||||
// Reuses UITask's COG ring (currentCourse) — works whether or not a trail is
|
// Reuses UITask's COG ring (currentCourse) — works whether or not a trail is
|
||||||
// being recorded.
|
// being recorded.
|
||||||
|
|
||||||
#include "../GeoUtils.h"
|
#include "../GeoUtils.h"
|
||||||
#include "GfxUtils.h"
|
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
class CompassScreen : public UIScreen {
|
class CompassScreen : public UIScreen {
|
||||||
UITask* _task;
|
UITask* _task;
|
||||||
|
|
||||||
bool gpsValid() const { int32_t lat, lon; return _task->currentLocation(lat, lon); }
|
bool gpsValid() const { int32_t lat, lon; return _task->currentLocation(lat, lon); }
|
||||||
|
|
||||||
|
static const char* cardinalLetter(int deg) {
|
||||||
|
switch (deg) { case 0: return "N"; case 90: return "E";
|
||||||
|
case 180: return "S"; case 270: return "W"; }
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
CompassScreen(UITask* task) : _task(task) {}
|
CompassScreen(UITask* task) : _task(task) {}
|
||||||
void enter() {}
|
void enter() {}
|
||||||
@@ -35,72 +43,71 @@ public:
|
|||||||
const int bigH = display.getLineHeight();
|
const int bigH = display.getLineHeight();
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
const int top = hdr + 2;
|
const int top = hdr + 2;
|
||||||
const int bottom = display.height() - bigH - 2;
|
const int readout_y = display.height() - bigH - 1; // size-2 readout baseline
|
||||||
const int cy = (top + bottom) / 2;
|
const int mid = (top + readout_y) / 2;
|
||||||
// The cardinal letters ride on the outer radius; the ring is drawn a glyph
|
|
||||||
// smaller so the rotating letters sit just outside it and never touch it.
|
|
||||||
int outer = (bottom - top) / 2;
|
|
||||||
int outerx = display.width() / 2 - 2;
|
|
||||||
if (outerx < outer) outer = outerx;
|
|
||||||
int lr = outer - ch / 2 - 1; // radius of the cardinal-letter centres
|
|
||||||
int r = lr - ch / 2 - 3; // ring radius (clear gap below the letters)
|
|
||||||
if (r < 6) { r = 6; lr = r + ch / 2 + 3; }
|
|
||||||
|
|
||||||
if (!gpsValid()) {
|
if (!gpsValid()) {
|
||||||
display.drawTextCentered(cx, cy - display.getLineHeight() / 2, "No GPS fix");
|
display.drawTextCentered(cx, mid - ch / 2, "No GPS fix");
|
||||||
return 1000;
|
return 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
int cog;
|
int cog;
|
||||||
bool have = _task->currentCourse(cog);
|
bool have = _task->currentCourse(cog);
|
||||||
|
|
||||||
gfx::drawCircle(display, cx, cy, r); // compass ring
|
|
||||||
|
|
||||||
// Fixed forward index at the top: the direction you are travelling is
|
|
||||||
// always "up". The compass card (North) rotates underneath it.
|
|
||||||
display.fillRect(cx - 1, cy - r - 2, 3, 1);
|
|
||||||
display.fillRect(cx, cy - r - 1, 1, 2);
|
|
||||||
|
|
||||||
if (!have) {
|
if (!have) {
|
||||||
// No stable heading yet — keep the ring as context but put the hint in
|
display.drawTextCentered(cx, mid - ch, "move to");
|
||||||
// the bottom readout band so it doesn't overlap the circle.
|
display.drawTextCentered(cx, mid, "set heading");
|
||||||
int lh = display.getLineHeight();
|
|
||||||
display.drawTextCentered(cx, bottom + 1, "move to");
|
|
||||||
display.drawTextCentered(cx, bottom + 1 + lh, "set heading");
|
|
||||||
return 1000;
|
return 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heads-up compass: with "up" = course, an absolute bearing B sits at
|
// ── heading tape ─────────────────────────────────────────────────────────
|
||||||
// screen angle (B - cog). Draw the rotating card — a North needle plus the
|
// Centre of the tape = current course; bearings spread left/right at a fixed
|
||||||
// other three cardinals as letters around the ring.
|
// pixels-per-degree. HALF_SPAN degrees reach each edge.
|
||||||
auto cardPos = [&](int bearing, int rad_px, int& px, int& py) {
|
const int area_x = 2;
|
||||||
float a = (bearing - cog) * (float)M_PI / 180.0f;
|
const int area_w = display.width() - 4;
|
||||||
px = cx + (int)(sinf(a) * rad_px);
|
const float HALF_SPAN = 80.0f; // degrees visible to each side
|
||||||
py = cy - (int)(cosf(a) * rad_px);
|
const float ppd = (area_w / 2.0f) / HALF_SPAN;
|
||||||
};
|
|
||||||
|
|
||||||
// North needle from centre.
|
const int pointerH = 6;
|
||||||
int nx, ny; cardPos(0, r - 2, nx, ny);
|
const int tickMajor = 6;
|
||||||
gfx::drawLine(display, cx, cy, nx, ny);
|
const int tickMinor = 3;
|
||||||
float nrad = (0 - cog) * (float)M_PI / 180.0f;
|
const int blockH = pointerH + 1 + tickMajor + 2 + ch;
|
||||||
for (int da = -25; da <= 25; da += 50) {
|
const int block_top = top + ((readout_y - top) - blockH) / 2;
|
||||||
float a = nrad + (float)M_PI + da * (float)M_PI / 180.0f;
|
const int line_y = block_top + pointerH; // scale baseline
|
||||||
gfx::drawLine(display, nx, ny, nx + (int)(sinf(a) * 4), ny - (int)(cosf(a) * 4));
|
const int label_y = line_y + 1 + tickMajor + 2;
|
||||||
|
const int x_lo = area_x, x_hi = area_x + area_w;
|
||||||
|
|
||||||
|
// Scale baseline.
|
||||||
|
display.fillRect(area_x, line_y, area_w, 1);
|
||||||
|
|
||||||
|
// Ticks + cardinal labels every 30°; majors (N/E/S/W) are taller + labelled.
|
||||||
|
for (int deg = 0; deg < 360; deg += 30) {
|
||||||
|
int delta = deg - cog;
|
||||||
|
while (delta > 180) delta -= 360;
|
||||||
|
while (delta < -180) delta += 360;
|
||||||
|
int x = cx + (int)(delta * ppd);
|
||||||
|
if (x < x_lo || x > x_hi) continue;
|
||||||
|
bool major = (deg % 90 == 0);
|
||||||
|
display.fillRect(x, line_y + 1, 1, major ? tickMajor : tickMinor);
|
||||||
|
if (major) {
|
||||||
|
const char* s = cardinalLetter(deg);
|
||||||
|
display.setCursor(x - cw / 2, label_y);
|
||||||
|
display.print(s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cardinal letters just outside the ring (N over the needle tip).
|
// Fixed travel-direction pointer: a downward triangle whose tip touches the
|
||||||
struct { int b; const char* s; } card[4] = { {0,"N"},{90,"E"},{180,"S"},{270,"W"} };
|
// baseline at screen centre (your current course always sits under it).
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < pointerH; i++) {
|
||||||
int lx, ly; cardPos(card[i].b, lr, lx, ly);
|
int half = pointerH - 1 - i;
|
||||||
display.setCursor(lx - cw / 2, ly - ch / 2);
|
display.fillRect(cx - half, block_top + i, 2 * half + 1, 1);
|
||||||
display.print(card[i].s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Numeric readout below the ring — the absolute course you're travelling.
|
// ── numeric readout ──────────────────────────────────────────────────────
|
||||||
char buf[16];
|
char buf[16];
|
||||||
snprintf(buf, sizeof(buf), "%d %s", cog, geo::bearingCardinal(cog));
|
snprintf(buf, sizeof(buf), "%d %s", cog, geo::bearingCardinal(cog));
|
||||||
display.setTextSize(2);
|
display.setTextSize(2);
|
||||||
display.drawTextCentered(cx, bottom + 1, buf);
|
display.drawTextCentered(cx, readout_y, buf);
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
return 1000;
|
return 1000;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user