mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
maps! version 1 - g key to access
This commit is contained in:
@@ -8,11 +8,11 @@
|
||||
#define FIRMWARE_VER_CODE 8
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "27 Feb 2026"
|
||||
#define FIRMWARE_BUILD_DATE "1 March 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v0.9.5"
|
||||
#define FIRMWARE_VERSION "Meck v0.9.6"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
|
||||
@@ -415,6 +415,7 @@ static uint32_t _atoi(const char* sp) {
|
||||
/* GLOBAL OBJECTS */
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "UITask.h"
|
||||
#include "MapScreen.h" // After BLE — PNGdec headers conflict with BLE if included earlier
|
||||
UITask ui_task(&board, &serial_interface);
|
||||
#endif
|
||||
|
||||
@@ -826,6 +827,35 @@ void loop() {
|
||||
|
||||
sensors.loop();
|
||||
|
||||
// Map screen: periodically update own GPS position and contact markers
|
||||
if (ui_task.isOnMapScreen()) {
|
||||
static unsigned long lastMapUpdate = 0;
|
||||
if (millis() - lastMapUpdate > 30000) { // Every 30 seconds
|
||||
lastMapUpdate = millis();
|
||||
MapScreen* ms = (MapScreen*)ui_task.getMapScreen();
|
||||
if (ms) {
|
||||
// Update own GPS position only when GPS hardware is active
|
||||
#if HAS_GPS
|
||||
if (gpsDuty.isHardwareOn()) {
|
||||
ms->updateGPSPosition(sensors.node_lat, sensors.node_lon);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Always refresh contact markers (new contacts arrive via radio)
|
||||
ms->clearMarkers();
|
||||
ContactsIterator it = the_mesh.startContactsIterator();
|
||||
ContactInfo ci;
|
||||
while (it.hasNext(&the_mesh, ci)) {
|
||||
if (ci.gps_lat != 0 || ci.gps_lon != 0) {
|
||||
double lat = ((double)ci.gps_lat) / 1000000.0;
|
||||
double lon = ((double)ci.gps_lon) / 1000000.0;
|
||||
ms->addMarker(lat, lon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CPU frequency auto-timeout back to idle
|
||||
cpuPower.loop();
|
||||
|
||||
@@ -1720,6 +1750,39 @@ void handleKeyboardInput() {
|
||||
break;
|
||||
#endif
|
||||
|
||||
case 'g':
|
||||
// Open map screen, or re-center on GPS if already on map
|
||||
if (ui_task.isOnMapScreen()) {
|
||||
ui_task.injectKey('g'); // Re-center on GPS
|
||||
} else {
|
||||
Serial.println("Opening map");
|
||||
{
|
||||
MapScreen* ms = (MapScreen*)ui_task.getMapScreen();
|
||||
if (ms) {
|
||||
ms->setSDReady(sdCardReady);
|
||||
ms->setGPSPosition(sensors.node_lat,
|
||||
sensors.node_lon);
|
||||
// Populate contact markers via iterator
|
||||
ms->clearMarkers();
|
||||
ContactsIterator it = the_mesh.startContactsIterator();
|
||||
ContactInfo ci;
|
||||
int markerCount = 0;
|
||||
while (it.hasNext(&the_mesh, ci)) {
|
||||
if (ci.gps_lat != 0 || ci.gps_lon != 0) {
|
||||
double lat = ((double)ci.gps_lat) / 1000000.0;
|
||||
double lon = ((double)ci.gps_lon) / 1000000.0;
|
||||
ms->addMarker(lat, lon);
|
||||
markerCount++;
|
||||
Serial.printf(" marker: %s @ %.4f,%.4f\n", ci.name, lat, lon);
|
||||
}
|
||||
}
|
||||
Serial.printf("MapScreen: %d contacts with GPS position\n", markerCount);
|
||||
}
|
||||
}
|
||||
ui_task.gotoMapScreen();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'n':
|
||||
// Open notes
|
||||
Serial.println("Opening notes");
|
||||
@@ -1735,11 +1798,12 @@ void handleKeyboardInput() {
|
||||
break;
|
||||
|
||||
case 's':
|
||||
// Open settings (from home), or navigate down on channel/contacts/admin/web
|
||||
// Open settings (from home), or navigate down on channel/contacts/admin/web/map
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()
|
||||
#ifdef MECK_WEB_READER
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
|| ui_task.isOnMapScreen()
|
||||
) {
|
||||
ui_task.injectKey('s'); // Pass directly for scrolling
|
||||
} else {
|
||||
@@ -1754,6 +1818,7 @@ void handleKeyboardInput() {
|
||||
#ifdef MECK_WEB_READER
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
|| ui_task.isOnMapScreen()
|
||||
) {
|
||||
ui_task.injectKey('w'); // Pass directly for scrolling
|
||||
} else {
|
||||
@@ -1764,7 +1829,7 @@ void handleKeyboardInput() {
|
||||
|
||||
case 'a':
|
||||
// Navigate left or switch channel (on channel screen)
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()) {
|
||||
ui_task.injectKey('a'); // Pass directly for channel/contacts switching
|
||||
} else {
|
||||
Serial.println("Nav: Previous");
|
||||
@@ -1774,7 +1839,7 @@ void handleKeyboardInput() {
|
||||
|
||||
case 'd':
|
||||
// Navigate right or switch channel (on channel screen)
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()) {
|
||||
ui_task.injectKey('d'); // Pass directly for channel/contacts switching
|
||||
} else {
|
||||
Serial.println("Nav: Next");
|
||||
@@ -1863,9 +1928,18 @@ void handleKeyboardInput() {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'z':
|
||||
// Zoom in on map screen
|
||||
if (ui_task.isOnMapScreen()) {
|
||||
ui_task.injectKey('z');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'x':
|
||||
// Export contacts to SD card (contacts screen only)
|
||||
if (ui_task.isOnContactsScreen()) {
|
||||
// Zoom out on map screen, or export contacts on contacts screen
|
||||
if (ui_task.isOnMapScreen()) {
|
||||
ui_task.injectKey('x');
|
||||
} else if (ui_task.isOnContactsScreen()) {
|
||||
Serial.println("Contacts: Exporting to SD...");
|
||||
int exported = exportContactsToSD();
|
||||
if (exported >= 0) {
|
||||
@@ -1957,6 +2031,11 @@ void handleKeyboardInput() {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
// Pass unhandled keys to map screen (+, -, i, o for zoom)
|
||||
if (ui_task.isOnMapScreen()) {
|
||||
ui_task.injectKey(key);
|
||||
break;
|
||||
}
|
||||
Serial.printf("Unhandled key in normal mode: '%c' (0x%02X)\n", key, key);
|
||||
break;
|
||||
}
|
||||
|
||||
794
examples/companion_radio/ui-new/Mapscreen.h
Normal file
794
examples/companion_radio/ui-new/Mapscreen.h
Normal file
@@ -0,0 +1,794 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// MapScreen — OSM Tile Map for T-Deck Pro E-Ink Display
|
||||
// =============================================================================
|
||||
//
|
||||
// Renders standard OSM "slippy map" PNG tiles from SD card onto the e-ink
|
||||
// display at native 240×320 resolution (bypassing the 128×128 logical grid).
|
||||
//
|
||||
// Tiles are B&W PNGs stored at /tiles/{zoom}/{x}/{y}.png — the same format
|
||||
// used by Ripple, tdeck-maps, and MTD-Script tile downloaders.
|
||||
//
|
||||
// REQUIREMENTS:
|
||||
// 1. Add PNGdec library to platformio.ini:
|
||||
// lib_deps = ... bitbank2/PNGdec@^1.0.1
|
||||
//
|
||||
// 2. Add raw display access to GxEPDDisplay.h (public section):
|
||||
// // --- Raw pixel access for MapScreen (bypasses scaling) ---
|
||||
// void drawPixelRaw(int16_t x, int16_t y, uint16_t color) {
|
||||
// display.drawPixel(x, y, color);
|
||||
// }
|
||||
// int16_t rawWidth() { return display.width(); }
|
||||
// int16_t rawHeight() { return display.height(); }
|
||||
// // Force endFrame() to push to display even if CRC unchanged
|
||||
// // (needed because drawPixelRaw bypasses CRC tracking)
|
||||
// void invalidateFrameCRC() { last_display_crc_value = 0; }
|
||||
//
|
||||
// 3. Add to UITask.h:
|
||||
// #include "MapScreen.h"
|
||||
// UIScreen* map_screen;
|
||||
// void gotoMapScreen();
|
||||
// bool isOnMapScreen() const { return curr == map_screen; }
|
||||
// UIScreen* getMapScreen() const { return map_screen; }
|
||||
//
|
||||
// 4. Initialise in UITask::begin():
|
||||
// map_screen = new MapScreen(this);
|
||||
//
|
||||
// 5. Implement UITask::gotoMapScreen() following gotoTextReader() pattern.
|
||||
//
|
||||
// 6. Hook 'g' key in main.cpp for GPS/Map access:
|
||||
// case 'g':
|
||||
// if (ui_task.isOnMapScreen()) {
|
||||
// // Already on map — 'g' re-centers on GPS
|
||||
// ui_task.injectKey('g');
|
||||
// } else {
|
||||
// Serial.println("Opening map");
|
||||
// {
|
||||
// MapScreen* ms = (MapScreen*)ui_task.getMapScreen();
|
||||
// if (ms) {
|
||||
// ms->setSDReady(sdCardReady);
|
||||
// ms->setGPSPosition(sensors.node_lat,
|
||||
// sensors.node_lon);
|
||||
// // Populate contact markers via iterator
|
||||
// ms->clearMarkers();
|
||||
// ContactsIterator it = the_mesh.startContactsIterator();
|
||||
// ContactInfo ci;
|
||||
// while (it.hasNext(&the_mesh, ci)) {
|
||||
// double lat = ((double)ci.gps_lat) / 1000000.0;
|
||||
// double lon = ((double)ci.gps_lon) / 1000000.0;
|
||||
// ms->addMarker(lat, lon);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ui_task.gotoMapScreen();
|
||||
// }
|
||||
// break;
|
||||
//
|
||||
// 7. Route WASD/zoom keys to map screen in main.cpp (in existing handlers):
|
||||
// For 'w', 's', 'a', 'd' cases, add:
|
||||
// if (ui_task.isOnMapScreen()) { ui_task.injectKey(key); break; }
|
||||
// For the default case, add map screen passthrough:
|
||||
// if (ui_task.isOnMapScreen()) { ui_task.injectKey(key); break; }
|
||||
// This covers +, -, i, o, g (re-center) keys too.
|
||||
//
|
||||
// TILE SOURCES (B&W recommended for e-ink):
|
||||
// - MTD-Script: github.com/fistulareffigy/MTD-Script
|
||||
// - tdeck-maps: github.com/JustDr00py/tdeck-maps
|
||||
// - Stamen Toner style gives best e-ink contrast
|
||||
// =============================================================================
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
#include <PNGdec.h>
|
||||
#undef local // PNGdec's zutil.h defines 'local' as 'static' — breaks any variable named 'local'
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/GxEPDDisplay.h>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layout constants (physical pixel coordinates, 240×320 display)
|
||||
// ---------------------------------------------------------------------------
|
||||
#define MAP_DISPLAY_W 240
|
||||
#define MAP_DISPLAY_H 320
|
||||
|
||||
// Footer bar occupies the bottom — matches other screens' setTextSize(1) footer
|
||||
#define MAP_FOOTER_H 24 // ~24px at bottom for nav hints
|
||||
#define MAP_VIEWPORT_Y 0 // Map starts at top
|
||||
#define MAP_VIEWPORT_H (MAP_DISPLAY_H - MAP_FOOTER_H) // 296px for map
|
||||
|
||||
#define MAP_TILE_SIZE 256 // Standard OSM tile size in pixels
|
||||
#define MAP_DEFAULT_ZOOM 13
|
||||
#define MAP_MIN_ZOOM 1
|
||||
#define MAP_MAX_ZOOM 17
|
||||
|
||||
// PNG decode buffer size — 256×256 RGB = 196KB, but PNGdec streams row-by-row
|
||||
// We only need a line buffer. Allocate in PSRAM for safety.
|
||||
#define MAP_PNG_BUF_SIZE (65536) // 64KB for PNG file read buffer
|
||||
|
||||
// Tile path on SD card
|
||||
#define MAP_TILE_ROOT "/tiles"
|
||||
|
||||
// Pan step: fraction of viewport to move per keypress
|
||||
#define MAP_PAN_FRACTION 4 // 1/4 of viewport per press
|
||||
|
||||
// Contact marker size (pixels)
|
||||
#define MAP_MARKER_SIZE 7 // 7×7 diamond marker
|
||||
#define MAP_MAX_MARKERS 32 // Max contact markers (matches MAX_CONTACTS)
|
||||
|
||||
|
||||
class MapScreen : public UIScreen {
|
||||
public:
|
||||
MapScreen(UITask* task)
|
||||
: _task(task),
|
||||
_einkDisplay(nullptr),
|
||||
_sdReady(false),
|
||||
_needsRedraw(true),
|
||||
_hasFix(false),
|
||||
_centerLat(-33.8688), // Default: Sydney (most Ripple users)
|
||||
_centerLon(151.2093),
|
||||
_gpsLat(0.0),
|
||||
_gpsLon(0.0),
|
||||
_zoom(MAP_DEFAULT_ZOOM),
|
||||
_zoomMin(MAP_MIN_ZOOM),
|
||||
_zoomMax(MAP_MAX_ZOOM),
|
||||
_pngBuf(nullptr),
|
||||
_tileFound(false)
|
||||
{}
|
||||
|
||||
~MapScreen() {
|
||||
if (_pngBuf) { free(_pngBuf); _pngBuf = nullptr; }
|
||||
}
|
||||
|
||||
void setSDReady(bool ready) { _sdReady = ready; }
|
||||
|
||||
// Set initial GPS position (called when opening map — centers viewport)
|
||||
void setGPSPosition(double lat, double lon) {
|
||||
if (lat != 0.0 || lon != 0.0) {
|
||||
_gpsLat = lat;
|
||||
_gpsLon = lon;
|
||||
_centerLat = lat;
|
||||
_centerLon = lon;
|
||||
_hasFix = true;
|
||||
_needsRedraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update own GPS position without moving viewport (called periodically)
|
||||
void updateGPSPosition(double lat, double lon) {
|
||||
if (lat == 0.0 && lon == 0.0) return;
|
||||
if (lat != _gpsLat || lon != _gpsLon) {
|
||||
_gpsLat = lat;
|
||||
_gpsLon = lon;
|
||||
_hasFix = true;
|
||||
_needsRedraw = true; // Redraw to move own-position marker
|
||||
}
|
||||
}
|
||||
|
||||
// Add a location marker (call once per contact before entering map)
|
||||
void clearMarkers() { _numMarkers = 0; }
|
||||
void addMarker(double lat, double lon) {
|
||||
if (_numMarkers >= MAP_MAX_MARKERS) return;
|
||||
if (lat == 0.0 && lon == 0.0) return; // Skip no-location contacts
|
||||
_markers[_numMarkers].lat = lat;
|
||||
_markers[_numMarkers].lon = lon;
|
||||
_numMarkers++;
|
||||
}
|
||||
|
||||
// Refresh contact markers (called periodically from main loop)
|
||||
// Clears and rebuilds — caller iterates contacts and calls addMarker()
|
||||
int getNumMarkers() const { return _numMarkers; }
|
||||
|
||||
// Called when navigating to map screen
|
||||
void enter(DisplayDriver& display) {
|
||||
_einkDisplay = static_cast<GxEPDDisplay*>(&display);
|
||||
_needsRedraw = true;
|
||||
|
||||
// Allocate PNG read buffer in PSRAM on first use
|
||||
if (!_pngBuf) {
|
||||
_pngBuf = (uint8_t*)ps_malloc(MAP_PNG_BUF_SIZE);
|
||||
if (!_pngBuf) {
|
||||
Serial.println("MapScreen: PSRAM alloc failed, trying heap");
|
||||
_pngBuf = (uint8_t*)malloc(MAP_PNG_BUF_SIZE);
|
||||
}
|
||||
if (_pngBuf) {
|
||||
Serial.printf("MapScreen: PNG buffer allocated (%d bytes)\n", MAP_PNG_BUF_SIZE);
|
||||
} else {
|
||||
Serial.println("MapScreen: PNG buffer alloc FAILED");
|
||||
}
|
||||
}
|
||||
|
||||
// Detect available zoom levels from SD card directories
|
||||
detectZoomRange();
|
||||
}
|
||||
|
||||
// ---- UIScreen interface ----
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
if (!_einkDisplay) {
|
||||
_einkDisplay = static_cast<GxEPDDisplay*>(&display);
|
||||
}
|
||||
|
||||
if (!_sdReady) {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(10, 20);
|
||||
display.print("SD card not found");
|
||||
display.setCursor(10, 35);
|
||||
display.print("Insert SD with");
|
||||
display.setCursor(10, 48);
|
||||
display.print("/tiles/{z}/{x}/{y}.png");
|
||||
return 5000;
|
||||
}
|
||||
|
||||
// Always render tiles — UITask clears the buffer via startFrame() before
|
||||
// calling us, so we must redraw every time (e.g. after alert overlays)
|
||||
bool wasRedraw = _needsRedraw;
|
||||
_needsRedraw = false;
|
||||
|
||||
// Render map tiles into the viewport
|
||||
renderMapViewport();
|
||||
|
||||
// Overlay contact markers
|
||||
renderContactMarkers();
|
||||
|
||||
// Crosshair at viewport center
|
||||
renderCrosshair();
|
||||
|
||||
// Footer bar (uses normal display API with scaling)
|
||||
renderFooter(display);
|
||||
|
||||
// Raw pixel writes bypass CRC tracking — force refresh
|
||||
_einkDisplay->invalidateFrameCRC();
|
||||
|
||||
// If user panned/zoomed, allow quick re-render; otherwise idle longer
|
||||
return wasRedraw ? 1000 : 30000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
// Pan distances in degrees — adaptive to zoom level
|
||||
// At zoom Z, one tile covers 360/2^Z degrees of longitude
|
||||
double tileLonSpan = 360.0 / (1 << _zoom);
|
||||
double tileLatSpan = tileLonSpan * cos(_centerLat * PI / 180.0); // Rough approx
|
||||
|
||||
// Pan by 1/MAP_PAN_FRACTION of viewport (viewport ≈ 1 tile)
|
||||
double panLon = tileLonSpan / MAP_PAN_FRACTION;
|
||||
double panLat = tileLatSpan / MAP_PAN_FRACTION;
|
||||
|
||||
switch (c) {
|
||||
// ---- WASD panning ----
|
||||
case 'w':
|
||||
case 'W':
|
||||
_centerLat += panLat;
|
||||
if (_centerLat > 85.05) _centerLat = 85.05; // Web Mercator limit
|
||||
_needsRedraw = true;
|
||||
return true;
|
||||
|
||||
case 's':
|
||||
case 'S':
|
||||
_centerLat -= panLat;
|
||||
if (_centerLat < -85.05) _centerLat = -85.05;
|
||||
_needsRedraw = true;
|
||||
return true;
|
||||
|
||||
case 'a':
|
||||
case 'A':
|
||||
_centerLon -= panLon;
|
||||
if (_centerLon < -180.0) _centerLon += 360.0;
|
||||
_needsRedraw = true;
|
||||
return true;
|
||||
|
||||
case 'd':
|
||||
case 'D':
|
||||
_centerLon += panLon;
|
||||
if (_centerLon > 180.0) _centerLon -= 360.0;
|
||||
_needsRedraw = true;
|
||||
return true;
|
||||
|
||||
// ---- Zoom controls ----
|
||||
case 'z':
|
||||
case 'Z':
|
||||
if (_zoom < _zoomMax) {
|
||||
_zoom++;
|
||||
_needsRedraw = true;
|
||||
Serial.printf("MapScreen: zoom in -> %d\n", _zoom);
|
||||
}
|
||||
return true;
|
||||
|
||||
case 'x':
|
||||
case 'X':
|
||||
if (_zoom > _zoomMin) {
|
||||
_zoom--;
|
||||
_needsRedraw = true;
|
||||
Serial.printf("MapScreen: zoom out -> %d\n", _zoom);
|
||||
}
|
||||
return true;
|
||||
|
||||
// ---- Re-center on GPS fix ----
|
||||
case 'g':
|
||||
if (_hasFix) {
|
||||
_centerLat = _gpsLat;
|
||||
_centerLon = _gpsLon;
|
||||
_needsRedraw = true;
|
||||
Serial.println("MapScreen: re-center on GPS");
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
GxEPDDisplay* _einkDisplay;
|
||||
bool _sdReady;
|
||||
bool _needsRedraw;
|
||||
bool _hasFix;
|
||||
|
||||
// Map state
|
||||
double _centerLat;
|
||||
double _centerLon;
|
||||
double _gpsLat; // Own GPS position (separate from viewport center)
|
||||
double _gpsLon;
|
||||
int _zoom;
|
||||
int _zoomMin; // Detected from SD card
|
||||
int _zoomMax; // Detected from SD card
|
||||
|
||||
// PNG decode buffer (PSRAM)
|
||||
uint8_t* _pngBuf;
|
||||
bool _tileFound; // Did last tile load succeed?
|
||||
|
||||
// PNGdec instance
|
||||
PNG _png;
|
||||
|
||||
// Contacts for marker overlay
|
||||
struct MapMarker { double lat; double lon; };
|
||||
MapMarker _markers[MAP_MAX_MARKERS];
|
||||
int _numMarkers = 0;
|
||||
|
||||
// ---- Rendering state passed to PNG callback ----
|
||||
// PNGdec calls our callback per scanline — we need to know where to draw.
|
||||
// Also carries a PNG* so the static callback can call getLineAsRGB565().
|
||||
struct DrawContext {
|
||||
GxEPDDisplay* display;
|
||||
PNG* png; // Pointer to the decoder (for getLineAsRGB565)
|
||||
int offsetX; // Screen X offset for this tile
|
||||
int offsetY; // Screen Y offset for this tile
|
||||
int viewportY; // Top of viewport (MAP_VIEWPORT_Y)
|
||||
int viewportH; // Height of viewport (MAP_VIEWPORT_H)
|
||||
};
|
||||
DrawContext _drawCtx;
|
||||
|
||||
// ==========================================================================
|
||||
// Detect available zoom levels from /tiles/{z}/ directories on SD
|
||||
// ==========================================================================
|
||||
|
||||
void detectZoomRange() {
|
||||
if (!_sdReady) return;
|
||||
|
||||
_zoomMin = MAP_MAX_ZOOM;
|
||||
_zoomMax = MAP_MIN_ZOOM;
|
||||
|
||||
char path[32];
|
||||
for (int z = MAP_MIN_ZOOM; z <= MAP_MAX_ZOOM; z++) {
|
||||
snprintf(path, sizeof(path), MAP_TILE_ROOT "/%d", z);
|
||||
if (SD.exists(path)) {
|
||||
if (z < _zoomMin) _zoomMin = z;
|
||||
if (z > _zoomMax) _zoomMax = z;
|
||||
}
|
||||
}
|
||||
|
||||
// If no tiles found, reset to defaults
|
||||
if (_zoomMin > _zoomMax) {
|
||||
_zoomMin = MAP_MIN_ZOOM;
|
||||
_zoomMax = MAP_MAX_ZOOM;
|
||||
Serial.println("MapScreen: no tile directories found");
|
||||
} else {
|
||||
Serial.printf("MapScreen: detected zoom range %d-%d\n", _zoomMin, _zoomMax);
|
||||
}
|
||||
|
||||
// Clamp current zoom to available range
|
||||
if (_zoom > _zoomMax) _zoom = _zoomMax;
|
||||
if (_zoom < _zoomMin) _zoom = _zoomMin;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Tile coordinate math (Web Mercator / Slippy Map convention)
|
||||
// ==========================================================================
|
||||
|
||||
// Convert lat/lon to tile X,Y and sub-tile pixel offset at given zoom
|
||||
static void latLonToTileXY(double lat, double lon, int zoom,
|
||||
int& tileX, int& tileY,
|
||||
int& pixelX, int& pixelY)
|
||||
{
|
||||
int n = 1 << zoom;
|
||||
|
||||
// Tile X (longitude is linear)
|
||||
double x = (lon + 180.0) / 360.0 * n;
|
||||
tileX = (int)floor(x);
|
||||
pixelX = (int)((x - tileX) * MAP_TILE_SIZE);
|
||||
|
||||
// Tile Y (latitude uses Mercator projection)
|
||||
double latRad = lat * PI / 180.0;
|
||||
double y = (1.0 - log(tan(latRad) + 1.0 / cos(latRad)) / PI) / 2.0 * n;
|
||||
tileY = (int)floor(y);
|
||||
pixelY = (int)((y - tileY) * MAP_TILE_SIZE);
|
||||
}
|
||||
|
||||
// Convert tile X,Y + pixel offset back to lat/lon
|
||||
static void tileXYToLatLon(int tileX, int tileY, int pixelX, int pixelY,
|
||||
int zoom, double& lat, double& lon)
|
||||
{
|
||||
int n = 1 << zoom;
|
||||
double x = tileX + (double)pixelX / MAP_TILE_SIZE;
|
||||
double y = tileY + (double)pixelY / MAP_TILE_SIZE;
|
||||
|
||||
lon = x / n * 360.0 - 180.0;
|
||||
double latRad = atan(sinh(PI * (1.0 - 2.0 * y / n)));
|
||||
lat = latRad * 180.0 / PI;
|
||||
}
|
||||
|
||||
// Convert a lat/lon to pixel position within the current viewport
|
||||
// Returns false if off-screen
|
||||
bool latLonToScreen(double lat, double lon, int& screenX, int& screenY) {
|
||||
int centerTileX, centerTileY, centerPixelX, centerPixelY;
|
||||
latLonToTileXY(_centerLat, _centerLon, _zoom,
|
||||
centerTileX, centerTileY, centerPixelX, centerPixelY);
|
||||
|
||||
int targetTileX, targetTileY, targetPixelX, targetPixelY;
|
||||
latLonToTileXY(lat, lon, _zoom,
|
||||
targetTileX, targetTileY, targetPixelX, targetPixelY);
|
||||
|
||||
// Calculate pixel delta from center
|
||||
int dx = (targetTileX - centerTileX) * MAP_TILE_SIZE + (targetPixelX - centerPixelX);
|
||||
int dy = (targetTileY - centerTileY) * MAP_TILE_SIZE + (targetPixelY - centerPixelY);
|
||||
|
||||
screenX = MAP_DISPLAY_W / 2 + dx;
|
||||
screenY = MAP_VIEWPORT_Y + MAP_VIEWPORT_H / 2 + dy;
|
||||
|
||||
return (screenX >= 0 && screenX < MAP_DISPLAY_W &&
|
||||
screenY >= MAP_VIEWPORT_Y && screenY < MAP_VIEWPORT_Y + MAP_VIEWPORT_H);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Tile loading and rendering
|
||||
// ==========================================================================
|
||||
|
||||
// Build tile file path: /tiles/{zoom}/{x}/{y}.png
|
||||
static void buildTilePath(char* buf, int bufSize, int zoom, int x, int y) {
|
||||
snprintf(buf, bufSize, MAP_TILE_ROOT "/%d/%d/%d.png", zoom, x, y);
|
||||
}
|
||||
|
||||
// Load a PNG tile from SD and decode it directly to the display
|
||||
// screenX, screenY = top-left corner on display where this tile goes
|
||||
bool loadAndRenderTile(int tileX, int tileY, int screenX, int screenY) {
|
||||
if (!_pngBuf || !_einkDisplay) return false;
|
||||
|
||||
char path[64];
|
||||
buildTilePath(path, sizeof(path), _zoom, tileX, tileY);
|
||||
|
||||
// Check existence first to avoid noisy ESP32 VFS error logs
|
||||
if (!SD.exists(path)) return false;
|
||||
|
||||
File f = SD.open(path, FILE_READ);
|
||||
if (!f) return false;
|
||||
|
||||
// Read entire PNG into buffer
|
||||
int fileSize = f.size();
|
||||
if (fileSize > MAP_PNG_BUF_SIZE) {
|
||||
Serial.printf("MapScreen: tile too large: %s (%d bytes)\n", path, fileSize);
|
||||
f.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
int bytesRead = f.read(_pngBuf, fileSize);
|
||||
f.close();
|
||||
|
||||
if (bytesRead != fileSize) {
|
||||
Serial.printf("MapScreen: short read: %s (%d/%d)\n", path, bytesRead, fileSize);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up draw context for the PNG callback
|
||||
_drawCtx.display = _einkDisplay;
|
||||
_drawCtx.png = &_png;
|
||||
_drawCtx.offsetX = screenX;
|
||||
_drawCtx.offsetY = screenY;
|
||||
_drawCtx.viewportY = MAP_VIEWPORT_Y;
|
||||
_drawCtx.viewportH = MAP_VIEWPORT_H;
|
||||
|
||||
// Open PNG from memory buffer
|
||||
int rc = _png.openRAM(_pngBuf, fileSize, pngDrawCallback);
|
||||
if (rc != PNG_SUCCESS) {
|
||||
Serial.printf("MapScreen: PNG open failed: %s (rc=%d)\n", path, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decode — triggers pngDrawCallback for each scanline.
|
||||
// First arg is user pointer, passed as pDraw->pUser in callback.
|
||||
rc = _png.decode(&_drawCtx, 0);
|
||||
_png.close();
|
||||
|
||||
if (rc != PNG_SUCCESS) {
|
||||
Serial.printf("MapScreen: PNG decode failed: %s (rc=%d)\n", path, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// PNGdec scanline callback — called once per row of the decoded image.
|
||||
// Draws directly to the e-ink display at raw pixel coordinates.
|
||||
// Uses getLineAsRGB565 with correct (little) endianness for ESP32.
|
||||
static int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
DrawContext* ctx = (DrawContext*)pDraw->pUser;
|
||||
if (!ctx || !ctx->display || !ctx->png) return 0;
|
||||
|
||||
int screenY = ctx->offsetY + pDraw->y;
|
||||
|
||||
// Clip to viewport vertically
|
||||
if (screenY < ctx->viewportY || screenY >= ctx->viewportY + ctx->viewportH) return 1;
|
||||
|
||||
// Debug: log format on first row of first tile only
|
||||
if (pDraw->y == 0 && ctx->offsetX >= 0 && ctx->offsetY >= 0) {
|
||||
static bool logged = false;
|
||||
if (!logged) {
|
||||
Serial.printf("MapScreen: PNG iBpp=%d iWidth=%d\n", pDraw->iBpp, pDraw->iWidth);
|
||||
logged = true;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t lineWidth = pDraw->iWidth;
|
||||
uint16_t lineBuf[MAP_TILE_SIZE];
|
||||
if (lineWidth > MAP_TILE_SIZE) lineWidth = MAP_TILE_SIZE;
|
||||
ctx->png->getLineAsRGB565(pDraw, lineBuf, PNG_RGB565_LITTLE_ENDIAN, 0xFFFFFFFF);
|
||||
|
||||
for (int x = 0; x < lineWidth; x++) {
|
||||
int screenX = ctx->offsetX + x;
|
||||
if (screenX < 0 || screenX >= MAP_DISPLAY_W) continue;
|
||||
|
||||
// RGB565 little-endian on ESP32: standard bit layout
|
||||
// R[15:11] G[10:5] B[4:0]
|
||||
uint16_t pixel = lineBuf[x];
|
||||
|
||||
// For B&W tiles this is 0x0000 (black) or 0xFFFF (white)
|
||||
// Simple threshold on full 16-bit value handles both cleanly
|
||||
uint16_t color = (pixel > 0x7FFF) ? GxEPD_WHITE : GxEPD_BLACK;
|
||||
ctx->display->drawPixelRaw(screenX, screenY, color);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Viewport rendering — stitch tiles to fill the screen
|
||||
// ==========================================================================
|
||||
|
||||
void renderMapViewport() {
|
||||
if (!_einkDisplay) return;
|
||||
|
||||
// Find which tile the center point falls in
|
||||
int centerTileX, centerTileY, centerPixelX, centerPixelY;
|
||||
latLonToTileXY(_centerLat, _centerLon, _zoom,
|
||||
centerTileX, centerTileY, centerPixelX, centerPixelY);
|
||||
|
||||
Serial.printf("MapScreen: center tile %d/%d/%d px(%d,%d)\n",
|
||||
_zoom, centerTileX, centerTileY, centerPixelX, centerPixelY);
|
||||
|
||||
// Screen position where the center tile's (0,0) corner should be placed
|
||||
// such that the GPS point ends up at viewport center
|
||||
int viewCenterX = MAP_DISPLAY_W / 2;
|
||||
int viewCenterY = MAP_VIEWPORT_Y + MAP_VIEWPORT_H / 2;
|
||||
|
||||
int baseTileScreenX = viewCenterX - centerPixelX;
|
||||
int baseTileScreenY = viewCenterY - centerPixelY;
|
||||
|
||||
// Determine tile grid range needed to cover the entire viewport
|
||||
int startDX = 0, startDY = 0;
|
||||
int endDX = 0, endDY = 0;
|
||||
|
||||
while (baseTileScreenX + startDX * MAP_TILE_SIZE > 0) startDX--;
|
||||
while (baseTileScreenY + startDY * MAP_TILE_SIZE > MAP_VIEWPORT_Y) startDY--;
|
||||
while (baseTileScreenX + (endDX + 1) * MAP_TILE_SIZE < MAP_DISPLAY_W) endDX++;
|
||||
while (baseTileScreenY + (endDY + 1) * MAP_TILE_SIZE < MAP_VIEWPORT_Y + MAP_VIEWPORT_H) endDY++;
|
||||
|
||||
int maxTile = (1 << _zoom) - 1;
|
||||
int loaded = 0, missing = 0;
|
||||
|
||||
for (int dy = startDY; dy <= endDY; dy++) {
|
||||
for (int dx = startDX; dx <= endDX; dx++) {
|
||||
int tx = centerTileX + dx;
|
||||
int ty = centerTileY + dy;
|
||||
|
||||
// Longitude wraps
|
||||
if (tx < 0) tx += (1 << _zoom);
|
||||
if (tx > maxTile) tx -= (1 << _zoom);
|
||||
|
||||
// Latitude doesn't wrap — skip out-of-range
|
||||
if (ty < 0 || ty > maxTile) continue;
|
||||
|
||||
int screenX = baseTileScreenX + dx * MAP_TILE_SIZE;
|
||||
int screenY = baseTileScreenY + dy * MAP_TILE_SIZE;
|
||||
|
||||
if (loadAndRenderTile(tx, ty, screenX, screenY)) {
|
||||
loaded++;
|
||||
} else {
|
||||
missing++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("MapScreen: rendered %d tiles, %d missing\n", loaded, missing);
|
||||
_tileFound = (loaded > 0);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Contact marker overlay
|
||||
// ==========================================================================
|
||||
|
||||
void renderContactMarkers() {
|
||||
if (!_einkDisplay) return;
|
||||
|
||||
Serial.printf("MapScreen: rendering %d contact markers\n", _numMarkers);
|
||||
|
||||
int visible = 0;
|
||||
for (int i = 0; i < _numMarkers; i++) {
|
||||
int sx, sy;
|
||||
if (latLonToScreen(_markers[i].lat, _markers[i].lon, sx, sy)) {
|
||||
drawDiamond(sx, sy, GxEPD_BLACK);
|
||||
visible++;
|
||||
}
|
||||
}
|
||||
|
||||
// Render own GPS position as a distinct marker (circle with crosshair)
|
||||
if (_hasFix) {
|
||||
int sx, sy;
|
||||
if (latLonToScreen(_gpsLat, _gpsLon, sx, sy)) {
|
||||
drawOwnPosition(sx, sy);
|
||||
visible++;
|
||||
}
|
||||
}
|
||||
|
||||
if (_numMarkers > 0 || _hasFix) {
|
||||
Serial.printf("MapScreen: %d markers visible on screen\n", visible);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a filled diamond marker for contacts/nodes
|
||||
void drawDiamond(int cx, int cy, uint16_t color) {
|
||||
int r = MAP_MARKER_SIZE / 2; // radius = 3
|
||||
|
||||
// Filled diamond
|
||||
for (int dy = -r; dy <= r; dy++) {
|
||||
int span = r - abs(dy);
|
||||
for (int dx = -span; dx <= span; dx++) {
|
||||
int px = cx + dx;
|
||||
int py = cy + dy;
|
||||
if (px >= 0 && px < MAP_DISPLAY_W &&
|
||||
py >= MAP_VIEWPORT_Y && py < MAP_VIEWPORT_Y + MAP_VIEWPORT_H) {
|
||||
_einkDisplay->drawPixelRaw(px, py, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// White outline for visibility on dark map areas
|
||||
for (int dy = -(r + 1); dy <= (r + 1); dy++) {
|
||||
int span = (r + 1) - abs(dy);
|
||||
int innerSpan = r - abs(dy);
|
||||
|
||||
for (int dx = -span; dx <= span; dx++) {
|
||||
if (abs(dy) <= r && abs(dx) <= innerSpan) continue;
|
||||
|
||||
int px = cx + dx;
|
||||
int py = cy + dy;
|
||||
if (px >= 0 && px < MAP_DISPLAY_W &&
|
||||
py >= MAP_VIEWPORT_Y && py < MAP_VIEWPORT_Y + MAP_VIEWPORT_H) {
|
||||
_einkDisplay->drawPixelRaw(px, py, GxEPD_WHITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw own-position marker: filled circle with crosshair arms
|
||||
// Visually distinct from contact diamonds
|
||||
void drawOwnPosition(int cx, int cy) {
|
||||
int r = 5; // Circle radius
|
||||
|
||||
// White background circle (clears map underneath)
|
||||
for (int dy = -(r + 1); dy <= (r + 1); dy++) {
|
||||
for (int dx = -(r + 1); dx <= (r + 1); dx++) {
|
||||
if (dx * dx + dy * dy <= (r + 1) * (r + 1)) {
|
||||
int px = cx + dx, py = cy + dy;
|
||||
if (px >= 0 && px < MAP_DISPLAY_W &&
|
||||
py >= MAP_VIEWPORT_Y && py < MAP_VIEWPORT_Y + MAP_VIEWPORT_H) {
|
||||
_einkDisplay->drawPixelRaw(px, py, GxEPD_WHITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Black circle outline
|
||||
for (int dy = -r; dy <= r; dy++) {
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
int d2 = dx * dx + dy * dy;
|
||||
if (d2 >= (r - 1) * (r - 1) && d2 <= r * r) {
|
||||
int px = cx + dx, py = cy + dy;
|
||||
if (px >= 0 && px < MAP_DISPLAY_W &&
|
||||
py >= MAP_VIEWPORT_Y && py < MAP_VIEWPORT_Y + MAP_VIEWPORT_H) {
|
||||
_einkDisplay->drawPixelRaw(px, py, GxEPD_BLACK);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Black dot at center
|
||||
_einkDisplay->drawPixelRaw(cx, cy, GxEPD_BLACK);
|
||||
if (cx + 1 < MAP_DISPLAY_W) _einkDisplay->drawPixelRaw(cx + 1, cy, GxEPD_BLACK);
|
||||
if (cx - 1 >= 0) _einkDisplay->drawPixelRaw(cx - 1, cy, GxEPD_BLACK);
|
||||
if (cy + 1 < MAP_VIEWPORT_Y + MAP_VIEWPORT_H) _einkDisplay->drawPixelRaw(cx, cy + 1, GxEPD_BLACK);
|
||||
if (cy - 1 >= MAP_VIEWPORT_Y) _einkDisplay->drawPixelRaw(cx, cy - 1, GxEPD_BLACK);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Crosshair at viewport center
|
||||
// ==========================================================================
|
||||
|
||||
void renderCrosshair() {
|
||||
if (!_einkDisplay) return;
|
||||
|
||||
int cx = MAP_DISPLAY_W / 2;
|
||||
int cy = MAP_VIEWPORT_Y + MAP_VIEWPORT_H / 2;
|
||||
int len = 6; // arm length in pixels
|
||||
|
||||
// Draw thin crosshair: black line with white border for contrast
|
||||
// Horizontal arm
|
||||
for (int x = cx - len; x <= cx + len; x++) {
|
||||
if (x >= 0 && x < MAP_DISPLAY_W) {
|
||||
// White border pixels above and below
|
||||
if (cy - 1 >= MAP_VIEWPORT_Y)
|
||||
_einkDisplay->drawPixelRaw(x, cy - 1, GxEPD_WHITE);
|
||||
if (cy + 1 < MAP_VIEWPORT_Y + MAP_VIEWPORT_H)
|
||||
_einkDisplay->drawPixelRaw(x, cy + 1, GxEPD_WHITE);
|
||||
// Black center line
|
||||
_einkDisplay->drawPixelRaw(x, cy, GxEPD_BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical arm
|
||||
for (int y = cy - len; y <= cy + len; y++) {
|
||||
if (y >= MAP_VIEWPORT_Y && y < MAP_VIEWPORT_Y + MAP_VIEWPORT_H) {
|
||||
if (cx - 1 >= 0)
|
||||
_einkDisplay->drawPixelRaw(cx - 1, y, GxEPD_WHITE);
|
||||
if (cx + 1 < MAP_DISPLAY_W)
|
||||
_einkDisplay->drawPixelRaw(cx + 1, y, GxEPD_WHITE);
|
||||
_einkDisplay->drawPixelRaw(cx, y, GxEPD_BLACK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Footer bar — zoom level, GPS status, navigation hints
|
||||
// ==========================================================================
|
||||
|
||||
void renderFooter(DisplayDriver& display) {
|
||||
// Use the standard footer pattern: setTextSize(1) at height()-12
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
int footerY = display.height() - 12;
|
||||
|
||||
// Separator line
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
|
||||
// Left: zoom level
|
||||
char left[8];
|
||||
snprintf(left, sizeof(left), "Z%d", _zoom);
|
||||
display.setCursor(0, footerY);
|
||||
display.print(left);
|
||||
|
||||
// Right: navigation hint
|
||||
const char* right = "WASD:pan Z/X:zoom";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "../MyMesh.h"
|
||||
#include "NotesScreen.h"
|
||||
#include "RepeaterAdminScreen.h"
|
||||
#include "MapScreen.h"
|
||||
#include "target.h"
|
||||
#include "GPSDutyCycle.h"
|
||||
#ifdef WIFI_SSID
|
||||
@@ -335,16 +336,20 @@ public:
|
||||
y += 10;
|
||||
display.drawTextCentered(display.width() / 2, y, "[N] Notes [S] Settings ");
|
||||
y += 10;
|
||||
#ifdef HAS_4G_MODEM
|
||||
display.drawTextCentered(display.width() / 2, y, "[E] Reader [T] Phone ");
|
||||
#elif defined(MECK_AUDIO_VARIANT)
|
||||
display.drawTextCentered(display.width() / 2, y, "[E] Reader [P] Audiobooks");
|
||||
#else
|
||||
display.drawTextCentered(display.width() / 2, y, "[E] Reader ");
|
||||
#endif
|
||||
#ifdef MECK_WEB_READER
|
||||
display.drawTextCentered(display.width() / 2, y, "[E] Reader [G] Maps ");
|
||||
y += 10;
|
||||
#if defined(HAS_4G_MODEM) && defined(MECK_WEB_READER)
|
||||
display.drawTextCentered(display.width() / 2, y, "[T] Phone [B] Browser ");
|
||||
#elif defined(HAS_4G_MODEM)
|
||||
display.drawTextCentered(display.width() / 2, y, "[T] Phone ");
|
||||
#elif defined(MECK_AUDIO_VARIANT) && defined(MECK_WEB_READER)
|
||||
display.drawTextCentered(display.width() / 2, y, "[P] Audiobooks [B] Browser ");
|
||||
#elif defined(MECK_AUDIO_VARIANT)
|
||||
display.drawTextCentered(display.width() / 2, y, "[P] Audiobooks ");
|
||||
#elif defined(MECK_WEB_READER)
|
||||
display.drawTextCentered(display.width() / 2, y, "[B] Browser ");
|
||||
#else
|
||||
y -= 10; // reclaim the row for standalone
|
||||
#endif
|
||||
y += 14;
|
||||
|
||||
@@ -917,6 +922,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
#ifdef HAS_4G_MODEM
|
||||
sms_screen = new SMSScreen(this);
|
||||
#endif
|
||||
map_screen = new MapScreen(this);
|
||||
setCurrScreen(splash);
|
||||
}
|
||||
|
||||
@@ -1542,6 +1548,19 @@ void UITask::gotoWebReader() {
|
||||
}
|
||||
#endif
|
||||
|
||||
void UITask::gotoMapScreen() {
|
||||
MapScreen* map = (MapScreen*)map_screen;
|
||||
if (_display != NULL) {
|
||||
map->enter(*_display);
|
||||
}
|
||||
setCurrScreen(map_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) {
|
||||
if (repeater_admin && isOnRepeaterAdmin()) {
|
||||
((RepeaterAdminScreen*)repeater_admin)->onLoginResult(success, permissions, server_time);
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
#include "WebReaderScreen.h"
|
||||
#endif
|
||||
|
||||
// MapScreen.h included in UITask.cpp and main.cpp only (PNGdec headers
|
||||
// conflict with BLE if pulled into the global include chain)
|
||||
|
||||
class UITask : public AbstractUITask {
|
||||
DisplayDriver* _display;
|
||||
SensorManager* _sensors;
|
||||
@@ -73,6 +76,7 @@ class UITask : public AbstractUITask {
|
||||
#ifdef MECK_WEB_READER
|
||||
UIScreen* web_reader; // Web reader screen (lazy-init, WiFi required)
|
||||
#endif
|
||||
UIScreen* map_screen; // Map tile screen (GPS + SD card tiles)
|
||||
UIScreen* curr;
|
||||
|
||||
void userLedHandler();
|
||||
@@ -104,6 +108,7 @@ public:
|
||||
void gotoOnboarding(); // Navigate to settings in onboarding mode
|
||||
void gotoAudiobookPlayer(); // Navigate to audiobook player
|
||||
void gotoRepeaterAdmin(int contactIdx); // Navigate to repeater admin
|
||||
void gotoMapScreen(); // Navigate to map tile screen
|
||||
#ifdef MECK_WEB_READER
|
||||
void gotoWebReader(); // Navigate to web reader (browser)
|
||||
#endif
|
||||
@@ -131,6 +136,7 @@ public:
|
||||
bool isOnSettingsScreen() const { return curr == settings_screen; }
|
||||
bool isOnAudiobookPlayer() const { return curr == audiobook_screen; }
|
||||
bool isOnRepeaterAdmin() const { return curr == repeater_admin; }
|
||||
bool isOnMapScreen() const { return curr == map_screen; }
|
||||
#ifdef MECK_WEB_READER
|
||||
bool isOnWebReader() const { return curr == web_reader; }
|
||||
#endif
|
||||
@@ -174,6 +180,7 @@ public:
|
||||
UIScreen* getAudiobookScreen() const { return audiobook_screen; }
|
||||
void setAudiobookScreen(UIScreen* s) { audiobook_screen = s; }
|
||||
UIScreen* getRepeaterAdminScreen() const { return repeater_admin; }
|
||||
UIScreen* getMapScreen() const { return map_screen; }
|
||||
#ifdef MECK_WEB_READER
|
||||
UIScreen* getWebReaderScreen() const { return web_reader; }
|
||||
#endif
|
||||
|
||||
@@ -12,7 +12,31 @@
|
||||
#include <Fonts/FreeSans9pt7b.h>
|
||||
#include <Fonts/FreeSansBold12pt7b.h>
|
||||
#include <Fonts/FreeSans18pt7b.h>
|
||||
#include <CRC32.h>
|
||||
|
||||
// Inline CRC32 for frame change detection (replaces bakercp/CRC32
|
||||
// to avoid naming collision with PNGdec's bundled CRC32.h)
|
||||
class FrameCRC32 {
|
||||
uint32_t _crc = 0xFFFFFFFF;
|
||||
public:
|
||||
void reset() { _crc = 0xFFFFFFFF; }
|
||||
template<typename T> void update(T val) {
|
||||
const uint8_t* p = (const uint8_t*)&val;
|
||||
for (size_t i = 0; i < sizeof(T); i++) {
|
||||
_crc ^= p[i];
|
||||
for (int b = 0; b < 8; b++)
|
||||
_crc = (_crc >> 1) ^ (0xEDB88320 & -(int32_t)(_crc & 1));
|
||||
}
|
||||
}
|
||||
template<typename T> void update(const T* data, size_t len) {
|
||||
const uint8_t* p = (const uint8_t*)data;
|
||||
for (size_t i = 0; i < len * sizeof(T); i++) {
|
||||
_crc ^= p[i];
|
||||
for (int b = 0; b < 8; b++)
|
||||
_crc = (_crc >> 1) ^ (0xEDB88320 & -(int32_t)(_crc & 1));
|
||||
}
|
||||
}
|
||||
uint32_t finalize() { return _crc ^ 0xFFFFFFFF; }
|
||||
};
|
||||
|
||||
#include "DisplayDriver.h"
|
||||
|
||||
@@ -34,7 +58,7 @@ class GxEPDDisplay : public DisplayDriver {
|
||||
bool _init = false;
|
||||
bool _isOn = false;
|
||||
uint16_t _curr_color;
|
||||
CRC32 display_crc;
|
||||
FrameCRC32 display_crc;
|
||||
int last_display_crc_value = 0;
|
||||
|
||||
public:
|
||||
@@ -60,4 +84,15 @@ public:
|
||||
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||
uint16_t getTextWidth(const char* str) override;
|
||||
void endFrame() override;
|
||||
};
|
||||
|
||||
// --- Raw pixel access for MapScreen (bypasses scaling) ---
|
||||
void drawPixelRaw(int16_t x, int16_t y, uint16_t color) {
|
||||
display.drawPixel(x, y, color);
|
||||
}
|
||||
int16_t rawWidth() { return display.width(); }
|
||||
int16_t rawHeight() { return display.height(); }
|
||||
|
||||
// Force endFrame() to push to display even if CRC unchanged
|
||||
// (needed because drawPixelRaw bypasses CRC tracking)
|
||||
void invalidateFrameCRC() { last_display_crc_value = 0; }
|
||||
};
|
||||
@@ -89,7 +89,7 @@ lib_deps =
|
||||
${sensor_base.lib_deps}
|
||||
zinggjm/GxEPD2@^1.5.9
|
||||
adafruit/Adafruit GFX Library@^1.11.0
|
||||
bakercp/CRC32@^2.0.0
|
||||
bitbank2/PNGdec@^1.0.1
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; Meck unified builds — one codebase, three variants via build flags
|
||||
@@ -157,7 +157,7 @@ build_flags =
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
-D FIRMWARE_VERSION='"Meck v0.9.5.4G"'
|
||||
-D FIRMWARE_VERSION='"Meck v0.9.6.4G"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -183,7 +183,7 @@ build_flags =
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
-D FIRMWARE_VERSION='"Meck v0.9.5.4G.SA"'
|
||||
-D FIRMWARE_VERSION='"Meck v0.9.6.4G.SA"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
|
||||
Reference in New Issue
Block a user