Files
microbadge/docs/sdd.md
Myrddin Dundragon 1c593aff9b Initial pass on the design documentation.
The design doc and software design doc.
The UML images are generated with plantUML from the *.puml files.
2025-07-18 12:14:01 -04:00

8.4 KiB
Raw Blame History

MicroBadge Software Design Document

1. Introduction

1.1 Purpose

MicroBadge is a software application suite for the BBC micro:bit v2, designed as a digital conference badge. It serves both functional and social purposes; displaying the users name, hosting small interactive demos, and offering contact sharing via NFC.

This project serves as a conversation starter and technical showcase during events like conferences, meetings, and interviews.

1.2 Scope

This document focuses exclusively on the software implementation of MicroBadge. It covers the architecture, data structures, behavior, and design choices used to implement the badges app-switching system and core applications using Rust and the embassy async runtime.

1.3 Audience

This document is intended for:

  • Reviewers evaluating its design.
  • Recruiters or interviewers reviewing technical work.
  • Anyone trying to learn how to write Rust on an embedded platform.

2. System Overview

MicroBadge is an embedded application for the micro:bit v2. It uses the Embassy async runtime to manage multiple cooperative tasks without a traditional RTOS. The system is modular and consists of an app switcher, an LED display task, button listeners, and multiple interactive apps.

2.1 Runtime and Concurrency

MicroBadge uses Embassy's async executor. It runs the following tasks:

  • display_task -- Consumes frame buffers and drives the LED matrix.
  • button_listener -- One per button (A, B, Start). Waits for input and debounces it before sending an event.
  • app_task -- Runs the currently selected app. Allows apps to yield and re-enter on each loop.

All communication is channel-based using embassy_sync::channel::Channel.

[Task UML][./uml/tasks.png]

2.2 App Switcher

The Switcher manages app selection and transition. It displays a menu and uses the A, B, and Start buttons to navigate between apps.

Each app implements a shared App trait with an async run() method. Apps are isolated and run cooperatively, returning control when done.

Current apps:

  • Menu. The top-level app that allows selecting from installed apps.
  • Badge. Scrolls a string (e.g. your name) across the LED matrix.
  • Snake. A basic snake game with food, direction control, and score.
  • NFC Card (in development). Will present contact info via NFC.

[Switcher UML][./uml/switcher.png]

2.3 Input System

Each button is handled by a separate button_listener task. When a button is pressed, it sends a Button enum into a shared channel.

Apps listen for button input using the receiver end of the channel.

  • A and B buttons are mapped to actions like turn left and right.
  • Start is used to confirm or start an app. It is mapped to the capacitive touch sensor logo.
  • A debounce delay of 100 ms is used for stability.

2.4 Rendering System

The rendering system uses a frame buffer that is written by the active app and read by the display_task.

Apps write into this buffer using a Renderer abstraction. Drawing is done in an offscreen buffer that is later pushed to the display.

  • The screen is a 5x5 LED grid.
  • Per-frame updates allow for animations and dynamic content.
  • LED brightness levels are supported.

2.5 Code Organization

The system is split into modules for clarity and reuse:

  • app. Defines the App trait and shared app interface.
  • display. Low-level display driver and LED control.
  • renderer. Provides drawing primitives for apps.
  • channel. Shared async channels for button and frame messages.
  • switcher. App selection logic and switching behavior.
  • snake, menu, badge. App implementations.
  • microbit. Definitions for button identifiers and device pins.

Each module is self-contained and uses only the shared channels and traits for interaction.

3. Application Features

3.1 Name Scroller

  • Scrolls a configured name across the LED display.
  • Uses an async timer to advance frames.
  • Simple input handling: Any Button returns to the menu.

3.2 Snake Game

  • 5x5 LED grid snake game using a wrapped grid (WrappedU8<0, 4>).
  • Buttons A and B turn the snake left/right.
  • Food spawns randomly in empty grid cells.
  • On collision with self, enters game-over state and displays score.

3.3 NFC Business Card (WIP)

  • Intended to broadcast a vCard or custom URI over NFC.
  • Plan to use the BLE softdevice on the chip.
  • Currently under development.

4. System Architecture

This system uses Embassy's async runtime to coordinate application execution, hardware input, and rendering on the micro:bit v2 board. It is divided into distinct tasks: input listeners, a display task, and an app task.

The overall architecture is message-passing oriented. Input events and screen updates are communicated over embassy channels.

Application logic is encapsulated in independent modules conforming to a shared App trait. The Switcher manages the active app and transitions between them.

4.1 Components

  • main.rs: Entry point. Spawns system tasks using Embassy.
  • Display: Renders 5x5 LED frames from a channel receiver using PWM.
  • ButtonListener: Listens for button presses and sends events via channel.
  • Switcher: Manages app lifecycle and transitions.
  • App: Trait for any runnable application module.
  • menu, badge, snake, nfc: App implementations.

5. Data Structures and State

5.1 Position, Direction, and Snake Body

The board is a fixed 5×5 grid. Positions are stored using a custom Position struct, which holds a ClampedU8 for both x and y axes, ensuring values remain within bounds.

  • Position: Represents a coordinate on the board with safe bounds.
  • Direction: Enum for movement direction: Up, Down, Left, Right.
  • Snake: Maintains a list of Position elements representing the snake's body. The first item is always the head.

Snake direction is updated via input, and movement wraps to stay within the board.

5.2 Message-Passing and Input State

User input is handled asynchronously via Embassy channels.

  • Button presses are detected using button_listener tasks.
  • Events are sent to the ButtonChannel.
  • Applications read input non-blockingly using try_receive().

This decouples physical input handling from application logic and allows clean, testable state transitions.

6. Component Interactions

6.1 How Components Interact Over Time

At runtime, three core tasks are running:

  • display_task: Receives rendered frames and presents them on the display.
  • button_listener: Spawns three tasks, one per button (A, B, Start).
  • app_task: Owns the app switcher and runs the current app.

All interactions are asynchronous and use message-passing over embassy channels.

6.2 Flow of Control

  1. User presses a button.
  2. The button task sends a message to the channel.
  3. The app reads the button event from the channel.
  4. The app updates internal state (e.g., direction or selection).
  5. The app prepares a frame and sends it to the frame channel.
  6. The display task renders the frame.

This loop repeats, giving a responsive, concurrent embedded UI.

7. Development Environment

7.1 Rust + Embassy

This project uses Rust with the embassy async runtime. It provides interrupt-driven, non-blocking execution suitable for low-power embedded devices.

7.2 Tools

  • probe-rs: For flashing and debugging firmware.
  • defmt: Lightweight logging for embedded targets.
  • panic-probe: Panic handler integrated with defmt output.
  • cargo-embed: For development workflow and flashing.

Development was done on Linux using vim and CLI tooling.

8. Design Decisions

8.1 Why Embassy

Embassy was chosen for its async-first architecture, which maps well to reactive, event-driven embedded applications like games and UI. It allows multiple concurrent tasks without needing an RTOS or blocking code.

8.2 Fixed Board Size

The micro:bit's 5×5 LED matrix is inherently fixed. Game logic and rendering are simplified by using a constant-size grid, avoiding the need for dynamic allocation or scaling logic.

8.3 Data Wrapping and Clamping

Out-of-bounds positions are prevented using custom ClampedU8 types. These provide safe arithmetic that prevents overflow and keeps all positions within 04 inclusive. This reduces bugs and runtime checks in critical loops.

9. Future Work

9.1 NFC Business Card App

An in-progress app will emulate a contact card via NFC. The goal is to allow devices to scan the badge and receive contact information, a URL, or a vCard.

9.3 UI Polish