device.py 6.8 KB

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