init commit
This commit is contained in:
@@ -0,0 +1,547 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StandardScene.Utils;
|
||||
using SimpleCore;
|
||||
using SimpleCore.Library;
|
||||
|
||||
namespace StandardScene.ExtendDevice.ButtonBox
|
||||
{
|
||||
/// <summary>
|
||||
/// Azowie 呼叫器按钮盒实现
|
||||
/// 基于 Modbus TCP 协议
|
||||
/// </summary>
|
||||
public class AzowieButtonBox : BasicButtonBox
|
||||
{
|
||||
/// <summary>
|
||||
/// Modbus TCP 客户端
|
||||
/// </summary>
|
||||
private ModbusRtu _modbusClient;
|
||||
|
||||
/// <summary>
|
||||
/// 同步锁
|
||||
/// </summary>
|
||||
private readonly object _syncLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 是否已启动
|
||||
/// </summary>
|
||||
private bool _isStarted = false;
|
||||
|
||||
/// <summary>
|
||||
/// 定时读取任务取消令牌
|
||||
/// </summary>
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// 定时读取任务
|
||||
/// </summary>
|
||||
private Task _readTask;
|
||||
|
||||
/// <summary>
|
||||
/// 读取间隔(毫秒),默认500ms
|
||||
/// </summary>
|
||||
public int ReadInterval { get; set; } = 500;
|
||||
|
||||
/// <summary>
|
||||
/// 重连间隔(毫秒),默认3000ms,避免过于频繁的重连
|
||||
/// </summary>
|
||||
public int ReconnectInterval { get; set; } = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// 上次重连尝试时间
|
||||
/// </summary>
|
||||
private DateTime _lastReconnectAttempt = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// Modbus 从站地址,默认1
|
||||
/// </summary>
|
||||
public byte SlaveAddress { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 呼叫器编号(只读)
|
||||
/// </summary>
|
||||
public int DeviceId { get; private set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 电池电量(0-100)
|
||||
/// </summary>
|
||||
public int BatteryLevel { get; private set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 按钮灯状态字典
|
||||
/// </summary>
|
||||
public Dictionary<int, ButtonLightState> ButtonLightStates { get; private set; } = new Dictionary<int, ButtonLightState>();
|
||||
|
||||
/// <summary>
|
||||
/// 按钮灯状态枚举
|
||||
/// </summary>
|
||||
public enum ButtonLightState
|
||||
{
|
||||
/// <summary>
|
||||
/// 常灭
|
||||
/// </summary>
|
||||
Off = 0,
|
||||
/// <summary>
|
||||
/// 常亮
|
||||
/// </summary>
|
||||
On = 1,
|
||||
/// <summary>
|
||||
/// 快闪,间隔0.5秒
|
||||
/// </summary>
|
||||
FastBlink = 2,
|
||||
/// <summary>
|
||||
/// 慢闪,间隔2秒
|
||||
/// </summary>
|
||||
SlowBlink = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接按钮盒
|
||||
/// </summary>
|
||||
public override void Connect()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_isStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
UpdateState(ButtonBoxState.Connecting);
|
||||
|
||||
// 根据配置的按钮初始化按钮状态
|
||||
var buttonIndices = ButtonConfigs.Keys.OrderBy(k => k).ToList();
|
||||
if (buttonIndices.Count == 0)
|
||||
{
|
||||
// 如果没有配置,默认初始化按钮1-8
|
||||
buttonIndices = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8 };
|
||||
}
|
||||
InitializeButtons(buttonIndices);
|
||||
|
||||
// 初始化按钮灯状态
|
||||
foreach (var index in buttonIndices)
|
||||
{
|
||||
ButtonLightStates[index] = ButtonLightState.Off;
|
||||
}
|
||||
|
||||
// 尝试连接 Modbus TCP 客户端
|
||||
try
|
||||
{
|
||||
_modbusClient = new ModbusRtu();
|
||||
_modbusClient.StartTcpRtu(Ip, Port);
|
||||
UpdateState(ButtonBoxState.Online);
|
||||
}
|
||||
catch (Exception connectEx)
|
||||
{
|
||||
// 初次连接失败,但不阻止启动读取循环,循环中会持续重连
|
||||
UpdateState(ButtonBoxState.Connecting);
|
||||
Diagnosis.Log($"AzowieButtonBox[{Index}] 初次连接失败,将在后台持续重连: {ExceptionFormatter.FormatEx(connectEx)}", "AzowieButtonBox", true);
|
||||
}
|
||||
|
||||
// 启动定时读取任务(即使连接失败也会启动,循环中会持续重连)
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_readTask = Task.Run(() => ReadButtonStatesLoop(_cancellationTokenSource.Token));
|
||||
|
||||
_isStarted = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateState(ButtonBoxState.Error, $"初始化失败: {ex.Message}");
|
||||
_isStarted = false;
|
||||
Diagnosis.Log($"AzowieButtonBox[{Index}] 初始化失败: {ExceptionFormatter.FormatEx(ex)}", "AzowieButtonBox", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断开连接
|
||||
/// </summary>
|
||||
public override void Disconnect()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_isStarted || _modbusClient == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 停止读取任务
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_readTask?.Wait(1000);
|
||||
|
||||
// 关闭 Modbus 连接
|
||||
_modbusClient?.Close();
|
||||
_modbusClient = null;
|
||||
|
||||
_isStarted = false;
|
||||
UpdateState(ButtonBoxState.Offline);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateState(ButtonBoxState.Error, $"断开连接失败: {ex.Message}");
|
||||
Diagnosis.Log($"AzowieButtonBox[{Index}] 断开连接失败: {ExceptionFormatter.FormatEx(ex)}", "AzowieButtonBox", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定时读取按钮状态循环
|
||||
/// </summary>
|
||||
private void ReadButtonStatesLoop(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_isStarted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查连接状态,如果未连接或连接断开,尝试重连
|
||||
bool isConnected = _modbusClient?.modbusRtu?.Connected ?? false;
|
||||
if (_modbusClient == null || !isConnected)
|
||||
{
|
||||
UpdateState(ButtonBoxState.Connecting);
|
||||
|
||||
// 控制重连频率,避免过于频繁的重连导致资源浪费
|
||||
var timeSinceLastReconnect = (DateTime.Now - _lastReconnectAttempt).TotalMilliseconds;
|
||||
if (timeSinceLastReconnect >= ReconnectInterval)
|
||||
{
|
||||
_lastReconnectAttempt = DateTime.Now;
|
||||
TryReconnect();
|
||||
}
|
||||
|
||||
// 如果重连后仍然未连接,等待后继续下一次循环
|
||||
isConnected = _modbusClient?.modbusRtu?.Connected ?? false;
|
||||
if (_modbusClient == null || !isConnected)
|
||||
{
|
||||
Thread.Sleep(ReadInterval);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取按钮状态(寄存器 01-04 对应按钮1-4,21-24 对应按钮5-8)
|
||||
ReadButtonStates();
|
||||
|
||||
// 读取设备信息(每5秒读取一次)
|
||||
//if (DateTime.Now.Second % 5 == 0)
|
||||
//{
|
||||
// ReadDeviceInfo();
|
||||
//}
|
||||
|
||||
// 更新在线状态
|
||||
UpdateState(ButtonBoxState.Online);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"AzowieButtonBox[{Index}] 读取状态失败: {ExceptionFormatter.FormatEx(ex)}", "AzowieButtonBox", true);
|
||||
UpdateState(ButtonBoxState.Error, $"读取状态失败: {ex.Message}");
|
||||
|
||||
// 如果连接失败,尝试重连
|
||||
TryReconnect();
|
||||
}
|
||||
|
||||
// 等待指定间隔
|
||||
Thread.Sleep(ReadInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试重连 Modbus TCP 连接
|
||||
/// </summary>
|
||||
private void TryReconnect()
|
||||
{
|
||||
ModbusRtu oldClient = null;
|
||||
try
|
||||
{
|
||||
// 安全关闭旧连接
|
||||
if (_modbusClient != null)
|
||||
{
|
||||
oldClient = _modbusClient;
|
||||
_modbusClient = null; // 先置空,避免并发访问
|
||||
|
||||
try
|
||||
{
|
||||
oldClient.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略关闭时的异常,继续创建新连接
|
||||
}
|
||||
finally
|
||||
{
|
||||
oldClient = null; // 确保引用释放
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新连接
|
||||
_modbusClient = new ModbusRtu();
|
||||
_modbusClient.StartTcpRtu(Ip, Port);
|
||||
|
||||
Diagnosis.Log($"AzowieButtonBox[{Index}] 重连成功", "AzowieButtonBox", false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 重连失败,确保资源释放
|
||||
if (_modbusClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_modbusClient.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略关闭异常
|
||||
}
|
||||
_modbusClient = null;
|
||||
}
|
||||
|
||||
// 记录日志但不抛出异常,等待下次循环继续尝试
|
||||
Diagnosis.Log($"AzowieButtonBox[{Index}] 重连失败: {ExceptionFormatter.FormatEx(ex)}", "AzowieButtonBox", false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取按钮状态
|
||||
/// 根据配置的按钮数量和编号读取对应的寄存器
|
||||
/// </summary>
|
||||
private void ReadButtonStates()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取配置的按钮索引列表
|
||||
var configuredButtons = ButtonConfigs.Keys.OrderBy(k => k).ToList();
|
||||
if (configuredButtons.Count == 0)
|
||||
{
|
||||
// 如果没有配置,默认读取按钮1-8
|
||||
configuredButtons = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8 };
|
||||
}
|
||||
|
||||
// 将按钮分为两组:按钮1-4(寄存器01-04)和按钮5-8(寄存器21-24)
|
||||
var buttons1_4 = configuredButtons.Where(b => b >= 1 && b <= 4).OrderBy(b => b).ToList();
|
||||
var buttons5_8 = configuredButtons.Where(b => b >= 5 && b <= 8).OrderBy(b => b).ToList();
|
||||
|
||||
// 读取按钮1-4的状态(寄存器地址 01-04)
|
||||
// 按钮索引直接对应寄存器地址:按钮1->寄存器01,按钮2->寄存器02,按钮3->寄存器03,按钮4->寄存器04
|
||||
if (buttons1_4.Count > 0)
|
||||
{
|
||||
// 计算需要读取的寄存器范围(从最小按钮索引到最大按钮索引)
|
||||
var minButton = buttons1_4.Min();
|
||||
var maxButton = buttons1_4.Max();
|
||||
var startReg = (ushort)minButton; // 按钮索引直接对应寄存器地址
|
||||
var count = maxButton - minButton + 1;
|
||||
|
||||
var buttonStates1_4 = _modbusClient.ReadRegisterBuffer_03(SlaveAddress, startReg, (ushort)count);
|
||||
|
||||
// 将读取结果映射到对应的按钮索引
|
||||
foreach (var buttonIndex in buttons1_4)
|
||||
{
|
||||
// 计算该按钮在读取结果数组中的位置
|
||||
var arrayIndex = buttonIndex - minButton;
|
||||
if (arrayIndex >= 0 && arrayIndex < buttonStates1_4.Length)
|
||||
{
|
||||
var state = buttonStates1_4[arrayIndex];
|
||||
UpdateButtonState(buttonIndex, state == 1 ? ButtonState.Pressed : ButtonState.Released);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 读取按钮5-8的状态(寄存器地址 21-24)
|
||||
// 按钮索引对应寄存器地址:按钮5->寄存器21,按钮6->寄存器22,按钮7->寄存器23,按钮8->寄存器24
|
||||
// 寄存器地址 = 20 + 按钮索引
|
||||
if (buttons5_8.Count > 0)
|
||||
{
|
||||
var minButton = buttons5_8.Min();
|
||||
var maxButton = buttons5_8.Max();
|
||||
var startReg = (ushort)(20 + minButton); // 按钮5对应寄存器21
|
||||
var count = maxButton - minButton + 1;
|
||||
|
||||
var buttonStates5_8 = _modbusClient.ReadRegisterBuffer_03(SlaveAddress, startReg, (ushort)count);
|
||||
|
||||
// 将读取结果映射到对应的按钮索引
|
||||
foreach (var buttonIndex in buttons5_8)
|
||||
{
|
||||
// 计算该按钮在读取结果数组中的位置
|
||||
var arrayIndex = buttonIndex - minButton;
|
||||
if (arrayIndex >= 0 && arrayIndex < buttonStates5_8.Length)
|
||||
{
|
||||
var state = buttonStates5_8[arrayIndex];
|
||||
UpdateButtonState(buttonIndex, state == 1 ? ButtonState.Pressed : ButtonState.Released);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"读取按钮状态失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新按钮配置信息
|
||||
/// 当配置变化时,同步更新按钮状态和按钮灯状态字典
|
||||
/// </summary>
|
||||
/// <param name="buttonConfigs">按钮配置列表</param>
|
||||
public override void UpdateButtonConfigs(List<ButtonModel> buttonConfigs)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
// 保存旧的按钮索引列表
|
||||
var oldButtonIndices = new HashSet<int>(ButtonConfigs.Keys);
|
||||
|
||||
// 调用基类方法更新配置
|
||||
base.UpdateButtonConfigs(buttonConfigs);
|
||||
|
||||
// 获取新的按钮索引列表
|
||||
var newButtonIndices = new HashSet<int>(ButtonConfigs.Keys);
|
||||
|
||||
// 如果配置发生变化,更新按钮状态和按钮灯状态字典
|
||||
if (!oldButtonIndices.SetEquals(newButtonIndices))
|
||||
{
|
||||
// 移除已删除的按钮状态
|
||||
var toRemove = oldButtonIndices.Where(k => !newButtonIndices.Contains(k)).ToList();
|
||||
foreach (var index in toRemove)
|
||||
{
|
||||
ButtonStates.Remove(index);
|
||||
ButtonLightStates.Remove(index);
|
||||
}
|
||||
|
||||
// 添加新按钮的状态(初始化为未按下和常灭)
|
||||
var toAdd = newButtonIndices.Where(k => !oldButtonIndices.Contains(k)).ToList();
|
||||
foreach (var index in toAdd)
|
||||
{
|
||||
if (!ButtonStates.ContainsKey(index))
|
||||
{
|
||||
ButtonStates[index] = ButtonState.Released;
|
||||
}
|
||||
if (!ButtonLightStates.ContainsKey(index))
|
||||
{
|
||||
ButtonLightStates[index] = ButtonLightState.Off;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有配置,默认初始化按钮1-8
|
||||
if (newButtonIndices.Count == 0)
|
||||
{
|
||||
var defaultButtons = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8 };
|
||||
InitializeButtons(defaultButtons);
|
||||
foreach (var index in defaultButtons)
|
||||
{
|
||||
if (!ButtonLightStates.ContainsKey(index))
|
||||
{
|
||||
ButtonLightStates[index] = ButtonLightState.Off;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Diagnosis.Log($"AzowieButtonBox[{Index}] 按钮配置已更新: 旧配置{oldButtonIndices.Count}个按钮, 新配置{newButtonIndices.Count}个按钮", "AzowieButtonBox", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清零指定按钮的寄存器状态
|
||||
/// </summary>
|
||||
/// <param name="buttonIndex">按钮索引(1-8)</param>
|
||||
public override void ClearButtonRegister(int buttonIndex)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_isStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonIndex < 1 || buttonIndex > 8)
|
||||
{
|
||||
Diagnosis.Log($"AzowieButtonBox[{Index}] 按钮索引超出范围: {buttonIndex}", "AzowieButtonBox", true);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 检查连接状态,如果未连接则尝试重连
|
||||
bool isConnected = _modbusClient?.modbusRtu?.Connected ?? false;
|
||||
if (!isConnected)
|
||||
{
|
||||
// 尝试重连
|
||||
TryReconnect();
|
||||
isConnected = _modbusClient?.modbusRtu?.Connected ?? false;
|
||||
|
||||
// 如果重连失败,记录日志并返回
|
||||
if (!isConnected)
|
||||
{
|
||||
Diagnosis.Log($"AzowieButtonBox[{Index}] 清零按钮{buttonIndex}寄存器失败: 连接未建立", "AzowieButtonBox", true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据按钮索引确定寄存器地址
|
||||
// 按钮1-4对应寄存器01-04,按钮5-8对应寄存器21-24
|
||||
ushort buttonRegisterAddress;
|
||||
if (buttonIndex >= 1 && buttonIndex <= 4)
|
||||
{
|
||||
buttonRegisterAddress = (ushort)buttonIndex; // 按钮索引直接对应寄存器地址
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonRegisterAddress = (ushort)(20 + buttonIndex); // 按钮5-8对应寄存器21-24
|
||||
}
|
||||
|
||||
// 清零按钮状态寄存器
|
||||
_modbusClient.WriteSingleRegister_06(SlaveAddress, buttonRegisterAddress, 0);
|
||||
|
||||
// 清零对应的按钮灯状态寄存器
|
||||
// 按钮灯1-4对应寄存器05-08(按钮索引 + 4),按钮灯5-8对应寄存器25-28(按钮索引 + 20)
|
||||
ushort lightRegisterAddress;
|
||||
if (buttonIndex is >= 1 and <= 4)
|
||||
{
|
||||
lightRegisterAddress = (ushort)(4 + buttonIndex); // 按钮灯寄存器 = 按钮索引 + 4
|
||||
}
|
||||
else
|
||||
{
|
||||
lightRegisterAddress = (ushort)(20 + buttonIndex); // 按钮灯寄存器 = 按钮索引 + 20
|
||||
}
|
||||
|
||||
_modbusClient.WriteSingleRegister_06(SlaveAddress, lightRegisterAddress, 0);
|
||||
|
||||
// 更新本地按钮灯状态
|
||||
if (ButtonLightStates.ContainsKey(buttonIndex))
|
||||
{
|
||||
ButtonLightStates[buttonIndex] = ButtonLightState.Off;
|
||||
}
|
||||
|
||||
Diagnosis.Log($"AzowieButtonBox[{Index}] 清零按钮{buttonIndex}状态和灯光寄存器成功", "AzowieButtonBox", false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"AzowieButtonBox[{Index}] 清零按钮{buttonIndex}寄存器失败: {ExceptionFormatter.FormatEx(ex)}", "AzowieButtonBox", true);
|
||||
|
||||
// 如果是因为连接问题导致的异常,尝试重连
|
||||
bool isConnected = _modbusClient?.modbusRtu?.Connected ?? false;
|
||||
if (!isConnected)
|
||||
{
|
||||
TryReconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 析构函数,确保资源释放
|
||||
/// </summary>
|
||||
~AzowieButtonBox()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using leegiot;
|
||||
using SimpleCore;
|
||||
using SimpleCore.Library;
|
||||
|
||||
namespace StandardScene.ExtendDevice.ButtonBox
|
||||
{
|
||||
public class LeegButtonBox : BasicButtonBox
|
||||
{
|
||||
/// <summary>
|
||||
/// LeegKey设备实例
|
||||
/// </summary>
|
||||
private LeegKeyDevice _device;
|
||||
|
||||
/// <summary>
|
||||
/// 同步锁
|
||||
/// </summary>
|
||||
private readonly object _syncLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 是否已启动
|
||||
/// </summary>
|
||||
private bool _isStarted = false;
|
||||
|
||||
/// <summary>
|
||||
/// 连接按钮盒
|
||||
/// </summary>
|
||||
public override void Connect()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_isStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
UpdateState(ButtonBoxState.Connecting);
|
||||
|
||||
// 创建LeegKey设备实例
|
||||
_device = new LeegKeyDevice(Ip, Port);
|
||||
|
||||
// 设置事件回调,使用Index作为userData
|
||||
_device.setEventCallback(OnLeegKeyEvent, Index);
|
||||
_device.autoHeartbeatEnable(2,5);
|
||||
// 启动设备
|
||||
_device.start(true);
|
||||
|
||||
_isStarted = true;
|
||||
UpdateState(ButtonBoxState.Online);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateState(ButtonBoxState.Error, $"连接失败: {ex.Message}");
|
||||
_isStarted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断开连接
|
||||
/// </summary>
|
||||
public override void Disconnect()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_isStarted || _device == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_device.stop();
|
||||
_device = null;
|
||||
_isStarted = false;
|
||||
UpdateState(ButtonBoxState.Offline);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateState(ButtonBoxState.Error, $"断开连接失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LeegKey事件回调
|
||||
/// </summary>
|
||||
private void OnLeegKeyEvent(LeegKeyEvent evt, object msg, object userData)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case LeegKeyEvent.KEY_HIT:
|
||||
HandleKeyHit((LeegKeyMsgKey)msg);
|
||||
break;
|
||||
|
||||
case LeegKeyEvent.STATUS_REP:
|
||||
HandleStatusReport((LeegKeyMsgStatus)msg);
|
||||
break;
|
||||
|
||||
case LeegKeyEvent.TIME:
|
||||
// 时间同步事件,可以用于保持连接状态
|
||||
UpdateState(ButtonBoxState.Online);
|
||||
break;
|
||||
|
||||
case LeegKeyEvent.LOG:
|
||||
// 日志事件,可以用于调试
|
||||
HandleLogEvent((LeegKeyMsgLog)msg);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateState(ButtonBoxState.Error, $"处理事件失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理按键按下事件
|
||||
/// </summary>
|
||||
private void HandleKeyHit(LeegKeyMsgKey msg)
|
||||
{
|
||||
var keyTag= msg.content.keys[0].First();
|
||||
var buttonIndexStr = keyTag.Key.Substring(3);
|
||||
if(!int.TryParse(buttonIndexStr,out var buttonIndex))
|
||||
return;
|
||||
switch (keyTag.Value)
|
||||
{
|
||||
case "up":
|
||||
UpdateButtonState(buttonIndex,ButtonState.Released);
|
||||
break;
|
||||
case "down":
|
||||
UpdateButtonState(buttonIndex,ButtonState.Pressed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理状态报告事件
|
||||
/// </summary>
|
||||
private void HandleStatusReport(LeegKeyMsgStatus msg)
|
||||
{
|
||||
// 状态报告表示设备在线
|
||||
UpdateState(ButtonBoxState.Online);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理日志事件
|
||||
/// </summary>
|
||||
private void HandleLogEvent(LeegKeyMsgLog msg)
|
||||
{
|
||||
// 可以根据日志内容更新状态
|
||||
// 这里可以根据实际需求实现
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 设置按钮灯光
|
||||
/// </summary>
|
||||
/// <param name="buttonIndex">按钮索引</param>
|
||||
/// <param name="rgb">RGB颜色值</param>
|
||||
public void SetButtonLight(int buttonIndex, Rgb rgb)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_isStarted || _device == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_device.lightSet(rgb, buttonIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"设置按钮灯光失败: {ExceptionFormatter.FormatEx(ex)}", "LeegButtonBox", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 控制IO输出
|
||||
/// </summary>
|
||||
/// <param name="content">IO控制内容</param>
|
||||
public void ControlIO(IoctrlContent content)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_isStarted || _device == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_device.ioctrl(content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"控制IO失败: {ExceptionFormatter.FormatEx(ex)}", "LeegButtonBox", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 析构函数,确保资源释放
|
||||
/// </summary>
|
||||
~LeegButtonBox()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
using SimpleLite.RCS;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using SimpleCore;
|
||||
using SimpleCore.Library;
|
||||
using SimpleCore.PropType;
|
||||
using StandardScene.Charge;
|
||||
using StandardScene.TCP;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StandardScene.ChargeStationType
|
||||
{
|
||||
public class FLChargeStation : AbstractChargeStation
|
||||
{
|
||||
private AsyncTcpClient Client;
|
||||
private IPEndPoint _endPoint;
|
||||
|
||||
// 重连相关
|
||||
private Timer _reconnectTimer;
|
||||
private bool _isManualDisconnect = false; // 标识是否为手动断开
|
||||
private int _reconnectAttempts = 0; // 重连尝试次数
|
||||
private const int MAX_RECONNECT_ATTEMPTS = 5; // 最大重连次数
|
||||
private const int RECONNECT_INTERVAL_MS = 3000; // 重连间隔(毫秒)
|
||||
private readonly object _connectionLock = new object(); // 连接锁
|
||||
private bool _isConnecting = false; // 是否正在连接中
|
||||
|
||||
// 连接状态
|
||||
public bool IsConnected { get; private set; } = false;
|
||||
public DateTime? LastConnectedTime { get; private set; }
|
||||
public DateTime? LastDisconnectedTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关闭当前TCP连接
|
||||
/// </summary>
|
||||
public override void CloseCommunication()
|
||||
{
|
||||
lock (_connectionLock)
|
||||
{
|
||||
_isManualDisconnect = true; // 标记为手动断开,不触发自动重连
|
||||
|
||||
// 停止重连定时器
|
||||
if (_reconnectTimer != null)
|
||||
{
|
||||
_reconnectTimer.Dispose();
|
||||
_reconnectTimer = null;
|
||||
}
|
||||
_isConnecting = false;
|
||||
if (Client != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 取消订阅事件,避免内存泄漏
|
||||
Client.PlaintextReceived -= OnPlaintextReceived;
|
||||
Client.ServerConnected -= OnServerConnected;
|
||||
Client.ServerDisconnected -= OnServerDisconnected;
|
||||
|
||||
// 关闭连接
|
||||
Client.Close();
|
||||
Client = null;
|
||||
|
||||
IsConnected = false;
|
||||
LastDisconnectedTime = DateTime.Now;
|
||||
|
||||
Diagnosis.Log($"FLChargeStation[{SiteId}] TCP connection closed manually");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"ERR:FLChargeStation[{SiteId}] failed to close connection: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建TCP连接
|
||||
/// </summary>
|
||||
public override void CreateCommunication(IPAddress ip, int port)
|
||||
{
|
||||
lock (_connectionLock)
|
||||
{
|
||||
// 先关闭旧连接
|
||||
if (Client != null)
|
||||
{
|
||||
CloseCommunication();
|
||||
}
|
||||
|
||||
_endPoint = new IPEndPoint(ip, port);
|
||||
_isManualDisconnect = false; // 重置手动断开标记
|
||||
_reconnectAttempts = 0; // 重置重连次数
|
||||
|
||||
ConnectInternal();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内部连接方法
|
||||
/// </summary>
|
||||
private void ConnectInternal()
|
||||
{
|
||||
if (_isConnecting)
|
||||
{
|
||||
Diagnosis.Log($"FLChargeStation[{SiteId}] is already connecting, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_isConnecting = true;
|
||||
|
||||
// 创建新的TCP客户端
|
||||
Client = new AsyncTcpClient(_endPoint.Address, _endPoint.Port);
|
||||
Client.PlaintextReceived += OnPlaintextReceived;
|
||||
Client.ServerConnected += OnServerConnected;
|
||||
Client.ServerDisconnected += OnServerDisconnected;
|
||||
|
||||
// 连接
|
||||
Client.Connect();
|
||||
|
||||
if (Client.Connected)
|
||||
{
|
||||
Diagnosis.Log($"FLChargeStation[{SiteId}] connecting to {_endPoint.Address}:{_endPoint.Port}...");
|
||||
}
|
||||
else
|
||||
{
|
||||
_isConnecting = false;
|
||||
Diagnosis.Log($"FLChargeStation[{SiteId}] connecting to {_endPoint.Address}:{_endPoint.Port}... 未建立连接");
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"ERR:FLChargeStation[{SiteId}] failed to create connection: {ex.Message}");
|
||||
_isConnecting = false;
|
||||
|
||||
// 触发重连
|
||||
if (!_isManualDisconnect)
|
||||
{
|
||||
ScheduleReconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动触发重连
|
||||
/// </summary>
|
||||
public void Reconnect()
|
||||
{
|
||||
lock (_connectionLock)
|
||||
{
|
||||
Diagnosis.Log($"FLChargeStation[{SiteId}] manual reconnect triggered");
|
||||
|
||||
_isManualDisconnect = false;
|
||||
_reconnectAttempts = 0;
|
||||
|
||||
// 关闭现有连接
|
||||
if (Client != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Client.PlaintextReceived -= OnPlaintextReceived;
|
||||
Client.ServerConnected -= OnServerConnected;
|
||||
Client.ServerDisconnected -= OnServerDisconnected;
|
||||
Client.Close();
|
||||
Client = null;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// 重新连接
|
||||
ConnectInternal();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安排自动重连
|
||||
/// </summary>
|
||||
private void ScheduleReconnect()
|
||||
{
|
||||
if (_isManualDisconnect)
|
||||
{
|
||||
Diagnosis.Log($"FLChargeStation[{SiteId}] manual disconnect, skip auto reconnect");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_reconnectAttempts >= MAX_RECONNECT_ATTEMPTS)
|
||||
{
|
||||
Diagnosis.Log($"ERR:FLChargeStation[{SiteId}] max reconnect attempts ({MAX_RECONNECT_ATTEMPTS}) reached, stop reconnecting");
|
||||
return;
|
||||
}
|
||||
|
||||
_reconnectAttempts++;
|
||||
|
||||
// 停止现有定时器
|
||||
if (_reconnectTimer != null)
|
||||
{
|
||||
_reconnectTimer.Dispose();
|
||||
}
|
||||
|
||||
Diagnosis.Log($"FLChargeStation[{SiteId}] scheduling reconnect attempt {_reconnectAttempts}/{MAX_RECONNECT_ATTEMPTS} in {RECONNECT_INTERVAL_MS}ms");
|
||||
|
||||
// 创建新的定时器
|
||||
_reconnectTimer = new Timer(
|
||||
callback: _ => AttemptReconnect(),
|
||||
state: null,
|
||||
dueTime: RECONNECT_INTERVAL_MS,
|
||||
period: Timeout.Infinite
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试重连
|
||||
/// </summary>
|
||||
private void AttemptReconnect()
|
||||
{
|
||||
lock (_connectionLock)
|
||||
{
|
||||
if (_isManualDisconnect || IsConnected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Diagnosis.Log($"FLChargeStation[{SiteId}] attempting to reconnect (attempt {_reconnectAttempts}/{MAX_RECONNECT_ATTEMPTS})...");
|
||||
|
||||
try
|
||||
{
|
||||
// 清理旧客户端
|
||||
if (Client != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Client.PlaintextReceived -= OnPlaintextReceived;
|
||||
Client.ServerConnected -= OnServerConnected;
|
||||
Client.ServerDisconnected -= OnServerDisconnected;
|
||||
Client.Close();
|
||||
}
|
||||
catch { }
|
||||
Client = null;
|
||||
}
|
||||
|
||||
// 重新连接
|
||||
ConnectInternal();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"ERR:FLChargeStation[{SiteId}] reconnect failed: {ex.Message}");
|
||||
|
||||
// 继续安排下一次重连
|
||||
if (!_isManualDisconnect && _reconnectAttempts < MAX_RECONNECT_ATTEMPTS)
|
||||
{
|
||||
ScheduleReconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlaintextReceived(object sender, TcpDatagramReceivedEventArgs<byte[]> e)
|
||||
{
|
||||
var messageService = CommunicationMessageService.Instance;
|
||||
var recBytes = e.Datagram.Take(35).ToArray();
|
||||
string IP = ((AsyncTcpClient)sender).RemoteIPEndPoint.Address.ToString();
|
||||
Diagnosis.Post($"{string.Join(" ", recBytes.Select(p => $"{p:X2}"))}", $"{IP}", true);
|
||||
IsSafe = recBytes[28] == 2;
|
||||
messageService.AddReceiveMessage(_endPoint.Address.ToString(), _endPoint.Port, BitConverter.ToString(recBytes).Replace("-", " "), "FRLDTall");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务器连接成功事件
|
||||
/// </summary>
|
||||
private void OnServerConnected(object sender, TcpServerConnectedEventArgs e)
|
||||
{
|
||||
lock (_connectionLock)
|
||||
{
|
||||
_isConnecting = false;
|
||||
IsConnected = true;
|
||||
LastConnectedTime = DateTime.Now;
|
||||
_reconnectAttempts = 0; // 重置重连次数
|
||||
|
||||
Diagnosis.Log($"FLChargeStation[{SiteId}] TCP connected successfully to {_endPoint.Address}:{_endPoint.Port}");
|
||||
|
||||
// 停止重连定时器
|
||||
if (_reconnectTimer != null)
|
||||
{
|
||||
_reconnectTimer.Dispose();
|
||||
_reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务器断开连接事件
|
||||
/// </summary>
|
||||
private void OnServerDisconnected(object sender, TcpServerDisconnectedEventArgs e)
|
||||
{
|
||||
lock (_connectionLock)
|
||||
{
|
||||
_isConnecting = false;
|
||||
IsConnected = false;
|
||||
LastDisconnectedTime = DateTime.Now;
|
||||
|
||||
Diagnosis.Log($"WARN:FLChargeStation[{SiteId}] TCP disconnected from {_endPoint.Address}:{_endPoint.Port}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送充电指令到充电站
|
||||
/// </summary>
|
||||
public override void SendToChargeStation(int isCharge, Car car,Site site)
|
||||
{
|
||||
// 检查连接状态
|
||||
if (!IsConnected || Client == null)
|
||||
{
|
||||
Diagnosis.Log($"WARN:FLChargeStation[{SiteId}] not connected, cannot send charge command");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var messageService = CommunicationMessageService.Instance;
|
||||
float soc = 0f, voltage = 0, electricCurrent = 0;
|
||||
int carId = 0;
|
||||
float setVoltage = 55.0f;
|
||||
float setElectricCurrent = 50.0f;
|
||||
//Site site = null;
|
||||
if (car != null)
|
||||
{
|
||||
soc = float.Parse(Commons.GetCarStatus(car, "Soc"));
|
||||
voltage = float.Parse(Commons.GetCarStatus(car, "Voltage"));
|
||||
electricCurrent = float.Parse(Commons.GetCarStatus(car, "ElectricCurrent"));
|
||||
carId = car.id;
|
||||
//site = SimpleLib.GetSite(car.status.holdingLocks.FirstOrDefault());
|
||||
|
||||
|
||||
|
||||
}
|
||||
if (site != null && site.fields.ContainsKey("setVoltage") && site.fields.ContainsKey("setElectricCurrent"))
|
||||
{
|
||||
setVoltage = float.Parse(site.fields["setVoltage"]);
|
||||
setElectricCurrent = float.Parse(site.fields["setElectricCurrent"]);
|
||||
}
|
||||
|
||||
var msg = GetSendBytes((byte)(isCharge),
|
||||
setVoltage, setElectricCurrent, 1000,
|
||||
Convert.ToSingle(carId), Convert.ToSingle(soc), Convert.ToSingle(electricCurrent), Convert.ToSingle(voltage));
|
||||
|
||||
messageService.AddSendMessage(_endPoint.Address.ToString(), _endPoint.Port, BitConverter.ToString(msg).Replace("-", " "), "FRLDTall", site?.name);
|
||||
|
||||
// 发送数据,带异常处理
|
||||
try
|
||||
{
|
||||
Client.Send(msg);
|
||||
Diagnosis.Post($"{string.Join(" ", msg.Select(d => $"{d:X2}"))}",
|
||||
$"sendChargeSite: {SiteId}", true);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
Diagnosis.Log($"ERR:FLChargeStation[{SiteId}] client disposed during send, triggering reconnect");
|
||||
IsConnected = false;
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
Diagnosis.Log($"ERR:FLChargeStation[{SiteId}] socket error during send: {ex.SocketErrorCode}, triggering reconnect");
|
||||
IsConnected = false;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Diagnosis.Post($"ERR:FLChargeStation[{SiteId}] SwitchCharge Fail: {ExceptionFormatter.FormatEx(e)}");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] GetSendBytes(byte startCharge, float chargeVoltage, float chargeElectricCurrent,
|
||||
float chargeTimeSpan, float carId,
|
||||
float carSoc, float carElectricCurrent, float carVoltage)
|
||||
{
|
||||
var sendByte = new byte[32];
|
||||
try
|
||||
{
|
||||
//BB 01 42 48 00 00 42 5C 00 00 00 00 00 02 00 5D 41 B4 00 00 41 28 00 00 00 00 00 00 00 00 00 EE
|
||||
sendByte = new byte[2] { 0xBB, startCharge }
|
||||
.Concat(BitConverter.GetBytes(chargeElectricCurrent).AsEnumerable().Reverse())
|
||||
.Concat(BitConverter.GetBytes(chargeVoltage).AsEnumerable().Reverse())
|
||||
.Concat(BitConverter.GetBytes((ushort)chargeTimeSpan).AsEnumerable().Reverse())
|
||||
.Concat(BitConverter.GetBytes((ushort)carId).AsEnumerable().Reverse())
|
||||
.Concat(BitConverter.GetBytes((ushort)carSoc).AsEnumerable().Reverse())
|
||||
.Concat(BitConverter.GetBytes(carElectricCurrent).AsEnumerable().Reverse())
|
||||
.Concat(BitConverter.GetBytes(carVoltage).AsEnumerable().Reverse())
|
||||
.Concat(new byte[] { 00, 00, 00, 00, 00, 00, 00, 0xEE }).ToArray();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Post($"下发充电控制{(startCharge == 1 ? "启动" : "停止")}异常+ex:{ExceptionFormatter.FormatEx(ex)}", "error");
|
||||
}
|
||||
|
||||
return sendByte;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
using SimpleLite.RCS;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using SimpleCore;
|
||||
using SimpleCore.Library;
|
||||
using SimpleCore.PropType;
|
||||
using StandardScene.Charge;
|
||||
using StandardScene.TCP;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace StandardScene.ChargeStationType
|
||||
|
||||
{
|
||||
public class MuXingChargeStation : AbstractChargeStation
|
||||
{
|
||||
public static TcpListener tcpListener;
|
||||
public static Thread listenerThread;
|
||||
public static NetworkStream stream;
|
||||
public static TcpClient client;
|
||||
//public Dictionary<IPAddress, (TcpClient, NetworkStream)> chargeClient = new Dictionary<IPAddress, (TcpClient, NetworkStream)> { };
|
||||
public System.Timers.Timer _Timer;
|
||||
public bool reciveHeartBeat;
|
||||
public bool running;
|
||||
public byte[] IdBytes = new byte[] { 0x0f, 0x01 };
|
||||
private static IPEndPoint _endPoint;
|
||||
|
||||
/// <summary>
|
||||
/// 关闭当前TCP连接
|
||||
/// </summary>
|
||||
public override void CloseCommunication()
|
||||
{
|
||||
try
|
||||
{
|
||||
running = false;
|
||||
|
||||
// 停止定时器
|
||||
if (_Timer != null)
|
||||
{
|
||||
_Timer.Stop();
|
||||
_Timer.Dispose();
|
||||
_Timer = null;
|
||||
}
|
||||
|
||||
// 关闭流
|
||||
if (stream != null)
|
||||
{
|
||||
stream.Close();
|
||||
stream = null;
|
||||
}
|
||||
|
||||
// 关闭客户端
|
||||
if (client != null)
|
||||
{
|
||||
client.Close();
|
||||
client = null;
|
||||
}
|
||||
|
||||
// 停止监听器
|
||||
if (tcpListener != null)
|
||||
{
|
||||
tcpListener.Stop();
|
||||
tcpListener = null;
|
||||
}
|
||||
|
||||
Diagnosis.Log($"MuXingChargeStation[{SiteId}] TCP connection closed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"ERR:MuXingChargeStation[{SiteId}] failed to close connection: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void CreateCommunication(IPAddress ip, int port)
|
||||
{
|
||||
// 先关闭旧连接
|
||||
CloseCommunication();
|
||||
|
||||
_endPoint = new IPEndPoint(ip, port);
|
||||
reciveHeartBeat = false;
|
||||
running = true;
|
||||
int n = 0;
|
||||
DateTime offlineTime = DateTime.Now;
|
||||
try
|
||||
{
|
||||
tcpListener = new TcpListener(ip, port);
|
||||
tcpListener.Start();
|
||||
Diagnosis.Post($"AGV与牧星充电站通信已建立,监听 IP: {ip}, 端口: {port}");
|
||||
client = tcpListener.AcceptTcpClient();
|
||||
client.SendTimeout = 5000;
|
||||
client.ReceiveTimeout = 5000;
|
||||
stream = client.GetStream();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"与充电站通信建立失败: {ExceptionFormatter.FormatEx(ex)}");
|
||||
throw;
|
||||
}
|
||||
|
||||
while (running)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (client == null)
|
||||
{
|
||||
client = tcpListener.AcceptTcpClient();
|
||||
client.SendTimeout = 5000;
|
||||
client.ReceiveTimeout = 5000;
|
||||
stream = client.GetStream();
|
||||
}
|
||||
|
||||
var type = ReceiveMesageType();
|
||||
//item1:byte0 帧头
|
||||
//item2:byte11 命令字
|
||||
//item3:byte12 动作码/故障码/状态码
|
||||
//item4:byte5 设备ID 低
|
||||
//item5:byte6 设备ID 高
|
||||
|
||||
//0x10 充电桩登录回复 1次
|
||||
if (type.Item1 == 0xAA && type.Item2 == 0x10)
|
||||
{
|
||||
var timestampBytes = GetTimeStamp();
|
||||
byte[] dataByte = new byte[16]
|
||||
{
|
||||
0, 0x0d,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0x10,
|
||||
0, 0,
|
||||
0x00, 0x00
|
||||
}; //长度必须大于10,与帧长度对应
|
||||
dataByte = InitSendBytes(dataByte, type, timestampBytes);
|
||||
byte[] mesSendByte =
|
||||
CombineDataAndCRC(dataByte, CalculateCRC16(dataByte.Skip(1).ToArray())); //拼接crc(去掉包头)
|
||||
Diagnosis.Post($"login => {string.Join(" ", mesSendByte.Select(d => $"{d:X2}"))}",
|
||||
$"充电桩登录回复");
|
||||
SendMessage(mesSendByte);
|
||||
}
|
||||
//0x12 充电桩对接完成回复 1次
|
||||
else if (type.Item1 == 0xAA && type.Item2 == 0x12)
|
||||
{
|
||||
var timestampBytes = GetTimeStamp();
|
||||
byte[] dataByte = new byte[12]
|
||||
{
|
||||
0, 0x09,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0x12
|
||||
};
|
||||
;
|
||||
dataByte = InitSendBytes(dataByte, type, timestampBytes);
|
||||
byte[] sendByte =
|
||||
CombineDataAndCRC(dataByte, CalculateCRC16(dataByte.Skip(1).ToArray())); //拼接crc(去掉包头)
|
||||
Diagnosis.Post($"docking => {string.Join(" ", sendByte.Select(d => $"{d:X2}"))}",
|
||||
$"充电桩对接完成回复");
|
||||
SendMessage(sendByte);
|
||||
}
|
||||
//0x13 故障上报回复
|
||||
else if (type.Item1 == 0xAA && type.Item2 == 0x13)
|
||||
{
|
||||
var timestampBytes = GetTimeStamp();
|
||||
byte[] dataByte = new byte[13]
|
||||
{
|
||||
0, 0x0A,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0x13,
|
||||
type.Item3
|
||||
};
|
||||
dataByte = InitSendBytes(dataByte, type, timestampBytes);
|
||||
byte[] sendByte =
|
||||
CombineDataAndCRC(dataByte, CalculateCRC16(dataByte.Skip(1).ToArray())); //拼接crc(去掉包头)
|
||||
Diagnosis.Post($"Error => {string.Join(" ", sendByte.Select(d => $"{d:X2}"))}",
|
||||
$"故障上报回复");
|
||||
SendMessage(sendByte);
|
||||
}
|
||||
|
||||
//收到至少1次充电桩心跳包上传
|
||||
if (type.Item1 == 0xAA && type.Item2 == 0xF0 && !reciveHeartBeat)
|
||||
{
|
||||
Diagnosis.Post($"收到充电桩心跳包上传", $"HeartBeatRecive");
|
||||
reciveHeartBeat = true;
|
||||
StartSendingHeartBeat();
|
||||
}
|
||||
|
||||
//异常情况处理
|
||||
//故障上报 故障码不为0 16 17 1
|
||||
if (type.Item1 == 0xAA && type.Item2 == 0x13 &&
|
||||
(type.Item3 != 0 && type.Item3 != 16 && type.Item3 != 17 && type.Item3 != 1))
|
||||
{
|
||||
//充电桩掉线
|
||||
if ((type.Item3 & (1 << 3)) != 0 && reciveHeartBeat)
|
||||
{
|
||||
if (n == 0) offlineTime = DateTime.Now;
|
||||
//充电桩掉线时间大于60s 重启WiFi模块
|
||||
if ((DateTime.Now - offlineTime).TotalSeconds > 60)
|
||||
{
|
||||
var timestampBytes = GetTimeStamp();
|
||||
//下发 命令码指令
|
||||
byte[] dataByte = new byte[17]
|
||||
{
|
||||
0, 0x0E,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0x15,
|
||||
0x04, //重启WiFi模块
|
||||
0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
dataByte = InitSendBytes(dataByte, type, timestampBytes);
|
||||
byte[] sendChargeByte =
|
||||
CombineDataAndCRC(dataByte, CalculateCRC16(dataByte.Skip(1).ToArray()));
|
||||
Diagnosis.Post(
|
||||
$"Restart => {string.Join(" ", sendChargeByte.Select(d => $"{d:X2}"))}",
|
||||
$"重启WiFi模块");
|
||||
SendMessage(sendChargeByte);
|
||||
n = 0;
|
||||
offlineTime = DateTime.Now;
|
||||
}
|
||||
|
||||
n++;
|
||||
}
|
||||
else
|
||||
{
|
||||
n = 0;
|
||||
offlineTime = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
//if (!client.Connected)
|
||||
//{
|
||||
// Console.WriteLine("客户端断开连接,退出循环");
|
||||
// running = false;
|
||||
//}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"充电桩通信异常:{ExceptionFormatter.FormatEx(ex)}");
|
||||
running = false;
|
||||
client.Close();
|
||||
tcpListener.Stop();
|
||||
stream.Close();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public override void SendToChargeStation(int isCharge, Car car,Site site)
|
||||
{
|
||||
var messageService = CommunicationMessageService.Instance;
|
||||
float setVoltage = 55.0f;
|
||||
float setElectricCurrent = 50.0f;
|
||||
float voltage = 430;
|
||||
// Site site = null;
|
||||
if (car != null)
|
||||
{
|
||||
// site = SimpleLib.GetSite(car.status.holdingLocks.FirstOrDefault());
|
||||
|
||||
|
||||
int carId = 0;
|
||||
if (car != null && car.GetType() != typeof(DummyCar))
|
||||
{
|
||||
voltage = float.Parse(Commons.GetCarStatus(car, "Voltage")) * 10;
|
||||
voltage = voltage > 430 ? voltage : 430;
|
||||
carId = car.id;
|
||||
}
|
||||
}
|
||||
if (site != null && site.fields.ContainsKey("setVoltage") && site.fields.ContainsKey("setElectricCurrent"))
|
||||
{
|
||||
setVoltage = float.Parse(site.fields["setVoltage"]);
|
||||
setElectricCurrent = float.Parse(site.fields["setElectricCurrent"]);
|
||||
}
|
||||
|
||||
var byte1 = BitConverter.GetBytes(voltage);
|
||||
var type = ReceiveMesageType();
|
||||
var openChargePort = isCharge == 1 ? 2 : 3;
|
||||
//下发打开充电口指令
|
||||
var timestampBytes = GetTimeStamp();
|
||||
byte[] dataByte = new byte[18]
|
||||
{
|
||||
0, 0x0F,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0x11,
|
||||
(byte)openChargePort,//开始充电/结束充电
|
||||
byte1[0], byte1[1],
|
||||
0xc8, 0x00,//下发最大充电电流
|
||||
0x02//电池种类
|
||||
};
|
||||
dataByte = InitSendBytes(dataByte, type, timestampBytes);
|
||||
byte[] openCharge =
|
||||
CombineDataAndCRC(dataByte, CalculateCRC16(dataByte.Skip(1).ToArray()));
|
||||
if (client.Connected)
|
||||
{
|
||||
stream.WriteAsync(openCharge, 0, openCharge.Length);
|
||||
stream.FlushAsync();
|
||||
}
|
||||
messageService.AddSendMessage(_endPoint.Address.ToString(), _endPoint.Port, BitConverter.ToString(openCharge).Replace("-", " "), site?.name, "MuXing");
|
||||
Diagnosis.Post($"openCharge => {string.Join(" ", openCharge.Select(d => $"{d:X2}"))}",
|
||||
$"下发开始充电");
|
||||
|
||||
}
|
||||
|
||||
public void StartSendingHeartBeat()
|
||||
{
|
||||
if (client == null)
|
||||
{
|
||||
Console.WriteLine("客户端为null,无法启动心跳包定时器");
|
||||
return;
|
||||
}
|
||||
_Timer = new System.Timers.Timer(2000);
|
||||
_Timer.Elapsed += SendHeartBeat;
|
||||
_Timer.AutoReset = true; // 反复执行
|
||||
_Timer.Enabled = true;
|
||||
|
||||
}
|
||||
|
||||
private void SendHeartBeat(Object item, ElapsedEventArgs e)
|
||||
{
|
||||
var timestampBytes = GetTimeStamp();
|
||||
var type = ReceiveMesageType();
|
||||
//下发心跳包报文
|
||||
var rcs = new byte[]
|
||||
{
|
||||
0xBB, 0x09, 0x00, 0x03, 0x00,
|
||||
IdBytes[0], IdBytes[1],
|
||||
timestampBytes[0], timestampBytes[1], timestampBytes[2], timestampBytes[3],
|
||||
0xF0
|
||||
};
|
||||
byte[] mesSendByte = CombineDataAndCRC(rcs, CalculateCRC16(rcs.Skip(1).ToArray()));
|
||||
// 日志记录
|
||||
Diagnosis.Post($"heartBeat => {string.Join(" ", mesSendByte.Select(d => $"{d:X2}"))}", $"下发心跳包");
|
||||
// 发送心跳包
|
||||
SendMessage(mesSendByte);
|
||||
}
|
||||
|
||||
private byte[] GetTimeStamp()
|
||||
{
|
||||
// 获取当前时间戳(秒级别)
|
||||
int timestamp = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
//将时间戳转换为字节数组,低字节在前
|
||||
byte[] timestampBytes = BitConverter.GetBytes(timestamp);
|
||||
return timestampBytes;
|
||||
}
|
||||
|
||||
private static Tuple<byte, byte, byte, byte, byte> ReceiveMesageType()
|
||||
{
|
||||
byte[] message = new byte[1024];
|
||||
int bytesRead = 0;
|
||||
try
|
||||
{
|
||||
if (client != null && client.Connected && stream != null && stream.CanRead && client.Available > 0)
|
||||
{
|
||||
bytesRead = stream.Read(message, 0, 1024);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
client = null;
|
||||
stream = null;
|
||||
Console.WriteLine($"Error reading message: {ex.Message}");
|
||||
}
|
||||
|
||||
//message长度判断:至少需要 13 字节才能安全访问 message[12]
|
||||
if (bytesRead >= 13)
|
||||
{
|
||||
|
||||
string receivedMessage = BitConverter.ToString(message, 0, bytesRead);
|
||||
Diagnosis.Post($"Received <= {string.Join(" ", message.Take(bytesRead).ToArray().Select(d => $"{d:X2}"))}", "读取报文");
|
||||
Diagnosis.Post($"item1:{message[0]:x2},item2:{message[11]:x2},item3:{message[12]:x2},item4:{message[5]:x2},item5:{message[6]:x2},", "Tuple.Item");
|
||||
var messageService = CommunicationMessageService.Instance;
|
||||
messageService.AddReceiveMessage(_endPoint.Address.ToString(), _endPoint.Port, string.Join(",", message), "MuXing");
|
||||
return Tuple.Create<byte, byte, byte, byte, byte>(message[0], message[11], message[12], message[5], message[6]);
|
||||
}
|
||||
return new Tuple<byte, byte, byte, byte, byte>(0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
public static byte[] CombineDataAndCRC(byte[] data, byte[] crc)
|
||||
{
|
||||
//byte[] crcBytes = BitConverter.GetBytes(crc);
|
||||
// 合并数据和CRC
|
||||
byte[] combined = new byte[data.Length + crc.Length];
|
||||
Array.Copy(data, combined, data.Length);
|
||||
Array.Copy(crc, 0, combined, data.Length, crc.Length);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
private static byte[] CalculateCRC16(byte[] data)
|
||||
{
|
||||
byte b = byte.MaxValue;
|
||||
byte b2 = byte.MaxValue;
|
||||
byte b3 = 1;
|
||||
byte b4 = 160;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
b = (byte)(b ^ data[i]);
|
||||
for (int j = 0; j <= 7; j++)
|
||||
{
|
||||
byte b5 = b2;
|
||||
byte b6 = b;
|
||||
b2 = (byte)(b2 >> 1);
|
||||
b = (byte)(b >> 1);
|
||||
if ((b5 & 1) == 1)
|
||||
{
|
||||
b = (byte)(b | 0x80u);
|
||||
}
|
||||
if ((b6 & 1) == 1)
|
||||
{
|
||||
b2 = (byte)(b2 ^ b4);
|
||||
b = (byte)(b ^ b3);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new byte[2]
|
||||
{
|
||||
b,b2
|
||||
};
|
||||
}
|
||||
|
||||
private void SendMessage(byte[] message)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (client != null && stream != null)
|
||||
{
|
||||
stream.Write(message, 0, message.Length);
|
||||
stream.Flush();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
client = null;
|
||||
Console.WriteLine($"Error sending message: {ExceptionFormatter.FormatEx(ex)}");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] InitSendBytes(byte[] sendBytes, Tuple<byte, byte, byte, byte, byte> type, byte[] timestampBytes)
|
||||
{
|
||||
sendBytes[0] = 0xBB; //帧头
|
||||
sendBytes[2] = 0x00; //帧长 高
|
||||
sendBytes[3] = 0x03; //设备类型 低
|
||||
sendBytes[4] = 0x00; //设备类型 高
|
||||
sendBytes[5] = IdBytes[0]; //设备ID 低
|
||||
sendBytes[6] = IdBytes[1]; //设备ID 高
|
||||
sendBytes[7] = timestampBytes[0]; //时间戳 低
|
||||
sendBytes[8] = timestampBytes[1];
|
||||
sendBytes[9] = timestampBytes[2];
|
||||
sendBytes[10] = timestampBytes[3]; //时间戳 高
|
||||
return sendBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using SimpleLite.RCS;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using SimpleCore;
|
||||
using SimpleCore.Library;
|
||||
using SimpleCore.PropType;
|
||||
using StandardScene.Charge;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using static SimpleCore.Traffic.TrafficControl;
|
||||
|
||||
namespace StandardScene.ChargeStationType
|
||||
{
|
||||
public class PCBChargeStation : AbstractChargeStation
|
||||
{
|
||||
private int _index = 0;
|
||||
|
||||
public int IndexReceive;
|
||||
private IPEndPoint _endPoint;
|
||||
|
||||
public override void OnUdpMessage(byte[] message)
|
||||
{
|
||||
if (message != null && message.Length > 1)
|
||||
IndexReceive = message[1];
|
||||
}
|
||||
|
||||
public override void CreateCommunication(IPAddress ip, int port)
|
||||
{
|
||||
_endPoint = new IPEndPoint(ip, port);
|
||||
}
|
||||
public override void SendToChargeStation(int isCharge, Car car,Site site)
|
||||
{
|
||||
var messageService = CommunicationMessageService.Instance;
|
||||
using (UdpClient udpClient = new UdpClient())
|
||||
{
|
||||
//float soc = 0f, voltage = 0, electricCurrent = 0, chargeTimeSpan = 30f;
|
||||
|
||||
float setVoltage = 29.2f;
|
||||
float setElectricCurrent = 40.0f;
|
||||
float soc = 0f, voltage = 0, electricCurrent = 0, chargeTimeSpan = 30f;
|
||||
int carId = 0;
|
||||
//Site site = null;
|
||||
if (car != null)
|
||||
{
|
||||
carId = car.id;
|
||||
soc = (float)Commons.CarValue(car, "Soc");
|
||||
voltage = (float)Commons.CarValue(car, "Voltage");
|
||||
electricCurrent = (float)Commons.CarValue(car, "ElectricCurrent");
|
||||
//site = SimpleLib.GetSite(car.status.holdingLocks.FirstOrDefault());
|
||||
|
||||
}
|
||||
if (site != null && site.fields.ContainsKey("setVoltage") && site.fields.ContainsKey("setElectricCurrent"))
|
||||
{
|
||||
setVoltage = float.Parse(site.fields["setVoltage"]);
|
||||
setElectricCurrent = float.Parse(site.fields["setElectricCurrent"]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
var msg = GetSendBytes((byte)isCharge, setVoltage, setElectricCurrent, chargeTimeSpan,
|
||||
carId, soc, electricCurrent, voltage);
|
||||
messageService.AddSendMessage(_endPoint.Address.ToString(), _endPoint.Port, BitConverter.ToString(msg).Replace("-", " "), "FRLDShort", site?.name);
|
||||
Diagnosis.Log($"ChargeStation ADD:[{BitConverter.ToString(msg).Replace("-", " ")}]", "UDP发送报文信息", true);
|
||||
udpClient.SendAsync(msg, msg.Length, _endPoint);
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] GetSendBytes(byte startCharge, float chargeVoltage, float chargeElectricCurrent, float chargeTimeSpan, int carId, float carSoc, float carElectricCurrent, float carVoltage)
|
||||
{
|
||||
var sendBytes = new byte[32];
|
||||
var indexNo = (byte)GetIndexNo();
|
||||
try
|
||||
{
|
||||
sendBytes = new byte[3] { 0xBB, indexNo, startCharge }
|
||||
.Concat(BitConverter.GetBytes((int)chargeElectricCurrent * 10).AsEnumerable().Reverse())
|
||||
.Concat(BitConverter.GetBytes((int)chargeVoltage * 10).AsEnumerable().Reverse())
|
||||
.Concat(BitConverter.GetBytes((ushort)chargeTimeSpan).AsEnumerable().Reverse())
|
||||
.Concat(BitConverter.GetBytes((ushort)carId).AsEnumerable().Reverse())
|
||||
.Concat([(byte)carSoc])
|
||||
.Concat(BitConverter.GetBytes((int)(carElectricCurrent * 10)).AsEnumerable().Reverse())
|
||||
.Concat(BitConverter.GetBytes((int)(carVoltage * 10)).AsEnumerable().Reverse())
|
||||
.Concat(new byte[] { 00, 00, 00, 00, 00, 00, 00, 0xEE }).ToArray();
|
||||
var crcCode = GetCRC(sendBytes.Skip(1).Take(28).ToArray()).AsEnumerable().Reverse().ToArray();
|
||||
sendBytes[29] = crcCode[0];
|
||||
sendBytes[30] = crcCode[1];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"充电报文组包异常 ex => {ex.Message}", "充电", true);
|
||||
}
|
||||
return sendBytes;
|
||||
}
|
||||
private byte[] GetCRC(byte[] data)
|
||||
{
|
||||
byte b = byte.MaxValue;
|
||||
byte b2 = byte.MaxValue;
|
||||
byte b3 = 1;
|
||||
byte b4 = 160;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
b = (byte)(b ^ data[i]);
|
||||
for (int j = 0; j <= 7; j++)
|
||||
{
|
||||
byte b5 = b2;
|
||||
byte b6 = b;
|
||||
b2 = (byte)(b2 >> 1);
|
||||
b = (byte)(b >> 1);
|
||||
if ((b5 & 1) == 1)
|
||||
{
|
||||
b = (byte)(b | 0x80u);
|
||||
}
|
||||
if ((b6 & 1) == 1)
|
||||
{
|
||||
b2 = (byte)(b2 ^ b4);
|
||||
b = (byte)(b ^ b3);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new byte[2]
|
||||
{
|
||||
b,b2
|
||||
};
|
||||
}
|
||||
|
||||
private int GetIndexNo()
|
||||
{
|
||||
if (_index < 255)
|
||||
{
|
||||
_index = _index + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
_index = 0;
|
||||
}
|
||||
return _index;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StandardScene.Utils;
|
||||
using SimpleCore.Library;
|
||||
|
||||
namespace StandardScene.ExtendDevice.Door
|
||||
{
|
||||
/// <summary>
|
||||
/// Modbus 门控制器实现
|
||||
/// </summary>
|
||||
[DoorType("ModbusDoorController")]
|
||||
public class ModbusDoorController : BasicDoorController
|
||||
{
|
||||
/// <summary>
|
||||
/// Modbus TCP 客户端
|
||||
/// </summary>
|
||||
private ModbusRtu _modbusClient;
|
||||
|
||||
/// <summary>
|
||||
/// 同步锁
|
||||
/// </summary>
|
||||
private readonly object _syncLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 是否已启动
|
||||
/// </summary>
|
||||
private bool _isStarted = false;
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次已下发的门控制状态,键为门索引
|
||||
/// </summary>
|
||||
private readonly Dictionary<int, bool> _lastSentControl = new Dictionary<int, bool>();
|
||||
|
||||
/// <summary>
|
||||
/// 定时读取任务取消令牌
|
||||
/// </summary>
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// 定时读取任务
|
||||
/// </summary>
|
||||
private Task _readTask;
|
||||
|
||||
/// <summary>
|
||||
/// 读取间隔(毫秒),默认1000ms
|
||||
/// </summary>
|
||||
public int ReadInterval { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// 重连间隔(毫秒),默认3000ms
|
||||
/// </summary>
|
||||
public int ReconnectInterval { get; set; } = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// 上次重连尝试时间
|
||||
/// </summary>
|
||||
private DateTime _lastReconnectAttempt = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// Modbus 从站地址,默认1
|
||||
/// </summary>
|
||||
public byte SlaveAddress { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 设置门的目标控制状态(线程安全实现)
|
||||
/// 仅修改内存字段,不直接进行通信,实际通信在内部线程中完成
|
||||
/// </summary>
|
||||
/// <param name="doorIndex">门索引</param>
|
||||
/// <param name="open">true=打开,false=关闭</param>
|
||||
public override void SetDoorControlTarget(int doorIndex, bool open)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
base.SetDoorControlTarget(doorIndex, open);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接门控制器
|
||||
/// </summary>
|
||||
public override void Connect()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_isStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
UpdateState(DoorControllerState.Connecting);
|
||||
|
||||
// 根据配置的门初始化门状态
|
||||
var doorIndices = DoorConfigs.Keys.OrderBy(k => k).ToList();
|
||||
InitializeDoors(doorIndices);
|
||||
|
||||
// 初始化最近一次已下发的控制状态
|
||||
_lastSentControl.Clear();
|
||||
foreach (var index in doorIndices)
|
||||
{
|
||||
_lastSentControl[index] = false;
|
||||
if (!DoorControlTargets.ContainsKey(index))
|
||||
{
|
||||
DoorControlTargets[index] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试连接 Modbus TCP 客户端
|
||||
try
|
||||
{
|
||||
_modbusClient = new ModbusRtu();
|
||||
_modbusClient.StartTcpRtu(Ip, Port);
|
||||
UpdateState(DoorControllerState.Online);
|
||||
}
|
||||
catch (Exception connectEx)
|
||||
{
|
||||
UpdateState(DoorControllerState.Connecting);
|
||||
Diagnosis.Log($"ModbusDoorController[{Index}] 初次连接失败,将在后台持续重连: {ExceptionFormatter.FormatEx(connectEx)}", "ModbusDoorController", true);
|
||||
}
|
||||
|
||||
// 启动定时读取任务
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_readTask = Task.Run(() => ReadDoorStatesLoop(_cancellationTokenSource.Token));
|
||||
|
||||
_isStarted = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateState(DoorControllerState.Error, $"初始化失败: {ex.Message}");
|
||||
_isStarted = false;
|
||||
Diagnosis.Log($"ModbusDoorController[{Index}] 初始化失败: {ExceptionFormatter.FormatEx(ex)}", "ModbusDoorController", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断开连接
|
||||
/// </summary>
|
||||
public override void Disconnect()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_isStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 停止读取任务
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_readTask?.Wait(1000);
|
||||
|
||||
// 关闭 Modbus 连接
|
||||
_modbusClient?.Close();
|
||||
_modbusClient = null;
|
||||
|
||||
_isStarted = false;
|
||||
UpdateState(DoorControllerState.Offline);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateState(DoorControllerState.Error, $"断开连接失败: {ex.Message}");
|
||||
Diagnosis.Log($"ModbusDoorController[{Index}] 断开连接失败: {ExceptionFormatter.FormatEx(ex)}", "ModbusDoorController", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定时读取门状态循环
|
||||
/// </summary>
|
||||
private void ReadDoorStatesLoop(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_isStarted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查连接状态
|
||||
bool isConnected = _modbusClient?.modbusRtu?.Connected ?? false;
|
||||
if (_modbusClient == null || !isConnected)
|
||||
{
|
||||
UpdateState(DoorControllerState.Connecting);
|
||||
|
||||
// 控制重连频率
|
||||
var timeSinceLastReconnect = (DateTime.Now - _lastReconnectAttempt).TotalMilliseconds;
|
||||
if (timeSinceLastReconnect >= ReconnectInterval)
|
||||
{
|
||||
_lastReconnectAttempt = DateTime.Now;
|
||||
TryReconnect();
|
||||
}
|
||||
|
||||
isConnected = _modbusClient?.modbusRtu?.Connected ?? false;
|
||||
if (_modbusClient == null || !isConnected)
|
||||
{
|
||||
Thread.Sleep(ReadInterval);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取所有门的状态
|
||||
ReadAllDoorStates();
|
||||
|
||||
// 根据目标控制状态下发控制指令
|
||||
ApplyDoorControlTargets();
|
||||
|
||||
// 更新在线状态
|
||||
UpdateState(DoorControllerState.Online);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"ModbusDoorController[{Index}] 读取状态失败: {ExceptionFormatter.FormatEx(ex)}", "ModbusDoorController", true);
|
||||
UpdateState(DoorControllerState.Error, $"读取状态失败: {ex.Message}");
|
||||
|
||||
// 如果连接失败,尝试重连
|
||||
TryReconnect();
|
||||
}
|
||||
|
||||
// 等待指定间隔
|
||||
Thread.Sleep(ReadInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试重连 Modbus TCP 连接
|
||||
/// </summary>
|
||||
private void TryReconnect()
|
||||
{
|
||||
ModbusRtu oldClient = null;
|
||||
try
|
||||
{
|
||||
// 安全关闭旧连接
|
||||
if (_modbusClient != null)
|
||||
{
|
||||
oldClient = _modbusClient;
|
||||
_modbusClient = null;
|
||||
|
||||
try
|
||||
{
|
||||
oldClient.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略关闭时的异常
|
||||
}
|
||||
finally
|
||||
{
|
||||
oldClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新连接
|
||||
_modbusClient = new ModbusRtu();
|
||||
_modbusClient.StartTcpRtu(Ip, Port);
|
||||
|
||||
Diagnosis.Log($"ModbusDoorController[{Index}] 重连成功", "ModbusDoorController", false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 重连失败,确保资源释放
|
||||
if (_modbusClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_modbusClient.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略关闭异常
|
||||
}
|
||||
_modbusClient = null;
|
||||
}
|
||||
|
||||
Diagnosis.Log($"ModbusDoorController[{Index}] 重连失败: {ExceptionFormatter.FormatEx(ex)}", "ModbusDoorController", false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取所有门的状态
|
||||
/// </summary>
|
||||
private void ReadAllDoorStates()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
foreach (var doorConfig in DoorConfigs.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
var state = ReadDoorState(doorConfig.Index);
|
||||
UpdateDoorState(doorConfig.Index, state ? DoorState.Open : DoorState.Closed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"ModbusDoorController[{Index}] 读取门{doorConfig.Index}状态失败: {ExceptionFormatter.FormatEx(ex)}", "ModbusDoorController", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 DoorControlTargets 中的目标状态,下发门控制指令
|
||||
/// </summary>
|
||||
private void ApplyDoorControlTargets()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
foreach (var doorConfig in DoorConfigs.Values)
|
||||
{
|
||||
var doorIndex = doorConfig.Index;
|
||||
|
||||
// 获取目标控制状态,默认false
|
||||
bool target = false;
|
||||
DoorControlTargets.TryGetValue(doorIndex, out target);
|
||||
|
||||
// 如果门配置为不允许发送任何控制指令,则跳过(既不打开也不关闭)
|
||||
if (doorConfig.NoControl)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取上一次已下发的状态
|
||||
bool last;
|
||||
var hasLast = _lastSentControl.TryGetValue(doorIndex, out last);
|
||||
|
||||
// 如果没有记录或状态发生变化,则下发控制
|
||||
if (!hasLast || last != target)
|
||||
{
|
||||
try
|
||||
{
|
||||
WriteDoorControl(doorIndex, target);
|
||||
_lastSentControl[doorIndex] = target;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Diagnosis.Log($"ModbusDoorController[{Index}] 下发门{doorIndex}控制指令失败: {ExceptionFormatter.FormatEx(ex)}", "ModbusDoorController", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取门状态(开到位信号)
|
||||
/// </summary>
|
||||
/// <param name="doorIndex">门索引</param>
|
||||
/// <returns>true=打开,false=关闭</returns>
|
||||
public override bool ReadDoorState(int doorIndex)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!DoorConfigs.TryGetValue(doorIndex, out var doorConfig))
|
||||
{
|
||||
throw new ArgumentException($"门{doorIndex}不存在");
|
||||
}
|
||||
|
||||
if (_modbusClient == null || !_modbusClient.modbusRtu.Connected)
|
||||
{
|
||||
throw new InvalidOperationException("Modbus连接未建立");
|
||||
}
|
||||
|
||||
// 读取开到位信号
|
||||
var data = _modbusClient.ReadDiscreteInputs_02(SlaveAddress, doorConfig.OpenStatusAddress, 1);
|
||||
return data != null && data.Length > 0 && data[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入门控制信号(开关控制)
|
||||
/// </summary>
|
||||
/// <param name="doorIndex">门索引</param>
|
||||
/// <param name="open">true=打开,false=关闭</param>
|
||||
public override void WriteDoorControl(int doorIndex, bool open)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!DoorConfigs.TryGetValue(doorIndex, out var doorConfig))
|
||||
{
|
||||
throw new ArgumentException($"门{doorIndex}不存在");
|
||||
}
|
||||
|
||||
if (_modbusClient == null || !_modbusClient.modbusRtu.Connected)
|
||||
{
|
||||
// 尝试重连
|
||||
TryReconnect();
|
||||
if (_modbusClient == null || !_modbusClient.modbusRtu.Connected)
|
||||
{
|
||||
throw new InvalidOperationException("Modbus连接未建立");
|
||||
}
|
||||
}
|
||||
|
||||
// 写入开关控制信号(线圈)
|
||||
_modbusClient.WriteMultipleCoils_15(SlaveAddress, doorConfig.ControlAddress, [open]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 析构函数,确保资源释放
|
||||
/// </summary>
|
||||
~ModbusDoorController()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<RootNamespace>StandardScene</RootNamespace>
|
||||
<AssemblyName>StandardScene.Devices</AssemblyName>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Deterministic>true</Deterministic>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<NoWarn>$(NoWarn);NU1701;CS0618;CS0612;MSB3277;CA1416</NoWarn>
|
||||
<AssemblySearchPaths>{HintPathFromItem};{TargetFrameworkDirectory};{RawFileName};{GAC}</AssemblySearchPaths>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- 插件清单:随 dll 输出,供 SimpleLite plugins 选择性加载(约定 <dll>.scene.json) -->
|
||||
<ItemGroup>
|
||||
<None Update="StandardScene.Devices.scene.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 基座:各设备抽象基类 + 注册特性 + 业务编排(Mission/Manager/UdpService) 均留 Core;本 dll 仅含具体驱动(门/充电桩/按钮盒) -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StandardScene.Core\StandardScene.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 与 Core 一致的本地契约/工具 dll + Leeg 按钮盒 SDK(leegiot);Modbus/TCP 经 Core 的 Utils/TCP 间接使用,无需 NuGet -->
|
||||
<ItemGroup>
|
||||
<Reference Include="SimpleLite">
|
||||
<HintPath>E:\Work\Core\Simple-FR\Simple\SimpleLite\bin\Debug\SimpleLite.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SimpleCore">
|
||||
<HintPath>E:\Work\Core\Simple-FR\Simple\SimpleCore\bin\Debug\netstandard2.0\SimpleCore.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="CommonUsage">
|
||||
<HintPath>D:\MDCS\Dependencies\Commons\CommonUsage.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Topaz">
|
||||
<HintPath>E:\Work\Core\Simple-FR\Simple\tools\Topaz.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="LessokajiWeaverUtilities">
|
||||
<HintPath>E:\Work\Core\Simple-FR\Simple\SimpleLite\bin\Debug\LessokajiWeaverUtilities.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="leegKeys-sdk">
|
||||
<HintPath>..\StandardScene.Core\Ref\leegKeys-sdk.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "scene.device",
|
||||
"displayName": "设备驱动(门 / 充电桩 / 按钮盒)",
|
||||
"assembly": "StandardScene.Devices.dll",
|
||||
"coreVersion": ">=1.0.0",
|
||||
"requiresCore": "StandardScene.dll",
|
||||
"provides": {
|
||||
"doorControllers": [ "ModbusDoorController" ],
|
||||
"chargeStations": [ "FLChargeStation", "PCBChargeStation", "MuXingChargeStation" ],
|
||||
"buttonBoxes": [ "LeegButtonBox", "AzowieButtonBox" ]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user