qlsdk2 0.3.0a3__py3-none-any.whl → 0.4.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.
- qlsdk/__init__.py +1 -0
- qlsdk/core/__init__.py +5 -0
- qlsdk/core/crc/__init__.py +5 -0
- qlsdk/core/crc/crctools.py +95 -0
- qlsdk/core/device.py +25 -0
- qlsdk/core/entity/__init__.py +92 -0
- qlsdk/core/exception.py +0 -0
- qlsdk/core/filter/__init__.py +1 -0
- qlsdk/core/filter/norch.py +59 -0
- qlsdk/core/local.py +34 -0
- qlsdk/core/message/__init__.py +2 -0
- qlsdk/core/message/command.py +324 -0
- qlsdk/core/message/tcp.py +0 -0
- qlsdk/core/message/udp.py +96 -0
- qlsdk/core/network/__init__.py +34 -0
- qlsdk/core/network/monitor.py +55 -0
- qlsdk/core/utils.py +70 -0
- qlsdk/persist/__init__.py +2 -1
- qlsdk/persist/rsc_edf.py +299 -0
- qlsdk/rsc/__init__.py +7 -0
- qlsdk/rsc/command/__init__.py +214 -0
- qlsdk/rsc/command/message.py +239 -0
- qlsdk/rsc/device_manager.py +119 -0
- qlsdk/rsc/discover.py +87 -0
- qlsdk/rsc/eegion.py +360 -0
- qlsdk/rsc/entity.py +447 -0
- qlsdk/rsc/paradigm.py +313 -0
- qlsdk/rsc/proxy.py +76 -0
- {qlsdk2-0.3.0a3.dist-info → qlsdk2-0.4.0.dist-info}/METADATA +2 -2
- qlsdk2-0.4.0.dist-info/RECORD +42 -0
- qlsdk2-0.3.0a3.dist-info/RECORD +0 -16
- {qlsdk2-0.3.0a3.dist-info → qlsdk2-0.4.0.dist-info}/WHEEL +0 -0
- {qlsdk2-0.3.0a3.dist-info → qlsdk2-0.4.0.dist-info}/top_level.txt +0 -0
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")
|