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.
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 state | What it gives you |
|---|---|
leftX, leftY, rightX, rightY | A stick, from -1 to 1. It's 0 at rest, and up and right are positive |
l2, r2 | A trigger, from 0 released to 1 fully pressed |
cross ✕ bottom, circle ○ right, square □ left, triangle △ top | The four face buttons, true while held |
l1, r1, l3, r3 | true while held. l3 and r3 are the sticks pressed in |
share, options, ps, touchpad | true while held |
dpad | Which 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.
These programs ran on a Raspberry Pi 4, as this page shows them.