← Blog Home

Bluetooth Integration Guide: Build Companion Apps for TripQube

By TripQube — Aug 24, 2026 • Developer Guide

TL;DR — We've published a complete developer guide for building companion apps that integrate with TripQube via Bluetooth. Receive real-time navigation data, route geometry, position updates, and waypoint information. Perfect for HUD glasses, smartwatches, custom displays, and IoT devices.

What's Possible Now

TripQube broadcasts navigation data over Bluetooth Low Energy (BLE), making it possible for secondary devices to access:

Use Cases: HUD glasses that display turn-by-turn without looking down • Smartwatch apps showing next maneuver • Custom dashboard for adventure bikes • IoT tracking for group rides • Motorcycle helmet displays • Aftermarket head-up displays

Getting Started in 5 Minutes

Step 1: UUIDs

All communication happens via a single Bluetooth service:

Service: 7b8f0001-4c2a-4f1d-9a6e-2f3b5c8d1e40 Navigation State: 7b8f0002-4c2a-4f1d-9a6e-2f3b5c8d1e40 Status: 7b8f0003-4c2a-4f1d-9a6e-2f3b5c8d1e40

Step 2: Scan & Connect

val scanner = bluetoothAdapter?.bluetoothLeScanner val filter = ScanFilter.Builder() .setServiceUuid(ParcelUuid(SERVICE_UUID)) .build() scanner?.startScan(listOf(filter), settings, scanCallback)

Step 3: Listen for Data

gatt.setCharacteristicNotification(navCharacteristic, true) override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray ) { val frame = decode(value) // Process: Nav, Position, Route, Waypoints, etc. }

That's it. You're now receiving live navigation data from TripQube.

Understanding the Protocol

Frame Format

Every message starts with a 2-byte header:

Offset Size Field
0 1 byte Version (6)
1 1 byte Frame Type (1-9)
2+ variable Frame-specific data

Frame Types

Pro Tip: Realtime frames (NAV, POSITION) are under 23 bytes and never fragment. Only route/road geometry uses multiple packets.

Example: Parse Navigation Instruction

// TYPE_NAV frame Offset Size Field 0 1 Version (6) 1 1 Type (1) 2 1 Maneuver code (0-20) 3..4 2 Distance metres (uint16) 5..6 2 Arrival minutes (uint16, 0xFFFF = unknown) 7..8 2 Altitude metres (int16, 0x7FFF = unknown) 9+ var Instruction text (UTF-8) // Example: "Turn right on Main St, 250 metres, arriving 6:42PM" Maneuver: 3 (turn right) Distance: 250 metres Arrival: minutes since midnight Instruction: "Turn right on Main St"

Maneuver Codes Reference

0 = Unknown 1 = Straight 2 = Turn Left 3 = Turn Right 4 = Slight Left 5 = Slight Right 6 = Sharp Left 7 = Sharp Right 8 = U-Turn Left 9 = U-Turn Right 10 = Keep Left 11 = Keep Right 12 = Merge 13 = Roundabout 14 = Destination 15 = Depart 16 = Ramp Left 17 = Ramp Right 18 = Fork Left 19 = Fork Right 20 = Free Drive (no route)

Complete Working Example

Here's a minimal but complete Android implementation:

import android.content.Context import android.bluetooth.* import android.bluetooth.le.* import java.nio.ByteBuffer import java.nio.ByteOrder import java.util.UUID class NavigationListener(private val context: Context) { private val bluetoothAdapter: BluetoothAdapter? get() = (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter private var gatt: BluetoothGatt? = null var onNav: ((maneuver: String, distance: Int, text: String) -> Unit)? = null var onPosition: ((lat: Double, lng: Double, speed: Int) -> Unit)? = null fun start() { val scanner = bluetoothAdapter?.bluetoothLeScanner ?: return val filter = ScanFilter.Builder() .setServiceUuid(ParcelUuid(SERVICE_UUID)) .build() scanner.startScan(listOf(filter), scanSettings(), scanCallback) } private val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { gatt = result.device.connectGatt(context, false, gattCallback) } } private val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { gatt.discoverServices() } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { val char = gatt.getService(SERVICE_UUID) ?.getCharacteristic(NAV_STATE_UUID) ?: return gatt.setCharacteristicNotification(char, true) } override fun onCharacteristicChanged(gatt: BluetoothGatt, char: BluetoothGattCharacteristic, value: ByteArray) { processFrame(value) } } private fun processFrame(payload: ByteArray) { if (payload.size < 2) return val buffer = ByteBuffer.wrap(payload).order(ByteOrder.BIG_ENDIAN) val version = buffer.get() val type = buffer.get() when (type.toInt()) { 1 -> parseNav(buffer, payload) 2 -> parsePosition(buffer) // Handle other types... } } private fun parseNav(buffer: ByteBuffer, payload: ByteArray) { val maneuver = buffer.get().toInt() and 0xFF val distance = buffer.short.toInt() and 0xFFFF val arrival = buffer.short.toInt() and 0xFFFF val altitude = buffer.short.toInt() val text = String(payload, buffer.position(), payload.size - buffer.position(), Charsets.UTF_8) val maneuverName = maneuverCodeToString(maneuver) onNav?.invoke(maneuverName, distance, text) } private fun parsePosition(buffer: ByteBuffer) { val lat = buffer.int / 1e7 val lng = buffer.int / 1e7 val bearing = buffer.short.toInt() and 0xFFFF val speed = buffer.get().toInt() and 0xFF onPosition?.invoke(lat, lng, speed) } fun stop() { gatt?.disconnect() gatt?.close() } private fun maneuverCodeToString(code: Int) = when (code) { 1 -> "Straight" 2 -> "Turn Left" 3 -> "Turn Right" 4 -> "Slight Left" 5 -> "Slight Right" 13 -> "Roundabout" 14 -> "Destination" else -> "Unknown" } companion object { val SERVICE_UUID = UUID.fromString("7b8f0001-4c2a-4f1d-9a6e-2f3b5c8d1e40") val NAV_STATE_UUID = UUID.fromString("7b8f0002-4c2a-4f1d-9a6e-2f3b5c8d1e40") } }

Permissions Required

<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />

Performance Tips

Resources & Documentation

Questions? The protocol is stable and versioned (currently v6). Check the GlassesProtocol.kt decoder in both the mapper and glasses-peripheral projects for authoritative field layouts.

What's Next?

You now have everything needed to build a companion app. Start with:

  1. Clone the glasses-peripheral repository to see a working reference implementation
  2. Download the complete BLUETOOTH_INTEGRATION_GUIDE.md for detailed frame specifications
  3. Implement the basic connection flow (scan → connect → subscribe → decode)
  4. Add parsing for the specific frame types your app needs
  5. Test with a live TripQube navigation session

The TripQube ecosystem is open for builders. Let's see what you create. 🏍️

Ready to Build? Download the full Bluetooth Integration Guide now and start building your companion app.