# Buttons and relays on GPIO

> Canonical HTML: https://screenrig.ai/docs/gpio

Wire a push button, a door contact or a relay board to a screen's GPIO pins, and the application on the screen hears the button, drives the relay and reports to your agent.

## What GPIO does

GPIO (general-purpose input/output) lines are the pins on a Player's board that an application can read and drive one at a time. A line is an **input** that reads high or low, or an **output** that the application sets high or low.

Inputs hear the room:

- A push button, such as a "call for service" or "next page" button.
- A door or window contact, a reed switch or a limit switch.
- Any sensor with a dry contact, such as a flow switch or a relay output on a motion detector.

Outputs act on it:

- A relay that releases a door strike, switches a stack light or sounds a buzzer.
- An LED that lights a button.

The application on the screen does the work through [`screenrig.device.gpio`](https://screenrig.ai/docs/sdk.md#device-gpio-write) and tells your agent what happened with `emit`, so "service requested" appears in the project event log. Your agent builds the application, uploads it and places it on the screen like any other. The [hardware page](https://screenrig.ai/docs/hardware.md) shows what else an application can reach.

## Which Players have GPIO

| Player | GPIO | Input pull | Output idle level | Pin numbers |
| --- | --- | --- | --- | --- |
| Raspberry Pi 3, 4 and 5 (Linux Player) | Yes, on the 40-pin header. The Player finds the header chip by itself | `"up"`, `"down"` or `"none"` | Yes | The BCM numbers |
| Other Linux boards | Yes, with `SCREENRIG_GPIO_CHIP` | `"up"`, `"down"` or `"none"` | Yes | The line numbers of that chip |
| Android and Amazon Signage Stick | Where the device exposes a writable sysfs GPIO chip | No: add a resistor | Yes | The line numbers of that chip |
| BrightSign | Yes: pins 0 to 7 on the GPIO connector, or a BrightSign USB-to-GPIO adapter | Built-in pull-ups | Yes | Pins 0 to 7, and the ranges in the BrightSign section |
| macOS, Windows, the Player web app, Samsung Smart Signage (Tizen), LG webOS Signage | No | No | No | No |

The flag goes to the page's controller, or to the application that is alone on its page. An application sees `capabilities["device.gpio"]` as `true` only on a Player and a device that has GPIO, so one build also runs on a screen without it.

## Set up the Player

### Raspberry Pi

A Raspberry Pi needs no setup. The Linux Player opens the header chip of a Pi 3, 4 or 5 by itself, and a pin is its BCM number. The Player's user needs access to `/dev/gpiochip*`. On Raspberry Pi OS the default user belongs to the `gpio` group, which gives it. A `--kiosk` install adds its account to the `video` and `i2c` groups only, so give a different `--kiosk-user` account the `gpio` group and restart the Player:

```sh
sudo usermod -aG gpio KIOSK_USER
```

### Other Linux boards

On another board, name the chip that carries the lines with `SCREENRIG_GPIO_CHIP`, in the Player's launch environment:

```sh
SCREENRIG_GPIO_CHIP=/dev/gpiochip4
```

For a `--kiosk` install, add it to the service with `sudo systemctl edit screenrig-player-kiosk.service`:

```ini
[Service]
Environment=SCREENRIG_GPIO_CHIP=/dev/gpiochip4
```

A pin is then a line number of that chip. The Player's user needs access to the chip's device file.

### Android

Android has GPIO where the device exposes a writable sysfs GPIO chip, such as a signage board or a custom image. A pin is the chip's global line number. A standard TV box has no such chip, and the application sees `device.gpio` as false. The Player holds a line only while the application uses it. Android cannot set an input's pull: wire a resistor, as in [Wire a button](#wire-a-button).

## Choose a pin

`pin` is an integer from 0 to 255: the line number of the chip. On a Raspberry Pi it is the **BCM** number, the GPIO number the Pi's documentation names, and not the position of the pin on the header. GPIO17 is physical pin 11. Common lines:

| BCM number (the `pin`) | Physical pin | Note |
| --- | --- | --- |
| 17 | 11 | A good first line |
| 27 | 13 | |
| 22 | 15 | |
| 23 | 16 | |
| 24 | 18 | |
| 25 | 22 | |
| 5 | 29 | |
| 6 | 31 | |
| 12 | 32 | |
| 13 | 33 | |
| 16 | 36 | |
| 26 | 37 | |
| 20 | 38 | |
| 21 | 40 | |
| 2 and 3 | 3 and 5 | I2C lines with fixed pull-ups; leave them for I2C |
| 14 and 15 | 8 and 10 | The serial console's lines |
| 0 and 1 | 27 and 28 | Reserved for HAT identification |

Ground is physical pins 6, 9, 14, 20, 25, 30, 34 and 39, and 3.3 V is pins 1 and 17. The Raspberry Pi documentation lists the whole header. A line another program already holds rejects `pin_busy`, and a line the chip does not have rejects `pin_not_found`.

<a id="wire-a-button"></a>

## Wire a button

Wire a push button between a GPIO line and ground, and turn on that line's pull-up. The line reads high until the button is pressed, and a press pulls it to ground, so a press reads **low**.

```text
 GPIO17 (pin 11) -----+
                      |
                   [ button ]
                      |
 GND (pin 9) ---------+
```

On a Raspberry Pi or another Linux board, ask the Player for the pull-up with `pull: "up"`:

```javascript
window.screenrig.device.gpio.onChange(17, handlePress, { pull: "up" });
```

`pull: "down"` suits a button wired to 3.3 V, which reads high when pressed. `"none"` is the default: the Player sets no bias, so an input with nothing connected floats and a button without a resistor reads random changes.

Android sets no pull, so add a resistor of about 10 kΩ from the line to 3.3 V (a pull-up) for a button wired to ground. A BrightSign has built-in pull-ups on its inputs, and a button wired to ground works as it is.

Asking for `"up"` or `"down"` on a Player that cannot set it does not stop the handler: the SDK watches the line without a pull and logs one `warn` entry with the code `gpio_pull_unsupported`.

<a id="relay"></a>

## Drive a relay

Drive a relay through a relay board, and never straight from a pin. A pin supplies a few milliamps at 3.3 V, which cannot switch a relay coil, a door strike, a stack light or a buzzer, and a coil's voltage spike can damage the board. A relay board has the transistor and the coil's protection on it.

```text
 5V (pin 2) -------------- VCC |
 GND (pin 6) ------------- GND | relay board
 GPIO27 (pin 13) --------- IN1 |
                               |  COM ---- supply for the load
                               |  NO  ---- the load (door strike, light, buzzer)
```

Power the load from its own supply through the relay's contacts. Choose a board whose input triggers at 3.3 V.

```javascript
const sr = window.screenrig;
await sr.device.gpio.write(27, true);   // relay on
await sr.device.gpio.write(27, false);  // relay off
```

Most boards energise on high. Many cheap boards are **active low**: the relay energises when the input is pulled low. Pass `idle: true` for an active-low board so a page that leaves releases the relay as off. See [Idle level](#idle-level).

```javascript
await sr.device.gpio.write(27, false, { idle: true }); // active-low board: relay on
await sr.device.gpio.write(27, true, { idle: true });  // relay off
```

An output on a BrightSign is 3.3 V through 100 Ω, so a BrightSign drives a relay board in the same way.

## 3.3 V only

Every GPIO line is 3.3 V logic. A Raspberry Pi's pins are not 5 V tolerant, and 5 V on a pin can damage the board. Connect dry contacts only: a button, a switch or a relay output that carries no voltage of its own. A sensor that outputs 5 V, 12 V, 24 V or mains voltage needs a level shifter, an optocoupler or a relay between it and the pin.

## Debounce

A mechanical contact bounces: for a few milliseconds a press flickers between high and low. The Player filters contact bounce, so the handler hears a press as one change and the release as one more. A Linux or Raspberry Pi line ignores changes that settle within about 20 ms. No code of yours is needed, and a pulse shorter than that is not a use for GPIO input.

## Hear a line

`onChange(pin, handler, options)` watches a line. The handler receives the level as a boolean, `true` for high. The first call carries the level the line has when the Player starts watching, and each later call is a change. The call returns a function that stops watching.

```javascript
const stop = window.screenrig.device.gpio.onChange(17, function (high) {
  doorOpen = high;
}, { pull: "up" });
```

At most 16 lines are watched at once, and a 17th throws `invalid_request`. Two handlers on one line share its pull: a second handler with a different `pull` throws `invalid_request` at once.

<a id="idle-level"></a>

## Idle level

`write(pin, value, options)` takes an `idle` option, the level the Player drives the line to when the page releases it. The default is low. The page releases a line when it leaves the screen, when its context ends and when it calls `screenrig.destroy()`. The Player then releases the line. The latest write for a pin sets its idle level.

This is why a relay does not stay energised after the page goes. Set `idle` to the level that means "off" for your board: low for most boards, high for an active-low board.

## When the page leaves

What a page holds ends with the page. When it leaves the screen, its inputs stop and every output goes to its idle level and is released. When the Player stops or the device loses power, the lines go with it. Plan the load for that: choose a door strike that locks or unlocks the way the building needs when the power fails.

## Limits and errors

- A pin is an integer from 0 to 255.
- At most 16 lines are watched at once.
- `write` rejects `invalid_request` for a bad `pin`, `value` or `idle`, `pin_not_found` for a line the chip does not have, `pin_busy` for a line another process holds and `gpio_unavailable` when the Player can open no GPIO chip.
- `onChange` throws `invalid_request` for a 17th line or a conflicting `pull`. A refused watch is logged, and the handler never runs.
- A call checks `inert`, `not_supported` and `capability_denied` before it sends anything. The [SDK reference](https://screenrig.ai/docs/sdk.md#hardware-rules) lists the order. A `write` with `idle` rejects `not_supported` on a Player that cannot set it.

## Example: call for service

A button on GPIO17, wired to ground, tells your agent that a customer wants help, and an LED or buzzer relay on GPIO27 answers for five seconds. The first reading is the level the line already has, so the handler acts only on a change from high to low.

```javascript
const sr = window.screenrig;

function active() {
  return sr.readyState === "active";
}

async function start() {
  if (!sr) return;
  await sr.waitUntilActive();
  await sr.ready();
  if (!sr.supports("device.gpio") || !sr.capabilities["device.gpio"]) return;

  let wasHigh = null;
  sr.device.gpio.onChange(17, function (high) {
    const pressed = wasHigh === true && high === false;
    wasHigh = high;
    if (!pressed || !active()) return;
    sr.emit("service.requested");
    document.getElementById("message").hidden = false;
    sr.device.gpio.write(27, true).catch(function () {});
    setTimeout(function () {
      document.getElementById("message").hidden = true;
      if (active()) sr.device.gpio.write(27, false).catch(function () {});
    }, 5000);
  }, { pull: "up" });
}
start();
```

The page leaving the screen releases line 27 at its idle level, low unless you pass `idle`, so a button pressed just before the page goes does not leave the relay on.

Your agent reads each press from the event log:

```console
screenrig events list --type application.event --screen-id scr_SCREEN
```

## Example: door-open input

A reed switch on GPIO17, wired to ground, closes when the door is shut, so the line reads low with the door closed and high with it open. The page shows a notice while the door is open and tells your agent.

```javascript
const sr = window.screenrig;
const notice = document.getElementById("door-notice");

function show(open) {
  notice.hidden = !open;
  document.body.classList.toggle("door-open", open);
}

async function start() {
  show(false);
  if (!sr) return;
  await sr.waitUntilActive();
  await sr.ready();
  if (!sr.supports("device.gpio") || !sr.capabilities["device.gpio"]) return;
  sr.device.gpio.onChange(17, function (high) {
    show(high);
    if (sr.readyState === "active") sr.emit(high ? "door.opened" : "door.closed");
  }, { pull: "up" });
}
start();
```

<a id="brightsign"></a>

## BrightSign

A BrightSign player runs the same API. Pins 0 to 7 are the GPIO connector's lines, the connector on the XT, XD and HD models (a DA-15 socket), pin 0 is the connector's line 0 and pin 7 its line 7. Its inputs have fixed pull-ups, so `pull` is not offered and a button wired to ground works as it is. `idle` works as on every Player. Outputs are 3.3 V through 100 Ω: drive a relay through a relay board.

A BrightSign adds two more banks of lines through USB:

| Pins | What they are |
| --- | --- |
| 0 to 7 | The GPIO connector |
| 8 to 39 | USB GPIO expanders, 8 pins each, up to 4: pins 8 to 15 are the first expander, 16 to 23 the second, and so on |
| 100 to 110 | A BP200 or BP900 button board, number 0: pin 100+k is button k as an input, and its LED as an output |
| 120 to 130, 140 to 150 and 160 to 170 | Button boards 1, 2 and 3, the same way |

A write to pin 100+k lights the LED of button k, and a watch on pin 100+k hears the button. A board or an expander that is not connected makes its pins reject `pin_not_found`.

## Try it in app dev

`screenrig app dev` simulates GPIO. The harness shows a GPIO chip switch and three simulated lines: 17 (low), 27 (high) and 22, which another process holds and so answers `pin_busy`. The examples above use 17 and 27. Click a line's level to change it, and your application hears the change when it watches the line. A write from the application moves the line, and the panel shows the idle level it set. The harness accepts `pull` and `idle`, so a build runs there before it reaches a screen. [Develop locally](https://screenrig.ai/docs/sdk.md#develop) covers the controls.

## Reference

- [`screenrig.device.gpio.write()`](https://screenrig.ai/docs/sdk.md#device-gpio-write) and [`screenrig.device.gpio.onChange()`](https://screenrig.ai/docs/sdk.md#device-gpio-onchange) in the SDK reference.
- [Hardware an application can use](https://screenrig.ai/docs/hardware.md): every Player and every device capability.
- [Raspberry Pi digital signage](https://screenrig.ai/digital-signage/raspberry-pi.md): the Pi as a screen.
- [Linux and Raspberry Pi](https://screenrig.ai/docs/players.md#linux): install the Player.

## Questions

**Can a screen drive a relay?**
Yes, through a relay board. An application sets a line high or low, and the board switches the door strike, stack light or buzzer. Never drive a relay straight from a pin.

**Does a button need a resistor?**
On a Raspberry Pi or another Linux board, no: ask for the pull-up and wire the button to ground. On Android, add a resistor of about 10 kΩ to 3.3 V. A BrightSign has pull-ups built in.

**Which number do I pass for a pin?**
On a Raspberry Pi, the BCM number: GPIO17 is 17 and sits on physical pin 11. On another board, the line number of its GPIO chip. On a BrightSign, the pin ranges in the BrightSign section.

**What happens to a relay when the page changes?**
The Player drives the line to its idle level, low unless the application chose another, and releases it. Choose high for an active-low relay board so the relay is off.

**Does the application need to run all the time?**
The application listens while its page is on the screen. When the page leaves, the inputs stop and the outputs go to their idle level.

**What does it cost?**
screenRIG is free. The board, the button and the relay are your cost.
