virtual-drive 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.
- virtual_drive/__init__.py +9 -0
- virtual_drive/_version.py +34 -0
- virtual_drive/adapters/__init__.py +8 -0
- virtual_drive/adapters/dispatcher.py +83 -0
- virtual_drive/adapters/mcb.py +181 -0
- virtual_drive/adapters/protocol.py +169 -0
- virtual_drive/adapters/sdo.py +194 -0
- virtual_drive/constants.py +43 -0
- virtual_drive/core.py +69 -0
- virtual_drive/enums.py +22 -0
- virtual_drive/environment.py +86 -0
- virtual_drive/exceptions.py +79 -0
- virtual_drive/py.typed +0 -0
- virtual_drive/register_service/__init__.py +15 -0
- virtual_drive/register_service/broker.py +59 -0
- virtual_drive/register_service/modules/__init__.py +45 -0
- virtual_drive/register_service/modules/capture/__init__.py +5 -0
- virtual_drive/register_service/modules/capture/base.py +279 -0
- virtual_drive/register_service/modules/capture/disturbance.py +326 -0
- virtual_drive/register_service/modules/capture/monitoring.py +423 -0
- virtual_drive/register_service/modules/capture/monitoring_v1.py +41 -0
- virtual_drive/register_service/modules/encoders/__init__.py +17 -0
- virtual_drive/register_service/modules/encoders/absolute_encoder_1.py +47 -0
- virtual_drive/register_service/modules/encoders/absolute_encoder_2.py +47 -0
- virtual_drive/register_service/modules/encoders/base_encoder.py +25 -0
- virtual_drive/register_service/modules/encoders/halls_encoder.py +59 -0
- virtual_drive/register_service/modules/encoders/incremental_encoder_1.py +49 -0
- virtual_drive/register_service/modules/encoders/incremental_encoder_2.py +48 -0
- virtual_drive/register_service/modules/encoders/secondary_ssi_encoder.py +47 -0
- virtual_drive/register_service/modules/feedbacks/__init__.py +0 -0
- virtual_drive/register_service/modules/feedbacks/commutation_feedback.py +63 -0
- virtual_drive/register_service/modules/feedbacks/position_feedback.py +46 -0
- virtual_drive/register_service/modules/gpios.py +76 -0
- virtual_drive/register_service/modules/identification.py +70 -0
- virtual_drive/register_service/modules/internal_generator.py +349 -0
- virtual_drive/register_service/modules/operation_mode.py +119 -0
- virtual_drive/register_service/modules/phasing.py +578 -0
- virtual_drive/register_service/modules/plants/__init__.py +1 -0
- virtual_drive/register_service/modules/plants/base_signal_register.py +423 -0
- virtual_drive/register_service/modules/plants/current_loop_rate.py +58 -0
- virtual_drive/register_service/modules/plants/plant_classes.py +121 -0
- virtual_drive/register_service/modules/plants/plant_module_bases.py +434 -0
- virtual_drive/register_service/modules/plants/plant_signal_handler.py +282 -0
- virtual_drive/register_service/modules/plants/position_plant_modules.py +119 -0
- virtual_drive/register_service/modules/plants/rl_direct_plant_modules.py +111 -0
- virtual_drive/register_service/modules/plants/rl_quadrature_plant_modules.py +111 -0
- virtual_drive/register_service/modules/plants/velocity_plant_modules.py +114 -0
- virtual_drive/register_service/modules/plants/voltage_to_current_plant_modules.py +59 -0
- virtual_drive/register_service/modules/plants/voltage_to_velocity_plant_module.py +58 -0
- virtual_drive/register_service/modules/state_control.py +110 -0
- virtual_drive/register_service/modules/storage.py +128 -0
- virtual_drive/register_service/modules/virtual_table.py +110 -0
- virtual_drive/register_service/owner_registry.py +30 -0
- virtual_drive/register_service/register_value.py +3 -0
- virtual_drive/register_service/service.py +761 -0
- virtual_drive/register_service/unowned_bank.py +35 -0
- virtual_drive/register_service/virtual_register.py +82 -0
- virtual_drive/resources/__init__.py +8 -0
- virtual_drive/resources/virtual_drive.xcf +323 -0
- virtual_drive/resources/virtual_drive.xdf +3734 -0
- virtual_drive/resources/virtual_drive_canopen.xdf +5008 -0
- virtual_drive/resources/virtual_drive_v3.0.xdf +4130 -0
- virtual_drive/signals.py +103 -0
- virtual_drive-0.1.0.dist-info/LICENSE.md +157 -0
- virtual_drive-0.1.0.dist-info/METADATA +221 -0
- virtual_drive-0.1.0.dist-info/RECORD +68 -0
- virtual_drive-0.1.0.dist-info/WHEEL +5 -0
- virtual_drive-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = 'gb9be2494f'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Protocol adapters implemented by the runtime."""
|
|
2
|
+
|
|
3
|
+
from virtual_drive.adapters.dispatcher import FrameDispatcher
|
|
4
|
+
from virtual_drive.adapters.mcb import MCBAdapter
|
|
5
|
+
from virtual_drive.adapters.protocol import ProtocolAdapter
|
|
6
|
+
from virtual_drive.adapters.sdo import SDOAdapter
|
|
7
|
+
|
|
8
|
+
__all__ = ["FrameDispatcher", "MCBAdapter", "SDOAdapter", "ProtocolAdapter"]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ingenialink.dictionary import Interface
|
|
6
|
+
|
|
7
|
+
from virtual_drive.adapters.mcb import MCBAdapter
|
|
8
|
+
from virtual_drive.adapters.protocol import (
|
|
9
|
+
OperationResult,
|
|
10
|
+
ProtocolAdapter,
|
|
11
|
+
)
|
|
12
|
+
from virtual_drive.adapters.sdo import SDOAdapter
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from virtual_drive.register_service import RegisterService
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FrameDispatcher:
|
|
19
|
+
"""Dispatch incoming frames through a configured protocol adapter."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
register_service: RegisterService,
|
|
24
|
+
protocol: Interface = Interface.ETH,
|
|
25
|
+
adapter: ProtocolAdapter | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self._register_service = register_service
|
|
28
|
+
self._adapter = adapter if adapter is not None else self._build_adapter(protocol)
|
|
29
|
+
|
|
30
|
+
def dispatch(self, frame: bytes) -> bytes | None:
|
|
31
|
+
"""Return the response for a frame, or ``None`` if it cannot be handled.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
frame: Incoming frame to be dispatched.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The response frame, or ``None`` if the frame cannot be handled.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
request = self._adapter.parse_request(frame)
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
if self._adapter.is_unknown_operation(request):
|
|
46
|
+
result = OperationResult(success=False, value=None)
|
|
47
|
+
return self._adapter.build_response(request, result)
|
|
48
|
+
register = None
|
|
49
|
+
operation_error = None
|
|
50
|
+
try:
|
|
51
|
+
register = self._adapter.resolve_register(self._register_service, request)
|
|
52
|
+
if self._adapter.is_write_operation(request):
|
|
53
|
+
value = self._adapter.decode_write_value(register, request)
|
|
54
|
+
self._register_service.write(register, value)
|
|
55
|
+
result = OperationResult(success=True, value=None)
|
|
56
|
+
else:
|
|
57
|
+
value = self._register_service.read(register)
|
|
58
|
+
result = OperationResult(success=True, value=value)
|
|
59
|
+
except Exception as error:
|
|
60
|
+
operation_error = error
|
|
61
|
+
result = OperationResult(success=False, value=None)
|
|
62
|
+
|
|
63
|
+
return self._adapter.build_response(request, result, register, operation_error)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def _build_adapter(protocol: Interface) -> ProtocolAdapter:
|
|
67
|
+
"""Instantiate the adapter for the selected protocol.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
protocol: Interface value used to select the protocol adapter.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
ProtocolAdapter: Adapter instance for the configured protocol.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: If the interface is not supported.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
if protocol == Interface.ETH:
|
|
80
|
+
return MCBAdapter()
|
|
81
|
+
if protocol in (Interface.ECAT, Interface.CAN):
|
|
82
|
+
return SDOAdapter()
|
|
83
|
+
raise ValueError(f"Unsupported protocol interface '{protocol.name}'.")
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from ingenialink.utils._utils import convert_bytes_to_dtype, convert_dtype_to_bytes
|
|
7
|
+
from ingenialink.utils.mcb import MCB
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ingenialink.register import Register
|
|
11
|
+
|
|
12
|
+
from virtual_drive.register_service import RegisterService
|
|
13
|
+
from virtual_drive.register_service.register_value import RegisterValue
|
|
14
|
+
|
|
15
|
+
from virtual_drive.adapters.protocol import (
|
|
16
|
+
MCBOperationRequest,
|
|
17
|
+
MCBOperationType,
|
|
18
|
+
OperationRequest,
|
|
19
|
+
OperationResult,
|
|
20
|
+
ProtocolAdapter,
|
|
21
|
+
)
|
|
22
|
+
from virtual_drive.exceptions import OPERATION_NOT_EXECUTED_ERROR_CODE, VirtualDriveError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MCBCommand(Enum):
|
|
26
|
+
"""Enum of the MCB commands."""
|
|
27
|
+
|
|
28
|
+
READ = 1
|
|
29
|
+
WRITE = 2
|
|
30
|
+
ACK = 3
|
|
31
|
+
ERROR_READ = 5
|
|
32
|
+
ERROR_WRITE = 6
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MCBAdapter(ProtocolAdapter):
|
|
36
|
+
"""Simple adapter that understands the IngeniaLink MCB/UDP protocol."""
|
|
37
|
+
|
|
38
|
+
def parse_request(self, frame: bytes) -> OperationRequest:
|
|
39
|
+
"""Parse the MCB request frame into an operation request.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
OperationRequest: Parsed operation request data.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
address, axis, command, data = MCB.read_mcb_frame(frame)
|
|
46
|
+
if command == MCBCommand.WRITE.value:
|
|
47
|
+
operation = MCBOperationType.WRITE
|
|
48
|
+
elif command == MCBCommand.READ.value:
|
|
49
|
+
operation = MCBOperationType.READ
|
|
50
|
+
else:
|
|
51
|
+
operation = MCBOperationType.UNKNOWN
|
|
52
|
+
return MCBOperationRequest(axis=axis, address=address, operation=operation, data=data)
|
|
53
|
+
|
|
54
|
+
def is_unknown_operation(self, request: OperationRequest) -> bool:
|
|
55
|
+
"""Return whether the request operation is unsupported."""
|
|
56
|
+
mcb_request = self._as_mcb_request(request)
|
|
57
|
+
return mcb_request.operation is MCBOperationType.UNKNOWN
|
|
58
|
+
|
|
59
|
+
def is_write_operation(self, request: OperationRequest) -> bool:
|
|
60
|
+
"""Return whether the request operation writes a register value."""
|
|
61
|
+
mcb_request = self._as_mcb_request(request)
|
|
62
|
+
return mcb_request.operation is MCBOperationType.WRITE
|
|
63
|
+
|
|
64
|
+
def resolve_register(
|
|
65
|
+
self,
|
|
66
|
+
register_service: RegisterService,
|
|
67
|
+
request: OperationRequest,
|
|
68
|
+
) -> Register:
|
|
69
|
+
"""Resolve the addressed register from an MCB request.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Register: Register identified by axis/address in the request.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
mcb_request = self._as_mcb_request(request)
|
|
76
|
+
return register_service.get_register_by_address(mcb_request.axis, mcb_request.address)
|
|
77
|
+
|
|
78
|
+
def decode_write_value(self, register: Register, request: OperationRequest) -> RegisterValue:
|
|
79
|
+
"""Decode the write payload into a register value.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
RegisterValue: The decoded register value.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
return convert_bytes_to_dtype(request.data, register.dtype)
|
|
86
|
+
|
|
87
|
+
def build_response(
|
|
88
|
+
self,
|
|
89
|
+
request: OperationRequest,
|
|
90
|
+
result: OperationResult,
|
|
91
|
+
register: Register | None = None,
|
|
92
|
+
error: Exception | None = None,
|
|
93
|
+
) -> bytes:
|
|
94
|
+
"""Return the response frame for the provided MCB request."""
|
|
95
|
+
mcb_request = self._as_mcb_request(request)
|
|
96
|
+
axis, address = mcb_request.axis, mcb_request.address
|
|
97
|
+
if not result.success:
|
|
98
|
+
return self._build_error_frame(mcb_request, error)
|
|
99
|
+
if mcb_request.operation is MCBOperationType.WRITE:
|
|
100
|
+
payload = mcb_request.data[:8]
|
|
101
|
+
return MCB.build_mcb_frame(MCBCommand.ACK.value, axis, address, payload)
|
|
102
|
+
if register is None or result.value is None:
|
|
103
|
+
return self._build_error_frame(mcb_request, None)
|
|
104
|
+
payload = self._encode_read_value(register, result.value)
|
|
105
|
+
return MCB.build_mcb_frame(MCBCommand.ACK.value, axis, address, payload)
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _as_mcb_request(request: OperationRequest) -> MCBOperationRequest:
|
|
109
|
+
if not isinstance(request, MCBOperationRequest):
|
|
110
|
+
raise TypeError("MCBAdapter requires MCBOperationRequest")
|
|
111
|
+
return request
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _encode_read_value(register: Register, value: RegisterValue) -> bytes:
|
|
115
|
+
if isinstance(value, bytes):
|
|
116
|
+
return value
|
|
117
|
+
return convert_dtype_to_bytes(value, register.dtype)
|
|
118
|
+
|
|
119
|
+
def _build_error_frame(
|
|
120
|
+
self,
|
|
121
|
+
request: MCBOperationRequest,
|
|
122
|
+
error: Exception | None,
|
|
123
|
+
) -> bytes:
|
|
124
|
+
"""Build an MCB error response with an embedded protocol error code.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
request: Original parsed MCB request.
|
|
128
|
+
error: The operation error that triggered the failure, if any.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
bytes: MCB error frame with the 32-bit error code in config payload.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
command = self._resolve_error_command(request.operation)
|
|
135
|
+
payload = self._build_error_payload(self._resolve_error_code(error))
|
|
136
|
+
return MCB.build_mcb_frame(command.value, request.axis, request.address, payload)
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _resolve_error_command(operation: MCBOperationType) -> MCBCommand:
|
|
140
|
+
"""Resolve the MCB error command to be used in the error response frame.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
operation: The type of the original request operation that failed.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The corresponding MCB error command for the failed operation.
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
if operation is MCBOperationType.WRITE:
|
|
150
|
+
return MCBCommand.ERROR_WRITE
|
|
151
|
+
return MCBCommand.ERROR_READ
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _build_error_payload(error_code: int) -> bytes:
|
|
155
|
+
"""Build the MCB error frame payload containing the 32-bit error code.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
error_code: The protocol-specific error code to be embedded in the frame.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
A bytes object of length 8, where the first 4 bytes represent the error code
|
|
162
|
+
in little-endian format, and the remaining 4 bytes are padding.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
return error_code.to_bytes(8, byteorder="little", signed=False)
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _resolve_error_code(error: Exception | None) -> int:
|
|
169
|
+
"""Resolve the protocol error code to be embedded in the MCB error frame.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
error: The original exception that caused the operation to fail, if any.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
The protocol-specific error code extracted from the exception, or a default
|
|
176
|
+
code if the error is not a VirtualDriveError or is None.
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
if isinstance(error, VirtualDriveError):
|
|
180
|
+
return error.error_code
|
|
181
|
+
return OPERATION_NOT_EXECUTED_ERROR_CODE
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, Union
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ingenialink.register import Register
|
|
9
|
+
|
|
10
|
+
from virtual_drive.register_service import RegisterService
|
|
11
|
+
from virtual_drive.register_service.register_value import RegisterValue
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MCBOperationType(Enum):
|
|
15
|
+
"""Register operations represented by incoming MCB frames."""
|
|
16
|
+
|
|
17
|
+
READ = "read"
|
|
18
|
+
WRITE = "write"
|
|
19
|
+
UNKNOWN = "unknown"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class MCBOperationRequest:
|
|
24
|
+
"""Operation extracted from an MCB protocol frame.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
axis: Target axis/subnode from the frame.
|
|
28
|
+
address: Register address to access.
|
|
29
|
+
operation: Register operation to execute.
|
|
30
|
+
data: Raw protocol payload associated with the request.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
axis: int
|
|
35
|
+
address: int
|
|
36
|
+
operation: MCBOperationType
|
|
37
|
+
data: bytes
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SDOOperationType(Enum):
|
|
41
|
+
"""Register operations represented by incoming SDO frames."""
|
|
42
|
+
|
|
43
|
+
READ = "read"
|
|
44
|
+
WRITE = "write"
|
|
45
|
+
UNKNOWN = "unknown"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class SDOOperationRequest:
|
|
50
|
+
"""Operation extracted from an SDO protocol frame.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
index: Object dictionary index.
|
|
54
|
+
subindex: Object dictionary subindex.
|
|
55
|
+
operation: Register operation to execute.
|
|
56
|
+
data: Raw protocol payload associated with the request.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
index: int
|
|
61
|
+
subindex: int
|
|
62
|
+
operation: SDOOperationType
|
|
63
|
+
data: bytes
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
OperationRequest = Union[MCBOperationRequest, SDOOperationRequest]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class OperationResult:
|
|
71
|
+
"""Result of executing a normalized register operation.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
success: Whether the operation completed successfully.
|
|
75
|
+
value: Read value for successful read operations, otherwise ``None``.
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
success: bool
|
|
80
|
+
value: RegisterValue | None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ProtocolAdapter(Protocol):
|
|
84
|
+
"""Adapter contract for translating frames to/from register operations.
|
|
85
|
+
|
|
86
|
+
Implementations are responsible for protocol-specific parsing, payload
|
|
87
|
+
conversion, and response encoding.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def parse_request(self, frame: bytes) -> OperationRequest:
|
|
92
|
+
"""Decode ``frame`` into a normalized :class:`OperationRequest`."""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
def is_unknown_operation(self, request: OperationRequest) -> bool:
|
|
96
|
+
"""Return whether ``request`` represents an unsupported operation."""
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
def is_write_operation(self, request: OperationRequest) -> bool:
|
|
100
|
+
"""Return whether ``request`` represents a write operation."""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
def resolve_register(
|
|
104
|
+
self,
|
|
105
|
+
register_service: RegisterService,
|
|
106
|
+
request: OperationRequest,
|
|
107
|
+
) -> Register:
|
|
108
|
+
"""Resolve the target register for ``request`` using ``register_service``."""
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
def decode_write_value(self, register: Register, request: OperationRequest) -> RegisterValue:
|
|
112
|
+
"""Decode the write payload in ``request`` into a register value.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
register: Target register metadata (dtype, etc.) used for decoding.
|
|
116
|
+
request: Parsed write request containing protocol payload bytes.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
RegisterValue: Value ready to be persisted by the register service.
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
def build_response(
|
|
125
|
+
self,
|
|
126
|
+
request: OperationRequest,
|
|
127
|
+
result: OperationResult,
|
|
128
|
+
register: Register | None = None,
|
|
129
|
+
error: Exception | None = None,
|
|
130
|
+
) -> bytes:
|
|
131
|
+
"""Encode an outgoing response frame for ``request``.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
request: Original normalized request that triggered the operation.
|
|
135
|
+
result: Execution outcome used to build ACK/NACK and payload.
|
|
136
|
+
register: Register metadata, when needed to encode result values.
|
|
137
|
+
error: Original execution error, when operation failed.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
bytes: Protocol-specific response frame.
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def is_unknown_operation(request: OperationRequest) -> bool:
|
|
147
|
+
"""Return ``True`` when the request operation is not supported."""
|
|
148
|
+
if isinstance(request, MCBOperationRequest):
|
|
149
|
+
return (
|
|
150
|
+
isinstance(request.operation, MCBOperationType)
|
|
151
|
+
and request.operation is MCBOperationType.UNKNOWN
|
|
152
|
+
)
|
|
153
|
+
return (
|
|
154
|
+
isinstance(request.operation, SDOOperationType)
|
|
155
|
+
and request.operation is SDOOperationType.UNKNOWN
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def is_write_operation(request: OperationRequest) -> bool:
|
|
160
|
+
"""Return ``True`` when the request is a write operation."""
|
|
161
|
+
if isinstance(request, MCBOperationRequest):
|
|
162
|
+
return (
|
|
163
|
+
isinstance(request.operation, MCBOperationType)
|
|
164
|
+
and request.operation is MCBOperationType.WRITE
|
|
165
|
+
)
|
|
166
|
+
return (
|
|
167
|
+
isinstance(request.operation, SDOOperationType)
|
|
168
|
+
and request.operation is SDOOperationType.WRITE
|
|
169
|
+
)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from ingenialink.utils._utils import convert_bytes_to_dtype, convert_dtype_to_bytes
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ingenialink.register import Register
|
|
9
|
+
|
|
10
|
+
from virtual_drive.register_service import RegisterService
|
|
11
|
+
from virtual_drive.register_service.register_value import RegisterValue
|
|
12
|
+
|
|
13
|
+
from ingenialink.virtual.codec import deserialize_sdo_frame, serialize_sdo_frame
|
|
14
|
+
|
|
15
|
+
from virtual_drive.adapters.protocol import (
|
|
16
|
+
OperationRequest,
|
|
17
|
+
OperationResult,
|
|
18
|
+
ProtocolAdapter,
|
|
19
|
+
SDOOperationRequest,
|
|
20
|
+
SDOOperationType,
|
|
21
|
+
)
|
|
22
|
+
from virtual_drive.exceptions import (
|
|
23
|
+
OPERATION_NOT_EXECUTED_ERROR_CODE,
|
|
24
|
+
InvalidCommandError,
|
|
25
|
+
VirtualDriveError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SDOAdapter(ProtocolAdapter):
|
|
30
|
+
"""Adapter for serialized SDO read/write JSON frames."""
|
|
31
|
+
|
|
32
|
+
def parse_request(self, frame: bytes) -> OperationRequest:
|
|
33
|
+
"""Parse a serialized SDO JSON request frame.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
frame: Serialized request frame.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
OperationRequest: Parsed and normalized operation request.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
InvalidCommandError: If the frame is not a valid serialized SDO request
|
|
43
|
+
or if the payload type is invalid.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
request = deserialize_sdo_frame(frame)
|
|
48
|
+
except (UnicodeDecodeError, ValueError, TypeError) as e:
|
|
49
|
+
raise InvalidCommandError("Invalid serialized SDO frame") from e
|
|
50
|
+
|
|
51
|
+
command = request.get("command")
|
|
52
|
+
if command == "read":
|
|
53
|
+
operation = SDOOperationType.READ
|
|
54
|
+
elif command == "write":
|
|
55
|
+
operation = SDOOperationType.WRITE
|
|
56
|
+
else:
|
|
57
|
+
operation = SDOOperationType.UNKNOWN
|
|
58
|
+
|
|
59
|
+
index, has_valid_index = self._parse_object_address_field(request, "index")
|
|
60
|
+
subindex, has_valid_subindex = self._parse_object_address_field(request, "subindex")
|
|
61
|
+
if not (has_valid_index and has_valid_subindex):
|
|
62
|
+
operation = SDOOperationType.UNKNOWN
|
|
63
|
+
|
|
64
|
+
payload: bytes
|
|
65
|
+
raw_data = request.get("data", b"")
|
|
66
|
+
if raw_data is None:
|
|
67
|
+
payload = b""
|
|
68
|
+
elif isinstance(raw_data, bytes):
|
|
69
|
+
payload = raw_data
|
|
70
|
+
elif isinstance(raw_data, bytearray):
|
|
71
|
+
payload = bytes(raw_data)
|
|
72
|
+
else:
|
|
73
|
+
raise InvalidCommandError("SDO write payload must be bytes")
|
|
74
|
+
|
|
75
|
+
return SDOOperationRequest(
|
|
76
|
+
index=index,
|
|
77
|
+
subindex=subindex,
|
|
78
|
+
operation=operation,
|
|
79
|
+
data=payload,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def is_unknown_operation(self, request: OperationRequest) -> bool:
|
|
83
|
+
"""Return whether the request operation is unsupported."""
|
|
84
|
+
sdo_request = self._as_sdo_request(request)
|
|
85
|
+
return sdo_request.operation is SDOOperationType.UNKNOWN
|
|
86
|
+
|
|
87
|
+
def is_write_operation(self, request: OperationRequest) -> bool:
|
|
88
|
+
"""Return whether the request operation writes a register value."""
|
|
89
|
+
sdo_request = self._as_sdo_request(request)
|
|
90
|
+
return sdo_request.operation is SDOOperationType.WRITE
|
|
91
|
+
|
|
92
|
+
def resolve_register(
|
|
93
|
+
self,
|
|
94
|
+
register_service: RegisterService,
|
|
95
|
+
request: OperationRequest,
|
|
96
|
+
) -> Register:
|
|
97
|
+
"""Resolve the target register from an SDO request.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Register: Register identified by index/subindex in the request.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
sdo_request = self._as_sdo_request(request)
|
|
104
|
+
return register_service.get_register_by_index_subindex(
|
|
105
|
+
index=sdo_request.index,
|
|
106
|
+
subindex=sdo_request.subindex,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def decode_write_value(self, register: Register, request: OperationRequest) -> RegisterValue:
|
|
110
|
+
"""Decode the write payload into a register value.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
register: Register metadata used for dtype conversion.
|
|
114
|
+
request: Parsed request containing the write payload bytes.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
RegisterValue: Value converted to the register dtype.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
return convert_bytes_to_dtype(request.data, register.dtype)
|
|
121
|
+
|
|
122
|
+
def build_response(
|
|
123
|
+
self,
|
|
124
|
+
request: OperationRequest,
|
|
125
|
+
result: OperationResult,
|
|
126
|
+
register: Register | None = None,
|
|
127
|
+
error: Exception | None = None,
|
|
128
|
+
) -> bytes:
|
|
129
|
+
"""Build a serialized SDO JSON response frame.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
request: Original parsed operation request.
|
|
133
|
+
result: Operation outcome.
|
|
134
|
+
register: Register metadata used to encode read values.
|
|
135
|
+
error: Original execution error, when operation fails.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
bytes: Serialized response payload.
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
sdo_request = self._as_sdo_request(request)
|
|
142
|
+
response: dict[str, Any] = {
|
|
143
|
+
"command": sdo_request.operation.value,
|
|
144
|
+
"index": sdo_request.index,
|
|
145
|
+
"subindex": sdo_request.subindex,
|
|
146
|
+
}
|
|
147
|
+
if not result.success:
|
|
148
|
+
response["error_code"] = self._resolve_error_code(error)
|
|
149
|
+
return serialize_sdo_frame(response)
|
|
150
|
+
|
|
151
|
+
if sdo_request.operation is SDOOperationType.READ:
|
|
152
|
+
if register is None or result.value is None:
|
|
153
|
+
response["error_code"] = OPERATION_NOT_EXECUTED_ERROR_CODE
|
|
154
|
+
return serialize_sdo_frame(response)
|
|
155
|
+
response["data"] = self._encode_read_value(register, result.value)
|
|
156
|
+
return serialize_sdo_frame(response)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _as_sdo_request(request: OperationRequest) -> SDOOperationRequest:
|
|
160
|
+
if not isinstance(request, SDOOperationRequest):
|
|
161
|
+
raise TypeError("SDOAdapter requires SDOOperationRequest")
|
|
162
|
+
return request
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _parse_object_address_field(request: dict[str, Any], key: str) -> tuple[int, bool]:
|
|
166
|
+
raw_value = request.get(key)
|
|
167
|
+
if raw_value is None:
|
|
168
|
+
return 0, False
|
|
169
|
+
try:
|
|
170
|
+
return int(raw_value), True
|
|
171
|
+
except (TypeError, ValueError):
|
|
172
|
+
return 0, False
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _encode_read_value(register: Register, value: RegisterValue) -> bytes:
|
|
176
|
+
if isinstance(value, bytes):
|
|
177
|
+
return value
|
|
178
|
+
return convert_dtype_to_bytes(value, register.dtype)
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def _resolve_error_code(error: Exception | None) -> int:
|
|
182
|
+
"""Resolve the protocol error code to be embedded in the SDO response.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
error: The original exception that caused the operation to fail, if any.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The protocol-specific error code extracted from the exception, or a default
|
|
189
|
+
code if the error is not a VirtualDriveError or is None.
|
|
190
|
+
|
|
191
|
+
"""
|
|
192
|
+
if isinstance(error, VirtualDriveError):
|
|
193
|
+
return error.error_code
|
|
194
|
+
return OPERATION_NOT_EXECUTED_ERROR_CODE
|