julianh2o / RokuAlexaLambdaSkill

An Alexa Skill that allows voice control of your Roku
MIT License
104 stars 55 forks source link

Skill response was marked as failure #15

Open michaeldeffendall opened 7 years ago

michaeldeffendall commented 7 years ago

Julian,

Let me start by saying your step by step guide was wonderful. One issue I had at first was that my alexa app didn't see the new skill because the email address that Amazon.com (and therefore, alexa) had was different than the one I wanted to use for my Amazon Developer account. That's a big no-no. Lambda doesn't seem to care. It sees the call requests, they are just failing.

Of note is on the Lambda function, after uploading the zip file, it shows an error on line 186 with the yellow triangle and ! with the message "Too many errors. (62% scanned)."

I double checked local IP, WAN IP, ports, and passwords, and they are all consistent., just to make sure that wasn't an error as well.

Thoughts on where to start? Thanks in advance for your help!

Michael P. Deffendall

julianh2o commented 7 years ago

Actually, @chris1642 contributed that guide. I'm glad you found it helpful!

An error like "Too many errors" sounds like there's something wrong at the top of the file or with the upload format/method. That error means that it found hundreds and hundreds of errors before that part of the file. If there are hundreds of errors before 62% scanned.. it's definitely something else going on.

Off the top of my head.. perhaps you have an invalid character in the beginning of the file, didn't correctly quote your APP ID or some other piece of information at the top of the file. Perhaps there are some weird invisible characters causing problems?

Check for those and let me know if you find anything.

julianh2o commented 7 years ago

Copy pasted JS files? You mean in one big block on the lambda dialog? That's definitely not right.. you should be uploading a zip file.

Can you post the zip file that is giving you the errors. (or email it to me?)

michaeldeffendall commented 7 years ago

File sent. And I only tried the copy/paste as the upload caused the error.

Mike

julianh2o commented 7 years ago

I don't see the file.. perhaps just upload it to pastebin or something like that? I only need index.js

michaeldeffendall commented 7 years ago

index.js.txt

julianh2o commented 7 years ago

Does it actually have a .txt extension? Because that could be the problem.. I don't see anything wrong with the top of this file otherwise.

michaeldeffendall commented 7 years ago

I had to add the TXT to upload it here

julianh2o commented 7 years ago

Hmm, I'm not sure where your problem might be then. You can try providing some more images with the errors you're seeing and the steps you're taking or perhaps try running through this tutorial to get a better feeling for how this is supposed to work and to see whether you can get a simple skill working.

My gut is still telling me that it's something along the lines of how you've uploaded or prepared your bundle for AWS Lambda.

chris1642 commented 7 years ago

Not sure if this reply works - but when the file was edited in notepad, it was saved as .txt - it needs to stay as .js.

An easy fix - within that compressed zip folder, right click on index.js.txt and rename. It should allow you to edit the entire name, including the extension .txt. Basically - remove the .txt and only leave .js. Then reupload. Also, if any other files renamed to .txt, same process.

Sent from my iPhone

On Jan 4, 2017, at 6:51 PM, michaeldeffendall notifications@github.com wrote:

index.js.txt

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

chris1642 commented 7 years ago

If the compressed zip folder doesn't allow you to change the extension, within any folder - you should have an option for "view" or file, view preferences (not in front of me to know for sure) - check the box that says something like "show file extensions"

Then right click on the file and so the same (no that the full extension will show and easily allow a change )

Sent from my iPhone

On Jan 4, 2017, at 8:08 PM, Chris Cusumano cwcusumano@gmail.com wrote:

Not sure if this reply works - but when the file was edited in notepad, it was saved as .txt - it needs to stay as .js.

An easy fix - within that compressed zip folder, right click on index.js.txt and rename. It should allow you to edit the entire name, including the extension .txt. Basically - remove the .txt and only leave .js. Then reupload. Also, if any other files renamed to .txt, same process.

Sent from my iPhone

On Jan 4, 2017, at 6:51 PM, michaeldeffendall notifications@github.com wrote:

index.js.txt

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

julianh2o commented 7 years ago

He said that he just changed the extension to upload it. I don't think that's the problem.

michaeldeffendall commented 7 years ago

I only made it a txt file for purposes of uploading. It was a .js when in the zip

On Jan 4, 2017 8:08 PM, "chris1642" notifications@github.com wrote:

Not sure if this reply works - but when the file was edited in notepad, it was saved as .txt - it needs to stay as .js.

An easy fix - within that compressed zip folder, right click on index.js.txt and rename. It should allow you to edit the entire name, including the extension .txt. Basically - remove the .txt and only leave .js. Then reupload. Also, if any other files renamed to .txt, same process.

Sent from my iPhone

On Jan 4, 2017, at 6:51 PM, michaeldeffendall notifications@github.com wrote:

index.js.txt

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/julianh2o/RokuAlexaLambdaSkill/issues/15#issuecomment-270545089, or mute the thread https://github.com/notifications/unsubscribe-auth/AXwEqKRX1ehPkkYOxuN4rd-6ZNZOoXO3ks5rPFC2gaJpZM4LbKkH .

chris1642 commented 7 years ago

Mike,

Outside of the file extension issue (if that is one), may be the same issue that someone else had and it's more unrelated to the lambda server (even though that is giving an error or just not working).

Will be home later this evening to offer help. Not a big deal if it's what I'm thinking.

Sent from my iPhone

On Jan 4, 2017, at 6:10 PM, michaeldeffendall notifications@github.com wrote:

Good news is I don't have any error messages anymore on Lambda. Bad news is it still doesn't work.

What I did was:

Attempt again to upload the .zip file, after removing all the code.. No change Removed all the code, copy and pasted the .js files content one at a time. No errors, but still not working. Does it matter the order of the files? I had index on top, then AlexaSkill and then serverinfo. Other thoughts?

Mike

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

chris1642 commented 7 years ago

Mike,

I don't believe the 'too many errors' within the Lambda server is the problem. I have that on mine now and it works fine (and since I uploaded that originally on my link, everyone has the same warning of errors and still seems to work).

As a side note - Lambda's test won't work unless you used Julian's test file for the lambda server (since the example 'hello world' function is not relevant) - but you can ignore that too....unrelated.

If you say something to your Echo and nothing happens....do this: go to developer.amazon.com, and into your skill - on the left side - you will see 'Test".

Within that page, under Service Simulator, within the 'text' box, type: "go home" (remove the quotations)

I am going to make a bet that you will get a lot of function-related text within the left side (Lambda Request)....but receive "The remote endpoint could not be called, or the response it returned was invalid." on the right side, (Lambda Response). This indicates that the lambda server is likely not the problem. The problem is one of a few things:

  1. You don't have the server.js file running (meaning the node server)
  2. the server.js file doesn't have the correct port number you selected for port forwarding.
  3. Port forwarding is set up incorrectly - so the Lambda server cannot reach your computer to get a response....this is usually the issue.

Hope this helps!

michaeldeffendall commented 7 years ago

Ok, I started from scratch and now I can't get the node server.js to see the Roku. I know I'm doing something stupid but I can't find it. I ran the npm install first.

Here is my server.js below. Roku is on .88. Using port forwarding on 32500.

When I run node server.js I get "Server listening on port 32500" and nothing else.

================================ var http = require('http'); var fs = require('fs'); var urllib = require("url"); var Client = require('node-ssdp').Client; var dgram = require('dgram');

//null will cause the server to discover the Roku on startup, hard coding a value will allow for faster startups // When manually setting this, include the protocol, port, and trailing slash eg: var rokuAddress = "http://192.168.1.88:8060/"; //var rokuAddress = null;

var PORT=32500; //this is the port you are enabling forwarding to. Reminder: you are port forwarding your public IP to the computer playing this script...NOT the roku IP var PASS='password' //this is the password used in the AWS lambda files to help stop others from running commands on your roku, should they guess your IP and port

var ssdp = new Client();

var keyDelay = 100; //typing delay in ms. If you have a faster roku, you can probably reduce this, slower ones may have to increase it.

//handle the ssdp response when the roku is found ssdp.on('response', function (headers, statusCode, rinfo) { rokuAddress = headers.LOCATION; console.log("Found Roku: ",rokuAddress); });

//this is called periodically and will only look for the roku if we don't already have an address function searchForRoku() { if (rokuAddress == null) { ssdp.search('roku:ecp'); } }

//a simple wrapper to post to a url with no payload (to send roku commands) function post(url,callback) { var info = urllib.parse(url); console.log("Posting: ",url); var opt = { host:info.hostname, port:info.port, path: info.path, method: 'POST', };

var req = http.request(opt, callback);

req.end();

}

//Performing an operation on the roku normally takes a handful of button presses //This function will perform the list of commands in order and if a numerical value is included in the sequence it will be inserted as a delay function postSequence(sequence,callback) { function handler() { if (sequence.length == 0) { if (callback) callback(); return; } var next = sequence.shift(); if (typeof next === "number") { setTimeout(handler,next); } else if (typeof next === "string") { post(next,function(res) { res.on("data",function() {}); //required for the request to go through without error handler(); }); } } handler(); }

//In order to send keyboard input to the roku, we use the keyress/Lit* endpoint which can be any alphanumeric character //This function turns a string into a series of these commands with delays built in function createTypeSequence(text) { var sequence = []; for (i=0; i<text.length; i++) { var c = text.charCodeAt(i); if (c == 32) { sequence.push(rokuAddress+"keypress/Lit%20"); } else if (c >= 97 && c <=122) { sequence.push(rokuAddress+"keypress/Lit_"+text.charAt(i)); } sequence.push(keyDelay);
} return sequence; } //simple helper function to pull the data out of a post request. This could be avoided by using a more capable library such function getRequestData(request,callback) { var body = ""; request.on("data",function(data) { body += String(data); }); request.on("end",function() { callback(body); }); }

function generateRepeatedKeyResponse(key,count) { var arr = []; for (var i=0; i<count; i++) { arr.push(rokuAddress+key); arr.push(keyDelay); } return function(request,response) { postSequence(arr); response.end("OK"); } }

//depending on the URL endpoint accessed, we use a different handler. //This is almost certainly not the optimal way to build a TCP server, but for our simple example, it is more than sufficient var handlers = { "/roku/playlast":function(request,response) { postSequence([ rokuAddress+"keypress/home", //wake the roku up, if its not already rokuAddress+"keypress/home", //go back to the home screen (even if we're in netflix, we need to reset the interface) 3000, //loading the home screen takes a few seconds rokuAddress+"launch/12", //launch the netflix channel (presumably this is always id 12..) 7000, //loading netflix also takes some time rokuAddress+"keypress/down", //the last searched item is always one click down and one click to the right of where the cursor starts rokuAddress+"keypress/right", 1000, //more delays, experimentally tweaked.. can probably be significantly reduced by more tweaking rokuAddress+"keypress/Select", //select the show from the main menu 3000, //give the show splash screen time to load up rokuAddress+"keypress/Play" //play the current/next episode (whichever one comes up by default) ]); response.end("OK"); //we provide an OK response before the operation finishes so that our AWS Lambda service doesn't wait around through our delays }, "/roku/downtwo":generateRepeatedKeyResponse("keypress/down",2), "/roku/downthree":generateRepeatedKeyResponse("keypress/down",3), "/roku/downfour":generateRepeatedKeyResponse("keypress/down",4), "/roku/downfive":generateRepeatedKeyResponse("keypress/down",5),

"/roku/uptwo":generateRepeatedKeyResponse("keypress/up",2),
"/roku/upthree":generateRepeatedKeyResponse("keypress/up",3),
"/roku/upfour":generateRepeatedKeyResponse("keypress/up",4),
"/roku/upfive":generateRepeatedKeyResponse("keypress/up",5),

"/roku/righttwo":generateRepeatedKeyResponse("keypress/right",2),
"/roku/rightthree":generateRepeatedKeyResponse("keypress/right",3),
"/roku/rightfour":generateRepeatedKeyResponse("keypress/right",4),
"/roku/rightfive":generateRepeatedKeyResponse("keypress/right",5),

"/roku/lefttwo":generateRepeatedKeyResponse("keypress/left",2),
"/roku/leftthree":generateRepeatedKeyResponse("keypress/left",3),
"/roku/leftfour":generateRepeatedKeyResponse("keypress/left",4),
"/roku/leftfive":generateRepeatedKeyResponse("keypress/left",5),
"/roku/captionson":function(request,response) {
    postSequence([
        rokuAddress+"keypress/info",    //this function only works with a Roku TV, as a regular roku's caption's sequence is based on the individual app.
        keyDelay,
        rokuAddress+"keypress/down",    
        keyDelay,
        rokuAddress+"keypress/down",   
        keyDelay,
        rokuAddress+"keypress/down",    
        keyDelay,
        rokuAddress+"keypress/down",    
        keyDelay,                           
        rokuAddress+"keypress/down",    
        keyDelay,                           
        rokuAddress+"keypress/right",    
        keyDelay,
        rokuAddress+"keypress/info",    //presses info a second time to exit menu
        keyDelay,                           
    ]);
    response.end("OK"); //we provide an OK response before the operation finishes so that our AWS Lambda service doesn't wait around through our delays
},
"/roku/captionsoff":function(request,response) {
    postSequence([
        rokuAddress+"keypress/info",    //this function only works with a Roku TV, as a regular roku's caption's sequence is based on the individual app.
        keyDelay,
        rokuAddress+"keypress/down",    
        keyDelay,
        rokuAddress+"keypress/down",   
        keyDelay,
        rokuAddress+"keypress/down",    
        keyDelay,
        rokuAddress+"keypress/down",    
        keyDelay,                          
        rokuAddress+"keypress/down",    
        keyDelay,                           
        rokuAddress+"keypress/left",    
        keyDelay,          
        rokuAddress+"keypress/info",    //presses info a second time to exit menu
        keyDelay,                 
    ]);
    response.end("OK"); //we provide an OK response before the operation finishes so that our AWS Lambda service doesn't wait around through our delays
},
//This endpoint doenst perform any operations, but it allows an easy way for you to dictate typed text without having to use the on screen keyboard
"/roku/type":function(request,response) {
    getRequestData(request,function(data) {
        var text = data.replace().toLowerCase(); 
        var sequence = createTypeSequence(text);
        postSequence(sequence,function() {

        });
        response.end("OK");    
    });
},
//Takes the POST data and uses it to search for a show and then immediate plays that show
"/roku/searchroku":function(request,response) {
    getRequestData(request,function(data) {
        var text = data.replace().toLowerCase();      //Master search....if a movie, will auto go to channel (first choice is always the free channel you have installed - if no free channel, will take you but not hit play.
        var sequence = [].concat([            //If a TV show....will stop before selecting a channel (first choice is based on how many episodes avaialble, NOT based on cost - meaning manually choose - will also allow you to choose the specific season and episode manually using voice or remote)
            rokuAddress+"keypress/home",    //wake roku
            rokuAddress+"keypress/home",    //reset to home screen
            2000,
            rokuAddress+"keypress/down",
            keyDelay,
            rokuAddress+"keypress/down",
            keyDelay,
            rokuAddress+"keypress/down",
            keyDelay,
            rokuAddress+"keypress/down",
            keyDelay,
            rokuAddress+"keypress/down",
            keyDelay,
            rokuAddress+"keypress/select",
            500,
            ],createTypeSequence(text),[
            rokuAddress+"keypress/right",
            keyDelay,
            rokuAddress+"keypress/right",
            keyDelay,
            rokuAddress+"keypress/right",
            keyDelay,
            rokuAddress+"keypress/right",
            keyDelay,
            rokuAddress+"keypress/right",
            keyDelay,
            rokuAddress+"keypress/right",
            500,
            rokuAddress+"keypress/select",
            1000,
            rokuAddress+"keypress/select",
            4000,
            ]);
        postSequence(sequence);
        response.end("OK");     //respond with OK before the operation finishes
    });
},
"/roku/searchplex":function(request,response) {
    getRequestData(request,function(data) {
        var text = data.replace().toLowerCase();      //Master search....if a movie, will auto go to channel (first choice is always the free channel you have installed - if no free channel, will take you but not hit play.
        var sequence = [].concat([            //If a TV show....will stop before selecting a channel (first choice is based on how many episodes avaialble, NOT based on cost - meaning manually choose - will also allow you to choose the specific season and episode manually using voice or remote)
            rokuAddress+"keypress/home",    //wake roku
            rokuAddress+"keypress/home",    //reset to home screen
            2000,            
            rokuAddress+"launch/13535",    //open plex
            5000,
            rokuAddress+"keypress/up",
            keyDelay,
            rokuAddress+"keypress/select",
            keyDelay,
            ],createTypeSequence(text),[
            rokuAddress+"keypress/right",
            keyDelay,
            rokuAddress+"keypress/right",
            keyDelay,
            rokuAddress+"keypress/right",
            keyDelay,
            rokuAddress+"keypress/right",
            keyDelay,
            rokuAddress+"keypress/right",
            1500,
            rokuAddress+"keypress/right",
            1000,
            rokuAddress+"keypress/select",
            500,
            rokuAddress+"keypress/select",
            500,
            ]);
        postSequence(sequence);
        response.end("OK");     //respond with OK before the operation finishes
    });
},
"/roku/playlastyoutube":function(request,response) {    //not working yet - youtube search does not allow keyboard input. Next best thing is to play most recent.
    getRequestData(request,function(data) {
        var sequence = [].concat([
            rokuAddress+"keypress/home",    //wake roku
            keyDelay,
            rokuAddress+"launch/837",        //launch youtube app
            6000,
            rokuAddress+"keypress/up",    //navigate to search
            200,
            rokuAddress+"keypress/up",  //Navigate to search
            200,
            rokuAddress+"keypress/select",  //select search
            200,
            rokuAddress+"keypress/up",   //go to search selections (which show up to the right of they keyboard.. we need to tap through them)
            200,
            rokuAddress+"keypress/select",
            2500,
            rokuAddress+"keypress/select", //selected the top result and returns to the main screen
            2500,                          //wait for main menu
        ]);
        postSequence(sequence);
        response.end("OK");     //respond with OK before the operation finishes
    });
},
"/roku/playpause":function(request,response) {        //the play and pause buttons are the same and is called "Play"
    post(rokuAddress+"keypress/Play");
    response.end("OK");    
},
"/roku/power":function(request,response) {        //Only for roku TV - can only turn TV OFF....not On, as once it is turned off, it will disable the network,
    post(rokuAddress+"keypress/Power");
    response.end("OK");    
},
"/roku/rewind":function(request,response) {        //rewind
    post(rokuAddress+"keypress/rev");
    response.end("OK");    
},
"/roku/fastforward":function(request,response) {    //fast forward
    post(rokuAddress+"keypress/fwd");
    response.end("OK");    
},
"/roku/up":function(request,response) {            //up
    post(rokuAddress+"keypress/up");
    response.end("OK");    
},
"/roku/down":function(request,response) {        //down
    post(rokuAddress+"keypress/down");
    response.end("OK");    
},
"/roku/back":function(request,response) {        //back
    post(rokuAddress+"keypress/back");
    response.end("OK");    
},
"/roku/left":function(request,response) {        //left
    post(rokuAddress+"keypress/left");
    response.end("OK");    
},
"/roku/instantreplay":function(request,response) {    //instant replay, go back 10 secounds
    post(rokuAddress+"keypress/instantreplay");
    response.end("OK");    
},
"/roku/right":function(request,response) {        //right
    post(rokuAddress+"keypress/right");
    response.end("OK");    
},
"/roku/select":function(request,response) {        //select - this is often more useful than play/pause - same as OK on the remote
    post(rokuAddress+"keypress/select");
    response.end("OK");    
},
"/roku/nextepisode":function(request,response) {    //NOT being utilized right now, needs tweaking
    postSequence([
        rokuAddress+"keypress/back",
        1000,
        rokuAddress+"keypress/down",
        keyDelay,
        rokuAddress+"keypress/down",
        keyDelay,
        rokuAddress+"keypress/select",
        2000,
        rokuAddress+"keypress/right",
        keyDelay,
        rokuAddress+"keypress/select",
        1000,
        rokuAddress+"keypress/Play",
    ],function() {

    });
    response.end("OK");
},
"/roku/lastepisode":function(request,response) {    //NOT being utilized right now, needs tweaking
    postSequence([
        rokuAddress+"keypress/back",
        1000,
        rokuAddress+"keypress/down",
        keyDelay,
        rokuAddress+"keypress/down",
        keyDelay,
        rokuAddress+"keypress/select",
        2000,
        rokuAddress+"keypress/left",
        keyDelay,
        rokuAddress+"keypress/select",
        1000,
        rokuAddress+"keypress/Play",
    ],function() {

    });
    response.end("OK");
},
"/roku/amazon":function(request,response) {            //function to open amazon, ID below
    postSequence([
        amazon(rokuAddress),
    ],function(){

    });
    response.end("OK");
},
"/roku/plex":function(request,response) {            //function to open plex, ID below
    postSequence([
        plex(rokuAddress),
    ],function(){

    });
    response.end("OK");
},
"/roku/pandora":function(request,response) {            //function to open pandora, ID below
    postSequence([
        pandora(rokuAddress),
    ],function(){

    });
    response.end("OK");
},
"/roku/hulu":function(request,response) {            //function to oen Hulu, ID below
    postSequence([
        hulu(rokuAddress),
    ],function(){

    });
    response.end("OK");
},
"/roku/home":function(request,response) {            //function for Home buddon, ID below
    postSequence([
        home(rokuAddress),
    ],function(){

    });
    response.end("OK");
},
"/roku/tv":function(request,response) {            //function for TV input - ROKU TV ONLY
    postSequence([
        tv(rokuAddress),
    ],function(){

    });
    response.end("OK");
},
"/roku/fourk":function(request,response) {        //Function for 4K Spotlight Channel - possibly 4k Roku version only
    postSequence([
        fourk(rokuAddress),
    ],function(){

    });
    response.end("OK");
},
"/roku/hbo":function(request,response) {        //function for HBOGO, ID below
    postSequence([
        hbo(rokuAddress),
    ],function(){

    });
    response.end("OK");
},        
"/roku/youtube":function(request,response) {            //function for youtube, ID below
    postSequence([
        youtube(rokuAddress),
    ],function(){

    });
    response.end("OK");
},
"/roku/fx":function(request,response) {            //function for FX Channel, ID below
    postSequence([
        fx(rokuAddress),
    ],function(){

    });
    response.end("OK");
}

}

//handles and incoming request by calling the appropriate handler based on the URL function handleRequest(request, response){ if (request.headers.authorization !== PASS) { console.log("Invalid authorization header"); response.end(); return; } if (handlers[request.url]) { handlersrequest.url; } else { console.log("Unknown request URL: ",request.url); response.end(); } }

// Launches the Amazon Video channel (id 13) function amazon(address){ return address+"launch/13"; } // Launches the Pandora channel (id 28) function pandora(address){ return address+"launch/28"; }

// Launches the Hulu channel (id 2285) function hulu(address){ return address+"launch/2285"; }

// Launches the Plex channel (id 13535) function plex(address){ return address+"launch/13535"; }

// Sends the Home button function home(address){ return address+"keypress/home"; }

// Launches the TV channel (id tvinput.dtv) function tv(address){ return address+"launch/tvinput.dtv"; }

// Launches the fourK channel (id 69091) function fourk(address){ return address+"launch/69091"; }

// Launches the HBO channel (id 8378) function hbo(address){ return address+"launch/8378"; }

// Launches the FX channel (id 47389) function fx(address){ return address+"launch/47389"; }

// Launches the YouTube channel (id 837) function youtube(address){ return address+"launch/837"; }

//start the MSEARCH background task to try every second (run it immediately too) setInterval(searchForRoku,1000); searchForRoku();

//start the tcp server http.createServer(handleRequest).listen(PORT,function(){ console.log("Server listening on port %s", PORT); });

chris1642 commented 7 years ago

This may not work but that randomly happened to me and I just closed the server app, restarted the Roku, waited until it was fully up, then restarted computer and then re-rand and it fixed it. Why did it fix it? A mystery... but worth a try.

Sent from my iPhone

On Jan 15, 2017, at 11:24 AM, michaeldeffendall notifications@github.com wrote:

Ok, I started from scratch and now I can't get the node server.js to see the Roku. I know I'm doing something stupid but I can't find it. I ran the npm install first.

Here is my server.js below. Roku is on .88. Using port forwarding on 32500.

When I run node server.js I get "Server listening on port 32500" and nothing else.

================================ var http = require('http'); var fs = require('fs'); var urllib = require("url"); var Client = require('node-ssdp').Client; var dgram = require('dgram');

//null will cause the server to discover the Roku on startup, hard coding a value will allow for faster startups // When manually setting this, include the protocol, port, and trailing slash eg: var rokuAddress = "http://192.168.1.88:8060/"; //var rokuAddress = null;

var PORT=32500; //this is the port you are enabling forwarding to. Reminder: you are port forwarding your public IP to the computer playing this script...NOT the roku IP var PASS='password' //this is the password used in the AWS lambda files to help stop others from running commands on your roku, should they guess your IP and port

var ssdp = new Client();

var keyDelay = 100; //typing delay in ms. If you have a faster roku, you can probably reduce this, slower ones may have to increase it.

//handle the ssdp response when the roku is found ssdp.on('response', function (headers, statusCode, rinfo) { rokuAddress = headers.LOCATION; console.log("Found Roku: ",rokuAddress); });

//this is called periodically and will only look for the roku if we don't already have an address function searchForRoku() { if (rokuAddress == null) { ssdp.search('roku:ecp'); } }

//a simple wrapper to post to a url with no payload (to send roku commands) function post(url,callback) { var info = urllib.parse(url); console.log("Posting: ",url); var opt = { host:info.hostname, port:info.port, path: info.path, method: 'POST', };

var req = http.request(opt, callback);

req.end(); }

//Performing an operation on the roku normally takes a handful of button presses //This function will perform the list of commands in order and if a numerical value is included in the sequence it will be inserted as a delay function postSequence(sequence,callback) { function handler() { if (sequence.length == 0) { if (callback) callback(); return; } var next = sequence.shift(); if (typeof next === "number") { setTimeout(handler,next); } else if (typeof next === "string") { post(next,function(res) { res.on("data",function() {}); //required for the request to go through without error handler(); }); } } handler(); }

//In order to send keyboard input to the roku, we use the keyress/Lit* endpoint which can be any alphanumeric character //This function turns a string into a series of these commands with delays built in function createTypeSequence(text) { var sequence = []; for (i=0; i<text.length; i++) { var c = text.charCodeAt(i); if (c == 32) { sequence.push(rokuAddress+"keypress/Lit%20"); } else if (c >= 97 && c <=122) { sequence.push(rokuAddress+"keypress/Lit_"+text.charAt(i)); } sequence.push(keyDelay); } return sequence; } //simple helper function to pull the data out of a post request. This could be avoided by using a more capable library such function getRequestData(request,callback) { var body = ""; request.on("data",function(data) { body += String(data); }); request.on("end",function() { callback(body); }); }

function generateRepeatedKeyResponse(key,count) { var arr = []; for (var i=0; i<count; i++) { arr.push(rokuAddress+key); arr.push(keyDelay); } return function(request,response) { postSequence(arr); response.end("OK"); } }

//depending on the URL endpoint accessed, we use a different handler. //This is almost certainly not the optimal way to build a TCP server, but for our simple example, it is more than sufficient var handlers = { "/roku/playlast":function(request,response) { postSequence([ rokuAddress+"keypress/home", //wake the roku up, if its not already rokuAddress+"keypress/home", //go back to the home screen (even if we're in netflix, we need to reset the interface) 3000, //loading the home screen takes a few seconds rokuAddress+"launch/12", //launch the netflix channel (presumably this is always id 12..) 7000, //loading netflix also takes some time rokuAddress+"keypress/down", //the last searched item is always one click down and one click to the right of where the cursor starts rokuAddress+"keypress/right", 1000, //more delays, experimentally tweaked.. can probably be significantly reduced by more tweaking rokuAddress+"keypress/Select", //select the show from the main menu 3000, //give the show splash screen time to load up rokuAddress+"keypress/Play" //play the current/next episode (whichever one comes up by default) ]); response.end("OK"); //we provide an OK response before the operation finishes so that our AWS Lambda service doesn't wait around through our delays }, "/roku/downtwo":generateRepeatedKeyResponse("keypress/down",2), "/roku/downthree":generateRepeatedKeyResponse("keypress/down",3), "/roku/downfour":generateRepeatedKeyResponse("keypress/down",4), "/roku/downfive":generateRepeatedKeyResponse("keypress/down",5),

"/roku/uptwo":generateRepeatedKeyResponse("keypress/up",2), "/roku/upthree":generateRepeatedKeyResponse("keypress/up",3), "/roku/upfour":generateRepeatedKeyResponse("keypress/up",4), "/roku/upfive":generateRepeatedKeyResponse("keypress/up",5),

"/roku/righttwo":generateRepeatedKeyResponse("keypress/right",2), "/roku/rightthree":generateRepeatedKeyResponse("keypress/right",3), "/roku/rightfour":generateRepeatedKeyResponse("keypress/right",4), "/roku/rightfive":generateRepeatedKeyResponse("keypress/right",5),

"/roku/lefttwo":generateRepeatedKeyResponse("keypress/left",2), "/roku/leftthree":generateRepeatedKeyResponse("keypress/left",3), "/roku/leftfour":generateRepeatedKeyResponse("keypress/left",4), "/roku/leftfive":generateRepeatedKeyResponse("keypress/left",5), "/roku/captionson":function(request,response) { postSequence([ rokuAddress+"keypress/info", //this function only works with a Roku TV, as a regular roku's caption's sequence is based on the individual app. keyDelay, rokuAddress+"keypress/down",
keyDelay, rokuAddress+"keypress/down",
keyDelay, rokuAddress+"keypress/down",
keyDelay, rokuAddress+"keypress/down",
keyDelay,
rokuAddress+"keypress/down",
keyDelay,
rokuAddress+"keypress/right",
keyDelay, rokuAddress+"keypress/info", //presses info a second time to exit menu keyDelay,
]); response.end("OK"); //we provide an OK response before the operation finishes so that our AWS Lambda service doesn't wait around through our delays }, "/roku/captionsoff":function(request,response) { postSequence([ rokuAddress+"keypress/info", //this function only works with a Roku TV, as a regular roku's caption's sequence is based on the individual app. keyDelay, rokuAddress+"keypress/down",
keyDelay, rokuAddress+"keypress/down",
keyDelay, rokuAddress+"keypress/down",
keyDelay, rokuAddress+"keypress/down",
keyDelay,
rokuAddress+"keypress/down",
keyDelay,
rokuAddress+"keypress/left",
keyDelay,
rokuAddress+"keypress/info", //presses info a second time to exit menu keyDelay,
]); response.end("OK"); //we provide an OK response before the operation finishes so that our AWS Lambda service doesn't wait around through our delays }, //This endpoint doenst perform any operations, but it allows an easy way for you to dictate typed text without having to use the on screen keyboard "/roku/type":function(request,response) { getRequestData(request,function(data) { var text = data.replace().toLowerCase(); var sequence = createTypeSequence(text); postSequence(sequence,function() {

    });
    response.end("OK");    
});

}, //Takes the POST data and uses it to search for a show and then immediate plays that show "/roku/searchroku":function(request,response) { getRequestData(request,function(data) { var text = data.replace().toLowerCase(); //Master search....if a movie, will auto go to channel (first choice is always the free channel you have installed - if no free channel, will take you but not hit play. var sequence = [].concat([ //If a TV show....will stop before selecting a channel (first choice is based on how many episodes avaialble, NOT based on cost - meaning manually choose - will also allow you to choose the specific season and episode manually using voice or remote) rokuAddress+"keypress/home", //wake roku rokuAddress+"keypress/home", //reset to home screen 2000, rokuAddress+"keypress/down", keyDelay, rokuAddress+"keypress/down", keyDelay, rokuAddress+"keypress/down", keyDelay, rokuAddress+"keypress/down", keyDelay, rokuAddress+"keypress/down", keyDelay, rokuAddress+"keypress/select", 500, ],createTypeSequence(text),[ rokuAddress+"keypress/right", keyDelay, rokuAddress+"keypress/right", keyDelay, rokuAddress+"keypress/right", keyDelay, rokuAddress+"keypress/right", keyDelay, rokuAddress+"keypress/right", keyDelay, rokuAddress+"keypress/right", 500, rokuAddress+"keypress/select", 1000, rokuAddress+"keypress/select", 4000, ]); postSequence(sequence); response.end("OK"); //respond with OK before the operation finishes }); }, "/roku/searchplex":function(request,response) { getRequestData(request,function(data) { var text = data.replace().toLowerCase(); //Master search....if a movie, will auto go to channel (first choice is always the free channel you have installed - if no free channel, will take you but not hit play. var sequence = [].concat([ //If a TV show....will stop before selecting a channel (first choice is based on how many episodes avaialble, NOT based on cost - meaning manually choose - will also allow you to choose the specific season and episode manually using voice or remote) rokuAddress+"keypress/home", //wake roku rokuAddress+"keypress/home", //reset to home screen 2000,
rokuAddress+"launch/13535", //open plex 5000, rokuAddress+"keypress/up", keyDelay, rokuAddress+"keypress/select", keyDelay, ],createTypeSequence(text),[ rokuAddress+"keypress/right", keyDelay, rokuAddress+"keypress/right", keyDelay, rokuAddress+"keypress/right", keyDelay, rokuAddress+"keypress/right", keyDelay, rokuAddress+"keypress/right", 1500, rokuAddress+"keypress/right", 1000, rokuAddress+"keypress/select", 500, rokuAddress+"keypress/select", 500, ]); postSequence(sequence); response.end("OK"); //respond with OK before the operation finishes }); }, "/roku/playlastyoutube":function(request,response) { //not working yet - youtube search does not allow keyboard input. Next best thing is to play most recent. getRequestData(request,function(data) { var sequence = [].concat([ rokuAddress+"keypress/home", //wake roku keyDelay, rokuAddress+"launch/837", //launch youtube app 6000, rokuAddress+"keypress/up", //navigate to search 200, rokuAddress+"keypress/up", //Navigate to search 200, rokuAddress+"keypress/select", //select search 200, rokuAddress+"keypress/up", //go to search selections (which show up to the right of they keyboard.. we need to tap through them) 200, rokuAddress+"keypress/select", 2500, rokuAddress+"keypress/select", //selected the top result and returns to the main screen 2500, //wait for main menu ]); postSequence(sequence); response.end("OK"); //respond with OK before the operation finishes }); }, "/roku/playpause":function(request,response) { //the play and pause buttons are the same and is called "Play" post(rokuAddress+"keypress/Play"); response.end("OK");
}, "/roku/power":function(request,response) { //Only for roku TV - can only turn TV OFF....not On, as once it is turned off, it will disable the network, post(rokuAddress+"keypress/Power"); response.end("OK");
}, "/roku/rewind":function(request,response) { //rewind post(rokuAddress+"keypress/rev"); response.end("OK");
}, "/roku/fastforward":function(request,response) { //fast forward post(rokuAddress+"keypress/fwd"); response.end("OK");
}, "/roku/up":function(request,response) { //up post(rokuAddress+"keypress/up"); response.end("OK");
}, "/roku/down":function(request,response) { //down post(rokuAddress+"keypress/down"); response.end("OK");
}, "/roku/back":function(request,response) { //back post(rokuAddress+"keypress/back"); response.end("OK");
}, "/roku/left":function(request,response) { //left post(rokuAddress+"keypress/left"); response.end("OK");
}, "/roku/instantreplay":function(request,response) { //instant replay, go back 10 secounds post(rokuAddress+"keypress/instantreplay"); response.end("OK");
}, "/roku/right":function(request,response) { //right post(rokuAddress+"keypress/right"); response.end("OK");
}, "/roku/select":function(request,response) { //select - this is often more useful than play/pause - same as OK on the remote post(rokuAddress+"keypress/select"); response.end("OK");
}, "/roku/nextepisode":function(request,response) { //NOT being utilized right now, needs tweaking postSequence([ rokuAddress+"keypress/back", 1000, rokuAddress+"keypress/down", keyDelay, rokuAddress+"keypress/down", keyDelay, rokuAddress+"keypress/select", 2000, rokuAddress+"keypress/right", keyDelay, rokuAddress+"keypress/select", 1000, rokuAddress+"keypress/Play", ],function() {

});
response.end("OK");

}, "/roku/lastepisode":function(request,response) { //NOT being utilized right now, needs tweaking postSequence([ rokuAddress+"keypress/back", 1000, rokuAddress+"keypress/down", keyDelay, rokuAddress+"keypress/down", keyDelay, rokuAddress+"keypress/select", 2000, rokuAddress+"keypress/left", keyDelay, rokuAddress+"keypress/select", 1000, rokuAddress+"keypress/Play", ],function() {

});
response.end("OK");

}, "/roku/amazon":function(request,response) { //function to open amazon, ID below postSequence([ amazon(rokuAddress), ],function(){

});
response.end("OK");

}, "/roku/plex":function(request,response) { //function to open plex, ID below postSequence([ plex(rokuAddress), ],function(){

});
response.end("OK");

}, "/roku/pandora":function(request,response) { //function to open pandora, ID below postSequence([ pandora(rokuAddress), ],function(){

});
response.end("OK");

}, "/roku/hulu":function(request,response) { //function to oen Hulu, ID below postSequence([ hulu(rokuAddress), ],function(){

});
response.end("OK");

}, "/roku/home":function(request,response) { //function for Home buddon, ID below postSequence([ home(rokuAddress), ],function(){

});
response.end("OK");

}, "/roku/tv":function(request,response) { //function for TV input - ROKU TV ONLY postSequence([ tv(rokuAddress), ],function(){

});
response.end("OK");

}, "/roku/fourk":function(request,response) { //Function for 4K Spotlight Channel - possibly 4k Roku version only postSequence([ fourk(rokuAddress), ],function(){

});
response.end("OK");

}, "/roku/hbo":function(request,response) { //function for HBOGO, ID below postSequence([ hbo(rokuAddress), ],function(){

});
response.end("OK");

},
"/roku/youtube":function(request,response) { //function for youtube, ID below postSequence([ youtube(rokuAddress), ],function(){

});
response.end("OK");

}, "/roku/fx":function(request,response) { //function for FX Channel, ID below postSequence([ fx(rokuAddress), ],function(){

});
response.end("OK");

} }

//handles and incoming request by calling the appropriate handler based on the URL function handleRequest(request, response){ if (request.headers.authorization !== PASS) { console.log("Invalid authorization header"); response.end(); return; } if (handlers[request.url]) { handlersrequest.url; } else { console.log("Unknown request URL: ",request.url); response.end(); } }

// Launches the Amazon Video channel (id 13) function amazon(address){ return address+"launch/13"; } // Launches the Pandora channel (id 28) function pandora(address){ return address+"launch/28"; }

// Launches the Hulu channel (id 2285) function hulu(address){ return address+"launch/2285"; }

// Launches the Plex channel (id 13535) function plex(address){ return address+"launch/13535"; }

// Sends the Home button function home(address){ return address+"keypress/home"; }

// Launches the TV channel (id tvinput.dtv) function tv(address){ return address+"launch/tvinput.dtv"; }

// Launches the fourK channel (id 69091) function fourk(address){ return address+"launch/69091"; }

// Launches the HBO channel (id 8378) function hbo(address){ return address+"launch/8378"; }

// Launches the FX channel (id 47389) function fx(address){ return address+"launch/47389"; }

// Launches the YouTube channel (id 837) function youtube(address){ return address+"launch/837"; }

//start the MSEARCH background task to try every second (run it immediately too) setInterval(searchForRoku,1000); searchForRoku();

//start the tcp server http.createServer(handleRequest).listen(PORT,function(){ console.log("Server listening on port %s", PORT); });

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

michaeldeffendall commented 7 years ago

Not a bad thought Chris, but no joy. Awaiting other thoughts.

julianh2o commented 7 years ago

When you configure the Roku address, you won't get a discovery notification. Are you sure it's not working? Try sending it a command by hitting your node server.

chris1642 commented 7 years ago

The alternate is simply replacing null with your actual IP.

Add quotation marks and put full address including port and final backslash

"Http://192.168.0.1:8060/"

Sent from my iPhone

On Jan 15, 2017, at 6:35 PM, michaeldeffendall notifications@github.com wrote:

Not a bad thought Chris, but no joy. Awaiting other thoughts.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

michaeldeffendall commented 7 years ago

Chris: It's already set var rokuAddress = "http://192.168.1.88:8060/";

Julian: Send it command from where?

chris1642 commented 7 years ago

If already set up, what happens when you do a voice command with the echo?

Both Alexa's response and within the node server?

Sent from my iPhone

On Jan 15, 2017, at 7:50 PM, michaeldeffendall notifications@github.com wrote:

Chris: It's already set var rokuAddress = "http://192.168.1.88:8060/";

Julian: Send it command from where?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

julianh2o commented 7 years ago

Open up your browser and visit http://[ip-address-of-server]:32500/roku/home

michaeldeffendall commented 7 years ago

Julian: Poking the Roku as you said displayed this:

Server listening on port 32500 Invalid authorization header Invalid authorization header

julianh2o commented 7 years ago

Great, looks like your server is working fine then!

That was a change implemented by a collaborator that requires a password in the header of any request coming in.

Your next step is to see if it's accessible from outside your network (and thus from the Lambda instance)