Arduino Mega/Uno制作宏定义键盘(不需刷firmware!!)

本人机械专业大一编程小白一枚,接触编程不到半年。近期开始自学Arduino。这应该是本人第一篇博客,有错误之处敬请斧正!

- 心路&思路

家住武汉,这个年嘛,大家应该都懂,被迫宅化。年前从网上买了一套Arduino的学习套件,在家里摆弄了一会儿就没管了。谁知道正准备过年出门走亲访友,一觉醒来就封城了。在家中闲着也是闲着,就想搞点事。

一直想买个宏键盘,能让我按一下按键就能完成一串快捷键操作,甚至能执行一系列指令。这个功能我的笔记本上就有,但是只有ROG这一个键可以定义。

我想到了可以用手头的Arduino。查找一番之后发现现在用Arduino做键盘主要有两种方法:

一是用Arduino Leonardo等使用ATmega32u4的板子。这个芯片集成了USB控制模块,配合Arduino官方的Keyboard.h库可以非常方便地把Arduino变成一个真真正正的USB键盘设备。

二是修改Arduino Uno等板子的Bootloader,在firmware层面把Uno或者Mega之类模拟成HID-USB设备。

然而我手头没有Leonardo板子,也没有刷Bootloader的下载器,要做个虚拟键盘怎么办呢?

毕竟是闲在家中,在补FATE和JOJO、玩P社游戏征服银河建设都市之余,我突然想到:

可以在电脑上做一个一直运行的后台程序做Arduino的上位机,他们之间进行串口通讯。下位机传递按键按下信息,上位机接到信号之后做出反应,向系统发出键盘消息,达到目的。

- 实现方式

下位机: Arduino Mega2560

上位机: 使用Qt Creator制作

其实上位机是后台程序,不需要界面。用图形界面的原因其实是一开始我准备把键位做成可以编辑的,就准备使用MFC做上位机(毕竟学校只教了MFC这一个界面)。然而MFC的串口通信实在是不方便,自带的串口控件已经弃用,第三方串口又难找技术文档和资源。在浏览相关内容的时候发现Qt的串口类十分好用,于是转战Qt。

-上位机:神奇的后台Qt界面程序[滑稽]

– 在Qt中使用WindowsAPI

在头文件#include <Windows.h>

除了头文件之外还需要一个静态库文件User32.LIB

这个文件在各位的电脑里位置都不一样,可以试着在C盘里搜索一下。我是在C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\um\x86下找到的。文件上一级文件夹是编译的目标平台,应该是要按自己编译器的目标平台来选,我目标是32位桌面应用程序,所以选的选的x86。

然后在pro文件中编译链接中添加一个静态库:LIBS +="C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\um\x86\User32.LIB"

接下来我们就可以任性使用WindowsAPI来达到我们的目的了。

– 使用WindowsAPI来向系统发送键盘消息。

发送消息的函数SendInput,定义如下:

UINT SendInput(
  UINT    cInputs,	//输入的个数
  LPINPUT pInputs,	//输入串(INPUT结构体数组的头指针、指向INPUT结构体的指针)
  int     cbSize	//pInputs的大小(不能多不能少,建议用sizeof()计算)
);

在Microsoft Docs中查阅SendInput函数

INPUT结构体:

typedef struct tagINPUT {
  DWORD type;		//输入的类型,这里我们用键盘输入的宏INPUT_KEYBOARD
 union {//这个共用体中如果是键盘输入就用ki
 MOUSEINPUT    mi;	//鼠标输入结构
   KEYBDINPUT    ki;	//键盘输入结构
   HARDWAREINPUT hi;	//硬件输入结构
  } DUMMYUNIONNAME;
} INPUT, *PINPUT, *LPINPUT;//这里解释了为什么SendInput()函数中LPINPUT类型的参数可以用INPUT的指针代替,因为他们就是一个东西

在Microsoft Docs中查阅INPUT结构

KEYBDINPUT结构体:

typedef struct tagKEYBDINPUT {
  WORD      wVk;//虚拟键值,
  WORD      wScan;//Unicode模式下的键值,这里我们不用
  DWORD     dwFlags;//标记键盘输入的一些状态,如果没有指定则为按下,指定为KEYEVENTF_KEYUP宏则为松开
  DWORD     time;//按下的时间,如果没有指定则系统自己处理
  ULONG_PTR dwExtraInfo;//额外信息
} KEYBDINPUT, *PKEYBDINPUT, *LPKEYBDINPUT;

在Microsoft Docs中查阅KETBDINPUT结构

我们只需要先定义好INPUT类型的输入,在想发送键盘消息的时候用sendInput调用就行。
需要注意的是系统需要时间相应按键按下,在按键按下的消息发送后一定要有延时,一般100ms就够。另外发送按下的消息之后一定要发送松开的消息,不然…

– Qt串口通信

这里只用了QtSerialPort/QSerialPort类的基本功能,但鉴于本项目“巨大的”发展空间(明明就是没做什么东西好吧!!!),日后可以添加内容较多,希望能深入了解单片机,把本程序做成可以通过软件编辑键值的!

准备工作:
在pro文件中
QT += serialport

在头文件中
#include <QSerialPort>

如果要进行搜寻显示串口信息操作再加一个
#include <QSerialPortInfo>

然后是基本的函数:

//serial initialization (Arduino Mega2560 @9600BaudRate)
    serial.setPortName("COM4");//设定串口名
    serial.setBaudRate(QSerialPort::Baud9600);//设定波特率为9600
    serial.setDataBits(QSerialPort::Data8);//设定数据位为8bit
    serial.setParity(QSerialPort::NoParity);//设定无奇偶校验
    serial.setStopBits(QSerialPort::OneStop);//设定停止位1位
    serial.setFlowControl(QSerialPort::NoFlowControl);//设定无流控制
    serial.open(QIODevice::ReadWrite);//打开串口,读写模式

这些我放在主窗口类的构造函数中运行。
这里serial是我在头文件中定义的QSerialPort对象。

QSerialPort类的对象有一个自带的信号readyRead。
只需要在主窗口类中添加一个槽并且和readyRead信号connect起来就好了。这个槽被我命名为serialportReadyRead,在这个槽内安置串口可以读取时读取串口缓冲区的数据并且做出处理。

//mainwindow.h中
private slots:
serialportReadyRead();

//mainwindow.cpp中
//在构造函数中
    connect(&serial, SIGNAL(readyRead()), this, SLOT(serialportReadyRead()));

//槽的实现void MainWindow::serialportReadyRead()
{
 QByteArray buffer=serial.readAll();
    //处理数据
}

在Qt Documentation中查阅QSerialPort类

– Qt程序后台运行

其实方法很简单,由于Qt的架构,窗口创建了之后需要调用show()方法才能显示窗口。只要在main()中不调用show()方法就可以让程序运行而不显示窗口。相比之下,MFC要实现相应操作就麻烦得多了。

– Qt代码

SendKeyEventTest.pro

QT       += core gui
QT       += serialport

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11

# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
    main.cpp \
    mainwindow.cpp

HEADERS += \
    mainwindow.h

FORMS += \
    mainwindow.ui

LIBS +="C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\um\x86\User32.LIB"
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <Windows.h>
#include <QtSerialPort/QSerialPort>
#include <QtSerialPort/QSerialPortInfo>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

    void Ctrl_Win_Left();
    void Ctrl_Win_Right();

private slots:
    void serialportReadyRead();

private:
    Ui::MainWindow *ui;

    QSerialPort serial;

    INPUT inputWinPress;
    INPUT inputWinRelease;
    INPUT inputCtrlPress;
    INPUT inputCtrlRelease;
    INPUT inputLeftPress;
    INPUT inputLeftRelease;
    INPUT inputRightPress;
    INPUT inputRightRelease;
    INPUT inputDelPress;
    INPUT inputDelRelease;
};
#endif // MAINWINDOW_H

main.cpp

#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    //w.show();
    return a.exec();
}

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    //win initialization
    inputWinPress.type = INPUT_KEYBOARD;
    inputWinPress.ki.wVk = VK_LWIN;
    //inputWinPress.ki.time = 100;

    inputWinRelease.type = INPUT_KEYBOARD;
    inputWinRelease.ki.wVk = VK_LWIN;
    inputWinRelease.ki.dwFlags = KEYEVENTF_KEYUP;
    //inputWinRelease.ki.time = 100;
    //Ctrl initialization
    inputCtrlPress.type = INPUT_KEYBOARD;
    inputCtrlPress.ki.wVk = VK_LCONTROL;
    //inputCtrlPress.ki.time = 100;

    inputCtrlRelease.type = INPUT_KEYBOARD;
    inputCtrlRelease.ki.wVk = VK_LCONTROL;
    inputCtrlRelease.ki.dwFlags = KEYEVENTF_KEYUP;
    //inputCtrlRelease.ki.time = 100;
    //Left Arrow initialization
    inputLeftPress.type = INPUT_KEYBOARD;
    inputLeftPress.ki.wVk = VK_LEFT;
    //inputLeftPress.ki.time = 100;

    inputLeftRelease.type = INPUT_KEYBOARD;
    inputLeftRelease.ki.wVk = VK_LEFT;
    inputLeftRelease.ki.dwFlags = KEYEVENTF_KEYUP;
    //inputLeftRelease.ki.time = 100;
    //Right Arrow initialization
    inputRightPress.type = INPUT_KEYBOARD;
    inputRightPress.ki.wVk = VK_RIGHT;
    //inputRightPress.ki.time = 100;

    inputRightRelease.type = INPUT_KEYBOARD;
    inputRightRelease.ki.wVk = VK_RIGHT;
    inputRightRelease.ki.dwFlags = KEYEVENTF_KEYUP;
    //inputRightRelease.ki.time = 100;
    //Delete initialization
    inputDelPress.type = INPUT_KEYBOARD;
    inputDelPress.ki.wVk = VK_DELETE;
    //inputDelPress.ki.time = 100;

    inputDelRelease.type = INPUT_KEYBOARD;
    inputDelRelease.ki.wVk = VK_DELETE;
    inputDelRelease.ki.dwFlags = KEYEVENTF_KEYUP;
    //inputDelRelease.ki.time = 100;

    //serial initialization (Arduino Mega2560 @9600BaudRate)
    serial.setPortName("COM4");
    serial.setBaudRate(QSerialPort::Baud9600);
    serial.setDataBits(QSerialPort::Data8);
    serial.setParity(QSerialPort::NoParity);
    serial.setStopBits(QSerialPort::OneStop);
    serial.setFlowControl(QSerialPort::NoFlowControl);
    serial.open(QIODevice::ReadWrite);
    connect(&serial, SIGNAL(readyRead()), this, SLOT(serialportReadyRead()));
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::Ctrl_Win_Right()
{
    SendInput(1,&inputWinPress,sizeof (inputWinPress));
    Sleep(100);
    SendInput(1,&inputCtrlPress,sizeof (inputCtrlPress));
    Sleep(100);
    SendInput(1,&inputRightPress,sizeof (inputRightPress));
    Sleep(100);
    SendInput(1,&inputRightRelease,sizeof (inputRightRelease));
    SendInput(1,&inputWinRelease,sizeof (inputWinRelease));
    SendInput(1,&inputCtrlRelease,sizeof (inputCtrlRelease));
}

void MainWindow::Ctrl_Win_Left()
{
    SendInput(1,&inputWinPress,sizeof (inputWinPress));
    Sleep(100);
    SendInput(1,&inputCtrlPress,sizeof (inputCtrlPress));
    Sleep(100);
    SendInput(1,&inputLeftPress,sizeof (inputLeftPress));
    Sleep(100);
    SendInput(1,&inputLeftRelease,sizeof (inputLeftRelease));
    SendInput(1,&inputWinRelease,sizeof (inputWinRelease));
    SendInput(1,&inputCtrlRelease,sizeof (inputCtrlRelease));
}

void MainWindow::serialportReadyRead()
{
    QByteArray buffer=serial.readAll();
    //qDebug(QString(buffer[0]));
    if (buffer[0] == '1')
    {
        Ctrl_Win_Left();
        qDebug("Case1");
    }
    else if (buffer[0] == '2')
    {
        Ctrl_Win_Right();
        qDebug("Case2");
    }
}

mainwindow.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>600</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget"/>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>800</width>
     <height>25</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

- 下位机

下位机比较平淡,就是读取两个数字接口然后根据结果向串口发送数据。
下面直接附上代码
VirtualMacroKeyboard.ino

//Using 2 simple switches 
//connect to D2 & D3 
const byte Sw1pin = 2; 
const byte Sw2pin = 3;

void setup()
{
    pinMode(Sw1pin, INPUT);
    pinMode(Sw2pin, INPUT);
    Serial.begin(9600);
    delay(1000);
}

void loop()
{
    if (digitalRead(Sw1pin) == HIGH)
    {
        for (;;)
        {
            if (digitalRead(Sw1pin) == LOW)
            {
                Serial.print("1");
                break;
            }
        }
    }
    if (digitalRead(Sw2pin) == HIGH)
    {
        for (;;)
        {
            if (digitalRead(Sw2pin) == LOW)
            {
                Serial.print("2");
                break;
            }
        }
    }
    delay(50);
}

- 展示

上传中…

- 改进

事实上我不想改进这个了…
等我买回来Leonardo,就不用搞这么麻烦了…

相关文章

感谢以下文章的作者!从你们的文章中我学到了很多!

Qt串口相关:QT开发(五十)——QT串口编程基础
修改bootloader法:Arduino uno 折腾笔记-uno 变 键盘
上拉电阻和下拉电阻的用处和区别
leonardo加矩阵键盘

发布了1 篇原创文章 · 获赞 0 · 访问量 20

猜你喜欢

转载自blog.csdn.net/qq_45916440/article/details/104114230