brp-lib 4.0.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.
brp_lib/__init__.py ADDED
@@ -0,0 +1,44 @@
1
+ import sys
2
+ import importlib.resources
3
+ from pathlib import Path
4
+ from typing import Optional, Union
5
+
6
+ from .protocols import BrpStack, UsbHid, RS232, SecureChannel
7
+ from .dll_wrapper import c_brp_lib
8
+ from . import err, protocols
9
+
10
+ __all__ = [
11
+ "c_brp_lib",
12
+ "BrpStack",
13
+ "UsbHid",
14
+ "RS232",
15
+ "SecureChannel",
16
+ "protocols",
17
+ "get_brp_lib_path",
18
+ "set_brp_lib_path",
19
+ ]
20
+
21
+ def get_brp_lib_path() -> Optional[Path]:
22
+ return c_brp_lib.dll_path
23
+
24
+
25
+ def set_brp_lib_path(path: Union[Path, str]) -> None:
26
+ c_brp_lib.dll_path = Path(path)
27
+
28
+
29
+ def _autodetect_brp_lib() -> None:
30
+ arch = 64 if sys.maxsize > 2 ** 32 else 32
31
+ platform = sys.platform.replace("win32", "win")
32
+ extension = {
33
+ "win": "dll"
34
+ }.get(platform, "unknown")
35
+ dll_name = f"brp_lib.{platform}{arch}.{extension}"
36
+
37
+ root_resource = importlib.resources.files("brp_lib")
38
+ dll_resource = root_resource / dll_name
39
+ dll_path = Path(str(dll_resource))
40
+ if dll_path.exists():
41
+ set_brp_lib_path(dll_path)
42
+
43
+
44
+ _autodetect_brp_lib()
Binary file
Binary file
brp_lib/dll_wrapper.py ADDED
@@ -0,0 +1,253 @@
1
+ import ctypes
2
+ import typing
3
+ import functools
4
+ from contextlib import suppress
5
+ from pathlib import Path
6
+ from typing import Any, Protocol, TypeVar, Callable, Optional
7
+ from typing_extensions import Annotated, ParamSpec, Never
8
+
9
+ from .err import BrpError
10
+
11
+
12
+ buf = ctypes.POINTER(ctypes.c_char)
13
+ time = ctypes.c_ulong
14
+ errcode = ctypes.c_uint
15
+ layer_id = ctypes.c_int
16
+ socket = ctypes.c_int
17
+
18
+
19
+ class protocol_t(ctypes.Structure):
20
+ pass
21
+
22
+
23
+ protocol = ctypes.POINTER(protocol_t)
24
+
25
+
26
+ class frame_t(ctypes.Structure):
27
+ _fields_ = [
28
+ ("ptr", buf), # type is brp_buf, but in python
29
+ # buf is defined in the scope of
30
+ # this class definition...
31
+ ("act_size", ctypes.c_size_t),
32
+ ("total_size", ctypes.c_size_t),
33
+ ] # last read/write of frame failed
34
+
35
+
36
+ frame = ctypes.POINTER(frame_t)
37
+
38
+
39
+ class frame_reader_t(ctypes.Structure):
40
+ _fields_ = [("frame", frame), ("ptr", buf), ("err", ctypes.c_bool)]
41
+
42
+
43
+ frame_reader = ctypes.POINTER(frame_reader_t)
44
+
45
+
46
+ class mempool_object_t(ctypes.Structure):
47
+ pass
48
+
49
+
50
+ mempool = ctypes.POINTER(mempool_object_t)
51
+
52
+ mempool_object_t._fields_ = [
53
+ ("prev", ctypes.POINTER(mempool_object_t)),
54
+ ("next", ctypes.POINTER(mempool_object_t)),
55
+ ("buf", ctypes.c_char_p),
56
+ ]
57
+
58
+
59
+ class ioiter_t(ctypes.Structure):
60
+ pass
61
+
62
+
63
+ ioiter = ctypes.POINTER(ioiter_t)
64
+ ioiter_p = ctypes.POINTER(ioiter)
65
+
66
+
67
+ cb_generic_t = ctypes.CFUNCTYPE(errcode, protocol)
68
+ cb_recv_any_frame_t = ctypes.CFUNCTYPE(errcode, protocol, time)
69
+ cb_fmt_spec_t = ctypes.CFUNCTYPE(ctypes.c_int, frame, ctypes.c_void_p)
70
+ cb_recv_frame_t = ctypes.CFUNCTYPE(
71
+ errcode, protocol, time, cb_fmt_spec_t, ctypes.c_void_p
72
+ )
73
+ cb_get_id_t = ctypes.CFUNCTYPE(
74
+ errcode, protocol, ctypes.POINTER(ctypes.c_char_p), frame
75
+ )
76
+
77
+ protocol_t._fields_ = [
78
+ ("protocol_id", ctypes.c_int),
79
+ ("layer_id", layer_id),
80
+ ("base_protocol", protocol),
81
+ ("cb_open", cb_generic_t),
82
+ ("cb_close", cb_generic_t),
83
+ ("cb_send_frame", cb_generic_t),
84
+ ("cb_recv_any_frame", cb_recv_any_frame_t),
85
+ ("cb_recv_frame", cb_recv_frame_t),
86
+ ("cb_flush", cb_generic_t),
87
+ ("cb_destroy", cb_generic_t),
88
+ ("cb_get_id", cb_get_id_t),
89
+ ("opened", ctypes.c_bool),
90
+ ("send_frame", frame_t),
91
+ ("recv_frame", frame_t),
92
+ ("recv_delay", time),
93
+ ("mempool", mempool_object_t),
94
+ ("friendly_name", ctypes.c_char_p),
95
+ ]
96
+
97
+
98
+ R = TypeVar("R", covariant=True)
99
+ P = ParamSpec("P")
100
+ C = TypeVar("C", bound=type)
101
+
102
+
103
+ class CBrpLibFunction(Protocol[P, R]):
104
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
105
+ ...
106
+
107
+
108
+ CharP = Annotated[ctypes.Array[ctypes.c_char], ctypes.c_char_p]
109
+ CharPP = Annotated[Any, ctypes.POINTER(ctypes.c_char_p)]
110
+ CInt = Annotated[int, ctypes.c_int]
111
+ CSize = Annotated[int, ctypes.c_size_t]
112
+ CSizeP = Annotated[Any, ctypes.POINTER(ctypes.c_size_t)]
113
+ CUShortP = Annotated[Any, ctypes.POINTER(ctypes.c_ushort)]
114
+ CChar = Annotated[bytes, ctypes.c_char]
115
+ CBool = Annotated[bool, ctypes.c_bool]
116
+ CBuf = Annotated[bytes, buf]
117
+ CLongLong = Annotated[int, ctypes.c_longlong]
118
+ CTime = Annotated[int, ctypes.c_ulong]
119
+ CProtocol = Annotated[Any, protocol]
120
+ CProtocolP = Annotated[Any, ctypes.POINTER(protocol)]
121
+ CFrame = Annotated[Any, frame]
122
+ CFrameReader = Annotated[Any, frame_reader]
123
+ CSocket = Annotated[int, socket]
124
+ CMempool = Annotated[Any, mempool]
125
+ CIoiter = Annotated[Any, ioiter]
126
+ CIoiterP = Annotated[Any, ioiter_p]
127
+ CUShort = Annotated[int, ctypes.c_ushort]
128
+
129
+ ErrorCode = Annotated[None, errcode]
130
+
131
+
132
+ def _to_ctype(t: Any) -> Any:
133
+ if t is type(None):
134
+ return None
135
+
136
+ args = typing.get_args(t)
137
+ if args:
138
+ return args[1]
139
+
140
+ return t
141
+
142
+
143
+ def _get_log_link(prot: Any) -> Optional[str]:
144
+ path_ptr = c_brp_lib.brp_get_log_path(prot, None)
145
+ if not path_ptr:
146
+ return None
147
+
148
+ path = Path(ctypes.string_at(path_ptr).decode('utf-8'))
149
+ if not path.exists():
150
+ return None
151
+ url = f"{path.as_uri()}"
152
+
153
+ anchor_ptr = ctypes.c_char_p()
154
+ c_brp_lib.brp_get_current_log_anchor(prot, ctypes.byref(anchor_ptr), None)
155
+ if anchor_ptr.value:
156
+ return f"{url}{anchor_ptr.value.decode('utf-8')}"
157
+ return url
158
+
159
+
160
+ def with_error_handling(fn: Callable[P, int]) -> Callable[P, None]:
161
+ @functools.wraps(fn)
162
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
163
+ result = fn(*args, **kwargs)
164
+ if result:
165
+ log_url = None
166
+ with suppress():
167
+ if len(args) > 0 and isinstance(args[0], protocol):
168
+ log_url = _get_log_link(args[0])
169
+ raise BrpError.from_error_code(result, log_url=log_url)
170
+
171
+ return wrapper
172
+
173
+
174
+ def _not_implemented(name: str, version: str, *args: object, **kwargs: object) -> Never:
175
+ raise NotImplementedError(f"{name} is not implemented in brp_lib {version}")
176
+
177
+
178
+ class CBrpLib:
179
+ brp_get_version: CBrpLibFunction[[], CharP]
180
+ brp_create: CBrpLibFunction[[], CProtocol]
181
+ brp_destroy: CBrpLibFunction[[CProtocol], ErrorCode]
182
+ brp_set_io: CBrpLibFunction[[CProtocol, CProtocol], ErrorCode]
183
+ brp_open: CBrpLibFunction[[CProtocol], ErrorCode]
184
+ brp_close: CBrpLibFunction[[CProtocol], ErrorCode]
185
+ brp_suppress_monitoring: CBrpLibFunction[[CProtocol], ErrorCode]
186
+ brp_get_io_id: CBrpLibFunction[[CProtocol, CharPP, CFrame], ErrorCode]
187
+ brp_get_friendly_name: CBrpLibFunction[[CProtocol, CharPP], ErrorCode]
188
+ brp_ioiter_create: CBrpLibFunction[[], CIoiter]
189
+ brp_ioiter_create_usb_hid: CBrpLibFunction[[], CIoiter]
190
+ brp_ioiter_next: CBrpLibFunction[[CIoiter, CProtocolP], ErrorCode]
191
+ brp_ioiter_destroy: CBrpLibFunction[[CIoiterP], None]
192
+ brp_create_usb_hid: CBrpLibFunction[[CLongLong], CProtocol]
193
+ brp_create_rs232: CBrpLibFunction[[CharP, CInt, CChar], CProtocol]
194
+ brp_create_secure_channel: CBrpLibFunction[[CInt, CBuf, CInt], CProtocol]
195
+ brp_set_crypto: CBrpLibFunction[[CProtocol, CProtocol], ErrorCode]
196
+ brp_set_monitor: CBrpLibFunction[[CProtocol, CInt], ErrorCode]
197
+ brp_send_frame: CBrpLibFunction[[CProtocol], ErrorCode]
198
+ brp_recv_any_frame: CBrpLibFunction[[CProtocol, CInt], ErrorCode]
199
+ brp_exec_cmd: CBrpLibFunction[[CProtocol, CInt, CBuf, CSize, CBuf, CSize, CSizeP, CTime], ErrorCode]
200
+ brp_frame_rest: CBrpLibFunction[[CFrameReader], CSize]
201
+ brp_frame_init: CBrpLibFunction[[CFrame], None]
202
+ brp_frame_deinit: CBrpLibFunction[[CFrame], None]
203
+ brp_frame_write_start: CBrpLibFunction[[CFrame], None]
204
+ brp_frame_write: CBrpLibFunction[[CFrame, CBuf, CSize], None]
205
+ brp_frame_write_err: CBrpLibFunction[[CFrame], CBool]
206
+ brp_frame_read_start: CBrpLibFunction[[CFrameReader, CFrame], None]
207
+ brp_frame_read: CBrpLibFunction[[CFrameReader, CBuf, CSize], None]
208
+ brp_frame_read_err: CBrpLibFunction[[CFrameReader], CBool]
209
+ brp_frame_read_eof: CBrpLibFunction[[CFrameReader], CBool]
210
+ brp_annotation_start: CBrpLibFunction[[CProtocol], None]
211
+ brp_annotation_end: CBrpLibFunction[[CProtocol, CBool, CharP], None]
212
+ brp_set_log_path: CBrpLibFunction[[CProtocol, CharP], ErrorCode]
213
+ brp_get_current_log_anchor: CBrpLibFunction[[CProtocol, CharPP, CMempool], ErrorCode]
214
+ brp_get_log_path: CBrpLibFunction[[CProtocol, CMempool], CharP]
215
+
216
+ def __init__(self) -> None:
217
+ self.__dll_path: Optional[Path] = None
218
+
219
+ @property
220
+ def dll_path(self) -> Optional[Path]:
221
+ return self.__dll_path
222
+
223
+ @dll_path.setter
224
+ def dll_path(self, dll_path: Path) -> None:
225
+ self.__dll_path = dll_path.absolute()
226
+ dll = ctypes.CDLL(str(self.__dll_path))
227
+
228
+ for name, annotation in type(self).__annotations__.items():
229
+ if typing.get_origin(annotation) == CBrpLibFunction:
230
+ argtypes, restype = typing.get_args(annotation)
231
+ fn = getattr(dll, name, None)
232
+ if fn is None:
233
+ fn = functools.partial(
234
+ _not_implemented,
235
+ name=name,
236
+ version=bytes(self.brp_get_version()).decode("ascii")
237
+ if name != "brp_get_version"
238
+ else "<unknown>"
239
+ )
240
+ else:
241
+ fn.restype = _to_ctype(restype)
242
+ fn.argtypes = tuple(map(_to_ctype, argtypes))
243
+ if restype == ErrorCode:
244
+ fn = with_error_handling(fn)
245
+ setattr(self, name, fn)
246
+
247
+ def __getattr__(self, item: str) -> Any:
248
+ if item in type(self).__annotations__:
249
+ raise RuntimeError("Please initialize the brp_lib path calling `brp_lib.set_brp_lib_path('.../libbrp_lib.so')`.")
250
+ return super().__getattribute__(item)
251
+
252
+
253
+ c_brp_lib = CBrpLib()
brp_lib/err.py ADDED
@@ -0,0 +1,328 @@
1
+ import abc
2
+ from typing import Dict, Type, Optional
3
+
4
+ __all__ = [
5
+ "BrpError",
6
+ "DeviceError",
7
+ "CommunicationError",
8
+ "CommUnsupported",
9
+ "CommAccessDeniedError",
10
+ "CommTimeoutError",
11
+ "CommFrameFormatError",
12
+ "CommUndefined",
13
+ "LibError",
14
+ "LibInvalidCallError",
15
+ "LibNonRecoverableError",
16
+ "LibOsError",
17
+ "LibUndefinedError",
18
+ "InternalError",
19
+ "CommandTimeoutError",
20
+ "FrameFormatError",
21
+ "InvalidApiCallError",
22
+ "OutOfMemoryError",
23
+ "BrpNotImplementedError",
24
+ "BusyError",
25
+ "ClosedError",
26
+ "BufferOverflowError",
27
+ "OpenIOError",
28
+ "WriteIOError",
29
+ "WaitIOError",
30
+ "ReadIOError",
31
+ "CloseIOError",
32
+ "PayloadFormatError",
33
+ "CryptoFormatError",
34
+ "SecurityLevelNotSupportedError",
35
+ "UnsupportedCommandError",
36
+ "CommandDeniedError",
37
+ "UnexpectedFrameError",
38
+ "BrpTimeoutError",
39
+ "CalledInvalidFrameError",
40
+ "ExistingLayerError",
41
+ "GenRandomDataError",
42
+ "InvalidKeyError",
43
+ "NoDeviceFoundError",
44
+ "AmbiguousDeviceError",
45
+ "PayloadTooLongError",
46
+ "PayloadTooShortError",
47
+ ]
48
+
49
+ class BrpError(Exception, metaclass=abc.ABCMeta):
50
+ ErrorCodeMap: Dict[int, Type["BrpError"]] = {}
51
+
52
+ ErrorCode: Optional[int] = None
53
+ URL: Optional[str] = None
54
+
55
+ def __init_subclass__(cls) -> None:
56
+ if cls.ErrorCode is not None:
57
+ already_registered_class = cls.ErrorCodeMap.get(cls.ErrorCode)
58
+ if already_registered_class is not None:
59
+ if issubclass(cls, already_registered_class):
60
+ return
61
+ raise TypeError(f"multiple exceptions defined for same status code {hex(cls.ErrorCode)} ({cls} and {already_registered_class})")
62
+ cls.ErrorCodeMap[cls.ErrorCode] = cls
63
+
64
+ @classmethod
65
+ def from_error_code(cls, ec: int, *, log_url: Optional[str] = None) -> "BrpError":
66
+ error_class: Type["BrpError"] = cls.ErrorCodeMap.get(ec, cls)
67
+ return error_class(log_url=log_url)
68
+
69
+ def __init__(self, message: str = "", *, log_url: Optional[str] = None) -> None:
70
+ self.log_url = log_url
71
+ super().__init__(message)
72
+
73
+ def __str__(self) -> str:
74
+ return "\n".join(
75
+ map(str,
76
+ filter(
77
+ lambda i: i is not None, [
78
+ f"0x{self.ErrorCode:08X} {super()!s}\n",
79
+ f"DESC: {self.__doc__.strip() if self.__doc__ else None}",
80
+ f"DOCS: {self.URL}" if self.URL else None,
81
+ f"LOGS: {self.log_url}" if self.log_url else None,
82
+ ]
83
+ )
84
+ )
85
+ )
86
+
87
+
88
+ class CommunicationError(BrpError, metaclass=abc.ABCMeta):
89
+ """Any kind of device <-> host communication problem"""
90
+
91
+
92
+ class CommUnsupported(CommunicationError, metaclass=abc.ABCMeta):
93
+ """A command cannot be executed as the device does not support the requested feature"""
94
+
95
+
96
+ class CommAccessDeniedError(CommunicationError, metaclass=abc.ABCMeta):
97
+ """A command cannot be executed as the device does not allow access this features in the current operations mode"""
98
+
99
+
100
+ class CommTimeoutError(CommunicationError, metaclass=abc.ABCMeta):
101
+ """A the device or host exceeded the allowed time for sending a frame"""
102
+
103
+
104
+ class CommFrameFormatError(CommunicationError, metaclass=abc.ABCMeta):
105
+ """The device or host send a frame that does not correspond the expected frame"""
106
+
107
+
108
+ class CommUndefined(CommunicationError, metaclass=abc.ABCMeta):
109
+ """any device <-> host communication problem which is not categoriezed by one of the BRP_ERRGRP_COMM_* macros"""
110
+
111
+
112
+ class LibError(BrpError, metaclass=abc.ABCMeta):
113
+ """Any error which occured in the library"""
114
+
115
+
116
+ class LibInvalidCallError(LibError, metaclass=abc.ABCMeta):
117
+ """The libraries API was used in an invalid manner"""
118
+
119
+
120
+ class LibNonRecoverableError(LibError, metaclass=abc.ABCMeta):
121
+ """A fatal error occured, which the library cannot recover from. The application should be closed"""
122
+
123
+
124
+ class LibOsError(LibError, metaclass=abc.ABCMeta):
125
+ """A error in the operating system layer (i.e. hardware drivers) occured"""
126
+
127
+
128
+ class LibUndefinedError(LibError, metaclass=abc.ABCMeta):
129
+ """any library specific error which is not one of the BRP_ERRGRP_LIB_* macros"""
130
+
131
+
132
+ class DeviceError(BrpError, metaclass=abc.ABCMeta):
133
+ """
134
+ A command failed to execute due to a error on the device side which is
135
+ not host communication related (card error, ...)
136
+
137
+ The lower 16bits of the error code will contains the command group code and
138
+ the status code (see also #BRP_ERR_STATUS() on how to construct a specific
139
+ error code).
140
+ The exact list of error codes returned by the device can be found
141
+ in the @ref baltech_sdk.
142
+ """
143
+
144
+
145
+ class InternalError(LibNonRecoverableError):
146
+ """This error must not be returned if the library operates correctly.
147
+ It is only returned, if a bug in the implementation was detected."""
148
+ ErrorCode = 0x20000001
149
+
150
+
151
+ class CommandTimeoutError(CommTimeoutError):
152
+ """A BRP command that was send by brp_send_frame() exceeded the specified
153
+ timeout (see also brp_send_cmd() / brp_exec_cmd()).
154
+
155
+ **Attention:** must not be intermixed with #BRP_ERR_TIMEOUT."""
156
+ ErrorCode = 0x02000002
157
+
158
+
159
+ class FrameFormatError(CommFrameFormatError):
160
+ """The frame returned from the reader does not match the format expected
161
+ by the BRP protocol."""
162
+ ErrorCode = 0x04000003
163
+
164
+
165
+ class InvalidApiCallError(LibInvalidCallError):
166
+ """Returned if the a parameter contains an invalid value.
167
+ I.e. if a required pointer is set to NULL."""
168
+ ErrorCode = 0x10000004
169
+
170
+
171
+ class OutOfMemoryError(LibNonRecoverableError):
172
+ """The heap has not enough memory to allocate a buffer that is required
173
+ for the wanted operation."""
174
+ ErrorCode = 0x20000005
175
+
176
+
177
+ class BrpNotImplementedError(LibInvalidCallError):
178
+ """The requested feature is not implemented yet"""
179
+ ErrorCode = 0x10000006
180
+
181
+
182
+ class BusyError(LibInvalidCallError):
183
+ """The Library is not ready to send/receive data. Probably it is still
184
+ processing another request."""
185
+ ErrorCode = 0x10000007
186
+
187
+
188
+ class ClosedError(LibInvalidCallError):
189
+ """The protocol/device is not opened"""
190
+ ErrorCode = 0x10000008
191
+
192
+
193
+ class BufferOverflowError(CommFrameFormatError):
194
+ """A Command's response is bigger than the buffer provided for the response."""
195
+ ErrorCode = 0x04000009
196
+
197
+
198
+ class OpenIOError(LibOsError):
199
+ """Failed to open the I/O connection"""
200
+ ErrorCode = 0x4000000A
201
+
202
+
203
+ class WriteIOError(LibOsError):
204
+ """Failed to send data via IO connection"""
205
+ ErrorCode = 0x4000000B
206
+
207
+
208
+ class WaitIOError(LibOsError):
209
+ """Failed to wait for data via IO connection"""
210
+ ErrorCode = 0x4000000C
211
+
212
+
213
+ class ReadIOError(LibOsError):
214
+ """Failed to wait for data via IO connection"""
215
+ ErrorCode = 0x4000000D
216
+
217
+
218
+ class CloseIOError(LibOsError):
219
+ """Failed to open the I/O connection"""
220
+ ErrorCode = 0x4000000E
221
+
222
+
223
+ class PayloadFormatError(CommFrameFormatError):
224
+ """The payload of a frame has invalid format"""
225
+ ErrorCode = 0x0400000F
226
+
227
+
228
+ class CryptoFormatError(CommFrameFormatError):
229
+ """The payload of an encrypted command had invalid format/response."""
230
+ ErrorCode = 0x04000010
231
+
232
+
233
+ class SecurityLevelNotSupportedError(CommAccessDeniedError):
234
+ """The given securitylevel is not supported"""
235
+ ErrorCode = 0x01000016
236
+
237
+
238
+ class UnsupportedCommandError(CommUnsupported):
239
+ """The specified BRP command is not supported by the connected device
240
+
241
+ Internally the driver maps the status code BRP_ERR_STATUS(xxxx, 0x41) to this
242
+ (cmdcode independant) error code."""
243
+ ErrorCode = 0x00800020
244
+
245
+
246
+ class CommandDeniedError(CommAccessDeniedError):
247
+ """The BRP command is not allowed to be executed from the current security level
248
+
249
+ Internally the driver maps the status code BRP_ERR_STATUS(xxxx, 0x42) to this
250
+ (cmdcode independant) error code."""
251
+ ErrorCode = 0x01000021
252
+
253
+
254
+ class UnexpectedFrameError(CommFrameFormatError):
255
+ """The received frame is a valid BRP frame but not expected in this protocol
256
+ state."""
257
+ ErrorCode = 0x04000022
258
+
259
+
260
+ class BrpTimeoutError(CommTimeoutError):
261
+ """The response to a brp_recv_frame() / brp_recv_any_frame() is not returned
262
+ within the specified timeout.
263
+
264
+ **Attention:** Must not be intermixed with #BRP_ERR_CMD_TIMEOUT, which is
265
+ returned if the timeout of a BRP command is exceeded (see brp_send_cmd())."""
266
+ ErrorCode = 0x02000023
267
+
268
+
269
+ class CalledInvalidFrameError(LibInvalidCallError):
270
+ """Returned if the a frame passed to a function to be transferred to the device
271
+ has invalid format"""
272
+ ErrorCode = 0x10000024
273
+
274
+
275
+ class ExistingLayerError(LibInvalidCallError):
276
+ """The layer is tried to be added to a composite protocol
277
+ (see brp_add_layer() / brp_set_layer()) although already added earlier.
278
+ Or it is tried to be removed (brp_detach_layer()) also not part of the
279
+ composite protocol."""
280
+ ErrorCode = 0x10000025
281
+
282
+
283
+ class GenRandomDataError(LibOsError):
284
+ """The OS failed to generate a random number"""
285
+ ErrorCode = 0x40000026
286
+
287
+
288
+ class InvalidKeyError(CommAccessDeniedError):
289
+ """The readers KEY does not match the key of the host."""
290
+ ErrorCode = 0x01000027
291
+
292
+
293
+ class NoDeviceFoundError(OpenIOError):
294
+ """No device was found when trying to open a connection."""
295
+ ErrorCode = 0x40000028
296
+
297
+
298
+ class AmbiguousDeviceError(OpenIOError):
299
+ """Multiple devices found but connection string does not uniquely identify one."""
300
+ ErrorCode = 0x40000029
301
+
302
+
303
+ ##############################################################################
304
+ # PYTHON ONLY EXCEPTIONS #
305
+ ##############################################################################
306
+
307
+ class PayloadTooLongError(PayloadFormatError):
308
+ """Raised when payload contains more data than expected."""
309
+
310
+ def __init__(self, additional_data: bytes) -> None:
311
+ super().__init__()
312
+ self.additional_data = additional_data
313
+
314
+ def __str__(self) -> str:
315
+ return f'{len(self.additional_data)} bytes of unexpected data'
316
+
317
+
318
+ class PayloadTooShortError(PayloadFormatError):
319
+ """Raised when payload contains less data than expected."""
320
+
321
+ def __init__(self, missing_bytes: int) -> None:
322
+ super().__init__()
323
+ self.missing_bytes = missing_bytes
324
+
325
+ def __str__(self) -> str:
326
+ if self.missing_bytes == 1:
327
+ return "1 byte is missing"
328
+ return f'{self.missing_bytes} bytes are missing'
brp_lib/protocols.py ADDED
@@ -0,0 +1,355 @@
1
+ import abc
2
+ import ctypes
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, Optional, TypeVar, Literal, Type, Union, Iterator
6
+ from contextlib import contextmanager
7
+
8
+ from .dll_wrapper import c_brp_lib, buf, frame_reader_t, frame_t, protocol
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ class CProtocolWrapper(metaclass=abc.ABCMeta):
14
+ def __init__(self, c_protocol: Any) -> None:
15
+ self._c_protocol = c_protocol
16
+
17
+ @property
18
+ def __c_protocol__(self) -> Any:
19
+ if self._c_protocol is None:
20
+ raise RuntimeError("Protocol ownership has been transferred to a parent protocol")
21
+ return self._c_protocol
22
+
23
+ def __take_c_protocol__(self) -> Any:
24
+ if self._c_protocol is None:
25
+ raise RuntimeError("Protocol ownership has already been transferred")
26
+ c_protocol = self._c_protocol
27
+ self._c_protocol = None
28
+ return c_protocol
29
+
30
+ @property
31
+ def closed(self) -> bool:
32
+ return not self.__c_protocol__.contents.opened
33
+
34
+ def open(self) -> None:
35
+ c_brp_lib.brp_open(self.__c_protocol__)
36
+
37
+ def close(self) -> None:
38
+ c_brp_lib.brp_close(self.__c_protocol__)
39
+
40
+ def __enter__(self) -> Any:
41
+ if self.closed:
42
+ self.open()
43
+ return self
44
+
45
+ def __exit__(self, exc_type: Type[Exception], exc_val: Exception, exc_tb: Any) -> None:
46
+ if not self.closed:
47
+ self.close()
48
+
49
+ def __del__(self) -> None:
50
+ # Check if _c_protocol exists and is not None (ownership not transferred)
51
+ if hasattr(self, '_c_protocol') and self._c_protocol is not None:
52
+ if not self.closed:
53
+ self.close()
54
+ c_brp_lib.brp_destroy(self._c_protocol)
55
+
56
+ @property
57
+ def id(self) -> str:
58
+ """Device ID (e.g., serial number for USB HID devices)."""
59
+ intf_name_ptr = ctypes.c_char_p()
60
+ device_id_frame = frame_t()
61
+ c_brp_lib.brp_frame_init(ctypes.pointer(device_id_frame))
62
+ try:
63
+ c_brp_lib.brp_get_io_id(self.__c_protocol__, ctypes.byref(intf_name_ptr), ctypes.pointer(device_id_frame))
64
+ if device_id_frame.ptr:
65
+ return ctypes.string_at(device_id_frame.ptr, device_id_frame.act_size).decode('utf-8', errors='replace').rstrip('\x00')
66
+ return ""
67
+ finally:
68
+ c_brp_lib.brp_frame_deinit(ctypes.pointer(device_id_frame))
69
+
70
+ @property
71
+ def friendly_name(self) -> Optional[str]:
72
+ """Human-readable device name (e.g., product name for USB HID devices)."""
73
+ friendly_name_ptr = ctypes.c_char_p()
74
+ c_brp_lib.brp_get_friendly_name(self.__c_protocol__, ctypes.byref(friendly_name_ptr))
75
+ if friendly_name_ptr.value:
76
+ return friendly_name_ptr.value.decode('utf-8', errors='replace')
77
+ return None
78
+
79
+ def __str__(self) -> str:
80
+ """Return a string representation of the protocol."""
81
+ name = self.friendly_name
82
+ if name:
83
+ return f"{name} {self.id} ({self.__class__.__name__})"
84
+ return f"{self.id} ({self.__class__.__name__})"
85
+
86
+
87
+ class IoProtocol(CProtocolWrapper, metaclass=abc.ABCMeta):
88
+ pass
89
+
90
+
91
+ class UsbHid(IoProtocol):
92
+ def __init__(self, serialnumber: Optional[int] = None) -> None:
93
+ self.serialnumber = serialnumber
94
+ super().__init__(c_brp_lib.brp_create_usb_hid(serialnumber or 0))
95
+
96
+ @classmethod
97
+ def enumerate(cls) -> Iterator['UsbHid']:
98
+ """
99
+ Enumerate all connected Baltech USB HID devices.
100
+
101
+ Yields UsbHid instances for each connected device. The caller is responsible
102
+ for managing the lifecycle of yielded devices (e.g., using context managers).
103
+
104
+ Example:
105
+ >>> for device in UsbHid.enumerate():
106
+ ... with device:
107
+ ... print(f"Device: {device.friendly_name or device.id}")
108
+ ... # Use device for communication
109
+ """
110
+ # Create USB HID-specific iterator
111
+ iter_handle = c_brp_lib.brp_ioiter_create_usb_hid()
112
+ if not iter_handle:
113
+ return
114
+
115
+ try:
116
+ while True:
117
+ # Get next protocol
118
+ protocol_ptr = protocol()
119
+ c_brp_lib.brp_ioiter_next(iter_handle, ctypes.byref(protocol_ptr))
120
+
121
+ # Check if we reached the end
122
+ if not protocol_ptr:
123
+ break
124
+
125
+ # Wrap the protocol from iterator in a UsbHid instance
126
+ # The protocol already has friendly_name populated from the iterator
127
+ device = cls.__new__(cls)
128
+ device._c_protocol = protocol_ptr
129
+ yield device
130
+ finally:
131
+ # Clean up iterator
132
+ iter_ref = ctypes.pointer(iter_handle)
133
+ c_brp_lib.brp_ioiter_destroy(iter_ref)
134
+
135
+
136
+ class RS232(IoProtocol):
137
+ def __init__(self, comport: str, baudrate: int = 115200, parity: Literal[b"O", b"E", b"N"] = b"N") -> None:
138
+ self.comport = comport
139
+ comport_cstr = ctypes.create_string_buffer(comport.encode("ascii"))
140
+ super().__init__(c_brp_lib.brp_create_rs232(comport_cstr, baudrate, parity))
141
+
142
+
143
+ class CryptoProtocol(CProtocolWrapper, metaclass=abc.ABCMeta):
144
+ pass
145
+
146
+
147
+ class SecureChannel(CryptoProtocol):
148
+ def __init__(self, *, security_level: int, key: bytes, security_mode: Literal["std", "plain", "stateless"] = "std") -> None:
149
+ secmode = dict(std=0, plain=1, stateless=2).get(security_mode, 0)
150
+ super().__init__(c_brp_lib.brp_create_secure_channel(security_level, key, secmode))
151
+
152
+
153
+ MonitorMode = Union[Literal["disabled", "enabled", "plaintext"], bool]
154
+
155
+
156
+ class AnnotationLogger:
157
+
158
+ def __init__(self) -> None:
159
+ self._message = ""
160
+ self._fail = False
161
+
162
+ def info(self, message: str) -> None:
163
+ self._message += message
164
+
165
+ def fail(self, message: str = "") -> None:
166
+ self._fail = True
167
+ self._message += message
168
+
169
+
170
+ class BrpStack(CProtocolWrapper):
171
+ def __init__(
172
+ self,
173
+ io: IoProtocol,
174
+ *,
175
+ crypto: Optional[CryptoProtocol] = None,
176
+ monitor: MonitorMode = "enabled",
177
+ log_path: Optional[Union[str, Path]] = None,
178
+ open: bool = False,
179
+ ) -> None:
180
+ super().__init__(c_brp_lib.brp_create())
181
+
182
+ c_brp_lib.brp_set_io(self.__c_protocol__, io.__take_c_protocol__())
183
+
184
+ if crypto:
185
+ c_brp_lib.brp_set_crypto(self.__c_protocol__, crypto.__take_c_protocol__())
186
+
187
+ if monitor not in (True, "enabled"):
188
+ self.monitor = monitor
189
+
190
+ if log_path is not None:
191
+ self.log_path = Path(log_path)
192
+
193
+ if open:
194
+ self.open()
195
+
196
+ def send_frame(self, frame: bytes) -> None:
197
+ send_frame = ctypes.pointer(self.__c_protocol__.contents.send_frame)
198
+
199
+ c_brp_lib.brp_frame_write_start(send_frame)
200
+ c_brp_lib.brp_frame_write(send_frame, frame, len(frame))
201
+ c_brp_lib.brp_frame_write_err(send_frame)
202
+ c_brp_lib.brp_send_frame(self.__c_protocol__)
203
+
204
+ def recv_any_frame(self, timeout: int = 0xFFFFFFFF) -> bytes:
205
+ reader = frame_reader_t()
206
+ c_brp_lib.brp_recv_any_frame(self.__c_protocol__, timeout)
207
+ c_brp_lib.brp_frame_read_start(
208
+ ctypes.pointer(reader), ctypes.pointer(self.__c_protocol__.contents.recv_frame)
209
+ )
210
+ rest = c_brp_lib.brp_frame_rest(ctypes.pointer(reader))
211
+
212
+ result = ctypes.create_string_buffer(rest)
213
+ c_brp_lib.brp_frame_read(ctypes.pointer(reader), buf(result), rest) # type: ignore[call-overload]
214
+ c_brp_lib.brp_frame_read_err(ctypes.pointer(reader))
215
+
216
+ eof = c_brp_lib.brp_frame_read_eof(ctypes.pointer(reader))
217
+ if not eof:
218
+ raise RuntimeError("UnexpectedData")
219
+
220
+ return result.raw
221
+
222
+ def process_frame(self, frame: bytes) -> bytes:
223
+ self.send_frame(frame)
224
+ return self.recv_any_frame()
225
+
226
+ def exec_cmd(self, cmd_code: int, params: bytes, *, timeout: int, max_resp_len: int = 65536) -> bytes:
227
+ resp_buf = ctypes.create_string_buffer(max_resp_len)
228
+ resp_len = ctypes.c_size_t()
229
+
230
+ c_brp_lib.brp_exec_cmd(
231
+ self.__c_protocol__,
232
+ cmd_code,
233
+ params,
234
+ len(params),
235
+ buf(resp_buf), # type: ignore[call-overload]
236
+ max_resp_len,
237
+ ctypes.pointer(resp_len),
238
+ timeout
239
+ )
240
+
241
+ return resp_buf.raw[:resp_len.value]
242
+
243
+ def set_monitor(self, new_val: MonitorMode) -> None:
244
+ map = {"disabled": 0, "enabled": 1, "plaintext": 2, False:0, True:1}
245
+ c_brp_lib.brp_set_monitor(self.__c_protocol__, map[new_val])
246
+
247
+ @contextmanager
248
+ def annotate_log(self, comment:str="") -> Iterator[AnnotationLogger]:
249
+ """
250
+ Adds an annotations/comment to all brplogs that are executed within
251
+ this context.
252
+
253
+ Returns a contextmanager that allows to add further log-messages
254
+ (log.info(...) or log.fail(...)). if at least one failure message is
255
+ send the whole block is marked as failed.
256
+
257
+ If an exception occurs the block will be marked as failed, too.
258
+ If no log.fail() was called the exception text will be added to
259
+ the log annotation.
260
+
261
+ :param comment: A string that shall be added as annotation to the logs
262
+ :return: a contextmanager of type AnnotationLogger()
263
+ """
264
+ annotation_logger = AnnotationLogger()
265
+ annotation_logger.info(comment)
266
+ try:
267
+ c_brp_lib.brp_annotation_start(self.__c_protocol__)
268
+ yield annotation_logger
269
+ except Exception as exc:
270
+ if not annotation_logger._fail:
271
+ exc_text = f"{type(exc).__name__}: {str(exc)}"
272
+ annotation_logger.fail(exc_text.splitlines()[0])
273
+ raise
274
+ finally:
275
+ message_utf8 = annotation_logger._message.encode('utf8')
276
+ message_cstr = ctypes.create_string_buffer(message_utf8)
277
+ fail = annotation_logger._fail
278
+ c_brp_lib.brp_annotation_end(
279
+ self.__c_protocol__, not fail, message_cstr)
280
+
281
+ monitor = property(fset=set_monitor)
282
+
283
+ @property
284
+ def current_log_anchor(self) -> Optional[str]:
285
+ """
286
+ Get the anchor ID of the current (last written) log entry.
287
+
288
+ The anchor can be used to create direct links to specific log entries,
289
+ e.g., ``f"file://{log_path}#{brp.current_log_anchor}"``
290
+
291
+ :return: The anchor string (e.g., "_a3") in hex format, or None
292
+ if no log entries have been written yet.
293
+ """
294
+ anchor_ptr = ctypes.c_char_p()
295
+ c_brp_lib.brp_get_current_log_anchor(self.__c_protocol__, ctypes.byref(anchor_ptr), None)
296
+ if anchor_ptr.value:
297
+ return anchor_ptr.value.decode("ascii")
298
+ return None
299
+
300
+ @property
301
+ def log_path(self) -> Optional[Path]:
302
+ """
303
+ Get the current log file path.
304
+
305
+ :return: The full path to the log file currently being used,
306
+ or None if no monitor protocol is present.
307
+ """
308
+ path_ptr = c_brp_lib.brp_get_log_path(self.__c_protocol__, None)
309
+ if path_ptr:
310
+ # path_ptr is a c_char_p, convert to string
311
+ path_str = ctypes.string_at(path_ptr).decode("utf-8")
312
+ return Path(path_str)
313
+ return None
314
+
315
+ @log_path.setter
316
+ def log_path(self, path: Union[str, Path]) -> None:
317
+ """
318
+ Set the log file path.
319
+
320
+ :param path: The log file path. Can be a full file path
321
+ (e.g., "C:/logs/debug.html"), a directory (uses the default
322
+ filename schema {interface}-{instance}.html),
323
+ or None to restore the default behavior.
324
+ """
325
+ if path is None:
326
+ path_cstr = None
327
+ else:
328
+ path_obj = Path(path)
329
+ if path_obj.is_dir():
330
+ path_str = str(path_obj) + os.sep
331
+ else:
332
+ path_str = str(path)
333
+ path_cstr = ctypes.create_string_buffer(path_str.encode("utf-8"))
334
+ c_brp_lib.brp_set_log_path(self.__c_protocol__, path_cstr)
335
+
336
+ @log_path.setter
337
+ def log_path(self, path: Optional[Union[str, Path]]) -> None:
338
+ """
339
+ Set the log file path.
340
+
341
+ :param path: The log file path. Can be a full file path
342
+ (e.g., "C:/logs/debug.html"), a directory (uses the default
343
+ filename schema {interface}-{instance}.html),
344
+ or None to restore the default behavior.
345
+ """
346
+ if path is None:
347
+ path_cstr = None
348
+ else:
349
+ path_obj = Path(path)
350
+ if path_obj.is_dir():
351
+ path_str = str(path_obj) + os.sep
352
+ else:
353
+ path_str = str(path)
354
+ path_cstr = ctypes.create_string_buffer(path_str.encode("utf-8"))
355
+ c_brp_lib.brp_set_log_path(self.__c_protocol__, path_cstr) # type: ignore[arg-type]
brp_lib/py.typed ADDED
File without changes
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: brp_lib
3
+ Version: 4.0.0
4
+ Summary: Low-level C library wrapper for Baltech RFID readers.
5
+ Author-email: Baltech AG <info@baltech.de>
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: typing-extensions
@@ -0,0 +1,11 @@
1
+ brp_lib/__init__.py,sha256=BLBfvZuxaSftpKd9n6h-Sr2oH8tKSUA-CF1v8Rp05AA,1082
2
+ brp_lib/brp_lib.win32.dll,sha256=S2HTbVM5M4lkjkitgOaO_sq_wvEB1U-QcflwiJYRE1c,271888
3
+ brp_lib/brp_lib.win64.dll,sha256=vgnSYcfdjSLvnd3Bnr1lGzdj1eXmASvLOZqFYW8Dj08,321040
4
+ brp_lib/dll_wrapper.py,sha256=W_Ax9-mtThrXPaX2vYXVm2hXrNHe-M0BRlAPUiq78Ck,8836
5
+ brp_lib/err.py,sha256=OI7FONYRr8mZ3yVc-fztqS3WQOLzDiqUwC-Oeahg7do,10780
6
+ brp_lib/protocols.py,sha256=y1fxZnyatZS88xFxoEPq1tDwrEItQXrAWGGuE5868_M,13186
7
+ brp_lib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ brp_lib-4.0.0.dist-info/METADATA,sha256=NXmVSaAfhEbBPSxokFAdBmIEyvXc83wG0-s53hu5Rm0,220
9
+ brp_lib-4.0.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
10
+ brp_lib-4.0.0.dist-info/top_level.txt,sha256=Lmc-it2tnEN23dUJJarKCeJKFWkM_S9EBYZ66nKbI1w,8
11
+ brp_lib-4.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ brp_lib