26#include <condition_variable>
35#include <shared_mutex>
39#include <unordered_map>
41#define WIN32_LEAN_AND_MEAN
43#include <SimConnect.h>
45#define LOGFAULT_THREAD_NAME WSMCMND_CLIENT_NAME
49#include "SimConnectHelper.h"
52#include "MSFS_EventsEnum.h"
53#include "key_events.h"
59using namespace WASimCommander::Utilities;
60using namespace WASimCommander::Enums;
62using Clock = std::chrono::steady_clock;
64static const uint32_t CUSTOM_KEY_EVENT_LEGACY_TRIGGER_FLAG = 0x80000000;
67#pragma region DataRequestRecord struct
85#pragma region RegisteredEvent struct
95#pragma region WASimClient::Private class
98class WASimClient::Private
102 static const uint32_t DISPATCH_LOOP_WAIT_TIME = 5000;
103 static const uint32_t SIMCONNECT_DATA_ALLOC_LIMIT = 1024UL * 1024UL;
105 enum SimConnectIDs : uint8_t
107 SIMCONNECTID_INIT = 0,
131 time_t lastUpdate = 0;
133 mutable shared_mutex m_dataMutex;
134 vector<uint8_t> data;
136 explicit TrackedRequest(
const DataRequest &req, uint32_t dataId) :
139 dataSize{Utilities::getActualValueSize(
valueSize)},
143 TrackedRequest & operator=(
const DataRequest &req) {
145 unique_lock lock(m_dataMutex);
146 dataSize = Utilities::getActualValueSize(req.
valueSize);
147 data = vector<uint8_t>(dataSize, 0xFF);
149 DataRequest::operator=(req);
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;
160 friend inline std::ostream& operator<<(std::ostream& os,
const TrackedRequest &r) {
162 return os <<
" TrackedRequest{" <<
" dataId: " << r.dataId <<
"; lastUpdate: " << r.lastUpdate <<
"; dataSize: " << r.dataSize <<
"; data: " << Utilities::byteArrayToHex(r.data.data(), r.dataSize) <<
'}';
167 struct TrackedResponse
170 chrono::system_clock::time_point sent { chrono::system_clock::now() };
171 weak_ptr<condition_variable_any> cv {};
175 explicit TrackedResponse(uint32_t t, weak_ptr<condition_variable_any> cv) : token(t), cv(cv) {}
180 bool sentToServer =
false;
184 using responseMap_t = map<uint32_t, TrackedResponse>;
185 using requestMap_t = map<uint32_t, TrackedRequest>;
186 using eventMap_t = map<uint32_t, TrackedEvent>;
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 {};
198 shared_lock lock(mutex);
203 unique_lock lock(mutex);
204 listType = LookupItemType::None;
210 struct ProgramSettings {
211 filesystem::path logFilePath;
212 int networkConfigId = -1;
213 uint32_t networkTimeout = 1000;
216 { LogLevel::Info, LogLevel::Info, LogLevel::None },
217 { LogLevel::None, LogLevel::None, LogLevel::None }
220 LogLevel logLevel(
LogSource s,
LogFacility f)
const {
return f > LogFacility::None && f <= LogFacility::Remote ? logLevels[srcIndex(s)][facIndex(f)] : LogLevel::None; }
222 static int facIndex(
LogFacility f) {
return f == LogFacility::Remote ? 2 : f == LogFacility::File ? 1 : 0; }
225 friend class WASimClient;
226 WASimClient *
const q;
228 const uint32_t clientId;
229 const string clientName;
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;
242 HANDLE hSim =
nullptr;
243 HANDLE hSimEvent =
nullptr;
244 HANDLE hDispatchStopEvent =
nullptr;
245 thread dispatchThread;
254 mutable shared_mutex mtxResponses;
255 mutable shared_mutex mtxRequests;
256 mutable shared_mutex mtxEvents;
258 responseMap_t reponses {};
259 requestMap_t requests {};
260 eventMap_t events {};
264 unordered_map<std::string, uint32_t> keyEventNameCache {};
265 shared_mutex mtxKeyEventNames;
270 map<std::string, uint32_t> mappedVarsNameCache {};
271 shared_mutex mtxMappedVarNames;
272 uint32_t nextMappedVarID = 1;
275#pragma region Callback handling templates ----------------------------------------------
277#if defined(WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS) && !WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS
278 mutable mutex mtxCallbacks;
282 template<
typename... Args>
283 void invokeCallbackImpl(function<
void(Args...)> f, Args&&... args)
const
287#if defined(WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS) && !WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS
288 lock_guard lock(mtxCallbacks);
290 bind(f, forward<Args>(args)...)();
292 catch (exception *e) { LOG_ERR <<
"Exception in callback handler: " << e->what(); }
297 template<
typename... Args>
298 void invokeCallbackImpl(function<
void(
const Args...)> f,
const Args... args)
const
302#if defined(WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS) && !WSMCMND_CLIENT_USE_CONCURRENT_CALLBACKS
303 lock_guard lock(mtxCallbacks);
307 catch (exception *e) { LOG_ERR <<
"Exception in callback handler: " << e->what(); }
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)...);
318#pragma region Constructor and status ----------------------------------------------
321 static std::string makeClientName(uint32_t
id) {
322 std::ostringstream oss;
323 oss.imbue(std::locale::classic());
324 oss << STREAM_HEX8(
id);
328 Private(WASimClient *qptr, uint32_t clientId,
const std::string &config) :
331 clientName{makeClientName(clientId)}
334 filesystem::path cwd = filesystem::current_path(ec);
336 cerr << ec.message() << endl;
339 settings.logFilePath = cwd;
341 int requestTrackingMaxRecords = SimConnectHelper::ENABLE_SIMCONNECT_REQUEST_TRACKING ? 200 : 0;
344 const char *fn =
"client_conf.ini";
345 filesystem::path cfgFile(cwd /+ fn);
346 if (!config.empty()) {
348 filesystem::file_status fs = filesystem::status(config, ec);
349 if (!ec && filesystem::exists(fs)) {
351 if (filesystem::is_directory(fs))
355 cerr <<
"Config file/path '" << config <<
"' not found or not accessible. Using default config location " << cwd << endl;;
359 ifstream is(cfgFile);
361 inipp::Ini<char> ini;
363 for (
const auto &e : ini.errors)
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);
376 if ((idx = indexOfString(
LogLevelNames, fileLevel.c_str())) > -1)
378 if ((idx = indexOfString(
LogLevelNames, consoleLevel.c_str())) > -1)
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;
388 settings.logFilePath /= WSMCMND_CLIENT_NAME;
392 if ((SimConnectHelper::ENABLE_SIMCONNECT_REQUEST_TRACKING = (requestTrackingMaxRecords > 0)))
393 SimConnectHelper::setMaxTrackedRequests(requestTrackingMaxRecords);
396 bool checkInit()
const {
return hSim !=
nullptr; }
401 static const char * evTypeNames[] = {
403 "Simulator Connecting",
"Simulator Connected",
"Simulator Disconnecting",
"Simulator Disconnected",
404 "Server Connecting",
"Server Connected",
"Server Disconnected"
424 newStat = (newStat & ~ClientStatus
::Connected) | s;
435 if (newStat == status)
441 ev.message = Utilities::getEnumName(ev.eventType, evTypeNames);
444 invokeCallback(eventCb, move(ev));
449 bool waitCondition(
const std::function<
bool(
void)> predicate, uint32_t timeout, uint32_t sleepMs = 1)
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);
459 HRESULT waitConditionAsync(std::function<
bool(
void)> predicate, uint32_t timeout = 0)
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);
472#pragma region Local logging ----------------------------------------------
474 void setFileLogLevel(
LogLevel 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)
486 void setConsoleLogLevel(
LogLevel 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)
497 void setCallbackLogLevel(
LogLevel level)
500 if (logfault::LogManager::Instance().HaveHandler(clientName))
501 logfault::LogManager::Instance().SetLevel(clientName, logfault::LogLevel(level));
505 void onProxyLoggerMessage(
const logfault::Message &msg) {
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),
521 else if (!cb && logfault::LogManager::Instance().HaveHandler(clientName)) {
522 logfault::LogManager::Instance().RemoveHandler(clientName);
527#pragma region SimConnect and Server core utilities ----------------------------------------------
529 HRESULT connectSimulator(uint32_t timeout) {
530 return connectSimulator(settings.networkConfigId, timeout);
533 HRESULT connectSimulator(
int networkConfigId, uint32_t timeout)
539 LOG_INF << WSMCMND_CLIENT_NAME
" already initialized.";
542 settings.networkConfigId = networkConfigId;
543 LOG_INF <<
"Initializing " << WSMCMND_CLIENT_NAME <<
" v" WSMCMND_VERSION_INFO
" for client " << clientName <<
" with net config ID " << settings.networkConfigId;
547 timeout = settings.networkTimeout;
549 if (!hDispatchStopEvent) {
550 hDispatchStopEvent = CreateEvent(
nullptr,
true,
false,
nullptr);
551 if (!hDispatchStopEvent) {
552 LOG_ERR <<
"Failed to CreateEvent() for dispatch loop: " << GetLastError();
557 hSimEvent = CreateEvent(
nullptr,
false,
false,
nullptr);
558 simConnected =
false;
561 HRESULT hr = SimConnect_Open(&hSim, clientName.c_str(),
nullptr, 0, hSimEvent, settings.networkConfigId);
564 LOG_ERR <<
"Network connection to Simulator failed with result: " << LOG_HR(hr);
570 ResetEvent(hDispatchStopEvent);
571 dispatchThread = thread(&Private::dispatchLoop,
this);
572 dispatchThread.detach();
574 if (!waitCondition([&]() {
return !!runDispatchLoop; }, 10)) {
575 LOG_CRT <<
"Could not start dispatch loop thread.";
580 if (!waitCondition([&]() {
return !runDispatchLoop || simConnected; }, timeout)) {
581 LOG_ERR <<
"Network connection to Simulator timed out after " << timeout <<
"ms.";
587 if (!runDispatchLoop)
591 if FAILED(hr = SimConnectHelper::newClientEvent(hSim, CLI_EVENT_CONNECT, EVENT_NAME_CONNECT)) {
596 SimConnectHelper::newClientEvent(hSim, CLI_EVENT_PING, EVENT_NAME_PING);
598 SimConnectHelper::newClientEvent(hSim, CLI_EVENT_PING_RESP, EVENT_NAME_PING_PFX + clientName, NOTIFY_GROUP_PING, SIMCONNECT_GROUP_PRIORITY_HIGHEST);
601 if FAILED(hr = registerDataArea(CDA_NAME_CMD_PFX, CLI_DATA_COMMAND, CLI_DATA_COMMAND,
sizeof(Command),
true)) {
606 if FAILED(hr = registerDataArea(CDA_NAME_RESP_PFX, CLI_DATA_RESPONSE, CLI_DATA_RESPONSE,
sizeof(Command),
false)) {
611 registerDataArea(CDA_NAME_DATA_PFX, CLI_DATA_REQUEST, CLI_DATA_REQUEST,
sizeof(
DataRequest),
true);
613 registerDataArea(CDA_NAME_KEYEV_PFX, CLI_DATA_KEYEVENT, CLI_DATA_KEYEVENT,
sizeof(KeyEvent),
true);
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
625 registerAllDataRequestAreas();
627 mapAllCustomKeyEvents();
637 LOG_INF <<
"No network connection. Shutdown complete.";
642 LOG_DBG <<
"Shutting down...";
646 if (!!hDispatchStopEvent) {
647 SetEvent(hDispatchStopEvent);
651 if (!waitCondition([&]() {
return !runDispatchLoop; }, DISPATCH_LOOP_WAIT_TIME + 100))
652 LOG_CRT <<
"Dispatch loop thread still running!";
656 simConnected =
false;
657 logCDAcreated =
false;
661 if (hSim && !onQuitEvent)
662 SimConnect_Close(hSim);
665 CloseHandle(hSimEvent);
670 unique_lock lock { mtxMappedVarNames };
671 mappedVarsNameCache.clear();
676 LOG_INF <<
"Shutdown complete, network disconnected.";
679 HRESULT connectServer(uint32_t timeout)
685 timeout = settings.networkTimeout;
688 if (!checkInit() && FAILED(hr = connectSimulator(timeout)))
691 LOG_INF <<
"Connecting to " WSMCMND_PROJECT_NAME
" server...";
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)) {
702 if (!waitCondition([&]() {
return !runDispatchLoop || serverConnected; }, timeout)) {
703 LOG_ERR <<
"Server connection timed out or refused after " << timeout <<
"ms.";
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);
716 updateServerLogLevel();
718 sendServerCommand(Command(CommandId::Subscribe, (requestsPaused ? 0 : 1)));
720 registerAllDataRequests();
734 sendServerCommand(Command(CommandId::Disconnect));
737 unique_lock lock(mtxResponses);
740 serverConnected =
false;
741 LOG_INF <<
"Disconnected from " WSMCMND_PROJECT_NAME
" server.";
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)
751 const string cdaName(nameIsFull ? name : name + clientName);
752 if FAILED(hr = SimConnectHelper::registerDataArea(hSim, cdaName, cdaID, cddId, szOrType,
true, readonly, deltaE))
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;
761#pragma region Command sending ----------------------------------------------
764 HRESULT sendServerCommand(
const Command &command)
const
766 if (!isConnected()) {
767 LOG_ERR <<
"Server not connected, cannot send " << command;
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
778 HRESULT sendServerCommand(Command &&command, weak_ptr<condition_variable_any> cv, uint32_t *newToken =
nullptr)
783 command.token = nextCmdToken++;
784 enqueueTrackedResponse(command.token, cv);
785 const HRESULT hr = sendServerCommand(command);
787 unique_lock vlock(mtxResponses);
788 reponses.erase(command.token);
791 *newToken = command.token;
796 HRESULT sendCommandWithResponse(Command &&command, Command *response, uint32_t timeout = 0)
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);
803 if FAILED(hr = waitCommandResponse(token, response, timeout))
804 LOG_ERR <<
"Command " << Utilities::getEnumName(origId,
CommandIdNames) <<
" timed out after " << (timeout ? timeout : settings.networkTimeout) <<
"ms.";
810#pragma region Command Response tracking ----------------------------------------------
812 TrackedResponse *findTrackedResponse(uint32_t token)
814 shared_lock lock{mtxResponses};
815 const responseMap_t::iterator pos = reponses.find(token);
816 if (pos != reponses.cend())
821 TrackedResponse *enqueueTrackedResponse(uint32_t token, weak_ptr<condition_variable_any> cv)
823 unique_lock lock{mtxResponses};
824 return &reponses.try_emplace(token, token, cv).first->second;
828 HRESULT waitCommandResponse(uint32_t token, Command *response, uint32_t timeout = 0, std::function<
bool(
void)> extraPredicate =
nullptr)
830 TrackedResponse *tr = findTrackedResponse(token);
831 shared_ptr<condition_variable_any> cv {};
832 if (!tr || !(cv = tr->cv.lock()))
836 timeout = settings.networkTimeout;
838 bool stopped =
false;
839 auto stop_waiting = [=, &stopped]() {
841 !serverConnected || (tr->response.commandId != CommandId::None && tr->response.token == token && (!extraPredicate || extraPredicate()));
845 if (stop_waiting()) {
849 unique_lock lock(tr->mutex);
851 if (cv->wait_for(lock, chrono::milliseconds(timeout), stop_waiting))
854 else if (!!extraPredicate) {
855 cv->wait(lock, stop_waiting);
856 hr = stopped ? ERROR_ABANDONED_WAIT_0 : S_OK;
860 LOG_DBG <<
"waitCommandResponse() requires a predicate condition when timeout parameter is < 0.";
863 if (SUCCEEDED(hr) && !!response) {
864 unique_lock lock(tr->mutex);
865 *response = move(tr->response);
868 unique_lock vlock(mtxResponses);
869 reponses.erase(token);
875 HRESULT waitCommandResponseAsync(uint32_t token, Command *response, uint32_t timeout = 0, std::function<
bool(
void)> extraPredicate =
nullptr)
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();
888 void waitListRequestEnd()
890 auto stop_waiting = [
this]() {
891 return listResult.nextTimeout.load() >= Clock::now();
895 HRESULT hr = waitCommandResponse(listResult.token, &response, -1, stop_waiting);
896 if (hr == ERROR_ABANDONED_WAIT_0) {
897 LOG_ERR <<
"List request timed out.";
900 else if (hr != S_OK) {
901 LOG_ERR <<
"List request failed with result: " << LOG_HR(hr);
903 else if (response.
commandId != CommandId::Ack) {
904 LOG_WRN <<
"Server returned Nak for list request of " << Utilities::getEnumName(listResult.listType.load(),
LookupItemTypeNames);
908 invokeCallback(listCb, ListResult { listResult.listType.load(), hr, listResult.getResult() });
913#pragma region Server-side logging commands ----------------------------------------------
916 if (fac == LogFacility::None || fac > LogFacility::All)
918 if (+fac & +LogFacility::Remote)
920 if (+fac & +LogFacility::Console)
922 if (+fac & +LogFacility::File)
925 updateServerLogLevel(fac);
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));
933 HRESULT registerLogDataArea() {
934 if (logCDAcreated || settings.logLevel(
LogSource::Server, LogFacility::Remote) == LogLevel::None)
938 if SUCCEEDED(hr = registerDataArea(CDA_NAME_LOG_PFX, CLI_DATA_LOG, CLI_DATA_LOG,
sizeof(LogRecord),
false)) {
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);
943 logCDAcreated = SUCCEEDED(hr);
948#pragma region Remote Variables accessors ----------------------------------------------
951 const string buildVariableCommandString(
const VariableRequest &v,
bool isLocal)
const
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);
958 if (v.variableType ==
'A' && v.simVarIndex)
959 sValue +=
':' + to_string(v.simVarIndex);
961 if (isIndexed && v.unitId > -1)
962 sValue +=
',' + to_string(v.unitId);
963 else if (!v.unitName.empty())
964 sValue +=
',' + v.unitName;
969 HRESULT getVariable(
const VariableRequest &v,
double *result, std::string *sResult =
nullptr,
double dflt = 0.0)
971 const string sValue = buildVariableCommandString(v,
false);
972 if (sValue.empty() || sValue.length() >=
STRSZ_CMD)
977 if FAILED(hr = sendCommandWithResponse(Command(v.createLVar && v.variableType ==
'L' ? CommandId::GetCreate : CommandId::Get, v.variableType, sValue.c_str(), dflt), &response))
979 if (response.
commandId != CommandId::Ack) {
980 LOG_WRN <<
"Get Variable request for " << quoted(sValue) <<
" returned Nak response. Reason, if any: " << quoted(response.
sData);
984 *result = response.
fData;
986 *sResult = response.
sData;
990 HRESULT setLocalVariable(
const VariableRequest &v,
const double value)
992 const string sValue = buildVariableCommandString(v,
true);
993 if (sValue.empty() || sValue.length() >=
STRSZ_CMD)
995 return sendServerCommand(Command(v.createLVar ? CommandId::SetCreate : CommandId::Set,
'L', sValue.c_str(), value));
998 HRESULT setVariable(
const VariableRequest &v,
const double value)
1000 if (v.variableType ==
'L')
1001 return setLocalVariable(v, value);
1003 if (!Utilities::isSettableVariableType(v.variableType)) {
1004 LOG_WRN <<
"Cannot Set a variable of type '" << v.variableType <<
"'.";
1005 return E_INVALIDARG;
1008 if (v.variableName.empty()) {
1009 LOG_WRN <<
"A variable name is required to set variable of type '" << v.variableType <<
"'.";
1010 return E_INVALIDARG;
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;
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;
1028 if (codeStr.tellp() == -1 || (
size_t)codeStr.tellp() >=
STRSZ_CMD)
1029 return E_INVALIDARG;
1032 return sendServerCommand(Command(CommandId::Exec, +CalcResultType::None, codeStr.str().c_str()));
1035 HRESULT setStringVariable(
const VariableRequest &v,
const std::string &value)
1038 LOG_ERR <<
"Simulator not connected, cannot set string variable.";
1041 if (v.variableType !=
'A') {
1042 LOG_WRN <<
"Cannot Set a string value on variable of type '" << v.variableType <<
"'.";
1043 return E_INVALIDARG;
1045 if (v.variableName.empty()) {
1046 LOG_WRN <<
"A variable name is required to set variable of type '" << v.variableType <<
"'.";
1047 return E_INVALIDARG;
1053 shared_lock lock { mtxMappedVarNames };
1054 const auto pos = mappedVarsNameCache.find(v.variableName);
1055 if (pos != mappedVarsNameCache.cend())
1056 mapId = pos->second;
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))
1063 mapId = nextMappedVarID++;
1064 unique_lock lock { mtxMappedVarNames };
1065 mappedVarsNameCache.insert(std::pair{ v.variableName, mapId });
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());
1072#pragma region Data Requests ----------------------------------------------
1074 TrackedRequest *findRequest(uint32_t
id)
1076 shared_lock lock{mtxRequests};
1077 const requestMap_t::iterator pos = requests.find(
id);
1078 if (pos != requests.cend())
1079 return &pos->second;
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;
1094 if (!isConnected()) {
1095 LOG_ERR <<
"Server not connected, cannot submit DataRequest " << req;
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);
1104 HRESULT sendDataRequest(
const DataRequest &req,
bool async)
1107 if FAILED(hr = writeDataRequest(req))
1111 if (async || req.
requestType == RequestType::None)
1114 shared_ptr<condition_variable_any> cv = make_shared<condition_variable_any>();
1115 enqueueTrackedResponse(req.
requestId, weak_ptr(cv));
1117 if FAILED(hr = waitCommandResponse(req.
requestId, &response)) {
1118 LOG_ERR <<
"Data Request timed out after " << settings.networkTimeout <<
"ms.";
1120 else if (response.
commandId != CommandId::Ack) {
1122 LOG_ERR <<
"Data Request returned Nak response for DataRequest ID " << req.
requestId <<
". Reason, if any: " << quoted(response.
sData);
1127 HRESULT registerDataRequestArea(
const TrackedRequest *
const tr,
bool isNewRequest,
bool dataAllocChanged =
false)
1130 shared_lock lock{mtxRequests};
1131 if (isNewRequest || dataAllocChanged) {
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)))
1137 else if (dataAllocChanged) {
1139 deregisterDataRequestArea(tr);
1141 if FAILED(hr = SimConnectHelper::addClientDataDefinition(hSim, tr->dataId, tr->valueSize, max(tr->deltaEpsilon, 0.0f)))
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
1154 HRESULT deregisterDataRequestArea(const TrackedRequest * const tr)
const
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
1163 return SimConnectHelper::removeClientDataDefinition(hSim, tr->dataId);
1166 HRESULT addOrUpdateRequest(
const DataRequest &req,
bool async)
1173 LOG_ERR <<
"Error in DataRequest ID: " << req.
requestId <<
"; Parameter 'nameOrCode' cannot be empty.";
1174 return E_INVALIDARG;
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;
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;
1187 TrackedRequest *tr = findRequest(req.
requestId);
1188 const bool isNewRequest = (tr ==
nullptr);
1189 bool dataAllocationChanged =
false;
1192 unique_lock lock{mtxRequests};
1193 tr = &requests.try_emplace(req.
requestId, req, nextDefId++).first->second;
1196 if (actualValSize > tr->dataSize) {
1197 LOG_ERR <<
"Value size cannot be increased after request is created.";
1198 return E_INVALIDARG;
1201 dataAllocationChanged = (actualValSize != tr->dataSize || !fuzzyCompare(req.
deltaEpsilon, tr->deltaEpsilon));
1203 unique_lock lock{mtxRequests};
1208 if (isConnected()) {
1210 if (isNewRequest || dataAllocationChanged)
1211 hr = registerDataRequestArea(tr, isNewRequest, dataAllocationChanged);
1214 hr = sendDataRequest(req, async);
1216 if (FAILED(hr) && isNewRequest) {
1220 LOG_TRC << (FAILED(hr) ?
"FAILED" : isNewRequest ?
"Added" :
"Updated") <<
" request: " << req;
1223 LOG_TRC <<
"Queued " << (isNewRequest ?
"New" :
"Updated") <<
" request: " << req;
1229 HRESULT removeRequest(
const uint32_t requestId)
1231 TrackedRequest *tr = findRequest(requestId);
1233 LOG_WRN <<
"DataRequest ID " << requestId <<
" not found.";
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.";
1244 requests.erase(requestId);
1245 LOG_TRC <<
"Removed Data Request " << requestId;
1251 void registerAllDataRequests()
1255 shared_lock lock(mtxRequests);
1256 for (
const auto & [_, tr] : requests) {
1257 if (tr.requestType != RequestType::None)
1258 writeDataRequest(tr);
1264 void registerAllDataRequestAreas()
1268 for (
const auto & [_, tr] : requests)
1269 registerDataRequestArea(&tr,
true);
1272#pragma endregion Data Requests
1274#pragma region Calculator Events --------------------------------------------
1276 TrackedEvent *findTrackedEvent(uint32_t eventId)
1278 shared_lock lock{mtxEvents};
1279 const eventMap_t::iterator pos = events.find(eventId);
1280 if (pos != events.cend())
1281 return &pos->second;
1286 HRESULT sendEventRegistration(TrackedEvent *ev,
bool resendName =
false)
1289 return E_INVALIDARG;
1291 if (ev->code.empty()) {
1292 hr = sendServerCommand(Command(CommandId::Register, ev->eventId));
1294 unique_lock lock(mtxEvents);
1295 events.erase(ev->eventId);
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()));
1304 HRESULT sendEventRegistration(uint32_t eventId) {
1305 return sendEventRegistration(findTrackedEvent(eventId));
1309 void registerAllEvents()
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);
1317 sendEventRegistration(&ev,
true);
1320 for (
const uint32_t &
id : removals)
1321 sendEventRegistration(
id);
1326#pragma region Simulator Key Events -------------------------------------------
1329 HRESULT writeKeyEvent(
const KeyEvent &kev)
const
1331 if (!isConnected()) {
1332 LOG_ERR <<
"Server not connected, cannot send " << kev;
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);
1342 HRESULT registerCustomKeyEvent(
const std::string &name, uint32_t *pRetId,
bool useLegacy)
1345 unique_lock lock(mtxKeyEventNames);
1347 const auto pos = keyEventNameCache.find(name);
1348 if (pos != keyEventNameCache.cend()) {
1351 *pRetId = pos->second;
1356 uint32_t keyId = nextCustomEventID++;
1359 keyId |= CUSTOM_KEY_EVENT_LEGACY_TRIGGER_FLAG;
1361 keyEventNameCache.insert(std::pair{ name, keyId });
1368 return mapCustomKeyEvent(keyId, name);
1369 LOG_DBG <<
"Queued registration of custom simulator key event " << quoted(name) <<
" with ID " << keyId <<
".";
1374 HRESULT mapCustomKeyEvent(uint32_t
id,
const std::string &name)
const
1376 const HRESULT hr = SimConnectHelper::newClientEvent(hSim,
id, name);
1378 LOG_DBG <<
"Registered custom simulator key event " << quoted(name) <<
" with id " <<
id <<
".";
1380 LOG_ERR <<
"Registration custom simulator key event " << quoted(name) <<
" for id " <<
id <<
" failed with HRESULT " << LOG_HR(hr);
1385 void mapAllCustomKeyEvents()
1387 shared_lock lock(mtxKeyEventNames);
1388 for (
const auto& [name,
id] : keyEventNameCache) {
1390 mapCustomKeyEvent(
id, name);
1395 HRESULT sendSimCustomKeyEvent(uint32_t keyEventId, DWORD v1, DWORD v2, DWORD v3, DWORD v4, DWORD v5)
const
1397 if (!simConnected) {
1398 LOG_ERR <<
"Simulator not connected, cannot send a Custom Key Event.";
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);
1410#pragma region SimConnect message processing ----------------------------------------------
1412 static void CALLBACK dispatchMessage(SIMCONNECT_RECV *pData, DWORD cbData,
void *pContext) {
1413 static_cast<Private*
>(pContext)->onSimConnectMessage(pData, cbData);
1418 LOG_DBG <<
"Dispatch loop started.";
1419 const HANDLE waitEvents[] = { hDispatchStopEvent, hSimEvent };
1421 runDispatchLoop =
true;
1422 while (runDispatchLoop) {
1423 hr = WaitForMultipleObjects(2, waitEvents,
false, DISPATCH_LOOP_WAIT_TIME);
1427 case WAIT_OBJECT_0 + 1:
1428 SimConnect_CallDispatch(hSim, Private::dispatchMessage,
this);
1433 LOG_ERR <<
"Dispatch loop WaitForMultipleObjects returned: " << LOG_HR(hr) <<
" Error: " << LOG_HR(GetLastError());
1436 runDispatchLoop =
false;
1438 LOG_DBG <<
"Dispatch loop stopped.";
1441 void onSimConnectMessage(SIMCONNECT_RECV *pData, DWORD cbData)
1443 LOG_TRC << LOG_SC_RECV(pData);
1444 switch (pData->dwID) {
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);
1450 const size_t dataSize = (size_t)pData->dwSize + 4 -
sizeof(SIMCONNECT_RECV_CLIENT_DATA);
1451 switch (data->dwRequestID)
1453 case DATA_REQ_RESPONSE: {
1455 if (dataSize !=
sizeof(Command)) {
1456 LOG_CRT <<
"Invalid Command struct data size! Expected " <<
sizeof(Command) <<
" but got " << dataSize;
1460 const Command *
const cmd =
reinterpret_cast<const Command *const
>(&data->dwData);
1461 LOG_DBG <<
"Got Command: " << *cmd;
1462 bool checkTracking =
true;
1465 case CommandId::Ack:
1466 case CommandId::Nak: {
1470 case CommandId::Connect:
1471 serverConnected = cmd->
commandId == CommandId::Ack && cmd->
token == clientId;
1472 serverVersion = (uint32_t)cmd->
fData;
1473 checkTracking =
false;
1479 invokeCallback(cmdResultCb, *cmd);
1484 case CommandId::Ping:
1485 sendServerCommand(Command(CommandId::Ack, (uint32_t)CommandId::Ping));
1486 checkTracking =
false;
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);
1497 LOG_WRN <<
"Received unexpected list result for wrong command token. Expected " << listResult.token <<
" got " << cmd->
token;
1499 checkTracking =
false;
1504 case CommandId::Disconnect:
1506 checkTracking =
false;
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.";
1519 unique_lock lock(tr->mutex);
1520 tr->response = *cmd;
1525 unique_lock lock(mtxResponses);
1526 reponses.erase(cmd->
token);
1527 LOG_TRC <<
"Got tracked command response but no CV, removing record.";
1532 invokeCallback(respCb, *cmd);
1536 case DATA_REQ_LOG: {
1538 if (dataSize !=
sizeof(LogRecord)) {
1539 LOG_CRT <<
"Invalid LogRecord struct data size! Expected " <<
sizeof(LogRecord) <<
" but got " << dataSize;
1543 const LogRecord *
const log =
reinterpret_cast<const LogRecord *const
>(&data->dwData);
1544 LOG_TRC <<
"Got Log Record: " << *log;
1551 if (data->dwRequestID >= SIMCONNECTID_LAST) {
1552 TrackedRequest *tr = findRequest(data->dwRequestID - SIMCONNECTID_LAST);
1554 LOG_WRN <<
"DataRequest ID " << data->dwRequestID - SIMCONNECTID_LAST <<
" not found in tracked requests.";
1558 if (dataSize < tr->dataSize) {
1559 LOG_CRT <<
"Invalid data result size! Expected " << tr->dataSize <<
" but got " << dataSize;
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();
1566 shared_lock rdlock(mtxRequests);
1567 LOG_TRC <<
"Got data result for request: " << *tr;
1568 invokeCallback(dataCb, tr->toRequestRecord());
1571 LOG_WRN <<
"Got unknown RequestID in SIMCONNECT_RECV_CLIENT_DATA struct: " << data->dwRequestID;
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);
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);
1596 case SIMCONNECT_RECV_ID_OPEN:
1597 simConnected =
true;
1598 SimConnectHelper::logSimVersion(pData);
1601 case SIMCONNECT_RECV_ID_QUIT: {
1602 LOG_INF <<
"Received quit command from SimConnect, disconnecting...";
1608 case SIMCONNECT_RECV_ID_EXCEPTION:
1609 SimConnectHelper::logSimConnectException(pData);
1617#pragma endregion SimConnect
1620#pragma endregion WASimClient::Private
1624#pragma region WASimClient class
1628#define d_const const_cast<const Private *>(d.get())
1631 d(new Private(this, clientId, configFile))
1637 if (d->hDispatchStopEvent)
1638 CloseHandle(d->hDispatchStopEvent);
1641#pragma region Connections ----------------------------------------------
1646 LOG_ERR <<
"Client ID must be greater than zero.";
1647 return E_INVALIDARG;
1649 return d->connectSimulator(timeout);
1653 return d->connectSimulator(networkConfigId, timeout);
1657 d->disconnectSimulator(
false);
1661 return d->connectServer(timeout);
1665 d->disconnectServer(
true);
1670 if (!d->checkInit() && FAILED(d->connectSimulator(timeout)))
1674 const Clock::time_point refTp = d->serverLastSeen;
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
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;
1688 LOG_WRN <<
"Server did not respond to ping after " << timeout <<
"ms";
1694#pragma region Exec calc ----------------------------------------------
1699 LOG_ERR <<
"Code string length " << code.length() <<
" is greater then maximum size of " <<
STRSZ_CMD-1;
1700 return E_INVALIDARG;
1708 if FAILED(hr = d->sendCommandWithResponse(
Command(
CommandId::Exec, +resultType, code.c_str()), &response))
1711 LOG_WRN <<
"Calculator Exec with code " << quoted(code) <<
" returned Nak response. Reason, if any: " << quoted(response.
sData);
1715 *fResult = response.
fData;
1717 *sResult = response.
sData;
1723#pragma region Variable accessors ----------------------------------------------
1728 LOG_ERR <<
"Cannot get variable type '" << variable.
variableType <<
"' by index.";
1729 return E_INVALIDARG;
1731 return d->getVariable(variable, pfResult, psResult);
1735 return d->getVariable(
VariableRequest(variableName,
false, unitName), pfResult);
1739 return d->getVariable(
VariableRequest(variableName,
true, unitName), pfResult,
nullptr, defaultValue);
1744 return d->setVariable(variable, value);
1748 return d->setStringVariable(variable, value);
1752 return d->setLocalVariable(
VariableRequest(variableName,
false, unitName), value);
1756 return d->setLocalVariable(
VariableRequest(variableName,
true, unitName), value);
1761#pragma region Data Requests ----------------------------------------------
1764 return d->addOrUpdateRequest(request, async);
1768 return d->removeRequest(requestId);
1773 if (d->requests.count(requestId))
1782 const Private::TrackedRequest *r = d_const->findRequest(requestId);
1784 return r->toRequestRecord();
1785 LOG_ERR <<
"Data Request ID " << requestId <<
" not found.";
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) {
1796 ret.emplace_back(value.toRequestRecord());
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) {
1817 d->requestsPaused = paused;
1820 d->requestsPaused = paused;
1824#pragma endregion Data
1826#pragma region Calculator Events ----------------------------------------------
1830 if (eventData.
code.empty())
1833 const size_t len = eventData.
code.length() + (eventData.
name.empty() ? 0 : eventData.
name.length() + 1);
1835 LOG_ERR <<
"Resulting command string length " << len <<
" is greater then maximum size of " <<
STRSZ_CMD-1 <<
" bytes.";
1836 return E_INVALIDARG;
1839 Private::TrackedEvent *ev = d->findTrackedEvent(eventData.
eventId);
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;
1846 LOG_INF <<
"No changes detected in code for event ID " << eventData.
eventId <<
", skipping update";
1852 unique_lock lock(d->mtxEvents);
1853 ev = &d->events.emplace(eventData.
eventId, Private::TrackedEvent(eventData.
eventId, eventData.
code, eventData.
name)).first->second;
1856 ev->sentToServer = SUCCEEDED(d->sendEventRegistration(ev)) || ev->sentToServer;
1858 LOG_DBG <<
"Queued event ID " << eventData.
eventId <<
" for next server connection.";
1865 Private::TrackedEvent *ev = d->findTrackedEvent(eventId);
1867 LOG_ERR <<
"Event ID " << eventId <<
" not found.";
1868 return E_INVALIDARG;
1871 if (!ev->sentToServer) {
1872 unique_lock lock(d->mtxEvents);
1873 d->events.erase(eventId);
1877 d->sendEventRegistration(ev);
1879 LOG_DBG <<
"Queued event ID " << eventId <<
" for deletion on next server connection.";
1890 if (Private::TrackedEvent *ev = d->findTrackedEvent(eventId))
1892 LOG_ERR <<
"Event ID " << eventId <<
" not found.";
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())
1908#pragma endregion Calculator Events
1910#pragma region Key Events ----------------------------------------------
1915 return d_const->writeKeyEvent(
KeyEvent(keyEventId, { v1, v2, v3, v4, v5 }, d->nextCmdToken++));
1917 return d_const->sendSimCustomKeyEvent(keyEventId, v1, v2, v3, v4, v5);
1920inline static bool isCustomKeyEventName(
const std::string &name) {
1921 return name[0] ==
'#' || name.find(
'.') != string::npos;
1926 if (keyEventName.empty())
1927 return E_INVALIDARG;
1931 shared_lock rdlock(d->mtxKeyEventNames);
1932 const auto pos = d_const->keyEventNameCache.find(keyEventName);
1933 if (pos != d_const->keyEventNameCache.cend())
1940 if (isCustomKeyEventName(keyEventName)) {
1942 const HRESULT hr = d->registerCustomKeyEvent(keyEventName, &keyId,
false);
1951 return hr == E_FAIL ? E_INVALIDARG : hr;
1952 keyId = (uint32_t)
id;
1954 unique_lock rwlock(d->mtxKeyEventNames);
1955 d->keyEventNameCache.insert(std::pair{ keyEventName, keyId });
1965 if (isCustomKeyEventName(customEventName))
1966 return d->registerCustomKeyEvent(customEventName, puiCustomEventId, useLegacyTransmit);
1968 LOG_ERR <<
"Custom event name " << quoted(customEventName) <<
" must start with '#' or include a period.";
1969 return E_INVALIDARG;
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);
1984 unique_lock lock(d->mtxKeyEventNames);
1985 return d->keyEventNameCache.erase(customEventName) > 0 ? S_OK : E_INVALIDARG;
1988#pragma endregion Key Events
1990#pragma region Meta Data ----------------------------------------------
1996 if (!validListLookups.count(itemsType)) {
1997 LOG_ERR <<
"Cannot list item of type " << Utilities::getEnumName(itemsType,
LookupItemTypeNames) <<
".";
1998 return E_INVALIDARG;
2002 LOG_ERR <<
"A List request for " << Utilities::getEnumName(d->listResult.listType.load(),
LookupItemTypeNames) <<
" is currently pending.";
2003 return E_INVALIDARG;
2005 unique_lock lock(d->listResult.mutex);
2006 d->listResult.listType = itemsType;
2007 d->listResult.nextTimeout = Clock::now() + chrono::milliseconds(
defaultTimeout());
2013 thread(&Private::waitListRequestEnd, d.get()).detach();
2015 catch (exception *e) {
2016 LOG_ERR <<
"Exception trying to schedule thread for List command: " << e->what();
2026 const int32_t keyId = Utilities::getKeyEventId(itemName);
2028 LOG_DBG <<
"Key Event named " << quoted(itemName) <<
" was not found in local lookup table.";
2029 else if (!!piResult)
2031 return keyId < 0 ? E_FAIL : S_OK;
2035 LOG_ERR <<
"Item name length " << itemName.length() <<
" is greater then maximum size of " <<
STRSZ_CMD-1 <<
" bytes.";
2036 return E_INVALIDARG;
2043 LOG_WRN <<
"Lookup command returned Nak response. Reason, if any: " << quoted(response.
sData);
2046 LOG_DBG <<
"Lookup: Server returned ID " << (int)response.
fData <<
" and echo name " << quoted(response.
sData);
2048 *piResult = (int)response.
fData;
2052#pragma endregion Meta
2054#pragma region Low Level ----------------------------------------------
2057 return d->sendServerCommand(command);
2061 return d->sendCommandWithResponse(
Command(command), response, timeout);
2064#pragma endregion Low Level
2066#pragma region Status / Network / Logging / Callbacks ----------------------------------------------
2086 d->setConsoleLogLevel(level);
2088 d->setFileLogLevel(level);
2090 d->setCallbackLogLevel(level);
2093 d->setServerLogLevel(level, facility);
2104#pragma endregion Misc
2106#pragma endregion WASimClient
2109#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 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...
static const HRESULT E_TIMEOUT
Error result: timeout communicating with server.
std::function< void __stdcall(const ListResult &)> listResultsCallback_t
Callback function for delivering list results, eg. of local variables sent from Server.
LogSource
Log entry source, Client or Server.
@ Client
Log record from WASimClient.
@ Server
Log record from WASimModule (Server)
std::function< void __stdcall(const ClientEvent &)> clientEventCallback_t
Callback function for Client events.
std::function< void __stdcall(const DataRequestRecord &)> dataCallback_t
Callback function for subscription result data.
std::function< void __stdcall(const Command &)> commandCallback_t
Callback function for commands sent from server.
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 LogRecord &, LogSource)> logCallback_t
Callback function for log entries (from both Client and Server).
ClientEventType
Client event type enumeration.
@ SimDisconnecting
ShuttingDown status.
@ ServerDisconnected
From Connected to SimConnected status change.
@ SimConnecting
Initializing status.
@ ServerConnecting
Connecting status.
@ ServerConnected
Connected status.
@ SimDisconnected
From SimConnected to Initializing status change.
std::vector< std::pair< int, std::string > > listResult_t
A mapping of IDs to names.
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
static const std::vector< const char * > LogLevelNames
Enums::LogLevel enum names.
static const std::vector< const char * > CommandIdNames
Enums::CommandId enum names.
LookupItemType
Types of things to look up or list.
@ DataRequest
Saved value subscription for current Client, indexed by requestId and nameOrCode values....
@ None
Null type, possible internal use, ignored by server.
@ LocalVariable
LVar ('L') names and IDs. Available for List and Lookup commands.
@ RegisteredEvent
Saved calculator event for current Client, indexed by eventId and code values. Available for List and...
@ KeyEventId
Key Event ID, value of KEY_* macros from "guauges.h" header. Available for Lookup command only (use e...
static const std::vector< const char * > LookupItemTypeNames
Enums::LookupItemType enum names.
CalcResultType
The type of result that calculator code is expected to produce.
@ None
No result is expected (eg. triggering an event).
CommandId
Commands for client-server interaction. Both sides can send commands via dedicated channels by writin...
@ Update
Trigger data update of a previously-added Data Request, with the request ID in uData....
@ List
Request a listing of items like local variables. uData should be one of WASimCommander::LookupItemTyp...
@ Transmit
Trigger an event previously registered with the Register command. uData should be the event ID from t...
@ Exec
Run calculator code contained in sData with WASimCommander::CalcResultType in uData....
@ Ack
Last command acknowledge. CommandId of the original command (which succeeded) is sent in uData....
@ Subscribe
Sending this command to the server with a uData value of 0 (zero) will suspend (pause) any and all da...
@ Lookup
Get information about an item, such as the ID of a variable or unit name. uData should be one of WASi...
RequestType
Types of things to request or set.
@ None
Use to remove a previously-added request.
LogFacility
Logging destination type.
@ File
Log file destination.
@ Console
Console logging, eg. stderr/stdout.
@ Remote
Remote destination, eg. network transmission or a callback event.
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.
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.
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.
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...