KVMote/Principal.cs

800 lines
36 KiB
C#

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<IKvmTransport?> DetectRaceAsync(CancellationToken outerCt)
{
using var raceCts = CancellationTokenSource.CreateLinkedTokenSource(outerCt);
var ct = raceCts.Token;
IKvmTransport[] candidates = [new SerialTransport(), new BleTransport()];
Task<bool>[] tasks = candidates.Select(t => t.DetectAsync(ct)).ToArray();
var pending = new List<Task<bool>>(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<MSLLHOOKSTRUCT>(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<KBDLLHOOKSTRUCT>(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<uint, byte> KeyMap =
new System.Collections.Generic.Dictionary<uint, byte>
{
{ 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<char, char?> PtBrMap =
new System.Collections.Generic.Dictionary<char, char?>
{
{ ';', '/' },
{ ':', '?' },
{ '[', ']' },
{ '{', '}' },
{ ']', '\\' },
{ '}', '|' },
{ '/', 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);
}
}
}