diff --git a/decoders/connector/globalstar/smartone-c/assets/logo.png b/decoders/connector/globalstar/smartone-c/assets/logo.png new file mode 100644 index 00000000..26ee838b Binary files /dev/null and b/decoders/connector/globalstar/smartone-c/assets/logo.png differ diff --git a/decoders/connector/globalstar/smartone-c/connector.jsonc b/decoders/connector/globalstar/smartone-c/connector.jsonc new file mode 100644 index 00000000..be38161a --- /dev/null +++ b/decoders/connector/globalstar/smartone-c/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Globalstar SmartOne C", + "images": { + "logo": "./assets/logo.png" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.js", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/connector/globalstar/smartone-c/description.md b/decoders/connector/globalstar/smartone-c/description.md new file mode 100644 index 00000000..47c25a4a --- /dev/null +++ b/decoders/connector/globalstar/smartone-c/description.md @@ -0,0 +1 @@ +Tracks mobile assets, monitors engine run times, sends GPS, battery, and alarm data via satellite diff --git a/decoders/connector/globalstar/smartone-c/v1.0.0/payload-config.jsonc b/decoders/connector/globalstar/smartone-c/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..bfec9f81 --- /dev/null +++ b/decoders/connector/globalstar/smartone-c/v1.0.0/payload-config.jsonc @@ -0,0 +1,11 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "**SmartOne C** is a GPS tracking device for mobile assets such as trailers, cargo containers, heavy construction equipment, generators, and boats/barges. It features two dry contact inputs for monitoring engine run times and alarm inputs, and a serial port for connecting passive and smart sensors. The device processes GPS satellite signals to determine location (longitude and latitude) and transmits data via the Globalstar Satellite Network. It also sends messages including battery status, input alarm status, and diagnostic details. Configuration is done using a computer and USB configuration cable, with messages communicated at specific times or under certain conditions.", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/globalstar/v1.0.0/payload.ts" + ] +} diff --git a/decoders/connector/globalstar/smartone-c/v1.0.0/payload.test.ts b/decoders/connector/globalstar/smartone-c/v1.0.0/payload.test.ts new file mode 100644 index 00000000..1a9683e3 --- /dev/null +++ b/decoders/connector/globalstar/smartone-c/v1.0.0/payload.test.ts @@ -0,0 +1,74 @@ +import { describe, test, expect, beforeEach } from "vitest"; +import { readFileSync } from "fs"; +import { join } from "path"; +import { DataToSend } from "@tago-io/sdk/lib/types"; +import * as ts from "typescript"; + +const file = readFileSync(join(__dirname, "./payload.ts")); +const transpiledCode = ts.transpile(file.toString()); + +let payload: DataToSend[] = []; + +describe("SmartOne C Payload Validation", () => { + beforeEach(() => { + payload = []; + }); + + test("Check all output variables", () => { + payload = [{ variable: "globalstar_payload", value: "002B5372BFF12F0A02", unit: "", metadata: {} }]; + const result = eval(transpiledCode); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ variable: "globalstar_payload", value: "002B5372BFF12F0A02", unit: "", metadata: {} }), + expect.objectContaining({ variable: "battery_state", value: "Good" }), + expect.objectContaining({ variable: "gps_data_valid", value: "Valid" }), + expect.objectContaining({ variable: "missed_input_1", value: 0 }), + expect.objectContaining({ variable: "missed_input_2", value: 0 }), + expect.objectContaining({ variable: "gps_fail_counter", value: 0 }), + expect.objectContaining({ + variable: "location", + value: "30.46356439590454,-90.0813889503479", + location: { type: "Point", coordinates: [-90.0813889503479, 30.46356439590454] }, + }), + expect.objectContaining({ variable: "message_type", value: "Location Message" }), + ]) + ); + }); + + test("Check all output variables - location", () => { + payload = [{ variable: "globalstar_payload", value: "C02B5387BFF129090C", unit: "", metadata: {} }]; + const result = eval(transpiledCode); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ variable: "globalstar_payload", value: "C02B5387BFF129090C", unit: "", metadata: {} }), + expect.objectContaining({ variable: "battery_state", value: "Good" }), + expect.objectContaining({ variable: "gps_data_valid", value: "Valid" }), + expect.objectContaining({ variable: "missed_input_1", value: 0 }), + expect.objectContaining({ variable: "missed_input_2", value: 0 }), + expect.objectContaining({ variable: "gps_fail_counter", value: 3 }), + expect.objectContaining({ + variable: "location", + value: "30.463789701461792,-90.08151769638062", + location: { type: "Point", coordinates: [-90.08151769638062, 30.463789701461792] }, + }), + expect.objectContaining({ variable: "message_type", value: "Location Message" }), + ]) + ); + }); + + test("Shall not be parsed", () => { + payload = [{ variable: "shallnotpass", value: "invalid_payload" }]; + const result = eval(transpiledCode); + + expect(result).toEqual(undefined); + }); + + test("Invalid Payload", () => { + payload = [{ variable: "payload", value: "invalid_payload", unit: "", metadata: {} }]; + const result = eval(transpiledCode); + + expect(result).toEqual([{ variable: "parse_error", value: "Invalid payload size" }]); + }); +}); diff --git a/decoders/connector/globalstar/smartone-c/v1.0.0/payload.ts b/decoders/connector/globalstar/smartone-c/v1.0.0/payload.ts new file mode 100644 index 00000000..5509976d --- /dev/null +++ b/decoders/connector/globalstar/smartone-c/v1.0.0/payload.ts @@ -0,0 +1,99 @@ +import { Data, DataToSend } from "@tago-io/sdk/lib/types"; + +declare let payload: DataToSend[]; + +function calculateLatitude(lat: number): number { + const degreePerCountLat = 90.0 / Math.pow(2, 23); + let latitude = lat * degreePerCountLat; + + if (latitude > 90) { + latitude = 180 - latitude; + } + + return latitude; +} + +function calculateLongitude(long: number): number { + const degreePerCountLong = 180.0 / Math.pow(2, 23); + let longitude = long * degreePerCountLong; + + if (longitude > 180) { + longitude = longitude - 360; + } + + return longitude; +} + +interface DecodedData extends Pick {} + +function ParsePayload(payload: string, group: string | undefined, receivedTime: Date | string | undefined): DecodedData[] { + const buffer = Buffer.from(payload, "hex"); + // const buffer = payload as unknown as Buffer; + + if (buffer.length < 2) { + throw new Error("Invalid payload size"); + } + + const data: DecodedData[] = []; + const checkTime = receivedTime || new Date().toISOString(); + const time = new Date(checkTime); + + const batteryState = (buffer.readUInt8(0) & 0x04) >> 2; + const gpsDataValid = (buffer.readUInt8(0) & 0x08) >> 3; + const missedInput1 = (buffer.readUInt8(0) & 0x10) >> 4; + const missedInput2 = (buffer.readUInt8(0) & 0x20) >> 5; + const gpsFailCounter = (buffer.readUInt8(0) & 0xc0) >> 6; + + const latitude = calculateLatitude(buffer.readIntBE(1, 3)); + const longitude = calculateLongitude(buffer.readIntBE(4, 3)); + + data.push({ variable: "battery_state", value: batteryState ? "Replace" : "Good", group, time }); + data.push({ variable: "gps_data_valid", value: gpsDataValid ? "Invalid" : "Valid", group, time }); + data.push({ variable: "missed_input_1", value: missedInput1, group, time }); + data.push({ variable: "missed_input_2", value: missedInput2, group, time }); + data.push({ variable: "gps_fail_counter", value: gpsFailCounter, group, time }); + data.push({ variable: "location", value: `${latitude},${longitude}`, location: { type: "Point", coordinates: [longitude, latitude] }, group, time }); + + const messageSubtype = (buffer.readUInt8(7) & 0xf0) >> 4; + + switch (messageSubtype) { + case 0: + data.push({ variable: "message_type", value: "Location Message", group, time }); + break; + case 1: + data.push({ variable: "message_type", value: "Device Turned On Message", group, time }); + break; + case 2: + data.push({ variable: "message_type", value: "Change of Location Area Alert Message", group, time }); + break; + case 3: + data.push({ variable: "message_type", value: "Input Status Changed Message", group, time }); + break; + case 4: + data.push({ variable: "message_type", value: "Undesired Input State Message", group, time }); + break; + case 5: + data.push({ variable: "message_type", value: "Re-Center Message", group, time }); + break; + default: + data.push({ variable: "message_type", value: "Unknown", group, time }); + break; + } + + return data; +} + +// Handle Received Data +const payload_raw = payload.find((x) => ["payload_raw", "payload", "data", "globalstar_payload"].includes(x.variable)); + +if (payload_raw) { + try { + const parsedData = ParsePayload(payload_raw.value as string, payload_raw.group, payload_raw.time); + payload = payload.concat(parsedData); + } catch (e) { + // Print the error to the Live Inspector. + console.error(e); + // Return the variable parse_error for debugging. + payload = [{ variable: "parse_error", value: e.message }]; + } +} diff --git a/decoders/network/globalstar/assets/xpto.txt b/decoders/network/globalstar/assets/xpto.txt new file mode 100644 index 00000000..e69de29b diff --git a/decoders/network/globalstar/description.md b/decoders/network/globalstar/description.md new file mode 100644 index 00000000..e69de29b diff --git a/decoders/network/globalstar/network.jsonc b/decoders/network/globalstar/network.jsonc new file mode 100644 index 00000000..4c7fc40a --- /dev/null +++ b/decoders/network/globalstar/network.jsonc @@ -0,0 +1,15 @@ +{ + "$schema": "../../../schema/network.json", + "name": "KPN Things", + "images": { + "logo": "", + "icon": "", + "banner": "" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.ts", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/network/globalstar/v1.0.0/payload-config.jsonc b/decoders/network/globalstar/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..d1cd3181 --- /dev/null +++ b/decoders/network/globalstar/v1.0.0/payload-config.jsonc @@ -0,0 +1,12 @@ +{ + "$schema": "../../../../schema/network_details.json", + "description": "../description.md", + "serial_number_config": { + "required": true, + "label": "EUI / IMEI / UUID", + "case": "upper" + }, + "device_parameters": [], + "middleware_endpoint": "kpn.middleware.tago.io", + "documentation_url": "https://help.tago.io/portal/en/community/topic/how-to-integrate-with-kpn-things" +} diff --git a/decoders/network/globalstar/v1.0.0/payload.test.ts b/decoders/network/globalstar/v1.0.0/payload.test.ts new file mode 100644 index 00000000..cddc39a5 --- /dev/null +++ b/decoders/network/globalstar/v1.0.0/payload.test.ts @@ -0,0 +1,39 @@ +import { describe, test, expect, beforeEach } from "vitest"; +import { readFileSync } from "fs"; +import { join } from "path"; +import { DataToSend } from "@tago-io/sdk/lib/types"; +import * as ts from "typescript"; + +const file = readFileSync(join(__dirname, "./payload.ts")); +const transpiledCode = ts.transpile(file.toString()); + +let payload: DataToSend[] = []; + +describe("Globalstar Payload Validation", () => { + beforeEach(() => { + payload = [ + { + variable: "globalstar_payload", + value: + '[{"bn":"urn:dev:DVNUUID:737b588b-547e-45d3-96d0-f79723291bf1:","bt":1713193637,"n":"battery","u":"%","vs":"32.0"},{"n":"accelerationX","u":"m/s2","v":0.39},{"n":"accelerationY","u":"m/s2","v":3.64},{"n":"accelerationZ","u":"m/s2","v":9.46},{"n":"latitude","u":"lat","v":51.90725},{"n":"longitude","u":"lon","v":4.48934}]', + unit: "", + metadata: {}, + }, + ]; + }); + + test("Check all output variables for acceleration", () => { + const result = eval(transpiledCode); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ variable: "battery", value: "32.0", unit: "%", time: new Date(1713193637 * 1000) }), + expect.objectContaining({ variable: "accelerationX", value: 0.39, unit: "m/s2", time: new Date(1713193637 * 1000) }), + expect.objectContaining({ variable: "accelerationY", value: 3.64, unit: "m/s2", time: new Date(1713193637 * 1000) }), + expect.objectContaining({ variable: "accelerationZ", value: 9.46, unit: "m/s2", time: new Date(1713193637 * 1000) }), + expect.objectContaining({ variable: "latitude", value: 51.90725, unit: "lat", time: new Date(1713193637 * 1000) }), + expect.objectContaining({ variable: "longitude", value: 4.48934, unit: "lon", time: new Date(1713193637 * 1000) }), + ]) + ); + }); +}); diff --git a/decoders/network/globalstar/v1.0.0/payload.ts b/decoders/network/globalstar/v1.0.0/payload.ts new file mode 100644 index 00000000..b438523a --- /dev/null +++ b/decoders/network/globalstar/v1.0.0/payload.ts @@ -0,0 +1,27 @@ +import { DataToSend } from "@tago-io/sdk/lib/types"; + +declare let payload: DataToSend[]; + +/** + * @description Decodes the SenML payload + * @param {SenML[]} senMLObj - SenML object + */ +function decoder(senMLObj: any[]) { + // * to be implemented +} + +// Handle Received Data +const kpnPayload = payload.find((x) => x.variable === "globalstar_payload"); + +if (kpnPayload) { + try { + const contentJSON = JSON.parse(kpnPayload.value as string); + const parsedData = decoder(contentJSON); + // payload = parsedData; + } catch (error) { + // Print the error to the Live Inspector. + // console.error(error); + // Return the variable parse_error for debugging. + payload = [{ variable: "parse_error", value: error.message }]; + } +}