├── mp2litchi
├── __init__.py
├── enums.py
├── global_mp_cmd.py
├── gui.py
└── mp2litchi.py
├── requirements.txt
├── .gitattributes
├── mp_to_litchi_icon.ico
├── docs
├── images
│ ├── polygon.JPG
│ ├── set_home.JPG
│ ├── survey_grid.JPG
│ ├── draw_polygon.JPG
│ ├── litchi_import.JPG
│ ├── mp2litchi_gui.JPG
│ ├── litchi_settings.JPG
│ ├── Litchi_Screenshot.jpg
│ ├── litchi_import_csv.JPG
│ ├── litchi_import_confirm.JPG
│ ├── litchi_mission_menu.JPG
│ ├── litchi_save_mission.JPG
│ ├── litchi_settings_menu.JPG
│ ├── mp2litchi_file_select.JPG
│ ├── MissionPlanner_Screenshot.jpg
│ ├── cam_ctrl
│ │ ├── survey_grid_simple.JPG
│ │ ├── litchi_import_success.JPG
│ │ ├── survey_grid_grid_options.JPG
│ │ └── survey_grid_camera_config.JPG
│ └── trig_dist
│ │ ├── survey_grid_simple.JPG
│ │ ├── litchi_import_success.JPG
│ │ ├── survey_grid_camera_config.JPG
│ │ └── survey_grid_grid_options.JPG
├── MP_trigger_dist.md
├── MP_cmd_list.md
└── MP_cam_ctrl.md
├── run.py
├── .gitignore
├── README.md
└── .github
└── workflows
└── build.yml
/mp2litchi/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | litchi_wp>=2.0.0,<3.0.0
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/mp_to_litchi_icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/mp_to_litchi_icon.ico
--------------------------------------------------------------------------------
/docs/images/polygon.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/polygon.JPG
--------------------------------------------------------------------------------
/docs/images/set_home.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/set_home.JPG
--------------------------------------------------------------------------------
/docs/images/survey_grid.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/survey_grid.JPG
--------------------------------------------------------------------------------
/docs/images/draw_polygon.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/draw_polygon.JPG
--------------------------------------------------------------------------------
/docs/images/litchi_import.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/litchi_import.JPG
--------------------------------------------------------------------------------
/docs/images/mp2litchi_gui.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/mp2litchi_gui.JPG
--------------------------------------------------------------------------------
/docs/images/litchi_settings.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/litchi_settings.JPG
--------------------------------------------------------------------------------
/docs/images/Litchi_Screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/Litchi_Screenshot.jpg
--------------------------------------------------------------------------------
/docs/images/litchi_import_csv.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/litchi_import_csv.JPG
--------------------------------------------------------------------------------
/docs/images/litchi_import_confirm.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/litchi_import_confirm.JPG
--------------------------------------------------------------------------------
/docs/images/litchi_mission_menu.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/litchi_mission_menu.JPG
--------------------------------------------------------------------------------
/docs/images/litchi_save_mission.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/litchi_save_mission.JPG
--------------------------------------------------------------------------------
/docs/images/litchi_settings_menu.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/litchi_settings_menu.JPG
--------------------------------------------------------------------------------
/docs/images/mp2litchi_file_select.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/mp2litchi_file_select.JPG
--------------------------------------------------------------------------------
/docs/images/MissionPlanner_Screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/MissionPlanner_Screenshot.jpg
--------------------------------------------------------------------------------
/docs/images/cam_ctrl/survey_grid_simple.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/cam_ctrl/survey_grid_simple.JPG
--------------------------------------------------------------------------------
/docs/images/trig_dist/survey_grid_simple.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/trig_dist/survey_grid_simple.JPG
--------------------------------------------------------------------------------
/docs/images/cam_ctrl/litchi_import_success.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/cam_ctrl/litchi_import_success.JPG
--------------------------------------------------------------------------------
/docs/images/trig_dist/litchi_import_success.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/trig_dist/litchi_import_success.JPG
--------------------------------------------------------------------------------
/docs/images/cam_ctrl/survey_grid_grid_options.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/cam_ctrl/survey_grid_grid_options.JPG
--------------------------------------------------------------------------------
/docs/images/cam_ctrl/survey_grid_camera_config.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/cam_ctrl/survey_grid_camera_config.JPG
--------------------------------------------------------------------------------
/docs/images/trig_dist/survey_grid_camera_config.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/trig_dist/survey_grid_camera_config.JPG
--------------------------------------------------------------------------------
/docs/images/trig_dist/survey_grid_grid_options.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YarosMallorca/MissionPlanner-to-Litchi/HEAD/docs/images/trig_dist/survey_grid_grid_options.JPG
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | """
2 | Starts the Mission Planner to Litchi converter
3 | """
4 | # pylint: disable=import-error
5 | from mp2litchi.mp2litchi import welcome
6 | from mp2litchi.gui import Gui
7 |
8 | welcome()
9 | gui = Gui()
10 | gui.render()
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .nox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | .hypothesis/
49 | .pytest_cache/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 | db.sqlite3
59 |
60 | # Flask stuff:
61 | instance/
62 | .webassets-cache
63 |
64 | # Scrapy stuff:
65 | .scrapy
66 |
67 | # Sphinx documentation
68 | docs/_build/
69 |
70 | # PyBuilder
71 | target/
72 |
73 | # Jupyter Notebook
74 | .ipynb_checkpoints
75 |
76 | # IPython
77 | profile_default/
78 | ipython_config.py
79 |
80 | # pyenv
81 | .python-version
82 |
83 | # celery beat schedule file
84 | celerybeat-schedule
85 |
86 | # SageMath parsed files
87 | *.sage.py
88 |
89 | # Environments
90 | .env
91 | .venv
92 | env/
93 | venv/
94 | ENV/
95 | env.bak/
96 | venv.bak/
97 |
98 | # Spyder project settings
99 | .spyderproject
100 | .spyproject
101 |
102 | # Rope project settings
103 | .ropeproject
104 |
105 | # mkdocs documentation
106 | /site
107 |
108 | # mypy
109 | .mypy_cache/
110 | .dmypy.json
111 | dmypy.json
112 |
113 | # Pyre type checker
114 | .pyre/
115 |
--------------------------------------------------------------------------------
/mp2litchi/enums.py:
--------------------------------------------------------------------------------
1 | """
2 | Module to bundle all enum classes
3 | """
4 | from enum import Enum
5 |
6 |
7 | class MPCommand(Enum):
8 | """
9 | Enum class for the command ids of mission planner waypoint export files
10 | """
11 |
12 | WAYPOINT = 16
13 | SPLINE_WAYPOINT = 82
14 | LOITER_TURNS = 18
15 | LOITER_TIME = 19
16 | LOITER_UNLIM = 17
17 | RTL = 20
18 | LAND = 21
19 | TAKEOFF = 22
20 | DELAY = 93
21 | GUIDED_ENABLE = 92
22 | PAYLOAD_PLACE = 94
23 | DO_GUIDED_LIMITS = 222
24 | DO_WINCH = 42600
25 | DO_SET_ROI = 201
26 | CONDITION_DELAY = 112
27 | CONDITION_CHANGE_ALT = 113
28 | CONDITION_DISTANCE = 114
29 | CONDITION_YAW = 115
30 | DO_JUMP = 177
31 | DO_CHANGE_SPEED = 178
32 | DO_GRIPPER = 211
33 | DO_PARACHUTE = 208
34 | DO_SET_CAM_TRIG_DISTANCE = 206
35 | DO_SET_RELAY = 181
36 | DO_REPEAT_RELAY = 182
37 | DO_SET_SERVO = 183
38 | DO_REPEAT_SERVO = 184
39 | DO_DIGICAM_CONFIGURE = 202
40 | DO_DIGICAM_CONTROL = 203
41 | DO_MOUNT_CONTROL = 205
42 | DO_SPRAYER = 216
43 |
44 |
45 | class InfoMessage(Enum):
46 | """
47 | Enum class for info messages to be shows in the gui or log
48 | """
49 | NO_SPEED_SET = 'No speed is set. Using Cruising Speed setting in litchi.'
50 |
51 |
52 | class WarningMessage(Enum):
53 | """
54 | Enum class for warning messages to be shows in the gui or log
55 | """
56 |
57 | SPEED_CAP = 'The speed has been set to 15m/s due to Litchi limitations.'
58 | SPEED_NEGATIVE = 'The speed has been set to cruise speed ' \
59 | '(see Litchi settings) because negative speed is not allowed.'
60 |
61 |
62 | class ErrorMessage(Enum):
63 | """
64 | Enum class for error messages to be shows in the gui or log
65 | """
66 |
67 | FILE_NOT_FOUND = 'The file was not found.'
68 | NO_FREE_ACTION_SLOTS = 'The waypoint has no more free action slots. ' \
69 | 'Maximum is 15 actions per Waypoint.'
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mission Planner to Litchi - Version 1.1.2
2 | ## What is this application for?
3 | Mission Planner to Litchi is a tool to convert
4 | [Mission Planner](https://ardupilot.org/planner/docs/mission-planner-installation.html) (ArduCopter)
5 | Waypoint files to [Litchi](https://flylitchi.com/) CSV Format to execute on Drones (e.g. DJI Drones).
6 |
7 | Mission Planner to Litchi was developed to be used to convert mapping survey missions from
8 | [Mission Planner](https://ardupilot.org/planner/docs/mission-planner-installation.html) to
9 | [Litchi](https://flylitchi.com/).
10 |
11 | ## Looking to do 3D Mapping with your Waypoint-capable Mavic?
12 | ### **Check out my new tool [DJI-Mapper](https://github.com/YarosMallorca/DJI-Mapper)**
13 |
14 |
15 |
16 | ### Survey missions in [Litchi](https://flylitchi.com/)
17 |
18 | Litchi doesn't support Survey mode yet, but here is a workaround!
19 | You will need [Mission Planner](https://ardupilot.org/planner/docs/mission-planner-installation.html) installed
20 | in order to plan your mission.
21 |
22 | Warning: This script was tested successfully many times, should work stably.
23 | I am not responsible for any damage caused by the use of this software.
24 |
25 | [Click here to Download for Windows](https://github.com/YarostheLaunchpadder/MissionPlanner-to-Litchi/releases/download/Alpha/Mission.Planner.to.Litchi.exe)
26 |
27 | Mac version not available yet
28 |
29 |
30 |
31 |
32 |
33 |
34 | ## Workflow from [Mission Planner](https://ardupilot.org/planner/docs/mission-planner-installation.html) to [Litchi](https://flylitchi.com/)
35 |
36 | There are two types of camera trigger methods supported.
37 |
38 | - Distance trigger
39 | - Waypoint trigger
40 |
41 | The two workflows are described in detail here:
42 |
43 | - [Distance trigger](https://github.com/YarostheLaunchpadder/MissionPlanner-to-Litchi/blob/main/docs/MP_trigger_dist.md)
44 | - [Waypoint trigger](https://github.com/YarostheLaunchpadder/MissionPlanner-to-Litchi/blob/main/docs/MP_cam_ctrl.md)
45 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Release
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | types: [closed]
7 | branches:
8 | - main
9 | jobs:
10 | create-release:
11 | runs-on: ubuntu-latest
12 | outputs:
13 | upload_url: ${{ steps.create_release.outputs.upload_url }}
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v2
17 |
18 | - name: Get version number
19 | id: get_version
20 | run: |
21 | version=$(grep -oE -m 1 '[0-9]+\.[0-9]+\.[0-9]+' README.md)
22 | echo "version=$version" >> $GITHUB_OUTPUT
23 |
24 | - name: Create release
25 | id: create_release
26 | uses: actions/create-release@v1
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.ACTIONS }}
29 | with:
30 | tag_name: v${{ steps.get_version.outputs.version }}
31 | release_name: v${{ steps.get_version.outputs.version }}
32 | body: "Version ${{ steps.get_version.outputs.version }}"
33 | draft: false
34 | prerelease: false
35 |
36 | build-and-upload:
37 | needs: create-release
38 | runs-on: ${{ matrix.os }}
39 | strategy:
40 | matrix:
41 | os: ['windows-latest', 'macos-latest', 'ubuntu-latest']
42 | python-version: ['3.10']
43 | steps:
44 | - name: Checkout code
45 | uses: actions/checkout@v2
46 |
47 | - name: Set up Python
48 | uses: actions/setup-python@v2
49 | with:
50 | python-version: ${{ matrix.python-version }}
51 |
52 | - name: Install PyInstaller
53 | run: pip install pyinstaller
54 |
55 | - name: Install dependencies
56 | run: pip install -r requirements.txt
57 |
58 | - name: Build executables
59 | run: |
60 | pyinstaller --onefile run.py -n mp2litchi-${{ matrix.os }}
61 |
62 | - name: Upload executables Windows
63 | if: matrix.os == 'windows-latest'
64 | uses: actions/upload-release-asset@v1
65 | env:
66 | GITHUB_TOKEN: ${{ secrets.ACTIONS }}
67 | with:
68 | upload_url: ${{ needs.create-release.outputs.upload_url }}
69 | asset_path: dist\mp2litchi-${{ matrix.os }}.exe
70 | asset_name: mp2litchi-${{ matrix.os }}.exe
71 | asset_content_type: application/octet-stream
72 |
73 | - name: Upload executables
74 | if: matrix.os != 'windows-latest'
75 | uses: actions/upload-release-asset@v1
76 | env:
77 | GITHUB_TOKEN: ${{ secrets.ACTIONS }}
78 | with:
79 | upload_url: ${{ needs.create-release.outputs.upload_url }}
80 | asset_path: dist/mp2litchi-${{ matrix.os }}
81 | asset_name: mp2litchi-${{ matrix.os }}
82 | asset_content_type: application/octet-stream
83 |
--------------------------------------------------------------------------------
/mp2litchi/global_mp_cmd.py:
--------------------------------------------------------------------------------
1 | """
2 | Module for global MP commands
3 | """
4 | from litchi_wp.waypoint import Waypoint
5 |
6 | from mp2litchi.enums import MPCommand
7 |
8 |
9 | # pylint: disable=too-few-public-methods
10 | class GlobalMPCmd:
11 | """
12 | Represents a single MP cmd
13 | """
14 |
15 | def __init__(self, cmd: MPCommand, param: int | float = 0, active: bool = False, loc: int = 0):
16 | """
17 | Constructor
18 |
19 | Args:
20 | cmd (MPCommand): The MP command
21 | param (int | float): The parameter of the command
22 | active (bool): True if global command is active
23 |
24 | """
25 |
26 | self.active: bool = active
27 | self.type: MPCommand = cmd
28 | self.param: int | float = param
29 | self.param_loc: int = loc
30 |
31 |
32 | class GlobalMPCmdManager:
33 | """
34 | Class for handling of global MP cmds
35 |
36 | Attributes:
37 | global_cmds (list[GlobalMPCmd]): List of commands that influence all following waypoints
38 |
39 | """
40 |
41 | global_cmds = [
42 | GlobalMPCmd(cmd=MPCommand.DO_CHANGE_SPEED, loc=5),
43 | GlobalMPCmd(cmd=MPCommand.DO_SET_CAM_TRIG_DISTANCE, loc=4)
44 | ]
45 |
46 | def get_active(self) -> list[GlobalMPCmd]:
47 | """
48 | Getter for all active global MP commands
49 |
50 | Returns:
51 | A list of GlobalMPCmd objects that are currently set to active
52 |
53 | """
54 |
55 | return [cmd for cmd in self.global_cmds if cmd.active]
56 |
57 | def apply_to_waypoint(self, cmd: GlobalMPCmd, waypoint: Waypoint):
58 | """
59 | Applies the global command to the waypoint
60 | Args:
61 | cmd: The command to be applied
62 | waypoint: The waypoint to be referenced
63 |
64 | """
65 |
66 | match cmd.type:
67 | case MPCommand.DO_SET_CAM_TRIG_DISTANCE:
68 | waypoint.set_photo_interval_distance(
69 | round(cmd.param, 1)
70 | )
71 | case MPCommand.DO_CHANGE_SPEED:
72 | waypoint.set_speed_ms(
73 | round(cmd.param, 2)
74 | )
75 |
76 | def apply_all_active_to_waypoint(self, waypoint: Waypoint):
77 | """
78 | Applies all active global commands to the Waypoint
79 | Args:
80 | waypoint: The waypoint to be referenced
81 |
82 | """
83 |
84 | cmds = self.get_active()
85 | for cmd in cmds:
86 | self.apply_to_waypoint(cmd, waypoint)
87 |
88 | def is_global(self, cmd: MPCommand) -> GlobalMPCmd | None:
89 | """
90 | Checks if a MP command has global effects
91 |
92 | Args:
93 | cmd: The MP command
94 |
95 | Returns:
96 | The command as GlobalMPCmd or None
97 |
98 | """
99 |
100 | for g_cmd in self.global_cmds:
101 | if g_cmd.type == cmd:
102 | return g_cmd
103 | return None
104 |
105 | def update(self, cmd: MPCommand, param: float | int) -> bool:
106 | """
107 | Checks if a given command has global effects.
108 | If that is the case the global command gets updated.
109 |
110 | Args:
111 | cmd: The MP command
112 | param: The Parameter of the command
113 |
114 | Returns:
115 | True if the command has global effects and was updated
116 |
117 | """
118 |
119 | for g_cmd in self.global_cmds:
120 | if g_cmd.type == cmd:
121 | g_cmd.param = param
122 | g_cmd.active = True
123 | return True
124 | return False
125 |
--------------------------------------------------------------------------------
/docs/MP_trigger_dist.md:
--------------------------------------------------------------------------------
1 | # [Mission Planner](https://ardupilot.org/planner/) survey flight plan using [trigger distance](https://ardupilot.org/copter/docs/mission-command-list.html#do-set-cam-trigg-dist)
2 |
3 | ## What is [trigger distance](https://ardupilot.org/copter/docs/mission-command-list.html#do-set-cam-trigg-dist)?
4 |
5 | [Trigger distance](https://ardupilot.org/copter/docs/mission-command-list.html#do-set-cam-trigg-dist)
6 | is the `Trigger Method` selected in [Mission Planner](https://ardupilot.org/planner/).
7 | This sets a distance that must be traveled before each shot.
8 | The [trigger distance](https://ardupilot.org/copter/docs/mission-command-list.html#do-set-cam-trigg-dist)
9 | is calculated based on the camera, the flight height and the overlap.
10 |
11 | ### Example
12 |
13 | The [trigger distance](https://ardupilot.org/copter/docs/mission-command-list.html#do-set-cam-trigg-dist)
14 | is calculated to be 22 meters.
15 | The aircraft will take a photo at intervals of 22 meters.
16 |
17 | ## What are the advantages and disadvantages?
18 |
19 | ### Pro
20 |
21 | - Short mission duration
22 | - Fewer waypoints
23 |
24 |
25 | ### Con
26 |
27 | - Image blur may occur due to movement while capturing
28 | - is aggravated by
29 | - low altitude
30 | - high fly speed
31 | - long exposure time / low light conditions
32 | - Altitude above ground can vary between images
33 |
34 | # Workflow
35 | To create a flight-plan in [Mission Planner](https://ardupilot.org/planner/) and import it
36 | into [Litchi Mission Hub](https://flylitchi.com/hub), follow these steps.
37 |
38 | ## [Mission Planner](https://ardupilot.org/planner/)
39 |
40 | Start [Mission Planner](https://ardupilot.org/planner/) and select `Plan` in the top left corner.
41 |
42 | ### Set Home Point
43 | Right-click on the location you want to set the home point and select `Set Home Here`.
44 |
45 |
46 |
47 | ### Draw polygon
48 |
49 | Left-click on the polygon icon in the top left corner and select `Draw Polygon`. Then draw the polygon area
50 | by left-clicking on the map.
51 |
52 |
53 |
54 |
55 |
56 | ### Start survey tool
57 |
58 | Right-click on the map and select `Auto WP` -> `Survey (Grid)`.
59 |
60 |
61 |
62 | ### Settings for `Simple` tab
63 |
64 | Select the `Camera`, set the `Altitude` and `Flying Speed`. Make sure to set `Use speed for this mission`
65 | and `Advanced Options`.
66 |
67 | If your camera is not in the list, then you can load an image in the `Camera Config` tab.
68 | The camera settings will be set automatically using the metadata in the image.
69 |
70 |
71 |
72 | ### Settings for `Grid Options` tab
73 |
74 | Set the `Overlap` (front-overlap) and the `Sidelap` (side-overlap) to your needs. Make sure to set `Delay at WP` to `0`.
75 |
76 | Also, if you need a cross-grid (e.g. 3D models or better DEMs), then set `Cross Grid` as well.
77 |
78 |
79 |
80 | ### Settings for `Camera Config` tab
81 |
82 | Make sure to select `CAM_TRIG_DIST` as the `Trigger Method`.
83 |
84 | Also, you can click `Load Sample Photo` if your camera was not listed. You can then click `Save` to add it to
85 | the list of cameras.
86 |
87 |
88 |
89 | ### Save Grid
90 |
91 | Navigate back to the `Simple` tab and click `Accept` in the bottom right corner.
92 |
93 | ### Export flight-plan as `.waypoints` file
94 |
95 | Press `ctrl + s` to export the flight-plan.
96 |
97 | ## MissionPlanner-to-Litchi
98 |
99 | Open `MissionPlanner-to-litchi` and click `Select files to convert`.
100 |
101 |
102 |
103 | ### Select File or Files
104 |
105 | Select the file you exported from [Mission Planner](https://ardupilot.org/planner/) and click `Open`.
106 | MissionPlanner-to-Litchi will create a `.csv` file in the same directory.
107 |
108 | In this example the file is named `diamonhead.waypoints` so the converted file will be `diamonhead.waypoints.csv`.
109 |
110 |
111 |
112 | ## [Litchi Mission Hub](https://flylitchi.com/hub)
113 |
114 | Go to the [Litchi Mission Hub](https://flylitchi.com/hub) and log in to your account (top right corner).
115 |
116 | ### Set global settings
117 |
118 | Click on `SETTINGS` in the bottom left corner. Make sure that the marked settings are set as shown in the image
119 | then click `Close`.
120 |
121 |
122 |
123 |
124 |
125 | ### Import file
126 |
127 | Click on `MISSIONS` in the bottom left corner.
128 |
129 | Then select `Import...`.
130 |
131 |
132 |
133 |
134 |
135 | Select the `.csv` file. In this example it is the `diamonhead.waypoints.csv`.
136 |
137 |
138 |
139 | Click `Import to new Mission` to import the flight-plan to the [Litchi Mission Hub](https://flylitchi.com/hub).
140 |
141 |
142 |
143 | ### Imported flight-plan
144 |
145 |
146 |
147 | ### Save the mission
148 |
149 | You need to save the mission to be able to sync it to your control device (mobile phone / tablet).
150 |
151 | To do this you click on `Missions` in the bottom left corner and select `Save...`.
152 |
153 |
154 |
155 | Enter a name for your mission and click on `Save`.
--------------------------------------------------------------------------------
/mp2litchi/gui.py:
--------------------------------------------------------------------------------
1 | """
2 | GUI Module
3 | """
4 | # pylint: disable=import-error
5 | import tkinter as tk
6 | from os import getcwd
7 | from os.path import dirname
8 | from tkinter import filedialog as fd
9 | from tkinter import ttk
10 | from tkinter import messagebox
11 | from typing import Callable, Literal
12 |
13 | from mp2litchi import mp2litchi
14 |
15 |
16 | class FileMessages:
17 | """
18 | Class for inf, warning, error messages
19 | """
20 |
21 | def __init__(
22 | self,
23 | filename: str,
24 | info_messages: list[str],
25 | warning_messages: list[str],
26 | error_messages: list[str]):
27 | """
28 | Constructor
29 |
30 | Args:
31 | filename (str): The filename the messages refer to
32 | info_messages list(str): A list of info messages
33 | warning_messages list(str): A list of warning messages
34 | error_messages list(str): A list of error messages
35 |
36 | """
37 |
38 | self.filename = filename
39 | self.info_messages = info_messages.copy()
40 | self.warning_messages = warning_messages.copy()
41 | self.error_messages = error_messages.copy()
42 |
43 | def get_info(self) -> str | None:
44 | """
45 | Getter for info messages
46 | Returns:
47 | A string containing the filename and all info messages
48 | or None if no messages exist
49 |
50 | """
51 |
52 | if len(self.info_messages) == 0:
53 | return None
54 | ret = f"{self.filename}:\n"
55 | for info_message in self.info_messages:
56 | ret += f"{info_message}\n"
57 | return ret
58 |
59 | def get_warn(self) -> str | None:
60 | """
61 | Getter for warning messages
62 | Returns:
63 | A string containing the filename and all warning messages
64 | or None if no messages exist
65 |
66 | """
67 |
68 | if len(self.warning_messages) == 0:
69 | return None
70 | ret = f"{self.filename}:\n"
71 | for warning_message in self.warning_messages:
72 | ret += f"{warning_message}\n"
73 | return ret
74 |
75 | def get_error(self) -> str | None:
76 | """
77 | Getter for error messages
78 | Returns:
79 | A string containing the filename and all error messages
80 | or None if no messages exist
81 |
82 | """
83 |
84 | if len(self.error_messages) == 0:
85 | return None
86 | ret = f"{self.filename}:\n"
87 | for error_message in self.error_messages:
88 | ret += f"{error_message}\n"
89 | return ret
90 |
91 |
92 | class Gui:
93 | """
94 | Class for the GUI
95 |
96 | Attributes:
97 | current_dir (str): The directory the fileselector will start in
98 | root (Tk): The tkinter instance
99 |
100 | """
101 |
102 | def __init__(
103 | self,
104 | current_dir: str = getcwd(),
105 | title: str = 'MP2Litchi',
106 | width: int = 300,
107 | height: int = 150
108 | ):
109 | """
110 | Constructor
111 |
112 | Parameters:
113 | current_dir (str): The directory the fileselector will start in
114 | title (str): The test in the titlebar of the window
115 | width (int): The width of the window
116 | height (int): The height of the window
117 |
118 | """
119 |
120 | self.current_dir = current_dir
121 | self.root = tk.Tk()
122 | self.root.title(title)
123 | self.root.resizable(False, False)
124 | self.root.geometry(f"{width}x{height}")
125 | self.populate_gui()
126 |
127 | def populate_gui(self):
128 | """
129 | Method to bundle the setup of all the GUI elements
130 | """
131 |
132 | self.add_button(
133 | text='Select files to convert',
134 | callback=self.convert_files
135 | )
136 |
137 | def add_button(self, text: str, callback: Callable):
138 | """
139 | Adds a button to the GUI
140 |
141 | Parameters:
142 | text (str): The text to be displayed on the button
143 | callback (Callable): The callback function to be called when the button is clicked
144 |
145 | """
146 |
147 | button = ttk.Button(
148 | self.root,
149 | text=text,
150 | command=callback
151 | )
152 | button.pack(expand=True)
153 |
154 | def get_files(self) -> Literal[""] | tuple[str, ...]:
155 | """
156 | Opens the file select window
157 |
158 | Returns:
159 | List (Literal[""] | tuple[str, ...]): All selected filenames including their path
160 |
161 | """
162 |
163 | filetypes = (
164 | ('text files', ['*.txt', '*.waypoints']),
165 | ('All files', '*.*')
166 | )
167 |
168 | filenames = fd.askopenfilenames(
169 | title='Select files',
170 | initialdir=self.current_dir,
171 | filetypes=filetypes)
172 | if len(filenames) > 0:
173 | self.current_dir = dirname(filenames[0])
174 | return filenames
175 |
176 | def convert_files(self):
177 | """
178 | Convert all selected files
179 | """
180 |
181 | file_messages = []
182 | filenames = self.get_files()
183 | for filename in filenames:
184 | infos, warnings, errors = mp2litchi.convert(filename)
185 | file_messages.append(
186 | FileMessages(
187 | filename,
188 | infos,
189 | warnings,
190 | errors
191 | )
192 | )
193 | for file_message in file_messages:
194 | info = file_message.get_info()
195 | warn = file_message.get_warn()
196 | error = file_message.get_error()
197 | if info is not None:
198 | self.show_info(
199 | info=info
200 | )
201 | if warn is not None:
202 | self.show_warning(
203 | warnings=warn
204 | )
205 | if error is not None:
206 | self.show_error(
207 | error=error
208 | )
209 |
210 | def show_info(self, info: str):
211 | """
212 | Displays an Info window
213 | Args:
214 | info: The Info message to be shown
215 |
216 | """
217 |
218 | messagebox.showinfo(
219 | title='Info',
220 | message=info
221 | )
222 |
223 | def show_warning(self, warnings: str):
224 | """
225 | Displays a warning window
226 | Args:
227 | warnings: The warning message to be shown
228 |
229 | """
230 |
231 | messagebox.showwarning(
232 | title='Warning',
233 | message=warnings
234 | )
235 |
236 | def show_error(self, error: str):
237 | """
238 | Displays an error window
239 | Args:
240 | error: The error message to be shown
241 |
242 | """
243 |
244 | messagebox.showerror(
245 | title='Error',
246 | message=error
247 | )
248 |
249 | def render(self):
250 | """
251 | Renders the GUI
252 | """
253 |
254 | self.root.mainloop()
255 |
--------------------------------------------------------------------------------
/docs/MP_cmd_list.md:
--------------------------------------------------------------------------------
1 | # command codes in a waypointfile exported from MP
2 | ## QGC WPL 110
3 |
4 | | human readable command | command id |
5 | |:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------|
6 | | [condition change alt]() | 113 |
7 | | [condition delay](https://ardupilot.org/copter/docs/mission-command-list.html#condition-delay) | 112 |
8 | | [condition distance](https://ardupilot.org/copter/docs/mission-command-list.html#condition-distance) | 114 |
9 | | [condition yaw](https://ardupilot.org/copter/docs/mission-command-list.html#condition-yaw) | 115 |
10 | | [delay](https://ardupilot.org/copter/docs/mission-command-list.html#delay) | 93 |
11 | | [do change speed](https://ardupilot.org/copter/docs/mission-command-list.html#do-change-speed) | 178 |
12 | | [do digicam configure](https://ardupilot.org/planner/docs/common-mavlink-mission-command-messages-mav_cmd.html?highlight=mission#mav-cmd-do-digicam-configure) | 202 |
13 | | [do digicam control](https://ardupilot.org/copter/docs/mission-command-list.html#do-digicam-control) | 203 |
14 | | [do gripper](https://ardupilot.org/copter/docs/mission-command-list.html#do-gripper) | 211 |
15 | | guided enable | 92 |
16 | | do guided limits | 222 |
17 | | [do jump](https://ardupilot.org/copter/docs/mission-command-list.html#do-jump) | 177 |
18 | | [do mount control](https://ardupilot.org/copter/docs/mission-command-list.html#do-mount-control) | 205 |
19 | | do parachute | 208 |
20 | | [do repeat relay](https://ardupilot.org/copter/docs/mission-command-list.html#do-repeat-relay) | 182 |
21 | | [do repeat servo](https://ardupilot.org/copter/docs/mission-command-list.html#do-repeat-servo) | 184 |
22 | | [do set cam trig distance](https://ardupilot.org/copter/docs/mission-command-list.html#do-set-cam-trigg-dist) | 206 |
23 | | [do set relay](https://ardupilot.org/copter/docs/mission-command-list.html#do-set-relay) | 181 |
24 | | [do set roi](https://ardupilot.org/copter/docs/mission-command-list.html#do-set-roi) | 201 |
25 | | [do set servo](https://ardupilot.org/copter/docs/mission-command-list.html#do-set-servo) | 183 |
26 | | do sprayer | 216 |
27 | | do winch | 42600 |
28 | | [land](https://ardupilot.org/copter/docs/mission-command-list.html#land) | 21 |
29 | | [loiter time](https://ardupilot.org/copter/docs/mission-command-list.html#loiter-time) | 19 |
30 | | [loiter turns](https://ardupilot.org/copter/docs/mission-command-list.html#loiter-turns) | 18 |
31 | | [loiter unlim](https://ardupilot.org/copter/docs/mission-command-list.html#loiter-unlimited) | 17 |
32 | | [payload place](https://ardupilot.org/copter/docs/mission-command-list.html#payload-place) | 94 |
33 | | [return to launch](https://ardupilot.org/copter/docs/mission-command-list.html#return-to-launch) | 20 |
34 | | [spline waypoint](https://ardupilot.org/copter/docs/mission-command-list.html#spline-waypoint) | 82 |
35 | | [takeoff](https://ardupilot.org/copter/docs/mission-command-list.html#takeoff) | 22 |
36 | | [waypoint](https://ardupilot.org/copter/docs/mission-command-list.html#waypoint) | 16 |
--------------------------------------------------------------------------------
/docs/MP_cam_ctrl.md:
--------------------------------------------------------------------------------
1 | # [Mission Planner](https://ardupilot.org/planner/) survey flight plan using [camera control](https://ardupilot.org/copter/docs/mission-command-list.html#do-digicam-control)
2 |
3 | ## What is [camera control](https://ardupilot.org/copter/docs/mission-command-list.html#do-digicam-control)?
4 |
5 | [Camera control](https://ardupilot.org/copter/docs/mission-command-list.html#do-digicam-control)
6 | is the `Trigger Method` selected in [Mission Planner](https://ardupilot.org/planner/).
7 | This creates a waypoint for each image to be taken.
8 | The [camera control](https://ardupilot.org/copter/docs/mission-command-list.html#do-digicam-control) waypoints
9 | are calculated based on the camera, the flight height and the overlap.
10 | The [camera control](https://ardupilot.org/copter/docs/mission-command-list.html#do-digicam-control) waypoints
11 | can be configured to have a delay. The delay stops the aircraft for the configured time, captures the image
12 | after that delay and continues with the flight-plan afterwards.
13 | The delay is meant to stabilize the aircraft and to capture the image without any blur due to movement. That way
14 | the flight speed can be much higher than in the
15 | [trigger distance](https://github.com/YarostheLaunchpadder/MissionPlanner-to-Litchi/blob/main/docs/MP_trigger_dist.md)
16 | mode.
17 |
18 | ### Example
19 |
20 | The [camera control](https://ardupilot.org/copter/docs/mission-command-list.html#do-digicam-control)
21 | waypoints are configured to have a delay time of 1 second.
22 | The aircraft will travel to the next
23 | [camera control](https://ardupilot.org/copter/docs/mission-command-list.html#do-digicam-control)
24 | waypoint, wait for 1 second, capture an image
25 | and continue to the next waypoint.
26 |
27 | ## What are the advantages and disadvantages?
28 |
29 | ### Pro
30 |
31 | - Avoids image blur
32 | - at low altitudes
33 | - during high exposure time / low light conditions
34 | - flight speed is 0 while capturing the image
35 | - Altitude above ground remains constant between images (each waypoint is set to AGL)
36 |
37 | ### Con
38 |
39 | - Lots of waypoints
40 | - Longer mission duration
41 |
42 | # Workflow
43 | To create a flight-plan in [Mission Planner](https://ardupilot.org/planner/) and import it
44 | into [Litchi Mission Hub](https://flylitchi.com/hub), follow these steps.
45 |
46 | ## [Mission Planner](https://ardupilot.org/planner/)
47 |
48 | Start [Mission Planner](https://ardupilot.org/planner/) and select `Plan` in the top left corner.
49 |
50 | ### Set Home Point
51 | Right-click on the location you want to set the home point and select `Set Home Here`.
52 |
53 |
54 |
55 | ### Draw polygon
56 |
57 | Left-click on the polygon icon in the top left corner and select `Draw Polygon`. Then draw the polygon area
58 | by left-clicking on the map.
59 |
60 |
61 |
62 |
63 |
64 | ### Start survey tool
65 |
66 | Right-click on the map and select `Auto WP` -> `Survey (Grid)`.
67 |
68 |
69 |
70 | ### Settings for `Simple` tab
71 |
72 | Select the `Camera`, set the `Altitude` and `Flying Speed`. Make sure to set `Use speed for this mission`
73 | and `Advanced Options`.
74 |
75 | If your camera is not in the list, then you can load an image in the `Camera Config` tab.
76 | The camera settings will be set automatically using the metadata in the image.
77 |
78 |
79 |
80 | ### Settings for `Grid Options` tab
81 |
82 | Set the `Overlap` (front-overlap) and the `Sidelap` (side-overlap) to your needs. Make sure to set `Delay at WP` to at
83 | least the time that the aircraft needs to stabilize. This depends on the aircraft, wind conditions and flight speed.
84 |
85 | Also, if you need a cross-grid (e.g. 3D models or better DEMs), then set `Cross Grid` as well.
86 |
87 |
88 |
89 | ### Settings for `Camera Config` tab
90 |
91 | Make sure to select `DO_DIGICAM_CONTROL` as the `Trigger Method`.
92 |
93 | Also, you can click `Load Sample Photo` if your camera was not listed. You can then click `Save` to add it to
94 | the list of cameras.
95 |
96 |
97 |
98 | ### Save Grid
99 |
100 | Navigate back to the `Simple` tab and click `Accept` in the bottom right corner.
101 |
102 | ### Export flight-plan as `.waypoints` file
103 |
104 | Press `ctrl + s` to export the flight-plan.
105 |
106 | ## MissionPlanner-to-Litchi
107 |
108 | Open `MissionPlanner-to-litchi` and click `Select files to convert`.
109 |
110 |
111 |
112 | ### Select File or Files
113 |
114 | Select the file you exported from [Mission Planner](https://ardupilot.org/planner/) and click `Open`.
115 | MissionPlanner-to-Litchi will create a `.csv` file in the same directory.
116 |
117 | In this example the file is named `diamonhead.waypoints` so the converted file will be `diamonhead.waypoints.csv`.
118 |
119 |
120 |
121 | ## [Litchi Mission Hub](https://flylitchi.com/hub)
122 |
123 | Go to the [Litchi Mission Hub](https://flylitchi.com/hub) and log in to your account (top right corner).
124 |
125 | ### Set global settings
126 |
127 | Click on `SETTINGS` in the bottom left corner. Make sure that the marked settings are set as shown in the image
128 | then click `Close`.
129 |
130 |
131 |
132 |
133 |
134 | ### Import file
135 |
136 | Click on `MISSIONS` in the bottom left corner.
137 |
138 | Then select `Import...`.
139 |
140 |
141 |
142 |
143 |
144 | Select the `.csv` file. In this example it is the `diamonhead.waypoints.csv`.
145 |
146 |
147 |
148 | Click `Import to new Mission` to import the flight-plan to the [Litchi Mission Hub](https://flylitchi.com/hub).
149 |
150 |
151 |
152 | ### Imported flight-plan
153 |
154 |
155 |
156 | ### Save the mission
157 |
158 | You need to save the mission to be able to sync it to your control device (mobile phone / tablet).
159 |
160 | To do this you click on `Missions` in the bottom left corner and select `Save...`.
161 |
162 |
163 |
164 | Enter a name for your mission and click on `Save`.
165 |
--------------------------------------------------------------------------------
/mp2litchi/mp2litchi.py:
--------------------------------------------------------------------------------
1 | """
2 | Mission Planner to Litchi Converter by https://github.com/YarostheLaunchpadder
3 | """
4 | import os
5 | from typing import Tuple
6 |
7 | from litchi_wp.enums import AltitudeMode, ActionType
8 | from litchi_wp.waypoint import Waypoint
9 |
10 | from mp2litchi.enums import MPCommand, WarningMessage, ErrorMessage, InfoMessage
11 | from mp2litchi.global_mp_cmd import GlobalMPCmdManager
12 |
13 |
14 | def welcome():
15 | """
16 | Welcome messages
17 | """
18 |
19 | print("WELCOME TO MISSION PLANNER TO LITCHI CONVERTER!\n")
20 | print("Converted files will be saved to the same directory the source file is in.\n")
21 |
22 |
23 | def parse_file(filename: str) -> list[list[str]]:
24 | """
25 |
26 | Args:
27 | filename (str): The path to the file. e.g.: /home/user/any/file.waypoints
28 |
29 | Returns:
30 | A List with sublists each containing the values of each line extracted from the file
31 |
32 | """
33 |
34 | if not filename.endswith(".waypoints"):
35 | filename += ".waypoints"
36 |
37 | # convert to list of single values
38 | with open(filename, "r", encoding='utf-8') as file_handle:
39 | file = file_handle.read()
40 | file = str(file.split("\t"))
41 | file = file.replace("\\n", "', '")
42 | file = file.replace("'", "")
43 | file_list = file.strip('][').split(', ')
44 | if 'WPL' in file_list[0]:
45 | del file_list[0] # remove header
46 | file_lists = [ # pack each line into a sublist
47 | file_list[x:x + 12] for x in range(0, len(file_list), 12)
48 | ]
49 | return file_lists
50 |
51 |
52 | def get_cmd(lst: list[str]) -> MPCommand | None:
53 | """
54 | Extracts the command from a mp waypoint line
55 | """
56 |
57 | try:
58 | return MPCommand(
59 | int(float(
60 | lst[3]
61 | ))
62 | )
63 | except (ValueError, IndexError):
64 | return None
65 |
66 |
67 | def get_delay(line: list[str]) -> float:
68 | """
69 | Extracts the delay from a waypoint
70 |
71 | Args:
72 | line (list[str]): The Line to be parsed
73 |
74 | Returns:
75 | The delay in seconds (float)
76 |
77 | """
78 |
79 | return float(line[4])
80 |
81 |
82 | def check_valid_line(line: list[str]) -> bool:
83 | """
84 | Checks if the line is valid
85 | (better use regexp https://www.w3schools.com/python/python_regex.asp))
86 |
87 | Args:
88 | line (list[str]): The line to be checked
89 |
90 | Returns:
91 | True if line passed the checks
92 |
93 | """
94 |
95 | if len(line) != 12: # invalid lines
96 | return False
97 | return True
98 |
99 |
100 | # pylint: disable=too-many-locals,too-many-branches,too-many-statements
101 | # (better split function so pylint is happy)
102 | def convert(filename: str, set_agl=True) -> Tuple[list[str], list[str], list[str]]:
103 | """
104 | Converts a single file
105 | Args:
106 | filename (str): The path to the file. e.g.: /home/user/any/file.waypoints
107 | set_agl (bool): Sets the AGL flag of the waypoints if True
108 |
109 | Returns:
110 | A tuple of (info, warning, error) messages
111 |
112 | """
113 |
114 | infos: list[str] = [] # List of info messages
115 | warnings: list[str] = [] # List of warning messages
116 | errors: list[str] = [] # List of error messages
117 | speed_not_set = True # Indicator if Mission Planner DO_CHANGE_SPEED is set
118 | if not os.path.exists(filename):
119 | errors.append(ErrorMessage.FILE_NOT_FOUND.value)
120 |
121 | receiver_cmds = [MPCommand.WAYPOINT] # mp commands that can be referenced by action commands
122 | file_list = parse_file(filename)
123 | global_cmd_manager = GlobalMPCmdManager()
124 | waypoint_list: list[Waypoint] = []
125 | temp_wp: Waypoint | None = None
126 |
127 | for row in file_list:
128 | if not check_valid_line(row): # skip invalid lines
129 | if file_list.index(row) == len(file_list) - 1: # is last row
130 | waypoint_list.append(temp_wp) # store waypoint
131 | temp_wp = None
132 | break # was last line and invalid
133 | continue
134 | if row[1] == '1': # skip home location
135 | continue
136 | if get_cmd(row) is MPCommand.TAKEOFF: # ignore any waypoints before takeoff
137 | waypoint_list.clear()
138 | continue
139 | if get_cmd(row) in receiver_cmds:
140 | if temp_wp is not None:
141 | waypoint_list.append(temp_wp) # store waypoint
142 | latitude = float(row[8]) # latitude is at location 8
143 | longitude = float(row[9]) # longitude is at location 9
144 | altitude = float(row[10]) # altitude is at location 10
145 | altitude_mode = AltitudeMode.AGL if set_agl else AltitudeMode.MSL
146 | temp_wp = Waypoint(lat=latitude, lon=longitude, alt=altitude) # create new waypoint
147 | temp_wp.set_altitude(value=altitude, mode=altitude_mode) # set the altitude mode
148 | stay_for = get_delay(row)
149 | if stay_for > 0: # is delay is set for waypoint
150 | if not temp_wp.set_action(
151 | action_type=ActionType.STAY_FOR,
152 | param=int(stay_for * 1000)
153 | ): # waypoint has no free action slot left
154 | errors.append(ErrorMessage.NO_FREE_ACTION_SLOTS.value)
155 | global_cmd_manager.apply_all_active_to_waypoint(
156 | waypoint=temp_wp
157 | ) # apply all active global commands to wp
158 | else: # not a command receiver. Be careful temp_wp might be None
159 | command = get_cmd(row)
160 | if command is None: # skip invalid commands
161 | continue
162 | global_cmd = global_cmd_manager.is_global(command)
163 | if global_cmd:
164 | param = float(row[global_cmd.param_loc])
165 | if command is MPCommand.DO_CHANGE_SPEED:
166 | speed_not_set = False
167 | if param > 15.0:
168 | param = 15.0 # Litchi limits speed to 15 m/s max
169 | warnings.append(
170 | f"Line {file_list.index(row)}: {WarningMessage.SPEED_CAP.value}"
171 | )
172 | elif param < 0.0:
173 | param = 0 # no negative speed in Litchi, replace with cruise speed
174 | warnings.append(
175 | f"Line {file_list.index(row)}: {WarningMessage.SPEED_NEGATIVE.value}"
176 | )
177 | global_cmd_manager.update(command, param)
178 | if temp_wp:
179 | global_cmd_manager.apply_to_waypoint(global_cmd, waypoint=temp_wp)
180 | else:
181 | # handle all non-global commands
182 | match command:
183 | case MPCommand.DO_DIGICAM_CONTROL:
184 | if temp_wp:
185 | if not temp_wp.set_action(
186 | action_type=ActionType.TAKE_PHOTO,
187 | ): # waypoint has no free action slot left
188 | errors.append(
189 | f"Line {file_list.index(row)}: "
190 | f"{ErrorMessage.NO_FREE_ACTION_SLOTS.value}"
191 | )
192 | if file_list.index(row) == len(file_list) - 1: # is last row
193 | waypoint_list.append(temp_wp) # store waypoint
194 | temp_wp = None
195 | output_string = Waypoint.get_header()
196 | for waypoint in waypoint_list:
197 | output_string += waypoint.to_line()
198 |
199 | with open(f"{filename}.csv", "w", encoding='utf-8') as output_file:
200 | output_file.write(output_string)
201 | output_file.close()
202 |
203 | print(f"\nFile saved: {filename}.csv")
204 | if speed_not_set:
205 | infos.append(str(InfoMessage.NO_SPEED_SET.value))
206 | return infos, warnings, errors
207 |
--------------------------------------------------------------------------------