Remote Control for Flipper Zero via Raspberry Pi and Telegram

Remote Control for Flipper Zero via Raspberry Pi and Telegram

In this article, I’ll show you how to turn your Flipper Zero into a smart home remote that transmits commands sent via Telegram. To connect Flipper to Telegram, you’ll need an additional device—in my case, a Raspberry Pi.

Project Idea

The standard Flipper Zero firmware has a great feature that lets you create virtual copies of IR remotes. I have several devices at home that are controlled this way: a projector and some LED candles from Aliexpress. When I’m home, it’s easy to grab the Flipper, select the remote, and press a button, or do the same from the app that controls the device via phone.

But both methods require you to be physically near the device, since the phone connects to the Flipper via Bluetooth. So, I decided to connect the Flipper to a Raspberry Pi and control it through the serial port.

Copying Remotes

To use Flipper instead of a proprietary remote, you’ll need to copy the signals. In the main menu, select the Infrared app, then “Learn New Remote,” and press the buttons on your remote to copy them.

After copying all remotes, you’ll see a list of your remotes under the “Saved Remotes” tab. Selecting one shows the available buttons.

Each remote is essentially a file with a .ir extension, storing information about the protocol, address, and commands to send. For example:

Filetype: IR signals file
Version: 1
#
name: On
type: parsed
protocol: NEC
address: 00 00 00 00
command: 5E 00 00 00
#
name: Off
type: parsed
protocol: NEC
address: 00 00 00 00
command: 0C 00 00 00

Command Line Interface

The Flipper Zero developers created a set of commands for controlling the device from a computer via the command line. According to the official documentation, you can read and emulate signals, run programs, manage files, and more. The app also provides features like reading logs, transferring data to other Flipper Zeros, and other tricks.

You can send commands directly using any serial port monitor, Termius, PuTTY, or the CLI tab in the Flipper Lab app.

To control the Flipper with Python, you can use the PyFlipper library. However, I noticed some methods are outdated, so I made a small fix and will use my fork of the library. Since it’s not on PyPi, install it directly from the repository:

pip install git+https://github.com/AviPaperno/pyFlipper.git

Creating an Interface for Flipper Control

For convenience, let’s create our own interface—a wrapper over the mentioned library. The standard CLI shell doesn’t let you “send this command from this remote,” so we’ll implement it ourselves.

What we need: parse the file, read the command and address, reverse them (since for some reason they’re stored reversed), and use the library methods to make Flipper send the command to the address.

For working with IR signals, let’s create an IR class. We’ll also add some helper functions for readability and ease of use.

class IR:
    def __init__(self, name_, type_, protocol_=None, address_=None, command_=None):
        def reverse_address_or_cmd(data_str: str) -> str:
            return ''.join(data_str.split()[::-1])
        self.name = name_
        self.type = type_
        self.protocol = protocol_
        self.address = reverse_address_or_cmd(address_) if address_ else None
        self.command = reverse_address_or_cmd(command_) if command_ else None

    def __str__(self):
        return f"{self.name}({self.protocol}) ADDR: {self.address} CMD: {self.command}"

Function to convert a .ir file’s contents into a list of IR class instances:

def decode_ir_file(data):
    removed = data.split("#")[1:]
    result = {}
    for elem in removed:
        template = {"name_": None, "type_": None, "protocol_": None, "address_": None, "command_": None}
        for part in elem.strip().split("\n"):
            key, value = part.split(":")
            template[f"{key.strip()}_"] = value.strip()
        result[template["name_"]] = IR(**template)
    return result

Function to get a list of available remotes:

def get_list_of_ir(flipper_device: PyFlipper):
    return [elem["name"] for elem in flipper_device.storage.list(path="/ext/infrared").get('files')]

Function to get the device’s unique name:

def get_device_name(flipper_device: PyFlipper):
    return flipper_device.device_info.info().get("hardware_name")

Function to get data from a .ir file:

def get_ir_file_data(flipper_device: PyFlipper, file_name: str):
    filepath = f"/ext/infrared/{file_name}"
    return flipper_device.storage.read(file=filepath)

Main function to send a command to the Flipper:

def send_ir_command(flipper_device: PyFlipper, remotes: Dict, command_name: str):
    command = remotes.get(command_name)
    if command:
        flipper_device.ir.tx(command.protocol, command.address, command.command)

Telegram Bot

Now it’s time to write our interface. We’ll use Telegram, so we need a library for it. I recommend aiogram—it’s easy to write async code with it. The documentation is clear and well-written.

First, import everything you need for the bot and Flipper Zero:

import asyncio
from aiogram import Bot, Dispatcher, Router, types
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.filters.command import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from pyflipper.pyflipper import PyFlipper

And your own modules: flipperInterface and config. The config stores two variables: BOT_TOKEN (from @BotFather) and COM_PORT (the port address for the Flipper).

from config import BOT_TOKEN, COM_PORT
from flipperInterface import get_device_name, get_list_of_ir, get_ir_file_data, decode_ir_file, send_ir_command

Add a helper function to create a custom keyboard:

def create_keyboard(options):
    """Create a keyboard from a list of options"""
    return types.ReplyKeyboardMarkup(keyboard=[[types.KeyboardButton(text=f"{elem}")] for elem in options])

The bot will have three states, grouped in a StatesGroup subclass:

class CurrentState(StatesGroup):
    start = State()
    selection_remote = State()
    selection_command = State()
  • start: when the bot just started
  • selection_remote: when the user selects a remote
  • selection_command: when the user “presses” a virtual remote button

Function to create a connection to the Flipper:

async def get_flipper():
    """Async context for working with Flipper"""
    try:
        flipper = PyFlipper(com=COM_PORT)
        return flipper
    except Exception as e:
        print(f"Error connecting to Flipper: {e}")
        return None

Create a router:

router = Router()

Now, let’s describe the handlers. The first handler runs when the user sends the /start command. It connects to the Flipper, sends a welcome message, creates a keyboard with available remotes, and moves the bot to the next state.

@router.message(CurrentState.start)
@router.message(Command("start"))
async def cmd_start(message: types.Message, state: FSMContext):
    flipper = await get_flipper()
    if flipper:
        await message.answer(
            f"Hi, I’m a bot for controlling IR devices via FlipperZero *{get_device_name(flipper)}*.\nChoose the remote you need.",
            reply_markup=create_keyboard(get_list_of_ir(flipper)),
            parse_mode="Markdown"
        )
        await state.set_state(CurrentState.selection_remote)
    else:
        await message.answer("Could not connect to Flipper. Please try again later.")

The second handler processes the remote selection, reads the file, checks for available buttons, and shows a keyboard with available commands for the selected remote:

@router.message(CurrentState.selection_remote)
async def cmd_select_ir(message: types.Message, state: FSMContext):
    flipper = await get_flipper()
    if flipper:
        available_remotes = get_list_of_ir(flipper)
        if message.text in available_remotes:
            ir_data = get_ir_file_data(flipper, message.text)
            if ir_data:
                currentIR = decode_ir_file(ir_data)
                await state.update_data(currentIR=currentIR)
                await state.set_state(CurrentState.selection_command)
                await message.answer(
                    "Here are the available commands. Choose the one you need.",
                    reply_markup=create_keyboard(list(currentIR.keys()) + ["Back"])
                )
            else:
                await message.answer("Could not find IR file. Try another remote.")
        else:
            await message.answer("Invalid remote selection. Please try again.")
    else:
        await message.answer("Could not connect to Flipper.")

The last handler checks if the command exists. If yes, it sends it to the Flipper. If the command is “Back,” it returns to the previous state.

@router.message(CurrentState.selection_command)
async def cmd_select_command(message: types.Message, state: FSMContext):
    if message.text == "Back":
        await state.set_state(CurrentState.start)
        await cmd_start(message, state)
    else:
        data = await state.get_data()
        flipper = await get_flipper()
        if flipper and message.text in data["currentIR"]:
            send_ir_command(flipper, data["currentIR"], message.text)
            await message.answer("Command executed!")
        else:
            await message.answer("Error: command not recognized or Flipper unavailable.")

Now, just start polling for new messages. Since my Raspberry Pi doesn’t have SSL certificates, I specified to work without them:

async def main():
    session = AiohttpSession()
    session._connector_init = {'ssl': False}
    bot = Bot(token=BOT_TOKEN, session=session)
    dp = Dispatcher()
    dp.include_router(router)
    await dp.start_polling(bot)

if __name__ == "__main__":
    asyncio.run(main())

Deployment

Now, let’s deploy the bot. Since we need a direct Flipper connection, we can’t use a remote service. We’ll use our local devices.

Theoretically, the bot can run on any computer with Python and the required libraries. But since Raspberry Pi is often used as a home server, let’s cover deployment on it.

  1. Clone the project repository:
    git clone https://github.com/AviPaperno/FlipperIRcontroller
  2. Install dependencies:
    pip3 install -r requirements.txt
  3. Edit config.py: copy your @BotFather token and specify the path to your Flipper connected via USB. To find the path, run:
    ls /dev/serial/by-id/

    You’ll see your device’s name. The full path will look like:

    /dev/serial/by-id/usb-Flipper_Devices_Inc._Flipper_<NAME>_flip_<NAME>-if00

    Here, <NAME> is your unique device name.

  4. Create a service:
    sudo nano /etc/systemd/system/flipperBot.service

    Add the following:

    [Service]
    ExecStart=python3 main.py
    WorkingDirectory=/path/to/your/FlipperIRcontroller
    Restart=always
    User=user_name
    RestartSec=5
    StandardOutput=append:/path/to/your/FlipperIRcontroller/bot.log
    StandardError=append:/path/to/your/FlipperIRcontroller/bot.log
    
    [Install]
    WantedBy=multi-user.target
    

    Replace /path/to/your/ with your project directory and user_name with your username.

  5. Start the service:
    sudo systemctl daemon-reload
    sudo systemctl enable flipperBot.service
    sudo systemctl start flipperBot.service
    

    Check its status:

    sudo systemctl status flipperBot.service

    If it looks like the image below, everything is OK and you can use the bot!

Conclusion

In this article, we learned how to use Flipper Zero as a universal remote controlled from a Raspberry Pi. We wrote a Telegram bot to send commands, since it’s a simple and universal method. However, if you’re building a smart home, you can integrate Flipper as well—for example, via Home Assistant.

Flipper Zero may not be brand new, but it still feels fresh. Thanks to the developers for not only releasing a cool device but also continuing support, and to the active community that creates modules, firmware, and apps for Flipper.

Source

Leave a Reply