honey_the_codewitch

EspSerialSession

Apr 26th, 2026 (edited)
180
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 30.71 KB | None | 0 0
  1. // for use with github.com/codewitch-honey-crisis/htcw_frame
  2. #pragma warning disable CS0649
  3. #if !ESS_NO_PORT_ENUM
  4. using Microsoft.Management.Infrastructure;
  5. #endif
  6. using Microsoft.Win32.SafeHandles;
  7.  
  8. using System.Buffers.Binary;
  9. using System.ComponentModel;
  10. using System.Diagnostics;
  11. using System.Runtime.CompilerServices;
  12. using System.Runtime.InteropServices;
  13. using System.Runtime.Versioning;
  14.  
  15. namespace Htcw;
  16. #if !ESS_NO_PORT_ENUM
  17. public sealed class PortEntry
  18. {
  19.     public PortEntry(string portName, string serialNumber)
  20.     {
  21.         PortName = portName;
  22.         SerialNumber = serialNumber;
  23.     }
  24.     public string PortName { get; }
  25.     public string SerialNumber { get; }
  26.  
  27. }
  28. #endif
  29. public sealed class FrameReceivedEventArgs : EventArgs
  30. {
  31.     public byte Command;
  32.     public byte[] Data { get; }
  33.  
  34.     public FrameReceivedEventArgs(byte command, byte[] data)
  35.     {
  36.         ArgumentOutOfRangeException.ThrowIfGreaterThan(command, 127, nameof(command));
  37.         Command = command;
  38.         Data = data;
  39.     }
  40. }
  41.  
  42. [SupportedOSPlatform("windows")]
  43. internal partial class EspSerialSession : IDisposable
  44. {
  45.     struct StateMachine
  46.     {
  47.         int state;
  48.         byte rawCmd;
  49.         int rawLen;
  50.         uint rawCrc;
  51.         public byte RawCommandByte => rawCmd;
  52.         public byte Command => (byte)(rawCmd - 128);
  53.         public int Length => Swap(rawLen);
  54.         public uint Crc => Swap(rawCrc);
  55.         public bool IsDone => state == 16;
  56.         private int Swap(int value)
  57.         {
  58.             var x = unchecked((uint)value);
  59.             return unchecked((int)Swap(x));
  60.         }
  61.         private uint Swap(uint x)
  62.         {
  63.             return (((x & 0x000000ff) << 24) +
  64.                    ((x & 0x0000ff00) << 8) +
  65.                    ((x & 0x00ff0000) >> 8) +
  66.                    ((x & 0xff000000) >> 24));
  67.         }
  68.         public void Reset() => state = 0;
  69.         public bool Step(List<byte>? log, object? logLock, byte data)
  70.         {
  71.             if (state == 0)
  72.             {
  73.                 if (data < 128)
  74.                 {
  75.                     if (logLock != null)
  76.                     {
  77.                         lock (logLock)
  78.                         {
  79.                             log?.Add(data);
  80.                         }
  81.                     }
  82.                     return false;
  83.                 }
  84.                 state = 1;
  85.                 rawCmd = data;
  86.                 rawLen = 0;
  87.             }
  88.             else if (state < 8)
  89.             {
  90.                 if (rawCmd != data)
  91.                 {
  92.                     if (logLock != null)
  93.                     {
  94.  
  95.                         lock (logLock)
  96.                         {
  97.                             for (var i = 0; i < state; ++i)
  98.                             {
  99.                                 log?.Add(rawCmd);
  100.                             }
  101.                             log?.Add(data);
  102.                         }
  103.                     }
  104.                     state = 0;
  105.                     return false;
  106.                 }
  107.                 ++state;
  108.             }
  109.             else if (state == 8)
  110.             {
  111.                 rawLen = data;
  112.                 ++state;
  113.             }
  114.             else if (state < 12)
  115.             {
  116.                 rawLen <<= 8;
  117.                 rawLen |= data;
  118.                 ++state;
  119.             }
  120.             else if (state == 12)
  121.             {
  122.                 rawCrc = data;
  123.                 ++state;
  124.             }
  125.             else if (state < 16)
  126.             {
  127.                 rawCrc <<= 8;
  128.                 rawCrc |= data;
  129.                 ++state;
  130.             }
  131.             else if (state == 16)
  132.             {
  133.                 state = 0;
  134.                 return Step(log, logLock, data);
  135.             }
  136.             else
  137.             {
  138.                 state = 0;
  139.                 return false;
  140.  
  141.             }
  142.             return true;
  143.         }
  144.     }
  145.  
  146.     /// <summary>
  147.     /// Captures the state for one pending overlapped I/O so the
  148.     /// IOCallback can resolve the right TaskCompletionSource.
  149.     /// </summary>
  150.     private sealed class ReadOp
  151.     {
  152.         public TaskCompletionSource<int> Tcs = null!;
  153.         public unsafe NativeOverlapped* Overlapped;
  154.     }
  155.  
  156.     private sealed class CommEventOp
  157.     {
  158.         public TaskCompletionSource<int> Tcs = null!;
  159.         public unsafe NativeOverlapped* Overlapped;
  160.         public GCHandle MaskHandle;
  161.     }
  162.  
  163.     volatile bool _closing;
  164.     volatile bool _connErrorFired;
  165.     string _portName;
  166.     bool _disposed;
  167.     SafeFileHandle? _handle;
  168.     ThreadPoolBoundHandle? _boundHandle;
  169.     readonly List<byte> _log;
  170.     readonly object _logLock;
  171.     readonly object _ioLock;
  172.     bool _logging;
  173.     SynchronizationContext? _sync;
  174.     Task? _readTask, _statTask;
  175.     IntPtr _powerNotifyHandle;
  176.     readonly byte[] _rx = new byte[4096];
  177.     int _rxHead;   // next unread byte in _rx
  178.     int _rxTail;   // number of valid bytes in _rx
  179.     DeviceNotifyCallbackRoutine? _powerCallback; // kept rooted: native code holds a pointer to it
  180.     public event EventHandler<EventArgs>? ConnectionError;
  181.     public event EventHandler<FrameReceivedEventArgs>? FrameReceived;
  182.     public event EventHandler<FrameReceivedEventArgs>? FrameError;
  183. #if !ESS_NO_PORT_ENUM
  184.     public static PortEntry[] GetPorts()
  185.     {
  186.         var result = new List<PortEntry>();
  187.         using var session = CimSession.Create(null); // null = local machine
  188.         var instances = session.QueryInstances(@"root\cimv2", "WQL",
  189.             "SELECT DeviceID, Name FROM Win32_PnPEntity WHERE ClassGuid = '{4d36e978-e325-11ce-bfc1-08002be10318}'");
  190.  
  191.         foreach (var instance in instances)
  192.         {
  193.             var deviceId = instance.CimInstanceProperties["DeviceID"]?.Value as string;
  194.             if (string.IsNullOrEmpty(deviceId))
  195.                 continue;
  196.  
  197.             // Extract Serial
  198.             int index = deviceId.LastIndexOf('\\');
  199.             if (index == -1)
  200.                 continue;
  201.  
  202.             string serialNo = deviceId.Substring(index + 1);
  203.  
  204.             // Extract port name from Name property
  205.             var nameValue = instance.CimInstanceProperties["Name"]?.Value as string;
  206.             if (string.IsNullOrEmpty(nameValue))
  207.                 continue;
  208.  
  209.             int idx = nameValue.IndexOf('(');
  210.             if (idx > -1)
  211.             {
  212.                 int lidx = nameValue.IndexOf(')', idx + 2);
  213.                 if (lidx > -1)
  214.                 {
  215.                     string extractedName = nameValue.Substring(idx + 1, lidx - idx - 1);
  216.                     result.Add(new PortEntry(extractedName, serialNo));
  217.                 }
  218.             }
  219.  
  220.         }
  221.         return result.ToArray();
  222.  
  223.     }
  224. #endif
  225.     protected virtual void Dispose(bool disposing)
  226.     {
  227.         if (!_disposed)
  228.         {
  229.             if (disposing)
  230.             {
  231.                 Close();
  232.             }
  233.             // Also runs on the finalizer path (disposing == false), where
  234.             // Close() is not called. Only touches the IntPtr handle, so it is
  235.             // finalizer-safe, and prevents the callback delegate from being
  236.             // collected while the native registration is still live.
  237.             UnregisterPowerNotification();
  238.             _disposed = true;
  239.         }
  240.     }
  241.  
  242.     ~EspSerialSession()
  243.     {
  244.         Dispose(false);
  245.     }
  246.  
  247.     void IDisposable.Dispose()
  248.     {
  249.         Dispose(disposing: true);
  250.         GC.SuppressFinalize(this);
  251.     }
  252.  
  253.     public void Send(byte cmd, ReadOnlySpan<byte> data)
  254.     {
  255.         ArgumentOutOfRangeException.ThrowIfGreaterThan(cmd, 127, nameof(cmd));
  256.         cmd += 128;
  257.         Span<byte> len = [0, 0, 0, 0];
  258.         BinaryPrimitives.WriteInt32LittleEndian(len, data.Length);
  259.         Span<byte> crc = [0, 0, 0, 0];
  260.         BinaryPrimitives.WriteUInt32LittleEndian(crc, Crc32(data));
  261.         Span<byte> toWrite = [cmd, cmd, cmd, cmd, cmd, cmd, cmd, cmd, .. len, .. crc, .. data];
  262.         try
  263.         {
  264.             WriteAll(toWrite);
  265.         }
  266.         catch (Win32Exception)
  267.         {
  268.             OnConnectionError(EventArgs.Empty);
  269.             Dispose(true);
  270.         }
  271.     }
  272.     private unsafe void WriteAll(ReadOnlySpan<byte> data)
  273.     {
  274.         fixed (byte* ptr = data)
  275.         {
  276.             int offset = 0;
  277.             while (offset < data.Length)
  278.             {
  279.                 var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
  280.                 NativeOverlapped* ov;
  281.  
  282.                 lock (_ioLock)
  283.                 {
  284.                     if (_closing || _boundHandle == null || _handle == null)
  285.                         return; // port is closing/closed — silently drop the write
  286.  
  287.                     ov = _boundHandle.AllocateNativeOverlapped(
  288.                         (errorCode, numBytes, pOv) =>
  289.                         {
  290.                             try { _boundHandle?.FreeNativeOverlapped(pOv); }
  291.                             catch (ObjectDisposedException) { } // lost race with dispose; overlapped is cleaned up by handle disposal
  292.                             if (errorCode == 0) tcs.TrySetResult((int)numBytes);
  293.                             else tcs.TrySetException(new Win32Exception((int)errorCode));
  294.                         },
  295.                         null, null);
  296.  
  297.                     int written0 = 0;
  298.                     if (!WriteFile(_handle, ptr + offset, data.Length - offset, ref written0, ov))
  299.                     {
  300.                         int err = Marshal.GetLastWin32Error();
  301.                         if (err != ERROR_IO_PENDING)
  302.                         {
  303.                             _boundHandle.FreeNativeOverlapped(ov);
  304.                             throw new Win32Exception(err);
  305.                         }
  306.                     }
  307.                 }
  308.  
  309.                 int written = tcs.Task.GetAwaiter().GetResult(); // block outside the lock
  310.                 offset += written;
  311.             }
  312.         }
  313.     }
  314.    
  315.     private void OnConnectionError(EventArgs args)
  316.     {
  317.         if (_disposed) return;
  318.         if (_connErrorFired) return;
  319.         _connErrorFired = true;
  320.         if (ConnectionError != null)
  321.         {
  322.             if (_sync == null)
  323.             {
  324.                 ConnectionError?.Invoke(this, args);
  325.             }
  326.             else
  327.             {
  328.                 _sync.Post((state) => ConnectionError?.Invoke(this, args), null);
  329.             }
  330.         }
  331.     }
  332.  
  333.     public byte[] GetNextLogData()
  334.     {
  335.         lock (_logLock)
  336.         {
  337.             var res = _log.ToArray();
  338.             _log.Clear();
  339.             return res;
  340.         }
  341.     }
  342.  
  343.     private void OnFrameReceived(FrameReceivedEventArgs args)
  344.     {
  345.         if (_disposed) return;
  346.         if (FrameReceived != null)
  347.         {
  348.             if (_sync == null)
  349.             {
  350.                 FrameReceived?.Invoke(this, args);
  351.             }
  352.             else
  353.             {
  354.                 _sync.Post((state) => FrameReceived?.Invoke(this, args), null);
  355.             }
  356.         }
  357.     }
  358.  
  359.     private void OnFrameError(FrameReceivedEventArgs args)
  360.     {
  361.         if (_disposed) return;
  362.         if (FrameError != null)
  363.         {
  364.             if (_sync == null)
  365.             {
  366.                 FrameError?.Invoke(this, args);
  367.             }
  368.             else
  369.             {
  370.                 _sync.Post((state) => FrameError?.Invoke(this, args), null);
  371.             }
  372.         }
  373.     }
  374.  
  375.     /// <summary>
  376.     /// Overlapped read via IOCP.  The CLR's thread pool dispatches the
  377.     /// completion callback — no manual event handles or registered waits.
  378.     /// </summary>
  379.     private unsafe Task<int> ReadAsync(byte[] buffer, int offset, int count)
  380.     {
  381.         var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
  382.         if (_boundHandle != null && _handle != null)
  383.         {
  384.             var ov = _boundHandle.AllocateNativeOverlapped(
  385.                 (errorCode, numBytes, pOv) =>
  386.                 {
  387.                     _boundHandle.FreeNativeOverlapped(pOv);
  388.                     if (errorCode == 0)
  389.                         tcs.TrySetResult((int)numBytes);
  390.                     else if (errorCode == ERROR_OPERATION_ABORTED)
  391.                         tcs.TrySetCanceled();
  392.                     else
  393.                         tcs.TrySetException(new Win32Exception((int)errorCode));
  394.                 },
  395.                 null,
  396.                 buffer);  // pins buffer until FreeNativeOverlapped
  397.  
  398.             int read = 0;
  399.             fixed (byte* pBuf = &buffer[offset])
  400.             {
  401.                 if (!ReadFile(_handle, pBuf, count, ref read, ov))
  402.                 {
  403.                     int err = Marshal.GetLastWin32Error();
  404.                     if (err != ERROR_IO_PENDING)
  405.                     {
  406.                         _boundHandle.FreeNativeOverlapped(ov);
  407.                         if (err == ERROR_OPERATION_ABORTED)
  408.                             tcs.TrySetCanceled();
  409.                         else
  410.                             tcs.TrySetException(new Win32Exception(err));
  411.                     }
  412.                     // else: pending — IOCP callback will fire
  413.                 }
  414.                 // else: completed synchronously — IOCP callback still fires for bound handles
  415.             }
  416.         }
  417.         else throw new InvalidOperationException("The port is not open");
  418.         return tcs.Task;
  419.     }
  420.  
  421.     private async Task ReadExactlyAsync(byte[] buffer, int count)
  422.     {
  423.         int got = 0;
  424.         while (got < count)
  425.         {
  426.             if (_rxHead >= _rxTail)                       // buffer drained
  427.             {
  428.                 _rxTail = await ReadAsync(_rx, 0, _rx.Length); // one real overlapped read
  429.                 _rxHead = 0;
  430.                 if (_rxTail == 0) continue;               // spurious return; pend again
  431.             }
  432.             int n = Math.Min(_rxTail - _rxHead, count - got);
  433.             Buffer.BlockCopy(_rx, _rxHead, buffer, got, n);
  434.             _rxHead += n;
  435.             got += n;
  436.         }
  437.     }
  438.     /// <summary>
  439.     /// Overlapped WaitCommEvent via IOCP.
  440.     /// WaitCommEvent writes the event mask to an int — we pin it via GCHandle
  441.     /// and read it in the callback.
  442.     /// </summary>
  443.     private unsafe Task<int> WaitCommEventAsync()
  444.     {
  445.         var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
  446.         var maskArr = new int[1];
  447.         var maskPin = GCHandle.Alloc(maskArr, GCHandleType.Pinned);
  448.         if (_boundHandle != null && _handle != null)
  449.         {
  450.             var ov = _boundHandle.AllocateNativeOverlapped(
  451.                 (errorCode, numBytes, pOv) =>
  452.                 {
  453.                     int mask = maskArr[0];
  454.                     maskPin.Free();
  455.                     _boundHandle.FreeNativeOverlapped(pOv);
  456.                     if (errorCode == 0)
  457.                         tcs.TrySetResult(mask);
  458.                     else if (errorCode == ERROR_OPERATION_ABORTED)
  459.                         tcs.TrySetCanceled();
  460.                     else
  461.                         tcs.TrySetException(new Win32Exception((int)errorCode));
  462.                 },
  463.                 null, null);
  464.             if (!WaitCommEvent(_handle, ref maskArr[0], ov))
  465.             {
  466.                 int err = Marshal.GetLastWin32Error();
  467.                 if (err != ERROR_IO_PENDING)
  468.                 {
  469.                     maskPin.Free();
  470.                     _boundHandle.FreeNativeOverlapped(ov);
  471.                     if (err == ERROR_OPERATION_ABORTED)
  472.                         tcs.TrySetCanceled();
  473.                     else
  474.                         tcs.TrySetException(new Win32Exception(err));
  475.                 }
  476.             }
  477.  
  478.         }
  479.         else throw new InvalidOperationException("The port is not open");
  480.  
  481.  
  482.         return tcs.Task;
  483.     }
  484.     public bool IsOpen
  485.     {
  486.         get
  487.         {
  488.             return _handle != null && !_handle.IsInvalid && !_handle.IsClosed;
  489.         }
  490.     }
  491.     public void Open()
  492.     {
  493.         if (IsOpen) return;
  494.         _closing = false;
  495.         _connErrorFired = false;
  496.         var rawHandle = CreateFile(
  497.                     $@"\\.\{_portName}",
  498.                     GENERIC_READ | GENERIC_WRITE,
  499.                     0,
  500.                     IntPtr.Zero,
  501.                     OPEN_EXISTING,
  502.                     FILE_FLAG_OVERLAPPED,
  503.                     IntPtr.Zero);
  504.         if (rawHandle.IsInvalid)
  505.         {
  506.             throw new Win32Exception(Marshal.GetLastWin32Error());
  507.         }
  508.         _handle = rawHandle;
  509.  
  510.         // Bind to the CLR's IOCP thread pool.  All overlapped completions
  511.         // on this handle are now dispatched via the thread pool — no need
  512.         // for manual event handles or RegisterWaitForSingleObject.
  513.         _boundHandle = ThreadPoolBoundHandle.BindHandle(_handle);
  514.  
  515.         DCB dcb = default;
  516.         dcb.DCBlength = (uint)Unsafe.SizeOf<DCB>();
  517.         if (!GetCommState(_handle, ref dcb))
  518.         {
  519.             throw new Win32Exception(Marshal.GetLastWin32Error());
  520.         }
  521.  
  522.         dcb.BaudRate = 115200;
  523.         dcb.ByteSize = 8;
  524.         dcb.Parity = 0;
  525.         dcb.StopBits = 0;
  526.         if (!SetCommState(_handle, ref dcb))
  527.         {
  528.             throw new Win32Exception(Marshal.GetLastWin32Error());
  529.         }
  530.  
  531.         if (!SetCommMask(_handle, EV_RLSD))
  532.         {
  533.             throw new Win32Exception(Marshal.GetLastWin32Error());
  534.         }
  535.         if (!SetupComm(_handle, 8192, 0))
  536.         {
  537.             throw new Win32Exception(Marshal.GetLastWin32Error());
  538.         }
  539.         var timeouts = new COMMTIMEOUTS
  540.         {
  541.             ReadIntervalTimeout = 10, // ms gap between bytes after which the read returns its burst
  542.             ReadTotalTimeoutMultiplier = 0,
  543.             ReadTotalTimeoutConstant = 0,  // 0 total => pend (0 CPU) until the FIRST byte, no overall timeout
  544.             WriteTotalTimeoutMultiplier = 0,
  545.             WriteTotalTimeoutConstant = 0,
  546.         };
  547.         if (!SetCommTimeouts(_handle, ref timeouts))
  548.             throw new Win32Exception(Marshal.GetLastWin32Error());
  549.         RegisterPowerNotification();
  550.  
  551.         _statTask = Task.Factory.StartNew(async () =>
  552.         {
  553.             try
  554.             {
  555.                 while (!_closing)
  556.                 {
  557.                     int mask = await WaitCommEventAsync();
  558.                     if ((mask & (int)EV_RLSD) != 0 && !_closing)
  559.                     {
  560.                         OnConnectionError(EventArgs.Empty);
  561.                         break;
  562.                     }
  563.                 }
  564.             }
  565.             catch (Exception)
  566.             {
  567.                 if (!_closing)
  568.                 {
  569.                     OnConnectionError(EventArgs.Empty);
  570.                 }
  571.             }
  572.         }, default, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
  573.  
  574.         _readTask = Task.Run(async () =>
  575.         {
  576.             byte[] tmp = new byte[1];
  577.             StateMachine mach = default;
  578.             try
  579.             {
  580.                 while (!_closing)
  581.                 {
  582.                     await ReadExactlyAsync(tmp, 1);
  583.                     mach.Step(_log, _logging ? _logLock : null, tmp[0]);
  584.                     if (mach.IsDone)
  585.                     {
  586.                         if (_closing) break;
  587.                         if (mach.Length > 0)
  588.                         {
  589.                             if (mach.Length > 32768)
  590.                             {
  591.                                 throw new IOException("Serial corruption detected in frame");
  592.                             }
  593.                             var frame = new byte[mach.Length];
  594.                             await ReadExactlyAsync(frame, mach.Length);
  595.  
  596.                             if (Crc32(frame) == mach.Crc)
  597.                             {
  598.                                 OnFrameReceived(new FrameReceivedEventArgs(mach.Command, frame));
  599.                             }
  600.                             else
  601.                             {
  602.                                 OnFrameError(new FrameReceivedEventArgs(mach.Command, frame));
  603.                             }
  604.                         }
  605.                         else
  606.                         {
  607.                             if (mach.Crc == UInt32.MaxValue / 3)
  608.                             {
  609.                                 OnFrameReceived(new FrameReceivedEventArgs(mach.Command, Array.Empty<byte>()));
  610.                             }
  611.                             else
  612.                             {
  613.                                 OnFrameError(new FrameReceivedEventArgs(mach.Command, Array.Empty<byte>()));
  614.                             }
  615.  
  616.                         }
  617.                     }
  618.                 }
  619.             }
  620.             catch (Exception)
  621.             {
  622.                 if (!_closing)
  623.                 {
  624.                     OnConnectionError(EventArgs.Empty);
  625.                 }
  626.             }
  627.  
  628.         });
  629.     }
  630.     public void Close()
  631.     {
  632.         UnregisterPowerNotification();
  633.         if (!IsOpen)
  634.         {
  635.             return;
  636.         }
  637.         _closing = true;
  638.         Thread.MemoryBarrier();
  639.         // CancelIoEx unblocks any pending ReadFile / WaitCommEvent.
  640.         // They will complete with ERROR_OPERATION_ABORTED and the
  641.         // IOCP callback will fire, resolving the tasks.
  642.         try
  643.         {
  644.             if (_handle != null && !_handle.IsInvalid && !_handle.IsClosed)
  645.             {
  646.                 CancelIoEx(_handle, IntPtr.Zero);
  647.             }
  648.         }
  649.         catch (Win32Exception) { }
  650.         try
  651.         {
  652.             if (_connErrorFired)
  653.             {
  654.                 if (_statTask != null && _readTask != null)
  655.                 {
  656.                     var tasks = new Task[2];
  657.                     int taskCount = 0;
  658.                     if (_readTask.Status == TaskStatus.Running)
  659.                     {
  660.                         tasks[taskCount++] = _readTask;
  661.                     }
  662.                     if (_statTask.Status == TaskStatus.Running)
  663.                     {
  664.                         tasks[taskCount++] = _statTask;
  665.                     }
  666.                     if (taskCount > 0)
  667.                     {
  668.                         Task.WaitAll(tasks.AsSpan(0, taskCount));
  669.                     }
  670.                 }
  671.             }
  672.         }
  673.         catch (AggregateException)
  674.         {
  675.  
  676.         }
  677.         _boundHandle?.Dispose();
  678.         _handle?.Dispose();
  679.         _closing = false;
  680.         _connErrorFired = false;
  681.     }
  682.  
  683.     private void RegisterPowerNotification()
  684.     {
  685.         // Keep the delegate in a field so it stays rooted for the lifetime of
  686.         // the registration; the OS holds a raw function pointer to it.
  687.         _powerCallback = PowerCallback;
  688.         var sub = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
  689.         {
  690.             Callback = Marshal.GetFunctionPointerForDelegate(_powerCallback),
  691.             Context = IntPtr.Zero
  692.         };
  693.         // Callback-based notification: no window or message pump required, so
  694.         // this works identically in a WinUI3 app and a headless service.
  695.         uint rc = PowerRegisterSuspendResumeNotification(
  696.             DEVICE_NOTIFY_CALLBACK, ref sub, out _powerNotifyHandle);
  697.         if (rc != 0)
  698.         {
  699.             _powerCallback = null;
  700.             _powerNotifyHandle = IntPtr.Zero;
  701.             throw new Win32Exception((int)rc);
  702.         }
  703.     }
  704.  
  705.     private void UnregisterPowerNotification()
  706.     {
  707.         var h = Interlocked.Exchange(ref _powerNotifyHandle, IntPtr.Zero);
  708.         if (h != IntPtr.Zero)
  709.         {
  710.             // Blocks until any in-progress callback returns, so after this the
  711.             // delegate is safe to release. Must NOT be called from inside the
  712.             // callback itself (would deadlock) — see OnSuspend.
  713.             try { PowerUnregisterSuspendResumeNotification(h); }
  714.             catch (Win32Exception) { }
  715.         }
  716.         _powerCallback = null;
  717.     }
  718.  
  719.     private uint PowerCallback(IntPtr context, uint type, IntPtr setting)
  720.     {
  721.         // Runs on an OS power-management thread. Must return promptly and must
  722.         // not block the suspend transition.
  723.         if (type == PBT_APMSUSPEND)
  724.         {
  725.             OnSuspend();
  726.         }
  727.         return 0; // ERROR_SUCCESS
  728.     }
  729.  
  730.     private void OnSuspend()
  731.     {
  732.         if (_closing || _disposed) return;
  733.  
  734.         // Abort any pending overlapped ReadFile / WaitCommEvent so the read and
  735.         // status loops unwind immediately. This is the same teardown path as a
  736.         // cable unplug: the loops complete with ERROR_OPERATION_ABORTED and
  737.         // raise ConnectionError themselves.
  738.         try
  739.         {
  740.             var h = _handle;
  741.             if (h != null && !h.IsInvalid && !h.IsClosed)
  742.             {
  743.                 CancelIoEx(h, IntPtr.Zero);
  744.             }
  745.         }
  746.         catch (Win32Exception) { }
  747.  
  748.         // Notify the host off this callback thread. Raising it inline risks a
  749.         // synchronous ConnectionError handler calling Close(), which would
  750.         // re-enter PowerUnregisterSuspendResumeNotification on the callback
  751.         // thread and deadlock. OnConnectionError is idempotent, so firing here
  752.         // in addition to the loop paths above is harmless.
  753.         ThreadPool.QueueUserWorkItem(_ => OnConnectionError(EventArgs.Empty));
  754.     }
  755.     public EspSerialSession(string port, bool logging = false, SynchronizationContext? syncContext = null)
  756.     {
  757.         _logLock = new object();
  758.         _ioLock = new object();
  759.         _sync = syncContext;
  760.         _log = new List<byte>();
  761.         _portName = port;
  762.         _logging = logging;
  763.  
  764.     }
  765.     public bool IsLogging
  766.     {
  767.         get { return _logging; }
  768.         set { _logging = value; }
  769.     }
  770.     static uint Crc32(ReadOnlySpan<byte> data, uint seed = uint.MaxValue / 3)
  771.     {
  772.         uint result = seed;
  773.         int length = data.Length;
  774.         int i = 0;
  775.         while (length-- > 0)
  776.         {
  777.             result ^= data[i++];
  778.         }
  779.         return result;
  780.     }
  781.  
  782.     #region Unmanaged
  783.     private struct COMSTAT
  784.     {
  785.         public uint Flags;
  786.         public uint cbInQue;
  787.         public uint cbOutQue;
  788.     }
  789.     private struct DCB
  790.     {
  791.         public uint DCBlength;
  792.         public uint BaudRate;
  793.         public uint Flags;
  794.         public ushort wReserved;
  795.         public ushort XonLim;
  796.         public ushort XoffLim;
  797.         public byte ByteSize;
  798.         public byte Parity;
  799.         public byte StopBits;
  800.         public byte XonChar;
  801.         public byte XoffChar;
  802.         public byte ErrorChar;
  803.         public byte EofChar;
  804.         public byte EvtChar;
  805.         public ushort wReserved1;
  806.     }
  807.     const int ERROR_IO_PENDING = 0x000003E5;
  808.     const uint ERROR_OPERATION_ABORTED = 995;
  809.     const uint GENERIC_READ = 0x80000000;
  810.     const uint GENERIC_WRITE = 0x40000000;
  811.     const uint OPEN_EXISTING = 3;
  812.     const uint FILE_FLAG_OVERLAPPED = 0x40000000;
  813.     const uint EV_RLSD = 0x0020;
  814.     const uint DEVICE_NOTIFY_CALLBACK = 0x00000002;
  815.     const uint PBT_APMSUSPEND = 0x0004;
  816.  
  817.     [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
  818.     private static extern SafeFileHandle CreateFile(
  819.         string lpFileName,
  820.         uint dwDesiredAccess,
  821.         uint dwShareMode,
  822.         IntPtr lpSecurityAttributes,
  823.         uint dwCreationDisposition,
  824.         uint dwFlagsAndAttributes,
  825.         IntPtr hTemplateFile);
  826.  
  827.     [DllImport("kernel32.dll", SetLastError = true)]
  828.     private static extern bool SetCommMask(
  829.         SafeFileHandle hFile,
  830.         uint dwEvtMask);
  831.  
  832.     [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
  833.     private static extern bool ClearCommError(
  834.         SafeFileHandle hFile,
  835.         ref int lpErrors,
  836.         ref COMSTAT lpStat);
  837.  
  838.     [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
  839.     private static extern bool GetCommState(
  840.         SafeFileHandle hFile,
  841.         ref DCB lpDCB);
  842.  
  843.     [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
  844.     private static extern bool SetCommState(
  845.         SafeFileHandle hFile,
  846.         ref DCB lpDCB);
  847.  
  848.     [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
  849.     internal static extern bool SetupComm(
  850.         SafeFileHandle hFile,     // handle to communications device
  851.         int dwInQueue,  // size of input buffer
  852.         int dwOutQueue  // size of output buffer
  853.         );
  854.     [StructLayout(LayoutKind.Sequential)]
  855.     private struct COMMTIMEOUTS
  856.     {
  857.         public uint ReadIntervalTimeout;
  858.         public uint ReadTotalTimeoutMultiplier;
  859.         public uint ReadTotalTimeoutConstant;
  860.         public uint WriteTotalTimeoutMultiplier;
  861.         public uint WriteTotalTimeoutConstant;
  862.     }
  863.  
  864.     [DllImport("kernel32.dll", SetLastError = true)]
  865.     private static extern bool SetCommTimeouts(SafeFileHandle hFile, ref COMMTIMEOUTS lpCommTimeouts);
  866.  
  867.     [DllImport("kernel32.dll", SetLastError = true)]
  868.     private static extern unsafe bool WaitCommEvent(
  869.         SafeFileHandle hFile,
  870.         ref int lpEvtMask,
  871.         NativeOverlapped* lpOverlapped);
  872.  
  873.     [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
  874.     private static extern unsafe bool GetOverlappedResult(
  875.         SafeFileHandle hFile,
  876.         NativeOverlapped* lpOverlapped,
  877.         ref int lpNumberOfBytesTransferred,
  878.         bool bWait);
  879.  
  880.     [DllImport("kernel32.dll", SetLastError = true)]
  881.     private static extern unsafe bool ReadFile(
  882.         SafeFileHandle hFile,
  883.         byte* lpBuffer,
  884.         int nNumberOfBytesToRead,
  885.         ref int lpNumberOfBytesRead,
  886.         NativeOverlapped* lpOverlapped);
  887.  
  888.     [DllImport("kernel32.dll", SetLastError = true)]
  889.     private static extern unsafe bool WriteFile(
  890.         SafeFileHandle hFile,
  891.         byte* lpBuffer,
  892.         int nNumberOfBytesToWrite,
  893.         ref int lpNumberOfBytesWritten,
  894.         NativeOverlapped* lpOverlapped);
  895.  
  896.     [DllImport("kernel32.dll", SetLastError = true)]
  897.     private static extern bool CancelIoEx(
  898.         SafeFileHandle hFile,
  899.         IntPtr lpOverlapped);  // IntPtr.Zero = cancel all
  900.  
  901.     [UnmanagedFunctionPointer(CallingConvention.StdCall)]
  902.     private delegate uint DeviceNotifyCallbackRoutine(IntPtr context, uint type, IntPtr setting);
  903.  
  904.     [StructLayout(LayoutKind.Sequential)]
  905.     private struct DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
  906.     {
  907.         public IntPtr Callback; // PDEVICE_NOTIFY_CALLBACK_ROUTINE (function pointer)
  908.         public IntPtr Context;
  909.     }
  910.  
  911.     [DllImport("powrprof.dll", SetLastError = false)]
  912.     private static extern uint PowerRegisterSuspendResumeNotification(
  913.         uint flags,
  914.         ref DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS recipient,
  915.         out IntPtr registrationHandle);
  916.  
  917.     [DllImport("powrprof.dll", SetLastError = false)]
  918.     private static extern uint PowerUnregisterSuspendResumeNotification(IntPtr registrationHandle);
  919.     #endregion
  920. }
  921. #pragma warning restore
Advertisement
Add Comment
Please, Sign In to add comment