Skip to content

Instantly share code, notes, and snippets.

@skylord123
Last active May 23, 2024 16:34
Show Gist options
  • Save skylord123/ad3e7d1952276d9f199a3ae2e79d8673 to your computer and use it in GitHub Desktop.
Save skylord123/ad3e7d1952276d9f199a3ae2e79d8673 to your computer and use it in GitHub Desktop.
Node-RED integration with Torque Script

NodeRED Integration for Torque Script

This script provides functionality for integrating with a Node-RED instance over a TCP connection from within the Torque Game Engine. It allows for sending messages to and receiving messages from Node-RED, facilitating communication between the game and external systems or devices controlled by Node-RED.

If you have never heard of Node-RED it basically allows you to connect various things (whether that be devices or services) together in new and interesting ways.

You can easily run Node-RED in a docker container on your local system for testing. See the Node-RED docs: https://nodered.org/docs/getting-started/docker

Example flow

image

You can import this flow by copying the contents of flow.json and using the import option in the Node-RED hamburger menu. You can then inject messages directly via the Node-RED web interface to send the example payloads to TS. This will also output all received messages to the debug sidebar in Node-RED via the debug node.

Torque Script Example

exec("base/nodeRed.cs");
getNodeRed().setDebugLevel(2);
getNodeRED().connect($BOT::ENV::NODE_RED::HOST);
if(!getNodeRED().hasReceiveCallback("socket_data_in")) {
  getNodeRED().addReceiveCallback("socket_data_in");
}
  
function socket_data_in(%msg) {
  // implement your own logic here
  echo("Node-RED: " @ %msg);
}
  
// send a message
getNodeRED().send("hi!");

jettison for JSON support

I've had great success in using jettison with this code. Parsing JSON on the TS side is painfully slow but serializing it is actually pretty fast. So you can send quite a lot of data back to Node-RED. Usually when you send commands to TS you can simplify them (ex: eval|echo("TS code ran from Node-RED");) so the need for JSON on that side isn't really there.

[{"id":"7b4f836b.c63e3c","type":"tcp out","z":"5fd187793239d604","name":"","host":"","port":"1881","beserver":"reply","base64":false,"end":false,"tls":"","x":560,"y":700,"wires":[]},{"id":"1993f5c2.4a154a","type":"tcp in","z":"5fd187793239d604","name":"","server":"server","host":"","port":"1881","datamode":"stream","datatype":"utf8","newline":"\\n\\n\\n","topic":"","trim":false,"base64":false,"tls":"","x":160,"y":580,"wires":[["d461697586d0556c"]]},{"id":"4c02e612c942936e","type":"inject","z":"5fd187793239d604","name":"Example data","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"commandToServer|action","payloadType":"str","x":190,"y":740,"wires":[["ccbe06d13f5d025c"]]},{"id":"ccbe06d13f5d025c","type":"function","z":"5fd187793239d604","name":"append newline","func":"msg.payload += \"\\n\";\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":700,"wires":[["7b4f836b.c63e3c"]]},{"id":"7151fa07a0ef8bf6","type":"inject","z":"5fd187793239d604","name":"Hello!","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello from Node-RED!","payloadType":"str","x":170,"y":700,"wires":[["ccbe06d13f5d025c"]]},{"id":"d461697586d0556c","type":"debug","z":"5fd187793239d604","name":"Debug incoming data","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":360,"y":580,"wires":[]},{"id":"cde49441308b2799","type":"comment","z":"5fd187793239d604","name":"Receive from TS","info":"","x":180,"y":540,"wires":[]},{"id":"710b0f8c8874ad21","type":"comment","z":"5fd187793239d604","name":"Send to TS","info":"","x":160,"y":660,"wires":[]}]
//-----------------------------------------------------------------------------
// NodeRED Integration for Torque Script
// Created by: skylord123 <github.com/skylord123>
//
// This script provides functionality for integrating with a Node-RED instance
// over a TCP connection from within the Torque Game Engine. It allows for
// sending messages to and receiving messages from Node-RED, facilitating
// communication between the game and external systems or devices controlled
// by Node-RED.
//
// Usage:
// - The `getNodeRED()` global function retrieves the singleton NodeRED object,
// creating it if it doesn't exist. This object manages the TCP connection and
// communication.
//
// - Call `NodeRED::connect(%host)` to establish a connection with the Node-RED
// instance. The `%host` parameter should specify the host and port (e.g.,
// "localhost:1880").
//
// - Use `NodeRED::send(%msg)` to send messages to the connected Node-RED instance.
// Ensure you are connected before attempting to send.
//
// - Register callback functions using `NodeRED::addReceiveCallback(%func)` to
// handle messages received from Node-RED. Each callback function must be
// capable of accepting a single argument: the message string received.
//
// - Disconnect from Node-RED using `NodeRED::disconnect()`.
//
// Debugging:
// - Set the debug level with `NodeRED::setDebugLevel(%level)`, where `%level`
// controls the verbosity of debug output. Level 0 suppresses all messages,
// level 1 shows connection state changes, and level 2 logs all sent and
// received messages.
//
// Example sending and receiving:
// ```
// getNodeRED().connect("localhost:1881");
// if(!getNodeRED().hasReceiveCallback("socket_data_in")) {
// getNodeRED().addReceiveCallback("socket_data_in");
// }
// getNodeRED().send("player_message|Skylord|Hey Buddy!");
//
// function socket_data_in(%msg) {
// error("Received: " @ %msg);
// }
// ```
//
// This script enhances the game's capability to interact with hardware,
// services, and other systems by leveraging the powerful flow-based programming
// environment of Node-RED.
//-----------------------------------------------------------------------------
// Define a global function to retrieve the NodeRED object, creating it if it doesn't exist.
function getNodeRED() {
if (!isObject(NodeRED)) {
%nodeRed = new ScriptObject(NodeRED) {
className = "NodeRED";
};
}
return NodeRED;
}
// Class definition for NodeRED
function NodeRED::onAdd(%this) {
// Initialize variables
%this.connection = ""; // TCPObject will be assigned here when connecting
%this.receiveCallbacks = ""; // Store callbacks
%this.receiveCallbacksLength = 0;
%this.debugLevel = 1;
%this.host = "localhost:1881";
}
function NodeRED::setHost(%this, %host) {
if (%this.host $= %host) {
return false;
}
%this.host = %host;
return true;
}
function NodeRED::isConnected(%this) {
return isObject(%this.connection) && %this.connection.connected;
}
function NodeRED::connect(%this, %host) {
if (%this.isConnected()) {
return false;
}
if (strLen(%host)) {
%this.setHost(%host);
}
if (!strLen(%this.host)) {
%this._debug("Missing host", 1);
return false;
}
%this._debug("Connecting..", 1);
if (!isObject(%this.connection)) {
%this.connection = new TCPObject(NodeRedTCP) {
class = "NodeRedTCP";
nodeRed = %this; // Link back to NodeRED object
};
}
%this.connection.connecting = true;
%this.connection.connectionAttempts++;
%this.connection.connect(%this.host);
return true;
}
function NodeRED::disconnect(%this) {
if (%this.isConnected()) {
%this.connection.disconnect();
return true;
}
return false;
}
function NodeRED::send(%this, %msg) {
if (%this.isConnected()) {
%this._debug("SENDING -> " @ %msg, 2);
%this.connection.send(%msg @ "\n");
return true;
}
return false;
}
function NodeRED::_debug(%this, %msg, %level) {
if (%this.debugLevel >= %level) {
error("[Node-RED" @ (%this.host !$= "" ? " " @ %this.host : "") @ "] " @ %msg);
}
}
function NodeRED::setDebugLevel(%this, %level) {
%this.debugLevel = %level;
}
function NodeRED::addReceiveCallback(%this, %func) {
%this.receiveCallbacksLength++;
%this.receiveCallbacks[%this.receiveCallbacksLength] = %func;
}
function NodeRED::hasReceiveCallback(%this, %func) {
for (%i = 1; %i <= %this.receiveCallbacksLength; %i++) {
if (%this.receiveCallbacks[%i] $= %func) {
return true;
}
}
return false;
}
function NodeRedTCP::_retryConnect(%this) {
if (%this.connected) return false;
%connectionDelay = %this.connectionAttempts < 5 ? 1000 : 5000;
%this.nodeRed._debug(%this.state @ " - retrying connection in " @ %connectionDelay @ "ms (attempts=" @ %this.connectionAttempts @ ")", 1);
%this.nodeRed.schedule(%connectionDelay, "connect", "");
return true;
}
function NodeRedTCP::onAdd(%this) {
%this.connected = false;
%this.connecting = false;
%this.connectionAttempts = 0;
}
function NodeRedTCP::onDNSFailed(%this) {
%this.state = "DNS resolution failed";
%this.connecting = false;
%this.connected = false;
if(!%this._retryConnect()){
%this.nodeRed._debug("DNS resolution failed for " @ %this.nodeRed.host, 1);
}
}
function NodeRedTCP::onConnected(%this) {
%this.state = "Connected";
%this.connected = true;
%this.connecting = false;
%this.connectionAttempts = 0;
%this.nodeRed._debug("Connected", 1);
}
function NodeRedTCP::onDisconnect(%this) {
%this.state = "Disconnected";
%this.connected = false;
%this.connecting = false;
if(!%this._retryConnect()){
%this.nodeRed._debug(%this.state, 1);
}
}
function NodeRedTCP::onConnectFailed(%this) {
%this.state = "Connection failed";
%this.connecting = false;
%this.connected = false;
if(!%this._retryConnect()){
%this.nodeRed._debug(%this.state, 1);
}
}
function NodeRedTCP::onLine(%this, %line) {
%this.nodeRed._debug("RECEIVED <- " @ %line, 2);
for (%i = 1; %i <= %this.nodeRed.receiveCallbacksLength; %i++) {
call(%this.nodeRed.receiveCallbacks[%i], %line);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment