aqpxlib 0.1.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.
aqpxlib/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ # Copyright 2025 Acute Technology Inc. All rights reserved.
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ """Acute Protocol Exerciser Library."""
4
+
5
+ from .toplevel import AqProtocolExerciser
6
+
7
+ __version__ = "0.0.1"
8
+ __all__ = ["AqProtocolExerciser"]
@@ -0,0 +1,13 @@
1
+ # Copyright 2025 Acute Technology Inc. All rights reserved.
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ """Acute Protocol Exerciser Bus Instances."""
4
+
5
+ from aqpxlib.abstract.abstract_bus import PxAbstractBus
6
+ from aqpxlib.abstract.abstract_device import PxAbstractController, PxAbstractDevice, PxAbstractTarget
7
+
8
+ __all__ = [
9
+ "PxAbstractBus",
10
+ "PxAbstractController",
11
+ "PxAbstractDevice",
12
+ "PxAbstractTarget",
13
+ ]
@@ -0,0 +1,551 @@
1
+ # Copyright 2025 Acute Technology Inc. All rights reserved.
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ """Abstract Bus Class for all bus types."""
4
+
5
+ import logging
6
+ from abc import abstractmethod
7
+ from collections.abc import Callable
8
+ from functools import cached_property
9
+ from typing import TYPE_CHECKING, Any, ClassVar
10
+
11
+ import betterproto
12
+
13
+ import aqpxlib.message as pxmsg
14
+ from aqpxlib.abstract.abstract_device import PxAbstractDevice
15
+ from aqpxlib.abstract.abstract_instance import PxAbstractInstance
16
+ from aqpxlib.exceptions import PxError
17
+
18
+ if TYPE_CHECKING:
19
+ from aqpxlib.toplevel import AqProtocolExerciser
20
+
21
+ _LOGGER = logging.getLogger(__name__)
22
+
23
+
24
+ class PxAbstractBus(PxAbstractInstance):
25
+ """Abstract Bus Class for all bus types."""
26
+
27
+ _available_devices: ClassVar[list[type[PxAbstractDevice]]] = []
28
+
29
+ controller_class: type[PxAbstractDevice]
30
+
31
+ @cached_property
32
+ def available_devices_by_protobuf_key(self):
33
+ """A mapping of device protobuf field name to device classes."""
34
+ return {cls.protobuf_key: cls for cls in self._available_devices}
35
+
36
+ @cached_property
37
+ def available_devices_by_identifier(self):
38
+ """A mapping of device identifier (from abstract instance) to device classes."""
39
+ return {cls.identifier: cls for cls in self._available_devices}
40
+
41
+ def __init__(self, exerciser: "AqProtocolExerciser") -> None:
42
+ """Initialize an abstract bus."""
43
+ super().__init__(exerciser)
44
+ self._handle_index: int = -1
45
+
46
+ def _get_electrical_config(self) -> pxmsg.PxElectricalConfig | None:
47
+ """Get the electrical configuration of the bus."""
48
+ assert self._handle_index != -1, "Bus handle index is not set"
49
+
50
+ _LOGGER.info("Getting electrical config for bus %d", self._handle_index)
51
+ msg = pxmsg.PxProtobufMsg(
52
+ get_electrical_config_request=pxmsg.PxElectricalConfigRequest(
53
+ handle_idx=self._handle_index,
54
+ ),
55
+ )
56
+
57
+ response = self.exerciser.send_request(msg)
58
+ assert response is not None
59
+ return response.get_electrical_config_request.config
60
+
61
+ def _set_electrical_config(self, config: pxmsg.PxElectricalConfig) -> bool:
62
+ """Set the electrical configuration of the bus."""
63
+ assert self._handle_index != -1, "Bus handle index is not set"
64
+
65
+ msg = pxmsg.PxProtobufMsg(
66
+ set_electrical_config_request=pxmsg.PxElectricalConfigRequest(
67
+ handle_idx=self._handle_index,
68
+ config=config,
69
+ ),
70
+ )
71
+
72
+ try:
73
+ _ = self.exerciser.send_request(msg)
74
+ except PxError as e:
75
+ _LOGGER.exception("Failed to set electrical config: %s", e)
76
+ return False
77
+
78
+ return True
79
+
80
+ @property
81
+ def handle_index(self) -> int:
82
+ """The index of the bus interface."""
83
+ return self._handle_index
84
+
85
+ @handle_index.setter
86
+ def handle_index(self, handle_index: int):
87
+ """Set the index of the bus interface."""
88
+ self._handle_index = handle_index
89
+
90
+ @property
91
+ def voltage(self) -> int | None:
92
+ """The voltage of the bus."""
93
+ electrical_config = self._get_electrical_config()
94
+ if electrical_config is None:
95
+ return None
96
+
97
+ return electrical_config.voltage
98
+
99
+ @voltage.setter
100
+ def voltage(self, voltage: int):
101
+ """Set the voltage of the bus."""
102
+ if voltage < 0:
103
+ err_msg = f"Invalid voltage value: {voltage}"
104
+ raise ValueError(err_msg)
105
+
106
+ config = pxmsg.PxElectricalConfig(
107
+ voltage=voltage,
108
+ )
109
+
110
+ if not self._set_electrical_config(config):
111
+ err_msg = f"Failed to set voltage: {voltage}"
112
+ raise ValueError(err_msg)
113
+
114
+ @property
115
+ def la_threshold_voltage(self) -> int | None:
116
+ """The la threshold voltage of the bus."""
117
+ electrical_config = self._get_electrical_config()
118
+ if electrical_config is None:
119
+ return None
120
+
121
+ return electrical_config.la_threshold_voltage
122
+
123
+ @la_threshold_voltage.setter
124
+ def la_threshold_voltage(self, voltage: int):
125
+ """Set the la threshold voltage of the bus."""
126
+ if voltage < 0:
127
+ err_msg = f"Invalid voltage value: {voltage}"
128
+ raise ValueError(err_msg)
129
+
130
+ config = pxmsg.PxElectricalConfig(
131
+ la_threshold_voltage=voltage,
132
+ )
133
+
134
+ if not self._set_electrical_config(config):
135
+ err_msg = f"Failed to set la threshold voltage: {voltage}"
136
+ raise ValueError(err_msg)
137
+
138
+ @property
139
+ def pullup_resistance(self) -> int | None:
140
+ """The pullup resistance of the bus."""
141
+ electrical_config = self._get_electrical_config()
142
+ if electrical_config is None:
143
+ return None
144
+
145
+ return electrical_config.pullup_resistance
146
+
147
+ @pullup_resistance.setter
148
+ def pullup_resistance(self, resistance: int):
149
+ """Set the pullup resistance of the bus."""
150
+ if resistance < 0:
151
+ err_msg = f"Invalid resistance value: {resistance}"
152
+ raise ValueError(err_msg)
153
+
154
+ config = pxmsg.PxElectricalConfig(
155
+ pullup_resistance=resistance,
156
+ )
157
+
158
+ if not self._set_electrical_config(config):
159
+ err_msg = f"Failed to set pullup resistance: {resistance}"
160
+ raise ValueError(err_msg)
161
+
162
+ @property
163
+ def pulldown_resistance(self) -> int | None:
164
+ """The pulldown resistance of the bus."""
165
+ electrical_config = self._get_electrical_config()
166
+ if electrical_config is None:
167
+ return None
168
+
169
+ return electrical_config.pulldown_resistance
170
+
171
+ @pulldown_resistance.setter
172
+ def pulldown_resistance(self, resistance: int):
173
+ """Set the pulldown resistance of the bus."""
174
+ if resistance < 0:
175
+ err_msg = f"Invalid resistance value: {resistance}"
176
+ raise ValueError(err_msg)
177
+
178
+ config = pxmsg.PxElectricalConfig(
179
+ pulldown_resistance=resistance,
180
+ )
181
+
182
+ if not self._set_electrical_config(config):
183
+ err_msg = f"Failed to set pulldown resistance: {resistance}"
184
+ raise ValueError(err_msg)
185
+
186
+ @property
187
+ def bus_hold_enable(self) -> bool | None:
188
+ """The bus hold enable of the bus."""
189
+ electrical_config = self._get_electrical_config()
190
+ if electrical_config is None:
191
+ return None
192
+
193
+ return electrical_config.bus_hold_enable
194
+
195
+ @bus_hold_enable.setter
196
+ def bus_hold_enable(self, enable: bool):
197
+ """Set the bus hold enable of the bus."""
198
+ config = pxmsg.PxElectricalConfig(
199
+ bus_hold_enable=enable,
200
+ )
201
+
202
+ if not self._set_electrical_config(config):
203
+ err_msg = f"Failed to set bus hold enable: {enable}"
204
+ raise ValueError(err_msg)
205
+
206
+ def get_config(self) -> pxmsg.PxHandle | None:
207
+ """Get the configuration of the bus."""
208
+ assert self._handle_index != -1, "Bus handle index is not set"
209
+
210
+ handle_config = self.exerciser.get_handle(self._handle_index)
211
+
212
+ bus_config = handle_config
213
+ bus_key, bus_config = betterproto.which_one_of(bus_config, "protocol")
214
+ if bus_key == self.protobuf_key:
215
+ return handle_config
216
+ _LOGGER.exception("Bus configuration is not supported")
217
+ return None
218
+
219
+ def get_target_pb_msg(
220
+ self,
221
+ target: PxAbstractDevice,
222
+ ) -> betterproto.Message | None:
223
+ """Get the configuration of the target from the bus."""
224
+ config = self.get_config()
225
+ if config is None:
226
+ _LOGGER.exception("Failed to get bus configuration")
227
+ return None
228
+
229
+ for target_config in config.targets:
230
+ if target_config.id == target.device_id:
231
+ return target_config
232
+ return None
233
+
234
+ def get_controller_pb_msg(
235
+ self,
236
+ controller: PxAbstractDevice,
237
+ ) -> betterproto.Message | None:
238
+ """Get the configuration of the controller from the bus."""
239
+ controllers_config = self.get_config().controllers
240
+ for controller_config in controllers_config:
241
+ if controller_config.id == controller.device_id:
242
+ return controller_config
243
+ return None
244
+
245
+ def remove_controller(self, controller: PxAbstractDevice):
246
+ """Remove a controller from the bus."""
247
+ config = self.get_config()
248
+ if config is None:
249
+ _LOGGER.exception("Failed to get bus configuration")
250
+ return
251
+
252
+ config.controllers = [
253
+ controller_pb_msg
254
+ for controller_pb_msg in config.controllers
255
+ if controller_pb_msg.id != controller.device_id
256
+ ]
257
+ self.set_config(config)
258
+
259
+ def remove_target(self, target: PxAbstractDevice):
260
+ """Remove a target from the bus."""
261
+ config = self.get_config()
262
+ if config is None:
263
+ _LOGGER.exception("Failed to get bus configuration")
264
+ return
265
+
266
+ config.targets = [target_pb_msg for target_pb_msg in config.targets if target_pb_msg.id != target.device_id]
267
+
268
+ self.set_config(config)
269
+
270
+ def remove_device(self, device: PxAbstractDevice):
271
+ """Remove a device from the bus."""
272
+ if self.controller_class is not None and isinstance(
273
+ device,
274
+ self.controller_class,
275
+ ):
276
+ self.remove_controller(device)
277
+ else:
278
+ self.remove_target(device)
279
+
280
+ def get_device_state(
281
+ self,
282
+ device: PxAbstractDevice,
283
+ ) -> betterproto.Message | None:
284
+ """Get the state of the device from the bus."""
285
+ if type(device) not in self._available_devices:
286
+ err_msg = f"Device {device} is not supported on {self} bus."
287
+ raise ValueError(err_msg)
288
+
289
+ msg = pxmsg.PxProtobufMsg(
290
+ px_device_operation=pxmsg.PxDeviceOperationRequest(
291
+ handle_idx=self.handle_index,
292
+ device_id=device.device_id,
293
+ op=pxmsg.PxDeviceOperation(
294
+ px_device_get_state=pxmsg.PxDeviceState(),
295
+ ),
296
+ ),
297
+ )
298
+
299
+ response = self.exerciser.send_request(
300
+ msg,
301
+ )
302
+ assert response is not None
303
+ dev_state = response.px_device_operation.op.px_device_get_state
304
+ _, state = betterproto.which_one_of(dev_state, "state")
305
+ return state
306
+
307
+ def set_device_state(self, device: PxAbstractDevice, state: betterproto.Message):
308
+ """Set the state of the device on the bus."""
309
+ if type(device) not in self._available_devices:
310
+ err_msg = f"Device {device} is not supported on {self} bus."
311
+ raise ValueError(err_msg)
312
+
313
+ msg = pxmsg.PxProtobufMsg(
314
+ px_device_operation=pxmsg.PxDeviceOperationRequest(
315
+ handle_idx=self.handle_index,
316
+ device_id=device.device_id,
317
+ op=pxmsg.PxDeviceOperation(
318
+ px_device_set_state=pxmsg.PxDeviceState(
319
+ **{
320
+ device.protobuf_key: state,
321
+ },
322
+ ),
323
+ ),
324
+ ),
325
+ )
326
+
327
+ _ = self.exerciser.send_request(msg)
328
+
329
+ def get_device_pb_msg(
330
+ self,
331
+ device: PxAbstractDevice,
332
+ ) -> betterproto.Message | None:
333
+ """Get the configuration of the device from the bus."""
334
+ if isinstance(device, self.controller_class):
335
+ return self.get_controller_pb_msg(device)
336
+ return self.get_target_pb_msg(device)
337
+
338
+ def set_config(self, config: pxmsg.PxHandle) -> pxmsg.PxHandle:
339
+ """Set the configuration of the bus."""
340
+ assert self._handle_index != -1, "Bus handle index is not set"
341
+
342
+ return self.exerciser.set_handle(self._handle_index, config)
343
+
344
+ def get_controllers(self) -> list[PxAbstractDevice]:
345
+ """Get the controllers of the bus."""
346
+ config = self.get_config()
347
+ if config is None:
348
+ _LOGGER.exception("Failed to get bus configuration")
349
+ return []
350
+
351
+ if hasattr(config, "controllers"):
352
+ # TODO: Handle buses that have more than 1 controller
353
+ for controller_config in config.controllers:
354
+ if self.controller_class is not None:
355
+ controller = self.controller_class(
356
+ self.exerciser,
357
+ bus=self,
358
+ device_id=controller_config.id,
359
+ )
360
+ return [controller]
361
+
362
+ _LOGGER.info("Can not find any controllers")
363
+ return []
364
+
365
+ def attach_controller(self, controller: PxAbstractDevice):
366
+ """Attach a controller to the bus."""
367
+ config = self.get_config()
368
+ if config is None:
369
+ _LOGGER.exception("Failed to get bus configuration")
370
+ return
371
+ if hasattr(config, "controller") and betterproto.serialized_on_wire(
372
+ config.controller,
373
+ ):
374
+ err_msg = f"Bus already has a controller, config = {config}"
375
+ raise ValueError(err_msg)
376
+
377
+ config.controllers.append(
378
+ pxmsg.PxControllerDevice.from_dict(
379
+ {
380
+ "id": 0,
381
+ "name": controller.name,
382
+ controller.protobuf_key: controller.config.to_dict(),
383
+ },
384
+ ),
385
+ )
386
+ controller.device_id = self.set_config(config).controllers[len(config.controllers) - 1].id
387
+ controller.bus = self
388
+
389
+ def attach_target(self, target: PxAbstractDevice):
390
+ """Attach a target to the bus."""
391
+ config = self.get_config()
392
+ if config is None:
393
+ _LOGGER.exception("Failed to get bus configuration")
394
+ return
395
+ config.targets.append(
396
+ pxmsg.PxTargetDevice.from_dict(
397
+ {
398
+ "id": 0,
399
+ "name": target.name,
400
+ target.protobuf_key: target.config.to_dict(),
401
+ },
402
+ ),
403
+ )
404
+
405
+ target.device_id = self.set_config(config).targets[len(config.targets) - 1].id
406
+ target.bus = self
407
+
408
+ def get_targets(self) -> list[PxAbstractDevice]:
409
+ """Get the targets of the bus."""
410
+ config = self.get_config()
411
+ if config is None:
412
+ _LOGGER.exception("Failed to get bus configuration")
413
+ return []
414
+
415
+ targets = []
416
+ for target_pb_msg in config.targets:
417
+ pb_key, _ = betterproto.which_one_of(target_pb_msg, "config")
418
+ target_class = self.available_devices_by_protobuf_key.get(pb_key, None)
419
+ if target_class is not None:
420
+ target = target_class(
421
+ self.exerciser,
422
+ bus=self,
423
+ device_id=target_pb_msg.id,
424
+ )
425
+ targets.append(target)
426
+ return targets
427
+
428
+ def get_devices(self) -> list[PxAbstractDevice]:
429
+ """Get the devices of the bus."""
430
+ return self.get_controllers() + self.get_targets()
431
+
432
+ def create_device(
433
+ self,
434
+ exerciser: "AqProtocolExerciser",
435
+ device_identifier: str,
436
+ *args,
437
+ **kwargs,
438
+ ) -> PxAbstractDevice:
439
+ """Create a new device on the bus.
440
+
441
+ A new device interface will be created and attached to the bus.
442
+
443
+ Args:
444
+ exerciser (AqProtocolExerciser): An exerciser instance which is used to send request to server
445
+ device_identifier (str): The unique identifier of the device to create
446
+ *args: Additional arguments to pass to the device constructor
447
+ **kwargs: Additional keyword arguments to pass to the device constructor
448
+
449
+ Returns:
450
+ PxAbstractDevice: The interface of the created device
451
+
452
+ Raises:
453
+ ValueError: If the `device_identifier` is not supported
454
+
455
+ """
456
+ if device_identifier not in self.available_devices_by_identifier:
457
+ err_msg = f"Device {device_identifier} not supported"
458
+ raise ValueError(err_msg)
459
+
460
+ dev = self.available_devices_by_identifier[device_identifier](
461
+ exerciser,
462
+ *args,
463
+ **kwargs,
464
+ )
465
+ dev.attach_to_bus(self)
466
+
467
+ return dev
468
+
469
+ @abstractmethod
470
+ def _to_protobuf_handle_config(self, *args, **kwargs) -> pxmsg.PxHandle:
471
+ """Convert the bus to a protobuf handle configuration.
472
+
473
+ Args:
474
+ *args: Additional arguments to pass to the bus constructor
475
+ **kwargs: Additional keyword arguments to pass to the bus constructor
476
+
477
+ Returns:
478
+ pxmsg.PxHandle: The protobuf structure of the bus
479
+
480
+ Raises:
481
+ NotImplementedError: If the bus subclass does not implement this method
482
+
483
+ """
484
+ err_msg = "Bus subclass must implement this method"
485
+ raise NotImplementedError(err_msg)
486
+
487
+ def __eq__(self, other: object) -> bool:
488
+ """Check if the bus is equal to another bus.
489
+
490
+ This method is used to check if the bus is equal to another bus.
491
+
492
+ Args:
493
+ other (Any): The other bus to compare with or a protobuf structure of the bus
494
+
495
+ Raises:
496
+ NotImplementedError: If the bus subclass does not implement this method
497
+
498
+ """
499
+ err_msg = "Bus subclass must implement this method"
500
+ raise NotImplementedError(err_msg)
501
+
502
+ def _find_or_create_bus(self, *args, **kwargs) -> int:
503
+ """Find or create a bus interface.
504
+
505
+ This method is used to find an available handle slot and create a new bus interface.
506
+ It the bus handle does not exist, a new bus interface will be created.
507
+ Otherwise, the existing bus interface will be returned.
508
+
509
+ Args:
510
+ *args: Additional arguments to pass to the bus constructor
511
+ **kwargs: Additional keyword arguments to pass to the bus constructor
512
+
513
+ Returns:
514
+ int: The index of the bus interface if found, otherwise -1
515
+
516
+ Raises:
517
+ ValueError: If no empty slot is found
518
+
519
+ """
520
+ handles = self.exerciser.get_handles()
521
+ if handles is None:
522
+ _LOGGER.exception("Failed to get handles")
523
+ return -1
524
+
525
+ available_handle_slot = -1
526
+ for idx, handle in enumerate(handles):
527
+ name, current_handle = betterproto.which_one_of(handle, "protocol")
528
+ if name == self.protobuf_key:
529
+ if self == current_handle:
530
+ return idx
531
+ elif name in ["resv", ""]:
532
+ # Compare with reserved handle and unset handles
533
+ available_handle_slot = idx if available_handle_slot == -1 else available_handle_slot
534
+
535
+ _LOGGER.info("No existing bus found, creating a new bus")
536
+ if available_handle_slot == -1:
537
+ _LOGGER.exception("No empty slot found, cannot create a new bus")
538
+ return -1
539
+
540
+ # Update topology
541
+ new_bus = self._to_protobuf_handle_config(*args, **kwargs)
542
+ self.exerciser.set_handle(available_handle_slot, new_bus)
543
+
544
+ _LOGGER.info("New bus created at index %d", available_handle_slot)
545
+
546
+ return available_handle_slot
547
+
548
+ def add_event_handler(self, event: str, handler: Callable[[Any], Any]):
549
+ """Add an event handler to the current bus instance."""
550
+ err_msg = "Bus subclass must implement this method"
551
+ raise NotImplementedError(err_msg)