BLE Communication with SitePoint GPS Devices

A comprehensive guide showing how to communicate with SitePoint GPS devices over Bluetooth Low Energy, including RTCM correction data transmission and SQTP protocol handling.

The BLEManager class demonstrates how to develop Bluetooth Low Energy applications for RTK GPS positioning using the Qt Bluetooth API. The implementation covers:

  • Scanning and connecting to SitePoint GPS devices

  • Queuing and transmitting RTCM v3 correction data

  • Receiving GPS position data (LLA) via SQTP protocol

  • Managing device configuration and mode changes

  • Battery and device information service integration

Key Qt Classes Used

The example demonstrates the use of the following Qt Bluetooth classes:

  • QBluetoothDeviceDiscoveryAgent - Device discovery

  • QLowEnergyController - Connection management

  • QLowEnergyService - Service access

  • QLowEnergyCharacteristic - Characteristic read/write/notify

  • QLowEnergyDescriptor - Descriptor configuration

SitePoint Device Architecture

SitePoint GPS devices expose a custom BLE service with UUID: 00000100-34ed-12ef-63f4-317792041d17

This service provides two primary characteristics:

UUID

Purpose

Properties

0x0102

RTCM transmission

Write (WriteWithoutResponse)

0x0105

SQTP protocol

Read, Write, Notify

Additionally, the devices support standard BLE services:

  • Device Information Service (0x180A)

  • Battery Service (0x180F)

Initializing the BLE Manager

The BLEManager constructor sets up the discovery agent, initializes the RTCM transmission queue, and configures the SQTP protocol handlers.

Key initialization steps:

  • Set global instance pointer for callback access

  • Configure SQTP frame transmission and parsing callbacks

  • Initialize pre-allocated RTCM buffer pool (50 messages)

  • Create transmission timer for FSM pacing

  • Connect discovery agent signals

Scanning for Devices

Device scanning is initiated with startScan(). The method performs several validation checks before starting discovery.

The scan process:

  1. Validates Bluetooth adapter is available and powered on

  2. Clears any previous discovery results

  3. Starts Low Energy device discovery

  4. Filters results to only SitePoint devices (via onDeviceDiscovered callback)

Each discovered SitePoint device triggers the deviceDiscovered() signal with the device name and Bluetooth address.

Connecting to a Device

Once a device is discovered, connect using its Bluetooth address.

The connection process:

  1. Creates QLowEnergyController for the target device

  2. Connects controller signals (connected, disconnected, errors, etc.)

  3. Initiates connection

  4. Automatically discovers services upon connection

  5. Negotiates MTU (Maximum Transmission Unit)

  6. Configures characteristics for RTCM and SQTP communication

Upon successful connection, the deviceConnected() signal is emitted and service discovery begins automatically.

Transmitting RTCM Correction Data

RTCM v3 correction messages from an NTRIP caster are queued for transmission using queueRTCMMessage().

The RTCM transmission architecture features:

  • Pre-allocated buffer pool (50 messages, no heap allocation)

  • Finite state machine for paced transmission

  • Automatic fragmentation for messages exceeding MTU

  • Timer-based pacing matching BLE connection intervals

  • Prioritization of RTCM over SQTP messages

RTCM Transmission FSM States

        stateDiagram-v2
   [*] --> RTCM_TX_IDLE
   RTCM_TX_IDLE --> RTCM_TX_SENDING : Message queued
   RTCM_TX_SENDING --> RTCM_TX_WAITING : Fragment sent
   RTCM_TX_WAITING --> RTCM_TX_SENDING : Timer expires & more data
   RTCM_TX_WAITING --> RTCM_TX_IDLE : Timer expires & queue empty
   RTCM_TX_SENDING --> RTCM_TX_IDLE : Transmission complete
    

The FSM has three states:

  • RTCM_TX_IDLE - No transmission active, queue empty

  • RTCM_TX_SENDING - Actively packing and sending messages

  • RTCM_TX_WAITING - Waiting for next connection interval

State transitions ensure messages are sent at optimal intervals to match the BLE connection timing (typically 7.5ms - 15ms intervals).

Receiving GPS Data

GPS position data is received via notifications on the SQTP characteristic (0x0105). The SQTP protocol encapsulates three types of data:

  • SQTP_ID_SITEPOINT_LLA - Position data (lat/lon/altitude, accuracy)

  • SQTP_ID_SITEPOINT_STATUS - Device status (satellites, corrections, etc.)

  • SQTP_ID_SITEPOINT_LOCAL_BASE_CONFIG - Configuration data

Received data is parsed by the sqtpBleParseSubframe() callback and stored in global variables for efficient access:

extern SqspLla_t myLla;        // GPS position data
extern SqspStatus_t myStatus;  // Device status
extern SqspLbConfig_t myConfig; // Configuration

Change detection ensures signals (llaDataChanged(), statusDataChanged(), configDataChanged()) are only emitted when data actually changes, not on every 8 Hz update from the device.

Device Mode Changes

The GPS device can operate in several modes:

  • DEVICE_MODE_ROVER - Standard rover (no corrections)

  • DEVICE_MODE_ROVER_NTRIP - Rover with NTRIP corrections via BLE

  • DEVICE_MODE_ROVER_NEAREST - Rover with Tripod network corrections

  • DEVICE_MODE_FIXED_BASE - Fixed base station

  • DEVICE_MODE_FIXED_BASE_BROADCAST - Fixed base with broadcast

Mode changes are sent using sendModeChangeCommand():

bleManager->sendModeChangeCommand(DEVICE_MODE_ROVER_NTRIP);

MTU Negotiation and Fragmentation

Upon connection, the BLE stack negotiates an MTU (Maximum Transmission Unit). The usable payload size is calculated with platform-specific limits in mind.

MTU Calculation

BLEManager uses a single-source-of-truth design for payload limits via the m_maxPayload member variable, calculated when MTU changes:

void BLEManager::onMtuChanged(int mtu) {
    m_currentMtu = mtu;
    // Platform-aware calculation (Android: 512 byte cap, Others: MTU-3)
    m_maxPayload = qMin(mtu - 3, MAX_BLE_GATT_ATTR_LEN);
}

Platform-Specific Behavior

Different platforms enforce BLE GATT attribute limits differently:

Platform

MTU Negotiated

Max Payload

Notes

Android

517 bytes

512 bytes

Strict BLE GATT spec enforcement (crashes if exceeded)

Windows

517 bytes

514 bytes (MTU-3)

Lenient enforcement, allows full MTU-3

iOS/macOS

517 bytes

514 bytes (MTU-3)

Version-dependent, generally lenient

Automatic Fragmentation

RTCM messages exceeding m_maxPayload are automatically fragmented across multiple BLE packets using sentOffset tracking:

  • Large messages split transparently across multiple transmission timer ticks

  • Example: 514-byte message on Android → 512 bytes (tick 1) + 2 bytes (tick 2)

  • No reassembly needed at device (RTCM v3 is byte-stream protocol)

  • Fragmentation counter tracked in m_messagesFragmented

Benefits

  • Consistency: Same payload limit used for validation, packing, and transmission

  • Safety: Android 512-byte limit prevents crashes

  • Efficiency: Windows/iOS utilize full MTU-3 when possible

  • Simplicity: Single calculation point, no redundant logic

The getCurrentMtu() method provides the current negotiated MTU value.

Complete Usage Example

// Create BLE manager
BLEManager *bleManager = new BLEManager(this);

// Connect signals
connect(bleManager, &BLEManager::deviceDiscovered,
        [](const QString &name, const QString &address) {
    qDebug() << "Found device:" << name << "at" << address;
});

connect(bleManager, &BLEManager::deviceConnected, []() {
    qDebug() << "Device connected successfully";
});

connect(bleManager, &BLEManager::llaDataChanged, [bleManager]() {
    SqspLla_t lla = bleManager->getLlaData();
    qDebug() << "Position:" << lla.latitude << "," << lla.longitude;
});

// Start scanning
bleManager->startScan();

// Later, connect to discovered device
bleManager->connectToDevice("12:34:56:78:9A:BC");

// Queue RTCM correction message (from NTRIP caster)
QByteArray rtcmMessage = /* received from NTRIP */;
bleManager->queueRTCMMessage(rtcmMessage);

// Change device mode
bleManager->sendModeChangeCommand(DEVICE_MODE_ROVER_NTRIP);

Performance Considerations

High-Frequency Data Access

GPS data arrives at 8-10 Hz. For optimal performance when accessing position data frequently (e.g., map updates), use direct extern access instead of function calls:

extern SqspLla_t myLla;  // Declared in BLEManager.cpp

// Direct access (fast, no function call overhead)
double lat = myLla.latitude;
double lon = myLla.longitude;

RTCM Queue Sizing

The RTCM queue is pre-allocated with 50 message slots. For typical NTRIP correction streams (MSM4/MSM7 messages at 1 Hz), this provides ample buffering. If messages are dropped (check statistics), consider reducing the correction stream data rate or message types.

No Heap Allocation During Transmission

The RTCM transmission system is designed for real-time performance:

  • Buffer pool pre-allocated at construction

  • Fixed-size send buffer (2048 bytes)

  • No dynamic allocation during message transmission

API Reference

For detailed API documentation, see:

class BLEManager : public QObject

Manages Bluetooth Low Energy communication with SitePoint GPS devices.

The BLEManager class provides comprehensive BLE functionality for communicating with SitePoint GPS devices. It handles:

  • Device discovery and connection management

  • RTCM v3 correction data transmission with queuing and FSM

  • SQTP (SitePoint Transport Protocol) message handling

  • SQSP (SitePoint Protocol) data reception (LLA, Status, Config)

  • Battery and Device Information Service support

  • MTU negotiation and automatic fragmentation

Usage

Basic workflow:

  1. Create BLEManager instance

  2. Call startScan() to discover nearby SitePoint devices

  3. Connect to desired device using connectToDevice()

  4. Queue RTCM messages using queueRTCMMessage()

  5. Monitor GPS data via llaDataChanged(), statusDataChanged(), configDataChanged() signals

Characteristics

The BLEManager works with two primary BLE characteristics on the SitePoint service:

  • 0x0102 (RTCM): Write-only characteristic for sending RTK correction data

  • 0x0105 (SQTP): Read/Write/Notify characteristic for bidirectional protocol communication

RTCM Transmission Architecture

RTCM messages are queued and transmitted via a finite state machine (FSM) that:

  • Uses a pre-allocated 50-message buffer pool (no heap allocation)

  • Paces transmission to match BLE connection intervals

  • Automatically fragments messages exceeding MTU limits

  • Prioritizes RTCM over SQTP (queues SQTP when RTCM active)

Global Data Structures

The following global variables (extern) provide fast access to device data:

  • myLla: GPS position data (latitude, longitude, altitude, accuracy)

  • myStatus: Device status (GPS state, satellites, corrections, aidingBins)

  • myConfig: Device configuration (base station position, mode settings)

These are updated automatically when notifications arrive on the 0x0105 characteristic. Change detection is implemented to emit signals only when data actually changes.

See also

QBluetoothDeviceDiscoveryAgent, QLowEnergyController, QLowEnergyService

Note

The RTCM characteristic uses WriteWithoutResponse for maximum throughput. Messages are sent and forgotten - retransmission is not supported as stale corrections are useless for RTK positioning.

Private Helper Methods

Reads all characteristics of a service (legacy, unused)

RTCM Queue Management Methods

Initializes the RTCM buffer pool

Public Functions

explicit BLEManager(QObject *parent = nullptr)

Constructs a BLEManager instance.

[blemanager-constructor]

Initializes the BLE discovery agent, RTCM buffer pool, transmission timer, and SQTP protocol handlers. The manager is ready to scan for devices after construction.

Parameters:

parent – Parent QObject for memory management

Q_INVOKABLE void startScan ()

Starts scanning for nearby BLE devices.

[blemanager-constructor]

Initiates Bluetooth Low Energy device discovery using the Low Energy method. Only devices advertising the SitePoint service UUID will be reported via the deviceDiscovered() signal.

[blemanager-startscan]

Note

Requires Bluetooth to be enabled on the host system. Will emit errorOccurred() if Bluetooth is unavailable or powered off.

Q_INVOKABLE void stopScan ()

Stops the ongoing BLE device scan.

[blemanager-startscan]

Halts the device discovery process if currently active. Safe to call even if no scan is in progress.

See also

startScan()

Q_INVOKABLE void connectToDevice (const QString &deviceAddress)

Connects to a discovered SitePoint device.

[blemanager-connect]

Establishes a BLE connection to the specified device and initiates service discovery. Upon successful connection:

  1. deviceConnected() signal is emitted

  2. Services are discovered automatically

  3. RTCM and SQTP characteristics are configured

  4. MTU is negotiated

  5. Notifications are enabled on the SQTP characteristic

Note

The device must have been discovered via startScan() first.

Parameters:

deviceAddress – Bluetooth address of the device (from deviceDiscovered signal)

Q_INVOKABLE void disconnectFromDevice ()

Disconnects from the currently connected device.

Terminates the BLE connection, clears all queues, stops transmission timer, and resets all device data (myLla, myStatus, myConfig) to zero. Emits deviceDisconnected() and sqtpDisconnected() signals.

Q_INVOKABLE void readCharacteristic (const QString &characteristicUuid)

Legacy characteristic read function (deprecated)

[blemanager-connect]

Deprecated:

This function is no longer used. Characteristic reading is now handled automatically during service discovery.

Parameters:

characteristicUuid – UUID of characteristic to read (unused)

Q_INVOKABLE SqspLla_t getLlaData ()

Returns the current GPS position data.

Provides direct access to the global myLla structure. Data is updated automatically when notifications arrive from the device.

See also

llaDataChanged()

Note

For high-frequency access (8-10 Hz), consider using extern myLla directly to avoid function call overhead.

Returns:

SqspLla_t structure containing latitude, longitude, altitude, and accuracy

bool sendRTCMData(const QByteArray &data)

Sends RTCM data immediately (low-level method)

Low-level method that writes RTCM data directly to the 0x0102 characteristic using WriteWithoutResponse. Does not use the queue or FSM.

Note

Message must fit within MTU-3 bytes or transmission will fail.

Warning

Most applications should use queueRTCMMessage() instead, which provides queuing, pacing, and fragmentation support.

Parameters:

data – RTCM message bytes to send

Returns:

true if sent successfully, false on error

bool queueRTCMMessage(const QByteArray &data)

Queues an RTCM message for transmission (recommended method)

[blemanager-queuertcm]

Adds an RTCM message to the transmission queue. The FSM will automatically transmit messages at appropriate intervals matching the BLE connection timing. Messages larger than MTU will be automatically fragmented.

Queue capacity: 50 messages (pre-allocated, no heap allocation)

See also

sendRTCMData()

Note

This is the recommended method for RTCM transmission. The FSM handles:

  • Proper pacing to match BLE connection intervals

  • Automatic fragmentation for large messages

  • Buffer management and overflow protection

Parameters:

data – RTCM message bytes to queue

Returns:

true if queued successfully, false if queue full or device not ready

SqtpStatus_t sendSQTPMessage(const QByteArray &data)

Sends an SQTP message on the 0x0105 characteristic.

Sends an SQTP protocol message to the device. If RTCM transmission is active, the message is queued in a single-slot pending buffer and sent when RTCM idle.

Note

Used internally by SQTP protocol handlers. Applications typically don’t call this directly - use sendModeChangeCommand() or sendConfigData() instead.

Parameters:

data – SQTP frame bytes to send

Returns:

SQTP_STATUS_SUCCESS on success, error code on failure

int getCurrentMtu() const

Returns the current negotiated MTU value.

The MTU (Maximum Transmission Unit) determines the maximum packet size. Usable payload is MTU-3 bytes (3 bytes for BLE ATT header).

Note

Not all platforms expose MTU (e.g., Linux returns -1). Default MTU of 23 bytes is assumed if platform doesn’t report.

Returns:

Current MTU in bytes, or 0 if not connected/unknown

SqtpStatus_t sendModeChangeCommand(DeviceMode mode)

Sends a device mode change command.

Changes the operating mode of the GPS device by sending an SQTP configuration message with the appropriate mode bits set.

Supported modes:

  • DEVICE_MODE_ROVER: Standard rover mode (no corrections)

  • DEVICE_MODE_ROVER_NTRIP: Rover with NTRIP corrections via BLE

  • DEVICE_MODE_ROVER_NEAREST: Rover with nearest base corrections (Tripod network)

  • DEVICE_MODE_FIXED_BASE: Fixed base station mode

  • DEVICE_MODE_FIXED_BASE_BROADCAST: Fixed base with RTK broadcast

See also

DeviceMode, sendConfigData()

Parameters:

mode – Desired device mode (ROVER, ROVER_NTRIP, ROVER_NEAREST, FIXED_BASE, etc.)

Returns:

SQTP_STATUS_SUCCESS on success, error code on failure

Q_INVOKABLE bool sendConfigData (double longitude, double latitude, double height, double hMSL, double hAcc, double vAcc, int duration)

Sends custom configuration data to the device.

Sends a complete configuration message to the device including base station position, accuracy estimates, and survey duration. Uses the current mode settings from myConfig.mode.

Note

Typically used in Fixed Base mode to set the known base station position.

Parameters:
  • longitude – Base station longitude in degrees

  • latitude – Base station latitude in degrees

  • height – Base station ellipsoidal height in meters

  • hMSL – Base station height above mean sea level in meters

  • hAcc – Horizontal accuracy in meters

  • vAcc – Vertical accuracy in meters

  • duration – Survey duration in seconds

Returns:

true on success, false on failure

Q_INVOKABLE int getBatteryLevel () const

Returns the current battery level.

Battery level is read from the Battery Service characteristic 0x2A19. Updates automatically via notifications if the device supports them.

Returns:

Battery percentage (0-100), or -1 if unknown

Q_INVOKABLE QString getBatteryPowerState () const

Returns the battery power state as a formatted string.

Provides detailed information about battery presence, wired/wireless power connection, and charging state. Read from Battery Power State characteristic 0x2A1A.

Returns:

Human-readable power state (e.g., “Battery: Present | Wired: Connected | Charge: Charging”)

Q_INVOKABLE QString getBatteryLevelState () const

Returns the battery level state.

Categorical battery level indication from Battery Level State characteristic 0x2A1B.

Returns:

“Unknown”, “Critical”, “Low”, or “Good”

Public Members

bool m_sqtpActive = false

True when SQTP protocol is actively connected.

Signals

void deviceDiscovered(const QString &deviceName, const QString &deviceAddress)

Emitted when a SitePoint device is discovered during scanning.

This signal is emitted for each SitePoint device found during BLE scanning. Only devices advertising the SitePoint service UUID (00000100-34ed-12ef-63f4-317792041d17) are reported.

Note

May be emitted multiple times for the same device as more details are discovered.

Parameters:
  • deviceName – Advertised name of the device

  • deviceAddress – Bluetooth address of the device (use with connectToDevice())

void deviceConnected()

Emitted when BLE connection to device is established.

Indicates successful connection. Service discovery begins automatically after this signal is emitted.

void deviceDisconnected()

Emitted when BLE connection to device is lost.

Emitted when connection is lost (either intentionally via disconnectFromDevice() or due to connection failure). All queues are cleared and device data is reset.

void errorOccurred(const QString &errorString)

Emitted when a BLE error occurs.

Reports BLE-related errors such as Bluetooth unavailable, connection failure, or characteristic access errors.

Parameters:

errorString – Human-readable description of the error

void inputReceived(const QString &input)

Legacy signal (unused)

Deprecated:

This signal is not currently used in the implementation.

Parameters:

input – Input data (unused)

void serviceDiscovered(const QString &serviceUuid)

Emitted when a BLE service is discovered on the device.

Emitted during service discovery for each service found on the device.

Parameters:

serviceUuid – UUID of the discovered service

void characteristicDiscovered(const QString &characteristicUuid)

Emitted when a characteristic is discovered.

Deprecated:

This signal is not currently used in the implementation.

Parameters:

characteristicUuid – UUID of the discovered characteristic

void characteristicValueRead(const QString &characteristicUuid, const QByteArray &value)

Emitted when a characteristic value is read.

Deprecated:

This signal is not currently used in the implementation.

Parameters:
  • characteristicUuid – UUID of the characteristic

  • value – Raw bytes read from the characteristic

void llaDataChanged()

Emitted when GPS position data (LLA) changes.

Indicates that the global myLla structure has been updated with new position data from the device. Change detection ensures this signal is only emitted when data actually changes (not on every 8 Hz update).

Access updated data via:

  • getLlaData() method

  • Direct extern access to myLla global variable

void statusDataChanged()

Emitted when device status data changes.

Indicates that the global myStatus structure has been updated with new device status information (GPS state, satellites, corrections, aidingBins).

Access updated data via direct extern access to myStatus global variable.

void configDataChanged()

Emitted when device configuration data changes.

Indicates that the global myConfig structure has been updated with new configuration information (mode settings, base position, etc.).

Access updated data via direct extern access to myConfig global variable.

void mtuChanged(int newMtu)

Emitted when MTU value changes.

Emitted when the BLE MTU (Maximum Transmission Unit) is negotiated or changes. Usable payload size is newMtu - 3 bytes.

See also

getCurrentMtu()

Parameters:

newMtu – New MTU value in bytes

void batteryLevelChanged(int level)

Emitted when battery level changes.

Emitted when battery level is read or updated via notification.

Parameters:

level – New battery level (0-100 percent)

void sqtpConnected()

Emitted when SQTP protocol connection is established.

Indicates that the first valid SQTP subframe has been received from the device. After this signal, the device is ready for full bidirectional communication.

void sqtpDisconnected()

Emitted when SQTP protocol connection is lost.

Indicates SQTP connection has been lost (either due to user disconnect, BLE connection loss, or device reporting disconnected state).

See also

sqtpConnected()

Public Static Functions

static BLEManager *getInstance()

Returns the global BLEManager instance.

Provides access to the BLEManager from C callbacks that don’t have direct object access (e.g., SQTP protocol handlers).

Note

Only one BLEManager instance should exist per application.

Returns:

Pointer to the singleton BLEManager instance