Building a React App to Interact with JACDAC Services on BBC micro:bit

Photo by Domenico Loia on Unsplash

In my last article, I’ve demonstrated how to interface JACDAC services we’ve installed on a BBC micro:bit. However, the event-driven nature and the lack of user-interface makes it not so convenient to be used. You have to pre-defined everything it needs to do.

This is probably the team behind JACDAC also provides a React package, which allows us to make an interactive web-based app. React is also one of the most popular front-end frameworks today, release by Facebook (yes, the same social media owned by a white robot man).

So, today we’ll create a simple accelerometer display app — and later, see another example of a more complicated showcase.

Flashing Firmware

We will use only the micro:bit accelerometer service, which still requires JACDAC firmware. You can find instruction in my previous post above.

Simple React JACDAC App

Photo by Green Chameleon on Unsplash

Just as before, you need to install Node.js. I also recommend to use VS Code or similar IDEs.

Create a New Project

We will use a tool called create-react-app to create a React template project. If you have used this before, it would be the best to uninstall the older version first:

npm uninstall -g create-react-app

Switch to the parent directory where you want to create a new React project. The following command will automatically download create-react-app and create a project “jacdac-react-simple”:

npx create-react-app jacdac-react-simple --template typescript

If you use VS Code, you can now use File -> Open Folder to open /jacdac-react-simple as project workspace.

To test the template project, run

npm start

This will start up a development server (a temperate server for the convenience of development) and automatically open it at localhost:3000:

You can now close the window now and press Ctrl + C in the console to stop the server.

Install Dependencies

create-react-app will install all its dependencies for you, but we still needs the JACDAC packages:

npm install jacdac-ts react-jacdac bootstrap

Here we will install jacdac-ts (the JACDAC JDOM package) and react-jacdac (additional JACDAC functionalities for React). We also install Bootstrap to get some easy-to-apply CSS styles.

Add JacdacContext

In order to add JACDAC features in the React VDOM Tree, we need to wrap <JacdacProvider> at the very top.

Open /src/index.tsx and change it as follows:

import React from 'react';
import ReactDOM from 'react-dom/client';
// import './index.css'; // comment out this
import App from './App';
import reportWebVitals from './reportWebVitals';
import { JacdacProvider } from "react-jacdac";
import 'bootstrap/dist/css/bootstrap.min.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<JacdacProvider>
<App />
</JacdacProvider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

index.tsx is the entry point of a React app, which loads the default component <App> and render it inside the <div id=”root”></div> node in /public/index.html.

Now with <JacdacProvider> wrapping <App>, we will be able to access the special “hooks” provided by react-jacdac in <App> or any of its child components.

Modify “App” Component

A React app (or Vue.js, Svelte and Solid, etc.) can be built with multiple (and multi-layered) components — they will be combined into a single, big component — but for now we will only write code in the App component.

Let’s replace /src/App.tsx with the following content:

import { FunctionComponent } from "react";
import { useBus, useServices, useRegister, useRegisterValue } from "react-jacdac";
import { SRV_ACCELEROMETER, AccelerometerReg } from "jacdac-ts";
const App: FunctionComponent = () => {
const bus = useBus();
const service = useServices({ serviceClass: SRV_ACCELEROMETER })[0];
const accelReg = useRegister(service, AccelerometerReg.Forces);
const [x = 0, y = 0, z = 0] = useRegisterValue(accelReg) as [number, number, number];
return (
<div className="container-sm">
<div className="p-2 m-2 fs-5">
Bus {bus.connected ? "connected" : "not connected"}
</div>
{bus?.connected ?
<div className="p-2 m-2">
<div className="p-1 m-1 display-6">Accelerometer</div>
<div className="p-1 m-1 lead">X: {x}</div>
<div className="p-1 m-1 lead">Y: {y}</div>
<div className="p-1 m-1 lead">Z: {z}</div>
</div>
:
<div className="p-2 m-2">
<button
type="button"
className="p-1 m-1 btn btn-lg btn-secondary"
onClick={() => bus.connect()}>
Connect bus
</button>
</div>}
</div>
)
}
export default App;

The App component is an arrow function with the type annotation FunctionComponent. The component has to return a JSX content (or TSX since we are using TypeScript), which is HTML tags with JavaScript embedded inside { and }. This will be rendered in the place of <App> in index.tsx.

A block of JSX content has to be wrapped in a single node, like <div></div>.

Of course, the JSX content in React is not exactly HTML, because React use these to generate a Virtual DOM tree and compare it to the real DOM later (this action is called diff). Some attribute names have to be changed to avoid errors — for example, “class” to “className” (we use these to set Bootstrap CSS styles) and “onclick” to “onClick”.

Photo by Steve Johnson on Unsplash

Hooks and Reactivity

React provides a hook called useState() to create “reactive” state variables. When a state is embedded in the JSX, any changes of the state value will trigger re-render of the component (or, the result you’ll see on the web page). React will not do it immediately to avoid performance problems, but still fast enough for you to see.

JACDAC has implemented some more hooks, quite a few actually. Here’s the four we will use in the first version of the project:

  1. useBus() gets the bus object (The micro:bit might be connected via some differrent ways, but it’s mostly WebUSB).
  2. useServices() gets the accelerometer service with the corresponding SRV_ACCELEROMETER constant.
  3. useRegister() gets the AccelerometerReg.Forces register from the service.
  4. Finally, useRegisterValue() read 3-axis acceleration data from the register.

Before the bus establishes connection, the service and register object would stay undefined. So we use bus.connected as the conditional parameter in a ternary operator: if the bus is connected, shows the x, y and z values (which will keep updating themselves). If not, display a button instead so the user can initialize connection. Once the bus is connected, the service and its register states will update themselves and become available.

Now run the dev server. You should be able to switch the page between two connection statuses:

npm start

Move to Sub-Components

When the complexity of your app grows, you don’t want to write everything in the same component. Instead, you can divide them into separate components.

Let’s modify /src/App.tsx:

import { FunctionComponent } from "react";
import { useBus, useChange, useServices, useRegister, useRegisterValue } from "react-jacdac";
import { SRV_ACCELEROMETER, AccelerometerReg } from "jacdac-ts";
const App: FunctionComponent = () => {
const bus = useBus();
const connected = useChange(bus, _ => _.connected);
const Connection: FunctionComponent = () => {
return (
<div className="p-2 m-2">
<button
type="button"
className="p-1 m-1 btn btn-lg btn-secondary"
onClick={() => bus.connect()}>
Connect bus
</button>
</div>
)
};
const Service: FunctionComponent = () => {
const service = useServices({ serviceClass: SRV_ACCELEROMETER })[0];
const accelReg = useRegister(service, AccelerometerReg.Forces);
const [x = 0, y = 0, z = 0] = useRegisterValue(accelReg) as [number, number, number];
return (
<div className="p-2 m-2">
<div className="p-1 m-1 display-6">Accelerometer</div>
<div className="p-1 m-1 lead">X: {x}</div>
<div className="p-1 m-1 lead">Y: {y}</div>
<div className="p-1 m-1 lead">Z: {z}</div>
</div>
)
};
return (
<div className="container-sm">
<div className="p-2 m-2 fs-5">
Bus {connected ? "connected" : "not connected"}
</div>
{connected ?
<Service />
:
<Connection />}

</div>
)
}
export default App;

If your dev server is still running, you can simply save App.tsx and see React automatically re-compiles and update the page.

Now we defined two components in App: “Service” and “Connection”. You can see how they are added into the JSX as <Service> and <Connection>.

We also use useChange() hook to create a reactive state connected from bus.connected. This is because when <Connection> have the bus to make connection, the internal change of bus.connected will not reflected in <App>. This is probably (well, I’m no React expert) because React still thinks bus in <App> is the same object, even though some of its attributes have already changed.

Photo by Kelly Sikkema on Unsplash

Separate Component Files

We can go a bit further, to put “Connection” and “Service ”in different files.

Create a sub-directory component under /src, then create a new file Component.tsx in it:

import { FunctionComponent } from "react";
import type { JDBus } from "jacdac-ts";
interface Props {
bus: JDBus
}
const Connection: FunctionComponent<Props> = (props) => {
return (
<div className="p-2 m-2">
<button
type="button"
className="p-1 m-1 btn btn-lg btn-secondary"
onClick={() => props.bus.connect()}>
Connect bus
</button>
</div>
)
};
export default Connection;

Since now “Connection” is in a separate file, it needs to receive the bus object from <App> via props. <App> still needs it to switch components. Since we are using TypeScript, we define a interface Props as the generic parameter to FunctionComponent, so that props can be properly typed.

props, like states, can trigger re-render if any values in it changes. Which means a parent component can update its child components with props, although there is nothing to update in <Connection> anyway.

Now create /src/component/Service.tsx:

import { FunctionComponent } from "react";
import { useServices, useRegister, useRegisterValue } from "react-jacdac";
import { SRV_ACCELEROMETER, AccelerometerReg } from "jacdac-ts";
const Service: FunctionComponent = () => {
const service = useServices({ serviceClass: SRV_ACCELEROMETER })[0];
const accelReg = useRegister(service, AccelerometerReg.Forces);
const [x = 0, y = 0, z = 0] = useRegisterValue(accelReg) as [number, number, number];
return (
<div className="p-2 m-2">
<div className="p-1 m-1 display-6">Accelerometer</div>
<div className="p-1 m-1 lead">X: {x}</div>
<div className="p-1 m-1 lead">Y: {y}</div>
<div className="p-1 m-1 lead">Z: {z}</div>
</div>
)
};
export default Service;

Right now “Service” does not need props so there are none here. We only display the component after the bus is connected,

Finally we can modify /src/App.tsx like this:

import { FunctionComponent } from "react";
import { useBus, useChange } from "react-jacdac";
import Connection from "./component/Connection";
import Service from "./component/Service";
const App: FunctionComponent = () => {
const bus = useBus();
const connected = useChange(bus, _ => _.connected);
return (
<div className="container-sm">
<div className="p-2 m-2 fs-5">
Bus {connected ? "connected" : "not connected"}
</div>
{connected ?
<Service />
:
<Connection bus={bus} />}
</div>
)
}
export default App;

Everything should be working just as before, but the code is easier to maintain now. This is basically what React (and other modern JS frameworks) is really about.

Events

Now we’ll see how to add a event of the accelerometer.

Modify /src/component/Service.tsx:

import { FunctionComponent } from "react";
import { useServices, useRegister, useRegisterValue, useEvent, useEventReceived } from "react-jacdac";
import { SRV_ACCELEROMETER, AccelerometerReg, AccelerometerEvent } from "jacdac-ts";
import type { JDEvent } from "jacdac-ts";
const Service: FunctionComponent = () => {
const service = useServices({ serviceClass: SRV_ACCELEROMETER })[0];
const accelReg = useRegister(service, AccelerometerReg.Forces);
const [x = 0, y = 0, z = 0] = useRegisterValue(accelReg) as [number, number, number];
const accelEvent = useEvent(service, AccelerometerEvent.Shake)
useEventReceived(accelEvent, (e: JDEvent) => {
alert(e.name);
});
return (
<div className="p-2 m-2">
<div className="p-1 m-1 display-6">Accelerometer</div>
<div className="p-1 m-1 lead">X: {x}</div>
<div className="p-1 m-1 lead">Y: {y}</div>
<div className="p-1 m-1 lead">Z: {z}</div>
</div>
)
};
export default Service;

We use useEvent() to get an reactive event object, then use useEventReceived to register a event handler function. The handler doesn't do much other than show an alert box when the event is triggered:

Build and Serve Production

When you are satisfied with the app — we will just pretend you are — you can have React to compile a JavaScript production of the app and serve it with any HTTP servers.

To build a production:

npm run build

The production will be in the /build directory. Then we’ll install a HTTP server globally:

npm install -g http-server

Serve the production:

http-server ./build

You will see

Starting up http-server, serving ./buildhttp-server version: 14.1.1http-server settings: 
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none
Available on:
http://192.168.xx.1:8080
http://10.195.xx.xxx:8080
http://127.0.0.1:8080
http://172.23.xxx.1:8080
Hit CTRL-C to stop the server

Now the app can be accessed via localhost:8080 or <your IP>:8080.

A More Complicated JACDAC Showcase

Photo by charlesdeluvio on Unsplash

I know, the project we’ve seen is as simple as it is. So I wrote another, more complicated example, as a showcase that how can you interact with possible devices and services in the React app. I will not explain everything — you can find the source code in my Github repo — but some important points gets to be mentioned.

You can download it and give it a try! I’m not much of a front-end guy though.

When the bus is connected, a component called “DeviceList” will show list of devices found on this bus. The user can select one, and the particular device object will be stored in a state using useState().

Another component called “Device” will show some information of it, and list all services found in this device (bear in mind that we don’t need to use hooks everywhere: sometimes we can simply use one to find another. See my previous post for the hierarchy of JACDAC objects.)

Service “Implementations”

So here’s the fun part: what do we do with the services? One of them is to view the service specification in a more readable way:

Still, it would be cooler if I can actually use services or interact with them. But since each service may be different, you’ll have to implement new components for each of them.

So what I did was write a “ServiceOperator” component, which will check the service class identifier and see if we have a specific component for it. If yes, that component will be shown. If not, the “ServiceSpec” component would be used instead.

...
import ServiceSpec from "./serviceSpec";
import Temperature from "./serviceImpl/temperature";
import Accelerometer from "./serviceImpl/accelerometer";
import SoundLevel from "./serviceImpl/soundlevel";
import DotMatrix from "./serviceImpl/dotmatrix";
interface Props {
serviceClass: number;
setServiceClass: (serviceClass: number) => any;
}
const ServiceOperater: FunctionComponent<Props> = (props) => { const operator = () => {
switch (props.serviceClass) {
case SRV_TEMPERATURE:
return <Temperature />;
case SRV_ACCELEROMETER:
return <Accelerometer />;
case SRV_SOUND_LEVEL:
return <SoundLevel />;
case SRV_DOT_MATRIX:
return <DotMatrix />;
default:
return <ServiceSpec serviceClass={props.serviceClass} />;
}
};
return (
<div className="p-1 m-1 shadow rounded">
{
props.serviceClass !== -1
&&
<div>
<div className="p-1 m-1">
<button
type="button"
className="btn btn-sm btn-close"
onClick={() => props.setServiceClass(-1)}
/>
</div>
<div className="p-1 m-1">
{operator()}
</div>
</div>
}
</div>
)
}
export default ServiceOperater;

You can see that I’ve implemented four components for corresponding services (all of them are in /src/component/serviceImpl). Only the control service is not implemented, so you will see the spec page instead.

These “implementation” components are all independent themselves, since they will each access a very specific service. You can use them directly, as long as the bus is connected.

There is a “close” button on top of the service area; when you click it, it will set <App>’s serviceClass state as -1. This is a way to indicate no service is currently selected.

useEffect()

In the Accelerometer component, I want to display pitch and roll angles instead of 3 axis raw values. So two states, pitch and roll are created, and are updated via React’s useEffect() whenever x, y and z are updated.

import { FunctionComponent, useState, useEffect } from "react";
...
const Accelerometer: FunctionComponent = () => { const [pitch, setPitch] = useState<number>();
const [roll, setRoll] = useState<number>();
const [event, setEvent] = useState<String>("");
const service = useServices({ serviceClass: SRV_ACCELEROMETER })[0]; const [x = 0, y = 0, z = 0] = useRegisterValue(
service.register(AccelerometerReg.Forces)) as [number, number, number];
useEffect(() => {
setPitch(Math.atan2(y, Math.sqrt(x ** 2 + z ** 2)) * (180 / Math.PI));
setRoll(Math.atan2(x, Math.sqrt(y ** 2 + z ** 2)) * (180 / Math.PI));
}, [x, y, z]);
useEventReceived(service.event(AccelerometerEvent.Shake), (e: JDEvent) => {
setEvent(e.name);
});
... useEffect(() => {
const timer = setTimeout(() => setEvent(""), 3000);
return () => clearTimeout(timer);
}, [event]);
return (
<div>
<div>
{
event !== ""
&&
<div className="p-3 m-3 fs-6 alert alert-success d-flex justify-content-between">
{event}
<span className="text-end">
<button
type="button"
className="btn btn-sm btn-close"
onClick={() => setEvent("")}
/>
</span>
</div>
}
</div>
<div className="p-1 m-1 lead">
<progress value={(pitch ?? 0) + 90} max="180" /> Pitch
</div>
<div className="p-1 m-1 lead">
<progress value={(roll ?? 0) + 90} max="180" /> Roll
</div>
</div>
)
}
export default Accelerometer;

You can also see that I use <progress> to display pitch and roll as progress bars (the original values are -90~90 but progress bars can only do with unsigned numbers, so I add 90 here).

Also, you might see I’ve register a lot of gesture event handlers in it. A state value called event is used to display the current event name in the component area. When any of the events happen, a “notification” will appear:

Again, with some help from my best friend Google, I use another useEffect() to call a time out function. This useEffect() depends on event (it will triggered by changes of event) and will clear out the state after 3 seconds (set it to empty string). The JSX will hide the notification when event goes back to empty. Boom, job done.

Challenges

I use Bootstrap to impose some not-bad-looking CSS styles, but some are not completely supported by React’s JSX grammar. I had to give up using Bootstrap’s progress bar style, because it doesn't work in React.

I also couldn’t think of a elegant way to create a 5 x 5 matrix input for the LED screen — in the end I settled for a simple brightness control (all LEDs would be light up). Perhaps I can find an answer in another UI packages another day…I hope.

Final Thoughts

Photo by Manja Vitolic on Unsplash

With React, we are able to create a interface for the micro:bit services (for now I do not have access to other JACDAC devices). Building a front-end app is an even bigger challenge than writing a single Node.js file, but the potential is also much greater.

(Although, I have to say JSX in React is tend to get messy — I’ve tried Vue.js and Svelte and I like their logic tags a lot. Mixing HTML and JavaScript is not an entirely healthy thing for your brain.

In fact, there’s a JACDAC .NET client as well, and God knows how many type of apps you can make in it. Not a dotnet fan myself, so I won’t go exploring that part.)

Then again, the device dashboard on JACDAC website is already more than enough for testing services. The point of writing a React app of your own — if the need ever arose — is to do custom tasks or to combine them with something else on the web. (I can throw you a quick idea: IFTTT. Or maybe a real game compiled in WebAssembly? Anyone?)

But behind all these coolness, it’s important not to forget that JACDAC is designed for STEM purposes. The easiness of hardware connecting may very well be its undoing: it could become just another expensive, flashy toy targeting anxious parents. Developers will have to think about how to create applications that can actually convey coding or logic thinking, and that will not be an easy task by all means.

--

--

Alan Wang

Alan Wang

Technical writer, former translator and IT editor.