Writing Joystick Code: Part 3
Work in progress This article or section will be worked on in the upcoming hours or days. See history for the latest developments. |
Joystick Scripting |
---|
Special snippets
Once again, the examples may use concepts that you need to explore in the Advanced Programming section.
To toggle the Parking brake, use this. Entire code-block shown. Non-repeatable button. (You have to specifically set <repeatable> to true if you want repeatable. It is assumed to be false.)
<button n="1">
<desc>Toggle parking brake</desc>
<binding>
<command>property-toggle</command>
<property>/controls/gear/brake-parking</property>
</binding>
</button>
This method can be used to toggle any property that is on/off. Use Debug - Browse Internal Properties to find suitable candidates. Also see getProp, setProp and setAll below.
Remember, it toggles the property, you can't use it to set the property into a specific state.
To make condition work with mixture on a lever (axis). This is pertinent to turboprops. Change
controls.mixtureAxis()
to
controls.mixtureAxis(); props.setAll("controls/engines/engine", "condition", getprop("controls/engines/engine/mixture"))
This is an alternative method to adjust a property
<repeatable>true</repeatable> <binding> <command>property-adjust</command> <property>/sim/current-view/goal-pitch-offset-deg</property> <step type="double">-2.0</step> </binding>
This will continuously reduce /sim/current-view/goal-pitch-offset-deg in steps of 2 while the button is held in.
Advanced Programming
if ... then ... else
Sometimes you only want to do something if a certain condition is true (or false.) In this case you use an if clause. The general form in most programming languages is
- if (condition) then action endif
In Nasal, we write "then" as "{" and "endif" as "}". Each "action" (or statement) is terminated by a semicolon:
if (condition)
{ # the opening curly brace means THEN
action;
} # the closing curly brace means ENDIF, i.e. end of this block
Actions can be arbitrary Nasal expressions, including function calls and other conditional blocks.
An example
Lets say that if x is less than 5 we want to add 2 to it. We write:
if (x < 5) { x = x + 2; }
The more readable way of writing it is
if (x < 5) {
x = x + 2;
}
And we can even omit the braces if there's only one action:
if (x < 5) # no opening brace...
x = x + 2; # ...so the if statement ends at the first semicolon
If we also want to add 1 to y if the condition is true, we write
if (x < 5) {
x = x + 2;
y = y + 1;
}
A common way to abbreivate such simple expression, is using shorthand operators like:
- +=
- -=
- *=
- /=
For example:
if (x < 5) {
x += 2; # means x = x + 2
y += 1; # means y = y + 1
}
Now lets pretend that we still want to increase x by 2 if it less than 5, and if it is 5 or more we want to add 1 to it. We use else.
It looks like this
if (x < 5) {
x = x + 2;
} else {
x = x + 1;
}
Now lets say we have the following rules:
- If x < 5 add 2 to it
- If x < 10 add 1 to it
- If x is 10 or more add 7 to it.
In English our code would be
- If (x < 5) then (x = x + 2) else if (x < 10) then (x = x + 1).
- If none of these are true then (x = x + 7).
- End of if clause.
We test for x < 5 first. If it fails we test for x < 10. If x , 5 succeeds we do (x = x + 2) and then move on past the end of if clause. So we can test for x < 5 and x < 10 in this way.
Writing our code properly we get
if (x < 5) {
x = x + 2;
} elsif (x < 10) {
x = x + 1;
} else {
x = x + 7;
}
You can use more than one elsif, but it gets messy. If you have a number of tests then it might be better to test for each case individually. Easier to understand each condition separately and much less chance of an error. Our above example would become
if (x < 5) {
x = x + 2;
}
if (x >= 5) and (x < 10) {
x = x + 1;
}
if (x >= 10) {
x = x + 7;
}
Because each if is tested we need to have the (x >= 5) otherwise if x = 1 both the first and second if conditions would be satisfied.
But the advantage is that we are forced to write out each condition exactly as it should be tested. Easier to understand and easier to maintain.
Another example
Lets say that you want something to happen only if the gear is up. But the only property you can read is GearDown. Then you write
if (!GearDown) {
action;
}
The ! means not. So it translates as: If the gear is not down perform action.
See the discussion of variables below for an example of the use of if.
getProp, setProp and setAll
To use a property you need its value. The best way to find out the path to a property is with Debug - Browse Internal Proprties.
Let us say we want to the current value of elevator-trim. From the menu select Debug and then Browse Internal Properties. Use the scroll-bar to go up and down the list and look at the options. One of them is controls. This is a good place to start, the elevator-trim is a control. Click on controls and inspect the new list of options.
One of them is flight. Good choice, the elevator-trim controls how the aircraft flies. Click on flight. Look at the list of variables displayed.
You will see elevator-trim = '0' (double) (It might not say 0.) The (double) means that it is a double-precision floating point number. This means that it can change in small amounts and has a decimal point with numbers after the decimal point. Make a note that it is a floating point number.
Look at the top of the dialog-box and you will see that it says /controls/flight. We can see that the variable is elevator-trim, so make a note that the path to the variable is /controls/flight/elevator-trim.
You can close the dialog-box.
To get the current value of elevator-trim in your xml file you will use
getProp("/controls/flight/elevator-trim")
Having got the current value we probably want to change it. To do that we use setProp. Lets pretend that the current value is 0.175 and we want to make it 0.200. We use
setProp("/controls/flight/elevator-trim", 0.200)
Elevator trim is a double (floating point) number. The range of values will be -1.000 through 0 to +1.000.
If the value is an integer (int) the range of values is ... -3, -2, -1, 0, 1, +2, +3 ... This doesn't mean that -3 is a suitable value for that specific property, it just means that you can pass -3 as a value without crashing the system.
Sometimes the value is a boolen (bool). This means that it is either true or false. Then you would use
setProp("path-to-property", "true") setProp("path-to-property", "false")
You can also use 1 instead of "true" and 0 instead of "false". If you use getProp, you will get 0 and 1 as your value, not "true" and "false."
In multi-engined aircraft you can use throttleAxis() when you are adjusting the throttle with an axis. This will do all engines. But if you are using a button you need to set the throttle for all engines at the same time. To do this you need props.setAll(). It is a waste of time to try to take the throttles past maximum, so you first get the throttle setting, check if it is less than maximum, and if it is, increase it a bit. You end up with
<![CDATA[ if(getprop("controls/engines/engine/throttle") < 1 ) { props.setAll("controls/engines/engine", "throttle", getprop("controls/engines/engine/mixture") + 0.001) } ]]>
You need CDATA because of the less-than sign.
You can use this for throttle, propeller, mixture and condition.
BTW Here the props in props.setAll is short for property, not propellers.
gui.popupTip
When you zoom in or out, a little window pops up giving you the field of view. You can do this with any adjustment you make.
gui.popupTip(sprintf(String-to-display),Time-to-display-in-seconds)
If you are happy with a 1 second display, you can leave out the Time-to-display (and the comma before it.
The String-to-display can get hairy. To start off, choose one of your buttons (not the hat.) Let's say it is button number 3. Just before </script> add this line:
gui.popupTip(sprintf("Button 3 pressed"),3)
Don't forget to add a ; at the end of the previous line.
Start (or restart) FG and press that button. You will see the message "Button 3 pressed" for 3 seconds.
Delete the line and the ; from your xml file.
This was not very useful in itself. What we normally want to display is some property. A good example is when adjusting a trim with a button - you have no idea where you are. You can use the HUD, but when you are transitioning to straight and level flight, it is much easier to remember a number from the last time, than the position of the trim on the HUD.
We are going to display the value of elevator-trim. See getProp, setProp and setAll above to learn how to get the current value.
To get the value of elevator-trim in your xml file you will use
getProp("/controls/flight/elevator-trim")
Now we want to display it. For a start we will have "Elevator-trim: " followed by the value that we got. But it is a number, so we need to tell sprintf that there is a number coming, so we change our string to "Elevator-trim: %". We still need to tell sprintf that it is a floating point number, so we use "Elevator-trim: %f". But we would like it accurate to 3 decimal places, so we use "Elevator-trim" %.3f". Now we will have "Elevator-trim: %.3f", getProp("/controls/flight/elevator-trim").
The line of code ends up as
gui.popupTip(sprintf("Elevator-trim: %.3f", getprop("/controls/flight/elevator-trim")))
Add it to both trim up and trim down, not forgetting the ; at the end of the previous line. Then play a bit, you'll like it.
Add the line to aileron-trim and rudder-trim (if you have it), not forgetting to change all occurrences of elevator to aileron and rudder.
We can make the display for elevator-trim a bit better. The elevator-trim control is usually a largish wheel that you turn. In the C172 it has a range of 9 turns. -4.5 to +4.5. Let's adjust our code to be similar. Our getProp returns -1.0 to +1.0, so if we multiply it by 4.5 we will get -4.5 to + 4.5. We will only need 2 decimal places, so the code becomes
gui.popupTip(sprintf("Elevator-trim: %.2f", 4.5 * getprop("/controls/flight/elevator-trim")))
If you get a property which is on or off, you can use this to display it
var i = getprop("controls/engines/engine/reverser"); gui.popupTip("Thrust Reverser " ~ ["OFF", "ON"][i]);
If the message is the wrong way round you must swap the "OFF" and "ON" around.
Interpolate
Sometimes we need a property's value to change over a set period of time. For this we use interpolate. It takes the form
interpolate(property-to-adjust, target-value, time-to-get-there-in-seconds)
An example of its use would be in using a non-repeatable button to adjust aileron-trim. You could make repetitive pushes of the button, but that is not satisfactory. Much easier to use interpolate. You want the aileron trim to go from center to max (1.0) in 15 seconds. the code is
interpolate("controls/flight/aileron-trim", 1, 15)
You give the button a tap and in 15 seconds time the aileron-trim is at 1.0.
But that was starting at 0. If the trim is at 0.5 when you start, you want it to get to max in half the time, 7.5 seconds. The trick is to get the current aileron-trim, subtract it from the max, thus giving you the change still needed, and use that to factor the time-to-get-there. This gives you
interpolate("controls/flight/aileron-trim", 1, (1 - getProp("controls/flight/aileron-trim") * 15)
All good and well, but if we are really using a non-repeatable button to adjust aileron-trim, we want to be able to stop the adjustment
when it gets to a suitable value. The secret is to use <mod-up> to stop the action. To stop the action we use interpolate again,
but this time we set the target to the current value, and the time-to-get-there as 0.
<mod-up> interpolate("controls/flight/aileron-trim", getProp("controls/flight/aileron-trim"), 0) </mod-up>
Now, when the button is pressed, interpolation starts. Just once, because it is a non-repeatable button. When we are happy with the aileron-trim we release the button and interpolation stops.
Nice.
Variables
Lets say we want our buttons to behave in different ways, depending on whether the aircraft is on the ground, aircraft is in the air, aircraft is an helicopter. To do this we have a variable called mode, which has 3 values
mode = 1 Aircraft on ground mode = 2 Aircraft in air mode = 3 Aircraft is an helicopter
Now, each time a button is pressed, we get the value of mode and use if to decide which action the button must carry out.
Of course, we need some way to set the value of mode, say one of the buttons. Non-repeatable. Each time the button is clicked mode goes up 1 in value. If it is 3, then clicking takes it round to 1 again.
There are two ways of defining variables.
Method 1
<data> <mode type="int">1</mode> # Initialise mode to 1 </data> <nasal> <script> <![CDATA[ var self = cmdarg().getParent(); var data = self.getNode("data"); var mode = data.getNode("mode"); get_mode = func { mode.getValue(); } ]]> </script> </nasal>
This goes right at the top of your xml file, just after </name>.
The code for the button (non-repeatable)that changes mode looks like this
var m = get_mode(); m = m + 1; if (m == 4) { m = 1 } mode.setIntValue(m)
When a button is pressed, the code looks like this
var m = get_mode(); if (m == 1) { ground-action } if (m == 2) { air-action } if (m == 3) { helicopter-action }
Advantages: Mode is defined within the xml file.
Disadvantages: Complicated code needed to declare the variable.
Method 2
Define your mode variable with --prop:mode=1 in the command line.
You also need var mode just after </name> at the top of the xml file.
The activator for changing mode has this code
mode = getprop("mode"); mode = mode +1; if (mode == 4) { mode = 1 } setProp("mode", mode)
Then the button pressed code looks like this
mode = getprop("mode"); if (m == 1) { ground-action } if (m == 2) { air-action } if (m == 3) { helicopter-action }
Advantages:
Variable is easy to create.
Disadvantages: You have to have at --prop: in the command line. You can't type --prop: in the comments section at the top of your file, because of the consecutive minus signs. You have to type minusminusprop: and say that "minus" must be substituted with "-"
Whichever method you use, choose a better name than mode for your variable. It is too common a word and may well one day
be used somewhere in the system files. In the xml file for the Saitek Yoke, where I use a variable to control the assignments
of the levers on the quadrant, I call the variable SaitekMultiMode. Unlikely to be used by the FG programmers, or anybody else.
perIndexAxisHandler
This only works with real axes.
You have a quadrant with 3 levers. Normally they are assigned to controls.throttleAxis(), controls.PropellerAxis() and controls.mixtureAxis(). Now let's say you are flying an aircraft with 2 propeller-engines, it has mixture control, but not propeller-feathering (or you are prepared to use buttons for this). Now you want the first 2 levers to control the throttles of each engine independently,and the last to do the mixture of both. In this case you would leave lever 3 assigned to controls.mixtureAxis(), but would use perIndexAxisHandler for the first 2.
It works like this: controls.perIndexAxisHandler(function, engine-number).
The values for function are
0 Throttle 1 Mixture 2 Propeller
Engine-number goes from 0 to 9.
So in our example, lever 1 would be assigned
controls.perIndexAxisHandler(0,0)
and lever 2
controls.perIndexAxisHandler(0,1)
See this post for an example of using this.
You can assign a n axis to more than one engine. Let's say you have 4 engines. You only have 3 levers on the quadrant. You want the
first lever to do the port-outer engine throttle , lever 3 to do the starboard-outer engine throttle, and the middle lever to do the two inboard engine throttles.
So you need to assign the middle lever a list of engines.
Remembering that engines are numbered from 0 upwards for furthest left towards the right, the lever assignments from left to right are
controls.perIndexAxisHandler(0,0) controls.perIndexAxisHandler(0,[1, 2]) controls.perIndexAxisHandler(0,3)
Notice the list of engine numbers between [ and ] for the middle lever.
Any complaints/suggestions/questions/kudos can be posted here.