26#include <condition_variable>
37#include <shared_mutex>
42#define WIN32_LEAN_AND_MEAN
44#include <SimConnect.h>
46#define LOGFAULT_THREAD_NAME WSMCMND_CLIENT_NAME
51#include "SimConnectHelper.h"
58using namespace WASimCommander::Utilities;
61using Clock = std::chrono::steady_clock;
63static const uint32_t CUSTOM_KEY_EVENT_LEGACY_TRIGGER_FLAG = 0x80000000;
66#pragma region DataRequestRecord struct
75 data(Utilities::getActualValueSize(valueSize), 0xFF) { }
79 data(Utilities::getActualValueSize(valueSize), 0xFF) { }
84#pragma region RegisteredEvent struct
88 eventId{eventId}, code{code}, name{name} { }
94#pragma region WASimClient::Private class
97class WASimClient::Private
101 static const uint32_t DISPATCH_LOOP_WAIT_TIME = 5000;
102 static const uint32_t SIMCONNECT_DATA_ALLOC_LIMIT = 1024UL * 1024UL;
104 enum SimConnectIDs : uint8_t
106 SIMCONNECTID_INIT = 0,
130 time_t lastUpdate = 0;
132 mutable shared_mutex m_dataMutex;
133 vector<uint8_t> data;
135 explicit TrackedRequest(
const DataRequest &req, uint32_t dataId) :
138 dataSize{Utilities::getActualValueSize(valueSize)},
142 TrackedRequest & operator=(
const DataRequest &req) {
144 unique_lock lock(m_dataMutex);
145 dataSize = Utilities::getActualValueSize(req.
valueSize);
146 data = vector<uint8_t>(dataSize, 0xFF);
148 DataRequest::operator=(req);
152 DataRequestRecord toRequestRecord()
const {
153 DataRequestRecord drr = DataRequestRecord(
static_cast<const DataRequest &
>(*
this));
154 drr.data.assign(data.cbegin(), data.cend());
155 drr.lastUpdate = lastUpdate;
159 friend inline std::ostream& operator<<(std::ostream& os,
const TrackedRequest &r) {
161 return os <<
" TrackedRequest{" <<
" dataId: " << r.dataId <<
"; lastUpdate: " << r.lastUpdate <<
"; dataSize: " << r.dataSize <<
"; data: " << Utilities::byteArrayToHex(r.data.data(), r.dataSize) <<
'}';
166 struct TrackedResponse
169 chrono::system_clock::time_point sent { chrono::system_clock::now() };
170 weak_ptr<condition_variable_any> cv {};
174 explicit TrackedResponse(uint32_t t, weak_ptr<condition_variable_any> cv) : token(t), cv(cv) {}
179 bool sentToServer =
false;
183 using responseMap_t = map<uint32_t, TrackedResponse>;
184 using requestMap_t = map<uint32_t, TrackedRequest>;
185 using eventMap_t = map<uint32_t, TrackedEvent>;
187 struct TempListResult {
188 atomic<LookupItemType> listType { LookupItemType::None };
189 uint32_t token { 0 };
190 shared_ptr<condition_variable_any> cv = make_shared<condition_variable_any>();
191 atomic<Clock::time_point> nextTimeout {};
197 shared_lock lock(mutex);
202 unique_lock lock(mutex);
203 listType = LookupItemType::None;
209 struct ProgramSettings {
210 filesystem::path logFilePath;
211 int networkConfigId = -1;
212 uint32_t networkTimeout = 1000;
215 { LogLevel::Info, LogLevel::Info, LogLevel::None },
216 { LogLevel::None, LogLevel::None, LogLevel::None }
221 static int facIndex(
LogFacility f) {
return f == LogFacility::Remote ? 2 : f == LogFacility::File ? 1 : 0; }
227 const uint32_t clientId;
228 const string clientName;
231 atomic<Clock::time_point> serverLastSeen = Clock::time_point();
232 atomic_size_t totalDataAlloc = 0;
233 atomic_uint32_t nextDefId = SIMCONNECTID_LAST;
234 atomic_uint32_t nextCmdToken = 1;
235 atomic_bool runDispatchLoop =
false;
236 atomic_bool simConnected =
false;
237 atomic_bool serverConnected =
false;
238 atomic_bool logCDAcreated =
false;
239 atomic_bool requestsPaused =
false;
241 HANDLE hSim =
nullptr;
242 HANDLE hSimEvent =
nullptr;
243 HANDLE hDispatchStopEvent =
nullptr;
244 thread dispatchThread;
253 mutable shared_mutex mtxResponses;
254 mutable shared_mutex mtxRequests;
255 mutable shared_mutex mtxEvents;
257 responseMap_t reponses {};
258 requestMap_t requests {};
259 eventMap_t events {};
263 unordered_map<std::string, uint32_t> keyEventNameCache {};
264 shared_mutex mtxKeyEventNames;
269 map<std::string, uint32_t> mappedVarsNameCache {};
270 shared_mutex mtxMappedVarNames;
271 uint32_t nextMappedVarID = 1;
274#pragma region Callback handling templates ----------------------------------------------
276#if defined(WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS) && !WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS
277 mutable mutex mtxCallbacks;
281 template<
typename... Args>
282 void invokeCallbackImpl(function<
void(Args...)> f, Args&&... args)
const
286#if defined(WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS) && !WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS
287 lock_guard lock(mtxCallbacks);
289 bind(f, forward<Args>(args)...)();
291 catch (exception *e) { LOG_ERR <<
"Exception in callback handler: " << e->what(); }
296 template<
typename... Args>
297 void invokeCallbackImpl(function<
void(
const Args...)> f,
const Args... args)
const
301#if defined(WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS) && !WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS
302 lock_guard lock(mtxCallbacks);
306 catch (exception *e) { LOG_ERR <<
"Exception in callback handler: " << e->what(); }
311 template<
typename F,
typename... Args>
312 void invokeCallback(F&& f, Args&&... args)
const {
313 invokeCallbackImpl<remove_cv_t<remove_reference_t<Args>>...>(forward<F>(f), forward<Args>(args)...);
317#pragma region Constructor and status ----------------------------------------------
319 Private(
WASimClient *qptr, uint32_t clientId,
const std::string &config) :
322 clientName{(ostringstream() << STREAM_HEX8(clientId)).str()}
325 filesystem::path cwd = filesystem::current_path(ec);
327 cerr << ec.message() << endl;
330 settings.logFilePath = cwd;
332 int requestTrackingMaxRecords = SimConnectHelper::ENABLE_SIMCONNECT_REQUEST_TRACKING ? 200 : 0;
335 const char *fn =
"client_conf.ini";
336 filesystem::path cfgFile(cwd /+ fn);
337 if (!config.empty()) {
339 filesystem::file_status fs = filesystem::status(config, ec);
340 if (!ec && filesystem::exists(fs)) {
342 if (filesystem::is_directory(fs))
346 cerr <<
"Config file/path '" << config <<
"' not found or not accessible. Using default config location " << cwd << endl;;
350 ifstream is(cfgFile);
352 inipp::Ini<char> ini;
354 for (
const auto &e : ini.errors)
356 string consoleLevel(getEnumName(settings.logLevel(
LogSource::Client, LogFacility::Console), LogLevelNames)),
357 fileLevel(getEnumName(settings.logLevel(
LogSource::Client, LogFacility::File), LogLevelNames));
358 const auto &logSect = ini.sections[
"logging"];
359 inipp::get_value(logSect,
"logFilepath", settings.logFilePath);
360 inipp::get_value(logSect,
"fileLogLevel", fileLevel);
361 inipp::get_value(logSect,
"consoleLogLevel", consoleLevel);
362 const auto &netSect = ini.sections[
"network"];
363 inipp::get_value(netSect,
"networkConfigId", settings.networkConfigId);
364 inipp::get_value(netSect,
"networkTimeout", settings.networkTimeout);
365 inipp::get_value(netSect,
"requestTrackingMaxRecords", requestTrackingMaxRecords);
367 if ((idx = indexOfString(LogLevelNames, fileLevel.c_str())) > -1)
369 if ((idx = indexOfString(LogLevelNames, consoleLevel.c_str())) > -1)
374 cerr <<
"Config file '" << cfgFile <<
"' not found or not accessible. Setting all logging to " << getEnumName(settings.logLevel(
LogSource::Client, LogFacility::Console), LogLevelNames)
375 <<
" levels, with file in " << quoted(settings.logFilePath.string()) << endl;
379 settings.logFilePath /= WSMCMND_CLIENT_NAME;
383 if ((SimConnectHelper::ENABLE_SIMCONNECT_REQUEST_TRACKING = (requestTrackingMaxRecords > 0)))
384 SimConnectHelper::setMaxTrackedRequests(requestTrackingMaxRecords);
387 bool checkInit()
const {
return hSim !=
nullptr; }
392 static const char * evTypeNames[] = {
394 "Simulator Connecting",
"Simulator Connected",
"Simulator Disconnecting",
"Simulator Disconnected",
395 "Server Connecting",
"Server Connected",
"Server Disconnected"
415 newStat = (newStat & ~ClientStatus::Connected) | s;
419 newStat = (newStat & ~ClientStatus::Connecting) | s;
432 ev.message = Utilities::getEnumName(ev.eventType, evTypeNames);
435 invokeCallback(eventCb, move(ev));
440 bool waitCondition(
const std::function<
bool(
void)> predicate, uint32_t timeout, uint32_t sleepMs = 1)
442 const auto endTime = Clock::now() + chrono::milliseconds(timeout);
443 const auto sf = chrono::milliseconds(sleepMs);
444 while (!predicate() && endTime > Clock::now())
445 this_thread::sleep_for(sf);
450 HRESULT waitConditionAsync(std::function<
bool(
void)> predicate, uint32_t timeout = 0)
455 timeout = settings.networkTimeout;
456 future<bool> taskResult = async(launch::async, &Private::waitCondition,
this, predicate, timeout, 1);
457 future_status taskStat = taskResult.wait_until(Clock::now() + chrono::milliseconds(timeout + 50));
458 return (taskStat == future_status::ready ? S_OK :
E_TIMEOUT);
463#pragma region Local logging ----------------------------------------------
465 void setFileLogLevel(
LogLevel level)
468 const string loggerName = string(WSMCMND_CLIENT_NAME) +
'_' + clientName;
469 if (logfault::LogManager::Instance().HaveHandler(loggerName))
470 logfault::LogManager::Instance().SetLevel(loggerName, logfault::LogLevel(level));
471 else if (level != LogLevel::None)
472 logfault::LogManager::Instance().AddHandler(
473 make_unique<logfault::FileHandler>(settings.logFilePath.string().c_str(), logfault::LogLevel(level), logfault::FileHandler::Handling::Rotate, loggerName)
477 void setConsoleLogLevel(
LogLevel level)
480 if (logfault::LogManager::Instance().HaveHandler(WSMCMND_CLIENT_NAME))
481 logfault::LogManager::Instance().SetLevel(WSMCMND_CLIENT_NAME, logfault::LogLevel(level));
482 else if (level != LogLevel::None)
483 logfault::LogManager::Instance().AddHandler(
484 make_unique<logfault::StreamHandler>(cout, logfault::LogLevel(level), WSMCMND_CLIENT_NAME)
488 void setCallbackLogLevel(
LogLevel level)
491 if (logfault::LogManager::Instance().HaveHandler(clientName))
492 logfault::LogManager::Instance().SetLevel(clientName, logfault::LogLevel(level));
496 void onProxyLoggerMessage(
const logfault::Message &msg) {
503 if (cb && !logfault::LogManager::Instance().HaveHandler(clientName)) {
504 logfault::LogManager::Instance().SetHandler(clientName,
505 make_unique<logfault::ProxyHandler>(
506 bind(&Private::onProxyLoggerMessage,
this, placeholders::_1),
512 else if (!cb && logfault::LogManager::Instance().HaveHandler(clientName)) {
513 logfault::LogManager::Instance().RemoveHandler(clientName);
518#pragma region SimConnect and Server core utilities ----------------------------------------------
530 LOG_INF << WSMCMND_CLIENT_NAME
" already initialized.";
533 settings.networkConfigId = networkConfigId;
534 LOG_INF <<
"Initializing " << WSMCMND_CLIENT_NAME <<
" v" WSMCMND_VERSION_INFO
" for client " << clientName <<
" with net config ID " << settings.networkConfigId;
538 timeout = settings.networkTimeout;
540 if (!hDispatchStopEvent) {
541 hDispatchStopEvent = CreateEvent(
nullptr,
true,
false,
nullptr);
542 if (!hDispatchStopEvent) {
543 LOG_ERR <<
"Failed to CreateEvent() for dispatch loop: " << GetLastError();
548 hSimEvent = CreateEvent(
nullptr,
false,
false,
nullptr);
549 simConnected =
false;
552 HRESULT hr = SimConnect_Open(&hSim, clientName.c_str(),
nullptr, 0, hSimEvent, settings.networkConfigId);
555 LOG_ERR <<
"Network connection to Simulator failed with result: " << LOG_HR(hr);
561 ResetEvent(hDispatchStopEvent);
562 dispatchThread = thread(&Private::dispatchLoop,
this);
563 dispatchThread.detach();
565 if (!waitCondition([&]() {
return !!runDispatchLoop; }, 10)) {
566 LOG_CRT <<
"Could not start dispatch loop thread.";
571 if (!waitCondition([&]() {
return !runDispatchLoop || simConnected; }, timeout)) {
572 LOG_ERR <<
"Network connection to Simulator timed out after " << timeout <<
"ms.";
578 if FAILED(hr = SimConnectHelper::newClientEvent(hSim, CLI_EVENT_CONNECT, EVENT_NAME_CONNECT)) {
583 SimConnectHelper::newClientEvent(hSim, CLI_EVENT_PING, EVENT_NAME_PING);
585 SimConnectHelper::newClientEvent(hSim, CLI_EVENT_PING_RESP, EVENT_NAME_PING_PFX + clientName, NOTIFY_GROUP_PING, SIMCONNECT_GROUP_PRIORITY_HIGHEST);
588 if FAILED(hr = registerDataArea(CDA_NAME_CMD_PFX, CLI_DATA_COMMAND, CLI_DATA_COMMAND,
sizeof(
Command),
true)) {
593 if FAILED(hr = registerDataArea(CDA_NAME_RESP_PFX, CLI_DATA_RESPONSE, CLI_DATA_RESPONSE,
sizeof(
Command),
false)) {
598 registerDataArea(CDA_NAME_DATA_PFX, CLI_DATA_REQUEST, CLI_DATA_REQUEST,
sizeof(
DataRequest),
true);
600 registerDataArea(CDA_NAME_KEYEV_PFX, CLI_DATA_KEYEVENT, CLI_DATA_KEYEVENT,
sizeof(
KeyEvent),
true);
603 if FAILED(hr = INVOKE_SIMCONNECT(
604 RequestClientData, hSim, (SIMCONNECT_CLIENT_DATA_ID)CLI_DATA_RESPONSE, (SIMCONNECT_DATA_REQUEST_ID)DATA_REQ_RESPONSE,
605 (SIMCONNECT_CLIENT_DATA_DEFINITION_ID)CLI_DATA_RESPONSE, SIMCONNECT_CLIENT_DATA_PERIOD_ON_SET, (SIMCONNECT_CLIENT_DATA_REQUEST_FLAG)0, 0UL, 0UL, 0UL
612 registerAllDataRequestAreas();
614 mapAllCustomKeyEvents();
624 LOG_INF <<
"No network connection. Shutdown complete.";
629 LOG_DBG <<
"Shutting down...";
632 SetEvent(hDispatchStopEvent);
636 if (!waitCondition([&]() {
return !runDispatchLoop; }, DISPATCH_LOOP_WAIT_TIME + 100))
637 LOG_CRT <<
"Dispatch loop thread still running!";
640 simConnected =
false;
641 logCDAcreated =
false;
645 if (hSim && !onQuitEvent)
646 SimConnect_Close(hSim);
649 CloseHandle(hSimEvent);
654 unique_lock lock { mtxMappedVarNames };
655 mappedVarsNameCache.clear();
660 LOG_INF <<
"Shutdown complete, network disconnected.";
669 timeout = settings.networkTimeout;
675 LOG_INF <<
"Connecting to " WSMCMND_PROJECT_NAME
" server...";
679 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)) {
686 if (!waitCondition([&]() {
return !runDispatchLoop || serverConnected; }, timeout)) {
687 LOG_ERR <<
"Server connection timed out or refused after " << timeout <<
"ms.";
692 LOG_INF <<
"Connected to " WSMCMND_PROJECT_NAME
" server v" << STREAM_HEX8(
serverVersion);
693 if ((
serverVersion & 0xFF000000) != (WSMCMND_VERSION & 0xFF000000))
694 LOG_WRN <<
"Server major version does not match WASimClient version " << STREAM_HEX8(WSMCMND_VERSION);
700 updateServerLogLevel();
702 sendServerCommand(
Command(CommandId::Subscribe, (requestsPaused ? 0 : 1)));
704 registerAllDataRequests();
718 sendServerCommand(
Command(CommandId::Disconnect));
721 unique_lock lock(mtxResponses);
724 serverConnected =
false;
725 LOG_INF <<
"Disconnected from " WSMCMND_PROJECT_NAME
" server.";
729 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)
735 const string cdaName(nameIsFull ? name : name + clientName);
736 if FAILED(hr = SimConnectHelper::registerDataArea(hSim, cdaName, cdaID, cddId, szOrType,
true, readonly, deltaE))
738 const uint32_t alloc = Utilities::getActualValueSize(szOrType);;
739 totalDataAlloc += alloc;
740 LOG_DBG <<
"Created CDA ID " << cdaID <<
" named " << quoted(cdaName) <<
" of size " << alloc <<
"; Total data allocation : " << totalDataAlloc;
745#pragma region Command sending ----------------------------------------------
748 HRESULT sendServerCommand(
const Command &command)
const
751 LOG_ERR <<
"Server not connected, cannot send " << command;
754 LOG_TRC <<
"Sending command: " << command;
755 return INVOKE_SIMCONNECT(SetClientData, hSim,
756 (SIMCONNECT_CLIENT_DATA_ID)CLI_DATA_COMMAND, (SIMCONNECT_CLIENT_DATA_DEFINITION_ID)CLI_DATA_COMMAND,
757 SIMCONNECT_CLIENT_DATA_SET_FLAG_DEFAULT, 0UL, (DWORD)
sizeof(
Command), (
void *)&command
762 HRESULT sendServerCommand(
Command &&command, weak_ptr<condition_variable_any> cv, uint32_t *newToken =
nullptr)
767 command.token = nextCmdToken++;
768 enqueueTrackedResponse(command.token, cv);
769 const HRESULT hr = sendServerCommand(command);
771 unique_lock vlock(mtxResponses);
772 reponses.erase(command.token);
775 *newToken = command.token;
783 shared_ptr<condition_variable_any> cv = make_shared<condition_variable_any>();
784 const CommandId origId = command.commandId;
785 HRESULT hr = sendServerCommand(move(command), weak_ptr(cv), &token);
787 if FAILED(hr = waitCommandResponse(token, response, timeout))
788 LOG_ERR <<
"Command " << Utilities::getEnumName(origId, CommandIdNames) <<
" timed out after " << (timeout ? timeout : settings.networkTimeout) <<
"ms.";
794#pragma region Command Response tracking ----------------------------------------------
796 TrackedResponse *findTrackedResponse(uint32_t token)
798 shared_lock lock{mtxResponses};
799 const responseMap_t::iterator pos = reponses.find(token);
800 if (pos != reponses.cend())
805 TrackedResponse *enqueueTrackedResponse(uint32_t token, weak_ptr<condition_variable_any> cv)
807 unique_lock lock{mtxResponses};
808 return &reponses.try_emplace(token, token, cv).first->second;
812 HRESULT waitCommandResponse(uint32_t token,
Command *response, uint32_t timeout = 0, std::function<
bool(
void)> extraPredicate =
nullptr)
814 TrackedResponse *tr = findTrackedResponse(token);
815 shared_ptr<condition_variable_any> cv {};
816 if (!tr || !(cv = tr->cv.lock()))
820 timeout = settings.networkTimeout;
822 bool stopped =
false;
823 auto stop_waiting = [=, &stopped]() {
825 !serverConnected || (tr->response.commandId != CommandId::None && tr->response.token == token && (!extraPredicate || extraPredicate()));
829 if (stop_waiting()) {
833 unique_lock lock(tr->mutex);
835 if (cv->wait_for(lock, chrono::milliseconds(timeout), stop_waiting))
838 else if (!!extraPredicate) {
839 cv->wait(lock, stop_waiting);
840 hr = stopped ? ERROR_ABANDONED_WAIT_0 : S_OK;
844 LOG_DBG <<
"waitCommandResponse() requires a predicate condition when timeout parameter is < 0.";
847 if (SUCCEEDED(hr) && !!response) {
848 unique_lock lock(tr->mutex);
849 *response = move(tr->response);
852 unique_lock vlock(mtxResponses);
853 reponses.erase(token);
859 HRESULT waitCommandResponseAsync(uint32_t token,
Command *response, uint32_t timeout = 0, std::function<
bool(
void)> extraPredicate =
nullptr)
862 timeout = settings.networkTimeout;
863 future<HRESULT> taskResult = async(launch::async, &Private::waitCommandResponse,
this, token, response, timeout, extraPredicate);
864 const future_status taskStat = taskResult.wait_for(chrono::milliseconds(timeout * 2));
865 if (taskStat == future_status::ready)
866 return taskResult.get();
872 void waitListRequestEnd()
874 auto stop_waiting = [
this]() {
875 return listResult.nextTimeout.load() >= Clock::now();
879 HRESULT hr = waitCommandResponse(listResult.token, &response, -1, stop_waiting);
880 if (hr == ERROR_ABANDONED_WAIT_0) {
881 LOG_ERR <<
"List request timed out.";
884 else if (hr != S_OK) {
885 LOG_ERR <<
"List request failed with result: " << LOG_HR(hr);
887 else if (response.
commandId != CommandId::Ack) {
888 LOG_WRN <<
"Server returned Nak for list request of " << Utilities::getEnumName(listResult.listType.load(), LookupItemTypeNames);
892 invokeCallback(listCb, ListResult { listResult.listType.load(), hr, listResult.getResult() });
897#pragma region Server-side logging commands ----------------------------------------------
900 if (fac == LogFacility::None || fac > LogFacility::All)
902 if (+fac & +LogFacility::Remote)
904 if (+fac & +LogFacility::Console)
906 if (+fac & +LogFacility::File)
909 updateServerLogLevel(fac);
912 void updateServerLogLevel(
LogFacility fac = LogFacility::Remote) {
913 if (checkInit() && (!(+fac & +LogFacility::Remote) || SUCCEEDED(registerLogDataArea())))
917 HRESULT registerLogDataArea() {
918 if (logCDAcreated || settings.logLevel(
LogSource::Server, LogFacility::Remote) == LogLevel::None)
922 if SUCCEEDED(hr = registerDataArea(CDA_NAME_LOG_PFX, CLI_DATA_LOG, CLI_DATA_LOG,
sizeof(
LogRecord),
false)) {
924 INVOKE_SIMCONNECT(RequestClientData, hSim, (SIMCONNECT_CLIENT_DATA_ID)CLI_DATA_LOG, (SIMCONNECT_DATA_REQUEST_ID)DATA_REQ_LOG,
925 (SIMCONNECT_CLIENT_DATA_DEFINITION_ID)CLI_DATA_LOG, SIMCONNECT_CLIENT_DATA_PERIOD_ON_SET, 0UL, 0UL, 0UL, 0UL);
927 logCDAcreated = SUCCEEDED(hr);
932#pragma region Remote Variables accessors ----------------------------------------------
935 const string buildVariableCommandString(
const VariableRequest &v,
bool isLocal)
const
937 const bool isIndexed = isLocal || Utilities::isIndexedVariableType(v.variableType);
938 const bool hasUnits = isLocal || Utilities::isUnitBasedVariableType(v.variableType);
939 string sValue = (isIndexed && v.variableId > -1 ? to_string(v.variableId) : v.variableName);
942 if (v.variableType ==
'A' && v.simVarIndex)
943 sValue +=
':' + to_string(v.simVarIndex);
945 if (isIndexed && v.unitId > -1)
946 sValue +=
',' + to_string(v.unitId);
947 else if (!v.unitName.empty())
948 sValue +=
',' + v.unitName;
953 HRESULT
getVariable(
const VariableRequest &v,
double *result, std::string *sResult =
nullptr,
double dflt = 0.0)
955 const string sValue = buildVariableCommandString(v,
false);
956 if (sValue.empty() || sValue.length() >=
STRSZ_CMD)
961 if FAILED(hr =
sendCommandWithResponse(
Command(v.createLVar && v.variableType ==
'L' ? CommandId::GetCreate : CommandId::Get, v.variableType, sValue.c_str(), dflt), &response))
963 if (response.
commandId != CommandId::Ack) {
964 LOG_WRN <<
"Get Variable request for " << quoted(sValue) <<
" returned Nak response. Reason, if any: " << quoted(response.
sData);
968 *result = response.
fData;
970 *sResult = response.
sData;
976 const string sValue = buildVariableCommandString(v,
true);
977 if (sValue.empty() || sValue.length() >=
STRSZ_CMD)
979 return sendServerCommand(
Command(v.createLVar ? CommandId::SetCreate : CommandId::Set,
'L', sValue.c_str(), value));
982 HRESULT
setVariable(
const VariableRequest &v,
const double value)
984 if (v.variableType ==
'L')
987 if (!Utilities::isSettableVariableType(v.variableType)) {
988 LOG_WRN <<
"Cannot Set a variable of type '" << v.variableType <<
"'.";
992 if (v.variableName.empty()) {
993 LOG_WRN <<
"A variable name is required to set variable of type '" << v.variableType <<
"'.";
996 if (v.unitId > -1 && v.unitName.empty()) {
997 LOG_WRN <<
"A variable of type '" << v.variableType <<
"' unit ID; use a unit name instead.";
1002 ostringstream codeStr = ostringstream();
1003 codeStr << fixed << setprecision(7) << value;
1004 codeStr <<
" (>" << v.variableType <<
':' << v.variableName;
1005 if (v.variableType ==
'A' && v.simVarIndex)
1006 codeStr <<
':' << (uint16_t)v.simVarIndex;
1007 if (!v.unitName.empty())
1008 codeStr <<
',' << v.unitName;
1011 if (codeStr.tellp() == -1 || (size_t)codeStr.tellp() >=
STRSZ_CMD)
1012 return E_INVALIDARG;
1015 return sendServerCommand(
Command(CommandId::Exec, +CalcResultType::None, codeStr.str().c_str()));
1018 HRESULT setStringVariable(
const VariableRequest &v,
const std::string &value)
1021 LOG_ERR <<
"Simulator not connected, cannot set string variable.";
1024 if (v.variableType !=
'A') {
1025 LOG_WRN <<
"Cannot Set a string value on variable of type '" << v.variableType <<
"'.";
1026 return E_INVALIDARG;
1028 if (v.variableName.empty()) {
1029 LOG_WRN <<
"A variable name is required to set variable of type '" << v.variableType <<
"'.";
1030 return E_INVALIDARG;
1036 shared_lock lock { mtxMappedVarNames };
1037 const auto pos = mappedVarsNameCache.find(v.variableName);
1038 if (pos != mappedVarsNameCache.cend())
1039 mapId = pos->second;
1044 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))
1046 mapId = nextMappedVarID++;
1047 unique_lock lock { mtxMappedVarNames };
1048 mappedVarsNameCache.insert(std::pair{ v.variableName, mapId });
1051 return INVOKE_SIMCONNECT(SetDataOnSimObject, hSim, (SIMCONNECT_DATA_DEFINITION_ID)mapId, SIMCONNECT_OBJECT_ID_USER, 0UL, 0UL, (DWORD)(value.length() + 1), (
void*)value.c_str());
1055#pragma region Data Requests ----------------------------------------------
1057 TrackedRequest *findRequest(uint32_t
id)
1059 shared_lock lock{mtxRequests};
1060 const requestMap_t::iterator pos = requests.find(
id);
1061 if (pos != requests.cend())
1062 return &pos->second;
1066 const TrackedRequest *findRequest(uint32_t
id)
const {
1067 shared_lock lock{mtxRequests};
1068 const requestMap_t::const_iterator pos = requests.find(
id);
1069 if (pos != requests.cend())
1070 return &pos->second;
1078 LOG_ERR <<
"Server not connected, cannot submit DataRequest " << req;
1081 LOG_DBG <<
"Sending request: " << req;
1082 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);
1087 HRESULT sendDataRequest(
const DataRequest &req,
bool async)
1090 if FAILED(hr = writeDataRequest(req))
1094 if (async || req.
requestType == RequestType::None)
1097 shared_ptr<condition_variable_any> cv = make_shared<condition_variable_any>();
1098 enqueueTrackedResponse(req.
requestId, weak_ptr(cv));
1100 if FAILED(hr = waitCommandResponse(req.
requestId, &response)) {
1101 LOG_ERR <<
"Data Request timed out after " << settings.networkTimeout <<
"ms.";
1103 else if (response.
commandId != CommandId::Ack) {
1105 LOG_ERR <<
"Data Request returned Nak response for DataRequest ID " << req.
requestId <<
". Reason, if any: " << quoted(response.
sData);
1110 HRESULT registerDataRequestArea(
const TrackedRequest *
const tr,
bool isNewRequest,
bool dataAllocChanged =
false)
1113 shared_lock lock{mtxRequests};
1114 if (isNewRequest || dataAllocChanged) {
1117 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)))
1120 else if (dataAllocChanged) {
1122 deregisterDataRequestArea(tr);
1124 if FAILED(hr = SimConnectHelper::addClientDataDefinition(hSim, tr->dataId, tr->valueSize, max(tr->deltaEpsilon, 0.0f)))
1129 return INVOKE_SIMCONNECT(
1130 RequestClientData, hSim,
1131 (SIMCONNECT_CLIENT_DATA_ID)tr->dataId, (SIMCONNECT_DATA_REQUEST_ID)tr->requestId + SIMCONNECTID_LAST,
1132 (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
1137 HRESULT deregisterDataRequestArea(const TrackedRequest * const tr)
const
1141 RequestClientData, hSim,
1142 (SIMCONNECT_CLIENT_DATA_ID)tr->dataId, (SIMCONNECT_DATA_REQUEST_ID)tr->requestId + SIMCONNECTID_LAST,
1143 (SIMCONNECT_CLIENT_DATA_DEFINITION_ID)tr->dataId, SIMCONNECT_CLIENT_DATA_PERIOD_NEVER, 0UL, 0UL, 0UL, 0UL
1146 return SimConnectHelper::removeClientDataDefinition(hSim, tr->dataId);
1149 HRESULT addOrUpdateRequest(
const DataRequest &req,
bool async)
1156 LOG_ERR <<
"Error in DataRequest ID: " << req.
requestId <<
"; Parameter 'nameOrCode' cannot be empty.";
1157 return E_INVALIDARG;
1160 const uint32_t actualValSize = Utilities::getActualValueSize(req.
valueSize);
1161 if (actualValSize > SIMCONNECT_CLIENTDATA_MAX_SIZE) {
1162 LOG_ERR <<
"Error in DataRequest ID: " << req.
requestId <<
"; Value size " << actualValSize <<
" exceeds SimConnect maximum size " << SIMCONNECT_CLIENTDATA_MAX_SIZE;
1163 return E_INVALIDARG;
1165 if (totalDataAlloc + actualValSize > SIMCONNECT_DATA_ALLOC_LIMIT) {
1166 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;
1167 return E_INVALIDARG;
1170 TrackedRequest *tr = findRequest(req.
requestId);
1171 const bool isNewRequest = (tr ==
nullptr);
1172 bool dataAllocationChanged =
false;
1175 unique_lock lock{mtxRequests};
1176 tr = &requests.try_emplace(req.
requestId, req, nextDefId++).first->second;
1179 if (actualValSize > tr->dataSize) {
1180 LOG_ERR <<
"Value size cannot be increased after request is created.";
1181 return E_INVALIDARG;
1184 dataAllocationChanged = (actualValSize != tr->dataSize || !fuzzyCompare(req.
deltaEpsilon, tr->deltaEpsilon));
1186 unique_lock lock{mtxRequests};
1193 if (isNewRequest || dataAllocationChanged)
1194 hr = registerDataRequestArea(tr, isNewRequest, dataAllocationChanged);
1197 hr = sendDataRequest(req, async);
1199 if (FAILED(hr) && isNewRequest) {
1203 LOG_TRC << (FAILED(hr) ?
"FAILED" : isNewRequest ?
"Added" :
"Updated") <<
" request: " << req;
1206 LOG_TRC <<
"Queued " << (isNewRequest ?
"New" :
"Updated") <<
" request: " << req;
1212 HRESULT removeRequest(
const uint32_t requestId)
1214 TrackedRequest *tr = findRequest(requestId);
1216 LOG_WRN <<
"DataRequest ID " << requestId <<
" not found.";
1220 unique_lock lock{mtxRequests};
1222 if FAILED(deregisterDataRequestArea(tr))
1223 LOG_WRN <<
"Failed to clear ClientDataDefinition in SimConnect, check log messages.";
1224 if FAILED(writeDataRequest(
DataRequest(requestId, 0, RequestType::None)))
1225 LOG_WRN <<
"Server removal of request " << requestId <<
" failed or timed out, check log messages.";
1227 requests.erase(requestId);
1228 LOG_TRC <<
"Removed Data Request " << requestId;
1234 void registerAllDataRequests()
1238 shared_lock lock(mtxRequests);
1239 for (
const auto & [_, tr] : requests) {
1240 if (tr.requestType != RequestType::None)
1241 writeDataRequest(tr);
1247 void registerAllDataRequestAreas()
1251 for (
const auto & [_, tr] : requests)
1252 registerDataRequestArea(&tr,
true);
1255#pragma endregion Data Requests
1257#pragma region Calculator Events --------------------------------------------
1259 TrackedEvent *findTrackedEvent(uint32_t eventId)
1261 shared_lock lock{mtxEvents};
1262 const eventMap_t::iterator pos = events.find(eventId);
1263 if (pos != events.cend())
1264 return &pos->second;
1269 HRESULT sendEventRegistration(TrackedEvent *ev,
bool resendName =
false)
1272 return E_INVALIDARG;
1274 if (ev->code.empty()) {
1275 hr = sendServerCommand(
Command(CommandId::Register, ev->eventId));
1277 unique_lock lock(mtxEvents);
1278 events.erase(ev->eventId);
1283 const string evStr = (!ev->sentToServer || resendName) && !ev->name.empty() ? ev->name +
'$' + ev->code : ev->code;
1284 return sendServerCommand(
Command(CommandId::Register, ev->eventId, evStr.c_str()));
1287 HRESULT sendEventRegistration(uint32_t eventId) {
1288 return sendEventRegistration(findTrackedEvent(eventId));
1292 void registerAllEvents()
1294 vector<uint32_t> removals {};
1295 shared_lock lock(mtxEvents);
1296 for (
auto & [key, ev] : events) {
1297 if (ev.code.empty())
1298 removals.push_back(key);
1300 sendEventRegistration(&ev,
true);
1303 for (
const uint32_t &
id : removals)
1304 sendEventRegistration(
id);
1309#pragma region Simulator Key Events -------------------------------------------
1312 HRESULT writeKeyEvent(
const KeyEvent &kev)
const
1315 LOG_ERR <<
"Server not connected, cannot send " << kev;
1318 LOG_TRC <<
"Sending: " << kev;
1319 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);
1328 unique_lock lock(mtxKeyEventNames);
1330 const auto pos = keyEventNameCache.find(name);
1331 if (pos != keyEventNameCache.cend()) {
1334 *pRetId = pos->second;
1339 uint32_t keyId = nextCustomEventID++;
1342 keyId |= CUSTOM_KEY_EVENT_LEGACY_TRIGGER_FLAG;
1344 keyEventNameCache.insert(std::pair{ name, keyId });
1351 return mapCustomKeyEvent(keyId, name);
1352 LOG_DBG <<
"Queued registration of custom simulator key event " << quoted(name) <<
" with ID " << keyId <<
".";
1357 HRESULT mapCustomKeyEvent(uint32_t
id,
const std::string &name)
const
1359 const HRESULT hr = SimConnectHelper::newClientEvent(hSim,
id, name);
1361 LOG_DBG <<
"Registered custom simulator key event " << quoted(name) <<
" with id " <<
id <<
".";
1363 LOG_ERR <<
"Registration custom simulator key event " << quoted(name) <<
" for id " <<
id <<
" failed with HRESULT " << LOG_HR(hr);
1368 void mapAllCustomKeyEvents()
1370 shared_lock lock(mtxKeyEventNames);
1371 for (
const auto& [name,
id] : keyEventNameCache) {
1373 mapCustomKeyEvent(
id, name);
1378 HRESULT sendSimCustomKeyEvent(uint32_t keyEventId, DWORD v1, DWORD v2, DWORD v3, DWORD v4, DWORD v5)
const
1380 if (!simConnected) {
1381 LOG_ERR <<
"Simulator not connected, cannot send a Custom Key Event.";
1385 LOG_TRC <<
"Invoking " << ((keyEventId & CUSTOM_KEY_EVENT_LEGACY_TRIGGER_FLAG) ?
"TransmitClientEvent" :
"TransmitClientEvent_EX1") <<
" with Event ID " << keyEventId <<
"; values: " << v1 <<
"; " << v2 <<
"; " << v3 <<
"; " << v4 <<
"; " << v5 <<
';';
1386 if (keyEventId & CUSTOM_KEY_EVENT_LEGACY_TRIGGER_FLAG)
1387 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);
1388 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);
1393#pragma region SimConnect message processing ----------------------------------------------
1395 static void CALLBACK dispatchMessage(SIMCONNECT_RECV *pData, DWORD cbData,
void *pContext) {
1396 static_cast<Private*
>(pContext)->onSimConnectMessage(pData, cbData);
1401 LOG_DBG <<
"Dispatch loop started.";
1402 const HANDLE waitEvents[] = { hDispatchStopEvent, hSimEvent };
1404 runDispatchLoop =
true;
1405 while (runDispatchLoop) {
1406 hr = WaitForMultipleObjects(2, waitEvents,
false, DISPATCH_LOOP_WAIT_TIME);
1410 case WAIT_OBJECT_0 + 1:
1411 SimConnect_CallDispatch(hSim, Private::dispatchMessage,
this);
1416 LOG_ERR <<
"Dispatch loop WaitForMultipleObjects returned: " << LOG_HR(hr) <<
" Error: " << LOG_HR(GetLastError());
1419 runDispatchLoop =
false;
1421 LOG_DBG <<
"Dispatch loop stopped.";
1424 void onSimConnectMessage(SIMCONNECT_RECV *pData, DWORD cbData)
1426 LOG_TRC << LOG_SC_RECV(pData);
1427 switch (pData->dwID) {
1429 case SIMCONNECT_RECV_ID_CLIENT_DATA: {
1430 SIMCONNECT_RECV_CLIENT_DATA* data = (SIMCONNECT_RECV_CLIENT_DATA*)pData;
1431 LOG_TRC << LOG_SC_RCV_CLIENT_DATA(data);
1433 const size_t dataSize = (size_t)pData->dwSize + 4 -
sizeof(SIMCONNECT_RECV_CLIENT_DATA);
1434 switch (data->dwRequestID)
1436 case DATA_REQ_RESPONSE: {
1438 if (dataSize !=
sizeof(
Command)) {
1439 LOG_CRT <<
"Invalid Command struct data size! Expected " <<
sizeof(
Command) <<
" but got " << dataSize;
1443 const Command *
const cmd =
reinterpret_cast<const Command *const
>(&data->dwData);
1444 LOG_DBG <<
"Got Command: " << *cmd;
1445 bool checkTracking =
true;
1448 case CommandId::Ack:
1449 case CommandId::Nak: {
1453 case CommandId::Connect:
1454 serverConnected = cmd->
commandId == CommandId::Ack && cmd->
token == clientId;
1456 checkTracking =
false;
1462 invokeCallback(cmdResultCb, *cmd);
1467 case CommandId::Ping:
1468 sendServerCommand(
Command(CommandId::Ack, (uint32_t)CommandId::Ping));
1469 checkTracking =
false;
1473 case CommandId::List: {
1474 unique_lock lock(listResult.mutex);
1475 if (cmd->
token == listResult.token) {
1476 listResult.nextTimeout = Clock::now() + chrono::milliseconds(settings.networkTimeout);
1477 listResult.result.emplace_back((
int)cmd->
uData, cmd->
sData);
1480 LOG_WRN <<
"Received unexpected list result for wrong command token. Expected " << listResult.token <<
" got " << cmd->
token;
1482 checkTracking =
false;
1487 case CommandId::Disconnect:
1489 checkTracking =
false;
1497 if (checkTracking) {
1498 if (TrackedResponse *tr = findTrackedResponse(cmd->
token)) {
1499 if (
auto cv = tr->cv.lock()) {
1500 LOG_TRC <<
"Got awaited command response, notifying CV.";
1502 unique_lock lock(tr->mutex);
1503 tr->response = *cmd;
1508 unique_lock lock(mtxResponses);
1509 reponses.erase(cmd->
token);
1510 LOG_TRC <<
"Got tracked command response but no CV, removing record.";
1515 invokeCallback(respCb, *cmd);
1519 case DATA_REQ_LOG: {
1522 LOG_CRT <<
"Invalid LogRecord struct data size! Expected " <<
sizeof(
LogRecord) <<
" but got " << dataSize;
1526 const LogRecord *
const log =
reinterpret_cast<const LogRecord *const
>(&data->dwData);
1527 LOG_TRC <<
"Got Log Record: " << *log;
1534 if (data->dwRequestID >= SIMCONNECTID_LAST) {
1535 TrackedRequest *tr = findRequest(data->dwRequestID - SIMCONNECTID_LAST);
1537 LOG_WRN <<
"DataRequest ID " << data->dwRequestID - SIMCONNECTID_LAST <<
" not found in tracked requests.";
1541 if (dataSize < tr->dataSize) {
1542 LOG_CRT <<
"Invalid data result size! Expected " << tr->dataSize <<
" but got " << dataSize;
1545 unique_lock datalock(tr->m_dataMutex);
1546 memcpy(tr->data.data(), (
void*)&data->dwData, tr->dataSize);
1547 tr->lastUpdate = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
1549 shared_lock rdlock(mtxRequests);
1550 LOG_TRC <<
"Got data result for request: " << *tr;
1551 invokeCallback(dataCb, tr->toRequestRecord());
1554 LOG_WRN <<
"Got unknown RequestID in SIMCONNECT_RECV_CLIENT_DATA struct: " << data->dwRequestID;
1562 case SIMCONNECT_RECV_ID_EVENT: {
1563 SIMCONNECT_RECV_EVENT *data =
static_cast<SIMCONNECT_RECV_EVENT *
>(pData);
1564 LOG_TRC << LOG_SC_RECV_EVENT(data);
1566 switch (data->uEventID) {
1567 case CLI_EVENT_PING_RESP:
1569 serverLastSeen = Clock::now();
1570 LOG_DBG <<
"Got ping response at " << Utilities::timePointToString(serverLastSeen.load()) <<
" with version " << STREAM_HEX8(
serverVersion);
1579 case SIMCONNECT_RECV_ID_OPEN:
1580 simConnected =
true;
1581 SimConnectHelper::logSimVersion(pData);
1584 case SIMCONNECT_RECV_ID_QUIT: {
1585 LOG_INF <<
"Received quit command from SimConnect, disconnecting...";
1591 case SIMCONNECT_RECV_ID_EXCEPTION:
1592 SimConnectHelper::logSimConnectException(pData);
1600#pragma endregion SimConnect
1603#pragma endregion WASimClient::Private
1607#pragma region WASimClient class
1611#define d_const const_cast<const Private *>(d.get())
1614 d(new Private(this, clientId, configFile))
1620 if (d->hDispatchStopEvent)
1621 CloseHandle(d->hDispatchStopEvent);
1624#pragma region Connections ----------------------------------------------
1629 LOG_ERR <<
"Client ID must be greater than zero.";
1630 return E_INVALIDARG;
1632 return d->connectSimulator(timeout);
1636 return d->connectSimulator(networkConfigId, timeout);
1640 d->disconnectSimulator(
false);
1644 return d->connectServer(timeout);
1648 d->disconnectServer(
true);
1653 if (!d->checkInit() && FAILED(d->connectSimulator(timeout)))
1657 const Clock::time_point refTp = d->serverLastSeen;
1659 if FAILED(INVOKE_SIMCONNECT(
1660 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
1667 if (d->waitCondition([&]() { return !d->runDispatchLoop || d->serverLastSeen.load() > refTp; }, timeout)) {
1668 LOG_INF <<
"Server responded to ping, reported version: " << STREAM_HEX8(d->serverVersion);
1669 return d->serverVersion;
1671 LOG_WRN <<
"Server did not respond to ping after " << timeout <<
"ms";
1677#pragma region Exec calc ----------------------------------------------
1682 LOG_ERR <<
"Code string length " << code.length() <<
" is greater then maximum size of " <<
STRSZ_CMD-1;
1683 return E_INVALIDARG;
1686 if (resultType == CalcResultType::None)
1687 return d->sendServerCommand(
Command(CommandId::Exec, +resultType, code.c_str()));
1691 if FAILED(hr = d->sendCommandWithResponse(
Command(CommandId::Exec, +resultType, code.c_str()), &response))
1693 if (response.
commandId != CommandId::Ack) {
1694 LOG_WRN <<
"Calculator Exec with code " << quoted(code) <<
" returned Nak response. Reason, if any: " << quoted(response.
sData);
1698 *fResult = response.
fData;
1700 *sResult = response.
sData;
1706#pragma region Variable accessors ----------------------------------------------
1711 LOG_ERR <<
"Cannot get variable type '" << variable.
variableType <<
"' by index.";
1712 return E_INVALIDARG;
1714 return d->getVariable(variable, pfResult, psResult);
1718 return d->getVariable(
VariableRequest(variableName,
false, unitName), pfResult);
1722 return d->getVariable(
VariableRequest(variableName,
true, unitName), pfResult,
nullptr, defaultValue);
1727 return d->setVariable(variable, value);
1731 return d->setStringVariable(variable, value);
1735 return d->setLocalVariable(
VariableRequest(variableName,
false, unitName), value);
1739 return d->setLocalVariable(
VariableRequest(variableName,
true, unitName), value);
1744#pragma region Data Requests ----------------------------------------------
1747 return d->addOrUpdateRequest(request, async);
1751 return d->removeRequest(requestId);
1756 if (d->requests.count(requestId))
1757 return d->sendServerCommand(
Command(CommandId::Update, requestId));
1765 const Private::TrackedRequest *r = d_const->findRequest(requestId);
1767 return r->toRequestRecord();
1768 LOG_ERR <<
"Data Request ID " << requestId <<
" not found.";
1774 vector<DataRequestRecord> ret;
1775 shared_lock lock(d_const->mtxRequests);
1776 ret.reserve(d_const->requests.size());
1777 for (
const auto & [_, value] : d_const->requests) {
1778 if (value.requestType != RequestType::None)
1779 ret.emplace_back(value.toRequestRecord());
1786 vector<uint32_t> ret;
1787 shared_lock lock(d_const->mtxRequests);
1788 ret.reserve(d_const->requests.size());
1789 for (
const auto & [key, value] : d_const->requests) {
1790 if (value.requestType != RequestType::None)
1798 HRESULT hr = d_const->sendServerCommand(
Command(CommandId::Subscribe, (paused ? 0 : 1)));
1800 d->requestsPaused = paused;
1803 d->requestsPaused = paused;
1807#pragma endregion Data
1809#pragma region Calculator Events ----------------------------------------------
1813 if (eventData.
code.empty())
1816 const size_t len = eventData.
code.length() + (eventData.
name.empty() ? 0 : eventData.
name.length() + 1);
1818 LOG_ERR <<
"Resulting command string length " << len <<
" is greater then maximum size of " <<
STRSZ_CMD-1 <<
" bytes.";
1819 return E_INVALIDARG;
1822 Private::TrackedEvent *ev = d->findTrackedEvent(eventData.
eventId);
1824 if (!eventData.
name.empty() && ev->name != eventData.
name) {
1825 LOG_ERR <<
"Cannot change name of event ID " << eventData.
eventId <<
" after it has been registered.";
1826 return E_INVALIDARG;
1828 if (ev->code == eventData.
code) {
1829 LOG_INF <<
"No changes detected in code for event ID " << eventData.
eventId <<
", skipping update";
1832 ev->code = eventData.
code;
1835 unique_lock lock(d->mtxEvents);
1836 ev = &d->events.emplace(eventData.
eventId, Private::TrackedEvent(eventData.
eventId, eventData.
code, eventData.
name)).first->second;
1839 ev->sentToServer = SUCCEEDED(d->sendEventRegistration(ev)) || ev->sentToServer;
1841 LOG_DBG <<
"Queued event ID " << eventData.
eventId <<
" for next server connection.";
1848 Private::TrackedEvent *ev = d->findTrackedEvent(eventId);
1850 LOG_ERR <<
"Event ID " << eventId <<
" not found.";
1851 return E_INVALIDARG;
1854 if (!ev->sentToServer) {
1855 unique_lock lock(d->mtxEvents);
1856 d->events.erase(eventId);
1860 d->sendEventRegistration(ev);
1862 LOG_DBG <<
"Queued event ID " << eventId <<
" for deletion on next server connection.";
1867 return d->sendServerCommand(
Command(CommandId::Transmit, eventId));
1873 if (Private::TrackedEvent *ev = d->findTrackedEvent(eventId))
1875 LOG_ERR <<
"Event ID " << eventId <<
" not found.";
1881 vector<RegisteredEvent> ret;
1882 shared_lock lock(d_const->mtxEvents);
1883 ret.reserve(d_const->events.size());
1884 for (
const auto & [_, value] : d_const->events) {
1885 if (!value.code.empty())
1891#pragma endregion Calculator Events
1893#pragma region Key Events ----------------------------------------------
1898 return d_const->writeKeyEvent(
KeyEvent(keyEventId, { v1, v2, v3, v4, v5 }, d->nextCmdToken++));
1900 return d_const->sendSimCustomKeyEvent(keyEventId, v1, v2, v3, v4, v5);
1903inline static bool isCustomKeyEventName(
const std::string &name) {
1904 return name[0] ==
'#' || name.find(
'.') != string::npos;
1909 if (keyEventName.empty())
1910 return E_INVALIDARG;
1914 shared_lock rdlock(d->mtxKeyEventNames);
1915 const auto pos = d_const->keyEventNameCache.find(keyEventName);
1916 if (pos != d_const->keyEventNameCache.cend())
1923 if (isCustomKeyEventName(keyEventName)) {
1925 const HRESULT hr = d->registerCustomKeyEvent(keyEventName, &keyId,
false);
1932 const HRESULT hr =
lookup(LookupItemType::KeyEventId, keyEventName, &
id);
1934 return hr == E_FAIL ? E_INVALIDARG : hr;
1935 keyId = (uint32_t)
id;
1937 unique_lock rwlock(d->mtxKeyEventNames);
1938 d->keyEventNameCache.insert(std::pair{ keyEventName, keyId });
1948 if (isCustomKeyEventName(customEventName))
1949 return d->registerCustomKeyEvent(customEventName, puiCustomEventId, useLegacyTransmit);
1951 LOG_ERR <<
"Custom event name " << quoted(customEventName) <<
" must start with '#' or include a period.";
1952 return E_INVALIDARG;
1957 unique_lock lock(d->mtxKeyEventNames);
1958 const auto pos = find_if(cbegin(d_const->keyEventNameCache), cend(d_const->keyEventNameCache), [eventId](
const auto &p) { return p.second == eventId; });
1959 if (pos == d_const->keyEventNameCache.cend())
1960 return E_INVALIDARG;
1961 d->keyEventNameCache.erase(pos);
1967 unique_lock lock(d->mtxKeyEventNames);
1968 return d->keyEventNameCache.erase(customEventName) > 0 ? S_OK : E_INVALIDARG;
1971#pragma endregion Key Events
1973#pragma region Meta Data ----------------------------------------------
1979 if (!validListLookups.count(itemsType)) {
1980 LOG_ERR <<
"Cannot list item of type " << Utilities::getEnumName(itemsType, LookupItemTypeNames) <<
".";
1981 return E_INVALIDARG;
1984 if (d->listResult.listType != LookupItemType::None) {
1985 LOG_ERR <<
"A List request for " << Utilities::getEnumName(d->listResult.listType.load(), LookupItemTypeNames) <<
" is currently pending.";
1986 return E_INVALIDARG;
1988 unique_lock lock(d->listResult.mutex);
1989 d->listResult.listType = itemsType;
1990 d->listResult.nextTimeout = Clock::now() + chrono::milliseconds(
defaultTimeout());
1992 if FAILED(hr = d->sendServerCommand(
Command(CommandId::List, +itemsType), weak_ptr(d->listResult.cv), &d->listResult.token))
1996 thread(&Private::waitListRequestEnd, d.get()).detach();
1998 catch (exception *e) {
1999 LOG_ERR <<
"Exception trying to schedule thread for List command: " << e->what();
2008 LOG_ERR <<
"Item name length " << itemName.length() <<
" is greater then maximum size of " <<
STRSZ_CMD-1 <<
" bytes.";
2009 return E_INVALIDARG;
2013 if FAILED(hr = d->sendCommandWithResponse(
Command(CommandId::Lookup, +itemType, itemName.c_str()), &response))
2015 if (response.
commandId != CommandId::Ack) {
2016 LOG_WRN <<
"Lookup command returned Nak response. Reason, if any: " << quoted(response.
sData);
2019 LOG_DBG <<
"Lookup: Server returned ID " << (int)response.
fData <<
" and echo name " << quoted(response.
sData);
2021 *piResult = (int)response.
fData;
2025#pragma endregion Meta
2027#pragma region Low Level ----------------------------------------------
2030 return d->sendServerCommand(command);
2034 return d->sendCommandWithResponse(
Command(command), response, timeout);
2037#pragma endregion Low Level
2039#pragma region Status / Network / Logging / Callbacks ----------------------------------------------
2058 if ((+facility & +LogFacility::Console) && d_const->settings.logLevel(source, LogFacility::Console) != level)
2059 d->setConsoleLogLevel(level);
2060 if ((+facility & +LogFacility::File) && d_const->settings.logLevel(source, LogFacility::File) != level)
2061 d->setFileLogLevel(level);
2062 if ((+facility & +LogFacility::Remote) && d_const->settings.logLevel(source, LogFacility::Remote) != level)
2063 d->setCallbackLogLevel(level);
2066 d->setServerLogLevel(level, facility);
2077#pragma endregion Misc
2079#pragma endregion WASimClient
2082#undef WAIT_CONDITION
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 server-side lookup of an named item to find the corresponding numeric ID.
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...
static const HRESULT E_TIMEOUT
Error result: timeout communicating with server.
std::function< void __stdcall(const LogRecord &, LogSource)> logCallback_t
Callback function for log entries (from both Client and Server).
std::function< void __stdcall(const DataRequestRecord &)> dataCallback_t
Callback function for subscription result data.
std::function< void __stdcall(const ListResult &)> listResultsCallback_t
Callback function for delivering list results, eg. of local variables sent from Server.
std::function< void __stdcall(const Command &)> commandCallback_t
Callback function for commands sent from server.
LogSource
Log entry source, Client or Server.
@ Client
Log record from WASimClient.
@ Server
Log record from WASimModule (Server)
std::vector< std::pair< int, std::string > > listResult_t
A mapping of IDs to names.
static const HRESULT E_NOT_CONNECTED
Error result: server not connected.
static const uint32_t CUSTOM_KEY_EVENT_ID_MIN
Starting ID range for "Custom Key Events" for use with registerCustomKeyEvent() generated IDs....
std::function< void __stdcall(const ClientEvent &)> clientEventCallback_t
Callback function for Client events.
@ SimDisconnecting
ShuttingDown status.
@ ServerDisconnected
From Connected to SimConnected status change.
@ SimConnecting
Initializing status.
@ SimConnected
SimConnected status.
@ ServerConnecting
Connecting status.
@ ServerConnected
Connected status.
@ SimDisconnected
From SimConnected to Initializing status change.
ClientStatus
Client status flags.
@ ShuttingDown
closing SimConnect link, before being back in Idle status mode
@ Connected
connected to WASim server
@ Initializing
trying to connect to SimConnect
@ SimConnected
have SimConnect link
@ Connecting
attempting connection to WASim server
@ Idle
no active SimConnect or WASim server connection
WASimCommander::Enums namespace. Contains all enum definitions for the base API.
LookupItemType
Types of things to look up or list.
@ DataRequest
Saved value subscription for current Client, indexed by requestId and nameOrCode values....
@ RegisteredEvent
Saved calculator event for current Client, indexed by eventId and code values. Available for List and...
CalcResultType
The type of result that calculator code is expected to produce.
CommandId
Commands for client-server interaction. Both sides can send commands via dedicated channels by writin...
RequestType
Types of things to request or set.
LogFacility
Logging destination type.
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...
DataRequestRecord()
< Inherits DataRequest assignment operators.
Structure to hold data for registered (reusable) calculator events. Used to submit events with WASimC...
std::string name
Optional custom name for this event. The name is for use with SimConnect_MapClientEventToSimEvent(id,...
std::string code
The calculator code string to execute as the event action. The code is pre-compiled and stored on the...
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.
Structure for using with WASimClient::getVariable() and WASimClient::setVariable() to specify informa...
int variableId
Numeric ID of the variable to get/set. Overrides the variableName field if greater than -1....
char variableType
A single character variable type identifier as documented in the MSFS SDK documentation (plus 'T' for...
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.
Data structure for sending Key Events to the sim with up to 5 event values. Events are specified usin...