Jump to content
Visit project

Microflow,

Microcontrollers made simple

8 minutes read
Technologies used
  • @xyflow/react
  • Arduino Firmata
  • Electron
  • Figma plugin API
  • Johnny-Five
  • shadcn/ui
  • MQTT

Interactivity

After helping students at the Master Digital Design for some years, I noticed that many students struggled with a common issue: microcontrollers.

The students want to create interactive (phygital) prototypes, but the learning curve is often too steep for most designers whom have never touched code before.

And I must agree; even though I’ve been working with microcontrollers for several years now, switching from my usual JavaScript environment to the Arduino IDE still is rough for me.

Rapid prototyping

While there are some tools available that simplify working with microcontrollers, such as Arduino Blocks, Code kit or Johnny Five, starting a new project still requires a lot of Arduino-like knowledge.

That is where Microflow comes in.

Microflow is a set of tools designed to facilitate prototyping for interactivity without the need to worry about low-level coding, or coding at all for that matter!

Microflow consists of 2 tools so far:

  1. Microflow hardware bridge — a Figma plugin that enables interaction with your Figma variables via MQTT.
  2. Microflow studio — a desktop application that allows you to create interactive prototypes using a visual, flow-based interface.
Microflow hardware bridge running in Figma

Microflow hardware bridge

I think Figma is an awesome tool.

After the introducion of variables in Figma, you can create some pretty nifty prototypes and fool any stakeholders, making them believe they’re already viewing a real application.

What is still missing, however, is the interaction with the physical world.

And for plugin developers to access the preview

MQTT

Microflow Hardware Bridge relies on MQTT to communicate between Figma and the plugin.

This enables any client – whether in your browser, mobile app, microcontroller, or even an IoT device like your fridge (if it sends the correct data) – to send and receive messages from Figma.

The core of this plugin is achieved through a simple React component:

export function MqttVariableMessenger() {
	const { publish, subscribe, uniqueId } = useMqtt();
	const publishedVariableValues = useRef(new Map());
 
	useEffect(() => {
		subscribe(`microflow/${uniqueId}/variables/request`, topic => {
			publish(`microflow/${uniqueId}/variables/response`, publishedVariableValues);
		});
 
		subscribe(`microflow/${uniqueId}/variable/+/set`, async (topic, message) => {
			const variableId = topic.split('/').at(2);
			await sendMessageToFigma(SetLocalValiable(variableId, message.toString()));
		});
	}, [subscribe, publish, uniqueId]);
 
	// Listen to changes in variables from Figma variable panel
	useMessageListener(MESSAGE_TYPE.GET_LOCAL_VARIABLES, variables => {
		variables.forEach(async variable => {
			await publish(`microflow/${uniqueId}/variable/${variable.id}`, variable.value);
			publishedVariableValues.current.set(variable.id, variable.value);
		});
	});
 
	return null;
}

Some more sorcery is happening in the sendMessageToFigma and useMqtt, but I’ll leave that up to your imagination.

Or check the code if you are a nerd like me who likes to know how things work.

Microflow studio connected to Figma
Example flow in Microflow studio, connected to Figma using the Microflow hardware bridge plugin

Microflow studio

Microflow studio is a tool that allows you to create complext interaction with microcontrollers without writing a single line of code.

This tool was build to make working with microcontrollers as easy as plug-and-play.

In order to achieve that, there is some magic happening behind the scenes.

Flashing firmware

When connecting a supported microcontroller, Microflow studio will automatically detect the board and flash it with the correct firmata firmware.

To make this work with Electron, the backbone of the application, I stole and adapted the good parts of avrgirl-arduino and gave it some TS-love.

import { Board, BoardName, BOARDS } from './constants';
import { SerialConnection } from './SerialConnection';
 
export class Flasher {
	private readonly connection: SerialConnection;
	private readonly board: Board;
 
	constructor(boardName: BoardName, usbPortPath: string) {
		const board = BOARDS.find(board => board.name === boardName);
 
		if (!board) {
			throw new Error(`Board ${boardName} is not a know board`);
		}
 
		this.board = board;
		this.connection = new SerialConnection(board.baudRate, usbPortPath);
	}
 
	async flash(filePath: string) {
		try {
			const protocol = new this.board.protocol(this.connection, this.board);
			await protocol.flash(filePath);
		} catch (error) {
			throw error; // Rethrow the error so the caller can handle it
		} finally {
			await this.connection.close(); // Always close the port again
		}
	}
}

Code to generate code

Microflow studio provides a visual flow-based interface to connect components and create interactions.

For this, I utilized @xyflow/react. Custom code is generated for the microcontroller based on how the user connected the nodes and edges.

import type { Edge, Node } from '@xyflow/react';
 
export function generateCode(nodes: Node[], edges: Edge[]) {
    let code = `const Microflow = require("@microflow/components");`;
 
    code += `const nodes = new Map();`;
 
    code += `new Microflow.Board({ port: process.argv.at(-1); });`;
 
    nodes.forEach(node => {
    code += `const ${node.type}_${node.id} = new Microflow.${node.type}(${node.data});`;
    code += `nodes.set("${node.id}", ${node.type}_${node.id});`;
 
    const edgesGroupedByHandle = edges.reduce(
        (acc, edge) => ({
        ...acc,
        [edge.sourceHandle]: [...(acc[action.sourceHandle] || []), edge],
        }),
        {} as Record<string, Edge[]>,
    );
 
    Object.entries(edgesGroupedByHandle).forEach(([handle, groupedEdges]) => {
        code += `${node.type}_${node.id}.on("${handle}", () => {`;
 
        groupedEdges.forEach(edge => {
            const valueTriggers = [
                'set', 'check', 'show', 'rotate',
                'red', 'green', 'blue', 'opacity',
                'from', 'to', 'publish',
            ];
 
            const shouldSetValue = valueTriggers.includes(edge.targetHandle);
            let value = shouldSetValue ? `${node.type}_${node.id}.value` : undefined;
 
            if (node.type === 'RangeMap' && shouldSetValue) {
                value = `${node.type}_${node.id}.value[1]`;
            }
 
            const targetNode = nodes.find(node => node.id === edge.target);
            code += `${targetNode?.type}_${targetNode?.id}.${edge.targetHandle}(${value});`;
        });
 
        code += `}); // ${node.type}_${node.id} - ${action}`;
    });
 
    return code;
}

Which is a whole lot of code, even after some simplifications, to generate the following few lines of code for the microcontroller:

const Microflow = require("@microflow/components");
 
const nodes = new Map();
 
new Microflow.Board({ port: process.argv.at(-1) });
 
const Led_zuhhq2 = new Microflow.Led({"pin":13,"id":"zuhhq2"});
nodes.set("zuhhq2", Led_zuhhq2);
const Interval_4aeu4a = new Microflow.Interval({"interval":500,"id":"4aeu4a"});
nodes.set("4aeu4a", Interval_4aeu4a);
 
Interval_4aeu4a.on("change", () => {
    Led_zuhhq2.toggle(undefined);
}); // Interval_4aeu4a - change

A wrapper around a wrapper around a wrapper

To communicate with the firmata firmware we have flashed on the microcontroller, all @microflow/components are wrappers around the Johnny-Five library — Which is a wrapper around the firmata.js library itself.

import JohnnyFive, { ButtonOption } from 'johnny-five';
import { BaseComponent, BaseComponentOptions } from './BaseComponent';
 
export type ButtonData = Omit<ButtonOption, 'board'>;
export type ButtonValueType = boolean | number;
 
type ButtonOptions = BaseComponentOptions & ButtonData;
 
export class Button extends BaseComponent<ButtonValueType> {
	private readonly component: JohnnyFive.Button;
 
	constructor(options: ButtonOptions) {
		super(options, false);
 
		this.component = new JohnnyFive.Button(options);
 
		this.component.on('up', () => {
			this.value = false;
			this.eventEmitter.emit('inactive', this.value, false);
		});
		this.component.on('down', () => {
			this.value = true;
			this.eventEmitter.emit('active', this.value, false);
		});
		this.component.on('hold', () => {
			this.eventEmitter.emit('hold', this.value);
		});
	}
}

🪆