Accurate control of button repeat rate

From FlightGear wiki
Jump to navigation Jump to search
WIP.png Work in progress
This article or section will be worked on in the upcoming hours or days.
See history for the latest developments.


Sometimes you might want to have control over the speed that a repeatable button works - maybe you want it to repeat exactly every 1.325 seconds, or you just want to slow it down. At the end of this article you will be shown how to use a button to set the frequency of a radio. This is certainly a time when the default (system) repeat rate of a button is too fast.

How it is done

The secret is in having a method of preventing a button from repeating until our chosen time has elapsed. It is possible, but messy, especially if you are doing this with a number of buttons, to do it inside the code for each button. It is much better to create a new type of button code. For this we need a class, and then create instances of this class for each controlled button. This, incidentally, is OOP, or Object Oriented Programming.

Here is thew code to create the new class. It is called timedControl. I have added (unnecessary) open lines between each member of the class to make understanding easier.

var timedControl = {
      last_busy: 0,

      timeout_dur: 1.0,

      new: func() {
          return { parents:[me] };

      _call: func(fn, arg) {
        if (!arg) { me.last_busy = 0; return; }
        var time = systime();
        if (time < me.timeout_dur + me.last_busy) return;
        me.last_busy = time;
        return call(fn, [arg], me);

Let's assume that we have a function, myFunction, which must only repeat every 0.25 seconds. We create it with

var myFunction =;

While creating myFunction, it sets last_busy to 0 (line 2) and time_dur to 1.0 (line 4).

and set the repeat rate to 0.25 seconds with

myFunction.timeout_dur = 0.25;

How it works

var myFunction = calls lines 6, 7, 8 of timedControl. This creates a new instance of timedControl named myFunction and returns a pointer to it. Now, whenever you use myControl it will point to where it exists in memory.

myFunction.timeout_dur = 0.25. myFunction points to the function in memory, .timout_dur then points to where timeout_dur exists in memory and changes its value to 0.25. Line 4 of the code set it to 1.0, just so it has a value.

Now we need something for the joystick button to call. Here we are using do, but you could just as easily use perform, or activate, or anything else. =  func(step) me._call(me._do, step);

Here, step is 1 for button pressed, and 0 for mod-up, pretty much standard for repeatable button code.

When is called it executes me._call(me._do, step). It calls lines 10 onwards of timedControl, passing me._do and step as parameters. The me is the way timedControl references itself, so if you have multiple buttons using the method, using me makes sure that you are always using the right timeout_dur or last_busy, etc.

Now let's look at lines 10 onwards of timedControl.

Line 10 receives the call _call with the parameters me._do and step, which it renames to fn and arg.

Line 11: If the value of arg, and hence the value of step is 0, it means mod-up (the button was released.) It sets the value of last_busy to 0 and returns. It does nothing further.

Line 12: Sets the variable time to systime(). This is the current date and time, accurate to fractions of a millisecond.

Line 13: Remember, last_busy was initialised to 0. This line checks if the sum of timeout_dur and last_busy is more than the current time. It isn't, since last_busy is 0.

Line 14: last_busy is set to current time.

Line 15: The function returns, calling fn with parameter arg.

Now fn is me._do. me is of course myFunction. So it is going to call a function by the name of myFunction._do. This is the actual code that must be executed when the joystick button is pressed. So we are going to need

myFunction._do =  func(step) {

So your code gets executed. The button is still pressed. A little while later (based on system repeat rate) the button is called again and me._call is called again.

Line 12: Updates the value of time. It is now 2 milliseconds later.

Line 13: last_busy was set to the current time last time through the call. Now time is less than last_busy + 0.25, so the call returns. Your code is not executed.

This will continue until 0.25 seconds has elapsed. Then line 15 will not return, and your code will be executed. So, as long as the button is pressed, your code is repeated every 0.25 seconds.

Now you release the button. mod-up passes 0 as step. Now the important line is 11. arg is 0, so last_busy is reset to 0, and the call returns. If you press the button again, the whole cycle restarts again because last_busy is 0.

The joystick end of things

All this code is of course in a Nasal file which you create. Give it a suitable name, based on your joystick name, for example logitechlib.nas. Put it in the same folder as your joystick xml file.

Then add this at the top of your xml file, just after all the <name> entries

        if (!contains(globals, "logitechlib")) {
          io.load_nasal(getprop("/sim/fg-root") ~ "/Input/Joysticks/Saitek/logitechlib.nas");

Make all the affected buttons repeatable. Then use this for the bindings


Making the declarations tidier

Currently, you need to do this

myFunction =;
myFunction.timeout_dur = 0.25; = func(step) me._call(me._do, step);
myFunction._do = func(step) {

It looks a bit neater if you have a helper function.

var createTimedControl = func(timeout, do) {
      var name =;
      name.timeout_dur = timeout; = func(step) { name._call(do, step); };
      append(arrayTimedControls, name);
      return name;

Then you use

myFunction = createTimedControl(0.25, var do = func(step) {
# Your code goes here

A practical example

We are going to use buttons to adjust the frequency of the NAV1 radio, up and down. We are going to have 3 speeds - adjust the decimals, adjust the units and adjust the tens. To do that, the joystick button passes +ve or -ve 1, or 2 for step. Mod-up is still 0, of course. It also pops the value up on the screen while adjusting. To implement all 3 steps rates, up and down, you will need (probably with the help of modifiers) 6 buttons.

# 1 for decimals, 2 for integers
# +ve for up, -ve for down
var NAV1Freq = createTimedControl(0.25, var do = func(step) {
  var val = 0.05 * step;
  if (math.abs(step) == 2) val = val * 20;
  var curr = getprop("/instrumentation/nav/frequencies/standby-mhz");
  setprop("/instrumentation/nav/frequencies/standby-mhz", curr + val);
  curr = getprop("/instrumentation/nav/frequencies/standby-mhz");
  if (curr > 117.95) {
    curr = 108.00;
    setprop("/instrumentation/nav/frequencies/standby-mhz", curr)
  if (curr < 108.00) {
    curr = 117.95;
    setprop("/instrumentation/nav/frequencies/standby-mhz", curr)
  gui.popupTip(sprintf("NAV1 %0.2f",curr));