mirror of
https://github.com/YuTaoV5/YuEEG.git
synced 2025-09-26 23:09:24 +08:00
add plot_only.py
This commit is contained in:
parent
3e88483030
commit
59281379ae
3
Software/.idea/.gitignore
generated
vendored
Normal file
3
Software/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
112
Software/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
112
Software/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,112 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<value>
|
||||
<list size="91">
|
||||
<item index="0" class="java.lang.String" itemvalue="six" />
|
||||
<item index="1" class="java.lang.String" itemvalue="wheel" />
|
||||
<item index="2" class="java.lang.String" itemvalue="python-dateutil" />
|
||||
<item index="3" class="java.lang.String" itemvalue="tzdata" />
|
||||
<item index="4" class="java.lang.String" itemvalue="PyQt5-sip" />
|
||||
<item index="5" class="java.lang.String" itemvalue="pyserial" />
|
||||
<item index="6" class="java.lang.String" itemvalue="setuptools" />
|
||||
<item index="7" class="java.lang.String" itemvalue="numpy" />
|
||||
<item index="8" class="java.lang.String" itemvalue="Pinyin2Hanzi" />
|
||||
<item index="9" class="java.lang.String" itemvalue="pandas" />
|
||||
<item index="10" class="java.lang.String" itemvalue="PyQt5-Qt5" />
|
||||
<item index="11" class="java.lang.String" itemvalue="pip" />
|
||||
<item index="12" class="java.lang.String" itemvalue="PyQt5" />
|
||||
<item index="13" class="java.lang.String" itemvalue="PyAudio" />
|
||||
<item index="14" class="java.lang.String" itemvalue="pytz" />
|
||||
<item index="15" class="java.lang.String" itemvalue="scipy" />
|
||||
<item index="16" class="java.lang.String" itemvalue="joblib" />
|
||||
<item index="17" class="java.lang.String" itemvalue="threadpoolctl" />
|
||||
<item index="18" class="java.lang.String" itemvalue="scikit-learn" />
|
||||
<item index="19" class="java.lang.String" itemvalue="dlib" />
|
||||
<item index="20" class="java.lang.String" itemvalue="opencv_python" />
|
||||
<item index="21" class="java.lang.String" itemvalue="onnxruntime-gpu" />
|
||||
<item index="22" class="java.lang.String" itemvalue="protobuf" />
|
||||
<item index="23" class="java.lang.String" itemvalue="nvidia-cuda-cupti-cu11" />
|
||||
<item index="24" class="java.lang.String" itemvalue="MarkupSafe" />
|
||||
<item index="25" class="java.lang.String" itemvalue="requests" />
|
||||
<item index="26" class="java.lang.String" itemvalue="torchvision" />
|
||||
<item index="27" class="java.lang.String" itemvalue="nvidia-cublas-cu11" />
|
||||
<item index="28" class="java.lang.String" itemvalue="opencv-python-headless" />
|
||||
<item index="29" class="java.lang.String" itemvalue="nvidia-cusparse-cu11" />
|
||||
<item index="30" class="java.lang.String" itemvalue="nvidia-cusolver-cu11" />
|
||||
<item index="31" class="java.lang.String" itemvalue="nvidia-curand-cu11" />
|
||||
<item index="32" class="java.lang.String" itemvalue="nvidia-nccl-cu11" />
|
||||
<item index="33" class="java.lang.String" itemvalue="gdown" />
|
||||
<item index="34" class="java.lang.String" itemvalue="filelock" />
|
||||
<item index="35" class="java.lang.String" itemvalue="nvidia-cudnn-cu11" />
|
||||
<item index="36" class="java.lang.String" itemvalue="lit" />
|
||||
<item index="37" class="java.lang.String" itemvalue="certifi" />
|
||||
<item index="38" class="java.lang.String" itemvalue="nvidia-cufft-cu11" />
|
||||
<item index="39" class="java.lang.String" itemvalue="soupsieve" />
|
||||
<item index="40" class="java.lang.String" itemvalue="urllib3" />
|
||||
<item index="41" class="java.lang.String" itemvalue="nvidia-cuda-runtime-cu11" />
|
||||
<item index="42" class="java.lang.String" itemvalue="beautifulsoup4" />
|
||||
<item index="43" class="java.lang.String" itemvalue="triton" />
|
||||
<item index="44" class="java.lang.String" itemvalue="nvidia-cuda-nvrtc-cu11" />
|
||||
<item index="45" class="java.lang.String" itemvalue="packaging" />
|
||||
<item index="46" class="java.lang.String" itemvalue="torch" />
|
||||
<item index="47" class="java.lang.String" itemvalue="tqdm" />
|
||||
<item index="48" class="java.lang.String" itemvalue="flatbuffers" />
|
||||
<item index="49" class="java.lang.String" itemvalue="nvidia-nvtx-cu11" />
|
||||
<item index="50" class="java.lang.String" itemvalue="cmake" />
|
||||
<item index="51" class="java.lang.String" itemvalue="typing_extensions" />
|
||||
<item index="52" class="java.lang.String" itemvalue="charset-normalizer" />
|
||||
<item index="53" class="java.lang.String" itemvalue="Pillow" />
|
||||
<item index="54" class="java.lang.String" itemvalue="transformers" />
|
||||
<item index="55" class="java.lang.String" itemvalue="altgraph" />
|
||||
<item index="56" class="java.lang.String" itemvalue="Bottleneck" />
|
||||
<item index="57" class="java.lang.String" itemvalue="brotlipy" />
|
||||
<item index="58" class="java.lang.String" itemvalue="greenlet" />
|
||||
<item index="59" class="java.lang.String" itemvalue="qt-material" />
|
||||
<item index="60" class="java.lang.String" itemvalue="cffi" />
|
||||
<item index="61" class="java.lang.String" itemvalue="cloudpickle" />
|
||||
<item index="62" class="java.lang.String" itemvalue="QtPy" />
|
||||
<item index="63" class="java.lang.String" itemvalue="mkl-random" />
|
||||
<item index="64" class="java.lang.String" itemvalue="mss" />
|
||||
<item index="65" class="java.lang.String" itemvalue="importlib-metadata" />
|
||||
<item index="66" class="java.lang.String" itemvalue="mkl-fft" />
|
||||
<item index="67" class="java.lang.String" itemvalue="gym-notices" />
|
||||
<item index="68" class="java.lang.String" itemvalue="pyOpenSSL" />
|
||||
<item index="69" class="java.lang.String" itemvalue="zipp" />
|
||||
<item index="70" class="java.lang.String" itemvalue="pywin32-ctypes" />
|
||||
<item index="71" class="java.lang.String" itemvalue="gevent" />
|
||||
<item index="72" class="java.lang.String" itemvalue="pyparsing" />
|
||||
<item index="73" class="java.lang.String" itemvalue="zope.event" />
|
||||
<item index="74" class="java.lang.String" itemvalue="auto-py-to-exe" />
|
||||
<item index="75" class="java.lang.String" itemvalue="QScintilla" />
|
||||
<item index="76" class="java.lang.String" itemvalue="pyinstaller" />
|
||||
<item index="77" class="java.lang.String" itemvalue="wincertstore" />
|
||||
<item index="78" class="java.lang.String" itemvalue="nes-py" />
|
||||
<item index="79" class="java.lang.String" itemvalue="opencv-python" />
|
||||
<item index="80" class="java.lang.String" itemvalue="pyglet" />
|
||||
<item index="81" class="java.lang.String" itemvalue="cryptography" />
|
||||
<item index="82" class="java.lang.String" itemvalue="zope.interface" />
|
||||
<item index="83" class="java.lang.String" itemvalue="QDarkStyle" />
|
||||
<item index="84" class="java.lang.String" itemvalue="pyinstaller-hooks-contrib" />
|
||||
<item index="85" class="java.lang.String" itemvalue="win-inet-pton" />
|
||||
<item index="86" class="java.lang.String" itemvalue="PySocks" />
|
||||
<item index="87" class="java.lang.String" itemvalue="gym" />
|
||||
<item index="88" class="java.lang.String" itemvalue="pypiwin32" />
|
||||
<item index="89" class="java.lang.String" itemvalue="mkl-service" />
|
||||
<item index="90" class="java.lang.String" itemvalue="numexpr" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N801" />
|
||||
<option value="N802" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
6
Software/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
Software/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
4
Software/.idea/misc.xml
generated
Normal file
4
Software/.idea/misc.xml
generated
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="SSVEP" project-jdk-type="Python SDK" />
|
||||
</project>
|
8
Software/.idea/modules.xml
generated
Normal file
8
Software/.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/software.iml" filepath="$PROJECT_DIR$/.idea/software.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
8
Software/.idea/software.iml
generated
Normal file
8
Software/.idea/software.iml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="SSVEP" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
Software/.idea/vcs.xml
generated
Normal file
6
Software/.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
BIN
Software/__pycache__/plot_card.cpython-38.pyc
Normal file
BIN
Software/__pycache__/plot_card.cpython-38.pyc
Normal file
Binary file not shown.
271
Software/plot_only.py
Normal file
271
Software/plot_only.py
Normal file
@ -0,0 +1,271 @@
|
||||
import sys
|
||||
import time
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
from collections import deque
|
||||
import pyqtgraph as pg
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import re
|
||||
import numpy as np
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout
|
||||
from pyqtgraph import LegendItem
|
||||
from qfluentwidgets import LineEdit, PushButton, TextEdit, CheckBox
|
||||
from scipy.signal import welch, butter, filtfilt
|
||||
|
||||
# Set the length of the data pool to handle 750 samples (3 seconds of data at 250Hz)
|
||||
data_len = 2000
|
||||
|
||||
|
||||
class SerialThread(QtCore.QThread):
|
||||
data_received = QtCore.pyqtSignal(str) # Signal to send data back to the main thread
|
||||
|
||||
def __init__(self, com_port):
|
||||
super().__init__()
|
||||
self.com_port = com_port
|
||||
self.serial_port = serial.Serial(self.com_port, baudrate=115200, timeout=1)
|
||||
self.data_pool = [deque(maxlen=data_len) for _ in range(9)] # Data pool for each channel
|
||||
self.running = True
|
||||
|
||||
def run(self):
|
||||
self.serial_port.write(b'1') # Send '1' to start data transmission
|
||||
while self.running:
|
||||
if self.serial_port.in_waiting:
|
||||
try:
|
||||
raw_data = self.serial_port.readline().decode('utf-8').strip()
|
||||
if raw_data:
|
||||
# Emit the raw data to the main thread for display
|
||||
self.data_received.emit(raw_data)
|
||||
# print(f"{raw_data}")
|
||||
# Integrity check (matches 'Channel:' followed by 9 float values)
|
||||
match = re.match(r'Channel:([\d\.\-]+,){8}[\d\.\-]+', raw_data)
|
||||
if match:
|
||||
values = [float(x) for x in raw_data.split('Channel:')[1].split(',')]
|
||||
# Add data to the pool for each channel
|
||||
for i in range(9):
|
||||
self.data_pool[i].append(values[i])
|
||||
except:
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
self.serial_port.close()
|
||||
|
||||
def send_data(self, data):
|
||||
self.serial_port.write(data.encode('utf-8') + b'\r\n') # 发送数据到串口
|
||||
|
||||
def get_latest_data(self, size):
|
||||
"""Return the latest 'size' data from the data pool for all channels."""
|
||||
if size > len(self.data_pool[0]):
|
||||
raise ValueError("Requested size is larger than the current data pool size.")
|
||||
|
||||
# Convert the latest 'size' elements from deque to numpy array for each channel
|
||||
return np.array([list(self.data_pool[i])[-size:] for i in range(9)])
|
||||
|
||||
|
||||
class ADCPlotter(QtWidgets.QMainWindow):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setWindowTitle("YuEEG Data Viewer")
|
||||
self.setWindowIcon(QIcon('./school_logo.ico'))
|
||||
self.setGeometry(100, 100, 800, 750)
|
||||
# Detect available COM ports and connect to the first available one
|
||||
com_port = self.detect_com_port()
|
||||
print(f"Connected to: {com_port}")
|
||||
if not com_port:
|
||||
raise Exception("No available COM ports detected.")
|
||||
|
||||
# Main layout
|
||||
self.central_widget = QtWidgets.QWidget()
|
||||
self.setCentralWidget(self.central_widget)
|
||||
self.layout = QtWidgets.QVBoxLayout(self.central_widget)
|
||||
|
||||
# Set up PyQtGraph plot
|
||||
self.plot_widget = pg.PlotWidget()
|
||||
self.layout.addWidget(self.plot_widget)
|
||||
# Create a legend
|
||||
self.legend = LegendItem(offset=(30, -30)) # Position the legend in the plot area
|
||||
self.legend.setParentItem(self.plot_widget.graphicsItem())
|
||||
# Create colored plots with thicker lines for each channel
|
||||
self.colors = ['r', 'g', 'b', 'c', 'm', 'y', 'w', 'grey', 'orange']
|
||||
self.plot_data = []
|
||||
for i in range(9):
|
||||
plot_item = self.plot_widget.plot(pen=pg.mkPen(color=self.colors[i], width=2))
|
||||
self.plot_data.append(plot_item)
|
||||
self.legend.addItem(plot_item, f"Channel {i + 1}") # Add each plot to the legend with a label
|
||||
|
||||
# Add a QTextEdit widget to display the raw data received
|
||||
self.text_box = TextEdit(self)
|
||||
self.text_box.setReadOnly(True)
|
||||
|
||||
self.send_text = LineEdit()
|
||||
self.send_button = PushButton("Send")
|
||||
self.adc_button = PushButton("Normal")
|
||||
self.adc_button.clicked.connect(self.mod_switch)
|
||||
self.send_button.clicked.connect(self.send_data)
|
||||
sendV_layout = QHBoxLayout()
|
||||
sendV_layout.addWidget(self.adc_button)
|
||||
sendV_layout.addWidget(self.send_text)
|
||||
sendV_layout.addWidget(self.send_button)
|
||||
|
||||
self.layout.addWidget(self.text_box)
|
||||
self.layout.addLayout(sendV_layout)
|
||||
|
||||
# Add checkboxes for selecting channels to display
|
||||
self.checkboxes = []
|
||||
self.checkbox_layout = QHBoxLayout()
|
||||
for i in range(9):
|
||||
checkbox = CheckBox(f"通道{i + 1}")
|
||||
checkbox.setChecked(True) # All channels checked by default
|
||||
self.checkboxes.append(checkbox)
|
||||
self.checkbox_layout.addWidget(checkbox)
|
||||
|
||||
self.layout.addLayout(self.checkbox_layout)
|
||||
|
||||
# Create a thread to handle serial data reception
|
||||
self.serial_thread = SerialThread(com_port)
|
||||
self.serial_thread.data_received.connect(self.handle_serial_data)
|
||||
self.serial_thread.start()
|
||||
|
||||
# Timer to refresh plot every 20ms
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.timeout.connect(self.update_plot)
|
||||
self.timer.start(100)
|
||||
self.beg = time.time()
|
||||
self.previous_psd = None
|
||||
|
||||
def send_data(self):
|
||||
data = self.send_text.text()
|
||||
self.serial_thread.send_data(data)
|
||||
|
||||
def mod_switch(self):
|
||||
if self.adc_button.text() == "Normal":
|
||||
self.serial_thread.send_data("3")
|
||||
self.adc_button.setText("Test")
|
||||
elif self.adc_button.text() == "Test":
|
||||
self.serial_thread.send_data("1")
|
||||
self.adc_button.setText("Normal")
|
||||
def detect_com_port(self):
|
||||
"""Automatically detect available COM ports, excluding virtual ports."""
|
||||
ports = list(serial.tools.list_ports.comports())
|
||||
for port in ports:
|
||||
if 'Bluetooth' not in port.description and 'Virtual' not in port.description:
|
||||
return port.device
|
||||
return None
|
||||
|
||||
def append_text_box(self, text):
|
||||
"""Append text to the text box, keeping only the last 10 lines."""
|
||||
current_text = self.text_box.toPlainText()
|
||||
lines = current_text.split('\n')
|
||||
|
||||
# Append new text
|
||||
lines.append(text)
|
||||
|
||||
# Keep only the last 10 lines
|
||||
if len(lines) > 10:
|
||||
lines = lines[-10:]
|
||||
|
||||
# Update the text box
|
||||
self.text_box.setPlainText('\n'.join(lines))
|
||||
# Move the cursor to the end
|
||||
# self.text_box.moveCursor(QtCore.QTextCursor.End)
|
||||
def check_psd(self, data, fs):
|
||||
threshold = 50
|
||||
if data is not None:
|
||||
psd_values = self.compute_psd(data, fs)
|
||||
current_psd_sum = np.sum(psd_values)
|
||||
if self.previous_psd is not None:
|
||||
# 比较当前PSD总和与上次总和的变化是否超过阈值
|
||||
if threshold < current_psd_sum - self.previous_psd < 500:
|
||||
print(f"发现向上突变,目前差值为:{(current_psd_sum - self.previous_psd)}")
|
||||
return True
|
||||
else:
|
||||
print(f"差值为:{current_psd_sum - self.previous_psd}")
|
||||
self.previous_psd = current_psd_sum
|
||||
def compute_psd(self, data, fs):
|
||||
# 定义感兴趣的频率范围
|
||||
low_freq = 3
|
||||
high_freq = 40
|
||||
|
||||
# 存储每个通道在3-40Hz的PSD值
|
||||
psd_values = []
|
||||
|
||||
for i in range(data.shape[0]):
|
||||
# 计算功率谱密度
|
||||
f, psd = welch(data[i, :], fs, nperseg=500)
|
||||
|
||||
# 只保留3到40Hz的频段
|
||||
freq_mask = (f >= low_freq) & (f <= high_freq)
|
||||
psd_in_band = psd[freq_mask]
|
||||
|
||||
# 计算3到40Hz频段内的PSD值(通过对频段内的PSD进行积分)
|
||||
psd_band_value = np.trapz(psd_in_band, f[freq_mask])
|
||||
|
||||
# 将结果存储起来
|
||||
psd_values.append(psd_band_value)
|
||||
return psd_values
|
||||
def handle_serial_data(self, raw_data):
|
||||
"""Handle data received from the serial thread."""
|
||||
self.append_text_box(raw_data)
|
||||
|
||||
# 定义 Buterworth 滤波器函数
|
||||
def apply_lowpass_filter(self, signal, cutoff_freq=50, sample_rate=500, order=5):
|
||||
nyquist_freq = 0.5 * sample_rate
|
||||
normalized_cutoff_freq = cutoff_freq / nyquist_freq
|
||||
b, a = butter(order, normalized_cutoff_freq, btype='low', analog=False)
|
||||
filtered_signal = filtfilt(b, a, signal)
|
||||
return filtered_signal
|
||||
|
||||
def update_plot(self):
|
||||
"""Update the plot based on the current data in the data pool."""
|
||||
try:
|
||||
# Fetch the latest 750 samples from the serial thread data pool
|
||||
latest_data = self.serial_thread.get_latest_data(data_len)
|
||||
|
||||
|
||||
# Update the plot for each channel
|
||||
for i in range(9):
|
||||
if self.checkboxes[i].isChecked(): # Only update if the channel is checked
|
||||
waveform = latest_data[i]
|
||||
filtered_waveform = self.apply_lowpass_filter(waveform)
|
||||
self.plot_data[i].setData(filtered_waveform)
|
||||
else:
|
||||
self.plot_data[i].setData([]) # Hide the waveform by clearing the data
|
||||
# self.plot_data[7].setData(latest_data[7])
|
||||
# Collect data from only the selected channels
|
||||
selected_data = [latest_data[i] for i in range(9) if self.checkboxes[i].isChecked()]
|
||||
|
||||
# If there is any selected data, calculate global min and max
|
||||
if selected_data:
|
||||
global_min = np.min([np.min(data) for data in selected_data])
|
||||
global_max = np.max([np.max(data) for data in selected_data])
|
||||
# Set Y-axis range based on global min/max values
|
||||
self.plot_widget.setYRange(global_min, global_max)
|
||||
else:
|
||||
global_min = -4.5
|
||||
global_max = 4.5
|
||||
# Set Y-axis range based on global min/max values
|
||||
self.plot_widget.setYRange(global_min, global_max)
|
||||
eeg_data = latest_data
|
||||
self.channel = [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
eeg_data = np.array([eeg_data[i - 1] for i in self.channel])
|
||||
if time.time() - self.beg > 1:
|
||||
psd_values = self.compute_psd(eeg_data, 500)
|
||||
current_psd_sum = np.sum(psd_values)
|
||||
print(f"psd:{current_psd_sum}")
|
||||
self.beg = time.time()
|
||||
except ValueError:
|
||||
pass # Skip plotting if not enough data is available
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Ensure the thread is stopped when the application is closed."""
|
||||
self.serial_thread.stop()
|
||||
self.serial_thread.wait()
|
||||
event.accept()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
main = ADCPlotter()
|
||||
main.show()
|
||||
sys.exit(app.exec_())
|
BIN
Software/school_logo.ico
Normal file
BIN
Software/school_logo.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
1
hardware/【先看】硬件链接.txt
Normal file
1
hardware/【先看】硬件链接.txt
Normal file
@ -0,0 +1 @@
|
||||
嘉立创链接:https://oshwhub.com/protodrive000/1299_pro 访问密码:yutaov5
|
Loading…
Reference in New Issue
Block a user