Introducción

El Flipper Zero es un dispositivo multifunción de hacking, pruebas de seguridad y exploración de protocolos de radiofrecuencia (RF). Entre sus características más destacadas está la capacidad de transmitir y recibir señales SubGHz, utilizando el chip Texas Instruments CC1101, un transceptor de radio programable de bajo consumo.

SubGHz hace referencia al rango de frecuencias de radio por debajo de 1 GHz (típicamente entre 300 MHz y 928 MHz, según la región). Estas frecuencias son usadas por dispositivos como controles remotos de garajes o sensores inalámbricos (temperatura, movimiento, alarmas).

El dispositivo puede grabar señales inalámbricas y reproducirlas (dependiendo de la codificación). Soporta las modulaciones AM y FM. Por defecto se encuentran configuradas las sub-modulaciones OOK (AM) y 2FSK (FM).

  • OOK_270 → OOK con ancho de banda 270 kHz.
  • OOK_650 → OOK con 650 kHz.
  • 2FSK_238 → 2-FSK con desviación de 2,38 kHz.
  • 2FSK_476 → 2-FSK con desviación de 47,6 kHz.

Esto convierte al Flipper Zero en una herramienta versátil para generar, analizar y probar señales SubGHz de manera sencilla. Se ha desarrollado un script que crea archivos en el formato .sub que soporta el dispositivo para enviar señales mediante las duraciones de los pulsos. Por ejemplo si se quiere enviar los datos binarios 1001110 y la duración mínima del pulso es de 100 ms, se enviarán los pulsos con duraciones 100 -200 300 -100

Uso del SubGhz Generator

Este script permite crear archivos .sub para el Flipper Zero en formato RAW o BinRAW, los cuales son usados para almacenar y reproducir señales capturadas o generadas. En la documentación se recoge el formato de los archivos en detalle

  • RAW: Representa las señales como secuencias de tiempos (RAW_Data) de pulsos.
  • BinRAW: Representa las señales como cadenas binarias organizadas en bloques de datos (Bit_RAW y Data_RAW).

El script convierte texto, una secuencia binaria o duraciones predefinidas en estos formatos para que puedan ser usados por el Flipper Zero. El script se ejecuta desde la línea de comandos:

python3 flipper_subghz.py [modo] [archivo_salida] [opciones]

Modo RAW

Genera un archivo .sub con datos en formato RAW, por ejemplo: crear un archivo desde texto codificado:

python3 flipper_subghz.py raw out.sub --freq 433920000 --preset OOK_650 --text "HELLO" --te 500
  • raw → modo RAW.
  • out.sub → archivo de salida.
  • --freq → frecuencia en Hz (ej. 433.92 MHz, común en controles remotos).
  • --preset → configuración del CC1101 (OOK_650, OOK_270, etc.).
  • --text → el mensaje a convertir en bits.
  • --te → unidad de tiempo en microsegundos para definir la duración de cada bit.

Ejemplo con binario:

python3 flipper_subghz.py raw garage.sub --freq 433920000 --preset OOK_270 --binary 1010101 --te 600

Modo BinRAW

Genera un archivo .sub con datos en formato BinRAW, por ejemplo, con texto:

python3 flipper_subghz.py binraw out.sub --freq 433920000 --preset OOK_650 --text "HELLO" --te 600

Ejemplo con binario:

python3 flipper_subghz.py binraw garage.sub --freq 433920000 --preset 2FSK_238 --binary 11001100 --te 500

Ejemplo con bloques manuales:

python3 flipper_subghz.py binraw out.sub --freq 433920000 --preset 2FSK_476 --blocks 8:FF 16:AABB --te 600
  • 8:FF: bloque de 8 bits con valor hexadecimal FF.
  • 16:AABB: bloque de 16 bits con valor AABB.

Ejemplos prácticos

  1. Recrear señal simple de un control remoto:
    python3 flipper_subghz.py raw remote.sub --freq 433920000 --preset OOK_270 --binary 10110011 --te 500
    
  2. Convertir texto en señal SubGHz:
    python3 flipper_subghz.py binraw text_signal.sub --freq 868350000 --preset 2FSK_238 --text "TEST" --te 700
    
  3. Definir manualmente bloques de datos para pruebas:
    python3 flipper_subghz.py binraw custom.sub --freq 315000000 --preset OOK_650 --blocks 12:ABC 8:FF --te 400
    

Código fuente

#!/usr/bin/env python3
"""
Optimized Flipper Zero .sub generator (RAW & BinRAW)
"""

import argparse
import sys
from typing import List, Tuple

# --- Constants
MAX_RAW_VALUES_PER_LINE = 512
MAX_BINRAW_BITS = 4096

# --- Presets
PRESET_MAP = {
    "OOK_270": "FuriHalSubGhzPresetOok270Async",
    "OOK_650": "FuriHalSubGhzPresetOok650Async",
    "2FSK_238": "FuriHalSubGhzPreset2FSKDev238Async",
    "2FSK_476": "FuriHalSubGhzPreset2FSKDev476Async",
}


# --- Core Classes
class SubGhzSubFile:
    FILETYPE_RAW = "Flipper SubGhz RAW File"
    VERSION = 1

    def __init__(self, frequency: int, preset_key: str):
        if preset_key not in PRESET_MAP:
            raise ValueError(f"Unknown preset '{preset_key}'. Allowed: {', '.join(PRESET_MAP.keys())}")
        self.frequency = frequency
        self.preset = PRESET_MAP[preset_key]

    def _write_header(self, f, filetype_str: str):
        f.write(f"Filetype: {filetype_str}\nVersion: {self.VERSION}\nFrequency: {self.frequency}\n".encode())

    def _write_preset(self, f):
        f.write(f"Preset: {self.preset}\n".encode())

    def write_raw(self, filename: str, raw_timings_lines: List[List[int]]):
        with open(filename, "wb") as f:
            self._write_header(f, self.FILETYPE_RAW)
            self._write_preset(f)
            f.write(b"Protocol: RAW\n")
            for line in raw_timings_lines:
                for i in range(0, len(line), MAX_RAW_VALUES_PER_LINE):
                    chunk = line[i:i+MAX_RAW_VALUES_PER_LINE]
                    f.write(f"RAW_Data: {' '.join(map(str, chunk))}\n".encode())

    def write_binraw(self, filename: str, total_bits: int, te: int, blocks: List[Tuple[int, bytes]]):
        with open(filename, "wb") as f:
            self._write_header(f, self.FILETYPE_RAW)
            self._write_preset(f)
            f.write(b"Protocol: BinRAW\n")
            f.write(f"Bit: {total_bits}\nTE: {te}\n".encode())
            for bitlen, data in blocks:
                f.write(f"Bit_RAW: {bitlen}\n".encode())
                f.write(f"Data_RAW: {' '.join(f'{b:02X}' for b in data)}\n".encode())


# --- Utilities
def bits_to_bytes(bits: str) -> bytes:
    padded = bits + '0' * ((8 - len(bits) % 8) % 8)
    return int(padded, 2).to_bytes(len(padded)//8, 'big') if bits else b''

def string_to_bits_utf8(text: str) -> str:
    return ''.join(f'{b:08b}' for b in text.encode('utf-8'))

def bits_to_raw_timings(bits: str, te: int) -> List[int]:
    if not bits:
        return []
    timings = [te]  # first bit positive
    last_sign = 1
    last_bit = bits[0]
    for bit in bits[1:]:
        if bit == last_bit:
            timings[-1] += te * last_sign  # accumulate duration
        else:
            last_sign *= -1
            timings.append(te * last_sign)
            last_bit = bit
    return timings

def split_bits_into_blocks(bits: str, max_bits: int) -> List[str]:
    return [bits[i:i+max_bits] for i in range(0, len(bits), max_bits)]


# --- CLI
def build_arg_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="flipper_subghz.py",
        description="Generate Flipper Zero .sub files (RAW/BinRAW) from timings, binary strings, or UTF-8 text.",
        epilog="Examples:\n"
               "RAW from text: flipper_subghz.py raw out.sub --freq 433920000 --preset OOK_650 --text 'HELLO' --te 500\n"
               "BinRAW from binary: flipper_subghz.py binraw out.sub --freq 433920000 --preset OOK_650 --binary 1010101 --te 600\n",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    raw_p = subparsers.add_parser("raw", help="Create RAW .sub file")
    raw_p.add_argument("output")
    raw_p.add_argument("--freq", type=int, required=True)
    raw_p.add_argument("--preset", choices=PRESET_MAP.keys(), required=True)
    grp_raw = raw_p.add_mutually_exclusive_group(required=True)
    grp_raw.add_argument("--timings", nargs="+")
    grp_raw.add_argument("--binary")
    grp_raw.add_argument("--text")
    raw_p.add_argument("--te", type=int)
    raw_p.add_argument("--preamble")

    bin_p = subparsers.add_parser("binraw", help="Create BinRAW .sub file")
    bin_p.add_argument("output")
    bin_p.add_argument("--freq", type=int, required=True)
    bin_p.add_argument("--preset", choices=PRESET_MAP.keys(), required=True)
    grp_bin = bin_p.add_mutually_exclusive_group(required=True)
    grp_bin.add_argument("--blocks", nargs="+")
    grp_bin.add_argument("--binary")
    grp_bin.add_argument("--text")
    bin_p.add_argument("--te", type=int, required=True)
    bin_p.add_argument("--preamble")
    return parser


# --- Mode Handlers
def run_raw_mode(args):
    sf = SubGhzSubFile(args.freq, args.preset)
    if args.timings:
        lines = [list(map(int, g.strip().split())) for g in args.timings]
        sf.write_raw(args.output, lines)
        print(f"Wrote RAW .sub to {args.output}")
        return
    if not args.te:
        sys.exit("Error: --te required for binary/text input")
    bits = args.binary if args.binary else string_to_bits_utf8(args.text)
    if args.preamble:
        bits = args.preamble + bits
    timings = bits_to_raw_timings(bits, args.te)
    sf.write_raw(args.output, [timings])
    print(f"Wrote RAW .sub to {args.output}")


def run_binraw_mode(args):
    sf = SubGhzSubFile(args.freq, args.preset)

    if args.blocks:
        blocks, total_bits = [], 0
        for b in args.blocks:
            bitlen_str, hexdata = b.split(":")
            bitlen = int(bitlen_str)
            data = bytes.fromhex(hexdata)
            blocks.append((bitlen, data))
            total_bits += bitlen

        if total_bits > MAX_BINRAW_BITS:
            sys.exit(f"Error: total bits ({total_bits}) exceed 4096 BitRAW limit.")

        sf.write_binraw(args.output, total_bits, args.te, blocks)
        print(f"Wrote BinRAW .sub to {args.output}, total bits: {total_bits}")
        return

    bits = args.binary if args.binary else string_to_bits_utf8(args.text)
    if args.preamble:
        bits = args.preamble + bits

    total_bits = len(bits)
    if total_bits > MAX_BINRAW_BITS:
        sys.exit(f"Error: total bits ({total_bits}) exceed 4096 BitRAW limit.")

    block_bytes = bits_to_bytes(bits)
    blocks_for_file = [(total_bits, block_bytes)]

    sf.write_binraw(args.output, total_bits, args.te, blocks_for_file)
    print(f"Wrote BinRAW .sub to {args.output}, total bits: {total_bits}")


# --- Main
def main():
    args = build_arg_parser().parse_args()
    if args.command == "raw":
        run_raw_mode(args)
    else:
        run_binraw_mode(args)


if __name__ == "__main__":
    main()