Skip to content

Commit

Permalink
Implement auto-version parsing for FDM, Ctrls, Gui connections
Browse files Browse the repository at this point in the history
  • Loading branch information
julianneswinoga committed May 19, 2024
1 parent 9ac08e8 commit 9e51476
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 50 deletions.
110 changes: 83 additions & 27 deletions flightgear_python/fg_if.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@
import multiprocess as mp
import requests

from construct import ConstError, Struct, Container
from construct import ConstError, Struct, Container, Construct, Int32ub, Int32ul

from .general_util import EventPipe, strip_end, deprecate_rename_wrapper
from .fg_util import FGConnectionError, FGCommunicationError, fix_fg_radian_parsing
from .fdm_v24 import fdm_struct as fdm_struct_v24
from .fdm_v25 import fdm_struct as fdm_struct_v25
from .ctrls_v27 import ctrls_struct as ctrls_struct_v27
from .gui_v8 import gui_struct as gui_struct_v8

rx_callback_type = Callable[[Container, EventPipe], Optional[Container]]
"""
Expand All @@ -34,6 +38,7 @@ class FGConnection:

# These are filled from the child class
fg_net_struct: Optional[Struct] = None
fg_auto_partial_parse: Optional['PartialParseSwitchStruct'] = None

def __init__(self, rx_timeout_s: float = 2.0):
self.event_pipe = EventPipe(duplex=True)
Expand Down Expand Up @@ -107,6 +112,11 @@ def _fg_packet_roundtrip(self):
else:
raise e

# Auto-version logic
if self.fg_auto_partial_parse and self.fg_net_struct is None:
# We lazily create the actual struct that will be used for parsing
self.fg_net_struct = self.fg_auto_partial_parse.resolve(rx_msg)

try:
s: Container = self.fg_net_struct.parse(rx_msg)
except ConstError as e:
Expand Down Expand Up @@ -148,63 +158,109 @@ def stop(self):
self.rx_proc.terminate()


class PartialParseSwitchStruct:
"""
A utility class that allows runtime switching of which version of a struct
is used to parse an incoming message. We use this (complexity) instead of
construct's conditional utilities because it makes looking at the individual
struct versions a bit less overwhelming, and easier to compare.
sphinx-no-autodoc
:param partial_parse_construct: The Construct that will be used to switch which
version of the Struct is resolved
:param replacement_full_structs: Mapping of struct versions to Structs
"""

def __init__(self, partial_parse_construct: Construct, replacement_full_structs: Dict[Any, Struct]):
self.partial_parse_construct = partial_parse_construct
self.replacement_full_structs = replacement_full_structs

def resolve(self, message: bytes) -> Struct:
"""
Resolve a message to a full Struct
:param message: The message to partially parse
:return: The full Struct based on the partial parsing of the message
"""
version: Any = self.partial_parse_construct.parse(message)
supported_version_list = list(self.replacement_full_structs.keys())
if version not in supported_version_list:
raise FGCommunicationError(
f'Auto-version detected {version} is not in the list of supported versions: {supported_version_list}'
)
return self.replacement_full_structs[version]


class FDMConnection(FGConnection):
"""
FlightGear Flight Dynamics Model Connection
:param fdm_version: Net FDM version (24 or 25)
:param fdm_version: Net FDM version (24, 25, or None for auto-detection)
:param rx_timeout_s: Optional timeout value in seconds when receiving data
"""

def __init__(self, fdm_version: int, rx_timeout_s: float = 2.0):
def __init__(self, fdm_version: Optional[int] = None, rx_timeout_s: float = 2.0):
super().__init__(rx_timeout_s=rx_timeout_s)
# TODO: Support auto-version check
if fdm_version == 24:
from .fdm_v24 import fdm_struct
elif fdm_version == 25:
from .fdm_v25 import fdm_struct
else:
raise NotImplementedError(f'FDM version {fdm_version} not supported yet')
fdm_support_dict: Dict[int, Struct] = {
24: fdm_struct_v24,
25: fdm_struct_v25,
}

self.fg_net_struct = fdm_struct
if fdm_version is None:
self.fg_auto_partial_parse = PartialParseSwitchStruct(
partial_parse_construct=Int32ub,
replacement_full_structs=fdm_support_dict,
)
else:
self.fg_net_struct = fdm_support_dict.get(fdm_version)
if self.fg_net_struct is None:
raise NotImplementedError(f'Manually specified FDM version {fdm_version} not supported yet')


class CtrlsConnection(FGConnection):
"""
FlightGear Controls Connection
:param ctrls_version: Net Ctrls version (27)
:param ctrls_version: Net Ctrls version (27, or None for auto-detection)
:param rx_timeout_s: Optional timeout value in seconds when receiving data
"""

def __init__(self, ctrls_version: int, rx_timeout_s: float = 2.0):
def __init__(self, ctrls_version: Optional[int] = None, rx_timeout_s: float = 2.0):
super().__init__(rx_timeout_s=rx_timeout_s)
# TODO: Support auto-version check
if ctrls_version == 27:
from .ctrls_v27 import ctrls_struct
ctrls_support_dict: Dict[int, Struct] = {
27: ctrls_struct_v27,
}
if ctrls_version is None:
self.fg_auto_partial_parse = PartialParseSwitchStruct(
partial_parse_construct=Int32ub,
replacement_full_structs=ctrls_support_dict,
)
else:
raise NotImplementedError(f'Controls version {ctrls_version} not supported yet')

self.fg_net_struct = ctrls_struct
self.fg_net_struct = ctrls_support_dict.get(ctrls_version)
if self.fg_net_struct is None:
raise NotImplementedError(f'Manually specified Controls version {ctrls_version} not supported yet')


class GuiConnection(FGConnection):
"""
FlightGear GUI Connection
:param gui_version: Net GUI version (8)
:param gui_version: Net GUI version (8, or None for auto-detection)
:param rx_timeout_s: Optional timeout value in seconds when receiving data
"""

def __init__(self, gui_version: int, rx_timeout_s: float = 2.0):
def __init__(self, gui_version: Optional[int] = None, rx_timeout_s: float = 2.0):
super().__init__(rx_timeout_s=rx_timeout_s)
# TODO: Support auto-version check
if gui_version == 8:
from .gui_v8 import gui_struct
gui_support_dict: Dict[int, Struct] = {
8: gui_struct_v8,
}
if gui_version is None:
self.fg_auto_partial_parse = PartialParseSwitchStruct(
partial_parse_construct=Int32ul,
replacement_full_structs=gui_support_dict,
)
else:
raise NotImplementedError(f'GUI version {gui_version} not supported yet')

self.fg_net_struct = gui_struct
self.fg_net_struct = gui_support_dict.get(gui_version)
if self.fg_net_struct is None:
raise NotImplementedError(f'Manually specified GUI version {gui_version} not supported yet')


class PropertyTreeValue(NamedTuple):
Expand Down
9 changes: 9 additions & 0 deletions tests/test_fdm_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

@pytest.mark.parametrize('fdm_version', supported_fdm_versions)
def test_fdm_length(mocker, fdm_version):
if fdm_version is None:
pytest.skip('Testing length makes no sense for auto version')

fdm_length = {
24: 408,
25: 552,
Expand Down Expand Up @@ -40,6 +43,9 @@ def mock_socket_bind(self, addr):

@pytest.mark.parametrize('fdm_version', supported_fdm_versions)
def test_fdm_rx_and_tx(mocker, fdm_version):
if fdm_version is None:
pytest.skip('Can\'t generate mocks for auto version')

def rx_cb(fdm_data, event_pipe):
(run_idx,) = event_pipe.child_recv()
callback_version = fdm_data['version']
Expand Down Expand Up @@ -73,6 +79,9 @@ def rx_cb(fdm_data, event_pipe):

@pytest.mark.parametrize('fdm_version', supported_fdm_versions)
def test_fdm_only_rx(mocker, fdm_version):
if fdm_version is None:
pytest.skip('Can\'t generate mocks for auto version')

def rx_cb(fdm_data, event_pipe):
callback_version = fdm_data['version']
event_pipe.child_send((callback_version,))
Expand Down
8 changes: 5 additions & 3 deletions tests/test_integration_FG_Ctrls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@


pytestmark = pytest.mark.fg_integration
ctrls_version = 27 # FlightGear-2020.3.19-x86_64.AppImage
ctrls_version_and_auto = [None, 27] # FlightGear-2020.3.19-x86_64.AppImage


def test_ctrls_rx_and_tx_integration():
@pytest.mark.parametrize('ctrls_version', ctrls_version_and_auto)
def test_ctrls_rx_and_tx_integration(ctrls_version):
def rx_cb(ctrls_data, event_pipe):
(run_idx,) = event_pipe.child_recv()
child_callback_version = ctrls_data['version']
Expand All @@ -29,7 +30,8 @@ def rx_cb(ctrls_data, event_pipe):
ctrls_c._fg_packet_roundtrip()
run_idx, parent_callback_version = ctrls_c.event_pipe.parent_recv()
assert run_idx == i
assert parent_callback_version == ctrls_version
if ctrls_version is not None:
assert parent_callback_version == ctrls_version

# Prevent 'ResourceWarning: unclosed' warning
ctrls_c.fg_rx_sock.close()
Expand Down
22 changes: 14 additions & 8 deletions tests/test_integration_FG_FDM.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@


pytestmark = pytest.mark.fg_integration
fdm_version = 24 # FlightGear-2020.3.19-x86_64.AppImage
fdm_version_and_auto = [None, 24] # FlightGear-2020.3.19-x86_64.AppImage


def test_fdm_rx_and_tx_integration():
@pytest.mark.parametrize('fdm_version', fdm_version_and_auto)
def test_fdm_rx_and_tx_integration(fdm_version):
def rx_cb(fdm_data, event_pipe):
(run_idx,) = event_pipe.child_recv()
(run_idx_child,) = event_pipe.child_recv()
child_callback_version = fdm_data['version']
event_pipe.child_send(
(
run_idx,
run_idx_child,
child_callback_version,
)
)
Expand All @@ -30,16 +31,21 @@ def rx_cb(fdm_data, event_pipe):
fdm_c.event_pipe.parent_send((i,))
# manually call the process instead of having the process spawn
fdm_c._fg_packet_roundtrip()
run_idx, parent_callback_version = fdm_c.event_pipe.parent_recv()
assert run_idx == i
assert parent_callback_version == fdm_version
run_idx_parent, parent_callback_version = fdm_c.event_pipe.parent_recv()
assert run_idx_parent == i
if fdm_version is not None:
assert parent_callback_version == fdm_version

# Prevent 'ResourceWarning: unclosed' warning
fdm_c.fg_rx_sock.close()
fdm_c.fg_tx_sock.close()


def test_fdm_wrong_version_in_stream():
@pytest.mark.parametrize('fdm_version', fdm_version_and_auto)
def test_fdm_wrong_version_in_stream(fdm_version):
if fdm_version is None:
pytest.skip('Wrong version test doesn\'t make sense for auto version')

fdm_version_supported_but_not_downloaded = fdm_version + 1
assert fdm_version_supported_but_not_downloaded in supported_fdm_versions

Expand Down
8 changes: 5 additions & 3 deletions tests/test_integration_FG_Gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@


pytestmark = pytest.mark.fg_integration
gui_version = 8 # FlightGear-2020.3.19-x86_64.AppImage
gui_version_and_auto = [None, 8] # FlightGear-2020.3.19-x86_64.AppImage


def test_gui_rx_and_tx_integration():
@pytest.mark.parametrize('gui_version', gui_version_and_auto)
def test_gui_rx_and_tx_integration(gui_version):
def rx_cb(gui_data, event_pipe):
(run_idx,) = event_pipe.child_recv()
child_callback_version = gui_data['version']
Expand All @@ -29,7 +30,8 @@ def rx_cb(gui_data, event_pipe):
gui_c._fg_packet_roundtrip()
run_idx, parent_callback_version = gui_c.event_pipe.parent_recv()
assert run_idx == i
assert parent_callback_version == gui_version
if gui_version is not None:
assert parent_callback_version == gui_version

# Prevent 'ResourceWarning: unclosed' warning
gui_c.fg_rx_sock.close()
Expand Down
27 changes: 18 additions & 9 deletions tests/test_integration_JSBSim_fdm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@

from flightgear_python.fg_if import FDMConnection

# from testing_common import supported_fdm_versions
from testing_common import supported_fdm_versions
from jsbsim_wrapper.jsbsim_wrapper import FlightGearUdpOutput, JsbConfig, Waypoint, setup_jsbsim

import pytest

# TODO: JSBSim 1.1.11 doesn't support FDM v25
# Once we drop 3.6 we can fully test v25
supported_fdm_versions = [24]
jsb_fg_versions = [24]


def pytest_generate_tests(metafunc):
test_params = []
for supported_fdm_version in supported_fdm_versions:
if supported_fdm_version is None:
# Special auto-version, should be tested with all available JSB FDM interface versions
for jsb_version in jsb_fg_versions:
test_params.append((supported_fdm_version, jsb_version))
elif supported_fdm_version in jsb_fg_versions:
test_params.append((supported_fdm_version, supported_fdm_version))
metafunc.parametrize('fdm_version, jsb_fg_version', test_params)


def fdm_callback(fdm_data, event_pipe):
Expand All @@ -22,9 +32,8 @@ def fdm_callback(fdm_data, event_pipe):
event_pipe.child_send(child_data)


@pytest.mark.parametrize('fdm_version', supported_fdm_versions)
def test_jsbsim_integration(fdm_version, capsys):
fg_to_py_port = 5000 + fdm_version # So that tests can run parallel
def test_jsbsim_integration(fdm_version, jsb_fg_version, capsys):
fg_to_py_port = 5000 + jsb_fg_version # So that tests can run parallel
fdm_conn = FDMConnection(fdm_version=fdm_version)
fdm_conn.connect_rx('localhost', fg_to_py_port, fdm_callback)

Expand All @@ -37,7 +46,7 @@ def test_jsbsim_integration(fdm_version, capsys):
Waypoint(46.765000, 7.626200, 3500),
],
flightgear_outputs=[
FlightGearUdpOutput('localhost', fg_to_py_port, update_rate, fg_version=fdm_version),
FlightGearUdpOutput('localhost', fg_to_py_port, update_rate, fg_version=jsb_fg_version),
],
time_step=jsb_time_step,
)
Expand All @@ -49,7 +58,7 @@ def test_jsbsim_integration(fdm_version, capsys):
pos_history = []
for sim_step_idx in range(total_sim_steps):
if not jsbfdm.run():
print(f'Test ended early {fdm_version}, {sim_step_idx}')
print(f'Test ended early {fdm_version}, {jsb_fg_version}, {sim_step_idx}')
assert False

assert fdm_conn.rx_proc.is_alive()
Expand Down
3 changes: 3 additions & 0 deletions tests/testing_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@


supported_fdm_versions = [
None, # Auto-version
24,
25,
]
supported_ctrls_versions = [
None, # Auto-version
27,
]
supported_gui_versions = [
None, # Auto-version
8,
]

Expand Down

0 comments on commit 9e51476

Please sign in to comment.