9 Commits

Author SHA1 Message Date
PlayeRom
cee3f44622 Upgrade version number 2022-08-16 03:30:03 +02:00
PlayeRom
602396729c Fix AI aircraft going underground during take-off due to uneven runway elevations.
Add possibility to set thermal with configuration of its parameters.
2022-08-16 03:27:37 +02:00
PlayeRom
2fa6110ba0 Add printing messages to the console 2022-08-15 01:25:13 +02:00
PlayeRom
2e5dbbc0b8 Add a check if the runway name exists in the airport runway table 2022-08-15 01:21:21 +02:00
PlayeRom
4a8e1e4981 Code review 2022-08-15 00:41:04 +02:00
PlayeRom
1f39ea2347 Corrections for comments the code 2022-08-14 21:10:57 +02:00
PlayeRom
33c0c12984 Corrections in readme file 2022-08-14 19:18:46 +02:00
PlayeRom
6ecea39828 Fix typo 2022-08-14 14:29:39 +02:00
PlayeRom
a31e99bf4b Update download link 2022-08-14 13:11:01 +02:00
11 changed files with 452 additions and 56 deletions

12
CHANGELOG.md Normal file
View File

@@ -0,0 +1,12 @@
# CHANGELOG
## v.1.1.0
- Fix AI aircraft going underground during take-off due to uneven runway elevations.
- Possibility to add thermal with configuration of its parameters.
## v.1.0.0
- Possibility to call a towing aircraft at any airport
- One defined route for the towing aircraft
- 3 types of tow airplanes

View File

@@ -2,10 +2,10 @@
This is an add-on designed to include an AI aircraft that will be able to tow a glider. The main idea is to be able to do this at any airport where you start with your glider.
## Instalation
## Installation
This add-on creates a flight plan in real-time based on the airport you are at. Unfortunately this causes a problem which you will have to solve manually by following the instructions below.
Namely, the flight plan for AI aircraft, by default, must be stored in the `$FG_ROOT/AI/FlightPlanes/` directory. But for security reasons, Nasal scripts cannot save files to that directory, but can save e.g. to `$FG_HOME/Export/`. Therefore, the run-time created flight plan will just be saved to `$FG_HOME/Export/Addons/org.flightgear.addons.Aerotow/AI/FlightPlanes/`. The problem is that FlightGear does not know that it is supposed to look for the flight plan file in this location as well, so you have to tell it manually by using the `--data` command line option.
Namely, the flight plan for AI aircraft, by default, must be stored in the `$FG_ROOT/AI/FlightPlans/` directory. But for security reasons, Nasal scripts cannot save files to that directory, but can save e.g. to `$FG_HOME/Export/`. Therefore, the run-time created flight plan will just be saved to `$FG_HOME/Export/Addons/org.flightgear.addons.Aerotow/AI/FlightPlans/`. The problem is that FlightGear does not know that it is supposed to look for the flight plan file in this location as well, so you have to tell it manually by using the `--data` command line option.
`$FG_HOME` is the FlightGear home path. It differs depending on the operating system.
Under Linux/macOS `$FG_HOME` is `/home/{username}/.fgfs/`.
@@ -32,14 +32,28 @@ You can disconnect from the aircraft at any time, most often by pressing the `o`
## Menu of add-on
This add-on add a new item to main menu named "Aerotow Everywhere" with following items:
This add-on add a new item to main menu named `Aerotow Everywhere` with following items:
1. `Call for Piper J3 Cub aircraft` - load AI tow sceneraio with Piper J3 Cub. Possible altitude to reach ~3,600 ft.
2. `Call for Robin DR400 aircraft` - load AI tow sceneraio with Robin DR400. This aircraft has better performance and can take you to over 4,500 ft.
3. `Call for Cessna 182 aircraft` - load AI tow sceneraio with Cessna 182. This aircraft has little bit better performance than Robin.
4. `Disable tow aircraft` - unload AI tow sceneraio.
5. `Help` - display help dialog.
6. `About` - display about dialog with add-on information.
5. `Add thermal` - display the dialog for configuring and adding thermal.
6. `Help` - display help dialog.
7. `About` - display about dialog with add-on information.
## Adding thermals
An additional feature of this add-on is the possibility of placing the thermals just in front of the glider. To do this, go to menu `Aerotow Everywhere` -> `Add thermal` where you can configure the following parameters:
1. `Distance` - the distance in metres at which the thermals will be placed in front of the gliders.
2. `Strength` - thermal strength in feet per second.
3. `Diameter` - diameter of thermal in feet.
4. `Height` - height of thermal in feet above mean sea level.
Click `Add thermal` button for add the thermal.
Many thanks to the forum user "wlbragg" for proposing and presenting a solution to this feature.
## Limitations

View File

@@ -27,6 +27,16 @@
<org.flightgear.addons.Aerotow>
<addon-devel>
<ai-model type="string">Cub</ai-model>
<add-thermal>
<distance-m type="int">300</distance-m>
<distance-nm type="double">0.16</distance-nm>
<strength-fps type="double">16.0</strength-fps>
<strength-mps type="double">4.88</strength-mps>
<diameter-ft type="int">4000</diameter-ft>
<diameter-m type="int">1219</diameter-m>
<height-msl type="int">9000</height-msl>
<height-msl-m type="int">2742</height-msl-m>
</add-thermal>
<help-text type="string">=== How to start? ===
From the top menu, select "Aerotow Everywhere" -> "Call for Piper J3 Cub aircraft", "Robin DR400" or "Cessna 182" (yes, you can choose from many aircrafts).

View File

@@ -21,11 +21,15 @@ var unload = func(addon) {
#
# Other resources should be freed by adding the corresponding code here,
# e.g. myCanvas.del();
aerotow.uninit();
thermal.uninit();
}
var main = func(addon) {
print("Aerotow Everywhere add-on initialized from path ", addon.basePath);
# Create $FG_HOME/Export/Addons/org.flightgear.addons.Aerotow directory
addon.createStorageDir();
# Create /AI/FlightPlans/ directory in $FG_HOME/Export/Addons/org.flightgear.addons.Aerotow/
@@ -36,17 +40,19 @@ var main = func(addon) {
loadExtraNasalFiles(addon);
setlistener("/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/ai-model", func(n) {
aerotow.startAerotow();
});
aerotow.init();
thermal.init();
}
#
# Load extra Nasal files in main add-on directory
#
var loadExtraNasalFiles = func (addon) {
foreach (var scriptName; ["aerotow", "messages"]) {
foreach (var scriptName; ["aerotow", "messages", "thermal"]) {
var fileName = addon.basePath ~ "/" ~ scriptName ~ ".nas";
print("Loading Aerotown Add-on module ", fileName);
io.load_nasal(fileName, scriptName);
if (io.load_nasal(fileName, scriptName)) {
print("Aerotown Add-on module \"", scriptName, "\" loaded OK");
}
}
}

View File

@@ -53,10 +53,6 @@
<value type="string">c182</value>
</binding>
</item>
<item>
<label>------------------</label>
<enabled>false</enabled>
</item>
<item>
<label>Disable tow aircraft</label>
<binding>
@@ -65,7 +61,18 @@
</binding>
</item>
<item>
<label>------------------</label>
<label>---------------------</label>
<enabled>false</enabled>
</item>
<item>
<label>Add thermal</label>
<binding>
<command>dialog-show</command>
<dialog-name>add-thermal</dialog-name>
</binding>
</item>
<item>
<label>---------------------</label>
<enabled>false</enabled>
</item>
<item>

View File

@@ -21,7 +21,7 @@
<addon>
<identifier type="string">org.flightgear.addons.Aerotow</identifier>
<name type="string">Aerotow Everywhere</name>
<version type="string">1.0.0</version>
<version type="string">1.1.0</version>
<authors>
<author>
@@ -81,7 +81,7 @@
</home-page>
<download type="string">
https://github.com/PlayeRom/flightgear-addon-aerotow-everywhere
https://github.com/PlayeRom/flightgear-addon-aerotow-everywhere/releases/latest
</download>
<support type="string">
@@ -97,6 +97,7 @@
<tag type="string">aerotow</tag>
<tag type="string">AI</tag>
<tag type="string">scenario</tag>
<tag type="string">thermal</tag>
</tags>
</addon>
</PropertyList>

View File

@@ -30,6 +30,26 @@ var g_fpFileHandler = nil; # Handler for wrire flight plan to file
var g_coord = nil; # Coordinates for flight plan
var g_heading = nil; # AI plane heading
var g_altitude = nil; # AI plane altitude
var g_listeners = [];
#
# Initialize thermal module
#
var init = func () {
# Listener for ai-model property triggered when the user select a tow aircraft from add-on menu
append(g_listeners, setlistener(addon.node.getPath() ~ "/addon-devel/ai-model", func () {
startAerotow();
}));
}
#
# Uninitialize thermal module
#
var uninit = func () {
foreach (var listener; g_listeners) {
removelistener(listener);
}
}
#
# Main function to prepare AI scenario and run it.
@@ -152,7 +172,7 @@ var isScenarioAdded = func () {
# Return name of selected aircraft. Possible values: "Cub", "DR400", "c182".
#
var getSelectedAircraft = func () {
return getprop("/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/ai-model") or "Cub";
return getprop(addon.node.getPath() ~ "/addon-devel/ai-model") or "Cub";
}
#
@@ -177,13 +197,19 @@ var generateFlightPlanXml = func () {
}
var airport = airportinfo(icao);
if (!contains(airport.runways, runwayName)) {
messages.displayError("The " ~ icao ~" airport does not have runway " ~ runwayName);
return 0;
}
var runway = airport.runways[runwayName];
var minRwyLength = getMinRunwayLength();
if (runway.length < minRwyLength) {
messages.displayError(
"This runway is too short. Please choose a longer one than " ~ minRwyLength ~ " m "
~ "(" ~ math.round(minRwyLength * M2FT) ~ " ft)."
~ "(" ~ math.round(minRwyLength * globals.M2FT) ~ " ft)."
);
return 0;
}
@@ -229,7 +255,7 @@ var generateFlightPlanXml = func () {
addWptGround({"hdgChange": 0, "dist": 10 * perf.rolling}, {"altChange": 0, "ktas": perf.speed});
# Takeof
addWptAir({"hdgChange": 0, "dist": 100 * perf.rolling}, {"altChange": 3, "ktas": perf.speed * 1.05});
addWptAir({"hdgChange": 0, "dist": 100 * perf.rolling}, {"elevationPlus": 3, "ktas": perf.speed * 1.05});
addWptAir({"hdgChange": 0, "dist": 100}, {"altChange": perf.vs / 10, "ktas": perf.speed * 1.025});
# Circle around airport
@@ -298,7 +324,7 @@ var getAircraftPerformance = func () {
"rolling": 2,
};
}
if (aiModel == "c182") {
return {
"vs": 295, # ft per 1000 m
@@ -318,27 +344,28 @@ var getAircraftPerformance = func () {
#
# Initialize AI aircraft variable
#
# isGliderPos - Pass 1 for set AI aircraft's coordinates as glider position, 0 set coordinates as runway threshold.
# airport - Object from airportinfo().
# runway - Object of runway from which the glider start.
# isGliderPos - Pass 1 for set AI aircraft's coordinates as glider position, 0 set coordinates as runway threshold.
#
var initAircraftVariable = func (airport, runway, isGliderPos = 1) {
var gliderCoord = geo.aircraft_position();
# Set coordinates as glider position or runway threshold
g_coord = isGliderPos
g_coord = isGliderPos
? gliderCoord
: geo.Coord.new().set_latlon(runway.lat, runway.lon);
# Set airplane heading as runway heading
g_heading = runway.heading;
# Set airplane altitude as airport elevation
g_altitude = gliderCoord.alt() * M2FT;
# Set AI airplane altitude as glider altitude (assumed it's on the ground).
# It is more accurate than airport.elevation.
g_altitude = gliderCoord.alt() * globals.M2FT;
}
#
# Get distance from glider to runway threshold
# Get distance from glider to runway threshold e.g. in case that the user taxi from the runway threshold
#
# runway - Object of runway from which the glider start
# Return the distance in metres, of the glider's displacement from the runway threshold.
@@ -358,7 +385,7 @@ var getMinRunwayLength = func () {
if (aiModel == "DR400") {
return 470;
}
if (aiModel == "c182") {
return 508;
}
@@ -397,25 +424,28 @@ var addWptAir = func (coordOffset, performance) {
# sec - Number of seconds for wait
#
var addWptWait = func (sec) {
wrireWpt("WAIT", {"hdgChange": nil, "dist": nil}, {"altChange": nil, "ktas": nil}, nil, sec);
wrireWpt("WAIT", {}, {}, nil, sec);
}
#
# Add "END" waypoint
#
var addWptEnd = func () {
wrireWpt("END", {"hdgChange": nil, "dist": nil}, {"altChange": nil, "ktas": nil});
wrireWpt("END", {}, {});
}
#
# Write waipoint to flight plan file
# Write waypoint to flight plan file
#
# name - The name of waypoint
# coordOffset.hdgChange - How the aircraft's heading supposed to change?
# coordOffset.dist - Distance in meters to calculate next waypoint coordinates
# performance.altChange - How the aircraft's altitude is supposed to change?
# performance.elevationPlus - Set aircraft altitude as current terrain elevation + given value in feets.
# It's best to use for the first point in the air to avoid the plane collapsing into
# the ground in a bumpy airport
# performance.ktas - True air speed of AI plane at the waypoint
# groundAir - Allowe value: "ground or "air". The "ground" means that AI plane is on the ground, "air" - in air
# groundAir - Allowed value: "ground or "air". The "ground" means that AI plane is on the ground, "air" - in air
# sec - Number of seconds for "WAIT" waypoint
#
var wrireWpt = func (
@@ -425,8 +455,8 @@ var wrireWpt = func (
groundAir = nil,
sec = nil
) {
var localCoord = nil;
if (coordOffset.hdgChange != nil and coordOffset.dist != nil) {
var coord = nil;
if (contains(coordOffset, "hdgChange") and contains(coordOffset, "dist")) {
g_heading = g_heading + coordOffset.hdgChange;
if (g_heading < 0) {
g_heading = 360 + g_heading;
@@ -437,24 +467,29 @@ var wrireWpt = func (
}
g_coord.apply_course_distance(g_heading, coordOffset.dist);
localCoord = g_coord;
coord = g_coord;
}
var localAlt = nil;
if (performance.altChange != nil) {
g_altitude = g_altitude + performance.altChange;
localAlt = g_altitude;
var alt = nil;
if (coord != nil and contains(performance, "elevationPlus")) {
var elevation = geo.elevation(coord.lat(), coord.lon());
if (elevation == nil) {
g_altitude = g_altitude + performance.elevationPlus;
}
else {
g_altitude = elevation * globals.M2FT + performance.elevationPlus;
}
alt = g_altitude;
}
else if (contains(performance, "altChange")) {
g_altitude = g_altitude + performance.altChange;
alt = g_altitude;
}
var ktas = contains(performance, "ktas") ? performance.ktas : nil;
name = name == nil ? g_wptCount : name;
var data = getWptString(
name,
localCoord,
localAlt,
performance.ktas,
groundAir,
sec
);
var data = getWptString(name, coord, alt, ktas, groundAir, sec);
io.write(g_fpFileHandler, data);

View File

@@ -45,7 +45,7 @@
<hrule/>
<text>
<label>Aerotow Everywhere version 1.0.0 - 13th August 2022</label>
<label>Aerotow Everywhere version 1.1.0 - 16th August 2022</label>
</text>
<text>

214
gui/dialogs/add-thermal.xml Normal file
View File

@@ -0,0 +1,214 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
#
# Aerotow Everywhere - Add-on for FlightGear
#
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
#
# Copyright (C) 2022 Roman Ludwicki
#
# Aerotow Everywhere is an Open Source project and it is licensed
# under the GNU Public License v3 (GPLv3)
#
-->
<!-- This file requires FlightGear version 2018.2 or newer -->
<PropertyList>
<name>add-thermal</name>
<resizable>false</resizable>
<layout>vbox</layout>
<default-padding>3</default-padding>
<group>
<layout>hbox</layout>
<empty>
<stretch>1</stretch>
</empty>
<text>
<label>Add thermal</label>
</text>
<empty>
<stretch>1</stretch>
</empty>
<button>
<pref-width>16</pref-width>
<pref-height>16</pref-height>
<legend></legend>
<keynum>27</keynum>
<border>2</border>
<binding>
<command>dialog-close</command>
</binding>
</button>
</group>
<hrule/>
<text>
<halign>left</halign>
<label> This option allows the thermal to be placed at the distance </label>
</text>
<text>
<halign>left</halign>
<label> designated below in front of the glider along with other parameters. </label>
</text>
<hrule/>
<group>
<layout>table</layout>
<text>
<row>0</row>
<col>0</col>
<halign>right</halign>
<label> Distance in front of the glider</label>
</text>
<input>
<row>0</row>
<col>1</col>
<label>m</label>
<halign>left</halign>
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/add-thermal/distance-m</property>
<live>true</live>
<binding>
<command>dialog-apply</command>
</binding>
</input>
<text>
<row>0</row>
<col>2</col>
<label>Dummy label</label>
<halign>left</halign>
<format>(%.2f nm)</format>
<live>true</live>
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/add-thermal/distance-nm</property>
</text>
<text>
<row>1</row>
<col>0</col>
<halign>right</halign>
<label>Strength</label>
</text>
<input>
<row>1</row>
<col>1</col>
<label>ft/s</label>
<halign>left</halign>
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/add-thermal/strength-fps</property>
<live>true</live>
<binding>
<command>dialog-apply</command>
</binding>
</input>
<text>
<row>1</row>
<col>2</col>
<label>Dummy label</label>
<halign>left</halign>
<format> (%.2f m/s)</format>
<live>true</live>
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/add-thermal/strength-mps</property>
</text>
<text>
<row>2</row>
<col>0</col>
<halign>right</halign>
<label>Diameter</label>
</text>
<input>
<row>2</row>
<col>1</col>
<label>ft</label>
<halign>left</halign>
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/add-thermal/diameter-ft</property>
<live>true</live>
<binding>
<command>dialog-apply</command>
</binding>
</input>
<text>
<row>2</row>
<col>2</col>
<label>Dummy label</label>
<halign>left</halign>
<format>(%.f m)</format>
<live>true</live>
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/add-thermal/diameter-m</property>
</text>
<text>
<row>3</row>
<col>0</col>
<halign>right</halign>
<label>Height MSL</label>
</text>
<input>
<row>3</row>
<col>1</col>
<label>ft</label>
<halign>left</halign>
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/add-thermal/height-msl</property>
<live>true</live>
<binding>
<command>dialog-apply</command>
</binding>
</input>
<text>
<row>3</row>
<col>2</col>
<label>Dummy label</label>
<halign>left</halign>
<format>(%.f m)</format>
<live>true</live>
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/add-thermal/height-msl-m</property>
</text>
</group>
<hrule/>
<group>
<empty>
<stretch>true</stretch>
</empty>
<layout>hbox</layout>
<button>
<legend>Add thermal</legend>
<equal>true</equal>
<binding>
<command>dialog-apply</command>
</binding>
<binding>
<command>nasal</command>
<script><![CDATA[thermal.add();]]></script>
</binding>
<binding>
<command>dialog-close</command>
</binding>
</button>
<button>
<legend>Cancel</legend>
<equal>true</equal>
<default>true</default>
<key>Esc</key>
<binding>
<command>dialog-close</command>
</binding>
</button>
<empty>
<stretch>true</stretch>
</empty>
</group>
</PropertyList>

View File

@@ -34,20 +34,20 @@ var displayError = func (message) {
# type - The type of message. It can take values as "ok" or "error".
#
var display = func (message, type) {
# Print to console
print("Aerotow Everywhere add-on: " ~ message);
# Read the message by speech synthesizer
props.globals.getNode("/sim/sound/voices/ai-plane").setValue(message);
# Display message on the screan
# Display message on the screen
var durationInSec = int(size(message) / 12) + 3;
var window = screen.window.new(nil, -40, 10, durationInSec);
window.bg = [0.0, 0.0, 0.0, 0.40];
if (type == "error") {
window.fg = [1.0, 0.0, 0.0, 1];
}
else {
window.fg = [0.0, 1.0, 0.0, 1];
}
window.fg = type == "error"
? [1.0, 0.0, 0.0, 1]
: [0.0, 1.0, 0.0, 1];
window.align = "center";
window.write(message);

97
thermal.nas Normal file
View File

@@ -0,0 +1,97 @@
#
# Aerotow Everywhere - Add-on for FlightGear
#
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
#
# Copyright (C) 2022 Roman Ludwicki
#
# Aerotow Everywhere is an Open Source project and it is licensed
# under the GNU Public License v3 (GPLv3)
#
#
# Constants
#
var addon = addons.getAddon("org.flightgear.addons.Aerotow");
#
# Variables
#
var g_thermalListeners = [];
#
# Initialize thermal module
#
var init = func () {
append(g_thermalListeners, setlistener(addon.node.getPath() ~ "/addon-devel/add-thermal/distance-m", func () {
setprop(
addon.node.getPath() ~ "/addon-devel/add-thermal/distance-nm",
getprop(addon.node.getPath() ~ "/addon-devel/add-thermal/distance-m") * globals.M2NM
);
}));
append(g_thermalListeners, setlistener(addon.node.getPath() ~ "/addon-devel/add-thermal/strength-fps", func () {
setprop(
addon.node.getPath() ~ "/addon-devel/add-thermal/strength-mps",
getprop(addon.node.getPath() ~ "/addon-devel/add-thermal/strength-fps") * globals.FPS2KT * globals.KT2MPS
);
}));
append(g_thermalListeners, setlistener(addon.node.getPath() ~ "/addon-devel/add-thermal/diameter-ft", func () {
setprop(
addon.node.getPath() ~ "/addon-devel/add-thermal/diameter-m",
getprop(addon.node.getPath() ~ "/addon-devel/add-thermal/diameter-ft") * globals.FT2M
);
}));
append(g_thermalListeners, setlistener(addon.node.getPath() ~ "/addon-devel/add-thermal/height-msl", func () {
setprop(
addon.node.getPath() ~ "/addon-devel/add-thermal/height-msl-m",
getprop(addon.node.getPath() ~ "/addon-devel/add-thermal/height-msl") * globals.FT2M
);
}));
}
#
# Uninitialize thermal module
#
var uninit = func () {
foreach (var listener; g_thermalListeners) {
removelistener(listener);
}
}
#
# Add thermal 300 m before glider position.
#
# Return 1 on successful, otherwise 0.
#
var add = func () {
var heading = getprop("/orientation/heading-deg") or 0;
var distance = getprop(addon.node.getPath() ~ "/addon-devel/add-thermal/distance-m") or 300;
var position = geo.aircraft_position();
position.apply_course_distance(heading, distance);
# Get random layer from 1 do 4
var layer = int(rand() * 4) + 1;
var args = props.Node.new({
"type": "thermal",
"model": "Models/Weather/altocumulus_layer" ~ layer ~ ".xml",
"latitude": position.lat(),
"longitude": position.lon(),
"strength-fps": getprop(addon.node.getPath() ~ "/addon-devel/add-thermal/strength-fps") or 16.0,
"diameter-ft": getprop(addon.node.getPath() ~ "/addon-devel/add-thermal/diameter-ft") or 4000,
"height-msl": getprop(addon.node.getPath() ~ "/addon-devel/add-thermal/height-msl") or 9000,
"search-order": "DATA_ONLY"
});
if (fgcommand("add-aiobject", args)) {
messages.displayOk("The thermal has been added");
return 1;
}
messages.displayError("Adding thermal failed");
return 0;
}