Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GSE-HEM support #38

Merged
merged 4 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dvb-gse"
version = "0.6.2"
version = "0.7.0"
edition = "2021"
authors = ["Daniel Estevez <[email protected]>"]
description = "DVB-GSE (Digital Video Brodcast Generic Stream Encapsulation)"
Expand All @@ -24,12 +24,12 @@ bytes = "1.2"
clap = { version = "4", features = ["derive"], optional = true }
crc = "3"
env_logger = { version = "0.11", optional = true }
faster-hex = "0.9"
faster-hex = "0.10"
lazy_static = "1.4"
libc = { version = "0.2", optional = true }
log = "0.4"
num_enum = "0.7"
thiserror = "1"
thiserror = "2"
tun-tap = { version = "0.1", default-features = false, optional = true }

[dev-dependencies]
Expand Down
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
[crates-url]: https://crates.io/crates/dvb-gse

dvg-gse is a Rust implementation of the DVB GSE (Generic Stream Encapsulation)
protocol and related protocols.
protocol, GSE-HEM, and related protocols.

It is mainly intended to be used as a CLI application that receives BBFRAMEs by
It is mainly intended to be used as a CLI application that receives BBFRAMEs in
UDP or TCP packets from a DVB-S2 receiver (such as
[Longmynd](https://github.com/BritishAmateurTelevisionClub/longmynd)), obtains
IP packets from a continous-mode GSE stream, and sends the IP packets to a TUN
[Longmynd](https://github.com/BritishAmateurTelevisionClub/longmynd) or
commercial receivers supporting BBFRAME output), obtains IP packets from a
continous-mode GSE stream or a GSE-HEM stream, and sends the IP packets to a TUN
device.

The crate can also be used as a library to process GSE Packets and
Expand Down Expand Up @@ -58,7 +59,7 @@ This corresponds to BBFRAMEs fragmented into multiple UDP packets (since usually
DVB-S2 BBFRAMEs are larger than a 1500 byte MTU). The following rules need to be
followed.

* The payload of each UDP packet can optionally be begin by a header of up to 64
* The payload of each UDP packet can optionally begin by a header of up to 64
bytes, which is discarded by this application. The header length is set with
the `--header-length` argument. By default, no header is assumed.

Expand Down Expand Up @@ -110,7 +111,21 @@ as server. The following rules need to be followed.
stream.

If an error occurrs or the client closes the connection, the CLI application
will continue listen for new clients.
will continue to listen for new clients.

## GSE-HEM

GSE-HEM is auto-detected by using the TS/GS field in the BBHEADER (both
continuous GSE and GSE-HEM are supported).

A test script that generates UDP packets containing GSE-HEM BBFRAMEs is included
in
[`utils/generate_test_gse_hem_bbframes.py`](util/generate_test_gse_hem_bbframes.py). This
test script can be used by first running `dvb-gse` as indicated in the
quickstart above (possibly by using `RUST_LOG=trace`), and then starting the
`generate_test_gse_hem_bbframes.py` script. IPv6 UDP packets should be received
in `tun0` and `dvb-gse` should log information about the received packets if the
logging level is debug or trace.

## API documentation

Expand Down
24 changes: 14 additions & 10 deletions src/bbframe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,15 @@ impl BBFrameValidator {
///
/// - CRC of the BBHEADER.
///
/// - TS/GS type matching generic continuous.
/// - TS/GS type matching generic continuous or GSE-HEM.
///
/// - SIS/MIS and ISI matching what expected according to the last
/// [`BBFrameValidator::set_isi`] call.
/// [`BBFrameValidator::set_isi`] call.
///
/// - ISSYI disabled.
/// - If the BBFRAME is not GSE-HEM, then ISSYI disabled.
///
/// - The DFL is a multiple of 8 bits and not larger than the maximum
/// BBFRAME length.
/// BBFRAME length.
///
/// If the BBFRAME is not valid, this function logs the reason using
/// the [`log`] crate.
Expand All @@ -129,9 +129,9 @@ impl BBFrameValidator {
return false;
}
log::trace!("received {} with valid CRC", bbheader);
if !matches!(bbheader.tsgs(), TsGs::GenericContinuous) {
if !matches!(bbheader.tsgs(), TsGs::GenericContinuous | TsGs::GseHem) {
log::error!(
"unsupported TS/GS type '{}' (only 'Generic continous' is supported)",
"unsupported TS/GS type '{}' (only 'Generic continous' and 'GSE-HEM' are supported)",
bbheader.tsgs()
);
return false;
Expand All @@ -154,8 +154,8 @@ impl BBFrameValidator {
}
}
}
if bbheader.issyi() {
log::error!("ISSYI unsupported");
if !matches!(bbheader.tsgs(), TsGs::GseHem) && bbheader.issyi() {
log::error!("ISSYI only supported in GSE-HEM mode");
return false;
}
if bbheader.dfl() % 8 != 0 {
Expand Down Expand Up @@ -782,6 +782,12 @@ mod test {
let mut validator = BBFrameValidator::new();
assert!(validator.bbheader_is_valid(BBHeader::new(&valid_header)));

let valid_hem_header = hex!("b2 00 00 00 02 f0 00 00 00 87");
assert!(validator.bbheader_is_valid(BBHeader::new(&valid_hem_header)));

let valid_hem_issy_header = hex!("ba 00 12 34 02 f0 56 02 11 7c");
assert!(validator.bbheader_is_valid(BBHeader::new(&valid_hem_issy_header)));

let wrong_crc = hex!("72 00 00 00 02 f0 00 00 00 14");
assert!(!BBHeader::new(&wrong_crc).crc_is_valid());
assert!(!validator.bbheader_is_valid(BBHeader::new(&wrong_crc)));
Expand All @@ -796,8 +802,6 @@ mod test {
test_invalid(&validator, &ts_header);
let packetized_header = hex!("32 00 00 00 02 f0 00 00 00 d7");
test_invalid(&validator, &packetized_header);
let hem_header = hex!("b2 00 00 00 02 f0 00 00 00 86");
test_invalid(&validator, &hem_header);
let mis_header = hex!("52 2a 00 00 02 f0 00 00 00 dc");
test_invalid(&validator, &mis_header);
let issyi_header = hex!("7a 00 00 00 02 f0 00 00 00 78");
Expand Down
146 changes: 130 additions & 16 deletions src/bbheader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ impl BBHeader<'_> {
TsGs::try_from(self.matype1()[..2].load_be::<u8>()).unwrap()
}

/// Returns `true` if the BBFRAME is a GSE-HEM BBFRAME.
///
/// GSE-HEM BBFRAMEs have a different layout and omit some fields.
pub fn is_gse_hem(&self) -> bool {
matches!(self.tsgs(), TsGs::GseHem)
}

/// Give the value of the SIS/MIS (Single Input Stream or Multiple Input
/// Stream) field.
pub fn sismis(&self) -> SisMis {
Expand Down Expand Up @@ -84,8 +91,34 @@ impl BBHeader<'_> {
}

/// Gives the value of the UPL (User Packet Length) field.
pub fn upl(&self) -> u16 {
u16::from_be_bytes(self.0[2..4].try_into().unwrap())
///
/// The function returns `None` if the UPL field is not present, which is
/// the case in GSE-HEM mode.
pub fn upl(&self) -> Option<u16> {
if self.is_gse_hem() {
None
} else {
Some(u16::from_be_bytes(self.0[2..4].try_into().unwrap()))
}
}

/// Gives the value of the ISSY field.
///
/// The ISSY field is only present in GSE-HEM BBFRAMEs which have the ISSYI
/// bit asserted. If the ISSY field is not present, this function returns
/// `None`. The ISSY field is split into 2-byte field and a 1-byte field in
/// the BBHEADER. These are concatenated into a 3-byte array in the output
/// of this function.
pub fn issy(&self) -> Option<[u8; 3]> {
if self.is_gse_hem() && self.issyi() {
let mut field = [0; 3];
field[0] = self.0[2];
field[1] = self.0[3];
field[2] = self.0[6];
Some(field)
} else {
None
}
}

/// Gives the value of the DFL (Data Field Length) field.
Expand All @@ -94,8 +127,15 @@ impl BBHeader<'_> {
}

/// Gives the value of the SYNC (User Packet Sync-byte) field.
pub fn sync(&self) -> u8 {
self.0[6]
///
/// The function returns `None` if the SYNC field is not present, which is
/// the case in GSE-HEM mode.
pub fn sync(&self) -> Option<u8> {
if self.is_gse_hem() {
None
} else {
Some(self.0[6])
}
}

/// Gives the value of the SYNCD field.
Expand All @@ -105,12 +145,22 @@ impl BBHeader<'_> {

/// Gives the value of the CRC-8 field.
pub fn crc8(&self) -> u8 {
self.0[9]
self.0[BBHeader::LEN - 1]
}

/// Computes and returns the CRC-8 of the BBHEADER.
pub fn compute_crc8(&self) -> u8 {
CRC8.checksum(&self.0[..9])
let crc = CRC8.checksum(&self.0[..BBHeader::LEN - 1]);
if self.is_gse_hem() {
// ETSI EN 302 307-2 V1.3.1 (2021-07) says that the CRC8_MODE field
// in GSE-HEM is the EXOR of the MODE field with CRC-8, and that the
// MODE field has the value 1_D.
//
// To confirm if this indeeds refers to the value 1 in decimal.
crc ^ 1
} else {
crc
}
}

/// Checks if the CRC-8 of the BBHEADER is valid.
Expand All @@ -124,20 +174,27 @@ impl Display for BBHeader<'_> {
write!(
f,
"BBHEADER(TS/GS = {}, SIS/MIS = {}, CCM/ACM = {}, ISSYI = {}, \
NPD/GSE-Lite = {}, {}, ISI = {}, UPL = {} bits, DFL = {} bits, \
SYNC = {:#04x}, SYNCD = {:#06x})",
NPD/GSE-Lite = {}, {}, ISI = {}, ",
self.tsgs(),
self.sismis(),
self.ccmacm(),
self.issyi(),
self.npd(),
self.rolloff(),
self.isi(),
self.upl(),
self.dfl(),
self.sync(),
self.syncd()
)
)?;
if let Some(upl) = self.upl() {
write!(f, "UPL = {} bits, ", upl)?;
}
if let Some(issy) = self.issy() {
let issy = (u32::from(issy[0]) << 16) | (u32::from(issy[1]) << 8) | u32::from(issy[2]);
write!(f, "ISSY = {:#06x}, ", issy)?;
}
write!(f, "DFL = {} bits, ", self.dfl())?;
if let Some(sync) = self.sync() {
write!(f, "SYNC = {:#04x}, ", sync)?;
}
write!(f, "SYNCD = {:#06x})", self.syncd())
}
}

Expand Down Expand Up @@ -267,6 +324,8 @@ mod test {
use hex_literal::hex;

const CONTINUOUS_GSE_HEADER: [u8; 10] = hex!("72 00 00 00 02 f0 00 00 00 15");
const GSE_HEM_HEADER: [u8; 10] = hex!("b2 00 00 00 02 f0 00 00 00 87");
const GSE_HEM_HEADER_ISSY: [u8; 10] = hex!("ba 00 12 34 02 f0 56 02 11 7c");

#[test]
fn continuous_gse_header() {
Expand All @@ -285,14 +344,69 @@ mod test {
assert!(!header.gse_lite());
assert_eq!(header.rolloff(), RollOff::Ro0_20);
assert_eq!(header.isi(), 0);
assert_eq!(header.upl(), 0);
assert_eq!(header.issy(), None);
assert_eq!(header.upl(), Some(0));
assert_eq!(header.dfl(), 752);
assert_eq!(header.sync(), 0);
assert_eq!(header.sync(), Some(0));
assert_eq!(header.syncd(), 0);
assert_eq!(header.crc8(), CONTINUOUS_GSE_HEADER[9]);
assert_eq!(header.compute_crc8(), header.crc8());
assert!(header.crc_is_valid());
}

#[test]
fn gse_hem_header() {
let header = BBHeader::new(&GSE_HEM_HEADER);
assert_eq!(
format!("{}", header),
"BBHEADER(TS/GS = GSE-HEM, SIS/MIS = single, CCM/ACM = CCM, \
ISSYI = false, NPD/GSE-Lite = false, α = 0.20, ISI = 0, \
DFL = 752 bits, SYNCD = 0x0000)"
);
assert_eq!(header.tsgs(), TsGs::GseHem);
assert_eq!(header.sismis(), SisMis::Sis);
assert_eq!(header.ccmacm(), CcmAcm::Ccm);
assert!(!header.issyi());
assert!(!header.npd());
assert!(!header.gse_lite());
assert_eq!(header.rolloff(), RollOff::Ro0_20);
assert_eq!(header.isi(), 0);
assert_eq!(header.issy(), None);
assert_eq!(header.upl(), None);
assert_eq!(header.dfl(), 752);
assert_eq!(header.sync(), None);
assert_eq!(header.syncd(), 0);
assert_eq!(header.crc8(), GSE_HEM_HEADER[9]);
assert_eq!(header.compute_crc8(), header.crc8());
assert!(header.crc_is_valid());
}

#[test]
fn gse_hem_header_issy() {
let header = BBHeader::new(&GSE_HEM_HEADER_ISSY);
assert_eq!(
format!("{}", header),
"BBHEADER(TS/GS = GSE-HEM, SIS/MIS = single, CCM/ACM = CCM, \
ISSYI = true, NPD/GSE-Lite = false, α = 0.20, ISI = 0, \
ISSY = 0x123456, DFL = 752 bits, SYNCD = 0x0211)"
);
assert_eq!(header.tsgs(), TsGs::GseHem);
assert_eq!(header.sismis(), SisMis::Sis);
assert_eq!(header.ccmacm(), CcmAcm::Ccm);
assert!(header.issyi());
assert!(!header.npd());
assert!(!header.gse_lite());
assert_eq!(header.rolloff(), RollOff::Ro0_20);
assert_eq!(header.isi(), 0);
assert_eq!(header.issy(), Some([0x12, 0x34, 0x56]));
assert_eq!(header.upl(), None);
assert_eq!(header.dfl(), 752);
assert_eq!(header.sync(), None);
assert_eq!(header.syncd(), 0x211);
assert_eq!(header.crc8(), GSE_HEM_HEADER_ISSY[9]);
assert_eq!(header.compute_crc8(), header.crc8());
assert!(header.crc_is_valid());
}
}

#[cfg(test)]
Expand All @@ -319,7 +433,7 @@ mod proptests {
header.crc8();
header.compute_crc8();
header.crc_is_valid();
format!("{header}");
let _ = format!("{header}");
}
}
}
2 changes: 1 addition & 1 deletion src/gseheader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ mod proptests {
#[test]
fn random_header(header in proptest::collection::vec(any::<u8>(), 0..=32)) {
if let Some(header) = GSEHeader::from_slice(&header, None) {
format!("{}", header);
let _ = format!("{}", header);
header.start();
header.end();
header.is_single_fragment();
Expand Down
Loading