Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bpemu — an emulator for the APH Braille+ Mobile Manager

Boots the real, unmodified Braille+ / LevelStar Icon firmware in QEMU, with working audio.

The Braille+ Mobile Manager (sold by the American Printing House for the Blind, built by LevelStar as the "Icon") is a PXA270 handheld whose entire user interface is spoken. This adds an everest machine type to QEMU that models the board well enough for the stock .lsi firmware image to boot from its own OneNAND flash, all the way to the Python application launcher, with speech and sound effects coming out of your speakers.

Machine: LevelStar Icon (Everest Board)
everest_init...
Everest battery driver loaded.
Everest motor driver loaded.
Muxed OneNAND 128MB 1.8V 16-bit (0x30)
4 cmdlinepart partitions found on MTD device <NULL>
Everest Keypad driver loadee.
ALSA device list:
  #0: Everest (WM9713)
VFS: Mounted root (jffs2 filesystem) on device 31:3.
INIT: version 2.86 booting
...
OpenedHand Linux (Poky) 3.1 everest ttyS0
everest login:

Nothing in the guest is patched: the shipped kernel.bin and JFFS2 root.bin run as they were flashed.

What works

Status
Boot to userspace and a login shell ✅ ~30 s under TCG once configured
OneNAND boot flash, 4 MTD partitions, JFFS2 root read-write
Audio output (AC97 + WM9713) ✅ new device models
Python application launcher starts
Serial console, SSH, Samba ✅ as far as the firmware takes them
Native Windows build (no WSL) ✅ self-contained qemu-system-arm.exe
Keypad input, including chorded braille ✅ see Keys
Wired networking ✅ emulated RTL8150, see Networking
Battery gauge ✅ via the WM9713 digitiser
WiFi and Bluetooth ❌ proprietary combo chip, cannot be emulated
Vibration motor ❌ driver loads, hardware unmodelled
Audio capture ❌ reads silence

QEMU has no PXA2xx AC97 controller and no WM9713 codec, so this repo adds both (qemu/hw/audio/pxa2xx_ac97.c). On a device where every interaction is spoken, that is the difference between an emulator and a curiosity.

Requirements

Linux, or Windows — either natively (see below) or through WSL2. For Linux/WSL you need a C toolchain and QEMU's build dependencies:

sudo apt install git build-essential ninja-build pkg-config \
    libglib2.0-dev libpixman-1-dev python3-venv zlib1g-dev \
    libpulse-dev libgtk-3-dev

Two more are optional: libasound2-dev adds ALSA alongside PulseAudio, and libsdl2-dev gets you an SDL window instead of a GTK one. setup.sh probes for both and builds with whatever it finds, and run.sh picks a display backend the binary actually has, so their absence costs you an option rather than the build.

QEMU 9.1 needs neither flex, bison nor libslirp-dev for this target: slirp is built from a bundled subproject, and the lexer and parser generators are not reached building arm-softmmu.

You also need a firmware bundle — an .lsi file such as 2.2.53.lsi. This repo does not redistribute one.

Quick start

./scripts/setup.sh                      # clone QEMU, graft the board on, build
./scripts/build-image.sh /path/to/2.2.53.lsi
./scripts/run.sh                        # boot it

setup.sh is safe to re-run; it re-applies the board sources and rebuilds. Log in as root (no password).

The first boot of a freshly built image is slow — several minutes, because the firmware runs its one-time ipkg-cl configure pass, rebuilds the MIME database and generates SSH host keys. That state is written back to the JFFS2 root, so later boots of the same flash.img reach a login prompt in well under a minute. Keep the image around rather than rebuilding it each time, and use QEMU snapshots (savevm/loadvm from the monitor) to skip the boot entirely.

To boot without audio, or with a different backend:

AUDIO_DRV=alsa ./scripts/run.sh
AUDIO_DRV=none ./scripts/run.sh

To capture what the device says to a file instead of your speakers:

AUDIO_DRV='driver=wav,path=/tmp/braille.wav' ./scripts/run.sh

Running natively on Windows

No WSL needed. The build uses MSYS2's MinGW-w64 toolchain and produces a plain qemu-system-arm.exe that plays audio through Windows directly.

scoop install msys2          # or install from msys2.org
.\scripts\setup-windows.ps1  # installs deps, builds, bundles runtime DLLs
python tools\bpimage.py unpack C:\path\to\2.2.53.lsi build\parts
python tools\bpimage.py mkflash build\parts build\flash.img
.\scripts\run.ps1

setup-windows.ps1 copies the MinGW runtime DLLs next to the binary, so the build directory is self-contained and can be moved.

Audio backends on Windows

QEMU has no WASAPI backend — its audio drivers are alsa, dbus, dsound, jack, oss, pa, pipewire, sdl, sndio and wav. Both Windows options reach WASAPI anyway:

-AudioDrv Path Notes
dsound (default) DirectSound, which Windows implements over WASAPI in user mode Most battle-tested in QEMU
sdl SDL2, whose Windows audio backend is WASAPI Direct, usually lower latency
.\scripts\run.ps1 -AudioDrv sdl
.\scripts\run.ps1 -AudioDrv 'driver=wav,path=C:\out.wav'

Two Windows-specific build fixes

scripts/setup.sh applies these automatically; they are called out because they patch the QEMU tree:

  • Symlinks. QEMU's configure builds a qemu-bundle view of the install tree out of symlinks, which Windows refuses without Developer Mode or Administrator rights — and the failure aborts meson setup entirely. Upstream's answer is to enable Developer Mode. Rather than require that, the setup script teaches the step to copy instead, skipping entries whose targets are build outputs that do not exist yet at configure time (a symlink may dangle; a copy may not).
  • Tests. QEMU 9.1's test binaries do not link against current mingw-w64 runtimes (undefined reference to qemu_ftruncate64), so the Windows build builds only the qemu-system-arm.exe target.

Driving it non-interactively

tools/autoboot.py boots the machine, waits for the login prompt, and runs shell commands — useful as a smoke test or for poking at the firmware without sitting through a boot:

python3 tools/autoboot.py \
    --kernel build/parts/kernel.bin --flash build/flash.img \
    --audio 'driver=wav,path=/tmp/out.wav' \
    -c 'aplay -l' \
    -c 'cat /proc/mtd' \
    -c 'dd if=/dev/urandom bs=1024 count=200 | aplay -f cd -'

That last command is the end-to-end audio test: it should exit 0 and leave about 200 KiB of 44.1 kHz stereo PCM in /tmp/out.wav. Expect the capture to come up ~10 KiB short of what you sent — that is the audio still in flight in the FIFO when QEMU is killed, not lost samples. Add a trailing -c 'sleep 2' if you want it flushed.

Note the guest is BusyBox 1.2.1 from 2010; its head has no -N form and many other options you would reach for are absent.

Networking

The device's own WiFi and Bluetooth are a proprietary Stonestreet One BGW200 combo chip on SPI, driven by a binary-only module, and QEMU dropped its Bluetooth stack in 5.0 — neither can be modelled. The firmware does, however, carry a driver for the Realtek RTL8150 USB ethernet chip, and the PXA270's OHCI controller is already wired up, so qemu/hw/usb/dev-rtl8150.c gives the guest a working network with no firmware changes:

.\scripts\run.ps1 -Net          # Windows
NET=1 ./scripts/run.sh          # Linux / WSL

The firmware never expected a wired adapter, so it does not configure one. From the device's shell:

ifconfig eth0 10.0.2.15 netmask 255.255.255.0 up
route add default gw 10.0.2.2

Verified with ping at 56, 82 and 1400 byte payloads. The docs record the three traps in the device model, of which the interesting one is that a bulk transfer is delivered a packet at a time rather than a frame at a time.

Building a standalone package

To give the emulator to someone who has no build tools — no MSYS2, no WSL, no Python:

.\scripts\setup-windows.ps1     # produces the binary
.\scripts\make-dist.ps1 -Zip    # assembles dist\BraillePlusEmulator[.zip]
.\scripts\test-dist.ps1         # proves it runs without any of the above

The result is a folder with the emulator, its DLLs, the firmware images, a double-clickable BraillePlus.bat and a plain-language README. The 60GB drive is created on first run with the bundled qemu-img.exe, so nothing large ships inside it.

test-dist.ps1 is the part worth keeping: it copies the package outside the repo, strips PATH down to the Windows directories, and boots it both headless and with the default window and sound. That is the only reliable way to catch a dependency that resolves on the build machine and nowhere else.

Pass -NoFirmware to build a package without the firmware images. That is the form that is unambiguously yours to redistribute — see Licence.

Keys

The device has a 6×7 keypad matrix — a telephone keypad, function and navigation keys, and six braille dot keys. Host keys map onto it as:

Host key Device key
19, 0 (number row or numpad) digit keys
S D F / J K L braille dots 3 2 1 / 4 5 6 (Perkins home row)
Space space
Left Shift / Left Ctrl shift / control
Enter / Esc OK / Cancel
Arrow keys Up / Down / Left / Right
F1 F2 F3 F4 Help / Menu / Info / Select
F5 F6 F7 Prog1 / Prog2 / Program
PgUp / PgDn / M / R Volume up / down / Mute / Record
Numpad * / . * / #

Chords are assembled by the driver, so pressing dot keys together types braille: dot 1 alone produces a, dot 3 alone produces an apostrophe.

Space, shift and control live in a sixth matrix row that only exists in the keypad's second keycode array, which is why keypad-id defaults to 1. Select keypad-id=0 and you get the first array, which has no space bar, no shift and no control at all.

Type into the QEMU window, not the terminal. run.sh / run.ps1 open an SDL or GTK window (whichever the binary was built with) that stays black — the device has no screen, so the guest never initialises a framebuffer — but that window is what captures keystrokes and feeds them to the emulated keypad. The terminal you launched from is the device's serial console (a root shell), which is a different input path. Pass -Display none / DISPLAY_BACKEND=none for a headless run.

The key lock switch must be released or the keypad is dead — that is real device behaviour, not an emulator quirk, and it silently drops every keypress. The board releases it by default; -M everest,key-lock=on engages it.

Machine options

-M everest,board-id=N,keypad-id=N

Both are hardware straps the firmware reads from GPIO and exposes under /proc/everest/. board_id defaults to 2 and is not cosmetic — the board setup code branches on board_id > 1. The values a real unit straps are not recoverable from firmware, so they are exposed as knobs.

RAM defaults to 64 MiB (-m). See docs/everest-board.md for why that, and the 128 MiB flash size, are assumptions rather than measurements.

Layout

qemu/hw/arm/everest.c              the board
qemu/hw/audio/pxa2xx_ac97.c        PXA27x AC97 controller + WM9713 codec
qemu/include/hw/audio/pxa2xx_ac97.h
qemu/hw/usb/dev-rtl8150.c          RTL8150 USB ethernet adapter
scripts/setup.sh                   clone + patch + build QEMU
scripts/setup-windows.ps1          the same, through MSYS2, for a native .exe
scripts/build-image.sh             .lsi -> OneNAND flash image
scripts/run.sh, run.ps1            boot it
scripts/make-dist.ps1              assemble the standalone package
scripts/test-dist.ps1              verify that package on a clean PATH
scripts/dist/                      launcher and README that ship inside it
tools/bpimage.py                   unpack firmware, assemble flash images
tools/autoboot.py                  scripted console driver
docs/everest-board.md              what the hardware is, and how we know

The board sources live here rather than in a QEMU fork; setup.sh copies them into a QEMU checkout and appends the Kconfig/meson entries, idempotently.

QEMU is pinned to v9.1.0, the last release that still carries PXA2xx support — it is deprecated upstream and removed in later versions. That pin is deliberate and is the main long-term maintenance risk for this project.

Limitations

The honest ones, in rough order of how much they matter:

  • The application's own behaviour is largely unexplored. Keys now reach it, but dbus-launch fails (it tries to autolaunch through X11), so the launcher repeats GConf Error: No D-BUS daemon running. How much of the UI works in practice has not been characterised.
  • RAM size, flash capacity and partition sizes are informed assumptions. They come from a bootloader we cannot read. Partition ordering is attested by the firmware itself. See the docs for the derivations.
  • WiFi and Bluetooth cannot be emulated. The BGW200 is a proprietary Stonestreet One combo part on SPI with a binary-only driver, and QEMU removed its Bluetooth stack in 5.0. The RTL8150 adapter is a substitute for wired connectivity, not a reproduction of what the device does.
  • Audio capture returns silence and the vibration motor goes nowhere.
  • The .so extension modules in the firmware are ARM binaries and stay that way — this is full-system emulation, so that is fine, but it is why a native port would be a very different project.

Provenance

Every hardware constant was recovered by static analysis of the shipped firmware — no schematics or vendor documentation. docs/everest-board.md records each finding together with how it was established, so anything wrong is traceable rather than folklore.

Useful companion material, if you have it: the levelstar/ Python tree from a device or SDK bundle is uncompyle6 output of the same application, and is far easier to read than the .pyo files in the firmware.

Licence

The QEMU board and device sources under qemu/ are GPL v2, matching QEMU. make-dist.ps1 copies them into the package's src/ folder, since shipping a QEMU binary carries the obligation to offer the corresponding source.

The firmware is a separate matter. It is the copyrighted work of the American Printing House for the Blind / LevelStar and includes a licensed copy of the Eloquence speech synthesiser. Nothing in this repository grants any right to it, and a package built with the firmware included is only yours to pass on if your rights to that firmware allow it. make-dist.ps1 -NoFirmware builds one that sidesteps the question entirely.

About

An emulator for the APH Braille+ Mobile Manager / LevelStar Icon: a QEMU board that boots the real firmware, speech and all

Resources

Stars

2 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages