Hackster is hosting Hackster Holidays, Ep. 7: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Friday!Stream Hackster Holidays, Ep. 7 on Friday!
Sam Jackson
Published © MIT

Phone controlled wifi RC car

Use a Raspberry Pi Zero W to control an old RC car with a Web cam to see where you're going.

IntermediateFull instructions provided229
Phone controlled wifi RC car

Things used in this project

Hardware components

Raspberry Pi Zero Wireless
Raspberry Pi Zero Wireless
×1
Used RC Car
An older model car is best with a older surface mount chip. These may be found at a thrift store for less than $5.00. A controller is not required so don't worry about that. A battery is nice but not always available with used cars.
×1
Logitech C270 Widescreen HD Webcam
×1
8GB SD Card w/ Stretch Lite
Adafruit sells this SD card with the OS pre-installed. If you happen to already have an SD card that you want to use don't worry. I'll go over how to install Raspbian.
×1
USB OTG Host Cable - MicroB OTG male to A female
×1
NiMH Battery
If the used car does not come with a battery one will need to be purchased. Be sure to get a voltage that matches what is required for the car.
×1
USB Car Charger
×1
RC Model Big Tamiya Connector
×1

Software apps and online services

Node-RED
Node-RED
Raspbian
Raspberry Pi Raspbian

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Code

settings.js

JavaScript
Node Red Settings
/**
 * Copyright JS Foundation and other contributors, http://js.foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/

// The `https` setting requires the `fs` module. Uncomment the following
// to make it available:
//var fs = require("fs");

module.exports = {
    // the tcp port that the Node-RED web server is listening on
    uiPort: process.env.PORT || 1880,

    // By default, the Node-RED UI accepts connections on all IPv4 interfaces.
    // The following property can be used to listen on a specific interface. For
    // example, the following would only allow connections from the local machine.
    //uiHost: "127.0.0.1",

    // Retry time in milliseconds for MQTT connections
    mqttReconnectTime: 15000,

    // Retry time in milliseconds for Serial port connections
    serialReconnectTime: 15000,

    // Retry time in milliseconds for TCP socket connections
    //socketReconnectTime: 10000,

    // Timeout in milliseconds for TCP server socket connections
    //  defaults to no timeout
    //socketTimeout: 120000,

    // Timeout in milliseconds for HTTP request connections
    //  defaults to 120 seconds
    //httpRequestTimeout: 120000,

    // The maximum length, in characters, of any message sent to the debug sidebar tab
    debugMaxLength: 1000,

    // To disable the option for using local files for storing keys and certificates in the TLS configuration
    //  node, set this to true
    //tlsConfigDisableLocalFiles: true,

    // Colourise the console output of the debug node
    //debugUseColors: true,

    // The file containing the flows. If not set, it defaults to flows_<hostname>.json
    //flowFile: 'flows.json',

    // To enabled pretty-printing of the flow within the flow file, set the following
    //  property to true:
    //flowFilePretty: true,

    // By default, credentials are encrypted in storage using a generated key. To
    // specify your own secret, set the following property.
    // If you want to disable encryption of credentials, set this property to false.
    // Note: once you set this property, do not change it - doing so will prevent
    // node-red from being able to decrypt your existing credentials and they will be
    // lost.
    //credentialSecret: "a-secret-key",

    // By default, all user data is stored in the Node-RED install directory. To
    // use a different location, the following property can be used
    //userDir: '/home/nol/.node-red/',

    // Node-RED scans the `nodes` directory in the install directory to find nodes.
    // The following property can be used to specify an additional directory to scan.
    nodesDir: '/home/pi/.node-red/nodes',

    // By default, the Node-RED UI is available at http://localhost:1880/
    // The following property can be used to specifiy a different root path.
    // If set to false, this is disabled.
    httpAdminRoot: '/node-red',

    // Some nodes, such as HTTP In, can be used to listen for incoming http requests.
    // By default, these are served relative to '/'. The following property
    // can be used to specifiy a different root path. If set to false, this is
    // disabled.
    //httpNodeRoot: '/node-red',

    // The following property can be used in place of 'httpAdminRoot' and 'httpNodeRoot',
    // to apply the same root to both parts.
    //httpRoot: '/red',

    // When httpAdminRoot is used to move the UI to a different root path, the
    // following property can be used to identify a directory of static content
    // that should be served at http://localhost:1880/.
    httpStatic: '/home/pi/picar/www/',

    // The maximum size of HTTP request that will be accepted by the runtime api.
    // Default: 5mb
    //apiMaxLength: '5mb',

    // If you installed the optional node-red-dashboard you can set it's path
    // relative to httpRoot
    //ui: { path: "ui" },

    // Securing Node-RED
    // -----------------
    // To password protect the Node-RED editor and admin API, the following
    // property can be used. See http://nodered.org/docs/security.html for details.
    adminAuth: {
        type: "credentials",
        users: [{
            username: "red-admin",
            password: "",
            permissions: "*"
        }]
    },

    // To password protect the node-defined HTTP endpoints (httpNodeRoot), or
    // the static content (httpStatic), the following properties can be used.
    // The pass field is a bcrypt hash of the password.
    // See http://nodered.org/docs/security.html#generating-the-password-hash
    //httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
    //httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},

    // The following property can be used to enable HTTPS
    // See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener
    // for details on its contents.
    // See the comment at the top of this file on how to load the `fs` module used by
    // this setting.
    //
    //https: {
    //    key: fs.readFileSync('privatekey.pem'),
    //    cert: fs.readFileSync('certificate.pem')
    //},

    // The following property can be used to cause insecure HTTP connections to
    // be redirected to HTTPS.
    //requireHttps: true

    // The following property can be used to disable the editor. The admin API
    // is not affected by this option. To disable both the editor and the admin
    // API, use either the httpRoot or httpAdminRoot properties
    //disableEditor: false,

    // The following property can be used to configure cross-origin resource sharing
    // in the HTTP nodes.
    // See https://github.com/troygoode/node-cors#configuration-options for
    // details on its contents. The following is a basic permissive set of options:
    //httpNodeCors: {
    //    origin: "*",
    //    methods: "GET,PUT,POST,DELETE"
    //},

    // If you need to set an http proxy please set an environment variable
    // called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system.
    // For example - http_proxy=http://myproxy.com:8080
    // (Setting it here will have no effect)
    // You may also specify no_proxy (or NO_PROXY) to supply a comma separated
    // list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk

    // The following property can be used to add a custom middleware function
    // in front of all http in nodes. This allows custom authentication to be
    // applied to all http in nodes, or any other sort of common request processing.
    //httpNodeMiddleware: function(req,res,next) {
    //    // Handle/reject the request, or pass it on to the http in node by calling next();
    //    // Optionally skip our rawBodyParser by setting this to true;
    //    //req.skipRawBodyParser = true;
    //    next();
    //},

    // The following property can be used to verify websocket connection attempts.
    // This allows, for example, the HTTP request headers to be checked to ensure
    // they include valid authentication information.
    //webSocketNodeVerifyClient: function(info) {
    //    // 'info' has three properties:
    //    //   - origin : the value in the Origin header
    //    //   - req : the HTTP request
    //    //   - secure : true if req.connection.authorized or req.connection.encrypted is set
    //    //
    //    // The function should return true if the connection should be accepted, false otherwise.
    //    //
    //    // Alternatively, if this function is defined to accept a second argument, callback,
    //    // it can be used to verify the client asynchronously.
    //    // The callback takes three arguments:
    //    //   - result : boolean, whether to accept the connection or not
    //    //   - code : if result is false, the HTTP error status to return
    //    //   - reason: if result is false, the HTTP reason string to return
    //},

    // Anything in this hash is globally available to all functions.
    // It is accessed as context.global.
    // eg:
    //    functionGlobalContext: { os:require('os') }
    // can be accessed in a function block as:
    //    context.global.os

    functionGlobalContext: {
        // os:require('os'),
        // octalbonescript:require('octalbonescript'),
        // jfive:require("johnny-five"),
        // j5board:require("johnny-five").Board({repl:false})
    },

    // The following property can be used to order the categories in the editor
    // palette. If a node's category is not in the list, the category will get
    // added to the end of the palette.
    // If not set, the following default order is used:
    //paletteCategories: ['subflows', 'input', 'output', 'function', 'social', 'mobile', 'storage', 'analysis', 'advanced'],

    // Configure the logging output
    logging: {
        // Only console logging is currently supported
        console: {
            // Level of logging to be recorded. Options are:
            // fatal - only those errors which make the application unusable should be recorded
            // error - record errors which are deemed fatal for a particular request + fatal errors
            // warn - record problems which are non fatal + errors + fatal errors
            // info - record information about the general running of the application + warn + error + fatal errors
            // debug - record information which is more verbose than info + info + warn + error + fatal errors
            // trace - record very detailed logging + debug + info + warn + error + fatal errors
            // off - turn off all logging (doesn't affect metrics or audit)
            level: "info",
            // Whether or not to include metric events in the log output
            metrics: false,
            // Whether or not to include audit events in the log output
            audit: false
        }
    }
}

flows_raspberrypi.json

JSON
The Flows file for node red
[{"id":"1fee60dc.191fff","type":"tab","label":"PiCar Auth"},{"id":"8cc8cd00.37ee","type":"tab","label":"PiCar Control","disabled":false},{"id":"e536286d.022a58","type":"tab","label":"PiCar Driver","disabled":false,"info":""},{"id":"32f33346.bc3b9c","type":"websocket-listener","z":"8cc8cd00.37ee","path":"/ws/picar","wholemsg":"false"},{"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":140,"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":160,"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":240,"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":280,"wires":[["812830b9.9fe19"]]},{"id":"812830b9.9fe19","type":"json","z":"1fee60dc.191fff","name":"","x":130,"y":320,"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":360,"wires":[["3b5b91b6.7d3a4e"]]},{"id":"3b5b91b6.7d3a4e","type":"http response","z":"1fee60dc.191fff","name":"","x":270,"y":360,"wires":[]},{"id":"aff0c571.204548","type":"http response","z":"1fee60dc.191fff","name":"","x":650,"y":140,"wires":[]},{"id":"d82fa1aa.d92c7","type":"http in","z":"1fee60dc.191fff","name":"","url":"/picar","method":"get","upload":false,"swaggerDoc":"","x":360,"y":120,"wires":[["3bd7d0a7.acf3a"]]},{"id":"78b167b4.a9f548","type":"function","z":"e536286d.022a58","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":180,"y":100,"wires":[["16f254da.af2e9b"]]},{"id":"abdf6535.c50fa8","type":"function","z":"e536286d.022a58","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":170,"y":380,"wires":[["1c3a89fe.3c8ea6"]]},{"id":"1c3a89fe.3c8ea6","type":"file","z":"e536286d.022a58","name":"Put keys","filename":"/home/pi/.picar/www/keys","appendNewline":true,"createDir":false,"overwriteFile":"true","x":160,"y":420,"wires":[]},{"id":"446b2f1.a7503d","type":"file in","z":"e536286d.022a58","name":"Get keys","filename":"/home/pi/.picar/www/keys","format":"utf8","sendError":true,"x":160,"y":300,"wires":[["b7a6615f.8451c"]]},{"id":"b7a6615f.8451c","type":"json","z":"e536286d.022a58","name":"","x":150,"y":340,"wires":[["abdf6535.c50fa8"]]},{"id":"8506b8c2.b64d08","type":"inject","z":"e536286d.022a58","name":"","topic":"","payload":"{\"driver\":\"\",\"key\":\"\"}","payloadType":"json","repeat":"","crontab":"","once":false,"x":190,"y":60,"wires":[["78b167b4.a9f548"]]},{"id":"a6603349.42ce1","type":"file in","z":"1fee60dc.191fff","name":"keys","filename":"/home/pi/.picar/www/keys","format":"utf8","sendError":true,"x":350,"y":200,"wires":[["92e3b880.587308"]]},{"id":"92e3b880.587308","type":"json","z":"1fee60dc.191fff","name":"","x":350,"y":240,"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":280,"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":260,"wires":[["7a0630ad.57f1d"]]},{"id":"7a0630ad.57f1d","type":"http response","z":"1fee60dc.191fff","name":"","x":630,"y":280,"wires":[]},{"id":"54124946.bfd8e8","type":"websocket in","z":"8cc8cd00.37ee","name":"","server":"32f33346.bc3b9c","client":"","x":150,"y":200,"wires":[["9ebd8f9d.38da"]]},{"id":"9ebd8f9d.38da","type":"json","z":"8cc8cd00.37ee","name":"","x":290,"y":200,"wires":[["1bfe69cb.9c2b36"]]},{"id":"1bfe69cb.9c2b36","type":"function","z":"8cc8cd00.37ee","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":200,"wires":[["41fb4523.00becc"],["86393fb4.c8342"],["5cb79342.dd7d1c"],["e2b0e06c.1d6c"],["f8b5c537.a5adb8"]]},{"id":"f8b5c537.a5adb8","type":"websocket out","z":"8cc8cd00.37ee","name":"","server":"32f33346.bc3b9c","client":"","x":580,"y":280,"wires":[]},{"id":"41fb4523.00becc","type":"rpi-gpio out","z":"8cc8cd00.37ee","name":"","pin":"7","set":true,"level":"0","freq":"","out":"out","x":560,"y":120,"wires":[]},{"id":"5cb79342.dd7d1c","type":"rpi-gpio out","z":"8cc8cd00.37ee","name":"","pin":"13","set":true,"level":"0","freq":"","out":"out","x":600,"y":200,"wires":[]},{"id":"86393fb4.c8342","type":"rpi-gpio out","z":"8cc8cd00.37ee","name":"","pin":"11","set":true,"level":"0","freq":"","out":"out","x":580,"y":160,"wires":[]},{"id":"e2b0e06c.1d6c","type":"rpi-gpio out","z":"8cc8cd00.37ee","name":"","pin":"15","set":true,"level":"0","freq":"","out":"out","x":580,"y":240,"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":300,"wires":[["7a0630ad.57f1d"]]},{"id":"2da48ba9.6645b4","type":"http in","z":"1fee60dc.191fff","name":"","url":"/picar","method":"post","upload":false,"swaggerDoc":"","x":150,"y":120,"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":160,"wires":[["acc22612.1a8258"],["a6603349.42ce1"]]},{"id":"d1f7af12.d2379","type":"function","z":"e536286d.022a58","name":"Get Cred","func":"msg.key = msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":160,"y":180,"wires":[["b1af3d39.bc287"]]},{"id":"b1af3d39.bc287","type":"file-exists","z":"e536286d.022a58","name":"Check keys","filename":"/home/pi/.picar/www/keys","x":170,"y":220,"wires":[["4d81c141.92f8e"]]},{"id":"4d81c141.92f8e","type":"function","z":"e536286d.022a58","name":"keys Exists","func":"if(!msg.payload){\n    return [{payload:'{}'}, null]\n}\nreturn [null, msg];","outputs":"2","noerr":0,"x":170,"y":260,"wires":[["2acfe82d.167398"],["446b2f1.a7503d"]]},{"id":"2acfe82d.167398","type":"file","z":"e536286d.022a58","name":"Make keys","filename":"/home/pi/.picar/www/keys","appendNewline":true,"createDir":false,"overwriteFile":"false","x":360,"y":260,"wires":[]},{"id":"32ae2dc5.59e942","type":"hmac","z":"1fee60dc.191fff","name":"","algorithm":"HmacSHA512","key":"Q43!0T!n&2Gx07Zs#IC%nx9JrXum0BW2","x":130,"y":200,"wires":[["ab57849c.aa3bc8","6b61a7a8.72bb18"]]},{"id":"16f254da.af2e9b","type":"hmac","z":"e536286d.022a58","name":"","algorithm":"HmacSHA512","key":"Q43!0T!n&2Gx07Zs#IC%nx9JrXum0BW2","x":150,"y":140,"wires":[["d1f7af12.d2379","c61bb8a9.13fa48"]]},{"id":"c61bb8a9.13fa48","type":"debug","z":"e536286d.022a58","name":"","active":true,"console":"false","complete":"false","x":381,"y":143,"wires":[]},{"id":"6b61a7a8.72bb18","type":"debug","z":"1fee60dc.191fff","name":"","active":true,"console":"false","complete":"false","x":304,"y":453,"wires":[]}]

mjpeg-streamer.service

Plain text
Systemd daemon to run Mjpeg-Streamer
[Unit]
Description=Mjpg-Streamer Daemon
After=network.target

[Service]
Type=simple
User=pi
WorkingDirectory=/usr/local/bin/
ExecStart=/usr/local/bin/mjpg_streamer -i "/usr/local/lib/input_uvc.so -y -r 320x240 -fps 15 -q 50" -o "/usr/local/lib/output_http.so -w /usr/local/www"
Restart=on-failure

[Install]
WantedBy=multi-user.target

PiCar

Credits

Sam Jackson
1 project • 1 follower

Comments