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.
- {thrustlab-0.4.0 → thrustlab-0.6.0}/CHANGELOG.md +59 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/PKG-INFO +34 -2
- {thrustlab-0.4.0 → thrustlab-0.6.0}/README.md +28 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/components/create_component.py +2 -2
- thrustlab-0.6.0/examples/dynamic/watch_and_cancel.py +132 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl/README.md +89 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl/fly_mission.py +225 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl/fmu.parm +6 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl/plot_mission.py +267 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl/quad_x.yaml +62 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl/render_mission_video.py +454 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl_tilttri/README.md +85 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl_tilttri/fly_transition.py +211 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl_tilttri/tilttri.parm +30 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl_tilttri/tilttri.yaml +128 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl_y6/README.md +75 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl_y6/plot_coax_split.py +131 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl_y6/y6.parm +6 -0
- thrustlab-0.6.0/examples/fmu/ardupilot_sitl_y6/y6.yaml +107 -0
- thrustlab-0.6.0/examples/fmu/betaflight_sitl/README.md +251 -0
- thrustlab-0.6.0/examples/fmu/betaflight_sitl/betaflight_config.txt +41 -0
- thrustlab-0.6.0/examples/fmu/betaflight_sitl/betaflight_quad_x.yaml +129 -0
- thrustlab-0.6.0/examples/fmu/betaflight_sitl/fly_hover.py +697 -0
- thrustlab-0.6.0/examples/fmu/endurance/README.md +40 -0
- thrustlab-0.6.0/examples/fmu/endurance/hover_endurance.py +215 -0
- thrustlab-0.6.0/examples/fmu/export_and_download.py +80 -0
- thrustlab-0.6.0/examples/fmu/gazebo/README.md +72 -0
- thrustlab-0.6.0/examples/fmu/gazebo/quad_fmu.sdf +373 -0
- thrustlab-0.6.0/examples/fmu/quad_5inch_x.yaml +142 -0
- thrustlab-0.6.0/examples/fmu/simulink/.gitignore +2 -0
- thrustlab-0.6.0/examples/fmu/simulink/README.md +53 -0
- thrustlab-0.6.0/examples/fmu/simulink/fmu_cosim.png +0 -0
- thrustlab-0.6.0/examples/fmu/simulink/run_fmu_simulink.m +93 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/outputs/read_dynamic.py +18 -6
- thrustlab-0.6.0/examples/sweeps/component_axis_grid.py +85 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/pyproject.toml +18 -1
- thrustlab-0.6.0/tests/sitl/__init__.py +1 -0
- thrustlab-0.6.0/tests/sitl/ardupilot_parser.py +160 -0
- thrustlab-0.6.0/tests/sitl/betaflight_fc.py +119 -0
- thrustlab-0.6.0/tests/sitl/conftest.py +47 -0
- thrustlab-0.6.0/tests/sitl/mock_fmu.py +249 -0
- thrustlab-0.6.0/tests/sitl/test_betaflight_bridge.py +149 -0
- thrustlab-0.6.0/tests/sitl/test_betaflight_protocol.py +329 -0
- thrustlab-0.6.0/tests/sitl/test_kinematics.py +283 -0
- thrustlab-0.6.0/tests/sitl/test_protocol.py +222 -0
- thrustlab-0.6.0/tests/sitl/test_rigidbody.py +250 -0
- thrustlab-0.6.0/tests/sitl/test_state_machine.py +256 -0
- thrustlab-0.6.0/tests/sitl/test_step_path.py +487 -0
- thrustlab-0.6.0/tests/sitl/test_tilt_wing.py +315 -0
- thrustlab-0.6.0/tests/sitl/test_vehicle.py +273 -0
- thrustlab-0.6.0/tests/test_resources/test_fmu.py +148 -0
- thrustlab-0.6.0/tests/test_resources/test_simulations_cancel_batch.py +62 -0
- thrustlab-0.6.0/tests/test_resources/test_simulations_run_batch.py +80 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_http.py +7 -3
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/__init__.py +30 -0
- thrustlab-0.6.0/thrustlab/_models/cancel_batch_result.py +97 -0
- thrustlab-0.6.0/thrustlab/_models/cancel_selected_body.py +42 -0
- thrustlab-0.6.0/thrustlab/_models/cancel_skipped.py +71 -0
- thrustlab-0.6.0/thrustlab/_models/cancel_skipped_reason.py +10 -0
- thrustlab-0.6.0/thrustlab/_models/component_filter_bounds.py +97 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_ids_response.py +27 -3
- thrustlab-0.6.0/thrustlab/_models/component_ids_response_filters.py +63 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_list_response.py +19 -0
- thrustlab-0.6.0/thrustlab/_models/component_list_response_filters.py +63 -0
- thrustlab-0.6.0/thrustlab/_models/component_specs_response.py +101 -0
- thrustlab-0.6.0/thrustlab/_models/component_specs_response_spec_json.py +47 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_sweep_axis_in.py +1 -1
- thrustlab-0.6.0/thrustlab/_models/concurrency_resource.py +108 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_balance_resource.py +1 -1
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_event_resource.py +1 -1
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_summary_resource.py +1 -1
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource.py +87 -4
- thrustlab-0.6.0/thrustlab/_models/dynamic_resource_progress_type_0.py +47 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/entitlements_resource.py +12 -0
- thrustlab-0.6.0/thrustlab/_models/fmu_export_accepted_response.py +82 -0
- thrustlab-0.6.0/thrustlab/_models/fmu_export_job_error.py +69 -0
- thrustlab-0.6.0/thrustlab/_models/fmu_export_status_response.py +393 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource.py +67 -31
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_create_body.py +6 -3
- thrustlab-0.6.0/thrustlab/_models/sweep_credits_breakdown.py +73 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_param_range_in.py +12 -6
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource.py +122 -31
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/user_resource.py +29 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_version.py +1 -1
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/client.py +3 -1
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/dynamic_simulations.py +6 -2
- thrustlab-0.6.0/thrustlab/resources/fmu.py +147 -0
- thrustlab-0.6.0/thrustlab/resources/simulations.py +186 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/sweeps.py +1 -1
- thrustlab-0.6.0/thrustlab/sitl/README.md +362 -0
- thrustlab-0.6.0/thrustlab/sitl/__init__.py +93 -0
- thrustlab-0.6.0/thrustlab/sitl/__main__.py +10 -0
- thrustlab-0.6.0/thrustlab/sitl/bridge.py +173 -0
- thrustlab-0.6.0/thrustlab/sitl/cli.py +217 -0
- thrustlab-0.6.0/thrustlab/sitl/fmu.py +494 -0
- thrustlab-0.6.0/thrustlab/sitl/kinematics.py +147 -0
- thrustlab-0.6.0/thrustlab/sitl/model.py +66 -0
- thrustlab-0.6.0/thrustlab/sitl/protocol.py +240 -0
- thrustlab-0.6.0/thrustlab/sitl/rigidbody.py +439 -0
- thrustlab-0.6.0/thrustlab/sitl/rpmlog.py +115 -0
- thrustlab-0.6.0/thrustlab/sitl/targets/__init__.py +42 -0
- thrustlab-0.6.0/thrustlab/sitl/targets/ardupilot.py +221 -0
- thrustlab-0.6.0/thrustlab/sitl/targets/base.py +292 -0
- thrustlab-0.6.0/thrustlab/sitl/targets/betaflight.py +714 -0
- thrustlab-0.6.0/thrustlab/sitl/vehicle.py +687 -0
- thrustlab-0.4.0/thrustlab/resources/simulations.py +0 -87
- {thrustlab-0.4.0 → thrustlab-0.6.0}/.gitignore +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/LICENSE +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/NOTICE +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/dynamic/run_and_poll.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/outputs/read_steady_state.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/projects/create.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/projects/list.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/projects/retrieve.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/quickstart.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/simulations/run_sync.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/simulations/wait.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/sweeps/run_and_poll.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/examples/webhooks/verify_handler.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/__init__.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/conftest.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_async_polling.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_client.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_components_create.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_components_find.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_dynamic_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_exceptions.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_http.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_pagination.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/__init__.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_components.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_compute_units.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_projects.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_simulations.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_starred_components.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_submissions.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_sweeps.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_users.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_resources/test_webhook_endpoints.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_spec_coverage.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/tests/test_webhooks.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/__init__.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_async_polling.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/airfoil_breakpoint.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/airfoil_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/airfoil_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_request.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_request_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_request_target_type_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_response_envelope_limiting_factor_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_geometry_response_envelope_status_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/analyze_station_row.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/body_import_airfoil_v1_airfoils_import_post.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/calibrate_accepted_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_brand.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_brands_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_create_body_spec_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_patch_spec_json_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_resource_spec_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_sweep_axis_in_axis.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_write_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_write_response_spec_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/component_write_warning.py +0 -0
- {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
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_bucket_breakdown.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_bucket_breakdown_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_event_resource_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_event_resource_unit_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_related_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_related_resource_object.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/credit_usage_summary_resource_window.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/design_request.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/design_request_target_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_estimate_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_estimate_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_battery_topology_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_display_labels_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_error_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_input_snapshot_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_result_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_resource_status.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_group_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_group_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_group_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_group_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/dynamic_rotor_in_rotation_sense.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/export_geometry_request.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/export_geometry_request_format.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/export_geometry_request_rotation_type_0.py +0 -0
- {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
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/generate_geometry_request.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/geometry_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/geometry_style_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/geometry_styles_list_response.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/http_validation_error.py +0 -0
- {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
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/list_events_v1_events_get_response_list_events_v1_events_get.py +0 -0
- {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
- {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
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/pack_leaf_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/pack_parallel_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/pack_series_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/pack_topology_in.py +0 -0
- {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
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/project_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/project_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/project_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/project_resource.py +0 -0
- {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
- {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
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_group_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_group_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_group_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_group_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_group_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/rotor_in_rotation_sense.py +0 -0
- {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
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/run_selected_body.py +0 -0
- {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
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/schedule_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/schedule_in_interpolation_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/schedule_in_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_group_command_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_group_command_in_throttle_ramp_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_group_command_in_tilt_ramp_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_in_airspeed_ramp.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_in_per_group.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_in_throttle_ramp.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/segment_in_vertical_speed_ramp.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/share_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_cancel_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_create_body_cooling_source_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_create_body_flight_regime.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_create_body_inflow_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_create_body_launch_intent.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_inputs.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_patch_plot_config_json_type_0_item.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_battery_topology_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_display_labels_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_error_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_input_snapshot_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_plot_config_json_type_0_item.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_result_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_result_type_0_additional_property.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/simulation_resource_status.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/spline_data.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/starred_component_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/starred_component_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/starred_component_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_create_body_data_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_patch_data_json_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/submission_resource_data_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_cancel_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_config_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_config_in_esc_timing_values_type_0_item.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_create_body_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_create_body_inflow_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_create_body_launch_intent.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_inputs.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_param_range_in_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_patch_plot_config_json_type_0_item.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_component_selection.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_inputs.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_list_response_display_labels_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_resource_rotors_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_point_resource_rotors_type_0_additional_property.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_error_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_input_snapshot_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_plot_config_json_type_0_item.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_status.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_summary_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_resource_sweep_config_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_group_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_group_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_group_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_group_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/sweep_rotor_in_rotation_sense.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/termination_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/termination_in_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/types.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/user_resource_gate_required_tiers.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/user_resource_unit_system.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/v1_custom_component_override.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/v1_custom_component_override_spec_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/v1_health_v1_health_get_response_v1_health_v1_health_get.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/validation_error.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/webhook_endpoint_create.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_models/webhook_endpoint_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/_pagination.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/exceptions.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/__init__.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/_base.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/components.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/compute_units.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/projects.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/starred_components.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/submissions.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/users.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.6.0}/thrustlab/resources/webhook_endpoints.py +0 -0
- {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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: thrustlab
|
|
3
|
-
Version: 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
|
|
22
|
-
|
|
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())
|