Documentation

Buttons and relays on 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 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 shows what else an application can reach.

Which Players have GPIO

PlayerGPIOInput pullOutput idle levelPin 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"YesThe BCM numbers
Other Linux boardsYes, with SCREENRIG_GPIO_CHIP"up", "down" or "none"YesThe line numbers of that chip
Android and Amazon Signage StickWhere the device exposes a writable sysfs GPIO chipNo: add a resistorYesThe line numbers of that chip
BrightSignYes: pins 0 to 7 on the GPIO connector, or a BrightSign USB-to-GPIO adapterBuilt-in pull-upsYesPins 0 to 7, and the ranges in the BrightSign section
macOS, Windows, the Player web app, Samsung Smart Signage (Tizen), LG webOS SignageNoNoNoNo

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:

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:

SCREENRIG_GPIO_CHIP=/dev/gpiochip4

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

[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.

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 pinNote
1711A good first line
2713
2215
2316
2418
2522
529
631
1232
1333
1636
2637
2038
2140
2 and 33 and 5I2C lines with fixed pull-ups; leave them for I2C
14 and 158 and 10The serial console's lines
0 and 127 and 28Reserved 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.

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.

 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":

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.

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.

 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.

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.

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.

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.

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 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.

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:

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.

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();

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:

PinsWhat they are
0 to 7The GPIO connector
8 to 39USB 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 110A 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 170Button 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 covers the controls.

Reference

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.