DEV Community

Cover image for Thin-Film Measurement Data: What to Check Before Building a Dashboard
ches
ches

Posted on

Thin-Film Measurement Data: What to Check Before Building a Dashboard

Imagine a coating-line dashboard showing 1,200 nm for thirty seconds.

The film might be consistent. Or the application might have stopped receiving measurements and kept the last value on screen. Looking at the number alone, you cannot tell.

That is the software problem I want to focus on here: what needs to travel with a thin-film measurement so that an application can use it sensibly?

I work on technical content for TDM Technology. Its thin-film thickness guide covers measurement conditions for semiconductor, photovoltaic and display applications, including the distinction between wet-film, physical and model-derived thickness. Those distinctions also belong in the data model, not just in a PDF beside the instrument.

The JSON and Python below are a proposed integration example using synthetic data. They are not TDM SDK calls, a published device payload or a report from a production deployment.

Give the number a record

For this example, the record looks like this:

{
  "schema_version": 1,
  "device_id": "sensor-demo-01",
  "session_id": "session-demo-01",
  "sequence": 42,
  "run_id": "coating-demo-01",
  "recipe_id": "wet-film-demo-v1",
  "measured_at": "2026-09-10T08:00:00.000Z",
  "received_at": "2026-09-10T08:00:00.120Z",
  "film_state": "wet",
  "measurement_status": "valid",
  "thickness": {
    "value": 1.2,
    "unit": "um"
  }
}
Enter fullscreen mode Exit fullscreen mode

The thickness is 1.2 micrometres, or 1,200 nanometres. The unit stays beside the original value, even if the application later standardizes everything to nanometres.

The other fields are there to answer fairly ordinary questions. Which device sent this? Which coating run was active? Was this a new observation or a replay? Which measurement recipe was in use?

Here, recipe_id refers to the measurement configuration, not the coater's operating recipe. The configuration behind it needs a version; reusing the same name after changing the settings would make old results ambiguous.

The device session and sequence number give this example a way to identify repeat deliveries. That depends on the adapter preserving identity across retries. Assigning a new ID every time the collector receives the same reading would defeat the point.

None of these field names should be assumed to exist in an instrument's output. Some may come from the device, some from the collector, and others from the production system. That ownership needs to be agreed before the integration is built.

Missing data should not become zero

There are at least three different situations worth separating:

  • A valid reading that meets the process specification.
  • A valid reading that falls outside the process specification.
  • No usable reading at all.

The second is potentially the most important result on the screen. It should not be discarded as bad data just because it missed the target.

The third should not become a zero or quietly inherit the previous value.

Here is a small Python function for the unit-conversion part. It only converts readings explicitly marked valid in the proposed schema. Unknown or unsuccessful measurement states return None. Malformed readings that claim to be valid raise an error.

This runs with Python 3.10 or later and needs no external packages.

from math import isfinite

TO_NM = {"nm": 1.0, "um": 1000.0, "µm": 1000.0, "μm": 1000.0}


def thickness_nm(record: dict[str, object]) -> float | None:
    """Return a normalized value only for an explicitly valid reading."""
    if record.get("measurement_status") != "valid":
        return None

    reading = record.get("thickness")
    if not isinstance(reading, dict):
        raise ValueError("Missing thickness object")

    value, unit = reading.get("value"), reading.get("unit")
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise ValueError("Thickness must be a number")
    if not isinstance(unit, str) or unit not in TO_NM:
        raise ValueError(f"Unsupported thickness unit: {unit!r}")

    try:
        normalized = float(value) * TO_NM[unit]
    except OverflowError as exc:
        raise ValueError("Thickness is too large to represent") from exc
    if not isfinite(normalized) or normalized < 0:
        raise ValueError("Thickness must be finite and non-negative")
    return normalized


if __name__ == "__main__":
    samples = [
        {"measurement_status": "valid", "thickness": {"value": 1.2, "unit": "um"}},
        {"measurement_status": "no_signal", "thickness": None},
        {"measurement_status": "valid", "thickness": {"value": 1200, "unit": "nm"}},
    ]
    for sample in samples:
        print(thickness_nm(sample))
Enter fullscreen mode Exit fullscreen mode

The output is:

1200.0
None
1200.0
Enter fullscreen mode Exit fullscreen mode

The two valid readings normalize to the same value. The missing reading remains missing. I have also allowed both Unicode versions of the micro symbol, µ and μ, alongside ASCII um.

The isfinite check rejects NaN and infinity. Numeric strings and booleans are rejected too; this example expects a JSON number rather than trying to guess what the sender intended.

This is deliberately not a complete validator. It does not check timestamps, sampling positions or device calibration. Nor does it decide whether 1,200 nm is acceptable for a particular film. That requires a separate, approved specification.

The collector should retain the original record and its status or validation error. None is a value for the normalized output, not a reason to lose the explanation. A missing result should appear as a gap or an explicit unavailable state, rather than a fresh-looking measurement.

There are two times to keep track of

measured_at and received_at answer different questions.

The first belongs to the observation. The second records when the receiving application got it. In the synthetic record above, they are 120 milliseconds apart.

That difference is only meaningful as an age or delay estimate when the clock sources are understood and sufficiently synchronized. Local processing time may also sit between acquisition and delivery, so I would not automatically label it “network latency.”

If the device does not expose an acquisition timestamp, keep it unknown. The collector can still record arrival time, but it should not pretend that arrival time is when the film was measured.

Freshness also needs its own rule. A reading can have been valid when acquired and still be too old to describe the current process. For a first dashboard, I would show the time of the last received record and a stale-data indicator, with the threshold agreed for that process.

A mapping application also needs measurement coordinates and their reference frame. A sequence of timestamps is not a wafer map, and a single measurement location does not establish uniformity across a whole sheet.

Keep an untouched path through the system

The first version of this integration could be fairly small:

Device adapter
    -> raw record log
    -> validation and unit conversion
    -> measurement store
    -> read-only dashboard
Enter fullscreen mode Exit fullscreen mode

The raw log comes before conversion so that a rejected record is still available for investigation. The adapter's job is to translate the documented device interface; it should not silently invent units or a successful status when those are missing.

Testing can start with synthetic records, before attaching hardware. The useful cases are not just a neat stream of valid values. Try an unsupported unit, a missing result, a duplicate delivery, a delayed record and a device restart. Check what is stored and what the dashboard actually shows in each case.

If averaging is added later, keep the original readings and identify the averaging window on the display. Mixing records from different runs or measurement recipes into one average would make a tidy chart harder to interpret.

Machine control is outside the scope of this version. Displaying measurements and deciding how a coater should respond are different responsibilities. A read-only path lets the team check units, timing and interpretation without also changing the process.

Once those checks work, there is a much better basis for adding charts, alerts or a connection to a manufacturing execution system. Until then, I would rather have a plain table with a trustworthy status and timestamp than a polished trend line that keeps moving when the data has stopped.

Top comments (0)