unit-safe / spec v1.1 / c99

Unit-safe serialization
for science and industry.

Every value carries its width, base, and physical unit inline — no schema, just human-readable text. A bare 9.81 is never ambiguous, and a mismatched unit is a parse error.

bovnar — telemetry.bvnr
telemetry.bvnr
examples telemetry.bvnr
1
2
3
4
5
6
7
8
# Every measurement carries a validated unit .host = "api.sensors.io"; .mass = 142.6 k~g; .sensor = { .pressure = <float:32,k~Pa> 101.325; .velocity = <float:64,m/s> 9.81; }; .matrix = [1,2,3]/[4,5,6];
◈ BOVNAR
UTF-8 Ln 8, Col 1 unit-safe
scroll

One format, end to endconfig in, decoded values out.

You saw the syntax — here's a spacecraft running on it. One format, the whole loop: config in, telemetry out. Edit the orbit; the craft encodes each measurement into a .bvnr frame and the ground station decodes it — the orbit you see is drawn only from those decoded values, a full round-trip through the format. Units, bit-widths and types ride inside the stream; no schema required. Hover a gauge to jump to the line it decoded.

How it works — the full round-trip
  1. The config on the left isn't a form — it's a real .bvnr document you can edit freely. Nothing changes until you press Apply.
  2. When you do, it's read straight from the text. If something doesn't make sense, you get a friendly error pointing at the line, and the live view keeps running on the last good config — a bad edit can't break it.
  3. Those values set the scene, and the demo takes it from there.
  4. From then on, every reading is written out and read back as .bvnr, the same format you just edited — units and types travel right alongside the numbers.
  5. So the gauges and charts show only what came back through the format, never a number taken on faith. Flip on the noise and you can watch it shrug off a garbled frame.
  6. Hover any gauge to light up the exact line it was read from.
every number you see travels:
editparsesimulateencode(corrupt?)parsedisplay
Spacecraft config editable · .bvnr
Orbit visualizer ECI · XY projection
Earth (day / night) spacecraft (decoded pos)
Downlink frame .bvnr stream
160×
frame 0 MET 0 s orbits 0.00 dropped 0 lock ACQ


      
With resync on, a corrupt assignment is skipped and the rest of the frame still decodes — the reader's recovery mode. Off, one bad line drops the whole frame. Flip noisy channel to compare.

The real event stream.

This is the reference C reader's on_verified callback stream, reproduced in the browser — the same events, in the same order, that bvnr_read() delivers. Bare values get the validator's synthesised default annotation; type, range, base and unit violations surface on the separate on_error channel, exactly as the C core reports them.

Editor
ready
on_verified · event tree 0 events
Events appear here as you type.
on_error
no errors

The context travels with the number.

In science and industry the costly failures are rarely bad syntax — they are unit confusion: pounds-force read as newtons, feet as meters. The number parses fine; the 9.81 is right but the context is wrong. Bovnar keeps that context — width, base, and unit — inline with every value, as plain human-readable text with no external schema, and rejects a mismatched unit as a parse error rather than a silent bug.

a number, no context
9.81
Width? Base? Unit? Anyone's guess without an out-of-band schema or naming convention.
a value, in context
.thrust = <float:64,k~g·m·s⁻²> 9.81;
Type, width, and unit ride inline, in the same byte stream as the value. No schema, no codegen, no lookup table.
01 Bit-width
<uint:16> 443
Exact storage width, declared inline and never inferred — from a single bit to thousands. A uint:16 stays exactly 16 bits: it can’t silently widen to 64, nor be reinterpreted as a float.
02 Numeric base
<uint:32,_2> 11001010
Binary, hex, or decimal — the radix is declared, so 11001010 is never mistaken for a decimal. Values whose digits include letters (e.g. hex ff) are written as a quoted string: <uint:_16> "ff".
03 Physical unit
<float:64,k~g*m*s^-2> 1.0
SI base & derived units, IEC prefixes, and compound expressions — validated by the parser, not the reader. Exponents accept a Unicode superscript (s⁻²) or a plain-ASCII caret (s^-2) interchangeably.
Typing it by hand? Both notations are first-class to the parser — the Unicode superscripts (s⁻², ) and an all-ASCII form that uses a caret for the exponent and * in place of the · separator. k~g*m*s^-2 and k~g·m·s⁻² parse to exactly the same unit. The ASCII form needs no obscure glyphs, so it’s the natural choice for hand-authoring .bvnr. The writer emits Unicode superscripts by default, or the caret form when you set BVN_UNIT_ASCII_EXP.

All of it is optional: omit annotations for well-defined defaults (uint:64, float:64), or add them for full parser validation. Both modes are unambiguous — and a .bvnr file stays UTF-8 prose you can read in any editor, no toolchain required.

The grammar of precision.

Every assignment is a declaration: the annotation is part of the value, not a note about it.

Typed Values
type · width · unit
# Integers .port = <uint:16> 443; .offset = <sint:64> -2147483648; .flags = <uint:32,_2> 11001010; # Floats with SI units .temp = <float:32,°C> 36.6; .speed = <float:64,m/s> 9.81; .force = <float:64,k~g*m/s^2> 1.0; .buffer = <uint:64,Mi~B> 16;
Structures & Arrays
nested · multidimensional
# Nested struct .sensor = { .id = <uint:16> 7; .name = "pressure_01"; .val = <float:32,k~Pa> 101.325; }; # 3×3 rotation matrix (rows separated by /) .rotation = [1.0, 0.0, 0.0]/ [0.0, 1.0, 0.0]/ [0.0, 0.0, 1.0];
<family:width,unit>
Type Annotation
Eight families: uint sint float float_fix float_dec utf8 bool datetime (datetime is spec 1.1). Width in bits. Unit in SI notation. All are optional.
k~g*m*s^-2
Compound Units
SI base and derived units, IEC binary prefixes, compound expressions. Units are validated by the parser. No external schema needed.
\x00 … \x00
Octet Streams
Raw binary embedding without Base64 overhead. Framed by null bytes. Text and binary payloads coexist in the same document.

Built for exactness.

Type System
Strong, optional typing
Eight type families with explicit bit-width and (for numbers) a numeric base. Defaults are defined, not guessed. Annotate when precision matters; omit when it doesn't.
<uint:16> 443 <float:32,_16> "1.0p+0" <sint:64> -9223372036
SI Units
First-class physical units
All seven SI base units, 22 derived SI units, and IEC binary prefixes. Compound units, exponents, and prefix enforcement are built into the grammar.
<float:64,k~g*m*s^-2> 1.0 <uint:64,Gi~B> 16 <float:32,m/s^2> 9.807
Streaming
SAX-style incremental reader
Parse from memory, file descriptor, or socket via a symmetric on_unverified / on_verified callback pair. No heap required for the lexer itself.
bvnr_open_read_source( r, &src, NULL, &opts); bvnr_read(r);
Resilience
Error recovery & resync
Optional resync mode skips broken assignments and continues parsing. Suitable for log streams, unreliable transports, and long-running telemetry.
opts.continue_on_error = true; /* parser skips broken record, continues with the next */
DOM API
Tree-based access
Build a navigable DOM from any Bovnar stream, then traverse, query, and mutate it with a clean C API. Or use the Python dict-like interface.
bvn_dom_lookup(doc, ".sensor.val"); doc["sensor"]["val"] # Python
Python
Pure-ctypes + NumPy bridge
No compiled extension. No Cython. A full high-level / streaming API — plus a zero-glue NumPy bridge that loads typed arrays straight into an ndarray with the physical unit attached.
bovnar.to_numpy(arr, return_unit=True) # → (ndarray float64, 'm/s²')

Up in minutes.

01
Read from memory
Register callbacks through bvnr_read_flags_t. Both callbacks receive the event type and parsed data.
#include "bovnar.h" static bool on_event(void *ud, bvnr_event_t ev, bvnr_data_t *d) { if (ev == ev_data) printf("val=%.*s\n", (int)d->length, (const char *)d->data); return true; } int main(void) { const char *src = ".velocity = <float:64,m/s> 9.81;"; bvnr_read_flags_t opts = {0}; opts.on_verified = on_event; bvnr_reader_t *r = bvnr_reader_create(); bvnr_open_read_mem(r, src, strlen(src), NULL, 0, &opts); bvnr_read(r); bvnr_reader_destroy(r); }
02
Write typed values
High-level write helpers accept a key string and a typed value directly. The format is emitted with correct annotations.
#include "bovnar.h" int main(void) { char buf[256]; bvnr_writer_t *w = bvnr_writer_create(); bvnr_sink_t sink; bvnr_sink_to_mem(&sink, (uint8_t *)buf, sizeof(buf)); bvnr_open_write_sink(w, &sink, true, NULL); bool ok; value_unit_t u = bvn_parse_unit( (const uint8_t *)"m/s", &ok); bvnr_write_float_unit( w, "velocity", 64, 9.81, u); bvnr_write_finish(w); bvnr_writer_destroy(w); fwrite(buf, 1, (size_t)bvnr_sink_bytes_written(&sink), stdout); // → .velocity = <float:64,m/s> 9.81; }
01
High-level API
Dict-like loads / dumps interface. Pure-ctypes — no compiled extension required.
import bovnar data = { "sensor_id": 42, "temperature": 36.6, "unit": "celsius", } raw = bovnar.dumps(data) doc = bovnar.loads(raw) print(doc["sensor_id"]) # 42 print(doc["temperature"]) # 36.6
02
Streaming event API
Full control over parse events. Inspect units, type annotations, and raw string representations as they arrive.
from bovnar import Reader, Event, unit_to_str def on_event(ev, data): if ev == Event.DATA: u = data.value_unit if u.num_components: print(data.raw_str(), unit_to_str(u)) src = b".velocity = <float:64,m/s> 9.81;" Reader().read_mem( src, on_verified=on_event ) # → 9.81 m/s
03
Typed round-trips with Quantity
loads(typed=True) wraps each typed value in a Quantity that preserves its exact text, bit width, and unit. Pass the dict directly back to dumps() for a lossless round-trip.
import bovnar src = b".pressure = <float:32,Pa> 101325.0;" doc = bovnar.loads(src, typed=True) q = doc["pressure"] # Quantity('101325.0', FLOAT [Pa]) print(q.raw) # '101325.0' print(q.unit_str()) # 'Pa' # dumps() re-emits annotation + raw text unchanged out = bovnar.dumps(doc) assert bovnar.loads(out, typed=True) == doc
04
NumPy bridge
Typed arrays load straight into a numpy.ndarray — bovnar widths map to native dtypes (float:32 → float32) and the whole-array unit rides alongside. NumPy is an optional, lazily-imported extra (pip install "bovnar[numpy]").
import bovnar, numpy as np src = b".accel = <float:64,m/s^2> " \ b"[9.81,0.02,-9.79]/[0.05,9.80,0.01];" doc = bovnar.loads(src, typed=True) a, unit = bovnar.to_numpy( doc["accel"], return_unit=True) # a.shape == (2, 3) a.dtype == float64 unit == 'm/s²' g = np.linalg.norm(a, axis=1) # vectorized, per row out = bovnar.array_to_bvnr("g_mag", g, unit=unit) # → .g_mag = <float:64,m/s²> [9.810…, 9.800…];
01
Build the library
Requires CMake ≥ 3.21 and a C99-conforming compiler. libm is the only mandatory dependency.
# Clone and build cmake -B build . cmake --build build # Release build cmake -B build \ -DCMAKE_BUILD_TYPE=Release . cmake --build build # Outputs: # build/libbvnr.a # build/libbvnr.so # build/bovnar (CLI)
02
Link & test
The test suite includes unit tests, socket-pair round-trip tests, fuzz harnesses, and a benchmark binary.
# Link your app gcc my_app.c \ -I include \ -L build \ -lbvnr \ -o my_app # Run tests cd build && ctest \ --output-on-failure # Python tests export LIBBOVNAR_PATH=\ $(pwd)/build/libbvnr.so cd python && pytest tests -v

When a wrong unit is a failure.

Reach for Bovnar when units must travel with the data and the receiver may not share a schema. Every measurement is unit-safe by construction — a mismatched unit is a parse error, not a silent bug discovered in production.

Scientific instrumentation & metrology

Readings stay self-describing from the lab bench to the published dataset — width, base, and physical unit ride along with every value.

Industrial telemetry & control

Pressures, flows, and temperatures cross process boundaries without a shared contract, and dimensional mismatches are caught at the parse step.

IoT sensor networks

Heterogeneous devices emit human-readable, type-precise records that any consumer can validate on its own, no central registry required.

Long-term measurement archival

Data written today still means exactly the same thing decades from now — the meaning lives in the file, not in lost tooling.

Everything you need to ship.

Ten documents — from a five-minute tutorial to the formal EBNF grammar and conformance protocol. Read inline below, or download the full set as PDF.