thrustlab 0.4.0__tar.gz → 0.5.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.5.0}/CHANGELOG.md +23 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/PKG-INFO +6 -2
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl/README.md +80 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl/fly_mission.py +225 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl/fmu.parm +6 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl/plot_mission.py +267 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl/quad_x.yaml +62 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl/render_mission_video.py +454 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl_tilttri/README.md +76 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl_tilttri/fly_transition.py +211 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl_tilttri/tilttri.parm +30 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl_tilttri/tilttri.yaml +128 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl_y6/README.md +62 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl_y6/plot_coax_split.py +131 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl_y6/y6.parm +6 -0
- thrustlab-0.5.0/examples/fmu/ardupilot_sitl_y6/y6.yaml +107 -0
- thrustlab-0.5.0/examples/fmu/betaflight_sitl/README.md +242 -0
- thrustlab-0.5.0/examples/fmu/betaflight_sitl/betaflight_config.txt +41 -0
- thrustlab-0.5.0/examples/fmu/betaflight_sitl/betaflight_quad_x.yaml +129 -0
- thrustlab-0.5.0/examples/fmu/betaflight_sitl/fly_hover.py +697 -0
- thrustlab-0.5.0/examples/fmu/endurance/README.md +31 -0
- thrustlab-0.5.0/examples/fmu/endurance/hover_endurance.py +215 -0
- thrustlab-0.5.0/examples/fmu/export_and_download.py +80 -0
- thrustlab-0.5.0/examples/fmu/gazebo/README.md +63 -0
- thrustlab-0.5.0/examples/fmu/gazebo/quad_fmu.sdf +373 -0
- thrustlab-0.5.0/examples/fmu/quad_5inch_x.yaml +142 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/outputs/read_dynamic.py +18 -6
- {thrustlab-0.4.0 → thrustlab-0.5.0}/pyproject.toml +15 -1
- thrustlab-0.5.0/tests/sitl/__init__.py +1 -0
- thrustlab-0.5.0/tests/sitl/ardupilot_parser.py +160 -0
- thrustlab-0.5.0/tests/sitl/betaflight_fc.py +119 -0
- thrustlab-0.5.0/tests/sitl/conftest.py +47 -0
- thrustlab-0.5.0/tests/sitl/mock_fmu.py +249 -0
- thrustlab-0.5.0/tests/sitl/test_betaflight_bridge.py +149 -0
- thrustlab-0.5.0/tests/sitl/test_betaflight_protocol.py +329 -0
- thrustlab-0.5.0/tests/sitl/test_kinematics.py +283 -0
- thrustlab-0.5.0/tests/sitl/test_protocol.py +222 -0
- thrustlab-0.5.0/tests/sitl/test_rigidbody.py +250 -0
- thrustlab-0.5.0/tests/sitl/test_state_machine.py +256 -0
- thrustlab-0.5.0/tests/sitl/test_step_path.py +487 -0
- thrustlab-0.5.0/tests/sitl/test_tilt_wing.py +315 -0
- thrustlab-0.5.0/tests/sitl/test_vehicle.py +273 -0
- thrustlab-0.5.0/tests/test_resources/test_fmu.py +148 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_http.py +7 -3
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/__init__.py +10 -0
- thrustlab-0.5.0/thrustlab/_models/component_specs_response.py +101 -0
- thrustlab-0.5.0/thrustlab/_models/component_specs_response_spec_json.py +47 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_resource.py +2 -2
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/entitlements_resource.py +12 -0
- thrustlab-0.5.0/thrustlab/_models/fmu_export_accepted_response.py +82 -0
- thrustlab-0.5.0/thrustlab/_models/fmu_export_job_error.py +69 -0
- thrustlab-0.5.0/thrustlab/_models/fmu_export_status_response.py +393 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_resource.py +2 -2
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_resource.py +2 -2
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_version.py +1 -1
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/client.py +3 -1
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/dynamic_simulations.py +6 -2
- thrustlab-0.5.0/thrustlab/resources/fmu.py +147 -0
- thrustlab-0.5.0/thrustlab/sitl/README.md +362 -0
- thrustlab-0.5.0/thrustlab/sitl/__init__.py +93 -0
- thrustlab-0.5.0/thrustlab/sitl/__main__.py +10 -0
- thrustlab-0.5.0/thrustlab/sitl/bridge.py +173 -0
- thrustlab-0.5.0/thrustlab/sitl/cli.py +217 -0
- thrustlab-0.5.0/thrustlab/sitl/fmu.py +494 -0
- thrustlab-0.5.0/thrustlab/sitl/kinematics.py +147 -0
- thrustlab-0.5.0/thrustlab/sitl/model.py +66 -0
- thrustlab-0.5.0/thrustlab/sitl/protocol.py +240 -0
- thrustlab-0.5.0/thrustlab/sitl/rigidbody.py +439 -0
- thrustlab-0.5.0/thrustlab/sitl/rpmlog.py +115 -0
- thrustlab-0.5.0/thrustlab/sitl/targets/__init__.py +42 -0
- thrustlab-0.5.0/thrustlab/sitl/targets/ardupilot.py +221 -0
- thrustlab-0.5.0/thrustlab/sitl/targets/base.py +292 -0
- thrustlab-0.5.0/thrustlab/sitl/targets/betaflight.py +714 -0
- thrustlab-0.5.0/thrustlab/sitl/vehicle.py +687 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/.gitignore +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/LICENSE +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/NOTICE +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/README.md +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/components/create_component.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/dynamic/run_and_poll.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/outputs/read_steady_state.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/projects/create.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/projects/list.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/projects/retrieve.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/quickstart.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/simulations/run_sync.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/simulations/wait.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/sweeps/run_and_poll.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/examples/webhooks/verify_handler.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/__init__.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/conftest.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_async_polling.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_client.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_components_create.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_components_find.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_dynamic_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_exceptions.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_http.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_pagination.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_resources/__init__.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_resources/test_components.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_resources/test_compute_units.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_resources/test_projects.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_resources/test_simulations.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_resources/test_starred_components.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_resources/test_submissions.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_resources/test_sweeps.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_resources/test_users.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_resources/test_webhook_endpoints.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_spec_coverage.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/tests/test_webhooks.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/__init__.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_async_polling.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/airfoil_breakpoint.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/airfoil_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/airfoil_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/analyze_geometry_request.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/analyze_geometry_request_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/analyze_geometry_request_target_type_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/analyze_geometry_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/analyze_geometry_response_envelope_limiting_factor_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/analyze_geometry_response_envelope_status_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/analyze_station_row.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/body_import_airfoil_v1_airfoils_import_post.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/calibrate_accepted_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_brand.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_brands_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_create_body_spec_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_ids_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_patch_spec_json_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_resource_spec_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_sweep_axis_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_sweep_axis_in_axis.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_write_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_write_response_spec_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/component_write_warning.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.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.5.0}/thrustlab/_models/credit_balance_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/credit_bucket_breakdown.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/credit_bucket_breakdown_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/credit_usage_event_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/credit_usage_event_resource_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/credit_usage_event_resource_unit_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/credit_usage_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/credit_usage_related_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/credit_usage_related_resource_object.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/credit_usage_summary_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/credit_usage_summary_resource_window.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/design_request.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/design_request_target_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_estimate_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_estimate_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_resource_battery_topology_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_resource_display_labels_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_resource_error_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_resource_input_snapshot_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_resource_result_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_resource_status.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_rotor_group_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_rotor_group_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_rotor_group_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_rotor_group_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_rotor_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_rotor_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_rotor_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_rotor_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/dynamic_rotor_in_rotation_sense.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/export_geometry_request.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/export_geometry_request_format.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/export_geometry_request_rotation_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.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.5.0}/thrustlab/_models/generate_geometry_request.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/geometry_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/geometry_style_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/geometry_styles_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.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.5.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.5.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.5.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.5.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.5.0}/thrustlab/_models/http_validation_error.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.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.5.0}/thrustlab/_models/list_events_v1_events_get_response_list_events_v1_events_get.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.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.5.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.5.0}/thrustlab/_models/pack_leaf_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/pack_parallel_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/pack_series_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/pack_topology_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.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.5.0}/thrustlab/_models/project_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/project_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/project_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/project_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.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.5.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.5.0}/thrustlab/_models/rotor_group_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/rotor_group_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/rotor_group_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/rotor_group_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/rotor_group_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/rotor_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/rotor_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/rotor_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/rotor_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/rotor_in_rotation_sense.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.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.5.0}/thrustlab/_models/run_selected_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.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.5.0}/thrustlab/_models/schedule_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/schedule_in_interpolation_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/schedule_in_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/segment_group_command_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/segment_group_command_in_throttle_ramp_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/segment_group_command_in_tilt_ramp_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/segment_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/segment_in_airspeed_ramp.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/segment_in_per_group.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/segment_in_throttle_ramp.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/segment_in_vertical_speed_ramp.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/share_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_cancel_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_create_body_cooling_source_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_create_body_flight_regime.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_create_body_inflow_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_create_body_launch_intent.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_inputs.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_patch_plot_config_json_type_0_item.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_resource_battery_topology_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_resource_display_labels_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_resource_error_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_resource_input_snapshot_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_resource_plot_config_json_type_0_item.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_resource_result_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_resource_result_type_0_additional_property.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/simulation_resource_status.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/spline_data.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/starred_component_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/starred_component_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/starred_component_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/submission_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/submission_create_body_data_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/submission_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/submission_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/submission_patch_data_json_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/submission_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/submission_resource_data_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_cancel_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_config_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_config_in_esc_timing_values_type_0_item.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_create_body.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_create_body_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_create_body_inflow_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_create_body_launch_intent.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_inputs.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_param_range_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_param_range_in_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_patch_plot_config_json_type_0_item.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_point_component_selection.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_point_inputs.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_point_list_response.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_point_list_response_display_labels_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_point_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_point_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_point_resource_rotors_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_point_resource_rotors_type_0_additional_property.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_resource_error_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_resource_input_snapshot_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_resource_plot_config_json_type_0_item.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_resource_status.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_resource_summary_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_resource_sweep_config_type_0.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_rotor_group_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_rotor_group_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_rotor_group_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_rotor_group_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_rotor_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_rotor_in_esc_timing.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_rotor_in_esc_type.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_rotor_in_motor_cooling_source.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/sweep_rotor_in_rotation_sense.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/termination_in.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/termination_in_mode.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/types.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/user_resource.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/user_resource_gate_required_tiers.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/user_resource_unit_system.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/v1_custom_component_override.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/v1_custom_component_override_spec_json.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/v1_health_v1_health_get_response_v1_health_v1_health_get.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/validation_error.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/webhook_endpoint_create.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_models/webhook_endpoint_patch.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/_pagination.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/exceptions.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/__init__.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/_base.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/components.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/compute_units.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/projects.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/simulations.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/starred_components.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/submissions.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/sweeps.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/users.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/resources/webhook_endpoints.py +0 -0
- {thrustlab-0.4.0 → thrustlab-0.5.0}/thrustlab/webhooks.py +0 -0
|
@@ -8,6 +8,29 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.5.0] - 2026-08-31
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `client.fmu` can export a completed simulation as an FMI 3.0 co-simulation
|
|
16
|
+
FMU with per-rotor inputs and outputs, battery and thermal state, and a
|
|
17
|
+
validity envelope. Exports can be enqueued, checked with paced polling,
|
|
18
|
+
downloaded when ready, or canceled.
|
|
19
|
+
- The new `thrustlab.sitl` module flies exported FMUs in ArduPilot SITL and
|
|
20
|
+
Betaflight SITL using a six-degree-of-freedom rigid body, per-rotor servo
|
|
21
|
+
mapping, and battery voltage, current, and airspeed feedback to the firmware.
|
|
22
|
+
The `thrustlab-sitl` console command launches the bridge.
|
|
23
|
+
- The new `examples/fmu` gallery includes quadcopter and Y6 SITL missions, an
|
|
24
|
+
endurance study, and replay rendering.
|
|
25
|
+
- Generated models now cover component datasheets and the `component_specs`
|
|
26
|
+
entitlement flag.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- NumPy, PyYAML, and FMPy are now included in the core installation, so
|
|
31
|
+
`pip install thrustlab` provides everything needed for SITL. The `[sitl]`
|
|
32
|
+
extra remains available as an empty compatibility alias.
|
|
33
|
+
|
|
11
34
|
## [0.4.0] - 2026-08-27
|
|
12
35
|
|
|
13
36
|
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.5.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
|
|
@@ -0,0 +1,80 @@
|
|
|
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 my-powertrain.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.
|
|
@@ -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,267 @@
|
|
|
1
|
+
"""Render a recorded FMU mission into flight-report PNGs.
|
|
2
|
+
|
|
3
|
+
Reads the bridge's per-rotor CSV (FMU truth: rpm, thrust, bus V/I, SOC) and
|
|
4
|
+
the mission telemetry/events CSVs written by fly_mission.py, aligns the two
|
|
5
|
+
clocks on the takeoff event, and renders three figures:
|
|
6
|
+
|
|
7
|
+
fmu_mission_track.png top-down track colored by electrical power + altitude band
|
|
8
|
+
fmu_mission_timeseries.png the powertrain story over time (rpm, V/I, power, SOC)
|
|
9
|
+
fmu_mission_loiter.png loiter close-up: per-rotor rpm split vs roll
|
|
10
|
+
|
|
11
|
+
Usage: python plot_mission.py <data_dir> <out_dir>
|
|
12
|
+
<data_dir> holds mission_rpm_log.csv (from the bridge's --rpm-log),
|
|
13
|
+
mission_telemetry.csv and mission_events.csv (from fly_mission.py).
|
|
14
|
+
"""
|
|
15
|
+
import csv
|
|
16
|
+
import math
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import matplotlib
|
|
21
|
+
|
|
22
|
+
matplotlib.use("Agg")
|
|
23
|
+
import matplotlib.pyplot as plt
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
# Gruvbox Soft (dark) — frontend/src/app/gruvbox-soft.css
|
|
27
|
+
BG = "#111111"
|
|
28
|
+
SURFACE = "#1b1b1b"
|
|
29
|
+
TEXT = "#f5efe2"
|
|
30
|
+
MUTED = "#bdb0a0"
|
|
31
|
+
AXIS = "#d4be98"
|
|
32
|
+
GRID = (1.0, 1.0, 1.0, 0.10)
|
|
33
|
+
ORANGE = "#e78a4e"
|
|
34
|
+
BLUE = "#7daea3"
|
|
35
|
+
GREEN = "#a9b665"
|
|
36
|
+
PURPLE = "#d3869b"
|
|
37
|
+
AQUA = "#89b482"
|
|
38
|
+
YELLOW = "#d8a657"
|
|
39
|
+
RED = "#ea6962"
|
|
40
|
+
ROTORS = [ORANGE, BLUE, GREEN, PURPLE]
|
|
41
|
+
|
|
42
|
+
# Your vehicle's weight in newtons (mass_kg * 9.81) — used only to detect
|
|
43
|
+
# takeoff in the bridge log for clock alignment between the two recordings.
|
|
44
|
+
WEIGHT_N = 11.77
|
|
45
|
+
M_PER_DEG_LAT = 111320.0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def style():
|
|
49
|
+
plt.rcParams.update(
|
|
50
|
+
{
|
|
51
|
+
"figure.facecolor": BG,
|
|
52
|
+
"axes.facecolor": BG,
|
|
53
|
+
"savefig.facecolor": BG,
|
|
54
|
+
"text.color": TEXT,
|
|
55
|
+
"axes.edgecolor": AXIS,
|
|
56
|
+
"axes.labelcolor": MUTED,
|
|
57
|
+
"xtick.color": MUTED,
|
|
58
|
+
"ytick.color": MUTED,
|
|
59
|
+
"axes.grid": True,
|
|
60
|
+
"grid.color": GRID,
|
|
61
|
+
"grid.linewidth": 0.8,
|
|
62
|
+
"font.family": "DejaVu Sans",
|
|
63
|
+
"axes.titlesize": 11,
|
|
64
|
+
"axes.titlecolor": TEXT,
|
|
65
|
+
"axes.spines.top": False,
|
|
66
|
+
"axes.spines.right": False,
|
|
67
|
+
"legend.frameon": False,
|
|
68
|
+
"legend.labelcolor": MUTED,
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_csv(path):
|
|
74
|
+
with open(path, newline="") as f:
|
|
75
|
+
rows = list(csv.DictReader(f))
|
|
76
|
+
return rows
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def col(rows, name, default=np.nan):
|
|
80
|
+
out = np.empty(len(rows))
|
|
81
|
+
for i, r in enumerate(rows):
|
|
82
|
+
v = r.get(name, "")
|
|
83
|
+
out[i] = float(v) if v not in ("", None) else default
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main():
|
|
88
|
+
data = Path(sys.argv[1])
|
|
89
|
+
out = Path(sys.argv[2])
|
|
90
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
style()
|
|
92
|
+
|
|
93
|
+
b = read_csv(data / "mission_rpm_log.csv")
|
|
94
|
+
t_rows = read_csv(data / "mission_telemetry.csv")
|
|
95
|
+
events = read_csv(data / "mission_events.csv")
|
|
96
|
+
|
|
97
|
+
n_rotors = sum(1 for k in b[0] if k.startswith("rpm_"))
|
|
98
|
+
bt = col(b, "timestamp_s")
|
|
99
|
+
thrust = sum(col(b, f"thrust_N_{i}") for i in range(n_rotors))
|
|
100
|
+
rpm = [col(b, f"rpm_{i}") for i in range(n_rotors)]
|
|
101
|
+
vbus = col(b, "voltage_bus_V")
|
|
102
|
+
ibus = col(b, "current_bus_A")
|
|
103
|
+
soc = col(b, "battery_soc")
|
|
104
|
+
power = vbus * ibus
|
|
105
|
+
|
|
106
|
+
tt = col(t_rows, "t_s")
|
|
107
|
+
alt = col(t_rows, "alt_rel_m")
|
|
108
|
+
gs = col(t_rows, "groundspeed_m_s")
|
|
109
|
+
lat = col(t_rows, "lat")
|
|
110
|
+
lon = col(t_rows, "lon")
|
|
111
|
+
roll = col(t_rows, "roll_deg")
|
|
112
|
+
|
|
113
|
+
# ---- clock alignment: takeoff (bridge thrust > weight) vs (alt > 1 m) ----
|
|
114
|
+
b_take = bt[np.argmax(thrust > 1.05 * WEIGHT_N)]
|
|
115
|
+
t_take = tt[np.argmax(alt > 1.0)]
|
|
116
|
+
bt = bt + (t_take - b_take)
|
|
117
|
+
print(f"clock offset applied: {t_take - b_take:+.2f} s")
|
|
118
|
+
|
|
119
|
+
# Smooth the 400 Hz bridge channels ONCE, globally, before any windowing —
|
|
120
|
+
# smoothing inside a window leaves edge artifacts at its boundaries.
|
|
121
|
+
def smooth(x, w=9):
|
|
122
|
+
return np.convolve(x, np.ones(w) / w, mode="same")
|
|
123
|
+
|
|
124
|
+
rpm_s = [smooth(r) for r in rpm]
|
|
125
|
+
vbus_s = smooth(vbus)
|
|
126
|
+
ibus_s = smooth(ibus)
|
|
127
|
+
power_s = smooth(power)
|
|
128
|
+
soc_s = smooth(soc)
|
|
129
|
+
|
|
130
|
+
# Flight window: a little pre-takeoff ramp, ending at the telemetry end;
|
|
131
|
+
# spool-up and post-kill frames (bus collapsed / negative current) are cut.
|
|
132
|
+
t0, t1 = t_take - 4.0, tt[-1]
|
|
133
|
+
bm = (bt >= t0) & (bt <= t1) & (vbus > 20.0)
|
|
134
|
+
tm = (tt >= t0) & (tt <= t1)
|
|
135
|
+
|
|
136
|
+
def dec(x, k=20):
|
|
137
|
+
return x[bm][::k]
|
|
138
|
+
|
|
139
|
+
bt_d = bt[bm][::20]
|
|
140
|
+
modes = [(float(e["t_s"]), e["detail"]) for e in events if e["kind"] == "mode"]
|
|
141
|
+
wps = [(float(e["t_s"]), int(e["detail"])) for e in events if e["kind"] == "wp"]
|
|
142
|
+
|
|
143
|
+
def mode_lines(ax, label_y=None):
|
|
144
|
+
for ts, name in modes:
|
|
145
|
+
if ts < t0 or ts > t1 or name in ("GUIDED",):
|
|
146
|
+
continue
|
|
147
|
+
ax.axvline(ts, color=MUTED, lw=0.7, alpha=0.5, ls=(0, (4, 3)))
|
|
148
|
+
if label_y is not None:
|
|
149
|
+
ax.annotate(
|
|
150
|
+
name, (ts, label_y), color=MUTED, fontsize=7.5,
|
|
151
|
+
xytext=(3, 0), textcoords="offset points",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# ================= Figure 1: track + altitude =================
|
|
155
|
+
east = (lon - lon[0]) * M_PER_DEG_LAT * math.cos(math.radians(lat[0]))
|
|
156
|
+
north = (lat - lat[0]) * M_PER_DEG_LAT
|
|
157
|
+
p_at_t = np.interp(tt, bt[bm], power_s[bm])
|
|
158
|
+
|
|
159
|
+
fig = plt.figure(figsize=(8.0, 9.0))
|
|
160
|
+
gs_ = fig.add_gridspec(2, 1, height_ratios=[2.4, 1.0], hspace=0.28)
|
|
161
|
+
ax = fig.add_subplot(gs_[0])
|
|
162
|
+
sc = ax.scatter(east[tm], north[tm], c=np.clip(p_at_t[tm], 0, None), s=7, cmap="plasma", lw=0)
|
|
163
|
+
ax.plot(0, 0, marker="^", ms=9, color=TEXT)
|
|
164
|
+
ax.annotate("launch", (0, 0), xytext=(8, -4), textcoords="offset points",
|
|
165
|
+
color=MUTED, fontsize=8)
|
|
166
|
+
ax.set_aspect("equal", adjustable="datalim")
|
|
167
|
+
ax.set_xlabel("east (m)")
|
|
168
|
+
ax.set_ylabel("north (m)")
|
|
169
|
+
ax.set_title("Mission track — colored by electrical power")
|
|
170
|
+
cb = fig.colorbar(sc, ax=ax, pad=0.02, fraction=0.04)
|
|
171
|
+
cb.set_label("power (W)", color=MUTED)
|
|
172
|
+
cb.ax.yaxis.set_tick_params(color=MUTED)
|
|
173
|
+
plt.setp(cb.ax.get_yticklabels(), color=MUTED)
|
|
174
|
+
cb.outline.set_edgecolor(AXIS)
|
|
175
|
+
|
|
176
|
+
ax2 = fig.add_subplot(gs_[1])
|
|
177
|
+
ax2.fill_between(tt[tm], 0, alt[tm], color=BLUE, alpha=0.25, lw=0)
|
|
178
|
+
ax2.plot(tt[tm], alt[tm], color=BLUE, lw=1.6)
|
|
179
|
+
mode_lines(ax2, label_y=max(alt[tm]) * 0.86)
|
|
180
|
+
ax2.set_xlabel("mission time (s)")
|
|
181
|
+
ax2.set_ylabel("altitude (m)")
|
|
182
|
+
fig.suptitle(
|
|
183
|
+
"ThrustLab FMU · ArduPilot SITL — the exported powertrain flying a real mission",
|
|
184
|
+
color=TEXT, fontsize=12.5, y=0.985,
|
|
185
|
+
)
|
|
186
|
+
fig.savefig(out / "fmu_mission_track.png", dpi=180, bbox_inches="tight")
|
|
187
|
+
plt.close(fig)
|
|
188
|
+
|
|
189
|
+
# ================= Figure 2: powertrain time series =================
|
|
190
|
+
fig, axes = plt.subplots(4, 1, figsize=(9.6, 9.4), sharex=True)
|
|
191
|
+
fig.subplots_adjust(hspace=0.16)
|
|
192
|
+
|
|
193
|
+
a = axes[0]
|
|
194
|
+
a.plot(tt[tm], alt[tm], color=BLUE, lw=1.5, label="altitude (m)")
|
|
195
|
+
a.set_ylabel("altitude (m)")
|
|
196
|
+
ag = a.twinx()
|
|
197
|
+
ag.plot(tt[tm], gs[tm], color=MUTED, lw=1.1, alpha=0.8, label="groundspeed (m/s)")
|
|
198
|
+
ag.set_ylabel("groundspeed (m/s)", color=MUTED)
|
|
199
|
+
ag.grid(False)
|
|
200
|
+
ag.spines["right"].set_visible(True)
|
|
201
|
+
mode_lines(a, label_y=max(alt[tm]) * 0.9)
|
|
202
|
+
a.set_title("Flight profile")
|
|
203
|
+
|
|
204
|
+
a = axes[1]
|
|
205
|
+
for i in range(n_rotors):
|
|
206
|
+
a.plot(bt_d, dec(rpm_s[i]), color=ROTORS[i % len(ROTORS)], lw=1.1, label=f"rotor {i}")
|
|
207
|
+
a.set_ylabel("rotor speed (rpm)")
|
|
208
|
+
a.legend(ncol=4, fontsize=8, loc="lower right")
|
|
209
|
+
a.set_title("Per-rotor speed — FMU state, integrated from torque imbalance")
|
|
210
|
+
|
|
211
|
+
a = axes[2]
|
|
212
|
+
a.plot(bt_d, dec(vbus_s), color=YELLOW, lw=1.4, label="bus voltage (V)")
|
|
213
|
+
a.set_ylabel("bus voltage (V)", color=YELLOW)
|
|
214
|
+
ai = a.twinx()
|
|
215
|
+
ai.plot(bt_d, dec(ibus_s), color=RED, lw=1.1, alpha=0.9, label="bus current (A)")
|
|
216
|
+
ai.set_ylabel("bus current (A)", color=RED)
|
|
217
|
+
ai.grid(False)
|
|
218
|
+
ai.spines["right"].set_visible(True)
|
|
219
|
+
a.set_title("Battery under load — sag and recovery from the pack surface")
|
|
220
|
+
|
|
221
|
+
a = axes[3]
|
|
222
|
+
a.plot(bt_d, dec(power_s), color=ORANGE, lw=1.3, label="electrical power (W)")
|
|
223
|
+
a.set_ylabel("power (W)", color=ORANGE)
|
|
224
|
+
asoc = a.twinx()
|
|
225
|
+
asoc.plot(bt_d, dec(soc_s) * 100.0, color=AQUA, lw=1.4)
|
|
226
|
+
asoc.set_ylabel("state of charge (%)", color=AQUA)
|
|
227
|
+
asoc.grid(False)
|
|
228
|
+
asoc.spines["right"].set_visible(True)
|
|
229
|
+
a.set_xlabel("mission time (s)")
|
|
230
|
+
a.set_title("Energy — power draw and state of charge")
|
|
231
|
+
|
|
232
|
+
fig.suptitle(
|
|
233
|
+
"ThrustLab FMU outputs over one mission — every line is the exported model",
|
|
234
|
+
color=TEXT, fontsize=12.5, y=0.945,
|
|
235
|
+
)
|
|
236
|
+
fig.savefig(out / "fmu_mission_timeseries.png", dpi=180, bbox_inches="tight")
|
|
237
|
+
plt.close(fig)
|
|
238
|
+
|
|
239
|
+
# ================= Figure 3: loiter close-up =================
|
|
240
|
+
lo_wp = [ts for ts, seq in wps if seq == 5]
|
|
241
|
+
lo_end = [ts for ts, seq in wps if seq == 7]
|
|
242
|
+
w1 = (lo_end[0] + 12.0) if lo_end else t0 + (t1 - t0) * 0.75
|
|
243
|
+
w0 = max(lo_wp[0] - 2.0, w1 - 75.0) if lo_wp else w1 - 75.0
|
|
244
|
+
bw = (bt >= w0) & (bt <= w1) & (vbus > 20.0)
|
|
245
|
+
tw = (tt >= w0) & (tt <= w1)
|
|
246
|
+
|
|
247
|
+
fig, axes = plt.subplots(2, 1, figsize=(9.6, 5.6), sharex=True)
|
|
248
|
+
fig.subplots_adjust(hspace=0.14)
|
|
249
|
+
a = axes[0]
|
|
250
|
+
for i in range(n_rotors):
|
|
251
|
+
a.plot(bt[bw][::8], rpm_s[i][bw][::8],
|
|
252
|
+
color=ROTORS[i % len(ROTORS)], lw=1.2, label=f"rotor {i}")
|
|
253
|
+
a.set_ylabel("rotor speed (rpm)")
|
|
254
|
+
a.legend(ncol=4, fontsize=8, loc="upper right")
|
|
255
|
+
a.set_title("Loiter and exit — the mixer splits the rotors, the FMU answers per rotor")
|
|
256
|
+
a2 = axes[1]
|
|
257
|
+
a2.plot(tt[tw], roll[tw], color=PURPLE, lw=1.3)
|
|
258
|
+
a2.set_ylabel("roll (deg)")
|
|
259
|
+
a2.set_xlabel("mission time (s)")
|
|
260
|
+
fig.savefig(out / "fmu_mission_loiter.png", dpi=180, bbox_inches="tight")
|
|
261
|
+
plt.close(fig)
|
|
262
|
+
|
|
263
|
+
print(f"wrote 3 figures to {out}")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
main()
|