From c77a9cac0d075d70a14b8aa38b59e5967ffbfc79 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro <71648276+ricarneiro@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:09:07 -0300 Subject: [PATCH] =?UTF-8?q?Add=20KVMote=20source=20=E2=80=94=20C#,=20Ardui?= =?UTF-8?q?no=20sketch=20and=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Principal.cs/Designer.cs: WinForms KVM host app (.NET 8) Global mouse/keyboard hooks, edge detection, BT serial protocol, clipboard paste via Ctrl+V, PT-BR ABNT2 layout translation - KVMote.ino: Arduino Leonardo sketch (HC-06 BT, USB HID) Non-blocking state machine, LED RGB feedback, full protocol support - KVMote.csproj/sln: project files with System.IO.Ports NuGet - .gitignore: C#/Visual Studio standard exclusions Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 32 ++ KVMote.csproj | 18 + KVMote.ino | 183 ++++++++++ KVMote.sln | 25 ++ Principal.Designer.cs | 197 +++++++++++ Principal.cs | 764 ++++++++++++++++++++++++++++++++++++++++++ Principal.resx | 120 +++++++ Program.cs | 15 + 8 files changed, 1354 insertions(+) create mode 100644 .gitignore create mode 100644 KVMote.csproj create mode 100644 KVMote.ino create mode 100644 KVMote.sln create mode 100644 Principal.Designer.cs create mode 100644 Principal.cs create mode 100644 Principal.resx create mode 100644 Program.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4470c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Visual Studio +.vs/ +*.user +*.suo +*.userosscache +*.sln.docstates + +# Build output +bin/ +obj/ + +# NuGet +*.nupkg +*.snupkg +packages/ +project.lock.json +project.fragment.lock.json +artifacts/ + +# Publish output +publish/ + +# Roslyn / analyzers +*.aps + +# OS +Thumbs.db +Desktop.ini +.DS_Store + +# Claude Code settings (local only) +.claude/settings.local.json diff --git a/KVMote.csproj b/KVMote.csproj new file mode 100644 index 0000000..82e1b17 --- /dev/null +++ b/KVMote.csproj @@ -0,0 +1,18 @@ + + + + WinExe + net8.0-windows + enable + true + disable + KVMote + KVMote + true + + + + + + + diff --git a/KVMote.ino b/KVMote.ino new file mode 100644 index 0000000..a622d00 --- /dev/null +++ b/KVMote.ino @@ -0,0 +1,183 @@ +/* + KVMote — Arduino Leonardo (Serial1 = HC-06) + + Baud rate: 9600 (padrão de fábrica do HC-06). + Se você já configurou o HC-06 para outra velocidade via AT commands, + altere a constante abaixo. + + Protocolo binário: + Mouse move → 'M' dx(int8) dy(int8) 3 bytes + Mouse wheel → 'W' delta(int8) 2 bytes + Tecla write → 'K' char 2 bytes + Clique → 'C' 'L'|'R' 2 bytes + Tecla press → 'P' keycode 2 bytes + Tecla release → 'U' keycode 2 bytes + ReleaseAll → 'A' 1 byte + Mouse press → 'D' 'L'|'R' 2 bytes + Mouse release → 'E' 'L'|'R' 2 bytes + LED cliente → 'O' (laranja — entrou no cliente) + LED host → 'H' (flash verde + volta azul — voltou ao host) + Ping/Pong → '~' → responde [PONG] 1 byte +*/ + +#include +#include + +// ── Pinos do LED RGB ────────────────────────────────────────────────────────── +#define PIN_R 5 +#define PIN_G 6 +#define PIN_B 9 + +#define BAUD_HC06 9600 // ← altere se necessário + +// ── Cor base do LED (muda conforme o modo) ──────────────────────────────────── +uint8_t basR = 0, basG = 0, basB = 255; // azul = idle + +// ── Máquina de estados ──────────────────────────────────────────────────────── +enum Estado : uint8_t { + AGUARDA_CMD, + AGUARDA_MOUSE_DX, + AGUARDA_MOUSE_DY, + AGUARDA_MOUSE_WHEEL, + AGUARDA_TECLA, + AGUARDA_CLIQUE, + AGUARDA_PRESS_KEY, + AGUARDA_RELEASE_KEY, + AGUARDA_MOUSE_PRESS, + AGUARDA_MOUSE_RELEASE +}; + +Estado estado = AGUARDA_CMD; +int8_t pendingDX = 0; + +// ── Helpers de LED ──────────────────────────────────────────────────────────── +void ledCor(int r, int g, int b) { + analogWrite(PIN_R, r); + analogWrite(PIN_G, g); + analogWrite(PIN_B, b); +} + +void piscaVerde() { + ledCor(0, 200, 0); + delay(40); + ledCor(basR, basG, basB); // retorna à cor base atual +} + +void piscaCiano() { + ledCor(0, 200, 200); + delay(40); + ledCor(basR, basG, basB); // retorna à cor base atual +} + +// ── Setup ───────────────────────────────────────────────────────────────────── +void setup() { + Serial1.begin(BAUD_HC06); + Keyboard.begin(); + Mouse.begin(); + ledCor(basR, basG, basB); // azul = pronto +} + +// ── Loop principal (non-blocking) ───────────────────────────────────────────── +void loop() { + while (Serial1.available() > 0) { + byte b = (byte)Serial1.read(); + + switch (estado) { + + // ── Aguarda o byte de comando ────────────────────────────────────────── + case AGUARDA_CMD: + if (b == 'M') estado = AGUARDA_MOUSE_DX; + else if (b == 'W') estado = AGUARDA_MOUSE_WHEEL; + else if (b == 'K') estado = AGUARDA_TECLA; + else if (b == 'C') estado = AGUARDA_CLIQUE; + else if (b == 'P') estado = AGUARDA_PRESS_KEY; + else if (b == 'U') estado = AGUARDA_RELEASE_KEY; + else if (b == 'D') estado = AGUARDA_MOUSE_PRESS; + else if (b == 'E') estado = AGUARDA_MOUSE_RELEASE; + else if (b == 'A') { + Keyboard.releaseAll(); + piscaVerde(); + } + else if (b == 'O') { + // Entrou no PC cliente → LED laranja + basR = 255; basG = 80; basB = 0; + ledCor(basR, basG, basB); + } + else if (b == 'H') { + // Voltou ao host → flash verde, LED azul + basR = 0; basG = 0; basB = 255; + piscaVerde(); + } + else if (b == '~') { + Serial1.print("[PONG]"); + piscaCiano(); + } + // qualquer outro byte é silenciosamente descartado + break; + + // ── Mouse: lê DeltaX ────────────────────────────────────────────────── + case AGUARDA_MOUSE_DX: + pendingDX = (int8_t)b; + estado = AGUARDA_MOUSE_DY; + break; + + // ── Mouse: lê DeltaY e executa o movimento ──────────────────────────── + case AGUARDA_MOUSE_DY: + Mouse.move(pendingDX, (int8_t)b, 0); + estado = AGUARDA_CMD; + // Sem piscaVerde() aqui — delay(40) bloquearia o processamento serial + break; + + // ── Mouse: roda do mouse ────────────────────────────────────────────── + case AGUARDA_MOUSE_WHEEL: + Mouse.move(0, 0, (int8_t)b); + estado = AGUARDA_CMD; + break; + + // ── Teclado: lê o caractere e digita (write) ────────────────────────── + case AGUARDA_TECLA: + Keyboard.write(b); + estado = AGUARDA_CMD; + piscaVerde(); + break; + + // ── Clique: lê L ou R e clica ───────────────────────────────────────── + case AGUARDA_CLIQUE: + if (b == 'L') Mouse.click(MOUSE_LEFT); + if (b == 'R') Mouse.click(MOUSE_RIGHT); + estado = AGUARDA_CMD; + piscaVerde(); + break; + + // ── Tecla press (mantém pressionada) ───────────────────────────────── + case AGUARDA_PRESS_KEY: + Keyboard.press(b); + estado = AGUARDA_CMD; + piscaVerde(); + break; + + // ── Tecla release ───────────────────────────────────────────────────── + case AGUARDA_RELEASE_KEY: + Keyboard.release(b); + estado = AGUARDA_CMD; + piscaVerde(); + break; + + // ── Mouse press (botão segurado) ────────────────────────────────────── + case AGUARDA_MOUSE_PRESS: + if (b == 'L') Mouse.press(MOUSE_LEFT); + if (b == 'R') Mouse.press(MOUSE_RIGHT); + estado = AGUARDA_CMD; + piscaVerde(); + break; + + // ── Mouse release ───────────────────────────────────────────────────── + case AGUARDA_MOUSE_RELEASE: + if (b == 'L') Mouse.release(MOUSE_LEFT); + if (b == 'R') Mouse.release(MOUSE_RIGHT); + estado = AGUARDA_CMD; + piscaVerde(); + break; + } + } +} diff --git a/KVMote.sln b/KVMote.sln new file mode 100644 index 0000000..505b780 --- /dev/null +++ b/KVMote.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35122.118 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KVMote", "KVMote.csproj", "{8F708910-2F3D-45A6-A6C3-2C0FE3FFEFA6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8F708910-2F3D-45A6-A6C3-2C0FE3FFEFA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F708910-2F3D-45A6-A6C3-2C0FE3FFEFA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F708910-2F3D-45A6-A6C3-2C0FE3FFEFA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F708910-2F3D-45A6-A6C3-2C0FE3FFEFA6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {94B06ABA-55BF-4DF3-B345-2CD4636F98B2} + EndGlobalSection +EndGlobal diff --git a/Principal.Designer.cs b/Principal.Designer.cs new file mode 100644 index 0000000..1c2944b --- /dev/null +++ b/Principal.Designer.cs @@ -0,0 +1,197 @@ +namespace KVMote +{ + partial class Principal + { + private System.Windows.Forms.Button btnAbove; + private System.Windows.Forms.Button btnLeft; + private System.Windows.Forms.Button btnRight; + private System.Windows.Forms.Button btnBelow; + private System.Windows.Forms.Label lblHost; + private System.Windows.Forms.Label lblPosition; + private System.Windows.Forms.Label lblPortInfo; + private System.Windows.Forms.Button btnDetect; + private System.Windows.Forms.Button btnConnect; + private System.Windows.Forms.Panel pnlBottom; + private System.Windows.Forms.Label lblStatus; + private System.Windows.Forms.Panel pnlContent; + private System.Windows.Forms.Label lblLayout; + private System.Windows.Forms.ComboBox cmbLayout; + + private void InitializeComponent() + { + this.pnlContent = new System.Windows.Forms.Panel(); + this.pnlBottom = new System.Windows.Forms.Panel(); + this.lblLayout = new System.Windows.Forms.Label(); + this.cmbLayout = new System.Windows.Forms.ComboBox(); + this.lblPosition = new System.Windows.Forms.Label(); + this.btnAbove = new System.Windows.Forms.Button(); + this.btnLeft = new System.Windows.Forms.Button(); + this.lblHost = new System.Windows.Forms.Label(); + this.btnRight = new System.Windows.Forms.Button(); + this.btnBelow = new System.Windows.Forms.Button(); + this.lblPortInfo = new System.Windows.Forms.Label(); + this.btnDetect = new System.Windows.Forms.Button(); + this.btnConnect = new System.Windows.Forms.Button(); + this.lblStatus = new System.Windows.Forms.Label(); + + this.pnlContent.SuspendLayout(); + this.pnlBottom.SuspendLayout(); + this.SuspendLayout(); + + var clrNormal = System.Drawing.Color.FromArgb(55, 55, 55); + var clrBorder = System.Drawing.Color.FromArgb(90, 90, 90); + var clrSilver = System.Drawing.Color.Silver; + var clrWhite = System.Drawing.Color.White; + var fntUI = new System.Drawing.Font("Segoe UI", 9f); + var fntBold = new System.Drawing.Font("Segoe UI", 9f, System.Drawing.FontStyle.Bold); + + int gx = 56, gy = 60; + int cw = 84, ch = 32, cg = 8; + + this.lblPosition.Text = "Posição do PC Cliente:"; + this.lblPosition.ForeColor = clrSilver; + this.lblPosition.Font = fntUI; + this.lblPosition.AutoSize = true; + this.lblPosition.Location = new System.Drawing.Point(gx, 22); + + this.btnAbove.Name = "btnAbove"; + this.btnAbove.Text = "Acima"; + this.btnAbove.Location = new System.Drawing.Point(gx + cw + cg, gy); + this.btnAbove.Size = new System.Drawing.Size(cw, ch); + this.btnAbove.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnAbove.BackColor = clrNormal; + this.btnAbove.ForeColor = clrSilver; + this.btnAbove.Font = fntUI; + this.btnAbove.FlatAppearance.BorderColor = clrBorder; + this.btnAbove.Click += new System.EventHandler(this.btnPosition_Click); + + this.btnLeft.Name = "btnLeft"; + this.btnLeft.Text = "Esquerda"; + this.btnLeft.Location = new System.Drawing.Point(gx, gy + ch + cg); + this.btnLeft.Size = new System.Drawing.Size(cw, ch); + this.btnLeft.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnLeft.BackColor = clrNormal; + this.btnLeft.ForeColor = clrSilver; + this.btnLeft.Font = fntUI; + this.btnLeft.FlatAppearance.BorderColor = clrBorder; + this.btnLeft.Click += new System.EventHandler(this.btnPosition_Click); + + this.lblHost.Text = "[HOST PC]"; + this.lblHost.Location = new System.Drawing.Point(gx + cw + cg, gy + ch + cg); + this.lblHost.Size = new System.Drawing.Size(cw, ch); + this.lblHost.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + this.lblHost.BackColor = System.Drawing.Color.FromArgb(30, 60, 100); + this.lblHost.ForeColor = clrWhite; + this.lblHost.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.lblHost.Font = new System.Drawing.Font("Segoe UI", 8f, System.Drawing.FontStyle.Bold); + + this.btnRight.Name = "btnRight"; + this.btnRight.Text = "Direita"; + this.btnRight.Location = new System.Drawing.Point(gx + (cw + cg) * 2, gy + ch + cg); + this.btnRight.Size = new System.Drawing.Size(cw, ch); + this.btnRight.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnRight.BackColor = clrNormal; + this.btnRight.ForeColor = clrSilver; + this.btnRight.Font = fntUI; + this.btnRight.FlatAppearance.BorderColor = clrBorder; + this.btnRight.Click += new System.EventHandler(this.btnPosition_Click); + + this.btnBelow.Name = "btnBelow"; + this.btnBelow.Text = "Abaixo"; + this.btnBelow.Location = new System.Drawing.Point(gx + cw + cg, gy + (ch + cg) * 2); + this.btnBelow.Size = new System.Drawing.Size(cw, ch); + this.btnBelow.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnBelow.BackColor = clrNormal; + this.btnBelow.ForeColor = clrSilver; + this.btnBelow.Font = fntUI; + this.btnBelow.FlatAppearance.BorderColor = clrBorder; + this.btnBelow.Click += new System.EventHandler(this.btnPosition_Click); + + int sepY = gy + (ch + cg) * 3 + 4; + + this.lblPortInfo.Text = "Porta: detectando..."; + this.lblPortInfo.Location = new System.Drawing.Point(gx, sepY + 10); + this.lblPortInfo.Size = new System.Drawing.Size(268, 20); + this.lblPortInfo.ForeColor = clrSilver; + this.lblPortInfo.Font = fntUI; + + this.btnDetect.Text = "Detectar"; + this.btnDetect.Location = new System.Drawing.Point(gx, sepY + 36); + this.btnDetect.Size = new System.Drawing.Size(100, 30); + this.btnDetect.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnDetect.BackColor = System.Drawing.Color.FromArgb(65, 65, 65); + this.btnDetect.ForeColor = clrWhite; + this.btnDetect.Font = fntUI; + this.btnDetect.FlatAppearance.BorderColor = clrBorder; + this.btnDetect.Click += new System.EventHandler(this.btnDetect_Click); + + this.btnConnect.Text = "Conectar"; + this.btnConnect.Location = new System.Drawing.Point(gx + 108, sepY + 36); + this.btnConnect.Size = new System.Drawing.Size(160, 30); + this.btnConnect.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnConnect.BackColor = System.Drawing.Color.FromArgb(0, 122, 204); + this.btnConnect.ForeColor = clrWhite; + this.btnConnect.Font = fntBold; + this.btnConnect.FlatAppearance.BorderColor = System.Drawing.Color.FromArgb(0, 100, 180); + this.btnConnect.Click += new System.EventHandler(this.btnConnect_Click); + + this.lblLayout.Text = "Layout do cliente:"; + this.lblLayout.ForeColor = clrSilver; + this.lblLayout.Font = fntUI; + this.lblLayout.AutoSize = true; + this.lblLayout.Location = new System.Drawing.Point(gx, sepY + 78); + + this.cmbLayout.Items.AddRange(new object[] { "US / Internacional", "PT-BR ABNT2" }); + this.cmbLayout.SelectedIndex = 0; + this.cmbLayout.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbLayout.Location = new System.Drawing.Point(gx + 120, sepY + 74); + this.cmbLayout.Size = new System.Drawing.Size(148, 22); + this.cmbLayout.Font = fntUI; + this.cmbLayout.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.cmbLayout.BackColor = System.Drawing.Color.FromArgb(40, 40, 40); + this.cmbLayout.ForeColor = clrWhite; + this.cmbLayout.SelectedIndexChanged += new System.EventHandler(this.cmbLayout_SelectedIndexChanged); + + this.pnlContent.BackColor = System.Drawing.Color.FromArgb(20, 20, 20); + this.pnlContent.Dock = System.Windows.Forms.DockStyle.Fill; + this.pnlContent.Controls.Add(this.lblPosition); + this.pnlContent.Controls.Add(this.btnAbove); + this.pnlContent.Controls.Add(this.btnLeft); + this.pnlContent.Controls.Add(this.lblHost); + this.pnlContent.Controls.Add(this.btnRight); + this.pnlContent.Controls.Add(this.btnBelow); + this.pnlContent.Controls.Add(this.lblPortInfo); + this.pnlContent.Controls.Add(this.btnDetect); + this.pnlContent.Controls.Add(this.btnConnect); + this.pnlContent.Controls.Add(this.lblLayout); + this.pnlContent.Controls.Add(this.cmbLayout); + this.pnlBottom.BackColor = System.Drawing.Color.FromArgb(40, 40, 40); + this.pnlBottom.Dock = System.Windows.Forms.DockStyle.Bottom; + this.pnlBottom.Height = 28; + this.pnlBottom.Controls.Add(this.lblStatus); + + this.lblStatus.Text = "Desconectado"; + this.lblStatus.ForeColor = System.Drawing.Color.Gray; + this.lblStatus.Font = fntUI; + this.lblStatus.AutoSize = true; + this.lblStatus.Location = new System.Drawing.Point(8, 6); + + int contentH = sepY + 74 + 22 + 16; + + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; + this.BackColor = System.Drawing.Color.FromArgb(20, 20, 20); + this.ClientSize = new System.Drawing.Size(380, contentH + 28); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.Text = "KVMote \u2014 KVM over Bluetooth"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Controls.Add(this.pnlContent); + this.Controls.Add(this.pnlBottom); + + this.pnlContent.ResumeLayout(false); + this.pnlBottom.ResumeLayout(false); + this.pnlBottom.PerformLayout(); + this.ResumeLayout(false); + } + } +} diff --git a/Principal.cs b/Principal.cs new file mode 100644 index 0000000..d79663f --- /dev/null +++ b/Principal.cs @@ -0,0 +1,764 @@ +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); + } + } +} diff --git a/Principal.resx b/Principal.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Principal.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..a79c7b3 --- /dev/null +++ b/Program.cs @@ -0,0 +1,15 @@ +using System; +using System.Windows.Forms; + +namespace KVMote +{ + internal static class Program + { + [STAThread] + static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new Principal()); + } + } +}