diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e12279..cf556a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,94 @@
# WASimCommander - Change Log
+## 1.2.0.0 (next)
+
+### WASimModule
+* Fix binary data representation in results for named variable requests with 1-4 byte integer value sizes (`int8` - `int32` types) -- the result data would be encoded as a float type instead. ([8c7724e6])
+* Restore ability to use Unit type specifiers when setting and getting Local vars. ([e16049ac])
+* Added ability to specify/set a default 'L' var value and unit type in `GetCreate` command to use if the variable needs to be created. ([61a52674])
+* `GetCreate` and `SetCreate` commands for non-L types now silently fall back to `Get` and `Set` respectively. ([61a52674])
+* Fixed that command response for `GetCreate` was always sent as if responding to a `Get` command. ([61a52674])
+* Added `requestId` to error logging and response output for data requests and add more info for `Get` command errors. ([17791eef])
+* Added ability to return string type results for Get commands and Named data requests by converting them to calculator expressions automatically on the server. ([983e7ab6])
+* Improved automatic conversion to calc code for other variable types by including the unit type, if given, and narrowing numeric results to integer types if needed. ([983e7ab6])
+* Prevent possible simulator hang on exit when quitting with active client(s) connections. ([70e0ef31])
+* Event loop processing is now paused/restarted also based on whether any connected client(s) have active data requests and if they are paused or not (previously it was only based on if any clients were connected at all). ([90242ed4])
+* Fixes a logged SimConnect error when trying to unsubscribe from the "Frame" event (cause unknown). ([90242ed4])
+* Data requests with `Once` type update period are now queued if data updates are paused when the request is submitted. These requests will be sent when/if updates are resumed again by the client. Fixes that data would be sent anyway when the request is initially submitted, even if updates are paused. ([fe99bbb2])
+* Update reference list of KEY events and aliases as of MSFS SDK v0.22.3.0. ([f045e150])
+
+[8c7724e6]: https://github.com/mpaperno/WASimCommander/commit/8c7724e60ed94e622d5ee2669cf7e000031c2c18
+[e16049ac]: https://github.com/mpaperno/WASimCommander/commit/e16049ac69ff15cdcdd9084c7fdab6920a1ffba1
+[61a52674]: https://github.com/mpaperno/WASimCommander/commit/61a52674e0dff7e1f3e63ed73a0bed711bb2c479
+[17791eef]: https://github.com/mpaperno/WASimCommander/commit/17791eefecc86454c031636a5da9c19d56e21139
+[983e7ab6]: https://github.com/mpaperno/WASimCommander/commit/983e7ab609e81af81525ff84431b1c4557447d87
+[70e0ef31]: https://github.com/mpaperno/WASimCommander/commit/70e0ef31b01a1a772d9e49102e0a77ec6f3e928b
+[90242ed4]: https://github.com/mpaperno/WASimCommander/commit/90242ed494069aba5bcdad839914b9fcfc6521e2
+[fe99bbb2]: https://github.com/mpaperno/WASimCommander/commit/fe99bbb25c5dd907e8a4d513769759c4b430580f
+[f045e150]: https://github.com/mpaperno/WASimCommander/commit/f045e15007abd6b7b05b97c004a7a55488a33a9b
+
+### WASimClient and WASimClient_CLI (managed .NET)
+* Fixed incoming data size check for variable requests which are less than 4 bytes in size. ([c8e74dfa])
+* Fixed early timeout being reported on long-running `list()` requests (eg.thousands of L vars). ([a05a28c3])
+* Restored ability to specify Unit type for L vars and support for GetCreate with default value/unit and added extra features: ([3090d534], [0a30646d])
+ * Added unit name parameter to `setLocalVariable()` and `setOrCreateLocalVariable()`.
+ * Added `getOrCreateLocalVariable()`.
+ * Added `VariableRequest::createLVar` property.
+ * Add optional `create` flag and unit name to `VariableRequest()` c'tor overloads.
+* Added async option to `saveDataRequest()` which doesn't wait for server response (`saveDataRequestAsync()` for the C# version). ([82ea4252], [0a30646d])
+* Added ability to return a string value with `getVariable()` to make use of new WASimModule feature. ([8e75eb8c], [0e54794b])
+* The request updates paused state (set with `setDataRequestsPaused()`) is now saved locally even if not connected to server and will be sent to server upon connection and before sending any queued data requests.
+ This allows connecting and sending queued requests but suspending any actual value checks until needed. ([bea8bccb])
+* The `setVariable()` method now verifies that the specified variable type is settable before sending the command to the server. ([576914a2])
+* Removed logged version mismatch warning on Ping response.
+* Documentation updates.
+
+[c8e74dfa]: https://github.com/mpaperno/WASimCommander/commit/c8e74dfa706647cf785c7e6c811731d8945e49c6
+[a05a28c3]: https://github.com/mpaperno/WASimCommander/commit/a05a28c3d1af56444be3fbe54f619e62548736a0
+[3090d534]: https://github.com/mpaperno/WASimCommander/commit/3090d5344c3a34c62e81f61237fe1fd91f6b11c5
+[0a30646d]: https://github.com/mpaperno/WASimCommander/commit/0a30646d0ae985580d67ed40c8a441a0f5a0ba17
+[82ea4252]: https://github.com/mpaperno/WASimCommander/commit/82ea4252bd25423bbeab354799d6be41f053880e
+[8e75eb8c]: https://github.com/mpaperno/WASimCommander/commit/8e75eb8c087f5a39fee93c2b7d073500e4f14664
+[0e54794b]: https://github.com/mpaperno/WASimCommander/commit/0e54794b2ec8411f42d34a7696426724ffc5e932
+[bea8bccb]: https://github.com/mpaperno/WASimCommander/commit/bea8bccba38fae987690d5af259f6f8b22fbc781
+[576914a2]: https://github.com/mpaperno/WASimCommander/commit/576914a235c81b73ba0ea85655d913b61cbc5015
+
+### WASimClient_CLI (managed .NET)
+* Fixed possible exception when assembling list lookup results dictionary in the off-case of duplicate keys. ([cf46967b])
+
+[cf46967b]: https://github.com/mpaperno/WASimCommander/commit/cf46967b499a9bb19a77a14a47bd2ac29b4d0989
+
+### WASimUI
+* Added database of Simulator Variables, Key Events, and Unit types imported from SimConnect SDK online documentation. This is used for:
+ * Typing suggestions in the related form fields when entering names of 'A' vars, Key Events, or Unit types.
+ * Available as a popup search window from each related form (Variables, Key Events, Data Requests) via button/menu/CTRL-F shortcut.
+ * Can be opened as a standalone window for browsing and searching all imported data by type.
+* Added ability to import and export Data Requests in _MSFS/SimConnect Touch Portal Plugin_ format with a new editor window available to adjust plugin-specific data before export (category, format, etc.)
+* Fixed that the state of current item selections in tables wasn't always properly detected and buttons didn't get enabled/disabled when needed (eg. "Remove Requests" button).
+* Added ability to toggle visibility of each main form area of the UI from the View menu (eg. Variables or Key Events groups). Choices are preserved between sessions.
+* Simplified the connection/disconnection procedure by providing one action/button for both Sim and Server connections (independent actions still available via extension menu).
+* Typing suggestions in combo boxes now use a drop-down menu style selection list by default, and the behavior can be configured independently for each one.
+* String type variables can now be used in the "Variables" section for `Get` commands.
+* Unit type specifier is now shown and used for 'L' variables as well (unit is optional).
+* Added "Get or Create" action/button for 'L' vars.
+* The list of 'L' variables loaded from simulator is now sorted alphabetically.
+* The Size field in Data Request form is automatically populated with a likely match when a new Unit type is selected.
+* Many improvements in table views (all options are saved to user settings and persist between sessions):
+ * All column widths are now re-sizable in all tables.
+ * Columns can be toggled on/off in the views (r-click for context menu).
+ * Can now be sorted by multiple columns (CTRL-click).
+ * Option to show filtering (searching) text fields for each column. Filters support wildcards and optional regular expressions.
+ * Font size can be adjusted (using context menu or CTRL key with `+`, `-`, or `0` to reset.
+ * Tooltips shown with data values when hovered over table cells (readable even if text is too long to fit in the column).
+* Numerous shortcuts and context menus added throughout, each relevant to the respective forms/tables currently being used or clicked.
+* Last selected variable types and data request type are saved between sessions.
+* Most actions/buttons which require a server connection to work are now disabled when not connected.
+* When loading data requests from a file while connected to the server, the requests are now sent asynchronously, improving UI responsiveness.
+* More minor quality-of-life improvements!
+
+**Full log:** [v1.1.2.0...HEAD](https://github.com/mpaperno/WASimCommander/compare/1.1.2.0...next)
+
+---
## 1.1.2.0 (23-Feb-2023)
### WASimModule
diff --git a/README.md b/README.md
index ceca2f6..71eb586 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
[![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/mpaperno/WASimCommander?include_prereleases)](https://github.com/mpaperno/WASimCommander/releases)
[![GPLv3 License](https://img.shields.io/badge/license-GPLv3-blue.svg)](LICENSE.GPL.txt)
[![LGPGv3 License](https://img.shields.io/badge/license-LGPLv3-blue.svg)](LICENSE.LGPL.txt)
+[![API Documentation](https://img.shields.io/badge/API-Documentation-07A7EC?labelColor=black)](https://wasimcommander.max.paperno.us/)
[![Discord](https://img.shields.io/static/v1?style=flat&color=7289DA&&labelColor=7289DA&message=Discord%20Chat&label=&logo=discord&logoColor=white)](https://discord.gg/meWyE4dcAt)
@@ -15,7 +16,7 @@
**A WASM module-based Server and a full Client API combination.**
This project is geared towards other MSFS developers/coders who need a convenient way to remotely access parts of the Simulator which are normally
-inaccessible via _SimConnect_, such as locally-defined aircraft variables or custom events.
+inaccessible via _SimConnect_, such as some variable types and 'H' events, and running RPN "calculator code" directly on the sim.
The Client API can be utilized natively from C++, or via .NET managed assembly from C#, Python, or other languages.
@@ -27,7 +28,7 @@ to _SimConnect_ for basic functionality like reading/setting Simulation Variable
One of the motivations for this project was seeing multiple MSFS tool authors and casual hackers creating their own WASM modules and protocols just to support their
own product or need. There is nothing wrong with this, of course, but for the Sim user it can be a disadvantage on several levels. They may end up running
-multiple versions of modules which all do eseentially the same thing, and it may be confusing which WASM module they need to support which tool,
+multiple versions of modules which all do essentially the same thing, and it may be confusing which WASM module they need to support which tool,
just to name two obvious issues. For the developer, programming the WASM modules comes with its own quirks, too, not to mention the time involved.
And regardless of the supposed isolated environment a WASM module is supposed to run in, it's still very easy to take down the whole Simulator with
some errant code... ;-)
@@ -60,7 +61,7 @@ On a more practical note, I am using it with the [MSFS Touch Portal Plugin](http
- Any calculator code saved in subscriptions is **pre-compiled to a more efficient byte code** representation before being passed to the respective calculator
functions. This significantly improves performance for recurring calculations.
- **Register Named Events**:
- - Save recurring "set events," like activiating controls using calculator code, for more efficient and simpler re-use.
+ - Save recurring "set events," like activating controls using calculator code, for more efficient and simpler re-use.
Saved calculator code is pre-compiled to a more efficient byte code representation before being passed to the calculator function.
This significantly improves performance for recurring events.
- Registered events can be executed "natively" via _WASim API_ by simply sending a short command with the saved event ID.
@@ -68,7 +69,7 @@ On a more practical note, I am using it with the [MSFS Touch Portal Plugin](http
- Event names can be completely custom (including a `.` (period) as per SimConnect convention), or derive from the connected Client's name (to ensure uniqueness).
- **Send Simulator "Key Events"** directly by ID or name (instead of going through the SimConnect mapping process or executing calculator code). Much more efficient than the other methods.
- **New in v1.1.0:** Send Key Events with up to 5 values (like the new `SimConnect_TransmitClientEvent_EX1()`).
-- **Remote Logging**: Log messages (errors, warnings, debug, etc) can optionally be sent to the Client, with specific minimum level (eg. only warnings and errros).
+- **Remote Logging**: Log messages (errors, warnings, debug, etc) can optionally be sent to the Client, with specific minimum level (eg. only warnings and errors).
- **Ping** the Server to check that the WASM module is installed and running before trying to connect or use its features.
#### Core Components
@@ -77,9 +78,9 @@ On a more practical note, I am using it with the [MSFS Touch Portal Plugin](http
- Well-defined message API for communication between Server module and any client implementation.
- Uses standard SimConnect messages for the base network "transport" layer.
- All data allocations are on client side, so SimConnect limits in WASM module are bypassed (can in theory support unlimited clients).
-- No wasted data allocations, each data/variable subscription is stored independently avoiding complications with offets or data overflows.
+- No wasted data allocations, each data/variable subscription is stored independently avoiding complications with offsets or data overflows.
- Minimum possible impact on MSFS in terms of memory and CPU usage; practically zero effect for Sim user when no clients are connected (Server is idle).
-- Server periodically checks that a client is still connected by sending "hearbeat" ping requests and enforcing a timeout if no response is received.
+- Server periodically checks that a client is still connected by sending "heartbeat" ping requests and enforcing a timeout if no response is received.
- Extensive logging at configurable levels (debug/info/warning/etc) to multiple destinations (file/console/remote) for both Server and Client.
- Uses an efficient **lazy logging** implementation which doesn't evaluate any arguments if the log message will be discarded anyway
(eg. a DEBUG level message when minimum logging level is INFO).
@@ -90,7 +91,9 @@ On a more practical note, I am using it with the [MSFS Touch Portal Plugin](http
#### Desktop GUI
- Includes a full-featured desktop application which demonstrates/tests all available features of the API.
-- Fully usable as a standalone application which saves preferences, imports/exports lists of data subscriptions/registered events, and other usabililty features.
+- Fully usable as a standalone application which saves preferences, imports/exports lists of data subscriptions/registered events, and other friendly features.
+- Very useful for "exploring" the simulator in general, like checking variable values, testing effects of key events and RPN calculator code.
+- Can be used with the [MSFS/SimConnect Touch Portal Plugin](https://github.com/mpaperno/MSFSTouchPortalPlugin) for import/export of custom variable request definitions.
@@ -100,7 +103,7 @@ On a more practical note, I am using it with the [MSFS Touch Portal Plugin](http
-------------
-### Downloads
+### Downloads and Updates
Over in the [Releases](https://github.com/mpaperno/WASimCommander/releases) there are 3 packages provided. (The actual file names have version numbers appended.)
- `WASimCommander_SDK` - All header files, pre-built static and dynamic libs, packaged WASM module, pre-build GUI, reference documentation, and other tools/examples.
@@ -111,14 +114,16 @@ _Watch_ -> _Custom_ -> _Releases_ this repo (button at top) or subscribe to the
Update announcements are also posted on my Discord server's [WASimCommander release announcement channel](https://discord.gg/StbmZ2ZgsF).
+The SDK and updates are [published on Flightsim.to](https://flightsim.to/file/36474/wasimcommander) where one could "subscribe" to release notifications (account required).
+
-------------
### Documentation & Examples
There are three basic console-style tests/examples included for `C++`, `C#`, and `Python` in the [src/Testing](https://github.com/mpaperno/WASimCommander/tree/main/src/Testing) folder.
If you like reading code, this is the place to start.
-API docuemntation generated from source comments is published here: https://mpaperno.github.io/WASimCommander/
-A good place to start with the docs is probably the [`WASimClient`](https://mpaperno.github.io/WASimCommander/class_w_a_sim_commander_1_1_client_1_1_w_a_sim_client.html) page.
+API documentation generated from source comments is published here: https://wasimcommander.max.paperno.us/
+A good place to start with the docs is probably the [`WASimClient`](https://wasimcommander.max.paperno.us/class_w_a_sim_commander_1_1_client_1_1_w_a_sim_client.html) page.
The GUI is written in C++ (using Qt library for UI), and while not the simplest example, _is_ a full implementation of almost all the available
API features. The main `WASimClient` interactions all happen in the `MainWindow::Private` class at the top of the
@@ -141,7 +146,7 @@ The module also logs to a file, though it's a bit tricky to find. On my edition
`D:\WpSystem\S-1-5-21-611220451-769921231-644967174-1000\AppData\Local\Packages\Microsoft.FlightSimulator_8wekyb3d8bbwe\LocalState\packages\wasimcommander-module\work`
To enable more verbose logging on the module at startup, edit the `server_conf.ini` file which is found in the module's install folder
-(`Comunity\wasimcommander-module\modules`). There are comments in there indicating the options.
+(`Community\wasimcommander-module\modules`). There are comments in there indicating the options.
Keep in mind that the server logging level can also be changed remotely at runtime, but
of course that only works if you can establish a connection to the module in the first place.
@@ -185,12 +190,17 @@ Uses and includes [_IniPP_ by Matthias C. M. Troffaes](https://github.com/mcmtro
Uses the _Microsoft SimConnect SDK_ under the terms of the _MS Flight Simulator SDK EULA (11/2019)_ document.
-The GUI component uses portions of the [_Qt Library_](http://qt.io) under the terms of the GPL v3 license.
+WASimUI (GUI):
+- Uses portions of the [_Qt Library_](http://qt.io) under the terms of the GPL v3 license.
+- Uses and includes the following symbol fonts for icons, under the terms of their respective licenses:
+ - [IcoMoon Free](https://icomoon.io/#icons-icomoon) - IcoMoon.io, GPL v3.
+ - [Material Icons](https://material.io/) - Google, Apache License v2.0.
+- Uses modified versions of `FilterTableHeader` and `FilterLineEdit` components from [DB Browser for SQLite](https://github.com/sqlitebrowser/sqlitebrowser) under GPL v3 license.
+- Uses modified version of `MultisortTableView` from under GPL v3 license.
+- Uses Natural (alpha-numeric) sorting algorithm implementation for _Qt_ by Litkevich Yuriy (public domain).
-The GUI component uses and includes the following symbol fonts for icons, under the terms of their respective licenses:
-- [IcoMoon Free](https://icomoon.io/#icons-icomoon) - IcoMoon.io, GPL v3.
-- [Material Icons](https://material.io/) - Google, Apache License v2.0.
+Documentation generated with [Doxygen](https://www.doxygen.nl/) and styled with the most excellent [Doxygen Awesome](https://jothepro.github.io/doxygen-awesome-css).
-------------
### Copyright, License, and Disclaimer
diff --git a/build/Make-Version.ps1 b/build/Make-Version.ps1
index 03bab61..07ca9a3 100644
--- a/build/Make-Version.ps1
+++ b/build/Make-Version.ps1
@@ -40,5 +40,7 @@ function Make-Version {
Merge-Tokens -InputFile $SrcPath\include\wasim_version.in -OutputFile $SrcPath\include\wasim_version.h -Tokens $Tokens -NoWarning
Merge-Tokens -InputFile $SrcPath\WASimClient_CLI\AssemblyInfo.cpp.in -OutputFile $SrcPath\WASimClient_CLI\AssemblyInfo.cpp -Tokens $Tokens -NoWarning
Merge-Tokens -InputFile $DocPath\version.Doxyfile.in -OutputFile $DocPath\version.Doxyfile -Tokens $Tokens -NoWarning
+ $path = "${SrcPath}\WASimModule\WASimModuleProject\WASimCommander-Module\PackageDefinitions\wasimcommander-module.xml"
+ Merge-Tokens -InputFile "${path}.in" -OutputFile $path -Tokens $Tokens -NoWarning
}
diff --git a/build/Merge-Tokens.ps1 b/build/Merge-Tokens.ps1
index 7833f4e..096d67d 100644
--- a/build/Merge-Tokens.ps1
+++ b/build/Merge-Tokens.ps1
@@ -83,11 +83,12 @@ function Merge-Tokens {
Exit 1
}
+ $TmpFile = [System.IO.Path]::GetTempFileName()
+
# If the OutputFile is null, we will write to a temporary file
if ([string]::IsNullOrWhiteSpace($OutputFile)) {
Write-Verbose "OutputFile was omitted. Replacing InputFile."
- $OutputFile = [System.IO.Path]::GetTempFileName()
- $ReplaceInputFile = $true
+ $OutputFile = $InputFile
}
# Empty OutputFile if it already exists
@@ -115,8 +116,6 @@ function Merge-Tokens {
$usedTokens = New-Object -TypeName "System.Collections.ArrayList"
#$sw = [System.IO.File]::AppendText($OutputFile)
- # hack to force no-BOM UTF8 on PS v5.x
- " " | Out-File -Encoding ASCII -NoNewline -FilePath $OutputFile
(Get-Content $InputFile) | ForEach-Object {
$line = $_
$totalTokens += GetTokenCount($line)
@@ -134,13 +133,11 @@ function Merge-Tokens {
}
$missedTokens += GetTokenCount($line)
#$sw.WriteLine($line)
- $line | Out-File -Append -Encoding UTF8 -FilePath $OutputFile
+ $line | Out-File -Append -Encoding UTF8 -FilePath $TmpFile
}
- # If no OutputFile was given, we will replace the InputFile with the temporary file
- if ($ReplaceInputFile) {
- Get-Content -Path $OutputFile | Out-File -FilePath $InputFile -Encoding UTF8
- }
+ # Remove UTF8 BOM
+ Get-Content -Path $TmpFile | Out-File -FilePath $OutputFile
# Write warning if there were tokens given in the Token parameter which were not replaced
if (!$NoWarning -and $usedTokens.Count -ne $Tokens.Count) {
diff --git a/build/build.ps1 b/build/build.ps1
index 1654baf..e9e3d85 100644
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -8,7 +8,7 @@
Param(
[string[]]$Targets = "all",
[string]$RootPath = "..",
- [string[]]$Configuration = @("Debug", "Debug-DLL", "Release-DLL", "Release-net6", "Release-netfw", "Release"),
+ [string[]]$Configuration = @("Debug", "Debug-DLL", "Release-DLL", "Release-net6", "Release-net7", "Release-netfw", "Release"),
[string]$Platform = "x64",
[string[]]$Projects = "all",
[string]$BuildType = "Clean,Rebuild",
@@ -154,6 +154,9 @@ if ($LastExitCode -ge 8) { Write-Output($LastExitCode); Exit 1 }
# .NET 6
robocopy "$BuildPath\${CLIENT_NAME}_CLI\Release-net6-$Platform" "${csLibPath}\net6" *.dll *.pdb *.xml *.ini $copyOptions
if ($LastExitCode -ge 8) { Write-Output($LastExitCode); Exit 1 }
+# .NET 7
+robocopy "$BuildPath\${CLIENT_NAME}_CLI\Release-net7-$Platform" "${csLibPath}\net7" *.dll *.pdb *.xml *.ini $copyOptions
+if ($LastExitCode -ge 8) { Write-Output($LastExitCode); Exit 1 }
# .NET Framework
robocopy "$BuildPath\${CLIENT_NAME}_CLI\Release-netfw-$Platform" "${csLibPath}\net46" *.dll *.pdb *.xml *.ini $copyOptions
if ($LastExitCode -ge 8) { Write-Output($LastExitCode); Exit 1 }
diff --git a/build/version.ps1 b/build/version.ps1
index 412ce4a..ad58de9 100644
--- a/build/version.ps1
+++ b/build/version.ps1
@@ -1,7 +1,7 @@
$VER_MAJOR = 1
-$VER_MINOR = 1
-$VER_PATCH = 2
+$VER_MINOR = 2
+$VER_PATCH = 0
$VER_BUILD = 0
$VER_COMIT = 0
$VER_NAME = ""
diff --git a/docs/Doxyfile b/docs/Doxyfile
index 39357a5..0ebb4fa 100644
--- a/docs/Doxyfile
+++ b/docs/Doxyfile
@@ -13,11 +13,14 @@ MULTILINE_CPP_IS_BRIEF = YES
TAB_SIZE = 2
ALIASES = \
"refwc{1}=\ref WASimCommander::\1 \"\1\"" \
- "refwce{1}=\ref WASimCommander::Enums::\1 \"\1\"" \
- "refwcc{1}=\ref WASimCommander::Client::\1 \"\1\"" \
- "refwcli{1}=\ref WASimCommander::CLI::\1 \"\1\"" \
- "refwclie{1}=\ref WASimCommander::CLI::Enums::\1 \"\1\"" \
- "refwclis{1}=\ref WASimCommander::CLI::Structs::\1 \"\1\"" \
+ "refwce{1}=\ref WASimCommander::Enums::\1 \"Enums::\1\"" \
+ "refwcc{1}=\ref WASimCommander::Client::\1 \"Client::\1\"" \
+ "refwccc{1}=\ref WASimCommander::Client::WASimClient::\1 \"WASimClient::\1\"" \
+ "refwcli{1}=\ref WASimCommander::CLI::\1 \"CLI::\1\"" \
+ "refwclie{1}=\ref WASimCommander::CLI::Enums::\1 \"CLI::Enums::\1\"" \
+ "refwclis{1}=\ref WASimCommander::CLI::Structs::\1 \"CLI::Structs::\1\"" \
+ "refwclic{1}=\ref WASimCommander::CLI::Client::\1 \"CLI::Client::\1\"" \
+ "refwclicc{1}=\ref WASimCommander::CLI::Client::WASimClient\1 \"CLI::Client::WASimClient::\1\"" \
"default{1}=\nDefault value is \c \1.\n" \
"reimp{1}=Reimplemented from \c \1." \
"reimp=Reimplemented from superclass." \
@@ -25,7 +28,8 @@ ALIASES = \
"pacc=\par Access functions:^^" \
"psig=\par Notifier signal:^^" \
"intern=\parInternal use only." \
- "qflags{2}=
The \ref \1 type is a typedef for `QFlags<\2>`. It stores an OR combination of \ref \2 values.
"
+ "qflags{2}=
The \ref \1 type is a typedef for `QFlags<\2>`. It stores an OR combination of \ref \2 values.
" \
+ "since{1}=\par **Since \1**"
TOC_INCLUDE_HEADINGS = 5
AUTOLINK_SUPPORT = YES
diff --git a/docs/version.Doxyfile b/docs/version.Doxyfile
index e2cf38f..26c8061 100644
--- a/docs/version.Doxyfile
+++ b/docs/version.Doxyfile
@@ -1,7 +1,7 @@
-
+
# Doxyfile 1.8.17
# THIS FILE IS GENERATED BY A SCRIPT, CHANGES WILL NOT PERSIST. EDIT THE CORRESPONDING .in TEMPLATE FILE INSTEAD.
PROJECT_NAME = "WASimCommander"
-PROJECT_NUMBER = v1.1.2.0
+PROJECT_NUMBER = v1.2.0.0
PROJECT_BRIEF = "Remote access to the Microsoft Flight Simulator 2020 Gauge API."
diff --git a/src/Testing/CS_BasicConsole/CS_BasicConsole.csproj b/src/Testing/CS_BasicConsole/CS_BasicConsole.csproj
index 4ced449..6407f30 100644
--- a/src/Testing/CS_BasicConsole/CS_BasicConsole.csproj
+++ b/src/Testing/CS_BasicConsole/CS_BasicConsole.csproj
@@ -2,7 +2,6 @@
Exe
- net6.0-windowsdisableenableCS_BasicConsole.Program
@@ -11,14 +10,26 @@
Falsenonex64
+ Debug;Release;Release-net7
+ 1.1.0.0
+ 1.1.0.0
+ net6.0-windowsembedded
+ net6.0-windows
+ embedded
+ True
+
+
+
+ net7.0-windowsembedded
+ True
diff --git a/src/Testing/CS_BasicConsole/Program.cs b/src/Testing/CS_BasicConsole/Program.cs
index cca9973..08c8050 100644
--- a/src/Testing/CS_BasicConsole/Program.cs
+++ b/src/Testing/CS_BasicConsole/Program.cs
@@ -129,7 +129,8 @@ static void Main(string[] _)
// Test subscribing to a string type value. We'll use the Sim var "TITLE" (airplane name), which can only be retrieved using calculator code.
// We allocate 32 Bytes here to hold the result and we request this one with an update period of Once, which will return a result right away
// but will not be scheduled for regular updates. If we wanted to update this value later, we could call the client's `updateDataRequest(requestId)` method.
- hr = client.saveDataRequest(new DataRequest(
+ // Also we can use the "async" version which doesn't wait for the server to respond before returning. We're going to wait for a result anyway after submitting the request.
+ hr = client.saveDataRequestAsync(new DataRequest(
requestId: (uint)Requests.REQUEST_ID_2_STR,
resultType: CalcResultType.String,
calculatorCode: "(A:TITLE, String)",
@@ -198,7 +199,8 @@ static void ClientStatusHandler(ClientEvent ev)
// Event handler for showing listing results (eg. local vars list)
static void ListResultsHandler(ListResult lr)
{
- Log(lr.ToString()); // just use the ToString() override
+ Log($"Got {lr.list.Count} results for list type {lr.listType}. (Uncomment next line in ListResultsHandler() to print them.)");
+ //Log(lr.ToString()); // To print all the items just use the ToString() override.
// signal completion
dataUpdateEvent.Set();
}
@@ -206,7 +208,7 @@ static void ListResultsHandler(ListResult lr)
// Event handler to process data value subscription updates.
static void DataSubscriptionHandler(DataRequestRecord dr)
{
- Console.Write($"<< Got Data for request {(Requests)dr.requestId} \"{dr.nameOrCode}\" with Value: ");
+ Console.Write($"[{DateTime.Now.ToString("mm:ss.fff")}] << Got Data for request {(Requests)dr.requestId} \"{dr.nameOrCode}\" with Value: ");
// Convert the received data into a value using DataRequestRecord's tryConvert() methods.
// This could be more efficient in a "real" application, but it's good enough for our tests with only 2 value types.
if (dr.tryConvert(out float fVal))
@@ -215,14 +217,14 @@ static void DataSubscriptionHandler(DataRequestRecord dr)
Console.WriteLine($"(string) \"{sVal}\"");
}
else
- Console.WriteLine("Could not convert result data to value!");
+ Log("Could not convert result data to value!", "!!");
// signal completion
dataUpdateEvent.Set();
}
static void Log(string msg, string prfx = "=:")
{
- Console.WriteLine(prfx + ' ' + msg);
+ Console.WriteLine("[{0}] {1} {2}", DateTime.Now.ToString("mm:ss.fff"), prfx, msg);
}
}
diff --git a/src/WASimClient/WASimClient.cpp b/src/WASimClient/WASimClient.cpp
index 8aa2668..4365817 100644
--- a/src/WASimClient/WASimClient.cpp
+++ b/src/WASimClient/WASimClient.cpp
@@ -234,6 +234,7 @@ class WASimClient::Private
atomic_bool simConnected = false;
atomic_bool serverConnected = false;
atomic_bool logCDAcreated = false;
+ atomic_bool requestsPaused = false;
HANDLE hSim = nullptr;
HANDLE hSimEvent = nullptr;
@@ -676,6 +677,8 @@ class WASimClient::Private
listResult.reset();
// make sure server knows our desired log level and set up data area/request if needed
updateServerLogLevel();
+ // set update status of data requests before adding any, in case we don't actually want results yet
+ sendServerCommand(Command(CommandId::Subscribe, (requestsPaused ? 0 : 1)));
// (re-)register (or delete) any saved DataRequests
registerAllDataRequests();
// same with calculator events
@@ -784,7 +787,7 @@ class WASimClient::Private
return &reponses.try_emplace(token, token, cv).first->second;
}
- // Blocks and waits for a response to a specific command token.
+ // Blocks and waits for a response to a specific command token. `timeout` can be -1 to use `extraPredicate` only (which is then required).
HRESULT waitCommandResponse(uint32_t token, Command *response, uint32_t timeout = 0, std::function extraPredicate = nullptr)
{
TrackedResponse *tr = findTrackedResponse(token);
@@ -795,8 +798,10 @@ class WASimClient::Private
if (!timeout)
timeout = settings.networkTimeout;
- auto stop_waiting = [=]() {
- return !serverConnected || (tr->response.commandId != CommandId::None && tr->response.token == token && (!extraPredicate || extraPredicate()));
+ bool stopped = false;
+ auto stop_waiting = [=, &stopped]() {
+ return stopped =
+ !serverConnected || (tr->response.commandId != CommandId::None && tr->response.token == token && (!extraPredicate || extraPredicate()));
};
HRESULT hr = E_TIMEOUT;
@@ -805,10 +810,20 @@ class WASimClient::Private
}
else {
unique_lock lock(tr->mutex);
- if (cv->wait_for(lock, chrono::milliseconds(timeout), stop_waiting))
- hr = S_OK;
+ if (timeout > 0) {
+ if (cv->wait_for(lock, chrono::milliseconds(timeout), stop_waiting))
+ hr = S_OK;
+ }
+ else if (!!extraPredicate) {
+ cv->wait(lock, stop_waiting);
+ hr = stopped ? ERROR_ABANDONED_WAIT_0 : S_OK;
+ }
+ else {
+ hr = E_INVALIDARG;
+ LOG_DBG << "waitCommandResponse() requires a predicate condition when timeout parameter is < 0.";
+ }
}
- if (SUCCEEDED(hr) && response) {
+ if (SUCCEEDED(hr) && !!response) {
unique_lock lock(tr->mutex);
*response = move(tr->response);
}
@@ -836,14 +851,17 @@ class WASimClient::Private
void waitListRequestEnd()
{
auto stop_waiting = [this]() {
- //shared_lock lock(listResult.mutex);
return listResult.nextTimeout.load() >= Clock::now();
};
Command response;
- HRESULT hr = waitCommandResponse(listResult.token, &response, 0, stop_waiting);
- if (hr == E_TIMEOUT) {
+ HRESULT hr = waitCommandResponse(listResult.token, &response, -1, stop_waiting);
+ if (hr == ERROR_ABANDONED_WAIT_0) {
LOG_ERR << "List request timed out.";
+ hr = E_TIMEOUT;
+ }
+ else if (hr != S_OK) {
+ LOG_ERR << "List request failed with result: " << LOG_HR(hr);
}
else if (response.commandId != CommandId::Ack) {
LOG_WRN << "Server returned Nak for list request of " << Utilities::getEnumName(listResult.listType.load(), LookupItemTypeNames);
@@ -911,7 +929,7 @@ class WASimClient::Private
return sValue;
}
- HRESULT getVariable(const VariableRequest &v, double *result)
+ HRESULT getVariable(const VariableRequest &v, double *result, std::string *sResult = nullptr, double dflt = 0.0)
{
const string sValue = buildVariableCommandString(v, false);
if (sValue.empty() || sValue.length() >= STRSZ_CMD)
@@ -919,7 +937,7 @@ class WASimClient::Private
HRESULT hr;
Command response;
- if FAILED(hr = sendCommandWithResponse(Command(CommandId::Get, v.variableType, sValue.c_str()), &response))
+ if FAILED(hr = sendCommandWithResponse(Command(v.createLVar && v.variableType == 'L' ? CommandId::GetCreate : CommandId::Get, v.variableType, sValue.c_str(), dflt), &response))
return hr;
if (response.commandId != CommandId::Ack) {
LOG_WRN << "Get Variable request for " << quoted(sValue) << " returned Nak response. Reason, if any: " << quoted(response.sData);
@@ -927,15 +945,21 @@ class WASimClient::Private
}
if (result)
*result = response.fData;
+ if (sResult)
+ *sResult = response.sData;
return S_OK;
}
- HRESULT setVariable(const VariableRequest &v, const double value, bool create = false)
+ HRESULT setVariable(const VariableRequest &v, const double value)
{
- const string sValue = buildVariableCommandString(v, true);
- if (sValue.empty() || sValue.length() >= STRSZ_CMD)
- return E_INVALIDARG;
- return sendServerCommand(Command(create ? CommandId::SetCreate : CommandId::Set, v.variableType, sValue.c_str(), value));
+ if (Utilities::isSettableVariableType(v.variableType)) {
+ const string sValue = buildVariableCommandString(v, true);
+ if (sValue.empty() || sValue.length() >= STRSZ_CMD)
+ return E_INVALIDARG;
+ return sendServerCommand(Command(v.createLVar && v.variableType == 'L' ? CommandId::SetCreate : CommandId::Set, v.variableType, sValue.c_str(), value));
+ }
+ LOG_WRN << "Cannot Set a variable of type '" << v.variableType << "'.";
+ return E_INVALIDARG;
}
#pragma endregion
@@ -971,14 +995,14 @@ class WASimClient::Private
// Writes DataRequest data to the corresponding CDA and waits for an Ack/Nak from server.
// If the DataRequest::requestType == None, the request will be deleted by the server and the response wait is skipped.
- HRESULT sendDataRequest(const DataRequest &req)
+ HRESULT sendDataRequest(const DataRequest &req, bool async)
{
HRESULT hr;
if FAILED(hr = writeDataRequest(req))
return hr;
// check if just deleting an existing request and don't wait around for that response
- if (req.requestType == RequestType::None)
+ if (async || req.requestType == RequestType::None)
return hr;
shared_ptr cv = make_shared();
@@ -1033,7 +1057,7 @@ class WASimClient::Private
return SimConnectHelper::removeClientDataDefinition(hSim, tr->dataId);
}
- HRESULT addOrUpdateRequest(const DataRequest &req)
+ HRESULT addOrUpdateRequest(const DataRequest &req, bool async)
{
if (req.requestType == RequestType::None)
return removeRequest(req.requestId);
@@ -1082,7 +1106,7 @@ class WASimClient::Private
hr = registerDataRequestArea(tr, isNewRequest, dataAllocationChanged);
if SUCCEEDED(hr) {
// send the request and wait for Ack; Request may timeout or return a Nak.
- hr = sendDataRequest(req);
+ hr = sendDataRequest(req, async);
}
if (FAILED(hr) && isNewRequest) {
// delete a new request if anything failed
@@ -1248,7 +1272,8 @@ class WASimClient::Private
case SIMCONNECT_RECV_ID_CLIENT_DATA: {
SIMCONNECT_RECV_CLIENT_DATA* data = (SIMCONNECT_RECV_CLIENT_DATA*)pData;
LOG_TRC << LOG_SC_RCV_CLIENT_DATA(data);
- const size_t dataSize = (size_t)pData->dwSize + 4 - sizeof(SIMCONNECT_RECV_CLIENT_DATA); // dwSize reports 4 bytes less than actual size of SIMCONNECT_RECV_CLIENT_DATA
+ // dwSize always under-reports by 4 bytes when sizeof(SIMCONNECT_RECV_CLIENT_DATA) is subtracted, and the minimum reported size is 4 bytes even for 0-3 bytes of actual data.
+ const size_t dataSize = (size_t)pData->dwSize + 4 - sizeof(SIMCONNECT_RECV_CLIENT_DATA);
switch (data->dwRequestID)
{
case DATA_REQ_RESPONSE: {
@@ -1355,12 +1380,11 @@ class WASimClient::Private
LOG_WRN << "DataRequest ID " << data->dwRequestID - SIMCONNECTID_LAST << " not found in tracked requests.";
return;
}
- // be paranoid
- if (dataSize != tr->dataSize) {
+ // be paranoid; note that the reported pData->dwSize is never less than 4 bytes.
+ if (dataSize < tr->dataSize) {
LOG_CRT << "Invalid data result size! Expected " << tr->dataSize << " but got " << dataSize;
return;
}
- //unique_lock lock(mtxRequests);
unique_lock datalock(tr->m_dataMutex);
memcpy(tr->data.data(), (void*)&data->dwData, tr->dataSize);
tr->lastUpdate = chrono::duration_cast(chrono::system_clock::now().time_since_epoch()).count();
@@ -1387,8 +1411,6 @@ class WASimClient::Private
serverVersion = data->dwData;
serverLastSeen = Clock::now();
LOG_DBG << "Got ping response at " << Utilities::timePointToString(serverLastSeen.load()) << " with version " << STREAM_HEX8(serverVersion);
- if (serverVersion != WSMCMND_VERSION)
- LOG_WRN << "Server version " << STREAM_HEX8(serverVersion) << " does not match WASimClient version " << STREAM_HEX8(WSMCMND_VERSION);
break;
default:
@@ -1526,38 +1548,41 @@ HRESULT WASimClient::executeCalculatorCode(const std::string &code, CalcResultTy
#pragma region Variable accessors ----------------------------------------------
-HRESULT WASimClient::getVariable(const VariableRequest & variable, double * pfResult)
+HRESULT WASimClient::getVariable(const VariableRequest & variable, double * pfResult, std::string *psResult)
{
if (variable.variableId > -1 && !Utilities::isIndexedVariableType(variable.variableType)) {
LOG_ERR << "Cannot get variable type '" << variable.variableType << "' by index.";
return E_INVALIDARG;
}
- return d->getVariable(variable, pfResult);
+ return d->getVariable(variable, pfResult, psResult);
}
-HRESULT WASimClient::getLocalVariable(const std::string &variableName, double * pfResult) {
- return d->getVariable(VariableRequest('L', variableName), pfResult);
+HRESULT WASimClient::getLocalVariable(const std::string &variableName, double *pfResult, const std::string &unitName) {
+ return d->getVariable(VariableRequest(variableName, false, unitName), pfResult);
}
+HRESULT WASimClient::getOrCreateLocalVariable(const std::string &variableName, double *pfResult, double defaultValue, const std::string &unitName) {
+ return d->getVariable(VariableRequest(variableName, true, unitName), pfResult, nullptr, defaultValue);
+}
HRESULT WASimClient::setVariable(const VariableRequest & variable, const double value) {
return d->setVariable(variable, value);
}
-HRESULT WASimClient::setLocalVariable(const std::string &variableName, const double value) {
- return d->setVariable(VariableRequest('L', variableName), value, false);
+HRESULT WASimClient::setLocalVariable(const std::string &variableName, const double value, const std::string &unitName) {
+ return d->setVariable(VariableRequest(variableName, false, unitName), value);
}
-HRESULT WASimClient::setOrCreateLocalVariable(const std::string &variableName, const double value) {
- return d->setVariable(VariableRequest('L', variableName), value, true);
+HRESULT WASimClient::setOrCreateLocalVariable(const std::string &variableName, const double value, const std::string &unitName) {
+ return d->setVariable(VariableRequest(variableName, true, unitName), value);
}
#pragma endregion
#pragma region Data Requests ----------------------------------------------
-HRESULT WASimClient::saveDataRequest(const DataRequest &request) {
- return d->addOrUpdateRequest(request);
+HRESULT WASimClient::saveDataRequest(const DataRequest &request, bool async) {
+ return d->addOrUpdateRequest(request, async);
}
HRESULT WASimClient::removeDataRequest(const uint32_t requestId) {
@@ -1607,7 +1632,14 @@ vector WASimClient::dataRequestIdsList() const
}
HRESULT WASimClient::setDataRequestsPaused(bool paused) const {
- return d_const->sendServerCommand(Command(CommandId::Subscribe, (paused ? 0 : 1)));
+ if (isConnected()) {
+ HRESULT hr = d_const->sendServerCommand(Command(CommandId::Subscribe, (paused ? 0 : 1)));
+ if SUCCEEDED(hr)
+ d->requestsPaused = paused;
+ return hr;
+ }
+ d->requestsPaused = paused;
+ return S_OK;
}
#pragma endregion Data
diff --git a/src/WASimClient_CLI/AssemblyInfo.cpp b/src/WASimClient_CLI/AssemblyInfo.cpp
index 7e6462d..c4989cc 100644
--- a/src/WASimClient_CLI/AssemblyInfo.cpp
+++ b/src/WASimClient_CLI/AssemblyInfo.cpp
@@ -1,4 +1,4 @@
-
+
// THIS FILE IS GENERATED BY A SCRIPT, CHANGES WILL NOT PERSIST. EDIT THE CORRESPONDING .in TEMPLATE FILE INSTEAD.
using namespace System;
@@ -22,8 +22,8 @@ using namespace System::Security::Permissions;
[assembly:AssemblyTrademarkAttribute(L"")];
[assembly:AssemblyCultureAttribute(L"")];
-[assembly:AssemblyVersionAttribute(L"1.1.2.0")];
-[assembly:AssemblyFileVersionAttribute("1.1.2.0")];
-[assembly:AssemblyInformationalVersionAttribute("1.1.2.0")];
+[assembly:AssemblyVersionAttribute(L"1.2.0.0")];
+[assembly:AssemblyFileVersionAttribute("1.2.0.0")];
+[assembly:AssemblyInformationalVersionAttribute("1.2.0.0")];
[assembly:ComVisible(false)];
diff --git a/src/WASimClient_CLI/Structs.h b/src/WASimClient_CLI/Structs.h
index 1fa01ff..16e5e5a 100644
--- a/src/WASimClient_CLI/Structs.h
+++ b/src/WASimClient_CLI/Structs.h
@@ -34,6 +34,9 @@ using namespace System::Collections::Generic;
using namespace System::Runtime::InteropServices;
using namespace msclr::interop;
+// We need this to ignore the errors about "unknown" pragmas which actually work to suppress bogus Intellisense errors. Yeah.
+#pragma warning(disable:4068)
+
/// WASimCommander::CLI::Structs namespace.
/// CLI/.NET versions of WASimCommander API and Client data structures.
namespace WASimCommander::CLI::Structs
@@ -148,9 +151,11 @@ namespace WASimCommander::CLI::Structs
explicit Command(CommandId id) : commandId(id) { }
explicit Command(CommandId id, uint32_t uData) : uData(uData), commandId(id) { }
explicit Command(CommandId id, uint32_t uData, double fData) : uData(uData), fData(fData), commandId(id) { }
+#pragma diag_suppress 144 // a value of type "System::String ^" cannot be used to initialize an entity of type "unsigned char" (sData is not a uchar... someone's confused)
explicit Command(CommandId id, uint32_t uData, String ^sData) : uData(uData), fData(0.0), commandId(id), sData{sData} { }
explicit Command(CommandId id, uint32_t uData, String ^sData, double fData) : uData(uData), fData(fData), commandId(id), sData{sData} { }
explicit Command(CommandId id, uint32_t uData, String ^sData, double fData, int32_t token) : token(token), uData(uData), fData(fData), commandId(id), sData{sData} { }
+#pragma diag_restore 144
void setStringData(String ^sData)
{
@@ -250,16 +255,19 @@ namespace WASimCommander::CLI::Structs
requestType(RequestType::Calculated), calcResultType(resultType), nameOrCode(calculatorCode)
{ }
+ /// Set the `nameOrCode` member using a `string` type value.
void setNameOrCode(String ^nameOrCode)
{
this->nameOrCode = char_array(nameOrCode);
}
+ /// Set the `unitName` member using a `string` type value.
void setUnitName(String ^unitName)
{
this->unitName = char_array(unitName);
}
+ /// Serializes this `DataRequest` to a string for debugging purposes.
String ^ToString() override
{
String ^str = String::Format(
@@ -319,11 +327,14 @@ namespace WASimCommander::CLI::Structs
array ^data {}; ///< Value data array.
- /// Tries to populate a value reference of the desired type and returns true or false
+ /// Tries to populate a value reference of the desired type `T` and returns true or false
/// depending on if the conversion was valid (meaning the size of requested type matches the data size).
/// If the conversion fails, result is default-initialized.
- /// The requested type must be a `value` type (not reference) and be default-constructible, (eg. numerics, chars), or fixed-size arrays of such types.
- generic where T : value class, gcnew()
+ /// The requested type (`T`) must be a `value` type (not reference) and be default-constructible, (eg. numerics, chars), or fixed-size arrays of such types.
+ generic
+#if !DOXYGEN
+ where T : value class, gcnew()
+#endif
inline bool tryConvert([Out] T %result)
{
if (data->Length == (int)sizeof(T)) {
@@ -349,7 +360,8 @@ namespace WASimCommander::CLI::Structs
return true;
}
- // Implicit conversion operators for various types
+ /// \name Implicit conversion operators for various types.
+ /// \{
inline static operator double(DataRequestRecord ^dr) { return dr->toType(); }
inline static operator float(DataRequestRecord ^dr) { return dr->toType(); }
inline static operator int64_t(DataRequestRecord ^dr) { return dr->toType(); }
@@ -361,6 +373,7 @@ namespace WASimCommander::CLI::Structs
inline static operator int8_t(DataRequestRecord ^dr) { return dr->toType(); }
inline static operator uint8_t(DataRequestRecord ^dr) { return dr->toType(); }
inline static operator String ^(DataRequestRecord ^dr) { return dr->toType(); }
+ /// \}
// can't get generic to work
//generic where T : value class, gcnew()
@@ -372,6 +385,8 @@ namespace WASimCommander::CLI::Structs
// return ret;
//}
+ /// Serializes this `DataRequestRecord` to string for debugging purposes.
+ /// To return the request's _value_ as a string, see `tryConvert()` or the `String ^()` operator. \sa DataRequest::ToString()
String ^ToString() override {
return String::Format(
"{0}; DataRequestRecord {{Last Update: {1}; Data: {2}}}",
@@ -445,8 +460,10 @@ namespace WASimCommander::CLI::Structs
explicit ListResult(const WASimCommander::Client::ListResult &r) :
listType{(LookupItemType)r.listType}, result(r.result), list{gcnew ListCollectionType((int)r.list.size()) }
{
+#pragma diag_suppress 2242 // for list[] operator: expression must have pointer-to-object or handle-to-C++/CLI-array type but it has type "ListCollectionType ^" (um... isn't `list` a pointer?)
for (const auto &pr : r.list)
- list->Add(pr.first, gcnew String(pr.second.c_str()));
+ list[pr.first] = gcnew String(pr.second.c_str());
+#pragma diag_default 2242
}
};
@@ -494,6 +511,7 @@ namespace WASimCommander::CLI::Structs
int variableId { -1 };
int unitId { -1 };
Byte simVarIndex { 0 };
+ bool createLVar = false;
VariableRequest() {}
/// Construct a variable request for specified variable type ('A', 'L', etc) and variable name.
@@ -517,6 +535,14 @@ namespace WASimCommander::CLI::Structs
/// Construct a variable request a Local variable ('L') with the specified name.
explicit VariableRequest(String ^localVariableName) :
variableType{'L'}, variableName{localVariableName} { }
+ /// Construct a variable request for a Local ('L') variable with the specified name.
+ /// `createVariable` will create the L var on the simulator if it doesn't exist yet (for "Get" as well as "Set" commands). An optional unit name can also be provided.
+ explicit VariableRequest(String ^localVariableName, bool createVariable) :
+ variableType{'L'}, variableName{localVariableName}, createLVar{createVariable} { }
+ /// Construct a variable request for a Local ('L') variable with the specified name.
+ /// `createVariable` will create the L var on the simulator if it doesn't exist yet (for "Get" as well as "Set" commands). An unit name can also be provided with this overload.
+ explicit VariableRequest(String ^localVariableName, bool createVariable, String ^unitName) :
+ variableType{'L'}, variableName{localVariableName}, unitName{unitName}, createLVar{createVariable} { }
/// Construct a variable request a Local variable ('L') with the specified numeric ID.
explicit VariableRequest(int localVariableId) :
variableType{'L'}, variableId{localVariableId} { }
@@ -532,7 +558,7 @@ namespace WASimCommander::CLI::Structs
inline operator WASimCommander::Client::VariableRequest()
{
marshal_context mc;
- WASimCommander::Client::VariableRequest r((char)variableType, mc.marshal_as(variableName), mc.marshal_as(unitName), simVarIndex);
+ WASimCommander::Client::VariableRequest r((char)variableType, mc.marshal_as(variableName), mc.marshal_as(unitName), simVarIndex, createLVar);
r.variableId = variableId;
r.unitId = unitId;
return r;
diff --git a/src/WASimClient_CLI/WASimClient_CLI.cpp b/src/WASimClient_CLI/WASimClient_CLI.cpp
index f234258..720e6f5 100644
--- a/src/WASimClient_CLI/WASimClient_CLI.cpp
+++ b/src/WASimClient_CLI/WASimClient_CLI.cpp
@@ -145,6 +145,24 @@ ref class WASimClient::Private
return (HR)hr;
}
+ inline HR getVariable(VariableRequest ^var, interior_ptr pfResult, interior_ptr psResult)
+ {
+ pin_ptr pf = pfResult;
+ std::string s { };
+ HR ret = (HR)client->getVariable(var, pf, psResult ? &s : nullptr);
+ if (psResult)
+ *psResult = marshal_as(s);
+ return ret;
+ }
+
+ inline HR getOrCreateLocalVariable(String ^ name, interior_ptr unit, double defaultValue, interior_ptr pfResult)
+ {
+ pin_ptr pf = pfResult;
+ if (unit)
+ return (HR)client->getOrCreateLocalVariable(marshal_as(name), pf, defaultValue, marshal_as((String ^)*unit));
+ return (HR)client->getOrCreateLocalVariable(marshal_as(name), pf, defaultValue);
+ }
+
~Private()
{
if (client) {
@@ -197,18 +215,38 @@ WASimClient::!WASimClient()
m_client = nullptr;
}
-inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, [Out] double % pfResult) {
+inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, double %pfResult) {
return d->executeCalculatorCode(code, resultType, &pfResult, nullptr);
}
-inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, [Out] String ^% psResult) {
+inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, String^ %psResult) {
return d->executeCalculatorCode(code, resultType, nullptr, &psResult);
}
-inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, [Out] double % pfResult, [Out] String ^% psResult) {
+inline HR WASimClient::executeCalculatorCode(String ^ code, CalcResultType resultType, double %pfResult, String^ %psResult) {
return d->executeCalculatorCode(code, resultType, &pfResult, &psResult);
}
+inline HR WASimClient::getVariable(VariableRequest ^ var, double %pfResult) {
+ return d->getVariable(var, &pfResult, nullptr);
+}
+
+inline HR WASimClient::getVariable(VariableRequest ^ var, String^ %psResult) {
+ return d->getVariable(var, nullptr, &psResult);
+}
+
+inline HR WASimClient::getVariable(VariableRequest ^ var, double %pfResult, String^ %psResult) {
+ return d->getVariable(var, &pfResult, &psResult);
+}
+
+inline HR WASimClient::getOrCreateLocalVariable(String ^variableName, double defaultValue, double %pfResult) {
+ return d->getOrCreateLocalVariable(variableName, nullptr, defaultValue, &pfResult);
+}
+
+inline HR WASimClient::getOrCreateLocalVariable(String ^variableName, String ^unitName, double defaultValue, double %pfResult) {
+ return d->getOrCreateLocalVariable(variableName, &unitName, defaultValue, &pfResult);
+}
+
inline array^ WASimClient::dataRequests()
{
const std::vector &res = m_client->dataRequests();
diff --git a/src/WASimClient_CLI/WASimClient_CLI.h b/src/WASimClient_CLI/WASimClient_CLI.h
index 33dd6a6..d7ccff8 100644
--- a/src/WASimClient_CLI/WASimClient_CLI.h
+++ b/src/WASimClient_CLI/WASimClient_CLI.h
@@ -52,18 +52,25 @@ namespace WASimCommander::CLI::Client
///
/// The main difference is that callbacks from the C++ version are delivered here as managed Events, with defined delegate types to handle them.
/// And unlike the callback system, the events can have multiple subscribers if needed.
+ ///
+ /// \note Events are delivered asyncronously from a separtely running thread. The event handlers should be reentrant since they could be callled at any time. \n
+ /// Typically, interactions with GUI components will not be possible directly from inside the event handlers -- use a `Dispatcher` to marshal GUI interactions
+ /// back to the main thread.
public ref class WASimClient
{
public:
// Delegates -----------------------------------
+ /// \name Event handler delegate types
+ /// \{
delegate void ClientEventDelegate(ClientEvent ^); ///< Event delegate for Client events (`OnClientEvent`)
delegate void ListResultsDelegate(ListResult ^); ///< Event delegate for delivering list results, eg. of local variables sent from Server (`OnListResults`).
delegate void DataDelegate(DataRequestRecord ^); ///< Event delegate for subscription result data (`OnDataReceived`).
delegate void LogDelegate(LogRecord ^, LogSource); ///< Event delegate for log entries (from both Client and Server) (`OnLogRecordReceived`).
delegate void CommandResultDelegate(Command ^); ///< Event delegate for command responses returned from server (`OnCommandResult`).
delegate void ResponseDelegate(Command ^); ///< Event delegate for all Command structures received from server (`OnResponseReceived`).
+ /// \}
// Events -----------------------------------
@@ -83,18 +90,26 @@ namespace WASimCommander::CLI::Client
#undef DELEGATE_DECL
/// Construct a new client with the given ID. The ID must be unique among any other possible clients and cannot be zero.
- /// See \refwcc{WASimClient::WASimClient()} for more details.
+ /// See \refwccc{WASimClient()} for more details.
explicit WASimClient(UInt32 clientId);
/// Construct a new client with the given ID and with initial settings read from the file specified in `configFile` (.ini format, see default file for example).
- /// The client ID must be unique among any other possible clients and cannot be zero. See \refwcc{WASimClient::WASimClient()} for more details.
+ /// The client ID must be unique among any other possible clients and cannot be zero. See \refwccc{WASimClient()} for more details.
explicit WASimClient(UInt32 clientId, String ^configFile);
- /// This class implements a Disposable type object and should be disposed-of appropriately.
+#if DOXYGEN
+ /// This class implements a Disposable type object and should be disposed-of appropriately by calling `client.Dispose()` when the instance is no longer needed.
/// Any open network connections are automatically closed upon destruction, though it is better to close them yourself before deleting the client.
+ void Dispose();
+#else
~WASimClient();
- !WASimClient(); ///< \private
+ !WASimClient();
+#endif
// Status -----------------------------------
+ /// \name Network actions, status, and settings
+ /// \{
+
+ /// Get current connection status of this client. \sa WASimCommander::Client::ClientStatus
ClientStatus status() { return (ClientStatus)m_client->status(); }
/// Returns true if connected to the Simulator (SimConnect).
bool isInitialized() { return m_client->isInitialized(); }
@@ -109,92 +124,131 @@ namespace WASimCommander::CLI::Client
/// Connect to the Simulator engine on a local connection.
/// (optional) Maximum time to wait for response, in milliseconds. Zero (default) means to use the `defaultTimeout()` value.
- /// \return See \refwcc{connectSimulator(uint32_t)}
+ /// \return See \refwccc{connectSimulator(uint32_t)}
HR connectSimulator([Optional] Nullable timeout) { return (HR)m_client->connectSimulator(timeout.HasValue ? timeout.Value : 0); }
/// Connect to the Simulator engine using a specific network configuration ID from a SimConnect.cfg file. The file must be in the same folder as the executable running the Client.
/// network configuration ID from a SimConnect.cfg file. The file must be in the same folder as the executable running the Client. \n
/// (optional) Maximum time to wait for response, in milliseconds. Zero (default) means to use the `defaultTimeout()` value.
- /// \return See \refwcc{connectSimulator(int,uint32_t)}
+ /// \return See \refwccc{connectSimulator(int,uint32_t)}
HR connectSimulator(int networkConfigId, [Optional] Nullable timeout) { return (HR)m_client->connectSimulator(networkConfigId, timeout.HasValue ? timeout.Value : 0); }
+ /// See \refwccc{disconnectSimulator()}
void disconnectSimulator() { m_client->disconnectSimulator(); }
+ /// See \refwccc{pingServer()}
uint32_t pingServer([Optional] Nullable timeout) { return m_client->pingServer(timeout.HasValue ? timeout.Value : 0); }
+ /// See \refwccc{connectServer()}
HR connectServer([Optional] Nullable timeout) { return (HR)m_client->connectServer(timeout.HasValue ? timeout.Value : 0); }
- void disconnectServer() { m_client->disconnectServer(); }
+ void disconnectServer() { m_client->disconnectServer(); } ///< See \refwccc{disconnectServer()}
// Settings -----------------------------------
- uint32_t defaultTimeout() { return m_client->defaultTimeout(); }
- void setDefaultTimeout(uint32_t ms) { m_client->setDefaultTimeout(ms); }
- int networkConfigurationId() { return m_client->networkConfigurationId(); }
- void setNetworkConfigurationId(int configId) { m_client->setNetworkConfigurationId(configId); }
+ uint32_t defaultTimeout() { return m_client->defaultTimeout(); } ///< See \refwccc{defaultTimeout()}
+ void setDefaultTimeout(uint32_t ms) { m_client->setDefaultTimeout(ms); } ///< See \refwccc{setDefaultTimeout()}
+ int networkConfigurationId() { return m_client->networkConfigurationId(); } ///< See \refwccc{networkConfigurationId()}
+ void setNetworkConfigurationId(int configId) { m_client->setNetworkConfigurationId(configId); } ///< See \refwccc{setNetworkConfigurationId()}
+
+ /// \}
+ /// \name High level API
+ /// \{
// Calculator code -----------------------------------
- /// Execute calculator code without result
+ /// Execute calculator code without result \sa \refwccc{executeCalculatorCode()}
HR executeCalculatorCode(String^ code) { return (HR)m_client->executeCalculatorCode(marshal_as(code)); }
- /// Execute calculator code with a numeric result type. \private
+ /// Execute calculator code with a numeric result type. \sa \refwccc{executeCalculatorCode()}
HR executeCalculatorCode(String^ code, CalcResultType resultType, [Out] double %pfResult);
- /// Execute calculator code with a string result type. \private
+ /// Execute calculator code with a string result type. \sa \refwccc{executeCalculatorCode()}
HR executeCalculatorCode(String^ code, CalcResultType resultType, [Out] String^ %psResult);
- /// Execute calculator code with both numeric and string results. \private
+ /// Execute calculator code with both numeric and string results. \sa \refwccc{executeCalculatorCode()}
HR executeCalculatorCode(String^ code, CalcResultType resultType, [Out] double %pfResult, [Out] String^ %psResult);
// Variables accessors ------------------------------
- HR getVariable(VariableRequest ^var, [Out] double %pfResult) {
- pin_ptr pf = &pfResult;
- return (HR)m_client->getVariable(var, pf);
- }
+ /// Get the value of a variable with a numeric result type. This is the most typical use case since most variable types are numeric. \sa \refwccc{getVariable()}
+ HR getVariable(VariableRequest ^var, [Out] double %pfResult);
+ /// Get the value of a variable with a string result type. The request is executed as calculator code since that is the only way to get string results.
+ /// Note that only some 'A', 'C', and 'T' type variables can have a string value type in the first place. \sa \refwccc{getVariable()}
+ HR getVariable(VariableRequest ^var, [Out] String^ %psResult);
+ /// Get the value of a variable with _either_ a numeric or string result based on the unit type of the requested variable.
+ /// The request is executed as calculator code since that is the only way to get string results. Unlike `executeCalculatorCode()`, this method will not return a string representation of a numeric value.
+ /// Note that only some 'A', 'C', and 'T' type variables can have a string value type in the first place. \sa \refwccc{getVariable()}
+ HR getVariable(VariableRequest ^var, [Out] double %pfResult, [Out] String^ %psResult); ///< See \refwccc{getVariable()}
+
+ /// See \refwccc{getLocalVariable()}
HR getLocalVariable(String ^variableName, [Out] double %pfResult) { return getVariable(gcnew VariableRequest(variableName), pfResult); }
-
+ /// See \refwccc{getLocalVariable()}
+ HR getLocalVariable(String ^variableName, String ^unitName, [Out] double %pfResult) { return getVariable(gcnew VariableRequest(variableName, false, unitName), pfResult); }
+ /// \sa \refwccc{getOrCreateLocalVariable()}
+ HR getOrCreateLocalVariable(String ^variableName, double defaultValue, [Out] double %pfResult);
+ /// \sa \refwccc{getOrCreateLocalVariable()}
+ HR getOrCreateLocalVariable(String ^variableName, String ^unitName, double defaultValue, [Out] double %pfResult);
+
+ /// See \refwccc{setVariable()}
HR setVariable(VariableRequest ^var, const double value) { return (HR)m_client->setVariable(var, value); }
+ /// See \refwccc{setLocalVariable()}
HR setLocalVariable(String ^variableName, const double value) { return (HR)m_client->setLocalVariable(marshal_as(variableName), value); }
+ HR setLocalVariable(String ^variableName, String ^unitName, const double value) {
+ return (HR)m_client->setLocalVariable(marshal_as(variableName), value, marshal_as(unitName));
+ }
+ /// See \refwccc{setOrCreateLocalVariable()}
HR setOrCreateLocalVariable(String ^variableName, const double value) { return (HR)m_client->setOrCreateLocalVariable(marshal_as(variableName), value); }
+ /// See \refwccc{setOrCreateLocalVariable()}
+ HR setOrCreateLocalVariable(String ^variableName, String ^unitName, const double value) {
+ return (HR)m_client->setOrCreateLocalVariable(marshal_as(variableName), value, marshal_as(unitName));
+ }
// Data subscriptions -------------------------------
- HR saveDataRequest(DataRequest ^request) { return (HR)m_client->saveDataRequest(request); }
- HR removeDataRequest(const uint32_t requestId) { return (HR)m_client->removeDataRequest(requestId); }
- HR updateDataRequest(uint32_t requestId) { return (HR)m_client->updateDataRequest(requestId); }
+ HR saveDataRequest(DataRequest ^request) { return (HR)m_client->saveDataRequest(request); } ///< See \refwccc{saveDataRequest()} as used with `async = false`
+ HR saveDataRequestAsync(DataRequest ^request) { return (HR)m_client->saveDataRequest(request, true); } ///< See \refwccc{saveDataRequest()} as used with `async = true`
+ HR removeDataRequest(const uint32_t requestId) { return (HR)m_client->removeDataRequest(requestId); } ///< See \refwccc{removeDataRequest()}
+ HR updateDataRequest(uint32_t requestId) { return (HR)m_client->updateDataRequest(requestId); } ///< See \refwccc{updateDataRequest()}
- DataRequestRecord ^dataRequest(uint32_t requestId) { return gcnew DataRequestRecord(m_client->dataRequest(requestId)); }
- array ^dataRequests();
- array ^dataRequestIdsList();
+ DataRequestRecord ^dataRequest(uint32_t requestId) { return gcnew DataRequestRecord(m_client->dataRequest(requestId)); } ///< See \refwccc{dataRequest()}
+ array ^dataRequests(); ///< See \refwccc{dataRequests()}
+ array ^dataRequestIdsList(); ///< See \refwccc{dataRequestIdsList()}
- HR setDataRequestsPaused(bool paused) { return (HR)m_client->setDataRequestsPaused(paused); }
+ HR setDataRequestsPaused(bool paused) { return (HR)m_client->setDataRequestsPaused(paused); } ///< See \refwccc{setDataRequestsPaused()}
// Custom Event registration --------------------------
- HR registerEvent(RegisteredEvent ^eventData) { return (HR)m_client->registerEvent(eventData); }
- HR removeEvent(uint32_t eventId) { return (HR)m_client->removeEvent(eventId); }
- HR transmitEvent(uint32_t eventId) { return (HR)m_client->transmitEvent(eventId); }
+ HR registerEvent(RegisteredEvent ^eventData) { return (HR)m_client->registerEvent(eventData); } ///< See \refwccc{registerEvent()}
+ HR removeEvent(uint32_t eventId) { return (HR)m_client->removeEvent(eventId); } ///< See \refwccc{removeEvent()}
+ HR transmitEvent(uint32_t eventId) { return (HR)m_client->transmitEvent(eventId); } ///< See \refwccc{transmitEvent()}
- RegisteredEvent ^registeredEvent(uint32_t eventId) { return gcnew RegisteredEvent(m_client->registeredEvent(eventId)); }
- array ^registeredEvents();
+ RegisteredEvent ^registeredEvent(uint32_t eventId) { return gcnew RegisteredEvent(m_client->registeredEvent(eventId)); } ///< See \refwccc{registeredEvent()}
+ array ^registeredEvents(); ///< See \refwccc{registeredEvents()}
// Simulator Key Events ------------------------------
+ /// See \refwccc{sendKeyEvent(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) const}
HR sendKeyEvent(uint32_t keyEventId, [Optional] Nullable v1, [Optional] Nullable v2, [Optional] Nullable v3, [Optional] Nullable v4, [Optional] Nullable v5) {
return (HR)m_client->sendKeyEvent(keyEventId, v1.GetValueOrDefault(0), v2.GetValueOrDefault(0), v3.GetValueOrDefault(0), v4.GetValueOrDefault(0), v5.GetValueOrDefault(0));
}
+ /// See \refwccc{sendKeyEvent(const std::string&, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t)}
HR sendKeyEvent(String ^keyEventName, [Optional] Nullable v1, [Optional] Nullable v2, [Optional] Nullable v3, [Optional] Nullable v4, [Optional] Nullable v5) {
return (HR)m_client->sendKeyEvent(marshal_as(keyEventName), v1.GetValueOrDefault(0), v2.GetValueOrDefault(0), v3.GetValueOrDefault(0), v4.GetValueOrDefault(0), v5.GetValueOrDefault(0));
}
// Meta data retrieval --------------------------------
+ /// See \refwccc{list()}
HR list(LookupItemType itemsType) { return (HR)m_client->list((WSE::LookupItemType)itemsType); }
+ /// See \refwccc{lookup()}
HR lookup(LookupItemType itemType, String ^itemName, [Out] Int32 %piResult)
{
pin_ptr pi = &piResult;
return (HR)m_client->lookup((WSE::LookupItemType)itemType, marshal_as(itemName), pi);
}
- // Low level API --------------------------------
+ /// \}
+ /// \name Low level API
+ /// \{
+ /// See \refwccc{sendCommand()}
HR sendCommand(Command ^command) { return (HR)m_client->sendCommand(command); }
+ /// See \refwccc{sendCommandWithResponse()}
HR sendCommandWithResponse(Command ^command, [Out] Command^ %response, [Optional] Nullable timeout)
{
WASimCommander::Command resp {};
@@ -203,15 +257,21 @@ namespace WASimCommander::CLI::Client
return (HR)hr;
}
- // Logging settings --------------------------------
+ /// \}
+ /// \name Logging settings
+ /// \{
+ /// See \refwccc{logLevel()}
LogLevel logLevel(LogFacility facility, LogSource source) {
return (LogLevel)m_client->logLevel((WSE::LogFacility)facility, (WASimCommander::Client::LogSource)source);
}
+ /// See \refwccc{setLogLevel()}
void setLogLevel(LogLevel level, LogFacility facility, LogSource source) {
m_client->setLogLevel((WSE::LogLevel)level, (WSE::LogFacility)facility, (WASimCommander::Client::LogSource)source);
}
+ /// \}
+
private:
WASimCommander::Client::WASimClient *m_client = nullptr;
diff --git a/src/WASimClient_CLI/WASimClient_CLI.vcxproj b/src/WASimClient_CLI/WASimClient_CLI.vcxproj
index 280e67b..b50c9aa 100644
--- a/src/WASimClient_CLI/WASimClient_CLI.vcxproj
+++ b/src/WASimClient_CLI/WASimClient_CLI.vcxproj
@@ -9,6 +9,10 @@
Release-net6x64
+
+ Release-net7
+ x64
+ Release-netfwx64
@@ -56,6 +60,16 @@
x64true
+
+ false
+ v143
+ NetCore
+ net7.0
+ DynamicLibrary
+ Unicode
+ x64
+ true
+ falsev143
@@ -83,6 +97,10 @@
+
+
+
+
@@ -108,6 +126,13 @@
$(MSFS_SDK)\SimConnect SDK\include;$(IncludePath)WASimCommander.WASimClient
+
+ true
+ true
+ false
+ $(MSFS_SDK)\SimConnect SDK\include;$(IncludePath)
+ WASimCommander.WASimClient
+ truefalse
@@ -192,7 +217,7 @@
$(ProjectDir)deps.manifest %(AdditionalManifestFiles)
-
+ NotUsingLevel3
@@ -218,6 +243,32 @@
$(ProjectDir)deps.manifest %(AdditionalManifestFiles)
+
+
+ NotUsing
+ Level3
+ NETFRAMEWORK;WSMCMND_API_STATIC;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE_DEBUG;_WINDOWS;NDEBUG;%(PreprocessorDefinitions)
+ stdcpp17
+ stdc17
+ /Zc:__cplusplus /Zc:twoPhase-
+ true
+ Speed
+ true
+ true
+ StdCall
+ MultiThreadedDLL
+ true
+ 4635;%(DisableSpecificWarnings)
+ true
+
+
+ /ignore:4099
+ Default
+
+
+ $(ProjectDir)deps.manifest %(AdditionalManifestFiles)
+
+
diff --git a/src/WASimCommander.sln b/src/WASimCommander.sln
index f3a2cb8..2a1577c 100644
--- a/src/WASimCommander.sln
+++ b/src/WASimCommander.sln
@@ -76,6 +76,8 @@ Global
Release-DLL|x64 = Release-DLL|x64
Release-net6|MSFS = Release-net6|MSFS
Release-net6|x64 = Release-net6|x64
+ Release-net7|MSFS = Release-net7|MSFS
+ Release-net7|x64 = Release-net7|x64
Release-netfw|MSFS = Release-netfw|MSFS
Release-netfw|x64 = Release-netfw|x64
EndGlobalSection
@@ -96,6 +98,9 @@ Global
{A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-DLL|x64.ActiveCfg = Release|MSFS
{A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-net6|MSFS.ActiveCfg = Release|MSFS
{A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-net6|x64.ActiveCfg = Release|MSFS
+ {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-net7|MSFS.ActiveCfg = Release|MSFS
+ {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-net7|MSFS.Build.0 = Release|MSFS
+ {A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-net7|x64.ActiveCfg = Release|MSFS
{A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-netfw|MSFS.ActiveCfg = Release|MSFS
{A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-netfw|MSFS.Build.0 = Release|MSFS
{A5468B35-BBBD-4C55-97ED-81BFE343B0E4}.Release-netfw|x64.ActiveCfg = Release|MSFS
@@ -115,6 +120,10 @@ Global
{639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net6|MSFS.ActiveCfg = Release|x64
{639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net6|x64.ActiveCfg = Release|x64
{639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net6|x64.Build.0 = Release|x64
+ {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net7|MSFS.ActiveCfg = Release-DLL|x64
+ {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net7|MSFS.Build.0 = Release-DLL|x64
+ {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net7|x64.ActiveCfg = Release|x64
+ {639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-net7|x64.Build.0 = Release|x64
{639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-netfw|MSFS.ActiveCfg = Release|x64
{639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-netfw|MSFS.Build.0 = Release|x64
{639093FF-FD94-4E89-92AC-C6FABE5DF664}.Release-netfw|x64.ActiveCfg = Release|x64
@@ -132,6 +141,9 @@ Global
{E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-DLL|x64.ActiveCfg = Release-DLL|x64
{E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-net6|MSFS.ActiveCfg = Release-DLL|x64
{E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-net6|x64.ActiveCfg = Release|x64
+ {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-net7|MSFS.ActiveCfg = Release-DLL|x64
+ {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-net7|MSFS.Build.0 = Release-DLL|x64
+ {E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-net7|x64.ActiveCfg = Release|x64
{E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-netfw|MSFS.ActiveCfg = Release|x64
{E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-netfw|MSFS.Build.0 = Release|x64
{E9AD8656-009B-4A6A-832B-0D19DE288BFC}.Release-netfw|x64.ActiveCfg = Release|x64
@@ -152,6 +164,10 @@ Global
{DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net6|MSFS.Build.0 = Release-net6|x64
{DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net6|x64.ActiveCfg = Release-net6|x64
{DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net6|x64.Build.0 = Release-net6|x64
+ {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net7|MSFS.ActiveCfg = Release-net7|x64
+ {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net7|MSFS.Build.0 = Release-net7|x64
+ {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net7|x64.ActiveCfg = Release-net7|x64
+ {DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-net7|x64.Build.0 = Release-net7|x64
{DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-netfw|MSFS.ActiveCfg = Release|x64
{DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-netfw|MSFS.Build.0 = Release|x64
{DAF5B792-C4E6-4E54-9CBF-0A0335E80306}.Release-netfw|x64.ActiveCfg = Release-netfw|x64
@@ -164,11 +180,14 @@ Global
{5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release|MSFS.ActiveCfg = Release|x64
{5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release|MSFS.Build.0 = Release|x64
{5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release|x64.ActiveCfg = Release|x64
- {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release|x64.Build.0 = Release|x64
{5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-DLL|MSFS.ActiveCfg = Release|x64
{5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-DLL|x64.ActiveCfg = Release|x64
{5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net6|MSFS.ActiveCfg = Release|x64
{5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net6|x64.ActiveCfg = Release|x64
+ {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net6|x64.Build.0 = Release|x64
+ {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net7|MSFS.ActiveCfg = Release|x64
+ {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net7|MSFS.Build.0 = Release|x64
+ {5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-net7|x64.ActiveCfg = Release-net7|x64
{5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-netfw|MSFS.ActiveCfg = Release|x64
{5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-netfw|MSFS.Build.0 = Release|x64
{5B7D7234-D6C8-4D1F-B135-C5297D6476D8}.Release-netfw|x64.ActiveCfg = Release|x64
@@ -187,6 +206,9 @@ Global
{523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-DLL|x64.ActiveCfg = Release-DLL|x64
{523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-net6|MSFS.ActiveCfg = Release-DLL|x64
{523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-net6|x64.ActiveCfg = Release|x64
+ {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-net7|MSFS.ActiveCfg = Release-DLL|x64
+ {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-net7|MSFS.Build.0 = Release-DLL|x64
+ {523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-net7|x64.ActiveCfg = Release|x64
{523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-netfw|MSFS.ActiveCfg = Release|x64
{523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-netfw|MSFS.Build.0 = Release|x64
{523ABD54-4C1A-4F21-8977-5EFA5821F71D}.Release-netfw|x64.ActiveCfg = Release|x64
@@ -200,6 +222,8 @@ Global
{C6D4303F-E717-4257-990B-2CAE894898A0}.Release-DLL|x64.ActiveCfg = Release|Any CPU
{C6D4303F-E717-4257-990B-2CAE894898A0}.Release-net6|MSFS.ActiveCfg = Release|Any CPU
{C6D4303F-E717-4257-990B-2CAE894898A0}.Release-net6|x64.ActiveCfg = Release|Any CPU
+ {C6D4303F-E717-4257-990B-2CAE894898A0}.Release-net7|MSFS.ActiveCfg = Release|Any CPU
+ {C6D4303F-E717-4257-990B-2CAE894898A0}.Release-net7|x64.ActiveCfg = Release|Any CPU
{C6D4303F-E717-4257-990B-2CAE894898A0}.Release-netfw|MSFS.ActiveCfg = Release|Any CPU
{C6D4303F-E717-4257-990B-2CAE894898A0}.Release-netfw|x64.ActiveCfg = Release|Any CPU
EndGlobalSection
diff --git a/src/WASimModule/WASimModule.cpp b/src/WASimModule/WASimModule.cpp
index b2bcdca..8ccc264 100644
--- a/src/WASimModule/WASimModule.cpp
+++ b/src/WASimModule/WASimModule.cpp
@@ -131,11 +131,23 @@ struct TrackedRequest : DataRequest
void checkRequestType()
{
- // Anything besides L/A/T type vars just gets converted to calc code.
- if (requestType == RequestType::Named && variableId < 0 && !Utilities::isIndexedVariableType(varTypePrefix)) {
- const ostringstream codeStr = ostringstream() << "(" << varTypePrefix << ':' << nameOrCode << ')';
+ // Anything besides L/A/T type vars just gets converted to calc code, as well as any A vars with "string" unit type.
+ bool isString = false;
+ if (requestType == RequestType::Named && variableId < 0 &&
+ (!Utilities::isIndexedVariableType(varTypePrefix) || (isString = (unitId < 0 && varTypePrefix == 'A' && !strcasecmp(unitName, "string"))))
+ ) {
+ ostringstream codeStr = ostringstream() << '(' << varTypePrefix << ':' << nameOrCode;
+ if (unitName[0] != '\0')
+ codeStr << ',' << unitName;
+ codeStr << ')';
setNameOrCode(codeStr.str().c_str());
requestType = RequestType::Calculated;
+ if (isString)
+ calcResultType = CalcResultType::String;
+ else if (valueSize > DATA_TYPE_INT64 || valueSize < 4)
+ calcResultType = CalcResultType::Integer;
+ else
+ calcResultType = CalcResultType::Double;
}
}
@@ -215,7 +227,7 @@ struct calcResult_t
string sVal;
void setF(const FLOAT64 val) { fVal = val; resultSize = sizeof(FLOAT64); resultMemberIndex = 0; }
void setI(const SINT32 val) { iVal = val; resultSize = sizeof(SINT32); resultMemberIndex = 1; }
- void setS(const string &&val) { sVal = move(val); sVal.resize(strSize); resultSize = strSize; resultMemberIndex = 2; }
+ void setS(const string &&val) { sVal = std::move(val); sVal.resize(strSize); resultSize = strSize; resultMemberIndex = 2; }
};
typedef map clientMap_t;
@@ -237,7 +249,6 @@ steady_clock::time_point g_tpNextTick { steady_clock::now() };
SIMCONNECT_CLIENT_EVENT_ID g_nextClientEventId = SIMCONNECTID_LAST;
SIMCONNECT_CLIENT_DATA_DEFINITION_ID g_nextClienDataId = SIMCONNECTID_LAST;
bool g_triggersRegistered = false;
-bool g_simConnectQuitEvent = false;
#pragma endregion Globals
//----------------------------------------------------------------------------
@@ -246,6 +257,8 @@ bool g_simConnectQuitEvent = false;
bool sendResponse(const Client *c, const Command &cmd)
{
+ if (c->status != ClientStatus::Connected)
+ return false;
LOG_TRC << "Sending command to " << c->name << ": " << cmd;
return INVOKE_SIMCONNECT(
SetClientData, g_hSimConnect,
@@ -279,6 +292,8 @@ bool sendPing(const Client *c) {
bool sendLogRecord(const Client *c, const LogRecord &log)
{
+ if (c->status != ClientStatus::Connected)
+ return false;
//if (c->logLevel < LogLevel::Trace) LOG_TRC << "Sending LogRecord to " << c->name << ": " << log;
return INVOKE_SIMCONNECT(
SetClientData, g_hSimConnect,
@@ -290,6 +305,8 @@ bool sendLogRecord(const Client *c, const LogRecord &log)
bool writeRequestData(const Client *c, const TrackedRequest *tr, void *data)
{
+ if (c->status != ClientStatus::Connected)
+ return false;
LOG_TRC << "Writing request ID " << tr->requestId << " data for " << c->name << " to CDA / CDD ID " << tr->dataId << " of size " << tr->dataSize;
return INVOKE_SIMCONNECT(
SetClientData, g_hSimConnect,
@@ -464,7 +481,7 @@ Client *getOrCreateClient(uint32_t clientId)
registerClientKeyEventDataArea(&c);
// move client record into map
- Client *pC = &g_mClients.emplace(clientId, move(c)).first->second;
+ Client *pC = &g_mClients.emplace(clientId, std::move(c)).first->second;
// save mappings of the command and request data IDs (which SimConnect sends us) to the client record; for lookup in message dispatch.
g_mDefinitionIds.emplace(piecewise_construct, forward_as_tuple(c.cddID_command), forward_as_tuple(RecordType::CommandData, pC)); // no try_emplace?
g_mDefinitionIds.emplace(piecewise_construct, forward_as_tuple(c.cddID_request), forward_as_tuple(RecordType::RequestData, pC));
@@ -496,7 +513,9 @@ Client *connectClient(uint32_t id)
return connectClient(getOrCreateClient(id));
}
-void checkTriggerEventNeeded(); // forward, in Utility Functions just below
+// forwards, in Utility Functions just below
+void checkTriggerEventNeeded();
+void resumeTriggerEvent();
// marks client as disconnected or timed out and clears any saved requests/events
void disconnectClient(Client *c, ClientStatus newStatus = ClientStatus::Disconnected)
@@ -532,6 +551,12 @@ void disconnectAllClients(const char *message = nullptr)
disconnectClient(&it.second, message, ClientStatus::Disconnected);
}
+void disableAllClients()
+{
+ for (auto &it : g_mClients)
+ it.second.status = ClientStatus::Disconnected;
+}
+
// callback for logfault IdProxyHandler log handler
void CALLBACK clientLogHandler(const uint32_t id, const logfault::Message &msg)
{
@@ -567,19 +592,19 @@ bool setClientLogLevel(Client *c, LogLevel level)
//----------------------------------------------------------------------------
// this runs once we have any requests to keep updated which essentially starts the tick() processing.
-void registerTriggerEvents()
+void resumeTriggerEvent()
{
// use "Frame" event as trigger for our tick() loop
- if FAILED(INVOKE_SIMCONNECT(SubscribeToSystemEvent, g_hSimConnect, (SIMCONNECT_CLIENT_EVENT_ID)EVENT_FRAME, "Frame"))
+ if FAILED(INVOKE_SIMCONNECT(SetSystemEventState, g_hSimConnect, (SIMCONNECT_CLIENT_EVENT_ID)EVENT_FRAME, SIMCONNECT_STATE_ON))
return;
g_triggersRegistered = true;
LOG_INF << "DataRequest data update processing started.";
}
// and here is the opposite... if all clients disconnect we can stop the ticker loop.
-void unregisterTriggerEvents()
+void pauseTriggerEvent()
{
- if FAILED(INVOKE_SIMCONNECT(UnsubscribeFromSystemEvent, g_hSimConnect, (SIMCONNECT_CLIENT_EVENT_ID)EVENT_FRAME))
+ if FAILED(INVOKE_SIMCONNECT(SetSystemEventState, g_hSimConnect, (SIMCONNECT_CLIENT_EVENT_ID)EVENT_FRAME, SIMCONNECT_STATE_OFF))
return;
g_triggersRegistered = false;
LOG_INF << "DataRequest update processing stopped.";
@@ -591,10 +616,11 @@ void unregisterTriggerEvents()
void checkTriggerEventNeeded()
{
for (const clientMap_t::value_type &it : g_mClients) {
- if (it.second.status == ClientStatus::Connected)
+ const Client &c = it.second;
+ if (c.status == ClientStatus::Connected && !c.pauseDataUpdates && c.requests.size())
return;
}
- unregisterTriggerEvents();
+ pauseTriggerEvent();
}
bool execCalculatorCode(const char *code, calcResult_t &result, bool precompiled = false)
@@ -634,11 +660,14 @@ bool getNamedVariableValue(char varType, calcResult_t &result)
case 'L':
if (result.varId < 0)
return false;
- result.setF(get_named_variable_value(result.varId));
+ if (result.unitId > -1)
+ result.setF(get_named_variable_typed_value(result.varId, result.unitId));
+ else
+ result.setF(get_named_variable_value(result.varId));
break;
case 'A':
- if (result.varId < 0)
+ if (result.varId < 0 || result.unitId < 0)
return false;
result.setF(aircraft_varget(result.varId, result.unitId, result.varIndex));
break;
@@ -754,11 +783,11 @@ int getVariableId(char varType, const char *name, bool createLocal = false)
// Parse a command string to find a variable name/unit/index and populates the respective reference params.
// Lookups are done on var names, depending on varType, and unit strings, to attempt conversion to IDs.
// Used by setVariable() and getVariable(). Only handles A/L/T var types (not needed for others).
-bool parseVariableString(const char varType, const char *data, ID &varId, bool createLocal, ENUM *unitId = nullptr, uint8_t *varIndex = nullptr, string *varName = nullptr)
+bool parseVariableString(const char varType, const char *data, ID &varId, bool createLocal, ENUM *unitId = nullptr, uint8_t *varIndex = nullptr, string *varName = nullptr, bool *existed = nullptr)
{
string_view svVar(data, strlen(data)), svUnit{};
- if (varType == 'A') {
+ if (varType != 'T') {
// Check for unit type after variable name/id and comma
const size_t idx = svVar.find(',');
if (idx != string::npos) {
@@ -766,12 +795,12 @@ bool parseVariableString(const char varType, const char *data, ID &varId, bool c
svVar.remove_suffix(svVar.size() - idx);
}
// check for index value at end of SimVar name/ID
- if (svVar.size() > 3) {
+ if (varType == 'A' && svVar.size() > 3) {
const string_view &svIndex = svVar.substr(svVar.size() - 3);
const size_t idx = svIndex.find(':');
if (idx != string::npos) {
// strtoul returns zero if conversion fails, which works fine since zero is not a valid simvar index
- if (varIndex)
+ if (!!varIndex)
*varIndex = strtoul(svIndex.data() + idx + 1, nullptr, 10);
svVar.remove_suffix(3 - idx);
}
@@ -781,18 +810,28 @@ bool parseVariableString(const char varType, const char *data, ID &varId, bool c
if (svVar.empty())
return false;
- if (varName)
- *varName = string(svVar);
// Try to parse the remaining var name string as a numeric ID
auto result = from_chars(svVar.data(), svVar.data() + svVar.size(), varId);
// if number conversion failed, look up variable id
- if (result.ec != errc())
- varId = getVariableId(varType, string(svVar).c_str(), createLocal);
+ if (result.ec != errc()) {
+ const std::string vname(svVar);
+ if (createLocal && !!existed) {
+ varId = check_named_variable(vname.c_str());
+ *existed = varId > -1;
+ if (!*existed)
+ varId = register_named_variable(vname.c_str());
+ }
+ else {
+ varId = getVariableId(varType, vname.c_str(), createLocal);
+ }
+ if (!!varName)
+ *varName = vname;
+ }
// failed to find a numeric id, whole input was invalid
if (varId < 0)
return false;
// check for unit specification
- if (unitId && !svUnit.empty()) {
+ if (!!unitId && !svUnit.empty()) {
// try to parse the string as a numeric ID
result = from_chars(svUnit.data(), svUnit.data() + svUnit.size(), *unitId);
// if number conversion failed, look up unit id
@@ -967,36 +1006,54 @@ void getVariable(const Client *c, const Command *const cmd)
const char varType = char(cmd->uData);
const char *data = cmd->sData;
LOG_TRC << "getVariable(" << varType << ", " << quoted(data) << ") for client " << c->name;
- if (cmd->commandId == CommandId::GetCreate && varType != 'L') {
- logAndNak(c, *cmd, ostringstream() << "The GetCreate command only supports the 'L' variable type.");
- return;
- }
- // Anything besides L/A/T type vars just gets converted to calc code.
- if (!Utilities::isIndexedVariableType(varType)) {
+
+ // Anything besides L/A/T type vars just gets converted to calc code. Also if a "string" type unit A var is requested.
+ size_t datalen;
+ bool isString = false;
+ if (!Utilities::isIndexedVariableType(varType) || (isString = varType == 'A' && (datalen = strlen(data)) > 6 && !strcasecmp(data + datalen-6, "string"))) {
const ostringstream codeStr = ostringstream() << "(" << varType << ':' << data << ')';
- const Command execCmd(cmd->commandId, +CalcResultType::Double, codeStr.str().c_str(), 0.0, cmd->token);
+ CalcResultType ctype = isString ? CalcResultType::String : CalcResultType::Double;
+ const Command execCmd(cmd->commandId, +ctype, codeStr.str().c_str(), 0.0, cmd->token);
return execCalculatorCode(c, &execCmd);
}
+
ID varId{-1};
ENUM unitId{-1};
uint8_t varIndex{0};
string varName;
- if (!parseVariableString(varType, data, varId, (cmd->commandId == CommandId::GetCreate), &unitId, &varIndex, &varName)) {
+ bool existed = true;
+ if (!parseVariableString(varType, data, varId, (cmd->commandId == CommandId::GetCreate && varType == 'L'), &unitId, &varIndex, &varName, &existed)) {
logAndNak(c, *cmd, ostringstream() << "Could not resolve Variable ID for Get command from string " << quoted(data));
return;
}
+ // !existed can only happen for an L var if we just created it. In that case it has a default value and unit type (0.0, number).
+ if (!existed) {
+ if (unitId > -1)
+ set_named_variable_typed_value(varId, cmd->fData, unitId);
+ else if (cmd->fData != 0.0)
+ set_named_variable_value(varId, cmd->fData);
+ sendResponse(c, Command(CommandId::Ack, (uint32_t)CommandId::GetCreate, nullptr, cmd->fData, cmd->token));
+ LOG_DBG << "getVariable(" << quoted(data) << ") created new variable for client " << c->name;
+ return;
+ }
+
+ if (unitId < 0 && varType == 'A') {
+ logAndNak(c, *cmd, ostringstream() << "Could not resolve Unit ID for Get command from string " << quoted(data));
+ return;
+ }
+
calcResult_t res = calcResult_t { CalcResultType::Double, STRSZ_CMD, varId, unitId, varIndex, varName.c_str() };
if (!getNamedVariableValue(varType, res))
- return logAndNak(c, *cmd, ostringstream() << "getNamedVariableValue() returned error result for code " << quoted(data));
- Command resp(CommandId::Ack, (uint32_t)CommandId::Get);
+ return logAndNak(c, *cmd, ostringstream() << "getNamedVariableValue() returned error result for variable: " << quoted(data));
+ Command resp(CommandId::Ack, (uint32_t)cmd->commandId);
resp.token = cmd->token;
switch (res.resultMemberIndex) {
case 0: resp.fData = res.fVal; break;
case 1: resp.fData = res.iVal; break;
case 2: resp.setStringData(res.sVal.c_str()); break;
default:
- return logAndNak(c, *cmd, ostringstream() << "getNamedVariableValue() returned invalid result index " << res.resultMemberIndex);
+ return logAndNak(c, *cmd, ostringstream() << "getNamedVariableValue() for " << quoted(data) << " returned invalid result index: " << res.resultMemberIndex);
}
sendResponse(c, resp);
}
@@ -1007,10 +1064,7 @@ void setVariable(const Client *c, const Command *const cmd)
const char *data = cmd->sData;
const double value = cmd->fData;
LOG_TRC << "setVariable(" << varType << ", " << quoted(data) << ", " << value << ") for client " << c->name;
- if (cmd->commandId == CommandId::SetCreate && varType != 'L') {
- logAndNak(c, *cmd, ostringstream() << "The SetCreate command only supports the 'L' variable type.");
- return;
- }
+
// Anything besides an L var just gets converted to calc code.
if (varType != 'L') {
const ostringstream codeStr = ostringstream() << fixed << setprecision(7) << value << " (>" << varType << ':' << data << ')';
@@ -1019,11 +1073,16 @@ void setVariable(const Client *c, const Command *const cmd)
}
ID varId{-1};
- if (!parseVariableString(varType, data, varId, (cmd->commandId == CommandId::SetCreate))) {
+ ENUM unitId{-1};
+ if (!parseVariableString(varType, data, varId, (cmd->commandId == CommandId::SetCreate), &unitId)) {
logAndNak(c, *cmd, ostringstream() << "Could not resolve Variable ID for Set command from string " << quoted(data));
return;
}
- set_named_variable_value(varId, value);
+
+ if (unitId > -1)
+ set_named_variable_typed_value(varId, value, unitId);
+ else
+ set_named_variable_value(varId, value);
sendAckNak(c, *cmd);
}
@@ -1077,17 +1136,45 @@ bool updateRequestValue(const Client *c, TrackedRequest *tr, bool compareCheck =
// double
case 0:
// convert the value if necessary for proper binary representation
- if (tr->dataSize == sizeof(float))
- data = &(f32 = (float)res.fVal);
- else if (tr->valueSize == DATA_TYPE_INT64) // better way?
- data = &(i64 = (int64_t)res.fVal);
- else
- data = &res.fVal;
+ switch (tr->valueSize) {
+ // ordered most to least likely
+ case DATA_TYPE_DOUBLE:
+ case sizeof(double):
+ data = &res.fVal;
+ break;
+ case DATA_TYPE_FLOAT:
+ case sizeof(float):
+ data = &(f32 = (float)res.fVal);
+ break;
+ case DATA_TYPE_INT32:
+ case 3:
+ data = &(res.iVal = (int32_t)res.fVal);
+ break;
+ case DATA_TYPE_INT8:
+ case 1:
+ data = &(res.iVal = (int8_t)res.fVal);
+ break;
+ case DATA_TYPE_INT16:
+ case 2:
+ data = &(res.iVal = (int16_t)res.fVal);
+ break;
+ case DATA_TYPE_INT64:
+ // the widest integer any gauge API function returns is 48b (for token/MODULE_VAR) so 53b precision is OK here
+ data = &(i64 = (int64_t)res.fVal);
+ break;
+ default:
+ data = &res.fVal;
+ break;
+ }
break;
// int32
- case 1: data = &res.iVal; break;
+ case 1:
+ data = &res.iVal;
+ break;
// string
- case 2: data = (void *)res.sVal.data(); break;
+ case 2:
+ data = (void *)res.sVal.data();
+ break;
}
if (compareCheck && tr->compareCheck && !memcmp(data, tr->data.data(), tr->dataSize)) {
@@ -1112,26 +1199,26 @@ bool removeRequest(Client *c, const uint32_t requestId)
c->requests.erase(tr->requestId);
LOG_DBG << "Deleted DataRequest ID " << requestId;
sendAckNak(c, CommandId::Subscribe, true, requestId);
+ if (g_triggersRegistered && !c->requests.size())
+ checkTriggerEventNeeded(); // check if anyone is still connected
return true;
}
-// returns true if request has been scheduled *and* will require regular updates (period > UpdatePeriod::Once)
+// returns true if request has been scheduled or removed
bool addOrUpdateRequest(Client *c, const DataRequest *const req)
{
LOG_DBG << "Got DataRequest from Client " << c->name << ": " << *req;
// request type of "None" actually means to delete an existing request
- if (req->requestType == RequestType::None) {
- removeRequest(c, req->requestId);
- return false; // no updates needed
- }
+ if (req->requestType == RequestType::None)
+ return removeRequest(c, req->requestId);
// setup response Command for Ack/Nak
const Command resp(CommandId::Subscribe, 0, nullptr, .0, req->requestId);
// check for empty name/code
if (req->nameOrCode[0] == '\0') {
- logAndNak(c, resp, ostringstream() << "Error in DataRequest ID: " << req->requestId << "; Parameter 'nameOrCode' cannot be empty.");
+ logAndNak(c, resp, ostringstream() << "Error in DataRequest ID " << req->requestId << ": Parameter 'nameOrCode' cannot be empty.");
return false;
}
@@ -1145,29 +1232,29 @@ bool addOrUpdateRequest(Client *c, const DataRequest *const req)
const SIMCONNECT_CLIENT_DATA_DEFINITION_ID newDataId = g_nextClienDataId++;
// create a new data area and add definition
if (!registerClientVariableDataArea(c, req->requestId, newDataId, actualValSize, req->valueSize)) {
- logAndNak(c, resp, ostringstream() << "Failed to create ClientDataDefinition, check log messages.");
+ logAndNak(c, resp, ostringstream() << "Error in DataRequest ID " << req->requestId << ": Failed to create ClientDataDefinition, check log messages.");
return false;
}
-
+ // this may change the request from a named to a calculated type for vars/string types which don't have native gauge API access functions.
tr = &c->requests.emplace(piecewise_construct, forward_as_tuple(req->requestId), forward_as_tuple(*req, newDataId)).first->second; // no try_emplace?
}
else {
// Existing request
if (actualValSize > tr->dataSize) {
- logAndNak(c, resp, ostringstream() << "Value size cannot be increased after request is created.");
+ logAndNak(c, resp, ostringstream() << "Error in DataRequest ID " << req->requestId << ": Value size cannot be increased after request is created.");
return false;
}
// recreate data definition if necessary
if (actualValSize != tr->dataSize) {
// remove definition
if FAILED(SimConnectHelper::removeClientDataDefinition(g_hSimConnect, tr->dataId)) {
- logAndNak(c, resp, ostringstream() << "Failed to clear ClientDataDefinition, check log messages.");
+ logAndNak(c, resp, ostringstream() << "Error in DataRequest ID " << req->requestId << ": Failed to clear ClientDataDefinition, check log messages.");
return false;
}
// add definition
if FAILED(SimConnectHelper::addClientDataDefinition(g_hSimConnect, tr->dataId, req->valueSize)) {
- logAndNak(c, resp, ostringstream() << "Failed to create ClientDataDefinition, check log messages.");
+ logAndNak(c, resp, ostringstream() << "Error in DataRequest ID " << req->requestId << ": Failed to create ClientDataDefinition, check log messages.");
return false;
}
}
@@ -1182,10 +1269,10 @@ bool addOrUpdateRequest(Client *c, const DataRequest *const req)
tr->variableId = getVariableId(tr->varTypePrefix, tr->nameOrCode);
if (tr->variableId < 0) {
if (tr->varTypePrefix == 'T') {
- LOG_WRN << "Token variable named " << quoted(tr->nameOrCode) << " was not found. Will fall back to initialize_var_by_name().";
+ LOG_WRN << "Warning in DataRequest ID " << req->requestId << ": Token variable named " << quoted(tr->nameOrCode) << " was not found. Will fall back to initialize_var_by_name().";
}
else {
- LOG_WRN << "Variable named " << quoted(tr->nameOrCode) << " was not found, disabling updates.";
+ LOG_ERR << "Error in DataRequest ID " << req->requestId << ": Variable named " << quoted(tr->nameOrCode) << " was not found, disabling updates.";
tr->period = UpdatePeriod::Never;
}
}
@@ -1193,13 +1280,21 @@ bool addOrUpdateRequest(Client *c, const DataRequest *const req)
// look up unit ID if we don't have one already
if (tr->unitId < 0 && tr->unitName[0] != '\0') {
tr->unitId = get_units_enum(tr->unitName);
- if (tr->unitId < 0)
- LOG_WRN << "Unit named " << quoted(tr->unitName) << " was not found.";
+ if (tr->unitId < 0) {
+ if (tr->varTypePrefix == 'A') {
+ LOG_ERR << "Error in DataRequest ID " << req->requestId << ": Unit named " << quoted(tr->unitName) << " was not found, disabling updates.";
+ tr->period = UpdatePeriod::Never;
+ }
+ // maybe an L var... unit is not technically required.
+ else {
+ LOG_WRN << "Warning in DataRequest ID " << req->requestId << ": Unit named " << quoted(tr->unitName) << " was not found, no unit type will be used.";
+ }
+ }
}
}
// calculated value, update compiled string if needed
// NOTE: compiling code for format_calculator_string() doesn't seem to work as advertised in the docs, see:
- // https://devsupport.flightsimulator.com/questions/9513/gauge-calculator-code-precompile-with-code-meant-f.html
+ // https://devsupport.flightsimulator.com/t/gauge-calculator-code-precompile-with-code-meant-for-format-calculator-string-reports-format-errors/4457
else if (tr->calcResultType != CalcResultType::Formatted && tr->calcBytecode.empty()) {
// assume the command has changed and re-compile
PCSTRINGZ pCompiled = nullptr;
@@ -1212,20 +1307,60 @@ bool addOrUpdateRequest(Client *c, const DataRequest *const req)
}
else {
LOG_WRN << "Calculator string compilation failed. gauge_calculator_code_precompile() returned: " << boolalpha << ok
- << "; size: " << pCompiledSize << "; Result null? " << (pCompiled == nullptr) << "; Original code : " << quoted(tr->nameOrCode);
+ << " for request ID " << tr->requestId << ". Size: " << pCompiledSize << "; Result null ? " << (pCompiled == nullptr) << "; Original code : " << quoted(tr->nameOrCode);
}
}
- // make sure any ms interval is >= our minimum tick time
- if (tr->period == UpdatePeriod::Millisecond)
- tr->interval = max((time_t)tr->interval, TICK_PERIOD_MS);
sendAckNak(c, resp, true);
- if (tr->period != UpdatePeriod::Never)
- updateRequestValue(c, tr, false);
+
+ if (tr->period != UpdatePeriod::Never) {
+ // make sure any ms interval is >= our minimum tick time
+ if (tr->period == UpdatePeriod::Millisecond && tr->interval < TICK_PERIOD_MS)
+ tr->interval = TICK_PERIOD_MS;
+ // If updates are not paused then send the value now; this takes care of "Once" type requests as well.
+ if (!c->pauseDataUpdates)
+ updateRequestValue(c, tr, false);
+ // If they're paused and this is a "Once" type request, use the interval as a flag to indicate that an update
+ // is pending for this request, which is then handled in setSuspendClientDataUpdates(false) below to send the value.
+ else if (tr->period == UpdatePeriod::Once)
+ tr->interval = 1;
+ }
LOG_DBG << (isNewRequest ? "Added " : "Updated ") << *tr;
- return (tr->period > UpdatePeriod::Once);
+ if (!g_triggersRegistered && tr->period > UpdatePeriod::Once && !c->pauseDataUpdates)
+ resumeTriggerEvent(); // start update loop
+ return true;
+}
+
+std::string setSuspendClientDataUpdates(Client *c, bool suspend)
+{
+ if (c->pauseDataUpdates == suspend)
+ return suspend ? "Data Subscriptions already paused" : "Data Subscriptions already active";
+
+ if (suspend) {
+ c->pauseDataUpdates = true;
+ checkTriggerEventNeeded();
+ return "Data Subscription updates suspended";
+ }
+
+ // Check for any "Once" type requests which are still pending and send them.
+ // While we're at it we can also check if there are any data updates which need scheduling.
+ bool resume = false;
+ for (requestMap_t::value_type &rp : c->requests) {
+ if (rp.second.period >= UpdatePeriod::Tick) {
+ resume = true;
+ }
+ else if (rp.second.period == UpdatePeriod::Once && rp.second.interval == 1) {
+ rp.second.interval = 0;
+ updateRequestValue(c, &rp.second);
+ }
+ }
+ c->pauseDataUpdates = false;
+ if (resume && !g_triggersRegistered)
+ resumeTriggerEvent();
+ return "Data Subscription updates resumed";
}
+
#pragma endregion Data Requests
#pragma region Registered Calculator Events ----------------------------------------------
@@ -1387,9 +1522,9 @@ void tick()
g_tpNextTick = now + milliseconds(TICK_PERIOD_MS);
for (clientMap_t::value_type &cp : g_mClients) {
- if (cp.second.status != ClientStatus::Connected)
- continue;
Client &c = cp.second;
+ if (c.status != ClientStatus::Connected || c.pauseDataUpdates)
+ continue;
// check for timeout
if (now >= c.nextTimeout) {
disconnectClient(&c, "Client connection timed out.", ClientStatus::TimedOut);
@@ -1400,8 +1535,7 @@ void tick()
c.nextHearbeat = now + seconds(CONN_HEARTBEAT_SEC);
sendPing(&c);
}
- if (c.pauseDataUpdates)
- continue;
+ // process data requests
for (requestMap_t::value_type &rp : c.requests) {
TrackedRequest &r = rp.second;
// check if update needed
@@ -1455,8 +1589,7 @@ void processCommand(Client *c, const Command *const cmd)
break;
case CommandId::Subscribe:
- c->pauseDataUpdates = !cmd->uData;
- ackMsg = c->pauseDataUpdates ? "Data Subscription updates suspended" : "Data Subscription updates resumed";
+ ackMsg = setSuspendClientDataUpdates(c, !cmd->uData);
break;
case CommandId::Update:
@@ -1473,8 +1606,7 @@ void processCommand(Client *c, const Command *const cmd)
case CommandId::Disconnect:
disconnectClient(c);
- ackMsg = "Disconnected by Client command.";
- break;
+ return;
case CommandId::Ping: // just ACK the ping
case CommandId::Connect: // client was already re-connected or we wouldn't be here, just ACK
@@ -1542,8 +1674,8 @@ void CALLBACK dispatchMessage(SIMCONNECT_RECV* pData, DWORD cbData, void*)
}
if (c->status == ClientStatus::Connected)
updateClientTimeout(c); // update heartbeat timer
- else if (!connectClient(c)) // assume client wants to re-connect if they're not already
- break; // unlikely
+ else
+ connectClient(c); // assume client wants to re-connect if they're not already
const size_t dataSize = (size_t)pData->dwSize + 4 - sizeof(SIMCONNECT_RECV_CLIENT_DATA); // dwSize reports 4 bytes less than actual size of SIMCONNECT_RECV_CLIENT_DATA
switch (dr->type) {
@@ -1571,8 +1703,7 @@ void CALLBACK dispatchMessage(SIMCONNECT_RECV* pData, DWORD cbData, void*)
LOG_CRT << "Invalid DataRequest struct data size! Expected " << sizeof(DataRequest) << " but got " << dataSize;
return;
}
- if (addOrUpdateRequest(c, reinterpret_cast(&data->dwData)) && !g_triggersRegistered)
- registerTriggerEvents(); // start update loop
+ addOrUpdateRequest(c, reinterpret_cast(&data->dwData));
break;
default:
@@ -1590,8 +1721,9 @@ void CALLBACK dispatchMessage(SIMCONNECT_RECV* pData, DWORD cbData, void*)
SimConnectHelper::logSimVersion(pData);
break;
+ // This doesn't seem to ever actually happen... but I guess it may just start to one day ¯\_(ツ)_/¯
case SIMCONNECT_RECV_ID_QUIT:
- g_simConnectQuitEvent = true;
+ disableAllClients();
LOG_DBG << "Received quit command from SimConnect.";
break;
@@ -1658,6 +1790,13 @@ MSFS_CALLBACK void module_init(void)
// register incoming Ping event and add to notification group (this is technically not "critical" to operation so do not exit on error here
SimConnectHelper::newClientEvent(g_hSimConnect, CLI_EVENT_PING, string(EVENT_NAME_PING, strlen(EVENT_NAME_PING)), GROUP_DEFAULT);
+ // use "Frame" event as trigger for our tick() loop
+ if FAILED(hr = SimConnect_SubscribeToSystemEvent(g_hSimConnect, (SIMCONNECT_CLIENT_EVENT_ID)EVENT_FRAME, "Frame")) {
+ LOG_CRT << "SimConnect_SubscribeToSystemEvent failed with " << LOG_HR(hr);;
+ return;
+ }
+ pauseTriggerEvent(); // pause frame updates for now
+
// Go
if FAILED(hr = SimConnect_CallDispatch(g_hSimConnect, dispatchMessage, nullptr)) {
LOG_CRT << "SimConnect_CallDispatch failed with " << LOG_HR(hr);;
@@ -1669,18 +1808,11 @@ MSFS_CALLBACK void module_init(void)
MSFS_CALLBACK void module_deinit(void)
{
+ // don't send out any more data at this point (especially logs!) as it may prevent the simulator from exiting (new in SU13, yay!)
+ disableAllClients();
LOG_INF << "Stopping " WSMCMND_PROJECT_NAME " " WSMCMND_SERVER_NAME;
-
- if (g_hSimConnect != INVALID_HANDLE_VALUE) {
- // Unloading of module would typically only happen when simulator quits, except during development and manual project rebuild.
- // The below code is just for that occasion. Normally any connected Clients should detect shutdown via the SimConnect Close event.
- if (!g_simConnectQuitEvent) {
- // Disconnect any clients (does not seem to actually send any data... SimConnect context destroyed?)
- LOG_DBG << "SimConnect apparently didn't send Quit message... disconnecting any clients.";
- disconnectAllClients("Server is shutting down.");
- }
+ if (g_hSimConnect != INVALID_HANDLE_VALUE)
SimConnect_Close(g_hSimConnect);
- }
g_hSimConnect = INVALID_HANDLE_VALUE;
}
diff --git a/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml b/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml
index fda8511..3b530ce 100644
--- a/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml
+++ b/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml
@@ -1,10 +1,10 @@
-
+MISCWASimCommander WASM Module
- Maxim Paperno
+ Copyright Maxim Paperno; All rights reserved.false
@@ -29,4 +29,3 @@
-
diff --git a/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml.in b/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml.in
new file mode 100644
index 0000000..1492c1a
--- /dev/null
+++ b/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module.xml.in
@@ -0,0 +1,31 @@
+
+
+
+ MISC
+ @PROJECT_NAME@ WASM Module
+
+ @PROJECT_COPY@
+
+
+ false
+ false
+
+
+
+ ContentInfo
+
+ false
+
+ PackageDefinitions\wasimcommander-module\ContentInfo\
+ ContentInfo\wasimcommander-module\
+
+
+ Copy
+
+ false
+
+ PackageSources\@SERVER_NAME@\
+ modules\
+
+
+
diff --git a/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module/ReleaseNotes.xml b/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module/ReleaseNotes.xml
deleted file mode 100644
index a82e22f..0000000
--- a/src/WASimModule/WASimModuleProject/WASimCommander-Module/PackageDefinitions/wasimcommander-module/ReleaseNotes.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-Initial release! v1.0.0.5-beta1
-v1.0.0.6-beta2
-* Fixed that Formatted type calculation
-results cannot be pre-compiled.
-* Event loop is paused when last client
-disconnects.
-WASimCommander v1.1.0.0
-* Minor updates for SU10: Updated lookup lists of Token vars
-and Key events; Use new trigger_key_event_EX1().
-* Rebuilt using /O1 optimization flag.
-WASimCommander v1.1.1.0
-* Updated lookup lists of Key events for SU11/SDK0.20.5.0.
-* Rebuilt using /O3 optimization flag as per new
-documentation recommendations and since fixes in SU11.
-WASimCommander v1.1.2.0
-* Fixed KEY event alias for "AUTORUDDER_TOGGLE" -> KEY_AUTOCOORD_TOGGLE.
diff --git a/src/WASimModule/key_events.h b/src/WASimModule/key_events.h
index b4ad340..befaa62 100644
--- a/src/WASimModule/key_events.h
+++ b/src/WASimModule/key_events.h
@@ -1899,64 +1899,79 @@ namespace WASimCommander {
{ "CYCLIC_LONGITUDINAL_DOWN", KEY_CYCLIC_LONGITUDINAL_DOWN },
{ "CYCLIC_LONGITUDINAL_UP", KEY_CYCLIC_LONGITUDINAL_UP },
+ // SDK 0.21.0.0
+ { "ELECT_FUEL_PUMP_SET", KEY_ELECT_FUEL_PUMP_SET },
+
+ // SDK 0.22.3.0
+ { "3RD_PARTY_WINDOW_OPEN_PRIMARY", KEY_3RD_PARTY_WINDOW_OPEN_PRIMARY },
+ { "3RD_PARTY_WINDOW_OPEN_SECONDARY", KEY_3RD_PARTY_WINDOW_OPEN_SECONDARY },
+ { "3RD_PARTY_WINDOW_MOVE_DOWN", KEY_3RD_PARTY_WINDOW_MOVE_DOWN },
+ { "3RD_PARTY_WINDOW_MOVE_UP", KEY_3RD_PARTY_WINDOW_MOVE_UP },
+ { "3RD_PARTY_WINDOW_VALIDATE", KEY_3RD_PARTY_WINDOW_VALIDATE },
+ { "HELI_BEEP_SET", KEY_HELI_BEEP_SET },
+
// Aliases for published Event IDs which do not match KEY IDs
- { "ADF1_WHOLE_DEC", KEY_ADF_WHOLE_DEC },
- { "ADF1_WHOLE_INC", KEY_ADF_WHOLE_INC },
- { "ALTITUDE_SLOT_INDEX_SET", KEY_AP_ALTITUDE_SLOT_INDEX_SET },
- { "ANTIDETONATION_TANK_VALVE_TOGGLE", KEY_TOGGLE_ANTIDETONATION_TANK_VALVE },
- { "AP_HEADING_BUG_SET_EX1", KEY_HEADING_BUG_SET_EX1 },
- { "AP_PANEL_MACH_HOLD_TOGGLE", KEY_AUTOPILOT_MACH_HOLD_CURRENT },
- { "AP_PANEL_SPEED_HOLD_TOGGLE", KEY_AUTOPILOT_AIRSPEED_HOLD_CURRENT },
- { "ATTITUDE_BARS_POSITION_DOWN", KEY_ATTITUDE_BARS_POSITION_DEC },
- { "ATTITUDE_BARS_POSITION_UP", KEY_ATTITUDE_BARS_POSITION_INC },
- { "ATTITUDE_CAGE_BUTTON", KEY_TOGGLE_ATTITUDE_CAGE },
- { "AUTORUDDER_TOGGLE", KEY_AUTOCOORD_TOGGLE },
- { "BACK_TO_FLY", KEY_NULL }, // Not a real Event ??
- { "COM_STBY_RADIO_SWAP", KEY_COM_STBY_RADIO_SWITCH_TO },
- { "DECREASE_AUTOBRAKE_CONTROL", KEY_DEC_AUTOBRAKE_CONTROL },
- { "DECREASE_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_DEC },
- { "DECREASE_DECISION_HEIGHT", KEY_DECISION_HEIGHT_DEC },
- { "INCREASE_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_INC },
- { "INCREASE_DECISION_HEIGHT", KEY_DECISION_HEIGHT_INC },
- { "SET_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_SET },
- { "FLIGHT_LEVEL_CHANGE", KEY_AP_FLIGHT_LEVEL_CHANGE },
- { "FLIGHT_LEVEL_CHANGE_OFF", KEY_AP_FLIGHT_LEVEL_CHANGE_OFF },
- { "FLIGHT_LEVEL_CHANGE_ON", KEY_AP_FLIGHT_LEVEL_CHANGE_ON },
- { "HEADING_SLOT_INDEX_SET", KEY_AP_HEADING_SLOT_INDEX_SET },
- { "INCREASE_AUTOBRAKE_CONTROL", KEY_INC_AUTOBRAKE_CONTROL },
- { "KNEEBOARD_VIEW", KEY_KNEEBOARD },
- { "MP_ACTIVATE_CHAT", KEY_MULTIPLAYER_ACTIVATE_CHAT },
- { "MP_BROADCAST_VOICE_CAPTURE_START", KEY_MULTIPLAYER_BROADCAST_VOICE_CAPTURE_START },
- { "MP_BROADCAST_VOICE_CAPTURE_STOP", KEY_MULTIPLAYER_BROADCAST_VOICE_CAPTURE_STOP },
- { "MP_CHAT", KEY_MULTIPLAYER_CHAT },
- { "MP_PAUSE_SESSION", KEY_MULTIPLAYER_PAUSE_SESSION },
- { "MP_PLAYER_CYCLE", KEY_MULTIPLAYER_PLAYER_CYCLE },
- { "MP_PLAYER_FOLLOW", KEY_MULTIPLAYER_PLAYER_FOLLOW },
- { "MP_TRANSFER_CONTROL", KEY_MULTIPLAYER_TRANSFER_CONTROL },
- { "MP_VOICE_CAPTURE_START", KEY_MULTIPLAYER_VOICE_CAPTURE_START },
- { "MP_VOICE_CAPTURE_STOP", KEY_MULTIPLAYER_VOICE_CAPTURE_STOP },
- { "NITROUS_TANK_VALVE_TOGGLE", KEY_TOGGLE_NITROUS_TANK_VALVE } ,
+ { "ADF1_WHOLE_DEC", KEY_ADF_WHOLE_DEC },
+ { "ADF1_WHOLE_INC", KEY_ADF_WHOLE_INC },
+ { "ALTITUDE_SLOT_INDEX_SET", KEY_AP_ALTITUDE_SLOT_INDEX_SET },
+ { "ANTIDETONATION_TANK_VALVE_TOGGLE", KEY_TOGGLE_ANTIDETONATION_TANK_VALVE },
+ { "AP_HEADING_BUG_SET_EX1", KEY_HEADING_BUG_SET_EX1 },
+ { "AP_PANEL_MACH_HOLD_TOGGLE", KEY_AUTOPILOT_MACH_HOLD_CURRENT },
+ { "AP_PANEL_SPEED_HOLD_TOGGLE", KEY_AUTOPILOT_AIRSPEED_HOLD_CURRENT },
+ { "ATTITUDE_BARS_POSITION_DOWN", KEY_ATTITUDE_BARS_POSITION_DEC },
+ { "ATTITUDE_BARS_POSITION_UP", KEY_ATTITUDE_BARS_POSITION_INC },
+ { "ATTITUDE_CAGE_BUTTON", KEY_TOGGLE_ATTITUDE_CAGE },
+ { "AUTORUDDER_TOGGLE", KEY_AUTOCOORD_TOGGLE },
+ { "BACK_TO_FLY", KEY_NULL }, // Not a real Event ??
+ { "COM_STBY_RADIO_SWAP", KEY_COM_STBY_RADIO_SWITCH_TO },
+ { "DECREASE_AUTOBRAKE_CONTROL", KEY_DEC_AUTOBRAKE_CONTROL },
+ { "DECREASE_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_DEC },
+ { "DECREASE_DECISION_HEIGHT", KEY_DECISION_HEIGHT_DEC },
+ { "DECREASE_HELO_GOV_BEEP", KEY_HELI_BEEP_DECREASE },
+ { "FLIGHT_LEVEL_CHANGE", KEY_AP_FLIGHT_LEVEL_CHANGE },
+ { "FLIGHT_LEVEL_CHANGE_OFF", KEY_AP_FLIGHT_LEVEL_CHANGE_OFF },
+ { "FLIGHT_LEVEL_CHANGE_ON", KEY_AP_FLIGHT_LEVEL_CHANGE_ON },
+ { "HEADING_SLOT_INDEX_SET", KEY_AP_HEADING_SLOT_INDEX_SET },
+ { "INCREASE_AUTOBRAKE_CONTROL", KEY_INC_AUTOBRAKE_CONTROL },
+ { "INCREASE_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_INC },
+ { "INCREASE_DECISION_HEIGHT", KEY_DECISION_HEIGHT_INC },
+ { "INCREASE_HELO_GOV_BEEP", KEY_HELI_BEEP_INCREASE },
+ { "KNEEBOARD_VIEW", KEY_KNEEBOARD },
+ { "MP_ACTIVATE_CHAT", KEY_MULTIPLAYER_ACTIVATE_CHAT },
+ { "MP_BROADCAST_VOICE_CAPTURE_START", KEY_MULTIPLAYER_BROADCAST_VOICE_CAPTURE_START },
+ { "MP_BROADCAST_VOICE_CAPTURE_STOP", KEY_MULTIPLAYER_BROADCAST_VOICE_CAPTURE_STOP },
+ { "MP_CHAT", KEY_MULTIPLAYER_CHAT },
+ { "MP_PAUSE_SESSION", KEY_MULTIPLAYER_PAUSE_SESSION },
+ { "MP_PLAYER_CYCLE", KEY_MULTIPLAYER_PLAYER_CYCLE },
+ { "MP_PLAYER_FOLLOW", KEY_MULTIPLAYER_PLAYER_FOLLOW },
+ { "MP_TRANSFER_CONTROL", KEY_MULTIPLAYER_TRANSFER_CONTROL },
+ { "MP_VOICE_CAPTURE_START", KEY_MULTIPLAYER_VOICE_CAPTURE_START },
+ { "MP_VOICE_CAPTURE_STOP", KEY_MULTIPLAYER_VOICE_CAPTURE_STOP },
+ { "NITROUS_TANK_VALVE_TOGGLE", KEY_TOGGLE_NITROUS_TANK_VALVE } ,
{ "PRESSURIZATION_PRESSURE_DUMP_SWTICH", KEY_PRESSURIZATION_PRESSURE_DUMP_SWITCH },
- { "RELOAD_USER_AIRCRAFT", KEY_CONTROL_RELOAD_USER_AIRCRAFT },
- { "REQUEST_FUEL_KEY", KEY_REQUEST_FUEL },
- { "ROTOR_BRAKE_SET", KEY_AXIS_ROTOR_BRAKE_SET },
- { "RPM_SLOT_INDEX_SET", KEY_AP_RPM_SLOT_INDEX_SET },
- { "SET_REVERSE_THRUST_OFF", KEY_SET_THROTTLE_REVERSE_THRUST_OFF },
- { "SET_REVERSE_THRUST_ON", KEY_SET_THROTTLE_REVERSE_THRUST_ON },
- { "SPEED_SLOT_INDEX_SET", KEY_AP_SPEED_SLOT_INDEX_SET },
- { "TOGGLE_AUTOFEATHER_ARM", KEY_TOGGLE_ARM_AUTOFEATHER },
- { "TOGGLE_DME", KEY_DME_TOGGLE } ,
- { "TOGGLE_PROPELLER_SYNC", KEY_TOGGLE_PROP_SYNC },
- { "TOGGLE_PUSHBACK", KEY_PUSHBACK_SET }, // ?
- { "TOW_PLANE_REQUEST", KEY_REQUEST_TOW_PLANE },
- { "TRUE_AIRSPEED_CAL_DEC", KEY_TRUE_AIRSPEED_CALIBRATE_DEC },
- { "TRUE_AIRSPEED_CAL_INC", KEY_TRUE_AIRSPEED_CALIBRATE_INC },
- { "VARIOMETER_SOUND_TOGGLE", KEY_TOGGLE_VARIOMETER_SWITCH }, // ?
- { "VERTICAL_SPEED_SET", KEY_AP_VS_SET },
- { "VIEW_AXIS_INDICATOR_CYCLE", KEY_AXIS_INDICATOR_CYCLE },
- { "VIEW_CAMERA_SELECT_START", KEY_VIEW_CAMERA_SELECT_STARTING },
- { "VIEW_WINDOW_TITLES_TOGGLE", KEY_WINDOW_TITLES_TOGGLE },
- { "VS_SLOT_INDEX_SET", KEY_AP_VS_SLOT_INDEX_SET },
+ { "RELOAD_USER_AIRCRAFT", KEY_CONTROL_RELOAD_USER_AIRCRAFT },
+ { "REQUEST_FUEL_KEY", KEY_REQUEST_FUEL },
+ { "ROTOR_AXIS_TAIL_ROTOR_SET", KEY_AXIS_TAIL_ROTOR_SET },
+ { "ROTOR_BRAKE_SET", KEY_AXIS_ROTOR_BRAKE_SET },
+ { "RPM_SLOT_INDEX_SET", KEY_AP_RPM_SLOT_INDEX_SET },
+ { "SET_DECISION_ALTITUDE_MSL", KEY_DECISION_ALTITUDE_MSL_SET },
+ { "SET_HELO_GOV_BEEP", KEY_HELI_BEEP_SET },
+ { "SET_REVERSE_THRUST_OFF", KEY_SET_THROTTLE_REVERSE_THRUST_OFF },
+ { "SET_REVERSE_THRUST_ON", KEY_SET_THROTTLE_REVERSE_THRUST_ON },
+ { "SPEED_SLOT_INDEX_SET", KEY_AP_SPEED_SLOT_INDEX_SET },
+ { "TOGGLE_AUTOFEATHER_ARM", KEY_TOGGLE_ARM_AUTOFEATHER },
+ { "TOGGLE_DME", KEY_DME_TOGGLE } ,
+ { "TOGGLE_PROPELLER_SYNC", KEY_TOGGLE_PROP_SYNC },
+ { "TOGGLE_PUSHBACK", KEY_PUSHBACK_SET }, // ?
+ { "TOW_PLANE_REQUEST", KEY_REQUEST_TOW_PLANE },
+ { "TRUE_AIRSPEED_CAL_DEC", KEY_TRUE_AIRSPEED_CALIBRATE_DEC },
+ { "TRUE_AIRSPEED_CAL_INC", KEY_TRUE_AIRSPEED_CALIBRATE_INC },
+ { "VARIOMETER_SOUND_TOGGLE", KEY_TOGGLE_VARIOMETER_SWITCH }, // ?
+ { "VERTICAL_SPEED_SET", KEY_AP_VS_SET },
+ { "VIEW_AXIS_INDICATOR_CYCLE", KEY_AXIS_INDICATOR_CYCLE },
+ { "VIEW_CAMERA_SELECT_START", KEY_VIEW_CAMERA_SELECT_STARTING },
+ { "VIEW_WINDOW_TITLES_TOGGLE", KEY_WINDOW_TITLES_TOGGLE },
+ { "VS_SLOT_INDEX_SET", KEY_AP_VS_SLOT_INDEX_SET },
};
diff --git a/src/WASimUI/DocImports.h b/src/WASimUI/DocImports.h
new file mode 100644
index 0000000..6f1a094
--- /dev/null
+++ b/src/WASimUI/DocImports.h
@@ -0,0 +1,219 @@
+/*
+This file is part of the WASimCommander project.
+https://github.com/mpaperno/WASimCommander
+
+COPYRIGHT: (c) Maxim Paperno; All Rights Reserved.
+
+This file may be used under the terms of the GNU General Public License (GPL)
+as published by the Free Software Foundation, either version 3 of the Licenses,
+or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+A copy of the GNU GPL is included with this project
+and is also available at .
+*/
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "Widgets.h"
+
+namespace WASimUiNS {
+ namespace DocImports
+{
+Q_NAMESPACE
+
+const QString DB_NAME = QStringLiteral("MSFS_SDK_Doc_Import");
+
+enum RecordType : quint8 { Unknown, SimVars, KeyEvents, SimVarUnits };
+Q_ENUM_NS(RecordType)
+
+static const QVector RecordTypeNames {
+ "Unknown", "Simulator Variables", "Key Events", "Variable Units"
+};
+static QString recordTypeName(RecordType type) {
+ return RecordTypeNames.value(type, RecordTypeNames[RecordType::Unknown]);
+}
+
+static bool createConnection()
+{
+ QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", DB_NAME);
+ db.setDatabaseName(DB_NAME + ".sqlite3");
+ db.setConnectOptions("QSQLITE_OPEN_READONLY;QSQLITE_ENABLE_SHARED_CACHE;QSQLITE_ENABLE_REGEXP=30");
+ if (db.open())
+ return true;
+ qDebug() << "Failed to open database" << DB_NAME << ".sqlite3 with:" << db.lastError();
+ db = QSqlDatabase();
+ QSqlDatabase::removeDatabase(DB_NAME);
+ return false;
+}
+
+static QSqlDatabase getConnection(bool open = true)
+{
+ if (!QSqlDatabase::contains(DB_NAME)) {
+ if (!createConnection())
+ return QSqlDatabase::database();
+ }
+ return QSqlDatabase::database(DB_NAME, open);
+}
+
+
+// ----------------------------------------
+// DocImportsModel
+// ----------------------------------------
+
+class DocImportsModel : public QSqlTableModel
+{
+ Q_OBJECT
+
+public:
+ DocImportsModel(QObject *parent = nullptr, RecordType type = RecordType::Unknown)
+ : QSqlTableModel(parent, getConnection())
+ {
+ setEditStrategy(QSqlTableModel::OnManualSubmit);
+ //qDebug() << "Opened DB" << database().connectionName() << database().databaseName();
+ if (type != RecordType::Unknown)
+ setRecordType(type);
+ }
+
+ RecordType recordType() const { return m_recordType; }
+
+ QDateTime lastDataUpdate(RecordType type, QString *fromUrl = nullptr)
+ {
+ QDateTime lu;
+ if (type == RecordType::Unknown)
+ return lu;
+
+ const QString table(QMetaEnum::fromType().valueToKey(type));
+ QSqlQuery qry("SELECT LastUpdate, FromURL FROM ImportMeta WHERE TableName = ? ", database());
+ qry.addBindValue(table);
+ qry.exec();
+ if (qry.next()) {
+ lu = qry.value(0).toDateTime();
+ lu.setTimeZone(QTimeZone::utc());
+ lu = lu.toLocalTime();
+ if (fromUrl)
+ *fromUrl = qry.value(1).toString();
+ }
+ else if (qry.lastError().isValid()) {
+ qDebug() << "ImportMeta query for table" << table << "failed with:" << qry.lastError();
+ }
+ else {
+ qDebug() << "ImportMeta query for table" << table << "returned no results.";
+ }
+ qry.finish();
+ return lu;
+ }
+
+ void setRecordType(RecordType type)
+ {
+ m_recordType = type;
+ QString table;
+ switch (type) {
+ case RecordType::SimVars:
+ case RecordType::KeyEvents:
+ case RecordType::SimVarUnits:
+ table = QString(QMetaEnum::fromType().valueToKey(type));
+ break;
+
+ default:
+ return;
+ }
+
+ setTable(table);
+ select();
+
+ //setQuery("SELECT * FROM " + table, m_db);
+ if (lastError().isValid()) {
+ qDebug() << "Query for table" << table << "failed with: " << lastError();
+ return;
+ }
+
+ int col = fieldIndex("Multiplayer");
+ if (col > -1)
+ removeColumn(col);
+ }
+
+ Qt::ItemFlags flags(const QModelIndex &idx) const override {
+ if (idx.isValid())
+ return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren;
+ return Qt::NoItemFlags;
+ }
+
+ QVariant data(const QModelIndex &idx, int role) const override {
+ if (role == Qt::ToolTipRole)
+ role = Qt::DisplayRole;
+ return QSqlTableModel::data(idx, role);
+ }
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override {
+ if (role == Qt::EditRole || role == Qt::ToolTipRole)
+ return QSqlTableModel::headerData(section, orientation, Qt::DisplayRole);
+ return QSqlTableModel::headerData(section, orientation, role);
+ }
+
+private:
+ RecordType m_recordType = RecordType::Unknown;
+
+};
+
+
+// ----------------------------------------
+// NameCompleter
+// ----------------------------------------
+
+class NameCompleter : public QCompleter
+{
+ Q_OBJECT
+
+public:
+ NameCompleter(RecordType recordType, QObject *parent = nullptr) :
+ QCompleter(parent)
+ {
+ DocImportsModel *m = new DocImportsModel(this, recordType);
+ const int modelColumn = m->fieldIndex("Name");
+ if (recordType != RecordType::SimVarUnits)
+ m->setFilter(QStringLiteral("Deprecated = 0"));
+ m->setSort(modelColumn, Qt::AscendingOrder);
+
+ setModel(m);
+ setCompletionColumn(modelColumn);
+ setModelSorting(QCompleter::CaseSensitivelySortedModel);
+ setCompletionMode(QCompleter::PopupCompletion);
+ setCaseSensitivity(Qt::CaseInsensitive);
+ setFilterMode(Qt::MatchContains);
+ setMaxVisibleItems(12);
+ }
+
+ DocImportsModel *model() const { return (DocImportsModel *)QCompleter::model(); }
+
+};
+
+
+// ----------------------------------------
+// RecordTypeComboBox
+// ----------------------------------------
+
+class RecordTypeComboBox : public EnumsComboBox
+{
+public:
+ RecordTypeComboBox(QWidget *p = nullptr) :
+ EnumsComboBox(DocImports::RecordTypeNames, DocImports::RecordType::SimVars, p) {
+ setToolTip(tr("Select a documentation record type to browse."));
+ }
+};
+
+ } // DocImports
+} // WASimUiNS
diff --git a/src/WASimUI/DocImportsBrowser.h b/src/WASimUI/DocImportsBrowser.h
new file mode 100644
index 0000000..de30a1d
--- /dev/null
+++ b/src/WASimUI/DocImportsBrowser.h
@@ -0,0 +1,242 @@
+/*
+This file is part of the WASimCommander project.
+https://github.com/mpaperno/WASimCommander
+
+COPYRIGHT: (c) Maxim Paperno; All Rights Reserved.
+
+This file may be used under the terms of the GNU General Public License (GPL)
+as published by the Free Software Foundation, either version 3 of the Licenses,
+or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+A copy of the GNU GPL is included with this project
+and is also available at .
+*/
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "ui_DocImportsBrowser.h"
+#include "DocImports.h"
+#include "FilterTableHeader.h"
+
+namespace WASimUiNS {
+ namespace DocImports
+{
+
+class DocImportsBrowser : public QWidget
+{
+ Q_OBJECT
+
+public:
+ enum ViewMode { FullViewMode, PopupViewMode };
+ Q_ENUM(ViewMode)
+
+ DocImportsBrowser(QWidget *parent = nullptr, RecordType type = RecordType::Unknown, ViewMode viewMode = ViewMode::FullViewMode)
+ : QWidget(parent),
+ m_model(new DocImportsModel(this))
+ {
+ setObjectName(QStringLiteral("DocImportsBrowser"));
+ ui.setupUi(this);
+
+ setWindowTitle(tr("SimConnect SDK Reference Browser"));
+ setContextMenuPolicy(Qt::ActionsContextMenu);
+
+ ui.cbRecordType->setCurrentIndex(-1);
+ connect(ui.cbRecordType, &DataComboBox::currentDataChanged, this, &DocImportsBrowser::setRecordTypeVar);
+
+ ui.tableView->setWordWrap(false);
+ ui.tableView->setEditTriggers(QTableView::NoEditTriggers);
+ ui.tableView->horizontalHeader()->setDefaultSectionSize(175);
+ ui.tableView->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
+ ui.tableView->setFiltersVisible(true);
+
+ ui.tableView->setModel(m_model);
+
+ //addAction(ui.tableView->actionsMenu(this)->menuAction());
+ addAction(ui.tableView->columnToggleMenuAction());
+ addAction(ui.tableView->fontSizeMenuAction());
+ addAction(ui.tableView->filterToggleAction());
+
+ QAction *closeAct = new QAction(QIcon("close.glyph"), tr("Close"), this);
+ closeAct->setShortcuts({ QKeySequence(Qt::Key_Escape), QKeySequence::Close });
+ connect(closeAct, &QAction::triggered, this, &DocImportsBrowser::close);
+ addAction(closeAct);
+
+ ui.textBrowser->document()->setIndentWidth(2.0);
+ ui.textBrowser->document()->setDefaultStyleSheet(QStringLiteral(
+ "dd { margin-bottom: 6px; }"
+ ));
+
+ if (viewMode == ViewMode::FullViewMode)
+ connect(ui.tableView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &DocImportsBrowser::showRecordDetails);
+ else
+ connect(ui.tableView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &DocImportsBrowser::onCurrentRowChanged);
+
+ connect(ui.tableView, &QTableView::doubleClicked, this, [=](const QModelIndex &idx) {
+ emit itemSelected(ui.tableView->proxyModel()->mapToSource(idx));
+ });
+
+ setRecordType(type);
+ setViewMode(viewMode);
+ setInitialSize();
+ loadSettings();
+ }
+
+ DocImportsModel *model() const { return m_model; }
+
+public Q_SLOTS:
+ void setViewMode(ViewMode mode)
+ {
+ m_viewMode = mode;
+ if (mode == ViewMode::FullViewMode) {
+ ui.cbRecordType->setVisible(true);
+ ui.toolbarLabel->setText(tr("Select Record Type:"));
+ ui.lblTitle->setText(
+ tr("Data is imported from SimConnect SDK web page documentation. Use the filters in each column to search.")
+ );
+ }
+ else {
+ ui.cbRecordType->setVisible(false);
+ ui.toolbarLabel->setText("" + DocImports::recordTypeName(m_model->recordType()) + "");
+ ui.lblTitle->setText(
+ tr("Double-click to select a record and return to the main window. Press Escape key to close. Use the filters to search.")
+ );
+ }
+ }
+
+ void setRecordType(RecordType type)
+ {
+ if (type == RecordType::Unknown || type == m_recordType)
+ return;
+ saveTypeSettings();
+ m_recordType = type;
+ m_model->setRecordType(type);
+ if (ui.textBrowser->property("currentRow").isValid())
+ ui.textBrowser->clear();
+ loadTypeSettings();
+ ui.lblLastUpdate->setText(tr("Data updated on: %1").arg(m_model->lastDataUpdate(type).toString("d MMMM yyyy")));
+
+ if (!qobject_cast(sender()))
+ ui.cbRecordType->setCurrentData((int)type);
+ }
+
+ void setRecordTypeVar(const QVariant &type) { setRecordType((RecordType)type.toUInt()); }
+
+ void showRecordDetails(const QModelIndex &key)
+ {
+ if (m_model->recordType() == RecordType::Unknown || !key.isValid())
+ return;
+
+ const QAbstractItemModel *model = ui.tableView->model();
+ QString sb(QStringLiteral("