Remote Control With Node.js, React.js, and Raspberry Pi
We will develop a remote control system that can be extended for various devices and equipment using popular enterprise technologies and the Raspberry Pi platform.
Join the DZone community and get the full member experience.
Join For FreeThe appearance of simple and cheap single-board computers (SBC) was a great promoting factor for the IoT world, providing a possibility to develop a wide range of control systems and devices for industrial, domestic, medical, and other usage. Now, everybody can develop stuff they need for their own needs, contribute to the development of public projects, and use products developed by others.
In this article, we are going to develop a control system to manage basic garden activities, like watering, illumination, etc. To make our application more flexible and expandable, we will develop it as a layered distributed system of loosely coupled components, communicating with each other via a standard (REST in our case) protocol. We will use well-known enterprise technologies, Node.js and React.js, and a Raspberry Pi Zero device for the sensor layer of our application.
The main function of our sensor layer component is to control devices performing main activities in our garden. Let’s suppose we need to control a watering circuit and a lighting circuit that is, to switch on/off a watering pump and an outdoor lamp. Firstly, we connect all the hardware units and then develop the necessary software to bring the hardware stuff to life.
Hardware Set-Up: Relay Board Connection, Circuit Assembling
For our case of two devices, we can use Raspberry Pi Zero W SBC and SB Components Zero Relay relay HAT (‘Hardware Attached on Top’). Each of the HAT relays has NC (normally closed) and NO (normally open) contacts. We need our watering and lighting circuits to close (switch on) only when we need to switch on the pump and the lamp, so we connect the circuit ends to the NO and COM contacts, as shown in Fig. 1.1.
Fig. 1.1. Relay connection diagram
Software Set-Up: OS and Library Installation, the Control Software Development
Provide all hardware components connected; we can add software to make the system work. First of all, we need to install an operation system on our Raspberry Pi device. There are several ways to do that; probably the most comfortable way is to use Raspberry Pi Imager. With the usage of this application, we can download an appropriate OS and write it on an SD card to boot the SBC; We can use Raspberry Pi OS (32-bit) from the installation menu.
Provided your Raspberry Pi is equipped with an appropriate OS and has access to the command line there, we can prepare all necessary software. Our component has two tasks: expose an API to accept commands for controlling the relays and pass these commands to the relays. Let’s start with the API implementation, which is a common task for most modern enterprise applications.
Implementation of the Control API
As we discussed earlier, we are going to use REST protocol for communication between our components, so we need to expose REST endpoints for the controlling interface. Considering the somehow restricted profile of our Raspberry Pi Zero computer, we should implement the API stuff as lightweight as possible. One of the suitable technologies for this case is the Node.js framework. Essentially, it is a JavaScript engine, which provides the possibility to run JavaScript code on the server-side.
Because of its design, Node.js is particularly useful for building web applications that handle requests over the internet and provide processed data and views in return. We are going to use the Express Web framework in our Node.js application to facilitate the request handling. Provided having Node.js running in our system, we can start implementing the control API. Let’s create a Web controller, which will be the main controlling unit of our component. We can implement three endpoints for each device relay – switch-on, switch-off, and status endpoints. It is a good idea to create a Node package for our application, so we create a new directory, "smartgarden“ in the Raspberry root and run the following commands inside the directory, to install all necessary dependencies and create the package descriptor:
pi@raspberrypiZero:~/smartgarden $ npm install express --save
pi@raspberrypiZero:~/smartgarden $ npm install http --save
We begin with the following basic script and will gradually add all necessary functionality.
Listing 2.1. smartgarden/outdoorController.js: the component Web API
const express = require("express");
const http = require("http");
var app = express();
app.use((req, res, next) => {
res.append('Access-Control-Allow-Origin', ['*']);
res.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.append('Access-Control-Allow-Headers', 'Content-Type');
next();
});
var server = new http.Server(app);
app.get("/water/on", (req, res) => {
console.log("Watering switched on");
});
app.get("/water/off", (req, res) => {
console.log("Watering switched off");
});
app.get("/water/status", (req, res) => {
console.log("TODO return the watering relay status");
});
app.get("/light/on", (req, res) => {
console.log("Light switched on");
});
app.get("/light/off", (req, res) => {
console.log("Light switched off");
});
app.get("/light/status", (req, res) => {
console.log("TODO return the light relay status");
});
server.listen(3030, () => {console.log("Outdoor controller listens to port 3030")});
We can run the application with the following command:
pi@raspberrypiZero:~ $ node smartgarden/outdoorController.js
There should be the following message in the terminal:
Outdoor controller listens to port 3030
When navigating to the defined endpoints, we should see the corresponding output, for example, “Light switched off” for the /light/off endpoint. If that’s the case, that means that our control API is working! It’s great, but actually, it doesn’t do any useful work for now. Let’s fix it by adding stuff to pass the commands to the physical devices, i.e., the relays.
Passing the Commands to the Physical Devices
There are several ways to communicate with physical devices from inside a JavaScript application. Here, we are going to use the Johnny-Five JavaScript library for it.
Johnny-Five is the JavaScript Robotics and IoT platform, which is adapted for many platforms, including Raspberry Pi. It provides support for various equipment like relays, sensors, servos, etc.
Provided having Node and npm tools installed in your Raspberry Pi, you can install Johnny-Five library with the following command:
pi@raspberrypiZero:~/smartgarden $
npm install johnny-five --save
Also, we need to install raspi-io package. Raspi IO is an I/O plugin for the Johnny-Five Node.js robotics platform that enables Johnny-Five to control the hardware on a Raspberry Pi.
pi@raspberrypiZero:~/smartgarden $
npm install raspi-io --save
To test the installation, we can run this script:
const Raspi = require('raspi-io').RaspiIO;
const five = require('johnny-five');
const board = new five.Board({
io: new Raspi()
});
board.on('ready', () => {
// Create an Led on pin 7 (GPIO4) on P1 and strobe it on/off
// Optionally set the speed; defaults to 100ms
(new five.Led('P1-7')).strobe();
});
Because of the conservative permissions for interacting with GPIO in Raspbian, you would need to execute this script using sudo. If our installation is successful, we should see the LED blinking with a default frequency of 100 ms.
Now we can add the Johnny-Five support for our controller, as it is shown in listing 2.2.
Listing 2.2. smartgarden/outdoorController.js with the relay control enabled
const express = require("express");
const http = require("http");
const five = require("johnny-five");
const { RaspiIO } = require('raspi-io');
var app = express();
app.use((req, res, next) => {
res.append('Access-Control-Allow-Origin', ['*']);
res.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.append('Access-Control-Allow-Headers', 'Content-Type');
next();
});
var server = new http.Server(app);
const board = new five.Board({io: new RaspiIO(), repl: false});
board.on("ready", function() {
var relay1 = new five.Relay({pin:"GPIO22",type: "NO"});
var relay2 = new five.Relay({pin:"GPIO5",type: "NO"});
app.get("/water/:action", (req, res) => {
switch(req.params.action) {
case 'on':
relay1.close();
res.send(relay1.value.toString());
break;
case 'off':
relay1.open();
res.send(relay1.value.toString());
break;
case 'status':
res.send(relay1.value.toString());
break;
default:
console.log('Unknown command: ' + req.params.action);
res.sendStatus(400);
}
});
app.get("/light/:action", (req, res) => {
switch(req.params.action) {
case 'on':
relay2.close();
res.send(relay2.value.toString());
break;
case 'off':
relay2.open();
res.send(relay2.value.toString());
break;
case 'status':
res.send(relay2.value.toString());
break;
default:
console.log('Unknown command: ' + req.params.action);
res.sendStatus(400);
}
});
server.listen(3030, () => {
console.log('Outdoor controller listens to port 3030');
});
});
We run the updated application as follows:
pi@raspberrypiZero:~ $ sudo node smartgarden/outdoorController.js
If we navigate to the endpoints now, we should see the corresponding relays switch on and switch-off. If this is the case, means that our controller component works properly!
Well done! However, it is not very convenient to send the command by typing REST requests in a http client. It would be good to provide a GUI for it.
Development of the Command User Interface
There are various options for implementing the control interface, and we are going to use React.js for this task. React.js is a lightweight JavaScript framework, which is oriented toward the creation of component-based web UIs, which is the right choice for our case of a distributed component application. To use React in whole its scope, we can install the create-react-app tool:
npm install -g create-react-app
After that, we can create a JavaScript application for our front-end stuff. Provide being in a project root directory, we run the following command:
.../project-root>create-react-app
smartgarden
This command creates a new folder ('smartgarden') with a ready-to-run prototype React application.
Now, we can enter the directory and run the application as follows:
.../project-root>cd
smartgarden
.../project-root/
smartgarden
>npm start
This starts the application in a new browser at http://localhost:3000. It is a trivial but completely functional front-end application, which we can use as a prototype for creating our UI. React supports component hierarchies, where each component can have a state, and the state can be shared between related components. Also, each component's behavior can be customized by passing properties to it. So, we can develop the main component, which works as the placeholder for displaying screens or forms for the corresponding actions. Also, to accelerate the development and get our UI looking in a commonly-used, user-friendly way, we are going to use the MUI component library, which is one of the most popular React component libraries. We can install the library with the following command:
npm install @mui/material @emotion/react @emotion/styled
To put all configuration settings in one place, we create a configuration class and put it to a particular configuration directory:
Listing 2.3. configuration.js: Configuration class, including all the application settings.
class Configuration {
WATERING_ON_PATH = "/water/on";
WATERING_OFF_PATH = "/water/off";
WATERING_STATUS_PATH = "/water/status";
LIGHT_ON_PATH = "/light/on";
LIGHT_OFF_PATH = "/light/off";
LIGHT_STATUS_PATH = "/light/status";
CONTROLLER_URL = process.env.REACT_APP_CONTROLLER_URL ? process.env.REACT_APP_CONTROLLER_URL :
window.CONTROLLER_URL ? window.CONTROLLER_URL : "http://localhost:3030";
}
export default Configuration;
The configuration class contains the controller server URL and paths to the control endpoints. For the controller URL, we provide two possibilities of external configuration and a default value, http://localhost:3030 in our case. You can substitute it for the corresponding URL of your controller server.
It is a good idea to put all related functionalities in one place. Putting our functionality behind a service, which exposes certain APIs, ensures more flexibility and testability for our application. So, we create a control service class, which implements all basic operations for data exchange with the controller server and exposes these operations as methods for all React components. To make our UI more responsive, we implement the methods as asynchronous. Provided the API is unchanged, we can change the implementation freely and none of the consumers will be affected. Our service can look like this.
Listing 2.4. services/ControlService.js – API for communication with the sensor layer
import Configuration from './configuration';
class ControlService {
constructor() {
this.config = new Configuration();
}
async switchWatering(switchOn) {
console.log("ControlService.switchWatering():");
let actionUrl = this.config.CONTROLLER_URL
+ (switchOn ? this.config.WATERING_ON_PATH : this.config.WATERING_OFF_PATH);
return fetch(actionUrl ,{
method: "GET",
mode: "cors"
})
.then(response => {
if (!response.ok) {
this.handleResponseError(response);
}
return response.text();
}).then(result => {
return result;
}).catch(error => {
this.handleError(error);
});
}
async switchLight(switchOn) {
console.log("ControlService.switchLight():");
let actionUrl = this.config.CONTROLLER_URL
+ (switchOn ? this.config.LIGHT_ON_PATH : this.config.LIGHT_OFF_PATH);
return fetch(actionUrl ,{
method: "GET",
mode: "cors"
})
.then(response => {
if (!response.ok) {
this.handleResponseError(response);
}
return response.text();
}).then(result => {
return result;
}).catch(error => {
this.handleError(error);
});
}
handleResponseError(response) {
throw new Error("HTTP error, status = " + response.status);
}
handleError(error) {
console.log(error.message);
}
}
export default ControlService;
To encapsulate the control functionality, we create the control panel component, as it is shown in listing 2.5. To keep our code structured, we put the component into “components” folder.
Listing 2.5. components/ControlPanel.js: React component containing UI elements to send commands for controlling the garden devices.
import React, { useEffect, useState } from 'react';
import Switch from '@mui/material/Switch';
import ControlService from '../ControlService';
import Configuration from '../configuration';
function ControlPanel() {
const controlService = new ControlService();
const [checked1, setChecked1] = useState(false);
const [checked2, setChecked2] = useState(false);
useEffect(() => {
const config = new Configuration();
const fetchData = async () => {
try {
let response = await fetch(config.CONTROLLER_URL + config.WATERING_STATUS_PATH);
const isWateringActive = await response.text();
setChecked1(isWateringActive === "1" ? true : false);
response = await fetch(config.CONTROLLER_URL + config.LIGHT_STATUS_PATH);
const isLightActive = await response.text();
setChecked2(isLightActive === "1" ? true : false);
} catch (error) {
console.log("error", error);
}
};
fetchData();
}, []);
const handleWatering = (event) => {
controlService.switchWatering(event.target.checked).then(result => {
setChecked1(result === "1" ? true : false);
});
};
const handleLight = (event) => {
controlService.switchLight(event.target.checked).then(result => {
setChecked2(result === "1" ? true : false);
});
};
return(
<React.Fragment>
<div>
<label htmlFor='device1'>
<span>Watering</span>
<Switch id="1" name="device1" checked={checked1} onChange={handleWatering} />
</label>
<label htmlFor='device2'>
<span>Light</span>
<Switch id="2" name="device2" checked={checked2} onChange={handleLight}/>
</label>
</div>
</React.Fragment>
);
}
export default ControlPanel;
Using the stuff generated by the create-react-app tool, we can change the content of app.js as follows.
Listing 2.6. The base UI component
import './App.css';
import React from 'react';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import ControlPanel from './components/ControlPanel';
import { createTheme, ThemeProvider } from '@mui/material/styles';
function App() {
const theme = createTheme(
{
palette: {
primary: {
main: '#1b5e20',
},
secondary: {
main: '#689f38',
},
},
});
return (
<div className="App">
<ThemeProvider theme={theme}>
<AppBar position="static">
<Toolbar>
<Typography variant="h5">Smart Garden</Typography>
</Toolbar>
</AppBar>
<ControlPanel/>
</ThemeProvider>
</div>
);
}
export default App;
Now, it is time to test our UI application. Probably, you would have to set the CONTROLLER_URL configuration parameter to the IP address of the Raspberry Pi device where the outdoor controller back-end application is running, something like “http://192.168.nn.nn:3030”. After starting the application, it opens in a new browser tab.
Fig. 2.1. Control UI front-end application
If our outdoor controller application runs in a Raspberry Pi device connected to the control circuit (see Fig. 1.1), now we can switch on and off the watering and lighting circuits. We should see a screen similar to that shown in Figure 2.1.
If this is the case, you have done it! Congratulations! Now, you have a working remote control system, which can be extended for various devices and equipment. Also, we can integrate the sensor layer controller package into any system and communicate with its REST endpoints from UI and business logic components.
The article's source code is available on GitHub.
Opinions expressed by DZone contributors are their own.
Comments