File wfm_frame.h¶
FileList > doppler > wfm > wfm_frame.h
Go to the source code of this file
A frame's BIT layout, described once and read from both ends. More...
#include <stddef.h>#include <stdint.h>#include "doppler/pn/pn_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | wfm_field_t One field of a frame — a run of bits that appears on the wire. |
| struct | wfm_frame_desc_layout_t Where every field and every stage landed. |
| struct | wfm_frame_desc_t A frame as a description: what is on the wire, and what covers it. |
| struct | wfm_frame_ops_t The kernels an assembly runs, and whatever state they carry. |
| struct | wfm_frame_rx_t What dp_wfm_frame_check found, stage by stage. |
| struct | wfm_frame_span_t A run of bits inside the assembled frame, [first, first + n) . |
| struct | wfm_frame_stage_rx_t What undoing one stage found. |
| struct | wfm_seq_t A run of bits, however it is produced. |
| struct | wfm_stage_op_t How one kind of stage actually transforms bits. |
| struct | wfm_stage_t One transform, and — the whole point — the fields it covers. |
Public Types¶
| Type | Name |
|---|---|
| enum | wfm_seq_kind_t Where a run of bits comes from. |
| enum | wfm_stage_kind_t Stage kinds doppler itself names. |
Public Functions¶
| Type | Name |
|---|---|
| size_t | dp_wfm_dsss_desc_chips (const wfm_frame_desc_t * d, const wfm_frame_ops_t * ops, const uint8_t * acq_code, size_t acq_len, size_t acq_reps, const uint8_t * data_code, size_t data_len, uint8_t * out, size_t max_out) Build a two-code DSSS burst from a description: assemble, spread. |
| size_t | dp_wfm_dsss_desc_chips_data (const wfm_frame_desc_t * d, const wfm_frame_ops_t * ops, const uint8_t * data, const uint8_t * acq_code, size_t acq_len, size_t acq_reps, const uint8_t * data_code, size_t data_len, uint8_t * out, size_t max_out) A DSSS burst over a data chunk: assemble, spread. |
| size_t | dp_wfm_dsss_desc_nchips (const wfm_frame_desc_t * d, size_t acq_len, size_t acq_reps, size_t data_len) Chip count of a DSSS burst built from a description. |
| size_t | dp_wfm_field_bits (const char * spec, uint8_t * out, size_t max_out, const char ** why) A Field's bits, straight from its text form. Returns the count. |
| size_t | dp_wfm_field_format (const wfm_field_t * field, char * buf, size_t cap) Write a field's canonical text form. Returns its length. |
| int | dp_wfm_field_parse (const char * spec, wfm_field_t * field, uint8_t ** owned, const char ** why) Read one Field from its text form. |
| size_t | dp_wfm_field_render (const wfm_field_t * f, uint8_t * out, size_t max_out) Write a field's bits: its sequence once, then repeated. Returns the count. |
| int | dp_wfm_frame_add_derived (wfm_frame_desc_t * d, const char * name, size_t bits) Append a named DERIVED field — one a stage will fill. Returns its index, or -1. |
| int | dp_wfm_frame_add_field (wfm_frame_desc_t * d, const char * name, const wfm_seq_t * seq, size_t reps) Append a named field. Returns its index, or -1. |
| int | dp_wfm_frame_add_stage (wfm_frame_desc_t * d, uint32_t kind, const char * first, const char * last) Append a stage covering [first .. last] BY NAME. Returns its index, or -1. |
| int | dp_wfm_frame_add_stage_at (wfm_frame_desc_t * d, uint32_t kind, unsigned first, unsigned n_fields) Append a stage covering n_fields fields from indexfirst . Returns its index, or -1. |
| size_t | dp_wfm_frame_assemble (const wfm_frame_desc_t * d, const wfm_frame_ops_t * ops, uint8_t * out, size_t max_out) Materialise a description: run every field, then every stage. |
| size_t | dp_wfm_frame_assemble_data (const wfm_frame_desc_t * d, const wfm_frame_ops_t * ops, const uint8_t * data, uint8_t * out, size_t max_out) dp_wfm_frame_assemble , with a data field's bits supplied. |
| int | dp_wfm_frame_check (const wfm_frame_desc_t * d, const wfm_frame_ops_t * ops, uint8_t * bits, wfm_frame_rx_t * rx) Undo a description's stages over a received frame, and report. |
| int | dp_wfm_frame_desc_crc_ok (const wfm_frame_desc_t * d, const uint8_t * rx_bits) Check a received frame's CRC against any description that has one. |
| int | dp_wfm_frame_desc_layout (const wfm_frame_desc_t * d, wfm_frame_desc_layout_t * out) Derive every field offset, every stage span and both lengths. |
| int | dp_wfm_frame_desc_rx (const wfm_frame_desc_t * d, uint8_t * sync, size_t sync_max, size_t * sync_len, size_t * frame_syms, const char ** why) What a receiver is told by a frame description: its sync word and its length. |
| int | dp_wfm_frame_field_index (const wfm_frame_desc_t * d, const char * name) Index of the field called name , or -1. |
| int | dp_wfm_frame_fixed (wfm_frame_desc_t * d, const wfm_seq_t * preamble, size_t reps, const wfm_seq_t * sync, const wfm_seq_t * payload, int crc) Describe the common frame: [preamble x reps | sync | payload | crc] . |
| int | dp_wfm_parse_u64 (const char * p, size_t n, uint64_t * v) Read an unsigned integer the way a Field's numbers are read. |
| size_t | dp_wfm_seq_bits (const wfm_seq_t * s, uint8_t * out, size_t max_out) Write s's bits, whatever produces them. Returns the count. |
| dp_pn_state_t * | dp_wfm_seq_pn_create (const wfm_seq_t * s) The PN generator a WFM_SEQ_PN sequence names, at its first bit. NULL if it names none. |
Macros¶
| Type | Name |
|---|---|
| define | WFM_FIELD_MAX_BITS 261120Bits one Field may hold, LEN * REPS ; the grammar refuses more. |
| define | WFM_FRAME_CRC_BITS 16uBits of CRC-16-CCITT, when a frame carries one. |
| define | WFM_FRAME_MAX_FIELDS 16Fields one description may carry. |
| define | WFM_FRAME_MAX_STAGES 8Stages one description may carry. |
| define | WFM_FRAME_NAME_MAX 16Bytes a field's name may use, NUL included. |
Detailed Description¶
One struct saying what a frame contains, used by the generator that builds it and by the measurer that scores it. The DSSS assembler already stated the reason it must be shared — it is "assembled in one place so TX and RX can
never drift" — and this generalises that from one waveform to all of them: dp_wfm_dsss_desc_chips() assembles a description and spreads it, rather than carrying a second copy of the layout.
It describes BITS¶
Not chips, not samples, not levels. Spreading, pulse shaping, oversampling, carrier and SNR layer above and stay wfm_synth's job. That boundary is what lets one descriptor serve an unspread BPSK stream and a two-code DSSS burst alike.
Every field is a sequence, and the generators already exist¶
The preamble, the sync word and the payload are all wfm_seq_t, so "a Gold
sync" is a configuration rather than a feature, and dp_pn_create() / dp_gold_create() stay the only implementations of those sequences.
The generated kinds are the ones that matter. A literal array is what a caller with real data has; a PN or Gold descriptor is a handful of numbers a receiver can REGENERATE, which is what makes a long-record BER practical — truth for a million-symbol run without a million-symbol array, and a capture reproducible from its metadata alone.
The CRC is the one we already have¶
dp_crc16_ccitt(), over the payload only, MSB-first — the WFM_STAGE_CRC16 stage, and the crc flag of dp_wfm_frame_fixed(). A second CRC would be a wire-format decision and nothing is asking for one.
See also: docs/design/rx-test.md section 7
Public Types Documentation¶
enum wfm_seq_kind_t¶
Where a run of bits comes from.
enum wfm_seq_kind_t {
WFM_SEQ_LITERAL = 0,
WFM_SEQ_PN = 1,
WFM_SEQ_GOLD = 2,
WFM_SEQ_DOTTED = 3,
WFM_SEQ_DATA = 4
};
enum wfm_stage_kind_t¶
Stage kinds doppler itself names.
enum wfm_stage_kind_t {
WFM_STAGE_CRC16 = 0,
WFM_STAGE_RS = 1,
WFM_STAGE_RANDOMISE = 2,
WFM_STAGE_CONV = 3,
WFM_STAGE_INTERLEAVE = 4,
WFM_STAGE_USER = 0x1000u
};
A stage's kind is an open uint32_t, not this enumeration. These are the values doppler has allocated; a caller allocates its own from WFM_STAGE_USER upward and supplies the kernel through wfm_frame_ops_t. That is the difference between a description a caller can extend and a fixed menu — and a closed enum here would make "a mission that is not CCSDS" a pull request against this header rather than a configuration, which is the opposite of the point.
The value is only ever a lookup key. Nothing in this component switches on it exhaustively, so an unrecognised kind is not undefined behaviour: it finds no kernel and the assembly is REFUSED, which is the honest answer and is the same one a declared-but-unsupplied stage already gets.
Public Functions Documentation¶
function dp_wfm_dsss_desc_chips¶
Build a two-code DSSS burst from a description: assemble, spread.
size_t dp_wfm_dsss_desc_chips (
const wfm_frame_desc_t * d,
const wfm_frame_ops_t * ops,
const uint8_t * acq_code,
size_t acq_len,
size_t acq_reps,
const uint8_t * data_code,
size_t data_len,
uint8_t * out,
size_t max_out
)
The only spreader: a common DSSS burst is this with a description from dp_wfm_frame_fixed, and a coded one is this with a coded one.
**The preamble is not a field of d, by design.** It is unmodulated, unspread and uncoded, because it is the coherent pull-in target a receiver correlates raw chips against; a stage covering "the whole
frame" therefore covers everything that is spread and not the preamble. That is the one place a DSSS burst's description differs from any other source's, and it is why this function takes the preamble separately.
A stage whose kernel ops does not supply makes the assembly fail and the burst is REFUSED — never transmitted with the stage quietly missing, which would produce a waveform that decodes against itself and syncs to nothing.
Parameters:
ddescription of the spread frame.opskernels beyond the built-in CRC; may be NULL.acq_codepreamble chips (0/1); NULL when there is no preamble.acq_lenpreamble length in chips.acq_repspreamble repetitions.data_codespreading code (0/1), lengthdata_len.data_lenchips per frame bit.outreceives the burst, one chip per byte.max_outcapacity ofout; must be at least dp_wfm_dsss_desc_nchips.
Returns:
chips written, or 0 if the geometry is refused, a stage has no kernel, or max_out is too small.
function dp_wfm_dsss_desc_chips_data¶
A DSSS burst over a data chunk: assemble, spread.
size_t dp_wfm_dsss_desc_chips_data (
const wfm_frame_desc_t * d,
const wfm_frame_ops_t * ops,
const uint8_t * data,
const uint8_t * acq_code,
size_t acq_len,
size_t acq_reps,
const uint8_t * data_code,
size_t data_len,
uint8_t * out,
size_t max_out
)
This is to dp_wfm_dsss_desc_chips what dp_wfm_frame_assemble_data is to dp_wfm_frame_assemble the same burst as dp_wfm_dsss_desc_chips, with the description's data:LEN field filled from data rather than refused. A data source on a dsss source builds one burst per chunk through this, at each burst boundary (payload-data-source.md). dp_wfm_dsss_desc_chips is this with data NULL, so there is one spreader.
Parameters:
ddescription of the spread frame.opskernels beyond the built-in CRC; may be NULL.datathedata:LENfield's bits (0/1), one per byte, as many as the layout gives the field; NULL when the description has no data field.acq_codepreamble chips (0/1); NULL when there is no preamble.acq_lenpreamble length in chips.acq_repspreamble repetitions.data_codespreading code (0/1), lengthdata_len.data_lenchips per frame bit.outreceives the burst, one chip per byte.max_outcapacity ofout; must be at least dp_wfm_dsss_desc_nchips.
Returns:
chips written, or 0 if the geometry is refused, a stage has no kernel, a data field has no data, or max_out is too small.
#include <doppler/wfm/wfm_frame.h>
int
main (void)
{
wfm_frame_desc_t d;
const wfm_seq_t data = { .kind = WFM_SEQ_DATA, .len = 2 };
dp_wfm_frame_fixed (&d, NULL, 0, NULL, &data, 0); // [data:2]
const uint8_t chunk[2] = { 0, 1 }, code[3] = { 1, 1, 0 };
uint8_t out[6];
// each data bit spreads the code: 0 sends it, 1 inverts it
if (dp_wfm_dsss_desc_chips_data (&d, NULL, chunk, NULL, 0, 0, code, 3,
out, sizeof out)
!= 6
|| out[0] != 1 || out[2] != 0 || out[3] != 0 || out[5] != 1)
return 1;
// with no chunk, the data field is refused
return dp_wfm_dsss_desc_chips_data (&d, NULL, NULL, NULL, 0, 0, code, 3,
out, sizeof out)
!= 0;
}
function dp_wfm_dsss_desc_nchips¶
Chip count of a DSSS burst built from a description.
size_t dp_wfm_dsss_desc_nchips (
const wfm_frame_desc_t * d,
size_t acq_len,
size_t acq_reps,
size_t data_len
)
acq_len * acq_reps + out_bits * data_len, where out_bits is what leaves the description's last emitting stage — so an inner code that doubles the frame doubles the burst, and nothing here restates the arithmetic the layout already did.
Parameters:
dthe description of everything that gets SPREAD.acq_lenpreamble code length in chips (0 = no preamble).acq_repspreamble repetitions.data_lenspreading-code length, i.e. chips per frame bit.
Returns:
burst chips, or 0 if the description is refused, or it has bits and data_len is 0, or there is nothing to transmit.
function dp_wfm_field_bits¶
A Field's bits, straight from its text form. Returns the count.
dp_wfm_field_parse, then dp_wfm_field_render, with the literal's storage released before returning — the one door from the text a person writes to the bits every object takes. With out NULL it only SIZES: it returns how many bits the field is, which is what a caller allocates before the second call.
Parameters:
specNUL-terminated text, as dp_wfm_field_parse reads it.outreceives the bits, one per byte; NULL to size.max_outcapacity ofoutin bits; ignored whenoutis NULL.whyoptional; as dp_wfm_field_parse, plus "the output is smaller than the field" and, fordata:LEN, that its bits come from the frame's data source.
Returns:
the field's length in bits (repetitions included), or 0 on a refusal. A Field is never empty, so 0 is unambiguous. Every text the grammar accepts renders, except data:LEN, which has no bits of its own and is refused, sizing or rendering: the parser refuses what a generator could not build (a 1-bit register with no POLY, a number wider than REG), so the sizing call's answer is what the rendering call writes, given room.
uint8_t b[124];
size_t n = dp_wfm_field_bits ("pn:31:5*4", NULL, 0, NULL); // 124
dp_wfm_field_bits ("pn:31:5*4", b, n, NULL);
function dp_wfm_field_format¶
Write a field's canonical text form. Returns its length.
The ONLY writer of the grammar, and the inverse of dp_wfm_field_parse. Parsing what this writes gives back the same field, for every field it accepts. The form is canonical, so two equal fields print identically:
- a literal prints as
0x…hex when its length is a multiple of 4, and as0/1digits otherwise; - a generator prints only what differs from its defaults — a zero seed or poly is omitted unless a later number needs its position, and
galoisis never written; *REPSappears only whenreps > 1.
Parameters:
fieldthe field. A derived field has no text form and is refused.bufreceives the text and a NUL; may be NULL to size it.capcapacity ofbufin bytes, NUL included.
Returns:
the length WITHOUT the NUL, whether or not it fit — so a buf == NULL call sizes the buffer — or 0 for a field with no text form. Nothing is written unless all of it fits.
function dp_wfm_field_parse¶
Read one Field from its text form.
int dp_wfm_field_parse (
const char * spec,
wfm_field_t * field,
uint8_t ** owned,
const char ** why
)
The ONLY reader of the grammar every text face shares — the CLI, a JSON scene and field_bits() in Python all call this, and none restates it (docs/design/frame-description.md §F.1):
field := seq [ "*" REPS ] REPS >= 1
seq := bin | hex | pn | gold | dotted
bin := [01]+
hex := "0x" [0-9A-Fa-f]+ 4 bits a digit, MSB first
pn := "pn:" LEN ":" REG [":" SEED [":" POLY]] [":" LFSR]
gold := "gold:" LEN ":" REG ":" TA ":" SA ":" TB ":" SB
dotted := "dotted:" LEN
LFSR := "galois" | "fibonacci" default galois
A number is decimal, or hex after 0x, and must be consumed WHOLE: 12abc, -1, 5 and an empty field (pn::10) are refused, not read as far as they go. A leading 0 is decimal, never octal. LEN is the output length and must be > 0; REG is the register width, 1..64. A pn with no POLY means the maximal-length polynomial for its register, so a register that has none (width 1) is refused unless a POLY is given. LEN * REPS is at most WFM_FIELD_MAX_BITS, and the refusal names that number: a longer run is a stream, not a Field. A 0/1 string with any other character in it is refused rather than filtered, because a typo that quietly shortens a sync word syncs to nothing and fails nowhere.
data:LEN parses to a WFM_SEQ_DATA field: LEN bits per frame, drawn from the frame's data source, so the description knows its length and never its bits. LEN must be > 0 and nothing may follow it (docs/design/payload-data-source.md).
Parameters:
specNUL-terminated text.fieldreceives the field:nameempty,derived_by0,repsas written (1 when absent). Untouched on refusal.ownedreceives the allocated bit array of a LITERAL field, whichfield->seq.bitspoints into and the caller mustfree(); NULL for a generated kind. Untouched on refusal.whyoptional; receives a STATIC sentence naming the cause of a refusal, NULL on success. Never freed.
Returns:
DP_OK, or DP_ERR_INVALID for text outside the grammar. A literal's storage is at most four times the spec's length, so it is allocated with the abort-on-OOM helper.
wfm_field_t f;
uint8_t *owned;
const char *why;
if (dp_wfm_field_parse ("pn:31:5*4", &f, &owned, &why) != DP_OK)
fprintf (stderr, "error: %s\n", why);
// f.seq.kind == WFM_SEQ_PN, f.seq.len == 31, f.seq.reg_bits == 5,
// f.reps == 4, owned == NULL
function dp_wfm_field_render¶
Write a field's bits: its sequence once, then repeated. Returns the count.
The one place a field's REPETITION is expanded, used by dp_wfm_frame_assemble for every caller-supplied field and by dp_wfm_field_bits for a field given as text. A repetition is the same bits again, never fresh ones: a generated field that drew new bits per repetition would not be a periodic acquisition target, and coherent integration across the repetitions would be void.
Parameters:
fthe field;reps == 0means one. A DERIVED field has no source of its own and is refused.outreceivesf->seq.len * repsbits, one per byte.max_outcapacity ofoutin bits.
Returns:
bits written, or 0 if the field is derived, empty, larger than max_out, a data field (its bits are its data source's), or its sequence cannot be built. On 0, out may have been partly written.
wfm_field_t f;
uint8_t *owned;
uint8_t b[124];
dp_wfm_field_parse ("pn:31:5*4", &f, &owned, NULL);
dp_wfm_field_render (&f, b, sizeof b); // 124: one period, then 3 copies
free (owned); // NULL for a generated field; a literal's bits otherwise
function dp_wfm_frame_add_derived¶
Append a named DERIVED field — one a stage will fill. Returns its index, or -1.
A field with a declared length and no source: a CRC trailer, a block of R-S check symbols. Its producer is wired by dp_wfm_frame_add_stage, not named here, because a stage does not exist yet when the field it derives is appended — fields are ordered by POSITION and stages by APPLICATION, and this is where those two orders meet.
Parameters:
dthe description.namethe field's name, or NULL/"" for anonymous.bitsits length, which its stage decides and the caller states.
Returns:
the new field's index, or -1 on NULL, a full description, a zero bits, or a name already taken.
function dp_wfm_frame_add_field¶
Append a named field. Returns its index, or -1.
int dp_wfm_frame_add_field (
wfm_frame_desc_t * d,
const char * name,
const wfm_seq_t * seq,
size_t reps
)
The building half of the description, and the reason a name is worth carrying: a caller says what a field IS rather than counting positions, and the stage that covers it says so by name too.
Parameters:
dthe description; appended in wire order.namethe field's name, or NULL/"" to leave it anonymous.seqwhere the bits come from; copied by value, so the LITERAL kind still borrows the caller's array and the caller still owns it for as long asdis used.repsrepetitions ofseq, verbatim; 0 means one.
Returns:
the new field's index, or -1 if d or seq is NULL, the description is full, or name is already taken.
function dp_wfm_frame_add_stage¶
Append a stage covering [first .. last] BY NAME. Returns its index, or -1.
int dp_wfm_frame_add_stage (
wfm_frame_desc_t * d,
uint32_t kind,
const char * first,
const char * last
)
The cover is the whole point of the representation and this is the form that reads: add_stage(d, WFM_STAGE_CRC16, "payload", "crc") says what three integers used to.
It wires a derived field's producer for you, and that is applying an invariant rather than adding one: dp_wfm_frame_desc_layout already refuses a description whose derived field is not the LAST of its producing stage's cover, so a field with a declared length and no source sitting at the end of this cover has exactly one possible producer. It is wired here so a caller cannot state it a second, different way.
Parameters:
dthe description.kinda wfm_stage_kind_t value, or a caller's own from WFM_STAGE_USER up.firstname of the first field covered.lastname of the last field covered; may equalfirst.
Returns:
the new stage's index, or -1 on NULL, a full description, a name neither field carries, or last before first.
function dp_wfm_frame_add_stage_at¶
Append a stage covering n_fields fields from indexfirst . Returns its index, or -1.
int dp_wfm_frame_add_stage_at (
wfm_frame_desc_t * d,
uint32_t kind,
unsigned first,
unsigned n_fields
)
The index form of dp_wfm_frame_add_stage, which resolves its two names and calls this. Both wire a derived field's producer the same way, by the one rule stated there, so a caller counting fields and a caller naming them build the same description.
Parameters:
dthe description.kinda wfm_stage_kind_t value, or a caller's own.firstindex of the first field covered.n_fieldsfields covered; 0 means the stage does not run.
Returns:
the new stage's index, or -1 on NULL or a full description. A cover past the fields is not refused here: the layout judges it.
function dp_wfm_frame_assemble¶
Materialise a description: run every field, then every stage.
size_t dp_wfm_frame_assemble (
const wfm_frame_desc_t * d,
const wfm_frame_ops_t * ops,
uint8_t * out,
size_t max_out
)
Fields are written in wire order, then each stage is applied over the span dp_wfm_frame_desc_layout gave it — over that span and no other, which is the whole content of the coverage table a standard's framing turns out to be.
Parameters:
dthe description.opskernels for the stage kinds beyond the built-in CRC; may beNULLwhen there are none.outreceives the unpacked output, one bit per byte.max_outcapacity ofoutin bits; must be at least the layout'sout_bits.
Returns:
The bits written, or 0 if the description is refused, a stage has no kernel, a field cannot be built, or max_out is too small — in which case out is untouched.
function dp_wfm_frame_assemble_data¶
dp_wfm_frame_assemble , with a data field's bits supplied.
size_t dp_wfm_frame_assemble_data (
const wfm_frame_desc_t * d,
const wfm_frame_ops_t * ops,
const uint8_t * data,
uint8_t * out,
size_t max_out
)
A WFM_SEQ_DATA field has no bits of its own: they are one chunk of the frame's data source, drawn per frame (wfm/wfm_data.h). This writes data at the data field's offset and then runs every stage, so a stage covering the payload a CRC, an outer code covers THIS frame's chunk. dp_wfm_frame_assemble is this with data NULL.
Parameters:
dthe description; at most one data field (the layout refuses two).opsas dp_wfm_frame_assemble.datathe data field's bits,LEN * REPSof them, one per byte, asdp_wfm_data_next()writes them; ignored whendhas no data field, and required when it has one.outas dp_wfm_frame_assemble.max_outas dp_wfm_frame_assemble.
Returns:
the bits written, or 0 as dp_wfm_frame_assemble, and also when d has a data field and data is NULL.
const wfm_seq_t data = { .kind = WFM_SEQ_DATA, .len = 24 };
wfm_frame_desc_t d;
dp_wfm_frame_fixed (&d, NULL, 0, NULL, &data, 1); // [data:24 | crc16]
uint8_t chunk[24] = { 1, 0, 1, 1 }, frame[40];
// the chunk at the data field's offset, then a CRC-16 over it
if (dp_wfm_frame_assemble_data (&d, NULL, chunk, frame, sizeof frame)
!= 40
|| frame[0] != 1 || frame[1] != 0)
return 1;
// with no chunk, the data field is refused, as by dp_wfm_frame_assemble
if (dp_wfm_frame_assemble_data (&d, NULL, NULL, frame, sizeof frame) != 0)
return 1;
function dp_wfm_frame_check¶
Undo a description's stages over a received frame, and report.
int dp_wfm_frame_check (
const wfm_frame_desc_t * d,
const wfm_frame_ops_t * ops,
uint8_t * bits,
wfm_frame_rx_t * rx
)
The receive mirror of dp_wfm_frame_assemble, reading the same description — so the two cannot disagree about which stage covered what, which is the failure the whole representation exists to prevent. Stages are reversed in the OPPOSITE order to the one they were applied in, each over the span the layout gives it.
This is what makes a truth-free frame error rate possible on a coded link, and it is a strictly better detector than a CRC. A CRC says one bit: right or wrong. An outer code says how much repair it took — ok == units with a rising symbols is margin being spent, visible before it is lost. A caller wanting only good frames compares ok with units; one doing accounting reads the rest.
It begins AFTER the inner code and after frame synchronisation, for the reason ccsds_tm_frame.h gives at length: a Viterbi is streaming and emits its decisions depth bits late, so the bits of one frame are not a function of that frame's symbols alone, and the marker that says where a frame starts is only readable once the inner code is undone. A stage with no undo kernel is reported as not checked, never as passed.
Parameters:
dthe description the bits are laid out by.opskernels for the stage kinds beyond the built-in CRC; may beNULL.bitsthe layout'sframe_bitsreceived bits, one per byte, CORRECTED IN PLACE by any stage that repairs.rxreceives the per-stage outcome; may beNULL.
Returns:
1 when every stage that was checked came out good, 0 when one did not, or -1 if the description is refused. A description with no checking stage at all returns -1, not 1: "carries no check" and "the check passed" are different answers, and an FER that conflated them would score every unprotected frame as perfect.
function dp_wfm_frame_desc_crc_ok¶
Check a received frame's CRC against any description that has one.
This is what makes a truth-free frame error rate possible. It needs the description and the received bits and no payload truth at all — so it works on a real capture, and unlike a self-referenced EVM or a blind M2M4 it still catches a false lock, because a rotated constellation fails the check rather than looking clean. What the CRC protects is everything its stage covers except the trailer that stage derived — read back from the same rule the assembler writes by, so the two cannot disagree about where the trailer is.
Parameters:
dthe description the bits are laid out by.rx_bitsreceived bits, the layout'sframe_bitsof them.
Returns:
1 pass, 0 fail, -1 if the description carries no CRC stage (or on NULL). The three are distinct on purpose: an FER that read "carries no check" as "the check failed" would count every unprotected frame as an error.
function dp_wfm_frame_desc_layout¶
Derive every field offset, every stage span and both lengths.
The one operation both shipped framers already have, widened: this is the common frame's arithmetic and dp_ccsds_tm_frame_layout()'s, with the field and stage lists supplied rather than fixed.
A derived field whose producing stage covers no caller-supplied bits is dropped to zero length — which is the general form of the rule the common frame has always applied, that a CRC over an empty payload protects nothing and is not emitted.
An EMITTING stage (emit_num set) is refused unless it covers the whole frame, and a second one is refused outright. Refusing here is the point: such a description used to lay out perfectly and then be unassemblable for ever, because out_bits was computed from the cover while dp_wfm_frame_assemble hands the kernel the whole frame. The caller got a 0 from assemble and no way to learn that the geometry, not the data, was wrong. Geometry is decided here, so it is refused here.
A field that declares bits but supplies no sequence is DERIVED, and one that names no producing stage (derived_by zero) is refused for the same reason. It used to lay out at zero length: the frame came out short, the stage that should have filled the field ran over a cover whose tail no longer existed, and the caller got a record rather than an error. Every reader funnels through here, so refusing at this one point covers the scene JSON and the CLI as well as the builder — which cannot reach the state at all, since dp_wfm_frame_add_stage wires the producer from the cover it is given.
A frame draws from ONE data source, so it carries at most one WFM_SEQ_DATA field; a second is refused. The data field's length counts like any other, so a frame's geometry is known before its data is.
Parameters:
dthe description.outreceives the layout.
Returns:
0, or -1 if d or out is NULL, a count or a cover runs past its array, a derived field names no producing stage, an emitting stage covers less than the whole frame or is not the only one, or the frame has two data fields.
function dp_wfm_frame_desc_rx¶
What a receiver is told by a frame description: its sync word and its length.
int dp_wfm_frame_desc_rx (
const wfm_frame_desc_t * d,
uint8_t * sync,
size_t sync_max,
size_t * sync_len,
size_t * frame_syms,
const char ** why
)
A receiver that demodulates a frame must know the bits to correlate for (the sync word) and how many symbols the frame occupies. Both are in the description the transmitter spread, so both are read from it here, by the layout the transmitter's assembler uses, rather than restated by each caller from the field lengths it happens to remember.
The sync word is field 0. That holds for dp_wfm_frame_fixed() without a preamble and for a CCSDS CADU, whose fields have no names. Field 0 is refused when it is empty, derived or a data field (the receiver needs bits it knows), when any stage covers it (a stage that rewrites the sync word on the wire leaves the receiver correlating against bits nobody sent), or when it is named preamble (a DSSS preamble is sent unspread and is not a field of the spread frame).
A description with an emitting stage is refused (out_bits differs from frame_bits, as a convolutional code makes it): the code covers the sync word, so frame synchronisation would have to run after the Viterbi.
Parameters:
dthe description.syncreceives field 0's bits, one per byte.sync_maxcapacity ofsync.sync_lenreceives field 0's length in bits.frame_symsreceives the frame's length in symbols, one per bit, counted from the sync word on and sync included.whyon refusal, receives a static sentence naming the fix; may beNULL.
Returns:
0, or -1 with why set. Nothing is written on refusal.
const uint8_t bits[3] = { 1, 0, 1 };
const wfm_seq_t sync = { .kind = WFM_SEQ_LITERAL, .bits = bits,
.len = 3 };
const wfm_seq_t data = { .kind = WFM_SEQ_DATA, .len = 8 };
wfm_frame_desc_t d;
dp_wfm_frame_fixed (&d, NULL, 0, &sync, &data, 1); // sync|data:8|crc16
uint8_t w[3];
size_t n = 0, frame = 0;
const char *why = NULL;
if (dp_wfm_frame_desc_rx (&d, w, sizeof w, &n, &frame, &why) != 0
|| n != 3 || frame != 3 + 8 + 16 || w[0] != 1 || w[1] != 0)
return 1;
function dp_wfm_frame_field_index¶
Index of the field called name , or -1.
The lookup the whole naming idea rests on, and it is deliberately the ONLY one: names resolve to indices here and nowhere else, so every existing index-taking entry point keeps working unchanged and there is one place a rename can be wrong.
An empty or NULL name finds nothing rather than matching the first unnamed field — an unnamed field is anonymous, not named "", and matching it would make an unnamed description answer questions about fields it does not have.
Parameters:
dthe description.namethe field name, NUL-terminated.
Returns:
the field's index, or -1 if d or name is NULL, name is empty, or no field carries it.
function dp_wfm_frame_fixed¶
Describe the common frame: [preamble x reps | sync | payload | crc] .
int dp_wfm_frame_fixed (
wfm_frame_desc_t * d,
const wfm_seq_t * preamble,
size_t reps,
const wfm_seq_t * sync,
const wfm_seq_t * payload,
int crc
)
The one fixed layout every face reaches without writing a description of its own — wfmgen's --acq-code, --sync and --crc, a scene's keys of the same names, the Frame object and the receiver harnesses. It is built through the general by-name builder, so what comes back is an ordinary description with fields called "preamble", "sync", "payload" and "crc", and there is no second layout behind it.
A field is present when its sequence has a LENGTH, never merely a pointer, and the preamble additionally needs reps: a length with no bits reaches dp_wfm_frame_assemble and is refused there, rather than being dropped here and assembling a frame quietly missing it. The payload is always a field, even an empty one, so that a CRC always has something to cover — and a CRC over an empty payload lays out as a stage that did not run, because a trailer over nothing protects nothing.
The sequences are BORROWED, as everywhere in a description: they must outlive d.
Parameters:
dreceives the description; overwritten.preamblepreamble sequence; NULL or zero-length for none.repspreamble repetitions; 0 means no preamble.syncsync-word sequence; NULL or zero-length for none.payloadpayload sequence; NULL for an empty payload.crcnon-zero: a CRC-16-CCITT trailer over the payload.
Returns:
0, or -1 if d is NULL.
// Barker-13 sync over a 16-bit payload, with a CRC-16 trailer.
static const uint8_t b13[13] = {1,1,1,1,1,0,0,1,1,0,1,0,1};
static const uint8_t pay[16] = {0,1,1,0,1,0,0,1,1,1,0,0,0,1,0,1};
wfm_seq_t sync = { .kind = WFM_SEQ_LITERAL, .bits = b13, .len = 13 };
wfm_seq_t data = { .kind = WFM_SEQ_LITERAL, .bits = pay, .len = 16 };
wfm_frame_desc_t d;
wfm_frame_desc_layout_t l;
dp_wfm_frame_fixed (&d, NULL, 0, &sync, &data, 1);
dp_wfm_frame_desc_layout (&d, &l); // l.frame_bits == 13 + 16 + 16
function dp_wfm_parse_u64¶
Read an unsigned integer the way a Field's numbers are read.
THE number reader of the Field grammar, public so that a caller reading integers from text wfmgen's numeric flags applies the same rule rather than a second one (doppler#1611): decimal, or hex after 0x (0X), consumed WHOLE. A sign, a space, a trailing character, an empty token, a bare 0x and a value past UINT64_MAX are refused, and a leading 0 is decimal, never octal.
Parameters:
pthe text; need not be NUL-terminated.nits length in bytes.vreceives the value; untouched on refusal.
Returns:
0, or -1 for text outside the rule.
uint64_t v;
int ok = dp_wfm_parse_u64 ("0x10", 4, &v); // 0, v == 16
int dec = dp_wfm_parse_u64 ("010", 3, &v); // 0, v == 10, not octal
int bad = dp_wfm_parse_u64 ("4x", 2, &v); // -1, v unchanged
function dp_wfm_seq_bits¶
Write s's bits, whatever produces them. Returns the count.
The one place a wfm_seq_t becomes bits. A descriptor materialises its own fields through this, and a consumer that takes a RAW ARRAY rather than a description the DSSS chip builder is the one in this tree calls it to expand a generated sequence into a buffer first. Without that, bits is NULL for every generated kind and the array consumer reads through it.
Parameters:
sthe sequence; a LITERAL copies, the generated kinds run their generator.outreceivess->lenbits, one per byte.max_outcapacity; 0 is returned ifs->lenexceeds it.
Returns:
bits written, or 0 if the sequence is unbuildable (a LITERAL with no array, a length past max_out, a generator that refused its own parameters) or has no bits of its own (WFM_SEQ_DATA, whose bits are its data source's).
function dp_wfm_seq_pn_create¶
The PN generator a WFM_SEQ_PN sequence names, at its first bit. NULL if it names none.
The ONE resolution of a PN sequence's defaults: a zero poly is the maximal-length polynomial for its register (and a register that has none is refused), and a zero seed is 1. dp_wfm_seq_bits renders a field through it, and a pn:0 data source keeps one running across frames, so a stream's chunk k is bits [k LEN, (k+1) LEN) of the very sequence its Field names.
Parameters:
sa sequence of kind WFM_SEQ_PN; any other kind is NULL.
Returns:
a generator the caller frees with dp_pn_destroy(), or NULL.
wfm_seq_t q = { .kind = WFM_SEQ_PN, .len = 31, .reg_bits = 5 };
dp_pn_state_t *p = dp_wfm_seq_pn_create (&q);
uint8_t b[31];
dp_pn_generate (p, 31, b, 31); // one period of the 5-bit m-sequence
dp_pn_destroy (p);
Macro Definition Documentation¶
define WFM_FIELD_MAX_BITS¶
Bits one Field may hold, LEN * REPS ; the grammar refuses more.
Derived, not picked. The longest frame a shipped stage accepts is the CCSDS Reed-Solomon codeblock at its deepest interleaving, 255 symbols x 8 bits x depth 8 = 16320 bits. The margin is 16, the smallest power of two that still admits a Field one whole period of the default CCSDS randomiser long (131071 bits; 8 x 16320 falls 511 short). A Field is a finite run of a frame, so a longer one is a STREAM, which is a data source's job, not a Field's. The bound is checked at parse, before any allocation sized by the caller's number. ccsds_tm/desc.c re-derives it from the R-S constants at compile time, so it cannot drift.
define WFM_FRAME_CRC_BITS¶
Bits of CRC-16-CCITT, when a frame carries one.
define WFM_FRAME_MAX_FIELDS¶
Fields one description may carry.
Raised from 8 against a measurement rather than a feeling: the deepest description doppler builds today is SIX fields (ASM, preamble, sync, payload, CRC, R-S parity) and FIVE stages, so 8 left room for two more fields — and a user frame that adds a header and a tail to that shape reaches the old ceiling exactly. The descriptor is a POD carried by value, so the cost is bytes on a stack frame: 1136 -> 2152, which is still a comfortable local.
define WFM_FRAME_MAX_STAGES¶
Stages one description may carry.
define WFM_FRAME_NAME_MAX¶
Bytes a field's name may use, NUL included.
The documentation for this class was generated from the following file native/inc/doppler/wfm/wfm_frame.h