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:
parent
58858487fa
commit
c77a9cac0d
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal 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
18
KVMote.csproj
Normal 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
183
KVMote.ino
Normal file
@ -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 <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
25
KVMote.sln
Normal 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
197
Principal.Designer.cs
generated
Normal 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
764
Principal.cs
Normal 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
120
Principal.resx
Normal 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
15
Program.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user