c
% classdef TemperatureMonitor < handle
% % 温度监测类 - 实时读取、显示和保存串口温度数据
% % 完整版本,支持串口扫描、波特率选择、数据解析等功能
%
% properties
% serialPort % 串口对象
% fig % 主图形窗口句柄
% ax % 坐标轴句柄
% temperaturePlot % 温度曲线绘图句柄
% dataBuffer % 温度数据缓冲区
% timeBuffer % 时间数据缓冲区
% maxPoints = 1000 % 显示的最大数据点数
% isRunning = false % 是否正在运行
% startTime % 监控开始时间
% dataTable % 数据表格
% queryTimer % 查询定时器
% deviceReady = false % 设备是否准备好
% sampleCount = 0 % 采样计数
% expectingTemperature = false % 是否期望接收温度数据
% isConnected = false % 串口连接状态
% end
%
% properties (Access = private)
% logFileName % 日志文件名
% fileID % 文件ID
% receiveText % 接收数据显示文本框句柄
% commandHistory % 命令历史记录
% portDropdown % 串口号下拉菜单句柄
% baudRateDropdown % 波特率下拉菜单句柄
% connectButton % 连接/断开按钮句柄
% connectionStatusText % 连接状态文本句柄
% connectionInfoText % 连接信息文本句柄
% messageText % 消息文本句柄
% startBtn % 开始/停止监控按钮句柄
% resendBtn % 重新发送命令按钮句柄
% autoSaveBtn % 自动保存开关按钮句柄
% deviceStatusText % 设备状态文本句柄
% systemStatusText % 系统状态文本句柄
% sampleCountText % 采样点数文本句柄
% currentTempText % 当前温度文本句柄
% comInfoText % 串口信息文本句柄
% timeoutTimer % 超时定时器
% tempTimeoutTimer % 温度数据超时定时器
%
% defaultPorts = {'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'COM10'}
% defaultBaudRates = {'9600', '19200', '38400', '57600', '115200', '230400', '460800', '921600'}
% end
%
% methods
% function obj = TemperatureMonitor(varargin)
% % 构造函数
% % TemperatureMonitor() - 无参数,创建GUI后手动选择串口
% % TemperatureMonitor(port, baudRate, queryInterval) - 带参数创建
%
% fprintf('=== 温度监测系统初始化 ===\n');
%
% % 初始化数据缓冲
% obj.dataBuffer = [];
% obj.timeBuffer = [];
% obj.dataTable = table();
% obj.commandHistory = {};
%
% % 解析输入参数
% if nargin >= 2
% port = varargin{1};
% baudRate = varargin{2};
% if nargin >= 3
% queryInterval = varargin{3};
% else
% queryInterval = 1;
% end
%
% % 尝试打开串口
% try
% obj.connectSerialPort(port, baudRate);
% catch ME
% warning('串口连接失败: %s', ME.message);
% obj.isConnected = false;
% end
% else
% % 无参数调用,先创建GUI,不立即连接串口
% port = 'COM3'; % 默认串口
% baudRate = 9600; % 默认波特率
% if nargin == 1
% queryInterval = varargin{1};
% else
% queryInterval = 1;
% end
% obj.isConnected = false;
% end
%
% % 创建查询定时器
% obj.queryTimer = timer(...
% 'ExecutionMode', 'fixedRate', ...
% 'Period', queryInterval, ...
% 'TimerFcn', @(~, ~)obj.queryTemperature(), ...
% 'Name', 'TemperatureQueryTimer', ...
% 'BusyMode', 'queue');
%
% % 创建GUI界面
% obj.createGUI(port, baudRate, queryInterval);
%
% fprintf('温度监测系统初始化完成\n');
% end
%
% function createGUI(obj, defaultPort, defaultBaudRate, queryInterval)
% % 创建图形界面
%
% fprintf('正在创建GUI界面...\n');
%
% obj.fig = figure('Name', '温度实时监测与串口调试系统', ...
% 'NumberTitle', 'off', ...
% 'Position', [50, 50, 1400, 800], ...
% 'CloseRequestFcn', @obj.closeFigure, ...
% 'MenuBar', 'none', ...
% 'ToolBar', 'figure');
%
% % 创建连接配置区域
% configPanel = uipanel('Parent', obj.fig, ...
% 'Title', '串口配置', ...
% 'Position', [0.01, 0.85, 0.98, 0.14], ...
% 'FontSize', 12, ...
% 'FontWeight', 'bold', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 串口号选择
% uicontrol('Parent', configPanel, ...
% 'Style', 'text', ...
% 'String', '串口号:', ...
% 'Position', [20, 80, 80, 30], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 获取可用串口
% availablePorts = obj.scanSerialPorts();
% if isempty(availablePorts)
% availablePorts = obj.defaultPorts;
% end
%
% % 设置默认选择的端口
% defaultPortIdx = find(strcmp(availablePorts, defaultPort), 1);
% if isempty(defaultPortIdx)
% defaultPortIdx = 1;
% if ~isempty(availablePorts)
% defaultPort = availablePorts{1};
% else
% defaultPort = 'COM1';
% end
% end
%
% obj.portDropdown = uicontrol('Parent', configPanel, ...
% 'Style', 'popupmenu', ...
% 'String', availablePorts, ...
% 'Value', defaultPortIdx, ...
% 'Position', [100, 80, 150, 30], ...
% 'FontSize', 11, ...
% 'BackgroundColor', 'white', ...
% 'Tag', 'portDropdown');
%
% % 扫描串口按钮
% uicontrol('Parent', configPanel, ...
% 'Style', 'pushbutton', ...
% 'String', '扫描串口', ...
% 'Position', [260, 80, 100, 30], ...
% 'FontSize', 11, ...
% 'BackgroundColor', [0.2, 0.6, 1.0], ...
% 'ForegroundColor', 'white', ...
% 'FontWeight', 'bold', ...
% 'Callback', @(~, ~)obj.scanAndUpdatePorts());
%
% % 波特率选择
% uicontrol('Parent', configPanel, ...
% 'Style', 'text', ...
% 'String', '波特率:', ...
% 'Position', [380, 80, 80, 30], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 设置默认选择的波特率
% defaultBaudRateIdx = find(strcmp(obj.defaultBaudRates, num2str(defaultBaudRate)), 1);
% if isempty(defaultBaudRateIdx)
% defaultBaudRateIdx = 1;
% end
%
% obj.baudRateDropdown = uicontrol('Parent', configPanel, ...
% 'Style', 'popupmenu', ...
% 'String', obj.defaultBaudRates, ...
% 'Value', defaultBaudRateIdx, ...
% 'Position', [460, 80, 150, 30], ...
% 'FontSize', 11, ...
% 'BackgroundColor', 'white', ...
% 'Tag', 'baudRateDropdown');
%
% % 连接/断开按钮
% obj.connectButton = uicontrol('Parent', configPanel, ...
% 'Style', 'pushbutton', ...
% 'String', '连接串口', ...
% 'Position', [620, 80, 120, 30], ...
% 'FontSize', 12, ...
% 'BackgroundColor', [0.3, 0.7, 0.3], ...
% 'ForegroundColor', 'white', ...
% 'FontWeight', 'bold', ...
% 'Callback', @obj.toggleConnection);
%
% % 退出系统按钮
% uicontrol('Parent', configPanel, ...
% 'Style', 'pushbutton', ...
% 'String', '退出系统', ...
% 'Position', [750, 80, 120, 30], ...
% 'FontSize', 12, ...
% 'BackgroundColor', [0.9, 0.3, 0.3], ...
% 'ForegroundColor', 'white', ...
% 'FontWeight', 'bold', ...
% 'Callback', @(~, ~)obj.closeFigure());
%
% % 连接状态显示
% obj.connectionStatusText = uicontrol('Parent', configPanel, ...
% 'Style', 'text', ...
% 'String', '未连接', ...
% 'Position', [880, 80, 200, 30], ...
% 'FontSize', 11, ...
% 'ForegroundColor', 'red', ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 连接信息显示
% obj.connectionInfoText = uicontrol('Parent', configPanel, ...
% 'Style', 'text', ...
% 'String', sprintf('端口: %s, 波特率: %s', defaultPort, num2str(defaultBaudRate)), ...
% 'Position', [20, 40, 300, 30], ...
% 'FontSize', 10, ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 消息显示区域
% obj.messageText = uicontrol('Parent', configPanel, ...
% 'Style', 'text', ...
% 'String', '请选择串口并点击"连接串口"按钮开始', ...
% 'Position', [20, 10, 500, 30], ...
% 'FontSize', 10, ...
% 'ForegroundColor', 'blue', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 创建温度监测区域
% tempPanel = uipanel('Parent', obj.fig, ...
% 'Title', '温度监测', ...
% 'Position', [0.01, 0.52, 0.65, 0.32], ...
% 'FontSize', 12, ...
% 'FontWeight', 'bold', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 温度绘图区域
% obj.ax = axes('Parent', tempPanel, ...
% 'Position', [0.08, 0.15, 0.88, 0.80]);
% xlabel(obj.ax, '时间 (秒)', 'FontSize', 11, 'FontWeight', 'bold');
% ylabel(obj.ax, '温度 (°C)', 'FontSize', 11, 'FontWeight', 'bold');
% title(obj.ax, '温度实时变化曲线', 'FontSize', 12, 'FontWeight', 'bold');
% grid(obj.ax, 'on');
% hold(obj.ax, 'on');
%
% % 设置坐标轴颜色和字体
% set(obj.ax, 'XColor', [0.2, 0.2, 0.2], 'YColor', [0.2, 0.2, 0.2]);
% set(obj.ax, 'FontSize', 10);
%
% % 初始化绘图
% obj.temperaturePlot = plot(obj.ax, NaN, NaN, ...
% 'b-', 'LineWidth', 2, ...
% 'Marker', 'o', 'MarkerSize', 5, ...
% 'MarkerFaceColor', 'r', ...
% 'MarkerEdgeColor', 'b');
%
% % 设置初始坐标轴范围
% xlim(obj.ax, [0, 10]);
% ylim(obj.ax, [0, 50]);
%
% % 创建串口调试区域
% debugPanel = uipanel('Parent', obj.fig, ...
% 'Title', '串口调试', ...
% 'Position', [0.01, 0.01, 0.65, 0.50], ...
% 'FontSize', 12, ...
% 'FontWeight', 'bold', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 接收数据显示区域
% uicontrol('Parent', debugPanel, ...
% 'Style', 'text', ...
% 'String', '接收数据:', ...
% 'Position', [10, 440, 100, 25], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 接收数据显示文本框(可滚动)
% obj.receiveText = uicontrol('Parent', debugPanel, ...
% 'Style', 'edit', ...
% 'String', '', ...
% 'Position', [10, 50, 620, 390], ...
% 'FontSize', 10, ...
% 'Max', 100, ... % 允许多行
% 'Min', 0, ... % 允许多行
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', 'white', ...
% 'Enable', 'inactive', ... % 设置为只读
% 'Tag', 'receiveText'); % 添加标签以便查找
%
% % 发送命令区域
% uicontrol('Parent', debugPanel, ...
% 'Style', 'text', ...
% 'String', '发送命令:', ...
% 'Position', [10, 470, 100, 25], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% commandEdit = uicontrol('Parent', debugPanel, ...
% 'Style', 'edit', ...
% 'String', 'AT+T?', ...
% 'Position', [100, 470, 400, 30], ...
% 'FontSize', 11, ...
% 'Tag', 'commandEdit', ...
% 'BackgroundColor', 'white');
%
% % 发送按钮
% uicontrol('Parent', debugPanel, ...
% 'Style', 'pushbutton', ...
% 'String', '发送', ...
% 'Position', [510, 470, 80, 30], ...
% 'FontSize', 11, ...
% 'BackgroundColor', [0.1, 0.5, 0.9], ...
% 'ForegroundColor', 'white', ...
% 'FontWeight', 'bold', ...
% 'Callback', @(~, ~)obj.sendCustomCommand());
%
% % 清空接收区按钮
% uicontrol('Parent', debugPanel, ...
% 'Style', 'pushbutton', ...
% 'String', '清空接收区', ...
% 'Position', [600, 470, 100, 30], ...
% 'FontSize', 11, ...
% 'BackgroundColor', [0.9, 0.5, 0.1], ...
% 'ForegroundColor', 'white', ...
% 'FontWeight', 'bold', ...
% 'TooltipString', '清除串口调试窗口中的所有内容', ...
% 'Callback', @(~, ~)obj.clearReceiveText());
%
% % 常用命令
% commands = {'AT+T?', 'AT+VER', 'AT+HELP', 'AT+STATUS'};
% commandNames = {'查询温度', '版本信息', '帮助信息', '状态查询'};
% for i = 1:length(commands)
% uicontrol('Parent', debugPanel, ...
% 'Style', 'pushbutton', ...
% 'String', commandNames{i}, ...
% 'Position', [100 + (i-1)*120, 20, 100, 25], ...
% 'FontSize', 10, ...
% 'BackgroundColor', [0.4, 0.4, 0.8], ...
% 'ForegroundColor', 'white', ...
% 'TooltipString', sprintf('发送命令: %s', commands{i}), ...
% 'Callback', @(src, ~)obj.setCommandText(commands{i}));
% end
%
% % 添加快捷键说明
% uicontrol('Parent', debugPanel, ...
% 'Style', 'text', ...
% 'String', '快捷键: Ctrl+L=清空接收区, Ctrl+S=保存数据', ...
% 'Position', [10, 20, 250, 25], ...
% 'FontSize', 9, ...
% 'ForegroundColor', [0.5, 0.5, 0.5], ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 创建控制面板
% controlPanel = uipanel('Parent', obj.fig, ...
% 'Title', '控制面板', ...
% 'Position', [0.68, 0.52, 0.31, 0.32], ...
% 'FontSize', 12, ...
% 'FontWeight', 'bold', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 查询间隔设置
% uicontrol('Parent', controlPanel, ...
% 'Style', 'text', ...
% 'String', '查询间隔 (秒):', ...
% 'Position', [20, 230, 200, 25], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% uicontrol('Parent', controlPanel, ...
% 'Style', 'edit', ...
% 'String', num2str(queryInterval), ...
% 'Position', [20, 200, 200, 30], ...
% 'FontSize', 11, ...
% 'Tag', 'intervalEdit', ...
% 'BackgroundColor', 'white', ...
% 'Callback', @(~, ~)obj.updateInterval);
%
% % 数据点数设置
% uicontrol('Parent', controlPanel, ...
% 'Style', 'text', ...
% 'String', '显示数据点数:', ...
% 'Position', [20, 160, 200, 25], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% uicontrol('Parent', controlPanel, ...
% 'Style', 'edit', ...
% 'String', num2str(obj.maxPoints), ...
% 'Position', [20, 130, 200, 30], ...
% 'FontSize', 11, ...
% 'Tag', 'maxPointsEdit', ...
% 'BackgroundColor', 'white', ...
% 'Callback', @(~, ~)obj.updateMaxPoints);
%
% % 开始/停止按钮
% obj.startBtn = uicontrol('Parent', controlPanel, ...
% 'Style', 'pushbutton', ...
% 'String', '开始监控', ...
% 'Position', [20, 80, 200, 40], ...
% 'FontSize', 12, ...
% 'BackgroundColor', [0.3, 0.7, 0.3], ...
% 'ForegroundColor', 'white', ...
% 'FontWeight', 'bold', ...
% 'Callback', @obj.toggleMonitoring, ...
% 'Enable', 'off'); % 初始时禁用,连接串口后启用
%
% % 重新发送命令按钮
% obj.resendBtn = uicontrol('Parent', controlPanel, ...
% 'Style', 'pushbutton', ...
% 'String', '重新发送AT+T?', ...
% 'Position', [20, 30, 200, 40], ...
% 'FontSize', 12, ...
% 'BackgroundColor', [0.9, 0.8, 0.1], ...
% 'ForegroundColor', 'black', ...
% 'FontWeight', 'bold', ...
% 'Callback', @(~, ~)obj.sendTemperatureQuery(), ...
% 'Enable', 'off'); % 初始时禁用
%
% % 创建数据保存面板
% savePanel = uipanel('Parent', obj.fig, ...
% 'Title', '数据保存', ...
% 'Position', [0.68, 0.35, 0.31, 0.16], ...
% 'FontSize', 12, ...
% 'FontWeight', 'bold', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 手动保存按钮
% uicontrol('Parent', savePanel, ...
% 'Style', 'pushbutton', ...
% 'String', '手动保存数据', ...
% 'Position', [20, 40, 200, 40], ...
% 'FontSize', 12, ...
% 'BackgroundColor', [0.1, 0.5, 0.9], ...
% 'ForegroundColor', 'white', ...
% 'FontWeight', 'bold', ...
% 'Callback', @obj.saveData);
%
% % 自动保存开关
% obj.autoSaveBtn = uicontrol('Parent', savePanel, ...
% 'Style', 'togglebutton', ...
% 'String', '自动保存: 关闭', ...
% 'Position', [20, 90, 200, 40], ...
% 'FontSize', 12, ...
% 'BackgroundColor', [0.8, 0.2, 0.2], ...
% 'ForegroundColor', 'white', ...
% 'FontWeight', 'bold', ...
% 'Tag', 'autoSaveBtn', ...
% 'Callback', @obj.toggleAutoSave);
%
% % 创建状态显示面板
% statusPanel = uipanel('Parent', obj.fig, ...
% 'Title', '状态信息', ...
% 'Position', [0.68, 0.01, 0.31, 0.33], ...
% 'FontSize', 12, ...
% 'FontWeight', 'bold', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 设备状态
% uicontrol('Parent', statusPanel, ...
% 'Style', 'text', ...
% 'String', '设备状态:', ...
% 'Position', [10, 240, 80, 25], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% obj.deviceStatusText = uicontrol('Parent', statusPanel, ...
% 'Style', 'text', ...
% 'String', '未连接', ...
% 'Position', [100, 240, 150, 25], ...
% 'FontSize', 11, ...
% 'ForegroundColor', 'red', ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 系统状态
% uicontrol('Parent', statusPanel, ...
% 'Style', 'text', ...
% 'String', '系统状态:', ...
% 'Position', [10, 210, 80, 25], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% obj.systemStatusText = uicontrol('Parent', statusPanel, ...
% 'Style', 'text', ...
% 'String', '等待连接', ...
% 'Position', [100, 210, 150, 25], ...
% 'FontSize', 11, ...
% 'ForegroundColor', 'blue', ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 采样点数
% uicontrol('Parent', statusPanel, ...
% 'Style', 'text', ...
% 'String', '采样点数:', ...
% 'Position', [10, 180, 80, 25], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% obj.sampleCountText = uicontrol('Parent', statusPanel, ...
% 'Style', 'text', ...
% 'String', '0', ...
% 'Position', [100, 180, 80, 25], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 当前温度
% uicontrol('Parent', statusPanel, ...
% 'Style', 'text', ...
% 'String', '当前温度:', ...
% 'Position', [10, 150, 80, 25], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% obj.currentTempText = uicontrol('Parent', statusPanel, ...
% 'Style', 'text', ...
% 'String', '-- °C', ...
% 'Position', [100, 150, 100, 25], ...
% 'FontSize', 12, ...
% 'ForegroundColor', 'blue', ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 串口信息
% uicontrol('Parent', statusPanel, ...
% 'Style', 'text', ...
% 'String', '串口信息:', ...
% 'Position', [10, 120, 80, 25], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% obj.comInfoText = uicontrol('Parent', statusPanel, ...
% 'Style', 'text', ...
% 'String', '未连接', ...
% 'Position', [100, 120, 150, 25], ...
% 'FontSize', 11, ...
% 'FontWeight', 'bold', ...
% 'HorizontalAlignment', 'left', ...
% 'BackgroundColor', [0.95, 0.95, 0.95]);
%
% % 清除所有数据按钮
% uicontrol('Parent', statusPanel, ...
% 'Style', 'pushbutton', ...
% 'String', '清除所有数据', ...
% 'Position', [20, 70, 200, 40], ...
% 'FontSize', 12, ...
% 'BackgroundColor', [0.8, 0.3, 0.3], ...
% 'ForegroundColor', 'white', ...
% 'FontWeight', 'bold', ...
% 'TooltipString', '清除接收区、温度数据和图表', ...
% 'Callback', @obj.clearAllData);
%
% % 添加快捷键支持
% set(obj.fig, 'KeyPressFcn', @obj.keyboardShortcut);
%
% % 更新控件状态
% obj.updateControlStates();
%
% fprintf('GUI界面创建完成\n');
% end
%
% function ports = scanSerialPorts(obj)
% % 扫描可用串口
% try
% fprintf('正在扫描可用串口...\n');
%
% % 尝试使用MATLAB的串口列表功能
% if exist('serialportlist', 'file')
% ports = serialportlist('available');
% fprintf('使用 serialportlist 函数\n');
% elseif exist('seriallist', 'file')
% ports = seriallist;
% fprintf('使用 seriallist 函数\n');
% else
% % 如果上述函数都不存在,使用默认端口列表
% ports = obj.defaultPorts;
% fprintf('使用默认端口列表\n');
% end
%
% % 转换为单元格数组
% if isstring(ports)
% ports = cellstr(ports);
% elseif ischar(ports)
% ports = {ports};
% end
%
% % 如果没有任何端口,使用默认列表
% if isempty(ports)
% ports = obj.defaultPorts;
% fprintf('未检测到可用串口,使用默认列表\n');
% end
%
% fprintf('扫描到 %d 个可用串口:\n', length(ports));
% for i = 1:length(ports)
% fprintf(' %s\n', ports{i});
% end
%
% catch ME
% fprintf('扫描串口失败: %s\n', ME.message);
% ports = obj.defaultPorts;
% end
% end
%
% function scanAndUpdatePorts(obj)
% % 扫描并更新串口下拉菜单
% try
% fprintf('手动扫描串口...\n');
%
% % 扫描可用串口
% availablePorts = obj.scanSerialPorts();
%
% % 更新下拉菜单
% set(obj.portDropdown, 'String', availablePorts);
% if ~isempty(availablePorts)
% set(obj.portDropdown, 'Value', 1);
% end
%
% % 更新消息
% set(obj.messageText, 'String', ...
% sprintf('扫描完成,找到 %d 个可用串口', length(availablePorts)));
% set(obj.messageText, 'ForegroundColor', 'green');
%
% fprintf('串口列表已更新\n');
%
% catch ME
% warning('扫描串口失败: %s', ME.message);
% set(obj.messageText, 'String', '扫描串口失败');
% set(obj.messageText, 'ForegroundColor', 'red');
% end
% end
%
% function updateControlStates(obj)
% % 更新控件状态(启用/禁用)
%
% if obj.isConnected
% % 串口已连接,启用相关按钮
% set(obj.startBtn, 'Enable', 'on');
% set(obj.resendBtn, 'Enable', 'on');
% set(obj.connectButton, 'String', '断开连接');
% set(obj.connectButton, 'BackgroundColor', [0.9, 0.3, 0.3]);
% set(obj.connectionStatusText, 'String', '已连接');
% set(obj.connectionStatusText, 'ForegroundColor', 'green');
% else
% % 串口未连接,禁用相关按钮
% set(obj.startBtn, 'Enable', 'off');
% set(obj.resendBtn, 'Enable', 'off');
% set(obj.connectButton, 'String', '连接串口');
% set(obj.connectButton, 'BackgroundColor', [0.3, 0.7, 0.3]);
% set(obj.connectionStatusText, 'String', '未连接');
% set(obj.connectionStatusText, 'ForegroundColor', 'red');
%
% % 如果正在运行,停止监控
% if obj.isRunning
% obj.stopMonitoring();
% end
% end
% end
%
% function toggleConnection(obj, ~, ~)
% % 连接/断开串口
% if ~obj.isConnected
% % 连接串口
% obj.connectSerialPort();
% else
% % 断开串口
% obj.disconnectSerialPort();
% end
% end
%
% function connectSerialPort(obj, port, baudRate)
% % 连接串口
% if nargin < 2
% % 从GUI获取参数
% portList = get(obj.portDropdown, 'String');
% portIdx = get(obj.portDropdown, 'Value');
% port = portList{portIdx};
%
% baudRateList = get(obj.baudRateDropdown, 'String');
% baudRateIdx = get(obj.baudRateDropdown, 'Value');
% baudRate = str2double(baudRateList{baudRateIdx});
% end
%
% try
% fprintf('正在连接串口 %s (波特率: %d)...\n', port, baudRate);
%
% % 如果已有串口连接,先断开
% if ~isempty(obj.serialPort) && isvalid(obj.serialPort)
% obj.disconnectSerialPort();
% end
%
% % 创建串口对象
% obj.serialPort = serialport(port, baudRate);
% obj.serialPort.Timeout = 5; % 设置超时为5秒
% configureTerminator(obj.serialPort, "CR/LF");
%
% % 设置回调函数
% configureCallback(obj.serialPort, "terminator", ...
% @(src, event)obj.readDataCallback(src, event));
%
% % 更新状态
% obj.isConnected = true;
% obj.deviceReady = false;
%
% % 更新GUI
% set(obj.messageText, 'String', sprintf('串口 %s 连接成功', port));
% set(obj.messageText, 'ForegroundColor', 'green');
%
% set(obj.connectionInfoText, 'String', ...
% sprintf('端口: %s, 波特率: %d', port, baudRate));
%
% % 更新状态面板中的串口信息
% set(obj.comInfoText, 'String', sprintf('%s, %d bps', port, baudRate));
%
% % 更新控件状态
% obj.updateControlStates();
%
% % 清空串口缓冲区
% flush(obj.serialPort);
%
% % 发送初始测试命令
% writeline(obj.serialPort, "AT");
% obj.appendToReceiveText('[发送] AT (测试连接)');
%
% fprintf('串口连接成功\n');
%
% catch ME
% % 连接失败
% obj.isConnected = false;
% set(obj.messageText, 'String', sprintf('连接失败: %s', ME.message));
% set(obj.messageText, 'ForegroundColor', 'red');
%
% fprintf('串口连接失败: %s\n', ME.message);
% errordlg(sprintf('无法打开串口 %s:\n%s', port, ME.message), '连接错误');
%
% % 更新控件状态
% obj.updateControlStates();
% end
% end
%
% function disconnectSerialPort(obj)
% % 断开串口连接
% if obj.isRunning
% obj.stopMonitoring();
% end
%
% if ~isempty(obj.serialPort) && isvalid(obj.serialPort)
% try
% fprintf('正在断开串口连接...\n');
%
% % 移除回调函数
% configureCallback(obj.serialPort, "off");
%
% % 关闭串口
% delete(obj.serialPort);
% obj.serialPort = [];
%
% set(obj.messageText, 'String', '串口已断开');
% set(obj.messageText, 'ForegroundColor', 'blue');
%
% fprintf('串口已断开\n');
%
% catch ME
% warning('断开串口时出错: %s', ME.message);
% end
% end
%
% % 更新状态
% obj.isConnected = false;
% obj.deviceReady = false;
%
% % 更新控件状态
% obj.updateControlStates();
%
% % 更新状态面板
% set(obj.comInfoText, 'String', '未连接');
% set(obj.deviceStatusText, 'String', '未连接');
% set(obj.deviceStatusText, 'ForegroundColor', 'red');
% end
%
% function keyboardShortcut(obj, ~, event)
% % 键盘快捷键处理
% switch event.Key
% case 'l'
% % Ctrl+L 清空接收区
% if strcmp(event.Modifier, 'control')
% obj.clearReceiveText();
% disp('快捷键: Ctrl+L - 已清空接收区');
% end
% case 'c'
% % Ctrl+C 复制接收区内容到剪贴板
% if strcmp(event.Modifier, 'control')
% obj.copyReceiveTextToClipboard();
% end
% case 's'
% % Ctrl+S 保存数据
% if strcmp(event.Modifier, 'control')
% obj.saveData();
% end
% end
% end
%
% function copyReceiveTextToClipboard(obj)
% % 复制接收区内容到剪贴板
% try
% currentText = get(obj.receiveText, 'String');
% if iscell(currentText)
% % 将单元格数组转换为字符串
% fullText = '';
% for i = 1:length(currentText)
% fullText = sprintf('%s%s\n', fullText, currentText{i});
% end
% elseif ischar(currentText)
% fullText = currentText;
% else
% fullText = '';
% end
%
% clipboard('copy', fullText);
% fprintf('接收区内容已复制到剪贴板 (%d 字符)\n', length(fullText));
%
% % 显示短暂提示
% msg = msgbox('接收区内容已复制到剪贴板', '复制成功', 'help');
% pause(1);
% if ishandle(msg)
% close(msg);
% end
% catch ME
% warning('复制到剪贴板失败: %s', ME.message);
% end
% end
%
% function clearReceiveText(obj)
% % 清空接收文本框内容
% try
% % 获取接收文本框句柄
% receiveTextHandle = findobj(obj.fig, 'Tag', 'receiveText');
% if isempty(receiveTextHandle)
% receiveTextHandle = obj.receiveText;
% end
%
% % 清空文本框内容
% set(receiveTextHandle, 'String', '');
%
% % 清除命令历史记录
% obj.commandHistory = {};
%
% % 添加一条清空确认消息
% currentTime = datetime('now', 'Format', 'HH:mm:ss');
% confirmationMsg = ['接收区已清空 [', char(currentTime), ']'];
%
% % 在接收区显示确认消息
% obj.appendToReceiveText(confirmationMsg);
%
% fprintf('%s\n', confirmationMsg);
%
% % 强制更新显示
% drawnow;
%
% % 显示短暂的成功提示
% msg = msgbox('接收区内容已清空', '清空成功', 'help');
% pause(0.5);
% if ishandle(msg)
% close(msg);
% end
%
% catch ME
% warning('清空接收区失败: %s', ME.message);
% errordlg(['清空接收区失败: ', ME.message], '错误');
% end
% end
%
% function clearAllData(obj, ~, ~)
% % 清除所有数据(接收区、温度数据和图表)
%
% % 确认对话框
% choice = questdlg('确定要清除所有数据吗?', ...
% '确认清除', ...
% '是', '否', '否');
%
% if strcmp(choice, '是')
% try
% fprintf('清除所有数据...\n');
%
% % 1. 清空接收区
% obj.clearReceiveText();
%
% % 2. 清空温度数据
% obj.dataBuffer = [];
% obj.timeBuffer = [];
% obj.dataTable = table();
% obj.sampleCount = 0;
%
% % 3. 重置温度图表
% set(obj.temperaturePlot, 'XData', NaN, 'YData', NaN);
%
% % 4. 重置温度显示
% set(obj.currentTempText, 'String', '-- °C');
%
% % 5. 重置采样计数
% set(obj.sampleCountText, 'String', '0');
%
% % 6. 重置坐标轴
% xlim(obj.ax, [0, 10]);
% ylim(obj.ax, [0, 50]);
%
% fprintf('所有数据已清除\n');
%
% % 显示成功提示
% msgbox('所有数据已成功清除', '清除成功', 'help');
%
% catch ME
% warning('清除所有数据失败: %s', ME.message);
% errordlg(['清除所有数据失败: ', ME.message], '错误');
% end
% end
% end
%
% function setCommandText(obj, command)
% % 设置命令文本框的内容
% commandEdit = findobj(obj.fig, 'Tag', 'commandEdit');
% set(commandEdit, 'String', command);
% end
%
% function sendCustomCommand(obj)
% % 发送自定义命令
% if ~obj.isConnected
% warndlg('请先连接串口!', '警告');
% return;
% end
%
% commandEdit = findobj(obj.fig, 'Tag', 'commandEdit');
% command = get(commandEdit, 'String');
%
% if isempty(command)
% return;
% end
%
% try
% % 发送命令
% writeline(obj.serialPort, command);
%
% % 在接收区显示发送的命令
% obj.appendToReceiveText(['[发送] ', command]);
%
% % 记录命令历史
% obj.commandHistory{end+1} = command;
%
% % 如果是AT+T?命令,初始化状态
% if strcmp(command, 'AT+T?')
% obj.expectingTemperature = false; % 先等待OK
%
% % 更新设备状态
% set(obj.deviceStatusText, 'String', '命令已发送,等待响应');
% set(obj.deviceStatusText, 'ForegroundColor', 'yellow');
%
% % 启动超时计时器
% obj.startTimeoutTimer();
% end
%
% fprintf('已发送命令: %s\n', command);
%
% catch ME
% warning('发送命令失败: %s', ME.message);
% obj.appendToReceiveText(['[错误] 发送失败: ', ME.message]);
% end
% end
%
% function appendToReceiveText(obj, text)
% % 向接收文本框追加文本
% if isstring(text)
% text = char(text); % 将string转换为字符向量
% end
%
% % 使用datetime生成时间戳
% timestamp = datetime('now', 'Format', 'HH:mm:ss.SSS');
%
% % 使用sprintf格式化字符串
% newText = sprintf('[%s] %s', char(timestamp), text);
%
% % 获取当前文本
% currentText = get(obj.receiveText, 'String');
%
% % 处理不同类型的输入
% if iscell(currentText)
% % 如果是单元格数组,添加到开头
% newCell = [{newText}; currentText];
%
% % 限制显示行数(最多200行)
% if length(newCell) > 200
% newCell = newCell(1:200);
% end
%
% set(obj.receiveText, 'String', newCell);
% elseif ischar(currentText) || isstring(currentText)
% % 如果是字符向量或字符串
% if isempty(currentText)
% set(obj.receiveText, 'String', {newText});
% else
% set(obj.receiveText, 'String', [{newText}; {currentText}]);
% end
% else
% % 其他情况,直接设置
% set(obj.receiveText, 'String', {newText});
% end
%
% % 强制更新显示
% drawnow;
% end
%
% function updateInterval(obj)
% % 更新查询间隔
% try
% intervalEdit = findobj(obj.fig, 'Tag', 'intervalEdit');
% newInterval = str2double(get(intervalEdit, 'String'));
%
% if isnan(newInterval) || newInterval <= 0
% warndlg('请输入有效的正数', '无效输入');
% set(intervalEdit, 'String', num2str(obj.queryTimer.Period));
% return;
% end
%
% % 更新定时器间隔
% if strcmp(obj.queryTimer.Running, 'on')
% stop(obj.queryTimer);
% obj.queryTimer.Period = newInterval;
% start(obj.queryTimer);
% else
% obj.queryTimer.Period = newInterval;
% end
%
% fprintf('查询间隔已更新为: %.2f 秒\n', newInterval);
% obj.appendToReceiveText(['查询间隔更新为: ', num2str(newInterval), ' 秒']);
%
% catch ME
% warning('更新查询间隔失败: %s', ME.message);
% end
% end
%
% function updateMaxPoints(obj)
% % 更新显示数据点数
% try
% maxPointsEdit = findobj(obj.fig, 'Tag', 'maxPointsEdit');
% newMaxPoints = str2double(get(maxPointsEdit, 'String'));
%
% if isnan(newMaxPoints) || newMaxPoints < 10
% warndlg('请输入有效的数字(最小10)', '无效输入');
% set(maxPointsEdit, 'String', num2str(obj.maxPoints));
% return;
% end
%
% obj.maxPoints = newMaxPoints;
%
% % 如果当前数据超过新的最大点数,进行截断
% if length(obj.dataBuffer) > obj.maxPoints
% obj.dataBuffer = obj.dataBuffer(end-obj.maxPoints+1:end);
% obj.timeBuffer = obj.timeBuffer(end-obj.maxPoints+1:end);
%
% % 更新绘图
% if ~isempty(obj.timeBuffer)
% set(obj.temperaturePlot, ...
% 'XData', obj.timeBuffer, ...
% 'YData', obj.dataBuffer);
% end
% end
%
% fprintf('显示数据点数已更新为: %d\n', newMaxPoints);
% obj.appendToReceiveText(['显示数据点数更新为: ', num2str(newMaxPoints)]);
%
% catch ME
% warning('更新数据点数失败: %s', ME.message);
% end
% end
%
% function toggleMonitoring(obj, ~, ~)
% % 开始/停止监控
% if ~obj.isConnected
% warndlg('请先连接串口!', '警告');
% return;
% end
%
% if ~obj.isRunning
% % 开始监控
% obj.startMonitoring();
% else
% % 停止监控
% obj.stopMonitoring();
% end
% end
%
% function startMonitoring(obj)
% % 开始监控
% fprintf('开始监控温度数据...\n');
%
% obj.isRunning = true;
% obj.startTime = datetime('now');
% obj.dataBuffer = [];
% obj.timeBuffer = [];
% obj.dataTable = table();
% obj.deviceReady = false;
% obj.expectingTemperature = false;
% obj.sampleCount = 0;
%
% % 清空串口缓冲区
% flush(obj.serialPort);
%
% % 更新按钮文本
% set(obj.startBtn, 'String', '停止监控');
% set(obj.startBtn, 'BackgroundColor', [0.9, 0.3, 0.3]);
%
% % 更新状态
% set(obj.systemStatusText, 'String', '监控中...');
% set(obj.systemStatusText, 'ForegroundColor', 'green');
%
% set(obj.deviceStatusText, 'String', '等待设备响应');
% set(obj.deviceStatusText, 'ForegroundColor', 'yellow');
%
% % 发送温度查询命令
% obj.sendTemperatureQuery();
%
% % 启动定时器
% start(obj.queryTimer);
%
% % 重置采样计数显示
% set(obj.sampleCountText, 'String', '0');
%
% % 使用datetime获取当前时间
% currentTime = datetime('now', 'Format', 'HH:mm:ss');
%
% disp('开始监控温度数据');
% obj.appendToReceiveText('开始监控温度数据');
% obj.appendToReceiveText(['已发送AT+T?命令,等待设备响应... [', char(currentTime), ']']);
% end
%
% function stopMonitoring(obj)
% % 停止监控
% fprintf('停止监控温度数据...\n');
%
% obj.isRunning = false;
% obj.expectingTemperature = false;
%
% % 停止定时器
% if isvalid(obj.queryTimer)
% stop(obj.queryTimer);
% end
%
% % 停止超时计时器
% if isfield(obj, 'timeoutTimer') && isvalid(obj.timeoutTimer)
% stop(obj.timeoutTimer);
% end
%
% if isfield(obj, 'tempTimeoutTimer') && isvalid(obj.tempTimeoutTimer)
% stop(obj.tempTimeoutTimer);
% end
%
% % 更新按钮文本
% set(obj.startBtn, 'String', '开始监控');
% set(obj.startBtn, 'BackgroundColor', [0.3, 0.7, 0.3]);
%
% % 更新状态
% set(obj.systemStatusText, 'String', '已停止');
% set(obj.systemStatusText, 'ForegroundColor', 'blue');
%
% set(obj.deviceStatusText, 'String', '监控已停止');
% set(obj.deviceStatusText, 'ForegroundColor', 'red');
%
% % 如果有自动保存,则保存数据
% if strcmp(get(obj.autoSaveBtn, 'String'), '自动保存: 开启')
% obj.saveData();
% end
%
% disp('停止监控温度数据');
% obj.appendToReceiveText('停止监控温度数据');
% end
%
% function sendTemperatureQuery(obj)
% % 发送AT+T?命令查询温度
% if ~obj.isRunning
% return;
% end
%
% try
% % 发送查询命令
% writeline(obj.serialPort, "AT+T?");
% obj.expectingTemperature = false; % 先等待OK响应
%
% % 在接收区显示发送的命令
% obj.appendToReceiveText('[发送] AT+T?');
%
% % 更新设备状态
% set(obj.deviceStatusText, 'String', '命令已发送,等待OK响应');
% set(obj.deviceStatusText, 'ForegroundColor', 'yellow');
%
% % 启动超时计时器
% obj.startTimeoutTimer();
%
% catch ME
% warning('发送查询命令失败: %s', ME.message);
% obj.appendToReceiveText(['[错误] 发送AT+T?失败: ', ME.message]);
%
% % 更新设备状态
% set(obj.deviceStatusText, 'String', '发送失败');
% set(obj.deviceStatusText, 'ForegroundColor', 'red');
% end
% end
%
% function queryTemperature(obj)
% % 定时查询温度(由定时器调用)
% if ~obj.isRunning
% return;
% end
%
% % 发送查询命令
% obj.sendTemperatureQuery();
% end
%
% function startTimeoutTimer(obj)
% % 启动超时计时器,防止等待响应超时
% if isfield(obj, 'timeoutTimer') && isvalid(obj.timeoutTimer)
% stop(obj.timeoutTimer);
% delete(obj.timeoutTimer);
% end
%
% obj.timeoutTimer = timer(...
% 'ExecutionMode', 'singleShot', ...
% 'StartDelay', 2, ... % 2秒超时
% 'TimerFcn', @(~, ~)obj.handleTimeout(), ...
% 'Name', 'ResponseTimeoutTimer');
%
% start(obj.timeoutTimer);
% end
%
% function handleTimeout(obj)
% % 处理响应超时
% if ~obj.isRunning
% return;
% end
%
% obj.appendToReceiveText('[超时] 等待响应超时');
%
% % 更新设备状态
% set(obj.deviceStatusText, 'String', '等待超时');
% set(obj.deviceStatusText, 'ForegroundColor', 'orange');
%
% % 重置状态
% obj.expectingTemperature = false;
% end
%
% function startTemperatureTimeoutTimer(obj)
% % 启动温度数据接收超时计时器
% if isfield(obj, 'tempTimeoutTimer') && isvalid(obj.tempTimeoutTimer)
% stop(obj.tempTimeoutTimer);
% delete(obj.tempTimeoutTimer);
% end
%
% obj.tempTimeoutTimer = timer(...
% 'ExecutionMode', 'singleShot', ...
% 'StartDelay', 1, ... % 1秒超时
% 'TimerFcn', @(~, ~)obj.handleTemperatureTimeout(), ...
% 'Name', 'TemperatureTimeoutTimer');
%
% start(obj.tempTimeoutTimer);
% end
%
% function handleTemperatureTimeout(obj)
% % 处理温度数据接收超时
% if ~obj.isRunning
% return;
% end
%
% if obj.expectingTemperature
% obj.appendToReceiveText('[警告] 温度数据接收超时');
%
% % 更新设备状态
% set(obj.deviceStatusText, 'String', '温度数据超时');
% set(obj.deviceStatusText, 'ForegroundColor', 'orange');
%
% % 重置期望标志
% obj.expectingTemperature = false;
% end
% end
%
% function readDataCallback(obj, src, ~)
% % 串口数据读取回调函数 - 完整修复版本
% % 正确处理通信流程:发送AT+T? -> 接收OK -> 接收温度数据
%
% if ~obj.isRunning
% return;
% end
%
% try
% % 读取一行数据
% data = readline(src);
% data = strtrim(data); % 去除首尾空白字符
%
% % 在接收区显示接收到的数据
% obj.appendToReceiveText(['[接收] ', data]);
%
% % 如果是空数据,忽略
% if isempty(data)
% return;
% end
%
% % 停止超时计时器
% if isfield(obj, 'timeoutTimer') && isvalid(obj.timeoutTimer)
% stop(obj.timeoutTimer);
% end
%
% if isfield(obj, 'tempTimeoutTimer') && isvalid(obj.tempTimeoutTimer)
% stop(obj.tempTimeoutTimer);
% end
%
% % 检查是否为OK响应
% if strcmpi(data, 'OK')
% % 收到OK响应
% obj.deviceReady = true;
%
% % 更新设备状态
% set(obj.deviceStatusText, 'String', '收到OK,等待温度数据');
% set(obj.deviceStatusText, 'ForegroundColor', 'green');
%
% % OK响应后期待温度数据
% obj.expectingTemperature = true;
%
% % 启动温度数据接收超时计时器
% obj.startTemperatureTimeoutTimer();
%
% return;
% end
%
% % 检查是否期望接收温度数据
% if obj.expectingTemperature
% % 尝试解析温度值
% [temperature, success] = obj.parseTemperatureData(data);
%
% if success
% % 数据除以10得到实际温度
% actualTemperature = temperature / 10;
%
% % 处理温度数据
% obj.processTemperatureData(actualTemperature, temperature);
%
% % 重置期望标志
% obj.expectingTemperature = false;
%
% % 更新设备状态
% set(obj.deviceStatusText, 'String', '温度接收完成');
% set(obj.deviceStatusText, 'ForegroundColor', 'green');
%
% else
% % 如果不是有效的温度数据
% obj.appendToReceiveText(['无效的温度数据: "', data, '"']);
% obj.expectingTemperature = false;
%
% % 更新设备状态
% set(obj.deviceStatusText, 'String', '数据格式错误');
% set(obj.deviceStatusText, 'ForegroundColor', 'red');
% end
% else
% % 如果不是期望接收温度数据,可能是其他响应
% obj.appendToReceiveText(['接收到非期望数据: "', data, '"']);
%
% % 尝试自动检测是否为温度数据
% [temperature, success] = obj.parseTemperatureData(data);
% if success
% obj.appendToReceiveText(['自动检测到温度数据: ', num2str(temperature)]);
% actualTemperature = temperature / 10;
% obj.processTemperatureData(actualTemperature, temperature);
% end
% end
%
% catch ME
% warning('数据读取错误: %s', ME.message);
% obj.appendToReceiveText(['[错误] 读取数据失败: ', ME.message]);
% end
% end
%
% function [temperature, success] = parseTemperatureData(obj, data)
% % 解析温度数据
% % 支持多种格式:纯数字、包含T=的数字、包含单位的数字等
%
% success = false;
% temperature = NaN;
%
% % 尝试直接转换为数字
% tempValue = str2double(data);
% if ~isnan(tempValue)
% temperature = tempValue;
% success = true;
% return;
% end
%
% % 尝试从字符串中提取数字
% % 支持格式:T=253, Temp:253, 253C, 25.3
% numStr = regexp(data, '[-+]?\d*\.?\d+', 'match');
% if ~isempty(numStr)
% tempValue = str2double(numStr{1});
% if ~isnan(tempValue)
% temperature = tempValue;
% success = true;
% return;
% end
% end
%
% % 如果都没有成功,尝试其他可能的格式
% if contains(data, 'OK')
% % 这可能是一个误判的OK响应
% success = false;
% elseif contains(data, 'ERROR') || contains(data, 'ERR')
% obj.appendToReceiveText(['设备报告错误: ', data]);
% success = false;
% end
% end
%
% function processTemperatureData(obj, actualTemperature, rawTemperature)
% % 处理温度数据
% % actualTemperature: 实际温度值
% % rawTemperature: 原始温度值
%
% % 增加采样计数
% obj.sampleCount = obj.sampleCount + 1;
%
% % 计算相对时间
% currentTime = seconds(datetime('now') - obj.startTime);
%
% % 调试输出
% obj.appendToReceiveText(['温度: ', num2str(actualTemperature, '%.2f'), ...
% ' °C (原始值: ', num2str(rawTemperature), ')']);
%
% % 更新数据缓冲区
% obj.dataBuffer(end+1) = actualTemperature;
% obj.timeBuffer(end+1) = currentTime;
%
% % 限制缓冲区大小
% if length(obj.dataBuffer) > obj.maxPoints
% obj.dataBuffer = obj.dataBuffer(end-obj.maxPoints+1:end);
% obj.timeBuffer = obj.timeBuffer(end-obj.maxPoints+1:end);
% end
%
% % 更新绘图
% if ~isempty(obj.timeBuffer)
% set(obj.temperaturePlot, ...
% 'XData', obj.timeBuffer, ...
% 'YData', obj.dataBuffer);
%
% % 自动调整坐标轴
% obj.adjustAxesLimits();
% end
%
% % 更新温度显示
% set(obj.currentTempText, 'String', [num2str(actualTemperature, '%.2f'), ' °C']);
%
% % 更新采样计数显示
% set(obj.sampleCountText, 'String', num2str(obj.sampleCount));
%
% % 添加到数据表
% newRow = table(datetime('now'), actualTemperature, currentTime, ...
% 'VariableNames', {'Timestamp', 'Temperature', 'ElapsedTime'});
% obj.dataTable = [obj.dataTable; newRow];
%
% % 实时保存到文件(如果启用自动保存)
% if strcmp(get(obj.autoSaveBtn, 'String'), '自动保存: 开启')
% obj.appendToFile(newRow);
% end
% end
%
% function adjustAxesLimits(obj)
% % 自动调整坐标轴范围
% if ~isempty(obj.timeBuffer)
% xlim(obj.ax, [min(obj.timeBuffer), max(obj.timeBuffer)]);
% end
%
% if ~isempty(obj.dataBuffer)
% yRange = [min(obj.dataBuffer), max(obj.dataBuffer)];
% if yRange(1) == yRange(2)
% % 如果所有值相同,设置一个合适的范围
% ylim(obj.ax, [yRange(1)-0.5, yRange(1)+0.5]);
% else
% % 添加10%的边距
% yMargin = (yRange(2) - yRange(1)) * 0.1;
% ylim(obj.ax, [yRange(1)-yMargin, yRange(2)+yMargin]);
% end
% end
% end
%
% function toggleAutoSave(obj, src, ~)
% % 切换自动保存状态
% if strcmp(get(src, 'String'), '自动保存: 关闭')
% set(src, 'String', '自动保存: 开启');
% set(src, 'BackgroundColor', [0.2, 0.8, 0.2]);
%
% % 开始自动保存
% obj.startAutoSave();
% obj.appendToReceiveText('自动保存已开启');
% else
% set(src, 'String', '自动保存: 关闭');
% set(src, 'BackgroundColor', [0.8, 0.2, 0.2]);
%
% % 停止自动保存
% obj.stopAutoSave();
% obj.appendToReceiveText('自动保存已关闭');
% end
% end
%
% function startAutoSave(obj)
% % 开始自动保存
% if isempty(obj.fileID)
% % 使用datetime生成文件名
% currentDateTime = datetime('now', 'Format', 'yyyyMMdd_HHmmss');
% obj.logFileName = ['temperature_log_', char(currentDateTime), '.csv'];
%
% try
% obj.fileID = fopen(obj.logFileName, 'w');
%
% if obj.fileID == -1
% error('无法创建日志文件: %s', obj.logFileName);
% end
%
% % 写入表头
% fprintf(obj.fileID, 'Timestamp,ElapsedTime(s),Temperature(C)\n');
% obj.appendToReceiveText(['开始自动保存到文件: ', obj.logFileName]);
%
% % 如果已有数据,保存现有数据
% if ~isempty(obj.dataTable)
% for i = 1:height(obj.dataTable)
% % 使用datetime格式化时间戳
% timestamp = obj.dataTable.Timestamp(i);
% if isdatetime(timestamp)
% timestampStr = char(timestamp);
% else
% timestampStr = char(datetime(timestamp, 'Format', 'yyyy-MM-dd HH:mm:ss.SSS'));
% end
% fprintf(obj.fileID, '%s,%.4f,%.2f\n', ...
% timestampStr, obj.dataTable.ElapsedTime(i), obj.dataTable.Temperature(i));
% end
% obj.appendToReceiveText(['已保存 ', num2str(height(obj.dataTable)), ...
% ' 条现有数据到文件']);
% end
% catch ME
% warning('无法开始自动保存: %s', ME.message);
% obj.appendToReceiveText(['[错误] 开始自动保存失败: ', ME.message]);
% obj.fileID = [];
% end
% end
% end
%
% function appendToFile(obj, newRow)
% % 追加新数据到文件
% if ~isempty(obj.fileID) && obj.fileID > 0
% try
% % 使用datetime格式化时间戳
% timestamp = newRow.Timestamp;
% if isdatetime(timestamp)
% timestampStr = char(timestamp);
% else
% timestampStr = char(datetime(timestamp, 'Format', 'yyyy-MM-dd HH:mm:ss.SSS'));
% end
% fprintf(obj.fileID, '%s,%.4f,%.2f\n', ...
% timestampStr, newRow.ElapsedTime, newRow.Temperature);
% catch ME
% warning('保存数据到文件失败: %s', ME.message);
% end
% end
% end
%
% function stopAutoSave(obj)
% % 停止自动保存
% if ~isempty(obj.fileID) && obj.fileID > 0
% try
% fclose(obj.fileID);
% obj.appendToReceiveText(['已停止自动保存数据到文件: ', obj.logFileName]);
% catch ME
% warning('关闭文件时出错: %s', ME.message);
% end
% obj.fileID = [];
% end
% end
%
% function saveData(obj, ~, ~)
% % 手动保存数据到文件
% if isempty(obj.dataTable)
% warndlg('没有数据可保存!', '警告');
% return;
% end
%
% % 使用datetime生成文件名
% currentDateTime = datetime('now', 'Format', 'yyyyMMdd_HHmmss');
% defaultName = ['temperature_data_', char(currentDateTime), '.csv'];
%
% % 选择保存位置
% [selectedFileName, selectedPathName] = uiputfile( ...
% {'*.csv', 'CSV文件 (*.csv)'; ...
% '*.mat', 'MAT文件 (*.mat)'; ...
% '*.xlsx', 'Excel文件 (*.xlsx)'}, ...
% '保存数据', defaultName);
%
% if selectedFileName ~= 0
% fullPath = fullfile(selectedPathName, selectedFileName);
% [~, ~, fileExtension] = fileparts(fullPath);
%
% try
% switch lower(fileExtension)
% case '.csv'
% writetable(obj.dataTable, fullPath);
% case '.mat'
% temperatureData = obj.dataTable;
% save(fullPath, 'temperatureData');
% case '.xlsx'
% writetable(obj.dataTable, fullPath);
% end
% obj.appendToReceiveText(['数据已保存到: ', fullPath]);
% msgbox(['数据已成功保存到: ', fullPath], '保存成功');
%
% fprintf('数据已保存到: %s\n', fullPath);
%
% catch ME
% errordlg(['保存失败: ', ME.message], '错误');
% obj.appendToReceiveText(['[错误] 保存失败: ', ME.message]);
% end
% end
% end
%
% function closeFigure(obj, ~, ~)
% % 关闭图形窗口时的清理工作
% % 确认对话框
% choice = questdlg('确定要退出系统吗?', ...
% '确认退出', ...
% '是', '否', '否');
%
% if ~strcmp(choice, '是')
% return;
% end
%
% fprintf('正在关闭系统...\n');
%
% if obj.isRunning
% obj.isRunning = false;
% end
%
% % 停止定时器
% if isvalid(obj.queryTimer)
% stop(obj.queryTimer);
% delete(obj.queryTimer);
% end
%
% % 停止超时计时器
% if isfield(obj, 'timeoutTimer') && isvalid(obj.timeoutTimer)
% stop(obj.timeoutTimer);
% delete(obj.timeoutTimer);
% end
%
% if isfield(obj, 'tempTimeoutTimer') && isvalid(obj.tempTimeoutTimer)
% stop(obj.tempTimeoutTimer);
% delete(obj.tempTimeoutTimer);
% end
%
% % 断开串口
% obj.disconnectSerialPort();
%
% % 关闭文件(如果打开)
% obj.stopAutoSave();
%
% % 删除图形
% try
% delete(obj.fig);
% catch ME
% warning('删除图形窗口时出错: %s', ME.message);
% end
%
% fprintf('系统已退出\n');
% end
%
% function delete(obj)
% % 析构函数
% obj.closeFigure();
% end
% end
% end
classdef TemperatureMonitor < handle
% 温度监测类 - 实时读取、显示和保存串口温度数据
% 完整版本,支持串口扫描、波特率选择、数据解析等功能
properties
serialPort % 串口对象
fig % 主图形窗口句柄
ax % 坐标轴句柄
temperaturePlot % 温度曲线绘图句柄
dataBuffer % 温度数据缓冲区
timeBuffer % 时间数据缓冲区
maxPoints = 1000 % 显示的最大数据点数
isRunning = false % 是否正在运行
startTime % 监控开始时间
dataTable % 数据表格
queryTimer % 查询定时器
deviceReady = false % 设备是否准备好
sampleCount = 0 % 采样计数
expectingTemperature = false % 是否期望接收温度数据
isConnected = false % 串口连接状态
end
properties (Access = private)
logFileName % 日志文件名
fileID % 文件ID
receiveText % 接收数据显示文本框句柄
commandHistory % 命令历史记录
portDropdown % 串口号下拉菜单句柄
baudRateDropdown % 波特率下拉菜单句柄
connectButton % 连接/断开按钮句柄
connectionStatusText % 连接状态文本句柄
connectionInfoText % 连接信息文本句柄
messageText % 消息文本句柄
startBtn % 开始/停止监控按钮句柄
resendBtn % 重新发送命令按钮句柄
autoSaveBtn % 自动保存开关按钮句柄
deviceStatusText % 设备状态文本句柄
systemStatusText % 系统状态文本句柄
sampleCountText % 采样点数文本句柄
currentTempText % 当前温度文本句柄
comInfoText % 串口信息文本句柄
timeoutTimer % 超时定时器
tempTimeoutTimer % 温度数据超时定时器
defaultPorts = {'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'COM10'}
defaultBaudRates = {'9600', '19200', '38400', '57600', '115200', '230400', '460800', '921600'}
end
methods
function obj = TemperatureMonitor(varargin)
% 构造函数
% TemperatureMonitor() - 无参数,创建GUI后手动选择串口
% TemperatureMonitor(port, baudRate, queryInterval) - 带参数创建
fprintf('=== 温度监测系统初始化 ===\n');
% 初始化数据缓冲
obj.dataBuffer = [];
obj.timeBuffer = [];
obj.dataTable = table();
obj.commandHistory = {};
% 解析输入参数
if nargin >= 2
port = varargin{1};
baudRate = varargin{2};
if nargin >= 3
queryInterval = varargin{3};
else
queryInterval = 1;
end
% 尝试打开串口
try
obj.connectSerialPort(port, baudRate);
catch ME
warning('串口连接失败: %s', ME.message);
obj.isConnected = false;
end
else
% 无参数调用,先创建GUI,不立即连接串口
port = 'COM3'; % 默认串口
baudRate = 9600; % 默认波特率
if nargin == 1
queryInterval = varargin{1};
else
queryInterval = 1;
end
obj.isConnected = false;
end
% 创建查询定时器
obj.queryTimer = timer(...
'ExecutionMode', 'fixedRate', ...
'Period', queryInterval, ...
'TimerFcn', @(~, ~)obj.queryTemperature(), ...
'Name', 'TemperatureQueryTimer', ...
'BusyMode', 'queue');
% 创建GUI界面
obj.createGUI(port, baudRate, queryInterval);
fprintf('温度监测系统初始化完成\n');
end
function createGUI(obj, defaultPort, defaultBaudRate, queryInterval)
% 创建图形界面 - 优化版本,确保所有元素可见
fprintf('正在创建GUI界面...\n');
% 获取屏幕尺寸
screenSize = get(0, 'ScreenSize');
screenWidth = screenSize(3);
screenHeight = screenSize(4);
% 计算窗口尺寸(适应屏幕)
windowWidth = min(1200, screenWidth * 0.7);
windowHeight = min(750, screenHeight * 0.7);
% 计算窗口位置(居中显示)
windowLeft = (screenWidth - windowWidth) / 2;
windowTop = (screenHeight - windowHeight) / 2;
obj.fig = figure('Name', '温度实时监测与串口调试系统', ...
'NumberTitle', 'off', ...
'Position', [windowLeft, windowTop, windowWidth, windowHeight], ...
'CloseRequestFcn', @obj.closeFigure, ...
'MenuBar', 'none', ...
'ToolBar', 'figure', ...
'Resize', 'on'); % 允许调整窗口大小
% 创建连接配置区域
configPanel = uipanel('Parent', obj.fig, ...
'Title', '串口配置', ...
'Position', [0.02, 0.85, 0.96, 0.14], ...
'FontSize', 11, ...
'FontWeight', 'bold', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 串口号选择
uicontrol('Parent', configPanel, ...
'Style', 'text', ...
'String', '串口号:', ...
'Position', [20, 70, 80, 25], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 获取可用串口
availablePorts = obj.scanSerialPorts();
if isempty(availablePorts)
availablePorts = obj.defaultPorts;
end
% 设置默认选择的端口
defaultPortIdx = find(strcmp(availablePorts, defaultPort), 1);
if isempty(defaultPortIdx)
defaultPortIdx = 1;
if ~isempty(availablePorts)
defaultPort = availablePorts{1};
else
defaultPort = 'COM1';
end
end
obj.portDropdown = uicontrol('Parent', configPanel, ...
'Style', 'popupmenu', ...
'String', availablePorts, ...
'Value', defaultPortIdx, ...
'Position', [100, 70, 120, 25], ...
'FontSize', 10, ...
'BackgroundColor', 'white', ...
'Tag', 'portDropdown');
% 扫描串口按钮
uicontrol('Parent', configPanel, ...
'Style', 'pushbutton', ...
'String', '扫描串口', ...
'Position', [230, 70, 80, 25], ...
'FontSize', 10, ...
'BackgroundColor', [0.2, 0.6, 1.0], ...
'ForegroundColor', 'white', ...
'FontWeight', 'bold', ...
'Callback', @(~, ~)obj.scanAndUpdatePorts());
% 波特率选择
uicontrol('Parent', configPanel, ...
'Style', 'text', ...
'String', '波特率:', ...
'Position', [320, 70, 80, 25], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 设置默认选择的波特率
defaultBaudRateIdx = find(strcmp(obj.defaultBaudRates, num2str(defaultBaudRate)), 1);
if isempty(defaultBaudRateIdx)
defaultBaudRateIdx = 1;
end
obj.baudRateDropdown = uicontrol('Parent', configPanel, ...
'Style', 'popupmenu', ...
'String', obj.defaultBaudRates, ...
'Value', defaultBaudRateIdx, ...
'Position', [400, 70, 120, 25], ...
'FontSize', 10, ...
'BackgroundColor', 'white', ...
'Tag', 'baudRateDropdown');
% 连接/断开按钮
obj.connectButton = uicontrol('Parent', configPanel, ...
'Style', 'pushbutton', ...
'String', '连接串口', ...
'Position', [530, 70, 100, 25], ...
'FontSize', 11, ...
'BackgroundColor', [0.3, 0.7, 0.3], ...
'ForegroundColor', 'white', ...
'FontWeight', 'bold', ...
'Callback', @obj.toggleConnection);
% 退出系统按钮
uicontrol('Parent', configPanel, ...
'Style', 'pushbutton', ...
'String', '退出系统', ...
'Position', [640, 70, 100, 25], ...
'FontSize', 11, ...
'BackgroundColor', [0.9, 0.3, 0.3], ...
'ForegroundColor', 'white', ...
'FontWeight', 'bold', ...
'Callback', @(~, ~)obj.closeFigure());
% 连接状态显示
obj.connectionStatusText = uicontrol('Parent', configPanel, ...
'Style', 'text', ...
'String', '未连接', ...
'Position', [750, 70, 150, 25], ...
'FontSize', 10, ...
'ForegroundColor', 'red', ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 连接信息显示
obj.connectionInfoText = uicontrol('Parent', configPanel, ...
'Style', 'text', ...
'String', sprintf('端口: %s, 波特率: %s', defaultPort, num2str(defaultBaudRate)), ...
'Position', [20, 30, 300, 25], ...
'FontSize', 9, ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 消息显示区域
obj.messageText = uicontrol('Parent', configPanel, ...
'Style', 'text', ...
'String', '请选择串口并点击"连接串口"按钮开始', ...
'Position', [20, 5, 500, 25], ...
'FontSize', 9, ...
'ForegroundColor', 'blue', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 创建温度监测区域
tempPanel = uipanel('Parent', obj.fig, ...
'Title', '温度监测', ...
'Position', [0.02, 0.48, 0.63, 0.35], ...
'FontSize', 11, ...
'FontWeight', 'bold', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 温度绘图区域
obj.ax = axes('Parent', tempPanel, ...
'Position', [0.10, 0.15, 0.85, 0.80]);
xlabel(obj.ax, '时间 (秒)', 'FontSize', 10, 'FontWeight', 'bold');
ylabel(obj.ax, '温度 (°C)', 'FontSize', 10, 'FontWeight', 'bold');
title(obj.ax, '温度实时变化曲线', 'FontSize', 11, 'FontWeight', 'bold');
grid(obj.ax, 'on');
hold(obj.ax, 'on');
% 设置坐标轴颜色和字体
set(obj.ax, 'XColor', [0.2, 0.2, 0.2], 'YColor', [0.2, 0.2, 0.2]);
set(obj.ax, 'FontSize', 9);
% 初始化绘图
obj.temperaturePlot = plot(obj.ax, NaN, NaN, ...
'b-', 'LineWidth', 1.5, ...
'Marker', 'o', 'MarkerSize', 4, ...
'MarkerFaceColor', 'r', ...
'MarkerEdgeColor', 'b');
% 设置初始坐标轴范围
xlim(obj.ax, [0, 10]);
ylim(obj.ax, [0, 50]);
% 创建串口调试区域
debugPanel = uipanel('Parent', obj.fig, ...
'Title', '串口调试', ...
'Position', [0.02, 0.02, 0.63, 0.45], ...
'FontSize', 11, ...
'FontWeight', 'bold', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 接收数据显示区域
uicontrol('Parent', debugPanel, ...
'Style', 'text', ...
'String', '接收数据:', ...
'Position', [10, 370, 80, 20], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 接收数据显示文本框(可滚动)
obj.receiveText = uicontrol('Parent', debugPanel, ...
'Style', 'edit', ...
'String', '', ...
'Position', [10, 50, 580, 320], ...
'FontSize', 9, ...
'Max', 100, ... % 允许多行
'Min', 0, ... % 允许多行
'HorizontalAlignment', 'left', ...
'BackgroundColor', 'white', ...
'Enable', 'inactive', ... % 设置为只读
'Tag', 'receiveText'); % 添加标签以便查找
% 发送命令区域
uicontrol('Parent', debugPanel, ...
'Style', 'text', ...
'String', '发送命令:', ...
'Position', [10, 400, 80, 20], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
commandEdit = uicontrol('Parent', debugPanel, ...
'Style', 'edit', ...
'String', 'AT+T?', ...
'Position', [100, 395, 350, 25], ...
'FontSize', 10, ...
'Tag', 'commandEdit', ...
'BackgroundColor', 'white');
% 发送按钮
uicontrol('Parent', debugPanel, ...
'Style', 'pushbutton', ...
'String', '发送', ...
'Position', [460, 395, 60, 25], ...
'FontSize', 10, ...
'BackgroundColor', [0.1, 0.5, 0.9], ...
'ForegroundColor', 'white', ...
'FontWeight', 'bold', ...
'Callback', @(~, ~)obj.sendCustomCommand());
% 清空接收区按钮
uicontrol('Parent', debugPanel, ...
'Style', 'pushbutton', ...
'String', '清空接收区', ...
'Position', [530, 395, 80, 25], ...
'FontSize', 10, ...
'BackgroundColor', [0.9, 0.5, 0.1], ...
'ForegroundColor', 'white', ...
'FontWeight', 'bold', ...
'TooltipString', '清除串口调试窗口中的所有内容', ...
'Callback', @(~, ~)obj.clearReceiveText());
% 常用命令
commands = {'AT+T?', 'AT+VER', 'AT+HELP', 'AT+STATUS'};
commandNames = {'查询温度', '版本信息', '帮助信息', '状态查询'};
for i = 1:length(commands)
uicontrol('Parent', debugPanel, ...
'Style', 'pushbutton', ...
'String', commandNames{i}, ...
'Position', [100 + (i-1)*120, 20, 100, 20], ...
'FontSize', 9, ...
'BackgroundColor', [0.4, 0.4, 0.8], ...
'ForegroundColor', 'white', ...
'TooltipString', sprintf('发送命令: %s', commands{i}), ...
'Callback', @(src, ~)obj.setCommandText(commands{i}));
end
% 添加快捷键说明
uicontrol('Parent', debugPanel, ...
'Style', 'text', ...
'String', '快捷键: Ctrl+L=清空, Ctrl+S=保存', ...
'Position', [10, 20, 150, 20], ...
'FontSize', 8, ...
'ForegroundColor', [0.5, 0.5, 0.5], ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 创建控制面板
controlPanel = uipanel('Parent', obj.fig, ...
'Title', '控制面板', ...
'Position', [0.67, 0.48, 0.31, 0.35], ...
'FontSize', 11, ...
'FontWeight', 'bold', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 查询间隔设置
uicontrol('Parent', controlPanel, ...
'Style', 'text', ...
'String', '查询间隔 (秒):', ...
'Position', [15, 250, 180, 20], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
uicontrol('Parent', controlPanel, ...
'Style', 'edit', ...
'String', num2str(queryInterval), ...
'Position', [15, 225, 180, 25], ...
'FontSize', 10, ...
'Tag', 'intervalEdit', ...
'BackgroundColor', 'white', ...
'Callback', @(~, ~)obj.updateInterval);
% 数据点数设置
uicontrol('Parent', controlPanel, ...
'Style', 'text', ...
'String', '显示数据点数:', ...
'Position', [15, 190, 180, 20], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
uicontrol('Parent', controlPanel, ...
'Style', 'edit', ...
'String', num2str(obj.maxPoints), ...
'Position', [15, 165, 180, 25], ...
'FontSize', 10, ...
'Tag', 'maxPointsEdit', ...
'BackgroundColor', 'white', ...
'Callback', @(~, ~)obj.updateMaxPoints);
% 开始/停止按钮
obj.startBtn = uicontrol('Parent', controlPanel, ...
'Style', 'pushbutton', ...
'String', '开始监控', ...
'Position', [15, 120, 180, 35], ...
'FontSize', 11, ...
'BackgroundColor', [0.3, 0.7, 0.3], ...
'ForegroundColor', 'white', ...
'FontWeight', 'bold', ...
'Callback', @obj.toggleMonitoring, ...
'Enable', 'off'); % 初始时禁用,连接串口后启用
% 重新发送命令按钮
obj.resendBtn = uicontrol('Parent', controlPanel, ...
'Style', 'pushbutton', ...
'String', '重新发送AT+T?', ...
'Position', [15, 75, 180, 35], ...
'FontSize', 11, ...
'BackgroundColor', [0.9, 0.8, 0.1], ...
'ForegroundColor', 'black', ...
'FontWeight', 'bold', ...
'Callback', @(~, ~)obj.sendTemperatureQuery(), ...
'Enable', 'off'); % 初始时禁用
% 创建数据保存面板
savePanel = uipanel('Parent', obj.fig, ...
'Title', '数据保存', ...
'Position', [0.67, 0.30, 0.31, 0.17], ...
'FontSize', 11, ...
'FontWeight', 'bold', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 手动保存按钮
uicontrol('Parent', savePanel, ...
'Style', 'pushbutton', ...
'String', '手动保存数据', ...
'Position', [15, 40, 180, 35], ...
'FontSize', 11, ...
'BackgroundColor', [0.1, 0.5, 0.9], ...
'ForegroundColor', 'white', ...
'FontWeight', 'bold', ...
'Callback', @obj.saveData);
% 自动保存开关
obj.autoSaveBtn = uicontrol('Parent', savePanel, ...
'Style', 'togglebutton', ...
'String', '自动保存: 关闭', ...
'Position', [15, 85, 180, 35], ...
'FontSize', 11, ...
'BackgroundColor', [0.8, 0.2, 0.2], ...
'ForegroundColor', 'white', ...
'FontWeight', 'bold', ...
'Tag', 'autoSaveBtn', ...
'Callback', @obj.toggleAutoSave);
% 创建状态显示面板
statusPanel = uipanel('Parent', obj.fig, ...
'Title', '状态信息', ...
'Position', [0.67, 0.02, 0.31, 0.27], ...
'FontSize', 11, ...
'FontWeight', 'bold', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 设备状态
uicontrol('Parent', statusPanel, ...
'Style', 'text', ...
'String', '设备状态:', ...
'Position', [15, 190, 70, 20], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
obj.deviceStatusText = uicontrol('Parent', statusPanel, ...
'Style', 'text', ...
'String', '未连接', ...
'Position', [90, 190, 120, 20], ...
'FontSize', 10, ...
'ForegroundColor', 'red', ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 系统状态
uicontrol('Parent', statusPanel, ...
'Style', 'text', ...
'String', '系统状态:', ...
'Position', [15, 165, 70, 20], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
obj.systemStatusText = uicontrol('Parent', statusPanel, ...
'Style', 'text', ...
'String', '等待连接', ...
'Position', [90, 165, 120, 20], ...
'FontSize', 10, ...
'ForegroundColor', 'blue', ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 采样点数
uicontrol('Parent', statusPanel, ...
'Style', 'text', ...
'String', '采样点数:', ...
'Position', [15, 140, 70, 20], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
obj.sampleCountText = uicontrol('Parent', statusPanel, ...
'Style', 'text', ...
'String', '0', ...
'Position', [90, 140, 60, 20], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 当前温度
uicontrol('Parent', statusPanel, ...
'Style', 'text', ...
'String', '当前温度:', ...
'Position', [15, 115, 70, 20], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
obj.currentTempText = uicontrol('Parent', statusPanel, ...
'Style', 'text', ...
'String', '-- °C', ...
'Position', [90, 115, 80, 20], ...
'FontSize', 11, ...
'ForegroundColor', 'blue', ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 串口信息
uicontrol('Parent', statusPanel, ...
'Style', 'text', ...
'String', '串口信息:', ...
'Position', [15, 90, 70, 20], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
obj.comInfoText = uicontrol('Parent', statusPanel, ...
'Style', 'text', ...
'String', '未连接', ...
'Position', [90, 90, 120, 20], ...
'FontSize', 10, ...
'FontWeight', 'bold', ...
'HorizontalAlignment', 'left', ...
'BackgroundColor', [0.95, 0.95, 0.95]);
% 清除所有数据按钮
uicontrol('Parent', statusPanel, ...
'Style', 'pushbutton', ...
'String', '清除所有数据', ...
'Position', [15, 50, 180, 30], ...
'FontSize', 11, ...
'BackgroundColor', [0.8, 0.3, 0.3], ...
'ForegroundColor', 'white', ...
'FontWeight', 'bold', ...
'TooltipString', '清除接收区、温度数据和图表', ...
'Callback', @obj.clearAllData);
% 添加快捷键支持
set(obj.fig, 'KeyPressFcn', @obj.keyboardShortcut);
% 更新控件状态
obj.updateControlStates();
fprintf('GUI界面创建完成\n');
end
function ports = scanSerialPorts(obj)
% 扫描可用串口
try
fprintf('正在扫描可用串口...\n');
% 尝试使用MATLAB的串口列表功能
if exist('serialportlist', 'file')
ports = serialportlist('available');
fprintf('使用 serialportlist 函数\n');
elseif exist('seriallist', 'file')
ports = seriallist;
fprintf('使用 seriallist 函数\n');
else
% 如果上述函数都不存在,使用默认端口列表
ports = obj.defaultPorts;
fprintf('使用默认端口列表\n');
end
% 转换为单元格数组
if isstring(ports)
ports = cellstr(ports);
elseif ischar(ports)
ports = {ports};
end
% 如果没有任何端口,使用默认列表
if isempty(ports)
ports = obj.defaultPorts;
fprintf('未检测到可用串口,使用默认列表\n');
end
fprintf('扫描到 %d 个可用串口:\n', length(ports));
for i = 1:length(ports)
fprintf(' %s\n', ports{i});
end
catch ME
fprintf('扫描串口失败: %s\n', ME.message);
ports = obj.defaultPorts;
end
end
function scanAndUpdatePorts(obj)
% 扫描并更新串口下拉菜单
try
fprintf('手动扫描串口...\n');
% 扫描可用串口
availablePorts = obj.scanSerialPorts();
% 更新下拉菜单
set(obj.portDropdown, 'String', availablePorts);
if ~isempty(availablePorts)
set(obj.portDropdown, 'Value', 1);
end
% 更新消息
set(obj.messageText, 'String', ...
sprintf('扫描完成,找到 %d 个可用串口', length(availablePorts)));
set(obj.messageText, 'ForegroundColor', 'green');
fprintf('串口列表已更新\n');
catch ME
warning('扫描串口失败: %s', ME.message);
set(obj.messageText, 'String', '扫描串口失败');
set(obj.messageText, 'ForegroundColor', 'red');
end
end
function updateControlStates(obj)
% 更新控件状态(启用/禁用)
if obj.isConnected
% 串口已连接,启用相关按钮
set(obj.startBtn, 'Enable', 'on');
set(obj.resendBtn, 'Enable', 'on');
set(obj.connectButton, 'String', '断开连接');
set(obj.connectButton, 'BackgroundColor', [0.9, 0.3, 0.3]);
set(obj.connectionStatusText, 'String', '已连接');
set(obj.connectionStatusText, 'ForegroundColor', 'green');
else
% 串口未连接,禁用相关按钮
set(obj.startBtn, 'Enable', 'off');
set(obj.resendBtn, 'Enable', 'off');
set(obj.connectButton, 'String', '连接串口');
set(obj.connectButton, 'BackgroundColor', [0.3, 0.7, 0.3]);
set(obj.connectionStatusText, 'String', '未连接');
set(obj.connectionStatusText, 'ForegroundColor', 'red');
% 如果正在运行,停止监控
if obj.isRunning
obj.stopMonitoring();
end
end
end
function toggleConnection(obj, ~, ~)
% 连接/断开串口
if ~obj.isConnected
% 连接串口
obj.connectSerialPort();
else
% 断开串口
obj.disconnectSerialPort();
end
end
function connectSerialPort(obj, port, baudRate)
% 连接串口
if nargin < 2
% 从GUI获取参数
portList = get(obj.portDropdown, 'String');
portIdx = get(obj.portDropdown, 'Value');
port = portList{portIdx};
baudRateList = get(obj.baudRateDropdown, 'String');
baudRateIdx = get(obj.baudRateDropdown, 'Value');
baudRate = str2double(baudRateList{baudRateIdx});
end
try
fprintf('正在连接串口 %s (波特率: %d)...\n', port, baudRate);
% 如果已有串口连接,先断开
if ~isempty(obj.serialPort) && isvalid(obj.serialPort)
obj.disconnectSerialPort();
end
% 创建串口对象
obj.serialPort = serialport(port, baudRate);
obj.serialPort.Timeout = 5; % 设置超时为5秒
configureTerminator(obj.serialPort, "CR/LF");
% 设置回调函数
configureCallback(obj.serialPort, "terminator", ...
@(src, event)obj.readDataCallback(src, event));
% 更新状态
obj.isConnected = true;
obj.deviceReady = false;
% 更新GUI
set(obj.messageText, 'String', sprintf('串口 %s 连接成功', port));
set(obj.messageText, 'ForegroundColor', 'green');
set(obj.connectionInfoText, 'String', ...
sprintf('端口: %s, 波特率: %d', port, baudRate));
% 更新状态面板中的串口信息
set(obj.comInfoText, 'String', sprintf('%s, %d bps', port, baudRate));
% 更新控件状态
obj.updateControlStates();
% 清空串口缓冲区
flush(obj.serialPort);
% 发送初始测试命令
writeline(obj.serialPort, "AT");
obj.appendToReceiveText('[发送] AT (测试连接)');
fprintf('串口连接成功\n');
catch ME
% 连接失败
obj.isConnected = false;
set(obj.messageText, 'String', sprintf('连接失败: %s', ME.message));
set(obj.messageText, 'ForegroundColor', 'red');
fprintf('串口连接失败: %s\n', ME.message);
errordlg(sprintf('无法打开串口 %s:\n%s', port, ME.message), '连接错误');
% 更新控件状态
obj.updateControlStates();
end
end
function disconnectSerialPort(obj)
% 断开串口连接
if obj.isRunning
obj.stopMonitoring();
end
if ~isempty(obj.serialPort) && isvalid(obj.serialPort)
try
fprintf('正在断开串口连接...\n');
% 移除回调函数
configureCallback(obj.serialPort, "off");
% 关闭串口
delete(obj.serialPort);
obj.serialPort = [];
set(obj.messageText, 'String', '串口已断开');
set(obj.messageText, 'ForegroundColor', 'blue');
fprintf('串口已断开\n');
catch ME
warning('断开串口时出错: %s', ME.message);
end
end
% 更新状态
obj.isConnected = false;
obj.deviceReady = false;
% 更新控件状态
obj.updateControlStates();
% 更新状态面板
set(obj.comInfoText, 'String', '未连接');
set(obj.deviceStatusText, 'String', '未连接');
set(obj.deviceStatusText, 'ForegroundColor', 'red');
end
function keyboardShortcut(obj, ~, event)
% 键盘快捷键处理
switch event.Key
case 'l'
% Ctrl+L 清空接收区
if strcmp(event.Modifier, 'control')
obj.clearReceiveText();
disp('快捷键: Ctrl+L - 已清空接收区');
end
case 'c'
% Ctrl+C 复制接收区内容到剪贴板
if strcmp(event.Modifier, 'control')
obj.copyReceiveTextToClipboard();
end
case 's'
% Ctrl+S 保存数据
if strcmp(event.Modifier, 'control')
obj.saveData();
end
end
end
function copyReceiveTextToClipboard(obj)
% 复制接收区内容到剪贴板
try
currentText = get(obj.receiveText, 'String');
if iscell(currentText)
% 将单元格数组转换为字符串
fullText = '';
for i = 1:length(currentText)
fullText = sprintf('%s%s\n', fullText, currentText{i});
end
elseif ischar(currentText)
fullText = currentText;
else
fullText = '';
end
clipboard('copy', fullText);
fprintf('接收区内容已复制到剪贴板 (%d 字符)\n', length(fullText));
% 显示短暂提示
msg = msgbox('接收区内容已复制到剪贴板', '复制成功', 'help');
pause(1);
if ishandle(msg)
close(msg);
end
catch ME
warning('复制到剪贴板失败: %s', ME.message);
end
end
function clearReceiveText(obj)
% 清空接收文本框内容
try
% 获取接收文本框句柄
receiveTextHandle = findobj(obj.fig, 'Tag', 'receiveText');
if isempty(receiveTextHandle)
receiveTextHandle = obj.receiveText;
end
% 清空文本框内容
set(receiveTextHandle, 'String', '');
% 清除命令历史记录
obj.commandHistory = {};
% 添加一条清空确认消息
currentTime = datetime('now', 'Format', 'HH:mm:ss');
confirmationMsg = ['接收区已清空 [', char(currentTime), ']'];
% 在接收区显示确认消息
obj.appendToReceiveText(confirmationMsg);
fprintf('%s\n', confirmationMsg);
% 强制更新显示
drawnow;
% 显示短暂的成功提示
msg = msgbox('接收区内容已清空', '清空成功', 'help');
pause(0.5);
if ishandle(msg)
close(msg);
end
catch ME
warning('清空接收区失败: %s', ME.message);
errordlg(['清空接收区失败: ', ME.message], '错误');
end
end
function clearAllData(obj, ~, ~)
% 清除所有数据(接收区、温度数据和图表)
% 确认对话框
choice = questdlg('确定要清除所有数据吗?', ...
'确认清除', ...
'是', '否', '否');
if strcmp(choice, '是')
try
fprintf('清除所有数据...\n');
% 1. 清空接收区
obj.clearReceiveText();
% 2. 清空温度数据
obj.dataBuffer = [];
obj.timeBuffer = [];
obj.dataTable = table();
obj.sampleCount = 0;
% 3. 重置温度图表
set(obj.temperaturePlot, 'XData', NaN, 'YData', NaN);
% 4. 重置温度显示
set(obj.currentTempText, 'String', '-- °C');
% 5. 重置采样计数
set(obj.sampleCountText, 'String', '0');
% 6. 重置坐标轴
xlim(obj.ax, [0, 10]);
ylim(obj.ax, [0, 50]);
fprintf('所有数据已清除\n');
% 显示成功提示
msgbox('所有数据已成功清除', '清除成功', 'help');
catch ME
warning('清除所有数据失败: %s', ME.message);
errordlg(['清除所有数据失败: ', ME.message], '错误');
end
end
end
function setCommandText(obj, command)
% 设置命令文本框的内容
commandEdit = findobj(obj.fig, 'Tag', 'commandEdit');
set(commandEdit, 'String', command);
end
function sendCustomCommand(obj)
% 发送自定义命令
if ~obj.isConnected
warndlg('请先连接串口!', '警告');
return;
end
commandEdit = findobj(obj.fig, 'Tag', 'commandEdit');
command = get(commandEdit, 'String');
if isempty(command)
return;
end
try
% 发送命令
writeline(obj.serialPort, command);
% 在接收区显示发送的命令
obj.appendToReceiveText(['[发送] ', command]);
% 记录命令历史
obj.commandHistory{end+1} = command;
% 如果是AT+T?命令,初始化状态
if strcmp(command, 'AT+T?')
obj.expectingTemperature = false; % 先等待OK
% 更新设备状态
set(obj.deviceStatusText, 'String', '命令已发送,等待响应');
set(obj.deviceStatusText, 'ForegroundColor', 'yellow');
% 启动超时计时器
obj.startTimeoutTimer();
end
fprintf('已发送命令: %s\n', command);
catch ME
warning('发送命令失败: %s', ME.message);
obj.appendToReceiveText(['[错误] 发送失败: ', ME.message]);
end
end
function appendToReceiveText(obj, text)
% 向接收文本框追加文本
if isstring(text)
text = char(text); % 将string转换为字符向量
end
% 使用datetime生成时间戳
timestamp = datetime('now', 'Format', 'HH:mm:ss.SSS');
% 使用sprintf格式化字符串
newText = sprintf('[%s] %s', char(timestamp), text);
% 获取当前文本
currentText = get(obj.receiveText, 'String');
% 处理不同类型的输入
if iscell(currentText)
% 如果是单元格数组,添加到开头
newCell = [{newText}; currentText];
% 限制显示行数(最多200行)
if length(newCell) > 200
newCell = newCell(1:200);
end
set(obj.receiveText, 'String', newCell);
elseif ischar(currentText) || isstring(currentText)
% 如果是字符向量或字符串
if isempty(currentText)
set(obj.receiveText, 'String', {newText});
else
set(obj.receiveText, 'String', [{newText}; {currentText}]);
end
else
% 其他情况,直接设置
set(obj.receiveText, 'String', {newText});
end
% 强制更新显示
drawnow;
end
function updateInterval(obj)
% 更新查询间隔
try
intervalEdit = findobj(obj.fig, 'Tag', 'intervalEdit');
newInterval = str2double(get(intervalEdit, 'String'));
if isnan(newInterval) || newInterval <= 0
warndlg('请输入有效的正数', '无效输入');
set(intervalEdit, 'String', num2str(obj.queryTimer.Period));
return;
end
% 更新定时器间隔
if strcmp(obj.queryTimer.Running, 'on')
stop(obj.queryTimer);
obj.queryTimer.Period = newInterval;
start(obj.queryTimer);
else
obj.queryTimer.Period = newInterval;
end
fprintf('查询间隔已更新为: %.2f 秒\n', newInterval);
obj.appendToReceiveText(['查询间隔更新为: ', num2str(newInterval), ' 秒']);
catch ME
warning('更新查询间隔失败: %s', ME.message);
end
end
function updateMaxPoints(obj)
% 更新显示数据点数
try
maxPointsEdit = findobj(obj.fig, 'Tag', 'maxPointsEdit');
newMaxPoints = str2double(get(maxPointsEdit, 'String'));
if isnan(newMaxPoints) || newMaxPoints < 10
warndlg('请输入有效的数字(最小10)', '无效输入');
set(maxPointsEdit, 'String', num2str(obj.maxPoints));
return;
end
obj.maxPoints = newMaxPoints;
% 如果当前数据超过新的最大点数,进行截断
if length(obj.dataBuffer) > obj.maxPoints
obj.dataBuffer = obj.dataBuffer(end-obj.maxPoints+1:end);
obj.timeBuffer = obj.timeBuffer(end-obj.maxPoints+1:end);
% 更新绘图
if ~isempty(obj.timeBuffer)
set(obj.temperaturePlot, ...
'XData', obj.timeBuffer, ...
'YData', obj.dataBuffer);
end
end
fprintf('显示数据点数已更新为: %d\n', newMaxPoints);
obj.appendToReceiveText(['显示数据点数更新为: ', num2str(newMaxPoints)]);
catch ME
warning('更新数据点数失败: %s', ME.message);
end
end
function toggleMonitoring(obj, ~, ~)
% 开始/停止监控
if ~obj.isConnected
warndlg('请先连接串口!', '警告');
return;
end
if ~obj.isRunning
% 开始监控
obj.startMonitoring();
else
% 停止监控
obj.stopMonitoring();
end
end
function startMonitoring(obj)
% 开始监控
fprintf('开始监控温度数据...\n');
obj.isRunning = true;
obj.startTime = datetime('now');
obj.dataBuffer = [];
obj.timeBuffer = [];
obj.dataTable = table();
obj.deviceReady = false;
obj.expectingTemperature = false;
obj.sampleCount = 0;
% 清空串口缓冲区
flush(obj.serialPort);
% 更新按钮文本
set(obj.startBtn, 'String', '停止监控');
set(obj.startBtn, 'BackgroundColor', [0.9, 0.3, 0.3]);
% 更新状态
set(obj.systemStatusText, 'String', '监控中...');
set(obj.systemStatusText, 'ForegroundColor', 'green');
set(obj.deviceStatusText, 'String', '等待设备响应');
set(obj.deviceStatusText, 'ForegroundColor', 'yellow');
% 发送温度查询命令
obj.sendTemperatureQuery();
% 启动定时器
start(obj.queryTimer);
% 重置采样计数显示
set(obj.sampleCountText, 'String', '0');
% 使用datetime获取当前时间
currentTime = datetime('now', 'Format', 'HH:mm:ss');
disp('开始监控温度数据');
obj.appendToReceiveText('开始监控温度数据');
obj.appendToReceiveText(['已发送AT+T?命令,等待设备响应... [', char(currentTime), ']']);
end
function stopMonitoring(obj)
% 停止监控
fprintf('停止监控温度数据...\n');
obj.isRunning = false;
obj.expectingTemperature = false;
% 停止定时器
if isvalid(obj.queryTimer)
stop(obj.queryTimer);
end
% 停止超时计时器
if isfield(obj, 'timeoutTimer') && isvalid(obj.timeoutTimer)
stop(obj.timeoutTimer);
end
if isfield(obj, 'tempTimeoutTimer') && isvalid(obj.tempTimeoutTimer)
stop(obj.tempTimeoutTimer);
end
% 更新按钮文本
set(obj.startBtn, 'String', '开始监控');
set(obj.startBtn, 'BackgroundColor', [0.3, 0.7, 0.3]);
% 更新状态
set(obj.systemStatusText, 'String', '已停止');
set(obj.systemStatusText, 'ForegroundColor', 'blue');
set(obj.deviceStatusText, 'String', '监控已停止');
set(obj.deviceStatusText, 'ForegroundColor', 'red');
% 如果有自动保存,则保存数据
if strcmp(get(obj.autoSaveBtn, 'String'), '自动保存: 开启')
obj.saveData();
end
disp('停止监控温度数据');
obj.appendToReceiveText('停止监控温度数据');
end
function sendTemperatureQuery(obj)
% 发送AT+T?命令查询温度
if ~obj.isRunning
return;
end
try
% 发送查询命令
writeline(obj.serialPort, "AT+T?");
obj.expectingTemperature = false; % 先等待OK响应
% 在接收区显示发送的命令
obj.appendToReceiveText('[发送] AT+T?');
% 更新设备状态
set(obj.deviceStatusText, 'String', '命令已发送,等待OK响应');
set(obj.deviceStatusText, 'ForegroundColor', 'yellow');
% 启动超时计时器
obj.startTimeoutTimer();
catch ME
warning('发送查询命令失败: %s', ME.message);
obj.appendToReceiveText(['[错误] 发送AT+T?失败: ', ME.message]);
% 更新设备状态
set(obj.deviceStatusText, 'String', '发送失败');
set(obj.deviceStatusText, 'ForegroundColor', 'red');
end
end
function queryTemperature(obj)
% 定时查询温度(由定时器调用)
if ~obj.isRunning
return;
end
% 发送查询命令
obj.sendTemperatureQuery();
end
function startTimeoutTimer(obj)
% 启动超时计时器,防止等待响应超时
if isfield(obj, 'timeoutTimer') && isvalid(obj.timeoutTimer)
stop(obj.timeoutTimer);
delete(obj.timeoutTimer);
end
obj.timeoutTimer = timer(...
'ExecutionMode', 'singleShot', ...
'StartDelay', 2, ... % 2秒超时
'TimerFcn', @(~, ~)obj.handleTimeout(), ...
'Name', 'ResponseTimeoutTimer');
start(obj.timeoutTimer);
end
function handleTimeout(obj)
% 处理响应超时
if ~obj.isRunning
return;
end
obj.appendToReceiveText('[超时] 等待响应超时');
% 更新设备状态
set(obj.deviceStatusText, 'String', '等待超时');
set(obj.deviceStatusText, 'ForegroundColor', 'orange');
% 重置状态
obj.expectingTemperature = false;
end
function startTemperatureTimeoutTimer(obj)
% 启动温度数据接收超时计时器
if isfield(obj, 'tempTimeoutTimer') && isvalid(obj.tempTimeoutTimer)
stop(obj.tempTimeoutTimer);
delete(obj.tempTimeoutTimer);
end
obj.tempTimeoutTimer = timer(...
'ExecutionMode', 'singleShot', ...
'StartDelay', 1, ... % 1秒超时
'TimerFcn', @(~, ~)obj.handleTemperatureTimeout(), ...
'Name', 'TemperatureTimeoutTimer');
start(obj.tempTimeoutTimer);
end
function handleTemperatureTimeout(obj)
% 处理温度数据接收超时
if ~obj.isRunning
return;
end
if obj.expectingTemperature
obj.appendToReceiveText('[警告] 温度数据接收超时');
% 更新设备状态
set(obj.deviceStatusText, 'String', '温度数据超时');
set(obj.deviceStatusText, 'ForegroundColor', 'orange');
% 重置期望标志
obj.expectingTemperature = false;
end
end
function readDataCallback(obj, src, ~)
% 串口数据读取回调函数 - 完整修复版本
% 正确处理通信流程:发送AT+T? -> 接收OK -> 接收温度数据
if ~obj.isRunning
return;
end
try
% 读取一行数据
data = readline(src);
data = strtrim(data); % 去除首尾空白字符
% 在接收区显示接收到的数据
obj.appendToReceiveText(['[接收] ', data]);
% 如果是空数据,忽略
if isempty(data)
return;
end
% 停止超时计时器
if isfield(obj, 'timeoutTimer') && isvalid(obj.timeoutTimer)
stop(obj.timeoutTimer);
end
if isfield(obj, 'tempTimeoutTimer') && isvalid(obj.tempTimeoutTimer)
stop(obj.tempTimeoutTimer);
end
% 检查是否为OK响应
if strcmpi(data, 'OK')
% 收到OK响应
obj.deviceReady = true;
% 更新设备状态
set(obj.deviceStatusText, 'String', '收到OK,等待温度数据');
set(obj.deviceStatusText, 'ForegroundColor', 'green');
% OK响应后期待温度数据
obj.expectingTemperature = true;
% 启动温度数据接收超时计时器
obj.startTemperatureTimeoutTimer();
return;
end
% 检查是否期望接收温度数据
if obj.expectingTemperature
% 尝试解析温度值
[temperature, success] = obj.parseTemperatureData(data);
if success
% 数据除以10得到实际温度
actualTemperature = temperature / 10;
% 处理温度数据
obj.processTemperatureData(actualTemperature, temperature);
% 重置期望标志
obj.expectingTemperature = false;
% 更新设备状态
set(obj.deviceStatusText, 'String', '温度接收完成');
set(obj.deviceStatusText, 'ForegroundColor', 'green');
else
% 如果不是有效的温度数据
obj.appendToReceiveText(['无效的温度数据: "', data, '"']);
obj.expectingTemperature = false;
% 更新设备状态
set(obj.deviceStatusText, 'String', '数据格式错误');
set(obj.deviceStatusText, 'ForegroundColor', 'red');
end
else
% 如果不是期望接收温度数据,可能是其他响应
obj.appendToReceiveText(['接收到非期望数据: "', data, '"']);
% 尝试自动检测是否为温度数据
[temperature, success] = obj.parseTemperatureData(data);
if success
obj.appendToReceiveText(['自动检测到温度数据: ', num2str(temperature)]);
actualTemperature = temperature / 10;
obj.processTemperatureData(actualTemperature, temperature);
end
end
catch ME
warning('数据读取错误: %s', ME.message);
obj.appendToReceiveText(['[错误] 读取数据失败: ', ME.message]);
end
end
function [temperature, success] = parseTemperatureData(obj, data)
% 解析温度数据
% 支持多种格式:纯数字、包含T=的数字、包含单位的数字等
success = false;
temperature = NaN;
% 尝试直接转换为数字
tempValue = str2double(data);
if ~isnan(tempValue)
temperature = tempValue;
success = true;
return;
end
% 尝试从字符串中提取数字
% 支持格式:T=253, Temp:253, 253C, 25.3
numStr = regexp(data, '[-+]?\d*\.?\d+', 'match');
if ~isempty(numStr)
tempValue = str2double(numStr{1});
if ~isnan(tempValue)
temperature = tempValue;
success = true;
return;
end
end
% 如果都没有成功,尝试其他可能的格式
if contains(data, 'OK')
% 这可能是一个误判的OK响应
success = false;
elseif contains(data, 'ERROR') || contains(data, 'ERR')
obj.appendToReceiveText(['设备报告错误: ', data]);
success = false;
end
end
function processTemperatureData(obj, actualTemperature, rawTemperature)
% 处理温度数据
% actualTemperature: 实际温度值
% rawTemperature: 原始温度值
% 增加采样计数
obj.sampleCount = obj.sampleCount + 1;
% 计算相对时间
currentTime = seconds(datetime('now') - obj.startTime);
% 调试输出
obj.appendToReceiveText(['温度: ', num2str(actualTemperature, '%.2f'), ...
' °C (原始值: ', num2str(rawTemperature), ')']);
% 更新数据缓冲区
obj.dataBuffer(end+1) = actualTemperature;
obj.timeBuffer(end+1) = currentTime;
% 限制缓冲区大小
if length(obj.dataBuffer) > obj.maxPoints
obj.dataBuffer = obj.dataBuffer(end-obj.maxPoints+1:end);
obj.timeBuffer = obj.timeBuffer(end-obj.maxPoints+1:end);
end
% 更新绘图
if ~isempty(obj.timeBuffer)
set(obj.temperaturePlot, ...
'XData', obj.timeBuffer, ...
'YData', obj.dataBuffer);
% 自动调整坐标轴
obj.adjustAxesLimits();
end
% 更新温度显示
set(obj.currentTempText, 'String', [num2str(actualTemperature, '%.2f'), ' °C']);
% 更新采样计数显示
set(obj.sampleCountText, 'String', num2str(obj.sampleCount));
% 添加到数据表
newRow = table(datetime('now'), actualTemperature, currentTime, ...
'VariableNames', {'Timestamp', 'Temperature', 'ElapsedTime'});
obj.dataTable = [obj.dataTable; newRow];
% 实时保存到文件(如果启用自动保存)
if strcmp(get(obj.autoSaveBtn, 'String'), '自动保存: 开启')
obj.appendToFile(newRow);
end
end
function adjustAxesLimits(obj)
% 自动调整坐标轴范围
if ~isempty(obj.timeBuffer)
xlim(obj.ax, [min(obj.timeBuffer), max(obj.timeBuffer)]);
end
if ~isempty(obj.dataBuffer)
yRange = [min(obj.dataBuffer), max(obj.dataBuffer)];
if yRange(1) == yRange(2)
% 如果所有值相同,设置一个合适的范围
ylim(obj.ax, [yRange(1)-0.5, yRange(1)+0.5]);
else
% 添加10%的边距
yMargin = (yRange(2) - yRange(1)) * 0.1;
ylim(obj.ax, [yRange(1)-yMargin, yRange(2)+yMargin]);
end
end
end
function toggleAutoSave(obj, src, ~)
% 切换自动保存状态
if strcmp(get(src, 'String'), '自动保存: 关闭')
set(src, 'String', '自动保存: 开启');
set(src, 'BackgroundColor', [0.2, 0.8, 0.2]);
% 开始自动保存
obj.startAutoSave();
obj.appendToReceiveText('自动保存已开启');
else
set(src, 'String', '自动保存: 关闭');
set(src, 'BackgroundColor', [0.8, 0.2, 0.2]);
% 停止自动保存
obj.stopAutoSave();
obj.appendToReceiveText('自动保存已关闭');
end
end
function startAutoSave(obj)
% 开始自动保存
if isempty(obj.fileID)
% 使用datetime生成文件名
currentDateTime = datetime('now', 'Format', 'yyyyMMdd_HHmmss');
obj.logFileName = ['temperature_log_', char(currentDateTime), '.csv'];
try
obj.fileID = fopen(obj.logFileName, 'w');
if obj.fileID == -1
error('无法创建日志文件: %s', obj.logFileName);
end
% 写入表头
fprintf(obj.fileID, 'Timestamp,ElapsedTime(s),Temperature(C)\n');
obj.appendToReceiveText(['开始自动保存到文件: ', obj.logFileName]);
% 如果已有数据,保存现有数据
if ~isempty(obj.dataTable)
for i = 1:height(obj.dataTable)
% 使用datetime格式化时间戳
timestamp = obj.dataTable.Timestamp(i);
if isdatetime(timestamp)
timestampStr = char(timestamp);
else
timestampStr = char(datetime(timestamp, 'Format', 'yyyy-MM-dd HH:mm:ss.SSS'));
end
fprintf(obj.fileID, '%s,%.4f,%.2f\n', ...
timestampStr, obj.dataTable.ElapsedTime(i), obj.dataTable.Temperature(i));
end
obj.appendToReceiveText(['已保存 ', num2str(height(obj.dataTable)), ...
' 条现有数据到文件']);
end
catch ME
warning('无法开始自动保存: %s', ME.message);
obj.appendToReceiveText(['[错误] 开始自动保存失败: ', ME.message]);
obj.fileID = [];
end
end
end
function appendToFile(obj, newRow)
% 追加新数据到文件
if ~isempty(obj.fileID) && obj.fileID > 0
try
% 使用datetime格式化时间戳
timestamp = newRow.Timestamp;
if isdatetime(timestamp)
timestampStr = char(timestamp);
else
timestampStr = char(datetime(timestamp, 'Format', 'yyyy-MM-dd HH:mm:ss.SSS'));
end
fprintf(obj.fileID, '%s,%.4f,%.2f\n', ...
timestampStr, newRow.ElapsedTime, newRow.Temperature);
catch ME
warning('保存数据到文件失败: %s', ME.message);
end
end
end
function stopAutoSave(obj)
% 停止自动保存
if ~isempty(obj.fileID) && obj.fileID > 0
try
fclose(obj.fileID);
obj.appendToReceiveText(['已停止自动保存数据到文件: ', obj.logFileName]);
catch ME
warning('关闭文件时出错: %s', ME.message);
end
obj.fileID = [];
end
end
function saveData(obj, ~, ~)
% 手动保存数据到文件
if isempty(obj.dataTable)
warndlg('没有数据可保存!', '警告');
return;
end
% 使用datetime生成文件名
currentDateTime = datetime('now', 'Format', 'yyyyMMdd_HHmmss');
defaultName = ['temperature_data_', char(currentDateTime), '.csv'];
% 选择保存位置
[selectedFileName, selectedPathName] = uiputfile( ...
{'*.csv', 'CSV文件 (*.csv)'; ...
'*.mat', 'MAT文件 (*.mat)'; ...
'*.xlsx', 'Excel文件 (*.xlsx)'}, ...
'保存数据', defaultName);
if selectedFileName ~= 0
fullPath = fullfile(selectedPathName, selectedFileName);
[~, ~, fileExtension] = fileparts(fullPath);
try
switch lower(fileExtension)
case '.csv'
writetable(obj.dataTable, fullPath);
case '.mat'
temperatureData = obj.dataTable;
save(fullPath, 'temperatureData');
case '.xlsx'
writetable(obj.dataTable, fullPath);
end
obj.appendToReceiveText(['数据已保存到: ', fullPath]);
msgbox(['数据已成功保存到: ', fullPath], '保存成功');
fprintf('数据已保存到: %s\n', fullPath);
catch ME
errordlg(['保存失败: ', ME.message], '错误');
obj.appendToReceiveText(['[错误] 保存失败: ', ME.message]);
end
end
end
function closeFigure(obj, ~, ~)
% 关闭图形窗口时的清理工作
% 确认对话框
choice = questdlg('确定要退出系统吗?', ...
'确认退出', ...
'是', '否', '否');
if ~strcmp(choice, '是')
return;
end
fprintf('正在关闭系统...\n');
if obj.isRunning
obj.isRunning = false;
end
% 停止定时器
if isvalid(obj.queryTimer)
stop(obj.queryTimer);
delete(obj.queryTimer);
end
% 停止超时计时器
if isfield(obj, 'timeoutTimer') && isvalid(obj.timeoutTimer)
stop(obj.timeoutTimer);
delete(obj.timeoutTimer);
end
if isfield(obj, 'tempTimeoutTimer') && isvalid(obj.tempTimeoutTimer)
stop(obj.tempTimeoutTimer);
delete(obj.tempTimeoutTimer);
end
% 断开串口
obj.disconnectSerialPort();
% 关闭文件(如果打开)
obj.stopAutoSave();
% 删除图形
try
delete(obj.fig);
catch ME
warning('删除图形窗口时出错: %s', ME.message);
end
fprintf('系统已退出\n');
end
function delete(obj)
% 析构函数
obj.closeFigure();
end
end
end
999999999999999
9999999999999999
c
% run_temperature_monitor.m
% 温度监测系统启动脚本
clear all;
close all;
clc;
fprintf('========================================\n');
fprintf(' 温度实时监测与串口调试系统\n');
fprintf('========================================\n\n');
fprintf('系统正在启动,请稍候...\n\n');
% 创建系统实例
try
monitor = TemperatureMonitor();
fprintf('\n系统启动成功!\n');
fprintf('使用说明:\n');
fprintf('1. 在"串口配置"区域选择正确的串口号\n');
fprintf('2. 选择正确的波特率(通常为9600或115200)\n');
fprintf('3. 点击"连接串口"按钮\n');
fprintf('4. 连接成功后,点击"开始监控"按钮\n');
fprintf('5. 观察温度曲线和接收数据\n\n');
fprintf('提示:\n');
fprintf('- 点击"扫描串口"可刷新串口列表\n');
fprintf('- 接收区可查看详细的收发数据\n');
fprintf('- 使用Ctrl+L可清空接收区\n');
fprintf('- 使用Ctrl+S可保存数据\n\n');
fprintf('系统变量已保存为 monitor\n');
fprintf('========================================\n');
catch ME
fprintf('\n系统启动失败: %s\n', ME.message);
fprintf('\n可能的原因:\n');
fprintf('1. Instrument Control Toolbox 未安装\n');
fprintf('2. MATLAB版本过低(需要R2019b或更高)\n');
fprintf('3. 串口设备未连接或驱动未安装\n');
fprintf('========================================\n');
end