pyflix 0.5__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.
pyflix-0.5/PKG-INFO ADDED
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyflix
3
+ Version: 0.5
4
+ Summary: Python API for Flix drone
5
+ Author-email: Oleg Kalachev <okalachev@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/okalachev/flix/tree/master/tools/pyflix
8
+ Project-URL: Repository, https://github.com/okalachev/flix
9
+ Project-URL: Issues, https://github.com/okalachev/flix/issues
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: pymavlink
13
+
14
+ # Flix Python library
15
+
16
+ The Flix Python library allows you to remotely connect to a Flix quadcopter. It provides access to telemetry data, supports executing CLI commands, and controlling the drone's flight.
17
+
18
+ To use the library, connect to the drone's Wi-Fi. To use it with the simulator, ensure the script runs on the same local network as the simulator.
19
+
20
+ ## Installation
21
+
22
+ If you have cloned the [repo](https://github.com/okalachev/flix), install the library from the repo:
23
+
24
+ ```bash
25
+ cd /path/to/flix/repo
26
+ pip install -e tools
27
+ ```
28
+
29
+ Alternatively, install from pip:
30
+
31
+ ```bash
32
+ pip install pyflix
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ The API is accessed through the `Flix` class:
38
+
39
+ ```python
40
+ from flix import Flix
41
+ flix = Flix() # create a Flix object and wait for connection
42
+ ```
43
+
44
+ ### Telemetry
45
+
46
+ Basic telemetry is available through object properties. The properties names generally match the corresponding variables in the firmware itself:
47
+
48
+ ```python
49
+ print(flix.connected) # True if connected to the drone
50
+ print(flix.mode) # current flight mode (str)
51
+ print(flix.armed) # True if the drone is armed
52
+ print(flix.landed) # True if the drone is landed
53
+ print(flix.attitude) # attitude quaternion [w, x, y, z]
54
+ print(flix.attitude_euler) # attitude as Euler angles [roll, pitch, yaw]
55
+ print(flix.rates) # angular rates [roll_rate, pitch_rate, yaw_rate]
56
+ print(flix.channels) # raw RC channels (list)
57
+ print(flix.motors) # motors outputs (list)
58
+ print(flix.acc) # accelerometer output (list)
59
+ print(flix.gyro) # gyroscope output (list)
60
+ ```
61
+
62
+ > [!NOTE]
63
+ > The library uses the Front-Left-Up coordinate system — the same as in the firmware. All angles are in radians.
64
+
65
+ ### Events
66
+
67
+ The Flix object implements the *Observable* pattern, allowing to listen for events. You can subscribe to events using `on` method:
68
+
69
+ ```python
70
+ flix.on('connected', lambda: print('Connected to Flix'))
71
+ flix.on('disconnected', lambda: print('Disconnected from Flix'))
72
+ flix.on('print', lambda text: print(f'Flix says: {text}'))
73
+ ```
74
+
75
+ You can also wait for specific events using `wait` method. This method returns the data associated with the event:
76
+
77
+ ```python
78
+ gyro = flix.wait('gyro') # wait for gyroscope update
79
+ attitude = flix.wait('attitude', timeout=3) # wait for attitude update, raise TimeoutError after 3 seconds
80
+ ```
81
+
82
+ The `value` argument specifies a condition for filtering events. It can be either an expected value or a callable:
83
+
84
+ ```python
85
+ flix.wait('armed', value=True) # wait until armed
86
+ flix.wait('armed', value=False) # wait until disarmed
87
+ flix.wait('motors', value=lambda motors: all(m == 0 for m in motors)) # wait until all motors stop
88
+ flix.wait('attitude_euler', value=lambda att: att[0] > 0) # wait until roll angle is positive
89
+ ```
90
+
91
+ Full list of events:
92
+
93
+ |Event|Description|Associated data|
94
+ |-----|-----------|----------------|
95
+ |`connected`|Connected to the drone||
96
+ |`disconnected`|Connection is lost||
97
+ |`armed`|Armed state update|Armed state (*bool*)|
98
+ |`mode`|Flight mode update|Flight mode (*str*)|
99
+ |`landed`|Landed state update|Landed state (*bool*)|
100
+ |`print`|The drone sends text to the console|Text|
101
+ |`attitude`|Attitude update|Attitude quaternion (*list*)|
102
+ |`attitude_euler`|Attitude update|Euler angles (*list*)|
103
+ |`rates`|Angular rates update|Angular rates (*list*)|
104
+ |`channels`|Raw RC channels update|Raw RC channels (*list*)|
105
+ |`motors`|Motors outputs update|Motors outputs (*list*)|
106
+ |`acc`|Accelerometer update|Accelerometer output (*list*)|
107
+ |`gyro`|Gyroscope update|Gyroscope output (*list*)|
108
+ |`mavlink`|Received MAVLink message|Message object|
109
+ |`mavlink.<message_name>`|Received specific MAVLink message|Message object|
110
+ |`mavlink.<message_id>`|Received specific MAVLink message|Message object|
111
+ |`value`|Named value update (see below)|Name, value|
112
+ |`value.<name>`|Specific named value update (see bellow)|Value|
113
+
114
+ > [!NOTE]
115
+ > Update events trigger on every new data from the drone, and do not mean the value is changed.
116
+
117
+ ### Common methods
118
+
119
+ Get and set firmware parameters using `get_param` and `set_param` methods:
120
+
121
+ ```python
122
+ print(flix.get_param('PITCH_P')) # get parameter value
123
+ flix.set_param('PITCH_P', 5) # set parameter value
124
+ ```
125
+
126
+ Execute CLI commands using `cli` method. This method returns command response:
127
+
128
+ ```python
129
+ imu = flix.cli('imu') # get detailed IMU data
130
+ time = flix.cli('time') # get detailed time data
131
+ flix.cli('reboot') # reboot the drone
132
+ ```
133
+
134
+ > [!TIP]
135
+ > Use `help` command to get the list of available commands.
136
+
137
+ ### Automatic flight
138
+
139
+ The flight control feature is in development. List of methods intended for automatic flight control:
140
+
141
+ * `set_position`
142
+ * `set_attitude`
143
+ * `set_rates`
144
+ * `set_motors`
145
+ * `set_controls`
146
+ * `set_mode`
147
+
148
+ ## Usage alongside QGroundControl
149
+
150
+ You can use the Flix library alongside the QGroundControl app, using a proxy mode. To do that:
151
+
152
+ 1. Run proxy for `pyflix` and QGroundControl in background:
153
+
154
+ ```bash
155
+ flix-proxy
156
+ ```
157
+
158
+ 2. Go to QGroundControl settings ⇒ *Comm Links*.
159
+ 3. Add new link with the following settings:
160
+ * *Name*: Proxy
161
+ * *Automatically Connect on Start*: ✓
162
+ * *Type*: UDP
163
+ * *Port*: 14560
164
+ 4. Restart QGroundControl.
165
+
166
+ Now you can run `pyflix` scripts and QGroundControl simultaneously.
167
+
168
+ ## Tools
169
+
170
+ The following scripts demonstrate how to use the library:
171
+
172
+ * [`cli.py`](../cli.py) — remote access to the drone's command line interface.
173
+ * [`log.py`](../log.py) — download flight logs from the drone.
174
+ * [`example.py`](../example.py) — a simple example, prints telemetry data and waits for events.
175
+
176
+ ## Advanced usage
177
+
178
+ ### MAVLink
179
+
180
+ You can access the most recently received messages using `messages` property:
181
+
182
+ ```python
183
+ print(flix.messages.get('HEARTBEAT')) # print the latest HEARTBEAT message
184
+ ```
185
+
186
+ You can wait for a specific message using `wait` method:
187
+
188
+ ```python
189
+ heartbeat = flix.wait('mavlink.HEARTBEAT')
190
+ ```
191
+
192
+ You can send raw messages using `mavlink` property:
193
+
194
+ ```python
195
+ from pymavlink.dialects.v20 import common as mavlink
196
+
197
+ flix.mavlink.heartbeat_send(mavlink.MAV_TYPE_GCS, mavlink.MAV_AUTOPILOT_INVALID,
198
+ mavlink.MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, 0, 0)
199
+ ```
200
+
201
+ ### Named values
202
+
203
+ You can pass arbitrary named values from the firmware to the Python script using `NAMED_VALUE_FLOAT`, `NAMED_VALUE_INT`, `DEBUG`, `DEBUG_VECT`, and `DEBUG_FLOAT_ARRAY` MAVLink messages.
204
+
205
+ All these named values will appear in the `values` dictionary:
206
+
207
+ ```python
208
+ print(flix.values['some_value'])
209
+ print(flix.values['some_vector'])
210
+ ```
211
+
212
+ You can send values from the firmware like this (`mavlink.ino`):
213
+
214
+ ```cpp
215
+ // Send float named value
216
+ mavlink_msg_named_value_float_pack(SYSTEM_ID, MAV_COMP_ID_AUTOPILOT1, &msg, t, "some_value", loopRate);
217
+ sendMessage(&msg);
218
+
219
+ // Send vector named value
220
+ mavlink_msg_debug_vect_pack(SYSTEM_ID, MAV_COMP_ID_AUTOPILOT1, &msg, "some_vector", t, gyroBias.x, gyroBias.y, gyroBias.z);
221
+ sendMessage(&msg);
222
+ ```
223
+
224
+ ### Logging
225
+
226
+ You can control Flix library verbosity using Python's `logging` module:
227
+
228
+ ```python
229
+ import logging
230
+
231
+ logger = logging.getLogger('flix')
232
+ logger.setLevel(logging.DEBUG) # be more verbose
233
+ logger.setLevel(logging.WARNING) # be less verbose
234
+ ```
235
+
236
+ ## Stability
237
+
238
+ The library is in development stage. The API is not stable.
@@ -0,0 +1,225 @@
1
+ # Flix Python library
2
+
3
+ The Flix Python library allows you to remotely connect to a Flix quadcopter. It provides access to telemetry data, supports executing CLI commands, and controlling the drone's flight.
4
+
5
+ To use the library, connect to the drone's Wi-Fi. To use it with the simulator, ensure the script runs on the same local network as the simulator.
6
+
7
+ ## Installation
8
+
9
+ If you have cloned the [repo](https://github.com/okalachev/flix), install the library from the repo:
10
+
11
+ ```bash
12
+ cd /path/to/flix/repo
13
+ pip install -e tools
14
+ ```
15
+
16
+ Alternatively, install from pip:
17
+
18
+ ```bash
19
+ pip install pyflix
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ The API is accessed through the `Flix` class:
25
+
26
+ ```python
27
+ from flix import Flix
28
+ flix = Flix() # create a Flix object and wait for connection
29
+ ```
30
+
31
+ ### Telemetry
32
+
33
+ Basic telemetry is available through object properties. The properties names generally match the corresponding variables in the firmware itself:
34
+
35
+ ```python
36
+ print(flix.connected) # True if connected to the drone
37
+ print(flix.mode) # current flight mode (str)
38
+ print(flix.armed) # True if the drone is armed
39
+ print(flix.landed) # True if the drone is landed
40
+ print(flix.attitude) # attitude quaternion [w, x, y, z]
41
+ print(flix.attitude_euler) # attitude as Euler angles [roll, pitch, yaw]
42
+ print(flix.rates) # angular rates [roll_rate, pitch_rate, yaw_rate]
43
+ print(flix.channels) # raw RC channels (list)
44
+ print(flix.motors) # motors outputs (list)
45
+ print(flix.acc) # accelerometer output (list)
46
+ print(flix.gyro) # gyroscope output (list)
47
+ ```
48
+
49
+ > [!NOTE]
50
+ > The library uses the Front-Left-Up coordinate system — the same as in the firmware. All angles are in radians.
51
+
52
+ ### Events
53
+
54
+ The Flix object implements the *Observable* pattern, allowing to listen for events. You can subscribe to events using `on` method:
55
+
56
+ ```python
57
+ flix.on('connected', lambda: print('Connected to Flix'))
58
+ flix.on('disconnected', lambda: print('Disconnected from Flix'))
59
+ flix.on('print', lambda text: print(f'Flix says: {text}'))
60
+ ```
61
+
62
+ You can also wait for specific events using `wait` method. This method returns the data associated with the event:
63
+
64
+ ```python
65
+ gyro = flix.wait('gyro') # wait for gyroscope update
66
+ attitude = flix.wait('attitude', timeout=3) # wait for attitude update, raise TimeoutError after 3 seconds
67
+ ```
68
+
69
+ The `value` argument specifies a condition for filtering events. It can be either an expected value or a callable:
70
+
71
+ ```python
72
+ flix.wait('armed', value=True) # wait until armed
73
+ flix.wait('armed', value=False) # wait until disarmed
74
+ flix.wait('motors', value=lambda motors: all(m == 0 for m in motors)) # wait until all motors stop
75
+ flix.wait('attitude_euler', value=lambda att: att[0] > 0) # wait until roll angle is positive
76
+ ```
77
+
78
+ Full list of events:
79
+
80
+ |Event|Description|Associated data|
81
+ |-----|-----------|----------------|
82
+ |`connected`|Connected to the drone||
83
+ |`disconnected`|Connection is lost||
84
+ |`armed`|Armed state update|Armed state (*bool*)|
85
+ |`mode`|Flight mode update|Flight mode (*str*)|
86
+ |`landed`|Landed state update|Landed state (*bool*)|
87
+ |`print`|The drone sends text to the console|Text|
88
+ |`attitude`|Attitude update|Attitude quaternion (*list*)|
89
+ |`attitude_euler`|Attitude update|Euler angles (*list*)|
90
+ |`rates`|Angular rates update|Angular rates (*list*)|
91
+ |`channels`|Raw RC channels update|Raw RC channels (*list*)|
92
+ |`motors`|Motors outputs update|Motors outputs (*list*)|
93
+ |`acc`|Accelerometer update|Accelerometer output (*list*)|
94
+ |`gyro`|Gyroscope update|Gyroscope output (*list*)|
95
+ |`mavlink`|Received MAVLink message|Message object|
96
+ |`mavlink.<message_name>`|Received specific MAVLink message|Message object|
97
+ |`mavlink.<message_id>`|Received specific MAVLink message|Message object|
98
+ |`value`|Named value update (see below)|Name, value|
99
+ |`value.<name>`|Specific named value update (see bellow)|Value|
100
+
101
+ > [!NOTE]
102
+ > Update events trigger on every new data from the drone, and do not mean the value is changed.
103
+
104
+ ### Common methods
105
+
106
+ Get and set firmware parameters using `get_param` and `set_param` methods:
107
+
108
+ ```python
109
+ print(flix.get_param('PITCH_P')) # get parameter value
110
+ flix.set_param('PITCH_P', 5) # set parameter value
111
+ ```
112
+
113
+ Execute CLI commands using `cli` method. This method returns command response:
114
+
115
+ ```python
116
+ imu = flix.cli('imu') # get detailed IMU data
117
+ time = flix.cli('time') # get detailed time data
118
+ flix.cli('reboot') # reboot the drone
119
+ ```
120
+
121
+ > [!TIP]
122
+ > Use `help` command to get the list of available commands.
123
+
124
+ ### Automatic flight
125
+
126
+ The flight control feature is in development. List of methods intended for automatic flight control:
127
+
128
+ * `set_position`
129
+ * `set_attitude`
130
+ * `set_rates`
131
+ * `set_motors`
132
+ * `set_controls`
133
+ * `set_mode`
134
+
135
+ ## Usage alongside QGroundControl
136
+
137
+ You can use the Flix library alongside the QGroundControl app, using a proxy mode. To do that:
138
+
139
+ 1. Run proxy for `pyflix` and QGroundControl in background:
140
+
141
+ ```bash
142
+ flix-proxy
143
+ ```
144
+
145
+ 2. Go to QGroundControl settings ⇒ *Comm Links*.
146
+ 3. Add new link with the following settings:
147
+ * *Name*: Proxy
148
+ * *Automatically Connect on Start*: ✓
149
+ * *Type*: UDP
150
+ * *Port*: 14560
151
+ 4. Restart QGroundControl.
152
+
153
+ Now you can run `pyflix` scripts and QGroundControl simultaneously.
154
+
155
+ ## Tools
156
+
157
+ The following scripts demonstrate how to use the library:
158
+
159
+ * [`cli.py`](../cli.py) — remote access to the drone's command line interface.
160
+ * [`log.py`](../log.py) — download flight logs from the drone.
161
+ * [`example.py`](../example.py) — a simple example, prints telemetry data and waits for events.
162
+
163
+ ## Advanced usage
164
+
165
+ ### MAVLink
166
+
167
+ You can access the most recently received messages using `messages` property:
168
+
169
+ ```python
170
+ print(flix.messages.get('HEARTBEAT')) # print the latest HEARTBEAT message
171
+ ```
172
+
173
+ You can wait for a specific message using `wait` method:
174
+
175
+ ```python
176
+ heartbeat = flix.wait('mavlink.HEARTBEAT')
177
+ ```
178
+
179
+ You can send raw messages using `mavlink` property:
180
+
181
+ ```python
182
+ from pymavlink.dialects.v20 import common as mavlink
183
+
184
+ flix.mavlink.heartbeat_send(mavlink.MAV_TYPE_GCS, mavlink.MAV_AUTOPILOT_INVALID,
185
+ mavlink.MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, 0, 0)
186
+ ```
187
+
188
+ ### Named values
189
+
190
+ You can pass arbitrary named values from the firmware to the Python script using `NAMED_VALUE_FLOAT`, `NAMED_VALUE_INT`, `DEBUG`, `DEBUG_VECT`, and `DEBUG_FLOAT_ARRAY` MAVLink messages.
191
+
192
+ All these named values will appear in the `values` dictionary:
193
+
194
+ ```python
195
+ print(flix.values['some_value'])
196
+ print(flix.values['some_vector'])
197
+ ```
198
+
199
+ You can send values from the firmware like this (`mavlink.ino`):
200
+
201
+ ```cpp
202
+ // Send float named value
203
+ mavlink_msg_named_value_float_pack(SYSTEM_ID, MAV_COMP_ID_AUTOPILOT1, &msg, t, "some_value", loopRate);
204
+ sendMessage(&msg);
205
+
206
+ // Send vector named value
207
+ mavlink_msg_debug_vect_pack(SYSTEM_ID, MAV_COMP_ID_AUTOPILOT1, &msg, "some_vector", t, gyroBias.x, gyroBias.y, gyroBias.z);
208
+ sendMessage(&msg);
209
+ ```
210
+
211
+ ### Logging
212
+
213
+ You can control Flix library verbosity using Python's `logging` module:
214
+
215
+ ```python
216
+ import logging
217
+
218
+ logger = logging.getLogger('flix')
219
+ logger.setLevel(logging.DEBUG) # be more verbose
220
+ logger.setLevel(logging.WARNING) # be less verbose
221
+ ```
222
+
223
+ ## Stability
224
+
225
+ The library is in development stage. The API is not stable.
@@ -0,0 +1 @@
1
+ from .flix import Flix
@@ -0,0 +1,136 @@
1
+ def ping(self) -> bool:
2
+ self.mav.ping_send(int(time.time() * 1e6), 0, 0, 0)
3
+
4
+ def set_wifi(self, ssid: str, password: str, mode: Literal['ap', 'client']='ap') -> bool:
5
+ if mode != 'ap':
6
+ raise NotImplementedError('Only AP mode is supported')
7
+ self.mav.wifi_config_ap_send(bytes(ssid, 'utf-8'), bytes(password, 'utf-8'))
8
+
9
+ def on_print(self, callback: Callable): # TODO: Callable[[int], str] possible
10
+ pass
11
+
12
+ def on_connected(self, callback: Callable[[], None]):
13
+ pass
14
+
15
+ def on_disconnected(self, callback: Callable[[], None]):
16
+ pass
17
+
18
+ def state(self) -> dict:
19
+ # Send status command to obtain the full internal state
20
+ # > state
21
+ # landed: 1
22
+ # gyroBias: 0 0 0
23
+ # ...
24
+ pass
25
+ # https://chatgpt.com/c/6872a832-5430-800e-8984-3f55a623d783
26
+
27
+ def take_off(self, alt: float, speed: float=0.5):
28
+ # send set_position_target
29
+ raise NotImplementedError
30
+
31
+ def _set_attitude(self, roll: float, pitch: float, yaw: float, thrust: float):
32
+ pass
33
+
34
+ def get_telemetry(self):
35
+ return {'connected': True,
36
+ 'motors': [0, 0, 0, 0],
37
+ 'pitch': 0, #?
38
+ 'attitude': [0, 0, 0],
39
+ # TODO:
40
+ }
41
+
42
+
43
+ # TODO: GCS bridge (TCP)
44
+
45
+
46
+ # TODO: By default: autodiscovery
47
+ # Listen to Flix itself (if not busy) and to QGC Forwarding
48
+ # https://docs.qgroundcontrol.com/Stable_V4.3/en/qgc-user-guide/settings_view/mavlink.html
49
+
50
+
51
+ """Flix quadcopter control library.
52
+
53
+ Attributes:
54
+ connected (bool): True if connected to the drone.
55
+ mode (str): Current flight mode.
56
+ armed (bool): True if the drone is armed.
57
+ """
58
+
59
+ # from pymavlink.mavutil import mavlink
60
+
61
+
62
+ def update_parameters(self):
63
+ self.connection.param_fetch_all()
64
+
65
+
66
+ @property
67
+ def parameters(self) -> dict:
68
+ return {}
69
+
70
+ def quaternion_to_euler(q: Sequence[float]) -> List[float]:
71
+ """Convert quaternion to euler angles (roll, pitch, yaw)"""
72
+ if len(q) != 4:
73
+ raise ValueError('Quaternion must have 4 values')
74
+ w, x, y, z = q
75
+ roll = math.atan2(2 * (y * w - x * z), 1 - 2 * (y**2 + z**2))
76
+ pitch = math.asin(2 * (x * y + w * z))
77
+ yaw = math.atan2(2 * (x * w - y * z), 1 - 2 * (x**2 + y**2))
78
+ return [roll, pitch, yaw]
79
+
80
+
81
+ # TODO: obtaining ESP32 images using ENCAPSULATED_DATA
82
+ # flix.get_camera_image()
83
+ # flix.on_camera_image()
84
+ # flix.get_camera_info()
85
+ # flix.set_camera_settings()
86
+
87
+
88
+ # TODO: close socket on __del__
89
+
90
+ # TODO: don't override telemetry state data, if timestamp decreases
91
+
92
+
93
+ def download_log(self) -> dict:
94
+ return {} # TODO:
95
+
96
+ # TODO: pyflix, flixpy, flixuav, flixlink, flixconn
97
+ # https://chatgpt.com/c/687cb782-2e18-800e-afaa-4ef60876153a
98
+
99
+ # TODO: test print event
100
+
101
+ # TODO: setup.py
102
+
103
+ # TODO:
104
+ # events:
105
+ # connected.true
106
+ # armed.true
107
+ # mode.MANUAL
108
+
109
+ # TODO: on SERIAL_CONTROL print like
110
+ # Flix 0: Text
111
+
112
+ if os.environ.get('DRONEKIT'):
113
+ import dronekit # TODO: remove
114
+
115
+ # TODO: use tuples for properties?
116
+
117
+ # TODO: print_cli
118
+
119
+ def __del__(self):
120
+ # TODO:
121
+ print('Close connection')
122
+ self.connection.close()
123
+
124
+ def set_attitude(self, attitude: List[float], thrust: float):
125
+ # if len(attitude) is 4 then quaternion, if 3 then euler angles
126
+ # TODO: send 3 times (redundancy) for to guarantee delivery on unreliable connection
127
+ # duplicate sending for reliability
128
+ raise NotImplementedError('Automatic flight is not implemented yet')
129
+
130
+
131
+ # set motors
132
+ # https://mavlink.io/en/messages/common.html#SET_ACTUATOR_CONTROL_TARGET ❓
133
+
134
+ @staticmethod
135
+ def _parse_serial_control(msg: mavlink.MAVLink_serial_control_message) -> str:
136
+ return bytes(msg.data)[:msg.count].decode('utf-8') # check
@@ -0,0 +1,324 @@
1
+ # Copyright (c) 2025 Oleg Kalachev <okalachev@gmail.com>
2
+ # Repository: https://github.com/okalachev/flix
3
+
4
+ """Python API for Flix drone."""
5
+
6
+ import os
7
+ import time
8
+ from queue import Queue, Empty
9
+ from typing import Literal, Optional, Callable, List, Dict, Any, Union
10
+ import logging
11
+ import errno
12
+ from threading import Thread, Timer
13
+ from pymavlink import mavutil
14
+ from pymavlink.quaternion import Quaternion
15
+ from pymavlink.dialects.v20 import common as mavlink
16
+
17
+ logger = logging.getLogger('flix')
18
+ if not logger.hasHandlers():
19
+ handler = logging.StreamHandler()
20
+ handler.setFormatter(logging.Formatter('%(name)s - %(levelname)s - %(message)s'))
21
+ logger.addHandler(handler)
22
+ logger.setLevel(logging.INFO)
23
+
24
+ class Flix:
25
+ connected: bool = False
26
+ mode: str = ''
27
+ armed: bool = False
28
+ landed: bool = False
29
+ attitude: List[float]
30
+ attitude_euler: List[float] # roll, pitch, yaw
31
+ rates: List[float]
32
+ channels: List[int]
33
+ motors: List[float]
34
+ acc: List[float]
35
+ gyro: List[float]
36
+
37
+ mav_id: int
38
+ messages: Dict[str, Dict[str, Any]] # MAVLink messages storage
39
+ values: Dict[Union[str, int], Union[float, List[float]]] = {} # named values
40
+
41
+ _connection_timeout = 3
42
+ _print_buffer: str = ''
43
+
44
+ def __init__(self, mav_id: int=1, wait_connection: bool=True):
45
+ if not (0 <= mav_id < 256):
46
+ raise ValueError('mav_id must be in range [0, 255]')
47
+ self._setup_mavlink()
48
+ self.mav_id = mav_id
49
+ self._init_state()
50
+ try:
51
+ # Direct connection
52
+ logger.debug('Listening on port 14550')
53
+ self.connection: mavutil.mavfile = mavutil.mavlink_connection('udpin:0.0.0.0:14550', source_system=255) # type: ignore
54
+ except OSError as e:
55
+ if e.errno != errno.EADDRINUSE:
56
+ raise
57
+ # Port busy - using proxy
58
+ logger.debug('Listening on port 14560 (proxy)')
59
+ self.connection: mavutil.mavfile = mavutil.mavlink_connection('udpin:0.0.0.0:14555', source_system=254) # type: ignore
60
+ self.connection.target_system = mav_id
61
+ self.mavlink: mavlink.MAVLink = self.connection.mav
62
+ self._event_listeners: Dict[str, List[Callable[..., Any]]] = {}
63
+ self.messages = {}
64
+ self._disconnected_timer = Timer(0, self._disconnected)
65
+ self._reader_thread = Thread(target=self._read_mavlink, daemon=True)
66
+ self._reader_thread.start()
67
+ self._heartbeat_thread = Thread(target=self._send_heartbeat, daemon=True)
68
+ self._heartbeat_thread.start()
69
+ if wait_connection:
70
+ self.wait('mavlink.HEARTBEAT')
71
+ time.sleep(0.2) # give some time to receive initial state
72
+
73
+ def _init_state(self):
74
+ self.attitude = [1, 0, 0, 0]
75
+ self.attitude_euler = [0, 0, 0]
76
+ self.rates = [0, 0, 0]
77
+ self.channels = [0, 0, 0, 0, 0, 0, 0, 0]
78
+ self.motors = [0, 0, 0, 0]
79
+ self.acc = [0, 0, 0]
80
+ self.gyro = [0, 0, 0]
81
+
82
+ def on(self, event: str, callback: Callable):
83
+ event = event.lower()
84
+ if event not in self._event_listeners:
85
+ self._event_listeners[event] = []
86
+ self._event_listeners[event].append(callback)
87
+
88
+ def off(self, callback: Callable):
89
+ for event in self._event_listeners:
90
+ if callback in self._event_listeners[event]:
91
+ self._event_listeners[event].remove(callback)
92
+
93
+ def _trigger(self, event: str, *args):
94
+ event = event.lower()
95
+ for callback in self._event_listeners.get(event, []):
96
+ try:
97
+ callback(*args)
98
+ except Exception as e:
99
+ logger.error(f'Error in event listener for event {event}: {e}')
100
+
101
+ def wait(self, event: str, value: Union[Any, Callable[..., bool]] = lambda *args: True, timeout=None) -> Any:
102
+ """Wait for an event"""
103
+ event = event.lower()
104
+ q = Queue()
105
+ def callback(*args):
106
+ if len(args) == 0:
107
+ result = None
108
+ elif len(args) == 1:
109
+ result = args[0]
110
+ else:
111
+ result = args
112
+ if callable(value) and value(*args):
113
+ q.put_nowait(result)
114
+ elif value == result:
115
+ q.put_nowait(result)
116
+ self.on(event, callback)
117
+ try:
118
+ return q.get(timeout=timeout)
119
+ except Empty:
120
+ raise TimeoutError
121
+ finally:
122
+ self.off(callback)
123
+
124
+ @staticmethod
125
+ def _setup_mavlink():
126
+ # otherwise it will use MAVLink 1.0 until connected
127
+ os.environ['MAVLINK20'] = '1'
128
+ mavutil.set_dialect('common')
129
+
130
+ def _read_mavlink(self):
131
+ while True:
132
+ try:
133
+ msg: Optional[mavlink.MAVLink_message] = self.connection.recv_match(blocking=True)
134
+ if msg is None:
135
+ continue
136
+ self._connected()
137
+ msg_dict = msg.to_dict()
138
+ msg_dict['_timestamp'] = time.time() # add timestamp
139
+ self.messages[msg.get_type()] = msg_dict
140
+ self._trigger('mavlink', msg)
141
+ self._trigger(f'mavlink.{msg.get_type()}', msg) # trigger mavlink.<message_type>
142
+ self._trigger(f'mavlink.{msg.get_msgId()}', msg) # trigger mavlink.<message_id>
143
+ self._handle_mavlink_message(msg)
144
+
145
+ except Exception as e:
146
+ logger.error(f'Error reading MAVLink message: {e}')
147
+
148
+ def _handle_mavlink_message(self, msg: mavlink.MAVLink_message):
149
+ if isinstance(msg, mavlink.MAVLink_heartbeat_message):
150
+ self.mode = ['MANUAL', 'ACRO', 'STAB', 'USER'][msg.custom_mode]
151
+ self.armed = msg.base_mode & mavlink.MAV_MODE_FLAG_SAFETY_ARMED != 0
152
+ self._trigger('mode', self.mode)
153
+ self._trigger('armed', self.armed)
154
+
155
+ if isinstance(msg, mavlink.MAVLink_extended_sys_state_message):
156
+ self.landed = msg.landed_state == mavlink.MAV_LANDED_STATE_ON_GROUND
157
+ self._trigger('landed', self.landed)
158
+
159
+ if isinstance(msg, mavlink.MAVLink_attitude_quaternion_message):
160
+ self.attitude = self._mavlink_to_flu([msg.q1, msg.q2, msg.q3, msg.q4])
161
+ self.rates = self._mavlink_to_flu([msg.rollspeed, msg.pitchspeed, msg.yawspeed])
162
+ self.attitude_euler = list(Quaternion(self.attitude).euler) # type: ignore
163
+ self._trigger('attitude', self.attitude)
164
+ self._trigger('attitude_euler', self.attitude_euler)
165
+
166
+ if isinstance(msg, mavlink.MAVLink_rc_channels_raw_message):
167
+ self.channels = [msg.chan1_raw, msg.chan2_raw, msg.chan3_raw, msg.chan4_raw,
168
+ msg.chan5_raw, msg.chan6_raw, msg.chan7_raw, msg.chan8_raw]
169
+ self._trigger('channels', self.channels)
170
+
171
+ if isinstance(msg, mavlink.MAVLink_actuator_output_status_message):
172
+ self.motors = msg.actuator[:4] # type: ignore
173
+ self._trigger('motors', self.motors)
174
+
175
+ if isinstance(msg, mavlink.MAVLink_scaled_imu_message):
176
+ self.acc = self._mavlink_to_flu([msg.xacc / 1000, msg.yacc / 1000, msg.zacc / 1000])
177
+ self.gyro = self._mavlink_to_flu([msg.xgyro / 1000, msg.ygyro / 1000, msg.zgyro / 1000])
178
+ self._trigger('acc', self.acc)
179
+ self._trigger('gyro', self.gyro)
180
+
181
+ if isinstance(msg, mavlink.MAVLink_serial_control_message):
182
+ # new chunk of data
183
+ text = bytes(msg.data)[:msg.count].decode('utf-8', errors='ignore')
184
+ logger.debug(f'Console: {repr(text)}')
185
+ self._trigger('print', text)
186
+ self._print_buffer += text
187
+ if msg.flags & mavlink.SERIAL_CONTROL_FLAG_MULTI == 0:
188
+ # last chunk
189
+ self._trigger('print_full', self._print_buffer)
190
+ self._print_buffer = ''
191
+
192
+ if isinstance(msg, mavlink.MAVLink_statustext_message):
193
+ logger.info(f'Flix #{msg.get_srcSystem()}: {msg.text}')
194
+ self._trigger('status', msg.text)
195
+
196
+ if isinstance(msg, (mavlink.MAVLink_named_value_float_message, mavlink.MAVLink_named_value_int_message)):
197
+ self.values[msg.name] = msg.value
198
+ self._trigger('value', msg.name, msg.value)
199
+ self._trigger(f'value.{msg.name}', msg.value)
200
+
201
+ if isinstance(msg, mavlink.MAVLink_debug_message):
202
+ self.values[msg.ind] = msg.value
203
+ self._trigger('value', msg.ind, msg.value)
204
+ self._trigger(f'value.{msg.ind}', msg.value)
205
+
206
+ if isinstance(msg, mavlink.MAVLink_debug_vect_message):
207
+ self.values[msg.name] = [msg.x, msg.y, msg.z]
208
+ self._trigger('value', msg.name, self.values[msg.name])
209
+ self._trigger(f'value.{msg.name}', self.values[msg.name])
210
+
211
+ if isinstance(msg, mavlink.MAVLink_debug_float_array_message):
212
+ self.values[msg.name] = list(msg.data)
213
+ self._trigger('value', msg.name, self.values[msg.name])
214
+ self._trigger(f'value.{msg.name}', self.values[msg.name])
215
+
216
+ def _send_heartbeat(self):
217
+ while True:
218
+ self.mavlink.heartbeat_send(mavlink.MAV_TYPE_GCS, mavlink.MAV_AUTOPILOT_INVALID, 0, 0, 0)
219
+ time.sleep(1)
220
+
221
+ @staticmethod
222
+ def _mavlink_to_flu(v: List[float]) -> List[float]:
223
+ if len(v) == 3: # vector
224
+ return [v[0], -v[1], -v[2]]
225
+ elif len(v) == 4: # quaternion
226
+ return [v[0], v[1], -v[2], -v[3]]
227
+ else:
228
+ raise ValueError(f'List must have 3 (vector) or 4 (quaternion) elements')
229
+
230
+ @staticmethod
231
+ def _flu_to_mavlink(v: List[float]) -> List[float]:
232
+ return Flix._mavlink_to_flu(v)
233
+
234
+ def _connected(self):
235
+ # Reset disconnection timer
236
+ self._disconnected_timer.cancel()
237
+ self._disconnected_timer = Timer(self._connection_timeout, self._disconnected)
238
+ self._disconnected_timer.start()
239
+
240
+ if not self.connected:
241
+ logger.info('Connection is established')
242
+ self.connected = True
243
+ self._trigger('connected')
244
+
245
+ def _disconnected(self):
246
+ logger.info('Connection is lost')
247
+ self.connected = False
248
+ self._trigger('disconnected')
249
+
250
+ def get_param(self, name: str) -> float:
251
+ if len(name.encode('ascii')) > 16:
252
+ raise ValueError('Parameter name must be 16 characters or less')
253
+ for attempt in range(3):
254
+ try:
255
+ logger.debug(f'Get param {name} (attempt #{attempt + 1})')
256
+ self.mavlink.param_request_read_send(self.mav_id, 0, name.encode('ascii'), -1)
257
+ msg: mavlink.MAVLink_param_value_message = \
258
+ self.wait('mavlink.PARAM_VALUE', value=lambda msg: msg.param_id == name, timeout=0.1)
259
+ return msg.param_value
260
+ except TimeoutError:
261
+ continue
262
+ raise RuntimeError(f'Failed to get parameter {name} after 3 attempts')
263
+
264
+ def set_param(self, name: str, value: float):
265
+ if len(name.encode('ascii')) > 16:
266
+ raise ValueError('Parameter name must be 16 characters or less')
267
+ for attempt in range(3):
268
+ try:
269
+ logger.debug(f'Set param {name} to {value} (attempt #{attempt + 1})')
270
+ self.mavlink.param_set_send(self.mav_id, 0, name.encode('ascii'), value, mavlink.MAV_PARAM_TYPE_REAL32)
271
+ self.wait('mavlink.PARAM_VALUE', value=lambda msg: msg.param_id == name, timeout=0.1)
272
+ return
273
+ except TimeoutError:
274
+ # on timeout try again
275
+ continue
276
+ raise RuntimeError(f'Failed to set parameter {name} to {value} after 3 attempts')
277
+
278
+ def set_position(self, position: List[float], yaw: Optional[float] = None, wait: bool = False, tolerance: float = 0.1):
279
+ raise NotImplementedError('Position control is not implemented yet')
280
+
281
+ def set_attitude(self, attitude: List[float], thrust: float):
282
+ raise NotImplementedError('Automatic flight is not implemented yet')
283
+
284
+ def set_rates(self, rates: List[float], thrust: float):
285
+ raise NotImplementedError('Automatic flight is not implemented yet')
286
+
287
+ def set_motors(self, motors: List[float]):
288
+ if len(motors) != 4:
289
+ raise ValueError('motors must have 4 values')
290
+ if not all(0 <= m <= 1 for m in motors):
291
+ raise ValueError('motors must be in range [0, 1]')
292
+ raise NotImplementedError
293
+
294
+ def set_controls(self, roll: float, pitch: float, yaw: float, throttle: float):
295
+ """Send pilot's controls. Warning: not intended for automatic control"""
296
+ if not (-1 <= roll <= 1 and -1 <= pitch <= 1 and -1 <= yaw <= 1):
297
+ raise ValueError('roll, pitch, yaw must be in range [-1, 1]')
298
+ if not 0 <= throttle <= 1:
299
+ raise ValueError('throttle must be in range [0, 1]')
300
+ self.mavlink.manual_control_send(self.mav_id, roll * 1000, pitch * 1000, yaw * 1000, throttle * 1000, 0) # type: ignore
301
+
302
+ def set_mode(self, mode: Literal['MANUAL', 'ACRO', 'STAB', 'USER']):
303
+ raise NotImplementedError('Setting mode is not implemented yet')
304
+
305
+ def cli(self, cmd: str, wait_response: bool = True) -> str:
306
+ cmd = cmd.strip()
307
+ if cmd == 'reboot':
308
+ wait_response = False # reboot command doesn't respond
309
+ cmd_bytes = (cmd + '\n').encode('utf-8')
310
+ if len(cmd_bytes) > 70:
311
+ raise ValueError(f'Command is too long: {len(cmd_bytes)} > 70')
312
+ cmd_bytes = cmd_bytes.ljust(70, b'\0')
313
+ response_prefix = f'> {cmd}\n'
314
+ for attempt in range(3):
315
+ logger.debug(f'Send command {cmd} (attempt #{attempt + 1})')
316
+ try:
317
+ self.mavlink.serial_control_send(0, 0, 0, 0, len(cmd_bytes), cmd_bytes)
318
+ if not wait_response:
319
+ return ''
320
+ response = self.wait('print_full', timeout=0.1, value=lambda text: text.startswith(response_prefix))
321
+ return response[len(response_prefix):].strip()
322
+ except TimeoutError:
323
+ continue
324
+ raise RuntimeError(f'Failed to send command {cmd} after 3 attempts')
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Proxy for running pyflix library alongside QGroundControl app."""
4
+
5
+ import socket
6
+
7
+ LOCAL = ('0.0.0.0', 14550) # from Flix
8
+ TARGETS = (
9
+ ('127.0.0.1', 14560), # to QGroundControl
10
+ ('127.0.0.1', 14555), # to pyflix
11
+ )
12
+
13
+ def main():
14
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
15
+ sock.bind(LOCAL)
16
+
17
+ source_addr = None
18
+ packets = 0
19
+
20
+ print('Proxy started - run QGroundControl')
21
+
22
+ while True:
23
+ data, addr = sock.recvfrom(1024) # read entire UDP packet
24
+ if addr in TARGETS: # packet from target
25
+ if source_addr is None:
26
+ continue
27
+ sock.sendto(data, source_addr)
28
+ else: # packet from source
29
+ source_addr = addr
30
+ for target in TARGETS:
31
+ sock.sendto(data, target)
32
+
33
+ packets += 1
34
+ print(f'\rPackets: {packets}', end='')
35
+
36
+ if __name__ == '__main__':
37
+ main()
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyflix
3
+ Version: 0.5
4
+ Summary: Python API for Flix drone
5
+ Author-email: Oleg Kalachev <okalachev@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/okalachev/flix/tree/master/tools/pyflix
8
+ Project-URL: Repository, https://github.com/okalachev/flix
9
+ Project-URL: Issues, https://github.com/okalachev/flix/issues
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: pymavlink
13
+
14
+ # Flix Python library
15
+
16
+ The Flix Python library allows you to remotely connect to a Flix quadcopter. It provides access to telemetry data, supports executing CLI commands, and controlling the drone's flight.
17
+
18
+ To use the library, connect to the drone's Wi-Fi. To use it with the simulator, ensure the script runs on the same local network as the simulator.
19
+
20
+ ## Installation
21
+
22
+ If you have cloned the [repo](https://github.com/okalachev/flix), install the library from the repo:
23
+
24
+ ```bash
25
+ cd /path/to/flix/repo
26
+ pip install -e tools
27
+ ```
28
+
29
+ Alternatively, install from pip:
30
+
31
+ ```bash
32
+ pip install pyflix
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ The API is accessed through the `Flix` class:
38
+
39
+ ```python
40
+ from flix import Flix
41
+ flix = Flix() # create a Flix object and wait for connection
42
+ ```
43
+
44
+ ### Telemetry
45
+
46
+ Basic telemetry is available through object properties. The properties names generally match the corresponding variables in the firmware itself:
47
+
48
+ ```python
49
+ print(flix.connected) # True if connected to the drone
50
+ print(flix.mode) # current flight mode (str)
51
+ print(flix.armed) # True if the drone is armed
52
+ print(flix.landed) # True if the drone is landed
53
+ print(flix.attitude) # attitude quaternion [w, x, y, z]
54
+ print(flix.attitude_euler) # attitude as Euler angles [roll, pitch, yaw]
55
+ print(flix.rates) # angular rates [roll_rate, pitch_rate, yaw_rate]
56
+ print(flix.channels) # raw RC channels (list)
57
+ print(flix.motors) # motors outputs (list)
58
+ print(flix.acc) # accelerometer output (list)
59
+ print(flix.gyro) # gyroscope output (list)
60
+ ```
61
+
62
+ > [!NOTE]
63
+ > The library uses the Front-Left-Up coordinate system — the same as in the firmware. All angles are in radians.
64
+
65
+ ### Events
66
+
67
+ The Flix object implements the *Observable* pattern, allowing to listen for events. You can subscribe to events using `on` method:
68
+
69
+ ```python
70
+ flix.on('connected', lambda: print('Connected to Flix'))
71
+ flix.on('disconnected', lambda: print('Disconnected from Flix'))
72
+ flix.on('print', lambda text: print(f'Flix says: {text}'))
73
+ ```
74
+
75
+ You can also wait for specific events using `wait` method. This method returns the data associated with the event:
76
+
77
+ ```python
78
+ gyro = flix.wait('gyro') # wait for gyroscope update
79
+ attitude = flix.wait('attitude', timeout=3) # wait for attitude update, raise TimeoutError after 3 seconds
80
+ ```
81
+
82
+ The `value` argument specifies a condition for filtering events. It can be either an expected value or a callable:
83
+
84
+ ```python
85
+ flix.wait('armed', value=True) # wait until armed
86
+ flix.wait('armed', value=False) # wait until disarmed
87
+ flix.wait('motors', value=lambda motors: all(m == 0 for m in motors)) # wait until all motors stop
88
+ flix.wait('attitude_euler', value=lambda att: att[0] > 0) # wait until roll angle is positive
89
+ ```
90
+
91
+ Full list of events:
92
+
93
+ |Event|Description|Associated data|
94
+ |-----|-----------|----------------|
95
+ |`connected`|Connected to the drone||
96
+ |`disconnected`|Connection is lost||
97
+ |`armed`|Armed state update|Armed state (*bool*)|
98
+ |`mode`|Flight mode update|Flight mode (*str*)|
99
+ |`landed`|Landed state update|Landed state (*bool*)|
100
+ |`print`|The drone sends text to the console|Text|
101
+ |`attitude`|Attitude update|Attitude quaternion (*list*)|
102
+ |`attitude_euler`|Attitude update|Euler angles (*list*)|
103
+ |`rates`|Angular rates update|Angular rates (*list*)|
104
+ |`channels`|Raw RC channels update|Raw RC channels (*list*)|
105
+ |`motors`|Motors outputs update|Motors outputs (*list*)|
106
+ |`acc`|Accelerometer update|Accelerometer output (*list*)|
107
+ |`gyro`|Gyroscope update|Gyroscope output (*list*)|
108
+ |`mavlink`|Received MAVLink message|Message object|
109
+ |`mavlink.<message_name>`|Received specific MAVLink message|Message object|
110
+ |`mavlink.<message_id>`|Received specific MAVLink message|Message object|
111
+ |`value`|Named value update (see below)|Name, value|
112
+ |`value.<name>`|Specific named value update (see bellow)|Value|
113
+
114
+ > [!NOTE]
115
+ > Update events trigger on every new data from the drone, and do not mean the value is changed.
116
+
117
+ ### Common methods
118
+
119
+ Get and set firmware parameters using `get_param` and `set_param` methods:
120
+
121
+ ```python
122
+ print(flix.get_param('PITCH_P')) # get parameter value
123
+ flix.set_param('PITCH_P', 5) # set parameter value
124
+ ```
125
+
126
+ Execute CLI commands using `cli` method. This method returns command response:
127
+
128
+ ```python
129
+ imu = flix.cli('imu') # get detailed IMU data
130
+ time = flix.cli('time') # get detailed time data
131
+ flix.cli('reboot') # reboot the drone
132
+ ```
133
+
134
+ > [!TIP]
135
+ > Use `help` command to get the list of available commands.
136
+
137
+ ### Automatic flight
138
+
139
+ The flight control feature is in development. List of methods intended for automatic flight control:
140
+
141
+ * `set_position`
142
+ * `set_attitude`
143
+ * `set_rates`
144
+ * `set_motors`
145
+ * `set_controls`
146
+ * `set_mode`
147
+
148
+ ## Usage alongside QGroundControl
149
+
150
+ You can use the Flix library alongside the QGroundControl app, using a proxy mode. To do that:
151
+
152
+ 1. Run proxy for `pyflix` and QGroundControl in background:
153
+
154
+ ```bash
155
+ flix-proxy
156
+ ```
157
+
158
+ 2. Go to QGroundControl settings ⇒ *Comm Links*.
159
+ 3. Add new link with the following settings:
160
+ * *Name*: Proxy
161
+ * *Automatically Connect on Start*: ✓
162
+ * *Type*: UDP
163
+ * *Port*: 14560
164
+ 4. Restart QGroundControl.
165
+
166
+ Now you can run `pyflix` scripts and QGroundControl simultaneously.
167
+
168
+ ## Tools
169
+
170
+ The following scripts demonstrate how to use the library:
171
+
172
+ * [`cli.py`](../cli.py) — remote access to the drone's command line interface.
173
+ * [`log.py`](../log.py) — download flight logs from the drone.
174
+ * [`example.py`](../example.py) — a simple example, prints telemetry data and waits for events.
175
+
176
+ ## Advanced usage
177
+
178
+ ### MAVLink
179
+
180
+ You can access the most recently received messages using `messages` property:
181
+
182
+ ```python
183
+ print(flix.messages.get('HEARTBEAT')) # print the latest HEARTBEAT message
184
+ ```
185
+
186
+ You can wait for a specific message using `wait` method:
187
+
188
+ ```python
189
+ heartbeat = flix.wait('mavlink.HEARTBEAT')
190
+ ```
191
+
192
+ You can send raw messages using `mavlink` property:
193
+
194
+ ```python
195
+ from pymavlink.dialects.v20 import common as mavlink
196
+
197
+ flix.mavlink.heartbeat_send(mavlink.MAV_TYPE_GCS, mavlink.MAV_AUTOPILOT_INVALID,
198
+ mavlink.MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, 0, 0)
199
+ ```
200
+
201
+ ### Named values
202
+
203
+ You can pass arbitrary named values from the firmware to the Python script using `NAMED_VALUE_FLOAT`, `NAMED_VALUE_INT`, `DEBUG`, `DEBUG_VECT`, and `DEBUG_FLOAT_ARRAY` MAVLink messages.
204
+
205
+ All these named values will appear in the `values` dictionary:
206
+
207
+ ```python
208
+ print(flix.values['some_value'])
209
+ print(flix.values['some_vector'])
210
+ ```
211
+
212
+ You can send values from the firmware like this (`mavlink.ino`):
213
+
214
+ ```cpp
215
+ // Send float named value
216
+ mavlink_msg_named_value_float_pack(SYSTEM_ID, MAV_COMP_ID_AUTOPILOT1, &msg, t, "some_value", loopRate);
217
+ sendMessage(&msg);
218
+
219
+ // Send vector named value
220
+ mavlink_msg_debug_vect_pack(SYSTEM_ID, MAV_COMP_ID_AUTOPILOT1, &msg, "some_vector", t, gyroBias.x, gyroBias.y, gyroBias.z);
221
+ sendMessage(&msg);
222
+ ```
223
+
224
+ ### Logging
225
+
226
+ You can control Flix library verbosity using Python's `logging` module:
227
+
228
+ ```python
229
+ import logging
230
+
231
+ logger = logging.getLogger('flix')
232
+ logger.setLevel(logging.DEBUG) # be more verbose
233
+ logger.setLevel(logging.WARNING) # be less verbose
234
+ ```
235
+
236
+ ## Stability
237
+
238
+ The library is in development stage. The API is not stable.
@@ -0,0 +1,12 @@
1
+ pyproject.toml
2
+ pyflix/README.md
3
+ pyflix/__init__.py
4
+ pyflix/__outtakes.py
5
+ pyflix/flix.py
6
+ pyflix/proxy.py
7
+ pyflix.egg-info/PKG-INFO
8
+ pyflix.egg-info/SOURCES.txt
9
+ pyflix.egg-info/dependency_links.txt
10
+ pyflix.egg-info/entry_points.txt
11
+ pyflix.egg-info/requires.txt
12
+ pyflix.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ flix-proxy = pyflix.proxy:main
@@ -0,0 +1 @@
1
+ pymavlink
@@ -0,0 +1 @@
1
+ pyflix
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "pyflix"
3
+ version = "0.5"
4
+ description = "Python API for Flix drone"
5
+ authors = [{ name="Oleg Kalachev", email="okalachev@gmail.com" }]
6
+ license = "MIT"
7
+ readme = "pyflix/README.md"
8
+ requires-python = ">=3.8"
9
+ dependencies = [
10
+ "pymavlink",
11
+ ]
12
+
13
+ [project.scripts]
14
+ flix-proxy = "pyflix.proxy:main"
15
+
16
+ [build-system]
17
+ requires = ["setuptools>=61.0"]
18
+ build-backend = "setuptools.build_meta"
19
+
20
+ [tool.setuptools]
21
+ packages = ["pyflix"]
22
+
23
+ [tool.setuptools.package-data]
24
+ pyflix = ["README.md"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/okalachev/flix/tree/master/tools/pyflix"
28
+ Repository = "https://github.com/okalachev/flix"
29
+ Issues = "https://github.com/okalachev/flix/issues"
pyflix-0.5/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+