device.py 6.4 KB

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