<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: UNITED UAV Official</title>
    <description>The latest articles on DEV Community by UNITED UAV Official (@united_uav).</description>
    <link>https://dev.to/united_uav</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4098805%2F139c6fc1-2032-4e3f-b48d-f5dce516f55e.png</url>
      <title>DEV Community: UNITED UAV Official</title>
      <link>https://dev.to/united_uav</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/united_uav"/>
    <language>en</language>
    <item>
      <title>Designing a Dual-Sensor Data Contract for a Thermal VTOL Pipeline</title>
      <dc:creator>UNITED UAV Official</dc:creator>
      <pubDate>Fri, 28 Aug 2026 11:30:37 +0000</pubDate>
      <link>https://dev.to/united_uav/designing-a-dual-sensor-data-contract-for-a-thermal-vtol-pipeline-2am5</link>
      <guid>https://dev.to/united_uav/designing-a-dual-sensor-data-contract-for-a-thermal-vtol-pipeline-2am5</guid>
      <description>&lt;p&gt;A dual-sensor UAV can produce visible video, thermal frames, aircraft position, attitude, gimbal angles, target detections, and operator events at different rates. The difficult part is not collecting every stream. It is proving which observations belong together without inventing precision that the source system never provided.&lt;/p&gt;

&lt;p&gt;This article develops a data contract for that problem. It uses the &lt;a href="https://store.uniteduav.com/products/uvh2-dual-sensor-fixed-wing-vtol-drone?utm_source=dev.to&amp;amp;utm_medium=community&amp;amp;utm_campaign=dev_uvh2_data_contract_20260828"&gt;UNITED UAV UVH2 dual-sensor fixed-wing VTOL drone&lt;/a&gt; as a concrete product context. The current listing describes a K8T-V2 payload with an 8 MP visible channel, 4K visible recording, a native 640 × 512 thermal detector, processed 1280 × 1024 thermal super-resolution, and Ethernet, serial, and SBUS interfaces. It also identifies the communication protocol as proprietary.&lt;/p&gt;

&lt;p&gt;That last detail defines the software boundary: obtain the supported interface-control document and use an approved adapter. The schemas and code below describe a downstream observation pipeline. They are not the UVH2 command protocol, do not reverse-engineer it, and must not be placed in a flight-critical control path.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Commercial disclosure:&lt;/strong&gt; UNITED UAV Official publishes this technical article and links to a product sold through the UNITED UAV store.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why a filename pair is not a data contract
&lt;/h2&gt;

&lt;p&gt;A common first implementation writes &lt;code&gt;visible_001.jpg&lt;/code&gt; and &lt;code&gt;thermal_001.jpg&lt;/code&gt;, then assumes the matching number proves that the files were captured together. That assumption breaks when one encoder drops a frame, a camera restarts, a buffer flushes late, or a transmission path reorders messages.&lt;/p&gt;

&lt;p&gt;The pipeline needs to preserve three different facts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;what each source reported;&lt;/li&gt;
&lt;li&gt;how the integration correlated the sources;&lt;/li&gt;
&lt;li&gt;how confident the integration is in that correlation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those facts should remain visible to every downstream consumer. An inspection model may accept a 200 ms pairing tolerance. A moving-target workflow may require a much tighter limit. A reviewer should never have to infer that tolerance from filenames or application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define an immutable capture bundle
&lt;/h2&gt;

&lt;p&gt;Treat every correlated observation as an immutable bundle. Large media files stay in object storage; the event carries their checksums, dimensions, formats, timestamps, and references.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schema_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"capture_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01J6FGP8B0V5TGQ1M9A2X4K7CZ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vehicle_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvh2-field-07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mission_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"roof-baseline-2026-08-28-r3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"observed_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-28T06:18:42.481Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_clock_ns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5842284190031&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"clock_epoch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payload-boot-0194"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"visible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"object_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://uav-raw/2026/08/28/visible/01J6F.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3840&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2160&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"encoding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source_sequence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18422&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"thermal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"object_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://uav-raw/2026/08/28/thermal/01J6F.bin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;640&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"representation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"native_detector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source_sequence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9211&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"correlation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"source_clock_nearest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"delta_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;18.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_allowed_delta_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"quality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accepted"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"position_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aircraft_telemetry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"attitude_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aircraft_telemetry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gimbal_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payload_telemetry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sample_delta_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;11.2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"adapter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.3.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"interface_document_revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vendor-approved-revision"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example deliberately avoids claiming that every payload exposes the same fields. The adapter should record &lt;code&gt;null&lt;/code&gt; plus a reason when the supported interface does not provide a value. Fabricated defaults are more dangerous than missing data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use two clocks, not one
&lt;/h2&gt;

&lt;p&gt;Wall-clock time answers when an observation occurred in a shared timeline. A monotonic source clock answers how events were ordered inside one boot session. Both are necessary.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;observed_at&lt;/code&gt; should use UTC with an explicit offset. &lt;code&gt;source_clock_ns&lt;/code&gt; should never move backward during one &lt;code&gt;clock_epoch&lt;/code&gt;. A payload restart creates a new epoch even if its sequence counter returns to zero. The ingestion service can then distinguish a reboot from delayed delivery.&lt;/p&gt;

&lt;p&gt;Store clock-quality metadata separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;synchronization source, such as GNSS, PTP, NTP, or platform-provided time;&lt;/li&gt;
&lt;li&gt;last successful synchronization time;&lt;/li&gt;
&lt;li&gt;estimated offset and uncertainty;&lt;/li&gt;
&lt;li&gt;detection of an unexpected time step;&lt;/li&gt;
&lt;li&gt;adapter receive time and storage time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A precise-looking timestamp is not proof of accurate synchronization. If uncertainty is unknown, say so and lower the correlation quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep native and derived thermal imagery distinct
&lt;/h2&gt;

&lt;p&gt;The UVH2 listing separates the native 640 × 512 detector from 1280 × 1024 thermal super-resolution output. A data model should preserve that distinction. Upscaled or enhanced output is useful, but it is not a higher-resolution detector measurement.&lt;/p&gt;

&lt;p&gt;Use an explicit representation field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;native_detector
processed_super_resolution
colorized_preview
visible_thermal_overlay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Derived assets should reference their parents and record the processing version, parameters, and checksum. Never overwrite the native input with an enhanced image. If a later model produces a different result, both versions should remain reproducible.&lt;/p&gt;

&lt;p&gt;Thermal observation also does not automatically mean calibrated temperature measurement. If radiometric values are required, the contract needs documented units, calibration state, emissivity handling, reflected-temperature assumptions, measurement range, accuracy, and invalid-pixel behavior. When the source interface does not provide those fields, the pipeline should label the asset as qualitative thermal imagery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make partial pairs first-class events
&lt;/h2&gt;

&lt;p&gt;A dual-sensor pipeline must tolerate one channel arriving late or not arriving at all. Use an explicit lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;received_visible_only
received_thermal_only
paired_pending_validation
paired_accepted
paired_rejected
expired_unpaired
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not keep an event in memory forever while waiting for its partner. Persist the first observation, start a bounded correlation window, and finalize it as &lt;code&gt;expired_unpaired&lt;/code&gt; when the deadline passes. A late partner can create a new reconciliation record without rewriting history.&lt;/p&gt;

&lt;p&gt;The difference matters operationally. A missing visible frame may still leave a useful thermal observation. A missing thermal frame may still preserve visual context. Downstream systems can decide whether a partial bundle is usable, but they should not be told that a complete pair exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate at ingestion
&lt;/h2&gt;

&lt;p&gt;Validation should reject malformed events before analytics or indexing. A dependency-free Python example can enforce some basic invariants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;


&lt;span class="n"&gt;ALLOWED_THERMAL_REPRESENTATIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;native_detector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processed_super_resolution&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;colorized_preview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visible_thermal_overlay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_utc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observed_at requires a timezone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_bundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsupported schema_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capture_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vehicle_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mission_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clock_epoch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;parse_utc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observed_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;correlation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correlation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;correlation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;correlation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_allowed_delta_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid correlation timing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;correlation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quality&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted pair exceeds timing limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;thermal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thermal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thermal&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;thermal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;representation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ALLOWED_THERMAL_REPRESENTATIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown thermal representation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visible&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thermal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;asset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; checksum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production validation should use a versioned schema format and bounded payload sizes. Keep rejected metadata in a quarantine stream with reason codes; do not silently discard it or mix it with accepted observations. Media decoders need their own resource limits because image dimensions in metadata cannot be trusted until the bytes are inspected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make idempotency content-aware
&lt;/h2&gt;

&lt;p&gt;Networks retry. Edge computers reboot. Upload workers receive the same message twice. A stable event identity prevents those normal behaviors from multiplying observations.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;capture_id&lt;/code&gt; as the business identity and verify that any repeat has the same immutable digest. The outcomes should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same ID, same digest: harmless retry;&lt;/li&gt;
&lt;li&gt;same ID, different digest: conflict requiring investigation;&lt;/li&gt;
&lt;li&gt;new ID, same source sequences and clock epoch: probable duplicate from an upstream identity defect;&lt;/li&gt;
&lt;li&gt;new ID, same media checksums: duplicate asset or repeated capture requiring a documented decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write the event and its asset references atomically, or keep an explicit state such as &lt;code&gt;metadata_committed_assets_pending&lt;/code&gt;. A database row that points to missing media is not a complete ingestion result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate observation from inference
&lt;/h2&gt;

&lt;p&gt;Target recognition, tracking, anomaly scoring, mapping, and thermography interpretation are downstream results. Store them as derived records that point back to the immutable capture bundle. Each result should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;model or algorithm name and version;&lt;/li&gt;
&lt;li&gt;model-file checksum;&lt;/li&gt;
&lt;li&gt;input capture ID and asset representation;&lt;/li&gt;
&lt;li&gt;thresholds and preprocessing version;&lt;/li&gt;
&lt;li&gt;output coordinate system;&lt;/li&gt;
&lt;li&gt;confidence and known limitations;&lt;/li&gt;
&lt;li&gt;reviewer state when a human decision is required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not write a detection back into the source event as if the camera observed a semantic truth. The camera produced pixels and metadata; an algorithm produced an interpretation. That separation makes reprocessing, disagreement analysis, and incident review possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect operational and personal data
&lt;/h2&gt;

&lt;p&gt;Visible and thermal UAV data may reveal people, vehicles, infrastructure, precise locations, schedules, and operational patterns. Minimize collection to the approved mission, encrypt data in transit and at rest, use short-lived scoped credentials, and log access to original media.&lt;/p&gt;

&lt;p&gt;Keep raw media separate from public application paths. Derived previews should remove unnecessary metadata and use redaction when the workflow requires it. Retention policy should apply to raw files, thumbnails, detections, exports, backups, and cached edge copies—not only to the primary database.&lt;/p&gt;

&lt;p&gt;The adapter that reads the supported payload interface should have no broader command permission than it needs. Observation ingestion must not become an undocumented route to aircraft or gimbal control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failure modes
&lt;/h2&gt;

&lt;p&gt;Before relying on the pipeline, replay at least these scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;visible frames arrive before, after, and without thermal frames;&lt;/li&gt;
&lt;li&gt;sequence counters restart after a payload reboot;&lt;/li&gt;
&lt;li&gt;wall time jumps while the monotonic clock continues;&lt;/li&gt;
&lt;li&gt;a processed thermal asset is incorrectly labelled as native;&lt;/li&gt;
&lt;li&gt;the same capture ID arrives with a different checksum;&lt;/li&gt;
&lt;li&gt;object storage succeeds but metadata commit fails, and vice versa;&lt;/li&gt;
&lt;li&gt;the adapter version changes during a mission;&lt;/li&gt;
&lt;li&gt;media is truncated, oversized, or declares false dimensions;&lt;/li&gt;
&lt;li&gt;telemetry and gimbal samples are older than the configured limit;&lt;/li&gt;
&lt;li&gt;a late event arrives after the correlation window expires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each test, define the expected event state and evidence before running it. A pipeline that merely avoids crashing is not necessarily correct. It should expose why a bundle was accepted, rejected, left partial, or quarantined.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical readiness checklist
&lt;/h2&gt;

&lt;p&gt;Before production use, confirm that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the delivered payload and approved interface document match the adapter version;&lt;/li&gt;
&lt;li&gt;every boot session has a distinct clock epoch;&lt;/li&gt;
&lt;li&gt;wall-clock accuracy and monotonic ordering are recorded separately;&lt;/li&gt;
&lt;li&gt;pairing tolerance is explicit and mission-appropriate;&lt;/li&gt;
&lt;li&gt;native and processed thermal representations cannot be confused;&lt;/li&gt;
&lt;li&gt;partial and late observations have bounded, auditable states;&lt;/li&gt;
&lt;li&gt;media bytes are verified independently of metadata;&lt;/li&gt;
&lt;li&gt;retries are idempotent and digest conflicts stop processing;&lt;/li&gt;
&lt;li&gt;inference outputs never overwrite source observations;&lt;/li&gt;
&lt;li&gt;retention, redaction, encryption, and access logging cover edge and cloud copies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A strong dual-sensor pipeline does not hide uncertainty. It turns timing, derivation, missing data, and interface revision into fields that can be tested. That makes the resulting data more useful to developers and more defensible to operators and reviewers.&lt;/p&gt;

&lt;p&gt;Review the current &lt;a href="https://store.uniteduav.com/products/uvh2-dual-sensor-fixed-wing-vtol-drone?utm_source=dev.to&amp;amp;utm_medium=community&amp;amp;utm_campaign=dev_uvh2_data_contract_20260828"&gt;UVH2 product page&lt;/a&gt; for the published platform and payload starting point. For a real integration, request the current K8T-V2 interface-control document, supported command and telemetry definitions, connector details, licensing terms, sample files, and a configuration-specific acceptance procedure.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Designing a Fail-Closed API Integration for an Autonomous Drone Dock</title>
      <dc:creator>UNITED UAV Official</dc:creator>
      <pubDate>Fri, 28 Aug 2026 11:23:30 +0000</pubDate>
      <link>https://dev.to/united_uav/designing-a-fail-closed-api-integration-for-an-autonomous-drone-dock-4jdj</link>
      <guid>https://dev.to/united_uav/designing-a-fail-closed-api-integration-for-an-autonomous-drone-dock-4jdj</guid>
      <description>&lt;p&gt;Autonomous drone docks sit at an awkward boundary: they look like IoT devices, but a command can eventually affect a real aircraft. That makes a conventional "send request and retry on failure" integration unsafe. The software must distinguish observation from authorization, reject stale or ambiguous commands, and fail closed whenever state cannot be proven.&lt;/p&gt;

&lt;p&gt;This article presents a reference architecture for that integration problem. It uses the &lt;a href="https://store.uniteduav.com/products/docking-station-uk03?utm_source=dev.to&amp;amp;utm_medium=community&amp;amp;utm_campaign=dev_uk03_api_20260828"&gt;UNITED UAV UK03 drone docking station&lt;/a&gt; as a concrete product context because its current listing includes API availability, RTK, internal and external cameras, weather sensing, optional edge computing, optional mesh networking, and optional SIM-based 4G. The listing does not publish an endpoint schema, so every endpoint and payload below is deliberately illustrative. A production implementation must use the current vendor documentation and the approved aircraft-management workflow.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Commercial disclosure:&lt;/strong&gt; UNITED UAV Official publishes this technical article and links to a product sold through the UNITED UAV store.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Start by separating four kinds of state
&lt;/h2&gt;

&lt;p&gt;Many integrations become dangerous because they compress several questions into one boolean such as &lt;code&gt;ready: true&lt;/code&gt;. A safer model records at least four independent states:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Observed state&lt;/strong&gt; — what the dock, aircraft, weather sensors, and network last reported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validated state&lt;/strong&gt; — whether those observations are recent, internally consistent, and within the operating envelope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorized intent&lt;/strong&gt; — which mission was approved, by whom, for which aircraft, site, route, and time window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executed state&lt;/strong&gt; — what the vendor platform accepted and what later telemetry proves actually happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A positive observation is not authorization. An accepted API response is not proof of aircraft motion. A dashboard that has stopped updating is not evidence that conditions remain unchanged. Keeping these states separate makes the system easier to audit and prevents a stale green status from becoming a launch decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put an adapter in front of the vendor API
&lt;/h2&gt;

&lt;p&gt;Do not let schedulers, dashboards, or business applications call the dock API directly. Put a narrow adapter between them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Planner -&amp;gt; Intent Store -&amp;gt; Safety Gate -&amp;gt; Dock Adapter -&amp;gt; Vendor Platform
              |               |              |
              v               v              v
          Audit Log      State Snapshot   Readback Worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter should expose only the small set of operations the organization has approved. It can normalize vendor-specific status codes, enforce request IDs, apply timeouts, and redact secrets from logs. If the vendor API changes, only the adapter should need to understand the new schema.&lt;/p&gt;

&lt;p&gt;This boundary is also where command and telemetry permissions can be separated. A monitoring service may need read-only access to weather, dock health, charging state, and alarms. It should not automatically inherit permission to open the dock, schedule a route, or dispatch an aircraft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make every command an expiring intent
&lt;/h2&gt;

&lt;p&gt;A mission request should be an immutable record, not a transient button click. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"intent_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01J6E7Y9M2K8Q4R5T6V7W8X9ZA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"site_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"solar-north-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aircraft_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uis220-07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mission_revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"approved_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ops-review-queue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"not_before"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-28T02:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-28T02:10:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required_state_revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;intent_id&lt;/code&gt; becomes the idempotency key. &lt;code&gt;mission_revision&lt;/code&gt; prevents an older route from being dispatched after an edit. &lt;code&gt;expires_at&lt;/code&gt; prevents a delayed queue message from starting a mission outside its approval window. &lt;code&gt;required_state_revision&lt;/code&gt; binds the decision to a particular validated snapshot instead of whatever state happens to exist later.&lt;/p&gt;

&lt;p&gt;If the vendor API supports an idempotency header, pass the intent ID through. If it does not, the adapter must keep its own durable command ledger and query vendor state before deciding whether a retry is safe. Never generate a new intent ID merely to make an uncertain request succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate freshness before values
&lt;/h2&gt;

&lt;p&gt;A weather value can be numerically safe and operationally useless if it is old. Every safety-relevant observation should carry three timestamps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when the sensor measured it;&lt;/li&gt;
&lt;li&gt;when the platform received it;&lt;/li&gt;
&lt;li&gt;when the integration read it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The safety gate should reject missing timestamps, future timestamps beyond an allowed clock-skew window, and observations older than the system's documented freshness limit. Apply the same rule to dock-door state, aircraft-in-place detection, charging state, RTK status, network health, and camera availability.&lt;/p&gt;

&lt;p&gt;A compact TypeScript-style check might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;GateResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;snapshotRevision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;GateResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nowMs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observedAtMs&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;snapshot_stale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aircraftInPlace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aircraft_not_confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeAlarmCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active_alarm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataQualityOk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weather_quality_unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commandPathHealthy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;command_path_unhealthy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maintenanceLock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maintenance_lock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;snapshotRevision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example intentionally does not hard-code wind, temperature, rainfall, or battery thresholds. Those limits must come from the most restrictive verified value across the aircraft, payload, dock configuration, route, regulation, and operator procedure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a command state machine
&lt;/h2&gt;

&lt;p&gt;A boolean &lt;code&gt;sent&lt;/code&gt; field cannot represent an external command safely. Use explicit states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prepared
  -&amp;gt; rejected_prewrite
  -&amp;gt; dispatch_requested
  -&amp;gt; accepted_unverified
  -&amp;gt; verified
  -&amp;gt; outcome_uncertain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rejected_prewrite&lt;/code&gt; means no external write occurred. It is safe to prepare a new intent after correcting the reason. &lt;code&gt;accepted_unverified&lt;/code&gt; means the vendor acknowledged the request, but independent state has not yet proved the result. &lt;code&gt;outcome_uncertain&lt;/code&gt; means a timeout or connection loss happened after transmission may have begun. That state must trigger readback, not a blind retry.&lt;/p&gt;

&lt;p&gt;Persist the transition before and after the external call. The log should contain the intent ID, payload digest, vendor request correlation ID when available, response status, timestamps, adapter version, state revision, and redacted error class. It should never contain API keys, session tokens, or complete signed URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read back the effect independently
&lt;/h2&gt;

&lt;p&gt;After a command is accepted, query the authoritative state surface. For a scheduled mission, verify the mission identifier, route revision, aircraft, dock, planned time, and enabled status. For a door or charging operation, verify the resulting hardware state and its observation timestamp.&lt;/p&gt;

&lt;p&gt;Use a different read path when the platform provides one. For example, a command endpoint may acknowledge queueing while a mission-status endpoint reports execution. Telemetry can then confirm physical progress. These layers answer different questions and should not be treated as duplicates.&lt;/p&gt;

&lt;p&gt;A successful readback should be narrow and deterministic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markAccepted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readMission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;missionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intentId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mission_revision&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aircraftId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aircraft_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markVerified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;finalState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any assertion fails, preserve both records and escalate. Do not overwrite the mismatch with the newest response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep humans at the right boundary
&lt;/h2&gt;

&lt;p&gt;Human approval should apply to a complete, reviewable mission intent. The reviewer should see the route revision, aircraft and payload, launch window, forecast and live weather, site status, airspace or operating approval, expected communications path, and defined abort behavior.&lt;/p&gt;

&lt;p&gt;The integration may automate collection, validation, expiry, and readback. It should not silently broaden an approval when a route, aircraft, payload, time window, safety limit, or site changes. Those changes create a new revision and may require a new approval.&lt;/p&gt;

&lt;p&gt;This distinction also helps during incidents. Operators can determine whether a bad outcome came from incorrect source data, a validation defect, an authorization error, an adapter failure, vendor-platform behavior, or a physical-system fault.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design network loss as a normal state
&lt;/h2&gt;

&lt;p&gt;The UK03 listing describes optional 4G and mesh capabilities, but multiple bearers do not make a network infallible. Define behavior for at least these cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the adapter cannot reach the vendor platform before dispatch;&lt;/li&gt;
&lt;li&gt;the request times out after bytes may have been transmitted;&lt;/li&gt;
&lt;li&gt;telemetry continues but the command channel is unavailable;&lt;/li&gt;
&lt;li&gt;the dock is reachable but the aircraft link is degraded;&lt;/li&gt;
&lt;li&gt;the WAN fails during a mission;&lt;/li&gt;
&lt;li&gt;buffered events arrive out of order after reconnection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each case, document which component owns the safe response. Flight-critical failsafes belong in the validated aircraft and approved control system, not in a remote web service. The external integration should observe and report those states without pretending it can replace them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test faults before unattended operation
&lt;/h2&gt;

&lt;p&gt;A staging environment is useful, but it is not enough. The production configuration should go through controlled acceptance tests with the real dock, supported aircraft, network path, and management platform. Include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate command delivery with the same intent ID;&lt;/li&gt;
&lt;li&gt;an expired intent arriving late;&lt;/li&gt;
&lt;li&gt;a route revision changing after approval;&lt;/li&gt;
&lt;li&gt;stale weather and dock-status observations;&lt;/li&gt;
&lt;li&gt;API timeout before and after request transmission;&lt;/li&gt;
&lt;li&gt;loss and recovery of WAN, RTK corrections, and telemetry;&lt;/li&gt;
&lt;li&gt;an aircraft not correctly detected in the dock;&lt;/li&gt;
&lt;li&gt;an active maintenance lock or unresolved alarm;&lt;/li&gt;
&lt;li&gt;a readback that disagrees with the accepted command;&lt;/li&gt;
&lt;li&gt;adapter restart during every state-machine transition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Record the expected safe state and pass criterion before each test. A fault-injection exercise is successful when the system prevents or contains the unsafe action and preserves evidence, not merely when it emits an error message.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical production checklist
&lt;/h2&gt;

&lt;p&gt;Before enabling an autonomous dock integration, confirm that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vendor API documentation matches the delivered hardware, licenses, and software version;&lt;/li&gt;
&lt;li&gt;read-only and command credentials are separated and can be rotated;&lt;/li&gt;
&lt;li&gt;every intent is immutable, versioned, expiring, and idempotent;&lt;/li&gt;
&lt;li&gt;observations include freshness and data-quality checks;&lt;/li&gt;
&lt;li&gt;command outcomes have explicit uncertain and verified states;&lt;/li&gt;
&lt;li&gt;retries require readback after any ambiguous transmission;&lt;/li&gt;
&lt;li&gt;logs are durable, redacted, time-synchronized, and reviewable;&lt;/li&gt;
&lt;li&gt;human approval covers the exact mission revision and operating window;&lt;/li&gt;
&lt;li&gt;aircraft and dock failsafes work without the external automation;&lt;/li&gt;
&lt;li&gt;controlled fault tests have been completed and defects closed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central design principle is simple: an automation system should only advance when it can prove that the required state, authorization, and prior outcome all match. Anything less becomes a stop condition. That approach may produce more visible refusals during development, but it also makes the integration understandable, testable, and safer to operate.&lt;/p&gt;

&lt;p&gt;Review the current &lt;a href="https://store.uniteduav.com/products/docking-station-uk03?utm_source=dev.to&amp;amp;utm_medium=community&amp;amp;utm_campaign=dev_uk03_api_20260828"&gt;UK03 product listing&lt;/a&gt; for the published configuration starting point, then request the applicable API documentation, supported-aircraft matrix, licensing details, and acceptance procedure for the actual deployment.&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>iot</category>
      <category>robotics</category>
    </item>
  </channel>
</rss>
