DEV Community

Cover image for Portable PL Test Equipment: Keeping Sample IDs Attached to the Data
ches
ches

Posted on

Portable PL Test Equipment: Keeping Sample IDs Attached to the Data

A folder with three files called reading_01.txt, reading_02.txt and reading_03.txt looks tidy enough.

Now someone asks which one came from sample PV-017, whether it was taken at the center or the edge, and which settings were active.

The filenames don't answer any of that.

That is the small software problem I want to look at here. When portable test equipment is used across several samples, the saved result needs an unambiguous link back to the sample and the measurement setup.

I work on technical content for TDM Technology. The hardware reference for this example is its SE10 portable PL metrology instrument, which lists local storage and a Bluetooth connection to a Windows PC.

That product description is not an API specification. It does not establish a file-export format or a public Bluetooth protocol. The example below starts with a completed file already on the computer; obtaining suitable files from the actual instrument software must be confirmed separately. There are no SE10 SDK calls here.

Start with the sample, not the filename

For this example, each local file gets a small JSON record beside it. This is often called a sidecar file. It holds the context without changing the original data.

I would begin with four references: the sample, the measurement position, the instrument and the setup.

PV-017 identifies the sample. P03 identifies a position on a photo or drawing kept with the session. portable-demo-01 identifies the instrument. setup-v3 points to a preserved settings record or notebook entry.

Those references need somewhere to point. A setup name is not much use if the settings behind it have been overwritten. Likewise, a position label needs a definition that another person can follow.

The code only records these labels. It cannot verify that the operator selected the right sample or used the stated settings. For a real workflow, I would make that confirmation part of the acquisition process, rather than reconstructing it from memory at the end of the day.

A small Python sidecar writer

This script reads a file, calculates its SHA-256 digest and writes the references into a neighboring JSON file. It leaves the input untouched and refuses to overwrite an existing sidecar.

Save it as pl_record.py. It uses the Python standard library and can be run with Python 3.10 or later.

"""Attach sample metadata to a completed local file; no device connection."""


import argparse
import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4


def add_metadata(
    path: Path, *, sample: str, position: str, setup: str, instrument: str
) -> Path:
    refs = {
        "sample_id": sample.strip(),
        "position_ref": position.strip(),
        "setup_ref": setup.strip(),
        "instrument_ref": instrument.strip(),
    }
    if not all(refs.values()):
        raise ValueError("Sample, position, setup and instrument are required.")

    digest = hashlib.sha256()
    with path.open("rb") as source:
        for chunk in iter(lambda: source.read(1024 * 1024), b""):
            digest.update(chunk)

    record = {
        "schema_version": 1,
        "record_id": str(uuid4()),
        **refs,
        "logged_at_utc": datetime.now(timezone.utc).isoformat(),
        "file_name": path.name,
        "file_sha256": digest.hexdigest(),
    }
    destination = path.with_name(path.name + ".metadata.json")
    payload = json.dumps(record, indent=2, ensure_ascii=False) + "\n"
    with destination.open("x", encoding="utf-8") as output:
        output.write(payload)
    return destination


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("file", type=Path)
    parser.add_argument("--sample", required=True)
    parser.add_argument("--position", required=True)
    parser.add_argument("--setup", required=True)
    parser.add_argument("--instrument", required=True)
    args = parser.parse_args()
    try:
        print(add_metadata(
            args.file, sample=args.sample, position=args.position,
            setup=args.setup, instrument=args.instrument,
        ))
    except (OSError, ValueError) as exc:
        parser.exit(2, f"Error: {exc}\n")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

To try it without an instrument, create a file named demo_reading.txt containing any clearly labeled synthetic text. Then run:

python pl_record.py demo_reading.txt --sample PV-017 --position P03 --setup setup-v3 --instrument portable-demo-01

The output is:

demo_reading.txt.metadata.json

There are now two files to keep together:

demo_reading.txt
demo_reading.txt.metadata.json
Enter fullscreen mode Exit fullscreen mode

The script does not interpret the file as a spectrum or assume any particular PL data format. The .txt extension is just for this demonstration.

A missing file or blank reference produces an error. Running the same command again also produces an error, because Python's exclusive-create mode, "x", will not replace the existing metadata file.

That protects against an accidental overwrite by this script. It does not make the record immutable or protect it against someone editing it with another program.

Two fields that are easy to misread

The timestamp is called logged_at_utc, deliberately. It records when this script created the record, using a timezone-aware UTC datetime. It is not the acquisition time.

If the instrument supplies a trustworthy measurement timestamp, preserve it separately, together with the clock's timezone or reference. A file's modification time or the moment it arrives on a laptop should not silently become the time of measurement.

The second field is file_sha256. Python's hashlib computes the digest from the file's bytes. Saving it gives a later check something to compare against when looking for file changes.

It does not prove that a measurement is correct, identify a physical sample, or establish that two identical files came from the same acquisition. Two separate acquisitions might produce identical file contents. Do not automatically throw one away as a duplicate measurement just because its hash matches.

Each sidecar therefore gets its own record_id. Even that identifies this logging record, not a device-generated acquisition. A future instrument adapter would need to preserve the device's acquisition identity separately, if available.

Keep the first version small—and be clear about its limits

Run this only on completed files that are no longer being written. The script does not detect transfer completion, lock the input or recover a half-written sidecar after an interrupted write. It is a local example, not a multi-user lab database.

The next useful step would be a simple operator screen: select the sample and position, confirm the setup, attach the completed file, and review the record before moving to the next sample. The screen should make the active sample obvious. Convenience is not worth quietly carrying the previous sample ID into the next measurement.

For portable PL test equipment, that handover between the physical sample and the software record deserves attention. Wireless connectivity can be part of the hardware workflow; it does not tell an application what an operator is measuring.

The acceptance test I have in mind is modest. Hand the session folder, its setup records and its position drawing to a colleague. Can they work out which file belongs to which sample without asking the person who collected it?

That would be a useful first result, before writing any plotting code.

Top comments (0)