800 lines
36 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|