FieldStation42 can be controlled with any IR remote using a Flirc USB receiver, or with a keyboard directly. You can also change channels and read player status from external scripts using a simple socket file interface.
Quick Start with Flirc USB
Flirc USB lets you use any IR remote with your Raspberry Pi. It maps remote buttons to keyboard keys, which FieldStation42's remote controller script then translates into commands.
1. Get and Program Flirc
Pick up a Flirc USB receiver and install the Flirc software on your computer. Use the Flirc GUI to program your remote buttons to the following keyboard keys:
| Remote Button | Map to Key |
|---|---|
| Numbers 0-9 | 0 through 9 |
| Home / Guide | Home |
| Volume Up | Right Arrow |
| Volume Down | Left Arrow |
| Mute | m |
| Channel Up | Up Arrow |
| Channel Down | Down Arrow |
| Last Channel | Backspace |
| Subtitles On/Off | v |
| Subtitle Track | j |
| Audio Track | a |
| Power / Stop | End |
Then plug the Flirc receiver into your Raspberry Pi.
2. Install Dependencies
pip install evdev requests
3. Run the Remote Controller
python3 fs42/pi/remote_controller.py
On Linux Mint, Ubuntu, and some other distributions you need sudo to access input devices. When using sudo, make sure to point at the virtual environment's Python directly rather than the system Python:
sudo env/bin/python3 fs42/pi/remote_controller.py
Using sudo python3 instead will use the system Python and won't have access to the packages installed in your FieldStation42 environment.
Remote Functions Reference
| Function | Default Key | Description |
|---|---|---|
| Numbers 0-9 | 0-9 |
Channel selection |
| Show Guide | Home |
Display program guide |
| Volume Up | Right Arrow |
Increase volume by 2% |
| Volume Down | Left Arrow |
Decrease volume by 2% |
| Mute | m |
Mute or unmute volume |
| Channel Up | Up Arrow |
Next channel |
| Channel Down | Down Arrow |
Previous channel |
| Last Channel | Backspace |
Jump back to the previously watched channel |
| Toggle Subtitles | v |
Show or hide subtitles |
| Cycle Subtitles | j |
Step through the available subtitle tracks |
| Cycle Audio | a |
Step through the available audio tracks |
| Power/Stop | End |
Toggle the FieldStation42 services on and off |
| Exit | Esc |
Exit remote controller |
Channel Selection
- Press
1, wait one second: switch to channel 1 - Press
1,2, wait one second: switch to channel 12 - Press
1,2,3: immediately switch to channel 123
If the station you are on has parental controls and is waiting for its PIN, the number keys enter PIN digits instead of selecting a channel. Channel up/down, the guide button, and the last-channel button still work, so you can leave the station without entering the PIN.
Subtitles and Audio Tracks
The subtitle and audio keys act on whatever is playing right now. j steps through each subtitle track in the current file and then back to off, while v hides and restores subtitles without changing which track is selected. a steps through the audio tracks, which is useful for media carrying alternate language dubs or commentary.
Your track choice sticks for the rest of the program. The player reapplies it each time the program resumes after a commercial break, and clears it when the next program starts. Only choices made while the program itself is on screen are remembered - pressing these keys during a commercial or bump affects just that clip and is not carried forward. Nothing is written to disk, so selections are lost when the player restarts.
Most media has a single audio track and no subtitles at all - on those files, pressing these keys will not appear to do anything because there is nothing to cycle to.
The same subtitle and audio cycling is available on the web remote.
The Power Button
By default the power key does not simply stop the player. USE_SYSTEMCTL is enabled at the top of remote_controller.py, so pressing it checks whether fs42.service is running and then starts or stops every service listed in SYSTEMCTL_TO_TOGGLE - the player, the cable box and the OSD. That gives a real power button that brings the whole system up and down.
If you would rather have the key only stop the player, set use_systemctl to false in the settings file below and it will call the player's stop command instead. The list of services it toggles can be changed there too.
The Settings File
Create runtime/remote_controller.json to change how the remote controller behaves without editing the script. The file is optional - when it is missing, the built-in defaults are used.
{
"use_systemctl": true,
"systemctl_to_toggle": [
"fs42.service",
"fs42-cable-box.service",
"fs42-osd.service"
],
"key_mappings": {
"volume_up": "pageup",
"volume_down": "pagedown"
}
}
| Setting | Type | Description |
|---|---|---|
use_systemctl |
boolean | Whether the power key toggles services instead of only stopping the player |
systemctl_to_toggle |
list | Service names the power key starts and stops |
key_mappings |
object | Function names mapped to key names - see Customizing Key Mappings below |
Every setting is optional and anything you leave out keeps its default, so a file containing nothing but use_systemctl is perfectly valid. Note that JSON booleans are lowercase true and false, not Python's True and False.
The file is read once when the remote controller starts, so restart it after making changes. If it cannot be read - a stray comma, a missing brace - the remote controller reports the error and carries on with the defaults rather than refusing to start.
Customizing Key Mappings
You don't need Flirc to use the remote controller. Any keyboard works. Add a key_mappings block to runtime/remote_controller.json listing only the functions you want to change:
{
"key_mappings": {
"volume_up": "pageup",
"volume_down": "pagedown"
}
}
Everything you leave out keeps its default key. The example above moves volume onto the page keys while the guide stays on Home, mute stays on m, and the rest are untouched.
These are the function names and the defaults you are overriding:
{
"key_mappings": {
"show_guide": "home",
"volume_up": "right",
"volume_down": "left",
"channel_up": "up",
"channel_down": "down",
"last_channel": "backspace",
"mute": "m",
"toggle_subtitles": "v",
"cycle_subtitles": "j",
"cycle_audio": "a",
"power_stop": "end",
"exit": "esc"
}
}
The number keys are always handled as channel entry and are not part of key_mappings.
Editing DEFAULT_KEY_MAPPINGS at the top of remote_controller.py still works, but the settings file is the better place - it survives updates to the code.
Taking a Key From Another Function
If you assign a key that another function holds by default, that function gives the key up rather than competing for it. Mapping channel_up to right means the right arrow changes channels, and volume up is left with no key at all:
{
"key_mappings": {
"channel_up": "right"
}
}
When you are rearranging keys this way, map the displaced function somewhere too:
{
"key_mappings": {
"channel_up": "right",
"volume_up": "pageup"
}
}
Available Key Names
- Letters:
athroughz - Numbers:
0through9 - Function keys:
f1throughf12 - Arrows:
up,down,left,right - Navigation:
home,end,pageup,pagedown,insert,delete - Common:
space,enter,tab,backspace,esc - Modifiers:
leftshift,rightshift,leftctrl,rightctrl,leftalt,rightalt
Selecting an Input Device
By default the remote controller auto-detects your Flirc device, or falls back to the first available input device. If it picks the wrong one, or you want to be explicit, start by listing what's available:
python3 fs42/pi/remote_controller.py --list-devices
On systems that require sudo for input device access:
sudo env/bin/python3 fs42/pi/remote_controller.py --list-devices
You'll see output like this:
Available input devices:
0: Flirc (/dev/input/event4)
1: Logitech M510 (/dev/input/event3)
2: Power Button (/dev/input/event1)
Using default device: Flirc
Then use -d to specify which device to use, by index, name pattern, or full path:
# By index
python3 fs42/pi/remote_controller.py -d 0
# By name pattern (matches first device containing that text)
python3 fs42/pi/remote_controller.py -d flirc
python3 fs42/pi/remote_controller.py -d keyboard
# By full device path
python3 fs42/pi/remote_controller.py -d /dev/input/event4
You can also set the device via environment variable instead of passing -d every time:
FS42_INPUT_DEVICE=flirc python3 fs42/pi/remote_controller.py
Server Location
If FieldStation42 is running on a different machine, set the host and port:
FS42_HOST=192.168.1.100 FS42_PORT=8080 python3 remote_controller.py
Or edit the values directly at the top of remote_controller.py.
Button Callbacks
You can run a script or shell command whenever a button is pressed. Create runtime/remote_callback_map.json:
{
"up": "echo Up pressed",
"end": "path/to/power_pressed.sh"
}
Any valid shell command works here, including Python scripts.
The keys in this file are key names, not function names - use end rather than power_stop, and backspace rather than last_channel. A callback runs in addition to the normal function for that key, so mapping end still toggles the services as usual. The file is read once when the remote controller starts, so restart it after making changes.
Troubleshooting
Permission denied: Run with sudo env/bin/python3 to access input devices. Using sudo python3 will use the system Python instead of the virtual environment.
No devices found: Check that Flirc is plugged in and verify with ls /dev/input/event*. Run sudo evtest to see what keys your device actually sends.
Remote not responding: Verify FieldStation42 is running, check the host and port settings, and use the Flirc GUI to confirm key mappings are set correctly. Use the "Full Keyboard" controller template in the Flirc GUI for best results.
Changing Channels from External Scripts
FieldStation42 monitors runtime/channel.socket for JSON-formatted channel commands. This lets any external script or program change the channel without going through the remote controller.
Tune to a specific channel:
{"command": "direct", "channel": 42}
Tune up one channel:
{"command": "up", "channel": -1}
Tune down one channel:
{"command": "down", "channel": -1}
Example: Python Script
import json
command = {"command": "direct", "channel": 42}
with open('runtime/channel.socket', 'w') as fp:
fp.write(json.dumps(command))
Reading Player Status
The player publishes its current status to runtime/play_status.socket as JSON. External scripts can read this to know what is currently playing.
Player is active:
{"status": "playing", "network_name": "MyStation", "channel_number": 42, "timestamp": "2025-02-03T08:13:46.431558", "awaiting_pin": false, "pin_digits_entered": 0}
Player is stopped:
{"status": "stopped", "network_name": "", "channel_number": -1, "timestamp": "2025-02-03T08:13:46.431558"}