completely rework the parsing structure because I now realize how frames work

This commit is contained in:
2023-09-10 23:40:06 -05:00
parent 7a1366303f
commit 17224d9319
9 changed files with 261 additions and 132 deletions
+1 -4
View File
@@ -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);
});
+115 -45
View File
@@ -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);
}
}
+70
View File
@@ -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;
+57
View File
@@ -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;
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "posistagenet",
"name": "@jwetzell/posistagenet",
"version": "0.0.1",
"description": "",
"main": "index.js",
+8 -2
View File
@@ -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,
};
+4 -26
View File
@@ -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,
};
+1 -10
View File
@@ -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);
+4 -44
View File
@@ -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,
};