device.py 6.4 KB

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