8 Commits
Author SHA1 Message Date
Oleg Kalachev 6064ee18f3 Disable Wi-Fi code 2026-08-12 00:47:52 +03:00
Oleg Kalachev dc26abe975 Implement Preferences.h for STM32 2026-08-12 00:13:03 +03:00
Oleg Kalachev 5ee828dc01 Fix 2026-08-12 00:06:17 +03:00
Oleg Kalachev b8c687f3ed Merge branch 'master' into stm 2026-08-11 23:17:04 +03:00
Oleg Kalachev c3b818c2ae Try using installable Preferences library 2025-11-18 18:19:02 +03:00
Oleg Kalachev 531b3f4d04 Use analogWrite api instead of ledc 2025-11-18 16:54:51 +03:00
Oleg Kalachev 795b248b94 Adapt firmware for non-esp32 boards 2025-11-04 13:47:41 +03:00
Oleg Kalachev 77c4b5fc5b Test build for STM32 2025-11-04 13:42:05 +03:00
48 changed files with 910 additions and 320 deletions
+18 -13
View File
@@ -10,23 +10,33 @@ on:
jobs: jobs:
build_linux: build_linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
ARDUINO_SKETCH_ALWAYS_EXPORT_BINARIES: 1
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Arduino CLI - name: Install Arduino CLI
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
- name: Build firmware - name: Build firmware for ESP32
env:
ARDUINO_SKETCH_ALWAYS_EXPORT_BINARIES: 1
run: make 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 EXTRA='--build-property "compiler.cpp.extra_flags=-DFLIX2" --output-dir=flix/build/esp32.esp32.flix2'
- name: Build firmware for STM32F4
run: |
arduino-cli core install STMicroelectronics:stm32 --additional-urls https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json
make BOARD=STMicroelectronics:stm32:GenF4
- name: Upload binaries - name: Upload binaries
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: firmware-binary name: firmware-binary
path: flix/build 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: Check c_cpp_properties.json - name: Check c_cpp_properties.json
run: tools/check_c_cpp_properties.py run: tools/check_c_cpp_properties.py
@@ -64,7 +74,7 @@ jobs:
apt-get update apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget build-essential cmake g++ pkg-config gnupg2 lsb-release sudo DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget build-essential cmake g++ pkg-config gnupg2 lsb-release sudo
- name: Install Arduino CLI - 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 - uses: actions/checkout@v4
- name: Install Gazebo - name: Install Gazebo
run: | run: |
@@ -76,11 +86,6 @@ jobs:
run: sudo apt-get install -y libsdl2-dev run: sudo apt-get install -y libsdl2-dev
- name: Build simulator - name: Build simulator
run: make build_simulator run: make build_simulator
- name: Run simulator
env:
GAZEBO_MODEL_PATH: ${{ github.workspace }}/gazebo/models
GAZEBO_PLUGIN_PATH: ${{ github.workspace }}/gazebo/build
run: timeout --preserve-status 120 gzserver --verbose gazebo/flix.world || [ $? -eq 143 ]
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: gazebo-plugin-binary name: gazebo-plugin-binary
+33
View File
@@ -8,6 +8,7 @@ on:
permissions: permissions:
contents: read contents: read
actions: read
pages: write pages: write
id-token: write id-token: write
@@ -30,6 +31,38 @@ jobs:
run: cargo install mdbook --vers 0.4.43 --locked run: cargo install mdbook --vers 0.4.43 --locked
- name: Build book - name: Build book
run: cd docs && mdbook build run: cd docs && mdbook build
- name: Wait for Build to complete
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
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
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
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 - name: Upload artifact
uses: actions/upload-pages-artifact@v3 uses: actions/upload-pages-artifact@v3
with: with:
+3 -2
View File
@@ -4,9 +4,10 @@ build/
tools/log/ tools/log/
tools/dist/ tools/dist/
*.egg-info/ *.egg-info/
.dependencies .core
.libs
.vscode/* .vscode/*
!.vscode/settings.json !.vscode/settings.default.json
!.vscode/c_cpp_properties.json !.vscode/c_cpp_properties.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
+21 -21
View File
@@ -6,18 +6,18 @@
"${workspaceFolder}/flix", "${workspaceFolder}/flix",
"${workspaceFolder}/gazebo", "${workspaceFolder}/gazebo",
"${workspaceFolder}/tools/**", "${workspaceFolder}/tools/**",
"~/.arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32", "~/.arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32",
"~/.arduino15/packages/esp32/hardware/esp32/3.3.6/libraries/**", "~/.arduino15/packages/esp32/hardware/esp32/3.3.10/libraries/**",
"~/.arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32", "~/.arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32",
"~/.arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/**", "~/.arduino15/packages/esp32/tools/esp32-libs/3.3.10/include/**",
"~/Arduino/libraries/**", "~/Arduino/libraries/**",
"/usr/include/gazebo-11/", "/usr/include/gazebo-11/",
"/usr/include/ignition/math6/" "/usr/include/ignition/math6/"
], ],
"forcedInclude": [ "forcedInclude": [
"${workspaceFolder}/.vscode/intellisense.h", "${workspaceFolder}/.vscode/intellisense.h",
"~/.arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32/Arduino.h", "~/.arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32/Arduino.h",
"~/.arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32/pins_arduino.h", "~/.arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32/pins_arduino.h",
"${workspaceFolder}/flix/cli.ino", "${workspaceFolder}/flix/cli.ino",
"${workspaceFolder}/flix/control.ino", "${workspaceFolder}/flix/control.ino",
"${workspaceFolder}/flix/estimate.ino", "${workspaceFolder}/flix/estimate.ino",
@@ -33,7 +33,7 @@
"${workspaceFolder}/flix/parameters.ino", "${workspaceFolder}/flix/parameters.ino",
"${workspaceFolder}/flix/safety.ino" "${workspaceFolder}/flix/safety.ino"
], ],
"compilerPath": "~/.arduino15/packages/esp32/tools/esp-x32/2511/bin/xtensa-esp32-elf-g++", "compilerPath": "~/.arduino15/packages/esp32/tools/esp-x32/2601/bin/xtensa-esp32-elf-g++",
"cStandard": "c11", "cStandard": "c11",
"cppStandard": "c++17", "cppStandard": "c++17",
"defines": [ "defines": [
@@ -53,18 +53,18 @@
"name": "Mac", "name": "Mac",
"includePath": [ "includePath": [
"${workspaceFolder}/flix", "${workspaceFolder}/flix",
"~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32", "~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32",
"~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.6/libraries/**", "~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.10/libraries/**",
"~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32", "~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32",
"~/Library/Arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/**", "~/Library/Arduino15/packages/esp32/tools/esp32-libs/3.3.10/include/**",
"~/Documents/Arduino/libraries/**", "~/Documents/Arduino/libraries/**",
"/opt/homebrew/include/gazebo-11/", "/opt/homebrew/include/gazebo-11/",
"/opt/homebrew/include/ignition/math6/" "/opt/homebrew/include/ignition/math6/"
], ],
"forcedInclude": [ "forcedInclude": [
"${workspaceFolder}/.vscode/intellisense.h", "${workspaceFolder}/.vscode/intellisense.h",
"~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32/Arduino.h", "~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32/Arduino.h",
"~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32/pins_arduino.h", "~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32/pins_arduino.h",
"${workspaceFolder}/flix/flix.ino", "${workspaceFolder}/flix/flix.ino",
"${workspaceFolder}/flix/cli.ino", "${workspaceFolder}/flix/cli.ino",
"${workspaceFolder}/flix/control.ino", "${workspaceFolder}/flix/control.ino",
@@ -80,7 +80,7 @@
"${workspaceFolder}/flix/parameters.ino", "${workspaceFolder}/flix/parameters.ino",
"${workspaceFolder}/flix/safety.ino" "${workspaceFolder}/flix/safety.ino"
], ],
"compilerPath": "~/Library/Arduino15/packages/esp32/tools/esp-x32/2511/bin/xtensa-esp32-elf-g++", "compilerPath": "~/Library/Arduino15/packages/esp32/tools/esp-x32/2601/bin/xtensa-esp32-elf-g++",
"cStandard": "c11", "cStandard": "c11",
"cppStandard": "c++17", "cppStandard": "c++17",
"defines": [ "defines": [
@@ -103,16 +103,16 @@
"${workspaceFolder}/flix", "${workspaceFolder}/flix",
"${workspaceFolder}/gazebo", "${workspaceFolder}/gazebo",
"${workspaceFolder}/tools/**", "${workspaceFolder}/tools/**",
"~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32", "~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32",
"~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.6/libraries/**", "~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.10/libraries/**",
"~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32", "~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32",
"~/AppData/Local/Arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/**", "~/AppData/Local/Arduino15/packages/esp32/tools/esp32-libs/3.3.10/include/**",
"~/Documents/Arduino/libraries/**" "~/Documents/Arduino/libraries/**"
], ],
"forcedInclude": [ "forcedInclude": [
"${workspaceFolder}/.vscode/intellisense.h", "${workspaceFolder}/.vscode/intellisense.h",
"~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32/Arduino.h", "~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32/Arduino.h",
"~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32/pins_arduino.h", "~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32/pins_arduino.h",
"${workspaceFolder}/flix/cli.ino", "${workspaceFolder}/flix/cli.ino",
"${workspaceFolder}/flix/control.ino", "${workspaceFolder}/flix/control.ino",
"${workspaceFolder}/flix/estimate.ino", "${workspaceFolder}/flix/estimate.ino",
@@ -128,7 +128,7 @@
"${workspaceFolder}/flix/parameters.ino", "${workspaceFolder}/flix/parameters.ino",
"${workspaceFolder}/flix/safety.ino" "${workspaceFolder}/flix/safety.ino"
], ],
"compilerPath": "~/AppData/Local/Arduino15/packages/esp32/tools/esp-x32/2511/bin/xtensa-esp32-elf-g++.exe", "compilerPath": "~/AppData/Local/Arduino15/packages/esp32/tools/esp-x32/2601/bin/xtensa-esp32-elf-g++.exe",
"cStandard": "c11", "cStandard": "c11",
"cppStandard": "c++17", "cppStandard": "c++17",
"defines": [ "defines": [
+1
View File
@@ -1,6 +1,7 @@
{ {
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
"recommendations": [ "recommendations": [
"dangmai.workspace-default-settings",
"ms-vscode.cpptools", "ms-vscode.cpptools",
"ms-vscode.cmake-tools", "ms-vscode.cmake-tools",
"ms-python.python" "ms-python.python"
+19 -11
View File
@@ -1,24 +1,32 @@
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*)) 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 export ARDUINO_NETWORK_CONNECTION_TIMEOUT := 1h
arduino-cli compile --fqbn $(BOARD) flix
build: .core .libs
arduino-cli compile --fqbn $(BOARD) --build-property "build.core_debug_level=1" flix $(EXTRA)
upload: build upload: build
arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" flix arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" flix
erase:
arduino-cli burn-bootloader --fqbn $(BOARD) -p "$(PORT)" -P esptool
monitor: monitor:
arduino-cli monitor -p "$(PORT)" -c baudrate=115200 arduino-cli monitor -p "$(PORT)" -c baudrate=115200
dependencies .dependencies: core .core:
arduino-cli core update-index --config-file arduino-cli.yaml arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
arduino-cli core install esp32:esp32@3.3.6 --config-file arduino-cli.yaml arduino-cli core install esp32:esp32@3.3.10 --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
touch .core
libs .libs:
arduino-cli lib update-index arduino-cli lib update-index
arduino-cli lib install "FlixPeriph" arduino-cli lib install "FlixPeriph"
arduino-cli lib install "MAVLink"@2.0.25 arduino-cli lib install "MAVLink"@2.0.25
touch .dependencies touch .libs
upload_proxy: .dependencies upload_proxy: .core .libs
arduino-cli compile --fqbn $(BOARD) tools/espnow-proxy arduino-cli compile --fqbn $(BOARD) tools/espnow-proxy
arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" tools/espnow-proxy arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" tools/espnow-proxy
@@ -26,7 +34,7 @@ gazebo/build cmake: gazebo/CMakeLists.txt
mkdir -p gazebo/build mkdir -p gazebo/build
cd gazebo/build && cmake .. cd gazebo/build && cmake ..
build_simulator: .dependencies gazebo/build build_simulator: .libs gazebo/build
make -C gazebo/build make -C gazebo/build
simulator: build_simulator simulator: build_simulator
@@ -41,6 +49,6 @@ plot:
plotjuggler -d $(shell ls -t tools/log/*.csv | head -n1) plotjuggler -d $(shell ls -t tools/log/*.csv | head -n1)
clean: clean:
rm -rf gazebo/build flix/build flix/cache .dependencies rm -rf gazebo/build flix/build flix/cache .core .libs
.PHONY: build upload monitor dependencies cmake build_simulator simulator log clean .PHONY: build upload monitor core libs cmake build_simulator simulator log clean
+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> <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 ## Simulation
The simulator is implemented using Gazebo and runs the original Arduino code: 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| |*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| |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| |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| |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| |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| |Battery connector cable|MX2.0 2P female|<img src="docs/img/mx.png" width=100>|1|
-5
View File
@@ -1,5 +0,0 @@
board_manager:
additional_urls:
- https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
network:
connection_timeout: 1h
+3
View File
@@ -79,6 +79,9 @@ To add a new parameter:
See examples of adding new parameters in commits: [c434107](https://github.com/okalachev/flix/commit/c434107), [a687303](https://github.com/okalachev/flix/commit/a687303). See examples of adding new parameters in commits: [c434107](https://github.com/okalachev/flix/commit/c434107), [a687303](https://github.com/okalachev/flix/commit/a687303).
> [!NOTE]
> Since all the parameters are internally stored and passed as floats, the safe range for `int` parameters is -16777216 to 16777215.
## Adding a subsystem ## Adding a subsystem
To add a new subsystem: To add a new subsystem:
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: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+74 -31
View File
@@ -1,34 +1,63 @@
# Usage: build, setup and flight # 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 ```bash
git clone https://github.com/okalachev/flix.git && cd flix 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 #### Arduino IDE (Windows, Linux, macOS)
You can build and upload the firmware using either **Arduino IDE** (easier for beginners) or **command line**.
### Arduino IDE (Windows, Linux, macOS)
<img src="img/arduino-ide.png" width="400" alt="Flix firmware open in Arduino IDE"> <img src="img/arduino-ide.png" width="400" alt="Flix firmware open in Arduino IDE">
1. Install [Arduino IDE](https://www.arduino.cc/en/software) (version 2 is recommended). 1. Install [Arduino IDE](https://www.arduino.cc/en/software) (version 2 is recommended).
2. *Windows users might need to install [USB to UART bridge driver from Silicon Labs](https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers).* 2. *Windows users might need to install [USB to UART bridge driver from Silicon Labs](https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers).*
3. Install ESP32 core, version 3.3.6. See the [official Espressif's instructions](https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html#installing-using-arduino-ide) on installing ESP32 Core in Arduino IDE. 3. Install ESP32 core, version 3.3.10. See the [official Espressif's instructions](https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html#installing-using-arduino-ide) on installing ESP32 Core in Arduino IDE.
4. Install the following libraries using [Library Manager](https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-installing-a-library): 4. Install the following libraries using [Library Manager](https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-installing-a-library):
* `FlixPeriph`, the latest version. * `FlixPeriph`, the latest version.
* `MAVLink`, version 2.0.25. * `MAVLink`, version 2.0.25.
5. Open the `flix/flix.ino` sketch from downloaded firmware sources in Arduino IDE. 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. [Build and upload](https://docs.arduino.cc/software/ide-v2/tutorials/getting-started/ide-v2-uploading-a-sketch) the firmware using Arduino IDE. 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/). 1. [Install Arduino CLI](https://arduino.github.io/arduino-cli/installation/).
@@ -57,6 +86,12 @@ You can build and upload the firmware using either **Arduino IDE** (easier for b
make upload monitor make upload monitor
``` ```
For ESP32-S3/ESP32-C3 boards, set the appropriate [FQBN](https://docs.arduino.cc/arduino-cli/FAQ/#whats-the-fqbn-string) using `BOARD` parameter:
```bash
make BOARD=esp32:esp32:esp32s3:FlashSize=4M,CDCOnBoot=cdc upload
```
See other available Make commands in [Makefile](../Makefile). See other available Make commands in [Makefile](../Makefile).
> [!TIP] > [!TIP]
@@ -64,15 +99,6 @@ See other available Make commands in [Makefile](../Makefile).
## Before first flight ## Before first flight
### Choose the IMU model
In case if using different IMU model than MPU9250, change `imu` variable declaration in the `imu.ino`:
```cpp
ICM20948 imu(SPI); // For ICM-20948
MPU6050 imu(Wire); // For MPU-6050
```
### Connect using QGroundControl ### Connect using QGroundControl
QGroundControl is a ground control station software that can be used to monitor and control the drone. QGroundControl is a ground control station software that can be used to monitor and control the drone.
@@ -82,6 +108,9 @@ QGroundControl is a ground control station software that can be used to monitor
3. Connect your computer or smartphone to the appeared `flix` Wi-Fi network (password: `flixwifi`). 3. Connect your computer or smartphone to the appeared `flix` Wi-Fi network (password: `flixwifi`).
4. Launch QGroundControl app. It should connect and begin showing the drone's telemetry automatically. 4. Launch QGroundControl app. It should connect and begin showing the drone's telemetry automatically.
> [!TIP]
> If QGroundControl doesn't connect, try to disable the firewall and/or VPN on your computer, as they may block the connection.
### Access console ### Access console
The console is a command line interface (CLI) that allows to interact with the drone, change parameters, and perform various actions. There are two ways of accessing the console: using **serial port** or using **QGroundControl (wirelessly)**. The console is a command line interface (CLI) that allows to interact with the drone, change parameters, and perform various actions. There are two ways of accessing the console: using **serial port** or using **QGroundControl (wirelessly)**.
@@ -95,7 +124,7 @@ To access the console using serial port:
To access the console using QGroundControl: To access the console using QGroundControl:
1. Connect to the drone using QGroundControl app. 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"> <img src="img/cli.png" width="400">
@@ -110,6 +139,17 @@ The drone is configured using parameters. To access and modify them, go to the Q
You can also work with parameters using `p` command in the console. Parameter names are case-insensitive. You can also work with parameters using `p` command in the console. Parameter names are case-insensitive.
### Configure the IMU
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 (should print `status: OK`).
### Define IMU orientation ### Define IMU orientation
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 IMU orientation (relative to the drone's axes) is defined using the parameters: `IMU_ROT_ROLL`, `IMU_ROT_PITCH`, and `IMU_ROT_YAW`.
@@ -138,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). 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). 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). 2. Decrease the PWM frequency using the `MOT_PWM_FREQ` parameter (400 is typical).
@@ -148,7 +188,7 @@ If using brushless motors and ESCs:
> [!CAUTION] > [!CAUTION]
> **Remove the props when configuring the motors!** If improperly configured, you may not be able to stop them. > **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: 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:
@@ -188,7 +228,7 @@ After this setup, you should see the battery voltage in QGroundControl top panel
## Setup remote control ## Setup remote control
There are several ways to control the drone's flight: using **smartphone** (Wi-Fi), using **SBUS remote control**, or using **USB remote control** (Wi-Fi). There are several ways to control the drone's flight: using **smartphone** (Wi-Fi), using **SBUS remote control**, or using **USB remote control** (Wi-Fi/ESP-NOW).
### Control with a smartphone ### Control with a smartphone
@@ -233,7 +273,7 @@ If your drone doesn't have RC receiver installed, you can use USB remote control
3. Power up the drone. 3. Power up the drone.
4. Connect your computer to the appeared `flix` Wi-Fi network (password: `flixwifi`). 4. Connect your computer to the appeared `flix` Wi-Fi network (password: `flixwifi`).
5. Launch QGroundControl app. It should connect and begin showing the drone's telemetry automatically. 5. Launch QGroundControl app. It should connect and begin showing the drone's telemetry automatically.
6. Go the the QGroundControl menu ⇒ *Vehicle Setup* ⇒ *Joystick*. Calibrate you USB remote control there. 6. Go to the QGroundControl menu ⇒ *Vehicle Setup* ⇒ *Joystick*. Calibrate your USB remote control there.
7. Use the USB remote control to fly the drone! 7. Use the USB remote control to fly the drone!
## Flight ## Flight
@@ -323,13 +363,13 @@ 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`. 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 espnow 7a:c8:e3:eb:bf:e9 &PiuSysxP9+$L&5E
``` ```
Run this line as a console command on each drone you want to bind to this proxy board. [The maximum number](https://github.com/espressif/esp-idf/blob/e95cab4be8fd293e3f3323181e7a2280874da6f7/components/esp_wifi/include/esp_now.h#L32-L33) of simultaneously connected drones is 20 (unencrypted) io 6 (encrypted). Run this line as a console command on each drone you want to bind to this proxy board. [The maximum number](https://github.com/espressif/esp-idf/blob/e95cab4be8fd293e3f3323181e7a2280874da6f7/components/esp_wifi/include/esp_now.h#L32-L33) of simultaneously connected drones is 20 (unencrypted) or 6 (encrypted).
3. Set the `WIFI_MODE` parameter to `3` on the drone: 3. Set the `WIFI_MODE` parameter to `3` on the drone:
@@ -342,11 +382,14 @@ To setup ESP-NOW communication:
* Type: Serial. * Type: Serial.
* Serial Port: choose the port of the proxy ESP32 board, e. g. `/dev/cu.usbserial-0001`. * Serial Port: choose the port of the proxy ESP32 board, e. g. `/dev/cu.usbserial-0001`.
* Baud Rate: 115200. * 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 ## Flight log
After the flight, you can download the flight log for analysis wirelessly. Use the following command on your computer for that: After the flight, you can download the flight log wirelessly for analysis. Use the following command on your computer for that:
```bash ```bash
make log make log
+60
View File
@@ -4,6 +4,55 @@ 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).
<img src="img/user/nerush/1.jpg" height=200> <img src="img/user/nerush/2.jpg" height=200>
[Flight video](https://drive.google.com/file/d/1jRXeGx34lJpUfw0GKLQeIzkWZvooQJSE/view?usp=sharing).
---
Author: [Konstantinos Paraskevas](https://github.com/Frapais).<br>
Description: drone with a custom single-boarded airframe, extending the [Sprig-C3 module](https://github.com/Frapais/Sprig-C3).
ESP32-C3 microcontroller, ICM-20948 IMU, on-board fuel-gauge, status LED indicator.<br>
Repository with all the code and PCB sources: https://github.com/Frapais/Sprig-Drone.
<img src="img/user/kostas/1.jpg" height=150> <img src="img/user/kostas/2.jpg" height=150>
Detailed video about making the drone:
<a href="https://youtu.be/82Q-uBq6s48"><img width=400 src="https://i3.ytimg.com/vi/82Q-uBq6s48/maxresdefault.jpg"></a>
---
Author: [Awab Anas](http://t.me/AW_VENOM).<br>
Description: ESP32 D1 Mini, MPU-6050, 8520 3.7V brushed motors, 55 mm propellers, battery li-po 1200 mAh, controlling via [Mavlink Joystick app](https://github.com/goldarte/mavlink-joystick/releases/latest).<br>
[Flight validation](https://drive.google.com/file/d/12z0jfctZDBA6b5UKCG0Uje5rAxj6DhF-/view?usp=sharing).
<img src="img/user/aw_venom/1.jpg" height=200>
---
Author: [Ina Tix](https://t.me/ina_tix).<br> 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> 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). [Flight validation](https://drive.google.com/file/d/1yqkKNuz4R_yxGqUNQxVpixJbXqEEcUSj/view?usp=share_link).
@@ -57,6 +106,17 @@ Author: [goldarte](https://t.me/goldarte).<br>
--- ---
Author: [malagis](https://oshwhub.com/malagis).<br>
A Chinese custom PCB version of Flix with a big community of users, lots of materials and modifications.
Main project's page: https://oshwhub.com/malagis/esp32-mini-plane.<br>
Video about the project: https://www.bilibili.com/video/BV14vyqBFEJn/.
<img src="img/user/malagis/1.jpg" height=200> <img src="img/user/malagis/2.jpg" height=200> <img src="img/user/malagis/3.jpg" height=200>
---
## School 548 course ## School 548 course
Special course on quadcopter design and engineering took place in october-november 2025 in School 548, Moscow. The course included UAV control theory, electronics, drone assembly and setup practice, using the Flix project. Special course on quadcopter design and engineering took place in october-november 2025 in School 548, Moscow. The course included UAV control theory, electronics, drone assembly and setup practice, using the Flix project.
+28 -27
View File
@@ -31,33 +31,33 @@ const char* motd =
"Commands:\n\n" "Commands:\n\n"
"help - show help\n" "help - show help\n"
"p - show all parameters\n" "p - show all parameters\n"
"p <name> - show parameter\n" "p <str> - show parameters starting with str\n"
"p <name> <value> - set parameter\n" "p <name> <value> - set parameter\n"
"preset - reset parameters\n" "preset - reset parameters\n"
"time - show time info\n" "time - show time info\n"
"ps - show pitch/roll/yaw\n"
"psq - show attitude quaternion\n"
"imu - show IMU data\n" "imu - show IMU data\n"
"ca - calibrate accel\n"
"st - show state estimation\n"
"arm - arm the drone\n" "arm - arm the drone\n"
"disarm - disarm the drone\n" "disarm - disarm the drone\n"
"raw/stab/acro/auto - set mode\n" "raw/stab/acro/auto - set mode\n"
"rc - show RC data\n" "rc - show RC data\n"
"cr - calibrate RC\n"
"pw - show power info\n" "pw - show power info\n"
"wifi - show Wi-Fi info\n" "wifi - show Wi-Fi info\n"
"ap <ssid> <password> - setup Wi-Fi access point\n" "wifi ap/sta/espnow/off - set Wi-Fi mode\n"
"sta <ssid> <password> - setup Wi-Fi client mode\n" "ap <ssid> <password> - configure Wi-Fi access point\n"
"espnow <mac> [<key>] - setup ESP-NOW peer\n" "sta <ssid> <password> - configure Wi-Fi client mode\n"
"espnow <mac> [<key>] - configure ESP-NOW peer\n"
"mot - show motor output\n" "mot - show motor output\n"
"log [dump] - print log header [and data]\n" "log [dump] - print log header [and data]\n"
"cr - calibrate RC\n" "mfr/mfl/mrr/mrl [<thrust>] - test motor (remove props)\n"
"ca - calibrate accel\n"
"mfr, mfl, mrr, mrl - test motor (remove props)\n"
"sys - show system info\n" "sys - show system info\n"
"reset - reset drone's state\n" "reset - reset drone's state\n"
"reboot - reboot the drone\n"; "reboot - reboot the drone\n";
void print(const char* format, ...) { void print(const char* format, ...) {
char buf[1000]; char buf[3000];
va_list args; va_list args;
va_start(args, format); va_start(args, format);
vsnprintf(buf, sizeof(buf), format, args); vsnprintf(buf, sizeof(buf), format, args);
@@ -92,10 +92,8 @@ void doCommand(String str, bool echo = false) {
// execute command // execute command
if (command == "help" || command == "motd") { if (command == "help" || command == "motd") {
print("%s\n", motd); print("%s\n", motd);
} else if (command == "p" && arg0 == "") { } else if (command == "p" && arg1 == "") {
printParameters(); printParameters(arg0.c_str());
} else if (command == "p" && arg0 != "" && arg1 == "") {
print("%s = %g\n", arg0.c_str(), getParameter(arg0.c_str()));
} else if (command == "p") { } else if (command == "p") {
bool success = setParameter(arg0.c_str(), arg1.toFloat()); bool success = setParameter(arg0.c_str(), arg1.toFloat());
if (success) { if (success) {
@@ -109,15 +107,15 @@ void doCommand(String str, bool echo = false) {
print("Time: %f\n", t); print("Time: %f\n", t);
print("Loop rate: %.0f\n", loopRate); print("Loop rate: %.0f\n", loopRate);
print("dt: %f\n", dt); print("dt: %f\n", dt);
} else if (command == "ps") {
Vector a = attitude.toEuler();
print("roll: %f pitch: %f yaw: %f\n", degrees(a.x), degrees(a.y), degrees(a.z));
} else if (command == "psq") {
print("qw: %f qx: %f qy: %f qz: %f\n", attitude.w, attitude.x, attitude.y, attitude.z);
} else if (command == "imu") { } else if (command == "imu") {
printIMUInfo(); printIMUInfo();
printIMUCalibration(); printIMUCalibration();
print("landed: %d\n", landed); print("landed: %d\n", landed);
} else if (command == "st") {
print("rates: %g %g %g\n", rates.x, rates.y, rates.z);
print("attitude: %g %g %g %g\n", attitude.w, attitude.x, attitude.y, attitude.z);
print("roll: %g° pitch: %g° yaw: %g°\n", degrees(attitude.getRoll()), degrees(attitude.getPitch()), degrees(attitude.getYaw()));
print("landed: %d\n", landed);
} else if (command == "arm") { } else if (command == "arm") {
armed = true; armed = true;
} else if (command == "disarm") { } else if (command == "disarm") {
@@ -142,8 +140,10 @@ void doCommand(String str, bool echo = false) {
print("armed: %d\n", armed); print("armed: %d\n", armed);
} else if (command == "pw") { } else if (command == "pw") {
print("Voltage: %.1f V\n", voltage); print("Voltage: %.1f V\n", voltage);
} else if (command == "wifi") { } else if (command == "wifi" && arg0 == "") {
printWiFiInfo(); printWiFiInfo();
} else if (command == "wifi") {
setWiFiMode(arg0);
} else if (command == "ap") { } else if (command == "ap") {
configWiFi(W_AP, arg0.c_str(), arg1.c_str()); configWiFi(W_AP, arg0.c_str(), arg1.c_str());
} else if (command == "sta") { } else if (command == "sta") {
@@ -161,21 +161,22 @@ void doCommand(String str, bool echo = false) {
} else if (command == "ca") { } else if (command == "ca") {
calibrateAccel(); calibrateAccel();
} else if (command == "mfr") { } else if (command == "mfr") {
testMotor(MOTOR_FRONT_RIGHT); testMotor(MOTOR_FRONT_RIGHT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "mfl") { } else if (command == "mfl") {
testMotor(MOTOR_FRONT_LEFT); testMotor(MOTOR_FRONT_LEFT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "mrr") { } else if (command == "mrr") {
testMotor(MOTOR_REAR_RIGHT); testMotor(MOTOR_REAR_RIGHT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "mrl") { } else if (command == "mrl") {
testMotor(MOTOR_REAR_LEFT); testMotor(MOTOR_REAR_LEFT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "sys") { } else if (command == "sys") {
#ifdef ESP32 #ifdef ESP32
print("Chip: %s\n", ESP.getChipModel()); print("Chip: %s\n", ESP.getChipModel());
print("Temperature: %.1f °C\n", temperatureRead()); print("Temperature: %.1f °C\n", temperatureRead());
print("Free heap: %d\n", ESP.getFreeHeap()); print("Total RAM: %d KB\n", ESP.getHeapSize() / 1024);
print("Free heap: %d KB\n", ESP.getFreeHeap() / 1024);
print("Firmware: " __DATE__ " " __TIME__ "\n"); print("Firmware: " __DATE__ " " __TIME__ "\n");
// Print tasks table // Print tasks table
print("Num Task Stack Prio Core CPU%%\n"); print("Num Task MinSt Prio Core CPU%%\n");
int taskCount = uxTaskGetNumberOfTasks(); int taskCount = uxTaskGetNumberOfTasks();
TaskStatus_t *systemState = new TaskStatus_t[taskCount]; TaskStatus_t *systemState = new TaskStatus_t[taskCount];
uint32_t totalRunTime; uint32_t totalRunTime;
@@ -209,7 +210,7 @@ void handleInput() {
while (Serial.available()) { while (Serial.available()) {
char c = Serial.read(); char c = Serial.read();
if (c == '\n') { if (c == '\n' || c == '\r') {
doCommand(input); doCommand(input);
input.clear(); input.clear();
} else { } else {
+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 -33
View File
@@ -9,31 +9,6 @@
#include "lpf.h" #include "lpf.h"
#include "util.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 const int RAW = 0, ACRO = 1, STAB = 2, AUTO = 3; // flight modes
int mode = STAB; int mode = STAB;
bool armed = false; bool armed = false;
@@ -44,14 +19,14 @@ Vector ratesExtra; // feedforward rates
Vector torqueTarget; Vector torqueTarget;
float thrustTarget; float thrustTarget;
PID rollRatePID(ROLLRATE_P, ROLLRATE_I, ROLLRATE_D, ROLLRATE_I_LIM, RATES_D_LPF_ALPHA); PID rollRatePID(0.05, 0.2, 0.001, 0.3, 0.2);
PID pitchRatePID(PITCHRATE_P, PITCHRATE_I, PITCHRATE_D, PITCHRATE_I_LIM, RATES_D_LPF_ALPHA); PID pitchRatePID(0.05, 0.2, 0.001, 0.3, 0.2);
PID yawRatePID(YAWRATE_P, YAWRATE_I, YAWRATE_D); PID yawRatePID(0.3, 0, 0, 0.3);
PID rollPID(ROLL_P, ROLL_I, ROLL_D); PID rollPID(6);
PID pitchPID(PITCH_P, PITCH_I, PITCH_D); PID pitchPID(6);
PID yawPID(YAW_P, 0, 0); PID yawPID(3);
Vector maxRate(ROLLRATE_MAX, PITCHRATE_MAX, YAWRATE_MAX); Vector maxRate(radians(360), radians(360), radians(360));
float tiltMax = TILT_MAX; float tiltMax = radians(30);
int flightModes[] = {STAB, STAB, STAB}; // map for rc mode switch 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; extern const int MOTOR_REAR_LEFT, MOTOR_REAR_RIGHT, MOTOR_FRONT_RIGHT, MOTOR_FRONT_LEFT;
@@ -149,6 +124,7 @@ void controlTorque() {
motors[MOTOR_REAR_LEFT] = thrustTarget + torqueTarget.x + torqueTarget.y - torqueTarget.z; motors[MOTOR_REAR_LEFT] = thrustTarget + torqueTarget.x + torqueTarget.y - torqueTarget.z;
motors[MOTOR_REAR_RIGHT] = thrustTarget - torqueTarget.x + torqueTarget.y + torqueTarget.z; motors[MOTOR_REAR_RIGHT] = thrustTarget - torqueTarget.x + torqueTarget.y + torqueTarget.z;
// Prioritize angle control over thrust control
desaturate(motors[MOTOR_FRONT_LEFT], motors[MOTOR_FRONT_RIGHT], motors[MOTOR_REAR_LEFT], motors[MOTOR_REAR_RIGHT]); 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[0] = constrain(motors[0], 0, 1);
+2 -2
View File
@@ -32,8 +32,7 @@ void applyGyro() {
void applyAcc() { void applyAcc() {
// test should we apply accelerometer gravity correction // test should we apply accelerometer gravity correction
float accNorm = acc.norm(); landed = !motorsActive() && abs(acc.norm() - ONE_G) < ONE_G * 0.1f;
landed = !motorsActive() && abs(accNorm - ONE_G) < ONE_G * 0.1f;
if (!landed) return; if (!landed) return;
@@ -47,6 +46,7 @@ void applyAcc() {
void applyLevel() { void applyLevel() {
if (landed) return; if (landed) return;
if (thrustTarget < 0.1) return; // skip at idle thrust
// assume the pilot keeps the drone more or less level in flight // assume the pilot keeps the drone more or less level in flight
Vector up = Quaternion::rotateVector(Vector(0, 0, 1), attitude); Vector up = Quaternion::rotateVector(Vector(0, 0, 1), attitude);
+1 -1
View File
@@ -17,7 +17,7 @@ extern float motors[4];
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
print("Initializing flix\n"); print("Initializing Flix\n");
setupParameters(); setupParameters();
setupPower(); setupPower();
setupLED(); setupLED();
+45 -21
View File
@@ -4,12 +4,17 @@
// Work with the IMU sensor // Work with the IMU sensor
#include <SPI.h> #include <SPI.h>
#include <Wire.h>
#include <FlixPeriph.h> #include <FlixPeriph.h>
#include "vector.h" #include "vector.h"
#include "lpf.h" #include "lpf.h"
#include "util.h" #include "util.h"
MPU9250 imu(SPI); 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 Vector imuRotation(0, 0, PI / 2); // imu orientation as Euler angles
Vector gyro; // gyroscope output, rad/s Vector gyro; // gyroscope output, rad/s
@@ -23,27 +28,42 @@ LowPassFilter<Vector> gyroBiasFilter(0.001);
void setupIMU() { void setupIMU() {
print("Setup IMU\n"); print("Setup IMU\n");
imu.begin(); free(imu);
if (imuModel == 3) imuBus = 1; // MPU6050 is I2C only
if (imuBus == 0) {
// SPI connection
SPI.begin(imuSckPin, imuMisoPin, imuMosiPin);
imu = IMU::create(imuModel, SPI, imuCsPin, imuIntPin);
} else {
// I2C connection
Wire.setPins(imuSdaPin, imuSclPin);
imu = IMU::create(imuModel, Wire, imuIntPin);
}
imu->begin();
configureIMU(); configureIMU();
} }
void configureIMU() { void configureIMU() {
imu.setAccelRange(imu.ACCEL_RANGE_4G); imu->setAccelRange(IMU::ACCEL_RANGE_4G);
imu.setGyroRange(imu.GYRO_RANGE_2000DPS); imu->setGyroRange(IMU::GYRO_RANGE_2000DPS);
imu.setDLPF(imu.DLPF_MAX); imu->setDLPF(IMU::DLPF_MAX);
imu.setRate(imu.RATE_1KHZ_APPROX); imu->setRate(IMU::RATE_1KHZ_APPROX);
imu.setupInterrupt(); imu->setupInterrupt();
} }
void readIMU() { void readIMU() {
imu.waitForData(); imu->waitForData();
imu.getGyro(gyro.x, gyro.y, gyro.z); imu->getGyro(gyro.x, gyro.y, gyro.z);
imu.getAccel(acc.x, acc.y, acc.z); imu->getAccel(acc.x, acc.y, acc.z);
calibrateGyroOnce(); calibrateGyroOnce();
// apply scale and bias
// Apply scale and bias
acc = (acc - accBias) / accScale; acc = (acc - accBias) / accScale;
gyro = gyro - gyroBias; gyro = gyro - gyroBias;
// rotate to body frame
// Rotate to body frame
Quaternion rotation = Quaternion::fromEuler(imuRotation); Quaternion rotation = Quaternion::fromEuler(imuRotation);
acc = Quaternion::rotateVector(acc, rotation.inversed()); acc = Quaternion::rotateVector(acc, rotation.inversed());
gyro = Quaternion::rotateVector(gyro, rotation.inversed()); gyro = Quaternion::rotateVector(gyro, rotation.inversed());
@@ -52,12 +72,13 @@ void readIMU() {
void calibrateGyroOnce() { void calibrateGyroOnce() {
static Delay landedDelay(2); static Delay landedDelay(2);
if (!landedDelay.update(landed)) return; // calibrate only if definitely stationary if (!landedDelay.update(landed)) return; // calibrate only if definitely stationary
gyroBias = gyroBiasFilter.update(gyro); gyroBias = gyroBiasFilter.update(gyro);
} }
void calibrateAccel() { void calibrateAccel() {
print("Calibrating accelerometer\n"); 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"); print("1/6 Place level [8 sec]\n");
pause(8); pause(8);
@@ -91,9 +112,9 @@ void calibrateAccelOnce() {
// Compute the average of the accelerometer readings // Compute the average of the accelerometer readings
acc = Vector(0, 0, 0); acc = Vector(0, 0, 0);
for (int i = 0; i < samples; i++) { for (int i = 0; i < samples; i++) {
imu.waitForData(); imu->waitForData();
Vector sample; Vector sample;
imu.getAccel(sample.x, sample.y, sample.z); imu->getAccel(sample.x, sample.y, sample.z);
acc = acc + sample; acc = acc + sample;
} }
acc = acc / samples; acc = acc / samples;
@@ -105,6 +126,7 @@ void calibrateAccelOnce() {
if (acc.x < accMin.x) accMin.x = acc.x; if (acc.x < accMin.x) accMin.x = acc.x;
if (acc.y < accMin.y) accMin.y = acc.y; if (acc.y < accMin.y) accMin.y = acc.y;
if (acc.z < accMin.z) accMin.z = acc.z; if (acc.z < accMin.z) accMin.z = acc.z;
// Compute scale and bias // Compute scale and bias
accScale = (accMax - accMin) / 2 / ONE_G; accScale = (accMax - accMin) / 2 / ONE_G;
accBias = (accMax + accMin) / 2; accBias = (accMax + accMin) / 2;
@@ -117,16 +139,18 @@ void printIMUCalibration() {
} }
void printIMUInfo() { void printIMUInfo() {
imu.status() ? print("status: ERROR %d\n", imu.status()) : print("status: OK\n"); imu->status() ? print("status: ERROR %d\n", imu->status()) : print("status: OK\n");
print("model: %s\n", imu.getModel()); print("model: %s\n", imu->getModel());
print("who am I: 0x%02X\n", imu.whoAmI()); print("who am I: 0x%02X\n", imu->whoAmI());
print("rate: %.0f\n", loopRate); print("rate: %.0f\n", loopRate);
print("interrupt mode: %s\n", imuIntPin != -1 ? "pin" : "timer");
print("temperature: %.1f °C\n", imu->getTemp());
print("gyro: %f %f %f\n", gyro.x, gyro.y, gyro.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); print("acc: %f %f %f\n", acc.x, acc.y, acc.z);
imu.waitForData(); imu->waitForData();
Vector rawGyro, rawAcc; Vector rawGyro, rawAcc;
imu.getGyro(rawGyro.x, rawGyro.y, rawGyro.z); imu->getGyro(rawGyro.x, rawGyro.y, rawGyro.z);
imu.getAccel(rawAcc.x, rawAcc.y, rawAcc.z); imu->getAccel(rawAcc.x, rawAcc.y, rawAcc.z);
print("raw gyro: %f %f %f\n", rawGyro.x, rawGyro.y, rawGyro.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); print("raw acc: %f %f %f\n", rawAcc.x, rawAcc.y, rawAcc.z);
} }
+22 -8
View File
@@ -10,10 +10,14 @@ extern float controlTime;
extern float voltage; extern float voltage;
int mavlinkSysId = 1; int mavlinkSysId = 1;
Rate telemetryFast(10);
Rate telemetrySlow(2);
bool mavlinkConnected = false; Rate telemetrySlow(2);
Rate telemetryAttitude(20);
Rate telemetryRC(10);
Rate telemetryMotors(10);
Rate telemetryIMU(15);
float mavlinkTime = NAN; // time of last received message
String mavlinkPrintBuffer; String mavlinkPrintBuffer;
void processMavlink() { void processMavlink() {
@@ -34,36 +38,46 @@ void sendMavlink() {
((mode == AUTO) ? MAV_MODE_FLAG_AUTO_ENABLED : MAV_MODE_FLAG_MANUAL_INPUT_ENABLED), ((mode == AUTO) ? MAV_MODE_FLAG_AUTO_ENABLED : MAV_MODE_FLAG_MANUAL_INPUT_ENABLED),
mode, MAV_STATE_STANDBY); mode, MAV_STATE_STANDBY);
sendMessage(&msg); 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, 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); MAV_VTOL_STATE_UNDEFINED, landed ? MAV_LANDED_STATE_ON_GROUND : MAV_LANDED_STATE_IN_AIR);
sendMessage(&msg); sendMessage(&msg);
}
if (telemetrySlow && valid(voltage)) {
uint16_t voltages[] = {(uint16_t)(voltage * 1000), UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX}; uint16_t voltages[] = {(uint16_t)(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}; uint16_t voltagesExt[] = {0, 0, 0, 0};
float remaining = constrain(mapf(voltage, 3.4, 4.2, 0, 1), 0, 1); 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, 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); MAV_BATTERY_TYPE_LIPO, INT16_MAX, voltages, -1, -1, -1, remaining * 100, 0, MAV_BATTERY_CHARGE_STATE_OK, voltagesExt, 0, 0);
if (valid(voltage)) sendMessage(&msg); sendMessage(&msg);
} }
if (telemetryFast && mavlinkConnected) { if (telemetryAttitude) {
const float offset[] = {0, 0, 0, 0}; const float offset[] = {0, 0, 0, 0};
mavlink_msg_attitude_quaternion_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, mavlink_msg_attitude_quaternion_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg,
time, attitude.w, attitude.x, -attitude.y, -attitude.z, rates.x, -rates.y, -rates.z, offset); // convert to frd time, attitude.w, attitude.x, -attitude.y, -attitude.z, rates.x, -rates.y, -rates.z, offset); // convert to frd
sendMessage(&msg); sendMessage(&msg);
}
if (telemetryRC && channels[0]) { // 0 means no RC input
mavlink_msg_rc_channels_raw_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, controlTime * 1000, 0, mavlink_msg_rc_channels_raw_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, controlTime * 1000, 0,
channels[0], channels[1], channels[2], channels[3], channels[4], channels[5], channels[6], channels[7], UINT8_MAX); channels[0], channels[1], channels[2], channels[3], channels[4], channels[5], channels[6], channels[7], UINT8_MAX);
if (channels[0] != 0) sendMessage(&msg); // 0 means no RC input sendMessage(&msg);
}
if (telemetryMotors) {
float controls[8]; float controls[8];
memcpy(controls, motors, sizeof(motors)); memcpy(controls, motors, sizeof(motors));
mavlink_msg_actuator_control_target_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, time, 0, controls); mavlink_msg_actuator_control_target_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, time, 0, controls);
sendMessage(&msg); sendMessage(&msg);
}
if (telemetryIMU) {
mavlink_msg_scaled_imu_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, time, mavlink_msg_scaled_imu_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, time,
acc.x / ONE_G * 1000, -acc.y / ONE_G * 1000, -acc.z / ONE_G * 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, gyro.x * 1000, -gyro.y * 1000, -gyro.z * 1000,
@@ -81,13 +95,13 @@ void sendMessage(const void *msg) {
void receiveMavlink() { void receiveMavlink() {
uint8_t buf[MAVLINK_MAX_PACKET_LEN]; uint8_t buf[MAVLINK_MAX_PACKET_LEN];
int len = receiveWiFi(buf, MAVLINK_MAX_PACKET_LEN); int len = receiveWiFi(buf, MAVLINK_MAX_PACKET_LEN);
if (len) mavlinkConnected = true;
// New packet, parse it // New packet, parse it
mavlink_message_t msg; mavlink_message_t msg;
mavlink_status_t status; mavlink_status_t status;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
if (mavlink_parse_char(MAVLINK_COMM_0, buf[i], &msg, &status)) { if (mavlink_parse_char(MAVLINK_COMM_0, buf[i], &msg, &status)) {
mavlinkTime = t;
handleMavlink(&msg); handleMavlink(&msg);
} }
} }
+16 -5
View File
@@ -17,24 +17,35 @@ int pwmMax = -1; // -1 means duty cycle mode
const int MOTOR_REAR_LEFT = 0, MOTOR_REAR_RIGHT = 1, MOTOR_FRONT_RIGHT = 2, MOTOR_FRONT_LEFT = 3; const int MOTOR_REAR_LEFT = 0, MOTOR_REAR_RIGHT = 1, MOTOR_FRONT_RIGHT = 2, MOTOR_FRONT_LEFT = 3;
void setupMotors() { void setupMotors() {
print("Setup Motors\n"); print("Setup motors\n");
// configure pins // Configure pins
#ifdef ESP32
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (motorPins[i] < 0) continue; // skip unassigned motors
ledcAttach(motorPins[i], pwmFrequency, pwmResolution); ledcAttach(motorPins[i], pwmFrequency, pwmResolution);
pwmFrequency = ledcChangeFrequency(motorPins[i], pwmFrequency, pwmResolution); // when reconfiguring pwmFrequency = ledcChangeFrequency(motorPins[i], pwmFrequency, pwmResolution); // when reconfiguring
} }
#else
analogWriteResolution(pwmResolution);
analogWriteFrequency(pwmFrequency);
#endif
sendMotors(); sendMotors();
print("Motors initialized\n");
} }
void sendMotors() { void sendMotors() {
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (motorPins[i] < 0) continue; // skip unassigned motors
#ifdef ESP32
ledcWrite(motorPins[i], getDutyCycle(motors[i])); ledcWrite(motorPins[i], getDutyCycle(motors[i]));
#else
analogWrite(motorPins[i], getDutyCycle(motors[i]));
#endif
} }
} }
int getDutyCycle(float value) { int getDutyCycle(float value) {
value = constrain(value, 0, 1); value = constrain(value, 0, 1);
if (pwmMax >= 0) { // pwm mode if (pwmMax >= 0) { // pwm mode
float pwm = mapf(value, 0, 1, pwmMin, pwmMax); float pwm = mapf(value, 0, 1, pwmMin, pwmMax);
if (value == 0) pwm = pwmStop; if (value == 0) pwm = pwmStop;
@@ -49,9 +60,9 @@ bool motorsActive() {
return motors[0] != 0 || motors[1] != 0 || motors[2] != 0 || motors[3] != 0; 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); 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 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(); sendMotors();
pause(3); pause(3);
+36 -11
View File
@@ -3,23 +3,26 @@
// Parameters storage in flash memory // Parameters storage in flash memory
#include <Preferences.h> #include "prefs.h"
#include "util.h" #include "util.h"
extern int channelZero[16], channelMax[16]; extern int channelZero[16], channelMax[16];
extern int rollChannel, pitchChannel, throttleChannel, yawChannel, armedChannel, modeChannel; extern int rollChannel, pitchChannel, throttleChannel, yawChannel, armedChannel, modeChannel;
extern int rcRxPin, voltagePin; extern int rcRxPin, voltagePin;
extern int wifiMode, wifiLongRange, udpLocalPort, udpRemotePort, espnowChannel; extern int wifiMode, wifiLongRange, wifiBroadcast, udpLocalPort, udpRemotePort, espnowChannel;
extern float rcLossTimeout, descendTime; extern float rcLossTimeout, descendTime, disarmTilt;
extern float voltageScale; extern float voltageScale;
extern LowPassFilter<float> voltageFilter; extern LowPassFilter<float> voltageFilter;
#include "config.h"
Preferences storage; Preferences storage;
struct Parameter { struct Parameter {
const char *name; // max length is 15 const char *name; // max length is 15
bool integer; bool integer;
union { float *f; int *i; }; // pointer to the variable union { float *f; int *i; }; // pointer to the variable
float inital; // default value
float cache; // what's stored in flash float cache; // what's stored in flash
void (*callback)(); // called after parameter change void (*callback)(); // called after parameter change
Parameter(const char *name, float *variable, void (*callback)() = nullptr) : name(name), integer(false), f(variable), callback(callback) {}; Parameter(const char *name, float *variable, void (*callback)() = nullptr) : name(name), integer(false), f(variable), callback(callback) {};
@@ -43,6 +46,7 @@ Parameter parameters[] = {
{"CTL_Y_RATE_P", &yawRatePID.p}, {"CTL_Y_RATE_P", &yawRatePID.p},
{"CTL_Y_RATE_I", &yawRatePID.i}, {"CTL_Y_RATE_I", &yawRatePID.i},
{"CTL_Y_RATE_D", &yawRatePID.d}, {"CTL_Y_RATE_D", &yawRatePID.d},
{"CTL_Y_RATE_WU", &yawRatePID.windup},
{"CTL_Y_RATE_D_A", &yawRatePID.lpf.alpha}, {"CTL_Y_RATE_D_A", &yawRatePID.lpf.alpha},
{"CTL_R_P", &rollPID.p}, {"CTL_R_P", &rollPID.p},
{"CTL_R_I", &rollPID.i}, {"CTL_R_I", &rollPID.i},
@@ -59,6 +63,15 @@ Parameter parameters[] = {
{"CTL_FLT_MODE_1", &flightModes[1]}, {"CTL_FLT_MODE_1", &flightModes[1]},
{"CTL_FLT_MODE_2", &flightModes[2]}, {"CTL_FLT_MODE_2", &flightModes[2]},
// imu // imu
{"IMU_MODEL", &imuModel},
{"IMU_BUS", &imuBus},
{"IMU_PIN_SCK", &imuSckPin},
{"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_ROLL", &imuRotation.x},
{"IMU_ROT_PITCH", &imuRotation.y}, {"IMU_ROT_PITCH", &imuRotation.y},
{"IMU_ROT_YAW", &imuRotation.z}, {"IMU_ROT_YAW", &imuRotation.z},
@@ -111,12 +124,16 @@ Parameter parameters[] = {
{"WIFI_PORT_LOC", &udpLocalPort}, {"WIFI_PORT_LOC", &udpLocalPort},
{"WIFI_PORT_REM", &udpRemotePort}, {"WIFI_PORT_REM", &udpRemotePort},
{"WIFI_LONG_RANGE", &wifiLongRange}, {"WIFI_LONG_RANGE", &wifiLongRange},
{"WIFI_BROADCAST", &wifiBroadcast},
// espnow // espnow
{"ESPNOW_CHANNEL", &espnowChannel}, {"ESPNOW_CHANNEL", &espnowChannel},
// mavlink // mavlink
{"MAV_SYS_ID", &mavlinkSysId}, {"MAV_SYS_ID", &mavlinkSysId},
{"MAV_RATE_SLOW", &telemetrySlow.rate}, {"MAV_RATE_SLOW", &telemetrySlow.rate},
{"MAV_RATE_FAST", &telemetryFast.rate}, {"MAV_RATE_ATT", &telemetryAttitude.rate},
{"MAV_RATE_RC", &telemetryRC.rate},
{"MAV_RATE_MOT", &telemetryMotors.rate},
{"MAV_RATE_IMU", &telemetryIMU.rate},
// power // power
{"PWR_VOLT_PIN", &voltagePin, setupPower}, {"PWR_VOLT_PIN", &voltagePin, setupPower},
{"PWR_VOLT_SCALE", &voltageScale}, {"PWR_VOLT_SCALE", &voltageScale},
@@ -124,17 +141,19 @@ Parameter parameters[] = {
// safety // safety
{"SF_RC_LOSS_TIME", &rcLossTimeout}, {"SF_RC_LOSS_TIME", &rcLossTimeout},
{"SF_DESCEND_TIME", &descendTime}, {"SF_DESCEND_TIME", &descendTime},
{"SF_DISARM_TILT", &disarmTilt},
}; };
void setupParameters() { void setupParameters() {
print("Setup parameters\n"); print("Setup parameters\n");
setDefaults();
storage.begin("flix"); storage.begin("flix");
// Read parameters from storage // Read parameters from storage
for (auto &parameter : parameters) { for (auto &parameter : parameters) {
if (!storage.isKey(parameter.name)) { parameter.inital = parameter.getValue();
storage.putFloat(parameter.name, parameter.getValue()); // store default value if (storage.isKey(parameter.name)) {
parameter.setValue(storage.getFloat(parameter.name));
} }
parameter.setValue(storage.getFloat(parameter.name, 0));
parameter.cache = parameter.getValue(); parameter.cache = parameter.getValue();
} }
} }
@@ -180,17 +199,23 @@ void syncParameters() {
if (motorsActive()) return; // don't use flash while flying, it may cause a delay if (motorsActive()) return; // don't use flash while flying, it may cause a delay
for (auto &parameter : parameters) { for (auto &parameter : parameters) {
if (parameter.getValue() == parameter.cache) continue; // no change if (floatEquals(parameter.getValue(), parameter.cache)) continue; // no change
if (isnan(parameter.getValue()) && isnan(parameter.cache)) continue; // both are NAN
storage.putFloat(parameter.name, parameter.getValue()); storage.putFloat(parameter.name, parameter.getValue());
parameter.cache = parameter.getValue(); // update cache parameter.cache = parameter.getValue(); // update cache
} }
} }
void printParameters() { void printParameters(const char *filter) {
print("Name Value [Default]\n");
for (auto &parameter : parameters) { for (auto &parameter : parameters) {
print("%s = %g\n", parameter.name, parameter.getValue()); if (strncasecmp(parameter.name, filter, strlen(filter))) continue;
if (floatEquals(parameter.getValue(), parameter.inital)) { // parameter changed
print("%-15s %-13g\n", parameter.name, parameter.getValue());
} else {
print("%-15s %-13g [%g]\n", parameter.name, parameter.getValue(), parameter.inital);
}
} }
} }
+1 -1
View File
@@ -18,7 +18,7 @@ public:
LowPassFilter<float> lpf; // low pass filter for derivative term 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) {} p(p), i(i), d(d), windup(windup), lpf(dAlpha), dtMax(dtMax) {}
float update(float error) { float update(float error) {
+6 -1
View File
@@ -3,23 +3,28 @@
// Power management // Power management
#ifdef ESP32
#include <soc/soc.h> #include <soc/soc.h>
#include <soc/rtc_cntl_reg.h> #include <soc/rtc_cntl_reg.h>
#endif
#include "lpf.h" #include "lpf.h"
#include "util.h" #include "util.h"
float voltage = NAN; float voltage = NAN;
LowPassFilter<float> voltageFilter(0.2); LowPassFilter<float> voltageFilter(1);
int voltagePin = -1; int voltagePin = -1;
float voltageScale = 2; float voltageScale = 2;
void setupPower() { void setupPower() {
#ifdef ESP32
REG_CLR_BIT(RTC_CNTL_BROWN_OUT_REG, RTC_CNTL_BROWN_OUT_ENA); // disable reset on low voltage REG_CLR_BIT(RTC_CNTL_BROWN_OUT_REG, RTC_CNTL_BROWN_OUT_ENA); // disable reset on low voltage
#endif
if (digitalPinToAnalogChannel(voltagePin) == -1) voltagePin = -1; // test ADC pin if (digitalPinToAnalogChannel(voltagePin) == -1) voltagePin = -1; // test ADC pin
} }
void readVoltage() { void readVoltage() {
if (voltagePin < 0) return; if (voltagePin < 0) return;
static Rate rate(10); static Rate rate(10);
if (!rate) return; if (!rate) return;
+283
View File
@@ -0,0 +1,283 @@
#ifdef ESP32
#include <Preferences.h>
#else
#include <Arduino.h>
#include <EEPROM.h>
#include <string.h>
class Preferences {
public:
Preferences() = default;
~Preferences() = default;
bool begin(const char *name, bool readOnly = false, const char *partition_label = NULL) {
(void)name;
(void)readOnly;
(void)partition_label;
load();
started = true;
return true;
}
void end() {
started = false;
}
bool clear() {
if (!started) return false;
for (auto &entry : entries) {
entry = Entry();
}
return save();
}
size_t putFloat(const char *key, float value) {
if (!started) return 0;
int index = ensureKey(key);
if (index < 0) return 0;
entries[index].hasFloat = true;
entries[index].hasString = false;
entries[index].stringValue = "";
entries[index].floatValue = value;
if (!save()) return 0;
return sizeof(float);
}
float getFloat(const char *key, float defaultValue = NAN) {
if (!started) return defaultValue;
int index = findKey(key);
if (index < 0 || !entries[index].hasFloat) return defaultValue;
return entries[index].floatValue;
}
size_t putString(const char *key, const char *value) {
if (!started) return 0;
int index = ensureKey(key);
if (index < 0) return 0;
if (!value) value = "";
if (strlen(value) > MAX_STRING_LEN) return 0;
entries[index].hasString = true;
entries[index].hasFloat = false;
entries[index].stringValue = value;
if (!save()) return 0;
return entries[index].stringValue.length();
}
size_t putString(const char *key, String value) {
return putString(key, value.c_str());
}
String getString(const char *key, String defaultValue = String()) {
if (!started) return defaultValue;
int index = findKey(key);
if (index < 0 || !entries[index].hasString) return defaultValue;
return entries[index].stringValue;
}
bool isKey(const char *key) {
if (!started) return false;
return findKey(key) >= 0;
}
private:
static const int MAX_ENTRIES = 128;
static const int MAX_KEY_LEN = 15;
static const int MAX_STRING_LEN = 95;
static const uint32_t MAGIC = 0x46504B56; // "VKPF"
static const uint8_t VERSION = 1;
static const uint8_t TYPE_FLOAT = 1;
static const uint8_t TYPE_STRING = 2;
static const int STORAGE_SIZE = 4096;
struct Entry {
bool used = false;
char key[MAX_KEY_LEN + 1] = {};
bool hasFloat = false;
float floatValue = NAN;
bool hasString = false;
String stringValue;
};
bool started = false;
Entry entries[MAX_ENTRIES];
static void writeU16(uint8_t *dst, uint16_t value) {
dst[0] = static_cast<uint8_t>(value & 0xFF);
dst[1] = static_cast<uint8_t>((value >> 8) & 0xFF);
}
static void writeU32(uint8_t *dst, uint32_t value) {
dst[0] = static_cast<uint8_t>(value & 0xFF);
dst[1] = static_cast<uint8_t>((value >> 8) & 0xFF);
dst[2] = static_cast<uint8_t>((value >> 16) & 0xFF);
dst[3] = static_cast<uint8_t>((value >> 24) & 0xFF);
}
static uint16_t readU16(const uint8_t *src) {
return static_cast<uint16_t>(src[0]) |
(static_cast<uint16_t>(src[1]) << 8);
}
static uint32_t readU32(const uint8_t *src) {
return static_cast<uint32_t>(src[0]) |
(static_cast<uint32_t>(src[1]) << 8) |
(static_cast<uint32_t>(src[2]) << 16) |
(static_cast<uint32_t>(src[3]) << 24);
}
int availableStorageSize() {
int n = STORAGE_SIZE;
if (EEPROM.length() > 0 && EEPROM.length() < n) n = EEPROM.length();
return n;
}
bool save() {
const int storageSize = availableStorageSize();
if (storageSize < 16) return false;
uint8_t buffer[STORAGE_SIZE] = {};
int pos = 0;
writeU32(buffer + pos, MAGIC);
pos += 4;
buffer[pos++] = VERSION;
buffer[pos++] = 0;
int lengthPos = pos;
pos += 2;
for (int i = 0; i < MAX_ENTRIES; i++) {
if (!entries[i].used) continue;
if (!entries[i].hasFloat && !entries[i].hasString) continue;
const uint8_t keyLen = static_cast<uint8_t>(strnlen(entries[i].key, MAX_KEY_LEN));
if (keyLen == 0) continue;
if (entries[i].hasFloat) {
const int recordSize = 3 + keyLen + 4;
if (pos + recordSize > storageSize) return false;
buffer[pos++] = TYPE_FLOAT;
buffer[pos++] = keyLen;
buffer[pos++] = 4;
memcpy(buffer + pos, entries[i].key, keyLen);
pos += keyLen;
float value = entries[i].floatValue;
memcpy(buffer + pos, &value, sizeof(value));
pos += sizeof(value);
} else if (entries[i].hasString) {
const uint8_t valueLen = static_cast<uint8_t>(entries[i].stringValue.length());
const int recordSize = 3 + keyLen + valueLen;
if (pos + recordSize > storageSize) return false;
buffer[pos++] = TYPE_STRING;
buffer[pos++] = keyLen;
buffer[pos++] = valueLen;
memcpy(buffer + pos, entries[i].key, keyLen);
pos += keyLen;
if (valueLen > 0) {
memcpy(buffer + pos, entries[i].stringValue.c_str(), valueLen);
pos += valueLen;
}
}
}
writeU16(buffer + lengthPos, static_cast<uint16_t>(pos));
for (int i = 0; i < storageSize; i++) {
EEPROM.write(i, buffer[i]);
}
EEPROM.commit();
return true;
}
void load() {
const int storageSize = availableStorageSize();
if (storageSize < 16) return;
for (auto &entry : entries) entry = Entry();
uint8_t buffer[STORAGE_SIZE] = {};
for (int i = 0; i < storageSize; i++) {
buffer[i] = EEPROM.read(i);
}
int pos = 0;
if (readU32(buffer + pos) != MAGIC) return;
pos += 4;
if (buffer[pos++] != VERSION) return;
pos++; // flags
const uint16_t totalLen = readU16(buffer + pos);
pos += 2;
if (totalLen < pos || totalLen > storageSize) return;
while (pos + 3 <= totalLen) {
const uint8_t type = buffer[pos++];
const uint8_t keyLen = buffer[pos++];
const uint8_t valueLen = buffer[pos++];
if (keyLen == 0 || keyLen > MAX_KEY_LEN) return;
if (pos + keyLen + valueLen > totalLen) return;
char key[MAX_KEY_LEN + 1] = {};
memcpy(key, buffer + pos, keyLen);
key[keyLen] = '\0';
pos += keyLen;
int index = ensureKey(key);
if (index < 0) return;
if (type == TYPE_FLOAT && valueLen == 4) {
float value = NAN;
memcpy(&value, buffer + pos, sizeof(value));
entries[index].hasFloat = true;
entries[index].hasString = false;
entries[index].stringValue = "";
entries[index].floatValue = value;
} else if (type == TYPE_STRING && valueLen <= MAX_STRING_LEN) {
char value[MAX_STRING_LEN + 1] = {};
if (valueLen > 0) memcpy(value, buffer + pos, valueLen);
value[valueLen] = '\0';
entries[index].hasString = true;
entries[index].hasFloat = false;
entries[index].stringValue = value;
}
pos += valueLen;
}
}
int findKey(const char *key) {
if (!key) return -1;
for (int i = 0; i < MAX_ENTRIES; i++) {
if (!entries[i].used) continue;
if (strncmp(entries[i].key, key, MAX_KEY_LEN + 1) == 0) return i;
}
return -1;
}
int ensureKey(const char *key) {
if (!key) return -1;
if (strlen(key) > MAX_KEY_LEN) return -1;
int index = findKey(key);
if (index >= 0) return index;
for (int i = 0; i < MAX_ENTRIES; i++) {
if (entries[i].used) continue;
entries[i].used = true;
entries[i].hasFloat = false;
entries[i].hasString = false;
entries[i].floatValue = NAN;
entries[i].stringValue = "";
strncpy(entries[i].key, key, MAX_KEY_LEN);
entries[i].key[MAX_KEY_LEN] = '\0';
return i;
}
return -1;
}
};
#endif
+7 -8
View File
@@ -27,14 +27,12 @@ void setupRC() {
bool readRC() { bool readRC() {
if (rcRxPin < 0) return false; if (rcRxPin < 0) return false;
if (rc.read()) { if (!rc.read()) return false;
SBUSData data = rc.data();
for (int i = 0; i < 16; i++) channels[i] = data.ch[i]; // copy channels data rc.getChannels(channels);
normalizeRC(); normalizeRC();
controlTime = t; controlTime = t;
return true; return true;
}
return false;
} }
void normalizeRC() { void normalizeRC() {
@@ -55,6 +53,7 @@ void calibrateRC() {
print("RC_RX_PIN = %d, set the RC pin!\n", rcRxPin); print("RC_RX_PIN = %d, set the RC pin!\n", rcRxPin);
return; return;
} }
uint16_t zero[16]; // for zero positions uint16_t zero[16]; // for zero positions
uint16_t center[16]; // for center positions uint16_t center[16]; // for center positions
uint16_t _[16]; // for unused data uint16_t _[16]; // for unused data
+15 -1
View File
@@ -8,10 +8,12 @@ extern float controlRoll, controlPitch, controlThrottle, controlYaw;
float rcLossTimeout = 1; float rcLossTimeout = 1;
float descendTime = 10; float descendTime = 10;
float disarmTilt = radians(120);
void failsafe() { void failsafe() {
rcLossFailsafe(); rcLossFailsafe();
autoFailsafe(); autoFailsafe();
tiltFailsafe();
} }
// RC loss failsafe // RC loss failsafe
@@ -36,7 +38,7 @@ void descend() {
// Allow pilot to interrupt automatic flight // Allow pilot to interrupt automatic flight
void autoFailsafe() { void autoFailsafe() {
static float roll, pitch, yaw, throttle; 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 // controls changed and mode switch is not configured
if (mode == AUTO && invalid(controlMode)) mode = STAB; // regain control by the pilot if (mode == AUTO && invalid(controlMode)) mode = STAB; // regain control by the pilot
} }
@@ -45,3 +47,15 @@ void autoFailsafe() {
yaw = controlYaw; yaw = controlYaw;
throttle = controlThrottle; 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;
}
}
+21 -5
View File
@@ -6,7 +6,11 @@
#pragma once #pragma once
#include <math.h> #include <math.h>
#ifdef ESP32
#include <soc/soc.h>
#include <soc/rtc_cntl_reg.h>
#include <ESP32_NOW_Serial.h> #include <ESP32_NOW_Serial.h>
#endif
const float ONE_G = 9.80665; const float ONE_G = 9.80665;
extern float t; extern float t;
@@ -23,6 +27,12 @@ bool valid(float x) {
return isfinite(x); return isfinite(x);
} }
bool floatEquals(float a, float b, float epsilon = 0) {
if (isnan(a) && isnan(b)) return true;
if (a == b) return true;
return fabsf(a - b) <= epsilon;
}
// Wrap angle to [-PI, PI) // Wrap angle to [-PI, PI)
float wrapAngle(float angle) { float wrapAngle(float angle) {
angle = fmodf(angle, 2 * PI); angle = fmodf(angle, 2 * PI);
@@ -47,24 +57,30 @@ void splitString(String& str, String& token0, String& token1, String& token2) {
if (token2.c_str() == NULL) token2 = ""; if (token2.c_str() == NULL) token2 = "";
} }
// Simplified ESP-NOW Serial without tx buffering and resends #ifdef ESP32
// Simplified ESP-NOW Serial without resends
class ESPNOWSerial : public ESP_NOW_Serial_Class { class ESPNOWSerial : public ESP_NOW_Serial_Class {
public: public:
int lost = 0;
using ESP_NOW_Serial_Class::ESP_NOW_Serial_Class; using ESP_NOW_Serial_Class::ESP_NOW_Serial_Class;
void onSent(bool success) override {} // disable resends void onSent(bool success) override {
size_t write(const uint8_t *data, size_t len) override { if (!success) lost++;
return ESP_NOW_Peer::send(data, len); // pure send without buffering ESP_NOW_Serial_Class::onSent(true); // always report success to avoid resends
} }
}; };
#endif
// Rate limiter // Rate limiter
class Rate { class Rate {
public: public:
float rate; float rate;
float last = 0; float last = -INFINITY;
Rate(float rate) : rate(rate) {} Rate(float rate) : rate(rate) {}
operator bool() { operator bool() {
if (t == last) {
return true; // the same step
}
if (t - last >= 1 / rate) { if (t - last >= 1 / rate) {
last = t; last = t;
return true; return true;
+113 -91
View File
@@ -3,13 +3,13 @@
// Wi-Fi and ESP-NOW communication // Wi-Fi and ESP-NOW communication
#include <WiFi.h> // #include <WiFi.h>
#include <WiFiAP.h> // #include <WiFiAP.h>
#include <WiFiUdp.h> // #include <WiFiUdp.h>
#include <MacAddress.h> // #include <MacAddress.h>
#include <ESP32_NOW_Serial.h> // #include <ESP32_NOW_Serial.h>
#include <Preferences.h> // #include "prefs.h"
#include "util.h" // #include "util.h"
extern Preferences storage; // use the main preferences storage extern Preferences storage; // use the main preferences storage
@@ -17,117 +17,139 @@ const int W_DISABLED = 0, W_AP = 1, W_STA = 2, W_ESPNOW = 3;
int wifiMode = W_AP; int wifiMode = W_AP;
int wifiLongRange = 0; int wifiLongRange = 0;
int wifiBroadcast = 0; // 0 - broadcast until connected, 1 - always broadcast
int udpLocalPort = 14550; int udpLocalPort = 14550;
int udpRemotePort = 14550; int udpRemotePort = 14550;
IPAddress udpRemoteIP = "255.255.255.255"; // IPAddress udpRemoteIP = "255.255.255.255";
WiFiUDP udp; // WiFiUDP udp;
ESPNOWSerial espnow(NULL, 0, WIFI_IF_AP); // ESPNOWSerial espnow(NULL, 0, WIFI_IF_AP);
ESPNOWSerial espnowBroadcast(ESP_NOW.BROADCAST_ADDR, 0, WIFI_IF_AP); // ESPNOWSerial espnowBroadcast(ESP_NOW.BROADCAST_ADDR, 0, WIFI_IF_AP);
int espnowChannel = 6; int espnowChannel = 6;
void setupWiFi() { void setupWiFi() {
print("Setup Wi-Fi\n"); // print("Setup Wi-Fi\n");
WiFi.enableLongRange(wifiLongRange); // WiFi.enableLongRange(wifiLongRange);
if (wifiMode == W_AP) { // if (wifiMode == W_AP) {
WiFi.softAP(storage.getString("WIFI_AP_SSID", "flix").c_str(), storage.getString("WIFI_AP_PASS", "flixwifi").c_str()); // WiFi.softAP(storage.getString("WIFI_AP_SSID", "flix").c_str(), storage.getString("WIFI_AP_PASS", "flixwifi").c_str());
udp.begin(udpLocalPort); // udp.begin(udpLocalPort);
} // }
if (wifiMode == W_STA) { // if (wifiMode == W_STA) {
WiFi.begin(storage.getString("WIFI_STA_SSID", "").c_str(), storage.getString("WIFI_STA_PASS", "").c_str()); // WiFi.begin(storage.getString("WIFI_STA_SSID", "").c_str(), storage.getString("WIFI_STA_PASS", "").c_str());
udp.begin(udpLocalPort); // udp.begin(udpLocalPort);
} // }
if (wifiMode == W_ESPNOW) { // if (wifiMode == W_ESPNOW) {
WiFi.mode(WIFI_AP); // WiFi.mode(WIFI_AP);
WiFi.setChannel(espnowChannel); // WiFi.setChannel(espnowChannel);
espnow.addr(MacAddress(storage.getString("ESPNOW_PEER_MAC", "FF:FF:FF:FF:FF:FF").c_str())); // espnow.addr(MacAddress(storage.getString("ESPNOW_PEER_MAC", "FF:FF:FF:FF:FF:FF").c_str()));
String key = storage.getString("ESPNOW_PEER_KEY", ""); // String key = storage.getString("ESPNOW_PEER_KEY", "");
espnow.setKey(key.isEmpty() ? nullptr : (const uint8_t *)key.c_str()); // espnow.setKey(key.isEmpty() ? nullptr : (const uint8_t *)key.c_str());
espnow.begin(); // espnow.begin();
espnowBroadcast.begin(); // espnowBroadcast.begin();
} // }
WiFi.setSleep(false); // disable power save // WiFi.setSleep(false); // disable power save
} }
void sendWiFi(const uint8_t *buf, int len) { void sendWiFi(const uint8_t *buf, int len) {
if (espnow) { // if (espnow) {
espnow.write(buf, 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
return;
}
if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return; // static Rate discovery(2);
// if (espnow.isEncrypted() && discovery) espnowBroadcast.write((const uint8_t *)"flix", 4); // broadcast message to help finding this device
// return;
// }
udp.beginPacket(udpRemoteIP, udpRemotePort); // if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return;
udp.write(buf, len);
udp.endPacket(); // 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();
} }
int receiveWiFi(uint8_t *buf, int len) { int receiveWiFi(uint8_t *buf, int len) {
if (espnow) { // if (espnow) {
return espnow.read(buf, len); // return espnow.read(buf, len);
} // }
if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return 0; // if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return 0;
udp.parsePacket(); // udp.parsePacket();
if (udp.remoteIP()) udpRemoteIP = udp.remoteIP(); // if (udp.remoteIP()) udpRemoteIP = udp.remoteIP();
return udp.read(buf, len); // return udp.read(buf, len);
return 0;
} }
void printWiFiInfo() { void printWiFiInfo() {
if (espnow) { // if (espnow) {
print("Mode: ESP-NOW\n"); // print("Mode: ESP-NOW\n");
print("ESP-NOW version: %d\n", ESP_NOW.getVersion()); // print("ESP-NOW version: %d\n", ESP_NOW.getVersion());
print("Max packet size: %d\n", ESP_NOW.getMaxDataLen()); // print("Max packet size: %d\n", ESP_NOW.getMaxDataLen());
print("MAC: %s\n", WiFi.softAPmacAddress().c_str()); // print("MAC: %s\n", WiFi.softAPmacAddress().c_str());
print("Peer MAC: %s\n", MacAddress(espnow.addr()).toString().c_str()); // print("Peer MAC: %s\n", MacAddress(espnow.addr()).toString().c_str());
print("Encrypted: %d\n", espnow.isEncrypted()); // print("Encrypted: %d\n", espnow.isEncrypted());
print("Channel: %d\n", espnow.getChannel()); // print("Channel: %d\n", espnow.getChannel());
} else if (WiFi.getMode() == WIFI_MODE_AP) { // print("Lost packets: %d\n", espnow.lost);
print("Mode: Access Point (AP)\n"); // } else if (WiFi.getMode() == WIFI_MODE_AP) {
print("MAC: %s\n", WiFi.softAPmacAddress().c_str()); // print("Mode: Access Point (AP)\n");
print("SSID: %s\n", WiFi.softAPSSID().c_str()); // print("MAC: %s\n", WiFi.softAPmacAddress().c_str());
print("Password: ***\n"); // print("SSID: %s\n", WiFi.softAPSSID().c_str());
print("Channel: %d\n", WiFi.channel()); // print("Password: ***\n");
print("Clients: %d\n", WiFi.softAPgetStationNum()); // print("Channel: %d\n", WiFi.channel());
print("IP: %s\n", WiFi.softAPIP().toString().c_str()); // print("Clients: %d\n", WiFi.softAPgetStationNum());
print("Remote IP: %s\n", udpRemoteIP.toString().c_str()); // print("IP: %s\n", WiFi.softAPIP().toString().c_str());
} else if (WiFi.getMode() == WIFI_MODE_STA) { // print("Remote IP: %s\n", udpRemoteIP.toString().c_str());
print("Mode: Client (STA)\n"); // } else if (WiFi.getMode() == WIFI_MODE_STA) {
print("Connected: %d\n", WiFi.isConnected()); // print("Mode: Client (STA)\n");
print("MAC: %s\n", WiFi.macAddress().c_str()); // print("Connected: %d\n", WiFi.isConnected());
print("SSID: %s\n", WiFi.SSID().c_str()); // print("MAC: %s\n", WiFi.macAddress().c_str());
print("Password: ***\n"); // print("SSID: %s\n", WiFi.SSID().c_str());
print("Channel: %d\n", WiFi.channel()); // print("Password: ***\n");
print("RSSI: %d dBm\n", WiFi.RSSI()); // print("Channel: %d\n", WiFi.channel());
print("IP: %s\n", WiFi.localIP().toString().c_str()); // print("RSSI: %d dBm\n", WiFi.RSSI());
print("Remote IP: %s\n", udpRemoteIP.toString().c_str()); // print("IP: %s\n", WiFi.localIP().toString().c_str());
} else { // print("Remote IP: %s\n", udpRemoteIP.toString().c_str());
print("Mode: Disabled\n"); // } 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) { void configWiFi(int mode, const char *first, const char *second) {
MacAddress mac; // MacAddress mac;
if (mode == W_AP && strlen(first) > 0 && strlen(second) >= 8) { // if (mode == W_AP && strlen(first) > 0 && strlen(second) >= 8) {
storage.putString("WIFI_AP_SSID", first); // storage.putString("WIFI_AP_SSID", first);
storage.putString("WIFI_AP_PASS", second); // storage.putString("WIFI_AP_PASS", second);
} else if (mode == W_STA && strlen(first) > 0 && strlen(second) >= 8) { // } else if (mode == W_STA && strlen(first) > 0 && strlen(second) >= 8) {
storage.putString("WIFI_STA_SSID", first); // storage.putString("WIFI_STA_SSID", first);
storage.putString("WIFI_STA_PASS", second); // storage.putString("WIFI_STA_PASS", second);
} else if (mode == W_ESPNOW && mac.fromString(first)) { // } else if (mode == W_ESPNOW && mac.fromString(first)) {
storage.putString("ESPNOW_PEER_MAC", first); // storage.putString("ESPNOW_PEER_MAC", first);
storage.putString("ESPNOW_PEER_KEY", strlen(second) == ESP_NOW_KEY_LEN ? second : ""); // storage.putString("ESPNOW_PEER_KEY", strlen(second) == ESP_NOW_KEY_LEN ? second : "");
// } else {
// print("Invalid configuration\n");
// return;
// }
// print("✓ Reboot to apply new settings\n");
}
void setWiFiMode(const String& mode) {
if (mode == "ap") {
wifiMode = W_AP;
} else if (mode == "sta") {
wifiMode = W_STA;
} else if (mode == "espnow") {
wifiMode = W_ESPNOW;
} else if (mode == "off") {
wifiMode = W_DISABLED;
} else { } else {
print("Invalid configuration\n"); print("Invalid Wi-Fi mode\n");
return; return;
} }
print("✓ Reboot to apply new settings\n"); static const char *modes[] = {"Disabled", "Access Point (AP)", "Client (STA)", "ESP-NOW"};
print("✓ Wi-Fi mode set to %s, reboot to apply\n", modes[wifiMode]);
} }
+4 -5
View File
@@ -15,12 +15,11 @@ public:
SBUS(HardwareSerial& bus, const int8_t rxpin, const int8_t txpin, const bool inv = true) {}; SBUS(HardwareSerial& bus, const int8_t rxpin, const int8_t txpin, const bool inv = true) {};
void begin(int rxpin = -1, int txpin = -1, bool inv = true, bool fast = false) {}; void begin(int rxpin = -1, int txpin = -1, bool inv = true, bool fast = false) {};
bool read() { return joystickInit(); }; bool read() { return joystickInit(); };
SBUSData data() { void getChannels(uint16_t (&channels)[16]) const {
SBUSData data; int16_t ch[16];
joystickGet(data.ch); joystickGet(ch);
for (int i = 0; i < 16; i++) { for (int i = 0; i < 16; i++) {
data.ch[i] = map(data.ch[i], -32768, 32767, 1000, 2000); // convert to pulse width style channels[i] = map(ch[i], -32768, 32767, 1000, 2000); // convert to pulse width style
} }
return data;
}; };
}; };
+7 -3
View File
@@ -21,6 +21,9 @@ extern float motors[4];
Vector gyro, acc, imuRotation; Vector gyro, acc, imuRotation;
Vector accBias, gyroBias, accScale(1, 1, 1); Vector accBias, gyroBias, accScale(1, 1, 1);
LowPassFilter<Vector> gyroBiasFilter(0); 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;
// declarations // declarations
void step(); void step();
@@ -38,7 +41,7 @@ const char* getModeName();
void sendMotors(); void sendMotors();
int getDutyCycle(float value); int getDutyCycle(float value);
bool motorsActive(); bool motorsActive();
void testMotor(int n); void testMotor(int, float);
void print(const char* format, ...); void print(const char* format, ...);
void pause(float duration); void pause(float duration);
void doCommand(String str, bool echo); void doCommand(String str, bool echo);
@@ -63,19 +66,20 @@ void failsafe();
void rcLossFailsafe(); void rcLossFailsafe();
void descend(); void descend();
void autoFailsafe(); void autoFailsafe();
void tiltFailsafe();
int parametersCount(); int parametersCount();
const char *getParameterName(int index); const char *getParameterName(int index);
float getParameter(int index); float getParameter(int index);
float getParameter(const char *name); float getParameter(const char *name);
bool setParameter(const char *name, const float value); bool setParameter(const char *name, const float value);
void printParameters(); void printParameters(const char *filter);
void resetParameters(); void resetParameters();
// mocks // mocks
void setLED(bool on) {}; void setLED(bool on) {};
void calibrateGyro() { print("Skip gyro calibrating\n"); };
void calibrateAccel() { print("Skip accel calibrating\n"); }; void calibrateAccel() { print("Skip accel calibrating\n"); };
void printIMUCalibration() { print("cal: N/A\n"); }; void printIMUCalibration() { print("cal: N/A\n"); };
void printIMUInfo() {}; void printIMUInfo() {};
void printWiFiInfo() {}; void printWiFiInfo() {};
void configWiFi(bool, const char*, const char*) { print("Skip WiFi config\n"); }; void configWiFi(bool, const char*, const char*) { print("Skip WiFi config\n"); };
void setWiFiMode(const String& mode) { print("Skip WiFi mode set\n"); };
+1
View File
@@ -55,6 +55,7 @@ public:
initNode(); initNode();
Serial.begin(0); Serial.begin(0);
setupParameters(); setupParameters();
rcRxPin = 1; // set rc pin to enable rc reading
gzmsg << "Flix plugin loaded" << endl; gzmsg << "Flix plugin loaded" << endl;
} }
+1 -1
View File
@@ -14,7 +14,7 @@
// Mocks // Mocks
int wifiMode = 1; int wifiMode = 1;
int wifiLongRange = 0; int wifiLongRange = 0;
// int espnowChannel = 6; int espnowChannel = 6;
const int W_DISABLED = 0, W_AP = 1, W_STA = 2, W_ESPNOW = 3; const int W_DISABLED = 0, W_AP = 1, W_STA = 2, W_ESPNOW = 3;
int udpLocalPort = 14580; int udpLocalPort = 14580;
+9
View File
@@ -28,6 +28,8 @@ from pyflix import Flix
flix = Flix() # create a Flix object and wait for connection flix = Flix() # create a Flix object and wait for connection
``` ```
If using ESP-NOW connection, specify the proxy device name in `FLIX_DEVICE` environment variable or pass it to the constructor: `Flix(device='/dev/cu.usbserial-0001')`.
### Telemetry ### Telemetry
Basic telemetry is available through object properties. The property names generally match the corresponding variables in the firmware code: Basic telemetry is available through object properties. The property names generally match the corresponding variables in the firmware code:
@@ -220,6 +222,13 @@ The following scripts demonstrate how to use the library:
* [`log.py`](../log.py) — download flight logs from the drone. * [`log.py`](../log.py) — download flight logs from the drone.
* [`example.py`](../example.py) — a simple example, prints telemetry data and waits for events. * [`example.py`](../example.py) — a simple example, prints telemetry data and waits for events.
> [!TIP]
> Set `FLIX_DEVICE` environment variable to use these tools with ESP-NOW connection, for example:
>
> ```bash
> FLIX_DEVICE=/dev/cu.usbserial-0001 tools/cli.py
> ```
## Advanced usage ## Advanced usage
### MAVLink ### MAVLink
+16 -11
View File
@@ -44,22 +44,27 @@ class Flix:
_print_buffer: str = '' _print_buffer: str = ''
_modes = ['RAW', 'ACRO', 'STAB', 'AUTO'] _modes = ['RAW', 'ACRO', 'STAB', 'AUTO']
def __init__(self, system_id: int=1, wait_connection: bool=True): def __init__(self, system_id: int=1, wait_connection: bool=True, device=os.getenv('FLIX_DEVICE')):
if not (0 <= system_id < 256): if not (0 <= system_id < 256):
raise ValueError('system_id must be in range [0, 255]') raise ValueError('system_id must be in range [0, 255]')
self._setup_mavlink() self._setup_mavlink()
self.system_id = system_id self.system_id = system_id
self._init_state() self._init_state()
try: if device is not None:
# Direct connection # User defined connection
logger.debug('Listening on port 14550') logger.debug(f'Connecting to {device}')
self.connection: mavutil.mavfile = mavutil.mavlink_connection('udpin:0.0.0.0:14550', source_system=255) # type: ignore self.connection: mavutil.mavfile = mavutil.mavlink_connection(device, source_system=255) # type: ignore
except OSError as e: else:
if e.errno != errno.EADDRINUSE: try:
raise # Direct connection
# Port busy - using proxy logger.debug('Listening on port 14550')
logger.debug('Listening on port 14555 (proxy)') self.connection: mavutil.mavfile = mavutil.mavlink_connection('udpin:0.0.0.0:14550', source_system=255) # type: ignore
self.connection: mavutil.mavfile = mavutil.mavlink_connection('udpin:0.0.0.0:14555', source_system=254) # type: ignore except OSError as e:
if e.errno != errno.EADDRINUSE:
raise
# Port busy - using proxy
logger.debug('Listening on port 14555 (proxy)')
self.connection: mavutil.mavfile = mavutil.mavlink_connection('udpin:0.0.0.0:14555', source_system=254) # type: ignore
self.connection.target_system = system_id self.connection.target_system = system_id
self.mavlink: mavlink.MAVLink = self.connection.mav self.mavlink: mavlink.MAVLink = self.connection.mav
self._event_listeners: Dict[str, List[Callable[..., Any]]] = {} self._event_listeners: Dict[str, List[Callable[..., Any]]] = {}
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "pyflix" name = "pyflix"
version = "0.15" version = "0.16"
description = "Python API for Flix drone" description = "Python API for Flix drone"
authors = [{ name="Oleg Kalachev", email="okalachev@gmail.com" }] authors = [{ name="Oleg Kalachev", email="okalachev@gmail.com" }]
license = "MIT" license = "MIT"