diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5917a8c --- /dev/null +++ b/CLAUDE.md @@ -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 +disable +``` +**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 diff --git a/KVMote.ino b/KVMote.ino index a622d00..a66b892 100644 --- a/KVMote.ino +++ b/KVMote.ino @@ -15,8 +15,9 @@ 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) + LED cliente → 'O' (laranja — mouse no cliente) + LED host ok → 'H' (azul — host conectado) + LED sem host → 'G' (verde — host desconectado) Ping/Pong → '~' → responde [PONG] 1 byte */ @@ -31,7 +32,7 @@ #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 +uint8_t basR = 0, basG = 255, basB = 0; // verde = aguardando conexão // ── Máquina de estados ──────────────────────────────────────────────────────── enum Estado : uint8_t { @@ -50,31 +51,19 @@ enum Estado : uint8_t { Estado estado = AGUARDA_CMD; int8_t pendingDX = 0; -// ── Helpers de LED ──────────────────────────────────────────────────────────── +// ── Helper 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 + ledCor(basR, basG, basB); // verde = aguardando conexão do host } // ── Loop principal (non-blocking) ───────────────────────────────────────────── @@ -96,21 +85,24 @@ void loop() { 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; + // Entrou no PC cliente → LED magenta + basR = 255; basG = 0; basB = 255; ledCor(basR, basG, basB); } else if (b == 'H') { - // Voltou ao host → flash verde, LED azul + // Host conectado → LED azul 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 == '~') { Serial1.print("[PONG]"); - piscaCiano(); } // qualquer outro byte é silenciosamente descartado break; @@ -125,10 +117,9 @@ void loop() { 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 ────────────────────────────────────────────── + // ── Mouse: roda do mouse / touchpad dois dedos ──────────────────────── case AGUARDA_MOUSE_WHEEL: Mouse.move(0, 0, (int8_t)b); estado = AGUARDA_CMD; @@ -138,7 +129,6 @@ void loop() { case AGUARDA_TECLA: Keyboard.write(b); estado = AGUARDA_CMD; - piscaVerde(); break; // ── Clique: lê L ou R e clica ───────────────────────────────────────── @@ -146,21 +136,18 @@ void loop() { 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) ────────────────────────────────────── @@ -168,7 +155,6 @@ void loop() { if (b == 'L') Mouse.press(MOUSE_LEFT); if (b == 'R') Mouse.press(MOUSE_RIGHT); estado = AGUARDA_CMD; - piscaVerde(); break; // ── Mouse release ───────────────────────────────────────────────────── @@ -176,7 +162,6 @@ void loop() { if (b == 'L') Mouse.release(MOUSE_LEFT); if (b == 'R') Mouse.release(MOUSE_RIGHT); estado = AGUARDA_CMD; - piscaVerde(); break; } } diff --git a/Principal.Designer.cs b/Principal.Designer.cs index 1c2944b..97bf18a 100644 --- a/Principal.Designer.cs +++ b/Principal.Designer.cs @@ -178,7 +178,8 @@ namespace KVMote 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.ClientSize = new System.Drawing.Size(380, contentH + 28); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; diff --git a/Principal.cs b/Principal.cs index d79663f..feadba5 100644 --- a/Principal.cs +++ b/Principal.cs @@ -89,6 +89,7 @@ namespace KVMote private int _pendingDX, _pendingDY; private bool _isWarping; private readonly Stopwatch _mouseThrottle = Stopwatch.StartNew(); + private int _wheelAccum; // acumula deltas do touchpad (smooth scroll) // Timers private readonly System.Windows.Forms.Timer _watchdog = new System.Windows.Forms.Timer { Interval = 2000 }; @@ -238,6 +239,7 @@ namespace KVMote _heartbeat = new System.Threading.Timer(OnHeartbeat, null, HeartbeatMs, HeartbeatMs); InstallHooks(); SetConnectedUI(true); + Send(new byte[] { (byte)'H' }); // LED azul no Arduino SetStatus("Modo Host \u25cf Conectado", System.Drawing.Color.Green); } else @@ -256,6 +258,9 @@ namespace KVMote _watchdog.Stop(); _heartbeat?.Dispose(); _heartbeat = null; UninstallHooks(); + // Sinaliza ao Arduino antes de fechar (LED verde = sem host) + if (_port?.IsOpen == true) + try { lock (_sendLock) { _port.Write(new byte[] { (byte)'G' }, 0, 1); } } catch { } ClosePort(); SetConnectedUI(false); if (userInitiated) SetStatus("Desconectado", System.Drawing.Color.Gray); @@ -346,6 +351,11 @@ namespace KVMote { if (_isReconnecting) return; _isReconnecting = true; + + // Garante que o cursor volta imediatamente se estava em modo cliente + if (InvokeRequired) Invoke((Action)(() => ExitClientMode(sendRelease: false))); + else ExitClientMode(sendRelease: false); + string portName = _port?.PortName ?? ""; SetStatus("Reconectando...", System.Drawing.Color.Orange); if (InvokeRequired) Invoke((Action)(() => SetConnectedUI(false))); @@ -404,7 +414,7 @@ namespace KVMote if (!_clientMode) { - if (msg == WM_MOUSEMOVE && _clientPos != ClientPos.None && IsAtExitEdge(cursorPt)) + if (msg == WM_MOUSEMOVE && _clientPos != ClientPos.None && !_isReconnecting && IsAtExitEdge(cursorPt)) EnterClientMode(cursorPt); return CallNextHookEx(_mouseHook, nCode, wParam, lParam); } @@ -447,10 +457,16 @@ namespace KVMote 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 }); + // Acumula deltas: mouse wheel envia ±120/notch, + // touchpad 2 dedos envia valores pequenos (±3..±15). + // Enviamos 1 unidade ao Arduino a cada 120 acumulados. + _wheelAccum += (short)(info.mouseData >> 16); + int toSend = _wheelAccum / 120; + if (toSend != 0) + { + _wheelAccum -= toSend * 120; + Send(new byte[] { (byte)'W', (byte)(sbyte)Math.Clamp(toSend, -127, 127) }); + } return (IntPtr)1; } diff --git a/Program.cs b/Program.cs index a79c7b3..e9306ee 100644 --- a/Program.cs +++ b/Program.cs @@ -8,6 +8,7 @@ namespace KVMote [STAThread] static void Main() { + Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); ApplicationConfiguration.Initialize(); Application.Run(new Principal()); } diff --git a/Properties/PublishProfiles/ClickOnceProfile.pubxml b/Properties/PublishProfiles/ClickOnceProfile.pubxml new file mode 100644 index 0000000..57e2071 --- /dev/null +++ b/Properties/PublishProfiles/ClickOnceProfile.pubxml @@ -0,0 +1,36 @@ + + + + + 1 + 1.0.0.* + True + Release + False + true + True + Disk + True + False + True + False + Any CPU + bin\Release\net8.0-windows\win-x64\app.publish\ + bin\publish\ + ClickOnce + False + False + win-x64 + True + (none) + False + false + net8.0-windows + False + Foreground + False + Publish.html + + \ No newline at end of file diff --git a/conhecimento.md b/conhecimento.md new file mode 100644 index 0000000..1a05913 --- /dev/null +++ b/conhecimento.md @@ -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 |