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

emcek / dcspy / 6536914078

16 Oct 2023 05:06PM UTC coverage: 94.343% (+0.6%) from 93.707%
6536914078

Pull #202

github

emcek
add and update tests for logitech
Pull Request #202: Load G-Keys configuration from YAML

90 of 90 new or added lines in 4 files covered. (100.0%)

1651 of 1750 relevant lines covered (94.34%)

0.94 hits per line

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

94.44
/dcspy/logitech.py
1
from functools import partial
1✔
2
from importlib import import_module
1✔
3
from logging import getLogger
1✔
4
from pathlib import Path
1✔
5
from pprint import pformat
1✔
6
from socket import socket
1✔
7
from time import sleep
1✔
8
from typing import List, Sequence, Union
1✔
9

10
from PIL import Image, ImageDraw
1✔
11

12
from dcspy import get_config_yaml_item
1✔
13
from dcspy.aircraft import BasicAircraft, MetaAircraft
1✔
14
from dcspy.dcsbios import ProtocolParser
1✔
15
from dcspy.models import (SEND_ADDR, SUPPORTED_CRAFTS, Gkey, KeyboardModel, LcdButton, LcdColor, LcdMono, ModelG13, ModelG15v1, ModelG15v2, ModelG19, ModelG510,
1✔
16
                          generate_gkey)
17
from dcspy.sdk import key_sdk, lcd_sdk
1✔
18
from dcspy.utils import get_ctrl, get_full_bios_for_plane, get_planes_list
1✔
19

20
LOG = getLogger(__name__)
1✔
21

22

23
class KeyboardManager:
1✔
24
    """General keyboard with LCD from Logitech."""
25
    def __init__(self, parser: ProtocolParser, **kwargs) -> None:
1✔
26
        """
27
        General keyboard with LCD from Logitech.
28

29
        It can be easily extended for any of:
30
        - Mono LCD: G13, G15 (v1 and v2) and G510
31
        - RGB LCD: G19
32

33
        However, it defines a bunch of functionally to be used be child class:
34
        - DCS-BIOS callback for currently used aircraft in DCS
35
        - auto-detecting aircraft and load its handling class
36
        - send button request to DCS-BIOS
37

38
        Child class needs redefine:
39
        - pass lcd_type argument as LcdInfo to super constructor
40

41
        :param parser: DCS-BIOS parser instance
42
        """
43
        detect_plane = {'parser': parser, 'address': 0x0, 'max_length': 0x10, 'callback': partial(self.detecting_plane)}
1✔
44
        getattr(import_module('dcspy.dcsbios'), 'StringBuffer')(**detect_plane)
1✔
45
        self.parser = parser
1✔
46
        self.plane_name = ''
1✔
47
        self.plane_detected = False
1✔
48
        self.lcdbutton_pressed = False
1✔
49
        self.gkey_pressed = False
1✔
50
        self._display: List[str] = []
1✔
51
        self.lcd = kwargs.get('lcd_type', LcdMono)
1✔
52
        self.model = KeyboardModel(name='', klass='', modes=1, gkeys=1, lcdkeys=1, lcd='mono')
1✔
53
        self.gkey: Sequence[Gkey] = ()
1✔
54
        self.buttons: Sequence[LcdButton] = ()
1✔
55
        lcd_sdk.logi_lcd_init('DCS World', self.lcd.type.value)
1✔
56
        key_sdk.logi_gkey_init()
1✔
57
        self.plane = BasicAircraft(self.lcd)
1✔
58
        self.vert_space = 0
1✔
59

60
    @property
1✔
61
    def display(self) -> List[str]:
1✔
62
        """
63
        Get the latest text from LCD.
64

65
        :return: list of strings with data, row by row
66
        """
67
        return self._display
1✔
68

69
    @display.setter
1✔
70
    def display(self, message: List[str]) -> None:
1✔
71
        """
72
        Display message as image at LCD.
73

74
        For G13/G15/G510 takes first 4 or fewer elements of list and display as 4 rows.
75
        For G19 takes first 8 or fewer elements of list and display as 8 rows.
76
        :param message: List of strings to display, row by row.
77
        """
78
        self._display = message
1✔
79
        lcd_sdk.update_display(self._prepare_image())
1✔
80

81
    @staticmethod
1✔
82
    def text(message: List[str]) -> None:
1✔
83
        """
84
        Display message at LCD.
85

86
        For G13/G15/G510 takes first 4 or fewer elements of list and display as 4 rows.
87
        For G19 takes first 8 or fewer elements of list and display as 8 rows.
88
        :param message: List of strings to display, row by row.
89
        """
90
        lcd_sdk.update_text(message)
1✔
91

92
    def detecting_plane(self, value: str) -> None:
1✔
93
        """
94
        Try to detect airplane base on value received from DCS-BIOS.
95

96
        :param value: data from DCS-BIOS
97
        """
98
        short_name = value.replace('-', '').replace('_', '')
1✔
99
        if self.plane_name != short_name:
1✔
100
            self.plane_name = short_name
1✔
101
            planes_list = get_planes_list(bios_dir=Path(str(get_config_yaml_item('dcsbios'))))
1✔
102
            if self.plane_name in SUPPORTED_CRAFTS:
1✔
103
                LOG.info(f'Advanced supported aircraft: {value}')
1✔
104
                self.display = ['Detected aircraft:', SUPPORTED_CRAFTS[self.plane_name]['name']]
1✔
105
                self.plane_detected = True
1✔
106
            elif self.plane_name not in SUPPORTED_CRAFTS and value in planes_list:
1✔
107
                LOG.info(f'Basic supported aircraft: {value}')
1✔
108
                self.plane_name = value
1✔
109
                self.display = ['Detected aircraft:', value]
1✔
110
                self.plane_detected = True
1✔
111
            elif value not in planes_list:
1✔
112
                LOG.warning(f'Not supported aircraft: {value}')
1✔
113
                self.display = ['Detected aircraft:', value, 'Not supported yet!']
1✔
114

115
    def load_new_plane(self) -> None:
1✔
116
        """
117
        Dynamic load of new detected aircraft.
118

119
        Setup callbacks for detected plane inside DCS-BIOS parser.
120
        """
121
        self.plane_detected = False
1✔
122
        if self.plane_name in SUPPORTED_CRAFTS:
1✔
123
            self.plane = getattr(import_module('dcspy.aircraft'), self.plane_name)(self.lcd)
1✔
124
            LOG.debug(f'Dynamic load of: {self.plane_name} as {SUPPORTED_CRAFTS[self.plane_name]["name"]} | BIOS: {self.plane.bios_name}')
1✔
125
            self._setup_plane_callback()
1✔
126
        else:
127
            self.plane = MetaAircraft(self.plane_name, (BasicAircraft,), {})(self.lcd)
×
128
            LOG.debug(f'Dynamic load of: {self.plane_name} as BasicAircraft | BIOS: {self.plane.bios_name}')  # todo: remove, check name
×
129
            self.plane.bios_name = self.plane_name
×
130
            LOG.debug(f'Dynamic load of: {self.plane_name} as BasicAircraft | BIOS: {self.plane.bios_name}')
×
131
        LOG.debug(f'{repr(self)}')
1✔
132

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

141
    def check_buttons(self) -> LcdButton:
1✔
142
        """
143
        Check if button was pressed and return it`s enum.
144

145
        :return: LcdButton enum of pressed button
146
        """
147
        for btn in self.buttons:
1✔
148
            if lcd_sdk.logi_lcd_is_button_pressed(btn.value):
1✔
149
                if not self.lcdbutton_pressed:
1✔
150
                    self.lcdbutton_pressed = True
1✔
151
                    return LcdButton(btn)
1✔
152
                return LcdButton.NONE
1✔
153
        self.lcdbutton_pressed = False
1✔
154
        return LcdButton.NONE
1✔
155

156
    def check_gkey(self) -> Gkey:
1✔
157
        """
158
        Check if G-Key was pressed and return it`s enum.
159

160
        :return: Gkey enum of pressed button
161
        """
162
        for key in self.gkey:
1✔
163
            if key_sdk.logi_gkey_is_keyboard_gkey_pressed(g_key=key.key, mode=key.mode):
1✔
164
                gkey = key_sdk.logi_gkey_is_keyboard_gkey_string(g_key=key.key, mode=key.mode).replace('/', '_')
1✔
165
                LOG.debug(f'Button {gkey} is pressed')
1✔
166
                if not self.gkey_pressed:
1✔
167
                    self.gkey_pressed = True
1✔
168
                    return key
1✔
169
                return Gkey(0, 0)
1✔
170
        self.gkey_pressed = False
1✔
171
        return Gkey(0, 0)
1✔
172

173
    def button_handle(self, sock: socket) -> None:
1✔
174
        """
175
        Button handler.
176

177
        * detect if button was pressed
178
        * fetch DCS-BIOS request from current plane
179
        * sent action to DCS-BIOS via network socket
180

181
        :param sock: network socket
182
        """
183
        button = self.check_buttons()
1✔
184
        gkey = self.check_gkey()
1✔
185
        if button.value:
1✔
186
            self._send_request(button, sock)
1✔
187
        if gkey:
1✔
188
            self._send_request(gkey, sock)
1✔
189

190
    def _send_request(self, button: Union[LcdButton, Gkey], sock) -> None:
1✔
191
        """
192
        Sent action to DCS-BIOS via network socket.
193

194
        :param button: LcdButton or Gkey
195
        :param sock: network socket
196
        """
197
        for request in self.plane.button_request(button).split('|'):
1✔
198
            sock.sendto(bytes(request, 'utf-8'), SEND_ADDR)
1✔
199
            sleep(0.05)
1✔
200

201
    def clear(self, true_clear=False) -> None:
1✔
202
        """
203
        Clear LCD.
204

205
        :param true_clear:
206
        """
207
        LOG.debug(f'Clear LCD type: {self.lcd.type}')
1✔
208
        lcd_sdk.clear_display(true_clear)
1✔
209

210
    def _prepare_image(self) -> Image.Image:
1✔
211
        """
212
        Prepare image for base of LCD type.
213

214
        For G13/G15/G510 takes first 4 or fewer elements of list and display as 4 rows.
215
        For G19 takes first 8 or fewer elements of list and display as 8 rows.
216
        :return: image instance ready display on LCD
217
        """
218
        img = Image.new(mode=self.lcd.mode.value, size=(self.lcd.width, self.lcd.height), color=self.lcd.background)
1✔
219
        draw = ImageDraw.Draw(img)
1✔
220
        for line_no, line in enumerate(self._display):
1✔
221
            draw.text(xy=(0, self.vert_space * line_no), text=line, fill=self.lcd.foreground, font=self.lcd.font_s)
1✔
222
        return img
1✔
223

224
    def __str__(self) -> str:
1✔
225
        return f'{type(self).__name__}: {self.lcd.width}x{self.lcd.height}'
1✔
226

227
    def __repr__(self) -> str:
1✔
228
        return f'{super().__repr__()} with: {pformat(self.__dict__)}'
1✔
229

230

231
class G13(KeyboardManager):
1✔
232
    """Logitech`s keyboard with mono LCD."""
233
    def __init__(self, parser: ProtocolParser, **kwargs) -> None:
1✔
234
        """
235
        Logitech`s keyboard with mono LCD.
236

237
        Support for: G13
238
        :param parser: DCS-BIOS parser instance
239
        """
240
        LcdMono.set_fonts(kwargs['fonts'])
1✔
241
        super().__init__(parser, lcd_type=LcdMono)
1✔
242
        self.model = ModelG13
1✔
243
        self.buttons = (LcdButton.ONE, LcdButton.TWO, LcdButton.THREE, LcdButton.FOUR)
1✔
244
        self.gkey = generate_gkey(key=self.model.gkeys, mode=self.model.modes)
1✔
245
        self.vert_space = 10
1✔
246

247

248
class G510(KeyboardManager):
1✔
249
    """Logitech`s keyboard with mono LCD."""
250
    def __init__(self, parser: ProtocolParser, **kwargs) -> None:
1✔
251
        """
252
        Logitech`s keyboard with mono LCD.
253

254
        Support for: G510
255
        :param parser: DCS-BIOS parser instance
256
        """
257
        LcdMono.set_fonts(kwargs['fonts'])
1✔
258
        super().__init__(parser, lcd_type=LcdMono)
1✔
259
        self.model = ModelG510
1✔
260
        self.buttons = (LcdButton.ONE, LcdButton.TWO, LcdButton.THREE, LcdButton.FOUR)
1✔
261
        self.gkey = generate_gkey(key=self.model.gkeys, mode=self.model.modes)
1✔
262
        self.vert_space = 10
1✔
263

264

265
class G15v1(KeyboardManager):
1✔
266
    """Logitech`s keyboard with mono LCD."""
267
    def __init__(self, parser: ProtocolParser, **kwargs) -> None:
1✔
268
        """
269
        Logitech`s keyboard with mono LCD.
270

271
        Support for: G15 v1
272
        :param parser: DCS-BIOS parser instance
273
        """
274
        LcdMono.set_fonts(kwargs['fonts'])
1✔
275
        super().__init__(parser, lcd_type=LcdMono)
1✔
276
        self.model = ModelG15v1
1✔
277
        self.buttons = (LcdButton.ONE, LcdButton.TWO, LcdButton.THREE, LcdButton.FOUR)
1✔
278
        self.gkey = generate_gkey(key=self.model.gkeys, mode=self.model.modes)
1✔
279
        self.vert_space = 10
1✔
280

281

282
class G15v2(KeyboardManager):
1✔
283
    """Logitech`s keyboard with mono LCD."""
284
    def __init__(self, parser: ProtocolParser, **kwargs) -> None:
1✔
285
        """
286
        Logitech`s keyboard with mono LCD.
287

288
        Support for: G15 v2
289
        :param parser: DCS-BIOS parser instance
290
        """
291
        LcdMono.set_fonts(kwargs['fonts'])
1✔
292
        super().__init__(parser, lcd_type=LcdMono)
1✔
293
        self.model = ModelG15v2
1✔
294
        self.buttons = (LcdButton.ONE, LcdButton.TWO, LcdButton.THREE, LcdButton.FOUR)
1✔
295
        self.gkey = generate_gkey(key=self.model.gkeys, mode=self.model.modes)
1✔
296
        self.vert_space = 10
1✔
297

298

299
class G19(KeyboardManager):
1✔
300
    """Logitech`s keyboard with color LCD."""
301
    def __init__(self, parser: ProtocolParser, **kwargs) -> None:
1✔
302
        """
303
        Logitech`s keyboard with color LCD.
304

305
        Support for: G19
306
        :param parser: DCS-BIOS parser instance
307
        """
308
        LcdColor.set_fonts(kwargs['fonts'])
1✔
309
        super().__init__(parser, lcd_type=LcdColor)
1✔
310
        self.model = ModelG19
1✔
311
        self.buttons = (LcdButton.LEFT, LcdButton.RIGHT, LcdButton.UP, LcdButton.DOWN, LcdButton.OK, LcdButton.CANCEL, LcdButton.MENU)
1✔
312
        self.gkey = generate_gkey(key=self.model.gkeys, mode=self.model.modes)
1✔
313
        self.vert_space = 40
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