Created
January 29, 2026 00:38
-
-
Save jesseadams/ad5a4a707bd2d2fc08d7c6bed3f2f510 to your computer and use it in GitHub Desktop.
Adafruit ESP32-S3 Feather with GPS Wing and 3.5" 480x320 TFT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: 2024 | |
| # SPDX-License-Identifier: MIT | |
| import time | |
| import board | |
| from adafruit_display_text.label import Label | |
| from displayio import Group | |
| from terminalio import FONT | |
| import busio | |
| import adafruit_gps | |
| import supervisor | |
| import adafruit_max1704x | |
| import rtc | |
| import displayio | |
| import fourwire | |
| import adafruit_hx8357 | |
| import adafruit_tsc2007 | |
| import adafruit_st7789 | |
| import os | |
| import storage | |
| import adafruit_sdcard | |
| from digitalio import DigitalInOut | |
| #set_time = time.struct_time((2026, 1, 25, 6, 41, 0, -1, -1, -1)) | |
| # Get the onboard RTC | |
| #r = rtc.RTC() | |
| # Set the RTC's time | |
| #r.datetime = set_time | |
| #print("RTC time set to:", r.datetime) | |
| print(board) | |
| # 3.5" TFT FeatherWing V2 | |
| # Release any resources currently in use for the displays | |
| displayio.release_displays() | |
| # Use Hardware SPI | |
| spi = board.SPI() | |
| tft_cs = board.D9 | |
| tft_dc = board.D10 | |
| display_width = 480 | |
| display_height = 320 | |
| display_bus = fourwire.FourWire(spi, command=tft_dc, chip_select=tft_cs) | |
| display = adafruit_hx8357.HX8357(display_bus, width=display_width, height=display_height) | |
| # Initialize I2C - the TFT FeatherWing should have pull-ups | |
| # If this fails, there may be a hardware issue | |
| print("Initializing I2C...") | |
| i2c = busio.I2C(board.SCL, board.SDA, frequency=100000) | |
| print("I2C initialized") | |
| irq_dio = None | |
| print("Initializing touchscreen...") | |
| tsc = adafruit_tsc2007.TSC2007(i2c, irq=irq_dio) | |
| print("Touchscreen initialized") | |
| # Mount SD card | |
| sd_cs = DigitalInOut(board.D5) # SD card chip select on TFT FeatherWing | |
| try: | |
| sdcard = adafruit_sdcard.SDCard(spi, sd_cs) | |
| vfs = storage.VfsFat(sdcard) | |
| storage.mount(vfs, "/sd") | |
| print("SD card mounted at /sd") | |
| sd_files = os.listdir('/sd') | |
| print(f"SD card files: {len(sd_files)} files") | |
| except Exception as e: | |
| print(f"SD card mount failed: {e}") | |
| print("Continuing without SD card...") | |
| # Initialize groups and labels for display | |
| groups = [] | |
| text_labels = [] | |
| index = 0 | |
| touch_state = False | |
| supervisor.runtime.autoreload = False | |
| monitor = adafruit_max1704x.MAX17048(i2c) | |
| # import busio | |
| uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) | |
| # i2c = board.I2C() # uses board.SCL and board.SDA | |
| # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector | |
| # Create a GPS module instance. | |
| gps = adafruit_gps.GPS(uart, debug=False) # Use UART | |
| # gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface | |
| # Turn on the basic GGA and RMC info (what you typically want) | |
| gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") | |
| # Set update rate to once a second 1hz (what you typically want) | |
| gps.send_command(b"PMTK220,1000") | |
| last_print = time.monotonic() | |
| last_console_print = time.monotonic() | |
| # Begin main loop | |
| from adafruit_datetime import date, datetime | |
| import math | |
| # Map drawing function | |
| def draw_map_marker(group, x, y, color=0xFF0000): | |
| """Draw a simple marker on the map at given pixel coordinates""" | |
| from vectorio import Circle, Polygon | |
| from adafruit_display_shapes.circle import Circle as ShapeCircle | |
| # Draw a simple circle marker | |
| marker = ShapeCircle(x, y, 8, fill=color, outline=0xFFFFFF) | |
| return marker | |
| # Convert lat/lon to pixel coordinates (simple mercator-like projection) | |
| def latlon_to_pixels(lat, lon, map_center_lat, map_center_lon, zoom_level=14): | |
| """Convert lat/lon to pixel offsets from center of display""" | |
| # Simple conversion - adjust scale based on zoom | |
| pixels_per_degree = zoom_level * 10 | |
| dx = (lon - map_center_lon) * pixels_per_degree * math.cos(math.radians(map_center_lat)) | |
| dy = (map_center_lat - lat) * pixels_per_degree | |
| # Center on display | |
| x = display_width // 2 + int(dx) | |
| y = display_height // 2 + int(dy) | |
| return x, y | |
| # ============================================================================ | |
| # DYNAMIC GPS-BASED TILE LOADING SYSTEM | |
| # ============================================================================ | |
| # Create a map display group | |
| map_group = displayio.Group() | |
| # Configuration | |
| TILES_PER_LOCATION = len([13, 14, 15, 16]) # Four zoom levels | |
| current_zoom = 15 | |
| available_zoom_levels = [13, 14, 15, 16] # Zoom levels to cache | |
| MARKER_SIZE = 12 # GPS marker size in pixels | |
| # Tile cache: {filename: (bitmap, palette, tilegrid)} | |
| tile_cache = {} | |
| cache_order = [] # Track LRU | |
| current_tile_filename = None | |
| current_location_tiles = {} # Store all zoom levels for current location: {zoom: filename} | |
| def find_tile_for_position(lat, lon, zoom): | |
| """Find the best tile filename for given GPS position""" | |
| # Zoom 17 uses 0.001 degree grid, others use 0.01 degree grid | |
| if zoom >= 17: | |
| lat_h = math.floor(lat * 1000) | |
| lon_h = math.floor(lon * 1000) | |
| scale = 1000 | |
| format_str = "03d" | |
| else: | |
| lat_h = math.floor(lat * 100) | |
| lon_h = math.floor(lon * 100) | |
| scale = 100 | |
| format_str = "02d" | |
| lat_snapped = lat_h / scale | |
| lon_snapped = lon_h / scale | |
| tile_center_lat = (lat_h + 0.5) / scale | |
| tile_center_lon = (lon_h + 0.5) / scale | |
| # Create filename | |
| lat_dir = 'N' if lat_h >= 0 else 'S' | |
| lon_dir = 'E' if lon_h >= 0 else 'W' | |
| lat_abs = abs(lat_h) | |
| lon_abs = abs(lon_h) | |
| if zoom >= 17: | |
| lat_str = f"{lat_abs // scale}_{lat_abs % scale:{format_str}}" | |
| lon_str = f"{lon_abs // scale}_{lon_abs % scale:{format_str}}" | |
| else: | |
| lat_str = f"{lat_abs // scale}_{lat_abs % scale:{format_str}}0" | |
| lon_str = f"{lon_abs // scale}_{lon_abs % scale:{format_str}}0" | |
| filename = f"/sd/map_z{zoom}_lat{lat_dir}{lat_str}_lon{lon_dir}{lon_str}.bmp" | |
| print(f"Looking for tile: {filename}") | |
| # Check if file exists | |
| try: | |
| os.stat(filename) | |
| return filename | |
| except: | |
| # Try nearby tiles | |
| for lat_offset in [-0.01, 0, 0.01]: | |
| for lon_offset in [-0.01, 0, 0.01]: | |
| test_lat = round(lat + lat_offset, 2) | |
| test_lon = round(lon + lon_offset, 2) | |
| lat_dir = 'N' if test_lat >= 0 else 'S' | |
| lon_dir = 'E' if test_lon >= 0 else 'W' | |
| lat_abs = abs(test_lat) | |
| lon_abs = abs(test_lon) | |
| lat_str = f"{lat_abs:.4f}".replace('.', '_') | |
| lon_str = f"{lon_abs:.4f}".replace('.', '_') | |
| test_filename = f"/sd/map_z{zoom}_lat{lat_dir}{lat_str}_lon{lon_dir}{lon_str}.bmp" | |
| try: | |
| os.stat(test_filename) | |
| return test_filename | |
| except: | |
| pass | |
| return None | |
| def load_tile_from_sd(filename): | |
| """Load a tile from SD card into RAM""" | |
| if filename in tile_cache: | |
| return tile_cache[filename] | |
| print(f"Loading tile: {filename}") | |
| try: | |
| with open(filename, 'rb') as f: | |
| # Read BMP header | |
| f.seek(10) | |
| data_offset = int.from_bytes(f.read(4), 'little') | |
| f.seek(18) | |
| width = int.from_bytes(f.read(4), 'little') | |
| height = int.from_bytes(f.read(4), 'little') | |
| f.seek(28) | |
| bits_per_pixel = int.from_bytes(f.read(2), 'little') | |
| if bits_per_pixel == 1: | |
| # 1-bit black and white | |
| palette = displayio.Palette(2) | |
| palette[0] = 0x000000 # Black | |
| palette[1] = 0xFFFFFF # White | |
| # Read pixel data | |
| f.seek(data_offset) | |
| pixel_data = bytearray(f.read()) | |
| # Create bitmap in RAM | |
| bitmap = displayio.Bitmap(width, height, 2) | |
| row_size = ((width + 31) // 32) * 4 # 1-bit rows padded to 4 bytes | |
| for y in range(height): | |
| src_y = height - 1 - y | |
| for x in range(width): | |
| byte_idx = src_y * row_size + x // 8 | |
| bit_idx = 7 - (x % 8) | |
| bitmap[x, y] = (pixel_data[byte_idx] >> bit_idx) & 1 | |
| # Create TileGrid | |
| tilegrid = displayio.TileGrid(bitmap, pixel_shader=palette) | |
| # Add to cache | |
| tile_cache[filename] = (bitmap, palette, tilegrid) | |
| print(f" ✓ Loaded into RAM") | |
| return tile_cache[filename] | |
| elif bits_per_pixel == 8 or bits_per_pixel == 4: | |
| # Read palette (256 colors for 8-bit, 16 colors for 4-bit) | |
| palette_size = 256 if bits_per_pixel == 8 else 16 | |
| f.seek(54) | |
| palette_data = f.read(palette_size * 4) | |
| palette = displayio.Palette(palette_size) | |
| for i in range(palette_size): | |
| b = palette_data[i*4] | |
| g = palette_data[i*4+1] | |
| r = palette_data[i*4+2] | |
| palette[i] = (r << 16) | (g << 8) | b | |
| # Read pixel data | |
| f.seek(data_offset) | |
| pixel_data = bytearray(f.read()) | |
| # Create bitmap in RAM | |
| bitmap = displayio.Bitmap(width, height, palette_size) | |
| if bits_per_pixel == 8: | |
| row_size = ((width + 3) // 4) * 4 | |
| for y in range(height): | |
| src_y = height - 1 - y | |
| for x in range(width): | |
| bitmap[x, y] = pixel_data[src_y * row_size + x] | |
| else: # 4-bit | |
| row_size = ((width + 7) // 8) * 4 | |
| for y in range(height): | |
| src_y = height - 1 - y | |
| for x in range(width): | |
| byte_idx = src_y * row_size + x // 2 | |
| if x % 2 == 0: | |
| bitmap[x, y] = pixel_data[byte_idx] >> 4 | |
| else: | |
| bitmap[x, y] = pixel_data[byte_idx] & 0x0F | |
| # Create TileGrid | |
| tilegrid = displayio.TileGrid(bitmap, pixel_shader=palette) | |
| # Add to cache | |
| tile_cache[filename] = (bitmap, palette, tilegrid) | |
| print(f" ✓ Loaded into RAM") | |
| return tile_cache[filename] | |
| except Exception as e: | |
| print(f" Failed: {e}") | |
| return None | |
| def preload_all_zoom_levels(lat, lon, show_current_first=False): | |
| """Preload tiles for all zoom levels at current GPS position""" | |
| global current_location_tiles, current_tile_filename | |
| print(f"Preloading zoom levels for position ({lat:.4f}, {lon:.4f})...") | |
| new_location_tiles = {} | |
| loaded_count = 0 | |
| current_tile_data = None | |
| # If show_current_first, load current zoom level first | |
| if show_current_first: | |
| filename = find_tile_for_position(lat, lon, current_zoom) | |
| if filename: | |
| new_location_tiles[current_zoom] = filename | |
| if filename not in tile_cache: | |
| tile_data = load_tile_from_sd(filename) | |
| if tile_data: | |
| loaded_count += 1 | |
| current_tile_data = tile_data | |
| current_tile_filename = filename | |
| print(f" ✓ Current zoom {current_zoom} loaded - ready to display!") | |
| else: | |
| current_tile_data = tile_cache[filename] | |
| current_tile_filename = filename | |
| print(f" Zoom {current_zoom}: Already in cache") | |
| # Now load all other zoom levels | |
| for zoom in available_zoom_levels: | |
| if show_current_first and zoom == current_zoom: | |
| continue # Already loaded above | |
| filename = find_tile_for_position(lat, lon, zoom) | |
| if filename: | |
| new_location_tiles[zoom] = filename | |
| # Load into cache if not already loaded | |
| if filename not in tile_cache: | |
| tile_data = load_tile_from_sd(filename) | |
| if tile_data: | |
| loaded_count += 1 | |
| else: | |
| print(f" Zoom {zoom}: Already in cache") | |
| else: | |
| print(f" Zoom {zoom}: No tile found") | |
| # Clear old location tiles from cache | |
| for old_filename in current_location_tiles.values(): | |
| if old_filename in tile_cache and old_filename not in new_location_tiles.values(): | |
| del tile_cache[old_filename] | |
| print(f" Evicted old tile: {old_filename}") | |
| current_location_tiles = new_location_tiles | |
| print(f"✓ Preloaded {loaded_count} new tiles, {len(tile_cache)} total in cache") | |
| return current_tile_data | |
| def update_map_for_gps(lat, lon, zoom): | |
| """Update the displayed map based on GPS position - uses cached tiles for instant zoom""" | |
| global current_tile_filename | |
| # Check if we have this zoom level cached for current location | |
| if zoom in current_location_tiles: | |
| filename = current_location_tiles[zoom] | |
| if filename in tile_cache: | |
| # Instant zoom - tile already in cache! | |
| bitmap, palette, tilegrid = tile_cache[filename] | |
| map_group[0] = tilegrid | |
| current_tile_filename = filename | |
| print(f"✓ Instant zoom to {zoom} (cached)") | |
| return True | |
| # Need to load new location - preload all zoom levels | |
| filename = preload_all_zoom_levels(lat, lon) | |
| if filename and filename in tile_cache: | |
| bitmap, palette, tilegrid = tile_cache[filename] | |
| map_group[0] = tilegrid | |
| current_tile_filename = filename | |
| print(f"Map updated for ({lat:.4f}, {lon:.4f}) zoom {zoom}") | |
| return True | |
| print(f"No tile found for ({lat:.4f}, {lon:.4f}) zoom {zoom}") | |
| return False | |
| def update_marker_position(gps_lat, gps_lon): | |
| """Update marker position based on GPS coordinates relative to tile grid""" | |
| # Zoom 17 uses 0.001 degree grid, others use 0.01 degree grid | |
| if current_zoom >= 17: | |
| lat_h = math.floor(gps_lat * 1000) | |
| lon_h = math.floor(gps_lon * 1000) | |
| tc_lat = (lat_h + 0.5) / 1000.0 | |
| tc_lon = (lon_h + 0.5) / 1000.0 | |
| else: | |
| lat_h = math.floor(gps_lat * 100) | |
| lon_h = math.floor(gps_lon * 100) | |
| tc_lat = (lat_h + 0.5) / 100.0 | |
| tc_lon = (lon_h + 0.5) / 100.0 | |
| # OSM tile calc (use current zoom) | |
| lat_rad = math.radians(tc_lat) | |
| n = 2.0 ** current_zoom | |
| ctx = int((tc_lon + 180.0) / 360.0 * n) | |
| tan_lat = math.tan(lat_rad) | |
| asinh_tan = math.log(tan_lat + math.sqrt(tan_lat * tan_lat + 1)) | |
| cty = int((1.0 - asinh_tan / math.pi) / 2.0 * n) | |
| # Canvas bounds (3x2 tiles) | |
| sx = ctx - 1 | |
| sy = cty - 1 | |
| # sinh helper | |
| def sinh(x): | |
| ex = math.exp(x) | |
| return (ex - 1.0/ex) / 2.0 | |
| # Canvas corners | |
| def tile2lat(y): | |
| return math.degrees(math.atan(sinh(math.pi * (1 - 2 * y / n)))) | |
| def tile2lon(x): | |
| return x / n * 360.0 - 180.0 | |
| nw_lat = tile2lat(sy) | |
| nw_lon = tile2lon(sx) | |
| se_lat = tile2lat(sy + 2) | |
| se_lon = tile2lon(sx + 3) | |
| # Mercator Y helper | |
| def merc_y(lat): | |
| lr = math.radians(lat) | |
| return math.log(math.tan(lr) + 1.0 / math.cos(lr)) | |
| # Center pixel in canvas | |
| cx_frac = (tc_lon - nw_lon) / (se_lon - nw_lon) | |
| cx_px = cx_frac * 768 | |
| cy_merc = merc_y(tc_lat) | |
| nw_merc = merc_y(nw_lat) | |
| se_merc = merc_y(se_lat) | |
| cy_frac = (nw_merc - cy_merc) / (nw_merc - se_merc) | |
| cy_px = cy_frac * 512 | |
| # GPS pixel in canvas | |
| gx_frac = (gps_lon - nw_lon) / (se_lon - nw_lon) | |
| gx_px = gx_frac * 768 | |
| gy_merc = merc_y(gps_lat) | |
| gy_frac = (nw_merc - gy_merc) / (nw_merc - se_merc) | |
| gy_px = gy_frac * 512 | |
| # Convert to tile coords | |
| pixel_x = int(gx_px - (cx_px - 240)) | |
| pixel_y = int(gy_px - (cy_px - 160)) | |
| # Clamp and update marker | |
| pixel_x = max(6, min(474, pixel_x)) | |
| pixel_y = max(6, min(314, pixel_y)) | |
| marker_grid.x = pixel_x - 6 | |
| marker_grid.y = pixel_y - 6 | |
| # Add GPS marker to map group (before GPS fix section) | |
| print("Adding GPS marker...") | |
| marker_bitmap = displayio.Bitmap(MARKER_SIZE, MARKER_SIZE, 2) | |
| marker_palette = displayio.Palette(2) | |
| marker_palette.make_transparent(0) | |
| marker_palette[1] = 0xFF0000 # Red | |
| for y in range(MARKER_SIZE): | |
| for x in range(MARKER_SIZE): | |
| if x == 0 or x == MARKER_SIZE-1 or y == 0 or y == MARKER_SIZE-1: | |
| marker_bitmap[x, y] = 1 | |
| elif x > 2 and x < MARKER_SIZE-3 and y > 2 and y < MARKER_SIZE-3: | |
| marker_bitmap[x, y] = 1 | |
| # Start at center - will be updated based on actual GPS position | |
| marker_grid = displayio.TileGrid( | |
| marker_bitmap, | |
| pixel_shader=marker_palette, | |
| x=display_width//2 - MARKER_SIZE//2, | |
| y=display_height//2 - MARKER_SIZE//2 | |
| ) | |
| map_group.append(marker_grid) | |
| print("Adding map label...") | |
| map_label = Label( | |
| FONT, | |
| text="GPS MAP", | |
| color=0xFFFFFF, | |
| x=10, | |
| y=10, | |
| line_spacing=1.0, | |
| background_color=0x000000 | |
| ) | |
| map_group.append(map_label) | |
| supervisor.runtime.autoreload = False | |
| monitor = adafruit_max1704x.MAX17048(i2c) | |
| # PHASE 1: Wait for GPS fix before loading map | |
| print("=== PHASE 1: Waiting for GPS fix ===") | |
| # Create waiting screen | |
| waiting_group = displayio.Group() | |
| waiting_background = displayio.Bitmap(display_width, display_height, 2) | |
| waiting_palette = displayio.Palette(2) | |
| waiting_palette[0] = 0x000000 # Black background | |
| waiting_palette[1] = 0xFFFFFF # White text | |
| for y in range(display_height): | |
| for x in range(display_width): | |
| waiting_background[x, y] = 0 | |
| waiting_tile_grid = displayio.TileGrid(waiting_background, pixel_shader=waiting_palette) | |
| waiting_group.append(waiting_tile_grid) | |
| waiting_label = Label(FONT, text="Waiting for GPS fix...\n\nSearching for satellites...", color=0xFFFFFF, x=10, y=display_height//2 - 20, line_spacing=1.2) | |
| waiting_group.append(waiting_label) | |
| # Show waiting screen | |
| display.root_group = waiting_group | |
| print("Displaying 'Waiting for GPS fix' screen...") | |
| # Wait for initial GPS fix | |
| gps_fix_acquired = False | |
| initial_lat = None | |
| initial_lon = None | |
| print("Waiting for GPS fix...") | |
| start_wait_time = time.monotonic() | |
| while not gps_fix_acquired: | |
| gps.update() | |
| if gps.has_fix: | |
| initial_lat = gps.latitude | |
| initial_lon = gps.longitude | |
| gps_fix_acquired = True | |
| print(f"✓ GPS fix acquired: ({initial_lat:.6f}, {initial_lon:.6f})") | |
| print(f" Satellites: {gps.satellites}, Quality: {gps.fix_quality}") | |
| # Update waiting screen to show we're loading map | |
| waiting_label.text = f"GPS fix acquired!\n\nLat: {initial_lat:.6f}\nLon: {initial_lon:.6f}\n\nLoading map..." | |
| time.sleep(0.5) | |
| else: | |
| # Update waiting screen | |
| elapsed = int(time.monotonic() - start_wait_time) | |
| waiting_label.text = f"Waiting for GPS fix...\n\nSearching for satellites...\n\nTime: {elapsed}s" | |
| time.sleep(0.5) | |
| # Now load the map tile for the GPS position and preload all zoom levels | |
| print(f"=== Loading map tiles for GPS position ===") | |
| # Step 1: Load current zoom level and display immediately | |
| print(f"Loading zoom {current_zoom} for display...") | |
| current_filename = find_tile_for_position(initial_lat, initial_lon, current_zoom) | |
| if current_filename: | |
| # Load directly from SD and display | |
| try: | |
| with open(current_filename, 'rb') as f: | |
| bitmap = displayio.OnDiskBitmap(current_filename) | |
| tilegrid = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader) | |
| # Insert at index 0 so marker and label appear on top | |
| map_group.insert(0, tilegrid) | |
| current_tile_filename = current_filename | |
| print(f"✓ Map displayed from SD card") | |
| # Update marker position for initial GPS coordinates | |
| # Note: marker_grid will be created later, so we'll update it after display | |
| initial_marker_lat = initial_lat | |
| initial_marker_lon = initial_lon | |
| # Show the map immediately | |
| print("Adding map to groups...") | |
| groups.append(map_group) | |
| text_labels.append(map_label) | |
| display.root_group = groups[0] | |
| print("✓ Map is now visible on screen!") | |
| # Update map label with initial status | |
| t = datetime.now() | |
| time_str = f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}" | |
| zoom_info = f"Zoom: {current_zoom}" | |
| map_label.text = f"{time_str} GPS MAP\n{zoom_info}\nGPS: FIX\nLat: {initial_lat:.6f}\nLon: {initial_lon:.6f}\nLoading tiles..." | |
| # Update marker position now that marker_grid exists | |
| update_marker_position(initial_marker_lat, initial_marker_lon) | |
| print(f"✓ Marker positioned at GPS coordinates") | |
| # Set last GPS position for movement tracking | |
| last_gps_lat = initial_lat | |
| last_gps_lon = initial_lon | |
| except Exception as e: | |
| print(f"Failed to load initial tile: {e}") | |
| current_filename = None | |
| if current_filename: | |
| # Step 2 & 3: Now preload all zoom levels into RAM (including current one) | |
| print(f"Preloading all zoom levels into RAM...") | |
| preload_all_zoom_levels(initial_lat, initial_lon, show_current_first=False) | |
| # Replace the OnDiskBitmap with RAM version for faster access | |
| if current_filename in tile_cache: | |
| bitmap, palette, tilegrid = tile_cache[current_filename] | |
| map_group[0] = tilegrid | |
| print(f"✓ Switched to RAM-based tile for instant zooming") | |
| else: | |
| print(f"⚠ No tile found for initial position ({initial_lat:.4f}, {initial_lon:.4f})") | |
| print("Creating placeholder...") | |
| # Create placeholder | |
| map_background = displayio.Bitmap(display_width, display_height, 2) | |
| map_palette = displayio.Palette(2) | |
| map_palette[0] = 0xE8F4E8 | |
| map_palette[1] = 0xFF0000 | |
| for y in range(display_height): | |
| for x in range(display_width): | |
| map_background[x, y] = 0 | |
| # Draw X | |
| for i in range(min(display_width, display_height)): | |
| map_background[i, i] = 1 | |
| map_background[display_width-1-i, i] = 1 | |
| map_tile_grid = displayio.TileGrid(map_background, pixel_shader=map_palette) | |
| # Insert at index 0 so marker and label appear on top | |
| map_group.insert(0, map_tile_grid) | |
| # Track last GPS position to detect movement (will be set after GPS fix) | |
| last_gps_lat = None | |
| last_gps_lon = None | |
| while True: | |
| # Update GPS | |
| gps.update() | |
| # Update current position if we have GPS fix | |
| if gps.has_fix: | |
| current_lat = gps.latitude | |
| current_lon = gps.longitude | |
| # Only show speed if GPS reports it and it's above threshold | |
| if gps.speed_knots and gps.speed_knots > 0.1: | |
| current_speed = gps.speed_knots * 1.15078 | |
| else: | |
| current_speed = 0.0 | |
| gps_has_fix = True | |
| else: | |
| # Keep last known position, just update status | |
| gps_has_fix = False | |
| current_speed = 0.0 | |
| # TFT FeatherWing - Handle touch for zoom | |
| if tsc.touched and not touch_state: | |
| point = tsc.touch | |
| print("Touchpoint: (%d, %d, %d)" % (point["x"], point["y"], point["pressure"])) | |
| x_coord = point["x"] | |
| y_coord = point["y"] | |
| # Top half = zoom in, bottom half = zoom out | |
| if x_coord < 2000: # Top half (zoom in) | |
| new_zoom_index = available_zoom_levels.index(current_zoom) if current_zoom in available_zoom_levels else 2 | |
| if new_zoom_index < len(available_zoom_levels) - 1: | |
| current_zoom = available_zoom_levels[new_zoom_index + 1] | |
| # Update map with new zoom | |
| update_map_for_gps(current_lat, current_lon, current_zoom) | |
| update_marker_position(current_lat, current_lon) | |
| print(f"Zoomed in to level {current_zoom}") | |
| else: | |
| print(f"Already at max zoom {current_zoom}") | |
| else: # Bottom half (zoom out) | |
| new_zoom_index = available_zoom_levels.index(current_zoom) if current_zoom in available_zoom_levels else 2 | |
| if new_zoom_index > 0: | |
| current_zoom = available_zoom_levels[new_zoom_index - 1] | |
| # Update map with new zoom | |
| update_map_for_gps(current_lat, current_lon, current_zoom) | |
| update_marker_position(current_lat, current_lon) | |
| print(f"Zoomed out to level {current_zoom}") | |
| else: | |
| print(f"Already at min zoom {current_zoom}") | |
| touch_state = True | |
| if not tsc.touched and touch_state: | |
| touch_state = False | |
| current = time.monotonic() | |
| # Debug battery levels every 30 seconds | |
| if current - last_console_print >= 30: | |
| last_console_print = current | |
| if monitor.cell_percent > 102: | |
| print(f"Battery not connected") | |
| else: | |
| print(f"Battery voltage: {monitor.cell_voltage:.2f} Volts") | |
| print(f"Battery percentage: {monitor.cell_percent:.1f} %") | |
| # Update display data every 0.25 seconds (4 times per second) | |
| if current - last_print >= 0.25: | |
| last_print = current | |
| # Make sure we're showing the map, not the waiting screen | |
| if len(groups) > 0 and display.root_group != groups[index]: | |
| display.root_group = groups[index] | |
| if gps_has_fix: | |
| fix_status = "GPS: FIX" | |
| fix_quality = gps.fix_quality | |
| satellites = gps.satellites | |
| gps_info = f"Sats: {satellites}, Q: {fix_quality}" | |
| else: | |
| fix_status = "GPS: NO FIX (last known)" | |
| gps_info = "Using last position" | |
| gps_lat = current_lat | |
| gps_lon = current_lon | |
| gps_speed = current_speed | |
| # Update marker position based on GPS coordinates | |
| update_marker_position(gps_lat, gps_lon) | |
| # Update map if GPS position changed significantly (>0.005 degrees ≈ 500m) | |
| if gps_has_fix and last_gps_lat is not None and (abs(gps_lat - last_gps_lat) > 0.005 or abs(gps_lon - last_gps_lon) > 0.005): | |
| update_map_for_gps(gps_lat, gps_lon, current_zoom) | |
| last_gps_lat = gps_lat | |
| last_gps_lon = gps_lon | |
| t = datetime.now() | |
| time_str = f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}" | |
| if monitor.cell_percent > 102: | |
| power_text = "Battery: Not connected" | |
| else: | |
| power_text = f"Battery: {monitor.cell_percent:.1f}%" | |
| # Update map label | |
| zoom_info = f"Zoom: {current_zoom} ({available_zoom_levels[0]}-{available_zoom_levels[-1]})" | |
| cache_info = f"Tiles: {len(tile_cache)}" | |
| map_label.text = f"{time_str} GPS MAP\n{zoom_info} {cache_info}\n{fix_status} - {gps_info}\nLat: {gps_lat:.6f}\nLon: {gps_lon:.6f}\nSpeed: {gps_speed:.1f} mph\n{power_text}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment