ngVikings, Beacons, and Fun with Web Bluetooth
Building sensor-based games with the power of Web Bluetooth, Angular, and Material Design
A few weeks, ago, I was fortunate enough to give a talk at the Angular ngVikings Conference 2017 about my favorite thing: Angular of Things!
Naturally, it was a great chance to show off the ng-beacon project, and so I decided to combine Angular and Web Bluetooth, and use the temperature sensor embedded in ng-beacon to create a “Hot or Not” game. Not the usual “Hot or Not” game, mind, but a temperature Hot or Not game.
I used the Angular Web Bluetooth library, developed by my friend Wassim Chegham, as the foundation of the project (if you’re interested in hearing a little more about the library, he wrote a great blog post about it). To add a little extra spice, I used Angular Material to sneak in some Material Design, as well as some rxjs sauce for good measure.
You can watch the full talk here (the real fun begins around 15:00).
ng-beacon implements the standard Bluetooth Service for temperature and humidity information called “Environmental Sensing,” and so the game I created will work with any BLE-based temperature sensor that follows that standard, too, making it easier for people to test and tinker with the game while ng-beacon’s R&D phase continues.
I thought it would be fun to do a post about the game, since the game is pretty quick to build, and shows off some of the cool things you can do with the Angular of Things.
And did I mention it was quick?
“Hot or Not” in 15 Minutes
Prerequisites: You will need Chrome on Mac, Chrome OS, Android or Linux to follow this tutorial, since Windows is still not supported by Web Bluetooth (though they are working on it). I used an Ubuntu Virtual Machine running inside Windows in my talk. In order to make the game (at least in 15 minutes), I recommend familiarity with setting up new projects using the Angular CLI, installing third party libraries, and working with Angular 2 modules.
Oh — and access to a beacon that offers the Standard Environmental Sensing Service. I realize this last one might be tricky, since it’s hard to know which products offer the service. If you’re interested in keeping up/helping out with the ng-beacon, fill out this survey.
Ready to go?
Setup: Creating the Project and Making a Pretty UI
Let’s start by creating a new Angular application, using the CLI:
ng new hot-or-not
This command creates the hot-or-not
directory and sets up the project (depending on your machine, it might take a couple minutes).
Next, install the Angular Web Bluetooth library:
cd hot-or-not
npm install --save @manekinekko/angular-web-bluetooth@0.4
After this, we can already run the project! Enter
ng serve
and go to http://localhost:4200 to see the (in?)famous “app works!” page:
Semi-related Tangent: I’ve always wondered if the port number they chose has something to do with 42, you know, the meaning of the universe. Can anyone verify this for me?
Now, let’s beef it up with some Material design. We will use Angular Material: follow their guide here, make sure you install Hammer.js, and set up the Material Design icons as explained in their tutorial. Also, add the following line to your src/styles.css
to pick the colors for your material design theme:
@import '~@angular/material/core/theming/prebuilt/indigo-pink.css';
Then, edit your component to add the header for the app. We will use the md-toolbar component for that. Add the following code to the template of your component at src/app/app.component.html
:
<md-toolbar color="primary">
Hot or Not?
<div class="fill"></div>
<button md-fab (click)="addPlayer()">
<md-icon>add</md-icon>
</button>
</md-toolbar>
This will create the title with a button to add a new player, which will eventually call the addPlayer()
method (which we will implement soon). We’re not done with the header yet though, so let’s add some styles to make it even more awesome! Add to src/app/app.component.css
:
.fill {
flex: 1;
}md-toolbar {
position: relative;
z-index: 10;
}md-toolbar button[md-fab] {
margin-top: 40px;
}
Now you should be getting something like this:
Now, let’s implement a card to display information about the players. We will start with some static data, just to get it to look right — and then wire the logic in.
Add the following to src/app/app.component.html
:
<md-card class="player-card">
<md-card-title>Miško Hevery</md-card-title>
<md-card-content>
<md-slider min="0" max="40" value="20"></md-slider>
<span>20</span>
</md-card-content>
</md-card>
And just like before, we’ll throw in some CSS styles as well back in src/app/app.component.css
:
md-card {
margin-top: 10px;
}.player-card md-card-header {
margin-bottom: 0;
}.player-card md-card-content {
display: flex;
align-items: center;
}.player-card md-slider {
flex: 1;
}.player-card span {
min-width: 50px;
text-align: right;
}
Now you should be getting the header with the card:
So, with the visuals (more or less) ready, let’s go ahead and add some logic.
We will start by defining a PlayerInfo
interface, that will hold information about a single player: name, score and whether the player is a winner (has the highest temperature). We’ll add this code in just before our Controller insrc/app/app.component.ts
:
interface PlayerInfo {
name: string;
score: number;
}
Then, we’ll create an array of players inside our controller. Replace the first line of the AppComponent
controller so it reads:
export class AppComponent {
players: PlayerInfo[] = [];
}
That’s right! We simply define an array of PlayerInfo
objects called players
and initialize it with an empty array. Easy enough, no?
Finally, let’s add a dummy addPlayer()
method inside the controller to create some random data (for now) to test the UI:
addPlayer() {
const player = {
name: 'Player #' + Math.round(Math.random() * 100),
score: Math.round(Math.random() * 40)
};
this.players.push(player);
}
Last but not least, we need to make sure that the code we have in the templates actually displays the data from the players array, so let’s change this to read:
<md-card class="player-card" *ngFor="let player of players">
<md-card-title>{{player.name}}</md-card-title>
<md-card-content>
<md-slider min="0" max="40" [value]="player.score"></md-slider>
<span>{{player.score}}</span>
</md-card-content>
</md-card>
Now, whenever you click the Add button, you will see a new card appear in the list.
The Angular of Things Part
Ok, let’s get to the real stuff — connecting this to the actual hardware (i.e., the temperature sensor in ng-beacon). As mentioned above, ng-beacon uses the standard Bluetooth service, “Environmental Sensing,” so we’ll be working with that.
First, we need to set up the Angular Web Bluetooth module by following the instructions here. Then, we want to create a new service to deal with the hardware part (don’t worry, it’ll be short and sweet). We will put it in a service, as it is not directly related to the presentation logic of our component, and thus it will be reusable in other parts of the app or even in other apps.
Let’s run the following command to create the service:
ng g service environmental-sensing
Then, we need to add it to the list of provides in our src/app/app.module.ts
, so it reads:
providers: [EnvironmentalSensingService],
(and don’t forget to import it at the top of the file!)
Finally, let’s implement the service! First of all, we need to import the BluetoothCore
service into our service, in environmental-sensing.ts
:
import { Injectable } from '@angular/core';
import { BluetoothCore } from '@manekinekko/angular-web-bluetooth';
import 'rxjs/add/operator/mergeMap';@Injectable()
export class EnvironmentalSensingService { constructor(private ble: BluetoothCore) { }}
If you remember from my first Web Bluetooth post, there are 5 steps you need to follow for interacting with BLE devices. We are going to implement the first two steps (Scan + Connect) in a method called getDevice()
in our service:
getDevice() {
return this.ble
.discover$({
filters: [{ services: ['environmental_sensing'] }]
});
}
This method basically scans for BLE devices around us that provide the Environmental Sensing service, and presents the user with a list of matching device. Once the user chooses one, it then connects to the chosen device. An observable is returned which we will subscribe to later, to get the chosen device.
Next, we’ll add a second method, getTemperature()
, that implements steps 3, 4, 5 (get service, get characteristic, subscribe for notifications):
getTemperature(gatt: BluetoothRemoteGATTServer) {
const ble = this.ble;
return ble.getPrimaryService$(gatt, 'environmental_sensing')
.mergeMap(svc => ble.getCharacteristic$(svc, 'temperature'))
.mergeMap(char => ble.observeValue$(char))
.map(value => value.getUint16(0, true) / 100.);
}
Nothing too fancy here — we get the Environmental Sensing service from the device, then get the Temperature characteristic from it, then subscribe to it and observe its value, and finally, extract the value and divide it by 100. The reason for that, per the BLE spec, the temperature value is multiplied by 100 and then represented as unsigned short value (little-endian, that’s where the second parameter of getUint16
comes into play), so we just do the reverse.
Thanks to the angular-web-bluetooth library, we didn’t have to write a lot of boilerplate code — and we can now go on with integrating everything and having fun with out little game!
First of all, let’s inject our new Service into the controller at app/src/app.component.ts
(again, don’t forget the import!)
constructor(private envSensing: EnvironmentalSensingService) {}
Then, we modify addPlayer()
so it actually connects with the device, reads it name, updates the player array and subscribes to the temperature values:
addPlayer() {
this.envSensing.getDevice()
.subscribe(gatt => {
const player = {
name: gatt.device.name,
score: 0
};
this.envSensing.getTemperature(gatt)
.subscribe(temperature => {
player.score = temperature;
});
this.players.push(player);
});
}
and then… we’re ready to play!
Final Thoughts
I should say that there’s more in the repo I put together for the game like adding “winner” highlighting, removing players on disconnect, etc., but I’ll leave that for you as an optional exercise :)
You can find the repo here: https://github.com/urish/hot-or-not
I also want to take a moment to give a huge shoutout to the ngVikings conference. The vikings theme was awesome and well done, and added tons of fun to the conference:
I also want to thank ngVikings for doing such a great job with the talk videos: they were all uploaded very quickly and the editing was superb! So big kudos to them :)