gpu-aux 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gpu_aux/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """Direct Python access to DisplayPort AUX functions."""
2
+
3
+ from .adl import Adapter, AmdAux, AuxError, Port
4
+ from .api import AuxPort, GpuPorts, enumerate_gpus, enumerate_gpus_and_ports, enumerate_ports
5
+ from .intel import IntelAux
6
+ from .nvapi import NvidiaAux
7
+
8
+ __all__ = [
9
+ "Adapter",
10
+ "AmdAux",
11
+ "AuxError",
12
+ "AuxPort",
13
+ "GpuPorts",
14
+ "IntelAux",
15
+ "NvidiaAux",
16
+ "Port",
17
+ "enumerate_gpus",
18
+ "enumerate_gpus_and_ports",
19
+ "enumerate_ports",
20
+ ]
gpu_aux/adl.py ADDED
@@ -0,0 +1,370 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import threading
5
+ import time
6
+ from dataclasses import dataclass
7
+ from ctypes import POINTER, Structure, byref, c_char, c_int, c_uint8, c_uint32, c_void_p
8
+
9
+
10
+ ADL_OK = 0
11
+ ADL_DISPLAY_CONNECTED = 1
12
+ AMD_VENDOR_IDS = {1002, 0x1002}
13
+ ADL_CONNECTOR_DISPLAY_PORT = 15
14
+ ADL_CONNECTOR_EDP = 16
15
+ AUX_CONNECTORS = {ADL_CONNECTOR_DISPLAY_PORT, ADL_CONNECTOR_EDP}
16
+ AUX_CHUNK_SIZE = 16
17
+ RETRY_COUNT = 10
18
+
19
+
20
+ class AuxError(RuntimeError):
21
+ """Raised when ADL or an AUX transaction fails."""
22
+
23
+
24
+ class _ADLAdapterInfo(Structure):
25
+ _fields_ = [
26
+ ("iSize", c_int),
27
+ ("iAdapterIndex", c_int),
28
+ ("strUDID", c_char * 256),
29
+ ("iBusNumber", c_int),
30
+ ("iDeviceNumber", c_int),
31
+ ("iFunctionNumber", c_int),
32
+ ("iVendorID", c_int),
33
+ ("strAdapterName", c_char * 256),
34
+ ("strDisplayName", c_char * 256),
35
+ ("iPresent", c_int),
36
+ ("iExist", c_int),
37
+ ("strDriverPath", c_char * 256),
38
+ ("strDriverPathExt", c_char * 256),
39
+ ("strPNPString", c_char * 256),
40
+ ("iOSDisplayIndex", c_int),
41
+ ]
42
+
43
+
44
+ class _ADLDisplayID(Structure):
45
+ _fields_ = [
46
+ ("iDisplayLogicalIndex", c_int),
47
+ ("iDisplayPhysicalIndex", c_int),
48
+ ("iDisplayLogicalAdapterIndex", c_int),
49
+ ("iDisplayPhysicalAdapterIndex", c_int),
50
+ ]
51
+
52
+
53
+ class _ADLDisplayInfo(Structure):
54
+ _fields_ = [
55
+ ("displayID", _ADLDisplayID),
56
+ ("iDisplayControllerIndex", c_int),
57
+ ("strDisplayName", c_char * 256),
58
+ ("strDisplayManufacturerName", c_char * 256),
59
+ ("iDisplayType", c_int),
60
+ ("iDisplayOutputType", c_int),
61
+ ("iDisplayConnector", c_int),
62
+ ("iDisplayInfoMask", c_int),
63
+ ("iDisplayInfoValue", c_int),
64
+ ]
65
+
66
+
67
+ class _NativeAuxRequest(Structure):
68
+ _fields_ = [
69
+ ("size", c_uint32),
70
+ ("result", c_uint32),
71
+ ("operation", c_uint32),
72
+ ("address", c_uint32),
73
+ ("data_size", c_uint32),
74
+ ("data", c_uint8 * AUX_CHUNK_SIZE),
75
+ ]
76
+
77
+
78
+ assert ctypes.sizeof(_ADLAdapterInfo) == 0x624
79
+ assert ctypes.sizeof(_ADLDisplayInfo) == 0x228
80
+ assert ctypes.sizeof(_NativeAuxRequest) == 0x24
81
+
82
+
83
+ def _text(value: bytes) -> str:
84
+ return value.split(b"\0", 1)[0].decode("utf-8", errors="replace")
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class Adapter:
89
+ index: int
90
+ name: str
91
+ display_name: str
92
+ bus: int
93
+ device: int
94
+ function: int
95
+ backend: str = "AMD"
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class Port:
100
+ adapter: Adapter
101
+ index: int
102
+ logical_display_index: int
103
+ physical_display_index: int
104
+ name: str
105
+ manufacturer: str
106
+ display_type: int
107
+ output_type: int
108
+ connector: int
109
+ connected: bool
110
+ backend: str = "adl"
111
+
112
+ @property
113
+ def identity(self) -> str:
114
+ return f"{self.backend}:{self.adapter.index}:{self.logical_display_index}"
115
+
116
+ @property
117
+ def kind(self) -> str:
118
+ return "eDP" if self.connector == ADL_CONNECTOR_EDP else "DP"
119
+
120
+
121
+ class AmdAux:
122
+ """Direct binding to AMD ADL AUX and DDC functions.
123
+
124
+ ADL is process-global, so one instance serializes all transactions.
125
+ """
126
+
127
+ def __init__(self) -> None:
128
+ if ctypes.sizeof(c_void_p) != 8:
129
+ raise AuxError("gpu_aux requires 64-bit Python")
130
+ self._lock = threading.RLock()
131
+ self._closed = False
132
+ self._crt = ctypes.CDLL("msvcrt")
133
+ self._crt.malloc.argtypes = [ctypes.c_size_t]
134
+ self._crt.malloc.restype = c_void_p
135
+ self._crt.free.argtypes = [c_void_p]
136
+ self._alloc_type = ctypes.CFUNCTYPE(c_void_p, c_int)
137
+ self._allocator = self._alloc_type(self._allocate)
138
+ self._adl = self._load_adl()
139
+ self._bind()
140
+ self._check(self._adl.ADL_Main_Control_Create(self._allocator, 1), "ADL initialization")
141
+
142
+ @staticmethod
143
+ def _load_adl():
144
+ for name in ("atiadlxx.dll", "atiadlxy.dll"):
145
+ try:
146
+ return ctypes.CDLL(name)
147
+ except OSError:
148
+ pass
149
+ raise AuxError("AMD ADL library atiadlxx.dll was not found")
150
+
151
+ def _allocate(self, size: int):
152
+ return self._crt.malloc(size)
153
+
154
+ def _bind(self) -> None:
155
+ adl = self._adl
156
+ adl.ADL_Main_Control_Create.argtypes = [self._alloc_type, c_int]
157
+ adl.ADL_Main_Control_Create.restype = c_int
158
+ adl.ADL_Main_Control_Destroy.argtypes = []
159
+ adl.ADL_Main_Control_Destroy.restype = c_int
160
+ adl.ADL_Adapter_NumberOfAdapters_Get.argtypes = [POINTER(c_int)]
161
+ adl.ADL_Adapter_NumberOfAdapters_Get.restype = c_int
162
+ adl.ADL_Adapter_AdapterInfo_Get.argtypes = [c_void_p, c_int]
163
+ adl.ADL_Adapter_AdapterInfo_Get.restype = c_int
164
+ adl.ADL_Display_DisplayInfo_Get.argtypes = [c_int, POINTER(c_int), POINTER(c_void_p), c_int]
165
+ adl.ADL_Display_DisplayInfo_Get.restype = c_int
166
+ adl.ADL_Display_NativeAUXChannel_Access.argtypes = [c_int, c_int, POINTER(_NativeAuxRequest)]
167
+ adl.ADL_Display_NativeAUXChannel_Access.restype = c_int
168
+ adl.ADL_Display_DDCBlockAccess_Get.argtypes = [
169
+ c_int, c_int, c_int, c_int, c_int, c_void_p, POINTER(c_int), c_void_p
170
+ ]
171
+ adl.ADL_Display_DDCBlockAccess_Get.restype = c_int
172
+
173
+ @staticmethod
174
+ def _check(rc: int, operation: str) -> None:
175
+ if rc != ADL_OK:
176
+ raise AuxError(f"{operation} failed: ADL error {rc}")
177
+
178
+ def _ensure_open(self) -> None:
179
+ if self._closed:
180
+ raise AuxError("AmdAux is closed")
181
+
182
+ def close(self) -> None:
183
+ with self._lock:
184
+ if not self._closed:
185
+ self._adl.ADL_Main_Control_Destroy()
186
+ self._closed = True
187
+
188
+ def __enter__(self) -> "AmdAux":
189
+ return self
190
+
191
+ def __exit__(self, exc_type, exc_value, traceback) -> None:
192
+ self.close()
193
+
194
+ def adapters(self) -> list[Adapter]:
195
+ with self._lock:
196
+ self._ensure_open()
197
+ count = c_int()
198
+ self._check(self._adl.ADL_Adapter_NumberOfAdapters_Get(byref(count)), "adapter enumeration")
199
+ raw = (_ADLAdapterInfo * count.value)()
200
+ for item in raw:
201
+ item.iSize = ctypes.sizeof(_ADLAdapterInfo)
202
+ self._check(self._adl.ADL_Adapter_AdapterInfo_Get(raw, ctypes.sizeof(raw)), "adapter information")
203
+ adapters = [
204
+ Adapter(
205
+ index=item.iAdapterIndex,
206
+ name=_text(item.strAdapterName),
207
+ display_name=_text(item.strDisplayName),
208
+ bus=item.iBusNumber,
209
+ device=item.iDeviceNumber,
210
+ function=item.iFunctionNumber,
211
+ )
212
+ for item in raw
213
+ if item.iPresent == 1 and item.iVendorID in AMD_VENDOR_IDS
214
+ ]
215
+ # ADL exposes one logical adapter per Windows DISPLAYx even when all
216
+ # entries refer to the same physical GPU. AUX ports belong to the
217
+ # physical PCI function, so retain one representative per BDF.
218
+ unique = {}
219
+ for adapter in adapters:
220
+ unique.setdefault((adapter.bus, adapter.device, adapter.function), adapter)
221
+ return list(unique.values())
222
+
223
+ def ports(self, adapter: Adapter, connected_only: bool = True) -> list[Port]:
224
+ with self._lock:
225
+ self._ensure_open()
226
+ count = c_int()
227
+ pointer = c_void_p()
228
+ self._check(
229
+ self._adl.ADL_Display_DisplayInfo_Get(adapter.index, byref(count), byref(pointer), 1),
230
+ "display enumeration",
231
+ )
232
+ try:
233
+ if not pointer.value or count.value <= 0:
234
+ return []
235
+ raw = ctypes.cast(pointer, POINTER(_ADLDisplayInfo))
236
+ result = []
237
+ for position in range(count.value):
238
+ item = raw[position]
239
+ connected = bool(item.iDisplayInfoValue & ADL_DISPLAY_CONNECTED)
240
+ if item.iDisplayConnector not in AUX_CONNECTORS:
241
+ continue
242
+ if connected_only and not connected:
243
+ continue
244
+ result.append(
245
+ Port(
246
+ adapter=adapter,
247
+ index=position,
248
+ logical_display_index=item.displayID.iDisplayLogicalIndex,
249
+ physical_display_index=item.displayID.iDisplayPhysicalIndex,
250
+ name=_text(item.strDisplayName),
251
+ manufacturer=_text(item.strDisplayManufacturerName),
252
+ display_type=item.iDisplayType,
253
+ output_type=item.iDisplayOutputType,
254
+ connector=item.iDisplayConnector,
255
+ connected=connected,
256
+ )
257
+ )
258
+ return result
259
+ finally:
260
+ if pointer.value:
261
+ self._crt.free(pointer)
262
+
263
+ @staticmethod
264
+ def _validate_port(port: Port) -> None:
265
+ if not port.connected:
266
+ raise ValueError("port is not connected")
267
+
268
+ def _native_aux(self, port: Port, operation: int, address: int, data: bytes) -> bytes:
269
+ self._validate_port(port)
270
+ if not 0 <= address <= 0xFFFFF:
271
+ raise ValueError("DPCD address must be in range 0x00000..0xFFFFF")
272
+ request = _NativeAuxRequest()
273
+ request.size = ctypes.sizeof(request)
274
+ request.operation = operation
275
+ request.address = address
276
+ request.data_size = len(data)
277
+ if operation == 1:
278
+ request.data[: len(data)] = data
279
+ rc = -1
280
+ for _ in range(RETRY_COUNT):
281
+ rc = self._adl.ADL_Display_NativeAUXChannel_Access(
282
+ port.adapter.index, port.logical_display_index, byref(request)
283
+ )
284
+ if rc == ADL_OK:
285
+ break
286
+ time.sleep(0.01)
287
+ self._check(rc, "DPCD read" if operation == 0 else "DPCD write")
288
+ if operation == 0 and request.data_size < len(data):
289
+ raise AuxError(f"DPCD read returned {request.data_size} of {len(data)} bytes")
290
+ return bytes(request.data[: len(data)])
291
+
292
+ def read_dpcd(self, port: Port, address: int, length: int) -> bytes:
293
+ if length <= 0:
294
+ raise ValueError("length must be positive")
295
+ with self._lock:
296
+ self._ensure_open()
297
+ output = bytearray()
298
+ while len(output) < length:
299
+ size = min(AUX_CHUNK_SIZE, length - len(output))
300
+ output += self._native_aux(port, 0, address + len(output), bytes(size))
301
+ return bytes(output)
302
+
303
+ def write_dpcd(self, port: Port, address: int, data: bytes) -> None:
304
+ data = bytes(data)
305
+ if not data:
306
+ raise ValueError("data must not be empty")
307
+ with self._lock:
308
+ self._ensure_open()
309
+ for offset in range(0, len(data), AUX_CHUNK_SIZE):
310
+ self._native_aux(port, 1, address + offset, data[offset : offset + AUX_CHUNK_SIZE])
311
+
312
+ @staticmethod
313
+ def _dev7(address: int) -> int:
314
+ if not 0 <= address <= 0xFF:
315
+ raise ValueError("I2C address must fit in one byte")
316
+ return address >> 1 if address >= 0x80 else address & 0x7F
317
+
318
+ def _ddc(self, port: Port, send: bytes, receive_length: int) -> bytes:
319
+ self._validate_port(port)
320
+ send_buffer = (c_uint8 * len(send)).from_buffer_copy(send)
321
+ receive_buffer = (c_uint8 * receive_length)() if receive_length else None
322
+ receive_size = c_int(receive_length)
323
+ rc = -1
324
+ for _ in range(RETRY_COUNT):
325
+ receive_size.value = receive_length
326
+ rc = self._adl.ADL_Display_DDCBlockAccess_Get(
327
+ port.adapter.index,
328
+ port.logical_display_index,
329
+ 0,
330
+ 0,
331
+ len(send),
332
+ send_buffer,
333
+ byref(receive_size),
334
+ receive_buffer,
335
+ )
336
+ if rc == ADL_OK:
337
+ break
338
+ time.sleep(0.01)
339
+ self._check(rc, "I2C-over-AUX transaction")
340
+ if receive_size.value < receive_length:
341
+ raise AuxError(f"I2C-over-AUX returned {receive_size.value} of {receive_length} bytes")
342
+ return bytes(receive_buffer) if receive_buffer is not None else b""
343
+
344
+ def i2c_read(self, port: Port, device: int, register: int, length: int) -> bytes:
345
+ if length <= 0 or not 0 <= register <= 0xFF:
346
+ raise ValueError("length must be positive and register must fit in one byte")
347
+ with self._lock:
348
+ self._ensure_open()
349
+ output = bytearray()
350
+ read_address = (self._dev7(device) << 1) | 1
351
+ while len(output) < length:
352
+ size = min(AUX_CHUNK_SIZE, length - len(output))
353
+ output += self._ddc(port, bytes((read_address, (register + len(output)) & 0xFF)), size)
354
+ return bytes(output)
355
+
356
+ def i2c_write(self, port: Port, device: int, register: int, data: bytes) -> None:
357
+ data = bytes(data)
358
+ if not data or not 0 <= register <= 0xFF:
359
+ raise ValueError("data must not be empty and register must fit in one byte")
360
+ with self._lock:
361
+ self._ensure_open()
362
+ write_address = self._dev7(device) << 1
363
+ if self._dev7(device) == 0x30:
364
+ if len(data) != 1:
365
+ raise ValueError("EDID segment-pointer writes require exactly one byte")
366
+ self._ddc(port, bytes((write_address, data[0])), 0)
367
+ return
368
+ for offset in range(0, len(data), AUX_CHUNK_SIZE):
369
+ chunk = data[offset : offset + AUX_CHUNK_SIZE]
370
+ self._ddc(port, bytes((write_address, (register + offset) & 0xFF)) + chunk, 0)
gpu_aux/api.py ADDED
@@ -0,0 +1,240 @@
1
+ """Object-oriented public API for selecting and using one AUX port."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from dataclasses import dataclass
7
+
8
+ from .adl import Adapter, AmdAux, AuxError, Port
9
+ from .intel import IntelAux
10
+ from .nvapi import NvidiaAux
11
+
12
+
13
+ _context_lock = threading.RLock()
14
+ _contexts = {}
15
+
16
+
17
+ def _normalize_backend(backend: str) -> str:
18
+ if not isinstance(backend, str):
19
+ raise TypeError("backend must be 'AMD', 'NVIDIA', or 'INTEL'")
20
+ normalized = backend.strip().upper()
21
+ if normalized in {"AMD", "NVIDIA", "INTEL"}:
22
+ return normalized
23
+ raise ValueError("backend must be 'AMD', 'NVIDIA', or 'INTEL'")
24
+
25
+
26
+ def _create_aux(backend: str):
27
+ if backend == "AMD":
28
+ return AmdAux()
29
+ if backend == "NVIDIA":
30
+ return NvidiaAux()
31
+ if backend == "INTEL":
32
+ return IntelAux()
33
+ raise ValueError("backend must be 'AMD', 'NVIDIA', or 'INTEL'")
34
+
35
+
36
+ def _acquire_aux(backend: str):
37
+ normalized_backend = _normalize_backend(backend)
38
+ with _context_lock:
39
+ context = _contexts.get(normalized_backend)
40
+ if context is None:
41
+ context = [_create_aux(normalized_backend), 0]
42
+ _contexts[normalized_backend] = context
43
+ context[1] += 1
44
+ return context[0]
45
+
46
+
47
+ def _release_aux(backend: str, aux) -> None:
48
+ normalized_backend = _normalize_backend(backend)
49
+ with _context_lock:
50
+ context = _contexts.get(normalized_backend)
51
+ if context is None or aux is not context[0] or context[1] <= 0:
52
+ return
53
+ context[1] -= 1
54
+ if context[1] == 0:
55
+ aux.close()
56
+ del _contexts[normalized_backend]
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class GpuPorts:
61
+ """A physical GPU and a snapshot of its connected DP/eDP ports."""
62
+
63
+ gpu_index: int
64
+ adapter: Adapter
65
+ ports: tuple[Port, ...]
66
+
67
+
68
+ def enumerate_gpus(backend: str) -> list[Adapter]:
69
+ """Return physical GPUs for one backend without creating an ``AuxPort``."""
70
+
71
+ normalized_backend = _normalize_backend(backend)
72
+ aux = _acquire_aux(normalized_backend)
73
+ try:
74
+ return aux.adapters()
75
+ finally:
76
+ _release_aux(normalized_backend, aux)
77
+
78
+
79
+ def enumerate_ports(backend: str, gpu_index: int = 0) -> list[Port]:
80
+ """Return connected DP/eDP ports on one GPU without creating an ``AuxPort``."""
81
+
82
+ if gpu_index < 0:
83
+ raise ValueError("gpu_index must not be negative")
84
+ normalized_backend = _normalize_backend(backend)
85
+ aux = _acquire_aux(normalized_backend)
86
+ try:
87
+ adapters = aux.adapters()
88
+ if gpu_index >= len(adapters):
89
+ raise AuxError(
90
+ f"GPU index {gpu_index} is unavailable for {normalized_backend}; "
91
+ f"found {len(adapters)} GPU(s)"
92
+ )
93
+ return aux.ports(adapters[gpu_index])
94
+ finally:
95
+ _release_aux(normalized_backend, aux)
96
+
97
+
98
+ def enumerate_gpus_and_ports(backend: str) -> list[GpuPorts]:
99
+ """Return a combined GPU/port snapshot without creating an ``AuxPort``."""
100
+
101
+ normalized_backend = _normalize_backend(backend)
102
+ aux = _acquire_aux(normalized_backend)
103
+ try:
104
+ result = []
105
+ for gpu_index, adapter in enumerate(aux.adapters()):
106
+ result.append(
107
+ GpuPorts(
108
+ gpu_index=gpu_index,
109
+ adapter=adapter,
110
+ ports=tuple(aux.ports(adapter)),
111
+ )
112
+ )
113
+ return result
114
+ finally:
115
+ _release_aux(normalized_backend, aux)
116
+
117
+
118
+ class AuxPort:
119
+ """An opened DP/eDP AUX endpoint.
120
+
121
+ ``index`` is counted within ports of the requested kind on one physical
122
+ GPU. For example, ``AuxPort("DP", index=1, backend="NVIDIA")`` selects the
123
+ second external DP port on the first NVIDIA GPU.
124
+ """
125
+
126
+ def __init__(self, kind: str, index: int = 0, gpu_index: int = 0, *, backend: str) -> None:
127
+ self._aux = None
128
+ self._port = None
129
+
130
+ normalized_backend = _normalize_backend(backend)
131
+ normalized_kind = self._normalize_kind(kind)
132
+ if index < 0 or gpu_index < 0:
133
+ raise ValueError("index and gpu_index must not be negative")
134
+
135
+ aux = _acquire_aux(normalized_backend)
136
+ try:
137
+ adapters = aux.adapters()
138
+ if gpu_index >= len(adapters):
139
+ raise AuxError(
140
+ f"GPU index {gpu_index} is unavailable for {normalized_backend}; "
141
+ f"found {len(adapters)} GPU(s)"
142
+ )
143
+ adapter = adapters[gpu_index]
144
+ matching_ports = [
145
+ port for port in aux.ports(adapter) if port.kind == normalized_kind
146
+ ]
147
+ if index >= len(matching_ports):
148
+ raise AuxError(
149
+ f"{normalized_kind} port index {index} is unavailable on GPU {gpu_index}; "
150
+ f"found {len(matching_ports)} matching port(s)"
151
+ )
152
+ self._aux = aux
153
+ self._port = matching_ports[index]
154
+ self._backend = normalized_backend
155
+ self._gpu_index = gpu_index
156
+ self._kind_index = index
157
+ except Exception:
158
+ _release_aux(normalized_backend, aux)
159
+ raise
160
+
161
+ @staticmethod
162
+ def _normalize_kind(kind: str) -> str:
163
+ if not isinstance(kind, str):
164
+ raise TypeError("kind must be 'eDP' or 'DP'")
165
+ normalized = kind.strip().lower()
166
+ if normalized == "edp":
167
+ return "eDP"
168
+ if normalized == "dp":
169
+ return "DP"
170
+ raise ValueError("kind must be 'eDP' or 'DP'")
171
+
172
+ def _require_open(self) -> tuple[AmdAux, Port]:
173
+ if self._aux is None or self._port is None:
174
+ raise AuxError("AuxPort is closed")
175
+ return self._aux, self._port
176
+
177
+ @property
178
+ def info(self) -> Port:
179
+ """Return the immutable port information."""
180
+
181
+ return self._require_open()[1]
182
+
183
+ @property
184
+ def identity(self) -> str:
185
+ return self.info.identity
186
+
187
+ @property
188
+ def kind(self) -> str:
189
+ return self.info.kind
190
+
191
+ @property
192
+ def gpu_index(self) -> int:
193
+ self._require_open()
194
+ return self._gpu_index
195
+
196
+ @property
197
+ def backend(self) -> str:
198
+ self._require_open()
199
+ return self._backend
200
+
201
+ @property
202
+ def index(self) -> int:
203
+ self._require_open()
204
+ return self._kind_index
205
+
206
+ def read_dpcd(self, address: int, length: int) -> bytes:
207
+ aux, port = self._require_open()
208
+ return aux.read_dpcd(port, address, length)
209
+
210
+ def write_dpcd(self, address: int, data: bytes) -> None:
211
+ aux, port = self._require_open()
212
+ aux.write_dpcd(port, address, data)
213
+
214
+ def i2c_read(self, device: int, register: int, length: int) -> bytes:
215
+ aux, port = self._require_open()
216
+ return aux.i2c_read(port, device, register, length)
217
+
218
+ def i2c_write(self, device: int, register: int, data: bytes) -> None:
219
+ aux, port = self._require_open()
220
+ aux.i2c_write(port, device, register, data)
221
+
222
+ def close(self) -> None:
223
+ if self._aux is not None:
224
+ _release_aux(self._backend, self._aux)
225
+ self._aux = None
226
+ self._port = None
227
+
228
+ def __enter__(self) -> "AuxPort":
229
+ self._require_open()
230
+ return self
231
+
232
+ def __exit__(self, exc_type, exc_value, traceback) -> None:
233
+ self.close()
234
+
235
+ def __del__(self) -> None:
236
+ try:
237
+ self.close()
238
+ except Exception:
239
+ # Interpreter shutdown may have already released module globals.
240
+ pass