Skip to main content

Overview

The lib/ui library is a pure rendering layer that draws OSSM screens into a U8g2 buffer. It has no hardware dependencies, no FreeRTOS, no mutexes — it takes a u8g2_t* pointer and draws pixels. Thread safety and display I/O are handled by the display service. This separation means the library can run on native (x86) for automated visual testing without an ESP32. Dependencies: U8g2, QRCode Source: Software/lib/ui/src/

TextPage

TextPage is the primary struct for content-driven screens. It covers help pages, error screens, update status, WiFi setup, pairing — anything that’s mostly text with optional QR codes.
struct TextPage {
    const char* title      = nullptr;
    const char* subtitle   = nullptr;
    const char* body       = nullptr;
    const char* bottomText = nullptr;
    const char* qrUrl      = nullptr;
    uint8_t qrVersion      = 3;
    int qrScale            = 2;
    bool centerBody        = false;
    int scrollPercent      = -1;
};
All fields are optional. Set only what you need.
FieldTypePurpose
titleconst char*Bold text at the top, with a separator line below
subtitleconst char*Medium or bold text below the title; auto-splits if too wide
bodyconst char*Word-wrapped text; supports \n line breaks
bottomTextconst char*Pinned to the bottom of the screen (y=62)
qrUrlconst char*QR code rendered bottom-right; constrains text width
qrVersionuint8_tQR version (default 3)
qrScaleintQR pixel scale (default 2)
centerBodyboolCenter-align body text
scrollPercentintShow scroll indicator (0–100); -1 to hide

Rendering order

drawTextPage() renders in this order:
  1. Clear the full screen (header + page + footer)
  2. QR code — bottom-right corner, reduces available text width
  3. Title — bold font, followed by a horizontal separator line
  4. Subtitle — tries medium font first; falls back to bold; splits across two lines if still too wide
  5. Body — word-wrapped with drawWrappedText() if a title is present; centered title-style (drawStr::title) if body is the only field
  6. Bottom text — fixed at y=62
  7. Scroll indicator — right-edge scrollbar if scrollPercent >= 0

Usage

// Use a predefined page
ui::drawTextPage(&u8g2, ui::pages::helpPage);

// Or build one inline
ui::TextPage page;
page.title = "Custom Title";
page.body = "Some content\nwith line breaks";
page.scrollPercent = 50;
ui::drawTextPage(&u8g2, page);

Predefined pages

TextPages.h defines static TextPage instances in the ui::pages namespace. These reference strings from Strings.h:
PageFields used
helpPagetitle, body, bottomText, qrUrl
updateCheckingPagebody
noUpdatePagebody, bottomText
updatingPagetitle, body
errorPagetitle
wifiDisconnectedPagetitle, body, bottomText, qrUrl
wifiConnectedPagetitle, body, bottomText
pairingPagetitle, body
Example definition:
static const TextPage helpPage = {
    .title = helpTitle,
    .body = helpBody,
    .bottomText = helpBottom,
    .qrUrl = helpQr,
};

Strings

All UI strings live in Strings.h under the ui::strings namespace. They are stored in flash using PROGMEM:
static const char helpTitle[] PROGMEM = "Get Help";
static const char helpBody[] PROGMEM = "On Discord,\nor GitHub";
Progmem.h defines PROGMEM as a no-op on non-Arduino platforms, so strings compile on both ESP32 and native test builds.

Categories

CategoryExamples
BrandingresearchAndDesire, kinkyMakers
General UIerror, homing, idle, restart, settings, skip
Menu / modessimplePenetration, strokeEngine, streaming, pairing, update, wifi
Play controlsspeed, stroke, sensation, depth, buffer, accel, max
WarningsspeedWarning, homingTookTooLong, strokeTooShort
HelphelpTitle, helpBody, helpBottom, helpQr
UpdateupdateChecking, noUpdateBody, updatingTitle, updatingBody
WiFiwifiSetup, wifiBody, wifiBottom, wifiQr, wifiConnected
PairingpairingTitle, pairingBody
PatternspatternName0patternName6, patternDesc0patternDesc6

Pattern arrays

Stroke engine pattern names and descriptions are indexed arrays:
static const char* const strokeEngineNames[7] = {
    patternName0, patternName1, patternName2,
    patternName3, patternName4, patternName5,
    patternName6,
};

static const char* const strokeEngineDescriptions[7] = {
    patternDesc0, patternDesc1, patternDesc2,
    patternDesc3, patternDesc4, patternDesc5,
    patternDesc6,
};
MenuItems.h maps Menu:: enum values to display strings:
static const char* menuStrings[Menu::NUM_OPTIONS] = {
    ui::strings::simplePenetration, ui::strings::strokeEngine,
    ui::strings::streaming,         ui::strings::pairing,
    ui::strings::update,            ui::strings::wifiSetup,
    ui::strings::helpTitle,         ui::strings::restart,
};

Images and icons

Icons (font glyphs)

Status icons use the Siji icon font. Glyphs are defined in DisplayConstants.h:
namespace IconGlyph {
    constexpr uint16_t WIFI_OFF        = 0xe218;
    constexpr uint16_t WIFI_CONNECTING = 0xe219;
    constexpr uint16_t WIFI_CONNECTED  = 0xe21a;
    constexpr uint16_t WIFI_ERROR      = 0xe21b;
    constexpr uint16_t BLE_CONNECTED   = 0xe00b;
    constexpr uint16_t BLE_SMALL       = 0xe0b0;
    constexpr uint16_t EXCLAMATION     = 0xe0b3;
}
drawHeaderIcons() renders WiFi and BLE status in the top-right corner of the screen. Error states overlay additional pixels (exclamation marks) on the base glyph.

Logos (XBM bitmaps)

Logos are PROGMEM XBM byte arrays in Logos.h:
  • RDLogo — Research & Desire, 57x50 pixels
  • KMLogo — Kinky Makers, 50x50 pixels
They are drawn via the LogoData struct and drawLogo():
struct LogoData {
    const char* title;
    const uint8_t* bitmap;
    int w, h, x, y;
};

Hello animation

HelloAnimation.h contains precomputed Y-position frames for the “OSSM” boot animation. Each frame specifies per-letter Y offsets:
struct HelloFrame {
    int heights[4];  // Y position for O, S, S, M
};
drawHelloFrame() renders a single frame. The test suite stitches all frames into a GIF.

Other draw functions

The full rendering API (ui.h):
FunctionPurpose
drawTextPage()Render a TextPage struct
drawMenu()Scrollable menu with selection highlight
drawPlayControls()Speed/stroke/sensation bars for play modes
drawPreflight()Pre-play safety check (speed knob at zero)
drawHeaderIcons()WiFi + BLE status icons
drawQR()Standalone QR code
drawHelloFrame()Single boot animation frame
drawLogo()XBM logo with title
Helper namespaces in DrawExtensions.h:
  • drawStr::centered() — horizontally centered text
  • drawStr::multiLine() — word-wrapped multi-line text with UTF-8 support
  • drawStr::title() — bold centered text at fixed position
  • drawShape::scroll() — scrollbar indicator
  • drawShape::settingBar() — labeled vertical bar with fill level
  • drawShape::settingBarSmall() — compact vertical bar (no label)

Testing and visual output

The display library has native tests that render every screen variant and export images for visual review.

Running tests

pio test -e test -f test_display
ImageMagick (magick) must be installed for PNG conversion. Without it, PBM files are still generated but PNGs are skipped.

How it works

  1. Tests create a software-only U8g2 SSD1306 128x64 display (no hardware, no-op I2C)
  2. Each test calls ui::draw*() functions to render into the buffer
  3. savePBM() writes the buffer as a PBM P4 bitmap
  4. After all tests complete, ImageMagick converts PBM to PNG (inverted colors, 400% scale)
  5. Hello animation frames are stitched into a GIF

Output structure

test/test_display/_output/
├── pbm/                        # Raw bitmaps (generated by tests)
│   ├── textpage/
│   │   ├── combos/             # Field combinations (title+body, all fields, etc.)
│   │   ├── scroll/             # Scroll positions (0%, 50%, 100%)
│   │   ├── stress/             # Edge cases (UTF-8, long strings)
│   │   ├── qr/                 # QR code variants
│   │   ├── body/               # Body rendering with/without title
│   │   ├── subtitle/           # Subtitle font sizing
│   │   ├── pages/              # All predefined pages
│   │   └── patterns/           # Stroke engine pattern descriptions
│   ├── header_icons/           # WiFi × BLE status combinations
│   ├── logos/                  # RD logo, KM logo
│   ├── hello/                  # Animation frames
│   ├── menu/                   # Menu rendering variants
│   └── play_controls/          # Preflight, play modes
└── png/                        # Same structure, converted to PNG

Test coverage

Test fileWhat it covers
test_textpage.cppTextPage field combinations, stress tests, QR, subtitle overflow, body layout
test_pages.cppAll predefined pages, stroke engine pattern descriptions
test_scroll.cppScroll bar at boundary and edge-case values
test_header_icons.cppAll 20 WiFi × BLE status combinations
test_logos.cppRD logo, KM logo
test_hello.cppAll hello animation frames, GIF generation
test_menu.cppMenu with various item counts, long text, wrapping
test_play_controls.cppPreflight, simple/stroke engine/streaming play controls