vial/src/main/python/util.py

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()