summaryrefslogtreecommitdiff
path: root/kvmd
diff options
context:
space:
mode:
authorDevaev Maxim <[email protected]>2020-11-12 21:03:28 +0300
committerDevaev Maxim <[email protected]>2020-11-12 21:03:28 +0300
commit87cc8cf7b0e00f7f0a9badf551067aafad6da4ce (patch)
treecdd802d124236ded6969dc5d80c764deec357e02 /kvmd
parent79ef26e2f441f6c742a5da78b08ea2d5a4cc6e18 (diff)
parent0984f0cb36c5881669010c62e986cc8bc7fe019a (diff)
Merge branch 'spi'
Diffstat (limited to 'kvmd')
-rw-r--r--kvmd/aiogp.py6
-rw-r--r--kvmd/keyboard/mappings.py216
-rw-r--r--kvmd/keyboard/mappings.py.mako8
-rw-r--r--kvmd/plugins/__init__.py2
-rw-r--r--kvmd/plugins/hid/_mcu/__init__.py423
-rw-r--r--kvmd/plugins/hid/_mcu/gpio.py78
-rw-r--r--kvmd/plugins/hid/serial.py438
-rw-r--r--kvmd/plugins/hid/spi.py179
8 files changed, 833 insertions, 517 deletions
diff --git a/kvmd/aiogp.py b/kvmd/aiogp.py
index ba406e8d..46252914 100644
--- a/kvmd/aiogp.py
+++ b/kvmd/aiogp.py
@@ -34,12 +34,12 @@ from . import aiotools
# =====
-async def pulse(line: gpiod.Line, delay: float, final: float) -> None:
+async def pulse(line: gpiod.Line, delay: float, final: float, inverted: bool=False) -> None:
try:
- line.set_value(1)
+ line.set_value(int(not inverted))
await asyncio.sleep(delay)
finally:
- line.set_value(0)
+ line.set_value(int(inverted))
await asyncio.sleep(final)
diff --git a/kvmd/keyboard/mappings.py b/kvmd/keyboard/mappings.py
index 0185a0bc..a841e8b8 100644
--- a/kvmd/keyboard/mappings.py
+++ b/kvmd/keyboard/mappings.py
@@ -27,7 +27,7 @@ from typing import Dict
# =====
@dataclasses.dataclass(frozen=True)
-class SerialKey:
+class McuKey:
code: int
@@ -39,117 +39,117 @@ class OtgKey:
@dataclasses.dataclass(frozen=True)
class Key:
- serial: SerialKey
+ mcu: McuKey
otg: OtgKey
KEYMAP: Dict[str, Key] = {
- "KeyA": Key(serial=SerialKey(code=1), otg=OtgKey(code=4, is_modifier=False)),
- "KeyB": Key(serial=SerialKey(code=2), otg=OtgKey(code=5, is_modifier=False)),
- "KeyC": Key(serial=SerialKey(code=3), otg=OtgKey(code=6, is_modifier=False)),
- "KeyD": Key(serial=SerialKey(code=4), otg=OtgKey(code=7, is_modifier=False)),
- "KeyE": Key(serial=SerialKey(code=5), otg=OtgKey(code=8, is_modifier=False)),
- "KeyF": Key(serial=SerialKey(code=6), otg=OtgKey(code=9, is_modifier=False)),
- "KeyG": Key(serial=SerialKey(code=7), otg=OtgKey(code=10, is_modifier=False)),
- "KeyH": Key(serial=SerialKey(code=8), otg=OtgKey(code=11, is_modifier=False)),
- "KeyI": Key(serial=SerialKey(code=9), otg=OtgKey(code=12, is_modifier=False)),
- "KeyJ": Key(serial=SerialKey(code=10), otg=OtgKey(code=13, is_modifier=False)),
- "KeyK": Key(serial=SerialKey(code=11), otg=OtgKey(code=14, is_modifier=False)),
- "KeyL": Key(serial=SerialKey(code=12), otg=OtgKey(code=15, is_modifier=False)),
- "KeyM": Key(serial=SerialKey(code=13), otg=OtgKey(code=16, is_modifier=False)),
- "KeyN": Key(serial=SerialKey(code=14), otg=OtgKey(code=17, is_modifier=False)),
- "KeyO": Key(serial=SerialKey(code=15), otg=OtgKey(code=18, is_modifier=False)),
- "KeyP": Key(serial=SerialKey(code=16), otg=OtgKey(code=19, is_modifier=False)),
- "KeyQ": Key(serial=SerialKey(code=17), otg=OtgKey(code=20, is_modifier=False)),
- "KeyR": Key(serial=SerialKey(code=18), otg=OtgKey(code=21, is_modifier=False)),
- "KeyS": Key(serial=SerialKey(code=19), otg=OtgKey(code=22, is_modifier=False)),
- "KeyT": Key(serial=SerialKey(code=20), otg=OtgKey(code=23, is_modifier=False)),
- "KeyU": Key(serial=SerialKey(code=21), otg=OtgKey(code=24, is_modifier=False)),
- "KeyV": Key(serial=SerialKey(code=22), otg=OtgKey(code=25, is_modifier=False)),
- "KeyW": Key(serial=SerialKey(code=23), otg=OtgKey(code=26, is_modifier=False)),
- "KeyX": Key(serial=SerialKey(code=24), otg=OtgKey(code=27, is_modifier=False)),
- "KeyY": Key(serial=SerialKey(code=25), otg=OtgKey(code=28, is_modifier=False)),
- "KeyZ": Key(serial=SerialKey(code=26), otg=OtgKey(code=29, is_modifier=False)),
- "Digit1": Key(serial=SerialKey(code=27), otg=OtgKey(code=30, is_modifier=False)),
- "Digit2": Key(serial=SerialKey(code=28), otg=OtgKey(code=31, is_modifier=False)),
- "Digit3": Key(serial=SerialKey(code=29), otg=OtgKey(code=32, is_modifier=False)),
- "Digit4": Key(serial=SerialKey(code=30), otg=OtgKey(code=33, is_modifier=False)),
- "Digit5": Key(serial=SerialKey(code=31), otg=OtgKey(code=34, is_modifier=False)),
- "Digit6": Key(serial=SerialKey(code=32), otg=OtgKey(code=35, is_modifier=False)),
- "Digit7": Key(serial=SerialKey(code=33), otg=OtgKey(code=36, is_modifier=False)),
- "Digit8": Key(serial=SerialKey(code=34), otg=OtgKey(code=37, is_modifier=False)),
- "Digit9": Key(serial=SerialKey(code=35), otg=OtgKey(code=38, is_modifier=False)),
- "Digit0": Key(serial=SerialKey(code=36), otg=OtgKey(code=39, is_modifier=False)),
- "Enter": Key(serial=SerialKey(code=37), otg=OtgKey(code=40, is_modifier=False)),
- "Escape": Key(serial=SerialKey(code=38), otg=OtgKey(code=41, is_modifier=False)),
- "Backspace": Key(serial=SerialKey(code=39), otg=OtgKey(code=42, is_modifier=False)),
- "Tab": Key(serial=SerialKey(code=40), otg=OtgKey(code=43, is_modifier=False)),
- "Space": Key(serial=SerialKey(code=41), otg=OtgKey(code=44, is_modifier=False)),
- "Minus": Key(serial=SerialKey(code=42), otg=OtgKey(code=45, is_modifier=False)),
- "Equal": Key(serial=SerialKey(code=43), otg=OtgKey(code=46, is_modifier=False)),
- "BracketLeft": Key(serial=SerialKey(code=44), otg=OtgKey(code=47, is_modifier=False)),
- "BracketRight": Key(serial=SerialKey(code=45), otg=OtgKey(code=48, is_modifier=False)),
- "Backslash": Key(serial=SerialKey(code=46), otg=OtgKey(code=49, is_modifier=False)),
- "Semicolon": Key(serial=SerialKey(code=47), otg=OtgKey(code=51, is_modifier=False)),
- "Quote": Key(serial=SerialKey(code=48), otg=OtgKey(code=52, is_modifier=False)),
- "Backquote": Key(serial=SerialKey(code=49), otg=OtgKey(code=53, is_modifier=False)),
- "Comma": Key(serial=SerialKey(code=50), otg=OtgKey(code=54, is_modifier=False)),
- "Period": Key(serial=SerialKey(code=51), otg=OtgKey(code=55, is_modifier=False)),
- "Slash": Key(serial=SerialKey(code=52), otg=OtgKey(code=56, is_modifier=False)),
- "CapsLock": Key(serial=SerialKey(code=53), otg=OtgKey(code=57, is_modifier=False)),
- "F1": Key(serial=SerialKey(code=54), otg=OtgKey(code=58, is_modifier=False)),
- "F2": Key(serial=SerialKey(code=55), otg=OtgKey(code=59, is_modifier=False)),
- "F3": Key(serial=SerialKey(code=56), otg=OtgKey(code=60, is_modifier=False)),
- "F4": Key(serial=SerialKey(code=57), otg=OtgKey(code=61, is_modifier=False)),
- "F5": Key(serial=SerialKey(code=58), otg=OtgKey(code=62, is_modifier=False)),
- "F6": Key(serial=SerialKey(code=59), otg=OtgKey(code=63, is_modifier=False)),
- "F7": Key(serial=SerialKey(code=60), otg=OtgKey(code=64, is_modifier=False)),
- "F8": Key(serial=SerialKey(code=61), otg=OtgKey(code=65, is_modifier=False)),
- "F9": Key(serial=SerialKey(code=62), otg=OtgKey(code=66, is_modifier=False)),
- "F10": Key(serial=SerialKey(code=63), otg=OtgKey(code=67, is_modifier=False)),
- "F11": Key(serial=SerialKey(code=64), otg=OtgKey(code=68, is_modifier=False)),
- "F12": Key(serial=SerialKey(code=65), otg=OtgKey(code=69, is_modifier=False)),
- "PrintScreen": Key(serial=SerialKey(code=66), otg=OtgKey(code=70, is_modifier=False)),
- "Insert": Key(serial=SerialKey(code=67), otg=OtgKey(code=73, is_modifier=False)),
- "Home": Key(serial=SerialKey(code=68), otg=OtgKey(code=74, is_modifier=False)),
- "PageUp": Key(serial=SerialKey(code=69), otg=OtgKey(code=75, is_modifier=False)),
- "Delete": Key(serial=SerialKey(code=70), otg=OtgKey(code=76, is_modifier=False)),
- "End": Key(serial=SerialKey(code=71), otg=OtgKey(code=77, is_modifier=False)),
- "PageDown": Key(serial=SerialKey(code=72), otg=OtgKey(code=78, is_modifier=False)),
- "ArrowRight": Key(serial=SerialKey(code=73), otg=OtgKey(code=79, is_modifier=False)),
- "ArrowLeft": Key(serial=SerialKey(code=74), otg=OtgKey(code=80, is_modifier=False)),
- "ArrowDown": Key(serial=SerialKey(code=75), otg=OtgKey(code=81, is_modifier=False)),
- "ArrowUp": Key(serial=SerialKey(code=76), otg=OtgKey(code=82, is_modifier=False)),
- "ControlLeft": Key(serial=SerialKey(code=77), otg=OtgKey(code=1, is_modifier=True)),
- "ShiftLeft": Key(serial=SerialKey(code=78), otg=OtgKey(code=2, is_modifier=True)),
- "AltLeft": Key(serial=SerialKey(code=79), otg=OtgKey(code=4, is_modifier=True)),
- "MetaLeft": Key(serial=SerialKey(code=80), otg=OtgKey(code=8, is_modifier=True)),
- "ControlRight": Key(serial=SerialKey(code=81), otg=OtgKey(code=16, is_modifier=True)),
- "ShiftRight": Key(serial=SerialKey(code=82), otg=OtgKey(code=32, is_modifier=True)),
- "AltRight": Key(serial=SerialKey(code=83), otg=OtgKey(code=64, is_modifier=True)),
- "MetaRight": Key(serial=SerialKey(code=84), otg=OtgKey(code=128, is_modifier=True)),
- "Pause": Key(serial=SerialKey(code=85), otg=OtgKey(code=72, is_modifier=False)),
- "ScrollLock": Key(serial=SerialKey(code=86), otg=OtgKey(code=71, is_modifier=False)),
- "NumLock": Key(serial=SerialKey(code=87), otg=OtgKey(code=83, is_modifier=False)),
- "ContextMenu": Key(serial=SerialKey(code=88), otg=OtgKey(code=101, is_modifier=False)),
- "NumpadDivide": Key(serial=SerialKey(code=89), otg=OtgKey(code=84, is_modifier=False)),
- "NumpadMultiply": Key(serial=SerialKey(code=90), otg=OtgKey(code=85, is_modifier=False)),
- "NumpadSubtract": Key(serial=SerialKey(code=91), otg=OtgKey(code=86, is_modifier=False)),
- "NumpadAdd": Key(serial=SerialKey(code=92), otg=OtgKey(code=87, is_modifier=False)),
- "NumpadEnter": Key(serial=SerialKey(code=93), otg=OtgKey(code=88, is_modifier=False)),
- "Numpad1": Key(serial=SerialKey(code=94), otg=OtgKey(code=89, is_modifier=False)),
- "Numpad2": Key(serial=SerialKey(code=95), otg=OtgKey(code=90, is_modifier=False)),
- "Numpad3": Key(serial=SerialKey(code=96), otg=OtgKey(code=91, is_modifier=False)),
- "Numpad4": Key(serial=SerialKey(code=97), otg=OtgKey(code=92, is_modifier=False)),
- "Numpad5": Key(serial=SerialKey(code=98), otg=OtgKey(code=93, is_modifier=False)),
- "Numpad6": Key(serial=SerialKey(code=99), otg=OtgKey(code=94, is_modifier=False)),
- "Numpad7": Key(serial=SerialKey(code=100), otg=OtgKey(code=95, is_modifier=False)),
- "Numpad8": Key(serial=SerialKey(code=101), otg=OtgKey(code=96, is_modifier=False)),
- "Numpad9": Key(serial=SerialKey(code=102), otg=OtgKey(code=97, is_modifier=False)),
- "Numpad0": Key(serial=SerialKey(code=103), otg=OtgKey(code=98, is_modifier=False)),
- "NumpadDecimal": Key(serial=SerialKey(code=104), otg=OtgKey(code=99, is_modifier=False)),
- "Power": Key(serial=SerialKey(code=105), otg=OtgKey(code=102, is_modifier=False)),
- "IntlBackslash": Key(serial=SerialKey(code=106), otg=OtgKey(code=100, is_modifier=False)),
+ "KeyA": Key(mcu=McuKey(code=1), otg=OtgKey(code=4, is_modifier=False)),
+ "KeyB": Key(mcu=McuKey(code=2), otg=OtgKey(code=5, is_modifier=False)),
+ "KeyC": Key(mcu=McuKey(code=3), otg=OtgKey(code=6, is_modifier=False)),
+ "KeyD": Key(mcu=McuKey(code=4), otg=OtgKey(code=7, is_modifier=False)),
+ "KeyE": Key(mcu=McuKey(code=5), otg=OtgKey(code=8, is_modifier=False)),
+ "KeyF": Key(mcu=McuKey(code=6), otg=OtgKey(code=9, is_modifier=False)),
+ "KeyG": Key(mcu=McuKey(code=7), otg=OtgKey(code=10, is_modifier=False)),
+ "KeyH": Key(mcu=McuKey(code=8), otg=OtgKey(code=11, is_modifier=False)),
+ "KeyI": Key(mcu=McuKey(code=9), otg=OtgKey(code=12, is_modifier=False)),
+ "KeyJ": Key(mcu=McuKey(code=10), otg=OtgKey(code=13, is_modifier=False)),
+ "KeyK": Key(mcu=McuKey(code=11), otg=OtgKey(code=14, is_modifier=False)),
+ "KeyL": Key(mcu=McuKey(code=12), otg=OtgKey(code=15, is_modifier=False)),
+ "KeyM": Key(mcu=McuKey(code=13), otg=OtgKey(code=16, is_modifier=False)),
+ "KeyN": Key(mcu=McuKey(code=14), otg=OtgKey(code=17, is_modifier=False)),
+ "KeyO": Key(mcu=McuKey(code=15), otg=OtgKey(code=18, is_modifier=False)),
+ "KeyP": Key(mcu=McuKey(code=16), otg=OtgKey(code=19, is_modifier=False)),
+ "KeyQ": Key(mcu=McuKey(code=17), otg=OtgKey(code=20, is_modifier=False)),
+ "KeyR": Key(mcu=McuKey(code=18), otg=OtgKey(code=21, is_modifier=False)),
+ "KeyS": Key(mcu=McuKey(code=19), otg=OtgKey(code=22, is_modifier=False)),
+ "KeyT": Key(mcu=McuKey(code=20), otg=OtgKey(code=23, is_modifier=False)),
+ "KeyU": Key(mcu=McuKey(code=21), otg=OtgKey(code=24, is_modifier=False)),
+ "KeyV": Key(mcu=McuKey(code=22), otg=OtgKey(code=25, is_modifier=False)),
+ "KeyW": Key(mcu=McuKey(code=23), otg=OtgKey(code=26, is_modifier=False)),
+ "KeyX": Key(mcu=McuKey(code=24), otg=OtgKey(code=27, is_modifier=False)),
+ "KeyY": Key(mcu=McuKey(code=25), otg=OtgKey(code=28, is_modifier=False)),
+ "KeyZ": Key(mcu=McuKey(code=26), otg=OtgKey(code=29, is_modifier=False)),
+ "Digit1": Key(mcu=McuKey(code=27), otg=OtgKey(code=30, is_modifier=False)),
+ "Digit2": Key(mcu=McuKey(code=28), otg=OtgKey(code=31, is_modifier=False)),
+ "Digit3": Key(mcu=McuKey(code=29), otg=OtgKey(code=32, is_modifier=False)),
+ "Digit4": Key(mcu=McuKey(code=30), otg=OtgKey(code=33, is_modifier=False)),
+ "Digit5": Key(mcu=McuKey(code=31), otg=OtgKey(code=34, is_modifier=False)),
+ "Digit6": Key(mcu=McuKey(code=32), otg=OtgKey(code=35, is_modifier=False)),
+ "Digit7": Key(mcu=McuKey(code=33), otg=OtgKey(code=36, is_modifier=False)),
+ "Digit8": Key(mcu=McuKey(code=34), otg=OtgKey(code=37, is_modifier=False)),
+ "Digit9": Key(mcu=McuKey(code=35), otg=OtgKey(code=38, is_modifier=False)),
+ "Digit0": Key(mcu=McuKey(code=36), otg=OtgKey(code=39, is_modifier=False)),
+ "Enter": Key(mcu=McuKey(code=37), otg=OtgKey(code=40, is_modifier=False)),
+ "Escape": Key(mcu=McuKey(code=38), otg=OtgKey(code=41, is_modifier=False)),
+ "Backspace": Key(mcu=McuKey(code=39), otg=OtgKey(code=42, is_modifier=False)),
+ "Tab": Key(mcu=McuKey(code=40), otg=OtgKey(code=43, is_modifier=False)),
+ "Space": Key(mcu=McuKey(code=41), otg=OtgKey(code=44, is_modifier=False)),
+ "Minus": Key(mcu=McuKey(code=42), otg=OtgKey(code=45, is_modifier=False)),
+ "Equal": Key(mcu=McuKey(code=43), otg=OtgKey(code=46, is_modifier=False)),
+ "BracketLeft": Key(mcu=McuKey(code=44), otg=OtgKey(code=47, is_modifier=False)),
+ "BracketRight": Key(mcu=McuKey(code=45), otg=OtgKey(code=48, is_modifier=False)),
+ "Backslash": Key(mcu=McuKey(code=46), otg=OtgKey(code=49, is_modifier=False)),
+ "Semicolon": Key(mcu=McuKey(code=47), otg=OtgKey(code=51, is_modifier=False)),
+ "Quote": Key(mcu=McuKey(code=48), otg=OtgKey(code=52, is_modifier=False)),
+ "Backquote": Key(mcu=McuKey(code=49), otg=OtgKey(code=53, is_modifier=False)),
+ "Comma": Key(mcu=McuKey(code=50), otg=OtgKey(code=54, is_modifier=False)),
+ "Period": Key(mcu=McuKey(code=51), otg=OtgKey(code=55, is_modifier=False)),
+ "Slash": Key(mcu=McuKey(code=52), otg=OtgKey(code=56, is_modifier=False)),
+ "CapsLock": Key(mcu=McuKey(code=53), otg=OtgKey(code=57, is_modifier=False)),
+ "F1": Key(mcu=McuKey(code=54), otg=OtgKey(code=58, is_modifier=False)),
+ "F2": Key(mcu=McuKey(code=55), otg=OtgKey(code=59, is_modifier=False)),
+ "F3": Key(mcu=McuKey(code=56), otg=OtgKey(code=60, is_modifier=False)),
+ "F4": Key(mcu=McuKey(code=57), otg=OtgKey(code=61, is_modifier=False)),
+ "F5": Key(mcu=McuKey(code=58), otg=OtgKey(code=62, is_modifier=False)),
+ "F6": Key(mcu=McuKey(code=59), otg=OtgKey(code=63, is_modifier=False)),
+ "F7": Key(mcu=McuKey(code=60), otg=OtgKey(code=64, is_modifier=False)),
+ "F8": Key(mcu=McuKey(code=61), otg=OtgKey(code=65, is_modifier=False)),
+ "F9": Key(mcu=McuKey(code=62), otg=OtgKey(code=66, is_modifier=False)),
+ "F10": Key(mcu=McuKey(code=63), otg=OtgKey(code=67, is_modifier=False)),
+ "F11": Key(mcu=McuKey(code=64), otg=OtgKey(code=68, is_modifier=False)),
+ "F12": Key(mcu=McuKey(code=65), otg=OtgKey(code=69, is_modifier=False)),
+ "PrintScreen": Key(mcu=McuKey(code=66), otg=OtgKey(code=70, is_modifier=False)),
+ "Insert": Key(mcu=McuKey(code=67), otg=OtgKey(code=73, is_modifier=False)),
+ "Home": Key(mcu=McuKey(code=68), otg=OtgKey(code=74, is_modifier=False)),
+ "PageUp": Key(mcu=McuKey(code=69), otg=OtgKey(code=75, is_modifier=False)),
+ "Delete": Key(mcu=McuKey(code=70), otg=OtgKey(code=76, is_modifier=False)),
+ "End": Key(mcu=McuKey(code=71), otg=OtgKey(code=77, is_modifier=False)),
+ "PageDown": Key(mcu=McuKey(code=72), otg=OtgKey(code=78, is_modifier=False)),
+ "ArrowRight": Key(mcu=McuKey(code=73), otg=OtgKey(code=79, is_modifier=False)),
+ "ArrowLeft": Key(mcu=McuKey(code=74), otg=OtgKey(code=80, is_modifier=False)),
+ "ArrowDown": Key(mcu=McuKey(code=75), otg=OtgKey(code=81, is_modifier=False)),
+ "ArrowUp": Key(mcu=McuKey(code=76), otg=OtgKey(code=82, is_modifier=False)),
+ "ControlLeft": Key(mcu=McuKey(code=77), otg=OtgKey(code=1, is_modifier=True)),
+ "ShiftLeft": Key(mcu=McuKey(code=78), otg=OtgKey(code=2, is_modifier=True)),
+ "AltLeft": Key(mcu=McuKey(code=79), otg=OtgKey(code=4, is_modifier=True)),
+ "MetaLeft": Key(mcu=McuKey(code=80), otg=OtgKey(code=8, is_modifier=True)),
+ "ControlRight": Key(mcu=McuKey(code=81), otg=OtgKey(code=16, is_modifier=True)),
+ "ShiftRight": Key(mcu=McuKey(code=82), otg=OtgKey(code=32, is_modifier=True)),
+ "AltRight": Key(mcu=McuKey(code=83), otg=OtgKey(code=64, is_modifier=True)),
+ "MetaRight": Key(mcu=McuKey(code=84), otg=OtgKey(code=128, is_modifier=True)),
+ "Pause": Key(mcu=McuKey(code=85), otg=OtgKey(code=72, is_modifier=False)),
+ "ScrollLock": Key(mcu=McuKey(code=86), otg=OtgKey(code=71, is_modifier=False)),
+ "NumLock": Key(mcu=McuKey(code=87), otg=OtgKey(code=83, is_modifier=False)),
+ "ContextMenu": Key(mcu=McuKey(code=88), otg=OtgKey(code=101, is_modifier=False)),
+ "NumpadDivide": Key(mcu=McuKey(code=89), otg=OtgKey(code=84, is_modifier=False)),
+ "NumpadMultiply": Key(mcu=McuKey(code=90), otg=OtgKey(code=85, is_modifier=False)),
+ "NumpadSubtract": Key(mcu=McuKey(code=91), otg=OtgKey(code=86, is_modifier=False)),
+ "NumpadAdd": Key(mcu=McuKey(code=92), otg=OtgKey(code=87, is_modifier=False)),
+ "NumpadEnter": Key(mcu=McuKey(code=93), otg=OtgKey(code=88, is_modifier=False)),
+ "Numpad1": Key(mcu=McuKey(code=94), otg=OtgKey(code=89, is_modifier=False)),
+ "Numpad2": Key(mcu=McuKey(code=95), otg=OtgKey(code=90, is_modifier=False)),
+ "Numpad3": Key(mcu=McuKey(code=96), otg=OtgKey(code=91, is_modifier=False)),
+ "Numpad4": Key(mcu=McuKey(code=97), otg=OtgKey(code=92, is_modifier=False)),
+ "Numpad5": Key(mcu=McuKey(code=98), otg=OtgKey(code=93, is_modifier=False)),
+ "Numpad6": Key(mcu=McuKey(code=99), otg=OtgKey(code=94, is_modifier=False)),
+ "Numpad7": Key(mcu=McuKey(code=100), otg=OtgKey(code=95, is_modifier=False)),
+ "Numpad8": Key(mcu=McuKey(code=101), otg=OtgKey(code=96, is_modifier=False)),
+ "Numpad9": Key(mcu=McuKey(code=102), otg=OtgKey(code=97, is_modifier=False)),
+ "Numpad0": Key(mcu=McuKey(code=103), otg=OtgKey(code=98, is_modifier=False)),
+ "NumpadDecimal": Key(mcu=McuKey(code=104), otg=OtgKey(code=99, is_modifier=False)),
+ "Power": Key(mcu=McuKey(code=105), otg=OtgKey(code=102, is_modifier=False)),
+ "IntlBackslash": Key(mcu=McuKey(code=106), otg=OtgKey(code=100, is_modifier=False)),
}
diff --git a/kvmd/keyboard/mappings.py.mako b/kvmd/keyboard/mappings.py.mako
index 399ed75e..5f0fc9ad 100644
--- a/kvmd/keyboard/mappings.py.mako
+++ b/kvmd/keyboard/mappings.py.mako
@@ -27,7 +27,7 @@ from typing import Dict
# =====
@dataclasses.dataclass(frozen=True)
-class SerialKey:
+class McuKey:
code: int
@@ -39,13 +39,13 @@ class OtgKey:
@dataclasses.dataclass(frozen=True)
class Key:
- serial: SerialKey
+ mcu: McuKey
otg: OtgKey
<%! import operator %>
KEYMAP: Dict[str, Key] = {
-% for km in sorted(keymap, key=operator.attrgetter("serial_code")):
- "${km.web_name}": Key(serial=SerialKey(code=${km.serial_code}), otg=OtgKey(code=${km.otg_key.code}, is_modifier=${km.otg_key.is_modifier})),
+% for km in sorted(keymap, key=operator.attrgetter("mcu_code")):
+ "${km.web_name}": Key(mcu=McuKey(code=${km.mcu_code}), otg=OtgKey(code=${km.otg_key.code}, is_modifier=${km.otg_key.is_modifier})),
% endfor
}
diff --git a/kvmd/plugins/__init__.py b/kvmd/plugins/__init__.py
index ebdeaf0b..ea25bcfc 100644
--- a/kvmd/plugins/__init__.py
+++ b/kvmd/plugins/__init__.py
@@ -52,6 +52,8 @@ class BasePlugin:
def get_plugin_class(sub: str, name: str) -> Type[BasePlugin]:
assert sub
assert name
+ if name.startswith("_"):
+ raise UnknownPluginError(f"Unknown plugin '{sub}/{name}'")
try:
module = importlib.import_module(f"kvmd.plugins.{sub}.{name}")
except ModuleNotFoundError:
diff --git a/kvmd/plugins/hid/_mcu/__init__.py b/kvmd/plugins/hid/_mcu/__init__.py
new file mode 100644
index 00000000..3f925cd8
--- /dev/null
+++ b/kvmd/plugins/hid/_mcu/__init__.py
@@ -0,0 +1,423 @@
+# ========================================================================== #
+# #
+# KVMD - The main Pi-KVM daemon. #
+# #
+# Copyright (C) 2018 Maxim Devaev <[email protected]> #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <https://www.gnu.org/licenses/>. #
+# #
+# ========================================================================== #
+
+
+import os
+import multiprocessing
+import dataclasses
+import contextlib
+import queue
+import struct
+import time
+
+from typing import Tuple
+from typing import List
+from typing import Dict
+from typing import Iterable
+from typing import Generator
+from typing import AsyncGenerator
+
+from ....logging import get_logger
+
+from ....keyboard.mappings import KEYMAP
+
+from .... import tools
+from .... import aiotools
+from .... import aiomulti
+from .... import aioproc
+
+from ....yamlconf import Option
+
+from ....validators.basic import valid_bool
+from ....validators.basic import valid_int_f0
+from ....validators.basic import valid_int_f1
+from ....validators.basic import valid_float_f01
+from ....validators.hw import valid_gpio_pin_optional
+
+from .. import BaseHid
+
+from .gpio import Gpio
+
+
+# =====
+class _RequestError(Exception):
+ def __init__(self, msg: str, online: bool=False) -> None:
+ super().__init__(msg)
+ self.msg = msg
+ self.online = online
+
+
+class _PermRequestError(_RequestError):
+ pass
+
+
+class _TempRequestError(_RequestError):
+ pass
+
+
+# =====
+class _BaseEvent:
+ def make_command(self) -> bytes:
+ raise NotImplementedError
+
+
+class _ClearEvent(_BaseEvent):
+ def make_command(self) -> bytes:
+ return b"\x10\x00\x00\x00\x00"
+
+
[email protected](frozen=True)
+class _KeyEvent(_BaseEvent):
+ name: str
+ state: bool
+
+ def __post_init__(self) -> None:
+ assert self.name in KEYMAP
+
+ def make_command(self) -> bytes:
+ code = KEYMAP[self.name].mcu.code
+ return struct.pack(">BBBxx", 0x11, code, int(self.state))
+
+
[email protected](frozen=True)
+class _MouseButtonEvent(_BaseEvent):
+ name: str
+ state: bool
+
+ def __post_init__(self) -> None:
+ assert self.name in ["left", "right", "middle", "up", "down"]
+
+ def make_command(self) -> bytes:
+ (code, state_pressed, is_main) = {
+ "left": (0b10000000, 0b00001000, True),
+ "right": (0b01000000, 0b00000100, True),
+ "middle": (0b00100000, 0b00000010, True),
+ "up": (0b10000000, 0b00001000, False), # Back
+ "down": (0b01000000, 0b00000100, False), # Forward
+ }[self.name]
+ if self.state:
+ code |= state_pressed
+ if is_main:
+ main_code = code
+ extra_code = 0
+ else:
+ main_code = 0
+ extra_code = code
+ return struct.pack(">BBBxx", 0x13, main_code, extra_code)
+
+
[email protected](frozen=True)
+class _MouseMoveEvent(_BaseEvent):
+ to_x: int
+ to_y: int
+
+ def __post_init__(self) -> None:
+ assert -32768 <= self.to_x <= 32767
+ assert -32768 <= self.to_y <= 32767
+
+ def make_command(self) -> bytes:
+ return struct.pack(">Bhh", 0x12, self.to_x, self.to_y)
+
+
[email protected](frozen=True)
+class _MouseWheelEvent(_BaseEvent):
+ delta_x: int
+ delta_y: int
+
+ def __post_init__(self) -> None:
+ assert -127 <= self.delta_x <= 127
+ assert -127 <= self.delta_y <= 127
+
+ def make_command(self) -> bytes:
+ # Горизонтальная прокрутка пока не поддерживается
+ return struct.pack(">Bxbxx", 0x14, self.delta_y)
+
+
+# =====
+class BasePhyConnection:
+ def send(self, request: bytes) -> bytes:
+ raise NotImplementedError
+
+
+class BasePhy:
+ def has_device(self) -> bool:
+ raise NotImplementedError
+
+ @contextlib.contextmanager
+ def connected(self) -> Generator[BasePhyConnection, None, None]:
+ raise NotImplementedError
+
+
+class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-instance-attributes
+ def __init__( # pylint: disable=too-many-arguments,super-init-not-called
+ self,
+ phy: BasePhy,
+
+ reset_pin: int,
+ reset_inverted: bool,
+ reset_delay: float,
+
+ read_retries: int,
+ common_retries: int,
+ retries_delay: float,
+ errors_threshold: int,
+ noop: bool,
+ ) -> None:
+
+ multiprocessing.Process.__init__(self, daemon=True)
+
+ self.__read_retries = read_retries
+ self.__common_retries = common_retries
+ self.__retries_delay = retries_delay
+ self.__errors_threshold = errors_threshold
+ self.__noop = noop
+
+ self.__phy = phy
+ self.__gpio = Gpio(reset_pin, reset_inverted, reset_delay)
+
+ self.__events_queue: "multiprocessing.Queue[_BaseEvent]" = multiprocessing.Queue()
+
+ self.__notifier = aiomulti.AioProcessNotifier()
+ self.__state_flags = aiomulti.AioSharedFlags({
+ "online": True,
+ "caps": False,
+ "scroll": False,
+ "num": False,
+ }, self.__notifier)
+
+ self.__stop_event = multiprocessing.Event()
+
+ @classmethod
+ def get_plugin_options(cls) -> Dict:
+ return {
+ "reset_pin": Option(-1, type=valid_gpio_pin_optional),
+ "reset_inverted": Option(False, type=valid_bool),
+ "reset_delay": Option(0.1, type=valid_float_f01),
+
+ "read_retries": Option(10, type=valid_int_f1),
+ "common_retries": Option(100, type=valid_int_f1),
+ "retries_delay": Option(0.1, type=valid_float_f01),
+ "errors_threshold": Option(5, type=valid_int_f0),
+ "noop": Option(False, type=valid_bool),
+ }
+
+ def sysprep(self) -> None:
+ self.__gpio.open()
+ get_logger(0).info("Starting HID daemon ...")
+ self.start()
+
+ async def get_state(self) -> Dict:
+ state = await self.__state_flags.get()
+ return {
+ "online": state["online"],
+ "keyboard": {
+ "online": state["online"],
+ "leds": {
+ "caps": state["caps"],
+ "scroll": state["scroll"],
+ "num": state["num"],
+ },
+ },
+ "mouse": {"online": state["online"]},
+ }
+
+ async def poll_state(self) -> AsyncGenerator[Dict, None]:
+ prev_state: Dict = {}
+ while True:
+ state = await self.get_state()
+ if state != prev_state:
+ yield state
+ prev_state = state
+ await self.__notifier.wait()
+
+ @aiotools.atomic
+ async def reset(self) -> None:
+ await self.__gpio.reset()
+
+ @aiotools.atomic
+ async def cleanup(self) -> None:
+ logger = get_logger(0)
+ try:
+ if self.is_alive():
+ logger.info("Stopping HID daemon ...")
+ self.__stop_event.set()
+ if self.exitcode is not None:
+ self.join()
+ if self.__phy.has_device():
+ get_logger().info("Clearing HID events ...")
+ try:
+ with self.__phy.connected() as conn:
+ self.__process_command(conn, b"\x10\x00\x00\x00\x00")
+ except Exception:
+ logger.exception("Can't clear HID events")
+ finally:
+ self.__gpio.close()
+
+ # =====
+
+ def send_key_events(self, keys: Iterable[Tuple[str, bool]]) -> None:
+ for (key, state) in keys:
+ self.__queue_event(_KeyEvent(key, state))
+
+ def send_mouse_button_event(self, button: str, state: bool) -> None:
+ self.__queue_event(_MouseButtonEvent(button, state))
+
+ def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
+ self.__queue_event(_MouseMoveEvent(to_x, to_y))
+
+ def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
+ self.__queue_event(_MouseWheelEvent(delta_x, delta_y))
+
+ def clear_events(self) -> None:
+ # FIXME: Если очистка производится со стороны процесса хида, то возможна гонка между
+ # очисткой и добавлением события _ClearEvent. Неприятно, но не смертельно.
+ # Починить блокировкой после перехода на асинхронные очереди.
+ tools.clear_queue(self.__events_queue)
+ self.__queue_event(_ClearEvent())
+
+ def __queue_event(self, event: _BaseEvent) -> None:
+ if not self.__stop_event.is_set():
+ self.__events_queue.put_nowait(event)
+
+ def run(self) -> None: # pylint: disable=too-many-branches
+ logger = get_logger(0)
+
+ logger.info("Started HID pid=%d", os.getpid())
+ aioproc.ignore_sigint()
+ aioproc.rename_process("hid")
+
+ while not self.__stop_event.is_set():
+ try:
+ if self.__phy.has_device():
+ with self.__phy.connected() as conn:
+ while not (self.__stop_event.is_set() and self.__events_queue.qsize() == 0):
+ try:
+ event = self.__events_queue.get(timeout=0.1)
+ except queue.Empty:
+ self.__process_command(conn, b"\x01\x00\x00\x00\x00") # Ping
+ else:
+ if not self.__process_command(conn, event.make_command()):
+ self.clear_events()
+ else:
+ logger.error("Missing HID device")
+ time.sleep(1)
+ except Exception:
+ self.clear_events()
+ logger.exception("Unexpected HID error")
+ time.sleep(1)
+
+ def __process_command(self, conn: BasePhyConnection, command: bytes) -> bool:
+ return self.__process_request(conn, self.__make_request(command))
+
+ def __process_request(self, conn: BasePhyConnection, request: bytes) -> bool: # pylint: disable=too-many-branches
+ logger = get_logger()
+ error_messages: List[str] = []
+ live_log_errors = False
+
+ common_retries = self.__common_retries
+ read_retries = self.__read_retries
+ error_retval = False
+
+ while common_retries and read_retries:
+ response = self.__send_request(conn, request)
+ try:
+ if len(response) < 4:
+ read_retries -= 1
+ raise _TempRequestError(f"No response from HID: request={request!r}")
+
+ assert len(response) == 4, response
+ if self.__make_crc16(response[-4:-2]) != struct.unpack(">H", response[-2:])[0]:
+ request = self.__make_request(b"\x02\x00\x00\x00\x00") # Repeat an answer
+ raise _TempRequestError("Invalid response CRC; requesting response again ...")
+
+ code = response[1]
+ if code == 0x48: # Request timeout # pylint: disable=no-else-raise
+ raise _TempRequestError(f"Got request timeout from HID: request={request!r}")
+ elif code == 0x40: # CRC Error
+ raise _TempRequestError(f"Got CRC error of request from HID: request={request!r}")
+ elif code == 0x45: # Unknown command
+ raise _PermRequestError(f"HID did not recognize the request={request!r}", online=True)
+ elif code == 0x24: # Rebooted?
+ raise _PermRequestError("No previous command state inside HID, seems it was rebooted", online=True)
+ elif code == 0x20: # Done
+ self.__state_flags.update(online=True)
+ return True
+ elif code & 0x80: # Pong with leds
+ self.__state_flags.update(
+ online=True,
+ caps=bool(code & 0b00000001),
+ scroll=bool(code & 0x00000010),
+ num=bool(code & 0x00000100),
+ )
+ return True
+ raise _TempRequestError(f"Invalid response from HID: request={request!r}; code=0x{code:02X}")
+
+ except _RequestError as err:
+ common_retries -= 1
+ self.__state_flags.update(online=err.online)
+ error_retval = err.online
+
+ if live_log_errors:
+ logger.error(err.msg)
+ else:
+ error_messages.append(err.msg)
+ if len(error_messages) > self.__errors_threshold:
+ for msg in error_messages:
+ logger.error(msg)
+ error_messages = []
+ live_log_errors = True
+
+ if isinstance(err, _PermRequestError):
+ break
+ if common_retries and read_retries:
+ time.sleep(self.__retries_delay)
+
+ for msg in error_messages:
+ logger.error(msg)
+ if not (common_retries and read_retries):
+ logger.error("Can't process HID request due many errors: %r", request)
+ return error_retval
+
+ def __send_request(self, conn: BasePhyConnection, request: bytes) -> bytes:
+ if not self.__noop:
+ response = conn.send(request)
+ else:
+ response = b"\x33\x20" # Magic + OK
+ response += struct.pack(">H", self.__make_crc16(response))
+ return response
+
+ def __make_request(self, command: bytes) -> bytes:
+ request = b"\x33" + command
+ request += struct.pack(">H", self.__make_crc16(request))
+ assert len(request) == 8, (request, command)
+ return request
+
+ def __make_crc16(self, data: bytes) -> int:
+ crc = 0xFFFF
+ for byte in data:
+ crc = crc ^ byte
+ for _ in range(8):
+ if crc & 0x0001 == 0:
+ crc = crc >> 1
+ else:
+ crc = crc >> 1
+ crc = crc ^ 0xA001
+ return crc
diff --git a/kvmd/plugins/hid/_mcu/gpio.py b/kvmd/plugins/hid/_mcu/gpio.py
new file mode 100644
index 00000000..87f4b547
--- /dev/null
+++ b/kvmd/plugins/hid/_mcu/gpio.py
@@ -0,0 +1,78 @@
+# ========================================================================== #
+# #
+# KVMD - The main Pi-KVM daemon. #
+# #
+# Copyright (C) 2018 Maxim Devaev <[email protected]> #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <https://www.gnu.org/licenses/>. #
+# #
+# ========================================================================== #
+
+
+from typing import Optional
+
+import gpiod
+
+from ....logging import get_logger
+
+from .... import env
+from .... import aiotools
+from .... import aiogp
+
+
+# =====
+class Gpio:
+ def __init__(
+ self,
+ reset_pin: int,
+ reset_inverted: bool,
+ reset_delay: float,
+ ) -> None:
+
+ self.__reset_pin = reset_pin
+ self.__reset_inverted = reset_inverted
+ self.__reset_delay = reset_delay
+
+ self.__chip: Optional[gpiod.Chip] = None
+ self.__reset_line: Optional[gpiod.Line] = None
+ self.__reset_wip = False
+
+ def open(self) -> None:
+ if self.__reset_pin >= 0:
+ assert self.__chip is None
+ assert self.__reset_line is None
+ self.__chip = gpiod.Chip(env.GPIO_DEVICE_PATH)
+ self.__reset_line = self.__chip.get_line(self.__reset_pin)
+ self.__reset_line.request("kvmd::hid-mcu::reset", gpiod.LINE_REQ_DIR_OUT, default_vals=[int(self.__reset_inverted)])
+
+ def close(self) -> None:
+ if self.__chip:
+ try:
+ self.__chip.close()
+ except Exception:
+ pass
+
+ @aiotools.atomic
+ async def reset(self) -> None:
+ if self.__reset_pin >= 0:
+ assert self.__reset_line
+ if not self.__reset_wip:
+ self.__reset_wip = True
+ try:
+ await aiogp.pulse(self.__reset_line, self.__reset_delay, 1, self.__reset_inverted)
+ finally:
+ self.__reset_wip = False
+ get_logger(0).info("Reset HID performed")
+ else:
+ get_logger(0).info("Another reset HID in progress")
diff --git a/kvmd/plugins/hid/serial.py b/kvmd/plugins/hid/serial.py
index a45cd89a..dee992b6 100644
--- a/kvmd/plugins/hid/serial.py
+++ b/kvmd/plugins/hid/serial.py
@@ -21,443 +21,77 @@
import os
-import multiprocessing
-import dataclasses
-import queue
-import struct
-import errno
-import time
+import contextlib
-from typing import Tuple
-from typing import List
from typing import Dict
-from typing import Iterable
-from typing import AsyncGenerator
-from typing import Optional
+from typing import Generator
+from typing import Any
-import gpiod
import serial
-from ...logging import get_logger
-
-from ...keyboard.mappings import KEYMAP
-
-from ... import env
-from ... import tools
-from ... import aiotools
-from ... import aiomulti
-from ... import aioproc
-from ... import aiogp
-
from ...yamlconf import Option
-from ...validators.basic import valid_bool
-from ...validators.basic import valid_int_f0
-from ...validators.basic import valid_int_f1
from ...validators.basic import valid_float_f01
from ...validators.os import valid_abs_path
from ...validators.hw import valid_tty_speed
-from ...validators.hw import valid_gpio_pin_optional
-from . import BaseHid
+from ._mcu import BasePhyConnection
+from ._mcu import BasePhy
+from ._mcu import BaseMcuHid
# =====
-class _RequestError(Exception):
- def __init__(self, msg: str, online: bool=False) -> None:
- super().__init__(msg)
- self.msg = msg
- self.online = online
-
-
-class _PermRequestError(_RequestError):
- pass
-
-
-class _TempRequestError(_RequestError):
- pass
-
-
-# =====
-class _BaseEvent:
- def make_command(self) -> bytes:
- raise NotImplementedError
-
-
-class _ClearEvent(_BaseEvent):
- def make_command(self) -> bytes:
- return b"\x10\x00\x00\x00\x00"
-
-
[email protected](frozen=True)
-class _KeyEvent(_BaseEvent):
- name: str
- state: bool
-
- def __post_init__(self) -> None:
- assert self.name in KEYMAP
-
- def make_command(self) -> bytes:
- code = KEYMAP[self.name].serial.code
- return struct.pack(">BBBxx", 0x11, code, int(self.state))
-
+class _SerialPhyConnection(BasePhyConnection):
+ def __init__(self, tty: serial.Serial) -> None:
+ self.__tty = tty
[email protected](frozen=True)
-class _MouseButtonEvent(_BaseEvent):
- name: str
- state: bool
+ def send(self, request: bytes) -> bytes:
+ assert len(request) == 8
+ assert request[0] == 0x33
+ if self.__tty.in_waiting:
+ self.__tty.read_all()
+ assert self.__tty.write(request) == 8
+ return self.__tty.read(4)
- def __post_init__(self) -> None:
- assert self.name in ["left", "right", "middle", "up", "down"]
- def make_command(self) -> bytes:
- (code, state_pressed, is_main) = {
- "left": (0b10000000, 0b00001000, True),
- "right": (0b01000000, 0b00000100, True),
- "middle": (0b00100000, 0b00000010, True),
- "up": (0b10000000, 0b00001000, False), # Back
- "down": (0b01000000, 0b00000100, False), # Forward
- }[self.name]
- if self.state:
- code |= state_pressed
- if is_main:
- main_code = code
- extra_code = 0
- else:
- main_code = 0
- extra_code = code
- return struct.pack(">BBBxx", 0x13, main_code, extra_code)
-
-
[email protected](frozen=True)
-class _MouseMoveEvent(_BaseEvent):
- to_x: int
- to_y: int
-
- def __post_init__(self) -> None:
- assert -32768 <= self.to_x <= 32767
- assert -32768 <= self.to_y <= 32767
-
- def make_command(self) -> bytes:
- return struct.pack(">Bhh", 0x12, self.to_x, self.to_y)
-
-
[email protected](frozen=True)
-class _MouseWheelEvent(_BaseEvent):
- delta_x: int
- delta_y: int
-
- def __post_init__(self) -> None:
- assert -127 <= self.delta_x <= 127
- assert -127 <= self.delta_y <= 127
-
- def make_command(self) -> bytes:
- # Горизонтальная прокрутка пока не поддерживается
- return struct.pack(">Bxbxx", 0x14, self.delta_y)
-
-
-class _Gpio:
- def __init__(self, reset_pin: int, reset_delay: float) -> None:
- self.__reset_pin = reset_pin
- self.__reset_delay = reset_delay
-
- self.__chip: Optional[gpiod.Chip] = None
- self.__reset_line: Optional[gpiod.Line] = None
- self.__reset_wip = False
-
- def open(self) -> None:
- if self.__reset_pin >= 0:
- assert self.__chip is None
- assert self.__reset_line is None
- self.__chip = gpiod.Chip(env.GPIO_DEVICE_PATH)
- self.__reset_line = self.__chip.get_line(self.__reset_pin)
- self.__reset_line.request("kvmd::hid-serial::reset", gpiod.LINE_REQ_DIR_OUT, default_vals=[0])
-
- def close(self) -> None:
- if self.__chip:
- try:
- self.__chip.close()
- except Exception:
- pass
-
- @aiotools.atomic
- async def reset(self) -> None:
- if self.__reset_pin >= 0:
- assert self.__reset_line
- if not self.__reset_wip:
- self.__reset_wip = True
- try:
- await aiogp.pulse(self.__reset_line, self.__reset_delay, 1)
- finally:
- self.__reset_wip = False
- get_logger(0).info("Reset HID performed")
- else:
- get_logger(0).info("Another reset HID in progress")
-
-
-# =====
-class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-instance-attributes
- def __init__( # pylint: disable=too-many-arguments,super-init-not-called
+class _SerialPhy(BasePhy):
+ def __init__(
self,
- reset_pin: int,
- reset_delay: float,
-
device_path: str,
speed: int,
read_timeout: float,
- read_retries: int,
- common_retries: int,
- retries_delay: float,
- errors_threshold: int,
- noop: bool,
) -> None:
- multiprocessing.Process.__init__(self, daemon=True)
-
self.__device_path = device_path
self.__speed = speed
self.__read_timeout = read_timeout
- self.__read_retries = read_retries
- self.__common_retries = common_retries
- self.__retries_delay = retries_delay
- self.__errors_threshold = errors_threshold
- self.__noop = noop
- self.__gpio = _Gpio(reset_pin, reset_delay)
+ def has_device(self) -> bool:
+ return os.path.exists(self.__device_path)
- self.__events_queue: "multiprocessing.Queue[_BaseEvent]" = multiprocessing.Queue()
+ @contextlib.contextmanager
+ def connected(self) -> Generator[_SerialPhyConnection, None, None]: # type: ignore
+ with serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout) as tty:
+ yield _SerialPhyConnection(tty)
- self.__notifier = aiomulti.AioProcessNotifier()
- self.__state_flags = aiomulti.AioSharedFlags({
- "online": True,
- "caps": False,
- "scroll": False,
- "num": False,
- }, self.__notifier)
- self.__stop_event = multiprocessing.Event()
+# =====
+class Plugin(BaseMcuHid):
+ def __init__(self, **kwargs: Any) -> None:
+ phy_kwargs: Dict = {key: kwargs.pop(key) for key in self.__get_phy_options()}
+ super().__init__(phy=_SerialPhy(**phy_kwargs), **kwargs)
@classmethod
def get_plugin_options(cls) -> Dict:
return {
- "reset_pin": Option(-1, type=valid_gpio_pin_optional),
- "reset_delay": Option(0.1, type=valid_float_f01),
-
- "device": Option("", type=valid_abs_path, unpack_as="device_path"),
- "speed": Option(115200, type=valid_tty_speed),
- "read_timeout": Option(2.0, type=valid_float_f01),
- "read_retries": Option(10, type=valid_int_f1),
- "common_retries": Option(100, type=valid_int_f1),
- "retries_delay": Option(0.1, type=valid_float_f01),
- "errors_threshold": Option(5, type=valid_int_f0),
- "noop": Option(False, type=valid_bool),
+ **cls.__get_phy_options(),
+ **BaseMcuHid.get_plugin_options(),
}
- def sysprep(self) -> None:
- self.__gpio.open()
- get_logger(0).info("Starting HID daemon ...")
- self.start()
-
- async def get_state(self) -> Dict:
- state = await self.__state_flags.get()
+ @classmethod
+ def __get_phy_options(cls) -> Dict:
return {
- "online": state["online"],
- "keyboard": {
- "online": state["online"],
- "leds": {
- "caps": state["caps"],
- "scroll": state["scroll"],
- "num": state["num"],
- },
- },
- "mouse": {"online": state["online"]},
+ "device": Option("", type=valid_abs_path, unpack_as="device_path"),
+ "speed": Option(115200, type=valid_tty_speed),
+ "read_timeout": Option(2.0, type=valid_float_f01),
}
-
- async def poll_state(self) -> AsyncGenerator[Dict, None]:
- prev_state: Dict = {}
- while True:
- state = await self.get_state()
- if state != prev_state:
- yield state
- prev_state = state
- await self.__notifier.wait()
-
- @aiotools.atomic
- async def reset(self) -> None:
- await self.__gpio.reset()
-
- @aiotools.atomic
- async def cleanup(self) -> None:
- logger = get_logger(0)
- try:
- if self.is_alive():
- logger.info("Stopping HID daemon ...")
- self.__stop_event.set()
- if self.exitcode is not None:
- self.join()
- if os.path.exists(self.__device_path):
- get_logger().info("Clearing HID events ...")
- try:
- with self.__get_serial() as tty:
- self.__process_command(tty, b"\x10\x00\x00\x00\x00")
- except Exception:
- logger.exception("Can't clear HID events")
- finally:
- self.__gpio.close()
-
- # =====
-
- def send_key_events(self, keys: Iterable[Tuple[str, bool]]) -> None:
- for (key, state) in keys:
- self.__queue_event(_KeyEvent(key, state))
-
- def send_mouse_button_event(self, button: str, state: bool) -> None:
- self.__queue_event(_MouseButtonEvent(button, state))
-
- def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
- self.__queue_event(_MouseMoveEvent(to_x, to_y))
-
- def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None:
- self.__queue_event(_MouseWheelEvent(delta_x, delta_y))
-
- def clear_events(self) -> None:
- # FIXME: Если очистка производится со стороны процесса хида, то возможна гонка между
- # очисткой и добавлением события _ClearEvent. Неприятно, но не смертельно.
- # Починить блокировкой после перехода на асинхронные очереди.
- tools.clear_queue(self.__events_queue)
- self.__queue_event(_ClearEvent())
-
- def __queue_event(self, event: _BaseEvent) -> None:
- if not self.__stop_event.is_set():
- self.__events_queue.put_nowait(event)
-
- def run(self) -> None: # pylint: disable=too-many-branches
- logger = get_logger(0)
-
- logger.info("Started HID pid=%d", os.getpid())
- aioproc.ignore_sigint()
- aioproc.rename_process("hid")
-
- while not self.__stop_event.is_set():
- try:
- with self.__get_serial() as tty:
- while not (self.__stop_event.is_set() and self.__events_queue.qsize() == 0):
- try:
- event = self.__events_queue.get(timeout=0.1)
- except queue.Empty:
- self.__process_command(tty, b"\x01\x00\x00\x00\x00") # Ping
- else:
- if not self.__process_command(tty, event.make_command()):
- self.clear_events()
-
- except Exception as err:
- self.clear_events()
- if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member
- logger.error("Missing HID serial device: %s", self.__device_path)
- else:
- logger.exception("Unexpected HID error")
- time.sleep(1)
-
- def __get_serial(self) -> serial.Serial:
- return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout)
-
- def __process_command(self, tty: serial.Serial, command: bytes) -> bool:
- return self.__process_request(tty, self.__make_request(command))
-
- def __process_request(self, tty: serial.Serial, request: bytes) -> bool: # pylint: disable=too-many-branches
- logger = get_logger()
- error_messages: List[str] = []
- live_log_errors = False
-
- common_retries = self.__common_retries
- read_retries = self.__read_retries
- error_retval = False
-
- while common_retries and read_retries:
- response = self.__send_request(tty, request)
- try:
- if len(response) < 4:
- read_retries -= 1
- raise _TempRequestError(f"No response from HID: request={request!r}")
-
- assert len(response) == 4, response
- if self.__make_crc16(response[-4:-2]) != struct.unpack(">H", response[-2:])[0]:
- request = self.__make_request(b"\x02\x00\x00\x00\x00") # Repeat an answer
- raise _TempRequestError("Invalid response CRC; requesting response again ...")
-
- code = response[1]
- if code == 0x48: # Request timeout # pylint: disable=no-else-raise
- raise _TempRequestError(f"Got request timeout from HID: request={request!r}")
- elif code == 0x40: # CRC Error
- raise _TempRequestError(f"Got CRC error of request from HID: request={request!r}")
- elif code == 0x45: # Unknown command
- raise _PermRequestError(f"HID did not recognize the request={request!r}", online=True)
- elif code == 0x24: # Rebooted?
- raise _PermRequestError("No previous command state inside HID, seems it was rebooted", online=True)
- elif code == 0x20: # Done
- self.__state_flags.update(online=True)
- return True
- elif code & 0x80: # Pong with leds
- self.__state_flags.update(
- online=True,
- caps=bool(code & 0b00000001),
- scroll=bool(code & 0x00000010),
- num=bool(code & 0x00000100),
- )
- return True
- raise _TempRequestError(f"Invalid response from HID: request={request!r}; code=0x{code:02X}")
-
- except _RequestError as err:
- common_retries -= 1
- self.__state_flags.update(online=err.online)
- error_retval = err.online
-
- if live_log_errors:
- logger.error(err.msg)
- else:
- error_messages.append(err.msg)
- if len(error_messages) > self.__errors_threshold:
- for msg in error_messages:
- logger.error(msg)
- error_messages = []
- live_log_errors = True
-
- if isinstance(err, _PermRequestError):
- break
- if common_retries and read_retries:
- time.sleep(self.__retries_delay)
-
- for msg in error_messages:
- logger.error(msg)
- if not (common_retries and read_retries):
- logger.error("Can't process HID request due many errors: %r", request)
- return error_retval
-
- def __send_request(self, tty: serial.Serial, request: bytes) -> bytes:
- if not self.__noop:
- if tty.in_waiting:
- tty.read(tty.in_waiting)
- assert tty.write(request) == len(request)
- response = tty.read(4)
- else:
- response = b"\x33\x20" # Magic + OK
- response += struct.pack(">H", self.__make_crc16(response))
- return response
-
- def __make_request(self, command: bytes) -> bytes:
- request = b"\x33" + command
- request += struct.pack(">H", self.__make_crc16(request))
- assert len(request) == 8, (request, command)
- return request
-
- def __make_crc16(self, data: bytes) -> int:
- crc = 0xFFFF
- for byte in data:
- crc = crc ^ byte
- for _ in range(8):
- if crc & 0x0001 == 0:
- crc = crc >> 1
- else:
- crc = crc >> 1
- crc = crc ^ 0xA001
- return crc
diff --git a/kvmd/plugins/hid/spi.py b/kvmd/plugins/hid/spi.py
new file mode 100644
index 00000000..8af03483
--- /dev/null
+++ b/kvmd/plugins/hid/spi.py
@@ -0,0 +1,179 @@
+# ========================================================================== #
+# #
+# KVMD - The main Pi-KVM daemon. #
+# #
+# Copyright (C) 2018 Maxim Devaev <[email protected]> #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <https://www.gnu.org/licenses/>. #
+# #
+# ========================================================================== #
+
+
+import os
+import contextlib
+import time
+
+from typing import List
+from typing import Dict
+from typing import Generator
+from typing import Callable
+from typing import Optional
+from typing import Any
+
+import spidev
+import gpiod
+
+from ...logging import get_logger
+
+from ...yamlconf import Option
+
+from ...validators.basic import valid_bool
+from ...validators.basic import valid_int_f0
+from ...validators.basic import valid_int_f1
+from ...validators.basic import valid_float_f01
+from ...validators.hw import valid_gpio_pin_optional
+
+from ... import env
+
+from ._mcu import BasePhyConnection
+from ._mcu import BasePhy
+from ._mcu import BaseMcuHid
+
+
+# =====
+class _SpiPhyConnection(BasePhyConnection):
+ def __init__(
+ self,
+ xfer: Callable[[bytes], bytes],
+ read_timeout: float,
+ ) -> None:
+
+ self.__xfer = xfer
+ self.__read_timeout = read_timeout
+
+ def send(self, request: bytes) -> bytes:
+ assert len(request) == 8
+ assert request[0] == 0x33
+
+ deadline_ts = time.time() + self.__read_timeout
+ dummy = b"\x00" * 8
+ while time.time() < deadline_ts:
+ if bytes(self.__xfer(dummy)) == dummy:
+ break
+ else:
+ get_logger(0).error("SPI timeout reached while garbage reading")
+ return b""
+
+ self.__xfer(request)
+
+ response: List[int] = []
+ deadline_ts = time.time() + self.__read_timeout
+ found = False
+ while time.time() < deadline_ts:
+ for byte in self.__xfer(b"\x00" * (5 - len(response))):
+ if not found:
+ if byte != 0x33:
+ continue
+ found = True
+ response.append(byte)
+ if len(response) == 4:
+ break
+ if len(response) == 4:
+ break
+ else:
+ get_logger(0).error("SPI timeout reached while responce waiting")
+ return b""
+ return bytes(response)
+
+
+class _SpiPhy(BasePhy): # pylint: disable=too-many-instance-attributes
+ def __init__(
+ self,
+ bus: int,
+ chip: int,
+ hw_cs: bool,
+ sw_cs_pin: int,
+ max_freq: int,
+ block_usec: int,
+ read_timeout: float,
+ ) -> None:
+
+ self.__bus = bus
+ self.__chip = chip
+ self.__hw_cs = hw_cs
+ self.__sw_cs_pin = sw_cs_pin
+ self.__max_freq = max_freq
+ self.__block_usec = block_usec
+ self.__read_timeout = read_timeout
+
+ def has_device(self) -> bool:
+ return os.path.exists(f"/dev/spidev{self.__bus}.{self.__chip}")
+
+ @contextlib.contextmanager
+ def connected(self) -> Generator[_SpiPhyConnection, None, None]: # type: ignore
+ with self.__sw_cs_connected() as sw_cs_line:
+ with contextlib.closing(spidev.SpiDev(self.__bus, self.__chip)) as spi:
+ spi.mode = 0
+ spi.no_cs = (not self.__hw_cs)
+ spi.max_speed_hz = self.__max_freq
+
+ def xfer(data: bytes) -> bytes:
+ try:
+ if sw_cs_line is not None:
+ sw_cs_line.set_value(0)
+ return spi.xfer(data, self.__max_freq, self.__block_usec)
+ finally:
+ if sw_cs_line is not None:
+ sw_cs_line.set_value(1)
+
+ yield _SpiPhyConnection(
+ xfer=xfer,
+ read_timeout=self.__read_timeout,
+ )
+
+ @contextlib.contextmanager
+ def __sw_cs_connected(self) -> Generator[Optional[gpiod.Line], None, None]:
+ if self.__sw_cs_pin > 0:
+ with contextlib.closing(gpiod.Chip(env.GPIO_DEVICE_PATH)) as chip:
+ line = chip.get_line(self.__sw_cs_pin)
+ line.request("kvmd::hid-mcu::sw_cs", gpiod.LINE_REQ_DIR_OUT, default_vals=[1])
+ yield line
+ else:
+ yield None
+
+
+# =====
+class Plugin(BaseMcuHid):
+ def __init__(self, **kwargs: Any) -> None:
+ phy_kwargs: Dict = {key: kwargs.pop(key) for key in self.__get_phy_options()}
+ super().__init__(phy=_SpiPhy(**phy_kwargs), **kwargs)
+
+ @classmethod
+ def get_plugin_options(cls) -> Dict:
+ return {
+ **cls.__get_phy_options(),
+ **BaseMcuHid.get_plugin_options(),
+ }
+
+ @classmethod
+ def __get_phy_options(cls) -> Dict:
+ return {
+ "bus": Option(-1, type=valid_int_f0),
+ "chip": Option(-1, type=valid_int_f0),
+ "hw_cs": Option(False, type=valid_bool),
+ "sw_cs_pin": Option(-1, type=valid_gpio_pin_optional),
+ "max_freq": Option(200000, type=valid_int_f1),
+ "block_usec": Option(1, type=valid_int_f0),
+ "read_timeout": Option(0.5, type=valid_float_f01),
+ }