MBC PS2 / PS4 Controller × LEGO SPIKE App (Python)

Read the Controller with SPIKE App Python in three steps

The LEGO SPIKE App (Education) runs Python too, with no hub firmware change. In the App the Controller receiver is a color sensor, and the App cannot import your own files, so the order is: first paste the reading helper at the top, then create an object and name the receiver, then call update() once per loop and call its functions with that name. PS2 and PS4 receivers are written exactly the same way. Work down the steps on the left and watch the program grow on the right; click buttons and drag the sticks on the simulated pad to see what the program prints.

1

Paste the reading helper

A SPIKE App Python project is a single file and cannot import your own .py, so paste the whole "reading helper" at the very top (its first lines say so: copy it as is). Without it the hub does not know GamepadApp and stops with NameError.

  1. Open the LEGO SPIKE App (Education, yellow SPIKE Prime hub), create a project and choose "Python".
  2. Paste the program marked ① on the right at the top (only the first helper line shows; press "Show helper" to see it all, and "Copy program" always copies everything). The helper alone: MBC_gamepad_App_helper.py (open it in a text editor and copy all; the file has Chinese and English comments).
  3. Plug the receiver into any port A–F; the hub powers it. The receiver needs the SPIKE firmware. PS2 and PS4 receivers are written the same way (the helper is also in the PS4 download folder: PS4 download, same file); switch PS2 / PS4 under the simulated pad.

The helper splits the numbers the App reads for you (the same conversion as the SPIKE App blocks page):

What the App readsWhat it holdsHelper function
color_sensor.reflection() (reflection)D-pad + shape buttons: up 1, right 2, down 4, left 8, △ 16, ○ 32, ✕ 64, □ 128, added up when held togetherpressed('UP')…, low 8 bits of buttons()
color_sensor.color() (color)shoulder + function buttons: L2 1, R2 2, L1 4, R1 8, SELECT (PS4: SHARE) 16, L3 32, R3 64, START (PS4: OPTIONS) −128pressed('L1')…, high 8 bits of buttons()
color_sensor.rgbi() item 1 (raw red)left stick X×10 + Y (each 1–9, 55 when released)stick('LX') (whole part of ÷ 10), stick('LY') (remainder)
rgbi() item 2 (raw green)right stick X×10 + Ystick('RX'), stick('RY')
rgbi() item 3 (raw blue)the 16 buttons in another layout (PSX)not used (same as the SPIKE App blocks page; see "Values the SPIKE App cannot read")
2

Create the object: name the sensor

A robot can have several sensors, so you "create an object": tell the helper which port the receiver is on (port.C comes from from hub import port) and give it a name. You use that name for every reading.

Which port is the receiver on?
3

update() every loop, then use a function

App programs go in async def main(): and runloop.run(main()) starts them; wait with await runloop.sleep_ms(100) (not Pybricks' wait()). Each loop first reads once with .update(), then uses "name + dot + function". Pick a function and the program and output switch to its example.

Gamepad simulator: click the buttons, drag the sticks, and watch the output on the right

The simulator is only a sketch: click a button once to hold it, click it again to let go. Drag the dot on a stick to move it (it stays where you let go; press "Release all" to centre it). Sticks go from 1 to 9, 5 = centre, and the number gets bigger to the right and up. The PS4 gamepad battery is fixed at 80 in this demo.

An example for every function

Every function has an example you can drop into main() (port C). "Try it" jumps to step 3 and the program and output on the right switch to it.

Full examples

Press "Copy" for the whole program (helper included).

Drive with the sticks (left wheel E, right wheel F)

        

!Left stick up/down drives the left wheel, right stick the right wheel (tank style); hold ✕ to stop. Too fast: lower SPEED; it drives backwards: swap the motor cables or make SPEED negative.

Values the SPIKE App cannot read

The App only reads the receiver's "combined data" (buttons and sticks), so the functions below are out of reach. For them use Pybricks MicroPython, Pybricks blocks (the hub needs the Pybricks firmware) or the MBC Blocks website.

Function (Pybricks library)Why not / what to use
connected()Whether the pad is connected is not readable. With no pad the receiver sends "all released, sticks centred", which looks the same as really letting go.
battery()Pad battery (PS4 / PS5 only) is not readable.
kind()Whether the receiver is PS2 or PS4 is not readable; both are written the same way.
buttons_psx(), pressed_psx()The PSX layout is in raw blue, but older PS4 receiver firmware lays it out differently, so the helper leaves it out like the SPIKE App blocks page; use Pybricks for it.
device_id(), firmware_version(), sensor_ok()Device ID, firmware version and "is it connected" are not readable; what the App does when the cable is pulled is not yet tested, so the simulator has no unplug option.

Tips

  1. Call update() every loop. All helper functions split the 8 numbers from the last update(); without it the values never change (they start as all 0, so the sticks read 0).
  2. This is SPIKE App 3 style, not Pybricks. Ports are port.C from from hub import port (not Port.C), and waiting is await runloop.sleep_ms() (not wait()), so Pybricks programs cannot be pasted in. The Robot Inventor (home) App reads differently; this page is for the Education App.
  3. Button names are upper-case strings. For example pressed('CROSS'); on PS4 / PS5 pads SHARE = 'SELECT' and OPTIONS = 'START', and 'SHARE', 'OPTIONS' work too. The PS button and touchpad are not readable. A wrong name gives ValueError.
  4. Use 7 and up, 3 and down when a stick is a switch. Pushed a little, left and down quickly become 4, while right and up need about half way to become 6, so the two sides differ; treat 4–6 as "centre". Right and up give bigger numbers (9 at the end), 5 when released.
  5. The App Python readings are not yet checked one by one on a real hub. The conversion follows the firmware and the SPIKE App blocks page; START reads −128 from color(), and the helper turns it into 128 with & 255, so pressed() still works.