qlsdk2 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.
- qlsdk/persist/rsc_edf.py +21 -6
- qlsdk/rsc/command/__init__.py +26 -22
- qlsdk/rsc/device/__init__.py +6 -1
- qlsdk/rsc/device/arskindling.py +4 -2
- qlsdk/rsc/device/base.py +80 -38
- qlsdk/rsc/device/c16_rs.py +205 -0
- qlsdk/rsc/device/c256_rs.py +1 -0
- qlsdk/rsc/device/c64_rs.py +3 -2
- qlsdk/rsc/device/c64s1.py +3 -2
- qlsdk/rsc/device/device_factory.py +1 -0
- qlsdk/rsc/interface/device.py +3 -0
- qlsdk/rsc/manager/container.py +17 -18
- qlsdk/rsc/network/discover.py +25 -36
- qlsdk/rsc/parser/base.py +103 -22
- qlsdk2-0.5.0.dist-info/METADATA +40 -0
- {qlsdk2-0.4.2.dist-info → qlsdk2-0.5.0.dist-info}/RECORD +18 -17
- qlsdk2-0.4.2.dist-info/METADATA +0 -121
- {qlsdk2-0.4.2.dist-info → qlsdk2-0.5.0.dist-info}/WHEEL +0 -0
- {qlsdk2-0.4.2.dist-info → qlsdk2-0.5.0.dist-info}/top_level.txt +0 -0
qlsdk/persist/rsc_edf.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
import time
|
|
2
3
|
from multiprocessing import Lock, Queue
|
|
3
4
|
from time import time_ns
|
|
4
5
|
from pyedflib import FILETYPE_BDFPLUS, FILETYPE_EDFPLUS, EdfWriter
|
|
@@ -77,8 +78,9 @@ class EDFStreamWriter(Thread):
|
|
|
77
78
|
if self._writer is None:
|
|
78
79
|
self._init_writer()
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
waits = 300
|
|
82
|
+
while waits > 0:
|
|
83
|
+
if not self.data_queue.empty():
|
|
82
84
|
try:
|
|
83
85
|
data = self.data_queue.get(timeout=30)
|
|
84
86
|
if data is None:
|
|
@@ -88,13 +90,20 @@ class EDFStreamWriter(Thread):
|
|
|
88
90
|
self._points += len(data[1])
|
|
89
91
|
logger.trace(f"已处理数据点数:{self._points}")
|
|
90
92
|
self._write_file(data)
|
|
93
|
+
# 有数据重置计数器
|
|
94
|
+
waits = 100 # 重置等待计数器
|
|
91
95
|
except Exception as e:
|
|
92
96
|
logger.error(f"异常或超时(30s)结束: {str(e)}")
|
|
93
97
|
break
|
|
94
98
|
else:
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
time.sleep(0.1)
|
|
100
|
+
# 记录状态等待30s、非记录状态等待3s
|
|
101
|
+
if self._recording:
|
|
102
|
+
waits -= 1
|
|
103
|
+
else:
|
|
104
|
+
waits -= 10
|
|
97
105
|
|
|
106
|
+
logger.info(f"数据记录完成:{self.file_path}")
|
|
98
107
|
self.close()
|
|
99
108
|
|
|
100
109
|
def _init_writer(self):
|
|
@@ -159,6 +168,9 @@ class EDFStreamWriter(Thread):
|
|
|
159
168
|
# 写入时转置为(样本数, 通道数)格式
|
|
160
169
|
self._writer.writeSamples(data_float64)
|
|
161
170
|
self._duration += 1
|
|
171
|
+
|
|
172
|
+
if self._duration % 10 == 0: # 每10秒打印一次进度
|
|
173
|
+
logger.info(f"数据记录中... 文件名:{self.file_path}, 已记录时长: {self._duration}秒")
|
|
162
174
|
|
|
163
175
|
# 用作数据结构一致化处理,通过调用公共类写入edf文件
|
|
164
176
|
# 入参包含写入edf的全部前置参数
|
|
@@ -243,6 +255,8 @@ class RscEDFHandler(object):
|
|
|
243
255
|
self._device_type = "LJ64S1"
|
|
244
256
|
elif device_type == 0x60:
|
|
245
257
|
self._device_type = "ARSKindling"
|
|
258
|
+
elif device_type == 0x339:
|
|
259
|
+
self._device_type = "C16R"
|
|
246
260
|
else:
|
|
247
261
|
self._device_type = device_type
|
|
248
262
|
|
|
@@ -264,17 +278,18 @@ class RscEDFHandler(object):
|
|
|
264
278
|
def write(self, packet: RscPacket):
|
|
265
279
|
# logger.trace(f"packet: {packet}")
|
|
266
280
|
if packet is None:
|
|
281
|
+
logger.info(f"收到结束信号,即将停止写入数据:{self.file_name}")
|
|
267
282
|
self._edf_writer_thread.stop_recording()
|
|
268
283
|
return
|
|
269
284
|
|
|
270
285
|
with self._lock:
|
|
271
286
|
if self.channels is None:
|
|
272
|
-
logger.
|
|
287
|
+
logger.debug(f"开始记录数据到文件...")
|
|
273
288
|
self.channels = packet.channels
|
|
274
289
|
self._first_pkg_id = packet.pkg_id if self._first_pkg_id is None else self._first_pkg_id
|
|
275
290
|
self._first_timestamp = packet.time_stamp if self._first_timestamp is None else self._first_timestamp
|
|
276
291
|
self._start_time = datetime.now()
|
|
277
|
-
logger.
|
|
292
|
+
logger.debug(f"第一个包id: {self._first_pkg_id }, 时间戳:{self._first_timestamp}, 当前时间:{datetime.now().timestamp()} offset: {datetime.now().timestamp() - self._first_timestamp}")
|
|
278
293
|
|
|
279
294
|
if self._last_pkg_id and self._last_pkg_id != packet.pkg_id - 1:
|
|
280
295
|
self._lost_packets += packet.pkg_id - self._last_pkg_id - 1
|
qlsdk/rsc/command/__init__.py
CHANGED
|
@@ -51,15 +51,29 @@ class DeviceCommand(abc.ABC):
|
|
|
51
51
|
device_id = int(self.device.device_id) if self.device and self.device.device_id else 0
|
|
52
52
|
device_type = int(self.device.device_type) if self.device and self.device.device_type else 0
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
#兼容设计
|
|
55
|
+
b_device_type = None
|
|
56
|
+
if device_type > 0xFF:
|
|
57
|
+
b_device_type = int.to_bytes(device_type, 2, 'little')
|
|
58
|
+
|
|
59
|
+
if b_device_type is None:
|
|
60
|
+
return (
|
|
61
|
+
DeviceCommand.HEADER_PREFIX
|
|
62
|
+
+ int(2).to_bytes(1, 'little') # pkgType
|
|
63
|
+
+ device_type.to_bytes(1, 'little')
|
|
64
|
+
+ device_id.to_bytes(4, 'little')
|
|
65
|
+
+ (DeviceCommand.HEADER_LEN + body_len + 2).to_bytes(4, 'little') # +1 for checksum
|
|
66
|
+
+ self.cmd_code.to_bytes(2, 'little')
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
return (
|
|
70
|
+
DeviceCommand.HEADER_PREFIX
|
|
71
|
+
+ b_device_type[0].to_bytes(1, 'little') # pkgType
|
|
72
|
+
+ b_device_type[1].to_bytes(1, 'little')
|
|
73
|
+
+ device_id.to_bytes(4, 'little')
|
|
74
|
+
+ (DeviceCommand.HEADER_LEN + body_len + 2).to_bytes(4, 'little') # +1 for checksum
|
|
75
|
+
+ self.cmd_code.to_bytes(2, 'little')
|
|
76
|
+
)
|
|
63
77
|
|
|
64
78
|
def unpack(self, payload: bytes) -> bytes:
|
|
65
79
|
"""解析消息体"""
|
|
@@ -70,7 +84,7 @@ class DeviceCommand(abc.ABC):
|
|
|
70
84
|
time = int.from_bytes(body[0:8], 'little')
|
|
71
85
|
# result - 1B
|
|
72
86
|
result = body[8]
|
|
73
|
-
logger.
|
|
87
|
+
logger.debug(f"[{time}]{self.cmd_desc}{'成功' if result == 0 else '失败'}")
|
|
74
88
|
|
|
75
89
|
|
|
76
90
|
|
|
@@ -294,18 +308,8 @@ class SignalDataCommand(DeviceCommand):
|
|
|
294
308
|
# 解析数据包
|
|
295
309
|
packet = RscPacket()
|
|
296
310
|
packet.transfer(body)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if self.device.edf_handler:
|
|
300
|
-
self.device.edf_handler.write(packet)
|
|
301
|
-
|
|
302
|
-
if len(self.device.signal_consumers) > 0 :
|
|
303
|
-
# 信号数字值转物理值
|
|
304
|
-
packet.eeg = self.device.eeg2phy(np.array(packet.eeg))
|
|
305
|
-
|
|
306
|
-
# 发送数据包到订阅者
|
|
307
|
-
for q in list(self.device.signal_consumers.values()):
|
|
308
|
-
q.put(packet)
|
|
311
|
+
# 将数据包传递给设备
|
|
312
|
+
self.device.produce(packet)
|
|
309
313
|
|
|
310
314
|
|
|
311
315
|
|
qlsdk/rsc/device/__init__.py
CHANGED
qlsdk/rsc/device/arskindling.py
CHANGED
|
@@ -167,7 +167,7 @@ class ARSKindling(QLBaseDevice):
|
|
|
167
167
|
self._edf_handler.set_device_type(self.device_type)
|
|
168
168
|
self._edf_handler.set_device_no(self.device_no)
|
|
169
169
|
self._edf_handler.set_storage_path(self._storage_path)
|
|
170
|
-
self._edf_handler.set_file_prefix(self._file_prefix)
|
|
170
|
+
self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'ARS')
|
|
171
171
|
|
|
172
172
|
@property
|
|
173
173
|
def edf_handler(self):
|
|
@@ -233,6 +233,8 @@ class ARSKindling(QLBaseDevice):
|
|
|
233
233
|
# 设置采集参数
|
|
234
234
|
def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
|
|
235
235
|
self._acq_param["original_channels"] = channels
|
|
236
|
+
|
|
237
|
+
# 根据映射关系做通道转换
|
|
236
238
|
for k in channels.keys():
|
|
237
239
|
if isinstance(channels[k], list):
|
|
238
240
|
temp = [k + str(i) for i in channels[k]]
|
|
@@ -241,7 +243,7 @@ class ARSKindling(QLBaseDevice):
|
|
|
241
243
|
channels[k] = [k + str(channels[k])]
|
|
242
244
|
|
|
243
245
|
|
|
244
|
-
|
|
246
|
+
# 更新采集参数
|
|
245
247
|
self._acq_param["channels"] = channels
|
|
246
248
|
self._acq_param["sample_rate"] = sample_rate
|
|
247
249
|
self._acq_param["sample_range"] = sample_range
|
qlsdk/rsc/device/base.py
CHANGED
|
@@ -5,12 +5,13 @@ from time import sleep, time_ns
|
|
|
5
5
|
from typing import Any, Dict, Literal
|
|
6
6
|
|
|
7
7
|
from loguru import logger
|
|
8
|
+
import numpy as np
|
|
9
|
+
from qlsdk.core.entity import RscPacket
|
|
8
10
|
from qlsdk.core.utils import to_bytes
|
|
9
11
|
from qlsdk.persist import RscEDFHandler
|
|
10
12
|
from qlsdk.rsc.interface import IDevice, IParser
|
|
11
13
|
from qlsdk.rsc.parser import TcpMessageParser
|
|
12
14
|
from qlsdk.rsc.command import StartImpedanceCommand, StopImpedanceCommand, StartStimulationCommand, StopStimulationCommand, SetAcquisitionParamCommand, StartAcquisitionCommand, StopAcquisitionCommand
|
|
13
|
-
# from qlsdk.rsc.command import *
|
|
14
15
|
|
|
15
16
|
class QLBaseDevice(IDevice):
|
|
16
17
|
def __init__(self, socket):
|
|
@@ -117,8 +118,8 @@ class QLBaseDevice(IDevice):
|
|
|
117
118
|
# 存储目录
|
|
118
119
|
|
|
119
120
|
#
|
|
120
|
-
self.
|
|
121
|
-
self.
|
|
121
|
+
self._signal_consumer: Dict[str, Queue[Any]]={}
|
|
122
|
+
self._impedance_consumer: Dict[str, Queue[Any]]={}
|
|
122
123
|
|
|
123
124
|
# EDF文件处理器
|
|
124
125
|
self._edf_handler = None
|
|
@@ -128,6 +129,38 @@ class QLBaseDevice(IDevice):
|
|
|
128
129
|
|
|
129
130
|
def parser(self) -> IParser:
|
|
130
131
|
return self._parser
|
|
132
|
+
|
|
133
|
+
# 数据包处理
|
|
134
|
+
def produce(self, data: RscPacket):
|
|
135
|
+
if data is None: return
|
|
136
|
+
|
|
137
|
+
# 处理信号数据
|
|
138
|
+
self._signal_wrapper(data)
|
|
139
|
+
|
|
140
|
+
# 存储
|
|
141
|
+
if self.storage_enable:
|
|
142
|
+
self._write_signal(data)
|
|
143
|
+
|
|
144
|
+
if len(self.signal_consumers) > 0 :
|
|
145
|
+
# 信号数字值转物理值
|
|
146
|
+
data.eeg = self.eeg2phy(np.array(data.eeg))
|
|
147
|
+
|
|
148
|
+
# 发送数据包到订阅者
|
|
149
|
+
for q in list(self.signal_consumers.values()):
|
|
150
|
+
q.put(data)
|
|
151
|
+
|
|
152
|
+
# 信号数据转换(默认不处理)
|
|
153
|
+
def _signal_wrapper(self, data: RscPacket):
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
def _write_signal(self, data: RscPacket):
|
|
157
|
+
# 文件写入到edf
|
|
158
|
+
if self._edf_handler is None:
|
|
159
|
+
logger.debug("Initializing EDF handler for data storage")
|
|
160
|
+
self.init_edf_handler()
|
|
161
|
+
|
|
162
|
+
if self._edf_handler:
|
|
163
|
+
self._edf_handler.write(data)
|
|
131
164
|
|
|
132
165
|
def start_listening(self):
|
|
133
166
|
|
|
@@ -142,7 +175,7 @@ class QLBaseDevice(IDevice):
|
|
|
142
175
|
return True
|
|
143
176
|
|
|
144
177
|
def stop_listening(self):
|
|
145
|
-
logger.
|
|
178
|
+
logger.trace(f"设备{self.device_no}停止socket监听")
|
|
146
179
|
self._listening = False
|
|
147
180
|
|
|
148
181
|
@property
|
|
@@ -152,15 +185,15 @@ class QLBaseDevice(IDevice):
|
|
|
152
185
|
def start_message_parser(self) -> None:
|
|
153
186
|
self._parser = TcpMessageParser(self)
|
|
154
187
|
self._parser.start()
|
|
155
|
-
logger.
|
|
188
|
+
logger.debug("TCP消息解析器已启动")
|
|
156
189
|
|
|
157
190
|
def start_message_listening(self) -> None:
|
|
158
191
|
def _accept():
|
|
159
192
|
while self._listening:
|
|
160
|
-
#
|
|
193
|
+
# 缓冲区4M
|
|
161
194
|
data = self.socket.recv(4096*1024)
|
|
162
195
|
if not data:
|
|
163
|
-
logger.warning(f"设备{self.device_name}连接结束")
|
|
196
|
+
logger.warning(f"设备[{self.device_name}]连接结束")
|
|
164
197
|
break
|
|
165
198
|
|
|
166
199
|
self._parser.append(data)
|
|
@@ -169,7 +202,7 @@ class QLBaseDevice(IDevice):
|
|
|
169
202
|
self._listening = True
|
|
170
203
|
message_accept = Thread(target=_accept, daemon=True)
|
|
171
204
|
message_accept.start()
|
|
172
|
-
logger.
|
|
205
|
+
logger.debug(f"socket消息监听已启动")
|
|
173
206
|
|
|
174
207
|
def set_device_type(self, type: int):
|
|
175
208
|
self._device_type = type
|
|
@@ -206,6 +239,7 @@ class QLBaseDevice(IDevice):
|
|
|
206
239
|
return self._digital_range
|
|
207
240
|
|
|
208
241
|
def init_edf_handler(self):
|
|
242
|
+
logger.warning("init_edf_handler not implemented in base class, should be overridden in subclass")
|
|
209
243
|
pass
|
|
210
244
|
# self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
211
245
|
# self._edf_handler.set_device_type(self.device_type)
|
|
@@ -221,6 +255,7 @@ class QLBaseDevice(IDevice):
|
|
|
221
255
|
@property
|
|
222
256
|
def edf_handler(self):
|
|
223
257
|
if not self.storage_enable:
|
|
258
|
+
logger.warning("EDF storage is disabled, no edf handler available")
|
|
224
259
|
return None
|
|
225
260
|
|
|
226
261
|
if self._edf_handler is None:
|
|
@@ -247,11 +282,11 @@ class QLBaseDevice(IDevice):
|
|
|
247
282
|
return 10
|
|
248
283
|
@property
|
|
249
284
|
def signal_consumers(self):
|
|
250
|
-
return self.
|
|
285
|
+
return self._signal_consumer
|
|
251
286
|
|
|
252
287
|
@property
|
|
253
288
|
def impedance_consumers(self):
|
|
254
|
-
return self.
|
|
289
|
+
return self._impedance_consumer
|
|
255
290
|
|
|
256
291
|
# 设置记录文件路径
|
|
257
292
|
def set_storage_path(self, path):
|
|
@@ -283,26 +318,26 @@ class QLBaseDevice(IDevice):
|
|
|
283
318
|
pass
|
|
284
319
|
|
|
285
320
|
def start_impedance(self):
|
|
286
|
-
logger.info("启动阻抗测量")
|
|
321
|
+
logger.info(f"[设备-{self.device_no}]启动阻抗测量")
|
|
287
322
|
msg = StartImpedanceCommand.build(self).pack()
|
|
288
|
-
logger.
|
|
323
|
+
logger.trace(f"start_impedance message is {msg.hex()}")
|
|
289
324
|
self.socket.sendall(msg)
|
|
290
325
|
|
|
291
326
|
def stop_impedance(self):
|
|
292
|
-
logger.info("停止阻抗测量")
|
|
327
|
+
logger.info(f"[设备{self.device_no}]停止阻抗测量")
|
|
293
328
|
msg = StopImpedanceCommand.build(self).pack()
|
|
294
|
-
logger.
|
|
329
|
+
logger.trace(f"stop_impedance message is {msg.hex()}")
|
|
295
330
|
self.socket.sendall(msg)
|
|
296
331
|
|
|
297
332
|
def start_stimulation(self):
|
|
298
333
|
if self.stim_paradigm is None:
|
|
299
334
|
logger.warning("刺激参数未设置,请先设置刺激参数")
|
|
300
335
|
return
|
|
301
|
-
logger.info("启动电刺激")
|
|
336
|
+
logger.info(f"[设备-{self.device_no}]启动电刺激")
|
|
302
337
|
msg = StartStimulationCommand.build(self).pack()
|
|
303
|
-
logger.
|
|
338
|
+
logger.trace(f"start_stimulation message is {msg.hex()}")
|
|
304
339
|
self.socket.sendall(msg)
|
|
305
|
-
t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,))
|
|
340
|
+
t = Thread(target=self._stop_stimulation_trigger, args=(self.stim_paradigm.duration,), daemon=True)
|
|
306
341
|
t.start()
|
|
307
342
|
|
|
308
343
|
def _stop_stimulation_trigger(self, duration):
|
|
@@ -310,67 +345,74 @@ class QLBaseDevice(IDevice):
|
|
|
310
345
|
while delay > 0:
|
|
311
346
|
sleep(1)
|
|
312
347
|
delay -= 1
|
|
313
|
-
logger.
|
|
348
|
+
logger.debug(f"_stop_stimulation_trigger duration: {duration}")
|
|
314
349
|
if self._edf_handler:
|
|
315
350
|
self._edf_handler.trigger("stimulation should be stopped")
|
|
316
351
|
else:
|
|
317
352
|
logger.warning("stop stim trigger fail. no edf writer alive")
|
|
318
353
|
|
|
319
354
|
def stop_stimulation(self):
|
|
320
|
-
logger.info("停止电刺激")
|
|
355
|
+
logger.info(f"[设备-{self.device_no}]停止电刺激")
|
|
321
356
|
msg = StopStimulationCommand.pack()
|
|
322
|
-
logger.
|
|
357
|
+
logger.trace(f"stop_stimulation message is {msg.hex()}")
|
|
323
358
|
self.socket.sendall(msg)
|
|
324
359
|
|
|
325
360
|
# 启动采集
|
|
326
361
|
def start_acquisition(self, recording = True):
|
|
327
|
-
logger.info("启动信号采集")
|
|
362
|
+
logger.info(f"[设备-{self.device_no}]启动信号采集")
|
|
328
363
|
self._recording = recording
|
|
364
|
+
# 初始化EDF处理器
|
|
365
|
+
self.init_edf_handler()
|
|
329
366
|
# 设置数据采集参数
|
|
330
367
|
param_bytes = SetAcquisitionParamCommand.build(self).pack()
|
|
331
368
|
# 启动数据采集
|
|
332
369
|
start_bytes = StartAcquisitionCommand.build(self).pack()
|
|
333
370
|
msg = param_bytes + start_bytes
|
|
334
|
-
logger.
|
|
371
|
+
logger.trace(f"start_acquisition message is {msg.hex()}")
|
|
335
372
|
self.socket.sendall(msg)
|
|
336
373
|
|
|
337
374
|
# 停止采集
|
|
338
375
|
def stop_acquisition(self):
|
|
339
|
-
logger.info("停止信号采集")
|
|
376
|
+
logger.info(f"[设备-{self.device_no}]停止信号采集")
|
|
340
377
|
msg = StopAcquisitionCommand.build(self).pack()
|
|
341
|
-
logger.
|
|
378
|
+
logger.trace(f"stop_acquisition message is {msg.hex()}")
|
|
342
379
|
self.socket.sendall(msg)
|
|
343
380
|
if self._edf_handler:
|
|
344
381
|
# 发送结束标识
|
|
345
382
|
self.edf_handler.write(None)
|
|
346
383
|
|
|
347
|
-
|
|
384
|
+
'''
|
|
385
|
+
订阅数据
|
|
386
|
+
topic: 订阅主题
|
|
387
|
+
q: 数据队列
|
|
388
|
+
type: 数据类型,signal-信号数据,impedance-阻抗数据
|
|
389
|
+
'''
|
|
348
390
|
def subscribe(self, topic:str=None, q : Queue=None, type : Literal["signal","impedance"]="signal"):
|
|
349
391
|
|
|
350
|
-
|
|
351
|
-
|
|
392
|
+
# 队列名称
|
|
393
|
+
if topic is None:
|
|
394
|
+
topic = f"{self.device_no}_{type}_{time_ns()}"
|
|
395
|
+
|
|
396
|
+
logger.debug(f"[设备-{self.device_no}]订阅数据流: {topic}, type: {type}")
|
|
397
|
+
|
|
352
398
|
# 数据队列
|
|
353
399
|
if q is None:
|
|
354
400
|
q = Queue(maxsize=1000)
|
|
355
|
-
|
|
356
|
-
# 队列名称
|
|
357
|
-
if topic is None:
|
|
358
|
-
topic = f"{type}_{time_ns()}"
|
|
359
401
|
|
|
360
402
|
# 订阅生理电信号数据
|
|
361
403
|
if type == "signal":
|
|
362
404
|
# topic唯一,用来区分不同的订阅队列(下同)
|
|
363
|
-
if topic in list(self.
|
|
364
|
-
logger.warning(f"
|
|
405
|
+
if topic in list(self._signal_consumer.keys()):
|
|
406
|
+
logger.warning(f"已存在主题[{topic}]的信号数据订阅!")
|
|
365
407
|
else:
|
|
366
|
-
self.
|
|
408
|
+
self._signal_consumer[topic] = q
|
|
367
409
|
|
|
368
410
|
# 订阅阻抗数据
|
|
369
411
|
if type == "impedance":
|
|
370
|
-
if topic in list(self.
|
|
371
|
-
logger.warning(f"
|
|
412
|
+
if topic in list(self._impedance_consumer.keys()):
|
|
413
|
+
logger.warning(f"已存在主题[{topic}]的阻抗数据订阅!")
|
|
372
414
|
else:
|
|
373
|
-
self.
|
|
415
|
+
self._impedance_consumer[topic] = q
|
|
374
416
|
|
|
375
417
|
return topic, q
|
|
376
418
|
|
|
@@ -378,7 +420,7 @@ class QLBaseDevice(IDevice):
|
|
|
378
420
|
if self._edf_handler:
|
|
379
421
|
self.edf_handler.trigger(desc)
|
|
380
422
|
else:
|
|
381
|
-
logger.warning("
|
|
423
|
+
logger.warning("没有开启文件记录时,无法记录trigger信息")
|
|
382
424
|
|
|
383
425
|
def gen_set_acquirement_param(self) -> bytes:
|
|
384
426
|
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
|
|
2
|
+
from multiprocessing import Queue
|
|
3
|
+
from threading import Thread
|
|
4
|
+
from time import sleep, time_ns
|
|
5
|
+
from typing import Any, Dict, Literal
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from qlsdk.persist import RscEDFHandler
|
|
9
|
+
from qlsdk.rsc.device.device_factory import DeviceFactory
|
|
10
|
+
from qlsdk.rsc.interface import IDevice
|
|
11
|
+
from qlsdk.rsc.command import *
|
|
12
|
+
from qlsdk.rsc.device.base import QLBaseDevice
|
|
13
|
+
|
|
14
|
+
'''
|
|
15
|
+
C16RS设备类,继承自QLBaseDevice
|
|
16
|
+
提供设备特定的属性和方法,包括设备类型、采集参数设置、电刺激参
|
|
17
|
+
'''
|
|
18
|
+
class C16RS(QLBaseDevice):
|
|
19
|
+
|
|
20
|
+
device_type = 0x339 # C16RS设备类型标识符
|
|
21
|
+
|
|
22
|
+
def __init__(self, socket):
|
|
23
|
+
|
|
24
|
+
super().__init__(socket)
|
|
25
|
+
# 存储通道反向映射位置值
|
|
26
|
+
self._reverse_ch_pos = None
|
|
27
|
+
self.channel_name_mapping = {
|
|
28
|
+
"FP1": 1,
|
|
29
|
+
"FP2": 2,
|
|
30
|
+
"C3": 3,
|
|
31
|
+
"C4": 4,
|
|
32
|
+
"O1": 5,
|
|
33
|
+
"O2": 6,
|
|
34
|
+
"CZ": 7,
|
|
35
|
+
"T7": 8,
|
|
36
|
+
"T8": 9,
|
|
37
|
+
"M1": 10,
|
|
38
|
+
"M2": 11,
|
|
39
|
+
"F3": 12,
|
|
40
|
+
"F4": 13,
|
|
41
|
+
"FZ": 14,
|
|
42
|
+
"F7": 15,
|
|
43
|
+
"F8": 16,
|
|
44
|
+
"PZ": 17,
|
|
45
|
+
"P3": 18,
|
|
46
|
+
"P7": 19,
|
|
47
|
+
"P4": 20,
|
|
48
|
+
"P8": 21
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# self.channel_mapping = {
|
|
52
|
+
# "1": 61,
|
|
53
|
+
# "2": 62,
|
|
54
|
+
# "3": 63,
|
|
55
|
+
# "4": 64,
|
|
56
|
+
# "5": 65,
|
|
57
|
+
# "6": 66,
|
|
58
|
+
# "7": 68,
|
|
59
|
+
# "8": 67,
|
|
60
|
+
# "9": 53,
|
|
61
|
+
# "10": 54,
|
|
62
|
+
# "11": 55,
|
|
63
|
+
# "12": 57,
|
|
64
|
+
# "13": 59,
|
|
65
|
+
# "14": 58,
|
|
66
|
+
# "15": 60,
|
|
67
|
+
# "16": 56,
|
|
68
|
+
# "17": 26,
|
|
69
|
+
# "18": 25,
|
|
70
|
+
# "19": 24,
|
|
71
|
+
# "20": 23,
|
|
72
|
+
# "21": 22
|
|
73
|
+
# }
|
|
74
|
+
|
|
75
|
+
self.channel_mapping = {
|
|
76
|
+
"1": 2,
|
|
77
|
+
"2": 3,
|
|
78
|
+
"3": 27,
|
|
79
|
+
"4": 31,
|
|
80
|
+
"5": 60,
|
|
81
|
+
"6": 61,
|
|
82
|
+
"7": 25,
|
|
83
|
+
"8": 29,
|
|
84
|
+
"9": 33,
|
|
85
|
+
"10": 62,
|
|
86
|
+
"11": 63,
|
|
87
|
+
"12": 10,
|
|
88
|
+
"13": 14,
|
|
89
|
+
"14": 8,
|
|
90
|
+
"15": 12,
|
|
91
|
+
"16": 16,
|
|
92
|
+
"17": 43,
|
|
93
|
+
"18": 45,
|
|
94
|
+
"19": 47,
|
|
95
|
+
"20": 49,
|
|
96
|
+
"21": 51
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
self.channel_display_mapping = {
|
|
100
|
+
2: 1,
|
|
101
|
+
3: 2,
|
|
102
|
+
27: 3,
|
|
103
|
+
31: 4,
|
|
104
|
+
60: 5,
|
|
105
|
+
61: 6,
|
|
106
|
+
25: 7,
|
|
107
|
+
29: 8,
|
|
108
|
+
33: 9,
|
|
109
|
+
62: 10,
|
|
110
|
+
63: 11,
|
|
111
|
+
10: 12,
|
|
112
|
+
14: 13,
|
|
113
|
+
8: 14,
|
|
114
|
+
12: 15,
|
|
115
|
+
16: 16,
|
|
116
|
+
43: 17,
|
|
117
|
+
45: 18,
|
|
118
|
+
47: 19,
|
|
119
|
+
49: 20,
|
|
120
|
+
51: 21
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def from_parent(cls, parent:IDevice) -> IDevice:
|
|
126
|
+
rlt = cls(parent.socket)
|
|
127
|
+
rlt.device_id = parent.device_id
|
|
128
|
+
rlt._device_no = parent.device_no
|
|
129
|
+
return rlt
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def init_edf_handler(self):
|
|
133
|
+
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
134
|
+
self._edf_handler.set_device_type(self.device_type)
|
|
135
|
+
self._edf_handler.set_device_no(self.device_no)
|
|
136
|
+
self._edf_handler.set_storage_path(self._storage_path)
|
|
137
|
+
self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'C16R')
|
|
138
|
+
|
|
139
|
+
# 设置刺激参数
|
|
140
|
+
def set_stim_param(self, param):
|
|
141
|
+
self.stim_paradigm = param
|
|
142
|
+
|
|
143
|
+
# 设置采集参数
|
|
144
|
+
def set_acq_param(self, channels, sample_rate = 500, sample_range = 188):
|
|
145
|
+
# 保存原始通道参数
|
|
146
|
+
self._acq_param["original_channels"] = channels
|
|
147
|
+
|
|
148
|
+
# 名称转换为数字通道
|
|
149
|
+
channels = [self.channel_name_mapping.get(str(i).upper(), i) for i in channels]
|
|
150
|
+
|
|
151
|
+
# 根据映射关系做通道转换-没有映射的默认到第一个通道
|
|
152
|
+
# 先设置不存在的通道为-1,再把-1替换为第一个通道,避免第一个通道也不合法的情况
|
|
153
|
+
channels = [self.channel_mapping.get(str(i), -1) for i in channels]
|
|
154
|
+
channels = [i if i != -1 else channels[0] for i in channels]
|
|
155
|
+
|
|
156
|
+
# 更新采集参数
|
|
157
|
+
self._acq_param["channels"] = channels
|
|
158
|
+
self._acq_param["sample_rate"] = sample_rate
|
|
159
|
+
self._acq_param["sample_range"] = sample_range
|
|
160
|
+
self._acq_channels = channels
|
|
161
|
+
self._sample_rate = sample_rate
|
|
162
|
+
self._sample_range = sample_range
|
|
163
|
+
|
|
164
|
+
logger.debug(f"C16RS: set_acq_param: {self._acq_param}")
|
|
165
|
+
|
|
166
|
+
# 参数改变后,重置通道位置映射
|
|
167
|
+
self._reverse_ch_pos = None
|
|
168
|
+
|
|
169
|
+
# 信号数据转换(默认不处理)
|
|
170
|
+
def _signal_wrapper(self, data: RscPacket):
|
|
171
|
+
if data is None:
|
|
172
|
+
return
|
|
173
|
+
# 根据映射关系做通道转换-(注意数据和通道的一致性)
|
|
174
|
+
# data.channels = [self.channel_display_mapping.get(i, i) for i in data.channels]
|
|
175
|
+
|
|
176
|
+
# 升级为类变量,减少计算
|
|
177
|
+
if self._reverse_ch_pos is None:
|
|
178
|
+
self._reverse_ch_pos = map_indices(self._acq_param["channels"], data.channels)
|
|
179
|
+
|
|
180
|
+
# 更新通道(数据)顺序和输入一致
|
|
181
|
+
data.channels = self._acq_param["original_channels"]
|
|
182
|
+
data.eeg = [data.eeg[i] for i in self._reverse_ch_pos]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def map_indices(A, B):
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
参数:
|
|
189
|
+
A: 源数组(无重复值)
|
|
190
|
+
B: 目标数组(无重复值)
|
|
191
|
+
|
|
192
|
+
返回:
|
|
193
|
+
C: 与A长度相同的数组,元素为A中对应值在B中的索引(不存在则为-1)
|
|
194
|
+
"""
|
|
195
|
+
# 创建B的值到索引的映射字典(O(n)操作)
|
|
196
|
+
b_map = {value: idx for idx, value in enumerate(B)}
|
|
197
|
+
|
|
198
|
+
# 遍历A,获取每个元素在B中的位置(O(m)操作)
|
|
199
|
+
return [b_map.get(a, -1) for a in A]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# Register the C16RS device with the DeviceFactory
|
|
203
|
+
DeviceFactory.register(C16RS.device_type, C16RS)
|
|
204
|
+
|
|
205
|
+
|
qlsdk/rsc/device/c256_rs.py
CHANGED
qlsdk/rsc/device/c64_rs.py
CHANGED
|
@@ -16,6 +16,7 @@ class C64RS(QLBaseDevice):
|
|
|
16
16
|
device_type = 0x39 # C64RS设备类型标识符
|
|
17
17
|
|
|
18
18
|
def __init__(self, socket):
|
|
19
|
+
super().__init__(socket)
|
|
19
20
|
self.socket = socket
|
|
20
21
|
|
|
21
22
|
self._id = None
|
|
@@ -145,9 +146,9 @@ class C64RS(QLBaseDevice):
|
|
|
145
146
|
def init_edf_handler(self):
|
|
146
147
|
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
147
148
|
self._edf_handler.set_device_type(self.device_type)
|
|
148
|
-
self._edf_handler.set_device_no(self.
|
|
149
|
+
self._edf_handler.set_device_no(self.device_no)
|
|
149
150
|
self._edf_handler.set_storage_path(self._storage_path)
|
|
150
|
-
self._edf_handler.set_file_prefix(self._file_prefix)
|
|
151
|
+
self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'C64RS')
|
|
151
152
|
|
|
152
153
|
@property
|
|
153
154
|
def edf_handler(self):
|
qlsdk/rsc/device/c64s1.py
CHANGED
|
@@ -16,6 +16,7 @@ class C64S1(QLBaseDevice):
|
|
|
16
16
|
device_type = 0x40 # C64RS设备类型标识符
|
|
17
17
|
|
|
18
18
|
def __init__(self, socket):
|
|
19
|
+
super().__init__(socket)
|
|
19
20
|
self.socket = socket
|
|
20
21
|
|
|
21
22
|
self._id = None
|
|
@@ -151,9 +152,9 @@ class C64S1(QLBaseDevice):
|
|
|
151
152
|
def init_edf_handler(self):
|
|
152
153
|
self._edf_handler = RscEDFHandler(self.sample_rate, self.sample_range * 1000 , - self.sample_range * 1000, self.resolution)
|
|
153
154
|
self._edf_handler.set_device_type(self.device_type)
|
|
154
|
-
self._edf_handler.set_device_no(self.
|
|
155
|
+
self._edf_handler.set_device_no(self.device_no)
|
|
155
156
|
self._edf_handler.set_storage_path(self._storage_path)
|
|
156
|
-
self._edf_handler.set_file_prefix(self._file_prefix)
|
|
157
|
+
self._edf_handler.set_file_prefix(self._file_prefix if self._file_prefix else 'C64S1')
|
|
157
158
|
|
|
158
159
|
@property
|
|
159
160
|
def edf_handler(self):
|
|
@@ -26,6 +26,7 @@ class DeviceFactory(object):
|
|
|
26
26
|
|
|
27
27
|
# Register the C64RS device with the DeviceFactory
|
|
28
28
|
DeviceFactory.register(C64RS.device_type, C64RS)
|
|
29
|
+
# Register the ARSKindling device with the DeviceFactory
|
|
29
30
|
DeviceFactory.register(ARSKindling.device_type, ARSKindling)
|
|
30
31
|
|
|
31
32
|
|
qlsdk/rsc/interface/device.py
CHANGED
qlsdk/rsc/manager/container.py
CHANGED
|
@@ -33,15 +33,15 @@ class DeviceContainer(object):
|
|
|
33
33
|
等待设备连接
|
|
34
34
|
'''
|
|
35
35
|
def connect(self, device_id: str, timeout:int=30) -> Optional[IDevice]:
|
|
36
|
-
logger.info(f"
|
|
36
|
+
logger.info(f"开始搜索设备: {device_id}")
|
|
37
37
|
self.add_search(device_id)
|
|
38
38
|
for s in range(timeout):
|
|
39
39
|
device = self.get_device(device_id)
|
|
40
40
|
if device:
|
|
41
|
-
logger.success(f"
|
|
41
|
+
logger.success(f"设备[{device_id}]已连接成功。")
|
|
42
42
|
return device
|
|
43
43
|
time.sleep(1)
|
|
44
|
-
logger.
|
|
44
|
+
logger.warning(f"在{timeout}内未搜索到设备:{device_id}")
|
|
45
45
|
return None
|
|
46
46
|
|
|
47
47
|
def _listening(self):
|
|
@@ -55,15 +55,16 @@ class DeviceContainer(object):
|
|
|
55
55
|
|
|
56
56
|
while True:
|
|
57
57
|
client_socket, addr = tcp_socket.accept()
|
|
58
|
-
logger.info(f"接收到来自
|
|
58
|
+
logger.info(f"接收到来自[{addr[0]}:{addr[1]}]的连接,待确认设备类型...")
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
# 为每个新连接创建线程处理
|
|
62
62
|
client_handler = Thread(
|
|
63
63
|
target=self.client_handler,
|
|
64
|
-
args=(client_socket,)
|
|
64
|
+
args=(client_socket,),
|
|
65
|
+
daemon=True
|
|
65
66
|
)
|
|
66
|
-
|
|
67
|
+
|
|
67
68
|
client_handler.start()
|
|
68
69
|
|
|
69
70
|
except KeyboardInterrupt:
|
|
@@ -84,18 +85,16 @@ class DeviceContainer(object):
|
|
|
84
85
|
device.start_listening()
|
|
85
86
|
# GET_DEVICE_INFO
|
|
86
87
|
msg = GetDeviceInfoCommand.build(device).pack()
|
|
87
|
-
logger.
|
|
88
|
+
logger.trace(f"发送获取设备信息命令: {msg.hex()}")
|
|
88
89
|
device.send(msg)
|
|
89
|
-
logger.info(f"base device {dir(device)}")
|
|
90
90
|
# 添加设备
|
|
91
91
|
while True:
|
|
92
92
|
if device.device_name:
|
|
93
|
-
logger.info(f"设备 {device.device_name} 已连接")
|
|
94
93
|
real_device = DeviceFactory.create_device(device)
|
|
95
94
|
device.stop_listening()
|
|
96
95
|
real_device.start_listening()
|
|
97
96
|
self.add_device(real_device)
|
|
98
|
-
logger.info(f"
|
|
97
|
+
logger.info(f"设备[{device.device_name}]已连接,设备类型为:{hex(real_device.device_type)}")
|
|
99
98
|
break
|
|
100
99
|
|
|
101
100
|
|
|
@@ -107,20 +106,20 @@ class DeviceContainer(object):
|
|
|
107
106
|
if device is None or device.device_no is None:
|
|
108
107
|
logger.warning("无效的设备")
|
|
109
108
|
|
|
110
|
-
self._devices
|
|
111
|
-
logger.
|
|
109
|
+
self._devices[device.device_no] = device
|
|
110
|
+
logger.info(f"添加设备[{device.device_no}]到设备列表,已连接设备数量:{len(self._devices)}")
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
# 标记设备为已连接
|
|
113
|
+
self._broadcaster.mark_device_as_connected(device.device_no)
|
|
115
114
|
|
|
116
|
-
def get_device(self,
|
|
117
|
-
logger.
|
|
115
|
+
def get_device(self, device_no:str=None)->IDevice:
|
|
116
|
+
logger.info(f"已连接设备数量:{len(self._devices)}")
|
|
118
117
|
if len(self._devices) == 0:
|
|
119
118
|
return None
|
|
120
119
|
|
|
121
120
|
# 未指定device_id,返回第一个设备
|
|
122
|
-
if
|
|
121
|
+
if device_no is None:
|
|
123
122
|
return list(self._devices.values())[0]
|
|
124
123
|
|
|
125
|
-
return self._devices.get(
|
|
124
|
+
return self._devices.get(device_no, None)
|
|
126
125
|
|
qlsdk/rsc/network/discover.py
CHANGED
|
@@ -5,6 +5,11 @@ from loguru import logger
|
|
|
5
5
|
|
|
6
6
|
from qlsdk.core.message import UDPMessage
|
|
7
7
|
|
|
8
|
+
'''
|
|
9
|
+
广播器类,用于发送和接收设备广播消息
|
|
10
|
+
主要功能:发送设备搜索消息,接收设备连接消息
|
|
11
|
+
注意:广播端口需要和ar4sdk做区分,使用54366时不能和x8同时使用
|
|
12
|
+
'''
|
|
8
13
|
class UdpBroadcaster:
|
|
9
14
|
# 广播端口需要和ar4sdk做区分, 使用54366时不能和x8同时使用
|
|
10
15
|
def __init__(self, port=54366):
|
|
@@ -18,70 +23,54 @@ class UdpBroadcaster:
|
|
|
18
23
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
19
24
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
20
25
|
|
|
26
|
+
# 添加设备序列号到待广播列表
|
|
21
27
|
def add_device(self, device_id):
|
|
22
|
-
|
|
28
|
+
|
|
23
29
|
with self.lock:
|
|
24
30
|
if device_id not in self.devices_to_broadcast:
|
|
25
31
|
self.devices_to_broadcast.append(device_id)
|
|
26
|
-
logger.info(f"
|
|
32
|
+
logger.info(f"添加设备[{device_id}]到搜索列表。")
|
|
27
33
|
|
|
34
|
+
# 从待广播列表移除设备序列号
|
|
28
35
|
def remove_device(self, device_id):
|
|
29
|
-
|
|
36
|
+
|
|
30
37
|
with self.lock:
|
|
31
38
|
if device_id in self.devices_to_broadcast:
|
|
32
39
|
self.devices_to_broadcast.remove(device_id)
|
|
33
|
-
logger.info(f"
|
|
40
|
+
logger.info(f"把设备[{device_id}]从搜索列表中移除。")
|
|
34
41
|
|
|
42
|
+
# 把设备标记为已连接
|
|
35
43
|
def mark_device_as_connected(self, device_id):
|
|
36
|
-
"""将设备标记为已连接,并从未广播列表中移除"""
|
|
37
44
|
with self.lock:
|
|
45
|
+
# 如果设备已连接,则从搜索列表中移除
|
|
38
46
|
if device_id in self.devices_to_broadcast:
|
|
39
47
|
self.devices_to_broadcast.remove(device_id)
|
|
48
|
+
|
|
49
|
+
# 添加到已连接设备集合
|
|
40
50
|
self.connected_devices.add(device_id)
|
|
41
|
-
|
|
51
|
+
|
|
52
|
+
logger.info(f"设备[{device_id}]已连接,从搜索列表中移除。")
|
|
42
53
|
|
|
54
|
+
# 广播设备信息,寻求配对
|
|
43
55
|
def broadcast_devices(self):
|
|
44
|
-
|
|
56
|
+
|
|
45
57
|
while self.running:
|
|
46
58
|
with self.lock:
|
|
47
59
|
for device_id in self.devices_to_broadcast:
|
|
48
60
|
message = UDPMessage.search(device_id)
|
|
49
61
|
self.sock.sendto(message, ('<broadcast>', self.broadcast_port))
|
|
50
|
-
logger.
|
|
51
|
-
|
|
62
|
+
logger.debug(f"设备[{device_id}]广播消息已发送。")
|
|
63
|
+
|
|
64
|
+
# 每隔1秒发送一次广播
|
|
65
|
+
time.sleep(1)
|
|
52
66
|
|
|
53
67
|
def start(self):
|
|
54
68
|
"""启动广播线程"""
|
|
55
|
-
self.broadcast_thread = Thread(target=self.broadcast_devices)
|
|
56
|
-
self.broadcast_thread.setDaemon(True)
|
|
69
|
+
self.broadcast_thread = Thread(target=self.broadcast_devices, daemon=True)
|
|
57
70
|
self.broadcast_thread.start()
|
|
58
71
|
|
|
59
72
|
def stop(self):
|
|
60
73
|
"""停止广播"""
|
|
61
74
|
self.running = False
|
|
62
75
|
self.broadcast_thread.join()
|
|
63
|
-
self.sock.close()
|
|
64
|
-
|
|
65
|
-
# 示例使用
|
|
66
|
-
if __name__ == "__main__":
|
|
67
|
-
broadcaster = UdpBroadcaster()
|
|
68
|
-
|
|
69
|
-
# 添加设备序列号到待广播列表
|
|
70
|
-
broadcaster.add_device_to_broadcast("390024130032")
|
|
71
|
-
|
|
72
|
-
# 启动广播
|
|
73
|
-
broadcaster.start()
|
|
74
|
-
|
|
75
|
-
try:
|
|
76
|
-
# 模拟运行一段时间
|
|
77
|
-
time.sleep(10)
|
|
78
|
-
|
|
79
|
-
# 标记设备为已连接
|
|
80
|
-
broadcaster.mark_device_as_connected("390024130032")
|
|
81
|
-
|
|
82
|
-
# 继续运行一段时间
|
|
83
|
-
time.sleep(10)
|
|
84
|
-
|
|
85
|
-
finally:
|
|
86
|
-
# 停止广播
|
|
87
|
-
broadcaster.stop()
|
|
76
|
+
self.sock.close()
|
qlsdk/rsc/parser/base.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
from multiprocessing import Lock
|
|
3
|
+
import time
|
|
1
4
|
from qlsdk.rsc.interface import IDevice, IParser
|
|
2
5
|
|
|
3
6
|
from loguru import logger
|
|
@@ -11,7 +14,13 @@ class TcpMessageParser(IParser):
|
|
|
11
14
|
self.device = device
|
|
12
15
|
self.running = False
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
# 网络实时数据缓存,选用BytesIO
|
|
18
|
+
# 临时缓冲区-用于接收数据
|
|
19
|
+
self.cache = BytesIO()
|
|
20
|
+
# 缓冲区-用于处理数据
|
|
21
|
+
self.buffer = BytesIO()
|
|
22
|
+
# 读写锁-用于临时缓冲区(避免读写冲突)
|
|
23
|
+
self._lock = Lock()
|
|
15
24
|
|
|
16
25
|
@property
|
|
17
26
|
def header(self):
|
|
@@ -28,42 +37,114 @@ class TcpMessageParser(IParser):
|
|
|
28
37
|
def set_device(self, device):
|
|
29
38
|
self.device = device
|
|
30
39
|
|
|
31
|
-
def append(self,
|
|
32
|
-
self.cache
|
|
33
|
-
|
|
40
|
+
def append(self, value):
|
|
41
|
+
# self.cache.write(buffer)
|
|
42
|
+
with self._lock:
|
|
43
|
+
self.cache.write(value)
|
|
34
44
|
|
|
35
45
|
def __parser__(self):
|
|
36
|
-
logger.
|
|
46
|
+
logger.trace("数据解析开始")
|
|
47
|
+
|
|
48
|
+
# 告警阈值(10M)
|
|
49
|
+
warn_len = 10 * 1024 * 1024
|
|
50
|
+
|
|
37
51
|
while self.running:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
buf_len = get_len(self.buffer)
|
|
53
|
+
|
|
54
|
+
# logger.info(f"当前操作区缓存长度: {buf_len}, 缓存内容: {self.buffer.getvalue().hex()}")
|
|
55
|
+
if buf_len < self.header_len:
|
|
56
|
+
# logger.trace(f"操作区缓存数据不足: {len}, 等待数据...")
|
|
57
|
+
if not self.__fill_from_cache():
|
|
58
|
+
time.sleep(0.05)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if buf_len > warn_len:
|
|
62
|
+
logger.warning(f"操作区缓存数据过大: {buf_len} bytes, 可能存在数据丢失风险")
|
|
63
|
+
|
|
64
|
+
start_pos = self.buffer.tell()
|
|
65
|
+
# logger.info(f"当前缓存位置: {start_pos}")
|
|
66
|
+
head = self.buffer.read(2)
|
|
67
|
+
# logger.info(f'当前缓存头部: {head.hex()}')
|
|
68
|
+
if head != self.header:
|
|
69
|
+
logger.debug(f"数据包头部不匹配: {head.hex()}, 期望: {self.header.hex()},继续查找...")
|
|
70
|
+
self.buffer.seek(start_pos + 1) # 移动到下一个字节
|
|
47
71
|
continue
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
|
|
73
|
+
# 移动下标(指向包长度的位置)
|
|
74
|
+
self.buffer.seek(start_pos + 8)
|
|
75
|
+
# 包总长度
|
|
76
|
+
pkg_len = int.from_bytes(self.buffer.read(4), 'little')
|
|
77
|
+
# logger.trace(f" cache len: {len(self.cache)}, pkg_len len: {len(self.cache)}")
|
|
78
|
+
|
|
79
|
+
buf_len = get_len(self.buffer)
|
|
80
|
+
# 直接等待长度足够(如果从头开始判断,因为逻辑相同,所以会执行一样的操作)
|
|
81
|
+
while buf_len < pkg_len:
|
|
82
|
+
if self.__fill_from_cache():
|
|
83
|
+
continue
|
|
84
|
+
else:
|
|
85
|
+
time.sleep(0.05)
|
|
86
|
+
|
|
87
|
+
# 读取剩余数据
|
|
88
|
+
self.buffer.seek(pkg_len)
|
|
89
|
+
tmp = self.buffer.read()
|
|
90
|
+
|
|
91
|
+
# 读取当前数据包
|
|
92
|
+
self.buffer.seek(start_pos)
|
|
93
|
+
pkg = self.buffer.read(pkg_len)
|
|
94
|
+
|
|
95
|
+
# 清空操作区缓存(truncate会保留内存,重新初始化)
|
|
96
|
+
self.buffer = BytesIO()
|
|
97
|
+
if len(tmp) > 0:
|
|
98
|
+
self.buffer.write(tmp)
|
|
99
|
+
self.buffer.seek(0)
|
|
100
|
+
|
|
50
101
|
self.unpack(pkg)
|
|
51
102
|
|
|
103
|
+
# 填充操作区缓存
|
|
104
|
+
def __fill_from_cache(self) -> bool:
|
|
105
|
+
result = False
|
|
106
|
+
|
|
107
|
+
cur_pos = self.buffer.tell()
|
|
108
|
+
# 移动到操作区缓存末尾,内容追加到缓冲区尾部
|
|
109
|
+
self.buffer.seek(0,2)
|
|
110
|
+
# 操作缓冲区
|
|
111
|
+
with self._lock:
|
|
112
|
+
self.cache.seek(0, 2)
|
|
113
|
+
|
|
114
|
+
# 临时缓冲区只要有数据,就写入操作缓冲区(避免分片传输导致数据不完整)
|
|
115
|
+
if self.cache.tell() > 0:
|
|
116
|
+
self.buffer.write(self.cache.getvalue())
|
|
117
|
+
self.cache = BytesIO() # 清空缓冲区
|
|
118
|
+
result = True
|
|
119
|
+
|
|
120
|
+
self.buffer.seek(cur_pos) # 恢复到原位置
|
|
121
|
+
|
|
122
|
+
return result
|
|
123
|
+
|
|
52
124
|
def unpack(self, packet):
|
|
53
125
|
# 提取指令码
|
|
54
126
|
cmd_code = int.from_bytes(packet[self.cmd_pos : self.cmd_pos + 2], 'little')
|
|
55
127
|
cmd_class = CommandFactory.create_command(cmd_code)
|
|
56
|
-
logger.
|
|
128
|
+
# logger.trace(f"收到指令:{cmd_class.cmd_desc}[{hex(cmd_code)}]")
|
|
57
129
|
instance = cmd_class(self.device)
|
|
58
130
|
start = time_ns()
|
|
59
|
-
logger.
|
|
131
|
+
# logger.trace(f"开始解析: {start}")
|
|
60
132
|
instance.parse_body(packet[self.header_len:-2])
|
|
61
|
-
logger.
|
|
133
|
+
# logger.trace(f"解析完成:{time_ns()}, 解析耗时:{time_ns() - start}ns")
|
|
62
134
|
return instance
|
|
63
135
|
|
|
64
136
|
def start(self):
|
|
65
137
|
self.running = True
|
|
66
|
-
parser = Thread(target=self.__parser__,)
|
|
67
|
-
parser.daemon = True
|
|
138
|
+
parser = Thread(target=self.__parser__, daemon=True)
|
|
68
139
|
parser.start()
|
|
69
|
-
|
|
140
|
+
|
|
141
|
+
# 工具方法
|
|
142
|
+
def get_len(buf: BytesIO) -> int:
|
|
143
|
+
if buf is None:
|
|
144
|
+
return 0
|
|
145
|
+
cur_pos = buf.tell()
|
|
146
|
+
buf.seek(0, 2) # 移动到操作区缓存末尾
|
|
147
|
+
len = buf.tell()
|
|
148
|
+
buf.seek(cur_pos) # 恢复到原位置
|
|
149
|
+
return len
|
|
150
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: qlsdk2
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: SDK for quanlan device
|
|
5
|
+
Home-page: https://github.com/hehuajun/qlsdk
|
|
6
|
+
Author: hehuajun
|
|
7
|
+
Author-email: hehuajun@eegion.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: loguru>=0.6.0
|
|
14
|
+
Requires-Dist: numpy>=1.23.5
|
|
15
|
+
Requires-Dist: bitarray>=1.5.3
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
18
|
+
Requires-Dist: twine>=3.0; extra == "dev"
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: requires-dist
|
|
26
|
+
Dynamic: requires-python
|
|
27
|
+
Dynamic: summary
|
|
28
|
+
|
|
29
|
+
版本:v0.5.0
|
|
30
|
+
时间:2025-07-29
|
|
31
|
+
[新特性]
|
|
32
|
+
1. C16R设备搜索
|
|
33
|
+
2、C16R设备连接
|
|
34
|
+
3、C16R信号采集/停止
|
|
35
|
+
4、C16R数据自动记录到文件
|
|
36
|
+
5、C16R采集通道设置支持数字和名称两种模式(可混用)
|
|
37
|
+
|
|
38
|
+
[优化]
|
|
39
|
+
1、提升信号接收及指令解析性能
|
|
40
|
+
2、日志级别及文案优化
|
|
@@ -20,7 +20,7 @@ qlsdk/core/network/monitor.py,sha256=QqjjPwSr1kgqDTTySp5bpalZmsBQTaAWSxrfPLdROZo
|
|
|
20
20
|
qlsdk/persist/__init__.py,sha256=b8qk1aOU6snEMCQNYDl1ijV3-2gwBmMt76fiAzNk1E8,107
|
|
21
21
|
qlsdk/persist/ars_edf.py,sha256=_pYtHqucB-utMw-xUXZc9IB8_8ThbLFpTl_-WBQR-Sc,10555
|
|
22
22
|
qlsdk/persist/edf.py,sha256=ETngb86CfkIUJYWmw86QR445MvTFC7Edk_CH9nyNgtY,7857
|
|
23
|
-
qlsdk/persist/rsc_edf.py,sha256=
|
|
23
|
+
qlsdk/persist/rsc_edf.py,sha256=wV3akdwzEihDAyR9DtmO0jNdLH1jEvx34ghJQtrfk2k,12578
|
|
24
24
|
qlsdk/persist/stream.py,sha256=TCVF1sqDrHiYBsJC27At66AaCs-_blXeXA_WXdJiIVA,5828
|
|
25
25
|
qlsdk/rsc/__init__.py,sha256=hOMiN0eYn4jYo7O4_0IPlQT0hD15SqqCQUihOVlTZvs,269
|
|
26
26
|
qlsdk/rsc/device_manager.py,sha256=1ucd-lzHkNeQPKPzXV6OBkAMqPp_vOcsLyS-9TJ7wRc,4448
|
|
@@ -29,27 +29,28 @@ qlsdk/rsc/eegion.py,sha256=lxrktO-3Z_MYdFIwc4NxvgLM5AL5kU3UItjH6tsKmHY,11670
|
|
|
29
29
|
qlsdk/rsc/entity.py,sha256=-fRWFkVWp9d8Y1uh6GiacXC5scdeEKNiNFf3aziGdCE,17751
|
|
30
30
|
qlsdk/rsc/paradigm.py,sha256=DGfwY36sMdPIMRjbGo661GvUTEwsRRi3jrmG405mSTk,12840
|
|
31
31
|
qlsdk/rsc/proxy.py,sha256=9CPdGNGWremwBUh4GvlXAykYB-x_BEPPLqsNvwuwIDE,2736
|
|
32
|
-
qlsdk/rsc/command/__init__.py,sha256=
|
|
32
|
+
qlsdk/rsc/command/__init__.py,sha256=FpumdCRV3aSnCvLT7MceMb7lF8WgGbdi_w0L8wX7ryg,12137
|
|
33
33
|
qlsdk/rsc/command/message.py,sha256=nTdG-Vp4MBnltyrgedAWiKD6kzOaPrg58Z_hq6yjhys,12220
|
|
34
|
-
qlsdk/rsc/device/__init__.py,sha256=
|
|
35
|
-
qlsdk/rsc/device/arskindling.py,sha256=
|
|
36
|
-
qlsdk/rsc/device/base.py,sha256=
|
|
37
|
-
qlsdk/rsc/device/
|
|
38
|
-
qlsdk/rsc/device/
|
|
39
|
-
qlsdk/rsc/device/
|
|
40
|
-
qlsdk/rsc/device/
|
|
34
|
+
qlsdk/rsc/device/__init__.py,sha256=xtTXLT9QFKtb-qS-A8-ewSxJ3zXgImFCX0OoAPw6hHE,185
|
|
35
|
+
qlsdk/rsc/device/arskindling.py,sha256=owci6MEGjyWqohEXzPdKj_ESeVIZKgO53StVj6Tmi18,15002
|
|
36
|
+
qlsdk/rsc/device/base.py,sha256=6cMaACVVgZ7oKdqbyVRl3B32M9Em6_jjr-FUuxRW6Ys,17404
|
|
37
|
+
qlsdk/rsc/device/c16_rs.py,sha256=IpJn4hBFHg67lvWkl8kAAztdZYsgi2-njFuxMi2SsHQ,6390
|
|
38
|
+
qlsdk/rsc/device/c256_rs.py,sha256=K1XmLqZpvHTAfCm_dr2VsGxHc67aJQVDV1cI41a1WTI,13955
|
|
39
|
+
qlsdk/rsc/device/c64_rs.py,sha256=cZIioIRGgd4Ub0ieho4_XujBNo8AQgJEjXcqgcEkyFQ,13644
|
|
40
|
+
qlsdk/rsc/device/c64s1.py,sha256=L7nKmsoMCGj6GMjHYfYkKgkBtrGfP516kQHQ5I1FAUE,13986
|
|
41
|
+
qlsdk/rsc/device/device_factory.py,sha256=P8nNDB2qk0kbu4OMYtEZMKSdXWp-7fLDzuNyR1Thf8Q,1315
|
|
41
42
|
qlsdk/rsc/interface/__init__.py,sha256=xeRzIlQSB7ZSf4r5kLfH5cDQLzCyWeJAReG8Xq5nOE0,70
|
|
42
43
|
qlsdk/rsc/interface/command.py,sha256=1s5Lxb_ejsd-JNvKMqU2aFSnOoW-_cx01VSD3czxmQI,199
|
|
43
|
-
qlsdk/rsc/interface/device.py,sha256=
|
|
44
|
+
qlsdk/rsc/interface/device.py,sha256=apBQAeu1g0Qmw73qQqr6uG1re9qCep6oKfjWKlGJdp4,3092
|
|
44
45
|
qlsdk/rsc/interface/handler.py,sha256=ADDe_a2RAxGMuooLyivH0JBPTGBcFP2JaTVX41R1A4w,198
|
|
45
46
|
qlsdk/rsc/interface/parser.py,sha256=DxuFZiprJJbG4pfFbbZPaG8MlBiBRe0S0lJrvc2Iees,251
|
|
46
47
|
qlsdk/rsc/manager/__init__.py,sha256=4ljT3mR8YPBDQur46B5xPqK5tjLKlsWfgCJVuA0gs-8,40
|
|
47
|
-
qlsdk/rsc/manager/container.py,sha256=
|
|
48
|
+
qlsdk/rsc/manager/container.py,sha256=N9QB85FOA_7Oa_8M1y1M2UODA_tpS9JQONhGj8ObBG8,4811
|
|
48
49
|
qlsdk/rsc/manager/search.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
50
|
qlsdk/rsc/network/__init__.py,sha256=PfYiqXS2pZV__uegQ1TjaeYhY1pefZ_shwE_X5HNVbs,23
|
|
50
|
-
qlsdk/rsc/network/discover.py,sha256=
|
|
51
|
+
qlsdk/rsc/network/discover.py,sha256=4aojzRFInTC3d8K2TYGbnP1Ji5fOFEi31ekghj7ce5k,2977
|
|
51
52
|
qlsdk/rsc/parser/__init__.py,sha256=8RgwbKCINu3eTsxVLF9cMoBXJnVrDocOEFP6NGP_atk,34
|
|
52
|
-
qlsdk/rsc/parser/base.py,sha256=
|
|
53
|
+
qlsdk/rsc/parser/base.py,sha256=s6tkWQXoMq8ZA4Nns_aG1JS5PV3UXLMFarTZiCoPagM,5504
|
|
53
54
|
qlsdk/sdk/__init__.py,sha256=v9LKP-5qXCqnAsCkiRE9LDb5Tagvl_Qd_fqrw7y9yd4,68
|
|
54
55
|
qlsdk/sdk/ar4sdk.py,sha256=tugH3UUeNebdka78AzLyrtAXbYQQE3iFJ227zUit6tY,27261
|
|
55
56
|
qlsdk/sdk/hub.py,sha256=uEOGZBZtMDCWlV8G2TZe6FAo6eTPcwHAW8zdqr1eq_0,1571
|
|
@@ -57,7 +58,7 @@ qlsdk/sdk/libs/libAr4SDK.dll,sha256=kZp9_DRwPdAJ5OgTFQSqS8tEETxUs7YmmETuBP2g60U,
|
|
|
57
58
|
qlsdk/sdk/libs/libwinpthread-1.dll,sha256=W77ySaDQDi0yxpnQu-ifcU6-uHKzmQpcvsyx2J9j5eg,52224
|
|
58
59
|
qlsdk/x8/__init__.py,sha256=FDpDK7GAYL-g3vzfU9U_V03QzoYoxH9YLm93PjMlANg,4870
|
|
59
60
|
qlsdk/x8m/__init__.py,sha256=cLeUqEEj65qXw4Qa4REyxoLh6T24anSqPaKe9_lR340,634
|
|
60
|
-
qlsdk2-0.
|
|
61
|
-
qlsdk2-0.
|
|
62
|
-
qlsdk2-0.
|
|
63
|
-
qlsdk2-0.
|
|
61
|
+
qlsdk2-0.5.0.dist-info/METADATA,sha256=Lp7nByzF1dCM1LGXQxyKv8sc-vkTDuLjZtw9nu_JFBk,1134
|
|
62
|
+
qlsdk2-0.5.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
63
|
+
qlsdk2-0.5.0.dist-info/top_level.txt,sha256=2CHzn0SY-NIBVyBl07Suh-Eo8oBAQfyNPtqQ_aDatBg,6
|
|
64
|
+
qlsdk2-0.5.0.dist-info/RECORD,,
|
qlsdk2-0.4.2.dist-info/METADATA
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.2
|
|
2
|
-
Name: qlsdk2
|
|
3
|
-
Version: 0.4.2
|
|
4
|
-
Summary: SDK for quanlan device
|
|
5
|
-
Home-page: https://github.com/hehuajun/qlsdk
|
|
6
|
-
Author: hehuajun
|
|
7
|
-
Author-email: hehuajun@eegion.com
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.9
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: loguru>=0.6.0
|
|
14
|
-
Requires-Dist: numpy>=1.23.5
|
|
15
|
-
Requires-Dist: bitarray>=1.5.3
|
|
16
|
-
Provides-Extra: dev
|
|
17
|
-
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
18
|
-
Requires-Dist: twine>=3.0; extra == "dev"
|
|
19
|
-
Dynamic: author
|
|
20
|
-
Dynamic: author-email
|
|
21
|
-
Dynamic: classifier
|
|
22
|
-
Dynamic: description
|
|
23
|
-
Dynamic: description-content-type
|
|
24
|
-
Dynamic: home-page
|
|
25
|
-
Dynamic: requires-dist
|
|
26
|
-
Dynamic: requires-python
|
|
27
|
-
Dynamic: summary
|
|
28
|
-
|
|
29
|
-
# qlsdk project
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
## Getting started
|
|
34
|
-
|
|
35
|
-
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
|
36
|
-
|
|
37
|
-
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
|
38
|
-
|
|
39
|
-
## Add your files
|
|
40
|
-
|
|
41
|
-
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
|
42
|
-
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
cd existing_repo
|
|
46
|
-
git remote add origin http://10.60.170.104/sw/qlsdk-project.git
|
|
47
|
-
git branch -M main
|
|
48
|
-
git push -uf origin main
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Integrate with your tools
|
|
52
|
-
|
|
53
|
-
- [ ] [Set up project integrations](http://10.60.170.104/sw/qlsdk-project/-/settings/integrations)
|
|
54
|
-
|
|
55
|
-
## Collaborate with your team
|
|
56
|
-
|
|
57
|
-
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
|
58
|
-
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
|
59
|
-
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
|
60
|
-
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
|
61
|
-
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
|
62
|
-
|
|
63
|
-
## Test and Deploy
|
|
64
|
-
|
|
65
|
-
Use the built-in continuous integration in GitLab.
|
|
66
|
-
|
|
67
|
-
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
|
|
68
|
-
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
|
69
|
-
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
|
70
|
-
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
|
71
|
-
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
|
72
|
-
|
|
73
|
-
***
|
|
74
|
-
|
|
75
|
-
# Editing this README
|
|
76
|
-
|
|
77
|
-
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
|
78
|
-
|
|
79
|
-
## Suggestions for a good README
|
|
80
|
-
|
|
81
|
-
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
|
82
|
-
|
|
83
|
-
## Name
|
|
84
|
-
Choose a self-explaining name for your project.
|
|
85
|
-
|
|
86
|
-
## Description
|
|
87
|
-
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
|
88
|
-
|
|
89
|
-
## Badges
|
|
90
|
-
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
|
91
|
-
|
|
92
|
-
## Visuals
|
|
93
|
-
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
|
94
|
-
|
|
95
|
-
## Installation
|
|
96
|
-
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
|
97
|
-
|
|
98
|
-
## Usage
|
|
99
|
-
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
|
100
|
-
|
|
101
|
-
## Support
|
|
102
|
-
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
|
103
|
-
|
|
104
|
-
## Roadmap
|
|
105
|
-
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
|
106
|
-
|
|
107
|
-
## Contributing
|
|
108
|
-
State if you are open to contributions and what your requirements are for accepting them.
|
|
109
|
-
|
|
110
|
-
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
|
111
|
-
|
|
112
|
-
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
|
113
|
-
|
|
114
|
-
## Authors and acknowledgment
|
|
115
|
-
Show your appreciation to those who have contributed to the project.
|
|
116
|
-
|
|
117
|
-
## License
|
|
118
|
-
For open source projects, say how it is licensed.
|
|
119
|
-
|
|
120
|
-
## Project status
|
|
121
|
-
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
|
File without changes
|
|
File without changes
|