/**
* StreamRoller Copyright 2023 "SilenusTA https://www.twitch.tv/olddepressedgamer"
*
* StreamRoller is an all in one streaming solution designed to give a single
* 'second monitor' control page and allow easy integration for configuring
* content (ie. tweets linked to chat, overlays triggered by messages, hue lights
* controlled by donations etc)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// ############################# msfs2020.js ##############################
// Allows data to be sent/received from Microsoft Flight Sim 2020
// ---------------------------- creation --------------------------------------
// Author: Silenus aka twitch.tv/OldDepressedGamer
// GitHub: https://github.com/SilenusTA/StreamRoller
// Date: 01-Jul-2023
// ============================================================================
/**
* @extension MSFS2020
* Game extension to allow access to control Microsoft Flight simulator and also receive data from on of ten's of thousands of sim variables
*/
// ============================================================================
// IMPORTS/VARIABLES
// ============================================================================
import * as fs from "fs";
import { MSFS_API, SystemEvents } from 'msfs-simconnect-api-wrapper';
import { SimVars } from "msfs-simconnect-api-wrapper/simvars/index.js";
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import * as logger from "../../backend/data_center/modules/logger.js";
import sr_api from "../../backend/data_center/public/streamroller-message-api.cjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const localConfig = {
OUR_CHANNEL: "MSFS2020_CHANNEL",
EXTENSION_NAME: "msfs2020",
SYSTEM_LOGGING_TAG: "[EXTENSION]",
DataCenterSocket: null,
MAX_CONNECTION_ATTEMPTS: 5,
state: {
color: "red",
msfsconnected: false,
msfsconnecting: false
},
heartBeatTimeout: 5000,
heartBeatHandle: null,
pollMSFSHandle: null,
msfs_api: new MSFS_API(),
msfs_api_connected_handle: null,
//msfs_api_recveventhandler: null,
//maintain a list of trigger handles for deregistering
EventCallabackHandles: [],
previousValue: [], // holds the last value read so we can provide 'onchange' only triggers
dumpTriggers: false //autodocs needs to manually parse this so we dump it to file and manually create the readme triggers and actions table
};
const default_serverConfig = {
__version__: "0.2.1",
extensionname: localConfig.EXTENSION_NAME,
channel: localConfig.OUR_CHANNEL,
msfs2020ennabled: "off",
msfs2020SimPollInterval: "5",
msfs2020extension_restore_defaults: "off"
};
let serverConfig = structuredClone(default_serverConfig)
const default_triggersandactions =
{
extensionname: serverConfig.extensionname,
description: "Connects to Microsoft Flight Sim 2020 and reads/writes simvars. when connected to MSFS2020 thousands more triggers/options will be available than appear in this default list. <BR> ie you can set a trigger on simvar 'FLAPS HANDLE INDEX' index 0 position 2 to trigger an action when the flaps are set to position 2<BR>Ie you can set an action on the simvar 'GENERAL ENG THROTTLE LEVER POSITION' using index 1 and a value of 50 to set the postition of throttle 1 to 50%. Please feel free to post useful triggers and actions on teh discord server for others to play with<BR><B>DON'T FORGET TO TURN ON MONITORING FOR ANY VARS YOU WANT TO TRIGGER ON (IN THE SETTINGS PAGE)</B>",
version: "0.2",
channel: serverConfig.channel,
triggers:
[
{
name: "onRequest_PLANE LATITUDE LONGITUDE",
displaytitle: "Current lat/long (OnRequest)",
description: "The current lat long in one message with separate variables",
messagetype: "trigger_onRequest_PLANE LATITUDE LONGITUDE",
parameters: { lat: "", long: "" }
}
],
actions:
[
{
name: "PLANE LATITUDE LONGITUDE_get",
displaytitle: "Get lat/long (Get)",
description: "Get the current lat long in one message",
messagetype: "action_PLANE LATITUDE LONGITUDE_get",
parameters: {}
}
],
}
let triggersandactions = structuredClone(default_triggersandactions)
const default_serverData =
{
__version__: "0.2",
SimVars: {},
EventVars:
["AIRCRAFT_LOADED", "CRASHED", "CRASH_RESET", "CUSTOM_MISSION_ACTION_EXECUTED"
, "FLIGHT_LOADED", "FLIGHT_SAVED", "FLIGHT_PLAN_ACTIVATED", "FLIGHT_PLAN_DEACTIVATED"
, "OBJECT_ADDED", "OBJECT_REMOVED", "PAUSE", "PAUSE_EX1", "PAUSED", "POSITION_CHANGED"
, "SIM_START", "SIM_STOP", "UNPAUSED", "VIEW"
],
triggersNamesArray: [] // names of triggers we are monitoring ie. FLAPS_POSITION:1
}
let serverData = structuredClone(default_serverData)
// ============================================================================
// FUNCTION: initialise
// ============================================================================
/**
* Starts the extension using the given data.
* @param {object:Express} app
* @param {string} host
* @param {number} port
* @param {number} heartbeat
*/
function initialise (app, host, port, heartbeat)
{
try
{
if (typeof (heartbeat) != "undefined")
localConfig.heartBeatTimeout = heartbeat;
else
logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".initialise", "DataCenterSocket no heatbeat passed:", heartbeat);
localConfig.DataCenterSocket = sr_api.setupConnection(onDataCenterMessage, onDataCenterConnect,
onDataCenterDisconnect, host, port);
} catch (err)
{
logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".initialise", "localConfig.DataCenterSocket connection failed:", err);
}
}
// ============================================================================
// FUNCTION: onDataCenterDisconnect
// ============================================================================
/**
* Called when connection is lost to StreamRoller
* @param {string} reason
*/
function onDataCenterDisconnect (reason)
{
logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterDisconnect", reason);
}
// ============================================================================
// FUNCTION: onDataCenterConnect
// ============================================================================
/**
* Called when server connects
* @param {object} socket
*/
function onDataCenterConnect (socket)
{
logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterConnect", "Creating our channel");
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket("RequestConfig", serverConfig.extensionname));
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket("RequestData", serverConfig.extensionname));
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket("CreateChannel", serverConfig.extensionname, serverConfig.channel)
);
clearTimeout(localConfig.heartBeatHandle);
heartBeatCallback();
}
// ============================================================================
// FUNCTION: onDataCenterMessage
// ============================================================================
/**
* Called when we receive a message from the server
* @param {object} server_packet
*/
function onDataCenterMessage (server_packet)
{
if (server_packet.type === "ConfigFile")
{
if (server_packet.data != "" && server_packet.to === serverConfig.extensionname)
{
if (server_packet.data.__version__ != default_serverConfig.__version__)
{
serverConfig = structuredClone(default_serverConfig);
console.log("\x1b[31m" + serverConfig.extensionname + " ConfigFile Updated", "The config file has been Restored. Your settings may have changed" + "\x1b[0m");
}
else
serverConfig = structuredClone(server_packet.data);
SaveConfigToServer();
pollMSFS();
}
}
else if (server_packet.type === "DataFile")
{
if (server_packet.data != "")
{
if (server_packet.data.__version__ != default_serverData.__version__)
{
serverData = structuredClone(default_serverData)
console.log("\x1b[31m" + serverConfig.extensionname + " DataFile Updated", "The data file has been Restored. Your settings may have changed" + "\x1b[0m");
}
if (server_packet.to === serverConfig.extensionname && server_packet.data != undefined && server_packet.data.SimVars != undefined
&& Object.keys(server_packet.data.SimVars).length > 0)
{
// SaveDataToServer();
serverData = structuredClone(server_packet.data)
// connect to msfs
initSimVarsandTriggers();
pollMSFS();
}
}
}
else if (server_packet.type === "ExtensionMessage")
{
let extension_packet = server_packet.data;
if (extension_packet.type === "RequestSettingsWidgetSmallCode")
SendSettingsWidgetSmall(extension_packet.from);
else if (extension_packet.type === 'RequestSettingsWidgetLargeCode')
SendSettingsWidgetLarge(extension_packet.from);
else if (extension_packet.type === "SettingsWidgetSmallData")
{
if (extension_packet.data.extensionname === serverConfig.extensionname)
{
let reconnect = false
let disconnect = false
// check if we are changing to on
if (serverConfig.msfs2020ennabled == "off" && extension_packet.data.msfs2020ennabled == "on")
reconnect = true
// check if we are changing to off
else if (serverConfig.msfs2020ennabled == "on" && !extension_packet.data)
disconnect = true
serverConfig.msfs2020ennabled = "off";
for (const [key, value] of Object.entries(extension_packet.data))
serverConfig[key] = value;
SaveConfigToServer();
if (reconnect)
{
initSimVarsandTriggers()
pollMSFS()
sendTriggersAndActions(server_packet.from)
}
else if (disconnect)
{
MSFS2020Disconnect()
}
else
pollMSFS()
SendSettingsWidgetSmall("");
SendSettingsWidgetLarge("");
}
}
else if (extension_packet.type === "SettingsWidgetLargeData")
{
if (extension_packet.to === serverConfig.extensionname)
{
if (extension_packet.data.demoextension_restore_defaults == "on")
{
serverConfig = structuredClone(default_serverConfig);
serverData = structuredClone(default_serverData)
console.log("\x1b[31m" + serverConfig.extensionname + " ConfigFile and Data files Updated.", "The config file has been Restored. Your settings may have changed" + "\x1b[0m");
}
else
{
let reconnect = false
if (serverConfig.msfs2020ennabled == "off" && extension_packet.data.msfs2020ennabled == "on")
reconnect = true
handleSettingsWidgetLargeData(extension_packet.data)
SaveConfigToServer();
SaveDataToServer();
if (reconnect || (extension_packet.data.msfs2020ennabled == "on" && !localConfig.state.msfsconnected))
{
initSimVarsandTriggers()
pollMSFS()
sendTriggersAndActions(server_packet.from)
}
// broadcast our modal out so anyone showing it can update it
SendSettingsWidgetSmall("");
SendSettingsWidgetLarge("");
}
}
}
else if (extension_packet.type === "SendTriggerAndActions")
{
if (extension_packet.to === serverConfig.extensionname)
sendTriggersAndActions(server_packet.from)
}
else if (extension_packet.type.indexOf("action_" == 0))
{
if (extension_packet.to === serverConfig.extensionname)
{
if (serverConfig.msfs2020ennabled == "on")
{
// test for our home grown versions
if (extension_packet.type == "action_PLANE LATITUDE LONGITUDE_get")
{
performActionGetLatLong(extension_packet)
}
else
performAction(extension_packet)
}
}
}
else
logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "received unhandled ExtensionMessage ", server_packet);
}
else if (server_packet.type === "UnknownChannel")
{
logger.info(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "Channel " + server_packet.data + " doesn't exist, scheduling rejoin");
setTimeout(() =>
{
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket(
"JoinChannel", serverConfig.extensionname, server_packet.data
));
}, 5000);
}
else if (server_packet.type === "ChannelData")
{
let extension_packet = server_packet.data;
if (extension_packet.type === "HeartBeat")
{
//Just ignore messages we know we don't want to handle
}
else
{
logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "received message from unhandled channel ", server_packet.dest_channel);
}
}
else if (server_packet.type === "InvalidMessage")
{
logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage",
"InvalidMessage ", server_packet.data.error, server_packet);
}
else if (server_packet.type === "LoggingLevel")
{
logger.setLoggingLevel(server_packet.data)
}
else if (server_packet.type === "ChannelJoined"
|| server_packet.type === "ChannelCreated"
|| server_packet.type === "ChannelLeft")
{
// just a blank handler for items we are not using to avoid message from the catchall
}
// ------------------------------------------------ unknown message type received -----------------------------------------------
else
logger.warn(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
".onDataCenterMessage", "Unhandled message type", server_packet.type);
}
// ===========================================================================
// FUNCTION: SendSettingsWidgetSmall
// ===========================================================================
/**
* Parses and sends out our small settings widget to the given extension
* @param {string} to
*/
function SendSettingsWidgetSmall (to)
{
fs.readFile(__dirname + '/msfs2020settingswidgetsmall.html', function (err, filedata)
{
if (err)
logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
".SendSettingsWidgetSmall", "failed to load modal", err);
//throw err;
else
{
let modalstring = filedata.toString();
for (const [key, value] of Object.entries(serverConfig))
{
if (value === "on")
modalstring = modalstring.replace(key + "checked", "checked");
else if (typeof (value) == "string")
modalstring = modalstring.replace(key + "text", value);
}
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket(
"ExtensionMessage",
serverConfig.extensionname,
sr_api.ExtensionPacket(
"SettingsWidgetSmallCode",
serverConfig.extensionname,
modalstring,
"",
to,
serverConfig.channel
),
"",
to
))
}
});
}
// ===========================================================================
// FUNCTION: handleSettingsWidgetSmallData
// ===========================================================================
/**
* Handles user submitted code from our large settings widget/page
* @param {object} modalcode
*/
function handleSettingsWidgetLargeData (modalcode)
{
/////////////////////////////////////////////////
// Restore Defaults
/////////////////////////////////////////////////
if (modalcode.msfs2020_restore_defaults == "on")
{
console.log("MSFS defaults restored")
console.log("\x1b[31m" + serverConfig.extensionname + " ConfigFile Updated", "The config file has been Restored. Your settings may have changed" + "\x1b[0m");
serverConfig = structuredClone(default_serverConfig);
serverData = structuredClone(default_serverData)
return;
}
//Clear our previous triggers (we will re-add the ones we have been sent)
serverData.triggersNamesArray = [];
/////////////////////////////////////////////////
// get serverConfig values
/////////////////////////////////////////////////
for (const [key, value] of Object.entries(modalcode))
{
if (key in serverConfig)
serverConfig[key] = value;
}
/////////////////////////////////////////////////
// get SimVar values
/////////////////////////////////////////////////
const varKeys = Object.keys(serverData.SimVars);
// loop through all our keys as we need to know what has been changed
for (let i = 0; i < varKeys.length; i++)
{
// check if we have been sent one (must be checked/set to "on" to be sent)
if (varKeys[i] in modalcode)
{
if (serverData.SimVars[varKeys[i]].enabled != modalcode[varKeys[i]])
{
// it is set to on and ours is off
serverData.SimVars[varKeys[i]].enabled = "on"
}
// else if it is the same do nothing
}
else
{
// not been sent it so it must be off
if (serverData.SimVars[varKeys[i]].enabled == "on")
{
// we are currently set to on and need to turn off
serverData.SimVars[varKeys[i]].enabled = "off"
}
}
// have we been sent a value (box checked)
if (varKeys[i] in modalcode)
{
// check for an index number simvar
if (varKeys[i].indexOf(":index") > 0)
{
let txtfieldname = varKeys[i].replace(":index", "") + "_index"
// check we have a field for the index ()
addToTriggersArray(varKeys[i], modalcode[txtfieldname])
serverData.SimVars[varKeys[i]].index = modalcode[txtfieldname]
}
else
{
// check we have a field for the index ()
addToTriggersArray(varKeys[i])
serverData.SimVars[varKeys[i]].index = modalcode[varKeys[i]]
}
}
}
}
// ===========================================================================
// FUNCTION: SendSettingsWidgetLarge
// ===========================================================================
/**
* Creates and sends our large settings widget html code to the given extension
* @param {string} to
*/
function SendSettingsWidgetLarge (to)
{
let generatedpage = "";
let first = true
let readwritestate = ""
fs.readFile(__dirname + "/msfs2020settingswidgetlarge.html", function (err, filedata)
{
if (err)
{
logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
".SendSettingsWidgetLarge", "failed to load modal", err);
//throw err;
}
else
{
//get the file as a string
let modalstring = filedata.toString();
//////////////////////////////////////////////////
// mormal replaces(ie enabled and reset checkboxes
//////////////////////////////////////////////////
for (const [key, value] of Object.entries(serverConfig))
{
// checkboxes
if (value === "on")
modalstring = modalstring.replace(key + "checked", "checked");
else if (typeof (value) === "string" || typeof (value) === "number")
modalstring = modalstring.replaceAll(key + "text", value);
}
//////////////////////////////////////////////////
// Enabled Simvars
// Done separately so we can delete them easier
//////////////////////////////////////////////////
const triggerKeys = Object.keys(serverData.triggersNamesArray);
first = false;
generatedpage = ""
generatedpage += "Polling too many Sim Variables too fast can cause lag in the game. Check the tool tips by hovering mouse over the SimVar name for description</p>"
generatedpage += "<p>Note: A simvar needs to be monitored to be able to set a trigger on it.</p>"
generatedpage += "<p> An example trigger might be to position your camera in obs to show your height. To do this you would set the trigger to ... <BR>"
generatedpage += "<BR>TRIGGER: 'msfs2020', 'trigger_INDICATED ALTITUDE'"
generatedpage += "<BR>ACTION: 'obs','action_SetSceneItemTransform','SceneName'= 'Camera_feed', 'sourceName' = '#4x3_Cam', 'positionY' = '((%%data%% - 10000) / (0 - 10000)) * (1040-50) + 50'"
generatedpage += "<BR>In this example my group name that the camera is in is called 'Camera_feed' and the camera source is called '#4x3_Cam'"
generatedpage += "<BR>I set the positionY of the camera based on the value in the 'data' field of the trigger (altitude from MSFS) but as this is a big range it needs converting to the pixel position in obs"
generatedpage += "<BR>The equasion set converts an alt range of 0 to 10,000 feet into a pixel position of 50 to 1040 pixels (what I need for my screen res in obs)"
generatedpage += "<HR><h5>Currently Active Simvars</H5> "
for (let i = 0; i < triggerKeys.length; i++)
{
// every 5 items start a new row but only close out after the first itme
if (!first && i % 5 == 0)
generatedpage += "</tr>"
if (i % 5 == 0)
generatedpage += "<tr>"
// ''''''''''''''''''''' TD '''''''''''''''''''''''''''''''''
generatedpage += "<td scope='row'>" + serverData.triggersNamesArray[triggerKeys[i]]
// Does this variable have an index part
if (serverData.triggersNamesArray[triggerKeys[i]].indexOf(":index") > 0)
generatedpage += ":" + serverData.triggersNamesArray[triggerKeys[i]].index
//check that we have this value
if (typeof (serverData.triggersNamesArray[triggerKeys[i]].enabled) == "undefined")
serverData.triggersNamesArray[triggerKeys[i]].enabled == "off"
generatedpage += "</td>"
first = false;
}
if (!first)
generatedpage += "</tr>"
modalstring = modalstring.replace("MSFSActiveCode", generatedpage);
//////////////////////////////////////////////////
// SimVars
//////////////////////////////////////////////////
const varKeys = Object.keys(serverData.SimVars);
first = false;
generatedpage = ""
generatedpage += "<h5>Simvars Available</H5><p>Select checkbox to set as a Trigger. Some simvars have "
generatedpage += "an index (ie GENERAL ENG RPM:index) for these an index number is needed to identify "
generatedpage += "a specific item (ie which engine rpm). The letters in brackets indicate it the item "
generatedpage += "is readonly (R) or ReadWrite (RW)</p>"
for (let i = 0; i < varKeys.length; i++)
{
// every 5 items start a new row but only close out after the first itme
if (!first && i % 5 == 0)
generatedpage += "</tr>"
if (i % 5 == 0)
generatedpage += "<tr>"
// ''''''''''''''''''''' TD '''''''''''''''''''''''''''''''''
// are we able to set this vaiable
if (serverData.SimVars[varKeys[i]].settable == true)
readwritestate = "(RW)"
else
readwritestate = "(R)"
generatedpage += "<td scope='row'>" + serverData.SimVars[varKeys[i]].name + " " + readwritestate + " "
// Does this variable have an index part
if (serverData.SimVars[varKeys[i]].name.indexOf(":index") > 0)
{
let fieldname = serverData.SimVars[varKeys[i]].name.replace(":index", "") + "_index"
generatedpage += "<input type='text' style='width: 30px' name='" + fieldname + "'"
generatedpage += " id='" + fieldname + "' value='" + serverData.SimVars[varKeys[i]].index + "'> "
}
//check that we have this value
if (typeof (serverData.SimVars[varKeys[i]].enabled) == "undefined")
serverData.SimVars[varKeys[i]].enabled == "off"
if (serverData.SimVars[varKeys[i]].enabled == "on")
generatedpage += " <input class='form-check-input' name='" + serverData.SimVars[varKeys[i]].name + "' type='checkbox' id='" + serverData.SimVars[varKeys[i]].name + "' checked >"
else
generatedpage += " <input class='form-check-input' name='" + serverData.SimVars[varKeys[i]].name + "' type='checkbox' id='" + serverData.SimVars[varKeys[i]].name + "'>"
generatedpage += "</td>"
first = false;
}
if (!first)
generatedpage += "</tr>"
modalstring = modalstring.replace("MSFSSimVarsCode", generatedpage);
//////////////////////////////////////////////////
// Send out the new code
//////////////////////////////////////////////////
// send the modified modal data to the server
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket(
"ExtensionMessage", // this type of message is just forwarded on to the extension
serverConfig.extensionname,
sr_api.ExtensionPacket(
"SettingsWidgetLargeCode", // message type
serverConfig.extensionname, //our name
modalstring,// data
"",
to,
serverConfig.channel
),
"",
to // in this case we only need the "to" channel as we will send only to the requester
))
}
});
// done as we might have added some enabled entries
SaveDataToServer();
}
// ============================================================================
// FUNCTION: SaveConfigToServer
// ============================================================================
/**
* Saves our config to the server
*/
function SaveConfigToServer ()
{
sr_api.sendMessage(localConfig.DataCenterSocket, sr_api.ServerPacket
("SaveConfig",
serverConfig.extensionname,
serverConfig))
}
// ============================================================================
// FUNCTION: SaveDataToServer
// ============================================================================
/**
* Saves our data to the server
*/
function SaveDataToServer ()
{
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket(
"SaveData",
serverConfig.extensionname,
serverData));
}
// ============================================================================
// FUNCTION: heartBeat
// ============================================================================
/**
* Sends out heartbeat messages so other extensions can see our status
*/
function heartBeatCallback ()
{
localConfig.state.msfsconnected = localConfig.msfs_api.connected
localConfig.state.color = "red"
if (serverConfig.msfs2020ennabled == "on")
{
// if we are not connected and we should be then attempt to reconnect
if (!localConfig.state.msfsconnected && !localConfig.state.msfsconnecting)
msfsapiconnect()
if (localConfig.state.msfsconnected != true)
localConfig.state.color = "orange"
else
localConfig.state.color = "green"
}
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket("ChannelData",
serverConfig.extensionname,
sr_api.ExtensionPacket(
"HeartBeat",
serverConfig.extensionname,
localConfig.state
,
serverConfig.channel),
serverConfig.channel
),
);
localConfig.heartBeatHandle = setTimeout(heartBeatCallback, localConfig.heartBeatTimeout)
}
// ############################################################################
// MSFS specific code
// ############################################################################
// ============================================================================
// FUNCTION: initSimVarsandTriggers
// ============================================================================
/*
serverData.SimVars['ACCELERATION BODY X']
simvar example {
desc: 'Acceleration relative to aircraft X axis, in east/west direction',
units: 'feet',
data_type: 4,
settable: false,
name: 'ACCELERATION BODY X',
enabled: 'off'
}
*/
/**
* initialises our sim variables dynamically creates triggers and actions based on what is available.
* Note there can be thousands of possible options created here
*/
function initSimVarsandTriggers ()
{
if (serverConfig.msfs2020ennabled == "on")
{
/// *************** Create our Simvars array *****************************
// make a clone of the simVars (so we can add extra fields for enabled etc)
const varKeys = Object.keys(SimVars);
varKeys.sort();
for (let i = 0; i < varKeys.length; i++)
{
if (typeof serverData.SimVars[varKeys[i]] == "undefined")
serverData.SimVars[varKeys[i]] = SimVars[varKeys[i]]
// this creates the enabled variable if it doesn't exist
if (serverData.SimVars[varKeys[i]].enabled != "on")
serverData.SimVars[varKeys[i]].enabled = "off"
if (varKeys[i].indexOf(":index") > 0 && typeof (serverData.SimVars[varKeys[i]].index) == "undefined")
{
serverData.SimVars[varKeys[i]].index = "0"
//(serverData.SimVars[varKeys[i]])
}
}
SaveDataToServer()
/// *************** Create our triggers and actions *****************************
// Create triggers and actions
triggersandactions = {}
triggersandactions = structuredClone(default_triggersandactions)
/// *************** Add Event variables *************************
for (let i = 0; i < serverData.EventVars.length; i++)
{
triggersandactions.triggers.push(
{
name: SystemEvents[serverData.EventVars[i]].name,
displaytitle: SystemEvents[serverData.EventVars[i]].name,
description: SystemEvents[serverData.EventVars[i]].desc,
messagetype: "trigger_" + SystemEvents[serverData.EventVars[i]].name,
parameters: { data: "" }
});
triggersandactions.triggers.push(
{
name: "onChange_" + SystemEvents[serverData.EventVars[i]].name,
displaytitle: SystemEvents[serverData.EventVars[i]].name + " (OnChange)",
description: SystemEvents[serverData.EventVars[i]].desc,
messagetype: "trigger_onChange_" + SystemEvents[serverData.EventVars[i]].name,
parameters: { data: "" }
});
}
//// *************** Add SimVars *************************
for (let i = 0; i < varKeys.length; i++)
{
//// *************** If it is writeable add an action to write the variable *************************
if (serverData.SimVars[varKeys[i]].settable == true)
{
if (varKeys[i].indexOf(":index") > 0)
{
triggersandactions.actions.push(
{
name: serverData.SimVars[varKeys[i]].name,
displaytitle: serverData.SimVars[varKeys[i]].name + "(Set)",
description: "Set the simvar " + serverData.SimVars[varKeys[i]].desc + ": Units " + serverData.SimVars[varKeys[i]].units,
messagetype: "action_" + serverData.SimVars[varKeys[i]].name,
parameters: { index: "0", data: "" }
}
)
}
else
{
triggersandactions.actions.push(
{
name: serverData.SimVars[varKeys[i]].name,
displaytitle: serverData.SimVars[varKeys[i]].name + "(Set)",
description: "Set the simvar " + serverData.SimVars[varKeys[i]].desc + ": Units " + serverData.SimVars[varKeys[i]].units,
messagetype: "action_" + serverData.SimVars[varKeys[i]].name,
parameters: { data: "" }
}
)
}
}
//// *************** If readable add an action to read it *************************
if (varKeys[i].indexOf(":index") > 0)
{
triggersandactions.actions.push(
{
name: serverData.SimVars[varKeys[i]].name + "_get",
displaytitle: serverData.SimVars[varKeys[i]].name + "(Get)",
description: "Request the value, will be returned in a 'name_(Single) 'trigger" + serverData.SimVars[varKeys[i]].desc + ": Units " + serverData.SimVars[varKeys[i]].units,
messagetype: "action_" + serverData.SimVars[varKeys[i]].name + "_get",
parameters: { index: "0" }
}
)
}
else
{
triggersandactions.actions.push(
{
name: serverData.SimVars[varKeys[i]].name + "_get",
displaytitle: serverData.SimVars[varKeys[i]].name + "(Get)",
description: "Request the value, will be returned in a 'name_(Single) 'trigger" + serverData.SimVars[varKeys[i]].desc + ": Units " + serverData.SimVars[varKeys[i]].units,
messagetype: "action_" + serverData.SimVars[varKeys[i]].name + "_get",
parameters: {}
}
)
}
// triggers with indexes
let paramdata = {}
if (varKeys[i].indexOf(":index") > 0)
{
paramdata = { index: "0", data: "" }
}
else
{
paramdata = { data: "" }
}
triggersandactions.triggers.push
(
{
name: serverData.SimVars[varKeys[i]].name,
displaytitle: serverData.SimVars[varKeys[i]].name + " (Poll)",
description: serverData.SimVars[varKeys[i]].desc + ": Units " + serverData.SimVars[varKeys[i]].units + ": settable " + serverData.SimVars[varKeys[i]].settable,
messagetype: "trigger_" + serverData.SimVars[varKeys[i]].name,
parameters: paramdata
}
)
// add an onchange trigger (we only send this when data changes)
triggersandactions.triggers.push
(
{
name: "onChange_" + serverData.SimVars[varKeys[i]].name,
displaytitle: serverData.SimVars[varKeys[i]].name + " (OnChange)",
description: serverData.SimVars[varKeys[i]].desc + ": Units " + serverData.SimVars[varKeys[i]].units + ": settable " + serverData.SimVars[varKeys[i]].settable,
messagetype: "trigger_onChange_" + serverData.SimVars[varKeys[i]].name,
parameters: paramdata
}
)
triggersandactions.triggers.push
(
{
name: "onRequest_" + serverData.SimVars[varKeys[i]].name,
displaytitle: serverData.SimVars[varKeys[i]].name + " (OnRequest)",
description: serverData.SimVars[varKeys[i]].desc + ": Units " + serverData.SimVars[varKeys[i]].units + ": settable " + serverData.SimVars[varKeys[i]].settable,
messagetype: "trigger_onRequest_" + serverData.SimVars[varKeys[i]].name,
parameters: paramdata
}
)
}
}
/* uncomment to log the data structures if you are interested in the data we get from msfs */
//file_log("SimVars", serverData.SimVars)
//file_log("triggersandactions", triggersandactions)
//file_log("SystemEvents", SystemEvents)
}
// ============================================================================
// FUNCTION: msfsapiconnect
// ============================================================================
/**
* connect to the MSFS2020 api interface
*/
function msfsapiconnect ()
{
if (serverConfig.msfs2020ennabled == "on")
{
localConfig.state.msfsconnecting = true;
localConfig.msfs_api.connect({ onConnect: (nodeSimconnectHandle) => MSFS2020RegisterSimvars(nodeSimconnectHandle) })
.catch(err =>
{
logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".msfsapiconnect", "Connection failed. Reconnecting in", serverConfig.msfs2020SimPollInterval, "seconds");
})
/* no idea how to put a handler in for recvevents :( I'll come back to this later
console.log(localConfig.msfs_api_recveventhandler)
if (localConfig.state.connected && localConfig.msfs_api_recveventhandler == null)
localConfig.msfs_api.on("exception", () =>
{
console.log("oops recv received")
})
*/
}
}
// ============================================================================
// FUNCTION: MSFS2020Disconnect
// ============================================================================
/**
* disconnect from the MSFS2020 interface and callbacks
*/
function MSFS2020Disconnect ()
{
clearTimeout(localConfig.pollMSFSHandle)
localConfig.state.msfsconnected = false
for (let i = 0; i < serverData.EventVars.length; i++)
{
if (localConfig.EventCallabackHandles[serverData.EventVars[i]])
localConfig.EventCallabackHandles[serverData.EventVars[i]]()
}
for (let i = 0; i < serverData.AirportVars.length; i++)
{
if (localConfig.EventCallabackHandles[serverData.AirportVars[i]])
localConfig.EventCallabackHandles[serverData.AirportVars[i]]()
}
}
// ============================================================================
// FUNCTION: MSFS2020RegisterSimvars
// ============================================================================
/**
* Registers with MSFS2020 for callbacks on simvars
* @param {object} handle simconnecnt handle
*/
async function MSFS2020RegisterSimvars (handle)
{
try
{
localConfig.state.msfsconnected = true;
localConfig.state.msfsconnecting = false;
localConfig.msfs_api_connected_handle = handle;
localConfig.EventHandles = []
for (let i = 0; i < serverData.EventVars.length; i++)
{
localConfig.EventCallabackHandles[serverData.EventVars[i]]
= localConfig.msfs_api.on(SystemEvents[serverData.EventVars[i]], (data) =>
{
let triggertopost = findtriggerByMessageType("trigger_" + SystemEvents[serverData.EventVars[i]].name)
triggertopost.parameters.data = data;
if (data != localConfig.previousValue[serverData.EventVars[i]])
{
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket(
"ChannelData",
serverConfig.extensionname,
sr_api.ExtensionPacket(
"trigger_onChange_" + SystemEvents[serverData.EventVars[i]].name,
serverConfig.extensionname,
triggertopost,//{ index: simvarindex, data: data[simvar] },
serverConfig.channel
),
serverConfig.channel
));
localConfig.previousValue[serverData.EventVars[i]] = data;
}
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket(
"ChannelData",
serverConfig.extensionname,
sr_api.ExtensionPacket(
"trigger_" + SystemEvents[serverData.EventVars[i]].name,
serverConfig.extensionname,
triggertopost,//{ index: simvarindex, data: data[simvar] },
serverConfig.channel
),
serverConfig.channel
));
});
}
/*
this is left in as a hint if we decide to add this in future. Airports appear
to use a different api that quick testing seemed to fail on.
localConfig.NEARBY_AIRPORTS = await localConfig.msfs_api.get(`NEARBY_AIRPORTS`);
//console.log(`${localConfig.NEARBY_AIRPORTS.length} nearby airports`);
localConfig.ALL_AIRPORTS = await localConfig.msfs_api.get(`ALL_AIRPORTS`);
//console.log(`${localConfig.ALL_AIRPORTS.length} total airports on the planet`);
*/
//const airportData = await localConfig.msfs_api.get(`AIRPORT:CYYJ`);
//console.log(JSON.stringify(airportData, null, 2));
//console.log(JSON.stringify(await localConfig.msfs_api.get(`AIRPORT:CYYJ`), null, 2));
/*
console.log("AIRPORTS_IN_RANGE", AIRPORTS_IN_RANGE)
const inRange = localConfig.msfs_api.on(AIRPORTS_IN_RANGE, (data) =>
{
console.log("in range airports:", data);
inRange();
});
/*
const outOfRange = localConfig.msfs_api.on(SystemEvents.AIRPORTS_OUT_OF_RANGE, (data) =>
{
console.log("outOfRange",data);
outOfRange();
});
*/
if (localConfig.dumpTriggers)
dumpTriggersActions()
}
catch (err)
{
localConfig.state.msfsconnected = false;
logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".MSFS2020RegisterSimvars", "Error ", err.message);
}
}
// ============================================================================
// FUNCTION: addToTriggersArray
// ============================================================================
function dumpTriggersActions ()
{
setTimeout(() =>
{
file_log("", "", true)
}, 10000);
}
// ============================================================================
// FUNCTION: addToTriggersArray
// ============================================================================
/**
* Adds a simvar to our known list of names. A simvar may be an array and have an index (ie engines have an index for each engine)
* others may not have index's
* @param {string} name
* @param {number} [index=0]
*/
function addToTriggersArray (name, index = 0)
{
if (name.indexOf(":index") > 0)
name = name.replace(":index", ":" + index)
// edge case when the user doesn't have this in their datafile
if (typeof serverData.triggersNamesArray == "undefined")
serverData.triggersNamesArray = [];
if (!serverData.triggersNamesArray.includes(name))
serverData.triggersNamesArray.push(name);
// make it alphabetical
serverData.triggersNamesArray.sort();
}
// ============================================================================
// FUNCTION: postTriggers
// ============================================================================
/**
* posts a trigger out when a given symvar fires. Will also post a "changed" trigger
* if the value was previously different
* possible message types posted (NAME) is the simvar name and (INDEX) is the
* index if an indexed simvar
* trigger_(NAME)
* trigger_(NAME):(INDEX)
* trigger_onChange_(NAME)
* trigger_onChange_(NAME):(INDEX)
*/
async function postTriggers ()
{
let index = ""
let simvarindex = 0
let name = ""
let simvar = "";
let indexToSplit = 0;
let data = ""
let messageType = ""
let onChangeMessageType = ""
//check if connected
if (!localConfig.state.msfsconnected)
return;
for (index in serverData.triggersNamesArray)
{
// check if we have an index (':n' part)
if (serverData.triggersNamesArray[index].indexOf(":") > 0)
{
// the name will have the index attached so we need to split it up
simvar = serverData.triggersNamesArray[index]
indexToSplit = simvar.indexOf(':');
name = simvar.slice(0, indexToSplit);
simvarindex = simvar.slice(indexToSplit + 1);
simvar = simvar.replaceAll(" ", "_")
}
else
{
name = serverData.triggersNamesArray[index]
simvar = serverData.triggersNamesArray[index]
simvar = simvar.replaceAll(" ", "_")
}
// get the simvar data
try
{
data = await localConfig.msfs_api.get(simvar);
}
catch (err)
{
console.log("Error during get", err.message)
}
// set the message type
if (simvar.indexOf(":") > 0)
{
messageType = "trigger_" + name + ":index"
onChangeMessageType = "trigger_onChange_" + name + ":index"
}
else
{
messageType = "trigger_" + name
onChangeMessageType = "trigger_onChange_" + name
}
let triggertopost = findtriggerByMessageType(messageType)
triggertopost.parameters.index = simvarindex;
triggertopost.parameters.data = data[simvar];
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket(
"ChannelData",
serverConfig.extensionname,
sr_api.ExtensionPacket(
messageType,
serverConfig.extensionname,
triggertopost,//{ index: simvarindex, data: data[simvar] },
serverConfig.channel
),
serverConfig.channel
));
if (localConfig.previousValue[simvar] != triggertopost.parameters.data)
{
let triggertopost = findtriggerByMessageType(onChangeMessageType)
triggertopost.parameters.index = simvarindex;
triggertopost.parameters.data = data[simvar];
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket(
"ChannelData",
serverConfig.extensionname,
sr_api.ExtensionPacket(
onChangeMessageType,
serverConfig.extensionname,
triggertopost,//{ index: simvarindex, data: data[simvar] },
serverConfig.channel
),
serverConfig.channel
));
localConfig.previousValue[simvar] = triggertopost.parameters.data
}
}
}
// ============================================================================
// FUNCTION: performActionGetLatLong
// ============================================================================
/**
* Special simvar just for streamroller :D
* Creates a simvar containing both lat and long rather
* than having to create two requests actions and getting two responses back
* @param {object} data
*/
function performActionGetLatLong (data)
{
let action = ""
if (localConfig.msfs_api.connected != true)
return;
// Request for data, not a set
if (data.type == "action_PLANE LATITUDE LONGITUDE_get")
{
try
{
let onRequestMessageType = "trigger_onRequest_PLANE LATITUDE LONGITUDE"
localConfig.msfs_api.get("PLANE LATITUDE")
.then(data =>
{
let triggertopost = findtriggerByMessageType(onRequestMessageType)
// returned data doesn't have spaces it has underscores
triggertopost.parameters.lat = String(data["PLANE_LATITUDE"] * 180 / Math.PI);
localConfig.msfs_api.get("PLANE LONGITUDE")
.then(data =>
{
triggertopost.parameters.long = String(data["PLANE_LONGITUDE"] * 180 / Math.PI);
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket(
"ChannelData",
serverConfig.extensionname,
sr_api.ExtensionPacket(
triggertopost.messagetype,
serverConfig.extensionname,
triggertopost,//{ index: simvarindex, data: data[simvar] },
serverConfig.channel
),
serverConfig.channel
));
}).catch(error => console.log("ERROR:performActionGetLatLong(2): Failed to get simvar", error))
}).catch(error => console.log("ERROR:performActionGetLatLong(1):Failed to get simvar", error))
}
catch (err)
{
console.log("ERROR:performActionGetLatLong: Error during get", err.message)
}
}
}
// ============================================================================
// FUNCTION: performAction
// ============================================================================
/**
* An action was received to set/change a simvar or retrieve some data
* A "trigger_onRequest_(simvarname)" will be fired on a request
* @param {object} data
*/
function performAction (data)
{
let action = ""
// Request for data, not a set
if (data.type.indexOf("_get") > -1)
{
try
{
let simvar = data.type.replace("action_", "").replace("_get", "")
let onRequestMessageType = "trigger_onRequest_" + simvar
localConfig.msfs_api.get(simvar)
.then(data =>
{
let triggertopost = findtriggerByMessageType(onRequestMessageType)
// returned data doesn't have spaces it has underscores
simvar = simvar.replaceAll(" ", "_")
triggertopost.parameters.data = data[simvar];
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket(
"ChannelData",
serverConfig.extensionname,
sr_api.ExtensionPacket(
triggertopost.messagetype,
serverConfig.extensionname,
triggertopost,//{ index: simvarindex, data: data[simvar] },
serverConfig.channel
),
serverConfig.channel
));
}).catch(error => console.log("ERROR:performAction: Failed to get simvar", error))
}
catch (err)
{
console.log("ERROR:performAction: Error during get", err.message)
}
} else
{
try
{
action = data.type.replace("action_", "")
if (action.indexOf(":index") > 0)
action = action.replace("index", data.data.index)
localConfig.msfs_api.set(action, data.data.data)
}
catch (err)
{
console.log("ERROR:performAction ", err.message)
}
}
}
// ============================================================================
// FUNCTION: pollMSFS
// ============================================================================
/**
* Poll MSFS for data we are monitoring for
*/
function pollMSFS ()
{
clearTimeout(localConfig.pollMSFSHandle);
if (serverConfig.msfs2020ennabled == "on")
postTriggers();
localConfig.pollMSFSHandle = setTimeout(pollMSFS, serverConfig.msfs2020SimPollInterval * 1000)
}
// ============================================================================
// FUNCTION: findtriggerByMessageType
// ============================================================================
/**
* Sends a list of our current triggers/actions ot the given extension
* @param {string} to
*/
function sendTriggersAndActions (to)
{
sr_api.sendMessage(localConfig.DataCenterSocket,
sr_api.ServerPacket("ExtensionMessage",
serverConfig.extensionname,
sr_api.ExtensionPacket(
"TriggerAndActions",
serverConfig.extensionname,
triggersandactions,
"",
to
),
"",
to
)
)
}
// ============================================================================
// FUNCTION: findtriggerByMessageType
// ============================================================================
/**
* Finds a specific trigger from the given name
* @param {string} messagetype
*/
function findtriggerByMessageType (messagetype)
{
for (let i = 0; i < triggersandactions.triggers.length; i++)
{
if (triggersandactions.triggers[i].messagetype.toLowerCase() == messagetype.toLowerCase()) return triggersandactions.triggers[i];
}
logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
".findtriggerByMessageType", "failed to find action", messagetype);
}
// ============================================================================
// FUNCTION: file_log
// For debug purposes. logs message data to file
// ============================================================================
let filestreams = [];
let basedir = "msfsdata/";
function file_log (type, data, apidoc = false)
{
try
{
if (apidoc)
{
let filename = serverConfig.extensionname + "_apidoctriggers.json"
fs.writeFileSync(filename, JSON.stringify(triggersandactions, null, 2), {
encoding: "utf8",
flag: "w+",
});
}
else
{
//console.log("file_log", type, tags, message)
var newfile = false;
var filename = "__noname__";
var buffer = "\n//#################################\n";
// sometimes tags are a string, lets create an object for it to log
if (typeof tags != "object")
data = { data: data }
if (!fs.existsSync(basedir))
{
newfile = true;
fs.mkdirSync(basedir, { recursive: true });
}
// check if we already have this handler
if (!filestreams.type)
{
filename = type;
filestreams[type] = fs.createWriteStream(basedir + filename + ".js", { flags: 'a' });
}
if (newfile)
{
buffer += "let " + type + ";\n";
buffer += "let message=" + JSON.stringify(data, null, 2) + "\n"
}
else
buffer += "let message=" + JSON.stringify(data, null, 2) + "\n"
filestreams[type].write(buffer);
//bad coding but can't end it here (due to async stuff) and it is just debug code (just left as a reminder we have a dangling pointer)
filestreams[type].end("")
}
}
catch (error)
{
console.log("debug file logging crashed", error.message)
}
}
// ============================================================================
// EXPORTS
// Note that initialise is mandatory to allow the server to start this extension
// ============================================================================
export { initialise, triggersandactions };