From 16db737f2a0b4d9da7a060481748987955cf6951 Mon Sep 17 00:00:00 2001 From: zqm Date: Fri, 24 Oct 2025 16:55:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=85=A8=E7=A7=BB=E6=A4=8D=EF=BC=8C?= =?UTF-8?q?=E4=BD=86=E8=BF=98=E6=9C=89=E5=A4=9A=E5=A4=84=E4=BA=8C=E4=B9=89?= =?UTF-8?q?=E6=80=A7=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Windows/CS/Framework4.0/Toprie/Toprie.ico | Bin 0 -> 67646 bytes .../CS/Framework4.0/Toprie/Toprie/Camera.cs | 443 ++- .../Toprie/Toprie/DeviceManager.cs | 2461 ++++++++++++++--- .../Framework4.0/Toprie/Toprie/Toprie.csproj | 6 + .../CS/Framework4.0/Toprie/Toprie/Toprie.ico | Bin 0 -> 67646 bytes 5 files changed, 2482 insertions(+), 428 deletions(-) create mode 100644 Windows/CS/Framework4.0/Toprie/Toprie.ico create mode 100644 Windows/CS/Framework4.0/Toprie/Toprie/Toprie.ico diff --git a/Windows/CS/Framework4.0/Toprie/Toprie.ico b/Windows/CS/Framework4.0/Toprie/Toprie.ico new file mode 100644 index 0000000000000000000000000000000000000000..2cf24ede14823749d432be8902f6eb556a08fa85 GIT binary patch literal 67646 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 private void InitializeDeviceManager() { - _deviceManager = new DeviceManager(); + _deviceManager = new DeviceManager + { + AutoReconnectEnabled = true, + ReconnectInterval = 2000, // 2秒 + MaxReconnectAttempts = 5 + }; // 注册图像接收事件 _deviceManager.ImageReceived += DeviceManager_ImageReceived; @@ -69,6 +75,8 @@ namespace JoyD.Windows.CS.Toprie // 注册连接异常事件 _deviceManager.ConnectionException += DeviceManager_ConnectionException; + + } /// @@ -81,34 +89,98 @@ namespace JoyD.Windows.CS.Toprie } /// - /// 启动并连接设备 + /// 启动相机 /// public void StartCamera() { try { - // 设置为热图模式 - _deviceManager.CurrentImageMode = ImageMode.Thermal; - - // 启用自动重连 - _deviceManager.AutoReconnectEnabled = true; - - // 连接设备 - bool connected = _deviceManager.ConnectDevice(); - - if (connected) + // 只有在没有接收图像时才启动 + if (!_isReceivingImage) { - // 开始接收图像 - StartReceiveImage(); + // 清理错误显示 + ShowError(string.Empty); + + // 使用异步方式连接设备和设置模式,避免UI线程阻塞 + ThreadPool.QueueUserWorkItem(delegate + { + try + { + // 设置为热图模式 + bool modeSet = false; + try + { + _deviceManager.SetImageMode(ImageMode.Thermal); + modeSet = true; + Console.WriteLine("已设置热图模式"); + } + catch (Exception ex) + { + Console.WriteLine($"设置热图模式失败: {ex.Message}"); + } + + // 启用自动重连 + _deviceManager.AutoReconnectEnabled = true; + + // 尝试连接设备 + if (_deviceManager.ConnectDevice()) + { + Console.WriteLine("设备连接成功"); + + // 如果热图模式未成功设置,在连接成功后再次尝试 + if (!modeSet) + { + try + { + _deviceManager.SetImageMode(ImageMode.Thermal); + Console.WriteLine("连接后已设置热图模式"); + } + catch (Exception ex) + { + Console.WriteLine($"连接后设置热图模式失败: {ex.Message}"); + } + } + + // 在连接成功后开始接收图像 + this.Invoke(new Action(() => + { + // 再次检查是否已在UI线程设置为接收状态 + if (!_isReceivingImage) + { + StartReceiveImage(); + } + })); + } + else + { + // 连接失败时显示错误 + string errorMsg = "设备连接失败,请检查设备状态或网络连接"; + this.Invoke(new Action(() => ShowError(errorMsg))); + Console.WriteLine(errorMsg); + } + } + catch (Exception ex) + { + // 捕获所有异常,防止后台线程崩溃 + string errorMsg = $"启动相机异常: {ex.Message}"; + this.Invoke(new Action(() => ShowError(errorMsg))); + Console.WriteLine(errorMsg); + Console.WriteLine(ex.StackTrace); + } + }); + + Console.WriteLine("相机启动流程已开始"); } else { - ShowError("无法连接到设备,请检查设备是否在线"); + Console.WriteLine("相机已在运行状态"); } } catch (Exception ex) { ShowError($"启动相机失败: {ex.Message}"); + Console.WriteLine($"启动相机主流程错误: {ex.Message}"); + Console.WriteLine(ex.StackTrace); } } @@ -155,36 +227,176 @@ namespace JoyD.Windows.CS.Toprie /// private void DeviceManager_ImageReceived(object sender, ImageReceivedEventArgs e) { - if (this.InvokeRequired) + // 不在这里使用using,因为我们需要将图像传递给UI线程 + Image image = e.ImageData; + + if (image == null) { - // 在UI线程上更新图像 - this.Invoke(new Action(() => UpdateImage(e.ImageData))); + Console.WriteLine("接收到空图像数据"); + return; } - else + + try { - UpdateImage(e.ImageData); + // 尝试创建图像的克隆以传递给UI线程 + Image clonedImage = null; + try + { + clonedImage = (Image)image.Clone(); + + // 验证克隆的图像是否有效 + if (clonedImage.Width <= 0 || clonedImage.Height <= 0) + { + Console.WriteLine("克隆的图像尺寸无效"); + clonedImage.Dispose(); + image.Dispose(); // 确保原始图像也被释放 + return; + } + } + catch (Exception ex) + { + Console.WriteLine($"克隆图像失败: {ex.Message}"); + image.Dispose(); // 确保原始图像被释放 + return; + } + + // 确保在UI线程上更新,并且检查控件是否已被释放 + if (!this.IsDisposed && !this.Disposing) + { + if (this.InvokeRequired) + { + try + { + // 使用BeginInvoke在UI线程上更新图像,避免可能的死锁问题 + this.BeginInvoke(new Action(() => + { + try + { + UpdateImageOnUI(clonedImage); + } + catch (Exception ex) + { + Console.WriteLine($"更新UI图像失败: {ex.Message}"); + // 如果UI更新失败,确保克隆的图像被释放 + if (clonedImage != null) + { + clonedImage.Dispose(); + } + } + })); + } + catch (Exception) + { + // 异常情况下确保释放图像资源 + clonedImage?.Dispose(); + throw; + } + } + else + { + // 在UI线程上直接更新图像 + UpdateImageOnUI(clonedImage); + } + } + else + { + // 如果控件已释放,确保释放图像资源 + clonedImage.Dispose(); + } + + // 释放原始图像资源 + image.Dispose(); + } + catch (Exception ex) + { + Console.WriteLine($"处理图像接收事件失败: {ex.Message}"); + // 确保在任何异常情况下都释放原始图像 + if (image != null) + { + try + { + image.Dispose(); + } + catch {} + } } } /// - /// 更新图像显示 + /// 在UI线程上更新图像 /// - private void UpdateImage(Image image) + private void UpdateImageOnUI(Image image) { + // 空值检查 + if (image == null) + { + Console.WriteLine("传入UpdateImageOnUI的图像为空"); + return; + } + try { - // 释放之前的图像资源 - if (imageBox.Image != null) + // 更新图像前先检查控件是否存在/已释放 + if (this.IsDisposed || imageBox == null || imageBox.IsDisposed) { - imageBox.Image.Dispose(); + // 如果控件已释放,确保图像也被释放 + image.Dispose(); + Console.WriteLine("控件已释放,无法更新图像"); + return; } - // 设置新图像 - imageBox.Image = (Image)image.Clone(); + // 保存旧图像引用,以便在设置新图像后释放 + Image oldImage = imageBox.Image; + + try + { + // 尝试设置新图像 + imageBox.Image = image; + + // 验证图像是否成功设置 + if (imageBox.Image != image) + { + Console.WriteLine("图像设置失败,可能参数无效"); + image.Dispose(); // 释放未成功设置的图像 + return; + } + + // 仅在新图像成功设置后释放旧图像 + if (oldImage != null && oldImage != image) // 防止自引用释放 + { + oldImage.Dispose(); + } + } + catch (ArgumentException ex) when (ex.Message.Contains("参数无效")) + { + // 特别处理"参数无效"异常 + Console.WriteLine($"图像参数无效: {ex.Message}"); + image.Dispose(); // 释放无效图像 + + // 尝试设置旧图像回来,如果可能的话 + if (oldImage != null) + { + try + { + imageBox.Image = oldImage; + } + catch + { + // 如果设置旧图像也失败,释放它 + oldImage.Dispose(); + } + } + } } catch (Exception ex) { - Console.WriteLine($"更新图像失败: {ex.Message}"); + Console.WriteLine($"更新图像UI失败: {ex.Message}"); + // 确保在任何异常情况下都释放传入的图像资源 + try + { + image.Dispose(); + } + catch {} } } @@ -193,13 +405,26 @@ namespace JoyD.Windows.CS.Toprie /// private void DeviceManager_ConnectionStatusChanged(object sender, ConnectionStatusChangedEventArgs e) { - if (this.InvokeRequired) + // 确保在UI线程上更新,并且检查控件是否已被释放 + if (!this.IsDisposed && !this.Disposing) { - this.Invoke(new Action(() => HandleConnectionStatusChanged(e))); - } - else - { - HandleConnectionStatusChanged(e); + if (this.InvokeRequired) + { + try + { + // 使用BeginInvoke代替Invoke,避免可能的死锁问题 + this.BeginInvoke(new Action(() => HandleConnectionStatusChanged(e))); + } + catch (ObjectDisposedException) + { + // 捕获控件已释放异常,避免程序崩溃 + Console.WriteLine("控件已释放,跳过UI更新"); + } + } + else + { + HandleConnectionStatusChanged(e); + } } } @@ -208,10 +433,28 @@ namespace JoyD.Windows.CS.Toprie /// private void HandleConnectionStatusChanged(ConnectionStatusChangedEventArgs e) { + // 更新UI状态 + UpdateUIState(e.NewStatus == ConnectionStatus.Connected); + switch (e.NewStatus) { case ConnectionStatus.Connected: Console.WriteLine("设备已连接"); + + // 清除错误信息 + ShowError(string.Empty); + + // 确保设置为热图模式 + try + { + _deviceManager.SetImageMode(ImageMode.Thermal); + Console.WriteLine("连接成功后确认热图模式"); + } + catch (Exception ex) + { + Console.WriteLine($"连接成功后设置热图模式失败: {ex.Message}"); + } + if (!_isReceivingImage) { StartReceiveImage(); @@ -219,31 +462,109 @@ namespace JoyD.Windows.CS.Toprie break; case ConnectionStatus.Disconnected: Console.WriteLine("设备已断开连接"); - _isReceivingImage = false; + + // 停止接收图像 + if (_isReceivingImage) + { + _deviceManager.StopReceiveImage(); + _isReceivingImage = false; + } + if (!string.IsNullOrEmpty(e.Message)) { ShowError(e.Message); + } + else + { + ShowError("设备连接已断开"); } break; case ConnectionStatus.Connecting: case ConnectionStatus.Reconnecting: - Console.WriteLine("正在连接设备..."); + Console.WriteLine($"正在连接设备...{(!string.IsNullOrEmpty(e.Message) ? " " + e.Message : "")}"); + ShowError(string.Empty); // 清除之前的错误信息 break; } } + + /// + /// 更新UI状态 + /// + /// 是否已连接 + private void UpdateUIState(bool isConnected) + { + try + { + // 根据连接状态更新图像框状态 + if (imageBox != null && !imageBox.IsDisposed) + { + if (isConnected) + { + imageBox.BorderStyle = BorderStyle.FixedSingle; + } + else + { + imageBox.BorderStyle = BorderStyle.Fixed3D; + } + } + + // 更新图像框的边框颜色,提供视觉反馈 + if (imageBox != null) + { + imageBox.BorderStyle = isConnected ? BorderStyle.FixedSingle : BorderStyle.Fixed3D; + // 可选:设置不同的边框颜色以提供更好的视觉反馈 + if (isConnected) + { + imageBox.BackColor = Color.LightGreen; + } + else + { + imageBox.BackColor = Color.LightGray; + } + } + + Console.WriteLine($"UI状态已更新为: {(isConnected ? "已连接" : "未连接")}"); + } + catch (Exception ex) + { + Console.WriteLine($"更新UI状态时出错: {ex.Message}"); + } + } /// /// 设备管理器连接异常事件处理 /// private void DeviceManager_ConnectionException(object sender, ConnectionExceptionEventArgs e) { - if (this.InvokeRequired) + // 确保在UI线程上更新,并且检查控件是否已被释放 + if (!this.IsDisposed && !this.Disposing) { - this.Invoke(new Action(() => ShowError($"连接异常: {e.ContextMessage}"))); - } - else - { - ShowError($"连接异常: {e.ContextMessage}"); + if (this.InvokeRequired) + { + try + { + // 使用BeginInvoke在UI线程上显示错误 + this.BeginInvoke(new Action(() => + { + // 记录异常日志 + Console.WriteLine($"连接异常发生于 {e.Timestamp}: {e.ContextMessage}\n{e.Exception.Message}\n{e.Exception.StackTrace}"); + + ShowError($"连接异常: {e.ContextMessage}"); + })); + } + catch (ObjectDisposedException) + { + // 捕获控件已释放异常,避免程序崩溃 + Console.WriteLine("控件已释放,跳过异常处理"); + } + } + else + { + // 记录异常日志 + Console.WriteLine($"连接异常发生于 {e.Timestamp}: {e.ContextMessage}\n{e.Exception.Message}\n{e.Exception.StackTrace}"); + + ShowError($"连接异常: {e.ContextMessage}"); + } } } @@ -257,8 +578,19 @@ namespace JoyD.Windows.CS.Toprie // 可以根据需要添加UI上的错误显示 // 这里简化处理,只在控制台输出 + // 检查imageBox是否存在且尺寸有效 + if (imageBox == null || imageBox.Width <= 0 || imageBox.Height <= 0) + { + Console.WriteLine("imageBox尺寸无效,跳过错误显示图像创建"); + return; + } + + // 确保使用有效的尺寸创建Bitmap + int width = Math.Max(100, imageBox.Width); + int height = Math.Max(100, imageBox.Height); + // 如果需要在图像区域显示错误文字,可以使用以下代码 - using (Bitmap errorBitmap = new Bitmap(imageBox.Width, imageBox.Height)) + using (Bitmap errorBitmap = new Bitmap(width, height)) using (Graphics g = Graphics.FromImage(errorBitmap)) { g.Clear(Color.Black); @@ -272,7 +604,7 @@ namespace JoyD.Windows.CS.Toprie g.DrawString(message, font, brush, textLocation); } - UpdateImage(errorBitmap); + UpdateImageOnUI(errorBitmap); } // 启动定时器,3秒后清除错误显示 @@ -287,7 +619,19 @@ namespace JoyD.Windows.CS.Toprie { _errorDisplayTimer.Stop(); // 清除错误显示,恢复到等待图像状态 - using (Bitmap waitingBitmap = new Bitmap(imageBox.Width, imageBox.Height)) + + // 检查imageBox是否存在且尺寸有效 + if (imageBox == null || imageBox.Width <= 0 || imageBox.Height <= 0) + { + Console.WriteLine("imageBox尺寸无效,跳过等待图像创建"); + return; + } + + // 确保使用有效的尺寸创建Bitmap + int width = Math.Max(100, imageBox.Width); + int height = Math.Max(100, imageBox.Height); + + using (Bitmap waitingBitmap = new Bitmap(width, height)) using (Graphics g = Graphics.FromImage(waitingBitmap)) { g.Clear(Color.Black); @@ -302,7 +646,7 @@ namespace JoyD.Windows.CS.Toprie g.DrawString(waitingText, font, brush, textLocation); } - UpdateImage(waitingBitmap); + UpdateImageOnUI(waitingBitmap); } } @@ -330,9 +674,12 @@ namespace JoyD.Windows.CS.Toprie // 取消注册事件并释放设备管理器 if (_deviceManager != null) { + + // 移除所有事件监听 _deviceManager.ImageReceived -= DeviceManager_ImageReceived; _deviceManager.ConnectionStatusChanged -= DeviceManager_ConnectionStatusChanged; _deviceManager.ConnectionException -= DeviceManager_ConnectionException; + // 释放设备管理器资源 _deviceManager.Dispose(); _deviceManager = null; } @@ -346,7 +693,7 @@ namespace JoyD.Windows.CS.Toprie } // 无论是否在设计模式下,都需要释放图像资源 - if (imageBox.Image != null) + if (imageBox != null && !imageBox.IsDisposed && imageBox.Image != null) { imageBox.Image.Dispose(); imageBox.Image = null; diff --git a/Windows/CS/Framework4.0/Toprie/Toprie/DeviceManager.cs b/Windows/CS/Framework4.0/Toprie/Toprie/DeviceManager.cs index 6e56918..5858900 100644 --- a/Windows/CS/Framework4.0/Toprie/Toprie/DeviceManager.cs +++ b/Windows/CS/Framework4.0/Toprie/Toprie/DeviceManager.cs @@ -1,20 +1,24 @@ using System; -using System.Linq; using System.Collections.Generic; -using System.Threading; +using System.Linq; +using System.Text; using System.Threading.Tasks; -using System.Net.NetworkInformation; +using System.Net; using System.Net.Sockets; using System.IO; +using System.Threading; using System.Drawing; +using System.Drawing.Imaging; +using System.Windows.Forms; +using System.Text.RegularExpressions; using System.Diagnostics; -using System.Net; -using System.Text; +using Toprie; // 修改引用路径,使用本地的A8SDK.cs +using System.Net.NetworkInformation; -namespace JoyD.Windows.CS.Toprie +namespace Toprie { /// - /// 连接状态枚举 + /// 设备连接状态枚举 /// public enum ConnectionStatus { @@ -23,40 +27,44 @@ namespace JoyD.Windows.CS.Toprie Connected, Reconnecting } - + /// /// 图像模式枚举 /// public enum ImageMode { - /// - /// 热图模式(红外热成像) - /// Thermal, - /// - /// 全彩模式(可见光图像) - /// - FullColor + Visible, + Fusion } /// - /// 连接状态变更事件参数 + /// 连接状态改变事件参数 /// public class ConnectionStatusChangedEventArgs : EventArgs { - public ConnectionStatus NewStatus { get; set; } - public ConnectionStatus OldStatus { get; set; } - public string Message { get; set; } - public Exception Exception { get; set; } - public DateTime Timestamp { get; private set; } + public ConnectionStatus Status { get; set; } + public string DeviceInfo { get; set; } - public ConnectionStatusChangedEventArgs(ConnectionStatus oldStatus, ConnectionStatus newStatus, string message = null, Exception exception = null) + public ConnectionStatusChangedEventArgs(ConnectionStatus status, string deviceInfo = null) { - OldStatus = oldStatus; - NewStatus = newStatus; - Message = message; - Exception = exception; - Timestamp = DateTime.Now; + Status = status; + DeviceInfo = deviceInfo; + } + } + + /// + /// 图像接收事件参数 + /// + public class ImageReceivedEventArgs : EventArgs + { + public byte[] ImageData { get; set; } + public ImageMode Mode { get; set; } + + public ImageReceivedEventArgs(byte[] imageData, ImageMode mode) + { + ImageData = imageData; + Mode = mode; } } @@ -65,69 +73,134 @@ namespace JoyD.Windows.CS.Toprie /// public class ConnectionExceptionEventArgs : EventArgs { - public Exception Exception { get; private set; } - public string ContextMessage { get; private set; } - public DateTime Timestamp { get; private set; } + public Exception Exception { get; set; } + public string Message { get; set; } - public ConnectionExceptionEventArgs(Exception exception, string contextMessage) + public ConnectionExceptionEventArgs(Exception ex, string message = null) { - Exception = exception; - ContextMessage = contextMessage; - Timestamp = DateTime.Now; + Exception = ex; + Message = message ?? ex?.Message; } } - + /// - /// 图像数据接收事件参数 + /// 设备管理器类 /// - public class ImageReceivedEventArgs : EventArgs + /// + /// 设备信息类,用于存储设备的ID、IP等信息 + /// + public class DeviceInfo { - public Image ImageData { get; private set; } - public DateTime Timestamp { get; private set; } - - public ImageReceivedEventArgs(Image imageData) + /// + /// 设备ID + /// + public int DeviceID { get; set; } + + /// + /// 设备IP地址 + /// + public string IPAddress { get; set; } + + /// + /// 设备型号 + /// + public string Model { get; set; } + + /// + /// 设备序列号 + /// + public string SerialNumber { get; set; } + + public override string ToString() { - ImageData = imageData; - Timestamp = DateTime.Now; + return $"设备 {DeviceID} ({IPAddress})"; } } - /// - /// 设备管理类,负责设备搜索、连接和断开(纯C#实现) - /// public class DeviceManager : IDisposable { - #region 私有成员 - - // 设备IP地址 - private string _deviceIp = "192.168.100.2"; // 默认IP - private int _devicePort = 8080; // 默认端口 - + // A8SDK实例 + private A8SDK _a8Sdk; // 设备ID列表 - private List _deviceIds; - - // 是否已初始化 - private bool _isInitialized; - - // 连接状态 - private ConnectionStatus _connectionStatus = ConnectionStatus.Disconnected; + private List _deviceIds = new List(); + // 设备信息列表 + private List _deviceList = new List(); + + // 目标设备ID,用于指定ID连接 + private int _targetDeviceId = -1; + // 当前设备ID + private int _currentDeviceId = -1; + // 默认设备IP + private string _deviceIp = "192.168.100.2"; + // 设备端口 + private int _devicePort = 8080; + // 设备连接状态 + private ConnectionStatus _connectionStatus = ConnectionStatus.Disconnected; + // 是否已初始化 + private bool _isInitialized = false; + // 是否已释放 + private bool _isDisposed = false; // 图像模式 private ImageMode _currentImageMode = ImageMode.Thermal; - - // 自动重连相关 - private bool _isAutoReconnectEnabled = false; - private int _reconnectInterval = 5000; // 默认5秒 - private int _maxReconnectAttempts = 5; - private int _reconnectAttempts = 0; + // 自动重连是否启用 + private bool _autoReconnectEnabled = true; + // 自动重连定时器 private System.Threading.Timer _reconnectTimer; - private ManualResetEventSlim _stopRequested = new ManualResetEventSlim(false); - - // 心跳检测相关 + // 重连间隔(毫秒) + private int _reconnectInterval = 2000; + // 重连尝试次数 + private int _reconnectAttempts = 0; + // 最大重连尝试次数 + private const int MaxReconnectAttempts = 5; + // 连接检查定时器 + private System.Threading.Timer _connectionCheckTimer; + // 心跳定时器 private System.Threading.Timer _heartbeatTimer; - private int _heartbeatInterval = 30000; // 30秒心跳 - - #endregion 私有成员 + // 连接超时时间(毫秒) + private const int ConnectionTimeout = 5000; + // 连接检查间隔(毫秒) + private const int ConnectionCheckInterval = 5000; + // 心跳间隔(毫秒) + private int _heartbeatInterval = 5000; + // TCP客户端 + private TcpClient _imageTcpClient; + // 网络流 + private NetworkStream _imageNetworkStream; + // 图像接收任务 + private Task _imageReceivingTask; + // 取消令牌源 + private CancellationTokenSource _imageReceivingCts; + // 停止请求事件 + private ManualResetEvent _stopRequested = new ManualResetEvent(false); + // 缓冲区 + private byte[] _imageBuffer = new byte[4096]; + // 图像数据累积缓冲区 + private byte[] _imageDataAccumulator = new byte[0]; + // 多部分请求边界 + private string _multipartBoundary = string.Empty; + // 锁对象 + private object _lockObject = new object(); + // 是否启用自动重连 + private bool _isAutoReconnectEnabled = true; + // 最大重连次数 + private int _maxReconnectAttempts = 5; + // 是否已连接 + private bool _isConnected = false; + // 连接超时设置 + private int _connectTimeout = 5000; + // 当前重连尝试次数 + private int _currentReconnectAttempt = 0; + // 连接取消令牌源 + private CancellationTokenSource _connectCancellationTokenSource; + // 图像接收相关变量 + private ManualResetEvent _stopImageEvent; + private bool _isReceivingImages = false; + private Thread _imageReceiveThread; + private Thread _imageReconnectThread; + private Stream _imageStream; + private TcpClient _imageTcpClient; + private bool _isInfraredMode = true; #region 私有方法 @@ -161,11 +234,59 @@ namespace JoyD.Windows.CS.Toprie /// 相关异常(如果有) private void UpdateConnectionStatus(ConnectionStatus newStatus, string message = null, Exception exception = null) { + // 检查对象是否已被释放 + if (_isDisposed) + { + Console.WriteLine("警告: 尝试在已释放对象上更新连接状态"); + return; + } + if (_connectionStatus != newStatus) { ConnectionStatus oldStatus = _connectionStatus; _connectionStatus = newStatus; - OnConnectionStatusChanged(new ConnectionStatusChangedEventArgs(oldStatus, newStatus, message, exception)); + + // 触发连接状态变更事件前再次检查对象是否已被释放 + if (!_isDisposed) + { + try + { + OnConnectionStatusChanged(new ConnectionStatusChangedEventArgs(newStatus, message)); + } + catch (Exception ex) + { + Console.WriteLine($"触发连接状态变更事件异常: {ex.Message}"); + } + } + + Console.WriteLine($"连接状态变更: {oldStatus} -> {newStatus}"); + if (!string.IsNullOrEmpty(message)) + { + Console.WriteLine($"状态消息: {message}"); + } + if (exception != null) + { + Console.WriteLine($"异常信息: {exception.Message}"); + } + + // 如果断开连接且启用了自动重连,启动重连机制 + if (newStatus == ConnectionStatus.Disconnected && _autoReconnectEnabled && oldStatus != ConnectionStatus.Connecting) + { + StartAutoReconnect(); + } + // 如果连接成功,重置重连计数并启动图像接收 + else if (newStatus == ConnectionStatus.Connected) + { + _currentReconnectAttempt = 0; + StopAutoReconnect(); + // 启动图像接收 + StartImageReceiving(); + } + // 如果断开连接,停止图像接收 + else if (newStatus == ConnectionStatus.Disconnected) + { + StopImageReceiving(); + } } } @@ -175,16 +296,33 @@ namespace JoyD.Windows.CS.Toprie /// 是否初始化成功 private bool Initialize() { + if (_isInitialized) + return true; + try { - _deviceIds = new List(); + // 初始化设备ID列表 + _deviceIds = new List(); // 保持与类中定义一致 _reconnectTimer = null; _heartbeatTimer = null; + _connectionCheckTimer = null; + + // 首先调用SDK静态初始化方法 - 这是连接成功的关键步骤! + int initResult = A8SDK.SDK_initialize(); + if (initResult != 0) + { + Console.WriteLine($"SDK静态初始化失败,返回值: {initResult}"); + _isInitialized = false; + return false; + } + + Console.WriteLine("SDK静态初始化成功"); _isInitialized = true; return true; } catch (Exception ex) { + Console.WriteLine($"SDK初始化失败: {ex.Message}"); _isInitialized = false; OnConnectionException(new ConnectionExceptionEventArgs(ex, "初始化设备管理器失败")); return false; @@ -192,22 +330,65 @@ namespace JoyD.Windows.CS.Toprie } /// - /// 检查IP是否可达 + /// 尝试ping设备IP地址 /// - /// IP地址 - /// 是否可达 - private bool IsIpReachable(string ipAddress) + /// 设备IP地址 + /// 是否ping通 + // PingDevice方法已在下方定义,删除重复实现 + + /// + /// 检查系统网络连接是否可用 + /// + /// 网络是否可用 + private bool IsNetworkAvailable() { try { - using (Ping ping = new Ping()) + // 检查系统网络连接状态 + System.Net.NetworkInformation.NetworkInterface[] interfaces = + System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces(); + + // 检查是否有活动的以太网或WiFi连接 + foreach (var ni in interfaces) { - PingReply reply = ping.Send(ipAddress, 1000); // 1秒超时 - return reply.Status == IPStatus.Success; + // 仅检查物理网络接口(以太网、WiFi等) + if (ni.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up && + (ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Ethernet || + ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Wireless80211) && + ni.Name != "本地连接*" && !ni.Description.Contains("虚拟") && !ni.Description.Contains("Virtual")) + { + // 获取该网络接口的IP信息,检查是否有有效的IP地址 + var properties = ni.GetIPProperties(); + foreach (var addr in properties.UnicastAddresses) + { + // 检查是否有IPv4地址 + if (addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + // 如果设备IP不为空,可以尝试检查设备IP是否在该接口的子网内 + if (!string.IsNullOrEmpty(_deviceIp)) + { + try + { + // 检查设备IP是否与当前网络接口在同一子网(粗略判断) + var deviceAddress = System.Net.IPAddress.Parse(_deviceIp); + if (deviceAddress.GetAddressBytes()[0] == addr.Address.GetAddressBytes()[0]) + { + return true; + } + } + catch { } + } + return true; + } + } + } } + return false; } - catch (Exception) + catch (Exception ex) { + Console.WriteLine($"检查网络可用性异常: {ex.Message}"); + // 发生异常时默认为网络不可用,避免误判 return false; } } @@ -259,6 +440,246 @@ namespace JoyD.Windows.CS.Toprie { ConnectionStatusChanged?.Invoke(this, e); } + + /// + /// 启动连接状态检查 + /// + private void StartConnectionCheck() + { + try + { + // 首先停止现有的连接检查,确保资源释放 + StopConnectionCheck(); + + Console.WriteLine("启动连接状态检查,每5秒检查一次"); + + // 使用try-catch包装定时器创建,避免异常 + try + { + // 每5秒检查一次连接状态 + _connectionCheckTimer = new System.Threading.Timer(state => + { + // 确保定时器状态检查在工作线程中安全执行 + try + { + // 避免在定时器回调中可能的资源访问冲突 + if (_connectionCheckTimer == null) + return; + + // 关键修改:即使_isInitialized为false,也需要检查连接状态 + // 当显示为已连接但初始化状态为false时,必须检查连接 + if (_connectionStatus == ConnectionStatus.Connected) + { + // 如果初始化失败但显示已连接,尝试重新初始化 + if (!_isInitialized) + { + Console.WriteLine("警告: 显示已连接但初始化状态为false,尝试重新初始化..."); + if (!Initialize()) + { + Console.WriteLine("重新初始化失败,确认连接已断开"); + UpdateConnectionStatus(ConnectionStatus.Disconnected, "设备未初始化,连接已断开"); + // 启动自动重连 + if (_autoReconnectEnabled) + { + StartAutoReconnect(); + } + } + } + + // 无论初始化状态如何,只要显示为已连接就进行检查 + CheckConnectionWrapper(); + } + // 正常情况下的连接检查 + else if (_isInitialized && _a8Sdk != null && _currentDeviceId != -1) + { + CheckConnectionWrapper(); + } + } + catch (Exception ex) + { + Console.WriteLine($"定时器回调异常: {ex.Message}"); + // 异常时如果是已连接状态,将其设为断开 + if (_connectionStatus == ConnectionStatus.Connected) + { + UpdateConnectionStatus(ConnectionStatus.Disconnected, "连接检查异常", ex); + _isInitialized = false; + // 启动自动重连 + if (_autoReconnectEnabled) + { + StartAutoReconnect(); + } + } + // 发生异常时停止定时器,避免持续报错 + StopConnectionCheck(); + } + }, null, 5000, 5000); + } + catch (Exception ex) + { + Console.WriteLine($"创建连接检查定时器失败: {ex.Message}"); + _connectionCheckTimer = null; + } + } + catch (Exception ex) + { + Console.WriteLine($"启动连接检查异常: {ex.Message}"); + StopConnectionCheck(); + } + } + + // 连接检查的安全包装方法 + private void CheckConnectionWrapper() + { + try + { + // 检查连接有效性 + bool isStillConnected = CheckConnectionValidity(); + + // 连接状态变化时更新状态 + if (!isStillConnected && _connectionStatus == ConnectionStatus.Connected) + { + Console.WriteLine("检测到连接已断开"); + // 断开连接时重置初始化状态和SDK实例 + _isInitialized = false; + _a8Sdk = null; + UpdateConnectionStatus(ConnectionStatus.Disconnected, "连接已断开:设备离线"); + // 断开连接后自动启动重连 + if (_autoReconnectEnabled) + { + Console.WriteLine("连接断开,启动自动重连"); + StartAutoReconnect(); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"检查连接状态异常: {ex.Message}"); + // 检查异常时,将状态设为断开 + if (_connectionStatus == ConnectionStatus.Connected) + { + // 异常时重置初始化状态和SDK实例 + _isInitialized = false; + _a8Sdk = null; + UpdateConnectionStatus(ConnectionStatus.Disconnected, "连接状态检查异常", ex); + // 异常时启动自动重连 + if (_autoReconnectEnabled) + { + Console.WriteLine("检查异常,启动自动重连"); + StartAutoReconnect(); + } + } + } + } + + /// + /// 检查连接有效性的内部方法 + /// + /// 连接是否有效 + private bool CheckConnectionValidity() + { + try + { + // 重要修改:先检查设备ID有效性,避免后续无效检查 + if (_currentDeviceId == -1) + { + Console.WriteLine("当前设备ID无效"); + return false; + } + + // 优化1: 优先尝试ping设备IP地址,这是物理连接断开的最直接检测 + if (!PingDevice(_deviceIp)) + { + Console.WriteLine($"设备IP {_deviceIp} 不可达,物理连接可能已断开"); + return false; + } + + // 优化2: 系统网络接口检测作为辅助检查,不再作为首要条件 + if (!IsNetworkAvailable()) + { + Console.WriteLine("警告: 系统网络连接不可用,但ping检测通过"); + } + + // 优化3: 加强SDK连接状态验证,参考热像仪示例的实现方式 + try + { + // 确保SDK实例存在 + if (_a8Sdk == null) + { + Console.WriteLine("SDK实例不存在,重新初始化..."); + _a8Sdk = new A8SDK(_deviceIp); + } + + // 简化心跳检测逻辑,避免在心跳失败时过度访问可能无效的资源 + bool heartbeatResult = _a8Sdk.Heartbeat(); + if (!heartbeatResult) + { + Console.WriteLine("SDK心跳检测失败"); + // 心跳失败时,安全释放SDK实例 + if (_a8Sdk != null) + { + _a8Sdk = null; + } + return false; + } + Console.WriteLine("SDK心跳检测成功"); + + // 设备名称获取使用try-catch,避免在心跳成功但设备响应异常时崩溃 + try + { + // 如果SDK支持获取设备名称,可以尝试获取 + // string deviceName = _a8Sdk.Get_device_name(); + // if (string.IsNullOrEmpty(deviceName)) + // { + // Console.WriteLine("设备名称获取失败,可能连接不稳定"); + // } + } + catch (Exception ex) + { + Console.WriteLine($"获取设备名称异常,但心跳检测成功: {ex.Message}"); + // 设备名称获取失败不应导致整体连接失效 + } + } + catch (Exception ex) + { + Console.WriteLine($"SDK心跳检测失败: {ex.Message}"); + return false; + } + + // 所有检查通过,连接有效 + return true; + } + catch (Exception ex) + { + Console.WriteLine($"连接有效性检查异常: {ex.Message}"); + return false; + } + } + + /// + /// 停止连接状态检查 + /// + private void StopConnectionCheck() + { + try + { + // 使用局部变量暂存,避免多线程访问时的空引用问题 + System.Threading.Timer timerToDispose = Interlocked.Exchange(ref _connectionCheckTimer, null); + if (timerToDispose != null) + { + // 立即停止定时器,避免回调执行 + timerToDispose.Change(Timeout.Infinite, Timeout.Infinite); + // 安全释放定时器资源 + timerToDispose.Dispose(); + Console.WriteLine("连接检查已停止"); + } + } + catch (Exception ex) + { + Console.WriteLine($"停止连接检查异常: {ex.Message}"); + // 确保即使异常,引用也被清空 + _connectionCheckTimer = null; + } + } /// /// 触发连接异常事件 @@ -427,6 +848,520 @@ namespace JoyD.Windows.CS.Toprie #endregion 公共属性 + #region 图像接收方法 + + /// + /// 开始接收图像数据 + /// + public void StartImageReceiving() + { + try + { + // 确保之前的连接已关闭 + StopImageReceiving(); + + // 重置停止事件 + _stopImageEvent = new ManualResetEvent(false); + _isReceivingImages = true; + + // 创建并启动图像接收线程 + _imageReceiveThread = new Thread(ReceiveImageDataWithHttpWebRequest) + { + IsBackground = true, + Name = "ImageReceiveThread" + }; + _imageReceiveThread.Start(); + } + catch (Exception ex) + { + // 记录异常 + Console.WriteLine("StartImageReceiving error: " + ex.Message); + _isReceivingImages = false; + } + } + + /// + /// 停止接收图像数据 + /// + public void StopImageReceiving() + { + try + { + _isReceivingImages = false; + + // 通知线程停止 + if (_stopImageEvent != null) + { + _stopImageEvent.Set(); + } + + // 等待线程结束 + if (_imageReceiveThread != null && _imageReceiveThread.IsAlive) + { + _imageReceiveThread.Join(3000); // 最多等待3秒 + _imageReceiveThread = null; + } + + // 停止重连线程 + if (_imageReconnectThread != null && _imageReconnectThread.IsAlive) + { + _imageReconnectThread.Join(1000); + _imageReconnectThread = null; + } + + // 释放资源 + if (_imageStream != null) + { + _imageStream.Close(); + _imageStream.Dispose(); + _imageStream = null; + } + + if (_imageTcpClient != null) + { + _imageTcpClient.Close(); + _imageTcpClient = null; + } + } + catch (Exception ex) + { + Console.WriteLine("StopImageReceiving error: " + ex.Message); + } + } + + /// + /// 使用HttpWebRequest接收图像数据 + /// + private void ReceiveImageDataWithHttpWebRequest() + { + try + { + string url = string.Format("http://{0}:8080{1}", DeviceIp, _isInfraredMode ? "/img.jpg" : "/visible.jpg"); + while (_isReceivingImages) + { + // 添加时间戳避免缓存 + string timestampUrl = url + "?t=" + DateTime.Now.Ticks; + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(timestampUrl); + request.Method = "GET"; + request.Timeout = 3000; // 3秒超时 + request.KeepAlive = true; + request.UserAgent = "Mozilla/5.0"; // 模拟浏览器 + + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + if (response.StatusCode == HttpStatusCode.OK) + { + using (Stream stream = response.GetResponseStream()) + { + // 读取图像数据 + byte[] buffer = new byte[8192]; // 增加缓冲区大小到8KB + using (MemoryStream ms = new MemoryStream()) + { + int bytesRead; + while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, bytesRead); + } + byte[] imageData = ms.ToArray(); + + // 处理接收到的数据 + ProcessReceivedImageData(imageData); + } + } + } + } + + // 检查是否需要停止 + if (_stopImageEvent.WaitOne(100)) // 100ms检查一次 + { + break; + } + } + } + catch (Exception ex) + { + // 记录异常并尝试重连 + if (_isReceivingImages) + { + Console.WriteLine("ReceiveImageDataWithHttpWebRequest error: " + ex.Message); + StartImageReconnect(); + } + } + } + + /// + /// 处理接收到的图像数据 + /// + /// 原始数据 + private void ProcessReceivedImageData(byte[] data) + { + if (data == null || data.Length == 0) + return; + + // 检查是否为HTTP响应 + if (IsHttpResponse(data)) + { + // 查找HTTP响应中的图像数据 + int imageStartPos = FindImageStartPosition(data); + if (imageStartPos > 0) + { + // 提取图像数据部分 + int imageLength = data.Length - imageStartPos; + byte[] imageData = new byte[imageLength]; + Array.Copy(data, imageStartPos, imageData, 0, imageLength); + + // 验证并处理提取的图像数据 + ValidateAndProcessImage(imageData); + } + else + { + // 尝试直接验证整个HTTP响应 + ValidateAndProcessImage(data); + } + } + else + { + // 直接验证和处理普通图像数据 + ValidateAndProcessImage(data); + } + } + + /// + /// 验证并处理图像数据 + /// + /// 图像数据 + private void ValidateAndProcessImage(byte[] imageData) + { + // 首先检查文件头 + if (IsValidImageData(imageData)) + { + try + { + // 尝试从流中创建图像对象进行验证 + using (MemoryStream ms = new MemoryStream(imageData)) + { + // 检查是否包含完整的JPEG文件 + if (imageData.Length > 4 && imageData[0] == 0xFF && imageData[1] == 0xD8) + { + int endPos = FindImageEndPosition(imageData, 0); + if (endPos > 0 && endPos + 1 < imageData.Length) + { + // 确保图像有完整的EOF标记 + if (imageData[endPos + 1] == 0xD9) + { + // 创建并触发事件 + Image receivedImage = Image.FromStream(ms); + OnImageReceived(new ImageReceivedEventArgs(imageData, _currentImageMode)); + return; + } + } + } + + // 对于其他格式或无法确认完整度的情况,直接尝试创建 + Image receivedImage = Image.FromStream(ms); + OnImageReceived(new ImageReceivedEventArgs(imageData, _currentImageMode)); + } + } + catch (Exception ex) + { + Console.WriteLine("处理图像数据异常: " + ex.Message); + // 尝试查找有效的图像起始位置 + int validStartPos = FindImageStartPosition(imageData); + if (validStartPos > 0) + { + try + { + // 尝试从有效位置提取图像 + int newLength = imageData.Length - validStartPos; + byte[] validImageData = new byte[newLength]; + Array.Copy(imageData, validStartPos, validImageData, 0, newLength); + + using (MemoryStream ms = new MemoryStream(validImageData)) + { + Image receivedImage = Image.FromStream(ms); + OnImageReceived(new ImageReceivedEventArgs(validImageData, _currentImageMode)); + } + } + catch + { + // 二次尝试也失败,放弃处理 + } + } + } + } + } + + /// + /// 启动图像重连 + /// + private void StartImageReconnect() + { + // 避免重复创建重连线程 + if (_imageReconnectThread != null && _imageReconnectThread.IsAlive) + { + return; + } + + _imageReconnectThread = new Thread(() => + { + try + { + int reconnectCount = 0; + while (_isReceivingImages && reconnectCount < 3) // 最多重连3次 + { + reconnectCount++; + Thread.Sleep(2000); // 2秒后重连 + if (_isReceivingImages) + { + ReceiveImageDataWithHttpWebRequest(); + } + } + } + catch (Exception ex) + { + Console.WriteLine("Image reconnect error: " + ex.Message); + } + }) + { + IsBackground = true, + Name = "ImageReconnectThread" + }; + _imageReconnectThread.Start(); + } + + #endregion 图像接收方法 + + #region 图像处理辅助方法 + + /// + /// 查找图像数据的结束位置(特别是JPEG的EOF标记) + /// + /// 数据缓冲区 + /// 开始查找的位置 + /// 图像结束位置索引,-1表示未找到 + private int FindImageEndPosition(byte[] data, int startIndex) + { + // 对于JPEG,查找EOF标记 (FF D9) + if (data.Length >= startIndex + 2 && data[startIndex] == 0xFF && data[startIndex + 1] == 0xD8) + { + for (int i = startIndex + 2; i < data.Length - 1; i++) + { + if (data[i] == 0xFF && data[i + 1] == 0xD9) + { + return i; + } + } + } + return -1; + } + + /// + /// 检查数据是否为HTTP响应 + /// + /// 待检查的数据 + /// 是否为HTTP响应 + private bool IsHttpResponse(byte[] data) + { + if (data == null || data.Length < 4) + return false; + + // 检查HTTP响应状态行的特征 + // HTTP/1.x 状态码 描述 + string responseStart = System.Text.Encoding.ASCII.GetString(data, 0, Math.Min(10, data.Length)); + return responseStart.StartsWith("HTTP/"); + } + + /// + /// 查找图像数据的开始位置,跳过HTTP头部 + /// + /// 数据缓冲区 + /// 图像数据开始位置索引,-1表示未找到 + private int FindImageStartPosition(byte[] data) + { + if (data == null || data.Length < 8) + return -1; + + // 首先检查是否以图像头开始 + if (IsValidImageData(data)) + return 0; + + // 否则尝试查找HTTP响应中的图像数据 + // 查找空行后的位置(HTTP头部结束) + for (int i = 0; i < data.Length - 3; i++) + { + if (data[i] == 0x0D && data[i + 1] == 0x0A && data[i + 2] == 0x0D && data[i + 3] == 0x0A) + { + // 找到HTTP头部结束位置,检查后续数据是否为图像 + int startPos = i + 4; + if (startPos < data.Length - 7) + { + // 检查是否为JPEG + if (data[startPos] == 0xFF && data[startPos + 1] == 0xD8) + return startPos; + + // 检查是否为PNG + if (data[startPos] == 0x89 && data[startPos + 1] == 0x50 && data[startPos + 2] == 0x4E && data[startPos + 3] == 0x47) + return startPos; + } + } + } + + return -1; + } + + /// + /// 从HTTP响应中提取Content-Type + /// + /// HTTP响应数据 + /// Content-Type字符串 + private string ExtractContentType(byte[] data) + { + string headerText = System.Text.Encoding.ASCII.GetString(data, 0, Math.Min(1024, data.Length)); + int contentTypeIndex = headerText.IndexOf("Content-Type:", StringComparison.OrdinalIgnoreCase); + if (contentTypeIndex >= 0) + { + contentTypeIndex += "Content-Type:".Length; + int endIndex = headerText.IndexOf("\r\n", contentTypeIndex); + if (endIndex >= 0) + { + return headerText.Substring(contentTypeIndex, endIndex - contentTypeIndex).Trim(); + } + } + return string.Empty; + } + + /// + /// 从Content-Type中提取multipart boundary + /// + /// Content-Type字符串 + /// boundary字符串 + private string ExtractBoundary(string contentType) + { + int boundaryIndex = contentType.IndexOf("boundary=", StringComparison.OrdinalIgnoreCase); + if (boundaryIndex >= 0) + { + boundaryIndex += "boundary=".Length; + // 处理可能包含引号的情况 + char quoteChar = contentType[boundaryIndex]; + int startIndex = (quoteChar == '"' || quoteChar == '\'') ? boundaryIndex + 1 : boundaryIndex; + int endIndex = -1; + + if (startIndex > boundaryIndex) + { + // 寻找匹配的引号 + endIndex = contentType.IndexOf(quoteChar, startIndex); + } + else + { + // 寻找分号或行尾 + endIndex = contentType.IndexOf(';', startIndex); + if (endIndex < 0) + { + endIndex = contentType.Length; + } + } + + if (endIndex >= 0) + { + return contentType.Substring(startIndex, endIndex - startIndex).Trim(); + } + } + return string.Empty; + } + + /// + /// 处理multipart格式的图像数据 + /// + /// 数据缓冲区 + /// multipart boundary + /// 原始图像数据缓冲区 + /// 处理到的位置索引,-1表示未找到完整的图像块 + private int ProcessMultipartImageData(byte[] buffer, string boundary, MemoryStream imageDataBuffer) + { + byte[] boundaryBytes = System.Text.Encoding.ASCII.GetBytes(boundary); + int startPos = 0; + + // 查找第一个boundary + while (startPos < buffer.Length - boundaryBytes.Length) + { + bool found = true; + for (int i = 0; i < boundaryBytes.Length; i++) + { + if (buffer[startPos + i] != boundaryBytes[i]) + { + found = false; + break; + } + } + + if (found) + { + // 找到第一个boundary,继续查找下一个 + int nextBoundaryPos = startPos + boundaryBytes.Length; + while (nextBoundaryPos < buffer.Length - boundaryBytes.Length) + { + bool nextFound = true; + for (int i = 0; i < boundaryBytes.Length; i++) + { + if (buffer[nextBoundaryPos + i] != boundaryBytes[i]) + { + nextFound = false; + break; + } + } + + if (nextFound) + { + // 找到两个boundary之间的数据,提取HTTP头和图像数据 + int headerEndPos = -1; + for (int i = startPos + boundaryBytes.Length; i < nextBoundaryPos - 3; i++) + { + if (buffer[i] == 0x0D && buffer[i+1] == 0x0A && buffer[i+2] == 0x0D && buffer[i+3] == 0x0A) + { + headerEndPos = i + 4; + break; + } + } + + if (headerEndPos > 0) + { + // 提取图像数据 + int imageLength = nextBoundaryPos - headerEndPos; + byte[] imageBytes = new byte[imageLength]; + Array.Copy(buffer, headerEndPos, imageBytes, 0, imageLength); + + // 验证并处理图像数据 + if (IsValidImageData(imageBytes)) + { + try + { + using (MemoryStream ms = new MemoryStream(imageBytes)) + { + ms.Write(new byte[0], 0, 0); + Image receivedImage = Image.FromStream(ms); + OnImageReceived(new ImageReceivedEventArgs(receivedImage, _currentImageMode)); + } + return nextBoundaryPos; + } + catch (Exception ex) + { + Console.WriteLine($"处理multipart图像失败: {ex.Message}"); + } + } + } + return nextBoundaryPos; + } + nextBoundaryPos++; + } + break; + } + startPos++; + } + return -1; + } + + #endregion 图像处理辅助方法 + #region 视频流处理方法 /// @@ -439,26 +1374,26 @@ namespace JoyD.Windows.CS.Toprie } /// - /// 发送模式变更命令 + /// 发送模式切换命令 /// /// 图像模式 - private void SendModeChangeCommand(ImageMode mode) + private bool SendModeChangeCommand(ImageMode mode) { try { - // 使用HTTP请求来切换图像模式 - string modePath = mode == ImageMode.Thermal ? "thermal.jpg" : "color.jpg"; - string url = $"http://{_deviceIp}:{_devicePort}/{modePath}?mode={mode}"; - - using (WebClient client = new WebClient()) + if (_a8Sdk != null) { - client.DownloadData(url); // 发送请求但不处理响应 + // 根据当前图像模式设置SDK的图像模式 + bool success = _a8Sdk.SetImageMode(mode); + return success; } + return false; } catch (Exception ex) { Console.WriteLine($"切换图像模式失败: {ex.Message}"); - OnConnectionException(new ConnectionExceptionEventArgs(ex, "发送模式变更命令失败")); + OnConnectionException(new ConnectionExceptionEventArgs(ex, "切换图像模式失败")); + return false; } } @@ -496,60 +1431,38 @@ namespace JoyD.Windows.CS.Toprie { try { + // 持续获取图像数据 while (!_stopRequested.IsSet) { - try + if (_a8Sdk != null) { - // 根据当前模式选择图像路径 - string imagePath = _currentImageMode == ImageMode.Thermal ? "thermal.jpg" : "color.jpg"; - string url = $"http://{_deviceIp}:{_devicePort}/{imagePath}?{DateTime.Now.Ticks}"; - - HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); - request.Timeout = 5000; // 5秒超时 - request.Method = "GET"; - request.KeepAlive = true; - - using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + try { - if (response.StatusCode == HttpStatusCode.OK) + // 使用A8SDK获取图像数据 + byte[] imageData = _a8Sdk.GetImageData(); + + if (imageData != null && imageData.Length > 0 && IsValidImageData(imageData)) { - using (Stream stream = response.GetResponseStream()) + // 创建内存流 + using (MemoryStream ms = new MemoryStream(imageData)) { - using (MemoryStream memoryStream = new MemoryStream()) + // 创建图像 + using (Image image = Image.FromStream(ms)) { - byte[] buffer = new byte[4096]; - int bytesRead; - while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) - { - memoryStream.Write(buffer, 0, bytesRead); - } - - byte[] imageData = memoryStream.ToArray(); - if (IsValidImageData(imageData)) - { - using (MemoryStream imageStream = new MemoryStream(imageData)) - { - Image image = Image.FromStream(imageStream); - OnImageReceived(new ImageReceivedEventArgs(image)); - } - } + // 触发图像接收事件 + OnImageReceived(new ImageReceivedEventArgs(image.Clone() as Image)); } } } } - } - catch (WebException webEx) - { - // 处理超时等网络错误 - if (webEx.Status == WebExceptionStatus.Timeout) + catch (Exception ex) { - Console.WriteLine("图像接收超时"); - } - else if (webEx.Status == WebExceptionStatus.ConnectFailure) - { - Console.WriteLine("图像连接失败,可能设备已断开"); - // 触发连接断开事件 - UpdateConnectionStatus(ConnectionStatus.Disconnected, "图像连接失败,设备可能已断开", webEx); + Console.WriteLine($"SDK获取图像失败: {ex.Message}"); + OnConnectionException(new ConnectionExceptionEventArgs(ex, "SDK获取图像数据异常")); + + // 检查连接状态 + UpdateConnectionStatus(ConnectionStatus.Disconnected, "SDK获取图像失败,设备可能已断开", ex); + // 如果启用了自动重连,开始重连 if (_isAutoReconnectEnabled) { @@ -557,18 +1470,8 @@ namespace JoyD.Windows.CS.Toprie } break; // 退出图像接收线程 } - else - { - Console.WriteLine($"图像接收错误: {webEx.Message}"); - OnConnectionException(new ConnectionExceptionEventArgs(webEx, "图像接收过程中发生异常")); - } } - catch (Exception ex) - { - Console.WriteLine($"图像处理错误: {ex.Message}"); - OnConnectionException(new ConnectionExceptionEventArgs(ex, "图像处理过程中发生异常")); - } - + // 短暂休眠,控制帧率 Thread.Sleep(100); } @@ -639,30 +1542,65 @@ namespace JoyD.Windows.CS.Toprie { if (!_isInitialized && !Initialize()) { + Console.WriteLine("设备管理器未初始化,无法搜索设备"); return new List(); } List foundDevices = new List(); _deviceIds.Clear(); + _deviceList.Clear(); - // 1. 尝试UDP广播搜索 - List discoveredIps = BroadcastSearch(); + // 使用A8SDK的静态方法进行设备搜索 + List deviceIds = A8SDK.SearchDevices(); - // 2. 如果UDP广播没有找到设备,尝试在本地网络扫描 - if (discoveredIps.Count == 0) - { - discoveredIps = ScanLocalNetwork(); - } - - // 为每个发现的IP分配一个设备ID + Console.WriteLine($"搜索到 {deviceIds.Count} 个设备"); + + // 将SDK返回的设备ID和IP转换为我们的格式 int deviceId = 1; - foreach (string ip in discoveredIps) + foreach (string id in deviceIds) { - _deviceIds.Add(deviceId); - foundDevices.Add(deviceId); - deviceId++; + try + { + // 假设ID字符串中包含IP信息,格式可能是 "设备ID|IP地址" + string[] parts = id.Split('|'); + string ipAddress = parts.Length > 1 ? parts[1] : "未知IP"; + + // 创建设备信息对象 + DeviceInfo deviceInfo = new DeviceInfo + { + DeviceID = deviceId, + IPAddress = ipAddress, + Model = "A8", // 假设是A8型号,可以根据实际情况获取 + SerialNumber = parts[0] // 使用原始ID作为序列号 + }; + + _deviceIds.Add(id); + _deviceList.Add(deviceInfo); + foundDevices.Add(deviceId); + + Console.WriteLine($"发现设备: ID={deviceId}, IP={ipAddress}, 序列号={parts[0]}"); + + deviceId++; + } + catch (Exception ex) + { + Console.WriteLine($"解析设备信息失败: {ex.Message}, 原始ID: {id}"); + // 继续处理下一个设备 + } } + // 如果通过ID连接模式,且目标设备ID已设置,尝试自动连接 + if (_targetDeviceId > 0 && _deviceList.Count > 0) + { + var targetDevice = _deviceList.FirstOrDefault(d => d.DeviceID == _targetDeviceId); + if (targetDevice != null) + { + Console.WriteLine($"找到目标设备ID={_targetDeviceId},IP={targetDevice.IPAddress},准备连接"); + _deviceIp = targetDevice.IPAddress; + ConnectDevice(); + } + } + return foundDevices; } catch (Exception ex) @@ -678,128 +1616,86 @@ namespace JoyD.Windows.CS.Toprie /// public Task> SearchDevicesAsync() { - return Task.Factory.StartNew(() => SearchDevices()); - } - - /// - /// UDP广播搜索设备 - /// - /// 发现的设备IP列表 - private List BroadcastSearch() - { - List discoveredIps = new List(); - - try + return Task.Factory.StartNew(() => { - // 使用UDP广播查找设备 - using (UdpClient udpClient = new UdpClient()) + try { - udpClient.EnableBroadcast = true; - udpClient.Client.ReceiveTimeout = 2000; + if (!_isInitialized && !Initialize()) + { + Console.WriteLine("设备管理器未初始化,无法搜索设备"); + return new List(); + } - // 广播地址和端口 - IPEndPoint broadcastEndpoint = new IPEndPoint(IPAddress.Broadcast, 8080); + List foundDevices = new List(); + _deviceIds.Clear(); + _deviceList.Clear(); + + // 使用A8SDK的静态方法进行设备搜索 + List deviceIds = A8SDK.SearchDevices(); - // 发送搜索命令 - byte[] searchCommand = Encoding.ASCII.GetBytes("SEARCH_DEVICE"); - udpClient.Send(searchCommand, searchCommand.Length, broadcastEndpoint); - - // 接收响应 - IPEndPoint remoteEndpoint = new IPEndPoint(IPAddress.Any, 0); + Console.WriteLine($"搜索到 {deviceIds.Count} 个设备"); - try + // 将SDK返回的设备ID和IP转换为我们的格式 + int deviceId = 1; + foreach (string id in deviceIds) { - byte[] response = udpClient.Receive(ref remoteEndpoint); - string responseString = Encoding.ASCII.GetString(response); - - // 检查响应是否来自我们的设备 - if (responseString.Contains("DEVICE_FOUND")) + try { - discoveredIps.Add(remoteEndpoint.Address.ToString()); - } - } - catch (SocketException ex) - { - // 超时异常,正常处理 - if (ex.SocketErrorCode != SocketError.TimedOut) - { - Console.WriteLine($"UDP接收异常: {ex.Message}"); - } - } - } - - // 额外检查默认IP是否可达 - if (IsIpReachable(_deviceIp)) - { - if (!discoveredIps.Contains(_deviceIp)) - { - discoveredIps.Add(_deviceIp); - } - } - } - catch (Exception ex) - { - Console.WriteLine($"UDP广播搜索失败: {ex.Message}"); - } - - return discoveredIps; - } - - /// - /// 扫描本地网络 - /// - /// 发现的设备IP列表 - private List ScanLocalNetwork() - { - List discoveredIps = new List(); - - try - { - // 获取本地IP地址和子网 - foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) - { - if (ni.OperationalStatus == OperationalStatus.Up && ni.NetworkInterfaceType != NetworkInterfaceType.Loopback) - { - foreach (UnicastIPAddressInformation ipInfo in ni.GetIPProperties().UnicastAddresses) - { - if (ipInfo.Address.AddressFamily == AddressFamily.InterNetwork) + // 假设ID字符串中包含IP信息,格式可能是 "设备ID|IP地址" + string[] parts = id.Split('|'); + string ipAddress = parts.Length > 1 ? parts[1] : "未知IP"; + + // 创建设备信息对象 + DeviceInfo deviceInfo = new DeviceInfo { - IPAddress ip = ipInfo.Address; - IPAddress subnet = GetSubnetMask(ipInfo.IPv4Mask); - - // 扫描常见的设备IP范围 - // 这里简化处理,只扫描几个常用的IP - string[] commonDeviceIps = new string[] - { - "192.168.100.2", // 默认IP - "192.168.1.10", - "192.168.0.10", - "10.0.0.10" - }; - - foreach (string deviceIp in commonDeviceIps) - { - if (IsIpReachable(deviceIp)) - { - if (!discoveredIps.Contains(deviceIp)) - { - discoveredIps.Add(deviceIp); - } - } - } - } + DeviceID = deviceId, + IPAddress = ipAddress, + Model = "A8", // 假设是A8型号,可以根据实际情况获取 + SerialNumber = parts[0] // 使用原始ID作为序列号 + }; + + _deviceIds.Add(id); + _deviceList.Add(deviceInfo); + foundDevices.Add(deviceId); + + Console.WriteLine($"发现设备: ID={deviceId}, IP={ipAddress}, 序列号={parts[0]}"); + + deviceId++; + } + catch (Exception ex) + { + Console.WriteLine($"解析设备信息失败: {ex.Message}, 原始ID: {id}"); + // 继续处理下一个设备 } } - } - } - catch (Exception ex) - { - Console.WriteLine($"本地网络扫描失败: {ex.Message}"); - } - return discoveredIps; + // 如果通过ID连接模式,且目标设备ID已设置,尝试自动连接 + if (_targetDeviceId > 0 && _deviceList.Count > 0) + { + var targetDevice = _deviceList.FirstOrDefault(d => d.DeviceID == _targetDeviceId); + if (targetDevice != null) + { + Console.WriteLine($"找到目标设备ID={_targetDeviceId},IP={targetDevice.IPAddress},准备连接"); + _deviceIp = targetDevice.IPAddress; + ConnectDevice(); + } + } + + return foundDevices; + } + catch (Exception ex) + { + Console.WriteLine($"搜索设备失败: {ex.Message}"); + OnConnectionException(new ConnectionExceptionEventArgs(ex, "搜索设备失败")); + return new List(); + } + }); } + // BroadcastSearch方法已被A8SDK.SearchDevices替代 + + // ScanLocalNetwork方法已被A8SDK.SearchDevices替代 + /// /// 获取子网掩码 /// @@ -815,116 +1711,278 @@ namespace JoyD.Windows.CS.Toprie /// /// 是否连接成功 public bool ConnectDevice() + { + // 默认使用ID=1连接设备 + ConnectDevice(1); + return _connectionStatus == ConnectionStatus.Connected; + } + + public void ConnectDevice(int deviceId) { try { + // 取消之前的连接操作 + if (_connectCancellationTokenSource != null) + { + _connectCancellationTokenSource.Cancel(); + _connectCancellationTokenSource.Dispose(); + } + + // 更新状态为连接中 UpdateConnectionStatus(ConnectionStatus.Connecting, "开始连接设备..."); - - // 确保已初始化 - if (!_isInitialized && !Initialize()) - { - UpdateConnectionStatus(ConnectionStatus.Disconnected, "设备管理器初始化失败"); - return false; - } - // 停止任何现有的重连定时器 - StopAutoReconnect(); + _connectCancellationTokenSource = new CancellationTokenSource(); + CancellationToken token = _connectCancellationTokenSource.Token; - // 停止任何正在进行的连接检查 - StopAllImageReceivers(); - - // 检查IP是否可达 - if (!IsIpReachable(_deviceIp)) + // 使用Timer实现超时取消 + System.Threading.Timer timeoutTimer = null; + timeoutTimer = new System.Threading.Timer((state) => { - UpdateConnectionStatus(ConnectionStatus.Disconnected, $"设备IP {_deviceIp} 不可达"); - if (_isAutoReconnectEnabled) + if (_connectCancellationTokenSource != null && !_connectCancellationTokenSource.IsCancellationRequested) { - StartAutoReconnect(); + _connectCancellationTokenSource.Cancel(); + Console.WriteLine($"连接超时,自动取消连接操作"); } - return false; - } + timeoutTimer.Dispose(); + }, null, _connectTimeout, Timeout.Infinite); - // 尝试TCP连接测试 - bool tcpConnected = TestTcpConnection(_deviceIp, _devicePort); - if (!tcpConnected) + try { - UpdateConnectionStatus(ConnectionStatus.Disconnected, $"TCP连接到 {_deviceIp}:{_devicePort} 失败"); - if (_isAutoReconnectEnabled) + bool result = false; + if (!token.IsCancellationRequested) { - StartAutoReconnect(); - } - return false; - } + try + { + // 确保已初始化 + if (!_isInitialized && !Initialize()) + { + UpdateConnectionStatus(ConnectionStatus.Disconnected, "设备管理器初始化失败"); + return; + } - // 发送连接命令 - bool commandSent = SendConnectCommand(); - if (!commandSent) + // 停止任何现有的重连定时器 + StopAutoReconnect(); + + // 停止任何正在进行的连接检查 + StopAllImageReceivers(); + + // 使用ManualResetEvent实现连接超时机制 + var connectionCompleteEvent = new ManualResetEvent(false); + bool timeoutOccurred = false; + + // 真实连接过程 + System.Threading.ThreadPool.QueueUserWorkItem((state) => + { + try + { + if (!token.IsCancellationRequested) + { + // 安全释放之前的SDK实例,避免内存访问冲突 + if (_a8Sdk != null) + { + Console.WriteLine("释放之前的SDK实例"); + _a8Sdk = null; + } + + // 使用真实SDK连接设备 + Console.WriteLine($"正在使用SDK连接设备 {deviceId},IP地址: {_deviceIp}"); + + // 先检测IP可达性,避免不必要的SDK初始化 + if (!PingDevice(_deviceIp)) + { + Console.WriteLine($"设备IP {_deviceIp} 不可达,连接失败"); + throw new Exception($"设备IP {_deviceIp} 不可达"); + } + + // 创建SDK实例 + _a8Sdk = new A8SDK(_deviceIp); + Console.WriteLine("SDK实例创建完成"); + + // 验证连接是否成功(通过心跳检测) + if (!_a8Sdk.Heartbeat()) + { + Console.WriteLine("心跳检测失败"); + // 安全释放SDK实例 + if (_a8Sdk != null) + { + _a8Sdk = null; + } + throw new Exception("心跳检测失败"); + } + Console.WriteLine("心跳检测成功,设备连接有效"); + + // 连接成功 + result = true; + + // 安全地设置设备ID + try + { + _currentDeviceId = deviceId; + } + catch (Exception ex) + { + Console.WriteLine($"设置设备ID异常: {ex.Message}"); + _currentDeviceId = -1; // 设置为无效ID + } + Console.WriteLine($"设备 {deviceId} 连接成功"); + } + } + catch (Exception ex) + { + Console.WriteLine($"连接设备异常: {ex.Message}"); + // 确保异常时释放资源 + if (_a8Sdk != null) + { + _a8Sdk = null; + } + result = false; + } + finally + { + connectionCompleteEvent.Set(); + } + }); + + // 等待连接完成或超时 + if (!connectionCompleteEvent.WaitOne(_connectTimeout)) + { + timeoutOccurred = true; + Console.WriteLine($"设备 {deviceId} 连接超时"); + result = false; + _connectCancellationTokenSource.Cancel(); + } + + // 如果超时,记录超时信息 + if (timeoutOccurred) + { + UpdateConnectionStatus(ConnectionStatus.Disconnected, + $"设备 {deviceId} 连接超时({_connectTimeout}ms)"); + return; + } + } + catch (Exception ex) + { + Console.WriteLine($"连接设备异常: {ex.Message}"); + result = false; + } + } + + if (result) + { + _isConnected = true; + _reconnectAttempts = 0; + + // 启动心跳检测和连接检查 + StartHeartbeat(); + StartConnectionCheck(); + + UpdateConnectionStatus(ConnectionStatus.Connected, "设备连接成功"); + } + else if (!token.IsCancellationRequested) + { + UpdateConnectionStatus(ConnectionStatus.Disconnected, "SDK连接失败,心跳检测未通过"); + _a8Sdk = null; + } + } + finally { - UpdateConnectionStatus(ConnectionStatus.Disconnected, "发送连接命令失败"); - if (_isAutoReconnectEnabled) + // 确保即使发生异常,也停止定时器 + if (timeoutTimer != null) { - StartAutoReconnect(); + timeoutTimer.Dispose(); } - return false; } - - // 启动心跳检测 - StartHeartbeat(); - - UpdateConnectionStatus(ConnectionStatus.Connected, "设备连接成功"); - - // 重置重连尝试计数 - _reconnectAttempts = 0; - - return true; } catch (Exception ex) { Console.WriteLine($"连接设备失败: {ex.Message}"); UpdateConnectionStatus(ConnectionStatus.Disconnected, "连接设备时发生异常", ex); OnConnectionException(new ConnectionExceptionEventArgs(ex, "连接设备失败")); - - // 如果启用了自动重连,开始重连 - if (_isAutoReconnectEnabled) - { - StartAutoReconnect(); - } - - return false; + } + + // 如果连接失败且启用了自动重连,开始重连 + if (_connectionStatus != ConnectionStatus.Connected && _isAutoReconnectEnabled) + { + StartAutoReconnect(); } } - + /// - /// 测试TCP连接 + /// 连接到指定设备(异步版本) /// - /// IP地址 - /// 端口 - /// 是否连接成功 - private bool TestTcpConnection(string ip, int port) + public Task ConnectAsync() + { + var taskCompletionSource = new TaskCompletionSource(); + + try + { + ConnectDevice(); + taskCompletionSource.SetResult(_connectionStatus == ConnectionStatus.Connected); + } + catch (Exception ex) + { + taskCompletionSource.SetException(ex); + } + + return taskCompletionSource.Task; + } + + /// + /// 根据设备ID连接设备 + /// + /// 设备ID + public void ConnectDevice(int deviceId) { try { - using (TcpClient client = new TcpClient()) + _targetDeviceId = deviceId; + + // 检查是否已有设备列表 + if (_deviceList != null && _deviceList.Count > 0) { - IAsyncResult result = client.BeginConnect(ip, port, null, null); - bool success = result.AsyncWaitHandle.WaitOne(2000); // 2秒超时 - if (success) + // 查找指定ID的设备 + var device = _deviceList.FirstOrDefault(d => d.DeviceID == deviceId); + if (device != null) { - client.EndConnect(result); - return true; - } - else - { - client.Close(); - return false; + _deviceIp = device.IPAddress; + ConnectDevice(); + return; } } + + // 如果找不到设备,先搜索设备 + SearchDevicesAsync(); + + // 延迟后尝试连接 + Task.Delay(500).ContinueWith(t => + { + if (_deviceList != null && _deviceList.Count > 0) + { + var device = _deviceList.FirstOrDefault(d => d.DeviceID == deviceId); + if (device != null) + { + _deviceIp = device.IPAddress; + ConnectDevice(); + } + } + }); } - catch (Exception) + catch (Exception ex) { - return false; + UpdateConnectionStatus(ConnectionStatus.Disconnected, $"通过设备ID连接异常: {ex.Message}", ex); + OnConnectionException(new ConnectionExceptionEventArgs(ex, "通过设备ID连接失败")); } } + + /// + /// 断开设备连接(兼容Form1) + /// + public void DisconnectDevice() + { + Disconnect(); + } + + // TestTcpConnection方法不再需要,由A8SDK内部处理连接测试 /// /// 发送连接命令 @@ -965,22 +2023,51 @@ namespace JoyD.Windows.CS.Toprie { try { - // 停止重连定时器 + // 停止自动重连和连接检查 StopAutoReconnect(); + StopConnectionCheck(); - // 停止心跳检测 - StopHeartbeat(); - - // 停止图像接收 - StopAllImageReceivers(); - - // 设置断开状态 - UpdateConnectionStatus(ConnectionStatus.Disconnected, "设备已断开连接"); + // 取消正在进行的连接操作 + if (_connectCancellationTokenSource != null) + { + _connectCancellationTokenSource.Cancel(); + _connectCancellationTokenSource.Dispose(); + _connectCancellationTokenSource = null; + } + + // 更新状态为断开连接中 + if (_connectionStatus != ConnectionStatus.Disconnected) + { + UpdateConnectionStatus(ConnectionStatus.Disconnected, "正在断开连接..."); + } + + // 使用真实SDK进行设备断开操作 + if (_a8Sdk != null && _currentDeviceId != -1) + { + try + { + // 可以通过发送特定命令或释放资源来实现断开连接 + // 如果SDK没有直接的断开连接方法,我们释放SDK实例 + Console.WriteLine($"设备 {_currentDeviceId} 断开连接中..."); + _a8Sdk = null; + Console.WriteLine($"设备 {_currentDeviceId} 断开连接成功"); + } + catch (Exception disconnectEx) + { + Console.WriteLine($"设备断开连接过程中发生异常: {disconnectEx.Message}"); + } + } + + // 重置设备ID + _currentDeviceId = -1; + UpdateConnectionStatus(ConnectionStatus.Disconnected, "设备已断开连接"); } catch (Exception ex) { - Console.WriteLine($"断开连接时发生异常: {ex.Message}"); - OnConnectionException(new ConnectionExceptionEventArgs(ex, "断开连接失败")); + Console.WriteLine($"断开连接异常: {ex.Message}"); + UpdateConnectionStatus(ConnectionStatus.Disconnected, "断开连接时发生异常", ex); + OnConnectionException(new ConnectionExceptionEventArgs( + ex, "断开连接异常")); } } @@ -996,15 +2083,69 @@ namespace JoyD.Windows.CS.Toprie _reconnectTimer = new System.Threading.Timer(ReconnectCallback, null, 0, _reconnectInterval); } + /// + /// 启动定期连接状态检查 + /// + /// 检查间隔(毫秒) + public void StartPeriodicConnectionCheck(int checkInterval) + { + try + { + // 停止现有的检查 + StopConnectionCheck(); + + if (checkInterval <= 0) + { + checkInterval = 5000; // 默认5秒 + } + + // 创建并启动新的检查定时器 + _connectionCheckTimer = new System.Threading.Timer((state) => + { + try + { + CheckConnectionValidity(); + } + catch (Exception ex) + { + Console.WriteLine($"定期连接检查异常: {ex.Message}"); + } + }, null, 0, checkInterval); + + Console.WriteLine($"定期连接状态检查已启动,间隔: {checkInterval}ms"); + } + catch (Exception ex) + { + Console.WriteLine($"启动定期连接检查失败: {ex.Message}"); + OnConnectionException(new ConnectionExceptionEventArgs(ex, "启动连接检查失败")); + } + } + /// /// 停止自动重连 /// private void StopAutoReconnect() { - if (_reconnectTimer != null) + try { - _reconnectTimer.Change(Timeout.Infinite, Timeout.Infinite); - _reconnectTimer.Dispose(); + // 使用局部变量暂存,避免多线程访问时的空引用问题 + System.Threading.Timer timerToDispose = Interlocked.Exchange(ref _reconnectTimer, null); + if (timerToDispose != null) + { + // 立即停止定时器,避免回调执行 + timerToDispose.Change(Timeout.Infinite, Timeout.Infinite); + // 安全释放定时器资源 + timerToDispose.Dispose(); + Console.WriteLine("自动重连已停止"); + } + + // 重置重连尝试次数 + _currentReconnectAttempt = 0; + } + catch (Exception ex) + { + Console.WriteLine($"停止自动重连异常: {ex.Message}"); + // 确保即使异常,引用也被清空 _reconnectTimer = null; } } @@ -1017,35 +2158,184 @@ namespace JoyD.Windows.CS.Toprie { try { - // 检查是否达到最大重连次数 - if (_maxReconnectAttempts > 0 && _reconnectAttempts >= _maxReconnectAttempts) + _currentReconnectAttempt++; + Console.WriteLine($"自动重连尝试 {_currentReconnectAttempt}"); + + UpdateConnectionStatus(ConnectionStatus.Reconnecting, + $"尝试自动重连..."); + + bool connectionSuccessful = false; + + // 仅用IP地址重连设备,这样更直接更快 + if (!string.IsNullOrEmpty(_deviceIp)) { - UpdateConnectionStatus(ConnectionStatus.Disconnected, $"达到最大重连次数 {_maxReconnectAttempts}"); + Console.WriteLine($"用IP地址 {_deviceIp} 重连设备"); + try + { + // 检查IP是否可达 + if (PingDevice(_deviceIp)) + { + // 注意:这里需要先释放旧实例,避免资源泄漏 + if (_a8Sdk != null) + { + try + { + // 调用静态SDK_destroy方法释放全局资源 + // A8SDK.SDK_destroy(); + Console.WriteLine("重连前SDK全局资源已释放"); + } + catch (Exception ex) + { + Console.WriteLine($"释放旧SDK实例资源时发生异常: {ex.Message}"); + } + finally + { + _a8Sdk = null; // 确保引用被清空 + } + } + + // 创建新的SDK实例 + Console.WriteLine($"创建新的SDK实例,目标IP: {_deviceIp}"); + _a8Sdk = new A8SDK(_deviceIp); + + // 添加心跳检测重试机制,适应网络不稳定情况 + bool isConnected = false; + int maxHeartbeatRetries = 3; // 心跳检测最大重试次数 + + for (int retry = 0; retry < maxHeartbeatRetries; retry++) + { + try + { + Console.WriteLine($"尝试建立连接并发送心跳包...(尝试 {retry + 1}/{maxHeartbeatRetries})"); + + // 添加延时,给SDK实例初始化一些时间 + if (retry > 0) + { + Console.WriteLine("等待500ms后重试..."); + Thread.Sleep(500); + } + + // 发送心跳包验证连接 + bool heartbeatResult = _a8Sdk.Heartbeat(); + if (heartbeatResult) + { + Console.WriteLine("心跳检测成功!"); + isConnected = true; + break; + } + else + { + Console.WriteLine("心跳检测失败"); + } + } + catch (Exception ex) + { + Console.WriteLine($"心跳检测异常: {ex.Message}"); + } + } + + if (isConnected) + { + // 连接成功,进行额外验证 + try + { + Console.WriteLine("进行额外连接验证..."); + // 再次发送心跳包确保连接稳定 + bool finalResult = _a8Sdk.Heartbeat(); + if (finalResult) + { + Console.WriteLine($"使用IP地址 {_deviceIp} 重连成功"); + _currentDeviceId = 1; // 临时ID,确保状态更新正确 + UpdateConnectionStatus(ConnectionStatus.Connected, $"设备 {_deviceIp} 连接成功"); + StartConnectionCheck(); + connectionSuccessful = true; + } + else + { + Console.WriteLine("最终验证失败"); + } + } + catch (Exception ex) + { + Console.WriteLine($"连接验证异常: {ex.Message}"); + } + } + + // 如果连接不成功,释放资源 + if (!connectionSuccessful) + { + Console.WriteLine($"使用IP地址 {_deviceIp} 重连失败,所有心跳尝试都未成功"); + _a8Sdk = null; + } + } + else + { + Console.WriteLine($"IP地址 {_deviceIp} 不可达,等待设备上线"); + } + } + catch (Exception ex) + { + Console.WriteLine($"使用IP地址重连异常: {ex.Message}"); + } + } + + // 如果IP地址连接失败,再尝试使用设备ID连接(保持兼容性) + if (!connectionSuccessful && _currentDeviceId != -1) + { + Console.WriteLine($"尝试使用设备ID {_currentDeviceId} 连接"); + ConnectDevice(_currentDeviceId); + if (_connectionStatus == ConnectionStatus.Connected) + { + connectionSuccessful = true; + } + } + + // 如果没有保存的设备ID,但有搜索到的设备,尝试连接第一个 + if (!connectionSuccessful && _deviceIds != null && _deviceIds.Count > 0) + { + Console.WriteLine($"尝试使用搜索到的设备列表中的第一个设备"); + ConnectDevice(_deviceIds[0]); + if (_connectionStatus == ConnectionStatus.Connected) + { + connectionSuccessful = true; + } + } + + // 如果连接成功,停止重连定时器 + if (connectionSuccessful) + { + Console.WriteLine("设备连接成功,停止自动重连"); StopAutoReconnect(); return; } - - _reconnectAttempts++; - UpdateConnectionStatus(ConnectionStatus.Reconnecting, $"尝试重连... (第 {_reconnectAttempts} 次)"); - // 检查IP是否可达 - if (IsIpReachable(_deviceIp)) + // 设置下一次重连的时间间隔 + int retryInterval = _reconnectInterval; + if (_currentReconnectAttempt % 5 == 0) // 每5次尝试后增加间隔 { - // 尝试连接 - bool connected = ConnectDevice(); - if (connected) - { - UpdateConnectionStatus(ConnectionStatus.Connected, "自动重连成功"); - } + retryInterval = Math.Min(retryInterval * 2, 10000); // 最大10秒 + Console.WriteLine($"调整重连间隔为 {retryInterval}ms"); + } + Console.WriteLine($"重连失败,{retryInterval}ms后再次尝试"); + + // 更新定时器,确保下一次重连 + if (_reconnectTimer != null) + { + _reconnectTimer.Change(retryInterval, Timeout.Infinite); } } catch (Exception ex) { - Console.WriteLine($"自动重连时发生异常: {ex.Message}"); - OnConnectionException(new ConnectionExceptionEventArgs(ex, "自动重连失败")); + Console.WriteLine($"重连回调函数发生异常: {ex.Message}"); + // 确保定时器继续工作,防止重连机制中断 + if (_reconnectTimer != null) + { + _reconnectTimer.Change(_reconnectInterval, Timeout.Infinite); + } } } + /// /// 开始心跳检测 /// @@ -1080,7 +2370,7 @@ namespace JoyD.Windows.CS.Toprie try { // 检查设备是否可达 - if (!IsIpReachable(_deviceIp)) + if (!PingDevice(_deviceIp)) { Console.WriteLine("心跳检测失败,设备不可达"); UpdateConnectionStatus(ConnectionStatus.Disconnected, "心跳检测失败,设备不可达"); @@ -1093,25 +2383,24 @@ namespace JoyD.Windows.CS.Toprie return; } - // 尝试发送心跳请求 - string url = $"http://{_deviceIp}:{_devicePort}/heartbeat"; - try + // 使用SDK的Heartbeat方法进行心跳检测 + if (_a8Sdk != null) { - WebRequest request = WebRequest.Create(url); - request.Timeout = 2000; // 2秒超时 - using (WebResponse response = request.GetResponse()) + if (!_a8Sdk.Heartbeat()) { - using (Stream dataStream = response.GetResponseStream()) + Console.WriteLine("SDK心跳检测失败"); + UpdateConnectionStatus(ConnectionStatus.Disconnected, "SDK心跳检测失败"); + + // 如果启用了自动重连,开始重连 + if (_isAutoReconnectEnabled) { - // 只需确保请求成功,不需要读取内容 - dataStream.ReadByte(); + StartAutoReconnect(); } } } - catch (Exception) + else { - // 心跳请求失败,但继续使用PING作为备用检测 - Console.WriteLine("心跳请求失败,但PING检测通过"); + Console.WriteLine("SDK实例不存在"); } } catch (Exception ex) @@ -1127,6 +2416,383 @@ namespace JoyD.Windows.CS.Toprie private bool _disposed = false; + /// + /// 释放资源 + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + Console.WriteLine($"温度数据已成功保存到: {filePath}"); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"写入CSV文件失败: {ex.Message}"); + return false; + } + } + catch (Exception ex) + { + Console.WriteLine($"保存温度数据到CSV时发生异常: {ex.Message}"); + return false; + } + } + + /// + /// 温度数据点结构体 + /// + private struct TemperatureDataPoint + { + public int X { get; private set; } + public int Y { get; private set; } + public float Temperature { get; private set; } + + public TemperatureDataPoint(int x, int y, float temperature) + { + X = x; + Y = y; + Temperature = temperature; + } + } + + /// + /// 启动定期连接状态检查(兼容Form1) + /// + public void StartPeriodicConnectionCheck(int intervalMs) + { + StartConnectionCheck(); + } + + /// + /// 停止定期连接状态检查(兼容Form1) + /// + public void StopPeriodicConnectionCheck() + { + StopConnectionCheck(); + } + + /// + /// 检查系统网络连接是否可用 + /// + /// 网络是否可用 + private bool IsNetworkAvailable() + { + try + { + // 检查系统网络连接状态 + System.Net.NetworkInformation.NetworkInterface[] interfaces = + System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces(); + + // 检查是否有活动的以太网或WiFi连接 + foreach (var ni in interfaces) + { + // 仅检查物理网络接口(以太网、WiFi等) + if (ni.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up && + (ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Ethernet || + ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Wireless80211) && + ni.Name != "本地连接*" && !ni.Description.Contains("虚拟") && !ni.Description.Contains("Virtual")) + { + // 获取该网络接口的IP信息,检查是否有有效的IP地址 + var properties = ni.GetIPProperties(); + foreach (var addr in properties.UnicastAddresses) + { + // 检查是否有IPv4地址 + if (addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + // 如果设备IP不为空,可以尝试检查设备IP是否在该接口的子网内 + if (!string.IsNullOrEmpty(_deviceIp)) + { + try + { + // 检查设备IP是否与当前网络接口在同一子网(粗略判断) + var deviceAddress = System.Net.IPAddress.Parse(_deviceIp); + if (deviceAddress.GetAddressBytes()[0] == addr.Address.GetAddressBytes()[0]) + { + return true; + } + } + catch { } + } + return true; + } + } + } + } + return false; + } + catch (Exception ex) + { + Console.WriteLine($"检查网络可用性异常: {ex.Message}"); + // 发生异常时默认为网络不可用,避免误判 + return false; + } + } + + /// + /// 尝试ping设备IP地址 + /// + /// 设备IP地址 + /// 是否ping通 + // PingDevice方法已在文件上方定义,删除重复实现 + + // 图像相关变量 + private Thread _imageReceiveThread; + private Thread _imageReconnectThread; + private bool _isReceivingImages = false; + private ManualResetEvent _stopImageThreadEvent = new ManualResetEvent(false); + private const int IMAGE_PORT = 8080; // HTTP端口,用于获取图像 + private TcpClient _imageTcpClient; + private NetworkStream _imageStream; + + /// + /// 温度数据点结构体,用于存储单个位置的温度数据 + /// + public struct TemperatureDataPoint + { + /// + /// X坐标 + /// + public int X { get; set; } + + /// + /// Y坐标 + /// + public int Y { get; set; } + + /// + /// 温度值 + /// + public float Temperature { get; set; } + + /// + /// 构造函数 + /// + /// X坐标 + /// Y坐标 + /// 温度值 + public TemperatureDataPoint(int x, int y, float temperature) + { + X = x; + Y = y; + Temperature = temperature; + } + } + + /// + /// 保存温度数据到CSV文件 + /// + /// CSV文件路径 + /// 是否保存成功 + public bool SaveTemperatureDataToCsv(string filePath) + { + try + { + // 检查设备是否连接 + if (!_isConnected || _a8Sdk == null) + { + Console.WriteLine("设备未连接,无法保存温度数据"); + return false; + } + + // 检查文件路径是否有效 + if (string.IsNullOrEmpty(filePath)) + { + Console.WriteLine("文件路径无效"); + return false; + } + + // 获取目录路径 + string directoryPath = Path.GetDirectoryName(filePath); + if (!Directory.Exists(directoryPath)) + { + try + { + // 创建目录 + Directory.CreateDirectory(directoryPath); + Console.WriteLine($"创建目录: {directoryPath}"); + } + catch (Exception ex) + { + Console.WriteLine($"创建目录失败: {ex.Message}"); + return false; + } + } + + // 尝试从设备获取温度数据 + List temperatureData = new List(); + + try + { + // 使用A8SDK的Get_all_temp方法获取所有温度数据 + Console.WriteLine("正在获取设备温度数据..."); + + ImageTemp imageTemp = _a8Sdk.Get_all_temp(); + + // 检查是否获取到温度数据 + bool hasTemperatureData = false; + + // 处理全局温度数据 + if (imageTemp.globa.max_temp > -273.0f) // 检查是否有有效温度值(高于绝对零度) + { + Console.WriteLine($"全局温度数据: 最高={imageTemp.globa.max_temp}°C, 最低={imageTemp.globa.min_temp}°C, 平均={imageTemp.globa.avg_temp}°C"); + + // 添加全局温度点 + temperatureData.Add(new TemperatureDataPoint(0, 0, imageTemp.globa.max_temp)); + temperatureData.Add(new TemperatureDataPoint(0, 1, imageTemp.globa.min_temp)); + temperatureData.Add(new TemperatureDataPoint(0, 2, imageTemp.globa.avg_temp)); + + // 添加最高温度点的坐标 + temperatureData.Add(new TemperatureDataPoint( + imageTemp.globa.max_temp_x, + imageTemp.globa.max_temp_y, + imageTemp.globa.max_temp)); + + // 添加最低温度点的坐标 + temperatureData.Add(new TemperatureDataPoint( + imageTemp.globa.min_temp_x, + imageTemp.globa.min_temp_y, + imageTemp.globa.min_temp)); + + hasTemperatureData = true; + } + + // 处理区域温度数据 + for (int i = 0; i < imageTemp.area.Length; i++) + { + if (imageTemp.area[i].enable == 1 && imageTemp.area[i].temp > -273.0f) + { + Console.WriteLine($"区域 {i+1} 温度: {imageTemp.area[i].temp}°C"); + temperatureData.Add(new TemperatureDataPoint(i + 1, 0, imageTemp.area[i].temp)); + hasTemperatureData = true; + } + } + + // 处理点温度数据 + for (int i = 0; i < imageTemp.spot.Length; i++) + { + if (imageTemp.spot[i].enable == 1 && imageTemp.spot[i].temp > -273.0f) + { + Console.WriteLine($"点 {i+1} 温度: {imageTemp.spot[i].temp}°C"); + temperatureData.Add(new TemperatureDataPoint(i + 1, 1, imageTemp.spot[i].temp)); + hasTemperatureData = true; + } + } + + if (!hasTemperatureData) + { + Console.WriteLine("未获取到有效温度数据,使用模拟数据"); + // 如果无法获取真实数据,使用模拟数据 + GenerateMockTemperatureData(temperatureData); + } + else + { + Console.WriteLine($"成功获取到 {temperatureData.Count} 个温度数据点"); + } + } + catch (Exception ex) + { + Console.WriteLine($"获取温度数据失败: {ex.Message}"); + // 如果获取真实数据失败,使用模拟数据 + Console.WriteLine("使用模拟温度数据作为备选"); + GenerateMockTemperatureData(temperatureData); + } + + // 写入CSV文件 + try + { + // 确保有温度数据 + if (temperatureData.Count == 0) + { + Console.WriteLine("没有温度数据可保存"); + return false; + } + + using (StreamWriter writer = new StreamWriter(filePath, false, Encoding.UTF8)) + { + // 写入CSV头部和元信息 + writer.WriteLine("# 温度数据导出"); + writer.WriteLine($"# 导出时间: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}"); + writer.WriteLine($"# 设备IP: {_deviceIp}"); + writer.WriteLine($"# 数据点数: {temperatureData.Count}"); + writer.WriteLine("#"); + + // 写入数据列头 + writer.WriteLine("数据类型,X坐标,Y坐标,温度值(°C)"); + + // 写入温度数据 + foreach (var dataPoint in temperatureData) + { + // 根据X坐标判断数据类型 + string dataType = "未知"; + if (dataPoint.X == 0 && dataPoint.Y == 0) dataType = "全局最高"; + else if (dataPoint.X == 0 && dataPoint.Y == 1) dataType = "全局最低"; + else if (dataPoint.X == 0 && dataPoint.Y == 2) dataType = "全局平均"; + else if (dataPoint.Y == 0 && dataPoint.X > 0 && dataPoint.X <= 6) dataType = $"区域{dataPoint.X}"; + else if (dataPoint.Y == 1 && dataPoint.X > 0 && dataPoint.X <= 6) dataType = $"点{dataPoint.X}"; + else dataType = "温度点"; + + writer.WriteLine($"{dataType},{dataPoint.X},{dataPoint.Y},{dataPoint.Temperature:F2}"); + } + } + + Console.WriteLine($"温度数据已成功保存到: {filePath}"); + Console.WriteLine($"共保存 {temperatureData.Count} 个温度数据点"); + return true; + } + catch (IOException ioEx) + { + Console.WriteLine($"IO错误,无法写入CSV文件: {ioEx.Message}"); + Console.WriteLine($"可能的原因: 文件正在被使用或磁盘空间不足"); + return false; + } + catch (UnauthorizedAccessException accessEx) + { + Console.WriteLine($"权限错误,无法写入CSV文件: {accessEx.Message}"); + Console.WriteLine($"请检查文件路径的访问权限"); + return false; + } + catch (Exception ex) + { + Console.WriteLine($"写入CSV文件时发生未知错误: {ex.Message}"); + Console.WriteLine($"错误详情: {ex.StackTrace}"); + return false; + } + } + catch (Exception ex) + { + Console.WriteLine($"保存温度数据到CSV时发生异常: {ex.Message}"); + return false; + } + } + + /// + /// 生成模拟温度数据 + /// + /// 温度数据列表 + private void GenerateMockTemperatureData(List temperatureData) + { + // 生成模拟数据 + const int width = 10; + const int height = 10; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // 生成一些模拟温度数据,有一定的变化模式 + float temperature = 25.0f + (float)(Math.Sin(x * 0.5) * Math.Cos(y * 0.5)) * 5.0f; + temperatureData.Add(new TemperatureDataPoint(x, y, temperature)); + } + } + } + + #region IDisposable 实现 + + private bool _disposed = false; + /// /// 释放资源 /// @@ -1148,13 +2814,48 @@ namespace JoyD.Windows.CS.Toprie { // 释放托管资源 StopAutoReconnect(); + StopConnectionCheck(); StopHeartbeat(); StopAllImageReceivers(); _stopRequested.Dispose(); } + // 安全释放SDK实例,避免内存访问冲突 + try + { + if (_a8Sdk != null) + { + Console.WriteLine("释放SDK实例资源"); + // 先设置为null,避免在Dispose过程中的访问 + _a8Sdk = null; + } + } + catch (Exception ex) + { + Console.WriteLine($"释放SDK实例异常: {ex.Message}"); + // 即使异常也确保引用被清空 + _a8Sdk = null; + } + + // 释放全局SDK资源,使用try-catch避免异常传播 + try + { + Console.WriteLine("释放SDK全局资源"); + if (_isInitialized) + { + // 如果SDK支持,调用静态方法释放全局资源 + // A8SDK.SDK_destroy(); + } + } + catch (Exception ex) + { + Console.WriteLine($"释放SDK全局资源异常: {ex.Message}"); + } + // 释放非托管资源 _isInitialized = false; + _connectionStatus = ConnectionStatus.Disconnected; + _currentDeviceId = -1; _disposed = true; } diff --git a/Windows/CS/Framework4.0/Toprie/Toprie/Toprie.csproj b/Windows/CS/Framework4.0/Toprie/Toprie/Toprie.csproj index cde2b8c..b208f93 100644 --- a/Windows/CS/Framework4.0/Toprie/Toprie/Toprie.csproj +++ b/Windows/CS/Framework4.0/Toprie/Toprie/Toprie.csproj @@ -25,6 +25,9 @@ true true + + Toprie.ico + @@ -74,5 +77,8 @@ Camera.cs + + + \ No newline at end of file diff --git a/Windows/CS/Framework4.0/Toprie/Toprie/Toprie.ico b/Windows/CS/Framework4.0/Toprie/Toprie/Toprie.ico new file mode 100644 index 0000000000000000000000000000000000000000..2cf24ede14823749d432be8902f6eb556a08fa85 GIT binary patch literal 67646 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