Skip to main content

Joystick

Plug a DualShock 4 into the Pi with a USB cable, and your program can read its sticks, triggers and buttons, and set its rumble and light bar.

Add the Raspberry Pi library first

Pi and its devices come from a library, not from the language, so this code won't build without it. In IntelliJ, choose Tools | Nylon | Add Nylon Library… and pick raspberry-pi. That adds one line to your project's build.markup.code:

(+dependencies : RepositoryDependency coordinate={"com.grandtheorem:raspberry-pi:0.1.0"} /)

Read the sticks​

DualShock4 comes from the Raspberry Pi library, and nylon.time gives us sleep.

namespace sticks

import grandtheorem.rpi.DualShock4
import namespace nylon.time

DualShock4() gives you the first pad plugged in. The program remembers where the left stick was, so it only prints when the stick moves.

entrypoint.default main() {
let pad = DualShock4()
var lastX = f32(0)
var lastY = f32(0)

read gives you the pad's newest state, whether or not anything is pressed, waiting up to the timeout for one. If none comes, if (let state? = ...) skips its block.

while (true) {
if (let state? = pad.read(100.milliseconds())) {

A stick is a pair of numbers. Print them only when they've changed, and remember them for next time.

if (state.leftX != lastX or state.leftY != lastY) {
println("left stick at ${state.leftX}, ${state.leftY}")
lastX = state.leftX
lastY = state.leftY
}
}

A pad reports hundreds of times a second, so wait a moment between reads, or the run window will flood.

sleep(100.milliseconds())
}
}

Move the left stick and its position appears in the run window, one line at a time. The program loops forever, so press Stop in IntelliJ to end it. Everything the pad can tell you is on state:

On stateWhat it gives you
leftX, leftY, rightX, rightYA stick, from -1 to 1. It's 0 at rest, and up and right are positive
l2, r2A trigger, from 0 released to 1 fully pressed
cross ✕ bottom, circle ○ right, square □ left, triangle △ topThe four face buttons, true while held
l1, r1, l3, r3true while held. l3 and r3 are the sticks pressed in
share, options, ps, touchpadtrue while held
dpadWhich way the D-pad points, or nothing

Read the D-pad​

The D-pad might not be pressed at all, so dpad may be missing. When it's there, it's one of eight directions.

Direction is the type of dpad.

namespace pointing

import grandtheorem.rpi.DualShock4
import grandtheorem.devices.Direction
import namespace nylon.time

match! makes the compiler check that you've handled all eight.

let name(way: Direction): string = match!(way) {
Direction.Up -> "up"
Direction.UpRight -> "up and right"
Direction.Right -> "right"
Direction.DownRight -> "down and right"
Direction.Down -> "down"
Direction.DownLeft -> "down and left"
Direction.Left -> "left"
Direction.UpLeft -> "up and left"
}

if (let way? = state.dpad) runs only while the D-pad is pressed.

entrypoint.default main() {
let pad = DualShock4()

while (true) {
if (let state? = pad.read(100.milliseconds())) {
if (let way? = state.dpad) {
println(name(way))
}
}

sleep(100.milliseconds())
}
}

Rumble and light​

The pad's rumble motors and light bar are outputs, and both belong to the DualShock 4 itself.

The start is the same as before.

namespace feel

import grandtheorem.rpi.DualShock4
import namespace nylon.time

light takes a colour as 0xRRGGBB, so this is blue. It stays that way until you set it again.

entrypoint.default main() {
let pad = DualShock4()

pad.light(0x0000FF)

rumble takes the strong motor and then the weak one, each from 0 to 1. The triggers are already in that range, so they can drive the motors directly.

while (true) {
if (let state? = pad.read(100.milliseconds())) {
pad.rumble(state.l2, state.r2)
}

sleep(20.milliseconds())
}
}

The pad buzzes as you squeeze the triggers. Let go before you press Stop, because the rumble stays at the last level set.

Brighten the light bar with a trigger​

The pad's own light bar can follow a trigger, so this needs nothing wired.

The start is the same as before.

namespace glow

import grandtheorem.rpi.DualShock4
import namespace nylon.time

One pad, and nothing else.

entrypoint.default main() {
let pad = DualShock4()

In 0xRRGGBB, blue is the last byte, so 0 to 255 runs from off to full blue. Scale the trigger's 0 to 1 up to that.

while (true) {
if (let state? = pad.read(100.milliseconds())) {
pad.light(i32(state.r2 * f32(255)))
}

sleep(20.milliseconds())
}
}

The light bar brightens as you squeeze R2, and dims as you let go.

Good to know​

A stick at rest reads exactly 0. Within 5% of the middle, a stick counts as centred, so a pad left alone won't drift.

Pulling a stick up is positive. leftY is 1 at the top, the way you'd expect, even though the pad itself reports it the other way up.

A pad can come and go. Unplug it and read finds nothing, and plug it back in and it carries on.

The DualShock 4 is the only pad for now. Other gamepads aren't supported yet.

USB only, for now. Bluetooth is coming, so a pad can pair without a cable.

note

These programs ran on a Raspberry Pi 4, as this page shows them.