Spaces:
Paused
Paused
| function isObject(value) { | |
| return value !== null && typeof value === "object" && !Array.isArray(value); | |
| } | |
| function toNullableNumber(value) { | |
| if (value === null || value === undefined || value === "") { | |
| return null; | |
| } | |
| if (typeof value === "number" && Number.isFinite(value)) { | |
| return value; | |
| } | |
| if (typeof value === "string") { | |
| const parsed = Number(value); | |
| return Number.isFinite(parsed) ? parsed : null; | |
| } | |
| return null; | |
| } | |
| function toNullableBoolean(value) { | |
| if (value === null || value === undefined || value === "") { | |
| return null; | |
| } | |
| if (typeof value === "boolean") { | |
| return value; | |
| } | |
| if (typeof value === "number") { | |
| if (value === 1) { | |
| return true; | |
| } | |
| if (value === 0) { | |
| return false; | |
| } | |
| } | |
| if (typeof value === "string") { | |
| const lower = value.trim().toLowerCase(); | |
| if (["true", "1", "yes", "on"].includes(lower)) { | |
| return true; | |
| } | |
| if (["false", "0", "no", "off"].includes(lower)) { | |
| return false; | |
| } | |
| } | |
| return null; | |
| } | |
| function normalizeTimestamp(payload) { | |
| const raw = | |
| payload.sourceTs || | |
| payload.timestamp || | |
| payload.ts || | |
| payload.time || | |
| payload.occurredAt || | |
| null; | |
| if (raw === null) { | |
| return new Date(); | |
| } | |
| const date = new Date(raw); | |
| if (!Number.isNaN(date.getTime())) { | |
| return date; | |
| } | |
| const asNumber = Number(raw); | |
| if (Number.isFinite(asNumber)) { | |
| const millis = asNumber > 1e12 ? asNumber : asNumber * 1000; | |
| const fromMillis = new Date(millis); | |
| if (!Number.isNaN(fromMillis.getTime())) { | |
| return fromMillis; | |
| } | |
| } | |
| return new Date(); | |
| } | |
| function validateAndNormalizeTelemetry(topicInfo, payload) { | |
| const errors = []; | |
| if (!isObject(payload)) { | |
| return { | |
| valid: false, | |
| errors: ["Payload must be a JSON object"], | |
| normalized: null, | |
| }; | |
| } | |
| const env = isObject(payload.env) ? payload.env : {}; | |
| const gas = isObject(payload.gas) ? payload.gas : {}; | |
| const motion = isObject(payload.motion) ? payload.motion : {}; | |
| const gpsCandidate = isObject(payload.gps) | |
| ? payload.gps | |
| : isObject(payload.location) | |
| ? payload.location | |
| : {}; | |
| const statusCandidate = isObject(payload.status) | |
| ? payload.status | |
| : isObject(payload.system) | |
| ? payload.system | |
| : {}; | |
| const temperatureC = toNullableNumber(env.temperatureC); | |
| const gasRaw = toNullableNumber(gas.mq2Raw); | |
| const shock = toNullableBoolean(motion.shock); | |
| if (temperatureC === null) { | |
| errors.push("env.temperatureC must be a number"); | |
| } | |
| if (gasRaw === null) { | |
| errors.push("gas.mq2Raw must be a number"); | |
| } | |
| if (shock === null) { | |
| errors.push("motion.shock must be a boolean"); | |
| } | |
| if (payload.truckId && String(payload.truckId) !== String(topicInfo.truckCode)) { | |
| errors.push("Payload truckId does not match topic truck segment"); | |
| } | |
| if ( | |
| payload.containerId && | |
| String(payload.containerId) !== String(topicInfo.containerCode) | |
| ) { | |
| errors.push("Payload containerId does not match topic container segment"); | |
| } | |
| const normalized = { | |
| seq: Math.max(0, Math.trunc(toNullableNumber(payload.seq) || 0)), | |
| sourceTs: normalizeTimestamp(payload).toISOString(), | |
| gpsLat: toNullableNumber(gpsCandidate.lat), | |
| gpsLon: toNullableNumber(gpsCandidate.lon), | |
| speedKph: toNullableNumber(gpsCandidate.speedKph), | |
| temperatureC, | |
| humidityPct: toNullableNumber(env.humidityPct), | |
| pressureHpa: toNullableNumber(env.pressureHpa), | |
| tiltDeg: toNullableNumber(motion.tiltDeg), | |
| shock: Boolean(shock), | |
| gasRaw, | |
| gasAlert: Boolean(toNullableBoolean(gas.alert)), | |
| sdOk: toNullableBoolean(statusCandidate.sdOk), | |
| gpsFix: | |
| toNullableBoolean(statusCandidate.gpsFix) ?? | |
| toNullableBoolean(gpsCandidate.gpsFix) ?? | |
| toNullableBoolean(payload.gpsFix), | |
| uplink: statusCandidate.uplink ? String(statusCandidate.uplink) : "unknown", | |
| rawPayload: payload, | |
| }; | |
| return { | |
| valid: errors.length === 0, | |
| errors, | |
| normalized, | |
| }; | |
| } | |
| module.exports = { | |
| validateAndNormalizeTelemetry, | |
| }; | |