LoRa and BLE Emitter Algorithms¶
IoT waveform synthesis for LoRa CSS and BLE GFSK families.
Note
Layer 3 shipped, Pass-1. The Layer 3 implementation (emitters,
device-fingerprint, tx-impairments, propagation, rx-frontend) landed
on branch rfgen-impl-2026-06-25-105955 (PR #94). The class names,
Pydantic schemas, and Transformation enum members referenced below
match the shipped surface; Pass-1 stubs (GNU Radio OOT emitters,
cellular emitters, Sionna propagation backends) construct cleanly and
raise an EmitterError or ChannelError tagged with
stage="pass1_stub" until backend wiring lands. See
Reference / rfgen.emitters and
Reference / rfgen.channels for the shipped
class roster.
3. LoRa CSS (Semtech AN1200.22)¶
Family. iot.lora. Implementation: custom NumPy + cross-checked against gr-lora_sdr (GPL, reference only).
Modulation. Chirp-Spread-Spectrum. A symbol is one of 2^SF cyclic shifts of a base up-chirp.
def generate_lora(*, class_label, sample_rate, duration_s, params, rng):
"""
class_label: e.g. "iot.lora.css.sf7.bw125.cr-4-5"
params:
sf: int # 7..12
bw: int # 125000, 250000, 500000 (Hz)
cr: str # "4-5", "4-6", "4-7", "4-8"
sync_word: int = 0x12 # 1 byte
preamble_symbols: int = 8
payload_bytes: bytes | None
explicit_header: bool = True
"""
sf, bw, cr = params["sf"], params["bw"], params["cr"]
n_per_symbol = (2 ** sf) * (sample_rate / bw) # at emitter native rate R_emitter
# Base up-chirp at LoRa rate
t = torch.arange(int(round(n_per_symbol))) / sample_rate
f_inst = -bw/2 + bw * t * (sample_rate / n_per_symbol) / sample_rate
base_chirp = torch.exp(1j * 2 * torch.pi * torch.cumsum(f_inst, dim=0) / sample_rate)
# Whitening + interleaving + Hamming + Gray + cyclic shift per LoRa frame structure
# See AN1200.22 §4 for the full encoding chain; this is a sketch.
payload = params.get("payload_bytes") or _draw_random_payload(20, rng)
symbols = _lora_encode_symbols(payload, sf, cr, params["sync_word"],
params["explicit_header"])
# Concatenate: preamble (up-chirps) + sync (downchirps) + 2.25 down-chirps + symbols
chirps = []
for _ in range(params["preamble_symbols"]):
chirps.append(base_chirp) # 8 up-chirps
chirps.extend(_sync_word_chirps(params["sync_word"], base_chirp))
chirps.append(_downchirp(base_chirp))
chirps.append(_downchirp(base_chirp))
chirps.append(_downchirp(base_chirp)[:int(0.25 * n_per_symbol)])
for sym in symbols:
# Cyclic-shift the up-chirp by `sym` samples (out of 2^SF)
shift = int(round(sym * n_per_symbol / (2 ** sf)))
chirps.append(torch.roll(base_chirp, shifts=-shift))
iq = torch.cat(chirps)
iq = _resample_to(iq, target_len=int(round(sample_rate * duration_s)))
return Signal(iq=iq, metadata=SignalMetadata(bandwidth_hz=float(bw), ...))
Reference. Semtech AN1200.22 (“LoRa Modulation Basics”); tapparelj/gr-lora_sdr v0.5.8 for cross-checking. The framework’s Python re-implementation is independent (algorithm in the public technical literature).
Verification. Round-trip a known payload through our encoder + a gr-lora_sdr decoder; reverse with gr-lora_sdr encoder + our decoder. Both must recover the payload bit-exact.
4. BLE GFSK¶
Family. iot.ble. BLE 5.4 PHY: LE 1M, LE 2M, Coded S=2, Coded S=8.
def generate_ble(*, class_label, sample_rate, duration_s, params, rng):
"""
class_label: "iot.ble.gfsk.{1m,2m,coded_s2,coded_s8}.{adv,data}"
params:
channel_index: int # 0..39 (37/38/39 are advertising)
access_address: int # 32 bits, 0x8E89BED6 for advertising
pdu_bytes: bytes
bt: float = 0.5 # Gaussian filter BT product
"""
phy = class_label.split(".")[3] # "1m" / "2m" / "coded_s2" / "coded_s8"
if phy == "1m":
bit_rate = 1e6
elif phy == "2m":
bit_rate = 2e6
elif phy == "coded_s2":
bit_rate = 1e6 # coded with S=2 (FEC R=1/2)
elif phy == "coded_s8":
bit_rate = 1e6 # coded with S=8
h = 0.5 # modulation index for GFSK
bt = params["bt"]
samples_per_bit = sample_rate / bit_rate
# Build bitstream: preamble + access address + PDU + CRC
bits = _ble_build_bitstream(
access_address=params["access_address"],
pdu=params["pdu_bytes"],
coded=(phy.startswith("coded")),
S=2 if phy == "coded_s2" else 8 if phy == "coded_s8" else 1,
)
# NRZ-encode: 0 → -1, 1 → +1
nrz = torch.tensor([2 * b - 1 for b in bits], dtype=torch.float32)
# Upsample
upsampled = nrz.repeat_interleave(int(round(samples_per_bit)))
# Gaussian filter (BT = 0.5)
gauss = _gaussian_filter(bt=bt, samples_per_bit=samples_per_bit, span_bits=4)
filtered = torch.nn.functional.conv1d(
upsampled.view(1, 1, -1), gauss.view(1, 1, -1), padding=len(gauss) // 2
).view(-1)
# Integrate to get phase, modulate
phase = torch.cumsum(filtered, dim=0) * (2 * torch.pi * h * bit_rate / sample_rate)
iq = torch.exp(1j * phase)
iq = _resample_to(iq, target_len=int(round(sample_rate * duration_s)))
bw = bit_rate * (1 + bt) # approx 99% occupied bandwidth for GFSK
return Signal(iq=iq, metadata=SignalMetadata(bandwidth_hz=bw, ...))
def _gaussian_filter(bt: float, samples_per_bit: float, span_bits: int) -> torch.Tensor:
"""Gaussian impulse response, normalized to unit area."""
sigma = math.sqrt(math.log(2) / 2) / (2 * math.pi * bt)
n = int(round(span_bits * samples_per_bit))
t = (torch.arange(n) - n / 2) / samples_per_bit
h = torch.exp(-t * t / (2 * sigma * sigma))
return h / h.sum()
Channel index → carrier offset:
Channel |
Frequency (MHz) |
Use |
|---|---|---|
37 |
2402 |
advertising |
0…10 |
2404…2424 |
data |
38 |
2426 |
advertising |
11…36 |
2428…2478 |
data |
39 |
2480 |
advertising |
The framework places the carrier offset relative to scene center_freq_hz.
See Also¶
Emitter Algorithms: overview and cross-family invariants.
Signal Catalog: browsable family list with parameter ranges.
References¶
LoRa CSS encoding is documented in Semtech’s public application note (proprietary header / sync details cross-checked against gr-lora_sdr). BLE GFSK follows the Bluetooth Core specification. Both PHYs are re-implemented from the public technical literature.
Semtech Corporation. AN1200.22 LoRa Modulation Basics, 2015. (Chirp-Spread-Spectrum symbol mapping; SF-to-symbol rate; preamble + sync-word + downchirp frame layout)
Reynders, B. and Pollin, S. Chirp Spread Spectrum as a Modulation Technique for Long Range Communication, Symposium on Communications and Vehicular Technology, 2016. https://doi.org/10.1109/SCVT.2016.7797659. (CSS bit-rate identity, demodulator de-chirp + FFT analysis used by the cross-check decoder)
Tapparel, J. et al. gr-lora_sdr: An end-to-end LoRa transceiver in GNU Radio, EPFL TCL, v0.5.8. https://github.com/tapparelj/gr-lora_sdr. (Whitening, interleaving, Hamming, Gray-coded cyclic-shift; reference encoder used for round-trip cross-check)
Bluetooth SIG. Bluetooth Core Specification 5.4, Vol 6 Part B (Link Layer Specification), 2023. https://www.bluetooth.com/specifications/specs/core-specification-5-4/. (LE 1M / 2M / Coded S=2 / S=8 PHY definitions; channel index 0…39, advertising channels 37/38/39 at 2402 / 2426 / 2480 MHz; access address 0x8E89BED6 for advertising)
Bluetooth SIG. Bluetooth Core Specification 5.4, Vol 6 Part A (Physical Layer Specification), 2023. (GFSK with BT = 0.5 Gaussian filter, h = 0.5 modulation index; preamble + access address + PDU + CRC bitstream)