thrustlab 0.4.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. {thrustlab-0.4.0 → thrustlab-0.6.0}/CHANGELOG.md +59 -0
  2. {thrustlab-0.4.0 → thrustlab-0.6.0}/PKG-INFO +34 -2
  3. {thrustlab-0.4.0 → thrustlab-0.6.0}/README.md +28 -0
  4. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/components/create_component.py +2 -2
  5. thrustlab-0.6.0/examples/dynamic/watch_and_cancel.py +132 -0
  6. thrustlab-0.6.0/examples/fmu/ardupilot_sitl/README.md +89 -0
  7. thrustlab-0.6.0/examples/fmu/ardupilot_sitl/fly_mission.py +225 -0
  8. thrustlab-0.6.0/examples/fmu/ardupilot_sitl/fmu.parm +6 -0
  9. thrustlab-0.6.0/examples/fmu/ardupilot_sitl/plot_mission.py +267 -0
  10. thrustlab-0.6.0/examples/fmu/ardupilot_sitl/quad_x.yaml +62 -0
  11. thrustlab-0.6.0/examples/fmu/ardupilot_sitl/render_mission_video.py +454 -0
  12. thrustlab-0.6.0/examples/fmu/ardupilot_sitl_tilttri/README.md +85 -0
  13. thrustlab-0.6.0/examples/fmu/ardupilot_sitl_tilttri/fly_transition.py +211 -0
  14. thrustlab-0.6.0/examples/fmu/ardupilot_sitl_tilttri/tilttri.parm +30 -0
  15. thrustlab-0.6.0/examples/fmu/ardupilot_sitl_tilttri/tilttri.yaml +128 -0
  16. thrustlab-0.6.0/examples/fmu/ardupilot_sitl_y6/README.md +75 -0
  17. thrustlab-0.6.0/examples/fmu/ardupilot_sitl_y6/plot_coax_split.py +131 -0
  18. thrustlab-0.6.0/examples/fmu/ardupilot_sitl_y6/y6.parm +6 -0
  19. thrustlab-0.6.0/examples/fmu/ardupilot_sitl_y6/y6.yaml +107 -0
  20. thrustlab-0.6.0/examples/fmu/betaflight_sitl/README.md +251 -0
  21. thrustlab-0.6.0/examples/fmu/betaflight_sitl/betaflight_config.txt +41 -0
  22. thrustlab-0.6.0/examples/fmu/betaflight_sitl/betaflight_quad_x.yaml +129 -0
  23. thrustlab-0.6.0/examples/fmu/betaflight_sitl/fly_hover.py +697 -0
  24. thrustlab-0.6.0/examples/fmu/endurance/README.md +40 -0
  25. thrustlab-0.6.0/examples/fmu/endurance/hover_endurance.py +215 -0
  26. thrustlab-0.6.0/examples/fmu/export_and_download.py +80 -0
  27. thrustlab-0.6.0/examples/fmu/gazebo/README.md +72 -0
  28. thrustlab-0.6.0/examples/fmu/gazebo/quad_fmu.sdf +373 -0
  29. thrustlab-0.6.0/examples/fmu/quad_5inch_x.yaml +142 -0
  30. thrustlab-0.6.0/examples/fmu/simulink/.gitignore +2 -0
  31. thrustlab-0.6.0/examples/fmu/simulink/README.md +53 -0
  32. thrustlab-0.6.0/examples/fmu/simulink/fmu_cosim.png +0 -0
  33. thrustlab-0.6.0/examples/fmu/simulink/run_fmu_simulink.m +93 -0
  34. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/outputs/read_dynamic.py +18 -6
  35. thrustlab-0.6.0/examples/sweeps/component_axis_grid.py +85 -0
  36. {thrustlab-0.4.0 → thrustlab-0.6.0}/pyproject.toml +18 -1
  37. thrustlab-0.6.0/tests/sitl/__init__.py +1 -0
  38. thrustlab-0.6.0/tests/sitl/ardupilot_parser.py +160 -0
  39. thrustlab-0.6.0/tests/sitl/betaflight_fc.py +119 -0
  40. thrustlab-0.6.0/tests/sitl/conftest.py +47 -0
  41. thrustlab-0.6.0/tests/sitl/mock_fmu.py +249 -0
  42. thrustlab-0.6.0/tests/sitl/test_betaflight_bridge.py +149 -0
  43. thrustlab-0.6.0/tests/sitl/test_betaflight_protocol.py +329 -0
  44. thrustlab-0.6.0/tests/sitl/test_kinematics.py +283 -0
  45. thrustlab-0.6.0/tests/sitl/test_protocol.py +222 -0
  46. thrustlab-0.6.0/tests/sitl/test_rigidbody.py +250 -0
  47. thrustlab-0.6.0/tests/sitl/test_state_machine.py +256 -0
  48. thrustlab-0.6.0/tests/sitl/test_step_path.py +487 -0
  49. thrustlab-0.6.0/tests/sitl/test_tilt_wing.py +315 -0
  50. thrustlab-0.6.0/tests/sitl/test_vehicle.py +273 -0
  51. thrustlab-0.6.0/tests/test_resources/test_fmu.py +148 -0
  52. thrustlab-0.6.0/tests/test_resources/test_simulations_cancel_batch.py +62 -0
  53. thrustlab-0.6.0/tests/test_resources/test_simulations_run_batch.py +80 -0
  54. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_http.py +7 -3
  55. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/__init__.py +30 -0
  56. thrustlab-0.6.0/thrustlab/_models/cancel_batch_result.py +97 -0
  57. thrustlab-0.6.0/thrustlab/_models/cancel_selected_body.py +42 -0
  58. thrustlab-0.6.0/thrustlab/_models/cancel_skipped.py +71 -0
  59. thrustlab-0.6.0/thrustlab/_models/cancel_skipped_reason.py +10 -0
  60. thrustlab-0.6.0/thrustlab/_models/component_filter_bounds.py +97 -0
  61. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_ids_response.py +27 -3
  62. thrustlab-0.6.0/thrustlab/_models/component_ids_response_filters.py +63 -0
  63. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_list_response.py +19 -0
  64. thrustlab-0.6.0/thrustlab/_models/component_list_response_filters.py +63 -0
  65. thrustlab-0.6.0/thrustlab/_models/component_specs_response.py +101 -0
  66. thrustlab-0.6.0/thrustlab/_models/component_specs_response_spec_json.py +47 -0
  67. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_sweep_axis_in.py +1 -1
  68. thrustlab-0.6.0/thrustlab/_models/concurrency_resource.py +108 -0
  69. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_balance_resource.py +1 -1
  70. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_event_resource.py +1 -1
  71. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_summary_resource.py +1 -1
  72. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource.py +87 -4
  73. thrustlab-0.6.0/thrustlab/_models/dynamic_resource_progress_type_0.py +47 -0
  74. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/entitlements_resource.py +12 -0
  75. thrustlab-0.6.0/thrustlab/_models/fmu_export_accepted_response.py +82 -0
  76. thrustlab-0.6.0/thrustlab/_models/fmu_export_job_error.py +69 -0
  77. thrustlab-0.6.0/thrustlab/_models/fmu_export_status_response.py +393 -0
  78. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource.py +67 -31
  79. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_create_body.py +6 -3
  80. thrustlab-0.6.0/thrustlab/_models/sweep_credits_breakdown.py +73 -0
  81. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_param_range_in.py +12 -6
  82. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource.py +122 -31
  83. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/user_resource.py +29 -0
  84. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_version.py +1 -1
  85. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/client.py +3 -1
  86. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/dynamic_simulations.py +6 -2
  87. thrustlab-0.6.0/thrustlab/resources/fmu.py +147 -0
  88. thrustlab-0.6.0/thrustlab/resources/simulations.py +186 -0
  89. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/sweeps.py +1 -1
  90. thrustlab-0.6.0/thrustlab/sitl/README.md +362 -0
  91. thrustlab-0.6.0/thrustlab/sitl/__init__.py +93 -0
  92. thrustlab-0.6.0/thrustlab/sitl/__main__.py +10 -0
  93. thrustlab-0.6.0/thrustlab/sitl/bridge.py +173 -0
  94. thrustlab-0.6.0/thrustlab/sitl/cli.py +217 -0
  95. thrustlab-0.6.0/thrustlab/sitl/fmu.py +494 -0
  96. thrustlab-0.6.0/thrustlab/sitl/kinematics.py +147 -0
  97. thrustlab-0.6.0/thrustlab/sitl/model.py +66 -0
  98. thrustlab-0.6.0/thrustlab/sitl/protocol.py +240 -0
  99. thrustlab-0.6.0/thrustlab/sitl/rigidbody.py +439 -0
  100. thrustlab-0.6.0/thrustlab/sitl/rpmlog.py +115 -0
  101. thrustlab-0.6.0/thrustlab/sitl/targets/__init__.py +42 -0
  102. thrustlab-0.6.0/thrustlab/sitl/targets/ardupilot.py +221 -0
  103. thrustlab-0.6.0/thrustlab/sitl/targets/base.py +292 -0
  104. thrustlab-0.6.0/thrustlab/sitl/targets/betaflight.py +714 -0
  105. thrustlab-0.6.0/thrustlab/sitl/vehicle.py +687 -0
  106. thrustlab-0.4.0/thrustlab/resources/simulations.py +0 -87
  107. {thrustlab-0.4.0 → thrustlab-0.6.0}/.gitignore +0 -0
  108. {thrustlab-0.4.0 → thrustlab-0.6.0}/LICENSE +0 -0
  109. {thrustlab-0.4.0 → thrustlab-0.6.0}/NOTICE +0 -0
  110. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/dynamic/run_and_poll.py +0 -0
  111. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/outputs/read_steady_state.py +0 -0
  112. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/projects/create.py +0 -0
  113. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/projects/list.py +0 -0
  114. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/projects/retrieve.py +0 -0
  115. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/quickstart.py +0 -0
  116. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/simulations/run_sync.py +0 -0
  117. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/simulations/wait.py +0 -0
  118. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/sweeps/run_and_poll.py +0 -0
  119. {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/webhooks/verify_handler.py +0 -0
  120. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/__init__.py +0 -0
  121. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/conftest.py +0 -0
  122. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_async_polling.py +0 -0
  123. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_client.py +0 -0
  124. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_components_create.py +0 -0
  125. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_components_find.py +0 -0
  126. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_dynamic_resource.py +0 -0
  127. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_exceptions.py +0 -0
  128. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_http.py +0 -0
  129. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_pagination.py +0 -0
  130. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/__init__.py +0 -0
  131. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_components.py +0 -0
  132. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_compute_units.py +0 -0
  133. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_projects.py +0 -0
  134. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_simulations.py +0 -0
  135. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_starred_components.py +0 -0
  136. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_submissions.py +0 -0
  137. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_sweeps.py +0 -0
  138. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_users.py +0 -0
  139. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_webhook_endpoints.py +0 -0
  140. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_spec_coverage.py +0 -0
  141. {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_webhooks.py +0 -0
  142. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/__init__.py +0 -0
  143. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_async_polling.py +0 -0
  144. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/airfoil_breakpoint.py +0 -0
  145. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/airfoil_list_response.py +0 -0
  146. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/airfoil_resource.py +0 -0
  147. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_request.py +0 -0
  148. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_request_mode.py +0 -0
  149. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_request_target_type_type_0.py +0 -0
  150. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_response.py +0 -0
  151. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_response_envelope_limiting_factor_type_0.py +0 -0
  152. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_response_envelope_status_type_0.py +0 -0
  153. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_station_row.py +0 -0
  154. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/body_import_airfoil_v1_airfoils_import_post.py +0 -0
  155. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/calibrate_accepted_response.py +0 -0
  156. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_brand.py +0 -0
  157. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_brands_response.py +0 -0
  158. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_create_body.py +0 -0
  159. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_create_body_spec_json.py +0 -0
  160. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_patch.py +0 -0
  161. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_patch_spec_json_type_0.py +0 -0
  162. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_resource.py +0 -0
  163. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_resource_spec_json.py +0 -0
  164. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_sweep_axis_in_axis.py +0 -0
  165. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_write_response.py +0 -0
  166. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_write_response_spec_json.py +0 -0
  167. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_write_warning.py +0 -0
  168. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/create_webhook_endpoint_v1_webhook_endpoints_post_response_create_webhook_endpoint_v1_webhook_endpoints_post.py +0 -0
  169. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_bucket_breakdown.py +0 -0
  170. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_bucket_breakdown_type.py +0 -0
  171. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_event_resource_type.py +0 -0
  172. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_event_resource_unit_type.py +0 -0
  173. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_list_response.py +0 -0
  174. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_related_resource.py +0 -0
  175. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_related_resource_object.py +0 -0
  176. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_summary_resource_window.py +0 -0
  177. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/design_request.py +0 -0
  178. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/design_request_target_mode.py +0 -0
  179. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_create_body.py +0 -0
  180. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_estimate_body.py +0 -0
  181. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_estimate_resource.py +0 -0
  182. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_list_response.py +0 -0
  183. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_patch.py +0 -0
  184. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_battery_topology_type_0.py +0 -0
  185. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_display_labels_type_0.py +0 -0
  186. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_error_type_0.py +0 -0
  187. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_input_snapshot_type_0.py +0 -0
  188. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_result_type_0.py +0 -0
  189. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_status.py +0 -0
  190. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_group_in.py +0 -0
  191. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_group_in_esc_timing.py +0 -0
  192. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_group_in_esc_type.py +0 -0
  193. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_group_in_motor_cooling_source.py +0 -0
  194. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_in.py +0 -0
  195. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_in_esc_timing.py +0 -0
  196. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_in_esc_type.py +0 -0
  197. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_in_motor_cooling_source.py +0 -0
  198. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_in_rotation_sense.py +0 -0
  199. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/export_geometry_request.py +0 -0
  200. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/export_geometry_request_format.py +0 -0
  201. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/export_geometry_request_rotation_type_0.py +0 -0
  202. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/fire_test_event_v1_webhook_endpoints_public_id_test_post_response_fire_test_event_v1_webhook_endpoints_public_id_test_post.py +0 -0
  203. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/generate_geometry_request.py +0 -0
  204. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/geometry_response.py +0 -0
  205. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/geometry_style_resource.py +0 -0
  206. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/geometry_styles_list_response.py +0 -0
  207. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/get_delivery_v1_webhook_endpoints_public_id_deliveries_delivery_public_id_get_response_get_delivery_v1_webhook_endpoints_public_id_deliveries_delivery_public_id_get.py +0 -0
  208. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/get_event_v1_events_event_id_get_response_get_event_v1_events_event_id_get.py +0 -0
  209. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/get_public_result_v1_public_results_slug_get_response_get_public_result_v1_public_results_slug_get.py +0 -0
  210. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/get_shared_result_v1_public_share_share_token_get_response_get_shared_result_v1_public_share_share_token_get.py +0 -0
  211. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/get_webhook_endpoint_v1_webhook_endpoints_public_id_get_response_get_webhook_endpoint_v1_webhook_endpoints_public_id_get.py +0 -0
  212. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/http_validation_error.py +0 -0
  213. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/list_deliveries_v1_webhook_endpoints_public_id_deliveries_get_response_list_deliveries_v1_webhook_endpoints_public_id_deliveries_get.py +0 -0
  214. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/list_events_v1_events_get_response_list_events_v1_events_get.py +0 -0
  215. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/list_public_propellers_v1_public_propellers_get_response_list_public_propellers_v1_public_propellers_get.py +0 -0
  216. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/list_webhook_endpoints_v1_webhook_endpoints_get_response_list_webhook_endpoints_v1_webhook_endpoints_get.py +0 -0
  217. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/pack_leaf_in.py +0 -0
  218. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/pack_parallel_in.py +0 -0
  219. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/pack_series_in.py +0 -0
  220. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/pack_topology_in.py +0 -0
  221. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/patch_webhook_endpoint_v1_webhook_endpoints_public_id_patch_response_patch_webhook_endpoint_v1_webhook_endpoints_public_id_patch.py +0 -0
  222. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/project_create_body.py +0 -0
  223. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/project_list_response.py +0 -0
  224. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/project_patch.py +0 -0
  225. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/project_resource.py +0 -0
  226. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/retry_delivery_v1_webhook_endpoints_public_id_deliveries_delivery_public_id_retry_post_response_retry_delivery_v1_webhook_endpoints_public_id_deliveries_delivery_public_id_retry_post.py +0 -0
  227. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotate_webhook_secret_v1_webhook_endpoints_public_id_rotate_secret_post_response_rotate_webhook_secret_v1_webhook_endpoints_public_id_rotate_secret_post.py +0 -0
  228. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_group_in.py +0 -0
  229. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_group_in_esc_timing.py +0 -0
  230. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_group_in_esc_type.py +0 -0
  231. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_group_in_motor_cooling_source.py +0 -0
  232. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_group_resource.py +0 -0
  233. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_in.py +0 -0
  234. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_in_esc_timing.py +0 -0
  235. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_in_esc_type.py +0 -0
  236. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_in_motor_cooling_source.py +0 -0
  237. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_in_rotation_sense.py +0 -0
  238. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/run_queued_v1v1_simulations_run_queued_post_response_run_queued_v1v1_simulations_run_queued_post.py +0 -0
  239. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/run_selected_body.py +0 -0
  240. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/run_selected_v1v1_simulations_run_selected_post_response_run_selected_v1v1_simulations_run_selected_post.py +0 -0
  241. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/schedule_in.py +0 -0
  242. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/schedule_in_interpolation_type_0.py +0 -0
  243. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/schedule_in_mode.py +0 -0
  244. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_group_command_in.py +0 -0
  245. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_group_command_in_throttle_ramp_type_0.py +0 -0
  246. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_group_command_in_tilt_ramp_type_0.py +0 -0
  247. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_in.py +0 -0
  248. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_in_airspeed_ramp.py +0 -0
  249. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_in_per_group.py +0 -0
  250. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_in_throttle_ramp.py +0 -0
  251. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_in_vertical_speed_ramp.py +0 -0
  252. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/share_resource.py +0 -0
  253. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_cancel_body.py +0 -0
  254. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_create_body.py +0 -0
  255. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_create_body_cooling_source_type_0.py +0 -0
  256. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_create_body_flight_regime.py +0 -0
  257. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_create_body_inflow_mode.py +0 -0
  258. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_create_body_launch_intent.py +0 -0
  259. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_inputs.py +0 -0
  260. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_list_response.py +0 -0
  261. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_patch.py +0 -0
  262. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_patch_plot_config_json_type_0_item.py +0 -0
  263. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_battery_topology_type_0.py +0 -0
  264. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_display_labels_type_0.py +0 -0
  265. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_error_type_0.py +0 -0
  266. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_input_snapshot_type_0.py +0 -0
  267. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_plot_config_json_type_0_item.py +0 -0
  268. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_result_type_0.py +0 -0
  269. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_result_type_0_additional_property.py +0 -0
  270. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_status.py +0 -0
  271. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/spline_data.py +0 -0
  272. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/starred_component_create_body.py +0 -0
  273. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/starred_component_list_response.py +0 -0
  274. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/starred_component_resource.py +0 -0
  275. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_create_body.py +0 -0
  276. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_create_body_data_json.py +0 -0
  277. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_list_response.py +0 -0
  278. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_patch.py +0 -0
  279. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_patch_data_json_type_0.py +0 -0
  280. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_resource.py +0 -0
  281. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_resource_data_json.py +0 -0
  282. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_cancel_body.py +0 -0
  283. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_config_in.py +0 -0
  284. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_config_in_esc_timing_values_type_0_item.py +0 -0
  285. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_create_body_cooling_source.py +0 -0
  286. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_create_body_inflow_mode.py +0 -0
  287. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_create_body_launch_intent.py +0 -0
  288. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_inputs.py +0 -0
  289. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_list_response.py +0 -0
  290. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_param_range_in_mode.py +0 -0
  291. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_patch.py +0 -0
  292. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_patch_plot_config_json_type_0_item.py +0 -0
  293. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_component_selection.py +0 -0
  294. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_inputs.py +0 -0
  295. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_list_response.py +0 -0
  296. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_list_response_display_labels_type_0.py +0 -0
  297. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_patch.py +0 -0
  298. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_resource.py +0 -0
  299. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_resource_rotors_type_0.py +0 -0
  300. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_resource_rotors_type_0_additional_property.py +0 -0
  301. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_error_type_0.py +0 -0
  302. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_input_snapshot_type_0.py +0 -0
  303. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_plot_config_json_type_0_item.py +0 -0
  304. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_status.py +0 -0
  305. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_summary_type_0.py +0 -0
  306. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_sweep_config_type_0.py +0 -0
  307. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_group_in.py +0 -0
  308. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_group_in_esc_timing.py +0 -0
  309. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_group_in_esc_type.py +0 -0
  310. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_group_in_motor_cooling_source.py +0 -0
  311. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_in.py +0 -0
  312. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_in_esc_timing.py +0 -0
  313. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_in_esc_type.py +0 -0
  314. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_in_motor_cooling_source.py +0 -0
  315. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_in_rotation_sense.py +0 -0
  316. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/termination_in.py +0 -0
  317. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/termination_in_mode.py +0 -0
  318. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/types.py +0 -0
  319. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/user_resource_gate_required_tiers.py +0 -0
  320. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/user_resource_unit_system.py +0 -0
  321. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/v1_custom_component_override.py +0 -0
  322. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/v1_custom_component_override_spec_json.py +0 -0
  323. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/v1_health_v1_health_get_response_v1_health_v1_health_get.py +0 -0
  324. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/validation_error.py +0 -0
  325. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/webhook_endpoint_create.py +0 -0
  326. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/webhook_endpoint_patch.py +0 -0
  327. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_pagination.py +0 -0
  328. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/exceptions.py +0 -0
  329. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/__init__.py +0 -0
  330. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/_base.py +0 -0
  331. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/components.py +0 -0
  332. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/compute_units.py +0 -0
  333. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/projects.py +0 -0
  334. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/starred_components.py +0 -0
  335. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/submissions.py +0 -0
  336. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/users.py +0 -0
  337. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/webhook_endpoints.py +0 -0
  338. {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/webhooks.py +0 -0
@@ -8,6 +8,65 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.6.0] - 2026-09-09
12
+
13
+ ### Added
14
+
15
+ - `client.simulations.cancel_selected(run_ids)` cancels up to 1000 runs of any
16
+ kind (`sim_`, `sweep_`, `dyn_`) in one call and reports which were canceled,
17
+ which were asked to stop, and which were skipped and why.
18
+ `client.simulations.cancel_queued(project_id=...)` cancels every queued run
19
+ you own, optionally within one project.
20
+ - `client.simulations.run_queued()` and
21
+ `client.simulations.run_selected(simulation_ids)` dispatch runs parked with
22
+ `launch_intent="queue"` — all of them, or a named subset. Both accept sweep
23
+ ids as well as single-point ids.
24
+ - Generated models for the batch cancel response (`CancelBatchResult`,
25
+ `CancelSkipped`) and request body (`CancelSelectedBody`).
26
+ - Two runnable examples. `examples/sweeps/component_axis_grid.py` crosses a
27
+ propeller component axis with a throttle range in one submission and reads
28
+ the propeller name back off each point.
29
+ `examples/dynamic/watch_and_cancel.py` watches a mission's `progress` and
30
+ cancels it part-way through.
31
+
32
+ ### Changed
33
+
34
+ - Cancelling a running sweep is now a durable request: the server records it
35
+ and the worker stops after the point it is computing, even if the stop takes
36
+ longer than a minute to reach.
37
+ - Cancelling a RUNNING dynamic mission now reports under `requested` rather
38
+ than `canceled`. The mission keeps its partial result (`run_meta.partial`,
39
+ `run_meta.canceled_at_t_s`), the credits are refunded, and the execution slot
40
+ frees when the worker stops at its next integration window.
41
+ - Run resources carry `started_at`, the instant the solver began, alongside
42
+ `created_at`, `dispatched_at` and `completed_at`. Queue wait is
43
+ `started_at - dispatched_at`.
44
+ - Dynamic run resources carry `progress` and `cancel_requested_at`. Sweep
45
+ resources carry `cancel_requested_at` and `credits_breakdown`.
46
+
47
+ ## [0.5.0] - 2026-08-31
48
+
49
+ ### Added
50
+
51
+ - `client.fmu` can export a completed simulation as an FMI 3.0 co-simulation
52
+ FMU with per-rotor inputs and outputs, battery and thermal state, and a
53
+ validity envelope. Exports can be enqueued, checked with paced polling,
54
+ downloaded when ready, or canceled.
55
+ - The new `thrustlab.sitl` module flies exported FMUs in ArduPilot SITL and
56
+ Betaflight SITL using a six-degree-of-freedom rigid body, per-rotor servo
57
+ mapping, and battery voltage, current, and airspeed feedback to the firmware.
58
+ The `thrustlab-sitl` console command launches the bridge.
59
+ - The new `examples/fmu` gallery includes quadcopter and Y6 SITL missions, an
60
+ endurance study, and replay rendering.
61
+ - Generated models now cover component datasheets and the `component_specs`
62
+ entitlement flag.
63
+
64
+ ### Changed
65
+
66
+ - NumPy, PyYAML, and FMPy are now included in the core installation, so
67
+ `pip install thrustlab` provides everything needed for SITL. The `[sitl]`
68
+ extra remains available as an empty compatibility alias.
69
+
11
70
  ## [0.4.0] - 2026-08-27
12
71
 
13
72
  Contract refresh for the production `/v1/` API. This release adds the current
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.5
1
+ Metadata-Version: 2.4
2
2
  Name: thrustlab
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Official Python SDK for the ThrustLab API
5
5
  Project-URL: Homepage, https://thrustlab.com
6
6
  Project-URL: Documentation, https://thrustlab.com/docs
@@ -20,14 +20,18 @@ Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
21
  Requires-Python: >=3.10
22
22
  Requires-Dist: attrs>=23.0
23
+ Requires-Dist: fmpy>=0.3.31
23
24
  Requires-Dist: httpx<1.0,>=0.27
25
+ Requires-Dist: numpy>=1.24
24
26
  Requires-Dist: pydantic<3.0,>=2.7
25
27
  Requires-Dist: python-dateutil>=2.8
28
+ Requires-Dist: pyyaml>=6.0
26
29
  Provides-Extra: dev
27
30
  Requires-Dist: openapi-python-client==0.28.4; extra == 'dev'
28
31
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
29
32
  Requires-Dist: pytest>=8; extra == 'dev'
30
33
  Requires-Dist: respx>=0.21; extra == 'dev'
34
+ Provides-Extra: sitl
31
35
  Description-Content-Type: text/markdown
32
36
 
33
37
  # thrustlab — official Python SDK for the ThrustLab API
@@ -89,6 +93,33 @@ Runnable end-to-end examples for every simulation type live in
89
93
  [`sweeps/run_and_poll.py`](./examples/sweeps/run_and_poll.py), and
90
94
  [`dynamic/run_and_poll.py`](./examples/dynamic/run_and_poll.py).
91
95
 
96
+ ## FMU export and SITL
97
+
98
+ Export a completed simulation as an FMI 3.0 co-simulation FMU:
99
+
100
+ ```python
101
+ job_id = client.fmu.export(sim_id)["id"]
102
+ client.fmu.wait(job_id)
103
+ client.fmu.download(job_id, "powertrain.fmu")
104
+ ```
105
+
106
+ The FMU exposes per-rotor inputs and outputs, battery and thermal state, and a
107
+ validity envelope; it runs in any FMI 3.0 co-simulation host and is verified
108
+ in Simulink.
109
+
110
+ `thrustlab-sitl`, installed with the SDK, flies the FMU in ArduPilot SITL or
111
+ Betaflight SITL (`--target betaflight`). See
112
+ [`thrustlab/sitl/README.md`](./thrustlab/sitl/README.md) for the full
113
+ documentation.
114
+
115
+ ```shell
116
+ thrustlab-sitl --fmu powertrain.fmu --vehicle quad_x.yaml --target betaflight
117
+ ```
118
+
119
+ See [`examples/fmu/`](./examples/fmu/) for bundled ready-to-fly FMUs with
120
+ quadcopter, Y6 coax, and tilt-tricopter missions, an endurance study, and
121
+ replay rendering.
122
+
92
123
  ## Why the SDK (and not `urllib` / `wget`)?
93
124
 
94
125
  The SDK gives you automatic retries with backoff, idempotency keys, cursor
@@ -127,6 +158,7 @@ client.submissions
127
158
  client.starred_components
128
159
  client.compute_units
129
160
  client.webhook_endpoints
161
+ client.fmu
130
162
  ```
131
163
 
132
164
  ## Error handling
@@ -57,6 +57,33 @@ Runnable end-to-end examples for every simulation type live in
57
57
  [`sweeps/run_and_poll.py`](./examples/sweeps/run_and_poll.py), and
58
58
  [`dynamic/run_and_poll.py`](./examples/dynamic/run_and_poll.py).
59
59
 
60
+ ## FMU export and SITL
61
+
62
+ Export a completed simulation as an FMI 3.0 co-simulation FMU:
63
+
64
+ ```python
65
+ job_id = client.fmu.export(sim_id)["id"]
66
+ client.fmu.wait(job_id)
67
+ client.fmu.download(job_id, "powertrain.fmu")
68
+ ```
69
+
70
+ The FMU exposes per-rotor inputs and outputs, battery and thermal state, and a
71
+ validity envelope; it runs in any FMI 3.0 co-simulation host and is verified
72
+ in Simulink.
73
+
74
+ `thrustlab-sitl`, installed with the SDK, flies the FMU in ArduPilot SITL or
75
+ Betaflight SITL (`--target betaflight`). See
76
+ [`thrustlab/sitl/README.md`](./thrustlab/sitl/README.md) for the full
77
+ documentation.
78
+
79
+ ```shell
80
+ thrustlab-sitl --fmu powertrain.fmu --vehicle quad_x.yaml --target betaflight
81
+ ```
82
+
83
+ See [`examples/fmu/`](./examples/fmu/) for bundled ready-to-fly FMUs with
84
+ quadcopter, Y6 coax, and tilt-tricopter missions, an endurance study, and
85
+ replay rendering.
86
+
60
87
  ## Why the SDK (and not `urllib` / `wget`)?
61
88
 
62
89
  The SDK gives you automatic retries with backoff, idempotency keys, cursor
@@ -95,6 +122,7 @@ client.submissions
95
122
  client.starred_components
96
123
  client.compute_units
97
124
  client.webhook_endpoints
125
+ client.fmu
98
126
  ```
99
127
 
100
128
  ## Error handling
@@ -18,8 +18,8 @@ Notes on validation:
18
18
  one resistance figure (c_rating or pack_resistance_mOhm), and bounding-box
19
19
  dims; form_factor is derived from chemistry when omitted.
20
20
  * Propeller: per-station radius/chord/twist and rotation are required, and
21
- persisting a propeller needs the hobbyist tier or higher — the create below
22
- reports the tier gate cleanly on a free account.
21
+ persisting a propeller needs the Pro plan — the create below reports the
22
+ tier gate cleanly on a free account.
23
23
  """
24
24
 
25
25
  from thrustlab import Client
@@ -0,0 +1,132 @@
1
+ """Dynamic progress + cancel: submit -> watch `progress` -> cancel -> read the partial.
2
+
3
+ Run it:
4
+ export THRUSTLAB_API_KEY=key_... # never hard-code the key
5
+ python examples/dynamic/watch_and_cancel.py
6
+
7
+ Submits a mission that flies until the pack is empty, watches `progress` while
8
+ it runs, cancels it part-way through, and reads the partial result.
9
+
10
+ `progress` is {fraction, sim_time_s, sim_time_total_s, wall_s, updated_at}, and
11
+ is None until the worker picks the run up.
12
+
13
+ Cancelling a RUNNING dynamic mission does not stop it instantly. The response
14
+ keeps status "running" and sets `cancel_requested_at`; the worker stops at its
15
+ next integration window, at most 20 seconds of mission time. What comes back is
16
+ a real partial result: `run_meta.partial` is true, `run_meta.canceled_at_t_s`
17
+ gives the mission time it stopped at, and the credits are refunded.
18
+
19
+ This mission runs to depletion, so `fraction` is an estimate rather than an
20
+ exact ratio.
21
+ """
22
+
23
+ import time
24
+
25
+ from thrustlab import Client
26
+
27
+ client = Client() # reads $THRUSTLAB_API_KEY from the environment
28
+
29
+ project = client.projects.create(name="sdk dynamic progress example")
30
+
31
+ motor = client.components.find(name="BadAss 2826-820Kv")
32
+ prop = client.components.find(name="10.5x4.5")
33
+ battery = client.components.find(
34
+ name="Liperior 5000mAh 4S 35C 14.8V Lipo Battery With XT90 Plug"
35
+ )
36
+
37
+ # A hover flown until the pack is empty.
38
+ #
39
+ # The schedule steps to 15% before it ramps, rather than ramping up from rest.
40
+ # One external study observed ramps starting at near-zero duty producing a
41
+ # handful of non-converged rows in their first milliseconds; as a fraction of a
42
+ # short flight's rows that was enough to get a run rejected with
43
+ # `solver_no_convergence`. Stepping to a working throttle first avoided it.
44
+ dyn = client.dynamic_simulations.create(
45
+ project_id=project["id"],
46
+ battery_component_id=battery["id"],
47
+ density_kg_m3=1.225,
48
+ battery_charge_pct=100,
49
+ ambient_temp_c=25,
50
+ rotor_groups=[
51
+ {
52
+ "label": "main",
53
+ "count": 4,
54
+ "motor_component_id": motor["id"],
55
+ "propeller_component_id": prop["id"],
56
+ }
57
+ ],
58
+ schedule={
59
+ "mode": "segments",
60
+ "segments": [
61
+ # Step to the ESC's working range first.
62
+ {
63
+ "duration_s": 0.2,
64
+ "airspeed_target": 0.0,
65
+ "per_group": {"main": {"throttle_target": 15, "throttle_ramp": "step"}},
66
+ },
67
+ # Then ramp from there to the hover setting.
68
+ {
69
+ "duration_s": 2.0,
70
+ "airspeed_target": 0.0,
71
+ "per_group": {"main": {"throttle_target": 65, "throttle_ramp": "linear"}},
72
+ },
73
+ # Hold until the pack is empty. `until_depleted` takes no duration.
74
+ {
75
+ "until_depleted": True,
76
+ "airspeed_target": 0.0,
77
+ "per_group": {"main": {"throttle_target": 65}},
78
+ },
79
+ ],
80
+ },
81
+ # The total mission time is not known in advance, so progress["fraction"]
82
+ # is an estimate rather than an exact ratio.
83
+ termination={"mode": "until_depleted", "soc_cutoff_pct": 20.0},
84
+ )
85
+ print(f"created {dyn['id']}, status={dyn['status']}")
86
+
87
+ # Watch progress. 2-5 s is the right cadence; anything faster spends requests
88
+ # on a value the worker only rewrites once per integration window.
89
+ CANCEL_AFTER_S = 45.0
90
+ deadline = time.monotonic() + CANCEL_AFTER_S
91
+
92
+ while time.monotonic() < deadline:
93
+ time.sleep(3.0)
94
+ run = client.dynamic_simulations.retrieve(dyn["id"])
95
+ if run["status"] not in ("queued", "running"):
96
+ break
97
+ # `progress` is None until the worker picks the run up.
98
+ p = run.get("progress")
99
+ if p is None:
100
+ print(f" {run['status']}, no progress yet")
101
+ continue
102
+ frac = p.get("fraction")
103
+ pct = f"{frac * 100:5.1f}%" if frac is not None else " ? %"
104
+ print(f" {pct} sim {p['sim_time_s']:.1f} s wall {p['wall_s']:.1f} s")
105
+
106
+ run = client.dynamic_simulations.retrieve(dyn["id"])
107
+ if run["status"] in ("queued", "running"):
108
+ # Cancel whatever state the deadline caught it in. A queued run comes back
109
+ # `canceled` immediately, with the compute units refunded and no result. A
110
+ # running one comes back still `running` with cancel_requested_at set, and
111
+ # the worker stops at its next integration window.
112
+ #
113
+ # Cancelling both matters here: with every execution slot busy this run can
114
+ # still be queued at the deadline, and skipping it would leave it to
115
+ # execute later against a script that has already exited.
116
+ accepted = client.dynamic_simulations.cancel(dyn["id"])
117
+ print(f"\ncancel accepted: status={accepted['status']}, "
118
+ f"cancel_requested_at={accepted.get('cancel_requested_at')}")
119
+ run = client.dynamic_simulations.wait(dyn["id"], timeout=300)
120
+
121
+ print(f"final status: {run['status']}")
122
+
123
+ blob = run.get("result")
124
+ if blob:
125
+ meta = blob["run_meta"]
126
+ if meta.get("partial"):
127
+ print(f"partial result, stopped at t = {meta['canceled_at_t_s']:.1f} s of mission time")
128
+ sc = blob["scorecard"]
129
+ print(f"flight time: {sc['flight_time_s']:.1f} s")
130
+ print(f"energy: {sc['energy_wh']:.1f} Wh")
131
+ print(f"peak winding: {sc['peak_winding_temp_c']:.1f} C")
132
+ print(f"samples returned: {len(blob['samples'])}")
@@ -0,0 +1,89 @@
1
+ # ArduPilot SITL — fly your exported powertrain
2
+
3
+ Fly a real ArduPilot mission where the vehicle physics is YOUR ThrustLab
4
+ powertrain: per-rotor thrust and torque, true rotor spin-up, battery sag and
5
+ state of charge — all from the `.fmu` you exported.
6
+
7
+ ## What you need
8
+
9
+ - An exported FMU (`examples/fmu/export_and_download.py`, or the dashboard's
10
+ Share menu → Export powertrain).
11
+ - `pip install thrustlab` — the SDK ships the JSON-backend physics bridge
12
+ (`thrustlab.sitl`).
13
+ - An ArduPilot SITL build (`./waf copter`), any recent Copter.
14
+ - `pip install pymavlink matplotlib numpy` for the mission script and plots.
15
+
16
+ ## Run it (three terminals)
17
+
18
+ 1. The bridge listens first, with your FMU and a vehicle layout. The rpm log
19
+ goes into `mission_out/` so the plot step finds every recording in one
20
+ place (create the directory first — the bridge won't):
21
+
22
+ ```sh
23
+ mkdir -p mission_out
24
+ python -m thrustlab.sitl \
25
+ --fmu ../validation-quad.fmu --vehicle quad_x.yaml \
26
+ --rpm-log mission_out/mission_rpm_log.csv
27
+ ```
28
+
29
+ `quad_x.yaml` maps servo outputs to rotors (positions, spin directions,
30
+ mass). Start from the annotated `../quad_5inch_x.yaml` and edit mass
31
+ and geometry to your airframe.
32
+
33
+ 2. ArduPilot with the JSON model — run from your ArduPilot checkout, and
34
+ point at this example directory's `fmu.parm` by absolute path:
35
+
36
+ ```sh
37
+ ./build/sitl/bin/arducopter --model JSON:127.0.0.1 \
38
+ --defaults Tools/autotest/default_params/copter.parm,/path/to/examples/fmu/ardupilot_sitl/fmu.parm -I0
39
+ ```
40
+
41
+ `fmu.parm` needs at least `BATT_MONITOR 4` (the bridge feeds pack voltage
42
+ and current back into the firmware's battery monitor).
43
+
44
+ 3. The mission:
45
+
46
+ ```sh
47
+ python fly_mission.py --out-dir ./mission_out
48
+ ```
49
+
50
+ It uploads a multi-leg AUTO mission (takeoff, 13 m/s dash, climb, loiter
51
+ turns, fast descent, RTL), flies it, and records MAVLink telemetry beside
52
+ the bridge's per-rotor log.
53
+
54
+ ## The plots
55
+
56
+ ```sh
57
+ python plot_mission.py ./mission_out ./mission_out/plots
58
+ ```
59
+
60
+ Three figures from the recorded flight: the mission track colored by
61
+ electrical power, the powertrain time series (per-rotor speed, bus voltage
62
+ and current, power and state of charge), and the loiter-exit close-up where
63
+ the mixer splits the four rotors.
64
+
65
+ The same recording also renders as an animated replay — the track draws
66
+ itself colored by power while live panels play the powertrain through the
67
+ mission:
68
+
69
+ ```sh
70
+ python render_mission_video.py ./mission_out ./mission_out/video
71
+ ```
72
+
73
+ Frames land in `video/frames/`; with ffmpeg on PATH the mp4 is encoded
74
+ automatically, otherwise the exact encode command is printed.
75
+
76
+ Per-rotor RPM never crosses the ArduPilot JSON protocol — the protocol has no
77
+ RPM lane — so the bridge logs it to `mission_out/mission_rpm_log.csv` for
78
+ reconciliation against flight logs. If you changed the vehicle mass in
79
+ `quad_x.yaml`, set `WEIGHT_N` at the top of `plot_mission.py` to match — it
80
+ aligns the two recordings' clocks on the takeoff event.
81
+
82
+ ## The bundled FMU
83
+
84
+ This example ships with a ready-to-fly FMU exported from ThrustLab
85
+ (`../validation-quad.fmu`). The exact powertrain it reproduces is documented
86
+ in the archive's own `resources/README.md`; unzip the FMU to read it. To
87
+ recreate the FMU, build the same powertrain in ThrustLab or through the
88
+ API/SDK, then export it with **Export → FMU** or `client.fmu`; your export
89
+ will fly these examples identically.
@@ -0,0 +1,225 @@
1
+ """Fly a multi-leg AUTO mission against a running ArduPilot SITL whose physics
2
+ is your exported ThrustLab FMU (via thrustlab.sitl), recording MAVLink
3
+ telemetry for the plot script.
4
+
5
+ Usage: python fly_mission.py [--connect tcp:127.0.0.1:5760] [--out-dir .]
6
+
7
+ Writes <out-dir>/mission_telemetry.csv and <out-dir>/mission_events.csv; the
8
+ per-rotor truth (rpm, thrust, bus V/I, SOC) is the bridge's own --rpm-log CSV.
9
+ """
10
+ import argparse
11
+ import csv
12
+ import math
13
+ import sys
14
+ import time
15
+ from pathlib import Path
16
+
17
+ from pymavlink import mavutil
18
+
19
+ M_PER_DEG_LAT = 111320.0
20
+ WALL_CAP_S = 900.0
21
+
22
+
23
+ def build_mission(home_lat, home_lon, speed_scale=1.0):
24
+ frame = mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT
25
+ mav = mavutil.mavlink
26
+ items = []
27
+
28
+ def spd(m_s):
29
+ return max(2.0, m_s * speed_scale)
30
+
31
+ def add(cmd, p1=0, p2=0, p3=0, p4=0, dn=0.0, de=0.0, alt=0.0):
32
+ lat = home_lat + dn / M_PER_DEG_LAT
33
+ lon = home_lon + de / (M_PER_DEG_LAT * math.cos(math.radians(home_lat)))
34
+ items.append((len(items), frame, cmd, p1, p2, p3, p4,
35
+ int(lat * 1e7), int(lon * 1e7), float(alt)))
36
+
37
+ add(mav.MAV_CMD_NAV_WAYPOINT) # home slot
38
+ add(mav.MAV_CMD_NAV_TAKEOFF, alt=10.0)
39
+ add(mav.MAV_CMD_DO_CHANGE_SPEED, p1=1, p2=spd(13.0))
40
+ add(mav.MAV_CMD_NAV_WAYPOINT, dn=220.0, de=0.0, alt=15.0) # fast dash
41
+ add(mav.MAV_CMD_DO_CHANGE_SPEED, p1=1, p2=spd(6.0))
42
+ add(mav.MAV_CMD_NAV_WAYPOINT, dn=220.0, de=140.0, alt=32.0) # slow climb
43
+ add(mav.MAV_CMD_NAV_LOITER_TURNS, p1=2, p3=25.0, dn=220.0, de=140.0, alt=32.0)
44
+ add(mav.MAV_CMD_DO_CHANGE_SPEED, p1=1, p2=spd(14.0))
45
+ add(mav.MAV_CMD_NAV_WAYPOINT, dn=60.0, de=60.0, alt=14.0) # fast descent
46
+ add(mav.MAV_CMD_DO_CHANGE_SPEED, p1=1, p2=spd(5.0))
47
+ add(mav.MAV_CMD_NAV_WAYPOINT, dn=25.0, de=0.0, alt=8.0) # slow approach
48
+ add(mav.MAV_CMD_NAV_RETURN_TO_LAUNCH)
49
+ return items
50
+
51
+
52
+ def upload_mission(m, items):
53
+ m.mav.mission_clear_all_send(m.target_system, m.target_component)
54
+ m.recv_match(type="MISSION_ACK", blocking=True, timeout=5)
55
+ m.mav.mission_count_send(m.target_system, m.target_component, len(items))
56
+ t0 = time.time()
57
+ while time.time() - t0 < 60:
58
+ msg = m.recv_match(
59
+ type=["MISSION_REQUEST", "MISSION_REQUEST_INT", "MISSION_ACK"],
60
+ blocking=True, timeout=10,
61
+ )
62
+ if msg is None:
63
+ raise RuntimeError("mission upload stalled")
64
+ if msg.get_type() == "MISSION_ACK":
65
+ if msg.type == mavutil.mavlink.MAV_MISSION_ACCEPTED:
66
+ print(f"mission accepted ({len(items)} items)", flush=True)
67
+ return
68
+ raise RuntimeError(f"mission rejected: ack type {msg.type}")
69
+ s, frame, cmd, p1, p2, p3, p4, lat, lon, alt = items[msg.seq]
70
+ m.mav.mission_item_int_send(
71
+ m.target_system, m.target_component, s, frame, cmd,
72
+ 1 if s == 0 else 0, 1, p1, p2, p3, p4, lat, lon, alt,
73
+ )
74
+ raise RuntimeError("mission upload timed out")
75
+
76
+
77
+ def main():
78
+ ap = argparse.ArgumentParser()
79
+ ap.add_argument("--connect", default="tcp:127.0.0.1:5760")
80
+ ap.add_argument("--out-dir", type=Path, default=Path("."))
81
+ ap.add_argument("--speed-scale", type=float, default=1.0,
82
+ help="scale every mission leg speed (floor 2 m/s). A coax "
83
+ "export's edgewise envelope is tighter than a solo "
84
+ "rotor's; ~0.3 keeps a Y6 inside it.")
85
+ args = ap.parse_args()
86
+ args.out_dir.mkdir(parents=True, exist_ok=True)
87
+
88
+ m = mavutil.mavlink_connection(args.connect, source_system=250)
89
+ m.wait_heartbeat(timeout=120)
90
+ print(f"heartbeat from sys {m.target_system}", flush=True)
91
+
92
+ # Let the EKF settle; keep the streams coming at 10 Hz.
93
+ t0 = time.time()
94
+ while time.time() - t0 < 40:
95
+ m.mav.request_data_stream_send(
96
+ m.target_system, m.target_component,
97
+ mavutil.mavlink.MAV_DATA_STREAM_ALL, 10, 1,
98
+ )
99
+ st = m.recv_match(type="STATUSTEXT", blocking=False)
100
+ if st:
101
+ print("statustext:", st.text, flush=True)
102
+ time.sleep(0.5)
103
+
104
+ # Home = where the vehicle sits now.
105
+ gpi = m.recv_match(type="GLOBAL_POSITION_INT", blocking=True, timeout=10)
106
+ home_lat, home_lon = gpi.lat / 1e7, gpi.lon / 1e7
107
+ upload_mission(m, build_mission(home_lat, home_lon, args.speed_scale))
108
+
109
+ m.set_mode("GUIDED")
110
+ # GUIDED refuses to arm until the EKF is actually fusing GPS, and how
111
+ # long that takes in SIM time depends on the machine (a heavy FMU lowers
112
+ # the effective speedup). Retry instead of racing, and surface every
113
+ # refusal text instead of hanging in motors_armed_wait().
114
+ arm_deadline = time.time() + 300
115
+ armed = False
116
+ while not armed:
117
+ if time.time() > arm_deadline:
118
+ raise RuntimeError("arming timed out — see the refusal texts above")
119
+ m.arducopter_arm()
120
+ drain_until = time.time() + 5.0
121
+ while time.time() < drain_until:
122
+ msg = m.recv_match(type=["HEARTBEAT", "STATUSTEXT"],
123
+ blocking=True, timeout=1)
124
+ if msg is None:
125
+ continue
126
+ if msg.get_type() == "STATUSTEXT":
127
+ print("statustext:", msg.text, flush=True)
128
+ elif (msg.get_srcSystem() == m.target_system
129
+ and msg.base_mode
130
+ & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED):
131
+ armed = True
132
+ break
133
+ print("armed", flush=True)
134
+ m.set_mode("AUTO")
135
+ for _ in range(10):
136
+ m.mav.command_long_send(
137
+ m.target_system, m.target_component,
138
+ mavutil.mavlink.MAV_CMD_MISSION_START, 0, 0, 0, 0, 0, 0, 0, 0,
139
+ )
140
+ ack = m.recv_match(type="COMMAND_ACK", blocking=True, timeout=3)
141
+ if ack and ack.command == mavutil.mavlink.MAV_CMD_MISSION_START:
142
+ if ack.result == mavutil.mavlink.MAV_RESULT_ACCEPTED:
143
+ break
144
+ time.sleep(1)
145
+
146
+ tele = open(args.out_dir / "mission_telemetry.csv", "w", newline="")
147
+ tw = csv.writer(tele)
148
+ tw.writerow(["t_s", "lat", "lon", "alt_rel_m", "vn_m_s", "ve_m_s", "vd_m_s",
149
+ "groundspeed_m_s", "climb_m_s", "throttle_pct",
150
+ "roll_deg", "pitch_deg", "yaw_deg",
151
+ "voltage_V", "current_A", "battery_pct", "mode"])
152
+ ev = open(args.out_dir / "mission_events.csv", "w", newline="")
153
+ ew = csv.writer(ev)
154
+ ew.writerow(["t_s", "kind", "detail"])
155
+
156
+ last = {"vfr": None, "att": None, "sys": None, "mode": None, "gpi": None}
157
+ armed, was_flying, rows = True, False, 0
158
+ wall0 = time.time()
159
+
160
+ def now_s():
161
+ return last["gpi"].time_boot_ms / 1000.0 if last["gpi"] else 0.0
162
+
163
+ while time.time() - wall0 < WALL_CAP_S:
164
+ msg = m.recv_match(blocking=True, timeout=10)
165
+ if msg is None:
166
+ continue
167
+ t = msg.get_type()
168
+ if t == "VFR_HUD":
169
+ last["vfr"] = msg
170
+ elif t == "ATTITUDE":
171
+ last["att"] = msg
172
+ elif t == "SYS_STATUS":
173
+ last["sys"] = msg
174
+ elif t == "HEARTBEAT":
175
+ mode = mavutil.mode_string_v10(msg)
176
+ if mode != last["mode"]:
177
+ ew.writerow([f"{now_s():.2f}", "mode", mode])
178
+ print(f"mode -> {mode}", flush=True)
179
+ last["mode"] = mode
180
+ armed = bool(msg.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED)
181
+ if was_flying and not armed:
182
+ print("disarmed after flight — mission complete", flush=True)
183
+ break
184
+ elif t == "MISSION_ITEM_REACHED":
185
+ ew.writerow([f"{now_s():.2f}", "wp", msg.seq])
186
+ print(f"reached wp {msg.seq}", flush=True)
187
+ elif t == "STATUSTEXT":
188
+ ew.writerow([f"{now_s():.2f}", "text", msg.text])
189
+ elif t == "GLOBAL_POSITION_INT":
190
+ last["gpi"] = msg
191
+ if msg.relative_alt / 1000.0 > 2.0:
192
+ was_flying = True
193
+ vfr, att, s = last["vfr"], last["att"], last["sys"]
194
+ tw.writerow([
195
+ f"{msg.time_boot_ms / 1000.0:.2f}", msg.lat / 1e7, msg.lon / 1e7,
196
+ f"{msg.relative_alt / 1000.0:.2f}",
197
+ f"{msg.vx / 100.0:.2f}", f"{msg.vy / 100.0:.2f}", f"{msg.vz / 100.0:.2f}",
198
+ f"{vfr.groundspeed:.2f}" if vfr else "",
199
+ f"{vfr.climb:.2f}" if vfr else "",
200
+ vfr.throttle if vfr else "",
201
+ f"{math.degrees(att.roll):.1f}" if att else "",
202
+ f"{math.degrees(att.pitch):.1f}" if att else "",
203
+ f"{math.degrees(att.yaw):.1f}" if att else "",
204
+ f"{s.voltage_battery / 1000.0:.3f}" if s else "",
205
+ f"{s.current_battery / 100.0:.2f}" if s else "",
206
+ s.battery_remaining if s else "",
207
+ last["mode"] or "",
208
+ ])
209
+ rows += 1
210
+
211
+ tele.close()
212
+ ev.close()
213
+ print(f"telemetry rows: {rows}", flush=True)
214
+ if not was_flying:
215
+ print("FAIL: never left the ground", flush=True)
216
+ return 2
217
+ if armed:
218
+ print("FAIL: still armed at wall cap", flush=True)
219
+ return 1
220
+ print("MISSION COMPLETE", flush=True)
221
+ return 0
222
+
223
+
224
+ if __name__ == "__main__":
225
+ sys.exit(main())
@@ -0,0 +1,6 @@
1
+ # Parameters the FMU bridge flight needs on top of copter.parm.
2
+ FRAME_CLASS 1
3
+ FRAME_TYPE 1
4
+ # The bridge feeds pack voltage/current back over the JSON protocol; monitor
5
+ # type 4 (analog V+I) is how the firmware reads them.
6
+ BATT_MONITOR 4