Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "tachywooting"
version = "0.2.3"
version = "0.2.4"
description = "Python interface for Wooting analog keyboards"
readme = "README.md"
requires-python = ">=3.9,<3.15"
Expand Down
27 changes: 20 additions & 7 deletions tachywooting/wooting_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,25 +961,39 @@ def _to_keycodes(self, target_keys: Sequence[str | int]) -> list[int]:

raise TypeError("target_keys must be a list of all strings or all integers")

def _warn_invalid_keycodes(self, target_codes: Sequence[int]) -> None:
def _warn_invalid_keycodes(self, target_codes: Sequence[int]) -> list[int]:
"""Warn if any target keycode is not present on the connected Wooting device.

Uses a raw ``read_analog`` probe at initialization time. The SDK returns
``WootingAnalogResult_NoMapping`` for keycodes that have no physical key
on the device (e.g. asking for F1 on a 3-key UwU keyboard).
"""
if not self.initialized or lib is None:
return
return []
no_mapping = int(lib.WootingAnalogResult_NoMapping)
missing = []
for code in target_codes:
raw = float(lib.wooting_analog_read_analog(int(code)))
if int(raw) == no_mapping:
missing.append(int(code))
label = convert_keycode_to_char(code) or str(code)
_log.warning(
"Keycode %d (%r) is not mapped on the connected Wooting device — "
"it will never register a press.",
code, label,
)
return missing

def validate_analog_keys(self, keys: Sequence[str | int], *, strict: bool = True) -> list[int]:
"""Validate that all requested keys are mapped as analog keys."""
if not self.initialized:
raise ValueError('Keyboard must be initialized through "initialize_keyboard()".')
codes = self._to_keycodes(list(keys))
missing = self._warn_invalid_keycodes(codes)
if strict and missing:
labels = ", ".join(convert_keycode_to_char(code) or str(code) for code in missing)
raise RuntimeError(f"Analog key(s) not available on the connected Wooting keyboard: {labels}")
return codes

def _ensure_target_cache(self, target_codes: Sequence[int]) -> tuple[tuple[int, ...], set[int]]:
tgt_tuple = tuple(int(c) for c in target_codes)
Expand Down Expand Up @@ -1552,7 +1566,7 @@ def acquire_analog_values(
"Use acquire_integer_values instead."
)

self._warn_invalid_keycodes(self._to_keycodes(list(target_keys)))
self.validate_analog_keys(target_keys)

result = self._acquire_raw_values(
target_keys=target_keys,
Expand Down Expand Up @@ -1650,7 +1664,7 @@ def acquire_integer_values(
"Use acquire_analog_values instead."
)

self._warn_invalid_keycodes(self._to_keycodes(list(target_keys)))
self.validate_analog_keys(target_keys)

result = self._acquire_raw_values(
target_keys=target_keys,
Expand Down Expand Up @@ -1744,11 +1758,10 @@ def wait_keys_light_press(
raise ValueError("timeout_seconds must be > 0 if provided")

# --- resolve keycodes ---
target_codes = self._to_keycodes(target_keys)
self._warn_invalid_keycodes(target_codes)
target_codes = self.validate_analog_keys(target_keys)

# >>> quit key resolution
quit_codes = self._to_keycodes([quit_key]) if quit_key is not None else []
quit_codes = self.validate_analog_keys([quit_key]) if quit_key is not None else []
quit_code = int(quit_codes[0]) if quit_codes else None

interval = 1.0 / 1000.0 # fixed 1000 Hz
Expand Down
28 changes: 28 additions & 0 deletions tests/test_analog_key_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest

from tachywooting import wooting_utils
from tachywooting.wooting_utils import WOOTING_ACQUISITION


class FakeLib:
WootingAnalogResult_NoMapping = -1993

def wooting_analog_read_analog(self, code):
return self.WootingAnalogResult_NoMapping if code == 4 else 0.0


def test_validate_analog_keys_rejects_unmapped_keys(monkeypatch):
acquisition = WOOTING_ACQUISITION.__new__(WOOTING_ACQUISITION)
acquisition.initialized = True
monkeypatch.setattr(wooting_utils, "lib", FakeLib())

with pytest.raises(RuntimeError, match="Analog key"):
acquisition.validate_analog_keys(["a"])


def test_validate_analog_keys_returns_mapped_keycodes(monkeypatch):
acquisition = WOOTING_ACQUISITION.__new__(WOOTING_ACQUISITION)
acquisition.initialized = True
monkeypatch.setattr(wooting_utils, "lib", FakeLib())

assert acquisition.validate_analog_keys(["b"]) == [5]