Introduction
This piece shows how to set up a two-way interface between a remote browser-enabled device, such as a cell phone, and a Raspberry Pi. The example app is a simple, domestic environment, controller. The Pi sends temperature information to a subscriber’s browser and responds to requests from the browser by switching on or off remote radio controlled plug sockets. It’s written in node.js.
Configuring the Pi
The project uses a Raspberry Pi 3 running the latest version of Raspbian with internet enabled WiFi. You can use earlier versions of the Pi, however, the port pin numbers may not correspond to the ones mentioned here. The Pi needs to have a few interfacing connections enabled. This used to be a long and tedious business but it has now been greatly simplified. The easiest way is to connect the Pi’s HDMI cable to a monitor, attach an internet connected Ethernet cable, a usb keyboard and mouse and boot to Raspbian. From the raspberry icon on the top left of the screen, select preferences/raspberry Pi configuration/interfaces and enable SSH, 1-Wire and Remote GPIO, then close the pop up window. To enable WiFi, click on the up/ down arrows icon on the top right of the screen and follow the prompts . Remove the Ethernet connection, then, click on the command window icon and enter the command ‘hostname –I
’. Make a note of the IP address that’s displayed, you'll need it to connect to the Pi. Then reboot the Pi (enter ‘reboot
’).
Installing and Updating the Software
There are a couple of software packages that need to be installed. The first is the Node.js package manager npm. This manages the packages needed by an application. It uses a package.json file to maintain, load and update the required packages. The second is Mocha. Mocha is a useful node.js test framework, it facilitates the building and running of tests. Finally, the pre-installed software should be updated to the latest Pi compatible versions. The best way to do this is to is to update the package list, then upgrade to the latest versions detailed in the updated package list. Enter ’Ctrl V' to continue, if the installation stops after displaying a page of installation log details, enter 'q',when prompted, to return to the input terminal . Skiing off-piste by downloading direct from a provider’s web site is not recommended because of possible incompatibility issues -some of the packages are Pi specific. Here are the commands to enter.
sudo apt-get update
sudo apt-get upgrade
sudo apt install npm
sudo npm install --global mocha
Setting Up the Server
The server needs to respond to both GET
and PUT
requests from the remote browser . The GET
request simply uploads the HTML web page. PUT
requests are sent when a button is pressed on the web page. The last word of the Url for the PUT
request, relates to the name of the button. The GPIO port of the Pi is interfaced to the remote controller of the plug sockets so that, when the voltage level of a particular port pin changes in response to a button press, the remote control sends a radio pulse to the appropriate plug socket to switch it to either on or off.
app.get('/', function (req, res) {
res.sendFile(__dirname + '/Views/home.html');
});
app.post('/:pinName', function (req, res) {
var pinName = req.params.pinName;
var portNumber = portDictionary[pinName];
if (portNumber) {
gpio.queuePulse(portNumber, pulseLength);
res.status(204).end();
}
else {
console.log("port requested is not found ");
res.status(404).end();
}
});
Updating the Web Page
The temperature displayed on the web page is updated every 5 seconds. This is achieved by utilising Server Sent Events (SSE). The client subscribes to the events by sending a GET
request to the SSE end point. The server responds by establishing an open connection with the client and starts sending message packets containing the updated temperature. The client unwraps the messages and displays the updated information. When the client closes the web page, the request's Close
event fires and server unsubscribes the client. Clients will automatically try to reconnect after about three seconds if their connection is unexpectedly terminated.
app.get('/sse', function (req, res) {
if (req.headers.accept && req.headers.accept == 'text/event-stream') {
req.on('close', function () {
sse.removeSubscriber(res);
res = null;
});
if (res != null) {
sse.addSubscriber(req, res);
}
} else {
res.writeHead(404);
res.end();
}
});
The addSubcriber
function adds the subscriber’s response object to an array and starts the message pump, if necessary. The response object is actually a stream and has a constant reference value for a given connection, so it can be stored and reused. Messages need to be separated from each other by a pair of newline characters.
function addSubscriber(req, res) {
subscribers.push(res);
res.writeHead(200,
{
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
);
writeMessage(res);
if (!cancellationToken) {
cancellationToken = setInterval(function () {
messageAllSubscribers();
}, sendInterval);
}
}
function messageAllSubscribers() {
subscribers.forEach(function (subscriberRes) {
writeMessage(subscriberRes);
});
}
function writeMessage(res) {
sensor.get(sensorId, function (err, temp) {
if (err) {
console.log(err);
return;
}
res.write("data: " + temp + '\n\n');
});
}
function removeSubscriber(subscriberRes) {
var i = subscribers.indexOf(subscriberRes);
if (i !== -1) {
subscribers.splice(i, 1);
}
if (subscribers.length == 0 && cancellationToken) {
clearInterval(cancellationToken);
cancellationToken = null;
}
}
Client-Side Code
All that’s necessary to process the server sent event is a few lines of JavaScript inside the HTML document.
if(typeof(EventSource) !== "undefined") {
var source = new EventSource("./sse");
source.onmessage = function(event) {
document.getElementById("result").innerHTML ="Temperature "+ event.data+" °C" ;
};
} else {
document.getElementById("result").innerHTML = "Unable to update the temperature";
}
There is also a function to send the POST
request to the server. The button-click event calls it with the button’s id set as the input parameter.
function postRequest(id) {
var x= document.getElementById(id);
x.disabled = true;
setTimeout(function(){ x.disabled=false }, 750);
var xhr = new XMLHttpRequest();
xhr.open('POST', "./"+id, true);
xhr.timeout = 2500;
xhr.send();
xhr.ontimeout = function (e) {
alert("Request timed out");
};
}
Interfacing with the Pi’s GPIO Port
The general purpose input output port (GPIO) is controlled using the superb wiring-pi package.
var wpi = require('wiring-pi');
wpi.setup('phys');
function setGpioPins(portDictionary) {
for (var key in portDictionary) {
wpi.pinMode(portDictionary[key], wpi.OUTPUT);
}
}
function pulsePin(pin, delay,cb) {
wpi.digitalWrite(pin, 1);
setTimeout(function () {
wpi.digitalWrite(pin, 0);
cb();
}, delay);
}
Throttling Requests to the Output Pins
The remote control for the plug sockets can only switch one socket at a time, so it’s important that only one pin is pulsed at a time. This is achieved by using a synchronous queue where tasks are executed sequentially. When one asynchronous task completes, another starts. In this case, the task is the pulsing of the pins.
module.exports = TaskQueue;
var taskWorker;
var queue = [];
function TaskQueue(worker) {
taskWorker = worker;
}
var processTask = function () {
var taskParams = queue[0];
taskWorker(taskParams, dequeue);
}
var dequeue = function () {
queue.splice(0, 1);
if (queue.length > 0) {
processTask();
}
}
TaskQueue.prototype.queueTask = function (taskParams) {
var length = queue.push(taskParams);
if (length == 1) {
processTask();
}
}
Reading the Temperature Sensor
The sensor used is a DS18B20. This sends information as a package using a single data line that's connected to physical pin 7 on the Pi. Each sensor has a unique id and several can share the one port. The input pin should have its internal resistor set to ‘pull-up’. This results in the pin going to the high state when it is not being driven.
The package used to read the sensor is ds18x20. It’s very easy to use.
var sensor = require('ds18x20');
var sensorId = '28-000007d4684f';
function writeMessage(res) {
sensor.get(sensorId, function (err, temp) {
if (err) {
console.log(err);
return;
}
res.write("data: " + temp + '\n\n');
});
}
Interfacing With the Remote Control
This is a bit off topic as it’s more about electronics than coding, so I’ve put the details in the README.html of the example app. Basically, what happens is the output pin of the Pi is connected to a simple transistor switch. When the pin goes high, the transistor switches on and shorts the same contacts that are usually connected by a button press on the remote. There’s no need for expensive relays as the switch only passes a few milliamps.
Testing
My preferred option is to carry out most of the development work and testing on a PC and then transfer the resulting files to the Pi. In order to do this, you need to download the following:
- A telnet client such as PuTTY so that the PC can act as a remote terminal for the Pi
- A file transfer utility like WinSCP. To transfer and synchronise files between Pi and PC
- A code editing and debugging platform. Visual Studio Code (vscode). Takes some beating.
When testing on a PC, it's a good idea to use mock versions of the modules that interface directly with the pi’s firmware. Trying to install wiring-pi on a PC will fail. The sample app is set up to run in debug mode and will use mock versions of 'wiring-pi' and 'ds18x20'. To run it hot on a Pi, change the "NODE_ENV
" value in config.json from ‘debug’ to ‘production'.
Useful Hardware
An insulated case for the Pi is considered to be essential unless, of course, you wish to experience the blue-smoke farewell display. Another bit of kit worth getting hold of is a breakout ribbon cable and connector, known as a ‘cobbler’, to interface the Pi ports to a prototyping breadboard. Go for the fully assembled version. It’s not worth soldering the pins yourself, one ‘dry’ joint and it’s worse than useless. A very useful test instrument is the logic probe. These are cheap to buy and give an audiovisual indication of the logic state, either High or Low, on any port pin. They are also able to detect voltage pulses. You can use a multimeter as a replacement but, with digital circuits, the logic state is more important than the actual voltage reading.
Adding Raspberry Pi to a Network
In order to debug the Pi from a remote machine, you need to add the Pi to a shared network. To do this, install a utility called Samba then backup its samba.conf file and edit the original file as detailed below. The commands to use are:
sudo apt-get install samba samba-common-bin
cd /etc/samba
sudo cp smb.conf smb.backup
sudo nano smb.conf
In the section [global]:
workgroup = WORKGROUP
wins support = yes
In section #=====Share Definitions=========
[homes]
readonly= yes
Finally, copy and paste the following with the cursor positioned on a blank line immediately above the [printers] section:
[piPublic]
comment= Pi Public
path=/home/pi/Public
browserable=Yes
writeable=Yes
guest ok=Yes
create mask=0775
directory mask=0775
public=yes
Save the file and then set the password to the one that user 'pi
' logs on with:
smbpasswd -a pi
Then reboot. You should see RASPBERRYPI
listed on the network. The piPublic folder has read/write permissions.
Debugging with Visual Studio Code
Use the read/write network address when you first open a folder, "\\RASPBERRYPI\piPublic\foldername" so that the workspaceRoot
variable is set correctly. It’s best to put break points inside the callback function when debugging http requests rather than at the beginning of the parent method. You also need to be aware of the possibility that the browser may time out as a result of the break point being hit.
If you launch an application from vscode, it will run on the local machine and not on the Pi. To debug remotely, you need to use the integrated node.js debugger. The procedure for adding break points is to simply add the key word debugger;
in the source code where you want to break. You can then inspect the variables in vscode. This is how to set things up.
- Configure PuTTY to allow connections on port 5858. On PuTTY, select category/Connection/SSH/Tunnels. Tick Local ports accept connections from other hosts. Enter the source port as
5858
and the Destination as localhost:5858
, then click Add.
- In the Session window, enter the Pi's IP address. Make sure connection type SSH is selected. Name the session in the Saved Sessions box and save it. Double clicking on the saved session name will load it and open the terminal.
- Navigate to the application's directory on the Pi using the network address "\\RASPBERRYPI\piPublic\HomeAuto" and enter '
node --debug index.js
' to start the application. In vscode debug mode, select the 'Attach to Port' configuration from the drop down menu on the top left and then start debugging by pressing the arrow next to it. If there are no choices available in the dropdown list, the launch.json config file hasn’t been built yet; press the Debug button and follow the prompts to build it.
Running the Application at Startup
This is just a matter of accessing the file /etc/rc.local as root.
sudo nano /etc/rc.local
Then, immediately above the last line, add the start
command for the application. So the last two lines look something like this:
sudo node /home/pi/Public/HomeAuto/index.js
exit 0
Conclusion
I hope this has been a useful introduction to configuring and programming the Raspberry Pi. You may like to have a go yourself, it's great fun and you can learn a lot.
History
- 4th February, 2017: Initial version