device.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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. (sub, rt) = route
  120. cap = bytes([sub, 1])
  121. if self.subsystems() & (1 << sub) == 0:
  122. raise XAPRouteError("subsystem not available")
  123. if self.capability(cap) & (1 << rt) == 0:
  124. raise XAPRouteError("route not available")
  125. def transaction(self, route: bytes, *args):
  126. """Request/Receive to XAP device
  127. Raises:
  128. XAPRouteError: Access to invalid route attempted
  129. """
  130. self._ensure_route(route)
  131. return super().transaction(route, *args)
  132. def int_transaction(self, route: bytes, *args):
  133. """transaction with int parsing
  134. """
  135. return int.from_bytes(self.transaction(route, *args) or bytes(0), 'little')
  136. @functools.lru_cache
  137. def capability(self, route: bytes):
  138. # use parent transaction as we want to ignore capability checks
  139. return int.from_bytes(super().transaction(route) or bytes(0), 'little')
  140. @functools.lru_cache
  141. def subsystems(self):
  142. # use parent transaction as we want to ignore capability checks
  143. return int.from_bytes(super().transaction(XAPRoutes.XAP_SUBSYSTEM_QUERY) or bytes(0), 'little')
  144. @functools.lru_cache
  145. def version(self) -> dict:
  146. """Query version data from device
  147. """
  148. xap = self.int_transaction(XAPRoutes.XAP_VERSION_QUERY)
  149. qmk = self.int_transaction(XAPRoutes.QMK_VERSION_QUERY)
  150. return {'xap': _u32_to_bcd(xap), 'qmk': _u32_to_bcd(qmk)}
  151. @functools.lru_cache
  152. def info(self) -> dict:
  153. """Query config data from device
  154. """
  155. data = self._query_device_info()
  156. data['_id'] = self.transaction(XAPRoutes.QMK_HARDWARE_ID)
  157. data['_version'] = self.version()
  158. return data
  159. def status(self) -> dict:
  160. """Query current device state
  161. """
  162. lock = self.int_transaction(XAPRoutes.XAP_SECURE_STATUS)
  163. data = {}
  164. data['lock'] = XAPSecureStatus(lock).name
  165. return data
  166. def unlock(self):
  167. """Initiate unlock procedure
  168. """
  169. self.transaction(XAPRoutes.XAP_SECURE_UNLOCK)
  170. def lock(self):
  171. """Lock device
  172. """
  173. self.transaction(XAPRoutes.XAP_SECURE_LOCK)
  174. def reset(self):
  175. """Request device reboot to bootloader - Requires previous unlock
  176. """
  177. status = self.int_transaction(XAPRoutes.QMK_BOOTLOADER_JUMP)
  178. return status == 1