Skip to content

Commit

Permalink
Merge pull request #202 from emcek/gkey_from_yaml
Browse files Browse the repository at this point in the history
Load G-Keys configuration from YAML
emcek authored Oct 16, 2023

Verified

This commit was signed with the committer’s verified signature.
snyk-bot Snyk bot
2 parents 26906a8 + 89904eb commit fa9668f
Showing 39 changed files with 157,178 additions and 9,377 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -20,8 +20,11 @@ jobs:
uses: ./.github/actions/setup-python

- name: "Test with pytest"
env:
PYSIDE_DESIGNER_PLUGINS: .
PYTEST_QT_API: PySide6
run: |
python -m pytest -q --disable-warnings --cov=dcspy --cov-report=xml --cov-report=html --cov-report=term-missing -m "not dcsbios"
python -m pytest -q --disable-warnings --cov=dcspy --cov-report=xml --cov-report=html --cov-report=term-missing
- name: "Upload pytest results"
uses: actions/upload-artifact@v3
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -39,8 +39,9 @@ jobs:
- name: "Test with pytest"
env:
PYSIDE_DESIGNER_PLUGINS: .
PYTEST_QT_API: PySide6
run: |
python -m pytest -q -m "not dcsbios"
python -m pytest -q
- name: "Upload test results"
uses: actions/upload-artifact@v3
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## 3.0.0
* Use PySIde6 instead of Custom Tkinter framework
* Use PySide6 instead of Custom Tkinter framework
* Recognize Git objects for DCS-BIOS live repository
* Improve DCS-BIOS update process
* Add support for G-Keys of Logitech keyboards
27 changes: 0 additions & 27 deletions dcspy/__init__.py
Original file line number Diff line number Diff line change
@@ -9,15 +9,6 @@
from dcspy.models import LOCAL_APPDATA
from dcspy.utils import check_dcs_ver, get_default_yaml, load_yaml, set_defaults

try:
from typing import NotRequired
except ImportError:
from typing_extensions import NotRequired
try:
from typing import TypedDict
except ImportError:
from typing_extensions import TypedDict

__version__ = '3.0.0'

default_yaml = get_default_yaml(local_appdata=LOCAL_APPDATA)
@@ -44,21 +35,3 @@ def get_config_yaml_item(key: str, /, default: Optional[Union[str, int]] = None)
:return: value from configuration
"""
return load_yaml(full_path=default_yaml).get(key, default)


class IntBuffArgs(TypedDict):
address: int
mask: int
shift_by: int


class StrBuffArgs(TypedDict):
address: int
max_length: int


class BiosValue(TypedDict):
klass: str
args: Union[StrBuffArgs, IntBuffArgs]
value: Union[int, str]
max_value: NotRequired[int]
408 changes: 219 additions & 189 deletions dcspy/aircraft.py

Large diffs are not rendered by default.

21 changes: 15 additions & 6 deletions dcspy/logitech.py
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@
from dcspy.models import (SEND_ADDR, SUPPORTED_CRAFTS, Gkey, KeyboardModel, LcdButton, LcdColor, LcdMono, ModelG13, ModelG15v1, ModelG15v2, ModelG19, ModelG510,
generate_gkey)
from dcspy.sdk import key_sdk, lcd_sdk
from dcspy.utils import get_planes_list
from dcspy.utils import get_ctrl, get_full_bios_for_plane, get_planes_list

LOG = getLogger(__name__)

@@ -105,6 +105,7 @@ def detecting_plane(self, value: str) -> None:
self.plane_detected = True
elif self.plane_name not in SUPPORTED_CRAFTS and value in planes_list:
LOG.info(f'Basic supported aircraft: {value}')
self.plane_name = value
self.display = ['Detected aircraft:', value]
self.plane_detected = True
elif value not in planes_list:
@@ -120,15 +121,23 @@ def load_new_plane(self) -> None:
self.plane_detected = False
if self.plane_name in SUPPORTED_CRAFTS:
self.plane = getattr(import_module('dcspy.aircraft'), self.plane_name)(self.lcd)
LOG.debug(f'Dynamic load of: {self.plane_name} as {SUPPORTED_CRAFTS[self.plane_name]["name"]}')
for field_name, proto_data in self.plane.bios_data.items():
dcsbios_buffer = getattr(import_module('dcspy.dcsbios'), proto_data['klass'])
dcsbios_buffer(parser=self.parser, callback=partial(self.plane.set_bios, field_name), **proto_data['args'])
LOG.debug(f'Dynamic load of: {self.plane_name} as {SUPPORTED_CRAFTS[self.plane_name]["name"]} | BIOS: {self.plane.bios_name}')
self._setup_plane_callback()
else:
self.plane = MetaAircraft(self.plane_name, (BasicAircraft,), {})(self.lcd)
LOG.debug(f'Dynamic load of: {self.plane_name} as BasicAircraft')
LOG.debug(f'Dynamic load of: {self.plane_name} as BasicAircraft | BIOS: {self.plane.bios_name}') # todo: remove, check name
self.plane.bios_name = self.plane_name
LOG.debug(f'Dynamic load of: {self.plane_name} as BasicAircraft | BIOS: {self.plane.bios_name}')
LOG.debug(f'{repr(self)}')

def _setup_plane_callback(self):
"""Setups DCS-BIOS parser callbacks for detected plane."""
plane_bios = get_full_bios_for_plane(plane=SUPPORTED_CRAFTS[self.plane_name]['bios'], bios_dir=Path(str(get_config_yaml_item('dcsbios'))))
for ctrl_name in self.plane.bios_data:
ctrl = get_ctrl(ctrl_name=ctrl_name, plane_bios=plane_bios)
dcsbios_buffer = getattr(import_module('dcspy.dcsbios'), ctrl.output.klass)
dcsbios_buffer(parser=self.parser, callback=partial(self.plane.set_bios, ctrl_name), **ctrl.output.args.model_dump())

def check_buttons(self) -> LcdButton:
"""
Check if button was pressed and return it`s enum.
27 changes: 25 additions & 2 deletions dcspy/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from re import search
from tempfile import gettempdir
from typing import Any, Dict, Iterator, List, NamedTuple, Optional, Sequence, Tuple, Union

from PIL import ImageFont
from pydantic import BaseModel, RootModel, field_validator
from pydantic import BaseModel, ConfigDict, RootModel, field_validator

# Network
SEND_ADDR = ('127.0.0.1', 7778)
@@ -140,7 +141,7 @@ def validate_interface(cls, value):

class Output(BaseModel):
address: int
description: str
description: Optional[str] = None # workaround: for Ka-50 `definePushButtonLed`
suffix: str


@@ -435,6 +436,15 @@ def get(self, item, default=None):
return getattr(self.root, item, default)


class CycleButton(BaseModel):
"""Map BIOS key string with iterator to keep current value."""
model_config = ConfigDict(arbitrary_types_allowed=True)

ctrl_name: str
max_value: int = 1
iter: Iterator[int] = iter([0])


class GuiPlaneInputRequest(BaseModel):
identifier: str
request: str
@@ -550,6 +560,19 @@ def to_dict(self):
"""
return {'g_key': self.key, 'mode': self.mode}

@classmethod
def from_yaml(cls, /, yaml_str: str) -> 'Gkey':
"""
Construct Gkey from YAML string.
:param yaml_str: ex. G2_M1
:return: Gkey instance
"""
match = search(r'G(\d+)_M(\d+)', yaml_str)
if match:
return cls(*[int(i) for i in match.groups()])
raise ValueError(f'Invalid Gkey format: {yaml_str}. Expected: G<i>_M<j>')


def generate_gkey(key: int, mode: int) -> Sequence[Gkey]:
"""
39 changes: 24 additions & 15 deletions dcspy/qt_gui.py
Original file line number Diff line number Diff line change
@@ -25,14 +25,14 @@
QMessageBox, QProgressBar, QPushButton, QRadioButton, QSlider, QSpinBox, QStatusBar, QSystemTrayIcon, QTableWidget, QTabWidget,
QToolBar, QWidget)

from dcspy import qtgui_rc
from dcspy.models import (CTRL_LIST_SEPARATOR, DCS_BIOS_REPO_DIR, DCSPY_REPO_NAME, KEYBOARD_TYPES, LOCAL_APPDATA, ControlKeyData, FontsConfig,
GuiPlaneInputRequest, KeyboardModel, MsgBoxTypes, SystemData)
from dcspy import default_yaml, qtgui_rc
from dcspy.models import (CTRL_LIST_SEPARATOR, DCS_BIOS_REPO_DIR, DCSPY_REPO_NAME, KEYBOARD_TYPES, ControlKeyData, FontsConfig, GuiPlaneInputRequest,
KeyboardModel, MsgBoxTypes, SystemData)
from dcspy.starter import dcspy_run
from dcspy.utils import (ConfigDict, ReleaseInfo, check_bios_ver, check_dcs_bios_entry, check_dcs_ver, check_github_repo, check_ver_at_github,
collect_debug_data, defaults_cfg, download_file, get_all_git_refs, get_default_yaml, get_inputs_for_plane, get_list_of_ctrls,
get_plane_aliases, get_planes_list, get_version_string, is_git_exec_present, is_git_object, is_git_repo, load_yaml, proc_is_running,
run_pip_command, save_yaml)
collect_debug_data, defaults_cfg, download_file, get_all_git_refs, get_inputs_for_plane, get_list_of_ctrls, get_plane_aliases,
get_planes_list, get_version_string, is_git_exec_present, is_git_object, is_git_repo, load_yaml, proc_is_running, run_pip_command,
save_yaml)

_ = qtgui_rc # prevent to remove import statement accidentally
__version__ = '3.0.0'
@@ -71,10 +71,9 @@ def __init__(self, cfg_dict: Optional[ConfigDict] = None) -> None:
self.r_bios = version.Version('0.0.0')
self.systray = QSystemTrayIcon()
self.traymenu = QMenu()
self.cfg_file = get_default_yaml(local_appdata=LOCAL_APPDATA)
self.config = cfg_dict
if not cfg_dict:
self.config = load_yaml(full_path=self.cfg_file)
self.config = load_yaml(full_path=default_yaml)
self.dw_gkeys.hide()
self.dw_keyboard.hide()
self.dw_keyboard.setFloating(True)
@@ -304,7 +303,7 @@ def _load_table_gkeys(self) -> None:
self.tw_gkeys.setVerticalHeaderLabels([f'G{i}' for i in range(1, self.keyboard.gkeys + 1)])
self.tw_gkeys.setHorizontalHeaderLabels([f'M{i}' for i in range(1, self.keyboard.modes + 1)])

self.input_reqs[self.current_plane] = self._load_plane_yaml_into_dict(plane_yaml=self.cfg_file.parent / f'{self.current_plane}.yaml')
self.input_reqs[self.current_plane] = self._load_plane_yaml_into_dict(plane_yaml=default_yaml.parent / f'{self.current_plane}.yaml')

for row in range(0, self.keyboard.gkeys):
for col in range(0, self.keyboard.modes):
@@ -380,7 +379,7 @@ def _load_plane_yaml_into_dict(plane_yaml: Path) -> Dict[str, GuiPlaneInputReque
'TOGGLE': 'rb_action',
'INC': 'rb_fixed_step_inc',
'DEC': 'rb_fixed_step_dec',
'CYCLE_': 'rb_set_state',
'CYCLE': 'rb_set_state',
'+': 'rb_variable_step_plus',
'-': 'rb_variable_step_minus',
}
@@ -444,7 +443,7 @@ def _get_input_iface_dict_request(ctrl_key: ControlKeyData, rb_iface: str) -> Gu
'rb_action': f'{ctrl_key.name} TOGGLE',
'rb_fixed_step_inc': f'{ctrl_key.name} INC',
'rb_fixed_step_dec': f'{ctrl_key.name} DEC',
'rb_set_state': f'{ctrl_key.name} CYCLE_{ctrl_key.max_value}',
'rb_set_state': f'{ctrl_key.name} CYCLE {ctrl_key.max_value}',
'rb_variable_step_plus': f'{ctrl_key.name} +{ctrl_key.suggested_step}',
'rb_variable_step_minus': f'{ctrl_key.name} -{ctrl_key.suggested_step}'
}
@@ -526,7 +525,7 @@ def _save_gkeys_cfg(self) -> None:
request = ''
plane_cfg_yaml[g_key_id] = request
LOG.debug(f'Save {self.current_plane}:\n{pformat(plane_cfg_yaml)}')
save_yaml(data=plane_cfg_yaml, full_path=self.cfg_file.parent / f'{self.current_plane}.yaml')
save_yaml(data=plane_cfg_yaml, full_path=default_yaml.parent / f'{self.current_plane}.yaml')

def _save_current_cell(self, currentRow: int, currentColumn: int, previousRow: int, previousColumn: int) -> None:
"""
@@ -718,6 +717,11 @@ def _check_bios_git(self, silence=False) -> Tuple[str, str]:
return sha, install_result

def _error_during_bios_update(self, exc_tuple):
"""
Show message box with error details.
:param exc_tuple: Exception tuple
"""
self._done_event.set()
exc_type, exc_val, exc_tb = exc_tuple
LOG.debug(exc_tb)
@@ -727,6 +731,11 @@ def _error_during_bios_update(self, exc_tuple):
self._done_event.clear()

def _check_bios_git_completed(self, result):
"""
Show message box with installation details.
:param result:
"""
self._done_event.set()
sha, install_result = result
self.statusbar.showMessage(sha)
@@ -980,12 +989,12 @@ def save_configuration(self) -> None:
'font_mono_s': self.hs_medium_font.value(),
'font_mono_xs': self.hs_small_font.value()}
cfg.update(font_cfg)
save_yaml(data=cfg, full_path=self.cfg_file)
save_yaml(data=cfg, full_path=default_yaml)

def _reset_defaults_cfg(self) -> None:
"""Set defaults and stop application."""
save_yaml(data=defaults_cfg, full_path=self.cfg_file)
self.config = load_yaml(full_path=self.cfg_file)
save_yaml(data=defaults_cfg, full_path=default_yaml)
self.config = load_yaml(full_path=default_yaml)
self.apply_configuration(self.config)
for name in ['large', 'medium', 'small']:
getattr(self, f'hs_{name}_font').setValue(getattr(self, f'{self.keyboard.lcd}_font')[name])
12 changes: 5 additions & 7 deletions dcspy/utils.py
Original file line number Diff line number Diff line change
@@ -507,7 +507,7 @@ def load_json(path: Path) -> Dict[str, Any]:
return json.loads(data)


def get_full_bios_for_plane(plane: str, bios_dir: Path) -> Dict[str, Any]:
def get_full_bios_for_plane(plane: str, bios_dir: Path) -> Dict[str, Dict[str, ControlKeyData]]:
"""
Collect full BIOS for plane with name.
@@ -587,17 +587,15 @@ def get_plane_aliases(bios_dir: Path, plane: Optional[str] = None) -> Dict[str,
return aircraft_aliases


def get_ctrl(ctrl_name: str, plane: str, bios_dir: Path) -> Optional[Control]:
def get_ctrl(ctrl_name: str, plane_bios: Dict[str, Dict[str, ControlKeyData]]) -> Optional[Control]:
"""
Get Control object with name of plane.
Get Control dict for control name.
:param ctrl_name: Control name
:param plane: plane name
:param bios_dir: path to DCS-BIOS
:param plane_bios: dict with controls of plane
:return: Control instance
"""
json_data = get_full_bios_for_plane(plane=plane, bios_dir=bios_dir)
for section, controllers in json_data.items():
for section, controllers in plane_bios.items():
for ctrl, data in controllers.items():
if ctrl == ctrl_name:
return Control.model_validate(data)
5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -35,11 +35,10 @@ dependencies = [
'packaging==23.2',
'Pillow==10.1.0',
'psutil==5.9.6',
'pydantic>2.0.0',
'pydantic==2.4.2',
'PySide6==6.5.3',
'PyYAML==6.0.1',
'requests==2.31.0',
'typing-extensions==4.8.0; python_version < "3.11"',
]

[project.urls]
@@ -95,11 +94,9 @@ dcspy = [
]

[tool.pytest.ini_options]
qt_api = 'pyside6'
addopts = ['-q']
testpaths = ['tests']
markers = [
'dcsbios: marks tests for verfification of DCS-BIOS data',
'qt6: marks tests for Qt6 verfification'
]

3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -3,8 +3,7 @@ GitPython==3.1.37
packaging==23.2
Pillow==10.1.0
psutil==5.9.6
pydantic>2.0.0
pydantic==2.4.2
PySide6==6.5.3
PyYAML==6.0.1
requests==2.31.0
typing-extensions==4.8.0; python_version < '3.11'
63 changes: 51 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -3,27 +3,61 @@

from pytest import fixture

from dcspy import aircraft, models
from dcspy import aircraft, logitech, models
from dcspy.models import FontsConfig


def generate_fixture(plane_model, lcdtype):
def generate_plane_fixtures(plane, lcd_type_with_fonts):
"""
Generate fixtures for any plane with any lcd type.
:param plane: any plane object
:param lcd_type_with_fonts: lcd_type with fonts
"""
@fixture()
def _fixture():
return plane_model(lcdtype)
"""Fixture."""
return plane(lcd_type_with_fonts)
return _fixture


for plane in ['AdvancedAircraft', 'FA18Chornet', 'F16C50', 'F15ESE', 'Ka50', 'Ka503', 'Mi8MT', 'Mi24P', 'AH64DBLKII', 'A10C', 'A10C2', 'F14B', 'F14A135GR', 'AV8BNA']:
def generate_keyboard_fixtures(keyboard, lcd_font_setting):
"""
Generate fixtures for any keyboard and with lcd_font_setting.
:param keyboard: any keyboard object
:param lcd_font_setting: FontSetting object
"""
@fixture()
def _fixture():
"""Fixture."""
from dcspy.dcsbios import ProtocolParser
from dcspy.sdk import key_sdk, lcd_sdk

with patch.object(lcd_sdk, 'logi_lcd_init', return_value=True), \
patch.object(key_sdk, 'logi_gkey_init', return_value=True):
return keyboard(parser=ProtocolParser(), fonts=lcd_font_setting)
return _fixture


for plane_model in ['AdvancedAircraft', 'FA18Chornet', 'F16C50', 'F15ESE', 'Ka50', 'Ka503', 'Mi8MT', 'Mi24P', 'AH64DBLKII', 'A10C', 'A10C2', 'F14B', 'F14A135GR', 'AV8BNA']:
for lcd in ['LcdMono', 'LcdColor']:
airplane = getattr(aircraft, plane)
airplane = getattr(aircraft, plane_model)
lcd_type = getattr(models, lcd)
if lcd == 'LcdMono':
lcd_type.set_fonts(FontsConfig(name='consola.ttf', small=9, medium=11, large=16))
else:
lcd_type.set_fonts(FontsConfig(name='consola.ttf', small=18, medium=22, large=32))
name = f'{airplane.__name__.lower()}_{lcd_type.type.name.lower()}'
globals()[name] = generate_fixture(airplane, lcd_type)
globals()[name] = generate_plane_fixtures(airplane, lcd_type)

for keyboard_model in ['G13', 'G510', 'G15v1', 'G15v2', 'G19']:
key = getattr(logitech, keyboard_model)
if keyboard_model == 'G19':
lcd_font = FontsConfig(name='consola.ttf', small=18, medium=22, large=32)
else:
lcd_font = FontsConfig(name='consola.ttf', small=9, medium=11, large=16)
globals()[keyboard_model] = generate_keyboard_fixtures(key, lcd_font)


def pytest_addoption(parser) -> None:
@@ -67,11 +101,13 @@ def protocol_parser():
# <=><=><=><=><=> logitech <=><=><=><=><=>
@fixture()
def lcd_font_mono():
"""Returns font configuration for mono LCD."""
return FontsConfig(name='consola.ttf', small=9, medium=11, large=16)


@fixture()
def lcd_font_color(protocol_parser):
"""Returns font configuration for color LCD."""
return FontsConfig(name='consola.ttf', small=18, medium=22, large=32)


@@ -83,11 +119,10 @@ def keyboard_base(protocol_parser):
:param protocol_parser: instance of ProtocolParser
:return: KeyboardManager
"""
from dcspy.logitech import KeyboardManager
from dcspy.sdk import key_sdk, lcd_sdk
with patch.object(lcd_sdk, 'logi_lcd_init', return_value=True), \
patch.object(key_sdk, 'logi_gkey_init', return_value=True):
return KeyboardManager(protocol_parser)
return logitech.KeyboardManager(protocol_parser)


@fixture()
@@ -99,18 +134,20 @@ def keyboard_mono(protocol_parser, lcd_font_mono):
:param lcd_font_mono font configuration for LCD
:return: KeyboardManager
"""
from dcspy.logitech import KeyboardManager
from dcspy.models import LcdButton, LcdMono, generate_gkey
from dcspy.sdk import key_sdk, lcd_sdk

class Mono(KeyboardManager):
class Mono(logitech.KeyboardManager):
def __init__(self, parser, **kwargs) -> None:
LcdMono.set_fonts(kwargs['fonts'])
super().__init__(parser, lcd_type=LcdMono)
self.buttons = (LcdButton.ONE, LcdButton.TWO, LcdButton.THREE, LcdButton.FOUR)
self.gkey = generate_gkey(key=3, mode=1)
self.vert_space = 10

def _setup_plane_callback(self) -> None:
print('empty callback setup')

with patch.object(lcd_sdk, 'logi_lcd_init', return_value=True), \
patch.object(key_sdk, 'logi_gkey_init', return_value=True):
return Mono(parser=protocol_parser, fonts=lcd_font_mono)
@@ -125,18 +162,20 @@ def keyboard_color(protocol_parser, lcd_font_color):
:param lcd_font_color font configuration for LCD
:return: KeyboardManager
"""
from dcspy.logitech import KeyboardManager
from dcspy.models import LcdButton, LcdColor, generate_gkey
from dcspy.sdk import key_sdk, lcd_sdk

class Color(KeyboardManager):
class Color(logitech.KeyboardManager):
def __init__(self, parser, **kwargs) -> None:
LcdColor.set_fonts(kwargs['fonts'])
super().__init__(parser, lcd_type=LcdColor)
self.buttons = (LcdButton.LEFT, LcdButton.RIGHT, LcdButton.UP, LcdButton.DOWN, LcdButton.OK, LcdButton.CANCEL, LcdButton.MENU)
self.gkey = generate_gkey(key=3, mode=1)
self.vert_space = 40

def _setup_plane_callback(self) -> None:
print('empty callback setup')

with patch.object(lcd_sdk, 'logi_lcd_init', return_value=True), \
patch.object(key_sdk, 'logi_gkey_init', return_value=True):
return Color(parser=protocol_parser, fonts=lcd_font_color)
156 changes: 2 additions & 154 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,167 +1,15 @@
from datetime import datetime
from json import loads
from pathlib import Path
from tempfile import gettempdir
from typing import Dict, List, Tuple, Union
from typing import List, Tuple, Union
from unittest.mock import patch

from PIL import Image, ImageChops
from requests import exceptions, get

from dcspy import BiosValue, aircraft
from dcspy import aircraft
from dcspy.sdk import lcd_sdk

all_plane_list = ['fa18chornet', 'f16c50', 'f15ese', 'ka50', 'ka503', 'mi8mt', 'mi24p', 'ah64dblkii', 'a10c', 'a10c2', 'f14a135gr', 'f14b', 'av8bna']


def check_dcsbios_data(plane_bios: dict, plane_name: str, git_bios: bool) -> Tuple[dict, str]:
"""
Verify if all aircraft's data are correct with DCS-BIOS.
:param plane_bios: BIOS data from plane
:param plane_name: DCS-BIOS json filename
:param git_bios: use live/git DCS-BIOS version
:return: result of checks and DCS-BIOS version
"""
results, local_json = {}, {}
bios_ver = _get_dcs_bios_version(use_git=git_bios)
aircraft_aliases = _get_dcs_bios_json(json='AircraftAliases', bios_ver=bios_ver)
for json_file in aircraft_aliases[plane_name]:
local_json = {**local_json, **_get_dcs_bios_json(json=json_file, bios_ver=bios_ver)}
for bios_key in plane_bios:
bios_ref = _recursive_lookup(bios_key, local_json)
if not bios_ref:
results[bios_key] = f'Not found in DCS-BIOS {bios_ver}'
continue
output_type = plane_bios[bios_key]['klass'].split('Buffer')[0].lower()
try:
bios_outputs = [out for out in bios_ref['outputs'] if output_type == out['type']][0]
except IndexError:
results[bios_key] = f'Wrong output type: {output_type}'
continue
results = _compare_dcspy_with_bios(bios_key, bios_outputs, plane_bios, results)
return results, bios_ver


def _get_dcs_bios_version(use_git) -> str:
"""
Fetch stable or live DCS-BIOS version.
:param use_git: use live/git version
:return: version as string
"""
bios_ver = 'master'
if not use_git:
try:
response = get(url='https://api.github.com/repos/DCSFlightpanels/dcs-bios/releases/latest', timeout=2)
if response.status_code == 200:
bios_ver = response.json()['tag_name']
except exceptions.ConnectTimeout:
bios_ver = 'v0.7.48'
return bios_ver


def _compare_dcspy_with_bios(bios_key: str, bios_outputs: dict, plane_bios: dict, results: dict) -> dict:
"""
Compare DCS-BIOS and Plane data and return all differences.
:param bios_key: BIOS key
:param bios_outputs: DCS-BIOS outputs dict
:param plane_bios: BIOS data from plane
:param results: dict with differences
:return: updated dict with differences
"""
for args_key in plane_bios[bios_key]['args']:
aircraft_value = plane_bios[bios_key]['args'][args_key]
dcsbios_value = bios_outputs[args_key]
if aircraft_value != dcsbios_value:
bios_issue = {args_key: f'dcspy: {aircraft_value} ({hex(aircraft_value)}) '
f'bios: {dcsbios_value} ({hex(dcsbios_value)})'}
if results.get(bios_key):
results[bios_key].update(bios_issue)
else:
results[bios_key] = bios_issue
return results


def _get_dcs_bios_json(json: str, bios_ver: str) -> dict:
"""
Download json file for plane and write it to temporary directory.
Json is downloaded when:
* file doesn't exist
* file is older the one week
:param json: DCS-BIOS json filename
:param bios_ver: DCS-BIOS version
:return: json as dict
"""
plane_path = Path(gettempdir()) / f'{json}.json'
try:
m_time = plane_path.stat().st_mtime
week = datetime.fromtimestamp(int(m_time)).strftime('%U')
if week == datetime.now().strftime('%U'):
with open(plane_path, encoding='utf-8') as plane_json_file:
data = plane_json_file.read()
return loads(data)
raise ValueError('File is outdated')
except (FileNotFoundError, ValueError):
json_data = get(f'https://raw.githubusercontent.com/DCSFlightpanels/dcs-bios/{bios_ver}/Scripts/DCS-BIOS/doc/json/{json}.json')
with open(plane_path, 'wb+') as plane_json_file:
plane_json_file.write(json_data.content)
return loads(json_data.content)


def _recursive_lookup(search_key: str, bios_dict: dict) -> dict:
"""
Search for search_key recursively in dict and return its value.
:param search_key: search value for this key
:param bios_dict: dict to be search
:return: value (dict) for search_key
"""
if search_key in bios_dict:
return bios_dict[search_key]
for value in bios_dict.values():
if isinstance(value, dict):
item = _recursive_lookup(search_key, value)
if item:
return item


def generate_bios_data_for_plane(plane_bios: dict, plane_name: str, git_bios: bool) -> Dict[str, BiosValue]:
"""
Generate dict of BIOS values for plane.
:param plane_bios: BIOS data from plane
:param plane_name: BIOS plane name
:param git_bios: use live/git DCS-BIOS version
:return: dict of BIOS_VALUE for plane
"""
results = {}
bios_ver = _get_dcs_bios_version(use_git=git_bios)
local_json = _get_dcs_bios_json(json=plane_name, bios_ver=bios_ver)
for bios_key in plane_bios:
bios_ref = _recursive_lookup(search_key=bios_key, bios_dict=local_json)
if not bios_ref:
results[bios_key] = f'Not found in DCS-BIOS {bios_ver}'
continue
bios_outputs = bios_ref['outputs'][0]
buff_type = f'{bios_outputs["type"].capitalize()}Buffer'
if 'String' in buff_type:
results[bios_key] = {'klass': buff_type,
'args': {'address': hex(bios_outputs['address']),
'max_length': hex(bios_outputs['max_length'])},
'value': ''}
elif 'Integer' in buff_type:
results[bios_key] = {'klass': buff_type,
'args': {'address': hex(bios_outputs['address']),
'mask': hex(bios_outputs['mask']),
'shift_by': hex(bios_outputs['shift_by'])},
'value': int()}
return results


def set_bios_during_test(aircraft_model: aircraft.BasicAircraft, bios_pairs: List[Tuple[str, Union[str, int]]]) -> None:
"""
Set BIOS values for a given aircraft model.
12,871 changes: 7,581 additions & 5,290 deletions tests/resources/dcs_bios/doc/json/A-10C.json

Large diffs are not rendered by default.

17,310 changes: 17,310 additions & 0 deletions tests/resources/dcs_bios/doc/json/AH-64D.json

Large diffs are not rendered by default.

11,764 changes: 11,764 additions & 0 deletions tests/resources/dcs_bios/doc/json/AV8BNA.json

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion tests/resources/dcs_bios/doc/json/AircraftAliases.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
{
"A-10C": ["CommonData", "A-10C"],
"F-16C_50": ["CommonData", "F-16C_50"]
"A-10C_2": [ "CommonData", "A-10C" ],
"AH-64D_BLK_II": [ "CommonData", "AH-64D" ],
"AV8BNA": [ "CommonData", "AV8BNA" ],
"F-14A-135-GR": [ "CommonData", "F-14" ],
"F-14B": [ "CommonData", "F-14" ],
"F-15ESE": [ "CommonData", "F-15E" ],
"F-16C_50": [ "CommonData", "F-16C_50" ],
"FA-18C_hornet": [ "CommonData", "FA-18C_hornet" ],
"Ka-50": [ "CommonData", "Ka-50" ],
"Ka-50_3": [ "CommonData", "Ka-50" ],
"Mi-24P": [ "CommonData", "Mi-24P" ],
"Mi-8MT": [ "CommonData", "Mi-8MT" ]
}
372 changes: 222 additions & 150 deletions tests/resources/dcs_bios/doc/json/CommonData.json

Large diffs are not rendered by default.

24,314 changes: 24,314 additions & 0 deletions tests/resources/dcs_bios/doc/json/F-14.json

Large diffs are not rendered by default.

19,325 changes: 19,325 additions & 0 deletions tests/resources/dcs_bios/doc/json/F-15E.json

Large diffs are not rendered by default.

8,405 changes: 4,948 additions & 3,457 deletions tests/resources/dcs_bios/doc/json/F-16C_50.json

Large diffs are not rendered by default.

12,990 changes: 12,990 additions & 0 deletions tests/resources/dcs_bios/doc/json/FA-18C_hornet.json

Large diffs are not rendered by default.

14,315 changes: 14,315 additions & 0 deletions tests/resources/dcs_bios/doc/json/Ka-50.json

Large diffs are not rendered by default.

24,492 changes: 24,492 additions & 0 deletions tests/resources/dcs_bios/doc/json/Mi-24P.json

Large diffs are not rendered by default.

19,411 changes: 19,411 additions & 0 deletions tests/resources/dcs_bios/doc/json/Mi-8MT.json

Large diffs are not rendered by default.

Binary file modified tests/resources/linux/f14a135gr_color_F14A135GR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/resources/linux/f14a135gr_mono_F14A135GR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/resources/linux/f14b_color_F14B.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/resources/linux/f14b_mono_F14B.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/resources/win32/f14a135gr_color_F14A135GR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/resources/win32/f14a135gr_mono_F14A135GR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/resources/win32/f14b_color_F14B.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/resources/win32/f14b_mono_F14B.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions tests/test_aircraft.py
Original file line number Diff line number Diff line change
@@ -222,10 +222,10 @@ def test_button_pressed_for_apache_color(button, result, ah64dblkii_color):
def test_get_next_value_for_cycle_buttons(plane, btn_name, btn, values, request):
from itertools import cycle
plane = request.getfixturevalue(plane)
assert not all([isinstance(cyc_btn, cycle) for cyc_btn in plane.cycle_buttons.values()])
assert not all([isinstance(cyc_btn.iter, cycle) for cyc_btn in plane.cycle_buttons.values()])
for val in values:
assert plane.button_request(btn) == f'{btn_name} {val}\n'
assert isinstance(plane.cycle_buttons[btn]['iter'], cycle)
assert isinstance(plane.cycle_buttons[btn].iter, cycle)


# <=><=><=><=><=> Set BIOS <=><=><=><=><=>
@@ -265,7 +265,7 @@ def test_get_next_value_for_cycle_buttons(plane, btn_name, btn, values, request)
def test_set_bios_for_airplane(plane, bios_pairs, result, request):
plane = request.getfixturevalue(plane)
set_bios_during_test(plane, bios_pairs)
assert plane.bios_data[bios_pairs[0][0]]['value'] == result
assert plane.bios_data[bios_pairs[0][0]] == result


@mark.parametrize('plane, bios_pairs, mode', [
@@ -290,7 +290,7 @@ def test_prepare_image_for_all_planes(model, lcd, resources, img_precision, requ
bios_pairs = request.getfixturevalue(f'{model}_{lcd}_bios')
set_bios_during_test(aircraft_model, bios_pairs)
img = aircraft_model.prepare_image()
# if 'f15ese' in model or 'av8bna' in model:
# if 'f14b' in model or 'f14a135gr' in model:
# img.save(resources / platform / f'{model}_{lcd}_{type(aircraft_model).__name__}.png')
# else:
assert compare_images(img=img, file_path=resources / platform / f'{model}_{lcd}_{type(aircraft_model).__name__}.png', precision=img_precision)
20 changes: 0 additions & 20 deletions tests/test_bios.py

This file was deleted.

65 changes: 52 additions & 13 deletions tests/test_logitech.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

from pytest import mark

from dcspy.logitech import G13, G19
from dcspy.logitech import G13, G19, G510, G15v1, G15v2
from dcspy.models import FontsConfig, Gkey, LcdButton, LcdInfo, LcdMode, LcdType, generate_gkey


@@ -12,7 +12,7 @@ def test_keyboard_base_basic_check(keyboard_base):
assert str(keyboard_base) == 'KeyboardManager: 160x43'
logitech_repr = repr(keyboard_base)
data = ('parser', 'ProtocolParser', 'plane_name', 'plane_detected', 'lcdbutton_pressed', 'gkey_pressed', 'buttons',
'_display', 'plane', 'Aircraft', 'vert_space', 'lcd')
'_display', 'plane', 'BasicAircraft', 'vert_space', 'lcd', 'LcdInfo', 'gkey', 'buttons', 'model', 'KeyboardModel')
for test_string in data:
assert test_string in logitech_repr

@@ -97,7 +97,7 @@ def test_keyboard_button_handle_gkey(keyboard, sock, request):
('F-14B', 'F14B', ['Detected aircraft:', 'F-14B Tomcat'], True),
('AV8BNA', 'AV8BNA', ['Detected aircraft:', 'AV-8B N/A Harrier'], True),
('SpitfireLFMkIX', 'SpitfireLFMkIX', ['Detected aircraft:', 'SpitfireLFMkIX'], True),
('F-22A', 'F22A', ['Detected aircraft:', 'F-22A'], True),
('F-22A', 'F-22A', ['Detected aircraft:', 'F-22A'], True),
('A-10A', 'A10A', ['Detected aircraft:', 'A-10A', 'Not supported yet!'], False),
('F-117_Nighthawk', 'F117Nighthawk', ['Detected aircraft:', 'F-117_Nighthawk', 'Not supported yet!'], False),
('', '', [], False),
@@ -134,10 +134,16 @@ def test_keyboard_mono_detecting_plane(plane_str, plane, display, detect, keyboa

@mark.parametrize('mode, size, lcd_type, lcd_font, keyboard', [
(LcdMode.BLACK_WHITE, (160, 43), LcdType.MONO, FontsConfig(name='consola.ttf', small=9, medium=11, large=16), G13),
(LcdMode.BLACK_WHITE, (160, 43), LcdType.MONO, FontsConfig(name='consola.ttf', small=9, medium=11, large=16), G510),
(LcdMode.BLACK_WHITE, (160, 43), LcdType.MONO, FontsConfig(name='consola.ttf', small=9, medium=11, large=16), G15v1),
(LcdMode.BLACK_WHITE, (160, 43), LcdType.MONO, FontsConfig(name='consola.ttf', small=9, medium=11, large=16), G15v2),
(LcdMode.TRUE_COLOR, (320, 240), LcdType.COLOR, FontsConfig(name='consola.ttf', small=18, medium=22, large=32), G19),
], ids=[
'Mono Keyboard',
'Color Keyboard',
'Mono G13',
'Mono G510',
'Mono G15v1',
'Mono G15v2',
'Color G19',
])
def test_check_keyboard_display_and_prepare_image(mode, size, lcd_type, lcd_font, keyboard, protocol_parser):
from dcspy.aircraft import BasicAircraft
@@ -160,10 +166,16 @@ def test_check_keyboard_display_and_prepare_image(mode, size, lcd_type, lcd_font

@mark.parametrize('lcd_font, keyboard', [
(FontsConfig(name='consola.ttf', small=9, medium=11, large=16), G13),
(FontsConfig(name='consola.ttf', small=9, medium=11, large=16), G510),
(FontsConfig(name='consola.ttf', small=9, medium=11, large=16), G15v1),
(FontsConfig(name='consola.ttf', small=9, medium=11, large=16), G15v2),
(FontsConfig(name='consola.ttf', small=18, medium=22, large=32), G19)
], ids=[
'Mono Keyboard',
'Color Keyboard'
'Mono G13',
'Mono G510',
'Mono G15v1',
'Mono G15v2',
'Color G19',
])
def test_check_keyboard_text(lcd_font, keyboard, protocol_parser):
from dcspy.sdk import lcd_sdk
@@ -181,11 +193,38 @@ def test_check_keyboard_text(lcd_font, keyboard, protocol_parser):
])
def test_keyboard_mono_load_plane(model, keyboard_mono):
from dcspy.aircraft import AdvancedAircraft
from dcspy.sdk import lcd_sdk
with patch.object(lcd_sdk, 'logi_lcd_is_connected', return_value=True), \
patch.object(lcd_sdk, 'logi_lcd_mono_set_background', return_value=True), \
patch.object(lcd_sdk, 'logi_lcd_update', return_value=True):
keyboard_mono.plane_name = model
keyboard_mono.load_new_plane()

keyboard_mono.plane_name = model
keyboard_mono.load_new_plane()
assert isinstance(keyboard_mono.plane, AdvancedAircraft)
assert model in type(keyboard_mono.plane).__name__


@mark.parametrize('model', [
'FA18Chornet', 'F16C50', 'F15ESE', 'Ka50', 'Ka503', 'Mi8MT', 'Mi24P', 'AH64DBLKII', 'A10C', 'A10C2', 'F14A135GR', 'F14B', 'AV8BNA',
])
def test_keyboard_color_load_plane(model, keyboard_color):
from dcspy.aircraft import AdvancedAircraft

keyboard_color.plane_name = model
keyboard_color.load_new_plane()
assert isinstance(keyboard_color.plane, AdvancedAircraft)
assert model in type(keyboard_color.plane).__name__


@mark.parametrize('model', [
'FA18Chornet', 'F16C50', 'F15ESE', 'Ka50', 'Ka503', 'Mi8MT', 'Mi24P', 'AH64DBLKII', 'A10C', 'A10C2', 'F14A135GR', 'F14B', 'AV8BNA'
])
@mark.parametrize('keyboard', [
'G13', 'G510', 'G15v1', 'G15v2', 'G19'
])
def test_all_keyboard_all_plane_load(model, keyboard, resources, request):
from dcspy.aircraft import AdvancedAircraft

keyboard = request.getfixturevalue(keyboard)
with patch('dcspy.logitech.get_config_yaml_item', return_value=resources / 'dcs_bios'):
keyboard.plane_name = model
keyboard.load_new_plane()

assert isinstance(keyboard.plane, AdvancedAircraft)
assert model in type(keyboard.plane).__name__
40 changes: 39 additions & 1 deletion tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pytest import mark
from pytest import mark, raises

AAP_PAGE = {
'category': 'AAP',
@@ -318,3 +318,41 @@ def test_get_next_value_for_button(control, curr_val, results):
assert seed == results[2]
assert cycle_btn['Button1']['bios'] == results[3]
assert isinstance(cycle_btn['Button1']['iter'], Iterable)


def test_gkey_from_yaml_success():
from dcspy.models import Gkey

gkey = Gkey.from_yaml('G22_M3')
assert gkey.key == 22
assert gkey.mode == 3


def test_gkey_from_yaml_value_error():
from dcspy.models import Gkey

with raises(ValueError):
_ = Gkey.from_yaml('G_M1')


def test_cycle_button_default_iter():
from dcspy.models import CycleButton

cb = CycleButton(ctrl_name='AAP_PAGE')
assert cb.max_value == 1
with raises(StopIteration):
next(cb.iter)
next(cb.iter)


def test_cycle_button_custom_iter():
from dcspy.models import CycleButton

max_val = 2
cb = CycleButton(ctrl_name='AAP_PAGE', max_value=max_val, iter=iter(range(max_val + 1)))
assert cb.max_value == max_val
with raises(StopIteration):
assert next(cb.iter) == 0
assert next(cb.iter) == 1
assert next(cb.iter) == 2
next(cb.iter)
24 changes: 12 additions & 12 deletions tests/test_qtgui.py
Original file line number Diff line number Diff line change
@@ -18,16 +18,16 @@ def test_qt(qtbot, resources, switch_dcs_bios_path_in_config):
QQuickWindow.setGraphicsApi(QSGRendererInterface.OpenGLRhi)
QtCore.QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts)

with patch.object(qt_gui, 'get_default_yaml', return_value=resources / 'c.yml'):
with patch('dcspy.qt_gui.default_yaml', resources / 'c.yml'):
dcspy_gui = qt_gui.DcsPyQtGui()
dcspy_gui.show()
qtbot.addWidget(dcspy_gui)
dcspy_gui.rb_g19.setChecked(True)
dcspy_gui.sp_completer.setValue(10)
dcspy_gui.combo_planes.setCurrentIndex(1)
sleep(0.7)
qtbot.mouseClick(dcspy_gui.pb_save, Qt.LeftButton)
qtbot.mouseClick(dcspy_gui.pb_start, Qt.LeftButton)
qtbot.mouseClick(dcspy_gui.pb_stop, Qt.LeftButton)
qtbot.mouseClick(dcspy_gui.pb_close, Qt.LeftButton)
sleep(0.7)
dcspy_gui.show()
qtbot.addWidget(dcspy_gui)
dcspy_gui.rb_g19.setChecked(True)
dcspy_gui.sp_completer.setValue(10)
dcspy_gui.combo_planes.setCurrentIndex(1)
sleep(0.7)
qtbot.mouseClick(dcspy_gui.pb_save, Qt.LeftButton)
qtbot.mouseClick(dcspy_gui.pb_start, Qt.LeftButton)
qtbot.mouseClick(dcspy_gui.pb_stop, Qt.LeftButton)
qtbot.mouseClick(dcspy_gui.pb_close, Qt.LeftButton)
sleep(0.7)
45 changes: 37 additions & 8 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from os import makedirs
from os import linesep, makedirs
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock, mock_open, patch

@@ -257,15 +257,15 @@ def test_collect_debug_data():
def test_run_pip_command_success():
rc, err, out = utils.run_pip_command('list')
assert rc == 0
assert err == ''
assert out
assert 'pip' in out, out
assert err == '' or err == f'WARNING: There was an error checking the latest version of pip.{linesep}', err


def test_run_pip_command_failed():
rc, err, out = utils.run_pip_command('bullshit')
assert rc == 1
assert err != ''
assert out == ''
assert out == '', out
assert err != '', err


def test_get_full_bios_for_plane(resources):
@@ -295,7 +295,21 @@ def test_get_inputs_for_wrong_plane(resources):

def test_get_plane_aliases_all(resources):
s = utils.get_plane_aliases(bios_dir=resources / 'dcs_bios')
assert s == {'A-10C': ['CommonData', 'A-10C'], 'F-16C_50': ['CommonData', 'F-16C_50']}
assert s == {
'A-10C': ['CommonData', 'A-10C'],
'A-10C_2': ['CommonData', 'A-10C'],
'AH-64D_BLK_II': ['CommonData', 'AH-64D'],
'AV8BNA': ['CommonData', 'AV8BNA'],
'F-14A-135-GR': ['CommonData', 'F-14'],
'F-14B': ['CommonData', 'F-14'],
'F-15ESE': ['CommonData', 'F-15E'],
'F-16C_50': ['CommonData', 'F-16C_50'],
'FA-18C_hornet': ['CommonData', 'FA-18C_hornet'],
'Ka-50': ['CommonData', 'Ka-50'],
'Ka-50_3': ['CommonData', 'Ka-50'],
'Mi-24P': ['CommonData', 'Mi-24P'],
'Mi-8MT': ['CommonData', 'Mi-8MT'],
}


def test_get_plane_aliases_one_plane(resources):
@@ -310,10 +324,25 @@ def test_get_plane_aliases_wrong_plane(resources):

def test_get_planes_list(resources):
plane_list = utils.get_planes_list(bios_dir=resources / 'dcs_bios')
assert plane_list == ['A-10C', 'F-16C_50'], plane_list
assert plane_list == [
'A-10C',
'A-10C_2',
'AH-64D_BLK_II',
'AV8BNA',
'F-14A-135-GR',
'F-14B',
'F-15ESE',
'F-16C_50',
'FA-18C_hornet',
'Ka-50',
'Ka-50_3',
'Mi-24P',
'Mi-8MT',
]


def test_get_ctrl(resources):
c = utils.get_ctrl(ctrl_name='TACAN_MODE', bios_dir=resources / 'dcs_bios', plane='A-10C')
json_data = utils.get_full_bios_for_plane(plane='A-10C', bios_dir=resources / 'dcs_bios')
c = utils.get_ctrl(ctrl_name='TACAN_MODE', plane_bios=json_data)
assert c.output.max_value == 4
assert c.input.one_input is False

0 comments on commit fa9668f

Please sign in to comment.