TTS Decoder — v1.3.0-preview
Experimental
This decoder requires unreleased Harvy2 RC firmware. Do not deploy to production devices. Source may change at any time.
Additions over stable v1.2.0:
- Port 2 — downlink response decoding (
key.getRPC) for Harvy2 RC firmware
// ############################################################
// _ .-') _ ('-. .-') _ ('-. _ .-')
// ( ( OO) ) _( OO) ( OO) ) _( OO) ( '.( OO )_
// \ .'_ (,------. ,(_)----. (,------. ,--. ,--.)
// ,`'--..._) | .---' | | | .---' | `.' |
// | | \ ' | | '--. / | | | |
// | | ' | (| '--. (_/ / (| '--. | |'.'| |
// | | / : | .--' / /___ | .--' | | | |
// | '--' / | `---. | | | `---. | | | |
// `-------' `------' `--------' `------' `--' `--'
// deZem GmbH - Harvy2 LoRaWAN Uplink Decoder
// ============================================================
// Platform : The Things Stack (TTS) / Chirpstack - ES5.1 compatible
// ------------------------------------------------------------
// Version : v1.3.0-preview (2026-04-22)
// Status : EXPERIMENTAL - requires Harvy2 RC firmware (unreleased).
// Decoder contents may change at any time without notice.
// ------------------------------------------------------------
// Decoder : port 2 (downlink response, e.g. key.get) -- REQUIRES RC FIRMWARE
// port 10 (measurement payload)
// port 30 (energy counter, kWh)
// port 31 (charge counter, Ah)
// port 42 (button press count)
// port 99 (reboot info)
// ------------------------------------------------------------
// Changelog: [v1.2.0...v1.3.0-preview]
// - add port 2 support (key.get RPC response decoding) for Harvy2 RC firmware
// ############################################################
// ============================================================
// Byte decode helpers (Little Endian, direct index access)
// ============================================================
function to_signed(val, bits) {
if ((val & (1 << (bits - 1))) > 0) {
var mask = Math.pow(2, bits) - 1;
val = (~val & mask) + 1;
val = val * -1;
}
return val;
}
function dec_u8(bytes, idx) {
return bytes[idx];
}
function dec_u16(bytes, idx) {
return (bytes[idx + 1] << 8) | bytes[idx];
}
function dec_u32(bytes, idx) {
return (bytes[idx + 3] << 24) | (bytes[idx + 2] << 16) | (bytes[idx + 1] << 8) | bytes[idx];
}
function dec_i8(bytes, idx) {
return to_signed(bytes[idx], 8);
}
function dec_i16(bytes, idx) {
return to_signed((bytes[idx + 1] << 8) | bytes[idx], 16);
}
function dec_i32(bytes, idx) {
return to_signed(dec_u32(bytes, idx), 32);
}
function dec_f16(bytes, idx) {
var h = (bytes[idx + 1] << 8) | bytes[idx];
var s = (h & 0x8000) >> 15;
var e = (h & 0x7c00) >> 10;
var f = h & 0x03ff;
if (e === 0) {
return (s ? -1 : 1) * 0.00006103515625 * (f / 1024.0);
}
if (e === 0x1f) {
return f ? NaN : (s ? -1 : 1) * Infinity;
}
return (s ? -1 : 1) * Math.pow(2, e - 15) * (1 + f / 1024.0);
}
function dec_f32(bytes, idx) {
var buf = new Uint8Array([bytes[idx], bytes[idx + 1], bytes[idx + 2], bytes[idx + 3]]);
return new DataView(buf.buffer).getFloat32(0, true);
}
function dec_f64(bytes, idx) {
var buf = new Uint8Array([
bytes[idx],
bytes[idx + 1],
bytes[idx + 2],
bytes[idx + 3],
bytes[idx + 4],
bytes[idx + 5],
bytes[idx + 6],
bytes[idx + 7]
]);
return new DataView(buf.buffer).getFloat64(0, true);
}
// ============================================================
// Type system — each type bundles its byte size and decoder
// ============================================================
// prettier-ignore
var TYPE_U8 = { size: 1, decode: function(b, o) { return dec_u8(b, o); } };
// prettier-ignore
var TYPE_U16 = { size: 2, decode: function(b, o) { return dec_u16(b, o); } };
// prettier-ignore
var TYPE_U32 = { size: 4, decode: function(b, o) { return dec_u32(b, o); } };
// prettier-ignore
var TYPE_I8 = { size: 1, decode: function(b, o) { return dec_i8(b, o); } };
// prettier-ignore
var TYPE_I16 = { size: 2, decode: function(b, o) { return dec_i16(b, o); } };
// prettier-ignore
var TYPE_I32 = { size: 4, decode: function(b, o) { return dec_i32(b, o); } };
// prettier-ignore
var TYPE_F16 = { size: 2, decode: function(b, o) { return dec_f16(b, o); } };
// prettier-ignore
var TYPE_F32 = { size: 4, decode: function(b, o) { return dec_f32(b, o); } };
// prettier-ignore
var TYPE_F64 = { size: 8, decode: function(b, o) { return dec_f64(b, o); } };
// ============================================================
// Shared helpers
// ============================================================
function num_to_fixed(x, digits) {
return Number(x.toFixed(digits));
}
function merge(target, source) {
for (var k in source) {
if (source.hasOwnProperty(k)) {
target[k] = source[k];
}
}
return target;
}
function applyFixedPrecision(res, channels) {
for (var i = 0; i < channels.length; i++) {
var name = channels[i][0];
var fixed = channels[i][5];
if (res[name] !== undefined) {
res[name] = num_to_fixed(res[name], fixed);
}
}
return res;
}
function sumIfDefined(channels, keys, sumKey) {
var sum = 0;
var hasAny = false;
for (var i = 0; i < keys.length; i++) {
if (channels[keys[i]] !== undefined) {
sum += channels[keys[i]];
hasAny = true;
}
}
if (hasAny) {
channels[sumKey] = sum;
}
}
function formatCounterElapsed(seconds) {
var days = Math.floor(seconds / (24 * 3600));
var hours = Math.floor((seconds % (24 * 3600)) / 3600);
var minutes = Math.floor((seconds % 3600) / 60);
var secs = seconds % 60;
return "[D:" + days + "|H:" + hours + "|M:" + minutes + "|S:" + secs + "]";
}
// ============================================================
// Generic channel parsing
// ============================================================
function parseBitFlags(bytes, offset, count, bitNames) {
var flags = {};
for (var i = 0; i < count; i++) {
for (var n = 0; n < 8; n++) {
var idx = i * 8 + n;
if (bitNames[idx]) {
flags[bitNames[idx]] = (bytes[offset + i] & (1 << n)) !== 0;
}
}
}
return flags;
}
// Channel tuple: [name, mask, type, scale, offset, fixed]
// 0 1 2 3 4 5
function parseChannels(bytes, offset, flags, channels) {
var result = {};
var idx = offset;
for (var i = 0; i < channels.length; i++) {
var ch = channels[i];
if (flags[ch[1]]) {
result[ch[0]] = ch[3] * ch[2].decode(bytes, idx) + ch[4];
idx += ch[2].size;
}
}
return result;
}
// ============================================================
// Channel definition helpers
// ============================================================
// ============================================================
// Port 2 — channel definitions + decoder
// ============================================================
// --- Port 2 key.get RPC response decoding ---
// Root command hash for "key.get"
var PORT2_ROOT_HASH = 0xc392;
// Pre-computed MurmurHash3-16 lookup: hash -> { name, type, size }
// Hashes from harvy2_downlink_protocol.md
var PORT2_KEY_REGISTRY = {
// dbstats
0x1b92: { name: "rogowski_mode", type: TYPE_U8 },
0x5999: { name: "rogowski_fft", type: TYPE_U8 },
0x037d: { name: "rogowski_ls", type: TYPE_U8 },
0x2a06: { name: "rog_noise_gate_A", type: TYPE_U8 },
0x4d8c: { name: "rog_snr_hi", type: TYPE_U8 },
0xb4d0: { name: "rog_snr_lo", type: TYPE_U8 },
0xd00a: { name: "rog_block_neg_P", type: TYPE_U8 },
0x6b81: { name: "ain_switch_on_delay", type: TYPE_U16 },
0xc853: { name: "lwan_downlink_timeout_sec", type: TYPE_U16 },
0x3711: { name: "ads_power_mode", type: TYPE_U8 },
0xeae0: { name: "usb_charge_only", type: TYPE_U8 },
0x7c85: { name: "counter.v0.mode", type: TYPE_U8 },
0x84c3: { name: "app.counter.reading.period", type: TYPE_U8 },
// app
0x96bf: { name: "app.dev_mode", type: TYPE_U8 },
0xd93a: { name: "app.power_save_mode", type: TYPE_U8 },
0xabbb: { name: "app.console_mode", type: TYPE_U8 },
0x2ff6: { name: "app.connection_mode", type: TYPE_U8 },
0xa0ba: { name: "app.log_level", type: TYPE_U8 },
// lorawan
0xca56: { name: "lorawan.adr_enable", type: TYPE_U8 },
0xc8a3: { name: "lorawan.dr_default", type: TYPE_U8 },
0x49cf: { name: "lorawan.dr_start", type: TYPE_U8 },
0x42f8: { name: "lorawan.dr_min", type: TYPE_U8 },
0x4426: { name: "lorawan.dr_max", type: TYPE_U8 },
0x7e3d: { name: "lorawan.confirmed", type: TYPE_U8 },
0x272d: { name: "lorawan.join_on_reboot", type: TYPE_U8 },
0xda01: { name: "lorawan.lorawan_region", type: TYPE_U8 },
0xb930: { name: "lorawan.tx_power", type: TYPE_I8 },
0x0175: { name: "lorawan.join_eui", type: null, size: 8 },
0x0d38: { name: "lorawan.app_key", type: null, size: 16 },
// measurement
0x3628: { name: "measurement.mode_id", type: TYPE_U8 },
0xd105: { name: "measurement.interval_sec", type: TYPE_U16 },
// ain
0x9a43: { name: "ain.ct_plus_mode", type: TYPE_U8 },
// ain.in1
0x89df: { name: "ain.in1.transmit", type: TYPE_U8 },
0x1cc6: { name: "ain.in1.harvesting", type: TYPE_U8 },
0xdea0: { name: "ain.in1.ct_type", type: TYPE_U8 },
0x6b12: { name: "ain.in1.phase", type: TYPE_U8 },
0x86d7: { name: "ain.in1.voltage_coeff", type: TYPE_F16 },
0xc6d8: { name: "ain.in1.ac_en", type: TYPE_U8 },
0xc0a1: { name: "ain.in1.dc_en", type: TYPE_U8 },
0x12e3: { name: "ain.in1.freq_en", type: TYPE_U8 },
0x9690: { name: "ain.in1.pow_factor_en", type: TYPE_U8 },
0xc494: { name: "ain.in1.scaled_mode", type: TYPE_U8 },
0x604f: { name: "ain.in1.voltage_mode", type: TYPE_U8 },
// ain.in2
0x183e: { name: "ain.in2.transmit", type: TYPE_U8 },
0xcfd1: { name: "ain.in2.harvesting", type: TYPE_U8 },
0x5037: { name: "ain.in2.ct_type", type: TYPE_U8 },
0xcbf8: { name: "ain.in2.phase", type: TYPE_U8 },
0x7d72: { name: "ain.in2.voltage_coeff", type: TYPE_F16 },
0xaf23: { name: "ain.in2.ac_en", type: TYPE_U8 },
0x5fbc: { name: "ain.in2.dc_en", type: TYPE_U8 },
0x8c55: { name: "ain.in2.freq_en", type: TYPE_U8 },
0x432c: { name: "ain.in2.pow_factor_en", type: TYPE_U8 },
0x2941: { name: "ain.in2.scaled_mode", type: TYPE_U8 },
0xc3cb: { name: "ain.in2.voltage_mode", type: TYPE_U8 },
// ain.in3
0x9d14: { name: "ain.in3.transmit", type: TYPE_U8 },
0x5b3d: { name: "ain.in3.harvesting", type: TYPE_U8 },
0x034f: { name: "ain.in3.ct_type", type: TYPE_U8 },
0x2fbc: { name: "ain.in3.phase", type: TYPE_U8 },
0x816d: { name: "ain.in3.voltage_coeff", type: TYPE_F16 },
0x110a: { name: "ain.in3.ac_en", type: TYPE_U8 },
0x27d9: { name: "ain.in3.dc_en", type: TYPE_U8 },
0x5e48: { name: "ain.in3.freq_en", type: TYPE_U8 },
0xbf98: { name: "ain.in3.pow_factor_en", type: TYPE_U8 },
0x32e0: { name: "ain.in3.scaled_mode", type: TYPE_U8 },
0x4bf1: { name: "ain.in3.voltage_mode", type: TYPE_U8 },
// ain.in4
0x19de: { name: "ain.in4.transmit", type: TYPE_U8 },
0x3431: { name: "ain.in4.harvesting", type: TYPE_U8 },
0xc8ce: { name: "ain.in4.ct_type", type: TYPE_U8 },
0x93cb: { name: "ain.in4.phase", type: TYPE_U8 },
0xd9c8: { name: "ain.in4.voltage_coeff", type: TYPE_F16 },
0x0168: { name: "ain.in4.ac_en", type: TYPE_U8 },
0xacad: { name: "ain.in4.dc_en", type: TYPE_U8 },
0x31e8: { name: "ain.in4.freq_en", type: TYPE_U8 },
0xd143: { name: "ain.in4.pow_factor_en", type: TYPE_U8 },
0xfe4f: { name: "ain.in4.scaled_mode", type: TYPE_U8 },
0xbcdc: { name: "ain.in4.voltage_mode", type: TYPE_U8 }
};
function decodePort2(bytes) {
if (bytes.length < 2) {
throw new Error("Port 2 payload too short: expected at least 2 bytes, got " + bytes.length);
}
var rootHash = bytes[0] | (bytes[1] << 8);
if (rootHash !== PORT2_ROOT_HASH) {
var rootHex = rootHash.toString(16);
while (rootHex.length < 4) rootHex = "0" + rootHex;
throw new Error("Port 2 unexpected root hash: 0x" + rootHex + " (expected 0xC392 key.get)");
}
var result = {};
var pos = 2;
while (pos + 2 <= bytes.length) {
var keyHash = bytes[pos] | (bytes[pos + 1] << 8);
pos += 2;
var reg = PORT2_KEY_REGISTRY[keyHash];
if (!reg) {
var unknownHex = keyHash.toString(16);
while (unknownHex.length < 4) unknownHex = "0" + unknownHex;
result._unknown_hash = "0x" + unknownHex;
break;
}
var valSize = reg.type ? reg.type.size : reg.size;
if (pos + valSize > bytes.length) {
result._truncated_key = reg.name;
break;
}
if (reg.type === null) {
var hex = "";
for (var j = 0; j < valSize; j++) {
var h = bytes[pos + j].toString(16).toUpperCase();
hex += h.length < 2 ? "0" + h : h;
}
result[reg.name] = hex;
} else {
result[reg.name] = reg.type.decode(bytes, pos);
}
pos += valSize;
}
return result;
}
// ============================================================
// Port 10 — channel definitions + decoder
// ============================================================
function makeBits_p10(prefix) {
return [prefix + "_ac_en", prefix + "_dc_en", prefix + "_freq_en", prefix + "_scaled_mode", prefix + "_voltage_mode", null, null, null];
}
function makeChannels_p10(prefix) {
return [
[prefix + "_ac_raw_A", prefix + "_ac_en", TYPE_F16, 1.0, 0, 6],
[prefix + "_dc_raw_A", prefix + "_dc_en", TYPE_F16, 1.0, 0, 6],
[prefix + "_freq", prefix + "_freq_en", TYPE_U16, 0.01, 0, 2],
[prefix + "_coeff", prefix + "_scaled_mode", TYPE_F16, 1.0, 0, 3]
];
}
var BITSET_P10 = [].concat(
["usb_powered", "ch_vsys_en", "ch_temp_en", null, null, null, null, null],
["ct_plus_mode", "rogowski_mode", null, null, null, null, null, null],
makeBits_p10("in1"),
makeBits_p10("in2"),
makeBits_p10("in3"),
makeBits_p10("in4")
);
var CHAN_SYSTEM_P10 = [
["vsys_V", "ch_vsys_en", TYPE_U8, 0.0075, 1.8, 3],
["temp_C", "ch_temp_en", TYPE_U8, 0.4, -22.0, 1]
];
var CHAN_PLUS_P10 = [
["in1_pow_factor", "ct_plus_mode", TYPE_F16, 1.0, 0, 4],
["in2_pow_factor", "ct_plus_mode", TYPE_F16, 1.0, 0, 4],
["in3_pow_factor", "ct_plus_mode", TYPE_F16, 1.0, 0, 4]
];
var CHANNELS_P10 = [].concat(
CHAN_SYSTEM_P10,
makeChannels_p10("in1"),
makeChannels_p10("in2"),
makeChannels_p10("in3"),
makeChannels_p10("in4"),
CHAN_PLUS_P10
);
var CHAN_POST_P10 = [
["in1_ac_A", null, null, 0, 0, 3],
["in2_ac_A", null, null, 0, 0, 3],
["in3_ac_A", null, null, 0, 0, 3],
["in4_ac_A", null, null, 0, 0, 3],
["in1_voltage_VAC", null, null, 0, 0, 2],
["in2_voltage_VAC", null, null, 0, 0, 2],
["in3_voltage_VAC", null, null, 0, 0, 2],
["in4_voltage_VAC", null, null, 0, 0, 2],
["sum_in1234_ac_A", null, null, 0, 0, 3],
["sum_in1234_dc_A", null, null, 0, 0, 3]
];
var CHAN_POST2_P10 = [
["in1_pow_app_VA", null, null, 0, 0, 2],
["in2_pow_app_VA", null, null, 0, 0, 2],
["in3_pow_app_VA", null, null, 0, 0, 2],
["in1_pow_act_W", null, null, 0, 0, 2],
["in2_pow_act_W", null, null, 0, 0, 2],
["in3_pow_act_W", null, null, 0, 0, 2],
["in1_pow_react_VAR", null, null, 0, 0, 2],
["in2_pow_react_VAR", null, null, 0, 0, 2],
["in3_pow_react_VAR", null, null, 0, 0, 2],
["sum_in123_pow_app_VA", null, null, 0, 0, 2],
["sum_in123_pow_act_W", null, null, 0, 0, 2],
["sum_in123_pow_react_VAR", null, null, 0, 0, 2]
];
var CHAN_ALL_P10 = [].concat(CHANNELS_P10, CHAN_POST_P10, CHAN_POST2_P10);
function decodePort10(bytes) {
if (bytes.length < 10) {
throw new Error("Port 10 payload too short: expected at least 10 bytes, got " + bytes.length);
}
var flags = parseBitFlags(bytes, 0, 6, BITSET_P10);
var channels = parseChannels(bytes, 10, flags, CHANNELS_P10);
var ct_plus_mode = flags.ct_plus_mode;
var rogowski_mode = flags.rogowski_mode;
var max_ac_scaled_A = rogowski_mode ? 10000 : 1000;
for (var ix = 1; ix <= 4; ix++) {
if (!flags["in" + ix + "_scaled_mode"]) continue;
var voltage_mode = flags["in" + ix + "_voltage_mode"];
var coeff = (channels["in" + ix + "_coeff"] || 1.0) * (voltage_mode ? 1000.0 : 1.0);
var ac_raw_A = channels["in" + ix + "_ac_raw_A"];
if (ac_raw_A !== undefined) {
var ac_scaled = ac_raw_A * coeff;
if (!voltage_mode && Math.abs(ac_scaled) > max_ac_scaled_A) {
throw new Error(
"Port 10 current limit exceeded: max " + max_ac_scaled_A + "A in " + (rogowski_mode ? "rogowski" : "normal") + " mode"
);
}
channels[voltage_mode ? "in" + ix + "_voltage_VAC" : "in" + ix + "_ac_A"] = ac_scaled;
}
var dc_raw_A = channels["in" + ix + "_dc_raw_A"];
if (dc_raw_A !== undefined) {
channels[voltage_mode ? "in" + ix + "_voltage_VDC" : "in" + ix + "_dc_A"] = dc_raw_A * coeff;
}
}
sumIfDefined(channels, ["in1_ac_A", "in2_ac_A", "in3_ac_A", "in4_ac_A"], "sum_in1234_ac_A");
sumIfDefined(channels, ["in1_dc_A", "in2_dc_A", "in3_dc_A", "in4_dc_A"], "sum_in1234_dc_A");
if (ct_plus_mode) {
var in4_voltage_VAC = channels.in4_voltage_VAC;
for (var j = 1; j <= 3; j++) {
channels["in" + j + "_pow_app_VA"] = in4_voltage_VAC * channels["in" + j + "_ac_A"];
}
for (var k = 1; k <= 3; k++) {
var app = channels["in" + k + "_pow_app_VA"];
var factor = Math.max(-1, Math.min(1, channels["in" + k + "_pow_factor"]));
var act = app * factor;
channels["in" + k + "_pow_act_W"] = act;
channels["in" + k + "_pow_react_VAR"] = Math.sqrt(app * app - act * act);
}
channels["sum_in123_pow_app_VA"] = channels.in1_pow_app_VA + channels.in2_pow_app_VA + channels.in3_pow_app_VA;
channels["sum_in123_pow_act_W"] = channels.in1_pow_act_W + channels.in2_pow_act_W + channels.in3_pow_act_W;
channels["sum_in123_pow_react_VAR"] = channels.in1_pow_react_VAR + channels.in2_pow_react_VAR + channels.in3_pow_react_VAR;
}
applyFixedPrecision(channels, CHAN_ALL_P10);
return merge({ flags: flags }, channels);
}
// ============================================================
// Port 30 — channel definitions + decoder
// ============================================================
function makeChannels_p30(prefix) {
return [
[prefix + "_kWh", prefix + "_power_en", TYPE_I32, 0.01, 0, 2],
[prefix + "_cosphi", prefix + "_cosphi_en", TYPE_F16, 1.0, 0, 3]
];
}
var BITSET_P30 = [].concat(
["in1_power_en", "in1_cosphi_en", null, null, null, null, null, null],
["in2_power_en", "in2_cosphi_en", null, null, null, null, null, null],
["in3_power_en", "in3_cosphi_en", null, null, null, null, null, null]
);
var CHANNELS_P30 = [].concat(
[["in_ctr_count", "dummy", TYPE_U32, 1.0, 0, 0]],
makeChannels_p30("in1"),
makeChannels_p30("in2"),
makeChannels_p30("in3")
);
var CHAN_POST_P30 = [["sum_in123_kWh", null, null, 0, 0, 2]];
var CHAN_ALL_P30 = [].concat(CHANNELS_P30, CHAN_POST_P30);
function decodePort30(bytes) {
if (bytes.length < 7) {
throw new Error("Port 30 payload too short: expected at least 7 bytes, got " + bytes.length);
}
var flags = parseBitFlags(bytes, 0, 3, BITSET_P30);
flags.dummy = true;
var channels = parseChannels(bytes, 3, flags, CHANNELS_P30);
sumIfDefined(channels, ["in1_kWh", "in2_kWh", "in3_kWh", "in4_kWh"], "sum_in123_kWh");
channels.in_ctr_time_elapse = formatCounterElapsed(channels.in_ctr_count);
applyFixedPrecision(channels, CHAN_ALL_P30);
return merge({ flags: flags }, channels);
}
// ============================================================
// Port 31 — channel definitions + decoder
// ============================================================
function makeChannels_p31(prefix) {
return [[prefix + "_Ah", prefix + "_amp_h_en", TYPE_I32, 0.01, 0, 2]];
}
var BITSET_P31 = [].concat(
["in1_amp_h_en", null, null, null, null, null, null, null],
["in2_amp_h_en", null, null, null, null, null, null, null],
["in3_amp_h_en", null, null, null, null, null, null, null],
["in4_amp_h_en", null, null, null, null, null, null, null]
);
var CHANNELS_P31 = [].concat(
[["in_ctr_count", "dummy", TYPE_U32, 1.0, 0, 0]],
makeChannels_p31("in1"),
makeChannels_p31("in2"),
makeChannels_p31("in3"),
makeChannels_p31("in4")
);
var CHAN_POST_P31 = [["sum_in1234_Ah", null, null, 0, 0, 2]];
var CHAN_ALL_P31 = [].concat(CHANNELS_P31, CHAN_POST_P31);
function decodePort31(bytes) {
if (bytes.length < 8) {
throw new Error("Port 31 payload too short: expected at least 8 bytes, got " + bytes.length);
}
var flags = parseBitFlags(bytes, 0, 4, BITSET_P31);
flags.dummy = true;
var channels = parseChannels(bytes, 4, flags, CHANNELS_P31);
sumIfDefined(channels, ["in1_Ah", "in2_Ah", "in3_Ah", "in4_Ah"], "sum_in1234_Ah");
channels.in_ctr_time_elapse = formatCounterElapsed(channels.in_ctr_count);
applyFixedPrecision(channels, CHAN_ALL_P31);
return merge({ flags: flags }, channels);
}
// ============================================================
// Port 42 + Port 99 — simple decoders
// ============================================================
function decodePort42(bytes) {
if (bytes.length !== 1) {
throw new Error("Port 42 payload size: expected 1 byte, got " + bytes.length);
}
return {
button_press_count: bytes[0]
};
}
function decodePort99(bytes) {
if (bytes.length < 7) {
throw new Error("Port 99 payload too short: expected at least 7 bytes, got " + bytes.length);
}
var result = {
reboot_counter: dec_u32(bytes, 0),
app_version_major: bytes[4],
app_version_minor: bytes[5],
app_version_patch: bytes[6],
app_version: bytes[4] + "." + bytes[5] + "." + bytes[6]
};
if (bytes.length >= 10) {
result.app_version_tweak = bytes[7];
result.app_version = bytes[4] + "." + bytes[5] + "." + bytes[6] + "." + bytes[7];
var hwVersionByte = bytes[8];
result.hw_version_patch = hwVersionByte & 0x07;
result.hw_version_minor = (hwVersionByte >> 3) & 0x07;
result.hw_version_major = (hwVersionByte >> 6) & 0x03;
result.hw_version = result.hw_version_major + "." + result.hw_version_minor + "." + result.hw_version_patch;
result.hw_variant = bytes[9];
var hex = bytes[9].toString(16).toUpperCase();
result.hw_variant_hex = "0x" + (hex.length < 2 ? "0" + hex : hex);
}
return result;
}
// ============================================================
// Entry points
// ============================================================
function decodePortX(bytes, port) {
switch (port) {
case 2:
return decodePort2(bytes);
case 10:
return decodePort10(bytes);
case 30:
return decodePort30(bytes);
case 31:
return decodePort31(bytes);
case 42:
return decodePort42(bytes);
case 99:
return decodePort99(bytes);
}
throw new Error("No decoder for port: " + port);
}
// Entry point for The Things Stack and ChirpStack v4.
// TTS passes { bytes, fPort }, ChirpStack v4 passes the same shape.
function decodeUplink(input) {
return {
data: decodePortX(input.bytes, input.fPort),
warnings: [],
errors: []
};
}
// Entry point for ChirpStack v3 (legacy "Custom JavaScript codec functions").
// ChirpStack v3 expects a global Decode(fPort, bytes, variables) function
// and returns the data object directly (no { data, warnings, errors } wrapper).
// eslint-disable-next-line no-unused-vars
function Decode(fPort, bytes, variables) {
return decodePortX(bytes, fPort);
}
// CommonJS export for Node.js test environment. Ignored by TTS/ChirpStack runtimes.
/* global module */
if (typeof module !== "undefined" && module.exports) {
module.exports = {
decodeUplink: decodeUplink,
Decode: Decode,
decodePortX: decodePortX,
decodePort2: decodePort2,
decodePort10: decodePort10,
decodePort30: decodePort30,
decodePort31: decodePort31,
decodePort42: decodePort42,
decodePort99: decodePort99,
dec_u8: dec_u8,
dec_u16: dec_u16,
dec_u32: dec_u32,
dec_i8: dec_i8,
dec_i16: dec_i16,
dec_i32: dec_i32,
dec_f16: dec_f16,
to_signed: to_signed,
parseBitFlags: parseBitFlags,
parseChannels: parseChannels,
sumIfDefined: sumIfDefined,
formatCounterElapsed: formatCounterElapsed,
num_to_fixed: num_to_fixed
};
}