📱 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.
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
Métodos de Conexão
| Método | Módulo | Pareamento | Alcance | Melhor para |
|---|---|---|---|---|
| BLE ⭐ | FC41D (BLE 5.2) | Não requer | ~30m | Apps mobile, baixo consumo |
| BT Classic | BX3105 (BT 4.2) | Sim (sem PIN) | ~10m | Terminal serial, SPP |
| Wi-Fi | FC41D (2.4GHz) | Mesma rede | Rede local | Monitoramento em rede fixa |
📶 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
| Elemento | UUID 16-bit | UUID 128-bit |
|---|---|---|
| Service | 0xFFFF | 0000ffff-0000-1000-8000-00805f9b34fb |
| Characteristic | 0xFF01 | 0000ff01-0000-1000-8000-00805f9b34fb |
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
BLUETOOTH_SCAN, BLUETOOTH_CONNECT e ACCESS_FINE_LOCATION (Android 12+). No iOS, é necessário declarar NSBluetoothAlwaysUsageDescription no Info.plist.
🔗 BLE — Conectar & GATT
Passo a passo detalhado
- Conectar ao peripheral pelo endereço MAC (Android) ou UUID (iOS)
- Descobrir serviços — chamar
discoverServices() - Encontrar serviço com UUID
0000ffff-0000-1000-8000-00805f9b34fb - Encontrar characteristic
0000ff01-0000-1000-8000-00805f9b34fb - Habilitar notificações — escrever
0x0100no descriptor CCCD (0x2902) - Pronto — agora pode enviar comandos (Write) e receber respostas (Notify)
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);
"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();
}
}
<.
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ário | Estratégia |
|---|---|
| Desconexão inesperada | Tentar reconectar com backoff exponencial: 1s → 2s → 4s → 8s → 16s (máx. 5 tentativas) |
| Erro na descoberta de serviços | Desconectar, aguardar 2s, reconectar |
| Timeout no scan | Parar scan após 10s. Informar usuário. Permitir retry manual. |
| Saída de alcance | Detectar via RSSI < -90dBm. Avisar usuário antes. |
| VL08 em sleep | Aguardar 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ção | Intervalo Mínimo | Notas |
|---|---|---|
| Entre comandos individuais | 200ms | O FC41D precisa processar cada resposta |
| Polling periódico (QTT) | 1–5s | Não há necessidade de polling mais rápido |
| Após erro/timeout | 1s | Dar tempo para o módulo se recuperar |
| Comandos múltiplos aglutinados | N/A | Use >QGP<>QIN< em vez de envios separados |
🔵 Bluetooth Classic — SPP
Para VL08 com módulo BX3105 (BT v4.2). Requer pareamento prévio (sem PIN).
Conexão SPP
| Parâmetro | Valor |
|---|---|
| Perfil | SPP (Serial Port Profile) |
| UUID SPP | 00001101-0000-1000-8000-00805f9b34fb |
| Pareamento | Obrigatório (sem PIN) |
| Baudrate equivalente | 115200 |
Fluxo de Comunicação
- Parear com o VL08 (dispositivo aparece como
VIRTEC_VL8_XXXX) - Abrir socket RFCOMM com UUID SPP
- Enviar comandos XVM como string ASCII via OutputStream
- 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 '<'
🛜 Wi-Fi — Socket TCP/UDP
O VL08 conecta como cliente a redes Wi-Fi 2.4GHz previamente cadastradas e envia dados para IPs configurados.
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
| Trigger | Evento |
|---|---|
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
>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 *).
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:
| Prefixo | Tipo | Exemplo |
|---|---|---|
RTT | Resposta de QTT | >RTT140521125208-2352502-04667893001267300... |
RGP | Resposta de QGP | >RGP140521125234-2352502-04667897001267300... |
RSN | Resposta de QSN | >RSN44K00077_VL10< |
RVR | Resposta de QVR | >RVR VIRTUALTEC VIRLOC8 BR061...< |
RSD | Resposta de QSD | >RSD0035< |
RCT | Resposta de QCT | >RCT01 01500< |
RAD | Resposta de QAD | >RAD00 010100001829 12483297...< |
RIN | Resposta de QIN | >RIN10000000< |
RUV | Report automático | >RUV01100,NT003,240622...< |
ACK | Confirmaçã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ção | Tam. | Campo | Exemplo | Valor |
|---|---|---|---|---|
| 3–8 | 6 | Data (DDMMAA) | 140521 | 14/05/2021 |
| 9–14 | 6 | Hora (HHMMSS) | 125208 | 12:52:08 UTC |
| 15–22 | 8 | Latitude | -2352502 | −23.52502° |
| 23–31 | 9 | Longitude | -04667893 | −46.67893° |
| 32–34 | 3 | Velocidade (km/h) | 001 | 1 km/h |
| 35–37 | 3 | Heading (0–359°) | 267 | 267° (Oeste) |
| 38 | 1 | Estado GPS | 3 | 3D fix (4+ sats) |
| 39–40 | 2 | Idade posição (hex) | 00 | 0 segundos |
| 41–42 | 2 | Entradas digitais (hex) | 5F | 0101 1111 |
| 43–44 | 2 | Nº evento | 00 | 0 |
| 45–46 | 2 | HDOP | 02 | diluição 2 |
| [espaço separador] | ||||
| 48 | 1 | Antena GPS ext. | 0 | 0=Normal |
| 49 | 1 | Posicionamento GPS | 4 | 4=3D fix |
| 50–51 | 2 | Sats usados | 05 | 5 satélites |
| 52–53 | 2 | Sats vistos | 07 | 7 satélites |
| 54–55 | 2 | Sats no céu | 11 | 11 satélites |
| [espaço separador] | ||||
| 57 | 1 | Saída OUT0 | 0 | desligada |
| 58 | 1 | Saída OUT1 | 1 | ligada |
| 59 | 1 | Saída OUT2 | 0 | desligada |
| [espaço separador] | ||||
| 61–64 | 4 | Analógica AD0 | 0002 | 0.02V |
| 65–68 | 4 | Analógica AD1 | 0422 | 4.22V |
| 69–72 | 4 | Analógica AD2 | 0422 | 4.22V |
| 73–76 | 4 | Bateria interna | 0395 | 3.95V |
| 77–80 | 4 | Alimentação principal | 1454 | 14.54V |
Estado GPS (campo posição 38)
| Valor | Significado |
|---|---|
0 | Posicionando com 1 satélite |
1 | Posicionando com 2 satélites |
2 | 3 satélites (2D) |
3 | 4+ satélites (3D) ✅ |
4 | 2D por quadrados mínimos |
5 | 3D por quadrados mínimos |
6 | Sem satélites (velocidade + tempo) |
7 | Sem movimento (última posição válida) |
8 | Antena em curto |
9 | Antena não conectada |
Entradas Digitais (campo hex posição 41–42)
| Bit | Entrada | Descrição |
|---|---|---|
| 7 (MSB) | IN07 | Ignição |
| 6 | IN06 | Alimentação principal |
| 5 | IN05 | Entrada digital 5 |
| 4 | IN04 | Entrada digital 4 |
| 3 | IN03 | Entrada digital 3 |
| 2 | IN02 | Entrada digital 2 |
| 1 | IN01 | Entrada digital 1 |
| 0 (LSB) | IN00 | Entrada digital 0 |
Exemplo: 5F = 01011111 → IGN=off, ALIM=on, IN05=off, IN04=on, IN03..IN00=on
Coordenadas — Formato Graus Decimais
| Campo | Formato | Exemplo | Cá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
| Comando | Resposta | Conteúdo |
|---|---|---|
>QGP< | RGP | Data, lat, lon, velocidade, heading, GPS, entradas |
>QGPD< | — | Mesmo que QGP mas com valores decimais |
>QSD< | RSD | Apenas velocidade (km/h) |
>QAL< | RAL | Altitude 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ção | Tam. | Campo | Exemplo | Valor |
|---|---|---|---|---|
| 3–14 | 12 | Data+Hora (DDMMAAhhmmss) | 140521125234 | 14/05/21 12:52:34 |
| 15–22 | 8 | Latitude | -2352502 | −23.52502° |
| 23–31 | 9 | Longitude | -04667897 | −46.67897° |
| 32–34 | 3 | Velocidade (km/h) | 001 | 1 km/h |
| 35–37 | 3 | Heading (0–359°) | 267 | 267° |
| 38 | 1 | Estado GPS | 3 | 3D fix |
| 39–40 | 2 | Idade posição (hex) | 00 | 0 seg |
| 41–42 | 2 | Entradas (hex) | 5F | 01011111 |
| 43–44 | 2 | Nº evento | 00 | 0 |
| 45–46 | 2 | HDOP | 15 | diluição 15 |
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:
| CT | Dado | Unidade | Comando |
|---|---|---|---|
| CT01 | RPM | rpm | >QCT01< |
| CT02 | Velocidade CAN | km/h | >QCT02< |
| CT03 | Temp. Água Motor | °C | >QCT03< |
| CT04 | Pedal Acelerador | % | >QCT04< |
| CT05 | Consumo Combustível | L/h ×10 | >QCT05< |
| CT06 | Carga Motor | % | >QCT06< |
| CT07 | Torque | % | >QCT07< |
| CT08 | Odômetro CAN | km | >QCT08< |
| CT09 | Horímetro CAN | h | >QCT09< |
| CT10 | Ní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
>QCT000095< para ver todos os 96 primeiros CTs.
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
| Entrada | Uso Comum | Interpretação |
|---|---|---|
IN07 | Ignição | 1 = ligada, 0 = desligada |
IN00 | Porta / Pânico | Depende da instalação |
AD0 | Sensor analógico | Tensã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)
V > 150 = 1.5G), acelerações fortes, curvas. Valores em repouso: V ≈ 100 (1G da gravidade).
📡 Rede Celular
| Comando | Dado |
|---|---|
>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
| Comando | Dado |
|---|---|
>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ódigo | Resposta | Conteúdo | Uso no evento (SED) |
|---|---|---|---|
NN | — | Nulo (sem report) | Ação sem envio |
TT | RTT | Estrutura RTT completa (=QTT) | SED001 TD01++ +- GF0 TT |
GP | RGP | Posição com data/hora (=QGP) | SED001 IN07++ +- GF0 GP |
PF | RPF | Dados GPS estendidos (=QPF) | Raramente usado |
AD | RAD | Leituras 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ódigo | Resposta | Descrição | Uso no evento |
|---|---|---|---|
Vx | RUVxx | Report configurável com nº de evento | SED001 TD01++ +- GF0 V1 |
Ux | RUSxx | Report configurável sem nº de evento | SED001 TD01++ +- GF0 U1 |
Nx | RUNxx | Numérico decimal (otimizado LEX) | SED001 TD01++ +- GF0 N1 |
Lx | RULxx | Diferencial 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<
| Campo | Exemplo | Descrição |
|---|---|---|
| RUV01 | RUV01 | Identificador: report configurável 01 |
| Evento | 100 | Índice do evento disparador (ver tabela abaixo) |
| Protocolo | NT003 | Versão do protocolo |
| Data | 240622 | UTC ddmmyy |
| Hora | 184546 | UTC hhmmss |
| Latitude | -1926542 | −19.26542° (graus decimais) |
| Longitude | -04689563 | −46.89563° |
| Velocidade | 000 | km/h (CAN ou GPS) |
| Heading | 000 | Direção 0–359° |
| GPS State | 9 | Estado do GPS (0–9) |
| Idade | FF | Segundos desde posição (hex) |
| Entradas | DE | Digitais hex (bit7=IGN) |
| — | 00 | Reservado |
| HDOP | 00 | Diluição de precisão |
| Bat. backup | 0422 | Tensão backup (×0.01V) → 4.22V |
| Bat. veículo | 1376 | Tensão principal (×0.01V) → 13.76V |
| Vel. máx | 00000 | Máxima em violação (km/h) |
| RPM máx | 00000 | Máxima RPM em violação |
| — | 00000 | Reservado |
| Horímetro | 1111111111 | Minutos de motor ligado |
| Hodômetro | 2222222222 | Distância em metros |
| RPM | 03333 | Rotação do motor |
| Temp. motor | 00444 | °C |
| Pressão óleo | 55555 | kPa |
| Combustível | 00100 | Nível em % |
| Vel. chuva | 0 | 0=desabilitado, 1=habilitado |
| Online | 0 | 1=online, 0=offline quando gerado |
| Rede | 4G:1 | 4G:0=2G, 4G:1=Cat-M1 |
| Motorista | 1644991399 | RFID ou iButton ID |
Eventos do RUV01
| Código | Evento |
|---|---|
100 | Ignição ligada |
101 | Ignição desligada |
102 | Tracking periódico (IGN on) |
103 | Tracking periódico (IGN off) |
104/105 | Início/fim excesso velocidade |
106/107 | Início/fim velocidade na chuva |
109/110 | Início/fim excesso RPM |
111/112 | Início/fim motor ocioso |
113/114 | Início/fim excesso embreagem |
115/116 | Início/fim banguela |
117 | Login motorista autorizado |
118 | Login motorista não autorizado |
119 | Temperatura alta |
120/121/122 | Aceleração / frenagem / curva brusca |
127/128 | Alimentação desconectada / reconectada |
129/130 | Bateria baixa / normalizada |
131/132 | Entrando / saindo do sleep |
Outros Formatos RUV
| Formato | Conteúdo | Uso |
|---|---|---|
RUV00 | Identificação (serial, FW, modelo, ICCID) | Instalação / apresentação |
RUV02 | Fim de viagem (faixas RPM, distância, consumo) | Relatório de viagem (evento 108) |
RUV03 | Telemetria 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<
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
| Dica | Detalhe |
|---|---|
| Negocie MTU alto | Use requestMtu(512) logo após conectar. O padrão de 20 bytes fragmenta muito. |
| Buffer de recepção | Sempre acumule dados até encontrar <. Nunca processe pacotes BLE individuais. |
| Timeout | Se não receber resposta em 5s, reenvie o comando. O VL08 pode estar ocupado processando CAN. |
| Polling interval | Intervalo 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 comandos | Use >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áticos | Configure eventos com destino BTT / BTT_HI / BTT_LO para receber dados sem polling. |
| Codificação | O protocolo XVM é ASCII puro (7-bit). UTF-8 funciona pois é compatível com ASCII. Nunca use codificação UTF-16. |
| Fila de comandos | Implemente uma fila (queue) — envie o próximo comando apenas após receber a resposta do anterior ou timeout de 5s. |
Problemas Comuns
| Problema | Causa | Solução |
|---|---|---|
| Não encontra dispositivo no scan | BLE desligado ou nome diferente | Verificar >QBN< no terminal serial; confirmar VIRTEC_VL8_ |
| Conecta mas não recebe dados | Notificações não habilitadas | Escrever 0x0100 no descriptor CCCD (0x2902) |
| Resposta truncada | MTU baixo (20 bytes) | requestMtu(512) — aguardar callback antes de enviar |
| Resposta vem em pedaços | Normal com BLE | Implementar buffer; processar apenas ao encontrar < |
| Múltiplas respostas coladas | Envio de múltiplos comandos (>QGP<>QIN<) | O buffer pode conter >RGP...<>RIN...<. Parse com loop: extraia cada par >...< separadamente. |
| Timeout em QTT | Equipamento em sleep | Enviar >QSN< primeiro para acordar, depois QTT |
| CTs retornam 0 | Script CAN não carregado | Verificar com >QEPE< se há eventos CAN programados |
| Desconexão frequente | Alcance, interferência | Manter distância <10m; evitar paredes metálicas |
Referência Rápida — Todos os Comandos úteis para App
| Comando | Descrição | Prioridade |
|---|---|---|
>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