openipc-core 0.1.18

Shared OpenIPC FPV packet, RTP, and Realtek RX parsing logic
Documentation

openipc-core

Shared protocol code for openipc-rs.

This crate contains the parts of the OpenIPC receive path that do not need to know whether bytes came from native USB, WebUSB, a capture file, or a test fixture. It is the right dependency when you want to parse or reconstruct OpenIPC video without taking a dependency on a specific USB frontend.

What It Does

  • Parse Realtek rtl88xx USB RX aggregates and 24-byte RX descriptors.
  • Filter OpenIPC/WFB 802.11 frames by channel id and radio port.
  • Handle WFB session packets, data decryption, and FEC recovery.
  • Expose recovered non-video WFB payload bytes from MAVLink, data, or custom radio ports without parsing those application protocols.
  • Parse RTP and depacketize H.264/H.265 into Annex-B access units.
  • Build adaptive-link feedback payloads and WFB uplink packets.
  • Parse legacy/HT/VHT radiotap TX modes and build Realtek USB TX descriptors for monitor-injection frames.

Basic Receive Shape

use openipc_core::{
    parse_rx_aggregate, ChannelId, FrameLayout, PipelineEvent, ReceiverPipeline,
    WfbKeypair,
};
use openipc_core::realtek::RxPacketType;

fn push_transfer(
    pipeline: &mut ReceiverPipeline,
    transfer: &[u8],
) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error>> {
    let mut frames = Vec::new();

    for packet in parse_rx_aggregate(transfer)? {
        if packet.attrib.pkt_rpt_type != RxPacketType::NormalRx {
            continue;
        }
        if packet.attrib.crc_err || packet.attrib.icv_err {
            continue;
        }

        for event in pipeline.push_80211_frame(packet.data)? {
            if let PipelineEvent::VideoFrame(frame) = event {
                frames.push(frame.data);
            }
        }
    }

    Ok(frames)
}

fn pipeline_from_keypair(keypair_bytes: &[u8]) -> Result<ReceiverPipeline, Box<dyn std::error::Error>> {
    let keypair = WfbKeypair::from_bytes(keypair_bytes)?;
    Ok(ReceiverPipeline::with_keypair(
        ChannelId::default_video(),
        FrameLayout::WithFcs,
        keypair,
        0,
    )?)
}

The returned frame data is still encoded video. Feed it to WebCodecs, a native decoder, a file writer, or an RTP/Annex-B bridge depending on your application.

Event Model

ReceiverPipeline emits every useful stage it observes:

  • IgnoredFrame means the frame did not match the configured channel or could not be parsed for this pipeline.
  • SessionEstablished means a WFB session packet updated the decrypt/FEC state.
  • WfbPayload means a decrypted and FEC-recovered payload fragment was accepted on the video channel.
  • RtpPacket means the recovered payload parsed as RTP. This is useful if an app wants to mirror RTP to UDP or inspect packet timing.
  • VideoFrame means one complete encoded Annex-B access unit is ready for a decoder or file writer.

One input 802.11 frame can produce more than one event. For example, a recovered RTP packet can be emitted first, and if that packet completes an access unit the same call can also emit VideoFrame. That is intentional: apps can subscribe to the boundary they care about without reparsing the transfer.

Raw Payload Bytes

Use PayloadPipeline when you want recovered bytes from a non-video WFB channel without RTP or video assumptions. The crate does not care whether those bytes are MAVLink, MSP, CRSF, IP, vendor data, or another protocol:

use openipc_core::{
    ChannelId, FrameLayout, PayloadPipeline, PayloadPipelineEvent, RadioPort,
    WfbKeypair,
};

fn telemetry_pipeline(
    keypair_bytes: &[u8],
    port: RadioPort,
) -> Result<PayloadPipeline, Box<dyn std::error::Error>> {
    let keypair = WfbKeypair::from_bytes(keypair_bytes)?;
    Ok(PayloadPipeline::with_keypair(
        ChannelId::from_link_port(7669206, port),
        FrameLayout::WithFcs,
        keypair,
        0,
    )?)
}

fn handle_packet(pipeline: &mut PayloadPipeline, frame: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
    for event in pipeline.push_80211_frame(frame)? {
        if let PayloadPipelineEvent::Payload(payload) = event {
            println!("raw telemetry bytes: {}", payload.data.len());
        }
    }
    Ok(())
}

RadioPort::MavlinkRx is the observed OpenIPC MAVLink downlink port. Use RadioPort::DataRx or RadioPort::Custom(n) for other payload channels. openipc-core does not parse MAVLink or any other telemetry protocol. Applications can parse, display, record, or inspect those bytes later.

Crate Boundaries

openipc-core intentionally has no USB device ownership. Pair it with:

  • openipc-rtl88xx for native or WebUSB Realtek adapter IO.
  • openipc-native for a CLI-style native receive loop.
  • openipc-web or @openipc-rs/web for browser/WASM applications.

Status

The protocol pipeline has unit tests for parser, crypto, FEC, RTP, and uplink helpers. Live radio behavior still depends on the USB driver and adapter validation in higher-level crates.