Skip to main content

Beebium Python Client

Python client for the Beebium BBC Micro emulator. Designed for pytest-based testing of BBC Micro software.

Installation

From a checkout (recommended: uv)

This repository is a uv project (pyproject.toml

  • uv.lock). uv manages an isolated environment for you, so it works on distributions that enforce PEP 668 (Arch, recent Debian/Ubuntu, Fedora) where a system-wide pip install is blocked with error: externally-managed-environment.
cd clients/beebium-python-client

# Run anything in the project environment (uv creates it on first use):
uv run python examples/serial_demo.py --port 50071

# Run the test suite. Development tooling (pytest, grpcio-tools) lives in the
# `dev` dependency-group, which uv installs by default -- no extra flags:
uv run python -m pytest

# Build the wheel and run tests against it in a fresh venv. With no arguments
# only the packaging assertions in tests_packaging/ run; pass pytest arguments
# to run any other tests against the installed wheel (CI runs the whole suite
# this way, so packaging faults are caught rather than passed over by a
# source-tree run). The plugin finds roms/ and the checkout's server build from
# pytest's rootdir, so no env vars are needed in-repo:
bash scripts/test-wheel.sh
bash scripts/test-wheel.sh tests/test_keyboard.py -q
# BEEBIUM_ROM_DIR / BEEBIUM_SERVER still override the in-checkout discovery.

# Or materialise a .venv you can activate (the dev group is included by default):
uv sync

No pip is required, and nothing is installed into your system Python.

From a checkout (plain virtualenv)

PEP 668 only blocks the system environment, so pip works fine inside a virtualenv:

cd clients/beebium-python-client
python -m venv .venv
. .venv/bin/activate
pip install -e .                 # the client
pip install --group dev          # dev tooling (pip >= 25.1); or just use uv

From PyPI

pip install beebium            # core client
pip install beebium[imaging]   # + Pillow for image capture
pip install beebium[discovery] # + zeroconf for mDNS server discovery

Development tooling is not a published extra -- contributors get it from the dev dependency-group via uv (above).

On a PEP 668 system, run these inside a virtualenv (above) or via uv pip install / pipx.

Running the examples

The examples/ scripts need a running beebium-server. Build the servers first (see the top-level build docs); they appear at build/src/server/beebium-model-b (and -plus, -plus-128k, -romram).

Either attach to a server you start yourself:

# terminal 1
./build/src/server/beebium-model-b --mos roms/acorn-mos_1_20.rom --port 50071
# terminal 2
cd clients/beebium-python-client
uv run python examples/serial_demo.py --port 50071

…or let an example launch (and stop) its own server. Point it at the server binary via --server / $BEEBIUM_SERVER and a MOS ROM via --mos:

cd clients/beebium-python-client
export BEEBIUM_SERVER="$PWD/../../build/src/server/beebium-model-b"
uv run python examples/serial_demo.py --mos ../../roms/acorn-mos_1_20.rom

serial_demo.py defaults --mos to <repo>/roms/acorn-mos_1_20.rom when that file exists, so from a full checkout uv run python examples/serial_demo.py just works.

Quick Start

from beebium.client import Beebium

# Connect to an existing server (default port 48875 / 0xBEEB)
with Beebium.connect() as bbc:
    bbc.debugger.stop()
    print(f"PC = ${bbc.cpu.pc:04X}")

# Launch and manage a server. With the `beebium-server` package installed
# (pip install beebium beebium-server) this needs no arguments: the server is
# found in that wheel and uses its own bundled ROMs.
with Beebium.launch() as bbc:
    bbc.keyboard.type("PRINT 42")
    bbc.keyboard.press_return()

# Or point at a specific server -- a binary, an install root, or a
# ServerInstallation -- and supply your own ROMs:
with Beebium.launch(server="/opt/beebium", mos_filepath="acorn-mos_1_20.rom") as bbc:
    ...

Features

Server Management

# Auto-allocate a free port
with Beebium.launch(mos_filepath="acorn-mos_1_20.rom") as bbc:
    print(f"Server running on {bbc.target}")

# Specify a port
with Beebium.launch(mos_filepath="acorn-mos_1_20.rom", port=48876) as bbc:
    ...

Debugger Control

# Execution control
bbc.debugger.stop()           # Pause execution
bbc.debugger.run()            # Resume execution
bbc.debugger.reset()          # Reset the machine
bbc.debugger.step(10)         # Step 10 instructions
bbc.debugger.step_cycles(100) # Step 100 cycles

# Run until a (temporary) breakpoint fires -- supports a condition
state = bbc.debugger.run_to(0xC000, condition="A == 0x42")

# add_breakpoint returns a Breakpoint you can act on
bp = bbc.debugger.add_breakpoint(0xC000, condition="A == 0x42")
bp.disable()                    # toggle without removing
bp.enable()
bp.remove()                     # or bbc.debugger.remove_breakpoint(bp)
bbc.debugger.clear_breakpoints()

# ...or scope it to a block (removed automatically on exit)
with bbc.debugger.breakpoint(0xC000, condition="hits == 3") as bp:
    # race-free: subscribes before resuming, unlike run() + wait_for_stop()
    bbc.debugger.run_and_wait_for_stop()

# State queries
state = bbc.debugger.get_state()
print(f"Running: {state.is_running}, Cycles: {state.cycle_count}")

CPU Register Access

# Read all registers as one coherent snapshot (one request)
regs = bbc.cpu.registers
print(regs)  # A=.. X=.. Y=.. SP=.. PC=.. P=.. [flags]

# Convenience single-register access (each read is its own snapshot)
if bbc.cpu.a == 0:
    print("Accumulator is zero")

# Atomic partial write; returns the complete new register snapshot
new = bbc.cpu.update(pc=0xC000, a=0x42)

# The individual setters route through update()
bbc.cpu.pc = 0xC000

# Status flags live on the decoded status register
if regs.status.zero:
    print("Zero flag is set")

Memory Access

Memory access is explicit about side effects and supports both flat address space and named region access.

Address Space Access (16-bit flat addressing)

# Side-effecting access (through memory bus like real hardware)
value = bbc.memory.address.bus[0x1000]
bbc.memory.address.bus[0x2000] = 0x42

# Range read (returns bytes)
data = bbc.memory.address.bus[0x1000:0x1010]

# Range write
bbc.memory.address.bus[0x2000:0x2010] = bytes([0x00] * 16)

# Side-effect-free peek (for I/O addresses)
value = bbc.memory.address.peek[0xFE4D]
data = bbc.memory.address.peek[0xFE40:0xFE50]

# Sequential read/write
data = bbc.memory.address.bus.read(0x1000, 16)
bbc.memory.address.bus.write(0x2000, b"HELLO")

# 16-bit little-endian words -- follow a 6502 pointer or vector
oswrch = bbc.memory.address.peek.word(0x020E)          # read the WRCHV vector
bbc.memory.address.bus.set_word(0x0070, 0x1234)        # write a 16-bit pointer

# Typed access using struct format strings (other widths / byte orders)
word = bbc.memory.address.bus.cast("<H")[0x0070]       # Read 16-bit little-endian
bbc.memory.address.bus.cast("<H")[0x0070] = 0x1234     # Write 16-bit little-endian
words = bbc.memory.address.bus.cast("<H")[0x70:0x78]   # Read 4 words as tuple

# Load/save binary files
bbc.memory.address.bus.load(0x1900, "mygame.bin")
bbc.memory.address.bus.save(0x1900, 0x1000, "dump.bin")

# Fill memory range
bbc.memory.address.bus.fill(0x1000, 0x2000, 0x00)

PC-Context Access (for B+ shadow RAM)

On BBC Model B+, memory access at 0x3000-0x7FFF is routed based on the program counter of the executing code. Use with_pc() to query what code at a given PC would see:

# What MOS code (at 0xD000) would see at address 0x5000
mos_view = bbc.memory.address.peek.with_pc(0xD000)[0x5000]

# What user code (at 0x1000) would see at the same address
user_view = bbc.memory.address.peek.with_pc(0x1000)[0x5000]

# Works with both bus and peek access modes
bbc.memory.address.bus.with_pc(0xD000)[0x5000]   # Side-effecting
bbc.memory.address.peek.with_pc(0xD000)[0x5000]  # Side-effect-free

# Typed access is supported
word = bbc.memory.address.peek.with_pc(0xD000).cast("<H")[0x5000]

On Model B (no shadow RAM), with_pc() has no effect.

Region-Based Access (named memory regions)

Access memory regions directly, bypassing bank switching:

# Access main RAM
main = bbc.memory.region("main_ram")
value = main.bus[0x1234]

# Access sideways banks (uses absolute addresses at 0x8000)
bank4 = bbc.memory.region("bank_4")
data = bank4.peek[0x8000:0x8100]

# Access Model B+ shadow RAM
shadow = bbc.memory.region("shadow_ram")
shadow.bus[0x3000:0x3100] = bytes(256)

# Region discovery
for region in bbc.memory.regions:
    print(f"{region.name}: base=0x{region.base_address:04X}, size={region.size}")

print(f"Machine: {bbc.memory.machine_type}")

Keyboard Input

# Type text (handles shift automatically)
bbc.keyboard.type("PRINT 42")
bbc.keyboard.press_return()

# Individual key control
bbc.keyboard.key_down('A')
bbc.keyboard.key_up('A')

# Special keys
bbc.keyboard.press_escape()
bbc.keyboard.press_delete()

# Matrix-level access
bbc.keyboard.matrix_down(row=4, column=1)  # 'A' key
bbc.keyboard.matrix_up(row=4, column=1)

Video Capture

# Get video config
config = bbc.video.config
print(f"Resolution: {config.width}x{config.height} @ {config.framerate_hz}Hz")

# Capture a single frame
frame = bbc.video.capture_frame()
frame.save_png("screenshot.png")  # Requires Pillow

# Stream frames
for frame in bbc.video.stream_frames(max_frames=100):
    process(frame.pixels)  # BGRA32 format

BBC BASIC Helpers

# Wait for BASIC prompt
bbc.basic.wait_for_prompt()

# Wait for specific text
bbc.basic.wait_for_text("BBC Computer 32K")

# Read screen text (Mode 7)
text = bbc.basic.read_screen_text(0, 5)

# Run a BASIC program
bbc.basic.run_program("""
10 PRINT "HELLO"
20 END
""")

# Get BASIC memory layout
print(f"PAGE={bbc.basic.get_page():04X}")
print(f"TOP={bbc.basic.get_top():04X}")
print(f"HIMEM={bbc.basic.get_himem():04X}")

Reading the MODE 7 Screen

beebium.screen reads what is actually displayed in MODE 7 (teletext), correcting for hardware scrolling: the BBC scrolls by moving the CRTC display start, so a fixed read of 0x7C00 returns a rotated grid once the screen has scrolled. These helpers anchor the read at the CRTC screen-start, so they take the client (not just bbc.memory).

from beebium.client.screen import (
    read_mode7_screen, screen_contains, find, dump_screen,
)

# 25 rows of 40 characters, in display order
rows = read_mode7_screen(bbc)

# Is some text on screen? (tolerant of words wrapped across lines)
if screen_contains(bbc, "BBC Computer 32K"):
    ...

# Where is it? -> (row, column), or None
location = find(bbc, ">")

# Formatted dump (with the screen-start address) for diagnostics
print(dump_screen(bbc))

Lower-level building blocks compose for custom needs: read_mode7_cells(bbc) returns the raw 25x40 byte grid, cells_to_text(cells) renders it as text, and linearise(rows, separator) flattens it for searching (with separator strategies spaced, lined, no_separator, and dewrapped). Read the screen while the machine is paused or settled, so the screen-start and memory describe one frame.

pytest Integration

The beebium pytest fixtures are auto-registered. After installing beebium, they're available in your tests:

def test_basic_print(bbc):
    """Test runs with a fresh BBC Micro instance."""
    bbc.basic.wait_for_prompt()
    bbc.keyboard.type("PRINT 42")
    bbc.keyboard.press_return()
    bbc.basic.wait_for_text("42")

def test_memory_access(stopped_bbc):
    """Test starts with emulator stopped."""
    stopped_bbc.memory.address.bus[0x1000] = 0x42
    assert stopped_bbc.memory.address.bus[0x1000] == 0x42

Available Fixtures

Fixture Scope Description
bbc function Fresh BBC Micro for each test
bbc_shared module Shared instance across tests in a module
stopped_bbc function BBC Micro starting in stopped state
mos_filepath session Path to MOS ROM
basic_filepath session Path to BASIC ROM (or None)

Configuration

Set ROM paths via environment variables or command line:

# Environment variables
export BEEBIUM_ROM_DIR=/path/to/roms
export BEEBIUM_SERVER=/path/to/beebium-server

# Command line options
pytest --beebium-rom-dir=/path/to/roms --beebium-server=/path/to/beebium-server

Requirements

  • Python 3.12+
  • A built beebium-server executable
  • ROM files (MOS, optionally BASIC)

License

GPL-3.0-or-later

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

beebium-0.1.6.tar.gz (409.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

beebium-0.1.6-py3-none-any.whl (402.2 kB view details)

Uploaded Python 3

File details

Details for the file beebium-0.1.6.tar.gz.

File metadata

  • Download URL: beebium-0.1.6.tar.gz
  • Upload date:
  • Size: 409.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","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}

File hashes

Hashes for beebium-0.1.6.tar.gz
Algorithm Hash digest
SHA256 4a1a36a5849d7577975fedfe54bed29582d8833f013fe7d3c7cfba1a4c78cf2c
MD5 872a67155a74d1229956f3bbd007b6b4
BLAKE2b-256 e084a9f7326a5b787abd0501681172407674acd6f07f3c9e76aab621622ede94

See more details on using hashes here.

File details

Details for the file beebium-0.1.6-py3-none-any.whl.

File metadata

  • Download URL: beebium-0.1.6-py3-none-any.whl
  • Upload date:
  • Size: 402.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","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}

File hashes

Hashes for beebium-0.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 b2ebd521797e0b126afbe614305d9e34a77a17b6911dc95fc46ef8bba0dc973f
MD5 9753fa816c75e486f31641b4ad2adf37
BLAKE2b-256 e34543a0010a94da16ed6cfe781945e20c02fb9a73b3fb84ddfec84c91b0fe41

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.6 This release

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page