diff --git a/lsn50-v2-xkc-y25-v-drainage/config/codec-production.js b/lsn50-v2-xkc-y25-v-drainage/config/codec-production.js new file mode 100644 index 0000000..398471e --- /dev/null +++ b/lsn50-v2-xkc-y25-v-drainage/config/codec-production.js @@ -0,0 +1,106 @@ +// ChirpStack v4+ Codec (JavaScript) +// LSN50 v2.x - Production codec for PA12 + Downlink control (TDC + 5VT) +// +// Verified mapping (uplink): PA12 = bytes[6] bit1 (0x02) on FPort 2 (MOD=1, 11 bytes) +// Downlink commands (Dragino common): +// - Set TDC (seconds): 0x01 + 3 bytes (big-endian) +// - Set 5VT (ms): 0x07 + 2 bytes (big-endian) + +const CFG = { + INVERT_LOGIC: false, + EXPECTED_UP_FPORT: 2, + EXPECTED_UP_LEN: 11, + CMD_FPORT: 2, + LIMITS: { + TDC_SEC_MIN: 5, + TDC_SEC_MAX: 86400, + VT_MS_MIN: 0, + VT_MS_MAX: 60000 + } +}; + +function decodeUplink(input) { + const b = input.bytes || []; + const fPort = input.fPort; + const rawHex = b.map(x => x.toString(16).padStart(2, "0")).join(""); + + const byte6 = (b.length > 6) ? b[6] : 0x00; + const pa12 = (byte6 & 0x02) ? 1 : 0; + const drenaje = CFG.INVERT_LOGIC ? (pa12 ? 0 : 1) : pa12; + + const warnings = []; + if (fPort !== CFG.EXPECTED_UP_FPORT) warnings.push(`Unexpected fPort=${fPort}, expected ${CFG.EXPECTED_UP_FPORT}`); + if (b.length !== CFG.EXPECTED_UP_LEN) warnings.push(`Unexpected payload length=${b.length}, expected ${CFG.EXPECTED_UP_LEN}`); + + return { + data: { + drenaje, + pa12, + byte6, + payload_len: b.length, + payload_hex: rawHex, + warnings: warnings.length ? warnings : undefined + } + }; +} + +function u16be(n) { + return [(n >> 8) & 0xff, n & 0xff]; +} + +function u24be(n) { + return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]; +} + +function assertInt(name, v) { + if (typeof v !== "number" || !isFinite(v) || Math.floor(v) !== v) { + throw new Error(`${name} must be an integer`); + } +} + +function clampInt(name, v, min, max) { + assertInt(name, v); + if (v < min || v > max) throw new Error(`${name} out of range [${min}, ${max}]: ${v}`); + return v; +} + +function encodeDownlink(input) { + const data = (input && input.data) ? input.data : {}; + const cmd = data.cmd; + + if (!cmd) { + return { bytes: [] }; + } + + if (cmd === "set_tdc") { + const tdc = clampInt("tdc_sec", data.tdc_sec, CFG.LIMITS.TDC_SEC_MIN, CFG.LIMITS.TDC_SEC_MAX); + const bytes = [0x01].concat(u24be(tdc)); + return { + fPort: CFG.CMD_FPORT, + bytes + }; + } + + if (cmd === "set_5vt") { + const vt = clampInt("vt_ms", data.vt_ms, CFG.LIMITS.VT_MS_MIN, CFG.LIMITS.VT_MS_MAX); + const bytes = [0x07].concat(u16be(vt)); + return { + fPort: CFG.CMD_FPORT, + bytes + }; + } + + if (cmd === "profile_active") { + const vt = clampInt("vt_ms", (data.vt_ms ?? 2000), CFG.LIMITS.VT_MS_MIN, CFG.LIMITS.VT_MS_MAX); + const bytes = [0x07].concat(u16be(vt)); + return { fPort: CFG.CMD_FPORT, bytes }; + } + + if (cmd === "profile_sleep") { + const vt = clampInt("vt_ms", (data.vt_ms ?? 0), CFG.LIMITS.VT_MS_MIN, CFG.LIMITS.VT_MS_MAX); + const bytes = [0x07].concat(u16be(vt)); + return { fPort: CFG.CMD_FPORT, bytes }; + } + + throw new Error(`Unknown cmd: ${cmd}`); +} \ No newline at end of file