v1.3.2.0
 
Loading...
Searching...
No Matches
WASimClient.cpp
1/*
2This file is part of the WASimCommander project.
3https://github.com/mpaperno/WASimCommander
4
5COPYRIGHT: (c) Maxim Paperno; All Rights Reserved.
6
7This file may be used under the terms of either the GNU General Public License (GPL)
8or the GNU Lesser General Public License (LGPL), as published by the Free Software
9Foundation, either version 3 of the Licenses, or (at your option) any later version.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16Copies of the GNU GPL and LGPL are included with this project
17and are available at <http://www.gnu.org/licenses/>.
18
19Except as contained in this notice, the names of the authors or
20their institutions shall not be used in advertising or otherwise to
21promote the sale, use or other dealings in this Software without
22prior written authorization from the authors.
23*/
24
25#include <chrono>
26#include <condition_variable>
27#include <cstring>
28#include <filesystem>
29#include <functional>
30#include <iomanip>
31#include <iostream>
32#include <list>
33#include <locale>
34#include <set>
35#include <shared_mutex>
36#include <string>
37#include <sstream>
38#include <thread>
39#include <unordered_map>
40
41#define WIN32_LEAN_AND_MEAN
42#include <Windows.h>
43#include <SimConnect.h>
44
45#define LOGFAULT_THREAD_NAME WSMCMND_CLIENT_NAME
46
47#include "client/WASimClient.h"
48#include "utilities.h"
49#include "SimConnectHelper.h"
50#include "inipp.h"
51
52#include "MSFS_EventsEnum.h" // from MSFS2024_SDK, for key_events.h
53#include "key_events.h"
54
56{
57
58using namespace std;
59using namespace WASimCommander::Utilities;
60using namespace WASimCommander::Enums;
61/// \private
62using Clock = std::chrono::steady_clock;
63
64static const uint32_t CUSTOM_KEY_EVENT_LEGACY_TRIGGER_FLAG = 0x80000000;
65
66// -------------------------------------------------------------
67#pragma region DataRequestRecord struct
68// -------------------------------------------------------------
69
73
75 DataRequest(req),
76 data(Utilities::getActualValueSize(valueSize), 0xFF) { }
77
79 DataRequest(req),
80 data(Utilities::getActualValueSize(valueSize), 0xFF) { }
81
82#pragma endregion
83
84// -------------------------------------------------------------
85#pragma region RegisteredEvent struct
86// -------------------------------------------------------------
87
88RegisteredEvent::RegisteredEvent(uint32_t eventId, const std::string &code, const std::string &name) :
90
91#pragma endregion
92
93
94// -------------------------------------------------------------
95#pragma region WASimClient::Private class
96// -------------------------------------------------------------
97
98class WASimClient::Private
99{
100#pragma region Locals
101
102 static const uint32_t DISPATCH_LOOP_WAIT_TIME = 5000;
103 static const uint32_t SIMCONNECT_DATA_ALLOC_LIMIT = 1024UL * 1024UL;
104
105 enum SimConnectIDs : uint8_t
106 {
107 SIMCONNECTID_INIT = 0,
108 // SIMCONNECT_NOTIFICATION_GROUP_ID
109 NOTIFY_GROUP_PING, // ping response notification group
110 // SIMCONNECT_CLIENT_EVENT_ID - custom named events
111 CLI_EVENT_CONNECT, // connect to server
112 CLI_EVENT_PING, // ping server
113 CLI_EVENT_PING_RESP, // ping response
114 // SIMCONNECT_CLIENT_DATA_ID - data area and definition IDs
115 CLI_DATA_COMMAND,
116 CLI_DATA_RESPONSE,
117 CLI_DATA_REQUEST,
118 CLI_DATA_KEYEVENT,
119 CLI_DATA_LOG,
120 // SIMCONNECT_DATA_REQUEST_ID - requests for data updates
121 DATA_REQ_RESPONSE, // command response data
122 DATA_REQ_LOG, // server log data
123
124 SIMCONNECTID_LAST // dynamic IDs start at this value
125
126 };
127
128 struct TrackedRequest : public DataRequest
129 {
130 uint32_t dataId; // our area and def ID
131 time_t lastUpdate = 0;
132 uint32_t dataSize;
133 mutable shared_mutex m_dataMutex;
134 vector<uint8_t> data;
135
136 explicit TrackedRequest(const DataRequest &req, uint32_t dataId) :
137 DataRequest(req),
138 dataId{dataId},
139 dataSize{Utilities::getActualValueSize(valueSize)},
140 data(dataSize, 0xFF)
141 { }
142
143 TrackedRequest & operator=(const DataRequest &req) {
144 if (req.valueSize != valueSize) {
145 unique_lock lock(m_dataMutex);
146 dataSize = Utilities::getActualValueSize(req.valueSize);
147 data = vector<uint8_t>(dataSize, 0xFF);
148 }
149 DataRequest::operator=(req);
150 return *this;
151 }
152
153 DataRequestRecord toRequestRecord() const {
154 DataRequestRecord drr = DataRequestRecord(static_cast<const DataRequest &>(*this));
155 drr.data.assign(data.cbegin(), data.cend());
156 drr.lastUpdate = lastUpdate;
157 return drr;
158 }
159
160 friend inline std::ostream& operator<<(std::ostream& os, const TrackedRequest &r) {
161 os << (const DataRequest &)r;
162 return os << " TrackedRequest{" << " dataId: " << r.dataId << "; lastUpdate: " << r.lastUpdate << "; dataSize: " << r.dataSize << "; data: " << Utilities::byteArrayToHex(r.data.data(), r.dataSize) << '}';
163 }
164
165 };
166
167 struct TrackedResponse
168 {
169 uint32_t token;
170 chrono::system_clock::time_point sent { chrono::system_clock::now() };
171 weak_ptr<condition_variable_any> cv {};
172 shared_mutex mutex;
173 Command response {};
174
175 explicit TrackedResponse(uint32_t t, weak_ptr<condition_variable_any> cv) : token(t), cv(cv) {}
176 };
177
178 struct TrackedEvent : public RegisteredEvent
179 {
180 bool sentToServer = false;
182 };
183
184 using responseMap_t = map<uint32_t, TrackedResponse>;
185 using requestMap_t = map<uint32_t, TrackedRequest>;
186 using eventMap_t = map<uint32_t, TrackedEvent>;
187
188 struct TempListResult {
189 atomic<LookupItemType> listType { LookupItemType::None };
190 uint32_t token { 0 };
191 shared_ptr<condition_variable_any> cv = make_shared<condition_variable_any>();
192 atomic<Clock::time_point> nextTimeout {};
194 shared_mutex mutex;
195
196 ListResult::listResult_t getResult()
197 {
198 shared_lock lock(mutex);
199 return ListResult::listResult_t(result);
200 }
201
202 void reset() {
203 unique_lock lock(mutex);
204 listType = LookupItemType::None;
205 result.clear();
206 }
207
208 } listResult;
209
210 struct ProgramSettings {
211 filesystem::path logFilePath;
212 int networkConfigId = -1;
213 uint32_t networkTimeout = 1000;
214 LogLevel logLevels[2][3] = {
215 // Console File Remote
216 { LogLevel::Info, LogLevel::Info, LogLevel::None }, // Client
217 { LogLevel::None, LogLevel::None, LogLevel::None } // Server
218 };
219 LogLevel &logLevel(LogSource s, LogFacility f) { return logLevels[srcIndex(s)][facIndex(f)]; }
220 LogLevel logLevel(LogSource s, LogFacility f) const { return f > LogFacility::None && f <= LogFacility::Remote ? logLevels[srcIndex(s)][facIndex(f)] : LogLevel::None; }
221 static int srcIndex(LogSource s) { return s == LogSource::Server ? 1 : 0; }
222 static int facIndex(LogFacility f) { return f == LogFacility::Remote ? 2 : f == LogFacility::File ? 1 : 0; }
223 } settings;
224
225 friend class WASimClient;
226 WASimClient *const q;
227
228 const uint32_t clientId;
229 const string clientName;
230 atomic<ClientStatus> status = ClientStatus::Idle;
231 uint32_t serverVersion = 0;
232 atomic<Clock::time_point> serverLastSeen = Clock::time_point();
233 atomic_size_t totalDataAlloc = 0;
234 atomic_uint32_t nextDefId = SIMCONNECTID_LAST;
235 atomic_uint32_t nextCmdToken = 1;
236 atomic_bool runDispatchLoop = false;
237 atomic_bool simConnected = false;
238 atomic_bool serverConnected = false;
239 atomic_bool logCDAcreated = false;
240 atomic_bool requestsPaused = false;
241
242 HANDLE hSim = nullptr;
243 HANDLE hSimEvent = nullptr;
244 HANDLE hDispatchStopEvent = nullptr;
245 thread dispatchThread;
246
247 clientEventCallback_t eventCb = nullptr; // main and dispatch threads (+ "on SimConnect_Quit" thread)
248 listResultsCallback_t listCb = nullptr; // list result wait thread
249 dataCallback_t dataCb = nullptr; // dispatch thread
250 logCallback_t logCb = nullptr; // main and dispatch threads
251 commandCallback_t cmdResultCb = nullptr; // dispatch thread
252 commandCallback_t respCb = nullptr; // dispatch thread
253
254 mutable shared_mutex mtxResponses;
255 mutable shared_mutex mtxRequests;
256 mutable shared_mutex mtxEvents;
257
258 responseMap_t reponses {};
259 requestMap_t requests {};
260 eventMap_t events {};
261
262 // Cached mapping of Key Event names to actual IDs, used in `sendKeyEvent(string)` convenience overload,
263 // and also to track registered (mapped) custom-named sim Events as used in `registerCustomKeyEvent()` and related methods.
264 unordered_map<std::string, uint32_t> keyEventNameCache {};
265 shared_mutex mtxKeyEventNames;
266 uint32_t nextCustomEventID = CUSTOM_KEY_EVENT_ID_MIN; // auto-generated IDs for custom event registrations
267
268 // Saved mappings of variable names to SIMCONNECT_DATA_DEFINITION_ID values we assigned to them.
269 // Used for setting string values on SimVars, which we do locally since WASM module can't.
270 map<std::string, uint32_t> mappedVarsNameCache {};
271 shared_mutex mtxMappedVarNames;
272 uint32_t nextMappedVarID = 1; // auto-generated IDs for mapping variable names to SimConnect IDs
273
274#pragma endregion
275#pragma region Callback handling templates ----------------------------------------------
276
277#if defined(WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS) && !WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS
278 mutable mutex mtxCallbacks;
279#endif
280
281 // Rvalue arguments version
282 template<typename... Args>
283 void invokeCallbackImpl(function<void(Args...)> f, Args&&... args) const
284 {
285 if (f) {
286 try {
287#if defined(WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS) && !WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS
288 lock_guard lock(mtxCallbacks);
289#endif
290 bind(f, forward<Args>(args)...)();
291 }
292 catch (exception *e) { LOG_ERR << "Exception in callback handler: " << e->what(); }
293 }
294 }
295
296 // const Lvalue arguments version
297 template<typename... Args>
298 void invokeCallbackImpl(function<void(const Args...)> f, const Args... args) const
299 {
300 if (f) {
301 try {
302#if defined(WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS) && !WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS
303 lock_guard lock(mtxCallbacks);
304#endif
305 bind(f, args...)();
306 }
307 catch (exception *e) { LOG_ERR << "Exception in callback handler: " << e->what(); }
308 }
309 }
310
311 // Type deduction helper
312 template<typename F, typename... Args>
313 void invokeCallback(F&& f, Args&&... args) const {
314 invokeCallbackImpl<remove_cv_t<remove_reference_t<Args>>...>(forward<F>(f), forward<Args>(args)...);
315 }
316
317#pragma endregion
318#pragma region Constructor and status ----------------------------------------------
319
320 // Returns integer ID as 8 char hex string using neutral locale formatting.
321 static std::string makeClientName(uint32_t id) {
322 std::ostringstream oss;
323 oss.imbue(std::locale::classic());
324 oss << STREAM_HEX8(id);
325 return oss.str();
326 }
327
328 Private(WASimClient *qptr, uint32_t clientId, const std::string &config) :
329 q{qptr},
330 clientId{clientId},
331 clientName{makeClientName(clientId)}
332 {
333 error_code ec;
334 filesystem::path cwd = filesystem::current_path(ec);
335 if (ec) {
336 cerr << ec.message() << endl;
337 cwd = ".";
338 }
339 settings.logFilePath = cwd;
340 // set default tracked requests limit based on current global setting
341 int requestTrackingMaxRecords = SimConnectHelper::ENABLE_SIMCONNECT_REQUEST_TRACKING ? 200 : 0;
342
343 // Determine config file to use.
344 const char *fn = "client_conf.ini";
345 filesystem::path cfgFile(cwd /+ fn);
346 if (!config.empty()) {
347 error_code ec;
348 filesystem::file_status fs = filesystem::status(config, ec);
349 if (!ec && filesystem::exists(fs)) {
350 cfgFile = config;
351 if (filesystem::is_directory(fs))
352 cfgFile /= fn;
353 }
354 else {
355 cerr << "Config file/path '" << config << "' not found or not accessible. Using default config location " << cwd << endl;;
356 }
357 }
358 // Read initial config from file
359 ifstream is(cfgFile);
360 if (is.good()) {
361 inipp::Ini<char> ini;
362 ini.parse(is);
363 for (const auto &e : ini.errors)
364 cerr << e << endl;
365 string consoleLevel(getEnumName(settings.logLevel(LogSource::Client, LogFacility::Console), LogLevelNames)),
366 fileLevel(getEnumName(settings.logLevel(LogSource::Client, LogFacility::File), LogLevelNames));
367 const auto &logSect = ini.sections["logging"];
368 inipp::get_value(logSect, "logFilepath", settings.logFilePath);
369 inipp::get_value(logSect, "fileLogLevel", fileLevel);
370 inipp::get_value(logSect, "consoleLogLevel", consoleLevel);
371 const auto &netSect = ini.sections["network"];
372 inipp::get_value(netSect, "networkConfigId", settings.networkConfigId);
373 inipp::get_value(netSect, "networkTimeout", settings.networkTimeout);
374 inipp::get_value(netSect, "requestTrackingMaxRecords", requestTrackingMaxRecords);
375 int idx;
376 if ((idx = indexOfString(LogLevelNames, fileLevel.c_str())) > -1)
377 settings.logLevel(LogSource::Client, LogFacility::File) = LogLevel(idx);
378 if ((idx = indexOfString(LogLevelNames, consoleLevel.c_str())) > -1)
379 settings.logLevel(LogSource::Client, LogFacility::Console) = LogLevel(idx);
380 //cout << settings.logFilePath << ';' << fileLevel << '=' << (int)settings.fileLogLevel << ';' << consoleLevel << '=' << (int)settings.consoleLogLevel << ';' << settings.networkConfigId << ';' << settings.networkTimeout << endl;
381 }
382 else {
383 cerr << "Config file '" << cfgFile << "' not found or not accessible. Setting all logging to " << getEnumName(settings.logLevel(LogSource::Client, LogFacility::Console), LogLevelNames)
384 << " levels, with file in " << quoted(settings.logFilePath.string()) << endl;
385 }
386
387 // set up logging
388 settings.logFilePath /= WSMCMND_CLIENT_NAME;
389 setFileLogLevel(settings.logLevel(LogSource::Client, LogFacility::File));
390 setConsoleLogLevel(settings.logLevel(LogSource::Client, LogFacility::Console));
391 // set up request tracking
392 if ((SimConnectHelper::ENABLE_SIMCONNECT_REQUEST_TRACKING = (requestTrackingMaxRecords > 0)))
393 SimConnectHelper::setMaxTrackedRequests(requestTrackingMaxRecords);
394 }
395
396 bool checkInit() const { return hSim != nullptr; }
397 bool isConnected() const { return checkInit() && (status & ClientStatus::Connected) == ClientStatus::Connected; }
398
399 void setStatus(ClientStatus s, const string &msg = "")
400 {
401 static const char * evTypeNames[] = {
402 "None",
403 "Simulator Connecting", "Simulator Connected", "Simulator Disconnecting", "Simulator Disconnected",
404 "Server Connecting", "Server Connected", "Server Disconnected"
405 };
406
407 ClientStatus newStat = status;
408 ClientEvent ev;
409 switch (s) {
412 newStat = s;
413 break;
416 newStat = s;
417 break;
421 break;
424 newStat = (newStat & ~ClientStatus::Connected) | s;
425 break;
428 newStat = (newStat & ~ClientStatus::Connecting) | s;
429 break;
432 newStat |= s;
433 break;
434 }
435 if (newStat == status)
436 return;
437 status = newStat;
438 if (eventCb) {
439 ev.status = status;
440 if (msg == "")
441 ev.message = Utilities::getEnumName(ev.eventType, evTypeNames);
442 else
443 ev.message = msg;
444 invokeCallback(eventCb, move(ev));
445 }
446 }
447
448 // simple thread burner loop to wait on a condition... don't abuse.
449 bool waitCondition(const std::function<bool(void)> predicate, uint32_t timeout, uint32_t sleepMs = 1)
450 {
451 const auto endTime = Clock::now() + chrono::milliseconds(timeout);
452 const auto sf = chrono::milliseconds(sleepMs);
453 while (!predicate() && endTime > Clock::now())
454 this_thread::sleep_for(sf);
455 return predicate();
456 }
457
458#if 0 // unused
459 HRESULT waitConditionAsync(std::function<bool(void)> predicate, uint32_t timeout = 0)
460 {
461 if (predicate())
462 return S_OK;
463 if (!timeout)
464 timeout = settings.networkTimeout;
465 future<bool> taskResult = async(launch::async, &Private::waitCondition, this, predicate, timeout, 1);
466 future_status taskStat = taskResult.wait_until(Clock::now() + chrono::milliseconds(timeout + 50));
467 return (taskStat == future_status::ready ? S_OK : E_TIMEOUT);
468 }
469#endif
470
471#pragma endregion
472#pragma region Local logging ----------------------------------------------
473
474 void setFileLogLevel(LogLevel level)
475 {
476 settings.logLevel(LogSource::Client, LogFacility::File) = level;
477 const string loggerName = string(WSMCMND_CLIENT_NAME) + '_' + clientName;
478 if (logfault::LogManager::Instance().HaveHandler(loggerName))
479 logfault::LogManager::Instance().SetLevel(loggerName, logfault::LogLevel(level));
480 else if (level != LogLevel::None)
481 logfault::LogManager::Instance().AddHandler(
482 make_unique<logfault::FileHandler>(settings.logFilePath.string().c_str(), logfault::LogLevel(level), logfault::FileHandler::Handling::Rotate, loggerName)
483 );
484 }
485
486 void setConsoleLogLevel(LogLevel level)
487 {
488 settings.logLevel(LogSource::Client, LogFacility::Console) = level;
489 if (logfault::LogManager::Instance().HaveHandler(WSMCMND_CLIENT_NAME))
490 logfault::LogManager::Instance().SetLevel(WSMCMND_CLIENT_NAME, logfault::LogLevel(level));
491 else if (level != LogLevel::None)
492 logfault::LogManager::Instance().AddHandler(
493 make_unique<logfault::StreamHandler>(cout, logfault::LogLevel(level), WSMCMND_CLIENT_NAME)
494 );
495 }
496
497 void setCallbackLogLevel(LogLevel level)
498 {
499 settings.logLevel(LogSource::Client, LogFacility::Remote) = level;
500 if (logfault::LogManager::Instance().HaveHandler(clientName))
501 logfault::LogManager::Instance().SetLevel(clientName, logfault::LogLevel(level));
502 }
503
504 // proxy log handler callback method for delivery of log records to remote listener
505 void onProxyLoggerMessage(const logfault::Message &msg) {
506 invokeCallback(logCb, LogRecord((LogLevel)msg.level_, msg.msg_.c_str(), msg.when_), LogSource::Client);
507 }
508
509 void setLogCallback(logCallback_t cb)
510 {
511 logCb = cb;
512 if (cb && !logfault::LogManager::Instance().HaveHandler(clientName)) {
513 logfault::LogManager::Instance().SetHandler(clientName,
514 make_unique<logfault::ProxyHandler>(
515 bind(&Private::onProxyLoggerMessage, this, placeholders::_1),
516 logfault::LogLevel(settings.logLevel(LogSource::Client, LogFacility::Remote)),
517 clientName
518 )
519 );
520 }
521 else if (!cb && logfault::LogManager::Instance().HaveHandler(clientName)) {
522 logfault::LogManager::Instance().RemoveHandler(clientName);
523 }
524 }
525
526#pragma endregion
527#pragma region SimConnect and Server core utilities ----------------------------------------------
528
529 HRESULT connectSimulator(uint32_t timeout) {
530 return connectSimulator(settings.networkConfigId, timeout);
531 }
532
533 HRESULT connectSimulator(int networkConfigId, uint32_t timeout)
534 {
536 return E_FAIL; // unlikely, but prevent recursion
537
538 if (checkInit()) {
539 LOG_INF << WSMCMND_CLIENT_NAME " already initialized.";
540 return E_FAIL;
541 }
542 settings.networkConfigId = networkConfigId;
543 LOG_INF << "Initializing " << WSMCMND_CLIENT_NAME << " v" WSMCMND_VERSION_INFO " for client " << clientName << " with net config ID " << settings.networkConfigId;
545
546 if (!timeout)
547 timeout = settings.networkTimeout;
548
549 if (!hDispatchStopEvent) {
550 hDispatchStopEvent = CreateEvent(nullptr, true, false, nullptr);
551 if (!hDispatchStopEvent) {
552 LOG_ERR << "Failed to CreateEvent() for dispatch loop: " << GetLastError();
553 return E_FAIL;
554 }
555 }
556
557 hSimEvent = CreateEvent(nullptr, false, false, nullptr);
558 simConnected = false;
559
560 // try to open connection; may return S_OK, E_FAIL, or E_INVALIDARG. May also time out and never actually send the Open event message.
561 HRESULT hr = SimConnect_Open(&hSim, clientName.c_str(), nullptr, 0, hSimEvent, settings.networkConfigId);
562
563 if FAILED(hr) {
564 LOG_ERR << "Network connection to Simulator failed with result: " << LOG_HR(hr);
565 hSim = nullptr;
566 setStatus(ClientStatus::Idle);
567 return hr;
568 }
569 // start message dispatch loop. required to do this now in order to receive the SimConnect Open event and finish the connection process.
570 ResetEvent(hDispatchStopEvent);
571 dispatchThread = thread(&Private::dispatchLoop, this);
572 dispatchThread.detach();
573 // wait for dispatch loop to start
574 if (!waitCondition([&]() { return !!runDispatchLoop; }, 10)) {
575 LOG_CRT << "Could not start dispatch loop thread.";
577 return E_FAIL;
578 }
579 // wait until sim connects or times out or disconnectSimulator() is called.
580 if (!waitCondition([&]() { return !runDispatchLoop || simConnected; }, timeout)) {
581 LOG_ERR << "Network connection to Simulator timed out after " << timeout << "ms.";
583 return E_TIMEOUT;
584 }
585
586 // bail out if connection was canceled while waiting
587 if (!runDispatchLoop)
588 return E_FAIL;
589
590 // register server's Connect command event, exit on failure
591 if FAILED(hr = SimConnectHelper::newClientEvent(hSim, CLI_EVENT_CONNECT, EVENT_NAME_CONNECT)) {
593 return hr;
594 }
595 // register server's ping command event
596 SimConnectHelper::newClientEvent(hSim, CLI_EVENT_PING, EVENT_NAME_PING);
597 // register and subscribe to server ping response (the server will append the name of our client to a new named event and trigger it).
598 SimConnectHelper::newClientEvent(hSim, CLI_EVENT_PING_RESP, EVENT_NAME_PING_PFX + clientName, NOTIFY_GROUP_PING, SIMCONNECT_GROUP_PRIORITY_HIGHEST);
599
600 // register area for writing commands (this is read-only for the server)
601 if FAILED(hr = registerDataArea(CDA_NAME_CMD_PFX, CLI_DATA_COMMAND, CLI_DATA_COMMAND, sizeof(Command), true)) {
603 return hr;
604 }
605 // register command response area for reading (server can write to this channel)
606 if FAILED(hr = registerDataArea(CDA_NAME_RESP_PFX, CLI_DATA_RESPONSE, CLI_DATA_RESPONSE, sizeof(Command), false)) {
608 return hr;
609 }
610 // register CDA for writing data requests (this is read-only for the server)
611 registerDataArea(CDA_NAME_DATA_PFX, CLI_DATA_REQUEST, CLI_DATA_REQUEST, sizeof(DataRequest), true);
612 // register CDA for writing key events (this is read-only for the server)
613 registerDataArea(CDA_NAME_KEYEV_PFX, CLI_DATA_KEYEVENT, CLI_DATA_KEYEVENT, sizeof(KeyEvent), true);
614
615 // start listening on the response channel
616 if FAILED(hr = INVOKE_SIMCONNECT(
617 RequestClientData, hSim, (SIMCONNECT_CLIENT_DATA_ID)CLI_DATA_RESPONSE, (SIMCONNECT_DATA_REQUEST_ID)DATA_REQ_RESPONSE,
618 (SIMCONNECT_CLIENT_DATA_DEFINITION_ID)CLI_DATA_RESPONSE, SIMCONNECT_CLIENT_DATA_PERIOD_ON_SET, (SIMCONNECT_CLIENT_DATA_REQUEST_FLAG)0, 0UL, 0UL, 0UL
619 )) {
621 return hr;
622 }
623
624 // possibly re-register SimConnect CDAs for any existing data requests
625 registerAllDataRequestAreas();
626 // re-register all Simulator Custom Events
627 mapAllCustomKeyEvents();
628
630 return S_OK;
631 }
632
633 // onQuitEvent == true when SimConnect is shutting down and sends Quit, in which case we don't try to use SimConnect again.
634 void disconnectSimulator(bool onQuitEvent = false)
635 {
636 if (!checkInit()) {
637 LOG_INF << "No network connection. Shutdown complete.";
638 setStatus(ClientStatus::Idle);
639 return;
640 }
641
642 LOG_DBG << "Shutting down...";
643 disconnectServer(!onQuitEvent);
645
646 if (!!hDispatchStopEvent) {
647 SetEvent(hDispatchStopEvent); // signal thread to exit
648 //d->dispatchThread.join();
649
650 // check for dispatch loop thread shutdown (this is really just for logging/debug purposes)
651 if (!waitCondition([&]() { return !runDispatchLoop; }, DISPATCH_LOOP_WAIT_TIME + 100))
652 LOG_CRT << "Dispatch loop thread still running!";
653 }
654
655 // reset flags/counters
656 simConnected = false;
657 logCDAcreated = false;
658 totalDataAlloc = 0;
659
660 // dispose objects
661 if (hSim && !onQuitEvent)
662 SimConnect_Close(hSim);
663 hSim = nullptr;
664 if (hSimEvent)
665 CloseHandle(hSimEvent);
666 hSimEvent = nullptr;
667
668 {
669 // clear cached var name mappings, new mapping will need to be made if re-connecting
670 unique_lock lock { mtxMappedVarNames };
671 mappedVarsNameCache.clear();
672 nextMappedVarID = 1;
673 }
674
675 setStatus(ClientStatus::Idle);
676 LOG_INF << "Shutdown complete, network disconnected.";
677 }
678
679 HRESULT connectServer(uint32_t timeout)
680 {
682 return E_FAIL; // unlikely, but prevent recursion
683
684 if (!timeout)
685 timeout = settings.networkTimeout;
686
687 HRESULT hr;
688 if (!checkInit() && FAILED(hr = connectSimulator(timeout)))
689 return hr;
690
691 LOG_INF << "Connecting to " WSMCMND_PROJECT_NAME " server...";
692 setStatus(ClientStatus::Connecting);
693
694 // Send the initial "connect" event which we registered in connectSimulator(), using this client's ID as the data parameter.
695 if FAILED(hr = INVOKE_SIMCONNECT(TransmitClientEvent, hSim, SIMCONNECT_OBJECT_ID_USER, (SIMCONNECT_CLIENT_EVENT_ID)CLI_EVENT_CONNECT, (DWORD)clientId, SIMCONNECT_GROUP_PRIORITY_HIGHEST, SIMCONNECT_EVENT_FLAG_GROUPID_IS_PRIORITY)) {
697 return hr;
698 }
699 // The connection response, if any, is written by the server into the "command response" client data area, CLI_DATA_ID_RESPONSE, processed in SIMCONNECT_RECV_ID_CLIENT_DATA
700
701 // wait until server connects or times out or disconnectSimulator() is called.
702 if (!waitCondition([&]() { return !runDispatchLoop || serverConnected; }, timeout)) {
703 LOG_ERR << "Server connection timed out or refused after " << timeout << "ms.";
705 return E_TIMEOUT;
706 }
707
708 LOG_INF << "Connected to " WSMCMND_PROJECT_NAME " server v" << STREAM_HEX8(serverVersion);
709 if ((serverVersion & 0xFF000000) != (WSMCMND_VERSION & 0xFF000000))
710 LOG_WRN << "Server major version does not match WASimClient version " << STREAM_HEX8(WSMCMND_VERSION);
711 setStatus(ClientStatus::Connected);
712
713 // clear any pending list request (unlikely)
714 listResult.reset();
715 // make sure server knows our desired log level and set up data area/request if needed
716 updateServerLogLevel();
717 // set update status of data requests before adding any, in case we don't actually want results yet
718 sendServerCommand(Command(CommandId::Subscribe, (requestsPaused ? 0 : 1)));
719 // (re-)register (or delete) any saved DataRequests
720 registerAllDataRequests();
721 // same with calculator events
722 registerAllEvents();
723
724 return S_OK;
725 }
726
727 void disconnectServer(bool notifyServer = true)
728 {
729 if (!serverConnected || (status & ClientStatus::Connecting) == ClientStatus::Connecting)
730 return;
731
732 // send disconnect command
733 if (notifyServer)
734 sendServerCommand(Command(CommandId::Disconnect));
735
736 // clear command tracking queue
737 unique_lock lock(mtxResponses);
738 reponses.clear();
739
740 serverConnected = false;
741 LOG_INF << "Disconnected from " WSMCMND_PROJECT_NAME " server.";
743 }
744
745 HRESULT registerDataArea(const string &name, SIMCONNECT_CLIENT_DATA_ID cdaID, SIMCONNECT_CLIENT_DATA_DEFINITION_ID cddId, DWORD szOrType, bool readonly, bool nameIsFull = false, float deltaE = 0.0f)
746 {
747 // Map a unique named data storage area to CDA ID.
748 if (!checkInit())
749 return E_NOT_CONNECTED;
750 HRESULT hr;
751 const string cdaName(nameIsFull ? name : name + clientName);
752 if FAILED(hr = SimConnectHelper::registerDataArea(hSim, cdaName, cdaID, cddId, szOrType, true, readonly, deltaE))
753 return hr;
754 const uint32_t alloc = Utilities::getActualValueSize(szOrType);;
755 totalDataAlloc += alloc;
756 LOG_DBG << "Created CDA ID " << cdaID << " named " << quoted(cdaName) << " of size " << alloc << "; Total data allocation : " << totalDataAlloc;
757 return S_OK;
758 }
759
760#pragma endregion
761#pragma region Command sending ----------------------------------------------
762
763 // Sends a const command as provided.
764 HRESULT sendServerCommand(const Command &command) const
765 {
766 if (!isConnected()) {
767 LOG_ERR << "Server not connected, cannot send " << command;
768 return E_NOT_CONNECTED;
769 }
770 LOG_TRC << "Sending command: " << command;
771 return INVOKE_SIMCONNECT(SetClientData, hSim,
772 (SIMCONNECT_CLIENT_DATA_ID)CLI_DATA_COMMAND, (SIMCONNECT_CLIENT_DATA_DEFINITION_ID)CLI_DATA_COMMAND,
773 SIMCONNECT_CLIENT_DATA_SET_FLAG_DEFAULT, 0UL, (DWORD)sizeof(Command), (void *)&command
774 );
775 }
776
777 // Sends command and enqueues a tracked response record with given condition var. Optionally returns new token.
778 HRESULT sendServerCommand(Command &&command, weak_ptr<condition_variable_any> cv, uint32_t *newToken = nullptr)
779 {
780 if (cv.expired())
781 return E_INVALIDARG;
782 if (!command.token)
783 command.token = nextCmdToken++;
784 enqueueTrackedResponse(command.token, cv);
785 const HRESULT hr = sendServerCommand(command);
786 if FAILED(hr) {
787 unique_lock vlock(mtxResponses);
788 reponses.erase(command.token);
789 }
790 if (newToken)
791 *newToken = command.token;
792 return hr;
793 }
794
795 // Sends command and waits for response. Default timeout is settings.networkTimeout.
796 HRESULT sendCommandWithResponse(Command &&command, Command *response, uint32_t timeout = 0)
797 {
798 uint32_t token;
799 shared_ptr<condition_variable_any> cv = make_shared<condition_variable_any>();
800 const CommandId origId = command.commandId;
801 HRESULT hr = sendServerCommand(move(command), weak_ptr(cv), &token);
802 if SUCCEEDED(hr) {
803 if FAILED(hr = waitCommandResponse(token, response, timeout))
804 LOG_ERR << "Command " << Utilities::getEnumName(origId, CommandIdNames) << " timed out after " << (timeout ? timeout : settings.networkTimeout) << "ms.";
805 }
806 return hr;
807 }
808
809#pragma endregion
810#pragma region Command Response tracking ----------------------------------------------
811
812 TrackedResponse *findTrackedResponse(uint32_t token)
813 {
814 shared_lock lock{mtxResponses};
815 const responseMap_t::iterator pos = reponses.find(token);
816 if (pos != reponses.cend())
817 return &pos->second;
818 return nullptr;
819 }
820
821 TrackedResponse *enqueueTrackedResponse(uint32_t token, weak_ptr<condition_variable_any> cv)
822 {
823 unique_lock lock{mtxResponses};
824 return &reponses.try_emplace(token, token, cv).first->second;
825 }
826
827 // Blocks and waits for a response to a specific command token. `timeout` can be -1 to use `extraPredicate` only (which is then required).
828 HRESULT waitCommandResponse(uint32_t token, Command *response, uint32_t timeout = 0, std::function<bool(void)> extraPredicate = nullptr)
829 {
830 TrackedResponse *tr = findTrackedResponse(token);
831 shared_ptr<condition_variable_any> cv {};
832 if (!tr || !(cv = tr->cv.lock()))
833 return E_FAIL;
834
835 if (!timeout)
836 timeout = settings.networkTimeout;
837
838 bool stopped = false;
839 auto stop_waiting = [=, &stopped]() {
840 return stopped =
841 !serverConnected || (tr->response.commandId != CommandId::None && tr->response.token == token && (!extraPredicate || extraPredicate()));
842 };
843
844 HRESULT hr = E_TIMEOUT;
845 if (stop_waiting()) {
846 hr = S_OK;
847 }
848 else {
849 unique_lock lock(tr->mutex);
850 if (timeout > 0) {
851 if (cv->wait_for(lock, chrono::milliseconds(timeout), stop_waiting))
852 hr = S_OK;
853 }
854 else if (!!extraPredicate) {
855 cv->wait(lock, stop_waiting);
856 hr = stopped ? ERROR_ABANDONED_WAIT_0 : S_OK;
857 }
858 else {
859 hr = E_INVALIDARG;
860 LOG_DBG << "waitCommandResponse() requires a predicate condition when timeout parameter is < 0.";
861 }
862 }
863 if (SUCCEEDED(hr) && !!response) {
864 unique_lock lock(tr->mutex);
865 *response = move(tr->response);
866 }
867
868 unique_lock vlock(mtxResponses);
869 reponses.erase(token);
870 return hr;
871 }
872
873#if 0 // not used
874 // Waits for a response to a specific command token using an async thread.
875 HRESULT waitCommandResponseAsync(uint32_t token, Command *response, uint32_t timeout = 0, std::function<bool(void)> extraPredicate = nullptr)
876 {
877 if (!timeout)
878 timeout = settings.networkTimeout;
879 future<HRESULT> taskResult = async(launch::async, &Private::waitCommandResponse, this, token, response, timeout, extraPredicate);
880 const future_status taskStat = taskResult.wait_for(chrono::milliseconds(timeout * 2));
881 if (taskStat == future_status::ready)
882 return taskResult.get();
883 return E_TIMEOUT;
884 }
885#endif
886
887 // Waits for completion of list request results, or a timeout. This is used on a new thread.
888 void waitListRequestEnd()
889 {
890 auto stop_waiting = [this]() {
891 return listResult.nextTimeout.load() >= Clock::now();
892 };
893
894 Command response;
895 HRESULT hr = waitCommandResponse(listResult.token, &response, -1, stop_waiting);
896 if (hr == ERROR_ABANDONED_WAIT_0) {
897 LOG_ERR << "List request timed out.";
898 hr = E_TIMEOUT;
899 }
900 else if (hr != S_OK) {
901 LOG_ERR << "List request failed with result: " << LOG_HR(hr);
902 }
903 else if (response.commandId != CommandId::Ack) {
904 LOG_WRN << "Server returned Nak for list request of " << Utilities::getEnumName(listResult.listType.load(), LookupItemTypeNames);
905 hr = E_FAIL;
906 }
907 if (listCb)
908 invokeCallback(listCb, ListResult { listResult.listType.load(), hr, listResult.getResult() });
909 listResult.reset(); // reset to none to indicate the current list request is finished
910 }
911
912#pragma endregion
913#pragma region Server-side logging commands ----------------------------------------------
914
915 void setServerLogLevel(LogLevel level, LogFacility fac = LogFacility::Remote) {
916 if (fac == LogFacility::None || fac > LogFacility::All)
917 return;
918 if (+fac & +LogFacility::Remote)
919 settings.logLevel(LogSource::Server, LogFacility::Remote) = level;
920 if (+fac & +LogFacility::Console)
921 settings.logLevel(LogSource::Server, LogFacility::Console) = level;
922 if (+fac & +LogFacility::File)
923 settings.logLevel(LogSource::Server, LogFacility::File) = level;
924
925 updateServerLogLevel(fac);
926 }
927
928 void updateServerLogLevel(LogFacility fac = LogFacility::Remote) {
929 if (checkInit() && (!(+fac & +LogFacility::Remote) || SUCCEEDED(registerLogDataArea())))
930 sendServerCommand(Command(CommandId::Log, +settings.logLevel(LogSource::Server, fac), nullptr, (double)fac));
931 }
932
933 HRESULT registerLogDataArea() {
934 if (logCDAcreated || settings.logLevel(LogSource::Server, LogFacility::Remote) == LogLevel::None)
935 return S_OK;
936 HRESULT hr;
937 // register log area for reading; server can write to this channel
938 if SUCCEEDED(hr = registerDataArea(CDA_NAME_LOG_PFX, CLI_DATA_LOG, CLI_DATA_LOG, sizeof(LogRecord), false)) {
939 // start/stop listening on the log channel
940 INVOKE_SIMCONNECT(RequestClientData, hSim, (SIMCONNECT_CLIENT_DATA_ID)CLI_DATA_LOG, (SIMCONNECT_DATA_REQUEST_ID)DATA_REQ_LOG,
941 (SIMCONNECT_CLIENT_DATA_DEFINITION_ID)CLI_DATA_LOG, SIMCONNECT_CLIENT_DATA_PERIOD_ON_SET, 0UL, 0UL, 0UL, 0UL);
942 }
943 logCDAcreated = SUCCEEDED(hr);
944 return hr;
945 }
946
947#pragma endregion
948#pragma region Remote Variables accessors ----------------------------------------------
949
950 // shared for getVariable() and setLocalVariable()
951 const string buildVariableCommandString(const VariableRequest &v, bool isLocal) const
952 {
953 const bool isIndexed = isLocal || Utilities::isIndexedVariableType(v.variableType);
954 const bool hasUnits = isLocal || Utilities::isUnitBasedVariableType(v.variableType);
955 string sValue = (isIndexed && v.variableId > -1 ? to_string(v.variableId) : v.variableName);
956 if (sValue.empty())
957 return sValue;
958 if (v.variableType == 'A' && v.simVarIndex)
959 sValue += ':' + to_string(v.simVarIndex);
960 if (hasUnits) {
961 if (isIndexed && v.unitId > -1)
962 sValue += ',' + to_string(v.unitId);
963 else if (!v.unitName.empty())
964 sValue += ',' + v.unitName;
965 }
966 return sValue;
967 }
968
969 HRESULT getVariable(const VariableRequest &v, double *result, std::string *sResult = nullptr, double dflt = 0.0)
970 {
971 const string sValue = buildVariableCommandString(v, false);
972 if (sValue.empty() || sValue.length() >= STRSZ_CMD)
973 return E_INVALIDARG;
974
975 HRESULT hr;
976 Command response;
977 if FAILED(hr = sendCommandWithResponse(Command(v.createLVar && v.variableType == 'L' ? CommandId::GetCreate : CommandId::Get, v.variableType, sValue.c_str(), dflt), &response))
978 return hr;
979 if (response.commandId != CommandId::Ack) {
980 LOG_WRN << "Get Variable request for " << quoted(sValue) << " returned Nak response. Reason, if any: " << quoted(response.sData);
981 return E_FAIL;
982 }
983 if (result)
984 *result = response.fData;
985 if (sResult)
986 *sResult = response.sData;
987 return S_OK;
988 }
989
990 HRESULT setLocalVariable(const VariableRequest &v, const double value)
991 {
992 const string sValue = buildVariableCommandString(v, true);
993 if (sValue.empty() || sValue.length() >= STRSZ_CMD)
994 return E_INVALIDARG;
995 return sendServerCommand(Command(v.createLVar ? CommandId::SetCreate : CommandId::Set, 'L', sValue.c_str(), value));
996 }
997
998 HRESULT setVariable(const VariableRequest &v, const double value)
999 {
1000 if (v.variableType == 'L')
1001 return setLocalVariable(v, value);
1002
1003 if (!Utilities::isSettableVariableType(v.variableType)) {
1004 LOG_WRN << "Cannot Set a variable of type '" << v.variableType << "'.";
1005 return E_INVALIDARG;
1006 }
1007 // All other variable types can only be set via calculator code and require a string-based name
1008 if (v.variableName.empty()) {
1009 LOG_WRN << "A variable name is required to set variable of type '" << v.variableType << "'.";
1010 return E_INVALIDARG;
1011 }
1012 if (v.unitId > -1 && v.unitName.empty()) {
1013 LOG_WRN << "A variable of type '" << v.variableType << "' unit ID; use a unit name instead.";
1014 return E_INVALIDARG;
1015 }
1016
1017 // Build the RPN code string.
1018 ostringstream codeStr = ostringstream();
1019 codeStr.imbue(std::locale::classic());
1020 codeStr << fixed << setprecision(7) << value;
1021 codeStr << " (>" << v.variableType << ':' << v.variableName;
1022 if (v.variableType == 'A' && v.simVarIndex)
1023 codeStr << ':' << (uint16_t)v.simVarIndex;
1024 if (!v.unitName.empty())
1025 codeStr << ',' << v.unitName;
1026 codeStr << ')';
1027
1028 if (codeStr.tellp() == -1 || (size_t)codeStr.tellp() >= STRSZ_CMD)
1029 return E_INVALIDARG;
1030
1031 // Send it.
1032 return sendServerCommand(Command(CommandId::Exec, +CalcResultType::None, codeStr.str().c_str()));
1033 }
1034
1035 HRESULT setStringVariable(const VariableRequest &v, const std::string &value)
1036 {
1037 if (!checkInit()) {
1038 LOG_ERR << "Simulator not connected, cannot set string variable.";
1039 return E_NOT_CONNECTED;
1040 }
1041 if (v.variableType != 'A') {
1042 LOG_WRN << "Cannot Set a string value on variable of type '" << v.variableType << "'.";
1043 return E_INVALIDARG;
1044 }
1045 if (v.variableName.empty()) {
1046 LOG_WRN << "A variable name is required to set variable of type '" << v.variableType << "'.";
1047 return E_INVALIDARG;
1048 }
1049
1050 uint32_t mapId = 0;
1051 {
1052 // Check for existing mapped ID
1053 shared_lock lock { mtxMappedVarNames };
1054 const auto pos = mappedVarsNameCache.find(v.variableName);
1055 if (pos != mappedVarsNameCache.cend())
1056 mapId = pos->second;
1057 }
1058 if (!mapId) {
1059 // Create a new mapping and save it
1060 HRESULT hr;
1061 if FAILED(hr = INVOKE_SIMCONNECT(AddToDataDefinition, hSim, (SIMCONNECT_DATA_DEFINITION_ID)nextMappedVarID, v.variableName.c_str(), (const char*)nullptr, SIMCONNECT_DATATYPE_STRINGV, 0.0f, SIMCONNECT_UNUSED))
1062 return hr;
1063 mapId = nextMappedVarID++;
1064 unique_lock lock { mtxMappedVarNames };
1065 mappedVarsNameCache.insert(std::pair{ v.variableName, mapId });
1066 }
1067 // Use SimConnect to set the value.
1068 return INVOKE_SIMCONNECT(SetDataOnSimObject, hSim, (SIMCONNECT_DATA_DEFINITION_ID)mapId, SIMCONNECT_OBJECT_ID_USER, 0UL, 0UL, (DWORD)(value.length() + 1), (void*)value.c_str());
1069 }
1070
1071#pragma endregion
1072#pragma region Data Requests ----------------------------------------------
1073
1074 TrackedRequest *findRequest(uint32_t id)
1075 {
1076 shared_lock lock{mtxRequests};
1077 const requestMap_t::iterator pos = requests.find(id);
1078 if (pos != requests.cend())
1079 return &pos->second;
1080 return nullptr;
1081 }
1082
1083 const TrackedRequest *findRequest(uint32_t id) const {
1084 shared_lock lock{mtxRequests};
1085 const requestMap_t::const_iterator pos = requests.find(id);
1086 if (pos != requests.cend())
1087 return &pos->second;
1088 return nullptr;
1089 }
1090
1091 // Writes DataRequest data to the corresponding CDA. If the DataRequest::requestType == None, the request will be deleted by the server.
1092 HRESULT writeDataRequest(const DataRequest &req)
1093 {
1094 if (!isConnected()) {
1095 LOG_ERR << "Server not connected, cannot submit DataRequest " << req;
1096 return E_NOT_CONNECTED;
1097 }
1098 LOG_DBG << "Sending request: " << req;
1099 return INVOKE_SIMCONNECT(SetClientData, hSim, (SIMCONNECT_CLIENT_DATA_ID)CLI_DATA_REQUEST, (SIMCONNECT_CLIENT_DATA_DEFINITION_ID)CLI_DATA_REQUEST, SIMCONNECT_CLIENT_DATA_SET_FLAG_DEFAULT, 0UL, (DWORD)sizeof(DataRequest), (void *)&req);
1100 }
1101
1102 // Writes DataRequest data to the corresponding CDA and waits for an Ack/Nak from server.
1103 // If the DataRequest::requestType == None, the request will be deleted by the server and the response wait is skipped.
1104 HRESULT sendDataRequest(const DataRequest &req, bool async)
1105 {
1106 HRESULT hr;
1107 if FAILED(hr = writeDataRequest(req))
1108 return hr;
1109
1110 // check if just deleting an existing request and don't wait around for that response
1111 if (async || req.requestType == RequestType::None)
1112 return hr;
1113
1114 shared_ptr<condition_variable_any> cv = make_shared<condition_variable_any>();
1115 enqueueTrackedResponse(req.requestId, weak_ptr(cv));
1116 Command response;
1117 if FAILED(hr = waitCommandResponse(req.requestId, &response)) {
1118 LOG_ERR << "Data Request timed out after " << settings.networkTimeout << "ms.";
1119 }
1120 else if (response.commandId != CommandId::Ack) {
1121 hr = E_FAIL;
1122 LOG_ERR << "Data Request returned Nak response for DataRequest ID " << req.requestId << ". Reason, if any: " << quoted(response.sData);
1123 }
1124 return hr;
1125 }
1126
1127 HRESULT registerDataRequestArea(const TrackedRequest * const tr, bool isNewRequest, bool dataAllocChanged = false)
1128 {
1129 HRESULT hr;
1130 shared_lock lock{mtxRequests};
1131 if (isNewRequest || dataAllocChanged) {
1132 if (isNewRequest) {
1133 // Create & allocate the data area which will hold result value (server can write to this channel)
1134 if FAILED(hr = registerDataArea(CDA_NAME_DATA_PFX + clientName + '.' + to_string(tr->requestId), tr->dataId, tr->dataId, tr->valueSize, false, true, max(tr->deltaEpsilon, 0.0f)))
1135 return hr;
1136 }
1137 else if (dataAllocChanged) {
1138 // remove definition, ignore errors (they will be logged)
1139 deregisterDataRequestArea(tr);
1140 // re-add definition, and now do not ignore errors
1141 if FAILED(hr = SimConnectHelper::addClientDataDefinition(hSim, tr->dataId, tr->valueSize, max(tr->deltaEpsilon, 0.0f)))
1142 return hr;
1143 }
1144 }
1145 // (re)start listening for result
1146 return INVOKE_SIMCONNECT(
1147 RequestClientData, hSim,
1148 (SIMCONNECT_CLIENT_DATA_ID)tr->dataId, (SIMCONNECT_DATA_REQUEST_ID)tr->requestId + SIMCONNECTID_LAST,
1149 (SIMCONNECT_CLIENT_DATA_DEFINITION_ID)tr->dataId, SIMCONNECT_CLIENT_DATA_PERIOD_ON_SET, (tr->deltaEpsilon < 0.0f ? 0UL : SIMCONNECT_CLIENT_DATA_REQUEST_FLAG_CHANGED), 0UL, 0UL, 0UL
1150 );
1151 }
1152
1153 // Note that this method does NOT acquire the requests mutex.
1154 HRESULT deregisterDataRequestArea(const TrackedRequest * const tr) const
1155 {
1156 // We need to first suspend updates for this request before removing it, otherwise it seems SimConnect will sometimes crash
1157 INVOKE_SIMCONNECT(
1158 RequestClientData, hSim,
1159 (SIMCONNECT_CLIENT_DATA_ID)tr->dataId, (SIMCONNECT_DATA_REQUEST_ID)tr->requestId + SIMCONNECTID_LAST,
1160 (SIMCONNECT_CLIENT_DATA_DEFINITION_ID)tr->dataId, SIMCONNECT_CLIENT_DATA_PERIOD_NEVER, 0UL, 0UL, 0UL, 0UL
1161 );
1162 // Now we can safely (we hope) remove the SimConnect definition.
1163 return SimConnectHelper::removeClientDataDefinition(hSim, tr->dataId);
1164 }
1165
1166 HRESULT addOrUpdateRequest(const DataRequest &req, bool async)
1167 {
1168 if (req.requestType == RequestType::None)
1169 return removeRequest(req.requestId);
1170
1171 // preliminary validation
1172 if (req.nameOrCode[0] == '\0') {
1173 LOG_ERR << "Error in DataRequest ID: " << req.requestId << "; Parameter 'nameOrCode' cannot be empty.";
1174 return E_INVALIDARG;
1175 }
1176
1177 const uint32_t actualValSize = Utilities::getActualValueSize(req.valueSize);
1178 if (actualValSize > SIMCONNECT_CLIENTDATA_MAX_SIZE) {
1179 LOG_ERR << "Error in DataRequest ID: " << req.requestId << "; Value size " << actualValSize << " exceeds SimConnect maximum size " << SIMCONNECT_CLIENTDATA_MAX_SIZE;
1180 return E_INVALIDARG;
1181 }
1182 if (totalDataAlloc + actualValSize > SIMCONNECT_DATA_ALLOC_LIMIT) {
1183 LOG_ERR << "Error in DataRequest ID: " << req.requestId << "; Adding request with value size " << actualValSize << " would exceed SimConnect total maximum size of " << SIMCONNECT_DATA_ALLOC_LIMIT;
1184 return E_INVALIDARG;
1185 }
1186
1187 TrackedRequest *tr = findRequest(req.requestId);
1188 const bool isNewRequest = (tr == nullptr);
1189 bool dataAllocationChanged = false;
1190
1191 if (isNewRequest) {
1192 unique_lock lock{mtxRequests};
1193 tr = &requests.try_emplace(req.requestId, req, nextDefId++).first->second;
1194 }
1195 else {
1196 if (actualValSize > tr->dataSize) {
1197 LOG_ERR << "Value size cannot be increased after request is created.";
1198 return E_INVALIDARG;
1199 }
1200 // flag if the definition size or the delta E has changed
1201 dataAllocationChanged = (actualValSize != tr->dataSize || !fuzzyCompare(req.deltaEpsilon, tr->deltaEpsilon));
1202 // update the tracked request from new request data
1203 unique_lock lock{mtxRequests};
1204 *tr = req;
1205 }
1206
1207 HRESULT hr = S_OK;
1208 if (isConnected()) {
1209 // Register the CDA and subscribe to data value changes
1210 if (isNewRequest || dataAllocationChanged)
1211 hr = registerDataRequestArea(tr, isNewRequest, dataAllocationChanged);
1212 if SUCCEEDED(hr) {
1213 // send the request and wait for Ack; Request may timeout or return a Nak.
1214 hr = sendDataRequest(req, async);
1215 }
1216 if (FAILED(hr) && isNewRequest) {
1217 // delete a new request if anything failed
1218 removeRequest(req.requestId);
1219 }
1220 LOG_TRC << (FAILED(hr) ? "FAILED" : isNewRequest ? "Added" : "Updated") << " request: " << req;
1221 }
1222 else {
1223 LOG_TRC << "Queued " << (isNewRequest ? "New" : "Updated") << " request: " << req;
1224 }
1225
1226 return hr;
1227 }
1228
1229 HRESULT removeRequest(const uint32_t requestId)
1230 {
1231 TrackedRequest *tr = findRequest(requestId);
1232 if (!tr) {
1233 LOG_WRN << "DataRequest ID " << requestId << " not found.";
1234 return E_FAIL;
1235 }
1236
1237 unique_lock lock{mtxRequests};
1238 if (isConnected()) {
1239 if FAILED(deregisterDataRequestArea(tr))
1240 LOG_WRN << "Failed to clear ClientDataDefinition in SimConnect, check log messages.";
1241 if FAILED(writeDataRequest(DataRequest(requestId, 0, RequestType::None)))
1242 LOG_WRN << "Server removal of request " << requestId << " failed or timed out, check log messages.";
1243 }
1244 requests.erase(requestId);
1245 LOG_TRC << "Removed Data Request " << requestId;
1246 return S_OK;
1247 }
1248
1249 // this (re)registers all saved data requests with the server (not SimConnect), or deletes any requests flagged for deletion while offline.
1250 // called from connectServer()
1251 void registerAllDataRequests()
1252 {
1253 if (!isConnected())
1254 return;
1255 shared_lock lock(mtxRequests);
1256 for (const auto & [_, tr] : requests) {
1257 if (tr.requestType != RequestType::None)
1258 writeDataRequest(tr);
1259 }
1260 }
1261
1262 // this only creates the SimConnect data definitions, not the server-side entries
1263 // called from connectSimulator()
1264 void registerAllDataRequestAreas()
1265 {
1266 if (!checkInit())
1267 return;
1268 for (const auto & [_, tr] : requests)
1269 registerDataRequestArea(&tr, true);
1270 }
1271
1272#pragma endregion Data Requests
1273
1274#pragma region Calculator Events --------------------------------------------
1275
1276 TrackedEvent *findTrackedEvent(uint32_t eventId)
1277 {
1278 shared_lock lock{mtxEvents};
1279 const eventMap_t::iterator pos = events.find(eventId);
1280 if (pos != events.cend())
1281 return &pos->second;
1282 return nullptr;
1283 }
1284
1285 // sends event registration to server. If event is being deleted and sending succeeds, event is removed from tracked events list.
1286 HRESULT sendEventRegistration(TrackedEvent *ev, bool resendName = false)
1287 {
1288 if (!ev)
1289 return E_INVALIDARG;
1290 HRESULT hr;
1291 if (ev->code.empty()) { // delete event
1292 hr = sendServerCommand(Command(CommandId::Register, ev->eventId));
1293 if SUCCEEDED(hr) {
1294 unique_lock lock(mtxEvents);
1295 events.erase(ev->eventId);
1296 }
1297 return hr;
1298 }
1299
1300 const string evStr = (!ev->sentToServer || resendName) && !ev->name.empty() ? ev->name + '$' + ev->code : ev->code;
1301 return sendServerCommand(Command(CommandId::Register, ev->eventId, evStr.c_str()));
1302 }
1303
1304 HRESULT sendEventRegistration(uint32_t eventId) {
1305 return sendEventRegistration(findTrackedEvent(eventId));
1306 }
1307
1308 // called from connectServer()
1309 void registerAllEvents()
1310 {
1311 vector<uint32_t> removals {};
1312 shared_lock lock(mtxEvents);
1313 for (auto & [key, ev] : events) {
1314 if (ev.code.empty())
1315 removals.push_back(key);
1316 else
1317 sendEventRegistration(&ev, true);
1318 }
1319 lock.unlock();
1320 for (const uint32_t &id : removals)
1321 sendEventRegistration(id);
1322 }
1323
1324#pragma endregion
1325
1326#pragma region Simulator Key Events -------------------------------------------
1327
1328 // Writes KeyEvent data to the corresponding CDA.
1329 HRESULT writeKeyEvent(const KeyEvent &kev) const
1330 {
1331 if (!isConnected()) {
1332 LOG_ERR << "Server not connected, cannot send " << kev;
1333 return E_NOT_CONNECTED;
1334 }
1335 LOG_TRC << "Sending: " << kev;
1336 return INVOKE_SIMCONNECT(SetClientData, hSim, (SIMCONNECT_CLIENT_DATA_ID)CLI_DATA_KEYEVENT, (SIMCONNECT_CLIENT_DATA_DEFINITION_ID)CLI_DATA_KEYEVENT, SIMCONNECT_CLIENT_DATA_SET_FLAG_DEFAULT, 0UL, (DWORD)sizeof(KeyEvent), (void *)&kev);
1337 }
1338
1339 // Custom Events
1340
1341 // This private version does _not_ check if the event name is valid. The `mtxKeyEventNames` _must_ be unlocked before entry.
1342 HRESULT registerCustomKeyEvent(const std::string &name, uint32_t *pRetId, bool useLegacy)
1343 {
1344 // Prevent multiple threads from registering the same Event
1345 unique_lock lock(mtxKeyEventNames);
1346 // Check for duplicates.
1347 const auto pos = keyEventNameCache.find(name);
1348 if (pos != keyEventNameCache.cend()) {
1349 // Custom Event already exists - return the eventId if required
1350 if (pRetId)
1351 *pRetId = pos->second;
1352 return S_OK;
1353 }
1354
1355 // Register a new event mapping with a unique ID. These start at CUSTOM_KEY_EVENT_ID_MIN.
1356 uint32_t keyId = nextCustomEventID++;
1357 // Use high bit as flag of ID for forcing "legacy" SimConnect_TransmitClientEvent() usage as trigger.
1358 if (useLegacy)
1359 keyId |= CUSTOM_KEY_EVENT_LEGACY_TRIGGER_FLAG;
1360 // Cache the event name and ID. This is possibly used for lookups later in `sendKeyEvent(string)` and/or to register event mappings when (re)connecting to SimConnect.
1361 keyEventNameCache.insert(std::pair{ name, keyId });
1362
1363 if (pRetId)
1364 *pRetId = keyId;
1365
1366 // Try to register the new Event now if already connected to SimConnect.
1367 if (simConnected)
1368 return mapCustomKeyEvent(keyId, name);
1369 LOG_DBG << "Queued registration of custom simulator key event " << quoted(name) << " with ID " << keyId << ".";
1370 return S_OK;
1371 }
1372
1373 // Invokes SimConnect_MapClientEventToSimEvent() with given name and id
1374 HRESULT mapCustomKeyEvent(uint32_t id, const std::string &name) const
1375 {
1376 const HRESULT hr = SimConnectHelper::newClientEvent(hSim, id, name);
1377 if SUCCEEDED(hr)
1378 LOG_DBG << "Registered custom simulator key event " << quoted(name) << " with id " << id << ".";
1379 else
1380 LOG_ERR << "Registration custom simulator key event " << quoted(name) << " for id " << id << " failed with HRESULT " << LOG_HR(hr);
1381 return hr;
1382 }
1383
1384 // called from connectSimulator()
1385 void mapAllCustomKeyEvents()
1386 {
1387 shared_lock lock(mtxKeyEventNames);
1388 for (const auto& [name, id] : keyEventNameCache) {
1389 if (id >= CUSTOM_KEY_EVENT_ID_MIN)
1390 mapCustomKeyEvent(id, name);
1391 }
1392 }
1393
1394 // Sends a registered Custom Event using TransmitClientEvent(_EX1)
1395 HRESULT sendSimCustomKeyEvent(uint32_t keyEventId, DWORD v1, DWORD v2, DWORD v3, DWORD v4, DWORD v5) const
1396 {
1397 if (!simConnected) {
1398 LOG_ERR << "Simulator not connected, cannot send a Custom Key Event.";
1399 return E_NOT_CONNECTED;
1400 }
1401
1402 LOG_TRC << "Invoking " << ((keyEventId & CUSTOM_KEY_EVENT_LEGACY_TRIGGER_FLAG) ? "TransmitClientEvent" : "TransmitClientEvent_EX1") << " with Event ID " << keyEventId << "; values: " << v1 << "; " << v2 << "; " << v3 << "; " << v4 << "; " << v5 << ';';
1403 if (keyEventId & CUSTOM_KEY_EVENT_LEGACY_TRIGGER_FLAG)
1404 return INVOKE_SIMCONNECT(TransmitClientEvent, hSim, SIMCONNECT_OBJECT_ID_USER, (SIMCONNECT_CLIENT_EVENT_ID)keyEventId, v1, SIMCONNECT_GROUP_PRIORITY_HIGHEST, SIMCONNECT_EVENT_FLAG_GROUPID_IS_PRIORITY);
1405 return INVOKE_SIMCONNECT(TransmitClientEvent_EX1, hSim, SIMCONNECT_OBJECT_ID_USER, (SIMCONNECT_CLIENT_EVENT_ID)keyEventId, SIMCONNECT_GROUP_PRIORITY_HIGHEST, SIMCONNECT_EVENT_FLAG_GROUPID_IS_PRIORITY, v1, v2, v3, v4, v5);
1406 }
1407
1408#pragma endregion
1409
1410#pragma region SimConnect message processing ----------------------------------------------
1411
1412 static void CALLBACK dispatchMessage(SIMCONNECT_RECV *pData, DWORD cbData, void *pContext) {
1413 static_cast<Private*>(pContext)->onSimConnectMessage(pData, cbData);
1414 }
1415
1416 void dispatchLoop()
1417 {
1418 LOG_DBG << "Dispatch loop started.";
1419 const HANDLE waitEvents[] = { hDispatchStopEvent, hSimEvent };
1420 DWORD hr;
1421 runDispatchLoop = true;
1422 while (runDispatchLoop) {
1423 hr = WaitForMultipleObjects(2, waitEvents, false, DISPATCH_LOOP_WAIT_TIME);
1424 switch (hr) {
1425 case WAIT_TIMEOUT:
1426 continue;
1427 case WAIT_OBJECT_0 + 1: // hSimEvent
1428 SimConnect_CallDispatch(hSim, Private::dispatchMessage, this);
1429 continue;
1430 case WAIT_OBJECT_0: // hDispatchStopEvent
1431 break;
1432 default:
1433 LOG_ERR << "Dispatch loop WaitForMultipleObjects returned: " << LOG_HR(hr) << " Error: " << LOG_HR(GetLastError());
1434 break;
1435 }
1436 runDispatchLoop = false;
1437 }
1438 LOG_DBG << "Dispatch loop stopped.";
1439 }
1440
1441 void onSimConnectMessage(SIMCONNECT_RECV *pData, DWORD cbData)
1442 {
1443 LOG_TRC << LOG_SC_RECV(pData);
1444 switch (pData->dwID) {
1445
1446 case SIMCONNECT_RECV_ID_CLIENT_DATA: {
1447 SIMCONNECT_RECV_CLIENT_DATA* data = (SIMCONNECT_RECV_CLIENT_DATA*)pData;
1448 LOG_TRC << LOG_SC_RCV_CLIENT_DATA(data);
1449 // 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.
1450 const size_t dataSize = (size_t)pData->dwSize + 4 - sizeof(SIMCONNECT_RECV_CLIENT_DATA);
1451 switch (data->dwRequestID)
1452 {
1453 case DATA_REQ_RESPONSE: {
1454 // be paranoid
1455 if (dataSize != sizeof(Command)) {
1456 LOG_CRT << "Invalid Command struct data size! Expected " << sizeof(Command) << " but got " << dataSize;
1457 return;
1458 }
1459 // dwData is our command struct
1460 const Command *const cmd = reinterpret_cast<const Command *const>(&data->dwData);
1461 LOG_DBG << "Got Command: " << *cmd;
1462 bool checkTracking = true;
1463 switch (cmd->commandId)
1464 {
1465 case CommandId::Ack:
1466 case CommandId::Nak: {
1467 // which command is this ack/nak for?
1468 switch ((CommandId)cmd->uData) {
1469 // Connected response
1470 case CommandId::Connect:
1471 serverConnected = cmd->commandId == CommandId::Ack && cmd->token == clientId;
1472 serverVersion = (uint32_t)cmd->fData;
1473 checkTracking = false;
1474 break;
1475 default:
1476 break;
1477 }
1478 // invoke result callback if anyone is listening
1479 invokeCallback(cmdResultCb, *cmd);
1480 break;
1481 }
1482
1483 // respond to pings to stay alive
1484 case CommandId::Ping:
1485 sendServerCommand(Command(CommandId::Ack, (uint32_t)CommandId::Ping));
1486 checkTracking = false;
1487 break;
1488
1489 // incoming list results, one at a time until Ack of List command.
1490 case CommandId::List: {
1491 unique_lock lock(listResult.mutex);
1492 if (cmd->token == listResult.token) {
1493 listResult.nextTimeout = Clock::now() + chrono::milliseconds(settings.networkTimeout);
1494 listResult.result.emplace_back((int)cmd->uData, cmd->sData);
1495 }
1496 else {
1497 LOG_WRN << "Received unexpected list result for wrong command token. Expected " << listResult.token << " got " << cmd->token;
1498 }
1499 checkTracking = false;
1500 break;
1501 }
1502
1503 // Server is disconnecting (shutting down/etc).
1504 case CommandId::Disconnect:
1505 disconnectServer(false);
1506 checkTracking = false;
1507 break;
1508
1509 default:
1510 break;
1511 } // commandId
1512
1513 // Check if this command response is tracked and possibly awaited.
1514 if (checkTracking) {
1515 if (TrackedResponse *tr = findTrackedResponse(cmd->token)) {
1516 if (auto cv = tr->cv.lock()) {
1517 LOG_TRC << "Got awaited command response, notifying CV.";
1518 //unique_lock lock(mtxResponses);
1519 unique_lock lock(tr->mutex);
1520 tr->response = *cmd;
1521 lock.unlock();
1522 cv->notify_all();
1523 }
1524 else {
1525 unique_lock lock(mtxResponses);
1526 reponses.erase(cmd->token);
1527 LOG_TRC << "Got tracked command response but no CV, removing record.";
1528 }
1529 }
1530 }
1531 // invoke response callback if anyone is listening
1532 invokeCallback(respCb, *cmd);
1533 break;
1534 } // DATA_REQ_RESPONSE
1535
1536 case DATA_REQ_LOG: {
1537 // be paranoid
1538 if (dataSize != sizeof(LogRecord)) {
1539 LOG_CRT << "Invalid LogRecord struct data size! Expected " << sizeof(LogRecord) << " but got " << dataSize;
1540 return;
1541 }
1542 // dwData is our log record struct
1543 const LogRecord *const log = reinterpret_cast<const LogRecord *const>(&data->dwData);
1544 LOG_TRC << "Got Log Record: " << *log;
1545 invokeCallback(logCb, *log, LogSource::Server);
1546 break;
1547 } // DATA_REQ_LOG
1548
1549 // possible request data
1550 default:
1551 if (data->dwRequestID >= SIMCONNECTID_LAST) {
1552 TrackedRequest *tr = findRequest(data->dwRequestID - SIMCONNECTID_LAST);
1553 if (!tr){
1554 LOG_WRN << "DataRequest ID " << data->dwRequestID - SIMCONNECTID_LAST << " not found in tracked requests.";
1555 return;
1556 }
1557 // be paranoid; note that the reported pData->dwSize is never less than 4 bytes.
1558 if (dataSize < tr->dataSize) {
1559 LOG_CRT << "Invalid data result size! Expected " << tr->dataSize << " but got " << dataSize;
1560 return;
1561 }
1562 unique_lock datalock(tr->m_dataMutex);
1563 memcpy(tr->data.data(), (void*)&data->dwData, tr->dataSize);
1564 tr->lastUpdate = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
1565 datalock.unlock();
1566 shared_lock rdlock(mtxRequests);
1567 LOG_TRC << "Got data result for request: " << *tr;
1568 invokeCallback(dataCb, tr->toRequestRecord());
1569 break;
1570 }
1571 LOG_WRN << "Got unknown RequestID in SIMCONNECT_RECV_CLIENT_DATA struct: " << data->dwRequestID;
1572 return;
1573 } // data->dwRequestID
1574
1575 //serverLastSeen = Clock::now();
1576 break;
1577 } // SIMCONNECT_RECV_ID_CLIENT_DATA
1578
1579 case SIMCONNECT_RECV_ID_EVENT: {
1580 SIMCONNECT_RECV_EVENT *data = static_cast<SIMCONNECT_RECV_EVENT *>(pData);
1581 LOG_TRC << LOG_SC_RECV_EVENT(data);
1582
1583 switch (data->uEventID) {
1584 case CLI_EVENT_PING_RESP:
1585 serverVersion = data->dwData;
1586 serverLastSeen = Clock::now();
1587 LOG_DBG << "Got ping response at " << Utilities::timePointToString(serverLastSeen.load()) << " with version " << STREAM_HEX8(serverVersion);
1588 break;
1589
1590 default:
1591 return;
1592 }
1593 break;
1594 } // SIMCONNECT_RECV_ID_EVENT
1595
1596 case SIMCONNECT_RECV_ID_OPEN:
1597 simConnected = true;
1598 SimConnectHelper::logSimVersion(pData);
1599 break;
1600
1601 case SIMCONNECT_RECV_ID_QUIT: {
1602 LOG_INF << "Received quit command from SimConnect, disconnecting...";
1603 // we're being called from within the message dispatch loop thread; to avoid deadlock we must disconnect asynchronously.
1604 thread([=]() { disconnectSimulator(true); }).detach();
1605 break;
1606 }
1607
1608 case SIMCONNECT_RECV_ID_EXCEPTION:
1609 SimConnectHelper::logSimConnectException(pData);
1610 break;
1611
1612 default:
1613 break;
1614 }
1615 } // onSimConnectMessage()
1616
1617#pragma endregion SimConnect
1618
1619};
1620#pragma endregion WASimClient::Private
1621
1622
1623// -------------------------------------------------------------
1624#pragma region WASimClient class
1625// -------------------------------------------------------------
1626
1627
1628#define d_const const_cast<const Private *>(d.get())
1629
1630WASimClient::WASimClient(uint32_t clientId, const std::string &configFile) :
1631 d(new Private(this, clientId, configFile))
1632{ }
1633
1635 if (isInitialized())
1637 if (d->hDispatchStopEvent)
1638 CloseHandle(d->hDispatchStopEvent);
1639}
1640
1641#pragma region Connections ----------------------------------------------
1642
1643HRESULT WASimClient::connectSimulator(uint32_t timeout)
1644{
1645 if (!d->clientId) {
1646 LOG_ERR << "Client ID must be greater than zero.";
1647 return E_INVALIDARG;
1648 }
1649 return d->connectSimulator(timeout);
1650}
1651
1652HRESULT WASimClient::connectSimulator(int networkConfigId, uint32_t timeout) {
1653 return d->connectSimulator(networkConfigId, timeout);
1654}
1655
1657 d->disconnectSimulator(false);
1658}
1659
1660HRESULT WASimClient::connectServer(uint32_t timeout) {
1661 return d->connectServer(timeout);
1662}
1663
1665 d->disconnectServer(true);
1666}
1667
1668uint32_t WASimClient::pingServer(uint32_t timeout)
1669{
1670 if (!d->checkInit() && FAILED(d->connectSimulator(timeout)))
1671 return 0;
1672
1673 // save the last time we saw the server
1674 const Clock::time_point refTp = d->serverLastSeen;
1675 // Send the previously-registered PING event with this client's ID as the data parameter.
1676 if FAILED(INVOKE_SIMCONNECT(
1677 TransmitClientEvent, d->hSim, SIMCONNECT_OBJECT_ID_USER, (SIMCONNECT_CLIENT_EVENT_ID)Private::CLI_EVENT_PING, (DWORD)d->clientId, SIMCONNECT_GROUP_PRIORITY_HIGHEST, SIMCONNECT_EVENT_FLAG_GROUPID_IS_PRIORITY
1678 ))
1679 return 0;
1680
1681 if (!timeout)
1682 timeout = defaultTimeout();
1683 // The ping response (if any) should come back via the CLI_EVENT_PING_RESP event which we've subscribed to (SIMCONNECT_RECV_ID_EVENT).
1684 if (d->waitCondition([&]() { return !d->runDispatchLoop || d->serverLastSeen.load() > refTp; }, timeout)) {
1685 LOG_INF << "Server responded to ping, reported version: " << STREAM_HEX8(d->serverVersion);
1686 return d->serverVersion;
1687 }
1688 LOG_WRN << "Server did not respond to ping after " << timeout << "ms";
1689 return 0;
1690}
1691
1692#pragma endregion
1693
1694#pragma region Exec calc ----------------------------------------------
1695
1696HRESULT WASimClient::executeCalculatorCode(const std::string &code, CalcResultType resultType, double *fResult, std::string *sResult) const
1697{
1698 if (code.length() >= STRSZ_CMD) {
1699 LOG_ERR << "Code string length " << code.length() << " is greater then maximum size of " << STRSZ_CMD-1;
1700 return E_INVALIDARG;
1701 }
1702
1703 if (resultType == CalcResultType::None)
1704 return d->sendServerCommand(Command(CommandId::Exec, +resultType, code.c_str()));
1705
1706 HRESULT hr;
1707 Command response;
1708 if FAILED(hr = d->sendCommandWithResponse(Command(CommandId::Exec, +resultType, code.c_str()), &response))
1709 return hr;
1710 if (response.commandId != CommandId::Ack) {
1711 LOG_WRN << "Calculator Exec with code " << quoted(code) << " returned Nak response. Reason, if any: " << quoted(response.sData);
1712 return E_FAIL;
1713 }
1714 if (fResult)
1715 *fResult = response.fData;
1716 if (sResult)
1717 *sResult = response.sData;
1718 return hr;
1719}
1720
1721#pragma endregion
1722
1723#pragma region Variable accessors ----------------------------------------------
1724
1725HRESULT WASimClient::getVariable(const VariableRequest & variable, double * pfResult, std::string *psResult)
1726{
1727 if (variable.variableId > -1 && !Utilities::isIndexedVariableType(variable.variableType)) {
1728 LOG_ERR << "Cannot get variable type '" << variable.variableType << "' by index.";
1729 return E_INVALIDARG;
1730 }
1731 return d->getVariable(variable, pfResult, psResult);
1732}
1733
1734HRESULT WASimClient::getLocalVariable(const std::string &variableName, double *pfResult, const std::string &unitName) {
1735 return d->getVariable(VariableRequest(variableName, false, unitName), pfResult);
1736}
1737
1738HRESULT WASimClient::getOrCreateLocalVariable(const std::string &variableName, double *pfResult, double defaultValue, const std::string &unitName) {
1739 return d->getVariable(VariableRequest(variableName, true, unitName), pfResult, nullptr, defaultValue);
1740}
1741
1742
1743HRESULT WASimClient::setVariable(const VariableRequest & variable, const double value) {
1744 return d->setVariable(variable, value);
1745}
1746
1747HRESULT WASimClient::setVariable(const VariableRequest &variable, const std::string &value) {
1748 return d->setStringVariable(variable, value);
1749}
1750
1751HRESULT WASimClient::setLocalVariable(const std::string &variableName, const double value, const std::string &unitName) {
1752 return d->setLocalVariable(VariableRequest(variableName, false, unitName), value);
1753}
1754
1755HRESULT WASimClient::setOrCreateLocalVariable(const std::string &variableName, const double value, const std::string &unitName) {
1756 return d->setLocalVariable(VariableRequest(variableName, true, unitName), value);
1757}
1758
1759#pragma endregion
1760
1761#pragma region Data Requests ----------------------------------------------
1762
1763HRESULT WASimClient::saveDataRequest(const DataRequest &request, bool async) {
1764 return d->addOrUpdateRequest(request, async);
1765}
1766
1767HRESULT WASimClient::removeDataRequest(const uint32_t requestId) {
1768 return d->removeRequest(requestId);
1769}
1770
1771HRESULT WASimClient::updateDataRequest(uint32_t requestId)
1772{
1773 if (d->requests.count(requestId))
1774 return d->sendServerCommand(Command(CommandId::Update, requestId));
1775 return E_FAIL;
1776}
1777
1779{
1780 static const DataRequestRecord nullRecord{ DataRequest((uint32_t)-1, 0, RequestType::None) };
1781
1782 const Private::TrackedRequest *r = d_const->findRequest(requestId);
1783 if (r)
1784 return r->toRequestRecord();
1785 LOG_ERR << "Data Request ID " << requestId << " not found.";
1786 return nullRecord;
1787}
1788
1789vector<DataRequestRecord> WASimClient::dataRequests() const
1790{
1791 vector<DataRequestRecord> ret;
1792 shared_lock lock(d_const->mtxRequests);
1793 ret.reserve(d_const->requests.size());
1794 for (const auto & [_, value] : d_const->requests) {
1795 if (value.requestType != RequestType::None)
1796 ret.emplace_back(value.toRequestRecord());
1797 }
1798 return ret;
1799}
1800
1801vector<uint32_t> WASimClient::dataRequestIdsList() const
1802{
1803 vector<uint32_t> ret;
1804 shared_lock lock(d_const->mtxRequests);
1805 ret.reserve(d_const->requests.size());
1806 for (const auto & [key, value] : d_const->requests) {
1807 if (value.requestType != RequestType::None)
1808 ret.push_back(key);
1809 }
1810 return ret;
1811}
1812
1813HRESULT WASimClient::setDataRequestsPaused(bool paused) const {
1814 if (isConnected()) {
1815 HRESULT hr = d_const->sendServerCommand(Command(CommandId::Subscribe, (paused ? 0 : 1)));
1816 if SUCCEEDED(hr)
1817 d->requestsPaused = paused;
1818 return hr;
1819 }
1820 d->requestsPaused = paused;
1821 return S_OK;
1822}
1823
1824#pragma endregion Data
1825
1826#pragma region Calculator Events ----------------------------------------------
1827
1829{
1830 if (eventData.code.empty())
1831 return removeEvent(eventData.eventId);
1832
1833 const size_t len = eventData.code.length() + (eventData.name.empty() ? 0 : eventData.name.length() + 1);
1834 if (len >= STRSZ_CMD) {
1835 LOG_ERR << "Resulting command string length " << len << " is greater then maximum size of " << STRSZ_CMD-1 << " bytes.";
1836 return E_INVALIDARG;
1837 }
1838
1839 Private::TrackedEvent *ev = d->findTrackedEvent(eventData.eventId);
1840 if (ev) {
1841 if (!eventData.name.empty() && ev->name != eventData.name) {
1842 LOG_ERR << "Cannot change name of event ID " << eventData.eventId << " after it has been registered.";
1843 return E_INVALIDARG;
1844 }
1845 if (ev->code == eventData.code) {
1846 LOG_INF << "No changes detected in code for event ID " << eventData.eventId << ", skipping update";
1847 return S_OK;
1848 }
1849 ev->code = eventData.code;
1850 }
1851 else {
1852 unique_lock lock(d->mtxEvents);
1853 ev = &d->events.emplace(eventData.eventId, Private::TrackedEvent(eventData.eventId, eventData.code, eventData.name)).first->second;
1854 }
1855 if (isConnected())
1856 ev->sentToServer = SUCCEEDED(d->sendEventRegistration(ev)) || ev->sentToServer;
1857 else
1858 LOG_DBG << "Queued event ID " << eventData.eventId << " for next server connection.";
1859
1860 return S_OK;
1861}
1862
1863HRESULT WASimClient::removeEvent(uint32_t eventId)
1864{
1865 Private::TrackedEvent *ev = d->findTrackedEvent(eventId);
1866 if (!ev) {
1867 LOG_ERR << "Event ID " << eventId << " not found.";
1868 return E_INVALIDARG;
1869 }
1870 ev->code.clear();
1871 if (!ev->sentToServer) {
1872 unique_lock lock(d->mtxEvents);
1873 d->events.erase(eventId);
1874 return S_OK;
1875 }
1876 if (isConnected())
1877 d->sendEventRegistration(ev);
1878 else
1879 LOG_DBG << "Queued event ID " << eventId << " for deletion on next server connection.";
1880 return S_OK;
1881}
1882
1883HRESULT WASimClient::transmitEvent(uint32_t eventId) {
1884 return d->sendServerCommand(Command(CommandId::Transmit, eventId));
1885}
1886
1888{
1889 static const RegisteredEvent nullRecord { };
1890 if (Private::TrackedEvent *ev = d->findTrackedEvent(eventId))
1891 return RegisteredEvent(*ev);
1892 LOG_ERR << "Event ID " << eventId << " not found.";
1893 return nullRecord;
1894}
1895
1896vector<RegisteredEvent> WASimClient::registeredEvents() const
1897{
1898 vector<RegisteredEvent> ret;
1899 shared_lock lock(d_const->mtxEvents);
1900 ret.reserve(d_const->events.size());
1901 for (const auto & [_, value] : d_const->events) {
1902 if (!value.code.empty())
1903 ret.emplace_back(RegisteredEvent(value));
1904 }
1905 return ret;
1906}
1907
1908#pragma endregion Calculator Events
1909
1910#pragma region Key Events ----------------------------------------------
1911
1912HRESULT WASimClient::sendKeyEvent(uint32_t keyEventId, uint32_t v1, uint32_t v2, uint32_t v3, uint32_t v4, uint32_t v5) const
1913{
1914 if (keyEventId < CUSTOM_KEY_EVENT_ID_MIN)
1915 return d_const->writeKeyEvent(KeyEvent(keyEventId, { v1, v2, v3, v4, v5 }, d->nextCmdToken++));
1916 else
1917 return d_const->sendSimCustomKeyEvent(keyEventId, v1, v2, v3, v4, v5);
1918}
1919
1920inline static bool isCustomKeyEventName(const std::string &name) {
1921 return name[0] == '#' || name.find('.') != string::npos;
1922}
1923
1924HRESULT WASimClient::sendKeyEvent(const std::string &keyEventName, uint32_t v1, uint32_t v2, uint32_t v3, uint32_t v4, uint32_t v5)
1925{
1926 if (keyEventName.empty())
1927 return E_INVALIDARG;
1928
1929 // check the cache first -- this stores both standard key events and custom (mapped) types.
1930 {
1931 shared_lock rdlock(d->mtxKeyEventNames);
1932 const auto pos = d_const->keyEventNameCache.find(keyEventName);
1933 if (pos != d_const->keyEventNameCache.cend())
1934 return sendKeyEvent(pos->second, v1, v2, v3, v4, v5);
1935 // mutex lock and iterator go out of scope
1936 }
1937
1938 // The name wasn't in the cache...
1939 uint32_t keyId;
1940 if (isCustomKeyEventName(keyEventName)) {
1941 // If the event name looks like a "custom" event then try to register it now.
1942 const HRESULT hr = d->registerCustomKeyEvent(keyEventName, &keyId, false);
1943 if FAILED(hr)
1944 return hr;
1945 }
1946 else {
1947 // Otherwise try a lookup() -- this can only work for standard Key Events, and only if we're currently connected.
1948 int32_t id;
1949 const HRESULT hr = lookup(LookupItemType::KeyEventId, keyEventName, &id);
1950 if FAILED(hr)
1951 return hr == E_FAIL ? E_INVALIDARG : hr; // E_FAIL means event name wasn't found at server
1952 keyId = (uint32_t)id;
1953 // save the lookup result
1954 unique_lock rwlock(d->mtxKeyEventNames);
1955 d->keyEventNameCache.insert(std::pair{ keyEventName, keyId });
1956 }
1957
1958 return sendKeyEvent(keyId, v1, v2, v3, v4, v5);
1959}
1960
1961// Custom-named Key Event registration
1962
1963HRESULT WASimClient::registerCustomKeyEvent(const std::string &customEventName, uint32_t *puiCustomEventId, bool useLegacyTransmit)
1964{
1965 if (isCustomKeyEventName(customEventName))
1966 return d->registerCustomKeyEvent(customEventName, puiCustomEventId, useLegacyTransmit);
1967
1968 LOG_ERR << "Custom event name " << quoted(customEventName) << " must start with '#' or include a period.";
1969 return E_INVALIDARG;
1970}
1971
1972HRESULT WASimClient::removeCustomKeyEvent(uint32_t eventId)
1973{
1974 unique_lock lock(d->mtxKeyEventNames);
1975 const auto pos = find_if(cbegin(d_const->keyEventNameCache), cend(d_const->keyEventNameCache), [eventId](const auto &p) { return p.second == eventId; });
1976 if (pos == d_const->keyEventNameCache.cend())
1977 return E_INVALIDARG;
1978 d->keyEventNameCache.erase(pos);
1979 return S_OK;
1980}
1981
1982HRESULT WASimClient::removeCustomKeyEvent(const std::string &customEventName)
1983{
1984 unique_lock lock(d->mtxKeyEventNames);
1985 return d->keyEventNameCache.erase(customEventName) > 0 ? S_OK : E_INVALIDARG;
1986}
1987
1988#pragma endregion Key Events
1989
1990#pragma region Meta Data ----------------------------------------------
1991
1993{
1994 // validation
1995 static const set<LookupItemType> validListLookups { LookupItemType::LocalVariable, LookupItemType::DataRequest, LookupItemType::RegisteredEvent };
1996 if (!validListLookups.count(itemsType)) {
1997 LOG_ERR << "Cannot list item of type " << Utilities::getEnumName(itemsType, LookupItemTypeNames) << ".";
1998 return E_INVALIDARG;
1999 }
2000 // TODO: Allow multiple simultaneous list requests for different item types
2001 if (d->listResult.listType != LookupItemType::None) {
2002 LOG_ERR << "A List request for " << Utilities::getEnumName(d->listResult.listType.load(), LookupItemTypeNames) << " is currently pending.";
2003 return E_INVALIDARG;
2004 }
2005 unique_lock lock(d->listResult.mutex);
2006 d->listResult.listType = itemsType;
2007 d->listResult.nextTimeout = Clock::now() + chrono::milliseconds(defaultTimeout());
2008 HRESULT hr;
2009 if FAILED(hr = d->sendServerCommand(Command(CommandId::List, +itemsType), weak_ptr(d->listResult.cv), &d->listResult.token))
2010 return hr;
2011 lock.unlock();
2012 try {
2013 thread(&Private::waitListRequestEnd, d.get()).detach();
2014 }
2015 catch (exception *e) {
2016 LOG_ERR << "Exception trying to schedule thread for List command: " << e->what();
2017 return E_FAIL;
2018 }
2019 return S_OK;
2020}
2021
2022HRESULT WASimClient::lookup(LookupItemType itemType, const std::string &itemName, int32_t *piResult)
2023{
2024 // Short-circuit Key Event ID lookups from local source
2025 if (itemType == LookupItemType::KeyEventId) {
2026 const int32_t keyId = Utilities::getKeyEventId(itemName);
2027 if (keyId < 0)
2028 LOG_DBG << "Key Event named " << quoted(itemName) << " was not found in local lookup table.";
2029 else if (!!piResult)
2030 *piResult = keyId;
2031 return keyId < 0 ? E_FAIL : S_OK;
2032 }
2033
2034 if (itemName.length() >= STRSZ_CMD) {
2035 LOG_ERR << "Item name length " << itemName.length() << " is greater then maximum size of " << STRSZ_CMD-1 << " bytes.";
2036 return E_INVALIDARG;
2037 }
2038 HRESULT hr;
2039 Command response;
2040 if FAILED(hr = d->sendCommandWithResponse(Command(CommandId::Lookup, +itemType, itemName.c_str()), &response))
2041 return hr;
2042 if (response.commandId != CommandId::Ack) {
2043 LOG_WRN << "Lookup command returned Nak response. Reason, if any: " << quoted(response.sData);
2044 return E_FAIL;
2045 }
2046 LOG_DBG << "Lookup: Server returned ID " << (int)response.fData << " and echo name " << quoted(response.sData);
2047 if (piResult)
2048 *piResult = (int)response.fData;
2049 return hr;
2050}
2051
2052#pragma endregion Meta
2053
2054#pragma region Low Level ----------------------------------------------
2055
2056HRESULT WASimClient::sendCommand(const Command &command) const {
2057 return d->sendServerCommand(command);
2058}
2059
2060HRESULT WASimClient::sendCommandWithResponse(const Command &command, Command *response, uint32_t timeout) {
2061 return d->sendCommandWithResponse(Command(command), response, timeout);
2062}
2063
2064#pragma endregion Low Level
2065
2066#pragma region Status / Network / Logging / Callbacks ----------------------------------------------
2067
2068ClientStatus WASimClient::status() const { return d_const->status; }
2069bool WASimClient::isInitialized() const { return d_const->checkInit(); }
2070bool WASimClient::isConnected() const { return d_const->isConnected(); }
2071uint32_t WASimClient::clientVersion() const { return WSMCMND_VERSION; }
2072uint32_t WASimClient::serverVersion() const { return d_const->serverVersion; }
2073
2074uint32_t WASimClient::defaultTimeout() const { return d_const->settings.networkTimeout; }
2075void WASimClient::setDefaultTimeout(uint32_t ms) { d->settings.networkTimeout = ms; }
2076
2077int WASimClient::networkConfigurationId() const { return d_const->settings.networkConfigId; }
2078void WASimClient::setNetworkConfigurationId(int configId) { d->settings.networkConfigId = configId; }
2079
2080LogLevel WASimClient::logLevel(LogFacility facility, LogSource source) const { return d_const->settings.logLevel(source, facility); }
2082{
2083 switch (source) {
2084 case LogSource::Client:
2085 if ((+facility & +LogFacility::Console) && d_const->settings.logLevel(source, LogFacility::Console) != level)
2086 d->setConsoleLogLevel(level);
2087 if ((+facility & +LogFacility::File) && d_const->settings.logLevel(source, LogFacility::File) != level)
2088 d->setFileLogLevel(level);
2089 if ((+facility & +LogFacility::Remote) && d_const->settings.logLevel(source, LogFacility::Remote) != level)
2090 d->setCallbackLogLevel(level);
2091 break;
2092 case LogSource::Server:
2093 d->setServerLogLevel(level, facility);
2094 }
2095}
2096
2100void WASimClient::setLogCallback(logCallback_t cb) { d->setLogCallback(cb); }
2103
2104#pragma endregion Misc
2105
2106#pragma endregion WASimClient
2107
2108#undef d_const
2109#undef WAIT_CONDITION
2110
2111}; // namespace WASimCommander::Client
bool isInitialized() const
Check if simulator network link is established.
HRESULT registerEvent(const RegisteredEvent &eventData)
Register a reusable event which executes a pre-set RPN calculator code string. The code is pre-compil...
HRESULT sendCommandWithResponse(const Command &command, Command *response, uint32_t timeout=0)
Sends a command, in the form of a WASimCommander::Command structure, to the server for processing and...
RegisteredEvent registeredEvent(uint32_t eventId)
Returns a copy of a RegisteredEvent which has been previously added with registerEvent()....
bool isConnected() const
Check WASimCommander server connection status.
void setDataCallback(dataCallback_t cb)
Sets a callback for value update data arriving from the server. Pass a nullptr value to remove a prev...
uint32_t defaultTimeout() const
Get the current default server response timeout value, which is used in all network requests....
HRESULT getLocalVariable(const std::string &variableName, double *pfResult, const std::string &unitName=std::string())
A convenience version of getVariable(VariableRequest(variableName, false, unitName),...
HRESULT executeCalculatorCode(const std::string &code, WASimCommander::Enums::CalcResultType resultType=WASimCommander::Enums::CalcResultType::None, double *pfResult=nullptr, std::string *psResult=nullptr) const
Run a string of MSFS Gauge API calculator code in RPN format, possibly with some kind of result expec...
~WASimClient()
Any open network connections are automatically closed upon destruction, though it is better to close ...
HRESULT getOrCreateLocalVariable(const std::string &variableName, double *pfResult, double defaultValue=0.0, const std::string &unitName=std::string())
Gets the value of a local variable just like getLocalVariable() but will also create the variable on ...
uint32_t clientVersion() const
Return the current WASimClient version number. Version numbers are in "BCD" format: MAJOR << 24 | MIN...
HRESULT getVariable(const VariableRequest &variable, double *pfResult, std::string *psResult=nullptr)
Get a Variable value by name, with optional named unit type. This is primarily useful for local ('L')...
ClientStatus status() const
Get current connection status of this client.
HRESULT updateDataRequest(uint32_t requestId)
Trigger a data update on a previously-added DataRequest. Designed to refresh data on subscriptions wi...
void setLogCallback(logCallback_t cb)
Sets a callback for logging activity, both from the server and the client itself. Pass a nullptr valu...
void setLogLevel(WASimCommander::Enums::LogLevel level, WASimCommander::Enums::LogFacility facility=WASimCommander::Enums::LogFacility::Remote, LogSource source=LogSource::Client)
Set the current minimum logging severity level for the specified facility and source to level.
HRESULT lookup(WASimCommander::Enums::LookupItemType itemType, const std::string &itemName, int32_t *piResult)
Request a lookup of a named item to find its corresponding numeric ID. Most lookup types are done on...
HRESULT connectSimulator(uint32_t timeout=0)
Initialize the simulator network link and set up minimum necessary for WASimCommander server ping or ...
HRESULT removeDataRequest(const uint32_t requestId)
Remove a previously-added DataRequest. This clears the subscription and any tracking/meta data from b...
std::vector< DataRequestRecord > dataRequests() const
Returns a list of all data requests which have been added to the Client so far. (These are returned b...
HRESULT sendCommand(const Command &command) const
Sends a command, in the form of a WASimCommander::Command structure, to the server for processing....
HRESULT connectServer(uint32_t timeout=0)
Connect to WASimCommander server. This will implicitly call connectSimulator() first if it hasn't alr...
HRESULT registerCustomKeyEvent(const std::string &customEventName, uint32_t *puiCustomEventId=nullptr, bool useLegacyTransmit=false)
Register a "Custom Simulator [Key] Event" by providing an event name. The method optionally returns t...
HRESULT removeEvent(uint32_t eventId)
Remove an event previously registered with registerEvent() method. This is effectively the same as ca...
void setNetworkConfigurationId(int configId)
SimConnect is used for the network layer. This setting specifies the SimConnect.cfg index to use,...
HRESULT transmitEvent(uint32_t eventId)
Trigger an event previously registered with registerEvent(). This is a more direct alternative to tri...
DataRequestRecord dataRequest(uint32_t requestId) const
Returns a copy of a DataRequestRecord which has been previously added. If the request with the given ...
WASimClient(uint32_t clientId, const std::string &configFile=std::string())
Instantiate the client with a unique ID and optional configuration file path.
HRESULT setOrCreateLocalVariable(const std::string &variableName, const double value, const std::string &unitName=std::string())
Set a Local Variable value by variable name, creating it first if it does not already exist....
HRESULT removeCustomKeyEvent(const std::string &customEventName)
Remove a Custom Event previously registered with registerCustomEvent() method using the event's name....
HRESULT setVariable(const VariableRequest &variable, const double value)
Set a Variable value by name, with optional named unit type. Although any settable variable type can ...
void setCommandResultCallback(commandCallback_t cb)
Sets a callback for delivering command results returned by the server. Pass a nullptr value to remove...
std::vector< uint32_t > dataRequestIdsList() const
Returns a list of all DataRequest::requestIds which have been added to the Client so far.
void disconnectSimulator()
Shut down all network connections (and disconnect WASimCommander server if connected)....
HRESULT setDataRequestsPaused(bool paused) const
Enables or disables all data request subscription updates at the same time. Use this to temporarily s...
void setListResultsCallback(listResultsCallback_t cb)
Sets a callback for list results arriving from the server. Pass a nullptr value to remove a previousl...
uint32_t pingServer(uint32_t timeout=0)
Check if WASimCommander Server exists (Simulator running, the WASIM module is installed and working)....
uint32_t serverVersion() const
Return the version number of the last-connected, or successfully pinged, WASimModule (sever),...
void setResponseCallback(commandCallback_t cb)
Sets a callback for delivering response commands sent to this client by the server module....
HRESULT sendKeyEvent(uint32_t keyEventId, uint32_t v1=0, uint32_t v2=0, uint32_t v3=0, uint32_t v4=0, uint32_t v5=0) const
Can be used to trigger standard Simulator "Key Events" as well as "custom" Gauge API/SimConnect event...
HRESULT list(WASimCommander::Enums::LookupItemType itemsType=WASimCommander::Enums::LookupItemType::LocalVariable)
Send a request for a list update to the server. The results are delivered using the callback set in s...
WASimCommander::Enums::LogLevel logLevel(WASimCommander::Enums::LogFacility facility, LogSource source=LogSource::Client) const
Get the current minimum logging severity level for the specified facility and source.
int networkConfigurationId() const
SimConnect is used for the network layer. This setting specifies the SimConnect.cfg index to use....
void disconnectServer()
Disconnect from the WASimCommander server. This does not close the Simulator network connection (use ...
void setClientEventCallback(clientEventCallback_t cb)
Sets a callback for Client event updates which indicate status changes. Pass a nullptr value to remov...
HRESULT setLocalVariable(const std::string &variableName, const double value, const std::string &unitName=std::string())
A convenience version of setVariable() for Local variable types. Equivalent to setVariable(VariableRe...
HRESULT saveDataRequest(const DataRequest &request, bool async=false)
Add a new WASimCommander::DataRequest for a variable or calculated result, or update an existing data...
std::vector< RegisteredEvent > registeredEvents() const
Returns a list of all registered events which have been added to the Client with registerEvent()....
void setDefaultTimeout(uint32_t ms)
Get current connection status of this client.
WASimCommander::Client namespace. Defines/declares everything needed to interact with the WASimComman...
Definition enums_impl.h:32
static const HRESULT E_TIMEOUT
Error result: timeout communicating with server.
Definition WASimClient.h:59
std::function< void __stdcall(const ListResult &)> listResultsCallback_t
Callback function for delivering list results, eg. of local variables sent from Server.
Definition WASimClient.h:73
LogSource
Log entry source, Client or Server.
Definition enums_impl.h:74
@ Client
Log record from WASimClient.
Definition enums_impl.h:75
@ Server
Log record from WASimModule (Server)
Definition enums_impl.h:76
std::function< void __stdcall(const ClientEvent &)> clientEventCallback_t
Callback function for Client events.
Definition WASimClient.h:72
std::function< void __stdcall(const DataRequestRecord &)> dataCallback_t
Callback function for subscription result data.
Definition WASimClient.h:74
std::function< void __stdcall(const Command &)> commandCallback_t
Callback function for commands sent from server.
Definition WASimClient.h:76
static const HRESULT E_NOT_CONNECTED
Error result: server not connected.
Definition WASimClient.h:56
static const uint32_t CUSTOM_KEY_EVENT_ID_MIN
Starting ID range for "Custom Key Events" for use with registerCustomKeyEvent() generated IDs....
Definition WASimClient.h:65
std::function< void __stdcall(const LogRecord &, LogSource)> logCallback_t
Callback function for log entries (from both Client and Server).
Definition WASimClient.h:75
ClientEventType
Client event type enumeration.
Definition enums_impl.h:55
@ SimDisconnecting
ShuttingDown status.
Definition enums_impl.h:59
@ ServerDisconnected
From Connected to SimConnected status change.
Definition enums_impl.h:63
@ SimConnecting
Initializing status.
Definition enums_impl.h:57
@ SimDisconnected
From SimConnected to Initializing status change.
Definition enums_impl.h:60
std::vector< std::pair< int, std::string > > listResult_t
A mapping of IDs to names.
Definition structs.h:43
ClientStatus
Client status flags.
Definition enums_impl.h:36
@ ShuttingDown
closing SimConnect link, before being back in Idle status mode
Definition enums_impl.h:42
@ Connected
connected to WASim server
Definition enums_impl.h:41
@ Initializing
trying to connect to SimConnect
Definition enums_impl.h:38
@ SimConnected
have SimConnect link
Definition enums_impl.h:39
@ Connecting
attempting connection to WASim server
Definition enums_impl.h:40
@ Idle
no active SimConnect or WASim server connection
Definition enums_impl.h:37
static const std::vector< const char * > LogLevelNames
Enums::LogLevel enum names.
Definition enums_impl.h:166
LogLevel
Logging levels.
Definition enums_impl.h:155
static const std::vector< const char * > CommandIdNames
Enums::CommandId enum names.
Definition enums_impl.h:89
LookupItemType
Types of things to look up or list.
Definition enums_impl.h:136
@ DataRequest
Saved value subscription for current Client, indexed by requestId and nameOrCode values....
Definition enums_impl.h:143
@ None
Null type, possible internal use, ignored by server.
Definition enums_impl.h:137
@ LocalVariable
LVar ('L') names and IDs. Available for List and Lookup commands.
Definition enums_impl.h:138
@ RegisteredEvent
Saved calculator event for current Client, indexed by eventId and code values. Available for List and...
Definition enums_impl.h:144
@ KeyEventId
Key Event ID, value of KEY_* macros from "guauges.h" header. Available for Lookup command only (use e...
Definition enums_impl.h:142
static const std::vector< const char * > LookupItemTypeNames
Enums::LookupItemType enum names.
Definition enums_impl.h:148
CalcResultType
The type of result that calculator code is expected to produce.
Definition enums_impl.h:109
@ None
No result is expected (eg. triggering an event).
Definition enums_impl.h:110
CommandId
Commands for client-server interaction. Both sides can send commands via dedicated channels by writin...
Definition enums_impl.h:39
@ Update
Trigger data update of a previously-added Data Request, with the request ID in uData....
Definition enums_impl.h:77
@ List
Request a listing of items like local variables. uData should be one of WASimCommander::LookupItemTyp...
Definition enums_impl.h:46
@ Transmit
Trigger an event previously registered with the Register command. uData should be the event ID from t...
Definition enums_impl.h:74
@ Exec
Run calculator code contained in sData with WASimCommander::CalcResultType in uData....
Definition enums_impl.h:65
@ Ack
Last command acknowledge. CommandId of the original command (which succeeded) is sent in uData....
Definition enums_impl.h:41
@ Subscribe
Sending this command to the server with a uData value of 0 (zero) will suspend (pause) any and all da...
Definition enums_impl.h:75
@ Lookup
Get information about an item, such as the ID of a variable or unit name. uData should be one of WASi...
Definition enums_impl.h:48
RequestType
Types of things to request or set.
Definition enums_impl.h:97
@ None
Use to remove a previously-added request.
Definition enums_impl.h:98
LogFacility
Logging destination type.
Definition enums_impl.h:171
@ File
Log file destination.
Definition enums_impl.h:174
@ Console
Console logging, eg. stderr/stdout.
Definition enums_impl.h:173
@ Remote
Remote destination, eg. network transmission or a callback event.
Definition enums_impl.h:175
static const size_t STRSZ_CMD
Maximum size of Command::sData member. Size optimizes alignment of Command struct.
DataRequestRecord inherits and extends WASimCommander::DataRequest with data pertinent for use by a d...
Definition structs.h:70
DataRequestRecord()
< Inherits DataRequest assignment operators.
DataRequest(uint32_t requestId, uint32_t valueSize=sizeof(double), WASimCommander::Enums::RequestType requestType=WASimCommander::Enums::RequestType::Calculated, WASimCommander::Enums::CalcResultType calcResultType=WASimCommander::Enums::CalcResultType::Double, WASimCommander::Enums::UpdatePeriod period=WASimCommander::Enums::UpdatePeriod::Tick, const char *nameOrCode=nullptr, const char *unitName=nullptr, char varTypePrefix='L', float deltaEpsilon=0.0f, uint8_t interval=0, uint8_t simVarIndex=0)
Constructor with required request ID, all other parameters optional. See member documentation for exp...
std::vector< uint8_t > data
Value data array.
Definition structs.h:72
Structure to hold data for registered (reusable) calculator events. Used to submit events with WASimC...
Definition structs.h:150
std::string name
Optional custom name for this event. The name is for use with SimConnect_MapClientEventToSimEvent(id,...
Definition structs.h:154
std::string code
The calculator code string to execute as the event action. The code is pre-compiled and stored on the...
Definition structs.h:152
RegisteredEvent(uint32_t eventId=-1, const std::string &code="", const std::string &name="")
Default implicit constructor.
uint32_t eventId
A unique ID for this event. The ID can later be used to modify, trigger, or remove this event.
Definition structs.h:151
Structure for using with WASimClient::getVariable() and WASimClient::setVariable() to specify informa...
Definition structs.h:110
int variableId
Numeric ID of the variable to get/set. Overrides the variableName field if greater than -1....
Definition structs.h:117
char variableType
A single character variable type identifier as documented in the MSFS SDK documentation (plus 'T' for...
Definition structs.h:111
Command data structure. The member contents depend on the command type as described in each command t...
char sData[STRSZ_CMD]
String command parameter value, meaning depends on the command being issued.
WASimCommander::Enums::CommandId commandId
What to do.
double fData
double-precision floating point command parameter value, meaning depends on the command being issued.
uint32_t token
A unique ID for this command instance. Echoed back by server in command responses....
uint32_t uData
DWORD command parameter value, meaning depends on the command being issued.
Structure for variable value subscription requests.
uint32_t requestId
Unique ID for the request, subsequent use of this ID overwrites any previous request definition (but ...
WASimCommander::Enums::RequestType requestType
Named variable or calculated value.
uint32_t valueSize
Byte size of stored value; can also be one of the predefined DATA_TYPE_* constants.
float deltaEpsilon
Minimum change in numeric value required to trigger an update. The default of 0.0 is to send updates ...
char nameOrCode[STRSZ_REQ]
Variable name or full calculator string.
DataRequest(uint32_t requestId, uint32_t valueSize=sizeof(double), WASimCommander::Enums::RequestType requestType=WASimCommander::Enums::RequestType::Calculated, WASimCommander::Enums::CalcResultType calcResultType=WASimCommander::Enums::CalcResultType::Double, WASimCommander::Enums::UpdatePeriod period=WASimCommander::Enums::UpdatePeriod::Tick, const char *nameOrCode=nullptr, const char *unitName=nullptr, char varTypePrefix='L', float deltaEpsilon=0.0f, uint8_t interval=0, uint8_t simVarIndex=0)
Constructor with required request ID, all other parameters optional. See member documentation for exp...
Data structure for sending Key Events to the sim with up to 5 event values. Events are specified usin...