device.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. # Copyright 2022 QMK
  2. # SPDX-License-Identifier: GPL-2.0-or-later
  3. import json
  4. import time
  5. import gzip
  6. import random
  7. import threading
  8. import functools
  9. from typing import Optional
  10. from struct import pack, unpack
  11. from platform import platform
  12. from .types import XAPSecureStatus, XAPFlags, XAPRequest, XAPResponse, XAPBroadcast
  13. from .routes import XAPRoutes, XAPRouteError
  14. def _u32_to_bcd(val: bytes) -> str: # noqa: N802
  15. """Create BCD string
  16. """
  17. tmp = "{:08x}".format(val)
  18. major = int(tmp[0:2])
  19. minor = int(tmp[2:4])
  20. patch = int(tmp[4:8])
  21. return f'{major}.{minor}.{patch}'
  22. def _gen_token() -> bytes:
  23. """Generate XAP token - cannot start with 00xx or 'reserved' (FFFE|FFFF)
  24. """
  25. token = random.randrange(0x0100, 0xFFFD)
  26. # swap endianness
  27. return unpack('<H', pack('>H', token))[0]
  28. class XAPDeviceBase:
  29. """Raw XAP interactions
  30. """
  31. def __init__(self, dev: dict, timeout: int = 1.0):
  32. """Constructor opens hid device and starts dependent services
  33. """
  34. self.responses = {}
  35. self.timeout = timeout
  36. self.running = True
  37. # lazy import to avoid compile issues
  38. import hid
  39. self.dev = hid.Device(path=dev['path'])
  40. self.bg = threading.Thread(target=self._read_loop, daemon=True)
  41. self.bg.start()
  42. def close(self):
  43. """Close device and stop dependent services
  44. """
  45. self.running = False
  46. time.sleep(1)
  47. self.dev.close()
  48. def _read_loop(self):
  49. """Background thread to signal waiting transactions
  50. """
  51. while self.running:
  52. data = self.dev.read(XAPResponse.fmt.size, 100)
  53. if data:
  54. r = XAPResponse.from_bytes(data)
  55. event = self.responses.get(r.token)
  56. if event:
  57. event._ret = data
  58. event.set()
  59. def transaction(self, *args) -> Optional[bytes]:
  60. """Request/Receive Helper
  61. """
  62. # convert args to array of bytes
  63. data = bytes()
  64. for arg in args:
  65. if isinstance(arg, (bytes, bytearray)):
  66. data += arg
  67. if isinstance(arg, int): # TODO: remove terrible assumption of u16
  68. data += arg.to_bytes(2, byteorder='little')
  69. token = _gen_token()
  70. buffer = XAPRequest(token, len(data), data).to_bytes()
  71. event = threading.Event()
  72. self.responses[token] = event
  73. # prepend 0 on windows because reasons...
  74. if 'windows' in platform().lower():
  75. buffer = b'\x00' + buffer
  76. self.dev.write(buffer)
  77. event.wait(timeout=self.timeout)
  78. self.responses.pop(token, None)
  79. if not hasattr(event, '_ret'):
  80. return None
  81. r = XAPResponse.from_bytes(event._ret)
  82. if r.flags & XAPFlags.SUCCESS == 0:
  83. return None
  84. return r.data[:r.length]
  85. def listen(self) -> dict:
  86. """Receive a single 'broadcast' message
  87. """
  88. token = 0xFFFF
  89. event = threading.Event()
  90. self.responses[token] = event
  91. # emulate a blocking read while allowing `ctrl+c` on windows
  92. while not hasattr(event, '_ret'):
  93. event.wait(timeout=0.25)
  94. r = XAPBroadcast.from_bytes(event._ret)
  95. return (r.event, r.data[:r.length])
  96. class XAPDevice(XAPDeviceBase):
  97. """XAP device interaction
  98. """
  99. def __enter__(self):
  100. return self
  101. def __exit__(self, exc_type, exc_value, exc_traceback):
  102. self.close()
  103. def _query_device_info(self) -> dict:
  104. """Helper to reconstruct info.json from requested chunks
  105. """
  106. datalen = self.int_transaction(XAPRoutes.QMK_CONFIG_BLOB_LEN)
  107. if not datalen:
  108. return {}
  109. data = []
  110. offset = 0
  111. while offset < datalen:
  112. chunk = self.transaction(XAPRoutes.QMK_CONFIG_BLOB_CHUNK, offset)
  113. data += chunk
  114. offset += len(chunk)
  115. str_data = gzip.decompress(bytearray(data[:datalen]))
  116. return json.loads(str_data)
  117. def _ensure_route(self, route: bytes):
  118. """Check a route can be accessed
  119. Raises:
  120. XAPRouteError: Access to invalid route attempted
  121. """
  122. # TODO: Remove assumption that capability is always xx01
  123. (remain, sub, rt) = (route[:-2], route[-2], route[-1])
  124. cap = remain + bytes([sub, 1])
  125. # recurse for nested routes
  126. if remain:
  127. self._ensure_route(remain + bytes([sub]))
  128. if self.subsystems() & (1 << sub) == 0:
  129. raise XAPRouteError("subsystem not available")
  130. if self.capability(cap) & (1 << rt) == 0:
  131. raise XAPRouteError("route not available")
  132. def transaction(self, route: bytes, *args):
  133. """Request/Receive to XAP device
  134. Raises:
  135. XAPRouteError: Access to invalid route attempted
  136. """
  137. self._ensure_route(route)
  138. return super().transaction(route, *args)
  139. def int_transaction(self, route: bytes, *args):
  140. """transaction with int parsing
  141. """
  142. return int.from_bytes(self.transaction(route, *args) or bytes(0), 'little')
  143. @functools.lru_cache
  144. def capability(self, route: bytes):
  145. # use parent transaction as we want to ignore capability checks
  146. return int.from_bytes(super().transaction(route) or bytes(0), 'little')
  147. @functools.lru_cache
  148. def subsystems(self):
  149. # use parent transaction as we want to ignore capability checks
  150. return int.from_bytes(super().transaction(XAPRoutes.XAP_SUBSYSTEM_QUERY) or bytes(0), 'little')
  151. @functools.lru_cache
  152. def version(self) -> dict:
  153. """Query version data from device
  154. """
  155. xap = self.int_transaction(XAPRoutes.XAP_VERSION_QUERY)
  156. qmk = self.int_transaction(XAPRoutes.QMK_VERSION_QUERY)
  157. return {'xap': _u32_to_bcd(xap), 'qmk': _u32_to_bcd(qmk)}
  158. @functools.lru_cache
  159. def info(self) -> dict:
  160. """Query config data from device
  161. """
  162. data = self._query_device_info()
  163. data['_id'] = self.transaction(XAPRoutes.QMK_HARDWARE_ID)
  164. data['_version'] = self.version()
  165. return data
  166. def status(self) -> dict:
  167. """Query current device state
  168. """
  169. lock = self.int_transaction(XAPRoutes.XAP_SECURE_STATUS)
  170. data = {}
  171. data['lock'] = XAPSecureStatus(lock).name
  172. return data
  173. def unlock(self):
  174. """Initiate unlock procedure
  175. """
  176. self.transaction(XAPRoutes.XAP_SECURE_UNLOCK)
  177. def lock(self):
  178. """Lock device
  179. """
  180. self.transaction(XAPRoutes.XAP_SECURE_LOCK)
  181. def reset(self):
  182. """Request device reboot to bootloader - Requires previous unlock
  183. """
  184. status = self.int_transaction(XAPRoutes.QMK_BOOTLOADER_JUMP)
  185. return status == 1
  186. def reinit(self):
  187. """Request device reset EEPROM - Requires previous unlock
  188. """
  189. status = self.int_transaction(XAPRoutes.QMK_EEPROM_RESET)
  190. return status == 1