Compare commits

..

53 Commits

Author SHA1 Message Date
Pernilla 7b902efdca Update description 2025-04-07 11:52:45 +02:00
Pernilla a3657c1396 Allow user to set choke position through parameter 2025-04-04 15:26:31 +02:00
Beat Küng 5d3083ec00 ci: copy px4 msgs directly to ros ws & upload failed logs 2025-04-04 09:12:53 +02:00
Beat Küng 845d65fe9b fix output_groups_from_timer_config.py: avoid invalid escape sequence
Python warning:
Tools/module_config/output_groups_from_timer_config.py:38:
SyntaxWarning: invalid escape sequence '\)'
2025-04-04 09:12:53 +02:00
GuillaumeLaine 648e730c4a ev_odom: always convert reference frame enum 2025-04-04 09:12:53 +02:00
Beat Küng 6fcfd5fac1 uxrce_dds_client: immediately create data writers on startup
There is some race condition where in rare cases the topic publication
right after creating the writer did not get received on the ROS side.
This happens even with reliable QoS & reliable transport.
2025-04-04 09:12:53 +02:00
GuillaumeLaine ba35ca461c ci: add external navigation integration tests 2025-04-04 09:12:53 +02:00
Beat Küng 0e4c794674 ros_tests: change filter to use all tests
The name is being changed in https://github.com/Auterion/px4-ros2-interface-lib/pull/8
2025-04-04 09:12:53 +02:00
Beat Küng 3d6056411f integration tests: add --force-color & set in CI
github actions supports color output, but does not report as a tty.
See https://github.com/actions/runner/issues/241.
2025-04-04 09:12:53 +02:00
Beat Küng abb80ae71e ci: add ros integration tests 2025-04-04 09:12:53 +02:00
Beat Küng a954ec4d55 test: add ros integration test runner script & config 2025-04-04 09:12:53 +02:00
Beat Küng 7c14a63855 refactor mavsdk_tests: move code into separate classes & extract mavsdk-specifics
Allows it to be reused for other integration tests, like ros.
2025-04-04 09:12:53 +02:00
Beat Küng d78af5436e mavsdk_tests: highlight px4 errors & reset color on gazebo output 2025-04-04 09:12:53 +02:00
Beat Küng 45285a57ad fix mavsdk_tests: add ',' to test_filter config 2025-04-04 09:12:53 +02:00
Beat Küng bbcd153e94 sitl: add generic way to override params via ENV variables 2025-04-04 09:12:53 +02:00
Julian Oes 3bde706cb3 cubepilot: enable heater for CubeOrange+
This adds everything required to use the heater on Orange+ but leaves it
disabled by default.
2025-04-04 08:31:06 +02:00
Julian Oes dbe57fad08 cubepilot: add heater support for CubeOrange
This adds the heater (via px4io) in but it's still disabled by default.
2025-04-04 08:31:06 +02:00
Julian Oes f7740bdfd2 heater: fix invalid file descriptor
We need to open the device later in the work queue and not in the
constructor during task_spawn.

There is already a lazy open in place, so just removing this fixes the
problem for me.
2025-04-04 08:31:06 +02:00
Julian Oes 74a8d897b2 ROMFS: start heater after px4io
Otherwise the device is not available when we start the heater.
2025-04-04 08:31:06 +02:00
Patrik Dominik Pordi 1ee3b7e77d [Pending] Updated Encrypted logs docs to reflect #24489 , (#24580)
* Updated Encrypted logs docs to reflect https://github.com/PX4/PX4-Autopilot/pull/24489

* Subedit

* Subedit the key generation bit

* Updated the docs with multiple command line args for the decryptor and added OpenSSL as a requirement

* Subedit

* Update log_encryption.md

---------

Co-authored-by: Hamish Willee <hamishwillee@gmail.com>
2025-04-04 07:36:35 +11:00
Patrik Dominik Pordi cc492bbf6e [Sponsored by ARK] Encryption (#24489)
* Added the board configs for encryption, I had to disable smbus and px4 io in the arkv6x

* Added the key generator script

* Added the decryptor, logs are needed for it though

* Added the log download and modified the decryptor

* Quick fixes & README

* Additional modifications & cleanup

* Tested upd connection
Adjusted the log downloader to handle multiple entry responses from the FC
Edited README

* Reverted IP address change

* Added pycryptodome to the requirements.txt

* fixes for log download and decryption

* Removed old log decryptors and updated README

* Pointed the ark borads to the dummy key updated the README accordingly

* Adjusted the folders in README, removed new lines

* Extended command line arguments for all possibilities for description

* Added MAV_CMD_REQUEST_AUTOPILOT_CAPABILITIES after heartbeat received to make sure log request is answered in all cases

* Update Tools/log_encryption/README.md

Co-authored-by: Jacob Dahl <37091262+dakejahl@users.noreply.github.com>

* Update Tools/log_encryption/README.md

Co-authored-by: Jacob Dahl <37091262+dakejahl@users.noreply.github.com>

* Update Tools/log_encryption/README.md

Co-authored-by: Jacob Dahl <37091262+dakejahl@users.noreply.github.com>

* Update Tools/log_encryption/README.md

Co-authored-by: Jacob Dahl <37091262+dakejahl@users.noreply.github.com>

* Edited README, changed the serial connection logic and updated logdownload, made decryption a bit easier to understand

* Update Tools/log_encryption/README.md

Co-authored-by: Hamish Willee <hamishwillee@gmail.com>

* Removed new lines

* arkv6x: add individual mags to default.px4board

---------

Co-authored-by: Jacob Dahl <dahl.jakejacob@gmail.com>
Co-authored-by: Alex Klimaj <alex@arkelectron.com>
Co-authored-by: Jacob Dahl <37091262+dakejahl@users.noreply.github.com>
Co-authored-by: Hamish Willee <hamishwillee@gmail.com>
2025-04-02 23:40:45 -08:00
Seungbin Lee 9cc1e01bd8 [Doc] Added J.Fi Wireless Telemetry Module (#24656)
* add jmarple product

* update youtube link

* Move/shrink images

* Move above discontinued items

* Minor update

* Fix broken link

* update PX4 Setup guide

* Subedit

---------

Co-authored-by: Hamish Willee <hamishwillee@gmail.com>
2025-04-03 15:16:56 +11:00
Hamish Willee 724987d59e Autotune large MC vehicles (#24614) 2025-04-03 14:21:00 +11:00
Hamish Willee 27ff547e07 Updates linkchecker and fixes up the doc contribution sections (#24660)
* Update yarn linkchecker

* Fix edit page links. Fix contribution pages
2025-04-03 12:41:56 +11:00
Eric Katzfey db97dd471d Added setting default for EKF2_EV_CTRL to 15 for VOXL 2 boards 2025-04-02 11:11:56 -04:00
Hamish Willee 5509061803 docs_flaw_checker.yml - attempt update (#24655) 2025-04-02 18:47:49 +11:00
Hamish Willee f2026343d7 [Doc] Data link loss exceptions (#24652)
* [Doc] Data link loss exceptions

* Tidy
2025-04-02 12:13:12 +11:00
Hamish Willee fee81a5c88 [Docs] Gimbal - improvements to mavlink setup instructions (#24613) 2025-04-02 11:59:28 +11:00
PX4 Build Bot d06e9cc302 New Crowdin translations - zh-CN (#24636)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-04-02 11:57:39 +11:00
bresch e35c1f430c EKF-AGP: only reset lat/lon when starting 2025-04-01 16:42:26 +03:00
Alex Klimaj 1928758fbc boards: ark_fpv add camera trigger and capture drivers (#24643) 2025-03-31 10:48:32 -08:00
bresch f73c7977dd ekf2-flow: limit minimum flow hagl 2025-03-31 11:34:25 +02:00
bresch 53bdceb895 ekf2-flow: check test ratio on Y axis separately 2025-03-31 11:34:25 +02:00
bresch cdab0cb6e4 ekf2-flow: use same measurement prediction as in jacobian derivation
Also avoid double division in flow prediction
2025-03-31 11:34:25 +02:00
bresch 82ea544e8c ekf2-test: add flow unit test for negative distance 2025-03-31 11:34:25 +02:00
Matthias Grob ddb9a5d0b9 gz_plugins: do not look for gz-transport12 (Gazebo garden) (#24633)
this tries to build the plugins and breaks the SITL build if you have
Gazebo garden isntalled even if you're not trying to simulate with
Gazebo.
2025-03-28 10:11:56 -08:00
Roman Bapst 2c8ef05c2d Add COM_DLL_EXCEPT to specifiy exceptions for data link loss failsafe 2025-03-28 17:41:24 +01:00
Alexander Lerach 72454c4fd2 dataman: clarify default storage backend (#24626) 2025-03-28 16:27:12 +01:00
Silvan Fuhrer 69b7a21f02 AirspeedValidated: add VERSION (#24620)
Signed-off-by: Silvan Fuhrer <silvan@auterion.com>
2025-03-28 12:15:00 +01:00
Jacob Dahl 7cb7977263 dshot: only use 1 DMA, round robin the channels. Fix esc telemetry. (#24610) 2025-03-27 11:41:38 -06:00
Julian Oes 8acf273917 Add RTL_TYPE to continue or reverse (#24581)
This adds RTL_TYPE 4 which means continue the mission or reverse back to
the takeoff location, whichever is closer in terms of mission items
in-between.

This would be nicer to have on a distance rather than mission item count
basis but that would require access to the dataman and make it more
complex.
2025-03-28 06:29:42 +13:00
Daan Smienk 3870992bac Fix spelling mistake (#24623) 2025-03-27 09:46:02 -06:00
Roman Bapst 550bbd9051 FW rate controller: Don't constrain airspeed for scaling to maximum airspeed (#24622)
* don't constrain airspeed for scaling to maximum airspeed

Signed-off-by: RomanBapst <bapstroman@gmail.com>

* fix max function

Signed-off-by: RomanBapst <bapstroman@gmail.com>

* remove hardcoded max

Signed-off-by: RomanBapst <bapstroman@gmail.com>

---------

Signed-off-by: RomanBapst <bapstroman@gmail.com>
2025-03-27 14:22:40 +01:00
Balduin 19d3e6285b vtol_att_control: shorter elapsed time calculation 2025-03-27 09:29:54 +01:00
Balduin 898d631b24 dds_topics: add vtol_vehicle_status 2025-03-27 09:29:54 +01:00
PX4 Build Bot e7eca72d02 New Crowdin translations - zh-CN (#24617)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-03-27 14:43:25 +11:00
PX4 Build Bot 9f4e642e9f New Crowdin translations - ko (#24591)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-03-27 14:41:16 +11:00
PX4 Build Bot 6f026f35b1 New Crowdin translations - uk (#24592)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-03-27 14:39:36 +11:00
Hamish Willee 46d1489d36 docs_crowdin_download.yml - attempt to fix the label (#24616) 2025-03-27 14:34:24 +11:00
Hamish Willee 4710366862 Open file locally respects frontmatter (#24615) 2025-03-27 14:32:28 +11:00
bresch 82a482ec0b ekf2: reset heading when mag calibration changed 2025-03-26 22:32:51 -04:00
bresch 49624a6457 mag: synchronize calibration count with newly calibrated data
The data contained a mix between the old and new calibration. This
caused the EKF to reset to an incorrect (intermediate) heading.
2025-03-26 22:32:51 -04:00
Alexander Lerach 7acd2e93eb gps: Parse RTCM3 & NAV in parallel 2025-03-26 17:45:10 +01:00
103 changed files with 2707 additions and 1696 deletions
+1 -1
View File
@@ -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:
+5 -5
View File
@@ -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
+123
View File
@@ -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
+1 -1
View File
@@ -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
+1
View File
@@ -108,3 +108,4 @@ src/systemcmds/topic_listener/listener_generated.cpp
# colcon
log/
keys/
+9
View File
@@ -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))
-6
View File
@@ -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
+8
View File
@@ -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
-172
View File
@@ -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
-91
View File
@@ -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()))
+86
View File
@@ -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.
+135
View File
@@ -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)
+190
View File
@@ -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 27 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()
+59
View File
@@ -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'
+1
View File
@@ -26,3 +26,4 @@ setuptools>=39.2.0
six>=1.12.0
toml>=0.9
sympy>=1.10.1
pycryptodome
+10 -1
View File
@@ -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
+2
View File
@@ -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
+7
View File
@@ -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
+6
View File
@@ -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"
+7 -3
View File
@@ -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
View File
@@ -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)
+3 -1
View File
@@ -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
+14
View File
@@ -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
+6
View File
@@ -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
View File
@@ -15,7 +15,7 @@ Simple changes to _existing content_ can be made by clicking the **Edit on GitHu
![Vitepress: Edit Page button](../../assets/vuepress/vuepress_edit_page_on_github_link.png)
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):
+4
View File
@@ -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)
+155 -36
View File
@@ -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"
```
+15 -7
View File
@@ -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)
+2 -1
View File
@@ -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).
+134
View File
@@ -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.
![J.Fi Wireless Telemetry Module](../../assets/hardware/telemetry/jmarple/jfi_telemetry_module.png)
## 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.
![J.Fi Wireless Telemetry Broadcast Communication](../../assets/hardware/telemetry/jmarple/jfi_telemetry_broadcast.png)
## 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.**
![J.Fi Wireless Telemetry Paired Communication](../../assets/hardware/telemetry/jmarple/jfi_telemetry_paired.png)
### 1:1 Pairing
![J.Fi Wireless Telemetry Buttons](../../assets/hardware/telemetry/jmarple/jfi_telemetry_buttons.png)
- 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)).
![J.Fi Wireless Telemetry Broadcast Communication](../../assets/hardware/telemetry/jmarple/jfi_telemetry_usage.png)
<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
![J.Fi Wireless Telemetry Broadcast Communication](../../assets/hardware/telemetry/jmarple/jfi_telemetry_config.jpg)
## Further info
- [User Manual](https://docs.google.com/document/d/1NaVwOLuMCuNpd0uxgilXZ_qfHAnsFgBmaPxX9WGY2h4/edit?usp=sharing)
- [ROS Github](https://github.com/SUV-Lab/J-Fi)
+8 -8
View File
@@ -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 -1
View File
@@ -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.
![ARK Pi6X Flow Flight Controller](../../assets/flight_controller/ark_pi6x_flow/ark_pi6xflow.jpg)
+1 -1
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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.
![ARK Pi6X Flow Flight Controller](../../assets/flight_controller/ark_pi6x_flow/ark_pi6xflow.jpg)
@@ -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 -1
View File
@@ -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).
信息流看起来像这样:
+4 -4
View File
@@ -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
View File
@@ -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;
-10
View File
@@ -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
View File
@@ -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;
+1 -7
View File
@@ -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)
+208
View File
@@ -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)
-13
View File
@@ -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
+15 -1
View File
@@ -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
*
+22 -6
View File
@@ -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,
+1 -1
View File
@@ -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
+7 -3
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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:
![Architecture](../../assets/hardware/ice/ice_control_state_machine.png)
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:
![Architecture](../../assets/hardware/ice/ice_control_diagram.png)
<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
+15 -1
View File
@@ -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();
+8
View File
@@ -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
*/
+1
View File
@@ -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)
+1 -1
View File
@@ -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
+15 -11
View File
@@ -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]
+6 -2
View File
@@ -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",
@@ -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
@@ -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