巧用Single.NAN设置控件属性

本文介绍了一种在页面设计中使控件自动填充容器的方法,通过将控件的Height和Width属性设置为Single.NaN实现控件自适应填充,避免了复杂的尺寸计算。

很多时候在设计页面的时候,就给控件设置了大小,比如设置了Height和Width属性,但是有时候又不需要设置属性,比如希望这个控件填充满一个容器,如果通过计算的方式来重设Height和Width,就显得很麻烦,这时候使用single.NaN就很好解决这个问题了。

 

例子中定义了一个Button,并且设置了Height和Width,运行时Button显示大小就是设定的大小。

点击Button后,重设了Height和Width为NAN,这样整个Button就直接充满了整个页面。

第三批上传文件:数据解析类 // ===== com/smallplanet/util/SPSXYPoint.java ===== package com.smallplanet.util; import com.smallplanet.jyd.JYDPoint; import com.smallplanet.jyd.JYDStorable; import com.smallplanet.util.SPSVector; import java.text.DecimalFormat; public class SPSXYPoint implements Comparable, JYDStorable { private static DecimalFormat fmt4 = new DecimalFormat("###0.0000"); double x = Double.NaN; double y = Double.NaN; private static final double Tolerance = 0.01D; public SPSXYPoint() {} public SPSXYPoint(double newX, double newY) { this.x = newX; this.y = newY; } public SPSXYPoint(float newX, float newY) { this.x = (double)newX; this.y = (double)newY; } public double getX() { return this.x; } public String getXData(){ return fmt4.format(this.x); } public double getY() { return this.y; } public String getYData(){ return fmt4.format(this.y); } public boolean approximatelyEquals(SPSXYPoint testPoint, float numericalError) { boolean returnFlag = false; if(testPoint != null && Math.abs(this.getX() - testPoint.getX()) < (double)numericalError && Math.abs(this.getY() - testPoint.getY()) < (double)numericalError) { returnFlag = true; } System.out.println("SPSXYPoint.approximatelyEquals() this = " + this + " testPoint = " + testPoint + " returnFlag = " + returnFlag); return returnFlag; } public SPSXYPoint addVector(SPSVector theVector) { SPSXYPoint tempPoint = new SPSXYPoint(this.x + theVector.getX(), this.y + theVector.getY()); return tempPoint; } public boolean isBefore(SPSXYPoint comparePoint) { return this.x < comparePoint.x; } public boolean isAfter(SPSXYPoint comparePoint) { return this.x > comparePoint.x; } public boolean isValid() { boolean returnFlag = true; if(!Double.isNaN(this.x) && !Double.isNaN(this.y)) { if(Double.isInfinite(this.x) || Double.isInfinite(this.y)) { returnFlag = false; } } else { returnFlag = false; } return returnFlag; } public int compareTo(Object testObject) { SPSXYPoint testPoint = (SPSXYPoint)testObject; double x0 = testPoint.getX(); int returnValue = this.x < x0?-1:(this.x == x0?0:1); return returnValue; } public String toString() { return "x = " + fmt4.format(this.x) + " y = " + fmt4.format(this.y); } public String toTabbedString() { return fmt4.format(this.x) + "\t" + fmt4.format(this.y) + "\n"; } public Object toJydStorableObject() { JYDPoint newPoint = new JYDPoint(this.getX(), this.getY()); return newPoint; } public boolean equals(Object ob) { boolean returnFlag = false; if(ob instanceof JYDPoint) { JYDPoint tempPoint = (JYDPoint)ob; if(Math.abs(this.x - (double)tempPoint.getX()) < 0.01D && Math.abs(this.y - (double)tempPoint.getY()) < 0.01D) { returnFlag = true; } } else if(ob instanceof SPSXYPoint) { SPSXYPoint tempPoint1 = (SPSXYPoint)ob; if(Math.abs(this.x - tempPoint1.getX()) < 0.01D && Math.abs(this.y - tempPoint1.getY()) < 0.01D) { returnFlag = true; } } return returnFlag; } } // ===== com/sienco/sonoclot/signature/SimpleSignature.java ===== package com.sienco.sonoclot.signature; import com.sienco.sonoclot.signature.FilterPointList; import com.sienco.sonoclot.signature.LineSegment; import com.sienco.sonoclot.signature.SignatureException; import com.smallplanet.jyd.JYDPoint; import com.smallplanet.util.SPSCurveData; import com.smallplanet.util.SPSGraphicsException; import com.smallplanet.util.SPSUtilities; import com.smallplanet.util.SPSXYPoint; import java.text.DecimalFormat; import java.util.*; public class SimpleSignature extends FilterPointList { protected static final float DEFAULT_MINIMUM_SIGNATURE_DATA_SPACING = 0.008333334F; protected static final float HISTORIC_MINIMUM_SIGNATURE_DATA_SPACING = 0.0016666667F; static final float MaxAnalysisDelay = 0.1F; private static DecimalFormat fmt = new DecimalFormat("###0.0000"); private SPSCurveData mySPSCurve = new SPSCurveData(); private boolean lastDataPointUncompressed = false; public SimpleSignature(float tolerance, float minimumSpacing) { super(minimumSpacing); } public SimpleSignature(float tolerance) { super(0.008333334F); } public SimpleSignature(float minimumspacing, List newData) throws SignatureException { super(minimumspacing); this.initializeSignature(newData); } public SimpleSignature(List newData) throws SignatureException { super(0.0016666667F); this.initializeSignature(newData); } void removeEndPoint() { super.removeEndPoint(); } private void initializeSignature(List newData) throws SignatureException { ListIterator tempIterator = newData.listIterator(); while(tempIterator.hasNext()) { Object tempObject = tempIterator.next(); if(tempObject != null) { SPSXYPoint tempPoint; if(tempObject instanceof SPSXYPoint) { tempPoint = (SPSXYPoint)tempObject; } else { JYDPoint jydPoint = (JYDPoint)tempObject; tempPoint = new SPSXYPoint(jydPoint.getX(), jydPoint.getY()); } super.addEndPoint(tempPoint); } } } public SPSCurveData getSignatureCurve() { this.updateSPSCurve(); return this.mySPSCurve; } private void updateSPSCurve() { this.mySPSCurve.clear(); Iterator tempIterator = this.iterator(); while(tempIterator.hasNext()) { Object tempObject = tempIterator.next(); if(tempObject != null && tempObject instanceof SPSXYPoint) { SPSXYPoint tempPoint = (SPSXYPoint)tempObject; try { this.mySPSCurve.addPoint(tempPoint); } catch (SPSGraphicsException var5) { SPSUtilities.throwNullPointerException("SimpleSignature.updateSPSCurve() bad data"); } } } } protected SPSXYPoint getMinClotSignalPointBetweenTimes(double t0, double t1) { double yMinimum = 100000.0D; SPSXYPoint minPoint = null; int index = this.getIndexBeforeT(t0); for(int maxIndex = this.getIndexBeforeT(t1); index <= maxIndex; ++index) { SPSXYPoint tempPoint = this.getPoint(index); double yTest = tempPoint.getY(); if(yTest < yMinimum) { yMinimum = yTest; minPoint = tempPoint; } } return minPoint; } float getTimeForYNearTime(float time, float yTarget, float timeRange) { float returnTime = Float.NaN; int ilow = this.getIndexBeforeT(time); int ihigh; LineSegment tempSegment; for(ihigh = ilow + 1; ilow >= 0; --ilow) { tempSegment = this.getLineSegment(ilow); if(tempSegment.isYOnSegment(yTarget)) { returnTime = tempSegment.getTforY(yTarget); break; } if(tempSegment.getStartTime() <= time - timeRange) { break; } } if(Float.isNaN(returnTime)) { while(ihigh < this.size() - 1) { tempSegment = this.getLineSegment(ihigh); if(tempSegment.isYOnSegment(yTarget)) { returnTime = tempSegment.getTforY(yTarget); break; } if(tempSegment.getEndTime() >= time + timeRange) { break; } ++ihigh; } } return returnTime; } float getLastClotSignal() { float tempLastClotSignal = this.getClotSignalAtTime(this.getEndTime()); return tempLastClotSignal; } public float getClotSignalAtTime(float t) { float y = this.getYAtT((double)t); return y; } double getTimeForClotSignalAfterTime(double testClotSignal, double initialTime) { double tempReturnTime = Double.NaN; double t0 = initialTime; double y0 = (double)this.getYAtT(initialTime); int i = this.getIndexAfterT((float)initialTime); double t1 = (double)this.getTimeAtIndex(i); double y1 = this.getClotSignalAtIndex(i); for(int indexLimit = this.size(); (y0 - testClotSignal) * (y1 - testClotSignal) > 0.0D && i < indexLimit; t1 = (double)this.getTimeAtIndex(i)) { y0 = y1; t0 = t1; ++i; y1 = this.getClotSignalAtIndex(i); } System.out.println("SimpleSignature.getTimeForClotSignalAfterTime():\n y0 = " + y0 + "\n y1 = " + y1 + "\n t0 = " + t0 + "\n t1 = " + t1 + "\n testClotSignal = " + testClotSignal); if((y0 - testClotSignal) * (y1 - testClotSignal) <= 0.0D) { double tempSlope = (y1 - y0) / (t1 - t0); double tempOffset = y0 - tempSlope * t0; tempReturnTime = Math.abs(tempSlope) > 1.0E-7D?(testClotSignal - tempOffset) / tempSlope:t0; } System.out.println("SimpleSignature.getTimeForClotSignalAfterTime():\n testClotSignal = " + testClotSignal + "\n initialTime = " + initialTime + "\n tempReturnTime = " + tempReturnTime); return tempReturnTime; } public SPSXYPoint getPointAtTime(float t) { float y = this.getYAtT((double)t); SPSXYPoint tempPoint = new SPSXYPoint(t, y); return tempPoint; } private double getTimeLength() { double tempTimeLength = (double)this.getEndTime(); return tempTimeLength; } private float getYAtT(double t) { float returnValue = Float.NaN; if(this.size() == 0) { returnValue = 0.0F; } else if(this.size() == 1) { SPSXYPoint index = this.getFirstPoint(); returnValue = (float)index.getY(); } else { int index1 = this.getIndexBeforeT(t); SPSXYPoint point0; SPSXYPoint point1; if(index1 < this.size() - 1) { point0 = this.getPoint(index1); point1 = this.getPoint(index1 + 1); } else { point0 = this.getPoint(index1 - 1); point1 = this.getPoint(index1); } float y = (float)(point0.getY() + (point1.getY() - point0.getY()) * (t - point0.getX()) / (point1.getX() - point0.getX())); returnValue = y; } return returnValue; } protected int getIndexBeforeT(double t) { int returnInt; if(this.size() < 2) { returnInt = 0; } else { int iLow = 0; int iHigh = this.size() - 1; boolean index = false; double testTime = this.getPoint(iLow).getX(); if(t < testTime) { returnInt = iLow; } else { testTime = this.getPoint(iHigh).getX(); if(t > testTime) { returnInt = iHigh; } else { while(iHigh - iLow > 1) { int index1 = (iHigh + iLow) / 2; testTime = this.getPoint(index1).getX(); if(t < testTime) { iHigh = index1; } else { iLow = index1; } } returnInt = iLow; } } } return returnInt; } public float getAnalysisEndTime(float tempAnalysisPeriod) { float returnTime = Float.NaN; int tempSize = this.size(); if(tempSize > 1 && this.getEndTime() - this.getStartTime() > tempAnalysisPeriod * 2.0F) { float tempTime0 = (float)this.getPoint(tempSize - 2).getX(); float tempTime1 = this.getEndTime(); if(tempTime1 - tempTime0 < tempAnalysisPeriod) { returnTime = tempTime0; } else { returnTime = tempTime1 - tempAnalysisPeriod; } } return returnTime; } protected float getTimeAtIndex(int index) { float tempFloat; try { SPSXYPoint e = this.getPoint(index); tempFloat = (float)e.getX(); } catch (ArrayIndexOutOfBoundsException var4) { tempFloat = 0.0F; } return tempFloat; } LineSegment getLineSegment(int index) { LineSegment returnSegment = null; if(this.size() > 1) { SPSXYPoint point0 = this.getPoint(index); SPSXYPoint point1 = this.getPoint(index + 1); returnSegment = new LineSegment(point0, point1); } return returnSegment; } public void addUncompressedPoint(SPSXYPoint newPoint) { if(this.size() > 0) { SPSXYPoint currentEndPoint = this.getLastPoint(); if(!this.isTooClose(currentEndPoint, newPoint)) { if(this.lastDataPointUncompressed) { this.removeEndPoint(); } super.addEndPoint(newPoint); this.updateSPSCurve(); this.lastDataPointUncompressed = true; } } } public void addEndPoint(SPSXYPoint newPoint) { SPSUtilities.throwNullPointerException("SimpleSignature.addEndPoint() this should never be called"); } public void addCompressedPoint(SPSXYPoint newPoint) { if(this.lastDataPointUncompressed) { this.removeEndPoint(); } super.addEndPoint(newPoint); this.updateSPSCurve(); this.lastDataPointUncompressed = false; } protected double getMaximumSlopeBetweenTimes(float tLow, float tHigh) { double maxSlope = Double.NaN; int iMin = this.getIndexBeforeT(tLow); int iMax = this.getIndexAfterT(tHigh); double y0 = Double.NaN; double y1 = Double.NaN; double t0 = Double.NaN; double t1 = Double.NaN; double tempSlope = Double.NaN; while(Double.isNaN(y0) || iMin < iMax) { t0 = (double)this.getTimeAtIndex(iMin); y0 = this.getClotSignalAtIndex(iMin++); t1 = (double)this.getTimeAtIndex(iMin); y1 = this.getClotSignalAtIndex(iMin); tempSlope = (y1 - y0) / (t1 - t0); if(Double.isNaN(maxSlope) || tempSlope > maxSlope) { maxSlope = tempSlope; } } if(Double.isNaN(maxSlope)) { System.out.println("SimpleSignature.getMaximumSlopeBetweenTimes(): unexpected NaN:"); System.out.println(" tLow = " + tLow); System.out.println(" tHigh = " + tHigh); System.out.println(" iMin = " + iMin); System.out.println(" iMax = " + iMax); System.out.println(" y0 = " + y0); System.out.println(" y1 = " + y1); System.out.println(" t0 = " + t0); System.out.println(" t1 = " + t1); System.out.println(" tempSlope = " + tempSlope); SPSUtilities.throwNullPointerException("SimpleSignature.getMaximumSlopeBetweenTimes():"); } return maxSlope; } double getClotSignalAtIndex(int index) { double tempDouble; try { SPSXYPoint e = this.getPoint(index); tempDouble = e.getY(); } catch (ArrayIndexOutOfBoundsException var5) { tempDouble = 0.0D; } return tempDouble; } protected double getAlignedTimeBefore(double t) { int index = this.getIndexBeforeT(t); double t0 = (double)this.getTimeAtIndex(index); return t0; } protected double getAlignedTimeAfter(double t) { int index = this.getIndexBeforeT(t); if(index + 1 < this.size()) { ++index; } double t0 = (double)this.getTimeAtIndex(index); return t0; } public String toString() { StringBuffer buf = new StringBuffer(); buf.append("SimpleSignature"); buf.append("\n Number of points = " + this.size()); Iterator iter = this.getPointsIterator(); while(iter.hasNext()) { SPSXYPoint tempPoint = (SPSXYPoint)iter.next(); buf.append("\n " + tempPoint); } return buf.toString(); } public List<Map<String,String>> getPointListData(){ List<Map<String,String>> result = new ArrayList<Map<String, String>>(); Iterator iter = this.getPointsIterator(); Map<String,String> data = null; int index = 0; while(iter.hasNext()) { SPSXYPoint tempPoint = (SPSXYPoint)iter.next(); data = new HashMap<String, String>(); result.add(data); data.put("i", index + ""); data.put("x", tempPoint.getXData()); data.put("y", tempPoint.getYData()); index++; } return result; } } ====== 你还要上传其他文件吗?如需要请告知文件名,如不需要就开始delphi 编程,请把与设备通讯的模块写入单独的单元中,方便调试与引用。另外请注意:引用控件HidControllerClassVersion = '1.0.35'的方法是引用 JvHidControllerClass.pas,不是HidController。如果需要,我可以提供JvHidControllerClass.pas文件内容中所有头部的定义函数。
最新发布
09-21
classdef ECG_Monitor_System < handle properties % 新增数据通信窗口属性 dataTransferFig % 数据传输子窗口 transferPortList % 可用串口列表 transferSerialPort % 传输用串口对象 fileData % 从文件加载的数据 transferTimer % 数据传输定时器 isTransferring = false % 传输状态标志 % 新增子窗口控件 filePathText portDropdown transferButton statusTextTransfer progressBar % 通信相关 serialPort % 串口对象 isConnected = false comPort = 'COM1' % 默认串口 baudRate = 115200 % OEM协议常用波特率 dataBits = 8 % 数据位 parity = 'none' % 校验位 stopBits = 1 % 停止位 % 数据管理 ecgData = zeros(12, 10000); filteredData = zeros(12, 10000); timeStamps = zeros(1, 10000); sampleRate = 1000; currentIndex = 1; dataLength = 0; isRecording = false; timerObj maxDataPoints = 10000 % 最大数据点数 numLeads = 12 % 导联数量 activeLead = 2 % 默认显示II导联 % 滤波参数 highPassFreq = 0.5; lowPassFreq = 100; notchFreq = 50; % 工频陷波 % 分析参数 heartRate = 0; rPeakPositions = []; qtInterval = 0; stSegment = 0; % 心电特征参数 qtIntervals = [] % QT间期数组(秒) stSegments = [] % ST段偏移数组(mV) avgQT = 0 % 平均QT间期 avgST = 0 % 平均ST偏移 % GUI组件 fig % 主控制窗口 overviewFig % 总览窗口 singleLeadFigs = gobjects(12, 1); % 单导联窗口 leadPlots = gobjects(12, 1); % 单导联绘图句柄 overviewPlots = gobjects(12, 1); % 总览窗口绘图句柄 leadNames = {'I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6'}; % 控制面板组件 controlPanel highPassEdit lowPassEdit notchCheckbox statusText heartRateText recordButton saveButton leadSelect dataSlider positionText % 频谱分析 spectrumAx % 报告生成 patientInfo = struct('name', '未知', 'gender', '未知', 'age', 0, 'id', ''); end methods function app = ECG_Monitor_System() app.createMainGUI(); app.createOverviewWindow(); app.initializeSystem(); end function createMainGUI(app) % 创建主控制窗口 app.fig = figure('Name', '心电数据管理系统', 'NumberTitle', 'off',... 'Position', [100, 100, 800, 600], 'CloseRequestFcn', @app.onClose,... 'MenuBar', 'none', 'ToolBar', 'none'); % 创建菜单栏 app.createMenus(); % 创建控制面板 app.createControlPanel(); end function createOverviewWindow(app) % 创建总览窗口 app.overviewFig = figure('Name', '心电图总览', 'NumberTitle', 'off',... 'Position', [950, 100, 1200, 800], 'CloseRequestFcn', @app.onOverviewClose); % 创建12导联网格布局 for i = 1:12 row = ceil(i/3); col = mod(i-1,3)+1; ax = subplot(4, 3, i, 'Parent', app.overviewFig); app.overviewPlots(i) = plot(ax, nan, nan, 'b-'); title(ax, app.leadNames{i}); xlabel(ax, '时间(s)'); ylabel(ax, '幅度(mV)'); grid(ax, 'on'); end end function createDataTransferWindow(app) % 创建数据传输子窗口 app.dataTransferFig = figure('Name', '数据通信', 'NumberTitle', 'off',... 'Position', [300, 300, 600, 400], 'MenuBar', 'none',... 'CloseRequestFcn', @app.onTransferClose); % 文件选择区域 uicontrol('Parent', app.dataTransferFig, 'Style', 'pushbutton',... 'String', '打开文件', 'Position', [20, 350, 80, 30],... 'Callback', @app.openFileForTransfer); app.filePathText = uicontrol('Parent', app.dataTransferFig, 'Style', 'text',... 'String', '未选择文件', 'Position', [110, 350, 350, 30],... 'HorizontalAlignment', 'left', 'FontSize', 10); % 串口选择区域 uicontrol('Parent', app.dataTransferFig, 'Style', 'text',... 'String', '选择串口:', 'Position', [20, 300, 80, 25], 'FontSize', 10); app.portDropdown = uicontrol('Parent', app.dataTransferFig, 'Style', 'popup',... 'String', {'无可用串口'}, 'Position', [110, 300, 150, 25],... 'FontSize', 10); % 扫描可用串口 app.scanAvailablePorts(); % 传输控制按钮 app.transferButton = uicontrol('Parent', app.dataTransferFig, 'Style', 'pushbutton',... 'String', '开始传输', 'Position', [280, 300, 100, 30],... 'Callback', @app.toggleTransfer, 'Enable', 'off'); % 状态显示 app.statusTextTransfer = uicontrol('Parent', app.dataTransferFig, 'Style', 'text',... 'String', '状态: 准备就绪', 'Position', [20, 250, 350, 25],... 'HorizontalAlignment', 'left', 'FontSize', 10); % 进度条 uicontrol('Parent', app.dataTransferFig, 'Style', 'text',... 'String', '传输进度:', 'Position', [20, 200, 80, 25], 'FontSize', 10); app.progressBar = uicontrol('Parent', app.dataTransferFig, 'Style', 'slider',... 'Position', [110, 200, 350, 25], 'Enable', 'off', 'Min', 0, 'Max', 1, 'Value', 0); % 绘图区域 - 显示12导联实时数据 plotPanel = uipanel('Parent', app.dataTransferFig, 'Title', '实时数据',... 'Position', [0.05, 0.05, 0.9, 0.4]); % 创建12导联网格布局 for i = 1:12 row = ceil(i/4); col = mod(i-1,4)+1; ax = subplot(3, 4, i, 'Parent', plotPanel); app.transferPlots(i) = plot(ax, nan, nan, 'b-'); title(ax, app.leadNames{i}); xlabel(ax, '时间(s)'); ylabel(ax, '幅度(mV)'); grid(ax, 'on'); end end function scanAvailablePorts(app) % 扫描系统可用串口 ports = serialportlist("available"); if isempty(ports) set(app.portDropdown, 'String', {'无可用串口'}); set(app.transferButton, 'Enable', 'off'); else set(app.portDropdown, 'String', ports); set(app.transferButton, 'Enable', 'on'); end app.transferPortList = ports; end %% function scanAvailablePorts2(app) % 扫描系统可用串口 ports = serialportlist("available"); if isempty(ports) set(app.portDropdown, 'String', {'无可用串口'}); set(app.transferButton, 'Enable', 'off'); else set(app.portDropdown, 'String', ports); set(app.transferButton, 'Enable', 'on'); end end function openFileForTransfer(app, ~, ~) % 打开心电数据文件 [filename, pathname] = uigetfile(... {'*.mat;*.csv;*.txt;*.bin', '心电数据文件 (*.mat, *.csv, *.txt, *.bin)'},... '选择心电数据文件'); if isequal(filename, 0) return; end filepath = fullfile(pathname, filename); set(app.filePathText, 'String', filepath); try [~, ~, ext] = fileparts(filepath); switch lower(ext) case '.mat' data = load(filepath); if isfield(data, 'ecgData') app.fileData.ecgData = data.ecgData; app.fileData.timeStamps = data.timeStamps; app.fileData.sampleRate = data.sampleRate; else error('MAT文件格式不支持'); end case {'.csv', '.txt'} data = readmatrix(filepath); if size(data, 2) >= 13 app.fileData.timeStamps = data(:, 1)'; app.fileData.ecgData = data(:, 2:13)'; app.fileData.sampleRate = 1 / mean(diff(app.fileData.timeStamps)); else error('CSV/TXT文件格式错误'); end case '.bin' % 二进制文件解析 (示例实现) fid = fopen(filepath, 'r'); header = fread(fid, 4, 'uint8'); app.fileData.sampleRate = fread(fid, 1, 'single'); numPoints = fread(fid, 1, 'uint32'); numLeads = fread(fid, 1, 'uint8'); data = fread(fid, [numLeads+1, numPoints], 'single'); fclose(fid); app.fileData.timeStamps = data(1, :); app.fileData.ecgData = data(2:end, :); end app.fileData.currentIndex = 1; app.fileData.totalPoints = size(app.fileData.ecgData, 2); set(app.statusTextTransfer, 'String',... sprintf('状态: 已加载 %s (%d 个样本)', filename, app.fileData.totalPoints)); set(app.progressBar, 'Min', 0, 'Max', app.fileData.totalPoints, 'Value', 0); catch ME errordlg(sprintf('文件读取错误: %s', ME.message), '文件错误'); end end function toggleTransfer(app, ~, ~) if app.isTransferring % 停止传输 app.stopTransfer(); set(app.transferButton, 'String', '开始传输'); set(app.statusTextTransfer, 'String', '状态: 传输已停止'); else % 开始传输 if isempty(app.fileData) || isempty(app.fileData.ecgData) errordlg('请先选择数据文件', '传输错误'); return; end ports = get(app.portDropdown, 'String'); selectedIdx = get(app.portDropdown, 'Value'); selectedPort = ports{selectedIdx}; app.startTransfer(selectedPort); set(app.transferButton, 'String', '停止传输'); set(app.statusTextTransfer, 'String', '状态: 传输中...'); end end function startTransfer(app, port) % 创建传输串口 try app.transferSerialPort = serialport(port, app.baudRate,... 'DataBits', app.dataBits,... 'Parity', app.parity,... 'StopBits', app.stopBits); % 创建传输定时器 interval = 1000 / app.fileData.sampleRate; % 按采样率间隔发送 app.transferTimer = timer(... 'ExecutionMode', 'fixedRate',... 'Period', interval,... 'TimerFcn', @(~,~) app.sendDataPacket(),... 'StopFcn', @(~,~) app.cleanupAfterTransfer()); app.isTransferring = true; start(app.transferTimer); catch ME errordlg(sprintf('串口初始化失败: %s', ME.message), '通信错误'); app.isTransferring = false; end end function sendDataPacket(app) % 发送单个数据包 if app.fileData.currentIndex > app.fileData.totalPoints app.stopTransfer(); return; end % 获取当前数据点 idx = app.fileData.currentIndex; timestamp = app.fileData.timeStamps(idx); ecgValues = app.fileData.ecgData(:, idx); % 生成OEM协议数据包 packet = zeros(1, 13, 'single'); packet(1) = timestamp; packet(2:13) = ecgValues; % 转换为字节流 dataBytes = typecast(packet, 'uint8'); % 添加帧头帧尾 fullPacket = [uint8([AA, 55]), dataBytes, uint8([55, AA])]; % 通过串口发送 write(app.transferSerialPort, fullPacket, 'uint8'); % 更新子窗口绘图 app.updateTransferPlots(idx); % 更新进度 app.fileData.currentIndex = idx + 1; set(app.progressBar, 'Value', idx); end function updateTransferPlots(app, idx) % 更新子窗口的12导联绘图 windowSize = 200; % 显示窗口大小 startIdx = max(1, idx - windowSize); endIdx = min(app.fileData.totalPoints, idx + windowSize); timeWindow = app.fileData.timeStamps(startIdx:endIdx); for i = 1:12 if ishandle(app.transferPlots(i)) set(app.transferPlots(i), 'XData', timeWindow,... 'YData', app.fileData.ecgData(i, startIdx:endIdx)); % 自动调整Y轴范围 data = app.fileData.ecgData(i, startIdx:endIdx); dataRange = range(data); if dataRange > 0 ax = get(app.transferPlots(i), 'Parent'); ylim(ax, [min(data)-0.1*dataRange, max(data)+0.1*dataRange]); end end end drawnow; end function stopTransfer(app) % 停止数据传输 if ~isempty(app.transferTimer) && isvalid(app.transferTimer) stop(app.transferTimer); delete(app.transferTimer); app.transferTimer = []; end if ~isempty(app.transferSerialPort) && isvalid(app.transferSerialPort) delete(app.transferSerialPort); app.transferSerialPort = []; end app.isTransferring = false; end function cleanupAfterTransfer(app) % 传输完成后的清理 set(app.transferButton, 'String', '开始传输'); set(app.statusTextTransfer, 'String', '状态: 传输完成'); app.isTransferring = false; end function onTransferClose(app, ~, ~) % 关闭数据传输窗口 app.stopTransfer(); delete(app.dataTransferFig); app.dataTransferFig = []; end %% function createMenus(app) % 文件菜单 fileMenu = uimenu(app.fig, 'Text', '文件'); uimenu(fileMenu, 'Text', '加载数据', 'MenuSelectedFcn', @app.loadDataFile); uimenu(fileMenu, 'Text', '保存数据', 'MenuSelectedFcn', @app.saveData); uimenu(fileMenu, 'Text', '生成报告', 'MenuSelectedFcn', @app.generateReport); uimenu(fileMenu, 'Text', '退出', 'Separator', 'on', 'MenuSelectedFcn', @app.onClose); % 通信菜单 comMenu = uimenu(app.fig, 'Text', '通信'); uimenu(comMenu, 'Text', '连接设备', 'MenuSelectedFcn', @app.connectDevice); uimenu(comMenu, 'Text', '断开连接', 'MenuSelectedFcn', @app.disconnect); uimenu(comMenu, 'Text', '数据传输控制', 'Separator', 'on','MenuSelectedFcn', @app.openTransferWindow); % 视图菜单 viewMenu = uimenu(app.fig, 'Text', '视图'); uimenu(viewMenu, 'Text', '显示总览窗口', 'MenuSelectedFcn', @app.showOverviewWindow); uimenu(viewMenu, 'Text', '显示所有导联窗口', 'MenuSelectedFcn', @app.showAllLeadWindows); uimenu(viewMenu, 'Text', '关闭所有导联窗口', 'MenuSelectedFcn', @app.closeAllLeadWindows); % 设置菜单 settingsMenu = uimenu(app.fig, 'Text', '设置'); uimenu(settingsMenu, 'Text', '通信设置', 'MenuSelectedFcn', @app.setCommunication); uimenu(settingsMenu, 'Text', '患者信息', 'MenuSelectedFcn', @app.setPatientInfo); uimenu(settingsMenu, 'Text', '分析参数', 'MenuSelectedFcn', @app.setAnalysisParams); % 添加新菜单项 transferMenu = uimenu(app.fig, 'Text', '数据通信'); uimenu(transferMenu, 'Text', '打开通信窗口', 'MenuSelectedFcn', @app.openTransferWindow); end function openTransferWindow(app, ~, ~) % 打开数据传输窗口 if isempty(app.dataTransferFig) || ~ishandle(app.dataTransferFig) app.createDataTransferWindow(); else figure(app.dataTransferFig); % 激活现有窗口 end function createDataTransferWindow(app) % 创建数据传输子窗口 app.dataTransferFig = figure('Name', '数据通信', 'NumberTitle', 'off',... 'Position', [300, 300, 600, 400], 'MenuBar', 'none',... 'CloseRequestFcn', @app.onTransferClose); % 文件选择区域 uicontrol('Parent', app.dataTransferFig, 'Style', 'pushbutton',... 'String', '打开文件', 'Position', [20, 350, 80, 30],... 'Callback', @app.openFileForTransfer); app.filePathText = uicontrol('Parent', app.dataTransferFig, 'Style', 'text',... 'String', '未选择文件', 'Position', [110, 350, 350, 30],... 'HorizontalAlignment', 'left', 'FontSize', 10); % 串口选择区域 uicontrol('Parent', app.dataTransferFig, 'Style', 'text',... 'String', '选择串口:', 'Position', [20, 300, 80, 25], 'FontSize', 10); app.portDropdown = uicontrol('Parent', app.dataTransferFig, 'Style', 'popup',... 'String', {'无可用串口'}, 'Position', [110, 300, 150, 25],... 'FontSize', 10); % 扫描可用串口 app.scanAvailablePorts(); % 传输控制按钮 app.transferButton = uicontrol('Parent', app.dataTransferFig, 'Style', 'pushbutton',... 'String', '开始传输', 'Position', [280, 300, 100, 30],... 'Callback', @app.toggleTransfer, 'Enable', 'off'); % 状态显示 app.statusTextTransfer = uicontrol('Parent', app.dataTransferFig, 'Style', 'text',... 'String', '状态: 准备就绪', 'Position', [20, 250, 350, 25],... 'HorizontalAlignment', 'left', 'FontSize', 10); % 进度条 uicontrol('Parent', app.dataTransferFig, 'Style', 'text',... 'String', '传输进度:', 'Position', [20, 200, 80, 25], 'FontSize', 10); app.progressBar = uicontrol('Parent', app.dataTransferFig, 'Style', 'slider',... 'Position', [110, 200, 350, 25], 'Enable', 'off', 'Min', 0, 'Max', 1, 'Value', 0); % 绘图区域 - 显示12导联实时数据 plotPanel = uipanel('Parent', app.dataTransferFig, 'Title', '实时数据',... 'Position', [0.05, 0.05, 0.9, 0.4]); % 创建12导联网格布局 app.transferPlots = gobjects(12, 1); for i = 1:12 row = ceil(i/4); col = mod(i-1,4)+1; ax = subplot(3, 4, i, 'Parent', plotPanel); app.transferPlots(i) = plot(ax, nan, nan, 'b-'); title(ax, app.leadNames{i}); xlabel(ax, '时间(s)'); ylabel(ax, '幅度(mV)'); grid(ax, 'on'); end end function scanAvailablePorts(app) % 扫描系统可用串口 ports = serialportlist("available"); if isempty(ports) set(app.portDropdown, 'String', {'无可用串口'}); set(app.transferButton, 'Enable', 'off'); else set(app.portDropdown, 'String', ports); set(app.transferButton, 'Enable', 'on'); end end function openFileForTransfer(app, ~, ~) % 打开心电数据文件 [filename, pathname] = uigetfile(... {'*.mat;*.csv;*.txt;*.bin', '心电数据文件 (*.mat, *.csv, *.txt, *.bin)'},... '选择心电数据文件'); if isequal(filename, 0) return; end filepath = fullfile(pathname, filename); set(app.filePathText, 'String', filepath); try [~, ~, ext] = fileparts(filepath); switch lower(ext) case '.mat' data = load(filepath); if isfield(data, 'ecgData') app.fileData.ecgData = data.ecgData; app.fileData.timeStamps = data.timeStamps; app.fileData.sampleRate = data.sampleRate; else error('MAT文件格式不支持'); end case {'.csv', '.txt'} data = readmatrix(filepath); if size(data, 2) >= 13 app.fileData.timeStamps = data(:, 1)'; app.fileData.ecgData = data(:, 2:13)'; app.fileData.sampleRate = 1 / mean(diff(app.fileData.timeStamps)); else error('CSV/TXT文件格式错误'); end case '.bin' % 二进制文件解析 (示例实现) fid = fopen(filepath, 'r'); header = fread(fid, 4, 'uint8'); app.fileData.sampleRate = fread(fid, 1, 'single'); numPoints = fread(fid, 1, 'uint32'); numLeads = fread(fid, 1, 'uint8'); data = fread(fid, [numLeads+1, numPoints], 'single'); fclose(fid); app.fileData.timeStamps = data(1, :); app.fileData.ecgData = data(2:end, :); end app.fileData.currentIndex = 1; app.fileData.totalPoints = size(app.fileData.ecgData, 2); set(app.statusTextTransfer, 'String',... sprintf('状态: 已加载 %s (%d 个样本)', filename, app.fileData.totalPoints)); set(app.progressBar, 'Min', 0, 'Max', app.fileData.totalPoints, 'Value', 0); catch ME errordlg(sprintf('文件读取错误: %s', ME.message), '文件错误'); end end function toggleTransfer(app, ~, ~) if app.isTransferring % 停止传输 app.stopTransfer(); set(app.transferButton, 'String', '开始传输'); set(app.statusTextTransfer, 'String', '状态: 传输已停止'); else % 开始传输 if isempty(app.fileData) || isempty(app.fileData.ecgData) errordlg('请先选择数据文件', '传输错误'); return; end ports = get(app.portDropdown, 'String'); selectedIdx = get(app.portDropdown, 'Value'); selectedPort = ports{selectedIdx}; app.startTransfer(selectedPort); set(app.transferButton, 'String', '停止传输'); set(app.statusTextTransfer, 'String', '状态: 传输中...'); end end function startTransfer(app, port) % 创建传输串口 try app.transferSerialPort = serialport(port, app.baudRate,... 'DataBits', app.dataBits,... 'Parity', app.parity,... 'StopBits', app.stopBits); % 创建传输定时器 interval = 1000 / app.fileData.sampleRate; % 按采样率间隔发送 app.transferTimer = timer(... 'ExecutionMode', 'fixedRate',... 'Period', interval,... 'TimerFcn', @(~,~) app.sendDataPacket(),... 'StopFcn', @(~,~) app.cleanupAfterTransfer()); app.isTransferring = true; start(app.transferTimer); catch ME errordlg(sprintf('串口初始化失败: %s', ME.message), '通信错误'); app.isTransferring = false; end end function sendDataPacket(app) % 发送单个数据包 if app.fileData.currentIndex > app.fileData.totalPoints app.stopTransfer(); return; end % 获取当前数据点 idx = app.fileData.currentIndex; timestamp = app.fileData.timeStamps(idx); ecgValues = app.fileData.ecgData(:, idx); % 生成OEM协议数据包 packet = zeros(1, 13, 'single'); packet(1) = timestamp; packet(2:13) = ecgValues; % 转换为字节流 dataBytes = typecast(packet, 'uint8'); % 添加帧头帧尾 fullPacket = [uint8([AA, 55]), dataBytes, uint8([55, AA])]; % 通过串口发送 write(app.transferSerialPort, fullPacket, 'uint8'); % 更新子窗口绘图 app.updateTransferPlots(idx); % 更新进度 app.fileData.currentIndex = idx + 1; set(app.progressBar, 'Value', idx); end function updateTransferPlots(app, idx) % 更新子窗口的12导联绘图 windowSize = 200; % 显示窗口大小 startIdx = max(1, idx - windowSize); endIdx = min(app.fileData.totalPoints, idx + windowSize); timeWindow = app.fileData.timeStamps(startIdx:endIdx); for i = 1:12 if ishandle(app.transferPlots(i)) set(app.transferPlots(i), 'XData', timeWindow,... 'YData', app.fileData.ecgData(i, startIdx:endIdx)); % 自动调整Y轴范围 data = app.fileData.ecgData(i, startIdx:endIdx); dataRange = range(data); if dataRange > 0 ax = get(app.transferPlots(i), 'Parent'); ylim(ax, [min(data)-0.1*dataRange, max(data)+0.1*dataRange]); end end end drawnow; end function stopTransfer(app) % 停止数据传输 if ~isempty(app.transferTimer) && isvalid(app.transferTimer) stop(app.transferTimer); delete(app.transferTimer); app.transferTimer = []; end if ~isempty(app.transferSerialPort) && isvalid(app.transferSerialPort) delete(app.transferSerialPort); app.transferSerialPort = []; end app.isTransferring = false; end function cleanupAfterTransfer(app) % 传输完成后的清理 set(app.transferButton, 'String', '开始传输'); set(app.statusTextTransfer, 'String', '状态: 传输完成'); app.isTransferring = false; end function onTransferClose(app, ~, ~) % 关闭数据传输窗口 app.stopTransfer(); delete(app.dataTransferFig); app.dataTransferFig = []; end function createControlPanel(app) % 创建控制面板 app.controlPanel = uipanel('Parent', app.fig, 'Title', '控制面板',... 'Position', [0.05, 0.05, 0.9, 0.9]); % 通信控制 uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '连接设备', 'Position', [20, 520, 100, 30],... 'Callback', @app.connectDevice); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '断开连接', 'Position', [140, 520, 100, 30],... 'Callback', @app.disconnect); app.statusText = uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '状态: 未连接', 'Position', [260, 520, 300, 30],... 'HorizontalAlignment', 'left', 'FontSize', 10); % 滤波设置 uicontrol('Parent', app.controlPanel, 'Style', 'text', 'String', '高通滤波(Hz):',... 'Position', [20, 470, 100, 25], 'FontSize', 10); app.highPassEdit = uicontrol('Parent', app.controlPanel, 'Style', 'edit',... 'String', '0.5', 'Position', [130, 470, 60, 25], 'FontSize', 10); uicontrol('Parent', app.controlPanel, 'Style', 'text', 'String', '低通滤波(Hz):',... 'Position', [20, 440, 100, 25], 'FontSize', 10); app.lowPassEdit = uicontrol('Parent', app.controlPanel, 'Style', 'edit',... 'String', '100', 'Position', [130, 440, 60, 25], 'FontSize', 10); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton', 'String', '应用滤波',... 'Position', [200, 450, 80, 30], 'Callback', @app.applyFilters); % 工频陷波 app.notchCheckbox = uicontrol('Parent', app.controlPanel, 'Style', 'checkbox',... 'String', '50Hz陷波', 'Position', [300, 450, 100, 25], 'Value', 1, 'FontSize', 10); % 数据记录 app.recordButton = uicontrol('Parent', app.controlPanel, 'Style', 'togglebutton',... 'String', '开始记录', 'Position', [20, 380, 100, 30],... 'Callback', @app.toggleRecording); app.saveButton = uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '保存数据', 'Position', [140, 380, 100, 30],... 'Callback', @app.saveData); % 导联选择 uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '导联选择:', 'Position', [20, 340, 80, 25], 'FontSize', 10); app.leadSelect = uicontrol('Parent', app.controlPanel, 'Style', 'popup',... 'String', app.leadNames, 'Position', [110, 340, 100, 25],... 'Callback', @app.onLeadSelected, 'FontSize', 10); % 打开导联窗口按钮 uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '打开导联窗口', 'Position', [230, 340, 120, 30],... 'Callback', @app.openSelectedLeadWindow); % 心率显示 uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '心率:', 'Position', [20, 300, 50, 25], 'FontSize', 10); app.heartRateText = uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '-- bpm', 'Position', [80, 300, 80, 25], 'FontSize', 10); % 数据导航 uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '数据位置:', 'Position', [20, 260, 80, 25], 'FontSize', 10); app.dataSlider = uicontrol('Parent', app.controlPanel, 'Style', 'slider',... 'Position', [110, 260, 300, 25], 'Callback', @app.sliderMoved,... 'Enable', 'off'); app.positionText = uicontrol('Parent', app.controlPanel, 'Style', 'text',... 'String', '0/0', 'Position', [420, 260, 80, 25], 'FontSize', 10); % 视图控制按钮 uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '显示总览窗口', 'Position', [20, 200, 120, 30],... 'Callback', @app.showOverviewWindow); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '显示所有导联窗口', 'Position', [160, 200, 150, 30],... 'Callback', @app.showAllLeadWindows); uicontrol('Parent', app.controlPanel, 'Style', 'pushbutton',... 'String', '关闭所有导联窗口', 'Position', [330, 200, 150, 30],... 'Callback', @app.closeAllLeadWindows); % 频谱分析 spectrumPanel = uipanel('Parent', app.controlPanel, 'Title', '频谱分析',... 'Position', [0.05, 0.05, 0.9, 0.15]); app.spectrumAx = axes('Parent', spectrumPanel, 'Position', [0.1, 0.2, 0.85, 0.7]); title(app.spectrumAx, 'ECG信号频谱'); xlabel(app.spectrumAx, '频率(Hz)'); ylabel(app.spectrumAx, '幅度(dB)'); grid(app.spectrumAx, 'on'); end function initializeSystem(app) % 初始化系统参数 app.sampleRate = 1000; app.dataLength = 0; app.currentIndex = 1; app.isRecording = false; % 初始化串口参数(默认值需符合OEM协议) app.comPort = 'COM1'; app.baudRate = 115200; app.dataBits = 8; app.parity = 'none'; app.stopBits = 1; % 设置默认患者信息 app.patientInfo = struct(... 'name', '张三', ... 'gender', '男', ... 'age', 45, ... 'id', 'P2023001', ... 'date', datestr(now, 'yyyy-mm-dd')); end %% 窗口管理功能 function showOverviewWindow(app, ~, ~) % 显示总览窗口 if ishandle(app.overviewFig) figure(app.overviewFig); else app.createOverviewWindow(); end end function showAllLeadWindows(app, ~, ~) % 显示所有导联窗口 for i = 1:12 app.createSingleLeadWindow(i); end end function closeAllLeadWindows(app, ~, ~) % 关闭所有导联窗口 for i = 1:12 if ishandle(app.singleLeadFigs(i)) delete(app.singleLeadFigs(i)); end end end function openSelectedLeadWindow(app, ~, ~) % 打开选中的导联窗口 leadIdx = get(app.leadSelect, 'Value'); app.createSingleLeadWindow(leadIdx); end function onLeadSelected(app, src, ~) % 导联选择回调 leadIdx = get(src, 'Value'); app.updateSpectrumForLead(leadIdx); end function onOverviewClose(app, ~, ~) % 总览窗口关闭回调 delete(app.overviewFig); app.overviewFig = gobjects(1); end function onSingleLeadClose(app, leadIndex) % 单导联窗口关闭回调 delete(app.singleLeadFigs(leadIndex)); app.singleLeadFigs(leadIndex) = gobjects(1); end %% 通信协议实现 function setCommunication(app, ~, ~) % 串口设置对话框 prompt = {'串口号:', '波特率:', '数据位:', '校验位:', '停止位:'}; dlgtitle = '串口设置'; dims = [1 20]; definput = {app.comPort, num2str(app.baudRate), num2str(app.dataBits), ... app.parity, num2str(app.stopBits)}; answer = inputdlg(prompt, dlgtitle, dims, definput); if ~isempty(answer) app.comPort = answer{1}; app.baudRate = str2double(answer{2}); app.dataBits = str2double(answer{3}); app.parity = answer{4}; app.stopBits = str2double(answer{5}); % 验证波特率是否合法(示例:仅允许常见波特率) validBaudRates = [9600, 19200, 38400, 57600, 115200, 230400]; if ~any(app.baudRate == validBaudRates) app.baudRate = 115200; % 默认恢复为常用值 warndlg('波特率设置不合法,已恢复为115200', '设置警告'); end set(app.statusText, 'String', '串口设置已更新'); end end function connectDevice(app, ~, ~) try % 关闭现有连接 if ~isempty(app.serialPort) && isvalid(app.serialPort) fclose(app.serialPort); delete(app.serialPort); end % 创建串口对象(根据OEM协议配置参数) app.serialPort = serialport(app.comPort, app.baudRate, ... 'DataBits', app.dataBits, ... 'Parity', app.parity, ... 'StopBits', app.stopBits); % 设置数据接收回调(OEM协议通常以特定字节结尾,如CR/LF或固定帧长) configureCallback(app.serialPort, "terminator", "linefeed", ... @(src, ~) app.serialCallback(src)); % 初始化数据缓冲区 app.ecgData = zeros(12, 10000); app.filteredData = zeros(12, 10000); app.timeStamps = zeros(1, 10000); app.currentIndex = 1; app.dataLength = 0; % 启用数据导航 set(app.dataSlider, 'Enable', 'on'); set(app.statusText, 'String', ['已连接到串口: ' app.comPort ' (波特率: ' num2str(app.baudRate) ')']); app.isConnected = true; catch ME errordlg(['串口连接失败: ' ME.message], '连接错误'); app.isConnected = false; end end function disconnect(app, ~, ~) if ~isempty(app.serialPort) && isvalid(app.serialPort) fclose(app.serialPort); delete(app.serialPort); app.serialPort = []; set(app.statusText, 'String', '已断开串口连接'); app.isConnected = false; end end function serialCallback(app, src) % OEM协议帧结构: [AA 55][2字节序号][12×4字节浮点数][2字节CRC][FF] FRAME_HEADER = uint8([170, 85]); % AA 55 FRAME_TAIL = uint8(255); % FF while src.NumBytesAvailable >= 64 % 完整帧大小 % 查找帧头 header = read(src, 2, 'uint8'); if ~isequal(header, FRAME_HEADER) read(src, 1, 'uint8'); % 丢弃1字节后重试 continue; end % 读取完整帧 frame = read(src, 62, 'uint8'); % 2+60=62字节 tail = frame(end); if tail ~= FRAME_TAIL warning('帧尾错误: %s', dec2hex(tail)); continue; end % 提取数据 seqBytes = frame(1:2); dataBytes = frame(3:50); % 12×4=48字节 crcReceived = typecast(frame(51:52), 'uint16'); % CRC验证 crcCalc = app.calculateCRC([seqBytes; dataBytes]); if crcReceived ~= crcCalc warning('CRC校验失败: 接收%X vs 计算%X', crcReceived, crcCalc); continue; end % 解析数据 seq = typecast(seqBytes, 'uint16'); ecgValues = typecast(dataBytes, 'single'); % 存储数据(添加时间戳) if app.dataLength < size(app.ecgData, 2) app.dataLength = app.dataLength + 1; app.timeStamps(app.dataLength) = posixtime(datetime('now')); app.ecgData(:, app.dataLength) = ecgValues; end end end % 新增的CRC计算函数 function crc = calculateCRC(~, data) crc = uint16(0); poly = uint16(hex2dec('1021')); % CRC-16/CCITT-FALSE for i = 1:length(data) crc = bitxor(crc, bitshift(uint16(data(i)),8)); for j = 1:8 if bitand(crc, hex2dec('8000')) crc = bitxor(bitshift(crc,1), poly); else crc = bitshift(crc,1); end end end end %% 数据处理模块 function realTimeFiltering(app, idx) % 实时滤波处理 if idx == 1 % 初始化滤波数据 app.filteredData(:, idx) = app.ecgData(:, idx); return; end % 获取滤波参数 hpFreq = str2double(get(app.highPassEdit, 'String')); lpFreq = str2double(get(app.lowPassEdit, 'String')); useNotch = get(app.notchCheckbox, 'Value'); for i = 1:12 % 应用高通滤波 if ~isnan(hpFreq) && hpFreq > 0 [b, a] = butter(2, hpFreq/(app.sampleRate/2), 'high'); if idx > 10 segment = app.ecgData(i, max(1, idx-10):idx); filteredSeg = filtfilt(b, a, segment); app.filteredData(i, idx) = filteredSeg(end); else app.filteredData(i, idx) = app.ecgData(i, idx); end else app.filteredData(i, idx) = app.ecgData(i, idx); end % 应用低通滤波 if ~isnan(lpFreq) && lpFreq > 0 [b, a] = butter(4, lpFreq/(app.sampleRate/2), 'low'); if idx > 10 segment = app.filteredData(i, max(1, idx-10):idx); filteredSeg = filtfilt(b, a, segment); app.filteredData(i, idx) = filteredSeg(end); end end % 应用50Hz陷波 if useNotch wo = 50/(app.sampleRate/2); bw = wo/10; [b, a] = iirnotch(wo, bw); if idx > 10 segment = app.filteredData(i, max(1, idx-10):idx); filteredSeg = filtfilt(b, a, segment); app.filteredData(i, idx) = filteredSeg(end); end end end end function applyFilters(app, ~, ~) % 应用全局滤波 if app.dataLength > 0 % 获取滤波参数 hpFreq = str2double(get(app.highPassEdit, 'String')); lpFreq = str2double(get(app.lowPassEdit, 'String')); useNotch = get(app.notchCheckbox, 'Value'); for i = 1:12 % 高通滤波 if ~isnan(hpFreq) && hpFreq > 0 [b, a] = butter(2, hpFreq/(app.sampleRate/2), 'high'); app.filteredData(i, 1:app.dataLength) = ... filtfilt(b, a, app.ecgData(i, 1:app.dataLength)); end % 低通滤波 if ~isnan(lpFreq) && lpFreq > 0 [b, a] = butter(4, lpFreq/(app.sampleRate/2), 'low'); app.filteredData(i, 1:app.dataLength) = ... filtfilt(b, a, app.filteredData(i, 1:app.dataLength)); end % 50Hz陷波 if useNotch wo = 50/(app.sampleRate/2); bw = wo/10; [b, a] = iirnotch(wo, bw); app.filteredData(i, 1:app.dataLength) = ... filtfilt(b, a, app.filteredData(i, 1:app.dataLength)); end end % 更新显示 app.updatePlots(); app.updateSpectrum(); set(app.statusText, 'String', '全局滤波已应用'); end end %% 实时分析模块 function realTimeAnalysis(app) % 实时分析ECG信号 if app.dataLength > 100 % 在II导联检测R波 leadIndex = 2; data = app.filteredData(leadIndex, max(1, app.dataLength-500):app.dataLength); % 使用Pan-Tompkins算法检测R波 [~, qrs_i_raw] = app.pan_tompkins(data, app.sampleRate); % 计算心率 if length(qrs_i_raw) > 1 rrIntervals = diff(qrs_i_raw) / app.sampleRate; app.heartRate = 60 / mean(rrIntervals); set(app.heartRateText, 'String', sprintf('%.1f bpm', app.heartRate)); % 更新图形标记 hold(app.ax(leadIndex), 'on'); if ~isempty(app.rPeakMarkers) && isvalid(app.rPeakMarkers) delete(app.rPeakMarkers); end % 计算实际时间位置 timeOffset = app.timeStamps(max(1, app.dataLength-500)); timePos = app.timeStamps(qrs_i_raw + max(1, app.dataLength-500) - 1); app.rPeakMarkers = plot(app.ax(leadIndex),... timePos, data(qrs_i_raw), 'ro', 'MarkerSize', 6, 'MarkerFaceColor', 'r'); hold(app.ax(leadIndex), 'off'); end end end function [qrs_amp_raw, qrs_i_raw] = pan_tompkins(~, ecg, fs) % Pan-Tompkins R波检测算法 % 1. 带通滤波 (5-15 Hz) f1 = 5/(fs/2); f2 = 15/(fs/2); [b, a] = butter(1, [f1, f2], 'bandpass'); ecg_filtered = filtfilt(b, a, ecg); % 2. 微分 diff_ecg = diff(ecg_filtered); diff_ecg = [diff_ecg(1), diff_ecg]; % 3. 平方 sqr_ecg = diff_ecg .^ 2; % 4. 移动平均窗口积分 window_size = round(0.150 * fs); integrated_ecg = movmean(sqr_ecg, window_size); % 5. 自适应阈值检测 max_h = max(integrated_ecg); threshold = 0.2 * max_h; % 6. 寻找R波位置 [qrs_amp_raw, qrs_i_raw] = findpeaks(integrated_ecg,... 'MinPeakHeight', threshold,... 'MinPeakDistance', round(0.2*fs)); end %% 数据管理 function toggleRecording(app, src, ~) % 开始/停止记录数据 if src.Value app.isRecording = true; set(src, 'String', '停止记录'); set(app.statusText, 'String', '记录中...'); % 初始化记录缓冲区 app.ecgData = zeros(12, 10000); app.filteredData = zeros(12, 10000); app.timeStamps = zeros(1, 10000); app.currentIndex = 1; app.dataLength = 0; else app.isRecording = false; set(src, 'String', '开始记录'); set(app.statusText, 'String', '记录已停止'); end end function loadDataFile(app, ~, ~) % 加载ECG数据文件 [filename, pathname] = uigetfile({'*.mat;*.csv;*.txt', '数据文件 (*.mat, *.csv, *.txt)'},... '选择ECG数据文件'); if isequal(filename, 0) return; end filepath = fullfile(pathname, filename); [~, ~, ext] = fileparts(filepath); try h = waitbar(0, '加载数据中...', 'Name', '数据处理'); switch lower(ext) case '.mat' data = load(filepath); waitbar(0.3, h); if isfield(data, 'ecgData') app.ecgData = data.ecgData; app.filteredData = data.filteredData; app.timeStamps = data.timeStamps; app.sampleRate = data.sampleRate; else error('无法识别MAT文件中的数据格式'); end case {'.csv', '.txt'} data = readmatrix(filepath); waitbar(0.3, h); if size(data, 2) >= 13 app.timeStamps = data(:, 1)'; app.ecgData = data(:, 2:13)'; app.sampleRate = 1 / mean(diff(app.timeStamps)); else error('CSV/TXT文件应包含时间戳和12导联数据'); end end app.dataLength = size(app.ecgData, 2); app.currentIndex = app.dataLength; % 应用滤波 waitbar(0.7, h, '应用滤波...'); app.applyFilters(); % 更新界面 waitbar(0.9, h, '更新界面...'); app.updatePlots(); app.updateSpectrum(); % 配置导航滑块 set(app.dataSlider, 'Min', 0, 'Max', 1, 'Value', 1,... 'SliderStep', [100/app.dataLength, 1000/app.dataLength],... 'Enable', 'on'); set(app.positionText, 'String',... sprintf('%d/%d', app.dataLength, size(app.ecgData, 2))); set(app.statusText, 'String',... sprintf('已加载: %s (%d个样本)', filename, app.dataLength)); waitbar(1, h, '完成!'); pause(0.5); delete(h); catch ME errordlg(sprintf('加载失败: %s', ME.message), '文件错误'); end end function saveData(app, ~, ~) % 保存ECG数据 if app.dataLength == 0 errordlg('没有可保存的数据', '保存错误'); return; end [filename, pathname] = uiputfile({'*.mat', 'MAT文件 (*.mat)'; '*.csv', 'CSV文件 (*.csv)'},... '保存ECG数据', 'ecg_data.mat'); if isequal(filename, 0) return; end filepath = fullfile(pathname, filename); [~, ~, ext] = fileparts(filepath); try app.ecgData = app.ecgData(:, 1:app.dataLength); app.filteredData = app.filteredData(:, 1:app.dataLength); app.timeStamps = app.timeStamps(1:app.dataLength); app.sampleRate = app.sampleRate; switch lower(ext) case '.mat' save(filepath, 'ecgData', 'filteredData', 'timeStamps', 'sampleRate'); case '.csv' dataToSave = [app.timeStamps', app.ecgData']; writematrix(dataToSave, filepath); end set(app.statusText, 'String', sprintf('数据已保存至: %s', filename)); catch ME errordlg(sprintf('保存失败: %s', ME.message), '保存错误'); end end %% 显示更新 function updatePlots(app) % 更新所有窗口的心电图显示 if app.dataLength > 0 windowSize = 500; % 显示窗口大小 startIdx = max(1, app.currentIndex - windowSize); endIdx = min(app.dataLength, app.currentIndex + windowSize); timeWindow = app.timeStamps(startIdx:endIdx); % 更新总览窗口 if ishandle(app.overviewFig) for i = 1:12 if ishandle(app.overviewPlots(i)) set(app.overviewPlots(i), 'XData', timeWindow,... 'YData', app.filteredData(i, startIdx:endIdx)); % 自动调整Y轴范围 dataRange = range(app.filteredData(i, startIdx:endIdx)); if dataRange > 0 ax = get(app.overviewPlots(i), 'Parent'); ylim(ax, [min(app.filteredData(i, startIdx:endIdx)) - 0.1*dataRange,... max(app.filteredData(i, startIdx:endIdx)) + 0.1*dataRange]); end end end drawnow; end % 更新单导联窗口 for i = 1:12 if ishandle(app.singleLeadFigs(i)) && ishandle(app.leadPlots(i)) set(app.leadPlots(i), 'XData', timeWindow,... 'YData', app.filteredData(i, startIdx:endIdx)); % 自动调整Y轴范围 dataRange = range(app.filteredData(i, startIdx:endIdx)); if dataRange > 0 ax = get(app.leadPlots(i), 'Parent'); ylim(ax, [min(app.filteredData(i, startIdx:endIdx)) - 0.1*dataRange,... max(app.filteredData(i, startIdx:endIdx)) + 0.1*dataRange]); end drawnow; end end end end function updateSpectrumForLead(app, leadIndex) % 更新指定导联的频谱分析 if app.dataLength > 0 && leadIndex >= 1 && leadIndex <= 12 signal = app.filteredData(leadIndex, max(1, app.dataLength-2000):app.dataLength); % 计算FFT N = length(signal); if N < 10, return; end f = (0:N-1)*(app.sampleRate/N); Y = fft(signal); P2 = abs(Y/N); P1 = P2(1:floor(N/2)+1); P1(2:end-1) = 2*P1(2:end-1); f = f(1:floor(N/2)+1); % 更新频谱分析图 plot(app.spectrumAx, f, 20*log10(P1), 'b'); title(app.spectrumAx, sprintf('导联 %s 频谱', app.leadNames{leadIndex})); xlabel(app.spectrumAx, '频率 (Hz)'); ylabel(app.spectrumAx, '幅度 (dB)'); xlim(app.spectrumAx, [0, min(150, app.sampleRate/2)]); grid(app.spectrumAx, 'on'); end end function showSpectrumForLead(app, leadIndex) % 在单独窗口中显示导联频谱 if app.dataLength > 0 && leadIndex >= 1 && leadIndex <= 12 figName = sprintf('导联 %s 频谱分析', app.leadNames{leadIndex}); app.fig = figure('Name', figName, 'NumberTitle', 'off',... 'Position', [200, 200, 600, 400]); ax = axes('Parent', app.fig, 'Position', [0.1, 0.15, 0.85, 0.75]); signal = app.filteredData(leadIndex, max(1, app.dataLength-2000):app.dataLength); N = length(signal); f = (0:N-1)*(app.sampleRate/N); Y = fft(signal); P2 = abs(Y/N); P1 = P2(1:floor(N/2)+1); P1(2:end-1) = 2*P1(2:end-1); f = f(1:floor(N/2)+1); plot(ax, f, 20*log10(P1), 'b'); title(ax, sprintf('导联 %s 频谱分析', app.leadNames{leadIndex})); xlabel(ax, '频率 (Hz)'); ylabel(ax, '幅度 (dB)'); xlim(ax, [0, min(150, app.sampleRate/2)]); grid(ax, 'on'); end end function detectRPeaksForLead(app, leadIndex) % 检测指定导联的R波 if app.dataLength > 0 && leadIndex >= 1 && leadIndex <= 12 data = app.filteredData(leadIndex, max(1, app.dataLength-2000):app.dataLength); % 使用Pan-Tompkins算法检测R波 [~, qrs_i_raw] = app.pan_tompkins(data, app.sampleRate); % 在图形上标记R波 if ishandle(app.singleLeadFigs(leadIndex)) app.fig = app.singleLeadFigs(leadIndex); ax = findobj(app.fig, 'Type', 'axes'); % 清除旧标记 oldMarkers = findobj(ax, 'Tag', 'RPeakMarker'); if ~isempty(oldMarkers) delete(oldMarkers); end % 添加新标记 hold(ax, 'on'); plot(ax, app.timeStamps(qrs_i_raw + max(1, app.dataLength-2000) - 1),... data(qrs_i_raw), 'ro', 'MarkerSize', 8, 'MarkerFaceColor', 'r',... 'Tag', 'RPeakMarker'); hold(ax, 'off'); % 计算并显示心率 if length(qrs_i_raw) > 1 rrIntervals = diff(qrs_i_raw) / app.sampleRate; app.heartRate = 60 / mean(rrIntervals); title(ax, sprintf('导联 %s - 心率: %.1f bpm',... app.leadNames{leadIndex}, app.heartRate)); end end end end %% 报告生成 function setPatientInfo(app, ~, ~) % 设置患者信息 prompt = {'姓名:', '性别:', '年龄:', '患者ID:'}; dlgtitle = '患者信息'; dims = [1 25]; definput = {app.patientInfo.name, app.patientInfo.gender,... num2str(app.patientInfo.age), app.patientInfo.id}; answer = inputdlg(prompt, dlgtitle, dims, definput); if ~isempty(answer) app.patientInfo.name = answer{1}; app.patientInfo.gender = answer{2}; app.patientInfo.age = str2double(answer{3}); app.patientInfo.id = answer{4}; app.patientInfo.date = datestr(now, 'yyyy-mm-dd HH:MM'); end end function generateReport(app, ~, ~) % 生成心电图报告 if app.dataLength == 0 errordlg('没有数据可生成报告', '报告错误'); return; end [filename, pathname] = uiputfile('*.pdf', '保存心电图报告', 'ECG_Report.pdf'); if filename == 0 return; end pdfFile = fullfile(pathname, filename); app.exportToPDF(pdfFile); msgbox(sprintf('心电图报告已保存至:\n%s', pdfFile), '报告生成'); end function exportToPDF(app, filename) % 导出PDF报告 app.fig = figure('Visible', 'off', 'Position', [100, 100, 1200, 900]); % 报告标题 uicontrol('Style', 'text', 'String', '心电图报告',... 'Position', [400, 850, 400, 40], 'FontSize', 20, 'FontWeight', 'bold'); % 患者信息 app.patientInfo = {sprintf('姓名: %s', app.patientInfo.name),... sprintf('性别: %s', app.patientInfo.gender),... sprintf('年龄: %d', app.patientInfo.age),... sprintf('ID: %s', app.patientInfo.id),... sprintf('检查日期: %s', app.patientInfo.date)}; uicontrol('Style', 'text', 'String', app.patientInfo,... 'Position', [50, 780, 300, 100], 'FontSize', 12, 'HorizontalAlignment', 'left'); % 分析结果 analysisInfo = {sprintf('心率: %.1f bpm', app.heartRate),... sprintf('采样率: %d Hz', app.sampleRate),... sprintf('数据长度: %.1f 秒', app.timeStamps(app.dataLength))}; uicontrol('Style', 'text', 'String', analysisInfo,... 'Position', [50, 680, 300, 60], 'FontSize', 12, 'HorizontalAlignment', 'left'); % 绘制12导联心电图 for i = 1:12 row = ceil(i/3); col = mod(i-1,3)+1; ax = subplot(4,3,i, 'Parent', app.fig); if app.dataLength > 0 % 显示完整数据 plot(ax, app.timeStamps(1:app.dataLength),... app.filteredData(i, 1:app.dataLength), 'b-'); end title(ax, app.leadNames{i}); xlabel(ax, '时间(s)'); ylabel(ax, '幅度(mV)'); grid(ax, 'on'); end % 导出为PDF exportgraphics(app.fig, filename, 'ContentType', 'vector'); close(app.fig); end %% 系统控制 function onClose(app, ~, ~) % 关闭应用程序 app.disconnect(); % 关闭所有窗口 if ishandle(app.overviewFig) delete(app.overviewFig); end for i = 1:12 if ishandle(app.singleLeadFigs(i)) delete(app.singleLeadFigs(i)); end end delete(app.fig); end end end end % 辅助函数 function result = iff(condition, trueValue, falseValue) if condition result = trueValue; else result = falseValue; end end
06-15
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值