Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- using Microsoft.Win32.SafeHandles;
- using System.Buffers.Binary;
- using System.ComponentModel;
- using System.Runtime.CompilerServices;
- using System.Runtime.InteropServices;
- namespace Espmon;
- public sealed class FrameReceivedEventArgs : EventArgs
- {
- public byte Command;
- public byte[] Data { get; }
- public FrameReceivedEventArgs(byte command, byte[] data)
- {
- ArgumentOutOfRangeException.ThrowIfLessThan(command,128,nameof(command));
- Command = (byte)(command>>1);
- Data = data;
- }
- }
- internal partial class EspSerialSession : IDisposable
- {
- struct StateMachine
- {
- int state;
- byte cmd;
- int len;
- uint crc;
- public byte RawCommandByte => cmd;
- public byte Command => (byte)(cmd >> 1);
- public int Length => len;
- public uint Crc => crc;
- public bool IsDone => state == 16;
- public void Reset() => state = 0;
- public bool Step(List<byte>? log, object? logLock, byte data)
- {
- if (state == 0 || state ==16)
- {
- if (data <128)
- {
- if (logLock != null)
- {
- lock (logLock)
- {
- log?.Add(data);
- }
- }
- return false;
- }
- state = 1;
- cmd = data;
- len = 0;
- }
- else if (state < 8)
- {
- if (cmd != data)
- {
- if (logLock != null)
- {
- lock (logLock)
- {
- for (var i = 0; i<state;++i)
- {
- log?.Add(cmd);
- }
- log?.Add(data);
- }
- }
- state = 0;
- return false;
- }
- ++state;
- }
- else if (state == 8)
- {
- len = data;
- ++state;
- }
- else if (state < 12)
- {
- len <<= 8;
- len |= data;
- ++state;
- }
- else if (state == 12)
- {
- crc = data;
- ++state;
- }
- else if (state < 16)
- {
- crc <<= 8;
- crc |= data;
- ++state;
- }
- else if (state == 16)
- {
- // do nothing "stall"
- }
- else
- {
- state = 0;
- return false;
- }
- return true;
- }
- }
- /// <summary>
- /// Captures the state for one pending overlapped I/O so the
- /// IOCallback can resolve the right TaskCompletionSource.
- /// </summary>
- private sealed class ReadOp
- {
- public TaskCompletionSource<int> Tcs = null!;
- public unsafe NativeOverlapped* Overlapped;
- }
- private sealed class CommEventOp
- {
- public TaskCompletionSource<int> Tcs = null!;
- public unsafe NativeOverlapped* Overlapped;
- public GCHandle MaskHandle;
- }
- private volatile bool _closing;
- private bool _disposed;
- SafeFileHandle _handle;
- ThreadPoolBoundHandle _boundHandle;
- readonly List<byte> _log;
- readonly object _lock;
- SynchronizationContext? _sync;
- Task _readTask, _statTask;
- public event EventHandler<EventArgs>? Disconnected;
- public event EventHandler<FrameReceivedEventArgs>? FrameReceived;
- public event EventHandler<FrameReceivedEventArgs>? FrameCrcError;
- protected virtual void Dispose(bool disposing)
- {
- if (!_disposed)
- {
- if (disposing)
- {
- _closing = true;
- // CancelIoEx unblocks any pending ReadFile / WaitCommEvent.
- // They will complete with ERROR_OPERATION_ABORTED and the
- // IOCP callback will fire, resolving the tasks.
- CancelIoEx(_handle, IntPtr.Zero);
- Task.WaitAll([_statTask, _readTask]);
- _boundHandle.Dispose();
- _handle.Dispose();
- }
- _disposed = true;
- }
- }
- ~EspSerialSession()
- {
- Dispose(false);
- }
- public void Dispose()
- {
- Dispose(disposing: true);
- GC.SuppressFinalize(this);
- }
- public void Send(byte cmd, ReadOnlySpan<byte> data)
- {
- ArgumentOutOfRangeException.ThrowIfGreaterThan(cmd, 127, nameof(cmd));
- cmd <<= 1;
- Span<byte> len = [0, 0, 0, 0];
- BinaryPrimitives.WriteInt32LittleEndian(len, data.Length);
- Span<byte> crc = [0, 0, 0, 0];
- BinaryPrimitives.WriteUInt32LittleEndian(crc, Crc32(data));
- Span<byte> toWrite = [cmd, cmd, cmd, cmd, cmd, cmd, cmd, cmd, .. len, .. crc, .. data];
- WriteAll(toWrite);
- }
- private unsafe void WriteAll(ReadOnlySpan<byte> data)
- {
- fixed (byte* ptr = data)
- {
- int offset = 0;
- while (offset < data.Length)
- {
- var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
- var ov = _boundHandle.AllocateNativeOverlapped(
- (errorCode, numBytes, pOv) =>
- {
- _boundHandle.FreeNativeOverlapped(pOv);
- if (errorCode == 0)
- tcs.TrySetResult((int)numBytes);
- else
- tcs.TrySetException(new Win32Exception((int)errorCode));
- },
- null, null);
- int written = 0;
- if (!WriteFile(_handle, ptr + offset, data.Length - offset, ref written, ov))
- {
- int err = Marshal.GetLastWin32Error();
- if (err != ERROR_IO_PENDING)
- {
- _boundHandle.FreeNativeOverlapped(ov);
- throw new Win32Exception(err);
- }
- // IO_PENDING — IOCP callback will fire
- }
- else
- {
- // Completed synchronously, but IOCP callback will still fire
- // for handles bound to IOCP. Wait for it.
- }
- // Block for write completion (Send is synchronous from caller's perspective)
- written = tcs.Task.GetAwaiter().GetResult();
- offset += written;
- }
- }
- }
- private void OnDisconnected(EventArgs args)
- {
- if (_disposed) return;
- if (Disconnected != null)
- {
- if (_sync == null)
- {
- Disconnected?.Invoke(this, args);
- }
- else
- {
- _sync.Post((state) => Disconnected?.Invoke(this, args), null);
- }
- }
- }
- public byte[] GetNextLogData()
- {
- lock (_lock)
- {
- var res = _log.ToArray();
- _log.Clear();
- return res;
- }
- }
- private void OnFrameReceived(FrameReceivedEventArgs args)
- {
- if (_disposed) return;
- if (FrameReceived != null)
- {
- if (_sync == null)
- {
- FrameReceived?.Invoke(this, args);
- }
- else
- {
- _sync.Post((state) => FrameReceived?.Invoke(this, args), null);
- }
- }
- }
- private void OnFrameCrcError(FrameReceivedEventArgs args)
- {
- if (_disposed) return;
- if (FrameCrcError != null)
- {
- if (_sync == null)
- {
- FrameCrcError?.Invoke(this, args);
- }
- else
- {
- _sync.Post((state) => FrameCrcError?.Invoke(this, args), null);
- }
- }
- }
- /// <summary>
- /// Overlapped read via IOCP. The CLR's thread pool dispatches the
- /// completion callback — no manual event handles or registered waits.
- /// </summary>
- private unsafe Task<int> ReadAsync(byte[] buffer, int offset, int count)
- {
- var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
- var ov = _boundHandle.AllocateNativeOverlapped(
- (errorCode, numBytes, pOv) =>
- {
- _boundHandle.FreeNativeOverlapped(pOv);
- if (errorCode == 0)
- tcs.TrySetResult((int)numBytes);
- else if (errorCode == ERROR_OPERATION_ABORTED)
- tcs.TrySetCanceled();
- else
- tcs.TrySetException(new Win32Exception((int)errorCode));
- },
- null,
- buffer); // pins buffer until FreeNativeOverlapped
- int read = 0;
- fixed (byte* pBuf = &buffer[offset])
- {
- if (!ReadFile(_handle, pBuf, count, ref read, ov))
- {
- int err = Marshal.GetLastWin32Error();
- if (err != ERROR_IO_PENDING)
- {
- _boundHandle.FreeNativeOverlapped(ov);
- if (err == ERROR_OPERATION_ABORTED)
- tcs.TrySetCanceled();
- else
- tcs.TrySetException(new Win32Exception(err));
- }
- // else: pending — IOCP callback will fire
- }
- // else: completed synchronously — IOCP callback still fires for bound handles
- }
- return tcs.Task;
- }
- private async Task ReadExactlyAsync(byte[] buffer, int count)
- {
- int offset = 0;
- while (offset < count)
- {
- int n = await ReadAsync(buffer, offset, count - offset);
- if (n == 0)
- throw new IOException("Serial port returned 0 bytes (device disconnected).");
- offset += n;
- }
- }
- /// <summary>
- /// Overlapped WaitCommEvent via IOCP.
- /// WaitCommEvent writes the event mask to an int — we pin it via GCHandle
- /// and read it in the callback.
- /// </summary>
- private unsafe Task<int> WaitCommEventAsync()
- {
- var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
- var maskArr = new int[1];
- var maskPin = GCHandle.Alloc(maskArr, GCHandleType.Pinned);
- var ov = _boundHandle.AllocateNativeOverlapped(
- (errorCode, numBytes, pOv) =>
- {
- int mask = maskArr[0];
- maskPin.Free();
- _boundHandle.FreeNativeOverlapped(pOv);
- if (errorCode == 0)
- tcs.TrySetResult(mask);
- else if (errorCode == ERROR_OPERATION_ABORTED)
- tcs.TrySetCanceled();
- else
- tcs.TrySetException(new Win32Exception((int)errorCode));
- },
- null, null);
- if (!WaitCommEvent(_handle, ref maskArr[0], ov))
- {
- int err = Marshal.GetLastWin32Error();
- if (err != ERROR_IO_PENDING)
- {
- maskPin.Free();
- _boundHandle.FreeNativeOverlapped(ov);
- if (err == ERROR_OPERATION_ABORTED)
- tcs.TrySetCanceled();
- else
- tcs.TrySetException(new Win32Exception(err));
- }
- }
- return tcs.Task;
- }
- public EspSerialSession(string port, bool logging = false, SynchronizationContext? syncContext = null)
- {
- _lock = new object();
- _sync = syncContext;
- _log = new List<byte>();
- var rawHandle = CreateFile(
- $@"\\.\{port}",
- GENERIC_READ | GENERIC_WRITE,
- 0,
- IntPtr.Zero,
- OPEN_EXISTING,
- FILE_FLAG_OVERLAPPED,
- IntPtr.Zero);
- if (rawHandle.IsInvalid)
- {
- throw new Win32Exception(Marshal.GetLastWin32Error());
- }
- _handle = rawHandle;
- // Bind to the CLR's IOCP thread pool. All overlapped completions
- // on this handle are now dispatched via the thread pool — no need
- // for manual event handles or RegisterWaitForSingleObject.
- _boundHandle = ThreadPoolBoundHandle.BindHandle(_handle);
- DCB dcb = default;
- dcb.DCBlength = (uint)Unsafe.SizeOf<DCB>();
- if (!GetCommState(_handle, ref dcb))
- {
- throw new Win32Exception(Marshal.GetLastWin32Error());
- }
- dcb.BaudRate = 115200;
- dcb.ByteSize = 8;
- dcb.Parity = 0;
- dcb.StopBits = 0;
- if (!SetCommState(_handle, ref dcb))
- {
- throw new Win32Exception(Marshal.GetLastWin32Error());
- }
- if (!SetCommMask(_handle, EV_RLSD))
- {
- throw new Win32Exception(Marshal.GetLastWin32Error());
- }
- if(!SetupComm(_handle,8192,0))
- {
- throw new Win32Exception(Marshal.GetLastWin32Error());
- }
- _statTask = Task.Run(async () =>
- {
- try
- {
- while (!_closing)
- {
- int mask = await WaitCommEventAsync();
- if ((mask & (int)EV_RLSD) != 0 && !_closing)
- {
- OnDisconnected(EventArgs.Empty);
- break;
- }
- }
- }
- catch (OperationCanceledException) { }
- catch (Exception)
- {
- if (!_closing)
- OnDisconnected(EventArgs.Empty);
- }
- });
- _readTask = Task.Run(async () =>
- {
- byte[] tmp = new byte[1];
- StateMachine mach = default;
- try
- {
- while (!_closing)
- {
- await ReadExactlyAsync(tmp, 1);
- mach.Step(_log, _lock, tmp[0]);
- if (mach.IsDone)
- {
- if (_closing) break;
- var frame = new byte[mach.Length];
- await ReadExactlyAsync(frame, mach.Length);
- if (Crc32(frame) == mach.Crc)
- {
- OnFrameReceived(new FrameReceivedEventArgs(mach.RawCommandByte, frame));
- }
- else
- {
- OnFrameCrcError(new FrameReceivedEventArgs(mach.RawCommandByte, frame));
- }
- }
- }
- }
- catch (OperationCanceledException) { }
- catch (IOException)
- {
- if (!_closing)
- OnDisconnected(EventArgs.Empty);
- }
- });
- }
- static uint Crc32(ReadOnlySpan<byte> data, uint seed = uint.MaxValue / 3)
- {
- uint result = seed;
- int length = data.Length;
- int i = 0;
- while (length-- > 0)
- {
- result ^= data[i++];
- }
- return result;
- }
- #region Unmanaged
- private struct COMSTAT
- {
- public uint Flags;
- public uint cbInQue;
- public uint cbOutQue;
- }
- private struct DCB
- {
- public uint DCBlength;
- public uint BaudRate;
- public uint Flags;
- public ushort wReserved;
- public ushort XonLim;
- public ushort XoffLim;
- public byte ByteSize;
- public byte Parity;
- public byte StopBits;
- public byte XonChar;
- public byte XoffChar;
- public byte ErrorChar;
- public byte EofChar;
- public byte EvtChar;
- public ushort wReserved1;
- }
- const int ERROR_IO_PENDING = 0x000003E5;
- const uint ERROR_OPERATION_ABORTED = 995;
- const uint GENERIC_READ = 0x80000000;
- const uint GENERIC_WRITE = 0x40000000;
- const uint OPEN_EXISTING = 3;
- const uint FILE_FLAG_OVERLAPPED = 0x40000000;
- const uint EV_RLSD = 0x0020;
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
- private static extern SafeFileHandle CreateFile(
- string lpFileName,
- uint dwDesiredAccess,
- uint dwShareMode,
- IntPtr lpSecurityAttributes,
- uint dwCreationDisposition,
- uint dwFlagsAndAttributes,
- IntPtr hTemplateFile);
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern bool SetCommMask(
- SafeFileHandle hFile,
- uint dwEvtMask);
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
- private static extern bool ClearCommError(
- SafeFileHandle hFile,
- ref int lpErrors,
- ref COMSTAT lpStat);
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
- private static extern bool GetCommState(
- SafeFileHandle hFile,
- ref DCB lpDCB);
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
- private static extern bool SetCommState(
- SafeFileHandle hFile,
- ref DCB lpDCB);
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
- internal static extern bool SetupComm(
- SafeFileHandle hFile, // handle to communications device
- int dwInQueue, // size of input buffer
- int dwOutQueue // size of output buffer
- );
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern unsafe bool WaitCommEvent(
- SafeFileHandle hFile,
- ref int lpEvtMask,
- NativeOverlapped* lpOverlapped);
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
- private static extern unsafe bool GetOverlappedResult(
- SafeFileHandle hFile,
- NativeOverlapped* lpOverlapped,
- ref int lpNumberOfBytesTransferred,
- bool bWait);
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern unsafe bool ReadFile(
- SafeFileHandle hFile,
- byte* lpBuffer,
- int nNumberOfBytesToRead,
- ref int lpNumberOfBytesRead,
- NativeOverlapped* lpOverlapped);
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern unsafe bool WriteFile(
- SafeFileHandle hFile,
- byte* lpBuffer,
- int nNumberOfBytesToWrite,
- ref int lpNumberOfBytesWritten,
- NativeOverlapped* lpOverlapped);
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern bool CancelIoEx(
- SafeFileHandle hFile,
- IntPtr lpOverlapped); // IntPtr.Zero = cancel all
- #endregion
- }
Advertisement