This is the open source repository for code that implements the Swift Navigation Console. Releases hosted on this page are provisional and are NOT official releases. For official (and supported) releases please visit our downloads page and see our guide on how to install the application.
Install Rust: https://rustup.rs/
Install cargo-make: cargo install --force cargo-make
.
Set up standalone Python environment:
cargo make setup-builder
Install cmake, clang, and capnp in your respective OS.
# Windows - install with installer, or via chocolatey
choco install cmake llvm capnproto zstandard
# Mac
xcode-select install
brew install cmake capnp zstd create-dmg wget
# Linux
apt install cmake libclang-dev capnproto zstd
Install ImageMagick:
# Windows
choco install imagemagick
# Mac
brew install imagemagick
# Linux
apt install imagemagick
The module used for generating rust bindings for native libraries; rust-bindgen
has been observed to fail to find system headers (i.e. assert.h
, math.h
) on
newer versions of macos. Fortunately we can add include search paths to pass to
clang by setting an environment variable:
export BINDGEN_EXTRA_CLANG_ARGS=-I$(xcrun --show-sdk-path)/usr/include
To run the app (in dev mode):
cargo make run
Or in "prod" mode (compiles a wheel for the backend):
cargo make prod-run
If you are interested in debugging the frontend, you can first record a capnp recording. Either connect to a device via the GUI or command line and include the flag:
--record-capnp-recording
This will save a .pickle
file in your current working directory.
NOTE: Use of this recording is intended only for debugging the swift-console and not for long term storage of streams. There is no guarantee the recording will be compatible with other console versions, as the messaging format between backend and frontend has no guarantee of backwards or forwards compatibility.
Debugging internal messaging should be version specific and recording it should be tied to its version.
If you have recorded a capnp recording pickle file as shown in the previous step, now you can replay this file. If you already have the standard development environment set up, you can simply use the command line flag:
--read-capnp-recording <path/to/pickle-file>
If you want to run the application without the standard development envionrment.
# Set up a python 3.8 environment.
# Install flit for generating a wheel from our pyproject.toml.
pip install flit
# Generate the wheel.
python -m flit build --no-setup-py
# Install the wheel.
pip install dist/swiftnav_console-0.1.0-py3-none-any.whl --force-reinstall
# Generate our resources file (may need to manually point to this binary installed by PySide6).
pyside6-rcc resources/console_resources.qrc -o swiftnav_console/console_resources.py -g python
# Run the application.
python -m swiftnav_console.main --read-capnp-recording path/to/pickle-file
# Note some of these calls may be different if you are attempting on windows,
# assume binaries end with ".exe". May also need to direct the correct python pip
# depending on how your python 3.8 environment is set up.
cargo make create-dist
# In order to create an installer:
cargo make dist-to-installer
The main github actions workflow will detect a new tag was created. If all checks and builds succeed a new release will be made with all assests attached to it. This process typically takes about 40 minutes. Although any official release should be triggered off the main branch, you can use this process to create a "test release" for debugging purposes (you can append a moniker to denote the release is a test e.g. v4.0.6-test)
git tag vX.X.X && git push origin vX.X.X
Prerequisites:
- Windows
- NSIS - Nullsoft Scriptable Install System
- NSIS ShellExecAsUser plugin
- All
cargo install hyperfine
To run the frontend benchmarks:
git lfs pull
cargo make create-dist
cargo make frontend-cpu-bench
The qmlformat
tool is not included in the Qt PySide6 pip packages so the quickest (only?) way to get the tool is by installing Qt on your machine. In CI, we rely on a quick installer tool aqt
to achieve this.
-
Create a python 3.7+ virtual environment:
conda create -n qt python=3.10
-
Install the
aqt
package:pip install aqtinstall
-
Install the desired version of qt. This website will help you customize the install command for your machine, https://ddalcino.github.io/aqt-list-server/ . Here are some OS specific generic install commands (visit the website if you need a different architecture):
# Linux aqt install-qt linux desktop 6.4.1 gcc_64 # MacOS aqt install-qt mac desktop 6.4.1 clang_64 # Windows aqt install-qt windows desktop 6.4.1 win64_msvc2019_64
-
Add the
qmlformat
tool to your path. It is located in<qt install dir>/<qt version>/<arch>/bin/
, this may be different depending on your system. -
Now you can run the tool, but use this cargo make command to match CI behavior:
cargo make qml-format
Download the universal Qt installer from qt.io. Run the installer, pick custom installation, and make sure that the Qt version installed matches the PySide6 version that is used for this project. Qt Creator will be automatically installed.
Once done, start the app using cargo make run --qmldebug
or
cargo make qml-run --qmldebug
.
The application will indicate that the client application is waiting for a
socket connection from the QML debugger. This is displayed to stderr.
QML Debugger: Waiting for connection on port 10002...
Launch Qt Creator and load the project swift-toolbox.pyproject.
From the Analyze
menu, choose QML Profiler (Attach to waiting application)
.
Connect to the same port that is shown in the shell you started the toolbox
from.
The application should now start-up and can be used. Perform whatever actions for which you wish to capture profiling information. Qt Creator's Profiler pane will just be showing an elapsed timer counting up, with no information shown. This is expected.
When you are done performing actions in the application, either click the red "recording" circle button to stop the recording, or close the client application.
Upon doing this, Qt Creator's QML Profiling view should show profiling data.
You now have profiling data for the session you can comb through in the Qt Creator QML Profiling view that shows.
In order to enable QML debugging, add the command line option -qmldebug
.
QML debugging does not entirely work currently for this project and
still needs to be fleshed out. The internal tracking issue for this is
CPP-400.
After making changes, run to tasks to ensure the code is ready for submission
# fetch test data
git lfs pull
cargo make check-all
cargo make tests
It may be helpful to profile the application. To do this, you can use valgrind. One mode of valgrind that is particularly useful is --callgrind.
In order to use this, you need to install valgrind in your favorite OS.
You can initiate this using the cargo make target callgrind-run
.
Once you run the app with callgrind and quit the application, it will generate a file named callgrind.out.<unique id>.
To analyze the results, use kcachegrind or qcachegrind, which you should be able
to find installable with your distribution's package manager.
Both are the same tool, just have slightly different dependencies. If you are
using KDE, install kcachegrind
. If you are using Gnome, install qcachegrind
.
Pass the callgrind.out
file to the tool like so:
qcachegrind callgrind.out.163238
To dive into the source code, download the sources for the libraries you wish to inspect (like Qt or Python), matching the exact version, and set the source directories in QCacheGrind from the configuration dialog accessible from the View menu (View->Configure...->Source Annotation->Add).
The Qt libraries contained in the PySide6 python module do not contain debug
symbols, and thus you see limited information from Qt when profiling. To add
debug symbols to this, download and install Qt from qt.io, matching the PySide6
version, and use the included utils/symlink-qt-installer-libs-to-pyside6.sh
script.
You can fetch the Qt source code either from the Qt installer, or from git
using the v6.n.n
tags.
This has only been tested on Linux, but it should also work on macOS and Windows.
Someone else may be able to expand this showing how to profile the Rust code.
Rust is used for the "backend" logic of the application. The library pyo3 (and companion library setuptools-rust) are used to implement a native Python extension.
We're using Qt 6 via PySide6 (the official Python bindings for Qt).
QML (QtQuick Mark-up Language) is used to model the UI.
python-build-standalone provides redistributable builds of Python, these builds are designed to function in a variety of generic Linux, Windows and macOS environments.
One of the things that made the old console difficult to maintain was the
number of Python dependencies that were required to implement the console. In
particular things like the TraitsUI
library bound us to particular versions of
PyQt that worked well with TraitsUI
. Additionally, if we wanted to use new libraries,
PyInstaller's "hooks" needed to be up-to-date enough to work with these
libraries (see PyInstaller hooks for reference).
PyInstaller can be a great way to package a massive framework like Qt, but it still requires PyInstaller to process and package your application, which creates an environment where your deployed app isn't quite the same as your development app.
In the current framework don't use PyInstaller, but rather rely on the redistributable nature of the "Python standalone" build to create a build of the app that is usable for both development and distribution. The Python standlone build can be packaged up and dropped onto a user's sytem as a simple zip file, or via a standard installer technology like NSIS/MSI, DEB/RPM or a macOS app bundle.
If we didn't bundle dependencies like Qt, we would have to rely on Qt being present on Unix like systems via package dependencies, and on other systems we need to use tools like macdeployqt and windeployqt.
Also in service of this goal, it makes sense to minimize the number of dependencies that we use for Python in order to avoid this problem. This ends up "dovetailing" well with the usage of Rust as the UI backend. Rust's package management prefers static linking, so depedencies are wrapped up into a single redistributable object, which makes distribution to client machines easier.
Resource management is another concern, for non-code assets like pictures,
protocol definitions (.capnp
files) and UI mark-up files (.qml
) we need a
system to bundle these -- we use Qt's resource system (via qrc) but we
could use things like Rust's include_bytes!
.
To this end the original prototype of this app attempted to impose these constraints:
- Minimal dependencies in Python: only Qt (via PySide6) and Capnproto (pycapnp)
- All other necessary external libraries should be include via Rust libraries
The QtQuick Mark-up Language (QML) is used to code the UI. QML provides an abstraction layer for describing the UI that doesn't require the UI to be hand coded in any particular language. While Qt already has support for this through their UIC files, QML also provides support for display logic to be directly embedded in the QML file via JavaScript - this helps with the portability of the QML since UI logic doesn't need to be encoded in the language hostign the QML.
To this end the prototype attempts to imposes these constraints:
- No UI logic in Python if possible, UI logic should be encoded in small bits of JavaScript (example)
- Python code should focus on "data binding" and "message passing" only
- For "data binding", this means Python code should be what's minimally necessary for Qt to fetch display data
- For "message passing", to implement features, Python code will need to be written to pass data to the backend, and to marshal data from the backend into the data binding objects.
The pyqtdeploy project: https://pypi.org/project/pyqtdeploy/ -- from the same people that maintain PyQt5 (not Qt/Nokia) is designed to allow PyQt5 applications to deploy to desktop and mobile environments, however the project page emphasizes that the project was designed specifically around allowing the PyQt5 apps to be deployed to iOS and Android.
It's possible that we can build a C++ shell that hosts the QML, and re-implements the "data binding" and "message passing" code if deploying with something like pyqtdeploy is not sucessful, or proves to be prohibitive. This would allow us to remove Python from the mix completely. Projects like Rust's qmetaobject may make this possible too.
The rust backend gives us a statically type checked, modern programming language with enough library support to support our development activities. The "big" libraries that the current console uses are numpy and pyserial.
The equivalents for Rust are:
Using Rust should give us a head start on speed and resource usage issues. However, we don't want to be bound to tightly with any particular FFI system. To that end, we integrate into Python with pyo3 but we try to minimize the dependence here by driving most of the interaction with the back-end via message passing. This gives the backend greater portability should we need to move to different application host (Rust/C++) for Qt/QML or a different UI system altogether (e.g. we could build native UIs for Android/iOS).
To this end the prototype attempts to imposes these constraints:
- Most interaction with the backend should be via message passing
- FFI with the host language (Python) should focus on start/stopping the backend and exchanging lightweight IPC messages (capnproto)
We use capnproto for message passing since it does not require parsing, the in memory representation is accessed directly by the library, without an unpacking/parsing step -- this makes it suitable for "high speed" scenarios like in memory IPC (where it's preferable to not pay the compute cost of parsing just to cross language barriers). Other formats like protobufs require a parsing phase -- capnproto also has very ergonomic Python bindings. Other formats such as flatbuffers achieve similar goals as capnproto but do not have good Python support.
We do not intend on allowing backward compatibility in internal message passing via capnproto. There may be releases that modify internal message structures and should not be relied on even though these internal packets are being exposed.
To modify map: https://github.com/swift-nav/swift-toolbox/tree/main/resources/web/map/js/trajectory_raw.js
Makefile.toml
uses task called map-js-token
to replace @ACCES_TOKEN@
in trajectory_raw.
Fill environment variable MAPBOX_TOKEN
before any resource generation (compile / run) will populate the access token.
Copyright © 2021-2022 Swift Navigation
Distributed under the MIT open source license.