Wire the NEO-6M
Connect the GPS module to the Flipper Zero GPIO header: 3.3V → Pin 9, GND → Pin 11, GPS TX → Pin 13 (RX), GPS RX → Pin 14 (TX). No level shifter required — both devices run at 3.3 V.
A full-featured tactical GPS application for the Flipper Zero, built around the NEO-6M module. Live position, trip odometer, satellite SNR charts — all in 128×64 pixels.
TacNav GPS is a custom Flipper Zero application that transforms the device into a pocket tactical GPS display. It parses NMEA sentences from a NEO-6M GPS module connected over UART and presents the data across five purpose-built screens.
Real-time latitude, longitude, altitude, and UTC time parsed from RMC and GGA sentences with hemisphere-aware display.
Military-style heads-up display with fix indicator, speed in knots, heading in degrees, and cardinal direction labels.
Haversine-based distance accumulator tracks total distance traveled. Long-press OK to reset. Noise-filtered at <2 m jumps.
Live bar chart of up to 12 satellites from GSV sentences, sorted by signal strength, with dBHz labels on strong signals.
Cycle through six baud rates (4800–115200) on the fly with the D-pad to match any NEO-6M configuration.
Dedicated worker thread with FuriStreamBuffer decouples ISR byte delivery from NMEA parsing. Mutex-protected shared state.
The u-blox NEO-6M is a compact, high-performance GPS receiver that outputs standard NMEA 0183 sentences at configurable baud rates. It runs on 3.3 V — perfectly matched to the Flipper Zero GPIO header.
| Flipper Pin | NEO-6M Pin | Type | Notes |
|---|---|---|---|
| Pin 9 — 3.3V | VCC | Power | 3.3 V supply |
| Pin 11 — GND | GND | Ground | Common ground |
| Pin 13 — RX | TX | UART | GPS transmits NMEA sentences |
| Pin 14 — TX | RX | UART | Flipper sends baud commands |
TacNav v2 provides five purpose-built screens navigable with the D-pad and OK button. The system auto-shows the splash screen whenever a GPS fix is lost.
Automatically displayed when no GPS fix is available. Inverted black background, double border frame, and corner crosshair brackets for a tactical aesthetic.
The primary display, showing full position data in a clean military-inspired layout. A solid disc indicates a valid fix; a blinking hollow circle means the fix is lost.
Dedicated screen for motion data. Speed is shown in both knots and km/h. The trip odometer accumulates Haversine distance and switches from meters to km once 1 km is exceeded.
Diagnostic view showing GPS signal health. HDOP is labeled with a five-tier quality rating. Active baud rate shown top-right.
Visual signal-to-noise ratio chart assembled from multi-message GSV sequences. Up to 12 satellites sorted strongest-first. Bars are 7 px wide with 2 px gaps, centered horizontally.
All navigation and settings are accessible through the Flipper Zero's D-pad and action buttons.
Short press OK or Right to advance to the next screen (HUD → Move → Signal → Sats → HUD).
Short press Left to go back one screen in the cycle.
Increase UART baud rate to the next step (4800 → 9600 → 19200 → 38400 → 57600 → 115200).
Decrease UART baud rate to the previous step. Wraps around at the bottom.
Long-press OK while on the Movement screen to zero the trip distance accumulator.
Short press Back at any time to cleanly exit. UART and GUI resources are properly freed.
TacNav GPS v2 ships as a pre-compiled .fap for convenience, or you can build from source with the official Flipper Zero firmware toolchain.
Connect the GPS module to the Flipper Zero GPIO header: 3.3V → Pin 9, GND → Pin 11, GPS TX → Pin 13 (RX), GPS RX → Pin 14 (TX). No level shifter required — both devices run at 3.3 V.
Using qFlipper or a microSD card, copy gps_tacsys_v2.fap from the dist/ folder to SD:/apps/GPIO/ on your Flipper Zero.
On the Flipper, navigate to Applications → GPIO → TacNav GPS v2. The splash screen will appear immediately and begin acquiring a fix.
The splash screen shows satellite count in real time. Once the NEO-6M achieves a valid RMC fix, the app automatically transitions to the Tactical HUD. Move to open sky for faster acquisition.
Clone the Flipper Zero firmware, place the TACNAV-V2/ folder under applications_user/, then run ./fbt fap_gps_tacsys_v2 from the firmware root.
Key implementation details for contributors and power users.
/* ISR context: byte arrives → stream → signal worker */ static void gps_uart_rx_callback(FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* ctx) { GpsUart* uart = (GpsUart*)ctx; if(event == FuriHalSerialRxEventData) { uint8_t byte = furi_hal_serial_async_rx(handle); furi_stream_buffer_send(uart->rx_stream, &byte, 1, 0); furi_thread_flags_set(furi_thread_get_id(uart->worker_thread), WorkerEventRxData); } } /* Worker thread: drain stream → assemble NMEA lines */ static int32_t gps_uart_worker(void* ctx) { GpsUart* uart = (GpsUart*)ctx; while(true) { uint32_t events = furi_thread_flags_wait(WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever); if(events & WorkerEventStop) break; if(events & WorkerEventRxData) { uint8_t byte; while(furi_stream_buffer_receive(uart->rx_stream, &byte, 1, 0) == 1) { if(byte == '\n' || byte == '\r') { if(uart->line_pos > 0) { uart->line_buf[uart->line_pos] = '\0'; uart->line_callback(uart->line_buf, uart->callback_context); uart->line_pos = 0; } } else if(uart->line_pos < GPS_NMEA_LINE_MAX - 1) uart->line_buf[uart->line_pos++] = (char)byte; else uart->line_pos = 0; } } } return 0; }
static float haversine_km(float lat1, float lon1, float lat2, float lon2) { float dlat = (lat2 - lat1) * DEG_TO_RAD; float dlon = (lon2 - lon1) * DEG_TO_RAD; float a = sinf(dlat * 0.5f) * sinf(dlat * 0.5f) + cosf(lat1 * DEG_TO_RAD) * cosf(lat2 * DEG_TO_RAD) * sinf(dlon * 0.5f) * sinf(dlon * 0.5f); return EARTH_R_KM * 2.0f * atan2f(sqrtf(a), sqrtf(1.0f - a)); } /* Trip odometer — only accumulate with valid fix and movement */ if(g->valid && g->speed_knots > 0.1f) { if(was_valid && g->odo_has_last) { float dist = haversine_km(g->odo_last_lat, g->odo_last_lon, g->latitude, g->longitude); if(dist > 0.002f) g->trip_distance_km += dist; /* ignore <2m GPS noise */ } g->odo_last_lat = g->latitude; g->odo_last_lon = g->longitude; g->odo_has_last = true; }
static const char* hdop_label(float hdop) { if(hdop <= 1.0f) return "IDEAL"; if(hdop <= 2.0f) return "EXLNT"; if(hdop <= 5.0f) return "GOOD "; if(hdop <= 10.0f) return "MOD "; return "POOR "; } static const char* course_to_cardinal(float deg) { static const char* dirs[] = { "N","NNE","NE","ENE","E","ESE","SE","SSE", "S","SSW","SW","WSW","W","WNW","NW","NNW" }; return dirs[((int)((deg + 11.25f) / 22.5f)) % 16]; }