-
Notifications
You must be signed in to change notification settings - Fork 0
Magma Hub and I2C
texastoast can read controller input from an I2C device instead of the keyboard. This is optional in every sense: the dependency is optional, the hardware is optional, and a game written against it runs unchanged on a laptop.
The Magma Hub hardware is still in development. This page documents the software interface — what the engine expects and how it behaves without a device attached.
Writing Pico firmware? See Magma Hub Firmware Spec for the exact I2C protocol, memory map, button bitmasks, and read handshake that the Pico must implement.
Developing without hardware? Use the simulator, not mock mode — see Hardware Dev Kit. It runs everything on this page for real, against simulated controllers.
pip install "texastoast[hardware]"That pulls in smbus2, which only works on Linux. Without it, everything below still imports and runs — in mock mode.
I2CBus falls back to mock mode when smbus2 is missing, the bus device does not
exist, or permissions deny it. Mock mode is not a simulator: reads report
failure rather than inventing data.
from texastoast import I2CBus
bus = I2CBus(1)
bus.is_mock # True on a machine with no I2C
bus.read_byte_data(0x08, 0x00) # None
bus.read_i2c_block_data(0x08, 0, 2) # NoneNone rather than 0x00 is the important part. A zero byte is a valid
controller report meaning "nothing pressed"; if a failed read returned zeros too,
nothing downstream could tell whether hardware was present.
This was a real bug in 0.1.x: fabricated zeros made
MagmaHub.connectedreportTruewith nothing attached,CompositeInputlatched onto the phantom hub, and keyboard input stopped reaching the game. See Migrating to 0.2.0.
bus = I2CBus(bus_number=1)
bus.read_byte_data(address, register) # int, or None
bus.read_i2c_block_data(address, register, length) # list[int], or None
bus.write_byte_data(address, register, value)
bus.write_i2c_block_data(address, register, data)
bus.probe(address) # bool — one read
bus.scan() # responding addresses
bus.close()
with I2CBus(1) as bus:
...probe() (0.4.0) checks a single address. Prefer it over scan(), which walks
every address from 0x03 to 0x77 — 117 blocking reads, fine for diagnostics
and much too slow for a game's startup path.
I2CBus(backend=...) (0.4.0) accepts any object with the smbus2 surface, which
is how the simulator is injected. A bus with
an injected backend is a real bus: is_mock is False and reads flow
through normally.
Reads never raise: an OSError from the kernel is logged at debug level and
turned into None. Turn on logging to see them:
import logging
logging.getLogger("texastoast.i2c").setLevel(logging.DEBUG)One hub, addressing one or more controllers:
from texastoast import MagmaHub, I2CBus
bus = I2CBus(1)
hub = MagmaHub(address=0x08, bus=bus, num_controllers=2)
states = hub.poll() # list[ControllerState]
hub.get_controller(0)
hub.connected
hub.stats # HubStats — 0.4.0poll() rate-limits itself to poll_interval (default 0.016 s ≈ 60 Hz) and
returns the cached states in between, so calling it every frame is fine. Since
0.4.0 it builds a fresh list each time and swaps it in with one assignment,
rather than mutating the previous list in place — that is what makes it safe to
read from another thread. Treat the returned list as read-only.
connected is True only while reads are actually succeeding. It starts
False, becomes True after a successful poll, and returns to False as soon
as reads start failing. With several controllers it is True if any of them
answered.
stats reports poll and error counts plus rolling latency figures; see
Hardware Dev Kit.
Reading the hub from a background thread, so a slow bus never costs a frame, is
HubPoller.
hubs = MagmaHub.scan_buses(bus_numbers=[1], num_controllers=1)Probes each candidate address and returns a MagmaHub for every one that
answers. Candidates default to DEFAULT_HUB_ADDRESSES (0x08, 0x09, 0x0A,
0x0B), so this is four reads per bus — before 0.4.0 it swept the whole address
range instead. Buses with no hub are closed rather than left open. On a machine
without I2C this returns an empty list.
MagmaHub.scan_buses(buses=[my_bus]) # scan buses you already builtTo keep discovery off the main thread entirely, use
scan_buses_async.
The raw two bytes per controller, decoded:
cs = hub.get_controller(0)
cs.buttons, cs.joystick # raw bytes
cs.up, cs.down, cs.left, cs.right
cs.a, cs.b, cs.start, cs.select
cs.direction() # -> (dx, dy)Buttons are one active-high bit each, in texastoast.i2c.protocol:
| Bit | Button |
|---|---|
0b00000001 |
up |
0b00000010 |
down |
0b00000100 |
left |
0b00001000 |
right |
0b00010000 |
a |
0b00100000 |
b |
0b01000000 |
start |
0b10000000 |
select |
Each controller occupies CONTROLLER_SIZE (2) bytes: buttons at
BUTTONS_ADDR (0x00), joystick at JOYSTICK_ADDR (0x01). Controller n
starts at n * CONTROLLER_SIZE.
MagmaHubInput wraps a hub in the same interface as KeyboardInput, and
CompositeInput picks whichever is live:
from texastoast import KeyboardInput, MagmaHub, MagmaHubInput, CompositeInput
keyboard = KeyboardInput(game.root)
hubs = MagmaHub.scan_buses(bus_numbers=[1])
hub_input = MagmaHubInput(hubs[0]) if hubs else None
controls = CompositeInput(keyboard, hub_input)
def update(dt):
state = controls.poll() # same InputState either way
player.move(state.dx, state.dy, dt, tilemap)Write the game against controls and it works on a desk with a keyboard and on
a Pi with a controller, with no branching. See examples/magma_hub_demo.py.
When you have a physical hub to test, texastoast-bench shows live button
state, raw bytes, connection status, poll latency and error rates:
texastoast-benchSee Hardware Dev Kit for the bench, and Testing on the Pi for the bring-up checklist.
texastoast · PyPI · Apache-2.0