借助网络配置,控制Modbus工具的日志显示,避免卡死界面
智能大石头 authored at 2021-12-11 22:09:19
36.97 KiB
XCoder
using ALib.CAN;
using NewLife;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ALib.Can_SAEJ1939
{
    #region 枚举

    /// <summary>
    /// TP.CM 控制字节(byte1),决定连接管理报文的具体结构
    /// </summary>
    public enum TpControl_e : byte
    {
        /// <summary>RTS 请求发送(点对点)</summary>
        Rts = 0x10,

        /// <summary>CTS 允许发送(点对点)</summary>
        Cts = 0x11,

        /// <summary>EOM 发送结束(点对点)</summary>
        EndOfMsg = 0x13,

        /// <summary>BAM 广播发送</summary>
        Bam = 0x20,

        /// <summary>Abort 中止</summary>
        Abort = 0xFF,
    }

    /// <summary>
    /// TP 中止原因
    /// </summary>
    public enum TpAbortReason_e : byte
    {
        /// <summary>已达最大重发次数</summary>
        MaxRetransmit = 1,

        /// <summary>资源超时</summary>
        Timeout = 2,

        /// <summary>连接异常中止</summary>
        AbortConnection = 3,

        /// <summary>暂存区不足</summary>
        NoBuffer = 4,
    }

    #endregion

    #region TP.CM 连接管理

    /// <summary>
    /// TP.CM 连接管理,PGN 60416 (0x00EC00)。
    /// 数据区 8 字节,byte1 为控制字节,其余按控制字节解释:
    /// <code>
    /// RTS (0x10)   : [ctrl][总长 L][总长 H][包数][最大包长][PGN 4字节]
    /// CTS (0x11)   : [ctrl][可发包数][下一包序号][保留 2][PGN 4字节]
    /// EOM (0x13)   : [ctrl][总长 L][总长 H][包数][保留 1][PGN 4字节]
    /// BAM (0x20)   : [ctrl][总长 L][总长 H][包数][保留 1][PGN 4字节]
    /// Abort (0xFF) : [ctrl][原因][保留 3][PGN 4字节]
    /// </code>
    /// </summary>
    public class Msg_TpCm
    {
        /// <summary>目标 PGN</summary>
        public const UInt32 PGN = J1939Pgn.TpCm;

        /// <summary>控制字节</summary>
        public TpControl_e Control = TpControl_e.Bam;

        /// <summary>报文总长度(字节),RTS/EOM/BAM 有效</summary>
        public UInt16 TotalLength = 0;

        /// <summary>总包数(每包 7 字节),RTS/EOM/BAM 有效</summary>
        public byte TotalPackets = 0;

        /// <summary>最大包长(即每包可容纳字节数),仅 RTS 有效,通常为 7</summary>
        public byte MaxPacketSize = 7;

        /// <summary>可发送的包数,仅 CTS 有效</summary>
        public byte PacketsToSend = 0;

        /// <summary>下一包序号,仅 CTS 有效(从 1 开始)</summary>
        public byte NextPacketNumber = 1;

        /// <summary>中止原因,仅 Abort 有效</summary>
        public TpAbortReason_e AbortReason = TpAbortReason_e.AbortConnection;

        /// <summary>本次传输所对应的报文 PGN(18bit)</summary>
        public UInt32 MessagePgn = 0;

        /// <summary>目标地址 DA,BAM 时为 0xFF</summary>
        public byte DestinationAddress = J1939Addr.Global;

        /// <summary>源地址 SA</summary>
        public byte SourceAddress = 0;

        /// <summary>优先级,TP.CM 固定为 7</summary>
        public int Priority = 7;

        public Msg_TpCm() { }

        /// <summary>构造一个 BAM 连接管理报文</summary>
        public static Msg_TpCm CreateBam(UInt32 messagePgn, UInt16 totalLength, byte sourceAddress)
        {
            return new Msg_TpCm
            {
                Control = TpControl_e.Bam,
                MessagePgn = messagePgn,
                TotalLength = totalLength,
                TotalPackets = (byte)((totalLength + 6) / 7),
                DestinationAddress = J1939Addr.Global,
                SourceAddress = sourceAddress,
            };
        }

        /// <summary>构造一个 RTS 连接管理报文</summary>
        public static Msg_TpCm CreateRts(UInt32 messagePgn, UInt16 totalLength, byte destinationAddress, byte sourceAddress)
        {
            return new Msg_TpCm
            {
                Control = TpControl_e.Rts,
                MessagePgn = messagePgn,
                TotalLength = totalLength,
                TotalPackets = (byte)((totalLength + 6) / 7),
                MaxPacketSize = 7,
                DestinationAddress = destinationAddress,
                SourceAddress = sourceAddress,
            };
        }

        /// <summary>构造 CTS 报文</summary>
        public static Msg_TpCm CreateCts(UInt32 messagePgn, byte packetsToSend, byte nextPacketNumber, byte destinationAddress, byte sourceAddress)
        {
            return new Msg_TpCm
            {
                Control = TpControl_e.Cts,
                MessagePgn = messagePgn,
                PacketsToSend = packetsToSend,
                NextPacketNumber = nextPacketNumber,
                DestinationAddress = destinationAddress,
                SourceAddress = sourceAddress,
            };
        }

        /// <summary>构造 EOM 报文</summary>
        public static Msg_TpCm CreateEom(UInt32 messagePgn, UInt16 totalLength, byte destinationAddress, byte sourceAddress)
        {
            return new Msg_TpCm
            {
                Control = TpControl_e.EndOfMsg,
                MessagePgn = messagePgn,
                TotalLength = totalLength,
                TotalPackets = (byte)((totalLength + 6) / 7),
                DestinationAddress = destinationAddress,
                SourceAddress = sourceAddress,
            };
        }

        /// <summary>构造 Abort 报文</summary>
        public static Msg_TpCm CreateAbort(UInt32 messagePgn, TpAbortReason_e reason, byte destinationAddress, byte sourceAddress)
        {
            return new Msg_TpCm
            {
                Control = TpControl_e.Abort,
                MessagePgn = messagePgn,
                AbortReason = reason,
                DestinationAddress = destinationAddress,
                SourceAddress = sourceAddress,
            };
        }

        /// <summary>构造 29bit ID(PDU2,PS = 0xEC;点对点时为 DA,需 PDU1 语义)</summary>
        public J1939Id ToId()
        {
            return new J1939Id
            {
                Priority = Priority,
                PF = 0xEC,
                PS = DestinationAddress,
                SourceAddress = SourceAddress,
            };
        }

        /// <summary>序列化为 8 字节数据区</summary>
        public byte[] ToData()
        {
            var bs = new byte[8];
            bs[0] = (byte)Control;

            switch (Control)
            {
                case TpControl_e.Rts:
                    bs[1] = (byte)TotalLength;
                    bs[2] = (byte)(TotalLength >> 8);
                    bs[3] = TotalPackets;
                    bs[4] = MaxPacketSize;
                    break;

                case TpControl_e.Cts:
                    bs[1] = PacketsToSend;
                    bs[2] = NextPacketNumber;
                    bs[3] = 0xFF;
                    bs[4] = 0xFF;
                    break;

                case TpControl_e.EndOfMsg:
                case TpControl_e.Bam:
                    bs[1] = (byte)TotalLength;
                    bs[2] = (byte)(TotalLength >> 8);
                    bs[3] = TotalPackets;
                    bs[4] = 0xFF;
                    break;

                case TpControl_e.Abort:
                    bs[1] = (byte)AbortReason;
                    bs[2] = 0xFF;
                    bs[3] = 0xFF;
                    bs[4] = 0xFF;
                    break;
            }

            bs[5] = (byte)MessagePgn;
            bs[6] = (byte)(MessagePgn >> 8);
            bs[7] = (byte)(MessagePgn >> 16);

            return bs;
        }

        /// <summary>转为 CAN 帧</summary>
        public CanMsg ToCanMsg()
        {
            return ToId().ToCanMsg(ToData());
        }

        /// <summary>从 CAN 帧解析</summary>
        public static Msg_TpCm Parse(CanMsg msg)
        {
            if (msg == null || msg.Data == null || msg.Data.Length < 8) return null;

            var id = J1939Id.FromCanMsg(msg);
            var d = msg.Data;

            var pkt = new Msg_TpCm
            {
                Control = (TpControl_e)d[0],
                MessagePgn = (UInt32)(d[5] | (d[6] << 8) | (d[7] << 16)),
                DestinationAddress = id.PS,
                SourceAddress = id.SourceAddress,
                Priority = id.Priority,
            };

            switch (pkt.Control)
            {
                case TpControl_e.Rts:
                    pkt.TotalLength = (UInt16)(d[1] | (d[2] << 8));
                    pkt.TotalPackets = d[3];
                    pkt.MaxPacketSize = d[4];
                    break;

                case TpControl_e.Cts:
                    pkt.PacketsToSend = d[1];
                    pkt.NextPacketNumber = d[2];
                    break;

                case TpControl_e.EndOfMsg:
                case TpControl_e.Bam:
                    pkt.TotalLength = (UInt16)(d[1] | (d[2] << 8));
                    pkt.TotalPackets = d[3];
                    break;

                case TpControl_e.Abort:
                    pkt.AbortReason = (TpAbortReason_e)d[1];
                    break;
            }

            return pkt;
        }

        public override String ToString()
        {
            switch (Control)
            {
                case TpControl_e.Rts:
                    return $"TP.CM RTS PGN=0x{MessagePgn:X5} Len={TotalLength} Pkts={TotalPackets} DA=0x{DestinationAddress:X2} SA=0x{SourceAddress:X2}";
                case TpControl_e.Cts:
                    return $"TP.CM CTS PGN=0x{MessagePgn:X5} Count={PacketsToSend} Next={NextPacketNumber} DA=0x{DestinationAddress:X2} SA=0x{SourceAddress:X2}";
                case TpControl_e.EndOfMsg:
                    return $"TP.CM EOM PGN=0x{MessagePgn:X5} Len={TotalLength} Pkts={TotalPackets} DA=0x{DestinationAddress:X2} SA=0x{SourceAddress:X2}";
                case TpControl_e.Bam:
                    return $"TP.CM BAM PGN=0x{MessagePgn:X5} Len={TotalLength} Pkts={TotalPackets} SA=0x{SourceAddress:X2}";
                case TpControl_e.Abort:
                    return $"TP.CM ABORT PGN=0x{MessagePgn:X5} Reason={AbortReason} DA=0x{DestinationAddress:X2} SA=0x{SourceAddress:X2}";
                default:
                    return $"TP.CM Unknown(0x{(byte)Control:X2})";
            }
        }
    }

    #endregion

    #region TP.DT 数据传输

    /// <summary>
    /// TP.DT 数据传输,PGN 60160 (0x00EB00)。
    /// 数据区 8 字节:byte1 为包序号(1~1785),byte2~8 为 7 字节负载数据。
    /// 最后一包不足 7 字节时用 0xFF 填充。
    /// </summary>
    public class Msg_TpDt
    {
        /// <summary>目标 PGN</summary>
        public const UInt32 PGN = J1939Pgn.TpDt;

        /// <summary>每包携带的有效负载字节数</summary>
        public const int PayloadPerPacket = 7;

        /// <summary>包序号,从 1 开始</summary>
        public byte PacketNumber = 1;

        /// <summary>本包负载(最多 7 字节)</summary>
        public byte[] Payload = new byte[PayloadPerPacket];

        /// <summary>目标地址 DA,BAM 时为 0xFF</summary>
        public byte DestinationAddress = J1939Addr.Global;

        /// <summary>源地址 SA</summary>
        public byte SourceAddress = 0;

        /// <summary>优先级,TP.DT 固定为 7</summary>
        public int Priority = 7;

        public Msg_TpDt() { }

        public Msg_TpDt(byte packetNumber, byte[] payload, byte da, byte sa)
        {
            PacketNumber = packetNumber;
            DestinationAddress = da;
            SourceAddress = sa;

            if (payload != null)
            {
                var len = Math.Min(payload.Length, PayloadPerPacket);
                for (int i = 0; i < len; i++) Payload[i] = payload[i];
            }
        }

        /// <summary>从一段数据中切分出第 n 包(n 从 1 开始)</summary>
        public static Msg_TpDt FromChunk(byte[] data, int packetNumber, byte da, byte sa)
        {
            var offset = (packetNumber - 1) * PayloadPerPacket;
            var bs = new byte[PayloadPerPacket];
            for (int i = 0; i < PayloadPerPacket; i++)
            {
                var idx = offset + i;
                bs[i] = idx < data.Length ? data[idx] : (byte)0xFF;
            }

            return new Msg_TpDt((byte)packetNumber, bs, da, sa);
        }

        /// <summary>构造 29bit ID</summary>
        public J1939Id ToId()
        {
            return new J1939Id
            {
                Priority = Priority,
                PF = 0xEB,
                PS = DestinationAddress,
                SourceAddress = SourceAddress,
            };
        }

        /// <summary>序列化为 8 字节数据区</summary>
        public byte[] ToData()
        {
            var bs = new byte[8];
            bs[0] = PacketNumber;
            for (int i = 0; i < PayloadPerPacket; i++) bs[i + 1] = Payload[i];
            return bs;
        }

        /// <summary>转为 CAN 帧</summary>
        public CanMsg ToCanMsg()
        {
            return ToId().ToCanMsg(ToData());
        }

        /// <summary>从 CAN 帧解析</summary>
        public static Msg_TpDt Parse(CanMsg msg)
        {
            if (msg == null || msg.Data == null || msg.Data.Length < 8) return null;

            var id = J1939Id.FromCanMsg(msg);
            var pkt = new Msg_TpDt
            {
                PacketNumber = msg.Data[0],
                DestinationAddress = id.PS,
                SourceAddress = id.SourceAddress,
                Priority = id.Priority,
            };

            for (int i = 0; i < PayloadPerPacket; i++) pkt.Payload[i] = msg.Data[i + 1];

            return pkt;
        }

        public override String ToString()
        {
            return $"TP.DT #{PacketNumber} DA=0x{DestinationAddress:X2} SA=0x{SourceAddress:X2} [{Payload.ToHex(" ")}]";
        }
    }

    #endregion

    #region TP 接收侧重组

    /// <summary>
    /// 完成一次 TP 多包传输后的结果
    /// </summary>
    public class J1939TpResult
    {
        /// <summary>原始报文 PGN</summary>
        public UInt32 MessagePgn;

        /// <summary>源地址</summary>
        public byte SourceAddress;

        /// <summary>拼接完成的完整数据</summary>
        public byte[] Data;
    }

    /// <summary>
    /// J1939 传输协议(TP)接收侧重组件。
    /// 处理 TP.CM(BAM/RTS) 与 TP.DT,按「源地址 + 目标 PGN」缓存并拼包,
    /// 拼满后触发 <see cref="Completed"/>。
    /// 用法:把收到的每一帧都喂给 <see cref="Process"/>。
    /// </summary>
    public class J1939TpReassembler
    {
        /// <summary>单包负载字节数</summary>
        public const int PayloadPerPacket = 7;

        /// <summary>单次拼包会话</summary>
        class Session
        {
            public byte SourceAddress;
            public byte DestinationAddress;
            public UInt32 MessagePgn;
            public int TotalLength;
            public int TotalPackets;
            public int NextPacket;          // 期望收到的下一包序号
            public byte[] Buffer;
            public DateTime LastTick;
        }

        readonly Dictionary<String, Session> _sessions = new Dictionary<String, Session>();
        readonly Object _lock = new Object();

        /// <summary>会话超时时间(毫秒),超时自动丢弃</summary>
        public int TimeoutMs = 1000;

        /// <summary>拼包完成事件</summary>
        public event EventHandler<J1939TpResult> Completed;

        /// <summary>异常/中止事件</summary>
        public event EventHandler<String> Aborted;

        static String KeyOf(byte sa, byte da, UInt32 pgn) => $"{sa:X2}-{da:X2}-{pgn:X5}";

        /// <summary>
        /// 处理一帧。若是 TP 相关帧则纳入拼包流程,返回到目前为止是否处理了该帧。
        /// </summary>
        public bool Process(CanMsg msg)
        {
            if (msg == null) return false;

            var id = J1939Id.FromCanMsg(msg);
            var pgn = id.PGNValue;
            var data = msg.Data;
            if (data == null || data.Length < 1) return false;

            if (pgn == J1939Pgn.TpCm) return ProcessTpCm(id, data);
            if (pgn == J1939Pgn.TpDt) return ProcessTpDt(id, data);

            return false;
        }

        bool ProcessTpCm(J1939Id id, byte[] data)
        {
            var cm = Msg_TpCm.Parse(new CanMsg((Int32)id.ID, data, false, true));
            if (cm == null) return false;

            lock (_lock)
            {
                Cleanup();

                var key = KeyOf(cm.SourceAddress, cm.DestinationAddress, cm.MessagePgn);

                switch (cm.Control)
                {
                    case TpControl_e.Bam:
                    case TpControl_e.Rts:
                        _sessions[key] = new Session
                        {
                            SourceAddress = cm.SourceAddress,
                            DestinationAddress = cm.DestinationAddress,
                            MessagePgn = cm.MessagePgn,
                            TotalLength = cm.TotalLength,
                            TotalPackets = cm.TotalPackets,
                            NextPacket = 1,
                            Buffer = new byte[cm.TotalLength],
                            LastTick = DateTime.Now,
                        };
                        break;

                    case TpControl_e.EndOfMsg:
                        // 发送方正常结束
                        _sessions.Remove(key);
                        break;

                    case TpControl_e.Abort:
                        _sessions.Remove(key);
                        Aborted?.Invoke(this, $"TP 中止: PGN=0x{cm.MessagePgn:X5} SA=0x{cm.SourceAddress:X2} 原因={cm.AbortReason}");
                        break;
                }
            }

            return true;
        }

        bool ProcessTpDt(J1939Id id, byte[] data)
        {
            lock (_lock)
            {
                Cleanup();
                foreach (var kv in _sessions.ToArray())
                {
                    var s = kv.Value;
                    if (s.SourceAddress != id.SourceAddress) continue;

                    var seq = data[0];
                    if (seq != s.NextPacket) continue;

                    // 拷贝 7 字节有效负载
                    var offset = (seq - 1) * PayloadPerPacket;
                    for (int i = 0; i < PayloadPerPacket; i++)
                    {
                        var idx = offset + i;
                        if (idx < s.Buffer.Length) s.Buffer[idx] = data[i + 1];
                    }

                    s.NextPacket++;
                    s.LastTick = DateTime.Now;

                    // 全部收齐
                    if (s.NextPacket > s.TotalPackets)
                    {
                        var result = new J1939TpResult
                        {
                            MessagePgn = s.MessagePgn,
                            SourceAddress = s.SourceAddress,
                            Data = s.Buffer,
                        };
                        _sessions.Remove(kv.Key);
                        Completed?.Invoke(this, result);
                    }

                    return true;
                }
            }

            return false;
        }

        /// <summary>清理超时会话</summary>
        void Cleanup()
        {
            var now = DateTime.Now;
            foreach (var kv in _sessions.ToArray())
            {
                if ((now - kv.Value.LastTick).TotalMilliseconds > TimeoutMs)
                {
                    _sessions.Remove(kv.Key);
                    Aborted?.Invoke(this, $"TP 超时: PGN=0x{kv.Value.MessagePgn:X5} SA=0x{kv.Value.SourceAddress:X2}");
                }
            }
        }

        /// <summary>清空所有会话</summary>
        public void Clear()
        {
            lock (_lock) _sessions.Clear();
        }
    }

    #endregion

    #region TP 发送侧

    /// <summary>
    /// TP 发送结果
    /// </summary>
    public enum TpSendState_e
    {
        /// <summary>空闲</summary>
        Idle = 0,

        /// <summary>发送中</summary>
        Sending = 1,

        /// <summary>发送完成</summary>
        Ok = 2,

        /// <summary>忙(已有会话进行中)</summary>
        Busy = 3,

        /// <summary>超时失败</summary>
        Timeout = 4,

        /// <summary>被对方中止</summary>
        Aborted = 5,

        /// <summary>参数非法</summary>
        Error = 6,
    }

    /// <summary>
    /// J1939 传输协议(TP)发送侧组件。
    /// 支持两种模式:
    /// <list type="bullet">
    /// <item>BAM 广播:无需对方应答,按 50ms 间隔主动发完所有 TP.DT。</item>
    /// <item>RTS/CTS 点对点:发送 RTS 后等待对方 CTS,按 CTS 给定的「包数 + 起始序号」发送 TP.DT,
    ///       全部发完后再等对方的 EOM 确认。</item>
    /// </list>
    /// 线程模型:<see cref="SendBam"/> / <see cref="SendRts"/> 会在**调用线程上阻塞**直到完成或超时;
    /// 因此调用方必须在**非接收线程**上发起(例如业务/UI 线程),
    /// 而接收线程需把收到的帧喂给 <see cref="Process"/> 以推进状态机。
    /// BAM 模式全在调用线程完成,不依赖 <see cref="Process"/>。
    /// 用法:
    /// <list type="number">
    /// <item><see cref="Bind"/> 绑定 CAN 端口。</item>
    /// <item>接收线程把每帧喂给 <see cref="Process"/>。</item>
    /// <item>业务线程调用 <see cref="SendBam"/> 或 <see cref="SendRts"/> 发起传输。</item>
    /// </list>
    /// </summary>
    public class J1939TpSender
    {
        /// <summary>单包负载字节数</summary>
        public const int PayloadPerPacket = 7;

        /// <summary>BAM 模式下每包之间的最小间隔(毫秒),协议要求 50ms 左右</summary>
        public int BamIntervalMs = 50;

        /// <summary>等待对方 CTS / EOM 的超时时间(毫秒)</summary>
        public int ReceiveTimeoutMs = 1000;

        /// <summary>单次 CTS 允许发送的最大包数(协议 1~16,常用 16)</summary>
        public byte MaxPacketsPerCts = 16;

        /// <summary>发送状态</summary>
        public TpSendState_e State { get; private set; } = TpSendState_e.Idle;

        /// <summary>待发送的完整数据</summary>
        byte[] _data;

        /// <summary>当前报文 PGN</summary>
        UInt32 _messagePgn;

        /// <summary>目标地址 DA,BAM 为 0xFF</summary>
        byte _destinationAddress;

        /// <summary>本机源地址</summary>
        byte _sourceAddress;

        /// <summary>是否使用 RTS/CTS 点对点模式</summary>
        bool _useRts;

        /// <summary>下一包待发序号(1 起始)</summary>
        int _nextPacket;

        /// <summary>总包数</summary>
        int _totalPackets;

        /// <summary>最近一次收发时刻,用于超时判定</summary>
        DateTime _lastTick;

        /// <summary>完成信号量,RTS 模式阻塞等待用</summary>
        readonly System.Threading.ManualResetEventSlim _done = new System.Threading.ManualResetEventSlim(false);

        readonly Object _lock = new Object();

        /// <summary>当前使用的 CAN 端口</summary>
        ICanPort _transport;

        /// <summary>发送完成/失败事件</summary>
        public event EventHandler<TpSendState_e> Finished;

        /// <summary>绑定 CAN 端口</summary>
        public void Bind(ICanPort transport) => _transport = transport;

        /// <summary>发起一次 BAM 广播传输(阻塞直到发完)</summary>
        /// <param name="messagePgn">被传输报文的 PGN</param>
        /// <param name="data">完整数据</param>
        /// <param name="sourceAddress">本机源地址</param>
        public TpSendState_e SendBam(UInt32 messagePgn, byte[] data, byte sourceAddress)
        {
            return Send(messagePgn, data, sourceAddress, J1939Addr.Global, false);
        }

        /// <summary>发起一次 BAM 广播传输(阻塞直到发完),并绑定端口</summary>
        public TpSendState_e SendBam(ICanPort transport, UInt32 messagePgn, byte[] data, byte sourceAddress)
        {
            Bind(transport);
            return Send(messagePgn, data, sourceAddress, J1939Addr.Global, false);
        }

        /// <summary>发起一次 RTS/CTS 点对点传输(阻塞直到完成或超时)</summary>
        public TpSendState_e SendRts(ICanPort transport, UInt32 messagePgn, byte[] data, byte sourceAddress, byte destinationAddress)
        {
            Bind(transport);
            return Send(messagePgn, data, sourceAddress, destinationAddress, true);
        }

        TpSendState_e Send(UInt32 messagePgn, byte[] data, byte sourceAddress, byte destinationAddress, bool useRts)
        {
            lock (_lock)
            {
                if (State == TpSendState_e.Sending) return TpSendState_e.Busy;
                if (_transport == null || data == null || data.Length == 0 || data.Length > 1785) return TpSendState_e.Error;

                _data = data;
                _messagePgn = messagePgn;
                _sourceAddress = sourceAddress;
                _destinationAddress = destinationAddress;
                _useRts = useRts;
                _totalPackets = (data.Length + PayloadPerPacket - 1) / PayloadPerPacket;
                _nextPacket = 1;
                _lastTick = DateTime.Now;
                _done.Reset();
                State = TpSendState_e.Sending;
            }

            try
            {
                if (useRts) SendRtsCore();
                else SendBamCore();

                var res = State;
                Finished?.Invoke(this, res);
                return res;
            }
            finally
            {
                lock (_lock)
                {
                    if (State == TpSendState_e.Sending) State = TpSendState_e.Idle;
                }
            }
        }

        /// <summary>BAM:无需应答,按序发完所有 TP.DT</summary>
        void SendBamCore()
        {
            var cm = Msg_TpCm.CreateBam(_messagePgn, (UInt16)_data.Length, _sourceAddress);
            _transport.Send(cm.ToCanMsg());

            for (int seq = 1; seq <= _totalPackets; seq++)
            {
                var dt = Msg_TpDt.FromChunk(_data, seq, J1939Addr.Global, _sourceAddress);
                _transport.Send(dt.ToCanMsg());
                System.Threading.Thread.Sleep(BamIntervalMs);
            }

            lock (_lock) State = TpSendState_e.Ok;
        }

        /// <summary>RTS/CTS:发 RTS → 等 CTS(可能多轮)→ 发 DT → 等 EOM</summary>
        void SendRtsCore()
        {
            // 1. 发 RTS
            var rts = Msg_TpCm.CreateRts(_messagePgn, (UInt16)_data.Length, _destinationAddress, _sourceAddress);
            _transport.Send(rts.ToCanMsg());

            // 2. 轮询等待状态机推进(Process 由接收线程调用)。
            //    用小块超时轮询而非一次性死等,便于会话超时后发 Abort。
            var deadline = DateTime.Now.AddMilliseconds(ReceiveTimeoutMs);
            while (DateTime.Now < deadline)
            {
                var st = State;
                if (st != TpSendState_e.Sending) return;

                _done.Wait(10);
                _done.Reset();

                // 数据已全部发完,只剩等 EOM
                if (_nextPacket > _totalPackets)
                {
                    // 继续等 EOM
                }
            }

            lock (_lock)
            {
                if (State == TpSendState_e.Sending)
                {
                    var abort = Msg_TpCm.CreateAbort(_messagePgn, TpAbortReason_e.Timeout, _destinationAddress, _sourceAddress);
                    _transport.Send(abort.ToCanMsg());
                    State = TpSendState_e.Timeout;
                }
            }
        }

        /// <summary>
        /// 处理接收到的帧,推进 RTS/CTS 状态机。
        /// 返回是否为本组件相关帧(TP.CM 的 CTS/EOM/Abort)。
        /// </summary>
        public bool Process(CanMsg msg)
        {
            if (msg == null) return false;
            if (State != TpSendState_e.Sending) return false;
            if (!_useRts) return false;

            var id = J1939Id.FromCanMsg(msg);
            if (id.PGNValue != J1939Pgn.TpCm) return false;

            var cm = Msg_TpCm.Parse(msg);
            if (cm == null) return false;
            if (cm.MessagePgn != _messagePgn) return false;
            if (cm.SourceAddress != _destinationAddress) return false;

            _lastTick = DateTime.Now;

            switch (cm.Control)
            {
                case TpControl_e.Cts:
                    // 对方允许发送若干包,从指定序号开始
                    SendWindow(cm.PacketsToSend, cm.NextPacketNumber);
                    _done.Set();
                    return true;

                case TpControl_e.EndOfMsg:
                    // 对方确认收齐
                    lock (_lock) State = TpSendState_e.Ok;
                    _done.Set();
                    return true;

                case TpControl_e.Abort:
                    lock (_lock) State = TpSendState_e.Aborted;
                    _done.Set();
                    return true;
            }

            return false;
        }

        /// <summary>按 CTS 许可发送一个窗口的 TP.DT</summary>
        void SendWindow(byte packetsToSend, byte nextPacketNumber)
        {
            // CTS 的包数 0 表示请求停止发送
            if (packetsToSend == 0)
            {
                lock (_lock) State = TpSendState_e.Aborted;
                return;
            }

            var start = nextPacketNumber;
            var count = Math.Min(packetsToSend, MaxPacketsPerCts);

            for (int i = 0; i < count; i++)
            {
                var seq = start + i;
                if (seq < 1 || seq > _totalPackets) break;

                var dt = Msg_TpDt.FromChunk(_data, seq, _destinationAddress, _sourceAddress);
                _transport?.Send(dt.ToCanMsg());
                _nextPacket = seq + 1;
            }
        }

        /// <summary>中止当前传输并通知对方</summary>
        public void Abort(TpAbortReason_e reason = TpAbortReason_e.AbortConnection)
        {
            lock (_lock)
            {
                if (State != TpSendState_e.Sending) return;

                var abort = Msg_TpCm.CreateAbort(_messagePgn, reason, _destinationAddress, _sourceAddress);
                _transport?.Send(abort.ToCanMsg());

                State = TpSendState_e.Aborted;
            }

            _done.Set();
        }

        /// <summary>释放资源</summary>
        public void Close()
        {
            _done.Set();
            _done.Dispose();
        }
    }

    #endregion

    #region TP 门面(每端口单例)

    /// <summary>
    /// J1939 传输协议(TP)门面类。
    /// 把「接收侧重组」与「发送侧会话」合为一个 <see cref="ICanRx"/> 处理器,
    /// 构造时自动注册到指定 <see cref="ICanPort"/>,并在该端口上按 key 挂载为端口级单例:
    /// <code>
    /// var tp = port.GetOrAdd(J1939Tp.KeyItems, () => new J1939Tp(port));
    /// tp.Completed += (s, e) => { /* 收到完整大包 */ };
    /// tp.SendBam(0x00FECA, data, sa: 0x00);
    /// </code>
    /// 线程模型:
    /// <list type="bullet">
    /// <item><see cref="Process"/> 由接收线程回调,负责收包重组与推进发送侧 CTS/EOM 状态机。</item>
    /// <item><see cref="SendBam"/> / <see cref="SendRts"/> 在调用线程阻塞,
    ///       **禁止在接收线程或 <see cref="Completed"/> 事件中调用**,否则会死锁。</item>
    /// </list>
    /// </summary>
    public class J1939Tp : ICanRx
    {
        /// <summary>在 <see cref="ICanPort"/> 上挂载本对象所使用的字典键</summary>
        public const String KeyItems = "J1939Tp";

        /// <summary>本对象所属的 CAN 端口</summary>
        public ICanPort Port { get; }

        /// <summary>接收侧重组器</summary>
        public J1939TpReassembler Reassembler { get; }

        /// <summary>发送侧会话管理器</summary>
        public J1939TpSender Sender { get; }

        /// <summary>
        /// 构造并注册到端口。
        /// 建议通过 <c>port.GetOrAdd(J1939Tp.KeyItems, () =&gt; new J1939Tp(port))</c> 获取,
        /// 以保证同一端口只有一个实例。
        /// </summary>
        /// <param name="port">CAN 端口</param>
        public J1939Tp(ICanPort port)
        {
            if (port == null) throw new ArgumentNullException(nameof(port));

            Port = port;

            // 接收侧:TP.CM + TP.DT
            Reassembler = new J1939TpReassembler();
            // 转发重组事件(含完整大包与中止/超时)
            Reassembler.Completed += (s, e) => Completed?.Invoke(this, e);
            Reassembler.Aborted += (s, e) => Aborted?.Invoke(this, e);

            // 发送侧:绑定端口,其 Process(CanMsg) 由本门面的 Process 转发
            Sender = new J1939TpSender();
            Sender.Bind(port);

            // 端口不再做 CAN ID 匹配,本处理器会收到该端口所有帧,
            // 由 Process 内部按 PGN 过滤 TP.CM / TP.DT。
            if (!port.Register(this)) throw new InvalidOperationException(L.T("This port has already registered a J1939 TP handler."));
        }

        /// <summary>收到完整 TP 多包报文(重组完成)</summary>
        public event EventHandler<J1939TpResult> Completed;

        /// <summary>TP 中止 / 超时事件</summary>
        public event EventHandler<String> Aborted;

        /// <summary>
        /// 接收线程回调:把帧分发给接收侧重组器与发送侧状态机。
        /// 仅当确实属于本机制(TP.CM / TP.DT)时返回 true,避免端口写"未识别"日志。
        /// </summary>
        public override bool Process(CanMsg msg, ICanPort port)
        {
            if (msg == null) return false;

            var id = J1939Id.FromCanMsg(msg);
            var pgn = id.PGNValue;

            // 非 TP 帧:不处理(可能被端口日志记录,属正常)
            if (pgn != J1939Pgn.TpCm && pgn != J1939Pgn.TpDt) return false;

            // 发送侧优先:我们在等 CTS/EOM/Abort 时,先让发送侧状态机消费
            if (Sender.Process(msg)) return true;

            // 否则作为接收侧(对方向我们发起 RTS/BAM,或发来 TP.DT)
            return Reassembler.Process(msg);
        }

        /// <summary>
        /// 快速预匹配:仅当帧为 29bit 扩展帧且 PGN 为 TP.CM(0x00EC00) 或 TP.DT(0x00EB00) 时才认领,
        /// 供端口层在分发前过滤掉与本机制无关的帧(如 UDS / CANopen / 普通 J1939 报文),
        /// 避免 J1939Tp 对端口上每一帧都做 <see cref="J1939Id.FromCanMsg"/> 全量解析。
        /// 最终是否消费仍以 <see cref="Process"/> 为准。
        /// </summary>
        public override bool Match(CanMsg msg)
        {
            if (msg == null || !msg.ExternFlag) return false;   // J1939 固定使用 29bit 扩展帧

            // 直接由 29bit ID 计算 18bit PGN(与 J1939Id.PGNValue 等价),避免额外对象分配
            var pgn = J1939Id.GetPgn((UInt32)msg.ID);

            return pgn == J1939Pgn.TpCm || pgn == J1939Pgn.TpDt;
        }

        /// <summary>发起一次 BAM 广播传输(阻塞)</summary>
        /// <param name="messagePgn">被传输报文的 PGN</param>
        /// <param name="data">完整数据</param>
        /// <param name="sourceAddress">本机源地址</param>
        public TpSendState_e SendBam(UInt32 messagePgn, byte[] data, byte sourceAddress)
        {
            return Sender.SendBam(messagePgn, data, sourceAddress);
        }

        /// <summary>发起一次 RTS/CTS 点对点传输(阻塞)</summary>
        /// <param name="messagePgn">被传输报文的 PGN</param>
        /// <param name="data">完整数据</param>
        /// <param name="sourceAddress">本机源地址</param>
        /// <param name="destinationAddress">目标地址</param>
        public TpSendState_e SendRts(UInt32 messagePgn, byte[] data, byte sourceAddress, byte destinationAddress)
        {
            return Sender.SendRts(Port, messagePgn, data, sourceAddress, destinationAddress);
        }

        /// <summary>当前发送状态</summary>
        public TpSendState_e State => Sender.State;

        /// <summary>接收侧会话超时时间(毫秒)</summary>
        public int TimeoutMs
        {
            get => Reassembler.TimeoutMs;
            set => Reassembler.TimeoutMs = value;
        }

        /// <summary>BAM 每包发送间隔(毫秒)</summary>
        public int BamIntervalMs
        {
            get => Sender.BamIntervalMs;
            set => Sender.BamIntervalMs = value;
        }

        /// <summary>从端口注销并释放资源</summary>
        public void Close()
        {
            try { Port?.Unregister(this); } catch { }
            try { Port?.RemoveItem(KeyItems); } catch { }

            Sender?.Close();
            Reassembler?.Clear();
        }
    }

    #endregion
}