68 Commits
Author SHA1 Message Date
Oleg Kalachev 3f11fa11e8 Test simulation run 2026-08-14 09:41:35 +03:00
Oleg Kalachev a05e35540e Bring back simpler version of Value::set 2026-08-14 06:18:17 +03:00
Oleg Kalachev 6098293776 Utilize Value class in parameters for
Now bool parameters also may be used.
2026-08-14 06:11:43 +03:00
Oleg Kalachev 2d1efac05f Change type hints of the most List arguments to Sequence in pyflix
These arguments are effectively immutable, so Sequence makes more sense - the user may use tuples in addition to lists.
2026-08-14 05:57:58 +03:00
Oleg Kalachev f99a9998b1 Add rates_extra argument to pyflix' set_attitude method
For controlling rates feed forward.
2026-08-14 05:32:39 +03:00
Oleg Kalachev 4d77c6c369 Simply set_attitude_target handler
Rates ignore flags are efficiently the same as zeros, so can be ignored.
2026-08-14 05:21:45 +03:00
Oleg Kalachev 94c70994b6 Fix simulation run 2026-08-14 04:19:25 +03:00
Oleg Kalachev 78be5b3a8d Minor changes 2026-08-14 03:15:27 +03:00
Oleg Kalachev 9b1f0bd593 Remove test line 2026-08-14 03:04:00 +03:00
Oleg Kalachev b72a10dd7b Trigger build 2026-08-14 02:25:29 +03:00
Oleg Kalachev d2d1c74842 Update all used actions
Some of them were deprecated.
2026-08-14 01:51:21 +03:00
Oleg Kalachev 7308159b74 Show cloc diff with the previous commit 2026-08-14 01:41:57 +03:00
Oleg Kalachev 87ce9c20cb Count and print sloc count in tools workflow 2026-08-14 01:30:51 +03:00
Oleg Kalachev d6b9228282 Fix default windup value in pid
INFINITY makes much more sense, since otherwise i term would be ineffective at all by default
2026-08-13 02:21:03 +03:00
Oleg Kalachev f355cc2938 Upload binaries to docs only in my repo and in master and dev branches 2026-08-12 07:19:23 +03:00
Oleg Kalachev 0139d71b12 Show git hash commit in sys command 2026-08-12 01:58:13 +03:00
Oleg Kalachev 58da0b9959 Add binaries to the docs only of DOCS_BINARIES repo variable is truthy 2026-08-11 21:28:16 +03:00
Oleg Kalachev 355f4ad49d Merge remote-tracking branch 'origin/master' into dev 2026-08-11 18:43:18 +03:00
Oleg Kalachev 55398a660d Remove unused variable 2026-08-11 04:59:54 +03:00
Oleg Kalachev 63bbea4d8b Fix 2026-08-11 02:26:20 +03:00
Oleg Kalachev 5543a363b3 Add yaw rate windup parameter 2026-08-11 01:56:49 +03:00
Oleg Kalachev 363c756c00 Fix windup for yaw rate 2026-08-11 01:56:41 +03:00
Oleg Kalachev 57b853361c Fix motor pins for Flix2 2026-08-11 01:55:56 +03:00
Oleg Kalachev ce37e5b724 Simplify parameter defaults definition 2026-08-11 00:48:10 +03:00
Oleg Kalachev 36b050a896 Add config for Flix2 board 2026-08-10 03:52:57 +03:00
Oleg Kalachev 93ebf75a44 Fix 2026-08-10 03:51:16 +03:00
Oleg Kalachev a60c84ce7a Forgotten image 2026-08-10 03:45:38 +03:00
Oleg Kalachev ae8931cb25 Move most parameter defaults to config.h 2026-08-10 03:45:28 +03:00
Oleg Kalachev e6072b0bc4 Make i and d arguments optional in pid constructor 2026-08-10 03:43:37 +03:00
Oleg Kalachev 67e772a23f Update wait-on-check-action version 2026-08-10 03:33:27 +03:00
Oleg Kalachev 04a24c4939 Add minor comment 2026-08-10 03:27:23 +03:00
Oleg Kalachev f62e176313 Update the docs to reflect possibility for using prebuilt binaries 2026-08-10 03:17:32 +03:00
Oleg Kalachev 8ae06eef85 Docs updates 2026-08-10 03:16:54 +03:00
Oleg Kalachev 9a17ca848d Simplify 2026-08-10 02:41:53 +03:00
Oleg Kalachev b8f93becca Make Rate always trigger on first step 2026-08-10 02:31:54 +03:00
Oleg Kalachev 5fc2143994 Make brushless motors separated block to avoid confusion 2026-08-10 02:31:14 +03:00
Oleg Kalachev b7102d395c Make default fqbn plain esp32
It works on d1 as well
2026-08-10 02:15:30 +03:00
Oleg Kalachev 58ea75d335 Fixes 2026-08-10 01:18:29 +03:00
Oleg Kalachev 93b3077fa7 Add I2C pin parameters 2026-08-09 23:59:45 +03:00
Oleg Kalachev 4c74768131 Fixes 2026-08-09 22:42:14 +03:00
Oleg Kalachev 9880eeccd3 Disable imu by default 2026-08-09 20:41:03 +03:00
Oleg Kalachev a26d096dc0 Disable motors by default 2026-08-09 20:40:58 +03:00
Oleg Kalachev 8a91201cf4 Add targets with psram 2026-08-09 06:35:54 +03:00
Oleg Kalachev 51ca049b97 Download binaries to docs site 2026-08-09 04:23:16 +03:00
Oleg Kalachev 2decd2d6dd Fix simulation build 2026-08-08 02:19:38 +03:00
Oleg Kalachev abb2e9f79b Use up-to-date method for installing arduino-cli in simulator ci 2026-08-08 02:19:28 +03:00
Oleg Kalachev 6c907b77f6 Fix simulation build 2026-08-08 01:31:41 +03:00
Oleg Kalachev 0a53170097 Make imu configuration parameters work 2026-08-08 01:24:43 +03:00
Oleg Kalachev 6b36488245 Use dev version of FlixPeriph 2026-08-08 01:21:01 +03:00
Oleg Kalachev f30a182b20 Add parameter WIFI_BROADCAST for always broadcasting mode
May be used in some cases, like multiple gcs
2026-08-08 01:04:20 +03:00
Oleg Kalachev 90b506e084 Allow hot reconnection from another gcs over wifi
Switch back to udp broadcasting if no messages from gcs for 5 seconds
2026-08-07 18:24:23 +03:00
Oleg Kalachev 5f9aae62b9 Upload all binaries to artifacts 2026-08-06 03:04:05 +03:00
Oleg Kalachev 0e0867ceab Merge branch 'master' into dev 2026-08-05 01:26:28 +03:00
Oleg Kalachev 83aac78cf7 Merge branch 'master' into dev 2026-08-03 16:02:29 +03:00
Oleg Kalachev eb93b0b29d Merge branch 'master' into dev 2026-08-03 16:01:39 +03:00
Oleg Kalachev 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
29 changed files with 920 additions and 266 deletions
+34 -17
View File
@@ -10,30 +10,38 @@ on:
jobs:
build_linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Arduino CLI
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
- name: Build firmware
env:
ARDUINO_SKETCH_ALWAYS_EXPORT_BINARIES: 1
steps:
- uses: actions/checkout@v7
- name: Install Arduino CLI
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
- name: Build firmware for ESP32
run: make
- name: Upload binaries
uses: actions/upload-artifact@v4
with:
name: firmware-binary
path: flix/build
- name: Build firmware for ESP32-C3
run: make BOARD=esp32:esp32:esp32c3
- name: Build firmware for ESP32-S3
run: make BOARD=esp32:esp32:esp32s3
run: make BOARD=esp32:esp32:esp32s3:CDCOnBoot=cdc
- name: Build firmware for ESP32-S3 with QSPI PSRAM
run: make BOARD=esp32:esp32:esp32s3:CDCOnBoot=cdc,PSRAM=enabled EXTRA=--output-dir=flix/build/esp32.esp32.esp32s3.qspi
- name: Build firmware for ESP32-S3 with OPI PSRAM
run: make BOARD=esp32:esp32:esp32s3:CDCOnBoot=cdc,PSRAM=opi EXTRA=--output-dir=flix/build/esp32.esp32.esp32s3.opi
- name: Build firmware for Flix2
run: make BOARD=esp32:esp32:esp32s3:FlashSize=4M,CDCOnBoot=cdc,PSRAM=opi FLAGS=-DFLIX2 EXTRA=--output-dir=flix/build/esp32.esp32.flix2
- name: Upload binaries
uses: actions/upload-artifact@v7
with:
name: firmware-binary
path: flix/build
- name: Build espnow-proxy
run: arduino-cli compile --fqbn esp32:esp32:esp32 tools/espnow-proxy
- name: Check c_cpp_properties.json
run: tools/check_c_cpp_properties.py
build_macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install Arduino CLI
run: brew install arduino-cli
- name: Build firmware
@@ -44,7 +52,7 @@ jobs:
build_windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install Arduino CLI
run: choco install arduino-cli
- name: Install Make
@@ -64,8 +72,8 @@ jobs:
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget build-essential cmake g++ pkg-config gnupg2 lsb-release sudo
- name: Install Arduino CLI
uses: arduino/setup-arduino-cli@v1.1.1
- uses: actions/checkout@v4
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
- uses: actions/checkout@v7
- name: Install Gazebo
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'
@@ -76,7 +84,16 @@ jobs:
run: sudo apt-get install -y libsdl2-dev
- name: Build simulator
run: make build_simulator
- uses: actions/upload-artifact@v4
- name: Run simulator
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:
name: gazebo-plugin-binary
path: gazebo/build/*.so
@@ -88,7 +105,7 @@ jobs:
steps:
- name: Install Arduino CLI
run: brew install arduino-cli
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Clean up python binaries # Workaround for https://github.com/actions/setup-python/issues/577
run: |
rm -f /usr/local/bin/2to3*
+44 -5
View File
@@ -8,6 +8,7 @@ on:
permissions:
contents: read
actions: read
pages: write
id-token: write
@@ -15,7 +16,7 @@ jobs:
markdownlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install markdownlint
run: npm install -g markdownlint-cli2
- name: Run markdownlint
@@ -24,19 +25,57 @@ jobs:
build_book:
runs-on: ubuntu-latest
needs: markdownlint
env:
BINARIES: ${{ github.event_name == 'push' && (github.ref_name == 'master' || github.ref_name == 'dev') && github.repository == 'okalachev/flix' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install mdBook
run: cargo install mdbook --vers 0.4.43 --locked
- name: Build book
run: cd docs && mdbook build
- name: Wait for Build to complete
if: ${{ env.BINARIES }}
uses: lewagon/wait-on-check-action@v1.9.1
with:
ref: ${{ github.sha }}
check-name: build_linux
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 30
- name: Find firmware binaries
if: ${{ env.BINARIES }}
id: build_run
run: |
RUN_ID=$(gh api "repos/${{ github.repository }}/actions/workflows/build.yml/runs?head_sha=${{ github.sha }}&per_page=1" --jq '.workflow_runs[0].id')
echo "id=$RUN_ID" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download firmware binaries
if: ${{ env.BINARIES }}
uses: actions/download-artifact@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
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v5
with:
path: docs/build
deploy:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
if: ${{ github.event_name == 'push' && github.ref_name == 'master' }}
concurrency:
group: "pages"
cancel-in-progress: true
@@ -48,4 +87,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
+24 -4
View File
@@ -10,7 +10,7 @@ jobs:
csv_to_ulog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Build csv_to_ulog
run: cd tools/csv_to_ulog && mkdir build && cd build && cmake .. && make
- name: Test csv_to_ulog
@@ -22,13 +22,13 @@ jobs:
pyflix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install Python build tools
run: pip install build
- name: Build pyflix
run: python3 -m build tools
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: pyflix
path: |
@@ -37,7 +37,7 @@ jobs:
python_tools:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install Python dependencies
run: pip install -r tools/requirements.txt
- name: Test csv_to_mcap tool
@@ -46,3 +46,23 @@ jobs:
echo -e "t,x,y,z\n0,1,2,3\n1,4,5,6" > log.csv
./csv_to_mcap.py log.csv
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
+10 -6
View File
@@ -1,13 +1,17 @@
BOARD = esp32:esp32:d1_mini32:DebugLevel=error
BOARD = esp32:esp32:esp32
PORT := $(strip $(wildcard /dev/serial/by-id/usb-Silicon_Labs_CP21* /dev/serial/by-id/usb-1a86_USB_Single_Serial_* /dev/cu.usbserial-* /dev/cu.usbmodem*))
VERSION = $(shell git describe --always --dirty)
export ARDUINO_NETWORK_CONNECTION_TIMEOUT := 1h
build: .core .libs
arduino-cli compile --fqbn $(BOARD) flix
arduino-cli compile flix \
--fqbn $(BOARD) \
--build-property "build.core_debug_level=1" \
--build-property "compiler.cpp.extra_flags=-DVERSION=$(VERSION) $(FLAGS)" $(EXTRA)
upload: build
arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" flix
arduino-cli upload flix --fqbn $(BOARD) -p "$(PORT)"
erase:
arduino-cli burn-bootloader --fqbn $(BOARD) -p "$(PORT)" -P esptool
@@ -22,13 +26,13 @@ core .core:
libs .libs:
arduino-cli lib update-index
arduino-cli lib install "FlixPeriph"
ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=1 arduino-cli lib install --git-url 'https://github.com/okalachev/flixperiph.git#dev'
arduino-cli lib install "MAVLink"@2.0.25
touch .libs
upload_proxy: .core .libs
arduino-cli compile --fqbn $(BOARD) tools/espnow-proxy
arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" tools/espnow-proxy
arduino-cli compile tools/espnow-proxy --fqbn $(BOARD)
arduino-cli upload tools/espnow-proxy --fqbn $(BOARD) -p "$(PORT)"
gazebo/build cmake: gazebo/CMakeLists.txt
mkdir -p gazebo/build
+1 -1
View File
@@ -73,7 +73,7 @@ Parameters subsystem (`parameters.ino`) uses standard [Preferences.h](https://do
To add a new parameter:
1. Define a global variable for the parameter, two types are supported: `float` and `int`.
1. Define a global variable for the parameter, three types are supported: `float`, `int`, and `bool`.
2. Add an entry to the `parameters` array, with the parameter name, a pointer to the variable, and optionally a callback function to call when the parameter is changed.
3. Everything else will be handled automatically.
Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+52 -22
View File
@@ -1,20 +1,48 @@
# Usage: build, setup and flight
To fly Flix quadcopter, you need to build the firmware, upload it to the ESP32 board, and set up the drone for flight.
To fly Flix quadcopter, you need to upload the firmware to the ESP32 board, and set up the drone for flight.
To get the firmware sources, clone the repository using git:
## Uploading the firmware
You can either use the **prebuilt binaries** or **build the firmware** from sources — this will let you modify the firmware and add new features.
### Prebuilt binaries (the easiest way)
1. Download the latest firmware file using the following links:
<!-- markdownlint-disable MD044 -->
|Type|Boards|Link|
|-|-|-|
|ESP32|DevKit, D1 Mini|[`quadcopter.dev/flix.esp32.merged.bin`](https://quadcopter.dev/flix.esp32.merged.bin)|
|ESP32-S3|Most S3 based|[`quadcopter.dev/flix.esp32s3.merged.bin`](https://quadcopter.dev/flix.esp32s3.merged.bin)|
|ESP32-S3 (2MB PSRAM)|S3 Super Mini, S3 Zero (2MB PSRAM)|[`quadcopter.dev/flix.esp32s3.qspi.merged.bin`](https://quadcopter.dev/flix.esp32s3.qspi.merged.bin)|
|ESP32-S3 (8/16MB PSRAM)|S3 Zero (8MB PSRAM)|[`quadcopter.dev/flix.esp32s3.opi.merged.bin`](https://quadcopter.dev/flix.esp32s3.opi.merged.bin)|
|ESP32-C3|C3 Super Mini|[`quadcopter.dev/flix.esp32c3.merged.bin`](https://quadcopter.dev/flix.esp32c3.merged.bin)|
|Flix2|Flix2 board|[`quadcopter.dev/flix.flix2.merged.bin`](https://quadcopter.dev/flix.flix2.merged.bin)|
<!-- markdownlint-enable MD044 -->
2. Flash your ESP32 board using [ESP32 Web Flasher](https://www.espboards.dev/tools/program/):
<img src="img/web-flasher.png" width="400">
* Connect the board to your computer, press *Connect to ESP*, choose the serial port.
* Go to the *Flash* tab.
* Choose the downloaded firmware file, set *Flash address* to *0* (important).
* Click *Program* button and wait until the process is finished.
### Building from sources (flexible)
You can build and upload the firmware using either **Arduino IDE** (easier for beginners) or **command line**.
Get the sources using git:
```bash
git clone https://github.com/okalachev/flix.git && cd flix
```
Beginners can [download the source code as a ZIP archive](https://github.com/okalachev/flix/archive/refs/heads/master.zip).
Beginners can [download the sources as a ZIP archive](https://github.com/okalachev/flix/archive/refs/heads/master.zip).
## Building the firmware
You can build and upload the firmware using either **Arduino IDE** (easier for beginners) or **command line**.
### Arduino IDE (Windows, Linux, macOS)
#### Arduino IDE (Windows, Linux, macOS)
<img src="img/arduino-ide.png" width="400" alt="Flix firmware open in Arduino IDE">
@@ -29,7 +57,7 @@ You can build and upload the firmware using either **Arduino IDE** (easier for b
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/).
@@ -61,7 +89,7 @@ You can build and upload the firmware using either **Arduino IDE** (easier for b
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:DebugLevel=error,FlashSize=4M,CDCOnBoot=cdc upload
make BOARD=esp32:esp32:esp32s3:FlashSize=4M,CDCOnBoot=cdc upload
```
See other available Make commands in [Makefile](../Makefile).
@@ -71,15 +99,6 @@ See other available Make commands in [Makefile](../Makefile).
## 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
QGroundControl is a ground control station software that can be used to monitor and control the drone.
@@ -120,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.
### 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
The IMU orientation (relative to the drone's axes) is defined using the parameters: `IMU_ROT_ROLL`, `IMU_ROT_PITCH`, and `IMU_ROT_YAW`.
@@ -148,9 +178,9 @@ Before flight you need to calibrate the accelerometer:
If using non-default motor pins, set the pin numbers using the parameters: `MOTOR_PIN_FL`, `MOTOR_PIN_FR`, `MOTOR_PIN_RL`, `MOTOR_PIN_RR` (front-left, front-right, rear-left, rear-right respectively).
Certain ESP32 models (such as ESP32-S3 and ESP32-C3) support a lower maximum PWM frequency; on these boards the parameter `MOT_PWM_FREQ` should be set to 38000 Hz.
#### Brushless motors
If using brushless motors and ESCs:
If using brushless motors with ESCs:
1. Set the appropriate PWM using the parameters: `MOT_PWM_STOP`, `MOT_PWM_MIN`, and `MOT_PWM_MAX` (1000, 1000, and 2000 is typical).
2. Decrease the PWM frequency using the `MOT_PWM_FREQ` parameter (400 is typical).
@@ -158,7 +188,7 @@ If using brushless motors and ESCs:
> [!CAUTION]
> **Remove the props when configuring the motors!** If improperly configured, you may not be able to stop them.
### Battery voltage monitoring
### Battery voltage monitoring (optional)
ESP32 ADC can measure only up to 3.3 V, so you need to use a voltage divider to monitor the battery voltage. To enable voltage measurement, set the following parameters:
+26 -6
View File
@@ -6,7 +6,7 @@
#include "pid.h"
#include "vector.h"
#include "util.h"
#include "lpf.h"
#include "filter.h"
extern const int MOTOR_REAR_LEFT, MOTOR_REAR_RIGHT, MOTOR_FRONT_RIGHT, MOTOR_FRONT_LEFT;
extern const int RAW, ACRO, STAB, AUTO;
@@ -50,8 +50,14 @@ const char* motd =
"sta <ssid> <password> - configure Wi-Fi client mode\n"
"espnow <mac> [<key>] - configure ESP-NOW peer\n"
"mot - show motor output\n"
"log [dump] - print log header [and data]\n"
"mfr/mfl/mrr/mrl [<thrust>] - test motor (remove props)\n"
"log [dump] - print log header [and data]\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"
"reset - reset drone's state\n"
"reboot - reboot the drone\n";
@@ -99,7 +105,7 @@ void doCommand(String str, bool echo = false) {
if (success) {
print("%s = %g\n", arg0.c_str(), getParameter(arg0.c_str()));
} else {
print("Parameter not found: %s\n", arg0.c_str());
print("Cannot set parameter: %s\n", arg0.c_str());
}
} else if (command == "preset") {
resetParameters();
@@ -153,9 +159,18 @@ void doCommand(String str, bool echo = false) {
} else if (command == "mot") {
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]);
} 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();
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") {
calibrateRC();
} else if (command == "ca") {
@@ -174,7 +189,12 @@ void doCommand(String str, bool echo = false) {
print("Temperature: %.1f °C\n", temperatureRead());
print("Total RAM: %d KB\n", ESP.getHeapSize() / 1024);
print("Free heap: %d KB\n", ESP.getFreeHeap() / 1024);
print("Firmware: " __DATE__ " " __TIME__ "\n");
print("PSRAM: %d KB\n", ESP.getPsramSize() / 1024);
print("Free PSRAM: %d KB\n", ESP.getFreePsram() / 1024);
#ifdef VERSION
print("Version: %s\n", STRINGIFY(VERSION));
#endif
print("Build date: " __DATE__ " " __TIME__ "\n");
// Print tasks table
print("Num Task MinSt Prio Core CPU%%\n");
int taskCount = uxTaskGetNumberOfTasks();
+27
View File
@@ -0,0 +1,27 @@
// Copyright (c) 2026 Oleg Kalachev <okalachev@gmail.com>
// Repository: https://github.com/okalachev/flix
// Parameter defaults
#pragma once
void setDefaults() {
// Set defaults here
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)
pwmFrequency = 38000;
#endif
#ifdef FLIX2
imuModel = 4; // ICM-40609-D
imuIntPin = 10;
imuCsPin = 14;
motorPins[MOTOR_REAR_LEFT] = 41;
motorPins[MOTOR_REAR_RIGHT] = 7;
motorPins[MOTOR_FRONT_RIGHT] = 18;
motorPins[MOTOR_FRONT_LEFT] = 38;
voltagePin = 3;
#endif
}
+10 -35
View File
@@ -6,34 +6,9 @@
#include "vector.h"
#include "quaternion.h"
#include "pid.h"
#include "lpf.h"
#include "filter.h"
#include "util.h"
#define PITCHRATE_P 0.05
#define PITCHRATE_I 0.2
#define PITCHRATE_D 0.001
#define PITCHRATE_I_LIM 0.3
#define ROLLRATE_P PITCHRATE_P
#define ROLLRATE_I PITCHRATE_I
#define ROLLRATE_D PITCHRATE_D
#define ROLLRATE_I_LIM PITCHRATE_I_LIM
#define YAWRATE_P 0.3
#define YAWRATE_I 0.0
#define YAWRATE_D 0.0
#define YAWRATE_I_LIM 0.3
#define ROLL_P 6
#define ROLL_I 0
#define ROLL_D 0
#define PITCH_P ROLL_P
#define PITCH_I ROLL_I
#define PITCH_D ROLL_D
#define YAW_P 3
#define PITCHRATE_MAX radians(360)
#define ROLLRATE_MAX radians(360)
#define YAWRATE_MAX radians(300)
#define TILT_MAX radians(30)
#define RATES_D_LPF_ALPHA 0.2 // cutoff frequency ~ 40 Hz
const int RAW = 0, ACRO = 1, STAB = 2, AUTO = 3; // flight modes
int mode = STAB;
bool armed = false;
@@ -41,17 +16,17 @@ bool armed = false;
Quaternion attitudeTarget;
Vector ratesTarget;
Vector ratesExtra; // feedforward rates
Vector torqueTarget;
Vector torqueTarget; // 0 - no torque, 1 - maximum torque
float thrustTarget;
PID rollRatePID(ROLLRATE_P, ROLLRATE_I, ROLLRATE_D, ROLLRATE_I_LIM, RATES_D_LPF_ALPHA);
PID pitchRatePID(PITCHRATE_P, PITCHRATE_I, PITCHRATE_D, PITCHRATE_I_LIM, RATES_D_LPF_ALPHA);
PID yawRatePID(YAWRATE_P, YAWRATE_I, YAWRATE_D);
PID rollPID(ROLL_P, ROLL_I, ROLL_D);
PID pitchPID(PITCH_P, PITCH_I, PITCH_D);
PID yawPID(YAW_P, 0, 0);
Vector maxRate(ROLLRATE_MAX, PITCHRATE_MAX, YAWRATE_MAX);
float tiltMax = TILT_MAX;
PID rollRatePID(0.05, 0.2, 0.001, 0.3, 0.2);
PID pitchRatePID(0.05, 0.2, 0.001, 0.3, 0.2);
PID yawRatePID(0.3, 0, 0, 0.3);
PID rollPID(6);
PID pitchPID(6);
PID yawPID(3);
Vector maxRate(radians(360), radians(360), radians(360));
float tiltMax = radians(30);
int flightModes[] = {STAB, STAB, STAB}; // map for rc mode switch
extern const int MOTOR_REAR_LEFT, MOTOR_REAR_RIGHT, MOTOR_FRONT_RIGHT, MOTOR_FRONT_LEFT;
+8 -1
View File
@@ -5,7 +5,7 @@
#include "quaternion.h"
#include "vector.h"
#include "lpf.h"
#include "filter.h"
#include "util.h"
Vector rates; // estimated angular rates, rad/s
@@ -15,6 +15,12 @@ bool landed;
float accWeight = 0.003;
float levelWeight = 0.0002;
LowPassFilter<Vector> ratesFilter(0.2); // cutoff frequency ~ 40 Hz
NotchFilter<Vector> ratesNotch(382, 40);
void setupEstimate() {
print("Setup estimation\n");
ratesNotch.reset();
}
void estimate() {
applyGyro();
@@ -25,6 +31,7 @@ void estimate() {
void applyGyro() {
// filter gyro to get angular rates
rates = ratesFilter.update(gyro);
rates = ratesNotch.update(rates);
// apply rates to attitude
attitude = Quaternion::rotate(attitude, Quaternion::fromRotationVector(rates * dt));
+98
View File
@@ -0,0 +1,98 @@
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
// Repository: https://github.com/okalachev/flix
// Low pass and notch filters
#pragma once
template <typename T> // Using template to make the filter usable for scalar and vector values
class LowPassFilter {
public:
float alpha; // smoothing constant, 1 means filter disabled
T output;
LowPassFilter(float alpha): alpha(alpha) {};
T update(const T input) {
if (!init) {
init = true;
return output = input;
}
return output += alpha * (input - output);
}
void setCutOffFrequency(float cutOffFreq, float dt) {
alpha = 1 - exp(-2 * PI * cutOffFreq * dt);
}
void reset() {
init = false;
}
private:
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();
setupIMU();
setupRC();
setupEstimate();
setupLog();
setLED(false);
print("Initializing complete\n");
}
@@ -40,6 +42,6 @@ void loop() {
handleInput();
processMavlink();
readVoltage();
logData();
loopLog();
syncParameters();
}
+40 -21
View File
@@ -4,12 +4,17 @@
// Work with the IMU sensor
#include <SPI.h>
#include <Wire.h>
#include <FlixPeriph.h>
#include "vector.h"
#include "lpf.h"
#include "filter.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 gyro; // gyroscope output, rad/s
@@ -23,22 +28,35 @@ LowPassFilter<Vector> gyroBiasFilter(0.001);
void setupIMU() {
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();
}
void configureIMU() {
imu.setAccelRange(imu.ACCEL_RANGE_4G);
imu.setGyroRange(imu.GYRO_RANGE_2000DPS);
imu.setDLPF(imu.DLPF_MAX);
imu.setRate(imu.RATE_1KHZ_APPROX);
imu.setupInterrupt();
imu->setAccelRange(IMU::ACCEL_RANGE_4G);
imu->setGyroRange(IMU::GYRO_RANGE_2000DPS);
imu->setDLPF(IMU::DLPF_MAX);
imu->setRate(IMU::RATE_1KHZ_APPROX);
imu->setupInterrupt();
}
void readIMU() {
imu.waitForData();
imu.getGyro(gyro.x, gyro.y, gyro.z);
imu.getAccel(acc.x, acc.y, acc.z);
imu->waitForData();
imu->getGyro(gyro.x, gyro.y, gyro.z);
imu->getAccel(acc.x, acc.y, acc.z);
calibrateGyroOnce();
// Apply scale and bias
@@ -60,7 +78,7 @@ void calibrateGyroOnce() {
void calibrateAccel() {
print("Calibrating accelerometer\n");
imu.setAccelRange(imu.ACCEL_RANGE_2G); // the most sensitive mode
imu->setAccelRange(IMU::ACCEL_RANGE_2G); // the most sensitive mode
print("1/6 Place level [8 sec]\n");
pause(8);
@@ -94,9 +112,9 @@ void calibrateAccelOnce() {
// Compute the average of the accelerometer readings
acc = Vector(0, 0, 0);
for (int i = 0; i < samples; i++) {
imu.waitForData();
imu->waitForData();
Vector sample;
imu.getAccel(sample.x, sample.y, sample.z);
imu->getAccel(sample.x, sample.y, sample.z);
acc = acc + sample;
}
acc = acc / samples;
@@ -121,17 +139,18 @@ void printIMUCalibration() {
}
void printIMUInfo() {
imu.status() ? print("status: ERROR %d\n", imu.status()) : print("status: OK\n");
print("model: %s\n", imu.getModel());
print("who am I: 0x%02X\n", imu.whoAmI());
imu->status() ? print("status: ERROR %d\n", imu->status()) : print("status: OK\n");
print("model: %s\n", imu->getModel());
print("who am I: 0x%02X\n", imu->whoAmI());
print("rate: %.0f\n", loopRate);
print("temperature: %.1f °C\n", imu.getTemp());
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("acc: %f %f %f\n", acc.x, acc.y, acc.z);
imu.waitForData();
imu->waitForData();
Vector rawGyro, rawAcc;
imu.getGyro(rawGyro.x, rawGyro.y, rawGyro.z);
imu.getAccel(rawAcc.x, rawAcc.y, rawAcc.z);
imu->getGyro(rawGyro.x, rawGyro.y, rawGyro.z);
imu->getAccel(rawAcc.x, rawAcc.y, rawAcc.z);
print("raw gyro: %f %f %f\n", rawGyro.x, rawGyro.y, rawGyro.z);
print("raw acc: %f %f %f\n", rawAcc.x, rawAcc.y, rawAcc.z);
}
+236 -41
View File
@@ -1,77 +1,272 @@
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
// Repository: https://github.com/okalachev/flix
// In-RAM logging
// Logging subsystem
#include "vector.h"
#include "util.h"
#define LOG_RATE 100
#define LOG_DURATION 10
#define LOG_SIZE LOG_DURATION * LOG_RATE
int logMemory = 0; // 0 - RAM, 1 - PSRAM, -1 - disabled
float logUsage = 0.5; // fraction of free memory to use for log
Vector attitudeEuler;
Vector attitudeTargetEuler;
struct LogEntry {
struct LogValue {
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[] = {
{"t", &t},
struct LogTopic {
LogValue values[10];
int length = 0; // number of logged values
float throttle; // max update rate, Hz
float lastUpdate = -INFINITY;
LogTopic(float throttle, LogValue v0, LogValue v1 = {}, LogValue v2 = {}, LogValue v3 = {}, LogValue v4 = {}, LogValue v5 = {}, LogValue v6 = {}, LogValue v7 = {}, LogValue v8 = {}, LogValue v9 = {}) :
throttle(throttle), values{v0, v1, v2, v3, v4, v5, v6, v7, v8, v9} {
// Count logged values
for (auto& v : values) {
if (v.name == nullptr) break;
if (v.logged) length++;
}
};
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) {};
};
LogTopic logTopics[] = {
// time
LogTopic({"t", &t}), // must be the first topic
LogTopic(1, {"loopRate", &loopRate}),
// imu
LogTopic(
{"gyro.x", &gyro.x},
{"gyro.y", &gyro.y},
{"gyro.z", &gyro.z}),
LogTopic(50,
{"acc.x", &acc.x},
{"acc.y", &acc.y},
{"acc.z", &acc.z}),
LogTopic(10,
{"gyroBias.x", &gyroBias.x},
{"gyroBias.y", &gyroBias.y},
{"gyroBias.z", &gyroBias.z}),
// estimation
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(); }}),
// rc
LogTopic(10,
{"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},
{"attitude.x", &attitudeEuler.x},
{"attitude.y", &attitudeEuler.y},
{"attitude.z", &attitudeEuler.z},
{"attitudeTarget.x", &attitudeTargetEuler.x},
{"attitudeTarget.y", &attitudeTargetEuler.y},
{"attitudeTarget.z", &attitudeTargetEuler.z},
{"thrustTarget", &thrustTarget}
{"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(); }}),
};
const int logColumns = sizeof(logEntries) / sizeof(logEntries[0]);
float logBuffer[LOG_SIZE][logColumns];
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 prepareLogData() {
attitudeEuler = attitude.toEuler();
attitudeTargetEuler = attitudeTarget.toEuler();
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 logData() {
if (!armed) return;
static int logPointer = 0;
static Rate period(LOG_RATE);
if (!period) return;
void loopLog() {
if (logBuffer == nullptr || !armed) return;
prepareLogData();
if (!logLength) resetLog(); // reset state on first log write
for (int i = 0; i < logColumns; i++) {
logBuffer[logPointer][i] = *logEntries[i].value;
static Rate sync(2);
if (sync) {
const uint8_t marker[] = {0x1A, 0x91, 0x4F, 0xF6, 0x7F};
writeLog(&marker, sizeof(marker)); // write sync marker
}
logPointer++;
if (logPointer >= LOG_SIZE) {
logPointer = 0;
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() {
for (int i = 0; i < logColumns; i++) {
print("%s%s", logEntries[i].name, i < logColumns - 1 ? "," : "\n");
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 printLogData() {
for (int i = 0; i < LOG_SIZE; i++) {
if (logBuffer[i][0] == 0) continue; // skip empty records
for (int j = 0; j < logColumns; j++) {
print("%g%s", logBuffer[i][j], j < logColumns - 1 ? "," : "\n");
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);
}
-34
View File
@@ -1,34 +0,0 @@
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
// Repository: https://github.com/okalachev/flix
// Low pass filter implementation
#pragma once
template <typename T> // Using template to make the filter usable for scalar and vector values
class LowPassFilter {
public:
float alpha; // smoothing constant, 1 means filter disabled
T output;
LowPassFilter(float alpha): alpha(alpha) {};
T update(const T input) {
if (!init) {
init = true;
return output = input;
}
return output += alpha * (input - output);
}
void setCutOffFrequency(float cutOffFreq, float dt) {
alpha = 1 - exp(-2 * PI * cutOffFreq * dt);
}
void reset() {
init = false;
}
private:
bool init = false;
};
+61 -17
View File
@@ -16,8 +16,9 @@ Rate telemetryAttitude(20);
Rate telemetryRC(10);
Rate telemetryMotors(10);
Rate telemetryIMU(15);
Rate telemetryTopic(10);
bool mavlinkConnected = false;
float mavlinkTime = NAN; // time of last received message
String mavlinkPrintBuffer;
void processMavlink() {
@@ -40,7 +41,7 @@ void sendMavlink() {
sendMessage(&msg);
}
if (!mavlinkConnected) return; // send only heartbeat until connected
if (!valid(mavlinkTime)) return; // send only heartbeat until connected
if (telemetrySlow) {
mavlink_msg_extended_sys_state_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg,
@@ -84,6 +85,12 @@ void sendMavlink() {
0, 0, 0, 0);
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) {
@@ -92,6 +99,26 @@ void sendMessage(const void *msg) {
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() {
uint8_t buf[MAVLINK_MAX_PACKET_LEN];
int len = receiveWiFi(buf, MAVLINK_MAX_PACKET_LEN);
@@ -101,7 +128,7 @@ void receiveMavlink() {
mavlink_status_t status;
for (int i = 0; i < len; i++) {
if (mavlink_parse_char(MAVLINK_COMM_0, buf[i], &msg, &status)) {
mavlinkConnected = true;
mavlinkTime = t;
handleMavlink(&msg);
}
}
@@ -196,18 +223,24 @@ void handleMavlink(const void *_msg) {
mavlink_msg_set_attitude_target_decode(&msg, &m);
if (m.target_system && m.target_system != mavlinkSysId) return;
// copy attitude, rates and thrust targets
ratesTarget.x = m.body_roll_rate;
ratesTarget.y = -m.body_pitch_rate; // convert to flu
ratesTarget.z = -m.body_yaw_rate;
if (!(m.type_mask & ATTITUDE_TARGET_TYPEMASK_ATTITUDE_IGNORE)) {
// Attitude control
attitudeTarget.w = m.q[0];
attitudeTarget.x = m.q[1];
attitudeTarget.y = -m.q[2];
attitudeTarget.y = -m.q[2]; // convert to flu
attitudeTarget.z = -m.q[3];
thrustTarget = m.thrust;
ratesExtra = Vector(0, 0, 0);
ratesExtra.x = m.body_roll_rate;
ratesExtra.y = -m.body_pitch_rate;
ratesExtra.z = -m.body_yaw_rate;
} else {
// Rates control
attitudeTarget.invalidate();
ratesTarget.x = m.body_roll_rate;
ratesTarget.y = -m.body_pitch_rate;
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;
}
@@ -225,18 +258,29 @@ void handleMavlink(const void *_msg) {
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) {
mavlink_log_request_data_t m;
mavlink_msg_log_request_data_decode(&msg, &m);
if (m.target_system && m.target_system != mavlinkSysId) return;
// Send all log records
for (int i = 0; i < sizeof(logBuffer) / sizeof(logBuffer[0]); i++) {
mavlink_message_t msg;
mavlink_msg_log_data_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, 0, i,
sizeof(logBuffer[0]), (uint8_t *)logBuffer[i]);
sendMessage(&msg);
for (int i = 0; i < m.count; i += MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN) {
int chunkSize = min(MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN, (int)(m.count - i));
mavlink_message_t response;
uint8_t data[MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN];
readLog(data, m.ofs + i, chunkSize);
mavlink_msg_log_data_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &response,
m.id, m.ofs + i, chunkSize, data);
batchMessage(&response);
}
flushBatchMessages();
}
// Handle commands
+49 -22
View File
@@ -9,24 +9,22 @@
extern int channelZero[16], channelMax[16];
extern int rollChannel, pitchChannel, throttleChannel, yawChannel, armedChannel, modeChannel;
extern int rcRxPin, voltagePin;
extern int wifiMode, wifiLongRange, udpLocalPort, udpRemotePort, espnowChannel;
extern int wifiMode, wifiLongRange, wifiBroadcast, udpLocalPort, udpRemotePort, espnowChannel;
extern float rcLossTimeout, descendTime, disarmTilt;
extern float voltageScale;
extern LowPassFilter<float> voltageFilter;
#include "config.h"
Preferences storage;
struct Parameter {
const char *name; // max length is 15
bool integer;
union { float *f; int *i; }; // pointer to the variable
float inital; // default value
Value value; // pointer to the variable
float initial; // default value
float cache; // what's stored in flash
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, int *variable, void (*callback)() = nullptr) : name(name), integer(true), i(variable), callback(callback) {};
float getValue() const { return integer ? *i : *f; };
void setValue(const float value) { if (integer) *i = value; else *f = value; };
Parameter(const char *name, Value value, void (*callback)() = nullptr) : name(name), value(value), callback(callback) {};
};
Parameter parameters[] = {
@@ -44,6 +42,7 @@ Parameter parameters[] = {
{"CTL_Y_RATE_P", &yawRatePID.p},
{"CTL_Y_RATE_I", &yawRatePID.i},
{"CTL_Y_RATE_D", &yawRatePID.d},
{"CTL_Y_RATE_WU", &yawRatePID.windup},
{"CTL_Y_RATE_D_A", &yawRatePID.lpf.alpha},
{"CTL_R_P", &rollPID.p},
{"CTL_R_I", &rollPID.i},
@@ -60,6 +59,15 @@ Parameter parameters[] = {
{"CTL_FLT_MODE_1", &flightModes[1]},
{"CTL_FLT_MODE_2", &flightModes[2]},
// 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_PITCH", &imuRotation.y},
{"IMU_ROT_YAW", &imuRotation.z},
@@ -74,6 +82,8 @@ Parameter parameters[] = {
{"EST_ACC_WEIGHT", &accWeight},
{"EST_LVL_WEIGHT", &levelWeight},
{"EST_RATES_LPF_A", &ratesFilter.alpha},
{"EST_RATES_NF_F", &ratesNotch.frequency, setupEstimate},
{"EST_RATES_NF_BW", &ratesNotch.bandwidth, setupEstimate},
// motors
{"MOT_PIN_FL", &motorPins[MOTOR_FRONT_LEFT], setupMotors},
{"MOT_PIN_FR", &motorPins[MOTOR_FRONT_RIGHT], setupMotors},
@@ -112,6 +122,7 @@ Parameter parameters[] = {
{"WIFI_PORT_LOC", &udpLocalPort},
{"WIFI_PORT_REM", &udpRemotePort},
{"WIFI_LONG_RANGE", &wifiLongRange},
{"WIFI_BROADCAST", &wifiBroadcast},
// espnow
{"ESPNOW_CHANNEL", &espnowChannel},
// mavlink
@@ -121,6 +132,22 @@ Parameter parameters[] = {
{"MAV_RATE_RC", &telemetryRC.rate},
{"MAV_RATE_MOT", &telemetryMotors.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
{"PWR_VOLT_PIN", &voltagePin, setupPower},
{"PWR_VOLT_SCALE", &voltageScale},
@@ -133,14 +160,15 @@ Parameter parameters[] = {
void setupParameters() {
print("Setup parameters\n");
setDefaults();
storage.begin("flix");
// Read parameters from storage
for (auto &parameter : parameters) {
parameter.inital = parameter.getValue();
parameter.initial = parameter.value.get();
if (storage.isKey(parameter.name)) {
parameter.setValue(storage.getFloat(parameter.name));
parameter.value.set(storage.getFloat(parameter.name));
}
parameter.cache = parameter.getValue();
parameter.cache = parameter.value.get();
}
}
@@ -155,13 +183,13 @@ const char *getParameterName(int index) {
float getParameter(int index) {
if (index < 0 || index >= parametersCount()) return NAN;
return parameters[index].getValue();
return parameters[index].value.get();
}
float getParameter(const char *name) {
for (auto &parameter : parameters) {
if (strcasecmp(parameter.name, name) == 0) {
return parameter.getValue();
return parameter.value.get();
}
}
return NAN;
@@ -170,10 +198,9 @@ float getParameter(const char *name) {
bool setParameter(const char *name, const float value) {
for (auto &parameter : parameters) {
if (strcasecmp(parameter.name, name) == 0) {
if (parameter.integer && !isfinite(value)) return false; // can't set integer to NaN or Inf
parameter.setValue(value);
bool success = parameter.value.set(value);
if (parameter.callback) parameter.callback();
return true;
return success;
}
}
return false;
@@ -185,10 +212,10 @@ void syncParameters() {
if (motorsActive()) return; // don't use flash while flying, it may cause a delay
for (auto &parameter : parameters) {
if (floatEquals(parameter.getValue(), parameter.cache)) continue; // no change
if (floatEquals(parameter.value.get(), parameter.cache)) continue; // no change
storage.putFloat(parameter.name, parameter.getValue());
parameter.cache = parameter.getValue(); // update cache
storage.putFloat(parameter.name, parameter.value.get());
parameter.cache = parameter.value.get(); // update cache
}
}
@@ -197,10 +224,10 @@ void printParameters(const char *filter) {
for (auto &parameter : parameters) {
if (strncasecmp(parameter.name, filter, strlen(filter))) continue;
if (floatEquals(parameter.getValue(), parameter.inital)) { // parameter changed
print("%-15s %-13g\n", parameter.name, parameter.getValue());
if (floatEquals(parameter.value.get(), parameter.initial)) { // parameter changed
print("%-15s %-13g\n", parameter.name, parameter.value.get());
} else {
print("%-15s %-13g [%g]\n", parameter.name, parameter.getValue(), parameter.inital);
print("%-15s %-13g [%g]\n", parameter.name, parameter.value.get(), parameter.initial);
}
}
}
+2 -2
View File
@@ -5,7 +5,7 @@
#pragma once
#include "lpf.h"
#include "filter.h"
class PID {
public:
@@ -18,7 +18,7 @@ public:
LowPassFilter<float> lpf; // low pass filter for derivative term
PID(float p, float i, float d, float windup = 0, float dAlpha = 1, float dtMax = 0.1) :
PID(float p, float i = 0, float d = 0, float windup = INFINITY, float dAlpha = 1, float dtMax = 0.1) :
p(p), i(i), d(d), windup(windup), lpf(dAlpha), dtMax(dtMax) {}
float update(float error) {
+1 -1
View File
@@ -5,7 +5,7 @@
#include <soc/soc.h>
#include <soc/rtc_cntl_reg.h>
#include "lpf.h"
#include "filter.h"
#include "util.h"
float voltage = NAN;
+47 -1
View File
@@ -11,6 +11,8 @@
const float ONE_G = 9.80665;
extern float t;
#define STRINGIFY(x) __STRINGIFY(x)
float mapf(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
@@ -64,11 +66,55 @@ 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;
}
};
bool set(float value) const {
switch (type) {
case FLOAT: *_float = value; break;
case INT: if (!isfinite(value)) return false; *_int = value; break;
case BOOL: *_bool = (value != 0); break;
default: return false;
}
return true;
}
};
// Rate limiter
class Rate {
public:
float rate;
float last = 0;
float last = -INFINITY;
Rate(float rate) : rate(rate) {}
operator bool() {
+4 -2
View File
@@ -17,6 +17,7 @@ const int W_DISABLED = 0, W_AP = 1, W_STA = 2, W_ESPNOW = 3;
int wifiMode = W_AP;
int wifiLongRange = 0;
int wifiBroadcast = 0; // 0 - broadcast until connected, 1 - always broadcast
int udpLocalPort = 14550;
int udpRemotePort = 14550;
IPAddress udpRemoteIP = "255.255.255.255";
@@ -64,7 +65,8 @@ void sendWiFi(const uint8_t *buf, int len) {
if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return;
udp.beginPacket(udpRemoteIP, udpRemotePort);
bool broadcast = wifiBroadcast || !(t - mavlinkTime < 5); // broadcast if lost connection
udp.beginPacket(broadcast ? IPAddress(255, 255, 255, 255) : udpRemoteIP, udpRemotePort);
udp.write(buf, len);
udp.endPacket();
}
@@ -113,7 +115,7 @@ void printWiFiInfo() {
} else {
print("Mode: Disabled\n");
}
print("MAVLink connected: %d\n", mavlinkConnected);
print("MAVLink connected: %d\n", valid(mavlinkTime));
}
void configWiFi(int mode, const char *first, const char *second) {
+11
View File
@@ -20,6 +20,10 @@
#define radians(deg) ((deg)*DEG_TO_RAD)
#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)))
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; }
@@ -156,6 +160,8 @@ HardwareSerial Serial, Serial1, Serial2;
class EspClass {
public:
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;
unsigned long __delayTime = 0;
@@ -165,11 +171,16 @@ void delay(uint32_t ms) {
__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 ledcWrite(uint8_t pin, uint32_t duty) { return true; }
uint32_t ledcChangeFrequency(uint8_t pin, uint32_t freq, uint8_t resolution) { return freq; }
int8_t digitalPinToAnalogChannel(uint8_t pin) { return -1; }
uint32_t analogReadMilliVolts(uint8_t pin) { return 0; }
float temperatureRead() { return 0; }
unsigned long __micros;
unsigned long __resetTime = 0;
+19 -2
View File
@@ -9,7 +9,7 @@
#include "quaternion.h"
#include "Arduino.h"
#include "wifi.h"
#include "lpf.h"
#include "filter.h"
extern float t, dt;
extern float controlRoll, controlPitch, controlYaw, controlThrottle, controlMode;
@@ -21,6 +21,9 @@ extern float motors[4];
Vector gyro, acc, imuRotation;
Vector accBias, gyroBias, accScale(1, 1, 1);
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
void step();
@@ -48,8 +51,17 @@ void normalizeRC();
void calibrateRC();
void calibrateRCChannel(int*, uint16_t[16], uint16_t[16], const char*);
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 printLogData();
void printLogValues(const char *filter);
void configLogThrottle(const char *name, float throttle);
void exposeLogValue(const char *name);
void processMavlink();
void sendMavlink();
void sendMessage(const void *msg);
@@ -80,3 +92,8 @@ void printIMUInfo() {};
void printWiFiInfo() {};
void configWiFi(bool, const char*, const char*) { print("Skip WiFi config\n"); };
void setWiFiMode(const String& mode) { print("Skip WiFi mode set\n"); };
class IMU {
public:
float getTemp() { return 0; }
} *imu;
+3 -2
View File
@@ -23,7 +23,7 @@
#include "estimate.ino"
#include "safety.ino"
#include "log.ino"
#include "lpf.h"
#include "filter.h"
#include "mavlink.ino"
#include "motors.ino"
#include "parameters.ino"
@@ -55,6 +55,7 @@ public:
initNode();
Serial.begin(0);
setupParameters();
setupLog();
rcRxPin = 1; // set rc pin to enable rc reading
gzmsg << "Flix plugin loaded" << endl;
}
@@ -88,7 +89,7 @@ public:
applyMotorForces();
publishTopics();
logData();
loopLog();
syncParameters();
}
+1
View File
@@ -14,6 +14,7 @@
// Mocks
int wifiMode = 1;
int wifiLongRange = 0;
int wifiBroadcast = 0;
int espnowChannel = 6;
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 json
import docopt
import math
from mcap.writer import Writer
args = docopt.docopt(__doc__)
@@ -39,7 +40,15 @@ channel_id = writer.register_channel(
)
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)
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)
+10 -9
View File
@@ -243,7 +243,7 @@ class Flix:
time.sleep(1)
@staticmethod
def _mavlink_to_flu(v: List[float]) -> List[float]:
def _mavlink_to_flu(v: Sequence[float]) -> List[float]:
if len(v) == 3: # vector
return [v[0], -v[1], -v[2]]
elif len(v) == 4: # quaternion
@@ -252,8 +252,8 @@ class Flix:
raise ValueError(f'List must have 3 (vector) or 4 (quaternion) elements')
@staticmethod
def _flu_to_mavlink(v: List[float]) -> List[float]:
return Flix._mavlink_to_flu(v)
def _flu_to_mavlink(v: Sequence[float]) -> List[float]:
return Flix._mavlink_to_flu(v) # flu to mavlink is the same as mavlink to flu
def _command_send(self, command: int, params: Sequence[float]):
if len(params) != 7:
@@ -320,13 +320,13 @@ class Flix:
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))
def set_position(self, position: List[float], yaw: Optional[float] = None, wait: bool = False, tolerance: float = 0.1):
def set_position(self, position: Sequence[float], yaw: Optional[float] = None, wait: bool = False, tolerance: float = 0.1):
raise NotImplementedError('Position control is not implemented yet')
def set_velocity(self, velocity: List[float], yaw: Optional[float] = None):
def set_velocity(self, velocity: Sequence[float], yaw: Optional[float] = None):
raise NotImplementedError('Velocity control is not implemented yet')
def set_attitude(self, attitude: List[float], thrust: float):
def set_attitude(self, attitude: Sequence[float], thrust: float, rates_extra: Sequence[float] = (0, 0, 0)):
if len(attitude) == 3:
attitude = Quaternion([attitude[0], attitude[1], attitude[2]]).q # type: ignore
elif len(attitude) != 4:
@@ -334,12 +334,13 @@ class Flix:
if not (0 <= thrust <= 1):
raise ValueError('Thrust must be in range [0, 1]')
attitude = self._flu_to_mavlink(attitude)
rates_extra = self._flu_to_mavlink(rates_extra)
for _ in range(2): # duplicate to ensure delivery
self.mavlink.set_attitude_target_send(0, self.system_id, 0, 0,
[attitude[0], attitude[1], attitude[2], attitude[3]],
0, 0, 0, thrust)
rates_extra[0], rates_extra[1], rates_extra[2], thrust)
def set_rates(self, rates: List[float], thrust: float):
def set_rates(self, rates: Sequence[float], thrust: float):
if len(rates) != 3:
raise ValueError('Rates must be [roll_rate, pitch_rate, yaw_rate]')
if not (0 <= thrust <= 1):
@@ -351,7 +352,7 @@ class Flix:
[1, 0, 0, 0],
rates[0], rates[1], rates[2], thrust)
def set_motors(self, motors: List[float]):
def set_motors(self, motors: Sequence[float]):
if len(motors) != 4:
raise ValueError('motors must have 4 values')
if not all(0 <= m <= 1 for m in motors):