Thursday 24 October 2019

Remote Control KitchenPi: playing with NodeJS

I wanted to change channels via a phone or iPad on our internet radio.


This looked like a good excuse to try out something new.


So I settled on getting my head around NodeJS, which looks ideal for headless Pi projects, including internet radios and birdbox camera systems.

background


To change from one radio stream to another on KitchenPi (our RaspberryPi based internet radio) we simply press-and-release a push button which steps the radio onto the next station. Using this simple approach does mean that the list of channels has to be restricted, otherwise it would be really tedious pumping the switch 20 times to get back to the station you wanted to listen to. So in practice, the system only has 5 radio stations plus one channel that just plays music from a large collection of files.

The attraction of being able to select stations from a device like an iPad, is that I could create a list containing dozens of stations to select from, while still retaining the small list of push-button selected stations. Apart from that, it sounds like an interesting mini-project.

what is NodeJS?


NodeJS is an open source, cross-platform JavaScript interpreter. It was created for server-side use as an alternative to languages like PHP. It is lightweight and easy to install and run on a RaspberryPi. Node.JS also has methods for reading and writing files to the server.

By running some simple JavaScript code on KitchenPi, I should be able to generate a web page containing my list of stations. This web page can then be accessed by any device on my home network.

let's write some code


To keep things really simple, you can write code in a text editor and test it on the same computer (i.e. run the code and test with a web browser on the same machine). Here is the simplest code example I could think of, along the lines of "hello World". I have called my file test_1.js.

var http = require("http");
var port = 9000;
var serverUrl = "localhost";

var server = http.createServer(function(req, res) {
  var html = "Hi"

  res.setHeader("Content-Type", "text/html");
  res.end(html);
});
server.listen(port, serverUrl);


To run it, open a terminal in the same folder as the test_1.js file (you can do this on Linux by hitting F4 in the file manager with the folder open).

Type in the terminal:-

nodejs test_1.js

If you don't get any response in the terminal, that's good. Now enter:-

http://localhost:9000/

...into your web browser. You should now see the text: Hi

Notes:-
  • I'm using the command nodejs because that is what is required on my Pi. For regular computers (running Linux, Apple or MS) you may find "node" and/or "nodejs" will work.
  • When the server function runs, it creates a Listening socket and runs in a non-blocking loop.
  • This function responds each time the browser requests the 'page'. The res.end() method ends the response and returns any text passed to it.

Its probably a good idea to log info to the console, especially if you have problems getting code like this to work. So you can expand the first example like this:-

var http = require("http");
var port = 9000;
var serverUrl = "localhost";

var server = http.createServer(function(req, res) {
  var html = "Hi"

  console.log("Request: " + req.url);
  res.end(html);
});

console.log("Address: " + serverUrl + ", port: " + port);
server.listen(port, serverUrl);



Now when you run the code and browse to localhost:9000 you should get some feedback in the terminal:-

nodejs javascript raspberry pi



controlling KitchenPi


My KitchenPi python code saves the current station/channel as a simple integer in a file called LastSource. So I've modified the python code to read back the value in this file every 2s or so, and switch if necessary.

The nodejs code includes a Switch/Case statement which tests the user's selection and writes the new selection into the LastSource file.

Initially I built a web page to look like this...

nodejs raspberry pi javascript
All stations can be selected remotely, but only the first 6 via the manual push-button switch


...by simply writing the details into my js file:-

var http = require("http");
var port = 9999;
var serverUrl = "192.168.0.90";

var server = http.createServer(function(req, res) {

  var fs = require('fs');
  var stream;       //kitchenPi channel or music source number

  console.log("Request: " + req.url);
  var html = "<h3>KitchenPi internet radio</h3><h1>Select a channel</h1><br>"
    html = html + "<p style='font-size:20px;'>"
    html = html + "<a href=/juke>Juke Box</a><br>"
    html = html + "<a href=/caroline>Radio Caroline</a><br>"
    html = html + "<a href=/smooth>Smooth Radio</a><br>"
    html = html + "<a href=/classic>Classic FM</a><br>"
    html = html + "<a href=/radio4>BBC Radio 4</a><br>"
    html = html + "<a href=/worldService>BBC World Service</a><br><br>"
    html = html + "<a href=/planet>Planet Rock</a><br>"
    html = html + "<a href=/rni>RNI</a><br>"
    html = html + "<a href=/virgin>Virgin Radio</a><br>"
    html = html + "<a href=/united>United DJ Radio</a><br>"
    html = html + "<a href=/jack>Jack Radio</a><br>"
    html = html + "<br></p>";
  switch(req.url) {

    case "/juke":
        var html = html + "<p>Switching to:-</p><h2 style='color:green;'>&emsp; Juke Box</h2>";
        stream = 0;
        SaveSourceNumber(stream);
        break;

    case "/caroline":
        var html = html + "<p>Switching to:-</p><h2 style='color:green;'>&emsp; Radio Caroline</h2>";
        stream = 1;
        SaveSourceNumber(stream);
        break;

    case "/smooth":
        var html = html + "<p>Switching to:-</p><h2 style='color:green;'>&emsp; Smooth Radio</h2>";
        stream = 2;
        SaveSourceNumber(stream);
        break;

    case "/classic":
        var html = html + "<p>Switching to:-</p><h2 style='color:green;'>&emsp; Classic FM</h2>";
        stream = 3;
        SaveSourceNumber(stream);
        break;

    case "/radio4":
        var html = html + "<p>Switching to:-</p><h2 style='color:green;'>&emsp; BBC Radio 4</h2>";
        stream = 4;

        SaveSourceNumber(stream);
        break;

    case "/worldService":
        var html = html + "<p>Switching to:-</p><h2 style='color:green;'>&emsp; BBC World Service</h2>";
        stream = 5;
        SaveSourceNumber(stream);
        break;

    case "/planet":
        var html = html + "<p>Switching to:-</p><h2 style='color:green;'>&emsp; Plant Rock</h2>";
        stream = 10;
        SaveSourceNumber(stream);
        break;

    case "/rni":
        var html = html + "<p>Switching to:-</p><h2 style='color:green;'>&emsp; Radio Northsea International</h3>";
        stream = 11;
        SaveSourceNumber(stream);
        break;

    case "/virgin":
        var html = html + "<p>Switching to:-</p><h2 style='color:green;'>&emsp; Virgin Radio</h3>";
        stream = 12;
        SaveSourceNumber(stream);
        break;

    case "/united":
        var html = html + "<p>Switching to:-</p><h2 style='color:green;'>&emsp; United DJ Radio</h3>";
        stream = 13;
        SaveSourceNumber(stream);
        break;

    case "/jack":
        var html = html + "<p>Switching to:-</p><h2 style='color:green;'>&emsp; Jack Radio</h3>";
        stream = 14;
        SaveSourceNumber(stream);
        break;

    default:
    }

  html = html + "<br><br>stream #" + stream
  res.setHeader("Content-Type", "text/html");
  res.end(html);

});

console.log("Listening at " + serverUrl + ":" + port);
server.listen(port, serverUrl);


function SaveSourceNumber(streamNo) {
  var fs = require('fs');
  fs.writeFile('LastSource', streamNo, function (err) {
     if (err) throw err;
          console.log('file saved');
  });
};




This is still a prototype, so it needs more work to tidy up. For example, the web page text is very small on a phone screen.

But hey! it works just fine!



...and one late edit...

run nodejs as user


One problem I struggled with was auto-running nodejs with my code as the Raspberry Pi boots up.

There were two issues;
  1.  can't create a server socket until the system has a running network
  2. the server does not communicate if run as root

My solution was to execute the nodejs command from the /etc/rc.local file as user "pi" after a short delay.  Although 10s seemed to be enough, I played it safe by using 20seconds.

So the command line in rc.local (at the end, but just before "exit 0") looks like this:-

...
python /home/pi/kitchenPi.py &

( sleep 20 && runuser -l  pi -c '/usr/bin/nodejs /home/pi/kitchenPiRemote.js' ) &
exit 0


Although the python code runs as root, the nodejs stuff must run as "pi".

Entering a delay may be avoided by loading the nodejs command from the /etc/network/if-up.d/ directory, but I have not tried this.


No comments:

Post a Comment