# Web Interface To Control The Rover
Over the past few weeks I have been building a raspberry pi zero w based rover. This post follows on from the previous posts which you can checkout below.
- Pi Zero W Rover Setup
- Customising Raspberry Pi Images with Github and Travis
- Using Rust to Control a Raspberry Pi Zero W Rover
- Small Refactor To Prepare For Writing The Rest API
- Writing A Rest API For The Pi Rover
In this post we will look at creating a web ui for the rover. It will make use of the rest api we developed in the last post to give us a nice way to interactively control the rover. Rather then keeping this to a bare bones example we will be building the start of a modern frontend web application that will act as a base we can build upon later. For this we are going to look at and tie together a few different frontend technologies such as vue.js, webpack and concise.css and others.
# Setting Up The Dev Environment And Project
Modern web development makes heavy use of node.js, especially npm, nodes package manager. This now include browser side code/dependencies in addition to server side node was originally written for. In our project the server side code is written in rust but we will still make use of npm for installing and managing our browser side dependencies in addition to running some tools useful in development.
Node.js is the first component we need to install as npm and many other tools
are based off it. Head over to the node.js install
guide and follow the instructions for your
system. Once done you should have both nodejs
and npm
installed, verify this
with the following.
node --version
#v7.7.2
npm --version
#4.4.1
The only other tool we will need is vue-cli
which can be install globally with
npm
by running the following.
npm install -g vue-cli
Our front end code is going to live inside the ui
subdirectory of the root of
our project. vue-cli
can be used to create this directory with all of the
boiler plate code needed by vue.js and webpack as well as many other niceties we
can make use of later.
vue init webpack ui
#
# This will install Vue 2.x version of the template.
#
# For Vue 1.x use: vue init webpack#1.0 ui
#
#? Project name ui
#? Project description Web application for a raspberry pi based rover
#? Author Michael Daffin <michael@daffin.io>
#? Vue build standalone
#? Install vue-router? Yes
#? Use ESLint to lint your code? Yes
#? Pick an ESLint preset AirBNB
#? Setup unit tests with Karma + Mocha? Yes
#? Setup e2e tests with Nightwatch? Yes
#
# vue-cli · Generated "ui".
#
# To get started:
#
# cd ui
# npm install
# npm run dev
#
# Documentation can be found at https://vuejs-templates.github.io/webpack
You can read more about vue-cli here and vue in general here.
We can start the dev server by running the following.
cd ui
npm install
npm run dev
This will start a web server and launch a web browser to display our application. This is an incredibly useful tool during development as it will automatically build run/reload a web browser whenever we change anything allowing you to instantly see the change you have made. You can leave this running in the background as we develop the site.
# Overview Of The Project
/build/
: Contains all the build scripts and webpack related configs./config/
: Contains our applications config./node_modules/
: Vendored packages and tools that we depend on, this is populated when you runnpm install
./static/
: Any static files we want to serve./test/
: Unit and integrations tests./src/
: Our applications code./src/main.js
: The applications entry point./src/App.vue
: The main vue component that will be the root of all other components./src/components/
: Any other components we require./src/router/
: The application router, it decides which paths use which components.
# Extra Dependencies
The ui has already been configured with a hole bunch of core dependencies but our application will require a few extra ones.
- Concise CSS is a simple css framework that give us a nice looking base theme to start from. It is similar to bootstrap but light weight, simpler and pure css.
- Vue Resources is a http client for the vue framework. It is what we will use to make http calls to our backend and handle the responses.
These dependencies can be installed and added tour ui/project.json
by
running.
npm install --save concise.css
npm install --save concise-ui
npm install --save vue-resource
vue-cli
has configured webpack to automatically build (ie concatenate and
minify) css files for us so all we need to do to is to import them in
ui/src/main.js
and webpack will find and process them along with the css from
the vue components into a single resource when we build the project.
We must also register vue-resource
with vue and set some configuration
options.
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue';
+import VueResource from 'vue-resource';
import App from './App';
import router from './router';
+import '../node_modules/concise.css/dist/concise.css';
+import '../node_modules/concise-ui/dist/concise-ui.css';
+
Vue.config.productionTip = false;
+Vue.use(VueResource);
+Vue.http.options.xhr = { withCredentials: true };
/* eslint-disable no-new */
new Vue({
# Configuration
The config
directory allows us to set different variables for different
environments. We can make use of this to set different api urls for dev and
production.
The development environment can be configured with ui/config/dev.env.js
. We
add a variable for the base url of our api and default it to the hostname of the
rover. But if zero-conf/avahi is not available in your environment replace this
with the ip address of the rover.
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
+ BASE_URL: '"http://rpizw-rover.local:3000"',
NODE_ENV: '"development"'
})
The production environment can be configured with ui/config/prod.env.js
. Like
above we also add the base url of our api but set it to an empty string instead.
This will tell it to use a relative url so we do not need to know how the user
initially connected to our rover.
module.exports = {
+ BASE_URL: '""',
NODE_ENV: '"production"'
}
Note the double quoted expressions, this is required by the DefinePlugin
from
webpack that is setting up these values. You can look inside the build directory
for exactly how this is setup if you are interested.
# The Controls Component
This will be our only functional component for now. All of its logic and styles
will live in ui/src/components/Controls.vue
and like all vue components it has
three main sections.
# Template
The template section contains the templated html code for our component.
If a call to the api, or anything else, goes wrong we want to alert the user to
this with a nice error message. We do this by creating an alert-box element
(part of concise-ui) that is visible only when the variable errorMessage
is
not false which displays the contents of that variable. This allows us to show
the error simply by setting that variable when something goes wrong. We hide it
by adding a close button that simply sets the variable to false
.
<template>
<main>
<section class="alert-box -error" v-if="errorMessage !== false">
<p>{{ errorMessage }}</p>
<a class="close" @click="errorMessage = false" href="#">×</a>
</section>
The rest of our template describes the actual controls we want. First, two range
sliders to show/set the current speed and direction of both the left and right
servos. These values are bound to the left
and right
variables on our
component which we will use to send the desired speeds to the rover later. We
have also adjusted the range from -10 (reverse) to 10 (forward) down from -100
to 100 as we do not need the full precision in the ui and it makes it slightly
nicer to set exact speeds, these will be multiplied by 10 before sending the
request to the rover.
<section>
<div class="controls">
<form>
<div grid>
<div column>
<label>
<span>Left Wheel Speed</span>
<input type="range" id="left" v-model="left" max="10" min="-10" value="0">
</label>
</div>
<div column>
<label>
<span>Right Wheel Speed</span>
<input type="range" id="right" v-model="right" max="10" min="-10" value="0">
</label>
</div>
</div>
For the rest of the controls we use three buttons, one to start/stop the rover,
one to enable/disable the rover and one to reset the rover. Both the toggleable
buttons have their text linked to either the stopped
or enabled
variable to
ensure they display the right text respectively. All the buttons are linked to a
function when they are clicked, which we will look at in the next section.
<div>
<button @click="toggleStopped">
<span v-if="stopped">Start</span>
<span v-else>Stop</span>
</button>
<button @click="toggleEnabled">
<span v-if="enabled">Disable</span>
<span v-else>Enable</span>
</button>
<button @click="reset">Reset</button>
</div>
</form>
</div>
</section>
</main>
</template>
# Script
The script section contains all of the javascript code related to our component. Here we can place any custom logic our component requires. We start by creating a constant variable for the base url to the backend api which we set in the configs above.
<script>
const API_URL = `${process.env.BASE_URL}/api`;
Vue requires some functions to be exported that it will use to create the components, all of the rest of the functions here are inside this export block. The name option gives our component an id vue can use to identify it.
export default {
name: 'controls',
The data function is called during the component creation to get the default variable that the component will contain. We use it to initialize all of the variable we require to some sane default.
data() {
return {
left: '0',
right: '0',
errorMessage: false,
stopped: true,
enabled: true,
wPressed: false,
aPressed: false,
sPressed: false,
dPressed: false,
interval: null,
};
},
The created function is called once the component has been created. We use it to
register a couple of functions to the keyup
and keydown
events. These
functions will be used to capture the users key presses and trigger certain
actions once pressed which will will look at below.
created() {
window.addEventListener('keyup', this.keyReleased);
window.addEventListener('keydown', this.keyPressed);
},
Similar to created beforeDestroy
is run by vue as part of the components
lifetime but just before it is destroyed as the name implies. We use this to
ensure we send one final stop
command to the rover.
beforeDestroy() {
this.stop();
},
The methods block describes all of the methods available to our component, they
can be bound to elements in our template or called from our components reference
(which will be exposed to us as this
from other component methods). All of the
remaining functions are defined within the methods block.
methods: {
The errorHandler
is a helper function that sets the errorMessage
from the
given response. It will be passed into all of the http calls to handle any
returned errors.
errorHandler(response) {
if (response.body && response.body.error) {
this.errorMessage = response.body.error;
} else {
this.errorMessage = `Unable to connect to ${response.url}`;
}
},
calculateSpeeds
is another helper function that sets the speeds of the left
and right servos based on which current buttons are pressed. It does this by
adding or subtracting 10 from the left and right speed based on which buttons
are pressed, then limiting each value to be between -10 and 10. This means that
if w
is pressed the rover will move at full speed forwards. If a
is pressed
then it will rotate on the spot to the left. But if both w
and a
are pressed
it will move in an arc to the left. It does not however call the api at all,
only sets up the speeds locally. Instead the api will be called periodically and
send the currently set values which we will see in the start
/stop
functions
below.
calculateSpeeds() {
let left = 0;
let right = 0;
if (this.wPressed) {
left += 10;
right += 10;
}
if (this.sPressed) {
left -= 10;
right -= 10;
}
if (this.aPressed) {
left -= 10;
right += 10;
}
if (this.dPressed) {
left += 10;
right -= 10;
}
if (left > 10) {
left = 10;
}
if (left < -10) {
left = -10;
}
if (right > 10) {
right = 10;
}
if (right < -10) {
right = -10;
}
this.$set(this, 'left', left);
this.$set(this, 'right', right);
},
The rover will be controlled by periodically sending the currently set speeds to it every 50 milliseconds while it is not in the stopped state. We do this instead of simply sending one command every time something changes as it gives a more predictable number of requests per second sent and then processed by the rover, effectively stopping you from overwhelming the rover with a burst of requests if you change the speed too quickly. It will also lets us build some fail safes into the rovers rest api. For example we could get it to auto stop if no request has been received in 200 milliseconds due to a connection loss or a browser crash. We will however not be looking at that in this post.
There is a trade off with how often we send the commands, slower and it is less responsive, faster and it requires more processing to handle all of the requests. 50-100 milliseconds gave a good balance between these tradeoffs but it was noted that the rest api is heaver on the pis cpu then I would have really liked. We will looking at more efficient ways to do this in the future for now it is good enough for the current task.
toggleStopped() {
this.$set(this, 'stopped', !this.stopped);
if (this.stopped) {
this.stop();
} else {
this.start();
}
},
stop() {
clearInterval(this.interval);
this.left = 0;
this.right = 0;
this.wPressed = false;
this.aPressed = false;
this.sPressed = false;
this.dPressed = false;
this.$http.put(`${API_URL}/stop`).then(null, this.errorHandler);
},
start() {
this.setSpeed();
this.interval = setInterval(this.setSpeed, 50);
},
The toggleEnabled
function acts like the toggleStopped
method above, except
it simply disables/enables the servos while preserving their current speeds and
does not stop the speed commands from being sent.
toggleEnabled() {
this.$set(this, 'enabled', !this.enabled);
if (this.enabled) {
this.$http.put(`${API_URL}/enable`).then(null, this.errorHandler);
} else {
this.$http.put(`${API_URL}/disable`).then(null, this.errorHandler);
}
},
The setSpeed
function sends the currently set speeds to the rover, with a very
short timeout as we expect to call this function many times a second and we do
not want connections to build up if there are network issues. We multiply the
values by 10 as the rovers speed has a greater precision then we really care
about in this ui.
setSpeed() {
this.$http.put(`${API_URL}/speed`, {
left: this.left * 10,
right: this.right * 10,
}, { timeout: 200 }).then(null, this.errorHandler);
},
The reset
function resets the rovers state in both the ui and sends the
request to the rover. This will help to fix any potential problems that might
occur in the rover - at least from a software configuration point of view.
reset() {
this.enabled = true;
this.left = '0';
this.right = '0';
this.wPressed = false;
this.aPressed = false;
this.sPressed = false;
this.dPressed = false;
this.$http.put(`${API_URL}/reset`).then(null, this.errorHandler);
},
Lastly we handle the key press and release events by setting the relevant
variables to true or false, or calling one off functions and then call the
calculateSpeeds
function we described above to update the speed values.
keyPressed(event) {
if (event.key === 'w') {
this.wPressed = true;
} else if (event.key === 'a') {
this.aPressed = true;
} else if (event.key === 's') {
this.sPressed = true;
} else if (event.key === 'd') {
this.dPressed = true;
} else if (event.key === 't') {
this.toggleStopped();
return;
} else if (event.key === 'y') {
this.toggleEnabled();
return;
} else {
return;
}
this.calculateSpeeds();
},
keyReleased(event) {
if (event.key === 'w') {
this.wPressed = false;
} else if (event.key === 'a') {
this.aPressed = false;
} else if (event.key === 's') {
this.sPressed = false;
} else if (event.key === 'd') {
this.dPressed = false;
} else {
return;
}
this.calculateSpeeds();
},
},
};
</script>
# Style
The style sections allows us to define any extra css our component needs. We
define it as scoped
to limit the css to only the elements we define above
rather then applying them globally. We only do some basic stuff to anchor the
controls to the bottom of the page and center them and generally make them look
nicer.
<style scoped>
.controls {
position: absolute;
bottom: 0;
width: 100%;
margin-bottom: 50px;
}
.controls form {
margin: auto;
width: 50%;
}
main {
height: 100vh;
width: 100vw;
background-color: #f5f5f5;
}
</style>
# The Root Component
The applications root component is located at ui/src/App.vue
and will hold
anything we require on all pages of our application - which is currently
nothing. So lets remove the logo and custom styling of the default boiler plate
that was generated for us. We can expand on this later as we develop them
application in future posts.
<template>
<div id="app">
- <img src="./assets/logo.png">
<router-view></router-view>
</div>
</template>
...
</script>
<style>
-#app {
- font-family: 'Avenir', Helvetica, Arial, sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- text-align: center;
- color: #2c3e50;
- margin-top: 60px;
-}
+
</style>
Note that the <router-view>
is where our component will be placed by the
router depending on which page we load. We don't strictly need the router and
could have just written our component as the root component but this will give
us more flexibility later should we decide to add more pages or other components
to the application.
# The Router
vue-router
allows us to render different components based on the url of the
page. We are not strictly taking advantage of this feature yet but will keep it
in place to make expanding our site easier at a later date. All we require is to
update the router to tell it about our new component and to use it in place of
the Hello
component for the root path /
. Edit ui/src/router/index.js
with
the following changes.
import Vue from 'vue';
import Router from 'vue-router';
-import Hello from '@/components/Hello';
+import Controls from '@/components/Controls';
Vue.use(Router);
...
routes: [
{
path: '/',
- name: 'Hello',
- component: Hello,
+ name: 'Controls',
+ component: Controls,
},
],
});
We can now delete ui/src/components/Hello.vue
as we will no longer require it.
# Building The UI Into The Image
Finally we can build and package our ui into the image alongside the rest api.
If you recall from our last post static files will be served from
/srv/rover/ui
so all we need to do is copy our built files to that location
inside the image. This can be done in the create-image
script in the same spot
that we copy the binaries across.
@@ -27,6 +27,11 @@ if [ ! -f "target/arm-unknown-linux-gnueabihf/release/rover-se
rver" ]; then
exit 1
fi
+if [ ! -d "ui/dist" ]; then
+ echo "'ui/dist' not found. Have you run 'cd ui; npm install && npm run build'?"
+ exit 1
+fi
+
# Unmount drives and general cleanup on exit, the trap ensures this will always
# run except in the most extreme cases.
cleanup() {
...
install -Dm755 "target/arm-unknown-linux-gnueabihf/release/rover-cli" "${mount}/usr/local/bin/rover-cli"
install -Dm755 "target/arm-unknown-linux-gnueabihf/release/rover-server" "${mount}/usr/local/bin/rover-server"
install -Dm755 "src/bin/rover-server.service" "${mount}/etc/systemd/system/rover-server.service"
+mkdir -p "${mount}/srv/rover/ui"
+cp -r ui/dist/* "${mount}/srv/rover/ui/"
# Prep the chroot
mount -t proc none ${mount}/proc
The ui can be built for production by running npm run build
inside the ui
directory. We do not include this command in the create-image
script above for
the same reason we excluded the cargo build
command - we do not want it to run
as root. Instead you must run these two commands before running the
create-image
script. This is automated in travis by editing .travis.yml
with
the following.
- export PATH="$PATH:$HOME/.cargo/bin"
- curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain=stable
- rustup target add arm-unknown-linux-gnueabihf
+- nvm install 7.7.3
+- nvm use 7.7.3
script:
- cargo build --release --target arm-unknown-linux-gnueabihf
+- ( cd ui && npm install && npm run build )
- sudo ./create-image
- xz -z rpizw-rover.img -c > rpizw-rover.img.xz
- zip rpizw-rover.img.zip rpizw-rover.img
# Conclusion
We now have a solid foundation to start working from, a nice ui and api to manually control our rover allowing us to position/reset it with ease. I would like to expand upon this in the future to allow running and uploading of custom scripts and programs to preform different dedicated tasks - to give similar workflow, in a way, to uploading a sketches to an arduino as well as to integrate the view from the pis camera. But in the next few posts I plan to start hooking the rover up to some sensors to get it to interact with the world.
Considering this application was not very complex we could have this with a simple html page and some simple javascript using jquery but I wanted to take this chance to learn more about some of the emerging web technologies that are popping up all over the place and to make it easier to grow the project in the future.
Overall I found vue.js very easy to work with, its simple and has some excellent documentation with plenty of examples about. Coupled with webpack makes the process very fluid and more familiar to the compiled work flows I am used to. Allowing you to have a highly organized and loosely coupled source that compiles down to a small and clean set of deployable artifacts without needing to set up a hugely complicated build pipeline by hand - something I have always missed when working on browser side code in previous projects.
Again I have skipped over the unit testing side of the project, not doing so would have distracted away from the core concepts of this post and made it far to long. I hope to look back at this in a future post, possibly once the application has evolved a bit more beyond the proof of concept stage.
You can view the final source code on the v0.5 branch or download the image created from this process here.