UNPKG

node-red-contrib-xkeys_lcd

Version:

Xkeys LCD node for Node-RED using Dynamic Control Data Protocol (DCDP)

431 lines (392 loc) 13.6 kB
module.exports = function(RED) { const XKEYS_VENDOR_ID = "1523"; var mqtt = require('mqtt'); const connectUrl = 'mqtt://localhost'; const qos = 0; var httpAdminDataProducts = {}; var httpAdminDataDevices = []; var httpAdminDataBacklightOn = true; const defaultText = "Clockwork Sport" var textLines = [defaultText,""]; var textLines_line_numbers = []; for (var i=0;i<textLines.length;i++) { textLines_line_numbers.push(i+1); } var backlight_on = true; function XkeysLCD(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 flowContext = this.context().flow; 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/#':{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") { node.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 an array of infos httpAdminDataDevices = []; message_obj.devices.forEach( (device) => { // Limit to xkeys devices if (device.vendorId == XKEYS_VENDOR_ID) { httpAdminDataDevices.push(device); } }); // Based on attached deviceList, are we connected to something? 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"} ); textLines = [defaultText,""]; } } else if (message_obj.msg_type == "all_product_data_result") { // data should be a dict of product objects httpAdminDataProducts = {}; Object.keys(message_obj.data).forEach( (device) => { // Restrict to X-keys products if (message_obj.data[device].vendorId == XKEYS_VENDOR_ID ) { httpAdminDataProducts[device] = message_obj.data[device]; } }); } else if ((message_obj.msg_type == "command_result") && (message_obj.command_type == "write_lcd_display")) { node.log(`command_result: ${message}`); } else { // Logging here may be useful but is quietened for production //node.log(`Received unhandled request: ${message_obj.msg_type}`); } } catch (e) { node.log(`ERROR parsing message: ${e}`); } }) /* * Input messages * We expect msg.payload of various styles: * msg.payload = { lcdtext: [line1Text, line2Text] | TEXTSTRING, linenum: LINENUM, backlight: TRUEFALSE, unit_id: UID } * where only lcdtext is required, the others optional * * msg.payload = { lcdtext: TEXTSTRING } * in which case, other option values from node.config are used */ node.on('input', function(incoming) { var mkeys = Object.keys(incoming.payload); //node.log(`node.config = ${JSON.stringify(node.config)}`); //console.log(`Incoming: ${JSON.stringify(incoming)}`); if (mkeys.includes("lcdtext")) { /* This 'msg' object what is ultimately to feed method parameters * We build it up from the 'incoming' received here. */ var msg = {"payload":{}}; /* * We assume the LCD device has 2 lines. * This is true right now when the only such device is the XK16LCD. */ var default_linenum; if (node.config.linenum) { default_linenum = node.config.linenum - 1; } else { default_linenum = 1; // The second line } // Does it contain a line number preference? var line_number; if (mkeys.includes("linenum")) { line_number = incoming.payload.linenum - 1; } else { line_number = default_linenum; // already zero based } var incoming_text; if (typeof(incoming.payload.lcdtext) == "string") { incoming_text = incoming.payload.lcdtext; } else if (Array.isArray(incoming.payload.lcdtext)) { incoming_text = incoming.payload.lcdtext; } else { node.log("Bad lcdtext format, so discontinuing ..."); return; } // Interpret text " " as "delete the line" // Interpret text "" as "leave line alone" // i.e. use previous text and // only update textLines if lcdtext has something to give if (Array.isArray(incoming_text)) { var temp_line_number_array = []; for (var i=0;i<incoming_text.length;i++) { if (incoming_text[i].length > 0) { textLines[i] = incoming_text[i]; temp_line_number_array.push(i); } } line_number = temp_line_number_array; } else { if (incoming_text.length > 0) { textLines[line_number] = incoming_text; } } msg.payload["lcdtext"] = textLines; /* Disable this for now - not working properly // Does it contain a backlight instruction? // Otherwise default to backlight ON if (mkeys.includes("backlight")) { msg.payload["backlight"] = incoming.payload.backlight; httpAdminDataBacklightOn = incoming.payload.backlight; } else { msg.payload["backlight"] = httpAdminDataBacklightOn; } */ httpAdminDataBacklightOn = node.config.lcd_backlight || "true"; //console.log(`httpAdminDataBacklightOn: ${httpAdminDataBacklightOn}, ${(httpAdminDataBacklightOn=="true")?1:0}`) //console.log(`msg.payload = ${JSON.stringify(msg.payload)}`); var pid_list = "[]"; var unit_id = ""; var duplicate_id = ""; /* If product_id is specified, install it into pid_list * (similar to node configuration editor). * Otherwise use pid_list "as is". */ if (mkeys.includes("product_id")) { var temp = []; temp.push(parseInt(msg.payload.product_id)); pid_list = JSON.stringify(temp); } else { if (mkeys.includes("pid_list")) { pid_list = incoming.payload.pid_list; } else { pid_list = node.config.pid_list; } } //console.log(`pid_list: ${pid_list}`); if (mkeys.includes("unit_id")) { } else { unit_id = incoming.payload.unit_id; } if (mkeys.includes("duplicate_id")) { duplicate_id = incoming.payload.duplicate_id; } else { duplicate_id = node.config.duplicate_id; } /* Find first member of pid_list that matches an attached device. */ var product_id; try { var configured_pid_list = JSON.parse(pid_list); configured_pid_list.every( (pid) => { if (httpAdminDataDevices.find( (device) => device.productId == pid)) { product_id = pid.toString(); return false; } return true; }); } catch { node.log(`Caught exception parsing pid_list. ${err}`); } //console.log(`product_id = ${product_id}`); /* An empty pid_list will not have given us a valid product_id. * That's OK since empty pid_list => all possible product_ids. * However if no product_id has been found when pid_list is not empty, * it means no attached device matched what was in the pid_list * i.e. nothing to do */ if (!product_id) { if (JSON.parse(pid_list).length > 0) { node.log(`No device matching product_id or pid_list was found. Discontinuing.`); return; } else if (JSON.parse(pid_list).length == 0) { /* No product_id has been found/calculated and pid_list is empty. * Could be ANY or NONE selected - but is wrong if NONE. */ if (node.config.device == "NONE") { node.log(`Shouldn't be texting a NONE device! Discontinuing ...`); return; } } } /* Generate a list of device_quad targets. */ var quad_list = []; var quad_list_regex_string = XKEYS_VENDOR_ID; if (product_id) { quad_list_regex_string += "-" + product_id; } else { quad_list_regex_string += "-\[0-9\]+"; } if (unit_id) { quad_list_regex_string += "-" + unit_id; } else { quad_list_regex_string += "-\[0-9\]+"; } if (duplicate_id) { quad_list_regex_string += "-" + duplicate_id; } else { quad_list_regex_string += "-\[0-9\]+"; } //console.log(`quad_list_regex_string = ${quad_list_regex_string}`); var regex = new RegExp(quad_list_regex_string); httpAdminDataDevices.forEach( (dev) => { //console.log(`dev: ${JSON.stringify(dev.device_quad)}`); if (regex.test(dev.device_quad)) { quad_list.push(dev.device_quad); } }); //console.log(`quad_list = ${JSON.stringify(quad_list)}`); if (quad_list.length == 0 ) { node.log(`No devices matched: discontinuing`); return; } /* Prepare output msg but only bother if we have a device with matching device_quad. */ httpAdminDataDevices.every( (device) => { if (!device.hasLCD) { // Don't consider devices without hasLCD property return true; } if (quad_list.includes(device.device_quad)) { const command_message = { msg_type : "command", command_type : "write_lcd_display", vendor_id : device.device_quad.split('-')[0], product_id : device.device_quad.split('-')[1], unit_id : device.device_quad.split('-')[2], duplicate_id : device.device_quad.split('-')[3], line : textLines_line_numbers, text : msg.payload.lcdtext, backlight : (httpAdminDataBacklightOn=="true")?1:0 } node.log(`command_message = ${JSON.stringify(command_message)}`); client.publish('/dcdp/node', JSON.stringify(command_message)); // Don't break the every loop on first match - find all matches. //return false; } return true; }); } else { console.log("Not interested in this msg (no \"lcdtext\" field)"); } }) this.on('close', function(done) { client.end(); done(); }) // Does any attached device match specified vendor_id, pids, unit_id & dup_id ? function device_connected(...deviceArgs) { const vendor_id = deviceArgs[0]; const pids = deviceArgs[1]; const unit_id = deviceArgs[2]; const dup_id = deviceArgs[3]; var device_matched = false; var regex_string = "" var regex; if (pids.length == 0) { if (vendor_id) { regex_string = vendor_id + "-"; } else { regex_string = "\[0-9\]+-"; } // No product_ids provided => ANY product_id 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; } // function device_connected } // function XkeysLCD RED.nodes.registerType("xkeys_lcd", XkeysLCD); RED.httpAdmin.get("/xkeys_lcd/products", function (req, res) { res.json(httpAdminDataProducts); }); RED.httpAdmin.get("/xkeys_lcd/devices", function (req, res) { res.json(httpAdminDataDevices); }); RED.httpAdmin.get("/xkeys_lcd/backlight_on", function (req, res) { res.json(httpAdminDataBacklightOn); }); RED.httpAdmin.post("/xkeys_lcd_inject/:id", RED.auth.needsPermission("xkeys_lcd_inject.write"), function(req,res) { 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); } }); }