diff --git a/.eslintrc.js b/.eslintrc.js index 88fe19e..ca7b4b5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,5 +14,6 @@ module.exports = { 'class-methods-use-this': 'off', 'import/extensions': 'off', 'import/no-relative-packages': 'off', + 'no-param-reassign': 'off', }, }; diff --git a/README.md b/README.md index b4019b0..b1f2102 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,18 @@ js implementation of the [PosiStageNet protocol](https://github.com/vyv/psn-cpp/ const { Decoder } = require('@jwetzell/posistagenet') const decoder = new Decoder() -const buffer = Buffer.from('source PSN packets from somewhere') +const infoPacketBuffer = Buffer.from('5667348000000c0001000000000000000203010101000b00536572766572204e616d650200118001000d8000000900547261636b65722031','hex') +const dataPacketBuffer = Buffer.from('5567288000000c00010000000000000002030101010014800100108000000c000000803f0000803f0000803f','hex') -decoder.decode(buffer) +decoder.decode(infoPacketBuffer) +decoder.decode(dataPacketBuffer) -// data from PSN Info packets -console.log(decoder.info) +// the system_name from info packets if available +console.log(decoder.system_name) -// data from PSN Data packets -console.log(decoder.data) +// map of trackers populated with any data that has been received +// this merges both info and data packet properties into one +console.log(decoder.trackers) ``` @@ -56,3 +59,6 @@ infoPackets.forEach((infoPacket)=>{ ``` +## Notes +- UInt64 + - PosiStageNet uses 64 bit UInt's for timestamp values make sure to use [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) if using values over `Number.MAX_SAFE_INTEGER` \ No newline at end of file diff --git a/examples/psn_client.js b/examples/psn_client.js index 09b118e..38161b7 100644 --- a/examples/psn_client.js +++ b/examples/psn_client.js @@ -1,3 +1,4 @@ +// recreation of psn_client.cpp example from https://github.com/vyv/psn-cpp const dgram = require('dgram'); const { Decoder } = require('..'); @@ -14,29 +15,29 @@ client.on('message', (buffer) => { client.bind(56565, '0.0.0.0'); setInterval(() => { - if (decoder.info.system_name) { - console.log(`System Name: ${decoder.info.system_name}`); + if (decoder.system_name) { + console.log(`System Name: ${decoder.system_name}`); } - if (Object.keys(decoder.info.trackers).length > 0) { - console.log(`Tracker Count: ${Object.keys(decoder.info.trackers).length}`); + if (Object.keys(decoder.trackers).length > 0) { + console.log(`Tracker Count: ${Object.keys(decoder.trackers).length}`); } - Object.entries(decoder.data.trackers).forEach(([trackerId, tracker]) => { + Object.entries(decoder.trackers).forEach(([trackerId, tracker]) => { console.log( `Tracker - id: ${trackerId} | name: ${ - decoder.info.trackers[trackerId]?.name ? decoder.info.trackers[trackerId]?.name : '' + decoder.trackers[trackerId]?.tracker_name ? decoder.trackers[trackerId]?.tracker_name : '' }` ); if (tracker.pos) { - console.log(`\tpos: ${tracker.pos.x}, ${tracker.pos.y}, ${tracker.pos.z}`); + console.log(`\tpos: ${tracker.pos.pos_x}, ${tracker.pos.pos_y}, ${tracker.pos.pos_z}`); } if (tracker.speed) { - console.log(`\tspeed: ${tracker.speed.x}, ${tracker.speed.y}, ${tracker.speed.z}`); + console.log(`\tspeed: ${tracker.speed.speed_x}, ${tracker.speed.speed_y}, ${tracker.speed.speed_z}`); } if (tracker.ori) { - console.log(`\tori: ${tracker.ori.x}, ${tracker.ori.y}, ${tracker.ori.z}`); + console.log(`\tori: ${tracker.ori.ori_x}, ${tracker.ori.ori_y}, ${tracker.ori.ori_z}`); } if (tracker.status) { @@ -44,11 +45,13 @@ setInterval(() => { } if (tracker.accel) { - console.log(`\taccel: ${tracker.accel.x}, ${tracker.accel.y}, ${tracker.accel.z}`); + console.log(`\taccel: ${tracker.accel.accel_x}, ${tracker.accel.accel_y}, ${tracker.accel.accel_z}`); } if (tracker.trgtpos) { - console.log(`\ttrgtpos: ${tracker.trgtpos.x}, ${tracker.trgtpos.y}, ${tracker.trgtpos.z}`); + console.log( + `\ttrgtpos: ${tracker.trgtpos.trgtpos_x}, ${tracker.trgtpos.trgtpos_y}, ${tracker.trgtpos.trgtpos_z}` + ); } if (tracker.timestamp) { diff --git a/examples/psn_server.js b/examples/psn_server.js index 9d72066..d29a72d 100644 --- a/examples/psn_server.js +++ b/examples/psn_server.js @@ -1,8 +1,10 @@ +// recreation of psn_server.cpp example from https://github.com/vyv/psn-cpp const dgram = require('dgram'); -const { Encoder, Tracker } = require('..'); +const { Encoder, Tracker, Decoder } = require('..'); const client = dgram.createSocket('udp4'); const encoder = new Encoder('Test PSN Server', 2, 0); +const decoder = new Decoder(); const trackers = []; @@ -22,7 +24,7 @@ const distFromSun = [0, 0.58, 1.08, 1.5, 2.28, 7.78, 14.29, 28.71, 45.04, 59.13] let timestamp = 0; setInterval(() => { - for (let index = 0; index < trackers.length; index += 1) { + orbits.forEach((orbit, index) => { const a = 1.0 / orbits[index]; const b = distFromSun[index]; const x = timestamp; @@ -36,19 +38,27 @@ setInterval(() => { trackers[index].setTrgtPos(3, 14, 16); trackers[index].setStatus(index / 10.0); trackers[index].setTimestamp(timestamp); - } + }); const dataPackets = encoder.getDataPackets(timestamp, trackers); - - dataPackets.forEach((packet) => { + dataPackets.forEach((packet, index) => { + const decodedPacket = decoder.decode(packet); + console.log( + `Sending PSN_DATA_PACKET : Frame Id = ${decodedPacket.packet_header.frame_id} , Packet Count = ${index + 1}` + ); client.send(packet, 56565, '236.10.10.10'); }); timestamp += 1; -}, 20); +}, 5); setInterval(() => { const infoPackets = encoder.getInfoPackets(timestamp, trackers); - infoPackets.forEach((packet) => { + infoPackets.forEach((packet, index) => { + const decodedPacket = decoder.decode(packet); + console.log( + `Sending PSN_INFO_PACKET : Frame Id = ${decodedPacket.packet_header.frame_id} , Packet Count = ${index + 1}` + ); + client.send(packet, 56565, '236.10.10.10'); }); -}, 1000); +}, 500); diff --git a/package-lock.json b/package-lock.json index 3e2cfa7..2cbe557 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "@jwetzell/posistagenet", "version": "0.2.1", "license": "ISC", - "dependencies": { - "binary-parser": "^2.2.1" - }, "devDependencies": { "eslint": "^8.48.0", "eslint-config-airbnb": "^19.0.4", @@ -429,14 +426,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/binary-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-2.2.1.tgz", - "integrity": "sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==", - "engines": { - "node": ">=12" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/package.json b/package.json index e23c96e..88b1647 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jwetzell/posistagenet", - "version": "0.2.1", + "version": "0.3.0", "description": "", "main": "index.js", "scripts": { @@ -19,8 +19,5 @@ "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^9.0.0", "prettier": "^3.0.3" - }, - "dependencies": { - "binary-parser": "^2.2.1" } } diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..32a45d4 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,5 @@ +module.exports = { + PACKET_HEADER_SIZE: 16, + MAX_UDP_PACKET_SIZE: 1500, + CHUNK_HEADER_SIZE: 4, +}; diff --git a/src/decoder.js b/src/decoder.js index d6b2a85..047d5b3 100644 --- a/src/decoder.js +++ b/src/decoder.js @@ -1,8 +1,7 @@ -const PacketParser = require('./decoders/packet'); -const InfoPacket = require('./models/packets/info-packet'); -const DataPacket = require('./models/packets/data-packet'); +const packetDecoder = require('./decoders/packet'); class Decoder { + // TODO(jwetzell): check if these last packet headers are necessary to hold on to lastInfoPacketHeader; lastDataPacketHeader; @@ -12,74 +11,58 @@ class Decoder { dataPacketFrames = {}; constructor() { - this.info = { - trackers: {}, - }; + this.system_name = ''; - this.data = { - trackers: {}, - }; + this.trackers = {}; } updateInfo(framePackets) { 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; - }); - } - }); + if (packet.system_name) { + this.system_name = packet.system_name.system_name; + } + + if (packet.tracker_list?.trackers) { + Object.entries(packet.tracker_list.trackers).forEach(([trackerId, tracker]) => { + this.trackers[trackerId] = { + ...this.trackers[trackerId], + ...tracker.tracker_name, + }; + }); + } }); } updateData(framePackets) { 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] = {}; - } - this.data.trackers[tracker.id] = { - pos: tracker.pos, - speed: tracker.speed, - ori: tracker.ori, - status: tracker.status, - accel: tracker.accel, - trgtpos: tracker.trgtpos, - timestamp: tracker.timestamp, - }; - }); - } - }); + if (packet.tracker_list) { + Object.entries(packet.tracker_list.trackers).forEach(([trackerId, tracker]) => { + this.trackers[trackerId] = { + ...this.trackers[trackerId], + ...tracker, + }; + }); + } }); } // TODO(jwetzell): add invalid frame id decoding. Scenario where a frame id is reused before one is complete decode(packetBuf) { - const packet = PacketParser.parse(packetBuf); - + const packet = packetDecoder(packetBuf); if (packet.id === 0x6756) { - const infoPacket = new InfoPacket(packet); + const infoPacket = packet; if (infoPacket) { - if (infoPacket.subChunks?.length > 0) { - const currentInfoPacketHeader = infoPacket.getHeaderPacket(); + if (infoPacket.has_subchunks > 0) { + const currentInfoPacketHeader = infoPacket.packet_header; if (!currentInfoPacketHeader) { // NOTE(jwetzell): not sure that info packets without a header subchunk are valid? - return; + throw new Error('malformed packet'); } - const systemSubChunk = infoPacket.getSystemPacket(); + const systemSubChunk = infoPacket.system_name; if (!systemSubChunk) { // NOTE(jwetzell): not sure that info packets without a system subchunk are valid? - return; + throw new Error('malformed packet'); } if (this.infoPacketFrames[currentInfoPacketHeader.frame_id] === undefined) { @@ -99,32 +82,27 @@ class Decoder { } } } else if (packet.id === 0x6755) { - 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? - return; - } - - 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 - ) { - this.updateData(this.dataPacketFrames[currentDataPacketHeader.frame_id]); - delete this.dataPacketFrames[currentDataPacketHeader.frame_id]; - } - - this.lastDataPacketHeader = currentDataPacketHeader; + const dataPacket = packet; + if (dataPacket.has_subchunks) { + const currentDataPacketHeader = dataPacket.packet_header; + if (!currentDataPacketHeader) { + // NOTE(jwetzell): not sure that info packets without a header subchunk are valid? + throw new Error('malformed packet'); } + 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 + ) { + this.updateData(this.dataPacketFrames[currentDataPacketHeader.frame_id]); + delete this.dataPacketFrames[currentDataPacketHeader.frame_id]; + } + this.lastDataPacketHeader = currentDataPacketHeader; } } + return packet; } } diff --git a/src/decoders/chunk.js b/src/decoders/chunk.js index dbb4ba7..a47cd6e 100644 --- a/src/decoders/chunk.js +++ b/src/decoders/chunk.js @@ -1,20 +1,16 @@ -const { Parser } = require('binary-parser'); +module.exports = (buffer, dataDecoder) => { + let offset = 0; + const chunk = {}; + chunk.id = buffer.readUInt16LE(offset); + offset += 2; -module.exports = 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'; - }, - }) - .buffer('chunk_data', { - length: 'data_len', - }); + // NOTE(jwetzell): this data is split up as 1 bit for has_subchunks and 15 bits for the data_len + const combinedLengthAndFlag = buffer.readUInt16LE(offset); + chunk.has_subchunks = combinedLengthAndFlag > 32768; + chunk.data_len = chunk.has_subchunks ? combinedLengthAndFlag - 32768 : combinedLengthAndFlag; + offset += 2; + chunk.chunk_data = buffer.subarray(offset, offset + chunk.data_len); + + dataDecoder(chunk); + return chunk; +}; diff --git a/src/decoders/data/data-packet-chunk.js b/src/decoders/data/data-packet-chunk.js new file mode 100644 index 0000000..c22bbd2 --- /dev/null +++ b/src/decoders/data/data-packet-chunk.js @@ -0,0 +1,29 @@ +const { CHUNK_HEADER_SIZE } = require('../../constants'); +const chunk = require('../chunk'); +const packetHeaderChunk = require('../packet-header-chunk'); +const dataTrackerListChunk = require('./data-tracker-list-chunk'); + +function decodeDataPacketChunk(dataPacketChunk) { + if (dataPacketChunk.has_subchunks && dataPacketChunk.chunk_data) { + let offset = 0; + while (offset < dataPacketChunk.data_len) { + const chunkId = dataPacketChunk.chunk_data.readUInt16LE(offset); + switch (chunkId) { + case 0x0000: + dataPacketChunk.packet_header = packetHeaderChunk(dataPacketChunk.chunk_data.subarray(offset)); + offset += CHUNK_HEADER_SIZE; + offset += dataPacketChunk.packet_header.data_len; + break; + case 0x0001: + dataPacketChunk.tracker_list = dataTrackerListChunk(dataPacketChunk.chunk_data.subarray(offset)); + offset += CHUNK_HEADER_SIZE; + offset += dataPacketChunk.tracker_list.data_len; + break; + default: + break; + } + } + } +} + +module.exports = (buffer) => chunk(buffer, decodeDataPacketChunk); diff --git a/src/decoders/data/data-tracker-chunk.js b/src/decoders/data/data-tracker-chunk.js new file mode 100644 index 0000000..b389d49 --- /dev/null +++ b/src/decoders/data/data-tracker-chunk.js @@ -0,0 +1,41 @@ +const { CHUNK_HEADER_SIZE } = require('../../constants'); +const chunk = require('../chunk'); +const dataTrackerFieldChunk = require('./data-tracker-field-chunk'); + +function decodeTrackerChunk(trackerChunk) { + if (trackerChunk.chunk_data && trackerChunk.data_len > 0) { + let offset = 0; + while (offset < trackerChunk.data_len) { + const trackerFieldChunk = dataTrackerFieldChunk(trackerChunk.chunk_data.subarray(offset)); + offset += CHUNK_HEADER_SIZE; + offset += trackerFieldChunk.data_len; + switch (trackerFieldChunk.id) { + case 0x0000: + trackerChunk.pos = trackerFieldChunk; + break; + case 0x0001: + trackerChunk.speed = trackerFieldChunk; + break; + case 0x0002: + trackerChunk.ori = trackerFieldChunk; + break; + case 0x0003: + trackerChunk.status = trackerFieldChunk; + break; + case 0x0004: + trackerChunk.accel = trackerFieldChunk; + break; + case 0x0005: + trackerChunk.trgtpos = trackerFieldChunk; + break; + case 0x0006: + trackerChunk.timestamp = trackerFieldChunk; + break; + default: + break; + } + } + } +} + +module.exports = (buffer) => chunk(buffer, decodeTrackerChunk); diff --git a/src/decoders/data/data-tracker-field-chunk.js b/src/decoders/data/data-tracker-field-chunk.js index 147bb4f..d2a7456 100644 --- a/src/decoders/data/data-tracker-field-chunk.js +++ b/src/decoders/data/data-tracker-field-chunk.js @@ -1,29 +1,43 @@ -const Parser = require('binary-parser').Parser; +/* eslint-disable no-case-declarations */ +const chunk = require('../chunk'); -module.exports = 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'; - }, - }) - .choice('data', { - tag: 'id', - choices: { - 0x0000: new Parser().floatle('x').floatle('y').floatle('z'), - 0x0001: new Parser().floatle('x').floatle('y').floatle('z'), - 0x0002: new Parser().floatle('x').floatle('y').floatle('z'), - 0x0003: new Parser().floatle('validity'), - 0x0004: new Parser().floatle('x').floatle('y').floatle('z'), - 0x0005: new Parser().floatle('x').floatle('y').floatle('z'), - 0x0006: new Parser().uint64le('tracker_timestamp'), - }, - }); +function readXYZ(trackerFieldChunk, prefix) { + if (prefix === undefined) { + prefix = ''; + } + + trackerFieldChunk[`${prefix}x`] = trackerFieldChunk.chunk_data.readFloatLE(0); + trackerFieldChunk[`${prefix}y`] = trackerFieldChunk.chunk_data.readFloatLE(4); + trackerFieldChunk[`${prefix}z`] = trackerFieldChunk.chunk_data.readFloatLE(8); +} +function decodeTrackerFieldChunk(trackerFieldChunk) { + if (trackerFieldChunk.id !== undefined) { + switch (trackerFieldChunk.id) { + case 0x0000: + readXYZ(trackerFieldChunk, 'pos_'); + break; + case 0x0001: + readXYZ(trackerFieldChunk, 'speed_'); + break; + case 0x0002: + readXYZ(trackerFieldChunk, 'ori_'); + break; + case 0x0003: + trackerFieldChunk.validity = trackerFieldChunk.chunk_data.readFloatLE(); + break; + case 0x0004: + readXYZ(trackerFieldChunk, 'accel_'); + break; + case 0x0005: + readXYZ(trackerFieldChunk, 'trgtpos_'); + break; + case 0x0006: + trackerFieldChunk.tracker_timestamp = trackerFieldChunk.chunk_data.readBigUInt64LE(); + break; + default: + break; + } + } +} + +module.exports = (buffer) => chunk(buffer, decodeTrackerFieldChunk); diff --git a/src/decoders/data/data-tracker-list-chunk.js b/src/decoders/data/data-tracker-list-chunk.js index 19893b7..5455632 100644 --- a/src/decoders/data/data-tracker-list-chunk.js +++ b/src/decoders/data/data-tracker-list-chunk.js @@ -1,24 +1,17 @@ -module.exports = (buffer) => { - const trackerList = { - trackers: [], - }; +const { CHUNK_HEADER_SIZE } = require('../../constants'); +const chunk = require('../chunk'); +const dataTrackerChunk = require('./data-tracker-chunk'); + +function decodeDataTrackerListChunk(dataTrackerListChunk) { + dataTrackerListChunk.trackers = {}; // TODO(jwetzell): add error handling let offset = 0; - while (offset < buffer.length) { - const trackerChunk = {}; - trackerChunk.id = buffer.readUInt16LE(offset); - offset += 2; - - // NOTE(jwetzell): this data is split up as 1 bit for has_subchunks and 15 bits for the data_len - const tempBytesAsBinary = buffer.readUInt16LE(offset).toString(2); - trackerChunk.has_subchunks = tempBytesAsBinary.charAt(0) === '0'; - trackerChunk.data_len = parseInt(tempBytesAsBinary.substring(1), 2); - offset += 2; - trackerChunk.data = buffer.subarray(offset, offset + trackerChunk.data_len); + while (offset < dataTrackerListChunk.chunk_data.length) { + const trackerChunk = dataTrackerChunk(dataTrackerListChunk.chunk_data.subarray(offset)); + offset += CHUNK_HEADER_SIZE; offset += trackerChunk.data_len; - trackerList.trackers.push(trackerChunk); + dataTrackerListChunk.trackers[trackerChunk.id] = trackerChunk; } - - return trackerList; -}; +} +module.exports = (buffer) => chunk(buffer, decodeDataTrackerListChunk); diff --git a/src/decoders/data/index.js b/src/decoders/data/index.js deleted file mode 100644 index 07a3f67..0000000 --- a/src/decoders/data/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const TrackerListChunk = require('./data-tracker-list-chunk'); -const TrackerFieldChunk = require('./data-tracker-field-chunk'); - -module.exports = { - TrackerFieldChunk, - TrackerListChunk, -}; diff --git a/src/decoders/index.js b/src/decoders/index.js deleted file mode 100644 index ed1599a..0000000 --- a/src/decoders/index.js +++ /dev/null @@ -1,17 +0,0 @@ -const Chunk = require('./chunk'); -const PacketHeaderChunk = require('./packet-header-chunk'); -const Packet = require('./packet'); -const Info = require('./info'); -const Data = require('./data'); - -module.exports = { - Chunk, - PacketHeaderChunk, - Packet, - Info: { - ...Info, - }, - Data: { - ...Data, - }, -}; diff --git a/src/decoders/info/index.js b/src/decoders/info/index.js deleted file mode 100644 index 4769124..0000000 --- a/src/decoders/info/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const SystemNameChunk = require('./info-system-name-chunk'); -const TrackerListChunk = require('./info-tracker-list-chunk'); - -module.exports = { - SystemNameChunk, - TrackerListChunk, -}; diff --git a/src/decoders/info/info-packet-chunk.js b/src/decoders/info/info-packet-chunk.js new file mode 100644 index 0000000..f56da20 --- /dev/null +++ b/src/decoders/info/info-packet-chunk.js @@ -0,0 +1,35 @@ +const { CHUNK_HEADER_SIZE } = require('../../constants'); +const chunk = require('../chunk'); +const packetHeaderChunk = require('../packet-header-chunk'); +const infoSystemNameChunk = require('./info-system-name-chunk'); +const infoTrackerListChunk = require('./info-tracker-list-chunk'); + +function decodeInfoPacketChunk(infoPacketChunk) { + if (infoPacketChunk.has_subchunks && infoPacketChunk.chunk_data) { + let offset = 0; + while (offset < infoPacketChunk.data_len) { + const chunkId = infoPacketChunk.chunk_data.readUInt16LE(offset); + switch (chunkId) { + case 0x0000: + infoPacketChunk.packet_header = packetHeaderChunk(infoPacketChunk.chunk_data.subarray(offset)); + offset += CHUNK_HEADER_SIZE; + offset += infoPacketChunk.packet_header.data_len; + break; + case 0x0001: + infoPacketChunk.system_name = infoSystemNameChunk(infoPacketChunk.chunk_data.subarray(offset)); + offset += CHUNK_HEADER_SIZE; + offset += infoPacketChunk.system_name.data_len; + break; + case 0x0002: + infoPacketChunk.tracker_list = infoTrackerListChunk(infoPacketChunk.chunk_data.subarray(offset)); + offset += CHUNK_HEADER_SIZE; + offset += infoPacketChunk.tracker_list.data_len; + break; + default: + break; + } + } + } +} + +module.exports = (buffer) => chunk(buffer, decodeInfoPacketChunk); diff --git a/src/decoders/info/info-system-name-chunk.js b/src/decoders/info/info-system-name-chunk.js index 0dc1c8f..6a44bac 100644 --- a/src/decoders/info/info-system-name-chunk.js +++ b/src/decoders/info/info-system-name-chunk.js @@ -1,3 +1,9 @@ -const Parser = require('binary-parser').Parser; +const chunk = require('../chunk'); -module.exports = new Parser().string('system_name', { greedy: true }); +function decodeSystemNameChunk(systemNameChunk) { + if (systemNameChunk.chunk_data !== undefined && systemNameChunk.data_len !== undefined) { + systemNameChunk.system_name = systemNameChunk.chunk_data.subarray(0, systemNameChunk.data_len).toString(); + } +} + +module.exports = (buffer) => chunk(buffer, decodeSystemNameChunk); diff --git a/src/decoders/info/info-tracker-chunk.js b/src/decoders/info/info-tracker-chunk.js new file mode 100644 index 0000000..468d30f --- /dev/null +++ b/src/decoders/info/info-tracker-chunk.js @@ -0,0 +1,24 @@ +const { CHUNK_HEADER_SIZE } = require('../../constants'); +const chunk = require('../chunk'); +const infoTrackerNameChunk = require('./info-tracker-name-chunk'); + +function decodeInfoTrackerChunk(infoTrackerChunk) { + if (infoTrackerChunk.has_subchunks && infoTrackerChunk.chunk_data) { + let offset = 0; + while (offset < infoTrackerChunk.data_len) { + const chunkId = infoTrackerChunk.chunk_data.readUInt16LE(offset); + switch (chunkId) { + case 0x0000: + infoTrackerChunk.tracker_name = infoTrackerNameChunk(infoTrackerChunk.chunk_data.subarray(offset)); + offset += CHUNK_HEADER_SIZE; + offset += infoTrackerChunk.tracker_name.data_len; + break; + + default: + break; + } + } + } +} + +module.exports = (buffer) => chunk(buffer, decodeInfoTrackerChunk); diff --git a/src/decoders/info/info-tracker-list-chunk.js b/src/decoders/info/info-tracker-list-chunk.js index 2db1431..c0a6ccf 100644 --- a/src/decoders/info/info-tracker-list-chunk.js +++ b/src/decoders/info/info-tracker-list-chunk.js @@ -1,39 +1,17 @@ -const Parser = require('binary-parser').Parser; +const { CHUNK_HEADER_SIZE } = require('../../constants'); +const chunk = require('../chunk'); +const infoTrackerChunk = require('./info-tracker-chunk'); -const infoTrackerNameParser = 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('tracker_name', { length: 'data_len' }); - -const infoTrackerParser = 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'; - }, - }) - .nest('tracker_name', { - type: infoTrackerNameParser, - }); - -module.exports = new Parser().array('trackers', { type: infoTrackerParser, readUntil: 'eof' }); +function decodeInfoTrackerListChunk(infoTrackerListChunk) { + infoTrackerListChunk.trackers = {}; + if (infoTrackerListChunk.has_subchunks && infoTrackerListChunk.chunk_data) { + let offset = 0; + while (offset < infoTrackerListChunk.data_len) { + const trackerChunk = infoTrackerChunk(infoTrackerListChunk.chunk_data.subarray(offset)); + offset += CHUNK_HEADER_SIZE; + offset += trackerChunk.data_len; + infoTrackerListChunk.trackers[trackerChunk.id] = trackerChunk; + } + } +} +module.exports = (buffer) => chunk(buffer, decodeInfoTrackerListChunk); diff --git a/src/decoders/info/info-tracker-name-chunk.js b/src/decoders/info/info-tracker-name-chunk.js new file mode 100644 index 0000000..c07cb93 --- /dev/null +++ b/src/decoders/info/info-tracker-name-chunk.js @@ -0,0 +1,8 @@ +const chunk = require('../chunk'); + +function decodeTrackerNameChunk(trackerNameChunk) { + if (trackerNameChunk.chunk_data !== undefined && trackerNameChunk.data_len !== undefined) { + trackerNameChunk.tracker_name = trackerNameChunk.chunk_data.subarray(0, trackerNameChunk.data_len).toString(); + } +} +module.exports = (buffer) => chunk(buffer, decodeTrackerNameChunk); diff --git a/src/decoders/packet-header-chunk.js b/src/decoders/packet-header-chunk.js index 6672db3..f2233ab 100644 --- a/src/decoders/packet-header-chunk.js +++ b/src/decoders/packet-header-chunk.js @@ -1,9 +1,12 @@ -const Parser = require('binary-parser').Parser; +const chunk = require('./chunk'); -module.exports = new Parser() - .uint64le('packet_timestamp') - .uint8('version_high') - .uint8('version_low') - .uint8('frame_id') - .uint8('frame_packet_count') - .seek(4); +function decodePacketHeaderChunk(packetHeaderChunk) { + // TODO(jwetzell): remove the string conversion + packetHeaderChunk.packet_timestamp = packetHeaderChunk.chunk_data.readBigUInt64LE(0); + packetHeaderChunk.version_high = packetHeaderChunk.chunk_data.readUInt8(8); + packetHeaderChunk.version_low = packetHeaderChunk.chunk_data.readUInt8(9); + packetHeaderChunk.frame_id = packetHeaderChunk.chunk_data.readUInt8(10); + packetHeaderChunk.frame_packet_count = packetHeaderChunk.chunk_data.readUInt8(11); +} + +module.exports = (buffer) => chunk(buffer, decodePacketHeaderChunk); diff --git a/src/decoders/packet.js b/src/decoders/packet.js index e5d915a..9c4d0d0 100644 --- a/src/decoders/packet.js +++ b/src/decoders/packet.js @@ -1,18 +1,14 @@ -const Parser = require('binary-parser').Parser; +const dataPacketChunk = require('./data/data-packet-chunk'); +const infoPacketChunk = require('./info/info-packet-chunk'); -module.exports = 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'; - }, - }) - .buffer('chunk_data', { readUntil: 'eof' }); +module.exports = (buffer) => { + const chunkId = buffer.readUInt16LE(); + switch (chunkId) { + case 0x6756: + return infoPacketChunk(buffer); + case 0x6755: + return dataPacketChunk(buffer); + default: + return {}; + } +}; diff --git a/src/encoder.js b/src/encoder.js index a1d2a4b..1bc101e 100644 --- a/src/encoder.js +++ b/src/encoder.js @@ -1,14 +1,13 @@ -const dataChunk = require('./encoders/data/data-chunk'); +const { MAX_UDP_PACKET_SIZE, PACKET_HEADER_SIZE, CHUNK_HEADER_SIZE } = require('./constants'); +const dataPacketChunk = require('./encoders/data/data-packet-chunk'); const dataTrackerListChunk = require('./encoders/data/data-tracker-list-chunk'); -const infoChunk = require('./encoders/info/info-chunk'); +const infoPacketChunk = require('./encoders/info/info-packet-chunk'); const infoSystemNameChunk = require('./encoders/info/info-system-name-chunk'); const infoTrackerListChunk = require('./encoders/info/info-tracker-list-chunk'); const packetHeaderChunk = require('./encoders/packet-header-chunk'); -const MAX_TRACKER_LIST_SIZE = 1000; - class Encoder { - constructor(systemName, versionHigh, versionLow) { + constructor(systemName, versionHigh = 2, versionLow = 3) { this.systemName = systemName; this.versionHigh = versionHigh; this.versionLow = versionLow; @@ -17,6 +16,8 @@ class Encoder { } getInfoPackets(timestamp, trackers) { + const systemNameChunk = infoSystemNameChunk(this.systemName); + const trackerChunks = trackers.map((tracker) => tracker.getInfoChunk()); const infoPackets = []; @@ -24,16 +25,16 @@ class Encoder { const trackerChunksLists = []; let currentTrackerList = []; - let currentTrackerListSize = 0; + let currentInfoPacketSize = PACKET_HEADER_SIZE + systemNameChunk.length + CHUNK_HEADER_SIZE; trackerChunks.forEach((trackerChunk) => { - if (currentTrackerListSize + trackerChunk.length > MAX_TRACKER_LIST_SIZE) { + if (currentInfoPacketSize + trackerChunk.length > MAX_UDP_PACKET_SIZE) { trackerChunksLists.push(currentTrackerList); currentTrackerList = []; - currentTrackerListSize = 0; + currentInfoPacketSize = 0; } currentTrackerList.push(trackerChunk); - currentTrackerListSize += trackerChunk.length; + currentInfoPacketSize += trackerChunk.length; }); trackerChunksLists.push(currentTrackerList); const header = packetHeaderChunk( @@ -44,7 +45,7 @@ class Encoder { trackerChunksLists.length ); trackerChunksLists.forEach((trackerChunkList) => { - infoPackets.push(infoChunk(header, infoSystemNameChunk(this.systemName), infoTrackerListChunk(trackerChunkList))); + infoPackets.push(infoPacketChunk(header, systemNameChunk, infoTrackerListChunk(trackerChunkList))); }); this.dataFrameId += 1; if (this.dataFrameId > 255) { @@ -55,22 +56,21 @@ class Encoder { getDataPackets(timestamp, trackers) { const allTrackerChunks = trackers.map((tracker) => tracker.getDataChunk()); - const dataPackets = []; const trackerChunksLists = []; let currentTrackerList = []; - let currentTrackerListSize = 0; + let currentDataPacketSize = PACKET_HEADER_SIZE + CHUNK_HEADER_SIZE; allTrackerChunks.forEach((trackerChunk) => { - if (currentTrackerListSize + trackerChunk.length > MAX_TRACKER_LIST_SIZE) { + if (currentDataPacketSize + trackerChunk.length > MAX_UDP_PACKET_SIZE) { trackerChunksLists.push(currentTrackerList); currentTrackerList = []; - currentTrackerListSize = 0; + currentDataPacketSize = 0; } currentTrackerList.push(trackerChunk); - currentTrackerListSize += trackerChunk.length; + currentDataPacketSize += trackerChunk.length; }); trackerChunksLists.push(currentTrackerList); @@ -82,7 +82,7 @@ class Encoder { trackerChunksLists.length ); trackerChunksLists.forEach((trackerChunks) => { - dataPackets.push(dataChunk(header, dataTrackerListChunk(trackerChunks))); + dataPackets.push(dataPacketChunk(header, dataTrackerListChunk(trackerChunks))); }); this.dataFrameId += 1; if (this.dataFrameId > 255) { diff --git a/src/encoders/data/data-chunk.js b/src/encoders/data/data-packet-chunk.js similarity index 100% rename from src/encoders/data/data-chunk.js rename to src/encoders/data/data-packet-chunk.js diff --git a/src/encoders/data/data-tracker-chunk.js b/src/encoders/data/data-tracker-chunk.js index f3d5985..76bb4a0 100644 --- a/src/encoders/data/data-tracker-chunk.js +++ b/src/encoders/data/data-tracker-chunk.js @@ -1,3 +1,3 @@ const chunk = require('../chunk'); -module.exports = (trackerId, fieldChunks) => chunk(trackerId, Buffer.concat(fieldChunks), true); +module.exports = (trackerId, fieldChunks) => chunk(trackerId, Buffer.concat(fieldChunks), fieldChunks.length > 0); diff --git a/src/encoders/data/data-tracker-timestamp-chunk.js b/src/encoders/data/data-tracker-timestamp-chunk.js index 13d340c..4bdf06f 100644 --- a/src/encoders/data/data-tracker-timestamp-chunk.js +++ b/src/encoders/data/data-tracker-timestamp-chunk.js @@ -2,9 +2,6 @@ const chunk = require('../chunk'); module.exports = (timestamp) => { const buf = Buffer.alloc(8); - const timestampHigh = timestamp.toString(2).padStart(64, '0').substring(0, 32); - const timestampLow = timestamp.toString(2).padStart(64, '0').substring(32); - buf.writeUInt32LE(parseInt(timestampLow, 2)); - buf.writeUInt32LE(parseInt(timestampHigh, 2), 4); + buf.writeBigUInt64LE(BigInt(timestamp)); return chunk(0x0006, buf, false); }; diff --git a/src/encoders/info/info-chunk.js b/src/encoders/info/info-packet-chunk.js similarity index 100% rename from src/encoders/info/info-chunk.js rename to src/encoders/info/info-packet-chunk.js diff --git a/src/encoders/packet-header-chunk.js b/src/encoders/packet-header-chunk.js index c4abb02..2edb990 100644 --- a/src/encoders/packet-header-chunk.js +++ b/src/encoders/packet-header-chunk.js @@ -5,11 +5,7 @@ module.exports = (timestamp, versionHigh, versionLow, frameId, framePacketCount) throw new Error('frame id must be >= 0 and <= 255'); } const packetHeader = Buffer.alloc(12); - const timestampHigh = timestamp.toString(2).padStart(64, '0').substring(0, 32); - const timestampLow = timestamp.toString(2).padStart(64, '0').substring(32); - packetHeader.writeUInt32LE(parseInt(timestampLow, 2)); - packetHeader.writeUInt32LE(parseInt(timestampHigh, 2), 4); - + packetHeader.writeBigUInt64LE(BigInt(timestamp)); packetHeader.writeUint8(versionHigh, 8); packetHeader.writeUint8(versionLow, 9); packetHeader.writeUint8(frameId, 10); diff --git a/src/models/packets/data-packet.js b/src/models/packets/data-packet.js deleted file mode 100644 index fb3454f..0000000 --- a/src/models/packets/data-packet.js +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint-disable no-case-declarations */ -/* eslint-disable no-param-reassign */ -const { Parser } = require('binary-parser'); -const Decoders = require('../../decoders'); - -class DataPacket { - constructor(packet) { - this.packet = packet; - this.subChunks = []; - // console.log(this.packet); - if (this.packet.has_subchunks) { - this.subChunkParser = new Parser().array('chunks', { - type: Decoders.Chunk, - 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, - ...Decoders.PacketHeaderChunk.parse(subChunk.chunk_data), - }; - break; - case 1: - const dataTrackerList = Decoders.Data.TrackerListChunk(subChunk.chunk_data); - dataTrackerList.trackers?.forEach((tracker) => { - if (tracker.data && tracker.data_len > 0) { - const fields = new Parser() - .array('fields', { - type: Decoders.Data.TrackerFieldChunk, - lengthInBytes: tracker.data_len, - }) - .parse(tracker.data); - if (fields.fields) { - fields.fields.forEach((field) => { - switch (field.id) { - case 0x0000: - tracker.pos = field.data; - break; - case 0x0001: - tracker.speed = field.data; - break; - case 0x0002: - tracker.ori = field.data; - break; - case 0x0003: - tracker.status = field.data; - break; - case 0x0004: - tracker.accel = field.data; - break; - case 0x0005: - tracker.trgtpos = field.data; - break; - case 0x0006: - tracker.timestamp = field.data; - break; - default: - break; - } - }); - } - } - }); - 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/src/models/packets/index.js b/src/models/packets/index.js deleted file mode 100644 index 3786239..0000000 --- a/src/models/packets/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const DataPacket = require('./data-packet'); -const InfoPacket = require('./info-packet'); - -module.exports = { - DataPacket, - InfoPacket, -}; diff --git a/src/models/packets/info-packet.js b/src/models/packets/info-packet.js deleted file mode 100644 index b9ff214..0000000 --- a/src/models/packets/info-packet.js +++ /dev/null @@ -1,56 +0,0 @@ -const { Parser } = require('binary-parser'); -const Decoders = require('../../decoders'); - -class InfoPacket { - constructor(packet) { - this.packet = packet; - this.subChunks = []; - if (this.packet.has_subchunks) { - this.subChunkParser = new Parser().array('chunks', { - type: Decoders.Chunk, - 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, - ...Decoders.PacketHeaderChunk.parse(subChunk.chunk_data), - }; - break; - case 1: - populatedSubChunk = { - ...subChunk, - ...Decoders.Info.SystemNameChunk.parse(subChunk.chunk_data), - }; - break; - case 2: - populatedSubChunk = { - ...subChunk, - ...Decoders.Info.TrackerListChunk.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;