pax_global_header 0000666 0000000 0000000 00000000064 15214172755 0014523 g ustar 00root root 0000000 0000000 52 comment=3cf17793f14709713ab297d3743314c658874068
pyulog-1.2.3/ 0000775 0000000 0000000 00000000000 15214172755 0013045 5 ustar 00root root 0000000 0000000 pyulog-1.2.3/.github/ 0000775 0000000 0000000 00000000000 15214172755 0014405 5 ustar 00root root 0000000 0000000 pyulog-1.2.3/.github/workflows/ 0000775 0000000 0000000 00000000000 15214172755 0016442 5 ustar 00root root 0000000 0000000 pyulog-1.2.3/.github/workflows/deploy.yml 0000664 0000000 0000000 00000001774 15214172755 0020472 0 ustar 00root root 0000000 0000000 name: Deploy
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
release:
types:
- published
jobs:
build_dist:
name: Build Dist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Build Dist
run: pipx run build
- name: Check Metadata
run: pipx run twine check --strict dist/*
- uses: actions/upload-artifact@v4
with:
path: dist/
upload_all:
name: Upload if release
needs: [build_dist]
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'published'
environment:
name: pypi
url: https://pypi.org/p/pyulog
permissions:
id-token: write
steps:
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- uses: actions/download-artifact@v4
with:
name: artifact
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.13.0 pyulog-1.2.3/.github/workflows/test.yml 0000664 0000000 0000000 00000001510 15214172755 0020141 0 ustar 00root root 0000000 0000000 name: Run Tests
on:
push:
branches:
- 'main'
pull_request:
branches:
- '*'
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false # don't cancel if a job from the matrix fails
matrix: # https://devguide.python.org/versions
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install Dependencies and pyulog
run: |
pip install pylint
pip install --verbose .[test]
- name: Running Tests
run: |
./run_tests.sh
pyulog-1.2.3/.gitignore 0000664 0000000 0000000 00000000102 15214172755 0015026 0 ustar 00root root 0000000 0000000 __pycache__
*.pyc
build/
dist/
*.egg-info
.eggs/
*.sqlite3
venv/
pyulog-1.2.3/.gitmodules 0000664 0000000 0000000 00000000156 15214172755 0015224 0 ustar 00root root 0000000 0000000 [submodule "3rd_party/libevents"]
path = 3rd_party/libevents
url = https://github.com/mavlink/libevents.git
pyulog-1.2.3/3rd_party/ 0000775 0000000 0000000 00000000000 15214172755 0014754 5 ustar 00root root 0000000 0000000 pyulog-1.2.3/3rd_party/libevents/ 0000775 0000000 0000000 00000000000 15214172755 0016747 5 ustar 00root root 0000000 0000000 pyulog-1.2.3/LICENSE.md 0000664 0000000 0000000 00000002724 15214172755 0014456 0 ustar 00root root 0000000 0000000 Copyright (c) 2016, PX4 Pro Drone Autopilot
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of GpsDrivers nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
pyulog-1.2.3/MANIFEST.in 0000664 0000000 0000000 00000000116 15214172755 0014601 0 ustar 00root root 0000000 0000000 include LICENSE.md
recursive-include pyulog/sql *
recursive-exclude test *.ulg pyulog-1.2.3/README.ko-KR.md 0000664 0000000 0000000 00000017607 15214172755 0015261 0 ustar 00root root 0000000 0000000 # pyulog
ì�´ ë ˆí�¬ì§€í† 리ì—�는 ULog 파ì�¼ ë°� 스í�¬ë¦½íŠ¸ë¥¼ 파싱하는 python 패키지가 í�¬í•¨ë�˜ì–´ 있습니다.
ULog는 self-describing 형ì‹�ì�„ 따르며, 해당 ê´€ë ¨ 문서는 다ì�Œê³¼ 같습니다(https://docs.px4.io/main/en/dev_log/ulog_file_format.html).
ì œê³µë�˜ëŠ” ëª…ë ¹ì–´ 스í�¬ë¦½íŠ¸ëŠ”(command line scripts)는 아래와 같습니다:
- `ulog_info`: ULog 파ì�¼ì�˜ ì •ë³´ë¥¼ 나타냅니다.
- `ulog_messages`: ULog 파ì�¼ì—� 기ë¡�ë�œ 로그 메시지(logged messages)를 ì¶œë ¥í•©ë‹ˆë‹¤.
- `ulog_params`: ULog 파ì�¼ì—� ì €ìž¥ë�œ 파ë�¼ë¯¸í„°ë“¤ì�„ 추출합니다.
- `ulog2csv`: ULog 파�� CSV 파�로 변환합니다.
- `ulog2kml`: ULog 파�� KML 파�로 변환합니다.
- `ulog2ros2bag`: ULog 파�� ros2bag 파�로 변환.
## 설치
패키지 설치:
```bash
pip install pyulog
```
소스코드를 통한 설치:
```bash
python setup.py build install
```
## 추가 개발
코드를 쉽게 변경 ë°� íŽ¸ì§‘í• ìˆ˜ 있는 형ì‹�으로 ì„¤ì¹˜í•˜ë ¤ë©´ 다ì�Œ ëª…ë ¹ 사용(해당 ëª…ë ¹ì�€ 패키지를 Repoì—� 대한 ë§�í�¬ë¡œ 설치합니다):
```bash
pip install -e .
```
## 테스트
```bash
pytest test
```
�는,
```bash
python setup.py test
```
## 코드 검사(Code Checking)
```bash
pylint pyulog/*.py
```
## ëª…ë ¹ì–´ 스í�¬ë¦½íЏ
ëª¨ë“ ìŠ¤í�¬ë¦½íŠ¸ëŠ” 시스템 ì „ì²´ 어플리케ì�´ì…˜ë‹¨ì—�서 설치ë�˜ë©°(Python ë˜�는 시스템 경로를 ì§€ì •í•˜ì§€ 않ê³
커맨드 ë�¼ì�¸ì—�서 호출), `-h` 플래그를 통해 ê°� 스í�¬ë¦½íЏì�˜ 사용법ì�„ 확ì�¸í• 수 있습니다.
아래 섹션ì—�서는 사용 구문 ë°� 샘플 ì¶œë ¥ì�„ 나타냅니다. (from [test/sample.ulg](test/sample.ulg)):
### ULog 파ì�¼ë¡œë¶€í„° ì •ë³´ ì¶œë ¥ (ulog_info)
사용:
```bash
usage: ulog_info [-h] [-v] file.ulg
Display information from an ULog file
positional arguments:
file.ulg ULog input file
optional arguments:
-h, --help show this help message and exit
-v, --verbose Verbose output
```
결과 예시:
```bash
$ ulog_info sample.ulg
Logging start time: 0:01:52, duration: 0:01:08
Dropouts: count: 4, total duration: 0.1 s, max: 62 ms, mean: 29 ms
Info Messages:
sys_name: PX4
time_ref_utc: 0
ver_hw: AUAV_X21
ver_sw: fd483321a5cf50ead91164356d15aa474643aa73
Name (multi id, message size in bytes) number of data points, total bytes
actuator_controls_0 (0, 48) 3269 156912
actuator_outputs (0, 76) 1311 99636
commander_state (0, 9) 678 6102
control_state (0, 122) 3268 398696
cpuload (0, 16) 69 1104
ekf2_innovations (0, 140) 3271 457940
estimator_status (0, 309) 1311 405099
sensor_combined (0, 72) 17070 1229040
sensor_preflight (0, 16) 17072 273152
telemetry_status (0, 36) 70 2520
vehicle_attitude (0, 36) 6461 232596
vehicle_attitude_setpoint (0, 55) 3272 179960
vehicle_local_position (0, 123) 678 83394
vehicle_rates_setpoint (0, 24) 6448 154752
vehicle_status (0, 45) 294 13230
```
### ULog 파ì�¼ì—� 기ë¡�ë�œ 로그 메시지 ì¶œë ¥ (ulog_messages)
사용:
```
usage: ulog_messages [-h] file.ulg
Display logged messages from an ULog file
positional arguments:
file.ulg ULog input file
optional arguments:
-h, --help show this help message and exit
```
결과 예시:
```
ubuntu@ubuntu:~/github/pyulog/test$ ulog_messages sample.ulg
0:02:38 ERROR: [sensors] no barometer found on /dev/baro0 (2)
0:02:42 ERROR: [sensors] no barometer found on /dev/baro0 (2)
0:02:51 ERROR: [sensors] no barometer found on /dev/baro0 (2)
0:02:56 ERROR: [sensors] no barometer found on /dev/baro0 (2)
```
### ULog 파ì�¼ì—� ì €ìž¥ë�œ 파ë�¼ë¯¸í„° 추출 (ulog_params)
사용:
```
usage: ulog_params [-h] [-d DELIMITER] [-i] [-o] file.ulg [params.txt]
Extract parameters from an ULog file
positional arguments:
file.ulg ULog input file
params.txt Output filename (default=stdout)
optional arguments:
-h, --help show this help message and exit
-d DELIMITER, --delimiter DELIMITER
Use delimiter in CSV (default is ',')
-i, --initial Only extract initial parameters
-o, --octave Use Octave format
```
ê²°ê³¼ 예시 (콘솔 ì¶œë ¥):
```
ubuntu@ubuntu:~/github/pyulog/test$ ulog_params sample.ulg
ATT_ACC_COMP,1
ATT_BIAS_MAX,0.0500000007451
ATT_EXT_HDG_M,0
...
VT_OPT_RECOV_EN,0
VT_TYPE,0
VT_WV_LND_EN,0
VT_WV_LTR_EN,0
VT_WV_YAWR_SCL,0.15000000596
```
### ULog 파�� CSV 파�로 변환 (ulog2csv)
사용:
```
usage: ulog2csv [-h] [-m MESSAGES] [-d DELIMITER] [-o DIR] file.ulg
Convert ULog to CSV
positional arguments:
file.ulg ULog input file
optional arguments:
-h, --help show this help message and exit
-m MESSAGES, --messages MESSAGES
Only consider given messages. Must be a comma-
separated list of names, like
'sensor_combined,vehicle_gps_position'
-d DELIMITER, --delimiter DELIMITER
Use delimiter in CSV (default is ',')
-o DIR, --output DIR Output directory (default is same as input file)
```
### ULog 파�� KML 파�로 변환 (ulog2kml)
> **Note** 모듈 `simplekml` ì�´ 사용ìž�ì�˜ PCì—� 설치ë�˜ì–´ 있어야 합니다. 만약 설치ë�˜ì–´ 있지 않다면, 아래 ëª…ë ¹ì–´ë¥¼ 통해 설치하ì‹ì‹œì˜¤.
```
pip install simplekml
```
사용:
```
usage: ulog2kml [-h] [-o OUTPUT_FILENAME] [--topic TOPIC_NAME]
[--camera-trigger CAMERA_TRIGGER]
file.ulg
Convert ULog to KML
positional arguments:
file.ulg ULog input file
optional arguments:
-h, --help show this help message and exit
-o OUTPUT_FILENAME, --output OUTPUT_FILENAME
output filename
--topic TOPIC_NAME topic name with position data
(default=vehicle_gps_position)
--camera-trigger CAMERA_TRIGGER
Camera trigger topic name (e.g. camera_capture)
```
### ULog 파�� ros2bag 파�로 변환 (ulog2ros2bag)
> **Note** `px4_msgs`가 설치� ROS2 환경� 필요합니다.
사용:
```
usage: ulog2ros2bag.py [-h] [-o BAG] [-m MESSAGES] [-i] [-v] file.ulg
Convert ULog to rosbag
positional arguments:
file.ulg ULog input file
options:
-h, --help show this help message and exit
-o BAG, --output BAG rosbag output folder
-m MESSAGES, --messages MESSAGES
Only consider given messages. Must be a comma-
separated list of names, like
'sensor_combined,vehicle_gps_position'
-i, --ignore Ignore string parsing exceptions
-v, --verbose Print extra debugging information
```
### Migrate/setup the database for use with the DatabaseULog class (ulog_migratedb)
사용:
```
usage: ulog_migratedb [-h] [-d DB_PATH] [-n] [-s SQL_DIR] [-f]
Setup the database for DatabaseULog
optional arguments:
-h, --help show this help message and exit
-d DB_PATH, --database DB_PATH
Path to the database file
-n, --noop Only print results, do not execute migration scripts.
-s SQL_DIR, --sql SQL_DIR
Directory with migration SQL files
-f, --force Run the migration script even if the database is not
created with this script.
```
ê²°ê³¼ 예시 (콘솔 ì¶œë ¥):
```
ubuntu@ubuntu:~/github/pyulog$ ulog_migratedb
Using migration files in /home/ubuntu/github/pyulog/pyulog/sql.
Database file pyulog.sqlite3 not found, creating it from scratch.
Current schema version: 0 (database) and 1 (code).
Executing /home/ubuntu/github/pyulog/pyulog/sql/pyulog.1.sql.
Migration done.
```
pyulog-1.2.3/README.md 0000664 0000000 0000000 00000017422 15214172755 0014332 0 ustar 00root root 0000000 0000000 # pyulog
This repository contains a python package to parse ULog files and scripts to
convert and display them. ULog is a self-describing logging format which is
documented [here](https://docs.px4.io/main/en/dev_log/ulog_file_format.html).
The provided [command line scripts](#scripts) are:
- `ulog_info`: display information from an ULog file.
- `ulog_messages`: display logged messages from an ULog file.
- `ulog_params`: extract parameters from an ULog file.
- `ulog2csv`: convert ULog to CSV files.
- `ulog2kml`: convert ULog to KML files.
- `ulog2ros2bag`: convert ULog to ROS2 bag files.
## Installation
Installation with package manager:
```bash
pip install pyulog
```
Installation from source:
```bash
python setup.py build install
```
## Development
To install the code in a format so that it can be easily edited use the
following command (this will install the package as a link to the repo):
```bash
pip install -e .
```
## Testing
```bash
pytest test
```
or
```bash
python setup.py test
```
## Code Checking
```bash
pylint pyulog/*.py
```
## Command Line Scripts
All scripts are installed as system-wide applications (i.e. they be called on the command line without specifying Python or a system path), and support the `-h` flag for getting usage instructions.
The sections below show the usage syntax and sample output (from [test/sample.ulg](test/sample.ulg)):
### Display information from an ULog file (ulog_info)
Usage:
```bash
usage: ulog_info [-h] [-v] file.ulg
Display information from an ULog file
positional arguments:
file.ulg ULog input file
optional arguments:
-h, --help show this help message and exit
-v, --verbose Verbose output
```
Example output:
```bash
$ ulog_info sample.ulg
Logging start time: 0:01:52, duration: 0:01:08
Dropouts: count: 4, total duration: 0.1 s, max: 62 ms, mean: 29 ms
Info Messages:
sys_name: PX4
time_ref_utc: 0
ver_hw: AUAV_X21
ver_sw: fd483321a5cf50ead91164356d15aa474643aa73
Name (multi id, message size in bytes) number of data points, total bytes
actuator_controls_0 (0, 48) 3269 156912
actuator_outputs (0, 76) 1311 99636
commander_state (0, 9) 678 6102
control_state (0, 122) 3268 398696
cpuload (0, 16) 69 1104
ekf2_innovations (0, 140) 3271 457940
estimator_status (0, 309) 1311 405099
sensor_combined (0, 72) 17070 1229040
sensor_preflight (0, 16) 17072 273152
telemetry_status (0, 36) 70 2520
vehicle_attitude (0, 36) 6461 232596
vehicle_attitude_setpoint (0, 55) 3272 179960
vehicle_local_position (0, 123) 678 83394
vehicle_rates_setpoint (0, 24) 6448 154752
vehicle_status (0, 45) 294 13230
```
### Display logged messages from an ULog file (ulog_messages)
Usage:
```
usage: ulog_messages [-h] file.ulg
Display logged messages from an ULog file
positional arguments:
file.ulg ULog input file
optional arguments:
-h, --help show this help message and exit
```
Example output:
```
ubuntu@ubuntu:~/github/pyulog/test$ ulog_messages sample.ulg
0:02:38 ERROR: [sensors] no barometer found on /dev/baro0 (2)
0:02:42 ERROR: [sensors] no barometer found on /dev/baro0 (2)
0:02:51 ERROR: [sensors] no barometer found on /dev/baro0 (2)
0:02:56 ERROR: [sensors] no barometer found on /dev/baro0 (2)
```
### Extract parameters from an ULog file (ulog_params)
Usage:
```
usage: ulog_params [-h] [-d DELIMITER] [-i] [-o] file.ulg [params.txt]
Extract parameters from an ULog file
positional arguments:
file.ulg ULog input file
params.txt Output filename (default=stdout)
optional arguments:
-h, --help show this help message and exit
-d DELIMITER, --delimiter DELIMITER
Use delimiter in CSV (default is ',')
-i, --initial Only extract initial parameters
-o, --octave Use Octave format
```
Example output (to console):
```
ubuntu@ubuntu:~/github/pyulog/test$ ulog_params sample.ulg
ATT_ACC_COMP,1
ATT_BIAS_MAX,0.0500000007451
ATT_EXT_HDG_M,0
...
VT_OPT_RECOV_EN,0
VT_TYPE,0
VT_WV_LND_EN,0
VT_WV_LTR_EN,0
VT_WV_YAWR_SCL,0.15000000596
```
### Convert ULog to CSV files (ulog2csv)
Usage:
```
usage: ulog2csv [-h] [-m MESSAGES] [-d DELIMITER] [-o DIR] file.ulg
Convert ULog to CSV
positional arguments:
file.ulg ULog input file
optional arguments:
-h, --help show this help message and exit
-m MESSAGES, --messages MESSAGES
Only consider given messages. Must be a comma-
separated list of names, like
'sensor_combined,vehicle_gps_position'
-d DELIMITER, --delimiter DELIMITER
Use delimiter in CSV (default is ',')
-o DIR, --output DIR Output directory (default is same as input file)
```
### Convert ULog to KML files (ulog2kml)
> **Note** The `simplekml` module must be installed on your computer. If not already present, you can install it with:
```
pip install simplekml
```
Usage:
```
usage: ulog2kml [-h] [-o OUTPUT_FILENAME] [--topic TOPIC_NAME]
[--camera-trigger CAMERA_TRIGGER]
file.ulg
Convert ULog to KML
positional arguments:
file.ulg ULog input file
optional arguments:
-h, --help show this help message and exit
-o OUTPUT_FILENAME, --output OUTPUT_FILENAME
output filename
--topic TOPIC_NAME topic name with position data
(default=vehicle_gps_position)
--camera-trigger CAMERA_TRIGGER
Camera trigger topic name (e.g. camera_capture)
```
### Convert ULog to ROS2 bag files (ulog2ros2bag)
> **Note** You need a ROS2 environment with the corresponding version of `px4_msgs` built and sourced.
Usage:
```
usage: ulog2ros2bag.py [-h] [-o BAG] [-m MESSAGES] [-i] [-v] file.ulg
Convert ULog to rosbag
positional arguments:
file.ulg ULog input file
options:
-h, --help show this help message and exit
-o BAG, --output BAG rosbag output folder
-m MESSAGES, --messages MESSAGES
Only consider given messages. Must be a comma-
separated list of names, like
'sensor_combined,vehicle_gps_position'
-i, --ignore Ignore string parsing exceptions
-v, --verbose Print extra debugging information
```
### Migrate/setup the database for use with the DatabaseULog class (ulog_migratedb)
> **Warning** This command must be run whenever the schema changes, otherwise DatabaseULog won't function.
> **Warning** Even if you store logs in the database, you should keep the original .ulg files. Otherwise you may lose your data.
Usage:
```
usage: ulog_migratedb [-h] [-d DB_PATH] [-n] [-s SQL_DIR] [-f]
Setup the database for DatabaseULog.
optional arguments:
-h, --help show this help message and exit
-d DB_PATH, --database DB_PATH
Path to the database file
-n, --noop Only print results, do not execute migration scripts.
-s SQL_DIR, --sql SQL_DIR
Directory with migration SQL files
-f, --force Run the migration script even if the database is not
created with this script.
```
Example output (to console):
```
ubuntu@ubuntu:~/github/pyulog$ ulog_migratedb
Using migration files in /home/ubuntu/github/pyulog/pyulog/sql.
Database file pyulog.sqlite3 not found, creating it from scratch.
Current schema version: 0 (database) and 1 (code).
Executing /home/ubuntu/github/pyulog/pyulog/sql/pyulog.1.sql.
Migration done.
```
pyulog-1.2.3/gen_expected_output.sh 0000775 0000000 0000000 00000000362 15214172755 0017457 0 ustar 00root root 0000000 0000000 #! /bin/bash
# generate the expected output for the unittests
for f in test/*.ulg; do
echo "Processing $f"
ulog_info -v "$f" > "${f%.*}"_info.txt
ulog_messages "$f" > "${f%.*}"_messages.txt
#ulog_params "$f" > "${f%.*}"_params.txt
done
pyulog-1.2.3/pylintrc 0000664 0000000 0000000 00000054267 15214172755 0014652 0 ustar 00root root 0000000 0000000 [MAIN]
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
# in a server-like mode.
clear-cache-post-run=no
# Load and enable all available extensions. Use --list-extensions to see a list
# all available extensions.
#enable-all-extensions=
# In error mode, messages with a category besides ERROR or FATAL are
# suppressed, and no reports are done by default. Error mode is compatible with
# disabling specific errors.
#errors-only=
# Always return a 0 (non-error) status code, even if lint errors are found.
# This is primarily useful in continuous integration scripts.
#exit-zero=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
# Specify a score threshold under which the program will exit with error.
fail-under=10.0
# Interpret the stdin as a python script, whose filename needs to be passed as
# the module_or_package argument.
#from-stdin=
# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS
# Add files or directories matching the regular expressions patterns to the
# ignore-list. The regex matches against paths and can be in Posix or Windows
# format. Because '\\' represents the directory delimiter on Windows systems,
# it can't be used as an escape character.
ignore-paths=
# Files or directories matching the regular expression patterns are skipped.
# The regex matches against base names, not paths. The default value ignores
# Emacs file locks
ignore-patterns=
# List of module names for which member attributes should not be checked and
# will not be imported (useful for modules/projects where namespaces are
# manipulated during runtime and thus existing member attributes cannot be
# deduced by static analysis). It supports qualified module names, as well as
# Unix pattern matching.
ignored-modules=numpy
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Resolve imports to .pyi stubs if available. May reduce no-member messages and
# increase not-an-iterable messages.
prefer-stubs=no
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.13
# Discover python modules and packages in the file system subtree.
recursive=no
# Add paths to the list of the source roots. Supports globbing patterns. The
# source root is an absolute path or a path relative to the current working
# directory used to determine a package namespace for modules located under the
# source root.
source-roots=
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_,
t,
x,
q
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
method-rgx=[a-z_][a-z0-9_]{2,40}$
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# Regular expression matching correct parameter specification variable names.
# If left empty, parameter specification variable names will be checked with
# the set naming style.
#paramspec-rgx=
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type alias names. If left empty, type
# alias names will be checked with the set naming style.
#typealias-rgx=
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Regular expression matching correct type variable tuple names. If left empty,
# type variable tuple names will be checked with the set naming style.
#typevartuple-rgx=
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
variable-rgx=[a-z_][a-z0-9_]{2,30}$
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=20
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of positional arguments for function / method.
max-positional-arguments=10
# Maximum number of public methods for a class (see R0904).
max-public-methods=25
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=100
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=builtins.Exception
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )??$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line. Pylint's default of 100 is
# based on PEP 8's guidance that teams may choose line lengths up to 99
# characters.
max-line-length=100
# Maximum number of lines in a module.
max-module-lines=1200
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow explicit reexports by alias from a package __init__.
allow-reexport-from-package=no
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=regsub,
TERMIOS,
Bastion,
rexec
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
# UNDEFINED.
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
use-implicit-booleaness-not-comparison-to-string,
use-implicit-booleaness-not-comparison-to-zero,
fixme,
trailing-newlines,
multiple-statements,
too-few-public-methods,
use-implicit-booleaness-not-len,
useless-object-inheritance,
consider-using-f-string,
unused-argument
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=
[METHOD_ARGS]
# List of qualified names (i.e., library.method) which require a timeout
# parameter e.g. 'requests.api.get,requests.api.post'
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
[MISCELLANEOUS]
# Whether or not to search for fixme's in docstrings.
check-fixme-in-docstring=no
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
notes-rgx=
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=6
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
# Let 'consider-using-join' be raised when the separator to join on would be
# non-empty (resulting in expected fixes of the type: ``"- " + " -
# ".join(items)``)
suggest-join-with-non-empty-separator=yes
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each
# category, as well as 'statement' which is the total number of statements
# analyzed. This score is used by the global evaluation report (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
msg-template=
# Set the output format. Available formats are: 'text', 'parseable',
# 'colorized', 'json2' (improved json format), 'json' (old json format), msvs
# (visual studio) and 'github' (GitHub actions). You can also give a reporter
# class, e.g. mypackage.mymodule.MyReporterClass.
#output-format=
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=yes
# Signatures are removed from the similarity computation
ignore-signatures=yes
# Minimum lines number of a similarity.
min-similarity-lines=5
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. No available dictionaries : You need to install
# both the python package and the system dependency for enchant to work.
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The maximum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy
# Argument names that match this expression will be ignored.
ignored-argument-names=_.*
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,future.builtins
pyulog-1.2.3/pyproject.toml 0000664 0000000 0000000 00000003151 15214172755 0015761 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["setuptools>=77.0.3", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[project]
name = "pyulog"
description = "Python log parser for ULog"
license = "BSD-3-Clause"
dependencies = [
"numpy < 1.25; python_version < '3.9'",
"numpy >= 1.25; python_version >= '3.9'",
]
dynamic = [
"version",
"readme",
]
maintainers = [
{ name = "James Goppert", email = "james.goppert@gmail.com" },
{ name = "Beat Kueng", email = "beat-kueng@gmx.net" },
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Science/Research",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Other",
"Topic :: Software Development",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Scientific/Engineering :: Mathematics",
"Topic :: Scientific/Engineering :: Physics",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Operating System :: Unix",
"Operating System :: MacOS",
]
[project.scripts]
ulog_extract_gps_dump = "pyulog.extract_gps_dump:main"
ulog_info = "pyulog.info:main"
ulog_messages = "pyulog.messages:main"
ulog_params = "pyulog.params:main"
ulog2csv = "pyulog.ulog2csv:main"
ulog2kml = "pyulog.ulog2kml:main"
ulog2ros2bag = "pyulog.ulog2ros2bag:main"
ulog_migratedb = "pyulog.migrate_db:main"
[project.urls]
Homepage = "https://github.com/PX4/pyulog"
Repository = "https://github.com/PX4/pyulog"
[project.optional-dependencies]
test = ['pytest', 'ddt']
[tool.setuptools_scm]
pyulog-1.2.3/pyulog/ 0000775 0000000 0000000 00000000000 15214172755 0014364 5 ustar 00root root 0000000 0000000 pyulog-1.2.3/pyulog/__init__.py 0000664 0000000 0000000 00000000135 15214172755 0016474 0 ustar 00root root 0000000 0000000 """ Wrapper to include the main library modules """
from .core import ULog
from . import px4
pyulog-1.2.3/pyulog/core.py 0000664 0000000 0000000 00000140362 15214172755 0015674 0 ustar 00root root 0000000 0000000 """ Main Module to load and parse an ULog file """
import struct
import copy
import sys
import contextlib
import numpy as np
#pylint: disable=too-many-instance-attributes, unused-argument, missing-docstring
#pylint: disable=protected-access, too-many-branches
__author__ = "Beat Kueng"
# check python version
if sys.hexversion >= 0x030000F0:
_RUNNING_PYTHON3 = True
def _parse_string(cstr, errors='strict'):
return str(cstr, 'utf-8', errors)
else:
_RUNNING_PYTHON3 = False
def _parse_string(cstr):
return str(cstr)
class ULog(object):
"""
This class parses an ulog file
"""
## constants ##
HEADER_BYTES = b'\x55\x4c\x6f\x67\x01\x12\x35'
SYNC_BYTES = b'\x2F\x73\x13\x20\x25\x0C\xBB\x12'
# message types
MSG_TYPE_FORMAT = ord('F')
MSG_TYPE_DATA = ord('D')
MSG_TYPE_INFO = ord('I')
MSG_TYPE_INFO_MULTIPLE = ord('M')
MSG_TYPE_PARAMETER = ord('P')
MSG_TYPE_PARAMETER_DEFAULT = ord('Q')
MSG_TYPE_ADD_LOGGED_MSG = ord('A')
MSG_TYPE_REMOVE_LOGGED_MSG = ord('R')
MSG_TYPE_SYNC = ord('S')
MSG_TYPE_DROPOUT = ord('O')
MSG_TYPE_LOGGING = ord('L')
MSG_TYPE_LOGGING_TAGGED = ord('C')
MSG_TYPE_FLAG_BITS = ord('B')
_UNPACK_TYPES = {
'int8_t': ['b', 1, np.int8],
'uint8_t': ['B', 1, np.uint8],
'int16_t': ['h', 2, np.int16],
'uint16_t': ['H', 2, np.uint16],
'int32_t': ['i', 4, np.int32],
'uint32_t': ['I', 4, np.uint32],
'int64_t': ['q', 8, np.int64],
'uint64_t': ['Q', 8, np.uint64],
'float': ['f', 4, np.float32],
'double': ['d', 8, np.float64],
'bool': ['?', 1, np.int8],
'char': ['c', 1, np.int8]
}
@staticmethod
def get_field_size(type_str):
"""
get the field size in bytes.
:param type_str: type string, eg. 'int8_t'
"""
return ULog._UNPACK_TYPES[type_str][1]
# pre-init unpack structs for quicker use
_unpack_ushort_byte = struct.Struct(' bytes:
if isinstance(value, int):
value_type = "int32_t"
elif isinstance(value, float):
value_type = "float"
else:
raise TypeError("Found unknown parameter value type")
key: str = value_type + ' ' + name
return self._make_info_message_data(key, value, value_type)
def _make_info_message_data(self, key: str, value, value_type: str, continued=None) -> bytes:
key_bytes = bytes(key, 'utf-8')
data = bytearray()
if continued is not None:
data.extend(struct.pack(' 0:
encoded_field = '%s[%d] %s;' % (field_type, array_size, field_name)
else:
encoded_field = '%s %s;' % (field_type, field_name)
data.extend(bytes(encoded_field, 'utf-8'))
header = struct.pack(' 8 + 8 + 3*8:
# we can still parse it but might miss some information
print('Warning: Flags Bit message is longer than expected')
self.compat_flags = list(struct.unpack('<'+'B'*8, data[0:8]))
self.incompat_flags = list(struct.unpack('<'+'B'*8, data[8:16]))
self.appended_offsets = list(struct.unpack('<'+'Q'*3, data[16:16+3*8]))
# remove the 0's at the end
while len(self.appended_offsets) > 0 and self.appended_offsets[-1] == 0:
self.appended_offsets.pop()
class MessageFormat(object):
""" ULog message format representation """
def __init__(self, data, header):
format_arr = ULog.parse_string(data).split(':')
self.name = format_arr[0]
types_str = format_arr[1].split(';')
self.fields = [] # list of tuples (type, array_size, name)
for t in types_str:
if len(t) > 0:
self.fields.append(self._extract_type(t))
def __eq__(self, other):
if not isinstance(other, ULog.MessageFormat):
return NotImplemented
return self.name == other.name and self.fields == other.fields
@staticmethod
def _extract_type(field_str):
field_str_split = field_str.split(' ')
type_str = field_str_split[0]
name_str = field_str_split[1]
a_pos = type_str.find('[')
if a_pos == -1:
array_size = 0
type_name = type_str
else:
b_pos = type_str.find(']')
array_size = int(type_str[a_pos+1:b_pos])
type_name = type_str[:a_pos]
return type_name, array_size, name_str
class MessageLogging(object):
""" ULog logged string message representation """
def __init__(self, data, header):
self.log_level, = struct.unpack(' 0 and
self.field_data[-1].field_name.startswith('_padding')):
self.field_data.pop()
def _parse_nested_type(self, prefix_str, type_name, message_formats):
# we flatten nested types
message_format = message_formats[type_name]
for (type_name_fmt, array_size, field_name) in message_format.fields:
if type_name_fmt in ULog._UNPACK_TYPES:
if array_size > 0:
for i in range(array_size):
self.field_data.append(ULog._FieldData(
prefix_str+field_name+'['+str(i)+']', type_name_fmt))
else:
self.field_data.append(ULog._FieldData(
prefix_str+field_name, type_name_fmt))
if prefix_str+field_name == 'timestamp':
self.timestamp_idx = len(self.field_data) - 1
else: # nested type
if array_size > 0:
for i in range(array_size):
self._parse_nested_type(prefix_str+field_name+'['+str(i)+'].',
type_name_fmt, message_formats)
else:
self._parse_nested_type(prefix_str+field_name+'.',
type_name_fmt, message_formats)
class _MessageData(object):
def __init__(self):
self.timestamp = 0
def initialize(self, data, header, subscriptions, ulog_object) -> bool:
has_corruption = False
msg_id, = ULog._unpack_ushort(data[:2])
if msg_id in subscriptions:
subscription = subscriptions[msg_id]
min_data_size = subscription.dtype.itemsize
data_size = len(data) - 2
if data_size < min_data_size or data_size > subscription.max_data_size:
# Corrupt data: skip
self.timestamp = 0
has_corruption = True
else:
if data_size > min_data_size:
# Strip extra data (_padding bytes)
data = data[:2+min_data_size]
# accumulate data to a buffer, will be parsed later
subscription.buffer += data[2:]
t_off = subscription.timestamp_offset
# TODO: the timestamp can have another size than uint64
self.timestamp, = ULog._unpack_uint64(data[t_off+2:t_off+10])
else:
if not msg_id in ulog_object._filtered_message_ids:
# this is an error, but make it non-fatal
if not msg_id in ulog_object._missing_message_ids:
ulog_object._missing_message_ids.add(msg_id)
if ulog_object._debug:
print(ulog_object._file_handle.tell())
print('Warning: no subscription found for message id {:}. Continuing,'
' but file is most likely corrupt'.format(msg_id))
has_corruption = True
self.timestamp = 0
return has_corruption
def _add_parameter_default(self, msg_param):
""" add a _MessageParameterDefault object """
default_types = msg_param.default_types
while default_types: # iterate over each bit
def_type = default_types & (~default_types+1)
default_types ^= def_type
def_type -= 1
if def_type not in self._default_parameters:
self._default_parameters[def_type] = {}
self._default_parameters[def_type][msg_param.key] = msg_param.value
def _add_message_info_multiple(self, msg_info):
""" add a message info multiple to self._msg_info_multiple_dict """
if msg_info.key in self._msg_info_multiple_dict:
if msg_info.is_continued:
self._msg_info_multiple_dict[msg_info.key][-1].append(msg_info.value)
else:
self._msg_info_multiple_dict[msg_info.key].append([msg_info.value])
else:
self._msg_info_multiple_dict[msg_info.key] = [[msg_info.value]]
self._msg_info_multiple_dict_types[msg_info.key] = msg_info.type
def _load_file(self, log_file, message_name_filter_list, parse_header_only=False):
""" load and parse an ULog file into memory """
if isinstance(log_file, str):
self._file_handle = open(log_file, "rb") #pylint: disable=consider-using-with
else:
self._file_handle = log_file
# parse the whole file
self._read_file_header()
self._last_timestamp = self._start_timestamp
self._read_file_definitions()
if self._debug:
print("header end offset: {:}".format(self._file_handle.tell()))
if parse_header_only:
self._file_handle.close()
del self._file_handle
return
if self.has_data_appended and len(self._appended_offsets) > 0:
if self._debug:
print('This file has data appended')
for offset in self._appended_offsets:
self._read_file_data(message_name_filter_list, read_until=offset)
self._file_handle.seek(offset)
# read the whole file, or the rest if data appended
self._read_file_data(message_name_filter_list)
self._file_handle.close()
del self._file_handle
def _read_file_header(self):
header_data = self._file_handle.read(16)
if len(header_data) != 16:
raise TypeError("Invalid file format (Header too short)")
if header_data[:7] != self.HEADER_BYTES:
raise TypeError("Invalid file format (Failed to parse header)")
self._file_version, = struct.unpack('B', header_data[7:8])
if self._file_version > 1:
print("Warning: unknown file version. Will attempt to read it anyway")
# read timestamp
self._start_timestamp, = ULog._unpack_uint64(header_data[8:])
def _read_file_definitions(self):
header = self._MessageHeader()
while True:
data = self._file_handle.read(3)
if not data:
break
header.initialize(data)
data = self._file_handle.read(header.msg_size)
try:
if header.msg_type == self.MSG_TYPE_INFO:
msg_info = self._MessageInfo(data, header)
self._msg_info_dict[msg_info.key] = msg_info.value
self._msg_info_dict_types[msg_info.key] = msg_info.type
elif header.msg_type == self.MSG_TYPE_INFO_MULTIPLE:
msg_info = self._MessageInfo(data, header, is_info_multiple=True)
self._add_message_info_multiple(msg_info)
elif header.msg_type == self.MSG_TYPE_FORMAT:
msg_format = self.MessageFormat(data, header)
self._message_formats[msg_format.name] = msg_format
elif header.msg_type == self.MSG_TYPE_PARAMETER:
msg_info = self._MessageInfo(data, header)
self._initial_parameters[msg_info.key] = msg_info.value
elif header.msg_type == self.MSG_TYPE_PARAMETER_DEFAULT:
msg_param = self._MessageParameterDefault(data, header)
self._add_parameter_default(msg_param)
elif header.msg_type in (self.MSG_TYPE_ADD_LOGGED_MSG,
self.MSG_TYPE_LOGGING, self.MSG_TYPE_LOGGING_TAGGED):
self._file_handle.seek(-(3+header.msg_size), 1)
break # end of section
elif header.msg_type == self.MSG_TYPE_FLAG_BITS:
# make sure this is the first message in the log
if self._file_handle.tell() != 16 + 3 + header.msg_size:
print('Error: FLAGS_BITS message must be first message. Offset:',
self._file_handle.tell())
msg_flag_bits = self._MessageFlagBits(data, header)
self._compat_flags = msg_flag_bits.compat_flags
self._incompat_flags = msg_flag_bits.incompat_flags
self._appended_offsets = msg_flag_bits.appended_offsets
if self._debug:
print('compat flags: ', self._compat_flags)
print('incompat flags:', self._incompat_flags)
print('appended offsets:', self._appended_offsets)
# check if there are bits set that we don't know
unknown_incompat_flag_msg = \
"Unknown incompatible flag set: cannot parse the log"
if self._incompat_flags[0] & ~1:
raise ValueError(unknown_incompat_flag_msg)
for i in range(1, 8):
if self._incompat_flags[i]:
raise NotImplementedError(unknown_incompat_flag_msg)
else:
if self._debug:
print('read_file_definitions: unknown message type: %i (%s)' %
(header.msg_type, chr(header.msg_type)))
file_position = self._file_handle.tell()
print('file position: %i (0x%x) msg size: %i' % (
file_position, file_position, header.msg_size))
if self._check_packet_corruption(header):
# seek back to advance only by a single byte instead of
# skipping the message
self._file_handle.seek(-2-header.msg_size, 1)
except IndexError:
if not self._file_corrupt:
print("File corruption detected while reading file definitions!")
self._file_corrupt = True
def _find_sync(self, last_n_bytes=-1):
"""
read the file from a given location until the end of sync_byte sequence is found
or an end condition is met(reached EOF or searched all last_n_bytes).
:param last_n_bytes: optional arg to search only last_n_bytes for sync_bytes.
when provided, _find_sync searches for sync_byte sequence in the last_n_bytes
from current location, else, from current location till end of file.
return true if successful, else return false and seek back to initial position and
set _has_sync to false if searched till end of file
"""
sync_seq_found = False
initial_file_position = self._file_handle.tell()
current_file_position = initial_file_position
search_chunk_size = 512 # number of bytes that are searched at once
if last_n_bytes != -1:
current_file_position = self._file_handle.seek(-last_n_bytes, 1)
search_chunk_size = last_n_bytes
chunk = self._file_handle.read(search_chunk_size)
while len(chunk) >= len(ULog.SYNC_BYTES):
current_file_position += len(chunk)
chunk_index = chunk.find(ULog.SYNC_BYTES)
if chunk_index >= 0:
if self._debug:
print("Found sync at %i" % (current_file_position - len(chunk) + chunk_index))
# seek to end of sync sequence and break
current_file_position = self._file_handle.seek(current_file_position - len(chunk)\
+ chunk_index + len(ULog.SYNC_BYTES), 0)
sync_seq_found = True
break
if last_n_bytes != -1:
# we read the whole last_n_bytes and did not find sync
break
# seek back 7 bytes to handle boundary condition and read next chunk
current_file_position = self._file_handle.seek(-(len(ULog.SYNC_BYTES)-1), 1)
chunk = self._file_handle.read(search_chunk_size)
if not sync_seq_found:
current_file_position = self._file_handle.seek(initial_file_position, 0)
if last_n_bytes == -1:
self._has_sync = False
if self._debug:
print("Failed to find sync in file from %i" % initial_file_position)
else:
if self._debug:
print("Failed to find sync in (%i, %i)" %\
(initial_file_position - last_n_bytes, initial_file_position))
else:
# declare file corrupt if we skipped bytes to sync sequence
self._file_corrupt = True
return sync_seq_found
def _read_file_data(self, message_name_filter_list, read_until=None):
"""
read the file data section
:param read_until: an optional file offset: if set, parse only up to
this offset (smaller than)
"""
if read_until is None:
read_until = 1 << 50 # make it larger than any possible log file
try:
# pre-init reusable objects
header = self._MessageHeader()
msg_data = self._MessageData()
curr_file_pos = self._file_handle.tell()
while True:
data = self._file_handle.read(3)
curr_file_pos += len(data)
header.initialize(data)
data = self._file_handle.read(header.msg_size)
curr_file_pos += len(data)
if len(data) < header.msg_size:
break # less data than expected. File is most likely cut
if curr_file_pos > read_until:
if self._debug:
print('read until offset=%i done, current pos=%i' %
(read_until, curr_file_pos))
break
try:
if header.msg_type == self.MSG_TYPE_INFO:
msg_info = self._MessageInfo(data, header)
self._msg_info_dict[msg_info.key] = msg_info.value
self._msg_info_dict_types[msg_info.key] = msg_info.type
elif header.msg_type == self.MSG_TYPE_INFO_MULTIPLE:
msg_info = self._MessageInfo(data, header, is_info_multiple=True)
self._add_message_info_multiple(msg_info)
elif header.msg_type == self.MSG_TYPE_PARAMETER:
msg_info = self._MessageInfo(data, header)
self._changed_parameters.append((self._last_timestamp,
msg_info.key, msg_info.value))
elif header.msg_type == self.MSG_TYPE_PARAMETER_DEFAULT:
msg_param = self._MessageParameterDefault(data, header)
self._add_parameter_default(msg_param)
elif header.msg_type == self.MSG_TYPE_ADD_LOGGED_MSG:
msg_add_logged = self._MessageAddLogged(data, header,
self._message_formats)
if (message_name_filter_list is None or
msg_add_logged.message_name in message_name_filter_list):
self._subscriptions[msg_add_logged.msg_id] = msg_add_logged
else:
self._filtered_message_ids.add(msg_add_logged.msg_id)
elif header.msg_type == self.MSG_TYPE_LOGGING:
msg_logging = self.MessageLogging(data, header)
self._logged_messages.append(msg_logging)
elif header.msg_type == self.MSG_TYPE_LOGGING_TAGGED:
msg_log_tagged = self.MessageLoggingTagged(data, header)
if msg_log_tagged.tag in self._logged_messages_tagged:
self._logged_messages_tagged[msg_log_tagged.tag].append(msg_log_tagged)
else:
self._logged_messages_tagged[msg_log_tagged.tag] = [msg_log_tagged]
elif header.msg_type == self.MSG_TYPE_DATA:
has_corruption = msg_data.initialize(data, header, self._subscriptions,
self)
if has_corruption:
self._file_corrupt = True
elif msg_data.timestamp > self._last_timestamp:
self._last_timestamp = msg_data.timestamp
elif header.msg_type == self.MSG_TYPE_DROPOUT:
msg_dropout = self.MessageDropout(data, header,
self._last_timestamp)
self._dropouts.append(msg_dropout)
elif header.msg_type == self.MSG_TYPE_SYNC:
self._sync_seq_cnt = self._sync_seq_cnt + 1
else:
if self._debug:
print('_read_file_data: unknown message type: %i (%s)' %
(header.msg_type, chr(header.msg_type)))
print('file position: %i msg size: %i' % (
curr_file_pos, header.msg_size))
if self._check_packet_corruption(header):
# seek back to advance only by a single byte instead of
# skipping the message
curr_file_pos = self._file_handle.seek(-2-header.msg_size, 1)
# try recovery with sync sequence in case of unknown msg_type
if self._has_sync:
self._find_sync()
else:
# seek back msg_size to look for sync sequence in payload
if self._has_sync:
self._find_sync(header.msg_size)
except IndexError:
if not self._file_corrupt:
print("File corruption detected while reading file data!")
self._file_corrupt = True
except struct.error:
pass #we read past the end of the file
# convert into final representation
while self._subscriptions:
_, value = self._subscriptions.popitem()
if len(value.buffer) > 0: # only add if we have data
data_item = ULog.Data(value)
self._data_list.append(data_item)
# Sorting is necessary to be able to compare two ULogs correctly
self.data_list.sort(key=lambda ds: (ds.name, ds.multi_id))
def _check_packet_corruption(self, header):
"""
check for data corruption based on an unknown message type in the header
set _file_corrupt flag to true if a corrupt packet is found
We need to handle 2 cases:
- corrupt file (we do our best to read the rest of the file)
- new ULog message type got added (we just want to skip the message)
return true if packet associated with header is corrupt, else return false
"""
data_corrupt = False
if header.msg_type == 0 or header.msg_size == 0 or header.msg_size > 10000:
if not self._file_corrupt and self._debug:
print('File corruption detected')
data_corrupt = True
self._file_corrupt = True
return data_corrupt
def get_version_info(self, key_name='ver_sw_release'):
"""
get the (major, minor, patch, type) version information as tuple.
Returns None if not found
definition of type is:
>= 0: development
>= 64: alpha version
>= 128: beta version
>= 192: RC version
== 255: release version
"""
if key_name in self._msg_info_dict:
val = self._msg_info_dict[key_name]
return ((val >> 24) & 0xff, (val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff)
return None
def get_version_info_str(self, key_name='ver_sw_release'):
"""
get version information in the form 'v1.2.3 (RC)', or None if version
tag either not found or it's a development version
"""
version = self.get_version_info(key_name)
if not version is None and version[3] >= 64:
type_str = ''
if version[3] < 128: type_str = ' (alpha)'
elif version[3] < 192: type_str = ' (beta)'
elif version[3] < 255: type_str = ' (RC)'
return 'v{}.{}.{}{}'.format(version[0], version[1], version[2], type_str)
return None
pyulog-1.2.3/pyulog/db.py 0000664 0000000 0000000 00000074640 15214172755 0015336 0 ustar 00root root 0000000 0000000 '''
Module containing the DatabaseULog class.
'''
import sqlite3
import hashlib
import contextlib
import numpy as np
from pyulog import ULog
# pylint: disable=too-many-instance-attributes
class DatabaseULog(ULog):
'''
This class can be used in place of a ULog, except that it reads from and
writes to a sqlite3 database instead of a file.
The first time you see a ulog file, instantiate a DatabaseULog directly
from a ulog file, and then call save() to write it to the database. Later
it can be accessed by providing the primary_key, upon which this class
loads all needed fields from the database. If you don't have the
primary_key value available, but you have the .ulg file and know it is in
the database, you can retrieve the hash with DatabaseULog.calc_sha256sum
and DatabaseULog.primary_key_from_sha256sum.
This class is currently designed to be write-once only, so you cannot
update existing database entries. To do so, first call delete() and then
save() again.
A weakness of the implementation is that there are some silently failing
states if you don't call save() immediately after instantiating from a ULog
object. The requirement of explicit save() is to prevent unexpected, sudden
side effects, which is considered worse than the current solution.
Example usage:
> from pyulog.db import DatabaseULog
>
> db_handle = DatabaseULog.get_db_handle('dbulog.sqlite3')
> dbulog = DatabaseULog(db_handle, log_file='example.ulg') # Slow
> dbulog.save()
> pk = dbulog.primary_key()
> # [...]
> dbulog = DatabaseULog(db_handle, primary_key=pk) # Fast
SCHEMA_VERSION specifies which version of the database schema (as read from
"PRAGMA user_version") this file is supposed to match. This is done to
ensure consistency between the database operations and the state of the
database. If SCHEMA_VERSION is higher than what is found in the database,
then it means that the datbaase is has not been updated, and the
contsructor will throw an exception. See the documentation of
"ulog_migratedb" for more information.
'''
SCHEMA_VERSION = 5
@staticmethod
def get_db_handle(db_path):
'''
Generate a database handle that can be used in subsequent database access.
'''
def db_handle():
con = sqlite3.connect(
db_path,
detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES
)
# The next line is necessary for sqlite3 to actually respect
# FOREIGN KEY constraints and ON DELETE CASCADE.
con.execute('PRAGMA foreign_keys=on')
return con
return db_handle
@staticmethod
def exists_in_db(db_handle, primary_key):
'''
Check whether an ULog row with Id=primary_key exists in the database
accessed with db_handle.
'''
with db_handle() as con:
cur = con.cursor()
cur.execute('''
SELECT COUNT(*)
FROM ULog
WHERE Id = ?
''', (primary_key,))
count, = cur.fetchone()
cur.close()
return count == 1
@staticmethod
def primary_key_from_sha256sum(db_handle, sha256sum):
'''
Search the database for a row with the given SHA256 digest and returns
the corresponding primary key. Returns None if not found.
'''
primary_key = None
with db_handle() as con:
cur = con.cursor()
cur.execute('''
SELECT Id
FROM ULog
WHERE SHA256Sum = ?
''', (sha256sum,))
row = cur.fetchone()
if row:
primary_key = row[0]
cur.close()
return primary_key
@staticmethod
def calc_sha256sum(log_file):
'''
Compute the SHA256 digest of a file, specified as a file or a valid file path.
'''
if log_file is None:
return None
if isinstance(log_file, str):
file_context = open(log_file, 'rb') # pylint: disable=consider-using-with
elif log_file.closed:
file_context = open(log_file.name, 'rb')
else:
file_context = contextlib.nullcontext(log_file)
file_hash = hashlib.sha256()
with file_context as open_file:
while True:
block = open_file.read(4096)
file_hash.update(block)
if block == b'':
break
return file_hash.hexdigest()
def __init__(self, db_handle, primary_key=None, log_file=None, lazy=True, **kwargs):
'''
You always need the database handle (which can be generated with
DatabaseULog.get_db_handle), but there are two options for the
parameters "primary_key" and "log_file":
- For storing a new log in the database, supply the corresponding
log_file parameter, but leave the primary_key field at None.
- For reading an existing log from the database, supply the desired
primary_key parameter, but leave the log_file parameter at None. If
you don't know the primary_key, you can try finding it with
DatabaseULog.primary_key_from_sha256sum.
You cannot supply both of these parameters.
Furthermore, the "lazy" parameter specifies whether all data fields
should be read on-demand when get_dataset is called, or if they should
all be read and populated into data_list when the object is
instantiated.
The constructor also checks that SCHEMA_VERSION matches the "PRAGMA
user_version" found in the database, see the documentation of "PRAGMA
user_version" for more information.
'''
with db_handle() as con:
cur = con.cursor()
cur.execute('PRAGMA user_version')
(db_version,) = cur.fetchone()
if db_version < DatabaseULog.SCHEMA_VERSION:
raise ValueError('Database version %d < schema version %d, migration needed.' % (
db_version,
DatabaseULog.SCHEMA_VERSION
))
if log_file is not None and primary_key is not None:
raise ValueError('You cannot provide both primary_key and log_file.')
if log_file is None and primary_key is None:
raise ValueError('You must provide either a primary_key or log_file.')
self._pk = primary_key
self._db = db_handle
self._lazy_loaded = lazy
if log_file is not None:
self._sha256sum = DatabaseULog.calc_sha256sum(log_file)
super().__init__(log_file, **kwargs)
if primary_key is not None:
self.load(lazy=lazy)
def __eq__(self, other):
"""
If the other object is a normal ULog, then we just want to compare ULog
data, not DatabaseULog specific fields, because we want to compare
theULog file contents.
"""
if type(other) is ULog: # pylint: disable=unidiomatic-typecheck
return other.__eq__(self)
return super().__eq__(other)
def write_ulog(self, log_file):
if self._lazy_loaded:
raise ValueError('Cannot write after lazy load because it has no datasets.')
super().write_ulog(log_file)
@property
def primary_key(self):
'''The primary key of the ulog, pointing to the correct "ULog" row in the database.'''
return self._pk
@property
def sha256sum(self):
'''The computed SHA256 digest of the file, stored for later use.'''
return self._sha256sum
# pylint: disable=too-many-locals,too-many-branches
def load(self, lazy=True):
'''
Load all necessary data from the database, possibly except for the data series
themselves, which can cost unnecessary resources.
If lazy=True, then the data series will be left unread, deferred until
get_dataset is called. If however lazy=False, then all data series will
be read at once.
Even if the log was originally saved with append_json=True, this
function will always use the faster BLOB column for retrieval.
'''
if not DatabaseULog.exists_in_db(self._db, self._pk):
raise KeyError(f'No ULog in database with Id={self._pk}')
with self._db() as con:
cur = con.cursor()
# ULog metadata
cur.execute('''
SELECT FileVersion,
StartTimestamp,
LastTimestamp,
CompatFlags,
IncompatFlags,
SyncCount,
HasSync,
SHA256Sum
FROM ULog
WHERE Id = ?
''', (self._pk,))
ulog_result = cur.fetchone()
self._file_version = ulog_result[0]
self._start_timestamp = ulog_result[1]
self._last_timestamp = ulog_result[2]
self._compat_flags = [ord(c) for c in ulog_result[3]]
self._incompat_flags = [ord(c) for c in ulog_result[4]]
self._sync_seq_cnt = ulog_result[5]
self._has_sync = ulog_result[6]
self._sha256sum = ulog_result[7]
# appended_offsets
cur.execute('''
SELECT Offset
FROM ULogAppendedOffsets
WHERE ULogId = ?
ORDER BY SeriesIndex
''', (self._pk,))
offsets_result = cur.fetchall()
for offset, in offsets_result:
self._appended_offsets.append(offset)
# data_list
self._data_list = []
cur.execute('''
SELECT DatasetName, MultiId
FROM ULogDataset
WHERE ULogId = ?
ORDER BY DatasetName, MultiId
''', (self._pk,))
dataset_results = cur.fetchall()
for dataset_name, multi_id in dataset_results:
dataset = self.get_dataset(dataset_name,
multi_instance=multi_id,
lazy=lazy,
db_cursor=cur,
caching=False)
self._data_list.append(dataset)
# dropouts
cur.execute('''
SELECT Timestamp, Duration
FROM ULogMessageDropout
WHERE ULogId = ?
''', (self._pk,))
for timestamp, duration in cur.fetchall():
self._dropouts.append(
DatabaseULog.DatabaseMessageDropout(
timestamp=timestamp,
duration=duration,
)
)
# logged_messages
cur.execute('''
SELECT LogLevel, Timestamp, Message
FROM ULogMessageLogging
WHERE ULogId = ?
''', (self._pk,))
for log_level, timestamp, message in cur.fetchall():
self._logged_messages.append(
DatabaseULog.DatabaseMessageLogging(
log_level=log_level,
timestamp=timestamp,
message=message,
)
)
# logged_messages_tagged
cur.execute('''
SELECT LogLevel, Tag, Timestamp, Message
FROM ULogMessageLoggingTagged
WHERE ULogId = ?
''', (self._pk,))
for log_level, tag, timestamp, message in cur.fetchall():
if tag not in self._logged_messages_tagged:
self._logged_messages_tagged[tag] = []
self._logged_messages_tagged[tag].append(
DatabaseULog.DatabaseMessageLoggingTagged(
log_level=log_level,
tag=tag,
timestamp=timestamp,
message=message,
)
)
# message_formats
cur.execute('''
SELECT
msg.Name,
field.FieldType,
field.ArraySize,
field.Name
FROM ULogMessageFormat msg JOIN ULogMessageFormatField field
ON field.MessageId = msg.Id
WHERE ULogId = ?
''', (self._pk,))
for row in cur.fetchall():
msg_name = row[0]
field_data = row[1:]
if msg_name in self._message_formats:
self._message_formats[msg_name].fields.append(field_data)
else:
self._message_formats[msg_name] = DatabaseULog.DatabaseMessageFormat(
name=msg_name,
fields=[field_data]
)
# msg_info_dict
cur.execute('''
SELECT Key, Typename, Value
FROM ULogMessageInfo
WHERE ULogId = ?
''', (self._pk,))
for key, typename, value in cur.fetchall():
self._msg_info_dict[key] = value
self._msg_info_dict_types[key] = typename
# msg_info_multiple_dict
cur.execute('''
SELECT Id, Key, Typename
FROM ULogMessageInfoMultiple
WHERE ULogId = ?
''', (self._pk,))
for message_id, key, typename in cur.fetchall():
self._msg_info_multiple_dict[key] = []
self._msg_info_multiple_dict_types[key] = typename
cur.execute('''
SELECT Id
FROM ULogMessageInfoMultipleList
WHERE MessageId = ?
ORDER BY SeriesIndex
''', (message_id,))
for (list_id,) in cur.fetchall():
cur.execute('''
SELECT Value
FROM ULogMessageInfoMultipleListElement
WHERE ListId = ?
ORDER BY SeriesIndex
''', (list_id,))
self._msg_info_multiple_dict[key].append([value for (value,) in cur.fetchall()])
# initial_parameters
cur.execute('''
SELECT Key, Value
FROM ULogInitialParameter
WHERE ULogId = ?
''', (self._pk,))
for key, value in cur.fetchall():
self._initial_parameters[key] = value
# _default_parameters
cur.execute('''
SELECT DefaultType, Key, Value
FROM ULogDefaultParameter
WHERE ULogId = ?
''', (self._pk,))
for default_type, key, value in cur.fetchall():
if default_type not in self._default_parameters:
self._default_parameters[default_type] = {}
self._default_parameters[default_type][key] = value
# changed_parameters
cur.execute('''
SELECT Timestamp, Key, Value
FROM ULogChangedParameter
WHERE ULogId = ?
''', (self._pk,))
for timestamp, key, value in cur.fetchall():
self._changed_parameters.append((timestamp, key, value))
cur.close()
self._lazy_loaded = lazy
def get_dataset(self, name, multi_instance=0, lazy=False, db_cursor=None, caching=True):
'''
Access a specific dataset and its data series from the database.
The "lazy" argument specifies whether only dataset metadata should be
retrieved from the database, or if the data series arrays should be
retrieved too.
The optional "db_cursor" argument can be used to avoid re-opening the
database connection each time get_dataset is called.
Since we don't expect the data to change often, we will normally use
self._data_list as a cache, and check there before reading from the
database. However, if caching=False, then we will always read anew
from the database.
'''
if db_cursor is None:
db_context = self._db()
cur = db_context.cursor()
else:
db_context = contextlib.nullcontext()
cur = db_cursor
existing_dataset = None
for dataset in self._data_list:
if dataset.name == name and dataset.multi_id == multi_instance:
existing_dataset = dataset
break
if (caching
and existing_dataset is not None
and (lazy or existing_dataset.data)):
return existing_dataset
with db_context:
cur.execute('''
SELECT Id, TimestampIndex, MessageId
FROM ULogDataset
WHERE DatasetName = ? AND MultiId = ? AND ULogId = ?
''', (name, multi_instance, self._pk))
dataset_result = cur.fetchone()
if dataset_result is None:
raise KeyError(f'Dataset with name {name} and multi id {multi_instance} not found.')
dataset_id, timestamp_idx, msg_id = dataset_result
fields = []
data = {}
if not lazy:
cur.execute('''
SELECT TopicName, DataType, ValueArray
FROM ULogField
WHERE DatasetId = ?
''', (dataset_id,))
field_results = cur.fetchall()
for field_name, data_type, value_bytes in field_results:
fields.append(DatabaseULog._FieldData(field_name=field_name,type_str=data_type))
dtype = DatabaseULog._UNPACK_TYPES[data_type][2]
data[field_name] = np.frombuffer(value_bytes, dtype=dtype)
# If caching=True but there is no existing dataset we could append a
# new one to self._data_list, but that could be considered a
# non-obvious side effect.
if caching and existing_dataset is not None:
existing_dataset.msg_id = msg_id
existing_dataset.timestamp_idx = timestamp_idx
existing_dataset.field_data = fields
existing_dataset.data = data
dataset = existing_dataset
else:
dataset = DatabaseULog.DatabaseData(
name=name,
multi_id=multi_instance,
msg_id=msg_id,
timestamp_idx=timestamp_idx,
field_data=fields,
data=data,
)
return dataset
def save(self, append_json=False):
'''
Save the DatabaseULog to the database. Throws a KeyError if the primary
key is already in the database.
The datasets are stored in a sqlite BLOB for fast retrieval, but if
append_json=True, then datasets are additionally stored in a JSON
field. This allows them to be directly queried using the sqlite
function json_each, but increases the writing time and database size.
'''
if self._pk is not None:
raise KeyError('Cannot save logs that are already in the database')
pk_from_hash = DatabaseULog.primary_key_from_sha256sum(self._db, self._sha256sum)
if pk_from_hash is not None:
self._pk = pk_from_hash
raise KeyError(f'Hash {self._sha256sum} already in database with Id={pk_from_hash}')
with self._db() as con:
cur = con.cursor()
# ULog metadata
cur.execute('''
INSERT INTO ULog
(FileVersion, StartTimestamp, LastTimestamp, CompatFlags, IncompatFlags, SyncCount, HasSync, SHA256Sum)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?)
''', (
self._file_version,
self._start_timestamp,
self._last_timestamp,
''.join([chr(n) for n in self._compat_flags]),
''.join([chr(n) for n in self._incompat_flags]),
self._sync_seq_cnt,
self._has_sync,
self._sha256sum,
)
)
self._pk = cur.lastrowid
# appended_offsets
cur.executemany('''
INSERT INTO ULogAppendedOffsets
(SeriesIndex, Offset, ULogId)
VALUES
(?, ?, ?)
''', [(
list_index,
offset,
self._pk,
) for list_index, offset in enumerate(self._appended_offsets)])
# data_list
for dataset in self.data_list:
cur.execute('''
INSERT INTO ULogDataSet
(DatasetName, MultiId, MessageId, TimestampIndex, ULogId)
VALUES
(?, ?, ?, ?, ?)
''', (
dataset.name,
dataset.multi_id,
dataset.msg_id,
dataset.timestamp_idx,
self._pk,
)
)
dataset_id = cur.lastrowid
for field in dataset.field_data:
values = dataset.data[field.field_name]
values_bytes = values.tobytes()
if append_json:
# Precision is only good enough up to a few decimals,
# depending on the default float formatter. The
# function np.array2string was tested, but was slower.
# Also note that doing the slow json.dumps is also
# unnecessary since we know that the object to be
# formatted is an array.
timestamp_list = [str(s) for s in dataset.data['timestamp'].tolist()]
values_json = str(
dict(zip(timestamp_list, values.tolist()))
).replace('nan', 'null').replace('inf', 'null').replace("'", '"')
json_placeholder = 'json(?)' # Saves some space
else:
values_json = None
json_placeholder = '?'
cur.execute(f'''
INSERT INTO ULogField
(TopicName, DataType, ValueArray, ValueJson, DatasetId)
VALUES
(?, ?, ?, {json_placeholder}, ?)
''', (
field.field_name,
field.type_str,
values_bytes,
values_json,
dataset_id,
)
)
# dropouts
cur.executemany('''
INSERT INTO ULogMessageDropout
(Timestamp, Duration, ULogId)
VALUES
(?, ?, ?)
''', [(
dropout.timestamp,
dropout.duration,
self._pk,
) for dropout in self._dropouts])
# logged_messages
cur.executemany('''
INSERT INTO ULogMessageLogging
(LogLevel, Timestamp, Message, ULogId)
VALUES
(?, ?, ?, ?)
''', [(
message.log_level,
message.timestamp,
message.message,
self._pk,
) for message in self._logged_messages])
# logged_messages_tagged
for tag, messages in self._logged_messages_tagged.items():
cur.executemany('''
INSERT INTO ULogMessageLoggingTagged
(LogLevel, Timestamp, Tag, Message, ULogId)
VALUES
(?, ?, ?, ?, ?)
''', [(
message.log_level,
message.timestamp,
tag,
message.message,
self._pk,
) for message in messages])
# message_formats
for name, message_format in self._message_formats.items():
cur.execute('''
INSERT INTO ULogMessageFormat
(Name, ULogId)
VALUES
(?, ?)
''', (name, self._pk))
format_id = cur.lastrowid
cur.executemany('''
INSERT INTO ULogMessageFormatField
(FieldType, ArraySize, Name, MessageId)
VALUES
(?, ?, ?, ?)
''', [(*field, format_id) for field in message_format.fields])
# msg_info_dict
cur.executemany('''
INSERT INTO ULogMessageInfo
(Key, Value, Typename, ULogId)
VALUES
(?, ?, ?, ?)
''', [(
key,
value,
self._msg_info_dict_types[key],
self._pk,
) for key, value in self.msg_info_dict.items()])
# msg_info_multiple_dict
for key, lists in self.msg_info_multiple_dict.items():
cur.execute('''
INSERT INTO ULogMessageInfoMultiple
(Key, Typename, ULogId)
VALUES
(?, ?, ?)
''', (key, self._msg_info_multiple_dict_types[key], self._pk))
message_id = cur.lastrowid
for list_index, message_list in enumerate(lists):
cur.execute('''
INSERT INTO ULogMessageInfoMultipleList
(SeriesIndex, MessageId)
VALUES
(?, ?)
''', (list_index, message_id))
list_id = cur.lastrowid
cur.executemany('''
INSERT INTO ULogMessageInfoMultipleListElement
(SeriesIndex, Value, ListId)
VALUES
(?, ?, ?)
''', [(
series_index,
value,
list_id,
) for series_index, value in enumerate(message_list)])
# initial_parameters
cur.executemany('''
INSERT INTO ULogInitialParameter
(Key, Value, ULogId)
VALUES
(?, ?, ?)
''', [(
key,
value,
self._pk,
) for key, value in self.initial_parameters.items()])
# _default_parameters
for default_type, parameters in self._default_parameters.items():
cur.executemany('''
INSERT INTO ULogDefaultParameter
(DefaultType, Key, Value, ULogId)
VALUES
(?, ?, ?, ?)
''', [(
default_type,
key,
value,
self._pk,
) for key, value in parameters.items()])
# changed_parameters
cur.executemany('''
INSERT INTO ULogChangedParameter
(Timestamp, Key, Value, ULogId)
VALUES
(?, ?, ?, ?)
''', [(
timestamp,
key,
value,
self._pk,
) for timestamp, key, value in self.changed_parameters])
cur.close()
def delete(self):
'''
Deletes the ULog row and cascading rows from the database.
'''
if self._pk is None:
raise KeyError('Cannot delete logs that are not in the database')
with self._db() as con:
cur = con.cursor()
cur.execute('''
DELETE FROM ULog
WHERE Id = ?
''', (self._pk,)
)
cur.close()
self._pk = None
class DatabaseData(ULog.Data):
'''
Overrides the ULog.Data class since its constructor only
reads ULog.MessageLogAdded objects, and we want to specify
the fields directly.
'''
# pylint: disable=super-init-not-called,too-many-arguments
def __init__(self, name, multi_id, msg_id, timestamp_idx, field_data, data=None):
self.name = name
self.multi_id = multi_id
self.msg_id = msg_id
self.timestamp_idx = timestamp_idx
self.field_data = field_data
self.data = data
class DatabaseMessageDropout(ULog.MessageDropout):
'''
Overrides the ULog.MessageDropout class since its
constructor is not suitable for out purpose.
'''
# pylint: disable=super-init-not-called
def __init__(self, timestamp, duration):
self.timestamp = timestamp
self.duration = duration
class DatabaseMessageFormat(ULog.MessageFormat):
'''
Overrides the ULog.MessageFormat class since its
constructor is not suitable for out purpose.
'''
# pylint: disable=super-init-not-called
def __init__(self, name, fields):
self.name = name
self.fields = fields
class DatabaseMessageLogging(ULog.MessageLogging):
'''
Overrides the ULog.MessageLogging class since its
constructor is not suitable for out purpose.
'''
# pylint: disable=super-init-not-called
def __init__(self, log_level, timestamp, message):
self.log_level = log_level
self.timestamp = timestamp
self.message = message
class DatabaseMessageLoggingTagged(ULog.MessageLoggingTagged):
'''
Overrides the ULog.MessageLoggingTagged class since its
constructor is not suitable for our purpose.
'''
# pylint: disable=super-init-not-called
def __init__(self, log_level, tag, timestamp, message):
self.log_level = log_level
self.tag = tag
self.timestamp = timestamp
self.message = message
pyulog-1.2.3/pyulog/extract_gps_dump.py 0000664 0000000 0000000 00000006765 15214172755 0020324 0 ustar 00root root 0000000 0000000 #! /usr/bin/env python
"""
Extract the raw gps communication from an ULog file.
"""
import argparse
import os
import sys
import numpy as np
from .core import ULog
#pylint: disable=too-many-locals, unused-wildcard-import, wildcard-import
def main():
"""
Command line interface
"""
parser = argparse.ArgumentParser(
description='Extract the raw gps communication from an ULog file')
parser.add_argument('filename', metavar='file.ulg', help='ULog input file')
def is_valid_directory(parser, arg):
"""Check if valid directory"""
if not os.path.isdir(arg):
parser.error('The directory {} does not exist'.format(arg))
# File exists so return the directory
return arg
parser.add_argument('-o', '--output', dest='output', action='store',
help='Output directory (default is CWD)',
metavar='DIR', type=lambda x: is_valid_directory(parser, x))
parser.add_argument('-x', '--ignore', dest='ignore', action='store_true',
help='Ignore string parsing exceptions', default=False)
parser.add_argument('-i', '--instance', dest='required_instance', action='store',
help='GPS instance. Use 0 (default)'
+ 'for main GPS, 1 for secondary GPS reciever.',
default=0)
args = parser.parse_args()
ulog_file_name = args.filename
disable_str_exceptions = args.ignore
required_instance = int(args.required_instance)
msg_filter = ['gps_dump']
ulog = ULog(ulog_file_name, msg_filter, disable_str_exceptions)
data = ulog.data_list
output_file_prefix = os.path.basename(ulog_file_name)
# strip '.ulg'
if output_file_prefix.lower().endswith('.ulg'):
output_file_prefix = output_file_prefix[:-4]
# write to different output path?
if args.output is not None:
output_file_prefix = os.path.join(args.output, output_file_prefix)
to_dev_filename = output_file_prefix + '_' + str(required_instance) + '_to_device.dat'
from_dev_filename = output_file_prefix + '_' + str(required_instance) + '_from_device.dat'
if len(data) == 0:
print("File {0} does not contain gps_dump messages!".format(ulog_file_name))
sys.exit(0)
gps_dump_data = data[0]
# message format check
field_names = [f.field_name for f in gps_dump_data.field_data]
if not 'len' in field_names or not 'data[0]' in field_names:
print('Error: gps_dump message has wrong format')
sys.exit(-1)
if len(ulog.dropouts) > 0:
print("Warning: file contains {0} dropouts".format(len(ulog.dropouts)))
print("Creating files {0} and {1}".format(to_dev_filename, from_dev_filename))
with open(to_dev_filename, 'wb') as to_dev_file:
with open(from_dev_filename, 'wb') as from_dev_file:
msg_lens = gps_dump_data.data['len']
instances = gps_dump_data.data.get('instance', [0]*len(msg_lens))
for i in range(len(gps_dump_data.data['timestamp'])):
instance = instances[i]
msg_len = msg_lens[i]
if instance == required_instance:
if msg_len & (1<<7):
msg_len = msg_len & ~(np.uint8(1) << 7)
file_handle = to_dev_file
else:
file_handle = from_dev_file
for k in range(msg_len):
file_handle.write(gps_dump_data.data['data['+str(k)+']'][i])
pyulog-1.2.3/pyulog/extract_message.py 0000664 0000000 0000000 00000003563 15214172755 0020123 0 ustar 00root root 0000000 0000000 """
Extract values from a ULog file message to use in scripting
"""
from typing import List
import numpy as np
from .core import ULog
def extract_message(ulog_file_name: str, message: str,
time_s: "int | None" = None, time_e: "int | None" = None,
disable_str_exceptions: bool = False) -> List[dict]:
"""
Extract values from a ULog file
:param ulog_file_name: (str) The ULog filename to open and read
:param message: (str) A ULog message to return values from
:param time_s: (int) Offset time for conversion in seconds
:param time_e: (int) Limit until time for conversion in seconds
:return: (List[dict]) A list of each record from the ULog as key-value pairs
"""
if not isinstance(message, str):
raise AttributeError("Must provide a message to pull from ULog file")
ulog = ULog(ulog_file_name, message, disable_str_exceptions)
try:
data = ulog.get_dataset(message)
except Exception as exc:
raise AttributeError("Provided message is not in the ULog file") from exc
values = []
# use same field order as in the log, except for the timestamp
data_keys = [f.field_name for f in data.field_data]
data_keys.remove('timestamp')
data_keys.insert(0, 'timestamp') # we want timestamp at first position
#get the index for row where timestamp exceeds or equals the required value
time_s_i = np.where(data.data['timestamp'] >= time_s * 1e6)[0][0] \
if time_s else 0
#get the index for row upto the timestamp of the required value
time_e_i = np.where(data.data['timestamp'] >= time_e * 1e6)[0][0] \
if time_e else len(data.data['timestamp'])
# write the data
for i in range(time_s_i, time_e_i):
row = {}
for key in data_keys:
row[key] = data.data[key][i]
values.append(row)
return values
pyulog-1.2.3/pyulog/info.py 0000664 0000000 0000000 00000010447 15214172755 0015677 0 ustar 00root root 0000000 0000000 #! /usr/bin/env python
"""
Display information from an ULog file
"""
import argparse
from .core import ULog
#pylint: disable=too-many-locals, unused-wildcard-import, wildcard-import
#pylint: disable=invalid-name
def show_info(ulog, verbose):
"""Show general information from an ULog"""
if ulog.file_corruption:
print("Warning: file has data corruption(s)")
m1, s1 = divmod(int(ulog.start_timestamp/1e6), 60)
h1, m1 = divmod(m1, 60)
m2, s2 = divmod(int((ulog.last_timestamp - ulog.start_timestamp)/1e6), 60)
h2, m2 = divmod(m2, 60)
print("Logging start time: {:d}:{:02d}:{:02d}, duration: {:d}:{:02d}:{:02d}".format(
h1, m1, s1, h2, m2, s2))
dropout_durations = [dropout.duration for dropout in ulog.dropouts]
if len(dropout_durations) == 0:
print("No Dropouts")
else:
print("Dropouts: count: {:}, total duration: {:.1f} s, max: {:} ms, mean: {:} ms"
.format(len(dropout_durations), sum(dropout_durations)/1000.,
max(dropout_durations),
int(sum(dropout_durations)/len(dropout_durations))))
version = ulog.get_version_info_str()
if not version is None:
print('SW Version: {}'.format(version))
print("Info Messages:")
for k in sorted(ulog.msg_info_dict):
if not k.startswith('perf_') or verbose:
print(" {0}: {1}".format(k, ulog.msg_info_dict[k]))
if len(ulog.msg_info_multiple_dict) > 0:
if verbose:
print("Info Multiple Messages:")
for k in sorted(ulog.msg_info_multiple_dict):
print(" {0}: {1}".format(k, ulog.msg_info_multiple_dict[k]))
else:
print("Info Multiple Messages: {}".format(
", ".join(["[{}: {}]".format(k, len(ulog.msg_info_multiple_dict[k])) for k in
sorted(ulog.msg_info_multiple_dict)])))
print("")
print("{:<41} {:7}, {:10}".format("Name (multi id, message size in bytes)",
"number of data points", "total bytes"))
data_list_sorted = sorted(ulog.data_list, key=lambda d: d.name + str(d.multi_id))
for d in data_list_sorted:
message_size = sum(ULog.get_field_size(f.type_str) for f in d.field_data)
num_data_points = len(d.data['timestamp'])
name_id = "{:} ({:}, {:})".format(d.name, d.multi_id, message_size)
print(" {:<40} {:7d} {:10d}".format(name_id, num_data_points,
message_size * num_data_points))
def main():
"""Commande line interface"""
parser = argparse.ArgumentParser(description='Display information from an ULog file')
parser.add_argument('filename', metavar='file.ulg', help='ULog input file')
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
help='Verbose output', default=False)
parser.add_argument('-m', '--message', dest='message',
help='Show a specific Info Multiple Message')
parser.add_argument('-n', '--newline', dest='newline', action='store_true',
help='Add newline separators (only with --message)', default=False)
parser.add_argument('-i', '--ignore', dest='ignore', action='store_true',
help='Ignore string parsing exceptions', default=False)
args = parser.parse_args()
ulog_file_name = args.filename
disable_str_exceptions = args.ignore
ulog = ULog(ulog_file_name, None, disable_str_exceptions)
message = args.message
if message:
separator = ""
if args.newline: separator = "\n"
if len(ulog.msg_info_multiple_dict) > 0 and message in ulog.msg_info_multiple_dict:
message_info_multiple = ulog.msg_info_multiple_dict[message]
for i, m in enumerate(message_info_multiple):
if len(m) > 0 and isinstance(m[0], (bytes, bytearray)):
print("# {} {} (len: {:}):".format(message, i, sum(len(item) for item in m)))
print(separator.join(' '.join('{:02x}'.format(x) for x in item) for item in m))
else:
print("# {} {}:".format(message, i))
print(separator.join(m))
else:
print("message {} not found".format(message))
else:
show_info(ulog, args.verbose)
pyulog-1.2.3/pyulog/libevents_parse 0000777 0000000 0000000 00000000000 15214172755 0031166 2../3rd_party/libevents/libs/python/libevents_parse ustar 00root root 0000000 0000000 pyulog-1.2.3/pyulog/messages.py 0000664 0000000 0000000 00000003177 15214172755 0016555 0 ustar 00root root 0000000 0000000 #! /usr/bin/env python
"""
Display logged messages from an ULog file
"""
import argparse
from .core import ULog
from .px4_events import PX4Events
#pylint: disable=invalid-name
def main():
"""Commande line interface"""
parser = argparse.ArgumentParser(description='Display logged messages from an ULog file')
parser.add_argument('filename', metavar='file.ulg', help='ULog input file')
parser.add_argument('-i', '--ignore', dest='ignore', action='store_true',
help='Ignore string parsing exceptions', default=False)
args = parser.parse_args()
ulog_file_name = args.filename
disable_str_exceptions = args.ignore
msg_filter = ['event']
ulog = ULog(ulog_file_name, msg_filter, disable_str_exceptions)
logged_messages = [(m.timestamp, m.log_level_str(), m.message) for m in ulog.logged_messages]
# If this is a PX4 log, try to get the events too
if ulog.msg_info_dict.get('sys_name', '') == 'PX4':
px4_events = PX4Events()
events = px4_events.get_logged_events(ulog)
for t, log_level, message in logged_messages:
# backwards compatibility: a string message with appended tab is output
# in addition to an event with the same message so we can ignore those
if message[-1] == '\t':
continue
events.append((t, log_level, message))
logged_messages = sorted(events, key=lambda m: m[0])
for t, log_level, message in logged_messages:
m1, s1 = divmod(int(t/1e6), 60)
h1, m1 = divmod(m1, 60)
print("{:d}:{:02d}:{:02d} {:}: {:}".format(h1, m1, s1, log_level, message))
pyulog-1.2.3/pyulog/migrate_db.py 0000664 0000000 0000000 00000014005 15214172755 0017033 0 ustar 00root root 0000000 0000000 '''
Tool for handling changes in the database schema. This is necessary for
avoiding breaking backwards compatibility whenver bugs are discovered in the
database model, or if the ULog format changes.
There are some options available, such as "alembic" or "migrations', but these
seem like overkill for us. For instance, we don't really need to migrate both up and
down, just up.
'''
import os
import argparse
from pyulog.db import DatabaseULog
def main():
'''
Entry point for the console script.
'''
parser = argparse.ArgumentParser(description='Setup the database for DatabaseULog')
parser.add_argument('-d', '--database', dest='db_path', action='store',
help='Path to the database file',
default='pyulog.sqlite3')
# The noop flag actually has a side effect if it is called on an uncreated
# database, since the "PRAGMA user_version" command implicitly creates the
# database. The created database will have user_version = 0, which will
# later confuse the migration tool. however, this edge case will mostly be
# relevant for advanced users, and can be handled with the -f flag.
parser.add_argument('-n', '--noop', dest='noop', action='store_true',
help='Only print results, do not execute migration scripts.',
default=False)
parser.add_argument('-s', '--sql', dest='sql_dir', action='store',
help='Directory with migration SQL files',
default=None)
parser.add_argument('-f', '--force', dest='force', action='store_true',
help=('Run the migration script even if the database is not created'
'with this script.'),
default=False)
args = parser.parse_args()
migrate_db(args.db_path, sql_dir=args.sql_dir, noop=args.noop, force=args.force)
def _read_db_schema_version(db_path, force):
'''
Read and validate the schema version defined by the "PRAGMA user_version"
field in the database. If the database file exists and schema version is 0,
then this means that the database was not created with the migration tool.
This means that the database is in a state unknown to the migration tool,
and hence a migration could cause schema corruption. The default behavior
in this case is to reject the migration, but it can be overriden with
force=True.
'''
db_handle = DatabaseULog.get_db_handle(db_path)
if not os.path.isfile(db_path):
print(f'Database file {db_path} not found, creating it from scratch.')
return 0
print(f'Found database file {db_path}.')
with db_handle() as con:
cur = con.cursor()
cur.execute('PRAGMA user_version')
(db_schema_version,) = cur.fetchone()
cur.close()
if db_schema_version is None:
raise ValueError(f'Could not fetch database schema version for {db_path}.')
if db_schema_version == 0 and not force:
raise FileExistsError('Database has user_version = 0, rejecting migration.'
'Use the "force" flag to migrate anyway.')
if not isinstance(db_schema_version, int) or db_schema_version < 0:
raise ValueError(f'Invalid database schema version {db_schema_version}.')
return db_schema_version
def _read_migration_file(migration_id, sql_dir):
'''
Read the migration file with id "migration_id" in directory "sql_dir", and
check that it handles transactions strictly.
'''
migration_filename_format = os.path.join(sql_dir, 'pyulog.{migration_id}.sql')
migration_filename = migration_filename_format.format(migration_id=migration_id)
if not os.path.exists(migration_filename):
raise FileNotFoundError(f'Migration file {migration_filename} does not exist. '
f'Stopped after migration {migration_id}.')
with open(migration_filename, 'r', encoding='utf8') as migration_file:
migration_lines = migration_file.read()
if not migration_lines.strip().startswith('BEGIN;'):
raise ValueError(f'Migration file {migration_filename} must start with "BEGIN;"')
if not migration_lines.strip().endswith('COMMIT;'):
raise ValueError(f'Migration file {migration_filename} must end with "COMMIT;"')
migration_lines += f'\nPRAGMA user_version = {migration_id};'
return migration_filename, migration_lines
def migrate_db(db_path, sql_dir=None, noop=False, force=False):
'''
Apply database migrations that have not yet been applied.
Compares "PRAGMA user_version" from the sqlite3 database at "db_path" with
the SCHEMA_VERSION in the DatabaseULog class. If the former is larger than
the latter, then migration scripts will be read and executed from files in
"sql_dir", and the user_version will be incremented, until the database is
up to date.
'''
if sql_dir is None:
module_dir = os.path.dirname(os.path.realpath(os.path.abspath(__file__)))
sql_dir = os.path.join(module_dir, 'sql')
if not os.path.isdir(sql_dir):
raise NotADirectoryError(f'{sql_dir} is not a directory.')
print(f'Using migration files in {sql_dir}.')
db_schema_version = _read_db_schema_version(db_path, force)
class_schema_version = DatabaseULog.SCHEMA_VERSION
print('Current schema version: {} (database) and {} (code).'.format(
db_schema_version,
class_schema_version,
))
db_handle = DatabaseULog.get_db_handle(db_path)
with db_handle() as con:
cur = con.cursor()
for migration_id in range(db_schema_version+1,
DatabaseULog.SCHEMA_VERSION+1):
migration_filename, migration_lines = _read_migration_file(migration_id, sql_dir)
print(f'Executing {migration_filename}.')
if noop:
print(migration_lines)
else:
cur.executescript(migration_lines)
cur.close()
print('Migration done.')
return db_path
if __name__ == '__main__':
raise SystemExit(main())
pyulog-1.2.3/pyulog/params.py 0000664 0000000 0000000 00000013031 15214172755 0016217 0 ustar 00root root 0000000 0000000 #! /usr/bin/env python
"""
Extract parameters from an ULog file
"""
import argparse
import sys
from .core import ULog
#pylint: disable=unused-variable, too-many-branches, consider-using-with
def get_defaults(ulog, default):
""" get default params from ulog """
assert ulog.has_default_parameters, "Log does not contain default parameters"
if default == 'system': return ulog.get_default_parameters(0)
if default == 'current_setup': return ulog.get_default_parameters(1)
raise ValueError('invalid value \'{}\' for --default'.format(default))
def main():
"""Commande line interface"""
parser = argparse.ArgumentParser(description='Extract parameters from an ULog file')
parser.add_argument('filename', metavar='file.ulg', help='ULog input file')
parser.add_argument('-l', '--delimiter', dest='delimiter', action='store',
help='Use delimiter in CSV (default is \',\')', default=',')
parser.add_argument('-i', '--initial', dest='initial', action='store_true',
help='Only extract initial parameters. (octave|csv)', default=False)
parser.add_argument('-t', '--timestamps', dest='timestamps', action='store_true',
help='Extract changed parameters with timestamps. (csv)', default=False)
parser.add_argument('-f', '--format', dest='format', action='store', type=str,
help='csv|octave|qgc', default='csv')
parser.add_argument('output_filename', metavar='params.txt',
type=str, nargs='?',
help='Output filename (default=stdout)', default=None)
parser.add_argument('--ignore', dest='ignore', action='store_true',
help='Ignore string parsing exceptions', default=False)
parser.add_argument('-d', '--default', dest='default', action='store', type=str,
help='Select default param values instead of configured '
'values (implies --initial). Valid values: system|current_setup',
default=None)
args = parser.parse_args()
ulog_file_name = args.filename
disable_str_exceptions = args.ignore
message_filter = []
if not args.initial: message_filter = None
ulog = ULog(ulog_file_name, message_filter, disable_str_exceptions)
params = ulog.initial_parameters
if args.default is not None:
params = get_defaults(ulog, args.default)
args.initial = True
param_keys = sorted(params.keys())
delimiter = args.delimiter
output_file = sys.stdout if args.output_filename is None \
else open(args.output_filename, 'w', encoding='utf-8')
if args.format == "csv":
for param_key in param_keys:
output_file.write(param_key)
if args.timestamps:
output_file.write(delimiter)
output_file.write(str(params[param_key]))
for t, name, value in ulog.changed_parameters:
if name == param_key:
output_file.write(delimiter)
output_file.write(str(value))
output_file.write('\n')
output_file.write("timestamp")
output_file.write(delimiter)
output_file.write('0')
for t, name, value in ulog.changed_parameters:
if name == param_key:
output_file.write(delimiter)
output_file.write(str(t))
output_file.write('\n')
else:
output_file.write(delimiter)
output_file.write(str(params[param_key]))
if not args.initial:
for t, name, value in ulog.changed_parameters:
if name == param_key:
output_file.write(delimiter)
output_file.write(str(value))
output_file.write('\n')
elif args.format == "octave":
for param_key in param_keys:
output_file.write('# name ')
output_file.write(param_key)
values = [params[param_key]]
if not args.initial:
for t, name, value in ulog.changed_parameters:
if name == param_key:
values += [value]
if len(values) > 1:
output_file.write('\n# type: matrix\n')
output_file.write('# rows: 1\n')
output_file.write('# columns: ')
output_file.write(str(len(values)) + '\n')
for value in values:
output_file.write(str(value) + ' ')
else:
output_file.write('\n# type: scalar\n')
output_file.write(str(values[0]))
output_file.write('\n')
elif args.format == "qgc":
for param_key in param_keys:
sys_id = 1
comp_id = 1
delimiter = '\t'
param_value = params[param_key]
output_file.write(str(sys_id))
output_file.write(delimiter)
output_file.write(str(comp_id))
output_file.write(delimiter)
output_file.write(param_key)
output_file.write(delimiter)
output_file.write(str(param_value))
output_file.write(delimiter)
if isinstance(param_value, float):
# Float
param_type = 9
else:
# Int
param_type = 6
output_file.write(str(param_type))
output_file.write('\n')
pyulog-1.2.3/pyulog/px4.py 0000664 0000000 0000000 00000010521 15214172755 0015450 0 ustar 00root root 0000000 0000000 """
PX4-specific ULog helper
"""
import numpy as np
__author__ = "Beat Kueng"
class PX4ULog(object):
"""
This class contains PX4-specific ULog things (field names, etc.)
"""
def __init__(self, ulog_object):
"""
@param ulog_object: ULog instance
"""
self._ulog = ulog_object
def get_mav_type(self):
""" return the MAV type as string from initial parameters """
mav_type = self._ulog.initial_parameters.get('MAV_TYPE', None)
return {0: 'Generic',
1: 'Fixed Wing',
2: 'Quadrotor',
3: 'Coaxial helicopter',
4: 'Normal helicopter with tail rotor',
5: 'Ground installation',
6: 'Ground Control Station',
7: 'Airship, controlled',
8: 'Free balloon, uncontrolled',
9: 'Rocket',
10: 'Ground Rover',
11: 'Surface Vessel, Boat, Ship',
12: 'Submarine',
13: 'Hexarotor',
14: 'Octorotor',
15: 'Tricopter',
16: 'Flapping wing',
17: 'Kite',
18: 'Onboard Companion Controller',
19: 'Two-rotor VTOL (Tailsitter)',
20: 'Quad-rotor VTOL (Tailsitter)',
21: 'Tiltrotor VTOL',
22: 'VTOL Standard',
23: 'VTOL Tailsitter',
24: 'VTOL reserved 4',
25: 'VTOL reserved 5',
26: 'Onboard Gimbal',
27: 'Onboard ADSB Peripheral'}.get(mav_type, 'unknown type')
def get_estimator(self):
"""return the configured estimator as string from initial parameters"""
mav_type = self._ulog.initial_parameters.get('MAV_TYPE', None)
if mav_type == 1: # fixed wing always uses EKF2
return 'EKF2'
mc_est_group = self._ulog.initial_parameters.get('SYS_MC_EST_GROUP', 2)
return {0: 'INAV',
1: 'LPE',
2: 'EKF2',
3: 'Q'}.get(mc_est_group, 'unknown ({})'.format(mc_est_group))
def add_roll_pitch_yaw(self, messages=None):
""" convenience method to add the fields 'roll', 'pitch', 'yaw' to the
loaded data using the quaternion fields (does not update field_data).
By default, messages are: 'vehicle_attitude.q',
'vehicle_attitude_setpoint.q_d', 'vehicle_attitude_groundtruth.q' and
'vehicle_vision_attitude.q' """
if messages is None:
messages = ['vehicle_attitude',
'vehicle_vision_attitude',
'vehicle_attitude_groundtruth',
'vehicle_attitude_setpoint:_d']
for message in messages:
if message.endswith(':_d'):
suffix = '_d'
message = message[:-3]
else:
suffix = ''
self._add_roll_pitch_yaw_to_message(message, suffix)
def _add_roll_pitch_yaw_to_message(self, message_name, field_name_suffix=''):
message_data_all = [elem for elem in self._ulog.data_list if elem.name == message_name]
for message_data in message_data_all:
q = [message_data.data['q'+field_name_suffix+'['+str(i)+']'] for i in range(4)]
roll = np.arctan2(2.0 * (q[0] * q[1] + q[2] * q[3]),
1.0 - 2.0 * (q[1] * q[1] + q[2] * q[2]))
pitch = np.arcsin(2.0 * (q[0] * q[2] - q[3] * q[1]))
yaw = np.arctan2(2.0 * (q[0] * q[3] + q[1] * q[2]),
1.0 - 2.0 * (q[2] * q[2] + q[3] * q[3]))
message_data.data['roll'+field_name_suffix] = roll
message_data.data['pitch'+field_name_suffix] = pitch
message_data.data['yaw'+field_name_suffix] = yaw
def get_configured_rc_input_names(self, channel):
"""
find all RC mappings to a given channel and return their names
:param channel: input channel (0=first)
:return: list of strings or None
"""
ret_val = []
for key in self._ulog.initial_parameters:
param_val = self._ulog.initial_parameters[key]
if key.startswith('RC_MAP_') and param_val == channel + 1:
ret_val.append(key[7:].capitalize())
if len(ret_val) > 0:
return ret_val
return None
pyulog-1.2.3/pyulog/px4_events.py 0000664 0000000 0000000 00000012152 15214172755 0017036 0 ustar 00root root 0000000 0000000 """ Event parsing """
import json
import lzma
import urllib.request
from typing import Optional, Callable, Any, List, Tuple
from .libevents_parse.parser import Parser
from .core import ULog
class PX4Events:
""" class to extract events from logs and combine them with metadata to get the messages """
DEFAULT_EVENTS_URL = \
'https://px4-travis.s3.amazonaws.com/Firmware/master/_general/all_events.json.xz'
def __init__(self):
self._events_profile = 'dev'
self._default_parser: Optional[Parser] = None
self._get_default_json_def_cb = self._get_default_json_definitions
@staticmethod
def _get_default_json_definitions(already_has_default_parser: bool) -> Optional[Any]:
""" Default implementation for retrieving the default json event definitions """
# If it already exists, return it to avoid re-downloading
if already_has_default_parser:
return None
with urllib.request.urlopen(PX4Events.DEFAULT_EVENTS_URL, timeout=4) as response:
data = response.read()
return json.loads(lzma.decompress(data))
def set_default_json_definitions_cb(self,
default_json_definitions_cb: Callable[[bool], Optional[Any]]):
""" Set the callback to retrieve the default event definitions json
data (can be used for caching) """
self._get_default_json_def_cb = default_json_definitions_cb
def _get_event_parser(self, ulog: ULog) -> Optional[Parser]:
""" get event parser instance or None on error """
if 'metadata_events' in ulog.msg_info_multiple_dict and \
'metadata_events_sha256' in ulog.msg_info_dict:
file_hash = ulog.msg_info_dict['metadata_events_sha256']
if len(file_hash) <= 64 and file_hash.isalnum():
events_metadata = ulog.msg_info_multiple_dict['metadata_events'][0]
event_definitions_json = json.loads(lzma.decompress(b''.join(events_metadata)))
parser = Parser()
parser.load_definitions(event_definitions_json)
parser.set_profile(self._events_profile)
return parser
# No json definitions in the log -> use default definitions
json_definitions = self._get_default_json_def_cb(
self._default_parser is not None)
if json_definitions is not None:
self._default_parser = Parser()
self._default_parser.load_definitions(json_definitions)
self._default_parser.set_profile(self._events_profile)
return self._default_parser
def get_logged_events(self, ulog: ULog) -> List[Tuple[int, str, str]]:
"""
Get the events as list of messages
:return: list of (timestamp, log level str, message) tuples
"""
def event_log_level_str(log_level: int):
return {0: 'EMERGENCY',
1: 'ALERT',
2: 'CRITICAL',
3: 'ERROR',
4: 'WARNING',
5: 'NOTICE',
6: 'INFO',
7: 'DEBUG',
8: 'PROTOCOL',
9: 'DISABLED'}.get(log_level, 'UNKNOWN')
# Parse events
messages = []
try:
events = ulog.get_dataset('event')
all_ids = events.data['id']
if len(all_ids) == 0:
return []
# Get the parser
try:
event_parser = self._get_event_parser(ulog)
except Exception as exception: # pylint: disable=broad-exception-caught
print('Failed to get event parser: {}'.format(exception))
return []
for event_idx, event_id in enumerate(all_ids):
log_level = (events.data['log_levels'][event_idx] >> 4) & 0xf
if log_level >= 8:
continue
args = []
i = 0
while True:
arg_str = 'arguments[{}]'.format(i)
if arg_str not in events.data:
break
arg = events.data[arg_str][event_idx]
args.append(arg)
i += 1
log_level_str = event_log_level_str(log_level)
t = events.data['timestamp'][event_idx]
event = None
if event_parser is not None:
event = event_parser.parse(event_id, bytes(args))
if event is None:
messages.append((t, log_level_str,
'[Unknown event with ID {:}]'.format(event_id)))
else:
# only show default group
if event.group() == "default":
messages.append((t, log_level_str, event.message()))
# we could expand this a bit for events:
# - show the description too
# - handle url's as link (currently it's shown as text, and all tags are escaped)
except (KeyError, IndexError, ValueError):
# no events in log
pass
return messages
pyulog-1.2.3/pyulog/sql/ 0000775 0000000 0000000 00000000000 15214172755 0015163 5 ustar 00root root 0000000 0000000 pyulog-1.2.3/pyulog/sql/pyulog.1.sql 0000664 0000000 0000000 00000005711 15214172755 0017366 0 ustar 00root root 0000000 0000000 BEGIN;
CREATE TABLE IF NOT EXISTS ULog (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
FileVersion INT,
StartTimestamp REAL,
LastTimestamp REAL,
CompatFlags TEXT,
IncompatFlags TEXT,
SyncCount INT,
HasSync BOOLEAN
);
CREATE TABLE IF NOT EXISTS ULogAppendedOffsets (
SeriesIndex INTEGER,
Offset INTEGER,
ULogId INT REFERENCES ULog (Id)
);
CREATE TABLE IF NOT EXISTS ULogDataset (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
DatasetName TEXT,
MultiId INT,
MessageId INT,
TimestampIndex INT,
ULogId INT REFERENCES ULog (Id),
UNIQUE (ULogId, DatasetName, MultiId)
);
CREATE TABLE IF NOT EXISTS ULogField (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
TopicName TEXT,
DataType TEXT,
ValueArray BLOB,
DatasetId INTEGER REFERENCES ULogDataset (Id)
);
CREATE INDEX IF NOT EXISTS btree_ulogfield_datasetid ON ULogField(DatasetId);
CREATE TABLE IF NOT EXISTS ULogMessageDropout (
Timestamp REAL,
Duration FLOAT,
ULogId INT REFERENCES ULog (Id)
);
CREATE TABLE IF NOT EXISTS ULogMessageFormat (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name TEXT,
ULogId INT REFERENCES ULog (Id)
);
CREATE TABLE IF NOT EXISTS ULogMessageFormatField (
FieldType TEXT,
ArraySize INT,
Name TEXT,
MessageId INT REFERENCES ULogMessageFormat (Id)
);
CREATE TABLE IF NOT EXISTS ULogMessageLogging (
LogLevel INT,
Timestamp REAL,
Message TEXT,
ULogId INT REFERENCES ULog (Id)
);
CREATE TABLE IF NOT EXISTS ULogMessageLoggingTagged (
LogLevel INT,
Timestamp REAL,
Tag INT,
Message TEXT,
ULogId INT REFERENCES ULog (Id)
);
CREATE TABLE IF NOT EXISTS ULogMessageInfo (
Key TEXT,
Typename TEXT,
Value BLOB,
ULogId INT REFERENCES ULog (Id)
);
CREATE TABLE IF NOT EXISTS ULogMessageInfoMultiple (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Key TEXT,
Typename TEXT,
ULogId INT REFERENCES ULog (Id)
);
CREATE TABLE IF NOT EXISTS ULogMessageInfoMultipleList (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
SeriesIndex INTEGER,
MessageId TEXT REFERENCES ULogMessageInfoMultiple (Id)
);
CREATE TABLE IF NOT EXISTS ULogMessageInfoMultipleListElement (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
SeriesIndex INTEGER,
Value TEXT,
ListId TEXT REFERENCES ULogMessageInfoMultipleList (Id)
);
CREATE TABLE IF NOT EXISTS ULogInitialParameter (
Key TEXT,
Value BLOB,
ULogId INT REFERENCES ULog (Id)
);
CREATE TABLE IF NOT EXISTS ULogChangedParameter (
Timestamp REAL,
Key TEXT,
Value BLOB,
ULogId INT REFERENCES ULog (Id)
);
CREATE TABLE IF NOT EXISTS ULogDefaultParameter (
DefaultType INT,
Key TEXT,
Value BLOB,
ULogId INT REFERENCES ULog (Id)
);
COMMIT;
pyulog-1.2.3/pyulog/sql/pyulog.2.sql 0000664 0000000 0000000 00000015004 15214172755 0017363 0 ustar 00root root 0000000 0000000 BEGIN;
PRAGMA foreign_keys=off;
CREATE TABLE IF NOT EXISTS ULog_tmp (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
SHA256Sum TEXT UNIQUE,
FileVersion INT,
StartTimestamp REAL,
LastTimestamp REAL,
CompatFlags TEXT,
IncompatFlags TEXT,
SyncCount INT,
HasSync BOOLEAN
);
INSERT OR IGNORE INTO ULog_tmp (Id, FileVersion, StartTimestamp, LastTimestamp, CompatFlags, IncompatFlags, SyncCount, HasSync) SELECT Id, FileVersion, StartTimestamp, LastTimestamp, CompatFlags, IncompatFlags, SyncCount, HasSync FROM ULog;
CREATE TABLE IF NOT EXISTS ULogAppendedOffsets_tmp (
SeriesIndex INTEGER,
Offset INTEGER,
ULogId INT REFERENCES ULog_tmp (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogAppendedOffsets_tmp SELECT * FROM ULogAppendedOffsets;
CREATE TABLE IF NOT EXISTS ULogDataset_tmp (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
DatasetName TEXT,
MultiId INT,
MessageId INT,
TimestampIndex INT,
ULogId INT REFERENCES ULog (Id) ON DELETE CASCADE,
UNIQUE (ULogId, DatasetName, MultiId)
);
INSERT OR IGNORE INTO ULogDataset_tmp SELECT * FROM ULogDataset;
CREATE TABLE IF NOT EXISTS ULogField_tmp (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
TopicName TEXT,
DataType TEXT,
ValueArray BLOB,
DatasetId INTEGER REFERENCES ULogDataset (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogField_tmp SELECT * FROM ULogField;
CREATE INDEX IF NOT EXISTS btree_ulogfield_datasetid ON ULogField_tmp(DatasetId);
CREATE TABLE IF NOT EXISTS ULogMessageDropout_tmp (
Timestamp REAL,
Duration FLOAT,
ULogId INT REFERENCES ULog (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogMessageDropout_tmp SELECT * FROM ULogMessageDropout;
CREATE TABLE IF NOT EXISTS ULogMessageFormat_tmp (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name TEXT,
ULogId INT REFERENCES ULog (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogMessageFormat_tmp SELECT * FROM ULogMessageFormat;
CREATE TABLE IF NOT EXISTS ULogMessageFormatField_tmp (
FieldType TEXT,
ArraySize INT,
Name TEXT,
MessageId INT REFERENCES ULogMessageFormat (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogMessageFormatField_tmp SELECT * FROM ULogMessageFormatField;
CREATE TABLE IF NOT EXISTS ULogMessageLogging_tmp (
LogLevel INT,
Timestamp REAL,
Message TEXT,
ULogId INT REFERENCES ULog (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogMessageLogging_tmp SELECT * FROM ULogMessageLogging;
CREATE TABLE IF NOT EXISTS ULogMessageLoggingTagged_tmp (
LogLevel INT,
Timestamp REAL,
Tag INT,
Message TEXT,
ULogId INT REFERENCES ULog (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogMessageLoggingTagged_tmp SELECT * FROM ULogMessageLoggingTagged;
CREATE TABLE IF NOT EXISTS ULogMessageInfo_tmp (
Key TEXT,
Typename TEXT,
Value BLOB,
ULogId INT REFERENCES ULog (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogMessageInfo_tmp SELECT * FROM ULogMessageInfo;
CREATE TABLE IF NOT EXISTS ULogMessageInfoMultiple_tmp (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Key TEXT,
Typename TEXT,
ULogId INT REFERENCES ULog (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogMessageInfoMultiple_tmp SELECT * FROM ULogMessageInfoMultiple;
CREATE TABLE IF NOT EXISTS ULogMessageInfoMultipleList_tmp (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
SeriesIndex INTEGER,
MessageId TEXT REFERENCES ULogMessageInfoMultiple (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogMessageInfoMultipleList_tmp SELECT * FROM ULogMessageInfoMultipleList;
CREATE TABLE IF NOT EXISTS ULogMessageInfoMultipleListElement_tmp (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
SeriesIndex INTEGER,
Value TEXT,
ListId TEXT REFERENCES ULogMessageInfoMultipleList (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogMessageInfoMultipleListElement_tmp SELECT * FROM ULogMessageInfoMultipleListElement;
CREATE TABLE IF NOT EXISTS ULogInitialParameter_tmp (
Key TEXT,
Value BLOB,
ULogId INT REFERENCES ULog (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogInitialParameter_tmp SELECT * FROM ULogInitialParameter;
CREATE TABLE IF NOT EXISTS ULogChangedParameter_tmp (
Timestamp REAL,
Key TEXT,
Value BLOB,
ULogId INT REFERENCES ULog (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogChangedParameter_tmp SELECT * FROM ULogChangedParameter;
CREATE TABLE IF NOT EXISTS ULogDefaultParameter_tmp (
DefaultType INT,
Key TEXT,
Value BLOB,
ULogId INT REFERENCES ULog (Id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO ULogDefaultParameter_tmp SELECT * FROM ULogDefaultParameter;
DROP TABLE IF EXISTS ULog;
DROP TABLE IF EXISTS ULogAppendedOffsets;
DROP TABLE IF EXISTS ULogDataset;
DROP TABLE IF EXISTS ULogField;
DROP TABLE IF EXISTS ULogMessageDropout;
DROP TABLE IF EXISTS ULogMessageFormat;
DROP TABLE IF EXISTS ULogMessageFormatField;
DROP TABLE IF EXISTS ULogMessageLogging;
DROP TABLE IF EXISTS ULogMessageLoggingTagged;
DROP TABLE IF EXISTS ULogMessageInfo;
DROP TABLE IF EXISTS ULogMessageInfoMultiple;
DROP TABLE IF EXISTS ULogMessageInfoMultipleList;
DROP TABLE IF EXISTS ULogMessageInfoMultipleListElement;
DROP TABLE IF EXISTS ULogInitialParameter;
DROP TABLE IF EXISTS ULogChangedParameter;
DROP TABLE IF EXISTS ULogDefaultParameter;
ALTER TABLE ULog_tmp RENAME TO ULog;
ALTER TABLE ULogAppendedOffsets_tmp RENAME TO ULogAppendedOffsets;
ALTER TABLE ULogDataset_tmp RENAME TO ULogDataset;
ALTER TABLE ULogField_tmp RENAME TO ULogField;
ALTER TABLE ULogMessageDropout_tmp RENAME TO ULogMessageDropout;
ALTER TABLE ULogMessageFormat_tmp RENAME TO ULogMessageFormat;
ALTER TABLE ULogMessageFormatField_tmp RENAME TO ULogMessageFormatField;
ALTER TABLE ULogMessageLogging_tmp RENAME TO ULogMessageLogging;
ALTER TABLE ULogMessageLoggingTagged_tmp RENAME TO ULogMessageLoggingTagged;
ALTER TABLE ULogMessageInfo_tmp RENAME TO ULogMessageInfo;
ALTER TABLE ULogMessageInfoMultiple_tmp RENAME TO ULogMessageInfoMultiple;
ALTER TABLE ULogMessageInfoMultipleList_tmp RENAME TO ULogMessageInfoMultipleList;
ALTER TABLE ULogMessageInfoMultipleListElement_tmp RENAME TO ULogMessageInfoMultipleListElement;
ALTER TABLE ULogInitialParameter_tmp RENAME TO ULogInitialParameter;
ALTER TABLE ULogChangedParameter_tmp RENAME TO ULogChangedParameter;
ALTER TABLE ULogDefaultParameter_tmp RENAME TO ULogDefaultParameter;
PRAGMA foreign_keys=on;
COMMIT;
pyulog-1.2.3/pyulog/sql/pyulog.3.sql 0000664 0000000 0000000 00000000100 15214172755 0017353 0 ustar 00root root 0000000 0000000 BEGIN;
ALTER TABLE ULogField ADD COLUMN ValueJson JSON;
COMMIT;
pyulog-1.2.3/pyulog/sql/pyulog.4.sql 0000664 0000000 0000000 00000003216 15214172755 0017367 0 ustar 00root root 0000000 0000000 BEGIN;
PRAGMA foreign_keys=off;
-- Change REAL timestamps to INT. SQLITE only supports INT64, but ULog -- changed from REAL
-- timestamps are UINT64. We accept losing 1 bit at the top end, since 2^63
-- microseconds = 400,000 years. which should be enough.
ALTER TABLE ULog RENAME COLUMN StartTimestamp TO StartTimestamp_old;
ALTER TABLE ULog ADD COLUMN StartTimestamp INT;
UPDATE ULog SET StartTimestamp = CAST(StartTimestamp_old AS INT);
ALTER TABLE ULog RENAME COLUMN LastTimestamp TO LastTimestamp_old;
ALTER TABLE ULog ADD COLUMN LastTimestamp INT;
UPDATE ULog SET LastTimestamp = CAST(LastTimestamp_old AS INT);
ALTER TABLE ULogMessageDropout RENAME COLUMN Timestamp TO Timestamp_old;
ALTER TABLE ULogMessageDropout ADD COLUMN Timestamp INT;
UPDATE ULogMessageDropout SET Timestamp = CAST(Timestamp_old AS INT);
ALTER TABLE ULogMessageDropout RENAME COLUMN Duration TO Duration_old;
ALTER TABLE ULogMessageDropout ADD COLUMN Duration INT;
UPDATE ULogMessageDropout SET Duration = CAST(Duration_old AS INT);
ALTER TABLE ULogMessageLogging RENAME COLUMN Timestamp TO Timestamp_old;
ALTER TABLE ULogMessageLogging ADD COLUMN Timestamp INT;
UPDATE ULogMessageLogging SET Timestamp = CAST(Timestamp_old AS INT);
ALTER TABLE ULogMessageLoggingTagged RENAME COLUMN Timestamp TO Timestamp_old;
ALTER TABLE ULogMessageLoggingTagged ADD COLUMN Timestamp INT;
UPDATE ULogMessageLoggingTagged SET Timestamp = CAST(Timestamp_old AS INT);
ALTER TABLE ULogChangedParameter RENAME COLUMN Timestamp TO Timestamp_old;
ALTER TABLE ULogChangedParameter ADD COLUMN Timestamp INT;
UPDATE ULogChangedParameter SET Timestamp = CAST(Timestamp_old AS INT);
PRAGMA foreign_keys=on;
COMMIT;
pyulog-1.2.3/pyulog/sql/pyulog.5.sql 0000664 0000000 0000000 00000002635 15214172755 0017374 0 ustar 00root root 0000000 0000000 BEGIN;
CREATE INDEX IF NOT EXISTS btree_ULogAppendedOffsets_ULogId ON ULogAppendedOffsets(ULogId);
CREATE INDEX IF NOT EXISTS btree_ULogDataset_ULogId ON ULogDataset(ULogId);
CREATE INDEX IF NOT EXISTS btree_ULogField_DatasetId ON ULogField(DatasetId);
CREATE INDEX IF NOT EXISTS btree_ULogMessageDropout_ULogId ON ULogMessageDropout(ULogId);
CREATE INDEX IF NOT EXISTS btree_ULogMessageFormat_ULogId ON ULogMessageFormat(ULogId);
CREATE INDEX IF NOT EXISTS btree_ULogMessageFormatField_MessageId ON ULogMessageFormatField(MessageId);
CREATE INDEX IF NOT EXISTS btree_ULogMessageLogging_ULogId ON ULogMessageLogging(ULogId);
CREATE INDEX IF NOT EXISTS btree_ULogMessageLoggingTagged_ULogId ON ULogMessageLoggingTagged(ULogId);
CREATE INDEX IF NOT EXISTS btree_ULogMessageInfo_ULogId ON ULogMessageInfo(ULogId);
CREATE INDEX IF NOT EXISTS btree_ULogMessageInfoMultiple_ULogId ON ULogMessageInfoMultiple(ULogId);
CREATE INDEX IF NOT EXISTS btree_ULogMessageInfoMultipleList_MessageId ON ULogMessageInfoMultipleList(MessageId);
CREATE INDEX IF NOT EXISTS btree_ULogMessageInfoMultipleListElement_ListId ON ULogMessageInfoMultipleListElement(ListId);
CREATE INDEX IF NOT EXISTS btree_ULogInitialParameter_ULogId ON ULogInitialParameter(ULogId);
CREATE INDEX IF NOT EXISTS btree_ULogChangedParameter_ULogId ON ULogChangedParameter(ULogId);
CREATE INDEX IF NOT EXISTS btree_ULogDefaultParameter_ULogId ON ULogDefaultParameter(ULogId);
COMMIT;
pyulog-1.2.3/pyulog/ulog2csv.py 0000664 0000000 0000000 00000013302 15214172755 0016501 0 ustar 00root root 0000000 0000000 #! /usr/bin/env python
"""
Convert a ULog file into CSV file(s)
"""
import argparse
import os
import re
import numpy as np
from .core import ULog
#pylint: disable=too-many-locals, invalid-name, consider-using-enumerate
def main():
"""Command line interface"""
parser = argparse.ArgumentParser(description='Convert ULog to CSV')
parser.add_argument('filename', metavar='file.ulg', help='ULog input file')
parser.add_argument(
'-m', '--messages', dest='messages',
help=("Only consider given messages. Must be a comma-separated list of"
" names, like 'sensor_combined,vehicle_gps_position'"))
parser.add_argument('-d', '--delimiter', dest='delimiter', action='store',
help="Use delimiter in CSV (default is ',')", default=',')
parser.add_argument('-o', '--output', dest='output', action='store',
help='Output directory (default is same as input file)',
metavar='DIR')
parser.add_argument('-i', '--ignore', dest='ignore', action='store_true',
help='Ignore string parsing exceptions', default=False)
parser.add_argument(
'-ts', '--time_s', dest='time_s', type = int,
help="Only convert data after this timestamp (in seconds)")
parser.add_argument(
'-te', '--time_e', dest='time_e', type=int,
help="Only convert data upto this timestamp (in seconds)")
args = parser.parse_args()
if args.output and not os.path.isdir(args.output):
print('Creating output directory {:}'.format(args.output))
os.mkdir(args.output)
convert_ulog2csv(args.filename, args.messages, args.output, args.delimiter,
args.time_s, args.time_e, args.ignore)
def read_string_data(data: ULog.Data, field_name: str, array_size: int, data_index: int) -> str:
""" Parse a data field as string """
s = ''
for index in range(array_size):
character = data.data[f'{field_name}[{index}]'][data_index]
if character == 0:
break
s += chr(character)
return s
def convert_ulog2csv(ulog_file_name, messages, output, delimiter, time_s, time_e,
disable_str_exceptions=False):
"""
Coverts and ULog file to a CSV file.
:param ulog_file_name: The ULog filename to open and read
:param messages: A list of message names
:param output: Output file path
:param delimiter: CSV delimiter
:param time_s: Offset time for conversion in seconds
:param time_e: Limit until time for conversion in seconds
:return: None
"""
msg_filter = messages.split(',') if messages else None
ulog = ULog(ulog_file_name, msg_filter, disable_str_exceptions)
data = ulog.data_list
output_file_prefix = ulog_file_name
# strip '.ulg'
if output_file_prefix.lower().endswith('.ulg'):
output_file_prefix = output_file_prefix[:-4]
# write to different output path?
if output:
base_name = os.path.basename(output_file_prefix)
output_file_prefix = os.path.join(output, base_name)
array_pattern = re.compile(r"(.*)\[(.*?)\]")
def get_fields(data: ULog.Data) -> tuple[list[str], dict[str, int]]:
# use same field order as in the log, except for the timestamp
data_keys = []
string_array_sizes = {}
for f in data.field_data:
if f.field_name.startswith('_padding'):
continue
result = array_pattern.fullmatch(f.field_name)
if result and f.type_str == 'char': # string (array of char's)
field, array_index = result.groups()
array_index = int(array_index)
string_array_sizes[field] = max(array_index + 1, string_array_sizes.get(field, 0))
if array_index == 0:
data_keys.append(field)
else:
data_keys.append(f.field_name)
data_keys.remove('timestamp')
data_keys.insert(0, 'timestamp') # we want timestamp at first position
return data_keys, string_array_sizes
for d in data:
name_without_slash = d.name.replace('/', '_')
output_file_name = f'{output_file_prefix}_{name_without_slash}_{d.multi_id}.csv'
num_data_points = len(d.data['timestamp'])
print(f'Writing {output_file_name} ({num_data_points} data points)')
with open(output_file_name, 'w', encoding='utf-8') as csvfile:
data_keys, string_array_sizes = get_fields(d)
# we don't use np.savetxt, because we have multiple arrays with
# potentially different data types. However the following is quite
# slow...
# write the header
csvfile.write(delimiter.join(data_keys) + '\n')
#get the index for row where timestamp exceeds or equals the required value
time_s_i = np.where(d.data['timestamp'] >= time_s * 1e6)[0][0] \
if time_s else 0
#get the index for row upto the timestamp of the required value
time_e_i = np.where(d.data['timestamp'] >= time_e * 1e6)[0][0] \
if time_e else len(d.data['timestamp'])
# write the data
last_elem = len(data_keys)-1
for i in range(time_s_i, time_e_i):
for k in range(len(data_keys)):
if data_keys[k] in string_array_sizes: # string
s = read_string_data(d, data_keys[k], string_array_sizes[data_keys[k]], i)
csvfile.write(s)
else:
csvfile.write(str(d.data[data_keys[k]][i]))
if k != last_elem:
csvfile.write(delimiter)
csvfile.write('\n')
pyulog-1.2.3/pyulog/ulog2kml.py 0000664 0000000 0000000 00000022024 15214172755 0016472 0 ustar 00root root 0000000 0000000 #! /usr/bin/env python
"""
Convert a ULog file into a KML file (positioning information)
"""
import argparse
import simplekml # pylint: disable=import-error
from .core import ULog
#pylint: disable=too-many-locals, invalid-name, consider-using-enumerate, too-many-arguments
#pylint: disable=unused-variable
def main():
"""Command line interface"""
parser = argparse.ArgumentParser(description='Convert ULog to KML')
parser.add_argument('filename', metavar='file.ulg', help='ULog input file')
parser.add_argument('-o', '--output', dest='output_filename',
help="output filename", default='track.kml')
parser.add_argument('--topic', dest='topic_name',
help="topic name with position data (default=vehicle_gps_position)",
default='vehicle_gps_position')
parser.add_argument('--camera-trigger', dest='camera_trigger',
help="Camera trigger topic name (e.g. camera_capture)",
default=None)
parser.add_argument('-i', '--ignore', dest='ignore', action='store_true',
help='Ignore string parsing exceptions', default=False)
args = parser.parse_args()
convert_ulog2kml(args.filename, args.output_filename,
position_topic_name=args.topic_name,
camera_trigger_topic_name=args.camera_trigger,
disable_str_exceptions=args.ignore)
# alternative example call:
# convert_ulog2kml(args.filename, 'test.kml', ['vehicle_global_position',
# 'vehicle_gps_position'], [_kml_default_colors, lambda x: simplekml.Color.green])
def _kml_default_colors(x):
""" flight mode to color conversion """
x = max([x, 0])
colors_arr = [simplekml.Color.red, simplekml.Color.green, simplekml.Color.blue,
simplekml.Color.violet, simplekml.Color.yellow, simplekml.Color.orange,
simplekml.Color.burlywood, simplekml.Color.azure, simplekml.Color.lightblue,
simplekml.Color.lawngreen, simplekml.Color.indianred, simplekml.Color.hotpink,
simplekml.Color.bisque, simplekml.Color.cyan, simplekml.Color.darksalmon,
simplekml.Color.deepskyblue, simplekml.Color.lime, simplekml.Color.orchid]
return colors_arr[x]
def convert_ulog2kml(ulog_file_name, output_file_name, position_topic_name=
'vehicle_gps_position', colors=_kml_default_colors, altitude_offset=0,
minimum_interval_s=0.1, style=None, camera_trigger_topic_name=None,
disable_str_exceptions=False):
"""
Coverts and ULog file to a CSV file.
:param ulog_file_name: The ULog filename to open and read
:param output_file_name: KML Output file name
:param position_topic_name: either name of a topic (must have 'lon', 'lat' &
'alt' fields), or a list of topic names
:param colors: lambda function with flight mode (int) (or -1) as input and
returns a color (eg 'fffff8f0') (or list of lambda functions if
multiple position_topic_name's)
:param altitude_offset: add this offset to the altitude [m]
:param minimum_interval_s: minimum time difference between two datapoints
(drop if more points)
:param style: dictionary with rendering options:
'extrude': Bool
'line_width': int
:param camera_trigger_topic_name: name of the camera trigger topic (must
have 'lon', 'lat' & 'seq')
:return: None
"""
default_style = {
'extrude': False,
'line_width': 3
}
used_style = default_style
if style is not None:
for key in style:
used_style[key] = style[key]
if not isinstance(position_topic_name, list):
position_topic_name = [position_topic_name]
colors = [colors]
kml = simplekml.Kml()
load_topic_names = position_topic_name + ['vehicle_status']
if camera_trigger_topic_name is not None:
load_topic_names.append(camera_trigger_topic_name)
ulog = ULog(ulog_file_name, load_topic_names, disable_str_exceptions)
# get flight modes
try:
cur_dataset = ulog.get_dataset('vehicle_status')
flight_mode_changes = cur_dataset.list_value_changes('nav_state')
flight_mode_changes.append((ulog.last_timestamp, -1))
except (KeyError, IndexError) as error:
flight_mode_changes = []
# add the graphs
for topic, cur_colors in zip(position_topic_name, colors):
_kml_add_position_data(kml, ulog, topic, cur_colors, used_style,
altitude_offset, minimum_interval_s, flight_mode_changes)
# camera triggers
_kml_add_camera_triggers(kml, ulog, camera_trigger_topic_name, altitude_offset)
kml.save(output_file_name)
def _kml_add_camera_triggers(kml, ulog, camera_trigger_topic_name, altitude_offset):
"""
Add camera trigger points to the map
"""
data = ulog.data_list
topic_instance = 0
cur_dataset = [elem for elem in data
if elem.name == camera_trigger_topic_name and elem.multi_id == topic_instance]
if len(cur_dataset) > 0:
cur_dataset = cur_dataset[0]
# 'longitude_deg' is used in newer PX4 versions
pos_lon = (cur_dataset.data['lon'] if 'lon' in cur_dataset.data else
cur_dataset.data['longitude_deg'])
pos_lat = (cur_dataset.data['lat'] if 'lat' in cur_dataset.data else
cur_dataset.data['latitude_deg'])
pos_alt = (cur_dataset.data['alt'] if 'alt' in cur_dataset.data else
cur_dataset.data['altitude_msl_m'])
sequence = cur_dataset.data['seq']
for i in range(len(pos_lon)):
pnt = kml.newpoint(name='Camera Trigger '+str(sequence[i]))
pnt.coords = [(pos_lon[i], pos_lat[i], pos_alt[i] + altitude_offset)]
# Balloons instead of text does not work
#pnt.style.balloonstyle.text = 'Camera Trigger '+str(sequence[i])
def _kml_add_position_data(kml, ulog, position_topic_name, colors, style,
altitude_offset=0, minimum_interval_s=0.1,
flight_mode_changes=None):
data = ulog.data_list
topic_instance = 0
if flight_mode_changes is None:
flight_mode_changes = []
cur_dataset = [elem for elem in data
if elem.name == position_topic_name and elem.multi_id == topic_instance]
if len(cur_dataset) == 0:
raise KeyError(position_topic_name+' not found in data')
cur_dataset = cur_dataset[0]
# 'longitude_deg' is used in newer PX4 versions
pos_lon = (cur_dataset.data['lon'] if 'lon' in cur_dataset.data else
cur_dataset.data['longitude_deg'])
pos_lat = (cur_dataset.data['lat'] if 'lat' in cur_dataset.data else
cur_dataset.data['latitude_deg'])
pos_alt = (cur_dataset.data['alt'] if 'alt' in cur_dataset.data else
cur_dataset.data['altitude_msl_m'])
pos_t = cur_dataset.data['timestamp']
if 'fix_type' in cur_dataset.data:
indices = cur_dataset.data['fix_type'] > 2 # use only data with a fix
pos_lon = pos_lon[indices]
pos_lat = pos_lat[indices]
pos_alt = pos_alt[indices]
pos_t = pos_t[indices]
# scale if it's an integer type
lon_type = [f.type_str for f in cur_dataset.field_data if f.field_name == 'lon']
if len(lon_type) > 0 and lon_type[0] == 'int32_t':
pos_lon = pos_lon / 1e7 # to degrees
pos_lat = pos_lat / 1e7
pos_alt = pos_alt / 1e3 # to meters
current_flight_mode = 0
current_flight_mode_idx = 0
if len(flight_mode_changes) > 0:
current_flight_mode = flight_mode_changes[0][1]
def create_linestring():
""" create a new kml linestring and set rendering options """
name = position_topic_name + ":" + str(current_flight_mode)
new_linestring = kml.newlinestring(name=name, altitudemode='absolute')
# set rendering options
if style['extrude']:
new_linestring.extrude = 1
new_linestring.style.linestyle.color = colors(current_flight_mode)
new_linestring.style.linestyle.width = style['line_width']
return new_linestring
current_kml_linestring = create_linestring()
last_t = 0
for i in range(len(pos_lon)):
cur_t = pos_t[i]
if (cur_t - last_t)/1e6 > minimum_interval_s: # assume timestamp is in [us]
pos_data = [pos_lon[i], pos_lat[i], pos_alt[i] + altitude_offset]
current_kml_linestring.coords.addcoordinates([pos_data])
last_t = cur_t
# flight mode change?
while current_flight_mode_idx < len(flight_mode_changes)-1 and \
flight_mode_changes[current_flight_mode_idx+1][0] <= cur_t:
current_flight_mode_idx += 1
current_flight_mode = flight_mode_changes[current_flight_mode_idx][1]
current_kml_linestring = create_linestring()
current_kml_linestring.coords.addcoordinates([pos_data])
pyulog-1.2.3/pyulog/ulog2ros2bag.py 0000664 0000000 0000000 00000031571 15214172755 0017255 0 ustar 00root root 0000000 0000000 #! /usr/bin/env python
"""
Convert a ULog file into a ROS2 bag.
"""
from collections import defaultdict
import argparse
import re
from os import environ
from pathlib import Path
from importlib.metadata import version
import numpy as np
from .core import ULog
# pylint: disable=too-many-locals, invalid-name, too-many-branches
# Do not exit on failing to import ROS2 packages so they don't block the help message.
ros2_available = True
try:
from rclpy.serialization import serialize_message # pylint: disable=import-error
import rosbag2_py # pylint: disable=import-error
except ImportError as e:
print(
"Error: Could not import ROS2 packages. Make sure you have sourced"
" your ROS2 installation."
)
print("Actual error:", e)
ros2_available = False
try:
from px4_msgs import msg as px4_msgs # pylint: disable=import-error
except ImportError as e:
print(
"Error: Could not import px4_msgs. Make sure you have built and sourced"
" the correct version of px4_msgs."
)
print("Actual error:", e)
ros2_available = False
def main():
"""Command line interface"""
parser = argparse.ArgumentParser(description="Convert ULog to rosbag")
parser.add_argument("filename", metavar="file.ulg", help="ULog input file")
parser.add_argument(
"-o",
"--output",
dest="bag",
help="rosbag output destination",
default=None,
)
parser.add_argument(
"--mcap",
dest="mcap",
action="store_true",
help="Use mcap storage in rosbag instead of sqlite3",
default=False,
)
parser.add_argument(
"-m",
"--messages",
dest="messages",
help=(
"Only consider given messages. Must be a comma-separated list of"
" names, like 'sensor_combined,vehicle_gps_position'"
),
)
parser.add_argument(
"-i",
"--ignore",
dest="ignore",
action="store_true",
help="Ignore string parsing exceptions",
default=False,
)
parser.add_argument(
"-v",
"--verbose",
dest="verbose",
action="store_true",
help="Print extra debugging information",
default=False,
)
args = parser.parse_args()
if not ros2_available:
return # Error messages printed in 'except ImportError' blocks above
convert_ulog2ros2bag(
args.filename, args.bag, args.mcap, args.messages, args.ignore, args.verbose
)
# https://stackoverflow.com/questions/19053707/converting-snake-case-to-lower-camel-case-lowercamelcase
def to_camel_case(snake_str):
"""Convert snake case string to camel case"""
components = snake_str.split("_")
return "".join(x.title() for x in components)
def convert_ulog2ros2bag(
ulog_file_name: str,
rosbag_name: str | None,
mcap: bool,
messages: str,
disable_str_exceptions=False,
verbose=False,
):
"""
Coverts and ULog file to a CSV file.
:param ulog_file_name: The ULog filename to open and read
:param rosbag_name: The rosbag filename to open and write
:param messages: A list of message names
:return: No
"""
msg_filter = messages.split(",") if messages else None
ulog = ULog(ulog_file_name, msg_filter, disable_str_exceptions)
multiids: dict[str, set[int]] = defaultdict(set)
for topic in ulog.data_list:
multiids[topic.name].add(topic.multi_id)
# ROS2 boilerplate
writer = rosbag2_py.SequentialWriter()
# Default rosbag name from ulg file
rosbag_name = rosbag_name or "rosbag_" + re.sub(
r"\.ulg$", "", Path(ulog_file_name).name
)
storage_options = rosbag2_py.StorageOptions(
uri=rosbag_name, storage_id="mcap" if mcap else "sqlite3"
)
converter_options = rosbag2_py.ConverterOptions(
input_serialization_format="cdr", output_serialization_format="cdr"
)
writer.open(storage_options, converter_options)
# Support rosbag2_py topic sequence number in ROS2 versions greater than Humble
if rosbag_write_uses_seqnum():
def rosbag_write(topic, msg, timestamp):
writer.write(topic, msg, timestamp, 0)
else:
def rosbag_write(topic, msg, timestamp):
writer.write(topic, msg, timestamp)
# Support rosbag2_py TopicMetadata ID in ROS2 versions greater than Humble
if rosbag_topicmetadata_uses_id():
def topic_metadata(name, topic_type):
return rosbag2_py.TopicMetadata(
0,
name=name, # type: ignore
type=topic_type,
serialization_format="cdr",
)
else:
def topic_metadata(name, topic_type):
return rosbag2_py.TopicMetadata(
name=name, # type: ignore
type=topic_type,
serialization_format="cdr",
)
if verbose:
print(f"I: ROS2 Distro: {environ.get('ROS_DISTRO') or 'not detected'}")
px4_msgs_share_dir = None
try:
# fmt: off
from ament_index_python.packages import get_package_share_directory # pylint: disable=import-error, import-outside-toplevel
# fmt: on
px4_msgs_share_dir = get_package_share_directory("px4_msgs")
except: # pylint: disable=bare-except
pass
print(f"I: ROS2 px4_msgs: {px4_msgs_share_dir or 'not found'}")
print(
f"I: PX4 firmware version from ulog: {ulog.get_version_info_str() or 'not found'}"
)
print("")
topic_count, message_count = 0, 0
for ulg_topic in ulog.data_list:
topic_message_count = len(ulg_topic.data["timestamp"])
# Determine ROS2 topic name
if multiids[ulg_topic.name] == {0}:
ros2_topic = f"/px4/{ulg_topic.name}"
else:
ros2_topic = f"/px4/{ulg_topic.name}_{ulg_topic.multi_id}"
if verbose:
ulg_topic_name = ulg_topic.name
if multiids[ulg_topic.name] != {0}:
ulg_topic_name += f" ({ulg_topic.multi_id})"
print(
f"D: Processing ulog topic {ulg_topic_name} with {topic_message_count}"
f" messages and ROS2 topic {ros2_topic}"
)
# Determine ROS2 message type (px4_msgs)
msg_type_name = calc_msgtype(ulg_topic.name)
if msg_type_name is None or not hasattr(px4_msgs, msg_type_name):
print(
f"W: Message type '{msg_type_name or to_camel_case(ulg_topic.name)}'"
f" for {ulg_topic.name} not found in px4_msgs, skipping."
)
continue
MsgType = getattr(px4_msgs, msg_type_name)
if verbose:
print(f"D: Found ROS2 message type px4_msgs/msg/{MsgType.__name__}")
# Check if it is a composed message type
if any(
re.match(r"(.*?)\.(.*?)", field.field_name)
for field in ulg_topic.field_data
):
# TODO: add support for composed message types
print(f"W: Message type for {ulg_topic.name} is composed, skipping.")
continue
# Verify message type
ros2_fields = MsgType.get_fields_and_field_types().keys()
px4_fields = [
re.sub(r"\[\d+\]$", "", data.field_name) for data in ulg_topic.field_data
] # strip array indices
px4_fields = list(set(px4_fields)) # remove duplicates
if not all(px4_field in ros2_fields for px4_field in px4_fields):
print(
f"W: Message type px4_msgs/msg/{MsgType.__name__} does not match topic"
f" '{ulg_topic.name}' in ulog, skipping. Please check your version of px4_msgs."
)
if verbose:
for px4_field in [
px4_field
for px4_field in px4_fields
if px4_field not in ros2_fields
]:
print(
f"D: field {px4_field} of {ulg_topic.name} not found in {MsgType.__name__}"
)
continue
# Register topic in rosbag
writer.create_topic(
topic_metadata(ros2_topic, f"px4_msgs/msg/{MsgType.__name__}")
)
# Write each message
for i in range(len(ulg_topic.data["timestamp"])):
msg = ros2_msg_from_ulg_topic(ulg_topic, i, MsgType)
ts = ulg_topic.data["timestamp"][i] * 1000 # us -> ns
rosbag_write(
ros2_topic,
serialize_message(msg),
int(ts),
)
topic_count += 1
message_count += topic_message_count
writer.close()
print(
f"\nWrote rosbag '{rosbag_name}' with {topic_count} topics and {message_count} messages."
)
def rosbag_topicmetadata_uses_id():
"""
rosbag2_py PR #1538 adds a parameter to TopicMetadata.__init__() for a topic ID,
breaking the interface. This attempts to add compability with both versions.
"""
return version("rosbag2_py") >= "0.25.0"
def rosbag_write_uses_seqnum():
"""
rosbag2_py commit 62e5f77 adds a parameter to SequentialWriter.write() for a
topic sequence number, breaking the interface. This attempts to add compatibility
with both versions.
"""
return version("rosbag2_py") >= "0.32.0"
def calc_msgtype(topic_name: str) -> str | None:
"""Calculate message type from topic name, if possible"""
REGEX_EXCEPTIONS = {
r"^actuator_controls_status_\d$": "ActuatorControlsStatus",
r"^actuator_outputs\w*": "ActuatorOutputs",
r"^config_overrides\w*": "ConfigOverrides",
r"^estimator_aid_src_((gnss_yaw)|(ev_yaw)|(fake_hgt)|(airspeed)|(sideslip)|(\w+_hgt))$": "EstimatorAidSource1d", # pylint: disable=line-too-long
r"^estimator_aid_src_((drag)|(aux_vel)|(optical_flow)|(\w+_pos)|(aux_global_position))$": "EstimatorAidSource2d", # pylint: disable=line-too-long
r"^estimator_aid_src_((ev_vel)|(gnss_vel)|(gravity)|(mag))$": "EstimatorAidSource3d",
r"^estimator_ev_pos_bias": "EstimatorBias3d",
r"^estimator_((baro)|(gnss_hgt))_bias": "EstimatorBias",
r"^estimator_innovation\w*": "EstimatorInnovations",
r"^px4io_status$": "Px4ioStatus",
r"^vehicle_gps_position$": "SensorGps",
r"^sensors_status_\w+": "SensorsStatus",
r"^vehicle_angular_velocity\w*": "VehicleAngularVelocity",
r"^\w+?attitude(_groundtruth)?$": "VehicleAttitude",
r"^\w+?attitude_setpoint$": "VehicleAttitudeSetpoint",
r"^\w+?_command\w*": "VehicleCommand",
r"^\w+?_control_\w+": "VehicleControlMode",
r"^((aux_)|(estimator_)|(vehicle_)|(external_ins_))global_position(_groundtruth)?$": "VehicleGlobalPosition", # pylint: disable=line-too-long
r"^\w+?_local_position(_groundtruth)?$": "VehicleLocalPosition",
r"^\w+?_odometry$": "VehicleOdometry",
r"^((estimator)|(vehicle))_optical_flow_vel$": "VehicleOpticalFlowVel",
r"^vehicle_thrust_setpoint\w*": "VehicleThrustSetpoint",
r"^vehicle_torque_setpoint\w*": "VehicleTorqueSetpoint",
r"^(estimator_)?wind$": "Wind",
}
direct_name = to_camel_case(topic_name)
if hasattr(px4_msgs, direct_name): # type: ignore
return direct_name
result = None
for pattern, type_name in REGEX_EXCEPTIONS.items():
if re.match(pattern, topic_name):
result = type_name
break
return result
def ros2ify_value(field_name: str, value, MsgType: object):
"""Translate a pyulog numpy value to ros2 plain python value"""
value_type: np.signedinteger | np.unsignedinteger | np.floating = value.dtype
# int8 could either be int8_t, bool, or char
# check the destination type
ros2_type: str = MsgType.get_fields_and_field_types()[field_name] # type: ignore
# startswith() accounts for array types
if value_type == np.int8 and ros2_type.startswith("bool"):
return bool(value)
return value.item()
def ros2_msg_from_ulg_topic(ulg_topic: ULog.Data, i: int, MsgType):
"""Create a ROS2 message object from a ulg topic message"""
msg = MsgType()
for field in ulg_topic.field_data:
array_match = re.match(r"(.*?)\[(\d+)\]", field.field_name)
value = ulg_topic.data[field.field_name][i]
if array_match:
field_name, array_index = array_match.groups()
array_index = int(array_index)
value = ros2ify_value(field_name, value, MsgType)
getattr(msg, field_name)[array_index] = value
else:
try:
value = ros2ify_value(field.field_name, value, MsgType)
setattr(msg, field.field_name, value)
except Exception as e:
print(
f"Exception when setting field '{field.field_name}' from"
f" topic '{ulg_topic.name}' with type: {type(value)}"
)
raise e
return msg
if __name__ == "__main__":
main()
pyulog-1.2.3/run_tests.sh 0000775 0000000 0000000 00000000073 15214172755 0015432 0 ustar 00root root 0000000 0000000 #! /bin/bash
pytest test && pylint pyulog/*.py test/*.py
pyulog-1.2.3/setup.py 0000664 0000000 0000000 00000000740 15214172755 0014560 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
This module allows you to parse ULog files, which are used within the PX4
autopilot middleware.
The file format is documented on https://docs.px4.io/main/en/dev_log/ulog_file_format.html
"""
from setuptools import setup
DOCLINES = __doc__.split("\n")
# pylint: disable=invalid-name
setup(
long_description="\n".join(DOCLINES),
long_description_content_type='text/x-rst',
platforms=["Windows", "Linux", "Solaris", "Mac OS-X", "Unix"],
)
pyulog-1.2.3/test/ 0000775 0000000 0000000 00000000000 15214172755 0014024 5 ustar 00root root 0000000 0000000 pyulog-1.2.3/test/__init__.py 0000664 0000000 0000000 00000000000 15214172755 0016123 0 ustar 00root root 0000000 0000000 pyulog-1.2.3/test/sample.ulg 0000664 0000000 0000000 00017354564 15214172755 0016047 0 ustar 00root root 0000000 0000000 ULog5 Ð�´ 8 Ichar[40] ver_swfd483321a5cf50ead91164356d15aa474643aa73 Ichar[8] ver_hwAUAV_X21 Ichar[3] sys_namePX4 Iint32_t time_ref_utc [ Fmc_virtual_rates_setpoint:uint64_t timestamp;float roll;float pitch;float yaw;float thrust;y Fsensor_baro:uint64_t timestamp;uint64_t error_count;float pressure;float altitude;float temperature;uint8_t[4] _padding0;f Fmission:uint64_t timestamp;int32_t dataman_id;uint32_t count;int32_t current_seq;uint8_t[4] _padding0;AFvehicle_status:uint64_t timestamp;uint32_t system_id;uint32_t component_id;uint32_t onboard_control_sensors_present;uint32_t onboard_control_sensors_enabled;uint32_t onboard_control_sensors_health;uint8_t nav_state;uint8_t arming_state;uint8_t hil_state;bool failsafe;uint8_t system_type;bool is_rotary_wing;bool is_vtol;bool vtol_fw_permanent_stab;bool in_transition_mode;bool in_transition_to_fw;bool rc_signal_lost;uint8_t rc_input_mode;bool data_link_lost;uint8_t data_link_lost_counter;bool engine_failure;bool engine_failure_cmd;bool mission_failure;uint8_t[3] _padding0;Ftransponder_report:uint64_t timestamp;double lat;double lon;uint32_t ICAO_address;float altitude;float heading;float hor_velocity;float ver_velocity;uint16_t flags;uint16_t squawk;uint8_t altitude_type;char[9] callsign;uint8_t emitter_type;uint8_t tslc;uint8_t[4] _padding0;zFfw_virtual_attitude_setpoint:uint64_t timestamp;float roll_body;float pitch_body;float yaw_body;float yaw_sp_move_rate;float[9] R_body;float[4] q_d;float[4] q_e;float thrust;bool R_valid;bool q_d_valid;bool q_e_valid;bool roll_reset_integral;bool pitch_reset_integral;bool yaw_reset_integral;bool fw_control_yaw;bool disable_mc_yaw_control;bool apply_flaps;uint8_t[7] _padding0;P Factuator_controls:uint64_t timestamp;uint64_t timestamp_sample;float[8] control;P Ffiltered_bottom_flow:uint64_t timestamp;float sumx;float sumy;float vx;float vy;n Fonboard_mission:uint64_t timestamp;int32_t dataman_id;uint32_t count;int32_t current_seq;uint8_t[4] _padding0;qFcontrol_state:uint64_t timestamp;float x_acc;float y_acc;float z_acc;float x_vel;float y_vel;float z_vel;float x_pos;float y_pos;float z_pos;float airspeed;float[3] vel_variance;float[3] pos_variance;float[4] q;float[4] delta_q_reset;float roll_rate;float pitch_rate;float yaw_rate;float horz_acc_mag;bool airspeed_valid;uint8_t quat_reset_counter;uint8_t[6] _padding0;X Fmount_orientation:uint64_t timestamp;float[3] attitude_euler_angle;uint8_t[4] _padding0;q Fsubsystem_info:uint64_t timestamp;uint64_t subsystem_type;bool present;bool enabled;bool ok;uint8_t[5] _padding0;À Fuavcan_parameter_value:uint64_t timestamp;int64_t int_value;float real_value;int16_t param_index;uint16_t param_count;uint8_t node_id;char[17] param_id;uint8_t param_type;uint8_t[5] _padding0;B Flog_message:uint64_t timestamp;uint8_t severity;uint8_t[127] text;\ Factuator_outputs:uint64_t timestamp;uint32_t noutputs;float[16] output;uint8_t[4] _padding0;[ Factuator_controls_virtual_fw:uint64_t timestamp;uint64_t timestamp_sample;float[8] control;Fesc_report:uint64_t timestamp;uint32_t esc_errorcount;int32_t esc_rpm;float esc_voltage;float esc_current;float esc_temperature;float esc_setpoint;uint16_t esc_setpoint_raw;uint16_t esc_address;uint16_t esc_version;uint16_t esc_state;uint8_t esc_vendor;uint8_t[7] _padding0;@ Ftest_motor:uint64_t timestamp;uint32_t motor_number;float value;ƒ Fesc_status:uint64_t timestamp;uint16_t counter;uint8_t esc_count;uint8_t esc_connectiontype;uint8_t[4] _padding0;esc_report[8] esc;¾ Fdifferential_pressure:uint64_t timestamp;uint64_t error_count;float differential_pressure_raw_pa;float differential_pressure_filtered_pa;float max_differential_pressure_pa;float temperature;W Ffence:uint64_t timestamp;uint32_t count;uint8_t[4] _padding0;fence_vertex[16] vertices;K Fadc_report:uint64_t timestamp;float[8] channel_value;int16_t[8] channel_id;6 Fcpuload:uint64_t timestamp;float load;float ram_usage;Ç Frc_channels:uint64_t timestamp;uint64_t timestamp_last_valid;float[18] channels;uint32_t frame_drop_count;uint8_t channel_count;int8_t[23] function;uint8_t rssi;bool signal_lost;uint8_t[2] _padding0;'Fekf2_replay:uint64_t timestamp;uint64_t time_ref;uint64_t magnetometer_timestamp;uint64_t baro_timestamp;uint64_t rng_timestamp;uint64_t flow_timestamp;uint64_t asp_timestamp;uint64_t ev_timestamp;uint64_t time_usec;uint64_t time_usec_vel;float gyro_integral_dt;float accelerometer_integral_dt;float[3] gyro_rad;float[3] accelerometer_m_s2;float[3] magnetometer_ga;float baro_alt_meter;int32_t lat;int32_t lon;int32_t alt;float eph;float epv;float sacc;float vel_m_s;float vel_n_m_s;float vel_e_m_s;float vel_d_m_s;float range_to_ground;float[2] flow_pixel_integral;float[2] flow_gyro_integral;uint32_t flow_time_integral;float indicated_airspeed_m_s;float true_airspeed_m_s;float[3] pos_ev;float[4] quat_ev;float pos_err;float ang_err;uint8_t fix_type;uint8_t nsats;bool vel_ned_valid;uint8_t flow_quality;i Fdebug_key_value:uint64_t timestamp;uint32_t timestamp_ms;float value;int8_t[10] key;uint8_t[6] _padding0;¸ Fairspeed:uint64_t timestamp;float indicated_airspeed_m_s;float true_airspeed_m_s;float true_airspeed_unfiltered_m_s;float air_temperature_celsius;float confidence;uint8_t[4] _padding0;R Factuator_controls_0:uint64_t timestamp;uint64_t timestamp_sample;float[8] control;R Factuator_controls_1:uint64_t timestamp;uint64_t timestamp_sample;float[8] control;R Factuator_controls_2:uint64_t timestamp;uint64_t timestamp_sample;float[8] control;R Factuator_controls_3:uint64_t timestamp;uint64_t timestamp_sample;float[8] control;\ Fsafety:uint64_t timestamp;bool safety_switch_available;bool safety_off;uint8_t[6] _padding0;D Fcamera_trigger:uint64_t timestamp;uint32_t seq;uint8_t[4] _padding0;à Fvision_position_estimate:uint64_t timestamp;uint64_t timestamp_received;uint32_t id;float x;float y;float z;float vx;float vy;float vz;float[4] q;float pos_err;float ang_err;uint8_t[4] _padding0;QFvehicle_attitude_setpoint:uint64_t timestamp;float roll_body;float pitch_body;float yaw_body;float yaw_sp_move_rate;float[4] q_d;float thrust;float landing_gear;bool q_d_valid;bool roll_reset_integral;bool pitch_reset_integral;bool yaw_reset_integral;bool fw_control_yaw;bool disable_mc_yaw_control;bool apply_flaps;uint8_t[1] _padding0;Ftelemetry_status:uint64_t timestamp;uint64_t heartbeat_time;uint64_t telem_time;uint16_t rxerrors;uint16_t fixed;uint8_t type;uint8_t rssi;uint8_t remote_rssi;uint8_t noise;uint8_t remote_noise;uint8_t txbuf;uint8_t system_id;uint8_t component_id;uint8_t[4] _padding0;… Fmultirotor_motor_limits:uint64_t timestamp;uint8_t lower_limit;uint8_t upper_limit;uint8_t yaw;uint8_t reserved;uint8_t[4] _padding0;o Foffboard_mission:uint64_t timestamp;int32_t dataman_id;uint32_t count;int32_t current_seq;uint8_t[4] _padding0;Fsensor_accel:uint64_t timestamp;uint64_t integral_dt;uint64_t error_count;float x;float y;float z;float x_integral;float y_integral;float z_integral;float temperature;float range_m_s2;float scaling;uint32_t device_id;int16_t x_raw;int16_t y_raw;int16_t z_raw;int16_t temperature_raw;iFvehicle_global_position:uint64_t timestamp;uint64_t time_utc_usec;double lat;double lon;double[2] delta_lat_lon;float alt;float delta_alt;float vel_n;float vel_e;float vel_d;float yaw;float eph;float epv;float terrain_alt;float pressure_alt;uint8_t lat_lon_reset_counter;uint8_t alt_reset_counter;bool terrain_alt_valid;bool dead_reckoning;uint8_t[4] _padding0;A Fservorail_status:uint64_t timestamp;float voltage_v;float rssi_v;¡ Factuator_armed:uint64_t timestamp;bool armed;bool prearmed;bool ready_to_arm;bool lockdown;bool force_failsafe;bool in_esc_calibration_mode;uint8_t[2] _padding0;uFvehicle_global_position_groundtruth:uint64_t timestamp;uint64_t time_utc_usec;double lat;double lon;double[2] delta_lat_lon;float alt;float delta_alt;float vel_n;float vel_e;float vel_d;float yaw;float eph;float epv;float terrain_alt;float pressure_alt;uint8_t lat_lon_reset_counter;uint8_t alt_reset_counter;bool terrain_alt_valid;bool dead_reckoning;uint8_t[4] _padding0; Fposition_setpoint_triplet:uint64_t timestamp;uint8_t nav_state;uint8_t[7] _padding0;position_setpoint previous;position_setpoint current;position_setpoint next;ÿFestimator_status:uint64_t timestamp;float[32] states;float n_states;float[3] vibe;float[28] covariances;float pos_horiz_accuracy;float pos_vert_accuracy;float mag_test_ratio;float vel_test_ratio;float pos_test_ratio;float hgt_test_ratio;float tas_test_ratio;float hagl_test_ratio;uint16_t gps_check_fail_flags;uint16_t control_mode_flags;uint16_t filter_fault_flags;uint16_t innovation_check_flags;uint16_t solution_status_flags;uint8_t nan_flags;uint8_t health_flags;uint8_t timeout_flags;uint8_t[3] _padding0;@Fvehicle_control_mode:uint64_t timestamp;bool flag_armed;bool flag_external_manual_override_ok;bool flag_system_hil_enabled;bool flag_control_manual_enabled;bool flag_control_auto_enabled;bool flag_control_offboard_enabled;bool flag_control_rates_enabled;bool flag_control_attitude_enabled;bool flag_control_rattitude_enabled;bool flag_control_force_enabled;bool flag_control_acceleration_enabled;bool flag_control_velocity_enabled;bool flag_control_position_enabled;bool flag_control_altitude_enabled;bool flag_control_climb_rate_enabled;bool flag_control_termination_enabled;[ Ffw_virtual_rates_setpoint:uint64_t timestamp;float roll;float pitch;float yaw;float thrust;° Fsystem_power:uint64_t timestamp;float voltage5V_v;uint8_t usb_connected;uint8_t brick_valid;uint8_t servo_valid;uint8_t periph_5V_OC;uint8_t hipower_5V_OC;uint8_t[7] _padding0;L Fvehicle_force_setpoint:uint64_t timestamp;float x;float y;float z;float yaw;?Fsensor_combined:uint64_t timestamp;float[3] gyro_rad;float gyro_integral_dt;int32_t accelerometer_timestamp_relative;float[3] accelerometer_m_s2;float accelerometer_integral_dt;int32_t magnetometer_timestamp_relative;float[3] magnetometer_ga;int32_t baro_timestamp_relative;float baro_alt_meter;float baro_temp_celcius;X Fvehicle_rates_setpoint:uint64_t timestamp;float roll;float pitch;float yaw;float thrust;>Fvehicle_local_position_groundtruth:uint64_t timestamp;uint64_t ref_timestamp;double ref_lat;double ref_lon;uint64_t surface_bottom_timestamp;float x;float y;float z;float[2] delta_xy;float delta_z;float vx;float vy;float vz;float[2] delta_vxy;float delta_vz;float yaw;float ref_alt;float dist_bottom;float dist_bottom_rate;float eph;float epv;bool xy_valid;bool z_valid;bool v_xy_valid;bool v_z_valid;uint8_t xy_reset_counter;uint8_t z_reset_counter;uint8_t vxy_reset_counter;uint8_t vz_reset_counter;bool xy_global;bool z_global;bool dist_bottom_valid;uint8_t[5] _padding0;y Fwind_estimate:uint64_t timestamp;float windspeed_north;float windspeed_east;float covariance_north;float covariance_east;¬Ftecs_status:uint64_t timestamp;float altitudeSp;float altitude_filtered;float flightPathAngleSp;float flightPathAngle;float flightPathAngleFiltered;float airspeedSp;float airspeed_filtered;float airspeedDerivativeSp;float airspeedDerivative;float totalEnergyError;float energyDistributionError;float totalEnergyRateError;float energyDistributionRateError;float throttle_integ;float pitch_integ;uint8_t mode;uint8_t[3] _padding0;}Foptical_flow:uint64_t timestamp;float pixel_flow_x_integral;float pixel_flow_y_integral;float gyro_x_rate_integral;float gyro_y_rate_integral;float gyro_z_rate_integral;float ground_distance_m;uint32_t integration_timespan;uint32_t time_since_last_sonar_update;uint16_t frame_count_since_last_readout;int16_t gyro_temperature;uint8_t sensor_id;uint8_t quality;uint8_t[2] _padding0;W Fpwm_input:uint64_t timestamp;uint64_t error_count;uint32_t pulse_width;uint32_t period;Fposition_setpoint:uint64_t timestamp;double lat;double lon;float x;float y;float z;float vx;float vy;float vz;float alt;float yaw;float yawspeed;float loiter_radius;float pitch_min;float a_x;float a_y;float a_z;float acceptance_radius;float cruising_speed;float cruising_throttle;bool valid;uint8_t type;bool position_valid;bool velocity_valid;bool alt_valid;bool yaw_valid;bool disable_mc_yaw_control;bool yawspeed_valid;int8_t loiter_direction;bool acceleration_valid;bool acceleration_is_force;uint8_t[1] _padding0;Í Fcollision_report:uint64_t timestamp;uint32_t id;float time_to_minimum_delta;float altitude_minimum_delta;float horizontal_minimum_delta;uint8_t src;uint8_t action;uint8_t threat_level;uint8_t[5] _padding0;} Fmc_att_ctrl_status:uint64_t timestamp;float roll_rate_integ;float pitch_rate_integ;float yaw_rate_integ;uint8_t[4] _padding0;†Fekf2_innovations:uint64_t timestamp;float[6] vel_pos_innov;float[3] mag_innov;float heading_innov;float airspeed_innov;float beta_innov;float[2] flow_innov;float hagl_innov;float[6] vel_pos_innov_var;float[3] mag_innov_var;float heading_innov_var;float airspeed_innov_var;float beta_innov_var;float[2] flow_innov_var;float hagl_innov_var;float[3] output_tracking_error;uint8_t[4] _padding0; Fuavcan_parameter_request:uint64_t timestamp;int64_t int_value;float real_value;int16_t param_index;uint8_t message_type;uint8_t node_id;char[17] param_id;uint8_t param_type;uint8_t[6] _padding0;¹Fvehicle_gps_position:uint64_t timestamp;uint64_t time_utc_usec;int32_t lat;int32_t lon;int32_t alt;int32_t alt_ellipsoid;float s_variance_m_s;float c_variance_rad;float eph;float epv;float hdop;float vdop;int32_t noise_per_ms;int32_t jamming_indicator;float vel_m_s;float vel_n_m_s;float vel_e_m_s;float vel_d_m_s;float cog_rad;int32_t timestamp_time_relative;uint8_t fix_type;bool vel_ned_valid;uint8_t satellites_used;uint8_t[5] _padding0;µ Frc_parameter_map:uint64_t timestamp;int32_t[3] param_index;float[3] scale;float[3] value0;float[3] value_min;float[3] value_max;bool[3] valid;char[51] param_id;uint8_t[6] _padding0;D Fparameter_update:uint64_t timestamp;bool saved;uint8_t[7] _padding0;g Fgeofence_result:uint64_t timestamp;bool geofence_violated;uint8_t geofence_action;uint8_t[6] _padding0;t Fvehicle_attitude:uint64_t timestamp;float rollspeed;float pitchspeed;float yawspeed;float[4] q;uint8_t[4] _padding0;l Fatt_pos_mocap:uint64_t timestamp;uint64_t timestamp_received;uint32_t id;float[4] q;float x;float y;float z;ì Fbattery_status:uint64_t timestamp;float voltage_v;float voltage_filtered_v;float current_a;float current_filtered_a;float discharged_mah;float remaining;float scale;int32_t cell_count;bool connected;uint8_t warning;uint8_t[6] _padding0;X Fvehicle_land_detected:uint64_t timestamp;bool landed;bool freefall;uint8_t[6] _padding0;2 Ftime_offset:uint64_t timestamp;uint64_t offset_ns;CFmission_result:uint64_t timestamp;uint32_t instance_count;uint32_t seq_reached;uint32_t seq_current;uint32_t seq_total;uint32_t item_changed_index;uint32_t item_do_jump_remaining;bool valid;bool warning;bool reached;bool finished;bool stay_in_failsafe;bool flight_termination;bool item_do_jump_changed;bool mission_failure;¡ Fsatellite_info:uint64_t timestamp;uint8_t count;uint8_t[20] svid;uint8_t[20] used;uint8_t[20] elevation;uint8_t[20] azimuth;uint8_t[20] snr;uint8_t[3] _padding0;Ê Fvtol_vehicle_status:uint64_t timestamp;float airspeed_tot;bool vtol_in_rw_mode;bool vtol_in_trans_mode;bool in_transition_to_fw;bool vtol_transition_failsafe;bool fw_permanent_stab;uint8_t[7] _padding0;B Fqshell_req:uint64_t timestamp;uint64_t strlen;int32_t[100] string;2Fvehicle_local_position:uint64_t timestamp;uint64_t ref_timestamp;double ref_lat;double ref_lon;uint64_t surface_bottom_timestamp;float x;float y;float z;float[2] delta_xy;float delta_z;float vx;float vy;float vz;float[2] delta_vxy;float delta_vz;float yaw;float ref_alt;float dist_bottom;float dist_bottom_rate;float eph;float epv;bool xy_valid;bool z_valid;bool v_xy_valid;bool v_z_valid;uint8_t xy_reset_counter;uint8_t z_reset_counter;uint8_t vxy_reset_counter;uint8_t vz_reset_counter;bool xy_global;bool z_global;bool dist_bottom_valid;uint8_t[5] _padding0;‘Fmanual_control_setpoint:uint64_t timestamp;float x;float y;float z;float r;float flaps;float aux1;float aux2;float aux3;float aux4;float aux5;uint8_t mode_switch;uint8_t return_switch;uint8_t rattitude_switch;uint8_t posctl_switch;uint8_t loiter_switch;uint8_t acro_switch;uint8_t offboard_switch;uint8_t kill_switch;uint8_t transition_switch;uint8_t gear_switch;int8_t mode_slot;uint8_t[5] _padding0;AFhil_sensor:uint64_t timestamp;uint64_t accelerometer_timestamp;uint64_t magnetometer_timestamp;uint64_t gyro1_timestamp;uint64_t accelerometer1_timestamp;uint64_t magnetometer1_timestamp;uint64_t gyro2_timestamp;uint64_t accelerometer2_timestamp;uint64_t magnetometer2_timestamp;uint64_t baro_timestamp;uint64_t baro1_timestamp;uint64_t differential_pressure_timestamp;uint64_t differential_pressure1_timestamp;float[3] gyro_rad_s;uint32_t gyro_errcount;float gyro_temp;float[3] accelerometer_m_s2;float accelerometer_range_m_s2;uint32_t accelerometer_errcount;float accelerometer_temp;float[3] magnetometer_ga;float magnetometer_range_ga;float magnetometer_cuttoff_freq_hz;uint32_t magnetometer_errcount;float magnetometer_temp;float[3] gyro1_rad_s;uint32_t gyro1_errcount;float gyro1_temp;float[3] accelerometer1_m_s2;uint32_t accelerometer1_errcount;float accelerometer1_temp;float[3] magnetometer1_ga;uint32_t magnetometer1_errcount;float magnetometer1_temp;float[3] gyro2_rad_s;uint32_t gyro2_errcount;float gyro2_temp;float[3] accelerometer2_m_s2;uint32_t accelerometer2_errcount;float accelerometer2_temp;float[3] magnetometer2_ga;uint32_t magnetometer2_errcount;float magnetometer2_temp;float baro_pres_mbar;float baro_alt_meter;float baro_temp_celcius;float baro1_pres_mbar;float baro1_alt_meter;float baro1_temp_celcius;float[10] adc_voltage_v;float mcu_temp_celcius;float differential_pressure_pa;float differential_pressure_filtered_pa;float differential_pressure1_pa;float differential_pressure1_filtered_pa;int16_t[3] gyro_raw;int16_t[3] accelerometer_raw;int16_t accelerometer_mode;int16_t[3] magnetometer_raw;int16_t magnetometer_mode;int16_t[3] gyro1_raw;int16_t[3] accelerometer1_raw;int16_t[3] magnetometer1_raw;int16_t[3] gyro2_raw;int16_t[3] accelerometer2_raw;int16_t[3] magnetometer2_raw;uint16_t[10] adc_mapping;uint8_t[6] _padding0;” Fvehicle_local_position_setpoint:uint64_t timestamp;float x;float y;float z;float yaw;float vx;float vy;float vz;float acc_x;float acc_y;float acc_z;d Fvehicle_global_velocity_setpoint:uint64_t timestamp;float vx;float vy;float vz;uint8_t[4] _padding0;™ Fhome_position:uint64_t timestamp;double lat;double lon;float alt;float x;float y;float z;float yaw;float direction_x;float direction_y;float direction_z;.Ffw_pos_ctrl_status:uint64_t timestamp;float nav_roll;float nav_pitch;float nav_bearing;float target_bearing;float wp_dist;float xtrack_error;float turn_distance;float landing_horizontal_slope_displacement;float landing_slope_angle_rad;float landing_flare_length;bool abort_landing;uint8_t[7] _padding0;c Fsensor_preflight:uint64_t timestamp;float accel_inconsistency_m_s_s;float gyro_inconsistency_rad_s;O Fgps_inject_data:uint64_t timestamp;uint8_t len;uint8_t flags;uint8_t[182] data;� Ffollow_target:uint64_t timestamp;double lat;double lon;float alt;float vy;float vx;float vz;uint8_t est_cap;uint8_t[7] _padding0;J Fulog_stream_ack:uint64_t timestamp;uint16_t sequence;uint8_t[6] _padding0;Fsensor_gyro:uint64_t timestamp;uint64_t integral_dt;uint64_t error_count;float x;float y;float z;float x_integral;float y_integral;float z_integral;float temperature;float range_rad_s;float scaling;uint32_t device_id;int16_t x_raw;int16_t y_raw;int16_t z_raw;int16_t temperature_raw;K Fcommander_state:uint64_t timestamp;uint8_t main_state;uint8_t[7] _padding0;’ Fulog_stream:uint64_t timestamp;uint16_t sequence;uint8_t length;uint8_t first_message_offset;uint8_t flags;uint8_t[249] data;uint8_t[2] _padding0;4 Ffence_vertex:uint64_t timestamp;float lat;float lon;Fvehicle_command:uint64_t timestamp;double param5;double param6;float param1;float param2;float param3;float param4;float param7;uint32_t command;uint32_t target_system;uint32_t target_component;uint32_t source_system;uint32_t source_component;uint8_t confirmation;uint8_t[7] _padding0;[ Factuator_controls_virtual_mc:uint64_t timestamp;uint64_t timestamp_sample;float[8] control;^ Foutput_pwm:uint64_t timestamp;uint32_t channel_count;uint16_t[16] values;uint8_t[4] _padding0;\ Fvehicle_command_ack:uint64_t timestamp;uint16_t command;uint8_t result;uint8_t[5] _padding0;² Fdistance_sensor:uint64_t timestamp;float min_distance;float max_distance;float current_distance;float covariance;uint8_t type;uint8_t id;uint8_t orientation;uint8_t[5] _padding0;Ù Foffboard_control_mode:uint64_t timestamp;bool ignore_thrust;bool ignore_attitude;bool ignore_bodyrate;bool ignore_position;bool ignore_velocity;bool ignore_acceleration_force;bool ignore_alt_hold;uint8_t[1] _padding0;9 Fgps_dump:uint64_t timestamp;uint8_t len;uint8_t[79] data;V Fmavlink_log:uint64_t timestamp;uint8_t[50] text;uint8_t severity;uint8_t[5] _padding0;Z Factuator_direct:uint64_t timestamp;uint32_t nvalues;float[16] values;uint8_t[4] _padding0;Finput_rc:uint64_t timestamp;uint64_t timestamp_last_signal;uint32_t channel_count;int32_t rssi;uint16_t rc_lost_frame_count;uint16_t rc_total_frame_count;uint16_t rc_ppm_frame_length;uint16_t[18] values;bool rc_failsafe;bool rc_lost;uint8_t input_source;uint8_t[3] _padding0;Š Fvehicle_roi:uint64_t timestamp;double lat;double lon;uint32_t mission_seq;uint32_t target_seq;float alt;uint8_t mode;uint8_t[3] _padding0;€ Fvehicle_attitude_groundtruth:uint64_t timestamp;float rollspeed;float pitchspeed;float yawspeed;float[4] q;uint8_t[4] _padding0;Ì Fsensor_mag:uint64_t timestamp;uint64_t error_count;float x;float y;float z;float range_ga;float scaling;float temperature;uint32_t device_id;int16_t x_raw;int16_t y_raw;int16_t z_raw;uint8_t[6] _padding0;zFmc_virtual_attitude_setpoint:uint64_t timestamp;float roll_body;float pitch_body;float yaw_body;float yaw_sp_move_rate;float[9] R_body;float[4] q_d;float[4] q_e;float thrust;bool R_valid;bool q_d_valid;bool q_e_valid;bool roll_reset_integral;bool pitch_reset_integral;bool yaw_reset_integral;bool fw_control_yaw;bool disable_mc_yaw_control;bool apply_flaps;uint8_t[7] _padding0; Pfloat ATT_W_ACCÍÌL> Pfloat ATT_W_MAGÍÌÌ= Pfloat ATT_W_EXT_HDGÍÌÌ= Pfloat ATT_W_GYRO_BIASÍÌÌ= Pfloat ATT_MAG_DECL Pint32_t ATT_MAG_DECL_A Pint32_t ATT_EXT_HDG_M Pint32_t ATT_ACC_COMP Pfloat ATT_BIAS_MAXÍÌL= Pfloat ATT_VIBE_THRESHÍÌL> Pfloat BAT_V_EMPTYš™Y@ Pfloat BAT_V_CHARGEDš™�@ Pfloat BAT_LOW_THRš™> Pfloat BAT_CRIT_THR)\�= Pfloat BAT_V_LOAD_DROPš™™> Pfloat BAT_R_INTERNAL €¿ Pint32_t BAT_N_CELLS Pfloat BAT_CAPACITY €¿ Pint32_t BAT_V_SCALE_IO' Pfloat BAT_CNT_V_VOLT33S: Pfloat BAT_CNT_V_CURR33S: Pfloat BAT_V_OFFS_CURR Pfloat BAT_V_DIVר"A Pfloat BAT_A_PER_V©AvA Pint32_t BAT_SOURCE Pint32_t TRIG_MODE Pint32_t CBRK_SUPPLY_CHK Pint32_t CBRK_RATE_CTRL Pint32_t CBRK_IO_SAFETY Pint32_t CBRK_AIRSPD_CHK Pint32_t CBRK_FLIGHTTERM|Ù Pint32_t CBRK_ENGINEFAILY Pint32_t CBRK_GPSFAIL Pint32_t CBRK_BUZZER Pint32_t CBRK_USB_CHK Pint32_t COM_DL_LOSS_T
Pint32_t COM_DL_REG_T Pfloat COM_EF_THROT ? Pfloat COM_EF_C2T @ Pfloat COM_EF_TIME A Pfloat COM_RC_LOSS_T ? Pfloat COM_HOME_H_T @ Pfloat COM_HOME_V_T A Pint32_t COM_AUTOS_PAR Pint32_t COM_RC_IN_MODE Pint32_t COM_RC_ARM_HYSTè Pint32_t COM_DISARM_LAND Pint32_t COM_ARM_WO_GPS Pint32_t COM_LOW_BAT_ACT Pfloat COM_OF_LOSS_T Pfloat COM_ARM_EKF_POS ? Pfloat COM_ARM_EKF_VEL ? Pfloat COM_ARM_EKF_HGT €? Pfloat COM_ARM_EKF_YAW ? Pfloat COM_ARM_EKF_ABo; Pfloat COM_ARM_EKF_GB4€·9 Pfloat COM_ARM_IMU_ACC333? Pfloat COM_ARM_IMU_GYRìQ¸= Pint32_t NAV_AH_LATþ{'ð Pint32_t NAV_AH_LONÒL�Z Pfloat NAV_AH_ALT D Pfloat NAV_DLL_CH_T ðB Pint32_t NAV_DLL_CH_LATÈ$ð Pint32_t NAV_DLL_CH_LON‚Ä�Z Pfloat NAV_DLL_CH_ALT D Pfloat NAV_DLL_AH_T ðB Pint32_t NAV_DLL_N Pint32_t NAV_DLL_CHSK Pint32_t EKF2_REC_RPL Pint32_t FW_ARSP_MODE Pfloat FW_MAN_R_SC €? Pfloat FW_MAN_P_SC €? Pfloat FW_MAN_Y_SC €? Pfloat FW_THR_CRUISEš™? Pfloat FW_CLMBOUT_DIFF A Pfloat FW_AIRSPD_TRIM pA Pfloat NAV_MIN_FT_HT A Pfloat NAV_FT_DST A Pint32_t NAV_FT_FS Pfloat NAV_FT_RS ? Pint32_t GPS_DUMP_COMM Pfloat NAV_GPSF_LT ðA Pfloat NAV_GPSF_R pA Pfloat NAV_GPSF_P Pfloat NAV_GPSF_TR333? Pint32_t GF_ACTION Pint32_t GF_ALTMODE Pint32_t GF_SOURCE Pint32_t GF_COUNTÿÿÿÿ Pfloat GF_MAX_HOR_DIST Pfloat GF_MAX_VER_DIST Pfloat LNDMC_Z_VEL_MAX333? Pfloat LNDMC_XY_VEL_MAX À? Pfloat LNDMC_ROT_MAX A Pfloat LNDMC_FFALL_THR @ Pfloat LNDMC_FFALL_TTRIš™™> Pfloat LPE_PUB_AGL_Z Pfloat LPE_FLW_OFF_Z Pfloat LPE_FLW_SCALEff¦? Pint32_t LPE_FLW_GYRO_CMP Pint32_t LPE_FLW_QMIN– Pfloat LPE_SNR_ZÍÌL= Pfloat LPE_SNR_OFF_Z Pfloat LPE_LDR_Z�Âõ< Pfloat LPE_LDR_OFF_Z Pfloat LPE_ACC_XY¦›D< Pfloat LPE_ACC_Z
×£< Pfloat LPE_BAR_Z @@ Pint32_t LPE_GPS_ON Pfloat LPE_GPS_DELAYáz”> Pfloat LPE_GPS_XY €? Pfloat LPE_GPS_Z @@ Pfloat LPE_GPS_VXY €> Pfloat LPE_GPS_VZ €> Pfloat LPE_EPH_MAX @@ Pfloat LPE_EPV_MAX @ Pfloat LPE_VIS_DELAYÍÌÌ= Pfloat LPE_VIS_XYÍÌÌ= Pfloat LPE_VIS_Z ? Pint32_t LPE_VIS_ON Pfloat LPE_VIC_Poƒ: Pfloat LPE_PN_PÍÌÌ= Pfloat LPE_PN_VÍÌÌ= Pfloat LPE_PN_Boƒ: Pfloat LPE_PN_Toƒ: Pfloat LPE_T_MAX_GRADE €? Pfloat LPE_FGYRO_HPoƒ: P
float LPE_LATR¸!B P
float LPE_LON¦Û Pfloat LPE_X_LP @ Pfloat LPE_VXY_PUBš™™> Pfloat LPE_Z_PUB €? Pfloat LPE_LAND_Z�Âõ< Pint32_t MAV_SYS_ID Pint32_t MAV_COMP_ID Pint32_t MAV_PROTO_VER Pint32_t MAV_RADIO_ID Pint32_t MAV_TYPE Pint32_t MAV_USEHILGPS Pint32_t MAV_FWDEXTSP Pint32_t MAV_BROADCAST Pint32_t MAV_TEST_PAR Pint32_t COM_OBL_ACT Pint32_t COM_OBL_RC_ACT Pfloat NAV_LOITER_RAD HB Pfloat NAV_ACC_RAD @ Pfloat NAV_FW_ALT_RAD A Pfloat NAV_MC_ALT_RAD @@ Pint32_t NAV_DLL_ACT Pint32_t NAV_RCL_ACT Pfloat MIS_TAKEOFF_ALT @ Pfloat MIS_LTRMIN_ALTš™™? Pint32_t MIS_ONBOARD_EN Pfloat MIS_DIST_1WP aD Pint32_t MIS_ALTMODE Pint32_t MIS_YAWMODE Pfloat MIS_YAW_TMT €¿ Pfloat MIS_YAW_ERR @A Pint32_t VT_WV_LND_EN Pint32_t VT_WV_LTR_EN Pfloat MC_ROLL_TCÍÌL> Pfloat MC_PITCH_TCÍÌL> Pfloat MC_ROLL_P Ð@ Pfloat MC_ROLLRATE_P)\> Pfloat MC_ROLLRATE_IÍÌÌ= Pfloat MC_ROLLRATE_Doƒ; Pfloat MC_ROLLRATE_FF Pfloat MC_PITCH_P À@ Pfloat MC_PITCHRATE_P)\> Pfloat MC_PITCHRATE_IìQ¸= Pfloat MC_PITCHRATE_Doƒ; Pfloat MC_PITCHRATE_FF Pfloat MC_YAW_P €@ Pfloat MC_YAWRATE_PÍÌL> Pfloat MC_YAWRATE_IÍÌÌ= Pfloat MC_YAWRATE_D Pfloat MC_YAWRATE_FF Pfloat MC_YAW_FF ? Pfloat MC_ROLLRATE_MAX \C Pfloat MC_PITCHRATE_MAX \C Pfloat MC_YAWRATE_MAX HC Pfloat MC_YAWRAUTO_MAX 4B Pfloat MC_ACRO_R_MAX ´C Pfloat MC_ACRO_P_MAX ´C Pfloat MC_ACRO_Y_MAX ´C Pfloat MC_RATT_TH €? Pfloat MC_TPA_BREAK €? Pfloat MC_TPA_SLOPE €? Pint32_t MC_BAT_SCALE_EN Pfloat MPC_THR_MIN�Âõ= Pfloat MPC_THR_HOVER ? Pfloat MPC_ALTCTL_DZÍÌÌ= Pfloat MPC_ALTCTL_DY Pfloat MPC_THR_MAXfff? Pfloat MPC_MANTHR_MIN
×£= Pfloat MPC_MANTHR_MAXfff? P
float MPC_Z_P €? Pfloat MPC_Z_VEL_PÍÌL> Pfloat MPC_Z_VEL_I
×£< Pfloat MPC_Z_VEL_D Pfloat MPC_Z_VEL_MAX_UP @@ Pfloat MPC_Z_VEL_MAX €? Pfloat MPC_Z_VEL_MAX_DN €? Pfloat MPC_Z_FF ? Pfloat MPC_XY_P33s? Pfloat MPC_XY_VEL_PìQ¸= Pfloat MPC_XY_VEL_I
×£< Pfloat MPC_XY_VEL_D
×#< Pfloat MPC_XY_CRUISE @ Pfloat MPC_XY_VEL_MAX A Pfloat MPC_XY_FF ? Pfloat MPC_TILTMAX_AIR 4B Pfloat MPC_TILTMAX_LND @A Pfloat MPC_LAND_SPEED ? Pfloat MPC_TKO_SPEED À? Pfloat MPC_MAN_R_MAX B Pfloat MPC_MAN_P_MAX B Pfloat MPC_MAN_Y_MAX HC Pfloat MPC_HOLD_XY_DZÍÌÌ= Pfloat MPC_HOLD_MAX_XYÍÌL? Pfloat MPC_HOLD_MAX_Zš™? Pfloat MPC_VELD_LP @ Pfloat MPC_ACC_HOR_MAX @ Pint32_t MPC_ALT_MODE Pint32_t PWM_RATE� Pint32_t PWM_MIN3 Pint32_t PWM_MAXž Pint32_t PWM_DISARMED„ Pint32_t PWM_AUX_MINè Pint32_t PWM_AUX_MAXÐ Pint32_t PWM_AUX_DISARMEDè Pfloat MOT_SLEW_MAX Pint32_t PWM_AUX_REV1 Pint32_t PWM_AUX_REV2 Pint32_t PWM_AUX_REV3 Pint32_t PWM_AUX_REV4 Pint32_t PWM_AUX_REV5 Pint32_t PWM_AUX_REV6 Pint32_t PWM_MAIN_REV1 Pint32_t PWM_MAIN_REV2 Pint32_t PWM_MAIN_REV3 Pint32_t PWM_MAIN_REV4 Pint32_t PWM_MAIN_REV5 Pint32_t PWM_MAIN_REV6 Pint32_t PWM_MAIN_REV7 Pint32_t PWM_MAIN_REV8 Pint32_t PWM_SBUS_MODE Pfloat TRIM_ROLL Pfloat TRIM_PITCH Pfloat TRIM_YAW P
float RC1_MIN zD Pfloat RC1_TRIM €»D P
float RC1_MAX úD P
float RC1_REV €? Pfloat RC1_DZ A P
float RC2_MIN zD Pfloat RC2_TRIM €»D P
float RC2_MAX úD P
float RC2_REV €? Pfloat RC2_DZ A P
float RC3_MIN zD Pfloat RC3_TRIM €»D P
float RC3_MAX úD P
float RC3_REV €? Pfloat RC3_DZ A P
float RC4_MIN zD Pfloat RC4_TRIM €»D P
float RC4_MAX úD P
float RC4_REV €? Pfloat RC4_DZ A P
float RC5_MIN zD Pfloat RC5_TRIM €»D P
float RC5_MAX úD P
float RC5_REV €? Pfloat RC5_DZ A P
float RC6_MIN zD Pfloat RC6_TRIM €»D P
float RC6_MAX úD P
float RC6_REV €? Pfloat RC6_DZ A P
float RC7_MIN zD Pfloat RC7_TRIM €»D P
float RC7_MAX úD P
float RC7_REV €? Pfloat RC7_DZ A P
float RC8_MIN zD Pfloat RC8_TRIM €»D P
float RC8_MAX úD P
float RC8_REV €? Pfloat RC8_DZ A P
float RC9_MIN zD Pfloat RC9_TRIM €»D P
float RC9_MAX úD P
float RC9_REV €? Pfloat RC9_DZ Pfloat RC10_MIN zD Pfloat RC10_TRIM €»D Pfloat RC10_MAX úD Pfloat RC10_REV €? P
float RC10_DZ Pfloat RC11_MIN zD Pfloat RC11_TRIM €»D Pfloat RC11_MAX úD Pfloat RC11_REV €? P
float RC11_DZ Pfloat RC12_MIN zD Pfloat RC12_TRIM €»D Pfloat RC12_MAX úD Pfloat RC12_REV €? P
float RC12_DZ Pfloat RC13_MIN zD Pfloat RC13_TRIM €»D Pfloat RC13_MAX úD Pfloat RC13_REV €? P
float RC13_DZ Pfloat RC14_MIN zD Pfloat RC14_TRIM €»D Pfloat RC14_MAX úD Pfloat RC14_REV €? P
float RC14_DZ Pfloat RC15_MIN zD Pfloat RC15_TRIM €»D Pfloat RC15_MAX úD Pfloat RC15_REV €? P
float RC15_DZ Pfloat RC16_MIN zD Pfloat RC16_TRIM €»D Pfloat RC16_MAX úD Pfloat RC16_REV €? P
float RC16_DZ Pfloat RC17_MIN zD Pfloat RC17_TRIM €»D Pfloat RC17_MAX úD Pfloat RC17_REV €? P
float RC17_DZ Pfloat RC18_MIN zD Pfloat RC18_TRIM €»D Pfloat RC18_MAX úD Pfloat RC18_REV €? P
float RC18_DZ Pint32_t RC_DSM_BINDÿÿÿÿ Pint32_t RC_CHAN_CNT Pint32_t RC_TH_USER Pint32_t RC_MAP_ROLL Pint32_t RC_MAP_PITCH Pint32_t RC_MAP_THROTTLE Pint32_t RC_MAP_YAW Pint32_t RC_MAP_AUX1 Pint32_t RC_MAP_AUX2 Pint32_t RC_MAP_AUX3 Pint32_t RC_MAP_AUX4 Pint32_t RC_MAP_AUX5 Pint32_t RC_MAP_PARAM1 Pint32_t RC_MAP_PARAM2 Pint32_t RC_MAP_PARAM3 Pint32_t RC_FAILS_THR Pint32_t RC_RSSI_PWM_CHAN Pint32_t RC_RSSI_PWM_MAXè Pint32_t RC_RSSI_PWM_MINÐ Pfloat NAV_RCL_LT ðB Pint32_t RC_MAP_FLTMODE Pint32_t RC_MAP_MODE_SW Pint32_t RC_MAP_RETURN_SW Pint32_t RC_MAP_RATT_SW Pint32_t RC_MAP_POSCTL_SW Pint32_t RC_MAP_LOITER_SW Pint32_t RC_MAP_ACRO_SW Pint32_t RC_MAP_OFFB_SW Pint32_t RC_MAP_KILL_SW Pint32_t RC_MAP_FLAPS Pint32_t RC_MAP_TRANS_SW Pint32_t RC_MAP_GEAR_SW Pfloat RC_ASSIST_TH €> Pfloat RC_AUTO_TH @? Pfloat RC_RATT_TH ? Pfloat RC_POSCTL_TH ? Pfloat RC_RETURN_TH ? Pfloat RC_LOITER_TH ? Pfloat RC_ACRO_TH ? Pfloat RC_OFFB_TH ? Pfloat RC_KILLSWITCH_TH €> Pfloat RC_TRANS_TH €> Pfloat RC_GEAR_TH €> Pfloat RTL_RETURN_ALT ðA Pfloat RTL_DESCEND_ALT A Pfloat RTL_LAND_DELAY Pfloat RTL_MIN_DIST @ Pint32_t SDLOG_RATEÿÿÿÿ Pint32_t SDLOG_EXTÿÿÿÿ Pint32_t SDLOG_GPSTIME Pint32_t SDLOG_PRIO_BOOST Pint32_t SDLOG_UTC_OFFSET Pint32_t CAL_GYRO0_ID ! Pfloat CAL_GYRO0_XOFFjz’: Pfloat CAL_GYRO0_YOFF}š¼ Pfloat CAL_GYRO0_ZOFFôù; Pfloat CAL_GYRO0_XSCALE €? Pfloat CAL_GYRO0_YSCALE €? Pfloat CAL_GYRO0_ZSCALE €? Pint32_t CAL_MAG0_ID Pint32_t CAL_MAG0_ROTÿÿÿÿ Pfloat CAL_MAG0_XOFFÚfç< Pfloat CAL_MAG0_YOFFlM¾ Pfloat CAL_MAG0_ZOFFÉ0¾ Pfloat CAL_MAG0_XSCALE €? Pfloat CAL_MAG0_YSCALE €? Pfloat CAL_MAG0_ZSCALE €? Pint32_t CAL_ACC0_ID
Pfloat CAL_ACC0_XOFFàXO¾ Pfloat CAL_ACC0_YOFF0ù²> Pfloat CAL_ACC0_ZOFF ¿ Pfloat CAL_ACC0_XSCALEŽ€? Pfloat CAL_ACC0_YSCALED �? Pfloat CAL_ACC0_ZSCALEBã|? Pint32_t CAL_GYRO1_ID $ Pfloat CAL_GYRO1_XOFF”桼 Pfloat CAL_GYRO1_YOFF÷‚rº Pfloat CAL_GYRO1_ZOFFŽ< Pfloat CAL_GYRO1_XSCALE €? Pfloat CAL_GYRO1_YSCALE €? Pfloat CAL_GYRO1_ZSCALE €? Pint32_t CAL_MAG1_ID Pint32_t CAL_MAG1_ROTÿÿÿÿ Pfloat CAL_MAG1_XOFF Pfloat CAL_MAG1_YOFF Pfloat CAL_MAG1_ZOFF Pfloat CAL_MAG1_XSCALE €? Pfloat CAL_MAG1_YSCALE €? Pfloat CAL_MAG1_ZSCALE €? Pint32_t CAL_ACC1_ID Pfloat CAL_ACC1_XOFF€hB½ Pfloat CAL_ACC1_YOFFÀ›> Pfloat CAL_ACC1_ZOFF k9> Pfloat CAL_ACC1_XSCALEg€? Pfloat CAL_ACC1_YSCALEŒ? Pfloat CAL_ACC1_ZSCALE¬Ê{? Pfloat CAL_GYRO2_XOFF Pfloat CAL_GYRO2_YOFF Pfloat CAL_GYRO2_ZOFF Pfloat CAL_GYRO2_XSCALE €? Pfloat CAL_GYRO2_YSCALE €? Pfloat CAL_GYRO2_ZSCALE €? Pint32_t CAL_MAG2_ID Pint32_t CAL_MAG2_ROTÿÿÿÿ Pfloat CAL_MAG2_XOFF Pfloat CAL_MAG2_YOFF Pfloat CAL_MAG2_ZOFF Pfloat CAL_MAG2_XSCALE €? Pfloat CAL_MAG2_YSCALE €? Pfloat CAL_MAG2_ZSCALE €? Pfloat CAL_ACC2_XOFF Pfloat CAL_ACC2_YOFF Pfloat CAL_ACC2_ZOFF Pfloat CAL_ACC2_XSCALE €? Pfloat CAL_ACC2_YSCALE €? Pfloat CAL_ACC2_ZSCALE €? Pint32_t CAL_ACC_PRIME
Pint32_t CAL_GYRO_PRIME ! Pint32_t CAL_MAG_PRIME Pint32_t CAL_MAG_SIDES? Pint32_t CAL_BARO_PRIME Pfloat SENS_DPRES_OFF Pfloat SENS_DPRES_ANSC Pfloat SENS_BARO_QNH P}D Pint32_t SENS_BOARD_ROT Pfloat SENS_BOARD_Y_OFF³ñ—? Pfloat SENS_BOARD_X_OFFN�À¿ Pfloat SENS_BOARD_Z_OFF Pint32_t SENS_EN_LL40LS Pint32_t SENS_EN_SF0X Pint32_t SENS_EN_MB12XX Pint32_t SENS_EN_TRONE Pint32_t SENS_EN_SF1XX Pint32_t SYS_AUTOSTART$' Pint32_t SYS_AUTOCONFIG Pint32_t SYS_USE_IO Pint32_t SYS_RESTART_TYPE Pint32_t SYS_MC_EST_GROUP Pint32_t SYS_COMPANION g Pint32_t SYS_PARAM_VER Pint32_t SYS_LOGGER Pint32_t UAVCAN_ENABLE Pint32_t VT_TYPE Pint32_t VT_OPT_RECOV_EN Pfloat VT_WV_YAWR_SCLš™> Pint32_t VT_NAV_FORCE_VT Pint32_t COM_FLTMODE1ÿÿÿÿ Pint32_t COM_FLTMODE2ÿÿÿÿ Pint32_t COM_FLTMODE3ÿÿÿÿ Pint32_t COM_FLTMODE4ÿÿÿÿ Pint32_t COM_FLTMODE5ÿÿÿÿ Pint32_t COM_FLTMODE6ÿÿÿÿ Pint32_t RC_MAP_FAILSAFE A vehicle_attitude A actuator_outputs A telemetry_status A vehicle_command A vehicle_status A vtol_vehicle_status A commander_state A satellite_info A vehicle_attitude_setpoint A vehicle_rates_setpoint A
actuator_controls A actuator_controls_0 A actuator_controls_1 A
vehicle_local_position" A vehicle_local_position_setpoint A vehicle_global_position# A vehicle_global_velocity_setpoint A battery_status A system_power A position_setpoint_triplet A att_pos_mocap A vision_position_estimate A optical_flow A rc_channels A input_rc A airspeed A differential_pressure A distance_sensor A) distance_sensor A* distance_sensor
A esc_status A estimator_status A ekf2_innovations A tecs_status A wind_estimate A ! control_state A " camera_trigger
A # cpuload A $ gps_dump A % sensor_preflight A &