Node Red will hadle the web pages and controlling the GPIOs. For a detailed setup guide visit
Node-RED : Running on Raspberry Pi
To get the latest node red for Raspberry Pi run:
bash <(curl -sL https://raw.githubusercontent.com/node-red/raspbian-deb-package/master/resources/update-nodejs-and-nodered)
To create the settings file for Node-Red it needs to be started once. Run:
sudo systemctl start nodered
We need to tell Node-Red where to look for the web page assets. This will become the root directory for any web pages served
by http nodes. Run:
nano /home/pi/.node-red/settings.js
And change the line:
//httpStatic: '/home/nol/node-red-static/',
to:
httpStatic: '/home/pi/.picar/www/,
Press
ctrl-X
, save and exit.
Now we need to copy the web asstets into the folder that was specified in the settings. Create the directories by running:
mkdir /home/pi/.picar
mkdir /home/pi/.picar/www
Download the web assets here
picar-nodered-www.zip. Unzip and copy the
www
directory into
/home/pi/.picar
with your ssh client.
Restart Node-Red:
sudo systemctl restart nodered
Node-Red uses graphical "nodes" to layout a program which it will execute. By connecting these nodes different programs can
be written without much actual code. These layouts are called workflows.
To set up the program for this project I have provided three flows to import: Web Server, Control, and Credentials. You can
access Node-Red by opening a browser to the Pi's ip address on port 1880 in the node-red
directory e.g. (
http://ip_address:1880
).
Web Server
Open the menu in the top right of the screen and select
Import > Clipboard
Paste the following into a new flow.
[
{
"id": "acc22612.1a8258",
"type": "file in",
"z": "1fee60dc.191fff",
"name": "auth.html",
"filename": "/home/pi/.picar/www/auth.html",
"format": "utf8",
"sendError": true,
"x": 520,
"y": 80,
"wires": [
[
"aff0c571.204548"
]
]
},
{
"id": "9d08714e.9b236",
"type": "function",
"z": "1fee60dc.191fff",
"name": "Get Submitted",
"func": "var driver = msg.payload.driver.toLowerCase();\nvar key = msg.payload.key;\n\nmsg.driver = driver;\n\nvar keyObject = {};\nkeyObject[driver] = key;\n\nmsg.payload = JSON.stringify(keyObject);\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 160,
"y": 100,
"wires": [
[
"32ae2dc5.59e942"
]
]
},
{
"id": "ab57849c.aa3bc8",
"type": "function",
"z": "1fee60dc.191fff",
"name": "Store Hash",
"func": "msg.key = msg.payload;\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 150,
"y": 180,
"wires": [
[
"b8ae7304.33a36"
]
]
},
{
"id": "b8ae7304.33a36",
"type": "file in",
"z": "1fee60dc.191fff",
"name": "keys",
"filename": "/home/pi/.picar/www/keys",
"format": "utf8",
"sendError": true,
"x": 130,
"y": 220,
"wires": [
[
"812830b9.9fe19"
]
]
},
{
"id": "812830b9.9fe19",
"type": "json",
"z": "1fee60dc.191fff",
"name": "",
"x": 130,
"y": 260,
"wires": [
[
"b6755af4.76fc78"
]
]
},
{
"id": "b6755af4.76fc78",
"type": "function",
"z": "1fee60dc.191fff",
"name": "Validate",
"func": "var keyRing = msg.payload;\nvar driver = msg.driver.toLowerCase();\nvar key = msg.key;\n\nif (keyRing[driver] && keyRing[driver] === key) {\n keyobject = {};\n keyobject[driver] = key;\n msg.cookies = {\n carkey: {\n value: JSON.stringify(keyobject),\n maxAge: 10*365*24*60*60*1000\n }\n }\n msg.payload = 'succeeded';\n return msg\n}\nmsg.payload = 'failed';\nreturn msg;",
"outputs": "1",
"noerr": 0,
"x": 140,
"y": 300,
"wires": [
[
"3b5b91b6.7d3a4e"
]
]
},
{
"id": "3b5b91b6.7d3a4e",
"type": "http response",
"z": "1fee60dc.191fff",
"name": "",
"x": 270,
"y": 300,
"wires": []
},
{
"id": "aff0c571.204548",
"type": "http response",
"z": "1fee60dc.191fff",
"name": "",
"x": 650,
"y": 80,
"wires": []
},
{
"id": "d82fa1aa.d92c7",
"type": "http in",
"z": "1fee60dc.191fff",
"name": "",
"url": "/picar",
"method": "get",
"upload": false,
"swaggerDoc": "",
"x": 360,
"y": 60,
"wires": [
[
"3bd7d0a7.acf3a"
]
]
},
{
"id": "a6603349.42ce1",
"type": "file in",
"z": "1fee60dc.191fff",
"name": "keys",
"filename": "/home/pi/.picar/www/keys",
"format": "utf8",
"sendError": true,
"x": 350,
"y": 140,
"wires": [
[
"92e3b880.587308"
]
]
},
{
"id": "92e3b880.587308",
"type": "json",
"z": "1fee60dc.191fff",
"name": "",
"x": 350,
"y": 180,
"wires": [
[
"4fa6a0c2.68daa"
]
]
},
{
"id": "4fa6a0c2.68daa",
"type": "function",
"z": "1fee60dc.191fff",
"name": "Validate",
"func": "var keyring = msg.payload;\nvar submitted = msg.submitted;\nvar driver = Object.keys(submitted)[0].toLowerCase();\n\nvar key = submitted[driver];\n\nif (keyring[driver] && keyring[driver] === key) {\n return [null, msg];\n}\n\nreturn [msg, null];\n",
"outputs": "2",
"noerr": 0,
"x": 360,
"y": 220,
"wires": [
[
"eba92dae.3e76e"
],
[
"ea78a51.6065e58"
]
]
},
{
"id": "eba92dae.3e76e",
"type": "file in",
"z": "1fee60dc.191fff",
"name": "auth.html",
"filename": "/home/pi/.picar/www/auth.html",
"format": "utf8",
"sendError": true,
"x": 500,
"y": 200,
"wires": [
[
"7a0630ad.57f1d"
]
]
},
{
"id": "7a0630ad.57f1d",
"type": "http response",
"z": "1fee60dc.191fff",
"name": "",
"x": 630,
"y": 220,
"wires": []
},
{
"id": "ea78a51.6065e58",
"type": "file in",
"z": "1fee60dc.191fff",
"name": "main.html",
"filename": "/home/pi/.picar/www/main.html",
"format": "utf8",
"sendError": true,
"x": 500,
"y": 240,
"wires": [
[
"7a0630ad.57f1d"
]
]
},
{
"id": "2da48ba9.6645b4",
"type": "http in",
"z": "1fee60dc.191fff",
"name": "",
"url": "/picar",
"method": "post",
"upload": false,
"swaggerDoc": "",
"x": 150,
"y": 60,
"wires": [
[
"9d08714e.9b236"
]
]
},
{
"id": "3bd7d0a7.acf3a",
"type": "function",
"z": "1fee60dc.191fff",
"name": "Get Cookie",
"func": "var c = msg.req.cookies;\nif (c.carkey){\n msg.payload = c.carkey;\n msg.submitted = JSON.parse(c.carkey);\n return [null, msg];\n}\nreturn [msg, null]",
"outputs": "2",
"noerr": 0,
"x": 370,
"y": 100,
"wires": [
[
"acc22612.1a8258"
],
[
"a6603349.42ce1"
]
]
},
{
"id": "32ae2dc5.59e942",
"type": "hmac",
"z": "1fee60dc.191fff",S
"name": "",
"algorithm": "HmacSHA512",
"key": "Enter-HMAC-Key-Here",
"x": 130,
"y": 140,
"wires": [
[
"ab57849c.aa3bc8"
]
]
}
]
Open the hmac node by double clicking it. Enter a secret key to encrypt passwords. This key should be the same as the one
in the Credentials flow (more on that).
Control
For the flow that will control the GPIOs, paste the following into a new flow.
[
{
"id": "df1c684d.9cb458",
"type": "websocket in",
"z": "ce96cab0.f0ae38",
"name": "",
"server": "ded96095.9ca83",
"client": "",
"x": 130,
"y": 160,
"wires": [
[
"b0e47c94.e6633"
]
]
},
{
"id": "b0e47c94.e6633",
"type": "json",
"z": "ce96cab0.f0ae38",
"name": "",
"x": 290,
"y": 160,
"wires": [
[
"c6623a05.7892f8"
]
]
},
{
"id": "c6623a05.7892f8",
"type": "function",
"z": "ce96cab0.f0ae38",
"name": "",
"func": "var m = msg.payload;\n\nvar dmv = context.global.get('dmv')||{};\nvar license = m.License;\nvar now = (new Date()).getTime();\nif(dmv[license] + 300000 < now){\n msg.payload = \"Connection Timeout\";\n return [null, null, null, null, msg];\n}\n\nvar thrust = 'Idle';\nvar yaw = 'Straight';\n\nif(m.Forward == 1 && m.Reverse == 1){\n m.Forward = 0;\n m.reverse = 0;\n}\nif(m.Left == 1 && m.Right == 1){\n m.Right = 0;\n m.Left = 0;\n}\nif(m.Forward == 1){thrust = 'Forward';}\nif(m.Reverse == 1){thrust = 'Reverse';}\nif(m.Left == 1){yaw = 'Left';}\nif(m.Right == 1){yaw = 'Right';}\n\nvar text = thrust + ' ' + yaw;\n\nreturn [{payload: m.Forward},\n {payload: m.Reverse},\n {payload: m.Left},\n {payload: m.Right},\n {payload: text}];",
"outputs": "5",
"noerr": 0,
"x": 410,
"y": 160,
"wires": [
[
"c7ca048b.847218"
],
[
"eea3a4ae.fa6728"
],
[
"5ae351ad.5a538"
],
[
"575f8ccf.7c4274"
],
[
"45ffa258.57e9cc"
]
]
},
{
"id": "45ffa258.57e9cc",
"type": "websocket out",
"z": "ce96cab0.f0ae38",
"name": "",
"server": "ded96095.9ca83",
"client": "",
"x": 560,
"y": 280,
"wires": []
},
{
"id": "c7ca048b.847218",
"type": "rpi-gpio out",
"z": "ce96cab0.f0ae38",
"name": "PIN: 35 - Forward",
"pin": "35",
"set": true,
"level": "0",
"freq": "",
"out": "out",
"x": 570,
"y": 40,
"wires": []
},
{
"id": "5ae351ad.5a538",
"type": "rpi-gpio out",
"z": "ce96cab0.f0ae38",
"name": "PIN: 38 - Left",
"pin": "38",
"set": true,
"level": "0",
"freq": "",
"out": "out",
"x": 570,
"y": 160,
"wires": []
},
{
"id": "eea3a4ae.fa6728",
"type": "rpi-gpio out",
"z": "ce96cab0.f0ae38",
"name": "PIN: 37 - Reverse",
"pin": "37",
"set": true,
"level": "0",
"freq": "",
"out": "out",
"x": 610,
"y": 100,
"wires": []
},
{
"id": "575f8ccf.7c4274",
"type": "rpi-gpio out",
"z": "ce96cab0.f0ae38",
"name": "PIN: 36 - Right",
"pin": "36",
"set": true,
"level": "0",
"freq": "",
"out": "out",
"x": 600,
"y": 220,
"wires": []
},
{
"id": "ded96095.9ca83",
"type": "websocket-listener",
"z": "ce96cab0.f0ae38",
"path": "/ws/picar",
"wholemsg": "false"
}
]
Note the pins use for controling the car. Top to bottom they are: 35-forward, 37-reverse, 38-left, 36-right. Use these pins
when connecting the Pi to the RC car receiver (more on that).
Credentials
Paste the following into a new flow. This flow will allow the creation of username & password combinations with the secret
key.
[
{
"id": "4f0d436a.ec82ac",
"type": "function",
"z": "c737272f.d855b8",
"name": "Get Submitted",
"func": "msg.driver= msg.payload.driver;\nvar keyobject = {};\nkeyobject[msg.driver] = msg.payload.key;\nmsg.payload = JSON.stringify(keyobject);\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 160,
"y": 100,
"wires": [
[
"8c74f440.f638a8"
]
]
},
{
"id": "b33337a3.31d4d8",
"type": "function",
"z": "c737272f.d855b8",
"name": "Save Cred",
"func": "var driver = msg.driver;\nvar key = msg.key;\n\nvar keyring = msg.payload || {};\nkeyring[driver] = key;\n\nmsg.payload = JSON.stringify(keyring);\nreturn msg;",
"outputs": "1",
"noerr": 0,
"x": 410,
"y": 180,
"wires": [
[
"d07d1ae3.8f6b68"
]
]
},
{
"id": "d07d1ae3.8f6b68",
"type": "file",
"z": "c737272f.d855b8",
"name": "Put keys",
"filename": "/home/pi/.picar/www/keys",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"x": 400,
"y": 220,
"wires": []
},
{
"id": "be908f37.6b4ce",
"type": "file in",
"z": "c737272f.d855b8",
"name": "Get keys",
"filename": "/home/pi/.picar/www/keys",
"format": "utf8",
"sendError": true,
"x": 400,
"y": 100,
"wires": [
[
"d2df6932.934d08"
]
]
},
{
"id": "d2df6932.934d08",
"type": "json",
"z": "c737272f.d855b8",
"name": "",
"x": 390,
"y": 140,
"wires": [
[
"b33337a3.31d4d8"
]
]
},
{
"id": "8a376d0d.b18bf",
"type": "inject",
"z": "c737272f.d855b8",
"name": "New Driver",
"topic": "",
"payload": "{\"driver\":\"\", \"key\":\"\"}",
"payloadType": "json",
"repeat": "",
"crontab": "",
"once": false,
"x": 140,
"y": 60,
"wires": [
[
"4f0d436a.ec82ac"
]
]
},
{
"id": "f0dd55f9.38ace8",
"type": "function",
"z": "c737272f.d855b8",
"name": "Get Cred",
"func": "msg.key = msg.payload;\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 140,
"y": 180,
"wires": [
[
"c2927ec8.d2996"
]
]
},
{
"id": "c2927ec8.d2996",
"type": "function",
"z": "c737272f.d855b8",
"name": "keys Exists",
"func": "if(!msg.payload){\n return [{payload:'{}'}, null]\n}\nreturn [null, msg];",
"outputs": "2",
"noerr": 0,
"x": 150,
"y": 220,
"wires": [
[
"761b678e.1e1158"
],
[
"be908f37.6b4ce"
]
]
},
{
"id": "761b678e.1e1158",
"type": "file",
"z": "c737272f.d855b8",
"name": "Make keys",
"filename": "/home/pi/.picar/www/keys",
"appendNewline": true,
"createDir": false,
"overwriteFile": "false",
"x": 390,
"y": 60,
"wires": []
},
{
"id": "8c74f440.f638a8",
"type": "hmac",
"z": "c737272f.d855b8",
"name": "",
"algorithm": "HmacSHA512",
"key": "The-same-secret-key-from-the-webserver-flow",
"x": 130,
"y": 140,
"wires": [
[
"f0dd55f9.38ace8"
]
]
}
]
Again open the hmac node by double clicking it. Enter the same secret key you used in the Web Server flow.
To create a new driver from this screen open the New Driver node by double clicking it. Edit the payload with the desired
credentials.
{"driver":"enter-user-name", "key":"enter-password"}
Click
Done
and then
Deploy
. Once the flow is deployed click the box on the left of the New Driver node to inject the
credentials. The username and a hash of the password will be added to the "keys" file. You
can now log into the page with those credentials. Any additional times this is done adds
a new user. If a username is reused it should update that user. To delete a user open the
"keys" file in
/home/pi/.picar/www/
and delete that entry. I hope to come up with a better solution for that in the future.
View the results
Point a browser to the root of the Pi's webserver on port 1880.
http://ip_address:1880/picar
. If everything has gone according to the plan the PiCar webpage should be visible. There
will be no video for the webcam because that has not been setup. If the direction buttons
are pressed the Pi should respond by updating the direction text.