Skip to content
This repository has been archived by the owner on May 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #4 from core-marine-dev/dev
Browse files Browse the repository at this point in the history
Version 1.2.0 ready
  • Loading branch information
crisconru authored Dec 22, 2023
2 parents e1f84f7 + d7d4a86 commit c3b9d00
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 47 deletions.
34 changes: 18 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ type NMEASentence = {
raw: string,
// Sentence checksum
checksum: number
// Sentence talker
talker: null | { id: string, description: string }
}
```
Expand All @@ -93,7 +95,7 @@ You can enable or disable memory in the parser. Why?
- You just enters string in slots so a frame could be splitted into string slots
- With memory the parser remember the last half frame
- So you can finally parse with the next string input
****
## Feeding the parser (adding known sentences)
One of the greatest features of the parser is you can expand with more NMEA-like sentences. Standard or propietary sentences, it doesn't mind.
Expand Down Expand Up @@ -261,35 +263,35 @@ protocols:
# 14
- name: reference_station_id
type: float64
note: " 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
note: "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
0002 CenterPoint or ViewPoint RTX\n
0002 CenterPoint or ViewPoint RTX\n
0005 RangePoint RTX\n
0005 RangePoint RTX\n
0006 FieldPoint RTX\n
0006 FieldPoint RTX\n
0100 VBS\n
0100 VBS\n
1000 HP\n
1000 HP\n
1001 HP/XP (Orbits)\n
1001 HP/XP (Orbits)\n
1002 HP/G2 (Orbits)\n
1002 HP/G2 (Orbits)\n
1008 XP (GPS)\n
1008 XP (GPS)\n
1012 G2 (GPS)\n
1012 G2 (GPS)\n
1013 G2 (GPS/GLONASS)\n
1013 G2 (GPS/GLONASS)\n
1014 G2 (GLONASS)\n
1014 G2 (GLONASS)\n
1016 HP/XP (GPS)\n
1016 HP/XP (GPS)\n
1020 HP/G2 (GPS)\n
1020 HP/G2 (GPS)\n
1021 HP/G2 (GPS/GLONASS)"
1021 HP/G2 (GPS/GLONASS)"
# Propietary GYROCOMPAS1
- protocol: GYROCOMPAS1
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coremarine/nmea-parser",
"version": "1.1.0",
"version": "1.2.0",
"description": "Library to parse NMEA 0183 sentences",
"author": "CoreMarine",
"license": "ISC",
Expand Down
111 changes: 111 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,117 @@ export const CHECKSUM_LENGTH = 2
export const END_FLAG_LENGTH = END_FLAG.length
export const MINIMAL_LENGTH = START_FLAG_LENGTH + DELIMITER_LENGTH + CHECKSUM_LENGTH + END_FLAG_LENGTH

export const NMEA_ID_LENGTH = 3
export const NMEA_TALKER_LENGTH = 2
export const NMEA_SENTENCE_LENGTH = NMEA_ID_LENGTH + NMEA_TALKER_LENGTH

export const TALKERS = new Map<string, string>
TALKERS.set('AB', 'Independent AIS Base Station')
TALKERS.set('AD', 'Dependent AIS Base Station')
TALKERS.set('AG', 'Autopilot - General')
TALKERS.set('AI', 'Mobile AIS Station')
TALKERS.set('AN', 'AIS Aid to Navigation')
TALKERS.set('AP', 'Autopilot - Magnetic')
TALKERS.set('AR', 'AIS Receiving Station')
TALKERS.set('AT', 'AIS Transmitting Station')
TALKERS.set('AX', 'AIS Simplex Repeater')
TALKERS.set('BD', 'BeiDou (China)')
TALKERS.set('BI', 'Bilge System')
TALKERS.set('BN', 'Bridge navigational watch alarm system')
TALKERS.set('CA', 'Central Alarm')
TALKERS.set('CC', 'Computer - Programmed Calculator (obsolete)')
TALKERS.set('CD', 'Communications - Digital Selective Calling (DSC)')
TALKERS.set('CM', 'Computer - Memory Data (obsolete)')
TALKERS.set('CR', 'Data Receiver')
TALKERS.set('CS', 'Communications - Satellite')
TALKERS.set('CT', 'Communications - Radio-Telephone (MF/HF)')
TALKERS.set('CV', 'Communications - Radio-Telephone (VHF)')
TALKERS.set('CX', 'Communications - Scanning Receiver')
TALKERS.set('DE', 'DECCA Navigation (obsolete)')
TALKERS.set('DF', 'Direction Finder')
TALKERS.set('DM', 'Velocity Sensor, Speed Log, Water, Magnetic')
TALKERS.set('DP', 'Dynamiv Position')
TALKERS.set('DU', 'Duplex repeater station')
TALKERS.set('EC', 'Electronic Chart Display & Information System (ECDIS)')
TALKERS.set('EP', 'Emergency Position Indicating Beacon (EPIRB)')
TALKERS.set('ER', 'Engine Room Monitoring Systems')
TALKERS.set('FD', 'Fire Door')
TALKERS.set('FS', 'Fire Sprinkler')
TALKERS.set('GA', 'Galileo Positioning System')
TALKERS.set('GB', 'BeiDou (China)')
TALKERS.set('GI', 'NavIC, IRNSS (India)')
TALKERS.set('GL', 'GLONASS, according to IEIC 61162-1')
TALKERS.set('GN', 'Combination of multiple satellite systems (NMEA 1083)')
TALKERS.set('GP', 'Global Positioning System receiver')
TALKERS.set('GQ', 'QZSS regional GPS augmentation system (Japan)')
TALKERS.set('HC', 'Heading - Magnetic Compass')
TALKERS.set('HD', 'Hull Door')
TALKERS.set('HE', 'Heading - North Seeking Gyro')
TALKERS.set('HF', 'Heading - Fluxgate')
TALKERS.set('HN', 'Heading - Non North Seeking Gyro')
TALKERS.set('HS', 'Hull Stress')
TALKERS.set('II', 'Integrated Instrumentation')
TALKERS.set('IN', 'Integrated Navigation')
TALKERS.set('JA', 'Alarm and Monitoring')
TALKERS.set('JB', 'Water Monitoring')
TALKERS.set('JC', 'Power Management')
TALKERS.set('JD', 'Propulsion Control')
TALKERS.set('JE', 'Engine Control')
TALKERS.set('JF', 'Propulsion Boiler')
TALKERS.set('JG', 'Aux Boiler')
TALKERS.set('JH', 'Engine Governor')
TALKERS.set('LA', 'Loran A (obsolete)')
TALKERS.set('LC', 'Loran C (obsolete)')
TALKERS.set('MP', 'Microwave Positioning System (obsolete)')
TALKERS.set('MX', 'Multiplexer')
TALKERS.set('NL', 'Navigation light controller')
TALKERS.set('OM', 'OMEGA Navigation System (obsolete)')
TALKERS.set('OS', 'Distress Alarm System (obsolete)')
TALKERS.set('QZ', 'QZSS regional GPS augmentation system (Japan)')
TALKERS.set('RA', 'RADAR and/or ARPA')
TALKERS.set('RB', 'Record Book')
TALKERS.set('RC', 'Propulsion Machinery')
TALKERS.set('RI', 'Rudder Angle Indicator')
TALKERS.set('SA', 'Physical Shore AUS Station')
TALKERS.set('SD', 'Depth Sounder')
TALKERS.set('SG', 'Steering Gear')
TALKERS.set('SN', 'Electronic Positioning System, other/general')
TALKERS.set('SS', 'Scanning Sounder')
TALKERS.set('ST', 'Skytraq debug output')
TALKERS.set('TC', 'Track Control')
TALKERS.set('TI', 'Turn Rate Indicator')
TALKERS.set('TR', 'TRANSIT Navigation System')
TALKERS.set('UP', 'Microprocessor controller')
TALKERS.set('VA', 'VHF Data Exchange System (VDES), ASM')
TALKERS.set('VD', 'Velocity Sensor, Doppler, other/general')
TALKERS.set('VM', 'Velocity Sensor, Speed Log, Water, Magnetic')
TALKERS.set('VR', 'Voyage Data recorder')
TALKERS.set('VS', 'VHF Data Exchange System (VDES), Satellite')
TALKERS.set('VT', 'VHF Data Exchange System (VDES), Terrestrial')
TALKERS.set('VW', 'Velocity Sensor, Speed Log, Water, Mechanical')
TALKERS.set('WD', 'Watertight Door')
TALKERS.set('WI', 'Weather Instruments')
TALKERS.set('WL', 'Water Level')
TALKERS.set('YC', 'Transducer - Temperature (obsolete)')
TALKERS.set('YD', 'Transducer - Displacement, Angular or Linear (obsolete)')
TALKERS.set('YF', 'Transducer - Frequency (obsolete)')
TALKERS.set('YL', 'Transducer - Level (obsolete)')
TALKERS.set('YP', 'Transducer - Pressure (obsolete)')
TALKERS.set('YR', 'Transducer - Flow Rate (obsolete)')
TALKERS.set('YT', 'Transducer - Tachometer (obsolete)')
TALKERS.set('YV', 'Transducer - Volume (obsolete)')
TALKERS.set('YX', 'Transducer')
TALKERS.set('ZA', 'Timekeeper - Atomic Clock')
TALKERS.set('ZC', 'Timekeeper - Chronometer')
TALKERS.set('ZQ', 'Timekeeper - Quartz')
TALKERS.set('ZV', 'Timekeeper - Radio Update, WWV or WWVH')

export const TALKERS_SPECIAL = {
'P': 'Vendor specific',
'U': 'U# where \'#\' is a digit 0 …​ 9; User Configured',
}


// GENERATE ASCII STRING
export const CODE_A = 'A'.charCodeAt(0)
export const CODE_Z = 'Z'.charCodeAt(0)
Expand Down
53 changes: 44 additions & 9 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readdirSync } from 'node:fs'
import Path from 'node:path'
import { END_FLAG, END_FLAG_LENGTH, MAX_CHARACTERS, START_FLAG, START_FLAG_LENGTH } from "./constants";
import { END_FLAG, END_FLAG_LENGTH, MAX_CHARACTERS, NMEA_ID_LENGTH, START_FLAG, START_FLAG_LENGTH, TALKERS, TALKERS_SPECIAL } from "./constants";
import { BooleanSchema, NMEALikeSchema, NaturalSchema, ProtocolsInputSchema, StringSchema } from "./schemas";
import { Data, FieldType, FieldUnknown, NMEAKnownSentence, NMEAParser, NMEAPreParsed, NMEASentence, NMEAUknownSentence, ParserSentences, ProtocolsFile, ProtocolsInput, StoredSentence, StoredSentences } from "./types";
import { getStoreSentences, readProtocolsFile, readProtocolsString } from './protocols';
Expand Down Expand Up @@ -111,39 +111,74 @@ export class Parser implements NMEAParser {
return null
}
try {
const knownSentence: NMEAKnownSentence = {...preparsed, ...storedSentence, data: [] } as NMEAKnownSentence
const knownSentence = {...preparsed, ...storedSentence, data: [] as Data[] }
preparsed.data.forEach((value, index) => {
const type = knownSentence.fields[index].type
const data = this.getField(value, type)
// @ts-ignore
knownSentence.fields[index].data = data
knownSentence.data.push(data)
})
// @ts-ignore
return knownSentence
} catch (error) {
if (error instanceof Error) {
console.debug(`Invalid NMEA Frame ${preparsed.sentence} -> ${preparsed.raw}\n\t${error.message}`)
} else {
console.error('Parser.getKnownFrame')
console.error(error)
}
}
return null
}

private getSpecialTalker(id: string): string {
if (id.startsWith('U') && id.length === 2 && !isNaN(Number(id[1]))) {
return TALKERS_SPECIAL['U']
}
if (id.startsWith('P')) {
return TALKERS_SPECIAL['P']
}
return 'unknown'
}

private getKnownTalkerFrame(preparsed: NMEAPreParsed): NMEAKnownSentence | null {
const { sentence: aux } = preparsed
// Not valid NMEA sentence ID length
if (aux.length < NMEA_ID_LENGTH) { return null }
const [id, sentence] = [aux.slice(0, aux.length - NMEA_ID_LENGTH), aux.slice(- NMEA_ID_LENGTH)]
// Unknown NMEA frame
if (!this._sentences.has(sentence)) { return null }
// Knowing the frame
const knownSentence = this.getKnownFrame({ ...preparsed, sentence })
if (knownSentence === null) { return null }
if (knownSentence.data.length !== preparsed.data.length) { return null }
// Knowing the talker
const desc = TALKERS.get(id)
const description = desc ?? this.getSpecialTalker(id)
return { ...knownSentence, talker: { id, description } }
}

private getFrame(text: string, timestamp: number): NMEASentence | null {
const unparsedSentence = getNMEAUnparsedSentence(text)
// Not valid NMEA sentence
if (unparsedSentence === null) {
console.debug(`Invalid NMEA frame -> ${text}}`)
return null
}
const preparsedSentence: NMEAPreParsed = { timestamp, ...unparsedSentence }
// Unknown NMEA sentence
if (!this._sentences.has(preparsedSentence.sentence)) {
console.debug(`Unknown NMEA sentence -> ${text}`)
return this.getUnknowFrame(preparsedSentence)
}
const preparsedSentence: NMEAPreParsed = { ...unparsedSentence, timestamp, talker: null }
// Known NMEA sentence
return this.getKnownFrame(preparsedSentence)
if (this._sentences.has(preparsedSentence.sentence)) {
const sentence = this.getKnownFrame(preparsedSentence)
if (sentence !== null) return sentence
// Probably known sentence by talker ID
} else {
const sentence = this.getKnownTalkerFrame(preparsedSentence)
if (sentence !== null) return sentence
}
// Unknown NMEA sentence
console.debug(`Unknown NMEA sentence -> ${text}`)
return this.getUnknowFrame(preparsedSentence)
}

private getFrames(text: string): NMEASentence[] {
Expand Down
15 changes: 13 additions & 2 deletions src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,25 @@ export const NMEALikeSchema = StringSchema
.includes(DELIMITER)
.endsWith(END_FLAG)

export const TalkerSchema = z.object({
id: StringSchema,
description: StringSchema,
})

export const NMEAUnparsedSentenceSchema = z.object({
raw: StringSchema,
sentence: StringSchema,
checksum: NaturalSchema,
data: StringArraySchema
})

export const NMEAPreParsedSentenceSchema = NMEAUnparsedSentenceSchema.extend({ timestamp: NaturalSchema })

export const DataSchema = z.union([StringSchema, NumberSchema, BooleanSchema]).nullish()
export const NMEAPreParsedSentenceSchema = NMEAUnparsedSentenceSchema.extend({
timestamp: NaturalSchema,
talker: TalkerSchema.nullable().default(null)
})

export const DataSchema = z.union([StringSchema, NumberSchema, BooleanSchema]).nullable()

export const FieldParsedSchema = FieldSchema.extend({
data: DataSchema
Expand All @@ -133,8 +142,10 @@ export const NMEAUknownSentenceSchema = NMEAPreParsedSentenceSchema.extend({
fields: z.array(FieldUnknownSchema),
})


export const NMEAKnownSentenceSchema = StoredSentenceDataSchema.extend({
timestamp: NaturalSchema,
talker: TalkerSchema.nullable().default(null),
checksum: NumberSchema,
fields: z.array(FieldParsedSchema),
data: z.array(DataSchema)
Expand Down
10 changes: 9 additions & 1 deletion src/sentences.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getChecksum, numberChecksumToString, stringChecksumToNumber } from "./checksum";
import { CHECKSUM_LENGTH, DELIMITER, END_FLAG_LENGTH, MINIMAL_LENGTH, SEPARATOR, START_FLAG, START_FLAG_LENGTH } from "./constants";
import { CHECKSUM_LENGTH, DELIMITER, END_FLAG, END_FLAG_LENGTH, MINIMAL_LENGTH, SEPARATOR, START_FLAG, START_FLAG_LENGTH } from "./constants";
import { NMEALikeSchema, NMEAUknownSentenceSchema, NMEAUnparsedSentenceSchema } from "./schemas";
import { Data, FieldType, FieldUnknown, NMEALike, NMEAPreParsed, NMEAUknownSentence, NMEAUnparsedSentence, StoredSentence } from "./types";
import { isLowerCharASCII, isNumberCharASCII, isUpperCharASCII } from "./utils";
Expand Down Expand Up @@ -142,3 +142,11 @@ export const generateSentence = (model: StoredSentence): NMEALike => {
sentence += `*${checksum}\r\n`
return sentence
}

export const getFakeSentece = (text: string, sentence: string): string => {
const [frame, _cs] = text.slice(START_FLAG_LENGTH, -END_FLAG_LENGTH).split(DELIMITER)
const [_emitter, ...info] = frame.split(SEPARATOR)
const newFrame = [sentence, ...info].join(SEPARATOR)
const checksum = numberChecksumToString(getChecksum(newFrame))
return `$${newFrame}${DELIMITER}${checksum}${END_FLAG}`
}
Loading

0 comments on commit c3b9d00

Please sign in to comment.