10) Envio de Trajeto na Aplicação

Esta seção documenta o envio do trajeto percorrido durante a aplicação em campo. O app deve coletar as coordenadas offline no celular e, quando houver internet, enviar o pacote completo para o ERP.

O trajeto fica vinculado ao id_aplicacao. O ERP identifica o aparelho pelo X-DEVICE-TOKEN, portanto device_id, device_name e account_type não devem ser informados manualmente pelo app.

Endpoint: receber_trajeto_aplicacao Auth: X-APP-KEY + X-DEVICE-TOKEN Modo: offline-first Vínculo: id_aplicacao Idempotência: client_event_id Envio: lote completo Amostragem: 3 segundos

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

  1. O usuário realiza os procedimentos anteriores da aplicação no app.
  2. O app obtém ou mantém o id_aplicacao da aplicação em andamento.
  3. Quando o operador estiver posicionado, ele clica em Iniciar trajeto.
  4. O app gera um client_event_id único para aquele trajeto.
  5. O app começa a gravar pontos de localização no banco local, normalmente a cada 3 segundos.
  6. Quando o operador finalizar a aplicação, o app marca o trajeto local como finalizado.
  7. Quando houver internet, o app envia o trajeto completo para f=receber_trajeto_aplicacao.
  8. Se a API retornar success=true, o app marca o trajeto local como sincronizado.
  9. 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.
Regra: usar 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>
O envio deve ser feito em 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_id e client_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
  }
]
Regra: o app coleta offline e envia o trajeto completo em lote quando houver internet.

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
Regra: o trajeto não deve depender de accuracy, speed ou heading para ser aceito.

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: header X-APP-KEY não enviado.
  • invalid_api_key: APP_KEY inválida.
  • missing_device_token: header X-DEVICE-TOKEN não enviado.
  • device_token_not_found: aparelho não pareado ou token inválido.
  • missing_id_aplicacao: id_aplicacao não enviado.
  • missing_client_event_id: client_event_id não enviado.
  • invalid_client_event_id: client_event_id inválido.
  • invalid_pontos_json: campo pontos nã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.