Skip to content

Fields — bits, written as text

Every place wfmgen takes a run of bits takes it the same way: as one Field, a short piece of text such as 0x1ACFFC1D or pn:1023:10. The command line's --data, --fill, --sync, --acq-code and --data-code, a scene's "spec", Python's field_bits() and C's dp_wfm_field_parse() all read it with one parser, so a Field means the same bits on every face. This page is the grammar; the reasoning behind it is the design's §F.

The forms

write means
1101 the bits themselves, left to right
0xA5 hex, 4 bits a digit, most significant first
pn:LEN:REG[:SEED[:POLY]][:fibonacci] LEN bits from one REG-bit LFSR. SEED 0 selects 1; POLY 0 the maximal-length one
gold:LEN:REG:TA:SA:TB:SB LEN bits of a Gold code: two REG-bit registers, taps and seeds
dotted:LEN 1010…, LEN bits, starting high

Any of them may end in *REPS, the same bits again REPS times.

>>> from doppler.wfm import field_bits
>>> field_bits("1101").tolist()
[1, 1, 0, 1]
>>> field_bits("0xA5").tolist()
[1, 0, 1, 0, 0, 1, 0, 1]
>>> field_bits("pn:15:4").tolist()
[1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0]
>>> field_bits("dotted:6").tolist()
[1, 0, 1, 0, 1, 0]
>>> field_bits("0101*3").tolist()
[0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]

LEN is the number of bits that come out; REG is the width of the register that makes them. They are different numbers: pn:15:4 is one whole period of a 4-bit register, and pn:100:4 is the same period cycled.

A repetition is the same bits again, never a fresh draw — a preamble repeated four times is one period four times, which is what a receiver integrates over:

>>> import numpy as np
>>> np.array_equal(field_bits("pn:31:5*4"), np.tile(field_bits("pn:31:5"), 4))
True

Numbers

Every number — LEN, REG, SEED, POLY, the Gold taps and seeds, REPS — is decimal, or hex after 0x, so a tap mask reads the way the literature writes it. A leading 0 is decimal, never octal. A number is read whole: 12abc, -1, 5 and an empty pn::10 are refused, not read as far as they go.

>>> np.array_equal(field_bits("pn:0x1f:5"), field_bits("pn:31:5"))
True

The limits

limit why
LEN ≥ 1, REPS ≥ 1 a Field is never empty
REG is 1 to 64 the register is a 64-bit word
SEED, POLY, taps fit in REG a wider number would be masked, silently: pn:31:5:32 is the all-zero register
a 1-bit REG needs a POLY one bit has no maximal-length polynomial to default to
LEN × REPS ≤ 261120 a longer run is a stream, not a Field; derived here

On the command line, *REPS belongs to --acq-code alone, because a preamble is the one field that repeats: a sync word or a payload sent twice is not what those flags mean, so --sync 'pn:31:5*2' is refused. Quote a Field with a * in it, or the shell globs it.

Refused, never repaired

A Field outside the grammar is refused with a sentence naming the rule, and nothing is built. A 0/1 string with a stray character is the case that matters most: a typo that quietly shortened a sync word would still assemble, and sync to nothing.

# Each line exits 2 and says why.
! wfmgen --type bits --data 01a1 -o x.cf32
! wfmgen --type bits --data pn:31:5:32 -o x.cf32
! wfmgen --type bits --data pn:4000000000:5 -o x.cf32
error: --data 01a1: a binary literal holds a character other than 0 or 1
error: --data pn:31:5:32: a pn SEED or POLY has a bit above its REG-bit register
error: --data pn:4000000000:5: LEN * REPS is past the Field bound of 261120 bits

One way to write each Field

Two spellings of one Field are the same Field, and it has one canonical text: a literal as hex when its length is a multiple of 4 and as bits otherwise; a generator with only what differs from its defaults; *REPS only past one. --record writes that text, so a capture's metadata says what produced its bits:

wfmgen --type bits --data 0101010111001010 \
       --sync pn:0x1f:5:0:0:galois --acq-code '1111*2' \
       --record run.json -o framed.cf32
grep -E '"(data|spec)"' run.json
            "data": "0x55ca",
                        "spec": "0xf*2"
                        "spec": "pn:31:5"
                        "spec": "data:16"

The preamble, the sync word and the payload are the fields of the frame description the flags spell, so the record stores them there, as Fields.

From C

The same parser, and the same refusals. This is native/examples/wfm_field_demo.c, which make test-examples-c builds and runs; its Python twin is src/doppler/examples/wfm_field_demo.py.

/**
 * wfm_field_demo.c — bits, written as text: the Field grammar from C.
 *
 * Every place wfmgen takes a run of bits — `--data`, `--sync`, `--acq-code`,
 * `--data-code`, a scene's "spec", Python's field_bits() — reads it with ONE
 * parser, dp_wfm_field_parse(). This is that parser's C face, and the twin of
 * src/doppler/examples/wfm_field_demo.py.
 *
 * What this demonstrates, in order:
 *
 *   1. A Field is text in, bits out: size with a NULL buffer, then render.
 *   2. Two spellings of one Field are the same Field, and it has ONE
 *      canonical text, which dp_wfm_field_format() writes and which parses
 *      back to the same field.
 *   3. A repetition is the same bits again, never a fresh draw.
 *   4. A Field outside the grammar is REFUSED with a sentence naming the
 *      rule — a stray character, a seed wider than its register, a length
 *      past the Field bound — and nothing is built.
 *
 * Every check is explicit and returns non-zero on failure; `assert()` is not
 * used, because examples build Release and NDEBUG would compile the checks
 * out.
 *
 * Build:
 *   make build && ./build/native/examples/wfm_field_demo
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "doppler/wfm/wfm_frame.h"

/* A Field's bits: the size, then the render. The caller frees. */
static uint8_t *
bits_of (const char *spec, size_t *n)
{
  const char *why = NULL;
  *n              = dp_wfm_field_bits (spec, NULL, 0, &why);
  if (*n == 0)
    {
      fprintf (stderr, "  %s: %s\n", spec, why);
      return NULL;
    }
  uint8_t *b = malloc (*n);
  if (!b || dp_wfm_field_bits (spec, b, *n, NULL) != *n)
    {
      free (b);
      return NULL;
    }
  return b;
}

int
main (void)
{
  /* 1. Text in, bits out. */
  size_t   n;
  uint8_t *b = bits_of ("0xA5", &n);
  if (!b || n != 8)
    return 1;
  printf ("0xA5 ->");
  for (size_t i = 0; i < n; i++)
    printf (" %u", b[i]);
  printf ("\n"); /* 0xA5 -> 1 0 1 0 0 1 0 1, most significant first */
  free (b);

  /* 2. One canonical text: the defaults and the radix are spelling. */
  wfm_field_t f;
  uint8_t    *owned = NULL; /* a literal's bits; NULL for a generator */
  if (dp_wfm_field_parse ("pn:0x1f:5:0:0:galois", &f, &owned, NULL) != 0)
    return 1;
  char text[64];
  dp_wfm_field_format (&f, text, sizeof text);
  printf ("pn:0x1f:5:0:0:galois is written %s\n", text);
  if (strcmp (text, "pn:31:5") != 0)
    return 1;

  /* 3. *REPS repeats the SAME bits: one period, four times. */
  size_t   n1, n4;
  uint8_t *one  = bits_of ("pn:31:5", &n1);
  uint8_t *four = bits_of ("pn:31:5*4", &n4);
  if (!one || !four || n4 != 4 * n1)
    return 1;
  for (size_t r = 0; r < 4; r++)
    if (memcmp (four + r * n1, one, n1) != 0)
      return 1;
  printf ("pn:31:5*4 is pn:31:5 four times over (%zu bits)\n", n4);
  free (one);
  free (four);

  /* 4. Refused, never repaired: each names its rule. */
  static const char *const bad[] = { "01a1", "pn:31:5:32", "pn:4000000000:5" };
  for (size_t i = 0; i < sizeof bad / sizeof *bad; i++)
    {
      const char *why = NULL;
      if (dp_wfm_field_parse (bad[i], &f, &owned, &why) == 0 || !why)
        return 1;
      printf ("%-16s refused: %s\n", bad[i], why);
    }
  return 0;
}