feat: port gr-sarsat to GNU Radio 3.10+
Ported from zleffke/gr-sarsat (2018, GNU Radio 3.7): - Updated Python 2 → Python 3 - Converted XML block definitions to YAML - Added pyproject.toml for modern packaging - Preserved original MIT license Blocks: - biphase_l_decode_bb: Manchester decoder (decim_block) - pds_frame_sync: Frame synchronizer with PDU output - sarp_msg_extract: SARP message splitter/validator
This commit is contained in:
commit
5867c54de3
64
README.md
Normal file
64
README.md
Normal file
@ -0,0 +1,64 @@
|
||||
# gr-sarsat-modern
|
||||
|
||||
GNU Radio 3.10+ compatible Cospas-Sarsat 406 MHz beacon decoder.
|
||||
|
||||
Ported from [zleffke/gr-sarsat](https://github.com/zleffke/gr-sarsat) (2018, GNU Radio 3.7).
|
||||
|
||||
## Blocks
|
||||
|
||||
| Block | Description |
|
||||
|-------|-------------|
|
||||
| **Biphase-L Decoder** | Decodes Manchester/Biphase-L encoded bit stream |
|
||||
| **PDS Frame Sync** | Synchronizes and extracts 72-byte PDS frames |
|
||||
| **SARP Message Extractor** | Splits PDS frames into 24-byte SARP messages |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Install Python package
|
||||
cd gr-sarsat-modern
|
||||
pip install -e .
|
||||
|
||||
# Copy GRC blocks to GNU Radio
|
||||
cp grc/*.yml ~/.local/share/gnuradio/grc/blocks/
|
||||
```
|
||||
|
||||
## Signal Chain
|
||||
|
||||
```
|
||||
RF Input (406 MHz)
|
||||
|
|
||||
v
|
||||
[FM Demod] -> [Clock Recovery] -> [Slicer]
|
||||
|
|
||||
v
|
||||
[Correlate Access Code - Tag] (Frame sync: 0x7FF, tag: "pds_sync")
|
||||
|
|
||||
v
|
||||
[Biphase-L Decoder]
|
||||
|
|
||||
v
|
||||
[PDS Frame Sync]
|
||||
|
|
||||
v
|
||||
[SARP Message Extractor]
|
||||
|
|
||||
+---> valid (sync word OK)
|
||||
+---> invalid (sync word mismatch)
|
||||
```
|
||||
|
||||
## Protocol Background
|
||||
|
||||
Cospas-Sarsat is the international satellite-based search and rescue system:
|
||||
|
||||
- **Frequency**: 406.025 MHz
|
||||
- **Modulation**: Phase-modulated carrier, Biphase-L encoded
|
||||
- **Bit rate**: 400 bps
|
||||
- **Frame structure**:
|
||||
- 15-bit frame sync (0x7FF)
|
||||
- 576-bit PDS frame (3 × 24-byte SARP messages)
|
||||
- Each SARP message starts with 0xD60 sync word
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Original work (c) 2018 zleffke
|
||||
31
grc/sarsat_biphase_l_decode_bb.block.yml
Normal file
31
grc/sarsat_biphase_l_decode_bb.block.yml
Normal file
@ -0,0 +1,31 @@
|
||||
id: sarsat_biphase_l_decode_bb
|
||||
label: Biphase-L Decoder
|
||||
category: '[Sarsat]'
|
||||
|
||||
templates:
|
||||
imports: from sarsat import biphase_l_decode_bb
|
||||
make: biphase_l_decode_bb()
|
||||
|
||||
inputs:
|
||||
- label: in
|
||||
domain: stream
|
||||
dtype: byte
|
||||
|
||||
outputs:
|
||||
- label: out
|
||||
domain: stream
|
||||
dtype: byte
|
||||
|
||||
documentation: |-
|
||||
Biphase-L (Manchester) Decoder for Cospas-Sarsat 406 MHz beacons.
|
||||
|
||||
Biphase-L encoding represents:
|
||||
- Bit 0: Low-to-High transition at bit center
|
||||
- Bit 1: High-to-Low transition at bit center
|
||||
|
||||
Input: Unpacked byte stream of Biphase-L encoded symbols (2 samples per bit)
|
||||
Output: Unpacked byte stream of decoded bits
|
||||
|
||||
Decimation factor: 2 (two input samples produce one output bit)
|
||||
|
||||
file_format: 1
|
||||
39
grc/sarsat_pds_frame_sync.block.yml
Normal file
39
grc/sarsat_pds_frame_sync.block.yml
Normal file
@ -0,0 +1,39 @@
|
||||
id: sarsat_pds_frame_sync
|
||||
label: PDS Frame Sync
|
||||
category: '[Sarsat]'
|
||||
|
||||
parameters:
|
||||
- id: tag_name
|
||||
label: Tag Name
|
||||
dtype: string
|
||||
default: pds_sync
|
||||
|
||||
templates:
|
||||
imports: from sarsat import pds_frame_sync
|
||||
make: pds_frame_sync(tag_name=${tag_name})
|
||||
|
||||
inputs:
|
||||
- label: in
|
||||
domain: stream
|
||||
dtype: byte
|
||||
|
||||
outputs:
|
||||
- label: out
|
||||
domain: message
|
||||
|
||||
documentation: |-
|
||||
PDS (Polar-orbiting satellite Data Service) Frame Synchronizer.
|
||||
|
||||
Expects "Correlate Access Code - Tag" block upstream with the Biphase-L
|
||||
decoded frame sync pattern (0x7FF = 15 ones).
|
||||
|
||||
When a frame sync tag is detected, collects the following 576 bits (72 bytes)
|
||||
and emits as a PDU message.
|
||||
|
||||
Parameters:
|
||||
tag_name: Name of the stream tag to look for (default: "pds_sync")
|
||||
|
||||
Input: Biphase-L decoded bit stream with frame sync tags
|
||||
Output: PDU messages containing 72-byte PDS frames
|
||||
|
||||
file_format: 1
|
||||
37
grc/sarsat_sarp_msg_extract.block.yml
Normal file
37
grc/sarsat_sarp_msg_extract.block.yml
Normal file
@ -0,0 +1,37 @@
|
||||
id: sarsat_sarp_msg_extract
|
||||
label: SARP Message Extractor
|
||||
category: '[Sarsat]'
|
||||
|
||||
templates:
|
||||
imports: from sarsat import sarp_msg_extract
|
||||
make: sarp_msg_extract()
|
||||
|
||||
inputs:
|
||||
- label: in
|
||||
domain: message
|
||||
|
||||
outputs:
|
||||
- label: valid
|
||||
domain: message
|
||||
- label: invalid
|
||||
domain: message
|
||||
|
||||
documentation: |-
|
||||
SARP (Search And Rescue Processor) Message Extractor.
|
||||
|
||||
Each 72-byte PDS frame contains three 24-byte SARP messages.
|
||||
Each SARP message should start with the sync word 0xD60 (12 bits).
|
||||
|
||||
This block splits incoming PDS frames into individual SARP messages
|
||||
and validates the sync word:
|
||||
|
||||
- 'valid' port: Messages with correct sync word (0xD60)
|
||||
- 'invalid' port: Messages with incorrect sync word
|
||||
|
||||
Note: Even messages on the 'invalid' port may contain recoverable
|
||||
beacon data - check BCH error correction codes.
|
||||
|
||||
Input: PDU from PDS Frame Sync block (72 bytes)
|
||||
Output: Individual 24-byte SARP messages
|
||||
|
||||
file_format: 1
|
||||
34
pyproject.toml
Normal file
34
pyproject.toml
Normal file
@ -0,0 +1,34 @@
|
||||
[project]
|
||||
name = "gr-sarsat"
|
||||
version = "1.0.0"
|
||||
description = "Cospas-Sarsat 406 MHz beacon decoder for GNU Radio 3.10+"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{name = "zleffke", email = ""},
|
||||
{name = "Ryan Malloy", email = "ryan@supported.systems"},
|
||||
]
|
||||
keywords = ["gnuradio", "sdr", "sarsat", "cospas", "beacon", "search-rescue"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications :: Ham Radio",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"numpy",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/zleffke/gr-sarsat"
|
||||
"Repository" = "https://git.supported.systems/MCP/gr-sarsat-modern"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["python"]
|
||||
24
python/sarsat/__init__.py
Normal file
24
python/sarsat/__init__.py
Normal file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# MIT License
|
||||
# Copyright (c) 2018 zleffke
|
||||
# Ported to GNU Radio 3.10+ by gr-mcp
|
||||
|
||||
"""
|
||||
gr-sarsat: Cospas-Sarsat 406 MHz beacon decoder blocks for GNU Radio.
|
||||
|
||||
Blocks:
|
||||
biphase_l_decode_bb - Biphase-L (Manchester) decoder
|
||||
pds_frame_sync - PDS frame synchronizer
|
||||
sarp_msg_extract - SARP message extractor
|
||||
"""
|
||||
|
||||
from .biphase_l_decode_bb import biphase_l_decode_bb
|
||||
from .pds_frame_sync import pds_frame_sync
|
||||
from .sarp_msg_extract import sarp_msg_extract
|
||||
|
||||
__all__ = [
|
||||
"biphase_l_decode_bb",
|
||||
"pds_frame_sync",
|
||||
"sarp_msg_extract",
|
||||
]
|
||||
45
python/sarsat/biphase_l_decode_bb.py
Normal file
45
python/sarsat/biphase_l_decode_bb.py
Normal file
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# MIT License
|
||||
# Copyright (c) 2018 zleffke
|
||||
# Ported to GNU Radio 3.10+ by gr-mcp
|
||||
|
||||
"""
|
||||
Biphase-L Decoder for Cospas-Sarsat 406 MHz beacons.
|
||||
|
||||
Biphase-L (Manchester) encoding represents:
|
||||
- Bit 0: Low-to-High transition at bit center
|
||||
- Bit 1: High-to-Low transition at bit center
|
||||
|
||||
This decoder extracts the data bit from each symbol pair.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from gnuradio import gr
|
||||
|
||||
|
||||
class biphase_l_decode_bb(gr.decim_block):
|
||||
"""
|
||||
Biphase-L Decoder.
|
||||
|
||||
Input: Unpacked byte stream of Biphase-L encoded symbols (2 samples per bit).
|
||||
Output: Unpacked byte stream of decoded bits.
|
||||
|
||||
Decimation factor: 2 (two input samples produce one output bit)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
gr.decim_block.__init__(
|
||||
self,
|
||||
name="biphase_l_decode_bb",
|
||||
in_sig=[np.int8],
|
||||
out_sig=[np.int8],
|
||||
decim=2,
|
||||
)
|
||||
|
||||
def work(self, input_items, output_items):
|
||||
in0 = input_items[0]
|
||||
out = output_items[0]
|
||||
# Extract first sample of each symbol pair (the data bit value)
|
||||
out[:] = in0[::2]
|
||||
return len(output_items[0])
|
||||
112
python/sarsat/pds_frame_sync.py
Normal file
112
python/sarsat/pds_frame_sync.py
Normal file
@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# MIT License
|
||||
# Copyright (c) 2018 zleffke
|
||||
# Ported to GNU Radio 3.10+ by gr-mcp
|
||||
|
||||
"""
|
||||
PDS (Polar-orbiting satellite Data Service) Frame Synchronizer.
|
||||
|
||||
Cospas-Sarsat 406 MHz beacons transmit:
|
||||
- 15-bit frame sync: 0x7FF (all ones, Biphase-L encoded)
|
||||
- 576-bit PDS frame containing 3 SARP messages
|
||||
|
||||
This block:
|
||||
1. Waits for frame sync tag from upstream correlator
|
||||
2. Collects the following 576 bits
|
||||
3. Packs bits into bytes
|
||||
4. Emits as PDU message
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pmt
|
||||
import binascii
|
||||
from gnuradio import gr
|
||||
|
||||
# State machine states
|
||||
SEARCH = 1
|
||||
COPY = 2
|
||||
|
||||
|
||||
class pds_frame_sync(gr.sync_block):
|
||||
"""
|
||||
PDS Frame Synchronizer.
|
||||
|
||||
Expects "Correlate Access Code - Tag" block upstream.
|
||||
Tag indicates the last bit of the Biphase-L DECODED frame sync.
|
||||
Operates on unpacked byte stream (one bit per byte).
|
||||
|
||||
Input: Biphase-L decoded bit stream with frame sync tags
|
||||
Output: PDU messages containing 72-byte PDS frames
|
||||
"""
|
||||
|
||||
def __init__(self, tag_name="pds_sync"):
|
||||
gr.sync_block.__init__(
|
||||
self,
|
||||
name="pds_frame_sync",
|
||||
in_sig=[np.int8],
|
||||
out_sig=None,
|
||||
)
|
||||
|
||||
self.tag_name = tag_name
|
||||
self.message_port_register_out(pmt.intern("out"))
|
||||
self.len_encoded_msg = 576 # Bits in PDS frame (72 bytes)
|
||||
self.pds_msg = []
|
||||
self.msg_packed = []
|
||||
self.msg_count = 0
|
||||
self.state = SEARCH
|
||||
|
||||
def pack_bytes(self):
|
||||
"""Pack unpacked bit list into bytearray."""
|
||||
self.msg_count += 1
|
||||
a = [
|
||||
int("".join(map(str, self.pds_msg[i : i + 8])), 2)
|
||||
for i in range(0, len(self.pds_msg), 8)
|
||||
]
|
||||
self.msg_packed = bytearray(a)
|
||||
|
||||
def work(self, input_items, output_items):
|
||||
in0 = input_items[0]
|
||||
num_input_items = len(in0)
|
||||
return_value = num_input_items
|
||||
nread = self.nitems_read(0)
|
||||
|
||||
if self.state == SEARCH:
|
||||
tags = self.get_tags_in_window(0, 0, num_input_items)
|
||||
if len(tags) > 0:
|
||||
for t in tags:
|
||||
t_str = pmt.symbol_to_string(t.key)
|
||||
if t_str == self.tag_name:
|
||||
# Frame sync detected - start collecting
|
||||
del self.pds_msg[:]
|
||||
del self.msg_packed[:]
|
||||
cur_idx = t.offset - nread
|
||||
self.pds_msg.extend(in0[cur_idx:])
|
||||
self.state = COPY
|
||||
|
||||
elif self.state == COPY:
|
||||
cur_msg_len = len(self.pds_msg)
|
||||
if (cur_msg_len + num_input_items) < self.len_encoded_msg:
|
||||
# Still collecting bits
|
||||
self.pds_msg.extend(in0)
|
||||
else:
|
||||
# Frame complete
|
||||
num_remain = self.len_encoded_msg - cur_msg_len
|
||||
self.pds_msg.extend(in0[0:num_remain])
|
||||
return_value = num_remain
|
||||
self.pack_bytes()
|
||||
|
||||
msg_str = f"[{self.msg_count:d}] {binascii.hexlify(self.msg_packed).decode()}"
|
||||
print(msg_str)
|
||||
|
||||
# Emit PDU with packed frame
|
||||
self.message_port_pub(
|
||||
pmt.intern("out"),
|
||||
pmt.cons(
|
||||
pmt.PMT_NIL,
|
||||
pmt.init_u8vector(len(self.msg_packed), self.msg_packed),
|
||||
),
|
||||
)
|
||||
self.state = SEARCH
|
||||
|
||||
return return_value
|
||||
83
python/sarsat/sarp_msg_extract.py
Normal file
83
python/sarsat/sarp_msg_extract.py
Normal file
@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# MIT License
|
||||
# Copyright (c) 2018 zleffke
|
||||
# Ported to GNU Radio 3.10+ by gr-mcp
|
||||
|
||||
"""
|
||||
SARP (Search And Rescue Processor) Message Extractor.
|
||||
|
||||
Each 72-byte PDS frame contains three 24-byte SARP messages.
|
||||
Each SARP message starts with a 12-bit sync word: 0xD60
|
||||
|
||||
This block:
|
||||
1. Receives PDS frame PDUs
|
||||
2. Splits into three 24-byte SARP messages
|
||||
3. Validates sync word (0xD60)
|
||||
4. Routes to 'valid' or 'invalid' output port
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pmt
|
||||
import binascii
|
||||
from gnuradio import gr
|
||||
|
||||
|
||||
class sarp_msg_extract(gr.basic_block):
|
||||
"""
|
||||
SARP Message Extractor.
|
||||
|
||||
Input: PDU from PDS Frame Sync Block (72 bytes)
|
||||
Output: Individual SARP messages (24 bytes each) on 'valid' or 'invalid' ports
|
||||
|
||||
A SARP message is considered valid if its sync word matches 0xD60.
|
||||
Even invalid messages may contain recoverable beacon data via BCH codes.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
gr.basic_block.__init__(
|
||||
self,
|
||||
name="sarp_msg_extract",
|
||||
in_sig=None,
|
||||
out_sig=None,
|
||||
)
|
||||
|
||||
self.message_port_register_in(pmt.intern("in"))
|
||||
self.set_msg_handler(pmt.intern("in"), self.handle_msg)
|
||||
self.message_port_register_out(pmt.intern("valid"))
|
||||
self.message_port_register_out(pmt.intern("invalid"))
|
||||
|
||||
def _check_sync_word(self, msg: bytearray) -> bool:
|
||||
"""Check if SARP message has valid sync word (0xD60)."""
|
||||
# Sync word is first 12 bits: 0xD6 followed by upper nibble 0x0
|
||||
return (msg[0] == 0xD6) and ((msg[1] & 0xF0) == 0x00)
|
||||
|
||||
def _publish_msg(self, msg: bytearray, valid: bool):
|
||||
"""Publish SARP message to appropriate output port."""
|
||||
port = "valid" if valid else "invalid"
|
||||
pdu = pmt.cons(pmt.PMT_NIL, pmt.init_u8vector(len(msg), msg))
|
||||
self.message_port_pub(pmt.intern(port), pdu)
|
||||
|
||||
def handle_msg(self, msg_pmt):
|
||||
"""Process incoming PDS frame PDU."""
|
||||
pds_msg = pmt.cdr(msg_pmt)
|
||||
|
||||
if not pmt.is_u8vector(pds_msg):
|
||||
print("[ERROR] Received invalid message type. Expected u8vector")
|
||||
return
|
||||
|
||||
buff = bytearray(pmt.u8vector_elements(pds_msg))
|
||||
|
||||
if len(buff) < 72:
|
||||
print(f"[ERROR] PDS frame too short: {len(buff)} bytes (expected 72)")
|
||||
return
|
||||
|
||||
# Extract three 24-byte SARP messages
|
||||
sarp_msg_1 = bytearray(buff[0:24])
|
||||
sarp_msg_2 = bytearray(buff[24:48])
|
||||
sarp_msg_3 = bytearray(buff[48:72])
|
||||
|
||||
# Validate and publish each message
|
||||
self._publish_msg(sarp_msg_1, self._check_sync_word(sarp_msg_1))
|
||||
self._publish_msg(sarp_msg_2, self._check_sync_word(sarp_msg_2))
|
||||
self._publish_msg(sarp_msg_3, self._check_sync_word(sarp_msg_3))
|
||||
Loading…
x
Reference in New Issue
Block a user