Add KVMote source — C#, Arduino sketch and .gitignore

- 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 <noreply@anthropic.com>
This commit is contained in:
Ricardo Carneiro 2026-03-22 15:09:07 -03:00
parent 58858487fa
commit c77a9cac0d
8 changed files with 1354 additions and 0 deletions

32
.gitignore vendored Normal file
View File

@ -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

18
KVMote.csproj Normal file
View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>disable</ImplicitUsings>
<AssemblyName>KVMote</AssemblyName>
<RootNamespace>KVMote</RootNamespace>
<Optimize>true</Optimize>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="9.0.0" />
</ItemGroup>
</Project>

183
KVMote.ino Normal file
View File

@ -0,0 +1,183 @@
/*
KVMote Arduino Leonardo (Serial1 = HC-06)
Baud rate: 9600 (padrão de fábrica do HC-06).
Se você 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 <Keyboard.h>
#include <Mouse.h>
// ── 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;
}
}
}

25
KVMote.sln Normal file
View File

@ -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

197
Principal.Designer.cs generated Normal file
View File

@ -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);
}
}
}

764
Principal.cs Normal file
View File

@ -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<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 && 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<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);
}
}
}

120
Principal.resx Normal file
View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

15
Program.cs Normal file
View File

@ -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());
}
}
}