• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

emcek / dcspy / 9231915424

25 May 2024 01:35AM UTC coverage: 96.349% (+0.02%) from 96.326%
9231915424

Pull #313

github

emcek
upate chnagelog
Pull Request #313: F-4E Phantom II

37 of 39 new or added lines in 6 files covered. (94.87%)

12 existing lines in 1 file now uncovered.

2032 of 2109 relevant lines covered (96.35%)

0.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.3
/dcspy/aircraft.py
1
from collections.abc import Sequence
1✔
2
from enum import Enum
1✔
3
from itertools import cycle
1✔
4
from logging import getLogger
1✔
5
from pathlib import Path
1✔
6
from pprint import pformat
1✔
7
from re import search
1✔
8
from tempfile import gettempdir
1✔
9

10
try:
1✔
11
    from typing import Union, Unpack
1✔
12
except ImportError:
×
13
    from typing_extensions import Unpack
×
14

NEW
15
    from typing import Union
×
16

17
from PIL import Image, ImageDraw, ImageFont
1✔
18

19
from dcspy import default_yaml, load_yaml
1✔
20
from dcspy.models import DEFAULT_FONT_NAME, NO_OF_LCD_SCREENSHOTS, AircraftKwargs, Gkey, LcdButton, LcdInfo, LcdType, MouseButton, RequestModel, RequestType
1✔
21
from dcspy.utils import KeyRequest, replace_symbols, substitute_symbols
1✔
22

23
LOG = getLogger(__name__)
1✔
24

25

26
class MetaAircraft(type):
1✔
27
    """Metaclass for all BasicAircraft."""
28
    def __new__(cls, name: str, bases: tuple[type, ...], namespace: dict):
1✔
29
        """
30
        Create new instance of any plane as BasicAircraft.
31

32
        You can crate instance of any plane:
33
        f22a = MetaAircraft('F-22A', (BasicAircraft,), {})(lcd_type: LcdInfo)
34

35
        :param name:
36
        :param bases:
37
        :param namespace:
38
        """
39
        return super().__new__(cls, name, bases, namespace)
1✔
40

41
    def __call__(cls, *args, **kwargs):
1✔
42
        """
43
        Create new instance of any BasicAircraft.
44

45
        :param args:
46
        :param kwargs:
47
        """
48
        LOG.debug(f'Creating {cls.__name__} with: {args[0].type}')
1✔
49
        return super().__call__(*args, **kwargs)
1✔
50

51

52
class BasicAircraft:
1✔
53
    """Basic Aircraft."""
54
    bios_name: str = ''
1✔
55

56
    def __init__(self, lcd_type: LcdInfo) -> None:
1✔
57
        """
58
        Create basic aircraft.
59

60
        :param lcd_type: LCD type
61
        """
62
        self.lcd = lcd_type
1✔
63
        self.cfg = load_yaml(full_path=default_yaml)
1✔
64
        self.bios_data: dict[str, Union[str, int]] = {}
1✔
65
        if self.bios_name:
1✔
66
            self.key_req = KeyRequest(yaml_path=default_yaml.parent / f'{self.bios_name}.yaml', get_bios_fn=self.get_bios)
1✔
67
            self.bios_data.update(self.key_req.cycle_button_ctrl_name)
1✔
68

69
    def button_request(self, button: Union[LcdButton, Gkey, MouseButton]) -> RequestModel:
1✔
70
        """
71
        Prepare aircraft specific DCS-BIOS request for button pressed.
72

73
        :param button: LcdButton, Gkey or MouseButton
74
        :return: RequestModel object
75
        """
76
        LOG.debug(f'{type(self).__name__} Button: {button}')
1✔
77
        request = self.key_req.get_request(button)
1✔
78
        LOG.debug(f'Request: {request}')
1✔
79
        return request
1✔
80

81
    def set_bios(self, selector: str, value: Union[str, int]) -> None:
1✔
82
        """
83
        Set value for DCS-BIOS selector.
84

85
        :param selector:
86
        :param value:
87
        """
88
        self.bios_data[selector] = value
1✔
89
        LOG.debug(f'{type(self).__name__} {selector} value: "{value}" ({type(value).__name__})')
1✔
90

91
    def get_bios(self, selector: str, default: Union[str, int, float] = '') -> Union[str, int, float]:
1✔
92
        """
93
        Get value for DCS-BIOS selector.
94

95
        :param selector: name of selector
96
        :param default: return this when fetch fail
97
        """
98
        try:
1✔
99
            return type(default)(self.bios_data[selector])
1✔
100
        except (KeyError, ValueError):
1✔
101
            return default
1✔
102

103
    def __repr__(self) -> str:
1✔
104
        return f'{super().__repr__()} with: {pformat(self.__dict__)}'
1✔
105

106

107
class AdvancedAircraft(BasicAircraft):
1✔
108
    """Advanced Aircraft."""
109
    def __init__(self, lcd_type: LcdInfo, **kwargs: Unpack[AircraftKwargs]) -> None:
1✔
110
        """
111
        Create advanced aircraft.
112

113
        :param lcd_type: LCD type
114
        """
115
        super().__init__(lcd_type=lcd_type)
1✔
116
        self.update_display = kwargs.get('update_display', None)
1✔
117
        if self.update_display:
1✔
118
            self.bios_data.update(kwargs.get('bios_data', {}))
1✔
119
        self._debug_img = cycle([f'{x:03}' for x in range(NO_OF_LCD_SCREENSHOTS)])
1✔
120

121
    def set_bios(self, selector: str, value: Union[str, int]) -> None:
1✔
122
        """
123
        Set value for DCS-BIOS selector and update LCD with image.
124

125
        :param selector:
126
        :param value:
127
        """
128
        super().set_bios(selector=selector, value=value)
1✔
129
        if self.update_display:
1✔
130
            self.update_display(self.prepare_image())
1✔
131

132
    def prepare_image(self) -> Image.Image:
1✔
133
        """
134
        Prepare image to be sent to correct type of LCD.
135

136
        :return: image instance ready display on LCD
137
        """
138
        img = Image.new(mode=self.lcd.mode.value, size=(self.lcd.width.value, self.lcd.height.value), color=self.lcd.background)
1✔
139
        getattr(self, f'draw_for_lcd_{self.lcd.type.name.lower()}')(img)
1✔
140
        if self.cfg.get('save_lcd', False):
1✔
141
            screen_shot_file = f'{type(self).__name__}_{next(self._debug_img)}.png'
1✔
142
            img.save(Path(gettempdir()) / screen_shot_file, 'PNG')
1✔
143
            LOG.debug(f'Save screenshot: {screen_shot_file}')
1✔
144
        return img
1✔
145

146
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
147
        """Prepare image for Aircraft for Mono LCD."""
148
        raise NotImplementedError
1✔
149

150
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
151
        """Prepare image for Aircraft for Color LCD."""
152
        raise NotImplementedError
1✔
153

154

155
class FA18Chornet(AdvancedAircraft):
1✔
156
    """F/A-18C Hornet."""
157
    bios_name: str = 'FA-18C_hornet'
1✔
158

159
    def __init__(self, lcd_type: LcdInfo, **kwargs: Unpack[AircraftKwargs]) -> None:
1✔
160
        """
161
        Create F/A-18C Hornet.
162

163
        :param lcd_type: LCD type
164
        """
165
        kwargs['bios_data'] = {
1✔
166
            'UFC_SCRATCHPAD_STRING_1_DISPLAY': '',
167
            'UFC_SCRATCHPAD_STRING_2_DISPLAY': '',
168
            'UFC_SCRATCHPAD_NUMBER_DISPLAY': '',
169
            'UFC_OPTION_DISPLAY_1': '',
170
            'UFC_OPTION_DISPLAY_2': '',
171
            'UFC_OPTION_DISPLAY_3': '',
172
            'UFC_OPTION_DISPLAY_4': '',
173
            'UFC_OPTION_DISPLAY_5': '',
174
            'UFC_COMM1_DISPLAY': '',
175
            'UFC_COMM2_DISPLAY': '',
176
            'UFC_OPTION_CUEING_1': '',
177
            'UFC_OPTION_CUEING_2': '',
178
            'UFC_OPTION_CUEING_3': '',
179
            'UFC_OPTION_CUEING_4': '',
180
            'UFC_OPTION_CUEING_5': '',
181
            'IFEI_FUEL_DOWN': '',
182
            'IFEI_FUEL_UP': '',
183
            'HUD_ATT_SW': int(),
184
            'IFEI_DWN_BTN': int(),
185
            'IFEI_UP_BTN': int(),
186
        }
187
        super().__init__(lcd_type=lcd_type, **kwargs)
1✔
188

189
    def _draw_common_data(self, draw: ImageDraw.ImageDraw, scale: int) -> ImageDraw.ImageDraw:
1✔
190
        """
191
        Draw common part (based on scale) for Mono and Color LCD.
192

193
        :param draw: ImageDraw instance
194
        :param scale: scaling factor (Mono 1, Color 2)
195
        :return: updated image to draw
196
        """
197
        scratch_1 = self.get_bios('UFC_SCRATCHPAD_STRING_1_DISPLAY')
1✔
198
        scratch_2 = self.get_bios('UFC_SCRATCHPAD_STRING_2_DISPLAY')
1✔
199
        scratch_num = self.get_bios('UFC_SCRATCHPAD_NUMBER_DISPLAY')
1✔
200
        draw.text(xy=(0, 0), fill=self.lcd.foreground, font=self.lcd.font_l,
1✔
201
                  text=f'{scratch_1}{scratch_2}{scratch_num}')
202
        draw.line(xy=(0, 20 * scale, 115 * scale, 20 * scale), fill=self.lcd.foreground, width=1)
1✔
203

204
        draw.rectangle(xy=(0, 29 * scale, 20 * scale, 42 * scale), fill=self.lcd.background, outline=self.lcd.foreground)
1✔
205
        draw.text(xy=(2 * scale, 29 * scale), text=str(self.get_bios('UFC_COMM1_DISPLAY')), fill=self.lcd.foreground, font=self.lcd.font_l)
1✔
206

207
        offset = 44 * scale
1✔
208
        draw.rectangle(xy=(139 * scale - offset, 29 * scale, 159 * scale - offset, 42 * scale), fill=self.lcd.background, outline=self.lcd.foreground)
1✔
209
        draw.text(xy=(140 * scale - offset, 29 * scale), text=str(self.get_bios('UFC_COMM2_DISPLAY')), fill=self.lcd.foreground, font=self.lcd.font_l)
1✔
210

211
        for i in range(1, 6):
1✔
212
            offset = (i - 1) * 8 * scale
1✔
213
            draw.text(xy=(120 * scale, offset), fill=self.lcd.foreground, font=self.lcd.font_s,
1✔
214
                      text=f'{i}{self.get_bios(f"UFC_OPTION_CUEING_{i}")}{self.get_bios(f"UFC_OPTION_DISPLAY_{i}")}')
215

216
        draw.text(xy=(36 * scale, 29 * scale), text=str(self.get_bios('IFEI_FUEL_UP')), fill=self.lcd.foreground, font=self.lcd.font_l)
1✔
217
        return draw
1✔
218

219
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
220
        """Prepare image for F/A-18C Hornet for Mono LCD."""
221
        self._draw_common_data(draw=ImageDraw.Draw(img), scale=1)
1✔
222

223
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
224
        """Prepare image for F/A-18C Hornet for Color LCD."""
225
        draw = self._draw_common_data(draw=ImageDraw.Draw(img), scale=2)
1✔
226
        draw.text(xy=(72, 100), text=str(self.get_bios('IFEI_FUEL_DOWN')), fill=self.lcd.foreground, font=self.lcd.font_l)
1✔
227

228
    def set_bios(self, selector: str, value: Union[str, int]) -> None:
1✔
229
        """
230
        Set new data.
231

232
        :param selector:
233
        :param value:
234
        """
235
        if selector in ('UFC_SCRATCHPAD_STRING_1_DISPLAY', 'UFC_SCRATCHPAD_STRING_2_DISPLAY',
1✔
236
                        'UFC_COMM1_DISPLAY', 'UFC_COMM2_DISPLAY'):
237
            value = str(value).replace('`', '1').replace('~', '2')
1✔
238
        super().set_bios(selector, value)
1✔
239

240

241
class F16C50(AdvancedAircraft):
1✔
242
    """F-16C Viper."""
243
    bios_name: str = 'F-16C_50'
1✔
244
    # List page
245
    COMMON_SYMBOLS_TO_REPLACE = (
1✔
246
        ('A\x10\x04', ''), ('\x82', ''), ('\x03', ''), ('\x02', ''), ('\x80', ''), ('\x08', ''), ('\x10', ''),
247
        ('\x07', ''), ('\x0f', ''), ('\xfe', ''), ('\xfc', ''), ('\x03', ''), ('\xff', ''), ('\xc0', '')
248
    )
249
    # degree sign, 'a' to up-down arrow 2195 or black diamond 2666, INVERSE WHITE CIRCLE
250
    MONO_SYMBOLS_TO_REPLACE = (('o', '\u00b0'), ('a', '\u2666'), ('*', '\u25d9'))
1✔
251
    # degree sign, fix up-down triangle arrow, fix to inverse star
252
    COLOR_SYMBOLS_TO_REPLACE = (('o', '\u005e'), ('a', '\u0040'), ('*', '\u00d7'))
1✔
253
    COLOR_SYMBOLS_TO_SUBSTITUTE = (
1✔
254
        (r'1DEST\s2BNGO\s3VIP\s{2}RINTG', '\u00c1DEST \u00c2BNGO \u00c3VIP  \u0072INTG'),
255
        (r'4NAV\s{2}5MAN\s{2}6INS\s{2}EDLNK', '\u00c4NAV  \u00c5MAN  \u00c6INS  \u0065DLNK'),
256
        (r'7CMDS\s8MODE\s9VRP\s{2}0MISC', '\u00c7CMDS \u00c8MODE \u00c9VRP  \u00c0MISC'),
257
        (r'1CORR\s2MAGV\s3OFP\s{2}RHMCS', '\u00c1CORR \u00c2MAGV \u00c3OFP  \u0072HMCS'),
258
        (r'4INSM\s5LASR\s6GPS\s{2}E', '\u00c4INSM \u00c5LASR \u00c6GPS  \u0065'),
259
        (r'7DRNG\s8BULL\s9\s{5}0', '\u00c7DRNG \u00c8BULL \u00c9     \u00c0'),
260
        (r'(M1\s:\d+\s+)M4(\s+\(\d\).*)', r'\1mÄ\2'),
261
        (r'M1(\s:\d+\s+)M4(\s+:\s+\(\d\).*)', r'mÁ\1mÄ\2'),
262
        (r'M3(\s+:\d+\s+×\s+\d×[A-Z]+\(\d\).*)', r'mÃ\1'),
263
        (r'(\s[\s|×])HUD BLNK([×|\s]\s+)', r'\1hud blnk\2'),
264
        (r'(\s[\s|×])CKPT BLNK([×|\s]\s+)', r'\1ckpt blnk\2')
265
    )
266

267
    def __init__(self, lcd_type: LcdInfo, **kwargs: Unpack[AircraftKwargs]) -> None:
1✔
268
        """
269
        Create F-16C Viper.
270

271
        :param lcd_type: LCD type
272
        """
273
        kwargs['bios_data'] = {f'DED_LINE_{i}': '' for i in range(1, 6)}
1✔
274
        super().__init__(lcd_type=lcd_type, **kwargs)
1✔
275
        self.font = self.lcd.font_s
1✔
276
        self.ded_font = self.cfg.get('f16_ded_font', True)
1✔
277
        if self.ded_font and self.lcd.type == LcdType.COLOR:
1✔
278
            self.font = ImageFont.truetype(str((Path(__file__) / '..' / 'resources' / 'falconded.ttf').resolve()), 25)
1✔
279

280
    def _draw_common_data(self, draw: ImageDraw.ImageDraw, separation: int) -> None:
1✔
281
        """
282
        Draw common part (based on scale) for Mono and Color LCD.
283

284
        :param draw: ImageDraw instance
285
        :param separation: between lines in pixels
286
        """
287
        for i in range(1, 6):
1✔
288
            offset = (i - 1) * separation
1✔
289
            draw.text(xy=(0, offset), text=str(self.get_bios(f'DED_LINE_{i}')), fill=self.lcd.foreground, font=self.font)
1✔
290

291
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
292
        """Prepare image for F-16C Viper for Mono LCD."""
293
        self._draw_common_data(draw=ImageDraw.Draw(img), separation=8)
1✔
294

295
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
296
        """Prepare image for F-16C Viper for Color LCD."""
297
        self._draw_common_data(draw=ImageDraw.Draw(img), separation=24)
1✔
298

299
    def set_bios(self, selector: str, value: Union[str, int]) -> None:
1✔
300
        """
301
        Catch BIOS changes and remove garbage characters and replace with correct ones.
302

303
        :param selector: selector name
304
        :param value: value form DCS-BIOS
305
        """
306
        if 'DED_LINE_' in selector:
1✔
307
            value = str(value)
1✔
308
            LOG.debug(f'{type(self).__name__} {selector} org  : "{value}"')
1✔
309
            value = self._clean_and_replace(value)
1✔
310
        super().set_bios(selector, value)
1✔
311

312
    def _clean_and_replace(self, value: str) -> str:
1✔
313
        """
314
        Clean and replace garbage characters before print to LCD.
315

316
        :param value: The string value to be cleaned and replaced.
317
        :return: The cleaned and replaced string value.
318
        """
319
        value = replace_symbols(value, self.COMMON_SYMBOLS_TO_REPLACE)
1✔
320
        if value and value[-1] == '@':
1✔
321
            value = value.replace('@', '')  # List - 6
1✔
322
        if self.lcd.type == LcdType.MONO:
1✔
323
            value = self._replace_symbols_for_mono_lcd(value)
1✔
324
        elif self.ded_font and self.lcd.type == LcdType.COLOR:
1✔
325
            value = self._replace_symbols_for_color_lcd(value)
1✔
326
        return value
1✔
327

328
    def _replace_symbols_for_mono_lcd(self, value: str) -> str:
1✔
329
        """
330
        Clean and replace garbage characters for Mono LCD.
331

332
        :param value: The input string that needs to be modified.
333
        :return: The modified string after replacing symbols based on the MONO_SYMBOLS_TO_REPLACE dictionary.
334
        """
335
        return replace_symbols(value, self.MONO_SYMBOLS_TO_REPLACE)
1✔
336

337
    def _replace_symbols_for_color_lcd(self, value: str) -> str:
1✔
338
        """
339
        Clean and replace garbage characters for Color LCD.
340

341
        :param value: The input string value.
342
        :return: The modified string with replaced symbols.
343
        """
344
        value = replace_symbols(value, self.COLOR_SYMBOLS_TO_REPLACE)
1✔
345
        value = substitute_symbols(value, self.COLOR_SYMBOLS_TO_SUBSTITUTE)
1✔
346
        return value
1✔
347

348

349
class F4E45MC(AdvancedAircraft):
1✔
350
    """F-4E Phantom II."""
351
    bios_name: str = 'F-4E-45MC'
1✔
352

353
    def __init__(self, lcd_type: LcdInfo, **kwargs: Unpack[AircraftKwargs]) -> None:
1✔
354
        """
355
        Create F-4E Phantom II.
356

357
        :param lcd_type: LCD type
358
        """
359
        kwargs['bios_data'] = {'PLT_MASTER_ARM_SW': ''}
1✔
360
        super().__init__(lcd_type=lcd_type, **kwargs)
1✔
361

362
    def _draw_common_data(self, draw: ImageDraw.ImageDraw) -> ImageDraw.ImageDraw:
1✔
363
        """
364
        Draw common part for Mono and Color LCD.
365

366
        :param draw: ImageDraw instance
367
        :return: updated image to draw
368
        """
369
        master_arm = 'Arm' if int(self.get_bios('PLT_MASTER_ARM_SW')) else 'Off'
1✔
370
        draw.text(xy=(0, 0), fill=self.lcd.foreground, font=self.lcd.font_s, text=f'Master Arm: {master_arm}')
1✔
371
        return draw
1✔
372

373
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
374
        """Prepare image for F-4E Phantom II Mono LCD."""
375
        self._draw_common_data(draw=ImageDraw.Draw(img))
1✔
376

377
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
378
        """Prepare image for F-4E Phantom II Color LCD."""
379
        self._draw_common_data(draw=ImageDraw.Draw(img))
1✔
380

381

382
class F15ESE(AdvancedAircraft):
1✔
383
    """F-15ESE Eagle."""
384
    bios_name: str = 'F-15ESE'
1✔
385

386
    def __init__(self, lcd_type: LcdInfo, **kwargs: Unpack[AircraftKwargs]) -> None:
1✔
387
        """
388
        Create F-15ESE Egle.
389

390
        :param lcd_type: LCD type
391
        """
392
        kwargs['bios_data'] = {f'F_UFC_LINE{i}_DISPLAY': '' for i in range(1, 7)}
1✔
393
        super().__init__(lcd_type=lcd_type, **kwargs)
1✔
394

395
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
396
        """Prepare image for F-15ESE Eagle for Mono LCD."""
397
        draw = ImageDraw.Draw(img)
1✔
398
        for i in range(1, 6):
1✔
399
            offset = (i - 1) * 8
1✔
400
            draw.text(xy=(0, offset), text=str(self.get_bios(f'F_UFC_LINE{i}_DISPLAY')), fill=self.lcd.foreground, font=self.lcd.font_s)
1✔
401
        if mat := search(r'\s*([0-9G]{1,2})\s+([0-9GV]{1,2})\s+', str(self.get_bios('F_UFC_LINE6_DISPLAY'))):
1✔
402
            uhf, v_uhf = mat.groups()
1✔
403
            draw.text(xy=(130, 30), text=f'{uhf:>2} {v_uhf:>2}', fill=self.lcd.foreground, font=self.lcd.font_s)
1✔
404

405
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
406
        """Prepare image for F-15ESE Eagle for Color LCD."""
407
        draw = ImageDraw.Draw(img)
1✔
408
        for i in range(1, 7):
1✔
409
            offset = (i - 1) * 24
1✔
410
            # todo: fix custom font for Color LCD
411
            draw.text(xy=(0, offset),
1✔
412
                      text=str(self.get_bios(f'F_UFC_LINE{i}_DISPLAY')),
413
                      fill=self.lcd.foreground,
414
                      font=ImageFont.truetype(DEFAULT_FONT_NAME, 29))
415

416

417
class Ka50(AdvancedAircraft):
1✔
418
    """Ka-50 Black Shark."""
419
    bios_name: str = 'Ka-50'
1✔
420

421
    def __init__(self, lcd_type: LcdInfo, **kwargs: Unpack[AircraftKwargs]) -> None:
1✔
422
        """
423
        Create Ka-50 Black Shark.
424

425
        :param lcd_type: LCD type
426
        """
427
        kwargs['bios_data'] = {
1✔
428
            'PVI_LINE1_APOSTROPHE1': '',
429
            'PVI_LINE1_APOSTROPHE2': '',
430
            'PVI_LINE1_POINT': '',
431
            'PVI_LINE1_SIGN': '',
432
            'PVI_LINE1_TEXT': '',
433
            'PVI_LINE2_APOSTROPHE1': '',
434
            'PVI_LINE2_APOSTROPHE2': '',
435
            'PVI_LINE2_POINT': '',
436
            'PVI_LINE2_SIGN': '',
437
            'PVI_LINE2_TEXT': '',
438
            'AP_ALT_HOLD_LED': int(),
439
            'AP_BANK_HOLD_LED': int(),
440
            'AP_FD_LED': int(),
441
            'AP_HDG_HOLD_LED': int(),
442
            'AP_PITCH_HOLD_LED': int(),
443
        }
444
        super().__init__(lcd_type=lcd_type, **kwargs)
1✔
445

446
    def _draw_common_data(self, draw: ImageDraw.ImageDraw, scale: int) -> None:
1✔
447
        """
448
        Draw common part (based on scale) for Mono and Color LCD.
449

450
        :param draw: ImageDraw instance
451
        :param scale: scaling factor (Mono 1, Color 2)
452
        """
453
        for rect_xy in [
1✔
454
            (0 * scale, 1 * scale, 85 * scale, 18 * scale),
455
            (0 * scale, 22 * scale, 85 * scale, 39 * scale),
456
            (88 * scale, 1 * scale, 103 * scale, 18 * scale),
457
            (88 * scale, 22 * scale, 103 * scale, 39 * scale),
458
        ]:
459
            draw.rectangle(xy=rect_xy, fill=self.lcd.background, outline=self.lcd.foreground)
1✔
460
        line1, line2 = self._generate_pvi_lines()
1✔
461
        draw.text(xy=(2 * scale, 3 * scale), text=line1, fill=self.lcd.foreground, font=self.lcd.font_l)
1✔
462
        draw.text(xy=(2 * scale, 24 * scale), text=line2, fill=self.lcd.foreground, font=self.lcd.font_l)
1✔
463
        self._auto_pilot_switch(draw, scale)
1✔
464

465
    def _generate_pvi_lines(self) -> Sequence[str]:
1✔
466
        """
467
        Generate coordinate strings.
468

469
        :return: tuple of string
470
        """
471
        text1, text2 = '', ''
1✔
472
        if line1_text := str(self.get_bios('PVI_LINE1_TEXT')):
1✔
473
            l1_apostr1 = self.get_bios('PVI_LINE1_APOSTROPHE1')
1✔
474
            l1_apostr2 = self.get_bios('PVI_LINE1_APOSTROPHE2')
1✔
475
            text1 = f'{line1_text[-6:-3]}{l1_apostr1}{line1_text[-3:-1]}{l1_apostr2}{line1_text[-1]}'
1✔
476
        if line2_text := str(self.get_bios('PVI_LINE2_TEXT')):
1✔
477
            l2_apostr1 = self.get_bios('PVI_LINE2_APOSTROPHE1')
1✔
478
            l2_apostr2 = self.get_bios('PVI_LINE2_APOSTROPHE2')
1✔
479
            text2 = f'{line2_text[-6:-3]}{l2_apostr1}{line2_text[-3:-1]}{l2_apostr2}{line2_text[-1]}'
1✔
480
        line1 = f'{self.get_bios("PVI_LINE1_SIGN")}{text1} {self.get_bios("PVI_LINE1_POINT")}'
1✔
481
        line2 = f'{self.get_bios("PVI_LINE2_SIGN")}{text2} {self.get_bios("PVI_LINE2_POINT")}'
1✔
482
        return line1, line2
1✔
483

484
    def _auto_pilot_switch(self, draw_obj: ImageDraw.ImageDraw, scale: int) -> None:
1✔
485
        """
486
        Draw rectangle and add text for autopilot channels in correct coordinates.
487

488
        :param draw_obj: ImageDraw object form PIL
489
        :param scale: scaling factor (Mono 1, Color 2)
490
        """
491
        for c_rect, c_text, ap_channel, turn_on in (
1✔
492
                ((111 * scale, 1 * scale, 124 * scale, 18 * scale), (113 * scale, 3 * scale), 'B', self.get_bios('AP_BANK_HOLD_LED', 0)),
493
                ((128 * scale, 1 * scale, 141 * scale, 18 * scale), (130 * scale, 3 * scale), 'P', self.get_bios('AP_PITCH_HOLD_LED', 0)),
494
                ((145 * scale, 1 * scale, 158 * scale, 18 * scale), (147 * scale, 3 * scale), 'F', self.get_bios('AP_FD_LED', 0)),
495
                ((111 * scale, 22 * scale, 124 * scale, 39 * scale), (113 * scale, 24 * scale), 'H', self.get_bios('AP_HDG_HOLD_LED', 0)),
496
                ((128 * scale, 22 * scale, 141 * scale, 39 * scale), (130 * scale, 24 * scale), 'A', self.get_bios('AP_ALT_HOLD_LED', 0)),
497
        ):
498
            draw_autopilot_channels(self.lcd, ap_channel, c_rect, c_text, draw_obj, turn_on)
1✔
499

500
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
501
        """Prepare image for Ka-50 Black Shark for Mono LCD."""
502
        self._draw_common_data(draw=ImageDraw.Draw(img), scale=1)
1✔
503

504
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
505
        """Prepare image for Ka-50 Black Shark for Mono LCD."""
506
        self._draw_common_data(draw=ImageDraw.Draw(img), scale=2)
1✔
507

508

509
class Ka503(Ka50):
1✔
510
    """Ka-50 Black Shark III."""
511
    bios_name: str = 'Ka-50_3'
1✔
512

513

514
class Mi8MT(AdvancedAircraft):
1✔
515
    """Mi-8MTV2 Magnificent Eight."""
516
    bios_name: str = 'Mi-8MT'
1✔
517

518
    def __init__(self, lcd_type: LcdInfo, **kwargs: Unpack[AircraftKwargs]) -> None:
1✔
519
        """
520
        Create Mi-8MTV2 Magnificent Eight.
521

522
        :param lcd_type: LCD type
523
        """
524
        kwargs['bios_data'] = {
1✔
525
            'LMP_AP_HDG_ON': int(),
526
            'LMP_AP_PITCH_ROLL_ON': int(),
527
            'LMP_AP_HEIGHT_ON': int(),
528
            'R863_CNL_SEL': int(),
529
            'R863_MOD': int(),
530
            'R863_FREQ': '',
531
            'R828_PRST_CHAN_SEL': int(),
532
            'YADRO1A_FREQ': '',
533
        }
534
        super().__init__(lcd_type=lcd_type, **kwargs)
1✔
535

536
    def _draw_common_data(self, draw: ImageDraw.ImageDraw, scale: int) -> None:
1✔
537
        """
538
        Draw common part (based on scale) for Mono and Color LCD.
539

540
        :param draw: ImageDraw instance
541
        :param scale: scaling factor (Mono 1, Color 2)
542
        """
543
        for c_rect, c_text, ap_channel, turn_on in (
1✔
544
                ((111 * scale, 1 * scale, 124 * scale, 18 * scale), (113 * scale, 3 * scale), 'H', self.get_bios('LMP_AP_HDG_ON', 0)),
545
                ((128 * scale, 1 * scale, 141 * scale, 18 * scale), (130 * scale, 3 * scale), 'P', self.get_bios('LMP_AP_PITCH_ROLL_ON', 0)),
546
                ((145 * scale, 1 * scale, 158 * scale, 18 * scale), (147 * scale, 3 * scale), 'A', self.get_bios('LMP_AP_HEIGHT_ON', 0)),
547
        ):
548
            draw_autopilot_channels(self.lcd, ap_channel, c_rect, c_text, draw, turn_on)
1✔
549

550
        r863, r828, yadro = self._generate_radio_values()
1✔
551
        for i, line in enumerate([f'R828 {r828}', f'YADRO1 {yadro}', f'R863 {r863}'], 1):
1✔
552
            offset = i * 10 * scale
1✔
553
            draw.text(xy=(0, offset), text=line, fill=self.lcd.foreground, font=self.lcd.font_s)
1✔
554

555
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
556
        """Prepare image for Mi-8MTV2 Magnificent Eight for Mono LCD."""
557
        self._draw_common_data(draw=ImageDraw.Draw(img), scale=1)
1✔
558

559
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
560
        """Prepare image for Mi-8MTV2 Magnificent Eight for Color LCD."""
561
        self._draw_common_data(draw=ImageDraw.Draw(img), scale=2)
1✔
562

563
    def _generate_radio_values(self) -> Sequence[str]:
1✔
564
        """
565
        Generate string data about Hip R863, R828, YADRO1A radios settings.
566

567
        :return: All 3 radios settings as strings
568
        """
569
        r863_mod = 'FM' if int(self.get_bios('R863_MOD')) else 'AM'
1✔
570
        r863_freq = float(self.get_bios('R863_FREQ', 0.0))
1✔
571
        yadro_freq = float(self.get_bios('YADRO1A_FREQ', 0.0))
1✔
572
        r863 = f'Ch:{int(self.get_bios("R863_CNL_SEL")) + 1:>2} {r863_mod} {r863_freq:.3f}'
1✔
573
        r828 = f'Ch:{int(self.get_bios("R828_PRST_CHAN_SEL")) + 1:>2}'
1✔
574
        yadro = f'{yadro_freq:>7.1f}'
1✔
575
        return r863, r828, yadro
1✔
576

577

578
class Mi24P(AdvancedAircraft):
1✔
579
    """Mi-24P Hind."""
580
    bios_name: str = 'Mi-24P'
1✔
581

582
    def __init__(self, lcd_type: LcdInfo, **kwargs: Unpack[AircraftKwargs]) -> None:
1✔
583
        """
584
        Create Mi-24P Hind.
585

586
        :param lcd_type: LCD type
587
        """
588
        kwargs['bios_data'] = {
1✔
589
            'PLT_R863_CHAN': int(),
590
            'PLT_R863_MODUL': int(),
591
            'PLT_R828_CHAN': int(),
592
            'JADRO_FREQ': '',
593
            'PLT_SAU_HOVER_MODE_ON_L': int(),
594
            'PLT_SAU_ROUTE_MODE_ON_L': int(),
595
            'PLT_SAU_ALT_MODE_ON_L': int(),
596
            'PLT_SAU_H_ON_L': int(),
597
            'PLT_SAU_K_ON_L': int(),
598
            'PLT_SAU_T_ON_L': int(),
599
            'PLT_SAU_B_ON_L': int(),
600
        }
601
        super().__init__(lcd_type=lcd_type, **kwargs)
1✔
602

603
    def _draw_common_data(self, draw: ImageDraw.ImageDraw, scale: int) -> None:
1✔
604
        """
605
        Draw common part (based on scale) for Mono and Color LCD.
606

607
        :param draw: ImageDraw instance
608
        :param scale: scaling factor (Mono 1, Color 2)
609
        """
610
        for c_rect, c_text, ap_channel, turn_on in (
1✔
611
                ((111 * scale, 1 * scale, 124 * scale, 18 * scale), (113 * scale, 3 * scale), 'H', self.get_bios('PLT_SAU_HOVER_MODE_ON_L', 0)),
612
                ((128 * scale, 1 * scale, 141 * scale, 18 * scale), (130 * scale, 3 * scale), 'R', self.get_bios('PLT_SAU_ROUTE_MODE_ON_L', 0)),
613
                ((145 * scale, 1 * scale, 158 * scale, 18 * scale), (147 * scale, 3 * scale), 'A', self.get_bios('PLT_SAU_ALT_MODE_ON_L', 0)),
614
                ((94 * scale, 22 * scale, 107 * scale, 39 * scale), (96 * scale, 24 * scale), 'Y', self.get_bios('PLT_SAU_H_ON_L', 0)),
615
                ((111 * scale, 22 * scale, 124 * scale, 39 * scale), (113 * scale, 24 * scale), 'R', self.get_bios('PLT_SAU_K_ON_L', 0)),
616
                ((128 * scale, 22 * scale, 141 * scale, 39 * scale), (130 * scale, 24 * scale), 'P', self.get_bios('PLT_SAU_T_ON_L', 0)),
617
                ((145 * scale, 22 * scale, 158 * scale, 39 * scale), (147 * scale, 24 * scale), 'A', self.get_bios('PLT_SAU_B_ON_L', 0)),
618
        ):
619
            draw_autopilot_channels(self.lcd, ap_channel, c_rect, c_text, draw, turn_on)
1✔
620

621
        r863, r828, yadro = self._generate_radio_values()
1✔
622
        for i, line in enumerate([f'R828 {r828}', f'R863 {r863}', f'YADRO1 {yadro}'], 1):
1✔
623
            offset = i * 10 * scale
1✔
624
            draw.text(xy=(0, offset), text=line, fill=self.lcd.foreground, font=self.lcd.font_s)
1✔
625

626
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
627
        """Prepare image for Mi-24P Hind for Mono LCD."""
628
        self._draw_common_data(draw=ImageDraw.Draw(img), scale=1)
1✔
629

630
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
631
        """Prepare image for Mi-24P Hind for Color LCD."""
632
        self._draw_common_data(draw=ImageDraw.Draw(img), scale=2)
1✔
633

634
    def _generate_radio_values(self) -> Sequence[str]:
1✔
635
        """
636
        Generate string data about Hind R863, R828, YADRO1I radios settings.
637

638
        :return: All 3 radios settings as strings
639
        """
640
        r863_mod = 'FM' if int(self.get_bios('PLT_R863_MODUL')) else 'AM'
1✔
641
        yadro_freq = float(self.get_bios('JADRO_FREQ', 0.0))
1✔
642
        r863 = f'Ch:{int(self.get_bios("PLT_R863_CHAN")) + 1:>2} {r863_mod}'
1✔
643
        r828 = f'Ch:{int(self.get_bios("PLT_R828_CHAN")) + 1:>2}'
1✔
644
        yadro = f'{yadro_freq:>7.1f}'
1✔
645
        return r863, r828, yadro
1✔
646

647

648
class ApacheEufdMode(Enum):
1✔
649
    """Apache EUFD Mode."""
650
    IDM = 1
1✔
651
    WCA = 2
1✔
652
    PRE = 4
1✔
653

654

655
class AH64DBLKII(AdvancedAircraft):
1✔
656
    """AH-64D Apache."""
657
    bios_name: str = 'AH-64D_BLK_II'
1✔
658

659
    def __init__(self, lcd_type: LcdInfo, **kwargs: Unpack[AircraftKwargs]) -> None:
1✔
660
        """
661
        Create AH-64D Apache.
662

663
        :param lcd_type: LCD type
664
        """
665
        kwargs['bios_data'] = {f'PLT_EUFD_LINE{i}': '' for i in range(1, 15)}
1✔
666
        super().__init__(lcd_type=lcd_type, **kwargs)
1✔
667
        self.mode = ApacheEufdMode.IDM
1✔
668
        self.warning_line = 1
1✔
669

670
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
671
        """Prepare image for AH-64D Apache for Mono LCD."""
672
        LOG.debug(f'Mode: {self.mode}')
1✔
673
        kwargs = {'draw': ImageDraw.Draw(img), 'scale': 1}
1✔
674
        if (mode := self.mode.name.lower()) == 'pre':
1✔
675
            kwargs['x_cords'] = [0] * 5 + [80] * 5
1✔
676
            kwargs['y_cords'] = [j * 8 for j in range(0, 5)] * 2
1✔
677
            kwargs['font'] = self.lcd.font_xs
1✔
678
            del kwargs['scale']
1✔
679
        getattr(self, f'_draw_for_{mode}')(**kwargs)
1✔
680

681
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
682
        """Prepare image for AH-64D Apache for Color LCD."""
683
        LOG.debug(f'Mode: {self.mode}')
1✔
684
        kwargs = {'draw': ImageDraw.Draw(img), 'scale': 2}
1✔
685
        if (mode := self.mode.name.lower()) == 'pre':
1✔
686
            kwargs['x_cords'] = [0] * 10
1✔
687
            kwargs['y_cords'] = [j * 24 for j in range(0, 10)]
1✔
688
            kwargs['font'] = self.lcd.font_l
1✔
689
            del kwargs['scale']
1✔
690
        getattr(self, f'_draw_for_{mode}')(**kwargs)
1✔
691

692
    def _draw_for_idm(self, draw: ImageDraw.ImageDraw, scale: int) -> None:
1✔
693
        """
694
        Draw image for IDM mode.
695

696
        :param draw: ImageDraw instance
697
        :param scale: scaling factor (Mono 1, Color 2)
698
        """
699
        for i in range(8, 13):
1✔
700
            offset = (i - 8) * 8 * scale
1✔
701
            if mat := search(r'(.*\*)\s+(\d+)([.\dULCA]+)[\s\dA-Z]*\s+(\d+)([.\dULCA]+)[\sA-Z]+', str(self.get_bios(f'PLT_EUFD_LINE{i}'))):
1✔
702
                spacer = ' ' * (6 - len(mat.group(3)))
1✔
703
                text = f'{mat.group(1):>7}{mat.group(2):>4}{mat.group(3):5<}{spacer}{mat.group(4):>4}{mat.group(5):5<}'
1✔
704
                draw.text(xy=(0, offset), text=text, fill=self.lcd.foreground, font=self.lcd.font_xs)
1✔
705

706
    def _draw_for_wca(self, draw: ImageDraw.ImageDraw, scale: int) -> None:
1✔
707
        """
708
        Draw image for WCA mode.
709

710
        :param draw: ImageDraw instance
711
        :param scale: scaling factor (Mono 1, Color 2)
712
        """
713
        warnings = self._fetch_warning_list()
1✔
714
        LOG.debug(f'Warnings: {warnings}')
1✔
715
        try:
1✔
716
            for idx, warn_no in enumerate(range(self.warning_line - 1, self.warning_line + 4)):
1✔
717
                line = idx * 8 * scale
1✔
718
                draw.text(xy=(0, line), text=f'{idx + self.warning_line:2} {warnings[warn_no]}', fill=self.lcd.foreground, font=self.lcd.font_s)
1✔
719
                if self.warning_line >= len(warnings) - 3:
1✔
720
                    self.warning_line = 1
1✔
721
        except IndexError:
1✔
722
            self.warning_line = 1
1✔
723

724
    def _fetch_warning_list(self) -> list[str]:
1✔
725
        """
726
        Fetch all warnings and return as list.
727

728
        :return: list of warnings (as strings)
729
        """
730
        warn = []
1✔
731
        for i in range(1, 8):
1✔
732
            if mat := search(r'(.*)\|(.*)\|(.*)', str(self.get_bios(f'PLT_EUFD_LINE{i}'))):
1✔
733
                warn.extend([w for w in [mat.group(1).strip(), mat.group(2).strip(), mat.group(3).strip()] if w])
1✔
734
        return warn
1✔
735

736
    def _draw_for_pre(self, draw: ImageDraw.ImageDraw, x_cords: list[int], y_cords: list[int], font: ImageFont.FreeTypeFont) -> None:
1✔
737
        """
738
        Draw image for PRE mode.
739

740
        :param draw: ImageDraw instance
741
        :param x_cords: list of X coordinates
742
        :param y_cords: list of Y coordinates
743
        :param font: font instance
744
        """
745
        match_dict = {
1✔
746
            2: r'.*\|.*\|([\u2192\s][A-Z]*\s\d)\s*([\d\.]*)',
747
            3: r'.*\|.*\|([\u2192\s][A-Z]*\s\d)\s*([\d\.]*)',
748
            4: r'.*\|.*\|([\u2192\s][A-Z]*\s\d)\s*([\d\.]*)',
749
            5: r'.*\|.*\|([\u2192\s][A-Z]*\s\d)\s*([\d\.]*)',
750
            6: r'.*\|.*\|([\u2192\s][A-Z]*\s\d)\s*([\d\.]*)',
751
            7: r'.*\|.*\|([\u2192\s][A-Z]*\s\d)\s*([\d\.]*)',
752
            8: r'\s*\|([\u2192\s][A-Z]*\s*\d*)\s*([\d\.]*)',
753
            9: r'\s*\|([\u2192\s][A-Z]*\s*\d*)\s*([\d\.]*)',
754
            10: r'\s*\|([\u2192\s][A-Z]*\s*\d*)\s*([\d\.]*)',
755
            11: r'\s*\|([\u2192\s][A-Z]*\s*\d*)\s*([\d\.]*)',
756
        }
757
        for i, x_cord, y_cord in zip(range(2, 12), x_cords, y_cords):
1✔
758
            if mat := search(match_dict[i], str(self.get_bios(f'PLT_EUFD_LINE{i}'))):
1✔
759
                draw.text(xy=(x_cord, y_cord), text=f'{mat.group(1):<9}{mat.group(2):>7}',
1✔
760
                          fill=self.lcd.foreground, font=font)
761

762
    def set_bios(self, selector: str, value: Union[str, int]) -> None:
1✔
763
        """
764
        Set new data.
765

766
        :param selector:
767
        :param value:
768
        """
769
        if selector == 'PLT_EUFD_LINE1':
1✔
770
            self.mode = ApacheEufdMode.IDM
1✔
771
            if search(r'.*\|.*\|(PRESET TUNE)\s\w+', str(value)):
1✔
772
                self.mode = ApacheEufdMode.PRE
1✔
773
        if selector in ('PLT_EUFD_LINE8', 'PLT_EUFD_LINE9', 'PLT_EUFD_LINE10', 'PLT_EUFD_LINE11', 'PLT_EUFD_LINE12'):
1✔
774
            LOG.debug(f'{type(self).__name__} {selector} original: "{value}"')
1✔
775
            value = str(value).replace(']', '\u2666').replace('[', '\u25ca').replace('~', '\u25a0'). \
1✔
776
                replace('>', '\u25b8').replace('<', '\u25c2').replace('=', '\u2219')
777
        if 'PLT_EUFD_LINE' in selector:
1✔
778
            LOG.debug(f'{type(self).__name__} {selector} original: "{value}"')
1✔
779
            value = str(value).replace('!', '\u2192')  # replace ! with ->
1✔
780
        super().set_bios(selector, value)
1✔
781

782
    def button_request(self, button: Union[LcdButton, Gkey, MouseButton]) -> RequestModel:
1✔
783
        """
784
        Prepare AH-64D Apache specific DCS-BIOS request for button pressed.
785

786
        :param button: LcdButton, Gkey or MouseButton
787
        :return: RequestModel object
788
        """
789
        wca_or_idm = f'PLT_EUFD_WCA {RequestType.CUSTOM.value} PLT_EUFD_WCA 0|PLT_EUFD_WCA 1|'
1✔
790
        if self.mode == ApacheEufdMode.IDM:
1✔
791
            wca_or_idm = f'PLT_EUFD_IDM {RequestType.CUSTOM.value} PLT_EUFD_IDM 0|PLT_EUFD_IDM 1|'
1✔
792

793
        if button in (LcdButton.FOUR, LcdButton.UP) and self.mode == ApacheEufdMode.IDM:
1✔
794
            self.mode = ApacheEufdMode.WCA
1✔
795
        elif button in (LcdButton.FOUR, LcdButton.UP) and self.mode != ApacheEufdMode.IDM:
1✔
796
            self.mode = ApacheEufdMode.IDM
1✔
797

798
        if button in (LcdButton.ONE, LcdButton.LEFT) and self.mode == ApacheEufdMode.WCA:
1✔
799
            self.warning_line += 1
1✔
800

801
        self.key_req.set_request(LcdButton.ONE, wca_or_idm)
1✔
802
        self.key_req.set_request(LcdButton.LEFT, wca_or_idm)
1✔
803
        return super().button_request(button)
1✔
804

805

806
class A10C(AdvancedAircraft):
1✔
807
    """A-10C Warthog."""
808
    bios_name: str = 'A-10C'
1✔
809

810
    def __init__(self, lcd_type: LcdInfo, **kwargs: Unpack[AircraftKwargs]) -> None:
1✔
811
        """
812
        Create A-10C Warthog or A-10C II Tank Killer.
813

814
        :param lcd_type: LCD type
815
        """
816
        kwargs['bios_data'] = {
1✔
817
            'VHFAM_FREQ1': int(),
818
            'VHFAM_FREQ2': int(),
819
            'VHFAM_FREQ3': int(),
820
            'VHFAM_FREQ4': int(),
821
            'VHFAM_PRESET': int(),
822
            'VHFFM_FREQ1': int(),
823
            'VHFFM_FREQ2': int(),
824
            'VHFFM_FREQ3': int(),
825
            'VHFFM_FREQ4': int(),
826
            'VHFFM_PRESET': int(),
827
            'UHF_100MHZ_SEL': int(),
828
            'UHF_10MHZ_SEL': int(),
829
            'UHF_1MHZ_SEL': int(),
830
            'UHF_POINT1MHZ_SEL': int(),
831
            'UHF_POINT25_SEL': int(),
832
            'UHF_PRESET': '',
833
            'ARC210_FREQUENCY': '',
834
            'ARC210_PREV_MANUAL_FREQ': '',
835
        }
836
        super().__init__(lcd_type=lcd_type, **kwargs)
1✔
837

838
    def _generate_vhf(self, modulation: str) -> str:
1✔
839
        """
840
        Generate frequency for VHF AM, VHF FM radio.
841

842
        :param modulation: 'AM' or 'FM'
843
        :return: frequency settings as strings
844
        """
845
        freq_2 = self.get_bios(f'VHF{modulation}_FREQ2')
1✔
846
        freq_3 = self.get_bios(f'VHF{modulation}_FREQ3')
1✔
847
        freq_1 = int(self.get_bios(f'VHF{modulation}_FREQ1', 0)) + 3
1✔
848
        freq_4 = int(self.get_bios(f'VHF{modulation}_FREQ4', 0)) * 25
1✔
849
        preset = int(self.get_bios(f'VHF{modulation}_PRESET', 0)) + 1
1✔
850
        return f'{freq_1:2}{freq_2}.{freq_3}{freq_4:02} ({preset:2})'
1✔
851

852
    def _generate_uhf(self) -> str:
1✔
853
        """
854
        Generate frequency for UHF radio.
855

856
        :return: frequency settings as strings
857
        """
858
        uhf_10 = self.get_bios('UHF_10MHZ_SEL')
1✔
859
        uhf_1 = self.get_bios('UHF_1MHZ_SEL')
1✔
860
        uhf_01 = self.get_bios('UHF_POINT1MHZ_SEL')
1✔
861
        uhf_preset = self.get_bios('UHF_PRESET')
1✔
862
        uhf_100 = int(self.get_bios('UHF_100MHZ_SEL', 0)) + 2
1✔
863
        uhf_100 = 'A' if uhf_100 == 4 else uhf_100  # type: ignore
1✔
864
        uhf_25 = int(self.get_bios('UHF_POINT25_SEL', 0)) * 25
1✔
865
        return f'{uhf_100}{uhf_10}{uhf_1}.{uhf_01}{uhf_25:02} ({uhf_preset})'
1✔
866

867
    def _generate_arc(self) -> str:
1✔
868
        """
869
        Generate frequency for ARC AM radio.
870

871
        :return: frequency settings as strings
872
        """
873
        return f'{self.get_bios("ARC210_FREQUENCY")} ({str(self.get_bios("ARC210_PREV_MANUAL_FREQ")).strip():>7})'
1✔
874

875
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
876
        """Prepare image for A-10C Warthog for Mono LCD."""
877
        draw = ImageDraw.Draw(img)
1✔
878
        uhf = self._generate_uhf()
1✔
879
        vhf_am = self._generate_vhf('AM')
1✔
880
        vhf_fm = self._generate_vhf('FM')
1✔
881
        for i, line in enumerate(['      *** RADIOS ***', f' AM: {vhf_am}', f'UHF: {uhf}', f' FM: {vhf_fm}']):
1✔
882
            offset = i * 10
1✔
883
            draw.text(xy=(0, offset), text=line, fill=self.lcd.foreground, font=self.lcd.font_s)
1✔
884

885
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
886
        """Prepare image for A-10C Warthog for Color LCD."""
887
        draw = ImageDraw.Draw(img)
1✔
888
        uhf = self._generate_uhf()
1✔
889
        vhf_am = self._generate_vhf('AM')
1✔
890
        vhf_fm = self._generate_vhf('FM')
1✔
891
        for i, line in enumerate(['      *** RADIOS ***', f' AM: {vhf_am}', f'UHF: {uhf}', f' FM: {vhf_fm}']):
1✔
892
            offset = i * 20
1✔
893
            draw.text(xy=(0, offset), text=line, fill=self.lcd.foreground, font=self.lcd.font_s)
1✔
894

895

896
class A10C2(A10C):
1✔
897
    """A-10C II Tank Killer."""
898
    bios_name: str = 'A-10C_2'
1✔
899

900
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
901
        """Prepare image for A-10C II Tank Killer for Mono LCD."""
902
        draw = ImageDraw.Draw(img)
1✔
903
        uhf = self._generate_uhf()
1✔
904
        vhf_fm = self._generate_vhf('FM')
1✔
905
        arc = self._generate_arc()
1✔
906
        for i, line in enumerate(['      *** RADIOS ***', f' AM: {arc}', f'UHF: {uhf}', f' FM: {vhf_fm}']):
1✔
907
            offset = i * 10
1✔
908
            draw.text(xy=(0, offset), text=line, fill=self.lcd.foreground, font=self.lcd.font_s)
1✔
909

910
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
911
        """Prepare image for A-10C II Tank Killer for Color LCD."""
912
        draw = ImageDraw.Draw(img)
1✔
913
        uhf = self._generate_uhf()
1✔
914
        vhf_fm = self._generate_vhf('FM')
1✔
915
        arc = self._generate_arc()
1✔
916
        for i, line in enumerate(['      *** RADIOS ***', f' AM: {arc}', f'UHF: {uhf}', f' FM: {vhf_fm}']):
1✔
917
            offset = i * 20
1✔
918
            draw.text(xy=(0, offset), text=line, fill=self.lcd.foreground, font=self.lcd.font_s)
1✔
919

920

921
class F14B(AdvancedAircraft):
1✔
922
    """F-14B Tomcat."""
923
    bios_name: str = 'F-14B'
1✔
924

925
    def _draw_common_data(self, draw: ImageDraw.ImageDraw) -> None:
1✔
926
        """
927
        Draw common part for Mono and Color LCD.
928

929
        :param draw: ImageDraw instance
930
        """
931
        draw.text(xy=(2, 3), text=f'{self.bios_name}', fill=self.lcd.foreground, font=self.lcd.font_l)
1✔
932

933
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
934
        """Prepare image for F-14B Tomcat for Mono LCD."""
935
        self._draw_common_data(draw=ImageDraw.Draw(img))
1✔
936

937
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
938
        """Prepare image for F-14B Tomcat for Color LCD."""
939
        self._draw_common_data(draw=ImageDraw.Draw(img))
1✔
940

941

942
class F14A135GR(F14B):
1✔
943
    """F-14A-135-GR Tomcat."""
944
    bios_name: str = 'F-14A-135-GR'
1✔
945

946

947
class AV8BNA(AdvancedAircraft):
1✔
948
    """AV-8B Night Attack."""
949
    bios_name: str = 'AV8BNA'
1✔
950

951
    def __init__(self, lcd_type: LcdInfo, **kwargs: Unpack[AircraftKwargs]) -> None:
1✔
952
        """
953
        Create AV-8B Night Attack.
954

955
        :param lcd_type: LCD type
956
        """
957
        kwargs['bios_data'] = {
1✔
958
            'UFC_SCRATCHPAD': '',
959
            'UFC_COMM1_DISPLAY': '',
960
            'UFC_COMM2_DISPLAY': '',
961
            'AV8BNA_ODU_1_SELECT': '',
962
            'AV8BNA_ODU_1_TEXT': '',
963
            'AV8BNA_ODU_2_SELECT': '',
964
            'AV8BNA_ODU_2_TEXT': '',
965
            'AV8BNA_ODU_3_SELECT': '',
966
            'AV8BNA_ODU_3_TEXT': '',
967
            'AV8BNA_ODU_4_SELECT': '',
968
            'AV8BNA_ODU_4_TEXT': '',
969
            'AV8BNA_ODU_5_SELECT': '',
970
            'AV8BNA_ODU_5_TEXT': '',
971
        }
972
        super().__init__(lcd_type=lcd_type, **kwargs)
1✔
973

974
    def _draw_common_data(self, draw: ImageDraw.ImageDraw, scale: int) -> ImageDraw.ImageDraw:
1✔
975
        """
976
        Draw common part (based on scale) for Mono and Color LCD.
977

978
        :param draw: ImageDraw instance
979
        :param scale: scaling factor (Mono 1, Color 2)
980
        :return: updated image to draw
981
        """
982
        draw.text(xy=(50 * scale, 0), fill=self.lcd.foreground, font=self.lcd.font_l, text=f'{self.get_bios("UFC_SCRATCHPAD")}')
1✔
983
        draw.line(xy=(50 * scale, 20 * scale, 160 * scale, 20 * scale), fill=self.lcd.foreground, width=1)
1✔
984

985
        draw.rectangle(xy=(50 * scale, 29 * scale, 70 * scale, 42 * scale), fill=self.lcd.background, outline=self.lcd.foreground)
1✔
986
        draw.text(xy=(52 * scale, 29 * scale), text=str(self.get_bios('UFC_COMM1_DISPLAY')), fill=self.lcd.foreground, font=self.lcd.font_l)
1✔
987

988
        draw.rectangle(xy=(139 * scale, 29 * scale, 159 * scale, 42 * scale), fill=self.lcd.background, outline=self.lcd.foreground)
1✔
989
        draw.text(xy=(140 * scale, 29 * scale), text=str(self.get_bios('UFC_COMM2_DISPLAY')), fill=self.lcd.foreground, font=self.lcd.font_l)
1✔
990

991
        for i in range(1, 6):
1✔
992
            offset = (i - 1) * 8 * scale
1✔
993
            draw.text(xy=(0 * scale, offset), fill=self.lcd.foreground, font=self.lcd.font_s,
1✔
994
                      text=f'{i}{self.get_bios(f"AV8BNA_ODU_{i}_SELECT")}{self.get_bios(f"AV8BNA_ODU_{i}_TEXT")}')
995
        return draw
1✔
996

997
    def draw_for_lcd_mono(self, img: Image.Image) -> None:
1✔
998
        """Prepare image for AV-8B N/A for Mono LCD."""
999
        self._draw_common_data(draw=ImageDraw.Draw(img), scale=1)
1✔
1000

1001
    def draw_for_lcd_color(self, img: Image.Image) -> None:
1✔
1002
        """Prepare image for AV-8B N/A for Color LCD."""
1003
        self._draw_common_data(draw=ImageDraw.Draw(img), scale=2)
1✔
1004

1005

1006
def draw_autopilot_channels(lcd: LcdInfo,
1✔
1007
                            ap_channel: str,
1008
                            c_rect: tuple[float, float, float, float],
1009
                            c_text: tuple[float, float],
1010
                            draw_obj: ImageDraw.ImageDraw,
1011
                            turn_on: Union[str, int, float]) -> None:
1012
    """
1013
    Draw rectangles with a background for autopilot channels.
1014

1015
    :param lcd: instance of LCD
1016
    :param ap_channel: channel name
1017
    :param c_rect: coordinates for rectangle
1018
    :param c_text: coordinates for a name
1019
    :param draw_obj: ImageDraw instance
1020
    :param turn_on: channel on/off, fill on/off
1021
    """
1022
    if turn_on:
1✔
1023
        draw_obj.rectangle(c_rect, fill=lcd.foreground, outline=lcd.foreground)
1✔
1024
        draw_obj.text(xy=c_text, text=ap_channel, fill=lcd.background, font=lcd.font_l)
1✔
1025
    else:
1026
        draw_obj.rectangle(xy=c_rect, fill=lcd.background, outline=lcd.foreground)
1✔
1027
        draw_obj.text(xy=c_text, text=ap_channel, fill=lcd.foreground, font=lcd.font_l)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc