using System; using System.Collections.Generic; using System.Diagnostics; using System.IO.Ports; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; 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); [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 BaudRate = 9600; 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; private const int MaxTimeouts = 3; // ── Enum ─────────────────────────────────────────────────────────────────── private enum ClientPos { None, Left, Right, Above, Below } private enum ClientLayout { US, PtBrAbnt2 } // ── Fields ───────────────────────────────────────────────────────────────── // Serial private SerialPort? _port; private bool _userConnected, _isReconnecting; private readonly object _sendLock = new(); private int _timeoutCount; private DateTime _lastPong = DateTime.MinValue; // Hooks private LowLevelProc? _mouseProc, _keyProc; private IntPtr _mouseHook, _keyHook; // KVM state private bool _clientMode; private bool _ctrlHeld, _shiftHeld; 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(); // 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 COM // ══════════════════════════════════════════════════════════════════════════ 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; while (!token.IsCancellationRequested) { string? found = await Task.Run(() => ProbePorts(), token); if (found != null) { SetPortInfo($"\u25cf {found} detectado"); if (btnConnect.InvokeRequired) btnConnect.Invoke((Action)(() => btnConnect.Enabled = true)); else btnConnect.Enabled = true; return; } SetPortInfo("Nenhuma porta encontrada..."); try { await Task.Delay(3000, token); } catch { return; } } } private string? ProbePorts() { foreach (string port in SerialPort.GetPortNames()) { try { using var sp = new SerialPort(port, BaudRate) { WriteTimeout = 200, ReadTimeout = 600, Encoding = Encoding.ASCII }; sp.Open(); Thread.Sleep(150); sp.Write("~"); Thread.Sleep(500); string resp = sp.ReadExisting(); sp.Close(); if (resp.Contains("[PONG]")) return port; } catch { } } return null; } // ══════════════════════════════════════════════════════════════════════════ // 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 void btnConnect_Click(object sender, EventArgs e) { if (!_userConnected) Connect(); else Disconnect(true); } private void Connect() { if (_clientPos == ClientPos.None) { MessageBox.Show("Selecione a posição do PC Cliente.", "KVMote", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } string raw = lblPortInfo.Text.Replace("Porta: ", "").Replace("\u25cf ", "").Replace(" detectado", "").Trim(); string portName = raw.StartsWith("COM") ? raw : ""; if (string.IsNullOrEmpty(portName)) { MessageBox.Show("Nenhuma porta detectada. Clique em Detectar.", "KVMote", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } SetStatus("Conectando...", System.Drawing.Color.Orange); btnConnect.Enabled = false; if (OpenPort(portName)) { _userConnected = true; _timeoutCount = 0; _lastPong = DateTime.MinValue; _watchdog.Start(); _heartbeat = new System.Threading.Timer(OnHeartbeat, null, HeartbeatMs, HeartbeatMs); InstallHooks(); SetConnectedUI(true); SetStatus("Modo Host \u25cf Conectado", System.Drawing.Color.Green); } else { SetStatus("Falha na conexão", System.Drawing.Color.Red); btnConnect.Enabled = true; } } private void Disconnect(bool userInitiated) { ExitClientMode(sendRelease: false); _detectCts?.Cancel(); _userConnected = false; _isReconnecting = false; _watchdog.Stop(); _heartbeat?.Dispose(); _heartbeat = null; UninstallHooks(); ClosePort(); SetConnectedUI(false); if (userInitiated) SetStatus("Desconectado", System.Drawing.Color.Gray); } 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 — Port management // ══════════════════════════════════════════════════════════════════════════ private bool OpenPort(string name) { try { _port = new SerialPort(name, BaudRate) { WriteTimeout = 200, Encoding = Encoding.ASCII }; _port.DataReceived += OnDataReceived; _port.Open(); return true; } catch (Exception ex) { Log($"OpenPort: {ex.Message}"); _port?.Dispose(); _port = null; return false; } } private void ClosePort() { if (_port is null) return; try { _port.DataReceived -= OnDataReceived; if (_port.IsOpen) _port.Close(); } catch { } finally { _port.Dispose(); _port = null; } } private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { try { string data = _port?.ReadExisting() ?? ""; if (data.Contains("[PONG]")) _lastPong = DateTime.UtcNow; } catch { } } // ══════════════════════════════════════════════════════════════════════════ // SECTION 6 — Watchdog + Heartbeat + Reconnect // ══════════════════════════════════════════════════════════════════════════ private void OnWatchdog(object? sender, EventArgs e) { if (!_userConnected || _isReconnecting) return; if (_port is null || !_port.IsOpen) 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; } lock (_sendLock) { try { _port?.Write("~"); } catch { BeginReconnect(); } } } private void BeginReconnect() { if (_isReconnecting) return; _isReconnecting = true; string portName = _port?.PortName ?? ""; SetStatus("Reconectando...", System.Drawing.Color.Orange); if (InvokeRequired) Invoke((Action)(() => SetConnectedUI(false))); else SetConnectedUI(false); ClosePort(); Task.Run(async () => { while (_isReconnecting && _userConnected) { await Task.Delay(2500); if (OpenPort(portName)) { _isReconnecting = false; _lastPong = DateTime.MinValue; SetStatus("Reconectado", System.Drawing.Color.Green); if (InvokeRequired) Invoke((Action)(() => SetConnectedUI(true))); else SetConnectedUI(true); return; } } }); } // ══════════════════════════════════════════════════════════════════════════ // SECTION 7 — 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 8 — 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 && IsAtExitEdge(cursorPt)) EnterClientMode(cursorPt); return CallNextHookEx(_mouseHook, nCode, wParam, lParam); } if (msg == WM_MOUSEMOVE) { // Ignora o WM_MOUSEMOVE gerado pelo próprio warp para o centro if (_isWarping) { _isWarping = false; 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; } // Warp de volta ao centro para obter deltas reais sem ClipCursor _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; SendMouse(new byte[] { (byte)'M', (byte)(sbyte)sdx, (byte)(sbyte)sdy }); } return (IntPtr)1; } if (msg == WM_LBUTTONDOWN) { Send(new byte[] { (byte)'D', (byte)'L' }); return (IntPtr)1; } if (msg == WM_LBUTTONUP) { Send(new byte[] { (byte)'E', (byte)'L' }); return (IntPtr)1; } if (msg == WM_RBUTTONDOWN) { Send(new byte[] { (byte)'D', (byte)'R' }); return (IntPtr)1; } if (msg == WM_RBUTTONUP) { Send(new byte[] { (byte)'E', (byte)'R' }); return (IntPtr)1; } if (msg == WM_MOUSEWHEEL) { short delta = (short)(info.mouseData >> 16); int notches = Math.Clamp(delta / 120, -127, 127); if (notches != 0) Send(new byte[] { (byte)'W', (byte)(sbyte)notches }); 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(); // Cursor fica invisível no centro da tela — deltas calculados por warp (técnica FPS) 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(); Send(new byte[] { (byte)'O' }); // LED laranja no Arduino 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) Send(new byte[] { (byte)'A' }); Send(new byte[] { (byte)'H' }); // flash verde + LED azul no Arduino SetStatus("Modo Host \u25cf Conectado", System.Drawing.Color.Green); } // ══════════════════════════════════════════════════════════════════════════ // SECTION 9 — Keyboard Hook Callback // ══════════════════════════════════════════════════════════════════════════ private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode < 0 || !_userConnected || !_clientMode) 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; // Rastreia Ctrl e Shift (mais confiável que GetKeyState dentro do hook) if (vk == 0xA2 || vk == 0xA3 || vk == 0x11) _ctrlHeld = isDown; if (vk == 0xA0 || vk == 0xA1 || vk == 0x10) _shiftHeld = isDown; // Ctrl+V ou Shift+Ins → envia clipboard do host como digitação if (isDown) { if ((vk == 0x56 && _ctrlHeld) || (vk == 0x2D && _shiftHeld)) { BeginInvoke((Action)SendClipboardToClient); return (IntPtr)1; } } byte? code = VkToArduino(vk); if (code.HasValue) Send(new byte[] { isDown ? (byte)'P' : (byte)'U', code.Value }); return (IntPtr)1; } // ══════════════════════════════════════════════════════════════════════════ // SECTION 10 — VK → Arduino keycode mapping // ══════════════════════════════════════════════════════════════════════════ private static readonly Dictionary KeyMap = new 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 11 — Send methods // ══════════════════════════════════════════════════════════════════════════ private void Send(byte[] data) { if (_port is null || !_port.IsOpen || !_userConnected) return; lock (_sendLock) { try { _port.Write(data, 0, data.Length); _timeoutCount = 0; } catch (TimeoutException) { _timeoutCount++; Log($"Timeout {_timeoutCount}/{MaxTimeouts}"); if (_timeoutCount >= MaxTimeouts && !_isReconnecting) BeginReconnect(); } catch (Exception ex) { Log($"Erro: {ex.Message}"); if (!_isReconnecting) BeginReconnect(); } } } private void SendMouse(byte[] data) { if (_port is null || !_port.IsOpen || !_userConnected) return; if (!Monitor.TryEnter(_sendLock)) return; try { _port.Write(data, 0, data.Length); _timeoutCount = 0; } catch { } finally { Monitor.Exit(_sendLock); } } // ══════════════════════════════════════════════════════════════════════════ // SECTION 12 — 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 = "Porta: " + msg; } private static void Log(string msg) => Debug.WriteLine($"[KVMote {DateTime.Now:HH:mm:ss.fff}] {msg}"); // ══════════════════════════════════════════════════════════════════════════ // SECTION 13 — Clipboard text send + layout translation // Ctrl+V (ou Shift+Ins) em modo cliente: lê clipboard do HOST e digita // no cliente caractere a caractere via comando 'K' do Arduino. // // Keyboard.write(byte) no Arduino usa layout US internamente. // O PC cliente interpreta o HID keycode com o layout configurado. // Para PT-BR ABNT2, remapeamos os chars cujo HID keycode difere. // ══════════════════════════════════════════════════════════════════════════ private const int MaxClipChars = 300; // Tabela PT-BR ABNT2: // Chave = char desejado no cliente // Valor = char que Keyboard.write() deve receber para gerar o HID correto // null = não é possível gerar via Keyboard.write() (char ignorado) // // Lógica: Keyboard.write('x') → HID do 'x' em US → cliente PT-BR produz 'y' // Ex: Keyboard.write('/') → HID 0x38 → PT-BR lê como ';' // Keyboard.write('?') → HID 0x38+SHIFT → PT-BR lê como ':' private static readonly Dictionary PtBrMap = new Dictionary { { ';', '/' }, // HID 0x38 → PT-BR ';' { ':', '?' }, // HID 0x38+SHIFT → PT-BR ':' { '[', ']' }, // HID 0x30 → PT-BR '[' { '{', '}' }, // HID 0x30+SHIFT → PT-BR '{' { ']', '\\' }, // HID 0x31 → PT-BR ']' { '}', '|' }, // HID 0x31+SHIFT → PT-BR '}' // Chars que PT-BR tem em posições sem equivalente no US padrão (ignorados): { '/', null }, // PT-BR '/' está em HID extra (0x87) — inacessível { '?', null }, // idem, SHIFT { '\\', null }, // PT-BR '\' está em HID 0x64 — não existe no US { '|', null }, // idem, SHIFT { '@', null }, // PT-BR '@' requer ALTGR+2 }; private byte? TranslateChar(char c) { if (_clientLayout == ClientLayout.PtBrAbnt2 && PtBrMap.TryGetValue(c, out char? mapped)) return mapped.HasValue ? (byte?)mapped.Value : null; // null = ignorar return (byte)c; } // Chamado via BeginInvoke (thread UI) para acessar Clipboard com segurança private void SendClipboardToClient() { if (!_userConnected) return; string text = Clipboard.GetText(); if (string.IsNullOrEmpty(text)) return; if (text.Length > MaxClipChars) { SetStatus($"Clipboard ({text.Length} chars) excede {MaxClipChars}. Não enviado.", System.Drawing.Color.Orange); return; } Task.Run(() => { // Solta todas as teclas modificadoras antes de digitar // (Ctrl pode estar pressionado no Arduino ao interceptar Ctrl+V) 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; } // não-ASCII (é, ã, ç, etc.) Send(new byte[] { (byte)'K', b }); Thread.Sleep(20); // ~50 chars/s — seguro para BT 9600 } 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; } protected override void OnFormClosing(FormClosingEventArgs e) { _detectCts?.Cancel(); ExitClientMode(sendRelease: false); _watchdog.Stop(); _watchdog.Dispose(); _heartbeat?.Dispose(); UninstallHooks(); ClosePort(); base.OnFormClosing(e); } } }