28 Commits

Author SHA1 Message Date
Oleg Kalachev fa848e589e Use Serial1 for sbus on all platforms 2026-05-10 01:17:00 +03:00
Oleg Kalachev bcacad7ff8 Use Serial1 for for rc on esp32-c3 2026-05-09 19:25:03 +03:00
Oleg Kalachev 3e59688818 Add esp32-c3 build to ci 2026-05-09 18:59:58 +03:00
Oleg Kalachev b62f2f9427 Rename some wifi parameters for better alphabetical sort 2026-05-09 17:55:55 +03:00
Oleg Kalachev 7dfef17165 Add Ina Tix' build
Co-authored-by: Marina Tikhomirova <Ina.tix@yandex.ru>
2026-05-09 17:49:59 +03:00
Oleg Kalachev 8c8046676b Simplify port definition in Makefile 2026-05-09 17:14:29 +03:00
Oleg Kalachev 702ec9792e Some updates in troubleshooting article 2026-05-09 15:57:25 +03:00
Oleg Kalachev 06e2047097 Make motor testing signal 0.2 instead of full power
Testing with full power is too dangerous and inconvenient.
2026-05-08 00:53:19 +03:00
Oleg Kalachev 87480476c2 Fix motors pwm frequency for esp32s3 etc in the docs 2026-05-08 00:47:29 +03:00
Oleg Kalachev 68271c508c Print firmware build date and time in sys command 2026-05-08 00:31:51 +03:00
Oleg Kalachev e81e84e7fc Some updates to docs 2026-05-07 20:15:55 +03:00
Oleg Kalachev 5f1a938d4f Fix imu rotation definition
The X axis should be pointing to the mounting holes, not pins side.
2026-05-07 19:54:16 +03:00
Oleg Kalachev bd270db493 Reduce angle drift by adding level correction to the estimator
Leverage a priori knowledge that the drone's average attitude is level.
Explanation: https://t.me/opensourcequadcopter/158.
2026-05-05 21:27:47 +03:00
Oleg Kalachev dbf24ea611 Expose lpf alpha of rate pids to parameters
Add parameters: CTL_R_RATE_D_A, CTL_P_RATE_D_A, CTL_Y_RATE_D_A.
2026-05-03 15:04:16 +03:00
Oleg Kalachev 08683d696d Some updates in the usage doc 2026-05-01 15:26:59 +03:00
Oleg Kalachev 9ca6841558 Exit auto mode when sticks moved only when mode switch is not configured 2026-04-29 15:07:44 +03:00
Oleg Kalachev 28da2d3c8e Fix in pyflix documentation
pyflix@0.14
2026-04-28 20:46:49 +03:00
Oleg Kalachev c6632ae6e4 Add info on setting flight modes using rc mode switch 2026-04-28 20:43:52 +03:00
Oleg Kalachev 35ca754583 Fix Vector::rotationVectorBetween implementation for parallel vectors 2026-04-28 15:38:52 +03:00
Oleg Kalachev 2ccda03573 Implement motors output desaturation
So the drone continues stabilization on max thrust.
2026-04-28 13:23:42 +03:00
Oleg Kalachev 485a39e740 Disable wi-fi power save to improve responsiveness 2026-04-27 16:46:36 +03:00
Oleg Kalachev 9bffe5b52f Some fixes in docs 2026-04-26 06:05:02 +03:00
Oleg Kalachev d6a79d6c66 Pass acc data in mG in SCALED_IMU to comply with mavlink standard
https://mavlink.io/en/messages/common.html#SCALED_IMU
pyflix@0.13
2026-04-24 07:42:39 +03:00
Oleg Kalachev 350a82bfed Minor fix 2026-04-23 15:34:54 +03:00
Oleg Kalachev 6e439859bc Move disabling brown-out code to power subsystem 2026-04-23 15:06:07 +03:00
Oleg Kalachev 835b2243e8 Minor fix in sys command
String works with printf %s, but actually it's a UB.
2026-04-23 07:25:59 +03:00
Oleg Kalachev ed4e2d87d1 Fix imu command output
Gyro field contained filtered gyro instead of scaled only gyro.
2026-04-23 07:12:25 +03:00
Oleg Kalachev 51cd5fc691 Implement battery voltage monitoring
Add power subsystem.
Add PWR_VOLT_PIN, PWR_VOLT_SCALE, PWR_VOLT_LPF_A parameters.
Support BATTERY_STATUS mavlink messages streaming.
Add pw cli command.
Add voltage field to pyflix library.
pyflix@0.12.
2026-04-22 11:35:37 +03:00
33 changed files with 181 additions and 51 deletions
+2
View File
@@ -25,6 +25,8 @@ jobs:
path: flix/build
- name: Build firmware for ESP32-S3
run: make BOARD=esp32:esp32:esp32s3
- name: Build firmware for ESP32-C3
run: make BOARD=esp32:esp32:esp32c3
- name: Check c_cpp_properties.json
run: tools/check_c_cpp_properties.py
+1 -2
View File
@@ -1,6 +1,5 @@
BOARD = esp32:esp32:d1_mini32
PORT := $(wildcard /dev/serial/by-id/usb-Silicon_Labs_CP21* /dev/serial/by-id/usb-1a86_USB_Single_Serial_* /dev/cu.usbserial-*)
PORT := $(strip $(PORT))
PORT := $(strip $(wildcard /dev/serial/by-id/usb-Silicon_Labs_CP21* /dev/serial/by-id/usb-1a86_USB_Single_Serial_* /dev/cu.usbserial-* /dev/cu.usbmodem*))
build: .dependencies
arduino-cli compile --fqbn $(BOARD) flix
+9 -5
View File
@@ -53,7 +53,7 @@ The simulator is implemented using Gazebo and runs the original Arduino code:
<img src="docs/img/simulator1.png" width=500 alt="Flix simulator">
## Documentation
## Documentation articles
1. [Assembly instructions](docs/assembly.md).
2. [Usage: build, setup and flight](docs/usage.md).
@@ -75,10 +75,10 @@ Additional articles:
|IMU (and barometer¹) board|GY91, MPU-9265 (or other MPU9250/MPU6500 board)<br>ICM20948V2 (ICM20948)³<br>GY-521 (MPU-6050)³⁻¹|<img src="docs/img/gy-91.jpg" width=90 align=center><br><img src="docs/img/icm-20948.jpg" width=100><br><img src="docs/img/gy-521.jpg" width=100>|1|
|Boost converter (optional, for more stable power supply)|5V output|<img src="docs/img/buck-boost.jpg" width=100>|1|
|Motor|8520 3.7V brushed motor.<br>Motor with exact 3.7V voltage is needed, not ranged working voltage (3.7V — 6V).<br>Make sure the motor shaft diameter and propeller hole diameter match!|<img src="docs/img/motor.jpeg" width=100>|4|
|Propeller|55 mm (alternatively 65 mm)|<img src="docs/img/prop.jpg" width=100>|4|
|Propeller|55 mm or 65 mm|<img src="docs/img/prop.jpg" width=100>|4|
|MOSFET (transistor)|100N03A or [analog](https://t.me/opensourcequadcopter/33)|<img src="docs/img/100n03a.jpg" width=100>|4|
|Pull-down resistor|10 kΩ|<img src="docs/img/resistor10k.jpg" width=100>|4|
|3.7V Li-Po battery|LW 952540 (or any compatible by the size)|<img src="docs/img/battery.jpg" width=100>|1|
|Pull-down resistor<br>Voltage measurement resistor|10 kΩ|<img src="docs/img/resistor10k.jpg" width=100>|6|
|3.7V Li-Po battery|LW 952540 (or any compatible by the size).<br>Make sure the battery has enough discharge rate — 25C or more!|<img src="docs/img/battery.jpg" width=100>|1|
|Battery connector cable|MX2.0 2P female|<img src="docs/img/mx.png" width=100>|1|
|Li-Po Battery charger|Any|<img src="docs/img/charger.jpg" width=100>|1|
|Screws for IMU board mounting|M3x5|<img src="docs/img/screw-m3.jpg" width=100>|2|
@@ -154,7 +154,11 @@ You can see a user-contributed [variant of complete circuit diagram](https://mir
|VIN|VCC (or 3.3V depending on the receiver)|
|Signal (TX)|GPIO4¹|
*¹ — UART2 RX pin was [changed](https://docs.espressif.com/projects/arduino-esp32/en/latest/migration_guides/2.x_to_3.0.html#id14) to GPIO4 in Arduino ESP32 core 3.0.*
*¹ — UART2 RX pin was [changed](https://docs.espressif.com/projects/arduino-esp32/en/latest/migration_guides/2.x_to_3.0.html#id14) to GPIO4 in Arduino ESP32 core 3.0.*
* Optionally connect the battery voltage divider for voltage monitoring to any ADC1 pin (e. g. *GPIO32* on ESP32, *GPIO3* on ESP32S3).
ESP32 and ESP32S3 [can measure](https://docs.espressif.com/projects/arduino-esp32/en/latest/api/adc.html#analogsetattenuation) up to 3.1 V and ESP32S3/ESP32C3 can measure up to 2.5 V, so choose the voltage divider resistors accordingly.
## Resources
+2
View File
@@ -28,6 +28,8 @@ Soldered components ([schematics variant](https://miro.com/app/board/uXjVN-dTjoo
<img src="img/assembly/7.jpg" width=600>
See an alternative assembly process photos here: https://drive.google.com/drive/folders/1FG5BH9RCzdf1XmJcC70PymiRMXcz6Fx7?usp=sharing.
## Motor directions
> [!WARNING]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+13 -7
View File
@@ -12,20 +12,25 @@ Do the following:
Do the following:
* **Check the battery voltage**. Use a multimeter to measure the battery voltage. It should be in range of 3.7-4.2 V.
* **Check the battery voltage**. Use a multimeter to measure the battery voltage. The fully charged battery should have about 4.2V.
* **Check the battery you use has enough discharge current**. The battery should be able to provide 15A of current. So the C-rating for a 1000 mAh battery should be at least 15C (higher is better).
* **Check if there are some startup errors**. Connect the ESP32 to the computer and check the Serial Monitor output. Use the Reset button or `reboot` command to see the whole startup output.
* **Check the baudrate is correct**. If you see garbage characters in the Serial Monitor, make sure the baudrate is set to 115200.
* **Make sure correct IMU model is chosen**. If using ICM-20948/MPU-6050 board, change `MPU9250` to `ICM20948`/`MPU6050` in the `imu.ino` file.
* **Check if the console is working**. Perform `help` command in Serial Monitor. You should see the list of available commands. You can also access the console using QGroundControl *(Vehicle Setup**Analyze Tools**MAVLink Console)*.
* **Configure QGroundControl correctly before connecting to the drone** if you use it to control the drone. Go to the settings and enable *Virtual Joystick*. *Auto-Center Throttle* setting **should be disabled**.
* **If QGroundControl doesn't connect**, you might need to disable the firewall and/or VPN on your computer.
* **Make sure correct IMU model is chosen**. If using ICM-20948/MPU-6050 board, change `MPU9250` to `ICM20948`/`MPU6050` in the `imu.ino` file.
* **Check the IMU is working**. Perform `imu` command and check its output:
* The `status` field should be `OK`.
* The `rate` field should be about 1000 (Hz).
* The `accel` and `gyro` fields should change as you move the drone.
* **Calibrate the accelerometer.** if is wasn't done before. Type `ca` command in Serial Monitor and follow the instructions.
* **Check the attitude estimation**. Connect to the drone using QGroundControl. Rotate the drone in different orientations and check if the attitude estimation shown in QGroundControl is correct.
* **Check the IMU orientation is set correctly**. If the attitude estimation is rotated, set the correct IMU orientation as described in the [tutorial](usage.md#define-imu-orientation).
* **Calibrate the accelerometer.** if is wasn't done before. Type `ca` command in Serial Monitor and follow the instructions.
* **Check the attitude estimation**. Connect to the drone using QGroundControl. Rotate the drone in different orientations and check if the attitude estimation is shown exactly as on the video below:
<a href="https://youtu.be/yVRN23-GISU"><img width=200 src="https://i3.ytimg.com/vi/yVRN23-GISU/maxresdefault.jpg"></a>
* **Check the IMU output**. Connect to the drone using QGroundControl on your computer. Go to the *Analyze* tab, *MAVLINK Inspector*. Plot the data from the `SCALED_IMU` message. The gyroscope and accelerometer data should change according to the drone movement.
* **Check the motors type**. Motors with exact 3.7V voltage are needed, not ranged working voltage (3.7V — 6V).
* **Check the motors**. Perform the following commands using Serial Monitor:
* `mfr` — should rotate front right motor (counter-clockwise).
@@ -33,9 +38,10 @@ Do the following:
* `mrl` — should rotate rear left motor (counter-clockwise).
* `mrr` — should rotate rear right motor (clockwise).
* **Check the propeller directions are correct**. Make sure your propeller types (A or B) are installed as on the picture:
<img src="img/user/peter_ukhov-2/1.jpg" width="200">
* **Check the remote control**. Using `rc` command, check the control values reflect your sticks movement. All the controls should change between -1 and 1, and throttle between 0 and 1.
* **If using SBUS receiver**:
* **If using an SBUS receiver**:
* **Define the used GPIO pin** in `RC_RX_PIN` parameter.
* **Calibrate the RC** using `cr` command in the console.
* **Check the IMU output using QGroundControl**. Connect to the drone using QGroundControl on your computer. Go to the *Analyze* tab, *MAVLINK Inspector*. Plot the data from the `SCALED_IMU` message. The gyroscope and accelerometer data should change according to the drone movement.
* **Check the controls** using `rc` command. All the controls should change between -1 and 1, and the throttle between 0 and 1.
+19 -8
View File
@@ -112,9 +112,9 @@ You can also work with parameters using `p` command in the console. Parameter na
### Define IMU orientation
Use parameters, to define the IMU board axes orientation relative to the drone's axes: `IMU_ROT_ROLL`, `IMU_ROT_PITCH`, and `IMU_ROT_YAW`.
The IMU orientation (relative to the drone's axes) is defined using the parameters: `IMU_ROT_ROLL`, `IMU_ROT_PITCH`, and `IMU_ROT_YAW`.
The drone has *X* axis pointing forward, *Y* axis pointing left, and *Z* axis pointing up, and the supported IMU boards have *X* axis pointing to the pins side and *Z* axis pointing up from the component side:
The drone has *X* axis pointing forward, *Y* axis pointing left, and *Z* axis pointing up, and the supported IMU boards have *X* axis pointing to the mounting holes side and *Z* axis pointing up from the component side:
<img src="img/imu-axes.png" width="200">
@@ -122,10 +122,10 @@ Use the following table to set the parameters for common IMU orientations:
|Orientation|Parameters|Orientation|Parameters|
|:-:|-|-|-|
|<img src="img/imu-rot-1.png" width="180">|`IMU_ROT_ROLL` = 0<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 0 |<img src="img/imu-rot-5.png" width="180">|`IMU_ROT_ROLL` = 3.142<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 0|
|<img src="img/imu-rot-2.png" width="180">|`IMU_ROT_ROLL` = 0<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 1.571|<img src="img/imu-rot-6.png" width="180">|`IMU_ROT_ROLL` = 3.142<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = -1.571|
|<img src="img/imu-rot-3.png" width="180">|`IMU_ROT_ROLL` = 0<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 3.142|<img src="img/imu-rot-7.png" width="180">|`IMU_ROT_ROLL` = 3.142<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 3.142|
|<img src="img/imu-rot-4.png" width="180"><br>☑️ **Default**|<br>`IMU_ROT_ROLL` = 0<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = -1.571|<img src="img/imu-rot-8.png" width="180">|`IMU_ROT_ROLL` = 3.142<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 1.571|
|<img src="img/imu-rot-3.png" width="180">|`IMU_ROT_ROLL` = 0<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 0 |<img src="img/imu-rot-7.png" width="180">|`IMU_ROT_ROLL` = 3.142<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 0|
|<img src="img/imu-rot-2.png" width="180">|`IMU_ROT_ROLL` = 0<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = -1.571|<img src="img/imu-rot-6.png" width="180">|`IMU_ROT_ROLL` = 3.142<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = -1.571|
|<img src="img/imu-rot-1.png" width="180">|`IMU_ROT_ROLL` = 0<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 3.142|<img src="img/imu-rot-5.png" width="180">|`IMU_ROT_ROLL` = 3.142<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 3.142|
|<img src="img/imu-rot-4.png" width="180"><br>☑️ **Default**|<br>`IMU_ROT_ROLL` = 0<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 1.571|<img src="img/imu-rot-8.png" width="180">|`IMU_ROT_ROLL` = 3.142<br>`IMU_ROT_PITCH` = 0<br>`IMU_ROT_YAW` = 1.571|
### Calibrate accelerometer
@@ -138,6 +138,8 @@ Before flight you need to calibrate the accelerometer:
If using non-default motor pins, set the pin numbers using the parameters: `MOTOR_PIN_FL`, `MOTOR_PIN_FR`, `MOTOR_PIN_RL`, `MOTOR_PIN_RR` (front-left, front-right, rear-left, rear-right respectively).
Certain ESP32 models (such as ESP32-S3) support a lower maximum PWM frequency; on these boards the parameter `MOT_PWM_FREQ` should be set to 38000 Hz.
If using brushless motors and ESCs:
1. Set the appropriate PWM using the parameters: `MOT_PWM_STOP`, `MOT_PWM_MIN`, and `MOT_PWM_MAX` (1000, 1000, and 2000 is typical).
@@ -146,6 +148,15 @@ If using brushless motors and ESCs:
> [!CAUTION]
> **Remove the props when configuring the motors!** If improperly configured, you may not be able to stop them.
### Battery voltage monitoring
ESP32 ADC can measure only up to 3.3 V, so you need to use a voltage divider to monitor the battery voltage. To enable voltage measurement, set the following parameters:
1. `PWR_VOLT_PIN` — GPIO pin number where the voltage divider is connected (*-1* to disable).
2. `PWR_VOLT_SCALE` — voltage divider coefficient (*2* for two equal resistors).
After this setup, you should see the battery voltage in QGroundControl top panel or using `pw` command in the console.
### Important: check everything works
1. Check the IMU is working: perform `imu` command in the console and check the output:
@@ -236,7 +247,7 @@ When finished flying, **disarm** the drone, moving the left stick to the bottom
### Flight modes
Flight mode is changed using mode switch on the remote control (if configured) or using the console commands. The main flight mode is *STAB*.
Flight mode is changed using mode switch on the remote control (if configured) or using the console commands. The main flight mode is *STAB*. In order to change modes using SBUS remote control, set the parameters: `CTL_FLT_MODE_0`, `CTL_FLT_MODE_1`, and `CTL_FLT_MODE_2` to required mode numbers (0 for *RAW*, 1 for *ACRO*, 2 for *STAB*, 3 for *AUTO*).
#### STAB
@@ -257,7 +268,7 @@ In this mode, the pilot controls the angular rates. This control method is diffi
In this mode, the pilot inputs are ignored (except the mode switch). The drone can be controlled using [pyflix](../tools/pyflix/) Python library, or by modifying the firmware to implement the needed behavior.
If the pilot moves the control sticks, the drone will switch back to *STAB* mode.
If the pilot moves the control sticks and mode switch is not configured, the drone will switch back to *STAB* mode.
## Wi-Fi configuration
+8
View File
@@ -4,6 +4,14 @@ This page contains user-built drones based on the Flix project. Publish your pro
---
Author: [Ina Tix](https://t.me/ina_tix).<br>
Description: XR2981 based DC-DC converter, ELRS MINI 2.4GHz RX SX1280 receiver (SBUS interface), Radiomaster TX12 remote control.<br>
[Flight validation](https://drive.google.com/file/d/1yqkKNuz4R_yxGqUNQxVpixJbXqEEcUSj/view?usp=share_link).
<img src="img/user/ina_tix/1.jpg" height=200> <img src="img/user/ina_tix/2.jpg" height=200> <img src="img/user/ina_tix/3.jpg" height=200>
---
Author: [FanBy0ru](https://https://github.com/FanBy0ru).<br>
Description: custom 3D-printed frame.<br>
Frame STLs and flight validation: https://cults3d.com/en/3d-model/gadget/armature-pour-flix-drone.
+6 -1
View File
@@ -16,6 +16,7 @@ extern float controlTime;
extern int mode;
extern bool armed;
extern LowPassFilter<Vector> gyroBiasFilter;
extern float voltage;
const char* motd =
"\nWelcome to\n"
@@ -39,6 +40,7 @@ const char* motd =
"disarm - disarm the drone\n"
"raw/stab/acro/auto - set mode\n"
"rc - show RC data\n"
"pw - show power info\n"
"wifi - show Wi-Fi info\n"
"ap <ssid> <password> - setup Wi-Fi access point\n"
"sta <ssid> <password> - setup Wi-Fi client mode\n"
@@ -135,6 +137,8 @@ void doCommand(String str, bool echo = false) {
print("time: %.1f\n", controlTime);
print("mode: %s\n", getModeName());
print("armed: %d\n", armed);
} else if (command == "pw") {
print("Voltage: %.1f V\n", voltage);
} else if (command == "wifi") {
printWiFiInfo();
} else if (command == "ap") {
@@ -164,6 +168,7 @@ void doCommand(String str, bool echo = false) {
print("Chip: %s\n", ESP.getChipModel());
print("Temperature: %.1f °C\n", temperatureRead());
print("Free heap: %d\n", ESP.getFreeHeap());
print("Firmware: " __DATE__ " " __TIME__ "\n");
// Print tasks table
print("Num Task Stack Prio Core CPU%%\n");
int taskCount = uxTaskGetNumberOfTasks();
@@ -174,7 +179,7 @@ void doCommand(String str, bool echo = false) {
String core = systemState[i].xCoreID == tskNO_AFFINITY ? "*" : String(systemState[i].xCoreID);
int cpuPercentage = systemState[i].ulRunTimeCounter / (totalRunTime / 100);
print("%-5d%-20s%-7d%-6d%-6s%d\n",systemState[i].xTaskNumber, systemState[i].pcTaskName,
systemState[i].usStackHighWaterMark, systemState[i].uxCurrentPriority, core, cpuPercentage);
systemState[i].usStackHighWaterMark, systemState[i].uxCurrentPriority, core.c_str(), cpuPercentage);
}
delete[] systemState;
#endif
+14 -1
View File
@@ -67,7 +67,7 @@ void control() {
void interpretControls() {
if (controlMode < 0.25) mode = flightModes[0];
else if (controlMode < 0.75) mode = flightModes[1];
else if (controlMode <= 0.75) mode = flightModes[1];
else if (controlMode > 0.75) mode = flightModes[2];
if (mode == AUTO) return; // pilot is not effective in AUTO mode
@@ -149,12 +149,25 @@ void controlTorque() {
motors[MOTOR_REAR_LEFT] = thrustTarget + torqueTarget.x + torqueTarget.y - torqueTarget.z;
motors[MOTOR_REAR_RIGHT] = thrustTarget - torqueTarget.x + torqueTarget.y + torqueTarget.z;
desaturate(motors[MOTOR_FRONT_LEFT], motors[MOTOR_FRONT_RIGHT], motors[MOTOR_REAR_LEFT], motors[MOTOR_REAR_RIGHT]);
motors[0] = constrain(motors[0], 0, 1);
motors[1] = constrain(motors[1], 0, 1);
motors[2] = constrain(motors[2], 0, 1);
motors[3] = constrain(motors[3], 0, 1);
}
void desaturate(float& a, float& b, float& c, float& d) {
float maxThrust = max(max(a, b), max(c, d));
if (maxThrust > 1) {
float diff = maxThrust - 1;
a -= diff;
b -= diff;
c -= diff;
d -= diff;
}
}
const char* getModeName() {
switch (mode) {
case RAW: return "RAW";
+11
View File
@@ -13,11 +13,13 @@ Quaternion attitude; // estimated attitude
bool landed;
float accWeight = 0.003;
float levelWeight = 0.0002;
LowPassFilter<Vector> ratesFilter(0.2); // cutoff frequency ~ 40 Hz
void estimate() {
applyGyro();
applyAcc();
applyLevel();
}
void applyGyro() {
@@ -42,3 +44,12 @@ void applyAcc() {
// apply correction
attitude = Quaternion::rotate(attitude, Quaternion::fromRotationVector(correction));
}
void applyLevel() {
if (landed) return;
// assume the pilot keeps the drone more or less level in flight
Vector up = Quaternion::rotateVector(Vector(0, 0, 1), attitude);
Vector correction = Vector::rotationVectorBetween(Vector(0, 0, 1), up) * levelWeight;
attitude = Quaternion::rotate(attitude, Quaternion::fromRotationVector(correction));
}
+2 -1
View File
@@ -18,8 +18,8 @@ extern float motors[4];
void setup() {
Serial.begin(115200);
print("Initializing flix\n");
disableBrownOut();
setupParameters();
setupPower();
setupLED();
setLED(true);
setupMotors();
@@ -39,6 +39,7 @@ void loop() {
sendMotors();
handleInput();
processMavlink();
readVoltage();
logData();
syncParameters();
}
+2 -2
View File
@@ -10,7 +10,7 @@
#include "util.h"
MPU9250 imu(SPI);
Vector imuRotation(0, 0, -PI / 2); // imu orientation as Euler angles
Vector imuRotation(0, 0, PI / 2); // imu orientation as Euler angles
Vector gyro; // gyroscope output, rad/s
Vector gyroBias;
@@ -121,7 +121,7 @@ void printIMUInfo() {
print("model: %s\n", imu.getModel());
print("who am I: 0x%02X\n", imu.whoAmI());
print("rate: %.0f\n", loopRate);
print("gyro: %f %f %f\n", rates.x, rates.y, rates.z);
print("gyro: %f %f %f\n", gyro.x, gyro.y, gyro.z);
print("acc: %f %f %f\n", acc.x, acc.y, acc.z);
imu.waitForData();
Vector rawGyro, rawAcc;
+9 -1
View File
@@ -7,6 +7,7 @@
#include "util.h"
extern float controlTime;
extern float voltage;
int mavlinkSysId = 1;
Rate telemetryFast(10);
@@ -39,6 +40,13 @@ void sendMavlink() {
mavlink_msg_extended_sys_state_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg,
MAV_VTOL_STATE_UNDEFINED, landed ? MAV_LANDED_STATE_ON_GROUND : MAV_LANDED_STATE_IN_AIR);
sendMessage(&msg);
uint16_t voltages[] = {voltage * 1000, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX};
uint16_t voltagesExt[] = {0, 0, 0, 0};
float remaining = constrain(mapf(voltage, 3.4, 4.2, 0, 1), 0, 1);
mavlink_msg_battery_status_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, 0, MAV_BATTERY_FUNCTION_ALL,
MAV_BATTERY_TYPE_LIPO, INT16_MAX, voltages, -1, -1, -1, remaining * 100, 0, MAV_BATTERY_CHARGE_STATE_OK, voltagesExt, 0, 0);
sendMessage(&msg);
}
if (telemetryFast && mavlinkConnected) {
@@ -57,7 +65,7 @@ void sendMavlink() {
sendMessage(&msg);
mavlink_msg_scaled_imu_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, time,
acc.x * 1000, -acc.y * 1000, -acc.z * 1000, // convert to frd
acc.x / ONE_G * 1000, -acc.y / ONE_G * 1000, -acc.z / ONE_G * 1000, // convert to frd
gyro.x * 1000, -gyro.y * 1000, -gyro.z * 1000,
0, 0, 0, 0);
sendMessage(&msg);
+1 -1
View File
@@ -54,7 +54,7 @@ bool motorsActive() {
void testMotor(int n) {
print("Testing motor %d\n", n);
motors[n] = 1;
motors[n] = 0.2;
delay(50); // ESP32 may need to wait until the end of the current cycle to change duty https://github.com/espressif/arduino-esp32/issues/5306
sendMotors();
pause(3);
+13 -2
View File
@@ -12,6 +12,9 @@ extern int rollChannel, pitchChannel, throttleChannel, yawChannel, armedChannel,
extern int rcRxPin;
extern int wifiMode, udpLocalPort, udpRemotePort;
extern float rcLossTimeout, descendTime;
extern int voltagePin;
extern float voltageScale;
extern LowPassFilter<float> voltageFilter;
Preferences storage;
@@ -33,13 +36,16 @@ Parameter parameters[] = {
{"CTL_R_RATE_I", &rollRatePID.i},
{"CTL_R_RATE_D", &rollRatePID.d},
{"CTL_R_RATE_WU", &rollRatePID.windup},
{"CTL_R_RATE_D_A", &rollRatePID.lpf.alpha},
{"CTL_P_RATE_P", &pitchRatePID.p},
{"CTL_P_RATE_I", &pitchRatePID.i},
{"CTL_P_RATE_D", &pitchRatePID.d},
{"CTL_P_RATE_WU", &pitchRatePID.windup},
{"CTL_P_RATE_D_A", &pitchRatePID.lpf.alpha},
{"CTL_Y_RATE_P", &yawRatePID.p},
{"CTL_Y_RATE_I", &yawRatePID.i},
{"CTL_Y_RATE_D", &yawRatePID.d},
{"CTL_Y_RATE_D_A", &yawRatePID.lpf.alpha},
{"CTL_R_P", &rollPID.p},
{"CTL_R_I", &rollPID.i},
{"CTL_R_D", &rollPID.d},
@@ -67,6 +73,7 @@ Parameter parameters[] = {
{"IMU_GYRO_BIAS_A", &gyroBiasFilter.alpha},
// estimate
{"EST_ACC_WEIGHT", &accWeight},
{"EST_LVL_WEIGHT", &levelWeight},
{"EST_RATES_LPF_A", &ratesFilter.alpha},
// motors
{"MOT_PIN_FL", &motorPins[MOTOR_FRONT_LEFT], setupMotors},
@@ -103,12 +110,16 @@ Parameter parameters[] = {
{"RC_MODE", &modeChannel},
// wifi
{"WIFI_MODE", &wifiMode},
{"WIFI_LOC_PORT", &udpLocalPort},
{"WIFI_REM_PORT", &udpRemotePort},
{"WIFI_PORT_LOC", &udpLocalPort},
{"WIFI_PORT_REM", &udpRemotePort},
// mavlink
{"MAV_SYS_ID", &mavlinkSysId},
{"MAV_RATE_SLOW", &telemetrySlow.rate},
{"MAV_RATE_FAST", &telemetryFast.rate},
// power
{"PWR_VOLT_PIN", &voltagePin},
{"PWR_VOLT_SCALE", &voltageScale},
{"PWR_VOLT_LPF_A", &voltageFilter.alpha},
// safety
{"SF_RC_LOSS_TIME", &rcLossTimeout},
{"SF_DESCEND_TIME", &descendTime},
+28
View File
@@ -0,0 +1,28 @@
// Copyright (c) 2026 Oleg Kalachev <okalachev@gmail.com>
// Repository: https://github.com/okalachev/flix
// Power management
#include <soc/soc.h>
#include <soc/rtc_cntl_reg.h>
#include "lpf.h"
#include "util.h"
float voltage;
LowPassFilter<float> voltageFilter(0.2);
int voltagePin = -1;
float voltageScale = 2;
void setupPower() {
// Disable reset on low voltage
REG_CLR_BIT(RTC_CNTL_BROWN_OUT_REG, RTC_CNTL_BROWN_OUT_ENA);
}
void readVoltage() {
if (voltagePin < 0) return;
static Rate rate(10);
if (!rate) return;
float v = analogReadMilliVolts(voltagePin) * voltageScale / 1000.0f;
voltage = voltageFilter.update(v);
}
+1 -1
View File
@@ -6,7 +6,7 @@
#include <SBUS.h>
#include "util.h"
SBUS rc(Serial2);
SBUS rc(Serial1);
int rcRxPin = -1; // -1 means disabled
uint16_t channels[16]; // raw rc channels
+2 -2
View File
@@ -37,8 +37,8 @@ void descend() {
void autoFailsafe() {
static float roll, pitch, yaw, throttle;
if (roll != controlRoll || pitch != controlPitch || yaw != controlYaw || abs(throttle - controlThrottle) > 0.05) {
// controls changed
if (mode == AUTO) mode = STAB; // regain control by the pilot
// controls changed and mode switch is not configured
if (mode == AUTO && invalid(controlMode)) mode = STAB; // regain control by the pilot
}
roll = controlRoll;
pitch = controlPitch;
-7
View File
@@ -6,8 +6,6 @@
#pragma once
#include <math.h>
#include <soc/soc.h>
#include <soc/rtc_cntl_reg.h>
const float ONE_G = 9.80665;
extern float t;
@@ -35,11 +33,6 @@ float wrapAngle(float angle) {
return angle;
}
// Disable reset on low voltage
void disableBrownOut() {
REG_CLR_BIT(RTC_CNTL_BROWN_OUT_REG, RTC_CNTL_BROWN_OUT_ENA);
}
// Trim and split string by spaces
void splitString(String& str, String& token0, String& token1, String& token2) {
str.trim();
+16 -3
View File
@@ -105,10 +105,23 @@ public:
}
static Vector rotationVectorBetween(const Vector& a, const Vector& b) {
float an = a.norm();
float bn = b.norm();
if (an < 1e-6 || bn < 1e-6) {
return Vector(0, 0, 0);
}
Vector direction = cross(a, b);
if (direction.zero()) {
// vectors are opposite, return any perpendicular vector
return cross(a, Vector(1, 0, 0));
if (direction.norm() < 1e-6) { // vectors are parallel
if (dot(a, b) > 0) { // same direction
return Vector(0, 0, 0);
}
// opposite direction
Vector perp = cross(a, Vector(1, 0, 0));
if (perp.norm() < 1e-6) {
perp = cross(a, Vector(0, 1, 0));
}
perp.normalize();
return perp * PI;
}
direction.normalize();
float angle = angleBetween(a, b);
+1
View File
@@ -25,6 +25,7 @@ void setupWiFi() {
} else if (wifiMode == W_STA) {
WiFi.begin(storage.getString("WIFI_STA_SSID", "").c_str(), storage.getString("WIFI_STA_PASS", "").c_str());
}
WiFi.setSleep(false); // disable power save
udp.begin(udpLocalPort);
}
+4 -1
View File
@@ -21,6 +21,8 @@
#define degrees(rad) ((rad)*RAD_TO_DEG)
#define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
template<typename T> T max(T a, T b) { return a > b ? a : b; }
template<typename T> T min(T a, T b) { return a < b ? a : b; }
long map(long x, long in_min, long in_max, long out_min, long out_max) {
const long run = in_max - in_min;
@@ -149,7 +151,7 @@ public:
void setRxInvert(bool invert) {};
};
HardwareSerial Serial, Serial2;
HardwareSerial Serial, Serial1, Serial2;
class EspClass {
public:
@@ -166,6 +168,7 @@ void delay(uint32_t ms) {
bool ledcAttach(uint8_t pin, uint32_t freq, uint8_t resolution) { return true; }
bool ledcWrite(uint8_t pin, uint32_t duty) { return true; }
uint32_t ledcChangeFrequency(uint8_t pin, uint32_t freq, uint8_t resolution) { return freq; }
uint32_t analogReadMilliVolts(uint8_t pin) { return 0; }
unsigned long __micros;
unsigned long __resetTime = 0;
+2
View File
@@ -27,11 +27,13 @@ void step();
void computeLoopRate();
void applyGyro();
void applyAcc();
void applyLevel();
void control();
void interpretControls();
void controlAttitude();
void controlRates();
void controlTorque();
void desaturate(float& a, float& b, float& c, float& d);
const char* getModeName();
void sendMotors();
int getDutyCycle(float value);
+1
View File
@@ -27,6 +27,7 @@
#include "mavlink.ino"
#include "motors.ino"
#include "parameters.ino"
#include "power.ino"
#include "rc.ino"
#include "time.ino"
-1
View File
@@ -1,4 +1,3 @@
// Dummy file to make it possible to compile simulator with Flix' util.h
#define WRITE_PERI_REG(addr, val) {}
#define REG_CLR_BIT(_r, _b) {}
+1
View File
@@ -10,6 +10,7 @@ print('Connected:', flix.connected)
print('Mode:', flix.mode)
print('Armed:', flix.armed)
print('Landed:', flix.landed)
print('Voltage:', flix.voltage, 'V')
print('Rates:', *[f'{math.degrees(r):.0f}°/s' for r in flix.rates])
print('Attitude:', *[f'{math.degrees(a):.0f}°' for a in flix.attitude_euler])
print('Motors:', flix.motors)
+4 -2
View File
@@ -24,19 +24,20 @@ pip install pyflix
The API is accessed through the `Flix` class:
```python
from flix import Flix
from pyflix import Flix
flix = Flix() # create a Flix object and wait for connection
```
### Telemetry
Basic telemetry is available through object properties. The property names generally match the corresponding variables in the firmware itself:
Basic telemetry is available through object properties. The property names generally match the corresponding variables in the firmware code:
```python
print(flix.connected) # True if connected to the drone
print(flix.mode) # current flight mode (str)
print(flix.armed) # True if the drone is armed
print(flix.landed) # True if the drone is landed
print(flix.voltage) # battery voltage
print(flix.attitude) # attitude quaternion [w, x, y, z]
print(flix.attitude_euler) # attitude as Euler angles [roll, pitch, yaw]
print(flix.rates) # angular rates [roll_rate, pitch_rate, yaw_rate]
@@ -95,6 +96,7 @@ Full list of events:
|`armed`|Armed state update|Armed state *(bool)*|
|`mode`|Flight mode update|Flight mode *(str)*|
|`landed`|Landed state update|Landed state *(bool)*|
|`voltage`|Battery voltage update|Voltage *(float)*|
|`print`|The drone prints text to the console|Text|
|`attitude`|Attitude update|Attitude quaternion *(list)*|
|`attitude_euler`|Attitude update|Euler angles *(list)*|
+8 -2
View File
@@ -26,6 +26,7 @@ class Flix:
mode: str = ''
armed: bool = False
landed: bool = False
voltage: float = 0
attitude: List[float]
attitude_euler: List[float] # roll, pitch, yaw
rates: List[float]
@@ -68,7 +69,7 @@ class Flix:
self._heartbeat_thread.start()
if wait_connection:
self.wait('mavlink.HEARTBEAT')
time.sleep(0.2) # give some time to receive initial state
time.sleep(0.6) # give some time to receive initial state
def _init_state(self):
self.attitude = [1, 0, 0, 0]
@@ -185,11 +186,16 @@ class Flix:
self._trigger('motors', self.motors)
if isinstance(msg, mavlink.MAVLink_scaled_imu_message):
self.acc = self._mavlink_to_flu([msg.xacc / 1000, msg.yacc / 1000, msg.zacc / 1000])
ONE_G = 9.80665
self.acc = self._mavlink_to_flu([msg.xacc * ONE_G / 1000, msg.yacc * ONE_G / 1000, msg.zacc * ONE_G / 1000])
self.gyro = self._mavlink_to_flu([msg.xgyro / 1000, msg.ygyro / 1000, msg.zgyro / 1000])
self._trigger('acc', self.acc)
self._trigger('gyro', self.gyro)
if isinstance(msg, mavlink.MAVLink_battery_status_message):
self.voltage = msg.voltages[0] / 1000
self._trigger('voltage', self.voltage)
if isinstance(msg, mavlink.MAVLink_serial_control_message):
# new chunk of data
text = bytes(msg.data)[:msg.count].decode('utf-8', errors='ignore')
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pyflix"
version = "0.11"
version = "0.14"
description = "Python API for Flix drone"
authors = [{ name="Oleg Kalachev", email="okalachev@gmail.com" }]
license = "MIT"