68 Commits
Author SHA1 Message Date
Oleg Kalachev 74b082957e Add threshold for rc sticks move for quitting auto mode 2026-07-21 10:58:22 +03:00
Oleg Kalachev 4582942919 Minor docs update 2026-07-21 10:51:50 +03:00
Oleg Kalachev df14a5678f Don't send discovery message if encrypting is disabled in espnow 2026-07-20 19:01:38 +03:00
Oleg Kalachev 81721abf91 Disable log as it may crash the program 2026-07-20 10:16:30 +03:00
Oleg Kalachev 1276d220a6 Add tilt disarm failsafe 2026-07-18 21:35:13 +03:00
Oleg Kalachev 79c7e282cb Fix esp-now proxy 2026-07-18 14:25:19 +03:00
Oleg Kalachev f031cf3046 Fix printing espnow channel number in wifi command 2026-07-18 14:16:33 +03:00
Oleg Kalachev 60f389a289 Fix espnow proxy 2026-07-18 11:35:20 +03:00
Oleg Kalachev 5c24ce113d Minor docs update 2026-07-18 00:45:19 +03:00
Oleg Kalachev b58c2e7c7a Add auto channel search to espnow-proxy 2026-07-18 00:03:18 +03:00
Oleg Kalachev 2137730a8d Make espnow-proxy output more obvious 2026-07-18 00:03:01 +03:00
Oleg Kalachev 309d2d2e29 Add espnow-proxy build to ci 2026-07-17 23:42:28 +03:00
Oleg Kalachev 0c91ccfab5 Use STA interface in ESP-NOW proxy 2026-07-17 23:39:21 +03:00
Oleg Kalachev 26d53d4ce1 Docs fix 2026-07-17 17:18:07 +03:00
Oleg Kalachev 0366b24ff3 Minor docs fix 2026-07-17 17:06:22 +03:00
Oleg Kalachev 3de4e9ab9a Add info on uploading the firmware to ESP32-S3 Super Mini 2026-07-17 17:04:45 +03:00
Oleg Kalachev f1e024b19a Add instructions on how to set pin numbers for imu 2026-07-17 14:13:02 +03:00
Oleg Kalachev d9be26c5be Disable gyro notch filter by default 2026-07-16 23:21:17 +03:00
Oleg Kalachev f8a4cbfee1 Fix in docs 2026-07-16 17:48:16 +03:00
Oleg Kalachev 658040e652 Merge branch 'master' into robolager2026 2026-07-16 16:54:47 +03:00
Oleg Kalachev 70459df8dd Disable swarming in espnow proxy by default 2026-07-16 11:30:10 +03:00
Oleg Kalachev 61213bb8a1 Unset default motor pins (for safety) 2026-07-15 20:19:02 +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 8917849711 Remove unneeded declaration from the sim 2026-07-15 18:40:51 +03:00
Oleg Kalachev 1cdc7a8641 Fix rc in simulator
Virtual rc is disabled if rxRxPin < 0
2026-07-15 18:40:35 +03:00
Oleg Kalachev 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 d64bf24c6d Add alicanerus' build 2026-07-08 22:29:33 +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 fabd5e072d Fixes in the docs 2026-07-04 21:40:39 +03:00
Oleg Kalachev 1ae85ff118 Add argument to motor testing commands for specifying thrust 2026-07-02 22:22:33 +03:00
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
Oleg Kalachev 26a0dd65be Minor fixes and updates 2026-06-30 12:19:29 +03:00
Oleg Kalachev 9d439afd80 Store only changed parameters in flash instead of all
Rationale:
1. Parameters NVS storage is limited and storing unchanged parameters is wasteful.
2. This allows changing parameters in the code without having to erase the flash storage.
3. Updating the firmware version may change the parameter defaults.
2026-06-28 17:21:07 +03:00
Oleg Kalachev 64b21a3a6b Show default parameter values in p command output
Also move float comparing logic to a distinct util function.
2026-06-24 06:42:09 +03:00
Oleg Kalachev 545eed8944 Add Nerush and Konstantinos Paraskevas builds 2026-06-19 05:15:17 +03:00
Oleg Kalachev 518abf1555 Simplify rc code
Utilize the new method for getting channel values.
2026-06-14 04:45:42 +03:00
Oleg Kalachev 17df1c5396 Separate repository vscode settings and local vscode settings
Use dangmai.workspace-default-settings externsion for that.
2026-06-11 20:37:35 +03:00
Oleg Kalachev 8e2ffd7c69 Remove core installation when running the sim
Split `dependencies` target to `core` and `libs` targets.
Move additional urls declaration and connection timeout from arduino-cli.yaml to Makefile for simplicity and transparency.
Update ESP32 core url.
Remove arduino-cli.yaml.
2026-06-10 18:10:42 +03:00
Oleg Kalachev 52b74afba6 Bring back espnow tx buffering keeping resends disabled
Buffering is needed for sending large prints, otherwise the espnow internal buffer overflows.
Make onSent always think there was success send, so there won't be any resends.
Print lost espnow packets count on wifi console command.
2026-06-09 03:23:30 +03:00
Oleg Kalachev 0ca2473655 Make each mavlink message rate configured separately
Add parameters:
* MAV_RATE_ATT - ATTITUDE_QUATERNION rate.
* MAV_RATE_RC - RC_CHANNELS_RAW rate.
* MAV_RATE_MOT - ACTUATOR_CONTROL_TARGET rate.
* MAV_RATE_IMU - SCALED_IMU rate.
2026-06-09 02:59:10 +03:00
Oleg Kalachev b4c2fe3988 Print total RAM in sys command 2026-06-09 02:49:39 +03:00
Oleg Kalachev e51b47b798 Add command for setting the wifi mode easier 2026-06-09 02:44:09 +03:00
Oleg Kalachev 71abe1bcdb Minor changes
Simplify the code, print imu temperature
2026-06-09 02:25:20 +03:00
Oleg Kalachev 0f2e384ce6 Don't apply level correction on idle thrust 2026-06-09 02:12:27 +03:00
Oleg Kalachev 5e153a210d Update ESP32-Core to 3.3.10 2026-06-09 02:05:52 +03:00
Oleg Kalachev 9d47bcb82e Minor changes in docs 2026-05-31 18:10:00 +03:00
Oleg Kalachev 1fafc27b39 Make it possible to unassign motor pin using -1 parameter value 2026-05-30 16:57:27 +03:00
Oleg Kalachev faca48ced3 Replace ps and psq commands with st command + minor changes
Re-arrange commands order.
Make command parser consider \r in addition to \n.
2026-05-30 16:51:15 +03:00
Oleg Kalachev a5dbd2c829 Add erase command to makefile 2026-05-30 16:48:12 +03:00
Oleg Kalachev 59f9528d34 Increase buffer for print, make sys output more correct
usStackHighWaterMark is not stack size, it's the minimum stack size
2026-05-30 11:11:35 +03:00
Oleg Kalachev 607b2ff0b7 Add malagis custom pcb version of Flix project to builds 2026-05-28 20:15:09 +03:00
Oleg Kalachev 22c06f76c4 Add Awab Anas' build 2026-05-28 19:29:05 +03:00
Oleg Kalachev 488ceb3004 Set the debug level to error by default to see the errors 2026-05-28 19:25:29 +03:00
Oleg Kalachev b83c9b3845 Consider mavlink connected only when the gcs message is parsed 2026-05-28 18:41:34 +03:00
Oleg Kalachev 2f4b1423e6 Typo and minor code style changes 2026-05-28 18:39:44 +03:00
Oleg Kalachev 4e32414dae Support ESP-NOW connection in pyflix
Set arbitrary pymavlink connection string using device parameter or FLIX_DEVICE env variable.
pyflix@0.16.
2026-05-28 18:22:16 +03:00
Oleg Kalachev a294883dea Make p command show all parameters starting with the arg 2026-05-27 13:58:51 +03:00
Oleg Kalachev cdfba72a0b Fix simulator run
Add missing extern variables.
Fix warning.
2026-05-27 11:03:50 +03:00
Oleg Kalachev 18e81720e0 Add video of pcb version flights to the readme 2026-05-26 14:23:56 +03:00
Oleg Kalachev 91173d06c9 Various minor changes 2026-05-22 08:03:46 +03:00
Oleg KalachevandGitHub fdcc9533b3 Implement ESP-NOW support (#40) 2026-05-21 10:48:31 +03:00
51 changed files with 999 additions and 283 deletions
+2
View File
@@ -27,6 +27,8 @@ jobs:
run: make BOARD=esp32:esp32:esp32c3 run: make BOARD=esp32:esp32:esp32c3
- name: Build firmware for ESP32-S3 - name: Build firmware for ESP32-S3
run: make BOARD=esp32:esp32:esp32s3 run: make BOARD=esp32:esp32:esp32s3
- name: Build espnow-proxy
run: arduino-cli compile --fqbn esp32:esp32:esp32 tools/espnow-proxy
- name: Check c_cpp_properties.json - name: Check c_cpp_properties.json
run: tools/check_c_cpp_properties.py run: tools/check_c_cpp_properties.py
+3 -2
View File
@@ -4,9 +4,10 @@ build/
tools/log/ tools/log/
tools/dist/ tools/dist/
*.egg-info/ *.egg-info/
.dependencies .core
.libs
.vscode/* .vscode/*
!.vscode/settings.json !.vscode/settings.default.json
!.vscode/c_cpp_properties.json !.vscode/c_cpp_properties.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
+21 -21
View File
@@ -6,18 +6,18 @@
"${workspaceFolder}/flix", "${workspaceFolder}/flix",
"${workspaceFolder}/gazebo", "${workspaceFolder}/gazebo",
"${workspaceFolder}/tools/**", "${workspaceFolder}/tools/**",
"~/.arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32", "~/.arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32",
"~/.arduino15/packages/esp32/hardware/esp32/3.3.6/libraries/**", "~/.arduino15/packages/esp32/hardware/esp32/3.3.10/libraries/**",
"~/.arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32", "~/.arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32",
"~/.arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/**", "~/.arduino15/packages/esp32/tools/esp32-libs/3.3.10/include/**",
"~/Arduino/libraries/**", "~/Arduino/libraries/**",
"/usr/include/gazebo-11/", "/usr/include/gazebo-11/",
"/usr/include/ignition/math6/" "/usr/include/ignition/math6/"
], ],
"forcedInclude": [ "forcedInclude": [
"${workspaceFolder}/.vscode/intellisense.h", "${workspaceFolder}/.vscode/intellisense.h",
"~/.arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32/Arduino.h", "~/.arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32/Arduino.h",
"~/.arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32/pins_arduino.h", "~/.arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32/pins_arduino.h",
"${workspaceFolder}/flix/cli.ino", "${workspaceFolder}/flix/cli.ino",
"${workspaceFolder}/flix/control.ino", "${workspaceFolder}/flix/control.ino",
"${workspaceFolder}/flix/estimate.ino", "${workspaceFolder}/flix/estimate.ino",
@@ -33,7 +33,7 @@
"${workspaceFolder}/flix/parameters.ino", "${workspaceFolder}/flix/parameters.ino",
"${workspaceFolder}/flix/safety.ino" "${workspaceFolder}/flix/safety.ino"
], ],
"compilerPath": "~/.arduino15/packages/esp32/tools/esp-x32/2511/bin/xtensa-esp32-elf-g++", "compilerPath": "~/.arduino15/packages/esp32/tools/esp-x32/2601/bin/xtensa-esp32-elf-g++",
"cStandard": "c11", "cStandard": "c11",
"cppStandard": "c++17", "cppStandard": "c++17",
"defines": [ "defines": [
@@ -53,18 +53,18 @@
"name": "Mac", "name": "Mac",
"includePath": [ "includePath": [
"${workspaceFolder}/flix", "${workspaceFolder}/flix",
"~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32", "~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32",
"~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.6/libraries/**", "~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.10/libraries/**",
"~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32", "~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32",
"~/Library/Arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/**", "~/Library/Arduino15/packages/esp32/tools/esp32-libs/3.3.10/include/**",
"~/Documents/Arduino/libraries/**", "~/Documents/Arduino/libraries/**",
"/opt/homebrew/include/gazebo-11/", "/opt/homebrew/include/gazebo-11/",
"/opt/homebrew/include/ignition/math6/" "/opt/homebrew/include/ignition/math6/"
], ],
"forcedInclude": [ "forcedInclude": [
"${workspaceFolder}/.vscode/intellisense.h", "${workspaceFolder}/.vscode/intellisense.h",
"~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32/Arduino.h", "~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32/Arduino.h",
"~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32/pins_arduino.h", "~/Library/Arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32/pins_arduino.h",
"${workspaceFolder}/flix/flix.ino", "${workspaceFolder}/flix/flix.ino",
"${workspaceFolder}/flix/cli.ino", "${workspaceFolder}/flix/cli.ino",
"${workspaceFolder}/flix/control.ino", "${workspaceFolder}/flix/control.ino",
@@ -80,7 +80,7 @@
"${workspaceFolder}/flix/parameters.ino", "${workspaceFolder}/flix/parameters.ino",
"${workspaceFolder}/flix/safety.ino" "${workspaceFolder}/flix/safety.ino"
], ],
"compilerPath": "~/Library/Arduino15/packages/esp32/tools/esp-x32/2511/bin/xtensa-esp32-elf-g++", "compilerPath": "~/Library/Arduino15/packages/esp32/tools/esp-x32/2601/bin/xtensa-esp32-elf-g++",
"cStandard": "c11", "cStandard": "c11",
"cppStandard": "c++17", "cppStandard": "c++17",
"defines": [ "defines": [
@@ -103,16 +103,16 @@
"${workspaceFolder}/flix", "${workspaceFolder}/flix",
"${workspaceFolder}/gazebo", "${workspaceFolder}/gazebo",
"${workspaceFolder}/tools/**", "${workspaceFolder}/tools/**",
"~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32", "~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32",
"~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.6/libraries/**", "~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.10/libraries/**",
"~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32", "~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32",
"~/AppData/Local/Arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/**", "~/AppData/Local/Arduino15/packages/esp32/tools/esp32-libs/3.3.10/include/**",
"~/Documents/Arduino/libraries/**" "~/Documents/Arduino/libraries/**"
], ],
"forcedInclude": [ "forcedInclude": [
"${workspaceFolder}/.vscode/intellisense.h", "${workspaceFolder}/.vscode/intellisense.h",
"~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32/Arduino.h", "~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.10/cores/esp32/Arduino.h",
"~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.6/variants/d1_mini32/pins_arduino.h", "~/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.3.10/variants/d1_mini32/pins_arduino.h",
"${workspaceFolder}/flix/cli.ino", "${workspaceFolder}/flix/cli.ino",
"${workspaceFolder}/flix/control.ino", "${workspaceFolder}/flix/control.ino",
"${workspaceFolder}/flix/estimate.ino", "${workspaceFolder}/flix/estimate.ino",
@@ -128,7 +128,7 @@
"${workspaceFolder}/flix/parameters.ino", "${workspaceFolder}/flix/parameters.ino",
"${workspaceFolder}/flix/safety.ino" "${workspaceFolder}/flix/safety.ino"
], ],
"compilerPath": "~/AppData/Local/Arduino15/packages/esp32/tools/esp-x32/2511/bin/xtensa-esp32-elf-g++.exe", "compilerPath": "~/AppData/Local/Arduino15/packages/esp32/tools/esp-x32/2601/bin/xtensa-esp32-elf-g++.exe",
"cStandard": "c11", "cStandard": "c11",
"cppStandard": "c++17", "cppStandard": "c++17",
"defines": [ "defines": [
+1
View File
@@ -1,6 +1,7 @@
{ {
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
"recommendations": [ "recommendations": [
"dangmai.workspace-default-settings",
"ms-vscode.cpptools", "ms-vscode.cpptools",
"ms-vscode.cmake-tools", "ms-vscode.cmake-tools",
"ms-python.python" "ms-python.python"
+18 -10
View File
@@ -1,24 +1,32 @@
BOARD = esp32:esp32:d1_mini32 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*))
build: .dependencies export ARDUINO_NETWORK_CONNECTION_TIMEOUT := 1h
arduino-cli compile --fqbn $(BOARD) flix
build: .core .libs
arduino-cli compile --fqbn $(BOARD) --build-property "build.core_debug_level=1" flix
upload: build upload: build
arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" flix arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" flix
erase:
arduino-cli burn-bootloader --fqbn $(BOARD) -p "$(PORT)" -P esptool
monitor: monitor:
arduino-cli monitor -p "$(PORT)" -c baudrate=115200 arduino-cli monitor -p "$(PORT)" -c baudrate=115200
dependencies .dependencies: core .core:
arduino-cli core update-index --config-file arduino-cli.yaml arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
arduino-cli core install esp32:esp32@3.3.6 --config-file arduino-cli.yaml arduino-cli core install esp32:esp32@3.3.10 --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
touch .core
libs .libs:
arduino-cli lib update-index arduino-cli lib update-index
arduino-cli lib install "FlixPeriph" arduino-cli lib install "FlixPeriph"
arduino-cli lib install "MAVLink"@2.0.25 arduino-cli lib install "MAVLink"@2.0.25
touch .dependencies touch .libs
upload_proxy: .dependencies upload_proxy: .core .libs
arduino-cli compile --fqbn $(BOARD) tools/espnow-proxy arduino-cli compile --fqbn $(BOARD) tools/espnow-proxy
arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" tools/espnow-proxy arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" tools/espnow-proxy
@@ -26,7 +34,7 @@ gazebo/build cmake: gazebo/CMakeLists.txt
mkdir -p gazebo/build mkdir -p gazebo/build
cd gazebo/build && cmake .. cd gazebo/build && cmake ..
build_simulator: .dependencies gazebo/build build_simulator: .libs gazebo/build
make -C gazebo/build make -C gazebo/build
simulator: build_simulator simulator: build_simulator
@@ -41,6 +49,6 @@ plot:
plotjuggler -d $(shell ls -t tools/log/*.csv | head -n1) plotjuggler -d $(shell ls -t tools/log/*.csv | head -n1)
clean: clean:
rm -rf gazebo/build flix/build flix/cache .dependencies rm -rf gazebo/build flix/build flix/cache .core .libs
.PHONY: build upload monitor dependencies cmake build_simulator simulator log clean .PHONY: build upload monitor core libs cmake build_simulator simulator log clean
+10 -2
View File
@@ -47,6 +47,14 @@ See the [user builds gallery](docs/user.md):
<a href="docs/user.md"><img src="docs/img/user/user.jpg" width=500></a> <a href="docs/user.md"><img src="docs/img/user/user.jpg" width=500></a>
### PCB
The official PCB *(Flix2)* is in development now. Follow the [project's channel](https://t.me/opensourcequadcopter) to track the progress.
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>
## 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:
@@ -73,10 +81,10 @@ Additional articles:
|-|-|:-:|:-:| |-|-|:-:|:-:|
|Microcontroller board|ESP32 Mini.<br>ESP32-S3/ESP32-C3 boards are also supported.|<img src="docs/img/esp32.jpg" width=100>|1| |Microcontroller board|ESP32 Mini.<br>ESP32-S3/ESP32-C3 boards are also supported.|<img src="docs/img/esp32.jpg" width=100>|1|
|IMU (and barometer¹) board|GY91, MPU-9265 (or other MPU9250/MPU6500 board)<br>ICM20948V2 (ICM20948)<br>GY-521 (MPU-6050)|<img src="docs/img/gy-91.jpg" width=90 align=center><br><img src="docs/img/icm-20948.jpg" width=100><br><img src="docs/img/gy-521.jpg" width=100>|1| |IMU (and barometer¹) board|GY91, MPU-9265 (or other MPU9250/MPU6500 board)<br>ICM20948V2 (ICM20948)<br>GY-521 (MPU-6050)|<img src="docs/img/gy-91.jpg" width=90 align=center><br><img src="docs/img/icm-20948.jpg" width=100><br><img src="docs/img/gy-521.jpg" width=100>|1|
|Boost converter (optional, for more stable power supply)|5V output|<img src="docs/img/buck-boost.jpg" width=100>|1| |*Boost converter (optional, for more stable power supply)*|*5V output*|<img src="docs/img/buck-boost.jpg" width=100>|1|
|Motor|8520 3.7V brushed motor.<br>Motor with exact 3.7V voltage is needed, not ranged working voltage (3.7V — 6V).<br>Make sure the motor shaft diameter and propeller hole diameter match!|<img src="docs/img/motor.jpeg" width=100>|4| |Motor|8520 3.7V brushed motor.<br>Motor with exact 3.7V voltage is needed, not ranged working voltage (3.7V — 6V).<br>Make sure the motor shaft diameter and propeller hole diameter match!|<img src="docs/img/motor.jpeg" width=100>|4|
|Propeller|55 mm or 65 mm|<img src="docs/img/prop.jpg" width=100>|4| |Propeller|55 mm or 65 mm|<img src="docs/img/prop.jpg" width=100>|4|
|MOSFET (transistor)|100N03A or [analog](https://t.me/opensourcequadcopter/33)|<img src="docs/img/100n03a.jpg" width=100>|4| |MOSFET (transistor)|UMW 100N03A or [analog](https://t.me/opensourcequadcopter/33).<br>Warning: don't use KIA 100N03A or other manufacturers, they might not work!|<img src="docs/img/100n03a.jpg" width=100>|4|
|Pull-down resistor<br>Voltage measurement resistor|10 kΩ|<img src="docs/img/resistor10k.jpg" width=100>|6| |Pull-down resistor<br>Voltage measurement resistor|10 kΩ|<img src="docs/img/resistor10k.jpg" width=100>|6|
|3.7V Li-Po battery|LW 952540 (or any compatible by the size).<br>Make sure the battery has enough discharge rate — 25C or more!|<img src="docs/img/battery.jpg" width=100>|1| |3.7V Li-Po battery|LW 952540 (or any compatible by the size).<br>Make sure the battery has enough discharge rate — 25C or more!|<img src="docs/img/battery.jpg" width=100>|1|
|Battery connector cable|MX2.0 2P female|<img src="docs/img/mx.png" width=100>|1| |Battery connector cable|MX2.0 2P female|<img src="docs/img/mx.png" width=100>|1|
-5
View File
@@ -1,5 +0,0 @@
board_manager:
additional_urls:
- https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
network:
connection_timeout: 1h
+3
View File
@@ -79,6 +79,9 @@ To add a new parameter:
See examples of adding new parameters in commits: [c434107](https://github.com/okalachev/flix/commit/c434107), [a687303](https://github.com/okalachev/flix/commit/a687303). See examples of adding new parameters in commits: [c434107](https://github.com/okalachev/flix/commit/c434107), [a687303](https://github.com/okalachev/flix/commit/a687303).
> [!NOTE]
> Since all the parameters are internally stored and passed as floats, the safe range for `int` parameters is -16777216 to 16777215.
## Adding a subsystem ## Adding a subsystem
To add a new subsystem: To add a new subsystem:
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+1 -1
View File
@@ -5,7 +5,7 @@
Do the following: Do the following:
* **Check ESP32 core is installed**. Check if the version matches the one used in the [tutorial](usage.md#building-the-firmware). * **Check ESP32 core is installed**. Check if the version matches the one used in the [tutorial](usage.md#building-the-firmware).
* **Check libraries**. Install all the required libraries from the tutorial. Make sure there are no MPU9250 or other peripherals libraries that may conflict with the ones used in the tutorial. * **Check libraries**. Install all the required libraries from the tutorial. Make sure there are no MPU-9250 or other peripherals libraries that may conflict with the ones used in the tutorial.
* **Check the chosen board**. The correct board to choose in Arduino IDE for ESP32 Mini is *WEMOS D1 MINI ESP32*. * **Check the chosen board**. The correct board to choose in Arduino IDE for ESP32 Mini is *WEMOS D1 MINI ESP32*.
## The drone doesn't fly ## The drone doesn't fly
+42 -12
View File
@@ -20,13 +20,14 @@ You can build and upload the firmware using either **Arduino IDE** (easier for b
1. Install [Arduino IDE](https://www.arduino.cc/en/software) (version 2 is recommended). 1. Install [Arduino IDE](https://www.arduino.cc/en/software) (version 2 is recommended).
2. *Windows users might need to install [USB to UART bridge driver from Silicon Labs](https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers).* 2. *Windows users might need to install [USB to UART bridge driver from Silicon Labs](https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers).*
3. Install ESP32 core, version 3.3.6. See the [official Espressif's instructions](https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html#installing-using-arduino-ide) on installing ESP32 Core in Arduino IDE. 3. Install ESP32 core, version 3.3.10. See the [official Espressif's instructions](https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html#installing-using-arduino-ide) on installing ESP32 Core in Arduino IDE.
4. Install the following libraries using [Library Manager](https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-installing-a-library): 4. Install the following libraries using [Library Manager](https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-installing-a-library):
* `FlixPeriph`, the latest version. * `FlixPeriph`, the latest version.
* `MAVLink`, version 2.0.25. * `MAVLink`, version 2.0.25.
5. Open the `flix/flix.ino` sketch from downloaded firmware sources in Arduino IDE. 5. Open the `flix/flix.ino` sketch from downloaded firmware sources in Arduino IDE.
6. Connect your ESP32 board to the computer and choose correct board type in Arduino IDE (*WEMOS D1 MINI ESP32* for ESP32 Mini) and the port. 6. Connect your ESP32 board to the computer and choose correct board type in Arduino IDE (*WEMOS D1 MINI ESP32* for ESP32 Mini, *ESP32S3 Dev Module* for ESP32-S3 Super Mini) and the port.
7. [Build and upload](https://docs.arduino.cc/software/ide-v2/tutorials/getting-started/ide-v2-uploading-a-sketch) the firmware using Arduino IDE. 7. Set *Tools**Core Debug Level* to *Error* to see the errors in the serial console. Set *Tools**USB CDC on Boot* to *Enabled* for ESP32-S3/ESP32-C3 boards.
8. [Build and upload](https://docs.arduino.cc/software/ide-v2/tutorials/getting-started/ide-v2-uploading-a-sketch) the firmware using Arduino IDE.
### Command line (Windows, Linux, macOS) ### Command line (Windows, Linux, macOS)
@@ -57,6 +58,12 @@ You can build and upload the firmware using either **Arduino IDE** (easier for b
make upload monitor make upload monitor
``` ```
For ESP32-S3/ESP32-C3 boards, set the appropriate [FQBN](https://docs.arduino.cc/arduino-cli/FAQ/#whats-the-fqbn-string) using `BOARD` parameter:
```bash
make BOARD=esp32:esp32:esp32s3:FlashSize=4M,CDCOnBoot=cdc upload
```
See other available Make commands in [Makefile](../Makefile). See other available Make commands in [Makefile](../Makefile).
> [!TIP] > [!TIP]
@@ -69,8 +76,25 @@ See other available Make commands in [Makefile](../Makefile).
In case if using different IMU model than MPU9250, change `imu` variable declaration in the `imu.ino`: In case if using different IMU model than MPU9250, change `imu` variable declaration in the `imu.ino`:
```cpp ```cpp
ICM20948 imu(SPI); // For ICM-20948 ICM20948 imu(SPI); // For ICM-20948 via SPI
MPU6050 imu(Wire); // For MPU-6050 // or
MPU6050 imu(Wire); // For MPU-6050 via I2C
```
If using non-default SPI pins, pass SCK, MISO, and MOSI pin numbers to `SPI.begin` call and SS pin to `imu` constructor like that:
```cpp
ICM20948(SPI, <SS>);
// ...
SPI.begin(<SCK>, <MISO>, <MOSI>);
imu.begin();
```
If using non-default I2C pins, pass SDA and SCL pin numbers to `Wire.setPins` call like that:
```cpp
Wire.begin(<SDA>, <SCL>);
imu.begin();
``` ```
### Connect using QGroundControl ### Connect using QGroundControl
@@ -82,6 +106,9 @@ QGroundControl is a ground control station software that can be used to monitor
3. Connect your computer or smartphone to the appeared `flix` Wi-Fi network (password: `flixwifi`). 3. Connect your computer or smartphone to the appeared `flix` Wi-Fi network (password: `flixwifi`).
4. Launch QGroundControl app. It should connect and begin showing the drone's telemetry automatically. 4. Launch QGroundControl app. It should connect and begin showing the drone's telemetry automatically.
> [!TIP]
> If QGroundControl doesn't connect, try to disable the firewall and/or VPN on your computer, as they may block the connection.
### Access console ### Access console
The console is a command line interface (CLI) that allows to interact with the drone, change parameters, and perform various actions. There are two ways of accessing the console: using **serial port** or using **QGroundControl (wirelessly)**. The console is a command line interface (CLI) that allows to interact with the drone, change parameters, and perform various actions. There are two ways of accessing the console: using **serial port** or using **QGroundControl (wirelessly)**.
@@ -95,7 +122,7 @@ To access the console using serial port:
To access the console using QGroundControl: To access the console using QGroundControl:
1. Connect to the drone using QGroundControl app. 1. Connect to the drone using QGroundControl app.
2. Go to the QGroundControl menu ⇒ *Vehicle Setup* ⇒ *Analyze Tools* ⇒ *MAVLink Console*. 2. Go to the QGroundControl menu ⇒ *Analyze Tools* ⇒ *MAVLink Console*.
<img src="img/cli.png" width="400"> <img src="img/cli.png" width="400">
@@ -188,7 +215,7 @@ After this setup, you should see the battery voltage in QGroundControl top panel
## Setup remote control ## Setup remote control
There are several ways to control the drone's flight: using **smartphone** (Wi-Fi), using **SBUS remote control**, or using **USB remote control** (Wi-Fi). There are several ways to control the drone's flight: using **smartphone** (Wi-Fi), using **SBUS remote control**, or using **USB remote control** (Wi-Fi/ESP-NOW).
### Control with a smartphone ### Control with a smartphone
@@ -233,7 +260,7 @@ If your drone doesn't have RC receiver installed, you can use USB remote control
3. Power up the drone. 3. Power up the drone.
4. Connect your computer to the appeared `flix` Wi-Fi network (password: `flixwifi`). 4. Connect your computer to the appeared `flix` Wi-Fi network (password: `flixwifi`).
5. Launch QGroundControl app. It should connect and begin showing the drone's telemetry automatically. 5. Launch QGroundControl app. It should connect and begin showing the drone's telemetry automatically.
6. Go the the QGroundControl menu ⇒ *Vehicle Setup* ⇒ *Joystick*. Calibrate you USB remote control there. 6. Go to the QGroundControl menu ⇒ *Vehicle Setup* ⇒ *Joystick*. Calibrate your USB remote control there.
7. Use the USB remote control to fly the drone! 7. Use the USB remote control to fly the drone!
## Flight ## Flight
@@ -323,13 +350,13 @@ To setup ESP-NOW communication:
1. Flash the second ESP32 board with ESP-NOW proxy sketch: [`tools/espnow-proxy/espnow-proxy.ino`](../tools/espnow-proxy/espnow-proxy.ino). Use Arduino IDE or command line: `make upload_proxy`. 1. Flash the second ESP32 board with ESP-NOW proxy sketch: [`tools/espnow-proxy/espnow-proxy.ino`](../tools/espnow-proxy/espnow-proxy.ino). Use Arduino IDE or command line: `make upload_proxy`.
2. Open Serial Monitor or use `make monitor` command. The ESP32 will print its MAC address and generated encryption key, for example: 2. Open Serial Monitor in Arduino IDE or use `make monitor` command. The ESP32 will print its MAC address and generated encryption key, for example:
``` ```
espnow 7a:c8:e3:eb:bf:e9 &PiuSysxP9+$L&5E espnow 7a:c8:e3:eb:bf:e9 &PiuSysxP9+$L&5E
``` ```
Run this line as a console command on each drone you want to bind to this proxy board. Run this line as a console command on each drone you want to bind to this proxy board. [The maximum number](https://github.com/espressif/esp-idf/blob/e95cab4be8fd293e3f3323181e7a2280874da6f7/components/esp_wifi/include/esp_now.h#L32-L33) of simultaneously connected drones is 20 (unencrypted) or 6 (encrypted).
3. Set the `WIFI_MODE` parameter to `3` on the drone: 3. Set the `WIFI_MODE` parameter to `3` on the drone:
@@ -342,11 +369,14 @@ To setup ESP-NOW communication:
* Type: Serial. * Type: Serial.
* Serial Port: choose the port of the proxy ESP32 board, e. g. `/dev/cu.usbserial-0001`. * Serial Port: choose the port of the proxy ESP32 board, e. g. `/dev/cu.usbserial-0001`.
* Baud Rate: 115200. * Baud Rate: 115200.
5. Click *Save*. QGroundControl should connect to the drone using ESP-NOW and begin showing the telemetry. 5. Click *Save*, click *Connect*. QGroundControl should connect to the drone using ESP-NOW and begin showing the telemetry.
> [!TIP]
> Make sure Arduino IDE is not running when using ESP-NOW proxy board, as it may block it.
## Flight log ## Flight log
After the flight, you can download the flight log for analysis wirelessly. Use the following command on your computer for that: After the flight, you can download the flight log wirelessly for analysis. Use the following command on your computer for that:
```bash ```bash
make log make log
+51
View File
@@ -4,6 +4,46 @@ This page contains user-built drones based on the Flix project. Publish your pro
--- ---
Author: Alican Erüst.<br>
Description: QX95 mm frame, 55 mm propellers, 3.7 V 25C 1050 mAh LiPo battery, MPU6050 IMU, Logitech F310 gamepad controller, with a total quadcopter weight of 66 g.
<img src="img/user/alicanerus/1.jpg" height=200> <img src="img/user/alicanerus/2.jpg" height=200> <img src="img/user/alicanerus/3.jpg" height=200>
[Flight video](https://drive.google.com/file/d/1k0WeWTKnCAfaugkX7LcmNxsUuq79RL8Z/view?usp=sharing).
---
Author: [Неруш Михаил](https://t.me/NerushMV).<br>
Description: custom frame made of 4 mm plywood, 8520 brushed motors, 75 mm propellers, MPU-6500. FlySky FS-i6X with ESP32-based adapter for ESP-NOW communication (using PPM output).<br>
Sources and materials: [link](https://drive.google.com/drive/folders/1uWiDcuorLrtVs_IIR7Y13omij-7Q1nx8).
<img src="img/user/nerush/1.jpg" height=200> <img src="img/user/nerush/2.jpg" height=200>
[Flight video](https://drive.google.com/file/d/1jRXeGx34lJpUfw0GKLQeIzkWZvooQJSE/view?usp=sharing).
---
Author: [Konstantinos Paraskevas](https://github.com/Frapais).<br>
Description: drone with a custom single-boarded airframe, extending the [Sprig-C3 module](https://github.com/Frapais/Sprig-C3).
ESP32-C3 microcontroller, ICM-20948 IMU, on-board fuel-gauge, status LED indicator.<br>
Repository with all the code and PCB sources: https://github.com/Frapais/Sprig-Drone.
<img src="img/user/kostas/1.jpg" height=150> <img src="img/user/kostas/2.jpg" height=150>
Detailed video about making the drone:
<a href="https://youtu.be/82Q-uBq6s48"><img width=400 src="https://i3.ytimg.com/vi/82Q-uBq6s48/maxresdefault.jpg"></a>
---
Author: [Awab Anas](http://t.me/AW_VENOM).<br>
Description: ESP32 D1 Mini, MPU-6050, 8520 3.7V brushed motors, 55 mm propellers, battery li-po 1200 mAh, controlling via [Mavlink Joystick app](https://github.com/goldarte/mavlink-joystick/releases/latest).<br>
[Flight validation](https://drive.google.com/file/d/12z0jfctZDBA6b5UKCG0Uje5rAxj6DhF-/view?usp=sharing).
<img src="img/user/aw_venom/1.jpg" height=200>
---
Author: [Ina Tix](https://t.me/ina_tix).<br> Author: [Ina Tix](https://t.me/ina_tix).<br>
Description: XR2981 based DC-DC converter, ELRS MINI 2.4GHz RX SX1280 receiver (SBUS interface), Radiomaster TX12 remote control.<br> Description: XR2981 based DC-DC converter, ELRS MINI 2.4GHz RX SX1280 receiver (SBUS interface), Radiomaster TX12 remote control.<br>
[Flight validation](https://drive.google.com/file/d/1yqkKNuz4R_yxGqUNQxVpixJbXqEEcUSj/view?usp=share_link). [Flight validation](https://drive.google.com/file/d/1yqkKNuz4R_yxGqUNQxVpixJbXqEEcUSj/view?usp=share_link).
@@ -57,6 +97,17 @@ Author: [goldarte](https://t.me/goldarte).<br>
--- ---
Author: [malagis](https://oshwhub.com/malagis).<br>
A Chinese custom PCB version of Flix with a big community of users, lots of materials and modifications.
Main project's page: https://oshwhub.com/malagis/esp32-mini-plane.<br>
Video about the project: https://www.bilibili.com/video/BV14vyqBFEJn/.
<img src="img/user/malagis/1.jpg" height=200> <img src="img/user/malagis/2.jpg" height=200> <img src="img/user/malagis/3.jpg" height=200>
---
## School 548 course ## School 548 course
Special course on quadcopter design and engineering took place in october-november 2025 in School 548, Moscow. The course included UAV control theory, electronics, drone assembly and setup practice, using the Flix project. Special course on quadcopter design and engineering took place in october-november 2025 in School 548, Moscow. The course included UAV control theory, electronics, drone assembly and setup practice, using the Flix project.
+48 -30
View File
@@ -6,7 +6,7 @@
#include "pid.h" #include "pid.h"
#include "vector.h" #include "vector.h"
#include "util.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 MOTOR_REAR_LEFT, MOTOR_REAR_RIGHT, MOTOR_FRONT_RIGHT, MOTOR_FRONT_LEFT;
extern const int RAW, ACRO, STAB, AUTO; extern const int RAW, ACRO, STAB, AUTO;
@@ -31,33 +31,39 @@ const char* motd =
"Commands:\n\n" "Commands:\n\n"
"help - show help\n" "help - show help\n"
"p - show all parameters\n" "p - show all parameters\n"
"p <name> - show parameter\n" "p <str> - show parameters starting with str\n"
"p <name> <value> - set parameter\n" "p <name> <value> - set parameter\n"
"preset - reset parameters\n" "preset - reset parameters\n"
"time - show time info\n" "time - show time info\n"
"ps - show pitch/roll/yaw\n"
"psq - show attitude quaternion\n"
"imu - show IMU data\n" "imu - show IMU data\n"
"ca - calibrate accel\n"
"st - show state estimation\n"
"arm - arm the drone\n" "arm - arm the drone\n"
"disarm - disarm the drone\n" "disarm - disarm the drone\n"
"raw/stab/acro/auto - set mode\n" "raw/stab/acro/auto - set mode\n"
"rc - show RC data\n" "rc - show RC data\n"
"cr - calibrate RC\n"
"pw - show power info\n" "pw - show power info\n"
"wifi - show Wi-Fi info\n" "wifi - show Wi-Fi info\n"
"ap <ssid> <password> - setup Wi-Fi access point\n" "wifi ap/sta/espnow/off - set Wi-Fi mode\n"
"sta <ssid> <password> - setup Wi-Fi client mode\n" "ap <ssid> <password> - configure Wi-Fi access point\n"
"espnow <mac> [<key>] - setup ESP-NOW peer\n" "sta <ssid> <password> - configure Wi-Fi client mode\n"
"espnow <mac> [<key>] - configure ESP-NOW peer\n"
"mot - show motor output\n" "mot - show motor output\n"
"mfr/mfl/mrr/mrl [<thrust>] - test motor (remove props)\n"
"log [dump] - print log header [and data]\n" "log [dump] - print log header [and data]\n"
"cr - calibrate RC\n" "log - show log info\n"
"ca - calibrate accel\n" "log header - show log header\n"
"mfr, mfl, mrr, mrl - test motor (remove props)\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";
void print(const char* format, ...) { void print(const char* format, ...) {
char buf[1000]; char buf[3000];
va_list args; va_list args;
va_start(args, format); va_start(args, format);
vsnprintf(buf, sizeof(buf), format, args); vsnprintf(buf, sizeof(buf), format, args);
@@ -92,10 +98,8 @@ void doCommand(String str, bool echo = false) {
// execute command // execute command
if (command == "help" || command == "motd") { if (command == "help" || command == "motd") {
print("%s\n", motd); print("%s\n", motd);
} else if (command == "p" && arg0 == "") { } else if (command == "p" && arg1 == "") {
printParameters(); printParameters(arg0.c_str());
} else if (command == "p" && arg0 != "" && arg1 == "") {
print("%s = %g\n", arg0.c_str(), getParameter(arg0.c_str()));
} else if (command == "p") { } else if (command == "p") {
bool success = setParameter(arg0.c_str(), arg1.toFloat()); bool success = setParameter(arg0.c_str(), arg1.toFloat());
if (success) { if (success) {
@@ -109,15 +113,15 @@ void doCommand(String str, bool echo = false) {
print("Time: %f\n", t); print("Time: %f\n", t);
print("Loop rate: %.0f\n", loopRate); print("Loop rate: %.0f\n", loopRate);
print("dt: %f\n", dt); print("dt: %f\n", dt);
} else if (command == "ps") {
Vector a = attitude.toEuler();
print("roll: %f pitch: %f yaw: %f\n", degrees(a.x), degrees(a.y), degrees(a.z));
} else if (command == "psq") {
print("qw: %f qx: %f qy: %f qz: %f\n", attitude.w, attitude.x, attitude.y, attitude.z);
} else if (command == "imu") { } else if (command == "imu") {
printIMUInfo(); printIMUInfo();
printIMUCalibration(); printIMUCalibration();
print("landed: %d\n", landed); print("landed: %d\n", landed);
} else if (command == "st") {
print("rates: %g %g %g\n", rates.x, rates.y, rates.z);
print("attitude: %g %g %g %g\n", attitude.w, attitude.x, attitude.y, attitude.z);
print("roll: %g° pitch: %g° yaw: %g°\n", degrees(attitude.getRoll()), degrees(attitude.getPitch()), degrees(attitude.getYaw()));
print("landed: %d\n", landed);
} else if (command == "arm") { } else if (command == "arm") {
armed = true; armed = true;
} else if (command == "disarm") { } else if (command == "disarm") {
@@ -142,8 +146,10 @@ void doCommand(String str, bool echo = false) {
print("armed: %d\n", armed); print("armed: %d\n", armed);
} else if (command == "pw") { } else if (command == "pw") {
print("Voltage: %.1f V\n", voltage); print("Voltage: %.1f V\n", voltage);
} else if (command == "wifi") { } else if (command == "wifi" && arg0 == "") {
printWiFiInfo(); printWiFiInfo();
} else if (command == "wifi") {
setWiFiMode(arg0);
} else if (command == "ap") { } else if (command == "ap") {
configWiFi(W_AP, arg0.c_str(), arg1.c_str()); configWiFi(W_AP, arg0.c_str(), arg1.c_str());
} else if (command == "sta") { } else if (command == "sta") {
@@ -153,29 +159,41 @@ 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); testMotor(MOTOR_FRONT_RIGHT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "mfl") { } else if (command == "mfl") {
testMotor(MOTOR_FRONT_LEFT); testMotor(MOTOR_FRONT_LEFT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "mrr") { } else if (command == "mrr") {
testMotor(MOTOR_REAR_RIGHT); testMotor(MOTOR_REAR_RIGHT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "mrl") { } else if (command == "mrl") {
testMotor(MOTOR_REAR_LEFT); testMotor(MOTOR_REAR_LEFT, arg0.isEmpty() ? 0.2 : arg0.toFloat());
} else if (command == "sys") { } else if (command == "sys") {
#ifdef ESP32 #ifdef ESP32
print("Chip: %s\n", ESP.getChipModel()); print("Chip: %s\n", ESP.getChipModel());
print("Temperature: %.1f °C\n", temperatureRead()); print("Temperature: %.1f °C\n", temperatureRead());
print("Free heap: %d\n", ESP.getFreeHeap()); print("Total RAM: %d KB\n", ESP.getHeapSize() / 1024);
print("Free heap: %d KB\n", ESP.getFreeHeap() / 1024);
print("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 Stack Prio Core CPU%%\n"); print("Num Task MinSt Prio Core CPU%%\n");
int taskCount = uxTaskGetNumberOfTasks(); int taskCount = uxTaskGetNumberOfTasks();
TaskStatus_t *systemState = new TaskStatus_t[taskCount]; TaskStatus_t *systemState = new TaskStatus_t[taskCount];
uint32_t totalRunTime; uint32_t totalRunTime;
@@ -209,7 +227,7 @@ void handleInput() {
while (Serial.available()) { while (Serial.available()) {
char c = Serial.read(); char c = Serial.read();
if (c == '\n') { if (c == '\n' || c == '\r') {
doCommand(input); doCommand(input);
input.clear(); input.clear();
} else { } else {
+2 -1
View File
@@ -6,7 +6,7 @@
#include "vector.h" #include "vector.h"
#include "quaternion.h" #include "quaternion.h"
#include "pid.h" #include "pid.h"
#include "lpf.h" #include "filter.h"
#include "util.h" #include "util.h"
#define PITCHRATE_P 0.05 #define PITCHRATE_P 0.05
@@ -149,6 +149,7 @@ void controlTorque() {
motors[MOTOR_REAR_LEFT] = thrustTarget + torqueTarget.x + torqueTarget.y - torqueTarget.z; motors[MOTOR_REAR_LEFT] = thrustTarget + torqueTarget.x + torqueTarget.y - torqueTarget.z;
motors[MOTOR_REAR_RIGHT] = thrustTarget - torqueTarget.x + torqueTarget.y + torqueTarget.z; motors[MOTOR_REAR_RIGHT] = thrustTarget - torqueTarget.x + torqueTarget.y + torqueTarget.z;
// Prioritize angle control over thrust control
desaturate(motors[MOTOR_FRONT_LEFT], motors[MOTOR_FRONT_RIGHT], motors[MOTOR_REAR_LEFT], motors[MOTOR_REAR_RIGHT]); desaturate(motors[MOTOR_FRONT_LEFT], motors[MOTOR_FRONT_RIGHT], motors[MOTOR_REAR_LEFT], motors[MOTOR_REAR_RIGHT]);
motors[0] = constrain(motors[0], 0, 1); motors[0] = constrain(motors[0], 0, 1);
+10 -3
View File
@@ -5,7 +5,7 @@
#include "quaternion.h" #include "quaternion.h"
#include "vector.h" #include "vector.h"
#include "lpf.h" #include "filter.h"
#include "util.h" #include "util.h"
Vector rates; // estimated angular rates, rad/s Vector rates; // estimated angular rates, rad/s
@@ -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, 0);
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));
@@ -32,8 +39,7 @@ void applyGyro() {
void applyAcc() { void applyAcc() {
// test should we apply accelerometer gravity correction // test should we apply accelerometer gravity correction
float accNorm = acc.norm(); landed = !motorsActive() && abs(acc.norm() - ONE_G) < ONE_G * 0.1f;
landed = !motorsActive() && abs(accNorm - ONE_G) < ONE_G * 0.1f;
if (!landed) return; if (!landed) return;
@@ -47,6 +53,7 @@ void applyAcc() {
void applyLevel() { void applyLevel() {
if (landed) return; if (landed) return;
if (thrustTarget < 0.1) return; // skip at idle thrust
// assume the pilot keeps the drone more or less level in flight // assume the pilot keeps the drone more or less level in flight
Vector up = Quaternion::rotateVector(Vector(0, 0, 1), attitude); Vector up = Quaternion::rotateVector(Vector(0, 0, 1), attitude);
+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;
};
+4 -2
View File
@@ -17,7 +17,7 @@ extern float motors[4];
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
print("Initializing flix\n"); print("Initializing Flix\n");
setupParameters(); setupParameters();
setupPower(); setupPower();
setupLED(); setupLED();
@@ -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();
} }
+8 -3
View File
@@ -6,7 +6,7 @@
#include <SPI.h> #include <SPI.h>
#include <FlixPeriph.h> #include <FlixPeriph.h>
#include "vector.h" #include "vector.h"
#include "lpf.h" #include "filter.h"
#include "util.h" #include "util.h"
MPU9250 imu(SPI); MPU9250 imu(SPI);
@@ -40,10 +40,12 @@ void readIMU() {
imu.getGyro(gyro.x, gyro.y, gyro.z); imu.getGyro(gyro.x, gyro.y, gyro.z);
imu.getAccel(acc.x, acc.y, acc.z); imu.getAccel(acc.x, acc.y, acc.z);
calibrateGyroOnce(); calibrateGyroOnce();
// apply scale and bias
// Apply scale and bias
acc = (acc - accBias) / accScale; acc = (acc - accBias) / accScale;
gyro = gyro - gyroBias; gyro = gyro - gyroBias;
// rotate to body frame
// Rotate to body frame
Quaternion rotation = Quaternion::fromEuler(imuRotation); Quaternion rotation = Quaternion::fromEuler(imuRotation);
acc = Quaternion::rotateVector(acc, rotation.inversed()); acc = Quaternion::rotateVector(acc, rotation.inversed());
gyro = Quaternion::rotateVector(gyro, rotation.inversed()); gyro = Quaternion::rotateVector(gyro, rotation.inversed());
@@ -52,6 +54,7 @@ void readIMU() {
void calibrateGyroOnce() { void calibrateGyroOnce() {
static Delay landedDelay(2); static Delay landedDelay(2);
if (!landedDelay.update(landed)) return; // calibrate only if definitely stationary if (!landedDelay.update(landed)) return; // calibrate only if definitely stationary
gyroBias = gyroBiasFilter.update(gyro); gyroBias = gyroBiasFilter.update(gyro);
} }
@@ -105,6 +108,7 @@ void calibrateAccelOnce() {
if (acc.x < accMin.x) accMin.x = acc.x; if (acc.x < accMin.x) accMin.x = acc.x;
if (acc.y < accMin.y) accMin.y = acc.y; if (acc.y < accMin.y) accMin.y = acc.y;
if (acc.z < accMin.z) accMin.z = acc.z; if (acc.z < accMin.z) accMin.z = acc.z;
// Compute scale and bias // Compute scale and bias
accScale = (accMax - accMin) / 2 / ONE_G; accScale = (accMax - accMin) / 2 / ONE_G;
accBias = (accMax + accMin) / 2; accBias = (accMax + accMin) / 2;
@@ -121,6 +125,7 @@ void printIMUInfo() {
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("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();
+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 = -1; // 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);
}
-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;
};
+83 -25
View File
@@ -10,8 +10,13 @@ extern float controlTime;
extern float voltage; extern float voltage;
int mavlinkSysId = 1; int mavlinkSysId = 1;
Rate telemetryFast(10);
Rate telemetrySlow(2); Rate telemetrySlow(2);
Rate telemetryAttitude(20);
Rate telemetryRC(10);
Rate telemetryMotors(10);
Rate telemetryIMU(15);
Rate telemetryTopic(10);
bool mavlinkConnected = false; bool mavlinkConnected = false;
String mavlinkPrintBuffer; String mavlinkPrintBuffer;
@@ -34,42 +39,58 @@ void sendMavlink() {
((mode == AUTO) ? MAV_MODE_FLAG_AUTO_ENABLED : MAV_MODE_FLAG_MANUAL_INPUT_ENABLED), ((mode == AUTO) ? MAV_MODE_FLAG_AUTO_ENABLED : MAV_MODE_FLAG_MANUAL_INPUT_ENABLED),
mode, MAV_STATE_STANDBY); mode, MAV_STATE_STANDBY);
sendMessage(&msg); sendMessage(&msg);
}
if (!mavlinkConnected) return; // send only heartbeat until connected if (!mavlinkConnected) return; // send only heartbeat until connected
if (telemetrySlow) {
mavlink_msg_extended_sys_state_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, mavlink_msg_extended_sys_state_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg,
MAV_VTOL_STATE_UNDEFINED, landed ? MAV_LANDED_STATE_ON_GROUND : MAV_LANDED_STATE_IN_AIR); MAV_VTOL_STATE_UNDEFINED, landed ? MAV_LANDED_STATE_ON_GROUND : MAV_LANDED_STATE_IN_AIR);
sendMessage(&msg); sendMessage(&msg);
}
uint16_t voltages[] = {voltage * 1000, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX}; if (telemetrySlow && valid(voltage)) {
uint16_t voltages[] = {(uint16_t)(voltage * 1000), UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX};
uint16_t voltagesExt[] = {0, 0, 0, 0}; uint16_t voltagesExt[] = {0, 0, 0, 0};
float remaining = constrain(mapf(voltage, 3.4, 4.2, 0, 1), 0, 1); float remaining = constrain(mapf(voltage, 3.4, 4.2, 0, 1), 0, 1);
mavlink_msg_battery_status_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, 0, MAV_BATTERY_FUNCTION_ALL, mavlink_msg_battery_status_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, 0, MAV_BATTERY_FUNCTION_ALL,
MAV_BATTERY_TYPE_LIPO, INT16_MAX, voltages, -1, -1, -1, remaining * 100, 0, MAV_BATTERY_CHARGE_STATE_OK, voltagesExt, 0, 0); MAV_BATTERY_TYPE_LIPO, INT16_MAX, voltages, -1, -1, -1, remaining * 100, 0, MAV_BATTERY_CHARGE_STATE_OK, voltagesExt, 0, 0);
if (valid(voltage)) sendMessage(&msg); sendMessage(&msg);
} }
if (telemetryFast && mavlinkConnected) { if (telemetryAttitude) {
const float offset[] = {0, 0, 0, 0}; const float offset[] = {0, 0, 0, 0};
mavlink_msg_attitude_quaternion_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, mavlink_msg_attitude_quaternion_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg,
time, attitude.w, attitude.x, -attitude.y, -attitude.z, rates.x, -rates.y, -rates.z, offset); // convert to frd time, attitude.w, attitude.x, -attitude.y, -attitude.z, rates.x, -rates.y, -rates.z, offset); // convert to frd
sendMessage(&msg); sendMessage(&msg);
}
if (telemetryRC && channels[0]) { // 0 means no RC input
mavlink_msg_rc_channels_raw_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, controlTime * 1000, 0, mavlink_msg_rc_channels_raw_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, controlTime * 1000, 0,
channels[0], channels[1], channels[2], channels[3], channels[4], channels[5], channels[6], channels[7], UINT8_MAX); channels[0], channels[1], channels[2], channels[3], channels[4], channels[5], channels[6], channels[7], UINT8_MAX);
if (channels[0] != 0) sendMessage(&msg); // 0 means no RC input sendMessage(&msg);
}
if (telemetryMotors) {
float controls[8]; float controls[8];
memcpy(controls, motors, sizeof(motors)); memcpy(controls, motors, sizeof(motors));
mavlink_msg_actuator_control_target_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, time, 0, controls); mavlink_msg_actuator_control_target_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, time, 0, controls);
sendMessage(&msg); sendMessage(&msg);
}
if (telemetryIMU) {
mavlink_msg_scaled_imu_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, time, mavlink_msg_scaled_imu_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, time,
acc.x / ONE_G * 1000, -acc.y / ONE_G * 1000, -acc.z / ONE_G * 1000, // convert to frd acc.x / ONE_G * 1000, -acc.y / ONE_G * 1000, -acc.z / ONE_G * 1000, // convert to frd
gyro.x * 1000, -gyro.y * 1000, -gyro.z * 1000, gyro.x * 1000, -gyro.y * 1000, -gyro.z * 1000,
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) {
@@ -78,16 +99,36 @@ 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);
if (len) mavlinkConnected = true;
// New packet, parse it // New packet, parse it
mavlink_message_t msg; mavlink_message_t msg;
mavlink_status_t status; mavlink_status_t status;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
if (mavlink_parse_char(MAVLINK_COMM_0, buf[i], &msg, &status)) { if (mavlink_parse_char(MAVLINK_COMM_0, buf[i], &msg, &status)) {
mavlinkConnected = true;
handleMavlink(&msg); handleMavlink(&msg);
} }
} }
@@ -182,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;
} }
@@ -211,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
@@ -241,7 +299,7 @@ void handleMavlink(const void *_msg) {
} }
if (m.command == MAV_CMD_COMPONENT_ARM_DISARM) { if (m.command == MAV_CMD_COMPONENT_ARM_DISARM) {
if (m.param1 && controlThrottle > 0.05) return; // don't arm if throttle is not low if (m.param1 == 1 && controlThrottle > 0.05) return; // don't arm if throttle is not low
accepted = true; accepted = true;
armed = m.param1 == 1; armed = m.param1 == 1;
} }
+9 -10
View File
@@ -7,37 +7,36 @@
float motors[4]; // normalized motor thrusts in range [0..1] float motors[4]; // normalized motor thrusts in range [0..1]
int motorPins[4] = {12, 13, 14, 15}; // default pin numbers int motorPins[4] = {-1, -1, -1, -1}; // default pin numbers
int pwmFrequency = 78000; int pwmFrequency = 78000;
int pwmResolution = 10; int pwmResolution = 10;
int pwmStop = 0; int pwmStop = 0;
int pwmMin = 0; int pwmMin = 0;
int pwmMax = -1; // -1 means duty cycle mode int pwmMax = -1; // -1 means duty cycle mode
const int MOTOR_REAR_LEFT = 0; const int MOTOR_REAR_LEFT = 0, MOTOR_REAR_RIGHT = 1, MOTOR_FRONT_RIGHT = 2, MOTOR_FRONT_LEFT = 3;
const int MOTOR_REAR_RIGHT = 1;
const int MOTOR_FRONT_RIGHT = 2;
const int MOTOR_FRONT_LEFT = 3;
void setupMotors() { void setupMotors() {
print("Setup Motors\n"); print("Setup motors\n");
// configure pins // Configure pins
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (motorPins[i] < 0) continue; // skip unassigned motors
ledcAttach(motorPins[i], pwmFrequency, pwmResolution); ledcAttach(motorPins[i], pwmFrequency, pwmResolution);
pwmFrequency = ledcChangeFrequency(motorPins[i], pwmFrequency, pwmResolution); // when reconfiguring pwmFrequency = ledcChangeFrequency(motorPins[i], pwmFrequency, pwmResolution); // when reconfiguring
} }
sendMotors(); sendMotors();
print("Motors initialized\n");
} }
void sendMotors() { void sendMotors() {
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (motorPins[i] < 0) continue; // skip unassigned motors
ledcWrite(motorPins[i], getDutyCycle(motors[i])); ledcWrite(motorPins[i], getDutyCycle(motors[i]));
} }
} }
int getDutyCycle(float value) { int getDutyCycle(float value) {
value = constrain(value, 0, 1); value = constrain(value, 0, 1);
if (pwmMax >= 0) { // pwm mode if (pwmMax >= 0) { // pwm mode
float pwm = mapf(value, 0, 1, pwmMin, pwmMax); float pwm = mapf(value, 0, 1, pwmMin, pwmMax);
if (value == 0) pwm = pwmStop; if (value == 0) pwm = pwmStop;
@@ -52,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) { void testMotor(int n, float thrust) {
print("Testing motor %d\n", n); print("Testing motor %d\n", n);
motors[n] = 0.2; motors[n] = thrust;
delay(50); // ESP32 may need to wait until the end of the current cycle to change duty https://github.com/espressif/arduino-esp32/issues/5306 delay(50); // ESP32 may need to wait until the end of the current cycle to change duty https://github.com/espressif/arduino-esp32/issues/5306
sendMotors(); sendMotors();
pause(3); pause(3);
+40 -13
View File
@@ -6,13 +6,11 @@
#include <Preferences.h> #include <Preferences.h>
#include "util.h" #include "util.h"
extern int channelZero[16]; extern int channelZero[16], channelMax[16];
extern int channelMax[16];
extern int rollChannel, pitchChannel, throttleChannel, yawChannel, armedChannel, modeChannel; extern int rollChannel, pitchChannel, throttleChannel, yawChannel, armedChannel, modeChannel;
extern int rcRxPin; extern int rcRxPin, voltagePin;
extern int wifiMode, wifiLongRange, udpLocalPort, udpRemotePort, espnowChannel; extern int wifiMode, wifiLongRange, udpLocalPort, udpRemotePort, espnowChannel;
extern float rcLossTimeout, descendTime; extern float rcLossTimeout, descendTime, disarmTilt;
extern int voltagePin;
extern float voltageScale; extern float voltageScale;
extern LowPassFilter<float> voltageFilter; extern LowPassFilter<float> voltageFilter;
@@ -22,6 +20,7 @@ struct Parameter {
const char *name; // max length is 15 const char *name; // max length is 15
bool integer; bool integer;
union { float *f; int *i; }; // pointer to the variable union { float *f; int *i; }; // pointer to the variable
float inital; // default value
float cache; // what's stored in flash float cache; // what's stored in flash
void (*callback)(); // called after parameter change void (*callback)(); // called after parameter change
Parameter(const char *name, float *variable, void (*callback)() = nullptr) : name(name), integer(false), f(variable), callback(callback) {}; Parameter(const char *name, float *variable, void (*callback)() = nullptr) : name(name), integer(false), f(variable), callback(callback) {};
@@ -75,6 +74,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},
@@ -118,7 +119,26 @@ Parameter parameters[] = {
// mavlink // mavlink
{"MAV_SYS_ID", &mavlinkSysId}, {"MAV_SYS_ID", &mavlinkSysId},
{"MAV_RATE_SLOW", &telemetrySlow.rate}, {"MAV_RATE_SLOW", &telemetrySlow.rate},
{"MAV_RATE_FAST", &telemetryFast.rate}, {"MAV_RATE_ATT", &telemetryAttitude.rate},
{"MAV_RATE_RC", &telemetryRC.rate},
{"MAV_RATE_MOT", &telemetryMotors.rate},
{"MAV_RATE_IMU", &telemetryIMU.rate},
{"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},
@@ -126,6 +146,7 @@ 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() {
@@ -133,10 +154,10 @@ void setupParameters() {
storage.begin("flix"); storage.begin("flix");
// Read parameters from storage // Read parameters from storage
for (auto &parameter : parameters) { for (auto &parameter : parameters) {
if (!storage.isKey(parameter.name)) { parameter.inital = parameter.getValue();
storage.putFloat(parameter.name, parameter.getValue()); // store default value if (storage.isKey(parameter.name)) {
parameter.setValue(storage.getFloat(parameter.name));
} }
parameter.setValue(storage.getFloat(parameter.name, 0));
parameter.cache = parameter.getValue(); parameter.cache = parameter.getValue();
} }
} }
@@ -182,17 +203,23 @@ void syncParameters() {
if (motorsActive()) return; // don't use flash while flying, it may cause a delay if (motorsActive()) return; // don't use flash while flying, it may cause a delay
for (auto &parameter : parameters) { for (auto &parameter : parameters) {
if (parameter.getValue() == parameter.cache) continue; // no change if (floatEquals(parameter.getValue(), parameter.cache)) continue; // no change
if (isnan(parameter.getValue()) && isnan(parameter.cache)) continue; // both are NAN
storage.putFloat(parameter.name, parameter.getValue()); storage.putFloat(parameter.name, parameter.getValue());
parameter.cache = parameter.getValue(); // update cache parameter.cache = parameter.getValue(); // update cache
} }
} }
void printParameters() { void printParameters(const char *filter) {
print("Name Value [Default]\n");
for (auto &parameter : parameters) { for (auto &parameter : parameters) {
print("%s = %g\n", parameter.name, parameter.getValue()); if (strncasecmp(parameter.name, filter, strlen(filter))) continue;
if (floatEquals(parameter.getValue(), parameter.inital)) { // parameter changed
print("%-15s %-13g\n", parameter.name, parameter.getValue());
} else {
print("%-15s %-13g [%g]\n", parameter.name, parameter.getValue(), parameter.inital);
}
} }
} }
+1 -1
View File
@@ -5,7 +5,7 @@
#pragma once #pragma once
#include "lpf.h" #include "filter.h"
class PID { class PID {
public: public:
+3 -2
View File
@@ -5,11 +5,11 @@
#include <soc/soc.h> #include <soc/soc.h>
#include <soc/rtc_cntl_reg.h> #include <soc/rtc_cntl_reg.h>
#include "lpf.h" #include "filter.h"
#include "util.h" #include "util.h"
float voltage = NAN; float voltage = NAN;
LowPassFilter<float> voltageFilter(0.2); LowPassFilter<float> voltageFilter(1);
int voltagePin = -1; int voltagePin = -1;
float voltageScale = 2; float voltageScale = 2;
@@ -20,6 +20,7 @@ void setupPower() {
void readVoltage() { void readVoltage() {
if (voltagePin < 0) return; if (voltagePin < 0) return;
static Rate rate(10); static Rate rate(10);
if (!rate) return; if (!rate) return;
+7 -8
View File
@@ -27,14 +27,12 @@ void setupRC() {
bool readRC() { bool readRC() {
if (rcRxPin < 0) return false; if (rcRxPin < 0) return false;
if (rc.read()) { if (!rc.read()) return false;
SBUSData data = rc.data();
for (int i = 0; i < 16; i++) channels[i] = data.ch[i]; // copy channels data rc.getChannels(channels);
normalizeRC(); normalizeRC();
controlTime = t; controlTime = t;
return true; return true;
}
return false;
} }
void normalizeRC() { void normalizeRC() {
@@ -55,6 +53,7 @@ void calibrateRC() {
print("RC_RX_PIN = %d, set the RC pin!\n", rcRxPin); print("RC_RX_PIN = %d, set the RC pin!\n", rcRxPin);
return; return;
} }
uint16_t zero[16]; // for zero positions uint16_t zero[16]; // for zero positions
uint16_t center[16]; // for center positions uint16_t center[16]; // for center positions
uint16_t _[16]; // for unused data uint16_t _[16]; // for unused data
+15 -1
View File
@@ -8,10 +8,12 @@ extern float controlRoll, controlPitch, controlThrottle, controlYaw;
float rcLossTimeout = 1; float rcLossTimeout = 1;
float descendTime = 10; float descendTime = 10;
float disarmTilt = radians(120);
void failsafe() { void failsafe() {
rcLossFailsafe(); rcLossFailsafe();
autoFailsafe(); autoFailsafe();
tiltFailsafe();
} }
// RC loss failsafe // RC loss failsafe
@@ -36,7 +38,7 @@ void descend() {
// Allow pilot to interrupt automatic flight // Allow pilot to interrupt automatic flight
void autoFailsafe() { void autoFailsafe() {
static float roll, pitch, yaw, throttle; static float roll, pitch, yaw, throttle;
if (roll != controlRoll || pitch != controlPitch || yaw != controlYaw || abs(throttle - controlThrottle) > 0.05) { if (abs(roll - controlRoll) > 0.05 || abs(pitch - controlPitch) > 0.05 || abs(yaw - controlYaw) > 0.05 || abs(throttle - controlThrottle) > 0.05) {
// controls changed and mode switch is not configured // controls changed and mode switch is not configured
if (mode == AUTO && invalid(controlMode)) mode = STAB; // regain control by the pilot if (mode == AUTO && invalid(controlMode)) mode = STAB; // regain control by the pilot
} }
@@ -45,3 +47,15 @@ void autoFailsafe() {
yaw = controlYaw; yaw = controlYaw;
throttle = controlThrottle; throttle = controlThrottle;
} }
// Disarm if tilted too much
void tiltFailsafe() {
if (!armed) return;
if (mode != STAB) return;
Vector up = Quaternion::rotateVector(Vector(0, 0, 1), attitude);
float tilt = acos(up.z);
if (disarmTilt && tilt > disarmTilt) {
armed = false;
}
}
+57 -4
View File
@@ -23,6 +23,12 @@ bool valid(float x) {
return isfinite(x); return isfinite(x);
} }
bool floatEquals(float a, float b, float epsilon = 0) {
if (isnan(a) && isnan(b)) return true;
if (a == b) return true;
return fabsf(a - b) <= epsilon;
}
// Wrap angle to [-PI, PI) // Wrap angle to [-PI, PI)
float wrapAngle(float angle) { float wrapAngle(float angle) {
angle = fmodf(angle, 2 * PI); angle = fmodf(angle, 2 * PI);
@@ -47,16 +53,60 @@ void splitString(String& str, String& token0, String& token1, String& token2) {
if (token2.c_str() == NULL) token2 = ""; if (token2.c_str() == NULL) token2 = "";
} }
// Simplified ESP-NOW Serial without tx buffering and resends // Simplified ESP-NOW Serial without resends
class ESPNOWSerial : public ESP_NOW_Serial_Class { class ESPNOWSerial : public ESP_NOW_Serial_Class {
public: public:
int lost = 0;
using ESP_NOW_Serial_Class::ESP_NOW_Serial_Class; using ESP_NOW_Serial_Class::ESP_NOW_Serial_Class;
void onSent(bool success) override {} // disable resends void onSent(bool success) override {
size_t write(const uint8_t *data, size_t len) override { if (!success) lost++;
return ESP_NOW_Peer::send(data, len); // pure send without buffering ESP_NOW_Serial_Class::onSent(true); // always report success to avoid resends
} }
}; };
// 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:
@@ -65,6 +115,9 @@ public:
Rate(float rate) : rate(rate) {} Rate(float rate) : rate(rate) {}
operator bool() { operator bool() {
if (t == last) {
return true; // the same step
}
if (t - last >= 1 / rate) { if (t - last >= 1 / rate) {
last = t; last = t;
return true; return true;
+28 -5
View File
@@ -8,7 +8,7 @@
#include <WiFiUdp.h> #include <WiFiUdp.h>
#include <MacAddress.h> #include <MacAddress.h>
#include <ESP32_NOW_Serial.h> #include <ESP32_NOW_Serial.h>
#include "Preferences.h" #include <Preferences.h>
#include "util.h" #include "util.h"
extern Preferences storage; // use the main preferences storage extern Preferences storage; // use the main preferences storage
@@ -33,10 +33,14 @@ void setupWiFi() {
if (wifiMode == W_AP) { if (wifiMode == W_AP) {
WiFi.softAP(storage.getString("WIFI_AP_SSID", "flix").c_str(), storage.getString("WIFI_AP_PASS", "flixwifi").c_str()); WiFi.softAP(storage.getString("WIFI_AP_SSID", "flix").c_str(), storage.getString("WIFI_AP_PASS", "flixwifi").c_str());
udp.begin(udpLocalPort); udp.begin(udpLocalPort);
} else if (wifiMode == W_STA) { }
if (wifiMode == W_STA) {
WiFi.begin(storage.getString("WIFI_STA_SSID", "").c_str(), storage.getString("WIFI_STA_PASS", "").c_str()); WiFi.begin(storage.getString("WIFI_STA_SSID", "").c_str(), storage.getString("WIFI_STA_PASS", "").c_str());
udp.begin(udpLocalPort); udp.begin(udpLocalPort);
} else if (wifiMode == W_ESPNOW) { }
if (wifiMode == W_ESPNOW) {
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP);
WiFi.setChannel(espnowChannel); WiFi.setChannel(espnowChannel);
espnow.addr(MacAddress(storage.getString("ESPNOW_PEER_MAC", "FF:FF:FF:FF:FF:FF").c_str())); espnow.addr(MacAddress(storage.getString("ESPNOW_PEER_MAC", "FF:FF:FF:FF:FF:FF").c_str()));
@@ -52,8 +56,9 @@ void setupWiFi() {
void sendWiFi(const uint8_t *buf, int len) { void sendWiFi(const uint8_t *buf, int len) {
if (espnow) { if (espnow) {
espnow.write(buf, len); espnow.write(buf, len);
static Rate discovery(2); static Rate discovery(2);
if (discovery) espnowBroadcast.write((const uint8_t *)"flix", 4); // broadcast message to help finding this device if (espnow.isEncrypted() && discovery) espnowBroadcast.write((const uint8_t *)"flix", 4); // broadcast message to help finding this device
return; return;
} }
@@ -84,7 +89,8 @@ void printWiFiInfo() {
print("MAC: %s\n", WiFi.softAPmacAddress().c_str()); print("MAC: %s\n", WiFi.softAPmacAddress().c_str());
print("Peer MAC: %s\n", MacAddress(espnow.addr()).toString().c_str()); print("Peer MAC: %s\n", MacAddress(espnow.addr()).toString().c_str());
print("Encrypted: %d\n", espnow.isEncrypted()); print("Encrypted: %d\n", espnow.isEncrypted());
print("Channel: %d\n", espnow.getChannel()); print("Channel: %d\n", WiFi.channel());
print("Lost packets: %d\n", espnow.lost);
} else if (WiFi.getMode() == WIFI_MODE_AP) { } else if (WiFi.getMode() == WIFI_MODE_AP) {
print("Mode: Access Point (AP)\n"); print("Mode: Access Point (AP)\n");
print("MAC: %s\n", WiFi.softAPmacAddress().c_str()); print("MAC: %s\n", WiFi.softAPmacAddress().c_str());
@@ -127,3 +133,20 @@ void configWiFi(int mode, const char *first, const char *second) {
} }
print("✓ Reboot to apply new settings\n"); print("✓ Reboot to apply new settings\n");
} }
void setWiFiMode(const String& mode) {
if (mode == "ap") {
wifiMode = W_AP;
} else if (mode == "sta") {
wifiMode = W_STA;
} else if (mode == "espnow") {
wifiMode = W_ESPNOW;
} else if (mode == "off") {
wifiMode = W_DISABLED;
} else {
print("Invalid Wi-Fi mode\n");
return;
}
static const char *modes[] = {"Disabled", "Access Point (AP)", "Client (STA)", "ESP-NOW"};
print("✓ Wi-Fi mode set to %s, reboot to apply\n", modes[wifiMode]);
}
+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;
+4 -5
View File
@@ -15,12 +15,11 @@ public:
SBUS(HardwareSerial& bus, const int8_t rxpin, const int8_t txpin, const bool inv = true) {}; SBUS(HardwareSerial& bus, const int8_t rxpin, const int8_t txpin, const bool inv = true) {};
void begin(int rxpin = -1, int txpin = -1, bool inv = true, bool fast = false) {}; void begin(int rxpin = -1, int txpin = -1, bool inv = true, bool fast = false) {};
bool read() { return joystickInit(); }; bool read() { return joystickInit(); };
SBUSData data() { void getChannels(uint16_t (&channels)[16]) const {
SBUSData data; int16_t ch[16];
joystickGet(data.ch); joystickGet(ch);
for (int i = 0; i < 16; i++) { for (int i = 0; i < 16; i++) {
data.ch[i] = map(data.ch[i], -32768, 32767, 1000, 2000); // convert to pulse width style channels[i] = map(ch[i], -32768, 32767, 1000, 2000); // convert to pulse width style
} }
return data;
}; };
}; };
+20 -5
View File
@@ -9,7 +9,7 @@
#include "quaternion.h" #include "quaternion.h"
#include "Arduino.h" #include "Arduino.h"
#include "wifi.h" #include "wifi.h"
#include "lpf.h" #include "filter.h"
extern float t, dt; extern float t, dt;
extern float controlRoll, controlPitch, controlYaw, controlThrottle, controlMode; extern float controlRoll, controlPitch, controlYaw, controlThrottle, controlMode;
@@ -38,7 +38,7 @@ const char* getModeName();
void sendMotors(); void sendMotors();
int getDutyCycle(float value); int getDutyCycle(float value);
bool motorsActive(); bool motorsActive();
void testMotor(int n); void testMotor(int, float);
void print(const char* format, ...); void print(const char* format, ...);
void pause(float duration); void pause(float duration);
void doCommand(String str, bool echo); void doCommand(String str, bool echo);
@@ -48,8 +48,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);
@@ -63,19 +72,25 @@ void failsafe();
void rcLossFailsafe(); void rcLossFailsafe();
void descend(); void descend();
void autoFailsafe(); void autoFailsafe();
void tiltFailsafe();
int parametersCount(); int parametersCount();
const char *getParameterName(int index); const char *getParameterName(int index);
float getParameter(int index); float getParameter(int index);
float getParameter(const char *name); float getParameter(const char *name);
bool setParameter(const char *name, const float value); bool setParameter(const char *name, const float value);
void printParameters(); void printParameters(const char *filter);
void resetParameters(); void resetParameters();
// mocks // mocks
void setLED(bool on) {}; void setLED(bool on) {};
void calibrateGyro() { print("Skip gyro calibrating\n"); };
void calibrateAccel() { print("Skip accel calibrating\n"); }; void calibrateAccel() { print("Skip accel calibrating\n"); };
void printIMUCalibration() { print("cal: N/A\n"); }; void printIMUCalibration() { print("cal: N/A\n"); };
void printIMUInfo() {}; void printIMUInfo() {};
void printWiFiInfo() {}; void printWiFiInfo() {};
void configWiFi(bool, const char*, const char*) { print("Skip WiFi config\n"); }; void configWiFi(bool, const char*, const char*) { print("Skip WiFi config\n"); };
void setWiFiMode(const String& mode) { print("Skip WiFi mode set\n"); };
class IMU {
public:
float getTemp() { return 0; }
} imu;
+4 -2
View File
@@ -23,7 +23,7 @@
#include "estimate.ino" #include "estimate.ino"
#include "safety.ino" #include "safety.ino"
#include "log.ino" #include "log.ino"
#include "lpf.h" #include "filter.h"
#include "mavlink.ino" #include "mavlink.ino"
#include "motors.ino" #include "motors.ino"
#include "parameters.ino" #include "parameters.ino"
@@ -55,6 +55,8 @@ public:
initNode(); initNode();
Serial.begin(0); Serial.begin(0);
setupParameters(); setupParameters();
setupLog();
rcRxPin = 1; // set rc pin to enable rc reading
gzmsg << "Flix plugin loaded" << endl; gzmsg << "Flix plugin loaded" << endl;
} }
@@ -87,7 +89,7 @@ public:
applyMotorForces(); applyMotorForces();
publishTopics(); publishTopics();
logData(); loopLog();
syncParameters(); syncParameters();
} }
+6 -1
View File
@@ -11,7 +11,12 @@
#include <sys/poll.h> #include <sys/poll.h>
#include <gazebo/gazebo.hh> #include <gazebo/gazebo.hh>
int wifiMode = 1; // mock // Mocks
int wifiMode = 1;
int wifiLongRange = 0;
int espnowChannel = 6;
const int W_DISABLED = 0, W_AP = 1, W_STA = 2, W_ESPNOW = 3;
int udpLocalPort = 14580; int udpLocalPort = 14580;
int udpRemotePort = 14550; int udpRemotePort = 14550;
const char *udpRemoteIP = "255.255.255.255"; const char *udpRemoteIP = "255.255.255.255";
+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,)
+30 -10
View File
@@ -11,18 +11,22 @@
#include <Preferences.h> #include <Preferences.h>
#include "../../flix/util.h" #include "../../flix/util.h"
const int CHANNEL = 6; const bool DISABLE_SWARM = true;
const int CHANNEL = -1; // -1 means auto search
char key[ESP_NOW_KEY_LEN + 1] = {0}; // with trailing null char key[ESP_NOW_KEY_LEN + 1] = {0}; // with trailing null
Preferences storage; Preferences storage;
std::vector<ESPNOWSerial *> peers; std::vector<ESPNOWSerial *> peers;
bool stop = false;
void onNewPeer(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) { void onNewPeer(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) {
if (len != 4 || memcmp(data, "flix", 4) != 0) return; // check if discovery message if (len != 4 || memcmp(data, "flix", 4) != 0) return; // check if discovery message
if (stop) return;
Serial.printf("New peer: " MACSTR "\n", MAC2STR(info->src_addr)); Serial.printf("New peer: " MACSTR "\n", MAC2STR(info->src_addr));
ESPNOWSerial *link = new ESPNOWSerial(info->src_addr, CHANNEL, WIFI_IF_AP); ESPNOWSerial *link = new ESPNOWSerial(info->src_addr, WiFi.channel(), WIFI_IF_STA);
link->begin(); link->begin();
link->setKey((const uint8_t *)key); link->setKey((const uint8_t *)key);
peers.push_back(link); peers.push_back(link);
@@ -30,9 +34,8 @@ void onNewPeer(const esp_now_recv_info_t *info, const uint8_t *data, int len, vo
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_STA);
WiFi.setSleep(false); WiFi.setSleep(false);
WiFi.setChannel(CHANNEL);
ESP_NOW.onNewPeer(onNewPeer, NULL); ESP_NOW.onNewPeer(onNewPeer, NULL);
ESP_NOW.begin(); ESP_NOW.begin();
@@ -43,12 +46,6 @@ void setup() {
storage.putString("key", key); storage.putString("key", key);
} }
strcpy(key, storage.getString("key").c_str()); strcpy(key, storage.getString("key").c_str());
// Discover the first peer
while (peers.empty()) {
Serial.printf("espnow %s %s\n", WiFi.softAPmacAddress().c_str(), key);
delay(500);
}
} }
void generateRandomKey() { void generateRandomKey() {
@@ -61,6 +58,20 @@ void generateRandomKey() {
void loop() { void loop() {
uint8_t buf[5000]; uint8_t buf[5000];
static int channelIndex = 0;
static const int channels[] = {6, 11, 1, 2, 6, 11, 3, 4, 5, 6, 1, 11, 8, 6, 9, 10, 11, 6, 1, 12, 13}; // 6, 1 and 11 are most common
static unsigned long last = 0;
if (!stop && millis() - last > 500) {
// Change search channel
last = millis();
channelIndex = (channelIndex + 1) % (sizeof(channels) / sizeof(channels[0]));
int channel = CHANNEL < 0 ? channels[channelIndex] : CHANNEL;
Serial.printf("Run on Flix: espnow %s %s\n", WiFi.STA.macAddress().c_str(), key);
Serial.printf("Searching channel %d\n", channel);
WiFi.setChannel(channel);
}
// Send from Serial to ESP-NOW // Send from Serial to ESP-NOW
while (Serial.available() > 0) { while (Serial.available() > 0) {
int b = Serial.read(); int b = Serial.read();
@@ -81,6 +92,15 @@ void loop() {
// Send from ESP-NOW to Serial // Send from ESP-NOW to Serial
for (ESPNOWSerial *link : peers) { for (ESPNOWSerial *link : peers) {
int len = link->read(buf, sizeof(buf)); int len = link->read(buf, sizeof(buf));
if (!stop) {
for (int i = 0; i < len; i++) {
if (buf[i] == MAVLINK_STX) {
// Got MAVLink message, stop discovery
Serial.printf("Received MAVLink from " MACSTR "\n", MAC2STR(link->addr()));
if (DISABLE_SWARM) stop = true;
}
}
}
if (len > 0) { if (len > 0) {
Serial.write(buf, len); Serial.write(buf, len);
} }
+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)
+9
View File
@@ -28,6 +28,8 @@ from pyflix import Flix
flix = Flix() # create a Flix object and wait for connection flix = Flix() # create a Flix object and wait for connection
``` ```
If using ESP-NOW connection, specify the proxy device name in `FLIX_DEVICE` environment variable or pass it to the constructor: `Flix(device='/dev/cu.usbserial-0001')`.
### Telemetry ### Telemetry
Basic telemetry is available through object properties. The property names generally match the corresponding variables in the firmware code: Basic telemetry is available through object properties. The property names generally match the corresponding variables in the firmware code:
@@ -220,6 +222,13 @@ The following scripts demonstrate how to use the library:
* [`log.py`](../log.py) — download flight logs from the drone. * [`log.py`](../log.py) — download flight logs from the drone.
* [`example.py`](../example.py) — a simple example, prints telemetry data and waits for events. * [`example.py`](../example.py) — a simple example, prints telemetry data and waits for events.
> [!TIP]
> Set `FLIX_DEVICE` environment variable to use these tools with ESP-NOW connection, for example:
>
> ```bash
> FLIX_DEVICE=/dev/cu.usbserial-0001 tools/cli.py
> ```
## Advanced usage ## Advanced usage
### MAVLink ### MAVLink
+16 -11
View File
@@ -44,22 +44,27 @@ class Flix:
_print_buffer: str = '' _print_buffer: str = ''
_modes = ['RAW', 'ACRO', 'STAB', 'AUTO'] _modes = ['RAW', 'ACRO', 'STAB', 'AUTO']
def __init__(self, system_id: int=1, wait_connection: bool=True): def __init__(self, system_id: int=1, wait_connection: bool=True, device=os.getenv('FLIX_DEVICE')):
if not (0 <= system_id < 256): if not (0 <= system_id < 256):
raise ValueError('system_id must be in range [0, 255]') raise ValueError('system_id must be in range [0, 255]')
self._setup_mavlink() self._setup_mavlink()
self.system_id = system_id self.system_id = system_id
self._init_state() self._init_state()
try: if device is not None:
# Direct connection # User defined connection
logger.debug('Listening on port 14550') logger.debug(f'Connecting to {device}')
self.connection: mavutil.mavfile = mavutil.mavlink_connection('udpin:0.0.0.0:14550', source_system=255) # type: ignore self.connection: mavutil.mavfile = mavutil.mavlink_connection(device, source_system=255) # type: ignore
except OSError as e: else:
if e.errno != errno.EADDRINUSE: try:
raise # Direct connection
# Port busy - using proxy logger.debug('Listening on port 14550')
logger.debug('Listening on port 14555 (proxy)') self.connection: mavutil.mavfile = mavutil.mavlink_connection('udpin:0.0.0.0:14550', source_system=255) # type: ignore
self.connection: mavutil.mavfile = mavutil.mavlink_connection('udpin:0.0.0.0:14555', source_system=254) # type: ignore except OSError as e:
if e.errno != errno.EADDRINUSE:
raise
# Port busy - using proxy
logger.debug('Listening on port 14555 (proxy)')
self.connection: mavutil.mavfile = mavutil.mavlink_connection('udpin:0.0.0.0:14555', source_system=254) # type: ignore
self.connection.target_system = system_id self.connection.target_system = system_id
self.mavlink: mavlink.MAVLink = self.connection.mav self.mavlink: mavlink.MAVLink = self.connection.mav
self._event_listeners: Dict[str, List[Callable[..., Any]]] = {} self._event_listeners: Dict[str, List[Callable[..., Any]]] = {}
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "pyflix" name = "pyflix"
version = "0.15" version = "0.16"
description = "Python API for Flix drone" description = "Python API for Flix drone"
authors = [{ name="Oleg Kalachev", email="okalachev@gmail.com" }] authors = [{ name="Oleg Kalachev", email="okalachev@gmail.com" }]
license = "MIT" license = "MIT"