ifc2
Version:
A Javascript client for Infinite Flight simulator API version 2
1,158 lines (849 loc) • 38.8 kB
JavaScript
/*
ifc2: A Node JS module providing a client the Infinite Flight Connect version 2 API.
Version: 1.0.22
Author: @likeablegeek (https://likeablegeek.com/)
Distributed by: FlightSim Ninja (http://flightim.ninja)
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*****
* Import required modules
*/
const dgram = require('dgram'); // For listening for UDP broadcasts
const net = require('net'); // For establishing socket connections
const events = require('events'); // For emitting events back to calling scripts
const { SlowBuffer } = require('buffer');
require('stringview'); // DataView extensions for reading/writing strings
/****
* Define IFC2 object
*/
let IFC2 = {
/*****
* Module name
*/
name: "IFC2", // Module name
/*****
* Constants for referencing error levels in logging
*/
INFO: 3,
WARN: 2,
ERROR: 1,
MANDATORY: 0,
/*****
* Constants for sending the correct flag for get/set calls in v2 API
*/
GETCMD: 0,
SETCMD: 1,
RUNCMD: -1,
LE: true,
/*****
* Constant for the manifest command
*/
MANIFESTCMD: -1,
/*****
* Constants for IF Connect v2 data types
*/
BOOLEAN: 0,
INTEGER: 1,
FLOAT: 2,
DOUBLE: 3,
STRING: 4,
LONG: 5,
/*****
* Object to hold connection data, manifest data, socket objects and more
*/
infiniteFlight: { // Infinite Flight connection data
broadcastPort: 15000, // Port to listen for broadcast from Infinite Flight
serverPort: 10112, // Port for socket connection to Infinite Flight
serverAddress: '127.0.0.1', // Default is localhost just as a placeholder
clientSocket: new net.Socket(), // Socket for regular one-off commands
manifestSocket: new net.Socket(), // Socket for fetching the manifest
pollSocket: new net.Socket(), // Socket for the regular polling loop
manifestTimeout: 1000, // How long to wait for the manifest before giving up
manifestData: "", // String to hold raw manifest data
manifestByName: {}, // Object to hold the manifest organised by command name
manifestByCommand: {}, // Object to hold the manifest organised by command number
manifestLength: 0, // Manifest length -- zero is initial placeholder
manifestBuffer: null // Placeholder variable for future manifest buffer
},
/*****
* Default logging state
*/
enableLog: false, // Control logging -- default is false
logLevel: this.MANDATORY, // Logging message level -- default is MANDATORY
/*****
* Default keepalive, reconnect and timeout
*/
keepAlive: false, // By default we don't keep alive
doReconnect: true, // By default we reconnect when sockets error
timeout: 0, // By default we don't time out the sockets (except the manifest)
/*****
* State tracking: are we connected? are we waiting?
*/
isConnected: false, // Are we connected to IF?
isWaiting: false, // Are we waiting?
isPollWaiting: false, // Are we waiting for a poll result?,
isCallback: false, // Are we using callbacks?
/*****
* Command queues
*/
q: [], // Queue for processing one-off requests
pollQ: [], // Queue for recurring poll requests
pollCurrent: 0, // Position in poll queue,
pollWaiting: 0, // Place holder for poll command currently pending data from IF
callbacks: {}, // Holds callback functions for when callbacks are enabled
/*****
* Timeout placeholder for slow polling handler
*/
pollTimeout: null,
/*****
*
* Queue buffers
*
*/
qBuffer: null,
pollBuffer: null,
/*****
* List to keep track of the commands pending responses from IF
*/
waitList: [],
/*****
* Event emitter for return events to client
*/
eventEmitter: new events.EventEmitter(),
/*****
* Default empty infoCallback function
*/
infoCallback: () => {},
/*****
* Default polling throttle (0ms)
*/
pollThrottle: 0,
/*****
* Object to hold last value fetched for all states that have been fetched from API
*/
ifData: {},
/*****
* Logging function
*/
log: (msg,level = IFC2.logLevel) => { // generic logging function
if (IFC2.enableLog) {
if (level <= IFC2.logLevel) {
let info = "(";
info += (IFC2.isConnected) ? 'c' : '';
info += (IFC2.isWaiting) ? 'w' : '';
info += IFC2.q.length;
info += IFC2.pollQ.length;
info += ")";
}
}
},
/*****
* Function to allow client to define listener for events emitted by module
*/
on: (event, listener) => {
IFC2.log("Setting listener for: " + event);
IFC2.eventEmitter.on(event, listener);
},
/*****
* Returns command formatted to send on TCP socket to API for getState commands
*/
getCommand: (cmd) => { // Prepare command ready to send to IF
IFC2.log('getCommand: ' + cmd);
let abCommand = new ArrayBuffer(5);
let dvCommand = new DataView(abCommand);
dvCommand.setInt32(0, cmd, IFC2.LE); // Encode the command itself
dvCommand.setInt8(4, IFC2.GETCMD, IFC2.LE); // Encode get marker
let u8Command = new Uint8Array(abCommand);
IFC2.log("getCommand: " + u8Command);
return u8Command;
},
/*****
* Sends command formatted to send on TCP socket to API for setState commands
*/
setCommand: (cmd, val) => { // Prepare command ready to send to IF
IFC2.log('setCommand: ' + cmd + "," + val, IFC2.MANDATORY);
let cmdType = IFC2.infiniteFlight.manifestByCommand[cmd].type;
let dataLength = 1;
switch(cmdType) {
case IFC2.INTEGER:
dataLength = 4;
break;
case IFC2.FLOAT:
dataLength = 4;
break;
case IFC2.DOUBLE:
dataLength = 4;
break;
case IFC2.STRING:
dataLength = 4 + val.length; // length is 4 for string length + string length
break;
case IFC2.LONG:
dataLength = 8;
break;
}
let abCommand = new ArrayBuffer(5 + dataLength); // 5 is command + true/false divider + value to be sent
let dvCommand = new DataView(abCommand);
dvCommand.setInt32(0, cmd, IFC2.LE); // Encode the command itself
dvCommand.setInt8(4, IFC2.SETCMD, IFC2.LE); // Encode set marker
switch(cmdType) {
case IFC2.BOOLEAN:
dvCommand.setInt8(5, val, true);
break;
case IFC2.INTEGER:
dvCommand.setInt32(5, val, true);
break;
case IFC2.FLOAT:
dvCommand.setFloat32(5, val, true);
break;
case IFC2.DOUBLE:
dvCommand.setFloat64(5, val, true);
break;
case IFC2.STRING:
dvCommand.setInt32(5, val.length, true);
dvCommand.setString(9, val);
break;
case IFC2.LONG:
dvCommand.setBigInt64(5, val, true);
break;
}
let u8Command = new Uint8Array(abCommand);
IFC2.log("setCommand u8Command: " + u8Command, IFC2.MANDATORY);
IFC2.log(dvCommand, IFC2.MANDATORY);
return u8Command;
},
/*****
* Process next command in one-off command queue (if any commands are pending)
*/
processQueue: () => {
IFC2.log("processQueue: isConnected: " + IFC2.isConnected);
IFC2.log("processQueue: isWaiting: " + IFC2.isWaiting);
if (IFC2.isConnected && !IFC2.isWaiting) { // only send if connected and not already waiting for a response
IFC2.log("Q length: " + IFC2.q.length);
if (IFC2.q.length > 0) { // only send if there is a command in the queue
let cmdObj = IFC2.q.shift(); // grab the next command from the queue
if (IFC2.infiniteFlight.manifestByName[cmdObj.cmd]) { // only send if the command is in the manifest
IFC2.isWaiting = true; // indicate we are now waiting for a response
IFC2.log('Sending command: ' + cmdObj.cmdCode);
IFC2.infiniteFlight.clientSocket.write(cmdObj.cmdBuf, () => { // Send the command
IFC2.waitList.push(cmdObj.cmdCode); // Add the command to the wait list
IFC2.log("Command sent: " + cmdObj.cmdCode);
});
}
} else {
setTimeout(IFC2.processQueue, 250); // No command in queue -- try again in 250ms
}
}
},
/****
* Add a one-off command to the command queue
*/
enqueueCommand: (cmd, action = IFC2.GETCMD, val) => {
IFC2.log("Enqueueing: " + cmd + "," + action);
let cmdCode = IFC2.infiniteFlight.manifestByName[cmd].command; // Get the command code
let cmdBuf = (action == IFC2.GETCMD || action == IFC2.RUNCMD) ? IFC2.getCommand(cmdCode) : IFC2.setCommand(cmdCode,val);
if (action == IFC2.GETCMD) {
IFC2.q.push({cmd: cmd, cmdCode: cmdCode, cmdBuf: cmdBuf}); // Push the command into the queue
IFC2.log(IFC2.q);
if (IFC2.q.length > 0 && !IFC2.isWaiting) { IFC2.processQueue(); } // If not currently waiting for a response, start processing the queue
} else if (action == IFC2.SETCMD) {
IFC2.infiniteFlight.clientSocket.write(cmdBuf, () => { // Send the command
IFC2.log("SetState Command sent: " + cmdBuf);
});
} else if (action == IFC2.RUNCMD) {
IFC2.infiniteFlight.clientSocket.write(cmdBuf, () => { // Send the command
IFC2.log("Run Command sent: " + cmdBuf);
});
}
},
/*****
* Function for client to request a one-off get command
*/
get: (cmd, callback) => {
IFC2.log("Processing get request: " + cmd);
if (IFC2.isConnected) { // Only enqueue if connected
if (IFC2.isCallback) { // Save the callback function if we are using it
IFC2.callbacks[cmd] = callback;
}
IFC2.enqueueCommand(cmd,IFC2.GETCMD);
}
},
/*****
* Function for client to request a one-off get command
*/
set: (cmd,val) => {
IFC2.log("Processing set request: " + cmd + "," + val);
if (IFC2.isConnected) { // Only enqueue if connected
IFC2.enqueueCommand(cmd,IFC2.SETCMD,val);
}
},
/*****
* Function run an Infinite Flight command
*/
run: (cmd) => {
IFC2.log("Processing run request: " + cmd);
if (IFC2.isConnected) { // Only enqueue if connected
IFC2.enqueueCommand(cmd,IFC2.RUNCMD);
}
},
/*****
* Process the manifest after fetching it
*/
processManifest: () => {
IFC2.log('Processing manifest into objects');
let manifestLines = IFC2.infiniteFlight.manifestData.split("\n"); // Split the data into lines
for (key in manifestLines) { // Loop through the lines
let line = manifestLines[key];
let lineData = line.split(','); // Split the line at commas
let command = parseInt(lineData[0]); // Get the command
let type = parseInt(lineData[1]); // Get the command data type
let name = lineData[2]; // Get the command name
if (!isNaN(command)) { // Save the manifest data for this command
IFC2.infiniteFlight.manifestByCommand[command] = {
name: name,
type: type
};
IFC2.infiniteFlight.manifestByName[name] = {
command: command,
type: type
};
}
}
// Emit Event
IFC2.eventEmitter.emit('IFC2manifest',IFC2.infiniteFlight.manifestByName); // Return data to calling script through an event
// Move on to post-manifest actions
IFC2.postManifest();
},
/*****
* Return the manifest by name
*/
manifestByName: () => {
return IFC2.infiniteFlight.manifestByName;
},
/*****
* Return the manifest by command
*/
manifestByCommand: () => {
return IFC2.infiniteFlight.manifestByCommand;
},
/*****
* Get the manifest
*/
getManifest: () => {
IFC2.log("Getting manifest from: " + IFC2.infiniteFlight.serverAddress);
// Reset manifest data variables
IFC2.infiniteFlight.manifestData = "";
IFC2.infiniteFlight.manifestByName = {};
IFC2.infiniteFlight.manifestByCommand = {};
IFC2.infiniteFlight.manifestLength = 0;
IFC2.infiniteFlight.manifestBuffer = null;
// Set up connection to the manifest socket
IFC2.infiniteFlight.manifestSocket.on('data', (data) => { // Handle "data" event
IFC2.log("Receiving Manifest Data");
if (IFC2.infiniteFlight.manifestBuffer == null) { // We haven't stored any buffer data yet
// Store the first batch of data in the buffer
IFC2.infiniteFlight.manifestBuffer = data;
} else { // We already have buffer data
// Concat the new buffer data into the main manifest buffer
let bufArr = [IFC2.infiniteFlight.manifestBuffer,data];
IFC2.infiniteFlight.manifestBuffer = Buffer.concat(bufArr);
}
IFC2.log("Buffer length: " + IFC2.infiniteFlight.manifestBuffer.length);
if (IFC2.infiniteFlight.manifestLength <= 0 && IFC2.infiniteFlight.manifestBuffer.length >= 12) {
// The first 12 bytes of the manifest are:
//
// 4 bytes: Int32 (-10 to specify the manifest command
// 4 bytes: Int32 Specify the length of all data to follow
// 4 bytes: Int32 specifying the length of the manifest data string to follow (so this is always four less than the preceding Int 32 value)
// If we don't have a manifest length and we have at least three Int32s in the buffer, get the manifest length from bytes 9-12
IFC2.infiniteFlight.manifestLength = IFC2.infiniteFlight.manifestBuffer.readInt32LE(8);
IFC2.log("Manifest length: " + IFC2.infiniteFlight.manifestLength);
} else {
// Check if we have hit the manifest length -- and remember the manifest length will be 12 less than the buffer length because
// the first 12 bytes are not part of the manifest itself
if (IFC2.infiniteFlight.manifestBuffer.length >= IFC2.infiniteFlight.manifestLength + 12) {
// Convert buffer to a string
IFC2.infiniteFlight.manifestData = IFC2.infiniteFlight.manifestBuffer.toString("utf8",12);
IFC2.log(IFC2.infiniteFlight.manifestData);
// Close the manifest socket
IFC2.infiniteFlight.manifestSocket.destroy();
// Process the manifest
IFC2.processManifest();
}
}
IFC2.log("-----");
});
IFC2.infiniteFlight.manifestSocket.on('timeout', () => { // Handle "timeout" evenet
IFC2.log("Manifest data done/timed out");
IFC2.eventEmitter.emit('IFC2msg',{"type": "error", code: "timeout", context: "manifest", "msg": "Manifest socket connection to Infinite Flight timed out"}); // Return data to calling script through an event
IFC2.infiniteFlight.manifestSocket.destroy(); // Destroy the socket
})
IFC2.infiniteFlight.manifestSocket.on('close', () => { // Handle "close" event
IFC2.log('Manifest Connection closed');
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "close", context: "manifest", "msg": "Manifest socket connection to Infinite Flight closed"}); // Return data to calling script through an event
});
// IFC2.infiniteFlight.manifestSocket.on('connect', () => { // Handle "connect" event
// });
IFC2.infiniteFlight.manifestSocket.on('error', function(data) {
IFC2.log('Error: ' + data, IFC2.INFO);
IFC2.eventEmitter.emit('IFC2msg',{"type": "error", code: "error", context: "manifest", "msg": "Error on Infinite Flight manifest socket"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.manifestSocket.on('drain', function(data) {
IFC2.log('Drain: ' + data, IFC2.INFO);
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "drain", context: "manifest", "msg": "Manifest socket connection to Infinite Flight drained"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.manifestSocket.on('end', function(data) {
IFC2.log('End: ' + data, IFC2.WARN);
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "end", context: "manifest", "msg": "Manifest socket connection to Infinite Flight ended"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.manifestSocket.on('lookup', function(data) {
IFC2.log('Lookup: ' + data, IFC2.INFO);
});
IFC2.infiniteFlight.manifestSocket.connect(IFC2.infiniteFlight.serverPort, IFC2.infiniteFlight.serverAddress, () => {
IFC2.log('Manifest Connected');
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "connect", context: "manifest", "msg": "Manifest socket connection to Infinite Flight created"}); // Return data to calling script through an event
IFC2.infiniteFlight.manifestSocket.setTimeout(IFC2.infiniteFlight.manifestTimeout); // Set the socket timeout
IFC2.infiniteFlight.manifestSocket.write(IFC2.getCommand(IFC2.MANIFESTCMD), () => {}); // Issue the get manifest command (-1)
});
},
/*****
* Place holder to hold success callback function provided by client
*/
successCallback: () => {},
/*****
* Process the poll queue
*/
processPoll: () => {
IFC2.log("Processing poll Q");
if (IFC2.pollQ.length > 0 && !IFC2.isPollWaiting) { // Only process if the queue has entries and we are not waiting for data from IF
IFC2.log(IFC2.pollQ);
// Get current command to process
let cmd = IFC2.pollQ[IFC2.pollCurrent];
let cmdCode = IFC2.infiniteFlight.manifestByName[cmd].command; // Get the command code
IFC2.log('Polling command: ' + cmdCode);
// Prep for next poll
IFC2.pollCurrent = ((IFC2.pollCurrent + 1) == IFC2.pollQ.length) ? 0 : (IFC2.pollCurrent + 1);
// Set isPollWaiting
IFC2.isPollWaiting = true;
// Send the command
if (IFC2.pollThrottle > 0) { // Wait before polling
setTimeout(() => {
IFC2.infiniteFlight.pollSocket.write(IFC2.getCommand(cmdCode), () => {
IFC2.log("Poll command sent: " + cmdCode);
if (IFC2.waitList.indexOf(cmdCode) < 0) { // Check if we are already waiting for this command
IFC2.waitList.push(cmdCode); // Add the command to the wait list
IFC2.pollWaiting = cmdCode;
}
});
}, IFC2.pollThrottle);
} else { // Don't delay -- just get on and poll
if (IFC2.waitList.indexOf(cmdCode) < 0) { // Check if we are already waiting for this command
IFC2.infiniteFlight.pollSocket.write(IFC2.getCommand(cmdCode), () => {
IFC2.log("Poll command sent: " + cmdCode);
IFC2.waitList.push(cmdCode); // Add the command to the wait list
IFC2.pollWaiting = cmdCode;
});
}
}
} else { // There was nothing in the queue
IFC2.log("Set poll timeout");
}
},
/*****
* Register a command into the poll queue
*/
pollRegister: (cmd, callback) => {
if (!IFC2.pollQ.hasOwnProperty(cmd)) {
if (IFC2.isCallback) { // Save callback function if we are using callbacks
IFC2.callbacks[cmd] = callback;
}
IFC2.pollQ.push(cmd);
if (!IFC2.isPollWaiting) { IFC2.processPoll(); }
}
},
/*****
* Deregister a command from the poll queue
*/
pollDeregister: (cmd) => {
let index = IFC2.pollQ.indexOf(cmd);
IFC2.pollQ.splice(index,1);
if (IFC2.pollCurrent >= IFC2.pollQ.length) { IFC2.pollCurrent = 0; }
if (IFC2.pollQ.length == 0) { IFC2.isPollWaiting = false; }
},
/*****
* Process command data returned by the API
*
* nextFN is a function to call after data processing is done
*/
processData: (source, nextFN) => {
IFC2.log('processData: Processing data source: ' + source, IFC2.INFO);
let data = (source == "client") ? IFC2.qBuffer : IFC2.pollBuffer;
let command = data.readInt32LE(0); // Get the command from the data
IFC2.log("processData: Got data for command: " + command);
let inManifest = IFC2.infiniteFlight.manifestByCommand.hasOwnProperty(command); // See if command is in manifest
IFC2.log("processData: inManifest: " + inManifest);
if (inManifest) { // Only proceed if we have the command in the manifest
let waitIndex = IFC2.waitList.indexOf(command); // See if the command is in the waitList
IFC2.log("processData: waitList: " + JSON.stringify(IFC2.waitList));
IFC2.log("processData: In waitList: " + waitIndex);
if (waitIndex >= 0) { // Only proceed if command is in the waitList
IFC2.log("processData: Waiting for command: " + command);
IFC2.log("processData: data length: " + data.length);
if (data.length > 4) { // See if we have a data length greater than 4
IFC2.log("processData: data length gt 4");
let bufLength = data.readInt32LE(4);
if (data.length >= bufLength + 8) { // Do we have the full command data?
IFC2.log("processData: data is complete so process");
IFC2.log(data);
IFC2.log("processData: waitList before splice: " + JSON.stringify(IFC2.waitList));
IFC2.waitList.splice(waitIndex,1);
IFC2.log("processData: waitList after splice: " + JSON.stringify(IFC2.waitList));
switch(IFC2.infiniteFlight.manifestByCommand[command].type) {
case IFC2.BOOLEAN:
IFC2.processResult(command, (data.readUInt8(8) == 1) ? true : false);
break;
case IFC2.INTEGER:
IFC2.processResult(command, data.readUInt32LE(8));
break;
case IFC2.FLOAT:
IFC2.processResult(command, data.readFloatLE(8));
break;
case IFC2.DOUBLE:
IFC2.processResult(command, data.readDoubleLE(8));
break;
case IFC2.STRING:
strLen = data.readUInt32LE(8);
IFC2.processResult(command, data.toString("utf8",12,strLen + 12));
break;
case IFC2.LONG:
IFC2.processResult(command, data.readBigInt64LE(8));
break;
}
// remove data from buffer
if (source == "client") {
if (data.length > bufLength + 8) {
IFC2.qBuffer = IFC2.qBuffer.slice(bufLength + 8,IFC2.qBuffer.length);
} else {
IFC2.qBuffer = null;
if (IFC2.waitList.length == 0) {
IFC2.isWaiting = false; // No longer waiting
}
}
} else {
if (data.length > bufLength + 8) {
IFC2.pollBuffer = IFC2.pollBuffer.slice(bufLength + 8,IFC2.pollBuffer.length);
} else {
IFC2.pollBuffer = null;
IFC2.isPollWaiting = false; // No longer waiting
}
}
nextFN();
} else {
nextFN();
}
} else {
nextFN();
}
} else {
nextFN();
}
} else {
nextFN();
}
},
/*****
* After processing the manifest, connect to IF Connect v2 API
*/
postManifest: function() {
IFC2.infiniteFlight.clientSocket.connect(IFC2.infiniteFlight.serverPort, IFC2.infiniteFlight.serverAddress, function() {
IFC2.infiniteFlight.clientSocket.setTimeout(IFC2.timeout);
IFC2.infiniteFlight.clientSocket.setKeepAlive(IFC2.keepAlive);
});
IFC2.infiniteFlight.clientSocket.on('data', function(data) {
IFC2.log('***** Received: ' + data, IFC2.INFO);
if (IFC2.qBuffer == null) { // We haven't stored any buffer data yet
// Store the first batch of data in the buffer
IFC2.qBuffer = data;
} else { // We already have buffer data
// Concat the new buffer data into the main manifest buffer
let bufArr = [IFC2.qBuffer,data];
IFC2.qBuffer = Buffer.concat(bufArr);
}
IFC2.processData("client", IFC2.processQueue);
});
IFC2.infiniteFlight.clientSocket.on('error', function(data) {
IFC2.log('Client: Error: ' + JSON.stringify(data), IFC2.INFO);
if (IFC2.isConnected && IFC2.doReconnect) {
IFC2.log("Client: Trying to reconnect");
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "reconnect", context: "client", "msg": "Reconnecting for general queries"}); // Return data to calling script through an event
IFC2.infiniteFlight.clientSocket.destroy();
// IFC2.infiniteFlight.pollSocket = new net.Socket();
IFC2.infiniteFlight.clientSocket.connect(IFC2.infiniteFlight.serverPort, IFC2.infiniteFlight.serverAddress, function() {
IFC2.infiniteFlight.clientSocket.setTimeout(IFC2.timeout);
IFC2.infiniteFlight.clientSocket.setKeepAlive(IFC2.keepAlive);
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "reconnected", context: "client", "msg": "Reconnected for general queries"}); // Return data to calling script through an event
});
}
IFC2.eventEmitter.emit('IFC2msg',{"type": "error", code: "error", context: "client", "msg": "Error on Infinite Flight socket"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.clientSocket.on('timeout', function(data) {
IFC2.log('Client: Timeout: ' + JSON.stringify(data), IFC2.INFO);
IFC2.eventEmitter.emit('IFC2msg',{"type": "error", code: "timeout", context: "client", "msg": "Timeout on socket connection to Infinite Flight"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.clientSocket.on('close', function(data) {
IFC2.log('Client: Closer: ' + JSON.stringify(data), IFC2.INFO);
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "close", context: "client", "msg": "Socket connection to Infinite Flight closed"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.clientSocket.on('connect', function(data) {
IFC2.log('Connected to IF server ' + IFC2.infiniteFlight.serverAddress, IFC2.MANDATORY);
/* if (IFC2.isConnected && IFC2.infiniteFlight.keepAlive) {
IFC2.infiniteFlight.clientSocket.connect(IFC2.infiniteFlight.serverPort, IFC2.infiniteFlight.serverAddress, function() {
IFC2.infiniteFlight.clientSocket.setTimeout(IFC2.timeout);
IFC2.infiniteFlight.clientSocket.setKeepAlive(IFC2.keepAlive);
});
}*/
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "connect", context: "client", "msg": "Socket connection to Infinite Flight created"}); // Return data to calling script through an event
if (!IFC2.isConnected) {
IFC2.postConnect();
}
});
IFC2.infiniteFlight.clientSocket.on('drain', function(data) {
IFC2.log('Client: Drain: ' + JSON.stringify(data), IFC2.INFO);
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "drain", context: "client", "msg": "Socket connection to Infinite Flight drained"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.clientSocket.on('end', function(data) {
IFC2.log('Client: End: ' + JSON.stringify(data), IFC2.INFO);
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "end", context: "client", "msg": "Socket connection to Infinite Flight ended"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.clientSocket.on('lookup', function(data) {
IFC2.log('Client: Lookup: ' + JSON.stringify(data), IFC2.INFO);
});
},
/*****
* Pre-connection tasts
*/
preConnect: function() {
IFC2.log("Connecting...", IFC2.INFO);
IFC2.getManifest();
},
/*****
* Post-connection tasks
*/
postConnect: function() {
IFC2.log("clientSocket Connected ...", IFC2.MANDATORY);
// Connect to Polling Socket
IFC2.infiniteFlight.pollSocket.connect(IFC2.infiniteFlight.serverPort, IFC2.infiniteFlight.serverAddress, function() {
IFC2.infiniteFlight.pollSocket.setTimeout(IFC2.timeout);
IFC2.infiniteFlight.pollSocket.setKeepAlive(IFC2.keepAlive);
});
IFC2.infiniteFlight.pollSocket.on('data', function(data) {
IFC2.log('Received poll: ' + data, IFC2.INFO);
if (IFC2.pollBuffer == null) { // We haven't stored any buffer data yet
// Store the first batch of data in the buffer
IFC2.pollBuffer = data;
} else { // We already have buffer data
// Concat the new buffer data into the main manifest buffer
let bufArr = [IFC2.pollBuffer,data];
IFC2.pollBuffer = Buffer.concat(bufArr);
}
// Clear poll timeout
clearTimeout(IFC2.pollTimeout);
IFC2.processData("poll", IFC2.processPoll);
});
IFC2.infiniteFlight.pollSocket.on('error', function(data) {
IFC2.log('Poll: Error: ' + JSON.stringify(data), IFC2.INFO);
if (IFC2.isConnected && IFC2.doReconnect) {
IFC2.log("Poll: Trying to reconnect");
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "reconnect", context: "poll", "msg": "Reconnecting for polling"}); // Return data to calling script through an event
IFC2.infiniteFlight.pollSocket.destroy();
// IFC2.infiniteFlight.pollSocket = new net.Socket();
IFC2.infiniteFlight.pollSocket.connect(IFC2.infiniteFlight.serverPort, IFC2.infiniteFlight.serverAddress, function() {
IFC2.infiniteFlight.pollSocket.setTimeout(IFC2.timeout);
IFC2.infiniteFlight.pollSocket.setKeepAlive(IFC2.keepAlive);
IFC2.isPollWaiting = false;
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "reconnected", context: "poll", "msg": "Reconnected for polling"}); // Return data to calling script through an event
IFC2.processPoll();
});
}
IFC2.eventEmitter.emit('IFC2msg',{"type": "error", code: "error", context: "poll", "msg": "Error polling Infinite Flight"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.pollSocket.on('timeout', function(data) {
IFC2.log('Poll: Timeout: ' + JSON.stringify(data), IFC2.INFO);
IFC2.eventEmitter.emit('IFC2msg',{"type": "error", code: "timeout", context: "poll", "msg": "Timeout on polling socket connection to Infinite Flight"}); // Return data to calling script through an event
IFC2.processPoll();
});
IFC2.infiniteFlight.pollSocket.on('close', function(data) {
IFC2.log('Poll: Close: ' + JSON.stringify(data), IFC2.INFO);
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "close", context: "poll", "msg": "Polling socket connection to Infinite Flight closed"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.pollSocket.on('connect', function(data) {
IFC2.log('Connected for polling to IF server ' + IFC2.infiniteFlight.serverAddress, IFC2.MANDATORY);
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "connect", context: "poll", "msg": "Polling socket connection to Infinite Flight created"}); // Return data to calling script through an event
IFC2.isConnected = true;
// Fetch one-time data about aircraft, IF, etc
if (IFC2.isCallback) {
IFC2.get('infiniteflight/app_state',IFC2.infoCallback);
IFC2.get('infiniteflight/app_version',IFC2.infoCallback);
IFC2.get('infiniteflight/api_version',IFC2.infoCallback);
IFC2.get('aircraft/0/name',IFC2.infoCallback);
IFC2.get('aircraft/0/livery',IFC2.infoCallback);
} else {
IFC2.get('infiniteflight/app_state');
IFC2.get('infiniteflight/app_version');
IFC2.get('infiniteflight/api_version');
IFC2.get('aircraft/0/name');
IFC2.get('aircraft/0/livery');
}
// Issue callback
IFC2.successCallback();
// Start Polling
IFC2.processPoll();
});
IFC2.infiniteFlight.pollSocket.on('drain', function(data) {
IFC2.log('Poll: Drain: ' + JSON.stringify(data), IFC2.INFO);
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "drain", context: "poll", "msg": "Polling socket connection to Infinite Flight drained"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.pollSocket.on('end', function(data) {
IFC2.log('Poll: End: ' + JSON.stringify(data), IFC2.INFO);
IFC2.eventEmitter.emit('IFC2msg',{"type": "info", code: "end", context: "poll", "msg": "Polling socket connection to Infinite Flight ended"}); // Return data to calling script through an event
});
IFC2.infiniteFlight.pollSocket.on('lookup', function(data) {
IFC2.log('Poll: Lookup: ' + JSON.stringify(data), IFC2.INFO);
});
},
/*****
* Process results of data returned from IF
*/
processResult: function(command, data) {
IFC2.log("Processing result: " + command + " > " + data);
// Save data in ifData data object
IFC2.ifData[IFC2.infiniteFlight.manifestByCommand[command].name] = {
data: data,
ts: Date.now()
};
// Return IFC2data event
if (IFC2.isCallback) { // Use a callback if one is available
IFC2.callbacks[IFC2.infiniteFlight.manifestByCommand[command].name]({"command": IFC2.infiniteFlight.manifestByCommand[command].name, "data": data})
} else { // Use an event
IFC2.eventEmitter.emit('IFC2data',{"command": IFC2.infiniteFlight.manifestByCommand[command].name, "data": data}); // Return data to calling script through an event
}
},
// SHORTCUTS FUNCTIONS //
/*****
* Initialise module and connection to IF
*/
init: function(successCallback, params={}) {
IFC2.log("Initialisting IFC2");
if (successCallback) IFC2.successCallback = successCallback; // Set success callback function
if (params.enableLog) IFC2.enableLog = params.enableLog; // Set Logging on/off
if (params.logLevel) IFC2.logLevel = params.logLevel; // Set logging message level
if (params.keepAlive) IFC2.keepAlive = params.keepAlive; // Set keepalive
if (params.doReconnect) IFC2.doReconnect = params.doReconnect; // Set reconnect
if (params.timeout) IFC2.timeout = params.timeout; // Set socket timeout
if (params.callback) IFC2.isCallback = params.callback; // Set if we are using callbacks
if (params.infoCallback) IFC2.infoCallback = params.infoCallback; // Callback function for initial info fetches
if (params.pollThrottle) IFC2.pollThrottle = params.pollThrottle; // Set polling throttle if provided
if (params.host && params.port) { // Host provided so connect directly to it
IFC2.infiniteFlight.serverAddress = params.host;
IFC2.infiniteFlight.serverPort = params.port;
IFC2.preConnect();
} else { // No host provided so search for a host via UDP
IFC2.searchHost(); // Search for Infinite Flight host
}
},
/*****
* Use UDP broadcast to find an IF client on the local network
*/
searchHost: function() {
// We only connect to the first device to respond.
// If you have multiple devices on the network you might not connect to the device you want.
//
// Future roadmap: send an event for each one that answers to calling script and let them decide what to do
IFC2.log('Searching for Infinite Flight device',IFC2.INFO);
// Create udp server socket object.
const server = dgram.createSocket("udp4");
// Make udp server listen on port 8089.
server.bind(IFC2.infiniteFlight.broadcastPort);
// When udp server receive message.
server.on("message", function (message) {
IFC2.log("UDP broadcast received",IFC2.INFO);
IFC2.log(message.toString(),IFC2.INFO);
let data = JSON.parse(message.toString());
let regex = /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]/;
for (key in data.Addresses) {
let ip = data.Addresses[key];
IFC2.log("Found IF on: " + ip,IFC2.INFO);
if (ip.match(regex)) { // only match IPv4 addresses for now
IFC2.infiniteFlight.serverAddress = ip;
}
}
server.close();
IFC2.preConnect();
});
// When udp server started and listening.
server.on('listening', function () {
// Get and print udp server listening ip address and port number in log console.
let address = server.address();
IFC2.log('UDP Server started and listening on ' + address.address + ":" + address.port,IFC2.INFO);
});
},
/*****
* Close active connections
*/
close: function(callback) {
// Only close if connected
if (IFC2.isConnected) {
// Reset isConnected flag
IFC2.isConnected = false;
// Reset the sucess callback function
IFC2.successCallback = () => {};
// Destroy client socket
IFC2.infiniteFlight.clientSocket.destroy();
IFC2.infiniteFlight.clientSocket.emit('error', new Error('ECONNRESET'));
// Delete client socket
delete IFC2.infiniteFlight.clientSocket;
// Recreate client socket
IFC2.infiniteFlight.clientSocket = new net.Socket();
// Destory poll socket
IFC2.infiniteFlight.pollSocket.destroy();
IFC2.infiniteFlight.pollSocket.emit('error', new Error('ECONNRESET'));
// Delete poll socket
delete IFC2.infiniteFlight.pollSocket;
// Recreate poll socket
IFC2.infiniteFlight.pollSocket = new net.Socket();
// Destory poll socket
IFC2.infiniteFlight.manifestSocket.destroy();
IFC2.infiniteFlight.manifestSocket.emit('error', new Error('ECONNRESET'));
// Delete manifest socket
delete IFC2.infiniteFlight.manifestSocket;
// Recreate manifest socket
IFC2.infiniteFlight.manifestSocket = new net.Socket();
// Reset manfiest data
IFC2.infiniteFlight.manifestData = ""; // String to hold raw manifest data
IFC2.infiniteFlight.manifestByName = {}, // Object to hold the manifest organised by command name
IFC2.infiniteFlight.manifestByCommand = {}; // Object to hold the manifest organised by command number
IFC2.infiniteFlight.manifestLength = 0; // Manifest length -- zero is initial placeholder
IFC2.infiniteFlight.manifestBuffer = null; // Placeholder variable for future manifest buffer
// Reset isWaiting flag
IFC2.isWaiting = false;
// Reset queues
IFC2.q = [];
IFC2.pollQ =[];
// Reset wait list
IFC2.waitList = [];
// Reset ifData data object
IFC2.ifData = {};
}
// Execute callback function
callback();
}
};
module.exports = IFC2;