honey_the_codewitch

EspSerialSession w/ ARQ (htcw_frame_arq)

Jul 7th, 2026 (edited)
78
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 47.80 KB | None | 0 0
  1. #pragma warning disable CS0649
  2. using Microsoft.Win32.SafeHandles;
  3. using System.Buffers.Binary;
  4. using System.ComponentModel;
  5. using System.Runtime.CompilerServices;
  6. using System.Runtime.InteropServices;
  7. using System.Runtime.Versioning;
  8.  
  9. namespace Espmon;
  10. public sealed class PortEntry
  11. {
  12.     public PortEntry(string portName, string serialNumber)
  13.     {
  14.         PortName = portName;
  15.         SerialNumber = serialNumber;
  16.     }
  17.     public string PortName { get; }
  18.     public string SerialNumber { get; }
  19.  
  20. }
  21. public sealed class FrameReceivedEventArgs : EventArgs
  22. {
  23.     public byte Command;
  24.     public byte[] Data { get; }
  25.  
  26.     public FrameReceivedEventArgs(byte command, byte[] data)
  27.     {
  28.         ArgumentOutOfRangeException.ThrowIfGreaterThan(command, 127, nameof(command));
  29.         Command = command;
  30.         Data = data;
  31.     }
  32. }
  33. public sealed class FrameErrorEventArgs : EventArgs
  34. {
  35.     public byte Command { get; }
  36.     public byte Seq { get; }
  37.     public int Attempts { get; }   // initial send + resends made before giving up
  38.     public FrameErrorEventArgs(byte command, byte seq, int attempts)
  39.     { Command = command; Seq = seq; Attempts = attempts; }
  40. }
  41.  
  42. public sealed class ResendRequestedEventArgs : EventArgs
  43. {
  44.     public byte Command { get; }
  45.     public byte Seq { get; }
  46.     public ResendRequestedEventArgs(byte command, byte seq)
  47.     { Command = command; Seq = seq; }
  48. }
  49. [SupportedOSPlatform("windows")]
  50. internal partial class EspSerialSession : IDisposable
  51. {
  52.     // Strong handle to 'this', round-tripped through the native Context token. Holding it
  53.     // keeps the instance alive while registered; freed on unregister. Not pinned — the
  54.     // token from ToIntPtr is opaque, not a pointer into the object.
  55.     GCHandle _powerCallbackHandle;
  56.  
  57.     struct StateMachine
  58.     {
  59.         int state;
  60.         byte rawCmd;
  61.         byte rawSeq;     // seq/type byte
  62.         uint rawLen;
  63.         uint rawCrc;
  64.  
  65.         public byte RawCommandByte => rawCmd;
  66.         public byte RawSeqByte => rawSeq;
  67.         public byte Command => (byte)(rawCmd - 128);   // 0 for control frames
  68.         public int FrameType => (rawSeq >> 6) & 0x03;   // 0=DATA 1=ACK 2=NACK
  69.         public byte Seq => (byte)(rawSeq & 0x3F);
  70.         public bool IsControl => rawCmd == 128 || FrameType != 0;
  71.         public int Length => (int)rawLen;
  72.         public uint Crc => rawCrc;
  73.         public bool IsDone => state == 17;
  74.  
  75.         public void Reset() => state = 0;
  76.  
  77.         public bool Step(List<byte>? log, object? logLock, byte data)
  78.         {
  79.             if (state == 17) state = 0;         // previous frame consumed; start fresh
  80.  
  81.             if (state == 0)
  82.             {
  83.                 if (data < 128)                 // sub-128 byte = transport-level log text
  84.                 {
  85.                     if (logLock != null) lock (logLock) { log?.Add(data); }
  86.                     return false;
  87.                 }
  88.                 state = 1;
  89.                 rawCmd = data;
  90.                 rawSeq = 0; rawLen = 0; rawCrc = 0;
  91.             }
  92.             else if (state < 8)                 // remaining 7 marker bytes must match
  93.             {
  94.                 if (rawCmd != data)
  95.                 {
  96.                     if (logLock != null) lock (logLock)
  97.                     {
  98.                         for (var i = 0; i < state; ++i) log?.Add(rawCmd);
  99.                         log?.Add(data);
  100.                     }
  101.                     state = 0;
  102.                     return false;
  103.                 }
  104.                 ++state;
  105.             }
  106.             else if (state == 8) { rawSeq = data; ++state; }                       // seq/type
  107.             else if (state < 13) { rawLen |= (uint)data << (8 * (state - 9)); ++state; } // len LE
  108.             else /* state<17 */   { rawCrc |= (uint)data << (8 * (state - 13)); ++state; } // crc LE
  109.             return true;
  110.         }
  111.     }
  112.  
  113.     /// <summary>
  114.     /// Captures the state for one pending overlapped I/O so the
  115.     /// IOCallback can resolve the right TaskCompletionSource.
  116.     /// </summary>
  117.     private sealed class ReadOp
  118.     {
  119.         public TaskCompletionSource<int> Tcs = null!;
  120.         public unsafe NativeOverlapped* Overlapped;
  121.     }
  122.  
  123.     private sealed class CommEventOp
  124.     {
  125.         public TaskCompletionSource<int> Tcs = null!;
  126.         public unsafe NativeOverlapped* Overlapped;
  127.         public GCHandle MaskHandle;
  128.     }
  129.  
  130.     volatile bool _closing;
  131.     volatile bool _connErrorFired;
  132.     string _portName;
  133.     bool _disposed;
  134.     SafeFileHandle? _handle;
  135.     ThreadPoolBoundHandle? _boundHandle;
  136.     readonly List<byte> _log;
  137.     readonly object _logLock;
  138.     readonly object _ioLock;
  139.     readonly object _closeLock;
  140.     bool _logging;
  141.     SynchronizationContext? _sync;
  142.     Task? _readTask, _statTask;
  143.     IntPtr _powerNotifyHandle;
  144.     readonly byte[] _rx = new byte[4096];
  145.     int _rxHead;   // next unread byte in _rx
  146.     int _rxTail;   // number of valid bytes in _rx
  147.     bool _deliveryNotified;   // add alongside the other ARQ fields
  148.  
  149.     const int FrameHeaderLength = 17;          // 8 marker + 1 seq/type + 4 len + 4 crc
  150.     const int MaxFrameLength = 32768;        // read bound; over this we NACK+resync
  151.     const int TypeData = 0, TypeAck = 1, TypeNack = 2;
  152.  
  153.     readonly object _arq = new object();   // guards the ARQ state below
  154.     readonly object _sendLock = new object();   // keeps one frame contiguous on the wire
  155.  
  156.     byte _txSeq = 0x3F;                  // last DATA seq sent; first send -> 0
  157.     byte _expectedRxSeq = 0;                     // next DATA seq we expect to receive
  158.     bool _awaiting;                             // a sent DATA frame is unacked
  159.     byte[]? _retain;                            // last DATA frame bytes, for resend
  160.                                                 // chunk 2 also adds: _ackTimeoutMs, _maxRetries, _retries, _sendQueue, _ackTimer
  161.     static readonly uint[] _crcTable = BuildCrcTable();
  162.     int _ackTimeoutMs;                 // -1 = explicit mode: NACK -> event, no timer, no auto-give-up
  163.     int _maxRetries = 5;
  164.     int _retries;                      // timeout-driven resends so far (NACKs never counted)
  165.     readonly Queue<(byte cmd, byte[] data)> _sendQueue = new();
  166.     System.Threading.Timer? _ackTimer;
  167.  
  168.     public event EventHandler<ResendRequestedEventArgs>? ResendRequested;  // explicit mode only
  169.     public event EventHandler<EventArgs>? ConnectionError;
  170.     public event EventHandler<FrameReceivedEventArgs>? FrameReceived;
  171.     public event EventHandler<FrameErrorEventArgs>? FrameError;
  172.    
  173.     /// <summary>
  174.     /// Enumerates present COM ports via SetupAPI and returns each port's name
  175.     /// (e.g. "COM3") paired with the trailing segment of its device instance ID,
  176.     /// which for USB devices is the descriptor serial number reported by the
  177.     /// device (or a Windows-synthesized instance ID when the device has none).
  178.     ///
  179.     /// Replacement for the former Win32_PnPEntity WMI query. AOT/trim clean:
  180.     /// plain P/Invoke over blittable signatures, no COM, no WMI. Output parity
  181.     /// with the old implementation is preserved:
  182.     ///   - PortName is read from the device's hardware key ("PortName" value),
  183.     ///     which yields the same "COMx" string the old code parsed out of the
  184.     ///     friendly name's parentheses, without depending on that string format.
  185.     ///   - SerialNumber is the substring after the final '\' of the instance ID,
  186.     ///     identical to the old DeviceID.Substring(LastIndexOf('\\') + 1).
  187.     /// </summary>
  188.     public static unsafe PortEntry[] GetPorts()
  189.     {
  190.         var result = new List<PortEntry>();
  191.  
  192.         IntPtr set;
  193.         fixed (Guid* g = &GUID_DEVCLASS_PORTS)
  194.             set = SetupDiGetClassDevsW(g, null, IntPtr.Zero, DIGCF_PRESENT);
  195.  
  196.         if (set == INVALID_HANDLE_VALUE)
  197.             return Array.Empty<PortEntry>();
  198.  
  199.         try
  200.         {
  201.             var dev = new SP_DEVINFO_DATA { cbSize = (uint)sizeof(SP_DEVINFO_DATA) };
  202.  
  203.             // Allocate ONCE and reuse every iteration. stackalloc lifetime is the whole
  204.             // method frame, not the loop body, so a per-iteration stackalloc accumulates
  205.             // on the frame and can overflow the stack when many ports are present. Each
  206.             // native call rewrites (and null-terminates) its buffer, so reuse is safe.
  207.             const int idCap = 512;
  208.             const int nameCap = 64;
  209.             char* idBuf = stackalloc char[idCap];
  210.             char* nameBuf = stackalloc char[nameCap];
  211.  
  212.             for (uint i = 0; ; i++)
  213.             {
  214.                 if (!SetupDiEnumDeviceInfo(set, i, &dev))
  215.                 {
  216.                     if (Marshal.GetLastWin32Error() == ERROR_NO_MORE_ITEMS)
  217.                         break;
  218.                     continue; // skip a transient failure on this index
  219.                 }
  220.  
  221.                 // --- device instance id -> serial (trailing segment) ---
  222.                 uint required = 0;
  223.                 if (!SetupDiGetDeviceInstanceIdW(set, &dev, idBuf, idCap, &required))
  224.                     continue;
  225.  
  226.                 var instanceId = new string(idBuf); // read to the id's own NUL terminator
  227.                 int slash = instanceId.LastIndexOf('\\');
  228.                 string serial = slash >= 0 ? instanceId[(slash + 1)..] : instanceId;
  229.  
  230.                 // --- PortName from the device's hardware key (e.g. "COM3") ---
  231.                 IntPtr hKey = SetupDiOpenDevRegKey(
  232.                     set, &dev, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ);
  233.                 if (hKey == INVALID_HANDLE_VALUE)
  234.                     continue;
  235.  
  236.                 try
  237.                 {
  238.                     uint cb = nameCap * sizeof(char); // in/out byte count
  239.                     uint type = 0;
  240.  
  241.                     int rc;
  242.                     fixed (char* name = "PortName")
  243.                         rc = RegQueryValueExW(hKey, name, IntPtr.Zero, &type, (byte*)nameBuf, &cb);
  244.  
  245.                     if (rc != ERROR_SUCCESS) // includes ERROR_MORE_DATA if it didn't fit
  246.                         continue;
  247.  
  248.                     int chars = (int)(cb / sizeof(char));
  249.                     if (chars > nameCap) chars = nameCap; // defensive clamp
  250.                     while (chars > 0 && nameBuf[chars - 1] == '\0') // REG_SZ may count a NUL
  251.                         chars--;
  252.  
  253.                     // explicit length -> stale trailing chars from a prior iteration are ignored
  254.                     var portName = new string(nameBuf, 0, chars);
  255.  
  256.                     // Class "Ports" also covers LPT; keep COM only (old code did too).
  257.                     if (!portName.StartsWith("COM", StringComparison.OrdinalIgnoreCase))
  258.                         continue;
  259.  
  260.                     result.Add(new PortEntry(portName, serial));
  261.                 }
  262.                 finally
  263.                 {
  264.                     RegCloseKey(hKey);
  265.                 }
  266.             }
  267.         }
  268.         finally
  269.         {
  270.             SetupDiDestroyDeviceInfoList(set);
  271.         }
  272.  
  273.         return result.ToArray();
  274.     }
  275.     protected virtual void Dispose(bool disposing)
  276.     {
  277.         if (!_disposed)
  278.         {
  279.             if (disposing)
  280.             {
  281.                 Close();
  282.                 _ackTimer?.Dispose();
  283.                 _ackTimer = null;
  284.                 lock (_arq) { _awaiting = false; _retries = 0; _sendQueue.Clear(); }
  285.             }
  286.             // Also runs on the finalizer path (disposing == false), where
  287.             // Close() is not called. Only touches the IntPtr handle, so it is
  288.             // finalizer-safe, and prevents the callback delegate from being
  289.             // collected while the native registration is still live.
  290.             UnregisterPowerNotification();
  291.             _disposed = true;
  292.         }
  293.     }
  294.  
  295.     ~EspSerialSession()
  296.     {
  297.         Dispose(false);
  298.     }
  299.  
  300.     void IDisposable.Dispose()
  301.     {
  302.         Dispose(disposing: true);
  303.         GC.SuppressFinalize(this);
  304.     }
  305.     byte[] BuildDataFrame(byte cmd, byte seq, ReadOnlySpan<byte> payload)
  306.     {
  307.         byte marker = (byte)(cmd + 128);
  308.         byte seqByte = (byte)(seq & 0x3F);        // TypeData (0) in the high bits
  309.         var frame = new byte[FrameHeaderLength + payload.Length];
  310.         for (int i = 0; i < 8; ++i) frame[i] = marker;
  311.         frame[8] = seqByte;
  312.         BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(9, 4), payload.Length);
  313.         payload.CopyTo(frame.AsSpan(FrameHeaderLength));
  314.         BinaryPrimitives.WriteUInt32LittleEndian(frame.AsSpan(13, 4),
  315.             Crc32(seqByte, payload.Length, payload));
  316.         return frame;
  317.     }
  318.  
  319.     byte[] BuildControlFrame(int type, byte seq)
  320.     {
  321.         byte seqByte = (byte)((type << 6) | (seq & 0x3F));
  322.         var frame = new byte[FrameHeaderLength];
  323.         for (int i = 0; i < 8; ++i) frame[i] = 128;   // control marker = cmd 0
  324.         frame[8] = seqByte;
  325.         BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(9, 4), 0);
  326.         BinaryPrimitives.WriteUInt32LittleEndian(frame.AsSpan(13, 4),
  327.             Crc32(seqByte, 0, ReadOnlySpan<byte>.Empty));
  328.         return frame;
  329.     }
  330.  
  331.     void SendControl(int type, byte seq) => WriteFrame(BuildControlFrame(type, seq));
  332.     // Stamp next seq, build+retain, put on the wire, mark outstanding.
  333.     void TransmitData(byte cmd, ReadOnlySpan<byte> payload)
  334.     {
  335.         byte[] frame;
  336.         lock (_arq)
  337.         {
  338.             _txSeq = (byte)((_txSeq + 1) & 0x3F);
  339.             frame = BuildDataFrame(cmd, _txSeq, payload);
  340.             _retain = frame;
  341.             _awaiting = true;
  342.         }
  343.         WriteFrame(frame);
  344.         // chunk 2: arm the ack timer here when _ackTimeoutMs > 0
  345.     }
  346.  
  347.     public bool Resend()
  348.     {
  349.         byte[]? frame;
  350.         lock (_arq) { if (!_awaiting || _retain == null) return false; frame = _retain; }
  351.         WriteFrame(frame);
  352.         return true;
  353.     }
  354.     void WriteFrame(ReadOnlySpan<byte> frame)
  355.     {
  356.         lock (_sendLock)
  357.         {
  358.             try { WriteAll(frame); }
  359.             catch (Win32Exception)
  360.             {
  361.                 // Do NOT Dispose/Close here — this runs on the read-loop thread, and
  362.                 // Close() joins that same task (self-join). Just report; the owner's
  363.                 // ConnectionError handler drives teardown. Suppress during deliberate
  364.                 // close, like the loops do.
  365.                 if (!_closing) OnConnectionError(EventArgs.Empty);
  366.             }
  367.         }
  368.     }
  369.     // Called holding _arq. Stamps seq, builds+retains, arms timer, returns bytes to write.
  370.     byte[] PrepareTransmit(byte cmd, byte[] payload)
  371.     {
  372.         _txSeq = (byte)((_txSeq + 1) & 0x3F);
  373.         var frame = BuildDataFrame(cmd, _txSeq, payload);
  374.         _retain = frame;
  375.         _awaiting = true;
  376.         _retries = 0;
  377.         _deliveryNotified = false;
  378.         ArmAckTimer();
  379.         return frame;
  380.     }
  381.  
  382.     public void Send(byte cmd, ReadOnlySpan<byte> data)
  383.     {
  384.         ArgumentOutOfRangeException.ThrowIfGreaterThan(cmd, 127, nameof(cmd));
  385.         if (cmd < 1) throw new ArgumentOutOfRangeException(nameof(cmd), "cmd must be 1..127");
  386.         if (data.Length > MaxFrameLength) throw new ArgumentOutOfRangeException(nameof(data));
  387.  
  388.         byte[] copy = data.ToArray();      // stop-and-wait: caller's span need not outlive the call
  389.         byte[]? toWrite = null;
  390.         lock (_arq)
  391.         {
  392.             if (_awaiting) _sendQueue.Enqueue((cmd, copy));   // one in flight; queue the rest
  393.             else toWrite = PrepareTransmit(cmd, copy);
  394.         }
  395.         if (toWrite != null) WriteFrame(toWrite);
  396.     }
  397.     private unsafe void WriteAll(ReadOnlySpan<byte> data)
  398.     {
  399.         fixed (byte* ptr = data)
  400.         {
  401.             int offset = 0;
  402.             while (offset < data.Length)
  403.             {
  404.                 var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
  405.                 NativeOverlapped* ov;
  406.  
  407.                 lock (_ioLock)
  408.                 {
  409.                     if (_closing || _boundHandle == null || _boundHandle.Handle.IsClosed || _boundHandle.Handle.IsInvalid || _handle == null)
  410.                         return; // port is closing/closed — silently drop the write
  411.  
  412.                     ov = _boundHandle.AllocateNativeOverlapped(
  413.                         (errorCode, numBytes, pOv) =>
  414.                         {
  415.                             try { _boundHandle?.FreeNativeOverlapped(pOv); }
  416.                             catch (ObjectDisposedException) { } // lost race with dispose; overlapped is cleaned up by handle disposal
  417.                             if (errorCode == 0) tcs.TrySetResult((int)numBytes);
  418.                             else tcs.TrySetException(new Win32Exception((int)errorCode));
  419.                         },
  420.                         null, null);
  421.  
  422.                     int written0 = 0;
  423.                     if (!WriteFile(_handle, ptr + offset, data.Length - offset, ref written0, ov))
  424.                     {
  425.                         int err = Marshal.GetLastWin32Error();
  426.                         if (err != ERROR_IO_PENDING)
  427.                         {
  428.                             _boundHandle.FreeNativeOverlapped(ov);
  429.                             throw new Win32Exception(err);
  430.                         }
  431.                     }
  432.                 }
  433.  
  434.                 int written = tcs.Task.GetAwaiter().GetResult(); // block outside the lock
  435.                 offset += written;
  436.             }
  437.         }
  438.     }
  439.  
  440.     private void OnConnectionError(EventArgs args)
  441.     {
  442.         if (_disposed) return;
  443.         if (_connErrorFired) return;
  444.         _connErrorFired = true;
  445.         var handler = ConnectionError;
  446.         if (handler == null) return;
  447.  
  448.         // The loops call this inline. With _sync null, invoking directly runs the
  449.         // host handler on the loop thread; if it calls Stop()/Close(), Close() waits
  450.         // on the task it's running -> self-join. Post elsewhere so Close() is always
  451.         // on another thread.
  452.         if (_sync != null)
  453.             _sync.Post(_ => handler(this, args), null);
  454.         else
  455.             ThreadPool.QueueUserWorkItem(_ => handler(this, args));
  456.     }
  457.  
  458.     public byte[] GetNextLogData()
  459.     {
  460.         lock (_logLock)
  461.         {
  462.             var res = _log.ToArray();
  463.             _log.Clear();
  464.             return res;
  465.         }
  466.     }
  467.  
  468.     private void OnFrameReceived(FrameReceivedEventArgs args)
  469.     {
  470.         if (_disposed) return;
  471.         if (FrameReceived != null)
  472.         {
  473.             if (_sync == null)
  474.             {
  475.                 FrameReceived?.Invoke(this, args);
  476.             }
  477.             else
  478.             {
  479.                 _sync.Post((state) => FrameReceived?.Invoke(this, args), null);
  480.             }
  481.         }
  482.     }
  483.  
  484.     private void OnFrameError(FrameErrorEventArgs args)
  485.     {
  486.         if (_disposed) return;
  487.         if (FrameError != null)
  488.         {
  489.             if (_sync == null)
  490.             {
  491.                 FrameError?.Invoke(this, args);
  492.             }
  493.             else
  494.             {
  495.                 _sync.Post((state) => FrameError?.Invoke(this, args), null);
  496.             }
  497.         }
  498.     }
  499.  
  500.     /// <summary>
  501.     /// Overlapped read via IOCP.  The CLR's thread pool dispatches the
  502.     /// completion callback — no manual event handles or registered waits.
  503.     /// </summary>
  504.     private unsafe Task<int> ReadAsync(byte[] buffer, int offset, int count)
  505.     {
  506.         var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
  507.         var boundHandle = _boundHandle;   // capture once — free on THIS instance, never the field
  508.         if (boundHandle != null && !boundHandle.Handle.IsClosed && !boundHandle.Handle.IsInvalid && _handle != null)
  509.         {
  510.             var ov = boundHandle.AllocateNativeOverlapped(
  511.                 (errorCode, numBytes, pOv) =>
  512.                 {
  513.                     try { boundHandle.FreeNativeOverlapped(pOv); }
  514.                     catch (ObjectDisposedException) { }   // lost race with a forced close; overlapped reclaimed via handle teardown
  515.                     if (errorCode == 0)
  516.                         tcs.TrySetResult((int)numBytes);
  517.                     else if (errorCode == ERROR_OPERATION_ABORTED)
  518.                         tcs.TrySetCanceled();
  519.                     else
  520.                         tcs.TrySetException(new Win32Exception((int)errorCode));
  521.                 },
  522.                 null,
  523.                 buffer);
  524.  
  525.             int read = 0;
  526.             fixed (byte* pBuf = &buffer[offset])
  527.             {
  528.                 if (!ReadFile(_handle, pBuf, count, ref read, ov))
  529.                 {
  530.                     int err = Marshal.GetLastWin32Error();
  531.                     if (err != ERROR_IO_PENDING)
  532.                     {
  533.                         boundHandle.FreeNativeOverlapped(ov);   // was _boundHandle
  534.                         if (err == ERROR_OPERATION_ABORTED)
  535.                             tcs.TrySetCanceled();
  536.                         else
  537.                             tcs.TrySetException(new Win32Exception(err));
  538.                     }
  539.                 }
  540.             }
  541.         }
  542.         else throw new InvalidOperationException("The port is not open");
  543.         return tcs.Task;
  544.     }
  545.  
  546.     private async Task ReadExactlyAsync(byte[] buffer, int count)
  547.     {
  548.         int got = 0;
  549.         while (got < count)
  550.         {
  551.             if (_rxHead >= _rxTail)                       // buffer drained
  552.             {
  553.                 _rxTail = await ReadAsync(_rx, 0, _rx.Length); // one real overlapped read
  554.                 _rxHead = 0;
  555.                 if (_rxTail == 0) continue;               // spurious return; pend again
  556.             }
  557.             int n = Math.Min(_rxTail - _rxHead, count - got);
  558.             Buffer.BlockCopy(_rx, _rxHead, buffer, got, n);
  559.             _rxHead += n;
  560.             got += n;
  561.         }
  562.     }
  563.     /// <summary>
  564.     /// Overlapped WaitCommEvent via IOCP.
  565.     /// WaitCommEvent writes the event mask to an int — we pin it via GCHandle
  566.     /// and read it in the callback.
  567.     /// </summary>
  568.     private unsafe Task<int> WaitCommEventAsync()
  569.     {
  570.         var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
  571.         var maskArr = new int[1];
  572.         var maskPin = GCHandle.Alloc(maskArr, GCHandleType.Pinned);
  573.         var boundHandle = _boundHandle;   // capture once — free on THIS instance, never the field
  574.         if (boundHandle != null && !boundHandle.Handle.IsClosed && !boundHandle.Handle.IsInvalid && _handle != null)
  575.         {
  576.             var ov = boundHandle.AllocateNativeOverlapped(
  577.                 (errorCode, numBytes, pOv) =>
  578.                 {
  579.                     int mask = maskArr[0];
  580.                     if (maskPin.IsAllocated) maskPin.Free();
  581.                     try { boundHandle.FreeNativeOverlapped(pOv); }
  582.                     catch (ObjectDisposedException) { }
  583.                     if (errorCode == 0)
  584.                         tcs.TrySetResult(mask);
  585.                     else if (errorCode == ERROR_OPERATION_ABORTED)
  586.                         tcs.TrySetCanceled();
  587.                     else
  588.                         tcs.TrySetException(new Win32Exception((int)errorCode));
  589.                 },
  590.                 null, null);
  591.             if (!WaitCommEvent(_handle, ref maskArr[0], ov))
  592.             {
  593.                 int err = Marshal.GetLastWin32Error();
  594.                 if (err != ERROR_IO_PENDING)
  595.                 {
  596.                     maskPin.Free();
  597.                     boundHandle.FreeNativeOverlapped(ov);
  598.                     if (err == ERROR_OPERATION_ABORTED)
  599.                         tcs.TrySetCanceled();
  600.                     else
  601.                         tcs.TrySetException(new Win32Exception(err));
  602.                 }
  603.             }
  604.         }
  605.         else
  606.         {
  607.             maskPin.Free();   // don't leak the pinned handle on the not-open path
  608.             throw new InvalidOperationException("The port is not open");
  609.         }
  610.         return tcs.Task;
  611.     }
  612.     public bool IsOpen
  613.     {
  614.         get
  615.         {
  616.             return _handle != null && !_handle.IsInvalid && !_handle.IsClosed;
  617.         }
  618.     }
  619.     private void OnResendRequested(ResendRequestedEventArgs args)
  620.     {
  621.         if (_disposed) return;
  622.         if (ResendRequested != null)
  623.         {
  624.             if (_sync == null)
  625.                 ResendRequested?.Invoke(this, args);
  626.             else
  627.                 _sync.Post((state) => ResendRequested?.Invoke(this, args), null);
  628.         }
  629.     }
  630.     void HandleAck(byte seq)
  631.     {
  632.         byte[]? next = null;
  633.         lock (_arq)
  634.         {
  635.             if (!_awaiting || seq != _txSeq) return;   // stale/duplicate ack
  636.             _awaiting = false;
  637.             DisarmAckTimer();
  638.             if (_sendQueue.Count > 0)
  639.             {
  640.                 var (cmd, data) = _sendQueue.Dequeue();
  641.                 next = PrepareTransmit(cmd, data);
  642.             }
  643.         }
  644.         if (next != null) WriteFrame(next);
  645.     }
  646.  
  647.     void HandleNack(byte seq)
  648.     {
  649.         bool explicitMode; byte cmd, s;
  650.         lock (_arq)
  651.         {
  652.             if (!_awaiting) return;
  653.             explicitMode = _ackTimeoutMs < 0;
  654.             cmd = (byte)(_retain![0] - 128);
  655.             s = _txSeq;
  656.             // deliberately: no _retries change, no ArmAckTimer() — the running timeout guards termination
  657.         }
  658.         if (explicitMode) OnResendRequested(new ResendRequestedEventArgs(cmd, s));
  659.         else Resend();
  660.     }
  661.  
  662.     void OnAckTimeout(object? _)
  663.     {
  664.         byte[]? resend = null;
  665.         FrameErrorEventArgs? failure = null;
  666.         lock (_arq)
  667.         {
  668.             if (!_awaiting || _ackTimeoutMs <= 0) return;
  669.             resend = _retain;                       // always keep retransmitting the same frame
  670.             _retries++;
  671.             if (_retries >= _maxRetries && !_deliveryNotified)
  672.             {
  673.                 _deliveryNotified = true;           // one-shot "not getting through" notice
  674.                 failure = new FrameErrorEventArgs((byte)(_retain![0] - 128), _txSeq, _retries);
  675.             }
  676.             ArmAckTimer();                          // never stops on its own; only an ACK or Close() ends it
  677.         }
  678.         if (resend != null) WriteFrame(resend);
  679.         if (failure != null) OnFrameError(failure);
  680.     }
  681.     byte VolatileExpectedRxSeq() { lock (_arq) return _expectedRxSeq; }
  682.  
  683.     void DispatchValidFrame(byte rawCmd, byte rawSeq, byte[] payload)
  684.     {
  685.         int type = (rawSeq >> 6) & 0x03;
  686.         byte seq = (byte)(rawSeq & 0x3F);
  687.  
  688.         if (rawCmd == 128 || type != TypeData)        // control frame
  689.         {
  690.             if (type == TypeAck) HandleAck(seq);
  691.             else if (type == TypeNack) HandleNack(seq);
  692.             return;                                    // reserved types ignored
  693.         }
  694.  
  695.         byte cmd = (byte)(rawCmd - 128);               // DATA frame
  696.         byte expected; lock (_arq) expected = _expectedRxSeq;
  697.  
  698.         if (seq == expected)
  699.         {
  700.             SendControl(TypeAck, seq);                 // ack = "received intact"
  701.             lock (_arq) _expectedRxSeq = (byte)((seq + 1) & 0x3F);
  702.             OnFrameReceived(new FrameReceivedEventArgs(cmd, payload));
  703.         }
  704.         else if (seq == (byte)((expected - 1) & 0x3F))
  705.         {
  706.             SendControl(TypeAck, seq);                 // duplicate (our ack was lost): re-ack only
  707.         }
  708.         else
  709.         {
  710.             SendControl(TypeNack, expected);           // gap: ask for what we expect
  711.         }
  712.     }
  713.     public void Open()
  714.     {
  715.         if (IsOpen) return;
  716.         _closing = false;
  717.         _connErrorFired = false;
  718.         lock (_arq)
  719.         {
  720.             _txSeq = 0x3F;          // first Send -> seq 0
  721.             _expectedRxSeq = 0;
  722.             _awaiting = false;
  723.             _retries = 0;
  724.             _sendQueue.Clear();
  725.         }
  726.         _ackTimer ??= new System.Threading.Timer(OnAckTimeout, null, Timeout.Infinite, Timeout.Infinite);
  727.         var rawHandle = CreateFile(
  728.                     $@"\\.\{_portName}",
  729.                     GENERIC_READ | GENERIC_WRITE,
  730.                     0,
  731.                     IntPtr.Zero,
  732.                     OPEN_EXISTING,
  733.                     FILE_FLAG_OVERLAPPED,
  734.                     IntPtr.Zero);
  735.         if (rawHandle.IsInvalid)
  736.         {
  737.             throw new Win32Exception(Marshal.GetLastWin32Error());
  738.         }
  739.         _handle = rawHandle;
  740.  
  741.         // Bind to the CLR's IOCP thread pool.  All overlapped completions
  742.         // on this handle are now dispatched via the thread pool — no need
  743.         // for manual event handles or RegisterWaitForSingleObject.
  744.         _boundHandle = ThreadPoolBoundHandle.BindHandle(_handle);
  745.  
  746.         DCB dcb = default;
  747.         dcb.DCBlength = (uint)Unsafe.SizeOf<DCB>();
  748.         if (!GetCommState(_handle, ref dcb))
  749.         {
  750.             throw new Win32Exception(Marshal.GetLastWin32Error());
  751.         }
  752.  
  753.         dcb.BaudRate = 115200;
  754.         dcb.ByteSize = 8;
  755.         dcb.Parity = 0;
  756.         dcb.StopBits = 0;
  757.         if (!SetCommState(_handle, ref dcb))
  758.         {
  759.             throw new Win32Exception(Marshal.GetLastWin32Error());
  760.         }
  761.  
  762.         if (!SetCommMask(_handle, EV_RLSD))
  763.         {
  764.             throw new Win32Exception(Marshal.GetLastWin32Error());
  765.         }
  766.         if (!SetupComm(_handle, 8192, 0))
  767.         {
  768.             throw new Win32Exception(Marshal.GetLastWin32Error());
  769.         }
  770.         var timeouts = new COMMTIMEOUTS
  771.         {
  772.             ReadIntervalTimeout = 10, // ms gap between bytes after which the read returns its burst
  773.             ReadTotalTimeoutMultiplier = 0,
  774.             ReadTotalTimeoutConstant = 0,  // 0 total => pend (0 CPU) until the FIRST byte, no overall timeout
  775.             WriteTotalTimeoutMultiplier = 0,
  776.             WriteTotalTimeoutConstant = 0,
  777.         };
  778.         if (!SetCommTimeouts(_handle, ref timeouts))
  779.             throw new Win32Exception(Marshal.GetLastWin32Error());
  780.         RegisterPowerNotification();
  781.  
  782.         _statTask = Task.Run(async () =>
  783.         {
  784.             try
  785.             {
  786.                 while (!_closing)
  787.                 {
  788.                     int mask = await WaitCommEventAsync();
  789.                     if ((mask & (int)EV_RLSD) != 0 && !_closing)
  790.                     {
  791.                         OnConnectionError(EventArgs.Empty);
  792.                         break;
  793.                     }
  794.                 }
  795.             }
  796.             catch (Exception)
  797.             {
  798.                 if (!_closing)
  799.                 {
  800.                     OnConnectionError(EventArgs.Empty);
  801.                 }
  802.             }
  803.         });
  804.  
  805.         _readTask = Task.Run(async () =>
  806.         {
  807.             byte[] tmp = new byte[1];
  808.             StateMachine mach = default;
  809.             try
  810.             {
  811.                 while (!_closing)
  812.                 {
  813.                     await ReadExactlyAsync(tmp, 1);
  814.                     mach.Step(_log, _logging ? _logLock : null, tmp[0]);
  815.                     if (mach.IsDone)
  816.                     {
  817.                         if (_closing) break;
  818.                         int len = mach.Length;
  819.  
  820.                         if (len < 0 || len > MaxFrameLength)          // CRC-protected, but bound the read
  821.                         {
  822.                             SendControl(TypeNack, VolatileExpectedRxSeq());
  823.                             mach.Reset();
  824.                             continue;
  825.                         }
  826.  
  827.                         byte[] payload = len > 0 ? new byte[len] : Array.Empty<byte>();
  828.                         if (len > 0) await ReadExactlyAsync(payload, len);
  829.  
  830.                         if (Crc32(mach.RawSeqByte, len, payload) != mach.Crc)
  831.                         {
  832.                             SendControl(TypeNack, VolatileExpectedRxSeq());   // corrupt: seq untrustworthy
  833.                             mach.Reset();
  834.                             continue;
  835.                         }
  836.  
  837.                         DispatchValidFrame(mach.RawCommandByte, mach.RawSeqByte, payload);
  838.                         mach.Reset();
  839.                     }
  840.                 }
  841.             }
  842.             catch (Exception)
  843.             {
  844.                 if (!_closing)
  845.                 {
  846.                     OnConnectionError(EventArgs.Empty);
  847.                 }
  848.             }
  849.  
  850.         });
  851.     }
  852.     public void Close()
  853.     {
  854.         UnregisterPowerNotification();
  855.  
  856.         // Fast-path guard: if we're somehow on a loop task, never join (that waits on
  857.         // the current task). Just request stop + abort I/O; the owner's Close joins.
  858.         // Best-effort only — see caveat below — but the timeout makes it non-fatal
  859.         // even when this misses.
  860.         int? cur = Task.CurrentId;
  861.         if ((_readTask != null && _readTask.Id == cur) ||
  862.             (_statTask != null && _statTask.Id == cur))
  863.         {
  864.             _closing = true;
  865.             Thread.MemoryBarrier();
  866.             try { if (_handle is { IsInvalid: false, IsClosed: false }) CancelIoEx(_handle, IntPtr.Zero); }
  867.             catch (Win32Exception) { }
  868.             return;
  869.         }
  870.  
  871.         lock (_closeLock)                 // serialize concurrent/nested closes
  872.         {
  873.             if (!IsOpen) return;
  874.  
  875.             _closing = true;
  876.             Thread.MemoryBarrier();
  877.  
  878.             try { if (_handle is { IsInvalid: false, IsClosed: false }) CancelIoEx(_handle, IntPtr.Zero); }
  879.             catch (Win32Exception) { }
  880.  
  881.             var pending = new List<Task>(2);
  882.             if (_readTask != null) pending.Add(_readTask);
  883.             if (_statTask != null) pending.Add(_statTask);
  884.  
  885.             bool drained = true;
  886.             if (pending.Count > 0)
  887.             {
  888.                 try { drained = Task.WaitAll(pending.ToArray(), TimeSpan.FromSeconds(5)); }
  889.                 catch (AggregateException) { drained = true; }   // faulted still means the loop exited
  890.             }
  891.  
  892.             if (drained)
  893.             {
  894.                 // Every overlapped is freed; safe to dispose both handles in order.
  895.                 _boundHandle?.Dispose();
  896.                 _handle?.Dispose();
  897.             }
  898.             else
  899.             {
  900.                 // A loop is wedged for an unknown reason and we refuse to hang.
  901.                 // Closing the FILE handle aborts any pending overlapped, so the loop
  902.                 // callbacks complete ABORTED and free their overlappeds against the
  903.                 // still-live bound handle. We deliberately do NOT dispose the bound
  904.                 // handle while an overlapped may be in flight: that is undefined
  905.                 // behavior (heap corruption, not a catchable exception). Leak it and
  906.                 // let finalization reclaim it after the handle close drains the I/O.
  907.                 System.Diagnostics.Debug.WriteLine(
  908.                     "[EspSerialSession] Close timed out waiting for I/O loops; forcing handle close.");
  909.                 try { _handle?.Dispose(); } catch { }
  910.                 try { Task.WaitAll(pending.ToArray(), TimeSpan.FromSeconds(2)); } catch { }  // grace for now-unblocked loops
  911.             }
  912.  
  913.             _handle = null;
  914.             _boundHandle = null;
  915.             // _closing / _connErrorFired are intentionally NOT reset here — Open()
  916.             // resets them (lines 706–707). Resetting now races a loop that is only
  917.             // just observing _closing == true.
  918.         }
  919.     }
  920.  
  921.     private void OnSuspend()
  922.     {
  923.         if (_closing || _disposed) return;
  924.  
  925.         // Abort any pending overlapped ReadFile / WaitCommEvent so the read and
  926.         // status loops unwind immediately. This is the same teardown path as a
  927.         // cable unplug: the loops complete with ERROR_OPERATION_ABORTED and
  928.         // raise ConnectionError themselves.
  929.         try
  930.         {
  931.             var h = _handle;
  932.             if (h != null && !h.IsInvalid && !h.IsClosed)
  933.             {
  934.                 CancelIoEx(h, IntPtr.Zero);
  935.             }
  936.         }
  937.         catch (Win32Exception) { }
  938.  
  939.         // Notify the host off this callback thread. Raising it inline risks a
  940.         // synchronous ConnectionError handler calling Close(), which would
  941.         // re-enter PowerUnregisterSuspendResumeNotification on the callback
  942.         // thread and deadlock. OnConnectionError is idempotent, so firing here
  943.         // in addition to the loop paths above is harmless.
  944.         ThreadPool.QueueUserWorkItem(_ => OnConnectionError(EventArgs.Empty));
  945.     }
  946.     public EspSerialSession(string port, bool logging = false,
  947.                         SynchronizationContext? syncContext = null, int ackTimeoutMs = 1000)
  948.     {
  949.         _logLock = new object();
  950.         _ioLock = new object();
  951.         _closeLock = new object();
  952.         _sync = syncContext;
  953.         _log = new List<byte>();
  954.         _portName = port;
  955.         _logging = logging;
  956.         _ackTimeoutMs = ackTimeoutMs;
  957.         _ackTimer = new System.Threading.Timer(OnAckTimeout, null, Timeout.Infinite, Timeout.Infinite);
  958.     }
  959.  
  960.     public int AckTimeout { get { lock (_arq) return _ackTimeoutMs; } set { lock (_arq) _ackTimeoutMs = value; } }
  961.     public int MaxRetries { get { lock (_arq) return _maxRetries; } set { lock (_arq) _maxRetries = value < 0 ? 0 : value; } }
  962.  
  963.     void ArmAckTimer() { if (_ackTimeoutMs > 0) _ackTimer?.Change(_ackTimeoutMs, Timeout.Infinite); }
  964.     void DisarmAckTimer() { _ackTimer?.Change(Timeout.Infinite, Timeout.Infinite); }
  965.     public bool IsLogging
  966.     {
  967.         get { return _logging; }
  968.         set { _logging = value; }
  969.     }
  970.     static uint[] BuildCrcTable()
  971.     {
  972.         var t = new uint[256];
  973.         for (uint n = 0; n < 256; ++n)
  974.         {
  975.             uint c = n;
  976.             for (int k = 0; k < 8; ++k)
  977.                 c = (c & 1) != 0 ? (0xEDB88320u ^ (c >> 1)) : (c >> 1);
  978.             t[n] = c;
  979.         }
  980.         return t;
  981.     }
  982.     static uint Crc32Byte(uint crc, byte b) => (crc >> 8) ^ _crcTable[(crc ^ b) & 0xFF];
  983.  
  984.     static uint Crc32(byte seqByte, int length, ReadOnlySpan<byte> payload)
  985.     {
  986.         uint c = 0xFFFFFFFFu;
  987.         c = Crc32Byte(c, seqByte);
  988.         Span<byte> len = stackalloc byte[4];
  989.         BinaryPrimitives.WriteInt32LittleEndian(len, length);
  990.         for (int i = 0; i < 4; ++i) c = Crc32Byte(c, len[i]);
  991.         for (int i = 0; i < payload.Length; ++i) c = Crc32Byte(c, payload[i]);
  992.         return c ^ 0xFFFFFFFFu;
  993.     }
  994.  
  995.     const int ERROR_IO_PENDING = 0x000003E5;
  996.     const uint ERROR_OPERATION_ABORTED = 995;
  997.     const uint GENERIC_READ = 0x80000000;
  998.     const uint GENERIC_WRITE = 0x40000000;
  999.     const uint OPEN_EXISTING = 3;
  1000.     const uint FILE_FLAG_OVERLAPPED = 0x40000000;
  1001.     const uint EV_RLSD = 0x0020;
  1002.     const uint DEVICE_NOTIFY_CALLBACK = 0x00000002;
  1003.     const uint PBT_APMSUSPEND = 0x0004;
  1004.     // GUID_DEVCLASS_PORTS {4d36e978-e325-11ce-bfc1-08002be10318}
  1005.     private static readonly Guid GUID_DEVCLASS_PORTS =
  1006.         new(0x4d36e978, 0xe325, 0x11ce, 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18);
  1007.  
  1008.     private const uint DIGCF_PRESENT = 0x00000002;
  1009.     private const uint DICS_FLAG_GLOBAL = 0x00000001;
  1010.     private const uint DIREG_DEV = 0x00000001;
  1011.     private const uint KEY_READ = 0x00020019;
  1012.     private const int ERROR_NO_MORE_ITEMS = 259;
  1013.     private const int ERROR_SUCCESS = 0;
  1014.     private static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
  1015.  
  1016.     [StructLayout(LayoutKind.Sequential)]
  1017.     private struct SP_DEVINFO_DATA
  1018.     {
  1019.         public uint cbSize;
  1020.         public Guid ClassGuid;
  1021.         public uint DevInst;
  1022.         public nuint Reserved;
  1023.     }
  1024.  
  1025.     private struct COMSTAT
  1026.     {
  1027.         public uint Flags;
  1028.         public uint cbInQue;
  1029.         public uint cbOutQue;
  1030.     }
  1031.  
  1032.     private struct DCB
  1033.     {
  1034.         public uint DCBlength;
  1035.         public uint BaudRate;
  1036.         public uint Flags;
  1037.         public ushort wReserved;
  1038.         public ushort XonLim;
  1039.         public ushort XoffLim;
  1040.         public byte ByteSize;
  1041.         public byte Parity;
  1042.         public byte StopBits;
  1043.         public byte XonChar;
  1044.         public byte XoffChar;
  1045.         public byte ErrorChar;
  1046.         public byte EofChar;
  1047.         public byte EvtChar;
  1048.         public ushort wReserved1;
  1049.     }
  1050.  
  1051.     [StructLayout(LayoutKind.Sequential)]
  1052.     private struct COMMTIMEOUTS
  1053.     {
  1054.         public uint ReadIntervalTimeout;
  1055.         public uint ReadTotalTimeoutMultiplier;
  1056.         public uint ReadTotalTimeoutConstant;
  1057.         public uint WriteTotalTimeoutMultiplier;
  1058.         public uint WriteTotalTimeoutConstant;
  1059.     }
  1060.  
  1061.     [StructLayout(LayoutKind.Sequential)]
  1062.     private struct DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
  1063.     {
  1064.         public IntPtr Callback; // PDEVICE_NOTIFY_CALLBACK_ROUTINE (function pointer)
  1065.         public IntPtr Context;
  1066.     }
  1067.     [LibraryImport("setupapi.dll", SetLastError = true)]
  1068.     private static unsafe partial IntPtr SetupDiGetClassDevsW(
  1069.        Guid* classGuid, char* enumerator, IntPtr hwndParent, uint flags);
  1070.  
  1071.     [LibraryImport("setupapi.dll", SetLastError = true)]
  1072.     [return: MarshalAs(UnmanagedType.Bool)]
  1073.     private static unsafe partial bool SetupDiEnumDeviceInfo(
  1074.         IntPtr deviceInfoSet, uint memberIndex, SP_DEVINFO_DATA* deviceInfoData);
  1075.  
  1076.     [LibraryImport("setupapi.dll", SetLastError = true)]
  1077.     [return: MarshalAs(UnmanagedType.Bool)]
  1078.     private static unsafe partial bool SetupDiGetDeviceInstanceIdW(
  1079.         IntPtr deviceInfoSet, SP_DEVINFO_DATA* deviceInfoData,
  1080.         char* deviceInstanceId, uint deviceInstanceIdSize, uint* requiredSize);
  1081.  
  1082.     [LibraryImport("setupapi.dll", SetLastError = true)]
  1083.     private static unsafe partial IntPtr SetupDiOpenDevRegKey(
  1084.         IntPtr deviceInfoSet, SP_DEVINFO_DATA* deviceInfoData,
  1085.         uint scope, uint hwProfile, uint keyType, uint samDesired);
  1086.  
  1087.     [LibraryImport("setupapi.dll", SetLastError = true)]
  1088.     [return: MarshalAs(UnmanagedType.Bool)]
  1089.     private static partial bool SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet);
  1090.  
  1091.     [LibraryImport("advapi32.dll", SetLastError = false)]
  1092.     private static unsafe partial int RegQueryValueExW(
  1093.         IntPtr hKey, char* lpValueName, IntPtr lpReserved,
  1094.         uint* lpType, byte* lpData, uint* lpcbData);
  1095.  
  1096.     [LibraryImport("advapi32.dll")]
  1097.     private static partial int RegCloseKey(IntPtr hKey);
  1098.     [UnmanagedFunctionPointer(CallingConvention.StdCall)]
  1099.     private delegate uint DeviceNotifyCallbackRoutine(IntPtr context, uint type, IntPtr setting);
  1100.  
  1101.     #region kernel32
  1102.     [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "CreateFileW",
  1103.         StringMarshalling = StringMarshalling.Utf16)]
  1104.     private static partial SafeFileHandle CreateFile(
  1105.         string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes,
  1106.         uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);
  1107.  
  1108.     [LibraryImport("kernel32.dll", SetLastError = true)]
  1109.     [return: MarshalAs(UnmanagedType.Bool)]
  1110.     private static partial bool SetCommMask(SafeFileHandle hFile, uint dwEvtMask);
  1111.  
  1112.     [LibraryImport("kernel32.dll", SetLastError = true)]
  1113.     [return: MarshalAs(UnmanagedType.Bool)]
  1114.     private static partial bool ClearCommError(SafeFileHandle hFile, ref int lpErrors, ref COMSTAT lpStat);
  1115.  
  1116.     [LibraryImport("kernel32.dll", SetLastError = true)]
  1117.     [return: MarshalAs(UnmanagedType.Bool)]
  1118.     private static partial bool GetCommState(SafeFileHandle hFile, ref DCB lpDCB);
  1119.  
  1120.     [LibraryImport("kernel32.dll", SetLastError = true)]
  1121.     [return: MarshalAs(UnmanagedType.Bool)]
  1122.     private static partial bool SetCommState(SafeFileHandle hFile, ref DCB lpDCB);
  1123.  
  1124.     [LibraryImport("kernel32.dll", SetLastError = true)]
  1125.     [return: MarshalAs(UnmanagedType.Bool)]
  1126.     internal static partial bool SetupComm(SafeFileHandle hFile, int dwInQueue, int dwOutQueue);
  1127.  
  1128.     [LibraryImport("kernel32.dll", SetLastError = true)]
  1129.     [return: MarshalAs(UnmanagedType.Bool)]
  1130.     private static partial bool SetCommTimeouts(SafeFileHandle hFile, ref COMMTIMEOUTS lpCommTimeouts);
  1131.  
  1132.     [LibraryImport("kernel32.dll", SetLastError = true)]
  1133.     [return: MarshalAs(UnmanagedType.Bool)]
  1134.     private static unsafe partial bool WaitCommEvent(
  1135.         SafeFileHandle hFile, ref int lpEvtMask, NativeOverlapped* lpOverlapped);
  1136.  
  1137.     [LibraryImport("kernel32.dll", SetLastError = true)]
  1138.     [return: MarshalAs(UnmanagedType.Bool)]
  1139.     private static unsafe partial bool GetOverlappedResult(
  1140.         SafeFileHandle hFile, NativeOverlapped* lpOverlapped,
  1141.         ref int lpNumberOfBytesTransferred, [MarshalAs(UnmanagedType.Bool)] bool bWait);
  1142.  
  1143.     [LibraryImport("kernel32.dll", SetLastError = true)]
  1144.     [return: MarshalAs(UnmanagedType.Bool)]
  1145.     private static unsafe partial bool ReadFile(
  1146.         SafeFileHandle hFile, byte* lpBuffer, int nNumberOfBytesToRead,
  1147.         ref int lpNumberOfBytesRead, NativeOverlapped* lpOverlapped);
  1148.  
  1149.     [LibraryImport("kernel32.dll", SetLastError = true)]
  1150.     [return: MarshalAs(UnmanagedType.Bool)]
  1151.     private static unsafe partial bool WriteFile(
  1152.         SafeFileHandle hFile, byte* lpBuffer, int nNumberOfBytesToWrite,
  1153.         ref int lpNumberOfBytesWritten, NativeOverlapped* lpOverlapped);
  1154.  
  1155.     [LibraryImport("kernel32.dll", SetLastError = true)]
  1156.     [return: MarshalAs(UnmanagedType.Bool)]
  1157.     private static partial bool CancelIoEx(SafeFileHandle hFile, IntPtr lpOverlapped);
  1158.     #endregion
  1159.  
  1160.  
  1161.     private unsafe void RegisterPowerNotification()
  1162.     {
  1163.         _powerCallbackHandle = GCHandle.Alloc(this);
  1164.  
  1165.         var sub = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
  1166.         {
  1167.             // Address of a static method — always valid, nothing to keep rooted for the OS.
  1168.             Callback = (IntPtr)(delegate* unmanaged[Stdcall]<IntPtr, uint, IntPtr, uint>)&PowerCallbackNative,
  1169.             Context = GCHandle.ToIntPtr(_powerCallbackHandle)
  1170.         };
  1171.  
  1172.         // Callback-based notification: no window or message pump required, so this works
  1173.         // identically in a WinUI3 app and a headless service.
  1174.         uint rc = PowerRegisterSuspendResumeNotification(
  1175.             DEVICE_NOTIFY_CALLBACK, ref sub, out _powerNotifyHandle);
  1176.         if (rc != 0)
  1177.         {
  1178.             _powerCallbackHandle.Free();
  1179.             _powerNotifyHandle = IntPtr.Zero;
  1180.             throw new Win32Exception((int)rc);
  1181.         }
  1182.     }
  1183.  
  1184.     private void UnregisterPowerNotification()
  1185.     {
  1186.         var h = Interlocked.Exchange(ref _powerNotifyHandle, IntPtr.Zero);
  1187.         if (h != IntPtr.Zero)
  1188.         {
  1189.             // Blocks until any in-progress callback returns, so after this the GCHandle
  1190.             // is safe to free. Must NOT be called from inside the callback itself
  1191.             // (would deadlock) — see OnSuspend. Only the thread that won the exchange
  1192.             // reaches here, so the handle is freed exactly once.
  1193.             try { PowerUnregisterSuspendResumeNotification(h); }
  1194.             catch (Win32Exception) { }
  1195.  
  1196.             if (_powerCallbackHandle.IsAllocated)
  1197.                 _powerCallbackHandle.Free();
  1198.         }
  1199.     }
  1200.  
  1201.     // Runs on an OS power-management thread. Must return promptly, and must never let a
  1202.     // managed exception escape to native code — under NativeAOT that fastfails the process.
  1203.     [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
  1204.     private static uint PowerCallbackNative(IntPtr context, uint type, IntPtr setting)
  1205.     {
  1206.         try
  1207.         {
  1208.             if (type == PBT_APMSUSPEND && context != IntPtr.Zero)
  1209.             {
  1210.                 var self = GCHandle.FromIntPtr(context).Target as EspSerialSession;
  1211.                 self?.OnSuspend();
  1212.             }
  1213.         }
  1214.         catch
  1215.         {
  1216.             // Never propagate across the native boundary.
  1217.         }
  1218.         return 0; // ERROR_SUCCESS
  1219.     }
  1220.     #region powrprof
  1221.     [LibraryImport("powrprof.dll", SetLastError = false)]
  1222.     private static partial uint PowerRegisterSuspendResumeNotification(
  1223.         uint flags, ref DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS recipient, out IntPtr registrationHandle);
  1224.  
  1225.     [LibraryImport("powrprof.dll", SetLastError = false)]
  1226.     private static partial uint PowerUnregisterSuspendResumeNotification(IntPtr registrationHandle);
  1227.     #endregion
  1228.    
  1229.  
  1230. }
  1231. #pragma warning restore
Advertisement
Add Comment
Please, Sign In to add comment