Project 6: Custom Application Launcher and Mini-OS

Build a mini-OS launcher that boots into a menu, manages app lifecycles, and provides shared services (display, input, storage) with safe resource arbitration.

Quick Reference

Attribute Value
Difficulty Expert
Time Estimate 1 month+
Main Programming Language C/C++ (ESP-IDF)
Alternative Programming Languages Arduino (limited), Rust
Coolness Level Very High
Business Potential High (portable tools, field devices)
Prerequisites FreeRTOS, embedded UI, memory management
Key Topics app lifecycle, shared services, memory discipline, event bus

1. Learning Objectives

By completing this project, you will:

  1. Design a launcher that loads and runs apps without reboot.
  2. Implement a service registry for shared peripherals.
  3. Define app lifecycle APIs (init/run/exit) and enforce them.
  4. Build a stable UI framework for multiple apps.
  5. Implement crash recovery and safe fallback behavior.

2. All Theory Needed (Per-Concept Breakdown)

2.1 App Lifecycle and Service Registry Design

Fundamentals

A mini-OS is about structure: apps must be registered, started, paused, and stopped in a consistent way. This requires a defined lifecycle API that each app implements, and a launcher that enforces that API. Shared services (display, input, WiFi, storage) should be centralized in a registry so that apps do not directly manipulate hardware. This prevents resource conflicts and makes it possible to switch apps without reboot.

Deep Dive into the concept

An embedded app lifecycle can be simple but must be explicit. A common pattern is init() (allocate resources), enter() (become active), loop() (run periodically), and exit() (release resources). The launcher owns the main event loop and calls the active app’s loop() each tick. When switching apps, the launcher calls exit() on the current app, then init()/enter() on the next. This ensures each app has a chance to clean up.

A service registry is a global struct that holds pointers to shared services: display driver, input manager, storage manager, WiFi manager. Apps receive a pointer to this registry rather than accessing globals. This is a form of dependency injection and makes apps testable. It also allows the launcher to arbitrate resources: for example, if an app requests WiFi while another has it, the launcher can deny or pause the other. This is critical in a multi-tool device.

The registry should include capabilities and state flags. An app can query whether the SD card is mounted or whether WiFi is available. This avoids hard-coded assumptions. To keep things safe, the registry should provide wrapper functions rather than raw driver handles. For example, instead of giving direct SPI access, provide display_draw_rect() or storage_write() API. This lets the launcher enforce timing and prevent a rogue app from blocking the system.

How this fits in projects

The service registry model is the backbone for the capstone P08-complete-cardputer-security-toolkit.md, and it borrows concurrency discipline from earlier projects.

Definitions & key terms

  • Lifecycle API → defined functions for app init/run/exit
  • Service registry → centralized access to shared peripherals
  • Arbitration → deciding which app controls a resource
  • Dependency injection → passing service references instead of globals

Mental model diagram (ASCII)

[Launcher] -> [Service Registry] <- [App A]
     |                              [App B]
     +--> [Lifecycle Calls]

How it works (step-by-step, with invariants and failure modes)

  1. Launcher initializes shared services.
  2. Launcher builds app table with lifecycle function pointers.
  3. On selection, launcher calls app.init() then app.enter().
  4. Launcher calls app.loop() each tick.
  5. On exit, launcher calls app.exit() and returns to menu.

Invariants: only one active app at a time; services accessed through registry. Failure modes: apps not releasing resources; blocking loops that freeze launcher.

Minimal concrete example

typedef void (*app_fn_t)(services_t *svc);

Common misconceptions

  • “Apps can just run forever.” → Without exit, you can’t switch.
  • “Global variables are fine.” → They cause conflicts and leaks.
  • “Rebooting is easiest.” → It hides lifecycle bugs.

Check-your-understanding questions

  1. Why use a service registry instead of global drivers?
  2. What happens if an app never returns from loop()?
  3. Why is exit() important even if you reboot?

Check-your-understanding answers

  1. It centralizes access and enables arbitration.
  2. The launcher freezes and you can’t switch apps.
  3. It reveals resource leaks and ensures stability.

Real-world applications

  • Multi-tool embedded devices.
  • Portable diagnostic tools.

Where you’ll apply it

  • This project: see §4.2 and §5.10.
  • Also used in: P08-complete-cardputer-security-toolkit.md.

References

  • Operating Systems: Three Easy Pieces – process lifecycle concepts.
  • Embedded UI frameworks and service locator patterns.

Key insight

A mini-OS is mostly about disciplined boundaries: define the contract and enforce it.

Summary

Define lifecycle APIs, centralize shared services, and control resource ownership for safe app switching.

Homework/Exercises to practice the concept

  1. Define a minimal app interface and implement a “Hello” app.
  2. Add a registry function for display and enforce usage.

Solutions to the homework/exercises

  1. Implement init/loop/exit functions and register them.
  2. Expose svc->display_draw_text() and remove direct driver calls.

2.2 Memory Management and Fragmentation Control

Fundamentals

Embedded systems have limited RAM. A launcher that runs multiple apps must avoid memory fragmentation and leaks. Each app should allocate memory in init and free in exit, or use static buffers. If you allocate and free unpredictably, you will eventually fragment the heap and crash. Therefore, memory discipline is a core concept for a mini-OS.

Deep Dive into the concept

ESP32-S3 has multiple memory regions with different capabilities. Some are DMA-capable, others are not. If you allocate large buffers for graphics or audio, you must choose the right region. Over time, repeated allocations with varying sizes create fragmentation, leaving small unusable holes. To avoid this, use fixed-size pools for common allocations, or allocate once and reuse. For example, the display buffer should be allocated at boot and reused by all apps.

You can also implement per-app memory limits by tracking allocations. A simple approach is to wrap malloc and record allocations by app ID. On app exit, verify all allocations are freed. This creates a “leak detector” and helps maintain stability. If an app exceeds its memory budget, the launcher can reject it or force it to exit. This adds robustness and prevents one app from starving others.

Memory monitoring should be visible in the UI or logs. Display free heap, largest free block, and DMA heap. This helps you diagnose fragmentation early. When memory is low, degrade gracefully by disabling non-essential features (e.g., reduce UI animations). This ensures the launcher remains responsive even under pressure.

How this fits in projects

Memory discipline is critical to the capstone P08-complete-cardputer-security-toolkit.md, where multiple heavy modules coexist.

Definitions & key terms

  • Fragmentation → heap broken into unusable small blocks
  • Pool allocator → fixed-size block allocation
  • Heap telemetry → tracking free and largest blocks
  • Leak detection → tracking allocations per app

Mental model diagram (ASCII)

[Heap] -> [App A allocations]
       -> [App B allocations]
       -> [Shared Buffers]

How it works (step-by-step, with invariants and failure modes)

  1. Allocate shared buffers at boot.
  2. Track per-app allocations via wrappers.
  3. On app exit, verify all memory freed.
  4. If leaks detected, log and reset app.

Invariants: shared buffers allocated once; apps free their memory. Failure modes: fragmentation grows, allocations fail mid-run.

Minimal concrete example

void* app_malloc(app_id, size_t n) { track_alloc(app_id, n); return malloc(n); }

Common misconceptions

  • “ESP32 has plenty of RAM.” → It doesn’t for multi-app systems.
  • “Fragmentation is rare.” → It accumulates with varied allocations.
  • “Leaks don’t matter.” → In long-running systems, they do.

Check-your-understanding questions

  1. Why allocate shared buffers at boot?
  2. How do pool allocators reduce fragmentation?
  3. What telemetry helps detect fragmentation?

Check-your-understanding answers

  1. To avoid repeated allocations and ensure DMA-capable buffers.
  2. Fixed-size blocks reduce variable-size holes.
  3. Free heap size and largest free block size.

Real-world applications

  • Long-lived embedded devices and kiosks.
  • Battery-powered field tools.

Where you’ll apply it

  • This project: see §5.10 Phase 2 and §7.1.
  • Also used in: P08-complete-cardputer-security-toolkit.md.

References

  • Effective C – memory safety chapters.
  • ESP-IDF heap debugging tools.

Key insight

Memory fragmentation is slow poison; catch it early or it will kill stability.

Summary

Use fixed allocations, track per-app memory, and expose heap telemetry to keep the mini-OS stable.

Homework/Exercises to practice the concept

  1. Implement a wrapper allocator that tracks allocations per app.
  2. Add a UI screen showing free heap and largest block.

Solutions to the homework/exercises

  1. Use a hash table keyed by pointer to store app ID and size.
  2. Call heap_caps_get_largest_free_block() and display the result.

2.3 Event Loops, Message Buses, and UI Framework

Fundamentals

A mini-OS needs a consistent way for apps to communicate with shared services and with the launcher. An event loop processes input and timers, dispatching events to the active app. A message bus allows apps and services to publish events without tight coupling. The UI framework provides widgets or drawing helpers to keep apps consistent.

Deep Dive into the concept

An event loop is the heartbeat of the system. Each tick, it polls input (keyboard, buttons), handles timers, and calls the active app’s loop() function with an event queue. This decouples app logic from hardware interrupts. Events can include key presses, timer ticks, or service status changes. The app can then act on events, update its state, and render its UI. This makes app behavior deterministic and testable.

A message bus is useful when multiple components need to react to the same event. For example, when WiFi connects, the launcher may want to update a status bar, while the active app may want to start a scan. Instead of direct calls, the WiFi manager publishes a WIFI_CONNECTED event that listeners can subscribe to. This reduces coupling and simplifies extensions.

UI frameworks on embedded devices should be lightweight. You can define basic widgets (list, menu, status bar) as helper functions. Each app can use these to build screens without re-implementing layouts. Consistency improves usability and reduces bugs. The framework should also handle input focus and navigation. For example, arrow keys move selection; OK selects; Back returns. These conventions make the device feel like a cohesive OS.

How this fits in projects

This event-driven architecture is applied across the capstone P08-complete-cardputer-security-toolkit.md and can integrate tools from previous projects as apps.

Definitions & key terms

  • Event loop → main loop handling input and timers
  • Message bus → publish/subscribe system for events
  • Widget → reusable UI component
  • Focus → current UI element receiving input

Mental model diagram (ASCII)

[Input] -> [Event Queue] -> [App Loop]
    ^             |
    |             v
[Services] -> [Event Bus] -> [UI]

How it works (step-by-step, with invariants and failure modes)

  1. Input manager generates events.
  2. Event loop pushes events to active app.
  3. App processes events and updates state.
  4. UI renderer draws based on state.

Invariants: events processed in order; app loop returns quickly. Failure modes: event backlog causes lag; app blocks and freezes UI.

Minimal concrete example

app->loop(app, event_queue_pop());

Common misconceptions

  • “Polling is enough.” → Event-based design is more scalable.
  • “Apps can draw directly anytime.” → Use consistent UI helpers.
  • “Message bus is overkill.” → It simplifies cross-cutting events.

Check-your-understanding questions

  1. Why should app loop return quickly?
  2. What is the benefit of an event bus?
  3. How do widgets improve consistency?

Check-your-understanding answers

  1. To keep the launcher responsive and allow task scheduling.
  2. Decouples producers and consumers of events.
  3. Apps share UI patterns and avoid re-implementing layouts.

Real-world applications

  • Embedded dashboards and handheld tools.
  • Simple operating environments for devices without MMUs.

Where you’ll apply it

  • This project: see §4.1 architecture and §5.10 Phase 3.
  • Also used in: P08-complete-cardputer-security-toolkit.md.

References

  • Event-driven programming patterns in embedded UI systems.

Key insight

A good event loop keeps the OS responsive and prevents one app from dominating the device.

Summary

Use an event loop and lightweight UI framework to keep apps consistent, responsive, and decoupled.

Homework/Exercises to practice the concept

  1. Implement a menu widget with selection and enter/back events.
  2. Add a simple event bus with subscribe/publish.

Solutions to the homework/exercises

  1. Track index and draw list; update on key events.
  2. Use a list of callbacks and invoke on publish.

3. Project Specification

3.1 What You Will Build

A mini-OS launcher that:

  • boots into a menu,
  • runs apps without reboot,
  • manages shared services,
  • provides consistent UI patterns.

3.2 Functional Requirements

  1. Launcher menu: list of apps with navigation.
  2. App lifecycle: init/loop/exit API enforced.
  3. Service registry: shared access to display, input, storage.
  4. Resource arbitration: prevent conflicts.
  5. Crash recovery: return to launcher on app failure.

3.3 Non-Functional Requirements

  • Performance: launcher responsive under load.
  • Reliability: no memory leaks across app switches.
  • Usability: consistent navigation across apps.

3.4 Example Usage / Output

1) Boot -> menu
2) Select “WiFi Tools” -> app runs
3) Press Back -> return to menu

3.5 Data Formats / Schemas / Protocols

  • App registry table (name, version, lifecycle pointers).
  • Settings storage in NVS or config file.

3.6 Edge Cases

  • App crashes in loop → launcher recovers.
  • SD missing → storage service reports error.
  • WiFi requested by two apps → arbitration denies one.

3.7 Real World Outcome

A successful build shows a fast boot menu, stable app switching, and shared services that persist across apps without conflicts.

3.7.1 How to Run (Copy/Paste)

idf.py set-target esp32s3
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor

3.7.2 Golden Path Demo (Deterministic)

  • Boot, launch App A, return, launch App B, repeat 20 times.
  • Expect no memory leaks or crashes.

Failure demo (deterministic):

  • Run a test app that intentionally blocks for 5 seconds. Expected: watchdog triggers, launcher returns to menu, and a crash log is written. Exit code: 2.

3.7.3 If CLI: exact terminal transcript

I (6100) os: boot menu
I (6101) os: app=WiFiTools init
I (6102) os: app=WiFiTools exit reason=back
I (6103) os: app=IRRemote init

Exit codes: 0 = success, 2 = app watchdog timeout, 3 = service init failure.

3.7.4 If Web App

Not applicable.

3.7.5 If API

Not applicable.

3.7.6 If Library

Not applicable.

3.7.7 If GUI / Desktop / Mobile

Not applicable.

3.7.8 If TUI

+----------------------------+
| CARDPUTER OS               |
| [1] WiFi Tools             |
| [2] IR Remote              |
| [3] Spectrum               |
| [4] Settings               |
+----------------------------+

4. Solution Architecture

4.1 High-Level Design

[Boot] -> [Launcher] -> [App Manager] -> [Active App]
              |
              v
        [Service Registry]

4.2 Key Components

Component Responsibility Key Decisions
Launcher Menu + app switching fast boot, stable UI
App manager lifecycle control enforce init/exit
Services shared peripherals registry abstraction
Event loop input/timers non-blocking loop

4.3 Data Structures (No Full Code)

typedef struct {
    const char *name;
    void (*init)(services_t*);
    void (*loop)(services_t*);
    void (*exit)(services_t*);
} app_t;

4.4 Algorithm Overview

Key Algorithm: App Switch

  1. Call current app exit().
  2. Reset per-app state.
  3. Call new app init().
  4. Enter loop.

Complexity Analysis:

  • Time: O(1) per switch
  • Space: O(1) per app

5. Implementation Guide

5.1 Development Environment Setup

idf.py set-target esp32s3
idf.py build

5.2 Project Structure

project-root/
├── main/
│   ├── launcher.c
│   ├── app_manager.c
│   ├── services.c
│   ├── ui_widgets.c
│   └── event_loop.c
└── README.md

5.3 The Core Question You’re Answering

“How do I build a stable multi-app platform on a microcontroller?”

5.4 Concepts You Must Understand First

  1. Lifecycle APIs and service registry.
  2. Memory allocation discipline.
  3. Event loops and UI consistency.

5.5 Questions to Guide Your Design

  1. What is the minimum app API?
  2. How will you enforce cleanup on exit?
  3. How do you handle app crashes?

5.6 Thinking Exercise

Design a table of apps and define the lifecycle functions each must implement.

5.7 The Interview Questions They Will Ask

  1. How do you prevent resource conflicts between apps?
  2. What strategies reduce memory fragmentation?
  3. How do you design an event loop?

5.8 Hints in Layers

Hint 1: Start with two static apps and a simple menu.

Hint 2: Add a service registry for display and input.

Hint 3: Add crash recovery to return to menu.

5.9 Books That Will Help

Topic Book Chapter
OS concepts OSTEP Scheduling & Concurrency
Embedded design Making Embedded Systems Ch. 9

5.10 Implementation Phases

Phase 1: Launcher + Menu (1 week)

Phase 2: App lifecycle + services (1–2 weeks)

Phase 3: Stability + crash recovery (1 week)

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
App API init/loop/exit yes simple and clear
Resource control direct, registry registry prevents conflicts
Crash handling reboot, fallback fallback faster recovery

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests App API calls init/exit correctness
Integration Tests switch loops 50 switches
Stress Tests memory use heap telemetry

6.2 Critical Test Cases

  1. App switch 50 times without leak.
  2. App loop hang triggers watchdog and returns to menu.
  3. Service registry denies conflicting WiFi requests.

6.3 Test Data

Synthetic app that allocates memory and frees on exit

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Apps using globals resource conflicts enforce registry
No cleanup memory leaks track allocations
Blocking loop frozen UI enforce timeouts

7.2 Debugging Strategies

  • Add a watchdog that returns to menu on stall.
  • Log heap usage on app switch.

7.3 Performance Traps

  • Excessive UI redraws across multiple apps.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add app icons in menu.

8.2 Intermediate Extensions

  • Add app metadata loaded from SD.

8.3 Advanced Extensions

  • Add simple app sandboxing via MPU-style limits.

9. Real-World Connections

9.1 Industry Applications

  • Multi-tool embedded devices (testers, meters).
  • Industrial handhelds.
  • LVGL apps and launcher patterns.

9.3 Interview Relevance

  • Lifecycle management, resource arbitration, and stability design.

10. Resources

10.1 Essential Reading

  • Operating Systems: Three Easy Pieces – scheduling and processes.

10.2 Video Resources

  • Event-driven UI architecture talks.

10.3 Tools & Documentation

  • ESP-IDF task watchdog documentation.
  • P08-complete-cardputer-security-toolkit.md

11. Self-Assessment Checklist

11.1 Understanding

  • I can describe app lifecycle APIs.
  • I can explain service registry benefits.

11.2 Implementation

  • Apps switch without reboot.
  • Shared services prevent conflicts.

11.3 Growth

  • I can explain how to recover from app crashes.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Launcher menu and two apps with init/loop/exit.

Full Completion:

  • Shared services, crash recovery, stable switching.

Excellence (Going Above & Beyond):

  • Load apps dynamically from SD and support metadata.