781 lines
36 KiB
C#
781 lines
36 KiB
C#
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();
|
|
private int _wheelAccum; // acumula deltas do touchpad (smooth scroll)
|
|
|
|
// 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);
|
|
Send(new byte[] { (byte)'H' }); // LED azul no Arduino
|
|
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();
|
|
// Sinaliza ao Arduino antes de fechar (LED verde = sem host)
|
|
if (_port?.IsOpen == true)
|
|
try { lock (_sendLock) { _port.Write(new byte[] { (byte)'G' }, 0, 1); } } catch { }
|
|
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;
|
|
|
|
// Garante que o cursor volta imediatamente se estava em modo cliente
|
|
if (InvokeRequired) Invoke((Action)(() => ExitClientMode(sendRelease: false)));
|
|
else ExitClientMode(sendRelease: false);
|
|
|
|
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<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)
|
|
{
|
|
// 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)
|
|
{
|
|
// Acumula deltas: mouse wheel envia ±120/notch,
|
|
// touchpad 2 dedos envia valores pequenos (±3..±15).
|
|
// Enviamos 1 unidade ao Arduino a cada 120 acumulados.
|
|
_wheelAccum += (short)(info.mouseData >> 16);
|
|
int toSend = _wheelAccum / 120;
|
|
if (toSend != 0)
|
|
{
|
|
_wheelAccum -= toSend * 120;
|
|
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();
|
|
|
|
// 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<KBDLLHOOKSTRUCT>(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<uint, byte> KeyMap = new 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 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<char, char?> PtBrMap = new Dictionary<char, char?>
|
|
{
|
|
{ ';', '/' }, // 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);
|
|
}
|
|
}
|
|
}
|