Beebium Python Client
What is Beebium?
Beebium is a headless, cycle-accurate BBC Micro emulator with a client-server architecture. The emulator runs as a server process -- one per machine variant (Model B, B+, B+ 128K, B with ROM/RAM board) -- and is driven entirely over gRPC, so front-ends and automation talk to it across a well-defined protocol rather than being wired into the emulation core.
What is the Python client?
beebium is the Python client for that protocol: a typed API over the gRPC
services for controlling and automating emulator instances from Python -- type
on the keyboard, read the screen in any display mode, peek and poke memory,
drive the debugger, mount discs, and more. It is built for pytest-based testing
of BBC Micro software: a bbc fixture gives each test a fresh machine.
Relationship to beebium-server
beebium-server is a separate, optional package: platform wheels that carry
the emulator itself -- the server binaries, the ROMs, the presets and the
bundled extensions. Install both and you have a complete, self-contained
headless system:
pip install beebium beebium-server
With beebium-server present, Beebium.launch() needs no arguments -- it finds
the server and its ROMs in that wheel. Without it, the client works just as well
with a server installed any other way (Homebrew, the .deb/.rpm packages,
Scoop, or a checkout build) or one already running, reached with
Beebium.connect().
Installation
With uv:
uv add beebium beebium-server # add to your project
# or, into a plain virtualenv:
uv pip install beebium beebium-server
With pip:
pip install beebium beebium-server
beebium-server is optional (see above). On an externally-managed system
install into a virtualenv (or use uv/pipx).
Extras
pip install beebium[imaging] # + Pillow, for saving captured frames as images
pip install beebium[discovery] # + zeroconf, for discovering servers over mDNS
Usage
Every example below is a real test that runs against the beebium-server wheel
(see readme/snippets/), so the code here is the code that runs.
Launch a server
The flagship path: with beebium-server installed, launch() takes no
arguments and stops the server again when the block exits.
from beebium.client import Beebium
# With the beebium-server package installed, launch() needs no arguments:
# the emulator binary and its ROMs come from that wheel, and the server is
# stopped again when the block exits.
with Beebium.launch() as bbc:
bbc.expect("BASIC") # wait for the boot banner / BASIC prompt
bbc.keyboard.type("PRINT 2+2")
bbc.keyboard.press_return()
print(bbc.expect("4")) # -> 4
Connect to a running server
from beebium.client import Beebium
# Connect to a server that is already running. The target defaults to
# localhost on port 48875 (0xBEEB); pass "host:port" to reach another.
with Beebium.connect() as bbc:
bbc.debugger.stop()
print(f"PC = 0x{bbc.cpu.pc:04X}")
Choose a variant or a specific server
from beebium.client import Beebium
# Pick a machine variant: "model-b" (default), "model-b-plus",
# "model-b-plus-128k" or "model-b-romram". To run a specific install
# instead of the default search, pass server="/path/to/beebium" (a binary
# or an install root), or set the BEEBIUM_SERVER environment variable.
with Beebium.launch(variant="model-b-plus") as bbc:
bbc.expect("BASIC")
Type on the keyboard
Given a launched or connected bbc:
bbc.keyboard.type("PRINT 2+2") # type text, shifting as needed
bbc.keyboard.press_return()
bbc.keyboard.key_down("A") # or drive individual keys
bbc.keyboard.key_up("A")
Typing needs no wait for boot: the server presses nothing until the guest has
finished resetting and asked the MOS for typed input, so text typed straight
after a launch or a Break is not lost in the reset. A test that wants boot
finished before doing something other than typing calls bbc.wait_until_ready().
Game control, by key_down() and key_up(), is never held. A guest that never
asks the MOS for input (a game scanning the keyboard itself) never becomes
ready, so wait_until_typing_complete() and wait_until_ready() give it
ready_within=5.0 emulated seconds after its reset and then raise; pass a
larger budget, or None, for a boot known to be slow.
Typing is paced in emulated time, 80 ms a character at 2 MHz (12.5 characters
an emulated second) whatever the speed multiplier, so N characters need
N x 0.08 emulated seconds before the last key, a final RETURN say, has gone in.
Rather than budget that, run the machine and call
bbc.keyboard.wait_until_typing_complete(). A reset of any kind -- Break,
Ctrl-Break, bbc.debugger.reset() -- discards the line being typed and
everything queued behind it rather than finishing it in the new session;
typing_status().discarded_by_reset counts what was lost, and
wait_until_typing_complete() raises on it. Type again after the reset for
the new session. A modifier meant as a reset option must stay down for emulated
time after Break is released, so use ctrl_break() and shift_break() rather
than ctrl_down(), break_down(), break_up(), ctrl_up() by hand.
Read the screen
bbc.video.screen_text() reads the displayed text in any mode. For MODE 7
(teletext) work, beebium.client.screen adds helpers -- read_mode7_screen,
screen_contains, find, dump_screen -- that read the screen as displayed,
following hardware scrolling and shadow modes.
# Read the text the machine is displaying, whatever the screen mode.
text = bbc.video.screen_text().text
# Wait (up to a timeout) for text to appear -- pexpect-style automation.
bbc.expect("BASIC")
To compare the display with memory, bbc.video.screen_region() says where the
displayed screen is, as the server computes it from the live 6845, latch and
paging: the region the video reads (main_ram; shadow_ram on a B+ or B+ 128K
in a shadow mode; always main_ram on an Integra-B, whose shadow RAM is the
CPU's), and the address ranges in display order, wrapping as the hardware
does. bbc.memory.region(r.region).peek[start:end] then reads each range.
Read and write memory
# Side-effect-free peek -- safe even for I/O registers.
via_flags = bbc.memory.address.peek[0xFE4D] # System VIA IFR
zero_page = bbc.memory.address.peek[0x0000:0x0100]
# Follow a 6502 vector (16-bit little-endian).
wrchv = bbc.memory.address.peek.word(0x020E) # the OSWRCH vector
# Bus access reads and writes through the memory bus, like real hardware.
bbc.memory.address.bus[0x1900] = 0x42
value = bbc.memory.address.bus[0x1900]
Drive the debugger
state = bbc.debugger.stop() # pause; returns the execution state
print(state.is_running) # -> False
print(bbc.cpu.registers) # A=.. X=.. Y=.. SP=.. PC=.. P=.. [flags]
# Break the next time the OS writes a character, then let it run. The
# breakpoint is removed automatically when the block exits.
with bbc.debugger.breakpoint(0xFFEE): # OSWRCH entry point
bbc.keyboard.type("X") # the echoed key drives OSWRCH
bbc.debugger.run_and_wait_for_stop()
print(f"stopped at PC=0x{bbc.cpu.pc:04X}")
Run state
Whether a call needs the machine running, and whether it leaves it running or
stopped, is in each method's docstring as two lines, Requires: ... and
Leaves the machine: ...; this table collects them (a test keeps the two in
step). The calls that run to a point -- run_for_emulated_seconds,
run_until_or_timeout and the debugger's run-to methods -- leave the machine
STOPPED; call bbc.debugger.run() before anything that needs it running.
launch() starts the machine itself once it has connected, so a launch means
the same thing in emulated time on every host; start_stopped=True leaves it
stopped at the reset vector, having executed no instruction, for a test that
sets breakpoints or watchpoints before bbc.debugger.run().
| Call | Requires | Leaves the machine |
|---|---|---|
Beebium.launch |
nothing | RUNNING |
Beebium.connect |
nothing | unchanged |
Beebium.expect |
a running machine, unless the text is already on the screen | unchanged |
Beebium.boot_disc |
any run state | RUNNING |
Beebium.wait_until_ready |
a running machine | unchanged |
Beebium.run_for_emulated_seconds |
any run state | STOPPED |
Beebium.run_until_or_timeout |
any run state | STOPPED |
Beebium.run_fields |
any run state | STOPPED |
Debugger.run |
a stopped machine | RUNNING |
Debugger.stop |
any run state | STOPPED |
Debugger.reset |
any run state | STOPPED |
Debugger.step |
a stopped machine | STOPPED |
Debugger.step_cycles |
a stopped machine | STOPPED |
Debugger.ensure_running |
any run state | RUNNING |
Debugger.ensure_stopped |
any run state | STOPPED |
Debugger.running |
any run state | as it was on entry |
Debugger.wait_for_stop |
any run state | STOPPED |
Debugger.run_and_wait_for_stop |
any run state | STOPPED |
Debugger.run_to_cycle |
any run state | STOPPED |
Debugger.run_to |
any run state | STOPPED |
Debugger.measure |
any run state | STOPPED |
Keyboard.type |
any run state | unchanged |
Keyboard.wait_until_typing_complete |
a running machine and no reset part-way, unless nothing is queued | unchanged |
Keyboard.wait_until_ready |
a running machine | unchanged |
Keyboard.press_break |
a running machine | RUNNING |
Keyboard.ctrl_break |
a running machine | RUNNING |
Keyboard.shift_break |
a running machine | RUNNING |
Keyboard.tap_caps_lock |
a running machine | RUNNING |
Keyboard.tap_shift_lock |
a running machine | RUNNING |
Keyboard.text_input |
a running machine | unchanged |
Video.capture_frame |
any run state; with after_cycle, a running machine | unchanged |
TubeSystem.run |
any run state | RUNNING |
TubeSystem.stop |
any run state | STOPPED |
TubeSystem.run_until_or_timeout |
any run state | STOPPED |
TubeSystem.run_for |
any run state | STOPPED |
Coprocessor run state
A coprocessor -- bbc.connect_coprocessor(), or TubeSystem.coprocessor --
runs on the host's emulation thread and advances only while the host runs. Its
debugger is the same Debugger, and each method's docstring carries two more
lines, Coprocessor requires: ... and Leaves the coprocessor: ..., which this
table collects (a test keeps them in step). A coprocessor call never changes
whether the host runs. A coprocessor run() with the host stopped is refused:
the server says the host is stopped and names TubeSystem.run(), which runs
the host first and then the coprocessor. So run_to, measure and the other
run-to methods need the host running; stepping does not.
| Call | Requires | Leaves the coprocessor |
|---|---|---|
Debugger.run |
a stopped coprocessor and a running host | RUNNING |
Debugger.stop |
any run state | STOPPED |
Debugger.reset |
any run state | STOPPED |
Debugger.step |
a stopped coprocessor | STOPPED |
Debugger.step_cycles |
a stopped coprocessor | STOPPED |
Debugger.ensure_running |
a running host | RUNNING |
Debugger.ensure_stopped |
any run state | STOPPED |
Debugger.running |
a running host | as it was on entry |
Debugger.wait_for_stop |
a running host | STOPPED |
Debugger.run_and_wait_for_stop |
a running host | STOPPED |
Debugger.run_to_cycle |
a running host | STOPPED |
Debugger.run_to |
a running host | STOPPED |
Debugger.measure |
a running host | STOPPED |
debugger.cycle_count is a property on the host and the coprocessor alike,
counting each processor's own cycles.
Testing with pytest
Installing beebium registers the pytest plugin, so the bbc fixture is
available with no extra configuration:
# The beebium pytest plugin registers a `bbc` fixture: a fresh BBC Micro,
# launched and torn down per test. Install beebium and it is available with no
# conftest wiring.
def test_print(bbc):
bbc.expect("BASIC")
bbc.keyboard.type("PRINT 2+2")
bbc.keyboard.press_return()
assert bbc.expect("4") == "4"
bbc is a bare Model B. To configure it, override the beebium_preset fixture
(or beebium_extra_args, for extra server arguments) in a conftest.py:
# In conftest.py: every `bbc` in scope boots this preset (an id from
# `list-presets`, or a preset file path) -- here a Model B with a disc system.
import pytest
@pytest.fixture(scope="session")
def beebium_preset():
return "model-b-disc"
def test_has_a_disc_filing_system(bbc):
bbc.expect("BASIC")
bbc.keyboard.type("*HELP")
bbc.keyboard.press_return()
bbc.expect("DFS")
A project-wide default can also come from --beebium-preset or a
beebium_preset ini key; a fixture override takes precedence over both. When a
test needs more than one machine, or a machine configured on the spot, use the
launch_bbc factory the other fixtures are built on:
# launch_bbc launches machines on demand -- any variant, any preset, as many
# as a test needs -- and tears every one of them down when the test ends.
def test_same_program_on_two_machines(launch_bbc):
model_b = launch_bbc(preset="model-b-disc")
b_plus = launch_bbc(variant="model-b-plus")
for bbc in (model_b, b_plus):
bbc.expect("BASIC")
bbc.keyboard.type("PRINT 6*7")
bbc.keyboard.press_return()
assert bbc.expect("42") == "42"
Companion fixtures: bbc_shared (one machine per module, configured like
bbc), stopped_bbc (starts paused), bbc_tube (a 65C02 second processor),
and the session-scoped mos_filepath / basic_filepath. Point the plugin at a
server or ROMs with --beebium-server / --beebium-rom-dir (or the
BEEBIUM_SERVER / BEEBIUM_ROM_DIR environment variables).
gRPC fork support
Importing beebium.client sets GRPC_ENABLE_FORK_SUPPORT=false unless it is
already set. The client forks only to start processes, and grpcio's fork
handlers, run in a child forked while a channel is busy, can kill that child
before it starts. That happens only where a child really is forked -- on macOS,
or on Linux with a preexec_fn or os.fork(), since Linux subprocess
otherwise uses vfork(), which runs no fork handlers -- but the setting is made
everywhere. The setting must be made before grpc is first imported, so a
program that imports grpc itself, or another gRPC-based library, before
beebium.client should set the variable in its environment.
Further reading
Fuller documentation and runnable programs live in the repository: see
docs/ for the
gRPC service reference and subsystem guides, and
examples/
for end-to-end scripts. (Later: links to the published API reference.)
Contributing
Development happens in the
Beebium repository. This README is
generated -- do not edit README.md directly. See
CONTRIBUTING.md for the checkout, uv and wheel-test workflow
and for how to edit the README (its template and snippets).
License
GPL-3.0-or-later. See COPYING.txt.
Metadata
Release files for beebium 0.13.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| beebium-0.13.1.tar.gz | 576.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| beebium-0.13.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.0 MB
Release files / beebium-0.13.1.tar.gz
| Download URL | beebium-0.13.1.tar.gz |
|---|---|
| Size | 576.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e536f2cfc73fd7519e3ef468d8af3eef01d6f0d67b9f3c8a65905d72b8e5c00b
|
|
BLAKE2b-256 checksum How to use checksums |
13712442f81215ccecfd9b43a360c55fffedfbcd99c43ba8d5585214595e7d47
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / beebium-0.13.1-py3-none-any.whl
| Download URL | beebium-0.13.1-py3-none-any.whl |
|---|---|
| Size | 462.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f41ab34fc3dceefae7af593a7a423e61e98dd0f9c272787ec96261f86c9e14e0
|
|
BLAKE2b-256 checksum How to use checksums |
5619db25cd0f4832dd7c5f439e6027e5d3fd503549a781e6f7514d6d6954b8ac
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for beebium 0.13.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.