53 Commits
Author SHA1 Message Date
Oleg Kalachev f355cc2938 Upload binaries to docs only in my repo and in master and dev branches 2026-08-12 07:19:23 +03:00
Oleg Kalachev 0139d71b12 Show git hash commit in sys command 2026-08-12 01:58:13 +03:00
Oleg Kalachev 58da0b9959 Add binaries to the docs only of DOCS_BINARIES repo variable is truthy 2026-08-11 21:28:16 +03:00
Oleg Kalachev 355f4ad49d Merge remote-tracking branch 'origin/master' into dev 2026-08-11 18:43:18 +03:00
Oleg Kalachev 55398a660d Remove unused variable 2026-08-11 04:59:54 +03:00
Oleg Kalachev 63bbea4d8b Fix 2026-08-11 02:26:20 +03:00
Oleg Kalachev 5543a363b3 Add yaw rate windup parameter 2026-08-11 01:56:49 +03:00
Oleg Kalachev 363c756c00 Fix windup for yaw rate 2026-08-11 01:56:41 +03:00
Oleg Kalachev 57b853361c Fix motor pins for Flix2 2026-08-11 01:55:56 +03:00
Oleg Kalachev ce37e5b724 Simplify parameter defaults definition 2026-08-11 00:48:10 +03:00
Oleg Kalachev 36b050a896 Add config for Flix2 board 2026-08-10 03:52:57 +03:00
Oleg Kalachev 93ebf75a44 Fix 2026-08-10 03:51:16 +03:00
Oleg Kalachev a60c84ce7a Forgotten image 2026-08-10 03:45:38 +03:00
Oleg Kalachev ae8931cb25 Move most parameter defaults to config.h 2026-08-10 03:45:28 +03:00
Oleg Kalachev e6072b0bc4 Make i and d arguments optional in pid constructor 2026-08-10 03:43:37 +03:00
Oleg Kalachev 67e772a23f Update wait-on-check-action version 2026-08-10 03:33:27 +03:00
Oleg Kalachev 04a24c4939 Add minor comment 2026-08-10 03:27:23 +03:00
Oleg Kalachev f62e176313 Update the docs to reflect possibility for using prebuilt binaries 2026-08-10 03:17:32 +03:00
Oleg Kalachev 8ae06eef85 Docs updates 2026-08-10 03:16:54 +03:00
Oleg Kalachev 9a17ca848d Simplify 2026-08-10 02:41:53 +03:00
Oleg Kalachev b8f93becca Make Rate always trigger on first step 2026-08-10 02:31:54 +03:00
Oleg Kalachev 5fc2143994 Make brushless motors separated block to avoid confusion 2026-08-10 02:31:14 +03:00
Oleg Kalachev b7102d395c Make default fqbn plain esp32
It works on d1 as well
2026-08-10 02:15:30 +03:00
Oleg Kalachev 58ea75d335 Fixes 2026-08-10 01:18:29 +03:00
Oleg Kalachev 93b3077fa7 Add I2C pin parameters 2026-08-09 23:59:45 +03:00
Oleg Kalachev 4c74768131 Fixes 2026-08-09 22:42:14 +03:00
Oleg Kalachev 9880eeccd3 Disable imu by default 2026-08-09 20:41:03 +03:00
Oleg Kalachev a26d096dc0 Disable motors by default 2026-08-09 20:40:58 +03:00
Oleg Kalachev 8a91201cf4 Add targets with psram 2026-08-09 06:35:54 +03:00
Oleg Kalachev 51ca049b97 Download binaries to docs site 2026-08-09 04:23:16 +03:00
Oleg Kalachev 2decd2d6dd Fix simulation build 2026-08-08 02:19:38 +03:00
Oleg Kalachev abb2e9f79b Use up-to-date method for installing arduino-cli in simulator ci 2026-08-08 02:19:28 +03:00
Oleg Kalachev 6c907b77f6 Fix simulation build 2026-08-08 01:31:41 +03:00
Oleg Kalachev 0a53170097 Make imu configuration parameters work 2026-08-08 01:24:43 +03:00
Oleg Kalachev 6b36488245 Use dev version of FlixPeriph 2026-08-08 01:21:01 +03:00
Oleg Kalachev f30a182b20 Add parameter WIFI_BROADCAST for always broadcasting mode
May be used in some cases, like multiple gcs
2026-08-08 01:04:20 +03:00
Oleg Kalachev 90b506e084 Allow hot reconnection from another gcs over wifi
Switch back to udp broadcasting if no messages from gcs for 5 seconds
2026-08-07 18:24:23 +03:00
Oleg Kalachev 5f9aae62b9 Upload all binaries to artifacts 2026-08-06 03:04:05 +03:00
Oleg Kalachev 0e0867ceab Merge branch 'master' into dev 2026-08-05 01:26:28 +03:00
Oleg Kalachev 8e276bde38 Add Oleg1405 build 2026-08-04 22:48:39 +03:00
Oleg Kalachev 83aac78cf7 Merge branch 'master' into dev 2026-08-03 16:02:29 +03:00
Oleg Kalachev eb93b0b29d Merge branch 'master' into dev 2026-08-03 16:01:39 +03:00
Oleg Kalachev a5991930d1 Disable voltage lpf by default
Filtering does more harm than good in voltage monitoring.
(alpha = 1 efficiently disables the filter.)
2026-08-03 15:57:29 +03:00
Oleg Kalachev 406f7a4af5 Add tilt disarm failsafe 2026-08-02 02:13:50 +03:00
Oleg Kalachev 5ee6d91440 Don't send discovery message if encrypting is disabled in espnow 2026-08-02 02:07:17 +03:00
Oleg Kalachev 38925d7885 Updates and fixes in usage article 2026-08-02 02:02:28 +03:00
Oleg Kalachev 5c81181221 Add threshold for rc sticks move for quitting auto mode 2026-08-02 01:58:16 +03:00
Oleg Kalachev fd249fcaa6 Add position control demo video to the readme 2026-08-01 22:33:26 +03:00
Oleg Kalachev 8917849711 Remove unneeded declaration from the sim 2026-07-15 18:40:51 +03:00
Oleg Kalachev 1cdc7a8641 Fix rc in simulator
Virtual rc is disabled if rxRxPin < 0
2026-07-15 18:40:35 +03:00
Oleg Kalachev d64bf24c6d Add alicanerus' build 2026-07-08 22:29:33 +03:00
Oleg Kalachev fabd5e072d Fixes in the docs 2026-07-04 21:40:39 +03:00
Oleg Kalachev 1ae85ff118 Add argument to motor testing commands for specifying thrust 2026-07-02 22:22:33 +03:00
24 changed files with 257 additions and 118 deletions
+14 -8
View File
@@ -10,23 +10,29 @@ on:
jobs:
build_linux:
runs-on: ubuntu-latest
env:
ARDUINO_SKETCH_ALWAYS_EXPORT_BINARIES: 1
steps:
- uses: actions/checkout@v4
- name: Install Arduino CLI
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
- name: Build firmware
env:
ARDUINO_SKETCH_ALWAYS_EXPORT_BINARIES: 1
- name: Build firmware for ESP32
run: make
- name: Build firmware for ESP32-C3
run: make BOARD=esp32:esp32:esp32c3
- name: Build firmware for ESP32-S3
run: make BOARD=esp32:esp32:esp32s3:CDCOnBoot=cdc
- name: Build firmware for ESP32-S3 with QSPI PSRAM
run: make BOARD=esp32:esp32:esp32s3:CDCOnBoot=cdc,PSRAM=enabled EXTRA=--output-dir=flix/build/esp32.esp32.esp32s3.qspi
- name: Build firmware for ESP32-S3 with OPI PSRAM
run: make BOARD=esp32:esp32:esp32s3:CDCOnBoot=cdc,PSRAM=opi EXTRA=--output-dir=flix/build/esp32.esp32.esp32s3.opi
- name: Build firmware for Flix2
run: make BOARD=esp32:esp32:esp32s3:FlashSize=4M,CDCOnBoot=cdc,PSRAM=opi FLAGS=-DFLIX2 EXTRA=--output-dir=flix/build/esp32.esp32.flix2
- name: Upload binaries
uses: actions/upload-artifact@v4
with:
name: firmware-binary
path: flix/build
- name: Build firmware for ESP32-C3
run: make BOARD=esp32:esp32:esp32c3
- name: Build firmware for ESP32-S3
run: make BOARD=esp32:esp32:esp32s3
- name: Build espnow-proxy
run: arduino-cli compile --fqbn esp32:esp32:esp32 tools/espnow-proxy
- name: Check c_cpp_properties.json
@@ -66,7 +72,7 @@ jobs:
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget build-essential cmake g++ pkg-config gnupg2 lsb-release sudo
- name: Install Arduino CLI
uses: arduino/setup-arduino-cli@v1.1.1
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
- uses: actions/checkout@v4
- name: Install Gazebo
run: |
+39
View File
@@ -8,6 +8,7 @@ on:
permissions:
contents: read
actions: read
pages: write
id-token: write
@@ -24,12 +25,50 @@ jobs:
build_book:
runs-on: ubuntu-latest
needs: markdownlint
env:
BINARIES: ${{ github.event_name == 'push' && (github.ref_name == 'master' || github.ref_name == 'dev') && github.repository == 'okalachev/flix' }}
steps:
- uses: actions/checkout@v4
- name: Install mdBook
run: cargo install mdbook --vers 0.4.43 --locked
- name: Build book
run: cd docs && mdbook build
- name: Wait for Build to complete
if: ${{ env.BINARIES }}
uses: lewagon/wait-on-check-action@v1.9.1
with:
ref: ${{ github.sha }}
check-name: build_linux
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 30
- name: Find firmware binaries
if: ${{ env.BINARIES }}
id: build_run
run: |
RUN_ID=$(gh api "repos/${{ github.repository }}/actions/workflows/build.yml/runs?head_sha=${{ github.sha }}&per_page=1" --jq '.workflow_runs[0].id')
echo "id=$RUN_ID" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download firmware binaries
if: ${{ env.BINARIES }}
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
run-id: ${{ steps.build_run.outputs.id }}
name: firmware-binary
path: docs/build
- name: Create shortcuts for firmware binaries
if: ${{ env.BINARIES }}
working-directory: docs/build
run: |
for FQBN in esp32.esp32.*; do
zip -r $FQBN.zip $FQBN
BOARD="${FQBN#esp32.esp32.}"
ln -s "$FQBN/flix.ino.merged.bin" "flix.$BOARD.merged.bin"
ln -s "$FQBN/flix.ino.bin" "flix.$BOARD.bin"
ln -s "$FQBN/flix.ino.bootloader.bin" "flix.$BOARD.bootloader.bin"
done
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
+10 -6
View File
@@ -1,13 +1,17 @@
BOARD = esp32:esp32:d1_mini32
BOARD = esp32:esp32:esp32
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*))
VERSION = $(shell git describe --always --dirty)
export ARDUINO_NETWORK_CONNECTION_TIMEOUT := 1h
build: .core .libs
arduino-cli compile --fqbn $(BOARD) --build-property "build.core_debug_level=1" flix
arduino-cli compile flix \
--fqbn $(BOARD) \
--build-property "build.core_debug_level=1" \
--build-property "compiler.cpp.extra_flags=-DVERSION=$(VERSION) $(FLAGS)" $(EXTRA)
upload: build
arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" flix
arduino-cli upload flix --fqbn $(BOARD) -p "$(PORT)"
erase:
arduino-cli burn-bootloader --fqbn $(BOARD) -p "$(PORT)" -P esptool
@@ -22,13 +26,13 @@ core .core:
libs .libs:
arduino-cli lib update-index
arduino-cli lib install "FlixPeriph"
ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=1 arduino-cli lib install --git-url 'https://github.com/okalachev/flixperiph.git#dev'
arduino-cli lib install "MAVLink"@2.0.25
touch .libs
upload_proxy: .core .libs
arduino-cli compile --fqbn $(BOARD) tools/espnow-proxy
arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" tools/espnow-proxy
arduino-cli compile tools/espnow-proxy --fqbn $(BOARD)
arduino-cli upload tools/espnow-proxy --fqbn $(BOARD) -p "$(PORT)"
gazebo/build cmake: gazebo/CMakeLists.txt
mkdir -p gazebo/build
+7 -1
View File
@@ -55,6 +55,12 @@ Outdoor flights demo video of the current prototype:
<a href="https://youtu.be/KXlNmvUTi4g"><img width=300 src="https://i3.ytimg.com/vi/KXlNmvUTi4g/maxresdefault.jpg"></a>
### Position control
The position control feature is in development. RoboCamp 2026 demo (using an overhead camera, [sources](https://github.com/xTimop/flix-poscontrol/compare/robolager2026...xTimop:flix-poscontrol:poscontrol)):
<a href="https://youtu.be/369Xowm4HcU"><img width=300 src="https://i3.ytimg.com/vi/369Xowm4HcU/maxresdefault.jpg"></a>
## Simulation
The simulator is implemented using Gazebo and runs the original Arduino code:
@@ -84,7 +90,7 @@ Additional articles:
|*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 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|
|MOSFET (transistor)|UMW 100N03A or [analog](https://t.me/opensourcequadcopter/33).<br>Warning: don't use KIA 100N03A or other manufacturers, they might not work!|<img src="docs/img/100n03a.jpg" width=100>|4|
|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|
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+50 -21
View File
@@ -1,20 +1,48 @@
# Usage: build, setup and flight
To fly Flix quadcopter, you need to build the firmware, upload it to the ESP32 board, and set up the drone for flight.
To fly Flix quadcopter, you need to upload the firmware to the ESP32 board, and set up the drone for flight.
To get the firmware sources, clone the repository using git:
## Uploading the firmware
You can either use the **prebuilt binaries** or **build the firmware** from sources — this will let you modify the firmware and add new features.
### Prebuilt binaries (the easiest way)
1. Download the latest firmware file using the following links:
<!-- markdownlint-disable MD044 -->
|Type|Boards|Link|
|-|-|-|
|ESP32|DevKit, D1 Mini|[`quadcopter.dev/flix.esp32.merged.bin`](https://quadcopter.dev/flix.esp32.merged.bin)|
|ESP32-S3|Most S3 based|[`quadcopter.dev/flix.esp32s3.merged.bin`](https://quadcopter.dev/flix.esp32s3.merged.bin)|
|ESP32-S3 (2MB PSRAM)|S3 Super Mini, S3 Zero (2MB PSRAM)|[`quadcopter.dev/flix.esp32s3.qspi.merged.bin`](https://quadcopter.dev/flix.esp32s3.qspi.merged.bin)|
|ESP32-S3 (8/16MB PSRAM)|S3 Zero (8MB PSRAM)|[`quadcopter.dev/flix.esp32s3.opi.merged.bin`](https://quadcopter.dev/flix.esp32s3.opi.merged.bin)|
|ESP32-C3|C3 Super Mini|[`quadcopter.dev/flix.esp32c3.merged.bin`](https://quadcopter.dev/flix.esp32c3.merged.bin)|
|Flix2|Flix2 board|[`quadcopter.dev/flix.flix2.merged.bin`](https://quadcopter.dev/flix.flix2.merged.bin)|
<!-- markdownlint-enable MD044 -->
2. Flash your ESP32 board using [ESP32 Web Flasher](https://www.espboards.dev/tools/program/):
<img src="img/web-flasher.png" width="400">
* Connect the board to your computer, press *Connect to ESP*, choose the serial port.
* Go to the *Flash* tab.
* Choose the downloaded firmware file, set *Flash address* to *0* (important).
* Click *Program* button and wait until the process is finished.
### Building from sources (flexible)
You can build and upload the firmware using either **Arduino IDE** (easier for beginners) or **command line**.
Get the sources using git:
```bash
git clone https://github.com/okalachev/flix.git && cd flix
```
Beginners can [download the source code as a ZIP archive](https://github.com/okalachev/flix/archive/refs/heads/master.zip).
Beginners can [download the sources as a ZIP archive](https://github.com/okalachev/flix/archive/refs/heads/master.zip).
## Building the firmware
You can build and upload the firmware using either **Arduino IDE** (easier for beginners) or **command line**.
### Arduino IDE (Windows, Linux, macOS)
#### Arduino IDE (Windows, Linux, macOS)
<img src="img/arduino-ide.png" width="400" alt="Flix firmware open in Arduino IDE">
@@ -25,11 +53,11 @@ You can build and upload the firmware using either **Arduino IDE** (easier for b
* `FlixPeriph`, the latest version.
* `MAVLink`, version 2.0.25.
5. Open the `flix/flix.ino` sketch from downloaded firmware sources in Arduino IDE.
6. Connect your ESP32 board to the computer and choose correct board type in Arduino IDE (*WEMOS D1 MINI ESP32* for ESP32 Mini) and the port.
6. Connect your ESP32 board to the computer and choose correct board type in Arduino IDE (*WEMOS D1 MINI ESP32* for ESP32 Mini, *ESP32S3 Dev Module* for ESP32-S3 Super Mini) and the port.
7. Set *Tools**Core Debug Level* to *Error* to see the errors in the serial console. Set *Tools**USB CDC on Boot* to *Enabled* for ESP32-S3/ESP32-C3 boards.
8. [Build and upload](https://docs.arduino.cc/software/ide-v2/tutorials/getting-started/ide-v2-uploading-a-sketch) the firmware using Arduino IDE.
### Command line (Windows, Linux, macOS)
#### Command line (Windows, Linux, macOS)
1. [Install Arduino CLI](https://arduino.github.io/arduino-cli/installation/).
@@ -96,7 +124,7 @@ To access the console using serial port:
To access the console using QGroundControl:
1. Connect to the drone using QGroundControl app.
2. Go to the QGroundControl menu ⇒ *Vehicle Setup* ⇒ *Analyze Tools* ⇒ *MAVLink Console*.
2. Go to the QGroundControl menu ⇒ *Analyze Tools* ⇒ *MAVLink Console*.
<img src="img/cli.png" width="400">
@@ -113,16 +141,14 @@ You can also work with parameters using `p` command in the console. Parameter na
### Configure the IMU
1. Configure the following parameters for the used IMU:
* `IMU_MODEL` — IMU model (0 for disabled, 1 for MPU-9250/MPU-6500¹, 2 for ICM-20948, 3 for MPU-6050, 4 for ICM-40609-D).
1. Configure the following parameters for the IMU:
* `IMU_MODEL` — IMU model (1 for MPU-9250/MPU-6500, 2 for ICM-20948, 3 for MPU-6050, 4 for ICM-40609-D).
* `IMU_BUS` — communication bus (0 for SPI, 1 for I²C).
* `IMU_PIN_SCK`, `IMU_PIN_MISO`, `IMU_PIN_MOSI`, `IMU_PIN_CS` — SPI pin numbers.
* `IMU_PIN_SCL`, `IMU_PIN_SDA` — I²C pin numbers.
* `IMU_PIN_INT` — IMU data ready pin number (-1 if not used).
2. Reboot the drone.
3. Check the IMU is working using `imu` command in the console (see details below).
¹ — not to be confused with MPU-6050.
3. Check the IMU is working using `imu` command in the console (should print `status: OK`).
### Define IMU orientation
@@ -152,9 +178,9 @@ 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 and ESP32-C3) support a lower maximum PWM frequency; on these boards the parameter `MOT_PWM_FREQ` should be set to 38000 Hz.
#### Brushless motors
If using brushless motors and ESCs:
If using brushless motors with 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).
2. Decrease the PWM frequency using the `MOT_PWM_FREQ` parameter (400 is typical).
@@ -162,7 +188,7 @@ 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
### Battery voltage monitoring (optional)
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:
@@ -337,7 +363,7 @@ To setup ESP-NOW communication:
1. Flash the second ESP32 board with ESP-NOW proxy sketch: [`tools/espnow-proxy/espnow-proxy.ino`](../tools/espnow-proxy/espnow-proxy.ino). Use Arduino IDE or command line: `make upload_proxy`.
2. Open Serial Monitor or use `make monitor` command. The ESP32 will print its MAC address and generated encryption key, for example:
2. Open Serial Monitor in Arduino IDE or use `make monitor` command. The ESP32 will print its MAC address and generated encryption key, for example:
```
espnow 7a:c8:e3:eb:bf:e9 &PiuSysxP9+$L&5E
@@ -356,7 +382,10 @@ To setup ESP-NOW communication:
* Type: Serial.
* Serial Port: choose the port of the proxy ESP32 board, e. g. `/dev/cu.usbserial-0001`.
* Baud Rate: 115200.
5. Click *Save*. QGroundControl should connect to the drone using ESP-NOW and begin showing the telemetry.
5. Click *Save*, click *Connect*. QGroundControl should connect to the drone using ESP-NOW and begin showing the telemetry.
> [!TIP]
> Make sure Arduino IDE is not running when using ESP-NOW proxy board, as it may block the serial port.
## Flight log
+18
View File
@@ -4,6 +4,24 @@ This page contains user-built drones based on the Flix project. Publish your pro
---
Author: [Oleg1405](https://t.me/Oleg1405).<br>
Description: ESP32 Mini, MPU-6500 IMU, boost converter, BT2.0 power connector, 65 mm props, BetaFPV ELRS Lite Receiver, Radiomaster Pocket + Mavlink Joystick (Android) control.
<img src="img/user/oleg1405/1.jpg" height=300>
[Flight video](https://www.youtube.com/shorts/rbXV4sHbpso).
---
Author: Alican Erüst.<br>
Description: QX95 mm frame, 55 mm propellers, 3.7 V 25C 1050 mAh LiPo battery, MPU6050 IMU, Logitech F310 gamepad controller, with a total quadcopter weight of 66 g.
<img src="img/user/alicanerus/1.jpg" height=200> <img src="img/user/alicanerus/2.jpg" height=200> <img src="img/user/alicanerus/3.jpg" height=200>
[Flight video](https://drive.google.com/file/d/1k0WeWTKnCAfaugkX7LcmNxsUuq79RL8Z/view?usp=sharing).
---
Author: [Неруш Михаил](https://t.me/NerushMV).<br>
Description: custom frame made of 4 mm plywood, 8520 brushed motors, 75 mm propellers, MPU-6500. FlySky FS-i6X with ESP32-based adapter for ESP-NOW communication (using PPM output).<br>
Sources and materials: [link](https://drive.google.com/drive/folders/1uWiDcuorLrtVs_IIR7Y13omij-7Q1nx8).
+9 -6
View File
@@ -50,8 +50,8 @@ const char* motd =
"sta <ssid> <password> - configure Wi-Fi client mode\n"
"espnow <mac> [<key>] - configure ESP-NOW peer\n"
"mot - show motor output\n"
"mfr/mfl/mrr/mrl [<thrust>] - test motor (remove props)\n"
"log [dump] - print log header [and data]\n"
"mfr, mfl, mrr, mrl - test motor (remove props)\n"
"log - show log info\n"
"log header - show log header\n"
"log reset - reset log\n"
@@ -176,13 +176,13 @@ void doCommand(String str, bool echo = false) {
} else if (command == "ca") {
calibrateAccel();
} else if (command == "mfr") {
testMotor(MOTOR_FRONT_RIGHT);
testMotor(MOTOR_FRONT_RIGHT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "mfl") {
testMotor(MOTOR_FRONT_LEFT);
testMotor(MOTOR_FRONT_LEFT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "mrr") {
testMotor(MOTOR_REAR_RIGHT);
testMotor(MOTOR_REAR_RIGHT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "mrl") {
testMotor(MOTOR_REAR_LEFT);
testMotor(MOTOR_REAR_LEFT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "sys") {
#ifdef ESP32
print("Chip: %s\n", ESP.getChipModel());
@@ -191,7 +191,10 @@ void doCommand(String str, bool echo = false) {
print("Free heap: %d KB\n", ESP.getFreeHeap() / 1024);
print("PSRAM: %d KB\n", ESP.getPsramSize() / 1024);
print("Free PSRAM: %d KB\n", ESP.getFreePsram() / 1024);
print("Firmware: " __DATE__ " " __TIME__ "\n");
#ifdef VERSION
print("Version: %s\n", STRINGIFY(VERSION));
#endif
print("Build date: " __DATE__ " " __TIME__ "\n");
// Print tasks table
print("Num Task MinSt Prio Core CPU%%\n");
int taskCount = uxTaskGetNumberOfTasks();
+27
View File
@@ -0,0 +1,27 @@
// Copyright (c) 2026 Oleg Kalachev <okalachev@gmail.com>
// Repository: https://github.com/okalachev/flix
// Parameter defaults
#pragma once
void setDefaults() {
// Set defaults here
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)
pwmFrequency = 38000;
#endif
#ifdef FLIX2
imuModel = 4; // ICM-40609-D
imuIntPin = 10;
imuCsPin = 14;
motorPins[MOTOR_REAR_LEFT] = 41;
motorPins[MOTOR_REAR_RIGHT] = 7;
motorPins[MOTOR_FRONT_RIGHT] = 18;
motorPins[MOTOR_FRONT_LEFT] = 38;
voltagePin = 3;
#endif
}
+9 -34
View File
@@ -9,31 +9,6 @@
#include "filter.h"
#include "util.h"
#define PITCHRATE_P 0.05
#define PITCHRATE_I 0.2
#define PITCHRATE_D 0.001
#define PITCHRATE_I_LIM 0.3
#define ROLLRATE_P PITCHRATE_P
#define ROLLRATE_I PITCHRATE_I
#define ROLLRATE_D PITCHRATE_D
#define ROLLRATE_I_LIM PITCHRATE_I_LIM
#define YAWRATE_P 0.3
#define YAWRATE_I 0.0
#define YAWRATE_D 0.0
#define YAWRATE_I_LIM 0.3
#define ROLL_P 6
#define ROLL_I 0
#define ROLL_D 0
#define PITCH_P ROLL_P
#define PITCH_I ROLL_I
#define PITCH_D ROLL_D
#define YAW_P 3
#define PITCHRATE_MAX radians(360)
#define ROLLRATE_MAX radians(360)
#define YAWRATE_MAX radians(300)
#define TILT_MAX radians(30)
#define RATES_D_LPF_ALPHA 0.2 // cutoff frequency ~ 40 Hz
const int RAW = 0, ACRO = 1, STAB = 2, AUTO = 3; // flight modes
int mode = STAB;
bool armed = false;
@@ -41,17 +16,17 @@ bool armed = false;
Quaternion attitudeTarget;
Vector ratesTarget;
Vector ratesExtra; // feedforward rates
Vector torqueTarget;
Vector torqueTarget; // 0 - no torque, 1 - maximum torque
float thrustTarget;
PID rollRatePID(ROLLRATE_P, ROLLRATE_I, ROLLRATE_D, ROLLRATE_I_LIM, RATES_D_LPF_ALPHA);
PID pitchRatePID(PITCHRATE_P, PITCHRATE_I, PITCHRATE_D, PITCHRATE_I_LIM, RATES_D_LPF_ALPHA);
PID yawRatePID(YAWRATE_P, YAWRATE_I, YAWRATE_D);
PID rollPID(ROLL_P, ROLL_I, ROLL_D);
PID pitchPID(PITCH_P, PITCH_I, PITCH_D);
PID yawPID(YAW_P, 0, 0);
Vector maxRate(ROLLRATE_MAX, PITCHRATE_MAX, YAWRATE_MAX);
float tiltMax = TILT_MAX;
PID rollRatePID(0.05, 0.2, 0.001, 0.3, 0.2);
PID pitchRatePID(0.05, 0.2, 0.001, 0.3, 0.2);
PID yawRatePID(0.3, 0, 0, 0.3);
PID rollPID(6);
PID pitchPID(6);
PID yawPID(3);
Vector maxRate(radians(360), radians(360), radians(360));
float tiltMax = radians(30);
int flightModes[] = {STAB, STAB, STAB}; // map for rc mode switch
extern const int MOTOR_REAR_LEFT, MOTOR_REAR_RIGHT, MOTOR_FRONT_RIGHT, MOTOR_FRONT_LEFT;
+31 -25
View File
@@ -10,9 +10,10 @@
#include "filter.h"
#include "util.h"
IMU imu;
int imuModel = 1, imuBus = 0;
int imuSckPin = SCK, imuMisoPin = MISO, imuMosiPin = MOSI, imuCsPin = -1, imuIntPin = -1;
IMU *imu;
int imuModel = -1; // 1 - MPU9250, 2 - ICM20948, 3 - MPU6050, 4 - ICM40609D
int imuBus = 0; // 0 - SPI, 1 - I2C
int imuSckPin = SCK, imuMisoPin = MISO, imuMosiPin = MOSI, imuCsPin = SS, imuIntPin = -1;
int imuSdaPin = SDA, imuSclPin = SCL;
Vector imuRotation(0, 0, PI / 2); // imu orientation as Euler angles
@@ -27,30 +28,35 @@ LowPassFilter<Vector> gyroBiasFilter(0.001);
void setupIMU() {
print("Setup IMU\n");
if (imuBus == 0 && imuCsPin > 0) {
free(imu);
if (imuModel == 3) imuBus = 1; // MPU6050 is I2C only
if (imuBus == 0) {
// SPI connection
SPI.begin(imuSckPin, imuMisoPin, imuMosiPin);
imu.begin((IMU::Model)imuModel, SPI, imuCsPin, imuIntPin);
} else if (imuBus == 1) {
imu = IMU::create(imuModel, SPI, imuCsPin, imuIntPin);
} else {
// I2C connection
Wire.setPins(imuSdaPin, imuSclPin);
imu.begin((IMU::Model)imuModel, Wire, imuIntPin);
imu = IMU::create(imuModel, Wire, imuIntPin);
}
imu->begin();
configureIMU();
}
void configureIMU() {
imu.setAccelRange(imu.ACCEL_RANGE_4G);
imu.setGyroRange(imu.GYRO_RANGE_2000DPS);
imu.setDLPF(imu.DLPF_MAX);
imu.setRate(imu.RATE_1KHZ_APPROX);
imu.setupInterrupt();
imu->setAccelRange(IMU::ACCEL_RANGE_4G);
imu->setGyroRange(IMU::GYRO_RANGE_2000DPS);
imu->setDLPF(IMU::DLPF_MAX);
imu->setRate(IMU::RATE_1KHZ_APPROX);
imu->setupInterrupt();
}
void readIMU() {
imu.waitForData();
imu.getGyro(gyro.x, gyro.y, gyro.z);
imu.getAccel(acc.x, acc.y, acc.z);
imu->waitForData();
imu->getGyro(gyro.x, gyro.y, gyro.z);
imu->getAccel(acc.x, acc.y, acc.z);
calibrateGyroOnce();
// Apply scale and bias
@@ -72,7 +78,7 @@ void calibrateGyroOnce() {
void calibrateAccel() {
print("Calibrating accelerometer\n");
imu.setAccelRange(imu.ACCEL_RANGE_2G); // the most sensitive mode
imu->setAccelRange(IMU::ACCEL_RANGE_2G); // the most sensitive mode
print("1/6 Place level [8 sec]\n");
pause(8);
@@ -106,9 +112,9 @@ void calibrateAccelOnce() {
// Compute the average of the accelerometer readings
acc = Vector(0, 0, 0);
for (int i = 0; i < samples; i++) {
imu.waitForData();
imu->waitForData();
Vector sample;
imu.getAccel(sample.x, sample.y, sample.z);
imu->getAccel(sample.x, sample.y, sample.z);
acc = acc + sample;
}
acc = acc / samples;
@@ -133,18 +139,18 @@ void printIMUCalibration() {
}
void printIMUInfo() {
imu.status() ? print("status: ERROR %d\n", imu.status()) : print("status: OK\n");
print("model: %s\n", imu.getModel());
print("who am I: 0x%02X\n", imu.whoAmI());
imu->status() ? print("status: ERROR %d\n", imu->status()) : print("status: OK\n");
print("model: %s\n", imu->getModel());
print("who am I: 0x%02X\n", imu->whoAmI());
print("rate: %.0f\n", loopRate);
print("interrupt mode: %s\n", imuIntPin != -1 ? "pin" : "timer");
print("temperature: %.1f °C\n", imu.getTemp());
print("temperature: %.1f °C\n", imu->getTemp());
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();
imu->waitForData();
Vector rawGyro, rawAcc;
imu.getGyro(rawGyro.x, rawGyro.y, rawGyro.z);
imu.getAccel(rawAcc.x, rawAcc.y, rawAcc.z);
imu->getGyro(rawGyro.x, rawGyro.y, rawGyro.z);
imu->getAccel(rawAcc.x, rawAcc.y, rawAcc.z);
print("raw gyro: %f %f %f\n", rawGyro.x, rawGyro.y, rawGyro.z);
print("raw acc: %f %f %f\n", rawAcc.x, rawAcc.y, rawAcc.z);
}
+1 -1
View File
@@ -100,7 +100,7 @@ LogTopic logTopics[] = {
LogTopic(5,
{"voltage", &voltage},
{"temp", &temperatureRead},
{"imuTemp", []() { return imu.getTemp(); }}),
{"imuTemp", []() { return imu->getTemp(); }}),
};
void *logBuffer; // buffer for log data
+3 -3
View File
@@ -18,7 +18,7 @@ Rate telemetryMotors(10);
Rate telemetryIMU(15);
Rate telemetryTopic(10);
bool mavlinkConnected = false;
float mavlinkTime = NAN; // time of last received message
String mavlinkPrintBuffer;
void processMavlink() {
@@ -41,7 +41,7 @@ void sendMavlink() {
sendMessage(&msg);
}
if (!mavlinkConnected) return; // send only heartbeat until connected
if (!valid(mavlinkTime)) return; // send only heartbeat until connected
if (telemetrySlow) {
mavlink_msg_extended_sys_state_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg,
@@ -128,7 +128,7 @@ void receiveMavlink() {
mavlink_status_t status;
for (int i = 0; i < len; i++) {
if (mavlink_parse_char(MAVLINK_COMM_0, buf[i], &msg, &status)) {
mavlinkConnected = true;
mavlinkTime = t;
handleMavlink(&msg);
}
}
+2 -2
View File
@@ -51,9 +51,9 @@ bool motorsActive() {
return motors[0] != 0 || motors[1] != 0 || motors[2] != 0 || motors[3] != 0;
}
void testMotor(int n) {
void testMotor(int n, float thrust) {
print("Testing motor %d\n", n);
motors[n] = 0.2;
motors[n] = thrust;
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);
+10 -2
View File
@@ -9,11 +9,13 @@
extern int channelZero[16], channelMax[16];
extern int rollChannel, pitchChannel, throttleChannel, yawChannel, armedChannel, modeChannel;
extern int rcRxPin, voltagePin;
extern int wifiMode, wifiLongRange, udpLocalPort, udpRemotePort, espnowChannel;
extern float rcLossTimeout, descendTime;
extern int wifiMode, wifiLongRange, wifiBroadcast, udpLocalPort, udpRemotePort, espnowChannel;
extern float rcLossTimeout, descendTime, disarmTilt;
extern float voltageScale;
extern LowPassFilter<float> voltageFilter;
#include "config.h"
Preferences storage;
struct Parameter {
@@ -44,6 +46,7 @@ Parameter parameters[] = {
{"CTL_Y_RATE_P", &yawRatePID.p},
{"CTL_Y_RATE_I", &yawRatePID.i},
{"CTL_Y_RATE_D", &yawRatePID.d},
{"CTL_Y_RATE_WU", &yawRatePID.windup},
{"CTL_Y_RATE_D_A", &yawRatePID.lpf.alpha},
{"CTL_R_P", &rollPID.p},
{"CTL_R_I", &rollPID.i},
@@ -66,6 +69,8 @@ Parameter parameters[] = {
{"IMU_PIN_MISO", &imuMisoPin},
{"IMU_PIN_MOSI", &imuMosiPin},
{"IMU_PIN_CS", &imuCsPin},
{"IMU_PIN_SDA", &imuSdaPin},
{"IMU_PIN_SCL", &imuSclPin},
{"IMU_PIN_INT", &imuIntPin},
{"IMU_ROT_ROLL", &imuRotation.x},
{"IMU_ROT_PITCH", &imuRotation.y},
@@ -121,6 +126,7 @@ Parameter parameters[] = {
{"WIFI_PORT_LOC", &udpLocalPort},
{"WIFI_PORT_REM", &udpRemotePort},
{"WIFI_LONG_RANGE", &wifiLongRange},
{"WIFI_BROADCAST", &wifiBroadcast},
// espnow
{"ESPNOW_CHANNEL", &espnowChannel},
// mavlink
@@ -153,10 +159,12 @@ Parameter parameters[] = {
// safety
{"SF_RC_LOSS_TIME", &rcLossTimeout},
{"SF_DESCEND_TIME", &descendTime},
{"SF_DISARM_TILT", &disarmTilt},
};
void setupParameters() {
print("Setup parameters\n");
setDefaults();
storage.begin("flix");
// Read parameters from storage
for (auto &parameter : parameters) {
+1 -1
View File
@@ -18,7 +18,7 @@ public:
LowPassFilter<float> lpf; // low pass filter for derivative term
PID(float p, float i, float d, float windup = 0, float dAlpha = 1, float dtMax = 0.1) :
PID(float p, float i = 0, float d = 0, float windup = 0, float dAlpha = 1, float dtMax = 0.1) :
p(p), i(i), d(d), windup(windup), lpf(dAlpha), dtMax(dtMax) {}
float update(float error) {
+15 -1
View File
@@ -8,10 +8,12 @@ extern float controlRoll, controlPitch, controlThrottle, controlYaw;
float rcLossTimeout = 1;
float descendTime = 10;
float disarmTilt = radians(120);
void failsafe() {
rcLossFailsafe();
autoFailsafe();
tiltFailsafe();
}
// RC loss failsafe
@@ -36,7 +38,7 @@ void descend() {
// Allow pilot to interrupt automatic flight
void autoFailsafe() {
static float roll, pitch, yaw, throttle;
if (roll != controlRoll || pitch != controlPitch || yaw != controlYaw || abs(throttle - controlThrottle) > 0.05) {
if (abs(roll - controlRoll) > 0.05 || abs(pitch - controlPitch) > 0.05 || abs(yaw - controlYaw) > 0.05 || abs(throttle - controlThrottle) > 0.05) {
// controls changed and mode switch is not configured
if (mode == AUTO && invalid(controlMode)) mode = STAB; // regain control by the pilot
}
@@ -45,3 +47,15 @@ void autoFailsafe() {
yaw = controlYaw;
throttle = controlThrottle;
}
// Disarm if tilted too much
void tiltFailsafe() {
if (!armed) return;
if (mode != STAB) return;
Vector up = Quaternion::rotateVector(Vector(0, 0, 1), attitude);
float tilt = acos(up.z);
if (disarmTilt && tilt > disarmTilt) {
armed = false;
}
}
+3 -1
View File
@@ -11,6 +11,8 @@
const float ONE_G = 9.80665;
extern float t;
#define STRINGIFY(x) __STRINGIFY(x)
float mapf(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
@@ -111,7 +113,7 @@ struct Value {
class Rate {
public:
float rate;
float last = 0;
float last = -INFINITY;
Rate(float rate) : rate(rate) {}
operator bool() {
+5 -3
View File
@@ -17,6 +17,7 @@ const int W_DISABLED = 0, W_AP = 1, W_STA = 2, W_ESPNOW = 3;
int wifiMode = W_AP;
int wifiLongRange = 0;
int wifiBroadcast = 0; // 0 - broadcast until connected, 1 - always broadcast
int udpLocalPort = 14550;
int udpRemotePort = 14550;
IPAddress udpRemoteIP = "255.255.255.255";
@@ -58,13 +59,14 @@ void sendWiFi(const uint8_t *buf, int len) {
espnow.write(buf, len);
static Rate discovery(2);
if (discovery) espnowBroadcast.write((const uint8_t *)"flix", 4); // broadcast message to help finding this device
if (espnow.isEncrypted() && discovery) espnowBroadcast.write((const uint8_t *)"flix", 4); // broadcast message to help finding this device
return;
}
if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return;
udp.beginPacket(udpRemoteIP, udpRemotePort);
bool broadcast = wifiBroadcast || !(t - mavlinkTime < 5); // broadcast if lost connection
udp.beginPacket(broadcast ? IPAddress(255, 255, 255, 255) : udpRemoteIP, udpRemotePort);
udp.write(buf, len);
udp.endPacket();
}
@@ -113,7 +115,7 @@ void printWiFiInfo() {
} else {
print("Mode: Disabled\n");
}
print("MAVLink connected: %d\n", mavlinkConnected);
print("MAVLink connected: %d\n", valid(mavlinkTime));
}
void configWiFi(int mode, const char *first, const char *second) {
+3 -3
View File
@@ -24,7 +24,6 @@ LowPassFilter<Vector> gyroBiasFilter(0);
int imuModel = 1, imuBus = 0;
int imuSckPin = 0, imuMisoPin = 0, imuMosiPin = 0, imuCsPin = -1, imuIntPin = -1;
int imuSdaPin = 0, imuSclPin = 0;
int rgbPin = -1, rgbNum = 1;
// declarations
void step();
@@ -42,7 +41,7 @@ const char* getModeName();
void sendMotors();
int getDutyCycle(float value);
bool motorsActive();
void testMotor(int n);
void testMotor(int, float);
void print(const char* format, ...);
void pause(float duration);
void doCommand(String str, bool echo);
@@ -76,6 +75,7 @@ void failsafe();
void rcLossFailsafe();
void descend();
void autoFailsafe();
void tiltFailsafe();
int parametersCount();
const char *getParameterName(int index);
float getParameter(int index);
@@ -96,4 +96,4 @@ void setWiFiMode(const String& mode) { print("Skip WiFi mode set\n"); };
class IMU {
public:
float getTemp() { return 0; }
} imu;
} *imu;