fix: ajustes
This commit is contained in:
parent
4232ec3e10
commit
91e43e73d5
171
CLAUDE.md
Normal file
171
CLAUDE.md
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
# CLAUDE.md — KVMote
|
||||||
|
|
||||||
|
Arquivo de contexto para o Claude Code. Leia antes de qualquer alteração no projeto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## O que é o KVMote
|
||||||
|
|
||||||
|
KVM (Keyboard/Video/Mouse) over Bluetooth. Controla um PC remoto (cliente) a partir de um PC host usando um Arduino Leonardo como dispositivo HID USB. O host captura teclado e mouse via hooks globais do Windows e retransmite os eventos ao Arduino por Bluetooth Serial (HC-06), que os injeta como HID no PC cliente.
|
||||||
|
|
||||||
|
**Sem software no PC cliente.** O Arduino aparece como teclado + mouse USB padrão.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
- **Arduino Leonardo** (ATmega32U4) — único modelo com HID USB nativo
|
||||||
|
- **HC-06** módulo Bluetooth (Slave) conectado ao Serial1 do Leonardo
|
||||||
|
- **LED RGB** nos pinos: R=5, G=6, B=9 (PWM, analogWrite)
|
||||||
|
- **Baud rate:** 9600 (padrão HC-06 de fábrica)
|
||||||
|
|
||||||
|
```
|
||||||
|
Host PC ──BT SPP──► HC-06 ──Serial1──► Arduino Leonardo ──USB HID──► Cliente PC
|
||||||
|
(KVMote.exe) (COM virtual) (sem software)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estrutura de arquivos
|
||||||
|
|
||||||
|
| Arquivo | Função |
|
||||||
|
|---|---|
|
||||||
|
| `KVMote.ino` | Firmware Arduino: máquina de estados, HID, LED |
|
||||||
|
| `Principal.cs` | Lógica principal C#: hooks, KVM, serial, clipboard |
|
||||||
|
| `Principal.Designer.cs` | UI WinForms: layout, controles |
|
||||||
|
| `Program.cs` | Entry point: DPI + ApplicationConfiguration |
|
||||||
|
| `Form1.cs` / `Form1.Designer.cs` | Stubs vazios (manter, não remover) |
|
||||||
|
| `KVMote.csproj` | .NET 8, ImplicitUsings=disable, System.IO.Ports |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocolo serial binário (Host → Arduino)
|
||||||
|
|
||||||
|
| Comando | Bytes | Descrição |
|
||||||
|
|---|---|---|
|
||||||
|
| `M` dx dy | 3 | Mouse move (int8 com sinal) |
|
||||||
|
| `W` delta | 2 | Mouse wheel / touchpad (int8) |
|
||||||
|
| `K` char | 2 | Keyboard.write(char) — digita caractere |
|
||||||
|
| `C` L\|R | 2 | Mouse click esquerdo ou direito |
|
||||||
|
| `P` keycode | 2 | Keyboard.press(keycode) — tecla segurada |
|
||||||
|
| `U` keycode | 2 | Keyboard.release(keycode) |
|
||||||
|
| `A` | 1 | Keyboard.releaseAll() |
|
||||||
|
| `D` L\|R | 2 | Mouse.press() — botão segurado |
|
||||||
|
| `E` L\|R | 2 | Mouse.release() |
|
||||||
|
| `O` | 1 | LED magenta (entrou no cliente) |
|
||||||
|
| `H` | 1 | LED azul (host conectado) |
|
||||||
|
| `G` | 1 | LED verde (host desconectado) |
|
||||||
|
| `~` | 1 | Ping → Arduino responde `[PONG]` |
|
||||||
|
|
||||||
|
**Arduino → Host:** apenas `[PONG]` como string ASCII.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LED RGB — cores e significados
|
||||||
|
|
||||||
|
| Cor | Estado |
|
||||||
|
|---|---|
|
||||||
|
| 🟢 Verde (0,255,0) | Boot / host desconectado |
|
||||||
|
| 🔵 Azul (0,0,255) | Host conectado, mouse no host |
|
||||||
|
| 🟣 Magenta (255,0,255) | Mouse no PC cliente |
|
||||||
|
|
||||||
|
Sem flashes de tráfego — cor sólida estática.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lógica KVM (Principal.cs)
|
||||||
|
|
||||||
|
### Auto-detect de porta
|
||||||
|
`AutoDetectAsync()` → `ProbePorts()`: abre cada COM, envia `~`, aguarda 500ms por `[PONG]`.
|
||||||
|
|
||||||
|
### Hooks globais
|
||||||
|
- `WH_MOUSE_LL` (14) e `WH_KEYBOARD_LL` (13) via `SetWindowsHookEx`
|
||||||
|
- Instalados após conectar, removidos ao desconectar
|
||||||
|
|
||||||
|
### Entrada no modo cliente
|
||||||
|
- Mouse atinge a borda configurada (Left/Right/Above/Below)
|
||||||
|
- `EnterClientMode()`: esconde cursor, warp para centro da tela
|
||||||
|
- Técnica FPS: `SetCursorPos` para posição fixa, acumula deltas reais
|
||||||
|
|
||||||
|
### Coordenadas virtuais
|
||||||
|
- `_virtualX/_virtualY`: acumulam deltas enviados ao cliente
|
||||||
|
- Retorno ao host quando virtual cruza `-ReturnThreshold` (15px) na direção de entrada
|
||||||
|
|
||||||
|
### Saída do modo cliente
|
||||||
|
- `ExitClientMode()`: mostra cursor, reposiciona 40px dentro da borda, envia `A` + `H`
|
||||||
|
- Também chamado em `BeginReconnect()` para não deixar cursor sumido
|
||||||
|
|
||||||
|
### Throttle de mouse
|
||||||
|
- `SendMouse()` usa `Monitor.TryEnter` (lossy) — descarta se canal ocupado
|
||||||
|
- Throttle de 50ms (~20 pacotes/s) — seguro para BT 9600 baud
|
||||||
|
|
||||||
|
### Wheel / touchpad
|
||||||
|
- Acumula `_wheelAccum += rawDelta`
|
||||||
|
- Envia ao Arduino quando acumula ±120 (1 notch de mouse wheel)
|
||||||
|
- Captura smooth scroll de touchpad (deltas pequenos ±3..±15)
|
||||||
|
|
||||||
|
### Clipboard (Ctrl+V em modo cliente)
|
||||||
|
- Hook intercepta Ctrl+V e Shift+Ins em modo cliente
|
||||||
|
- Lê `Clipboard.GetText()` do host
|
||||||
|
- Limite: `MaxClipChars = 300`
|
||||||
|
- Envia `A` (releaseAll) + 100ms antes de digitar
|
||||||
|
- Digita via `K` + byte a cada 20ms (~50 chars/s)
|
||||||
|
- Layout PT-BR ABNT2: `PtBrMap` remapeia chars via substituição de byte antes de `Keyboard.write()`
|
||||||
|
|
||||||
|
### Reconexão
|
||||||
|
- `BeginReconnect()`: chamado em timeout serial ou exceção de porta
|
||||||
|
- Retenta a cada 2500ms
|
||||||
|
- Chama `ExitClientMode()` imediatamente (cursor volta sempre)
|
||||||
|
- `!_isReconnecting` bloqueia entrada no modo cliente durante reconexão
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Convenções C# obrigatórias
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
|
```
|
||||||
|
**Todo `using` deve ser explícito.** Faltou um → CS0103 em runtime.
|
||||||
|
|
||||||
|
**Designer file:** todos os membros de Form precisam do prefixo `this.`:
|
||||||
|
```csharp
|
||||||
|
this.btnConnect.Text = "Conectar"; // ✓
|
||||||
|
btnConnect.Text = "Conectar"; // ✗ CS0103 em ImplicitUsings=disable
|
||||||
|
```
|
||||||
|
|
||||||
|
**AutoScaleMode:** usar `Dpi` com `AutoScaleDimensions = new SizeF(96F, 96F)`. Nunca `None` (quebra DPI alto) nem `Font` (escala diferente por máquina).
|
||||||
|
|
||||||
|
**Form1.cs / Form1.Designer.cs:** manter como stubs vazios (`namespace KVMote { }`). O VS inclui no .csproj automaticamente; remover do disco pode causar erro de build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como gerar o .exe portable
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Self-contained (~70MB) — sem dependência de .NET no destino
|
||||||
|
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Saída: `bin\Release\net8.0-windows\win-x64\publish\KVMote.exe`
|
||||||
|
|
||||||
|
**Não usar o "Publicar" do Visual Studio** — gera ClickOnce (setup.exe + arquivos separados).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitações conhecidas
|
||||||
|
|
||||||
|
- **Canal único 9600 baud:** mouse trava durante paste de clipboard (design proposital)
|
||||||
|
- **Chars não-ASCII** (`é`, `ã`, `ç`...): não enviáveis via clipboard (filtrados)
|
||||||
|
- **PT-BR:** `/`, `?`, `\`, `|`, `@` não mapeáveis via `Keyboard.write()` — ignorados no paste
|
||||||
|
- **Monitor único:** `Screen.PrimaryScreen` — multi-monitor não implementado
|
||||||
|
- **Um cliente por vez:** arquitetura 1:1 (um HC-06, um Arduino)
|
||||||
|
- **BT desconectado:** se Arduino perder USB, BT cai, `BeginReconnect()` age em ~3s
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roadmap (não implementado)
|
||||||
|
|
||||||
|
- **Clipboard Bridge:** app separado para compartilhar clipboard via OneDrive (pasta compartilhada) entre host e cliente corporativo
|
||||||
|
- **Multi-monitor:** detectar em qual monitor o cursor está ao cruzar a borda
|
||||||
|
- **Layout US-International completo:** mapeamento de dead keys para paste
|
||||||
|
- **Baud rate configurável:** AT commands no HC-06 para 115200
|
||||||
47
KVMote.ino
47
KVMote.ino
@ -15,8 +15,9 @@
|
|||||||
ReleaseAll → 'A' 1 byte
|
ReleaseAll → 'A' 1 byte
|
||||||
Mouse press → 'D' 'L'|'R' 2 bytes
|
Mouse press → 'D' 'L'|'R' 2 bytes
|
||||||
Mouse release → 'E' 'L'|'R' 2 bytes
|
Mouse release → 'E' 'L'|'R' 2 bytes
|
||||||
LED cliente → 'O' (laranja — entrou no cliente)
|
LED cliente → 'O' (laranja — mouse no cliente)
|
||||||
LED host → 'H' (flash verde + volta azul — voltou ao host)
|
LED host ok → 'H' (azul — host conectado)
|
||||||
|
LED sem host → 'G' (verde — host desconectado)
|
||||||
Ping/Pong → '~' → responde [PONG] 1 byte
|
Ping/Pong → '~' → responde [PONG] 1 byte
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -31,7 +32,7 @@
|
|||||||
#define BAUD_HC06 9600 // ← altere se necessário
|
#define BAUD_HC06 9600 // ← altere se necessário
|
||||||
|
|
||||||
// ── Cor base do LED (muda conforme o modo) ────────────────────────────────────
|
// ── Cor base do LED (muda conforme o modo) ────────────────────────────────────
|
||||||
uint8_t basR = 0, basG = 0, basB = 255; // azul = idle
|
uint8_t basR = 0, basG = 255, basB = 0; // verde = aguardando conexão
|
||||||
|
|
||||||
// ── Máquina de estados ────────────────────────────────────────────────────────
|
// ── Máquina de estados ────────────────────────────────────────────────────────
|
||||||
enum Estado : uint8_t {
|
enum Estado : uint8_t {
|
||||||
@ -50,31 +51,19 @@ enum Estado : uint8_t {
|
|||||||
Estado estado = AGUARDA_CMD;
|
Estado estado = AGUARDA_CMD;
|
||||||
int8_t pendingDX = 0;
|
int8_t pendingDX = 0;
|
||||||
|
|
||||||
// ── Helpers de LED ────────────────────────────────────────────────────────────
|
// ── Helper de LED ─────────────────────────────────────────────────────────────
|
||||||
void ledCor(int r, int g, int b) {
|
void ledCor(int r, int g, int b) {
|
||||||
analogWrite(PIN_R, r);
|
analogWrite(PIN_R, r);
|
||||||
analogWrite(PIN_G, g);
|
analogWrite(PIN_G, g);
|
||||||
analogWrite(PIN_B, b);
|
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 ─────────────────────────────────────────────────────────────────────
|
// ── Setup ─────────────────────────────────────────────────────────────────────
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial1.begin(BAUD_HC06);
|
Serial1.begin(BAUD_HC06);
|
||||||
Keyboard.begin();
|
Keyboard.begin();
|
||||||
Mouse.begin();
|
Mouse.begin();
|
||||||
ledCor(basR, basG, basB); // azul = pronto
|
ledCor(basR, basG, basB); // verde = aguardando conexão do host
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Loop principal (non-blocking) ─────────────────────────────────────────────
|
// ── Loop principal (non-blocking) ─────────────────────────────────────────────
|
||||||
@ -96,21 +85,24 @@ void loop() {
|
|||||||
else if (b == 'E') estado = AGUARDA_MOUSE_RELEASE;
|
else if (b == 'E') estado = AGUARDA_MOUSE_RELEASE;
|
||||||
else if (b == 'A') {
|
else if (b == 'A') {
|
||||||
Keyboard.releaseAll();
|
Keyboard.releaseAll();
|
||||||
piscaVerde();
|
|
||||||
}
|
}
|
||||||
else if (b == 'O') {
|
else if (b == 'O') {
|
||||||
// Entrou no PC cliente → LED laranja
|
// Entrou no PC cliente → LED magenta
|
||||||
basR = 255; basG = 80; basB = 0;
|
basR = 255; basG = 0; basB = 255;
|
||||||
ledCor(basR, basG, basB);
|
ledCor(basR, basG, basB);
|
||||||
}
|
}
|
||||||
else if (b == 'H') {
|
else if (b == 'H') {
|
||||||
// Voltou ao host → flash verde, LED azul
|
// Host conectado → LED azul
|
||||||
basR = 0; basG = 0; basB = 255;
|
basR = 0; basG = 0; basB = 255;
|
||||||
piscaVerde();
|
ledCor(basR, basG, basB);
|
||||||
|
}
|
||||||
|
else if (b == 'G') {
|
||||||
|
// Host desconectado → LED verde
|
||||||
|
basR = 0; basG = 255; basB = 0;
|
||||||
|
ledCor(basR, basG, basB);
|
||||||
}
|
}
|
||||||
else if (b == '~') {
|
else if (b == '~') {
|
||||||
Serial1.print("[PONG]");
|
Serial1.print("[PONG]");
|
||||||
piscaCiano();
|
|
||||||
}
|
}
|
||||||
// qualquer outro byte é silenciosamente descartado
|
// qualquer outro byte é silenciosamente descartado
|
||||||
break;
|
break;
|
||||||
@ -125,10 +117,9 @@ void loop() {
|
|||||||
case AGUARDA_MOUSE_DY:
|
case AGUARDA_MOUSE_DY:
|
||||||
Mouse.move(pendingDX, (int8_t)b, 0);
|
Mouse.move(pendingDX, (int8_t)b, 0);
|
||||||
estado = AGUARDA_CMD;
|
estado = AGUARDA_CMD;
|
||||||
// Sem piscaVerde() aqui — delay(40) bloquearia o processamento serial
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// ── Mouse: roda do mouse ──────────────────────────────────────────────
|
// ── Mouse: roda do mouse / touchpad dois dedos ────────────────────────
|
||||||
case AGUARDA_MOUSE_WHEEL:
|
case AGUARDA_MOUSE_WHEEL:
|
||||||
Mouse.move(0, 0, (int8_t)b);
|
Mouse.move(0, 0, (int8_t)b);
|
||||||
estado = AGUARDA_CMD;
|
estado = AGUARDA_CMD;
|
||||||
@ -138,7 +129,6 @@ void loop() {
|
|||||||
case AGUARDA_TECLA:
|
case AGUARDA_TECLA:
|
||||||
Keyboard.write(b);
|
Keyboard.write(b);
|
||||||
estado = AGUARDA_CMD;
|
estado = AGUARDA_CMD;
|
||||||
piscaVerde();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// ── Clique: lê L ou R e clica ─────────────────────────────────────────
|
// ── Clique: lê L ou R e clica ─────────────────────────────────────────
|
||||||
@ -146,21 +136,18 @@ void loop() {
|
|||||||
if (b == 'L') Mouse.click(MOUSE_LEFT);
|
if (b == 'L') Mouse.click(MOUSE_LEFT);
|
||||||
if (b == 'R') Mouse.click(MOUSE_RIGHT);
|
if (b == 'R') Mouse.click(MOUSE_RIGHT);
|
||||||
estado = AGUARDA_CMD;
|
estado = AGUARDA_CMD;
|
||||||
piscaVerde();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// ── Tecla press (mantém pressionada) ─────────────────────────────────
|
// ── Tecla press (mantém pressionada) ─────────────────────────────────
|
||||||
case AGUARDA_PRESS_KEY:
|
case AGUARDA_PRESS_KEY:
|
||||||
Keyboard.press(b);
|
Keyboard.press(b);
|
||||||
estado = AGUARDA_CMD;
|
estado = AGUARDA_CMD;
|
||||||
piscaVerde();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// ── Tecla release ─────────────────────────────────────────────────────
|
// ── Tecla release ─────────────────────────────────────────────────────
|
||||||
case AGUARDA_RELEASE_KEY:
|
case AGUARDA_RELEASE_KEY:
|
||||||
Keyboard.release(b);
|
Keyboard.release(b);
|
||||||
estado = AGUARDA_CMD;
|
estado = AGUARDA_CMD;
|
||||||
piscaVerde();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// ── Mouse press (botão segurado) ──────────────────────────────────────
|
// ── Mouse press (botão segurado) ──────────────────────────────────────
|
||||||
@ -168,7 +155,6 @@ void loop() {
|
|||||||
if (b == 'L') Mouse.press(MOUSE_LEFT);
|
if (b == 'L') Mouse.press(MOUSE_LEFT);
|
||||||
if (b == 'R') Mouse.press(MOUSE_RIGHT);
|
if (b == 'R') Mouse.press(MOUSE_RIGHT);
|
||||||
estado = AGUARDA_CMD;
|
estado = AGUARDA_CMD;
|
||||||
piscaVerde();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// ── Mouse release ─────────────────────────────────────────────────────
|
// ── Mouse release ─────────────────────────────────────────────────────
|
||||||
@ -176,7 +162,6 @@ void loop() {
|
|||||||
if (b == 'L') Mouse.release(MOUSE_LEFT);
|
if (b == 'L') Mouse.release(MOUSE_LEFT);
|
||||||
if (b == 'R') Mouse.release(MOUSE_RIGHT);
|
if (b == 'R') Mouse.release(MOUSE_RIGHT);
|
||||||
estado = AGUARDA_CMD;
|
estado = AGUARDA_CMD;
|
||||||
piscaVerde();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
Principal.Designer.cs
generated
3
Principal.Designer.cs
generated
@ -178,7 +178,8 @@ namespace KVMote
|
|||||||
|
|
||||||
int contentH = sepY + 74 + 22 + 16;
|
int contentH = sepY + 74 + 22 + 16;
|
||||||
|
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||||
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||||
this.BackColor = System.Drawing.Color.FromArgb(20, 20, 20);
|
this.BackColor = System.Drawing.Color.FromArgb(20, 20, 20);
|
||||||
this.ClientSize = new System.Drawing.Size(380, contentH + 28);
|
this.ClientSize = new System.Drawing.Size(380, contentH + 28);
|
||||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
|
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
|
||||||
|
|||||||
26
Principal.cs
26
Principal.cs
@ -89,6 +89,7 @@ namespace KVMote
|
|||||||
private int _pendingDX, _pendingDY;
|
private int _pendingDX, _pendingDY;
|
||||||
private bool _isWarping;
|
private bool _isWarping;
|
||||||
private readonly Stopwatch _mouseThrottle = Stopwatch.StartNew();
|
private readonly Stopwatch _mouseThrottle = Stopwatch.StartNew();
|
||||||
|
private int _wheelAccum; // acumula deltas do touchpad (smooth scroll)
|
||||||
|
|
||||||
// Timers
|
// Timers
|
||||||
private readonly System.Windows.Forms.Timer _watchdog = new System.Windows.Forms.Timer { Interval = 2000 };
|
private readonly System.Windows.Forms.Timer _watchdog = new System.Windows.Forms.Timer { Interval = 2000 };
|
||||||
@ -238,6 +239,7 @@ namespace KVMote
|
|||||||
_heartbeat = new System.Threading.Timer(OnHeartbeat, null, HeartbeatMs, HeartbeatMs);
|
_heartbeat = new System.Threading.Timer(OnHeartbeat, null, HeartbeatMs, HeartbeatMs);
|
||||||
InstallHooks();
|
InstallHooks();
|
||||||
SetConnectedUI(true);
|
SetConnectedUI(true);
|
||||||
|
Send(new byte[] { (byte)'H' }); // LED azul no Arduino
|
||||||
SetStatus("Modo Host \u25cf Conectado", System.Drawing.Color.Green);
|
SetStatus("Modo Host \u25cf Conectado", System.Drawing.Color.Green);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -256,6 +258,9 @@ namespace KVMote
|
|||||||
_watchdog.Stop();
|
_watchdog.Stop();
|
||||||
_heartbeat?.Dispose(); _heartbeat = null;
|
_heartbeat?.Dispose(); _heartbeat = null;
|
||||||
UninstallHooks();
|
UninstallHooks();
|
||||||
|
// Sinaliza ao Arduino antes de fechar (LED verde = sem host)
|
||||||
|
if (_port?.IsOpen == true)
|
||||||
|
try { lock (_sendLock) { _port.Write(new byte[] { (byte)'G' }, 0, 1); } } catch { }
|
||||||
ClosePort();
|
ClosePort();
|
||||||
SetConnectedUI(false);
|
SetConnectedUI(false);
|
||||||
if (userInitiated) SetStatus("Desconectado", System.Drawing.Color.Gray);
|
if (userInitiated) SetStatus("Desconectado", System.Drawing.Color.Gray);
|
||||||
@ -346,6 +351,11 @@ namespace KVMote
|
|||||||
{
|
{
|
||||||
if (_isReconnecting) return;
|
if (_isReconnecting) return;
|
||||||
_isReconnecting = true;
|
_isReconnecting = true;
|
||||||
|
|
||||||
|
// Garante que o cursor volta imediatamente se estava em modo cliente
|
||||||
|
if (InvokeRequired) Invoke((Action)(() => ExitClientMode(sendRelease: false)));
|
||||||
|
else ExitClientMode(sendRelease: false);
|
||||||
|
|
||||||
string portName = _port?.PortName ?? "";
|
string portName = _port?.PortName ?? "";
|
||||||
SetStatus("Reconectando...", System.Drawing.Color.Orange);
|
SetStatus("Reconectando...", System.Drawing.Color.Orange);
|
||||||
if (InvokeRequired) Invoke((Action)(() => SetConnectedUI(false)));
|
if (InvokeRequired) Invoke((Action)(() => SetConnectedUI(false)));
|
||||||
@ -404,7 +414,7 @@ namespace KVMote
|
|||||||
|
|
||||||
if (!_clientMode)
|
if (!_clientMode)
|
||||||
{
|
{
|
||||||
if (msg == WM_MOUSEMOVE && _clientPos != ClientPos.None && IsAtExitEdge(cursorPt))
|
if (msg == WM_MOUSEMOVE && _clientPos != ClientPos.None && !_isReconnecting && IsAtExitEdge(cursorPt))
|
||||||
EnterClientMode(cursorPt);
|
EnterClientMode(cursorPt);
|
||||||
return CallNextHookEx(_mouseHook, nCode, wParam, lParam);
|
return CallNextHookEx(_mouseHook, nCode, wParam, lParam);
|
||||||
}
|
}
|
||||||
@ -447,10 +457,16 @@ namespace KVMote
|
|||||||
|
|
||||||
if (msg == WM_MOUSEWHEEL)
|
if (msg == WM_MOUSEWHEEL)
|
||||||
{
|
{
|
||||||
short delta = (short)(info.mouseData >> 16);
|
// Acumula deltas: mouse wheel envia ±120/notch,
|
||||||
int notches = Math.Clamp(delta / 120, -127, 127);
|
// touchpad 2 dedos envia valores pequenos (±3..±15).
|
||||||
if (notches != 0)
|
// Enviamos 1 unidade ao Arduino a cada 120 acumulados.
|
||||||
Send(new byte[] { (byte)'W', (byte)(sbyte)notches });
|
_wheelAccum += (short)(info.mouseData >> 16);
|
||||||
|
int toSend = _wheelAccum / 120;
|
||||||
|
if (toSend != 0)
|
||||||
|
{
|
||||||
|
_wheelAccum -= toSend * 120;
|
||||||
|
Send(new byte[] { (byte)'W', (byte)(sbyte)Math.Clamp(toSend, -127, 127) });
|
||||||
|
}
|
||||||
return (IntPtr)1;
|
return (IntPtr)1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ namespace KVMote
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
static void Main()
|
static void Main()
|
||||||
{
|
{
|
||||||
|
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
|
||||||
ApplicationConfiguration.Initialize();
|
ApplicationConfiguration.Initialize();
|
||||||
Application.Run(new Principal());
|
Application.Run(new Principal());
|
||||||
}
|
}
|
||||||
|
|||||||
36
Properties/PublishProfiles/ClickOnceProfile.pubxml
Normal file
36
Properties/PublishProfiles/ClickOnceProfile.pubxml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||||
|
-->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<ApplicationRevision>1</ApplicationRevision>
|
||||||
|
<ApplicationVersion>1.0.0.*</ApplicationVersion>
|
||||||
|
<BootstrapperEnabled>True</BootstrapperEnabled>
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<CreateWebPageOnPublish>False</CreateWebPageOnPublish>
|
||||||
|
<GenerateManifests>true</GenerateManifests>
|
||||||
|
<Install>True</Install>
|
||||||
|
<InstallFrom>Disk</InstallFrom>
|
||||||
|
<IsRevisionIncremented>True</IsRevisionIncremented>
|
||||||
|
<IsWebBootstrapper>False</IsWebBootstrapper>
|
||||||
|
<MapFileExtensions>True</MapFileExtensions>
|
||||||
|
<OpenBrowserOnPublish>False</OpenBrowserOnPublish>
|
||||||
|
<Platform>Any CPU</Platform>
|
||||||
|
<PublishDir>bin\Release\net8.0-windows\win-x64\app.publish\</PublishDir>
|
||||||
|
<PublishUrl>bin\publish\</PublishUrl>
|
||||||
|
<PublishProtocol>ClickOnce</PublishProtocol>
|
||||||
|
<PublishReadyToRun>False</PublishReadyToRun>
|
||||||
|
<PublishSingleFile>False</PublishSingleFile>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>True</SelfContained>
|
||||||
|
<SignatureAlgorithm>(none)</SignatureAlgorithm>
|
||||||
|
<SignManifests>False</SignManifests>
|
||||||
|
<SkipPublishVerification>false</SkipPublishVerification>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<UpdateEnabled>False</UpdateEnabled>
|
||||||
|
<UpdateMode>Foreground</UpdateMode>
|
||||||
|
<UpdateRequired>False</UpdateRequired>
|
||||||
|
<WebPageFileName>Publish.html</WebPageFileName>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
236
conhecimento.md
Normal file
236
conhecimento.md
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
# KVMote — Conhecimento do Projeto
|
||||||
|
|
||||||
|
Documento de referência técnica e histórico de decisões do projeto KVMote.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## O que é
|
||||||
|
|
||||||
|
KVMote é um KVM (Keyboard, Video, Mouse) over Bluetooth que permite controlar um PC remoto usando o teclado e mouse do seu PC principal, sem instalar nenhum software no PC controlado.
|
||||||
|
|
||||||
|
**Inspirado em:** Barrier / InputLeap / Mouse Without Borders — mas funciona em ambientes corporativos restritivos porque o PC cliente enxerga apenas um teclado e mouse USB comuns (Arduino).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como funciona
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐ Bluetooth SPP ┌──────────────────┐
|
||||||
|
│ PC Host │ ◄────── (COM virtual) ────────► │ HC-06 │
|
||||||
|
│ KVMote.exe │ │ (Bluetooth) │
|
||||||
|
│ - captura mouse │ └──────┬───────────┘
|
||||||
|
│ - captura teclado │ │ Serial 9600
|
||||||
|
│ - detecta borda │ ┌──────▼───────────┐
|
||||||
|
└─────────────────────┘ │ Arduino Leonardo │
|
||||||
|
│ (USB HID) │
|
||||||
|
└──────┬────────────┘
|
||||||
|
│ USB
|
||||||
|
┌──────▼───────────┐
|
||||||
|
│ PC Cliente │
|
||||||
|
│ (sem software) │
|
||||||
|
└───────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
1. O **KVMote.exe** roda no PC host (onde estão teclado e mouse físicos)
|
||||||
|
2. O mouse chega na borda da tela configurada → cursor some → modo cliente ativo
|
||||||
|
3. Movimentos de mouse e teclas são capturados por hooks globais do Windows
|
||||||
|
4. Enviados via **Bluetooth Serial** ao HC-06 → Arduino Leonardo
|
||||||
|
5. O Arduino injeta os eventos como **USB HID** no PC cliente
|
||||||
|
6. Para voltar: mova o mouse ~15px de volta na direção do host
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hardware necessário
|
||||||
|
|
||||||
|
| Componente | Modelo | Observação |
|
||||||
|
|---|---|---|
|
||||||
|
| Microcontrolador | **Arduino Leonardo** | Obrigatório — único com HID USB nativo (ATmega32U4) |
|
||||||
|
| Bluetooth | **HC-06** | Módulo slave, 9600 baud padrão |
|
||||||
|
| LED indicador | LED RGB catodo comum | Pinos R=5, G=6, B=9 (PWM) |
|
||||||
|
| Resistores | ~100Ω por canal | Proteção do LED |
|
||||||
|
|
||||||
|
**Arduino Uno não funciona** — não tem suporte HID nativo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Instalação e configuração
|
||||||
|
|
||||||
|
### PC Cliente (onde o Arduino fica conectado)
|
||||||
|
1. Conectar o Arduino Leonardo via USB (aparece como teclado + mouse)
|
||||||
|
2. Verificar que o HC-06 está pareado com o PC Host via Bluetooth Windows
|
||||||
|
3. Nenhum software adicional necessário
|
||||||
|
|
||||||
|
### PC Host (onde o KVMote.exe roda)
|
||||||
|
1. Parear o HC-06 no Bluetooth do Windows → anota a porta COM gerada
|
||||||
|
2. Rodar `KVMote.exe` (portable, sem instalação)
|
||||||
|
3. Selecionar a posição do PC cliente (Esquerda / Direita / Acima / Abaixo)
|
||||||
|
4. Clicar **Detectar** → aguarda auto-detecção da porta COM
|
||||||
|
5. Selecionar o layout do teclado do PC cliente (US ou PT-BR ABNT2)
|
||||||
|
6. Clicar **Conectar**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Indicadores LED do Arduino
|
||||||
|
|
||||||
|
| Cor | Significado |
|
||||||
|
|---|---|
|
||||||
|
| 🟢 Verde | Arduino ligado, aguardando conexão do host |
|
||||||
|
| 🔵 Azul | Host conectado, mouse no PC host |
|
||||||
|
| 🟣 Magenta | Mouse no PC cliente (modo ativo) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocolo serial binário
|
||||||
|
|
||||||
|
Comunicação Host → Arduino a 9600 baud:
|
||||||
|
|
||||||
|
| Comando | Bytes | Ação |
|
||||||
|
|---|---|---|
|
||||||
|
| `M` + dx + dy | 3 | Mover mouse (valores int8 com sinal) |
|
||||||
|
| `W` + delta | 2 | Roda do mouse / scroll touchpad |
|
||||||
|
| `K` + char | 2 | Digitar caractere (Keyboard.write) |
|
||||||
|
| `P` + keycode | 2 | Pressionar tecla (mantém pressionada) |
|
||||||
|
| `U` + keycode | 2 | Soltar tecla |
|
||||||
|
| `A` | 1 | Soltar todas as teclas |
|
||||||
|
| `C` + L/R | 2 | Clique do mouse |
|
||||||
|
| `D` + L/R | 2 | Pressionar botão do mouse (arrastar) |
|
||||||
|
| `E` + L/R | 2 | Soltar botão do mouse |
|
||||||
|
| `O` | 1 | LED magenta (entrou no cliente) |
|
||||||
|
| `H` | 1 | LED azul (host conectado) |
|
||||||
|
| `G` | 1 | LED verde (host desconectado) |
|
||||||
|
| `~` | 1 | Ping → Arduino responde `[PONG]` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Funcionalidades implementadas
|
||||||
|
|
||||||
|
### KVM básico
|
||||||
|
- ✅ Controle de mouse (move, clique, arraste, scroll)
|
||||||
|
- ✅ Controle de teclado (todas as teclas, modificadores, F-keys, numpad)
|
||||||
|
- ✅ Detecção de borda (Left, Right, Above, Below)
|
||||||
|
- ✅ Retorno ao host por coordenadas virtuais (threshold 15px)
|
||||||
|
- ✅ Cursor escondido durante modo cliente
|
||||||
|
|
||||||
|
### Conexão
|
||||||
|
- ✅ Auto-detecção de porta COM por PONG handshake
|
||||||
|
- ✅ Heartbeat a cada 3s (PONG timeout = 9s)
|
||||||
|
- ✅ Reconexão automática em loop
|
||||||
|
- ✅ Saída do modo cliente ao perder conexão (cursor sempre volta)
|
||||||
|
|
||||||
|
### Scroll
|
||||||
|
- ✅ Mouse wheel físico
|
||||||
|
- ✅ Touchpad dois dedos (acumulador de delta para smooth scroll)
|
||||||
|
|
||||||
|
### Clipboard
|
||||||
|
- ✅ Ctrl+V (ou Shift+Ins) em modo cliente envia texto do host como digitação
|
||||||
|
- ✅ Limite de 300 caracteres
|
||||||
|
- ✅ Suporte a layout PT-BR ABNT2 (remapeamento de pontuação)
|
||||||
|
- ✅ ReleaseAll antes de digitar (evita Ctrl+letra indesejado)
|
||||||
|
|
||||||
|
### Layout de teclado (clipboard)
|
||||||
|
| Layout | Chars problemáticos corrigidos |
|
||||||
|
|---|---|
|
||||||
|
| US / Internacional | — (padrão) |
|
||||||
|
| PT-BR ABNT2 | `;` `:` `[` `]` `{` `}` mapeados corretamente |
|
||||||
|
|
||||||
|
Chars não mapeáveis em PT-BR (ignorados no paste): `/` `?` `\` `|` `@`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitetura do código C#
|
||||||
|
|
||||||
|
### Principal.cs — seções
|
||||||
|
1. **P/Invoke** — SetWindowsHookEx, ClipCursor, SetCursorPos
|
||||||
|
2. **Auto-detect** — ProbePorts, PONG handshake
|
||||||
|
3. **Position selector** — botões Acima/Esquerda/Direita/Abaixo
|
||||||
|
4. **Connect/Disconnect** — abertura de porta, timers, hooks
|
||||||
|
5. **Port management** — OpenPort, ClosePort, OnDataReceived
|
||||||
|
6. **Watchdog + Heartbeat + Reconnect** — saúde da conexão
|
||||||
|
7. **Global Hooks** — instala/remove WH_MOUSE_LL e WH_KEYBOARD_LL
|
||||||
|
8. **Mouse Hook Callback** — edge detection, warp, virtual coords, scroll
|
||||||
|
9. **Keyboard Hook Callback** — Ctrl+V intercept, P/U por VK code
|
||||||
|
10. **VK → Arduino keycode mapping** — tabela + ranges (a-z, 0-9, numpad)
|
||||||
|
11. **Send methods** — Send (blocking+lock), SendMouse (lossy TryEnter)
|
||||||
|
12. **Utilities** — SetStatus, SetPortInfo, Log
|
||||||
|
13. **Clipboard send + layout** — TranslateChar, SendClipboardToClient
|
||||||
|
|
||||||
|
### Técnica de mouse (FPS warp)
|
||||||
|
- `ClipCursor` não é usado — causava problemas
|
||||||
|
- Em vez disso: cursor fica em posição fixa no centro (`_lastRawPos`)
|
||||||
|
- A cada WM_MOUSEMOVE: calcula delta, acumula em `_pendingDX/_pendingDY`, faz warp de volta
|
||||||
|
- Throttle de 50ms antes de enviar ao Arduino
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisões de projeto
|
||||||
|
|
||||||
|
| Decisão | Alternativa considerada | Motivo |
|
||||||
|
|---|---|---|
|
||||||
|
| Arduino Leonardo | Arduino Uno | Uno não tem HID nativo |
|
||||||
|
| 9600 baud | 115200 baud | HC-06 padrão de fábrica, simples |
|
||||||
|
| WH_MOUSE_LL hook | Raw Input API | Mais simples, suficiente |
|
||||||
|
| Monitor.TryEnter para mouse | Queue assíncrona | Lossy é OK para mouse, não bloqueia |
|
||||||
|
| Coordenadas virtuais para retorno | Timer de inatividade | Mais natural, sem delay artificial |
|
||||||
|
| AutoScaleMode.Dpi | AutoScaleMode.None | None quebrava em telas de alta resolução |
|
||||||
|
| TCP não usado | TCP local seria mais rápido | Corporativo bloqueia portas |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitações conhecidas
|
||||||
|
|
||||||
|
- **Velocidade:** 9600 baud limita a ~50 teclas/s no clipboard; mouse trava durante paste
|
||||||
|
- **Chars acentuados:** `é`, `ã`, `ç` etc. não enviáveis via clipboard (não-ASCII)
|
||||||
|
- **Monitor único:** sem suporte a multi-monitor no host
|
||||||
|
- **Um cliente:** arquitetura 1:1
|
||||||
|
- **PT-BR parcial:** `/`, `?`, `\`, `|`, `@` não mapeáveis via Keyboard.write()
|
||||||
|
- **Tela do cliente:** não capturada (KV sem o V — só K e M)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### Clipboard Bridge (próximo projeto)
|
||||||
|
App separado para compartilhar clipboard entre host e cliente. Transporte via:
|
||||||
|
1. **OneDrive pasta compartilhada** — funciona em corporativo (Microsoft 365 whitelisted)
|
||||||
|
2. **TCP/IP local** — rápido, para redes domésticas
|
||||||
|
3. **Bluetooth RFCOMM** — PC a PC direto, sem rede
|
||||||
|
|
||||||
|
Suporte planejado: texto (qualquer tamanho), imagens PNG (comprimidas), não arquivos.
|
||||||
|
|
||||||
|
### Melhorias futuras
|
||||||
|
- Baud rate configurável (AT commands HC-06)
|
||||||
|
- Multi-monitor
|
||||||
|
- Layout US-International completo com dead keys
|
||||||
|
- Indicador visual de latência BT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como gerar o executável portable
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Na pasta do projeto:
|
||||||
|
|
||||||
|
# Self-contained (~70MB) — roda sem .NET instalado
|
||||||
|
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
|
||||||
|
|
||||||
|
# Dependente do runtime (~2MB) — requer .NET 8 Desktop Runtime
|
||||||
|
dotnet publish -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Saída: `bin\Release\net8.0-windows\win-x64\publish\KVMote.exe`
|
||||||
|
|
||||||
|
> ⚠️ Não usar o botão "Publicar" do Visual Studio — gera ClickOnce (múltiplos arquivos).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Sintoma | Causa provável | Solução |
|
||||||
|
|---|---|---|
|
||||||
|
| Porta não detectada | HC-06 não pareado | Parear BT no Windows antes |
|
||||||
|
| Cursor some e não volta | Conexão caiu em modo cliente | Mover mouse — reconexão automática restaura cursor |
|
||||||
|
| Teclas erradas no cliente | Layout diferente | Ajustar "Layout do cliente" no app |
|
||||||
|
| Scroll não funciona | Apenas no touchpad? | Acumulador captura, mas pode ser lento |
|
||||||
|
| Mouse lento no cliente | Throttle 50ms | Normal — limitação do BT 9600 baud |
|
||||||
|
| Texto colado com chars errados | Layout PT-BR | Selecionar PT-BR ABNT2 no app |
|
||||||
|
| App muito pequeno no notebook | Tela alta resolução | App já corrigido com AutoScaleMode.Dpi |
|
||||||
Loading…
Reference in New Issue
Block a user