ifcjs
Version:
A Javascript client for Infinite Flight simulator Connect API v1 - forked from earlier clients
485 lines (413 loc) • 18.1 kB
JavaScript
/*
ifc: A Node JS module providing a client the Infinite Flight Connect API.
Version: 1.1.7
Author: @likeablegeek
This version is based on previous versions by @nicolasbd and @Velocity23 and
and contains portions of their work.
Copyright 2019.
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.
*/
var isIp = require('is-ip'); // Used to test if broadcast IP addresses are IPv6 vs IPv4
var dgram = require('dgram'); // For listening for UDP broadcasts
var net = require('net'); // For establishing socket connections
var events = require('events'); // For emitting events back to calling scripts
const INFO = 3; // Constants for referencing error levels in logging calls
const WARN = 2;
const ERROR = 1;
const MANDATORY = 0;
var IFC = {
infiniteFlight: { // Infinite Flight connection data
broadcastPort: 15000, // Port to listen for broadcast from Infinite Flight
serverPort: 0, // Port for socket connection to Infinite Flight
serverAddress: null,
discoverSocket: false,
clientSocket: false
},
enableLog: false, // Control logging -- default is false
logLevel: MANDATORY, // Logging message level -- default is MANDATORY
name: "IF Connect", // Module name
isConnected: false, // Are we connected to IF?
eventEmitter: new events.EventEmitter(), // Event emitter
ifData: { // Object to hold data returned by information API calls
"Fds.IFAPI.APIAircraftState": "",
"Fds.IFAPI.APIEngineStates": "",
"Fds.IFAPI.APIFuelTankStates": "",
"Fds.IFAPI.APIAircraftInfo": "",
"Fds.IFAPI.APILightsState": "",
"Fds.IFAPI.APIAutopilotState": "",
"Fds.IFAPI.IFAPIStatus": "",
"Fds.IFAPI.APINearestAirportsResponse": "",
"Fds.IFAPI.APIFlightPlan": "",
"Fds.IFAPI.LiveAirplaneList": "",
"Fds.IFAPI.FacilityList": "",
"Fds.IFAPI.APIFrequencyInfoList": ""
},
intervalCommands: { // Commands for retrieving each data type
"Fds.IFAPI.APIAircraftState": function(interval) {
IFC.activeIntervals["Fds.IFAPI.APIAircraftState"] = setInterval(
() => {
IFC.sendCommand({
"Command": "airplane.getstate",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.APIAircraftState [airplane.getstate]",ERROR);
},interval);
},
"Fds.IFAPI.APIEngineStates": function(interval) {
IFC.activeIntervals["Fds.IFAPI.APIEngineStates"] = setInterval(
() => {
IFC.sendCommand({
"Command": "airplane.getenginesstate",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.APIEngineStates [airplane.getenginesstate]",ERROR);
},interval);
},
"Fds.IFAPI.APIFuelTankStates": function(interval) {
IFC.activeIntervals["Fds.IFAPI.APIFuelTankStates"] = setInterval(
() => {
IFC.sendCommand({
"Command": "airplane.getfuelstate",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.APIFuelTankStates [airplane.getfuelstate]",ERROR);
},interval);
},
"Fds.IFAPI.APIAircraftInfo": function(interval) {
IFC.activeIntervals["Fds.IFAPI.APIAircraftInfo"] = setInterval(
() => {
IFC.sendCommand({
"Command": "airplane.getinfo",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.APIAircraftInfo [airplane.getinfo]",ERROR);
},interval);
},
"Fds.IFAPI.APILightsState": function(interval) {
IFC.activeIntervals["Fds.IFAPI.APILightsState"] = setInterval(
() => {
IFC.sendCommand({
"Command": "airplane.getlightsstate",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.APILightsState [airplane.getlightsstate]",ERROR);
},interval);
},
"Fds.IFAPI.APIAutopilotState": function(interval) {
IFC.activeIntervals["Fds.IFAPI.APIAutopilotState"] = setInterval(
() => {
IFC.sendCommand({
"Command": "autopilot.getstate",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.APIAutopilotState [autopilot.getstate]",ERROR);
},interval);
},
"Fds.IFAPI.IFAPIStatus": function(interval) {
IFC.activeIntervals["Fds.IFAPI.IFAPIStatus"] = setInterval(
() => {
IFC.sendCommand({
"Command": "infiniteflight.getstatus",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.IFAPIStatus [infiniteflight.getstatus]",ERROR);
},interval);
},
"Fds.IFAPI.APINearestAirportsResponse": function(interval) {
IFC.activeIntervals["Fds.IFAPI.APINearestAirportsResponse"] = setInterval(
() => {
IFC.sendCommand({
"Command": "infiniteflight.getnearestairports",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.APINearestAirportsResponse [infiniteflight.getnearestairports]",ERROR);
},interval);
},
"Fds.IFAPI.APIFlightPlan": function(interval) {
IFC.activeIntervals["Fds.IFAPI.APIFlightPlan"] = setInterval(
() => {
IFC.sendCommand({
"Command": "flightplan.get",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.APIFlightPlan [flightplan.get]",ERROR);
},interval);
},
"Fds.IFAPI.LiveAirplaneList": function(interval) {
IFC.activeIntervals["Fds.IFAPI.LiveAirplaneList"] = setInterval(
() => {
IFC.sendCommand({
"Command": "live.gettraffic",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.LiveAirplaneList [live.gettraffic]",ERROR);
},interval);
},
"Fds.IFAPI.FacilityList": function(interval) {
IFC.activeIntervals["Fds.IFAPI.FacilityList"] = setInterval(
() => {
IFC.sendCommand({
"Command": "live.atcfacilities",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.FacilityList [live.atcfacilities]",ERROR);
},interval);
},
"Fds.IFAPI.APIFrequencyInfoList": function(interval) {
IFC.activeIntervals["Fds.IFAPI.APIFrequencyInfoList"] = setInterval(
() => {
IFC.sendCommand({
"Command": "live.getcurrentcomfrequencies",
"Parameters": []
});
IFC.log("Interval called Fds.IFAPI.APIFrequencyInfoList [live.getcurrentcomfrequencies]",ERROR);
},interval);
}
},
pollingIntervals: { // Interval values for polling for each info type
"Fds.IFAPI.APIAircraftState": 0,
"Fds.IFAPI.APIEngineStates": 0,
"Fds.IFAPI.APIFuelTankStates": 0,
"Fds.IFAPI.APIAircraftInfo": 0,
"Fds.IFAPI.APILightsState": 0,
"Fds.IFAPI.APIAutopilotState": 0,
"Fds.IFAPI.IFAPIStatus": 0,
"Fds.IFAPI.APINearestAirportsResponse": 0,
"Fds.IFAPI.APIFlightPlan": 0,
"Fds.IFAPI.LiveAirplaneList": 0,
"Fds.IFAPI.FacilityList": 0,
"Fds.IFAPI.APIFrequencyInfoList": 0
},
activeIntervals: { // Active intervals for polling for each info type
},
initSocketOnHostDiscovered: true,
closeUDPSocketOnHostDiscovered: true,
log: function(msg,level) { // generic logging function
if (IFC.enableLog) {
if (level <= IFC.logLevel) { console.log (IFC.name, msg); }
}
},
beforeInitSocket: function() { IFC.log("Connecting...", INFO); }, // What to do before connecting to sockit
onHostUndefined: function() { IFC.log("Host Undefined", INFO); }, // What to do if host is undefined
onHostSearchStarted: function() { IFC.log("Searching for host", INFO); }, // What to do when starting search for host
onSocketConnected: function() { // What to do when connected
IFC.log("Connected", MANDATORY);
},
onSocketConnectionError: function() { IFC.log("Connection error", MANDATORY); }, // What to do on a connection error
onHostDiscovered: function(host, port, callback) { IFC.log("Host Discovered", INFO); }, // What to do when host discovered
onDataReceived: function(dataString) { // What to do when receiving data back from API
if (dataString.match("{")) {
var results = IFC._dataParse(dataString);
for (i in results) {
var data = results[i];
if (data.Type) { // Store structured data results in ifData objects
IFC.log("Storing " + data.Type, ERROR);
IFC.log("Data: " + JSON.stringify(data), INFO);
IFC.ifData[data.Type] = data;
}
IFC.eventEmitter.emit('IFCdata',data); // Return data to calling script through an event
}
}
},
onHostSearchFailed: function() {}, // What to do if search failed
// SHORTCUTS FUNCTIONS //
init: function(successCallback, errorCallback, params) { // Initialise module
if (successCallback) IFC.onSocketConnected = successCallback; // Set success callback function
if (errorCallback) IFC.onSocketConnectionError = errorCallback; // Set error callback function
if (params.intervals) IFC.setPollingIntervals(params.intervals); // Set poll timeouts
if (params.enableLog) IFC.enableLog = params.enableLog; // Set Logging on/off
if (params.loggingLevel) IFC.loggingLevel = params.loggingLevel; // Set logging message level
if (params.host && params.port) { // Host provided so connect directly to it
IFC.infiniteFlight.serverAddress = params.host;
IFC.infiniteFlight.serverPort = params.port;
IFC.initIFClient(IFC.infiniteFlight.serverAddress, IFC.infiniteFlight.serverPort);
} else { // No host provided so search for a host via UDP
IFC.searchHost(successCallback, errorCallback); // Search for Infinite Flight host
}
},
searchHost: function(successCallback, errorCallback) { // Search for an Infinite Flight host
if (IFC.infiniteFlight.discoverSocket) return;
IFC.infiniteFlight.discoverSocket = dgram.createSocket('udp4');
IFC.infiniteFlight.discoverSocket.on('message', function (info){
IFC.log("Discover socket : data received", INFO);
var dataStr = info;
IFC.log(dataStr, INFO);
var data = {};
try {
data = JSON.parse(IFC._ab2str(dataStr));
IFC.log(data, INFO);
} catch(e) {
IFC.log("Discover socket : parsing error", INFO);
}
if (data.Addresses && data.Port) {
IFC.log("Host Discovered", INFO);
// IFC.isConnected = true;
// IFC.infiniteFlight.serverAddress = data.Addresses[0];
IFC.infiniteFlight.serverAddress = "";
for (var i = 0; i < data.Addresses.length; i++) { // Find an IP v4 address
if (isIp.v4(data.Addresses[i])) {
IFC.infiniteFlight.serverAddress = data.Addresses[i];
}
}
IFC.infiniteFlight.serverPort = data.Port;
IFC.initIFClient(IFC.infiniteFlight.serverAddress, IFC.infiniteFlight.serverPort);
IFC.infiniteFlight.discoverSocket.close(function() {
IFC.infiniteFlight.discoverSocket = false;
});
} else {
IFC.onDataReceived(data);
}
});
IFC.infiniteFlight.discoverSocket.on('listening', function(){
var address = IFC.infiniteFlight.discoverSocket.address();
IFC.log("IF discoverer listening on :" + address.address + ":" + address.port, INFO);
});
IFC.infiniteFlight.discoverSocket.on('error', function(e){
IFC.log(e, MANDATORY);
IFC.eventEmitter.emit('IFCCommandError',e); // Return data to calling script through an event
});
IFC.infiniteFlight.discoverSocket.on('close', function(){
IFC.log("DGRAM closed", MANDATORY);
IFC.eventEmitter.emit('IFCCommandError',"DGRAM closed"); // Return data to calling script through an event
});
IFC.infiniteFlight.discoverSocket.bind(IFC.infiniteFlight.broadcastPort);
},
initIFClient: function(host, port) { // Initiaise the module
IFC.log('initializing socket', INFO);
if (IFC.infiniteFlight.clientSocket) IFC.infiniteFlight.clientSocket.end();
IFC.beforeInitSocket();
if (!host) { return IFC.onHostUndefined(); }
IFC.infiniteFlight.clientSocket = new net.Socket();
IFC.infiniteFlight.clientSocket.connect(port, host, function() {
IFC.log('Connected to IF server ' + host, MANDATORY);
IFC.isConnected = true;
IFC.startPollingIntervals();
IFC.onSocketConnected();
});
IFC.infiniteFlight.clientSocket.on('data', function(data) {
IFC.log('Received: ' + data, INFO);
try {
IFC.onDataReceived(IFC._ab2str(data));
} catch(e) {
IFC.log(e, MANDATORY);
}
});
IFC.infiniteFlight.clientSocket.on('close', function(e) {
IFC.infiniteFlight.clientSocket = false;
IFC.eventEmitter.emit('IFCCommandError',{"type": "info", code: "close", context: "client", "msg": "Polling socket connection to Infinite Flight closed"}); // Return data to calling script through an event
});
IFC.infiniteFlight.clientSocket.on('drain', function(e) {
IFC.log(e, MANDATORY);
IFC.eventEmitter.emit('IFCCommandError',{"type": "info", code: "drain", context: "client", "msg": "Polling socket connection to Infinite Flight drained"}); // Return data to calling script through an event
});
IFC.infiniteFlight.clientSocket.on('lookup', function(e) {
IFC.log(e, MANDATORY);
});
IFC.infiniteFlight.clientSocket.on('end', function(e) {
IFC.log(e, MANDATORY);
IFC.eventEmitter.emit('IFCCommandError',{"type": "info", code: "end", context: "client", "msg": "Polling socket connection to Infinite Flight ended"}); // Return data to calling script through an event
});
IFC.infiniteFlight.clientSocket.on('timeout', function(e) {
IFC.log(e, MANDATORY);
IFC.eventEmitter.emit('IFCCommandError',{"type": "error", code: "timeout", context: "client", "msg": "Timeout on polling socket connection to Infinite Flight"}); // Return data to calling script through an event
});
IFC.infiniteFlight.clientSocket.on('error', function(e) {
IFC.log(e, MANDATORY);
IFC.eventEmitter.emit('IFCCommandError',{"type": "error", code: "error", context: "client", "msg": "Error polling Infinite Flight"}); // Return data to calling script through an event
});
},
cmd: function(cmd) { // Send a command to IF API -- short form
IFC.sendCommand({
"Command": "Commands." + cmd,
"Parameters": []
});
},
getAirplaneState: function(onDataReceived) { // Get airplane state -- redundant?
if (onDataReceived) IFC.onDataReceived = onDataReceived;
IFC.sendCommand({ "Command": "Airplane.GetState", "Parameters": []});
},
sendCommand: function(cmd) { // Send a command to IF API
IFC.log("Sending command " + JSON.stringify(cmd), ERROR);
try {
var jsonStr = JSON.stringify(cmd);
var data = new Uint8Array(jsonStr.length + 4);
data[0] = jsonStr.length;
for (var i = 0; i < jsonStr.length; i++) {
data[i+4] = jsonStr.charCodeAt(i);
}
var buffer = Buffer.from(data);
IFC.infiniteFlight.clientSocket.write(buffer);
IFC.eventEmitter.emit('IFCCommandSent',cmd); // Return data to calling script through an event
IFC.log("Emitting IFCCommandSent for " + cmd, INFO);
} catch(e) {
IFC.log(e);
IFC.eventEmitter.emit('IFCCommandError',cmd); // Return data to calling script through an event
IFC.log("Emitting IFCommandError for " + cmd, INFO);
}
},
setPollingIntervals: function(intervals) { // Set poll timeouts
for (key in intervals) {
IFC.pollingIntervals[key] = intervals[key];
IFC.log("Setting interval for " + key + ": " + intervals[key], MANDATORY);
}
// if (IFC.isConnected) { IFC.startPollingIntervals; }
},
resetPollingIntervals: function(intervals) { // Reset poll intervals and retart them
IFC.setPollingIntervals(intervals);
IFC.startPollingIntervals(intervals);
},
startPollingIntervals: function() { // Start polling
if (IFC.isConnected) {
for (var key in IFC.pollingIntervals) {
IFC.log("Clear polling for " + key,MANDATORY);
clearInterval(IFC.activeIntervals[key]);
if (IFC.pollingIntervals[key] > 0) {
IFC.log("Starting polling for " + key, MANDATORY);
IFC.intervalCommands[key](IFC.pollingIntervals[key]);
}
}
}
},
stopPolling: function() { // Stop polling
for (var key in IFC.pollingIntervals) {
IFC.log("Clear polling for " + key,MANDATORY);
clearInterval(IFC.activeIntervals[key]);
}
},
// Converters from https://developer.chrome.com/trunk/apps/app_hardware.html
// String to ArrayBuffer
_str2ab: function(str) {
var buf=new ArrayBuffer(str.length);
var bufView=new Uint8Array(buf);
for (var i=0; i<str.length; i++) {
bufView[i]=str.charCodeAt(i);
}
return buf;
},
// ArrayBuffer to String
_ab2str: function(buf) {
var dataString = String.fromCharCode.apply(null, new Uint8Array(buf));
return dataString;
},
_dataParse: function(dataString) {
// var regex = /^[^{]*({[\S\s]*})[^}]*$/g;
var regex = /}[^{^}]+{/g;
var dataString = dataString.replace(regex, "},{")
regex = /^[^{]*({[\S\s]*})[^}]*$/;
dataString = dataString.replace(regex, "$1");
regex = /:NaN/g;
dataString = dataString.replace(regex, ":\"NaN\"");
var resultString = "[" + dataString + "]";
IFC.log("resultString: " + resultString,INFO);
return JSON.parse(resultString);
}
};
module.exports = IFC;