mirror of
https://gitee.com/mirrors_PX4/PX4-Autopilot.git
synced 2026-06-09 18:20:04 +08:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b902efdca | |||
| a3657c1396 | |||
| 5d3083ec00 | |||
| 845d65fe9b | |||
| 648e730c4a | |||
| 6fcfd5fac1 | |||
| ba35ca461c | |||
| 0e4c794674 | |||
| 3d6056411f | |||
| abb80ae71e | |||
| a954ec4d55 | |||
| 7c14a63855 | |||
| d78af5436e | |||
| 45285a57ad | |||
| bbcd153e94 | |||
| 3bde706cb3 | |||
| dbe57fad08 | |||
| f7740bdfd2 | |||
| 74a8d897b2 | |||
| 1ee3b7e77d | |||
| cc492bbf6e | |||
| 9cc1e01bd8 | |||
| 724987d59e | |||
| 27ff547e07 | |||
| db97dd471d | |||
| 5509061803 | |||
| f2026343d7 | |||
| fee81a5c88 | |||
| d06e9cc302 | |||
| e35c1f430c | |||
| 1928758fbc | |||
| f73c7977dd | |||
| 53bdceb895 | |||
| cdab0cb6e4 | |||
| 82ea544e8c | |||
| ddb9a5d0b9 | |||
| 2c8ef05c2d | |||
| 72454c4fd2 | |||
| 69b7a21f02 | |||
| 7cb7977263 | |||
| 8acf273917 | |||
| 3870992bac | |||
| 550bbd9051 | |||
| 19d3e6285b | |||
| 898d631b24 | |||
| e7eca72d02 | |||
| 9f4e642e9f | |||
| 6f026f35b1 | |||
| 46d1489d36 | |||
| 4710366862 | |||
| 82a482ec0b | |||
| 49624a6457 | |||
| 7acd2e93eb |
@@ -41,7 +41,7 @@ jobs:
|
||||
pull_request_base_branch_name: 'main'
|
||||
pull_request_title: New PX4 guide translations (Crowdin) - ${{ matrix.lc }}
|
||||
pull_request_body: 'New PX4 guide Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action) for ${{ matrix.lc }}'
|
||||
pull_request_labels: Documentation
|
||||
pull_request_labels: 'Documentation 📑'
|
||||
pull_request_reviewers: hamishwillee
|
||||
download_language: ${{ matrix.lc }}
|
||||
env:
|
||||
|
||||
@@ -21,13 +21,13 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '18'
|
||||
|
||||
- name: Create logs directory
|
||||
run: |
|
||||
mkdir logs
|
||||
|
||||
- name: Get changed english files
|
||||
- name: Get changed english doc files
|
||||
id: get_changed_markdown_english
|
||||
uses: tj-actions/changed-files@v35.9.2
|
||||
with:
|
||||
@@ -48,10 +48,10 @@ jobs:
|
||||
- name: Run link checker
|
||||
id: link-check
|
||||
run: |
|
||||
npm -g install markdown_link_checker_sc@0.0.134
|
||||
markdown_link_checker_sc -r ${{ github.workspace }} -d en -f ./docs/logs/prFiles.json -i assets -u docs.px4.io/main/ > ./docs/logs/errorsFilteredByPrPages.md
|
||||
npm -g install markdown_link_checker_sc@0.0.138
|
||||
markdown_link_checker_sc -r ${{ github.workspace }} -d docs -e en -f ./logs/prFiles.json -i assets -u docs.px4.io/main/ > ./logs/errorsFilteredByPrPages.md
|
||||
mkdir -p ./pr
|
||||
cp ./docs/logs/errorsFilteredByPrPages.md ./pr/errorsFilteredByPrPages.md
|
||||
cp ./logs/errorsFilteredByPrPages.md ./pr/errorsFilteredByPrPages.md
|
||||
|
||||
- name: Read errorsFilteredByPrPages.md file
|
||||
id: read-errors-by-page
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# NOTE: this workflow is now running on Dronecode / PX4 AWS account.
|
||||
# - If you want to keep the tests running in GitHub Actions you need to uncomment the "runs-on: ubuntu-latest" lines
|
||||
# and comment the "runs-on: [runs-on,runner=..." lines.
|
||||
# - If you would like to duplicate this setup try setting up "RunsOn" on your own AWS account try https://runs-on.com
|
||||
|
||||
name: ROS Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [runs-on,runner=16cpu-linux-x64,image=ubuntu22-full-x64,"run-id=${{ github.run_id }}",spot=false]
|
||||
container:
|
||||
image: px4io/px4-dev-ros2-galactic:2021-09-08
|
||||
options: --privileged --ulimit core=-1 --security-opt seccomp=unconfined
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Git Ownership Workaround
|
||||
run: git config --system --add safe.directory '*'
|
||||
|
||||
- name: Install gazebo
|
||||
run: |
|
||||
apt update && apt install -y gazebo11 libgazebo11-dev gstreamer1.0-plugins-bad gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly libgstreamer-plugins-base1.0-dev
|
||||
|
||||
- name: Prepare ccache timestamp
|
||||
id: ccache_cache_timestamp
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
string(TIMESTAMP current_date "%Y-%m-%d-%H;%M;%S" UTC)
|
||||
message("::set-output name=timestamp::${current_date}")
|
||||
- name: ccache cache files
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.ccache
|
||||
key: ros_integration_tests-${{matrix.config.build_type}}-ccache-${{steps.ccache_cache_timestamp.outputs.timestamp}}
|
||||
restore-keys: ros_integration_tests-${{matrix.config.build_type}}-ccache-
|
||||
- name: setup ccache
|
||||
run: |
|
||||
mkdir -p ~/.ccache
|
||||
echo "base_dir = ${GITHUB_WORKSPACE}" > ~/.ccache/ccache.conf
|
||||
echo "compression = true" >> ~/.ccache/ccache.conf
|
||||
echo "compression_level = 6" >> ~/.ccache/ccache.conf
|
||||
echo "max_size = 300M" >> ~/.ccache/ccache.conf
|
||||
echo "hash_dir = false" >> ~/.ccache/ccache.conf
|
||||
ccache -s
|
||||
ccache -z
|
||||
|
||||
- name: Get and build micro-xrce-dds-agent
|
||||
run: |
|
||||
cd /opt
|
||||
git clone --recursive https://github.com/eProsima/Micro-XRCE-DDS-Agent.git
|
||||
cd Micro-XRCE-DDS-Agent
|
||||
git checkout v2.2.1 # recent versions require cmake 3.22, but px4-dev-ros2-galactic:2021-09-08 is on 3.16
|
||||
sed -i 's/_fastdds_tag 2.8.x/_fastdds_tag 2.8.2/g' CMakeLists.txt
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make -j2
|
||||
- name: ccache post-run micro-xrce-dds-agent
|
||||
run: ccache -s
|
||||
|
||||
- name: Get and build the ros2 interface library
|
||||
shell: bash
|
||||
run: |
|
||||
PX4_DIR="$(pwd)"
|
||||
. /opt/ros/galactic/setup.bash
|
||||
mkdir -p /opt/px4_ws/src
|
||||
cd /opt/px4_ws/src
|
||||
git clone --recursive https://github.com/Auterion/px4-ros2-interface-lib.git
|
||||
cd ..
|
||||
# Copy messages to ROS workspace
|
||||
"${PX4_DIR}/Tools/copy_to_ros_ws.sh" "$(pwd)"
|
||||
rm -rf src/translation_node src/px4_msgs_old
|
||||
colcon build --symlink-install
|
||||
- name: ccache post-run ros workspace
|
||||
run: ccache -s
|
||||
|
||||
- name: Build PX4
|
||||
run: make px4_sitl_default
|
||||
- name: ccache post-run px4/firmware
|
||||
run: ccache -s
|
||||
- name: Build SITL Gazebo
|
||||
run: make px4_sitl_default sitl_gazebo-classic
|
||||
- name: ccache post-run sitl_gazebo-classic
|
||||
run: ccache -s
|
||||
|
||||
- name: Core dump settings
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
echo "`pwd`/%e.core" > /proc/sys/kernel/core_pattern
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: |
|
||||
. /opt/px4_ws/install/setup.bash
|
||||
/opt/Micro-XRCE-DDS-Agent/build/MicroXRCEAgent udp4 localhost -p 8888 -v 0 &
|
||||
test/ros_test_runner.py --verbose --model iris --upload --force-color
|
||||
timeout-minutes: 45
|
||||
|
||||
- name: Upload failed logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: failed-logs.zip
|
||||
path: |
|
||||
logs/**/**/**/*.log
|
||||
logs/**/**/**/*.ulg
|
||||
build/px4_sitl_default/tmp_ros_tests/rootfs/log/**/*.ulg
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
PX4_HOME_LON: ${{matrix.config.longitude}}
|
||||
PX4_HOME_ALT: ${{matrix.config.altitude}}
|
||||
PX4_CMAKE_BUILD_TYPE: ${{matrix.config.build_type}}
|
||||
run: test/mavsdk_tests/mavsdk_test_runner.py --speed-factor 10 --abort-early --model ${{matrix.config.model}} --upload test/mavsdk_tests/configs/sitl.json --verbose
|
||||
run: test/mavsdk_tests/mavsdk_test_runner.py --speed-factor 10 --abort-early --model ${{matrix.config.model}} --upload test/mavsdk_tests/configs/sitl.json --verbose --force-color
|
||||
timeout-minutes: 45
|
||||
|
||||
- name: Upload failed logs
|
||||
|
||||
@@ -108,3 +108,4 @@ src/systemcmds/topic_listener/listener_generated.cpp
|
||||
|
||||
# colcon
|
||||
log/
|
||||
keys/
|
||||
|
||||
@@ -126,6 +126,15 @@ then
|
||||
set AUTOCNF yes
|
||||
fi
|
||||
|
||||
# Allow overriding parameters via env variables: export PX4_PARAM_{name}={value}
|
||||
env | while IFS='=' read -r line; do
|
||||
value=${line#*=}
|
||||
name=${line%%=*}
|
||||
case $name in
|
||||
"PX4_PARAM_"*) param set "${name#PX4_PARAM_}" "$value" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# multi-instance setup
|
||||
# shellcheck disable=SC2154
|
||||
param set MAV_SYS_ID $((px4_instance+1))
|
||||
|
||||
@@ -71,12 +71,6 @@ then
|
||||
fi
|
||||
|
||||
|
||||
# Heater driver for temperature regulated IMUs.
|
||||
if param compare -s SENS_EN_THERMAL 1
|
||||
then
|
||||
heater start
|
||||
fi
|
||||
|
||||
# Teraranger one tof sensor
|
||||
if param greater -s SENS_EN_TRANGER 0
|
||||
then
|
||||
|
||||
@@ -404,6 +404,14 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
# Heater driver for temperature regulated IMUs.
|
||||
# The heater needs to start after px4io.
|
||||
if param compare -s SENS_EN_THERMAL 1
|
||||
then
|
||||
heater start
|
||||
fi
|
||||
|
||||
|
||||
#
|
||||
# RC update (map raw RC input to calibrate manual control)
|
||||
# start before commander
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import nacl.encoding
|
||||
import nacl.signing
|
||||
import nacl.hash
|
||||
import struct
|
||||
import binascii
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
def make_public_key_h_file(signing_key,key_name):
|
||||
"""
|
||||
This file generate the public key header file
|
||||
to be included into the bootloader build.
|
||||
"""
|
||||
public_key_c='\n'
|
||||
for i,c in enumerate(signing_key.verify_key.encode(encoder=nacl.encoding.RawEncoder)):
|
||||
public_key_c+= hex(c)
|
||||
public_key_c+= ', '
|
||||
if((i+1)%8==0):
|
||||
public_key_c+= '\n'
|
||||
with open(key_name+'.pub' ,mode='w') as f:
|
||||
f.write("//Public key to verify signed binaries")
|
||||
f.write(public_key_c)
|
||||
|
||||
def make_key_file(signing_key, key_name):
|
||||
"""
|
||||
Writes the key.json file.
|
||||
Attention do not override your existing key files.
|
||||
Do not publish your private key!!
|
||||
"""
|
||||
|
||||
key_file = Path(key_name+'.json')
|
||||
if key_file.is_file():
|
||||
print("ATTENTION: key.json already exists, are you sure you want to overwrite it?")
|
||||
print("Remove file and run script again.")
|
||||
print("Script aborted!")
|
||||
sys.exit(1)
|
||||
|
||||
keys={}
|
||||
keys["date"] = time.asctime()
|
||||
keys["public"] = (signing_key.verify_key.encode(encoder=nacl.encoding.HexEncoder)).decode()
|
||||
keys["private"] = binascii.hexlify(signing_key._seed).decode()
|
||||
#print (keys)
|
||||
with open(key_name+'.json', "w") as write_file:
|
||||
json.dump(keys, write_file)
|
||||
return keys
|
||||
|
||||
def ed25519_sign(private_key, signee_bin):
|
||||
"""
|
||||
This function creates the signature. It takes the private key and the binary file
|
||||
and returns the tuple (signature, public key)
|
||||
"""
|
||||
|
||||
signing_key = nacl.signing.SigningKey(private_key, encoder=nacl.encoding.HexEncoder)
|
||||
|
||||
# Sign a message with the signing key
|
||||
signed = signing_key.sign(signee_bin,encoder=nacl.encoding.RawEncoder)
|
||||
|
||||
# Obtain the verify key for a given signing key
|
||||
verify_key = signing_key.verify_key
|
||||
|
||||
# Serialize the verify key to send it to a third party
|
||||
verify_key_hex = verify_key.encode(encoder=nacl.encoding.HexEncoder)
|
||||
|
||||
return signed.signature, verify_key_hex
|
||||
|
||||
|
||||
def sign(bin_file_path, key_file_path=None, generated_key_file=None):
|
||||
"""
|
||||
reads the binary file and the key file.
|
||||
If the key file does not exist, it generates a
|
||||
new key file.
|
||||
"""
|
||||
|
||||
with open(bin_file_path,mode='rb') as f:
|
||||
signee_bin = f.read()
|
||||
# Align to 4 bytes. Signature always starts at
|
||||
# 4 byte aligned address, but the signee size
|
||||
# might not be aligned
|
||||
if len(signee_bin)%4 != 0:
|
||||
signee_bin += bytearray(b'\xff')*(4-len(signee_bin)%4)
|
||||
|
||||
try:
|
||||
with open(key_file_path,mode='r') as f:
|
||||
keys = json.load(f)
|
||||
#print(keys)
|
||||
except:
|
||||
print('ERROR: Key file',key_file_path,'not found')
|
||||
sys.exit(1)
|
||||
|
||||
signature, public_key = ed25519_sign(keys["private"], signee_bin)
|
||||
|
||||
# Do a sanity check. This type of signature is always 64 bytes long
|
||||
assert len(signature) == 64
|
||||
|
||||
# Print out the signing information
|
||||
print("Binary \"%s\" signed."%bin_file_path)
|
||||
print("Signature:",binascii.hexlify(signature))
|
||||
print("Public key:",binascii.hexlify(public_key))
|
||||
|
||||
return signee_bin + signature, public_key
|
||||
|
||||
def generate_key(key_file):
|
||||
"""
|
||||
Generate two files:
|
||||
"key_file.pub" containing the public key in C-format to be included in the bootloader build
|
||||
"key_file.json, containt both private and public key.
|
||||
Do not leak or loose the key file. This is mandatory for signing
|
||||
all future binaries you want to deploy!
|
||||
"""
|
||||
|
||||
# Generate a new random signing key
|
||||
signing_key = nacl.signing.SigningKey.generate()
|
||||
# Serialize the verify key to send it to a third party
|
||||
verify_key_hex = signing_key.verify_key.encode(encoder=nacl.encoding.HexEncoder)
|
||||
print("public key :",verify_key_hex)
|
||||
|
||||
private_key_hex=binascii.hexlify(signing_key._seed)
|
||||
print("private key :",private_key_hex)
|
||||
|
||||
keys = make_key_file(signing_key,key_file)
|
||||
make_public_key_h_file(signing_key,key_file)
|
||||
return keys
|
||||
|
||||
if(__name__ == "__main__"):
|
||||
|
||||
parser = argparse.ArgumentParser(description="""CLI tool to calculate and add signature to px4. bin files\n
|
||||
if given it takes an existing key file, else it generate new keys""",
|
||||
epilog="Output: SignedBin.bin and a key.json file")
|
||||
parser.add_argument("signee", help=".bin file to add signature", nargs='?', default=None)
|
||||
parser.add_argument("signed", help="signed output .bin", nargs='?', default=None)
|
||||
|
||||
parser.add_argument("--key", help="key.json file", default="Tools/test_keys/test_keys.json")
|
||||
parser.add_argument("--rdct", help="binary R&D certificate file", default=None)
|
||||
parser.add_argument("--genkey", help="new generated key", default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Only generate a key pair, don't sign
|
||||
if args.genkey:
|
||||
# Only create a key file, don't sign
|
||||
generate_key(args.genkey)
|
||||
print('New key file generated:',args.genkey)
|
||||
sys.exit(0);
|
||||
|
||||
# Check that both signee and signed exist
|
||||
if not args.signee or not args.signed:
|
||||
print("ERROR: Must either provide file names for both signee and signed")
|
||||
print(" or --genkey [key] to generate a new key pair")
|
||||
sys.exit(1)
|
||||
|
||||
# Issue a warning when signing with testing key
|
||||
if args.key=='Tools/test_keys/test_keys.json':
|
||||
print("WARNING: Signing with PX4 test key")
|
||||
|
||||
# Sign the binary
|
||||
signed, public_key = sign(args.signee, args.key, args.genkey)
|
||||
|
||||
with open(args.signed, mode='wb') as fs:
|
||||
# Write signed binary
|
||||
fs.write(signed)
|
||||
|
||||
# Append rdcert if given
|
||||
try:
|
||||
with open(args.rdct ,mode='rb') as f:
|
||||
with open(args.signed, mode='ab') as fs:
|
||||
fs.write(f.read())
|
||||
except:
|
||||
pass
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import ChaCha20
|
||||
except ImportError as e:
|
||||
print("Failed to import crypto: " + str(e))
|
||||
print("")
|
||||
print("You may need to install it using:")
|
||||
print(" pip3 install --user pycryptodome")
|
||||
print("")
|
||||
sys.exit(1)
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_OAEP
|
||||
from Crypto.Hash import SHA256
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(description="""CLI tool to decrypt an ulog file\n""")
|
||||
parser.add_argument("ulog_file", help=".ulge/.ulgc, encrypted log file", nargs='?', default=None)
|
||||
parser.add_argument("ulog_key", help=".ulgk, legacy encrypted key (give empty string '' to ignore for .ulge)", nargs='?', default=None)
|
||||
parser.add_argument("rsa_key", help=".pem format key for decrypting the ulog key", nargs='?', default=None)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check all arguments are given
|
||||
if not args.rsa_key:
|
||||
print('Need all arguments, the encrypted ulog file, key file (or empty string if not needed) and the key decryption key (.pem)')
|
||||
sys.exit(1)
|
||||
|
||||
# Read the private RSA key to decrypt the cahcha key
|
||||
with open(args.rsa_key, 'rb') as f:
|
||||
r = RSA.importKey(f.read(), passphrase='')
|
||||
|
||||
if args.ulog_key == "":
|
||||
key_data_filename = args.ulog_file
|
||||
magic = "ULogEnc"
|
||||
else:
|
||||
key_data_filename = args.ulog_key
|
||||
magic = "ULogKey"
|
||||
|
||||
with open(key_data_filename, 'rb') as f:
|
||||
# Read the encrypted xchacha key and the nonce
|
||||
ulog_key_header = f.read(22)
|
||||
|
||||
# Parse the header
|
||||
try:
|
||||
# magic
|
||||
if not ulog_key_header.startswith(bytearray(magic.encode())):
|
||||
print("Incorrect header magic")
|
||||
raise Exception()
|
||||
# version
|
||||
if ulog_key_header[7] != 1:
|
||||
print("Unsupported header version")
|
||||
raise Exception()
|
||||
# expected key exchange algorithm (RSA_OAEP)
|
||||
if ulog_key_header[16] != 4:
|
||||
print("Unsupported key algorithm")
|
||||
raise Exception()
|
||||
key_size = ulog_key_header[19] << 8 | ulog_key_header[18]
|
||||
nonce_size = ulog_key_header[21] << 8 | ulog_key_header[20]
|
||||
ulog_key_cipher = f.read(key_size)
|
||||
nonce = f.read(nonce_size)
|
||||
except:
|
||||
print("Keydata format error")
|
||||
sys.exit(1)
|
||||
|
||||
if magic == "ULogEnc":
|
||||
data_offset = 22 + key_size + nonce_size
|
||||
else:
|
||||
data_offset = 0
|
||||
|
||||
# Decrypt the xchacha key
|
||||
cipher_rsa = PKCS1_OAEP.new(r,SHA256)
|
||||
ulog_key = cipher_rsa.decrypt(ulog_key_cipher)
|
||||
#print(binascii.hexlify(ulog_key))
|
||||
|
||||
# Read and decrypt the ulog data
|
||||
cipher = ChaCha20.new(key=ulog_key, nonce=nonce)
|
||||
|
||||
outfilename = Path(args.ulog_file).stem + ".ulog"
|
||||
with open(args.ulog_file, 'rb') as f:
|
||||
if data_offset > 0:
|
||||
f.seek(data_offset)
|
||||
with open(outfilename, 'wb') as out:
|
||||
out.write(cipher.decrypt(f.read()))
|
||||
@@ -0,0 +1,86 @@
|
||||
# PX4 Log Encryption Tools
|
||||
|
||||
Tools for generating encryption keys, building PX4 firmware with encrypted logs, downloading logs, and decrypting them.
|
||||
|
||||
For more information see: https://docs.px4.io/main/en/dev_log/log_encryption.html
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Get the board file**:
|
||||
In order to use these tools you need to create an `encrypted_logs` target in your target board directory. For example:
|
||||
```bash
|
||||
encrypted_logs.px4board
|
||||
```
|
||||
Using `make menuconfig` you should enable these settings: `Blake2s hash algorithm`, `entropy pool` and `strong random number generator` and select `use interrupts` to feed timing randomness to the entropy pool.
|
||||
Once you have generated the keys make sure you add them to the boardconfig.
|
||||
|
||||
```bash
|
||||
make <your_board_name>_encrypted_logs menuconfig
|
||||
```
|
||||
|
||||
2. **Generate Keys**:
|
||||
```bash
|
||||
cd PX4-Autopilot/Tools/log_encryption
|
||||
python3 generate_keys.py
|
||||
```
|
||||
|
||||
Make sure you have the right key in your board file
|
||||
```CONFIG_PUBLIC_KEY1="../../../keys/public/public_key.pub"```
|
||||
|
||||
3. **Build Firmware**:
|
||||
```bash
|
||||
cd PX4-Autopilot
|
||||
|
||||
AND
|
||||
|
||||
make <your_board_name>_encrypted_logs
|
||||
|
||||
FOR INSTANCE
|
||||
make_ark_fpv_encrypted_logs
|
||||
|
||||
Upload the custom firmware on your flight controller and record some logs
|
||||
```
|
||||
|
||||
4. **Download Logs**:
|
||||
```bash
|
||||
cd PX4-Autopilot/Tools/log_encryption
|
||||
|
||||
python3 download_logs.py /dev/ttyACM0 --baudrate 57600
|
||||
|
||||
OR
|
||||
|
||||
python3 download_logs.py udp:0.0.0.0:14550
|
||||
```
|
||||
|
||||
Addresses might need to be adjusted
|
||||
|
||||
5. **Decrypt Logs**:
|
||||
The easiest way to run this is to have your private key and encrypted logs in the following folders respectively:
|
||||
```bash
|
||||
PX4-Autopilot/keys/private
|
||||
PX4-Autopilot/logs/encrypted
|
||||
```
|
||||
Then run:
|
||||
```bash
|
||||
cd PX4-Autopilot/Tools/log_encryption
|
||||
|
||||
AND
|
||||
# Uses default key + default folder
|
||||
python3 decrypt_logs.py
|
||||
|
||||
OR
|
||||
# Use --help to get all the options
|
||||
python3 decrypt_logs.py --help
|
||||
```
|
||||
|
||||
Your decrypted logs can be found in:
|
||||
```bash
|
||||
PX4-Autopilot/logs/decrypted
|
||||
```
|
||||
Otherwise
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- **`keys/`**: Encryption keys.
|
||||
- **`logs/encrypted/`**: Downloaded encrypted logs.
|
||||
- **`logs/decrypted/`**: Decrypted logs.
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import ChaCha20
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_OAEP
|
||||
from Crypto.Hash import SHA256
|
||||
except ImportError as e:
|
||||
print("Failed to import crypto: " + str(e))
|
||||
print("You may need to install it using:")
|
||||
print(" pip3 install --user pycryptodome")
|
||||
sys.exit(1)
|
||||
|
||||
PX4_MAIN_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
ENCRYPTED_LOGS_DIR = os.path.join(PX4_MAIN_DIR, "logs/encrypted")
|
||||
DECRYPTED_LOGS_DIR = os.path.join(PX4_MAIN_DIR, "logs/decrypted")
|
||||
DEFAULT_PRIVATE_KEY = os.path.join(PX4_MAIN_DIR, "keys/private/private_key.pem")
|
||||
|
||||
def decrypt_log_file(ulog_file, private_key, output_folder):
|
||||
"""Decrypts a single encrypted log file (.ulge) and saves it as .ulg in the output folder."""
|
||||
|
||||
try:
|
||||
# Read the private RSA key
|
||||
with open(private_key, 'rb') as f:
|
||||
key = RSA.import_key(f.read())
|
||||
|
||||
magic = "ULogEnc"
|
||||
header_size = 22
|
||||
|
||||
with open(ulog_file, 'rb') as f:
|
||||
# Encrypted .ulge file contains following sections:
|
||||
# -------------------------
|
||||
# | Header |
|
||||
# -------------------------
|
||||
# | Wrapped symmetric key |
|
||||
# -------------------------
|
||||
# | Encrypted ulog data |
|
||||
# -------------------------
|
||||
header = f.read(header_size)
|
||||
|
||||
# Parse the header
|
||||
if not header.startswith(bytearray(magic.encode())):
|
||||
print(f"Skipping {ulog_file}: Incorrect header magic")
|
||||
return
|
||||
if header[7] != 1:
|
||||
print(f"Skipping {ulog_file}: Unsupported header version")
|
||||
return
|
||||
if header[16] != 4:
|
||||
print(f"Skipping {ulog_file}: Unsupported key algorithm")
|
||||
return
|
||||
|
||||
key_size = header[19] << 8 | header[18]
|
||||
nonce_size = header[21] << 8 | header[20]
|
||||
cipher = f.read(key_size)
|
||||
nonce = f.read(nonce_size)
|
||||
|
||||
data_offset = header_size + key_size + nonce_size
|
||||
|
||||
# Try to decrypt the ChaCha key
|
||||
cipher_rsa = PKCS1_OAEP.new(key, SHA256)
|
||||
try:
|
||||
ulog_key = cipher_rsa.decrypt(cipher)
|
||||
except ValueError:
|
||||
print(f"Skipping {ulog_file}: Incorrect decryption (wrong key)")
|
||||
return
|
||||
|
||||
# Read and decrypt the log data
|
||||
cipher = ChaCha20.new(key=ulog_key, nonce=nonce)
|
||||
|
||||
# Save decrypted log with .ulg extension
|
||||
output_path = os.path.join(output_folder, Path(ulog_file).stem + ".ulg")
|
||||
with open(ulog_file, 'rb') as f:
|
||||
if data_offset > 0:
|
||||
f.seek(data_offset)
|
||||
with open(output_path, 'wb') as out:
|
||||
out.write(cipher.decrypt(f.read()))
|
||||
|
||||
print(f"{output_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Skipping {ulog_file}: Error occurred - {e}")
|
||||
|
||||
|
||||
def decrypt_all_logs(private_key_path, log_source_path=None):
|
||||
"""Decrypts all logs in the given folder or a single file."""
|
||||
|
||||
if log_source_path and os.path.isfile(log_source_path):
|
||||
logs = [log_source_path]
|
||||
else:
|
||||
# Use default encrypted logs directory if not provided
|
||||
folder = log_source_path if log_source_path else ENCRYPTED_LOGS_DIR
|
||||
logs = [os.path.join(folder, f) for f in os.listdir(folder) if f.endswith(".ulge")]
|
||||
|
||||
if not logs:
|
||||
print("No encrypted logs found.")
|
||||
return
|
||||
|
||||
print(f"Found {len(logs)} encrypted log(s). Decrypting...")
|
||||
|
||||
os.makedirs(DECRYPTED_LOGS_DIR, exist_ok=True)
|
||||
|
||||
for log_path in logs:
|
||||
decrypt_log_file(log_path, private_key_path, DECRYPTED_LOGS_DIR)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Decrypt PX4 encrypted log files (.ulge) using a ChaCha20+RSA scheme.\n\n"
|
||||
"Usage examples:\n"
|
||||
" python3 decrypt_logs.py /path/to/private_key.pem /path/to/custom_log.ulge\n"
|
||||
" python3 decrypt_logs.py /path/to/private_key.pem /path/to/folder_with_ulge_files\n"
|
||||
" python3 decrypt_logs.py # Uses default key + default log folder\n",
|
||||
formatter_class=argparse.RawTextHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument("private_key", nargs="?", default=None,
|
||||
help="Path to the private RSA key (.pem). If omitted, uses default key.")
|
||||
parser.add_argument("log_file_or_folder", nargs="?", default=None,
|
||||
help="Path to a single .ulge file or folder containing them. If omitted, uses default encrypted log folder.")
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
private_key_path = args.private_key if args.private_key else DEFAULT_PRIVATE_KEY
|
||||
|
||||
if not os.path.exists(private_key_path):
|
||||
print(f"Error: Private key file not found at {private_key_path}")
|
||||
sys.exit(1)
|
||||
|
||||
decrypt_all_logs(private_key_path, args.log_file_or_folder)
|
||||
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
import threading
|
||||
from pymavlink import mavutil
|
||||
from argparse import ArgumentParser
|
||||
|
||||
class MavlinkLogDownloader:
|
||||
def __init__(self, connection_url, output_dir, baudrate=57600, source_system=254):
|
||||
self.connection_url = connection_url
|
||||
self.output_dir = output_dir
|
||||
self.encrypted_dir = os.path.join(output_dir, "encrypted")
|
||||
self.running = True
|
||||
|
||||
# Ensure directories exist
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
os.makedirs(self.encrypted_dir, exist_ok=True)
|
||||
|
||||
# # Handle serial or UDP connections
|
||||
if os.path.exists(connection_url): # likely a serial device
|
||||
self.mav = mavutil.mavlink_connection(connection_url, baud=baudrate, source_system=source_system)
|
||||
else:
|
||||
self.mav = mavutil.mavlink_connection(connection_url, source_system=source_system)
|
||||
|
||||
|
||||
self.mav.WIRE_PROTOCOL_VERSION = "2.0"
|
||||
|
||||
# Start heartbeat thread
|
||||
self.heartbeat_thread = threading.Thread(target=self.send_heartbeat_thread)
|
||||
self.heartbeat_thread.daemon = True
|
||||
self.heartbeat_thread.start()
|
||||
|
||||
self.mav.wait_heartbeat()
|
||||
print(f"Heartbeat received from system {self.mav.target_system}, component {self.mav.target_component}")
|
||||
|
||||
# Waking up the autopilot, it is needed to ensure we get answer for log request
|
||||
self.mav.mav.command_long_send(
|
||||
self.mav.target_system,
|
||||
self.mav.target_component,
|
||||
mavutil.mavlink.MAV_CMD_REQUEST_MESSAGE, # Command ID 512
|
||||
0, # Confirmation
|
||||
mavutil.mavlink.MAVLINK_MSG_ID_AUTOPILOT_VERSION, # param1: Message ID 148
|
||||
0, 0, 0, 0, 0, 0 # params 2–7 are not used for this message
|
||||
)
|
||||
|
||||
# Allow heartbeats to establish connection
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
def send_heartbeat_thread(self):
|
||||
while self.running:
|
||||
self.mav.mav.heartbeat_send(
|
||||
mavutil.mavlink.MAV_TYPE_GCS,
|
||||
mavutil.mavlink.MAV_AUTOPILOT_GENERIC,
|
||||
0, 0, 0
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def download_logs(self):
|
||||
"""Downloads logs to the output_dir."""
|
||||
print("Request logs...")
|
||||
self.mav.mav.log_request_list_send(self.mav.target_system, self.mav.target_component, 0, 0xFFFF)
|
||||
|
||||
log_entries = {}
|
||||
total_logs = None
|
||||
start_time = time.time()
|
||||
last_entry_time = None
|
||||
|
||||
while True:
|
||||
current_time = time.time()
|
||||
|
||||
# Case 1: If we haven't received any entries yet and we've waited more than 5 seconds
|
||||
if not log_entries and current_time > start_time + 5:
|
||||
print("Timed out waiting for first log entry (5s)")
|
||||
break
|
||||
|
||||
# Case 2: If we have received at least one entry and it's been more than 3 seconds since the last one
|
||||
if last_entry_time and current_time - last_entry_time > 3:
|
||||
print(f"No new log entries received for 3 seconds. Moving on.")
|
||||
break
|
||||
|
||||
# Case 3: If we've received all expected logs
|
||||
if total_logs is not None and len(log_entries) >= total_logs:
|
||||
print(f"Received all {total_logs} log entries.")
|
||||
break
|
||||
|
||||
msg = self.mav.recv_match(type='LOG_ENTRY', blocking=True, timeout=1)
|
||||
|
||||
if msg and msg.id not in log_entries:
|
||||
last_entry_time = time.time()
|
||||
log_entries[msg.id] = msg
|
||||
|
||||
if total_logs is None:
|
||||
total_logs = msg.num_logs
|
||||
|
||||
print(f"Log ID: {msg.id}, Size: {msg.size} bytes, Date: {msg.time_utc} ({len(log_entries)}/{total_logs})")
|
||||
|
||||
if not log_entries:
|
||||
print("No log entries found.")
|
||||
return
|
||||
|
||||
for entry in log_entries.values():
|
||||
self.download_log_file(entry)
|
||||
|
||||
self.classify_logs()
|
||||
|
||||
|
||||
def download_log_file(self, log_entry):
|
||||
"""Downloads a log file to the output_dir."""
|
||||
log_id = log_entry.id
|
||||
log_size = log_entry.size
|
||||
log_date = time.strftime("%Y-%m-%d_%H-%M-%S", time.gmtime(log_entry.time_utc))
|
||||
output_filename = os.path.join(self.output_dir, f"log-{log_date}_{log_id}.ulg")
|
||||
|
||||
print(f"Downloading log {log_id} ({log_size} bytes) to {output_filename}...")
|
||||
|
||||
with open(output_filename, 'wb') as f:
|
||||
self.mav.mav.log_request_data_send(self.mav.target_system, self.mav.target_component, log_id, 0, 0xFFFFFFFF)
|
||||
|
||||
bytes_received = 0
|
||||
while bytes_received < log_size:
|
||||
msg = self.mav.recv_match(type='LOG_DATA', blocking=True, timeout=5)
|
||||
if msg:
|
||||
data_bytes = bytes(msg.data[:msg.count])
|
||||
f.write(data_bytes)
|
||||
bytes_received += msg.count
|
||||
else:
|
||||
print("Timeout waiting for log data.")
|
||||
break
|
||||
|
||||
print(f"Finished downloading log {log_id}.")
|
||||
|
||||
def classify_logs(self):
|
||||
"""Classifies logs as encrypted (.ulge) based on file content."""
|
||||
for log_file in os.listdir(self.output_dir):
|
||||
log_path = os.path.join(self.output_dir, log_file)
|
||||
|
||||
if not os.path.isfile(log_path):
|
||||
continue
|
||||
|
||||
# Read first 10 bytes to check for "ULogEnc"
|
||||
with open(log_path, 'rb') as f:
|
||||
first_bytes = f.read(10)
|
||||
|
||||
if b'ULogEnc' in first_bytes:
|
||||
new_filename = log_file.replace(".ulg", ".ulge")
|
||||
new_path = os.path.join(self.encrypted_dir, new_filename)
|
||||
print(f"Found encrypted log: {new_path}")
|
||||
shutil.move(log_path, new_path)
|
||||
|
||||
|
||||
def cleanup(self):
|
||||
"""Stop the heartbeat thread and clean up resources."""
|
||||
self.running = False
|
||||
if self.heartbeat_thread.is_alive():
|
||||
self.heartbeat_thread.join(timeout=2.0)
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(description="Download PX4 log files over MAVLink.")
|
||||
parser.add_argument('connection_url', help="MAVLink connection URL (e.g., udp:0.0.0.0:14550, /dev/ttyACM0 --baudrate 57600)")
|
||||
parser.add_argument('--output', '-o', default=os.path.join(os.path.dirname(__file__), "../..", "logs"), help="Output directory for log files (default: ../../logs)")
|
||||
parser.add_argument('--baudrate', type=int, default=57600, help="Baudrate for serial connection (default: 57600)")
|
||||
parser.add_argument('--source-system', type=int, default=254, help="MAVLink source system ID (default: 254)")
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
output_dir = os.path.abspath(args.output)
|
||||
|
||||
print(f"Connecting to {args.connection_url}...")
|
||||
log_downloader = MavlinkLogDownloader(
|
||||
args.connection_url,
|
||||
output_dir,
|
||||
baudrate=args.baudrate,
|
||||
source_system=args.source_system
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
log_downloader.download_logs()
|
||||
finally:
|
||||
log_downloader.cleanup()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,59 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
# Define the main PX4 directory (one level up from Tools)
|
||||
PX4_MAIN_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
|
||||
# Define the key folder paths
|
||||
KEY_FOLDER = os.path.join(PX4_MAIN_DIR, "keys")
|
||||
PUBLIC_KEY_FOLDER = os.path.join(KEY_FOLDER, "public")
|
||||
PRIVATE_KEY_FOLDER = os.path.join(KEY_FOLDER, "private")
|
||||
|
||||
# Define the key file paths
|
||||
PRIVATE_KEY_PATH = os.path.join(PRIVATE_KEY_FOLDER, "private_key.pem")
|
||||
PUBLIC_KEY_DER_PATH = os.path.join(PUBLIC_KEY_FOLDER, "public_key.der")
|
||||
PUBLIC_KEY_PUB_PATH = os.path.join(PUBLIC_KEY_FOLDER, "public_key.pub")
|
||||
|
||||
def create_key_folders():
|
||||
"""Creates key, public, and private folders if they do not exist."""
|
||||
for folder in [KEY_FOLDER, PUBLIC_KEY_FOLDER, PRIVATE_KEY_FOLDER]:
|
||||
if not os.path.exists(folder):
|
||||
os.makedirs(folder)
|
||||
print(f"Created '{folder}' directory.")
|
||||
else:
|
||||
print(f"'{folder}' directory already exists.")
|
||||
|
||||
def generate_private_key():
|
||||
"""Generates a private key if it does not exist."""
|
||||
if not os.path.exists(PRIVATE_KEY_PATH):
|
||||
print("Generating private key...")
|
||||
subprocess.run(["openssl", "genpkey", "-algorithm", "RSA", "-out", PRIVATE_KEY_PATH, "-pkeyopt", "rsa_keygen_bits:2048"])
|
||||
print(f"Private key generated at: {PRIVATE_KEY_PATH}")
|
||||
else:
|
||||
print("Private key already exists.")
|
||||
|
||||
def generate_public_key():
|
||||
"""Generates a public key in DER and PUB formats if they do not exist."""
|
||||
if not os.path.exists(PUBLIC_KEY_DER_PATH):
|
||||
print("Generating public key in DER format...")
|
||||
subprocess.run(["openssl", "rsa", "-pubout", "-in", PRIVATE_KEY_PATH, "-outform", "DER", "-out", PUBLIC_KEY_DER_PATH])
|
||||
print(f"Public key (DER) generated at: {PUBLIC_KEY_DER_PATH}")
|
||||
else:
|
||||
print("Public key (DER) already exists.")
|
||||
|
||||
if not os.path.exists(PUBLIC_KEY_PUB_PATH):
|
||||
print("Generating public key in hex format...")
|
||||
with open(PUBLIC_KEY_PUB_PATH, "w") as pub_file:
|
||||
process = subprocess.Popen(["xxd", "-p", PUBLIC_KEY_DER_PATH], stdout=subprocess.PIPE)
|
||||
output, _ = process.communicate()
|
||||
hex_string = output.decode().strip().replace("\n", "")
|
||||
formatted_hex = ", ".join(f"0x{hex_string[i:i+2]}" for i in range(0, len(hex_string), 2))
|
||||
pub_file.write(formatted_hex)
|
||||
print(f"Public key (hex) generated at: {PUBLIC_KEY_PUB_PATH}")
|
||||
else:
|
||||
print("Public key (hex) already exists.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_key_folders()
|
||||
generate_private_key()
|
||||
generate_public_key()
|
||||
@@ -30,12 +30,12 @@ def find_matching_brackets(brackets, s, verbose):
|
||||
|
||||
def extract_timer(line):
|
||||
# Try format: initIOTimer(Timer::Timer5, DMA{DMA::Index1, DMA::Stream0, DMA::Channel6}),
|
||||
search = re.search('Timer::([0-9a-zA-Z_]+)[,\)]', line, re.IGNORECASE)
|
||||
search = re.search('Timer::([0-9a-zA-Z_]+)[,)]', line, re.IGNORECASE)
|
||||
if search:
|
||||
return search.group(1), 'generic'
|
||||
|
||||
# NXP FlexPWM format format: initIOPWM(PWM::FlexPWM2),
|
||||
search = re.search('PWM::Flex([0-9a-zA-Z_]+)..PWM::Submodule([0-9])[,\)]', line, re.IGNORECASE)
|
||||
search = re.search('PWM::Flex([0-9a-zA-Z_]+)..PWM::Submodule([0-9])[,)]', line, re.IGNORECASE)
|
||||
if search:
|
||||
return (search.group(1) + '_' + search.group(2)), 'imxrt'
|
||||
|
||||
|
||||
@@ -26,3 +26,4 @@ setuptools>=39.2.0
|
||||
six>=1.12.0
|
||||
toml>=0.9
|
||||
sympy>=1.10.1
|
||||
pycryptodome
|
||||
|
||||
@@ -25,7 +25,16 @@ CONFIG_DRIVERS_IMU_INVENSENSE_IIM42652=y
|
||||
CONFIG_DRIVERS_IMU_INVENSENSE_IIM42653=y
|
||||
CONFIG_DRIVERS_IMU_MURATA_SCH16T=y
|
||||
CONFIG_COMMON_LIGHT=y
|
||||
CONFIG_COMMON_MAGNETOMETER=y
|
||||
CONFIG_DRIVERS_MAGNETOMETER_BOSCH_BMM150=y
|
||||
CONFIG_DRIVERS_MAGNETOMETER_HMC5883=y
|
||||
CONFIG_DRIVERS_MAGNETOMETER_QMC5883L=y
|
||||
CONFIG_DRIVERS_MAGNETOMETER_ISENTEK_IST8308=y
|
||||
CONFIG_DRIVERS_MAGNETOMETER_ISENTEK_IST8310=y
|
||||
CONFIG_DRIVERS_MAGNETOMETER_LIS3MDL=y
|
||||
CONFIG_DRIVERS_MAGNETOMETER_LSM303AGR=y
|
||||
CONFIG_DRIVERS_MAGNETOMETER_RM3100=y
|
||||
CONFIG_DRIVERS_MAGNETOMETER_MEMSIC_MMC5983MA=y
|
||||
CONFIG_DRIVERS_MAGNETOMETER_ST_IIS2MDC=y
|
||||
CONFIG_DRIVERS_POWER_MONITOR_INA226=y
|
||||
CONFIG_DRIVERS_POWER_MONITOR_INA228=y
|
||||
CONFIG_DRIVERS_POWER_MONITOR_INA238=y
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
CONFIG_DRIVERS_BATT_SMBUS=n
|
||||
CONFIG_DRIVERS_PX4IO=n
|
||||
CONFIG_BOARD_CRYPTO=y
|
||||
CONFIG_DRIVERS_STUB_KEYSTORE=y
|
||||
CONFIG_DRIVERS_SW_CRYPTO=y
|
||||
# CONFIG_EKF2_AUX_GLOBAL_POSITION is not set
|
||||
CONFIG_PUBLIC_KEY0="../../../Tools/test_keys/key0.pub"
|
||||
CONFIG_PUBLIC_KEY1="../../../Tools/test_keys/rsa2048.pub"
|
||||
@@ -84,6 +84,8 @@ CONFIG_CDCACM_RXBUFSIZE=600
|
||||
CONFIG_CDCACM_TXBUFSIZE=12000
|
||||
CONFIG_CDCACM_VENDORID=0x3185
|
||||
CONFIG_CDCACM_VENDORSTR="ARK"
|
||||
CONFIG_CRYPTO=y
|
||||
CONFIG_CRYPTO_RANDOM_POOL=y
|
||||
CONFIG_DEBUG_FULLOPT=y
|
||||
CONFIG_DEBUG_HARDFAULT_ALERT=y
|
||||
CONFIG_DEBUG_MEMFAULT=y
|
||||
|
||||
@@ -8,6 +8,8 @@ CONFIG_BOARD_SERIAL_TEL4="/dev/ttyS3"
|
||||
CONFIG_BOARD_SERIAL_RC="/dev/ttyS5"
|
||||
CONFIG_DRIVERS_ADC_BOARD_ADC=y
|
||||
CONFIG_DRIVERS_BAROMETER_BMP388=y
|
||||
CONFIG_DRIVERS_CAMERA_CAPTURE=y
|
||||
CONFIG_DRIVERS_CAMERA_TRIGGER=y
|
||||
CONFIG_DRIVERS_CDCACM_AUTOSTART=y
|
||||
CONFIG_COMMON_DIFFERENTIAL_PRESSURE=y
|
||||
CONFIG_COMMON_DISTANCE_SENSOR=y
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
CONFIG_BOARD_CRYPTO=y
|
||||
CONFIG_DRIVERS_STUB_KEYSTORE=y
|
||||
CONFIG_DRIVERS_SW_CRYPTO=y
|
||||
# CONFIG_EKF2_AUX_GLOBAL_POSITION is not set
|
||||
CONFIG_PUBLIC_KEY0="../../../Tools/test_keys/key0.pub"
|
||||
CONFIG_PUBLIC_KEY1="../../../Tools/test_keys/rsa2048.pub"
|
||||
@@ -82,6 +82,8 @@ CONFIG_CDCACM_RXBUFSIZE=600
|
||||
CONFIG_CDCACM_TXBUFSIZE=12000
|
||||
CONFIG_CDCACM_VENDORID=0x3185
|
||||
CONFIG_CDCACM_VENDORSTR="ARK"
|
||||
CONFIG_CRYPTO=y
|
||||
CONFIG_CRYPTO_RANDOM_POOL=y
|
||||
CONFIG_DEBUG_FULLOPT=y
|
||||
CONFIG_DEBUG_HARDFAULT_ALERT=y
|
||||
CONFIG_DEBUG_MEMFAULT=y
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
CONFIG_BOARD_CRYPTO=y
|
||||
CONFIG_DRIVERS_STUB_KEYSTORE=y
|
||||
CONFIG_DRIVERS_SW_CRYPTO=y
|
||||
# CONFIG_EKF2_AUX_GLOBAL_POSITION is not set
|
||||
CONFIG_PUBLIC_KEY0="../../../Tools/test_keys/key0.pub"
|
||||
CONFIG_PUBLIC_KEY1="../../../Tools/test_keys/rsa2048.pub"
|
||||
@@ -82,6 +82,8 @@ CONFIG_CDCACM_RXBUFSIZE=600
|
||||
CONFIG_CDCACM_TXBUFSIZE=12000
|
||||
CONFIG_CDCACM_VENDORID=0x3185
|
||||
CONFIG_CDCACM_VENDORSTR="ARK"
|
||||
CONFIG_CRYPTO=y
|
||||
CONFIG_CRYPTO_RANDOM_POOL=y
|
||||
CONFIG_DEBUG_FULLOPT=y
|
||||
CONFIG_DEBUG_HARDFAULT_ALERT=y
|
||||
CONFIG_DEBUG_MEMFAULT=y
|
||||
|
||||
@@ -31,6 +31,7 @@ CONFIG_DRIVERS_POWER_MONITOR_INA226=y
|
||||
CONFIG_DRIVERS_PWM_INPUT=y
|
||||
CONFIG_DRIVERS_PWM_OUT=y
|
||||
CONFIG_DRIVERS_PX4IO=y
|
||||
CONFIG_DRIVERS_HEATER=y
|
||||
CONFIG_DRIVERS_SMART_BATTERY_BATMON=y
|
||||
CONFIG_COMMON_TELEMETRY=y
|
||||
CONFIG_DRIVERS_TONE_ALARM=y
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
#define PX4IO_SERIAL_CLOCK STM32_PCLK2_FREQUENCY
|
||||
#define PX4IO_SERIAL_BITRATE 1500000 /* 1.5Mbps -> max rate for IO */
|
||||
|
||||
#define PX4IO_HEATER_ENABLED
|
||||
|
||||
/* LEDs */
|
||||
#define GPIO_nLED_AMBER /* PE12 */ (GPIO_OUTPUT|GPIO_OPENDRAIN|GPIO_SPEED_50MHz|GPIO_OUTPUT_SET|GPIO_PORTE|GPIO_PIN12)
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ CONFIG_DRIVERS_POWER_MONITOR_INA226=y
|
||||
CONFIG_DRIVERS_PWM_INPUT=y
|
||||
CONFIG_DRIVERS_PWM_OUT=y
|
||||
CONFIG_DRIVERS_PX4IO=y
|
||||
CONFIG_DRIVERS_HEATER=y
|
||||
CONFIG_DRIVERS_SMART_BATTERY_BATMON=y
|
||||
CONFIG_COMMON_TELEMETRY=y
|
||||
CONFIG_DRIVERS_TONE_ALARM=y
|
||||
|
||||
@@ -12,6 +12,4 @@ param set-default BAT2_A_PER_V 17
|
||||
# Disable IMU thermal control
|
||||
param set-default SENS_EN_THERMAL 0
|
||||
|
||||
param set-default -s SENS_TEMP_ID 2621474
|
||||
|
||||
set IOFW "/etc/extras/cubepilot_io-v2_default.bin"
|
||||
|
||||
@@ -17,6 +17,9 @@ ms5611 -s -b 4 start
|
||||
|
||||
if icm42688p -s -b 4 -R 10 -q start -c 15
|
||||
then
|
||||
# We need to use the temperature of the first isolated IMU for heater control.
|
||||
param set-default SENS_TEMP_ID 2490402
|
||||
|
||||
if ! icm20948 -s -b 4 -R 10 -M -q start
|
||||
then
|
||||
icm42688p -s -b 4 -R 6 start -c 13
|
||||
@@ -24,6 +27,8 @@ then
|
||||
else
|
||||
icm45686 -s -b 4 -R 10 start -c 15
|
||||
icm45686 -s -b 4 -R 6 start -c 13
|
||||
|
||||
param set-default SENS_TEMP_ID 3407906
|
||||
fi
|
||||
|
||||
# SPI1, body-fixed
|
||||
@@ -33,7 +38,6 @@ then
|
||||
ak09916 start -I -R 13
|
||||
else
|
||||
icm20649 -s -b 1 start
|
||||
|
||||
fi
|
||||
|
||||
ms5611 -s -b 1 start
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
#define PX4IO_SERIAL_CLOCK STM32_PCLK2_FREQUENCY
|
||||
#define PX4IO_SERIAL_BITRATE 1500000 /* 1.5Mbps -> max rate for IO */
|
||||
|
||||
#define PX4IO_HEATER_ENABLED
|
||||
|
||||
/* LEDs */
|
||||
#define GPIO_nLED_AMBER /* PE12 */ (GPIO_OUTPUT|GPIO_OPENDRAIN|GPIO_SPEED_50MHz|GPIO_OUTPUT_SET|GPIO_PORTE|GPIO_PIN12)
|
||||
|
||||
|
||||
@@ -62,6 +62,10 @@ param select /data/px4/param/parameters
|
||||
# Load in all of the parameters that have been saved in the file
|
||||
param load
|
||||
|
||||
# This was the default pre-v1.16.0 and for folks relying on that
|
||||
# we set it up here
|
||||
param set-default EKF2_EV_CTRL 15
|
||||
|
||||
# IMU (accelerometer / gyroscope)
|
||||
if [ "$PLATFORM" == "M0104" ]; then
|
||||
/bin/echo "Starting IMU driver with rotation 12"
|
||||
|
||||
@@ -81,15 +81,19 @@ export default defineConfig({
|
||||
? ({ filePath, frontmatter }) => {
|
||||
if (frontmatter.newEditLink) {
|
||||
//newEditLink defines a frontmatter key you can use to append a path to main
|
||||
return `https://github.com/PX4/PX4-Autopilot/edit/main/${frontmatter.newEditLink}`;
|
||||
return `https://github.com/PX4/PX4-Autopilot/edit/main/docs/${frontmatter.newEditLink}`;
|
||||
} else {
|
||||
return `https://github.com/PX4/PX4-Autopilot/edit/main/${filePath}`;
|
||||
return `https://github.com/PX4/PX4-Autopilot/edit/main/docs/${filePath}`;
|
||||
}
|
||||
}
|
||||
: (c) =>
|
||||
`${
|
||||
window.location.origin
|
||||
}/__open-in-editor?file=${encodeURIComponent(c.filePath)}`,
|
||||
}/__open-in-editor?file=${encodeURIComponent(
|
||||
c.frontmatter.newEditLink
|
||||
? c.frontmatter.newEditLink
|
||||
: c.filePath
|
||||
)}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 463 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 239 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 450 KiB |
+5
-3
@@ -165,7 +165,7 @@
|
||||
- [AirMind MindPX](flight_controller/mindpx.md)
|
||||
- [AirMind MindRacer](flight_controller/mindracer.md)
|
||||
- [ARK Electronics ARKV6X](flight_controller/ark_v6x.md)
|
||||
- [ARK FPV Flight Controller](flight_controller/ark_fpv.md)
|
||||
- [ARK FPV Flight Controller](flight_controller/ark_fpv.md)
|
||||
- [ARK Pi6X Flow Flight Controller](flight_controller/ark_pi6x.md)
|
||||
- [CUAV X7](flight_controller/cuav_x7.md)
|
||||
- [CUAV Nora](flight_controller/cuav_nora.md)
|
||||
@@ -253,7 +253,7 @@
|
||||
- [GNSS (GPS)](gps_compass/index.md)
|
||||
- [ARK GPS (CAN)](dronecan/ark_gps.md)
|
||||
- [ARK SAM GPS](gps_compass/ark_sam_gps.md)
|
||||
- [ARK TESEO GPS](dronecan/ark_teseo_gps.md)
|
||||
- [ARK TESEO GPS](dronecan/ark_teseo_gps.md)
|
||||
- [CUAV NEO 3 GPS](gps_compass/gps_cuav_neo_3.md)
|
||||
- [CUAV NEO 3 Pro GPS (CAN)](gps_compass/gps_cuav_neo_3pro.md)
|
||||
- [CUAV NEO 3X GPS (CAN)](gps_compass/gps_cuav_neo_3x.md)
|
||||
@@ -326,7 +326,9 @@
|
||||
- [ARK Electron Microhard Serial Telemetry Radio](telemetry/ark_microhard_serial.md)
|
||||
- [Holybro Microhard P900 Telemetry Radio](telemetry/holybro_microhard_p900_radio.md)
|
||||
- [CUAV P8 Telemetry Radio](telemetry/cuav_p8_radio.md)
|
||||
- [J.Fi Wireless Telemetry Module](telemetry/jfi_telemetry.md)
|
||||
- [HolyBro XBP9X - Discontinued](telemetry/holybro_xbp9x_radio.md)
|
||||
|
||||
- [FrSky Telemetry](peripherals/frsky_telemetry.md)
|
||||
- [TBS Crossfire (CRSF) Telemetry](telemetry/crsf_telemetry.md)
|
||||
- [Satellite Comms (Iridium/RockBlock)](advanced_features/satcom_roadblock.md)
|
||||
@@ -838,4 +840,4 @@
|
||||
- [1.15 (stable)](releases/1.15.md)
|
||||
- [1.14](releases/1.14.md)
|
||||
- [1.13](releases/1.13.md)
|
||||
- [1.12](releases/1.12.md)
|
||||
- [1.12](releases/1.12.md)
|
||||
|
||||
@@ -43,7 +43,9 @@ The gimbal can be connected to _any free serial port_ using the instructions in
|
||||
For example, if the `TELEM2` port on the flight controller is unused you can connect it to the gimbal and set the following PX4 parameters:
|
||||
|
||||
- [MAV_1_CONFIG](../advanced_config/parameter_reference.md#MAV_1_CONFIG) to **TELEM2** (if `MAV_1_CONFIG` is already used for a companion computer (say), use `MAV_2_CONFIG`).
|
||||
- [MAV_1_MODE](../advanced_config/parameter_reference.md#MAV_1_MODE) to **NORMAL**
|
||||
- [MAV_1_MODE](../advanced_config/parameter_reference.md#MAV_1_MODE) to **Gimbal**
|
||||
- [MAV_1_FLOW_CTRL](../advanced_config/parameter_reference.md#MAV_1_FLOW_CTRL) to **Off (0)** (very few gimbals will have RST/CST wires connected).
|
||||
- [MAV_1_FORWARD](../advanced_config/parameter_reference.md#MAV_1_FORWARD) to **Enabled** (Note strictly necessary as forwarding is enabled when `MAV_1_MODE` is set to Gimbal).
|
||||
- [SER_TEL2_BAUD](../advanced_config/parameter_reference.md#SER_TEL2_BAUD) to manufacturer recommended baud rate.
|
||||
|
||||
### Multiple Gimbal Support
|
||||
|
||||
@@ -127,6 +127,20 @@ Additional notes:
|
||||
|
||||
- Whether tuning is applied while flying or after landing can be [configured using parameters](#apply-tuning-when-in-air-landed).
|
||||
|
||||
<div v-if="$frontmatter.frame === 'Multicopter'">
|
||||
|
||||
## Autotuning Large Vehicles
|
||||
|
||||
For big multicopter vehicles you may need to increase the desired raise time of the step response [MC_AT_RISE_TIME](../advanced_config/parameter_reference.md#MC_AT_RISE_TIME).
|
||||
This requires some trial and error as an appropriate rise time depends on both vehicle size and the rotor response.
|
||||
|
||||
Note that if there are slow oscillations in pretuning it may mean that the attitude loop is too fast compared to the rate loop.
|
||||
In this case you should troubleshoot as described in [Drone oscillates when performing the pre-tuning test](#drone-oscillates-when-performing-the-pre-tuning-test).
|
||||
|
||||
<!-- Fixed wing rise time is hardcoded (it's lower than on multirotors and is probably enough for most platforms). -->
|
||||
|
||||
</div>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Drone oscillates when performing the pre-tuning test
|
||||
|
||||
@@ -146,6 +146,12 @@ The settings and underlying parameters are shown below.
|
||||
| Data Link Loss Timeout | [COM_DL_LOSS_T](../advanced_config/parameter_reference.md#COM_DL_LOSS_T) | Amount of time after losing the data connection before the failsafe will trigger. |
|
||||
| Failsafe Action | [NAV_DLL_ACT](../advanced_config/parameter_reference.md#NAV_DLL_ACT) | Disabled, Hold mode, Return mode, Land mode, Disarm, Terminate. |
|
||||
|
||||
The following settings also apply, but are not displayed in the QGC UI.
|
||||
|
||||
| Setting | Parameter | Description |
|
||||
| ----------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| <a id="COM_DLL_EXCEPT"></a>Mode exceptions for DLL failsafe | [COM_DLL_EXCEPT](../advanced_config/parameter_reference.md#COM_DLL_EXCEPT) | Set modes where DL loss will not trigger a failsafe. |
|
||||
|
||||
## Geofence Failsafe
|
||||
|
||||
The _Geofence Failsafe_ is triggered when the drone breaches a "virtual" perimeter.
|
||||
|
||||
+51
-22
@@ -15,7 +15,7 @@ Simple changes to _existing content_ can be made by clicking the **Edit on GitHu
|
||||
|
||||

|
||||
|
||||
To edit an existing page:
|
||||
To edit an existing English page:
|
||||
|
||||
1. Open the page.
|
||||
1. Click the **Edit on GitHub** link below the page content.
|
||||
@@ -24,49 +24,62 @@ To edit an existing page:
|
||||
|
||||
The documentation team will review the request and either merge it or work with you to update it.
|
||||
|
||||
## Changes using Git (New Pages and Images)
|
||||
Note that you can only make changes to the English version directly in the source.
|
||||
[Translations are handled in Crowdin](../contribute/translation.md).
|
||||
|
||||
## Changes using Git
|
||||
|
||||
More substantial changes, including adding new pages or adding/modifying images, aren't as easy to make (or properly test) on Github.
|
||||
|
||||
For these kinds of changes we suggest using the same approach as for _code_:
|
||||
|
||||
1. Use the _git_ toolchain to get the documentation source code onto your local computer.
|
||||
1. Use the _git_ toolchain to get the PX4 source code onto your local computer.
|
||||
1. Modify the documentation as needed (add, change, delete).
|
||||
1. _Test_ that it builds properly using Vitepress.
|
||||
1. Create a branch for your changes and create a pull request (PR) to pull it back into the documentation.
|
||||
1. Create a branch for your changes and create a pull request (PR) to pull it back into the [PX4-Autopilot](https://github.com/PX4/PX4-Autopilot.git) repo.
|
||||
|
||||
The following explain how to get the source code, build locally (to test), and modify the code.
|
||||
|
||||
### Get/Push Documentation Source Code
|
||||
### Get Documentation Source Code
|
||||
|
||||
Documentation sources are in the [PX4-Autopilot](https://github.com/PX4/PX4-Autopilot/) repo, alongside all the other PX4 source code.
|
||||
The sources are markdown files located the [/docs](https://github.com/PX4/PX4-Autopilot/tree/main/docs) subdirectory.
|
||||
The English source files are in the [/docs/en/](https://github.com/PX4/PX4-Autopilot/tree/main/docs/en) subdirectory and can be edited directly.
|
||||
[Translation](../contribute/translation.md) sources are in language specific subdirectories, such as `ko` for korean and `zh` for Chinese: these are edited via the Crowdin tool, and should not be edited directly.
|
||||
|
||||
::: tip
|
||||
If you already have a clone of the [PX4-Autopilot](https://github.com/PX4/PX4-Autopilot/) you can ignore this section.
|
||||
:::
|
||||
|
||||
To get the library(s) sources onto your local computer you will need to use the git toolchain.
|
||||
The instructions below explain how to get git and use it on your local computer.
|
||||
|
||||
1. Download git for your computer from [https://git-scm.com/downloads](https://git-scm.com/downloads)
|
||||
1. [Sign up](https://github.com/join) for Github if you haven't already
|
||||
1. Create a copy (Fork) of the [PX4 User Guide repo](https://github.com/PX4/PX4-user_guide) on Github ([instructions here](https://docs.github.com/en/get-started/quickstart/fork-a-repo)).
|
||||
1. Create a copy (Fork) of the [PX4-Autopilot repo](https://github.com/PX4/PX4-Autopilot) on Github ([instructions here](https://docs.github.com/en/get-started/quickstart/fork-a-repo)).
|
||||
1. Clone (copy) your forked repository to your local computer:
|
||||
|
||||
```sh
|
||||
cd ~/wherever/
|
||||
git clone https://github.com/<your git name>/PX4-user_guide.git
|
||||
git clone https://github.com/<your git name>/PX4-Autopilot.git
|
||||
```
|
||||
|
||||
For example, to clone the PX4 userguide fork for a user with Github account "john_citizen":
|
||||
For example, to clone PX4 source fork for a user with Github account "john_citizen":
|
||||
|
||||
```sh
|
||||
git clone https://github.com/john_citizen/PX4-user_guide.git
|
||||
git clone https://github.com/john_citizen/PX4-Autopilot.git
|
||||
```
|
||||
|
||||
1. Navigate to your local repository:
|
||||
|
||||
```sh
|
||||
cd ~/wherever/PX4-user_guide
|
||||
cd ~/wherever/PX4-Autopilot
|
||||
```
|
||||
|
||||
1. Add a _remote_ called "upstream" to point to the PX4 version of the library:
|
||||
1. Add a _remote_ called "upstream" to point to the "official" PX4 version of the library:
|
||||
|
||||
```sh
|
||||
git remote add upstream https://github.com/PX4/PX4-user_guide.git
|
||||
git remote add upstream https://github.com/PX4/PX4-Autopilot.git
|
||||
```
|
||||
|
||||
:::tip
|
||||
@@ -75,7 +88,19 @@ The instructions below explain how to get git and use it on your local computer.
|
||||
Above you create a new remote _upstream_ that points to the PX4 project version of the documents.
|
||||
:::
|
||||
|
||||
1. Create a branch for your changes:
|
||||
### Make/Push Documentation Changes
|
||||
|
||||
Within the repository you created above:
|
||||
|
||||
1. Bring your copy of the repository `main` branch up to date:
|
||||
|
||||
```sh
|
||||
git checkout main
|
||||
git fetch upstream main
|
||||
git pull upstream main
|
||||
```
|
||||
|
||||
2. Create a new branch for your changes:
|
||||
|
||||
```sh
|
||||
git checkout -b <your_feature_branch_name>
|
||||
@@ -83,8 +108,8 @@ The instructions below explain how to get git and use it on your local computer.
|
||||
|
||||
This creates a local branch on your computer named `your_feature_branch_name`.
|
||||
|
||||
1. Make changes to the documentation as needed (general guidance on this in following sections)
|
||||
1. Once you are satisfied with your changes, you can add them to your local branch using a "commit":
|
||||
3. Make changes to the documentation as needed (general guidance on this in following sections)
|
||||
4. Once you are satisfied with your changes, you can add them to your local branch using a "commit":
|
||||
|
||||
```sh
|
||||
git add <file name>
|
||||
@@ -93,21 +118,25 @@ The instructions below explain how to get git and use it on your local computer.
|
||||
|
||||
For a good commit message, please refer to the [Source Code Management](../contribute/code.md#commits-and-commit-messages) section.
|
||||
|
||||
1. Push your local branch (including commits added to it) to your forked repository on Github.
|
||||
5. Push your local branch (including commits added to it) to your forked repository on Github.
|
||||
|
||||
```sh
|
||||
git push origin your_feature_branch_name
|
||||
```
|
||||
|
||||
1. Go to your forked repository on Github in a web browser, e.g.: `https://github.com/<your git name>/PX4-user_guide.git`.
|
||||
6. Go to your forked repository on Github in a web browser, e.g.: `https://github.com/<your git name>/PX4-Autopilot.git`.
|
||||
There you should see the message that a new branch has been pushed to your forked repository.
|
||||
1. Create a pull request (PR):
|
||||
7. Create a pull request (PR):
|
||||
|
||||
- On the right hand side of the "new branch message" (see one step before), you should see a green button saying "Compare & Create Pull Request".
|
||||
Press it.
|
||||
- A pull request template will be created.
|
||||
It will list your commits and you can (must) add a meaningful title (in case of a one commit PR, it's usually the commit message) and message (<span style="color:orange">explain what you did for what reason</span>.
|
||||
Check [other pull requests](https://github.com/PX4/PX4-user_guide/pulls) for comparison)
|
||||
1. You're done!
|
||||
Check [other pull requests](https://github.com/PX4/PX4-Autopilot/pulls) for comparison).
|
||||
- Add the "Documentation" label.
|
||||
|
||||
8. You're done!
|
||||
|
||||
Maintainers for the PX4 User Guide will now have a look at your contribution and decide if they want to integrate it.
|
||||
Check if they have questions on your changes every once in a while.
|
||||
|
||||
@@ -120,10 +149,10 @@ Build the library locally to test that any changes you have made have rendered p
|
||||
- [Nodejs 18+](https://nodejs.org/en)
|
||||
- [Yarn classic](https://classic.yarnpkg.com/en/docs/install)
|
||||
|
||||
1. Navigate to your local repository:
|
||||
1. Navigate to your local repository and the `/docs` subdirectory:
|
||||
|
||||
```sh
|
||||
cd ~/wherever/PX4-user_guide
|
||||
cd ~/wherever/PX4-Autopilot/docs
|
||||
```
|
||||
|
||||
1. Install dependencies (including Vitepress):
|
||||
|
||||
@@ -11,3 +11,7 @@ This section provides information about various radio systems that you can use,
|
||||
- [FrSky Telemetry](../peripherals/frsky_telemetry.md) — Telemetry on your (FRSky) RC Receiver
|
||||
- [TBS Crossfire (CRSF) Telemetry](../telemetry/crsf_telemetry.md) — Telemetry on your (TBS Crossfire) RC Receiver
|
||||
- [Satellite Comms (Iridium/RockBlock)](../advanced_features/satcom_roadblock.md) — High-latency comms via satellite
|
||||
|
||||
## See Also
|
||||
|
||||
- [Safety Configuration > Data Link Loss Failsafe](../config/safety.md#data-link-loss-failsafe)
|
||||
|
||||
@@ -189,67 +189,186 @@ You can now build and test.
|
||||
|
||||
## Download & Decrypt Log Files
|
||||
|
||||
Encrypted log files are downloaded using the QGroundControl [Log Download](https://docs.qgroundcontrol.com/master/en/qgc-user-guide/analyze_view/log_download.html) view (**Analyze Tools > Log Download**) just like ordinary log files.
|
||||
Before you can analyse your logs they must first be downloaded and decrypted.
|
||||
PX4 includes Python scripts in [Tools/log_encryption](https://github.com/PX4/PX4-Autopilot/blob/main/Tools/) that make this process easier:
|
||||
|
||||
Note that the encrypted files will be downloaded with the `.ulg` suffix, instead of `.ulge`.
|
||||
- [download_logs.py](https://github.com/PX4/PX4-Autopilot/blob/main/Tools/log_encryption/download_logs.py): Downloads the logs to `/logs/encrypted`.
|
||||
- [decrypt_logs.py](https://github.com/PX4/PX4-Autopilot/blob/main/Tools/log_encryption/decrypt_logs.py): Decrypts encrypted logs in `/logs/encrypted` to `/logs/decrypted` using a specified (or default) key.
|
||||
|
||||
The following sections show how these are used.
|
||||
|
||||
### Download Log Files
|
||||
|
||||
The easiest way to download the files is to use [download_logs.py](https://github.com/PX4/PX4-Autopilot/blob/main/Tools/log_encryption/download_logs.py).
|
||||
This takes a single argument that sets the serial or UDP MAVLink connection to the device as shown below (adjust parameters as needed):
|
||||
|
||||
- UDP connection
|
||||
|
||||
```sh
|
||||
cd PX4-Autopilot/Tools/log_encryption
|
||||
python3 download_logs.py udp:0.0.0.0:14550
|
||||
```
|
||||
|
||||
- USB serial port on Linux
|
||||
|
||||
```sh
|
||||
cd PX4-Autopilot/Tools/log_encryption
|
||||
python3 download_logs.py /dev/ttyACM0 --baudrate 57600
|
||||
```
|
||||
|
||||
The files are downloaded to `/logs/encrypted`, which is the location expected by the decryption script.
|
||||
|
||||
::: info
|
||||
Encrypted log files can also be downloaded manually using the QGroundControl [Log Download](https://docs.qgroundcontrol.com/master/en/qgc-user-guide/analyze_view/log_download.html) view (**Analyze Tools > Log Download**) just like ordinary log files.
|
||||
|
||||
Note that in this case you will need to copy the files to `/logs/encrypted` and rename them to the `.ulge` suffix (they are downloaded with the `.ulg` suffix)
|
||||
:::
|
||||
|
||||
### Decrypt ULogs
|
||||
|
||||
Before you can analyze your encrypted logs, you will need to decrypt them.
|
||||
There is a Python script that can be used to decrypt logs in `Tools/decrypt_ulog.py`.
|
||||
By default, the [decrypt_logs.py](https://github.com/PX4/PX4-Autopilot/blob/main/Tools/log_encryption/decrypt_logs.py) script decrypts encrypted logs in `/logs/encrypted` using the private key in `keys/private/private_key.pem`, and generates the unencrypted logs in `/logs/decrypted`.
|
||||
|
||||
When decrypting a `.ulge` file the script takes 3 arguments:
|
||||
|
||||
1. The encrypted log file.
|
||||
2. An empty string `''`.
|
||||
3. The decryption key (the RSA2048 `.pem` private key which is used to unwrap the symmetric key).
|
||||
|
||||
For example:
|
||||
Navigate into the `Tools/log_encryption` folder and run the tool as shown below:
|
||||
|
||||
```sh
|
||||
python3 decrypt_ulog.py \
|
||||
/home/john/Downloads/log_24_2024-10-6-23-39-50.ulg '' \
|
||||
new_keys/private_key.pem
|
||||
cd PX4-Autopilot/Tools/log_encryption
|
||||
python3 decrypt_logs.py
|
||||
```
|
||||
|
||||
On success the decrypted log file is created with the `.ulog` suffix.
|
||||
|
||||
::: info
|
||||
The script can be used with both `.ulge` logs and the `.ulgc`/`.ulgk` files used in [PX4 v1.15 Log Encryption](https://docs.px4.io/v1.15/en/dev_log/log_encryption.html).
|
||||
The full command line syntax is given below:
|
||||
On success the decrypted logs can be found in the decrypted folder.
|
||||
|
||||
```sh
|
||||
usage: decrypt_ulog.py [-h] [ulog_file] [ulog_key] [rsa_key]
|
||||
PX4-Autopilot/logs/decrypted
|
||||
```
|
||||
|
||||
CLI tool to decrypt an ulog file
|
||||
The expected folder structure showing the location of encrypted logs, decrypted logs and the default private key is shown below:
|
||||
|
||||
positional arguments:
|
||||
ulog_file .ulge/.ulgc, encrypted log file
|
||||
ulog_key .ulgk, legacy encrypted key (give empty string '' to ignore for .ulge)
|
||||
rsa_key .pem format key for decrypting the ulog key
|
||||
```sh
|
||||
PX4-Autopilot/
|
||||
│
|
||||
├── logs/ # Main directory for logs
|
||||
│ ├── encrypted/ # Stores encrypted logs (.ulge)
|
||||
│ │ ├── log-YYYY-MM-DD_HH-MM-SS_ID.ulge # Encrypted logs
|
||||
│ │
|
||||
│ ├── decrypted/
|
||||
│ │ ├── log-YYYY-MM-DD_HH-MM-SS_ID.ulg # Regular PX4 logs
|
||||
|
|
||||
├── keys/ # Main directory for keys
|
||||
├── private/ # Stores private keys
|
||||
├── private_key.pem # RSA private key (2048-bit)
|
||||
```
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
::: tip
|
||||
The script also allows you to specify a particular key and/or to specify a particular file or folder to be decrypted using optional positional arguments:
|
||||
|
||||
```sh
|
||||
python3 decrypt_logs.py ["" | custom_key] [log_file.ulge | log_folder]
|
||||
```
|
||||
|
||||
The full set of command options are shown below:
|
||||
|
||||
```sh
|
||||
# Default key + default folder
|
||||
python3 decrypt_logs.py
|
||||
|
||||
# Specific key + default folder
|
||||
python3 decrypt_logs.py path/to/private_key.pem
|
||||
|
||||
# Specific key + specific file
|
||||
python3 decrypt_logs.py path/to/private_key.pem path/to/log_file.ulge
|
||||
|
||||
# Specific key + specific folder
|
||||
python3 decrypt_logs.py path/to/private_key.pem path/to/log_folder
|
||||
|
||||
# Default key + specific file
|
||||
python3 decrypt_logs.py "" path/to/log_file.ulge
|
||||
|
||||
# Default key + specific folder
|
||||
python3 decrypt_logs.py "" path/to/log_folder
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Generate RSA Public & Private Keys
|
||||
|
||||
To generate a RSA2048 private and public key, you can use OpenSSL:
|
||||
The [Tools/log_encryption/generate_keys.py](https://github.com/PX4/PX4-Autopilot/blob/main/Tools/log_encryption/generate_keys.py) script can be used to generate the public key that is used on the device for encryption, and the private key that is used on the computer as part of log decryption.
|
||||
|
||||
::: details The script depends on OpenSSL.
|
||||
|
||||
Run the following command to check if OpenSSL is present:
|
||||
|
||||
```sh
|
||||
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
|
||||
openssl version
|
||||
```
|
||||
|
||||
Then you can create a public key from this private key:
|
||||
If there is no output you can install OpenSSL as shown below:
|
||||
|
||||
- Ubuntu/Debian
|
||||
|
||||
```sh
|
||||
sudo apt update
|
||||
sudo apt install openssl
|
||||
```
|
||||
|
||||
- macOS
|
||||
|
||||
```sh
|
||||
brew install openssl
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The script is used as shown:
|
||||
|
||||
```sh
|
||||
# Convert private_key.pem to a DER file
|
||||
openssl rsa -pubout -in private_key.pem -outform DER -out public_key.der
|
||||
# From the DER file, generate a public key in hex format, separated by commas
|
||||
xxd -p public_key.der | tr -d '\n' | sed 's/\(..\)/0x\1, /g' > public_key.pub
|
||||
cd PX4-Autopilot/Tools/log_encryption
|
||||
python3 generate_keys.py
|
||||
```
|
||||
|
||||
To use this key you would modify your `.px4board` file to point `CONFIG_PUBLIC_KEY1` to the file location of `public_key.pub`.
|
||||
The private key generated should be stored safely and used when you need to decrypt log files.
|
||||
The private and public key will be generated into the folder structure below.
|
||||
The private key should be stored safely and used when you need to [decrypt log files](#decrypt-ulogs).
|
||||
|
||||
```sh
|
||||
PX4-Autopilot/
|
||||
│
|
||||
├── keys/ # Main directory for keys
|
||||
│ ├── private/ # Stores private keys
|
||||
│ │ ├── private_key.pem # RSA private key (2048-bit)
|
||||
│ │
|
||||
│ ├── public/ # Stores public keys
|
||||
│ │ ├── public_key.der # Public key in DER format
|
||||
│ │ ├── public_key.pub # Public key in hex format
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The script will not overwrite any existing keys in the folders.
|
||||
It will generate a new public key if the folder only includes a private key.
|
||||
- The public key is created with the default name and location expected by the toolchain when building PX4 (i.e. in `CONFIG_PUBLIC_KEY1`), and the private key is created in the default location expected by the script we use for [decrypting ulogs](#decrypt-ulogs).
|
||||
|
||||
### Manual Key Generation
|
||||
|
||||
This section explains how you might manually run the same steps as the script (should you so wish):
|
||||
|
||||
1. First install OpenSSL, as described in the previous section.
|
||||
2. Use OpenSSL to generate a RSA2048 private and public key:
|
||||
|
||||
```sh
|
||||
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
|
||||
```
|
||||
|
||||
3. Create a public key from this private key:
|
||||
|
||||
```sh
|
||||
# Convert private_key.pem to a DER file
|
||||
openssl rsa -pubout -in private_key.pem -outform DER -out public_key.der
|
||||
# From the DER file, generate a public key in hex format, separated by commas
|
||||
xxd -p public_key.der | tr -d '\n' | sed 's/\(..\)/0x\1, /g' > public_key.pub
|
||||
```
|
||||
|
||||
4. Copy the keys into the appropriate locations expected by the rest of the toolchain (as shown in previous section).
|
||||
5. To use this key, modify your `.px4board` file to point `CONFIG_PUBLIC_KEY1` to the file location of `public_key.pub`.
|
||||
|
||||
```sh
|
||||
CONFIG_PUBLIC_KEY1="../../../keys/public/public_key.pub"
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MAVLink Peripherals (GCS/OSD/Companion)
|
||||
# MAVLink Peripherals (GCS/OSD/Gimbal/Camera/Companion)
|
||||
|
||||
Ground Control Stations (GCS), On-Screen Displays (OSD), Companion Computers, ADS-B receivers, and other MAVLink peripherals interact with PX4 using separate MAVLink streams, sent via different serial ports.
|
||||
Ground Control Stations (GCS), On-Screen Displays (OSD), MAVLink Cameras & Gimbals, Remote IDs, Companion Computers, ADS-B receivers, and other MAVLink peripherals interact with PX4 using separate MAVLink streams, sent via different serial ports.
|
||||
|
||||
In order to configure that a particular serial port is used for MAVLink traffic with a particular peripheral, we use [Serial Port Configuration](../peripherals/serial_configuration.md), assigning one of the abstract "MAVLink instance" configuration parameters to the desired port.
|
||||
We then set other properties of the MAVLink channel using the parameters associated with our selected MAVLink instance, so that they match the requirements of our particular peripheral.
|
||||
@@ -36,8 +36,10 @@ The parameters for each instance are:
|
||||
- _OSD_: Standard set of messages for an OSD system.
|
||||
- _Config_: Standard set of messages and rate configuration for a fast link (e.g. USB).
|
||||
- _Minimal_: Minimal set of messages for use with a GCS connected on a high latency link.
|
||||
- _ExtVision_ or _ExtVisionMin_: Messages for offboard vision systems (ExtVision needed for VIO).
|
||||
- _Iridium_: Messages for an [Iridium satellite communication system](../advanced_features/satcom_roadblock.md).
|
||||
- _External Vision_: Messages for offboard vision systems.
|
||||
- _Gimbal_: Messages for a gimbal. Note this also enables [message forwarding](#MAV_X_FORWARD)
|
||||
- _Onboard Low Bandwidth_: Standard set of messages for a companion computer connected on a lower speed link.
|
||||
- _uAvionix_: Messages for a uAvionix ADS-B beacon.
|
||||
|
||||
::: info
|
||||
If you need to find the specific set of message for each mode search for `MAVLINK_MODE_` in [/src/modules/mavlink/mavlink_main.cpp](https://github.com/PX4/PX4-Autopilot/blob/main/src/modules/mavlink/mavlink_main.cpp).
|
||||
@@ -64,9 +66,7 @@ You will need to reboot PX4 to make the parameter available (i.e. in QGroundCont
|
||||
The parameter used will depend on the [assigned serial port](../advanced_config/parameter_reference.md#serial) - for example: `SER_GPS1_BAUD`, `SER_TEL2_BAUD`, etc.
|
||||
The value you use will depend on the type of connection and the capabilities of the connected MAVLink peripheral.
|
||||
|
||||
<a id="default_ports"></a>
|
||||
|
||||
## Default MAVLink Ports
|
||||
## Default MAVLink Ports {#default_ports}
|
||||
|
||||
### TELEM1
|
||||
|
||||
@@ -108,8 +108,16 @@ On this hardware, there is a [default serial port mapping](../peripherals/serial
|
||||
|
||||
For more information see: [PX4 Ethernet Setup](../advanced_config/ethernet_setup.md)
|
||||
|
||||
## Device Specific Setup
|
||||
|
||||
Links to setup instructions for specific MAVLink components:
|
||||
|
||||
- [MAVLink Cameras (Camera Protocol v2) > PX4 Configuration](../camera/mavlink_v2_camera.md#px4-configuration)
|
||||
- [Gimbal Configuration > MAVLink Gimbal (MNT_MODE_OUT=MAVLINK)](../advanced/gimbal_control.md#mavlink-gimbal-mnt-mode-out-mavlink)
|
||||
|
||||
## See Also
|
||||
|
||||
- [Serial Port Configuration](../peripherals/serial_configuration.md)
|
||||
- [PX4 Ethernet Setup > PX4 MAVLink Serial Port Configuration](../advanced_config/ethernet_setup.md#px4-mavlink-serial-port-configuration)
|
||||
- [Serial Port Mapping](../hardware/serial_port_mapping.md)
|
||||
|
||||
|
||||
@@ -10,13 +10,14 @@ PX4 supports a number of types of telemetry radios:
|
||||
- <del>_HKPilot Telemetry Radio_</del> (Discontinued)
|
||||
- <del>_3DR Telemetry Radio_</del> (Discontinued)
|
||||
- [Telemetry Wifi](../telemetry/telemetry_wifi.md)
|
||||
- [J.Fi Wireless Telemetry Module](../telemetry/jfi_telemetry.md)
|
||||
- [Microhard Serial Telemetry Radio](../telemetry/microhard_serial.md)
|
||||
- [ARK Electron Microhard Serial Telemetry Radio](../telemetry/ark_microhard_serial.md)
|
||||
- [Holybro Microhard P900 Telemetry Radio](../telemetry/holybro_microhard_p900_radio.md)
|
||||
- CUAV Serial Telemetry Radio
|
||||
- [CUAV P8 Telemetry Radio](../telemetry/cuav_p8_radio.md)
|
||||
- XBee Serial Telemetry Radio
|
||||
- [HolyBro XBP9X Telemetry Radio](../telemetry/holybro_xbp9x_radio.md) (Discontinued)
|
||||
- <del>[HolyBro XBP9X Telemetry Radio](../telemetry/holybro_xbp9x_radio.md)</del> (Discontinued)
|
||||
|
||||
PX4 is protocol compatible with [SiK Radio](../telemetry/sik_radio.md) and will generally work out of the box (though you may need to change/use an appropriate connector).
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# J.Fi Wireless Telemetry Module
|
||||
|
||||
The J.MARPLE [J.Fi telemetry module](https://jmarple.ai/j-fi/) is a compact and lightweight wireless communication device featuring a PCB-integrated antenna or external antenna, enabling seamless telemetry connections between various drone flight controllers (FC) and ground control stations.
|
||||
|
||||
This module includes a Pixhawk-standard JST 6-pin `TELEM` connector, ensuring compatibility with all PX4-based flight controllers.
|
||||
It supports quick plug-and-play operation to `TELEM1` with default settings, requiring no additional configuration.
|
||||
|
||||
The J.Fi telemetry module provides reliable communication up to approximately 500 meters when using a PCB-integrated antenna.
|
||||
Operating in the 2.4GHz frequency band, it allows unrestricted global use without regulatory limitations.
|
||||
|
||||

|
||||
|
||||
## Where to Buy
|
||||
|
||||
- [https://jmarple.ai/j-fi/](https://jmarple.ai/j-fi/)
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
### Wireless Performance
|
||||
|
||||
- **Frequency Band:** 2.4GHz
|
||||
- **Speed:** Up to 11 Mbps (adjustable)
|
||||
- **Range:** Up to 500 meters (varies upon environments)
|
||||
- **Payload Capacity:** Up to 1400 bytes
|
||||
|
||||
### Network Schemes
|
||||
|
||||
- **Supported Topologies:** 1:1, 1:N, N:N
|
||||
- **Collision Management:** Time Slot-Based Response Delay
|
||||
|
||||
### User-Friendly Features
|
||||
|
||||
- **Buttons:** Pairing and Mode Switching
|
||||
- **LED Indicators:** Real-time status updates
|
||||
- **Configuration:** Web browser-based setup
|
||||
- **Micro USB Port for connecting to PC or GCS**
|
||||
|
||||
## Broadcast Communication
|
||||
|
||||
With default settings enabled, the device automatically broadcasts data to all nearby J.Fi devices.
|
||||
Connect your external device or system to the **Broadcast port**.
|
||||
No additional setup is required.
|
||||
|
||||

|
||||
|
||||
## Paired Communication
|
||||
|
||||
- Modules must first undergo an initial pairing procedure.
|
||||
- Once paired, communication is _restricted to paired J.Fi devices_. Connect your external device or system to the **Pair port.**
|
||||
|
||||

|
||||
|
||||
### 1:1 Pairing
|
||||
|
||||

|
||||
|
||||
- On **each device,** press and hold _button A_, then click the _RST button_.
|
||||
Release _button A_ when _LED 1_ blinks.
|
||||
- Both devices will enter pairing mode
|
||||
- Choose one module and double-click _button A_
|
||||
- On the other module, click _button A_ once
|
||||
- On the first module, click _button A_ once again to finish pairing
|
||||
- Pairing complete
|
||||
|
||||
### 1:N Pairing
|
||||
|
||||
- On **each device,** press and hold _button A_, then click the _RST button_.
|
||||
Release _button A_ when _LED 1_ blinks.
|
||||
- All devices will enter pairing mode
|
||||
- **Host module (1):** Double-click _button A_
|
||||
- **Client modules (N):** Click _button A_ once on each module to pair
|
||||
- **Host module (1):** Click _button A_ again to finish pairing
|
||||
- Pairing complete.
|
||||
|
||||
<lite-youtube videoid="CnjhTfvARmw" title="J.Fi Wireless Telemetry Module Pairing Guide"/>
|
||||
|
||||
## PX4 Setup
|
||||
|
||||
PX4 is plug-and-play with J.Fi if connected to the `TELEM1` port, and should connect without further connection.
|
||||
|
||||
If you want to use another port you will need to assign a MAVLink instance to the serial port (see [MAVLink Peripherals (GCS/OSD/Companion)](../peripherals/mavlink_peripherals.md)) (and possibly unassign whatever is currently using the port).
|
||||
|
||||
### One-to-one (1:1) Setups
|
||||
|
||||
The `TELEM1` port is set to use `57600` as the baud rate by default (and J.Fi is set to match).
|
||||
This default baud rate is calibrated for inexpensive low-power telemetry radios.
|
||||
While this should be sufficient for 1:1 setups, J.Fi will work with much higher rates (i.e., `115200`).
|
||||
|
||||
If you want to change the baud rate:
|
||||
|
||||
1. Change [SER_TEL1_BAUD](../advanced_config/parameter_reference.md#SER_TEL1_BAUD) if you're using the `TELEM1` port (see [Serial Port Configuration](../peripherals/serial_configuration.md) for other ports).
|
||||
2. Update the baud rate in the [J.Fi Configuration](#j-fi-configuration) to match.
|
||||
|
||||
### One-to-many (1:N) Setups
|
||||
|
||||
For one-to-many (1:N) setups a higher baud rate is _highly recommended_ to ensure stable data reception.
|
||||
All J.Fi devices should be set to the same baud rate (although communication may work even when when devices use different baud rates).
|
||||
This should be changed in both PX4 and the J.Fi modules as explained in the previous section.
|
||||
|
||||
You will also need to make sure that all vehicles on the MAVLink network are assigned a unique **System ID** ([MAV_SYS_ID](../advanced_config/parameter_reference.md#MAV_SYS_ID)).
|
||||
|
||||

|
||||
|
||||
<lite-youtube videoid="tPeJA2gn7Zw" title="Simultaneous Control using J.Fi Wireless Telemetry Module"/>
|
||||
|
||||
## QGroundControl Configuration
|
||||
|
||||
The J.Fi will connect plug-and-play to **QGroundControl** and automatically connect just like a SiK Radio.
|
||||
|
||||
However if you change the baud rate from 57600 you will need to create and use a new link configuration:
|
||||
|
||||
1. Disable SiK Radio in QGC (**Application Settings → General → AutoConnect**).
|
||||
2. Create a new link configuration:
|
||||
- Go to **Application Settings → Comms Links**.
|
||||
- Click **Add**.
|
||||
- Set **Type** to **Serial**, configure the **Serial Port** and **Baud Rate** to match the J.Fi device.
|
||||
3. Select **Connect** to connect with the new configuration.
|
||||
|
||||
## J.Fi Configuration
|
||||
|
||||
- **Device:** Press and hold _button B_, then click the _RST button_.
|
||||
Release _button B_ when _LED 2_ blinks.
|
||||
- Device enters configuration mode
|
||||
- **Smart device:** Connect to Wi-Fi network named `J.Fi-xxxxxx` (x: alphanumeric characters)
|
||||
- **Browser:** Go to `192.168.4.1` to open the **configuration page**.
|
||||
- **Configuration page:** Adjust settings as needed, then click **Save**
|
||||
- _LED 1_ blinks once upon saving
|
||||
|
||||

|
||||
|
||||
## Further info
|
||||
|
||||
- [User Manual](https://docs.google.com/document/d/1NaVwOLuMCuNpd0uxgilXZ_qfHAnsFgBmaPxX9WGY2h4/edit?usp=sharing)
|
||||
- [ROS Github](https://github.com/SUV-Lab/J-Fi)
|
||||
@@ -158,7 +158,7 @@ Fast oscillations (more than 1 oscillation per second): this is because the gain
|
||||
|
||||
</div>
|
||||
|
||||
### The auto-tuning sequence fails
|
||||
### 자동 튜닝 실패
|
||||
|
||||
If the drone was not moving enough during auto-tuning, the system identification algorithm might have issues to find the correct coefficients.
|
||||
|
||||
@@ -169,7 +169,7 @@ Increase the <div style="display: inline;" v-if="$frontmatter.frame === 'Multico
|
||||
Due to effects not included in the mathematical model such as delays, saturation, slew-rate, airframe flexibility, the loop gain can be too high.
|
||||
To fix this, follow the same steps described [when the drone oscillates in the pre-tuning test](#drone-oscillates-when-performing-the-pre-tuning-test).
|
||||
|
||||
### I still can't get it to work
|
||||
### 여전히 정상 작동하지 않는 경우
|
||||
|
||||
Attempt manual tuning using the guides listed in [See also](#see-also) below.
|
||||
|
||||
@@ -191,7 +191,7 @@ This behaviour can be configured using the [FW_AT_APPLY](../advanced_config/para
|
||||
</div>
|
||||
|
||||
- `0`: the gains are not applied.
|
||||
This is used for testing purposes if the user wants to inspect results of the auto-tuning algorithm without using them directly.
|
||||
자동 튜닝의 결과를 직접적으로 사용하지 않은 체로 검사하는 경우에 사용합니다.
|
||||
- `1`: apply the gains after disarm (default for multirotors).
|
||||
The operator can then test the new tuning while taking-off carefully.
|
||||
- `2`: apply immediately (default for fixed-wings).
|
||||
@@ -225,7 +225,7 @@ Fixed-wing vehicles (only) can select which axes are tuned using the [FW_AT_AXES
|
||||
|
||||
</div>
|
||||
|
||||
## Developers/SDKs
|
||||
## 개발자 SDK
|
||||
|
||||
Autotuning is started using [MAV_CMD_DO_AUTOTUNE_ENABLE](https://mavlink.io/en/messages/common.html#MAV_CMD_DO_AUTOTUNE_ENABLE) MAVLink command.
|
||||
|
||||
@@ -239,7 +239,7 @@ PX4 should stream progress as the protocol does not allow polling.
|
||||
|
||||
The feature is not yet supported by MAVSDK.
|
||||
|
||||
## Background/Detail
|
||||
## 배경 및 세부 사항
|
||||
|
||||
PX4 uses [PID controllers](../flight_stack/controller_diagrams.md) (rate, attitude, velocity, and position) to calculate the outputs required to move a vehicle from its current estimated state to match a desired setpoint.
|
||||
The controllers must be well tuned in order to get the best performance out of a vehicle.
|
||||
@@ -266,20 +266,20 @@ The default behaviour can be configured using [parameters](#optional-configurati
|
||||
|
||||
### 자주 묻는 질문
|
||||
|
||||
#### What frames types are supported?
|
||||
#### 어떤 기체 유형이 지원됩니까?
|
||||
|
||||
Autotuning is enabled for multicopter, fixed-wing, and hybrid VTOL fixed-wing vehicles.
|
||||
|
||||
While it is not yet enabled for other frame types, in theory it an be used with any frame that uses a rate controller.
|
||||
|
||||
#### Does autotuning work for all supported airframes?
|
||||
#### 지원되는 모든 기체에 대해 자동 튜닝이 작동됩니까?
|
||||
|
||||
The mathematical model used by autotuning to estimate the dynamics of the drone assumes this it is a linear system with no coupling between the axes (SISO), and with a limited complexity (2 poles and 2 zeros).
|
||||
If the real drone is too far from those conditions, the model will not be able to represent the real dynamics of the drone.
|
||||
|
||||
In practise, autotuning generally works well for fixed-wing and multicopter, provided the frame is not too flexible.
|
||||
|
||||
#### How long does autotuning take?
|
||||
#### 자동 튜닝은 얼마나 걸립니까?
|
||||
|
||||
Tuning takes 5s-20s per axis (aborted if tuning could not be established in 20s) + 2s pause between each axis + 4s of testing if the new gains are applied in air.
|
||||
|
||||
|
||||
@@ -83,6 +83,22 @@ This is the airspeed which, when reached, will trigger the transition out of mul
|
||||
It is critical that you have properly calibrated your airspeed sensor.
|
||||
It is also important that you pick an airspeed that is comfortably above your airframes stall speed (check `FW_AIRSPD_MIN`) as this is currently not checked.
|
||||
|
||||
#### Openloop Transition Time
|
||||
|
||||
Parameter: [VT_F_TR_OL_TM](../advanced_config/parameter_reference.md#VT_F_TR_OL_TM)
|
||||
|
||||
This specifies the duration of the front transition in seconds when no airspeed feedback is available (e.g. no airspeed sensor present).
|
||||
It should be set to a value which ensures that the vehicle reaches a high enough airspeed to complete the transition, e.g. airspeed should exceed [VT_ARSP_TRANS](../advanced_config/parameter_reference.md#VT_ARSP_TRANS).
|
||||
|
||||
#### Transition Timeout
|
||||
|
||||
[VT_TRANS_TIMEOUT](../advanced_config/parameter_reference.md#VT_TRANS_TIMEOUT)
|
||||
|
||||
This specifies the upper limit for the duration of the front transition. If the vehicle has not reached the transition airspeed after this time, then the transition will be aborted and a [Quadchute](../config/safety.md#quad-chute-failsafe) event will be triggered.
|
||||
:::note
|
||||
Additionally, if an airspeed sensor is present, the transition will also be aborted if the airspeed has not reached [VT_ARSP_BLEND](../advanced_config/parameter_reference.md#VT_ARSP_BLEND) after the openloop transition time [VT_F_TR_OL_TM](../advanced_config/parameter_reference.md#VT_F_TR_OL_TM) has elapsed. This checks is used to avoid a scenario where the vehicle gains excessive speed when the airspeed sensor is faulty.
|
||||
:::
|
||||
|
||||
### 전환 팁
|
||||
|
||||
As already mentioned make sure you have a well tuned multirotor mode.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ARK Pi6X Flow
|
||||
|
||||
The [ARK Pi6X Flow](\(https://arkelectron.gitbook.io/ark-documentation/flight-controllers/ark-pi6x-flow\)) integrates a Raspberry Pi Compute Module 4 (CM4) Carrier, [ARKV6X Flight Controller](../flight_controller/ark_v6x.md), [ARK Flow sensors](../dronecan/ark_flow.md) , [ARK PAB Power Module](../power_module/ark_pab_power_module.md), and a 4-in-1 ESC, all mounted onto one compact board.
|
||||
The [ARK Pi6X Flow](https://arkelectron.gitbook.io/ark-documentation/flight-controllers/ark-pi6x-flow) integrates a Raspberry Pi Compute Module 4 (CM4) Carrier, [ARKV6X Flight Controller](../flight_controller/ark_v6x.md), [ARK Flow sensors](../dronecan/ark_flow.md) , [ARK PAB Power Module](../power_module/ark_pab_power_module.md), and a 4-in-1 ESC, all mounted onto one compact board.
|
||||
|
||||

|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"docs:gen_alt_sidebar_ubuntu": "python3 ./scripts/gen_alt_sidebar.py",
|
||||
"docs:get_alt_sidebar_windows": "python ./scripts/gen_alt_sidebar.py",
|
||||
"start": "yarn docs:dev",
|
||||
"linkcheck": "markdown_link_checker_sc -d en -i assets"
|
||||
"linkcheck": "markdown_link_checker_sc -r .. -d docs -e en -i assets"
|
||||
},
|
||||
"dependencies": {
|
||||
"@red-asuka/vitepress-plugin-tabs": "^0.0.3",
|
||||
|
||||
+26
-26
@@ -158,9 +158,9 @@ Fast oscillations (more than 1 oscillation per second): this is because the gain
|
||||
|
||||
</div>
|
||||
|
||||
### The auto-tuning sequence fails
|
||||
### Послідовність автоматичної настройки не вдається
|
||||
|
||||
If the drone was not moving enough during auto-tuning, the system identification algorithm might have issues to find the correct coefficients.
|
||||
Якщо безпілотник не рухався достатньо під час автоматичного налаштування, алгоритм ідентифікації системи може мати проблеми з визначенням правильних коефіцієнтів.
|
||||
|
||||
Increase the <div style="display: inline;" v-if="$frontmatter.frame === 'Multicopter'">[MC_AT_SYSID_AMP](../advanced_config/parameter_reference.md#MC_AT_SYSID_AMP)</div><div style="display: inline;" v-else-if="$frontmatter.frame === 'Plane'">[FW_AT_SYSID_AMP](../advanced_config/parameter_reference.md#FW_AT_SYSID_AMP)</div> parameter by steps of 1 and trigger the auto-tune again.
|
||||
|
||||
@@ -169,11 +169,11 @@ Increase the <div style="display: inline;" v-if="$frontmatter.frame === 'Multico
|
||||
Due to effects not included in the mathematical model such as delays, saturation, slew-rate, airframe flexibility, the loop gain can be too high.
|
||||
To fix this, follow the same steps described [when the drone oscillates in the pre-tuning test](#drone-oscillates-when-performing-the-pre-tuning-test).
|
||||
|
||||
### I still can't get it to work
|
||||
### Я все ще не можу зрозуміти, як це працює
|
||||
|
||||
Attempt manual tuning using the guides listed in [See also](#see-also) below.
|
||||
|
||||
## Optional Configuration
|
||||
## Необов'язкова Конфігурація
|
||||
|
||||
### Apply Tuning when In-Air/Landed
|
||||
|
||||
@@ -191,12 +191,12 @@ This behaviour can be configured using the [FW_AT_APPLY](../advanced_config/para
|
||||
</div>
|
||||
|
||||
- `0`: the gains are not applied.
|
||||
This is used for testing purposes if the user wants to inspect results of the auto-tuning algorithm without using them directly.
|
||||
Це використовується для тестування, якщо користувач хоче перевірити результати автоналаштування алгоритму без прямого їх використання.
|
||||
- `1`: apply the gains after disarm (default for multirotors).
|
||||
The operator can then test the new tuning while taking-off carefully.
|
||||
Оператор може перевірити нове налаштування під час обережного зльоту.
|
||||
- `2`: apply immediately (default for fixed-wings).
|
||||
The new tuning is applied, disturbances are sent to the controller and the stability is monitored during the next 4 seconds.
|
||||
If the control loop is unstable, the control gains are immediately reverted back to their previous value.
|
||||
Нове налаштування застосовується, перешкоди надсилаються контролеру, а стабільність контролюється протягом наступних 4 секунд.
|
||||
Якщо керуюче коло нестійке, керуючі коефіцієнти негайно повертаються до свого попереднього значення.
|
||||
If the test passes, the pilot can then use the new tuning.
|
||||
|
||||
<div v-if="$frontmatter.frame === 'Plane'">
|
||||
@@ -225,33 +225,33 @@ Fixed-wing vehicles (only) can select which axes are tuned using the [FW_AT_AXES
|
||||
|
||||
</div>
|
||||
|
||||
## Developers/SDKs
|
||||
## Розробники/SDKs
|
||||
|
||||
Autotuning is started using [MAV_CMD_DO_AUTOTUNE_ENABLE](https://mavlink.io/en/messages/common.html#MAV_CMD_DO_AUTOTUNE_ENABLE) MAVLink command.
|
||||
|
||||
At time of writing the message is resent at regular intervals to poll PX4 for progress: the `COMMAND_ACK` includes result that the operation is in progress, and also the progress as a percentage.
|
||||
The operation completes when the progress is 100% or the vehicle lands and disarms.
|
||||
Операція завершується, коли прогрес досягає 100% або транспортний засіб приземляється і роззброюється.
|
||||
|
||||
:::info
|
||||
This is not a MAVLink-compliant implementation of a [command protocol long running command](https://mavlink.io/en/services/command.html#long_running_commands).
|
||||
PX4 should stream progress as the protocol does not allow polling.
|
||||
PX4 повинен транслювати прогрес, оскільки протокол не дозволяє опитування.
|
||||
:::
|
||||
|
||||
The feature is not yet supported by MAVSDK.
|
||||
Функція ще не підтримується MAVSDK.
|
||||
|
||||
## Background/Detail
|
||||
|
||||
PX4 uses [PID controllers](../flight_stack/controller_diagrams.md) (rate, attitude, velocity, and position) to calculate the outputs required to move a vehicle from its current estimated state to match a desired setpoint.
|
||||
The controllers must be well tuned in order to get the best performance out of a vehicle.
|
||||
In particular, a poorly tuned rate controller results in less stable flight in all modes, and takes longer to recover from disturbances.
|
||||
Контролери повинні бути добре налаштовані, щоб отримати найкращу продуктивність з автомобіля.
|
||||
Зокрема, погано налаштований регулятор швидкості призводить до менш стабільного польоту у всіх режимах і потребує більше часу на відновлення після перешкод.
|
||||
|
||||
Generally if you use a [frame configuration](../config/airframe.md) that is similar to your vehicle then the vehicle will be able to fly.
|
||||
However unless the configuration precisely matches your hardware you should tune the rate and attitude controllers.
|
||||
Tuning the velocity and position controllers is less important because they are less affected by vehicle dynamics, and the default tuning configuration for a similar airframe is often sufficient.
|
||||
Однак, якщо конфігурація точно не відповідає вашому обладнанню, вам слід налаштувати регулятори швидкості та кута нахилу.
|
||||
Налаштування контролерів швидкості та позиції менш важливе, оскільки вони менше піддаються динаміці транспортного засобу, і типова конфігурація налаштування для схожого аеродинамічного корпусу часто є достатньою.
|
||||
|
||||
Autotuning provides an automatic mechanism to tune the rate and attitude controllers.
|
||||
Автоналаштування забезпечує автоматичний механізм для налаштування регуляторів швидкості та кута нахилу.
|
||||
It can be used to tune fixed-wing and multicopter vehicles, and VTOL vehicles when flying as a multicopter or as a fixed-wing (transition between modes must be manually tuned).
|
||||
In theory it should work for other vehicle types that have a rate controller, but currently only the above types are supported.
|
||||
Теоретично це повинно працювати для інших типів транспортних засобів, які мають регулятор швидкості, але наразі підтримуються лише вищезазначені типи.
|
||||
|
||||
Automatic tuning works well for the multicopter and fixed-wing vehicle configurations supported by PX4, provided the frame is not too flexible
|
||||
(see [below for more information](#does-autotuning-work-for-all-supported-airframes)).
|
||||
@@ -266,32 +266,32 @@ The default behaviour can be configured using [parameters](#optional-configurati
|
||||
|
||||
### Часто Запитувані Питання
|
||||
|
||||
#### What frames types are supported?
|
||||
#### Які типи кадрів підтримуються?
|
||||
|
||||
Autotuning is enabled for multicopter, fixed-wing, and hybrid VTOL fixed-wing vehicles.
|
||||
|
||||
While it is not yet enabled for other frame types, in theory it an be used with any frame that uses a rate controller.
|
||||
|
||||
#### Does autotuning work for all supported airframes?
|
||||
#### Чи працює автоналадка для всіх підтримуваних конструкцій?
|
||||
|
||||
The mathematical model used by autotuning to estimate the dynamics of the drone assumes this it is a linear system with no coupling between the axes (SISO), and with a limited complexity (2 poles and 2 zeros).
|
||||
If the real drone is too far from those conditions, the model will not be able to represent the real dynamics of the drone.
|
||||
|
||||
In practise, autotuning generally works well for fixed-wing and multicopter, provided the frame is not too flexible.
|
||||
|
||||
#### How long does autotuning take?
|
||||
#### Як довго триває автоналагодження?
|
||||
|
||||
Tuning takes 5s-20s per axis (aborted if tuning could not be established in 20s) + 2s pause between each axis + 4s of testing if the new gains are applied in air.
|
||||
|
||||
A multicopter must tune all three axes, and by default does not test the new gains in-air.
|
||||
Мультикоптер повинен налаштовувати всі три осі, і за замовчуванням не перевіряє нові виграші у повітрі.
|
||||
Tuning will therefore take between 19s (`5 + 2 + 5 + 2 + 5`) and 64s (`20x3 + 2x2`).
|
||||
|
||||
By default a fixed-wing vehicle tunes all three axes and then tests the new gains in-air.
|
||||
За замовчуванням літак налагоджує всі три осі, а потім перевіряє нові коефіцієнти в повітрі.
|
||||
The range is therefore between 25s (`5 + 2 + 5 + 2 + 5 + 2 + 4`) and 70s (`20x3 + 3x2 + 4`).
|
||||
|
||||
Note however that the above settings are defaults.
|
||||
A multicopter can choose to run the tests in air, and a fixed-wing can choose not to.
|
||||
Further, a fixed-wing can choose to tune fewer axes.
|
||||
Зверніть увагу, що вищезазначені налаштування є значеннями за замовчуванням.
|
||||
Мультикоптер може вибрати проведення тестів в повітрі, а планер може вибрати не робити цього.
|
||||
Додатково, літак з фіксованим крилом може вибрати налаштувати менше вісей.
|
||||
|
||||
Anecdotally, it usually takes around 40s for either vehicle.
|
||||
|
||||
|
||||
@@ -84,6 +84,22 @@ Parameter: [VT_ARSP_TRANS](../advanced_config/parameter_reference.md#VT_ARSP_TRA
|
||||
Дуже важливо правильно калібрувати ваш датчик швидкості повітря.
|
||||
It is also important that you pick an airspeed that is comfortably above your airframes stall speed (check `FW_AIRSPD_MIN`) as this is currently not checked.
|
||||
|
||||
#### Openloop Transition Time
|
||||
|
||||
Parameter: [VT_F_TR_OL_TM](../advanced_config/parameter_reference.md#VT_F_TR_OL_TM)
|
||||
|
||||
This specifies the duration of the front transition in seconds when no airspeed feedback is available (e.g. no airspeed sensor present).
|
||||
It should be set to a value which ensures that the vehicle reaches a high enough airspeed to complete the transition, e.g. airspeed should exceed [VT_ARSP_TRANS](../advanced_config/parameter_reference.md#VT_ARSP_TRANS).
|
||||
|
||||
#### Transition Timeout
|
||||
|
||||
[VT_TRANS_TIMEOUT](../advanced_config/parameter_reference.md#VT_TRANS_TIMEOUT)
|
||||
|
||||
This specifies the upper limit for the duration of the front transition. If the vehicle has not reached the transition airspeed after this time, then the transition will be aborted and a [Quadchute](../config/safety.md#quad-chute-failsafe) event will be triggered.
|
||||
:::note
|
||||
Additionally, if an airspeed sensor is present, the transition will also be aborted if the airspeed has not reached [VT_ARSP_BLEND](../advanced_config/parameter_reference.md#VT_ARSP_BLEND) after the openloop transition time [VT_F_TR_OL_TM](../advanced_config/parameter_reference.md#VT_F_TR_OL_TM) has elapsed. This checks is used to avoid a scenario where the vehicle gains excessive speed when the airspeed sensor is faulty.
|
||||
:::
|
||||
|
||||
### Поради щодо переходу
|
||||
|
||||
Як вже зазначалося, переконайтеся, що у вас добре налаштований режим багатокоптера.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ARK Pi6X Flow
|
||||
|
||||
The [ARK Pi6X Flow](\(https://arkelectron.gitbook.io/ark-documentation/flight-controllers/ark-pi6x-flow\)) integrates a Raspberry Pi Compute Module 4 (CM4) Carrier, [ARKV6X Flight Controller](../flight_controller/ark_v6x.md), [ARK Flow sensors](../dronecan/ark_flow.md) , [ARK PAB Power Module](../power_module/ark_pab_power_module.md), and a 4-in-1 ESC, all mounted onto one compact board.
|
||||
The [ARK Pi6X Flow](https://arkelectron.gitbook.io/ark-documentation/flight-controllers/ark-pi6x-flow) integrates a Raspberry Pi Compute Module 4 (CM4) Carrier, [ARKV6X Flight Controller](../flight_controller/ark_v6x.md), [ARK Flow sensors](../dronecan/ark_flow.md) , [ARK PAB Power Module](../power_module/ark_pab_power_module.md), and a 4-in-1 ESC, all mounted onto one compact board.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -83,6 +83,22 @@ This is the airspeed which, when reached, will trigger the transition out of mul
|
||||
It is critical that you have properly calibrated your airspeed sensor.
|
||||
It is also important that you pick an airspeed that is comfortably above your airframes stall speed (check `FW_AIRSPD_MIN`) as this is currently not checked.
|
||||
|
||||
#### Openloop Transition Time
|
||||
|
||||
Parameter: [VT_F_TR_OL_TM](../advanced_config/parameter_reference.md#VT_F_TR_OL_TM)
|
||||
|
||||
This specifies the duration of the front transition in seconds when no airspeed feedback is available (e.g. no airspeed sensor present).
|
||||
It should be set to a value which ensures that the vehicle reaches a high enough airspeed to complete the transition, e.g. airspeed should exceed [VT_ARSP_TRANS](../advanced_config/parameter_reference.md#VT_ARSP_TRANS).
|
||||
|
||||
#### Transition Timeout
|
||||
|
||||
[VT_TRANS_TIMEOUT](../advanced_config/parameter_reference.md#VT_TRANS_TIMEOUT)
|
||||
|
||||
This specifies the upper limit for the duration of the front transition. If the vehicle has not reached the transition airspeed after this time, then the transition will be aborted and a [Quadchute](../config/safety.md#quad-chute-failsafe) event will be triggered.
|
||||
:::note
|
||||
Additionally, if an airspeed sensor is present, the transition will also be aborted if the airspeed has not reached [VT_ARSP_BLEND](../advanced_config/parameter_reference.md#VT_ARSP_BLEND) after the openloop transition time [VT_F_TR_OL_TM](../advanced_config/parameter_reference.md#VT_F_TR_OL_TM) has elapsed. This checks is used to avoid a scenario where the vehicle gains excessive speed when the airspeed sensor is faulty.
|
||||
:::
|
||||
|
||||
### 过渡模式小提示
|
||||
|
||||
As already mentioned make sure you have a well tuned multirotor mode.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Debugging with GDB
|
||||
|
||||
The [GNU DeBugger (GDB)](https://sourceware.org/gdb/documentation/) comes installed with the compiler toolchain in the form of the `arm-none-eabi-gdb` binary.
|
||||
调试器读取ELF文件内的调试富豪,以了解PX4固件的静态和动态内存布局。
|
||||
调试器读取ELF文件内的调试符号,以了解PX4固件的静态和动态内存布局。
|
||||
To access the PX4 autopilot microcontroller, it needs to connect to a [Remote Target](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Connecting.html), which is provided by a [SWD debug probe](swd_debug.md).
|
||||
|
||||
信息流看起来像这样:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Hardware in the Loop Simulation (HITL)
|
||||
# 硬体仿真(HITL)
|
||||
|
||||
:::warning
|
||||
HITL is [community supported and maintained](../simulation/community_supported_simulators.md).
|
||||
硬体仿真被 [社区支持和维护](../simulation/community_supported_simulators.md)
|
||||
It may or may not work with current versions of PX4.
|
||||
|
||||
See [Toolchain Installation](../dev_setup/dev_env.md) for information about the environments and tools supported by the core development team.
|
||||
@@ -10,13 +10,13 @@ See [Toolchain Installation](../dev_setup/dev_env.md) for information about the
|
||||
硬件在环仿真模式 (HITL 或 HIL) 下 PX4 固件代码运行在真实的飞行控制器硬件平台上。
|
||||
这种方法的优点是可以在实际硬件上测试大多数的实际飞行代码。
|
||||
|
||||
PX4 supports HITL for multicopters (using [jMAVSim](../sim_jmavsim/index.md) or [Gazebo Classic](../sim_gazebo_classic/index.md)) and VTOL (using Gazebo Classic).
|
||||
PX4 支持多轴( [jMAVSim](../sim_jmavsim/index.md)或[Gazebo Classic](../sim_gazebo_classic/index.md))及VTOL (using Gazebo Classic)的仿真。
|
||||
|
||||
<a id="compatible_airframe"></a>
|
||||
|
||||
## HITL兼容机架
|
||||
|
||||
The set of compatible airframes vs simulators is:
|
||||
机架与模拟器兼容情况:
|
||||
|
||||
| 机架 | `SYS_AUTOSTART` | Gazebo Classic | jMAVSim |
|
||||
| ---------------------------------------------------------------------------------------------------------------- | --------------- | -------------- | ------- |
|
||||
|
||||
+1
-1
@@ -45,7 +45,6 @@ set(msg_files
|
||||
ActuatorTest.msg
|
||||
AdcReport.msg
|
||||
Airspeed.msg
|
||||
AirspeedValidated.msg
|
||||
AirspeedWind.msg
|
||||
AutotuneAttitudeControlStatus.msg
|
||||
ButtonEvent.msg
|
||||
@@ -234,6 +233,7 @@ set(msg_files
|
||||
YawEstimatorStatus.msg
|
||||
versioned/ActuatorMotors.msg
|
||||
versioned/ActuatorServos.msg
|
||||
versioned/AirspeedValidated.msg
|
||||
versioned/ArmingCheckReply.msg
|
||||
versioned/ArmingCheckRequest.msg
|
||||
versioned/BatteryStatus.msg
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
uint32 MESSAGE_VERSION = 0
|
||||
|
||||
uint64 timestamp # time since system start (microseconds)
|
||||
|
||||
float32 indicated_airspeed_m_s # indicated airspeed in m/s (IAS), set to NAN if invalid
|
||||
@@ -86,33 +86,29 @@
|
||||
|
||||
static void init_timer_config(uint32_t channel_mask);
|
||||
static void init_timers_dma_up(void);
|
||||
static void init_timers_dma_capt_comp(uint8_t timer_index);
|
||||
static int32_t init_timer_channels(uint8_t timer_index);
|
||||
|
||||
static void configure_channels_round_robin(uint8_t timer_index);
|
||||
static void select_next_capture_channel(uint8_t timer_index);
|
||||
static uint8_t output_channel_from_timer_channel(uint8_t timer_index, uint8_t timer_channel);
|
||||
|
||||
static void dma_burst_finished_callback(DMA_HANDLE handle, uint8_t status, void *arg);
|
||||
static void capture_complete_callback(void *arg);
|
||||
|
||||
static void process_capture_results(uint8_t timer_index);
|
||||
static void process_capture_results(uint8_t timer_index, uint8_t channel_index);
|
||||
static unsigned calculate_period(uint8_t timer_index, uint8_t channel_index);
|
||||
|
||||
// Timer configuration struct
|
||||
typedef struct timer_config_t {
|
||||
DMA_HANDLE dma_up_handle; // DMA stream for DMA update
|
||||
DMA_HANDLE dma_ch_handle[4]; // DMA streams for bidi CaptComp
|
||||
DMA_HANDLE dma_handle; // DMA stream for DMA update and eRPM Capture Compare
|
||||
bool enabled; // Timer enabled
|
||||
bool enabled_channels[4]; // Timer Channels enabled (requested)
|
||||
bool initialized; // Timer initialized
|
||||
bool initialized_channels[4]; // Timer channels initialized (successfully started)
|
||||
bool bidirectional; // Timer in bidi (inverted) mode
|
||||
bool captcomp_channels[4]; // Channels configured for CaptComp
|
||||
bool round_robin_enabled;
|
||||
bool bidirectional; // Timer in bidir (inverted) mode
|
||||
int capture_channel_index; // Timer channel currently being catured in bidirectional mode
|
||||
uint8_t timer_index; // Timer index. Necessary to have memory for passing pointer to hrt callback
|
||||
} timer_config_t;
|
||||
|
||||
static uint8_t _num_dma_available = 0;
|
||||
|
||||
static timer_config_t timer_configs[MAX_IO_TIMERS] = {};
|
||||
|
||||
// Output buffer of interleaved motor output bytes
|
||||
@@ -129,8 +125,8 @@ static uint8_t _bidi_timer_index = 0; // TODO: BDSHOT_TIM param to select timer
|
||||
static uint32_t _dshot_frequency = 0;
|
||||
|
||||
// eRPM data for channels on the singular timer
|
||||
static int32_t _erpms[MAX_NUM_CHANNELS_PER_TIMER] = {};
|
||||
static bool _erpms_ready[MAX_NUM_CHANNELS_PER_TIMER] = {};
|
||||
static int32_t _erpms[MAX_TIMER_IO_CHANNELS] = {};
|
||||
static bool _erpms_ready[MAX_TIMER_IO_CHANNELS] = {};
|
||||
|
||||
// hrt callback handle for captcomp post dma processing
|
||||
static struct hrt_call _cc_call;
|
||||
@@ -143,7 +139,7 @@ static uint32_t read_fail_zero[MAX_NUM_CHANNELS_PER_TIMER] = {};
|
||||
|
||||
static void init_timer_config(uint32_t channel_mask)
|
||||
{
|
||||
// Mark timers in use, channels in use, and timers for bidi dshot
|
||||
// Mark timers in use, channels in use, and timers for bidir dshot
|
||||
for (unsigned output_channel = 0; output_channel < MAX_TIMER_IO_CHANNELS; output_channel++) {
|
||||
if (channel_mask & (1 << output_channel)) {
|
||||
uint8_t timer_index = timer_io_channels[output_channel].timer_index;
|
||||
@@ -203,9 +199,9 @@ static void init_timers_dma_up(void)
|
||||
}
|
||||
|
||||
// Attempt to allocate DMA for Burst
|
||||
timer_configs[timer_index].dma_up_handle = stm32_dmachannel(io_timers[timer_index].dshot.dma_map_up);
|
||||
timer_configs[timer_index].dma_handle = stm32_dmachannel(io_timers[timer_index].dshot.dma_map_up);
|
||||
|
||||
if (timer_configs[timer_index].dma_up_handle == NULL) {
|
||||
if (timer_configs[timer_index].dma_handle == NULL) {
|
||||
PX4_DEBUG("Failed to allocate Timer %u DMA UP", timer_index);
|
||||
continue;
|
||||
}
|
||||
@@ -216,87 +212,14 @@ static void init_timers_dma_up(void)
|
||||
|
||||
// Free the allocated DMA channels
|
||||
for (uint8_t timer_index = 0; timer_index < MAX_IO_TIMERS; timer_index++) {
|
||||
if (timer_configs[timer_index].dma_up_handle != NULL) {
|
||||
stm32_dmafree(timer_configs[timer_index].dma_up_handle);
|
||||
timer_configs[timer_index].dma_up_handle = NULL;
|
||||
if (timer_configs[timer_index].dma_handle != NULL) {
|
||||
stm32_dmafree(timer_configs[timer_index].dma_handle);
|
||||
timer_configs[timer_index].dma_handle = NULL;
|
||||
PX4_DEBUG("Freed DMA UP Timer Index %u", timer_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void init_timers_dma_capt_comp(uint8_t timer_index)
|
||||
{
|
||||
if (timer_configs[timer_index].enabled && timer_configs[timer_index].initialized) {
|
||||
|
||||
// Allocate DMA for each enabled channel
|
||||
for (uint8_t output_channel = 0; output_channel < MAX_TIMER_IO_CHANNELS; output_channel++) {
|
||||
|
||||
uint8_t timer_channel = timer_io_channels[output_channel].timer_channel;
|
||||
|
||||
if ((timer_channel <= 0) || (timer_channel >= 5)) {
|
||||
// invalid channel, only 1 - 4
|
||||
continue;
|
||||
}
|
||||
|
||||
// For each enabled channel on this timer, try allocating DMA
|
||||
uint8_t timer_channel_index = timer_channel - 1;
|
||||
bool this_timer = timer_index == timer_io_channels[output_channel].timer_index;
|
||||
bool channel_enabled = timer_configs[timer_index].enabled_channels[timer_channel_index];
|
||||
|
||||
if (this_timer && channel_enabled) {
|
||||
uint32_t dma_map_ch = io_timers[timer_index].dshot.dma_map_ch[timer_channel_index];
|
||||
|
||||
if (dma_map_ch == 0) {
|
||||
// Timer channel is not mapped
|
||||
PX4_WARN("Error! Timer %u Channel %u DMA is unmapped", timer_index, timer_channel_index);
|
||||
continue;
|
||||
}
|
||||
|
||||
PX4_DEBUG("Allocating DMA CH for Timer Index %u Channel %u", timer_index, timer_channel_index);
|
||||
// Allocate DMA
|
||||
timer_configs[timer_index].dma_ch_handle[timer_channel_index] = stm32_dmachannel(dma_map_ch);
|
||||
|
||||
if (timer_configs[timer_index].dma_ch_handle[timer_channel_index] == NULL) {
|
||||
PX4_WARN("Failed to allocate Timer %u Channel DMA CH %u, output_channel %u", timer_index, timer_channel_index,
|
||||
output_channel);
|
||||
continue;
|
||||
}
|
||||
|
||||
PX4_DEBUG("Allocated DMA CH Timer Index %u Channel %u", timer_index, timer_channel_index);
|
||||
// Mark this timer channel as bidirectional
|
||||
timer_configs[timer_index].captcomp_channels[timer_channel_index] = true;
|
||||
_num_dma_available++;
|
||||
|
||||
if (_num_dma_available >= BOARD_DMA_NUM_DSHOT_CHANNELS) {
|
||||
PX4_INFO("Limiting DMA channels to %u", _num_dma_available);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// De-allocate DMA for each channel
|
||||
for (uint8_t output_channel = 0; output_channel < MAX_TIMER_IO_CHANNELS; output_channel++) {
|
||||
if (timer_index == timer_io_channels[output_channel].timer_index) {
|
||||
|
||||
uint8_t timer_channel = timer_io_channels[output_channel].timer_channel;
|
||||
|
||||
if ((timer_channel <= 0) || (timer_channel >= 5)) {
|
||||
// invalid channel, only 1 - 4
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t timer_channel_index = timer_channel - 1;
|
||||
|
||||
if (timer_configs[timer_index].dma_ch_handle[timer_channel_index]) {
|
||||
stm32_dmafree(timer_configs[timer_index].dma_ch_handle[timer_channel_index]);
|
||||
timer_configs[timer_index].dma_ch_handle[timer_channel_index] = NULL;
|
||||
PX4_DEBUG("Freed DMA CH Timer Index %u Channel %u", timer_index, timer_channel_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t init_timer_channels(uint8_t timer_index)
|
||||
{
|
||||
int32_t channels_init_mask = 0;
|
||||
@@ -366,18 +289,6 @@ int up_dshot_init(uint32_t channel_mask, unsigned dshot_pwm_freq, bool enable_bi
|
||||
// Initializes dshot on each timer if DShot mode is enabled and DMA is available
|
||||
init_timers_dma_up();
|
||||
|
||||
// Initializes a single timer in Bidirectional DShot mode
|
||||
if (_bidirectional) {
|
||||
// Use first configured DShot timer (Timer index 0)
|
||||
// TODO: BDSHOT_TIM param to select timer index?
|
||||
init_timers_dma_capt_comp(_bidi_timer_index);
|
||||
|
||||
// Enable round robin if we have 1 - 3 DMA
|
||||
if ((_num_dma_available < 4) && _num_dma_available > 0) {
|
||||
timer_configs[_bidi_timer_index].round_robin_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t channels_init_mask = 0;
|
||||
|
||||
for (uint8_t timer_index = 0; timer_index < MAX_IO_TIMERS; timer_index++) {
|
||||
@@ -426,10 +337,10 @@ void up_dshot_trigger()
|
||||
io_timer_set_dshot_burst_mode(timer_index, _dshot_frequency, channel_count);
|
||||
|
||||
// Allocate DMA
|
||||
if (timer_configs[timer_index].dma_up_handle == NULL) {
|
||||
timer_configs[timer_index].dma_up_handle = stm32_dmachannel(io_timers[timer_index].dshot.dma_map_up);
|
||||
if (timer_configs[timer_index].dma_handle == NULL) {
|
||||
timer_configs[timer_index].dma_handle = stm32_dmachannel(io_timers[timer_index].dshot.dma_map_up);
|
||||
|
||||
if (timer_configs[timer_index].dma_up_handle == NULL) {
|
||||
if (timer_configs[timer_index].dma_handle == NULL) {
|
||||
PX4_WARN("DMA allocation for timer %u failed", timer_index);
|
||||
continue;
|
||||
}
|
||||
@@ -440,7 +351,7 @@ void up_dshot_trigger()
|
||||
(uintptr_t) dshot_output_buffer[timer_index] +
|
||||
DSHOT_OUTPUT_BUFFER_SIZE(channel_count));
|
||||
|
||||
px4_stm32_dmasetup(timer_configs[timer_index].dma_up_handle,
|
||||
px4_stm32_dmasetup(timer_configs[timer_index].dma_handle,
|
||||
io_timers[timer_index].base + STM32_GTIM_DMAR_OFFSET,
|
||||
(uint32_t)(dshot_output_buffer[timer_index]),
|
||||
channel_count * CHANNEL_OUTPUT_BUFF_SIZE,
|
||||
@@ -451,12 +362,12 @@ void up_dshot_trigger()
|
||||
|
||||
// Trigger DMA (DShot Outputs)
|
||||
if (timer_configs[timer_index].bidirectional) {
|
||||
stm32_dmastart(timer_configs[timer_index].dma_up_handle, dma_burst_finished_callback,
|
||||
stm32_dmastart(timer_configs[timer_index].dma_handle, dma_burst_finished_callback,
|
||||
&timer_configs[timer_index].timer_index,
|
||||
false);
|
||||
|
||||
} else {
|
||||
stm32_dmastart(timer_configs[timer_index].dma_up_handle, NULL, NULL, false);
|
||||
stm32_dmastart(timer_configs[timer_index].dma_handle, NULL, NULL, false);
|
||||
}
|
||||
|
||||
// Enable DMA update request
|
||||
@@ -465,67 +376,41 @@ void up_dshot_trigger()
|
||||
}
|
||||
}
|
||||
|
||||
static void configure_channels_round_robin(uint8_t timer_index)
|
||||
static void select_next_capture_channel(uint8_t timer_index)
|
||||
{
|
||||
switch (_num_dma_available) {
|
||||
case 1: {
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
if (timer_configs[timer_index].captcomp_channels[i]) {
|
||||
timer_configs[timer_index].captcomp_channels[i] = false;
|
||||
bool found = false;
|
||||
int next_index = timer_configs[timer_index].capture_channel_index;
|
||||
|
||||
if (i == 3) {
|
||||
timer_configs[timer_index].captcomp_channels[0] = true;
|
||||
while (!found) {
|
||||
next_index++;
|
||||
|
||||
} else {
|
||||
timer_configs[timer_index].captcomp_channels[i + 1] = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
if (next_index > 3) {
|
||||
next_index = 0;
|
||||
}
|
||||
|
||||
case 2: {
|
||||
if (timer_configs[timer_index].captcomp_channels[0]) {
|
||||
timer_configs[timer_index].captcomp_channels[0] = false;
|
||||
timer_configs[timer_index].captcomp_channels[1] = true;
|
||||
timer_configs[timer_index].captcomp_channels[2] = false;
|
||||
timer_configs[timer_index].captcomp_channels[3] = true;
|
||||
|
||||
} else {
|
||||
timer_configs[timer_index].captcomp_channels[0] = true;
|
||||
timer_configs[timer_index].captcomp_channels[1] = false;
|
||||
timer_configs[timer_index].captcomp_channels[2] = true;
|
||||
timer_configs[timer_index].captcomp_channels[3] = false;
|
||||
}
|
||||
|
||||
break;
|
||||
if (timer_configs[timer_index].initialized_channels[next_index]) {
|
||||
found = true;
|
||||
}
|
||||
|
||||
case 3: {
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
if (!timer_configs[timer_index].captcomp_channels[i]) {
|
||||
timer_configs[timer_index].captcomp_channels[i] = true;
|
||||
|
||||
if (i == 3) {
|
||||
timer_configs[timer_index].captcomp_channels[0] = false;
|
||||
|
||||
} else {
|
||||
timer_configs[timer_index].captcomp_channels[i + 1] = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
timer_configs[timer_index].capture_channel_index = next_index;
|
||||
}
|
||||
|
||||
static uint8_t output_channel_from_timer_channel(uint8_t timer_index, uint8_t timer_channel_index)
|
||||
{
|
||||
for (uint8_t output_channel = 0; output_channel < MAX_TIMER_IO_CHANNELS; output_channel++) {
|
||||
|
||||
bool is_this_timer = timer_index == timer_io_channels[output_channel].timer_index;
|
||||
uint8_t channel_index = timer_io_channels[output_channel].timer_channel - 1;
|
||||
|
||||
if (is_this_timer && (channel_index == timer_channel_index)) {
|
||||
// We found the output channel associated with this timer channel
|
||||
return output_channel;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: error handling?
|
||||
return 0;
|
||||
}
|
||||
|
||||
void dma_burst_finished_callback(DMA_HANDLE handle, uint8_t status, void *arg)
|
||||
@@ -533,10 +418,10 @@ void dma_burst_finished_callback(DMA_HANDLE handle, uint8_t status, void *arg)
|
||||
uint8_t timer_index = *((uint8_t *)arg);
|
||||
|
||||
// Clean DMA UP configuration
|
||||
if (timer_configs[timer_index].dma_up_handle != NULL) {
|
||||
stm32_dmastop(timer_configs[timer_index].dma_up_handle);
|
||||
stm32_dmafree(timer_configs[timer_index].dma_up_handle);
|
||||
timer_configs[timer_index].dma_up_handle = NULL;
|
||||
if (timer_configs[timer_index].dma_handle != NULL) {
|
||||
stm32_dmastop(timer_configs[timer_index].dma_handle);
|
||||
stm32_dmafree(timer_configs[timer_index].dma_handle);
|
||||
timer_configs[timer_index].dma_handle = NULL;
|
||||
}
|
||||
|
||||
// Disable DMA update request
|
||||
@@ -550,63 +435,44 @@ void dma_burst_finished_callback(DMA_HANDLE handle, uint8_t status, void *arg)
|
||||
up_clean_dcache((uintptr_t) dshot_capture_buffer,
|
||||
(uintptr_t) dshot_capture_buffer + DSHOT_CAPTURE_BUFFER_SIZE(MAX_NUM_CHANNELS_PER_TIMER));
|
||||
|
||||
// If round robin is enabled reconfigure which channels we capture on
|
||||
if (timer_configs[timer_index].round_robin_enabled) {
|
||||
configure_channels_round_robin(timer_index);
|
||||
// Unallocate timer channel for currently selected capture_channel
|
||||
uint8_t capture_channel = timer_configs[timer_index].capture_channel_index;
|
||||
uint8_t output_channel = output_channel_from_timer_channel(timer_index, capture_channel);
|
||||
|
||||
// Re-initialize output for CaptureDMA for next time
|
||||
io_timer_unallocate_channel(output_channel);
|
||||
io_timer_channel_init(output_channel, IOTimerChanMode_CaptureDMA, NULL, NULL);
|
||||
|
||||
// Select the next capture channel
|
||||
select_next_capture_channel(timer_index);
|
||||
|
||||
// Allocate DMA for currently selected capture_channel
|
||||
capture_channel = timer_configs[timer_index].capture_channel_index;
|
||||
timer_configs[timer_index].dma_handle = stm32_dmachannel(io_timers[timer_index].dshot.dma_map_ch[capture_channel]);
|
||||
|
||||
// If DMA handler is valid, start DMA
|
||||
if (timer_configs[timer_index].dma_handle == NULL) {
|
||||
PX4_WARN("failed to allocate dma for timer %u channel %u", timer_index, capture_channel);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate DMA for all enabled channels on this timer
|
||||
for (uint8_t output_channel = 0; output_channel < MAX_TIMER_IO_CHANNELS; output_channel++) {
|
||||
// Set Capture mode for this channel
|
||||
io_timer_set_dshot_capture_mode(timer_index, capture_channel, _dshot_frequency);
|
||||
io_timer_capture_dma_req(timer_index, capture_channel, true);
|
||||
|
||||
bool is_this_timer = timer_index == timer_io_channels[output_channel].timer_index;
|
||||
uint8_t timer_channel = timer_io_channels[output_channel].timer_channel;
|
||||
// Choose which CC register for this DMA stream
|
||||
uint32_t periph_addr = io_timers[timer_index].base + STM32_GTIM_CCR1_OFFSET + (4 * capture_channel);
|
||||
|
||||
if ((timer_channel <= 0) || (timer_channel >= 5)) {
|
||||
// invalid channel, only 1 - 4
|
||||
continue;
|
||||
}
|
||||
// Setup DMA for this channel
|
||||
px4_stm32_dmasetup(timer_configs[timer_index].dma_handle,
|
||||
periph_addr,
|
||||
(uint32_t) dshot_capture_buffer[capture_channel],
|
||||
CHANNEL_CAPTURE_BUFF_SIZE,
|
||||
DSHOT_BIDIRECTIONAL_DMA_SCR);
|
||||
|
||||
uint8_t timer_channel_index = timer_channel - 1;
|
||||
bool channel_initialized = timer_configs[timer_index].initialized_channels[timer_channel_index];
|
||||
bool captcomp_enabled = timer_configs[timer_index].captcomp_channels[timer_channel_index];
|
||||
|
||||
if (is_this_timer && channel_initialized && captcomp_enabled) {
|
||||
|
||||
// Re-initialize output for CaptureDMA
|
||||
io_timer_unallocate_channel(output_channel);
|
||||
io_timer_channel_init(output_channel, IOTimerChanMode_CaptureDMA, NULL, NULL);
|
||||
|
||||
// Allocate DMA
|
||||
if (timer_configs[timer_index].dma_ch_handle[timer_channel_index] == NULL) {
|
||||
timer_configs[timer_index].dma_ch_handle[timer_channel_index] = stm32_dmachannel(
|
||||
io_timers[timer_index].dshot.dma_map_ch[timer_channel_index]);
|
||||
}
|
||||
|
||||
// If DMA handler is valid, start DMA
|
||||
if (timer_configs[timer_index].dma_ch_handle[timer_channel_index] == NULL) {
|
||||
PX4_WARN("failed to allocate dma for timer %u channel %u", timer_index, timer_channel_index);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set Capture mode for this channel
|
||||
io_timer_set_dshot_capture_mode(timer_index, timer_channel_index, _dshot_frequency);
|
||||
io_timer_capture_dma_req(timer_index, timer_channel_index, true);
|
||||
|
||||
// Choose which CC register for this DMA stream
|
||||
uint32_t periph_addr = io_timers[timer_index].base + STM32_GTIM_CCR1_OFFSET + (4 * timer_channel_index);
|
||||
|
||||
// Setup DMA for this channel
|
||||
px4_stm32_dmasetup(timer_configs[timer_index].dma_ch_handle[timer_channel_index],
|
||||
periph_addr,
|
||||
(uint32_t) dshot_capture_buffer[timer_channel_index],
|
||||
CHANNEL_CAPTURE_BUFF_SIZE,
|
||||
DSHOT_BIDIRECTIONAL_DMA_SCR);
|
||||
|
||||
// NOTE: we can't use DMA callback since GCR encoding creates a variable length pulse train. Instead
|
||||
// we use an hrt callback to schedule the processing of the received and DMAd eRPM frames.
|
||||
stm32_dmastart(timer_configs[timer_index].dma_ch_handle[timer_channel_index], NULL, NULL, false);
|
||||
}
|
||||
}
|
||||
// NOTE: we can't use DMA callback since GCR encoding creates a variable length pulse train. Instead
|
||||
// we use an hrt callback to schedule the processing of the received and DMAd eRPM frames.
|
||||
stm32_dmastart(timer_configs[timer_index].dma_handle, NULL, NULL, false);
|
||||
|
||||
// Enable CaptureDMA and on all configured channels
|
||||
io_timer_set_enable(true, IOTimerChanMode_CaptureDMA, IO_TIMER_ALL_MODES_CHANNELS);
|
||||
@@ -624,33 +490,26 @@ static void capture_complete_callback(void *arg)
|
||||
// Unallocate the timer as CaptureDMA
|
||||
io_timer_unallocate_timer(timer_index);
|
||||
|
||||
uint8_t capture_channel = timer_configs[timer_index].capture_channel_index;
|
||||
|
||||
// Disable capture DMA
|
||||
io_timer_capture_dma_req(timer_index, capture_channel, false);
|
||||
|
||||
if (timer_configs[timer_index].dma_handle != NULL) {
|
||||
stm32_dmastop(timer_configs[timer_index].dma_handle);
|
||||
stm32_dmafree(timer_configs[timer_index].dma_handle);
|
||||
timer_configs[timer_index].dma_handle = NULL;
|
||||
}
|
||||
|
||||
// Re-initialize all output channels on this timer
|
||||
for (uint8_t output_channel = 0; output_channel < MAX_TIMER_IO_CHANNELS; output_channel++) {
|
||||
|
||||
bool is_this_timer = timer_index == timer_io_channels[output_channel].timer_index;
|
||||
uint8_t timer_channel = timer_io_channels[output_channel].timer_channel;
|
||||
|
||||
if ((timer_channel <= 0) || (timer_channel >= 5)) {
|
||||
// invalid channel, only 1 - 4
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t timer_channel_index = timer_channel - 1;
|
||||
uint8_t timer_channel_index = timer_io_channels[output_channel].timer_channel - 1;
|
||||
bool channel_initialized = timer_configs[timer_index].initialized_channels[timer_channel_index];
|
||||
bool captcomp_enabled = timer_configs[timer_index].captcomp_channels[timer_channel_index];
|
||||
|
||||
if (is_this_timer && channel_initialized) {
|
||||
|
||||
if (captcomp_enabled) {
|
||||
// Disable capture DMA
|
||||
io_timer_capture_dma_req(timer_index, timer_channel_index, false);
|
||||
|
||||
if (timer_configs[timer_index].dma_ch_handle[timer_channel_index] != NULL) {
|
||||
stm32_dmastop(timer_configs[timer_index].dma_ch_handle[timer_channel_index]);
|
||||
stm32_dmafree(timer_configs[timer_index].dma_ch_handle[timer_channel_index]);
|
||||
timer_configs[timer_index].dma_ch_handle[timer_channel_index] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
io_timer_unallocate_channel(output_channel);
|
||||
// Initialize back to DShotInverted to bring IO back to the expected idle state
|
||||
io_timer_channel_init(output_channel, IOTimerChanMode_DshotInverted, NULL, NULL);
|
||||
@@ -662,39 +521,34 @@ static void capture_complete_callback(void *arg)
|
||||
(uintptr_t) dshot_capture_buffer + DSHOT_CAPTURE_BUFFER_SIZE(MAX_NUM_CHANNELS_PER_TIMER));
|
||||
|
||||
// Process eRPM frames from all channels on this timer
|
||||
process_capture_results(timer_index);
|
||||
process_capture_results(timer_index, capture_channel);
|
||||
|
||||
// Enable all channels configured as DShotInverted
|
||||
io_timer_set_enable(true, IOTimerChanMode_DshotInverted, IO_TIMER_ALL_MODES_CHANNELS);
|
||||
}
|
||||
|
||||
void process_capture_results(uint8_t timer_index)
|
||||
void process_capture_results(uint8_t timer_index, uint8_t channel_index)
|
||||
{
|
||||
for (uint8_t channel_index = 0; channel_index < MAX_NUM_CHANNELS_PER_TIMER; channel_index++) {
|
||||
const unsigned period = calculate_period(timer_index, channel_index);
|
||||
|
||||
if (!timer_configs[timer_index].captcomp_channels[channel_index]) {
|
||||
continue;
|
||||
}
|
||||
uint8_t output_channel = output_channel_from_timer_channel(timer_index, channel_index);
|
||||
|
||||
// Calculate the period for each channel
|
||||
const unsigned period = calculate_period(timer_index, channel_index);
|
||||
|
||||
if (period == 0) {
|
||||
// If the parsing failed, set the eRPM to 0
|
||||
_erpms[channel_index] = 0;
|
||||
if (period == 0) {
|
||||
// If the parsing failed, set the eRPM to 0
|
||||
_erpms[output_channel] = 0;
|
||||
|
||||
} else if (period == 65408) {
|
||||
// Special case for zero motion (e.g., stationary motor)
|
||||
_erpms[channel_index] = 0;
|
||||
} else if (period == 65408) {
|
||||
// Special case for zero motion (e.g., stationary motor)
|
||||
_erpms[output_channel] = 0;
|
||||
|
||||
} else {
|
||||
// Convert the period to eRPM
|
||||
_erpms[channel_index] = (1000000 * 60) / period;
|
||||
}
|
||||
|
||||
// We set it ready anyway, not to hold up other channels when used in round robin.
|
||||
_erpms_ready[channel_index] = true;
|
||||
} else {
|
||||
// Convert the period to eRPM
|
||||
_erpms[output_channel] = (1000000 * 60) / period;
|
||||
}
|
||||
|
||||
// We set it ready anyway, not to hold up other channels when used in round robin.
|
||||
_erpms_ready[output_channel] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -757,7 +611,7 @@ int up_bdshot_num_erpm_ready(void)
|
||||
{
|
||||
int num_ready = 0;
|
||||
|
||||
for (unsigned i = 0; i < MAX_NUM_CHANNELS_PER_TIMER; ++i) {
|
||||
for (unsigned i = 0; i < MAX_TIMER_IO_CHANNELS; ++i) {
|
||||
if (_erpms_ready[i]) {
|
||||
++num_ready;
|
||||
}
|
||||
@@ -773,8 +627,8 @@ int up_bdshot_get_erpm(uint8_t output_channel, int *erpm)
|
||||
bool channel_initialized = timer_configs[timer_index].initialized_channels[timer_channel_index];
|
||||
|
||||
if (channel_initialized) {
|
||||
*erpm = _erpms[timer_channel_index];
|
||||
_erpms_ready[timer_channel_index] = false;
|
||||
*erpm = _erpms[output_channel];
|
||||
_erpms_ready[output_channel] = false;
|
||||
return PX4_OK;
|
||||
}
|
||||
|
||||
@@ -802,11 +656,6 @@ void up_bdshot_status(void)
|
||||
|
||||
if (_bidirectional) {
|
||||
PX4_INFO("Bidirectional DShot enabled");
|
||||
PX4_INFO("Available DMA: %u", _num_dma_available);
|
||||
|
||||
if (_num_dma_available < 4) {
|
||||
PX4_INFO("Round robin enabled");
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t timer_index = _bidi_timer_index;
|
||||
|
||||
+1
-1
Submodule src/drivers/gps/devices updated: 91e3dbb296...4cea5de87c
@@ -64,16 +64,6 @@ Heater::Heater() :
|
||||
ModuleParams(nullptr),
|
||||
ScheduledWorkItem(MODULE_NAME, px4::wq_configurations::lp_default)
|
||||
{
|
||||
#ifdef HEATER_PX4IO
|
||||
_io_fd = px4_open(IO_HEATER_DEVICE_PATH, O_RDWR);
|
||||
|
||||
if (_io_fd < 0) {
|
||||
PX4_ERR("Unable to open heater device path");
|
||||
return;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
_heater_status_pub.advertise();
|
||||
}
|
||||
|
||||
|
||||
+18
-26
@@ -57,8 +57,6 @@ Battery::Battery(int index, ModuleParams *parent, const int sample_interval_us,
|
||||
_current_average_filter_a.setParameters(expected_filter_dt, 50.f);
|
||||
_ocv_filter_v.setParameters(expected_filter_dt, 1.f);
|
||||
_cell_voltage_filter_v.setParameters(expected_filter_dt, 1.f);
|
||||
_soc_derivative_filter.setParameters(expected_filter_dt, 50.f);
|
||||
_flight_time_filter.setParameters(expected_filter_dt, 50.f);
|
||||
|
||||
if (index > 9 || index < 1) {
|
||||
PX4_ERR("Battery index must be between 1 and 9 (inclusive). Received %d. Defaulting to 1.", index);
|
||||
@@ -94,7 +92,6 @@ Battery::Battery(int index, ModuleParams *parent, const int sample_interval_us,
|
||||
_param_handles.emergen_thr = param_find("BAT_EMERGEN_THR");
|
||||
|
||||
_param_handles.bat_avrg_current = param_find("BAT_AVRG_CURRENT");
|
||||
_param_handles.bat_avrg_soc = param_find("BAT_AVRG_SOC");
|
||||
|
||||
updateParams();
|
||||
}
|
||||
@@ -156,7 +153,7 @@ battery_status_s Battery::getBatteryStatus()
|
||||
battery_status.discharged_mah = _discharged_mah;
|
||||
battery_status.remaining = _state_of_charge;
|
||||
battery_status.scale = _scale;
|
||||
battery_status.time_remaining_s = computeRemainingTime(_state_of_charge);
|
||||
battery_status.time_remaining_s = computeRemainingTime(_current_a);
|
||||
battery_status.temperature = _temperature_c;
|
||||
battery_status.cell_count = _params.n_cells;
|
||||
battery_status.connected = _connected;
|
||||
@@ -348,11 +345,10 @@ void Battery::computeScale()
|
||||
}
|
||||
}
|
||||
|
||||
float Battery::computeRemainingTime(const float state_of_charge)
|
||||
float Battery::computeRemainingTime(float current_a)
|
||||
{
|
||||
float time_remaining_s = NAN;
|
||||
bool reset_soc_derivative_filter = false;
|
||||
hrt_abstime timestamp = hrt_absolute_time();
|
||||
bool reset_current_avg_filter = false;
|
||||
|
||||
if (_vehicle_status_sub.updated()) {
|
||||
vehicle_status_s vehicle_status;
|
||||
@@ -361,7 +357,7 @@ float Battery::computeRemainingTime(const float state_of_charge)
|
||||
_armed = (vehicle_status.arming_state == vehicle_status_s::ARMING_STATE_ARMED);
|
||||
|
||||
if (vehicle_status.vehicle_type == vehicle_status_s::VEHICLE_TYPE_FIXED_WING && !_vehicle_status_is_fw) {
|
||||
reset_soc_derivative_filter = true;
|
||||
reset_current_avg_filter = true;
|
||||
}
|
||||
|
||||
_vehicle_status_is_fw = (vehicle_status.vehicle_type == vehicle_status_s::VEHICLE_TYPE_FIXED_WING);
|
||||
@@ -371,31 +367,28 @@ float Battery::computeRemainingTime(const float state_of_charge)
|
||||
_flight_phase_estimation_sub.update();
|
||||
|
||||
// reset filter if not feasible, negative or we did a VTOL transition to FW mode
|
||||
if (!PX4_ISFINITE(_soc_derivative_filter.getState()) || _soc_derivative_filter.getState() < FLT_EPSILON
|
||||
|| reset_soc_derivative_filter) {
|
||||
_soc_derivative_filter.reset(_params.bat_avrg_soc);
|
||||
if (!PX4_ISFINITE(_current_average_filter_a.getState()) || _current_average_filter_a.getState() < FLT_EPSILON
|
||||
|| reset_current_avg_filter) {
|
||||
_current_average_filter_a.reset(_params.bat_avrg_current);
|
||||
}
|
||||
|
||||
const float dt = math::constrain(timestamp - _last_flight_time_update, 1_ms, 5000_ms) * 1e-6f;
|
||||
|
||||
if (_armed && _state_of_charge < 0.99f && PX4_ISFINITE(dt)) {
|
||||
if (_armed && PX4_ISFINITE(current_a)) {
|
||||
// For FW only update when we are in level flight
|
||||
if (!_vehicle_status_is_fw || ((timestamp - _flight_phase_estimation_sub.get().timestamp) < 2_s
|
||||
if (!_vehicle_status_is_fw || ((hrt_absolute_time() - _flight_phase_estimation_sub.get().timestamp) < 2_s
|
||||
&& _flight_phase_estimation_sub.get().flight_phase == flight_phase_estimation_s::FLIGHT_PHASE_LEVEL)) {
|
||||
_soc_derivative_filter.update((_prev_state_of_charge - _state_of_charge) / dt);
|
||||
// only update with positive numbers
|
||||
_current_average_filter_a.update(fmaxf(current_a, 0.f));
|
||||
}
|
||||
|
||||
} else {
|
||||
_soc_derivative_filter.reset(_params.bat_avrg_soc);
|
||||
_flight_time_filter.reset(_state_of_charge / fmaxf(_soc_derivative_filter.getState(), FLT_EPSILON));
|
||||
}
|
||||
|
||||
time_remaining_s = _state_of_charge / fmaxf(_soc_derivative_filter.getState(), FLT_EPSILON);
|
||||
_flight_time_filter.update(time_remaining_s);
|
||||
_last_flight_time_update = timestamp;
|
||||
_prev_state_of_charge = state_of_charge;
|
||||
// Remaining time estimation only possible with capacity
|
||||
if (_params.capacity > 0.f) {
|
||||
const float remaining_capacity_mah = _state_of_charge * _params.capacity;
|
||||
const float current_ma = fmaxf(_current_average_filter_a.getState() * 1e3f, FLT_EPSILON);
|
||||
time_remaining_s = remaining_capacity_mah / current_ma * 3600.f;
|
||||
}
|
||||
|
||||
return _flight_time_filter.getState();
|
||||
return time_remaining_s;
|
||||
}
|
||||
|
||||
void Battery::updateParams()
|
||||
@@ -411,7 +404,6 @@ void Battery::updateParams()
|
||||
param_get(_param_handles.crit_thr, &_params.crit_thr);
|
||||
param_get(_param_handles.emergen_thr, &_params.emergen_thr);
|
||||
param_get(_param_handles.bat_avrg_current, &_params.bat_avrg_current);
|
||||
param_get(_param_handles.bat_avrg_soc, &_params.bat_avrg_soc);
|
||||
|
||||
if (n_cells != _params.n_cells) {
|
||||
_internal_resistance_initialized = false;
|
||||
|
||||
@@ -124,7 +124,6 @@ protected:
|
||||
param_t emergen_thr;
|
||||
param_t source;
|
||||
param_t bat_avrg_current;
|
||||
param_t bat_avrg_soc;
|
||||
} _param_handles{};
|
||||
|
||||
struct {
|
||||
@@ -138,7 +137,6 @@ protected:
|
||||
float emergen_thr;
|
||||
int32_t source;
|
||||
float bat_avrg_current;
|
||||
float bat_avrg_soc;
|
||||
} _params{};
|
||||
|
||||
const int _index;
|
||||
@@ -153,7 +151,7 @@ private:
|
||||
uint8_t determineWarning(float state_of_charge);
|
||||
uint16_t determineFaults();
|
||||
void computeScale();
|
||||
float computeRemainingTime(const float state_of_charge);
|
||||
float computeRemainingTime(float current_a);
|
||||
|
||||
uORB::Subscription _vehicle_status_sub{ORB_ID(vehicle_status)};
|
||||
uORB::SubscriptionData<flight_phase_estimation_s> _flight_phase_estimation_sub{ORB_ID(flight_phase_estimation)};
|
||||
@@ -168,8 +166,6 @@ private:
|
||||
float _voltage_v{0.f};
|
||||
AlphaFilter<float> _ocv_filter_v;
|
||||
AlphaFilter<float> _cell_voltage_filter_v;
|
||||
AlphaFilter<float> _soc_derivative_filter;
|
||||
AlphaFilter<float> _flight_time_filter;
|
||||
float _current_a{-1};
|
||||
AlphaFilter<float>
|
||||
_current_average_filter_a; ///< averaging filter for current. For FW, it is the current in level flight.
|
||||
@@ -178,14 +174,12 @@ private:
|
||||
float _discharged_mah_loop{0.f};
|
||||
float _state_of_charge_volt_based{-1.f}; // [0,1]
|
||||
float _state_of_charge{-1.f}; // [0,1]
|
||||
float _prev_state_of_charge{-1.f}; // [0, 1]
|
||||
float _scale{1.f};
|
||||
uint8_t _warning{battery_status_s::WARNING_NONE};
|
||||
hrt_abstime _last_timestamp{0};
|
||||
bool _armed{false};
|
||||
bool _vehicle_status_is_fw{false};
|
||||
hrt_abstime _last_unconnected_timestamp{0};
|
||||
hrt_abstime _last_flight_time_update{0};
|
||||
|
||||
// Internal Resistance estimation
|
||||
void updateInternalResistanceEstimation(const float voltage_v, const float current_a);
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
# Test internal resistance and flight time estimator on flight logs
|
||||
# run with:
|
||||
# python3 battery_estimation_replay.py -f <(required)pathToLogFile>
|
||||
# -r <(optional)useLoggedRemainingForFlightTimeEstimation>
|
||||
# -c <(optional)#batteryCells>
|
||||
# -u <(optional)fullCellVoltage>
|
||||
# -e <(optional)emptyCellVoltage>
|
||||
# -l <(optional)forgettingFactor>
|
||||
# -s <(optional)averageSocConsumption>
|
||||
# -t <(optional)remainingFilterTimeConstant>
|
||||
# -k <(optional)flightTimeFilterTimeConstant>
|
||||
# Note: Can lead to slightly different results than the online estimation due to the fact that
|
||||
# the log frequency of the voltage and current are not the same as the online frequency.
|
||||
|
||||
from pyulog import ULog
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import argparse
|
||||
|
||||
def getData(log, topic_name, variable_name, instance=0):
|
||||
for elem in log.data_list:
|
||||
if elem.name == topic_name and instance == elem.multi_id:
|
||||
return elem.data[variable_name]
|
||||
return np.array([])
|
||||
|
||||
def us2s(time_us):
|
||||
return time_us * 1e-6
|
||||
|
||||
def getParam(log, param_name):
|
||||
if param_name in log.initial_parameters:
|
||||
return log.initial_parameters[param_name]
|
||||
else:
|
||||
print(f"Parameter {param_name} not found in log.")
|
||||
return None
|
||||
|
||||
def alphaFilter(filter_state, alpha, sample):
|
||||
return filter_state + alpha * (sample - filter_state)
|
||||
|
||||
def updateRLS(theta, P, x, voltage, current, lam):
|
||||
gamma = P @ x / (lam + x.T @ P @ x)
|
||||
error = voltage - x.T @ theta
|
||||
data_cov = x.T @ P @ x
|
||||
theta_temp = theta + gamma * error
|
||||
P_temp = (P - gamma @ x.T @ P) / lam
|
||||
if (abs(np.linalg.norm(P)) < abs(np.linalg.norm(P_temp))):
|
||||
theta_corr = np.array([voltage + theta[1] * current, theta[1]]) # Correct OCV estimation
|
||||
P_corr = P
|
||||
return theta_corr, P_corr, error[0][0], data_cov[0][0], 0, 0
|
||||
return theta_temp, P_temp, error[0][0], data_cov[0][0], gamma[0][0], gamma[1][0]
|
||||
|
||||
def main(log_name, logged_remaining, n_cells, full_cell, empty_cell, lam, soc_consumption_avg, time_constant_ocv_derivative, time_constant_flight_time):
|
||||
log = ULog(log_name)
|
||||
|
||||
log_n_cells = getParam(log, 'BAT1_N_CELLS')
|
||||
log_full_cell = getParam(log, 'BAT1_V_CHARGED')
|
||||
log_empty_cell = getParam(log, 'BAT1_V_EMPTY')
|
||||
log_capacity = getParam(log, 'BAT1_CAPACITY')
|
||||
|
||||
# Debug information
|
||||
print("\nParameters:")
|
||||
print(f"Extracted from log - BAT1_N_CELLS: {log_n_cells}, BAT1_V_CHARGED: {log_full_cell}, BAT1_V_EMPTY: {log_empty_cell}, BAT1_CAPACITY: {log_capacity}")
|
||||
|
||||
# Use log parameters unless overridden
|
||||
if n_cells is None:
|
||||
n_cells = log_n_cells
|
||||
else:
|
||||
print(f"Using override for n_cells: {n_cells}")
|
||||
if full_cell is None:
|
||||
full_cell = log_full_cell
|
||||
else:
|
||||
print(f"Using override for full_cell: {full_cell}")
|
||||
if empty_cell is None:
|
||||
empty_cell = log_empty_cell
|
||||
else:
|
||||
print(f"Using override for empty_cell: {empty_cell}")
|
||||
|
||||
# Debug information for final parameter values
|
||||
print(f"Using parameters - n_cells: {n_cells}, full_cell: {full_cell}, empty_cell: {empty_cell}")
|
||||
|
||||
# Extract data from log
|
||||
timestamps = us2s(getData(log, 'battery_status', 'timestamp'))
|
||||
current = getData(log, 'battery_status', 'current_a')
|
||||
current_average = getData(log, 'battery_status', 'current_average_a')
|
||||
voltage = getData(log, 'battery_status', 'voltage_v')
|
||||
remaining = getData(log, 'battery_status', 'remaining')
|
||||
remaining_flight_time = getData(log, 'battery_status', 'time_remaining_s')
|
||||
|
||||
if not timestamps.size or not current.size or not current_average.size or not voltage.size or not remaining.size:
|
||||
print("Error: Incomplete data.")
|
||||
return
|
||||
|
||||
## Internal resistance estimation
|
||||
|
||||
# Containers for plotting
|
||||
ocv_est = np.zeros_like(timestamps)
|
||||
ocv_est_filtered = np.zeros_like(timestamps)
|
||||
r_est = np.zeros_like(timestamps)
|
||||
error_hist = np.zeros_like(timestamps)
|
||||
v_est = np.zeros_like(timestamps)
|
||||
internal_resistance_stable = np.zeros_like(timestamps)
|
||||
cov_norm = np.zeros_like(timestamps)
|
||||
r_cov = np.zeros_like(timestamps)
|
||||
ocv_cov = np.zeros_like(timestamps)
|
||||
mixed_cov = np.zeros_like(timestamps)
|
||||
data_cov_hist = np.zeros_like(timestamps)
|
||||
gamma_ocv_hist = np.zeros_like(timestamps)
|
||||
gamma_r_hist = np.zeros_like(timestamps)
|
||||
remaining_est = np.zeros_like(timestamps)
|
||||
remaining_est_filtered = np.zeros_like(timestamps)
|
||||
|
||||
# Initializations
|
||||
theta = np.array([[voltage[0] + 0.005 * n_cells * current[0]], [0.005 * n_cells]]) # Initial VOC and R
|
||||
P = np.diag([1.2 * n_cells, 0.1 * n_cells]) # Initial covariance
|
||||
error = 0
|
||||
sample_interval = timestamps[1] - timestamps[0]
|
||||
alpha_ocv = sample_interval / (sample_interval + 1)
|
||||
internal_resistance_stable[-1] = 0.005
|
||||
|
||||
for index in range(len(current)):
|
||||
# RLS algorithm
|
||||
x = np.array([[1.0], [-current[index]]]) # Input vector
|
||||
theta, P, error, data_cov, gamma_ocv_hist[index], gamma_r_hist[index] = updateRLS(theta, P, x, voltage[index], current[index], lam) # Run RLS
|
||||
|
||||
# Save steps for plotting
|
||||
ocv_est[index] = theta[0][0]
|
||||
if (index == 0):
|
||||
ocv_est_filtered[index] = ocv_est[index]
|
||||
else:
|
||||
ocv_est_filtered[index] = alphaFilter(ocv_est_filtered[index - 1], alpha_ocv, ocv_est[index])
|
||||
r_est[index] = theta[1][0]
|
||||
error_hist[index] = error
|
||||
v_est[index] = (x.T @ theta)[0][0]
|
||||
cov_norm[index] = abs(np.linalg.norm(P))
|
||||
ocv_cov[index] = P[0][0]
|
||||
r_cov[index] = P[1][1]
|
||||
mixed_cov[index] = P[0][1]
|
||||
data_cov_hist[index] = data_cov
|
||||
internal_resistance_stable[index] = max(r_est[index]/n_cells, 0.001)
|
||||
remaining_est[index] = np.interp((voltage[index] + internal_resistance_stable[index] * n_cells * current[index]) / n_cells, [empty_cell, full_cell], [0, 1])
|
||||
remaining_est_filtered[index] = np.interp(ocv_est_filtered[index] / n_cells, [empty_cell, full_cell], [0, 1])
|
||||
|
||||
## Flight time estimation
|
||||
|
||||
# Containers for plotting
|
||||
flight_time_estimated = np.zeros_like(timestamps)
|
||||
flight_time_estimated_filtered = np.zeros_like(timestamps)
|
||||
remaining_consumption = np.zeros_like(timestamps)
|
||||
remaining_consumption_average = np.zeros_like(timestamps)
|
||||
|
||||
# Initializations
|
||||
alpha_ocv_derivative = (sample_interval) / (sample_interval + time_constant_ocv_derivative)
|
||||
alpha_flight_time = (sample_interval) / (sample_interval + time_constant_flight_time)
|
||||
if (logged_remaining):
|
||||
state_of_charge = remaining
|
||||
else:
|
||||
state_of_charge = remaining_est_filtered
|
||||
flight_time_estimated[0] = remaining[0] / soc_consumption_avg
|
||||
flight_time_estimated_filtered[0] = flight_time_estimated[0]
|
||||
remaining_consumption_average[0] = soc_consumption_avg
|
||||
dt = 0
|
||||
|
||||
for index in range(len(current)):
|
||||
if index == 0:
|
||||
continue
|
||||
dt = timestamps[index] - timestamps[index - 1]
|
||||
remaining_consumption[index] = (state_of_charge[index - 1] - state_of_charge[index]) / dt
|
||||
if state_of_charge[index] > 0.99:
|
||||
remaining_consumption_average[index] = soc_consumption_avg
|
||||
flight_time_estimated[index] = state_of_charge[index] / soc_consumption_avg
|
||||
else:
|
||||
remaining_consumption_average[index] = np.clip(alphaFilter(remaining_consumption_average[index - 1], alpha_ocv_derivative, remaining_consumption[index]), 0.0005, 0.1)
|
||||
flight_time_estimated[index] = state_of_charge[index] / remaining_consumption_average[index]
|
||||
flight_time_estimated_filtered[index] = alphaFilter(flight_time_estimated_filtered[index - 1], alpha_flight_time, flight_time_estimated[index])
|
||||
|
||||
### Plot data
|
||||
|
||||
## Internal resistance estimation plots
|
||||
print("\nInternal Resistance estimation:")
|
||||
print("Internal Resistance mean (per cell): ", np.mean(r_est) / n_cells)
|
||||
sumFig = plt.figure("Battery Estimation with RLS")
|
||||
|
||||
volt = plt.subplot(2, 3, 1)
|
||||
volt.plot(timestamps, voltage, label='Measured voltage')
|
||||
volt.plot(timestamps, v_est, label='Estimated voltage')
|
||||
volt.plot(timestamps, np.array(voltage) + np.array(internal_resistance_stable) * np.array(current) * n_cells, label='OCV estimate')
|
||||
volt.plot(timestamps, ocv_est_filtered, label='OCV estimate smoothed')
|
||||
volt.plot(timestamps, np.full_like(current, full_cell * n_cells), label='100% SOC')
|
||||
volt.set_title("Measured Voltage vs Estimated voltage vs Estimated Open circuit voltage [voltage]")
|
||||
volt.legend()
|
||||
|
||||
intR = plt.subplot(2, 3, 2)
|
||||
intR.plot(timestamps, np.array(r_est) * 1000 / n_cells, label='Internal resistance estimate')
|
||||
intR.set_title("Internal resistance estimate (per cell) [mOhm]")
|
||||
intR.legend()
|
||||
|
||||
soc = plt.subplot(2, 3, 3)
|
||||
soc.plot(timestamps, remaining, label='SoC logged')
|
||||
soc.plot(timestamps, remaining_est, label='SoC with estimator')
|
||||
soc.plot(timestamps, remaining_est_filtered, label='SoC with estimator smoothed')
|
||||
soc.set_title("State of charge")
|
||||
soc.legend()
|
||||
|
||||
curr = plt.subplot(2, 3, 4)
|
||||
curr.plot(timestamps, current, label='Measured current')
|
||||
curr.set_title("Measured Current [A]")
|
||||
curr.legend()
|
||||
|
||||
err = plt.subplot(2, 3, 5)
|
||||
err.plot(timestamps, error_hist, label='$Error$')
|
||||
err.set_title("Voltage estimation error [voltage]")
|
||||
err.legend()
|
||||
|
||||
cov = plt.subplot(2, 3, 6)
|
||||
cov.plot(timestamps, cov_norm, label = 'Covariance norm')
|
||||
cov.set_title("Covariance norm")
|
||||
cov.legend()
|
||||
|
||||
# # SoC estimation plots
|
||||
# socFig = plt.figure("SoC estimation")
|
||||
# plt.plot(timestamps, np.interp((np.array(voltage) + np.array(current) * 0.005 * n_cells) / n_cells, [empty_cell, full_cell], [0, 1]), label='SoC with default $R_{int}$')
|
||||
# plt.plot(timestamps, remaining, label='SoC logged')
|
||||
# plt.plot(timestamps, np.interp((np.array(voltage) + np.array(internal_resistance_stable) * n_cells * np.array(current)) / n_cells, [empty_cell, full_cell], [0, 1]), label='SoC with estimator')
|
||||
# # plt.plot(timestamps, np.convolve(np.interp((np.array(voltage) + np.array(internal_resistance_stable) * n_cells * np.array(current)) / n_cells, [empty_cell, full_cell], [0, 1]), np.ones(500)/500, mode='full')[0:np.size(timestamps)], label='SoC with estimator smoothed')
|
||||
# # plt.plot(timestamps, np.interp((np.array(voltage) + np.array(current) * 0.0009 * n_cells) / n_cells, [empty_cell, full_cell], [0, 1]), label='SoC with $R_{int}$ measured beforehand')
|
||||
# # plt.plot(timestamps, np.interp(ocv_est/n_cells, [empty_cell, full_cell], [0, 1]), label='SoC with VOC estimate')
|
||||
# plt.legend()
|
||||
|
||||
# # Covariance plots
|
||||
# covFig = plt.figure("Covariance plots")
|
||||
# covR = plt.subplot(2, 2, 1)
|
||||
# covR.plot(timestamps, r_cov, label = 'r_cov')
|
||||
# covR.set_title("Internal resistance covariance")
|
||||
# covR.legend()
|
||||
# covVOC = plt.subplot(2, 2, 2)
|
||||
# covVOC.plot(timestamps, ocv_cov, label = 'ocv_cov')
|
||||
# covVOC.set_title("Open circuit covariance")
|
||||
# covVOC.legend()
|
||||
# covM = plt.subplot(2, 2, 3)
|
||||
# covM.plot(timestamps, mixed_cov, label = 'mixed_cov')
|
||||
# covM.set_title("Mixed covariance")
|
||||
# covM.legend()
|
||||
# covM = plt.subplot(2, 2, 4)
|
||||
# covM.plot(timestamps, cov_norm, label = 'cov_norm')
|
||||
# covM.set_title("Covariance norm")
|
||||
# covM.legend()
|
||||
|
||||
# # Gain plots
|
||||
# gainFig = plt.figure("Gain plots")
|
||||
# gainVoc = plt.subplot(1, 2, 1)
|
||||
# gainVoc.plot(timestamps, gamma_ocv_hist, label = 'gain_voc')
|
||||
# gainVoc.set_title("Gain VOC")
|
||||
# gainVoc.legend()
|
||||
# gainR = plt.subplot(1, 2, 2)
|
||||
# gainR.plot(timestamps, gamma_r_hist, label = 'gain_r')
|
||||
# gainR.set_title("Gain R")
|
||||
# gainR.legend()
|
||||
|
||||
## Flight time estimation plots
|
||||
print("\nFlight time estimation:")
|
||||
if (logged_remaining):
|
||||
print("Using logged remaining for flight time estimation")
|
||||
else:
|
||||
print("Using estimated remaining for flight time estimation")
|
||||
print(f"Alpha for remaining derivative: {round(alpha_ocv_derivative, 6)}, Alpha for flight time estimation: {round(alpha_flight_time, 6)}")
|
||||
print(f"In {round(timestamps[-1] - timestamps[0])} seconds, the remaining flight time estimate was reduced by {round(flight_time_estimated_filtered[0] - flight_time_estimated_filtered[-1])} seconds.")
|
||||
|
||||
flightTimeEstFig = plt.figure("Flight Time Estimation")
|
||||
|
||||
flightTime = plt.subplot(2, 2, 1)
|
||||
if (remaining_flight_time.size):
|
||||
flightTime.plot(timestamps, remaining_flight_time, label='Flight time remaining (logged)')
|
||||
flightTime.plot(timestamps, flight_time_estimated, label='Flight time remaining (estimated)')
|
||||
flightTime.plot(timestamps, flight_time_estimated_filtered, label='Flight time remaining (estimated, filtered)')
|
||||
flightTime.set_title("Flight time remaining [s]")
|
||||
flightTime.legend()
|
||||
|
||||
remainingPlot = plt.subplot(2, 2, 2)
|
||||
remainingPlot.plot(timestamps, remaining, label='Remaining (logged)')
|
||||
remainingPlot.plot(timestamps, remaining_est, label='Remaining (estimated)')
|
||||
remainingPlot.plot(timestamps, remaining_est_filtered, label='Remaining (estimated, filtered)')
|
||||
remainingPlot.set_title("Remaining [0, 1]")
|
||||
remainingPlot.legend()
|
||||
|
||||
currentPlot = plt.subplot(2, 2, 3)
|
||||
currentPlot.plot(timestamps, current, label='Current')
|
||||
currentPlot.plot(timestamps, current_average, label='Current average')
|
||||
currentPlot.set_title("Current [A]")
|
||||
currentPlot.legend()
|
||||
|
||||
remainingPlot = plt.subplot(2, 2, 4)
|
||||
remainingPlot.plot(timestamps, remaining_consumption, label='Remaining consumption')
|
||||
remainingPlot.plot(timestamps, remaining_consumption_average, label='Remaining consumption average')
|
||||
remainingPlot.set_title("Remaining consumption [1/s]")
|
||||
remainingPlot.legend()
|
||||
|
||||
plt.show()
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Estimate battery parameters from ulog file.')
|
||||
parser.add_argument('-f', type = str, required = True, help = 'Full path to ulog file')
|
||||
parser.add_argument('-r', type = bool, required = False, help = 'Use logged remaining value for flight time estimation', default = False)
|
||||
parser.add_argument('-c', type = float, required = False, help = 'Number of cells in battery', default = None )
|
||||
parser.add_argument('-u', type = float, required = False, help = 'Full cell voltage', default = None )
|
||||
parser.add_argument('-e', type = float, required = False, help = 'Empty cell voltage', default = None )
|
||||
parser.add_argument('-l', type = float, required = False, help = 'Forgetting factor', default = 0.99 )
|
||||
parser.add_argument('-s', type = float, required = False, help = 'Average SoC consumption per second', default = 0.001)
|
||||
parser.add_argument('-t', type = int, required = False, help = 'Time constant for alpha filter of remaining derivative', default = 50 )
|
||||
parser.add_argument('-k', type = int, required = False, help = 'Time constant for alpha filter of remaining flight time estimation', default = 5 )
|
||||
args = parser.parse_args()
|
||||
main(args.f, args.r, args.c, args.u, args.e, args.l, args.s, args.t, args.k)
|
||||
@@ -0,0 +1,208 @@
|
||||
# Test internal resistance estimator on flight logs
|
||||
# run with:
|
||||
# python3 int_res_est_replay.py -f <pathToLogFile> -c <#batteryCells>
|
||||
# -u <(optional)fullCellVoltage> -e <(optional)emptyCellVoltage> -l <(optional)forgettingFactor> -d <(optional)filterMeasurements>
|
||||
# Note: Can lead to slightly different results than the online estimation due to the fact that
|
||||
# the log frequency of the voltage and current are not the same as the online frequency.
|
||||
|
||||
from pyulog import ULog
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import argparse
|
||||
|
||||
def getData(log, topic_name, variable_name, instance=0):
|
||||
for elem in log.data_list:
|
||||
if elem.name == topic_name and instance == elem.multi_id:
|
||||
return elem.data[variable_name]
|
||||
return np.array([])
|
||||
|
||||
def us2s(time_us):
|
||||
return time_us * 1e-6
|
||||
|
||||
def getParam(log, param_name):
|
||||
if param_name in log.initial_parameters:
|
||||
return log.initial_parameters[param_name]
|
||||
else:
|
||||
print(f"Parameter {param_name} not found in log.")
|
||||
return None
|
||||
|
||||
def rls_update(theta, P, x, V, I, lam):
|
||||
gamma = P @ x / (lam + x.T @ P @ x)
|
||||
error = V - x.T @ theta
|
||||
data_cov = x.T @ P @ x
|
||||
theta_temp = theta + gamma * error
|
||||
P_temp = (P - gamma @ x.T @ P) / lam
|
||||
if (abs(np.linalg.norm(P)) < abs(np.linalg.norm(P_temp))):
|
||||
theta_corr = np.array([V + theta[1] * I, theta[1]]) # Correct OCV estimation
|
||||
P_corr = P
|
||||
return theta_corr, P_corr, error, data_cov, 0, 0
|
||||
return theta_temp, P_temp, error, data_cov, gamma[0], gamma[1]
|
||||
|
||||
def main(log_name, n_cells, full_cell, empty_cell, lam):
|
||||
log = ULog(log_name)
|
||||
|
||||
log_n_cells = getParam(log, 'BAT1_N_CELLS')
|
||||
log_full_cell = getParam(log, 'BAT1_V_CHARGED')
|
||||
log_empty_cell = getParam(log, 'BAT1_V_EMPTY')
|
||||
|
||||
# Debug information
|
||||
print(f"Extracted from log - BAT1_N_CELLS: {log_n_cells}, BAT1_V_CHARGED: {log_full_cell}, BAT1_V_EMPTY: {log_empty_cell}")
|
||||
|
||||
# Use log parameters unless overridden
|
||||
if n_cells is None:
|
||||
n_cells = log_n_cells
|
||||
else:
|
||||
print(f"Using override for n_cells: {n_cells}")
|
||||
if full_cell is None:
|
||||
full_cell = log_full_cell
|
||||
else:
|
||||
print(f"Using override for full_cell: {full_cell}")
|
||||
if empty_cell is None:
|
||||
empty_cell = log_empty_cell
|
||||
else:
|
||||
print(f"Using override for empty_cell: {empty_cell}")
|
||||
|
||||
# Debug information for final parameter values
|
||||
print(f"Using parameters - n_cells: {n_cells}, full_cell: {full_cell}, empty_cell: {empty_cell}")
|
||||
|
||||
timestamps = us2s(getData(log, 'battery_status', 'timestamp'))
|
||||
I = getData(log, 'battery_status', 'current_a')
|
||||
V = getData(log, 'battery_status', 'voltage_v')
|
||||
remaining = getData(log, 'battery_status', 'remaining')
|
||||
|
||||
if not timestamps.size or not I.size or not V.size or not remaining.size:
|
||||
print("Error: Incomplete data.")
|
||||
return
|
||||
|
||||
# Initializations
|
||||
theta = np.array([[V[0] + 0.005 * n_cells * I[0]], [0.005 * n_cells]]) # Initial VOC and R
|
||||
P = np.diag([1.2 * n_cells, 0.1 * n_cells]) # Initial covariance
|
||||
error = 0
|
||||
|
||||
# For plotting
|
||||
VOC_est = np.zeros_like(I)
|
||||
R_est = np.zeros_like(I)
|
||||
error_hist = np.zeros_like(I)
|
||||
v_est = np.zeros_like(I)
|
||||
internal_resistance_stable = np.zeros_like(I)
|
||||
internal_resistance_stable[-1] = 0.005
|
||||
cov_norm = np.zeros_like(I)
|
||||
r_cov = np.zeros_like(I)
|
||||
ocv_cov = np.zeros_like(I)
|
||||
mixed_cov = np.zeros_like(I)
|
||||
data_cov_hist = np.zeros_like(I)
|
||||
gamma_voc_hist = np.zeros_like(I)
|
||||
gamma_r_hist = np.zeros_like(I)
|
||||
|
||||
for index in range(len(I)):
|
||||
# RLS algorithm
|
||||
x = np.array([[1.0], [-I[index]]]) # Input vector
|
||||
theta, P, error, data_cov, gamma_voc_hist[index], gamma_r_hist[index] = rls_update(theta, P, x, V[index], I[index], lam) # Run RLS
|
||||
|
||||
# For plotting
|
||||
VOC_est[index] = theta[0][0]
|
||||
R_est[index] = theta[1][0]
|
||||
error_hist[index] = error
|
||||
v_est[index] = x.T @ theta
|
||||
cov_norm[index] = abs(np.linalg.norm(P))
|
||||
ocv_cov[index] = P[0][0]
|
||||
r_cov[index] = P[1][1]
|
||||
mixed_cov[index] = P[0][1]
|
||||
data_cov_hist[index] = data_cov
|
||||
internal_resistance_stable[index] = max(R_est[index]/n_cells, 0.001)
|
||||
|
||||
# Plot data
|
||||
print("Internal Resistance mean (per cell): ", np.mean(R_est) / n_cells)
|
||||
|
||||
# Summary plot
|
||||
sumFig = plt.figure("Battery Estimation with RLS")
|
||||
|
||||
volt = plt.subplot(2, 3, 1)
|
||||
volt.plot(timestamps, V, label='Measured voltage')
|
||||
volt.plot(timestamps, v_est, label='Estimated voltage')
|
||||
volt.plot(timestamps, np.array(V) + np.array(internal_resistance_stable) * np.array(I) * n_cells, label='OCV estimate')
|
||||
ocv_smoothed = np.convolve(np.array(V) + np.array(internal_resistance_stable) * np.array(I) * n_cells, np.ones(30)/30, mode='full')[0:np.size(timestamps)]
|
||||
ocv_smoothed[0:30] = ocv_smoothed[31]
|
||||
volt.plot(timestamps, ocv_smoothed, label='OCV estimate smoothed')
|
||||
volt.plot(timestamps, np.full_like(I, full_cell * n_cells), label='100% SOC')
|
||||
volt.set_title("Measured Voltage vs Estimated voltage vs Estimated Open circuit voltage [V]")
|
||||
volt.legend()
|
||||
|
||||
intR = plt.subplot(2, 3, 2)
|
||||
intR.plot(timestamps, np.array(R_est) * 1000 / n_cells, label='Internal resistance estimate')
|
||||
intR.set_title("Internal resistance estimate (per cell) [mOhm]")
|
||||
intR.legend()
|
||||
|
||||
soc = plt.subplot(2, 3, 3)
|
||||
soc.plot(timestamps, remaining, label='SoC logged')
|
||||
soc.plot(timestamps, np.interp((np.array(V) + np.array(internal_resistance_stable) * n_cells * np.array(I)) / n_cells, [empty_cell, full_cell], [0, 1]), label='SoC with estimator')
|
||||
soc.plot(timestamps, np.interp(ocv_smoothed / n_cells, [empty_cell, full_cell], [0, 1]), label='SoC with estimator smoothed')
|
||||
soc.set_title("State of charge")
|
||||
soc.legend()
|
||||
|
||||
curr = plt.subplot(2, 3, 4)
|
||||
curr.plot(timestamps, I, label='Measured current')
|
||||
curr.set_title("Measured Current [A]")
|
||||
curr.legend()
|
||||
|
||||
err = plt.subplot(2, 3, 5)
|
||||
err.plot(timestamps, error_hist, label='$Error$')
|
||||
err.set_title("Voltage estimation error [V]")
|
||||
err.legend()
|
||||
|
||||
cov = plt.subplot(2, 3, 6)
|
||||
cov.plot(timestamps, cov_norm, label = 'Covariance norm')
|
||||
cov.set_title("Covariance norm")
|
||||
cov.legend()
|
||||
|
||||
# # SoC estimation plots
|
||||
# socFig = plt.figure("SoC estimation")
|
||||
# plt.plot(timestamps, np.interp((np.array(V) + np.array(I) * 0.005 * n_cells) / n_cells, [empty_cell, full_cell], [0, 1]), label='SoC with default $R_{int}$')
|
||||
# plt.plot(timestamps, remaining, label='SoC logged')
|
||||
# plt.plot(timestamps, np.interp((np.array(V) + np.array(internal_resistance_stable) * n_cells * np.array(I)) / n_cells, [empty_cell, full_cell], [0, 1]), label='SoC with estimator')
|
||||
# # plt.plot(timestamps, np.convolve(np.interp((np.array(V) + np.array(internal_resistance_stable) * n_cells * np.array(I)) / n_cells, [empty_cell, full_cell], [0, 1]), np.ones(500)/500, mode='full')[0:np.size(timestamps)], label='SoC with estimator smoothed')
|
||||
# # plt.plot(timestamps, np.interp((np.array(V) + np.array(I) * 0.0009 * n_cells) / n_cells, [empty_cell, full_cell], [0, 1]), label='SoC with $R_{int}$ measured beforehand')
|
||||
# # plt.plot(timestamps, np.interp(VOC_est/n_cells, [empty_cell, full_cell], [0, 1]), label='SoC with VOC estimate')
|
||||
# plt.legend()
|
||||
|
||||
# # Covariance plots
|
||||
# covFig = plt.figure("Covariance plots")
|
||||
# covR = plt.subplot(2, 2, 1)
|
||||
# covR.plot(timestamps, r_cov, label = 'r_cov')
|
||||
# covR.set_title("Internal resistance covariance")
|
||||
# covR.legend()
|
||||
# covVOC = plt.subplot(2, 2, 2)
|
||||
# covVOC.plot(timestamps, ocv_cov, label = 'ocv_cov')
|
||||
# covVOC.set_title("Open circuit covariance")
|
||||
# covVOC.legend()
|
||||
# covM = plt.subplot(2, 2, 3)
|
||||
# covM.plot(timestamps, mixed_cov, label = 'mixed_cov')
|
||||
# covM.set_title("Mixed covariance")
|
||||
# covM.legend()
|
||||
# covM = plt.subplot(2, 2, 4)
|
||||
# covM.plot(timestamps, cov_norm, label = 'cov_norm')
|
||||
# covM.set_title("Covariance norm")
|
||||
# covM.legend()
|
||||
|
||||
# # Gain plots
|
||||
# gainFig = plt.figure("Gain plots")
|
||||
# gainVoc = plt.subplot(1, 2, 1)
|
||||
# gainVoc.plot(timestamps, gamma_voc_hist, label = 'gain_voc')
|
||||
# gainVoc.set_title("Gain VOC")
|
||||
# gainVoc.legend()
|
||||
# gainR = plt.subplot(1, 2, 2)
|
||||
# gainR.plot(timestamps, gamma_r_hist, label = 'gain_r')
|
||||
# gainR.set_title("Gain R")
|
||||
# gainR.legend()
|
||||
|
||||
plt.show()
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Estimate battery parameters from ulog file.')
|
||||
parser.add_argument('-f', type = str, required = True, help = 'Full path to ulog file')
|
||||
parser.add_argument('-c', type = float, required = False, help = 'Number of cells in battery')
|
||||
parser.add_argument('-u', type = float, required = False, default = None, help = 'Full cell voltage')
|
||||
parser.add_argument('-e', type = float, required = False, default = None, help = 'Empty cell voltage')
|
||||
parser.add_argument('-l', type = float, required = False, default = 0.99, help = 'Forgetting factor')
|
||||
args = parser.parse_args()
|
||||
main(args.f, args.c, args.u, args.e, args.l)
|
||||
@@ -178,16 +178,3 @@ parameters:
|
||||
decimal: 2
|
||||
increment: 0.1
|
||||
default: 15
|
||||
|
||||
BAT_AVRG_SOC:
|
||||
description:
|
||||
short: Expected average SoC derivative during flight.
|
||||
long: |
|
||||
This value is used to initialize the in-flight average SoC derivative,
|
||||
which in turn is used for estimating remaining flight time and RTL triggering.
|
||||
type: float
|
||||
min: 0
|
||||
max: 100
|
||||
decimal: 4
|
||||
increment: 0.0001
|
||||
default: 0.001
|
||||
|
||||
@@ -615,7 +615,7 @@ PARAM_DEFINE_INT32(NAV_RCL_ACT, 2);
|
||||
* Specify modes in which RC loss is ignored and the failsafe action not triggered.
|
||||
*
|
||||
* @min 0
|
||||
* @max 31
|
||||
* @max 7
|
||||
* @bit 0 Mission
|
||||
* @bit 1 Hold
|
||||
* @bit 2 Offboard
|
||||
@@ -623,6 +623,20 @@ PARAM_DEFINE_INT32(NAV_RCL_ACT, 2);
|
||||
*/
|
||||
PARAM_DEFINE_INT32(COM_RCL_EXCEPT, 0);
|
||||
|
||||
/**
|
||||
* Datalink loss exceptions
|
||||
*
|
||||
* Specify modes in which datalink loss is ignored and the failsafe action not triggered.
|
||||
*
|
||||
* @min 0
|
||||
* @max 7
|
||||
* @bit 0 Mission
|
||||
* @bit 1 Hold
|
||||
* @bit 2 Offboard
|
||||
* @group Commander
|
||||
*/
|
||||
PARAM_DEFINE_INT32(COM_DLL_EXCEPT, 0);
|
||||
|
||||
/**
|
||||
* Set the actuator failure failsafe mode
|
||||
*
|
||||
|
||||
@@ -452,8 +452,9 @@ void Failsafe::checkStateAndMode(const hrt_abstime &time_us, const State &state,
|
||||
|
||||
// Do not enter failsafe while doing a vtol takeoff after the vehicle has started a transition and before it reaches the loiter
|
||||
// altitude. The vtol takeoff navigaton mode will set mission_finished to true as soon as the loiter is established
|
||||
const bool ignore_link_failsafe = state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_AUTO_VTOL_TAKEOFF
|
||||
&& in_forward_flight && !state.mission_finished;
|
||||
const bool ignore_any_link_loss_vtol_takeoff_fixedwing = state.user_intended_mode ==
|
||||
vehicle_status_s::NAVIGATION_STATE_AUTO_VTOL_TAKEOFF
|
||||
&& in_forward_flight && !state.mission_finished;
|
||||
|
||||
// Manual control (RC) loss
|
||||
if (!status_flags.manual_control_signal_lost) {
|
||||
@@ -470,8 +471,10 @@ void Failsafe::checkStateAndMode(const hrt_abstime &time_us, const State &state,
|
||||
const bool rc_loss_ignored_takeoff = (state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_AUTO_TAKEOFF ||
|
||||
state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_AUTO_VTOL_TAKEOFF)
|
||||
&& (_param_com_rcl_except.get() & (int)ManualControlLossExceptionBits::Hold);
|
||||
|
||||
const bool rc_loss_ignored = rc_loss_ignored_mission || rc_loss_ignored_loiter || rc_loss_ignored_offboard ||
|
||||
rc_loss_ignored_takeoff || ignore_link_failsafe || _manual_control_lost_at_arming;
|
||||
rc_loss_ignored_takeoff || ignore_any_link_loss_vtol_takeoff_fixedwing
|
||||
|| _manual_control_lost_at_arming;
|
||||
|
||||
if (_param_com_rc_in_mode.get() != int32_t(RcInMode::StickInputDisabled) && !rc_loss_ignored) {
|
||||
CHECK_FAILSAFE(status_flags, manual_control_signal_lost,
|
||||
@@ -479,10 +482,23 @@ void Failsafe::checkStateAndMode(const hrt_abstime &time_us, const State &state,
|
||||
}
|
||||
|
||||
// GCS connection loss
|
||||
const bool gcs_connection_loss_ignored = state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_AUTO_LAND ||
|
||||
state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_AUTO_PRECLAND || ignore_link_failsafe;
|
||||
const bool dll_loss_ignored_land = state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_AUTO_LAND
|
||||
|| state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_AUTO_PRECLAND;
|
||||
|
||||
if (_param_nav_dll_act.get() != int32_t(gcs_connection_loss_failsafe_mode::Disabled) && !gcs_connection_loss_ignored) {
|
||||
const bool dll_loss_ignored_mission = state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_AUTO_MISSION
|
||||
&& (_param_com_dll_except.get() & (int)DatalinkLossExceptionBits::Mission);
|
||||
const bool dll_loss_ignored_loiter = state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_AUTO_LOITER
|
||||
&& (_param_com_dll_except.get() & (int)DatalinkLossExceptionBits::Hold);
|
||||
const bool dll_loss_ignored_offboard = state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_OFFBOARD
|
||||
&& (_param_com_dll_except.get() & (int)DatalinkLossExceptionBits::Offboard);
|
||||
const bool dll_loss_ignored_takeoff = (state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_AUTO_TAKEOFF ||
|
||||
state.user_intended_mode == vehicle_status_s::NAVIGATION_STATE_AUTO_VTOL_TAKEOFF)
|
||||
&& (_param_com_dll_except.get() & (int)DatalinkLossExceptionBits::Hold);
|
||||
|
||||
const bool dll_loss_ignored = dll_loss_ignored_mission || dll_loss_ignored_loiter || dll_loss_ignored_offboard ||
|
||||
dll_loss_ignored_takeoff || ignore_any_link_loss_vtol_takeoff_fixedwing || dll_loss_ignored_land;
|
||||
|
||||
if (_param_nav_dll_act.get() != int32_t(gcs_connection_loss_failsafe_mode::Disabled) && !dll_loss_ignored) {
|
||||
CHECK_FAILSAFE(status_flags, gcs_connection_lost,
|
||||
fromNavDllOrRclActParam(_param_nav_dll_act.get()).causedBy(Cause::GCSConnectionLoss));
|
||||
}
|
||||
|
||||
@@ -59,6 +59,12 @@ private:
|
||||
Offboard = (1 << 2)
|
||||
};
|
||||
|
||||
enum class DatalinkLossExceptionBits : int32_t {
|
||||
Mission = (1 << 0),
|
||||
Hold = (1 << 1),
|
||||
Offboard = (1 << 2)
|
||||
};
|
||||
|
||||
// COM_LOW_BAT_ACT parameter values
|
||||
enum class LowBatteryAction : int32_t {
|
||||
Warning = 0, // Warning
|
||||
@@ -193,6 +199,7 @@ private:
|
||||
(ParamInt<px4::params::NAV_DLL_ACT>) _param_nav_dll_act,
|
||||
(ParamInt<px4::params::NAV_RCL_ACT>) _param_nav_rcl_act,
|
||||
(ParamInt<px4::params::COM_RCL_EXCEPT>) _param_com_rcl_except,
|
||||
(ParamInt<px4::params::COM_DLL_EXCEPT>) _param_com_dll_except,
|
||||
(ParamInt<px4::params::COM_RC_IN_MODE>) _param_com_rc_in_mode,
|
||||
(ParamInt<px4::params::COM_POSCTL_NAVL>) _param_com_posctl_navl,
|
||||
(ParamInt<px4::params::GF_ACTION>) _param_gf_action,
|
||||
|
||||
@@ -40,7 +40,7 @@ parameters:
|
||||
short: Control allocation method
|
||||
long: |
|
||||
Selects the algorithm and desaturation method.
|
||||
If set to Automtic, the selection is based on the airframe (CA_AIRFRAME).
|
||||
If set to Automatic, the selection is based on the airframe (CA_AIRFRAME).
|
||||
type: enum
|
||||
values:
|
||||
0: Pseudo-inverse with output clipping
|
||||
|
||||
@@ -34,10 +34,14 @@
|
||||
/**
|
||||
* Dataman storage backend
|
||||
*
|
||||
* If the board supports persistent storage (i.e., the KConfig variable DATAMAN_PERSISTENT_STORAGE is set),
|
||||
* the 'Default storage' backend uses a file on persistent storage. If not supported, this backend uses
|
||||
* non-persistent storage in RAM.
|
||||
*
|
||||
* @group System
|
||||
* @value -1 Disabled
|
||||
* @value 0 default (SD card)
|
||||
* @value 1 RAM (not persistent)
|
||||
* @value -1 Dataman disabled
|
||||
* @value 0 Default storage
|
||||
* @value 1 RAM storage
|
||||
* @boolean
|
||||
* @reboot_required true
|
||||
*/
|
||||
|
||||
@@ -108,7 +108,7 @@ void AuxGlobalPosition::update(Ekf &ekf, const estimator::imuSample &imu_delayed
|
||||
bool reset = false;
|
||||
|
||||
if (!fused && !ekf.isOtherSourceOfHorizontalPositionAidingThan(ekf.control_status_flags().aux_gpos)) {
|
||||
ekf.resetGlobalPositionTo(sample.latitude, sample.longitude, sample.altitude_amsl, pos_var, sq(sample.epv));
|
||||
ekf.resetHorizontalPositionTo(sample.latitude, sample.longitude, Vector2f(aid_src.observation_variance));
|
||||
ekf.resetAidSourceStatusZeroInnovation(aid_src);
|
||||
reset = true;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,12 @@ void Ekf::controlMagFusion(const imuSample &imu_sample)
|
||||
_mag_lpf.reset(mag_sample.mag);
|
||||
_mag_counter = 1;
|
||||
|
||||
if (!_control_status.flags.in_air) {
|
||||
// Assume that a reset on the ground is caused by a change in mag calibration
|
||||
// Clear alignment to force a clean reset
|
||||
_control_status.flags.yaw_align = false;
|
||||
}
|
||||
|
||||
} else {
|
||||
_mag_lpf.update(mag_sample.mag);
|
||||
_mag_counter++;
|
||||
|
||||
@@ -46,18 +46,17 @@ bool Ekf::fuseOptFlow(VectorState &H, const bool update_terrain)
|
||||
{
|
||||
const auto state_vector = _state.vector();
|
||||
|
||||
_innov_check_fail_status.flags.reject_optflow_X = (_aid_src_optical_flow.test_ratio[0] > 1.f);
|
||||
_innov_check_fail_status.flags.reject_optflow_Y = (_aid_src_optical_flow.test_ratio[1] > 1.f);
|
||||
|
||||
// if either axis fails we abort the fusion
|
||||
if (_aid_src_optical_flow.innovation_rejected) {
|
||||
_innov_check_fail_status.flags.reject_optflow_X = true;
|
||||
_innov_check_fail_status.flags.reject_optflow_Y = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// fuse observation axes sequentially
|
||||
for (uint8_t index = 0; index <= 1; index++) {
|
||||
if (index == 0) {
|
||||
// everything was already computed above
|
||||
// everything was already computed before
|
||||
|
||||
} else if (index == 1) {
|
||||
// recalculate innovation variance because state covariances have changed due to previous fusion (linearise using the same initial state for all axes)
|
||||
@@ -69,6 +68,16 @@ bool Ekf::fuseOptFlow(VectorState &H, const bool update_terrain)
|
||||
const Vector3f flow_gyro_corrected = _flow_sample_delayed.gyro_rate - _flow_gyro_bias;
|
||||
_aid_src_optical_flow.innovation[1] = predictFlow(flow_gyro_corrected)(1) - static_cast<float>
|
||||
(_aid_src_optical_flow.observation[1]);
|
||||
|
||||
// recalculate the test ratio as the measurement jacobian is highly non linear
|
||||
// when close to the ground (singularity at 0) and the innovation can suddenly become really
|
||||
// large and destabilize the filter
|
||||
_aid_src_optical_flow.test_ratio[1] = sq(_aid_src_optical_flow.innovation[1]) / (sq(
|
||||
_params.flow_innov_gate) * _aid_src_optical_flow.innovation_variance[1]);
|
||||
|
||||
if (_aid_src_optical_flow.test_ratio[1] > 1.f) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (_aid_src_optical_flow.innovation_variance[index] < _aid_src_optical_flow.observation_variance[index]) {
|
||||
@@ -91,6 +100,9 @@ bool Ekf::fuseOptFlow(VectorState &H, const bool update_terrain)
|
||||
_fault_status.flags.bad_optflow_X = false;
|
||||
_fault_status.flags.bad_optflow_Y = false;
|
||||
|
||||
_innov_check_fail_status.flags.reject_optflow_X = (_aid_src_optical_flow.test_ratio[0] > 1.f);
|
||||
_innov_check_fail_status.flags.reject_optflow_Y = (_aid_src_optical_flow.test_ratio[1] > 1.f);
|
||||
|
||||
_aid_src_optical_flow.time_last_fuse = _time_delayed_us;
|
||||
_aid_src_optical_flow.fused = true;
|
||||
|
||||
@@ -103,7 +115,7 @@ bool Ekf::fuseOptFlow(VectorState &H, const bool update_terrain)
|
||||
return true;
|
||||
}
|
||||
|
||||
float Ekf::predictFlowRange() const
|
||||
float Ekf::predictFlowHagl() const
|
||||
{
|
||||
// calculate the sensor position relative to the IMU
|
||||
const Vector3f pos_offset_body = _params.flow_pos_body - _params.imu_pos_body;
|
||||
@@ -113,17 +125,19 @@ float Ekf::predictFlowRange() const
|
||||
|
||||
// calculate the height above the ground of the optical flow camera. Since earth frame is NED
|
||||
// a positive offset in earth frame leads to a smaller height above the ground.
|
||||
const float height_above_gnd_est = getHagl() - pos_offset_earth(2);
|
||||
const float height_above_gnd_est = fabsf(getHagl() - pos_offset_earth(2));
|
||||
|
||||
// Never return a really small value to avoid generating insanely large flow innovations
|
||||
// that could destabilize the filter
|
||||
constexpr float min_hagl = 1e-2f;
|
||||
|
||||
return fmaxf(height_above_gnd_est, min_hagl);
|
||||
}
|
||||
float Ekf::predictFlowRange() const
|
||||
{
|
||||
// calculate range from focal point to centre of image
|
||||
float flow_range = height_above_gnd_est / _R_to_earth(2, 2); // absolute distance to the frame region in view
|
||||
|
||||
// avoid the flow prediction singularity at range = 0
|
||||
if (fabsf(flow_range) < FLT_EPSILON) {
|
||||
flow_range = signNoZero(flow_range) * FLT_EPSILON;
|
||||
}
|
||||
|
||||
return flow_range;
|
||||
// absolute distance to the frame region in view
|
||||
return predictFlowHagl() / _R_to_earth(2, 2);
|
||||
}
|
||||
|
||||
Vector2f Ekf::predictFlow(const Vector3f &flow_gyro) const
|
||||
@@ -142,9 +156,9 @@ Vector2f Ekf::predictFlow(const Vector3f &flow_gyro) const
|
||||
const Vector2f vel_body = _state.quat_nominal.rotateVectorInverse(vel_rel_earth).xy();
|
||||
|
||||
// calculate range from focal point to centre of image
|
||||
const float range = predictFlowRange();
|
||||
const float scale = _R_to_earth(2, 2) / predictFlowHagl();
|
||||
|
||||
return Vector2f(vel_body(1) / range, -vel_body(0) / range);
|
||||
return Vector2f(vel_body(1) * scale, -vel_body(0) * scale);
|
||||
}
|
||||
|
||||
float Ekf::calcOptFlowMeasVar(const flowSample &flow_sample) const
|
||||
|
||||
@@ -791,6 +791,7 @@ private:
|
||||
// calculate optical flow body angular rate compensation
|
||||
void calcOptFlowBodyRateComp(const flowSample &flow_sample);
|
||||
|
||||
float predictFlowHagl() const;
|
||||
float predictFlowRange() const;
|
||||
Vector2f predictFlow(const Vector3f &flow_gyro) const;
|
||||
|
||||
|
||||
+33
-33
@@ -2197,26 +2197,26 @@ bool EKF2::UpdateExtVisionSample(ekf2_timestamps_s &ekf2_timestamps)
|
||||
const Vector3f ev_odom_vel(ev_odom.velocity);
|
||||
const Vector3f ev_odom_vel_var(ev_odom.velocity_variance);
|
||||
|
||||
bool velocity_frame_valid = false;
|
||||
|
||||
switch (ev_odom.velocity_frame) {
|
||||
case vehicle_odometry_s::VELOCITY_FRAME_NED:
|
||||
ev_data.vel_frame = VelocityFrame::LOCAL_FRAME_NED;
|
||||
velocity_frame_valid = true;
|
||||
break;
|
||||
|
||||
case vehicle_odometry_s::VELOCITY_FRAME_FRD:
|
||||
ev_data.vel_frame = VelocityFrame::LOCAL_FRAME_FRD;
|
||||
velocity_frame_valid = true;
|
||||
break;
|
||||
|
||||
case vehicle_odometry_s::VELOCITY_FRAME_BODY_FRD:
|
||||
ev_data.vel_frame = VelocityFrame::BODY_FRAME_FRD;
|
||||
velocity_frame_valid = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ev_odom_vel.isAllFinite()) {
|
||||
bool velocity_frame_valid = false;
|
||||
|
||||
switch (ev_odom.velocity_frame) {
|
||||
case vehicle_odometry_s::VELOCITY_FRAME_NED:
|
||||
ev_data.vel_frame = VelocityFrame::LOCAL_FRAME_NED;
|
||||
velocity_frame_valid = true;
|
||||
break;
|
||||
|
||||
case vehicle_odometry_s::VELOCITY_FRAME_FRD:
|
||||
ev_data.vel_frame = VelocityFrame::LOCAL_FRAME_FRD;
|
||||
velocity_frame_valid = true;
|
||||
break;
|
||||
|
||||
case vehicle_odometry_s::VELOCITY_FRAME_BODY_FRD:
|
||||
ev_data.vel_frame = VelocityFrame::BODY_FRAME_FRD;
|
||||
velocity_frame_valid = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (velocity_frame_valid) {
|
||||
ev_data.vel = ev_odom_vel;
|
||||
|
||||
@@ -2241,21 +2241,21 @@ bool EKF2::UpdateExtVisionSample(ekf2_timestamps_s &ekf2_timestamps)
|
||||
const Vector3f ev_odom_pos(ev_odom.position);
|
||||
const Vector3f ev_odom_pos_var(ev_odom.position_variance);
|
||||
|
||||
bool position_frame_valid = false;
|
||||
|
||||
switch (ev_odom.pose_frame) {
|
||||
case vehicle_odometry_s::POSE_FRAME_NED:
|
||||
ev_data.pos_frame = PositionFrame::LOCAL_FRAME_NED;
|
||||
position_frame_valid = true;
|
||||
break;
|
||||
|
||||
case vehicle_odometry_s::POSE_FRAME_FRD:
|
||||
ev_data.pos_frame = PositionFrame::LOCAL_FRAME_FRD;
|
||||
position_frame_valid = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ev_odom_pos.isAllFinite()) {
|
||||
bool position_frame_valid = false;
|
||||
|
||||
switch (ev_odom.pose_frame) {
|
||||
case vehicle_odometry_s::POSE_FRAME_NED:
|
||||
ev_data.pos_frame = PositionFrame::LOCAL_FRAME_NED;
|
||||
position_frame_valid = true;
|
||||
break;
|
||||
|
||||
case vehicle_odometry_s::POSE_FRAME_FRD:
|
||||
ev_data.pos_frame = PositionFrame::LOCAL_FRAME_FRD;
|
||||
position_frame_valid = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (position_frame_valid) {
|
||||
ev_data.pos = ev_odom_pos;
|
||||
|
||||
|
||||
@@ -71,3 +71,22 @@ TEST(FlowGenerated, distBottom0y)
|
||||
sym::ComputeFlowYInnovVarAndH(state.vector(), P, R, FLT_EPSILON, &innov_var, &H);
|
||||
EXPECT_GT(innov_var, 1e12);
|
||||
}
|
||||
|
||||
TEST(FlowGenerated, distBottomNeg)
|
||||
{
|
||||
// GIVEN: a small negative distance to the ground (singularity)
|
||||
StateSample state{};
|
||||
state.quat_nominal = Quatf();
|
||||
state.pos(2) = 1e-3f;
|
||||
|
||||
const float R = sq(radians(sq(0.5f)));
|
||||
SquareMatrixState P = createRandomCovarianceMatrix();
|
||||
|
||||
VectorState H;
|
||||
Vector2f innov_var;
|
||||
sym::ComputeFlowXyInnovVarAndHx(state.vector(), P, R, FLT_EPSILON, &innov_var, &H);
|
||||
EXPECT_GT(innov_var(0), 1e6);
|
||||
EXPECT_GT(innov_var(1), 1e6);
|
||||
sym::ComputeFlowYInnovVarAndH(state.vector(), P, R, FLT_EPSILON, &innov_var(1), &H);
|
||||
EXPECT_GT(innov_var(1), 1e6);
|
||||
}
|
||||
|
||||
@@ -179,10 +179,16 @@ float FixedwingRateControl::get_airspeed_and_update_scaling()
|
||||
*
|
||||
* Forcing the scaling to this value allows reasonable handheld tests.
|
||||
*/
|
||||
const float airspeed_constrained = constrain(constrain(airspeed, _param_fw_airspd_stall.get(),
|
||||
_param_fw_airspd_max.get()), 0.1f, 1000.0f);
|
||||
|
||||
_airspeed_scaling = (_param_fw_arsp_scale_en.get()) ? (_param_fw_airspd_trim.get() / airspeed_constrained) : 1.0f;
|
||||
|
||||
if (_param_fw_arsp_scale_en.get()) {
|
||||
const float min_airspeed = math::max(_param_fw_airspd_stall.get(), 0.1f);
|
||||
const float airspeed_constrained = math::max(airspeed, min_airspeed);
|
||||
_airspeed_scaling = _param_fw_airspd_trim.get() / airspeed_constrained;
|
||||
|
||||
} else {
|
||||
_airspeed_scaling = 1.0f;
|
||||
}
|
||||
|
||||
return airspeed;
|
||||
}
|
||||
|
||||
@@ -319,7 +319,8 @@ void InternalCombustionEngineControl::controlEngineStartup(const hrt_abstime now
|
||||
|
||||
_ignition_on = true;
|
||||
_throttle_control = _param_ice_strt_thr.get();
|
||||
_choke_control = now < _state_start_time + (choke_duration + ignition_delay) * 1_s ? 1.f : 0.f;
|
||||
_choke_control = now < _state_start_time + (choke_duration + ignition_delay) * 1_s ? math::constrain(
|
||||
_param_ice_strt_choke.get(), 0.f, 1.f) : 0.f;
|
||||
_starter_engine_control = now > _state_start_time + (ignition_delay * 1_s) ? 1.f : 0.f;
|
||||
const hrt_abstime cycle_timeout_duration = (ignition_delay + choke_duration + starter_duration) * 1_s;
|
||||
|
||||
@@ -355,7 +356,7 @@ int InternalCombustionEngineControl::print_usage(const char *reason)
|
||||
PRINT_MODULE_DESCRIPTION(
|
||||
R"DESCR_STR(
|
||||
### Description
|
||||
|
||||
|
||||
The module controls internal combustion engine (ICE) features including:
|
||||
ignition (on/off), throttle and choke level, starter engine delay, and user request.
|
||||
|
||||
@@ -389,18 +390,18 @@ The ICE is implemented with a (4) state machine:
|
||||

|
||||
|
||||
The state machine:
|
||||
|
||||
|
||||
- Checks if [Rpm.msg](../msg_docs/Rpm.md) is updated to know if the engine is running
|
||||
- Allows for user inputs from:
|
||||
- AUX{N}
|
||||
- Arming state in [VehicleStatus.msg](../msg_docs/VehicleStatus.md)
|
||||
|
||||
The module publishes [InternalCombustionEngineControl.msg](../msg_docs/InternalCombustionEngineControl.md).
|
||||
|
||||
|
||||
The architecture is as shown below:
|
||||
|
||||

|
||||
|
||||
|
||||
<a id="internal_combustion_engine_control_usage"></a>
|
||||
)DESCR_STR");
|
||||
|
||||
|
||||
@@ -152,7 +152,8 @@ private:
|
||||
(ParamFloat<px4::params::ICE_STRT_THR>) _param_ice_strt_thr,
|
||||
(ParamInt<px4::params::ICE_STOP_CHOKE>) _param_ice_stop_choke,
|
||||
(ParamFloat<px4::params::ICE_THR_SLEW>) _param_ice_thr_slew,
|
||||
(ParamFloat<px4::params::ICE_IGN_DELAY>) _param_ice_ign_delay
|
||||
(ParamFloat<px4::params::ICE_IGN_DELAY>) _param_ice_ign_delay,
|
||||
(ParamFloat<px4::params::ICE_STRT_CHOKE>) _param_ice_strt_choke
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -118,3 +118,16 @@ parameters:
|
||||
decimal: 1
|
||||
increment: 0.1
|
||||
default: 0
|
||||
|
||||
ICE_STRT_CHOKE:
|
||||
description:
|
||||
short: Choke position during startup, 100 % for fully choked.
|
||||
long: |
|
||||
If the engine does not fire fully choked, open the choke slightly to allow for more air into the fuel/air mixture by reducing a few percentages until fire.
|
||||
type: float
|
||||
unit: norm
|
||||
min: 0
|
||||
max: 1
|
||||
decimal: 0
|
||||
increment: 0.01
|
||||
default: 1.0
|
||||
|
||||
@@ -318,7 +318,7 @@ void RTL::setRtlTypeAndDestination()
|
||||
|
||||
uint8_t safe_point_index{0U};
|
||||
|
||||
if (_param_rtl_type.get() != 2) {
|
||||
if (_param_rtl_type.get() != 2 && _param_rtl_type.get() != 4) {
|
||||
// check the closest allowed destination.
|
||||
DestinationType destination_type{DestinationType::DESTINATION_TYPE_HOME};
|
||||
PositionYawSetpoint rtl_position;
|
||||
@@ -566,6 +566,14 @@ void RTL::init_rtl_mission_type()
|
||||
} else {
|
||||
new_rtl_mission_type = RtlType::RTL_MISSION_FAST_REVERSE;
|
||||
}
|
||||
|
||||
} else if (_param_rtl_type.get() == 4) {
|
||||
if (hasMissionLandStart() && reverseIsFurther()) {
|
||||
new_rtl_mission_type = RtlType::RTL_MISSION_FAST;
|
||||
|
||||
} else {
|
||||
new_rtl_mission_type = RtlType::RTL_MISSION_FAST_REVERSE;
|
||||
}
|
||||
}
|
||||
|
||||
if (_set_rtl_mission_type == new_rtl_mission_type) {
|
||||
@@ -630,6 +638,12 @@ bool RTL::hasMissionLandStart() const
|
||||
&& _navigator->get_mission_result()->valid;
|
||||
}
|
||||
|
||||
bool RTL::reverseIsFurther() const
|
||||
{
|
||||
return (_mission_sub.get().land_start_index - _mission_sub.get().current_seq) < _mission_sub.get().current_seq;
|
||||
}
|
||||
|
||||
|
||||
bool RTL::hasVtolLandApproach(const PositionYawSetpoint &rtl_position) const
|
||||
{
|
||||
return readVtolLandApproaches(rtl_position).isAnyApproachValid();
|
||||
|
||||
@@ -105,6 +105,14 @@ private:
|
||||
*/
|
||||
bool hasMissionLandStart() const;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Check whether there are more waypoints between current waypoint
|
||||
* and the takeoff location than the end/land location.
|
||||
* @return true if the reverse is more items away.
|
||||
*/
|
||||
bool reverseIsFurther() const;
|
||||
|
||||
/**
|
||||
* @brief function to call regularly to do background work
|
||||
*/
|
||||
|
||||
@@ -112,6 +112,7 @@ PARAM_DEFINE_FLOAT(RTL_MIN_DIST, 10.0f);
|
||||
* @value 1 Return to closest safe point other than home (mission landing pattern or rally point), via direct path. If no mission landing or rally points are defined return home via direct path. Always chose closest safe landing point if vehicle is a VTOL in hover mode.
|
||||
* @value 2 Return to a planned mission landing, if available, using the mission path, else return to home via the reverse mission path. Do not consider rally points.
|
||||
* @value 3 Return via direct path to closest destination: home, start of mission landing pattern or safe point. If the destination is a mission landing pattern, follow the pattern to land.
|
||||
* @value 4 Return to the planned mission landing, or to home via the reverse mission path, whichever is closer by counting waypoints. Do not consider rally points.
|
||||
* @group Return Mode
|
||||
*/
|
||||
PARAM_DEFINE_INT32(RTL_TYPE, 0);
|
||||
|
||||
@@ -191,6 +191,13 @@ bool VehicleMagnetometer::ParametersUpdate(bool force)
|
||||
|
||||
if (calibration_updated) {
|
||||
_last_calibration_update = hrt_absolute_time();
|
||||
|
||||
for (int instance = 0; instance < MAX_SENSOR_COUNT; instance++) {
|
||||
// avoid mixing data currected using old calibration
|
||||
_timestamp_sample_sum[instance] = 0;
|
||||
_data_sum[instance].zero();
|
||||
_data_sum_count[instance] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
project(px4_gz_plugins)
|
||||
|
||||
if(NOT DEFINED ENV{GZ_DISTRO} OR NOT "$ENV{GZ_DISTRO}" STREQUAL "harmonic")
|
||||
find_package(gz-transport NAMES gz-transport14 gz-transport13 gz-transport12)
|
||||
find_package(gz-transport NAMES gz-transport14 gz-transport13)
|
||||
find_package(gz-sim NAMES gz-sim9 gz-sim8)
|
||||
find_package(gz-sensors NAMES gz-sensors9 gz-sensors8)
|
||||
find_package(gz-plugin NAMES gz-plugin3 gz-plugin2 COMPONENTS register)
|
||||
|
||||
@@ -41,7 +41,7 @@ parameters:
|
||||
short: Control allocation method
|
||||
long: |
|
||||
Selects the algorithm and desaturation method.
|
||||
If set to Automtic, the selection is based on the airframe (CA_AIRFRAME).
|
||||
If set to Automatic, the selection is based on the airframe (CA_AIRFRAME).
|
||||
type: enum
|
||||
values:
|
||||
0: Pseudo-inverse with output clipping
|
||||
|
||||
@@ -87,17 +87,27 @@ struct SendTopicsSubs {
|
||||
|
||||
uint32_t num_payload_sent{};
|
||||
|
||||
void init();
|
||||
bool init(uxrSession *session, uxrStreamId reliable_out_stream_id, uxrStreamId reliable_in_stream_id, uxrStreamId best_effort_in_stream_id, uxrObjectId participant_id, const char *client_namespace);
|
||||
void update(uxrSession *session, uxrStreamId reliable_out_stream_id, uxrStreamId best_effort_stream_id, uxrObjectId participant_id, const char *client_namespace);
|
||||
void reset();
|
||||
};
|
||||
|
||||
void SendTopicsSubs::init() {
|
||||
bool SendTopicsSubs::init(uxrSession *session, uxrStreamId reliable_out_stream_id, uxrStreamId reliable_in_stream_id, uxrStreamId best_effort_in_stream_id, uxrObjectId participant_id, const char *client_namespace) {
|
||||
bool ret = true;
|
||||
for (unsigned idx = 0; idx < sizeof(send_subscriptions)/sizeof(send_subscriptions[0]); ++idx) {
|
||||
fds[idx].fd = orb_subscribe(send_subscriptions[idx].orb_meta);
|
||||
fds[idx].events = POLLIN;
|
||||
orb_set_interval(fds[idx].fd, UXRCE_DEFAULT_POLL_RATE);
|
||||
if (fds[idx].events == 0) {
|
||||
fds[idx].fd = orb_subscribe(send_subscriptions[idx].orb_meta);
|
||||
fds[idx].events = POLLIN;
|
||||
orb_set_interval(fds[idx].fd, UXRCE_DEFAULT_POLL_RATE);
|
||||
}
|
||||
|
||||
if (!create_data_writer(session, reliable_out_stream_id, participant_id, static_cast<ORB_ID>(send_subscriptions[idx].orb_meta->o_id), client_namespace, send_subscriptions[idx].topic,
|
||||
send_subscriptions[idx].message_version,
|
||||
send_subscriptions[idx].dds_type_name, send_subscriptions[idx].data_writer)) {
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void SendTopicsSubs::reset() {
|
||||
@@ -119,12 +129,6 @@ void SendTopicsSubs::update(uxrSession *session, uxrStreamId reliable_out_stream
|
||||
if (fds[idx].revents & POLLIN) {
|
||||
// Topic updated, copy data and send
|
||||
orb_copy(send_subscriptions[idx].orb_meta, fds[idx].fd, &topic_data);
|
||||
if (send_subscriptions[idx].data_writer.id == UXR_INVALID_ID) {
|
||||
// data writer not created yet
|
||||
create_data_writer(session, reliable_out_stream_id, participant_id, static_cast<ORB_ID>(send_subscriptions[idx].orb_meta->o_id), client_namespace, send_subscriptions[idx].topic,
|
||||
send_subscriptions[idx].message_version,
|
||||
send_subscriptions[idx].dds_type_name, send_subscriptions[idx].data_writer);
|
||||
}
|
||||
|
||||
if (send_subscriptions[idx].data_writer.id != UXR_INVALID_ID) {
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ publications:
|
||||
- topic: /fmu/out/airspeed_validated
|
||||
type: px4_msgs::msg::AirspeedValidated
|
||||
|
||||
- topic: /fmu/out/vtol_vehicle_status
|
||||
type: px4_msgs::msg::VtolVehicleStatus
|
||||
|
||||
- topic: /fmu/out/home_position
|
||||
type: px4_msgs::msg::HomePosition
|
||||
|
||||
|
||||
@@ -366,6 +366,11 @@ bool UxrceddsClient::setupSession(uxrSession *session)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_subs->init(session, _reliable_out, reliable_in, best_effort_in, _participant_id, _client_namespace)) {
|
||||
PX4_ERR("subs init failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// create VehicleCommand replier
|
||||
if (_num_of_repliers < MAX_NUM_REPLIERS) {
|
||||
if (add_replier(new VehicleCommandSrv(session, _reliable_out, reliable_in, _participant_id, _client_namespace,
|
||||
@@ -388,11 +393,6 @@ void UxrceddsClient::deleteSession(uxrSession *session)
|
||||
_session_created = false;
|
||||
}
|
||||
|
||||
if (_subs_initialized) {
|
||||
_subs->reset();
|
||||
_subs_initialized = false;
|
||||
}
|
||||
|
||||
_last_payload_tx_rate = 0;
|
||||
_timesync.reset_filter();
|
||||
}
|
||||
@@ -650,9 +650,6 @@ void UxrceddsClient::run()
|
||||
int poll_error_counter = 0;
|
||||
resetConnectivityCounters();
|
||||
|
||||
_subs->init();
|
||||
_subs_initialized = true;
|
||||
|
||||
while (!should_exit() && _connected) {
|
||||
perf_begin(_loop_perf);
|
||||
perf_count(_loop_interval_perf);
|
||||
|
||||
@@ -197,7 +197,6 @@ private:
|
||||
bool _connected{false};
|
||||
bool _session_created{false};
|
||||
bool _timesync_converged{false};
|
||||
bool _subs_initialized{false};
|
||||
|
||||
Timesync _timesync{timesync_status_s::SOURCE_PROTOCOL_DDS};
|
||||
|
||||
|
||||
@@ -427,9 +427,16 @@ VtolAttitudeControl::Run()
|
||||
_vehicle_thrust_setpoint0_pub.publish(_thrust_setpoint_0);
|
||||
_vehicle_thrust_setpoint1_pub.publish(_thrust_setpoint_1);
|
||||
|
||||
// Advertise/Publish vtol vehicle status
|
||||
_vtol_vehicle_status.timestamp = hrt_absolute_time();
|
||||
_vtol_vehicle_status_pub.publish(_vtol_vehicle_status);
|
||||
// Advertise/publish vtol vehicle status -- immediately if changed, otherwise at 1 Hz
|
||||
const bool vtol_vehicle_status_changed =
|
||||
(_vtol_vehicle_status.vehicle_vtol_state != _prev_published_vtol_vehicle_status.vehicle_vtol_state) ||
|
||||
(_vtol_vehicle_status.fixed_wing_system_failure != _prev_published_vtol_vehicle_status.fixed_wing_system_failure);
|
||||
|
||||
if (vtol_vehicle_status_changed || hrt_elapsed_time(&_prev_published_vtol_vehicle_status.timestamp) >= 1_s) {
|
||||
_vtol_vehicle_status.timestamp = hrt_absolute_time();
|
||||
_vtol_vehicle_status_pub.publish(_vtol_vehicle_status);
|
||||
_prev_published_vtol_vehicle_status = _vtol_vehicle_status;
|
||||
}
|
||||
|
||||
// Publish flaps/spoiler setpoint with configured deflection in Hover if in Auto.
|
||||
// In Manual always published in FW rate controller, and in Auto FW in FW Position Controller.
|
||||
|
||||
@@ -205,6 +205,7 @@ private:
|
||||
vehicle_local_position_setpoint_s _local_pos_sp{};
|
||||
vehicle_status_s _vehicle_status{};
|
||||
vtol_vehicle_status_s _vtol_vehicle_status{};
|
||||
vtol_vehicle_status_s _prev_published_vtol_vehicle_status{};
|
||||
float _home_position_z{NAN};
|
||||
|
||||
float _air_density{atmosphere::kAirDensitySeaLevelStandardAtmos}; // [kg/m^3]
|
||||
|
||||
@@ -7,14 +7,18 @@
|
||||
{
|
||||
"model": "iris",
|
||||
"vehicle": "iris",
|
||||
"test_filter": "[multicopter],[offboard][offboard_attitude]",
|
||||
"test_filter": "[multicopter],[offboard],[offboard_attitude]",
|
||||
"timeout_min": 10
|
||||
},
|
||||
{
|
||||
"model": "iris",
|
||||
"vehicle": "iris",
|
||||
"test_filter": "[offboard_attitude]",
|
||||
"timeout_min": 10
|
||||
"timeout_min": 10,
|
||||
"env": {
|
||||
"PX4_PARAM_EKF2_EN": 0,
|
||||
"PX4_PARAM_ATT_EN": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "standard_vtol",
|
||||
|
||||
+2
-3
@@ -1,11 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
|
||||
force_color = False
|
||||
|
||||
class color(Enum):
|
||||
PURPLE = '\033[95m'
|
||||
@@ -22,7 +21,7 @@ class color(Enum):
|
||||
|
||||
|
||||
def colorize(text: str, c: color) -> str:
|
||||
if _supports_color():
|
||||
if force_color or _supports_color():
|
||||
return str(c.value) + text + color.RESET.value
|
||||
else:
|
||||
return text
|
||||
+51
-8
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import queue
|
||||
import time
|
||||
import os
|
||||
@@ -9,6 +7,7 @@ import shutil
|
||||
import threading
|
||||
import errno
|
||||
from typing import Any, Dict, List, TextIO, Optional
|
||||
from .logger_helper import colorize, color
|
||||
|
||||
PX4_SITL_GAZEBO_PATH = "Tools/simulation/gazebo-classic/sitl_gazebo-classic"
|
||||
|
||||
@@ -96,7 +95,7 @@ class Runner:
|
||||
|
||||
def wait(self, timeout_min: float) -> Optional[int]:
|
||||
try:
|
||||
return self.process.wait(timeout=timeout_min*60)
|
||||
return self.process.wait(timeout=timeout_min * 60)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("Timeout of {} min{} reached, stopping...".
|
||||
format(timeout_min, "s" if timeout_min > 1 else ""))
|
||||
@@ -152,11 +151,12 @@ class Runner:
|
||||
class Px4Runner(Runner):
|
||||
def __init__(self, workspace_dir: str, log_dir: str,
|
||||
model: str, case: str, speed_factor: float,
|
||||
debugger: str, verbose: bool, build_dir: str):
|
||||
debugger: str, verbose: bool, build_dir: str,
|
||||
rootfs_base_dirname: str):
|
||||
super().__init__(log_dir, model, case, verbose)
|
||||
self.name = "px4"
|
||||
self.cwd = os.path.join(workspace_dir, build_dir,
|
||||
"tmp_mavsdk_tests/rootfs")
|
||||
rootfs_base_dirname, "rootfs")
|
||||
self.cmd = "nice"
|
||||
self.args = [
|
||||
"--20",
|
||||
@@ -214,6 +214,16 @@ class Px4Runner(Runner):
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
def get_output_line(self) -> Optional[str]:
|
||||
line = super().get_output_line()
|
||||
if line is not None:
|
||||
# colorize warnings and errors
|
||||
if 'ERROR' in line:
|
||||
line = colorize(line, color.RED)
|
||||
elif 'WARN' in line:
|
||||
line = colorize(line, color.YELLOW)
|
||||
return line
|
||||
|
||||
|
||||
class GzserverRunner(Runner):
|
||||
def __init__(self,
|
||||
@@ -248,12 +258,19 @@ class GzserverRunner(Runner):
|
||||
for line in f.readlines():
|
||||
if 'Connected to gazebo master' in line:
|
||||
return True
|
||||
time.sleep(float(timeout_s)/float(steps))
|
||||
time.sleep(float(timeout_s) / float(steps))
|
||||
|
||||
print("gzserver did not connect within {}s"
|
||||
.format(timeout_s))
|
||||
return False
|
||||
|
||||
def get_output_line(self) -> Optional[str]:
|
||||
line = super().get_output_line()
|
||||
# Some gazebo versions don't seem to reset the color, so always add a RESET
|
||||
if line is not None:
|
||||
line = line + color.RESET.value
|
||||
return line
|
||||
|
||||
|
||||
class GzmodelspawnRunner(Runner):
|
||||
def __init__(self,
|
||||
@@ -303,7 +320,7 @@ class GzmodelspawnRunner(Runner):
|
||||
for _ in range(steps):
|
||||
returncode = self.process.poll()
|
||||
if returncode is None:
|
||||
time.sleep(float(timeout_s)/float(steps))
|
||||
time.sleep(float(timeout_s) / float(steps))
|
||||
continue
|
||||
|
||||
with open(self.log_filename, 'r') as f:
|
||||
@@ -337,7 +354,7 @@ class GzclientRunner(Runner):
|
||||
"--verbose"]
|
||||
|
||||
|
||||
class TestRunner(Runner):
|
||||
class TestRunnerMavsdk(Runner):
|
||||
def __init__(self,
|
||||
workspace_dir: str,
|
||||
log_dir: str,
|
||||
@@ -359,3 +376,29 @@ class TestRunner(Runner):
|
||||
"--url", mavlink_connection,
|
||||
"--speed-factor", str(speed_factor),
|
||||
case]
|
||||
|
||||
class TestRunnerRos(Runner):
|
||||
def __init__(self,
|
||||
workspace_dir: str,
|
||||
log_dir: str,
|
||||
model: str,
|
||||
case: str,
|
||||
verbose: bool,
|
||||
ros_package_build_dir: str):
|
||||
super().__init__(log_dir, model, case, verbose)
|
||||
self.name = "integration_tests"
|
||||
self.cwd = workspace_dir
|
||||
self.cmd = "nice"
|
||||
self.args = ["-17",
|
||||
os.path.join(
|
||||
ros_package_build_dir,
|
||||
"integration_tests"),
|
||||
"--gtest_filter="+case, "--gtest_color=yes"]
|
||||
|
||||
def get_output_line(self) -> Optional[str]:
|
||||
line = super().get_output_line()
|
||||
if line is not None:
|
||||
# colorize assertion failures & errors
|
||||
if 'Failure' in line or '[ERROR]' in line or '[FATAL]' in line:
|
||||
line = colorize(line, color.RED)
|
||||
return line
|
||||
@@ -0,0 +1,569 @@
|
||||
import datetime
|
||||
import fnmatch
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, List, NoReturn, TextIO, Optional
|
||||
from types import FrameType
|
||||
from . import process_helper as ph
|
||||
from .logger_helper import color, colorize
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError as e:
|
||||
print("Failed to import requests: " + str(e))
|
||||
print("")
|
||||
print("You may need to install it using:")
|
||||
print(" pip3 install --user requests")
|
||||
print("")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class TesterInterface:
|
||||
|
||||
def query_test_cases(self, build_dir: str, filter: str) -> List[str]:
|
||||
"""Get a list of test cases that match a given filter"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_test_runner(self,
|
||||
workspace_dir: str,
|
||||
log_dir: str,
|
||||
model: str,
|
||||
case: str,
|
||||
config: Dict[str, Any],
|
||||
speed_factor: float,
|
||||
verbose: bool,
|
||||
build_dir: str) -> ph.Runner:
|
||||
"""Instantiate a Runner that starts the test executable"""
|
||||
raise NotImplementedError
|
||||
|
||||
def rootfs_base_dirname(self) -> str:
|
||||
"""Get a directory name to be used as rootfs dir in PX4 inside the build directory"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Tester:
|
||||
def __init__(self,
|
||||
config: Dict[str, Any],
|
||||
iterations: int,
|
||||
abort_early: bool,
|
||||
speed_factor: float,
|
||||
model: str,
|
||||
case: str,
|
||||
debugger: str,
|
||||
log_dir: str,
|
||||
gui: bool,
|
||||
verbose: bool,
|
||||
upload: bool,
|
||||
build_dir: str,
|
||||
tester_interface: TesterInterface):
|
||||
self.config = config
|
||||
self.build_dir = build_dir
|
||||
self.active_runners: List[ph.Runner]
|
||||
self.iterations = iterations
|
||||
self.abort_early = abort_early
|
||||
self.speed_factor = speed_factor
|
||||
self.debugger = debugger
|
||||
self.log_dir = log_dir
|
||||
self.gui = gui
|
||||
self.verbose = verbose
|
||||
self.upload = upload
|
||||
self.start_time = datetime.datetime.now()
|
||||
self.log_fd: Any[TextIO] = None
|
||||
self.tester_interface = tester_interface
|
||||
self.tests = self.determine_tests(config['tests'], model, case)
|
||||
self.active_runners = []
|
||||
|
||||
@staticmethod
|
||||
def wildcard_match(pattern: str, potential_match: str) -> bool:
|
||||
return fnmatch.fnmatchcase(potential_match, pattern)
|
||||
|
||||
@staticmethod
|
||||
def plural_s(n_items: int) -> str:
|
||||
if n_items > 1:
|
||||
return "s"
|
||||
else:
|
||||
return ""
|
||||
|
||||
def determine_tests(self,
|
||||
tests: List[Dict[str, Any]],
|
||||
model: str,
|
||||
case: str) -> List[Dict[str, Any]]:
|
||||
for test in tests:
|
||||
test['selected'] = (model == 'all' or model == test['model'])
|
||||
test['cases'] = dict.fromkeys(
|
||||
self.tester_interface.query_test_cases(self.build_dir, test['test_filter']))
|
||||
for key in test['cases'].keys():
|
||||
test['cases'][key] = {
|
||||
'selected': (test['selected'] and
|
||||
(case == 'all' or
|
||||
self.wildcard_match(case, key)))}
|
||||
return tests
|
||||
|
||||
def run(self) -> bool:
|
||||
self.show_plans()
|
||||
self.prepare_for_results()
|
||||
self.run_tests()
|
||||
self.show_detailed_results()
|
||||
self.show_overall_result()
|
||||
return self.was_overall_pass()
|
||||
|
||||
def show_plans(self) -> None:
|
||||
print()
|
||||
print(colorize(
|
||||
"About to run {} test case{} for {} selected model{} "
|
||||
"({} iteration{}):".
|
||||
format(self.num_cases(), self.plural_s(self.num_cases()),
|
||||
self.num_models(), self.plural_s(self.num_models()),
|
||||
self.iterations, self.plural_s(self.iterations)),
|
||||
color.BOLD))
|
||||
|
||||
for test in self.tests:
|
||||
if not test['selected']:
|
||||
print(colorize(colorize(
|
||||
" - {} (not selected)".format(test['model']),
|
||||
color.BOLD), color.GRAY))
|
||||
continue
|
||||
print(colorize(" - {}:".format(test['model']), color.BOLD))
|
||||
for key, case in test['cases'].items():
|
||||
if case['selected']:
|
||||
print(" - '{}'".format(key))
|
||||
else:
|
||||
print(colorize(" - '{}' (not selected)".format(key),
|
||||
color.GRAY))
|
||||
print()
|
||||
|
||||
def num_models(self) -> int:
|
||||
return sum(map(
|
||||
lambda test: 1 if test['selected'] else 0,
|
||||
self.tests))
|
||||
|
||||
def num_cases(self) -> int:
|
||||
n_cases = 0
|
||||
for test in self.tests:
|
||||
if not test['selected']:
|
||||
continue
|
||||
n_cases += self.num_cases_for_test(test)
|
||||
|
||||
return n_cases
|
||||
|
||||
def num_cases_for_test(self, test: Dict[str, Any]) -> int:
|
||||
n_cases = 0
|
||||
for case in test['cases'].values():
|
||||
if not case['selected']:
|
||||
continue
|
||||
n_cases += 1
|
||||
return n_cases
|
||||
|
||||
def prepare_for_results(self) -> None:
|
||||
for test in self.tests:
|
||||
if not test['selected']:
|
||||
continue
|
||||
for key, value in test['cases'].items():
|
||||
if not value['selected']:
|
||||
continue
|
||||
value['results'] = []
|
||||
|
||||
def run_tests(self) -> None:
|
||||
for iteration in range(self.iterations):
|
||||
if self.iterations > 1:
|
||||
print(colorize("%%% Test iteration: {} / {}".
|
||||
format(iteration + 1, self.iterations), color.BOLD))
|
||||
|
||||
success = self.run_iteration(iteration)
|
||||
if not success:
|
||||
if self.abort_early:
|
||||
break
|
||||
|
||||
def run_iteration(self, iteration: int) -> bool:
|
||||
iteration_success = True
|
||||
for test in self.tests:
|
||||
if not test['selected']:
|
||||
continue
|
||||
|
||||
print(colorize(
|
||||
"==> Running tests for {}".format(test['model']),
|
||||
color.BOLD))
|
||||
|
||||
test_i = 0
|
||||
for key, case_value in test['cases'].items():
|
||||
if not case_value['selected']:
|
||||
continue
|
||||
|
||||
print("--> Test case {} of {}: '{}' running ...".
|
||||
format(test_i + 1,
|
||||
self.num_cases_for_test(test),
|
||||
key))
|
||||
|
||||
log_dir = self.get_log_dir(iteration, test['model'], key)
|
||||
if self.verbose:
|
||||
print("Creating log directory: {}"
|
||||
.format(log_dir))
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
was_success = self.run_test_case(test, key, log_dir)
|
||||
|
||||
print("--- Test case {} of {}: '{}' {}."
|
||||
.format(test_i + 1,
|
||||
self.num_cases_for_test(test),
|
||||
key,
|
||||
colorize("succeeded", color.GREEN)
|
||||
if was_success
|
||||
else colorize("failed", color.RED)))
|
||||
|
||||
if not was_success:
|
||||
iteration_success = False
|
||||
|
||||
if not was_success and self.abort_early:
|
||||
print("Aborting early")
|
||||
return False
|
||||
|
||||
test_i += 1
|
||||
|
||||
return iteration_success
|
||||
|
||||
def get_log_dir(self, iteration: int, model: str, case: str) -> str:
|
||||
date_and_time = self.start_time.strftime("%Y-%m-%dT%H-%M-%SZ")
|
||||
foldername = os.path.join(self.log_dir, date_and_time)
|
||||
|
||||
if self.iterations > 1:
|
||||
# Use as many zeros in foldername as required.
|
||||
digits = math.floor(math.log10(self.iterations)) + 1
|
||||
foldername = os.path.join(foldername,
|
||||
str(iteration + 1).zfill(digits))
|
||||
|
||||
foldername = os.path.join(foldername, model)
|
||||
foldername = os.path.join(foldername, case.replace(" ", "_"))
|
||||
|
||||
return foldername
|
||||
|
||||
def get_max_speed_factor(self, test: Dict[str, Any]) -> float:
|
||||
speed_factor = self.speed_factor
|
||||
if "max_speed_factor" in test:
|
||||
speed_factor = min(float(speed_factor), test["max_speed_factor"])
|
||||
return speed_factor
|
||||
|
||||
def run_test_case(self, test: Dict[str, Any],
|
||||
case: str, log_dir: str) -> bool:
|
||||
|
||||
logfile_path = self.determine_logfile_path(log_dir, 'combined')
|
||||
self.start_combined_log(logfile_path)
|
||||
|
||||
self.start_runners(log_dir, test, case)
|
||||
|
||||
test_timeout_s = test['timeout_min'] * 60
|
||||
while self.active_runners[-1].time_elapsed_s() < test_timeout_s:
|
||||
returncode = self.active_runners[-1].poll()
|
||||
|
||||
self.collect_runner_output()
|
||||
|
||||
if returncode is not None:
|
||||
is_success = (returncode == 0)
|
||||
break
|
||||
|
||||
else:
|
||||
print(colorize(
|
||||
"Test timeout of {} mins triggered!".
|
||||
format(test['timeout_min']),
|
||||
color.BOLD))
|
||||
is_success = False
|
||||
|
||||
self.stop_runners()
|
||||
# Collect what was left in output buffers.
|
||||
self.collect_runner_output()
|
||||
self.stop_combined_log()
|
||||
|
||||
result = {'success': is_success,
|
||||
'logfiles': [runner.get_log_filename()
|
||||
for runner in self.active_runners]}
|
||||
test['cases'][case]['results'].append(result)
|
||||
|
||||
if not is_success:
|
||||
if not self.verbose:
|
||||
print(self.get_combined_log(logfile_path))
|
||||
print("Logfiles: ")
|
||||
print(" - {}".format(logfile_path))
|
||||
for runner in self.active_runners:
|
||||
print(" - {}".format(runner.get_log_filename()))
|
||||
|
||||
if self.upload:
|
||||
ulog_file = self.parse_for_ulog_file(
|
||||
self.get_combined_log(logfile_path))
|
||||
self.upload_log(ulog_file, test['model'], case, is_success)
|
||||
|
||||
return is_success
|
||||
|
||||
def start_runners(self,
|
||||
log_dir: str,
|
||||
test: Dict[str, Any],
|
||||
case: str) -> None:
|
||||
self.active_runners = []
|
||||
|
||||
if self.config['mode'] == 'sitl':
|
||||
if self.config['simulator'] == 'gazebo':
|
||||
# Use RegEx to extract worldname.world from case name
|
||||
match = re.search(r'\((.*?\.world)\)', case)
|
||||
if match:
|
||||
world_name = match.group(1)
|
||||
else:
|
||||
world_name = 'empty.world'
|
||||
|
||||
gzserver_runner = ph.GzserverRunner(
|
||||
os.getcwd(),
|
||||
log_dir,
|
||||
test['vehicle'],
|
||||
case,
|
||||
self.get_max_speed_factor(test),
|
||||
self.verbose,
|
||||
self.build_dir,
|
||||
world_name)
|
||||
self.active_runners.append(gzserver_runner)
|
||||
|
||||
gzmodelspawn_runner = ph.GzmodelspawnRunner(
|
||||
os.getcwd(),
|
||||
log_dir,
|
||||
test['vehicle'],
|
||||
case,
|
||||
self.verbose,
|
||||
self.build_dir)
|
||||
self.active_runners.append(gzmodelspawn_runner)
|
||||
|
||||
if self.gui:
|
||||
gzclient_runner = ph.GzclientRunner(
|
||||
os.getcwd(),
|
||||
log_dir,
|
||||
test['model'],
|
||||
case,
|
||||
self.verbose)
|
||||
self.active_runners.append(gzclient_runner)
|
||||
|
||||
# We must start the PX4 instance at the end, as starting
|
||||
# it in the beginning, then connecting Gazebo server freaks
|
||||
# out the PX4 (it needs to have data coming in when started),
|
||||
# and can lead to EKF to freak out, or the instance itself
|
||||
# to die unexpectedly.
|
||||
px4_runner = ph.Px4Runner(
|
||||
os.getcwd(),
|
||||
log_dir,
|
||||
test['model'],
|
||||
case,
|
||||
self.get_max_speed_factor(test),
|
||||
self.debugger,
|
||||
self.verbose,
|
||||
self.build_dir,
|
||||
self.tester_interface.rootfs_base_dirname())
|
||||
for env_key in test.get('env', []):
|
||||
px4_runner.env[env_key] = str(test['env'][env_key])
|
||||
self.active_runners.append(px4_runner)
|
||||
|
||||
self.active_runners.append(self.tester_interface.create_test_runner(
|
||||
os.getcwd(),
|
||||
log_dir,
|
||||
test['model'],
|
||||
case,
|
||||
self.config,
|
||||
self.speed_factor,
|
||||
self.verbose,
|
||||
self.build_dir
|
||||
))
|
||||
|
||||
abort = False
|
||||
for runner in self.active_runners:
|
||||
runner.set_log_filename(
|
||||
self.determine_logfile_path(log_dir, runner.name))
|
||||
|
||||
if not self.try_to_run_several_times(runner):
|
||||
abort = True
|
||||
break
|
||||
|
||||
if abort:
|
||||
print("Could not start runner: {}".format(runner.name))
|
||||
self.collect_runner_output()
|
||||
self.stop_combined_log()
|
||||
self.stop_runners()
|
||||
sys.exit(1)
|
||||
|
||||
def try_to_run_several_times(self, runner: ph.Runner) -> bool:
|
||||
for _ in range(3):
|
||||
runner.start()
|
||||
|
||||
if runner.has_started_ok():
|
||||
return True
|
||||
|
||||
else:
|
||||
runner.stop()
|
||||
time.sleep(1)
|
||||
|
||||
return False
|
||||
|
||||
def stop_runners(self) -> None:
|
||||
for runner in self.active_runners:
|
||||
runner.stop()
|
||||
|
||||
def collect_runner_output(self) -> None:
|
||||
for runner in self.active_runners:
|
||||
while True:
|
||||
line = runner.get_output_line()
|
||||
if not line:
|
||||
break
|
||||
|
||||
self.add_to_combined_log(line)
|
||||
if self.verbose:
|
||||
print(line, end="")
|
||||
|
||||
def parse_for_ulog_file(self, log: str) -> Optional[str]:
|
||||
|
||||
match = "[logger] Opened full log file: ./"
|
||||
for line in log.splitlines():
|
||||
found = line.find(match)
|
||||
if found != -1:
|
||||
return os.path.join(os.getcwd(), self.build_dir,
|
||||
self.tester_interface.rootfs_base_dirname(),
|
||||
"rootfs",
|
||||
line[found + len(match):])
|
||||
return None
|
||||
|
||||
def upload_log(self, ulog_path: Optional[str],
|
||||
model: str, case: str, success: bool) -> None:
|
||||
if not ulog_path:
|
||||
print(" Could not find ulog log file to upload")
|
||||
return
|
||||
|
||||
if not os.getenv('GITHUB_RUN_ID'):
|
||||
print(" Upload only implemented for GitHub Actions CI")
|
||||
return
|
||||
|
||||
print(" Uploading logfile '{}' ...".format(ulog_path))
|
||||
|
||||
server = "https://logs.px4.io"
|
||||
|
||||
result_str = "passing" if success else "failing"
|
||||
|
||||
payload = {
|
||||
"type": "flightreport",
|
||||
"description": "SITL integration test - {}: '{}' -> {}"
|
||||
.format(model, case, result_str),
|
||||
"feedback":
|
||||
"{}/{}/actions/runs/{}"
|
||||
.format(os.getenv("GITHUB_SERVER_URL"),
|
||||
os.getenv("GITHUB_REPOSITORY"),
|
||||
os.getenv("GITHUB_RUN_ID")),
|
||||
"email": "",
|
||||
"source": "CI",
|
||||
"videoUrl": "",
|
||||
"rating": "notset",
|
||||
"windSpeed": -1,
|
||||
"public": "true"
|
||||
}
|
||||
|
||||
with open(ulog_path, 'rb') as f:
|
||||
r = requests.post(server + "/upload",
|
||||
data=payload,
|
||||
files={'filearg': f},
|
||||
allow_redirects=False)
|
||||
if r.status_code == 302: # redirect
|
||||
if 'Location' in r.headers:
|
||||
plot_url = r.headers['Location']
|
||||
if len(plot_url) > 0 and plot_url[0] == '/':
|
||||
plot_url = server + plot_url
|
||||
print(" Uploaded to: " + plot_url)
|
||||
else:
|
||||
print(" Upload failed with status_code: {}"
|
||||
.format(r.status_code))
|
||||
|
||||
def start_combined_log(self, filename: str) -> None:
|
||||
self.log_fd = open(filename, 'w')
|
||||
|
||||
def stop_combined_log(self) -> None:
|
||||
if self.log_fd:
|
||||
self.log_fd.close()
|
||||
|
||||
def add_to_combined_log(self, output: str) -> None:
|
||||
self.log_fd.write(output)
|
||||
|
||||
def get_combined_log(self, filename: str) -> str:
|
||||
with open(filename, 'r') as f:
|
||||
return f.read()
|
||||
|
||||
@staticmethod
|
||||
def determine_logfile_path(log_dir: str, desc: str) -> str:
|
||||
return os.path.join(log_dir, "log-{}.log".format(desc))
|
||||
|
||||
def show_detailed_results(self) -> None:
|
||||
print()
|
||||
print(colorize("Results:", color.BOLD))
|
||||
|
||||
for test in self.tests:
|
||||
if not test['selected']:
|
||||
print(colorize(colorize(
|
||||
" - {} (not selected)".
|
||||
format(test['model']), color.BOLD), color.GRAY))
|
||||
continue
|
||||
else:
|
||||
print(colorize(" - {}:".format(test['model']), color.BOLD))
|
||||
|
||||
for name, case in test['cases'].items():
|
||||
if not case['selected']:
|
||||
continue
|
||||
print(" - '{}': ".format(name), end="")
|
||||
|
||||
n_succeeded = [result['success']
|
||||
for result in case['results']].count(True)
|
||||
n_failed = [result['success']
|
||||
for result in case['results']].count(False)
|
||||
n_cancelled = self.iterations - len(case['results'])
|
||||
|
||||
notes = [
|
||||
self.note_if_any(
|
||||
colorize("{}succeeded", color.GREEN),
|
||||
n_succeeded, self.iterations),
|
||||
self.note_if_any(
|
||||
colorize("{}failed", color.RED),
|
||||
n_failed, self.iterations),
|
||||
self.note_if_any(
|
||||
colorize("{}cancelled", color.GRAY),
|
||||
n_cancelled, self.iterations)]
|
||||
notes_without_none = list(filter(None, notes))
|
||||
print(", ".join(notes_without_none))
|
||||
|
||||
def was_overall_pass(self) -> bool:
|
||||
for test in self.tests:
|
||||
if not test['selected']:
|
||||
continue
|
||||
|
||||
for case in test['cases'].values():
|
||||
if not case['selected']:
|
||||
continue
|
||||
for result in case['results']:
|
||||
if result['success'] is not True:
|
||||
return False
|
||||
return True
|
||||
|
||||
def show_overall_result(self) -> None:
|
||||
print(colorize(
|
||||
"Overall result: {}".format(
|
||||
colorize("PASS", color.GREEN)
|
||||
if self.was_overall_pass()
|
||||
else colorize("FAIL", color.RED)), color.BOLD))
|
||||
|
||||
@staticmethod
|
||||
def note_if_any(text_to_format: str, n: int, total: int) -> Optional[str]:
|
||||
assert not n < 0
|
||||
if n == 0:
|
||||
return None
|
||||
elif n == 1 and total == 1:
|
||||
return text_to_format.format("")
|
||||
else:
|
||||
return text_to_format.format(str(n) + " ")
|
||||
|
||||
def sigint_handler(self, sig: int, frame: Optional[FrameType]) \
|
||||
-> NoReturn:
|
||||
print("Received SIGINT")
|
||||
print("Stopping all processes ...")
|
||||
self.stop_runners()
|
||||
self.stop_combined_log()
|
||||
print("Stopping all processes done.")
|
||||
self.show_detailed_results()
|
||||
sys.exit(-sig)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user