Connected car is an emerging market for startups, from vehicle control (eg. remote start or opening doors) to vehicle diagnostics (mileage and maintenance) to vehicle tracking. As a whole, its market was valued at $52.62 billion in 2016 and is projected to reach $219.21 billion by 2025. That's a whole lot of opportunity.
One key player in the game is Smartcar, a platform for adding an interface on top of most modern cars, allowing developers and tinkerers to create intelligent, robust connected car applications. We're going to combine Smartcar with PubNub to create a scalable, realistic connected car solution.
Before we get started, there are two considerations. First, we need to make sure our application is secure. In this case we're dealing with a car, an expensive investment, so we need to make sure everything is secure. As cars advance and autonomous functionality is added, we need to ensure that a bad actor can't get control of our car and do something destructive. Second, we need to deliver a usable app. Usability has less to do with the technical portion and more to do with the design. Not only should we be making an application that is functional but something that is also easy and understandable.
Keeping those points in mind, this is what we’ll need to accomplish to have a finished project:
- A dashboard with UI to show the user relevant connected car information.
- A way for the user to choose their vehicle and securely authenticate themselves as a user.
Building the Vehicle Dashboard
We'll start with the frontend of our application, our dashboard which includes our map for tracking the car, a daily odometer reading, and a control panel for controlling components of the car (in this case, the doors).
In our terminal, we're going to start off by creating a new project folder with the create-react-app
starter project, then navigating into it.
npx create-react-app smart-vehicle-dashboard cd smart-vehicle-dashboard
Let's also add material-ui, google-map-react, and styled-components to our project since it will add pizzazz without too much effort.
npm install @material-ui/core npm install google-map-react
Now, let's open up our favorite code editor and open the App.js
file in our source folder. In here we'll create the structure for our application.
import React, { Component } from "react"; import "./App.css"; import PropTypes from "prop-types"; // Material UI components import { withStyles } from "@material-ui/core/styles"; import Card from "@material-ui/core/Card"; import CardActionArea from "@material-ui/core/CardActionArea"; import CardActions from "@material-ui/core/CardActions"; import CardContent from "@material-ui/core/CardContent"; import Button from "@material-ui/core/Button"; import Typography from "@material-ui/core/Typography"; import styled from "styled-components"; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import Snackbar from '@material-ui/core/Snackbar'; // Maps Plugin import GoogleMapReact from "google-map-react"; // Charts import ReactChartkick, { LineChart } from "react-chartkick"; import Chart from "chart.js"; ReactChartkick.addAdapter(Chart); const Container = styled.div` display: grid; grid-template-columns: 1fr 1fr; justify-items: center; `; const Marker = styled.div` font-size: 20pt; `; const SideMenu = styled.div` display: grid; grid-template-rows: 1fr 1fr; `; const Center = styled.div` `; const styles = { card: { maxWidth: "80%", margin: "auto" }, root: { flexGrow: 1, }, grow: { flexGrow: 1, }, menuButton: { marginLeft: -12, marginRight: 20, }, }; class App extends Component { constructor(props) { super(props); this.state = { model: "", make: "", year: "", id: "", odometer: 0, location: { lat: 37.741490, lng: -122.413230 } } } handleClick = () => { } render() { const { classes } = this.props; const { id, odometer, location, model, make, year} = this.state; const mapCenter = { lat: 37.741490, lng: -122.413230 } return ( <div className={classes.root}> <AppBar position="static" style={{margin: "0px 0px 20px 0px"}}> <Toolbar> <Typography variant="title" color="inherit" className={classes.grow}> My Vehicle Dashboard </Typography> <Button color="inherit" onClick={this.handleOnClick}>Connect Vehicle</Button> </Toolbar> </AppBar> <Container> <Center> <Card className={classes.card}> <div style={{height: "70vh", width: "100%" }}> <GoogleMapReact bootstrapURLKeys={{ key: "your google maps key" }} defaultCenter={mapCenter} defaultZoom={3} > <Marker lat={location.latitude} lng={location.longitude}> {this.state.make} </Marker> </GoogleMapReact> </div> <CardContent> <Typography gutterBottom variant="headline" component="h2"> {`${year} ${make} ${model}`} </Typography> <Typography component="p"> <strong> ID </strong> {id} <strong> Make </strong> {make} <strong> Model </strong> {model} <strong> Year </strong> {year} </Typography> </CardContent> </Card> </Center> <SideMenu> <Card styles={{width: "100%", height: "50px", psa:"20px 20px auto auto"}}> <CardContent> <Typography gutterBottom variant="headline" component="h2"> Odometer Reading </Typography> <LineChart data={{ "2017-09-07": odometer - (odometer - 300), "2017-09-08": odometer - (odometer - 20), "2017-09-09": odometer - (odometer - 50), "2017-09-10": odometer - (odometer - 10), "2017-09-11": odometer - (odometer - 5), "2017-09-12": odometer - (odometer - 10) }} /> </CardContent> </Card> <Card className={classes.card}> <CardActionArea> <CardContent> <Typography gutterBottom variant="headline" component="h2"> Interact with car </Typography> <Typography component="p"> Here you can do things like open the car door, open the trunk or even start your engine. All remotely and in real time. </Typography> </CardContent> </CardActionArea> <CardActions> <Button size="small" color="primary" onClick={() => { alert("Something happened"); }}> unlock Door </Button> </CardActions> </Card> </SideMenu> </Container> </div> ); } } App.propTypes = { classes: PropTypes.object.isRequired }; export default withStyles(styles)(App);
Token Based Authentication Without Hosting a Server (OAuth 2.0)
Using the Smartcar API follows a fairly typical path which is commonly used in modern APIs: using OAuth to give secured access to data. A common barrier for developers to use platforms with this type of architecture is that they need some sort of server to act as a redirect URI. The server acts as a place where the API will send back the token which the client can use to access data. Luckily, there is a simple and secure fix for this.
Serverless Function to Handle OAuth with PubNub
We'll now create a serverless Function which will act as our redirect URL. To get started with that, you'll first need to sign up for a PubNub account.
To work through this tutorial, sign up for a free PubNub account. You won’t need the Pub/Sub keys just yet, but you do need an account to create Functions. Usage is 100% free up to 1 Million requests per month.
Once we've registered, we'll need to navigate to our apps and create a new app. In that app, we can click the Functions icon on the left sidebar. This will allow us to create a module to house all of our Functions. The process looks like this.
Great! Now let's add the logic for creating an auth request to an API.
export default (request, response) => { const kvstore = require('kvstore'); const xhr = require('xhr'); const pubnub = require('pubnub'); const authCode = request.params.code; const state = request.params.state; const http_options = { "method": "POST", "headers": { "Authorization": "Basic {Client ID and Secret}", 'Content-Type': 'application/x-www-form-urlencoded' }, "body": `grant_type=authorization_code&code=${authCode}&redirect_uri=https://pubsub.pubnub.com/v1/blocks/sub-key/sub-c-d4fb4c70-a0d8-11e8-ab44-96e83d2b591d/smartcar` }; const url = "https://auth.smartcar.com/oauth/token"; return xhr.fetch(url, http_options).then((x) => { const body = JSON.parse(x.body); return kvstore.setItem("token", body.access_token).then(() => { response.status = 302; response.headers['Location'] = 'http://localhost:3000/'; return response.send(); }); }); }
The Function does two things. First, it sends a POST request to the API, in this case Smartcar, and in the request body, we define the parameters the body requires. Then, once we get the response token from the API we store it in a local KV Store which is shared across the Functions in this module.
This will later allow us to do different requests using the shared value but outside this Function. The benefit is that it allows us to make concise Functions contained within their own scope.
Configuring the Smartcar API
Head over to Smartcar and signup for an account. Once we've logged in we can register a new application with a display name. Doing that will give us our client id and client secret. The client secret won't be shown again so make sure you have it saved somewhere.
You'll see that there is a section named redirect URIs which lets us add a call back for authentication. This is where we'll copy the endpoint from our Function and add it to the list. By adding it to the list of redirect URI's it tells Smartcar that the link is secure to send the token to, allowing us to authenticate.
The next step will be to make sure that we hit the endpoint from our application to trigger the authentication flow. For that we'll create a handleOnClick
method which will pass in our clientID, Client Secret and also the permissions we need from the user. The data allowed for developers to get from the Smartcar API are well described in their documentation and are great for further read.
handleOnClick () { const permissions = [ "control_security", "control_security:unlock", "control_security:lock", "read_odometer", "read_location", "read_vehicle_info" ]; window.open(`https://connect.smartcar.com/oauth/authorize?response_type=code&client_id=your-client-id&scope=${permissions.join(" ")}&redirect_uri=https://pubsub.pubnub.com/v1/blocks/sub-key/you-sub-key/smartcar&state=0facda3319&mode=test`, 'toolbar=0,status=0,width=548,height=325'); }
Storing the Token on the Serverless Function
Now that we have the Smartcar API and auth configured, we need to somehow get the token from the serverless function.
We'll create a new Function on a channel called auth. This will allow different users connected to that channel to be able to receive the token and use it within the app. Our Function will look something like this:
export default (request) => { const kvstore = require('kvstore'); const xhr = require('xhr'); return kvstore.getItem("token").then((token) =>{ request.message = token; return request.ok(); // Return a promise when you're done }); }
All this Function does is grab the token which is stored in the previous Function and send it out to the channel.
Remotely Opening the Door of the Car
While we're in the console we'll also add functionality for opening doors from our dashboard. This Function will work on the lock channel, meaning whenever a user sends a message to the lock channel it will trigger and open the doors.
export default (request) => { const kvstore = require('kvstore'); const xhr = require('xhr'); const url = `https://api.smartcar.com/v1.0/vehicles/${request.message.vehicle}/security`; return kvstore.getItem("token").then((token) =>{ console.log(token); const http_options = { "method": "POST", "headers": { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }, "body": { "action": "LOCK" } }; return xhr.fetch(url, http_options).then((x) => { const body = JSON.parse(x.body); console.log(body.status); return request.ok(); // Return a promise when you're done }); }); }
Using the Fetch API to Request Data from a REST Endpoint
The Fetch API lets us make multiple requests one in succession by chaining them together. This is all possible because Fetch returns a promise which will allows us to execute another request based on the resolution.
Smartcar provides a wide array of endpoints to fetch data from. In particular we'll be grabbing the info related to the location, odometer and vehicle.
So our function in the App.js
of our project will look something like this:
fetchData(token){ const url = "https://api.smartcar.com/v1.0/vehicles/"; const http_options = { "headers": { "Authorization" : `Bearer ${token}` } }; fetch(url, http_options).then((res) => { if(res.ok){ return res.json(); }else { return Promise.reject(res.status); } }).then(({vehicles}) => { const urls = [ `${url + vehicles[0]}`, `${url + vehicles[0]}/location`, `${url + vehicles[0]}/odometer` ]; Promise.all(urls.map(url => fetch(url, http_options) .then(res => {return res.json()}) .then((value) => {return value}) .catch(error => {console.log(error)}) )) .then(data => { const { id, make, model, year } = data[0]; const {distance} = data[2]; this.setState( { id: id, model: model, make: make, year: year, odometer: distance, location: data[1] } ); }); }).catch((error)=> { console.log("error", error); }); }
Now that we have this, we'll update our constructor method will bind to the this context to our fetchData
Function.
constructor(props) { super(props); this.pubnub = new PubNubReact({ publishKey: 'your-publish-key', subscribeKey: 'your-subscribe-key' }); this.state = { model: "", make: "", year: "", id: "", odometer: 0, location: { lat: 37.741490, lng: -122.413230 } } this.fetchData = this.fetchData.bind(this) this.pubnub.init(this); }
Using the data we receive from our fetch, we can hydrate our components to display the data. When all is said and done, our component should look like this:
class App extends Component { constructor(props) { super(props); this.pubnub = new PubNubReact({ publishKey: 'your-publish-key', subscribeKey: 'your-subscribe-key' }); this.state = { model: "", make: "", year: "", id: "", odometer: 0, location: { lat: 37.741490, lng: -122.413230 } } this.fetchData = this.fetchData.bind(this) this.pubnub.init(this); } componentWillMount() { this.pubnub.subscribe({ channels: ['auth'], withPresence: true }); this.pubnub.getMessage(['auth'], (msg) => { this.fetchData(msg.message); }); this.pubnub.getStatus((st) => { this.pubnub.publish({ message: 'Req', channel: 'auth' }); }); } componentWillUnmount() { this.pubnub.unsubscribe({ channels: ['auth'] }); } handleOnClick () { // Code from earlier } fetchData(token){ // Code from earlier } render() { const { classes } = this.props; const { id, odometer, location, model, make, year} = this.state; const mapCenter = { lat: 37.741490, lng: -122.413230 } return ( <div className={classes.root}> <AppBar position="static" style={{margin: "0px 0px 20px 0px"}}> <Toolbar> <Typography variant="title" color="inherit" className={classes.grow}> My Vehicle Dashboard </Typography> <Button color="inherit" onClick={this.handleOnClick}>Connect Vehicle</Button> </Toolbar> </AppBar> <Container> <Center> <Card className={classes.card}> <div style={{height: "70vh", width: "100%" }}> <GoogleMapReact bootstrapURLKeys={{ key: "AIzaSyC1AhKe8qh8W0jgIvfJdGu8Nr5_aXnvddQ" }} defaultCenter={mapCenter} defaultZoom={3} > <Marker lat={location.latitude} lng={location.longitude}> {this.state.make} </Marker> </GoogleMapReact> </div> <CardContent> <Typography gutterBottom variant="headline" component="h2"> {`${year} ${make} ${model}`} </Typography> <Typography component="p"> <strong> ID </strong> {id} <strong> Make </strong> {make} <strong> Model </strong> {model} <strong> Year </strong> {year} </Typography> </CardContent> </Card> </Center> <SideMenu> <Card styles={{width: "100%", height: "50px", psa:"20px 20px auto auto"}}> <CardContent> <Typography gutterBottom variant="headline" component="h2"> Odometer Reading </Typography> <LineChart data={{ "2017-09-07": odometer - (odometer - 300), "2017-09-08": odometer - (odometer - 20), "2017-09-09": odometer - (odometer - 50), "2017-09-10": odometer - (odometer - 10), "2017-09-11": odometer - (odometer - 5), "2017-09-12": odometer - (odometer - 10) }} /> </CardContent> </Card> <Card className={classes.card}> <CardActionArea> <CardContent> <Typography gutterBottom variant="headline" component="h2"> Interact with car </Typography> <Typography component="p"> Here you can do things like open the car door, open the trunk or even start your engine. All remotely and in real time. </Typography> </CardContent> </CardActionArea> <CardActions> <Button size="small" color="primary" onClick={() => { this.pubnub.publish({ message: {vehicle: id}, channel: 'lock' }); alert("doors unlocked"); }}> unlock Door </Button> </CardActions> </Card> </SideMenu> </Container> </div> ); } } App.propTypes = { classes: PropTypes.object.isRequired }; export default withStyles(styles)(App);
The Complete Car Dashboard
Now that we've gone through those steps, our application will be able to go through the authentication flow, get the vehicle information, and display that information to the user.
We can see the tool in action in this video!