diff --git a/example.js b/example.js index 4c274a9..91cd4ad 100644 --- a/example.js +++ b/example.js @@ -6,13 +6,10 @@ const client = dgram.createSocket('udp4'); const psnDecoder = new Decoder(); client.on('listening', () => { - const address = client.address(); - client.setBroadcast(true); - client.setMulticastTTL(128); client.addMembership('236.10.10.10'); }); -client.on('message', (message, remote) => { +client.on('message', (message) => { psnDecoder.decode(message); }); diff --git a/index.js b/index.js index d634d0a..0051f51 100644 --- a/index.js +++ b/index.js @@ -1,71 +1,141 @@ -/* eslint-disable no-param-reassign */ -const { Parser } = require('binary-parser'); -const psnParser = require('./parsers/index.js'); -const { PSN_DATA_TRACKER_FIELD_PARSER } = require('./parsers/data/index.js'); +const PacketParser = require('./parsers/index.js'); +const InfoPacket = require('./models/InfoPacket.js'); +const DataPacket = require('./models/DataPacket.js'); const DATA_PACKET = 0x6755; const INFO_PACKET = 0x6756; class Decoder { + lastInfoPacketHeader; + + lastDataPacketHeader; + + infoPacketFrames = {}; + + dataPacketFrames = {}; + constructor() { - this.info = {}; + this.info = { + trackers: {}, + }; this.data = { trackers: {}, }; } + updateInfo(framePackets) { + console.log('info update'); + framePackets.forEach((packet) => { + packet.subChunks?.forEach((subChunk) => { + if (subChunk.id === 0x0001) { + // NOTE(jwetzell): system name subChunk + this.info.system_name = subChunk.system_name; + } else if (subChunk.id === 0x0002) { + subChunk.trackers?.forEach((tracker) => { + if (this.info.trackers[tracker.id] === undefined) { + this.info.trackers[tracker.id] = {}; + } + this.info.trackers[tracker.id].name = tracker.tracker_name.tracker_name; + }); + } + }); + }); + } + + updateData(framePackets) { + console.log('data update'); + framePackets.forEach((packet) => { + packet.subChunks?.forEach((subChunk) => { + if (subChunk.id === 0x0001) { + subChunk.trackers?.forEach((tracker) => { + if (this.data.trackers[tracker.id] === undefined) { + this.data.trackers[tracker.id] = {}; + } + tracker.fields?.forEach((field) => { + Object.entries(field).forEach(([key, value]) => { + this.data.trackers[tracker.id][key] = value; + }); + }); + }); + } + }); + }); + } + // TODO(jwetzell): support multiple frame packets this might require some rework of how chunks are assumed decode(packetBuf) { - const packet = psnParser(packetBuf); + const packet = PacketParser(packetBuf); + if (packet.id === INFO_PACKET) { - if (packet.chunk_data?.tracker_list?.trackers) { - packet.chunk_data?.tracker_list?.trackers.forEach((tracker) => { - if (this.data.trackers[tracker.id] === undefined) { - this.data.trackers[tracker.id] = {}; + const infoPacket = new InfoPacket(packet); + if (infoPacket) { + if (infoPacket.subChunks?.length > 0) { + const currentInfoPacketHeader = infoPacket.getHeaderPacket(); + if (!currentInfoPacketHeader) { + // NOTE(jwetzell): not sure that info packets without a header subchunk are valid? + return; } - this.data.trackers[tracker.id] = { - ...this.data.trackers[tracker.id], - id: tracker.id, - name: tracker.tracker_name.tracker_name, - }; - }); + + const systemSubChunk = infoPacket.getSystemPacket(); + if (!systemSubChunk) { + // NOTE(jwetzell): not sure that info packets without a system subchunk are valid? + return; + } + + if (this.infoPacketFrames[currentInfoPacketHeader.frame_id] === undefined) { + this.infoPacketFrames[currentInfoPacketHeader.frame_id] = []; + } + this.infoPacketFrames[currentInfoPacketHeader.frame_id].push(infoPacket); + + if ( + this.infoPacketFrames[currentInfoPacketHeader.frame_id].length === + currentInfoPacketHeader.frame_packet_count + ) { + console.log('found complete info frame'); + this.updateInfo(this.infoPacketFrames[currentInfoPacketHeader.frame_id]); + delete this.infoPacketFrames[currentInfoPacketHeader.frame_id]; + } else { + // TODO(jwetzell): need to compute whether a frame is complete + console.log('compute frame completion'); + } + + this.lastInfoPacketHeader = currentInfoPacketHeader; + } } } else if (packet.id === DATA_PACKET) { - if (packet.chunk_data?.tracker_list?.trackers) { - packet.chunk_data?.tracker_list?.trackers.forEach((tracker) => { - if (this.data.trackers[tracker.id] === undefined) { - this.data.trackers[tracker.id] = {}; + const dataPacket = new DataPacket(packet); + if (dataPacket) { + if (dataPacket.subChunks?.length > 0) { + const currentDataPacketHeader = dataPacket.getHeaderPacket(); + if (!currentDataPacketHeader) { + // NOTE(jwetzell): not sure that info packets without a header subchunk are valid? + console.error('data packet with no header'); + return; } - this.data.trackers[tracker.id] = { - ...this.data.trackers[tracker.id], - id: tracker.id, - }; - if (tracker.data && tracker.data_len > 0) { - const fields = new Parser() - .array('fields', { - type: PSN_DATA_TRACKER_FIELD_PARSER, - lengthInBytes: tracker.data_len, - }) - .parse(tracker.data); - if (fields.fields) { - fields.fields.forEach((field) => { - if (field.data) { - this.data.trackers[tracker.id] = { - ...this.data.trackers[tracker.id], - ...field.data, - }; - } - }); - } - tracker.fields = fields.fields; - delete tracker.data; + if (this.dataPacketFrames[currentDataPacketHeader.frame_id] === undefined) { + this.dataPacketFrames[currentDataPacketHeader.frame_id] = []; } - }); + this.dataPacketFrames[currentDataPacketHeader.frame_id].push(dataPacket); + + if ( + this.dataPacketFrames[currentDataPacketHeader.frame_id].length === + currentDataPacketHeader.frame_packet_count + ) { + console.log('found complete data frame'); + this.updateData(this.dataPacketFrames[currentDataPacketHeader.frame_id]); + delete this.dataPacketFrames[currentDataPacketHeader.frame_id]; + } else { + // TODO(jwetzell): need to compute whether a frame is complete + console.log('compute data frame completion'); + } + + this.lastDataPacketHeader = currentDataPacketHeader; + } } } - return packet; + console.log(this); } } diff --git a/models/DataPacket.js b/models/DataPacket.js new file mode 100644 index 0000000..32398b0 --- /dev/null +++ b/models/DataPacket.js @@ -0,0 +1,70 @@ +/* eslint-disable no-case-declarations */ +/* eslint-disable no-param-reassign */ +const { Parser } = require('binary-parser'); +const { ChunkParser, HeaderParser } = require('../parsers/common'); +const { DataTrackerListParser, DataTrackerFieldParser } = require('../parsers/data'); + +class DataPacket { + constructor(packet) { + this.packet = packet; + this.subChunks = []; + if (this.packet.has_subchunks) { + this.subChunkParser = new Parser().array('chunks', { + type: ChunkParser, + lengthInBytes: this.packet.data_len, + }); + this.subChunks = this.subChunkParser.parse(this.packet.chunk_data)?.chunks; + this.subChunks = this.subChunks.map((subChunk) => { + let populatedSubChunk = {}; + populatedSubChunk.data_len_valid = subChunk.chunk_data.length === subChunk.data_len; + switch (subChunk.id) { + case 0: + populatedSubChunk = { + ...subChunk, + ...HeaderParser.parse(subChunk.chunk_data), + }; + break; + case 1: + const dataTrackerList = DataTrackerListParser.parse(subChunk.chunk_data); + dataTrackerList.trackers?.forEach((tracker) => { + if (tracker.data && tracker.data_len > 0) { + const fields = new Parser() + .array('fields', { + type: DataTrackerFieldParser, + lengthInBytes: tracker.data_len, + }) + .parse(tracker.data); + if (fields.fields) { + tracker.fields = []; + fields.fields.forEach((field) => { + tracker.fields.push(field.data); + }); + } + } + }); + populatedSubChunk = { + ...subChunk, + ...dataTrackerList, + }; + break; + default: + populatedSubChunk = { + ...subChunk, + }; + break; + } + return populatedSubChunk; + }); + } + } + + getHeaderPacket() { + return this.subChunks.find((subChunk) => subChunk.id === 0x0000); + } + + getSystemPacket() { + return this.subChunks.find((subChunk) => subChunk.id === 0x0001); + } +} + +module.exports = DataPacket; diff --git a/models/InfoPacket.js b/models/InfoPacket.js new file mode 100644 index 0000000..5595d6d --- /dev/null +++ b/models/InfoPacket.js @@ -0,0 +1,57 @@ +const { Parser } = require('binary-parser'); +const { ChunkParser, HeaderParser } = require('../parsers/common'); +const { InfoSystemNameParser, InfoTrackerListParser } = require('../parsers/info'); + +class InfoPacket { + constructor(packet) { + this.packet = packet; + this.subChunks = []; + if (this.packet.has_subchunks) { + this.subChunkParser = new Parser().array('chunks', { + type: ChunkParser, + lengthInBytes: this.packet.data_len, + }); + this.subChunks = this.subChunkParser.parse(this.packet.chunk_data)?.chunks; + this.subChunks = this.subChunks.map((subChunk) => { + let populatedSubChunk = {}; + populatedSubChunk.data_len_valid = subChunk.chunk_data.length === subChunk.data_len; + switch (subChunk.id) { + case 0: + populatedSubChunk = { + ...subChunk, + ...HeaderParser.parse(subChunk.chunk_data), + }; + break; + case 1: + populatedSubChunk = { + ...subChunk, + ...InfoSystemNameParser.parse(subChunk.chunk_data), + }; + break; + case 2: + populatedSubChunk = { + ...subChunk, + ...InfoTrackerListParser.parse(subChunk.chunk_data), + }; + break; + default: + populatedSubChunk = { + ...subChunk, + }; + break; + } + return populatedSubChunk; + }); + } + } + + getHeaderPacket() { + return this.subChunks.find((subChunk) => subChunk.id === 0x0000); + } + + getSystemPacket() { + return this.subChunks.find((subChunk) => subChunk.id === 0x0001); + } +} + +module.exports = InfoPacket; diff --git a/package.json b/package.json index c9e73ce..45c1a51 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "posistagenet", + "name": "@jwetzell/posistagenet", "version": "0.0.1", "description": "", "main": "index.js", diff --git a/parsers/common/index.js b/parsers/common/index.js index 431c9b4..05e9559 100644 --- a/parsers/common/index.js +++ b/parsers/common/index.js @@ -1,6 +1,6 @@ const Parser = require('binary-parser').Parser; -const PSN_PACKET_HEADER_PARSER = new Parser() +const ChunkParser = new Parser() .uint16le('id') .uint16le('data_len', { formatter: (item) => { @@ -15,6 +15,11 @@ const PSN_PACKET_HEADER_PARSER = new Parser() return binary.charAt(0) === '1'; }, }) + .buffer('chunk_data', { + length: 'data_len', + }); + +const HeaderParser = new Parser() .uint64le('packet_timestamp') .uint8('version_high') .uint8('version_low') @@ -23,5 +28,6 @@ const PSN_PACKET_HEADER_PARSER = new Parser() .seek(4); module.exports = { - PSN_PACKET_HEADER_PARSER, + ChunkParser, + HeaderParser, }; diff --git a/parsers/data/index.js b/parsers/data/index.js index 0bd5664..7f664aa 100644 --- a/parsers/data/index.js +++ b/parsers/data/index.js @@ -1,7 +1,6 @@ -const { PSN_PACKET_HEADER_PARSER } = require('../common'); const Parser = require('binary-parser').Parser; -const PSN_DATA_TRACKER_FIELD_PARSER = new Parser() +const DataTrackerFieldParser = new Parser() .uint16le('id') .uint16le('data_len', { formatter: (item) => { @@ -48,30 +47,9 @@ const PSN_DATA_TRACKER_PARSER = new Parser() length: 'data_len', }); -const PSN_DATA_TRACKER_LIST_PARSER = new Parser() - .uint16le('id') - .uint16le('data_len', { - formatter: (item) => { - const binary = item.toString(2).padStart(16, '0'); - return Number.parseInt(binary.substring(1), 2); - }, - }) - .seek(-2) - .uint16le('has_subchunks', { - formatter: (item) => { - const binary = item.toString(2).padStart(16, '0'); - return binary.charAt(0) === '1'; - }, - }) - .array('trackers', { type: PSN_DATA_TRACKER_PARSER, lengthInBytes: 'data_len' }); - -const PSN_DATA_CHUNK_DATA_PARSER = new Parser() - .nest('packet_header', { type: PSN_PACKET_HEADER_PARSER }) - .nest('tracker_list', { type: PSN_DATA_TRACKER_LIST_PARSER }); +const DataTrackerListParser = new Parser().array('trackers', { type: PSN_DATA_TRACKER_PARSER, readUntil: 'eof' }); module.exports = { - PSN_DATA_CHUNK_DATA_PARSER, - PSN_DATA_TRACKER_FIELD_PARSER, - PSN_DATA_TRACKER_LIST_PARSER, - PSN_DATA_TRACKER_PARSER, + DataTrackerFieldParser, + DataTrackerListParser, }; diff --git a/parsers/index.js b/parsers/index.js index 4393b6f..f485af9 100644 --- a/parsers/index.js +++ b/parsers/index.js @@ -1,6 +1,3 @@ -const { PSN_DATA_CHUNK_DATA_PARSER } = require('./data'); -const { PSN_INFO_CHUNK_DATA_PARSER } = require('./info'); - const Parser = require('binary-parser').Parser; const PSN_PACKET_PARSER = new Parser() @@ -18,12 +15,6 @@ const PSN_PACKET_PARSER = new Parser() return binary.charAt(0) === '1'; }, }) - .choice('chunk_data', { - tag: 'id', - choices: { - 0x6755: PSN_DATA_CHUNK_DATA_PARSER, - 0x6756: PSN_INFO_CHUNK_DATA_PARSER, - }, - }); + .buffer('chunk_data', { readUntil: 'eof' }); module.exports = (buffer) => PSN_PACKET_PARSER.parse(buffer); diff --git a/parsers/info/index.js b/parsers/info/index.js index 6365101..f224125 100644 --- a/parsers/info/index.js +++ b/parsers/info/index.js @@ -1,24 +1,5 @@ -const { PSN_PACKET_HEADER_PARSER } = require('../common'); - const Parser = require('binary-parser').Parser; -const PSN_INFO_SYSTEM_NAME_PARSER = new Parser() - .uint16le('id') - .uint16le('data_len', { - formatter: (item) => { - const binary = item.toString(2).padStart(16, '0'); - return Number.parseInt(binary.substring(1), 2); - }, - }) - .seek(-2) - .uint16le('has_subchunks', { - formatter: (item) => { - const binary = item.toString(2).padStart(16, '0'); - return binary.charAt(0) === '1'; - }, - }) - .string('system_name', { length: 'data_len' }); - const PSN_INFO_TRACKER_NAME_PARSER = new Parser() .uint16le('id') .uint16le('data_len', { @@ -55,32 +36,11 @@ const PSN_INFO_TRACKER_PARSER = new Parser() type: PSN_INFO_TRACKER_NAME_PARSER, }); -const PSN_INFO_TRACKER_LIST_PARSER = new Parser() - .uint16le('id') - .uint16le('data_len', { - formatter: (item) => { - const binary = item.toString(2).padStart(16, '0'); - return Number.parseInt(binary.substring(1), 2); - }, - }) - .seek(-2) - .uint16le('has_subchunks', { - formatter: (item) => { - const binary = item.toString(2).padStart(16, '0'); - return binary.charAt(0) === '1'; - }, - }) - .array('trackers', { type: PSN_INFO_TRACKER_PARSER, lengthInBytes: 'data_len' }); +const InfoSystemNameParser = new Parser().string('system_name', { greedy: true }); -const PSN_INFO_CHUNK_DATA_PARSER = new Parser() - .nest('packet_header', { type: PSN_PACKET_HEADER_PARSER }) - .nest('system_name', { type: PSN_INFO_SYSTEM_NAME_PARSER }) - .nest('tracker_list', { type: PSN_INFO_TRACKER_LIST_PARSER }); +const InfoTrackerListParser = new Parser().array('trackers', { type: PSN_INFO_TRACKER_PARSER, readUntil: 'eof' }); module.exports = { - PSN_INFO_SYSTEM_NAME_PARSER, - PSN_INFO_TRACKER_NAME_PARSER, - PSN_INFO_TRACKER_PARSER, - PSN_INFO_TRACKER_LIST_PARSER, - PSN_INFO_CHUNK_DATA_PARSER, + InfoSystemNameParser, + InfoTrackerListParser, };