Oh hi there, good day to you. My name is Sander, a passionate developer with a creative mind™.
Jump to content
Sander

Microflow

8 min read
Microcontrollers made simple

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 prototypes, but the learning curve is often too steep for most designers who had (almost) 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’s 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 bridgeMicroflow hardware bridge

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.

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 Microflow Hardware Bridge.

The core of this plugin is achieved through a 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);
			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 studioMicroflow studio with some hardware components

Microflow studio

This tool was build to make working with microcontrollers 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; // This is important to 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 react-flow. Custom code is generated for the microcontroller based on the nodes and edges, as well as how the user connected them.

    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,"value":0,"id":"zuhhq2"});
nodes.set("zuhhq2", Led_zuhhq2);
const Interval_4aeu4a = new Microflow.Interval({"interval":500,"value":0,"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’ve flashed on the microcontroller, all @microflow/components are wrappers around the johnny-five library — Which is a wrapper around the firmata.js library.

    import JohnnyFive, { LedOption } from 'johnny-five';
import { BaseComponent, BaseComponentOptions } from './BaseComponent';

export type LedData = Omit<LedOption, 'board'>;
export type LedValueType = number;

type LedOptions = BaseComponentOptions & LedData;

export class Led extends BaseComponent<LedValueType> {
	private readonly component: JohnnyFive.Led;

	constructor(private readonly options: LedOptions) {
		super(options);
		this.component = new JohnnyFive.Led(options);
	}

	on(action: string, callback: (...args: any[]) => void) {
		if (action) {
			this.eventEmitter.on(action, callback);
			return;
		}

		this.component.on();
		this.value = 1;
	}

	off() {
		this.component.off();
		this.value = 0;
	}

	toggle() {
		this.component.toggle();
		this.value = this.value === 0 ? 1 : 0;
	}
}

    
  

And that’s roughly two months of work condensed into three code snippets.

And you’ve read through it all, have a🍪!

Inspired by

References