<?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>From Mission Requirements to a UAV Motor Operating Point: An MN4010 Sizing Workflow</title>
      <dc:creator>UNITED UAV Official</dc:creator>
      <pubDate>Sun, 20 Sep 2026 15:18:24 +0000</pubDate>
      <link>https://dev.to/united_uav/from-mission-requirements-to-a-uav-motor-operating-point-an-mn4010-sizing-workflow-3pjn</link>
      <guid>https://dev.to/united_uav/from-mission-requirements-to-a-uav-motor-operating-point-an-mn4010-sizing-workflow-3pjn</guid>
      <description>&lt;p&gt;A motor is not selected when its maximum-thrust number exceeds the aircraft weight. It is selected only after the motor, propeller, voltage, ESC, battery, airframe, and control reserve have been evaluated at the operating points the mission will actually use.&lt;/p&gt;

&lt;p&gt;This article builds that evaluation around the randomly selected &lt;a href="https://store.uniteduav.com/products/mn4010-kv370" rel="noopener noreferrer"&gt;UNITED UAV T-MOTOR MN4010 KV370 Navigator motor&lt;/a&gt;. The useful part of this example is not the product name. It is the unusually complete set of &lt;a href="https://store.tmotor.com/product/mn4010-kv370-motor-navigator-type.html" rel="noopener noreferrer"&gt;manufacturer-published MN4010 test rows&lt;/a&gt;: several propellers, both 14.8 V and 22.2 V supplies, and current, power, thrust, RPM, and temperature at multiple command points. That is enough data to show how a mission requirement can become a traceable propulsion decision instead of a spreadsheet guess.&lt;/p&gt;

&lt;p&gt;No UNITED UAV flight test or calibrated thrust-stand test was performed for this article. All MN4010 measurements cited below are published manufacturer reference data. The aircraft and mission are an explicit engineering example, not a claim about a production vehicle. Before flight, reproduce the relevant points on the installed propulsion system and use limits approved for that aircraft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commercial disclosure:&lt;/strong&gt; UNITED UAV Official publishes this article and sells the linked motor. The workflow is vendor-neutral and is designed to expose, rather than hide, the assumptions behind a selection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Begin with a load-case contract
&lt;/h2&gt;

&lt;p&gt;Suppose a team is designing a six-rotor mapping aircraft with a maximum takeoff mass of 4.5 kg. Its nominal static hover allocation is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;4500 g / 6 = 750 g per rotor&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That value is not yet a motor requirement. The installed aircraft may have unequal arm loading, non-ideal inflow near the fuselage, propeller-to-propeller variation, battery voltage sag, and lower air density than the bench environment. For a first screening calculation, the team assigns a 13% installed-condition factor:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;750 g × 1.13 = 847.5 g per rotor&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We will round the screening hover target to 850 g. The factor is an example input, not a universal safety constant. A real project should derive it from mass-properties analysis, expected density altitude, installation measurements, manufacturing tolerance, and the applicable safety process.&lt;/p&gt;

&lt;p&gt;The same contract requires at least 1.6 times the corrected hover target at the published high-command point:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;850 g × 1.6 = 1360 g per rotor&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This second constraint matters. A combination can be extremely efficient at hover and still leave too little authority for climb, gust rejection, load redistribution, or control saturation. Conversely, a combination with impressive maximum thrust can waste energy during the long hover segment. A defensible choice has to pass both the normal operating point and the reserve condition.&lt;/p&gt;

&lt;p&gt;Write the contract as data before inspecting candidates. At minimum, include aircraft mass, lifting-rotor count, corrected hover target, required thrust ratio, maximum propeller diameter, allowed supply-voltage range, mission duration, ambient envelope, maximum permitted current at each electrical boundary, and the policy for interpolation. If a value is unknown, mark it unknown. Quietly replacing an unknown with an optimistic default is how a screening spreadsheet becomes an unsafe specification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the source rows, not just a marketing maximum
&lt;/h2&gt;

&lt;p&gt;The manufacturer's current page identifies the KV370 version as a 4–8S motor with a listed 137 g mass including its cable, 98 mΩ internal resistance, 0.8 A idle current at 10 V, and 450 W / 20 A figures labeled for 180 seconds. Its test table includes these representative KV370 observations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Supply&lt;/th&gt;
&lt;th&gt;Propeller&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Current&lt;/th&gt;
&lt;th&gt;Power&lt;/th&gt;
&lt;th&gt;Thrust&lt;/th&gt;
&lt;th&gt;RPM&lt;/th&gt;
&lt;th&gt;Listed temperature&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;14.8 V&lt;/td&gt;
&lt;td&gt;14×4.8 CF&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;5.4 A&lt;/td&gt;
&lt;td&gt;79.92 W&lt;/td&gt;
&lt;td&gt;810 g&lt;/td&gt;
&lt;td&gt;4,400&lt;/td&gt;
&lt;td&gt;44 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14.8 V&lt;/td&gt;
&lt;td&gt;14×4.8 CF&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;6.5 A&lt;/td&gt;
&lt;td&gt;96.20 W&lt;/td&gt;
&lt;td&gt;920 g&lt;/td&gt;
&lt;td&gt;4,700&lt;/td&gt;
&lt;td&gt;44 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14.8 V&lt;/td&gt;
&lt;td&gt;15×5 CF&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;5.1 A&lt;/td&gt;
&lt;td&gt;75.48 W&lt;/td&gt;
&lt;td&gt;820 g&lt;/td&gt;
&lt;td&gt;3,800&lt;/td&gt;
&lt;td&gt;44 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14.8 V&lt;/td&gt;
&lt;td&gt;15×5 CF&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;8.2 A&lt;/td&gt;
&lt;td&gt;121.36 W&lt;/td&gt;
&lt;td&gt;1,160 g&lt;/td&gt;
&lt;td&gt;4,450&lt;/td&gt;
&lt;td&gt;44 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14.8 V&lt;/td&gt;
&lt;td&gt;16×5.4 CF&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;td&gt;4.3 A&lt;/td&gt;
&lt;td&gt;63.64 W&lt;/td&gt;
&lt;td&gt;770 g&lt;/td&gt;
&lt;td&gt;3,300&lt;/td&gt;
&lt;td&gt;45 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14.8 V&lt;/td&gt;
&lt;td&gt;16×5.4 CF&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;6.1 A&lt;/td&gt;
&lt;td&gt;90.28 W&lt;/td&gt;
&lt;td&gt;970 g&lt;/td&gt;
&lt;td&gt;3,650&lt;/td&gt;
&lt;td&gt;45 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14.8 V&lt;/td&gt;
&lt;td&gt;16×5.4 CF&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;9.6 A&lt;/td&gt;
&lt;td&gt;142.08 W&lt;/td&gt;
&lt;td&gt;1,380 g&lt;/td&gt;
&lt;td&gt;4,300&lt;/td&gt;
&lt;td&gt;45 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22.2 V&lt;/td&gt;
&lt;td&gt;12×4 CF&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;5.0 A&lt;/td&gt;
&lt;td&gt;111.00 W&lt;/td&gt;
&lt;td&gt;860 g&lt;/td&gt;
&lt;td&gt;6,200&lt;/td&gt;
&lt;td&gt;40 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22.2 V&lt;/td&gt;
&lt;td&gt;13×4.4 CF&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;td&gt;4.3 A&lt;/td&gt;
&lt;td&gt;95.46 W&lt;/td&gt;
&lt;td&gt;810 g&lt;/td&gt;
&lt;td&gt;5,300&lt;/td&gt;
&lt;td&gt;47 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22.2 V&lt;/td&gt;
&lt;td&gt;14×4.8 CF&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;3.5 A&lt;/td&gt;
&lt;td&gt;77.70 W&lt;/td&gt;
&lt;td&gt;780 g&lt;/td&gt;
&lt;td&gt;4,400&lt;/td&gt;
&lt;td&gt;51 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22.2 V&lt;/td&gt;
&lt;td&gt;14×4.8 CF&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;td&gt;5.7 A&lt;/td&gt;
&lt;td&gt;126.54 W&lt;/td&gt;
&lt;td&gt;1,060 g&lt;/td&gt;
&lt;td&gt;5,100&lt;/td&gt;
&lt;td&gt;51 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22.2 V&lt;/td&gt;
&lt;td&gt;14×4.8 CF&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;11.7 A&lt;/td&gt;
&lt;td&gt;259.74 W&lt;/td&gt;
&lt;td&gt;1,830 g&lt;/td&gt;
&lt;td&gt;6,600&lt;/td&gt;
&lt;td&gt;51 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The page notes that the temperature field is motor surface temperature after a ten-minute run at 100% command. That note is part of the data. It does not establish winding temperature, hot-day continuous capability, or the temperature of a motor partly shielded by an airframe. Likewise, the 180-second current and power labels are duration-limited component figures, not permission to operate the whole installed system at those values.&lt;/p&gt;

&lt;p&gt;Keep the original source URL, retrieval date, model and winding, voltage, propeller identity, and every published column. Do not copy only thrust and current into a design sheet. RPM can reveal a transcription error. Voltage is required to reconcile power. Test notes define what the temperature means. Provenance lets a later reviewer distinguish manufacturer literature from local acceptance evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not treat KV as a complete propulsion model
&lt;/h2&gt;

&lt;p&gt;KV is approximately the no-load speed constant expressed in RPM per volt. Multiplying 370 KV by 22.2 V may be useful as a rough no-load sanity check, but it does not predict loaded RPM, thrust, torque, current, or efficiency. The loaded operating point emerges from the motor, ESC timing, supply impedance, propeller torque curve, and airflow together.&lt;/p&gt;

&lt;p&gt;The table demonstrates this directly. At 22.2 V with the 14×4.8 propeller, the published 100% point is 6,600 RPM, well below the simplistic no-load product. The difference is not a defect; it is evidence that the propeller is loading the motor. A design process that estimates thrust from KV and voltage alone has discarded the dominant aerodynamic interaction.&lt;/p&gt;

&lt;p&gt;Command percentage is also not a portable physical unit. It is a point on one ESC/controller mapping under one test setup. The required quantity is thrust. When comparing propellers, interpolate against thrust inside each measured curve. Never assume that 70% on one propeller is equivalent to 70% on another, and never extrapolate outside the measured range merely because a polynomial can produce a number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interpolate with explicit refusal rules
&lt;/h2&gt;

&lt;p&gt;For the 850 g screening target, the 14.8 V 16×5.4 curve brackets the requirement between 770 g and 970 g. Linear interpolation places the target 40% of the way through that interval. That produces approximately 5.02 A and 74.30 W. The same calculation for the 22.2 V 14×4.8 curve, bracketed by 780 g and 1,060 g, produces approximately 4.05 A and 89.91 W.&lt;/p&gt;

&lt;p&gt;Linear interpolation is not a claim that the underlying motor-propeller physics are linear. It is a local screening method between adjacent measurements. Its result should retain the two bracket rows and an &lt;code&gt;interpolated&lt;/code&gt; flag. If the target is outside the source curve, if voltage differs, if the propeller identity changes, or if the bracket is too wide for the project's accuracy requirement, the program must refuse the calculation.&lt;/p&gt;

&lt;p&gt;The 14.8 V 15×5 curve also brackets 850 g and would use about 79.48 W by the same method. However, its published maximum of 1,160 g fails the 1,360 g reserve requirement. The 14.8 V 14×4.8 curve fails that reserve even more clearly. Ranking only by interpolated hover power without enforcing reserve would therefore return a result that violates the aircraft contract.&lt;/p&gt;

&lt;p&gt;The 14.8 V 16×5.4 combination reaches a published 1,380 g, only 20 g above the illustrative reserve threshold. It technically passes the simple screen but leaves about 1.5% margin between the required and published values. That is too narrow to absorb the uncertainties we explicitly said existed. A production rule might require the source maximum to exceed the reserve target by an additional measured uncertainty allowance. Under such a rule, this candidate would be rejected or moved to a mandatory local test rather than silently accepted.&lt;/p&gt;

&lt;p&gt;The 22.2 V 14×4.8 combination reaches a published 1,830 g and therefore offers more screening headroom, but it consumes about 21% more electrical power at the interpolated 850 g point than the 14.8 V 16×5.4 combination. This is the engineering trade: hover energy, command headroom, propeller clearance, acoustic behavior, current, voltage, control response, and thermal state are coupled. There is no single “best motor efficiency” number that resolves all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the screening algorithm auditable
&lt;/h2&gt;

&lt;p&gt;The following dependency-free Python program implements the narrow decision described above. It validates the source rows, groups only identical motor/voltage/propeller configurations, brackets a target without extrapolation, interpolates current and power, and then applies a separate maximum-thrust reserve constraint. It deliberately does not invent density corrections or thermal limits.&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;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;isfinite&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;voltage_v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;propeller&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;command_pct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;current_a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;power_w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;thrust_g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;rpm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Point&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="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;voltage_v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;command_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;power_w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rpm&lt;/span&gt;&lt;span class="p"&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="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;propeller&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isfinite&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&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 source point&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;voltage_v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;power_w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rpm&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;measurements must be positive&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="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;command_pct&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;100&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;command must be in (0, 100]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;relative_power_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;power_w&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;voltage_v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_a&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;power_w&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;relative_power_error&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.015&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;published power is inconsistent with V × A&lt;/span&gt;&lt;span class="sh"&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;interpolate_at_thrust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;target_g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&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;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;target_g&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;target thrust must be positive and finite&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;points&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;2&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;at least two source points are required&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;point&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;voltages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;voltage_v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;propellers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;propeller&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;points&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;voltages&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="ow"&gt;or&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;propellers&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;mixed configurations cannot be interpolated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ordered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&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;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&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;thrust points must be unique and increasing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;bracket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&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="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&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;low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&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;if&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;target_g&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&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;bracket&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;target would require extrapolation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bracket&lt;/span&gt;
    &lt;span class="n"&gt;fraction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_g&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&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="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&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;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&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;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voltage_v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;voltage_v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;propeller&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;propeller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target_thrust_g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;target_g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;power_w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;power_w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;power_w&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;command_pct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;command_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;command_pct&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bracket_thrust_g&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="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thrust_g&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_max_thrust_g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ordered&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="n"&gt;thrust_g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;interpolated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&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;screen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;hover_target_g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;minimum_thrust_ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;interpolate_at_thrust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hover_target_g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;reserve_target_g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hover_target_g&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;minimum_thrust_ratio&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reserve_target_g&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="n"&gt;reserve_target_g&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passes_published_reserve&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="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_max_thrust_g&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;gt;=&lt;/span&gt; &lt;span class="n"&gt;reserve_target_g&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reserve_margin_g&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="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_max_thrust_g&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="n"&gt;reserve_target_g&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;result&lt;/span&gt;


&lt;span class="n"&gt;mn4010_4s_16x54&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;14.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;T-MOTOR 16x5.4 CF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;4.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;63.64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;770&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;14.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;T-MOTOR 16x5.4 CF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;90.28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;970&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3650&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;14.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;T-MOTOR 16x5.4 CF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;9.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;142.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1380&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;mn4010_6s_14x48&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;22.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;T-MOTOR 14x4.8 CF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;77.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;780&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4400&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;22.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;T-MOTOR 14x4.8 CF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;126.54&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1060&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;22.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;T-MOTOR 14x4.8 CF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;11.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;259.74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1830&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6600&lt;/span&gt;&lt;span class="p"&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;curve&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mn4010_4s_16x54&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mn4010_6s_14x48&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;curve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hover_target_g&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;850&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minimum_thrust_ratio&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output should show roughly 5.02 A, 74.30 W, and 20 g of published reserve margin for the 4S 16×5.4 curve; and roughly 4.05 A, 89.91 W, and 470 g for the 6S 14×4.8 curve. The lower current on 6S does not mean lower battery stress by itself: pack construction, capacity, C-rate, wiring, connector loss, state of charge, and total six-motor current all matter. Power and heat must be evaluated at the correct boundary.&lt;/p&gt;

&lt;p&gt;The function also performs a 1.5% consistency check between the published power and &lt;code&gt;voltage × current&lt;/code&gt;. Such checks catch unit and transcription mistakes, but small differences can result from rounded display values or measurements averaged over different windows. Store the source value and the recomputed value; do not overwrite the source to make the table look cleaner.&lt;/p&gt;

&lt;p&gt;For production use, add a schema version, source identifier, retrieval timestamp, test report number, unit metadata, uncertainty fields, calibration state, and immutable raw-data hash. Keep rejected rows with their rejection reasons. A system that silently drops malformed observations can produce a polished but unauditable ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrate power over the mission, not over one row
&lt;/h2&gt;

&lt;p&gt;Hover is often the longest segment, but it is not the whole mission. Create a timeline of takeoff, climb, transit, mapping, loiter, descent, and reserve. Each segment should specify duration and per-rotor thrust distribution, not merely a throttle percentage. Interpolate power only within a validated curve and sum energy across the timeline:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mission energy = Σ(power at segment operating point × segment duration)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For six rotors at the illustrative 850 g point, the interpolated motor/ESC input implied by the manufacturer rows is about 446 W for the 4S 16×5.4 option or 539 W for the 6S 14×4.8 option. Those numbers exclude avionics, payload, conversion losses outside the measurement boundary, and any extra control activity. They are not battery-size recommendations.&lt;/p&gt;

&lt;p&gt;Battery sizing must use the worst credible loaded voltage rather than the nominal label alone. Repeat the operating-point analysis at the voltage the ESC will actually see near the minimum allowed state of charge. If no source curve exists at that voltage, test it. Scaling thrust by the square of voltage or scaling current linearly may be tempting, but those approximations can be wrong once ESC duty, motor loading, propeller RPM, and battery resistance interact.&lt;/p&gt;

&lt;p&gt;Log current at the pack and, during development, at selected ESC inputs. The sum of ESC currents need not exactly equal pack current if timestamps, filters, and measurement boundaries differ. Synchronize clocks and document averaging windows before using the data to estimate wiring loss or diagnose an outlier motor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Size the ESC and wiring from waveforms and duration
&lt;/h2&gt;

&lt;p&gt;An ESC should not be chosen by matching one published current number. Confirm supported cell count, continuous and transient current at the installed cooling condition, commutation compatibility, firmware settings, command protocol, telemetry behavior, and protection response. Consider the current step during rapid attitude correction, not just steady hover average. A slow current logger can miss short peaks that heat semiconductors or trigger protection.&lt;/p&gt;

&lt;p&gt;Connector, cable, distribution-board, and solder-joint resistance turn current into localized heat. Measure voltage at both the pack and ESC during a restrained installed test. A few tenths of a volt lost in distribution can alter available control margin while the battery monitor still reports an acceptable pack voltage. Use appropriately rated protection and an independent emergency disconnect on the test stand.&lt;/p&gt;

&lt;p&gt;Do not run an automated script as a flight-safety controller. The code above is offline analysis. Propeller testing requires a mechanically secure stand, guarding, an exclusion zone, remote observation, and a qualified operator. Emergency shutdown must not depend on a browser, wireless link, cloud service, or the analysis process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat temperature as a time series
&lt;/h2&gt;

&lt;p&gt;A single temperature cell cannot prove thermal suitability. Record ambient temperature, pressure, motor surface or winding sensor location, ESC sensor location, airflow configuration, command, current, RPM, and time. Plot temperature rise and its slope. If the slope is still positive at the end of the required segment, the system has not demonstrated a stable continuous condition.&lt;/p&gt;

&lt;p&gt;The manufacturer's listed test temperatures were produced under its stated procedure. An installed arm, fairing, payload, or coaxial arrangement can change cooling and recirculation. Copper resistance rises with temperature, battery sag changes with state and temperature, and hot electronics may derate. Validate the hottest expected operating environment and the actual duty cycle. Do not subtract a cool bench ambient from a listed surface temperature and then add that difference to a hot-day forecast as if the system were linear.&lt;/p&gt;

&lt;p&gt;Air density affects the operating point as well. Density altitude is not solved by adding an arbitrary fixed thrust percentage to every mission. Use atmospheric inputs appropriate to the site, then verify the predicted correction against an installed test. If the aircraft must operate across a wide altitude range, store separate validated envelopes or use a model whose error bounds are supported by test data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect the calculation to telemetry and review
&lt;/h2&gt;

&lt;p&gt;The design record and flight log should share stable identifiers for motor model, winding, propeller part and revision, ESC hardware and firmware, battery type, airframe revision, and analysis version. Without those joins, an analyst can observe elevated current but cannot determine which physical configuration produced it.&lt;/p&gt;

&lt;p&gt;At minimum, log battery voltage and current, per-motor command, RPM or ESC telemetry where available, attitude-controller saturation, vehicle mass configuration, ambient conditions, and relevant temperatures. Use monotonic timestamps on the vehicle and preserve clock-correlation metadata for external payloads. Flag sensor dropouts explicitly; zero is a measurement, not a substitute for missing data.&lt;/p&gt;

&lt;p&gt;Define operational signals before flight. Examples include hover power drifting upward relative to comparable flights, persistent RPM divergence at similar commands, reduced command headroom, rising vibration, connector temperature growth, or one ESC reporting repeated protection events. Compare like with like: similar mass, propeller, battery state, wind, altitude, and mission segment. A fleet dashboard that ignores configuration identity can turn normal differences into false alarms and real degradation into averages.&lt;/p&gt;

&lt;p&gt;Review the result as an envelope, not a winner. A candidate can be &lt;code&gt;rejected&lt;/code&gt;, &lt;code&gt;requires_test&lt;/code&gt;, or &lt;code&gt;qualified_for_defined_conditions&lt;/code&gt;. The last state must name those conditions. “Qualified” without voltage, propeller, mass, ambient range, firmware, and duration is not a reusable engineering decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build evidence in stages
&lt;/h2&gt;

&lt;p&gt;Start with document reconciliation: current drawing, winding, propeller, voltage, ESC manual, connector and mounting requirements. Then perform receiving inspection and a no-propeller electrical check. Move to a guarded thrust stand with calibrated thrust, voltage, current, RPM, vibration, and temperature sensing. Reproduce the intended hover and reserve points, including the worst expected loaded voltage and duration.&lt;/p&gt;

&lt;p&gt;Next test the installed but restrained propulsion system. Installation can reveal arm resonance, airflow obstruction, wiring loss, telemetry interference, or controller behavior absent from an isolated stand. Only then conduct a conservative first flight with explicit abort conditions and a limited envelope. Review the complete log before expanding mass, duration, wind, temperature, or altitude.&lt;/p&gt;

&lt;p&gt;Acceptance criteria should have been written before the test. They may include maximum stabilized current at a specified thrust, minimum command reserve, temperature limits at declared sensor locations, vibration thresholds, allowable sensor disagreement, and post-test mechanical inspection. If the evidence fails a criterion, preserve the failure and its configuration. Do not simply rerun until a passing trace appears.&lt;/p&gt;

&lt;p&gt;The MN4010 data illustrates the central lesson: component selection is a constrained, provenance-aware software problem as much as it is a hardware problem. At the same 850 g screening target, a larger 4S propeller can use less published electrical power, while a smaller 6S propeller provides substantially more published thrust headroom. The right choice depends on the aircraft's mission, geometry, uncertainty, power system, thermal envelope, and control requirements. A professional workflow makes every one of those dependencies visible—and refuses to manufacture certainty when the required evidence does not exist.&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>python</category>
      <category>robotics</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Stop Comparing UAV Motors at Equal Throttle: A Reproducible MN505-S Bench Workflow</title>
      <dc:creator>UNITED UAV Official</dc:creator>
      <pubDate>Sun, 13 Sep 2026 11:58:16 +0000</pubDate>
      <link>https://dev.to/united_uav/stop-comparing-uav-motors-at-equal-throttle-a-reproducible-mn505-s-bench-workflow-5cd4</link>
      <guid>https://dev.to/united_uav/stop-comparing-uav-motors-at-equal-throttle-a-reproducible-mn505-s-bench-workflow-5cd4</guid>
      <description>&lt;p&gt;A motor table is often read as a shopping chart: find the largest thrust number, check that the ESC amperage looks close, and move on. For an aircraft that must fly repeatedly, that is not a propulsion design. The useful question is whether a particular motor, propeller, ESC, battery, mount, and cooling path can deliver the required thrust &lt;em&gt;at the actual operating point&lt;/em&gt;, for the required duration, with measurable margin.&lt;/p&gt;

&lt;p&gt;The randomly selected product for this example is the &lt;a href="https://store.uniteduav.com/products/mn505-s-kv320" rel="noopener noreferrer"&gt;UNITED UAV T-MOTOR MN505-S KV320 Navigator motor&lt;/a&gt;. It provides an unusually useful case study because &lt;a href="https://store.tmotor.com/product/mn505-s-kv320-motor-navigator-type.html" rel="noopener noreferrer"&gt;T-MOTOR publishes separate bench curves&lt;/a&gt; for the KV320 winding with P20×6 and P22×6.6 propellers. Those curves let us demonstrate a mistake that survives many otherwise careful reviews: comparing combinations at equal throttle percentage rather than equal thrust.&lt;/p&gt;

&lt;p&gt;This is a software-and-test-engineering article, not a recommendation that a motor is suitable for any particular aircraft. No flight test, calibrated bench test, or independent endurance measurement was performed for this post. Published numbers are reference data to audit and reproduce. The Python example is an analysis tool for recorded observations; it does not command a motor or an aircraft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commercial disclosure:&lt;/strong&gt; UNITED UAV Official publishes this article and sells the linked motor. The analysis method is intended to be useful even if you choose a different supplier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the decision before opening a motor spreadsheet
&lt;/h2&gt;

&lt;p&gt;Start with an aircraft-level requirement rather than a component-level target. For a multirotor, record the maximum takeoff mass, number of lifting rotors, permitted center-of-gravity range, intended hover duration, climb requirement, ambient-temperature envelope, operating altitude, and minimum reserve after a credible degradation. A VTOL needs separate lift, transition, and cruise conditions because the same average mission power can hide a short but severe vertical-load phase.&lt;/p&gt;

&lt;p&gt;A first-pass hover estimate is aircraft weight divided by the number of lifting rotors. It is &lt;em&gt;not&lt;/em&gt; the design thrust per rotor. It omits maneuvering, gust rejection, thrust imbalance, degraded battery voltage, manufacturing variation, and whatever reserve the airframe safety analysis requires. It also assumes every rotor shares load equally. That may not hold when the center of gravity moves with payload or battery placement. Calculate the required operating points from the complete aircraft, then ask which propulsion combination can meet them.&lt;/p&gt;

&lt;p&gt;Write the acceptance criteria before testing. Examples include a maximum measured current at a specified thrust, a maximum stabilized motor and ESC temperature at a specified ambient condition, a minimum control reserve at the worst expected battery voltage, and a vibration threshold under the installed propeller. The numerical limits must come from the airframe design and verified component documentation; borrowing a generic percentage from another aircraft is not engineering evidence.&lt;/p&gt;

&lt;p&gt;The output should be a decision record with the exact motor winding, propeller part number and revision, ESC model and firmware, battery or power-supply configuration, mount, calibration identifiers, environmental conditions, raw data hashes, analysis version, and reviewer. Without configuration identity, a beautiful thrust plot is difficult to reproduce and almost impossible to use safely for a later replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the published numbers with their conditions attached
&lt;/h2&gt;

&lt;p&gt;The current &lt;a href="https://store.uniteduav.com/products/mn505-s-kv320" rel="noopener noreferrer"&gt;UNITED UAV listing&lt;/a&gt; identifies the MN505-S &lt;strong&gt;KV320&lt;/strong&gt; item separately from other windings. The &lt;a href="https://store.tmotor.com/product/mn505-s-kv320-motor-navigator-type.html" rel="noopener noreferrer"&gt;manufacturer's specification and test table&lt;/a&gt; list 6S LiPo as the rated voltage, 55 A peak current for 180 seconds, 1,300 W maximum power for 180 seconds, approximately 225 g motor mass including cable, and suggested 20–22 inch propellers. The same page identifies IP45 for the motor. These are not whole-aircraft ratings. They do not tell you that a chosen ESC, connector, battery, flight controller, or airframe can tolerate rain, dust, heat, or a 180-second peak.&lt;/p&gt;

&lt;p&gt;The published table gives these KV320 points:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Propeller&lt;/th&gt;
&lt;th&gt;Throttle label&lt;/th&gt;
&lt;th&gt;Voltage&lt;/th&gt;
&lt;th&gt;Current&lt;/th&gt;
&lt;th&gt;Thrust&lt;/th&gt;
&lt;th&gt;Input power&lt;/th&gt;
&lt;th&gt;Listed efficiency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P20×6&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;td&gt;24.03 V&lt;/td&gt;
&lt;td&gt;16.30 A&lt;/td&gt;
&lt;td&gt;2,921 g&lt;/td&gt;
&lt;td&gt;393 W&lt;/td&gt;
&lt;td&gt;7.44 g/W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P22×6.6&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;23.99 V&lt;/td&gt;
&lt;td&gt;15.20 A&lt;/td&gt;
&lt;td&gt;2,968 g&lt;/td&gt;
&lt;td&gt;364 W&lt;/td&gt;
&lt;td&gt;8.15 g/W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P20×6&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;23.96 V&lt;/td&gt;
&lt;td&gt;38.50 A&lt;/td&gt;
&lt;td&gt;5,372 g&lt;/td&gt;
&lt;td&gt;921 W&lt;/td&gt;
&lt;td&gt;5.83 g/W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P22×6.6&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;23.87 V&lt;/td&gt;
&lt;td&gt;52.40 A&lt;/td&gt;
&lt;td&gt;6,680 g&lt;/td&gt;
&lt;td&gt;1,249 W&lt;/td&gt;
&lt;td&gt;5.35 g/W&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first two rows are near the &lt;em&gt;same thrust&lt;/em&gt;, even though their throttle labels differ by ten points. In those specific published conditions, P22×6.6 produces about 47 g more thrust while drawing about 29 W less electrical power. That is a useful clue for a roughly 3 kg-per-rotor design point, not proof that P22×6.6 is always superior. It may have different mechanical clearance, inertia, control response, vibration, or high-temperature behavior. The last two rows instead show why “more maximum thrust” carries a cost: the larger propeller's published full-throttle point is 52.4 A and 1,249 W, close to the motor's listed 55 A and 1,300 W 180-second figures. Treat those maxima as boundary data, not a continuous operating prescription.&lt;/p&gt;

&lt;p&gt;The manufacturer notes an ambient temperature of 9.5 °C for the bench curves and describes motor-surface temperature after a ten-minute full-throttle run. Its page also labels peak current and maximum power as 180-second values. Do not resolve that duration mismatch by assuming a ten-minute high-power rating. Ask the manufacturer which conditions, sensors, and duty limits apply to the exact combination, then validate the intended duty cycle locally. A cool laboratory is not a hot installed nacelle.&lt;/p&gt;

&lt;p&gt;A quick arithmetic check is worthwhile. At the P20×6 70% row, 24.03 V × 16.30 A is about 392 W, consistent with the listed 393 W after rounding. Thrust divided by listed power is about 7.43 g/W, consistent with 7.44 g/W. These checks cannot certify the test, but they catch transcription or unit errors before the data enters a selection model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare at equal thrust, not equal command
&lt;/h2&gt;

&lt;p&gt;Throttle percentage is a command coordinate, not an aerodynamic requirement. Two ESCs can map the same percentage to different electrical timing and speed; two propellers impose different torque on the same motor. Even within one manufacturer's table, equal percentages do not imply equal thrust, current, efficiency, or thermal load.&lt;/p&gt;

&lt;p&gt;For candidate comparison, choose a target thrust derived from the aircraft load case. Interpolate within a measured curve only between nearby points of the &lt;em&gt;same motor, propeller, voltage class, and test setup&lt;/em&gt;. Do not extrapolate beyond its measured range. Report the bracket points and interpolation method with the result. If battery voltage differs materially between curves, normalize with an appropriate validated model or repeat the test at matched voltage rather than silently treating them as equal.&lt;/p&gt;

&lt;p&gt;Near 3 kg, the two rows above are close enough to illustrate the principle without pretending to be a high-precision interpolation. At 70% command, P20×6 reports 2,921 g and 393 W. At 60% command, P22×6.6 reports 2,968 g and 364 W. A chart organized by throttle would put those observations in different columns and could obscure the comparison. A chart with thrust on the x-axis and electrical input power on the y-axis makes the design decision visible.&lt;/p&gt;

&lt;p&gt;At other load points, the answer can change. Propeller aerodynamic efficiency, motor copper loss, ESC behavior, and cooling all vary with torque and speed. A single g/W value is not a universal property of the motor. The candidate should be evaluated over the mission's &lt;em&gt;thrust distribution&lt;/em&gt;: minutes at hover, seconds during climb, time in transition, and the battery voltage at each phase. One row at full throttle should never be used to predict an entire flight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrument the bench so the data can be trusted
&lt;/h2&gt;

&lt;p&gt;A useful bench has a restrained, guarded motor mount; a calibrated thrust sensor; voltage and current measurement at a declared point in the electrical path; RPM measurement; motor and ESC temperature sensors; ambient temperature; and a record of propeller identity. If torque is measured, log the torque-sensor calibration and sign convention. If it is not measured, do not infer mechanical efficiency from electrical input and thrust alone.&lt;/p&gt;

&lt;p&gt;Specify sensor placement. Battery-terminal voltage is not necessarily ESC-input voltage after cables, connectors, and distribution hardware. An ESC's reported current may be filtered or estimated differently from a calibrated shunt. An infrared image of a shiny rotor can be misleading because emissivity and viewing angle are uncontrolled. For temperature trends, use repeatable sensor locations and document whether the value is winding, stator case, rotor surface, ESC case, or ambient air.&lt;/p&gt;

&lt;p&gt;Define a common timebase. Record raw sensor timestamps, sample rates, clock offsets, and the aggregation window used for each test point. A row that joins thrust at second 12 with current at second 18 can create a false efficiency result during a throttle ramp. A stable-point result should be computed from a window in which command, RPM, thrust, and current have settled. Keep the raw series so an analyst can see oscillation or heating that a single average hides.&lt;/p&gt;

&lt;p&gt;Calibrate and zero the load cell before each session. Record the thrust-stand orientation, fixture geometry, propeller plane, and nearby surfaces. Recirculation, wall proximity, ground effect, and test-stand blockage can move results. Compare candidate propellers on the same fixture under a controlled procedure. Randomize test order or repeat a baseline combination at the end; otherwise ambient warming, battery depletion, or sensor drift may look like a propeller effect.&lt;/p&gt;

&lt;p&gt;Propeller testing is hazardous. Use a suitable enclosure and exclusion zone, secure the stand, keep people out of the propeller plane, provide an independent emergency power disconnect, and have a qualified operator supervise the test. A browser dashboard or Python script should only observe the stand. Flight-critical protection and physical emergency controls must not depend on a cloud service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persist a minimal data contract
&lt;/h2&gt;

&lt;p&gt;A row should be self-describing enough to survive export from the original logging system. A compact JSON record can carry the exact configuration, measurements, and quality flags:&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;"run_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;"mn505s-kv320-p22x66-20260913-b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"point_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;"hover-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"motor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MN505-S KV320"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"propeller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T-MOTOR P22x6.6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"esc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"record-exact-model-and-firmware"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"supply"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6S test supply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"window_start_utc"&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-09-13T03: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;"window_duration_s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"voltage_v"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;23.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"current_a"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;15.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"thrust_g"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2968&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rpm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3790&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"motor_surface_c"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"esc_case_c"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ambient_c"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;9.5&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_flags"&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="s2"&gt;"illustrative_from_manufacturer_table"&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 sample values reproduce one published manufacturer row; the run ID and timestamp are illustrative, not a claim that UNITED UAV performed this test. Null temperatures mean “not available in this row,” not zero degrees. A production record should include sensor IDs, calibration dates, fixture revision, raw-data URI and checksum, operator, and the exact source document revision. Use a separate field for a vendor-published row versus a measurement made by your team. Otherwise downstream code may accidentally merge literature data and acceptance-test evidence.&lt;/p&gt;

&lt;p&gt;Keep current and voltage at the same electrical boundary. If a regulator, distribution board, or cable lies between their measurement points, their product does not represent the motor/ESC input power you think it does. Record whether the power measurement includes ESC loss. “Efficiency” in g/W is a useful thrust-per-electrical-input metric, but it is not motor shaft efficiency and should not be labeled as such.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate observations before ranking candidates
&lt;/h2&gt;

&lt;p&gt;The following dependency-free Python function checks the minimum numerical invariants in a list of bench records and selects a point near a target thrust. It intentionally refuses to compare records outside a declared thrust tolerance. It does not extrapolate, interpolate, or claim a production safety decision.&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;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;isfinite&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rank_near_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tolerance_g&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&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;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;target_g&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;target_g must be positive and finite&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;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tolerance_g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;tolerance_g&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;tolerance_g must be positive and finite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;rejected&lt;/span&gt; &lt;span class="o"&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;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&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;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;row&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;point_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;try&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;row&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&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="n"&gt;row&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;motor&lt;/span&gt;&lt;span class="sh"&gt;"&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;row&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;propeller&lt;/span&gt;&lt;span class="sh"&gt;"&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;missing configuration identity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;v&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;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voltage_v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;a&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;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;thrust&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;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thrust_g&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;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thrust&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;non-finite measurement&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thrust&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;non-positive measurement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;power_w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
            &lt;span class="n"&gt;efficiency_g_per_w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;thrust&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;power_w&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thrust&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;target_g&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;tolerance_g&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;outside target thrust window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;propeller&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;propeller&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;thrust_g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;thrust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;power_w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;power_w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;g_per_w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;efficiency_g_per_w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distance_g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thrust&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;target_g&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;

    &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;power_w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distance_g&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ranked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the two published near-3 kg observations, the function reports roughly 392 W for P20×6 and 365 W for P22×6.6 when calculated from the rounded voltage and current fields. That slight difference from the table's 393 W and 364 W is a rounding reminder, not a reason to “correct” the manufacturer's data. Retain both the published value and the recomputed value with provenance if exact reconciliation matters.&lt;/p&gt;

&lt;p&gt;This small function is not enough for an acceptance pipeline. Production code should validate units, schema revisions, unique identities, calibration validity, timestamps, sensor saturation, and the distinction between measured and source-derived records. It should keep rejected rows in an auditable quarantine set rather than silently dropping them. Property-based tests can generate zero, negative, infinite, NaN, duplicate, and out-of-range inputs to prove that ranking never accepts them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model uncertainty instead of hiding it
&lt;/h2&gt;

&lt;p&gt;Any thrust/power comparison needs an uncertainty budget. The load cell contributes calibration and repeatability error. Current and voltage sensors contribute gain, offset, and timing error. The battery or power supply can sag. The stand can alter flow. Ambient density changes with temperature, pressure, and humidity. Propeller manufacturing tolerances and balance change both thrust and vibration. The total is not necessarily the simple sum of each individual sensor tolerance, but it should be documented.&lt;/p&gt;

&lt;p&gt;Report at least the number of repeats, mean, spread, and test conditions for each target point. If the apparent power advantage is smaller than the combined uncertainty or varies by test order, label the result inconclusive. If the candidate is selected on a 29 W difference near 3 kg of thrust, repeat the measurements with a calibrated system before using that difference in an endurance promise.&lt;/p&gt;

&lt;p&gt;Thermal uncertainty is especially easy to underestimate. The manufacturer's surface-temperature figures are tied to its bench and a cool listed ambient. A motor installed close to an arm, fairing, spray system, or tether hardware may have a different cooling path. Winding temperature can differ from a case or rotor measurement. Build a controlled duration test at the expected thrust, then review the temperature slope and stabilization behavior. If temperature is still rising near the end of the planned duty interval, a single final value does not establish continuous suitability.&lt;/p&gt;

&lt;p&gt;Do not extrapolate “temperature rise above ambient” linearly from one laboratory point to a hot field site. Air density, propeller loading, convective cooling, ESC loss, and motor resistance can all change. A hot-condition test or validated thermal model is needed for a high-consequence mission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrate the whole power path
&lt;/h2&gt;

&lt;p&gt;A 55 A motor peak-current figure does not authorize a 55 A aircraft design. Confirm the ESC's voltage and continuous/transient current capability for the exact firmware and cooling condition. Confirm connector and cable ampacity, distribution-board heating, battery discharge limits, fuse or protection strategy where applicable, and voltage sag at the worst expected state of charge. Log at both battery and ESC boundaries if cable loss matters.&lt;/p&gt;

&lt;p&gt;Mechanical integration is equally specific. The manufacturer's page lists an approximately 55.6 × 38.9 mm motor envelope, 225 g mass including cable, and a warning to keep mounting-thread engagement within its stated limit. Obtain the current drawing, verify bolt pattern and screw depth, and inspect for any contact with windings or rotating parts. Validate propeller retention, clearance, rotation direction, and arm stiffness. A well-matched electrical system can still fail because the mount resonates or a screw damages the motor.&lt;/p&gt;

&lt;p&gt;The motor's IP45 claim does not transfer to the ESC, connectors, bearings after wear, or the complete aircraft. Environmental qualification should consider the installed system and its maintenance state. Record water or dust exposure, cleaning method, corrosion findings, bearing play, cable damage, vibration trend, and current drift over service life. A numerical MTBF claim, where listed, is not a substitute for a fleet-specific inspection program.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the bench result into a flight-readiness gate
&lt;/h2&gt;

&lt;p&gt;A sensible sequence is receiving inspection; document and connector review; no-propeller rotation check; guarded stand test; installed restrained ground test; conservative first flight; and log review before expanding the envelope. Each transition should have written entry conditions and stop criteria. The first flight should not be asked to discover a propeller mismatch that a guarded bench could have found.&lt;/p&gt;

&lt;p&gt;For an installed multirotor, compare equivalent arms at a controlled operating condition. Look for differences in current at similar thrust command, RPM where available, ESC temperature, motor temperature, and vibration. An outlier may indicate a damaged propeller, bearing issue, cable resistance, ESC configuration difference, or structural resonance. Do not assume that swapping only the motor isolates the cause; change one variable at a time and preserve the configuration history.&lt;/p&gt;

&lt;p&gt;After flight, compare the measured thrust-demand distribution with the load cases used in selection. If the aircraft spends more time at high command than expected, or one arm consistently works harder, revisit the mass, center of gravity, propeller, and control assumptions. A propulsion design is validated by the operating envelope it actually sees, not merely by a catalog point it once matched.&lt;/p&gt;

&lt;p&gt;The engineering takeaway is narrow but important: use the MN505-S KV320 manufacturer's table as a starting dataset, not a certificate of aircraft suitability. Compare propellers at equal required thrust, carry measurement provenance and uncertainty through analysis, validate the thermal and electrical path at the intended duty cycle, and require independent acceptance evidence before flight. That workflow is portable to other UAV motors—and more valuable than any isolated “maximum thrust” number.&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>performance</category>
      <category>tools</category>
    </item>
    <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>
