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.
| 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.
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-devTwo 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.
./scripts/setup.sh # clone QEMU, graft the board on, build
./scripts/build-image.sh /path/to/2.2.53.lsi
./scripts/run.sh # boot itsetup.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.shTo capture what the device says to a file instead of your speakers:
AUDIO_DRV='driver=wav,path=/tmp/braille.wav' ./scripts/run.shNo 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.ps1setup-windows.ps1 copies the MinGW runtime DLLs next to the binary, so the
build directory is self-contained and can be moved.
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'scripts/setup.sh applies these automatically; they are called out because they
patch the QEMU tree:
- Symlinks. QEMU's configure builds a
qemu-bundleview of the install tree out of symlinks, which Windows refuses without Developer Mode or Administrator rights — and the failure abortsmeson setupentirely. 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 theqemu-system-arm.exetarget.
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.
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 / WSLThe 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.2Verified 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.
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 aboveThe 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.
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 |
|---|---|
1–9, 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.
-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.
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.
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-launchfails (it tries to autolaunch through X11), so the launcher repeatsGConf 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
.soextension 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.
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.
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.