Added first two Lua script conversions; autopilot airspeed and ATC visibility

Updated DelphiLua
Added scripting documentation
This commit is contained in:
Mark van Renswoude 2017-06-04 11:18:48 +02:00
parent 6dbea6f211
commit 897ba9310e
16 changed files with 905 additions and 155 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ __history
*.exe
*.identcache
Docs/_build/
*.sublime-workspace

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -6,4 +6,5 @@ G940LEDControl documentation
:caption: Contents:
introduction
scripting
scripting
scriptingreference

View File

@ -3,7 +3,7 @@ Introduction
G940LEDControl allows you to bind functions to the throttle's buttons which control the LEDs. Each function may contains several states, each of which can be configured for a certain colour and/of flashing pattern.
Since version 2.0 all functions are implemented using Lua scripts. This means it is fairly easy to create customized versions of the standard functions, or even add a completely new function. For more information, see the page on :doc:`scripting`.
Since version 2.0 all functions are implemented using Lua scripts. This means it is fairly easy to create customized versions of the standard functions, or even add a completely new function. For more information, see the page on :doc:`scripting` or dive straight into the :doc:`scriptingreference`.
Source code
-----------
@ -12,8 +12,11 @@ Since version 2.0, G940LEDControl is released as open-source under the GNU Gener
G940LEDControl is compiled using Delphi XE2. The following additional libraries are required:
* `OmniThreadLibrary <http://www.omnithreadlibrary.com/>`_ (1.0.4)
* `VirtualTreeView <http://www.jam-software.com/virtual-treeview/>`_ (5.3.0)
* `X2Log <https://git.x2software.net/delphi/x2log>`_
* `X2Utils <https://git.x2software.net/delphi/x2utils>`_
* ToDo: any more?
A copy of `DelphiLua <https://git.x2software.net/delphi/delphilua>`_ is already included in the G940LEDControl repository.
Newer versions Delphi and/or of the libraries might work as well, though have not been tested yet.
A copy of `DelphiLua <https://git.x2software.net/delphi/delphilua>`_ is included in the G940LEDControl repository.

View File

@ -1,7 +1,117 @@
Scripting
=========
All functionality introduced by G940LEDControl is described in the :doc:`scriptingreference`. For more information about Lua in general, please refer to the `Lua 5.2 Reference Manual <https://www.lua.org/manual/5.2/>`_.
This guide will walk through how Lua scripting works in G940LEDControl. To avoid confusion when talking about functions in this context they will be referred to as 'button functions' and 'Lua functions'.
Script locations
----------------
The default scripts included with G940LEDControl can be found in the folder selected during installation, for example "C:\\Program Files (x86)\\G940 LED Control". In there you will find a Scripts\\FSX folder containing the Lua files.
In addition scripts are loaded from your user data path. This folder is automatically created by G940LEDControl after you have changed any button configuration or settings. To open the folder, type or paste "%APPDATA%\\G940LEDControl" into a Windows Explorer address bar and press Enter. Inside you can create the same Scripts\\FSX folder structure. Scripts in this folder will not be overwritten when installing a new version.
Anatomy of a button function
----------------------------
Let's take the Autopilot airspeed button function as an example, which at the time of writing looks like this:
::
-- this could be a great Lua example!
local strings = require './lib/strings'
RegisterFunction(
{
uid = 'autoPilotAirspeed',
category = strings.Category.FSX.AutoPilot,
displayName = 'Autopilot airspeed',
states = {
on = { displayName = 'On', default = LEDColor.Green },
off = { displayName = 'Off', default = LEDColor.Red },
notAvailable = { displayName = 'Not available', default = LEDColor.Off }
}
},
function(context)
SetState(context, 'notAvailable')
OnSimConnect(context,
{
autoPilotAvailable = { variable = 'AUTOPILOT AVAILABLE', type = SimConnectDataType.Bool },
autoPilotAirspeed = { variable = 'AUTOPILOT AIRSPEED HOLD', type = SimConnectDataType.Bool }
},
function(context, data)
if data.autoPilotAvailable then
if data.autoPilotAirspeed then
SetState(context, 'on')
else
SetState(context, 'off')
end
else
SetState(context, 'notAvailable')
end
end)
end
)
Using anonymous functions like this to implement the various callbacks results in compact code, which can arguably be more difficult to read. For clarity let's expand it first. The following example works exactly the same:
::
local strings = require './lib/strings'
local function variablesChanged(context, data)
if data.autoPilotAvailable then
if data.autoPilotAirspeed then
SetState(context, 'on')
else
SetState(context, 'off')
end
else
SetState(context, 'notAvailable')
end
end
local function setup(context)
SetState(context, 'notAvailable')
OnSimConnect(context,
{
autoPilotAvailable = { variable = 'AUTOPILOT AVAILABLE', type = SimConnectDataType.Bool },
autoPilotAirspeed = { variable = 'AUTOPILOT AIRSPEED HOLD', type = SimConnectDataType.Bool }
},
variablesChanged)
end
RegisterFunction(
{
uid = 'autoPilotAirspeed',
category = strings.Category.FSX.AutoPilot,
displayName = 'Autopilot airspeed',
states = {
on = { displayName = 'On', default = LEDColor.Green },
off = { displayName = 'Off', default = LEDColor.Red },
notAvailable = { displayName = 'Not available', default = LEDColor.Off }
}
},
setup)
So what's happening? When the script is loaded it is automatically run. At this time you should call :ref:`ref-registerfunction` for each button function you want to be visible in G940LEDControl. :ref:`ref-registerfunction` accepts two parameters: a table which describes the button function you want to add, and a Lua function to be called when the button function is assigned to a button.
Every button function must have a unique 'uid'. It is used to save and load profiles and should therefore not be changed once in use. The category and displayName are only used in the main and button function selection screens and can be freely changed.
A button function must also have one or more states. Each state has a key which, like the 'uid', is used to save and load profiles and should also not be changed once in use. A default :ref:`LED color <ref-ledcolor>` can also be set, which sets the initial value in the selection screen when assigning it to a button:
.. image:: autoPilotAirspeedStates.png
Setup function
~~~~~~~~~~~~~~
As soon as the button function is attached to one of the buttons the setup function passed to :ref:`ref-registerfunction` is called. It receives a 'context' parameter, the contents of which are not useable by the script directly, but which you need to pass along to for example :ref:`ref-setstate` later on so that it knows which button function's state needs to be changed.
In the above example the first thing we do is to set the default state using :ref:`ref-setstate`. The second parameter is a string containing the key of one of the states as defined in the :ref:`ref-registerfunction` call.
After that you will normally call one of the built-in Lua functions to be notified of certain events. At the time of writing you can either call :ref:`ref-ontimer` to perform checks on a regular interval or, more common in the case of FSX, :ref:`ref-onsimconnect` to be notified when one or more of the `simulation variables <https://msdn.microsoft.com/en-us/library/cc526981.aspx>`_ change.

323
Docs/scriptingreference.rst Normal file
View File

@ -0,0 +1,323 @@
Scripting reference
===================
G940LEDControl uses Lua 5.2. Please refer to the `Lua 5.2 Reference Manual <https://www.lua.org/manual/5.2/>`_ for more information.
.. contents::
:local:
Global functions
----------------
.. _ref-log:
Log functions
~~~~~~~~~~~~~
::
Log.Verbose(msg, ...)
Log.Info(msg, ...)
Log.Warning(msg, ...)
Log.Error(msg, ...)
Writes a message to the application log. If you pass more than one parameter, they will be concatenated in the log message separated by spaces. The parameters do not need to be strings, other simple types will be converted and tables will be output as '{ key = value, ... }'.
The application log can be opened on the Configuration tab of the main screen.
.. _ref-registerfunction:
RegisterFunction
~~~~~~~~~~~~~~~~
::
RegisterFunction(info, setupCallback)
Registers a button function.
**Parameters**
| **info**: table
| A Lua table describing the function. The following keys are recognized:
|
| **uid**: string
| *Required.* A unique ID for this function. Used to save and load profiles.
|
| **category**: string
| The category under which this function is grouped in the button function selection screen.
|
| **displayName**: string
| The name of the function which is shown in the main screen and button function selection screen.
|
| **states**: table
| A table of states supported by this function.
| Each state has it's own unique key and a table describing the state. The following keys are recognized for the table:
|
| **displayName**: string
| The name of the state which is shown in the button function selection screen.
|
| **default**: string
| The default color and/or animation assigned to the state when it is first selected. See :ref:`ref-ledcolor` for a list of valid values.
|
| **setupCallback**: function
| A Lua function which is called when the button function is configured. Please note that if a button function is linked to multiple G940 throttle buttons, setupCallback is called multiple times, so be careful with variables which are outside of the setupCallback's scope (global or script-local)!
|
| setupCallback is passed a single parameter 'context'.
|
**Example**
::
RegisterFunction(
{
uid = 'autoPilotAirspeed',
category = strings.Category.FSX.AutoPilot,
displayName = 'Autopilot airspeed',
states = {
on = { displayName = 'On', default = LEDColor.Green },
off = { displayName = 'Off', default = LEDColor.Red },
notAvailable = { displayName = 'Not available', default = LEDColor.Off }
}
},
function(context)
-- implementation of setupCallback
end)
.. _ref-setstate:
SetState
~~~~~~~~
::
SetState(context, newState)
Sets the current state of a button function.
**Parameters**
| **context**
| The context parameter as passed to setupCallback which determines the button function to be updated.
|
| **newState**: string
| The new state. Must be the name of a state key as passed to :ref:`ref-registerfunction`.
|
**Example**
::
SetState(context, 'on')
.. _ref-onsimconnect:
OnSimConnect
~~~~~~~~~~~~
::
OnSimConnect(context, variables, variablesChangedCallback)
Registers a Lua function to be called when the specified SimConnect variable(s) change. For a list of variables please refer to `Simulation variables <https://msdn.microsoft.com/en-us/library/cc526981.aspx>`_.
**Parameters**
| **context**
| The context parameter as passed to setupCallback.
|
| **variables**: table
| A table containing information about the simulation variables you want to monitor. Each key will be reflected in the 'data' table passed to the variablesChangedCallback. Each value is a Lua table describing the variable.
|
| **variable**: string
| The name of the variable as described in `Simulation variables <https://msdn.microsoft.com/en-us/library/cc526981.aspx>`_.
|
| **data**: string
| One of the :ref:`ref-simconnectdatatype` values.
|
| **variablesChangedCallback**: function
| A Lua function which is called when the variable's value changes. It receives 2 parameters: 'context' and 'data'. The data parameter is a Lua table where each key corresponds to a variable defined in the 'variables' parameter and it's value is the current value of the simulation variable.
|
**Example**
::
OnSimConnect(context,
{
autoPilotAvailable = { variable = 'AUTOPILOT AVAILABLE', type = SimConnectDataType.Bool },
autoPilotAirspeed = { variable = 'AUTOPILOT AIRSPEED HOLD', type = SimConnectDataType.Bool }
},
function(context, data)
if data.autoPilotAvailable then
if data.autoPilotAirspeed then
SetState(context, 'on')
else
SetState(context, 'off')
end
else
SetState(context, 'notAvailable')
end
end)
.. _ref-ontimer:
OnTimer
~~~~~~~
::
OnTimer(context, interval, timerCallback)
Registers a Lua function to be called when the specified interval elapses.
**Parameters**
| **context**
| The context parameter as passed to setupCallback.
|
| **interval**
| The interval between calls to timerCallback in milliseconds. At the time of writing the minimum value is 100 milliseconds.
|
| **timerCallback**
| A Lua function which is called when the interval elapses. It is passed a single parameter 'context'.
|
**Example**
::
OnTimer(context, 1000,
function(context)
if FSXWindowVisible('ATC Menu') then
SetState(context, 'visible')
else
SetState(context, 'hidden')
end
end)
.. _ref-windowvisible:
WindowVisible
~~~~~~~~~~~~~
Checks if a window is currently visible. This is a thin wrapper around the FindWindow/FindWindowEx/IsWindowVisible Windows API. In the context of FSX panels you are probably looking for :ref:`ref-fsxwindowvisible`.
All parameters are optional, but at least one parameter is required. To skip a parameter simply pass nil instead.
To get a window's class name, use a tool like `Greatis Windowse <https://www.greatis.com/delphicb/windowse/>`_.
**Parameters**
| **className**
| The window class name of the window
|
| **title**
| The title / caption / text of the window
|
| **parentClassName**
| The parent window's class name. If specified, the first two parameters are considered to be a child window of this parent.
|
| **parentTitle**
| The parent window's title / caption / text. If specified, the first two parameters are considered to be a child window of this parent.
|
.. _ref-fsxwindowvisible:
FSXWindowVisible
~~~~~~~~~~~~~~~~
Checks if an FSX window is currently visible. Uses WindowVisible as a workaround because SimConnect does not expose this information directly.
**Parameters**
| **title**
| The title of the panel.
|
Checks for both docked and undocked windows. Equal to:
::
WindowVisible('FS98CHILD', title, 'FS98MAIN') or WindowVisible('FS98FLOAT', title)
Global variables
----------------
.. _ref-ledcolor:
LEDColor
~~~~~~~~
**Keys**
- Off
- Green
- Amber
- Red
- FlashingGreenFast
- FlashingGreenNormal
- FlashingAmberFast
- FlashingAmberNormal
- FlashingRedFast
- FlashingRedNormal
The 'Fast' flashing versions stay on and off for half a second, the 'Normal' version for 1 second.
**Example**
::
{ default = LEDColor.Green }
.. _ref-simconnectdatatype:
SimConnectDataType
~~~~~~~~~~~~~~~~~~
**Keys**
- Float64
- Float32
- Int64
- Int32
- String
- Bool
- XYZ
- LatLonAlt
- Waypoint
The XYZ, LatLonAlt and Waypoint data types will return a table in the 'data' parameter for the OnSimConnect callback with the following keys:
**XYZ**
- X
- Y
- Z
**LatLonAlt**
- Latitude
- Longitude
- Altitude
**Waypoint**
- Latitude
- Longitude
- Altitude
- KtsSpeed
- PercentThrottle
- Flags
The Flags value is a table containing the following keys, where each is a boolean:
- SpeedRequested
- ThrottleRequested
- ComputeVerticalSpeed
- IsAGL
- OnGround
- Reverse
- WrapToFirst

View File

@ -340,7 +340,7 @@ type
isvararg: Byte;
istailcall: Byte;
short_src: array[0..LUA_IDSIZE - 1] of AnsiChar;
//struct CallInfo *i_ci; /* active function */
i_ci: Integer;
end;
{ Functions to be called by the debugger in specific events }

View File

@ -398,6 +398,13 @@ type
procedure OpenLibraries(ALibraries: TLuaLibraries); virtual;
{ Get or set the current path(s) used for require calls. Paths must be separated
by semicolons and questions marks will be replaced with the requested file name,
as per the Lua documentation at: https://www.lua.org/pil/8.1.html }
function GetRequirePath: string;
procedure SetRequirePath(const APath: string);
procedure AddRequirePath(const APath: string);
{ These methods should only be called right after one of the
LoadFrom methods, which must have AutoRun set to False. }
procedure Run; virtual;
@ -1400,6 +1407,51 @@ begin
end;
function TLua.GetRequirePath: string;
begin
CheckState;
lua_getglobal(State, 'package');
try
lua_getfield(State, -1, 'path');
try
Result := TLuaHelpers.LuaToString(State, -1);
finally
lua_pop(State, 1);
end;
finally
lua_pop(State, 1);
end;
end;
procedure TLua.SetRequirePath(const APath: string);
begin
CheckState;
lua_getglobal(State, 'package');
try
TLuaHelpers.PushString(State, APath);
lua_setfield(State, -2, 'path');
finally
lua_pop(State, 1);
end;
end;
procedure TLua.AddRequirePath(const APath: string);
var
path: string;
begin
path := GetRequirePath;
if (Length(path) > 0) and (path[Length(path)] <> ';') then
path := path + ';';
SetRequirePath(path + APath);
end;
procedure TLua.Run;
begin
CheckIsFunction;

View File

@ -1,7 +1,7 @@
object MainForm: TMainForm
Left = 0
Top = 0
ActiveControl = cmbProfiles
ActiveControl = cbTrayIcon
BorderIcons = [biSystemMenu, biMinimize]
BorderStyle = bsSingle
Caption = 'G940 LED Control'
@ -30,7 +30,7 @@ object MainForm: TMainForm
Margins.Top = 8
Margins.Right = 8
Margins.Bottom = 8
ActivePage = tsButtons
ActivePage = tsConfiguration
Align = alClient
TabOrder = 0
object tsButtons: TTabSheet
@ -375,10 +375,6 @@ object MainForm: TMainForm
object tsConfiguration: TTabSheet
Caption = ' Configuration '
ImageIndex = 2
ExplicitLeft = 0
ExplicitTop = 0
ExplicitWidth = 0
ExplicitHeight = 0
DesignSize = (
442
452)

View File

@ -228,6 +228,7 @@ type
implementation
uses
System.IOUtils,
System.SysUtils,
System.Types,
System.Win.ComObj,
@ -334,6 +335,12 @@ begin
scriptPaths[0] := App.Path + FSXScriptsPath;
scriptPaths[1] := App.UserPath + UserDataPath + FSXScriptsPath;
if DebugHook <> 0 then
begin
SetLength(scriptPaths, 3);
scriptPaths[2] := TPath.GetFullPath(App.Path + '..\' + FSXScriptsPath);
end;
FunctionRegistry.Register(TFSXLEDFunctionProvider.Create(scriptPaths));

View File

@ -51,7 +51,7 @@
<PropertyGroup Condition="'$(Base)'!=''">
<VerInfo_Release>6</VerInfo_Release>
<VerInfo_MinorVer>1</VerInfo_MinorVer>
<DCC_UsePackage>rtl;dbrtl;$(DCC_UsePackage)</DCC_UsePackage>
<DCC_UsePackage>rtl;dbrtl;xmlrtl;$(DCC_UsePackage)</DCC_UsePackage>
<DCC_DcuOutput>Lib</DCC_DcuOutput>
<DCC_ExeOutput>Bin</DCC_ExeOutput>
<DCC_UnitSearchPath>..\Shared;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
@ -63,10 +63,12 @@
<VerInfo_Locale>1033</VerInfo_Locale>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win64)'!=''">
<DCC_UsePackage>vclx;vcl;vclactnband;$(DCC_UsePackage)</DCC_UsePackage>
<Icon_MainIcon>G940LEDControl_Icon.ico</Icon_MainIcon>
<Manifest_File>$(BDS)\bin\default_app.manifest</Manifest_File>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win32)'!=''">
<DCC_UsePackage>vclx;vcl;vclactnband;$(DCC_UsePackage)</DCC_UsePackage>
<DCC_PACKAGE_NO_LINK>false</DCC_PACKAGE_NO_LINK>
<DCC_UNIT_PLATFORM>false</DCC_UNIT_PLATFORM>
<DCC_SYMBOL_PLATFORM>false</DCC_SYMBOL_PLATFORM>

View File

@ -0,0 +1,35 @@
local strings = require './lib/strings'
RegisterFunction(
{
uid = 'autoPilotAirspeed',
category = strings.Category.FSX.AutoPilot,
displayName = 'Autopilot airspeed',
states = {
on = { displayName = 'On', default = LEDColor.Green },
off = { displayName = 'Off', default = LEDColor.Red },
notAvailable = { displayName = 'Not available', default = LEDColor.Off }
}
},
function(context)
SetState(context, 'notAvailable')
OnSimConnect(context,
{
autoPilotAvailable = { variable = 'AUTOPILOT AVAILABLE', type = SimConnectDataType.Bool },
autoPilotAirspeed = { variable = 'AUTOPILOT AIRSPEED HOLD', type = SimConnectDataType.Bool }
},
function(context, data)
if data.autoPilotAvailable then
if data.autoPilotAirspeed then
SetState(context, 'on')
else
SetState(context, 'off')
end
else
SetState(context, 'notAvailable')
end
end)
end
)

View File

@ -0,0 +1,16 @@
local strings = {}
strings.Category = {}
strings.Category.FSX = {}
strings.Category.FSX.Default = 'Flight Simulator X'
strings.Category.FSX.AutoPilot = strings.Category.FSX.Default..' - Autopilot'
strings.Category.FSX.ControlSurfaces = strings.Category.FSX.Default..' - Control surfaces'
strings.Category.FSX.Engines = strings.Category.FSX.Default..' - Engines'
strings.Category.FSX.Instruments = strings.Category.FSX.Default..' - Instruments'
strings.Category.FSX.Lights = strings.Category.FSX.Default..' - Lights'
strings.Category.FSX.Panels = strings.Category.FSX.Default..' - Panels'
strings.Category.FSX.Radios = strings.Category.FSX.Default..' - Radios'
strings.Category.FSX.Systems = strings.Category.FSX.Default..' - Systems'
return strings

View File

@ -0,0 +1,26 @@
local strings = require './lib/strings'
RegisterFunction(
{
uid = 'atcVisiblity',
category = strings.Category.FSX.Panels,
displayName = 'ATC Visibility',
states = {
hidden = { displayName = 'Hidden', default = LEDColor.Green },
visible = { displayName = 'Visible', default = LEDColor.FlashingAmberNormal },
}
},
function(context)
SetState(context, 'hidden')
OnTimer(context, 1000,
function(context)
if FSXWindowVisible('ATC Menu') then
SetState(context, 'visible')
else
SetState(context, 'hidden')
end
end)
end
)

View File

@ -26,12 +26,14 @@ type
FSimConnect: TInterfacedObject;
FSimConnectLock: TCriticalSection;
FProfileMenuSimConnect: IFSXSimConnectProfileMenu;
FScriptSimConnect: TObject;
protected
function GetUID: string; override;
function CreateLuaLEDFunction(AInfo: ILuaTable; AOnSetup: ILuaFunction): TCustomLuaLEDFunction; override;
procedure InitInterpreter; override;
procedure ScriptOnSimConnect(Context: ILuaContext);
procedure ScriptFSXWindowVisible(Context: ILuaContext);
procedure SetupWorker(AWorker: TFSXLEDFunctionWorker; AOnSetup: ILuaFunction);
{ IFSXSimConnectObserver }
@ -132,18 +134,6 @@ type
end;
TLuaSimConnect = class(TPersistent)
private
FProvider: TFSXLEDFunctionProvider;
protected
property Provider: TFSXLEDFunctionProvider read FProvider;
public
constructor Create(AProvider: TFSXLEDFunctionProvider);
published
procedure Monitor(Context: ILuaContext);
end;
const
LuaSimConnectDataTypes: array[TLuaSimConnectDataType] of string =
(
@ -173,7 +163,6 @@ end;
constructor TFSXLEDFunctionProvider.Create(const AScriptFolders: TStringDynArray);
begin
FSimConnectLock := TCriticalSection.Create;
FScriptSimConnect := TLuaSimConnect.Create(Self);
inherited Create(AScriptFolders);
end;
@ -183,7 +172,6 @@ destructor TFSXLEDFunctionProvider.Destroy;
begin
inherited Destroy;
FreeAndNil(FScriptSimConnect);
FreeAndNil(FSimConnectLock);
end;
@ -271,7 +259,8 @@ var
begin
inherited InitInterpreter;
Interpreter.RegisterFunctions(FScriptSimConnect, 'SimConnect');
Interpreter.RegisterFunction('OnSimConnect', ScriptOnSimConnect);
Interpreter.RegisterFunction('FSXWindowVisible', ScriptFSXWindowVisible);
simConnectDataType := TLuaTable.Create;
for dataType := Low(TLuaSimConnectDataType) to High(TLuaSimConnectDataType) do
@ -281,6 +270,116 @@ begin
end;
procedure TFSXLEDFunctionProvider.ScriptOnSimConnect(Context: ILuaContext);
var
workerID: string;
variables: ILuaTable;
onData: ILuaFunction;
worker: TCustomLuaLEDFunctionWorker;
definition: IFSXSimConnectDefinition;
variable: TLuaKeyValuePair;
info: ILuaTable;
dataType: TLuaSimConnectDataType;
simConnectDataType: SIMCONNECT_DATAType;
units: string;
luaVariables: TList<TLuaSimConnectVariable>;
luaVariable: TLuaSimConnectVariable;
begin
CheckParameters('OnSimConnect', Context.Parameters, [VariableString, VariableTable, VariableFunction]);
workerID := Context.Parameters[0].AsString;
variables := Context.Parameters[1].AsTable;
onData := Context.Parameters[2].AsFunction;
worker := FindWorker(workerID);
if not Assigned(worker) then
raise ELuaScriptError.Create('OnSimConnect: invalid context');
definition := GetSimConnect.CreateDefinition;
luaVariables := TList<TLuaSimConnectVariable>.Create;
try
for variable in variables do
begin
if variable.Value.VariableType = VariableTable then
begin
info := variable.Value.AsTable;
if info.HasValue('variable') then
begin
luaVariable.Name := variable.Key.AsString;
units := '';
simConnectDataType := SIMCONNECT_DATAType_FLOAT64;
if info.HasValue('type') and GetDataType(info.GetValue('type').AsString, dataType) then
begin
luaVariable.DataType := dataType;
case dataType of
Float32: simConnectDataType := SIMCONNECT_DATAType_FLOAT32;
Int64: simConnectDataType := SIMCONNECT_DATAType_INT64;
Int32,
Bool:
begin
simConnectDataType := SIMCONNECT_DATAType_INT32;
units := 'bool';
end;
// TODO change to STRINGV
StringValue: simConnectDataType := SIMCONNECT_DATAType_STRING256;
XYZ: simConnectDataType := SIMCONNECT_DATAType_XYZ;
LatLonAlt: simConnectDataType := SIMCONNECT_DATAType_LATLONALT;
Waypoint: simConnectDataType := SIMCONNECT_DATAType_WAYPOINT;
end;
if info.HasValue('units') then
units := info.GetValue('units').AsString
else if not (dataType in [Bool, StringValue, XYZ, LatLonAlt, Waypoint]) then
raise ELuaScriptError.CreateFmt('OnSimConnect: missing units for variable %s', [variable.Key.AsString]);
end else
begin
if not info.HasValue('units') then
raise ELuaScriptError.CreateFmt('OnSimConnect: missing units or type for variable %s', [variable.Key.AsString]);
units := info.GetValue('units').AsString;
end;
luaVariables.Add(luaVariable);
definition.AddVariable(info.GetValue('variable').AsString, units, simConnectDataType);
end;
end;
end;
(worker as TFSXLEDFunctionWorker).AddDefinition(definition, TFSXFunctionWorkerDataHandler.Create(luaVariables, worker.UID, onData));
finally
FreeAndNil(luaVariables);
end;
end;
procedure TFSXLEDFunctionProvider.ScriptFSXWindowVisible(Context: ILuaContext);
const
ClassNameMainWindow = 'FS98MAIN';
ClassNameChildWindow = 'FS98CHILD';
ClassNameFloatWindow = 'FS98FLOAT';
var
windowTitle: string;
begin
CheckParameters('FSXWindowVisible', Context.Parameters, [VariableString]);
windowTitle := Context.Parameters[0].AsString;
Context.Result.Push(
{ Docked }
WindowVisible(ClassNameChildWindow, windowTitle, ClassNameMainWindow, '') or
{ Undocked }
WindowVisible(ClassNameFloatWindow, windowTitle, '', ''));
end;
// #ToDo1 -oMvR: 4-6-2017: move up to LuaLEDFunctionProvider
procedure TFSXLEDFunctionProvider.SetupWorker(AWorker: TFSXLEDFunctionWorker; AOnSetup: ILuaFunction);
begin
try
@ -562,110 +661,4 @@ begin
end;
end;
{ TLuaSimConnect }
constructor TLuaSimConnect.Create(AProvider: TFSXLEDFunctionProvider);
begin
inherited Create;
FProvider := AProvider;
end;
procedure TLuaSimConnect.Monitor(Context: ILuaContext);
var
workerID: string;
variables: ILuaTable;
onData: ILuaFunction;
worker: TCustomLuaLEDFunctionWorker;
definition: IFSXSimConnectDefinition;
variable: TLuaKeyValuePair;
info: ILuaTable;
dataType: TLuaSimConnectDataType;
simConnectDataType: SIMCONNECT_DATAType;
units: string;
luaVariables: TList<TLuaSimConnectVariable>;
luaVariable: TLuaSimConnectVariable;
begin
if Context.Parameters.Count < 3 then
raise ELuaScriptError.Create('Not enough parameters for SimConnect.Monitor');
if Context.Parameters[0].VariableType <> VariableString then
raise ELuaScriptError.Create('Context expected for SimConnect.Monitor parameter 1');
if Context.Parameters[1].VariableType <> VariableTable then
raise ELuaScriptError.Create('Table expected for SimConnect.Monitor parameter 2');
if Context.Parameters[2].VariableType <> VariableFunction then
raise ELuaScriptError.Create('Function expected for SimConnect.Monitor parameter 3');
workerID := Context.Parameters[0].AsString;
variables := Context.Parameters[1].AsTable;
onData := Context.Parameters[2].AsFunction;
worker := Provider.FindWorker(workerID);
if not Assigned(worker) then
raise ELuaScriptError.Create('Context expected for SimConnect.Monitor parameter 1');
definition := Provider.GetSimConnect.CreateDefinition;
luaVariables := TList<TLuaSimConnectVariable>.Create;
try
for variable in variables do
begin
if variable.Value.VariableType = VariableTable then
begin
info := variable.Value.AsTable;
if info.HasValue('variable') then
begin
luaVariable.Name := variable.Key.AsString;
units := '';
simConnectDataType := SIMCONNECT_DATAType_FLOAT64;
if info.HasValue('type') and GetDataType(info.GetValue('type').AsString, dataType) then
begin
luaVariable.DataType := dataType;
case dataType of
Float32: simConnectDataType := SIMCONNECT_DATAType_FLOAT32;
Int64: simConnectDataType := SIMCONNECT_DATAType_INT64;
Int32,
Bool:
begin
simConnectDataType := SIMCONNECT_DATAType_INT32;
units := 'bool';
end;
// TODO change to STRINGV
StringValue: simConnectDataType := SIMCONNECT_DATAType_STRING256;
XYZ: simConnectDataType := SIMCONNECT_DATAType_XYZ;
LatLonAlt: simConnectDataType := SIMCONNECT_DATAType_LATLONALT;
Waypoint: simConnectDataType := SIMCONNECT_DATAType_WAYPOINT;
end;
if info.HasValue('units') then
units := info.GetValue('units').AsString
else if not (dataType in [Bool, StringValue, XYZ, LatLonAlt, Waypoint]) then
raise ELuaScriptError.CreateFmt('Missing units for variable %s', [variable.Key.AsString]);
end else
begin
if not info.HasValue('units') then
raise ELuaScriptError.CreateFmt('Missing units or type for variable %s', [variable.Key.AsString]);
units := info.GetValue('units').AsString;
end;
luaVariables.Add(luaVariable);
definition.AddVariable(info.GetValue('variable').AsString, units, simConnectDataType);
end;
end;
end;
(worker as TFSXLEDFunctionWorker).AddDefinition(definition, TFSXFunctionWorkerDataHandler.Create(luaVariables, worker.UID, onData));
finally
FreeAndNil(luaVariables);
end;
end;
end.

View File

@ -6,6 +6,8 @@ uses
System.SysUtils,
System.Types,
OtlTask,
OtlTaskControl,
X2Log.Intf,
LEDFunction,
@ -52,6 +54,7 @@ type
protected
function CreateLuaLEDFunction(AInfo: ILuaTable; ASetup: ILuaFunction): TCustomLuaLEDFunction; virtual; abstract;
procedure CheckParameters(const AFunctionName: string; AParameters: ILuaReadParameters; AExpectedTypes: array of TLuaVariableType);
procedure AppendVariable(ABuilder: TStringBuilder; AVariable: ILuaVariable);
procedure AppendTable(ABuilder: TStringBuilder; ATable: ILuaTable);
@ -60,6 +63,10 @@ type
procedure ScriptRegisterFunction(Context: ILuaContext);
procedure ScriptSetState(Context: ILuaContext);
procedure ScriptOnTimer(Context: ILuaContext);
procedure ScriptWindowVisible(Context: ILuaContext);
function WindowVisible(const AClassName, AWindowTitle, AParentClassName, AParentWindowTitle: string): Boolean;
procedure InitInterpreter; virtual;
procedure RegisterFunctions; override;
@ -81,10 +88,14 @@ type
private
FProvider: TCustomLuaLEDFunctionProvider;
FUID: string;
FTasks: TList<IOmniTaskControl>;
procedure SetProvider(const Value: TCustomLuaLEDFunctionProvider);
protected
procedure AddTask(ATask: IOmniTaskControl);
property Provider: TCustomLuaLEDFunctionProvider read FProvider write SetProvider;
property Tasks: TList<IOmniTaskControl> read FTasks;
public
constructor Create(const AProviderUID, AFunctionUID: string; AStates: ILEDMultiStateFunction; ASettings: ILEDFunctionWorkerSettings; const APreviousState: string = ''); override;
destructor Destroy; override;
@ -97,6 +108,8 @@ implementation
uses
System.Classes,
System.IOUtils,
System.StrUtils,
Winapi.Windows,
Lua.API,
X2Log.Global,
@ -123,6 +136,18 @@ type
end;
TLuaTimerTask = class(TOmniWorker)
private
FOnTimer: TProc;
protected
property OnTimer: TProc read FOnTimer;
public
constructor Create(AOnTimer: TProc);
procedure Run;
end;
const
LuaLEDColors: array[TLEDColor] of string =
(
@ -173,12 +198,33 @@ end;
procedure TCustomLuaLEDFunctionProvider.InitInterpreter;
var
requirePath: TStringBuilder;
scriptFolder: string;
table: ILuaTable;
color: TLEDColor;
begin
requirePath := TStringBuilder.Create;
try
for scriptFolder in ScriptFolders do
begin
if requirePath.Length > 0 then
requirePath.Append(';');
requirePath.Append(IncludeTrailingPathDelimiter(scriptFolder)).Append('?;')
.Append(IncludeTrailingPathDelimiter(scriptFolder)).Append('?.lua');
end;
Interpreter.SetRequirePath(requirePath.ToString);
Interpreter.GetRequirePath;
finally
FreeAndNil(requirePath);
end;
Interpreter.RegisterFunction('RegisterFunction', ScriptRegisterFunction);
Interpreter.RegisterFunction('SetState', ScriptSetState);
Interpreter.RegisterFunction('OnTimer', ScriptOnTimer);
Interpreter.RegisterFunction('WindowVisible', ScriptWindowVisible);
Interpreter.RegisterFunctions(FScriptLog, 'Log');
@ -187,9 +233,30 @@ begin
table.SetValue(LuaLEDColors[color], LuaLEDColors[color]);
Interpreter.SetGlobalVariable('LEDColor', table);
end;
// #ToDo1 -oMvR: 29-5-2017: Timer
// #ToDo1 -oMvR: 29-5-2017: FindWindow / FindFSXWindow
procedure TCustomLuaLEDFunctionProvider.CheckParameters(const AFunctionName: string; AParameters: ILuaReadParameters; AExpectedTypes: array of TLuaVariableType);
const
VariableTypeName: array[TLuaVariableType] of string =
(
'None', 'Boolean', 'Integer',
'Number', 'UserData', 'String',
'Table', 'Function'
);
var
parameterIndex: Integer;
begin
if AParameters.Count < Length(AExpectedTypes) then
raise ELuaScriptError.CreateFmt('%s: expected at least %d parameter%s', [AFunctionName, Length(AExpectedTypes), IfThen(Length(AExpectedTypes) <> 1, 's', '')]);
for parameterIndex := 0 to High(AExpectedTypes) do
if AParameters[parameterIndex].VariableType <> AExpectedTypes[parameterIndex] then
raise ELuaScriptError.CreateFmt('%s: expected %s for parameter %d, got %s',
[AFunctionName, VariableTypeName[AExpectedTypes[parameterIndex]],
Succ(parameterIndex), VariableTypeName[AParameters[parameterIndex].VariableType]]);
end;
@ -280,20 +347,13 @@ var
setup: ILuaFunction;
begin
if Context.Parameters.Count < 2 then
raise ELuaScriptError.Create('Not enough parameters for RegisterFunction');
if Context.Parameters[0].VariableType <> VariableTable then
raise ELuaScriptError.Create('Table expected for RegisterFunction parameter 1');
if Context.Parameters[1].VariableType <> VariableFunction then
raise ELuaScriptError.Create('Function expected for RegisterFunction parameter 2');
CheckParameters('RegisterFunction', Context.Parameters, [VariableTable, VariableFunction]);
info := Context.Parameters[0].AsTable;
setup := Context.Parameters[1].AsFunction;
if not info.HasValue('uid') then
raise ELuaScriptError.Create('"uid" value is required for RegisterFunction parameter 1');
raise ELuaScriptError.Create('RegisterFunction: "uid" value is required');
DoLogMessage(Context, TX2LogLevel.Info, Format('Registering function: %s', [info.GetValue('uid').AsString]));
RegisterFunction(CreateLuaLEDFunction(info, setup));
@ -307,27 +367,114 @@ var
stateUID: string;
begin
if Context.Parameters.Count < 2 then
raise ELuaScriptError.Create('Not enough parameters for SetState');
if Context.Parameters[0].VariableType <> VariableString then
raise ELuaScriptError.Create('Context expected for SetState parameter 1');
if Context.Parameters[1].VariableType <> VariableString then
raise ELuaScriptError.Create('State expected for SetState parameter 2');
CheckParameters('SetState', Context.Parameters, [VariableString, VariableString]);
workerID := Context.Parameters[0].AsString;
stateUID := Context.Parameters[1].AsString;
worker := FindWorker(workerID);
if not Assigned(worker) then
raise ELuaScriptError.Create('Context expected for SetState parameter 1');
raise ELuaScriptError.Create('SetState: invalid context');
DoLogMessage(Context, TX2LogLevel.Info, Format('Setting state for %s to: %s', [worker.GetFunctionUID, stateUID]));
worker.SetCurrentState(stateUID);
end;
procedure TCustomLuaLEDFunctionProvider.ScriptOnTimer(Context: ILuaContext);
var
workerID: string;
interval: Integer;
timerCallback: ILuaFunction;
worker: TCustomLuaLEDFunctionWorker;
begin
CheckParameters('OnTimer', Context.Parameters, [VariableString, VariableNumber, VariableFunction]);
workerID := Context.Parameters[0].AsString;
interval := Context.Parameters[1].AsInteger;
timerCallback := Context.Parameters[2].AsFunction;
worker := FindWorker(workerID);
if not Assigned(worker) then
raise ELuaScriptError.Create('OnTimer: invalid context');
DoLogMessage(Context, TX2LogLevel.Info, Format('Adding timer for %s, interval: %d', [worker.GetFunctionUID, interval]));
worker.AddTask(CreateTask(TLuaTimerTask.Create(
procedure
begin
try
timerCallback.Call([workerID]);
except
on E:Exception do
TX2GlobalLog.Category('Lua').Exception(E);
end;
end))
.SetTimer(1, MSecsPerSec, @TLuaTimerTask.Run)
.Run);
end;
procedure TCustomLuaLEDFunctionProvider.ScriptWindowVisible(Context: ILuaContext);
var
className: string;
windowTitle: string;
parentClassName: string;
parentWindowTitle: string;
begin
if Context.Parameters.Count = 0 then
raise ELuaScriptError.Create('WindowVisible: expected at least 1 parameter');
className := '';
windowTitle := '';
parentClassName := '';
parentWindowTitle := '';
if Context.Parameters.Count >= 1 then className := Context.Parameters[0].AsString;
if Context.Parameters.Count >= 2 then windowTitle := Context.Parameters[1].AsString;
if Context.Parameters.Count >= 3 then parentClassName := Context.Parameters[2].AsString;
if Context.Parameters.Count >= 4 then parentWindowTitle := Context.Parameters[3].AsString;
Context.Result.Push(WindowVisible(className, windowTitle, parentClassName, parentWindowTitle));
end;
function TCustomLuaLEDFunctionProvider.WindowVisible(const AClassName, AWindowTitle, AParentClassName, AParentWindowTitle: string): Boolean;
function GetNilPChar(const AValue: string): PChar;
begin
if Length(AValue) > 0 then
Result := PChar(AValue)
else
Result := nil;
end;
var
parentWindow: THandle;
childWindow: THandle;
window: THandle;
begin
Result := False;
if (Length(AParentClassName) > 0) or (Length(AParentWindowTitle) > 0) then
begin
parentWindow := FindWindow(GetNilPChar(AParentClassName), GetNilPChar(AParentWindowTitle));
if parentWindow <> 0 then
begin
childWindow := FindWindowEx(parentWindow, 0, GetNilPChar(AClassName), GetNilPChar(AWindowTitle));
Result := (childWindow <> 0) and IsWindowVisible(childWindow);
end;
end else
begin
window := FindWindow(GetNilPChar(AClassName), GetNilPChar(AWindowTitle));
Result := (window <> 0) and IsWindowVisible(window);
end;
end;
procedure TCustomLuaLEDFunctionProvider.RegisterFunctions;
var
scriptFolder: string;
@ -461,13 +608,36 @@ begin
end;
destructor TCustomLuaLEDFunctionWorker.Destroy;
var
task: IOmniTaskControl;
begin
if Assigned(Tasks) then
begin
for task in Tasks do
begin
task.Stop;
task.WaitFor(INFINITE);
end;
FreeAndNil(FTasks);
end;
SetProvider(nil);
inherited Destroy;
end;
procedure TCustomLuaLEDFunctionWorker.AddTask(ATask: IOmniTaskControl);
begin
if not Assigned(Tasks) then
FTasks := TList<IOmniTaskControl>.Create;
Tasks.Add(ATask);
end;
procedure TCustomLuaLEDFunctionWorker.SetProvider(const Value: TCustomLuaLEDFunctionProvider);
begin
if Value <> FProvider then
@ -515,4 +685,19 @@ begin
OnLog(Context, TX2LogLevel.Error);
end;
{ TLuaTimerTask }
constructor TLuaTimerTask.Create(AOnTimer: TProc);
begin
inherited Create;
FOnTimer := AOnTimer;
end;
procedure TLuaTimerTask.Run;
begin
FOnTimer();
end;
end.