samples_content/raspberry_pi/node/dashboard_sample
RVR--- id: dashboard_sample title: A Dashboard to Allow You to Control and Observe Your RVR sidebar_label: Dashboard
Deprecated
Because we are going to be building a real-time application, where we want the graphs and other numbers on our dashboard to update as items like the speed or power change AND so that we can spend less time building our own versions of elements like toggles and page separators, our example dashboard is going to be built as a React app that employs Material-UI.
Initializing Your React Application
To get you on your feet quickly, we recommend utilizing Facebook’s create-react-app to initiate your app. There will be some files that we’ll need to get rid of or change, but it gets us started with a general React application and the proper way of structuring such an app. Scrolling down past the files in the create-react-app Github, to the README will take you to the breakdown of the steps of using create-react-app. If you run the start terminal command after creating your app, as they suggest (we'll use npm start for this app, since we used npm to initiate our app), you’ll see the example app at http://localhost:3000/ and can get a better idea of the items we might need to change in order to create a dashboard.
Cleaning House
Your app will be initiated with (1) the node modules that you’ll need in order to create your app (you can always add or remove modules as needed, but create-react-app has taken care of the basics for us), (2) a public directory to contain all of the assets, like logos, that will be visible to the “public”, and (3) a src directory that contains the main structure of our application. Additionally, we’ll have a .gitignore file that lists all of the files that we don’t want git to pay attention to changes in, a package.json file to list and configure all of our packages, a README.md file where we can take notes and explain, in layman’s terms, what is going on in our app, and a package.lock file that is autogenerated to store information about our packages and their versioning.
Let’s make some quick changes to make the app feel a bit more like our own… We’ll start by jumping in to the public directory and switching out some of our logos and the favicon, which is the image that will show up in the browser tab. Now that I replaced the default favicon.ico with my own favicon.png and swapped out the default logos for some RVR+/RVR logos, my public directory looks like this:
Note: When you make these file changes, make sure you also change the references to them, in this case, in index.html (below) and manifest.json!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Sphero Public SDK</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
Next, we can jump into the src directory and begin our creation journey by exploring the files that have been created for us and what they do.
Diving In
App.js
At this point, we don’t actually need a logo to be the main content of the page but its presence, for the moment, is helpful for us to visualize how to incorporate internal assets, now, before we start incorporating our own internal elements. The logo is its own element in the src directory that gets imported into the files where it is used, like App.js, just like the imports we'll do later:
Original App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
Using the Client SDK
In order to start to transform our app into a Sphero-specific dashboard, we'll make a few edits to the App.js file, including removing all of the HTML that renders the icon in the middle of the page. Before diving into edits, let's import the ClientJS SDK. We created a prepackaged version of it that you can steal from the GitHub directory for this example, called "sdk-v4-convenience-raspberry-pi-client-js-0.0.0.tgz"; we can import the SDK into the application by loading (ie copying) it in our root directory and then including it in our node modules by running:
npm install sdk-v4-convenience-raspberry-pi-client-js-0.0.0.tgz
Running the Node SDK on the Pi
In order for our desktop dashboard to communicate with and receive information from our Pi, we'll need to be running the Node SDK on our Pi. The first step would be to ensure you have the Node SDK on your Pi (there are a few ways to add the Node SDK to your Pi, per the Pi Setup Guide).
Once you are sure that you have the Node SDK on your Pi, cd into that directory (likely called sphero-sdk-raspberrypi-nodejs) and start by running
npm run build
and then, once that completes, run
npm run start
Taking these two steps will allow you to start streaming data from your Pi and see the dashboard you are building display data in real time, as you are building it! (as an aside, your RVR+/RVR will not stream data while it is asleep; if you notice that the data is not displaying as you expect it to, make sure your RVR+/RVR is not asleep 😉)
Importing Additional Libraries
There are a few things that are unique to our application that we'll need to load in to our app to allow us to have elements like graphs, the color picker and the joystick. Additionally, we'll need to load in Material-UI and the styled components therein; let's start with those imports:
npm install @material-ui/core
npm install @material-ui/styles
Start Your Edits
Because we'll be communicating with the RVR+/RVR throughout the application, we initiate our specific instance of the RVR+/RVR (with our RVR's IP address and port), called rvrToy, here, at the top level, by first importing SpheroRvrToy from the ClientJS SDK we just added to the app. We'll also create a styled div, called TheBigRoot to be the basis of our application:
Our Brand-New, Shiny App.js
import React from 'react';
import { SpheroRvrToy } from 'sdk-v4-convenience-raspberry-pi-client-js';
import { styled } from '@material-ui/styles';
let rvrToy = new SpheroRvrToy('10.211.2.18', '2010');
// This is a styled component; we are using the ones specific to Material UI, as opposed to the original, vanilla version
const TheBigRoot = styled('div')({
display: 'flex',
font: 'Roboto'
});
const App = () => {
return (
<TheBigRoot>
</TheBigRoot>
);
};
export default App;
That's it for now! We'll come back and add to our App.js file once we've had a chance to walk through the other files that compose the skeleton of our app.
index.js
We got a peek at the index.html file that contains the structure (skeleton) of our site, which is in our public directory, but the files here, in the src directory, are where the magic happens. Our index.**js** file is what connects our skeleton to all the elements that make our app pretty and fun, by selecting an element and rendering it where it is told to; render is a function that takes two arguments:
Which component to render (<App />)
Where to render that component (in the root element, which is in index.html)
Luckily, we won't need to make any changes to index.js for it to work with our app:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
serviceWorker.unregister();
index.css
We won't need to make too many edits to our index.css file, which contains styles we'll want, in general, for the app:
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
The styles we'll focus on changing are the fonts; here at Sphero, we like to use "Roboto":
body {
margin: 0;
font-family: 'Roboto';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: 'Roboto';
}
That concludes the files we'll be using from the base create-react-app application and the changes we'll need to make to them (aside from adding some base components we're about to create to App.js)!
Initial Dashboard Structure
Let's create a couple of components that give our app a little character: (1) a header bar across the top and (2) the body, where all of the views and controls will live. Because it will take quite a few components to realize our final vision, we created a components directory in the src directory to contain them all:
Header Bar
The first component we can add to our components directory is for the bar that goes across the top of the app; we chose to name this TopBar.js. In the version of the app that we chose to create, no Sphero components were used in the bar across the top; we used a Material-UI AppBar/ToolBar combination with Material-UI Typography for the text:
TopBar.js
import React from 'react';
import { styled } from '@material-ui/styles';
import { AppBar, Toolbar, Typography } from '@material-ui/core';
const drawerWidth = 240;
const StyledTopBar = styled(AppBar)({
width: '100%',
marginLeft: drawerWidth,
backgroundColor: '#4200b7'
});
const TopBar = () => {
return (
<StyledTopBar position='fixed'>
<Toolbar>
<Typography variant='h4' noWrap>
RVR Dashboard
</Typography>
</Toolbar>
</StyledTopBar>
);
};
export default TopBar;
Page Body
Our page body element is what will contain all of the rest of the components we create (or at least the components that contain those components, etc). For now, we can just set up the page body component to later be able to contain the components we'll create. We decided to, creatively, call our page body element "PageBody.js":
PageBody.js
import React from 'react';
import { Paper, Grid } from '@material-ui/core';
import { styled } from '@material-ui/styles';
const BodyContent = styled('div')({
flexGrow: 1,
backgroundColor: 'aliceblue',
padding: '20px'
});
const PageBody = props => {
return (
<BodyContent>
</BodyContent>
);
};
export default PageBody;
Adding the Header Bar and Page Body to App.js
Our app can now be composed of a pretty colored header and the body that will contain all of the visuals and controls for our dashboard. We'll just need to import the components we just created (TopBar and PageBody) and add them to the jsx in App.js. When we add PageBody to the page, we'll also pass in the rvrToy element so that we can use it within the components we'll be creating to do things like ask RVR+/RVR about its battery status and change some of RVR's LEDs:
App.js
import React from 'react';
import { SpheroRvrToy } from 'sdk-v4-convenience-raspberry-pi-client-js';
import { styled } from '@material-ui/styles';
import PageBody from './components/PageBody';
import TopBar from './components/TopBar';
let rvrToy = new SpheroRvrToy('10.211.2.18', '2010');
const TheBigRoot = styled('div')({
display: 'flex',
font: 'Roboto'
});
const App = () => {
return (
<TheBigRoot>
<TopBar></TopBar>
<PageBody rvrToy={rvrToy}></PageBody>
</TheBigRoot>
);
};
export default App;
Adding Content
As was mentioned above, the page body is where we'll be adding all of our content. We'll use this section to walk through the different page elements and touch briefly on organization (but we do want to focus more on Sphero than on the nuances of React 😉).
Let's work from the top down, starting with the information we'll query the RVR+/RVR for as soon as we connect...
System Info
For the info we'll grab about the system and the power info in the next section, we chose to break each piece of information out into its own card for organization and readability. The "Main App Version Card" is the first one we display; let's take a look at how that is constructed...
There are two processors in the RVR+/RVR to handle different capabilities like bluetooth or driving, each with its own software. We call the getMainApplicationVersion method on both processors, which, behind the scenes, have been given permanent addresses of "1" and "2", hence the arguments of "primaryTarget" and "secondaryTarget". The response to getMainApplicationVersion contains 3 elements: 1) the major version, 2) the minor version, 3) the revision number, which we then use to construct the period-separated "Main App Version" that we display on the card:
MainAppVersion.js
import React from 'react';
import { Card, CardContent, Typography } from '@material-ui/core';
import { styled } from '@material-ui/styles';
import { SpheroRvrToy } from 'sdk-v4-convenience-raspberry-pi-client-js';
const MainAppVersionCard = styled(Card)({
width: '240px',
padding: '10px',
textAlign: 'center',
color: 'dark-gray'
});
const Title = styled(Typography)({
fontSize: '20px'
});
class MainAppVersion extends React.Component {
constructor(props) {
super(props);
this.state = { data1: '', data2: '' };
}
componentDidMount() {
this.props.rvrToy
.getMainApplicationVersion(SpheroRvrToy.primaryTarget)
.then(data => {
this.setState({
data1: JSON.parse(data)
});
});
this.props.rvrToy
.getMainApplicationVersion(SpheroRvrToy.secondaryTarget)
.then(data => {
this.setState({
data2: JSON.parse(data)
});
});
}
render() {
return (
<MainAppVersionCard>
<CardContent>
<Title>Main Application Version</Title>
{this.state.data1 && (
<Typography>
Nordic: {this.state.data1.major}.{this.state.data1.minor}.
{this.state.data1.revision}
</Typography>
)}
{this.state.data2 && (
<Typography>
ST: {this.state.data2.major}.{this.state.data2.minor}.
{this.state.data2.revision}
</Typography>
)}
</CardContent>
</MainAppVersionCard>
);
}
}
export default MainAppVersion;
Each of the cards in our System Information Paper (Material-UI element) will be constructed similarly to the "Main App Version card"; you can see the code for each of the others in our GitHub Repo.
Once we have created all of our System Information Cards, we can construct our System Information Paper that contains them all. We pass rvrToy into each of our components via props, as the commands to retrieve system information are all called directly on the SpheroRvrToy object (which we created an instance of in App.js, called rvrToy, to use across this application). You'll also notice that we are using a Grid to organize all of the elements within the Paper, creating the rows within by setting the containing Grid to a type of "container" with a direction of "row":
SystemInformation.js
import React from 'react';
import { Paper, Grid, Typography } from '@material-ui/core';
import MainAppVersion from './MainAppVersion';
import BootloaderVersion from './BootloaderVersion';
import BoardRevision from './BoardRevision';
import { styled } from '@material-ui/styles';
import MACAddress from './MACAddress';
import SKU from './SKU';
import BluetoothAdvertisingName from './BluetoothAdvertisingName';
const SystemInformationPaper = styled(Paper)({
padding: '10px',
margin: '20px',
color: 'dark-gray',
height: '415px'
});
const Title = styled(Typography)({
fontSize: '24px'
});
const StyledSubGrid = styled(Grid)({
margin: '20px 30px'
});
const SystemInformation = props => {
return (
<React.Fragment>
<SystemInformationPaper>
<Title gutterBottom>System Information</Title>
<Grid>
<Grid container direction='row' alignItems='center'>
<StyledSubGrid item>
<MainAppVersion rvrToy={props.rvrToy}>xs=3</MainAppVersion>
</StyledSubGrid>
<StyledSubGrid item>
<BootloaderVersion rvrToy={props.rvrToy}>xs=3</BootloaderVersion>
</StyledSubGrid>
<StyledSubGrid item>
<BoardRevision rvrToy={props.rvrToy}>xs=3</BoardRevision>
</StyledSubGrid>
</Grid>
<Grid container direction='row' alignItems='center'>
<StyledSubGrid item>
<MACAddress rvrToy={props.rvrToy}>xs=3</MACAddress>
</StyledSubGrid>
<StyledSubGrid item>
<SKU rvrToy={props.rvrToy}>xs=3</SKU>
</StyledSubGrid>
<StyledSubGrid item>
<BluetoothAdvertisingName
rvrToy={props.rvrToy}
></BluetoothAdvertisingName>
</StyledSubGrid>
</Grid>
</Grid>
</SystemInformationPaper>
</React.Fragment>
);
};
export default SystemInformation;
We've finally worked our way back to the top and can add our SystemInformation component to our PageBody component! You'll, again, notice that we are using a Grid system and have already taken the liberty of making the Grid containing the SystermInformation component into a "row-oriented container", as we'll want to add more elements next to the System Information (specifically the Power Information in the next section):
PageBody.js (with System Information)
import React from 'react';
import { Paper, Grid } from '@material-ui/core';
import { styled } from '@material-ui/styles';
import SystemInformation from './systemInformation/SystemInformation';
const BodyContent = styled('div')({
flexGrow: 1,
backgroundColor: 'aliceblue',
padding: '20px'
});
const PageRow = styled(Paper)({
margin: '20px auto 0px auto',
background: 'transparent',
boxShadow: 'none'
});
const TopRow = styled(PageRow)({
marginTop: '50px'
});
const PageBody = props => {
return (
<BodyContent>
<Grid container>
<Grid container direction='row' alignItems='center'>
<TopRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<SystemInformation
rvrToy={props.rvrToy}
xs={7}
></SystemInformation>
</Grid>
</Grid>
</TopRow>
</Grid>
</Grid>
</BodyContent>
);
};
export default PageBody;
Power Info
The second section of our Dashboard displays "power information" about the current state of the battery, in terms of percentage, voltage and an arbitrary "state". Because this information needs to update in real time, we will be accessing it slightly differently than we did the system information.
Let's use the "Battery Percentage" as an example; we can get the Battery Percentage from the Nordic Chip (which has a permanent address of "1") using the getBatteryPercentage command. In order to have the battery percentage stay up to date, we call the getBatteryPercentage command on an interval, in our updateBatteryPercentage method (but only frequently enough to stay up to date, not so frequently as to drain the battery unnecessarily or clog up the communication channels).
BatteryPercentage.js
import React from 'react';
import { Card, CardContent, Typography } from '@material-ui/core';
import { styled } from '@material-ui/styles';
const BatteryPercentageCard = styled(Card)({
padding: '10px',
textAlign: 'center',
color: 'dark-gray',
width: '250px'
});
const Title = styled(Typography)({
fontSize: '20px'
});
class BatteryPercentage extends React.Component {
constructor(props) {
super(props);
this.state = { data: '' };
}
componentDidMount() {
const { rvrToy } = this.props;
rvrToy.getBatteryPercentage(1).then(data => {
this.setState({
data: JSON.parse(data)
});
});
this.updateBatteryPercent();
}
updateBatteryPercent = () => {
const interval = setInterval(() => {
this.props.rvrToy.getBatteryPercentage(1).then(data => {
this.setState({
data: JSON.parse(data)
});
});
}, 900000);
return () => clearInterval(interval);
};
render() {
return (
<BatteryPercentageCard>
<CardContent>
<Title>Battery Percentage</Title>
{this.state.data && (
<Typography>{this.state.data.percentage}%</Typography>
)}
</CardContent>
</BatteryPercentageCard>
);
}
}
export default BatteryPercentage;
Just like with the System Info, each of the "Power" pieces of information is contained on its own Card and all of those Cards are contained on a larger Paper (you can see the code for each of the other Power Info components in our GitHub Repo). We pass rvrToy into each of our components (Cards) via props, as the commands to retrieve battery information are all called directly on the SpheroRvrToy object (which we created an instance of in App.js, called rvrToy, to use across this application). You'll also notice that we are using a Grid to organize all of the elements within the Paper, creating the rows within by setting the containing Grid to a type of "container" with a direction of "row":
Power.js
import React from 'react';
import { Paper, Grid, Typography } from '@material-ui/core';
import BatteryPercentage from './BatteryPercentage';
import BatteryVoltage from './BatteryVoltage';
import VoltageState from './VoltageState';
import { styled } from '@material-ui/styles';
const PowerPaper = styled(Paper)({
padding: '10px',
margin: '20px',
color: 'dark-gray',
height: '415px'
});
const Title = styled(Typography)({
fontSize: '24px'
});
const StyledSubGrid = styled(Grid)({
margin: '30px 30px'
});
const VersionDetails = props => {
return (
<React.Fragment>
<PowerPaper>
<Title gutterBottom>Power Info</Title>
<Grid>
<Grid container direction='row' alignItems='center'>
<StyledSubGrid item>
<BatteryPercentage rvrToy={props.rvrToy}>xs=3</BatteryPercentage>
</StyledSubGrid>
<StyledSubGrid item>
<BatteryVoltage rvrToy={props.rvrToy}>xs=3</BatteryVoltage>
</StyledSubGrid>
</Grid>
<Grid container direction='row' alignItems='center'>
<StyledSubGrid item>
<VoltageState rvrToy={props.rvrToy}>xs=3</VoltageState>
</StyledSubGrid>
<StyledSubGrid item></StyledSubGrid>
</Grid>
</Grid>
</PowerPaper>
</React.Fragment>
);
};
export default VersionDetails;
We've finally worked our way back to the top and can add our Power Information component to our PageBody component! You'll, again, notice that we are using a Grid system and have a Grid containing both the SystemInformation component and the Power component in a "row-oriented container", so that they are at the same vertical level on the page:
PageBody.js (with Power Information)
import React from 'react';
import { Paper, Grid } from '@material-ui/core';
import { styled } from '@material-ui/styles';
import Power from './power/Power';
import SystemInformation from './systemInformation/SystemInformation';
const BodyContent = styled('div')({
flexGrow: 1,
backgroundColor: 'aliceblue',
padding: '20px'
});
const PageRow = styled(Paper)({
margin: '20px auto 0px auto',
background: 'transparent',
boxShadow: 'none'
});
const TopRow = styled(PageRow)({
marginTop: '50px'
});
const PageBody = props => {
return (
<BodyContent>
<Grid container>
<Grid container direction='row' alignItems='center'>
<TopRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<SystemInformation
rvrToy={props.rvrToy}
xs={7}
></SystemInformation>
</Grid>
<Grid>
<Power rvrToy={props.rvrToy} xs={5}></Power>
</Grid>
</Grid>
</TopRow>
</Grid>
</Grid>
</BodyContent>
);
};
export default PageBody;
Sensor Streaming
The Sensor Streaming Graphs are another element that updates in real-time! In this case, each graph will be its own component that enables the corresponding sensor for its graph, and the Paper where all of the graphs are contained is where we'll start streaming all of the enabled sensors, at once. Let's start by just looking at the AmbientLightChart component. In our componentDidMount lifecycle event, we enable the ammbientLight sensor and say "when the sensor is enabled, update the state to add another element to our array of ambient light values that have been streamed, then, re-render the chart so the new point is included":
AmbientLightChart.js
import React from 'react';
import c3 from 'c3';
class AmbientLightChart extends React.Component {
constructor(props) {
super(props);
this.state = { ambientLight: ['Ambient Light Value'] };
}
renderChart() {
c3.generate({
bindto: '#ambientLight',
data: {
columns: [this.state.ambientLight],
type: 'scatter'
},
point: {
r: 10
},
axis: {
y: {
max: Math.max(this.state.ambientLight) + 5,
min: Math.min(this.state.ambientLight) - 5
},
x: { show: true }
}
});
}
async componentDidMount() {
const { sensorControl } = this.props;
sensorControl.enableSensor(sensorControl.ambientLight, data => {
this.setState({
ambientLight: this.state.ambientLight.concat(data.Light)
}, this.renderChart());
});
}
render() {
return (
<div>
<div id='ambientLight'></div>
</div>
);
}
}
export default AmbientLightChart;
We'll make a component just like that one for each of the sensors that we would like to stream data from (you can see the code for each of the other Sensor Streaming components in our GitHub Repo); each of those components can then be used in our SensorStreaming component! Just like we mentioned earlier, in the componentDidMount in our SensorStreaming component is where we'll actually start streaming all the sensors (and stop them). This SensorStreaming component looks a little different than the other ones we've built thus far in that each graph is in its own Tab that can be flipped through, rather than them being on multiple Cards that are all displayed at the same time. Additionally, because each of the streaming services is called on the "sensor control" property of the rvrToy object (which we will feed into this SensorStreaming object as simply "sensorControl", next, in the PageBody component), we feed sensorControl (rather than rvrToy) into each of the streaming graph components:
SensorStreaming.js
import React from 'react';
import PropTypes from 'prop-types';
import { Tabs, Tab, Typography, Box, AppBar } from '@material-ui/core';
import { styled } from '@material-ui/styles';
import AmbientLightChart from './AmbientLightChart';
import AttitudeChart from './AttitudeChart';
import AccelerometerChart from './AccelerometerChart';
import GyroscopeChart from './GyroscopeChart';
import VelocityChart from './VelocityChart';
import SpeedChart from './SpeedChart';
import LocatorChart from './LocatorChart';
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<Typography
component='div'
role='tabpanel'
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
<Box p={3}>{children}</Box>
</Typography>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.any.isRequired,
value: PropTypes.any.isRequired
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`
};
}
const GraphsContainer = styled('div')({
flexGrow: 1,
backgroundColor: '#fff',
width: '1350px'
});
const TabsBar = styled(AppBar)({
backgroundColor: '#4200b7'
});
class SensorStreaming extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 0
};
}
handleChange = (event, newValue) => {
this.setState({ value: newValue });
};
componentDidMount() {
const { sensorControl } = this.props;
if (!sensorControl.isStreaming) {
sensorControl.startSensorStreaming(1000);
}
setTimeout(() => {
sensorControl.clearSensorStreaming();
}, 60000);
}
render() {
const { value } = this.state;
const { sensorControl } = this.props;
return (
<GraphsContainer>
<TabsBar position='static'>
<Tabs
value={value}
onChange={this.handleChange}
aria-label='simple tabs example'
>
<Tab label='Ambient Light' {...a11yProps(0)} />
<Tab label='IMU' {...a11yProps(1)} />
<Tab label='Accelerometer' {...a11yProps(2)} />
<Tab label='Gyroscope' {...a11yProps(3)} />
<Tab label='Locator' {...a11yProps(4)} />
<Tab label='Velocity' {...a11yProps(5)} />
<Tab label='Speed' {...a11yProps(6)} />
</Tabs>
</TabsBar>
<TabPanel value={value} index={0}>
<AmbientLightChart
sensorControl={sensorControl}
></AmbientLightChart>
</TabPanel>
<TabPanel value={value} index={1}>
<AttitudeChart
sensorControl={sensorControl}
></AttitudeChart>
</TabPanel>
<TabPanel value={value} index={2}>
<AccelerometerChart
sensorControl={sensorControl}
></AccelerometerChart>
</TabPanel>
<TabPanel value={value} index={3}>
<GyroscopeChart
sensorControl={sensorControl}
></GyroscopeChart>
</TabPanel>
<TabPanel value={value} index={4}>
<LocatorChart sensorControl={sensorControl}></LocatorChart>
</TabPanel>
<TabPanel value={value} index={5}>
<VelocityChart
sensorControl={sensorControl}
></VelocityChart>
</TabPanel>
<TabPanel value={value} index={6}>
<SpeedChart sensorControl={sensorControl}></SpeedChart>
</TabPanel>
</GraphsContainer>
);
}
}
export default SensorStreaming;
Now we get to the fun part, where we add another row to the page body! This is where our Grid really starts to take shape. Again, we'll make our row's Grid into a "row-oriented container". As we mentioned above, for Sensor Streaming, we only need the sensors part of RVR+/RVR. So that we are not having to type "props.rvrToy.getSensorControl().thingYouWant" every time we do something with the sensors, we just pass a "sensorControl" prop into our SensorStreaming component (which is equal to rvrToy.getSensorControl()):
PageBody.js (with Sensor Streaming)
import React from 'react';
import { Paper, Grid } from '@material-ui/core';
import { styled } from '@material-ui/styles';
import Power from './power/Power';
import SensorStreaming from './sensorStreaming/SensorStreaming';
import SystemInformation from './systemInformation/SystemInformation';
const BodyContent = styled('div')({
flexGrow: 1,
backgroundColor: 'aliceblue',
padding: '20px'
});
const PageRow = styled(Paper)({
margin: '20px auto 0px auto',
background: 'transparent',
boxShadow: 'none'
});
const TopRow = styled(PageRow)({
marginTop: '50px'
});
const MainPaper = styled(Paper)({
padding: '10px',
marginRight: '50px',
textAlign: 'center'
});
const PageBody = props => {
return (
<BodyContent>
<Grid container>
<Grid container direction='row' alignItems='center'>
<TopRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<SystemInformation
rvrToy={props.rvrToy}
xs={7}
></SystemInformation>
</Grid>
<Grid>
<Power rvrToy={props.rvrToy} xs={5}></Power>
</Grid>
</Grid>
</TopRow>
</Grid>
<Grid container direction='row' alignItems='center'>
<PageRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<MainPaper>
<SensorStreaming
sensorControl={props.rvrToy.getSensorControl()}
></SensorStreaming>
</MainPaper>
</Grid>
</Grid>
</PageRow>
</Grid>
</Grid>
</BodyContent>
);
};
export default PageBody;
Sleep/Wake Buttons
Our Sleep and Wake buttons make it super easy for you to send a command to the RVR+/RVR to either wake it up so that you can ask RVR+/RVR to do things (like drive) after it has put itself to sleep after being inactive (remember how we talked about, if you aren't seeing the data you would expect to, you need to wake your RVR+/RVR up?? This button will make doing that a lot easier) OR put RVR+/RVR to sleep, so that you can save battery while you are tinkering with your code! Let's take a peek at how our WakeUpButton is formatted....
As we mentioned, in order to make requests of RVR+/RVR to do anything (drive, change lights, etc), the RVR+/RVR must be powered on and not in soft- or deep sleep. This component is just a Material-UI Button that we've styled and attached the Sphero wake() method to:
WakeUpButton.js
import React from 'react';
import Button from '@material-ui/core/Button';
import { styled } from '@material-ui/styles';
const WakeButton = styled(Button)({
margin: '10px',
borderRadius: '100px',
borderWidth: '5px',
fontSize: '20px',
padding: '20px 25px',
borderColor: '#4200b7',
color: '#4200b7',
textAlign: 'center'
});
const WakeUpButton = props => {
function wakeUpRvr() {
props.rvrToy.wake();
}
return (
<WakeButton variant='outlined' onClick={wakeUpRvr}>
WAKE
<br />
UP!
</WakeButton>
);
};
export default WakeUpButton;
Once we have both our Wake and Sleep buttons ready, we'll group them on their own Paper in our SleepWakeButtons component. We place the buttons on a Paper to organize them, but we don't actually want to see the Paper containing them, so we make the background transparent in the SleepWakeSection styled component. Putting the components on a Paper also allows us to put them in a Grid, so we can stack and space them the way we'd like:
SleepWakeButton.js
import React from 'react';
import WakeUpButton from './control/WakeUpButton';
import NightNightButton from './control/NightNightButton';
import { Paper, Card, Grid } from '@material-ui/core';
import { styled } from '@material-ui/styles';
const SleepWakeSection = styled(Paper)({
padding: '10px',
background: 'transparent',
boxShadow: 'none'
});
const ButtonCard = styled(Card)({
margin: '15px',
padding: '10px',
textAlign: 'center'
});
const SleepWakeButtons = props => {
return (
<React.Fragment>
<SleepWakeSection>
<Grid container direction='column' alignItems='center'>
<Grid item xs>
<ButtonCard>
<WakeUpButton rvrToy={props.rvrToy}></WakeUpButton>
</ButtonCard>
</Grid>
<Grid item xs>
<ButtonCard>
<NightNightButton rvrToy={props.rvrToy}></NightNightButton>
</ButtonCard>
</Grid>
</Grid>
</SleepWakeSection>
</React.Fragment>
);
};
export default SleepWakeButtons;
Since our buttons don't take up much space, we can add our SleepWakeButton component to the PageBody in the same row as the SensorStreaming component. Because the sleep and wake methods are called directly on the SpheroRvrToy object, we can just pass rvrToy as our prop for our SleepWakeButton component:
PageBody.js (with Wake and Sleep Buttons)
import React from 'react';
import { Paper, Grid } from '@material-ui/core';
import { styled } from '@material-ui/styles';
import Power from './power/Power';
import SensorStreaming from './sensorStreaming/SensorStreaming';
import SleepWakeButtons from './SleepWakeButtons';
import SystemInformation from './systemInformation/SystemInformation';
const BodyContent = styled('div')({
flexGrow: 1,
backgroundColor: 'aliceblue',
padding: '20px'
});
const PageRow = styled(Paper)({
margin: '20px auto 0px auto',
background: 'transparent',
boxShadow: 'none'
});
const TopRow = styled(PageRow)({
marginTop: '50px'
});
const MainPaper = styled(Paper)({
padding: '10px',
marginRight: '50px',
textAlign: 'center'
});
const PageBody = props => {
return (
<BodyContent>
<Grid container>
<Grid container direction='row' alignItems='center'>
<TopRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<SystemInformation
rvrToy={props.rvrToy}
xs={7}
></SystemInformation>
</Grid>
<Grid>
<Power rvrToy={props.rvrToy} xs={5}></Power>
</Grid>
</Grid>
</TopRow>
</Grid>
<Grid container direction='row' alignItems='center'>
<PageRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<MainPaper>
<SensorStreaming
sensorControl={props.rvrToy.getSensorControl()}
></SensorStreaming>
</MainPaper>
</Grid>
<Grid>
<SleepWakeButtons rvrToy={props.rvrToy}></SleepWakeButtons>
</Grid>
</Grid>
</PageRow>
</Grid>
</Grid>
</BodyContent>
);
};
export default PageBody;
Color Wheel
Our "Color Wheel" allows you to select LEDs that you would like to change the color of and then a color you would like to change them to. We used SketchPicker from react-color to create our color picker, but you are welcome to choose another tool and implement it here. When a new color is selected by the user, we start by grabbing the RGB values for that color and checking for which LEDs are selected to be changed. We have two different methods we can use: 1) setAllLedsRgb, which we'll use if all or none of the LED boxes are checked and 2) setMultipleLedsRgb, which allows us to set the color of some, but not all of the LEDs:
colorWheel.js
import React from 'react';
import {
Card,
CardContent,
Checkbox,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
Grid,
Paper,
Typography,
styled
} from '@material-ui/core';
import { SketchPicker } from 'react-color';
const ColorfulPaper = styled(Paper)({
margin: '20px',
padding: '10px'
});
const Title = styled(Typography)({
fontSize: '24px'
});
const ColorfulFormBase = styled(FormControl)({
margin: '20px'
});
const SketchCard = styled(Card)({
margin: '15px 20px 25px 20px'
});
const colorHexToRgbArray = colorHex => {
let redHex = colorHex.substr(1).slice(0, 2);
let greenHex = colorHex.substr(1).slice(2, 4);
let blueHex = colorHex.substr(1).slice(4, 6);
return {
red: parseInt(redHex, 16),
green: parseInt(greenHex, 16),
blue: parseInt(blueHex, 16)
};
};
const initialColor = { hex: '#1B3C80' };
class ColorWheel extends React.Component {
constructor(props) {
super(props);
this.state = {
headlightLeft: true,
headlightRight: true,
brakelightLeft: true,
brakelightRight: true,
batteryDoorFront: true,
batteryDoorRear: true,
powerButtonFront: true,
powerButtonRear: true,
statusIndicationLeft: true,
statusIndicationRight: true
};
}
handleCheck = name => event => {
this.setState({ ...this.state, [name]: event.target.checked });
};
handleColorChange = ({ hex }) => {
let rgbValues = colorHexToRgbArray(hex);
let ledGroups = Object.keys(this.state).filter(key => this.state[key] === true);
if (ledGroups.length === 0 || ledGroups.length === 10) {
this.props.ledControl.setAllLedsRgb(
rgbValues.red,
rgbValues.green,
rgbValues.blue
);
} else {
this.props.ledControl.setMultipleLedsRgb(
ledGroups,
rgbValues.red,
rgbValues.green,
rgbValues.blue
);
}
};
componentDidUpdate() {
this.handleColorChange(initialColor);
}
render() {
const {
headlightLeft,
headlightRight,
brakelightLeft,
brakelightRight,
batteryDoorFront,
batteryDoorRear,
powerButtonFront,
powerButtonRear,
statusIndicationLeft,
statusIndicationRight
} = this.state;
return (
<ColorfulPaper>
<Title gutterBottom>Change LED Colors</Title>
<Grid container direction='row' alignItems='center'>
<Grid>
<SketchCard>
<CardContent>
<SketchPicker
color={initialColor.hex}
onChangeComplete={this.handleColorChange}
/>
</CardContent>
</SketchCard>
</Grid>
<Grid>
<ColorfulFormBase component='fieldset'>
<FormLabel component='legend'>Front and Back LEDs</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={headlightLeft}
onChange={this.handleCheck('headlightLeft')}
value='headlightLeft'
/>
}
label='Left Headlight'
/>
<FormControlLabel
control={
<Checkbox
checked={headlightRight}
onChange={this.handleCheck('headlightRight')}
value='headlightRight'
/>
}
label='Right Headlight'
/>
<FormControlLabel
control={
<Checkbox
checked={brakelightLeft}
onChange={this.handleCheck('brakelightLeft')}
value='brakelightLeft'
/>
}
label='Left Brake Light'
/>
<FormControlLabel
control={
<Checkbox
checked={brakelightRight}
onChange={this.handleCheck('brakelightRight')}
value='brakelightRight'
/>
}
label='Right Brake Light'
/>
</FormGroup>
</ColorfulFormBase>
</Grid>
<Grid>
<ColorfulFormBase component='fieldset'>
<FormLabel component='legend'>Side LEDs</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={batteryDoorFront}
onChange={this.handleCheck('batteryDoorFront')}
value='batteryDoorFront'
/>
}
label='Battery Door Front'
/>
<FormControlLabel
control={
<Checkbox
checked={batteryDoorRear}
onChange={this.handleCheck('batteryDoorRear')}
value='batteryDoorRear'
/>
}
label='Battery Door Back'
/>
<FormControlLabel
control={
<Checkbox
checked={powerButtonFront}
onChange={this.handleCheck('powerButtonFront')}
value='powerButtonFront'
/>
}
label='Power Front'
/>
<FormControlLabel
control={
<Checkbox
checked={powerButtonRear}
onChange={this.handleCheck('powerButtonRear')}
value='powerButtonRear'
/>
}
label='Power Back'
/>
</FormGroup>
</ColorfulFormBase>
</Grid>
<Grid>
<ColorfulFormBase component='fieldset'>
<FormLabel component='legend'>Internal LEDs</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={statusIndicationLeft}
onChange={this.handleCheck('statusIndicationLeft')}
value='statusIndicationLeft'
/>
}
label='Interior Left'
/>
<FormControlLabel
control={
<Checkbox
checked={statusIndicationRight}
onChange={this.handleCheck('statusIndicationRight')}
value='statusIndicationRight'
/>
}
label='Interior Right'
/>
</FormGroup>
</ColorfulFormBase>
</Grid>
</Grid>
</ColorfulPaper>
);
}
}
export default ColorWheel;
The ColorWheel is, itself the "containing Paper" and the component that directly communicates with RVR+/RVR, so we can just load ColorWheel into PageBody! In doing so, we'll create a third and final row on the page, which will contain the Color Wheel and the Joystick (coming up next!):
PageBody.js (with LED Color Changer)
import React from 'react';
import { Paper, Grid } from '@material-ui/core';
import { styled } from '@material-ui/styles';
import ColorWheel from './colorWheel/colorWheel';
import Power from './power/Power';
import SensorStreaming from './sensorStreaming/SensorStreaming';
import SleepWakeButtons from './SleepWakeButtons';
import SystemInformation from './systemInformation/SystemInformation';
const BodyContent = styled('div')({
flexGrow: 1,
backgroundColor: 'aliceblue',
padding: '20px'
});
const PageRow = styled(Paper)({
margin: '20px auto 0px auto',
background: 'transparent',
boxShadow: 'none'
});
const TopRow = styled(PageRow)({
marginTop: '50px'
});
const MainPaper = styled(Paper)({
padding: '10px',
marginRight: '50px',
textAlign: 'center'
});
const PageBody = props => {
return (
<BodyContent>
<Grid container>
<Grid container direction='row' alignItems='center'>
<TopRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<SystemInformation
rvrToy={props.rvrToy}
xs={7}
></SystemInformation>
</Grid>
<Grid>
<Power rvrToy={props.rvrToy} xs={5}></Power>
</Grid>
</Grid>
</TopRow>
</Grid>
<Grid container direction='row' alignItems='center'>
<PageRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<MainPaper>
<SensorStreaming
sensorControl={props.rvrToy.getSensorControl()}
></SensorStreaming>
</MainPaper>
</Grid>
<Grid>
<SleepWakeButtons rvrToy={props.rvrToy}></SleepWakeButtons>
</Grid>
</Grid>
</PageRow>
</Grid>
<Grid container direction='row' alignItems='center'>
<PageRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<ColorWheel
ledControl={props.rvrToy.getLedControl()}
></ColorWheel>
</Grid>
</Grid>
</PageRow>
</Grid>
</Grid>
</BodyContent>
);
};
export default PageBody;
Joystick
The final component we need to add to our dashboard is a joystick to steer our RVR+/RVR. We'll use the driveWithHeading command, which takes in a speed you'd like to drive at and a direction (angle between 0 and 360) that you'd like to drive in. There is also an optional third parameter that allows you to drive the RVR+/RVR in reverse, with a value of "1" (which we use for our "Back it Up" button):
RVRJoystick.js
import React from 'react';
import { Joystick } from 'react-joystick-component';
import { Paper, Typography, Button, Grid } from '@material-ui/core';
import { styled } from '@material-ui/styles';
/**
* Calculates the degree given coordinates x and y
* @param x horizontal distance from origin; x-coordinate on unit circle; -1 <= x <= 1
* @param y vertical distance from origin; y-coordinate on unit circle; -1 <= y <= 1
* @returns {*} degree of line that has x and y as coordinates with positive half of x-axis
*/
function getTheta(x, y) {
console.assert(x >= -1 && x <= 1, 'Argument x is out of range');
console.assert(y >= -1 && y <= 1, 'Argument y is out of range');
// Transposing x, and y values in Atan2 to get angle that starts at 0 on the positive y axis, and increases clockwise.
let radians = Math.atan2(x, y);
let theta = (radians * 180) / Math.PI;
// Wrap degrees if they cross over -180/180 threshold on the negative y axis.
if (theta < 0) {
theta += 360;
}
console.assert(theta >= 0 && x <= 359, 'Return value theta is out of range');
return theta;
}
/**
* Normalizes a value to be on a new scale defined by newMin and newMax.
* Example: normalize(10, 5, 15, 0, 1) -> 0.5
*/
function normalize(value, min, max, newMin, newMax) {
return ((value - min) / (max - min)) * (newMax - newMin) + newMin;
}
const DrivePaper = styled(Paper)({
margin: '20px',
padding: '10px'
});
const Title = styled(Typography)({
fontSize: '24px'
});
const JoystickPaper = styled(Paper)({
padding: '30px 30px',
textAlign: 'center',
background: 'transparent',
boxShadow: 'none'
});
const JoystickButton = styled(Button)({
backgroundColor: '#4200b7',
color: '#fff',
padding: '10px 20px'
});
const SpeedPaper = styled(Paper)({
marginLeft: '30px',
background: 'transparent',
boxShadow: 'none',
top: '0px'
});
const SpeedButton = styled(Button)({
backgroundColor: '#4200b7',
color: '#fff',
padding: '10px 20px',
margin: '10px 20px'
});
const PRNDLButton = styled(SpeedButton)({
margin: '100px 20px 0px 20px'
});
class RVRJoystick extends React.Component {
constructor(props) {
super(props);
this.state = {
direction: 'Stopped',
theta: 0,
radius: 0,
speed: 0,
maxSpeed: 90,
backward: false
};
}
handleMove = data => {
const { rvrToy } = this.props;
this.setState({
direction: data.direction,
theta: getTheta(
normalize(data.x, -100, 100, -1, 1),
normalize(data.y, -100, 100, -1, 1)
),
radius: Math.sqrt(
Math.pow(Math.abs(data.x), 2) + Math.pow(Math.abs(data.y), 2)
),
speed: normalize(this.state.radius, 0, 142, 0, this.state.maxSpeed)
});
if (!this.state.backward) {
rvrToy.driveWithHeading(this.state.speed, this.state.theta);
} else {
rvrToy.driveWithHeading(this.state.speed, this.state.theta, 1);
}
};
handleStop = data => {
this.setState({
direction: 'Stopped',
radius: Math.sqrt(
Math.pow(Math.abs(data.x), 2) + Math.pow(Math.abs(data.y), 2)
),
speed: 0
});
this.props.driveControl.rollStop(this.state.theta);
};
resetAim = () => {
const { driveControl } = this.props;
driveControl.resetHeading();
};
setMaxSpeed = speed => {
this.setState({
maxSpeed: speed
});
};
setPRNDL = () => {
this.setState({
backward: this.state.backward == false ? true : false
});
};
render() {
return (
<DrivePaper>
<Title gutterBottom>Drive</Title>
<Grid container direction='row' alignItems='center'>
<Grid>
<JoystickPaper>
<Joystick
move={this.handleMove}
stop={this.handleStop}
size={200}
throttle={300}
stickColor='#4200b7'
baseColor='#8512da'
style={{ margin: '50px' }}
/>
<p>{this.state.direction}</p>
<br />
<br />
<JoystickButton onClick={this.resetAim}>
Reset Aim!
</JoystickButton>
</JoystickPaper>
</Grid>
<Grid>
<SpeedPaper>
<Grid container direction='column' alignItems='center'>
<Grid item xs>
<SpeedButton onClick={() => this.setMaxSpeed(45)}>
Slow
</SpeedButton>
</Grid>
<Grid item xs>
<SpeedButton onClick={() => this.setMaxSpeed(90)}>
Normal
</SpeedButton>
</Grid>
<Grid item xs>
<SpeedButton onClick={() => this.setMaxSpeed(128)}>
Fast
</SpeedButton>
</Grid>
<Grid item xs>
<PRNDLButton onClick={this.setPRNDL}>
{this.state.backward ? 'Go Forward' : 'Back It Up'}
</PRNDLButton>
</Grid>
</Grid>
</SpeedPaper>
</Grid>
</Grid>
</DrivePaper>
);
}
}
export default RVRJoystick;
Once we complete the Joystick component, we can add it to our PageBody in the same row as the ColorWheel. Because we only need the drive element of rvrToy, we'll feed in a driveControl prop that is equal to rvrToy.getDriveControl(), rather than having to call getDriveControl on the rvrToy element for every command in the Joystick component:
PageBody.js (with Joystick)
import React from 'react';
import { Paper, Grid } from '@material-ui/core';
import { styled } from '@material-ui/styles';
import ColorWheel from './colorWheel/colorWheel';
import Power from './power/Power';
import RVRJoystick from './control/RVRJoystick';
import SensorStreaming from './sensorStreaming/SensorStreaming';
import SleepWakeButtons from './SleepWakeButtons';
import SystemInformation from './systemInformation/SystemInformation';
const BodyContent = styled('div')({
flexGrow: 1,
backgroundColor: 'aliceblue',
padding: '20px'
});
const PageRow = styled(Paper)({
margin: '20px auto 0px auto',
background: 'transparent',
boxShadow: 'none'
});
const TopRow = styled(PageRow)({
marginTop: '50px'
});
const MainPaper = styled(Paper)({
padding: '10px',
marginRight: '50px',
textAlign: 'center'
});
const PageBody = props => {
return (
<BodyContent>
<Grid container>
<Grid container direction='row' alignItems='center'>
<TopRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<SystemInformation
rvrToy={props.rvrToy}
xs={7}
></SystemInformation>
</Grid>
<Grid>
<Power rvrToy={props.rvrToy} xs={5}></Power>
</Grid>
</Grid>
</TopRow>
</Grid>
<Grid container direction='row' alignItems='center'>
<PageRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<MainPaper>
<SensorStreaming
sensorControl={props.rvrToy.getSensorControl()}
></SensorStreaming>
</MainPaper>
</Grid>
<Grid>
<SleepWakeButtons rvrToy={props.rvrToy}></SleepWakeButtons>
</Grid>
</Grid>
</PageRow>
</Grid>
<Grid container direction='row' alignItems='center'>
<PageRow>
<Grid container direction='row' alignItems='center'>
<Grid>
<ColorWheel
ledControl={props.rvrToy.getLedControl()}
></ColorWheel>
</Grid>
<Grid>
<RVRJoystick
rvrToy={props.rvrToy}
driveControl={props.rvrToy.getDriveControl()}
></RVRJoystick>
</Grid>
</Grid>
</PageRow>
</Grid>
</Grid>
</BodyContent>
);
};
export default PageBody;
That's All, Folks!
Hopefully, this little runthrough of how to create your own dashboard to monitor and control your RVR+/RVR has given you a few ideas as to what YOU can accomplish with our Node and Client SDKs! Feel free to spruce up this example with some different libraries, or styles or even extra components!