Introduction

The Flipper Zero is a multifunction device for hacking, security testing and radio frequency protocol exploration. One of its most highlighted features is the ability to transmit and receive SubGHz signals, using the Texas Instruments CC1101 chip, a programmable low-power RF transmitter.

SubGHz refers to the range of radio frequencies below 1 GHz (typically between 300 MHz and 928 MHz, depending on the region). These frequencies are used by devices such as remote garage door controls or wireless sensors (temperature, movement, alarms).

The device can record and play back wireless signals (depending on encoding). It supports AM and FM modulation. By default, OOK (AM) and 2FSK (FM) sub-modulations are configured.

  • OOK_270 → OOK with a bandwidth of 270 kHz.
  • OOK_650 → OOK with a bandwidth of 650 kHz.
  • 2FSK_238 → 2-FSK with a deviation of 2.38 kHz.
  • 2FSK_476 → 2-FSK with a deviation of 47.6 kHz.

This makes the Flipper Zero a versatile tool for generating, analyzing and testing SubGHz signals in a straightforward manner. A script has been developed to create .sub files that support the device for sending signals using pulse durations. For example, if you want to send binary data 1001110 with a minimum pulse duration of 100 ms, the pulses will be sent with durations 100 -200 300 -100.

Use of the SubGhz Generator

This script allows creating .sub files for the Flipper Zero in RAW or BinRAW formats, which are used to store and play back captured or generated signals. In the documentation is detailed information on file format.

  • RAW: Represents signals as sequences of times (RAW_Data) of pulses.
  • BinRAW: Represents signals as binary chains organized into data blocks (Bit_RAW and Data_RAW).

The script converts text, binary sequences or predefined durations into these formats so they can be used by the Flipper Zero. The script is executed from the command line:

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

RAW Mode

Generate a .sub file with data in RAW format, for example: create an archive from text encoded:

python3 flipper_subghz.py raw out.sub --freq 433920000 --preset OOK_650 --text "HELLO" --te 500
  • raw → RAW mode.
  • out.sub → output file.
  • --freq → frequency in Hz (e.g. 433.92 MHz, common for remote controls).
  • --preset → CC1101 configuration (OOK_650, OOK_270, etc.).
  • --text → the message to convert into bits.
  • --te → time unit in microseconds to define each bit duration.

Example with binary:

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

BinRAW Mode

Generate a .sub file with data in BinRAW format, for example, with text:

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

Example with binary:

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

Example with manual blocks:

python3 flipper_subghz.py binraw out.sub --freq 433920000 --preset 2FSK_476 --blocks 8:FF 16:AABB --te 600
  • 8:FF: block of 8 bits with hexadecimal value FF.
  • 16:AABB: block of 16 bits with value AABB.

Examples

  1. Recreate Simple Signal from Remote Control:
    python3 flipper_subghz.py raw remote.sub --freq 433920000 --preset OOK_270 --binary 10110011 --te 500
    
  2. Convert Signal from Text:
    python3 flipper_subghz.py binraw text_signal.sub --freq 868350000 --preset 2FSK_238 --text "TEST" --te 700
    
  3. Define manually data blocks for testing:
    python3 flipper_subghz.py binraw custom.sub --freq 315000000 --preset OOK_650 --blocks 12:ABC 8:FF --te 400
    

Source code

#!/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()