Maker.io main logo

Minecraft GPS Compass

12

License: See Original Project Board / Sensor Interface Cables Environmental Magnetic Movement 3D Printing LCD / TFT Displays Microcontrollers Adafruit Feather Qwiic STEMMA Raspberry Pi MCU

Courtesy of Adafruit

Guide by Ruiz Brothers

Overview

gps_1

You can build a Minecraft-inspired compass with GPS.

Inspired by summer camp adventures, this compass saves and loads waypoints so you always know the way back.

The menu lets you pick previously saved locations and points you toward them as you explore outdoors.

build_2

This feature-packed compass has plenty of settings.

There are light and dark modes for any lighting, a marine mode, and adjustments for magnetic declination and units of measurement.

compass_3

compass_4

Powered by an Adafruit Feather RP2350, this project pairs a mini GPS module with a gyroscope and accelerometer.

The boards work together to sense the Earth's magnetic field and gather your GPS location.

Sensor calibration is handled right on the device, with a step-by-step walk-through that needs no computer.

sensor_5

sensor_6

Powered by CircuitPython, the code lets you rename the preset waypoints, set your local magnetic declination, and switch between miles and kilometers.

You can fine-tune the feel by editing the needle smoothing, the button hold time, or the case-fit offsets for your own enclosure.

powered_7

Parts

Circuit Diagram

This provides a visual reference for wiring of the components. This diagram was created using Fritzing software.

diagram_8

Install CircuitPython

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.

CircuitPython QuickStart

Follow this step-by-step to quickly get CircuitPython running on your board.

Download the latest version of CircuitPython for this board via circuitpython.org

Click the link above to download the latest CircuitPython UF2 file.

Save it wherever is convenient for you.

save_9

board_10

To enter the bootloader, hold down the BOOT/BOOTSEL button (highlighted in red above), and while continuing to hold it (don't let go!), press and release the reset button (highlighted in red or blue above). Continue to hold the BOOT/BOOTSEL button until the RP2350 drive appears!

If the drive does not appear, release all the buttons, and then repeat the process above.

You can also start with your board unplugged from USB, press and hold the BOOTSEL button (highlighted in red above), continue to hold it while plugging it into USB, and wait for the drive to appear before releasing the button.

A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.

You will see a new disk drive appear called RP2350.

Drag the adafruit-circuitpython-boardname-language-version.uf2 file to RP2350.

drag_11

drag_12

The RP2350 drive will disappear and a new disk drive called CIRCUITPY will appear.

That's it, you're done! :)

drive_13

Safe Mode

You want to edit your code.py or modify the files on your CIRCUITPY drive, but find that you can't. Perhaps your board has gotten into a state where CIRCUITPY is read-only. You may have turned off the CIRCUITPY drive altogether. Whatever the reason, safe mode can help.

Safe mode in CircuitPython does not run any user code on startup, and disables auto-reload. This means a few things. First, safe mode bypasses any code in boot.py (where you can set CIRCUITPY read-only or turn it off completely). Second, it does not run the code in code.py. And finally, it does not automatically soft-reload when data is written to the CIRCUITPY drive.

Therefore, whatever you may have done to put your board in a non-interactive state, safe mode gives you the opportunity to correct it without losing all of the data on the CIRCUITPY drive.

Entering Safe Mode

To enter safe mode when using CircuitPython, plug in your board or hit reset (highlighted in red above). Immediately after the board starts up or resets, it waits 1000ms. On some boards, the onboard status LED (highlighted in green above) will blink yellow during that time. If you press reset during that 1000ms, the board will start up in safe mode. It can be difficult to react to the yellow LED, so you may want to think of it simply as a slow double click of the reset button. (Remember, a fast double click of reset enters the bootloader.)

In Safe Mode

If you successfully enter safe mode on CircuitPython, the LED will intermittently blink yellow three times.

If you connect to the serial console, you'll find the following message.

Copy Code
Auto-reload is off.
Running in safe mode! Not running saved code.

CircuitPython is in safe mode because you pressed the reset button during boot. Press again to exit safe mode.

Press any key to enter the REPL. Use CTRL-D to reload.

You can now edit the contents of the CIRCUITPY drive. Remember, your code will not run until you press the reset button, or unplug and plug in your board, to get out of safe mode.

Flash Resetting UF2

If your board ever gets into a really weird state and CIRCUITPY doesn't show up as a disk drive after installing CircuitPython, try loading this 'nuke' UF2 to RP2350. which will do a 'deep clean' on your Flash Memory. You will lose all the files on the board, but at least you'll be able to revive it! After loading this UF2, follow the steps above to re-install CircuitPython.

Download flash erasing "nuke" UF2 for RP2350

Code

Add the CircuitPython Libraries

This project uses several libraries from the Adafruit CircuitPython Library Bundle. Using the Project Bundle below copies them into your lib folder automatically, but for reference the project imports:

  • adafruit_hx8357 - the 3.5" TFT display driver

  • adafruit_tsc2007 - the resistive touch controller

  • adafruit_lis3mdl - the magnetometer

  • adafruit_lsm6ds - the gyroscope and accelerometer

  • adafruit_gps - the PA1010D GPS module

  • adafruit_max1704x - the optional LiPo battery monitor

  • adafruit_debouncer - button press timing

  • adafruit_display_text - on-screen labels

project_14

Upload the Code and Files

Click the Download Project Bundle button below to download the code and assets as a zip file. Copy the contents to your CIRCUITPY drive, keeping the file and folder structure the same.

Download Project Bundle

Copy Code
# SPDX-FileCopyrightText: 2026 Pedro Ruiz for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""Minecraft Compass - a GPS waypoint navigator with a pixel-art needle.

A Minecraft-styled prop compass for kids navigating a campsite. The needle
points at the cardinal you face (or toward a saved waypoint), driven by a
tilt-compensated heading from the LSM6DSOX+LIS3MDL 9-DoF and a PA1010D GPS.

This file is the orchestrator: it creates the hardware, hands it to the
sensor and UI modules, runs the calibration hand-off if needed, and then
runs the main loop. The detail lives in three modules:
  - mc_config: constants and the editable settings (waypoint names, etc.)
  - mc_compass: sensor reads, heading math, and storage
  - mc_ui: the display - dial, needle, menus, and touch handling

Hardware (all Adafruit): Feather RP2350 8MB, 3.5" TFT FeatherWing (HX8357
+ TSC2007 touch), LSM6DSOX+LIS3MDL 9-DoF, PA1010D GPS, optional MAX17048
battery gauge, and a momentary button on D5. CircuitPython 10.x.
"""

import gc
import time

import board
import digitalio
import displayio
import fourwire
import supervisor

import adafruit_gps
import adafruit_lis3mdl
import adafruit_tsc2007
from adafruit_debouncer import Button
from adafruit_lsm6ds.lsm6dsox import LSM6DSOX
from adafruit_max1704x import MAX17048
from adafruit_hx8357 import HX8357

import mc_compass
import mc_settings
import mc_ui
import mc_waypoints
from mc_compass import (
    declination_index, marine_mode, mode, rotation_flipped,
    waypoints,
)
from mc_config import (
    BUTTON_PIN, DECLINATION_OPTIONS, HEADING_SMOOTHING, LONG_PRESS_MS,
    MAX_WAYPOINTS,
    MODE_COMPASS, MODE_WAYPOINT, NEEDLE_DEADBAND_DEG,
)

# --- Display: 3.5" TFT FeatherWing (HX8357) over SPI ---
displayio.release_displays()
spi = board.SPI()
# board.SPI() defaults to a conservative baud rate, which made each
# display.refresh() take ~150 ms. Raising it to 24 MHz roughly halved that
# to ~76 ms. The RP2350 SPI clock is an integer divisor of the system clock,
# so it rounds the request DOWN to the nearest achievable rate: 24 MHz lands
# on a good divisor, while 32 MHz rounds to a SLOWER real rate here, so
# 24 MHz is the sweet spot. (Measured.)
display_bus = fourwire.FourWire(
    spi, command=board.D10, chip_select=board.D9, baudrate=24_000_000
)
display = HX8357(display_bus, width=480, height=320)
# Manual refresh: the needle is redrawn by clearing its bitmap then drawing
# the rotated image. With auto-refresh on, the display could refresh in the
# gap between clear and draw, showing a blank "blink". Refreshing manually
# once per loop (after all drawing) makes each frame appear all at once.
display.auto_refresh = False
root = displayio.Group()
display.root_group = root

# --- Sensors on the STEMMA QT I2C chain ---
i2c = board.STEMMA_I2C()
mag = adafruit_lis3mdl.LIS3MDL(i2c)
imu = LSM6DSOX(i2c)

# PA1010D GPS on the same chain (I2C 0x10).
gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False)
# Stream just the sentences we need: GGA (fix + sats) and RMC (position).
gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0")
# Update position once per second (plenty for walking navigation).
gps.send_command(b"PMTK220,1000")


def detect_battery_gauge():
    """Return a MAX17048 if one answers at 0x36, else None.

    The Feather RP2350 has no onboard gauge, so this is an optional external
    breakout that may not be mounted yet. If absent (or if it fails to
    initialize), the battery indicator is hidden and everything else runs
    normally.
    """
    try:
        return MAX17048(i2c)
    except (ValueError, OSError):
        return None


battery = detect_battery_gauge()

# TSC2007 resistive touch (I2C 0x48). invert_x + swap_xy map raw touch to
# the 480x320 landscape orientation (per Adafruit's reference for this wing).
touch = adafruit_tsc2007.TSC2007(i2c, invert_x=True, swap_xy=True)

# Momentary button on D5, active-low with an internal pull-up. Button (from
# adafruit_debouncer) tracks press timing: a tap switches mode or closes a
# menu, a long press (>= LONG_PRESS_MS) saves a waypoint.
button_io = digitalio.DigitalInOut(BUTTON_PIN)
button_io.direction = digitalio.Direction.INPUT
button_io.pull = digitalio.Pull.UP
button = Button(button_io, long_duration_ms=LONG_PRESS_MS)

# --- Wire the modules together ---
# mc_compass takes the sensors and loads saved settings + waypoints; mc_ui
# takes the display objects and builds the scene.
mc_compass.setup(imu, mag, gps, battery, button)
mc_ui.setup(display, root, touch)
# Assemble the finished scene: add the background and every UI group to the
# root in draw order, hide the battery gauge if no fuel gauge is present, then
# apply the saved theme and rotation. The overlay groups live in their own
# modules; everything is appended here so the draw order is explicit.
root.append(mc_ui.bg_rect)
root.append(mc_ui.static_group)
root.append(mc_ui.needle_group)
root.append(mc_ui.header_shadow)
root.append(mc_ui.header_fg)
root.append(mc_ui.heading_num_shadow)
root.append(mc_ui.heading_num_fg)
root.append(mc_ui.battery_group)
root.append(mc_ui.inventory_group)
root.append(mc_ui.gear_group)
root.append(mc_waypoints.list_group)
root.append(mc_waypoints.picker_group)
root.append(mc_settings.settings_group)
root.append(mc_settings.info_group)
mc_ui.battery_group.hidden = not mc_compass.hw["battery_present"]
mc_ui.apply_theme()
mc_ui.apply_rotation()

# --- Phase routing ---
# If there is no calibration yet, hand off to the calibration program rather
# than dead-ending. supervisor runs calibration.py on the reload; when it
# finishes it hands control back here. This is what makes field
# recalibration work with no computer or file renaming.
cal = mc_compass.load_calibration()
if cal is None:
    print("No calibration found - launching calibration.py")
    supervisor.set_next_code_file("calibration.py")
    supervisor.reload()
    # reload() is not instant; stop here so nothing runs with cal=None.
    while True:
        time.sleep(0.1)

heading_offset = cal[6]
print("Minecraft Compass running.")
print(f"Heading offset: {heading_offset:0.2f} deg")
print("Tap: switch mode (or close a menu).  Hold: save waypoint.")

def handle_save_waypoint():
    """Open the name picker to capture the current fix, or preview it.

    With a fix, the picker assigns a name and the waypoint is saved. Without
    a fix the picker still opens so the names can be browsed (a preview of
    what locations can be called), but tapping one saves nothing and just
    reports there is no fix yet. A full list is the one hard block.
    """
    if len(waypoints) >= MAX_WAYPOINTS:
        mc_ui.flash_message("List full")
        return
    if not mc_compass.hw["gps"].has_fix:
        # Preview mode: open the picker with no coordinates. Selecting a name
        # won't save (handled in handle_picker_touch); it alerts instead.
        mc_waypoints.open_name_picker(None, None, keep_active=mode[0] == MODE_WAYPOINT)
        return
    # Capture the fix now and let the picker assign the name. In Waypoint
    # Mode keep the current target active (breadcrumb save); in Compass Mode
    # make the new point active.
    gps_obj = mc_compass.hw["gps"]
    mc_waypoints.open_name_picker(
        gps_obj.latitude, gps_obj.longitude,
        keep_active=mode[0] == MODE_WAYPOINT,
    )


# --- Loop state ---
last_needle_update = 0.0
last_text_update = 0.0
last_gps_update = 0.0
last_battery_update = 0.0
last_footer_text = [""]     # last footer string, to skip no-op text updates
smoothed_heading = [None]   # low-pass filtered heading (None until first read)
drawn_needle_angle = [None]  # last angle actually drawn, for the deadband
touch_was_down = [False]    # tracks touch state across polls for tap edges
mc_ui.update_battery()      # initial read so the icon is correct on frame 1
mc_ui.apply_mode_header(mode[0])

while True:
    now = time.monotonic()
    # Set True by anything that changes the screen this iteration; the single
    # display.refresh() at the bottom fires only when it's True.
    needs_refresh = False

    # GPS reads are expensive: the adafruit_gps GtopI2C driver reads one byte
    # per I2C transaction, so one NMEA sentence is ~70 locked transactions
    # (~300-500 ms). Position only changes at 1 Hz for walking, so read once
    # per second - the unavoidable stall then happens 4x less often. (A
    # faster fix would need a custom bulk-I2C reader the stock lib lacks.)
    if now - last_gps_update >= 1.0:
        gps.update()
        mc_ui.update_nav_target()
        last_gps_update = now

    event = mc_compass.poll_button()

    # Touch: route to whichever overlay is open (picker, list, settings),
    # else to the mode's button (inventory in Waypoint, gear in Compass).
    # Acts once per tap on the press edge.
    touch_point = mc_ui.read_touch()
    if touch_point is not None and not touch_was_down[0]:
        touch_was_down[0] = True
        if not mc_settings.info_group.hidden:
            mc_settings.handle_info_touch(touch_point)
        elif not mc_waypoints.picker_group.hidden:
            mc_waypoints.handle_picker_touch(touch_point)
        elif not mc_waypoints.list_group.hidden:
            mc_waypoints.handle_list_touch(touch_point)
        elif not mc_settings.settings_group.hidden:
            mc_settings.handle_settings_touch(touch_point)
        elif mode[0] == MODE_WAYPOINT and mc_ui.touched_inventory_button(touch_point):
            mc_waypoints.open_list()
        elif mode[0] == MODE_COMPASS and mc_ui.touched_gear(touch_point):
            mc_settings.open_settings()
        needs_refresh = True   # a tap may have changed an overlay/menu
    elif touch_point is None:
        touch_was_down[0] = False

    # While an overlay is open, the physical button closes it (matching the
    # tap-outside behavior) rather than switching modes or saving behind it.
    # A short press closes the topmost overlay; holds are ignored so a
    # hold-to-save/theme can't fire behind a menu.
    any_overlay = not (mc_waypoints.list_group.hidden and mc_waypoints.picker_group.hidden
                       and mc_settings.settings_group.hidden
                       and mc_settings.info_group.hidden)
    if any_overlay:
        if event == "short":
            if not mc_settings.info_group.hidden:
                mc_settings.info_group.hidden = True     # info closes to settings
            elif not mc_waypoints.picker_group.hidden:
                mc_waypoints.pending_save.clear()          # cancel pending save
                mc_waypoints.picker_group.hidden = True
                mc_ui.apply_mode_header(mode[0])
            elif not mc_settings.settings_group.hidden:
                mc_settings.close_settings()
            elif not mc_waypoints.list_group.hidden:
                mc_waypoints.close_list()
            needs_refresh = True
        event = "none"   # never fall through to mode-switch/save while open

    if event == "short":
        mode[0] = MODE_WAYPOINT if mode[0] == MODE_COMPASS else MODE_COMPASS
        mc_ui.apply_mode_header(mode[0])
        needs_refresh = True
    elif event == "save":
        # Hold-to-save works in both modes. In Compass Mode the new point
        # becomes the active target (you saved it to go to it). In Waypoint
        # Mode it's saved as a breadcrumb without redirecting the navigation
        # you're already following - select it from the list to switch.
        handle_save_waypoint()
        mc_ui.apply_mode_header(mode[0])
        needs_refresh = True

    # Sensor reads (I2C) and heading math are the loop's most expensive work,
    # so do them only when we actually update the needle (30 Hz) rather than
    # every ~5 ms loop.
    if now - last_needle_update >= 0.033:  # 30 Hz
        mag_cal = mc_compass.read_mag_calibrated(cal)
        accel = mc_compass.read_accel()
        # corrected_heading is magnetic; add declination (E positive) to get
        # true heading so compass and waypoint bearings read true north.
        declination = DECLINATION_OPTIONS[declination_index[0]]
        raw_heading = (
            mc_compass.corrected_heading(mag_cal, accel, heading_offset)
            + declination
        ) % 360
        # Low-pass the heading so magnetometer noise doesn't make the needle
        # twitch. Jitter both looks bad and forces a large dirty region (slow
        # refresh), so smoothing helps the look and the frame rate.
        smoothed_heading[0] = mc_compass.smooth_heading(
            raw_heading, smoothed_heading[0], HEADING_SMOOTHING
        )
        heading_deg = smoothed_heading[0]

        # When the case is built button-at-bottom, the display is flipped 180
        # and the IMU is physically turned 180 with it, so the same facing
        # reads a heading 180 off. Correct it so N still reads N.
        if rotation_flipped[0]:
            heading_deg = (heading_deg + 180) % 360

        if mode[0] == MODE_WAYPOINT:
            needle_angle, footer_text = mc_ui.waypoint_needle_and_footer(
                heading_deg
            )
        else:
            # Marine mode: needle always points to true north. Intuitive mode
            # (default): needle points at the cardinal you face.
            if marine_mode[0]:
                needle_angle = (0.0 - heading_deg) % 360
            else:
                needle_angle = heading_deg
            cardinal = mc_compass.cardinal_from_heading(heading_deg)
            footer_text = f"{heading_deg:3.0f} deg  {cardinal}"

        # Only redraw the needle when it actually moved beyond a small
        # deadband. Holding still then does almost no refreshing (the needle
        # bitmap clear+rotozoom and the SPI push are the costly steps), and
        # tiny sub-degree wobble is ignored.
        if (drawn_needle_angle[0] is None
                or abs(mc_compass.angle_diff(needle_angle, drawn_needle_angle[0]))
                >= NEEDLE_DEADBAND_DEG):
            mc_ui.set_needle_heading(needle_angle)
            drawn_needle_angle[0] = needle_angle
            needs_refresh = True
        last_needle_update = now

        if now - last_text_update >= 0.2:  # 5 Hz text update
            # Only touch the labels when the string actually changed. The
            # footer is far from the needle, so a footer change forces a
            # refresh spanning both (the ~158 ms spikes); skipping no-op
            # updates keeps most frames to the needle-only ~76 ms path.
            if footer_text != last_footer_text[0]:
                mc_ui.heading_num_fg.text = footer_text
                mc_ui.heading_num_shadow.text = footer_text
                last_footer_text[0] = footer_text
                needs_refresh = True
            last_text_update = now

    if now - last_battery_update >= 10.0:  # battery changes slowly
        mc_ui.update_battery()
        last_battery_update = now
        needs_refresh = True

    # Restore the header when a brief flashed message ("List full", "No fix
    # yet") has expired. Non-blocking, so the needle never freezes for it.
    if mc_ui.tick_message():
        needs_refresh = True

    # Refresh only when something actually changed (at most the 30 Hz needle
    # rate), not every ~5 ms loop. Refreshing every loop was pushing the SPI
    # display ~200x/sec for no reason, throttling the whole loop.
    if needs_refresh:
        display.refresh(minimum_frames_per_second=0)

    # Collect garbage at a controlled point each loop so GC pauses stay small
    # and regular instead of building up and firing mid-render (which showed
    # as the needle going smooth-then-choppy and laggy taps).
    gc.collect()

    time.sleep(0.005)

View on GitHub

bundle_15

After copying, your CIRCUITPY drive should contain these files:

  • code.py - the main program: hardware setup, the screen, and the main loop

  • mc_config.py - constants and the editable settings

  • mc_compass.py - the sensors, heading math, and storage

  • mc_ui.py - the compass dial, needle, and touch handling

  • mc_waypoints.py - the waypoint list and name picker screens

  • mc_settings.py - the settings panel and info guide screens

  • calibration.py - the guided magnetometer calibration

  • boot.py - filesystem setup so the compass can save waypoints

  • needle.bmp - the compass needle artwork

  • d-map.bmp - the magnetic declination map shown in the info guide

The program is split across several files so each part stays small and easy to read. You don't need to edit any of them to use the compass, but the split makes it simple to find a feature if you want to change one.

Customize It

Everything you are likely to want to change lives in the EDITABLE SETTINGS block at the top of mc_config.py. Open that file and look for these values near the top.

Rename the waypoint presets by editing PICK_NAMES. Keep names to about 10 characters so they fit the header cleanly. There are 10 slots:

Copy Code
PICK_NAMES = (
    "Home", "Spawn", "Portal", "Mine", "Castle",
    "Nether", "End", "Stronghold", "Outpost", "Camp",
)

Set your local magnetic declination. This is also adjustable on the device in Settings, so you can usually leave it at zero:

Copy Code
MAGNETIC_DECLINATION = 0.0

Tune the feel of the needle. Lower smoothing values are steadier but slower to respond; the deadband ignores tiny wobble so the needle holds still when you do:

Copy Code
HEADING_SMOOTHING = 0.25
NEEDLE_DEADBAND_DEG = 1.5

Change how long you hold the button to save a waypoint (in milliseconds):

Copy Code
LONG_PRESS_MS = 1500

If you design your own case, the case-fit offsets line the screen content up with your cutout. These were measured for our enclosure and a different case may need them nudged:

Copy Code
ROTATED_X_SHIFT = 21
ROTATED_TOUCH_Y_FIX = 22
HEADER_RIGHT_X = 254

First Boot and Calibration

The first time the compass runs without a saved calibration, it automatically launches the calibration walk-through. Follow the on-screen steps to rotate the device through each axis. When calibration finishes, it saves the result and hands control back to the main program. No file renaming or computer is needed, so you can recalibrate in the field at any time from the Settings menu.

boot_16

How the Files Fit Together

The files form a simple chain. mc_config.py holds the settings and depends on nothing else. mc_compass.py reads the sensors and does the math. mc_ui.py builds the screen. mc_waypoints.py and mc_settings.py add the menu screens on top. code.py creates the hardware once and hands it to the other files, then runs the loop. Keeping the hardware setup in one place is what lets the rest of the code stay small and avoid tangled imports.

3D Printing

3MF files for 3D printing are oriented and ready to print on FDM machines using PLA filament. Original design source files may be downloaded using the links below.

3d_18

Minecraft GPS.3mf

Edit Design

The dropdown on the Fusion 360 site allows you to pick your preferred 3D file format like STEP, STL, etc.

Slice with settings for PLA material

The parts were sliced using BambuStudio using the slice settings below.

  • PLA filament 220c extruder

  • 0.2 layer height

  • 10% gyroid infill

  • 200mm/s print speed

  • Tree Supports

  • 60 C heated bed

slice_19

Design Source Files

The project assembly was designed in Fusion 360. Once opened in Fusion 360, It can be exported in different formats like STEP, STL and more.

Electronic components like Adafruit's boards, displays, connectors and more can be downloaded from the Adafruit CAD parts GitHub Repo.

files_20

Assembly

Solder Short header

Short headers are soldered to the Feather so it fits into the sockets on the TFT FeatherWing.

See the guide below on tips on how to solder headers.

header_21

header_22

solder_23

How To Solder Headers

By Erin St Blaine

Overview

View Guide

Sensor frame

Use M2.5x6mm machine screws to mount the sensor to the printed frame.

Position the STEMMA ports as shown to allow the short STEMMA cables to reach each sensor.

frame_23

Align sensor frame

Position the notch to the USB port and the standoffs to the screw mounts on the TFT display.

align_24

Mount sensor frame

Use M3x6mm machine screws to fasten the frame to the display.

mount_25

Anti-Glare film

For outdoor use a piece of anti-glare film can be cut to size and laid over the screen.

film_26

film_27

Connect sensors to TFT

Use the STEMMA port on the TFT to connect the Feather into the rest of the daisy chained sensors.

connect_28

Battery frame

The optional battery monitor can mount to the screw mounts on the battery frame with M2x6mm screws. The 2500mAh LiPo battery slides into the frame with the JST cable facing outward.

The battery plugs into the battery monitor and a JST-PH 2-pin jumper cable connects from the battery monitor to the JST port on the Feather.

frame_29

Tactile Push Button

To fit inside the case, you'll need to temporally remove the button cap from the switch stem by gently pulling it off.

Align the STEMMA JST PH port on the tactile button in the same direction of the LiPo battery and use an M2.5x6mm machine screw to fasten to the mount on the battery frame.

tactile_30

Connect STEMMA Battery Monitor

Plug in the last STEMMA board into the chain.

plug_31

Ground Socket Splitter

Measure and cut a jumper wire with a pin end to a two socket header to create a splitter to easily share the ground connection to the button and the slide switch. Bend the pin to allow it to plug into the socket header on the TFT display.

ground_32

Slide Switch

Use another set of pin to socket jumper wires to easily plug the slide switch into the EN and GND sockets on the TFT.

slide_33

STEMMA JST PH Jumper Cable

The STEMMA JST PH port can use a cable with pin ends to connect to the sockets on the TFT. Clip off the center red wire and connect the black wire to the shared GND socket and the white wire to Pin 5 socket on the TFT display.

cable_34

Socket connections

Bend and connect the jumper pins to the sockets on the TFT.

socket_35

Connect Tactile Button

Plug in the STEMMA JST PH cable into the port on the tactile button.

button_36

Align Battery Frame to Sensor Frame

Make sure to route the slide switch cable to the opposite side of the USB port on the Feather before placing the battery frame over the sensor frame.

The stand-offs on the battery frame align to the screws mounts on the sensor frame.

align_37

Tactile Button Slot

The sensor frame features a cavity to allow the push button PCB to seat into.

slot_38

Attach battery frame to sensor frame

Use M2.5x6mm machine screws to attach together.

use_39

Insert assembly into case

Align the Feather USB port to the cutout on the case. Insert the assembly at an angle and place the USB port against the cutout.

insert_40

Attach Slide Switch to the Lid

The slide switch press fits into the cutout and walls on the lid with the wires coiled up on the opposite side of the USB port.

attach_41

Fasten Lid to Case

Align the walls on the lid to battery frame to help it hold the assembly against the USB port. Use M2.5x6mm machine screws to attach the lid to the case.

fasten_42

Reattach the Button Cap

Align the button cap the stem on the push button and complete the build!

reattach_43

reattach_44

Use

The compass has two physical inputs: the touchscreen and the side button.

  • Tap the button - switch between Compass Mode and Waypoint Mode (or close an open menu)

  • Hold the button - save your current location as a waypoint

  • Tap the gear (Compass Mode) - open Settings

  • Tap the menu icon (Waypoint Mode) - open the waypoint list

  • Tap outside a menu - close it

use_45

Boot Up

Power on with the side slide switch. The compass starts in Compass Mode. The first time it runs without a saved calibration, it launches the calibration walk-through automatically. After calibration it returns here on its own.

boot_46

Compass Mode

The needle points toward the direction you are facing, and the heading is shown below the dial in degrees with its cardinal letter, like 045 deg NE. Hold the device flat for the most accurate reading; the heading is tilt-compensated, so a small tilt is fine. A gear icon sits in the bottom corner to open Settings.

compass_47

Waypoint Mode

The needle points toward your active saved waypoint instead of a compass direction, and the footer shows the distance to it. The header shows the waypoint's name. A menu icon sits in the bottom corner to open the waypoint list. Before the GPS has a fix, the footer shows that it is still searching for satellites.

mode_48

Battery

With the optional LiPo monitor installed, a battery gauge appears in the top corner showing the charge level, with a small bolt while charging. If no monitor is installed, the gauge is hidden and everything else works normally.

battery_49

Alerts

Short messages appear briefly in the header and then clear on their own. No fix yet means a waypoint was selected before the GPS locked on; the choice still applies and navigation begins once a fix is acquired. List full means all waypoint slots are used and you need to delete one before saving another.

alerts_50

The Side Button

A quick tap of the side button switches between Compass Mode and Waypoint Mode. If a menu is open, a tap closes it instead.

Holding the button saves your current GPS location as a waypoint and opens the name picker. In Compass Mode the new waypoint becomes your active target. In Waypoint Mode the new point is saved as a breadcrumb without interrupting the waypoint you are already navigating toward, so you can drop a marker and keep going.

Saving and Naming a Waypoint

Holding the button opens the name picker, a grid of preset names. Tap a name to save the location under it. If there is no GPS fix yet, the picker still opens so you can browse the names, and its title shows Names (no fix); tapping a name in this state saves nothing and reminds you that there is no fix yet.

The Waypoint List

Tap the menu icon in Waypoint Mode to open the list of saved waypoints. Use the arrows to scroll if you have more than fit on screen.

Each row uses an arm-then-confirm tap to prevent accidents. Tap a waypoint's name once to arm it; it highlights gold. Tap it again to make it your active navigation target and close the list.

To delete a waypoint, tap its trash box once to arm it; it turns red. Tap it again to confirm the delete. Tapping elsewhere cancels the armed state.

list_51

Settings

Tap the gear icon in Compass Mode to open Settings. Scroll with the arrows. Each option uses an arm-then-confirm tap: tap a row to arm it, then tap again to apply the change. The available settings are:

  • Theme - switch between dark and light. Light mode is easier to read in bright sunlight.

  • Marine Mode - the needle points to true north instead of the direction you face.

  • Units - switch waypoint distances between miles and kilometers.

  • Declination - adjust for the difference between magnetic north and true north for your area. While this row is armed, the arrows step the value up and down.

  • Rotation - flip the display 180 degrees, for a case built with the button at the bottom.

  • Recalibrate - restart the magnetometer calibration walk-through. This row arms red as a confirmation, since it reboots into calibration.

settings_52

The Info Guide

Tap the question mark in the Settings title bar to open the info guide. It has two pages: a summary of the controls, and a magnetic declination map to help you look up your local value. Tap inside to flip pages, and tap the X or outside to return to Settings.

guide_53

Magnetic Declination

True north and magnetic north are not the same, and the difference depends on where you are. For the most accurate headings, set your local declination in Settings. The info guide includes a map, or you can look up your value from an online declination calculator. If you leave it at zero the compass still works; headings will simply be off by your local declination amount.

decline_54

Recalibrating in the Field

Magnetometers can drift, especially near metal or electronics. If the heading seems off, open Settings and choose Recalibrate. The device reboots into the guided walk-through, and when it finishes it returns to the compass. No computer is needed, so you can do this anywhere.

recal_55

Mfr Part # 6130
ADAFRUIT FEATHER RP2350 WITH HST
Adafruit Industries LLC
Mfr Part # 3002
SHORT FEATHER MALE HEADERS - 12-
Adafruit Industries LLC
Mfr Part # 3651
3.5" 480X320 TOUCHSCREEN FOR FEA
Adafruit Industries LLC
Mfr Part # 4415
MINI GPS PA1010D STEMMA QT
Adafruit Industries LLC
Mfr Part # 4517
LSM6DSOX + LIS3MDL 9 DOF IMU
Adafruit Industries LLC
Mfr Part # 4431
STEMMA TACTILE PUSH-BUTTON 5PACK
Adafruit Industries LLC
Mfr Part # 3893
JST PH 3PIN TO MALE HEADER CABLE
Adafruit Industries LLC
Mfr Part # 5580
ADAFRUIT MAX17048 LIPOLY / LIION
Adafruit Industries LLC
Mfr Part # 4714
JST-PH 2-PIN JUMPER CABLE - 100M
Adafruit Industries LLC
Mfr Part # 328
BATTERY LITH-ION 3.7V 2.5AH
Adafruit Industries LLC
Mfr Part # 4399
STEMMA QWIIC JST SH CABLE 50MM
Adafruit Industries LLC
Mfr Part # 805
BREADBOARD-FRIENDLY SPDT SLIDE S
Adafruit Industries LLC
Mfr Part # 5585
SHORT SOCKET HEADERS FOR RASPBER
Adafruit Industries LLC
Mfr Part # 5837
PREMIUM COLORFUL SILICONE EXTENS
Adafruit Industries LLC
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.