Contexto
ASTERIX es un estándar utilizado para el intercambio de información de vigilancia y tráfico aéreo. El proyecto nació porque, en algunos aeropuertos, parte de los datos aeronáuticos enviados por ENAIRE se descartaban antes de que la aplicación pudiera procesarlos. La mayoría de los eventos afectados llegaban en ASTERIX Categoría 62.
Para investigar la causa había que observar el dato antes del descarte. El plan consistió en capturar el tráfico en la capa de red, conservar los mensajes binarios, representarlos en hexadecimal y parsearlos campo a campo. De ese modo se podía comparar lo que realmente llegaba con lo que la aplicación esperaba, sin depender de los logs generados después del punto de rechazo.
Un mensaje ASTERIX no es un JSON ni una estructura textual. Es una representación binaria compacta cuyo significado depende de:
- la categoría;
- la versión de esa categoría;
- el FSPEC;
- los Data Items presentes;
- los bits internos de cada Data Item;
- las extensiones;
- los factores de escala definidos por EUROCONTROL.
El alcance fue creciendo hasta involucrar distintas categorías:
- CAT010;
- CAT019;
- CAT020;
- CAT048;
- CAT062;
- CAT065.
Dificultad real del problema
Una primera implementación de un protocolo binario puede caer fácilmente en una solución como:
text
Data Item -> bytes -> enteroPero eso no es suficiente.
Un campo puede contener, por ejemplo:
text
Byte 1
+---+---+---+---+---+---+---+---+
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+---+---+---+---+---+---+---+---+
| A | B | MODE | C | D | FX |
+---+---+---+---+---+---+---+---+El entero completo tiene poco valor para la aplicación. Lo correcto es devolver algo semántico:
json
{
"statusA": true,
"statusB": false,
"mode": 3,
"statusC": true,
"statusD": false,
"extensionPresent": true
}Además, una posición codificada en un entero puede necesitar convertirse a grados; un tiempo puede estar expresado con una resolución de 1/128 de segundo; una velocidad puede tener un LSB diferente.
Decisión: parsing semántico frente a parsing superficial
Alternativa sencilla
Guardar los bytes o el entero bruto de cada Data Item.
Ventajas:
- menos código;
- implementación más rápida;
- menor riesgo inicial de equivocarse con una tabla de bits.
- traslada la complejidad a todos los consumidores;
- dificulta validar el dato;
- hace que el backend no conozca realmente el protocolo;
- reduce la utilidad de la API;
- complica las comparaciones con herramientas especializadas.
Decisión adoptada
Interpretar los subcampos y aplicar resoluciones físicas en el parser.
La salida debía aproximarse a:
json
{
"raw": 12345,
"value": 96.4453125,
"unit": "s"
}o, para flags:
json
{
"confirmed": true,
"simulated": false,
"source": "ADS-B"
}según la semántica del Data Item.
Qué demuestra esta decisión
- capacidad de leer especificaciones;
- trabajo con representación binaria;
- atención a precisión;
- diseño pensando en consumidores posteriores;
- comprensión de que un protocolo no debe filtrarse sin interpretar a las capas superiores.
Decisión: mantener el concepto FSPEC visible
ASTERIX utiliza FSPEC para determinar qué Data Items aparecen realmente en un registro.
Una decisión relevante fue conservar información como fspecFields en las estructuras procesadas.
Esto facilita:
- depuración;
- trazabilidad;
- saber qué FRN estaba activo;
- comparar mensaje real con especificación;
- descubrir errores de desplazamiento;
- crear pruebas;
- investigar mensajes parcialmente implementados.
Gestión del offset
Una fuente crítica de errores en parsers binarios es el desplazamiento.
El patrón conceptual es:
ts
let offset = 0;if (fieldAExists) {
fieldA = parseFieldA(buffer, offset);
offset += FIELD_A_LENGTH;
}
if (fieldBExists) {
fieldB = parseFieldB(buffer, offset);
offset += fieldB.bytesConsumed;
}
El problema aparece cuando:
- la longitud es variable;
- existe FX;
- existe REP;
- hay extensiones;
- un bit activa más bytes;
- el parser consume un byte incorrecto.
Decisión
Cada función de parsing debía tener una responsabilidad clara sobre los bytes consumidos.
Para estructuras variables, resulta más seguro conceptualmente devolver:
ts
{
value,
bytesConsumed
}que calcular longitudes de forma dispersa.
Interpretación signed y complemento a dos
Otro punto delicado fue diferenciar valores unsigned de signed.
Un número de n bits con signo no puede interpretarse siempre con una lectura unsigned.
Conceptualmente:
text
if value >= 2^(n-1):
signed = value - 2^n
else:
signed = valueDespués se aplica la resolución:
text
physical_value = signed * LSBEsta combinación aparece de forma recurrente en velocidades, coordenadas, aceleraciones y otros parámetros.
Decisión: aplicar unidades físicas en el backend
En lugar de obligar al frontend o a los consumidores a conocer que:
text
raw 1234 * 1/128 segundosel parser debía realizar esa conversión.
Esto centraliza conocimiento del protocolo.
La arquitectura queda conceptualmente así:
mermaid
flowchart LR
A[Hexadecimal] --> B[Buffer]
B --> C[Cabecera ASTERIX]
C --> D[FSPEC]
D --> E[Data Items]
E --> F[Bit parsing]
F --> G[Escalas fisicas]
G --> H[Objeto TypeScript]
H --> I[API REST]Soporte para varios mensajes
Otro requisito importante fue no asumir que una entrada contenía siempre un único registro.
Por tanto, la lógica debía recorrer el bloque completo y mantener correctamente:
- longitud de categoría;
- longitud de bloque;
- offset global;
- offset dentro del registro.
Arquitectura NestJS
El uso de NestJS permitió separar:
text
Controller
|
v
Service
|
v
ASTERIX Router
|
+--> CAT010 parser
+--> CAT019 parser
+--> CAT020 parser
+--> CAT048 parser
+--> CAT062 parser
+--> CAT065 parserLa responsabilidad del router es identificar la categoría y delegar.
Cada parser concentra el conocimiento específico de su categoría.
Las utilidades comunes pueden concentrar:
- lectura de FSPEC;
- operaciones bitwise;
- signed conversion;
- escalas;
- validación de longitudes;
- extracción de bytes.
Problema de tipado detectado
Durante el desarrollo apareció un error TypeScript relacionado con pasar un Buffer a una función que esperaba string.
Este tipo de error suele indicar una frontera conceptual incorrecta.
Si el parser trabaja internamente con binario, la arquitectura más limpia es:
text
string hexadecimal
|
v
conversión única a Buffer
|
v
toda la lógica binaria trabaja con Buffery no alternar repetidamente entre string y Buffer.
Lo que aprendí
En procesamiento binario, definir claramente el tipo de dato de cada capa evita:
- conversiones innecesarias;
- ambigüedad;
- bugs;
- errores de TypeScript;
- pérdidas de rendimiento.
Estrategia incremental
Debido al tamaño de algunas categorías, se optó por implementar y revisar bloques de FRN/Data Items progresivamente.
Esto permite:
1. limitar el tamaño de cada revisión;
2. comparar cada grupo con la documentación;
3. evitar introducir cientos de líneas no verificadas;
4. facilitar pruebas con mensajes reales;
5. mantener control sobre el offset.
La fragmentación del trabajo no es únicamente una cuestión de productividad. Es una estrategia de reducción de riesgo.
Hallazgos y resolución
El análisis permitió seguir el recorrido completo desde el paquete capturado hasta los campos semánticos obtenidos por el parser. Al comparar los eventos aceptados con los descartados aparecieron diferencias en bits que, una vez interpretados, formaban los campos utilizados para identificar los aeropuertos de origen y destino. En determinados mensajes esos valores llegaban incorrectos y provocaban el rechazo antes del procesamiento normal.
Posteriormente apareció un segundo fallo, más difícil de reproducir porque se manifestaba de forma aleatoria. En este caso el valor incorrecto estaba en el bit que distinguía una operación de despegue de una de aterrizaje. Mantener visibles el FSPEC, los offsets y los valores binarios originales permitió aislar también esta diferencia.
Ambos hallazgos se comunicaron a ENAIRE con las evidencias obtenidas durante la captura y el parsing. Los errores fueron corregidos en origen, y los mensajes dejaron de descartarse por estas causas. El parser pasó así de ser una herramienta de interpretación del estándar a una pieza de diagnóstico para demostrar un problema real en la cadena de datos aeronáuticos.
Cómo validé el resultado
Organicé la validación en una matriz para comprobar por separado cada punto en el que podía desviarse la interpretación:
| Validación | Objetivo |
|---|---|
| Mensajes conocidos | comprobar valores esperados |
| FSPEC | comprobar Data Items presentes |
| Longitud | comprobar consumo exacto de bytes |
| Hex dump | localizar errores de offset |
| Herramienta externa | comparar interpretación |
| Casos signed | comprobar positivos y negativos |
| FX | comprobar extensiones |
| Multi-record | comprobar recorrido de bloques |
| Valores límite | comprobar overflow y escalado |
