Objetivo do módulo
Registrar no ERP o caminho percorrido pelo operador/trator durante a aplicação. O app deve coletar latitude, longitude, data, hora e dados complementares do GPS a cada intervalo definido, normalmente a cada 3 segundos.
Como o uso acontece no campo, o app não deve depender de internet durante a aplicação. Os pontos devem ser salvos no banco local do celular e enviados posteriormente para a API em um único pacote.
Fluxo esperado no app
- O usuário realiza os procedimentos anteriores da aplicação no app.
- O app obtém ou mantém o
id_aplicacaoda aplicação em andamento. - Quando o operador estiver posicionado, ele clica em Iniciar trajeto.
- O app gera um
client_event_idúnico para aquele trajeto. - O app começa a gravar pontos de localização no banco local, normalmente a cada 3 segundos.
- Quando o operador finalizar a aplicação, o app marca o trajeto local como finalizado.
- Quando houver internet, o app envia o trajeto completo para
f=receber_trajeto_aplicacao. - Se a API retornar
success=true, o app marca o trajeto local como sincronizado. - Se retornar
already_registered=true, o app também deve considerar como sincronizado.
Uso do client_event_id no trajeto
O client_event_id é o identificador único gerado pelo app para representar um evento
operacional offline. Como o trajeto também é um evento operacional, ele deve seguir o mesmo padrão
já definido no modo offline do aplicativo.
Portanto, o trajeto não usa client_trajeto_id. O campo correto é
client_event_id.
client_event_id=550e8400-e29b-41d4-a716-446655440000
Esse valor não vem do ERP. Ele nasce no app no momento em que o usuário inicia o trajeto.
Em todos os reenvios do mesmo trajeto, o app deve reutilizar exatamente o mesmo
client_event_id.
Regra de idempotência
- 1 trajeto local = 1
client_event_id. - O mesmo trajeto reenviado deve manter o mesmo
client_event_id. - Eventos diferentes nunca devem reutilizar o mesmo
client_event_id. - O ERP usa esse campo para evitar duplicidade.
client_event_id no trajeto para seguir o padrão offline do app.
Diferença entre client_event_id e client_point_id
O trajeto é enviado em lote, contendo vários pontos de localização. Por isso existem dois níveis de identificação:
| Campo | Onde aparece | Função |
|---|---|---|
client_event_id |
No cabeçalho do envio do trajeto | Identifica o trajeto completo. Evita duplicar o trajeto se o app reenviar o pacote. |
client_point_id |
Em cada item do array pontos |
Identifica cada ponto do trajeto. Evita duplicar pontos em reprocessamentos. |
{
"client_event_id": "550e8400-e29b-41d4-a716-446655440000",
"id_aplicacao": 123,
"pontos": [
{
"client_point_id": "pt_001",
"latitude": -21.8588800,
"longitude": -47.4845167,
"ordem": 1
},
{
"client_point_id": "pt_002",
"latitude": -21.8588900,
"longitude": -47.4845200,
"ordem": 2
}
]
}
Endpoint
POST
https://agroecologia.grupoekos.com.br/router/action.php
Headers obrigatórios
X-APP-KEY: <APP_KEY_FIXA_DO_APP>
X-DEVICE-TOKEN: <DEVICE_TOKEN_DO_APARELHO>
Body
action=apiG10
f=receber_trajeto_aplicacao
id_aplicacao=<ID_DA_APLICACAO>
client_event_id=<UUID_LOCAL_DO_EVENTO_TRAJETO>
amostragem_segundos=3
observacao=<OPCIONAL>
pontos=<JSON_COM_OS_PONTOS>
multipart/form-data ou
application/x-www-form-urlencoded.
Por que enviar os pontos em JSON?
O aplicativo coleta as coordenadas do trajeto a cada 3 segundos e salva tudo no banco offline local. Como o uso acontece em campo, o app não deve depender de internet durante a aplicação.
Por esse motivo, o endpoint recebe o campo pontos como um JSON contendo todos os pontos
coletados no trajeto. Esse formato permite que o app envie os dados em lote, em uma única
requisição, quando houver conexão disponível.
Por que não enviar ponto por ponto?
Se o app enviasse cada ponto individualmente para a API, haveria muitas requisições HTTP. Em um trajeto de 30 minutos, coletando a cada 3 segundos, seriam aproximadamente:
30 minutos = 1.800 segundos
1.800 / 3 = 600 pontos
Enviar 600 requisições separadas aumentaria o tempo de sincronização, consumiria mais bateria e internet, além de aumentar o risco de falha no meio do processo. Se a conexão caísse durante o envio, parte dos pontos poderia chegar ao ERP e outra parte não, exigindo controles adicionais para evitar duplicidade ou perda de dados.
Vantagem do envio em lote
- O app coleta tudo offline, sem depender de sinal no campo.
- O app envia todos os pontos em uma única requisição quando houver internet.
- O ERP recebe o trajeto completo de uma vez.
- O controle de duplicidade fica mais simples usando
client_event_ideclient_point_id. - Reduz o risco de sincronização parcial.
- Reduz o número de chamadas entre app e API.
Formato esperado
pontos=<JSON_COM_OS_PONTOS>
O valor de pontos deve ser um array JSON. Cada item representa uma coordenada coletada
pelo app durante o trajeto.
[
{
"client_point_id": "pt_001",
"data": "2026-04-27",
"hora": "08:56:00",
"latitude": -21.8588800,
"longitude": -47.4845167,
"accuracy": 4.5,
"speed": null,
"heading": null,
"ordem": 1
},
{
"client_point_id": "pt_002",
"data": "2026-04-27",
"hora": "08:56:03",
"latitude": -21.8588900,
"longitude": -47.4845200,
"accuracy": 4.3,
"speed": null,
"heading": null,
"ordem": 2
}
]
Campos enviados pelo app
| Campo | Obrigatório | Exemplo | Descrição |
|---|---|---|---|
id_aplicacao |
Sim | 123 |
ID da aplicação no ERP. O trajeto sempre deve ficar vinculado a este registro. |
client_event_id |
Sim | 550e8400-e29b-41d4-a716-446655440000 |
ID único local gerado pelo app para o evento do trajeto. Deve ser reutilizado em reenvios do mesmo trajeto para evitar duplicidade no ERP. |
amostragem_segundos |
Não | 3 |
Intervalo esperado entre os pontos coletados. Se não enviado, o ERP considera 3 segundos. |
observacao |
Não | Aplicação realizada no talhão 1. |
Texto livre opcional enviado pelo app. |
pontos |
Sim | [{...}] |
JSON contendo a lista de coordenadas coletadas durante o trajeto. |
Campos de cada ponto do trajeto
| Campo | Obrigatório | Exemplo | Descrição |
|---|---|---|---|
client_point_id |
Sim | pt_001 |
ID único do ponto dentro do trajeto. Evita duplicidade em reprocessamentos. |
data |
Sim | 2026-04-27 |
Data do ponto no formato YYYY-MM-DD. |
hora |
Sim | 08:56:03 |
Hora do ponto no formato HH:MM:SS. |
latitude |
Sim | -21.8588800 |
Latitude coletada pelo GPS. |
longitude |
Sim | -47.4845167 |
Longitude coletada pelo GPS. |
ordem |
Sim | 1 |
Ordem sequencial do ponto no trajeto. |
accuracy |
Não | 4.5 |
Precisão estimada da localização em metros, quando o aparelho/biblioteca disponibilizar. Campo recomendado para auditoria, mas não obrigatório. |
speed |
Não | 2.1 |
Velocidade estimada pelo GPS, quando disponível. Pode vir null,
0 ou imprecisa, principalmente em baixa velocidade.
|
heading |
Não | 180 |
Direção/rumo estimado pelo GPS, quando disponível. Deve ser usado apenas como informação complementar, pois pode ser impreciso em baixa velocidade ou com sinal ruim. |
Campos opcionais do GPS: accuracy, speed e heading
O aplicativo será executado em um aparelho celular, usando a localização do próprio dispositivo.
Em React Native com Expo, é possível usar a biblioteca expo-location para capturar
latitude, longitude e informações complementares do GPS.
O objeto retornado pela localização possui coords, onde podem vir campos como
latitude, longitude, accuracy, speed e
heading, dependendo do aparelho, sistema operacional, permissão concedida e qualidade
do sinal. Por isso, esses campos devem ser tratados como opcionais.
Campos obrigatórios para o trajeto
Para o ERP desenhar o trajeto e auditar a aplicação, os campos realmente obrigatórios em cada ponto são:
data
hora
latitude
longitude
ordem
client_point_id
Exemplo React Native com Expo Location
O exemplo abaixo mostra como o app pode capturar a localização do aparelho e montar os pontos
que serão enviados posteriormente para o endpoint receber_trajeto_aplicacao.
A coleta pode ser feita a cada 3 segundos.
Instalação
npx expo install expo-location
Exemplo TypeScript
import * as Location from 'expo-location';
type TrajetoPonto = {
client_point_id: string;
data: string;
hora: string;
latitude: number;
longitude: number;
accuracy: number | null;
speed: number | null;
heading: number | null;
ordem: number;
};
let ordemAtual = 0;
let pontos: TrajetoPonto[] = [];
function formatarDataHora(timestamp: number) {
const dt = new Date(timestamp);
const yyyy = dt.getFullYear();
const mm = String(dt.getMonth() + 1).padStart(2, '0');
const dd = String(dt.getDate()).padStart(2, '0');
const hh = String(dt.getHours()).padStart(2, '0');
const mi = String(dt.getMinutes()).padStart(2, '0');
const ss = String(dt.getSeconds()).padStart(2, '0');
return {
data: `${yyyy}-${mm}-${dd}`,
hora: `${hh}:${mi}:${ss}`,
};
}
export async function iniciarColetaTrajeto() {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
throw new Error('Permissão de localização não concedida.');
}
const subscription = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
timeInterval: 3000,
distanceInterval: 0,
},
(location) => {
ordemAtual += 1;
const { data, hora } = formatarDataHora(location.timestamp);
const ponto: TrajetoPonto = {
client_point_id: `pt_${ordemAtual}`,
data,
hora,
latitude: location.coords.latitude,
longitude: location.coords.longitude,
accuracy: location.coords.accuracy ?? null,
speed: location.coords.speed ?? null,
heading: location.coords.heading ?? null,
ordem: ordemAtual,
};
pontos.push(ponto);
// Aqui o app deve salvar o ponto no banco offline local.
console.log('Ponto coletado:', ponto);
}
);
return subscription;
}
Exemplo de ponto gerado
{
"client_point_id": "pt_1",
"data": "2026-04-27",
"hora": "08:56:00",
"latitude": -21.85888,
"longitude": -47.4845167,
"accuracy": 4.5,
"speed": null,
"heading": null,
"ordem": 1
}
Exemplo de payload
O campo pontos deve ser enviado como string JSON no corpo do POST.
action=apiG10
f=receber_trajeto_aplicacao
id_aplicacao=123
client_event_id=550e8400-e29b-41d4-a716-446655440000
amostragem_segundos=3
observacao=Aplicação realizada no talhão 1
pontos=[
{
"client_point_id": "pt_001",
"data": "2026-04-27",
"hora": "08:56:00",
"latitude": -21.8588800,
"longitude": -47.4845167,
"accuracy": 4.5,
"speed": null,
"heading": null,
"ordem": 1
},
{
"client_point_id": "pt_002",
"data": "2026-04-27",
"hora": "08:56:03",
"latitude": -21.8588900,
"longitude": -47.4845300,
"accuracy": 4.3,
"speed": null,
"heading": null,
"ordem": 2
},
{
"client_point_id": "pt_003",
"data": "2026-04-27",
"hora": "08:56:06",
"latitude": -21.8589000,
"longitude": -47.4845450,
"accuracy": 4.2,
"speed": null,
"heading": null,
"ordem": 3
}
]
Resposta esperada da API
Sucesso HTTP 200
{
"success": true,
"msg": "Trajeto da aplicação recebido com sucesso.",
"data": {
"id_trajeto": 88,
"id_aplicacao": 123,
"client_event_id": "550e8400-e29b-41d4-a716-446655440000",
"device_id": 26,
"device_name": "testeG10",
"account_type": "produtor",
"total_pontos": 240,
"inseridos": 240,
"duplicados": 0,
"duracao_segundos": 720,
"sequencia_ok": true,
"total_gaps": 0,
"maior_gap_segundos": 3,
"already_registered": false
}
}
Reenvio idempotente
{
"success": true,
"msg": "Trajeto já registrado.",
"data": {
"id_trajeto": 88,
"id_aplicacao": 123,
"client_event_id": "550e8400-e29b-41d4-a716-446655440000",
"already_registered": true
}
}
Validação da sequência dos pontos
O app deve coletar pontos a cada amostragem_segundos, normalmente 3 segundos.
O ERP não deve bloquear o registro se houver falhas, mas deve marcar os indicadores de auditoria.
sequencia_ok=true: os intervalos entre os pontos estão dentro do esperado.sequencia_ok=false: existem buracos relevantes na sequência de horários.total_gaps: quantidade de intervalos acima da tolerância.maior_gap_segundos: maior intervalo encontrado entre dois pontos.
Sugestão inicial: considerar gap quando o intervalo entre dois pontos for maior que
amostragem_segundos * 3. Para amostragem de 3 segundos, isso gera tolerância de 9
segundos.
Como salvar no app
O app deve manter uma fila local de trajetos pendentes. Cada trajeto deve ser enviado usando o mesmo
client_event_id até a API confirmar sucesso.
Estrutura local sugerida
trajetos_local
- client_event_id
- id_aplicacao
- data_inicio
- hora_inicio
- data_fim
- hora_fim
- amostragem_segundos
- status_local
- sincronizado
- created_at
- updated_at
trajeto_pontos_local
- client_event_id
- client_point_id
- data
- hora
- latitude
- longitude
- accuracy
- speed
- heading
- ordem
Exemplo TypeScript — montagem do payload
type TrajetoPonto = {
client_point_id: string;
data: string;
hora: string;
latitude: number;
longitude: number;
accuracy?: number | null;
speed?: number | null;
heading?: number | null;
ordem: number;
};
type EnvioTrajetoPayload = {
action: 'apiG10';
f: 'receber_trajeto_aplicacao';
id_aplicacao: number;
client_event_id: string;
amostragem_segundos: number;
observacao?: string;
pontos: string;
};
function montarPayloadTrajeto(params: {
idAplicacao: number;
clientEventId: string;
pontos: TrajetoPonto[];
observacao?: string;
}): EnvioTrajetoPayload {
return {
action: 'apiG10',
f: 'receber_trajeto_aplicacao',
id_aplicacao: params.idAplicacao,
client_event_id: params.clientEventId,
amostragem_segundos: 3,
observacao: params.observacao || '',
pontos: JSON.stringify(params.pontos),
};
}
Exemplo TypeScript — envio para API
async function enviarTrajetoAplicacao(params: {
appKey: string;
deviceToken: string;
payload: EnvioTrajetoPayload;
}) {
const formData = new FormData();
Object.entries(params.payload).forEach(([key, value]) => {
formData.append(key, String(value));
});
const response = await fetch('https://agroecologia.grupoekos.com.br/router/action.php', {
method: 'POST',
headers: {
'X-APP-KEY': params.appKey,
'X-DEVICE-TOKEN': params.deviceToken,
},
body: formData,
});
const json = await response.json();
if (!json.success) {
throw new Error(json.msg || 'Erro ao enviar trajeto da aplicação.');
}
return json.data;
}
Exemplo de chamada cURL
curl -i -X POST "https://agroecologia.grupoekos.com.br/router/action.php" \
-H "X-APP-KEY: APP_KEY_FIXA_DO_APP" \
-H "X-DEVICE-TOKEN: dev_xxxxxxxxxxxxxxxxx" \
--data-urlencode "action=apiG10" \
--data-urlencode "f=receber_trajeto_aplicacao" \
--data-urlencode "id_aplicacao=123" \
--data-urlencode "client_event_id=550e8400-e29b-41d4-a716-446655440000" \
--data-urlencode "amostragem_segundos=3" \
--data-urlencode "observacao=Aplicação realizada no talhão 1" \
--data-urlencode 'pontos=[{"client_point_id":"pt_001","data":"2026-04-27","hora":"08:56:00","latitude":-21.8588800,"longitude":-47.4845167,"accuracy":4.5,"speed":null,"heading":null,"ordem":1}]'
Erros comuns
missing_app_key: headerX-APP-KEYnão enviado.invalid_api_key: APP_KEY inválida.missing_device_token: headerX-DEVICE-TOKENnão enviado.device_token_not_found: aparelho não pareado ou token inválido.missing_id_aplicacao:id_aplicacaonão enviado.missing_client_event_id:client_event_idnão enviado.invalid_client_event_id:client_event_idinválido.invalid_pontos_json: campopontosnão é um JSON válido.empty_pontos: lista de pontos vazia.invalid_location: latitude/longitude inválidas em algum ponto.aplicacao_not_found: aplicação não localizada no ERP.
Resumo para o desenvolvedor
- O app coleta o trajeto offline e envia depois.
- O envio é feito em um único endpoint.
- O vínculo principal é
id_aplicacao. - O trajeto usa
client_event_id, seguindo o padrão do modo offline. - Cada ponto usa
client_point_id. - O ERP identifica o aparelho pelo
X-DEVICE-TOKEN. - O ERP calcula início, fim, duração, total de pontos e gaps de sequência.