using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using KVMote.Transport; namespace KVMote { public partial class Principal : Form { // ── P/Invoke ─────────────────────────────────────────────────────────────── private delegate IntPtr LowLevelProc(int nCode, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll")] private static extern IntPtr SetWindowsHookEx(int id, LowLevelProc fn, IntPtr mod, uint tid); [DllImport("user32.dll")] private static extern bool UnhookWindowsHookEx(IntPtr h); [DllImport("user32.dll")] private static extern IntPtr CallNextHookEx(IntPtr h, int n, IntPtr w, IntPtr l); [DllImport("user32.dll")] private static extern bool SetCursorPos(int x, int y); [DllImport("user32.dll")] private static extern bool ClipCursor(ref RECT r); [DllImport("user32.dll")] private static extern bool ClipCursor(IntPtr zero); [DllImport("kernel32.dll")] private static extern IntPtr GetModuleHandle(string? n); [DllImport("user32.dll")] private static extern short GetKeyState(int vk); [StructLayout(LayoutKind.Sequential)] private struct POINT { public int x, y; } [StructLayout(LayoutKind.Sequential)] private struct RECT { public int Left, Top, Right, Bottom; } [StructLayout(LayoutKind.Sequential)] private struct MSLLHOOKSTRUCT { public POINT pt; public uint mouseData, flags, time; public IntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct KBDLLHOOKSTRUCT { public uint vk, sc, flags, time; public IntPtr dwExtraInfo; } // ── Constants ────────────────────────────────────────────────────────────── private const int WH_MOUSE_LL = 14; private const int WH_KEYBOARD_LL = 13; private const int WM_MOUSEMOVE = 0x0200; private const int WM_LBUTTONDOWN = 0x0201; private const int WM_LBUTTONUP = 0x0202; private const int WM_RBUTTONDOWN = 0x0204; private const int WM_RBUTTONUP = 0x0205; private const int WM_KEYDOWN = 0x0100; private const int WM_KEYUP = 0x0101; private const int WM_SYSKEYDOWN = 0x0104; private const int WM_SYSKEYUP = 0x0105; private const int WM_MOUSEWHEEL = 0x020A; private const int MouseThrottleMs = 50; private const int ReturnThreshold = 15; private const int HeartbeatMs = 3000; private const int PongTimeoutMs = 9000; // ── Enum ─────────────────────────────────────────────────────────────────── private enum ClientPos { None, Left, Right, Above, Below } private enum ClientLayout { US, PtBrAbnt2 } // ── Fields ───────────────────────────────────────────────────────────────── // Transport (strategy) private IKvmTransport? _transport; private bool _userConnected, _isReconnecting; private DateTime _lastPong = DateTime.MinValue; // Hooks private LowLevelProc? _mouseProc, _keyProc; private IntPtr _mouseHook, _keyHook; // KVM state private bool _clientMode; private bool _ctrlHeld, _shiftHeld, _altHeld; private ClientLayout _clientLayout = ClientLayout.US; private ClientPos _clientPos = ClientPos.None; private System.Drawing.Point _edgeEntry; private System.Drawing.Point _lastRawPos; private int _virtualX, _virtualY; private int _pendingDX, _pendingDY; private bool _isWarping; private readonly Stopwatch _mouseThrottle = Stopwatch.StartNew(); private int _wheelAccum; private bool _scrollActive; private readonly Stopwatch _scrollTimer = new Stopwatch(); private bool _clipboardReady; // Timers private readonly System.Windows.Forms.Timer _watchdog = new System.Windows.Forms.Timer { Interval = 2000 }; private System.Threading.Timer? _heartbeat; // Auto-detect private CancellationTokenSource? _detectCts; // ── Constructor ──────────────────────────────────────────────────────────── public Principal() { InitializeComponent(); _watchdog.Tick += OnWatchdog; SetStatus("Desconectado", System.Drawing.Color.Gray); btnConnect.Enabled = false; _ = AutoDetectAsync(); } // ══════════════════════════════════════════════════════════════════════════ // SECTION 1 — Auto-detect (Serial + BLE em paralelo — Opção A) // ══════════════════════════════════════════════════════════════════════════ private async Task AutoDetectAsync() { _detectCts?.Cancel(); _detectCts = new CancellationTokenSource(); var token = _detectCts.Token; SetPortInfo("Detectando..."); if (btnConnect.InvokeRequired) btnConnect.Invoke((Action)(() => btnConnect.Enabled = false)); else btnConnect.Enabled = false; // Fast path: reutiliza transport existente (endereço já conhecido) if (_transport is not null) { try { await Task.Delay(2500, token); } catch { return; } if (!token.IsCancellationRequested && await _transport.DetectAsync(token)) { SetPortInfo($"\u25cf {_transport.DeviceLabel} detectado"); if (btnConnect.InvokeRequired) btnConnect.Invoke((Action)(() => btnConnect.Enabled = true)); else btnConnect.Enabled = true; return; } // Dispositivo não responde mais — descarta e faz scan completo _transport.Dispose(); _transport = null; } while (!token.IsCancellationRequested) { IKvmTransport? found = await DetectRaceAsync(token); if (found is not null) { _transport?.Dispose(); _transport = found; SetPortInfo($"\u25cf {found.DeviceLabel} detectado"); if (btnConnect.InvokeRequired) btnConnect.Invoke((Action)(() => btnConnect.Enabled = true)); else btnConnect.Enabled = true; return; } SetPortInfo("Nenhum dispositivo encontrado..."); try { await Task.Delay(3000, token); } catch { return; } } } // Serial e BLE disputam: primeiro a responder [PONG] vence. // Serial termina rápido (varre COMs). BLE tem timeout interno de 8s. // Se ambos falharem, AutoDetectAsync retenta após 3s. private static async Task DetectRaceAsync(CancellationToken outerCt) { using var raceCts = CancellationTokenSource.CreateLinkedTokenSource(outerCt); var ct = raceCts.Token; IKvmTransport[] candidates = [new SerialTransport(), new BleTransport()]; Task[] tasks = candidates.Select(t => t.DetectAsync(ct)).ToArray(); var pending = new List>(tasks); IKvmTransport? winner = null; while (pending.Count > 0) { var done = await Task.WhenAny(pending); pending.Remove(done); bool found = false; try { found = await done; } catch { } if (found) { int idx = Array.IndexOf(tasks, done); winner = candidates[idx]; raceCts.Cancel(); // cancela o perdedor break; } } // Descarta os transportes que não venceram for (int i = 0; i < candidates.Length; i++) if (candidates[i] != winner) candidates[i].Dispose(); return winner; } // ══════════════════════════════════════════════════════════════════════════ // SECTION 2 — Position selector // ══════════════════════════════════════════════════════════════════════════ private void btnPosition_Click(object sender, EventArgs e) { if (sender is not Button btn) return; var normal = System.Drawing.Color.FromArgb(55, 55, 55); btnAbove.BackColor = normal; btnLeft.BackColor = normal; btnRight.BackColor = normal; btnBelow.BackColor = normal; btn.BackColor = System.Drawing.Color.FromArgb(0, 122, 204); btn.ForeColor = System.Drawing.Color.White; _clientPos = btn.Name switch { "btnAbove" => ClientPos.Above, "btnLeft" => ClientPos.Left, "btnRight" => ClientPos.Right, "btnBelow" => ClientPos.Below, _ => ClientPos.None }; } // ══════════════════════════════════════════════════════════════════════════ // SECTION 3 — Detect button // ══════════════════════════════════════════════════════════════════════════ private void btnDetect_Click(object sender, EventArgs e) => _ = AutoDetectAsync(); // ══════════════════════════════════════════════════════════════════════════ // SECTION 4 — Connect / Disconnect // ══════════════════════════════════════════════════════════════════════════ private async void btnConnect_Click(object sender, EventArgs e) { if (!_userConnected) await ConnectAsync(); else Disconnect(true); } private async Task ConnectAsync() { if (_clientPos == ClientPos.None) { MessageBox.Show("Selecione a posição do PC Cliente.", "KVMote", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (_transport is null) { MessageBox.Show("Nenhum dispositivo detectado. Clique em Detectar.", "KVMote", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } SetStatus("Conectando...", System.Drawing.Color.Orange); btnConnect.Enabled = false; if (await _transport.ConnectAsync()) { _transport.DataReceived += OnTransportData; _userConnected = true; _lastPong = DateTime.MinValue; _watchdog.Start(); _heartbeat = new System.Threading.Timer(OnHeartbeat, null, HeartbeatMs, HeartbeatMs); InstallHooks(); SetConnectedUI(true); _transport.Send(new byte[] { (byte)'H' }); // LED azul no Arduino/ESP32 SetStatus("Modo Host \u25cf Conectado", System.Drawing.Color.Green); } else { SetStatus("Falha na conexão", System.Drawing.Color.Red); btnConnect.Enabled = true; _ = AutoDetectAsync(); // reinicia detecção (ex: BLE resetou endereço após AccessDenied) } } private void Disconnect(bool userInitiated) { ExitClientMode(sendRelease: false); _detectCts?.Cancel(); _userConnected = false; _isReconnecting = false; _watchdog.Stop(); _heartbeat?.Dispose(); _heartbeat = null; UninstallHooks(); if (_transport is not null) { _transport.DataReceived -= OnTransportData; if (_transport.IsConnected) try { _transport.Send(new byte[] { (byte)'G' }); } catch { } // LED verde _transport.Disconnect(); } SetConnectedUI(false); if (userInitiated) { SetStatus("Desconectado", System.Drawing.Color.Gray); _ = AutoDetectAsync(); // reinicia detecção automática } } private void SetConnectedUI(bool connected) { if (InvokeRequired) { Invoke((Action)(() => SetConnectedUI(connected))); return; } btnConnect.Text = connected ? "Desconectar" : "Conectar"; btnConnect.Enabled = true; btnDetect.Enabled = !connected; btnAbove.Enabled = !connected; btnLeft.Enabled = !connected; btnRight.Enabled = !connected; btnBelow.Enabled = !connected; } // ══════════════════════════════════════════════════════════════════════════ // SECTION 5 — Transport data + Watchdog + Heartbeat + Reconnect // ══════════════════════════════════════════════════════════════════════════ private void OnTransportData(string data) { if (data.Contains("[PONG]")) _lastPong = DateTime.UtcNow; } private void OnWatchdog(object? sender, EventArgs e) { if (!_userConnected || _isReconnecting) return; if (_transport is null || !_transport.IsConnected) BeginReconnect(); } private void OnHeartbeat(object? state) { if (!_userConnected || _isReconnecting) return; if (_lastPong != DateTime.MinValue && (DateTime.UtcNow - _lastPong).TotalMilliseconds > PongTimeoutMs) { Log("PONG timeout. Reconectando..."); BeginReconnect(); return; } _transport?.Send(new byte[] { (byte)'~' }); } private void BeginReconnect() { if (_isReconnecting) return; _isReconnecting = true; if (InvokeRequired) Invoke((Action)(() => ExitClientMode(sendRelease: false))); else ExitClientMode(sendRelease: false); SetStatus("Reconectando...", System.Drawing.Color.Orange); if (InvokeRequired) Invoke((Action)(() => SetConnectedUI(false))); else SetConnectedUI(false); _transport?.Disconnect(); Task.Run(async () => { while (_isReconnecting && _userConnected) { await Task.Delay(2500); if (_transport is null) break; // Re-detecta antes de conectar: BLE pode ter resetado o endereço // após AccessDenied+unpair; fast-path retorna imediatamente se válido. using var detectCts = new CancellationTokenSource(10000); if (!await _transport.DetectAsync(detectCts.Token)) continue; if (await _transport.ConnectAsync()) { _isReconnecting = false; _lastPong = DateTime.MinValue; SetStatus("Reconectado", System.Drawing.Color.Green); if (InvokeRequired) Invoke((Action)(() => SetConnectedUI(true))); else SetConnectedUI(true); return; } } }); } // ══════════════════════════════════════════════════════════════════════════ // SECTION 6 — Global Hooks // ══════════════════════════════════════════════════════════════════════════ private void InstallHooks() { _mouseProc = MouseHookCallback; _keyProc = KeyboardHookCallback; IntPtr mod = GetModuleHandle(null); _mouseHook = SetWindowsHookEx(WH_MOUSE_LL, _mouseProc, mod, 0); _keyHook = SetWindowsHookEx(WH_KEYBOARD_LL, _keyProc, mod, 0); } private void UninstallHooks() { if (_mouseHook != IntPtr.Zero) { UnhookWindowsHookEx(_mouseHook); _mouseHook = IntPtr.Zero; } if (_keyHook != IntPtr.Zero) { UnhookWindowsHookEx(_keyHook); _keyHook = IntPtr.Zero; } } // ══════════════════════════════════════════════════════════════════════════ // SECTION 7 — Mouse Hook Callback // ══════════════════════════════════════════════════════════════════════════ private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode < 0 || !_userConnected) return CallNextHookEx(_mouseHook, nCode, wParam, lParam); var info = Marshal.PtrToStructure(lParam); var cursorPt = new System.Drawing.Point(info.pt.x, info.pt.y); int msg = (int)wParam; if (!_clientMode) { if (msg == WM_MOUSEMOVE && _clientPos != ClientPos.None && !_isReconnecting && IsAtExitEdge(cursorPt)) EnterClientMode(cursorPt); return CallNextHookEx(_mouseHook, nCode, wParam, lParam); } if (msg == WM_MOUSEMOVE) { if (_isWarping) { _isWarping = false; return (IntPtr)1; } // Suppress warp during two-finger touchpad scroll (gesture-driver conflict) if (_scrollActive) { if (_scrollTimer.ElapsedMilliseconds > 150) _scrollActive = false; else { _lastRawPos = new System.Drawing.Point(info.pt.x, info.pt.y); return (IntPtr)1; } } int dx = info.pt.x - _lastRawPos.X; int dy = info.pt.y - _lastRawPos.Y; _virtualX += dx; _virtualY += dy; _pendingDX += dx; _pendingDY += dy; if (ShouldReturnToHost()) { ExitClientMode(sendRelease: true); return (IntPtr)1; } _isWarping = true; SetCursorPos(_lastRawPos.X, _lastRawPos.Y); if (_mouseThrottle.ElapsedMilliseconds >= MouseThrottleMs && (_pendingDX != 0 || _pendingDY != 0)) { _mouseThrottle.Restart(); int sdx = Math.Clamp(_pendingDX, -127, 127); int sdy = Math.Clamp(_pendingDY, -127, 127); _pendingDX = 0; _pendingDY = 0; _transport?.SendLossy(new byte[] { (byte)'M', (byte)(sbyte)sdx, (byte)(sbyte)sdy }); } return (IntPtr)1; } if (msg == WM_LBUTTONDOWN) { _transport?.Send(new byte[] { (byte)'D', (byte)'L' }); return (IntPtr)1; } if (msg == WM_LBUTTONUP) { _transport?.Send(new byte[] { (byte)'E', (byte)'L' }); return (IntPtr)1; } if (msg == WM_RBUTTONDOWN) { _transport?.Send(new byte[] { (byte)'D', (byte)'R' }); return (IntPtr)1; } if (msg == WM_RBUTTONUP) { _transport?.Send(new byte[] { (byte)'E', (byte)'R' }); return (IntPtr)1; } if (msg == WM_MOUSEWHEEL) { _scrollActive = true; _scrollTimer.Restart(); _wheelAccum += (short)(info.mouseData >> 16); int toSend = _wheelAccum / 120; if (toSend != 0) { _wheelAccum -= toSend * 120; _transport?.Send(new byte[] { (byte)'W', (byte)(sbyte)Math.Clamp(toSend, -127, 127) }); } return (IntPtr)1; } return (IntPtr)1; } private bool IsAtExitEdge(System.Drawing.Point p) { var s = Screen.PrimaryScreen!.Bounds; return _clientPos switch { ClientPos.Right => p.X >= s.Right - 1, ClientPos.Left => p.X <= s.Left, ClientPos.Above => p.Y <= s.Top, ClientPos.Below => p.Y >= s.Bottom - 1, _ => false }; } private bool ShouldReturnToHost() => _clientPos switch { ClientPos.Right => _virtualX < -ReturnThreshold, ClientPos.Left => _virtualX > ReturnThreshold, ClientPos.Below => _virtualY < -ReturnThreshold, ClientPos.Above => _virtualY > ReturnThreshold, _ => false }; private void EnterClientMode(System.Drawing.Point edgePt) { _clientMode = true; _edgeEntry = edgePt; _virtualX = 0; _virtualY = 0; _pendingDX = 0; _pendingDY = 0; _mouseThrottle.Restart(); var s = Screen.PrimaryScreen!.Bounds; var center = new System.Drawing.Point(s.Left + s.Width / 2, s.Top + s.Height / 2); _lastRawPos = center; _isWarping = true; SetCursorPos(center.X, center.Y); Cursor.Hide(); _transport?.Send(new byte[] { (byte)'O' }); // LED magenta SetStatus("Modo Cliente \u25cf", System.Drawing.Color.DodgerBlue); } private void ExitClientMode(bool sendRelease) { if (!_clientMode) return; _clientMode = false; _isWarping = false; _ctrlHeld = false; _shiftHeld = false; Cursor.Show(); var s = Screen.PrimaryScreen!.Bounds; var ret = _clientPos switch { ClientPos.Right => new System.Drawing.Point(s.Right - 40, _edgeEntry.Y), ClientPos.Left => new System.Drawing.Point(s.Left + 40, _edgeEntry.Y), ClientPos.Above => new System.Drawing.Point(_edgeEntry.X, s.Top + 40), ClientPos.Below => new System.Drawing.Point(_edgeEntry.X, s.Bottom - 40), _ => _edgeEntry }; SetCursorPos(ret.X, ret.Y); if (sendRelease) _transport?.Send(new byte[] { (byte)'A' }); _transport?.Send(new byte[] { (byte)'H' }); // LED azul SetStatus("Modo Host \u25cf Conectado", System.Drawing.Color.Green); } // ══════════════════════════════════════════════════════════════════════════ // SECTION 8 — Keyboard Hook Callback // ══════════════════════════════════════════════════════════════════════════ private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode < 0 || !_userConnected) return CallNextHookEx(_keyHook, nCode, wParam, lParam); int msg = (int)wParam; bool isDown = msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN; bool isUp = msg == WM_KEYUP || msg == WM_SYSKEYUP; if (!isDown && !isUp) return CallNextHookEx(_keyHook, nCode, wParam, lParam); var info = Marshal.PtrToStructure(lParam); uint vk = info.vk; // Update modifier states (shared for both modes) if (vk == 0xA2 || vk == 0xA3 || vk == 0x11) _ctrlHeld = isDown; if (vk == 0xA0 || vk == 0xA1 || vk == 0x10) _shiftHeld = isDown; if (vk == 0xA4 || vk == 0xA5 || vk == 0x12) _altHeld = isDown; // ── Shortcut: Ctrl + Alt + Insert (0x2D) ────────────────────────── // Intercepts and sends Ctrl+Alt+Delete to client. // Works in both Host and Client mode as long as connected. if (isDown && _ctrlHeld && _altHeld && vk == 0x2D) { SendCtrlAltDelToClient(); return (IntPtr)1; // Suppress from Host } // Host mode: track Ctrl state and mark clipboard ready on Ctrl+C if (!_clientMode) { if (isDown && vk == 0x43 && _ctrlHeld) _clipboardReady = true; return CallNextHookEx(_keyHook, nCode, wParam, lParam); } if (isDown) { if ((vk == 0x56 && _ctrlHeld) || (vk == 0x2D && _shiftHeld)) { if (_clipboardReady) { _clipboardReady = false; BeginInvoke((Action)SendClipboardToClient); } else { // No host clipboard ready — forward Ctrl+V to client normally byte? vc = VkToArduino(vk); if (vc.HasValue) _transport?.Send(new byte[] { (byte)'P', vc.Value }); } return (IntPtr)1; } } byte? code = VkToArduino(vk); if (code.HasValue) _transport?.Send(new byte[] { isDown ? (byte)'P' : (byte)'U', code.Value }); return (IntPtr)1; } // ══════════════════════════════════════════════════════════════════════════ // SECTION 9 — VK → Arduino keycode mapping // ══════════════════════════════════════════════════════════════════════════ private static readonly System.Collections.Generic.Dictionary KeyMap = new System.Collections.Generic.Dictionary { { 0xA0, 0x81 }, { 0xA1, 0x85 }, { 0xA2, 0x80 }, { 0xA3, 0x84 }, { 0xA4, 0x82 }, { 0xA5, 0x86 }, { 0x5B, 0x83 }, { 0x5C, 0x87 }, { 0x10, 0x81 }, { 0x11, 0x80 }, { 0x12, 0x82 }, { 0x70, 0xC2 }, { 0x71, 0xC3 }, { 0x72, 0xC4 }, { 0x73, 0xC5 }, { 0x74, 0xC6 }, { 0x75, 0xC7 }, { 0x76, 0xC8 }, { 0x77, 0xC9 }, { 0x78, 0xCA }, { 0x79, 0xCB }, { 0x7A, 0xCC }, { 0x7B, 0xCD }, { 0x26, 0xDA }, { 0x28, 0xD9 }, { 0x25, 0xD8 }, { 0x27, 0xD7 }, { 0x24, 0xD2 }, { 0x23, 0xD5 }, { 0x21, 0xD3 }, { 0x22, 0xD6 }, { 0x2D, 0xD1 }, { 0x2E, 0xD4 }, { 0x0D, 0xB0 }, { 0x1B, 0xB1 }, { 0x08, 0xB2 }, { 0x09, 0xB3 }, { 0x14, 0xC1 }, { 0x2C, 0xCE }, { 0x91, 0xCF }, { 0x13, 0xD0 }, }; private static byte? VkToArduino(uint vk) { if (KeyMap.TryGetValue(vk, out byte mapped)) return mapped; if (vk >= 0x41 && vk <= 0x5A) return (byte)(vk + 0x20); if (vk >= 0x30 && vk <= 0x39) return (byte)vk; if (vk >= 0x60 && vk <= 0x69) return (byte)('0' + vk - 0x60); return vk switch { 0x20 => (byte)' ', 0xBD => (byte)'-', 0xBB => (byte)'=', 0xDB => (byte)'[', 0xDD => (byte)']', 0xDC => (byte)'\\', 0xBA => (byte)';', 0xDE => (byte)'\'', 0xBC => (byte)',', 0xBE => (byte)'.', 0xBF => (byte)'/', 0xC0 => (byte)'`', _ => null }; } // ══════════════════════════════════════════════════════════════════════════ // SECTION 10 — Utilities + form closing // ══════════════════════════════════════════════════════════════════════════ private void SetStatus(string msg, System.Drawing.Color color) { if (InvokeRequired) { Invoke((Action)(() => SetStatus(msg, color))); return; } lblStatus.Text = msg; lblStatus.ForeColor = color; } private void SetPortInfo(string msg) { if (InvokeRequired) { Invoke((Action)(() => SetPortInfo(msg))); return; } lblPortInfo.Text = msg; } private static void Log(string msg) => Debug.WriteLine($"[KVMote {DateTime.Now:HH:mm:ss.fff}] {msg}"); // ══════════════════════════════════════════════════════════════════════════ // SECTION 11 — Clipboard text send + layout translation // ══════════════════════════════════════════════════════════════════════════ private static readonly System.Collections.Generic.Dictionary PtBrMap = new System.Collections.Generic.Dictionary { { ';', '/' }, { ':', '?' }, { '[', ']' }, { '{', '}' }, { ']', '\\' }, { '}', '|' }, { '/', null }, { '?', null }, { '\\', (char)0xEC }, // KEY_NON_US_BACKSLASH → \ em ABNT2 { '|', null }, { '@', null }, }; private byte? TranslateChar(char c) { if (_clientLayout == ClientLayout.PtBrAbnt2 && PtBrMap.TryGetValue(c, out char? mapped)) return mapped.HasValue ? (byte?)mapped.Value : null; return (byte)c; } private void SendClipboardToClient() { if (!_userConnected) return; var t = _transport; if (t is null) return; string text = System.Windows.Forms.Clipboard.GetText(); if (string.IsNullOrEmpty(text)) return; int maxChars = t.ClipboardMaxChars; int delayMs = t.ClipboardDelayMs; if (text.Length > maxChars) { SetStatus($"Clipboard ({text.Length} chars) excede {maxChars}. Não enviado.", System.Drawing.Color.Orange); return; } Task.Run(() => { t.Send(new byte[] { (byte)'A' }); Thread.Sleep(100); int skipped = 0; foreach (char c in text) { byte b; if (c == '\n' || c == '\r') b = (byte)'\n'; else if (c == '\t') b = (byte)'\t'; else if (c >= 32 && c <= 126) { byte? translated = TranslateChar(c); if (!translated.HasValue) { skipped++; continue; } b = translated.Value; } else { skipped++; continue; } t.Send(new byte[] { (byte)'K', b }); Thread.Sleep(delayMs); } string suffix = skipped > 0 ? $" ({skipped} ignorados)" : ""; SetStatus("Modo Cliente \u25cf" + suffix, System.Drawing.Color.DodgerBlue); }); SetStatus($"Enviando {text.Length} chars...", System.Drawing.Color.DodgerBlue); } private void cmbLayout_SelectedIndexChanged(object sender, EventArgs e) { _clientLayout = cmbLayout.SelectedIndex == 1 ? ClientLayout.PtBrAbnt2 : ClientLayout.US; } private void btnSendCad_Click(object sender, EventArgs e) => SendCtrlAltDelToClient(); private void SendCtrlAltDelToClient() { if (!_userConnected) return; var t = _transport; if (t is null) return; Task.Run(() => { // Sequence: Press Ctrl, Press Alt, Press Del, Release Del, Release Alt, Release Ctrl t.Send(new byte[] { (byte)'P', 0x80 }); // L Ctrl t.Send(new byte[] { (byte)'P', 0x82 }); // L Alt t.Send(new byte[] { (byte)'P', 0xD4 }); // Delete Thread.Sleep(50); t.Send(new byte[] { (byte)'U', 0xD4 }); t.Send(new byte[] { (byte)'U', 0x82 }); t.Send(new byte[] { (byte)'U', 0x80 }); SetStatus("Ctrl+Alt+Del enviado", System.Drawing.Color.DodgerBlue); Thread.Sleep(1500); if (_userConnected) SetStatus(_clientMode ? "Modo Cliente \u25cf" : "Modo Host \u25cf Conectado", _clientMode ? System.Drawing.Color.DodgerBlue : System.Drawing.Color.Green); }); } protected override void OnFormClosing(FormClosingEventArgs e) { _detectCts?.Cancel(); ExitClientMode(sendRelease: false); _watchdog.Stop(); _watchdog.Dispose(); _heartbeat?.Dispose(); UninstallHooks(); _transport?.Disconnect(); _transport?.Dispose(); base.OnFormClosing(e); } } }