Architecture¶
This chapter describes XCSoar’s internal code architecture.
Source Organisation¶
XCSoar’s source code is stored in the src directory. This
section tries to give a rough overview where you can find what.
util/: generic C++ utilities that do not depend on external libraries, such as data structures, string operationsMath/: math data types (fixed-point math, angles) and generic formulasGeo/: geographic data structures and formulasFormatter/: code that formats internal values to stringsUnits/: conversion from SI units (“System” units) to configured user unitsNMEA/: data structures for values parsed from NMEAProfile/: user profiles, loading from and saving toIGC/: support for the IGC file formatLogger/: all loggers (NMEA, IGC, flights)thread/: multi-threading support (OS specific)ui/: base library for the graphical user interfaceRenderer/: various graphical renderers, for map and analysisMapWindow/: the mapForm/: modal dialogs and their controls (based on the screen library)Dialogs/: modal dialogs implementations (based on the form library)net/: networking code (OS specific)Operation/: generic code to support cancellable long-running operationsAndroid/: code specific to Android (the native part only; Java code is inandroid/src/Engine/PathSolvers/: an implementation of Dijkstra’s path finding algorithm, for task and contest optimisationEngine/Airspace/: airspace data structures and airspace warningsEngine/Waypoint/: waypoint data structuresEngine/GlideSolvers/: a MacCready implementationEngine/Task/: task data structures and calculationsEngine/Contest/: contest optimisationEngine/Route/: the route planner (airspace and terrain)io/: stream and file I/O (readers, writers, archives). Keep this layer free of UI and backend singletons; device-specific listing belongs inStorage/Repository/: filename patterns and typed data directories (FileType); see XCSoar data directory for the on-disk layoutStorage/: removable storage enumeration, hotplug monitors, and theStorageDeviceabstraction (platform code inStorage/linux/,Storage/win/,Storage/android/)Device/,Computer/,Blackboard/: sensor drivers, glide computer, and thread-specific data copiesDialogs/DataManagement/: data management UI (import, export, backup, file explorer)
Layer dependencies¶
Rough dependency direction (see also project rules in
.cursor/rules/xcsoar-project-rules.mdc):
Foundation (
util/,Math/,Geo/,io/,system/) must not include Engine, Backend, or UI headers.Engine uses Foundation only.
Backend (
Device/,Computer/,Storage/,NOTAM/, …) uses Foundation and Engine. Access UI only through event queues (InputEvents,UI::Notify), not dialogs.UI (
Dialogs/,Form/,Interface.hpp) may use all layers below it. Helpers such asStorage/StorageUtil.cppthat readBackendComponentsare backend/UI glue, not Foundation.
Threads and Locking¶
Threads¶
XCSoar runs on multiple threads, to make the UI responsive but still allow expensive background calculations.
This is how it looks like on Windows and Linux/SDL (software rendering):
The UI thread is the main thread. It starts the other threads and is
responsible for the UI event loop. No other thread is allowed to
manipulate windows. The UI thread has a timer which does regular house
keeping twice per second (ProcessTimer.cpp).
The calculation thread (CalculationThread.cpp,
GlideComputer*.cpp) does all the expensive calculations in
background. It gets data from the devices (through MergeThread) and
forwards it together with calculation results to the drawing thread and
the main thread.
Each device has its own thread (SerialPort.cpp). This is
needed because Windows CE does not support asynchronous COMM port
I/O. The thread is stopped during task declaration (which happens in
the UI thread).
When new data arrives on the serial port, the MergeThread gets
notified, which will merge all sensor values into one data structure. It
will then run cheap calculations, and forwards everything to the
CalculationThread.
With OpenGL, the map is rendered live without a buffer. There is no DrawThread.
On Android, the UI thread is not the main thread - the main thread is
implemented in Java, managed by Android itself. The UI thread listens
for events which the Java part drops into the event queue
(NativeView.java and others). The internal GPS does not need a
thread, it is implemented with Java callbacks. For Bluetooth I/O, there
are two threads implemented in Java (InputThread.java and
OutputThread.java, managed by BluetoothHelper.java).
Network thread and background HTTP¶
In addition to the sensor and UI threads above, XCSoar runs a dedicated
asio event-loop thread (io/async/GlobalAsioThread.cpp,
started from XCSoar.cpp). It hosts CurlGlobal and runs
coroutines injected with Co::InjectTask / Net::AsyncTask.
Typical uses:
NOTAM fetches (
NOTAM/NOTAMGlue.cpp)EDL tile downloads (
Weather/EDL/DownloadGlue.cpp)TIM thermal index, LiveTrack24, SkyLines, and similar clients in
NetComponents.hpp
The UI thread (main event loop) must not perform blocking network
I/O during flight. Long-lived background work belongs on the network
thread; modal downloads tied to one dialog may still use
ShowCoDialog on the UI thread.
Thread rules (same as for Interface.hpp elsewhere):
Do not call
CommonInterface,ActionInterface, or other UI-only APIs from the network thread.InMainThread()checks will fail.Do read any UI state needed for a download on the main thread before starting the coroutine (for example forecast time and isobar for EDL tiles), and pass snapshots into the network work.
Do report completion to the UI with
UI::Notify(ui/event/Notify.cpp), which queues a callback on the main event loop. Apply overlays, update status labels, and callActionInterface::SendUIStateonly from that callback.
NetComponents (NetComponents.hpp) owns long-lived network
clients. Each client that uses Net::AsyncTask should implement
BeginShutdown() and be stopped from
NetComponents::BeginShutdown() before the map and other UI are
torn down. The global pointer is cleared later in Startup.cpp
(DestroyNetComponents).
Shutdown order (see Startup.cpp; simplified):
NetComponents::BeginShutdown()— cancel coroutines and queued downloads; clear map pointers to TIM / SkyLines dataMainWindow::BeginShutdown(); stop merge and calculation threads; join them; deinitialise map and devicesMainWindow::DeinitialiseStorage()— unregister storage UI listenersStorageManager::StopMonitoring()— stop hotplug; destructor joins the enumeration worker whendelete backend_componentsrunsdelete backend_componentsanddelete data_componentsDestroyNetComponents(); destroyMainWindowOn process exit,
Net::DeinitialisedestroysCurlGlobalon the asio thread (DrainCurlinnet/http/Init.cpp)
Deferred UI refresh: callbacks such as async terrain load or
blackboard updates must not call PageActions::Update or
ActionInterface::SendUIState synchronously if that can re-enter
layout while InfoBoxes are being created. Use
MainWindow::SchedulePageActionsUpdate and
ScheduleRefreshInfoBoxes instead (next event-loop iteration).
InfoBoxManager skips work until Create() has finished
(infoboxes_ready).
Weather overlays: map overlays may combine a blackboard listener
(ongoing GPS/time sync, for example Weather/EDL/Glue.cpp) with a
download glue in NetComponents (HTTP fetch and cache, for
example Weather/EDL/DownloadGlue.cpp). New providers (such as
XCTherm) should follow the same split: listener on the UI thread,
network I/O on the asio thread, UI updates via UI::Notify.
Background file jobs¶
Not all background work uses the network thread. Local file jobs (tar backup/restore, import/export copies, device enumeration) use other mechanisms:
Modal progress on the UI thread:
JobDialogruns aJobsubclass on a short-lived worker thread (Job/Thread.cpp) while showingProgressDialog. The UI thread stays responsive; progress is reported throughOperationEnvironment.Modal network work:
ShowCoDialogruns a coroutine on the asio thread (see above). Do not useJobDialogfor HTTP.Fire-and-forget helpers: some UI actions start a detached
std::threadfor a single task (for example deleting a file on removable media). Keep captured state alive (for examplestd::shared_ptr<StorageDevice>) and avoid UI calls from that thread.
When modifying shared backend data (waypoints, airspaces) during such
jobs, use ScopeSuspendAllThreads from Protection.hpp where
appropriate.
Storage and removable media¶
BackendComponents (BackendComponents.hpp) owns backend
singletons including storage_manager (StorageManager).
NetComponents is separate and holds long-lived HTTP clients only.
StorageManager (Storage/StorageManager.cpp):
Owns the platform hotplug monitor and storage enumerator.
Receives topology events (from a platform worker or the UI event loop, depending on the backend).
Runs device re-enumeration on a dedicated worker thread so sysfs, Win32, or SAF walks do not block the UI.
Invokes a constructor-supplied
NotifyCallback(wired inStartup.cpptoMainWindow::SendStorageNotification) so the UI thread callsProcessPendingChanges()and dispatchesStorageEventnotifications to listeners.
UI code registers StorageEventListener instances on the main
thread (for example StorageLocationPickerDialog). Use
StorageUtil (FindDeviceByName, FormatStorageCaption,
EnumerateTarFiles) from UI or backend glue — not from Foundation
io/ (stream-only tar create/restore lives in io/TarBackup).
Thread lifetime: when starting a new storage worker, join any
finished previous std::thread before move-assigning a new one;
otherwise the C++ runtime calls std::terminate(). The destructor
joins the worker after StopMonitoring().
Locking¶
Some data structures are rarely modified. There is no lock for them. For a modifications, all threads must be suspended. Example: waypoints, airspaces.
Other data structures are modified so often that correct locking would
be too much overhead. Each thread and each instance has its own copy.
The lock needs to be obtained only for making the private copy. The
private copy can be used without locking. Example: NMEA_INFO,
DERIVED_INFO.
There are objects which are too expensive to copy. Normal locking
applies to them. We have a template class called Guard to enforce
proper read/write locking. Example: the task.
Accessing Sensor Data¶
Much of XCSoar deals with obtaining sensor data and visualising it.
Suppose you want to write a dialog that needs the current GPS location,
where do you get it? The short and simple answer is: from
CommonInterface::Basic() (the InterfaceBlackboard). Example:
#include "Interface.hpp"
...
const auto &basic = CommonInterface::Basic();
if (basic.location_available)
current_location = basic.location;
This is true for the main thread (aka the “user interface thread”).
Other threads must not use the Interface.hpp library, because the
InterfaceBlackboard is not protected in any way. It contains copies
of various data structures just for the main thread.
This is how sensor data moves inside XCSoar:
The device driver parses input received from its device into its own
NMEAInfo instance inside DeviceBlackboard (i.e.
per_device_data). Then it wakes up the MergeThread to merge the
new data into the central NMEAInfo instance. The MergeThread
hosts the BasicComputer which attempts to calculate missing data
(for example, derives vario from GPS altitude).
The CalculationThread wakes up and receives the MoreData object
from DeviceBlackboard. Here, expensive calculations are performed
(GlideComputer: task engine, airspace warnings, …), resulting in a
DerivedInfo object. The CalculationThread runs no more than
twice per second.
Finally, the UI thread wakes up and receives MoreData and
DerivedInfo via DeviceBlackboard. This updates InfoBoxes and
other UI elements. On Windows, the map is drawn in a separate thread, so
there’s another layer.
Let’s get back to the question: where do I get sensor data? That depends on who you are:
you are the user interface: (InfoBoxes, dialogs, any Window callback):
InterfaceBlackboard(see above). To get notified on changes, register aBlackboardListener(and don’t forget to unregister it).you are the MapWindow: depends! If you’re being called from
OnPaintBuffer(i.e. inside theDrawThread), you must use theMapWindowBlackboard, all others must use theInterfaceBlackboard.you are a “computer” library: you will get the values as a parameter. Don’t try to use the
GlideComputerBlackboarddirectly.you are a device driver: implement the method
OnSensorUpdateorOnCalculatedUpdateif you need to know values from other devices or calculation results.everybody else may use the
DeviceBlackboard, but be sure to lock it while using its data.
Debugging XCSoar¶
The XCSoar source repository contains a module for the GNU debugger
(gdb). It contains pretty-printers for various XCSoar types,
including Angle, GeoPoint and others. These are helpful when you
print values in the debugger. To use it, start the debugging session and
load the module:
$ gdb -ex "source tools/gdb.py" output/UNIX/bin/xcsoar
(gdb) run
The module will automatically convert fixed-point to floating point, radian angles to degrees and more. You can now do fancy stuff like:
(gdb) p basic.location
$1 = GeoPoint(7.93911242887 51.1470221074)
(gdb) p basic.date_time_utc
$2 = DateTime(2012/12/23 21:41:57)
(gdb) p basic.track
$3 = 55.2254197961
(gdb) p basic.external_wind
$4 = GeoVector::ZERO
(gdb) p current_leg.vector_remaining
$5 = GeoVector(267.899420345 107957.109724)
General¶
Minimise the number of colours, and re-use colour groups already defined.
Too much use of colour where it is not required serves only to reduce the effectiveness of bright colours for important items.
High colour saturation elements should be reserved for high importance items
High contrast against background should be reserved for high importance items
Attempt to adopt colours that are intuitive based the function of the item
Minimise the clutter where possible — readibility is essential for use in flight
Use colours defined in
Graphicsaccording to functional name, not their actual colour.Try to maintain consistent use of colours in all uses of that function, such as dialogue graphics as well as map overlays and infoboxes.
Text should always be monochrome.
Use aviation conventions or adopt best aviation human factors standards where possible, in particular:
ICAO Internation Standards and Recommended Practices, Annex 4 to the Convention on International Civil Aviation (Aeronautical Charts).
- `NASA Colour Usage recommendations and design guidelines
- `DOT/FAA/AR-03/67 Human Factors Considerations in the Design and
Evaluation of Electronic Flight Bags (EFBs) <http://www.volpe.dot.gov/hf/aviation/efb/docs/efb_version2.pdf>`__
DOT/FAA/AM-01/17 Human Factors Design Guidelines for Multifunction Displays
Check for performance with respect to colour blindness. This site has a useful tool that can be used to convert screenshots to how they would look to a person with common color blindness: http://www.etre.com/tools/colourcheck/
For safety purposes, avoid use of elements that may encourage or require the user to stare at the screen continuously.
For safety purposes, avoid user controls that have significant risk of producing unsafe results if misconfigured by the pilot.
General colour conventions¶
Colour conventions generally in use throughout the program:
Red for indicator of warning
Orange for indicator of caution
Green for positive indicator of safety
Blue for neutral indicator of safety
Displayed data¶
Where data is invalid, indicate this by not presenting the data or showing dashes.
Present data in user-defined units.
Display numerical data with significant digits appropriate to the accuracy of the calculations, or its functional use by the pilot, whichever is lower.
Main graphics¶
Colors¶
Colour conventions in use, in order of priority, are:
Aircraft black and white, for neutrality but clear identification
Traffic (FLARM) use alarm green, orange, and red.
Lift is vibrant green, sink is copper orange.
Aircraft navigation (route, best cruise track) is (ICAO) dark purple-blue
Task navigation lines and areas are (ICAO) magenta.
Updraft sources and other updraft derived data is sky blue.
(Todo) airspace alert colours
Map culture (topography) and terrain rendering should conform to ICAO Annex 4 where appropriate. Note that some modifications are reasonable for electronic use given that Annex 4 deals with paper charts. Nevertheless, the colour conventions are useful to adopt as they are likely to be intuitive and are designed for aviation use.
Pen styles¶
Map culture should be rendered with a thin pen
Thicker pens used for important (e.g. task, navigational, airspace) lines
Dashed lines are used to increase perceptual priority
Map overlays¶
Elements on the map that are not part of the map layer, such as additional informational widgets (final glide bar, wind, north arrow) should be rendered so as to help those elements be visually separated from the map:
Generally adopt higher contrast (higher colour saturation or darker shade) than the background map layer elements.
For elements covering an area (non line), draw the entire element or a border with a luminosity contrasting pen, of width
IBLSCALE(1).Consider whether the widget is required in all flying states and display modes. if it does not serve a direct functional purpose in some states/modes, do not render it.
Avoid locating widgets at the aircraft symbol (ownship symbol). It is important to keep this area clear so the aircraft symbol can be easily found.
Elements that may be rendered over each other should be organised in order of priority, particularly with alert warning items above caution items above non-alert items.
Terminology¶
Glide Ratio¶
’Glide ratio’ is a non-specific term which can refer to the ratio of horizontal to vertical motion with reference to either the surrounding airmass or the ground.
To reduce confusion, ground-referenced glide ratios (eg distance travelled over ground vs altitude lost) should be referred to by the term ’glide ratio over ground’ when space allows, or ’glide ratio’ / ’GR’.
Air-referenced glide ratios (eg airspeed vs sink rate) should be specified as ’lift/drag ratio’ / ’L/D ratio’ / ’LD’. The lift/drag ratio is numerically equal to the air-referenced glide ratio when flying at constant speed.
If usage spans both air-referenced and ground-referenced glide ratios, the non-specific term ’glide ratio’ / ’GR’ should be used. ’Lift/drag ratio’ should never be used to refer to ground-referenced glide ratios.