Bluetooth Integration Guide: Build Companion Apps for TripQube
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.
Quick Links
What's Possible Now
TripQube broadcasts navigation data over Bluetooth Low Energy (BLE), making it possible for secondary devices to access:
- Real-time navigation: Maneuver instructions, distance to next turn, arrival time
- Current position: GPS coordinates, bearing/heading, speed in real-time
- Complete route: Full route geometry for visual displays (delta-encoded for efficiency)
- Road context: Major and minor roads near your position
- Waypoints: Multi-stop routes with check-in status
- Travel history: Path already ridden during the current trip
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
TYPE_NAV (1)— Maneuver, distance, arrival time, instruction text (~1 Hz)TYPE_POSITION (2)— Current location, bearing, speed (~1 Hz)TYPE_ROUTE_BEGIN (3)— Route started: point count + generation IDTYPE_ROUTE_CHUNK (4)— Route geometry, delta-encodedTYPE_STOPS (5)— Waypoints and destinationTYPE_ROADS_BEGIN (6)— Road context startedTYPE_ROADS_LINE (7)— Road geometryTYPE_TRAVELLED_BEGIN (8)— Travelled path startedTYPE_TRAVELLED_CHUNK (9)— Travelled path geometry
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
- MTU negotiation: Request higher MTU (up to 517 bytes) to receive more geometry per packet:
gatt.requestMtu(517) - Scan mode: Use
SCAN_MODE_LOW_POWERto reduce battery drain - Coalesce updates: Queue frames instead of rendering each one if your UI is slow
- Route changes: Only redraw routes when generation ID changes (indicates new route)
- Reconnection: Implement exponential backoff for reconnect attempts
Resources & Documentation
- Full Protocol Guide: See
BLUETOOTH_INTEGRATION_GUIDE.mdin the mapper repository for complete frame specifications, coordinate encoding, and advanced topics - Reference Implementation: The Glasses Peripheral project shows a complete working BLE receiver in Kotlin
- Android BLE: Official Android Bluetooth Low Energy documentation
- Protocol Details:
GlassesProtocol.ktin both mapper and glasses-peripheral for exact encoding specs
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:
- Clone the glasses-peripheral repository to see a working reference implementation
- Download the complete
BLUETOOTH_INTEGRATION_GUIDE.mdfor detailed frame specifications - Implement the basic connection flow (scan → connect → subscribe → decode)
- Add parsing for the specific frame types your app needs
- 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.