Files
I150/srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Drivers/Tada/TadaRs485Client.cs
2026-07-03 16:37:12 +07:00

360 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
namespace RobotNet10.RobotApp.Drivers.Tada;
[Flags]
public enum DataKind1 : byte
{
Voltage = 1 << 0,
Current = 1 << 1,
SOC = 1 << 2,
Status = 1 << 3,
ChargeTime = 1 << 4,
DischargeTime = 1 << 5,
Temp = 1 << 6
}
[Flags]
public enum DataKind2 : byte
{
SOH = 1 << 0,
RemainCapacity = 1 << 1,
RemainEnergy = 1 << 2
}
public class TadaRs485Client : IDisposable
{
private readonly ILogger _logger;
private SerialPort _port;
private readonly string _portName;
private readonly int _baud;
private readonly Parity _parity;
private readonly int _dataBits;
private readonly StopBits _stopBits;
private readonly int _readTimeoutMs;
private readonly int _writeTimeoutMs;
public bool IsFaulted => _faulted;
private readonly Lock _lock = new();
private bool _faulted = false;
private DateTime _lastRetry = DateTime.MinValue;
private int _retryDelayMs = 1000; // backoff min = 1s
private DateTime _lastDataTime = DateTime.MinValue;
private readonly int _dataTimeoutMs = 10_000; // 10 giây
private int _consecutiveFails = 0;
private readonly int _maxFails = 3; // sau 3 lần fail liên tiếp thì coi như lost
public TadaRs485Client(
ILogger logger,
string portName,
int baud = 19200,
Parity parity = Parity.None,
int dataBits = 8,
StopBits stopBits = StopBits.One,
int readTimeoutMs = 2000,
int writeTimeoutMs = 1000)
{
_logger = logger;
_portName = portName;
_baud = baud;
_parity = parity;
_dataBits = dataBits;
_stopBits = stopBits;
_readTimeoutMs = readTimeoutMs;
_writeTimeoutMs = writeTimeoutMs;
_lastDataTime = DateTime.Now;
_consecutiveFails = 0;
_port = null!;
EnsureConnected();
}
private void CheckDataTimeout()
{
if (_lastDataTime != DateTime.MinValue &&
(DateTime.Now - _lastDataTime).TotalMilliseconds > _dataTimeoutMs)
{
_logger.LogError("[TadaRs485Client] Communication lost (data timeout).");
_faulted = true;
_lastDataTime = DateTime.MinValue; // reset để tránh spam log
}
}
private void EnsureConnected()
{
lock (_lock)
{
if (_port != null && _port.IsOpen && !_faulted) return;
if ((DateTime.Now - _lastRetry).TotalMilliseconds < _retryDelayMs)
return;
_lastRetry = DateTime.Now;
try
{
_port?.Dispose();
_port = new SerialPort(_portName, _baud, _parity, _dataBits, _stopBits)
{
ReadTimeout = _readTimeoutMs,
WriteTimeout = _writeTimeoutMs
};
_port.Open();
_faulted = false;
_retryDelayMs = 1000;
}
catch (Exception ex)
{
_logger.LogError("[TadaRs485Client] Connect failed: {ex.Message}", ex.Message);
_faulted = true;
// exponential backoff up to 30s
_retryDelayMs = Math.Min(_retryDelayMs * 2, 30_000);
}
}
}
private static byte Checksum(byte[] data, int start, int len)
{
int sum = 0;
for (int i = start; i < start + len; i++) sum += data[i];
return (byte)(sum & 0xFF);
}
private static string ToHex(byte[] data, int len)
{
var sb = new StringBuilder();
for (int i = 0; i < len; i++)
{
sb.Append(data[i].ToString("X2"));
if (i < len - 1) sb.Append('-');
}
return sb.ToString();
}
public void RequestStatus(byte address = 0x60,
DataKind1 kind1 = DataKind1.Voltage | DataKind1.Current | DataKind1.SOC | DataKind1.Status |
DataKind1.ChargeTime | DataKind1.DischargeTime | DataKind1.Temp,
DataKind2 kind2 = DataKind2.SOH | DataKind2.RemainCapacity | DataKind2.RemainEnergy)
{
EnsureConnected();
if (_port == null || !_port.IsOpen || _faulted) return;
try
{
byte kind1Byte = (byte)kind1;
byte kind2Byte = (byte)kind2;
byte[] frame =
[
0xAF, 0xFA,
address,
0x05,
0x01,
address,
kind1Byte, kind2Byte,
0x00,
0xAF, 0xA0
];
frame[8] = Checksum(frame, 2, 6);
_port.DiscardInBuffer();
_port.DiscardOutBuffer();
_port.Write(frame, 0, frame.Length);
Thread.Sleep(50);
}
catch (Exception ex)
{
_logger.LogError("[TadaRs485Client] Write error: {ex.Message}", ex.Message);
_faulted = true;
}
}
private byte[] ReadFrame()
{
EnsureConnected();
if (_port == null || !_port.IsOpen || _faulted) return [];
var buffer = new List<byte>();
int expectedLen = -1;
var start = DateTime.Now;
try
{
while ((DateTime.Now - start).TotalMilliseconds < _readTimeoutMs)
{
int bytesAvailable = _port.BytesToRead;
if (bytesAvailable > 0)
{
byte[] tempBuffer = new byte[bytesAvailable];
int bytesRead = _port.Read(tempBuffer, 0, bytesAvailable);
buffer.AddRange(tempBuffer.Take(bytesRead));
// check start marker (chuẩn AF FA hoặc bản partial 4D)
if (buffer.Count >= 3 && expectedLen == -1)
{
if (buffer[0] == 0xAF && buffer[1] == 0xFA)
{
expectedLen = buffer[3] + 6;
}
else if (buffer[0] == 0x4D)
{
// frame thiếu AF FA -> vẫn tính chiều dài như thường
expectedLen = buffer[2] + 5; // vì mất 2 byte start
}
}
if (expectedLen > 0 && buffer.Count >= expectedLen)
{
if (buffer[^2] == 0xAF && buffer[^1] == 0xA0)
{
return [.. buffer];
}
else
{
buffer.Clear();
expectedLen = -1;
}
}
}
else
{
Thread.Sleep(2);
}
}
// hết thời gian chờ
if (buffer.Count > 0)
_logger.LogWarning("[TadaRs485Client] Timeout / partial frame ({buffer.Count} bytes): {frame}", buffer.Count, ToHex([.. buffer], buffer.Count));
return [];
}
catch (Exception ex)
{
_logger.LogError("[TadaRs485Client] Read error: {ex.Message}", ex.Message);
_faulted = true;
return [];
}
}
public Dictionary<string, double>? ReadResponse()
{
var frame = ReadFrame();
if (frame == null || frame.Length < 9)
{
_consecutiveFails++;
if (_consecutiveFails >= _maxFails)
{
_logger.LogError("[TadaRs485Client] Communication lost (too many failed reads).");
_faulted = true;
}
CheckDataTimeout();
return null;
}
if (frame[0] == 0x4D)
{
// Partial frame - fix it by prepending AF FA
var newFrame = new byte[frame.Length + 1];
newFrame[0] = 0xAF; newFrame[1] = 0xFA;
Array.Copy(frame, 1, newFrame, 2, frame.Length - 1);
frame = newFrame;
}
else if (frame[0] == 0xAF && frame.Length > 1 && frame[1] == 0xFA)
{
// Valid full frame - continue processing
}
else
{
// Invalid frame format
_consecutiveFails++;
if (_consecutiveFails >= _maxFails)
{
_logger.LogError("[TadaRs485Client] Communication lost (invalid frame format).");
_faulted = true;
}
CheckDataTimeout();
return null;
}
if (frame[^2] != 0xAF || frame[^1] != 0xA0)
{
_logger.LogError("[TadaRs485Client] Footer mismatch");
throw new Exception("invalid frame: footer");
}
if (frame[4] != 0x03)
{
_logger.LogError("[TadaRs485Client] Command code mismatch");
throw new Exception("invalid frame: command");
}
var result = new Dictionary<string, double>();
int dataLen = frame[3] - 3;
int dataStart = 6;
for (int i = 0; i + 1 < dataLen; i += 2)
{
int idx = dataStart + i;
if (idx + 1 >= frame.Length) break;
ushort raw = (ushort)((frame[idx] << 8) | frame[idx + 1]);
int index = i / 2;
switch (index)
{
case 0: result["Voltage"] = raw / 100.0; break;
case 1: result["Current"] = (short)raw / 10.0; break;
case 2: result["SOC"] = raw; break;
case 3: result["Status"] = raw; break;
case 4: result["ChargeTime"] = raw; break;
case 5: result["DischargeTime"] = raw; break;
case 6: result["Temp"] = (short)raw / 10.0; break;
case 7: result["SOH"] = raw; break;
case 8: result["RemainCapacity"] = raw / 100.0; break;
case 9: result["RemainEnergy"] = raw / 10.0; break;
}
}
_lastDataTime = DateTime.Now; // reset watchdog
_consecutiveFails = 0; // reset fail counter
return result;
}
public void ForceReconnect()
{
lock (_lock)
{
try
{
_port?.Close();
}
catch { }
_faulted = false;
_lastRetry = DateTime.MinValue;
_retryDelayMs = 1000;
EnsureConnected();
}
}
public void Dispose()
{
try
{
if (_port != null)
{
if (_port.IsOpen)
{
_port.Close();
}
_port.Dispose();
_port = null!;
}
}
catch (Exception ex)
{
_logger.LogError("[TadaRs485Client] Error in Dispose: {ex.Message}", ex.Message);
}
GC.SuppressFinalize(this);
}
}