Logo
< Back to home

Micro Bluetooth

I've created a simple React.js app that communicates with the BBC micro:bit through web bluetooth to visualise the motion made when moving the device.

View the micro:bit bluetooth demo. Note, due to the limitations of web bluetooth support the demo will only work for Android, Chrome OS and Mac. You'll also need a micro:bit to connect to the page and set it up as outlined below.

This guide assumes you have knowledge of React.js, styled components and how to program a micro:bit.

Setting up the micro:bit

I've used the makecode blocks website to program the micro:bit. You'll need to enable to the bluetooth extension, which disables the radio, as well as setting the project to not require bluetooth pairing. Next add the bluetooth accelerometer service into the on start block. Then add an icon to display on the LEDs to help show your code is working. I've also added a yes icon into the on bluetooth connected block and a no icon into the on bluetooth disconnected block.

Connecting via web bluetooth

The are a few security constraints when using web bluetooth, the site must run on HTTPS and the request to connect to a bluetooth device must be triggered by a user action, e.g. clicking a button.

This is a simplified example of how I've written the functionality to connect to the micro:bit over bluetooth.

// bluetooth uuid for the accelerometer service
const accelerometerService = 'e95d0753-251d-470a-a062-fa1922dfa9a8';
// bluetooth uuid for the accelerometer data
const accelerometerData = 'e95dca4b-251d-470a-a062-fa1922dfa9a8';
const options = {
	// this shows all nearby bluetooth devices
	acceptAllDevices: true,
	// define services that you wish to use to prevent 
	// errors accessing later
	optionalServices: [
		accelerometerService,
	],
};

navigator.bluetooth
	.requestDevice(options)
	.then(device => {
		// Attempts to connect to remote GATT Server.
		return device.gatt.connect();
	})
	.then(server => {
		// get the primary service for the accelerometer
		return server.getPrimaryService(accelerometerService);
	})
	.then(service => { 
		// get the characteristic for the accelerometer data
		service.getCharacteristic(accelerometerData);
	})
	.then(characteristic => {
		// start notifications for the characteristic
		characteristic.startNotifications();
	})
	.then(characteristic => {
		// listen to the characteristic changes
		characteristic.addEventListener('characteristicvaluechanged', 
			handleCharacteristicValueChanged);
	});

const handleCharacteristicValueChanged = (event) => {
	// get the data for x, y and z axis of the accelerometer
	const accelerometer = {
		x: event.target.value.getInt16(0, true),
		y: event.target.value.getInt16(2, true),
		z: event.target.value.getInt16(4, true),
	};
	console.log(accelerometer);
};

From data to animation

To create the animation I needed to convert the x, y and z accelerometer data into roll and pitch, these are the angles of rotation of the x and y axis. The calculations used are based off of the Accelerometer Service example by Lancaster University.

handleAccelerometer({x, y, z}) {
	// x, y and z range between -1024 and 1024 so need to normalise the values
	x = x / 1000;
	y = y / 1000;
	z = z / 1000;
	const radians = 180 / Math.PI;
	const pitch = Math.atan(x / Math.sqrt(Math.pow(y, 2) + Math.pow(z, 2))) * radians;
	const roll = -1 * Math.atan(y, Math.sqrt(Math.pow(x, 2) + Math.pow(z, 2))) * radians;
	// the accelerometer data is passed through to the Microbit component through the state
	this.setState({
		accelerometer: {
			roll,
			pitch,
		},
	});
}

Animating Microbit component, composed of the React.js component and the styles using the module styled-components.

// Microbit.js
import React from 'react';
import PropTypes from 'prop-types';

import StyledMicrobit, { Device, Front, Back } from './Microbit.styles';

const Microbit = ({accelerometer}) => (
	<StyledMicrobit>
		<Device accelerometer={accelerometer}>
			<Front />
			<Back />
		</Device>
	</StyledMicrobit>
);

Microbit.propTypes = {
	accelerometer: PropTypes.shape({
		roll: PropTypes.number,
		pitch: PropTypes.number,
	}),
};

export default Microbit;

Styled component file, creates the animation from the roll and pitch values:

// Microbit.styles.js
import styled from 'styled-components';

// images used for the front and back of the micro:bit
import MicrobitFront from '../../media/microbit-front.png';
import MicrobitBack from '../../media/microbit-back.png';

const StyledMicrobit = styled.div`
	position: relative;
	/* perspective is used to give a "3D"
	appearance to the animation */
	perspective: 1000;
`;

export default StyledMicrobit;

export const Device = styled.div.attrs({
	// accelerometer is a prop passed to the styled 
	// Device component
	style: ({accelerometer}) => ({
		transform: `rotateX(${accelerometer.roll}deg) 
			rotateY(${accelerometer.pitch}deg)`,
	}),
})`
	position: relative;
	transform-origin: 50% 50%;
	transform-style: preserve-3d;
	height: 364px;
	width: 431px;
	max-width: calc(100% - 40px);
	margin: 40px auto;
`;

const Side = styled.div`
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	backface-visibility: hidden;
	background-size: contain;
	background-repeat: no-repeat;
`;

export const Front = Side.extend`
	background-image: url(${MicrobitFront});
	z-index: 10;
`;

export const Back = Side.extend`
	background-image: url(${MicrobitBack});
	transform: rotateY(180deg);
`;