--- myst: html_meta: description: "Design an upload script for a fleet: one asset per vehicle, one dataset per data source and one run per test." --- # Data integration tutorial {.lead} Plan a script that uploads every test's telemetry, MCAP recordings, video and logs for a fleet of vehicles, and marks each test as a run. It applies the [asset](/guides/assets.md), [dataset](/guides/datasets/overview.md) and [run](/guides/runs.md) guides to one example. ```{include} /guides/_snippets/connect-the-client.md ``` ## The example A company flies two models of electric glider, `shimmer` and `sidewinder`, with serial numbers `SN-001` to `SN-999`. After each test flight, a glider has: - telemetry as CSV and in a proprietary binary format, - an MCAP recording with telemetry and a camera, - MP4 video from other cameras, and - logs from `journald`. The script runs after every flight, so each step finds what already exists rather than creating it again. ## One asset per glider Properties that identify the physical glider let the script find its asset every time: ```python asset = client.get_or_create_asset_by_properties( {"platform": "shimmer", "serial_num": "SN-001"}, name="Shimmer SN-001", ) ``` Choose properties your organization already uses, such as a model and tail number. ## One dataset per data source Each data source on the glider gets a dataset under a data scope name that's the same on every glider, such as `flight_data` or `front_camera`. {py:meth}`~nominal.core.Asset.get_or_create_dataset` returns the existing dataset after the first flight, so each upload adds to it: ```python telemetry = asset.get_or_create_dataset("flight_data", prefix_tree_delimiter=".") mcap = asset.get_or_create_dataset("mcap_data") front_camera = asset.get_or_create_dataset("front_camera") logs = asset.get_or_create_dataset("flight_controller_logs") ``` Matching data scope names let one workbook template or checklist work on every glider. ## Convert each format Convert anything Nominal can't read before uploading it: - **Tabular data:** write CSV or Parquet with one timestamp column, as in [CSV and Parquet](/guides/datasets/csv.md). Parquet files are smaller and ingest faster. Flatten nested values into dotted column names, such as `gps.lat`, and the `prefix_tree_delimiter` above groups them in the UI. For a proprietary format you upload often, a [file extractor](/guides/extractors/overview.md) can convert it during ingest. - **MCAP:** no conversion needed for protobuf, ROS and ROS 2 messages, or H.264 and H.265 video. - **Video:** convert to H.264 with [`normalize_video`](/guides/video/video-ingest.md#convert-the-video). - **Logs:** export them with `journalctl --output json`, as in [Journal JSON](/guides/datasets/journal-json.md). Programs can log to `journald` with libraries such as [systemd-python](https://pypi.org/project/systemd-python/) for Python, [spdlog](https://github.com/gabime/spdlog) for C++ and [systemd-journal-logger](https://docs.rs/systemd-journal-logger/) for Rust. Splitting data across many files is fine. Every file uploaded to a dataset adds to the same channels, and new columns add new channels. Uploading the same channel and timestamp twice overwrites the earlier value. ## Upload the flight ```python from datetime import datetime, timezone from pathlib import Path for path in sorted(Path("flight_042").glob("*.parquet")): telemetry.add_tabular_data(path, timestamp_column="unix_time", timestamp_type="epoch_seconds") mcap.add_mcap("flight_042/recording.mcap") mcap.add_mcap_video("flight_042/recording.mcap", channel="cockpit_cam", topic="video") front_camera.add_video( "flight_042/front.mp4", channel="front_camera", start=datetime(2026, 9, 10, 14, 38, 19, tzinfo=timezone.utc), ) logs.add_journal_json("flight_042/controller.jsonl") ``` To upload many files as one tracked batch, use an [ingest job](/guides/ingest/ingestion-jobs.md) instead. For ArduPilot dataflash `.bin` files, use {py:meth}`~nominal.core.Dataset.add_ardupilot_dataflash`. ## One run per flight An asset collects every flight. A run selects one: ```python run = asset.create_run( "Flight 042: unpowered glide", start=datetime(2026, 9, 10, 14, 30, tzinfo=timezone.utc), end=datetime(2026, 9, 10, 15, 30, tzinfo=timezone.utc), properties={"flight": "042"}, ) ``` The run's bounds decide what workbooks and checklists on it see. If you only know them once the data is read, create the run with `end=None` first and set the bounds later with {py:meth}`~nominal.core.Run.update`. From here, create [workbooks](/guides/workbooks.md) from templates and run [data reviews](/guides/checklists/data-review.md) on each flight's run.