node-red-contrib-xkeys_joystick
Version:
Xkeys joystick node for Node-RED using DCD Protocol
291 lines (267 loc) • 9.84 kB
JavaScript
module.exports = function(RED) {
const XKEYS_VENDOR_ID = "1523";
var mqtt = require('mqtt');
const connectUrl = 'mqtt://localhost';
const qos = 0; // Connection to local broker should be good
var httpAdminDataProducts = {};
var httpAdminDataDevices = {};
function XkeysJoystick(config) {
RED.nodes.createNode(this, config);
var node = this;
node.config = config;
//node.log("node.config = " + JSON.stringify(node.config));
//node.log("myId = " + node.config.id);
var client = mqtt.connect(connectUrl);
client.on('reconnect', (error) => {
node.log('reconnecting:', error)
})
client.on('error', (error) => {
node.log('Connection failed:', error)
})
client.on('connect', () => {
node.log('connected')
//client.subscribe('/dcdp/server/#', function (err, granted) {
client.subscribe({'/dcdp/server/#':{qos:qos}}, function (err, granted) {
if (!err) {
node.log("Subscribed OK, granted: " + JSON.stringify(granted));
client.publish('/dcdp/node', JSON.stringify({msg_type:"all_product_data"}));
client.publish('/dcdp/node', JSON.stringify({msg_type:"list_attached"}));
} else {
node.log('Subscription failed: ' + err)
}
})
})
client.on('close', () => {
node.log("connection closed");
})
client.on('message', (topic, message) => {
//node.log(`received topic:${topic}, msg: ${message}`);
var message_obj = "";
try {
message_obj = JSON.parse(message);
//node.log(`SID = ${message_obj.server_id}`);
if (message_obj.msg_type == "hello") {
console.log(`Hello from DCDP server at ${message_obj.server_id} - must have just (re)started `);
// In case dcdp-server restarted with updated devices/product list
client.publish('/dcdp/node', JSON.stringify({msg_type:"all_product_data"}));
client.publish('/dcdp/node', JSON.stringify({msg_type:"list_attached"}));
}
else if (message_obj.msg_type == "list_attached_result") {
// data should be a dict of info objects
httpAdminDataDevices = [];
message_obj.devices.forEach( (device) => {
if (device.vendorId == XKEYS_VENDOR_ID) {
httpAdminDataDevices.push(device);
}
});
var pid_list = this.config.pid_list || "[]";
if (device_connected(this.config.vendor_id || XKEYS_VENDOR_ID, JSON.parse(pid_list), this.config.unit_id, this.config.duplicate_id)) {
node.status( {fill:"green",shape:"dot",text:"connected"} );
} else {
node.status( {fill:"red",shape:"ring",text:"disconnected"} );
}
}
else if (message_obj.msg_type == "all_product_data_result") {
// data should be a dict of product objects
httpAdminDataProducts = {};
// Restrict products to X-keys devices
Object.keys(message_obj.data).forEach( (device) => {
if (message_obj.data[device].vendorId == XKEYS_VENDOR_ID ) {
httpAdminDataProducts[device] = message_obj.data[device];
}
});
}
else if (message_obj.msg_type == "device_layout_result") {
//console.log(`device_layout_result: ${JSON.stringify(message_obj.svg_layout)}`);
var layout = message_obj.svg_layout;
const devicenam = Object.keys(layout)[0]
/* Only proceed if result is for this device
*/
if ((devicenam.length == 0) || (devicenam != previous_devicenam) ) {
return;
}
var devicedata = layout[devicenam];
if (Object.keys(devicedata).length == 0) {
//console.log(`devicedata: None was returned`);
/* Generate a buffer with data.length == 0
* Then client will respond by clearing existing layout
*/
layout[devicenam] = {"no_device":{"type":"Buffer","data":[]}};
devicedata = layout[devicenam];
}
//console.log(`devicenam: ${devicenam}`);
//console.log(`devicedata: ${JSON.stringify(devicedata)}`);
const svg_filename_base = Object.keys(devicedata)[0];
const buf = Buffer.from(layout[devicenam][svg_filename_base]);
//console.log(`buffer: ${buf}`);
var d = { id:node.config.id };
d.data = buf.toString("base64");
try {
RED.comms.publish("xkeys_joystick-image", d);
}
catch (err) {
console.error(err);
}
}
else if (message_obj.msg_type == "joystick_event") {
// Check that PID & UID are what we're interested in
var pid_list = this.config.pid_list || "[]";
var pids = JSON.parse(pid_list);
if (pids.length == 0 || pids.includes(parseInt(message_obj.product_id))) {
// Either no PID was configured i.e. any PID will do
// or this event is for a PID that has been configured
// Now filter Controller #
if (message_obj.control_id == this.config.control_id || this.config.control_id == "") {
// Either this event was for a configured control_id or no control_id was configured
// Now filter Unit #
if (message_obj.unit_id == this.config.unit_id || this.config.unit_id == "") {
// Either event was for a configured unit_id or no unit_id was configured
// Now filter Duplicate #
if (message_obj.duplicate_id == this.config.duplicate_id || this.config.duplicate_id == "") {
// Either event was for a configured duplicate_id or no duplicate_id was configured
// Prepare output msg
var msg = {};
msg["payload"] = {
"device" : message_obj.device,
"vendor_id" : message_obj.vendor_id,
"product_id" : message_obj.product_id,
"unit_id" : message_obj.unit_id,
"duplicate_id" : message_obj.duplicate_id,
"control_id" : message_obj.control_id,
"x" : message_obj.x,
"y" : message_obj.y,
"z" : message_obj.z,
"deltaZ" : message_obj.deltaZ
};
node.send(msg);
}
}
}
}
}
else {
// Logging here may be useful but is quietened for production
//node.log('Received unhandled request: ' + message_obj.request);
}
}
catch (e) {
node.log('ERROR parsing message: ' + e);
}
})
// Input
var previous_devicenam = "Any X-keys";
node.on('input', function(msg) {
const mkeys = Object.keys(msg.payload);
if (mkeys.includes("device") && (msg.payload.device == "Any X-keys")) {
// Erase any existing layout (send empty image)
var d = { id:node.id };
d.data = {"no_device":{"type":"Buffer","data":[]}}.toString("base64");
try {
RED.comms.publish("xkeys_joystick-image", d);
}
catch (err) {
console.error(err);
}
}
else if (mkeys.includes("msg_type") && (msg.payload.msg_type == "layout_output")) {
node.log(`Requesting: JSON.stringify({msg_type:"device_layout", device: msg.payload.device})`);
client.publish('/dcdp/node', JSON.stringify({msg_type:"device_layout", device: msg.payload.device}));
}
previous_devicenam = msg.payload["device"];
});
this.on('close', function(done) {
client.end();
done();
})
// Does any attached device match specified pids, unit_id & dup_id ?
// pids: array of possible PIDs for a device (empty => ANY)
// unit_id: unitId of a device
// dup_id: duplicate_id of a device
function device_connected(...deviceArgs) {
const vendor_id = deviceArgs[0];
const pids = deviceArgs[1];
const unit_id = deviceArgs[2];
const dup_id = deviceArgs[3];
const devs = Object.keys(httpAdminDataDevices);
var device_matched = false;
var regex_string = ""
var regex;
if (pids.length == 0) {
// No product_ids provided => ANY product_id
if (vendor_id) {
regex_string = vendor_id + "-";
} else {
regex_string = "\[0-9\]+-";
}
regex_string = regex_string + "\[0-9\]+-";
if (unit_id) {
regex_string = regex_string + unit_id + "-";
} else {
regex_string = regex_string + "\[0-9\]+-";
}
if (dup_id) {
regex_string = regex_string + dup_id;
} else {
regex_string = regex_string + "\[0-9\]+";
}
regex = new RegExp(regex_string);
httpAdminDataDevices.forEach( (item) => {
if (regex.test(item.device_quad)) { device_matched = true; }
});
} else {
// An array of endpoints provided
pids.forEach(function (item) {
if (vendor_id) {
regex_string = vendor_id + "-";
} else {
regex_string = "\[0-9\]+-";
}
regex_string = regex_string + item + "-";
if (unit_id) {
regex_string = regex_string + unit_id + "-";
} else {
regex_string = regex_string + "\[0-9\]+-";
}
if (dup_id) {
regex_string = regex_string + dup_id;
} else {
regex_string = regex_string + "\[0-9\]+";
}
regex = new RegExp(regex_string);
httpAdminDataDevices.forEach( (item) => {
if (regex.test(item.device_quad)) { device_matched = true; }
});
});
}
return device_matched;
} // END function device_connected
} // END function XkeysJoystick
RED.nodes.registerType("xkeys_joystick", XkeysJoystick);
RED.httpAdmin.get("/xkeys_joystick/products", function (req, res) {
res.json(httpAdminDataProducts);
});
RED.httpAdmin.get("/xkeys_joystick/devices", function (req, res) {
res.json(httpAdminDataDevices);
});
RED.httpAdmin.post("/xkeys_joystick_inject/:id", RED.auth.needsPermission("xkeys_joystick_inject.write"), function(req,res) {
//console.log(`posting something: ${JSON.stringify(req.body)}`);
var node = RED.nodes.getNode(req.params.id);
if (node != null) {
try {
if (req.body) {
node.receive(req.body);
} else {
node.receive();
}
res.sendStatus(200);
}
catch (err) {
res.sendStatus(500);
node.error(RED._("inject.failed",{error:err.toString()}));
}
} else {
console.log("bad post");
res.sendStatus(404);
}
}); // END RED.httpAdmin.post()
}