ssinfo-sender/Form1.cs
2025-07-31 17:15:47 +08:00

760 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using uPLibrary.Networking.M2Mqtt;
using uPLibrary.Networking.M2Mqtt.Messages;
using Newtonsoft.Json;
using aiot_paho_csharp;
namespace sscom_sender
{
public class MqttConfig
{
public string? ProductKey { get; set; }
public string? Topic { get; set; }
public string? DeviceName { get; set; }
public string? DeviceSecret { get; set; }
public string? Broker { get; set; }
public int Port { get; set; }
public bool UseSsl { get; set; }
}
public partial class Form1 : Form
{
private SerialPort? gpsPort;
private SerialPort? spectrometerPort;
private MqttClient? mqttClient;
private bool isRunning = false;
private string? logFileName;
// 数据队列
private ConcurrentQueue<GPSData> gpsDataQueue = new ConcurrentQueue<GPSData>();
private ConcurrentQueue<SpectrometerData> spectrometerDataQueue = new ConcurrentQueue<SpectrometerData>();
private ConcurrentQueue<string> logQueue = new ConcurrentQueue<string>();
// 线程控制
private CancellationTokenSource? cancellationTokenSource;
private Task? gpsTask;
private Task? spectrometerTask;
private Task? mqttTask;
private Task? logTask;
// 数据缓冲区
private StringBuilder gpsBuffer = new StringBuilder();
private StringBuilder spectrometerBuffer = new StringBuilder();
public Form1()
{
InitializeComponent();
InitializeForm();
}
private void InitializeForm()
{
// 初始化串口列表
RefreshSerialPorts();
// 加载MQTT配置
LoadMqttConfig();
// 用异步的方式InitializeMQTT();
// 初始化MQTT连接
_ = Task.Run(() => InitializeMQTT());
// 创建日志文件名
logFileName = Path.Combine(Application.StartupPath, "logs", $"log_{DateTime.Now:yyyyMMdd_HHmmss}.txt");
string? logDir = Path.GetDirectoryName(logFileName);
if (!string.IsNullOrEmpty(logDir))
{
_ = Directory.CreateDirectory(logDir);
}
}
private void RefreshSerialPorts()
{
string[] ports = SerialPort.GetPortNames();
comboBoxGPSPort.Items.Clear();
comboBoxSpectrometerPort.Items.Clear();
foreach (string port in ports)
{
_ = comboBoxGPSPort.Items.Add(port);
_ = comboBoxSpectrometerPort.Items.Add(port);
}
if (ports.Length > 0)
{
comboBoxGPSPort.SelectedIndex = 0;
if (ports.Length > 1)
comboBoxSpectrometerPort.SelectedIndex = 1;
else
comboBoxSpectrometerPort.SelectedIndex = 0;
}
}
private void buttonStartStop_Click(object sender, EventArgs e)
{
if (!isRunning)
{
StartDataCollection();
}
else
{
StopDataCollection();
}
}
private void StartDataCollection()
{
try
{
// 初始化串口
InitializeSerialPorts();
// 启动线程
cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
gpsTask = Task.Run(() => GPSDataReader(token), token);
spectrometerTask = Task.Run(() => SpectrometerDataReader(token), token);
mqttTask = Task.Run(() => MQTTDataSender(token), token);
logTask = Task.Run(() => LogWriter(token), token);
isRunning = true;
buttonStartStop.Text = "停止";
buttonStartStop.BackColor = Color.Red;
LogMessage("数据采集已启动");
}
catch (Exception ex)
{
_ = MessageBox.Show($"启动失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void StopDataCollection()
{
try
{
isRunning = false;
cancellationTokenSource?.Cancel();
// 立即更新UI状态
buttonStartStop.Text = "开始";
buttonStartStop.BackColor = SystemColors.Control;
LogMessage("正在停止数据采集...");
// 异步等待任务完成避免阻塞UI线程
_ = Task.Run(() =>
{
try
{
// 给任务一些时间优雅退出
var tasks = new[] { gpsTask, spectrometerTask, mqttTask, logTask }
.Where(t => t != null)
.Cast<Task>()
.ToArray();
if (tasks.Length > 0)
{
_ = Task.WaitAll(tasks, 2000); // 减少等待时间到2秒
}
// 关闭串口
try { gpsPort?.Close(); } catch { }
try { spectrometerPort?.Close(); } catch { }
// 断开MQTT
try
{
if (mqttClient != null)
{
mqttClient.Disconnect();
mqttClient = null;
this.Invoke(new Action(() => UpdateMqttStatus("未连接", false)));
}
}
catch { }
// 在UI线程上更新状态
this.Invoke(new Action(() =>
{
LogMessage("数据采集已停止");
}));
}
catch (Exception ex)
{
this.Invoke(new Action(() =>
{
LogMessage($"停止时发生错误: {ex.Message}");
}));
}
});
}
catch (Exception ex)
{
LogMessage($"停止时发生错误: {ex.Message}");
}
}
private void InitializeSerialPorts()
{
// GPS串口
if (comboBoxGPSPort.SelectedItem != null)
{
gpsPort = new SerialPort(comboBoxGPSPort.SelectedItem.ToString(), 115200, Parity.None, 8, StopBits.One);
gpsPort.DataReceived += GPSPort_DataReceived;
gpsPort.Open();
// 发送GPS初始化命令
gpsPort.WriteLine("bestposa com1 1");
}
// 谱仪串口
if (comboBoxSpectrometerPort.SelectedItem != null)
{
spectrometerPort = new SerialPort(comboBoxSpectrometerPort.SelectedItem.ToString(), 115200, Parity.None, 8, StopBits.One);
spectrometerPort.DataReceived += SpectrometerPort_DataReceived;
spectrometerPort.Open();
if (!spectrometerPort.IsOpen) {
LogMessage("谱仪端口打开失败");
return;
}
LogMessage("谱仪端口已打开");
// 发送谱仪开始命令
}
}
private MqttConfig? mqttConfig;
private void LoadMqttConfig()
{
try
{
string configPath = Path.Combine(Application.StartupPath, "mqtt_config.json");
LogMessage($"加载MQTT配置文件: {configPath}");
if (File.Exists(configPath))
{
string json = File.ReadAllText(configPath);
var config = JsonConvert.DeserializeObject<MqttConfig>(json);
if (config != null)
{
mqttConfig = config;
LogMessage("MQTT配置已加载");
}
else
{
LogMessage("MQTT配置文件内容无效使用默认配置");
mqttConfig = new MqttConfig
{
ProductKey = "gfcq950RDqt",
Topic = "/gfcq950RDqt/PubDevice/user/update",
DeviceName = "PubDevice",
DeviceSecret = "1031a49a4f61c29a086f79b41ed971c7",
Broker = "iot-06z00jfiubx584v.mqtt.iothub.aliyuncs.com",
Port = 1883,
UseSsl = true
};
string defaultJson = JsonConvert.SerializeObject(mqttConfig, Formatting.Indented);
File.WriteAllText(configPath, defaultJson);
LogMessage("已创建默认MQTT配置文件");
}
}
else
{
LogMessage("MQTT配置文件不存在使用默认配置");
mqttConfig = new MqttConfig
{
ProductKey = "gfcq950RDqt",
Topic = "/gfcq950RDqt/PubDevice/user/update",
DeviceName = "PubDevice",
DeviceSecret = "1031a49a4f61c29a086f79b41ed971c7",
Broker = "iot-06z00jfiubx584v.mqtt.iothub.aliyuncs.com",
Port = 1883,
UseSsl = true
};
// 保存默认配置到文件
string json = JsonConvert.SerializeObject(mqttConfig, Formatting.Indented);
File.WriteAllText(configPath, json);
LogMessage("已创建默认MQTT配置文件");
}
}
catch (Exception ex)
{
LogMessage($"加载MQTT配置失败: {ex.Message}");
mqttConfig = null;
}
}
private void InitializeMQTT()
{
try
{
if (mqttConfig == null)
{
LogMessage("MQTT配置未加载跳过MQTT连接");
UpdateMqttStatus("配置未加载", false);
return;
}
if (string.IsNullOrEmpty(mqttConfig.ProductKey) ||
string.IsNullOrEmpty(mqttConfig.DeviceName) ||
string.IsNullOrEmpty(mqttConfig.DeviceSecret))
{
LogMessage("MQTT配置项 ProductKey、DeviceName 或 DeviceSecret 为空无法连接MQTT");
UpdateMqttStatus("配置项缺失", false);
return;
}
MqttSign sign = new MqttSign();
_ = sign.calculate(mqttConfig.ProductKey, mqttConfig.DeviceName, mqttConfig.DeviceSecret);
string broker = mqttConfig.Broker!;
LogMessage($"MQTT连接到: {broker}");
mqttClient = new MqttClient(broker, mqttConfig.Port, mqttConfig.UseSsl, null, null, MqttSslProtocols.TLSv1_2);
_ = mqttClient.Connect(sign.getClientid(), sign.getUsername(), sign.getPassword());
LogMessage($"MQTT已连接到: {broker}");
UpdateMqttStatus("已连接", true);
}
catch (Exception ex)
{
LogMessage($"MQTT连接失败: {ex.Message},将继续进行数据采集和日志记录");
UpdateMqttStatus("连接失败", false);
mqttClient = null; // 确保mqttClient为null避免后续使用
}
}
private void UpdateMqttStatus(string status, bool isConnected)
{
if (this.InvokeRequired)
{
this.Invoke(new Action(() => UpdateMqttStatus(status, isConnected)));
return;
}
labelMqttStatus.Text = status;
labelMqttStatus.ForeColor = isConnected ? System.Drawing.Color.Green : System.Drawing.Color.Red;
}
private void GPSPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
if (gpsPort != null)
{
string data = gpsPort.ReadExisting();
_ = gpsBuffer.Append(data);
}
// 处理完整的行
string bufferContent = gpsBuffer.ToString();
string[] lines = bufferContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
if (bufferContent.EndsWith("\r") || bufferContent.EndsWith("\n"))
{
_ = gpsBuffer.Clear();
foreach (string line in lines)
{
ProcessGPSData(line);
}
}
else
{
_ = gpsBuffer.Clear();
for (int i = 0; i < lines.Length - 1; i++)
{
ProcessGPSData(lines[i]);
}
_ = gpsBuffer.Append(lines[lines.Length - 1]);
}
}
catch (Exception ex)
{
LogMessage($"GPS数据接收错误: {ex.Message}");
}
}
private void SpectrometerPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
string data = spectrometerPort != null ? spectrometerPort.ReadExisting() : string.Empty;
_ = spectrometerBuffer.Append(data);
// 处理缓冲区中的数据
string bufferContent = spectrometerBuffer.ToString();
// 清理无用的字符串
bufferContent = bufferContent.Replace("Please again", "").Replace("ok", "");
// 按空格分割所有数据
string[] allParts = bufferContent.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (allParts.Length < 16) {
return;
}
// 查找日期格式的数据位置
for (int i = 0; i < allParts.Length - 15; i++) // 确保后面还有15个字段
{
// 检查是否为日期格式 xxxx.xx.xx
if (System.Text.RegularExpressions.Regex.IsMatch(allParts[i], @"^\d{4}\.\d{2}\.\d{2}$"))
{
// 检查后面是否有时间格式 xx:xx:xx
if (i + 1 < allParts.Length && System.Text.RegularExpressions.Regex.IsMatch(allParts[i + 1], @"^\d{2}:\d{2}:\d{2}$"))
{
// 检查是否有足够的16个字段日期 + 时间 + 14个数值
if (i + 15 < allParts.Length)
{
// 验证后面14个字段都是数值格式
bool allValid = true;
for (int j = i + 2; j <= i + 15; j++)
{
if (!System.Text.RegularExpressions.Regex.IsMatch(allParts[j], @"^\d+\.\d{2}$"))
{
allValid = false;
break;
}
}
if (allValid)
{
// 构建完整的数据行
string[] validData = new string[16];
for (int k = 0; k < 16; k++)
{
validData[k] = allParts[i + k];
}
string completeDataLine = string.Join(" ", validData);
// 处理这条完整的数据
ProcessSpectrometerData(completeDataLine);
// 从缓冲区中移除已处理的数据
int endIndex = i + 16;
if (endIndex < allParts.Length)
{
string[] remainingParts = new string[allParts.Length - endIndex];
Array.Copy(allParts, endIndex, remainingParts, 0, remainingParts.Length);
_ = spectrometerBuffer.Clear();
_ = spectrometerBuffer.Append(string.Join(" ", remainingParts));
}
else
{
_ = spectrometerBuffer.Clear();
}
return; // 处理完一条数据后退出
}
}
}
}
}
// 如果缓冲区太大,清理一部分旧数据
if (spectrometerBuffer.Length > 10000)
{
string content = spectrometerBuffer.ToString();
_ = spectrometerBuffer.Clear();
_ = spectrometerBuffer.Append(content.Substring(content.Length / 2));
}
}
catch (Exception ex)
{
LogMessage($"谱仪数据接收错误: {ex.Message}");
}
}
private void ProcessGPSData(string data)
{
LogMessage($"GPS原始数据: {data}", false);
if (data.StartsWith("#BESTPOSA"))
{
try
{
string[] parts = data.Split(',');
// 查找WGS84标识的位置
// LogMessage($"GPS数据部分数量: {parts.Length}");
int wgs84Index = -1;
for (int i = 0; i < parts.Length; i++)
{
// LogMessage($"GPS数据部分: {parts[i]}");
if (parts[i].Contains("WGS84"))
{
wgs84Index = i;
break;
}
}
// LogMessage($"WGS84索引: {wgs84Index}");
// WGS84后面的两个字段是经纬度
if (wgs84Index >= 0 && parts.Length > wgs84Index + 2)
{
double lat = double.Parse(parts[wgs84Index + 1]);
double lng = double.Parse(parts[wgs84Index + 2]);
GPSData gpsData = new GPSData
{
Latitude = lat,
Longitude = lng,
Timestamp = DateTime.Now
};
gpsDataQueue.Enqueue(gpsData);
LogMessage($"GPS数据: 纬度={lat:F8}, 经度={lng:F8}", false);
}
}
catch (Exception ex)
{
LogMessage($"GPS数据解析错误: {ex.Message}");
}
}
}
private void ProcessSpectrometerData(string data)
{
LogMessage($"谱仪完整原始数据: {data}", false);
if (data.Equals("ok")) {
LogMessage("谱仪已准备好");
return;
}
try
{
string[] parts = data.Split(' ');
// 检查是否为完整的反馈格式16个字段日期 时间 + 14个数值
if (parts.Length == 16)
{
// 验证日期格式 xxxx.xx.xx
if (!System.Text.RegularExpressions.Regex.IsMatch(parts[0], @"^\d{4}\.\d{2}\.\d{2}$"))
return;
// 验证时间格式 xx:xx:xx
if (!System.Text.RegularExpressions.Regex.IsMatch(parts[1], @"^\d{2}:\d{2}:\d{2}$"))
return;
// 解析时间戳 (xxxx.xx.xx xx:xx:xx)
// string dateStr = parts[0];
// string timeStr = parts[1];
SpectrometerData spectData = new SpectrometerData
{
Timestamp = DateTime.Now, // 使用当前时间,也可以解析返回的时间
RealTime = double.Parse(parts[2]), // aaaaaaa.aa 实时间
DeadTime = double.Parse(parts[3]), // bbbbbbb.bb 死时间
DoseRate = double.Parse(parts[14]), // JJJJJJ.JJ 环境总辐射剂量率值第15个字段
TotalCountRate = double.Parse(parts[15]) // cccccccccc.cc γ谱的全谱计数率第16个字段
};
spectrometerDataQueue.Enqueue(spectData);
LogMessage($"谱仪数据: 时间={spectData.Timestamp}, 剂量率={spectData.DoseRate:F2} nSv/h, 全谱计数率={spectData.TotalCountRate:F2} cps");
}
}
catch (Exception ex)
{
LogMessage($"谱仪数据解析错误: {ex.Message}, 数据: {data}");
}
}
private async void GPSDataReader(CancellationToken token)
{
while (!token.IsCancellationRequested && isRunning)
{
await Task.Delay(1000, token);
}
}
private async void SpectrometerDataReader(CancellationToken token)
{
while (!token.IsCancellationRequested && isRunning)
{
try
{
LogMessage("尝试读取谱仪数据");
if (spectrometerPort != null && spectrometerPort.IsOpen)
{
spectrometerPort.DiscardInBuffer();
spectrometerPort.Write("$clear\r\n");
await Task.Delay(50, token); // 等待刷新完成
int interval = (int)(numericUpDownRefreshInterval.Value * 1000);
spectrometerPort.Write("$start\r\n");
// spectrometerPort.ReadTimeout = 1500;
await Task.Delay(interval, token); // 等待刷新完成
// // 先发送刷新指令
// spectrometerPort.Write("$refresh\r\n");
// await Task.Delay(500, token); // 等待刷新完成
spectrometerPort.Write("$getSperesult\r\n");
await Task.Delay(50, token); // 等待刷新完成
}
await Task.Delay(50, token);
}
catch (Exception ex)
{
LogMessage($"谱仪数据读取错误: {ex.Message}");
await Task.Delay(5000, token);
}
}
}
private async void MQTTDataSender(CancellationToken token)
{
// 在数据发送线程中初始化MQTT连接
// InitializeMQTT();
while (!token.IsCancellationRequested && isRunning)
{
try
{
if (spectrometerDataQueue.TryDequeue(out SpectrometerData? spectData) && spectData != null)
{
// 查找最近的GPS数据
GPSData? nearestGPS = FindNearestGPSData(spectData.Timestamp);
if (nearestGPS != null)
{
var mqttData = new
{
OptTime = spectData.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"),
PointLat = nearestGPS.Latitude.ToString("F8"),
PointLng = nearestGPS.Longitude.ToString("F8"),
Value = spectData.DoseRate / 1000000000.0 // 转换为合适的单位
};
string json = JsonConvert.SerializeObject(mqttData);
string? topic = mqttConfig?.Topic;
if (mqttClient != null && mqttClient.IsConnected)
{
_ = mqttClient.Publish(topic, Encoding.UTF8.GetBytes(json));
LogMessage($"MQTT发送: {json}");
}
else
{
LogMessage($"MQTT未连接数据记录: {json}");
}
}
}
await Task.Delay(100, token);
}
catch (Exception ex)
{
LogMessage($"MQTT发送错误: {ex.Message}");
await Task.Delay(5000, token);
}
}
}
private GPSData? FindNearestGPSData(DateTime targetTime)
{
GPSData? nearest = null;
TimeSpan minDiff = TimeSpan.MaxValue;
var gpsDataList = gpsDataQueue.ToArray();
foreach (var gpsData in gpsDataList)
{
TimeSpan diff = Math.Abs((gpsData.Timestamp - targetTime).Ticks) == (gpsData.Timestamp - targetTime).Ticks ?
gpsData.Timestamp - targetTime : targetTime - gpsData.Timestamp;
if (diff < minDiff)
{
minDiff = diff;
nearest = gpsData;
}
}
return nearest;
}
private async void LogWriter(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
if (logQueue.TryDequeue(out string? logMessage) && logMessage != null && !string.IsNullOrEmpty(logFileName))
{
await File.AppendAllTextAsync(logFileName, logMessage + Environment.NewLine, token);
}
await Task.Delay(100, token);
}
catch (Exception ex)
{
// 避免日志写入错误导致的无限循环
Console.WriteLine($"日志写入错误: {ex.Message}");
await Task.Delay(1000, token);
}
}
}
private void LogMessage(string message, bool screenOutput = true)
{
// 处理日志消息
message = message.Replace("\r", "RR");
message = message.Replace("\n", "NN");
string logEntry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}";
// 添加到日志队列
logQueue.Enqueue(logEntry);
if (screenOutput)
{
// 更新UI
if (textBoxLog.InvokeRequired)
{
textBoxLog.Invoke(new Action(() =>
{
textBoxLog.AppendText(logEntry + Environment.NewLine);
textBoxLog.ScrollToCaret();
}));
}
else
{
textBoxLog.AppendText(logEntry + Environment.NewLine);
textBoxLog.ScrollToCaret();
}
}
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (isRunning)
{
StopDataCollection();
}
base.OnFormClosing(e);
}
}
public class GPSData
{
public double Latitude { get; set; }
public double Longitude { get; set; }
public DateTime Timestamp { get; set; }
}
public class SpectrometerData
{
public DateTime Timestamp { get; set; }
public double RealTime { get; set; }
public double DeadTime { get; set; }
public double DoseRate { get; set; }
public double TotalCountRate { get; set; }
}
}