📱 Guia de Desenvolvimento — App VL08

Como desenvolver um app (mobile ou desktop) para conectar ao VL08 via BLE, Bluetooth Classic ou Wi-Fi e consultar dados do veículo em tempo real.

Método recomendado: BLE (Bluetooth Low Energy) — sem pareamento, compatível com Android/iOS/Web, baixo consumo. O VL08 com módulo FC41D suporta BLE 5.2.

O que você vai aprender

  • Conectar ao VL08 via BLE sem pareamento
  • Enviar comandos XVM e receber respostas
  • Consultar GPS, velocidade, CAN, entradas, acelerômetro e mais
  • Parsear as respostas para exibir no app
  • Exemplos de código em Flutter/Dart, Kotlin (Android) e Swift (iOS)

🏗️ Arquitetura de Comunicação

📱
Seu App
Android / iOS / Desktop
⟶ BLE / BT / Wi-Fi ⟶
📡
VIRLOC VL08
FC41D (BLE 5.2) ou BX3105 (BT 4.2)

Métodos de Conexão

MétodoMóduloPareamentoAlcanceMelhor para
BLEFC41D (BLE 5.2)Não requer~30mApps mobile, baixo consumo
BT ClassicBX3105 (BT 4.2)Sim (sem PIN)~10mTerminal serial, SPP
Wi-FiFC41D (2.4GHz)Mesma redeRede localMonitoramento em rede fixa
Protocolo único: Independente do meio de conexão (BLE, BT, Wi-Fi, Serial), todos usam o mesmo protocolo XVM. Aprenda uma vez, use em qualquer meio.

📶 BLE — Visão Geral

O método mais prático para apps mobile. O VL08 com módulo FC41D atua como GATT Server (peripheral) que aceita conexões sem necessidade de pareamento.

Parâmetros GATT

ElementoUUID 16-bitUUID 128-bit
Service0xFFFF0000ffff-0000-1000-8000-00805f9b34fb
Characteristic0xFF010000ff01-0000-1000-8000-00805f9b34fb
Uma única characteristic para Write (enviar comandos) e Notify (receber respostas). Não precisa buscar outras.

Nome do Dispositivo BLE

O VL08 anuncia como VIRTEC_VL8_XXXX onde XXXX é o ID do equipamento.

Filtre pelo prefixo VIRTEC_VL8_ no scan para encontrar apenas VL08.


🔍 BLE — Scan & Descoberta

1 Iniciar BLE Scan
2 Filtrar por "VIRTEC_VL8_"
3 Listar dispositivos
4 Usuário seleciona
⚠️ Android: requer permissões BLUETOOTH_SCAN, BLUETOOTH_CONNECT e ACCESS_FINE_LOCATION (Android 12+). No iOS, é necessário declarar NSBluetoothAlwaysUsageDescription no Info.plist.

🔗 BLE — Conectar & GATT

1 Connect
2 Discover Services
3 Find Service 0xFFFF
4 Find Char 0xFF01
5 Enable Notify
Pronto!

Passo a passo detalhado

  1. Conectar ao peripheral pelo endereço MAC (Android) ou UUID (iOS)
  2. Descobrir serviços — chamar discoverServices()
  3. Encontrar serviço com UUID 0000ffff-0000-1000-8000-00805f9b34fb
  4. Encontrar characteristic 0000ff01-0000-1000-8000-00805f9b34fb
  5. Habilitar notificações — escrever 0x0100 no descriptor CCCD (0x2902)
  6. Pronto — agora pode enviar comandos (Write) e receber respostas (Notify)
MTU: O padrão é 20 bytes. Negocie MTU maior (até 512) com requestMtu(512) para respostas mais longas como QTT.

💬 BLE — Enviar & Receber Dados

Envio (Write)

Escreva o comando XVM como bytes ASCII (7-bit) na characteristic 0xFF01. Como o protocolo XVM usa apenas caracteres ASCII imprimíveis, tanto US-ASCII quanto UTF-8 produzem o mesmo resultado:

// Enviar consulta de status completo
byte[] cmd = ">QTT<".getBytes("US-ASCII");
characteristic.setValue(cmd);
gatt.writeCharacteristic(characteristic);
Codificação: O protocolo XVM é ASCII puro (7-bit). Todos os valores numéricos são representados como texto (ex: "0422" não 0x0422). UTF-8 funciona porque é compatível com ASCII para caracteres 0–127.

Recepção (Notify)

A resposta chega como notificação na mesma characteristic. Pode vir fragmentada em múltiplas notificações se for maior que o MTU.

// Callback de notificação
onCharacteristicChanged(gatt, characteristic) {
  String chunk = new String(characteristic.getValue(), "UTF-8");
  buffer.append(chunk);

  // Resposta completa quando encontrar ">"...algo..."<"
  if (buffer.contains("<")) {
    String response = buffer.toString();
    parseXvmResponse(response);
    buffer.clear();
  }
}
⚠️ Fragmentação: Respostas longas (ex: QTT retorna ~80 chars) chegam em vários pacotes BLE. Sempre acumule em buffer até receber o delimitador <.

Máquina de Estados — Conexão BLE

Implemente uma state machine para gerenciar a conexão de forma robusta:

┌──────────┐     scan()     ┌───────────┐    connect()    ┌────────────┐
│          │ ──────────────→ │           │ ──────────────→ │            │
│ IDLE     │                 │ SCANNING  │                 │ CONNECTING │
│          │ ←────────────── │           │ ←────────────── │            │
└──────────┘   timeout/stop  └───────────┘   error/timeout └──────┬─────┘
     ↑                                                            │
     │ disconnect()                                    onConnected│
     │                                                            ↓
┌──────────┐   onDisconnect  ┌────────────┐  discoverSvcs ┌────────────┐
│          │ ←────────────── │            │ ←──────────── │            │
│DISCONN.  │                 │ CONNECTED  │               │ DISCOVERING│
│          │ ──(retry?)────→ │            │ ─────────────→│            │
└──────────┘                 └────────────┘  enable notify└────────────┘

Estratégia de Reconexão

CenárioEstratégia
Desconexão inesperadaTentar reconectar com backoff exponencial: 1s → 2s → 4s → 8s → 16s (máx. 5 tentativas)
Erro na descoberta de serviçosDesconectar, aguardar 2s, reconectar
Timeout no scanParar scan após 10s. Informar usuário. Permitir retry manual.
Saída de alcanceDetectar via RSSI < -90dBm. Avisar usuário antes.
VL08 em sleepAguardar despertar (pode levar 30–600s). Enviar >QSN< como heartbeat.
// Exemplo: reconexão com backoff
int retries = 0;
void onDisconnected() {
  if (retries < 5) {
    int delay = pow(2, retries).toInt();  // 1, 2, 4, 8, 16 segundos
    Future.delayed(Duration(seconds: delay), () => connect(device));
    retries++;
  } else {
    notifyUser("Não foi possível reconectar. Verifique o VL08.");
  }
}

void onConnected() {
  retries = 0;  // Reset contador
}

Intervalo entre Comandos

SituaçãoIntervalo MínimoNotas
Entre comandos individuais200msO FC41D precisa processar cada resposta
Polling periódico (QTT)1–5sNão há necessidade de polling mais rápido
Após erro/timeout1sDar tempo para o módulo se recuperar
Comandos múltiplos aglutinadosN/AUse >QGP<>QIN< em vez de envios separados
⚠️ Enviar comandos muito rápido (intervalo < 100ms) pode causar perda de respostas ou travamento do módulo BLE. Implemente uma fila de comandos com delay mínimo.

🔵 Bluetooth Classic — SPP

Para VL08 com módulo BX3105 (BT v4.2). Requer pareamento prévio (sem PIN).

Conexão SPP

ParâmetroValor
PerfilSPP (Serial Port Profile)
UUID SPP00001101-0000-1000-8000-00805f9b34fb
PareamentoObrigatório (sem PIN)
Baudrate equivalente115200

Fluxo de Comunicação

  1. Parear com o VL08 (dispositivo aparece como VIRTEC_VL8_XXXX)
  2. Abrir socket RFCOMM com UUID SPP
  3. Enviar comandos XVM como string ASCII via OutputStream
  4. Ler respostas via InputStream (delimitadas por >...<)
// Android - Bluetooth Classic SPP
BluetoothSocket socket = device.createRfcommSocketToServiceRecord(
    UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
);
socket.connect();

OutputStream out = socket.getOutputStream();
out.write(">QTT<\r\n".getBytes());

InputStream in = socket.getInputStream();
// Ler até encontrar '<'
Nota: iOS não suporta SPP nativo. Use BLE (FC41D) para apps iOS.

🛜 Wi-Fi — Socket TCP/UDP

O VL08 conecta como cliente a redes Wi-Fi 2.4GHz previamente cadastradas e envia dados para IPs configurados.

⚠️ O VL08 não atua como servidor (não aceita conexões entrantes). Para receber dados via Wi-Fi, seu app precisa ser um servidor TCP/UDP na mesma rede.

Configuração no VL08

// 1. Cadastrar rede Wi-Fi
>VSWI20,"MinhaRede""MinhaSenha"<

// 2. Configurar IP do seu servidor
>VSIP8,192.168.1.100.05000.05000,UDP,AUX0<

// 3. Criar evento para enviar via Wi-Fi
>SED001 TD01++ +- WF8 TT<

App como Servidor

// Servidor UDP simples (Python)
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 5000))

while True:
    data, addr = sock.recvfrom(1024)
    print(f"VL08: {data.decode('ascii')}")

    # Enviar comando de volta
    sock.sendto(b">QTT<", addr)

Triggers Wi-Fi úteis

TriggerEvento
WF20++Wi-Fi conectou à rede
WF20--Wi-Fi desconectou
WF30++Socket 0 conectou
WF30--Socket 0 desconectou

📡 Protocolo XVM — Formato

Todas as mensagens (query, config, resposta) seguem o formato XVM.

Formato Simplificado (BLE/Serial)

Via BLE ou serial, o formato mínimo é:

>COMANDO<

// Exemplos:
>QTT<           // Consultar status completo
>QGP<           // Consultar posição GPS
>QSN<           // Consultar serial
>QVR<           // Consultar versão firmware
✅ Via BLE/Serial: header ID, número de mensagem e checksum são opcionais. Basta enviar >COMANDO<.

Formato Completo (TCP/UDP)

Para comunicação via rede (TCP/UDP), o formato completo com checksum é recomendado:

>COMANDO;ID=XXXX;#NNNN;*CC<[CR][LF]

│         │       │      │
│         │       │      └─ Checksum (XOR de todos bytes de '>' até último ';')
│         │       └─ Número da mensagem (hex)
│         └─ ID do equipamento
└─ Delimitador de início

// Numeração:
// 8000–FFFF = mensagens originadas pelo app
// 0001–7FFF = mensagens originadas pelo dispositivo

Múltiplos Comandos

Envie vários comandos separados por <> em uma única mensagem (máx. 180 chars):

>QSN<>QVR<       // Serial + Versão
>QGP<>QIN<>QAD<  // GPS + Entradas + Analógicas

✅ Checksum XOR

Necessário apenas para comunicação TCP/UDP. Calcula-se o XOR de todos os bytes ASCII do > até o último ; (sem incluir *).

Dart / Flutter
Kotlin
Swift
JavaScript
String xvmChecksum(String msg) {
  int xor = 0;
  for (int i = 0; i < msg.length; i++) {
    xor ^= msg.codeUnitAt(i);
  }
  return xor.toRadixString(16).toUpperCase().padLeft(2, '0');
}

// Uso:
String body = '>QTT;ID=1234;#8001;';
String cs = xvmChecksum(body);
String fullMsg = '$body*$cs<\r\n';
fun xvmChecksum(msg: String): String {
    var xor = 0
    for (c in msg) xor = xor xor c.code
    return String.format("%02X", xor)
}

// Uso:
val body = ">QTT;ID=1234;#8001;"
val cs = xvmChecksum(body)
val fullMsg = "${body}*${cs}<\r\n"
func xvmChecksum(_ msg: String) -> String {
    var xor: UInt8 = 0
    for byte in msg.utf8 { xor ^= byte }
    return String(format: "%02X", xor)
}

// Uso:
let body = ">QTT;ID=1234;#8001;"
let cs = xvmChecksum(body)
let fullMsg = "\(body)*\(cs)<\r\n"
function xvmChecksum(msg) {
  let xor = 0;
  for (let i = 0; i < msg.length; i++) {
    xor ^= msg.charCodeAt(i);
  }
  return xor.toString(16).toUpperCase().padStart(2, '0');
}

// Uso:
const body = '>QTT;ID=1234;#8001;';
const cs = xvmChecksum(body);
const fullMsg = `${body}*${cs}<\r\n`;

📩 Parsing de Respostas

Padrão de Resposta

Toda resposta XVM é delimitada por > e <. O tipo de resposta é identificado pelo prefixo:

PrefixoTipoExemplo
RTTResposta de QTT>RTT140521125208-2352502-04667893001267300...
RGPResposta de QGP>RGP140521125234-2352502-04667897001267300...
RSNResposta de QSN>RSN44K00077_VL10<
RVRResposta de QVR>RVR VIRTUALTEC VIRLOC8 BR061...<
RSDResposta de QSD>RSD0035<
RCTResposta de QCT>RCT01 01500<
RADResposta de QAD>RAD00 010100001829 12483297...<
RINResposta de QIN>RIN10000000<
RUVReport automático>RUV01100,NT003,240622...<
ACKConfirmação>ACK;ID=1234;...<

Estratégia de Parsing

// Pseudocódigo
function parseResponse(raw: String) {
  // Extrair conteúdo entre > e <
  content = raw.substringBetween('>', '<')

  // Identificar tipo pela primeiras 3 letras
  type = content.substring(0, 3)

  switch (type) {
    case "RTT": return parseQTT(content)
    case "RGP": return parseQGP(content)
    case "RSN": return parseQSN(content)
    case "RSD": return parseQSD(content)
    case "RUV": return parseRUV(content)
    // ...
  }
}

📊 Status Completo — QTT

O comando mais importante. Retorna todos os dados principais em uma única consulta.

Enviar

>QTT<

Resposta — RTT (formato contíguo)

A resposta RTT é composta por 4 blocos separados por espaços. O primeiro bloco é contíguo (sem espaços internos).

>RTT140521125208-2352502-046678930012673005F0002 04050711 010 00020422042203951454<
     │      │      │        │        │  │  ││ │  │  ││ │    │  │ │  │  │    │    │    │    │
     │      │      │        │        │  │  ││ │  │  ││ │    │  │ │  │  │    │    │    │    └ Alim. principal (×0.01V)
     │      │      │        │        │  │  ││ │  │  ││ │    │  │ │  │  │    │    │    └ Bateria interna (×0.01V)
     │      │      │        │        │  │  ││ │  │  ││ │    │  │ │  │  │    │    └ Analógica 2 (×0.01V)
     │      │      │        │        │  │  ││ │  │  ││ │    │  │ │  │  │    └ Analógica 1 (×0.01V)
     │      │      │        │        │  │  ││ │  │  ││ │    │  │ │  │  └ Analógica 0 (×0.01V)
     │      │      │        │        │  │  ││ │  │  ││ │    │  │ │  └ Saídas (OUT0 OUT1 OUT2)
     │      │      │        │        │  │  ││ │  │  ││ │    │  │ └ Sats céu
     │      │      │        │        │  │  ││ │  │  ││ │    │  └ Sats vistos
     │      │      │        │        │  │  ││ │  │  ││ │    └ Sats usados
     │      │      │        │        │  │  ││ │  │  ││ └ Estado posicionamento GPS (0-7)
     │      │      │        │        │  │  ││ │  │  │└ Antena GPS (0=ok 1=curto 2=desc)
     │      │      │        │        │  │  ││ │  │  └─ [espaço]
     │      │      │        │        │  │  ││ │  └ HDOP (0-50)
     │      │      │        │        │  │  ││ └ Nº evento (decimal)
     │      │      │        │        │  │  │└ Entradas digitais (hex, bit7=IGN)
     │      │      │        │        │  │  └ Idade posição (hex, segundos)
     │      │      │        │        │  └ Estado GPS (0-9)
     │      │      │        │        └ Heading (0-359°)
     │      │      │        └ Velocidade (km/h)
     │      │      └ Longitude (sinal + 3 graus + 5 decimais) ex: -046.67893°
     │      └ Latitude (sinal + 2 graus + 5 decimais) ex: -23.52502°
     └ Data/Hora UTC (DDMMAAhhmmss)

Mapa de Campos (posições fixas)

PosiçãoTam.CampoExemploValor
3–86Data (DDMMAA)14052114/05/2021
9–146Hora (HHMMSS)12520812:52:08 UTC
15–228Latitude-2352502−23.52502°
23–319Longitude-04667893−46.67893°
32–343Velocidade (km/h)0011 km/h
35–373Heading (0–359°)267267° (Oeste)
381Estado GPS33D fix (4+ sats)
39–402Idade posição (hex)000 segundos
41–422Entradas digitais (hex)5F0101 1111
43–442Nº evento000
45–462HDOP02diluição 2
[espaço separador]
481Antena GPS ext.00=Normal
491Posicionamento GPS44=3D fix
50–512Sats usados055 satélites
52–532Sats vistos077 satélites
54–552Sats no céu1111 satélites
[espaço separador]
571Saída OUT00desligada
581Saída OUT11ligada
591Saída OUT20desligada
[espaço separador]
61–644Analógica AD000020.02V
65–684Analógica AD104224.22V
69–724Analógica AD204224.22V
73–764Bateria interna03953.95V
77–804Alimentação principal145414.54V

Estado GPS (campo posição 38)

ValorSignificado
0Posicionando com 1 satélite
1Posicionando com 2 satélites
23 satélites (2D)
34+ satélites (3D) ✅
42D por quadrados mínimos
53D por quadrados mínimos
6Sem satélites (velocidade + tempo)
7Sem movimento (última posição válida)
8Antena em curto
9Antena não conectada

Entradas Digitais (campo hex posição 41–42)

BitEntradaDescrição
7 (MSB)IN07Ignição
6IN06Alimentação principal
5IN05Entrada digital 5
4IN04Entrada digital 4
3IN03Entrada digital 3
2IN02Entrada digital 2
1IN01Entrada digital 1
0 (LSB)IN00Entrada digital 0

Exemplo: 5F = 01011111 → IGN=off, ALIM=on, IN05=off, IN04=on, IN03..IN00=on

Coordenadas — Formato Graus Decimais

⚠️ Atenção: As coordenadas são em graus e decimais, NÃO em graus/minutos. Os 5 últimos dígitos são a parte fracionária dos graus, divididos por 100.000.
CampoFormatoExemploCálculo
Latitude±DDXXXXX-2352502−(23 + 52502÷100000) = −23.52502°
Longitude±DDDXXXXX-04667893−(046 + 67893÷100000) = −46.67893°

Parser RTT — Dart (posicional)

class RTTData {
  final String date, time;
  final double lat, lon;
  final int speed, heading, gpsState, ageSeconds;
  final int inputs, eventNum, hdop;
  final int antennaState, gpsFix, satsUsed, satsSeen, satsSky;
  final int out0, out1, out2;
  final int ad0, ad1, ad2, batteryMv, mainMv;

  RTTData._({required this.date, required this.time, required this.lat,
    required this.lon, required this.speed, required this.heading,
    required this.gpsState, required this.ageSeconds, required this.inputs,
    required this.eventNum, required this.hdop, required this.antennaState,
    required this.gpsFix, required this.satsUsed, required this.satsSeen,
    required this.satsSky, required this.out0, required this.out1,
    required this.out2, required this.ad0, required this.ad1,
    required this.ad2, required this.batteryMv, required this.mainMv});

  /// Tensões em Volts
  double get batteryVolts => batteryMv / 100.0;
  double get mainVolts    => mainMv / 100.0;
  bool   get ignition     => (inputs & 0x80) != 0;
  bool   get powered      => (inputs & 0x40) != 0;
  bool   get hasGpsFix    => gpsState >= 2 && gpsState <= 5;

  static RTTData parse(String raw) {
    String s = raw.replaceAll(RegExp(r'[><]'), '');
    // s = "RTT140521125208-2352502-046678930012673005F0002 04050711 010 0002..."

    return RTTData._(
      date:         s.substring(3, 9),               // "140521" → 14/05/21
      time:         s.substring(9, 15),               // "125208" → 12:52:08
      lat:          _parseCoord(s.substring(15, 23), 2), // "-2352502" → -23.52502
      lon:          _parseCoord(s.substring(23, 32), 3), // "-04667893" → -46.67893
      speed:        int.parse(s.substring(32, 35)),   // "001"
      heading:      int.parse(s.substring(35, 38)),   // "267"
      gpsState:     int.parse(s.substring(38, 39)),   // "3"
      ageSeconds:   int.parse(s.substring(39, 41), radix: 16), // "00" hex
      inputs:       int.parse(s.substring(41, 43), radix: 16), // "5F" hex
      eventNum:     int.parse(s.substring(43, 45)),   // "00"
      hdop:         int.parse(s.substring(45, 47)),   // "02"
      // [espaço pos 47]
      antennaState: int.parse(s.substring(48, 49)),   // "0"
      gpsFix:       int.parse(s.substring(49, 50)),   // "4"
      satsUsed:     int.parse(s.substring(50, 52)),   // "05"
      satsSeen:     int.parse(s.substring(52, 54)),   // "07"
      satsSky:      int.parse(s.substring(54, 56)),   // "11"
      // [espaço pos 56]
      out0:         int.parse(s.substring(57, 58)),   // "0"
      out1:         int.parse(s.substring(58, 59)),   // "1"
      out2:         int.parse(s.substring(59, 60)),   // "0"
      // [espaço pos 60]
      ad0:          int.parse(s.substring(61, 65)),   // "0002"
      ad1:          int.parse(s.substring(65, 69)),   // "0422"
      ad2:          int.parse(s.substring(69, 73)),   // "0422"
      batteryMv:    int.parse(s.substring(73, 77)),   // "0395" → 3.95V
      mainMv:       int.parse(s.substring(77, 81)),   // "1454" → 14.54V
    );
  }

  /// Graus decimais: ±DD(D)XXXXX → grau + XXXXX/100000
  static double _parseCoord(String s, int degDigits) {
    int sign = s.startsWith('-') ? -1 : 1;
    String num = s.replaceAll(RegExp(r'[+-]'), '');
    double deg = double.parse(num.substring(0, degDigits));
    double dec = double.parse(num.substring(degDigits)) / 100000;
    return (deg + dec) * sign;
  }
}

📍 Posição GPS

Comandos Disponíveis

ComandoRespostaConteúdo
>QGP<RGPData, lat, lon, velocidade, heading, GPS, entradas
>QGPD<Mesmo que QGP mas com valores decimais
>QSD<RSDApenas velocidade (km/h)
>QAL<RALAltitude em metros
>QDM<Distância percorrida (km)
>QMS<Velocidade máxima
>QAS<Velocidade média
>QTM<Tempo em movimento (segundos)
>QIM<Hora de início do movimento
>QEM<Hora de fim do movimento

Resposta RGP — Formato Documentado

A resposta QGP é mais compacta que QTT — ideal para consultas rápidas de posição.

>RGP140521125234-2352502-046678970012673005F0015<
     │      │      │        │        │  │  ││ │  ││
     │      │      │        │        │  │  ││ │  │└ HDOP (0-50)
     │      │      │        │        │  │  ││ │  └ Nº evento (decimal)
     │      │      │        │        │  │  ││ └ Entradas digitais (hex)
     │      │      │        │        │  │  │└ Idade posição (hex, segundos)
     │      │      │        │        │  │  └ Estado GPS (0-9)
     │      │      │        │        │  └ Heading (0-359°)
     │      │      │        │        └ Velocidade (km/h)
     │      │      │        └ Longitude (±DDD + 5 decimais)
     │      │      └ Latitude (±DD + 5 decimais)
     └──────┴── Data/Hora (DDMMAAhhmmss)
PosiçãoTam.CampoExemploValor
3–1412Data+Hora (DDMMAAhhmmss)14052112523414/05/21 12:52:34
15–228Latitude-2352502−23.52502°
23–319Longitude-04667897−46.67897°
32–343Velocidade (km/h)0011 km/h
35–373Heading (0–359°)267267°
381Estado GPS33D fix
39–402Idade posição (hex)000 seg
41–422Entradas (hex)5F01011111
43–442Nº evento000
45–462HDOP15diluição 15
RGP vs RTT: RGP não inclui antena GPS, satélites, saídas ou analógicas. Use QGP para posição rápida e QTT para status completo.

Parser RGP — Dart (posicional)

class RGPData {
  final String dateTime;
  final double lat, lon;
  final int speed, heading, gpsState;

  static RGPData parse(String raw) {
    String s = raw.replaceAll(RegExp(r'[><]'), '');
    return RGPData(
      dateTime: s.substring(3, 15),
      lat:     _coord(s.substring(15, 23), 2),
      lon:     _coord(s.substring(23, 32), 3),
      speed:   int.parse(s.substring(32, 35)),
      heading: int.parse(s.substring(35, 38)),
      gpsState: int.parse(s.substring(38, 39)),
    );
  }

  static double _coord(String s, int dg) {
    int sign = s.startsWith('-') ? -1 : 1;
    String n = s.replaceAll(RegExp(r'[+-]'), '');
    return (double.parse(n.substring(0, dg))
          + double.parse(n.substring(dg)) / 100000) * sign;
  }
}

Consulta rápida de velocidade

// Enviar
>QSD<

// Resposta
>RSD0035<    // 35 km/h
// Parser: int.parse(response.substring(5))  → 35

🚗 Dados CAN / OBD2

Os dados CAN do veículo são armazenados em Contadores (CT). Consulte diretamente.

CTs Padrão — Script Newtec

Com o script padrão vl08_conf_newtec + biblioteca CAN, os dados ficam nos CTs:

CTDadoUnidadeComando
CT01RPMrpm>QCT01<
CT02Velocidade CANkm/h>QCT02<
CT03Temp. Água Motor°C>QCT03<
CT04Pedal Acelerador%>QCT04<
CT05Consumo CombustívelL/h ×10>QCT05<
CT06Carga Motor%>QCT06<
CT07Torque%>QCT07<
CT08Odômetro CANkm>QCT08<
CT09Horímetro CANh>QCT09<
CT10Nível Combustível%>QCT10<

Consultar faixa de CTs

// Consultar CT01 a CT10 de uma vez
>QCT001010<

// Resposta:
>RCT01 01500 02 085 03 078 04 032 05 128 06 045 07 088 08 125340 09 4520 10 065<

// Cada par é: índice + valor

OBD2 — VIN do Veículo

// Solicitar VIN
>VOBD_ASK_VIN<

// Aguardar ~2s, depois consultar
>VOBD_VIN<

// Resposta contém o VIN de 17 chars
Dica: Os CTs disponíveis dependem do script CAN carregado no VL08. Consulte >QCT000095< para ver todos os 96 primeiros CTs.
⚠️ Importante: A tabela de CTs acima reflete o script padrão vl08_conf_newtec com biblioteca CAN J1939/OBD2. Veículos diferentes podem ter CTs em posições diferentes (ex: Volvo, Scania, Mercedes têm scripts CAN específicos). Sempre confirme com >QEPE< quais eventos CAN estão configurados.

🔌 Entradas & Saídas

Entradas Digitais

// Consultar
>QIN<

// Resposta
>RIN0000000001000000<
// 8 dígitos: IN00 a IN07 (0=aberto, 1=fechado)
// IN07 = Ignição (mais comum)

Entradas Analógicas — QAD (formato RAD)

// Consultar todas
>QAD<

// Resposta com estrutura posicional:
>RAD00 010100001829 1248329725662566000003632709<
     ││ │            │   │   │   │   │   │   │
     ││ │            │   │   │   │   │   │   └ Alimentação principal (×0.01V)
     ││ │            │   │   │   │   │   └ Bateria interna (×0.01V)
     ││ │            │   │   │   │   └ Reservado (0000)
     ││ │            │   │   │   └ Analógica IN2 (×0.01V)
     ││ │            │   │   └ Analógica IN2 (×0.01V)
     ││ │            │   └ Analógica IN1 (×0.01V)
     ││ │            └ Analógica AD0 (×0.01V)
     ││ └ Data/Hora (DDMMAAhhmmss)
     │└ Nº evento
     └ RAD

// Valores de tensão: 4 dígitos, 2 inteiros + 2 decimais
// Ex: 0363 = 3.63V | 1248 = 12.48V | 2709 = 27.09V

// Consultar analógica específica:
>QAD00<   // AD0 (fio marrom)
>QAD05<   // Bateria interna
>QAD06<   // Alimentação principal

Uso típico no App

EntradaUso ComumInterpretação
IN07Ignição1 = ligada, 0 = desligada
IN00Porta / PânicoDepende da instalação
AD0Sensor analógicoTensão 0-3.3V

📐 Acelerômetro

Leitura 3 eixos

// Consultar
>GMV<

// Resposta
>GMV V±xxxx X±xxxx Y±xxxx Z±xxxx S H<
//     │       │       │       │    │ └ Heading
//     │       │       │       │    └ Speed
//     │       │       │       └ Eixo Z (G×100)
//     │       │       └ Eixo Y (G×100)
//     │       └ Eixo X (G×100)
//     └ Módulo (G×100)

// Exemplo: GMV V+0098 X+0011 Y-0003 Z+0097 035 270
// → 0.98G módulo, eixo Z ~1G (parado na horizontal)
Uso no app: Detecte frenagens bruscas (V > 150 = 1.5G), acelerações fortes, curvas. Valores em repouso: V ≈ 100 (1G da gravidade).

📡 Rede Celular

ComandoDado
>QG2<Status 2G/3G — online, sinal, registro, LAC, Cell ID
>QG4<Status LTE/CAT-M1 — status 4G, banda, registro
>QEN11<IMEI do modem
>QEN12<ICCID do SIM Card
>QVP<Status do SIM: ativo, bandas configuradas
>QISF<APN configurada

🔋 Bateria & Alimentação

ComandoDado
>GCH<Info bateria: modo de carregamento, tensão, corrente, temperatura
>SBM60<Iniciar teste de bateria (60s de descarga)
>QBM<Resultado do teste: tensão inicial, final, diferencial

Tensões via QTT (posições fixas na RTT)

Os campos finais da resposta RTT contêm as tensões (posições 73–80):

  • Pos. 73–76: Bateria interna (×0.01V) → 0395 = 3.95V
  • Pos. 77–80: Alimentação principal (×0.01V) → 1454 = 14.54V

Valores de 0000 a 3000 (0.00V a 30.00V). Bateria de lítio normal: 3.50–4.20V. Bateria veículo: 12.00–14.50V.


📄 Reports Automáticos (RUV)

O VL08 pode ser configurado para enviar reports periódicos automaticamente. Se seu app estiver conectado via BLE com destino BTT, receberá esses reports como notificações.

Formatos de Report Definidos

Reports pré-configurados com estrutura fixa. Usados em eventos com destino TT, GP, AD, etc.

CódigoRespostaConteúdoUso no evento (SED)
NNNulo (sem report)Ação sem envio
TTRTTEstrutura RTT completa (=QTT)SED001 TD01++ +- GF0 TT
GPRGPPosição com data/hora (=QGP)SED001 IN07++ +- GF0 GP
PFRPFDados GPS estendidos (=QPF)Raramente usado
ADRADLeituras analógicas (=QAD)SED002 TD01++ +- GF0 AD

Formatos de Report Configuráveis (RUV)

Reports configuráveis com SUCxx. O conteúdo depende da programação.

CódigoRespostaDescriçãoUso no evento
VxRUVxxReport configurável com nº de eventoSED001 TD01++ +- GF0 V1
UxRUSxxReport configurável sem nº de eventoSED001 TD01++ +- GF0 U1
NxRUNxxNumérico decimal (otimizado LEX)SED001 TD01++ +- GF0 N1
LxRULxxDiferencial numérico (delta LEX)SED001 TD01++ +- GF0 L1

x = 0–15 (slot do report). Exemplo: V1 envia RUV01, U3 envia RUS03.

RUV01: Dados Básicos de Tracking — Campo a Campo

O formato mais usado para tracking periódico. Vem como resposta de V1 nos eventos.

>RUV01100,NT003,240622184546-1926542-046895630000009FFDE0000,04221376,00000,00000,00000,
  1111111111,2222222222,03333,00444,55555,00100,0,0,4G:1,1644991399;ID=0081;#8012;*18<
CampoExemploDescrição
RUV01RUV01Identificador: report configurável 01
Evento100Índice do evento disparador (ver tabela abaixo)
ProtocoloNT003Versão do protocolo
Data240622UTC ddmmyy
Hora184546UTC hhmmss
Latitude-1926542−19.26542° (graus decimais)
Longitude-04689563−46.89563°
Velocidade000km/h (CAN ou GPS)
Heading000Direção 0–359°
GPS State9Estado do GPS (0–9)
IdadeFFSegundos desde posição (hex)
EntradasDEDigitais hex (bit7=IGN)
00Reservado
HDOP00Diluição de precisão
Bat. backup0422Tensão backup (×0.01V) → 4.22V
Bat. veículo1376Tensão principal (×0.01V) → 13.76V
Vel. máx00000Máxima em violação (km/h)
RPM máx00000Máxima RPM em violação
00000Reservado
Horímetro1111111111Minutos de motor ligado
Hodômetro2222222222Distância em metros
RPM03333Rotação do motor
Temp. motor00444°C
Pressão óleo55555kPa
Combustível00100Nível em %
Vel. chuva00=desabilitado, 1=habilitado
Online01=online, 0=offline quando gerado
Rede4G:14G:0=2G, 4G:1=Cat-M1
Motorista1644991399RFID ou iButton ID

Eventos do RUV01

CódigoEvento
100Ignição ligada
101Ignição desligada
102Tracking periódico (IGN on)
103Tracking periódico (IGN off)
104/105Início/fim excesso velocidade
106/107Início/fim velocidade na chuva
109/110Início/fim excesso RPM
111/112Início/fim motor ocioso
113/114Início/fim excesso embreagem
115/116Início/fim banguela
117Login motorista autorizado
118Login motorista não autorizado
119Temperatura alta
120/121/122Aceleração / frenagem / curva brusca
127/128Alimentação desconectada / reconectada
129/130Bateria baixa / normalizada
131/132Entrando / saindo do sleep

Outros Formatos RUV

FormatoConteúdoUso
RUV00Identificação (serial, FW, modelo, ICCID)Instalação / apresentação
RUV02Fim de viagem (faixas RPM, distância, consumo)Relatório de viagem (evento 108)
RUV03Telemetria CAN (acelerador, RPM, temp, torque, freio)Telemetria em tempo real (eventos 150-153)

Configurar Report via BLE

// Evento: enviar RUV01 a cada 10 segundos via BLE
>STD01999999990010<             // TD01 = cada 10s
>SED001 TD01++ +- BTT V1<      // Report V1 via Bluetooth

// No app, o report RUV01 chegará como notificação BLE a cada 10s
// A prioridade de envio BT pode ser:
//   BTT     → Normal (5 tentativas)
//   BTT_LO  → Baixa prioridade (1 tentativa)
//   BTT_HI  → Alta prioridade (retenta até ACK)

⚙️ Enviar Configurações pelo App

O app pode não só ler dados, mas também configurar o VL08 remotamente via BLE.

Exemplos de Configurações

// Configurar APN
>SCF0000,MX2,APN zap.vivo.com.br,SUP2 vivo vivo<

// Configurar IP do servidor
>VSIP0,200.100.50.25.05000.05000,UDP,AUX0<

// Configurar Wi-Fi
>VSWI20,"MinhaRede""MinhaSenha"<

// Programar evento
>SED001 TD01++ +- GF0 V0<

// Limpar configuração
>CLN0000<

// Resetar
>RST<
Cuidado: Comandos de configuração alteram o comportamento do equipamento. Implemente confirmação no app antes de enviar CLN ou RST.

🎯 Exemplo Completo — Flutter / Dart

Usando o pacote flutter_blue_plus.

import 'dart:convert';
import 'dart:async';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';

class VL08BleService {
  static const String SERVICE_UUID = '0000ffff-0000-1000-8000-00805f9b34fb';
  static const String CHAR_UUID    = '0000ff01-0000-1000-8000-00805f9b34fb';

  BluetoothDevice? _device;
  BluetoothCharacteristic? _char;
  StringBuffer _buffer = StringBuffer();
  StreamController<String> _responseStream = StreamController.broadcast();

  Stream<String> get responses => _responseStream.stream;

  /// Scan para dispositivos VL08
  Stream<ScanResult> scan() {
    FlutterBluePlus.startScan(
      withNames: ['VIRTEC_VL8_'],
      timeout: Duration(seconds: 10),
    );
    return FlutterBluePlus.scanResults
        .expand((results) => results)
        .where((r) => r.device.platformName.startsWith('VIRTEC_VL8_'));
  }

  /// Conectar ao dispositivo
  Future<void> connect(BluetoothDevice device) async {
    _device = device;
    await device.connect(autoConnect: false);
    await device.requestMtu(512);

    List<BluetoothService> services = await device.discoverServices();
    BluetoothService svc = services.firstWhere(
      (s) => s.uuid.toString() == SERVICE_UUID,
    );
    _char = svc.characteristics.firstWhere(
      (c) => c.uuid.toString() == CHAR_UUID,
    );

    // Habilitar notificações
    await _char!.setNotifyValue(true);
    _char!.onValueReceived.listen(_onData);
  }

  /// Callback de dados recebidos
  void _onData(List<int> value) {
    String chunk = utf8.decode(value);
    _buffer.write(chunk);

    String current = _buffer.toString();
    // Processar todas as respostas completas no buffer
    while (current.contains('<')) {
      int start = current.indexOf('>');
      int end = current.indexOf('<');
      if (start >= 0 && end > start) {
        String response = current.substring(start, end + 1);
        _responseStream.add(response);
        current = current.substring(end + 1);
      } else {
        break;
      }
    }
    _buffer = StringBuffer(current);
  }

  /// Enviar comando XVM
  Future<void> sendCommand(String cmd) async {
    if (_char == null) throw Exception('Não conectado');
    await _char!.write(utf8.encode(cmd), withoutResponse: false);
  }

  /// Conveniência: enviar e aguardar resposta
  Future<String> query(String cmd, {Duration timeout = const Duration(seconds: 5)}) async {
    Completer<String> completer = Completer();
    late StreamSubscription sub;
    sub = responses.listen((response) {
      if (!completer.isCompleted) {
        completer.complete(response);
        sub.cancel();
      }
    });
    await sendCommand(cmd);
    return completer.future.timeout(timeout);
  }

  /// Consultar status completo
  Future<Map<String, dynamic>> getStatus() async {
    String resp = await query('>QTT<');
    return _parseQTT(resp);
  }

  /// Consultar velocidade
  Future<int> getSpeed() async {
    String resp = await query('>QSD<');
    return int.parse(resp.replaceAll(RegExp(r'[><]'), '').substring(5));
  }

  /// Consultar dados CAN (CT01 a CT10)
  Future<Map<String, int>> getCanData() async {
    String resp = await query('>QCT001010<');
    // Parse response...
    return {};
  }

  Map<String, dynamic> _parseQTT(String raw) {
    String s = raw.replaceAll(RegExp(r'[><]'), '');
    // s = "RTT140521125208-2352502-046678930012673005F0002 04050711 010 00020422042203951454"
    return {
      'date': s.substring(3, 9),
      'time': s.substring(9, 15),
      'latitude': _parseCoord(s.substring(15, 23), 2),
      'longitude': _parseCoord(s.substring(23, 32), 3),
      'speed': int.parse(s.substring(32, 35)),
      'heading': int.parse(s.substring(35, 38)),
      'gpsState': int.parse(s.substring(38, 39)),
      'inputs': int.parse(s.substring(41, 43), radix: 16),
      'ignition': (int.parse(s.substring(41, 43), radix: 16) & 0x80) != 0,
      'batteryV': int.parse(s.substring(73, 77)) / 100.0,
      'mainV': int.parse(s.substring(77, 81)) / 100.0,
    };
  }

  /// Graus decimais: ±DD(D)XXXXX → grau + XXXXX/100000
  double _parseCoord(String s, int degDigits) {
    int sign = s.startsWith('-') ? -1 : 1;
    String num = s.replaceAll(RegExp(r'[+-]'), '');
    double deg = double.parse(num.substring(0, degDigits));
    double dec = double.parse(num.substring(degDigits)) / 100000;
    return (deg + dec) * sign;
  }

  Future<void> disconnect() async {
    await _device?.disconnect();
    _device = null;
    _char = null;
  }
}

🤖 Exemplo — Kotlin (Android)

Usando BLE nativo do Android.

import android.bluetooth.*
import android.bluetooth.le.*
import java.util.UUID

class VL08BleManager(private val context: Context) {
    companion object {
        val SERVICE_UUID = UUID.fromString("0000ffff-0000-1000-8000-00805f9b34fb")
        val CHAR_UUID    = UUID.fromString("0000ff01-0000-1000-8000-00805f9b34fb")
        val CCCD_UUID    = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
    }

    private var gatt: BluetoothGatt? = null
    private var char: BluetoothGattCharacteristic? = null
    private val buffer = StringBuilder()
    var onResponse: ((String) -> Unit)? = null

    // 1. Scan
    fun startScan(onFound: (BluetoothDevice) -> Unit) {
        val scanner = BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner
        scanner.startScan(object : ScanCallback() {
            override fun onScanResult(callbackType: Int, result: ScanResult) {
                val name = result.device.name ?: return
                if (name.startsWith("VIRTEC_VL8_")) {
                    onFound(result.device)
                }
            }
        })
    }

    // 2. Connect
    fun connect(device: BluetoothDevice) {
        gatt = device.connectGatt(context, false, gattCallback)
    }

    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(g: BluetoothGatt, status: Int, state: Int) {
            if (state == BluetoothProfile.STATE_CONNECTED) {
                g.requestMtu(512)
            }
        }

        override fun onMtuChanged(g: BluetoothGatt, mtu: Int, status: Int) {
            g.discoverServices()
        }

        override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
            val svc = g.getService(SERVICE_UUID)
            char = svc?.getCharacteristic(CHAR_UUID)
            char?.let {
                g.setCharacteristicNotification(it, true)
                val desc = it.getDescriptor(CCCD_UUID)
                desc.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
                g.writeDescriptor(desc)
            }
        }

        override fun onCharacteristicChanged(g: BluetoothGatt, c: BluetoothGattCharacteristic) {
            val chunk = String(c.value)
            buffer.append(chunk)
            val str = buffer.toString()
            if (str.contains("<")) {
                val start = str.indexOf(">")
                val end = str.indexOf("<")
                if (start >= 0 && end > start) {
                    val response = str.substring(start, end + 1)
                    onResponse?.invoke(response)
                    buffer.clear()
                    buffer.append(str.substring(end + 1))
                }
            }
        }
    }

    // 3. Send command
    fun send(command: String) {
        char?.let {
            it.value = command.toByteArray()
            gatt?.writeCharacteristic(it)
        }
    }

    // 4. Query helpers
    fun queryStatus() = send(">QTT<")
    fun querySpeed()  = send(">QSD<")
    fun queryGPS()    = send(">QGP<")
    fun queryCAN()    = send(">QCT001010<")
    fun queryInputs() = send(">QIN<")

    fun disconnect() {
        gatt?.disconnect()
        gatt?.close()
    }
}

🍎 Exemplo — Swift (iOS)

Usando CoreBluetooth.

import CoreBluetooth

class VL08BleManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
    static let serviceUUID = CBUUID(string: "0000FFFF-0000-1000-8000-00805F9B34FB")
    static let charUUID    = CBUUID(string: "0000FF01-0000-1000-8000-00805F9B34FB")

    private var central: CBCentralManager!
    private var peripheral: CBPeripheral?
    private var characteristic: CBCharacteristic?
    private var buffer = ""

    var onDeviceFound: ((CBPeripheral) -> Void)?
    var onResponse: ((String) -> Void)?

    override init() {
        super.init()
        central = CBCentralManager(delegate: self, queue: nil)
    }

    // 1. Scan
    func startScan() {
        central.scanForPeripherals(
            withServices: [VL08BleManager.serviceUUID],
            options: nil
        )
    }

    func centralManager(_ central: CBCentralManager,
                         didDiscover peripheral: CBPeripheral,
                         advertisementData: [String: Any],
                         rssi: NSNumber) {
        if let name = peripheral.name, name.hasPrefix("VIRTEC_VL8_") {
            onDeviceFound?(peripheral)
        }
    }

    // 2. Connect
    func connect(_ device: CBPeripheral) {
        peripheral = device
        peripheral?.delegate = self
        central.connect(device, options: nil)
    }

    func centralManager(_ central: CBCentralManager,
                         didConnect peripheral: CBPeripheral) {
        peripheral.discoverServices([VL08BleManager.serviceUUID])
    }

    func peripheral(_ peripheral: CBPeripheral,
                     didDiscoverServices error: Error?) {
        guard let svc = peripheral.services?.first(where: {
            $0.uuid == VL08BleManager.serviceUUID
        }) else { return }
        peripheral.discoverCharacteristics([VL08BleManager.charUUID], for: svc)
    }

    func peripheral(_ peripheral: CBPeripheral,
                     didDiscoverCharacteristicsFor service: CBService,
                     error: Error?) {
        guard let char = service.characteristics?.first(where: {
            $0.uuid == VL08BleManager.charUUID
        }) else { return }
        characteristic = char
        peripheral.setNotifyValue(true, for: char)
    }

    // 3. Receive
    func peripheral(_ peripheral: CBPeripheral,
                     didUpdateValueFor characteristic: CBCharacteristic,
                     error: Error?) {
        guard let data = characteristic.value,
              let chunk = String(data: data, encoding: .utf8) else { return }
        buffer += chunk
        while let start = buffer.range(of: ">"),
              let end = buffer.range(of: "<", range: start.upperBound..<buffer.endIndex) {
            let response = String(buffer[start.lowerBound...end.lowerBound])
            onResponse?(response)
            buffer = String(buffer[end.upperBound...])
        }
    }

    // 4. Send
    func send(_ command: String) {
        guard let char = characteristic,
              let data = command.data(using: .utf8) else { return }
        peripheral?.writeValue(data, for: char, type: .withResponse)
    }

    func queryStatus() { send(">QTT<") }
    func queryGPS()    { send(">QGP<") }
    func querySpeed()  { send(">QSD<") }
    func queryCAN()    { send(">QCT001010<") }

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn { print("BLE ready") }
    }
}

💡 Dicas & Troubleshooting

Boas Práticas

DicaDetalhe
Negocie MTU altoUse requestMtu(512) logo após conectar. O padrão de 20 bytes fragmenta muito.
Buffer de recepçãoSempre acumule dados até encontrar <. Nunca processe pacotes BLE individuais.
TimeoutSe não receber resposta em 5s, reenvie o comando. O VL08 pode estar ocupado processando CAN.
Polling intervalIntervalo mínimo entre comandos: 200ms. Para polling periódico (QTT), 1–5s é ideal. Enviar mais rápido pode travar o módulo BLE.
Múltiplos comandosUse >QGP<>QIN<>QAD< para consultar vários dados em um envio (máx 180 chars). Cada comando gera sua própria resposta — acumule no buffer.
Reports automáticosConfigure eventos com destino BTT / BTT_HI / BTT_LO para receber dados sem polling.
CodificaçãoO protocolo XVM é ASCII puro (7-bit). UTF-8 funciona pois é compatível com ASCII. Nunca use codificação UTF-16.
Fila de comandosImplemente uma fila (queue) — envie o próximo comando apenas após receber a resposta do anterior ou timeout de 5s.

Problemas Comuns

ProblemaCausaSolução
Não encontra dispositivo no scanBLE desligado ou nome diferenteVerificar >QBN< no terminal serial; confirmar VIRTEC_VL8_
Conecta mas não recebe dadosNotificações não habilitadasEscrever 0x0100 no descriptor CCCD (0x2902)
Resposta truncadaMTU baixo (20 bytes)requestMtu(512) — aguardar callback antes de enviar
Resposta vem em pedaçosNormal com BLEImplementar buffer; processar apenas ao encontrar <
Múltiplas respostas coladasEnvio de múltiplos comandos (>QGP<>QIN<)O buffer pode conter >RGP...<>RIN...<. Parse com loop: extraia cada par >...< separadamente.
Timeout em QTTEquipamento em sleepEnviar >QSN< primeiro para acordar, depois QTT
CTs retornam 0Script CAN não carregadoVerificar com >QEPE< se há eventos CAN programados
Desconexão frequenteAlcance, interferênciaManter distância <10m; evitar paredes metálicas

Referência Rápida — Todos os Comandos úteis para App

ComandoDescriçãoPrioridade
>QTT<Status completo (GPS, vel, IO, tensão)⭐⭐⭐
>QGP<Posição GPS compacta⭐⭐⭐
>QSD<Velocidade⭐⭐
>QIN<Entradas digitais (ignição)⭐⭐
>QCTxxx<Contadores (dados CAN)⭐⭐
>GMV<Acelerômetro 3 eixos⭐⭐
>QAD<Entradas analógicas
>GCH<Bateria
>QSN<Número serial
>QVR<Versão firmware
>QG2< / >QG4<Status rede celular

VIRLOC VL08 — Guia de Desenvolvimento de App

Fonte: wiki.newtectelemetria.com.br  |  Atualizado: Fev 2026