13 Commits
Author SHA1 Message Date
Oleg Kalachev 9289b042af Add espnow-proxy build to ci 2026-07-17 23:45:54 +03:00
Oleg Kalachev 1203a3ae8f Make imu model and pins configurable using parameters 2026-07-16 23:09:07 +03:00
Oleg Kalachev d48b71fb1a Disable voltage lpf by default
(Setting alpha = 1 efficiently disables the filter.)
Filtering does more harm than good in voltage monitoring.
2026-07-15 18:42:34 +03:00
Oleg Kalachev 0439407d76 Remove unneeded declaration from the sim 2026-07-15 18:29:36 +03:00
Oleg Kalachev c2005a2ca2 Fix rc in simulator 2026-07-15 17:40:11 +03:00
Oleg Kalachev 6a804862da Fix simulator build with new logging 2026-07-15 14:17:24 +03:00
Oleg Kalachev 590bfe10b0 Minor doc changes 2026-07-15 10:52:52 +03:00
Oleg Kalachev 70af1a1c09 Remove DebugLevel from fqbn in usage article 2026-07-15 10:38:04 +03:00
Oleg Kalachev 9e9dafbdfb Support feed forward rates in mavlink control 2026-07-15 10:11:15 +03:00
Oleg Kalachev 86a4418813 Move debug level definition from fbqn to arduino-cli commaand 2026-07-13 22:05:31 +03:00
Oleg Kalachev 7c53e88963 Add notch filter for the gyro 2026-07-08 16:07:03 +03:00
Oleg Kalachev 28f015569b Add log reset command to cli 2026-07-08 01:07:59 +03:00
Oleg Kalachev 83d1c5c68a Implement entirely new logging subsystem
Make topic-based logging mechanics.
Print any log value to console.
Possibility to expose any logging value to mavlink.
2026-06-30 13:03:43 +03:00
36 changed files with 720 additions and 379 deletions
+15 -30
View File
@@ -10,29 +10,23 @@ 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@v7 - 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 for ESP32 - name: Build firmware
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: Upload binaries - name: Upload binaries
uses: actions/upload-artifact@v7 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: Build espnow-proxy - name: Build espnow-proxy
run: arduino-cli compile --fqbn esp32:esp32:esp32 tools/espnow-proxy run: arduino-cli compile --fqbn esp32:esp32:esp32 tools/espnow-proxy
- name: Check c_cpp_properties.json - name: Check c_cpp_properties.json
@@ -41,7 +35,7 @@ jobs:
build_macos: build_macos:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v4
- name: Install Arduino CLI - name: Install Arduino CLI
run: brew install arduino-cli run: brew install arduino-cli
- name: Build firmware - name: Build firmware
@@ -52,7 +46,7 @@ jobs:
build_windows: build_windows:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v4
- name: Install Arduino CLI - name: Install Arduino CLI
run: choco install arduino-cli run: choco install arduino-cli
- name: Install Make - name: Install Make
@@ -72,8 +66,8 @@ 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
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh uses: arduino/setup-arduino-cli@v1.1.1
- uses: actions/checkout@v7 - uses: actions/checkout@v4
- name: Install Gazebo - name: Install Gazebo
run: | run: |
sudo sh -c 'echo "deb http://packages.osrfoundation.org/gazebo/ubuntu-stable `lsb_release -cs` main" > /etc/apt/sources.list.d/gazebo-stable.list' sudo sh -c 'echo "deb http://packages.osrfoundation.org/gazebo/ubuntu-stable `lsb_release -cs` main" > /etc/apt/sources.list.d/gazebo-stable.list'
@@ -84,16 +78,7 @@ 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 - uses: actions/upload-artifact@v4
env:
GAZEBO_MODEL_PATH: ${{ github.workspace }}/gazebo/models
GAZEBO_PLUGIN_PATH: ${{ github.workspace }}/gazebo/build
run: |
OUT=$(timeout -k 10s 120s gzserver --verbose gazebo/flix.world 2>&1 | tee /dev/stderr)
if echo "$OUT" | grep -Pq "\[Err\](?! \[RenderEngine)"; then
exit 1
fi
- uses: actions/upload-artifact@v7
with: with:
name: gazebo-plugin-binary name: gazebo-plugin-binary
path: gazebo/build/*.so path: gazebo/build/*.so
@@ -105,7 +90,7 @@ jobs:
steps: steps:
- name: Install Arduino CLI - name: Install Arduino CLI
run: brew install arduino-cli run: brew install arduino-cli
- uses: actions/checkout@v7 - uses: actions/checkout@v4
- name: Clean up python binaries # Workaround for https://github.com/actions/setup-python/issues/577 - name: Clean up python binaries # Workaround for https://github.com/actions/setup-python/issues/577
run: | run: |
rm -f /usr/local/bin/2to3* rm -f /usr/local/bin/2to3*
+5 -44
View File
@@ -8,7 +8,6 @@ on:
permissions: permissions:
contents: read contents: read
actions: read
pages: write pages: write
id-token: write id-token: write
@@ -16,7 +15,7 @@ jobs:
markdownlint: markdownlint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v4
- name: Install markdownlint - name: Install markdownlint
run: npm install -g markdownlint-cli2 run: npm install -g markdownlint-cli2
- name: Run markdownlint - name: Run markdownlint
@@ -25,57 +24,19 @@ jobs:
build_book: build_book:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: markdownlint needs: markdownlint
env:
BINARIES: ${{ github.event_name == 'push' && (github.ref_name == 'master' || github.ref_name == 'dev') && github.repository == 'okalachev/flix' }}
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v4
- name: Install mdBook - name: Install mdBook
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
if: ${{ env.BINARIES }}
uses: lewagon/wait-on-check-action@v1.9.1
with:
ref: ${{ github.sha }}
check-name: build_linux
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 30
- name: Find firmware binaries
if: ${{ env.BINARIES }}
id: build_run
run: |
RUN_ID=$(gh api "repos/${{ github.repository }}/actions/workflows/build.yml/runs?head_sha=${{ github.sha }}&per_page=1" --jq '.workflow_runs[0].id')
echo "id=$RUN_ID" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download firmware binaries
if: ${{ env.BINARIES }}
uses: actions/download-artifact@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
run-id: ${{ steps.build_run.outputs.id }}
name: firmware-binary
path: docs/build
- name: Create shortcuts for firmware binaries
if: ${{ env.BINARIES }}
working-directory: docs/build
run: |
for FQBN in esp32.esp32.*; do
zip -r $FQBN.zip $FQBN
BOARD="${FQBN#esp32.esp32.}"
ln -s "$FQBN/flix.ino.merged.bin" "flix.$BOARD.merged.bin"
ln -s "$FQBN/flix.ino.bin" "flix.$BOARD.bin"
ln -s "$FQBN/flix.ino.bootloader.bin" "flix.$BOARD.bootloader.bin"
done
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v5 uses: actions/upload-pages-artifact@v3
with: with:
path: docs/build path: docs/build
deploy: deploy:
if: ${{ github.event_name == 'push' && github.ref_name == 'master' }} if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
concurrency: concurrency:
group: "pages" group: "pages"
cancel-in-progress: true cancel-in-progress: true
@@ -87,4 +48,4 @@ jobs:
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v5 uses: actions/deploy-pages@v4
+4 -24
View File
@@ -10,7 +10,7 @@ jobs:
csv_to_ulog: csv_to_ulog:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v4
- name: Build csv_to_ulog - name: Build csv_to_ulog
run: cd tools/csv_to_ulog && mkdir build && cd build && cmake .. && make run: cd tools/csv_to_ulog && mkdir build && cd build && cmake .. && make
- name: Test csv_to_ulog - name: Test csv_to_ulog
@@ -22,13 +22,13 @@ jobs:
pyflix: pyflix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v4
- name: Install Python build tools - name: Install Python build tools
run: pip install build run: pip install build
- name: Build pyflix - name: Build pyflix
run: python3 -m build tools run: python3 -m build tools
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v4
with: with:
name: pyflix name: pyflix
path: | path: |
@@ -37,7 +37,7 @@ jobs:
python_tools: python_tools:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v4
- name: Install Python dependencies - name: Install Python dependencies
run: pip install -r tools/requirements.txt run: pip install -r tools/requirements.txt
- name: Test csv_to_mcap tool - name: Test csv_to_mcap tool
@@ -46,23 +46,3 @@ jobs:
echo -e "t,x,y,z\n0,1,2,3\n1,4,5,6" > log.csv echo -e "t,x,y,z\n0,1,2,3\n1,4,5,6" > log.csv
./csv_to_mcap.py log.csv ./csv_to_mcap.py log.csv
test $(stat -c %s log.mcap) -eq 883 test $(stat -c %s log.mcap) -eq 883
sloc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: sudo apt-get install -y cloc jq
- name: Print source lines of code
run: cloc --by-file-by-lang flix
- name: Checkout previous revision
uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
path: prev
- name: Annotate total source lines
run: |
SLOC_CURR=$(cloc flix --json | jq -r '.SUM.code')
SLOC_PREV=$(cloc prev/flix --json | jq -r '.SUM.code')
DIFF=$(printf '%+d' "$((SLOC_CURR - SLOC_PREV))")
echo "* Current SLOC: $SLOC_CURR" >> $GITHUB_STEP_SUMMARY
echo "* Previous SLOC: $SLOC_PREV" >> $GITHUB_STEP_SUMMARY
echo "* Diff: $DIFF" >> $GITHUB_STEP_SUMMARY
+5 -5
View File
@@ -1,13 +1,13 @@
BOARD = esp32:esp32:esp32 BOARD = esp32:esp32:d1_mini32
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*))
export ARDUINO_NETWORK_CONNECTION_TIMEOUT := 1h export ARDUINO_NETWORK_CONNECTION_TIMEOUT := 1h
build: .core .libs build: .core .libs
arduino-cli compile flix --fqbn $(BOARD) --build-property "build.core_debug_level=1" $(EXTRA) arduino-cli compile --fqbn $(BOARD) --build-property "build.core_debug_level=1" flix
upload: build upload: build
arduino-cli upload flix --fqbn $(BOARD) -p "$(PORT)" arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" flix
erase: erase:
arduino-cli burn-bootloader --fqbn $(BOARD) -p "$(PORT)" -P esptool arduino-cli burn-bootloader --fqbn $(BOARD) -p "$(PORT)" -P esptool
@@ -27,8 +27,8 @@ libs .libs:
touch .libs touch .libs
upload_proxy: .core .libs upload_proxy: .core .libs
arduino-cli compile tools/espnow-proxy --fqbn $(BOARD) arduino-cli compile --fqbn $(BOARD) tools/espnow-proxy
arduino-cli upload tools/espnow-proxy --fqbn $(BOARD) -p "$(PORT)" arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" tools/espnow-proxy
gazebo/build cmake: gazebo/CMakeLists.txt gazebo/build cmake: gazebo/CMakeLists.txt
mkdir -p gazebo/build mkdir -p gazebo/build
+1 -7
View File
@@ -55,12 +55,6 @@ 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:
@@ -90,7 +84,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)|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| |MOSFET (transistor)|100N03A or [analog](https://t.me/opensourcequadcopter/33)|<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|
+1 -1
View File
@@ -31,7 +31,7 @@
* [`vector.h`](https://github.com/okalachev/flix/blob/master/flix/vector.h), [`quaternion.h`](https://github.com/okalachev/flix/blob/master/flix/quaternion.h) — библиотеки векторов и кватернионов. * [`vector.h`](https://github.com/okalachev/flix/blob/master/flix/vector.h), [`quaternion.h`](https://github.com/okalachev/flix/blob/master/flix/quaternion.h) — библиотеки векторов и кватернионов.
* [`pid.h`](https://github.com/okalachev/flix/blob/master/flix/pid.h) — ПИД-регулятор. * [`pid.h`](https://github.com/okalachev/flix/blob/master/flix/pid.h) — ПИД-регулятор.
* [`filter.h`](https://github.com/okalachev/flix/blob/master/flix/filter.h) — фильтр нижних частот. * [`lpf.h`](https://github.com/okalachev/flix/blob/master/flix/lpf.h) — фильтр нижних частот.
### Подсистема управления ### Подсистема управления
+1 -1
View File
@@ -34,7 +34,7 @@ Utility files:
* [`vector.h`](../flix/vector.h), [`quaternion.h`](../flix/quaternion.h) — vector and quaternion libraries. * [`vector.h`](../flix/vector.h), [`quaternion.h`](../flix/quaternion.h) — vector and quaternion libraries.
* [`pid.h`](../flix/pid.h) — generic PID controller. * [`pid.h`](../flix/pid.h) — generic PID controller.
* [`filter.h`](../flix/filter.h) — generic low-pass filter. * [`lpf.h`](../flix/lpf.h) — generic low-pass filter.
### Control subsystem ### Control subsystem
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

+21 -50
View File
@@ -1,48 +1,20 @@
# Usage: build, setup and flight # Usage: build, setup and flight
To fly Flix quadcopter, you need to upload the firmware to the ESP32 board, and set up the drone for flight. To fly Flix quadcopter, you need to build the firmware, upload it to the ESP32 board, and set up the drone for flight.
## Uploading the firmware To get the firmware sources, clone the repository using git:
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 sources as a ZIP archive](https://github.com/okalachev/flix/archive/refs/heads/master.zip). Beginners can [download the source code as a ZIP archive](https://github.com/okalachev/flix/archive/refs/heads/master.zip).
#### Arduino IDE (Windows, Linux, macOS) ## Building the firmware
You can build and upload the firmware using either **Arduino IDE** (easier for beginners) or **command line**.
### Arduino IDE (Windows, Linux, macOS)
<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">
@@ -53,11 +25,11 @@ Beginners can [download the sources as a ZIP archive](https://github.com/okalach
* `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, *ESP32S3 Dev Module* for ESP32-S3 Super 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) and the port.
7. Set *Tools**Core Debug Level* to *Error* to see the errors in the serial console. Set *Tools**USB CDC on Boot* to *Enabled* for ESP32-S3/ESP32-C3 boards. 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. 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/).
@@ -124,7 +96,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 ⇒ *Analyze Tools* ⇒ *MAVLink Console*. 2. Go to the QGroundControl menu ⇒ *Vehicle Setup* ⇒ *Analyze Tools* ⇒ *MAVLink Console*.
<img src="img/cli.png" width="400"> <img src="img/cli.png" width="400">
@@ -141,14 +113,16 @@ You can also work with parameters using `p` command in the console. Parameter na
### Configure the IMU ### Configure the IMU
1. Configure the following parameters for the IMU: 1. Configure the following parameters for the used 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_MODEL` — IMU model (0 for disabled, 1 for MPU-9250/MPU-6500¹, 2 for ICM-20948, 3 for MPU-6050, 4 for ICM-40609-D).
* `IMU_BUS` — communication bus (0 for SPI, 1 for I²C). * `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_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_SCL`, `IMU_PIN_SDA` — I²C pin numbers.
* `IMU_PIN_INT` — IMU data ready pin number (-1 if not used). * `IMU_PIN_INT` — IMU data ready pin number (-1 if not used).
2. Reboot the drone. 2. Reboot the drone.
3. Check the IMU is working using `imu` command in the console (should print `status: OK`). 3. Check the IMU is working using `imu` command in the console (see details below).
¹ — not to be confused with MPU-6050.
### Define IMU orientation ### Define IMU orientation
@@ -178,9 +152,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).
#### Brushless motors 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.
If using brushless motors with ESCs: If using brushless motors and ESCs:
1. Set the appropriate PWM using the parameters: `MOT_PWM_STOP`, `MOT_PWM_MIN`, and `MOT_PWM_MAX` (1000, 1000, and 2000 is typical). 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).
@@ -188,7 +162,7 @@ If using brushless motors with 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 (optional) ### Battery voltage monitoring
ESP32 ADC can measure only up to 3.3 V, so you need to use a voltage divider to monitor the battery voltage. To enable voltage measurement, set the following parameters: 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:
@@ -363,7 +337,7 @@ To setup ESP-NOW communication:
1. Flash the second ESP32 board with ESP-NOW proxy sketch: [`tools/espnow-proxy/espnow-proxy.ino`](../tools/espnow-proxy/espnow-proxy.ino). Use Arduino IDE or command line: `make upload_proxy`. 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 in Arduino IDE or use `make monitor` command. The ESP32 will print its MAC address and generated encryption key, for example: 2. Open Serial Monitor 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
@@ -382,10 +356,7 @@ 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*, click *Connect*. QGroundControl should connect to the drone using ESP-NOW and begin showing the telemetry. 5. Click *Save*. 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
-18
View File
@@ -4,24 +4,6 @@ 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> 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> 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). Sources and materials: [link](https://drive.google.com/drive/folders/1uWiDcuorLrtVs_IIR7Y13omij-7Q1nx8).
+24 -7
View File
@@ -51,7 +51,13 @@ const char* motd =
"espnow <mac> [<key>] - configure ESP-NOW peer\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"
"mfr/mfl/mrr/mrl [<thrust>] - test motor (remove props)\n" "mfr, mfl, mrr, mrl - test motor (remove props)\n"
"log - show log info\n"
"log header - show log header\n"
"log reset - reset log\n"
"log <name> <rate> - setup log topic rate\n"
"l <str> - show log values starting with str\n"
"l expose <name> - expose log value to telemetry\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";
@@ -153,27 +159,38 @@ void doCommand(String str, bool echo = false) {
} else if (command == "mot") { } else if (command == "mot") {
print("front-right %g front-left %g rear-right %g rear-left %g\n", print("front-right %g front-left %g rear-right %g rear-left %g\n",
motors[MOTOR_FRONT_RIGHT], motors[MOTOR_FRONT_LEFT], motors[MOTOR_REAR_RIGHT], motors[MOTOR_REAR_LEFT]); motors[MOTOR_FRONT_RIGHT], motors[MOTOR_FRONT_LEFT], motors[MOTOR_REAR_RIGHT], motors[MOTOR_REAR_LEFT]);
} else if (command == "log") { } else if (command == "log" && arg0 == "") {
printLogInfo();
} else if (command == "log" && arg1 != "") {
configLogThrottle(arg0.c_str(), arg1.toFloat());
} else if (command == "log" && arg0 == "header") {
printLogHeader(); printLogHeader();
if (arg0 == "dump") printLogData(); } else if (command == "log" && arg0 == "reset") {
resetLog();
} else if (command == "l" && arg0 == "expose" && arg1 != "") {
exposeLogValue(arg1.c_str());
} else if (command == "l") {
printLogValues(arg0.c_str());
} else if (command == "cr") { } else if (command == "cr") {
calibrateRC(); calibrateRC();
} else if (command == "ca") { } else if (command == "ca") {
calibrateAccel(); calibrateAccel();
} else if (command == "mfr") { } else if (command == "mfr") {
testMotor(MOTOR_FRONT_RIGHT, arg0.isEmpty() ? 0.2 : arg0.toFloat()); testMotor(MOTOR_FRONT_RIGHT);
} else if (command == "mfl") { } else if (command == "mfl") {
testMotor(MOTOR_FRONT_LEFT, arg0.isEmpty() ? 0.2 : arg0.toFloat()); testMotor(MOTOR_FRONT_LEFT);
} else if (command == "mrr") { } else if (command == "mrr") {
testMotor(MOTOR_REAR_RIGHT, arg0.isEmpty() ? 0.2 : arg0.toFloat()); testMotor(MOTOR_REAR_RIGHT);
} else if (command == "mrl") { } else if (command == "mrl") {
testMotor(MOTOR_REAR_LEFT, arg0.isEmpty() ? 0.2 : arg0.toFloat()); testMotor(MOTOR_REAR_LEFT);
} 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("Total RAM: %d KB\n", ESP.getHeapSize() / 1024); print("Total RAM: %d KB\n", ESP.getHeapSize() / 1024);
print("Free heap: %d KB\n", ESP.getFreeHeap() / 1024); print("Free heap: %d KB\n", ESP.getFreeHeap() / 1024);
print("PSRAM: %d KB\n", ESP.getPsramSize() / 1024);
print("Free PSRAM: %d KB\n", ESP.getFreePsram() / 1024);
print("Firmware: " __DATE__ " " __TIME__ "\n"); print("Firmware: " __DATE__ " " __TIME__ "\n");
// Print tasks table // Print tasks table
print("Num Task MinSt Prio Core CPU%%\n"); print("Num Task MinSt Prio Core CPU%%\n");
-27
View File
@@ -1,27 +0,0 @@
// 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
}
+33 -8
View File
@@ -9,6 +9,31 @@
#include "filter.h" #include "filter.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;
@@ -19,14 +44,14 @@ Vector ratesExtra; // feedforward rates
Vector torqueTarget; Vector torqueTarget;
float thrustTarget; float thrustTarget;
PID rollRatePID(0.05, 0.2, 0.001, 0.3, 0.2); PID rollRatePID(ROLLRATE_P, ROLLRATE_I, ROLLRATE_D, ROLLRATE_I_LIM, RATES_D_LPF_ALPHA);
PID pitchRatePID(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 yawRatePID(0.3, 0, 0, 0.3); PID yawRatePID(YAWRATE_P, YAWRATE_I, YAWRATE_D);
PID rollPID(6); PID rollPID(ROLL_P, ROLL_I, ROLL_D);
PID pitchPID(6); PID pitchPID(PITCH_P, PITCH_I, PITCH_D);
PID yawPID(3); PID yawPID(YAW_P, 0, 0);
Vector maxRate(radians(360), radians(360), radians(360)); Vector maxRate(ROLLRATE_MAX, PITCHRATE_MAX, YAWRATE_MAX);
float tiltMax = radians(30); float tiltMax = TILT_MAX;
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;
+7
View File
@@ -15,6 +15,12 @@ bool landed;
float accWeight = 0.003; float accWeight = 0.003;
float levelWeight = 0.0002; float levelWeight = 0.0002;
LowPassFilter<Vector> ratesFilter(0.2); // cutoff frequency ~ 40 Hz LowPassFilter<Vector> ratesFilter(0.2); // cutoff frequency ~ 40 Hz
NotchFilter<Vector> ratesNotch(382, 40);
void setupEstimate() {
print("Setup estimation\n");
ratesNotch.reset();
}
void estimate() { void estimate() {
applyGyro(); applyGyro();
@@ -25,6 +31,7 @@ void estimate() {
void applyGyro() { void applyGyro() {
// filter gyro to get angular rates // filter gyro to get angular rates
rates = ratesFilter.update(gyro); rates = ratesFilter.update(gyro);
rates = ratesNotch.update(rates);
// apply rates to attitude // apply rates to attitude
attitude = Quaternion::rotate(attitude, Quaternion::fromRotationVector(rates * dt)); attitude = Quaternion::rotate(attitude, Quaternion::fromRotationVector(rates * dt));
+65 -1
View File
@@ -1,7 +1,7 @@
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com> // Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
// Repository: https://github.com/okalachev/flix // Repository: https://github.com/okalachev/flix
// Low pass filter implementation // Low pass and notch filters
#pragma once #pragma once
@@ -32,3 +32,67 @@ public:
private: private:
bool init = false; bool init = false;
}; };
template <typename T>
class NotchFilter {
public:
float frequency;
float bandwidth;
T output;
NotchFilter(float frequency, float bandwidth): frequency(frequency), bandwidth(bandwidth) {
reset();
};
T update(const T input) {
if (frequency <= 0 || bandwidth <= 0) return input;
if (!init) {
init = true;
x1 = x2 = input;
y1 = y2 = input;
return output = input;
}
output = b0 * input + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
x2 = x1;
x1 = input;
y2 = y1;
y1 = output;
return output;
}
void reset() {
const float dt = 0.001f;
float f = frequency;
float bw = bandwidth;
if (f < 0) f = 0;
if (bw < 1e-6f) bw = 1e-6f;
float q = f / bw;
if (q < 1e-3f) q = 1e-3f;
const float w0 = 2.0f * PI * f * dt;
const float c = cos(w0);
const float s = sin(w0);
const float alpha = s / (2.0f * q);
const float a0 = 1.0f + alpha;
const float invA0 = 1.0f / a0;
b0 = 1.0f * invA0;
b1 = -2.0f * c * invA0;
b2 = 1.0f * invA0;
a1 = -2.0f * c * invA0;
a2 = (1.0f - alpha) * invA0;
init = false;
}
private:
float b0, b1, b2, a1, a2;
T x1, x2, y1, y2;
bool init = false;
};
+3 -1
View File
@@ -26,6 +26,8 @@ void setup() {
setupWiFi(); setupWiFi();
setupIMU(); setupIMU();
setupRC(); setupRC();
setupEstimate();
setupLog();
setLED(false); setLED(false);
print("Initializing complete\n"); print("Initializing complete\n");
} }
@@ -40,6 +42,6 @@ void loop() {
handleInput(); handleInput();
processMavlink(); processMavlink();
readVoltage(); readVoltage();
logData(); loopLog();
syncParameters(); syncParameters();
} }
+25 -31
View File
@@ -10,10 +10,9 @@
#include "filter.h" #include "filter.h"
#include "util.h" #include "util.h"
IMU *imu; IMU imu;
int imuModel = -1; // 1 - MPU9250, 2 - ICM20948, 3 - MPU6050, 4 - ICM40609D int imuModel = 1, imuBus = 0;
int imuBus = 0; // 0 - SPI, 1 - I2C int imuSckPin = SCK, imuMisoPin = MISO, imuMosiPin = MOSI, imuCsPin = -1, imuIntPin = -1;
int imuSckPin = SCK, imuMisoPin = MISO, imuMosiPin = MOSI, imuCsPin = SS, imuIntPin = -1;
int imuSdaPin = SDA, imuSclPin = SCL; 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
@@ -28,35 +27,30 @@ LowPassFilter<Vector> gyroBiasFilter(0.001);
void setupIMU() { void setupIMU() {
print("Setup IMU\n"); print("Setup IMU\n");
free(imu); if (imuBus == 0 && imuCsPin > 0) {
if (imuModel == 3) imuBus = 1; // MPU6050 is I2C only
if (imuBus == 0) {
// SPI connection // SPI connection
SPI.begin(imuSckPin, imuMisoPin, imuMosiPin); SPI.begin(imuSckPin, imuMisoPin, imuMosiPin);
imu = IMU::create(imuModel, SPI, imuCsPin, imuIntPin); imu.begin((IMU::Model)imuModel, SPI, imuCsPin, imuIntPin);
} else { } else if (imuBus == 1) {
// I2C connection // I2C connection
Wire.setPins(imuSdaPin, imuSclPin); Wire.setPins(imuSdaPin, imuSclPin);
imu = IMU::create(imuModel, Wire, imuIntPin); imu.begin((IMU::Model)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
@@ -78,7 +72,7 @@ void calibrateGyroOnce() {
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);
@@ -112,9 +106,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;
@@ -139,18 +133,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("interrupt mode: %s\n", imuIntPin != -1 ? "pin" : "timer");
print("temperature: %.1f °C\n", imu->getTemp()); print("temperature: %.1f °C\n", imu.getTemp());
print("gyro: %f %f %f\n", gyro.x, gyro.y, gyro.z); print("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);
} }
+247 -52
View File
@@ -1,77 +1,272 @@
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com> // Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
// Repository: https://github.com/okalachev/flix // Repository: https://github.com/okalachev/flix
// In-RAM logging // Logging subsystem
#include "vector.h" #include "vector.h"
#include "util.h" #include "util.h"
#define LOG_RATE 100 int logMemory = 0; // 0 - RAM, 1 - PSRAM, -1 - disabled
#define LOG_DURATION 10 float logUsage = 0.5; // fraction of free memory to use for log
#define LOG_SIZE LOG_DURATION * LOG_RATE
Vector attitudeEuler; struct LogValue {
Vector attitudeTargetEuler;
struct LogEntry {
const char *name; const char *name;
float *value; Value value;
float lastValue = NAN;
bool logged = true; // if false, use only for triggering log update
LogValue() : name(nullptr), value() {}; // empty value constructor
template <typename T>
LogValue(const char *name, T value, bool logged = true) : name(name), value(value), logged(logged) {};
}; };
LogEntry logEntries[] = { struct LogTopic {
{"t", &t}, LogValue values[10];
{"rates.x", &rates.x}, int length = 0; // number of logged values
{"rates.y", &rates.y}, float throttle; // max update rate, Hz
{"rates.z", &rates.z}, float lastUpdate = -INFINITY;
{"ratesTarget.x", &ratesTarget.x},
{"ratesTarget.y", &ratesTarget.y}, LogTopic(float throttle, LogValue v0, LogValue v1 = {}, LogValue v2 = {}, LogValue v3 = {}, LogValue v4 = {}, LogValue v5 = {}, LogValue v6 = {}, LogValue v7 = {}, LogValue v8 = {}, LogValue v9 = {}) :
{"ratesTarget.z", &ratesTarget.z}, throttle(throttle), values{v0, v1, v2, v3, v4, v5, v6, v7, v8, v9} {
{"attitude.x", &attitudeEuler.x}, // Count logged values
{"attitude.y", &attitudeEuler.y}, for (auto& v : values) {
{"attitude.z", &attitudeEuler.z}, if (v.name == nullptr) break;
{"attitudeTarget.x", &attitudeTargetEuler.x}, if (v.logged) length++;
{"attitudeTarget.y", &attitudeTargetEuler.y}, }
{"attitudeTarget.z", &attitudeTargetEuler.z}, };
{"thrustTarget", &thrustTarget}
LogTopic(LogValue v0, LogValue v1 = {}, LogValue v2 = {}, LogValue v3 = {}, LogValue v4 = {}, LogValue v5 = {}, LogValue v6 = {}, LogValue v7 = {}, LogValue v8 = {}, LogValue v9 = {}) :
LogTopic(INFINITY, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9) {};
}; };
const int logColumns = sizeof(logEntries) / sizeof(logEntries[0]); LogTopic logTopics[] = {
float logBuffer[LOG_SIZE][logColumns]; // time
LogTopic({"t", &t}), // must be the first topic
LogTopic(1, {"loopRate", &loopRate}),
void prepareLogData() { // imu
attitudeEuler = attitude.toEuler(); LogTopic(
attitudeTargetEuler = attitudeTarget.toEuler(); {"gyro.x", &gyro.x},
} {"gyro.y", &gyro.y},
{"gyro.z", &gyro.z}),
void logData() { LogTopic(50,
if (!armed) return; {"acc.x", &acc.x},
static int logPointer = 0; {"acc.y", &acc.y},
static Rate period(LOG_RATE); {"acc.z", &acc.z}),
if (!period) return;
prepareLogData(); LogTopic(10,
{"gyroBias.x", &gyroBias.x},
{"gyroBias.y", &gyroBias.y},
{"gyroBias.z", &gyroBias.z}),
for (int i = 0; i < logColumns; i++) { // estimation
logBuffer[logPointer][i] = *logEntries[i].value; LogTopic(50,
} {"rates.x", &rates.x},
{"rates.y", &rates.y},
{"rates.z", &rates.z},
{"attitude.roll", []() { return attitude.getRoll(); }},
{"attitude.pitch", []() { return attitude.getPitch(); }},
{"attitude.yaw", []() { return attitude.getYaw(); }}),
logPointer++; // rc
if (logPointer >= LOG_SIZE) { LogTopic(10,
logPointer = 0; {"controlTime", &controlTime, false}, // trigger value
{"controlRoll", &controlRoll},
{"controlPitch", &controlPitch},
{"controlYaw", &controlYaw},
{"controlThrottle", &controlThrottle}),
// control
LogTopic({"armed", &armed}),
LogTopic({"mode", &mode}),
LogTopic(10,
{"ratesTarget.x", &ratesTarget.x},
{"ratesTarget.y", &ratesTarget.y},
{"ratesTarget.z", &ratesTarget.z},
{"attitudeTarget.roll", []() { return attitudeTarget.getRoll(); }},
{"attitudeTarget.pitch", []() { return attitudeTarget.getPitch(); }},
{"attitudeTarget.yaw", []() { return attitudeTarget.getYaw(); }},
{"thrustTarget", &thrustTarget}),
// motors
LogTopic(
{"motors[0]", &motors[0]},
{"motors[1]", &motors[1]},
{"motors[2]", &motors[2]},
{"motors[3]", &motors[3]}),
// misc
LogTopic(5,
{"voltage", &voltage},
{"temp", &temperatureRead},
{"imuTemp", []() { return imu.getTemp(); }}),
};
void *logBuffer; // buffer for log data
size_t logCapacity;
size_t logCursor = 0;
size_t logLength = 0;
LogValue *logExposed = nullptr; // log values exposed to telemetry
void setupLog() {
print("Setup log\n");
free(logBuffer); // when reconfiguring
logBuffer = nullptr;
logCursor = 0;
logLength = 0;
if (logMemory == 0) {
logCapacity = ESP.getFreeHeap() * logUsage;
logBuffer = (uint8_t *)calloc(logCapacity, 1);
} else if (logMemory == 1) {
logCapacity = ESP.getFreePsram() * logUsage;
logBuffer = (uint8_t *)heap_caps_calloc(logCapacity, 1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
} }
} }
void printLogHeader() { void loopLog() {
for (int i = 0; i < logColumns; i++) { if (logBuffer == nullptr || !armed) return;
print("%s%s", logEntries[i].name, i < logColumns - 1 ? "," : "\n");
}
}
void printLogData() { if (!logLength) resetLog(); // reset state on first log write
for (int i = 0; i < LOG_SIZE; i++) {
if (logBuffer[i][0] == 0) continue; // skip empty records static Rate sync(2);
for (int j = 0; j < logColumns; j++) { if (sync) {
print("%g%s", logBuffer[i][j], j < logColumns - 1 ? "," : "\n"); const uint8_t marker[] = {0x1A, 0x91, 0x4F, 0xF6, 0x7F};
writeLog(&marker, sizeof(marker)); // write sync marker
}
for (uint8_t i = 0; i < sizeof(logTopics) / sizeof(logTopics[0]); i++) {
LogTopic& topic = logTopics[i];
if (t - topic.lastUpdate < 1 / topic.throttle) continue; // throttle topic
if (!isTopicUpdated(i)) continue; // skip if topic was't updated
topic.lastUpdate = t;
writeLog(&i, sizeof(i)); // write topic index
for (auto& value : topic.values) {
if (value.name == nullptr) break;
if (!value.logged) continue;
value.lastValue = value.value.get();
writeLog(&value.lastValue, sizeof(float)); // write value
} }
} }
} }
void resetLog() {
for (auto& topic : logTopics) {
topic.lastUpdate = -INFINITY;
for (auto& value : topic.values) {
value.lastValue = NAN;
}
}
logCursor = 0;
logLength = 0;
}
void writeLog(const void *data, size_t size) {
size_t first = min(size, logCapacity - logCursor);
size_t second = size - first;
memcpy(logBuffer + logCursor, data, first);
logCursor = (logCursor + first) % logCapacity;
if (second > 0) {
memcpy(logBuffer + logCursor, data + first, second);
logCursor = (logCursor + second) % logCapacity;
}
logLength = min(logLength + size, logCapacity);
}
void readLog(void *data, size_t position, size_t size) {
if (logLength == logCapacity) {
position = (logCursor + position) % logCapacity;
}
size_t first = min(size, logCapacity - position);
size_t second = size - first;
memcpy(data, logBuffer + position, first);
if (second > 0) {
memcpy(data + first, logBuffer, second);
}
}
bool isTopicUpdated(const uint8_t topic) {
LogTopic& logTopic = logTopics[topic];
bool updated = false;
for (auto& value : logTopic.values) {
if (value.name == nullptr) break;
float v = value.value.get();
if (!floatEquals(value.lastValue, v)) {
value.lastValue = v;
updated = true;
}
}
return updated;
}
void printLogInfo() {
if (logMemory == -1) return print("Log: disabled\n");
print("Memory: %s\n", logMemory == 0 ? "RAM" : "PSRAM");
print("Usage: %.f%%\n", logUsage * 100);
print("Capacity: %u bytes\n", (unsigned)logCapacity);
print("Used: %u bytes\n", (unsigned)logLength);
print("Estimated duration: %d seconds\n", estimateLogDuration());
}
int estimateLogDuration() {
float bandwidth = 0;
for (LogTopic& topic : logTopics) {
float rate = isinf(topic.throttle) ? loopRate : topic.throttle;
bandwidth += rate * topic.length * sizeof(float);
}
return logCapacity / bandwidth;
}
void printLogHeader() {
int i = 0;
for (auto& topic : logTopics) {
print("Topic #%d (%g Hz):\n", i++, topic.throttle);
for (auto& value : topic.values) {
if (value.name == nullptr) break;
print(" %s%s\n", value.name, value.logged ?"" : " (not logged)");
}
}
}
void printLogValues(const char *filter) {
for (LogTopic& topic : logTopics) {
for (LogValue& value : topic.values) {
if (value.name == nullptr) break;
if (strncasecmp(value.name, filter, strlen(filter))) continue;
print("%s = %g\n", value.name, value.value.get());
}
}
}
void configLogThrottle(const char *name, float throttle) {
for (LogTopic& topic : logTopics) {
for (LogValue& value : topic.values) {
if (value.name == nullptr) break;
if (strcasecmp(value.name, name) != 0) continue;
topic.throttle = throttle;
print("Log throttle for %s set to %.1f Hz\n", name, throttle);
return;
}
}
print("Log value not found: %s\n", name);
}
void exposeLogValue(const char *name) {
for (int i = 0; i < sizeof(logTopics) / sizeof(logTopics[0]); i++) {
LogTopic& topic = logTopics[i];
for (LogValue& value : topic.values) {
if (value.name == nullptr) break;
if (strcasecmp(value.name, name) != 0) continue;
logExposed = &value;
print("Log value %s exposed\n", name);
return;
}
}
print("Log value not found: %s\n", name);
}
+64 -20
View File
@@ -16,8 +16,9 @@ Rate telemetryAttitude(20);
Rate telemetryRC(10); Rate telemetryRC(10);
Rate telemetryMotors(10); Rate telemetryMotors(10);
Rate telemetryIMU(15); Rate telemetryIMU(15);
Rate telemetryTopic(10);
float mavlinkTime = NAN; // time of last received message bool mavlinkConnected = false;
String mavlinkPrintBuffer; String mavlinkPrintBuffer;
void processMavlink() { void processMavlink() {
@@ -40,7 +41,7 @@ void sendMavlink() {
sendMessage(&msg); sendMessage(&msg);
} }
if (!valid(mavlinkTime)) return; // send only heartbeat until connected if (!mavlinkConnected) return; // send only heartbeat until connected
if (telemetrySlow) { 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,
@@ -84,6 +85,12 @@ void sendMavlink() {
0, 0, 0, 0); 0, 0, 0, 0);
sendMessage(&msg); sendMessage(&msg);
} }
if (telemetryTopic && logExposed != nullptr) {
mavlink_msg_named_value_float_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg,
time, logExposed->name, logExposed->value.get());
sendMessage(&msg);
}
} }
void sendMessage(const void *msg) { void sendMessage(const void *msg) {
@@ -92,6 +99,26 @@ void sendMessage(const void *msg) {
sendWiFi(buf, len); sendWiFi(buf, len);
} }
static uint8_t mavlinkBatch[ESP_NOW_MAX_DATA_LEN_V2];
static int mavlinkBatchSize = 0;
void batchMessage(const void *msg) {
uint8_t buf[MAVLINK_MAX_PACKET_LEN];
int len = mavlink_msg_to_send_buffer(buf, (mavlink_message_t *)msg);
if (mavlinkBatchSize + len > sizeof(mavlinkBatch)) {
sendWiFi(mavlinkBatch, mavlinkBatchSize);
mavlinkBatchSize = 0;
}
memcpy(mavlinkBatch + mavlinkBatchSize, buf, len);
mavlinkBatchSize += len;
}
void flushBatchMessages() {
sendWiFi(mavlinkBatch, mavlinkBatchSize);
mavlinkBatchSize = 0;
}
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);
@@ -101,7 +128,7 @@ void receiveMavlink() {
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; mavlinkConnected = true;
handleMavlink(&msg); handleMavlink(&msg);
} }
} }
@@ -196,18 +223,24 @@ void handleMavlink(const void *_msg) {
mavlink_msg_set_attitude_target_decode(&msg, &m); mavlink_msg_set_attitude_target_decode(&msg, &m);
if (m.target_system && m.target_system != mavlinkSysId) return; if (m.target_system && m.target_system != mavlinkSysId) return;
// copy attitude, rates and thrust targets if (!(m.type_mask & ATTITUDE_TARGET_TYPEMASK_ATTITUDE_IGNORE)) {
ratesTarget.x = m.body_roll_rate; // Attitude control
ratesTarget.y = -m.body_pitch_rate; // convert to flu attitudeTarget.w = m.q[0];
ratesTarget.z = -m.body_yaw_rate; attitudeTarget.x = m.q[1];
attitudeTarget.w = m.q[0]; attitudeTarget.y = -m.q[2];
attitudeTarget.x = m.q[1]; attitudeTarget.z = -m.q[3];
attitudeTarget.y = -m.q[2]; ratesExtra.x = m.type_mask & ATTITUDE_TARGET_TYPEMASK_BODY_ROLL_RATE_IGNORE ? 0 : m.body_roll_rate;
attitudeTarget.z = -m.q[3]; ratesExtra.y = m.type_mask & ATTITUDE_TARGET_TYPEMASK_BODY_PITCH_RATE_IGNORE ? 0 : -m.body_pitch_rate; // convert to flu
thrustTarget = m.thrust; ratesExtra.z = m.type_mask & ATTITUDE_TARGET_TYPEMASK_BODY_YAW_RATE_IGNORE ? 0 : -m.body_yaw_rate;
ratesExtra = Vector(0, 0, 0); } else {
// Rates control
attitudeTarget.invalidate();
ratesTarget.x = m.type_mask & ATTITUDE_TARGET_TYPEMASK_BODY_ROLL_RATE_IGNORE ? ratesTarget.x : m.body_roll_rate;
ratesTarget.y = m.type_mask & ATTITUDE_TARGET_TYPEMASK_BODY_PITCH_RATE_IGNORE ? ratesTarget.y : -m.body_pitch_rate;
ratesTarget.z = m.type_mask & ATTITUDE_TARGET_TYPEMASK_BODY_YAW_RATE_IGNORE ? ratesTarget.z : -m.body_yaw_rate;
}
if (m.type_mask & ATTITUDE_TARGET_TYPEMASK_ATTITUDE_IGNORE) attitudeTarget.invalidate(); thrustTarget = valid(m.thrust) ? m.thrust : thrustTarget;
armed = m.thrust > 0; armed = m.thrust > 0;
} }
@@ -225,18 +258,29 @@ void handleMavlink(const void *_msg) {
armed = motors[0] > 0 || motors[1] > 0 || motors[2] > 0 || motors[3] > 0; armed = motors[0] > 0 || motors[1] > 0 || motors[2] > 0 || motors[3] > 0;
} }
if (msg.msgid == MAVLINK_MSG_ID_LOG_REQUEST_LIST) {
const uint32_t qgcEpoch = 1262304000; // qgc accepts only timestamps after 2010-01-01
mavlink_message_t response;
mavlink_msg_log_entry_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &response,
0, 1, 0, qgcEpoch + t * 60, logLength); // put fake unique date to make qgc happy with saving logs
sendMessage(&response);
}
if (msg.msgid == MAVLINK_MSG_ID_LOG_REQUEST_DATA) { if (msg.msgid == MAVLINK_MSG_ID_LOG_REQUEST_DATA) {
mavlink_log_request_data_t m; mavlink_log_request_data_t m;
mavlink_msg_log_request_data_decode(&msg, &m); mavlink_msg_log_request_data_decode(&msg, &m);
if (m.target_system && m.target_system != mavlinkSysId) return; if (m.target_system && m.target_system != mavlinkSysId) return;
// Send all log records for (int i = 0; i < m.count; i += MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN) {
for (int i = 0; i < sizeof(logBuffer) / sizeof(logBuffer[0]); i++) { int chunkSize = min(MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN, (int)(m.count - i));
mavlink_message_t msg; mavlink_message_t response;
mavlink_msg_log_data_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, 0, i, uint8_t data[MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN];
sizeof(logBuffer[0]), (uint8_t *)logBuffer[i]); readLog(data, m.ofs + i, chunkSize);
sendMessage(&msg); mavlink_msg_log_data_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &response,
m.id, m.ofs + i, chunkSize, data);
batchMessage(&response);
} }
flushBatchMessages();
} }
// Handle commands // Handle commands
+2 -2
View File
@@ -51,9 +51,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, float thrust) { void testMotor(int n) {
print("Testing motor %d\n", n); print("Testing motor %d\n", n);
motors[n] = thrust; motors[n] = 0.2;
delay(50); // ESP32 may need to wait until the end of the current cycle to change duty https://github.com/espressif/arduino-esp32/issues/5306 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);
+24 -14
View File
@@ -9,20 +9,18 @@
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, wifiBroadcast, udpLocalPort, udpRemotePort, espnowChannel; extern int wifiMode, wifiLongRange, udpLocalPort, udpRemotePort, espnowChannel;
extern float rcLossTimeout, descendTime, disarmTilt; extern float rcLossTimeout, descendTime;
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 initial; // default value 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) {};
@@ -46,7 +44,6 @@ 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},
@@ -69,8 +66,6 @@ Parameter parameters[] = {
{"IMU_PIN_MISO", &imuMisoPin}, {"IMU_PIN_MISO", &imuMisoPin},
{"IMU_PIN_MOSI", &imuMosiPin}, {"IMU_PIN_MOSI", &imuMosiPin},
{"IMU_PIN_CS", &imuCsPin}, {"IMU_PIN_CS", &imuCsPin},
{"IMU_PIN_SDA", &imuSdaPin},
{"IMU_PIN_SCL", &imuSclPin},
{"IMU_PIN_INT", &imuIntPin}, {"IMU_PIN_INT", &imuIntPin},
{"IMU_ROT_ROLL", &imuRotation.x}, {"IMU_ROT_ROLL", &imuRotation.x},
{"IMU_ROT_PITCH", &imuRotation.y}, {"IMU_ROT_PITCH", &imuRotation.y},
@@ -86,6 +81,8 @@ Parameter parameters[] = {
{"EST_ACC_WEIGHT", &accWeight}, {"EST_ACC_WEIGHT", &accWeight},
{"EST_LVL_WEIGHT", &levelWeight}, {"EST_LVL_WEIGHT", &levelWeight},
{"EST_RATES_LPF_A", &ratesFilter.alpha}, {"EST_RATES_LPF_A", &ratesFilter.alpha},
{"EST_RATES_NF_F", &ratesNotch.frequency, setupEstimate},
{"EST_RATES_NF_BW", &ratesNotch.bandwidth, setupEstimate},
// motors // motors
{"MOT_PIN_FL", &motorPins[MOTOR_FRONT_LEFT], setupMotors}, {"MOT_PIN_FL", &motorPins[MOTOR_FRONT_LEFT], setupMotors},
{"MOT_PIN_FR", &motorPins[MOTOR_FRONT_RIGHT], setupMotors}, {"MOT_PIN_FR", &motorPins[MOTOR_FRONT_RIGHT], setupMotors},
@@ -124,7 +121,6 @@ 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
@@ -134,6 +130,22 @@ Parameter parameters[] = {
{"MAV_RATE_RC", &telemetryRC.rate}, {"MAV_RATE_RC", &telemetryRC.rate},
{"MAV_RATE_MOT", &telemetryMotors.rate}, {"MAV_RATE_MOT", &telemetryMotors.rate},
{"MAV_RATE_IMU", &telemetryIMU.rate}, {"MAV_RATE_IMU", &telemetryIMU.rate},
{"MAV_RATE_TOPIC", &telemetryTopic.rate},
// log
{"LOG_MEMORY", &logMemory, setupLog},
{"LOG_USAGE", &logUsage, setupLog},
{"LOG_RATE_000", &logTopics[0].throttle},
{"LOG_RATE_001", &logTopics[1].throttle},
{"LOG_RATE_002", &logTopics[2].throttle},
{"LOG_RATE_003", &logTopics[3].throttle},
{"LOG_RATE_004", &logTopics[4].throttle},
{"LOG_RATE_005", &logTopics[5].throttle},
{"LOG_RATE_006", &logTopics[6].throttle},
{"LOG_RATE_007", &logTopics[7].throttle},
{"LOG_RATE_008", &logTopics[8].throttle},
{"LOG_RATE_009", &logTopics[9].throttle},
{"LOG_RATE_010", &logTopics[10].throttle},
{"LOG_RATE_011", &logTopics[11].throttle},
// power // power
{"PWR_VOLT_PIN", &voltagePin, setupPower}, {"PWR_VOLT_PIN", &voltagePin, setupPower},
{"PWR_VOLT_SCALE", &voltageScale}, {"PWR_VOLT_SCALE", &voltageScale},
@@ -141,16 +153,14 @@ 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) {
parameter.initial = parameter.getValue(); parameter.inital = parameter.getValue();
if (storage.isKey(parameter.name)) { if (storage.isKey(parameter.name)) {
parameter.setValue(storage.getFloat(parameter.name)); parameter.setValue(storage.getFloat(parameter.name));
} }
@@ -211,10 +221,10 @@ void printParameters(const char *filter) {
for (auto &parameter : parameters) { for (auto &parameter : parameters) {
if (strncasecmp(parameter.name, filter, strlen(filter))) continue; if (strncasecmp(parameter.name, filter, strlen(filter))) continue;
if (floatEquals(parameter.getValue(), parameter.initial)) { // parameter changed if (floatEquals(parameter.getValue(), parameter.inital)) { // parameter changed
print("%-15s %-13g\n", parameter.name, parameter.getValue()); print("%-15s %-13g\n", parameter.name, parameter.getValue());
} else { } else {
print("%-15s %-13g [%g]\n", parameter.name, parameter.getValue(), parameter.initial); 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 = 0, float d = 0, float windup = INFINITY, float dAlpha = 1, float dtMax = 0.1) : PID(float p, float i, float d, 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) {
+1 -15
View File
@@ -8,12 +8,10 @@ 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
@@ -38,7 +36,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 (abs(roll - controlRoll) > 0.05 || abs(pitch - controlPitch) > 0.05 || abs(yaw - controlYaw) > 0.05 || abs(throttle - controlThrottle) > 0.05) { if (roll != controlRoll || pitch != controlPitch || yaw != controlYaw || 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
} }
@@ -47,15 +45,3 @@ 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;
}
}
+44 -1
View File
@@ -64,11 +64,54 @@ public:
} }
}; };
// Simple variant type for logging and parameters
struct Value {
enum { EMPTY, FLOAT, INT, BOOL, FLOAT_FN, INT_FN, BOOL_FN } type;
union {
void *pointer;
float *_float;
int *_int;
bool *_bool;
float (*floatFn)();
int (*intFn)();
bool (*boolFn)();
};
Value() : type(EMPTY), pointer(nullptr) {};
Value(float *pt) : type(FLOAT), _float(pt) {};
Value(int *pt) : type(INT), _int(pt) {};
Value(bool *pt) : type(BOOL), _bool(pt) {};
Value(float (*fn)()) : type(FLOAT_FN), floatFn(fn) {};
Value(int (*fn)()) : type(INT_FN), intFn(fn) {};
Value(bool (*fn)()) : type(BOOL_FN), boolFn(fn) {};
float get() const {
switch (type) {
case FLOAT: return *_float;
case INT: return *_int;
case BOOL: return *_bool ? 1 : 0;
case FLOAT_FN: return floatFn();
case INT_FN: return intFn();
case BOOL_FN: return boolFn() ? 1 : 0;
default: return NAN;
}
};
void set(float value) const {
switch (type) {
case FLOAT: *_float = value; break;
case INT: *_int = value; break;
case BOOL: *_bool = (value != 0); break;
default: break;
}
};
};
// Rate limiter // Rate limiter
class Rate { class Rate {
public: public:
float rate; float rate;
float last = -INFINITY; float last = 0;
Rate(float rate) : rate(rate) {} Rate(float rate) : rate(rate) {}
operator bool() { operator bool() {
+3 -5
View File
@@ -17,7 +17,6 @@ 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";
@@ -59,14 +58,13 @@ void sendWiFi(const uint8_t *buf, int len) {
espnow.write(buf, len); espnow.write(buf, len);
static Rate discovery(2); static Rate discovery(2);
if (espnow.isEncrypted() && discovery) espnowBroadcast.write((const uint8_t *)"flix", 4); // broadcast message to help finding this device if (discovery) espnowBroadcast.write((const uint8_t *)"flix", 4); // broadcast message to help finding this device
return; return;
} }
if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return; if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return;
bool broadcast = wifiBroadcast || !(t - mavlinkTime < 5); // broadcast if lost connection udp.beginPacket(udpRemoteIP, udpRemotePort);
udp.beginPacket(broadcast ? IPAddress(255, 255, 255, 255) : udpRemoteIP, udpRemotePort);
udp.write(buf, len); udp.write(buf, len);
udp.endPacket(); udp.endPacket();
} }
@@ -115,7 +113,7 @@ void printWiFiInfo() {
} else { } else {
print("Mode: Disabled\n"); print("Mode: Disabled\n");
} }
print("MAVLink connected: %d\n", valid(mavlinkTime)); print("MAVLink connected: %d\n", mavlinkConnected);
} }
void configWiFi(int mode, const char *first, const char *second) { void configWiFi(int mode, const char *first, const char *second) {
+11
View File
@@ -20,6 +20,10 @@
#define radians(deg) ((deg)*DEG_TO_RAD) #define radians(deg) ((deg)*DEG_TO_RAD)
#define degrees(rad) ((rad)*RAD_TO_DEG) #define degrees(rad) ((rad)*RAD_TO_DEG)
#define MALLOC_CAP_SPIRAM (1<<10)
#define MALLOC_CAP_8BIT (1<<2)
#define ESP_NOW_MAX_DATA_LEN_V2 1470
#define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt))) #define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
template<typename T> T max(T a, T b) { return a > b ? a : b; } template<typename T> T max(T a, T b) { return a > b ? a : b; }
template<typename T> T min(T a, T b) { return a < b ? a : b; } template<typename T> T min(T a, T b) { return a < b ? a : b; }
@@ -156,6 +160,8 @@ HardwareSerial Serial, Serial1, Serial2;
class EspClass { class EspClass {
public: public:
void restart() { Serial.println("Ignore reboot in simulation"); } void restart() { Serial.println("Ignore reboot in simulation"); }
uint32_t getFreeHeap() { return 300 * 1024; } // assume 300 KB free heap
uint32_t getFreePsram() { return 8 * 1024 * 1024; } // assume 8 MB free PSRAM
} ESP; } ESP;
unsigned long __delayTime = 0; unsigned long __delayTime = 0;
@@ -165,11 +171,16 @@ void delay(uint32_t ms) {
__delayTime += ms * 1000; __delayTime += ms * 1000;
} }
void *heap_caps_calloc(size_t n, size_t size, uint32_t caps) {
return calloc(n, size);
}
bool ledcAttach(uint8_t pin, uint32_t freq, uint8_t resolution) { return true; } bool ledcAttach(uint8_t pin, uint32_t freq, uint8_t resolution) { return true; }
bool ledcWrite(uint8_t pin, uint32_t duty) { return true; } bool ledcWrite(uint8_t pin, uint32_t duty) { return true; }
uint32_t ledcChangeFrequency(uint8_t pin, uint32_t freq, uint8_t resolution) { return freq; } uint32_t ledcChangeFrequency(uint8_t pin, uint32_t freq, uint8_t resolution) { return freq; }
int8_t digitalPinToAnalogChannel(uint8_t pin) { return -1; } int8_t digitalPinToAnalogChannel(uint8_t pin) { return -1; }
uint32_t analogReadMilliVolts(uint8_t pin) { return 0; } uint32_t analogReadMilliVolts(uint8_t pin) { return 0; }
float temperatureRead() { return 0; }
unsigned long __micros; unsigned long __micros;
unsigned long __resetTime = 0; unsigned long __resetTime = 0;
+17 -3
View File
@@ -24,6 +24,7 @@ LowPassFilter<Vector> gyroBiasFilter(0);
int imuModel = 1, imuBus = 0; int imuModel = 1, imuBus = 0;
int imuSckPin = 0, imuMisoPin = 0, imuMosiPin = 0, imuCsPin = -1, imuIntPin = -1; int imuSckPin = 0, imuMisoPin = 0, imuMosiPin = 0, imuCsPin = -1, imuIntPin = -1;
int imuSdaPin = 0, imuSclPin = 0; int imuSdaPin = 0, imuSclPin = 0;
int rgbPin = -1, rgbNum = 1;
// declarations // declarations
void step(); void step();
@@ -41,7 +42,7 @@ const char* getModeName();
void sendMotors(); void sendMotors();
int getDutyCycle(float value); int getDutyCycle(float value);
bool motorsActive(); bool motorsActive();
void testMotor(int, float); void testMotor(int n);
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);
@@ -51,8 +52,17 @@ void normalizeRC();
void calibrateRC(); void calibrateRC();
void calibrateRCChannel(int*, uint16_t[16], uint16_t[16], const char*); void calibrateRCChannel(int*, uint16_t[16], uint16_t[16], const char*);
void printRCCalibration(); void printRCCalibration();
void loopLog();
void resetLog();
void writeLog(const void *data, size_t size);
void readLog(void *data, size_t position, size_t size);
bool isTopicUpdated(const uint8_t topic);
void printLogInfo();
int estimateLogDuration();
void printLogHeader(); void printLogHeader();
void printLogData(); void printLogValues(const char *filter);
void configLogThrottle(const char *name, float throttle);
void exposeLogValue(const char *name);
void processMavlink(); void processMavlink();
void sendMavlink(); void sendMavlink();
void sendMessage(const void *msg); void sendMessage(const void *msg);
@@ -66,7 +76,6 @@ 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);
@@ -83,3 +92,8 @@ 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"); }; void setWiFiMode(const String& mode) { print("Skip WiFi mode set\n"); };
class IMU {
public:
float getTemp() { return 0; }
} imu;
+2 -1
View File
@@ -55,6 +55,7 @@ public:
initNode(); initNode();
Serial.begin(0); Serial.begin(0);
setupParameters(); setupParameters();
setupLog();
rcRxPin = 1; // set rc pin to enable rc reading rcRxPin = 1; // set rc pin to enable rc reading
gzmsg << "Flix plugin loaded" << endl; gzmsg << "Flix plugin loaded" << endl;
} }
@@ -88,7 +89,7 @@ public:
applyMotorForces(); applyMotorForces();
publishTopics(); publishTopics();
logData(); loopLog();
syncParameters(); syncParameters();
} }
-1
View File
@@ -14,7 +14,6 @@
// Mocks // Mocks
int wifiMode = 1; int wifiMode = 1;
int wifiLongRange = 0; int wifiLongRange = 0;
int wifiBroadcast = 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;
+10 -1
View File
@@ -9,6 +9,7 @@ Usage:
import csv import csv
import json import json
import docopt import docopt
import math
from mcap.writer import Writer from mcap.writer import Writer
args = docopt.docopt(__doc__) args = docopt.docopt(__doc__)
@@ -39,7 +40,15 @@ channel_id = writer.register_channel(
) )
for row in csv_reader: for row in csv_reader:
data = {key: float(value) for key, value in zip(header, row)} if row[0] == '': continue
data = {}
for key, value in zip(header, row):
if value == '' or math.isnan(float(value)):
data[key] = None
else:
data[key] = float(value)
data = {key: float(value) if value != '' else None for key, value in zip(header, row)}
timestamp = round(float(row[0]) * 1e9) timestamp = round(float(row[0]) * 1e9)
writer.add_message(channel_id=channel_id, log_time=timestamp, data=json.dumps(data).encode(), publish_time=timestamp,) writer.add_message(channel_id=channel_id, log_time=timestamp, data=json.dumps(data).encode(), publish_time=timestamp,)
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Convert flight from Flix format to CSV
Usage:
log_to_csv.py <input_file>
"""
import os
from pyflix import Flix
import docopt
import struct
import csv
DIR = os.path.dirname(os.path.realpath(__file__))
HEADER_FILE = os.path.join(DIR, 'log/log_header.txt')
# Read log header
try:
# read from file
header = open(HEADER_FILE, 'r').read()
except FileNotFoundError:
flix = Flix()
header = flix.cli('log header') # receive the log schema
open(HEADER_FILE, 'w').write(header) # save to file
# Parse log header
topics = []
for line in header.splitlines():
if not line.startswith(' '):
topics.append([])
elif 'not logged' not in line:
topics[-1].append(line.strip())
# Read log file
args = docopt.docopt(__doc__)
input_file = args['<input_file>']
outfile_file = input_file + '.csv'
data = open(input_file, 'rb').read()
# Search for sync marker
SYNC_MARKER = bytes.fromhex('1A 91 4F F6 7F')
sync_offset = data.find(SYNC_MARKER)
if sync_offset == -1:
raise ValueError('Sync marker not found in log file')
data = data[sync_offset + len(SYNC_MARKER):]
data = data.replace(SYNC_MARKER, b'') # remove all other sync markers
header_row = [f'{value}' for topic in topics for value in topic]
rows = []
offset = 0
while offset < len(data):
try:
topic = struct.unpack_from('B', data, offset)[0]
offset += 1
if topic >= len(topics):
raise ValueError(f'Invalid topic {topic} at offset {offset}')
if topic == 0 or not rows:
rows.append({})
for name in topics[topic]:
value = struct.unpack_from('<f', data, offset)[0]
offset += 4
rows[-1][name] = value
except struct.error as e:
break
# Write CSV file
with open(outfile_file, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=header_row, extrasaction='ignore')
writer.writeheader()
rows = filter(lambda row: float(row.get('t', 0)), rows)
writer.writerows(rows)
+8 -8
View File
@@ -243,7 +243,7 @@ class Flix:
time.sleep(1) time.sleep(1)
@staticmethod @staticmethod
def _mavlink_to_flu(v: Sequence[float]) -> List[float]: def _mavlink_to_flu(v: List[float]) -> List[float]:
if len(v) == 3: # vector if len(v) == 3: # vector
return [v[0], -v[1], -v[2]] return [v[0], -v[1], -v[2]]
elif len(v) == 4: # quaternion elif len(v) == 4: # quaternion
@@ -252,8 +252,8 @@ class Flix:
raise ValueError(f'List must have 3 (vector) or 4 (quaternion) elements') raise ValueError(f'List must have 3 (vector) or 4 (quaternion) elements')
@staticmethod @staticmethod
def _flu_to_mavlink(v: Sequence[float]) -> List[float]: def _flu_to_mavlink(v: List[float]) -> List[float]:
return Flix._mavlink_to_flu(v) # flu to mavlink is the same as mavlink to flu return Flix._mavlink_to_flu(v)
def _command_send(self, command: int, params: Sequence[float]): def _command_send(self, command: int, params: Sequence[float]):
if len(params) != 7: if len(params) != 7:
@@ -320,13 +320,13 @@ class Flix:
def set_armed(self, armed: bool): def set_armed(self, armed: bool):
self._command_send(mavlink.MAV_CMD_COMPONENT_ARM_DISARM, (1 if armed else 0, 0, 0, 0, 0, 0, 0)) self._command_send(mavlink.MAV_CMD_COMPONENT_ARM_DISARM, (1 if armed else 0, 0, 0, 0, 0, 0, 0))
def set_position(self, position: Sequence[float], yaw: Optional[float] = None, wait: bool = False, tolerance: float = 0.1): def set_position(self, position: List[float], yaw: Optional[float] = None, wait: bool = False, tolerance: float = 0.1):
raise NotImplementedError('Position control is not implemented yet') raise NotImplementedError('Position control is not implemented yet')
def set_velocity(self, velocity: Sequence[float], yaw: Optional[float] = None): def set_velocity(self, velocity: List[float], yaw: Optional[float] = None):
raise NotImplementedError('Velocity control is not implemented yet') raise NotImplementedError('Velocity control is not implemented yet')
def set_attitude(self, attitude: Sequence[float], thrust: float): def set_attitude(self, attitude: List[float], thrust: float):
if len(attitude) == 3: if len(attitude) == 3:
attitude = Quaternion([attitude[0], attitude[1], attitude[2]]).q # type: ignore attitude = Quaternion([attitude[0], attitude[1], attitude[2]]).q # type: ignore
elif len(attitude) != 4: elif len(attitude) != 4:
@@ -339,7 +339,7 @@ class Flix:
[attitude[0], attitude[1], attitude[2], attitude[3]], [attitude[0], attitude[1], attitude[2], attitude[3]],
0, 0, 0, thrust) 0, 0, 0, thrust)
def set_rates(self, rates: Sequence[float], thrust: float): def set_rates(self, rates: List[float], thrust: float):
if len(rates) != 3: if len(rates) != 3:
raise ValueError('Rates must be [roll_rate, pitch_rate, yaw_rate]') raise ValueError('Rates must be [roll_rate, pitch_rate, yaw_rate]')
if not (0 <= thrust <= 1): if not (0 <= thrust <= 1):
@@ -351,7 +351,7 @@ class Flix:
[1, 0, 0, 0], [1, 0, 0, 0],
rates[0], rates[1], rates[2], thrust) rates[0], rates[1], rates[2], thrust)
def set_motors(self, motors: Sequence[float]): def set_motors(self, motors: List[float]):
if len(motors) != 4: if len(motors) != 4:
raise ValueError('motors must have 4 values') raise ValueError('motors must have 4 values')
if not all(0 <= m <= 1 for m in motors): if not all(0 <= m <= 1 for m in motors):