qlsdk2 0.3.0a2__py3-none-any.whl → 0.4.0a1__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.
qlsdk/rsc/eegion.py ADDED
@@ -0,0 +1,360 @@
1
+ import socket
2
+ import json
3
+ import threading
4
+ import logging
5
+ import time
6
+ import queue
7
+ from abc import ABC, abstractmethod
8
+ from typing import Dict, List, Callable, Optional, Tuple
9
+ from loguru import logger
10
+
11
+ from rsc import UdpBroadcaster
12
+
13
+ # ----------------------
14
+ # Constants and Exceptions
15
+ # ----------------------
16
+ UDP_DISCOVERY_PORT = 50000
17
+ TCP_COMMUNICATION_PORT = 50001
18
+ PROXY_PORT = 50002
19
+ BUFFER_SIZE = 4096
20
+
21
+ class DeviceError(Exception):
22
+ """Base exception for device related errors"""
23
+
24
+ class DeviceNotFoundError(DeviceError):
25
+ """Requested device not found"""
26
+
27
+ class ConnectionError(DeviceError):
28
+ """Device connection failure"""
29
+
30
+ class UnsupportedFeatureError(DeviceError):
31
+ """Requested feature not supported"""
32
+
33
+ # ----------------------
34
+ # Data Structures
35
+ # ----------------------
36
+ class DeviceInfo:
37
+ def __init__(self, serial: str, device_type: str, ip: str, tcp_port: int):
38
+ self.serial = serial
39
+ self.type = device_type
40
+ self.ip = ip
41
+ self.tcp_port = tcp_port
42
+
43
+ def __repr__(self):
44
+ return f"<Device {self.serial} ({self.type}) @ {self.ip}:{self.tcp_port}>"
45
+
46
+
47
+ class SecureTCPConnection:
48
+ def __init__(self, ip: str, port: int):
49
+ self.ip = ip
50
+ self.port = port
51
+ self._socket = None
52
+ self._lock = threading.Lock()
53
+ self._connected = False
54
+ self._logger = logging.getLogger("Connection")
55
+ self._message_queue = queue.Queue()
56
+ self._receive_thread = None
57
+
58
+ def connect(self):
59
+ """Establish and maintain TCP connection"""
60
+ with self._lock:
61
+ if self._connected:
62
+ return
63
+
64
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
65
+ try:
66
+ self._socket.connect((self.ip, self.port))
67
+ self._connected = True
68
+ self._start_receive_thread()
69
+ except socket.error as e:
70
+ raise ConnectionError(f"Connection failed: {str(e)}")
71
+
72
+ def _start_receive_thread(self):
73
+ self._receive_thread = threading.Thread(
74
+ target=self._receive_worker,
75
+ daemon=True
76
+ )
77
+ self._receive_thread.start()
78
+
79
+ def _receive_worker(self):
80
+ while self._connected:
81
+ try:
82
+ data = self._socket.recv(BUFFER_SIZE)
83
+ if not data:
84
+ break
85
+ self._message_queue.put(data)
86
+ except (socket.error, ConnectionResetError):
87
+ break
88
+ self.disconnect()
89
+
90
+ def disconnect(self):
91
+ """Close TCP connection"""
92
+ with self._lock:
93
+ if self._connected and self._socket:
94
+ self._socket.close()
95
+ self._connected = False
96
+
97
+ def send(self, data: bytes):
98
+ """Send data through TCP connection"""
99
+ with self._lock:
100
+ if not self._connected:
101
+ raise ConnectionError("Not connected")
102
+ try:
103
+ self._socket.sendall(data)
104
+ except socket.error as e:
105
+ raise ConnectionError(f"Send failed: {str(e)}")
106
+
107
+ def receive(self) -> Optional[bytes]:
108
+ """Get received data from queue"""
109
+ try:
110
+ return self._message_queue.get_nowait()
111
+ except queue.Empty:
112
+ return None
113
+
114
+ # ----------------------
115
+ # Device Abstraction Layer
116
+ # ----------------------
117
+ class BaseDevice(ABC):
118
+ def __init__(self, info: DeviceInfo, connection: SecureTCPConnection):
119
+ self.info = info
120
+ self.conn = connection
121
+ self._data_callbacks: List[Callable[[bytes], None]] = []
122
+ self._command_callbacks: List[Callable[[dict], None]] = []
123
+ self._stream_active = False
124
+ self._stream_thread: Optional[threading.Thread] = None
125
+
126
+ @property
127
+ @abstractmethod
128
+ def supports_acquisition(self) -> bool:
129
+ pass
130
+
131
+ @property
132
+ @abstractmethod
133
+ def supports_stimulation(self) -> bool:
134
+ pass
135
+
136
+ def register_data_callback(self, callback: Callable[[bytes], None]):
137
+ self._data_callbacks.append(callback)
138
+
139
+ def register_command_callback(self, callback: Callable[[dict], None]):
140
+ self._command_callbacks.append(callback)
141
+
142
+ # 启动信号采集
143
+ def start_acq(self, persist=False, path=None):
144
+ self.send_command({"type": "acquire", "param": "start", "value": persist})
145
+ if not self.supports_acquisition:
146
+ raise UnsupportedFeatureError("signal acquisition not supported")
147
+
148
+ self._stream_active = True
149
+ self._stream_thread = threading.Thread(
150
+ target=self._stream_worker,
151
+ daemon=True
152
+ )
153
+ self._stream_thread.start()
154
+
155
+ # 停止信号采集
156
+ def stop_acq(self):
157
+ self._stream_active = False
158
+ if self._stream_thread:
159
+ self._stream_thread.join()
160
+
161
+ # 设置信号采集配置
162
+ def set_acq_config(self, config: dict):
163
+ """Set data acquisition configuration"""
164
+ self.send_command({"type": "acquire", "param": "config", "value": config})
165
+
166
+ # 设置电刺激配置
167
+ def set_stim_config(self, config: dict):
168
+ """Set stimulation configuration"""
169
+ self.send_command({"type": "stim", "param": "config", "value": config})
170
+
171
+ def start_stim(self):
172
+ """Start stimulation"""
173
+ self.send_command({"type": "stim", "param": "start"})
174
+
175
+ def _stream_worker(self):
176
+ while self._stream_active:
177
+ data = self.conn.receive()
178
+ if data:
179
+ for callback in self._data_callbacks:
180
+ callback(data)
181
+ time.sleep(0.001)
182
+
183
+ def send_command(self, command: dict):
184
+ """Send command and notify callbacks"""
185
+ try:
186
+ self.conn.send(json.dumps(command).encode())
187
+ for callback in self._command_callbacks:
188
+ callback(command)
189
+ except ConnectionError as e:
190
+ logging.error(f"Command failed: {str(e)}")
191
+
192
+ # ----------------------
193
+ # Device Implementations
194
+ # ----------------------
195
+ class LJS1(BaseDevice):
196
+ @property
197
+ def supports_acquisition(self) -> bool:
198
+ return True
199
+
200
+ @property
201
+ def supports_stimulation(self) -> bool:
202
+ return False
203
+
204
+ def set_sampling_rate(self, rate: int):
205
+ self.send_command({"type": "config", "param": "rate", "value": rate})
206
+
207
+ class ARS(BaseDevice):
208
+ @property
209
+ def supports_acquisition(self) -> bool:
210
+ return True
211
+
212
+ @property
213
+ def supports_stimulation(self) -> bool:
214
+ return False
215
+ def set_stimulation(self, intensity: float, duration: float):
216
+ self.send_command({
217
+ "type": "stimulate",
218
+ "intensity": intensity,
219
+ "duration": duration
220
+ })
221
+
222
+ class X8(BaseDevice):
223
+ @property
224
+ def supports_acquisition(self) -> bool:
225
+ return True
226
+
227
+ @property
228
+ def supports_stimulation(self) -> bool:
229
+ return False
230
+ def set_stimulation(self, intensity: float, duration: float):
231
+ self.send_command({
232
+ "type": "stimulate",
233
+ "intensity": intensity,
234
+ "duration": duration
235
+ })
236
+
237
+ class C64RS(BaseDevice):
238
+ @property
239
+ def supports_acquisition(self) -> bool:
240
+ return True
241
+
242
+ @property
243
+ def supports_stimulation(self) -> bool:
244
+ return False
245
+ def set_stimulation(self, intensity: float, duration: float):
246
+ self.send_command({
247
+ "type": "stimulate",
248
+ "intensity": intensity,
249
+ "duration": duration
250
+ })
251
+
252
+ class C256RS(BaseDevice):
253
+ @property
254
+ def supports_acquisition(self) -> bool:
255
+ return True
256
+
257
+ @property
258
+ def supports_stimulation(self) -> bool:
259
+ return False
260
+ def set_stimulation(self, intensity: float, duration: float):
261
+ self.send_command({
262
+ "type": "stimulate",
263
+ "intensity": intensity,
264
+ "duration": duration
265
+ })
266
+
267
+ # ----------------------
268
+ # Proxy Server
269
+ # ----------------------
270
+ class DeviceProxy:
271
+ def __init__(self, sdk):
272
+ self.sdk = sdk
273
+ self._running = False
274
+ self._server_socket = None
275
+ self._clients = {}
276
+ self._logger = logging.getLogger("DeviceProxy")
277
+
278
+ def start(self, port: int = PROXY_PORT):
279
+ self._running = True
280
+ self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
281
+ self._server_socket.bind(('0.0.0.0', port))
282
+ self._server_socket.listen(5)
283
+ threading.Thread(target=self._accept_clients, daemon=True).start()
284
+
285
+ def stop(self):
286
+ self._running = False
287
+ if self._server_socket:
288
+ self._server_socket.close()
289
+
290
+ def _accept_clients(self):
291
+ while self._running:
292
+ try:
293
+ client_socket, addr = self._server_socket.accept()
294
+ client_id = f"{addr[0]}:{addr[1]}"
295
+ self._clients[client_id] = client_socket
296
+ threading.Thread(
297
+ target=self._handle_client,
298
+ args=(client_socket, addr),
299
+ daemon=True
300
+ ).start()
301
+ except OSError:
302
+ break
303
+
304
+ def _handle_client(self, client_socket: socket.socket, addr: Tuple[str, int]):
305
+ client_id = f"{addr[0]}:{addr[1]}"
306
+ self._logger.info(f"New client connected: {client_id}")
307
+
308
+ while self._running:
309
+ try:
310
+ data = client_socket.recv(BUFFER_SIZE)
311
+ if not data:
312
+ break
313
+
314
+ # Process proxy command
315
+ try:
316
+ command = json.loads(data.decode())
317
+ self._process_command(client_socket, command)
318
+ except json.JSONDecodeError:
319
+ self._logger.warning(f"Invalid command from {client_id}")
320
+
321
+ except (ConnectionResetError, socket.error):
322
+ break
323
+
324
+ self._logger.info(f"Client disconnected: {client_id}")
325
+ client_socket.close()
326
+ del self._clients[client_id]
327
+
328
+ def _process_command(self, client_socket: socket.socket, command: dict):
329
+ required_fields = ["device_id", "action", "params"]
330
+ if not all(field in command for field in required_fields):
331
+ self._logger.warning("Invalid command structure")
332
+ return
333
+
334
+ try:
335
+ device = self.sdk.get_device(command["device_id"])
336
+ if not device:
337
+ raise DeviceNotFoundError
338
+
339
+ # Execute device command
340
+ if command["action"] == "send":
341
+ device.send_command(command["params"])
342
+ client_socket.send(b"Command executed")
343
+ elif command["action"] == "stream":
344
+ self._setup_streaming(client_socket, device)
345
+ else:
346
+ client_socket.send(b"Invalid action")
347
+
348
+ except DeviceError as e:
349
+ client_socket.send(f"Error: {str(e)}".encode())
350
+
351
+ def _setup_streaming(self, client_socket: socket.socket, device: BaseDevice):
352
+ def forward_data(data: bytes):
353
+ try:
354
+ client_socket.send(data)
355
+ except (socket.error, ConnectionResetError):
356
+ device.stop_data_stream()
357
+
358
+ device.register_data_callback(forward_data)
359
+ device.start_data_stream()
360
+ client_socket.send(b"Streaming started")