Part I: Foundations of Sensory AI
Chapter 3: Signals, Sampling, Time, and Synchronization

Acquisition protocols and metadata

"A number with no units, no clock, and no sensor ID is not data. It is a rumor. I can fit a model to a rumor, and it will be exactly as trustworthy as one."

A Fastidious AI Agent

The big picture

The previous six sections treated the sample as if it arrived on your desk labeled and ready. It does not. Between the transducer and the array your model consumes sits an acquisition stack: a chain of buses, transports, and drivers that moves raw counts off silicon, plus a body of metadata that records what those counts mean. Get the protocol wrong and you drop samples or scramble their order; omit the metadata and you ship a stream that is numerically intact yet scientifically meaningless. This section is about the layer where measurement becomes a durable, self-describing record. It is unglamorous plumbing, and it is where most real sensor projects quietly fail: not in the model, but in a dataset whose sample rate was guessed, whose units were assumed, and whose provenance nobody wrote down.

This section closes Chapter 3, connecting the acquisition machinery to the timing discipline of Section 3.3 and the framing of Section 3.6, and drawing on the calibration and units vocabulary of Chapter 2. It is the on-ramp to Chapter 5: where that chapter builds leakage-safe datasets, this one supplies the raw material, the fields you must capture at acquisition time because they cannot be reconstructed afterward.

The acquisition stack: from counts to a timestamped record

A single dropped frame on the wrong bus can quietly turn a 4 kHz stream into a 2 kHz one that still looks perfectly regular, and a classifier trained on it will place every feature at the wrong frequency with no error ever surfacing. Knowing the stack that carries counts off silicon is what lets you catch that corruption before it hardens into a dataset. What it is. Getting a sample from a transducer to storage crosses at least two protocol layers. At the bus level, a microcontroller pulls raw registers over a wired interface: Inter-Integrated Circuit (I2C) and Serial Peripheral Interface (SPI) for on-board chips (an Inertial Measurement Unit (IMU) or a pressure sensor), Universal Asynchronous Receiver-Transmitter (UART) or Controller Area Network (CAN) in vehicles, and analog lines into an Analog-to-Digital Converter (ADC) for everything else. At the transport level, framed samples travel to a host or the cloud over Message Queuing Telemetry Transport (MQTT), gRPC (a high-performance remote procedure call framework), WebSocket, a Robot Operating System (ROS) topic, or Lab Streaming Layer (LSL). Each layer imposes its own bandwidth ceiling, its own framing, and its own failure mode. Figure 3.7.1 traces this two-layer path from transducer to stored record and marks the one place a trustworthy timestamp can be applied.

The acquisition stack from transducer to timestamped record A transducer feeds a bus layer, then an MCU that stamps the timestamp, then a transport layer, then host storage, with metadata attached to the final record. Bus layer Transport layer Transducer raw counts Bus I2C / SPI / ADC MCU stamp clock here Transport MQTT / LSL / gRPC Host storage durable record Metadata units, rate, ID, firmware
Figure 3.7.1. The acquisition stack: raw counts leave the transducer over a bus (I2C, SPI, or an ADC line), the MCU applies the earliest trustworthy timestamp, framed samples cross a transport (MQTT, LSL, or gRPC), and the host writes a durable record to which the metadata (units, rate, device ID, firmware) is attached. The dashed arrow marks metadata joining the samples at storage time.

Take I2C, one of the most common on-board buses, as the concrete case. It is a two-wire serial standard (one clock line, one data line) on which a single controller addresses each sensor by a 7-bit address and clocks registers out one bit at a time, so its typical 100 kHz to 400 kHz ceiling is shared across every chip on the pair rather than granted to each. That matters because aggregate throughput, not per-sensor throughput, is the budget you must respect: hang eight chips off two pins and the bus, not the sensor, becomes the bottleneck. Reach for I2C when pin count and board space dominate and rates are modest, and switch to SPI, which gives each device its own line and tens of megahertz, when one high-rate sensor needs the bandwidth to itself.

Why the choice matters. The protocol decides three things a model inherits. First, ordering and loss: a lossy transport, such as the User Datagram Protocol (UDP) or Bluetooth Low Energy (BLE) notifications, can reorder or drop frames, so you cannot assume sample \(n+1\) followed sample \(n\) in time (revisit Section 3.4 on packet loss). Second, where the timestamp is stamped: a clock stamped at the sensor microcontroller unit (MCU) beats one stamped when a buffer finally reaches the host. The queue delay in between is variable and unknown, so the host-side clock drifts from the true sample time. Third, throughput headroom: an I2C bus at 400 kHz cannot sustain a 4 kHz burst across eight chips, so the driver silently decimates and your effective rate is a fiction. Timestamp as early as you physically can, prefer transports that expose sequence numbers, and measure the delivered rate rather than trusting the configured one. In short: the protocol you choose silently stamps ordering, timing, and rate into every sample, so pick it deliberately before you trust a single number it hands you.

Common Misconception

The misconception is that the sample rate you configure is the sample rate you get. Writing 4 kHz to a control register is a request, not a guarantee: bus contention, first-in, first-out (FIFO) overflow, host scheduling jitter, and dropped frames routinely leave the delivered rate below the configured one, and the samples you receive are still evenly numbered even though they are no longer evenly spaced in time. This is exactly why the metadata must record the measured rate alongside the nominal one, and why the ingestion gate later in this section cross-checks the two rather than believing the register.

Push versus pull. Two acquisition disciplines dominate. In polling, the host reads on a timer, which is simple but couples your timeline to the host scheduler's jitter. In interrupt or FIFO mode, the sensor buffers samples on-chip and raises a data-ready line, so the sensor's own crystal governs timing while the host merely drains a queue. FIFO acquisition is almost always the right default above a few hundred hertz. It decouples the sample clock from the noisy host and lets you batch transfers to save power, a tradeoff that returns in force in Chapter 59. Getting those counts off the chip intact and in order is only half the task; the other half is recording what each count means, which is where metadata takes over.

Metadata: the fields that make a stream self-describing

What it is. Metadata is the set of facts required to interpret a sample that are not themselves samples. A defensible sensor record carries, at minimum: a device and channel identifier, the nominal and measured sample rate, physical units and the scale factor from raw counts to those units, the sensor range and configured gain, the timestamp source and its epoch (the zero-reference instant the timestamps count from, such as the Unix epoch) and timezone, the firmware and calibration versions, and the sensor's placement or mounting. None of these are optional decoration. Each answers a question a downstream model or auditor will eventually ask, and each is cheap to record now and impossible to recover later. Figure 3.7.2 illustrates Anatomy of a self-describing sensor record versus a bare array.

Anatomy of a self-describing sensor record versus a bare array
Figure 3.7.2: A bare CSV column loses its units, rate, and provenance the moment it is written, while a self-describing record glues those metadata fields (device ID, units, measured and nominal rate, scale, epoch and timezone, firmware, calibration, coordinate frame, mounting) to the array itself so they cannot detach.

Why it is load-bearing. Consider what breaks without each field. No units, and a fusion stage adds acceleration in \(g\) to acceleration in \(\text{m/s}^2\). No measured rate, and your spectral analysis in Chapter 7 places every peak at the wrong frequency. No calibration version, and a firmware update that changed the gain silently splits your dataset into two incompatible populations. No device ID, and you cannot split train and test by device, arguably the single most important guard against the leakage that Chapter 5 is built around. Metadata is the join key between a raw stream and its physical meaning.

Mental Model

Think of a jar of homemade jam. The jam is the samples; the handwritten label (fruit, sugar ratio, date canned) is the metadata. If you keep the labels on a separate list in a kitchen drawer, the first time a jar gets moved to another shelf the list and the jars drift apart, and you are reduced to opening lids and tasting to guess what each one holds. Glue the label to the jar itself, the way a self-describing format glues attributes to the array, and the two can never separate: whoever picks up the jar knows instantly what is inside and when it was made, with no external index to consult or lose.

Key insight

Record the metadata that cannot be recovered from the samples, and record it at the moment of acquisition. A sample rate can sometimes be re-estimated from timestamps; a sensor's firmware version, mounting orientation, and coordinate convention cannot be inferred from the numbers at all. The test for whether a field belongs in metadata is simple: if a stranger handed only the raw array could reconstruct it, it is derivable; if they could not, it must be captured now or it is gone. Treat acquisition metadata as write-once provenance, not as a configuration file you might edit later.

Schemas and standards: agreeing on the record's shape

Why standards exist. Every lab that invents its own metadata layout invents its own way to be incompatible with everyone else, including its future self. Community schemas encode the hard-won list of required fields so you do not rediscover it after losing a dataset. In biosignals, the European Data Format Plus (EDF+) standardized channel labels, per-channel scaling, and annotations for clinical recordings; the neuroimaging world formalized folder-level provenance as the Brain Imaging Data Structure (BIDS). For self-describing arrays, Hierarchical Data Format version 5 (HDF5) and its cloud-native cousin Zarr attach named dimensions and arbitrary attributes directly to the data, so units and sample rate travel inside the file rather than in a fragile sidecar. For sensor descriptions on the web, the World Wide Web Consortium (W3C) SOSA/SSN (Semantic Sensor Network) ontology and Open Geospatial Consortium (OGC) SensorML give a vocabulary for observations, procedures, and platforms. Robotics leans on ROS bag files and message definitions to keep a timestamped, typed record of every topic (as of 2024, ROS 2 and its rosbag2 format have largely replaced the original ROS 1 bags, whose Noetic release reached end-of-life in 2025, though the self-describing, typed-topic discipline is unchanged). What all these standards share, beneath their differing vocabularies, is one organizing principle that a single acronym now names.

Checkpoint

So far: a sample crosses a bus and then a transport, the earliest trustworthy timestamp belongs at the MCU, and the facts that cannot be recovered from the numbers (units, measured rate, device ID, firmware, mounting) must be captured at acquisition time. Community schemas such as EDF+, BIDS, HDF5, and Zarr exist so that this same discipline rides inside the file rather than in a fragile sidecar.

The FAIR frame. The unifying principle is that data should be Findable, Accessible, Interoperable, and Reusable (FAIR). In practice FAIR reduces to a discipline you can apply on any project: a stable identifier per recording, a documented schema, units and vocabularies that other tools recognize, and enough provenance that a result can be reproduced. You do not need a heavyweight ontology on day one; you need to pick one self-describing container and never write a bare comma-separated values (CSV) file of unlabeled columns again.

Research Frontier

The standards above describe sensors and files; the newer frontier is describing the whole dataset in a way a machine learning (ML) pipeline can load without human glue code. MLCommons Croissant (2024) is a JSON-LD (JSON Linked Data, JSON whose fields are tagged with shared vocabulary terms) metadata format that layers on schema.org to bundle a dataset's structure, provenance, field units, and train/test splits into a single machine-readable manifest, and it is already consumed directly by Hugging Face, Kaggle, and TensorFlow Datasets. For sensor work it points toward a future where the acquisition metadata this section insists on is not merely written down but becomes the loader itself, carrying the device-wise split and unit declarations straight into training so the leakage failure in the automotive story cannot recur.

In practice: the automotive fleet whose two trucks disagreed

A logistics company logged wheel-speed and IMU data over the vehicle CAN bus across a fleet to train a road-surface classifier. The pilot model scored beautifully in validation and then behaved erratically on new trucks. The cause was buried in missing metadata. Half the fleet ran a firmware revision that reported yaw rate in degrees per second; the other half, after an over-the-air update, reported radians per second, and nobody logged the firmware version alongside the samples. The two conventions differ by a factor of about 57, so the model had effectively learned to separate trucks by their units and called it "road surface." Worse, because validation split randomly rather than by vehicle, both unit conventions appeared in training and test, hiding the defect until deployment on a truck the split had never isolated. The fix was not a better model. It was a one-line addition to the CAN logger that stamped the firmware version, units, and vehicle ID into every record, plus a device-wise split. The classifier's honest accuracy dropped, then became real.

The code below encodes the discipline as a small guard: an ingestion gate that refuses any batch missing the fields required to interpret it, and that flags when the measured rate drifts from the nominal one. Wiring this in front of storage turns a whole class of silent dataset corruption into a loud, early failure.

import numpy as np

REQUIRED = ("device_id", "channel", "units", "scale",
            "nominal_rate_hz", "t0_unix", "timezone",
            "firmware", "calibration_id")

def ingest(samples_raw, timestamps, meta, rate_tol=0.02):
    missing = [k for k in REQUIRED if k not in meta]
    if missing:
        raise ValueError(f"refusing batch: missing metadata {missing}")

    # Verify the delivered rate against the claim; a drift here means
    # dropped samples, a wrong config, or a lying driver.
    dt = np.diff(timestamps)
    measured = 1.0 / np.median(dt)
    if abs(measured - meta["nominal_rate_hz"]) / meta["nominal_rate_hz"] > rate_tol:
        raise ValueError(f"rate mismatch: claimed {meta['nominal_rate_hz']} Hz, "
                         f"measured {measured:.1f} Hz")

    physical = samples_raw.astype(np.float64) * meta["scale"]   # counts -> units
    return {"values": physical, "t": timestamps, "meta": meta,
            "measured_rate_hz": float(measured)}

meta = dict(device_id="imu-0421", channel="gyro_z", units="rad/s", scale=1.526e-4,
            nominal_rate_hz=200.0, t0_unix=1_752_000_000.0, timezone="UTC",
            firmware="2.3.1", calibration_id="cal-2026-05-tumble")
raw = np.random.randint(-2000, 2000, size=1000)
t = meta["t0_unix"] + np.arange(1000) / 200.0
print(ingest(raw, t, meta)["measured_rate_hz"])
An acquisition gate that rejects any batch lacking the nine fields needed to interpret it and cross-checks the delivered rate against the nominal one. The scale field converts raw gyro counts to rad/s, and the rate check catches the dropped-sample and wrong-config failures from Section 3.4 before they reach storage. This function is the enforcement point referenced by the metadata discipline above.

Step-Through: the ingestion gate on a six-sample batch

Trace ingest with a tiny gyro batch that has one dropped sample, to see why a robust rate check still passes. Nominal rate is 200 Hz, so the expected period is 0.005 s, and scale is 1.526e-4 rad/s per count.

  1. Metadata check. All nine REQUIRED keys are present, so missing is the empty list and the gate does not raise.
  2. Timestamps arrive as t = [0.000, 0.005, 0.010, 0.020, 0.025, 0.030] seconds. Notice the jump from 0.010 to 0.020: the sample that should have landed near 0.015 was dropped.
  3. Inter-sample gaps. np.diff(t) = [0.005, 0.005, 0.010, 0.005, 0.005]. Four gaps are nominal, one is doubled by the drop.
  4. Median, not mean. np.median(dt) = 0.005, so measured = 1 / 0.005 = 200.0 Hz. The mean gap would have been 0.006 s (about 167 Hz) and falsely tripped the alarm; the median shrugs off the single outlier, which is the point.
  5. Tolerance test. abs(200.0 - 200.0) / 200.0 = 0.0, well under rate_tol = 0.02, so the batch is accepted. (A bus that had silently halved the rate to 100 Hz would give 0.5 here and be rejected.)
  6. Scaling. A raw count of 1310 becomes 1310 * 1.526e-4 = 0.1999 rad/s. The gate returns the physical values plus measured_rate_hz = 200.0 stamped into the record.

The lesson: the median makes the rate check robust to isolated drops, so it flags systematic rate errors (wrong config, sustained loss) without false-alarming on the occasional missing frame. Catching that isolated drop is the job of the packet-loss proxy in the exercise below.

Real-World Application: autonomous-vehicle datasets

The nuScenes dataset ships exactly the acquisition metadata this section argues for: every one of its six cameras, five radars, and one lidar carries a calibrated_sensor record with per-sensor extrinsics and intrinsics (the sensor's pose in the vehicle frame, and its internal optical parameters), an ego_pose with a timestamp, and channel identifiers that pin each measurement to a known device and mounting. Because that provenance travels with the samples, a fusion stack can transform every sensor into a common frame and split train and validation by scene rather than by frame. Strip those tables out and the same raw point clouds and images become an uninterpretable pile of numbers.

A $327 million missing units field

In 1999 NASA's Mars Climate Orbiter fired its thrusters on navigation data in which one team's ground software produced impulse in pound-force-seconds while the spacecraft's software expected newton-seconds, a factor of about 4.45. No field in the exchanged stream declared the units, so nobody caught the mismatch until the probe dipped too low into the Martian atmosphere and was destroyed. The mission review board's conclusion reads like the thesis of this section: the numbers were correct, the units were unstated, and an unlabeled quantity is not a measurement. The same missing-units defect that vaporized a spacecraft is the one that made two trucks disagree about the road.

The right tool: self-describing arrays instead of a sidecar zoo

Hand-managing a parallel JSON sidecar for every array (keeping units, rates, coordinates, and axis labels in sync with the data through every slice and resample) is roughly eighty lines of bookkeeping that drifts out of date the first time someone edits one file and not the other. An xarray dataset attaches named dimensions, coordinates, and attributes to the array itself, so the metadata rides inside the object and survives slicing:

import numpy as np, xarray as xr

da = xr.DataArray(
    np.random.randn(1000, 3), dims=("time", "axis"),
    coords={"time": np.arange(1000) / 200.0, "axis": ["x", "y", "z"]},
    attrs={"units": "rad/s", "device_id": "imu-0421",
           "nominal_rate_hz": 200.0, "firmware": "2.3.1"})
da.to_netcdf("gyro.nc")            # units + coords + attrs travel with the data
window = da.sel(time=slice(0.0, 1.0))   # metadata preserved through the slice
print(window.attrs["units"], window.sizes)
A self-describing gyro record with xarray: named axes, a real time coordinate, and physical attributes stored with the array and written to a single netCDF/HDF5 file. Eighty lines of sidecar synchronization collapse to a constructor call, and the units cannot silently detach from the numbers.

Exercise

Harden the ingestion gate into something you would trust in a fleet:

  1. Extend ingest to also reject a batch whose timestamps are non-monotonic (out-of-order delivery) and to report the fraction of inter-sample gaps that exceed twice the nominal period, a proxy for packet loss.
  2. Add a metadata field coordinate_frame and write a check that two IMU streams claiming to fuse share the same frame, refusing the fusion otherwise. Explain which single character of a wrong answer this would have caught in the automotive story.
  3. Serialize an accepted batch to both a bare CSV and an xarray netCDF file. Hand each to a colleague with no context and note which fields they can recover from each.
Hint

For part one, np.all(np.diff(timestamps) > 0) tests monotonicity and the gap fraction is np.mean(np.diff(timestamps) > 2 / rate). For part three, the CSV strips units, rate, and provenance the moment it is written; that asymmetry is the whole argument for self-describing containers.

Self-check

  1. Why is a timestamp applied at the sensor MCU generally more trustworthy than one applied when the buffer reaches the host?
  2. Name three metadata fields that cannot be reconstructed from the raw samples alone, and one that sometimes can.
  3. A dataset trains and tests by random split and reaches suspiciously high accuracy. Which single metadata field would let you re-split to expose device-level leakage, and why does that field belong in the acquisition record rather than added later?

Lab 3

build a streaming ingestion pipeline with windowing, missing-data markers, and cross-sensor time alignment.

Try It: give a CSV amnesia, then cure it

Feel the difference between a bare array and a self-describing one on your own laptop, using only numpy, pandas, and xarray (pip install xarray netcdf4).

  1. Synthesize a gyro stream: x = np.random.randn(1000) sampled at 200 Hz, and save it with pd.DataFrame({"col0": x}).to_csv("gyro.csv", index=False), deliberately writing one unlabeled column and nothing else.
  2. Reopen only gyro.csv and try to answer three questions from the file alone: what are the units, what is the sample rate, and when did acquisition start? You cannot, and that is the point.
  3. Rebuild the same numbers as an xr.DataArray with dims=("time",), a real coords={"time": np.arange(1000)/200.0}, and attrs={"units": "rad/s", "device_id": "imu-0421", "nominal_rate_hz": 200.0, "firmware": "2.3.1"}, then write it with da.to_netcdf("gyro.nc").
  4. Reload with xr.open_dataarray("gyro.nc") and print .attrs and .sizes; confirm the units, rate, and device ID survived the round trip that the CSV destroyed.
  5. Slice one second with .sel(time=slice(0.0, 1.0)) and print the slice's .attrs; verify the physical metadata is still attached to the sub-window, not stripped off by the operation.

Bibliography

Sensor description and metadata standards

Haller, A., Janowicz, K., Cox, S., et al. (2017). Semantic Sensor Network Ontology (SOSA/SSN). W3C Recommendation.

The web standard vocabulary for describing sensors, observations, procedures, and platforms; the reference model when your metadata must interoperate across organizations.

Botts, M., and Robin, A. (2014). OGC SensorML: Model and XML Encoding Standard. Open Geospatial Consortium.

A rich schema for sensor systems and processing chains, widely used in geospatial and environmental sensor networks where provenance of the measurement process is mandatory.

Kemp, B., and Olivan, J. (2003). European data format 'plus' (EDF+), an EDF alike standard format for the exchange of physiological data. Clinical Neurophysiology.

The de facto container for clinical biosignals, standardizing per-channel labels, physical scaling, and annotations; the model for how to make a physiological recording self-describing.

Self-describing data formats

The HDF Group (1998-present). Hierarchical Data Format version 5 (HDF5).

The workhorse binary container that stores named datasets with attached attributes, letting units and sample rates live inside the file; the substrate under netCDF, NWB, and many sensor archives.

Zarr Developers (2024). Zarr: chunked, compressed, N-dimensional arrays.

Cloud-native self-describing arrays with per-chunk compression and JSON attribute metadata; the format of choice for large sensor archives read in parallel from object storage.

Gorgolewski, K. J., Auer, T., Calhoun, V. D., et al. (2016). The Brain Imaging Data Structure (BIDS), a format for organizing and describing neuroimaging experiments. Scientific Data.

A concrete, adopted example of folder-level provenance and naming conventions; a template for how a community turns metadata discipline into a shareable standard.

Provenance, documentation, and reproducibility

Wilkinson, M. D., Dumontier, M., Aalbersberg, I. J., et al. (2016). The FAIR Guiding Principles for scientific data management and stewardship. Scientific Data.

The Findable, Accessible, Interoperable, Reusable principles that frame why acquisition metadata is not optional; the citation behind most modern data-management mandates.

Gebru, T., Morgenstern, J., Vecchione, B., et al. (2021). Datasheets for Datasets. Communications of the ACM.

Argues that every dataset should ship a datasheet documenting how it was collected and its intended use; the machine-learning analogue of acquisition metadata, and a direct feed into Chapter 5.

Kothe, C., et al. (2014-present). Lab Streaming Layer (LSL).

A transport that unifies time-series streams from many devices with a common clock and per-stream metadata header; a practical reference design for real-time multi-sensor acquisition.

What's Next

Chapter 4 leaves the mechanics of getting clean, labeled, timestamped samples behind and turns to what they tell us. The metadata gate here decides whether a number is admissible; the probability toolkit next decides what to believe once it is admitted, giving us estimators, priors, and the aleatoric-versus-epistemic split that lets every later chapter attach honest uncertainty to a sensor reading rather than a bare point estimate.