syndesi 0.4.2__py3-none-any.whl → 0.5.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.
Files changed (57) hide show
  1. syndesi/__init__.py +22 -2
  2. syndesi/adapters/adapter.py +332 -489
  3. syndesi/adapters/adapter_worker.py +820 -0
  4. syndesi/adapters/auto.py +58 -25
  5. syndesi/adapters/descriptors.py +38 -0
  6. syndesi/adapters/ip.py +203 -71
  7. syndesi/adapters/serialport.py +154 -25
  8. syndesi/adapters/stop_conditions.py +354 -0
  9. syndesi/adapters/timeout.py +58 -21
  10. syndesi/adapters/visa.py +236 -11
  11. syndesi/cli/console.py +51 -16
  12. syndesi/cli/shell.py +95 -47
  13. syndesi/cli/terminal_tools.py +8 -8
  14. syndesi/component.py +315 -0
  15. syndesi/protocols/delimited.py +92 -107
  16. syndesi/protocols/modbus.py +2368 -868
  17. syndesi/protocols/protocol.py +186 -33
  18. syndesi/protocols/raw.py +45 -62
  19. syndesi/protocols/scpi.py +65 -102
  20. syndesi/remote/remote.py +188 -0
  21. syndesi/scripts/syndesi.py +12 -2
  22. syndesi/tools/errors.py +49 -31
  23. syndesi/tools/log_settings.py +21 -8
  24. syndesi/tools/{log.py → logmanager.py} +24 -13
  25. syndesi/tools/types.py +9 -7
  26. syndesi/version.py +5 -1
  27. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/METADATA +1 -1
  28. syndesi-0.5.0.dist-info/RECORD +41 -0
  29. syndesi/adapters/backend/__init__.py +0 -0
  30. syndesi/adapters/backend/adapter_backend.py +0 -438
  31. syndesi/adapters/backend/adapter_manager.py +0 -48
  32. syndesi/adapters/backend/adapter_session.py +0 -346
  33. syndesi/adapters/backend/backend.py +0 -438
  34. syndesi/adapters/backend/backend_status.py +0 -0
  35. syndesi/adapters/backend/backend_tools.py +0 -66
  36. syndesi/adapters/backend/descriptors.py +0 -153
  37. syndesi/adapters/backend/ip_backend.py +0 -149
  38. syndesi/adapters/backend/serialport_backend.py +0 -241
  39. syndesi/adapters/backend/stop_condition_backend.py +0 -219
  40. syndesi/adapters/backend/timed_queue.py +0 -39
  41. syndesi/adapters/backend/timeout.py +0 -252
  42. syndesi/adapters/backend/visa_backend.py +0 -197
  43. syndesi/adapters/ip_server.py +0 -102
  44. syndesi/adapters/stop_condition.py +0 -90
  45. syndesi/cli/backend_console.py +0 -96
  46. syndesi/cli/backend_status.py +0 -274
  47. syndesi/cli/backend_wrapper.py +0 -61
  48. syndesi/scripts/syndesi_backend.py +0 -37
  49. syndesi/tools/backend_api.py +0 -175
  50. syndesi/tools/backend_logger.py +0 -64
  51. syndesi/tools/exceptions.py +0 -16
  52. syndesi/tools/internal.py +0 -0
  53. syndesi-0.4.2.dist-info/RECORD +0 -60
  54. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/WHEEL +0 -0
  55. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/entry_points.txt +0 -0
  56. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/licenses/LICENSE +0 -0
  57. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,438 +0,0 @@
1
- # File : backend.py
2
- # Author : Sébastien Deriaz
3
- # License : GPL
4
- #
5
- # The backend is responsible for managing incoming client connections (frontend)
6
- # and creating backend client threads if necessary
7
-
8
-
9
- import argparse
10
- import logging
11
- import os
12
- import select
13
- import threading
14
- import time
15
- from collections.abc import Callable
16
- from multiprocessing.connection import Client, Listener
17
- from types import EllipsisType
18
- from typing import Any, TypeGuard, cast
19
-
20
- from syndesi.adapters.backend.adapter_backend import Selectable
21
- from syndesi.adapters.backend.backend_tools import NamedConnection
22
- from syndesi.tools.types import NumberLike
23
-
24
- from ...tools.backend_api import (
25
- BACKEND_PORT,
26
- LOCALHOST,
27
- Action,
28
- BackendResponse,
29
- frontend_send,
30
- )
31
- from ...tools.log_settings import LoggerAlias
32
- from .adapter_session import AdapterSession
33
-
34
- # There is a single backend, each connection to the backend creates a thread
35
- # We need something to manage all of these threads so that two can access the same ressource
36
-
37
- DEFAULT_BACKEND_SHUTDOWN_DELAY = 2
38
- DEFAULT_SESSION_SHUTDOWN_DELAY = 2
39
-
40
-
41
- class LogRelayHandler(logging.Handler):
42
- def __init__(self, history_size: int = 100):
43
- super().__init__()
44
- self.connections: set[NamedConnection] = (
45
- set()
46
- ) # Set of active logger connections
47
- self.connections_lock = threading.Lock()
48
- self.log_history: list[logging.LogRecord] = []
49
- self.log_history_lock = threading.Lock()
50
- self.history_size = history_size
51
-
52
- def add_connection(
53
- self, conn: NamedConnection, delete_callback: Callable[[NamedConnection], None]
54
- ) -> None:
55
- # Send log history to the new connection
56
- with self.log_history_lock:
57
- for record in self.log_history:
58
- try:
59
- conn.conn.send(record)
60
- except (BrokenPipeError, OSError):
61
- delete_callback(conn)
62
- return
63
- # Add to active connections
64
- with self.connections_lock:
65
- self.connections.add(conn)
66
-
67
- def remove_connection(self, conn: NamedConnection) -> None:
68
- with self.connections_lock:
69
- self.connections = {c for c in self.connections if c != conn}
70
-
71
- def emit(self, record: logging.LogRecord) -> None:
72
- # Add to history
73
- with self.log_history_lock:
74
- self.log_history.append(record)
75
- if len(self.log_history) > self.history_size:
76
- self.log_history.pop(0)
77
- # Broadcast to all connections
78
- to_remove: list[NamedConnection] = []
79
- with self.connections_lock:
80
- for conn in list(self.connections):
81
- try:
82
- conn.conn.send(record)
83
- except (BrokenPipeError, OSError):
84
- to_remove.append(conn)
85
-
86
- for conn in to_remove:
87
- self.remove_connection(conn)
88
-
89
-
90
- def is_request(x: object) -> TypeGuard[tuple[str, object]]:
91
- if not isinstance(x, tuple) or not x:
92
- return False
93
- if not isinstance(x[0], str):
94
- return False
95
-
96
- return True
97
-
98
-
99
- class Backend:
100
- MONITORING_DELAY = 0.5
101
- _session_shutdown_delay: NumberLike | None
102
- _backend_shutdown_delay: NumberLike | None
103
- _backend_shutdown_timestamp: NumberLike | None
104
- shutdown_timer: threading.Timer | None
105
-
106
- def __init__(
107
- self,
108
- host: str,
109
- port: int,
110
- backend_shutdown_delay: None | NumberLike | EllipsisType = ...,
111
- session_shutdown_delay: None | NumberLike | EllipsisType = ...,
112
- ):
113
-
114
- if backend_shutdown_delay is ...:
115
- self._backend_shutdown_delay = DEFAULT_BACKEND_SHUTDOWN_DELAY
116
- else:
117
- self._backend_shutdown_delay = backend_shutdown_delay
118
-
119
- if session_shutdown_delay is ...:
120
- self._session_shutdown_delay = DEFAULT_SESSION_SHUTDOWN_DELAY
121
- else:
122
- self._session_shutdown_delay = session_shutdown_delay
123
-
124
- if self._backend_shutdown_delay is None:
125
- self._backend_shutdown_timestamp = None
126
- else:
127
- self._backend_shutdown_timestamp = (
128
- time.time() + self._backend_shutdown_delay
129
- )
130
-
131
- self.host = host
132
- self.port = port
133
-
134
- # self.adapters_lock = threading.Lock()
135
- self.listener: Listener = Listener((self.host, self.port), backlog=10)
136
- self.adapter_sessions: dict[str, AdapterSession] = {}
137
- self.shutdown_timer = None
138
-
139
- # Monitoring connections
140
- self._monitoring_connections: list[NamedConnection] = []
141
-
142
- # Logger connections
143
- # self._logger_connections: list[NamedConnection] = []
144
-
145
- # Configure loggers
146
- self._log_handler = LogRelayHandler(history_size=100)
147
-
148
- self._adapter_session_logger = logging.getLogger(
149
- LoggerAlias.ADAPTER_BACKEND.value
150
- )
151
- self._adapter_session_logger.addHandler(self._log_handler)
152
- self._adapter_session_logger.setLevel(logging.DEBUG)
153
- self._logger = logging.getLogger(LoggerAlias.BACKEND.value)
154
- self._logger.setLevel(logging.DEBUG)
155
- self._logger.addHandler(self._log_handler)
156
-
157
- self._logger.info(f"Init backend on {self.host}:{self.port}")
158
-
159
- self.running = True
160
-
161
- def _remove_logger_connection(self, conn: NamedConnection) -> None:
162
- self._log_handler.remove_connection(conn)
163
- self._update_monitoring(monitoring_sessions=True)
164
-
165
- def _remove_monitoring_connection(self, conn: NamedConnection) -> None:
166
- try:
167
- self._monitoring_connections.remove(conn)
168
- except ValueError:
169
- pass
170
- self._update_monitoring(monitoring_sessions=True)
171
-
172
- def _remove_session(self, descriptor: str) -> None:
173
- self._logger.info(f"Remove adapter session {descriptor}")
174
- # with self.adapters_lock:
175
- self.adapter_sessions.pop(descriptor, None)
176
- self._update_monitoring(adapter_sessions=True)
177
-
178
- def manage_monitoring_clients(self, conn: NamedConnection) -> None:
179
- try:
180
- raw: object = conn.conn.recv()
181
- except (EOFError, ConnectionResetError):
182
- # Monitor disconnected
183
- # Remove from the list of monitoring connections
184
- self._monitoring_connections.remove(conn)
185
- conn.conn.close()
186
- else:
187
- response: BackendResponse
188
- response = (Action.ERROR_GENERIC, "Unknown error")
189
- if not is_request(raw):
190
- response = (
191
- Action.ERROR_INVALID_REQUEST,
192
- "Invalid backend debugger request",
193
- )
194
- else:
195
- action: Action = Action(raw[0])
196
-
197
- if action == Action.BACKEND_STATS:
198
- response = (Action.BACKEND_STATS, os.getpid())
199
- elif action == Action.SET_LOG_LEVEL:
200
- response = (Action.SET_LOG_LEVEL,)
201
- level: int = cast(int, raw[1])
202
- self._logger.setLevel(level)
203
- self._adapter_session_logger.setLevel(level)
204
- else:
205
- response = (Action.ERROR_UNKNOWN_ACTION, f"{action}")
206
-
207
- if not frontend_send(conn.conn, *response):
208
- self._monitoring_connections.remove(conn)
209
-
210
- def _broadcast_to_monitoring_clients(self, action: Action, *args: Any) -> None:
211
- for conn in self._monitoring_connections:
212
- if not frontend_send(conn.conn, action, *args):
213
- self._remove_monitoring_connection(conn)
214
-
215
- def _update_monitoring(
216
- self,
217
- adapter_sessions: bool = False,
218
- monitoring_sessions: bool = False,
219
- stats: bool = False,
220
- ) -> None:
221
- if adapter_sessions:
222
- snapshot: dict[str, tuple[bool, list[str]]] = {}
223
- for adapter_descriptor, thread in self.adapter_sessions.items():
224
- adapter_clients = thread.enumerate_connections()
225
- status = thread.is_adapter_opened()
226
- snapshot[adapter_descriptor] = status, adapter_clients
227
- self._broadcast_to_monitoring_clients(
228
- Action.ENUMERATE_ADAPTER_CONNECTIONS, snapshot
229
- )
230
-
231
- if monitoring_sessions:
232
- self._broadcast_to_monitoring_clients(
233
- Action.ENUMERATE_MONITORING_CONNECTIONS,
234
- [(x.remote(), "logging") for x in self._log_handler.connections]
235
- + [(x.remote(), "monitoring") for x in self._monitoring_connections],
236
- )
237
-
238
- if stats:
239
- self._broadcast_to_monitoring_clients(Action.BACKEND_STATS, os.getpid())
240
-
241
- def _monitoring(self, ready_monitoring_clients: list[NamedConnection]) -> None:
242
- t = time.time()
243
-
244
- for conn in ready_monitoring_clients:
245
- self.manage_monitoring_clients(conn)
246
-
247
- if self._backend_shutdown_delay is not None:
248
- active_threads = self.active_threads()
249
-
250
- if active_threads == 0:
251
- if self._backend_shutdown_timestamp is not None:
252
- if time.time() >= self._backend_shutdown_timestamp:
253
- self.stop()
254
- else:
255
- self._backend_shutdown_timestamp = t + self._backend_shutdown_delay
256
-
257
- def active_threads(self) -> int:
258
- # Check all threads, if one is active, return True
259
- # Remove all of the dead ones
260
- # Make as many passes as necessary
261
- # with self.adapters_lock:
262
- while True:
263
- i = 0
264
- for k, t in self.adapter_sessions.items():
265
- if t.is_alive():
266
- i += 1
267
- else:
268
- self._remove_session(k)
269
- # Break because the dict changed size
270
- break
271
- else:
272
- # Get out of the loop
273
- break
274
-
275
- return i
276
-
277
- def _manage_new_adapter_client(self, client: NamedConnection) -> None:
278
- # Wait for adapter
279
- # ready = wait([client.conn], timeout=0.1)
280
- # selectors to work on Unix and Windows
281
- ready, _, _ = select.select([client.conn], [], [], 0.1)
282
- if len(ready) == 0:
283
- client.conn.close()
284
- return
285
-
286
- try:
287
- adapter_request = client.conn.recv()
288
- except EOFError:
289
- return
290
- action = Action(adapter_request[0])
291
- if action == Action.SELECT_ADAPTER:
292
- adapter_descriptor = adapter_request[1]
293
- # If the session exists but it is dead, delete it
294
- if (
295
- adapter_descriptor in self.adapter_sessions
296
- and not self.adapter_sessions[adapter_descriptor].is_alive()
297
- ):
298
- self._remove_session(adapter_descriptor)
299
-
300
- if adapter_descriptor not in self.adapter_sessions:
301
- # Create the adapter backend thread
302
- self._logger.info(f"Creating adapter session for {adapter_descriptor}")
303
- thread = AdapterSession(
304
- adapter_descriptor, shutdown_delay=self._session_shutdown_delay
305
- ) # TODO : Put another delay here ?
306
- # with self.adapters_lock:
307
- thread.start()
308
- self.adapter_sessions[adapter_descriptor] = thread
309
-
310
- self.adapter_sessions[adapter_descriptor].add_connection(client)
311
- frontend_send(client.conn, action)
312
- else:
313
- client.conn.close()
314
-
315
- self._update_monitoring(adapter_sessions=True)
316
-
317
- def _new_monitoring_client(self, client: NamedConnection) -> None:
318
- # with self._monitoring_connections_lock:
319
- self._monitoring_connections.append(client)
320
- self._update_monitoring(monitoring_sessions=True, stats=True)
321
-
322
- def _new_logger_client(self, client: NamedConnection) -> None:
323
- # Add connection to the single log handler
324
- self._log_handler.add_connection(client, self._remove_logger_connection)
325
- # self._logger_connections.append(client)
326
- self._update_monitoring(monitoring_sessions=True)
327
-
328
- def _new_adapter_client(self, client: NamedConnection) -> None:
329
- try:
330
- role_request = client.conn.recv()
331
- except EOFError:
332
- return
333
-
334
- action = Action(role_request[0])
335
- frontend_send(client.conn, action)
336
-
337
- # Send confirmation directly
338
- if action == Action.SET_ROLE_ADAPTER:
339
- self._manage_new_adapter_client(client)
340
- elif action == Action.SET_ROLE_MONITORING:
341
- self._new_monitoring_client(client)
342
- elif action == Action.SET_ROLE_LOGGER:
343
- self._new_logger_client(client)
344
- elif action == Action.PING:
345
- frontend_send(client.conn, Action.PING)
346
- elif action == Action.STOP:
347
- self._logger.info("Stop request from client")
348
- client.conn.close()
349
- self.stop()
350
- else:
351
- frontend_send(client.conn, Action.ERROR_INVALID_ROLE)
352
- client.conn.close()
353
-
354
- def start(self) -> None:
355
- while self.running:
356
- # Use selectors to work on both Linux and Windows
357
-
358
- selectables: list[Selectable] = [
359
- x.conn for x in self._monitoring_connections
360
- ]
361
- selectables.append(self.listener._listener._socket) # type: ignore
362
-
363
- ready, _, _ = select.select(selectables, [], [], self.MONITORING_DELAY)
364
-
365
- # ready : List[Selectable] = wait( # type: ignore
366
- # [self.listener._listener._socket] + [x.conn for x in self._monitoring_connections], # type: ignore
367
- # timeout=self.MONITORING_DELAY,
368
- # )
369
-
370
- if self.listener._listener._socket in ready: # type: ignore
371
- conn = self.listener.accept()
372
-
373
- self._new_adapter_client(NamedConnection(conn))
374
- ready.remove(self.listener._listener._socket) # type: ignore
375
-
376
- self._monitoring(
377
- [c for c in self._monitoring_connections if c.conn in ready]
378
- )
379
-
380
- self.listener.close()
381
- self._logger.info("Backend stopped")
382
-
383
- def _delayed_stop(self) -> None:
384
- if self._backend_shutdown_delay is not None:
385
- self.shutdown_timer: threading.Timer | None = threading.Timer(
386
- float(self._backend_shutdown_delay), self.stop
387
- )
388
- self.shutdown_timer.start()
389
-
390
- def stop(self) -> None:
391
- self.running = False
392
- # Open a connection to stop the server
393
- try:
394
- # If the listener is on all interfaces, use localhost
395
- if self.host == "0.0.0.0": # ALL_ADDRESSES:
396
- address = LOCALHOST
397
- else:
398
- address = self.host
399
- # Always connect to localhost
400
- conn = Client((address, self.port))
401
- frontend_send(conn, Action.STOP)
402
- conn.close()
403
- except Exception:
404
- pass
405
-
406
-
407
- def main(input_args: list[str] | None = None) -> None:
408
-
409
- argument_parser = argparse.ArgumentParser()
410
-
411
- argument_parser.add_argument(
412
- "-a",
413
- "--address",
414
- type=str,
415
- default=LOCALHOST,
416
- help="Listening address, set it to the interface that will be used by the client",
417
- )
418
- argument_parser.add_argument("-p", "--port", type=int, default=BACKEND_PORT)
419
- argument_parser.add_argument(
420
- "-s",
421
- "--shutdown-delay",
422
- type=int,
423
- default=None,
424
- help="Delay before the backend shutdowns automatically",
425
- )
426
- argument_parser.add_argument("-q", "--quiet", default=False, action="store_true")
427
- argument_parser.add_argument("-v", "--verbose", default=False, action="store_true")
428
-
429
- args = argument_parser.parse_args(input_args)
430
-
431
- backend = Backend(
432
- host=args.address, port=args.port, backend_shutdown_delay=args.shutdown_delay
433
- )
434
- backend.start()
435
-
436
-
437
- if __name__ == "__main__":
438
- main()
File without changes
@@ -1,66 +0,0 @@
1
- import socket
2
- from multiprocessing.connection import Connection
3
-
4
- BACKEND_REQUEST_DEFAULT_TIMEOUT = 0.5
5
-
6
-
7
- def get_conn_addresses(conn: Connection) -> tuple[tuple[str, int], tuple[str, int]]:
8
- try:
9
- fd = conn.fileno()
10
- except OSError:
11
- return (("closed", 0), ("closed", 0))
12
- else:
13
- sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
14
- # try:
15
- # TODO : Implement exception
16
- # address, port = sock.getpeername() # (IP, port) tuple
17
- peer_address = sock.getpeername()
18
- sock_address = sock.getsockname()
19
- return sock_address, peer_address
20
- # except Exception:
21
- # return (("error", 0), ("closed", 0))
22
-
23
-
24
- class ConnectionDescriptor:
25
- def __init__(self, conn: Connection) -> None:
26
- """
27
- Description of a multiprocessing Connection
28
- """
29
- local, remote = get_conn_addresses(conn)
30
- self._remote_address = remote[0]
31
- self._remote_port = int(remote[1])
32
- self._local_address = local[0]
33
- self._local_port = int(local[1])
34
-
35
- def remote(self) -> str:
36
- return f"{self._remote_address}:{self._remote_port}"
37
-
38
- def local(self) -> str:
39
- return f"{self._local_address}:{self._local_port}"
40
-
41
- def remote_address(self) -> str:
42
- return self._remote_address
43
-
44
- def remote_port(self) -> int:
45
- return self._remote_port
46
-
47
- def local_address(self) -> str:
48
- return self._local_address
49
-
50
- def local_port(self) -> int:
51
- return self._local_port
52
-
53
- def __str__(self) -> str:
54
- return f"{self.local()}->{self.remote()}"
55
-
56
-
57
- class NamedConnection(ConnectionDescriptor):
58
- def __init__(self, conn: Connection) -> None:
59
- super().__init__(conn)
60
- self.conn = conn
61
-
62
- def __str__(self) -> str:
63
- return f"Connection {self.remote()}"
64
-
65
- def __repr__(self) -> str:
66
- return self.__str__()
@@ -1,153 +0,0 @@
1
- # File : descriptors.py
2
- # Author : Sébastien Deriaz
3
- # License : GPL
4
- #
5
- # Descriptors are classes that describe how an adapter is connected to its device.
6
- # Depending on the protocol, they can hold strings, integers or enums
7
-
8
- import re
9
- from abc import abstractmethod
10
- from dataclasses import dataclass
11
- from enum import Enum
12
-
13
-
14
- class Descriptor:
15
- DETECTION_PATTERN = ""
16
-
17
- def __init__(self) -> None:
18
- return None
19
-
20
- @staticmethod
21
- @abstractmethod
22
- def from_string(string: str) -> "Descriptor":
23
- pass
24
-
25
- @abstractmethod
26
- def is_initialized(self) -> bool:
27
- pass
28
-
29
-
30
- @dataclass
31
- class SerialPortDescriptor(Descriptor):
32
- DETECTION_PATTERN = r"(COM\d+|/dev[/\w\d]+):\d+"
33
- port: str
34
- baudrate: int | None = None
35
-
36
- @staticmethod
37
- def from_string(string: str) -> "SerialPortDescriptor":
38
- parts = string.split(":")
39
- port = parts[0]
40
- baudrate = int(parts[1])
41
- return SerialPortDescriptor(port, baudrate)
42
-
43
- def set_default_baudrate(self, baudrate: int) -> bool:
44
- if self.baudrate is not None:
45
- self.baudrate = baudrate
46
- return True
47
- else:
48
- return False
49
-
50
- def __str__(self) -> str:
51
- return f"{self.port}:{self.baudrate}"
52
-
53
- def is_initialized(self) -> bool:
54
- return self.baudrate is not None
55
-
56
-
57
- @dataclass
58
- class IPDescriptor(Descriptor):
59
- class Transport(Enum):
60
- TCP = "TCP"
61
- UDP = "UDP"
62
-
63
- @classmethod
64
- def from_str(cls, value: str) -> "IPDescriptor":
65
- for member in cls:
66
- if member.value.lower() == value.lower():
67
- return member # type: ignore # TODO : Check this
68
- raise ValueError(f"{value} is not a valid {cls.__name__}")
69
-
70
- DETECTION_PATTERN = r"(\d+.\d+.\d+.\d+|[\w\.]+):\d+:(UDP|TCP)"
71
- address: str
72
- transport: Transport
73
- port: int | None = None
74
- # transport: Transport | None = None
75
-
76
- @staticmethod
77
- def from_string(string: str) -> "IPDescriptor":
78
- parts = string.split(":")
79
- address = parts[0]
80
- port = int(parts[1])
81
- transport = IPDescriptor.Transport(parts[2])
82
- return IPDescriptor(address, transport, port)
83
-
84
- def __str__(self) -> str:
85
- return f"{self.address}:{self.port}:{self.Transport(self.transport).value}"
86
-
87
- def is_initialized(self) -> bool:
88
- return self.port is not None and self.transport is not None
89
-
90
-
91
- @dataclass
92
- class VisaDescriptor(Descriptor):
93
- # VISA Resource Address Examples
94
- # GPIB (IEEE-488)
95
- # GPIB0::14::INSTR
96
- # # Serial (RS-232 or USB-Serial)
97
- # ASRL1::INSTR # Windows COM1
98
- # ASRL/dev/ttyUSB0::INSTR # Linux USB serial port
99
- #
100
- # # TCPIP INSTR (LXI/VXI-11/HiSLIP-compatible instruments)
101
- # TCPIP0::192.168.1.100::INSTR
102
- # TCPIP0::my-scope.local::inst0::INSTR
103
- #
104
- # # TCPIP SOCKET (Raw TCP communication)
105
- # TCPIP0::192.168.1.42::5025::SOCKET
106
- #
107
- # # USB (USBTMC-compliant instruments)
108
- # USB0::0x0957::0x1796::MY12345678::INSTR
109
- #
110
- # # VXI (Legacy modular instruments)
111
- # VXI0::2::INSTR
112
- #
113
- # # PXI (Modular instrument chassis)
114
- # PXI0::14::INSTR
115
- DETECTION_PATTERN = r"([A-Z]+)(\d*|\/[^:]+)?::([^:]+)(?:::([^:]+))?(?:::([^:]+))?(?:::([^:]+))?::(INSTR|SOCKET)"
116
-
117
- descriptor: str
118
-
119
- class Interface(Enum):
120
- GPIB = "GPIB"
121
- SERIAL = "ASRL"
122
- TCP = "TCPIP"
123
- USB = "USB"
124
- VXI = "VXI"
125
- PXI = "PXI"
126
-
127
- @staticmethod
128
- def from_string(string: str) -> "VisaDescriptor":
129
- if re.match(VisaDescriptor.DETECTION_PATTERN, string):
130
- return VisaDescriptor(descriptor=string)
131
- else:
132
- raise ValueError(f"Could not parse descriptor : {string}")
133
-
134
- def __str__(self) -> str:
135
- return self.descriptor
136
-
137
- def is_initialized(self) -> bool:
138
- return True
139
-
140
-
141
- descriptors: list[type[Descriptor]] = [
142
- SerialPortDescriptor,
143
- IPDescriptor,
144
- VisaDescriptor,
145
- ]
146
-
147
-
148
- def adapter_descriptor_by_string(string_descriptor: str) -> Descriptor:
149
- for descriptor in descriptors:
150
- if re.match(descriptor.DETECTION_PATTERN, string_descriptor):
151
- x = descriptor.from_string(string_descriptor)
152
- return x
153
- raise ValueError(f"Could not parse descriptor string : {string_descriptor}")