El estándar DL/T 645 es un protocolo de comunicación ampliamente utilizado en medidores electrónicos de energía. El siguiente script en JavaScript implementa la lógica necesaria para decodificar las tramas provenientes de los dispositivos (Device-to-Cloud) y codificar comandos para su envío (Cloud-to-Device), soportando operaciones de lectura y escritura de parámetros eléctricos, fecha y hora.
Estructura del Diccionario de Datos
Para la gestión del protocolo, se define un mapeo de identificadores técnicos que asocian el código de datos (DataCode) con su longitud, precisión decimal y tipo de procesamiento (ASCII o numérico).
const METODOS_IOT = {
POST: 'thing.event.property.post',
GET: 'thing.service.property.get',
SET: 'thing.service.property.set'
};
const MAPA_REGISTROS = {
LECTURA: {
Ua: { codigo: "02010100", bytes: 2, decimales: 1, nombre: 'Tensión Fase A', conSigno: 0 },
Ia: { codigo: "02020100", bytes: 3, decimales: 3, nombre: 'Corriente Fase A', conSigno: 1 },
Pa: { codigo: "02030100", bytes: 3, decimales: 4, nombre: 'Potencia Activa Fase A', conSigno: 1 },
EnergiaActivaTotal: { codigo: "00010000", bytes: 4, decimales: 2, nombre: 'Energía Activa Total', conSigno: 0 },
Frecuencia: { codigo: "02800002", bytes: 2, decimales: 2, nombre: 'Frecuencia de Red', conSigno: 0 },
VersionSW: { codigo: "04800001", bytes: 20, decimales: 0, nombre: 'Versión Software', conSigno: 0, esASCII: true },
FechaYMDW: { codigo: "04000101", bytes: 4, decimales: 0, nombre: 'Fecha y Día', conSigno: 0, tipo: "text" },
HoraHMS: { codigo: "04000102", bytes: 3, decimales: 0, nombre: 'Hora, Minuto, Segundo', conSigno: 0, tipo: "text" }
},
ESCRITURA: {
FechaYMDW: { codigo: "04000101", bytes: 4 },
HoraHMS: { codigo: "04000102", bytes: 3 }
}
};
Decodificación de Tramas (Dispositivo a Nube)
La función decodificarTrama645 procesa la carga útil en hexadecimal, realiza la validación de integridad mediante Checksum (CS) y resta el desplazamiento de seguridad (0x33) obligatorio en este protocolo.
function decodificarTrama645(jsonInput) {
const input = JSON.parse(jsonInput);
const hexRaw = Buffer.from(input.result.data, 'base64').toString('hex').toUpperCase();
const idEsperado = input.inputConfig.identifier;
// Validación de marcadores de inicio (68H)
if (hexRaw.substring(0, 2) !== '68' || hexRaw.substring(14, 16) !== '68') {
throw new Error('Formato de trama inválido: falta marcador 68H');
}
// Validación de Checksum
const cuerpoParaCS = hexRaw.substring(0, hexRaw.length - 4);
if (calcularChecksum(cuerpoParaCS) !== hexRaw.slice(-4, -2)) {
throw new Error('Error de suma de comprobación (CS)');
}
const codigoControl = hexRaw.substring(16, 18);
let resultado = {
deviceName: invertirBytes(hexRaw.substring(2, 14)),
data: "",
identifier: idEsperado,
method: codigoControl === "91" ? METODOS_IOT.GET : METODOS_IOT.SET
};
if (codigoControl === "91") {
// Extraer y procesar identificador de datos (4 bytes con offset 33H)
let dataCodeHex = procesarOffset(hexRaw.substring(20, 28), -0x33);
const config = MAPA_REGISTROS.LECTURA[idEsperado];
// Extraer valor de datos
let segmentoDatos = hexRaw.substring(28, 28 + (config.bytes * 2));
let datosLimpios = procesarOffset(segmentoDatos, -0x33);
if (config.esASCII) {
resultado.data = Buffer.from(datosLimpios, 'hex').toString().trim();
} else {
let valorNum = parseInt(datosLimpios, 16);
// Manejo de signo si el bit más significativo está activo
if (config.conSigno && parseInt(datosLimpios.substring(0, 2), 16) >= 0x80) {
valorNum = -(valorNum - 0x80 * Math.pow(256, config.bytes - 1));
}
resultado.data = valorNum / Math.pow(10, config.decimales);
}
} else if (codigoControl === "94") {
resultado.data = "SUCCESS_WRITE";
}
return resultado;
}
Codificación de Comandos (Nube a Dispositivo)
Para enviar instrucciones al medidor, es necesario construir la trama con la dirección del dispositivo invertida, el código de función correspondiente y aplicar el desplazamiento de 0x33 a los datos de salida.
function generarComando645(jsonInput) {
const obj = JSON.parse(jsonInput);
const tipo = obj.type;
const deviceAddr = invertirBytes(obj.deviceName);
const identificador = obj.params.identifier;
let trama = `68${deviceAddr}68`;
if (tipo === 'get') {
trama += "1104"; // Control: Leer, Longitud: 4 (DataCode)
const reg = MAPA_REGISTROS.LECTURA[identificador];
trama += procesarOffset(reg.codigo, 0x33, true);
} else if (tipo === 'set') {
const regSet = MAPA_REGISTROS.ESCRITURA[identificador];
const valRaw = obj.params.inputData[identificador];
// Longitud = ID(4) + Password(4) + Operator(4) + Data(N)
const len = 12 + regSet.bytes;
trama += `14${len.toString(16).padStart(2, '0').toUpperCase()}`;
trama += procesarOffset(regSet.codigo, 0x33, true);
// Autenticación por defecto (Nivel 2 + Password + Código Operador)
trama += "35555555C0C1C2C3";
// Valor a escribir con offset
trama += procesarOffset(valRaw, 0x33, true);
}
trama += calcularChecksum(trama);
trama += "16"; // Fin de trama
return Buffer.from(trama, 'hex').toString('base64');
}
Funciones Auxiliares de Utilidad
Estas funciones gestionan la manipulación de bytes y cálculos aritméticos específicos del protocolo DL/T 645.
function calcularChecksum(hexString) {
let suma = 0;
for (let i = 0; i < hexString.length; i += 2) {
suma += parseInt(hexString.substring(i, i + 2), 16);
}
return (suma % 256).toString(16).toUpperCase().padStart(2, '0');
}
function invertirBytes(hex) {
let grupos = hex.match(/.{2}/g) || [];
return grupos.reverse().join('');
}
function procesarOffset(hex, offset, invertir = false) {
let grupos = hex.match(/.{2}/g) || [];
let procesados = grupos.map(g => {
let n = (parseInt(g, 16) + offset) % 256;
if (n < 0) n += 256;
return n.toString(16).toUpperCase().padStart(2, '0');
});
return invertir ? procesados.reverse().join('') : procesados.join('');
}
Ejemplo de Respuesta del Medidor
Para un caso de lectura de tensión (Ua), la trama recibida tras la conversión de Base64 a Hexadecimal tendría una estructura similar a:
// Entrada (Propiedad Ua)
// Base64: aAAAmHAAAWiRBjMzOTXMu8sW
// Hex: 68 00 00 98 70 00 01 68 91 06 33 34 34 35 69 43 EC 16
// Resultado: { deviceName: "010070980000", data: 220.5, identifier: "Ua", method: "thing.service.property.get" }