subghz

package
v0.660.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 9, 2026 License: AGPL-3.0 Imports: 8 Imported by: 0

Documentation

Overview

Package subghz provides pure-Go classifiers for common Sub-GHz radio protocols captured by the Flipper Zero. It parses Flipper .sub capture files, demodulates raw pulse sequences, and identifies protocols without invoking the urh-ng Docker container bridge. urh-ng remains the fallback for unknown or exotic protocols not covered here.

Supported protocols (20)

The following 20 protocols are implemented. Where a protocol from the original list had insufficient public documentation to implement a clean-room decoder, a better-documented alternative was substituted (noted below).

  1. Princeton PT2262 — 12-bit address + 4-bit data, OOK, PWM encoding. Ref: Princeton Technology PT2262 datasheet (rev 1.6).

  2. CAME — 12-bit fixed code, OOK, Italian gate openers. Ref: CAME protocol description, DarkFlippers/unleashed-firmware.

  3. Holtek HT12E — 8-bit address + 4-bit data, OOK, PWM encoding. Ref: Holtek HT12E encoder datasheet.

  4. Linear — 8-bit code, OOK, US garage doors (multi-code). Ref: Linear compatibility notes; rtl_433 linear.c.

  5. NICE FloR-S — 52-bit rolling code (KeeLoq variant), OOK. Ref: NICE FloR-S protocol white paper; Flipper firmware.

  6. KeeLoq HCS200/300 — 32-bit hopping + 32-bit fixed, OOK. Ref: Microchip AN66115; internal/keeloq package.

  7. Faac SLH — 64-bit dynamic code, OOK. Ref: FAAC SLH protocol notes; DarkFlippers/unleashed-firmware.

  8. Beninca — 12-bit OOK, Italian gate openers (CAME variant). Ref: Beninca protocol documentation; Flipper firmware lib/subghz.

  9. Prastel — 12-bit OOK, Manchester-like timing. Ref: Prastel MRC12 protocol; DarkFlippers/unleashed-firmware.

  10. Ansonic — 12-bit OOK with Manchester modulation. Ref: Ansonic AS2260R datasheet; rtl_433 source.

  11. Smartgate — 24-bit OOK, proprietary rolling code. Ref: Flipper firmware lib/subghz/protocols/smartgate.c.

  12. Hormann HSM — 44-bit BiSS/FSK, German garage doors. NOTE: Hormann HSM uses a proprietary BiSS protocol with encrypted rolling codes. Insufficient public documentation exists for a full clean-room decoder. SUBSTITUTED with Aerolite (24-bit OOK), a well-documented Italian gate protocol present in both Flipper and rtl_433 catalogues. Ref: Flipper firmware lib/subghz/protocols/nero_radio.c (Aerolite).

  13. Doitrand — 12-bit OOK, French gate openers. Ref: Flipper firmware lib/subghz/protocols/doitrand.c.

  14. Linkmaster — 12-bit OOK. NOTE: Linkmaster has no reliable public protocol documentation. SUBSTITUTED with Secplus v1 (Security+ v1, 40-bit, Chamberlain/LiftMaster). Ref: Weston Embedded "Security+ Protocol Analysis"; Flipper firmware.

  15. Magicode — 28-bit OOK, UK/EU remotes. Ref: Flipper firmware lib/subghz/protocols/magicode.c.

  16. Honeywell WS — 24-bit ASK, wireless sensors (5800 series). Ref: rtl_433 honeywell.c; Honeywell 5800 datasheet.

  17. Princeton-Holtek — composite clone of PT2262/HT12E, OOK. Ref: Clone chip markings; Flipper firmware lib/subghz/protocols/princeton.c.

  18. CAME TWIN — 12-bit + alternative timing variant, OOK. Ref: CAME TWIN protocol; Flipper firmware lib/subghz/protocols/came_tw.c.

  19. Aprimatic — 24-bit OOK, Italian/Spanish gate openers. Ref: Flipper firmware lib/subghz/protocols/aprimatic.c.

  20. Phoenix V2 — 12-bit OOK (Italy/EU), rolling-code variant. Ref: Flipper firmware lib/subghz/protocols/phoenix_v2.c.

Architecture

Each protocol implements the Protocol interface. NewClassifier returns a Classifier pre-loaded with all 20 protocols. Classifier.Classify tries every registered protocol against the demodulated pulses and returns the top-N matches ordered by confidence.

The SubFile parser ingests Flipper .sub format (key/value text with a "RAW_Data:" pulse list). The modulation layer (DemodulateOOK, DemodulatePWM, DemodulateManchester) converts pulse durations to bits.

An [Encoder] helper in encode.go synthesises .sub fixtures for round-trip testing without external hardware.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BitsToBytes

func BitsToBytes(bits []byte) []byte

BitsToBytes packs a bit slice (MSB first) into bytes, zero-padding the last byte if the slice length is not a multiple of 8.

func BytesToBits

func BytesToBits(data []byte, n int) []byte

BytesToBits unpacks bytes to a bit slice (MSB first), n bits total. If n > len(data)*8 the extra bits are zero.

func DemodulateManchester

func DemodulateManchester(pulses []int) []byte

DemodulateManchester decodes Manchester-encoded pulses to bits.

Manchester encoding uses transitions mid-symbol:

  • A low-to-high transition (short space then short mark) = 0 (IEEE 802.3)
  • A high-to-low transition (short mark then short space) = 1 (IEEE 802.3)

The decoder reconstructs the bit stream from alternating mark/space pairs. The median pulse duration serves as the half-symbol reference (TE). Pulses roughly equal to TE are half-symbols; pulses ≈ 2×TE are full symbols (no transition, so the previous bit repeats).

func DemodulateOOK

func DemodulateOOK(pulses []int) []byte

DemodulateOOK decodes On-Off Keying (OOK) pulses to bits.

OOK encodes a "1" as a long mark pulse and a "0" as a short mark pulse (or vice versa). The decoder classifies each mark (positive) pulse as 1 or 0 by comparing its duration to the median mark duration. Space (negative) pulses are used only as separators and are not decoded.

The returned slice has one bit per mark pulse (1 or 0).

func DemodulatePWM

func DemodulatePWM(pulses []int, oneRatio float64) []byte

DemodulatePWM decodes Pulse Width Modulation (PWM) encoded pulses to bits.

PWM (also called Pulse Width Modulation) encodes bits via the duration of a mark pulse relative to TE (the base pulse unit). oneRatio specifies the multiplier above which a mark pulse is decoded as "1"; pulses below are "0".

TE is estimated as the 25th-percentile mark duration (the smaller cluster centre), which is robust when "1" bits (long marks) outnumber "0" bits (short marks) in the captured payload.

Common values: Princeton PT2262 uses oneRatio = 2.0 (marks ≥ 2×TE are "1", since "1" = 3×TE and "0" = 1×TE, with the midpoint at 2×TE). The returned slice has one bit per mark pulse.

func EncodeManchesterPulses

func EncodeManchesterPulses(bits []byte, te, repeat int) []int

EncodeManchesterPulses synthesises a Manchester-encoded pulse sequence. Each bit occupies two half-symbol slots (each TE microseconds wide). IEEE 802.3 convention: 0 = low-to-high, 1 = high-to-low.

func EncodePWMPulses

func EncodePWMPulses(bits []byte, te, syncHigh, syncLow, oneHigh, oneLow, zeroHigh, zeroLow, repeat int) []int

EncodePWMPulses synthesises a raw pulse sequence for a PWM/OOK frame.

Parameters:

  • bits : the payload bit sequence (1 or 0 per element)
  • te : timing element in microseconds (shortest pulse unit)
  • syncHigh : sync mark duration in TE units (0 to omit sync)
  • syncLow : sync space duration in TE units (0 to omit sync)
  • oneHigh : mark duration for bit-1 in TE units
  • oneLow : space duration for bit-1 in TE units
  • zeroHigh : mark duration for bit-0 in TE units
  • zeroLow : space duration for bit-0 in TE units
  • repeat : number of times to repeat the full frame (minimum 1)

func SubFileString

func SubFileString(frequency uint64, preset string, pulses []int) string

SubFileString serialises a pulse slice as a minimal Flipper .sub file (Protocol: RAW). The result can be written directly to a .sub file or base64-encoded for the subghz_classify tool.

Types

type Classifier

type Classifier struct {
	// contains filtered or unexported fields
}

Classifier tries every registered protocol against a pulse sequence and returns the top matches by confidence.

func NewClassifier

func NewClassifier() *Classifier

NewClassifier returns a Classifier pre-loaded with all 32 protocol decoders.

func (*Classifier) Classify

func (c *Classifier) Classify(pulses []int, n int) []Match

Classify attempts every registered protocol decoder against pulses and returns the top-n matches ordered by descending confidence. When n <= 0 all matches with confidence > 0 are returned.

type JammingAnalysis added in v0.514.0

type JammingAnalysis struct {
	SamplesAnalyzed int `json:"samples_analyzed"`

	MinDbm        float64 `json:"min_dbm"`
	MaxDbm        float64 `json:"max_dbm"`
	MeanDbm       float64 `json:"mean_dbm"`
	MedianDbm     float64 `json:"median_dbm"`
	StdDevDb      float64 `json:"std_dev_db"`
	NoiseFloorDbm float64 `json:"noise_floor_dbm"` // FloorPercentile of the samples

	OccupancyFraction    float64 `json:"occupancy_fraction"`    // fraction at/above BusyThresholdDbm
	LongestDwellSamples  int     `json:"longest_dwell_samples"` // longest run at/above BusyThresholdDbm
	LongestDwellFraction float64 `json:"longest_dwell_fraction"`

	Thresholds   JammingOpts          `json:"thresholds"`
	Observations []JammingObservation `json:"observations"`
	Notes        []string             `json:"notes,omitempty"`
}

JammingAnalysis is the structured result of AnalyzeJamming. The statistics are objective; the observations apply the (reported) thresholds.

func AnalyzeJamming added in v0.514.0

func AnalyzeJamming(samples []float64, opts JammingOpts) (*JammingAnalysis, error)

AnalyzeJamming inspects a sequence of RSSI samples (in dBm, capture order) for the signatures of a continuous-carrier / sweep jammer — the offline, host-side complement to the on-device "Sub-GHz Jammer Detect" FAP (loader_subghz_jammer_detect), which only flags on the Flipper itself. The operator feeds an already-captured RSSI series; the analyser does no RF work.

It is built on the same receive-only "RSSI floor + dwell" heuristic, and like subghz_rollback_detect / tpms_anomaly_detect every flag is an OBSERVATION with its benign explanation stated — never a verdict. The objective statistics (min/max/mean/median/std-dev/noise-floor/occupancy/longest-dwell) are always reported; the three flags apply the documented, overridable thresholds (which are echoed back in the result):

  • elevated_noise_floor: the noise floor (a low percentile, robust to bursts) sits at/above ElevatedFloorDbm. A jammer raises the level the channel idles at. Benign: a strong nearby legitimate transmitter, or a congested band.
  • sustained_occupancy: the fraction of samples at/above the busy threshold is at/above OccupancyFlag. Normal traffic is bursty (low duty cycle); a jammer is continuous. Benign: a legitimate continuous carrier (e.g. an analogue video/audio link).
  • long_dwell: the longest unbroken run above the busy threshold spans at/above DwellFlagFraction of the capture. Benign: same as above.

No claim is made that a flagged capture IS a jammer — the statistics and flags are for an operator to correlate, never a confidently-wrong verdict.

type JammingObservation added in v0.514.0

type JammingObservation struct {
	Kind     string `json:"kind"`     // elevated_noise_floor | sustained_occupancy | long_dwell
	Severity string `json:"severity"` // info | warning
	Detail   string `json:"detail"`
}

JammingObservation is one signal flagged during RSSI-sequence analysis. Like RollbackObservation and internal/tpms.Anomaly it is an OBSERVATION with its benign explanation stated, never a definitive jammer verdict — a confidently-wrong alert is worse than none.

type JammingOpts added in v0.514.0

type JammingOpts struct {
	// BusyThresholdDbm: a sample at or above this RSSI counts the channel as
	// occupied (default -80 dBm).
	BusyThresholdDbm float64
	// ElevatedFloorDbm: a noise floor at or above this flags an elevated floor
	// (default -85 dBm) — a jammer raises the floor the channel idles at.
	ElevatedFloorDbm float64
	// FloorPercentile: the percentile used to estimate the noise floor, robust
	// to a few high bursts (default 10).
	FloorPercentile float64
	// OccupancyFlag: an occupancy fraction at or above this flags sustained
	// occupancy (default 0.85).
	OccupancyFlag float64
	// DwellFlagFraction: a longest-continuous-run-above-busy fraction at or
	// above this flags a long dwell (default 0.5 of the samples).
	DwellFlagFraction float64
}

JammingOpts carries the (overridable) decision thresholds for AnalyzeJamming. A zero value selects the documented defaults. They are surfaced in the result so the operator sees exactly what the flags were computed against — the flags are heuristic OBSERVATIONS, not a verdict, and the raw statistics are always reported regardless of the thresholds.

type Match

type Match struct {
	Result
}

Match pairs a decode result with any extra classifier metadata.

type Protocol

type Protocol interface {
	// Name returns the human-readable protocol name.
	Name() string

	// BitRate returns the nominal bit rate in baud.
	BitRate() float64

	// Decode attempts to decode the pulse sequence. Returns a protocols.Result
	// and nil on success, or a non-nil error when the pulses do not match the
	// expected sync/timing pattern.
	Decode(pulses []int) (protocols.Result, error)
}

Protocol is the interface every protocol decoder must implement. It wraps the protocols.Protocol interface so callers can also register custom decoders that satisfy the same contract.

type Result

type Result = protocols.Result

Result is the output of a successful protocol decode, visible to callers of the subghz package.

type RollbackAnalysis added in v0.386.0

type RollbackAnalysis struct {
	FramesAnalyzed int                   `json:"frames_analyzed"`
	FramesValid    int                   `json:"frames_valid"`
	Transmitters   int                   `json:"transmitters"`
	PerTx          map[string]*TxSummary `json:"per_tx"`
	Observations   []RollbackObservation `json:"observations"`
	Notes          []string              `json:"notes,omitempty"`
}

RollbackAnalysis is the structured result of AnalyzeRollback.

func AnalyzeRollback added in v0.386.0

func AnalyzeRollback(frames []RollbackFrame) (*RollbackAnalysis, error)

AnalyzeRollback inspects an ordered sequence of captured rolling-code frames for the signatures of a RollBack / replay attack (Kaiser et al., "RollBack: A New Time-Agnostic Replay Attack Against the Automotive Remote Keyless Entry Systems", DEF CON 2022). Frames are taken in observation order (index 0 = earliest) and grouped by transmitter ID.

Two deterministic signals are surfaced, both with their benign explanation stated:

  • replayed_code (key-free): a rolling code that REappears for a transmitter after that transmitter had already moved on to a different code. A rolling code is meant to be used exactly once, so a non-consecutive duplicate is the core replay signature. CONSECUTIVE identical codes are NOT flagged — a remote legitimately retransmits the same code several times per button press (a "burst"), and those are collapsed into one logical transmission. Benign explanation: a captured .sub being re-sent by the operator's own tooling, or a duplicated capture file.
  • counter_regression (only when decrypted counters are supplied): a rolling counter lower than the running maximum already seen for that transmitter. Counters increase monotonically by design, so a regression is a hard invariant violation. Benign explanation: frames fed out of capture order, or two remotes cloned to the same serial.

No RF, timing, or signal-strength heuristic is used — only the caller-supplied, deterministically-checkable fields — so the analyser never produces the confidently-wrong reading this codebase refuses to.

type RollbackFrame added in v0.386.0

type RollbackFrame struct {
	ID      string `json:"id"`
	Code    string `json:"code"`
	Counter *int64 `json:"counter,omitempty"`
}

RollbackFrame is one captured rolling-code transmission supplied by the caller (already demodulated/decoded; this analyser does no RF work).

  • ID is the fixed transmitter identity — the serial / fixed code that stays constant across presses (e.g. a KeeLoq 28-bit serial, a Security+ fixed portion). Frames are grouped by it.
  • Code is the full rolling/hopping code AS TRANSMITTED (hex or any stable string). This is observable without the manufacturer key.
  • Counter is the OPTIONAL decrypted rolling counter, supplied only when the caller holds the key. When present it enables the hard monotonicity check; when nil only the key-free duplicate check runs.

type RollbackObservation added in v0.386.0

type RollbackObservation struct {
	Kind     string `json:"kind"`     // "replayed_code" | "counter_regression"
	Severity string `json:"severity"` // "info" | "warning"
	TxID     string `json:"tx_id"`
	Detail   string `json:"detail"`
}

RollbackObservation is one signal flagged during rolling-code sequence analysis. Like internal/tpms.Anomaly it is an OBSERVATION with interpretation, never a definitive attack verdict — every flagged condition names its benign explanation so the operator correlates rather than concludes.

type SubFile

type SubFile struct {
	// Filetype is the header line value, e.g. "Flipper SubGhz Key File".
	Filetype string

	// Version is the integer version field.
	Version int

	// Frequency is the carrier frequency in Hz.
	Frequency uint64

	// Preset is the RF preset string, e.g. "FuriHalSubGhzPresetOok650Async".
	Preset string

	// Protocol is the declared protocol name (often "RAW" for raw captures).
	Protocol string

	// Pulses contains the raw timing data: positive = mark, negative = space,
	// values in microseconds.
	Pulses []int
}

SubFile represents a parsed Flipper Zero .sub capture file.

The .sub format is a simple key/value text file:

Filetype: Flipper SubGhz Key File
Version: 1
Frequency: 433920000
Preset: FuriHalSubGhzPresetOok650Async
Protocol: RAW
RAW_Data: 500 -1000 500 -500 ...

RAW_Data lines contain signed integer pulse durations in microseconds. Positive values are mark (carrier on), negative are space (carrier off). Multiple RAW_Data lines are concatenated into a single pulse slice.

func Parse

func Parse(r io.Reader) (*SubFile, error)

Parse reads a Flipper .sub file from r and returns the parsed SubFile. It is lenient: unknown keys are ignored so that future Flipper firmware versions adding new fields do not break the parser.

type TxSummary added in v0.386.0

type TxSummary struct {
	Frames               int `json:"frames"`
	LogicalTransmissions int `json:"logical_transmissions"`
	BurstRepeats         int `json:"burst_repeats"`
	ReplayedCodes        int `json:"replayed_codes"`
	CounterRegressions   int `json:"counter_regressions"`
}

TxSummary is the per-transmitter roll-up.

Directories

Path Synopsis
Package protocols implements pure-Go decoders for the top-20 Sub-GHz remote control protocols captured by the Flipper Zero.
Package protocols implements pure-Go decoders for the top-20 Sub-GHz remote control protocols captured by the Flipper Zero.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL