200 lines
6.5 KiB
Python
200 lines
6.5 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
import logging
|
|
import os
|
|
import time
|
|
from logging.handlers import RotatingFileHandler
|
|
|
|
from PyQt5.QtCore import QCoreApplication, QStandardPaths
|
|
from PyQt5.QtGui import QPalette
|
|
from PyQt5.QtWidgets import QApplication
|
|
|
|
from hidproxy import hid
|
|
from keycodes import Keycode
|
|
from keymaps import KEYMAPS
|
|
|
|
tr = QCoreApplication.translate
|
|
|
|
# For Vial keyboard
|
|
VIAL_SERIAL_NUMBER_MAGIC = "vial:f64c2b3c"
|
|
|
|
# For bootloader
|
|
VIBL_SERIAL_NUMBER_MAGIC = "vibl:d4f8159c"
|
|
|
|
MSG_LEN = 32
|
|
|
|
# these should match what we have in vial-qmk/keyboards/vial_example
|
|
# so that people don't accidentally reuse a sample keyboard UID
|
|
EXAMPLE_KEYBOARDS = [
|
|
0xD4A36200603E3007, # vial_stm32f103_vibl
|
|
0x32F62BC2EEF2237B, # vial_atmega32u4
|
|
0x38CEA320F23046A5, # vial_stm32f072
|
|
]
|
|
|
|
|
|
def hid_send(dev, msg, retries=1):
|
|
if len(msg) > MSG_LEN:
|
|
raise RuntimeError("message must be less than 32 bytes")
|
|
msg += b"\x00" * (MSG_LEN - len(msg))
|
|
|
|
data = b""
|
|
first = True
|
|
|
|
while retries > 0:
|
|
retries -= 1
|
|
if not first:
|
|
time.sleep(0.5)
|
|
first = False
|
|
try:
|
|
# add 00 at start for hidapi report id
|
|
if dev.write(b"\x00" + msg) != MSG_LEN + 1:
|
|
continue
|
|
|
|
data = bytes(dev.read(MSG_LEN, timeout_ms=500))
|
|
if not data:
|
|
continue
|
|
except OSError:
|
|
continue
|
|
break
|
|
|
|
if not data:
|
|
raise RuntimeError("failed to communicate with the device")
|
|
return data
|
|
|
|
|
|
def is_rawhid(desc):
|
|
if desc["usage_page"] != 0xFF60 or desc["usage"] != 0x61:
|
|
logging.warning("is_rawhid: {} does not match - usage_page={:04X} usage={:02X}".format(
|
|
desc["path"], desc["usage_page"], desc["usage"]))
|
|
return False
|
|
|
|
dev = hid.device()
|
|
|
|
try:
|
|
dev.open_path(desc["path"])
|
|
except OSError as e:
|
|
logging.warning("is_rawhid: {} does not match - open_path error {}".format(desc["path"], e))
|
|
return False
|
|
|
|
# probe VIA version and ensure it is supported
|
|
data = b""
|
|
try:
|
|
data = hid_send(dev, b"\x01", retries=3)
|
|
except RuntimeError as e:
|
|
logging.warning("is_rawhid: {} does not match - hid_send error {}".format(desc["path"], e))
|
|
pass
|
|
dev.close()
|
|
|
|
# must have VIA protocol version = 9
|
|
if data[0:3] != b"\x01\x00\x09":
|
|
logging.warning("is_rawhid: {} does not match - unexpected data in response {}".format(
|
|
desc["path"], data.hex()))
|
|
return False
|
|
|
|
logging.info("is_rawhid: {} matched OK".format(desc["path"]))
|
|
return True
|
|
|
|
|
|
def find_vial_devices(via_stack_json, sideload_vid=None, sideload_pid=None):
|
|
from vial_device import VialBootloader, VialKeyboard, VialDummyKeyboard
|
|
|
|
filtered = []
|
|
for dev in hid.enumerate():
|
|
if dev["vendor_id"] == sideload_vid and dev["product_id"] == sideload_pid:
|
|
logging.info("Trying VID={:04X}, PID={:04X}, serial={}, path={} - sideload".format(
|
|
dev["vendor_id"], dev["product_id"], dev["serial_number"], dev["path"]
|
|
))
|
|
if is_rawhid(dev):
|
|
filtered.append(VialKeyboard(dev, sideload=True))
|
|
elif VIAL_SERIAL_NUMBER_MAGIC in dev["serial_number"]:
|
|
logging.info("Matching VID={:04X}, PID={:04X}, serial={}, path={} - vial serial magic".format(
|
|
dev["vendor_id"], dev["product_id"], dev["serial_number"], dev["path"]
|
|
))
|
|
if is_rawhid(dev):
|
|
filtered.append(VialKeyboard(dev))
|
|
elif VIBL_SERIAL_NUMBER_MAGIC in dev["serial_number"]:
|
|
logging.info("Matching VID={:04X}, PID={:04X}, serial={}, path={} - vibl serial magic".format(
|
|
dev["vendor_id"], dev["product_id"], dev["serial_number"], dev["path"]
|
|
))
|
|
filtered.append(VialBootloader(dev))
|
|
elif str(dev["vendor_id"] * 65536 + dev["product_id"]) in via_stack_json["definitions"]:
|
|
logging.info("Matching VID={:04X}, PID={:04X}, serial={}, path={} - VIA stack".format(
|
|
dev["vendor_id"], dev["product_id"], dev["serial_number"], dev["path"]
|
|
))
|
|
if is_rawhid(dev):
|
|
filtered.append(VialKeyboard(dev, via_stack=True))
|
|
|
|
if sideload_vid == sideload_pid == 0:
|
|
filtered.append(VialDummyKeyboard())
|
|
|
|
return filtered
|
|
|
|
|
|
def chunks(data, sz):
|
|
for i in range(0, len(data), sz):
|
|
yield data[i:i+sz]
|
|
|
|
|
|
def pad_for_vibl(msg):
|
|
""" Pads message to vibl fixed 64-byte length """
|
|
if len(msg) > 64:
|
|
raise RuntimeError("vibl message too long")
|
|
return msg + b"\x00" * (64 - len(msg))
|
|
|
|
|
|
def init_logger():
|
|
logging.basicConfig(level=logging.INFO)
|
|
directory = QStandardPaths.writableLocation(QStandardPaths.AppLocalDataLocation)
|
|
if not os.path.exists(directory):
|
|
os.mkdir(directory)
|
|
path = os.path.join(directory, "vial.log")
|
|
handler = RotatingFileHandler(path, maxBytes=5 * 1024 * 1024, backupCount=5)
|
|
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s"))
|
|
logging.getLogger().addHandler(handler)
|
|
|
|
|
|
class KeycodeDisplay:
|
|
|
|
keymap_override = KEYMAPS[0][1]
|
|
clients = []
|
|
|
|
@classmethod
|
|
def get_label(cls, code):
|
|
""" Get label for a specific keycode """
|
|
if cls.code_is_overriden(code):
|
|
return cls.keymap_override[Keycode.find_outer_keycode(code).qmk_id]
|
|
return Keycode.label(code)
|
|
|
|
@classmethod
|
|
def code_is_overriden(cls, code):
|
|
""" Check whether a country-specific keymap overrides a code """
|
|
key = Keycode.find_outer_keycode(code)
|
|
return key is not None and key.qmk_id in cls.keymap_override
|
|
|
|
@classmethod
|
|
def display_keycode(cls, widget, code):
|
|
text = cls.get_label(code)
|
|
tooltip = Keycode.tooltip(code)
|
|
mask = Keycode.is_mask(code)
|
|
mask_text = cls.get_label(code & 0xFF)
|
|
if mask:
|
|
text = text.split("\n")[0]
|
|
widget.masked = mask
|
|
widget.setText(text)
|
|
widget.setMaskText(mask_text)
|
|
widget.setToolTip(tooltip)
|
|
if cls.code_is_overriden(code):
|
|
widget.setColor(QApplication.palette().color(QPalette.Link))
|
|
else:
|
|
widget.setColor(None)
|
|
|
|
@classmethod
|
|
def set_keymap_override(cls, override):
|
|
cls.keymap_override = override
|
|
for client in cls.clients:
|
|
client.on_keymap_override()
|
|
|
|
@classmethod
|
|
def notify_keymap_override(cls, client):
|
|
cls.clients.append(client)
|
|
client.on_keymap_override()
|