├── LICENSE ├── README.md ├── doc ├── mobiflightVarAccess.drawio └── mobiflightVarAccess.png ├── prototype ├── example.py ├── mobiflight_variable_requests.py └── simconnect_mobiflight.py └── src ├── example.py ├── mobiflight_variable_requests.py └── simconnect_mobiflight.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Koseng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MSFS Python SimConnect MobiFlight Extension 2 | Extends the [python simconnect library](https://github.com/odwdinc/Python-SimConnect) for MSFS2020 with the capability to read A and L variables with help of the [MobiFlight](https://www.mobiflight.com) WASM module. 3 | 4 | To just execute Mobiflight SimConnect Events this extension is NOT necessary. This can already be done with the Python-SimConnect library. 5 | 6 | * ```SimConnectMobiFlight``` inherits from ```SimConnect```, fixes an issue and adds the feature to register additional client data handlers. 7 | * ```MobiFlightVariableRequests``` implements the variable access via the MobiFlight WASM module. 8 | * A collection of possible variables can be found [here](https://github.com/Mobiflight/MobiFlight-Connector/blob/main/Presets/msfs2020_simvars.cip) in the [MobiFlight repository](https://github.com/Mobiflight/MobiFlight-Connector). 9 | * ATTENTION: At the moment it is only possible for one client at a time to access variables via MobiFlight WASM. There is not yet multi client support. 10 | 11 | Instead of the SimConnect class you need to use the SimConnectMobiFlight class in your application. 12 | 13 | ## Example 14 | 15 | **Example code on how to use the library and read variables:** 16 | ```python 17 | from simconnect_mobiflight import SimConnectMobiFlight 18 | from mobiflight_variable_requests import MobiFlightVariableRequests 19 | from time import sleep 20 | 21 | sm = SimConnectMobiFlight() 22 | vr = MobiFlightVariableRequests(sm) 23 | vr.clear_sim_variables() 24 | 25 | # Example write variable 26 | vr.set("0 (>L:A32NX_COCKPIT_DOOR_LOCKED)") 27 | 28 | while True: 29 | alt_ground = vr.get("(A:GROUND ALTITUDE,Meters)") 30 | alt_plane = vr.get("(A:PLANE ALTITUDE,Feet)") 31 | # FlyByWire A320 32 | ap1 = vr.get("(L:A32NX_AUTOPILOT_1_ACTIVE)") 33 | hdg = vr.get("(L:A32NX_AUTOPILOT_HEADING_SELECTED)") 34 | mode = vr.get("(L:A32NX_FMA_LATERAL_MODE)") 35 | sleep(1) 36 | ``` 37 | 38 | ## MobiFlight WASM working principle 39 | 40 | ![Picture principle](doc/mobiflightVarAccess.png) 41 | -------------------------------------------------------------------------------- /doc/mobiflightVarAccess.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /doc/mobiflightVarAccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koseng/MSFSPythonSimConnectMobiFlightExtension/d1a2f4f2acc1a24771e9e95248918d36e163289d/doc/mobiflightVarAccess.png -------------------------------------------------------------------------------- /prototype/example.py: -------------------------------------------------------------------------------- 1 | import logging, logging.handlers 2 | from simconnect_mobiflight import SimConnectMobiFlight 3 | from mobiflight_variable_requests import MobiFlightVariableRequests 4 | from time import sleep 5 | 6 | def setupLogging(logFileName): 7 | logFormatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s") 8 | rootLogger = logging.getLogger() 9 | rootLogger.setLevel(logging.DEBUG) 10 | fileHandler = logging.handlers.RotatingFileHandler(logFileName, maxBytes=500000, backupCount=7) 11 | fileHandler.setFormatter(logFormatter) 12 | rootLogger.addHandler(fileHandler) 13 | consoleHandler = logging.StreamHandler() 14 | consoleHandler.setFormatter(logFormatter) 15 | rootLogger.addHandler(consoleHandler) 16 | 17 | # MAIN 18 | setupLogging("SimConnectMobiFlight.log") 19 | sm = SimConnectMobiFlight() 20 | vr = MobiFlightVariableRequests(sm) 21 | vr.clear_sim_variables() 22 | 23 | 24 | while True: 25 | alt_ground = vr.get("(A:GROUND ALTITUDE,Meters)") 26 | alt_plane = vr.get("(A:PLANE ALTITUDE,Feet)") 27 | # FlyByWire A320 28 | ap1 = vr.get("(L:A32NX_AUTOPILOT_1_ACTIVE)") 29 | hdg = vr.get("(L:A32NX_AUTOPILOT_HEADING_SELECTED)") 30 | mode = vr.get("(L:A32NX_FMA_LATERAL_MODE)") 31 | sleep(1) -------------------------------------------------------------------------------- /prototype/mobiflight_variable_requests.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import struct 3 | import time 4 | from ctypes import sizeof 5 | from ctypes.wintypes import FLOAT 6 | from SimConnect.Enum import SIMCONNECT_CLIENT_DATA_PERIOD, SIMCONNECT_UNUSED 7 | import pathlib 8 | 9 | class SimVariable: 10 | def __init__(self, id, name, float_value = 0): 11 | self.id = id 12 | self.name = name 13 | self.float_value = float_value 14 | def __str__(self): 15 | return f"Id={self.id}, value={self.float_value}, name={self.name}" 16 | 17 | class MobiClient: 18 | 19 | def __init__(self, client_name): 20 | self.CLIENT_NAME = client_name 21 | self.CLIENT_DATA_AREA_LVARS = None 22 | self.CLIENT_DATA_AREA_CMD = None 23 | self.CLIENT_DATA_AREA_RESPONSE = None 24 | self.DATA_STRING_DEFINITION_ID = None 25 | def __str__(self): 26 | s = f"Name={self.CLIENT_NAME}, LVARS_ID={self.CLIENT_DATA_AREA_LVARS}, CMD_ID={self.CLIENT_DATA_AREA_CMD}, " 27 | return s + f"RESPONSE_ID={self.CLIENT_DATA_AREA_RESPONSE}, DEF_ID={self.DATA_STRING_DEFINITION_ID}" 28 | 29 | 30 | class MobiFlightVariableRequests: 31 | 32 | def __init__(self, simConnect): 33 | logging.info("MobiFlightVariableRequests: __init__") 34 | 35 | self.init_ready = False 36 | self.sm = simConnect 37 | self.sim_vars = {} 38 | self.sim_var_name_to_id = {} 39 | 40 | self.init_client = MobiClient("MobiFlight") 41 | self.init_client.CLIENT_DATA_AREA_LVARS = 0 42 | self.init_client.CLIENT_DATA_AREA_CMD = 1 43 | self.init_client.CLIENT_DATA_AREA_RESPONSE = 2 44 | self.init_client.DATA_STRING_DEFINITION_ID = 0 45 | 46 | working_dir_hash = hash(pathlib.Path.cwd()) % 100000000 # limit to 8 digits 47 | last_folder_name = pathlib.Path.cwd().name 48 | if len(last_folder_name) > 12: 49 | last_folder_name = last_folder_name[:12] 50 | client_name = last_folder_name + str(working_dir_hash) 51 | 52 | self.my_client = MobiClient(client_name) 53 | self.my_client.CLIENT_DATA_AREA_LVARS = 3 54 | self.my_client.CLIENT_DATA_AREA_CMD = 4 55 | self.my_client.CLIENT_DATA_AREA_RESPONSE = 5 56 | self.my_client.DATA_STRING_DEFINITION_ID = 1 57 | 58 | self.FLAG_DEFAULT = 0 59 | self.FLAG_CHANGED = 1 60 | self.DATA_STRING_SIZE = 1024 61 | self.DATA_STRING_OFFSET = 0 62 | self.SIMVAR_DEF_OFFSET = 1000 63 | 64 | self.sm.register_client_data_handler(self._client_data_callback_handler) 65 | self._initialize_client_data_areas(self.init_client) 66 | 67 | # Sometimes first command after reconnect is ignored. Therefore send just some arbitrary command. 68 | self._send_command("Do Nothing", self.init_client) 69 | self._send_command(("MF.Clients.Add." + client_name), self.init_client) 70 | # Wait for init ready 71 | while not self.init_ready: 72 | time.sleep(0.05) 73 | 74 | 75 | def _add_to_client_data_definition(self, definition_id, offset, size): 76 | logging.info("add_to_client_data_definition: definition_id=%s, offset=%s, size=%s", definition_id, offset, size) 77 | self.sm.dll.AddToClientDataDefinition( 78 | self.sm.hSimConnect, 79 | definition_id, 80 | offset, 81 | size, 82 | 0, # fEpsilon 83 | SIMCONNECT_UNUSED) # DatumId 84 | 85 | 86 | def _subscribe_to_data_change(self, data_area_id, request_id, definition_id, interval=0): 87 | logging.info("subscribe_to_data_change: data_area_id=%s, request_id=%s, definition_id=%s", data_area_id, request_id, definition_id) 88 | self.sm.dll.RequestClientData( 89 | self.sm.hSimConnect, 90 | data_area_id, 91 | request_id, 92 | definition_id, 93 | SIMCONNECT_CLIENT_DATA_PERIOD.SIMCONNECT_CLIENT_DATA_PERIOD_ON_SET, 94 | self.FLAG_CHANGED, 95 | 0, # origin 96 | interval, # interval 97 | 0) # limit 98 | 99 | 100 | def _send_data(self, client, size, dataBytes): 101 | logging.info("send_data: client: %s, size=%s, dataBytes=%s", client, size, dataBytes) 102 | self.sm.dll.SetClientData( 103 | self.sm.hSimConnect, 104 | client.CLIENT_DATA_AREA_CMD, 105 | client.DATA_STRING_DEFINITION_ID, 106 | self.FLAG_DEFAULT, 107 | 0, # dwReserved 108 | size, 109 | dataBytes) 110 | 111 | 112 | def _send_command(self, command, client): 113 | logging.info("send_command: command=%s", command) 114 | data_byte_array = bytearray(command, "ascii") 115 | data_byte_array.extend(bytearray(self.DATA_STRING_SIZE - len(data_byte_array))) # extend to fix DATA_STRING_SIZE 116 | my_bytes = bytes(data_byte_array) 117 | self._send_data(client, self.DATA_STRING_SIZE, my_bytes) 118 | 119 | 120 | def _initialize_client_data_areas(self, client): 121 | logging.info("initialize_client_data_areas: Client: %s", client) 122 | # register client data area for receiving simvars, sending commands and receiving responses 123 | dataAreaLVarsName = (client.CLIENT_NAME + ".Lvars").encode("ascii") 124 | dataAreaCmdName = (client.CLIENT_NAME + ".Command").encode("ascii") 125 | dataAreaResponseName = (client.CLIENT_NAME + ".Response").encode("ascii") 126 | self.sm.dll.MapClientDataNameToID(self.sm.hSimConnect, dataAreaLVarsName, client.CLIENT_DATA_AREA_LVARS) 127 | self.sm.dll.MapClientDataNameToID(self.sm.hSimConnect, dataAreaCmdName, client.CLIENT_DATA_AREA_CMD) 128 | self.sm.dll.MapClientDataNameToID(self.sm.hSimConnect, dataAreaResponseName, client.CLIENT_DATA_AREA_RESPONSE) 129 | # subscribe to WASM Module responses 130 | self._add_to_client_data_definition(client.DATA_STRING_DEFINITION_ID, self.DATA_STRING_OFFSET, self.DATA_STRING_SIZE) 131 | self._subscribe_to_data_change(client.CLIENT_DATA_AREA_RESPONSE, client.DATA_STRING_DEFINITION_ID, client.DATA_STRING_DEFINITION_ID) 132 | 133 | 134 | def _c_string_bytes_to_string(self, data_bytes): 135 | return data_bytes[0:data_bytes.index(0)].decode(encoding='ascii') # index(0) for end of c string 136 | 137 | 138 | # simconnect library callback 139 | def _client_data_callback_handler(self, callback_data): 140 | # SimVar Data 141 | if callback_data.dwDefineID in self.sim_vars: 142 | data_bytes = struct.pack("I", callback_data.dwData[0]) 143 | float_data = struct.unpack(' [0] 144 | self.sim_vars[callback_data.dwDefineID].float_value = round(float_data, 5) 145 | logging.debug("client_data_callback_handler: %s", self.sim_vars[callback_data.dwDefineID]) 146 | 147 | # Response string of init_client 148 | elif callback_data.dwDefineID == self.init_client.DATA_STRING_DEFINITION_ID: 149 | response = self._c_string_bytes_to_string(bytes(callback_data.dwData)) 150 | logging.debug("client_data_callback_handler: init_client response string: %s", response) 151 | # Check for response of registering new client 152 | if (self.my_client.CLIENT_NAME in response): 153 | self._initialize_client_data_areas(self.my_client) 154 | self.init_ready = True 155 | 156 | # Response string of my_client 157 | elif callback_data.dwDefineID == self.my_client.DATA_STRING_DEFINITION_ID: 158 | response = self._c_string_bytes_to_string(bytes(callback_data.dwData)) 159 | logging.debug("client_data_callback_handler: get my_client response string: %s", response) 160 | else: 161 | logging.warning("client_data_callback_handler: DefinitionID %s not found!", callback_data.dwDefineID) 162 | 163 | 164 | def get(self, variableString): 165 | client = self.my_client 166 | if variableString not in self.sim_var_name_to_id: 167 | # add new variable 168 | id = len(self.sim_vars) + self.SIMVAR_DEF_OFFSET 169 | self.sim_vars[id] = SimVariable(id, variableString) 170 | self.sim_var_name_to_id[variableString] = id 171 | # subscribe to variable data change 172 | offset = (id-self.SIMVAR_DEF_OFFSET)*sizeof(FLOAT) 173 | self._add_to_client_data_definition(id, offset, sizeof(FLOAT)) 174 | self._subscribe_to_data_change(client.CLIENT_DATA_AREA_LVARS, id, id) 175 | self._send_command(("MF.SimVars.Add." + variableString), client) 176 | # determine id and return value 177 | variable_id = self.sim_var_name_to_id[variableString] 178 | float_value = self.sim_vars[variable_id].float_value 179 | logging.debug("get: %s. Return=%s", variableString, float_value) 180 | return float_value 181 | 182 | 183 | def set(self, variableString): 184 | logging.debug("set: %s", variableString) 185 | self._send_command(("MF.SimVars.Set." + variableString), self.my_client) 186 | 187 | 188 | def _list_sim_variables(self): 189 | logging.info("list_sim_variables MF.LVars.List") 190 | self._send_command("MF.LVars.List", self.my_client) 191 | 192 | 193 | def clear_sim_variables(self): 194 | logging.info("clear_sim_variables") 195 | self.sim_vars.clear() 196 | self.sim_var_name_to_id.clear() 197 | self._send_command("MF.SimVars.Clear", self.my_client) 198 | 199 | 200 | -------------------------------------------------------------------------------- /prototype/simconnect_mobiflight.py: -------------------------------------------------------------------------------- 1 | import logging, logging.handlers 2 | import ctypes 3 | from ctypes import wintypes 4 | from SimConnect import SimConnect 5 | from SimConnect.Enum import SIMCONNECT_CLIENT_DATA_ID, SIMCONNECT_RECV_ID, SIMCONNECT_RECV_CLIENT_DATA 6 | 7 | 8 | class SimConnectMobiFlight(SimConnect): 9 | 10 | def __init__(self, auto_connect=True, library_path=None): 11 | self.client_data_handlers = [] 12 | if library_path: 13 | super().__init__(auto_connect, library_path) 14 | else: 15 | super().__init__(auto_connect) 16 | # Fix missing types 17 | self.dll.MapClientDataNameToID.argtypes = [wintypes.HANDLE, ctypes.c_char_p, SIMCONNECT_CLIENT_DATA_ID] 18 | 19 | 20 | def register_client_data_handler(self, handler): 21 | if not handler in self.client_data_handlers: 22 | logging.info("Register new client data handler") 23 | self.client_data_handlers.append(handler) 24 | 25 | 26 | def unregister_client_data_handler(self, handler): 27 | if handler in self.client_data_handlers: 28 | logging.info("Unregister client data handler") 29 | self.client_data_handlers.remove(handler) 30 | 31 | 32 | def my_dispatch_proc(self, pData, cbData, pContext): 33 | dwID = pData.contents.dwID 34 | if dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_CLIENT_DATA: 35 | client_data = ctypes.cast(pData, ctypes.POINTER(SIMCONNECT_RECV_CLIENT_DATA)).contents 36 | for handler in self.client_data_handlers: 37 | handler(client_data) 38 | else: 39 | super().my_dispatch_proc(pData, cbData, pContext) -------------------------------------------------------------------------------- /src/example.py: -------------------------------------------------------------------------------- 1 | import logging, logging.handlers 2 | from simconnect_mobiflight import SimConnectMobiFlight 3 | from mobiflight_variable_requests import MobiFlightVariableRequests 4 | from time import sleep 5 | 6 | def setupLogging(logFileName): 7 | logFormatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s") 8 | rootLogger = logging.getLogger() 9 | rootLogger.setLevel(logging.DEBUG) 10 | fileHandler = logging.handlers.RotatingFileHandler(logFileName, maxBytes=500000, backupCount=7) 11 | fileHandler.setFormatter(logFormatter) 12 | rootLogger.addHandler(fileHandler) 13 | consoleHandler = logging.StreamHandler() 14 | consoleHandler.setFormatter(logFormatter) 15 | rootLogger.addHandler(consoleHandler) 16 | 17 | # MAIN 18 | setupLogging("SimConnectMobiFlight.log") 19 | sm = SimConnectMobiFlight() 20 | vr = MobiFlightVariableRequests(sm) 21 | vr.clear_sim_variables() 22 | 23 | # Example write variable 24 | vr.set("0 (>L:A32NX_COCKPIT_DOOR_LOCKED)") 25 | 26 | while True: 27 | alt_ground = vr.get("(A:GROUND ALTITUDE,Meters)") 28 | alt_plane = vr.get("(A:PLANE ALTITUDE,Feet)") 29 | # FlyByWire A320 30 | ap1 = vr.get("(L:A32NX_AUTOPILOT_1_ACTIVE)") 31 | hdg = vr.get("(L:A32NX_AUTOPILOT_HEADING_SELECTED)") 32 | mode = vr.get("(L:A32NX_FMA_LATERAL_MODE)") 33 | sleep(1) -------------------------------------------------------------------------------- /src/mobiflight_variable_requests.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import struct 3 | from time import sleep 4 | from ctypes import sizeof 5 | from ctypes.wintypes import FLOAT 6 | from SimConnect.Enum import SIMCONNECT_CLIENT_DATA_PERIOD, SIMCONNECT_UNUSED 7 | 8 | 9 | class SimVariable: 10 | def __init__(self, id, name, float_value = None): 11 | self.id = id 12 | self.name = name 13 | self.float_value = float_value 14 | self.initialized = False 15 | def __str__(self): 16 | return f"Id={self.id}, value={self.float_value}, name={self.name}" 17 | 18 | 19 | class MobiFlightVariableRequests: 20 | 21 | def __init__(self, simConnect): 22 | logging.info("MobiFlightVariableRequests __init__") 23 | self.sm = simConnect 24 | self.sim_vars = {} 25 | self.sim_var_name_to_id = {} 26 | self.CLIENT_DATA_AREA_LVARS = 0 27 | self.CLIENT_DATA_AREA_CMD = 1 28 | self.CLIENT_DATA_AREA_RESPONSE = 2 29 | self.FLAG_DEFAULT = 0 30 | self.FLAG_CHANGED = 1 31 | self.DATA_STRING_SIZE = 256 32 | self.DATA_STRING_OFFSET = 0 33 | self.DATA_STRING_DEFINITION_ID = 0 34 | self.sm.register_client_data_handler(self.client_data_callback_handler) 35 | self.initialize_client_data_areas() 36 | 37 | 38 | def add_to_client_data_definition(self, definition_id, offset, size): 39 | logging.info("add_to_client_data_definition definition_id=%s, offset=%s, size=%s", definition_id, offset, size) 40 | self.sm.dll.AddToClientDataDefinition( 41 | self.sm.hSimConnect, 42 | definition_id, 43 | offset, 44 | size, 45 | 0, # fEpsilon 46 | SIMCONNECT_UNUSED) # DatumId 47 | 48 | 49 | def subscribe_to_data_change(self, data_area_id, request_id, definition_id): 50 | logging.info("subscribe_to_data_change data_area_id=%s, request_id=%s, definition_id=%s", data_area_id, request_id, definition_id) 51 | self.sm.dll.RequestClientData( 52 | self.sm.hSimConnect, 53 | data_area_id, 54 | request_id, 55 | definition_id, 56 | SIMCONNECT_CLIENT_DATA_PERIOD.SIMCONNECT_CLIENT_DATA_PERIOD_ON_SET, 57 | self.FLAG_CHANGED, 58 | 0, # origin 59 | 0, # interval 60 | 0) # limit 61 | 62 | 63 | def send_data(self, data_area_id, definition_id, size, dataBytes): 64 | logging.info("send_data data_area_id=%s, definition_id=%s, size=%s, dataBytes=%s", data_area_id, definition_id, size, dataBytes) 65 | self.sm.dll.SetClientData( 66 | self.sm.hSimConnect, 67 | data_area_id, 68 | definition_id, 69 | self.FLAG_DEFAULT, 70 | 0, # dwReserved 71 | size, 72 | dataBytes) 73 | 74 | 75 | def send_command(self, command): 76 | logging.info("send_command command=%s", command) 77 | data_byte_array = bytearray(command, "ascii") 78 | data_byte_array.extend(bytearray(self.DATA_STRING_SIZE - len(data_byte_array))) # extend to fix DATA_STRING_SIZE 79 | self.send_data(self.CLIENT_DATA_AREA_CMD, self.DATA_STRING_DEFINITION_ID, self.DATA_STRING_SIZE, bytes(data_byte_array)) 80 | 81 | 82 | def initialize_client_data_areas(self): 83 | logging.info("initialize_client_data_areas") 84 | # register client data area for receiving simvars 85 | self.sm.dll.MapClientDataNameToID(self.sm.hSimConnect, "MobiFlight.LVars".encode("ascii"), self.CLIENT_DATA_AREA_LVARS) 86 | self.sm.dll.CreateClientData(self.sm.hSimConnect, self.CLIENT_DATA_AREA_LVARS, 4096, self.FLAG_DEFAULT) 87 | # register client data area for sending commands 88 | self.sm.dll.MapClientDataNameToID(self.sm.hSimConnect, "MobiFlight.Command".encode("ascii"), self.CLIENT_DATA_AREA_CMD) 89 | self.sm.dll.CreateClientData(self.sm.hSimConnect, self.CLIENT_DATA_AREA_CMD, self.DATA_STRING_SIZE, self.FLAG_DEFAULT) 90 | # register client data area for receiving responses 91 | self.sm.dll.MapClientDataNameToID(self.sm.hSimConnect, "MobiFlight.Response".encode("ascii"), self.CLIENT_DATA_AREA_RESPONSE) 92 | self.sm.dll.CreateClientData(self.sm.hSimConnect, self.CLIENT_DATA_AREA_RESPONSE, self.DATA_STRING_SIZE, self.FLAG_DEFAULT) 93 | # subscribe to WASM Module responses 94 | self.add_to_client_data_definition(self.DATA_STRING_DEFINITION_ID, self.DATA_STRING_OFFSET, self.DATA_STRING_SIZE) 95 | self.subscribe_to_data_change(self.CLIENT_DATA_AREA_RESPONSE, self.DATA_STRING_DEFINITION_ID, self.DATA_STRING_DEFINITION_ID) 96 | 97 | 98 | # simconnect library callback 99 | def client_data_callback_handler(self, client_data): 100 | if client_data.dwDefineID in self.sim_vars: 101 | data_bytes = struct.pack("I", client_data.dwData[0]) 102 | float_data = struct.unpack(' [0] 103 | float_value = round(float_data, 5) 104 | sim_var = self.sim_vars[client_data.dwDefineID] 105 | if not sim_var.initialized and float_value == 0.0: 106 | sim_var.initialized = True 107 | else: 108 | self.sim_vars[client_data.dwDefineID].float_value = float_value 109 | logging.debug("client_data_callback_handler %s, raw=%s", sim_var, float_value) 110 | else: 111 | logging.warning("client_data_callback_handler DefinitionID %s not found!", client_data.dwDefineID) 112 | 113 | 114 | def get(self, variableString): 115 | if variableString not in self.sim_var_name_to_id: 116 | # add new variable 117 | id = len(self.sim_vars) + 1 118 | self.sim_vars[id] = SimVariable(id, variableString) 119 | self.sim_var_name_to_id[variableString] = id 120 | # subscribe to variable data change 121 | offset = (id-1)*sizeof(FLOAT) 122 | self.add_to_client_data_definition(id, offset, sizeof(FLOAT)) 123 | self.subscribe_to_data_change(self.CLIENT_DATA_AREA_LVARS, id, id) 124 | self.send_command("MF.SimVars.Add." + variableString) 125 | # determine id and return value 126 | variable_id = self.sim_var_name_to_id[variableString] 127 | sim_var = self.sim_vars[variable_id] 128 | wait_counter = 0 129 | while wait_counter < 50: # wait max 500ms 130 | if sim_var.float_value is None: 131 | sleep(0.01) # wait 10ms 132 | wait_counter = wait_counter + 1 133 | else: 134 | break 135 | if sim_var.float_value is None and sim_var.initialized: 136 | sim_var.float_value = 0.0 137 | logging.debug("get %s. wait_counter=%s, Return=%s", variableString, wait_counter, sim_var.float_value) 138 | return sim_var.float_value 139 | 140 | 141 | def set(self, variableString): 142 | logging.debug("set: %s", variableString) 143 | self.send_command("MF.SimVars.Set." + variableString) 144 | 145 | 146 | def clear_sim_variables(self): 147 | logging.info("clear_sim_variables") 148 | self.sim_vars.clear() 149 | self.sim_var_name_to_id.clear() 150 | self.send_command("MF.SimVars.Clear") 151 | 152 | 153 | -------------------------------------------------------------------------------- /src/simconnect_mobiflight.py: -------------------------------------------------------------------------------- 1 | import logging, logging.handlers 2 | import ctypes 3 | from ctypes import wintypes 4 | from SimConnect import SimConnect 5 | from SimConnect.Enum import SIMCONNECT_CLIENT_DATA_ID, SIMCONNECT_RECV_ID, SIMCONNECT_RECV_CLIENT_DATA 6 | 7 | 8 | class SimConnectMobiFlight(SimConnect): 9 | 10 | def __init__(self, auto_connect=True, library_path=None): 11 | self.client_data_handlers = [] 12 | if library_path: 13 | super().__init__(auto_connect, library_path) 14 | else: 15 | super().__init__(auto_connect) 16 | # Fix missing types 17 | self.dll.MapClientDataNameToID.argtypes = [wintypes.HANDLE, ctypes.c_char_p, SIMCONNECT_CLIENT_DATA_ID] 18 | 19 | 20 | def register_client_data_handler(self, handler): 21 | if not handler in self.client_data_handlers: 22 | logging.info("Register new client data handler") 23 | self.client_data_handlers.append(handler) 24 | 25 | 26 | def unregister_client_data_handler(self, handler): 27 | if handler in self.client_data_handlers: 28 | logging.info("Unregister client data handler") 29 | self.client_data_handlers.remove(handler) 30 | 31 | 32 | def my_dispatch_proc(self, pData, cbData, pContext): 33 | dwID = pData.contents.dwID 34 | if dwID == SIMCONNECT_RECV_ID.SIMCONNECT_RECV_ID_CLIENT_DATA: 35 | client_data = ctypes.cast(pData, ctypes.POINTER(SIMCONNECT_RECV_CLIENT_DATA)).contents 36 | for handler in self.client_data_handlers: 37 | handler(client_data) 38 | else: 39 | super().my_dispatch_proc(pData, cbData, pContext) --------------------------------------------------------------------------------