dcs-bios-api-mazeppa
Version:
DCS BIOS export parser that returns JSON data
228 lines (196 loc) • 7.92 kB
JavaScript
const EventEmitter = require('events');
const path = require('path');
const glob = require('glob');
const JsonParser = require('./dcs-bios-json-parser');
const UdpClient = require('./udp-multicast-client');
const BufferReader = require('buffer-utils').BufferReader;
const log = require('./logger');
const MAX_DATA_SIZE = 65536;
const defaultOptions = {
receivePort: 5010,
sendPort: 7778,
multicastAddress: '239.255.50.10',
emitAllUpdates: false,
logLevel: 'NONE',
autoStartListening: true
};
class DcsBiosExportParser extends EventEmitter {
/**
* DCS BIOS export data parser. Takes messages emitted from the UDP client and parses them.
* @param {Object} options - configuration options
* @example
* // possible options and their defaults
* new DcsBiosExportParser({
* receivePort: 5010,
* sendPort: 7778,
* multicastAddress: '239.255.50.10',
* emitAllUpdates: false,
* logLevel: 'NONE'
* });
*/
constructor(options = {}) {
super();
// Set the options from the constructor param.
this._options = Object.assign(defaultOptions, options);
this.setLogLevel(this._options.logLevel);
// Parse all the JSON files in the json folder.
var files = glob.sync(path.join(__dirname, '/../docs/json/*.json'));
var { aircraftData, controls, addressLookup } = JsonParser.parseFiles(files);
this._aircraftData = aircraftData;
this._controls = controls;
this._addressLookup = addressLookup;
// Create the UDP client.
this._client = new UdpClient(this._options);
// Create a buffer to hold all the incoming data from the DCS BIOS exporter. It's mainly used to re-construct
// strings when there's only a partial string update.
this._data = Buffer.alloc(MAX_DATA_SIZE);
// Queue of controls that need to emit an update once we receive a sync command.
this._emitQueue = [];
this._client.on('message', (message) => {
this._parseMessage(message);
});
if (options.autoStartListening) {
this.startListening();
}
}
/**
* Start listening for new UDP messages from the DCS BIOS exporter.
* @returns {Promise} when the client has started listening
*/
startListening() {
// Don't do anything if the UDP client is already listening.
if (this._isListening) { return; }
return this._client.start().then(() => this._isListening = true);
}
/**
* Stop listening for new UDP messages from the DCS BIOS exporter.
*/
stopListening() {
// Don't do anything if the UDP client has already stopped listening.
if (!this._isListening) { return; }
return this._client.stop().then(() => this._isListening = false);
}
/**
* Get the value of a control.
* @param {string} controlId - control identifier
* @param {string} outputSuffix - output suffix, if the control has more than one output
* @returns {string|Number} value of the control that was requested
*/
getControlValue(controlId, outputSuffix) {
var control = this._controls[controlId];
// If no control was found, return undefined.
if (!control) {
return undefined;
}
// If the control only has one output, return its value.
if (control.outputs.length == 1) {
return control.outputs[0].value;
}
// If the control has more than one output, return the value of the one with the provided suffix.
var output = control.outputs.find((output) => output.suffix == outputSuffix);
return output ? output.value : undefined;
}
/**
* Get the aircraft data. Contains every aircraft with its categories, controls, outputs, and values.
* @returns {Object} aircraft object
*/
get aircraftData() {
return this._aircraftData;
}
/**
* Set the logging level.
* @param {String} level - log level
*/
setLogLevel(level) {
log.setLogLevel(level);
}
/**
* Send a message to the DCS BIOS importer.
* @returns {Promise} when the message has finished sending
*/
sendMessage(message) {
return this._client.sendMessage(message.trim() + '\n');
}
/**
* Remove all control listeners.
*/
removeControlListeners() {
this.removeAllListeners();
}
/** Parse the DCS BIOS export message from the UDP client.
* @param {Buffer} message - DCS BIOS export message
*/
_parseMessage(message) {
var reader = new BufferReader(message);
if (reader.bytesLeft() < 4) {
log.warn('incomplete message received from UDP client: length is < 4');
return;
}
// Go through each update block in the message.
while (reader.bytesLeft()) {
var address = reader.readUInt16LE();
var count = reader.readUInt16LE();
// EARLY OUT: If this is a sync command, we're safe to emit. Process the emit queue.
if (address == 0x5555 && count == 0x5555) {
this._processEmitQueue();
continue;
}
else {
while (count) {
var controlAddress = address;
var addressValue = reader.readUInt16LE();
count = count - 2;
address = address + 2;
// Save the address value into the data buffer that holds all the data.
this._data.writeUInt16LE(addressValue, controlAddress);
// Find the closest address that has a control. Need to do this because string updates can be partial updates
// and the address that the exporter gives us will not correspond to a control in the address lookup.
while (!this._addressLookup[controlAddress]) {
controlAddress = controlAddress - 2;
}
// For each control in the address, process the value and add the control to the update queue.
this._addressLookup[controlAddress].forEach((control) => {
control.outputs.forEach((output) => {
var controlValue;
// This is a string, get the string using the starting address and its maximum length.
if (output.type == 'string') {
controlValue = this._data.toString('utf8', controlAddress, controlAddress + output.max_length);
}
// This is a number, get the value using the mask and the bit shift amount.
else {
controlValue = (addressValue & output.mask) >> output.shift_by;
}
var valueHasChanged = (this._options.emitAllUpdates || output.value != controlValue);
var isAlreadyAdded = this._emitQueue.some((item) => item.control == control && item.output == output);
// Don't add to the emit queue if the value hasn't changed (can be overriden by emitAllUpdates), or if the
// emit queue already has the control and output queued up for an emit.
if (valueHasChanged && !isAlreadyAdded) {
// Save the value on the output object.
output.value = controlValue;
this._emitQueue.push({ output, control });
}
});
});
}
}
}
}
/**
* Process the controls that need to trigger events.
*/
_processEmitQueue() {
// Emit all the updated controls. Useful if someone wants to process all the updates as an array.
this.emit('update', this._emitQueue);
// Emit each updated control as a separate event. Useful if someone only cares about a single control.
this._emitQueue.forEach((item) => {
var identifier = item.control.identifier + item.output.suffix;
log.debug('emit:', identifier, item.output.value);
// Emit the name of the control, i.e. 'ACFT_NAME'
this.emit(identifier, item.output.value, item.control, item.output);
// Emit the name of the control with its value, i.e. 'ACFT_NAME:Ka-50'
this.emit(identifier + ':' + item.output.value, item.control, item.output);
});
this._emitQueue = [];
}
}
module.exports = DcsBiosExportParser;