- Updated docs and inventory files - Added new architecture docs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
32 KiB
Integracion GPS Multi-Provider
Proyecto: ERP Transportistas | Version: 1.0.0 | Actualizado: 2026-01-27 Referencia GAP: GAP-001 - Modulo GPS Completo (multi-provider)
Descripcion General
El sistema de tracking de ERP Transportistas implementa una arquitectura multi-provider para la ingestion de posiciones GPS. Esta arquitectura permite integrar diferentes proveedores de telematica sin modificar la logica de negocio, utilizando el patron Adapter para normalizar los datos de cada proveedor al esquema unificado del sistema.
La arquitectura esta disenada para:
- Soportar multiples proveedores GPS simultaneamente
- Permitir migracion entre proveedores sin afectar la operacion
- Mantener historico de posiciones independiente del proveedor
- Facilitar la incorporacion de nuevos proveedores
Arquitectura Multi-Provider
Patron de Abstraccion
El sistema utiliza el patron Adapter combinado con una Factory para abstraer la comunicacion con los diferentes proveedores GPS. Cada proveedor implementa una interface comun que normaliza los datos al formato interno del sistema.
+------------------+ +-------------------+ +------------------+
| Traccar API | --> | TraccarAdapter | --> | |
+------------------+ +-------------------+ | |
| |
+------------------+ +-------------------+ | IGpsProvider |
| Wialon API | --> | WialonAdapter | --> | Interface |
+------------------+ +-------------------+ | |
| |
+------------------+ +-------------------+ | |
| Samsara API | --> | SamsaraAdapter | --> | |
+------------------+ +-------------------+ +--------+---------+
|
+------------------+ +-------------------+ |
| Geotab API | --> | GeotabAdapter | -------------+
+------------------+ +-------------------+ |
v
+------------------+ +-------------------+ +------------------+
| Manual Entry | --> | ManualAdapter | --> | GpsPositionDto |
+------------------+ +-------------------+ +------------------+
|
v
+------------------+
| tracking. |
| posiciones_gps |
+------------------+
Interface IGpsProvider
La interface principal que todos los adapters deben implementar:
// src/modules/tracking/interfaces/gps-provider.interface.ts
export interface GpsPosition {
deviceId: string; // IMEI o identificador unico del dispositivo
timestamp: Date; // Fecha/hora del GPS (UTC)
latitude: number; // Latitud (-90 a 90)
longitude: number; // Longitud (-180 a 180)
altitude?: number; // Altitud en metros
speed?: number; // Velocidad en km/h
course?: number; // Rumbo (0-360 grados)
odometer?: number; // Odometro en km
engineOn?: boolean; // Estado del motor
hdop?: number; // Precision horizontal (1-50)
satellites?: number; // Numero de satelites
attributes?: Record<string, any>; // Datos adicionales del proveedor
}
export interface DeviceInfo {
deviceId: string;
imei: string;
name: string;
model?: string;
phone?: string;
status: 'online' | 'offline' | 'unknown';
lastPosition?: GpsPosition;
}
export interface IGpsProvider {
// Identificador del proveedor
readonly providerName: string;
// Conexion y autenticacion
connect(): Promise<void>;
disconnect(): Promise<void>;
isConnected(): boolean;
// Dispositivos
getDevices(): Promise<DeviceInfo[]>;
getDevice(deviceId: string): Promise<DeviceInfo | null>;
// Posiciones
getLastPosition(deviceId: string): Promise<GpsPosition | null>;
getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]>;
// Tiempo real (opcional - no todos los proveedores lo soportan)
subscribeToPositions?(callback: (position: GpsPosition) => void): void;
unsubscribeFromPositions?(): void;
// Webhook (opcional - para proveedores que soportan push)
parseWebhookPayload?(payload: any): GpsPosition | GpsPosition[] | null;
}
GpsProviderFactory
Factory para obtener el adapter correcto segun configuracion:
// src/modules/tracking/providers/gps-provider.factory.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IGpsProvider } from '../interfaces/gps-provider.interface';
import { TraccarAdapter } from './traccar.adapter';
import { WialonAdapter } from './wialon.adapter';
import { SamsaraAdapter } from './samsara.adapter';
import { GeotabAdapter } from './geotab.adapter';
import { ManualAdapter } from './manual.adapter';
export type GpsProviderType = 'traccar' | 'wialon' | 'samsara' | 'geotab' | 'manual';
@Injectable()
export class GpsProviderFactory {
constructor(private configService: ConfigService) {}
create(provider?: GpsProviderType): IGpsProvider {
const providerType = provider || this.configService.get<GpsProviderType>('GPS_PROVIDER', 'manual');
switch (providerType) {
case 'traccar':
return new TraccarAdapter(this.configService);
case 'wialon':
return new WialonAdapter(this.configService);
case 'samsara':
return new SamsaraAdapter(this.configService);
case 'geotab':
return new GeotabAdapter(this.configService);
case 'manual':
default:
return new ManualAdapter();
}
}
getSupportedProviders(): GpsProviderType[] {
return ['traccar', 'wialon', 'samsara', 'geotab', 'manual'];
}
}
Proveedores Soportados
1. Traccar (Recomendado)
Tipo: Open Source, Auto-hospedado Licencia: Apache 2.0 (Gratuito) Documentacion: https://www.traccar.org/documentation/
| Caracteristica | Valor |
|---|---|
| API | REST + WebSocket |
| Autenticacion | Basic Auth / Token |
| Protocolos GPS | 200+ (GT06, Teltonika, Queclink, etc.) |
| Tiempo Real | WebSocket nativo |
| Webhook | Soportado |
| Geocercas | Soportado |
| Alertas | Soportado |
Ventajas:
- Sin costo de licencias
- Control total de datos (on-premise)
- Soporta la mayoria de dispositivos GPS del mercado
- WebSocket para actualizaciones en tiempo real
- API REST completa y bien documentada
- Comunidad activa
Configuracion:
// src/modules/tracking/providers/traccar.adapter.ts
import { IGpsProvider, GpsPosition, DeviceInfo } from '../interfaces/gps-provider.interface';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosInstance } from 'axios';
import WebSocket from 'ws';
export class TraccarAdapter implements IGpsProvider {
readonly providerName = 'traccar';
private client: AxiosInstance;
private ws: WebSocket | null = null;
private connected = false;
constructor(private configService: ConfigService) {
const baseURL = this.configService.get<string>('TRACCAR_API_URL', 'http://localhost:8082/api');
const email = this.configService.get<string>('TRACCAR_EMAIL');
const password = this.configService.get<string>('TRACCAR_PASSWORD');
this.client = axios.create({
baseURL,
auth: { username: email, password },
headers: { 'Content-Type': 'application/json' },
});
}
async connect(): Promise<void> {
// Verificar conexion obteniendo sesion
const response = await this.client.get('/session');
if (response.status === 200) {
this.connected = true;
}
}
async disconnect(): Promise<void> {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.connected = false;
}
isConnected(): boolean {
return this.connected;
}
async getDevices(): Promise<DeviceInfo[]> {
const response = await this.client.get('/devices');
return response.data.map((d: any) => this.mapDevice(d));
}
async getDevice(deviceId: string): Promise<DeviceInfo | null> {
const response = await this.client.get(`/devices?uniqueId=${deviceId}`);
if (response.data.length > 0) {
return this.mapDevice(response.data[0]);
}
return null;
}
async getLastPosition(deviceId: string): Promise<GpsPosition | null> {
const device = await this.getDevice(deviceId);
if (!device) return null;
const response = await this.client.get(`/positions?deviceId=${device.deviceId}`);
if (response.data.length > 0) {
return this.mapPosition(response.data[0]);
}
return null;
}
async getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]> {
const device = await this.getDevice(deviceId);
if (!device) return [];
const response = await this.client.get('/positions', {
params: {
deviceId: device.deviceId,
from: from.toISOString(),
to: to.toISOString(),
},
});
return response.data.map((p: any) => this.mapPosition(p));
}
subscribeToPositions(callback: (position: GpsPosition) => void): void {
const wsUrl = this.configService.get<string>('TRACCAR_WS_URL', 'ws://localhost:8082/api/socket');
this.ws = new WebSocket(wsUrl);
this.ws.on('message', (data: string) => {
const message = JSON.parse(data);
if (message.positions) {
message.positions.forEach((p: any) => {
callback(this.mapPosition(p));
});
}
});
}
unsubscribeFromPositions(): void {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
parseWebhookPayload(payload: any): GpsPosition | null {
if (payload.position) {
return this.mapPosition(payload.position);
}
return null;
}
private mapDevice(traccarDevice: any): DeviceInfo {
return {
deviceId: traccarDevice.id.toString(),
imei: traccarDevice.uniqueId,
name: traccarDevice.name,
model: traccarDevice.model,
phone: traccarDevice.phone,
status: traccarDevice.status === 'online' ? 'online' : 'offline',
};
}
private mapPosition(traccarPosition: any): GpsPosition {
return {
deviceId: traccarPosition.deviceId.toString(),
timestamp: new Date(traccarPosition.deviceTime || traccarPosition.fixTime),
latitude: traccarPosition.latitude,
longitude: traccarPosition.longitude,
altitude: traccarPosition.altitude,
speed: traccarPosition.speed ? traccarPosition.speed * 1.852 : 0, // knots to km/h
course: traccarPosition.course,
odometer: traccarPosition.attributes?.totalDistance
? traccarPosition.attributes.totalDistance / 1000
: undefined,
engineOn: traccarPosition.attributes?.ignition,
hdop: traccarPosition.attributes?.hdop,
satellites: traccarPosition.attributes?.sat,
attributes: traccarPosition.attributes,
};
}
}
Variables de Entorno:
# Traccar Configuration
GPS_PROVIDER=traccar
TRACCAR_API_URL=http://traccar.example.com:8082/api
TRACCAR_WS_URL=ws://traccar.example.com:8082/api/socket
TRACCAR_EMAIL=admin@example.com
TRACCAR_PASSWORD=secret
2. Wialon (Gurtam)
Tipo: SaaS / On-Premise Licencia: Comercial (por unidad/mes) Documentacion: https://sdk.wialon.com/
| Caracteristica | Valor |
|---|---|
| API | REST (RemoteAPI) |
| Autenticacion | Token |
| Protocolos GPS | 2500+ |
| Tiempo Real | Long Polling |
| Webhook | Limitado |
| Geocercas | Avanzado |
| Alertas | Avanzado |
Ventajas:
- Plataforma madura y estable
- Soporte para casi cualquier dispositivo GPS
- Herramientas de analisis avanzadas
- Soporte tecnico profesional
Configuracion:
// src/modules/tracking/providers/wialon.adapter.ts
export class WialonAdapter implements IGpsProvider {
readonly providerName = 'wialon';
private sid: string | null = null;
private baseUrl: string;
private token: string;
constructor(private configService: ConfigService) {
this.baseUrl = this.configService.get<string>('WIALON_API_URL', 'https://hst-api.wialon.com/wialon/ajax.html');
this.token = this.configService.get<string>('WIALON_TOKEN', '');
}
async connect(): Promise<void> {
const response = await axios.get(this.baseUrl, {
params: {
svc: 'token/login',
params: JSON.stringify({ token: this.token }),
},
});
if (response.data.eid) {
this.sid = response.data.eid;
} else {
throw new Error('Wialon authentication failed');
}
}
async getDevices(): Promise<DeviceInfo[]> {
const response = await this.request('core/search_items', {
spec: {
itemsType: 'avl_unit',
propName: 'sys_name',
propValueMask: '*',
sortType: 'sys_name',
},
force: 1,
flags: 1025, // 1 (base) + 1024 (last message)
from: 0,
to: 0,
});
return response.items.map((unit: any) => this.mapDevice(unit));
}
async getLastPosition(deviceId: string): Promise<GpsPosition | null> {
const response = await this.request('core/search_item', {
id: deviceId,
flags: 1025,
});
if (response.item && response.item.pos) {
return this.mapPosition(response.item);
}
return null;
}
async getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]> {
const response = await this.request('messages/load_interval', {
itemId: deviceId,
timeFrom: Math.floor(from.getTime() / 1000),
timeTo: Math.floor(to.getTime() / 1000),
flags: 0,
flagsMask: 0,
loadCount: 10000,
});
return response.messages
.filter((m: any) => m.pos)
.map((m: any) => this.mapMessageToPosition(deviceId, m));
}
private async request(svc: string, params: any): Promise<any> {
const response = await axios.get(this.baseUrl, {
params: {
svc,
params: JSON.stringify(params),
sid: this.sid,
},
});
return response.data;
}
private mapPosition(unit: any): GpsPosition {
return {
deviceId: unit.id.toString(),
timestamp: new Date(unit.pos.t * 1000),
latitude: unit.pos.y,
longitude: unit.pos.x,
altitude: unit.pos.z,
speed: unit.pos.s,
course: unit.pos.c,
satellites: unit.pos.sc,
};
}
}
Variables de Entorno:
# Wialon Configuration
GPS_PROVIDER=wialon
WIALON_API_URL=https://hst-api.wialon.com/wialon/ajax.html
WIALON_TOKEN=your_wialon_token_here
3. Samsara
Tipo: SaaS (Fleet Management Moderno) Licencia: Comercial (por vehiculo/mes) Documentacion: https://developers.samsara.com/
| Caracteristica | Valor |
|---|---|
| API | REST (OpenAPI 3.0) |
| Autenticacion | Bearer Token |
| Dispositivos | Hardware propietario |
| Tiempo Real | Webhooks |
| Geocercas | Soportado |
| Alertas | Avanzado |
| Dashcam | Integrado |
Ventajas:
- API moderna y bien documentada
- Webhooks para tiempo real
- Integracion con dashcam y ELD
- Soporte para IFTA, DVIR, HOS
Configuracion:
// src/modules/tracking/providers/samsara.adapter.ts
export class SamsaraAdapter implements IGpsProvider {
readonly providerName = 'samsara';
private client: AxiosInstance;
constructor(private configService: ConfigService) {
const apiKey = this.configService.get<string>('SAMSARA_API_KEY');
this.client = axios.create({
baseURL: 'https://api.samsara.com',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
});
}
async getDevices(): Promise<DeviceInfo[]> {
const response = await this.client.get('/fleet/vehicles');
return response.data.data.map((v: any) => ({
deviceId: v.id,
imei: v.serial,
name: v.name,
model: v.make + ' ' + v.model,
status: v.gps?.time ? 'online' : 'offline',
}));
}
async getLastPosition(deviceId: string): Promise<GpsPosition | null> {
const response = await this.client.get(`/fleet/vehicles/${deviceId}/locations`);
const location = response.data.data[0];
if (!location) return null;
return {
deviceId,
timestamp: new Date(location.time),
latitude: location.latitude,
longitude: location.longitude,
speed: location.speed ? location.speed * 3.6 : 0, // m/s to km/h
course: location.heading,
};
}
async getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]> {
const response = await this.client.get(`/fleet/vehicles/${deviceId}/locations/history`, {
params: {
startTime: from.toISOString(),
endTime: to.toISOString(),
},
});
return response.data.data.map((loc: any) => ({
deviceId,
timestamp: new Date(loc.time),
latitude: loc.latitude,
longitude: loc.longitude,
speed: loc.speed ? loc.speed * 3.6 : 0,
course: loc.heading,
}));
}
parseWebhookPayload(payload: any): GpsPosition | null {
if (payload.eventType === 'VehicleGpsUpdated') {
const data = payload.data;
return {
deviceId: data.vehicle.id,
timestamp: new Date(data.gps.time),
latitude: data.gps.latitude,
longitude: data.gps.longitude,
speed: data.gps.speedMilesPerHour ? data.gps.speedMilesPerHour * 1.60934 : 0,
course: data.gps.headingDegrees,
};
}
return null;
}
}
Variables de Entorno:
# Samsara Configuration
GPS_PROVIDER=samsara
SAMSARA_API_KEY=your_samsara_api_key_here
SAMSARA_WEBHOOK_SECRET=your_webhook_secret
4. Geotab
Tipo: SaaS (Telematica Empresarial) Licencia: Comercial (por dispositivo/mes) Documentacion: https://developers.geotab.com/
| Caracteristica | Valor |
|---|---|
| API | REST + OData |
| Autenticacion | Session Token |
| Dispositivos | Hardware propietario (GO) |
| Tiempo Real | Data Feed |
| Geocercas | Avanzado |
| Alertas | Reglas configurables |
| Add-Ins | Marketplace extenso |
Ventajas:
- Precision de datos excepcional
- SDK y Add-In framework
- Integraciones empresariales
- Reportes de conduccion detallados
Configuracion:
// src/modules/tracking/providers/geotab.adapter.ts
export class GeotabAdapter implements IGpsProvider {
readonly providerName = 'geotab';
private credentials: any;
private sessionId: string | null = null;
constructor(private configService: ConfigService) {
this.credentials = {
database: this.configService.get<string>('GEOTAB_DATABASE'),
userName: this.configService.get<string>('GEOTAB_USERNAME'),
password: this.configService.get<string>('GEOTAB_PASSWORD'),
};
}
async connect(): Promise<void> {
const response = await axios.post(
`https://${this.configService.get<string>('GEOTAB_SERVER', 'my.geotab.com')}/apiv1`,
{
method: 'Authenticate',
params: this.credentials,
}
);
this.sessionId = response.data.result.credentials.sessionId;
}
async getDevices(): Promise<DeviceInfo[]> {
const response = await this.call('Get', {
typeName: 'Device',
resultsLimit: 1000,
});
return response.map((d: any) => ({
deviceId: d.id,
imei: d.serialNumber,
name: d.name,
model: d.deviceType,
status: 'unknown',
}));
}
async getLastPosition(deviceId: string): Promise<GpsPosition | null> {
const response = await this.call('Get', {
typeName: 'DeviceStatusInfo',
search: { deviceSearch: { id: deviceId } },
});
if (response.length === 0) return null;
const status = response[0];
return {
deviceId,
timestamp: new Date(status.dateTime),
latitude: status.latitude,
longitude: status.longitude,
speed: status.speed,
course: status.bearing,
};
}
async getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]> {
const response = await this.call('Get', {
typeName: 'LogRecord',
search: {
deviceSearch: { id: deviceId },
fromDate: from.toISOString(),
toDate: to.toISOString(),
},
});
return response.map((log: any) => ({
deviceId,
timestamp: new Date(log.dateTime),
latitude: log.latitude,
longitude: log.longitude,
speed: log.speed,
}));
}
private async call(method: string, params: any): Promise<any> {
const response = await axios.post(
`https://${this.configService.get<string>('GEOTAB_SERVER', 'my.geotab.com')}/apiv1`,
{
method,
params: {
...params,
credentials: { ...this.credentials, sessionId: this.sessionId },
},
}
);
return response.data.result;
}
}
Variables de Entorno:
# Geotab Configuration
GPS_PROVIDER=geotab
GEOTAB_SERVER=my.geotab.com
GEOTAB_DATABASE=your_database
GEOTAB_USERNAME=your_username
GEOTAB_PASSWORD=your_password
5. Manual (Fallback)
Tipo: Entrada Manual / Importacion Licencia: N/A (Sin costo) Uso: Fallback cuando no hay GPS, pruebas, migracion de datos
| Caracteristica | Valor |
|---|---|
| API | Endpoints propios |
| Autenticacion | JWT del sistema |
| Dispositivos | Virtuales |
| Tiempo Real | No |
Casos de Uso:
- Empresas sin dispositivos GPS
- Periodo de transicion
- Pruebas y desarrollo
- Importacion de datos historicos
// src/modules/tracking/providers/manual.adapter.ts
export class ManualAdapter implements IGpsProvider {
readonly providerName = 'manual';
private connected = true;
async connect(): Promise<void> {
this.connected = true;
}
async disconnect(): Promise<void> {
this.connected = false;
}
isConnected(): boolean {
return this.connected;
}
async getDevices(): Promise<DeviceInfo[]> {
// Retorna dispositivos virtuales registrados en el sistema
return [];
}
async getDevice(deviceId: string): Promise<DeviceInfo | null> {
return null;
}
async getLastPosition(deviceId: string): Promise<GpsPosition | null> {
// Consulta directamente a la BD local
return null;
}
async getPositions(deviceId: string, from: Date, to: Date): Promise<GpsPosition[]> {
// Consulta directamente a la BD local
return [];
}
// El adapter manual no requiere parseo de webhook
// Las posiciones se ingresan via endpoint POST /api/tracking/posiciones
}
Entidades de Base de Datos
GpsDevice (Dispositivo GPS)
Almacena la configuracion de dispositivos GPS vinculados a unidades.
Tabla: tracking.gps_devices
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | Identificador unico |
| tenant_id | UUID | Tenant propietario |
| unidad_id | UUID | FK a fleet.unidades |
| imei | VARCHAR(50) | IMEI del dispositivo |
| proveedor | VARCHAR(50) | traccar, wialon, samsara, geotab, manual |
| device_id_externo | VARCHAR(100) | ID en el sistema del proveedor |
| nombre | VARCHAR(100) | Nombre descriptivo |
| modelo | VARCHAR(100) | Modelo del dispositivo |
| sim_numero | VARCHAR(20) | Numero de SIM |
| sim_operador | VARCHAR(50) | Operador de la SIM |
| intervalo_segundos | INTEGER | Intervalo de reporte (default 30) |
| activo | BOOLEAN | Estado activo/inactivo |
| ultima_posicion_at | TIMESTAMPTZ | Fecha de ultima posicion |
| created_at | TIMESTAMPTZ | Fecha de creacion |
| updated_at | TIMESTAMPTZ | Fecha de actualizacion |
Indices:
idx_gps_device_tenant(tenant_id)idx_gps_device_imeiUNIQUE (tenant_id, imei)idx_gps_device_unidad(unidad_id)
GpsPosition (Posicion GPS)
Almacena las posiciones GPS recibidas. Tabla particionada por mes.
Tabla: tracking.posiciones_gps
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | Identificador unico |
| tenant_id | UUID | Tenant propietario |
| unidad_id | UUID | FK a fleet.unidades |
| viaje_id | UUID | FK a transport.viajes (opcional) |
| imei | VARCHAR(50) | IMEI del dispositivo |
| proveedor_gps | VARCHAR(50) | Proveedor de la posicion |
| timestamp_gps | TIMESTAMPTZ | Fecha/hora del GPS |
| timestamp_servidor | TIMESTAMPTZ | Fecha/hora de recepcion |
| latitud | DECIMAL(10,7) | Latitud |
| longitud | DECIMAL(10,7) | Longitud |
| altitud | DECIMAL(8,2) | Altitud en metros |
| velocidad_kmh | DECIMAL(6,2) | Velocidad en km/h |
| rumbo | SMALLINT | Direccion (0-360) |
| odometro_km | DECIMAL(12,2) | Odometro en km |
| motor_encendido | BOOLEAN | Estado del motor |
| hdop | DECIMAL(4,2) | Precision horizontal |
| satelites | SMALLINT | Numero de satelites |
| valida | BOOLEAN | Posicion valida/invalida |
| datos_extra | JSONB | Datos adicionales |
| fecha_particion | DATE | Campo de particionamiento |
Particionamiento:
CREATE TABLE tracking.posiciones_gps (
-- campos...
) PARTITION BY RANGE (fecha_particion);
-- Crear particiones mensuales automaticamente
CREATE TABLE tracking.posiciones_gps_2026_01 PARTITION OF tracking.posiciones_gps
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
RouteSegment (Segmento de Ruta)
Almacena segmentos de ruta calculados entre posiciones.
Tabla: tracking.route_segments
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | Identificador unico |
| tenant_id | UUID | Tenant propietario |
| viaje_id | UUID | FK a transport.viajes |
| posicion_inicio_id | UUID | FK a posiciones_gps |
| posicion_fin_id | UUID | FK a posiciones_gps |
| distancia_km | DECIMAL(10,3) | Distancia del segmento |
| duracion_segundos | INTEGER | Duracion en segundos |
| velocidad_promedio_kmh | DECIMAL(6,2) | Velocidad promedio |
| en_movimiento | BOOLEAN | Si estaba en movimiento |
| geometria | GEOMETRY(LINESTRING, 4326) | LineString PostGIS |
| created_at | TIMESTAMPTZ | Fecha de creacion |
Configuracion del Sistema
Variables de Entorno
# ===========================================
# GPS Provider Configuration
# ===========================================
# Proveedor activo: traccar | wialon | samsara | geotab | manual
GPS_PROVIDER=traccar
# URL para recibir webhooks de proveedores GPS
GPS_WEBHOOK_URL=https://api.example.com/webhooks/gps
# Intervalo de polling para proveedores sin tiempo real (segundos)
GPS_POLLING_INTERVAL=30
# ===========================================
# Traccar (Recomendado)
# ===========================================
TRACCAR_API_URL=http://traccar.example.com:8082/api
TRACCAR_WS_URL=ws://traccar.example.com:8082/api/socket
TRACCAR_EMAIL=admin@example.com
TRACCAR_PASSWORD=secret
# ===========================================
# Wialon
# ===========================================
WIALON_API_URL=https://hst-api.wialon.com/wialon/ajax.html
WIALON_TOKEN=your_token_here
# ===========================================
# Samsara
# ===========================================
SAMSARA_API_KEY=samsara_api_key
SAMSARA_WEBHOOK_SECRET=webhook_secret
# ===========================================
# Geotab
# ===========================================
GEOTAB_SERVER=my.geotab.com
GEOTAB_DATABASE=your_database
GEOTAB_USERNAME=your_username
GEOTAB_PASSWORD=your_password
Configuracion Multi-Tenant
Cada tenant puede tener su propia configuracion de proveedor GPS:
// tenant_settings en la tabla de tenants
{
"gps": {
"provider": "traccar",
"config": {
"apiUrl": "https://tenant-traccar.example.com/api",
"credentials": "encrypted_reference"
},
"defaultInterval": 30,
"validationRules": {
"maxSpeed": 180,
"minSatellites": 4,
"maxHdop": 10
}
}
}
API Endpoints
Ingestion de Posiciones
POST /api/tracking/posiciones
Endpoint para recibir posiciones GPS (usado por webhooks o adapter manual).
Request:
{
"deviceId": "352093088937641",
"timestamp": "2026-01-27T14:30:00Z",
"latitude": 19.4326,
"longitude": -99.1332,
"speed": 65.5,
"course": 180,
"altitude": 2240,
"engineOn": true,
"hdop": 1.2,
"satellites": 12
}
Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"unidadId": "unit-uuid",
"viajeId": "viaje-uuid",
"procesado": true,
"alertasGeneradas": 0
}
Webhook Endpoint
POST /api/webhooks/gps/:provider
Endpoint generico para recibir webhooks de cualquier proveedor.
POST /api/webhooks/gps/traccar
POST /api/webhooks/gps/samsara
Validacion de Posiciones
El sistema valida cada posicion recibida antes de almacenarla:
// src/modules/tracking/services/position-validator.service.ts
export interface ValidationResult {
valid: boolean;
reasons: string[];
}
export class PositionValidatorService {
validate(position: GpsPosition, lastPosition?: GpsPosition): ValidationResult {
const reasons: string[] = [];
// 1. Coordenadas validas
if (position.latitude < -90 || position.latitude > 90) {
reasons.push('Latitud fuera de rango');
}
if (position.longitude < -180 || position.longitude > 180) {
reasons.push('Longitud fuera de rango');
}
// 2. Velocidad razonable (max 200 km/h para transporte de carga)
if (position.speed && position.speed > 200) {
reasons.push('Velocidad excesiva');
}
// 3. HDOP aceptable (precision)
if (position.hdop && position.hdop > 10) {
reasons.push('Precision GPS baja');
}
// 4. Minimo de satelites
if (position.satellites && position.satellites < 4) {
reasons.push('Satelites insuficientes');
}
// 5. Salto de posicion (teleportacion)
if (lastPosition) {
const distance = this.calculateDistance(
lastPosition.latitude, lastPosition.longitude,
position.latitude, position.longitude
);
const timeDiff = (position.timestamp.getTime() - lastPosition.timestamp.getTime()) / 1000;
const maxPossibleDistance = (200 / 3600) * timeDiff; // km a max 200 km/h
if (distance > maxPossibleDistance * 1.5) {
reasons.push('Salto de posicion detectado');
}
}
return {
valid: reasons.length === 0,
reasons,
};
}
private calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
// Formula Haversine
const R = 6371; // Radio de la Tierra en km
const dLat = this.toRad(lat2 - lat1);
const dLon = this.toRad(lon2 - lon1);
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
private toRad(deg: number): number {
return deg * Math.PI / 180;
}
}
Migracion Entre Proveedores
Procedimiento para cambiar de proveedor GPS:
-
Preparacion:
- Configurar nuevo proveedor en paralelo
- Mapear IMEI/device_id entre sistemas
- Ejecutar pruebas de conectividad
-
Migracion Gradual:
- Activar nuevo proveedor para unidades piloto
- Monitorear recepcion de posiciones
- Validar consistencia de datos
-
Cutover:
- Actualizar GPS_PROVIDER en configuracion
- Actualizar mapping de dispositivos
- Desactivar proveedor anterior
-
Post-Migracion:
- Verificar todas las unidades reportando
- Revisar alertas y geocercas
- Documentar configuracion final
Referencias
Integracion GPS Multi-Provider - ERP Transportistas v1.0.0