Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b2496231a | |||
| e1ff92c5f0 |
@@ -1,11 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[*.{ino,cpp,c,h,hpp,sdf,world}]
|
|
||||||
charset = utf-8
|
|
||||||
indent_style = tab
|
|
||||||
tab_width = 4
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
name: Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ '*' ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Install Arduino CLI
|
|
||||||
uses: arduino/setup-arduino-cli@v1.1.1
|
|
||||||
- name: Build firmware
|
|
||||||
run: make
|
|
||||||
|
|
||||||
build_macos:
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Install Arduino CLI
|
|
||||||
run: brew install arduino-cli
|
|
||||||
- name: Build firmware
|
|
||||||
run: make
|
|
||||||
|
|
||||||
build_windows:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Install Arduino CLI
|
|
||||||
run: choco install arduino-cli
|
|
||||||
- name: Install Make
|
|
||||||
run: choco install make
|
|
||||||
- name: Build firmware
|
|
||||||
run: make
|
|
||||||
|
|
||||||
build_simulator:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Install Gazebo
|
|
||||||
run: curl -sSL http://get.gazebosim.org | sh
|
|
||||||
- name: Install SDL2
|
|
||||||
run: sudo apt-get install libsdl2-dev
|
|
||||||
- name: Build simulator
|
|
||||||
run: make build_simulator
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: gazebo-plugin-binary
|
|
||||||
path: gazebo/build/*.so
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
# build_simulator_macos:
|
|
||||||
# runs-on: macos-latest
|
|
||||||
# steps:
|
|
||||||
# - uses: actions/checkout@v3
|
|
||||||
# - name: Install Gazebo
|
|
||||||
# run: brew tap osrf/simulation && brew install gazebo11
|
|
||||||
# - name: Install SDL2
|
|
||||||
# run: brew install sdl2
|
|
||||||
# - name: Build simulator
|
|
||||||
# run: make build_simulator
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
*.hex
|
|
||||||
*.elf
|
|
||||||
gazebo/build/
|
|
||||||
tools/log/
|
|
||||||
.dependencies
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
BOARD = esp32:esp32:d1_mini32
|
|
||||||
PORT := $(wildcard /dev/serial/by-id/usb-Silicon_Labs_CP2104_USB_to_UART_Bridge_Controller_* /dev/serial/by-id/usb-1a86_USB_Single_Serial_* /dev/cu.usbserial-*)
|
|
||||||
PORT := $(strip $(PORT))
|
|
||||||
|
|
||||||
build: .dependencies
|
|
||||||
arduino-cli compile --fqbn $(BOARD) flix
|
|
||||||
|
|
||||||
upload: build
|
|
||||||
arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" flix
|
|
||||||
|
|
||||||
monitor:
|
|
||||||
arduino-cli monitor -p "$(PORT)" -c baudrate=115200
|
|
||||||
|
|
||||||
dependencies .dependencies:
|
|
||||||
arduino-cli core update-index --config-file arduino-cli.yaml
|
|
||||||
arduino-cli core install esp32:esp32@2.0.11 --config-file arduino-cli.yaml
|
|
||||||
arduino-cli lib install "Bolder Flight Systems SBUS"@1.0.1
|
|
||||||
arduino-cli lib install --git-url https://github.com/okalachev/MPU9250.git --config-file arduino-cli.yaml
|
|
||||||
touch .dependencies
|
|
||||||
|
|
||||||
gazebo/build cmake: gazebo/CMakeLists.txt
|
|
||||||
mkdir -p gazebo/build
|
|
||||||
cd gazebo/build && cmake ..
|
|
||||||
|
|
||||||
build_simulator: gazebo/build
|
|
||||||
make -C gazebo/build
|
|
||||||
|
|
||||||
simulator: build_simulator
|
|
||||||
GAZEBO_MODEL_PATH=$$GAZEBO_MODEL_PATH:${CURDIR}/gazebo/models \
|
|
||||||
GAZEBO_PLUGIN_PATH=$$GAZEBO_PLUGIN_PATH:${CURDIR}/gazebo/build \
|
|
||||||
gazebo --verbose ${CURDIR}/gazebo/flix.world
|
|
||||||
|
|
||||||
log:
|
|
||||||
PORT=$(PORT) tools/grab_log.py
|
|
||||||
|
|
||||||
plot:
|
|
||||||
plotjuggler -d $(shell ls -t tools/log/*.csv | head -n1)
|
|
||||||
|
|
||||||
docs:
|
|
||||||
for FILE in docs/*.d2; do d2 $$FILE; done
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf gazebo/build flix/build flix/cache .dependencies
|
|
||||||
|
|
||||||
.PHONY: build upload monitor dependencies cmake build_simulator simulator log clean
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# flix
|
|
||||||
|
|
||||||
**flix** (*flight + X*) — making an open source ESP32-based quadcopter from scratch.
|
|
||||||
|
|
||||||
<img src="docs/img/flix.jpg" width=500>
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* Simple and clear Arduino based source code.
|
|
||||||
* Acro and Stabilized flight using remote control.
|
|
||||||
* Precise simulation using Gazebo.
|
|
||||||
* In-RAM logging.
|
|
||||||
* Command line interface through USB port.
|
|
||||||
* Wi-Fi support.
|
|
||||||
* ESCs with reverse mode support.
|
|
||||||
* *Textbook and videos for students on writing a flight controller\*.*
|
|
||||||
* *MAVLink support\*.*
|
|
||||||
* *Completely 3D-printed frame*.*
|
|
||||||
* *Position control and autonomous flights using external camera\**.
|
|
||||||
* [Building and running instructions](docs/build.md).
|
|
||||||
|
|
||||||
*\* — planned.*
|
|
||||||
|
|
||||||
## It actually flies
|
|
||||||
|
|
||||||
<a href="https://youtu.be/8GzzIQ3C6DQ"><img width=500 src="https://i3.ytimg.com/vi/8GzzIQ3C6DQ/maxresdefault.jpg"></a>
|
|
||||||
|
|
||||||
See YouTube demo video: https://youtu.be/8GzzIQ3C6DQ.
|
|
||||||
|
|
||||||
## Simulation
|
|
||||||
|
|
||||||
Simulation in Gazebo using a plugin that runs original Arduino code is implemented:
|
|
||||||
|
|
||||||
<img src="docs/img/simulator.png" width=500>
|
|
||||||
|
|
||||||
## Schematics
|
|
||||||
|
|
||||||
<img src="docs/img/schematics.svg" width=800>
|
|
||||||
|
|
||||||
## Version 0
|
|
||||||
|
|
||||||
### Components
|
|
||||||
|
|
||||||
|Component|Type|Image|Quantity|
|
|
||||||
|-|-|-|-|
|
|
||||||
|ESP32 Mini|Microcontroller board|<img src="docs/img/esp32.jpg" width=100>|1|
|
|
||||||
|GY-91|IMU+LDO+barometer board|<img src="docs/img/gy-91.jpg" width=100>|1|
|
|
||||||
|K100|Quadcopter frame|<img src="docs/img/frame.jpg" width=100>|1|
|
|
||||||
|8520 3.7V brushed motor|Motor|<img src="docs/img/motor.jpeg" width=100>|4|
|
|
||||||
|Hubsan 55 mm| Propeller|<img src="docs/img/prop.jpg" width=100>|4|
|
|
||||||
|2.7A 1S Dual Way Micro Brush ESC|Motor ESC|<img src="docs/img/esc.jpg" width=100>|4|
|
|
||||||
|KINGKONG TINY X8|RC transmitter|<img src="docs/img/tx.jpg" width=100>|1|
|
|
||||||
|DF500 (SBUS)|RC receiver|<img src="docs/img/rx.jpg" width=100>|1|
|
|
||||||
||SBUS inverter|<img src="docs/img/inv.jpg" width=100>|1|
|
|
||||||
|3.7 Li-Po 850 MaH 60C|Battery|||
|
|
||||||
||Battery charger|<img src="docs/img/charger.jpg" width=100>|1|
|
|
||||||
||Wires, connectors, tape, ...||
|
|
||||||
||3D-printed frame parts||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
board_manager:
|
|
||||||
additional_urls:
|
|
||||||
- https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
|
|
||||||
library:
|
|
||||||
enable_unsafe_install: true
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Building and running
|
|
||||||
|
|
||||||
## Simulation
|
|
||||||
|
|
||||||
Dependencies are [Gazebo Classic simulator](https://classic.gazebosim.org) and [SDL2](https://www.libsdl.org) library.
|
|
||||||
|
|
||||||
### Ubuntu
|
|
||||||
|
|
||||||
1. Install Gazebo 11:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sSL http://get.gazebosim.org | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Set up your Gazebo environment variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "source /usr/share/gazebo/setup.sh" >> ~/.bashrc
|
|
||||||
source ~/.bashrc
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install SDL2:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get install libsdl2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Run the simulation:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
### macOS
|
|
||||||
|
|
||||||
1. Install Homebrew package manager, if you don't have it installed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install Gazebo 11 and SDL2:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew tap osrf/simulation
|
|
||||||
brew install gazebo11
|
|
||||||
brew install sdl2
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Run the simulation:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make simulator
|
|
||||||
```
|
|
||||||
|
|
||||||
## Firmware
|
|
||||||
|
|
||||||
### Arduino IDE (Windows, Linux, macOS)
|
|
||||||
|
|
||||||
1. Install [Arduino IDE](https://www.arduino.cc/en/software).
|
|
||||||
2. Install ESP32 core using [Boards Manager](https://docs.arduino.cc/learn/starting-guide/cores).
|
|
||||||
3. Build and upload the firmware using Arduino IDE.
|
|
||||||
|
|
||||||
### Command line (Windows, Linux, macOS)
|
|
||||||
|
|
||||||
1. [Install Arduino CLI](https://arduino.github.io/arduino-cli/installation/).
|
|
||||||
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. Compile the firmware using `make`. Arduino dependencies will be installed automatically:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
You can flash the firmware to the board using command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make upload
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also compile the firmware, upload it and start serial port monitoring using command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make upload monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
See other available Make commands in the [Makefile](../Makefile).
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
@@ -1,782 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
width="2560"
|
|
||||||
height="1440"
|
|
||||||
viewBox="0 0 2560 1440"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs1">
|
|
||||||
<color-profile
|
|
||||||
name="sRGB-IEC61966-2.1"
|
|
||||||
xlink:href="data:application/vnd.iccprofile;base64,AAAMbGxjbXMCEAAAbW50clJHQiBYWVogB84AAgAJAAYAMQAAYWNzcEFQUEwAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1sY21zAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARY3BydAAAAVAAAAAzZGVzYwAAAYQAAACQd3RwdAAAAhQAAAAUYmtwdAAAAigAAAAUclhZWgAAAjwAAAAUZ1hZWgAAAlAAAAAUYlhZWgAAAmQAAAAUZG1uZAAAAngAAABwZG1kZAAAAugAAACIdnVlZAAAA3AAAACGdmlldwAAA/gAAAAkbHVtaQAABBwAAAAUbWVhcwAABDAAAAAkdGVjaAAABFQAAAAMclRSQwAABGAAAAgMZ1RSQwAABGAAAAgMYlRSQwAABGAAAAgMdGV4dAAAAABDb3B5cmlnaHQgKGMpIDE5OTggSGV3bGV0dC1QYWNrYXJkIENvbXBhbnkAAGRlc2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAASAHMAUgBHAEIAIABJAEUAQwA2ADEAOQA2ADYALQAyAC4AMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9kZXNjAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2aWV3AAAAAAATpP4AFF8uABDPFAAD7cwABBMLAANcngAAAAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf//"
|
|
||||||
id="color-profile2" />
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath2">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
id="path2" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath4">
|
|
||||||
<path
|
|
||||||
d="m 735.7776,400.7546 h 448.4447 V 726.5893 H 735.7776 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-877.16305,540.06451)"
|
|
||||||
id="path4" />
|
|
||||||
</clipPath>
|
|
||||||
<color-profile
|
|
||||||
name="Display-P3"
|
|
||||||
xlink:href="data:application/vnd.iccprofile;base64,AAACGGxjbXMEAAAAbW50clJHQiBYWVogB+YAAQABAAAAAAAAYWNzcEFQUEwAAAAAQVBQTAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1sY21z7P2jjjiFR8NttL1PetoYLwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKZGVzYwAAAPwAAAAwY3BydAAAASwAAABQd3RwdAAAAXwAAAAUclhZWgAAAZAAAAAUZ1hZWgAAAaQAAAAUYlhZWgAAAbgAAAAUclRSQwAAAcwAAAAgY2hhZAAAAewAAAAsYlRSQwAAAcwAAAAgZ1RSQwAAAcwAAAAgbWx1YwAAAAAAAAABAAAADGVuVVMAAAAUAAAAHABEAGkAcwBwAGwAYQB5ACAAUAAzbWx1YwAAAAAAAAABAAAADGVuVVMAAAA0AAAAHABDAG8AcAB5AHIAaQBnAGgAdAAgAEEAcABwAGwAZQAgAEkAbgBjAC4ALAAgADIAMAAyADJYWVogAAAAAAAA9tUAAQAAAADTLFhZWiAAAAAAAACD3wAAPb////+7WFlaIAAAAAAAAEq/AACxNwAACrlYWVogAAAAAAAAKDgAABELAADIuXBhcmEAAAAAAAMAAAACZmYAAPKnAAANWQAAE9AAAApbc2YzMgAAAAAAAQxCAAAF3v//8yYAAAeTAAD9kP//+6L///2jAAAD3AAAwG4="
|
|
||||||
id="color-profile3" />
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath6">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-84.355832,687.17702)"
|
|
||||||
id="path6" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath8">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-84.355832,230.07061)"
|
|
||||||
id="path8" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath9">
|
|
||||||
<path
|
|
||||||
d="m 69.25037,77.53967 h 445.3672 v 152.5309 H 69.25037 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-134.2582,129.9201)"
|
|
||||||
id="path9" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath11">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-752.42814,230.07061)"
|
|
||||||
id="path11" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath12">
|
|
||||||
<path
|
|
||||||
d="m 737.3226,77.53967 h 445.3672 v 152.5309 H 737.3226 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-774.39594,130.08053)"
|
|
||||||
id="path12" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath14">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-502.51162,523.78481)"
|
|
||||||
id="path14" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath16">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
id="path16" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath17">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-580.02442,458.62641)"
|
|
||||||
id="path17" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath19">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-975.72865,400.75401)"
|
|
||||||
id="path19" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath21">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-866.79025,301.17428)"
|
|
||||||
id="path21" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath23">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-502.51111,133.32953)"
|
|
||||||
id="path23" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath25">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-562.46581,156.45998)"
|
|
||||||
id="path25" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath27">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1166.116,543.19631)"
|
|
||||||
id="path27" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath29">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1236.025,558.75382)"
|
|
||||||
id="path29" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath31">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1603.7991,484.40641)"
|
|
||||||
id="path31" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath33">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1450.2771,343.00021)"
|
|
||||||
id="path33" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath35">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-183.9595,646.56341)"
|
|
||||||
id="path35" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath36">
|
|
||||||
<path
|
|
||||||
d="M 176.4655,540.4207 H 407.3902 V 646.5634 H 176.4655 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-208.1755,599.41831)"
|
|
||||||
id="path36" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath37">
|
|
||||||
<path
|
|
||||||
d="M 176.4655,540.4207 H 407.3902 V 646.5634 H 176.4655 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-276.56905,599.41831)"
|
|
||||||
id="path37" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath38">
|
|
||||||
<path
|
|
||||||
d="M 176.4655,540.4207 H 407.3902 V 646.5634 H 176.4655 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-315.94406,599.41831)"
|
|
||||||
id="path38" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath39">
|
|
||||||
<path
|
|
||||||
d="M 176.4655,540.4207 H 407.3902 V 646.5634 H 176.4655 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-263.67901,563.28652)"
|
|
||||||
id="path39" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath40">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-171.24471,470.09881)"
|
|
||||||
id="path40" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath42">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-401.39651,599.29651)"
|
|
||||||
id="path42" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath44">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-581.82901,621.43931)"
|
|
||||||
id="path44" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath46">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-752.42814,1002.46)"
|
|
||||||
id="path46" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath47">
|
|
||||||
<path
|
|
||||||
d="m 737.3226,849.9294 h 445.3672 v 152.5309 H 737.3226 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-865.05512,902.30993)"
|
|
||||||
id="path47" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath49">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-327.87531,885.82422)"
|
|
||||||
id="path49" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath51">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
id="path51" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath52">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-398.91041,812.44552)"
|
|
||||||
id="path52" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath54">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1170.5841,918.51212)"
|
|
||||||
id="path54" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath56">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1396.2291,882.18742)"
|
|
||||||
id="path56" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath58">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1420.5,230.07061)"
|
|
||||||
id="path58" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath59">
|
|
||||||
<path
|
|
||||||
d="m 1405.395,77.53967 h 445.3672 v 152.5309 H 1405.395 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1507.956,129.9201)"
|
|
||||||
id="path59" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath60">
|
|
||||||
<path
|
|
||||||
d="m 1405.395,77.53967 h 445.3672 v 152.5309 H 1405.395 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1706.5596,129.9201)"
|
|
||||||
id="path60" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath62">
|
|
||||||
<path
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1420.5,639.93742)"
|
|
||||||
id="path62" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath63">
|
|
||||||
<path
|
|
||||||
d="m 1405.395,487.4065 h 445.3672 V 639.9374 H 1405.395 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1546.321,539.78701)"
|
|
||||||
id="path63" />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath64">
|
|
||||||
<path
|
|
||||||
d="m 1405.395,487.4065 h 445.3672 V 639.9374 H 1405.395 Z"
|
|
||||||
transform="matrix(1,0,0,-1,-1668.196,539.78701)"
|
|
||||||
id="path64" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
id="g1">
|
|
||||||
<path
|
|
||||||
id="path1"
|
|
||||||
d="M 0,0 H 1920 V 1080 H 0 Z"
|
|
||||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,1440)"
|
|
||||||
clip-path="url(#clipPath2)" />
|
|
||||||
<g
|
|
||||||
id="g2">
|
|
||||||
<path
|
|
||||||
id="path3"
|
|
||||||
d="m 782.3957,726.5893 h 355.2083 c 8.367,0 13.387,0 16.734,-1.3973 4.825,-1.756 8.626,-5.5566 10.382,-10.3813 1.397,-3.3468 1.397,-8.367 1.397,-16.7341 V 429.2673 c 0,-8.367 0,-13.3872 -1.397,-16.734 -1.756,-4.8248 -5.557,-8.6254 -10.382,-10.3814 -3.347,-1.3973 -8.367,-1.3973 -16.734,-1.3973 H 782.3957 c -8.367,0 -13.3872,0 -16.734,1.3973 -4.8247,1.756 -8.6253,5.5566 -10.3814,10.3814 -1.3972,3.3468 -1.3972,8.367 -1.3972,16.734 v 268.8093 c 0,8.3671 0,13.3873 1.3972,16.7341 1.7561,4.8247 5.5567,8.6253 10.3814,10.3813 3.3468,1.3973 8.367,1.3973 16.734,1.3973 z"
|
|
||||||
style="fill:#0076ba;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,1440)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g3">
|
|
||||||
<text
|
|
||||||
id="text3"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1169.5507,719.914)"
|
|
||||||
clip-path="url(#clipPath4)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:60px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 33.66 67.085999 100.164 132.918"
|
|
||||||
y="0"
|
|
||||||
id="tspan3">ESP32</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g4">
|
|
||||||
<path
|
|
||||||
id="path5"
|
|
||||||
d="M 22.77821,0 H 392.3655 c 6.6843,0 10.6948,0 13.3685,1.116234 3.8544,1.402875 6.8906,4.43911 8.2935,8.293479 1.1162,2.673697 1.1162,6.684247 1.1162,13.368497 V 230.8406 c 0,6.6843 0,10.6948 -1.1162,13.3685 -1.4029,3.8544 -4.4391,6.8906 -8.2935,8.2935 -2.6737,1.1162 -6.6842,1.1162 -13.3685,1.1162 H 22.77821 c -6.68425,0 -10.6948,0 -13.368497,-1.1162 C 5.555344,251.0997 2.519109,248.0635 1.116234,244.2091 0,241.5354 0,237.5249 0,230.8406 V 22.77821 C 0,16.09396 0,12.08341 1.116234,9.409713 2.519109,5.555344 5.555344,2.519109 9.409713,1.116234 12.08341,0 16.09396,0 22.77821,0 Z"
|
|
||||||
style="fill:none;stroke:#0076ba;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,112.47444,523.764)"
|
|
||||||
clip-path="url(#clipPath6)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g6">
|
|
||||||
<path
|
|
||||||
id="path7"
|
|
||||||
d="M 25.43947,0 H 389.7043 c 7.4652,0 11.9443,0 14.9304,1.246647 4.3046,1.566779 7.6956,4.957749 9.2624,9.262433 1.2466,2.98608 1.2466,7.4652 1.2466,14.93039 V 127.0915 c 0,7.4652 0,11.9443 -1.2466,14.9304 -1.5668,4.3046 -4.9578,7.6956 -9.2624,9.2624 -2.9861,1.2466 -7.4652,1.2466 -14.9304,1.2466 H 25.43947 c -7.46519,0 -11.94431,0 -14.93039,-1.2466 C 6.204396,149.7175 2.813426,146.3265 1.246647,142.0219 0,139.0358 0,134.5567 0,127.0915 V 25.43947 C 0,17.97428 0,13.49516 1.246647,10.50908 2.813426,6.204396 6.204396,2.813426 10.50908,1.246647 13.49516,0 17.97428,0 25.43947,0 Z"
|
|
||||||
style="fill:none;stroke:#0076ba;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,112.47444,1133.2392)"
|
|
||||||
clip-path="url(#clipPath8)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g8">
|
|
||||||
<text
|
|
||||||
id="text8"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,179.01093,1266.7732)"
|
|
||||||
clip-path="url(#clipPath9)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:60px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 37.236 73.272003 92.028 128.18401 159.78 187.476 219.07201 232.78799 262.224 293.82001"
|
|
||||||
y="0"
|
|
||||||
id="tspan8">RC Receiver</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g9">
|
|
||||||
<path
|
|
||||||
id="path10"
|
|
||||||
d="M 25.43947,0 H 389.7043 c 7.4652,0 11.9443,0 14.9304,1.246647 4.3046,1.566779 7.6956,4.957749 9.2624,9.262433 1.2466,2.98608 1.2466,7.4652 1.2466,14.93039 V 127.0915 c 0,7.4652 0,11.9443 -1.2466,14.9304 -1.5668,4.3046 -4.9578,7.6956 -9.2624,9.2624 -2.9861,1.2466 -7.4652,1.2466 -14.9304,1.2466 H 25.43947 c -7.46519,0 -11.94431,0 -14.93039,-1.2466 C 6.204396,149.7175 2.813426,146.3265 1.246647,142.0219 0,139.0358 0,134.5567 0,127.0915 V 25.43947 C 0,17.97428 0,13.49516 1.246647,10.50908 2.813426,6.204396 6.204396,2.813426 10.50908,1.246647 13.49516,0 17.97428,0 25.43947,0 Z"
|
|
||||||
style="fill:none;stroke:#0076ba;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1003.2375,1133.2392)"
|
|
||||||
clip-path="url(#clipPath11)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g11">
|
|
||||||
<text
|
|
||||||
id="text11"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1032.5279,1266.5593)"
|
|
||||||
clip-path="url(#clipPath12)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:60px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 33.425999 68.772003 108.138 141.564 160.28999 182.37601 215.32201 244.728 276.29401 297.89999 317.94601 349.51199"
|
|
||||||
y="0"
|
|
||||||
id="tspan11">SBUS Inverter</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g12">
|
|
||||||
<path
|
|
||||||
id="path13"
|
|
||||||
d="M 0,3.168352 C 74.57495,10.14042 150.4092,9.923605 227.5027,2.517898 l 2.9842,-0.314811"
|
|
||||||
style="fill:none;stroke:#d5d5d5;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,670.01547,741.62027)"
|
|
||||||
clip-path="url(#clipPath14)" />
|
|
||||||
<path
|
|
||||||
id="path15"
|
|
||||||
d="m 731.274,509.3332 22.6086,14.4516 -25.1264,9.416 z"
|
|
||||||
style="fill:#d5d5d5;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,1440)"
|
|
||||||
clip-path="url(#clipPath16)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g16" />
|
|
||||||
<g
|
|
||||||
id="g17">
|
|
||||||
<text
|
|
||||||
id="text17"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,773.36587,828.49813)"
|
|
||||||
clip-path="url(#clipPath17)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:40px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 22.284 44.327999"
|
|
||||||
y="0"
|
|
||||||
id="tspan17">SPI</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g18">
|
|
||||||
<path
|
|
||||||
id="path18"
|
|
||||||
d="M 0,167.6836 C 6.83206,120.8451 9.566947,72.94383 8.20466,23.9797 L 8.080895,20.98212"
|
|
||||||
style="fill:none;stroke:#d5d5d5;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1300.9715,905.66133)"
|
|
||||||
clip-path="url(#clipPath19)" />
|
|
||||||
<path
|
|
||||||
id="path20"
|
|
||||||
d="M 995.923,377.2695 982.9432,400.754 971.9435,376.2794 Z"
|
|
||||||
style="fill:#d5d5d5;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,1440)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g20" />
|
|
||||||
<g
|
|
||||||
id="g21">
|
|
||||||
<text
|
|
||||||
id="text21"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1155.7203,1038.4343)"
|
|
||||||
clip-path="url(#clipPath21)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:40px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 26.035999 50.032001 73.788002"
|
|
||||||
y="0"
|
|
||||||
id="tspan21">UART</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g22">
|
|
||||||
<path
|
|
||||||
id="path22"
|
|
||||||
d="M 0,0.9506769 C 72.79023,4.65132 147.106,4.743867 222.9474,1.22832 l 2.9963,-0.153549"
|
|
||||||
style="fill:none;stroke:#d5d5d5;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,670.0148,1262.2273)"
|
|
||||||
clip-path="url(#clipPath23)" />
|
|
||||||
<path
|
|
||||||
id="path24"
|
|
||||||
d="m 726.0729,120.1169 23.3544,13.2126 -24.5827,10.7559 z"
|
|
||||||
style="fill:#d5d5d5;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,1440)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g24" />
|
|
||||||
<g
|
|
||||||
id="g25">
|
|
||||||
<text
|
|
||||||
id="text25"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,749.9544,1231.3867)"
|
|
||||||
clip-path="url(#clipPath25)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:40px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 22.284 45.848 72.092003"
|
|
||||||
y="0"
|
|
||||||
id="tspan25">SBUS</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g26">
|
|
||||||
<path
|
|
||||||
id="path26"
|
|
||||||
d="m 0,0.718922 c 74.2163,3.9271 150.0213,4.096899 227.4149,0.509398 l 2.9963,-0.153548"
|
|
||||||
style="fill:none;stroke:#d5d5d5;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1554.8213,715.73827)"
|
|
||||||
clip-path="url(#clipPath27)" />
|
|
||||||
<path
|
|
||||||
id="path28"
|
|
||||||
d="m 1394.145,529.9837 23.355,13.2126 -24.583,10.756 z"
|
|
||||||
style="fill:#d5d5d5;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,1440)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g28" />
|
|
||||||
<g
|
|
||||||
id="g29">
|
|
||||||
<text
|
|
||||||
id="text29"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1648.0333,694.99493)"
|
|
||||||
clip-path="url(#clipPath29)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:40px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 22.052 58.144001"
|
|
||||||
y="0"
|
|
||||||
id="tspan29">PWM</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g30">
|
|
||||||
<path
|
|
||||||
id="path30"
|
|
||||||
d="M 8.544339,0 C -2.139819,73.24743 -2.804077,149.0941 6.551564,227.5399 l 0.391861,2.9755"
|
|
||||||
style="fill:none;stroke:#d5d5d5;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,2138.3987,794.1248)"
|
|
||||||
clip-path="url(#clipPath31)" />
|
|
||||||
<path
|
|
||||||
id="path32"
|
|
||||||
d="m 1598.454,255.2985 15.031,-22.2277 8.763,25.3614 z"
|
|
||||||
style="fill:#d5d5d5;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,1440)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g32" />
|
|
||||||
<g
|
|
||||||
id="g33">
|
|
||||||
<text
|
|
||||||
id="text33"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1933.7027,982.6664)"
|
|
||||||
clip-path="url(#clipPath33)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:40px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 22.032 43.751999 52.972 66.351997 87.348 109.456"
|
|
||||||
y="0"
|
|
||||||
id="tspan33">Voltage</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g34">
|
|
||||||
<path
|
|
||||||
id="path34"
|
|
||||||
d="M 15.17917,0 H 200.7573 c 4.4543,0 7.1269,0 8.9086,0.7438468 2.5686,0.9348622 4.5919,2.9581782 5.5267,5.5266902 0.7439,1.781727 0.7439,4.454313 0.7439,8.908633 v 75.78435 c 0,4.45431 0,7.1269 -0.7439,8.90863 -0.9348,2.56855 -2.9581,4.59185 -5.5267,5.52665 -1.7817,0.7439 -4.4543,0.7439 -8.9086,0.7439 H 15.17917 c -4.45432,0 -7.126906,0 -8.908633,-0.7439 C 3.702025,104.464 1.678709,102.4407 0.7438468,99.87215 0,98.09042 0,95.41783 0,90.96352 V 15.17917 C 0,10.72485 0,8.052264 0.7438468,6.270537 1.678709,3.702025 3.702025,1.678709 6.270537,0.7438468 8.052264,0 10.72485,0 15.17917,0 Z"
|
|
||||||
style="fill:none;stroke:#0076ba;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,245.27933,577.91547)"
|
|
||||||
clip-path="url(#clipPath35)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g35">
|
|
||||||
<text
|
|
||||||
id="text35"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,277.56733,640.7756)"
|
|
||||||
clip-path="url(#clipPath36)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:30px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 16.377001 24.743999 41.120998 59.028"
|
|
||||||
y="0"
|
|
||||||
id="tspan35">3.7V </tspan></text>
|
|
||||||
<text
|
|
||||||
id="text36"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,368.75873,640.7756)"
|
|
||||||
clip-path="url(#clipPath37)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:30px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
id="tspan36">→</tspan></text>
|
|
||||||
<text
|
|
||||||
id="text37"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,421.25873,640.7756)"
|
|
||||||
clip-path="url(#clipPath38)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:30px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 16.365 25.440001 41.805 59.700001"
|
|
||||||
y="0"
|
|
||||||
id="tspan37">3.3V </tspan></text>
|
|
||||||
<text
|
|
||||||
id="text38"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,351.572,688.95133)"
|
|
||||||
clip-path="url(#clipPath39)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:30px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 14.928 35.285999"
|
|
||||||
y="0"
|
|
||||||
id="tspan38">LDO</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g39" />
|
|
||||||
<g
|
|
||||||
id="g40">
|
|
||||||
<text
|
|
||||||
id="text40"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,228.32627,813.2016)"
|
|
||||||
clip-path="url(#clipPath40)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:60px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 22.379999 68.610001 107.958 126.708 166.758 197.49001 219.28799 252.04201"
|
|
||||||
y="0"
|
|
||||||
id="tspan40">IMU GY-91</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g41">
|
|
||||||
<path
|
|
||||||
id="path41"
|
|
||||||
d="M 0,0.9074655 C 105.6857,-1.737837 215.2075,1.400243 328.5653,10.32171 l 2.9904,0.24547"
|
|
||||||
style="fill:none;stroke:#d5d5d5;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,535.19533,640.938)"
|
|
||||||
clip-path="url(#clipPath42)" />
|
|
||||||
<path
|
|
||||||
id="path43"
|
|
||||||
d="m 728.9806,577.015 24.9013,9.9963 -22.9378,13.9233 z"
|
|
||||||
style="fill:#d5d5d5;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,1440)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g43" />
|
|
||||||
<g
|
|
||||||
id="g44">
|
|
||||||
<text
|
|
||||||
id="text44"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,775.772,611.41427)"
|
|
||||||
clip-path="url(#clipPath44)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:40px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 21.836 33.952 55.787998"
|
|
||||||
y="0"
|
|
||||||
id="tspan44">3.3V</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g45">
|
|
||||||
<path
|
|
||||||
id="path45"
|
|
||||||
d="M 25.43947,0 H 389.7043 c 7.4652,0 11.9443,0 14.9304,1.246647 4.3046,1.566779 7.6956,4.957749 9.2624,9.262433 1.2466,2.98608 1.2466,7.4652 1.2466,14.93039 V 127.0915 c 0,7.4652 0,11.9443 -1.2466,14.9304 -1.5668,4.3046 -4.9578,7.6956 -9.2624,9.2624 -2.9861,1.2466 -7.4652,1.2466 -14.9304,1.2466 H 25.43947 c -7.46519,0 -11.94431,0 -14.93039,-1.2466 C 6.204396,149.7175 2.813426,146.3265 1.246647,142.0219 0,139.0358 0,134.5567 0,127.0915 V 25.43947 C 0,17.97428 0,13.49516 1.246647,10.50908 2.813426,6.204396 6.204396,2.813426 10.50908,1.246647 13.49516,0 17.97428,0 25.43947,0 Z"
|
|
||||||
style="fill:none;stroke:#0076ba;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1003.2375,103.38667)"
|
|
||||||
clip-path="url(#clipPath46)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g46">
|
|
||||||
<text
|
|
||||||
id="text46"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1153.4068,236.92013)"
|
|
||||||
clip-path="url(#clipPath47)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:60px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 35.652 67.164001 86.736 106.788 138.36 159.972"
|
|
||||||
y="0"
|
|
||||||
id="tspan46">Battery</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g47">
|
|
||||||
<path
|
|
||||||
id="path48"
|
|
||||||
d="M 421.5528,0 C 235.366,47.76378 99.90873,120.8197 15.1811,219.1677 l -1.89962,2.3266"
|
|
||||||
style="fill:none;stroke:#d5d5d5;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,437.16707,258.90107)"
|
|
||||||
clip-path="url(#clipPath49)" />
|
|
||||||
<path
|
|
||||||
id="path50"
|
|
||||||
d="m 333.7589,674.2432 -5.8836,-26.1798 24.474,11.001 z"
|
|
||||||
style="fill:#d5d5d5;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,1440)"
|
|
||||||
clip-path="url(#clipPath51)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g51" />
|
|
||||||
<g
|
|
||||||
id="g52">
|
|
||||||
<text
|
|
||||||
id="text52"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,531.88053,356.73933)"
|
|
||||||
clip-path="url(#clipPath52)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:40px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 29.1 50.919998 62.060001 83.879997"
|
|
||||||
y="0"
|
|
||||||
id="tspan52">>3.7V</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g53">
|
|
||||||
<path
|
|
||||||
id="path53"
|
|
||||||
d="m 0,0 c 227.9607,25.58255 371.5626,109.9501 430.8056,253.1027 l 1.0572,2.8138"
|
|
||||||
style="fill:none;stroke:#d5d5d5;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1560.7787,215.3172)"
|
|
||||||
clip-path="url(#clipPath54)" />
|
|
||||||
<path
|
|
||||||
id="path55"
|
|
||||||
d="m 1590.159,661.1831 19.675,-18.2457 2.791,26.6872 z"
|
|
||||||
style="fill:#d5d5d5;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,1440)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g55" />
|
|
||||||
<g
|
|
||||||
id="g56">
|
|
||||||
<text
|
|
||||||
id="text56"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1861.6387,263.75013)"
|
|
||||||
clip-path="url(#clipPath56)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:40px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 29.1 50.919998 62.060001 83.879997"
|
|
||||||
y="0"
|
|
||||||
id="tspan56">>3.7V</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g57">
|
|
||||||
<path
|
|
||||||
id="path57"
|
|
||||||
d="M 25.43947,0 H 389.7043 c 7.4652,0 11.9443,0 14.9304,1.246647 4.3046,1.566779 7.6956,4.957749 9.2624,9.262433 1.2466,2.98608 1.2466,7.4652 1.2466,14.93039 V 127.0915 c 0,7.4652 0,11.9443 -1.2466,14.9304 -1.5668,4.3046 -4.9578,7.6956 -9.2624,9.2624 -2.9861,1.2466 -7.4652,1.2466 -14.9304,1.2466 H 25.43947 c -7.46519,0 -11.94431,0 -14.93039,-1.2466 C 6.204396,149.7175 2.813426,146.3265 1.246647,142.0219 0,139.0358 0,134.5567 0,127.0915 V 25.43947 C 0,17.97428 0,13.49516 1.246647,10.50908 2.813426,6.204396 6.204396,2.813426 10.50908,1.246647 13.49516,0 17.97428,0 25.43947,0 Z"
|
|
||||||
style="fill:none;stroke:#ff9300;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1894,1133.2392)"
|
|
||||||
clip-path="url(#clipPath58)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g58">
|
|
||||||
<text
|
|
||||||
id="text58"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,2010.608,1266.7732)"
|
|
||||||
clip-path="url(#clipPath59)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:60px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 46.23 78.809998 98.879997 131.46001 153.084 179.862"
|
|
||||||
y="0"
|
|
||||||
id="tspan58">Motors </tspan></text>
|
|
||||||
<text
|
|
||||||
id="text59"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,2275.4127,1266.7732)"
|
|
||||||
clip-path="url(#clipPath60)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:40px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 19.804001"
|
|
||||||
y="0"
|
|
||||||
id="tspan59">x4</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g60">
|
|
||||||
<path
|
|
||||||
id="path61"
|
|
||||||
d="M 25.43947,0 H 389.7043 c 7.4652,0 11.9443,0 14.9304,1.246647 4.3046,1.566779 7.6956,4.957749 9.2624,9.262433 1.2466,2.98608 1.2466,7.4652 1.2466,14.93039 V 127.0915 c 0,7.4652 0,11.9443 -1.2466,14.9304 -1.5668,4.3046 -4.9578,7.6956 -9.2624,9.2624 -2.9861,1.2466 -7.4652,1.2466 -14.9304,1.2466 H 25.43947 c -7.46519,0 -11.94431,0 -14.93039,-1.2466 C 6.204396,149.7175 2.813426,146.3265 1.246647,142.0219 0,139.0358 0,134.5567 0,127.0915 V 25.43947 C 0,17.97428 0,13.49516 1.246647,10.50908 2.813426,6.204396 6.204396,2.813426 10.50908,1.246647 13.49516,0 17.97428,0 25.43947,0 Z"
|
|
||||||
style="fill:none;stroke:#0076ba;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,1894,586.75013)"
|
|
||||||
clip-path="url(#clipPath62)" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g62">
|
|
||||||
<text
|
|
||||||
id="text62"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,2061.7613,720.284)"
|
|
||||||
clip-path="url(#clipPath63)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:60px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 33.66 67.085999 103.122"
|
|
||||||
y="0"
|
|
||||||
id="tspan62">ESC </tspan></text>
|
|
||||||
<text
|
|
||||||
id="text63"
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(1.3333333,0,0,1.3333333,2224.2613,720.284)"
|
|
||||||
clip-path="url(#clipPath64)"><tspan
|
|
||||||
style="font-variant:normal;font-weight:normal;font-size:40px;font-family:Tahoma,Arial;writing-mode:lr-tb;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
x="0 19.804001"
|
|
||||||
y="0"
|
|
||||||
id="tspan63">x4</tspan></text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 36 KiB |
@@ -1,169 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Implementation of command line interface
|
|
||||||
|
|
||||||
#include "pid.h"
|
|
||||||
#include "vector.h"
|
|
||||||
|
|
||||||
extern PID rollRatePID, pitchRatePID, yawRatePID, rollPID, pitchPID;
|
|
||||||
extern LowPassFilter<Vector> ratesFilter;
|
|
||||||
|
|
||||||
const char* motd =
|
|
||||||
"\nWelcome to\n"
|
|
||||||
" _______ __ __ ___ ___\n"
|
|
||||||
"| ____|| | | | \\ \\ / /\n"
|
|
||||||
"| |__ | | | | \\ V /\n"
|
|
||||||
"| __| | | | | > <\n"
|
|
||||||
"| | | `----.| | / . \\\n"
|
|
||||||
"|__| |_______||__| /__/ \\__\\\n\n"
|
|
||||||
"Commands:\n\n"
|
|
||||||
"help - show help\n"
|
|
||||||
"show - show all parameters\n"
|
|
||||||
"<name> <value> - set parameter\n"
|
|
||||||
"ps - show pitch/roll/yaw\n"
|
|
||||||
"psq - show attitude quaternion\n"
|
|
||||||
"imu - show IMU data\n"
|
|
||||||
"rc - show RC data\n"
|
|
||||||
"mot - show motor data\n"
|
|
||||||
"log - dump in-RAM log\n"
|
|
||||||
"cg - calibrate gyro\n"
|
|
||||||
"ca - calibrate accel\n"
|
|
||||||
"fullmot <n> - test motor on all signals\n"
|
|
||||||
"reset - reset drone's state\n";
|
|
||||||
|
|
||||||
const struct Param {
|
|
||||||
const char* name;
|
|
||||||
float* value;
|
|
||||||
float* value2;
|
|
||||||
} params[] = {
|
|
||||||
{"rp", &rollRatePID.p, &pitchRatePID.p},
|
|
||||||
{"ri", &rollRatePID.i, &pitchRatePID.i},
|
|
||||||
{"rd", &rollRatePID.d, &pitchRatePID.d},
|
|
||||||
|
|
||||||
{"ap", &rollPID.p, &pitchPID.p},
|
|
||||||
{"ai", &rollPID.i, &pitchPID.i},
|
|
||||||
{"ad", &rollPID.d, &pitchPID.d},
|
|
||||||
|
|
||||||
{"yp", &yawRatePID.p, nullptr},
|
|
||||||
{"yi", &yawRatePID.i, nullptr},
|
|
||||||
{"yd", &yawRatePID.d, nullptr},
|
|
||||||
|
|
||||||
{"lpr", &ratesFilter.alpha, nullptr},
|
|
||||||
{"lpd", &rollRatePID.lpf.alpha, &pitchRatePID.lpf.alpha},
|
|
||||||
|
|
||||||
{"ss", &loopFreq, nullptr},
|
|
||||||
{"dt", &dt, nullptr},
|
|
||||||
{"t", &t, nullptr},
|
|
||||||
};
|
|
||||||
|
|
||||||
void doCommand(String& command, String& value)
|
|
||||||
{
|
|
||||||
if (command == "help" || command == "motd") {
|
|
||||||
Serial.println(motd);
|
|
||||||
} else if (command == "show") {
|
|
||||||
showTable();
|
|
||||||
} else if (command == "ps") {
|
|
||||||
Vector a = attitude.toEulerZYX();
|
|
||||||
Serial.printf("roll: %f pitch: %f yaw: %f\n", a.x * RAD_TO_DEG, a.y * RAD_TO_DEG, a.z * RAD_TO_DEG);
|
|
||||||
} else if (command == "psq") {
|
|
||||||
Serial.printf("qx: %f qy: %f qz: %f qw: %f\n", attitude.x, attitude.y, attitude.z, attitude.w);
|
|
||||||
} else if (command == "imu") {
|
|
||||||
Serial.printf("gyro: %f %f %f\n", rates.x, rates.y, rates.z);
|
|
||||||
Serial.printf("acc: %f %f %f\n", acc.x, acc.y, acc.z);
|
|
||||||
printIMUCal();
|
|
||||||
} else if (command == "rc") {
|
|
||||||
Serial.printf("Raw: throttle %d yaw %d pitch %d roll %d aux %d mode %d\n",
|
|
||||||
channels[RC_CHANNEL_THROTTLE], channels[RC_CHANNEL_YAW], channels[RC_CHANNEL_PITCH],
|
|
||||||
channels[RC_CHANNEL_ROLL], channels[RC_CHANNEL_AUX], channels[RC_CHANNEL_MODE]);
|
|
||||||
Serial.printf("Control: throttle %f yaw %f pitch %f roll %f aux %f mode %f\n",
|
|
||||||
controls[RC_CHANNEL_THROTTLE], controls[RC_CHANNEL_YAW], controls[RC_CHANNEL_PITCH],
|
|
||||||
controls[RC_CHANNEL_ROLL], controls[RC_CHANNEL_AUX], controls[RC_CHANNEL_MODE]);
|
|
||||||
Serial.printf("Mode: %s\n", getModeName());
|
|
||||||
} else if (command == "mot") {
|
|
||||||
Serial.printf("MOTOR front-right %f front-left %f rear-right %f rear-left %f\n",
|
|
||||||
motors[MOTOR_FRONT_RIGHT], motors[MOTOR_FRONT_LEFT], motors[MOTOR_REAR_RIGHT], motors[MOTOR_REAR_LEFT]);
|
|
||||||
} else if (command == "log") {
|
|
||||||
dumpLog();
|
|
||||||
} else if (command == "cg") {
|
|
||||||
calibrateGyro();
|
|
||||||
} else if (command == "ca") {
|
|
||||||
calibrateAccel();
|
|
||||||
} else if (command == "mfr") {
|
|
||||||
cliTestMotor(MOTOR_FRONT_RIGHT);
|
|
||||||
} else if (command == "mfl") {
|
|
||||||
cliTestMotor(MOTOR_FRONT_LEFT);
|
|
||||||
} else if (command == "mrr") {
|
|
||||||
cliTestMotor(MOTOR_REAR_RIGHT);
|
|
||||||
} else if (command == "mrl") {
|
|
||||||
cliTestMotor(MOTOR_REAR_LEFT);
|
|
||||||
} else if (command == "fullmot") {
|
|
||||||
fullMotorTest(value.toInt(), false);
|
|
||||||
} else if (command == "reset") {
|
|
||||||
attitude = Quaternion();
|
|
||||||
} else {
|
|
||||||
float val = value.toFloat();
|
|
||||||
// TODO: on error returns 0, check invalid value
|
|
||||||
|
|
||||||
for (uint8_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
|
||||||
if (command == params[i].name) {
|
|
||||||
*params[i].value = val;
|
|
||||||
if (params[i].value2 != nullptr) *params[i].value2 = val;
|
|
||||||
Serial.print(command);
|
|
||||||
Serial.print(" = ");
|
|
||||||
Serial.println(val, 4);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Serial.println("Invalid command: " + command);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void showTable()
|
|
||||||
{
|
|
||||||
for (uint8_t i = 0; i < sizeof(params) / sizeof(params[0]); i++) {
|
|
||||||
Serial.print(params[i].name);
|
|
||||||
Serial.print(" ");
|
|
||||||
Serial.println(*params[i].value, 5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void cliTestMotor(uint8_t n)
|
|
||||||
{
|
|
||||||
Serial.printf("Testing motor %d\n", n);
|
|
||||||
motors[n] = 1;
|
|
||||||
sendMotors();
|
|
||||||
delay(5000);
|
|
||||||
motors[n] = 0;
|
|
||||||
sendMotors();
|
|
||||||
Serial.println("Done");
|
|
||||||
}
|
|
||||||
|
|
||||||
void parseInput()
|
|
||||||
{
|
|
||||||
static bool showMotd = true;
|
|
||||||
static String command;
|
|
||||||
static String value;
|
|
||||||
static bool parsingCommand = true;
|
|
||||||
|
|
||||||
if (showMotd) {
|
|
||||||
Serial.println(motd);
|
|
||||||
showMotd = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (Serial.available()) {
|
|
||||||
char c = Serial.read();
|
|
||||||
if (c == '\n') {
|
|
||||||
parsingCommand = true;
|
|
||||||
if (!command.isEmpty()) {
|
|
||||||
doCommand(command, value);
|
|
||||||
}
|
|
||||||
command.clear();
|
|
||||||
value.clear();
|
|
||||||
} else if (c == ' ') {
|
|
||||||
parsingCommand = false;
|
|
||||||
} else {
|
|
||||||
(parsingCommand ? command : value) += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Flight control
|
|
||||||
|
|
||||||
#include "vector.h"
|
|
||||||
#include "quaternion.h"
|
|
||||||
#include "pid.h"
|
|
||||||
#include "lpf.h"
|
|
||||||
|
|
||||||
#define PITCHRATE_P 0.05
|
|
||||||
#define PITCHRATE_I 0.2
|
|
||||||
#define PITCHRATE_D 0.001
|
|
||||||
#define PITCHRATE_I_LIM 0.3
|
|
||||||
#define ROLLRATE_P PITCHRATE_P
|
|
||||||
#define ROLLRATE_I PITCHRATE_I
|
|
||||||
#define ROLLRATE_D PITCHRATE_D
|
|
||||||
#define ROLLRATE_I_LIM PITCHRATE_I_LIM
|
|
||||||
#define YAWRATE_P 0.3
|
|
||||||
#define YAWRATE_I 0.0
|
|
||||||
#define YAWRATE_D 0.0
|
|
||||||
#define YAWRATE_I_LIM 0.3
|
|
||||||
#define ROLL_P 4.5
|
|
||||||
#define ROLL_I 0
|
|
||||||
#define ROLL_D 0
|
|
||||||
#define PITCH_P ROLL_P
|
|
||||||
#define PITCH_I ROLL_I
|
|
||||||
#define PITCH_D ROLL_D
|
|
||||||
#define YAW_P 3
|
|
||||||
#define PITCHRATE_MAX 360 * DEG_TO_RAD
|
|
||||||
#define ROLLRATE_MAX 360 * DEG_TO_RAD
|
|
||||||
#define YAWRATE_MAX 360 * DEG_TO_RAD
|
|
||||||
#define MAX_TILT 30 * DEG_TO_RAD
|
|
||||||
|
|
||||||
#define RATES_LFP_ALPHA 0.8 // cutoff frequency ~ 250 Hz
|
|
||||||
#define RATES_D_LPF_ALPHA 0.2 // cutoff frequency ~ 40 Hz
|
|
||||||
|
|
||||||
enum { MANUAL, ACRO, STAB } mode = STAB;
|
|
||||||
enum { YAW, YAW_RATE } yawMode = YAW;
|
|
||||||
bool armed = false;
|
|
||||||
|
|
||||||
PID rollRatePID(ROLLRATE_P, ROLLRATE_I, ROLLRATE_D, ROLLRATE_I_LIM, RATES_D_LPF_ALPHA);
|
|
||||||
PID pitchRatePID(PITCHRATE_P, PITCHRATE_I, PITCHRATE_D, PITCHRATE_I_LIM, RATES_D_LPF_ALPHA);
|
|
||||||
PID yawRatePID(YAWRATE_P, YAWRATE_I, YAWRATE_D);
|
|
||||||
PID rollPID(ROLL_P, ROLL_I, ROLL_D);
|
|
||||||
PID pitchPID(PITCH_P, PITCH_I, PITCH_D);
|
|
||||||
PID yawPID(YAW_P, 0, 0);
|
|
||||||
|
|
||||||
LowPassFilter<Vector> ratesFilter(RATES_LFP_ALPHA);
|
|
||||||
|
|
||||||
Quaternion attitudeTarget;
|
|
||||||
Vector ratesTarget;
|
|
||||||
Vector torqueTarget;
|
|
||||||
float thrustTarget;
|
|
||||||
|
|
||||||
void control()
|
|
||||||
{
|
|
||||||
interpretRC();
|
|
||||||
if (mode == STAB) {
|
|
||||||
controlAttitude();
|
|
||||||
controlRate();
|
|
||||||
controlTorque();
|
|
||||||
} else if (mode == ACRO) {
|
|
||||||
controlRate();
|
|
||||||
controlTorque();
|
|
||||||
} else if (mode == MANUAL) {
|
|
||||||
controlTorque();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void interpretRC()
|
|
||||||
{
|
|
||||||
if (controls[RC_CHANNEL_MODE] < 0.25) {
|
|
||||||
mode = MANUAL;
|
|
||||||
} else if (controls[RC_CHANNEL_MODE] < 0.75) {
|
|
||||||
mode = ACRO;
|
|
||||||
} else {
|
|
||||||
mode = STAB;
|
|
||||||
}
|
|
||||||
|
|
||||||
armed = controls[RC_CHANNEL_THROTTLE] >= 0.05 && controls[RC_CHANNEL_AUX] >= 0.5;
|
|
||||||
thrustTarget = controls[RC_CHANNEL_THROTTLE];
|
|
||||||
|
|
||||||
if (mode == ACRO) {
|
|
||||||
yawMode = YAW_RATE;
|
|
||||||
ratesTarget.x = controls[RC_CHANNEL_ROLL] * ROLLRATE_MAX;
|
|
||||||
ratesTarget.y = -controls[RC_CHANNEL_PITCH] * PITCHRATE_MAX; // up pitch stick means tilt clockwise in frd
|
|
||||||
ratesTarget.z = controls[RC_CHANNEL_YAW] * YAWRATE_MAX;
|
|
||||||
|
|
||||||
} else if (mode == STAB) {
|
|
||||||
yawMode = controls[RC_CHANNEL_YAW] == 0 ? YAW : YAW_RATE;
|
|
||||||
|
|
||||||
attitudeTarget = Quaternion::fromEulerZYX(
|
|
||||||
controls[RC_CHANNEL_ROLL] * MAX_TILT,
|
|
||||||
-controls[RC_CHANNEL_PITCH] * MAX_TILT,
|
|
||||||
attitudeTarget.getYaw());
|
|
||||||
ratesTarget.z = controls[RC_CHANNEL_YAW] * YAWRATE_MAX;
|
|
||||||
|
|
||||||
} else if (mode == MANUAL) {
|
|
||||||
// passthrough mode
|
|
||||||
yawMode = YAW_RATE;
|
|
||||||
torqueTarget = Vector(controls[RC_CHANNEL_ROLL], -controls[RC_CHANNEL_PITCH], controls[RC_CHANNEL_YAW]) * 0.01;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (yawMode == YAW_RATE || !motorsActive()) {
|
|
||||||
// update yaw target as we don't have control over the yaw
|
|
||||||
attitudeTarget.setYaw(attitude.getYaw());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void controlAttitude()
|
|
||||||
{
|
|
||||||
if (!armed) {
|
|
||||||
rollPID.reset();
|
|
||||||
pitchPID.reset();
|
|
||||||
yawPID.reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Vector up(0, 0, -1);
|
|
||||||
Vector upActual = attitude.rotate(up);
|
|
||||||
Vector upTarget = attitudeTarget.rotate(up);
|
|
||||||
|
|
||||||
Vector error = Vector::angularRatesBetweenVectors(upTarget, upActual);
|
|
||||||
|
|
||||||
ratesTarget.x = rollPID.update(error.x, dt);
|
|
||||||
ratesTarget.y = pitchPID.update(error.y, dt);
|
|
||||||
|
|
||||||
if (yawMode == YAW) {
|
|
||||||
ratesTarget.z = yawPID.update(wrapAngle(attitudeTarget.getYaw() - attitude.getYaw()), dt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void controlRate()
|
|
||||||
{
|
|
||||||
if (!armed) {
|
|
||||||
rollRatePID.reset();
|
|
||||||
pitchRatePID.reset();
|
|
||||||
yawRatePID.reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector ratesFiltered = ratesFilter.update(rates);
|
|
||||||
|
|
||||||
torqueTarget.x = rollRatePID.update(ratesTarget.x - ratesFiltered.x, dt); // un-normalized "torque"
|
|
||||||
torqueTarget.y = pitchRatePID.update(ratesTarget.y - ratesFiltered.y, dt);
|
|
||||||
torqueTarget.z = yawRatePID.update(ratesTarget.z - ratesFiltered.z, dt);
|
|
||||||
}
|
|
||||||
|
|
||||||
void controlTorque()
|
|
||||||
{
|
|
||||||
if (!armed) {
|
|
||||||
memset(motors, 0, sizeof(motors));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
motors[MOTOR_FRONT_LEFT] = thrustTarget + torqueTarget.y + torqueTarget.x - torqueTarget.z;
|
|
||||||
motors[MOTOR_FRONT_RIGHT] = thrustTarget + torqueTarget.y - torqueTarget.x + torqueTarget.z;
|
|
||||||
motors[MOTOR_REAR_LEFT] = thrustTarget - torqueTarget.y + torqueTarget.x + torqueTarget.z;
|
|
||||||
motors[MOTOR_REAR_RIGHT] = thrustTarget - torqueTarget.y - torqueTarget.x - torqueTarget.z;
|
|
||||||
|
|
||||||
motors[0] = constrain(motors[0], 0, 1);
|
|
||||||
motors[1] = constrain(motors[1], 0, 1);
|
|
||||||
motors[2] = constrain(motors[2], 0, 1);
|
|
||||||
motors[3] = constrain(motors[3], 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool motorsActive()
|
|
||||||
{
|
|
||||||
return motors[0] > 0 || motors[1] > 0 || motors[2] > 0 || motors[3] > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* getModeName()
|
|
||||||
{
|
|
||||||
switch (mode) {
|
|
||||||
case MANUAL: return "MANUAL";
|
|
||||||
case ACRO: return "ACRO";
|
|
||||||
case STAB: return "STAB";
|
|
||||||
default: return "UNKNOWN";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Attitude estimation from gyro and accelerometer
|
|
||||||
|
|
||||||
#include "quaternion.h"
|
|
||||||
#include "vector.h"
|
|
||||||
|
|
||||||
#define ONE_G 9.807f
|
|
||||||
#define ACC_MIN 0.9f
|
|
||||||
#define ACC_MAX 1.1f
|
|
||||||
#define WEIGHT_ACC 0.5f
|
|
||||||
|
|
||||||
void estimate()
|
|
||||||
{
|
|
||||||
applyGyro();
|
|
||||||
applyAcc();
|
|
||||||
signalizeHorizontality();
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyGyro()
|
|
||||||
{
|
|
||||||
// applying gyro
|
|
||||||
attitude *= Quaternion::fromAngularRates(rates * dt);
|
|
||||||
attitude.normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyAcc()
|
|
||||||
{
|
|
||||||
// test should we apply accelerometer gravity correction
|
|
||||||
float accNorm = acc.norm();
|
|
||||||
bool landed = !motorsActive() && abs(accNorm - ONE_G) < ONE_G * 0.1f;
|
|
||||||
|
|
||||||
if (!landed) return;
|
|
||||||
|
|
||||||
// calculate accelerometer correction
|
|
||||||
Vector up = attitude.rotate(Vector(0, 0, -1));
|
|
||||||
Vector correction = Vector::angularRatesBetweenVectors(acc, up) * dt * WEIGHT_ACC;
|
|
||||||
|
|
||||||
// apply correction
|
|
||||||
attitude *= Quaternion::fromAngularRates(correction);
|
|
||||||
attitude.normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
void signalizeHorizontality()
|
|
||||||
{
|
|
||||||
float angle = Vector::angleBetweenVectors(attitude.rotate(Vector(0, 0, -1)), Vector(0, 0, -1));
|
|
||||||
setLED(angle < 15 * DEG_TO_RAD);
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Main firmware file
|
|
||||||
|
|
||||||
#include "vector.h"
|
|
||||||
#include "quaternion.h"
|
|
||||||
|
|
||||||
#define SERIAL_BAUDRATE 115200
|
|
||||||
|
|
||||||
#define WIFI_ENABLED 0
|
|
||||||
|
|
||||||
#define RC_CHANNELS 6
|
|
||||||
#define RC_CHANNEL_THROTTLE 2
|
|
||||||
#define RC_CHANNEL_YAW 3
|
|
||||||
#define RC_CHANNEL_PITCH 1
|
|
||||||
#define RC_CHANNEL_ROLL 0
|
|
||||||
#define RC_CHANNEL_AUX 4
|
|
||||||
#define RC_CHANNEL_MODE 5
|
|
||||||
|
|
||||||
#define MOTOR_REAR_LEFT 0
|
|
||||||
#define MOTOR_FRONT_LEFT 3
|
|
||||||
#define MOTOR_FRONT_RIGHT 2
|
|
||||||
#define MOTOR_REAR_RIGHT 1
|
|
||||||
|
|
||||||
float t = NAN; // current step time, s
|
|
||||||
float dt; // time delta from previous step, s
|
|
||||||
float loopFreq; // loop frequency, Hz
|
|
||||||
uint16_t channels[16]; // raw rc channels
|
|
||||||
float controls[RC_CHANNELS]; // normalized controls in range [-1..1] ([0..1] for throttle)
|
|
||||||
Vector rates; // angular rates, rad/s
|
|
||||||
Vector acc; // accelerometer data, m/s/s
|
|
||||||
Quaternion attitude; // estimated attitude
|
|
||||||
float motors[4]; // normalized motors thrust in range [-1..1]
|
|
||||||
|
|
||||||
void setup()
|
|
||||||
{
|
|
||||||
Serial.begin(SERIAL_BAUDRATE);
|
|
||||||
Serial.println("Initializing flix");
|
|
||||||
setupLED();
|
|
||||||
setupMotors();
|
|
||||||
setLED(true);
|
|
||||||
#if WIFI_ENABLED == 1
|
|
||||||
setupWiFi();
|
|
||||||
#endif
|
|
||||||
setupIMU();
|
|
||||||
setupRC();
|
|
||||||
|
|
||||||
setLED(false);
|
|
||||||
Serial.println("Initializing complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop()
|
|
||||||
{
|
|
||||||
if (!readIMU()) return;
|
|
||||||
|
|
||||||
step();
|
|
||||||
readRC();
|
|
||||||
estimate();
|
|
||||||
control();
|
|
||||||
sendMotors();
|
|
||||||
parseInput();
|
|
||||||
#if WIFI_ENABLED == 1
|
|
||||||
sendMavlink();
|
|
||||||
#endif
|
|
||||||
logData();
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Work with the IMU sensor
|
|
||||||
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <MPU9250.h>
|
|
||||||
|
|
||||||
#define IMU_CS_PIN 4 // chip-select pin for IMU SPI connection
|
|
||||||
#define CALIBRATE_GYRO_ON_START true
|
|
||||||
|
|
||||||
MPU9250 IMU(SPI, IMU_CS_PIN);
|
|
||||||
|
|
||||||
void setupIMU()
|
|
||||||
{
|
|
||||||
Serial.println("Setup IMU");
|
|
||||||
|
|
||||||
auto status = IMU.begin();
|
|
||||||
if (status < 0) {
|
|
||||||
while (true) {
|
|
||||||
Serial.printf("IMU begin error: %d\n", status);
|
|
||||||
delay(1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CALIBRATE_GYRO_ON_START) {
|
|
||||||
calibrateGyro();
|
|
||||||
} else {
|
|
||||||
loadGyroCal();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadAccelCal();
|
|
||||||
|
|
||||||
IMU.setSrd(0); // set sample rate to 1000 Hz
|
|
||||||
// NOTE: very important, without the above the rate would be terrible 50 Hz
|
|
||||||
}
|
|
||||||
|
|
||||||
bool readIMU()
|
|
||||||
{
|
|
||||||
if (IMU.readSensor() < 0) {
|
|
||||||
Serial.println("IMU read error");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto lastRates = rates;
|
|
||||||
|
|
||||||
rates.x = IMU.getGyroX_rads();
|
|
||||||
rates.y = IMU.getGyroY_rads();
|
|
||||||
rates.z = IMU.getGyroZ_rads();
|
|
||||||
acc.x = IMU.getAccelX_mss();
|
|
||||||
acc.y = IMU.getAccelY_mss();
|
|
||||||
acc.z = IMU.getAccelZ_mss();
|
|
||||||
|
|
||||||
return rates != lastRates;
|
|
||||||
}
|
|
||||||
|
|
||||||
void calibrateGyro()
|
|
||||||
{
|
|
||||||
Serial.println("Calibrating gyro, stand still");
|
|
||||||
delay(500);
|
|
||||||
int status = IMU.calibrateGyro();
|
|
||||||
Serial.printf("Calibration status: %d\n", status);
|
|
||||||
IMU.setSrd(0);
|
|
||||||
printIMUCal();
|
|
||||||
}
|
|
||||||
|
|
||||||
void calibrateAccel()
|
|
||||||
{
|
|
||||||
Serial.println("Cal accel: place level"); delay(3000);
|
|
||||||
IMU.calibrateAccel();
|
|
||||||
Serial.println("Cal accel: place nose up"); delay(3000);
|
|
||||||
IMU.calibrateAccel();
|
|
||||||
Serial.println("Cal accel: place nose down"); delay(3000);
|
|
||||||
IMU.calibrateAccel();
|
|
||||||
Serial.println("Cal accel: place on right side"); delay(3000);
|
|
||||||
IMU.calibrateAccel();
|
|
||||||
Serial.println("Cal accel: place on left side"); delay(3000);
|
|
||||||
IMU.calibrateAccel();
|
|
||||||
Serial.println("Cal accel: upside down"); delay(300);
|
|
||||||
IMU.calibrateAccel();
|
|
||||||
printIMUCal();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadAccelCal()
|
|
||||||
{
|
|
||||||
// NOTE: this should be changed to the actual values
|
|
||||||
IMU.setAccelCalX(-0.0048542023, 1.0008112192);
|
|
||||||
IMU.setAccelCalY(0.0521845818, 0.9985780716);
|
|
||||||
IMU.setAccelCalZ(0.5754694939, 1.0045746565);
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadGyroCal()
|
|
||||||
{
|
|
||||||
// NOTE: this should be changed to the actual values
|
|
||||||
IMU.setGyroBiasX_rads(-0.0185128022);
|
|
||||||
IMU.setGyroBiasY_rads(-0.0262369743);
|
|
||||||
IMU.setGyroBiasZ_rads(0.0163032326);
|
|
||||||
}
|
|
||||||
|
|
||||||
void printIMUCal()
|
|
||||||
{
|
|
||||||
Serial.printf("gyro bias: %f %f %f\n", IMU.getGyroBiasX_rads(), IMU.getGyroBiasY_rads(), IMU.getGyroBiasZ_rads());
|
|
||||||
Serial.printf("accel bias: %f %f %f\n", IMU.getAccelBiasX_mss(), IMU.getAccelBiasY_mss(), IMU.getAccelBiasZ_mss());
|
|
||||||
Serial.printf("accel scale: %f %f %f\n", IMU.getAccelScaleFactorX(), IMU.getAccelScaleFactorY(), IMU.getAccelScaleFactorZ());
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Main LED control
|
|
||||||
|
|
||||||
#define BLINK_PERIOD 500000
|
|
||||||
|
|
||||||
void setupLED()
|
|
||||||
{
|
|
||||||
pinMode(LED_BUILTIN, OUTPUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setLED(bool on)
|
|
||||||
{
|
|
||||||
digitalWrite(LED_BUILTIN, on ? HIGH : LOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
void blinkLED()
|
|
||||||
{
|
|
||||||
setLED(micros() / BLINK_PERIOD % 2);
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// In-RAM logging
|
|
||||||
|
|
||||||
#define LOG_RATE 100
|
|
||||||
#define LOG_DURATION 10
|
|
||||||
#define LOG_PERIOD 1.0 / LOG_RATE
|
|
||||||
#define LOG_SIZE LOG_DURATION * LOG_RATE
|
|
||||||
#define LOG_COLUMNS 14
|
|
||||||
|
|
||||||
float logBuffer[LOG_SIZE][LOG_COLUMNS]; // * 4 (float)
|
|
||||||
int logPointer = 0;
|
|
||||||
|
|
||||||
void logData()
|
|
||||||
{
|
|
||||||
if (!armed) return;
|
|
||||||
|
|
||||||
static float logTime = 0;
|
|
||||||
if (t - logTime < LOG_PERIOD) return;
|
|
||||||
logTime = t;
|
|
||||||
|
|
||||||
logBuffer[logPointer][0] = t;
|
|
||||||
logBuffer[logPointer][1] = rates.x;
|
|
||||||
logBuffer[logPointer][2] = rates.y;
|
|
||||||
logBuffer[logPointer][3] = rates.z;
|
|
||||||
logBuffer[logPointer][4] = ratesTarget.x;
|
|
||||||
logBuffer[logPointer][5] = ratesTarget.y;
|
|
||||||
logBuffer[logPointer][6] = ratesTarget.z;
|
|
||||||
logBuffer[logPointer][7] = attitude.toEulerZYX().x;
|
|
||||||
logBuffer[logPointer][8] = attitude.toEulerZYX().y;
|
|
||||||
logBuffer[logPointer][9] = attitude.toEulerZYX().z;
|
|
||||||
logBuffer[logPointer][10] = attitudeTarget.toEulerZYX().x;
|
|
||||||
logBuffer[logPointer][11] = attitudeTarget.toEulerZYX().y;
|
|
||||||
logBuffer[logPointer][12] = attitudeTarget.toEulerZYX().z;
|
|
||||||
logBuffer[logPointer][13] = thrustTarget;
|
|
||||||
|
|
||||||
logPointer++;
|
|
||||||
if (logPointer >= LOG_SIZE) {
|
|
||||||
logPointer = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void dumpLog()
|
|
||||||
{
|
|
||||||
Serial.printf("t,rates.x,rates.y,rates.z,ratesTarget.x,ratesTarget.y,ratesTarget.z,"
|
|
||||||
"attitude.x,attitude.y,attitude.z,attitudeTarget.x,attitudeTarget.y,attitudeTarget.z,thrustTarget\n");
|
|
||||||
for (int i = 0; i < LOG_SIZE; i++) {
|
|
||||||
for (int j = 0; j < LOG_COLUMNS - 1; j++) {
|
|
||||||
Serial.printf("%f,", logBuffer[i][j]);
|
|
||||||
}
|
|
||||||
Serial.printf("%f\n", logBuffer[i][LOG_COLUMNS - 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +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 (alpha == 1) { // filter disabled
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!initialized) {
|
|
||||||
output = input;
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
return output = output * (1 - alpha) + input * alpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setCutOffFrequency(float cutOffFreq, float dt)
|
|
||||||
{
|
|
||||||
alpha = 1 - exp(-2 * PI * cutOffFreq * dt);
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset()
|
|
||||||
{
|
|
||||||
initialized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool initialized = false;
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// MAVLink communication
|
|
||||||
|
|
||||||
#if WIFI_ENABLED == 1
|
|
||||||
|
|
||||||
#include "mavlink/common/mavlink.h"
|
|
||||||
|
|
||||||
#define SYSTEM_ID 1
|
|
||||||
#define PERIOD_SLOW 1.0
|
|
||||||
#define PERIOD_FAST 0.1
|
|
||||||
|
|
||||||
void sendMavlink()
|
|
||||||
{
|
|
||||||
static float lastSlow = 0;
|
|
||||||
static float lastFast = 0;
|
|
||||||
|
|
||||||
mavlink_message_t msg;
|
|
||||||
uint32_t time = t * 1000;
|
|
||||||
|
|
||||||
if (t - lastSlow >= PERIOD_SLOW) {
|
|
||||||
lastSlow = t;
|
|
||||||
|
|
||||||
mavlink_msg_heartbeat_pack(SYSTEM_ID, MAV_COMP_ID_AUTOPILOT1, &msg, MAV_TYPE_QUADROTOR,
|
|
||||||
MAV_AUTOPILOT_GENERIC, MAV_MODE_FLAG_MANUAL_INPUT_ENABLED | armed ? MAV_MODE_FLAG_SAFETY_ARMED : 0,
|
|
||||||
0, MAV_STATE_STANDBY);
|
|
||||||
sendMessage(&msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t - lastFast >= PERIOD_FAST) {
|
|
||||||
lastFast = t;
|
|
||||||
|
|
||||||
const float zeroQuat[] = {0, 0, 0, 0};
|
|
||||||
mavlink_msg_attitude_quaternion_pack(SYSTEM_ID, MAV_COMP_ID_AUTOPILOT1, &msg,
|
|
||||||
time, attitude.w, attitude.x, attitude.y, attitude.z, rates.x, rates.y, rates.z, zeroQuat);
|
|
||||||
sendMessage(&msg);
|
|
||||||
|
|
||||||
mavlink_msg_rc_channels_scaled_pack(SYSTEM_ID, MAV_COMP_ID_AUTOPILOT1, &msg, time, 0,
|
|
||||||
controls[0] * 10000, controls[1] * 10000, controls[2] * 10000,
|
|
||||||
controls[3] * 10000, controls[4] * 10000, controls[5] * 10000,
|
|
||||||
UINT16_MAX, UINT16_MAX, 255);
|
|
||||||
sendMessage(&msg);
|
|
||||||
|
|
||||||
float actuator[32];
|
|
||||||
memcpy(motors, actuator, 4 * sizeof(float));
|
|
||||||
mavlink_msg_actuator_output_status_pack(SYSTEM_ID, MAV_COMP_ID_AUTOPILOT1, &msg, time, 4, actuator);
|
|
||||||
sendMessage(&msg);
|
|
||||||
|
|
||||||
mavlink_msg_scaled_imu_pack(SYSTEM_ID, MAV_COMP_ID_AUTOPILOT1, &msg, time,
|
|
||||||
acc.x * 1000, acc.y * 1000, acc.z * 1000,
|
|
||||||
rates.x * 1000, rates.y * 1000, rates.z * 1000,
|
|
||||||
0, 0, 0, 0);
|
|
||||||
sendMessage(&msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void sendMessage(const void *msg)
|
|
||||||
{
|
|
||||||
uint8_t buf[MAVLINK_MAX_PACKET_LEN];
|
|
||||||
uint16_t len = mavlink_msg_to_send_buffer(buf, (mavlink_message_t *)msg);
|
|
||||||
sendWiFi(buf, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Motors output control
|
|
||||||
// Motor: 8520 3.7V
|
|
||||||
// ESC: KINGDUO Micro Mini 4A 1S Brushed Esc 3.6-6V
|
|
||||||
|
|
||||||
#define MOTOR_0_PIN 12
|
|
||||||
#define MOTOR_1_PIN 13
|
|
||||||
#define MOTOR_2_PIN 14
|
|
||||||
#define MOTOR_3_PIN 15
|
|
||||||
|
|
||||||
#define PWM_FREQUENCY 200
|
|
||||||
#define PWM_RESOLUTION 8
|
|
||||||
|
|
||||||
#define PWM_NEUTRAL 1500
|
|
||||||
|
|
||||||
const uint16_t pwmMin[] = {1600, 1600, 1600, 1600};
|
|
||||||
const uint16_t pwmMax[] = {2300, 2300, 2300, 2300};
|
|
||||||
const uint16_t pwmReverseMin[] = {1390, 1440, 1440, 1440};
|
|
||||||
const uint16_t pwmReverseMax[] = {1100, 1100, 1100, 1100};
|
|
||||||
|
|
||||||
void setupMotors() {
|
|
||||||
Serial.println("Setup Motors");
|
|
||||||
|
|
||||||
// configure PWM channels
|
|
||||||
ledcSetup(0, PWM_FREQUENCY, PWM_RESOLUTION);
|
|
||||||
ledcSetup(1, PWM_FREQUENCY, PWM_RESOLUTION);
|
|
||||||
ledcSetup(2, PWM_FREQUENCY, PWM_RESOLUTION);
|
|
||||||
ledcSetup(3, PWM_FREQUENCY, PWM_RESOLUTION);
|
|
||||||
|
|
||||||
// attach channels to motor pins
|
|
||||||
ledcAttachPin(MOTOR_0_PIN, 0);
|
|
||||||
ledcAttachPin(MOTOR_1_PIN, 1);
|
|
||||||
ledcAttachPin(MOTOR_2_PIN, 2);
|
|
||||||
ledcAttachPin(MOTOR_3_PIN, 3);
|
|
||||||
|
|
||||||
sendMotors();
|
|
||||||
Serial.println("Motors initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t getPWM(float val, int n)
|
|
||||||
{
|
|
||||||
if (val == 0) {
|
|
||||||
return PWM_NEUTRAL;
|
|
||||||
} else if (val > 0) {
|
|
||||||
return mapff(val, 0, 1, pwmMin[n], pwmMax[n]);
|
|
||||||
} else {
|
|
||||||
return mapff(val, 0, -1, pwmReverseMin[n], pwmReverseMax[n]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t pwmToDutyCycle(uint16_t pwm) {
|
|
||||||
return map(pwm, 0, 1000000 / PWM_FREQUENCY, 0, (1 << PWM_RESOLUTION) - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendMotors()
|
|
||||||
{
|
|
||||||
ledcWrite(0, pwmToDutyCycle(getPWM(motors[0], 0)));
|
|
||||||
ledcWrite(1, pwmToDutyCycle(getPWM(motors[1], 1)));
|
|
||||||
ledcWrite(2, pwmToDutyCycle(getPWM(motors[2], 2)));
|
|
||||||
ledcWrite(3, pwmToDutyCycle(getPWM(motors[3], 3)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void fullMotorTest(int n, bool reverse)
|
|
||||||
{
|
|
||||||
printf("Full test for motor %d\n", n);
|
|
||||||
for (int pwm = PWM_NEUTRAL; pwm <= 2300 && pwm >= 700; pwm += reverse ? -100 : 100) {
|
|
||||||
printf("Motor %d: %d\n", n, pwm);
|
|
||||||
ledcWrite(n, pwmToDutyCycle(pwm));
|
|
||||||
delay(3000);
|
|
||||||
}
|
|
||||||
printf("Motor %d: %d\n", n, PWM_NEUTRAL);
|
|
||||||
ledcWrite(n, pwmToDutyCycle(PWM_NEUTRAL));
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// PID controller implementation
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "lpf.h"
|
|
||||||
|
|
||||||
class PID
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
float p = 0;
|
|
||||||
float i = 0;
|
|
||||||
float d = 0;
|
|
||||||
float windup = 0;
|
|
||||||
|
|
||||||
float derivative = 0;
|
|
||||||
float integral = 0;
|
|
||||||
|
|
||||||
LowPassFilter<float> lpf; // low pass filter for derivative term
|
|
||||||
|
|
||||||
PID(float p, float i, float d, float windup = 0, float dAlpha = 1) : p(p), i(i), d(d), windup(windup), lpf(dAlpha) {};
|
|
||||||
|
|
||||||
float update(float error, float dt)
|
|
||||||
{
|
|
||||||
integral += error * dt;
|
|
||||||
|
|
||||||
if (isfinite(prevError) && dt > 0) {
|
|
||||||
// calculate derivative if both dt and prevError are valid
|
|
||||||
derivative = (error - prevError) / dt;
|
|
||||||
|
|
||||||
// apply low pass filter to derivative
|
|
||||||
derivative = lpf.update(derivative);
|
|
||||||
}
|
|
||||||
|
|
||||||
prevError = error;
|
|
||||||
|
|
||||||
return p * error + constrain(i * integral, -windup, windup) + d * derivative; // PID
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset()
|
|
||||||
{
|
|
||||||
prevError = NAN;
|
|
||||||
integral = 0;
|
|
||||||
derivative = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
float prevError = NAN;
|
|
||||||
};
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Lightweight rotation quaternion library
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "vector.h"
|
|
||||||
|
|
||||||
class Quaternion : public Printable {
|
|
||||||
public:
|
|
||||||
float w, x, y, z;
|
|
||||||
|
|
||||||
Quaternion(): w(1), x(0), y(0), z(0) {};
|
|
||||||
|
|
||||||
Quaternion(float w, float x, float y, float z): w(w), x(x), y(y), z(z) {};
|
|
||||||
|
|
||||||
static Quaternion fromAxisAngle(float a, float b, float c, float angle)
|
|
||||||
{
|
|
||||||
float halfAngle = angle * 0.5;
|
|
||||||
float sin2 = sin(halfAngle);
|
|
||||||
float cos2 = cos(halfAngle);
|
|
||||||
float sinNorm = sin2 / sqrt(a * a + b * b + c * c);
|
|
||||||
return Quaternion(cos2, a * sinNorm, b * sinNorm, c * sinNorm);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Quaternion fromAngularRates(float x, float y, float z)
|
|
||||||
{
|
|
||||||
return Quaternion::fromAxisAngle(x, y, z, sqrt(x * x + y * y + z * z));
|
|
||||||
}
|
|
||||||
|
|
||||||
static Quaternion fromAngularRates(const Vector& rates)
|
|
||||||
{
|
|
||||||
if (rates.zero()) {
|
|
||||||
return Quaternion();
|
|
||||||
}
|
|
||||||
return Quaternion::fromAxisAngle(rates.x, rates.y, rates.z, rates.norm());
|
|
||||||
}
|
|
||||||
|
|
||||||
static Quaternion fromEulerZYX(float x, float y, float z)
|
|
||||||
{
|
|
||||||
float cx = cos(x / 2);
|
|
||||||
float cy = cos(y / 2);
|
|
||||||
float cz = cos(z / 2);
|
|
||||||
float sx = sin(x / 2);
|
|
||||||
float sy = sin(y / 2);
|
|
||||||
float sz = sin(z / 2);
|
|
||||||
|
|
||||||
return Quaternion(
|
|
||||||
cx * cy * cz + sx * sy * sz,
|
|
||||||
sx * cy * cz - cx * sy * sz,
|
|
||||||
cx * sy * cz + sx * cy * sz,
|
|
||||||
cx * cy * sz - sx * sy * cz);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Quaternion fromBetweenVectors(Vector u, Vector v)
|
|
||||||
{
|
|
||||||
float dot = u.x * v.x + u.y * v.y + u.z * v.z;
|
|
||||||
float w1 = u.y * v.z - u.z * v.y;
|
|
||||||
float w2 = u.z * v.x - u.x * v.z;
|
|
||||||
float w3 = u.x * v.y - u.y * v.x;
|
|
||||||
|
|
||||||
Quaternion ret(
|
|
||||||
dot + sqrt(dot * dot + w1 * w1 + w2 * w2 + w3 * w3),
|
|
||||||
w1,
|
|
||||||
w2,
|
|
||||||
w3);
|
|
||||||
ret.normalize();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
void toAxisAngle(float& a, float& b, float& c, float& angle)
|
|
||||||
{
|
|
||||||
angle = acos(w) * 2;
|
|
||||||
a = x / sin(angle / 2);
|
|
||||||
b = y / sin(angle / 2);
|
|
||||||
c = z / sin(angle / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector toEulerZYX() const
|
|
||||||
{
|
|
||||||
return Vector(
|
|
||||||
atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y)),
|
|
||||||
asin(2 * (w * y - z * x)),
|
|
||||||
atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)));
|
|
||||||
}
|
|
||||||
|
|
||||||
float getYaw() const
|
|
||||||
{
|
|
||||||
// https://github.com/ros/geometry2/blob/589caf083cae9d8fae7effdb910454b4681b9ec1/tf2/include/tf2/impl/utils.h#L122
|
|
||||||
float yaw;
|
|
||||||
float sqx = x * x;
|
|
||||||
float sqy = y * y;
|
|
||||||
float sqz = z * z;
|
|
||||||
float sqw = w * w;
|
|
||||||
|
|
||||||
double sarg = -2 * (x * z - w * y) / (sqx + sqy + sqz + sqw);
|
|
||||||
|
|
||||||
if (sarg <= -0.99999) {
|
|
||||||
yaw = -2 * atan2(y, x);
|
|
||||||
} else if (sarg >= 0.99999) {
|
|
||||||
yaw = 2 * atan2(y, x);
|
|
||||||
} else {
|
|
||||||
yaw = atan2(2 * (x * y + w * z), sqw + sqx - sqy - sqz);
|
|
||||||
}
|
|
||||||
return yaw;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setYaw(float yaw)
|
|
||||||
{
|
|
||||||
// TODO: optimize?
|
|
||||||
Vector euler = toEulerZYX();
|
|
||||||
(*this) = Quaternion::fromEulerZYX(euler.x, euler.y, yaw);
|
|
||||||
}
|
|
||||||
|
|
||||||
Quaternion& operator *= (const Quaternion& q)
|
|
||||||
{
|
|
||||||
Quaternion ret(
|
|
||||||
w * q.w - x * q.x - y * q.y - z * q.z,
|
|
||||||
w * q.x + x * q.w + y * q.z - z * q.y,
|
|
||||||
w * q.y + y * q.w + z * q.x - x * q.z,
|
|
||||||
w * q.z + z * q.w + x * q.y - y * q.x);
|
|
||||||
return (*this = ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
Quaternion operator * (const Quaternion& q)
|
|
||||||
{
|
|
||||||
return Quaternion(
|
|
||||||
w * q.w - x * q.x - y * q.y - z * q.z,
|
|
||||||
w * q.x + x * q.w + y * q.z - z * q.y,
|
|
||||||
w * q.y + y * q.w + z * q.x - x * q.z,
|
|
||||||
w * q.z + z * q.w + x * q.y - y * q.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
Quaternion inversed() const
|
|
||||||
{
|
|
||||||
float normSqInv = 1 / (w * w + x * x + y * y + z * z);
|
|
||||||
return Quaternion(
|
|
||||||
w * normSqInv,
|
|
||||||
-x * normSqInv,
|
|
||||||
-y * normSqInv,
|
|
||||||
-z * normSqInv);
|
|
||||||
}
|
|
||||||
|
|
||||||
float norm() const
|
|
||||||
{
|
|
||||||
return sqrt(w * w + x * x + y * y + z * z);
|
|
||||||
}
|
|
||||||
|
|
||||||
void normalize()
|
|
||||||
{
|
|
||||||
float n = norm();
|
|
||||||
w /= n;
|
|
||||||
x /= n;
|
|
||||||
y /= n;
|
|
||||||
z /= n;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector conjugate(const Vector& v)
|
|
||||||
{
|
|
||||||
Quaternion qv(0, v.x, v.y, v.z);
|
|
||||||
Quaternion res = (*this) * qv * inversed();
|
|
||||||
return Vector(res.x, res.y, res.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector conjugateInversed(const Vector& v)
|
|
||||||
{
|
|
||||||
Quaternion qv(0, v.x, v.y, v.z);
|
|
||||||
Quaternion res = inversed() * qv * (*this);
|
|
||||||
return Vector(res.x, res.y, res.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Vector rotate(const Vector& v)
|
|
||||||
{
|
|
||||||
return conjugateInversed(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool finite() const
|
|
||||||
{
|
|
||||||
return isfinite(w) && isfinite(x) && isfinite(y) && isfinite(z);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t printTo(Print& p) const {
|
|
||||||
size_t r = 0;
|
|
||||||
r += p.print(w, 15) + p.print(" ");
|
|
||||||
r += p.print(x, 15) + p.print(" ");
|
|
||||||
r += p.print(y, 15) + p.print(" ");
|
|
||||||
r += p.print(z, 15);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Work with the RC receiver
|
|
||||||
|
|
||||||
#include <SBUS.h>
|
|
||||||
|
|
||||||
const uint16_t channelNeutral[] = {995, 883, 200, 972, 512, 512};
|
|
||||||
const uint16_t channelMax[] = {1651, 1540, 1713, 1630, 1472, 1472};
|
|
||||||
|
|
||||||
SBUS RC(Serial2);
|
|
||||||
|
|
||||||
void setupRC()
|
|
||||||
{
|
|
||||||
Serial.println("Setup RC");
|
|
||||||
RC.begin();
|
|
||||||
}
|
|
||||||
|
|
||||||
void readRC()
|
|
||||||
{
|
|
||||||
bool failSafe, lostFrame;
|
|
||||||
if (RC.read(channels, &failSafe, &lostFrame)) {
|
|
||||||
if (failSafe) { return; } // TODO:
|
|
||||||
if (lostFrame) { return; }
|
|
||||||
normalizeRC();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void normalizeRC() {
|
|
||||||
for (uint8_t i = 0; i < RC_CHANNELS; i++) {
|
|
||||||
controls[i] = mapf(channels[i], channelNeutral[i], channelMax[i], 0, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Time related functions
|
|
||||||
|
|
||||||
void step()
|
|
||||||
{
|
|
||||||
float now = micros() / 1000000.0;
|
|
||||||
dt = now - t;
|
|
||||||
t = now;
|
|
||||||
|
|
||||||
if (!(dt > 0)) {
|
|
||||||
dt = 0; // assume dt to be zero on first step and on reset
|
|
||||||
}
|
|
||||||
|
|
||||||
computeLoopFreq();
|
|
||||||
}
|
|
||||||
|
|
||||||
void computeLoopFreq()
|
|
||||||
{
|
|
||||||
static float windowStart = 0;
|
|
||||||
static uint32_t freq = 0;
|
|
||||||
freq++;
|
|
||||||
if (t - windowStart >= 1) { // 1 second window
|
|
||||||
loopFreq = freq;
|
|
||||||
windowStart = t;
|
|
||||||
freq = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Utility functions
|
|
||||||
|
|
||||||
#include "math.h"
|
|
||||||
|
|
||||||
float mapf(long x, long in_min, long in_max, float out_min, float out_max)
|
|
||||||
{
|
|
||||||
return (float)(x - in_min) * (out_max - out_min) / (float)(in_max - in_min) + out_min;
|
|
||||||
}
|
|
||||||
|
|
||||||
float mapff(float x, float in_min, float in_max, float out_min, float out_max)
|
|
||||||
{
|
|
||||||
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
|
||||||
}
|
|
||||||
|
|
||||||
int8_t sign(float x)
|
|
||||||
{
|
|
||||||
return (x > 0) - (x < 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
float randomFloat(float min, float max)
|
|
||||||
{
|
|
||||||
return min + (max - min) * (float)rand() / RAND_MAX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrap angle to [-PI, PI)
|
|
||||||
float wrapAngle(float angle)
|
|
||||||
{
|
|
||||||
angle = fmodf(angle, 2 * PI);
|
|
||||||
if (angle > PI) {
|
|
||||||
angle -= 2 * PI;
|
|
||||||
} else if (angle < -PI) {
|
|
||||||
angle += 2 * PI;
|
|
||||||
}
|
|
||||||
return angle;
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Lightweight vector library
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
class Vector : public Printable
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
float x, y, z;
|
|
||||||
|
|
||||||
Vector(): x(0), y(0), z(0) {};
|
|
||||||
|
|
||||||
Vector(float x, float y, float z): x(x), y(y), z(z) {};
|
|
||||||
|
|
||||||
float norm() const
|
|
||||||
{
|
|
||||||
return sqrt(x * x + y * y + z * z);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool zero() const
|
|
||||||
{
|
|
||||||
return x == 0 && y == 0 && z == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void normalize()
|
|
||||||
{
|
|
||||||
float n = norm();
|
|
||||||
x /= n;
|
|
||||||
y /= n;
|
|
||||||
z /= n;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector operator * (const float b) const
|
|
||||||
{
|
|
||||||
return Vector(x * b, y * b, z * b);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector operator / (const float b) const
|
|
||||||
{
|
|
||||||
return Vector(x / b, y / b, z / b);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector operator + (const Vector& b) const
|
|
||||||
{
|
|
||||||
return Vector(x + b.x, y + b.y, z + b.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector operator - (const Vector& b) const
|
|
||||||
{
|
|
||||||
return Vector(x - b.x, y - b.y, z - b.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool operator == (const Vector& b) const
|
|
||||||
{
|
|
||||||
return x == b.x && y == b.y && z == b.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool operator != (const Vector& b) const
|
|
||||||
{
|
|
||||||
return !(*this == b);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool finite() const
|
|
||||||
{
|
|
||||||
return isfinite(x) && isfinite(y) && isfinite(z);
|
|
||||||
}
|
|
||||||
|
|
||||||
static float dot(const Vector& a, const Vector& b)
|
|
||||||
{
|
|
||||||
return a.x * b.x + a.y * b.y + a.z * b.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Vector cross(const Vector& a, const Vector& b)
|
|
||||||
{
|
|
||||||
return Vector(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
static float angleBetweenVectors(const Vector& a, const Vector& b)
|
|
||||||
{
|
|
||||||
return acos(constrain(dot(a, b) / (a.norm() * b.norm()), -1, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
static Vector angularRatesBetweenVectors(const Vector& u, const Vector& v)
|
|
||||||
{
|
|
||||||
Vector direction = cross(u, v);
|
|
||||||
direction.normalize();
|
|
||||||
float angle = angleBetweenVectors(u, v);
|
|
||||||
return direction * angle;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t printTo(Print& p) const {
|
|
||||||
return
|
|
||||||
p.print(x, 15) + p.print(" ") +
|
|
||||||
p.print(y, 15) + p.print(" ") +
|
|
||||||
p.print(z, 15);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Wi-Fi support
|
|
||||||
|
|
||||||
#if WIFI_ENABLED == 1
|
|
||||||
|
|
||||||
#include <WiFi.h>
|
|
||||||
#include <WiFiClient.h>
|
|
||||||
#include <WiFiAP.h>
|
|
||||||
#include "SBUS.h"
|
|
||||||
#include "mavlink/common/mavlink.h"
|
|
||||||
|
|
||||||
#define WIFI_SSID "flix"
|
|
||||||
#define WIFI_PASSWORD "flixwifi"
|
|
||||||
#define WIFI_UDP_IP "255.255.255.255"
|
|
||||||
#define WIFI_UDP_PORT 14550
|
|
||||||
|
|
||||||
WiFiUDP udp;
|
|
||||||
|
|
||||||
void setupWiFi()
|
|
||||||
{
|
|
||||||
Serial.println("Setup Wi-Fi");
|
|
||||||
WiFi.softAP(WIFI_SSID, WIFI_PASSWORD);
|
|
||||||
IPAddress myIP = WiFi.softAPIP();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void sendWiFi(const uint8_t *buf, size_t len)
|
|
||||||
{
|
|
||||||
udp.beginPacket(WIFI_UDP_IP, WIFI_UDP_PORT);
|
|
||||||
udp.write(buf, len);
|
|
||||||
udp.endPacket();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Partial implementation of Arduino API for simulation
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
#include <string>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <sys/poll.h>
|
|
||||||
|
|
||||||
#define PI 3.1415926535897932384626433832795
|
|
||||||
#define DEG_TO_RAD 0.017453292519943295769236907684886
|
|
||||||
#define RAD_TO_DEG 57.295779513082320876798154814105
|
|
||||||
|
|
||||||
#define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
|
|
||||||
|
|
||||||
long map(long x, long in_min, long in_max, long out_min, long out_max) {
|
|
||||||
const long run = in_max - in_min;
|
|
||||||
const long rise = out_max - out_min;
|
|
||||||
const long delta = x - in_min;
|
|
||||||
return (delta * rise) / run + out_min;
|
|
||||||
}
|
|
||||||
|
|
||||||
class __FlashStringHelper;
|
|
||||||
|
|
||||||
// Arduino String partial implementation
|
|
||||||
// https://www.arduino.cc/reference/en/language/variables/data-types/stringobject/
|
|
||||||
class String: public std::string {
|
|
||||||
public:
|
|
||||||
long toInt() const { return atol(this->c_str()); }
|
|
||||||
float toFloat() const { return atof(this->c_str()); }
|
|
||||||
bool isEmpty() const { return this->empty(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
class Print;
|
|
||||||
|
|
||||||
class Printable {
|
|
||||||
public:
|
|
||||||
virtual size_t printTo(Print& p) const = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Print {
|
|
||||||
public:
|
|
||||||
size_t printf(const char *format, ...)
|
|
||||||
{
|
|
||||||
va_list args;
|
|
||||||
va_start(args, format);
|
|
||||||
size_t result = vprintf(format, args);
|
|
||||||
va_end(args);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t print(float n, int digits = 2)
|
|
||||||
{
|
|
||||||
return printf("%.*f", digits, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t println(float n, int digits = 2)
|
|
||||||
{
|
|
||||||
return printf("%.*f\n", digits, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t print(const char* s)
|
|
||||||
{
|
|
||||||
return printf("%s", s);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t println()
|
|
||||||
{
|
|
||||||
return print("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t println(const char* s)
|
|
||||||
{
|
|
||||||
return printf("%s\n", s);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t println(const Printable& p)
|
|
||||||
{
|
|
||||||
return p.printTo(*this) + print("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t print(const String& s)
|
|
||||||
{
|
|
||||||
return printf("%s", s.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t println(const std::string& s)
|
|
||||||
{
|
|
||||||
return printf("%s\n", s.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t println(const String& s)
|
|
||||||
{
|
|
||||||
return printf("%s\n", s.c_str());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class HardwareSerial: public Print {
|
|
||||||
public:
|
|
||||||
void begin(unsigned long baud) {
|
|
||||||
// server is running in background by default, so doesn't have access to stdin
|
|
||||||
// https://github.com/gazebosim/gazebo-classic/blob/d45feeb51f773e63960616880b0544770b8d1ad7/gazebo/gazebo_main.cc#L216
|
|
||||||
// set foreground process group to current process group to allow reading from stdin
|
|
||||||
// https://stackoverflow.com/questions/58918188/why-is-stdin-not-propagated-to-child-process-of-different-process-group
|
|
||||||
signal(SIGTTOU, SIG_IGN);
|
|
||||||
tcsetpgrp(STDIN_FILENO, getpgrp());
|
|
||||||
signal(SIGTTOU, SIG_DFL);
|
|
||||||
};
|
|
||||||
|
|
||||||
int available() {
|
|
||||||
// to implement for Windows, see https://stackoverflow.com/a/71992965/6850197
|
|
||||||
struct pollfd pfd = { .fd = STDIN_FILENO, .events = POLLIN };
|
|
||||||
return poll(&pfd, 1, 0) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int read() {
|
|
||||||
if (available()) {
|
|
||||||
char c;
|
|
||||||
::read(STDIN_FILENO, &c, 1); // use raw read to avoid C++ buffering
|
|
||||||
// https://stackoverflow.com/questions/45238997/does-getchar-function-has-its-own-buffer-to-store-remaining-input
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
HardwareSerial Serial, Serial2;
|
|
||||||
|
|
||||||
void delay(uint32_t ms) {
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long __micros;
|
|
||||||
|
|
||||||
unsigned long micros() {
|
|
||||||
return __micros;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
|
|
||||||
project(flix_gazebo)
|
|
||||||
|
|
||||||
# === gazebo plugin
|
|
||||||
find_package(gazebo REQUIRED)
|
|
||||||
find_package(SDL2 REQUIRED)
|
|
||||||
include_directories(${GAZEBO_INCLUDE_DIRS})
|
|
||||||
link_directories(${GAZEBO_LIBRARY_DIRS})
|
|
||||||
list(APPEND CMAKE_CXX_FLAGS "${GAZEBO_CXX_FLAGS}")
|
|
||||||
|
|
||||||
set(FLIX_SOURCE_DIR ../flix)
|
|
||||||
include_directories(${FLIX_SOURCE_DIR})
|
|
||||||
file(GLOB_RECURSE FLIX_INO_FILES ${FLIX_SOURCE_DIR}/*.ino)
|
|
||||||
|
|
||||||
set(CMAKE_BUILD_TYPE RelWithDebInfo)
|
|
||||||
add_library(flix SHARED flix.cpp)
|
|
||||||
target_link_libraries(flix ${GAZEBO_LIBRARIES} ${SDL2_LIBRARIES})
|
|
||||||
target_include_directories(flix PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Gazebo plugin for running Arduino code and simulating the drone
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <cmath>
|
|
||||||
#include <gazebo/gazebo.hh>
|
|
||||||
#include <gazebo/physics/physics.hh>
|
|
||||||
#include <gazebo/rendering/rendering.hh>
|
|
||||||
#include <gazebo/common/common.hh>
|
|
||||||
#include <gazebo/sensors/sensors.hh>
|
|
||||||
#include <gazebo/msgs/msgs.hh>
|
|
||||||
#include <ignition/math/Vector3.hh>
|
|
||||||
#include <ignition/math/Pose3.hh>
|
|
||||||
#include <ignition/math/Quaternion.hh>
|
|
||||||
#include <iostream>
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
#include "Arduino.h"
|
|
||||||
#include "flix.h"
|
|
||||||
#include "util.ino"
|
|
||||||
#include "joystick.h"
|
|
||||||
#include "time.ino"
|
|
||||||
#include "estimate.ino"
|
|
||||||
#include "control.ino"
|
|
||||||
#include "log.ino"
|
|
||||||
#include "cli.ino"
|
|
||||||
#include "lpf.h"
|
|
||||||
|
|
||||||
using ignition::math::Vector3d;
|
|
||||||
using ignition::math::Pose3d;
|
|
||||||
using namespace gazebo;
|
|
||||||
using namespace std;
|
|
||||||
|
|
||||||
Pose3d flu2frd(const Pose3d& p)
|
|
||||||
{
|
|
||||||
return ignition::math::Pose3d(p.Pos().X(), -p.Pos().Y(), -p.Pos().Z(),
|
|
||||||
p.Rot().W(), p.Rot().X(), -p.Rot().Y(), -p.Rot().Z());
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector flu2frd(const Vector3d& v)
|
|
||||||
{
|
|
||||||
return Vector(v.X(), -v.Y(), -v.Z());
|
|
||||||
}
|
|
||||||
|
|
||||||
class ModelFlix : public ModelPlugin
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
physics::ModelPtr model, estimateModel;
|
|
||||||
physics::LinkPtr body;
|
|
||||||
sensors::ImuSensorPtr imu;
|
|
||||||
event::ConnectionPtr updateConnection, resetConnection;
|
|
||||||
transport::NodePtr nodeHandle;
|
|
||||||
transport::PublisherPtr motorPub[4];
|
|
||||||
LowPassFilter<Vector> accFilter = LowPassFilter<Vector>(0.1);
|
|
||||||
|
|
||||||
public:
|
|
||||||
void Load(physics::ModelPtr _parent, sdf::ElementPtr /*_sdf*/)
|
|
||||||
{
|
|
||||||
this->model = _parent;
|
|
||||||
this->body = this->model->GetLink("body");
|
|
||||||
|
|
||||||
this->imu = std::dynamic_pointer_cast<sensors::ImuSensor>(sensors::get_sensor(model->GetScopedName(true) + "::body::imu")); // default::flix::body::imu
|
|
||||||
if (imu == nullptr) {
|
|
||||||
gzerr << "IMU sensor not found" << std::endl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->estimateModel = model->GetWorld()->ModelByName("flix_estimate");
|
|
||||||
|
|
||||||
this->updateConnection = event::Events::ConnectWorldUpdateBegin(
|
|
||||||
std::bind(&ModelFlix::OnUpdate, this));
|
|
||||||
|
|
||||||
this->resetConnection = event::Events::ConnectWorldReset(
|
|
||||||
std::bind(&ModelFlix::OnReset, this));
|
|
||||||
|
|
||||||
initNode();
|
|
||||||
|
|
||||||
Serial.begin(0);
|
|
||||||
|
|
||||||
gzmsg << "Flix plugin loaded" << endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
void OnReset()
|
|
||||||
{
|
|
||||||
attitude = Quaternion();
|
|
||||||
gzmsg << "Flix plugin reset" << endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnUpdate()
|
|
||||||
{
|
|
||||||
__micros = model->GetWorld()->SimTime().Double() * 1000000;
|
|
||||||
step();
|
|
||||||
|
|
||||||
// read imu
|
|
||||||
rates = flu2frd(imu->AngularVelocity());
|
|
||||||
acc = this->accFilter.update(flu2frd(imu->LinearAcceleration()));
|
|
||||||
|
|
||||||
// read rc
|
|
||||||
joystickGet();
|
|
||||||
controls[RC_CHANNEL_MODE] = 1; // 0 acro, 1 stab
|
|
||||||
controls[RC_CHANNEL_AUX] = 1; // armed
|
|
||||||
|
|
||||||
estimate();
|
|
||||||
|
|
||||||
// correct yaw to the actual yaw
|
|
||||||
attitude.setYaw(-this->model->WorldPose().Yaw());
|
|
||||||
|
|
||||||
control();
|
|
||||||
parseInput();
|
|
||||||
|
|
||||||
applyMotorsThrust();
|
|
||||||
updateEstimatePose();
|
|
||||||
publishTopics();
|
|
||||||
logData();
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyMotorsThrust()
|
|
||||||
{
|
|
||||||
// thrusts
|
|
||||||
const double d = 0.035355;
|
|
||||||
const double maxThrust = 0.03 * ONE_G; // 30 g, https://www.youtube.com/watch?v=VtKI4Pjx8Sk
|
|
||||||
// 65 mm prop ~40 g
|
|
||||||
|
|
||||||
const float scale0 = 1.0, scale1 = 1.1, scale2 = 0.9, scale3 = 1.05;
|
|
||||||
const float minThrustRel = 0;
|
|
||||||
|
|
||||||
// apply min thrust
|
|
||||||
float mfl = mapff(motors[MOTOR_FRONT_LEFT], 0, 1, minThrustRel, 1);
|
|
||||||
float mfr = mapff(motors[MOTOR_FRONT_RIGHT], 0, 1, minThrustRel, 1);
|
|
||||||
float mrl = mapff(motors[MOTOR_REAR_LEFT], 0, 1, minThrustRel, 1);
|
|
||||||
float mrr = mapff(motors[MOTOR_REAR_RIGHT], 0, 1, minThrustRel, 1);
|
|
||||||
|
|
||||||
if (motors[MOTOR_FRONT_LEFT] < 0.001) mfl = 0;
|
|
||||||
if (motors[MOTOR_FRONT_RIGHT] < 0.001) mfr = 0;
|
|
||||||
if (motors[MOTOR_REAR_LEFT] < 0.001) mrl = 0;
|
|
||||||
if (motors[MOTOR_REAR_RIGHT] < 0.001) mrr = 0;
|
|
||||||
|
|
||||||
// TODO: min_thrust
|
|
||||||
|
|
||||||
body->AddLinkForce(Vector3d(0.0, 0.0, scale0 * maxThrust * abs(mfl)), Vector3d(d, d, 0.0));
|
|
||||||
body->AddLinkForce(Vector3d(0.0, 0.0, scale1 * maxThrust * abs(mfr)), Vector3d(d, -d, 0.0));
|
|
||||||
body->AddLinkForce(Vector3d(0.0, 0.0, scale2 * maxThrust * abs(mrl)), Vector3d(-d, d, 0.0));
|
|
||||||
body->AddLinkForce(Vector3d(0.0, 0.0, scale3 * maxThrust * abs(mrr)), Vector3d(-d, -d, 0.0));
|
|
||||||
|
|
||||||
// TODO: indicate if > 1
|
|
||||||
|
|
||||||
// torque
|
|
||||||
const double maxTorque = 0.0023614413; // 24.08 g*cm
|
|
||||||
int direction = 1;
|
|
||||||
// z is counter clockwise, normal rotation direction is minus
|
|
||||||
body->AddRelativeTorque(Vector3d(0.0, 0.0, direction * scale0 * maxTorque * motors[MOTOR_FRONT_LEFT]));
|
|
||||||
body->AddRelativeTorque(Vector3d(0.0, 0.0, direction * scale1 * -maxTorque * motors[MOTOR_FRONT_RIGHT]));
|
|
||||||
body->AddRelativeTorque(Vector3d(0.0, 0.0, direction * scale2 * -maxTorque * motors[MOTOR_REAR_LEFT]));
|
|
||||||
body->AddRelativeTorque(Vector3d(0.0, 0.0, direction * scale3 * maxTorque * motors[MOTOR_REAR_RIGHT]));
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateEstimatePose() {
|
|
||||||
if (estimateModel == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!attitude.finite()) {
|
|
||||||
// gzerr << "attitude is nan" << std::endl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Pose3d pose(
|
|
||||||
model->WorldPose().Pos().X(), model->WorldPose().Pos().Y(), model->WorldPose().Pos().Z(),
|
|
||||||
attitude.w, attitude.x, -attitude.y, -attitude.z // frd to flu
|
|
||||||
);
|
|
||||||
// std::cout << pose.Pos().X() << " " << pose.Pos().Y() << " " << pose.Pos().Z() <<
|
|
||||||
// " " << pose.Rot().W() << " " << pose.Rot().X() << " " << pose.Rot().Y() << " " << pose.Rot().Z() << std::endl;
|
|
||||||
|
|
||||||
// calculate attitude estimation error
|
|
||||||
|
|
||||||
Quaternion groundtruthAttitude(estimateModel->WorldPose().Rot().W(), estimateModel->WorldPose().Rot().X(), -estimateModel->WorldPose().Rot().Y(), -estimateModel->WorldPose().Rot().Z());
|
|
||||||
float angle = Vector::angleBetweenVectors(attitude.rotate(Vector(0, 0, -1)), groundtruthAttitude.rotate(Vector(0, 0, -1)));
|
|
||||||
if (angle < 0.3) {
|
|
||||||
//gzwarn << "att err: " << angle << endl;
|
|
||||||
// TODO: warning
|
|
||||||
// position under the floor to make it invisible
|
|
||||||
pose.SetZ(-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
estimateModel->SetWorldPose(pose);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void initNode() {
|
|
||||||
nodeHandle = transport::NodePtr(new transport::Node());
|
|
||||||
nodeHandle->Init();
|
|
||||||
string ns = "~/" + model->GetName();
|
|
||||||
motorPub[0] = nodeHandle->Advertise<msgs::Int>(ns + "/motor0");
|
|
||||||
motorPub[1] = nodeHandle->Advertise<msgs::Int>(ns + "/motor1");
|
|
||||||
motorPub[2] = nodeHandle->Advertise<msgs::Int>(ns + "/motor2");
|
|
||||||
motorPub[3] = nodeHandle->Advertise<msgs::Int>(ns + "/motor3");
|
|
||||||
}
|
|
||||||
|
|
||||||
void publishTopics() {
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
msgs::Int msg;
|
|
||||||
msg.set_data(static_cast<int>(std::round(motors[i] * 1000)));
|
|
||||||
motorPub[i]->Publish(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
GZ_REGISTER_MODEL_PLUGIN(ModelFlix)
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Declarations of some functions and variables in Arduino code
|
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include "vector.h"
|
|
||||||
#include "quaternion.h"
|
|
||||||
#include "Arduino.h"
|
|
||||||
|
|
||||||
#define RC_CHANNELS 6
|
|
||||||
|
|
||||||
#define MOTOR_REAR_LEFT 0
|
|
||||||
#define MOTOR_FRONT_LEFT 3
|
|
||||||
#define MOTOR_FRONT_RIGHT 2
|
|
||||||
#define MOTOR_REAR_RIGHT 1
|
|
||||||
|
|
||||||
float t = NAN;
|
|
||||||
float dt;
|
|
||||||
float loopFreq;
|
|
||||||
float motors[4];
|
|
||||||
int16_t channels[16]; // raw rc channels WARNING: unsigned on hardware
|
|
||||||
float controls[RC_CHANNELS];
|
|
||||||
Vector acc;
|
|
||||||
Vector rates;
|
|
||||||
Quaternion attitude;
|
|
||||||
|
|
||||||
// declarations
|
|
||||||
void computeLoopFreq();
|
|
||||||
void applyGyro();
|
|
||||||
void applyAcc();
|
|
||||||
void signalizeHorizontality();
|
|
||||||
void control();
|
|
||||||
void interpretRC();
|
|
||||||
void controlAttitude();
|
|
||||||
void controlRate();
|
|
||||||
void controlTorque();
|
|
||||||
void showTable();
|
|
||||||
bool motorsActive();
|
|
||||||
void cliTestMotor(uint8_t n);
|
|
||||||
|
|
||||||
// mocks
|
|
||||||
void setLED(bool on) {};
|
|
||||||
void calibrateGyro() { printf("Skip gyro calibrating\n"); };
|
|
||||||
void calibrateAccel() { printf("Skip accel calibrating\n"); };
|
|
||||||
void fullMotorTest(int n, bool reverse) { printf("Skip full motor test\n"); };
|
|
||||||
void sendMotors() {};
|
|
||||||
void printIMUCal() { printf("cal: N/A\n"); };
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<sdf version="1.4">
|
|
||||||
<world name="default">
|
|
||||||
<scene>
|
|
||||||
<ambient>1 1 1 1</ambient>
|
|
||||||
<sky/>
|
|
||||||
<origin_visual>false</origin_visual>
|
|
||||||
<grid>false</grid>
|
|
||||||
</scene>
|
|
||||||
<gui>
|
|
||||||
<camera name="user_camera">
|
|
||||||
<pose>-2 -0.3 1.5 0 0.5 0.1</pose>
|
|
||||||
</camera>
|
|
||||||
</gui>
|
|
||||||
<physics type="ode">
|
|
||||||
<max_step_size>0.001</max_step_size>
|
|
||||||
</physics>
|
|
||||||
<include>
|
|
||||||
<uri>model://floor</uri>
|
|
||||||
</include>
|
|
||||||
<include>
|
|
||||||
<uri>model://sun</uri>
|
|
||||||
</include>
|
|
||||||
<include>
|
|
||||||
<uri>model://flix</uri>
|
|
||||||
<pose>0 0 0.2 0 0 0</pose>
|
|
||||||
</include>
|
|
||||||
<model name="flix_estimate">
|
|
||||||
<static>true</static>
|
|
||||||
<link name="estimate">
|
|
||||||
<visual name="estimate">
|
|
||||||
<pose>0 0 0 0 0 1.57</pose>
|
|
||||||
<geometry>
|
|
||||||
<box>
|
|
||||||
<size>0.125711 0.125711 0.022</size>
|
|
||||||
</box>
|
|
||||||
</geometry>
|
|
||||||
</visual>
|
|
||||||
</link>
|
|
||||||
</model>
|
|
||||||
</world>
|
|
||||||
</sdf>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
|
|
||||||
// Repository: https://github.com/okalachev/flix
|
|
||||||
|
|
||||||
// Joystick support for simulation
|
|
||||||
|
|
||||||
#include <SDL2/SDL.h>
|
|
||||||
#include <gazebo/gazebo.hh>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
using namespace std;
|
|
||||||
|
|
||||||
static const int16_t channelNeutralMin[] = {-1290, -258, -26833, 0, 0, 0};
|
|
||||||
static const int16_t channelNeutralMax[] = {-1032, -258, -27348, 3353, 0, 0};
|
|
||||||
|
|
||||||
static const int16_t channelMax[] = {27090, 27090, 27090, 27090, 0, 0};
|
|
||||||
|
|
||||||
#define RC_CHANNEL_ROLL 0
|
|
||||||
#define RC_CHANNEL_PITCH 1
|
|
||||||
#define RC_CHANNEL_THROTTLE 2
|
|
||||||
#define RC_CHANNEL_YAW 3
|
|
||||||
#define RC_CHANNEL_AUX 4
|
|
||||||
#define RC_CHANNEL_MODE 5
|
|
||||||
|
|
||||||
static SDL_Joystick *joystick;
|
|
||||||
|
|
||||||
bool joystickInitialized = false, warnShown = false;
|
|
||||||
|
|
||||||
void normalizeRC();
|
|
||||||
|
|
||||||
void joystickInit()
|
|
||||||
{
|
|
||||||
SDL_Init(SDL_INIT_JOYSTICK);
|
|
||||||
joystick = SDL_JoystickOpen(0);
|
|
||||||
if (joystick != NULL) {
|
|
||||||
joystickInitialized = true;
|
|
||||||
gzmsg << "Joystick initialized: " << SDL_JoystickNameForIndex(0) << endl;
|
|
||||||
} else if (!warnShown) {
|
|
||||||
gzwarn << "Joystick not found, begin waiting for joystick..." << endl;
|
|
||||||
warnShown = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void joystickGet()
|
|
||||||
{
|
|
||||||
if (!joystickInitialized) {
|
|
||||||
joystickInit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_JoystickUpdate();
|
|
||||||
|
|
||||||
for (uint8_t i = 0; i < 4; i++) {
|
|
||||||
channels[i] = SDL_JoystickGetAxis(joystick, i);
|
|
||||||
}
|
|
||||||
channels[RC_CHANNEL_MODE] = SDL_JoystickGetButton(joystick, 0) ? 1 : 0;
|
|
||||||
controls[RC_CHANNEL_MODE] = channels[RC_CHANNEL_MODE];
|
|
||||||
|
|
||||||
normalizeRC();
|
|
||||||
}
|
|
||||||
|
|
||||||
void normalizeRC() {
|
|
||||||
for (uint8_t i = 0; i < 4; i++) {
|
|
||||||
if (channels[i] >= channelNeutralMin[i] && channels[i] <= channelNeutralMax[i]) {
|
|
||||||
controls[i] = 0;
|
|
||||||
} else {
|
|
||||||
controls[i] = mapf(channels[i], (channelNeutralMin[i] + channelNeutralMax[i]) / 2, channelMax[i], 0, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
controls[RC_CHANNEL_THROTTLE] = constrain(controls[RC_CHANNEL_THROTTLE], 0, 1);
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<sdf version="1.5">
|
|
||||||
<model name="flix">
|
|
||||||
<link name="body">
|
|
||||||
<inertial>
|
|
||||||
<mass>0.065</mass>
|
|
||||||
<inertia>
|
|
||||||
<ixx>3.55E-5</ixx>
|
|
||||||
<iyy>4.23E-5</iyy>
|
|
||||||
<izz>7.47E-5</izz>
|
|
||||||
</inertia>
|
|
||||||
</inertial>
|
|
||||||
<collision name="collision">
|
|
||||||
<geometry>
|
|
||||||
<box>
|
|
||||||
<size>0.125711 0.125711 0.022</size>
|
|
||||||
</box>
|
|
||||||
</geometry>
|
|
||||||
</collision>
|
|
||||||
<visual name="body">
|
|
||||||
<geometry>
|
|
||||||
<mesh><uri>model://flix/flix.dae</uri></mesh>
|
|
||||||
</geometry>
|
|
||||||
</visual>
|
|
||||||
<sensor name="imu" type="imu">
|
|
||||||
<always_on>1</always_on>
|
|
||||||
<visualize>1</visualize>
|
|
||||||
<update_rate>1000</update_rate>
|
|
||||||
<imu>
|
|
||||||
<angular_velocity>
|
|
||||||
<x>
|
|
||||||
<noise type="gaussian">
|
|
||||||
<stddev>0.00174533</stddev><!-- 0.1 degrees per second -->
|
|
||||||
</noise>
|
|
||||||
</x>
|
|
||||||
<y>
|
|
||||||
<noise type="gaussian">
|
|
||||||
<stddev>0.00174533</stddev>
|
|
||||||
</noise>
|
|
||||||
</y>
|
|
||||||
<z>
|
|
||||||
<noise type="gaussian">
|
|
||||||
<stddev>0.00174533</stddev>
|
|
||||||
</noise>
|
|
||||||
</z>
|
|
||||||
</angular_velocity>
|
|
||||||
<linear_acceleration>
|
|
||||||
<x>
|
|
||||||
<noise type="gaussian">
|
|
||||||
<stddev>0.0784</stddev><!-- 8 mg -->
|
|
||||||
</noise>
|
|
||||||
</x>
|
|
||||||
<y>
|
|
||||||
<noise type="gaussian">
|
|
||||||
<stddev>0.0784</stddev>
|
|
||||||
</noise>
|
|
||||||
</y>
|
|
||||||
<z>
|
|
||||||
<noise type="gaussian">
|
|
||||||
<stddev>0.0784</stddev>
|
|
||||||
</noise>
|
|
||||||
</z>
|
|
||||||
</linear_acceleration>
|
|
||||||
</imu>
|
|
||||||
</sensor>
|
|
||||||
</link>
|
|
||||||
<plugin name="flix" filename="libflix.so"/>
|
|
||||||
</model>
|
|
||||||
</sdf>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<model>
|
|
||||||
<name>flix</name>
|
|
||||||
<version>1.0</version>
|
|
||||||
<sdf version="1.5">flix.sdf</sdf>
|
|
||||||
<author>
|
|
||||||
<name>Oleg Kalachev</name>
|
|
||||||
<email>okalachev@gmail.com</email>
|
|
||||||
</author>
|
|
||||||
<license>Unknown</license>
|
|
||||||
<description>
|
|
||||||
Flix quadrotor
|
|
||||||
</description>
|
|
||||||
</model>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<sdf version="1.5">
|
|
||||||
<model name="floor">
|
|
||||||
<static>true</static>
|
|
||||||
<link name="link">
|
|
||||||
<pose>0 0 -0.02 0 0 0</pose>
|
|
||||||
<collision name="collision">
|
|
||||||
<geometry>
|
|
||||||
<box>
|
|
||||||
<size>200 200 .02</size>
|
|
||||||
</box>
|
|
||||||
</geometry>
|
|
||||||
</collision>
|
|
||||||
<visual name="visual">
|
|
||||||
<cast_shadows>false</cast_shadows>
|
|
||||||
<geometry>
|
|
||||||
<box>
|
|
||||||
<size>200 200 .02</size>
|
|
||||||
</box>
|
|
||||||
</geometry>
|
|
||||||
<material>
|
|
||||||
<script>
|
|
||||||
<uri>model://floor/materials/scripts</uri>
|
|
||||||
<uri>model://floor/materials/textures</uri>
|
|
||||||
<name>parquet</name>
|
|
||||||
</script>
|
|
||||||
</material>
|
|
||||||
</visual>
|
|
||||||
</link>
|
|
||||||
</model>
|
|
||||||
</sdf>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
material parquet
|
|
||||||
{
|
|
||||||
technique
|
|
||||||
{
|
|
||||||
pass
|
|
||||||
{
|
|
||||||
ambient 0.5 0.5 0.5 1.0
|
|
||||||
diffuse 0.5 0.5 0.5 1.0
|
|
||||||
specular 0.2 0.2 0.2 1.0 12.5
|
|
||||||
|
|
||||||
texture_unit
|
|
||||||
{
|
|
||||||
texture floor.jpg
|
|
||||||
filtering anistropic
|
|
||||||
max_anisotropy 16
|
|
||||||
scale 0.01 0.01
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 11 MiB |
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
|
|
||||||
<model>
|
|
||||||
<name>Floor</name>
|
|
||||||
<version>1.0</version>
|
|
||||||
<sdf version="1.5">floor.sdf</sdf>
|
|
||||||
|
|
||||||
<author>
|
|
||||||
<name>Oleg Kalachev</name>
|
|
||||||
<email>okalachev@gmail.com</email>
|
|
||||||
</author>
|
|
||||||
|
|
||||||
<license>Unknown</license>
|
|
||||||
|
|
||||||
<description>
|
|
||||||
Floor.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
</model>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<sdf version="1.5">
|
|
||||||
<model name="table1">
|
|
||||||
<static>true</static>
|
|
||||||
<link name="link">
|
|
||||||
<pose>0 0 0 0 0 0</pose>
|
|
||||||
<collision name="mesh">
|
|
||||||
<geometry>
|
|
||||||
<mesh>
|
|
||||||
<uri>model://linnmon/linnmon.dae</uri>
|
|
||||||
</mesh>
|
|
||||||
</geometry>
|
|
||||||
</collision>
|
|
||||||
<visual name="visual">
|
|
||||||
<geometry>
|
|
||||||
<mesh>
|
|
||||||
<uri>model://linnmon/linnmon.dae</uri>
|
|
||||||
</mesh>
|
|
||||||
</geometry>
|
|
||||||
</visual>
|
|
||||||
</link>
|
|
||||||
</model>
|
|
||||||
</sdf>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
|
|
||||||
<model>
|
|
||||||
<name>linnmon</name>
|
|
||||||
<version>1.0</version>
|
|
||||||
<sdf version="1.5">linnmon.sdf</sdf>
|
|
||||||
|
|
||||||
<author>
|
|
||||||
<name>Oleg Kalachev</name>
|
|
||||||
<email>okalachev@gmail.com</email>
|
|
||||||
</author>
|
|
||||||
|
|
||||||
<license>Unknown</license>
|
|
||||||
|
|
||||||
<description>
|
|
||||||
Linnmon table from https://3dlancer.net/en/producermodels/tables-coffee-tables/3dmodel-table-with-ikea-linnmon-36059
|
|
||||||
</description>
|
|
||||||
|
|
||||||
</model>
|
|
||||||
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 180 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="black"/></svg>
|
||||||
|
After Width: | Height: | Size: 961 B |
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 54 54" style="enable-background:new 0 0 54 54;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:none;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<rect x="-0.2" y="0.1" class="st0" width="53.8" height="53.4"/>
|
||||||
|
<path d="M49.7,16.5c1.3,3.1,2,6.3,2,9.7s-0.7,6.6-2,9.7c-1.3,3.1-3.1,5.7-5.3,8c-2.2,2.2-4.9,4-8,5.3c-3.1,1.3-6.3,2-9.7,2
|
||||||
|
c-3.4,0-6.6-0.7-9.7-2s-5.7-3.1-8-5.3c-2.2-2.2-4-4.9-5.3-8c-1.3-3.1-2-6.3-2-9.7s0.7-6.6,2-9.7c1.3-3.1,3.1-5.7,5.3-8
|
||||||
|
c2.2-2.2,4.9-4,8-5.3s6.3-2,9.7-2c3.4,0,6.6,0.7,9.7,2c3.1,1.3,5.7,3.1,8,5.3C46.6,10.8,48.3,13.5,49.7,16.5z M34.8,37.7l4.1-19.3
|
||||||
|
c0.2-0.8,0.1-1.4-0.3-1.8c-0.4-0.4-0.8-0.4-1.4-0.2l-24.1,9.3c-0.5,0.2-0.9,0.4-1.1,0.7c-0.2,0.3-0.2,0.5-0.1,0.7
|
||||||
|
c0.1,0.2,0.4,0.4,0.9,0.5l6.2,1.9l14.3-9c0.4-0.3,0.7-0.3,0.9-0.2c0.1,0.1,0.1,0.2-0.1,0.4L22.5,31.3L22,37.7
|
||||||
|
c0.4,0,0.8-0.2,1.3-0.6l3-2.9l6.2,4.6C33.8,39.5,34.5,39.1,34.8,37.7z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,73 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Flix — разработка квадрокоптера с нуля</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #d9d9d7;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 22px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
max-width: 800px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
img.logo {
|
||||||
|
margin-top: -100px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.github {
|
||||||
|
background-image: url(img/tg.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: left center;
|
||||||
|
background-size: auto 30px;
|
||||||
|
padding-left: 40px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: rgb(42, 45, 183);
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
display: inline-block;
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
.links li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.links li img {
|
||||||
|
height: 30px;
|
||||||
|
margin-right: 10px;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!-- Yandex.Metrika counter --> <script type="text/javascript" > (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; m[i].l=1*new Date(); for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }} k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)}) (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); ym(97589916, "init", { clickmap:true, trackLinks:true, accurateTrackBounce:true }); </script> <noscript><div><img src="https://mc.yandex.ru/watch/97589916" style="position:absolute; left:-9999px;" alt="" /></div></noscript> <!-- /Yandex.Metrika counter -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<article>
|
||||||
|
<img class="logo" src="img/flix.jpg" alt="Flix, версия 0 (прототип)" title="Flix, версия 0 (прототип)">
|
||||||
|
<ul class="links">
|
||||||
|
<li>
|
||||||
|
<img src="img/gh.svg">
|
||||||
|
GitHub: <a href="https://github.com/okalachev/flix">github.com/okalachev/flix</a>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<img src="img/tg.svg">
|
||||||
|
Telegram-канал: <a href="https://t.me/opensourcequadcopter">@opensourcequadcopter</a>.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""Show Fast Fourier Transform plot for log entry.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
fft.py <csv_log_file> <log_entry>
|
|
||||||
"""
|
|
||||||
|
|
||||||
import docopt
|
|
||||||
import csv
|
|
||||||
import numpy as np
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
args = docopt.docopt(__doc__)
|
|
||||||
log_file = args['<csv_log_file>']
|
|
||||||
log_entry = args['<log_entry>']
|
|
||||||
|
|
||||||
csv_reader = csv.reader(open(log_file, 'r'), delimiter=',')
|
|
||||||
header = next(csv_reader)
|
|
||||||
log_entry_index = header.index(log_entry)
|
|
||||||
|
|
||||||
data = [[float(value) for value in row] for row in csv_reader]
|
|
||||||
data = sorted(data, key=lambda row: row[0])
|
|
||||||
records = [row[log_entry_index] for row in data]
|
|
||||||
duration = data[-1][0] - data[0][0]
|
|
||||||
sample_rate = len(data) / duration
|
|
||||||
|
|
||||||
print('Duration: ', duration)
|
|
||||||
print('Mean sample rate: ', sample_rate)
|
|
||||||
print('Mean dt: ', 1 / sample_rate)
|
|
||||||
|
|
||||||
N = int(sample_rate * duration)
|
|
||||||
|
|
||||||
yf = np.fft.rfft(records)
|
|
||||||
xf = np.fft.rfftfreq(N, 1 / sample_rate)
|
|
||||||
|
|
||||||
# print out fft data
|
|
||||||
print('Frequency, Amplitude')
|
|
||||||
for i in range(len(xf)):
|
|
||||||
print(xf[i], ',', abs(yf[i]))
|
|
||||||
|
|
||||||
plt.plot(xf, np.abs(yf))
|
|
||||||
plt.title('FFT of ' + log_entry)
|
|
||||||
plt.xlabel('Frequency')
|
|
||||||
plt.ylabel('Amplitude')
|
|
||||||
plt.grid(True, which='both', color='#eeeeee')
|
|
||||||
plt.minorticks_on()
|
|
||||||
plt.get_current_fig_manager().set_window_title(log_entry)
|
|
||||||
plt.show()
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# grab flight log and save to file
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import serial
|
|
||||||
import os
|
|
||||||
|
|
||||||
PORT = os.environ['PORT']
|
|
||||||
DIR = os.path.dirname(os.path.realpath(__file__))
|
|
||||||
|
|
||||||
dev = serial.Serial(port=PORT, baudrate=115200, timeout=0.5)
|
|
||||||
|
|
||||||
log = open(f'{DIR}/log/{datetime.datetime.now().isoformat()}.csv', 'wb')
|
|
||||||
|
|
||||||
print('Downloading log...')
|
|
||||||
count = 0
|
|
||||||
dev.write('log\n'.encode())
|
|
||||||
while True:
|
|
||||||
line = dev.readline()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
log.write(line)
|
|
||||||
count += 1
|
|
||||||
print(f'\r{count} lines', end='')
|
|
||||||
|
|
||||||
print(f'\nWritten {os.path.relpath(log.name, os.curdir)}')
|
|
||||||