我们通常使用的鼠标发送的都是相对坐标,比如:输出 X=100,Y=200, 意思是通知系统将鼠标从目前的位置移动(100,200)个单位。此外,Windows 还有一个鼠标优化机制,比如,你的鼠标输出范围是 +/- 127, 如果你的屏幕宽度是 1920, 经过优化之后无需滑动 1920/127 次才能将鼠标指针从最左边移动到最右边。
这次带来的PVD (Physical Virtual Device)是一款绝对值鼠标,它模拟了一个绝对值鼠标,能够将从USB串口输入的数据转化为绝对值鼠标数据发送出来。这样,可以方便的实现鼠标点击屏幕上的任意位置。
硬件部分使用和上一次设计相同【参考1】,核心是 WCH出品的 CH554e单片机。

代码是 Arduino 来实现的,屏幕左上角是 (0,0), 输出坐标为 0-32767。实践表明:送出来的数据和屏幕分辨率存在定比例关系。比如,在 1920*1024屏幕上,如果想让鼠标移动到 (300, 200)的位置,那么需要设备输出(300/1920 *32768, 200/1024 *32768)这样的数据。
主程序代码如下:
#ifndef USER_USB_RAM
#error "This example needs to be compiled with a USER USB setting"
#endif
#include <WS2812.h>
#include "src/CdcHidCombo/USBCDC.h"
#include "src/CdcHidCombo/USBHIDKeyboardMouse.h"
#define NUM_LEDS 1
#define COLOR_PER_LEDS 3
#define NUM_BYTES (NUM_LEDS*COLOR_PER_LEDS)
__xdata uint8_t ledData[NUM_BYTES];
#define KeyboardReportID 0x01
#define rMouseReportID 0x02
#define aMouseReportID 0x03
#define OnBoardLED 0x04
// Data format
// Keyboard(Total 9 bytes): 01(ReportID 01) + Keyboard data (8 Bytes)
// Mouse(Total 5 bytes): 02(ReportID 02) + Mouse Data (4 Bytes)
uint8_t recvStr[9];
uint8_t recvStrPtr = 0;
unsigned long Elsp;
void setup() {
USBInit();
Serial0_begin(115200);
delay(1000);
Serial0_print("start");
Elsp=0;
}
void loop() {
while (USBSerial_available()) {
uint8_t serialChar = USBSerial_read();
recvStr[recvStrPtr++] = serialChar;
if (recvStrPtr == 10) {
for (uint8_t i = 0; i < 9; i++) {
Serial0_write(recvStr[i]);
}
if (recvStr[0] == KeyboardReportID) { // Keyboard
USB_EP3_send(recvStr, 9);
}
if (recvStr[0] == rMouseReportID) {
USB_EP3_send(recvStr, 5); // Relative Mouse
}
if (recvStr[0] == aMouseReportID) {
USB_EP3_send(recvStr, 7); // Absolute Mouse
}
if (recvStr[0] == OnBoardLED) {
set_pixel_for_GRB_LED(ledData, 0, recvStr[1], recvStr[2], recvStr[3]);
neopixel_show_P1_5(ledData, NUM_BYTES);
}
recvStrPtr = 0;
}
Elsp=millis();
}
// If there is no data in 100ms, clear the receive buffer
if (millis()-Elsp>100) {
recvStrPtr = 0;
Elsp=millis();
}
}
如果USB CDC 收到以 aMouseReportID 起始的9个字节,那么会将数据直接转到绝对值鼠标端点然后发送给主机。
VS2019 编写一个上位机程序进行测试,代码如下:
// CDC_vKBMSTest.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
#include <windows.h>
#include <SetupAPI.h>
#include <tchar.h>
#include <iostream>
#include <cstring>
#include <atlstr.h>
#pragma comment(lib, "Setupapi.lib")
#define MY_USB_PID_VID _T("VID_1209&PID_C55C")
#define SCREENWIDTH 1920
#define SCREENHEIGHT 1200
int Port;
class ComPortException : public std::exception {
public:
ComPortException(DWORD errorCode) : errorCode(errorCode) {}
DWORD getErrorCode() const {
return errorCode;
}
private:
DWORD errorCode;
};
// 对串口portName 发送数据
// 无法打开串口返回 1
// 无法设置串口参数 2
void SendToComPort(const int port, const char* data) {
int Result = 0;
HANDLE hCom = INVALID_HANDLE_VALUE;
TCHAR portName[10]; // 用于存储 "COMp" 字符串
// 将整数 p 转换为 "COMp" 字符串
#ifdef UNICODE
swprintf(portName, 10, _T("\\\\.\\COM%d"), port);
#else
sprintf(portName, "COM%d", port);
#endif
try {
// 打开串口
hCom = CreateFile(portName,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL);
if (hCom == INVALID_HANDLE_VALUE) {
throw ComPortException(GetLastError());
}
// 设置串口参数
DCB dcb;
SecureZeroMemory(&dcb, sizeof(DCB));
dcb.DCBlength = sizeof(DCB);
if (!GetCommState(hCom, &dcb)) {
throw ComPortException(GetLastError());
}
dcb.BaudRate = CBR_115200; // 波特率
dcb.ByteSize = 8; // 数据位
dcb.StopBits = ONESTOPBIT; // 停止位
dcb.Parity = NOPARITY; // 校验位
if (!SetCommState(hCom, &dcb)) {
throw ComPortException(GetLastError());
}
// 设置超时参数
COMMTIMEOUTS timeouts;
timeouts.ReadIntervalTimeout = 50;
timeouts.ReadTotalTimeoutConstant = 50;
timeouts.ReadTotalTimeoutMultiplier = 10;
timeouts.WriteTotalTimeoutConstant = 50;
timeouts.WriteTotalTimeoutMultiplier = 10;
if (!SetCommTimeouts(hCom, &timeouts)) {
throw ComPortException(GetLastError());
}
// 发送数据
DWORD bytesWritten;
if (!WriteFile(hCom, data, 10, &bytesWritten, NULL)) {
std::cerr << "Failed to write to COM port." << std::endl;
}
else {
std::cout << "Successfully sent data to COM port: [" << port << "]" << std::endl;
}
for (int i = 0; i < 10; i++) {
printf("%02x ", data[i]&0xFF);
}
printf("\n");
// 关闭串口
if (hCom != INVALID_HANDLE_VALUE) {
CloseHandle(hCom);
}
}
catch (const ComPortException& ex) {
std::cerr << "Error: " << ex.getErrorCode() << std::endl;
if (hCom != INVALID_HANDLE_VALUE) {
CloseHandle(hCom);
}
}
}
/************************************************************************/
/* 根据USB描述信息字符串中读取
/************************************************************************/
int MTGetPortFromVidPid(CString strVidPid)
{
// 获取当前系统所有使用的设备
int nPort = -1;
int nStart = -1;
int nEnd = -1;
int i = 0;
CString strTemp, strName;
DWORD dwFlag = (DIGCF_ALLCLASSES | DIGCF_PRESENT);
HDEVINFO hDevInfo = INVALID_HANDLE_VALUE;
SP_DEVINFO_DATA sDevInfoData;
TCHAR szDis[2048] = { 0x00 };// 存储设备实例ID
TCHAR szFN[MAX_PATH] = { 0x00 };// 存储设备实例属性
DWORD nSize = 0;
// 准备遍历所有设备查找USB
hDevInfo = SetupDiGetClassDevs(NULL, L"USB", NULL, dwFlag);
if (INVALID_HANDLE_VALUE == hDevInfo)
goto STEP_END;
// 开始遍历所有设备
memset(&sDevInfoData, 0x00, sizeof(SP_DEVICE_INTERFACE_DATA));
sDevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
for (i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, &sDevInfoData); i++)
{
nSize = 0;
// 无效设备
if (!SetupDiGetDeviceInstanceId(hDevInfo, &sDevInfoData, szDis, sizeof(szDis), &nSize))
goto STEP_END;
// 根据设备信息寻找VID PID一致的设备
strTemp.Format(_T("%s"), szDis);
strTemp.MakeUpper();
if (strTemp.Find(strVidPid, 0) == -1)
continue;
// 查找设备属性
nSize = 0;
SetupDiGetDeviceRegistryProperty(hDevInfo, &sDevInfoData,
SPDRP_FRIENDLYNAME,
0, (PBYTE)szFN,
sizeof(szFN),
&nSize);
// "XXX Virtual Com Port (COM7)"
strName.Format(_T("%s"), szFN);
_tprintf(_T("%s"), szFN);
if (strName.IsEmpty())
//goto STEP_END;
continue;
// 寻找串口信息
nStart = strName.Find(_T("(COM"), 0);
nEnd = strName.Find(_T(")"), 0);
if (nStart == -1 || nEnd == -1)
//goto STEP_END;
continue;
strTemp = strName.Mid(nStart + 4, nEnd - nStart - 4);
nPort = _ttoi(strTemp);
}
STEP_END:
// 关闭设备信息集句柄
if (hDevInfo != INVALID_HANDLE_VALUE)
{
SetupDiDestroyDeviceInfoList(hDevInfo);
hDevInfo = INVALID_HANDLE_VALUE;
}
return nPort;
}
int ScreenX2Abs(int X) {
return (X* 32768 / SCREENWIDTH);
}
int ScreenY2Abs(int Y) {
return (Y * 32768 / SCREENHEIGHT);
}
void ChangeColor(
int X,
int Y
) {
char Data[10];
memset(Data, 0, sizeof(Data));
Data[0] = 3; // 绝对值鼠标
Data[1] = 1; // 点击
Data[2] = ScreenX2Abs(X) & 0xFF;
Data[3] = (ScreenX2Abs(X) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(Y) & 0xFF;
Data[5] = (ScreenY2Abs(Y) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
memset(Data, 0, sizeof(Data));
Data[0] = 3; // 绝对值鼠标
Data[1] = 0; // 点击
Data[2] = ScreenX2Abs(X) & 0xFF;
Data[3] = (ScreenX2Abs(X) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(Y) & 0xFF;
Data[5] = (ScreenY2Abs(Y) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
}
void LedColor(
int Color
) {
char Data[10];
memset(Data, 0, sizeof(Data));
Data[0] = 4; // LED
Data[1] = Color&0xFF; // 点击
Data[2] = (Color>>8) & 0xFFF;
Data[3] = (Color >>16) & 0xFF;
SendToComPort(Port, (char*)&Data);
}
void DrawRect(
int StartX,
int StartY,
int Width,
int Height,
int Dx,
int Dy
) {
char Data[10];
//从左到右横线
//line(StartX, StartY, StartX + Width, StartY);
memset(Data, 0, sizeof(Data));
Data[0] = 3; // 绝对值鼠标
Data[1] = 1; // 点击
Data[2] = ScreenX2Abs(StartX) & 0xFF;
Data[3] = (ScreenX2Abs(StartX) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY) & 0xFF;
Data[5] = (ScreenY2Abs(StartY) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
Data[0] = 3; // 绝对值鼠标
Data[1] = 1; // 点击
Data[2] = ScreenX2Abs(StartX+Width) & 0xFF;
Data[3] = (ScreenX2Abs(StartX+Width) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY) & 0xFF;
Data[5] = (ScreenY2Abs(StartY) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
Data[0] = 3; // 绝对值鼠标
Data[1] = 0; // 抬起
Data[2] = ScreenX2Abs(StartX + Width) & 0xFF;
Data[3] = (ScreenX2Abs(StartX + Width) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY) & 0xFF;
Data[5] = (ScreenY2Abs(StartY) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
//从上到下,竖线
//line(StartX + Width, StartY,StartX+Width,StartY+Height);
memset(Data, 0, sizeof(Data));
Data[0] = 3; // 绝对值鼠标
Data[1] = 1; // 点击
Data[2] = ScreenX2Abs(StartX+Width) & 0xFF;
Data[3] = (ScreenX2Abs(StartX + Width) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY) & 0xFF;
Data[5] = (ScreenY2Abs(StartY) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
Data[0] = 3; // 绝对值鼠标
Data[1] = 1; // 点击
Data[2] = ScreenX2Abs(StartX + Width) & 0xFF;
Data[3] = (ScreenX2Abs(StartX + Width) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY+Height) & 0xFF;
Data[5] = (ScreenY2Abs(StartY+Height) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
Data[0] = 3; // 绝对值鼠标
Data[1] = 0; // 抬起
Data[2] = ScreenX2Abs(StartX + Width) & 0xFF;
Data[3] = (ScreenX2Abs(StartX + Width) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY + Height) & 0xFF;
Data[5] = (ScreenY2Abs(StartY + Height) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
//从右到左,横线
//line(StartX + Width, StartY + Height,StartX+Dx,StartY+Height);
memset(Data, 0, sizeof(Data));
Data[0] = 3; // 绝对值鼠标
Data[1] = 1; // 点击
Data[2] = ScreenX2Abs(StartX + Width) & 0xFF;
Data[3] = (ScreenX2Abs(StartX + Width) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY+Height) & 0xFF;
Data[5] = (ScreenY2Abs(StartY+Height) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
Data[0] = 3; // 绝对值鼠标
Data[1] = 1; // 点击
Data[2] = ScreenX2Abs(StartX + Dx) & 0xFF;
Data[3] = (ScreenX2Abs(StartX + Dx) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY + Height) & 0xFF;
Data[5] = (ScreenY2Abs(StartY + Height) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
Data[0] = 3; // 绝对值鼠标
Data[1] = 0; // 抬起
Data[2] = ScreenX2Abs(StartX + Dx) & 0xFF;
Data[3] = (ScreenX2Abs(StartX + Dx) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY + Height) & 0xFF;
Data[5] = (ScreenY2Abs(StartY + Height) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
//从下到上竖线
//line(StartX + Dx, StartY + Height, StartX + Dx, StartY + Dy);
memset(Data, 0, sizeof(Data));
Data[0] = 3; // 绝对值鼠标
Data[1] = 1; // 点击
Data[2] = ScreenX2Abs(StartX + Dx) & 0xFF;
Data[3] = (ScreenX2Abs(StartX + Dx) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY + Height) & 0xFF;
Data[5] = (ScreenY2Abs(StartY + Height) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
Data[0] = 3; // 绝对值鼠标
Data[1] = 1; // 点击
Data[2] = ScreenX2Abs(StartX + Dx) & 0xFF;
Data[3] = (ScreenX2Abs(StartX + Dx) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY + Dy) & 0xFF;
Data[5] = (ScreenY2Abs(StartY + Dy) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
Data[0] = 3; // 绝对值鼠标
Data[1] = 0; // 抬起
Data[2] = ScreenX2Abs(StartX + Dx) & 0xFF;
Data[3] = (ScreenX2Abs(StartX + Dx) >> 8) & 0xFF;
Data[4] = ScreenY2Abs(StartY + Dy) & 0xFF;
Data[5] = (ScreenY2Abs(StartY + Dy) >> 8) & 0xFF;
SendToComPort(Port, (char*)&Data);
Sleep(100);
}
int main()
{
printf("%d[%x] %d[%x]\n", ScreenX2Abs(100), ScreenX2Abs(100), ScreenY2Abs(300), ScreenY2Abs(300));
printf("Virutal KB MS Test\n");
Port = MTGetPortFromVidPid(MY_USB_PID_VID);
if (Port == -1) {
printf("No device is found\n");
goto EndProgram;
}
else {
printf("Found COM%d\n",Port);
}
Sleep(5000);
for (int i = 0; i < 8; i++) {
DrawRect(100+20*i, 300+20*i, SCREENWIDTH - 200-20*i*2, SCREENHEIGHT - 500-20*i*2, 20, 20);
if ((i % 3) == 0) {
LedColor(0x01);
}
if ((i % 3) == 1) {
LedColor(0x0100);
}
if ((i % 3) == 2) {
LedColor(0x010000);
}
}
ChangeColor(1329,125);
for (int i = 8; i < 18; i++) {
DrawRect(100 + 20 * i, 300 + 20 * i, SCREENWIDTH - 200 - 20 * i * 2, SCREENHEIGHT - 500 - 20 * i * 2, 20, 20);
if ((i % 3) == 0) {
LedColor(0x01);
}
if ((i % 3) == 1) {
LedColor(0x0100);
}
if ((i % 3) == 2) {
LedColor(0x010000);
}
}
EndProgram:
printf("End\n");
getchar();
}
这个代码的作用是在画笔中绘制蛇形线,在绘制过程中还有更换颜色的动作。
工作的视频: 【PVD 计划:绝对值鼠标测试】
参考: