UNPKG

@coremarine/nmea-parser

Version:
1,117 lines (1,106 loc) 38.2 kB
// src/checksum.ts var calculateChecksum = (data) => Array.from(data).reduce((acc, cur) => acc ^ cur.charCodeAt(0), 0); var stringChecksumToNumber = (checksum) => Number.parseInt(checksum, 16); var numberChecksumToString = (checksum) => checksum.toString(16).padStart(2, "0").toUpperCase(); var getChecksum = (cs) => ({ sample: cs, value: stringChecksumToNumber(cs) }); // src/constants.ts var FIELD_TYPES = [ // Unsigned Integers "uint8", // 'char', "uint16", // 'unsigned short', "uint32", // 'unsigned int', "uint64", // 'unsigned long', // Integers "int8", // 'signed char', "int16", // 'short', "int32", // 'int', "int64", // 'long', // Floats "float32", // 'float', "float64", // 'double', // Strings "string", // Boolean "boolean" // 'bool' ]; var START_FLAG = "$"; var SEPARATOR = ","; var DELIMITER = "*"; var END_FLAG = "\r\n"; var START_FLAG_LENGTH = START_FLAG.length; var SEPARATOR_LENGTH = SEPARATOR.length; var DELIMITER_LENGTH = DELIMITER.length; var CHECKSUM_LENGTH = 2; var END_FLAG_LENGTH = END_FLAG.length; var MINIMAL_LENGTH = START_FLAG_LENGTH + DELIMITER_LENGTH + CHECKSUM_LENGTH + END_FLAG_LENGTH; var NMEA_ID_LENGTH = 3; var NMEA_TALKER_LENGTH = 2; var NMEA_SENTENCE_LENGTH = NMEA_ID_LENGTH + NMEA_TALKER_LENGTH; var TALKERS = [ ["AB", "Independent AIS Base Station"], ["AD", "Dependent AIS Base Station"], ["AG", "Autopilot - General"], ["AI", "Mobile AIS Station"], ["AN", "AIS Aid to Navigation"], ["AP", "Autopilot - Magnetic"], ["AR", "AIS Receiving Station"], ["AT", "AIS Transmitting Station"], ["AX", "AIS Simplex Repeater"], ["BD", "BeiDou (China)"], ["BI", "Bilge System"], ["BN", "Bridge navigational watch alarm system"], ["CA", "Central Alarm"], ["CC", "Computer - Programmed Calculator (obsolete)"], ["CD", "Communications - Digital Selective Calling (DSC)"], ["CM", "Computer - Memory Data (obsolete)"], ["CR", "Data Receiver"], ["CS", "Communications - Satellite"], ["CT", "Communications - Radio-Telephone (MF/HF)"], ["CV", "Communications - Radio-Telephone (VHF)"], ["CX", "Communications - Scanning Receiver"], ["DE", "DECCA Navigation (obsolete)"], ["DF", "Direction Finder"], ["DM", "Velocity Sensor, Speed Log, Water, Magnetic"], ["DP", "Dynamiv Position"], ["DU", "Duplex repeater station"], ["EC", "Electronic Chart Display & Information System (ECDIS)"], ["EP", "Emergency Position Indicating Beacon (EPIRB)"], ["ER", "Engine Room Monitoring Systems"], ["FD", "Fire Door"], ["FS", "Fire Sprinkler"], ["GA", "Galileo Positioning System"], ["GB", "BeiDou (China)"], ["GI", "NavIC, IRNSS (India)"], ["GL", "GLONASS, according to IEIC 61162-1"], ["GN", "Combination of multiple satellite systems (NMEA 1083)"], ["GP", "Global Positioning System receiver"], ["GQ", "QZSS regional GPS augmentation system (Japan)"], ["HC", "Heading - Magnetic Compass"], ["HD", "Hull Door"], ["HE", "Heading - North Seeking Gyro"], ["HF", "Heading - Fluxgate"], ["HN", "Heading - Non North Seeking Gyro"], ["HS", "Hull Stress"], ["II", "Integrated Instrumentation"], ["IN", "Integrated Navigation"], ["JA", "Alarm and Monitoring"], ["JB", "Water Monitoring"], ["JC", "Power Management"], ["JD", "Propulsion Control"], ["JE", "Engine Control"], ["JF", "Propulsion Boiler"], ["JG", "Aux Boiler"], ["JH", "Engine Governor"], ["LA", "Loran A (obsolete)"], ["LC", "Loran C (obsolete)"], ["MP", "Microwave Positioning System (obsolete)"], ["MX", "Multiplexer"], ["NL", "Navigation light controller"], ["OM", "OMEGA Navigation System (obsolete)"], ["OS", "Distress Alarm System (obsolete)"], ["QZ", "QZSS regional GPS augmentation system (Japan)"], ["RA", "RADAR and/or ARPA"], ["RB", "Record Book"], ["RC", "Propulsion Machinery"], ["RI", "Rudder Angle Indicator"], ["SA", "Physical Shore AUS Station"], ["SD", "Depth Sounder"], ["SG", "Steering Gear"], ["SN", "Electronic Positioning System, other/general"], ["SS", "Scanning Sounder"], ["ST", "Skytraq debug output"], ["TC", "Track Control"], ["TI", "Turn Rate Indicator"], ["TR", "TRANSIT Navigation System"], ["UP", "Microprocessor controller"], ["VA", "VHF Data Exchange System (VDES), ASM"], ["VD", "Velocity Sensor, Doppler, other/general"], ["VM", "Velocity Sensor, Speed Log, Water, Magnetic"], ["VR", "Voyage Data recorder"], ["VS", "VHF Data Exchange System (VDES), Satellite"], ["VT", "VHF Data Exchange System (VDES), Terrestrial"], ["VW", "Velocity Sensor, Speed Log, Water, Mechanical"], ["WD", "Watertight Door"], ["WI", "Weather Instruments"], ["WL", "Water Level"], ["YC", "Transducer - Temperature (obsolete)"], ["YD", "Transducer - Displacement, Angular or Linear (obsolete)"], ["YF", "Transducer - Frequency (obsolete)"], ["YL", "Transducer - Level (obsolete)"], ["YP", "Transducer - Pressure (obsolete)"], ["YR", "Transducer - Flow Rate (obsolete)"], ["YT", "Transducer - Tachometer (obsolete)"], ["YV", "Transducer - Volume (obsolete)"], ["YX", "Transducer"], ["ZA", "Timekeeper - Atomic Clock"], ["ZC", "Timekeeper - Chronometer"], ["ZQ", "Timekeeper - Quartz"], ["ZV", "Timekeeper - Radio Update, WWV or WWVH"] ]; var TALKERS_SPECIAL = { P: "Vendor specific", U: "U# where '#' is a digit 0 \u2026\u200B 9; User Configured" }; var CODE = { A: "A".charCodeAt(0), Z: "Z".charCodeAt(0), a: "a".charCodeAt(0), z: "z".charCodeAt(0), 0: "0".charCodeAt(0), 9: "9".charCodeAt(0) }; var UINT8_MAX = Uint8Array.from([255])[0]; var UINT16_MAX = Uint16Array.from([65535])[0]; var UINT32_MAX = Uint32Array.from([4294967295])[0]; var [INT8_MIN, INT8_MAX] = Int8Array.from([128, 127]); var [INT16_MIN, INT16_MAX] = Int16Array.from([32768, 32767]); var [INT32_MIN, INT32_MAX] = Int32Array.from([2147483648, 2147483647]); var MAX_FLOAT = 999999999999999; var MIN_FLOAT = -999999999999999; var MAX_CHARACTERS = 1024; var MAX_NMEA_CHARACTERS = 82; var UNKNOWN_NMEA_SENTENCE_SCAFOLDING = { id: "unknown", protocol: { name: "unknown", standard: false }, description: "unknown nmea sentence" }; // src/nmea-sentences.ts var NMEA_SENTENCES = { protocols: [ { protocol: "NMEA", version: "3.1", standard: true, sentences: [ { id: "AAM", description: "Waypoint Arrival Alarm", payload: [ { name: "status", type: "string", description: "BOOLEAN\n\nA = arrival circle entered\n\nV = arrival circle not passed" }, { name: "status", type: "string", description: "BOOLEAN\n\nA = perpendicular passed at waypoint\n\nV = perpendicular not passed" }, { name: "arrival_circle_radius", type: "float32" }, { name: "radius_units", type: "string", units: "nautic miles" }, { name: "waypoint_id", type: "string" } ] }, { id: "GGA", description: "Global Positioning System Fix Data", payload: [ { name: "utc_position", type: "string", units: "ms" }, { name: "latitude", type: "string", units: "deg-min" }, { name: "latitude_direction", type: "string", description: "N: North\n S: South" }, { name: "longitude", type: "string", units: "deg-min" }, { name: "longitude_direction", type: "string", description: "E - East\n W - West" }, { name: "quality", type: "int8", description: "0: Fix not valid\n 1: GPS fix\n 2: Differential GPS fix (DGNSS), SBAS, OmniSTAR VBS, Beacon, RTX in GVBS mode\n 3: Not applicable\n 4: RTK Fixed, xFill\n 5: RTK Float, OmniSTAR XP/HP, Location RTK, RTX\n 6: INS Dead reckoning\n 7: Manual Input Mode\n 8: Simulator Mode" }, { name: "satellites", type: "uint8" }, { name: "hdop", type: "float64" }, { name: "altitude", type: "float64", units: "m", description: "Orthometric height Mean-Sea-Level (MSL reference)" }, { name: "altitude_units", type: "string", units: "m" }, { name: "geoid_separation", type: "float64", units: "m", description: 'Geoidal Separation: the difference between the WGS-84 earth ellipsoid surface and mean-sea-level (geoid) surface, "-" = mean-sea-level surface below WGS-84 ellipsoid surface.' }, { name: "geoid_separation_units", type: "string", units: "m" }, { name: "age_of_differential_gps_data", type: "uint32", units: "sec", description: "Time in seconds since last SC104 Type 1 or 9 update, null field when DGPS is not used300" }, { name: "reference_station_id", type: "uint16", description: "Reference station ID, range 0000 to 4095. A null field when any reference station ID is selected and no corrections are received. See table below for a description of the field values.\n\n0002 CenterPoint or ViewPoint RTX\n\n0005 RangePoint RTX\n\n0006 FieldPoint RTX\n\n0100 VBS\n\n1000 HP\n\n1001 HP/XP (Orbits)\n\n1002 HP/G2 (Orbits)\n\n1008 XP (GPS)\n\n1012 G2 (GPS)\n\n1013 G2 (GPS/GLONASS)\n\n1014 G2 (GLONASS)\n\n1016 HP/XP (GPS)\n\n1020 HP/G2 (GPS)\n\n1021 HP/G2 (GPS/GLONASS)" } ] }, { id: "HDT", description: "Heading - True", payload: [ { name: "heading", type: "float32", description: "Heading, degrees True" }, { name: "true", type: "string", description: "T = True" } ] }, { id: "ZDA", description: "Time & Date - UTC, day, month, year and local time zone", payload: [ { name: "utc_time", type: "string", description: "UTC time (hours, minutes, seconds, may have fractional subseconds)" }, { name: "day", type: "int8", description: "Day, 01 to 31" }, { name: "month", type: "int8", description: "Month, 01 to 12" }, { name: "year", type: "int16", description: "Year (4 digits)" }, { name: "local_zone_hours", type: "int8", description: "Local zone description, 00 to +- 13 hours" }, { name: "local_zone_minutes", type: "int8", description: "Local zone minutes description, 00 to 59, apply same sign as local hours" } ] } ] } ] }; // src/protocols.ts import yaml from "js-yaml"; import fs from "node:fs"; // src/schemas.ts import { Float32Schema as ValibotFloat32Schema, Float64Schema as ValibotFloat64Schema, Int16Schema as ValibotInt16Schema, Int32Schema as ValibotInt32Schema, Int8Schema as ValibotInt8Schema, IntegerSchema as ValibotIntegerSchema, Uint16Schema as ValibotUint16Schema, Uint32Schema as ValibotUint32Schema, Uint8Schema as ValibotUint8Schema, UnsignedIntegerSchema as ValibotUnsignedIntegerSchema } from "@schemasjs/valibot-numbers"; import { ValibotValidator } from "@schemasjs/validator"; import * as v from "valibot"; var ValibotStringSchema = v.string(); var StringSchema = ValibotValidator(ValibotStringSchema); var ValibotStringArraySchema = v.array(ValibotStringSchema); var StringArraySchema = ValibotValidator(ValibotStringArraySchema); var ValibotBooleanSchema = v.boolean(); var BooleanSchema = ValibotValidator(ValibotBooleanSchema); var ValibotNumberSchema = v.number(); var NumberSchema = ValibotValidator(ValibotNumberSchema); var IntegerSchema = ValibotValidator(ValibotIntegerSchema); var Int8Schema = ValibotValidator(ValibotInt8Schema); var Int16Schema = ValibotValidator(ValibotInt16Schema); var Int32Schema = ValibotValidator(ValibotInt32Schema); var ValibotInt64Schema = v.bigint(); var Int64Schema = ValibotValidator(ValibotInt64Schema); var UnsignedIntegerSchema = ValibotValidator(ValibotUnsignedIntegerSchema); var Uint8Schema = ValibotValidator(ValibotUint8Schema); var Uint16Schema = ValibotValidator(ValibotUint16Schema); var Uint32Schema = ValibotValidator(ValibotUint32Schema); var ValibotUint64Schema = v.pipe(v.bigint(), v.minValue(0n)); var Uint64Schema = ValibotValidator(ValibotUint64Schema); var Float32Schema = ValibotValidator(ValibotFloat32Schema); var Float64Schema = ValibotValidator(ValibotFloat64Schema); var ValibotProtocolFieldTypeSchema = v.picklist(FIELD_TYPES, "invalid type"); var ProtocolFieldTypeSchema = ValibotValidator(ValibotProtocolFieldTypeSchema); var ValibotProtocolFieldSchema = v.object({ name: ValibotStringSchema, type: ValibotProtocolFieldTypeSchema, units: v.optional(ValibotStringSchema), description: v.optional(ValibotStringSchema) }); var ProtocolFieldSchema = ValibotValidator(ValibotProtocolFieldSchema); var ValibotProtocolSentencePayloadSchema = v.array(ValibotProtocolFieldSchema, "invalid payload"); var ProtocolSentencePayloadSchema = ValibotValidator(ValibotProtocolSentencePayloadSchema); var ValibotProtocolSentenceSchema = v.object({ id: ValibotStringSchema, payload: ValibotProtocolSentencePayloadSchema, description: v.optional(ValibotStringSchema) }); var ProtocolSentenceSchema = ValibotValidator(ValibotProtocolSentenceSchema); var ValibotMaxThreeFields = v.check((input) => input.split(".").length < 4, "VersionSchema: more than 3 fields"); var ValibotValidMajor = v.check((val) => { const major = Number(val.split(".")[0]); return !Number.isNaN(major) && major > 0; }, "VersionSchema: Invalid major"); var ValibotValidMinor = v.check((val) => { const fields = val.split("."); if (fields.length < 2) return true; const minor = Number(fields[1]); return !Number.isNaN(minor) && minor > 0; }, "VersionSchema: Invalid major"); var ValibotValidPatch = v.check((val) => { const fields = val.split("."); if (fields.length < 3) return true; const patch = Number.parseInt(fields[2]); return !Number.isNaN(patch) && patch > 0; }, "VersionSchema: Invalid patch"); var ValibotVersionSchema = v.pipe( v.custom((val) => v.is(ValibotStringSchema, val)), ValibotMaxThreeFields, ValibotValidMajor, ValibotValidMinor, ValibotValidPatch ); var VersionSchema = ValibotValidator(ValibotVersionSchema); var ValibotProtocolSchema = v.object({ protocol: ValibotStringSchema, version: v.optional(ValibotStringSchema), standard: v.optional(ValibotBooleanSchema, false), sentences: v.array(ValibotProtocolSentenceSchema) }); var ProtocolSchema = ValibotValidator(ValibotProtocolSchema); var ValibotProtocolsFileContentSchema = v.object({ protocols: v.array(ValibotProtocolSchema) }); var ProtocolsFileContentSchema = ValibotValidator(ValibotProtocolsFileContentSchema); var ValibotProtocolsInputSchema = v.object({ file: v.optional(ValibotStringSchema), content: v.optional(ValibotStringSchema), protocols: v.optional(v.array(ValibotProtocolSchema)) }); var ProtocolsInputSchema = ValibotValidator(ValibotProtocolsInputSchema); var ValibotStoredSentenceSchema = v.object({ id: ValibotStringSchema, protocol: v.object({ name: ValibotStringSchema, standard: v.optional(ValibotBooleanSchema, false), version: v.optional(ValibotStringSchema) }), payload: v.array(ValibotProtocolFieldSchema), description: v.optional(ValibotStringSchema) }); var StoredSentenceSchema = ValibotValidator(ValibotStoredSentenceSchema); var ValibotMapStoredSentencesSchema = v.map(ValibotStringSchema, ValibotStoredSentenceSchema); var MapStoredSentencesSchema = ValibotValidator(ValibotMapStoredSentencesSchema); var ValibotJSONSchemaInputSchema = v.object({ path: v.optional(ValibotStringSchema), filename: v.optional(ValibotStringSchema, "nmea_protocols_schema.json") }); var JSONSchemaInputSchema = ValibotValidator(ValibotJSONSchemaInputSchema); var ValibotChecksumSchema = v.object({ sample: ValibotStringSchema, value: ValibotUint8Schema }); var ChecksumSchema = ValibotValidator(ValibotChecksumSchema); var ValibotValueSchema = v.union([ValibotStringSchema, ValibotBooleanSchema, ValibotNumberSchema, v.bigint(), v.null()], "invalid value"); var ValueSchema = ValibotValidator(ValibotValueSchema); var ValibotTalkerSchema = v.object({ value: ValibotStringSchema, description: ValibotStringSchema }); var TalkerSchema = ValibotValidator(ValibotTalkerSchema); var ValibotNMEALikeSchema = v.custom((input) => { if (typeof input !== "string") { return false; } if (!input.startsWith(START_FLAG)) { return false; } if (!input.endsWith(END_FLAG)) { return false; } const parts = input.split(DELIMITER); if (parts.length !== 2) { return false; } const [info, cs] = parts; if (cs.length !== CHECKSUM_LENGTH + END_FLAG.length) { return false; } const checksum = cs.slice(0, CHECKSUM_LENGTH); const numChecksum = stringChecksumToNumber(checksum); if (!v.safeParse(ValibotUint8Schema, numChecksum).success) { return false; } const data = info.slice(START_FLAG.length); if (data.length < NMEA_SENTENCE_LENGTH) { return false; } return info.includes(SEPARATOR); }); var NMEALikeSchema = ValibotValidator(ValibotNMEALikeSchema); var ValibotNMEAParsedFieldSchema = v.object({ name: v.optional(v.string("payload name bad"), "unknown"), sample: v.string("payload sample bad"), value: ValibotValueSchema, type: v.optional(v.union([v.picklist(FIELD_TYPES), v.literal("unknown")], "payload type bad"), "unknown"), units: v.optional(v.string("payload units bad"), "unknown"), description: v.optional(v.string("payload description bad")), metadata: v.optional(v.any()) }); var NMEAParsedFieldchema = ValibotValidator(ValibotNMEAParsedFieldSchema); var ValibotNMEAParsedPayloadSchema = v.array(ValibotNMEAParsedFieldSchema); var NMEAParsedPayloadSchema = ValibotValidator(ValibotNMEAParsedPayloadSchema); var ValibotNMEASentenceSchema = v.object({ received: ValibotUnsignedIntegerSchema, sample: ValibotNMEALikeSchema, id: ValibotStringSchema, description: v.optional(ValibotStringSchema), checksum: ValibotChecksumSchema, payload: v.array(ValibotNMEAParsedFieldSchema), metadata: v.optional(v.any()), protocol: v.optional( v.object({ name: ValibotStringSchema, standard: ValibotBooleanSchema, version: v.optional(ValibotStringSchema) }), { name: "unknown", standard: false } ), talker: v.optional(ValibotTalkerSchema) }); var NMEASentenceSchema = ValibotValidator(ValibotNMEASentenceSchema); // src/protocols.ts var readProtocolsYAMLString = (content) => { const fileData = yaml.load(content); const parsed = ProtocolsFileContentSchema.safeParse(fileData); if (!parsed.success) { throw new Error(parsed.errors?.toString()); } return parsed.data; }; var readProtocolsYAMLFile = (file) => { const filename = StringSchema.parse(file); const content = fs.readFileSync(filename, "utf-8"); return readProtocolsYAMLString(content); }; var getStoreSentencesFromProtocol = (protocol) => { const { protocol: name, standard, version, sentences } = protocol; const storedSentences = /* @__PURE__ */ new Map(); sentences.forEach((element) => { const obj = { id: element.id, payload: element.payload, protocol: { name, standard, version }, description: element?.description }; storedSentences.set(element.id, obj); }); return storedSentences; }; var getStoreSentences = ({ protocols }) => { let storedSentences = /* @__PURE__ */ new Map(); protocols.forEach((protocol) => { storedSentences = new Map([...storedSentences, ...getStoreSentencesFromProtocol(protocol)]); }); return storedSentences; }; // src/sentences.ts import crypto from "node:crypto"; // src/utils.ts var isBoundedASCII = (char, min, max) => { const num = char.charCodeAt(0); return min <= num && num <= max; }; var isLowerCharASCII = (char) => isBoundedASCII(char, CODE.a, CODE.z); var isUpperCharASCII = (char) => isBoundedASCII(char, CODE.A, CODE.Z); var isNumberCharASCII = (char) => isBoundedASCII(char, CODE["0"], CODE["9"]); // src/nmea-metadata.ts var metadataGGA = (sentence) => { const getUTCPosition = ( /** * Description placeholder * * @param {string} utcPosition hhmmss.ss where hh hours, mm minutes and ss.ss seconds * @returns {Uint32 | null} */ (utcPosition) => { if (utcPosition.length !== 9) { return null; } if (isNaN(Number(utcPosition))) { return null; } const hours = Number(utcPosition.slice(0, 2)); const minutes = Number(utcPosition.slice(2, 4)); const seconds = Number(utcPosition.slice(4, 6)); const millis = Number(utcPosition.slice(7)); const date = /* @__PURE__ */ new Date(); date.setHours(hours, minutes, seconds, millis); return date.getTime(); } ); const getLatitudeDegrees = (latitude, letter) => { const [left, minutesRight] = latitude.split("."); const degrees = left.slice(0, -2); const minutesLeft = left.slice(-2); const sign = letter === "S" ? -1 : 1; const minutes = `${minutesLeft}.${minutesRight}`; return sign * (Number(degrees) + Number(minutes) / 60); }; const getLongitudeDegrees = (longitude, letter) => { const [left, minutesRight] = longitude.split("."); const degrees = left.slice(0, -2); const minutesLeft = left.slice(-2); const sign = letter === "W" ? -1 : 1; const minutes = `${minutesLeft}.${minutesRight}`; return sign * (Number(degrees) + Number(minutes) / 60); }; const getQuality = (quality) => { const QUALITIES = { 0: "Fix not valid", 1: "GPS fix", 2: "Differential GPS fix (DGNSS), SBAS, OmniSTAR VBS, Beacon, RTX in GVBS mode", 3: "Not applicable", 4: "RTK Fixed, xFill", 5: "RTK Float, OmniSTAR XP/HP, Location RTK, RTX", 6: "INS Dead reckoning", 7: "Manual Input Mode", 8: "Simulator Mode" }; return QUALITIES[quality] ?? "unknown"; }; sentence.payload.forEach((field, index) => { if (field.name === "utc_position") { const utcPosition = field.value; const timestamp = getUTCPosition(utcPosition); if (timestamp !== null) { sentence.payload[index].metadata = { timestamp }; sentence.metadata = { ...sentence.metadata, timestamp }; } } if (field.name === "latitude") { const latitude = field.value; const letter = sentence.payload[index + 1].value; const degrees = getLatitudeDegrees(latitude, letter); sentence.payload[index].metadata = { degrees }; sentence.metadata = { ...sentence.metadata, latitude: degrees }; return; } if (field.name === "longitude") { const longitude = field.value; const letter = sentence.payload[index + 1].value; const degrees = getLongitudeDegrees(longitude, letter); sentence.payload[index].metadata = { degrees }; sentence.metadata = { ...sentence.metadata, longitude: degrees }; return; } if (field.name === "quality") { sentence.metadata = { ...sentence.metadata, quality: getQuality(field.value) }; } }); return { ...sentence }; }; var METADATA = { GGA: metadataGGA }; var addMetadata = (sentence) => { if (sentence.id in METADATA) { return METADATA[sentence.id](sentence); } return sentence; }; // src/sentences.ts var lastUncompletedFrame = (text) => { const lastStartIndex = text.lastIndexOf(START_FLAG); if (lastStartIndex === -1) { return null; } const remainder = text.slice(lastStartIndex); if (remainder.includes(END_FLAG)) { return null; } return remainder; }; var getUnparsedNMEAFrames = (text) => { if ([START_FLAG, SEPARATOR, DELIMITER, END_FLAG].some((flag) => !text.includes(flag))) { return []; } return text.split(END_FLAG).filter((str) => str.length > MINIMAL_LENGTH).filter((str) => str.includes(START_FLAG)).map((str) => str.split(START_FLAG).at(-1)).filter((str) => { const first = str.indexOf(DELIMITER); const last = str.lastIndexOf(DELIMITER); return first !== -1 && first === last; }).filter((str) => { const [payload, checksum] = str.split(DELIMITER); if (checksum.length !== CHECKSUM_LENGTH) { console.debug(`Invalid sentence: checksum has not two characters -> $${str}`); return false; } if (!/[0-9A-Fa-f]{2}/.test(checksum)) { console.debug(`Invalid sentence: checksum is not a hexadecimal digit -> $${str}`); return false; } const numChecksum = stringChecksumToNumber(checksum); const computedChecksum = calculateChecksum(payload); if (numChecksum !== computedChecksum) { console.debug(`Invalid sentence: calculated checksum ${numberChecksumToString(computedChecksum)} is not equal to given checksum ${checksum} -> $${str}`); return false; } return true; }).filter((str) => { const payload = str.split(DELIMITER).at(0); if (!payload.includes(SEPARATOR)) { console.debug(`Invalid sentence: payload has not separator character "${SEPARATOR}" -> $${str}`); return false; } if (["\r", "\n"].some((char) => payload.includes(char))) { console.debug(`Invalid sentence: payload has invalid characters -> $${str}`); return false; } return true; }).map((str) => `${START_FLAG}${str}${END_FLAG}`); }; var getIdPayloadAndChecksum = (frame) => { const [info, checksum] = frame.slice(START_FLAG.length, -END_FLAG_LENGTH).split(DELIMITER); const id = info.split(SEPARATOR)[0]; const payload = info.slice(id.length + 1); return { id, payload, checksum }; }; var hasSameNumberOfFields = (payload, sentence) => payload.split(SEPARATOR).length === sentence.payload.length; var parseNumber = (value, type) => { if (type === "int8") return Int8Schema.safeParse(Number(value)).data; if (type === "int16") return Int16Schema.safeParse(Number(value)).data; if (type === "int32") return Int32Schema.safeParse(Number(value)).data; if (type === "int64") return Int64Schema.safeParse(BigInt(value)).data; if (type === "uint8") return Uint8Schema.safeParse(Number(value)).data; if (type === "uint16") return Uint16Schema.safeParse(Number(value)).data; if (type === "uint32") return Uint32Schema.safeParse(Number(value)).data; if (type === "uint64") return Uint64Schema.safeParse(BigInt(value)).data; if (type === "float32") return Float32Schema.safeParse(Number(value)).data; if (type === "float64") return Float64Schema.safeParse(Number(value)).data; }; var parseBoolean = (value) => { if (value.toLowerCase() === "false" || value === "0") return false; if (value.toLowerCase() === "true" || value === "1") return true; }; var parseValue = (value, type) => { try { if (type === "string") { return value; } if (type === "boolean") { const b = parseBoolean(value); if (b !== void 0) { return b; } } const num = parseNumber(value, type); if (num !== void 0) { return num; } } catch (error) { console.debug(`Error parsing value: ${value} is not ${type}`); } return null; }; var getKnownNMEASentence = ({ received, sample, sentenceID, sentencePayload, checksum, model }) => { if (!hasSameNumberOfFields(sentencePayload, model)) return null; const fields = sentencePayload.split(SEPARATOR); const payload = model.payload.map(({ name, type, units, description }, index) => { const sample2 = fields[index]; const value = parseValue(sample2, type); return { name, sample: sample2, value, type, units: units ?? "unknown", description }; }); const { protocol } = model; const nmeaSentence = { received, sample, id: sentenceID, checksum, payload, protocol }; const nmeaSentenceWithMetadata = addMetadata(nmeaSentence); return NMEASentenceSchema.parse(nmeaSentenceWithMetadata); }; var getTalker = (sentenceID) => { if (sentenceID.length <= NMEA_ID_LENGTH) return null; const talker = TALKERS.filter(([talkerID, _talkerDescription]) => sentenceID.startsWith(talkerID)); if (talker.length === 1) { const value = talker[0][0]; return { value, description: talker[0][1] }; } if (sentenceID.startsWith("U") && !isNaN(Number(sentenceID[1]))) { const value = sentenceID.slice(0, 2); return { value, description: TALKERS_SPECIAL.U }; } if (sentenceID.startsWith("P")) { return { value: sentenceID, description: TALKERS_SPECIAL.P }; } return null; }; var getUnknowNMEASentence = ({ received, sample, sentenceID, sentencePayload, checksum }) => { const fields = sentencePayload.split(SEPARATOR); const response = fields.map((field) => ({ name: "unknown", sample: field, value: field, type: "string", units: "unknown" })); const sent = NMEASentenceSchema.parse({ received, sample, id: sentenceID, checksum, payload: response, description: "unknown nmea sentence" }); return sent; }; var createNumberValue = (type) => { const sign = Math.random() < 0.5 ? -1 : 1; const useed = Math.round(Math.random() * (Number.MAX_SAFE_INTEGER - Number.MIN_SAFE_INTEGER) + Number.MIN_SAFE_INTEGER); const seed = useed * sign; const fseed = Math.random() * sign; const uint64 = new BigUint64Array([0n]); crypto.getRandomValues(uint64); const biguintseed = uint64[0]; const int64 = new BigInt64Array([0n]); crypto.getRandomValues(int64); const bigintseed = int64[0]; switch (type) { case "uint8": return new Uint8Array([useed])[0]; case "uint16": return new Uint16Array([useed])[0]; case "uint32": return new Uint32Array([useed])[0]; case "uint64": return biguintseed; case "int8": return new Int8Array([seed])[0]; case "int16": return new Int16Array([seed])[0]; case "int32": return new Int32Array([seed])[0]; case "int64": return bigintseed; case "float32": return new Float32Array([fseed])[0]; case "float64": return new Float64Array([fseed])[0]; } return null; }; var createStringValue = () => { const text = Buffer.from(Math.random().toString(36).substring(2)).toString("ascii"); const array2 = Array.from(text).map((letter) => { if (isLowerCharASCII(letter) || isUpperCharASCII(letter) || isNumberCharASCII(letter)) { return letter; } return "a"; }); return array2.join(""); }; var createValue = (type) => { switch (type) { case "boolean": return Math.random() > 0.5; case "string": return createStringValue(); } return createNumberValue(type); }; var createPayload = (model) => { let payload = ""; model.payload.forEach((field) => { const value = createValue(field.type); payload += value !== null ? `${value.toString()},` : ","; }); return payload.slice(0, -1); }; var createFakeSentence = (model, talker) => { const id = talker !== void 0 ? `${talker}${model.id}` : model.id; const payload = createPayload(model); const info = `${id},${payload}`; const checksum = numberChecksumToString(calculateChecksum(info)); return `${START_FLAG}${info}${DELIMITER}${checksum}${END_FLAG}`; }; // src/parser.ts var Parser = class { // Memory - Buffer _memory = true; get memory() { return this._memory; } set memory(mem) { this._memory = BooleanSchema.parse(mem); } _buffer = ""; _bufferLength = MAX_CHARACTERS; get bufferLimit() { return this._bufferLength; } set bufferLimit(limit) { this._bufferLength = UnsignedIntegerSchema.parse(limit); } // Sentences _sentences = /* @__PURE__ */ new Map(); // get sentences() { return this._sentences } constructor(memory = false, limit = MAX_CHARACTERS) { this.memory = memory; this.bufferLimit = limit; this.readInternalProtocols(); } // Mandatory -------------------------------------------------------------------------------------------------------- readInternalProtocols() { const parsed = ProtocolsInputSchema.parse(NMEA_SENTENCES); this.addProtocols(parsed); } readProtocols(input) { if (input.file !== void 0) return readProtocolsYAMLFile(input.file); if (input.content !== void 0) return readProtocolsYAMLString(input.content); if (input.protocols !== void 0) return { protocols: input.protocols }; throw new Error("Invalid protocols to add"); } addProtocols(input) { if (!ProtocolsInputSchema.is(input)) { const error = "Parser: invalid protocols to parse"; console.error(error); console.error(input); throw new Error(error); } const { protocols } = this.readProtocols(input); const sentences = getStoreSentences({ protocols }); this._sentences = new Map([...this._sentences, ...sentences]); } parseData(text) { if (!StringSchema.is(text)) return []; const data = this.memory ? this._buffer + text : text; return this.getFrames(data); } getFrames(text) { if (this._memory) { const lastFrame = lastUncompletedFrame(text); if (lastFrame !== null) { this._buffer = lastFrame; } } const unparsedFrames = getUnparsedNMEAFrames(text); return unparsedFrames.map((frame) => this.getFrame(frame)); } getFrame(text) { const received = Date.now(); const { id: sentenceID, payload: pl, checksum: cs } = getIdPayloadAndChecksum(text); const checksum = getChecksum(cs); const sentence = this._sentences.get(sentenceID); if (sentence !== void 0) { const response = getKnownNMEASentence({ received, sample: text, sentenceID, sentencePayload: pl, checksum, model: sentence }); if (response !== null) { return response; } } const talker = getTalker(sentenceID); if (talker !== null) { const id = sentenceID.replace(talker.value, ""); const talkerSentence = this._sentences.get(id); if (talkerSentence !== void 0) { const response = getKnownNMEASentence({ received, sample: text, sentenceID: id, sentencePayload: pl, checksum, model: talkerSentence }); if (response !== null) { return { ...response, talker }; } } } const unknown = getUnknowNMEASentence({ received, sample: text, sentenceID, sentencePayload: pl, checksum }); return talker !== null ? { ...unknown, talker } : unknown; } // Nice to have ----------------------------------------------------------------------------------------------------- getSentences() { return Array.from(this._sentences.values()); } getSentencesByProtocol() { const sentences = this.getSentences(); const response = {}; sentences.forEach((sentence) => { const key = sentence.protocol.name; if (!(key in response)) { response[key] = [sentence]; } response[key].push(sentence); }); return response; } getSentence(id) { if (!StringSchema.is(id) || id.length < NMEA_ID_LENGTH) { return null; } const sentence = this._sentences.get(id); if (sentence !== void 0) { return { ...sentence }; } const talker = getTalker(id); if (talker === null) { return null; } const sentenceID = id.slice(talker.value.length); const sent = this._sentences.get(sentenceID); if (sent !== void 0) { return { ...sent, talker }; } return null; } getFakeSentenceByID(id) { if (!StringSchema.is(id) || id.length < NMEA_ID_LENGTH) { return null; } const sentence = this._sentences.get(id); if (sentence !== void 0) { return createFakeSentence(sentence); } const talker = getTalker(id); if (talker !== null) { const sentenceID = id.slice(talker.value.length); const sent = this._sentences.get(sentenceID); if (sent !== void 0) { return createFakeSentence(sent, talker.value); } } return null; } }; export { BooleanSchema, CHECKSUM_LENGTH, CODE, ChecksumSchema, DELIMITER, DELIMITER_LENGTH, END_FLAG, END_FLAG_LENGTH, FIELD_TYPES, Float32Schema, Float64Schema, INT16_MAX, INT16_MIN, INT32_MAX, INT32_MIN, INT8_MAX, INT8_MIN, Int16Schema, Int32Schema, Int64Schema, Int8Schema, IntegerSchema, JSONSchemaInputSchema, MAX_CHARACTERS, MAX_FLOAT, MAX_NMEA_CHARACTERS, MINIMAL_LENGTH, MIN_FLOAT, MapStoredSentencesSchema, NMEALikeSchema, NMEAParsedFieldchema, NMEAParsedPayloadSchema, Parser as NMEAParser, NMEASentenceSchema, NMEA_ID_LENGTH, NMEA_SENTENCE_LENGTH, NMEA_TALKER_LENGTH, NumberSchema, ProtocolFieldSchema, ProtocolFieldTypeSchema, ProtocolSchema, ProtocolSentencePayloadSchema, ProtocolSentenceSchema, ProtocolsFileContentSchema, ProtocolsInputSchema, SEPARATOR, SEPARATOR_LENGTH, START_FLAG, START_FLAG_LENGTH, StoredSentenceSchema, StringArraySchema, StringSchema, TALKERS, TALKERS_SPECIAL, TalkerSchema, UINT16_MAX, UINT32_MAX, UINT8_MAX, UNKNOWN_NMEA_SENTENCE_SCAFOLDING, Uint16Schema, Uint32Schema, Uint64Schema, Uint8Schema, UnsignedIntegerSchema, ValibotProtocolsFileContentSchema, ValueSchema, VersionSchema };