Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 206 additions & 24 deletions Scripts/Winwing/ifly_737_winwing_cdu.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from ctypes import Structure, c_int, c_long, c_ubyte, c_double, c_bool, c_char
from ctypes import Structure, c_int, c_int32, c_long, c_ubyte, c_double, c_bool, c_char, c_wchar
from enum import Enum
import ctypes
import json
import logging
Expand All @@ -16,13 +17,39 @@
CELLS = COLUMNS * ROWS

# Memory map name
MEMORY_MAP_NAME = "iFly737MAX_SDK_FileMappingObject"
NG_SDK2_MEMORY_MAP_NAME = "iFly737NG_SDK2_FileMappingObject"
MAX_SDK_MEMORY_MAP_NAME = "iFly737MAX_SDK_FileMappingObject"

class iFlySDK_Identifier(Enum):
SDK_UNKNOWN = 1
SDK_NG = 2
SDK_MAX = 3

class ShareMemory737NGSDK2(ctypes.Structure):
"""Structure matching the iFly 737 NG SDK2 SDK memory layout"""
_fields_ = [
# Manually gathered from SDK_CDU.h

("LSKChar", ((c_wchar * 24) * 14) * 2), # <WCHAR> = 16bit unicode character (2 bytes)
("LSK_SmallFont", ((c_int32 * 24) * 14) * 2), # <BOOL> = 32bit int (4 bytes)
("LSK_Color", ((c_int * 24) * 14) * 2),
("CDU_Can_Display", c_int32 * 2), # <BOOL> # FALSE: the screen is blank due to power loss or other situation: TRUE: the screen can display normally
# Unused, but might be useful at some point
("CDU_MSG_Status", c_int32 * 2), # <BOOL> = 32bit int (4 bytes)
("CDU_EXEC_Status", c_int32 * 2), # <BOOL> = 32bit int (4 bytes)
("CDU_CALL_Status", c_int32 * 2), # <BOOL> = 32bit int (4 bytes)
("CDU_OFST_Status", c_int32 * 2), # <BOOL> = 32bit int (4 bytes)
("CDU_TEST_Status", c_int * 2), # 0: no test, test colours = 1:RED, 2:GREEN, 3:BLUE, 4:AMBER, 5:MAGENTA, 6:CYAN, 7:WHITE, 8:GRAYSCALE, 8:CHECKERBOARD
("iFly737NG_State", c_int), # iFly737NG is running
]


class ShareMemory737MAXSDK(ctypes.Structure):
"""Structure matching the iFly 737 MAX SDK memory layout"""
_fields_ = [
# Manually gathered from SDK_Defines.h
("OFFSET", c_ubyte * 0x42C),
("iFly737MAX_State", c_int32), # iFly737MAX is running
("OFFSET", c_ubyte * 0x428),

("LSKChar", ((c_char * 24) * 14) * 2),
("LSK_SmallFont", ((c_bool * 24) * 14) * 2),
Expand All @@ -36,6 +63,7 @@ class ShareMemory737MAXSDK(ctypes.Structure):
("CDU_BRT_Switch_Status", c_ubyte * 2),
]


class MobiFlightClient:
def __init__(self, url: str) -> None:
self.url: str = url
Expand Down Expand Up @@ -76,8 +104,62 @@ async def close(self) -> None:
await self.websocket.close()
self.websocket = None

def create_mobi_json(memory_map: ShareMemory737MAXSDK, cdu_index: int) -> Dict:
"""Create JSON message for MobiFlight WebSocket from memory map data"""
#
# CDU: Display "WAITING FOR IFLY 737" while waiting to connect to iFly
#
def create_wait_ifly_json() -> Dict:
"""Create JSON message for MobiFlight WebSocket"""
message: Dict[str, Union[str, List[List[Union[str, int]]]]] = {
"Target": "Display",
"Data": [[] for _ in range(CELLS)]
}

data = []

# Empty cells for first 5 rows
data.extend([[]] * (5 * COLUMNS))

# Display message lines
line6 = [[],[],[],[],[],["W","c",1],[],["A","c",1],[],["I","c",1],[],["T","c",1],[],["I","c",1],[],["N","c",1],[],["G","c",1],[],[],[],[],[],[]]
line7 = [[],[],[],[],[],[],[],[],[],["F","c",1],[],["O","c",1],[],["R","c",1],[],[],[],[],[],[],[],[],[],[]]
line8 = [[],[],[],[],[],["I","w",1],[],["F","w",1],[],["L","w",1],[],["Y","w",1],[],["7","m",1],[],["3","m",1],[],["7","m",1],[],[],[],[],[],[]]

data.extend(line6 + line7 + line8)

# Empty cells for remaining rows
data.extend([[]] * (6 * COLUMNS))

message["Data"] = data

return message
#
# CDU/NG: Display we're switched off / empty
#
def create_ng_nopower_cdu_json() -> Dict:
"""Create JSON message for MobiFlight WebSocket"""
message: Dict[str, Union[str, List[List[Union[str, int]]]]] = {
"Target": "Display",
"Data": [[] for _ in range(CELLS)]
}

data = [[]] * (ROWS * COLUMNS)

message["Data"] = data

return message
#
# CDU: Display contents of the iFly CDU (works for both NG and MAX)
#
def create_cdu_mobi_json(memory_map: Union[ShareMemory737NGSDK2, ShareMemory737MAXSDK], cdu_index: int) -> Dict:
"""Create JSON message for MobiFlight WebSocket from memory map data
Args:
memory_map: Either ShareMemory737NGSDK2 or ShareMemory737MAXSDK structure
cdu_index: 0 for captain, 1 for first officer
Returns:
Dictionary with Target and Data fields for MobiFlight WebSocket
"""
message: Dict[str, Union[str, List[List[Union[str, int]]]]] = {
"Target": "Display",
"Data": [[] for _ in range(CELLS)]
Expand All @@ -97,15 +179,21 @@ def create_mobi_json(memory_map: ShareMemory737MAXSDK, cdu_index: int) -> Dict:
9: "w", # Left Arrow (White)
10: "w" # Right Arrow (White)
}

try:
data = []
data = []
for row in range(ROWS):
for col in range(COLUMNS):
char = memory_map.LSKChar[cdu_index][row][col].decode('ascii', errors='replace')
char_raw = memory_map.LSKChar[cdu_index][row][col]
# Handle both c_char (bytes) from MAX and c_wchar (str) from NG
if isinstance(char_raw, bytes):
char = char_raw.decode('ascii', errors='replace')
else:
char = char_raw

small_font = memory_map.LSK_SmallFont[cdu_index][row][col]
color = memory_map.LSK_Color[cdu_index][row][col]

if color == 0 and char in [' ', '\0']:
data.append([])
else:
Expand All @@ -118,13 +206,13 @@ def create_mobi_json(memory_map: ShareMemory737MAXSDK, cdu_index: int) -> Dict:
char = "\u2192" # Unicode right arrow
elif color in (6, 7, 8): # Degree symbol
char = "\u00B0" # Unicode degree symbol

data.append([
char,
color_map.get(color, "w"),
1 if small_font else 0
])

message["Data"] = data

except Exception as e:
Expand All @@ -139,32 +227,126 @@ def __init__(self, cdu_index: int) -> None:
self.client = MobiFlightClient(CAPTAIN_CDU_URL if cdu_index == 0 else FO_CDU_URL)
self.memory_map: Optional[mmap.mmap] = None
self._running: bool = False
self.iflySDK: iFlySDK_Identifier = iFlySDK_Identifier.SDK_UNKNOWN

def _memory_map_exists(self, name: str) -> bool:
"""Check if a named memory map exists without creating it"""
try:
kernel32 = ctypes.windll.kernel32
# Try to open existing mapping (FILE_MAP_READ = 0x0004)
handle = kernel32.OpenFileMappingW(
0x0004, # FILE_MAP_READ
False, # Don't inherit handle
name
)

if handle:
kernel32.CloseHandle(handle)
return True
return False
except Exception as e:
logging.debug(f"Error checking memory map '{name}': {e}")
return False

def setup_memory_map(self) -> bool:
"""Setup memory map by detecting which iFly variant is running"""
try:
self.memory_map = mmap.mmap(-1, ctypes.sizeof(ShareMemory737MAXSDK),
MEMORY_MAP_NAME,
access=mmap.ACCESS_READ)
logging.info(f"Successfully opened memory map for CDU {self.cdu_index}")
return True
# Check MAX first
if self._memory_map_exists(MAX_SDK_MEMORY_MAP_NAME):
try:
self.memory_map = mmap.mmap(-1, ctypes.sizeof(ShareMemory737MAXSDK),
MAX_SDK_MEMORY_MAP_NAME,
access=mmap.ACCESS_READ)
data = self.memory_map.read(ctypes.sizeof(ShareMemory737MAXSDK))
self.memory_map.seek(0)
memory_struct = ShareMemory737MAXSDK.from_buffer_copy(data)

if 1 == memory_struct.iFly737MAX_State:
self.iflySDK = iFlySDK_Identifier.SDK_MAX
logging.info(f"Successfully opened memory map for iFly737MAX CDU {self.cdu_index}")
return True
else:
# MAX memory map exists but simulator not running
logging.warning(f"iFly737MAX memory map exists but State={memory_struct.iFly737MAX_State}")
self.memory_map.close()
self.memory_map = None
except Exception as e:
logging.error(f"Failed to read MAX memory map: {e}")
if self.memory_map:
self.memory_map.close()
self.memory_map = None

# Check NG
if self._memory_map_exists(NG_SDK2_MEMORY_MAP_NAME):
try:
self.memory_map = mmap.mmap(-1, ctypes.sizeof(ShareMemory737NGSDK2),
NG_SDK2_MEMORY_MAP_NAME,
access=mmap.ACCESS_READ)
data = self.memory_map.read(ctypes.sizeof(ShareMemory737NGSDK2))
self.memory_map.seek(0)
memory_struct = ShareMemory737NGSDK2.from_buffer_copy(data)

if 1 == memory_struct.iFly737NG_State:
self.iflySDK = iFlySDK_Identifier.SDK_NG
logging.info(f"Successfully opened memory map for iFly737NG CDU {self.cdu_index}")
return True
else:
# NG memory map exists but simulator not running
logging.warning(f"iFly737NG memory map exists but State={memory_struct.iFly737NG_State}")
self.memory_map.close()
self.memory_map = None
except Exception as e:
logging.error(f"Failed to read NG memory map: {e}")
if self.memory_map:
self.memory_map.close()
self.memory_map = None

# Neither memory map exists or both failed state checks
logging.error(f"No active iFly 737 simulator found for CDU {self.cdu_index}")
return False

except Exception as e:
logging.error(f"Failed to open memory map for CDU {self.cdu_index}: {e}")
logging.error(f"Failed to setup memory map for CDU {self.cdu_index}: {e}")
if self.memory_map:
self.memory_map.close()
self.memory_map = None
return False

async def process_memory_map(self) -> None:
if not self.memory_map:
return

try:
# Read the entire structure
data = self.memory_map.read(ctypes.sizeof(ShareMemory737MAXSDK))
# Get the appropriate structure type and size based on SDK
if self.iflySDK == iFlySDK_Identifier.SDK_MAX:
structure_class = ShareMemory737MAXSDK
state_field = 'iFly737MAX_State'
has_power_check = False
elif self.iflySDK == iFlySDK_Identifier.SDK_NG:
structure_class = ShareMemory737NGSDK2
state_field = 'iFly737NG_State'
has_power_check = True
else:
return

# Read and parse memory structure (common code)
data = self.memory_map.read(ctypes.sizeof(structure_class))
self.memory_map.seek(0)
memory_struct = structure_class.from_buffer_copy(data)

# Create structure from memory map data
memory_struct = ShareMemory737MAXSDK.from_buffer_copy(data)
# Determine which JSON to generate based on state
state_value = getattr(memory_struct, state_field)

# Create and send JSON message
json_data = create_mobi_json(memory_struct, self.cdu_index)
if state_value == 0:
# iFly unavailable
json_data = create_wait_ifly_json()
elif has_power_check and memory_struct.CDU_Can_Display[self.cdu_index] == 0:
# NG only: CDU powered off
json_data = create_ng_nopower_cdu_json()
else:
# Normal operation
json_data = create_cdu_mobi_json(memory_struct, self.cdu_index)

await self.client.send(json_data)

except Exception as e:
Expand Down Expand Up @@ -215,4 +397,4 @@ async def main() -> None:
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
asyncio.run(main())
asyncio.run(main())