zoologist 发表于 2021-3-16 20:56:23

FireBeetle ESP32 制作一个自动输入器

有个朋友问我是否可以帮忙做一个自动输入的工具,具体场景是:他们的图书馆为了避免病毒之类的影响,内网的机器无法使用U盘,这样对于制作一些文档来说非常麻烦,因此询问我能否在对于系统影响最小的情况下编写一个能够自动输入的工具,这样可以在自己的笔记本电脑上准备好文档,需要时直接输入即可。经过研究,我使用 FirBeetle 加 CH55X 来制作了一个自动输入器可以满足他的需求。首先是硬件方案,选择了如下板卡
编号板卡用途
1FireBeetle ESP32作为主控
2FireBeetle萤火虫OLED12864显示屏板上有按钮,用于选择文件;同时OLED用于展示当前的提示信息
3FireBeetle 摄像头及音频扩展板使用这个板子上的 SD 插槽用于读取事先准备好的文件
4自制CH55XShield通过CH55X模拟一个 USB键盘从而实现串口转 USB 的目的。这里可以使用 CH552 也可以使用 CH554
可以看到,上面的方案中自制 CH55X Shield 是最大的难点。首先,绘制电路图如下,左侧是 FIreBeetle 的引脚,它和Shield是通过 IO16/17 相连的,这是ESP32的 Serial2。在 USB 引脚附件有一个跳线 H1,这是用来切换当前供电的。正常情况下是FIreBeetle给 Shield 供电,但是真正使用时,却需要从USB口对 FireBeetle 供电。因此,使用时需要将 H1 短接来给 FIreBeetle 供电。
CH55X 部分,对于这个芯片使用3.3V供电,因此 V33和 VCC 是接在一起的。此外,在 USB+(P3.6) 上有一个按钮和跳线,它们的作用是相同的:短接后经过10K 电阻上拉,这样在上电时可以让 CH55X 进入 BootMode便于更新Firmware。如果没有焊接按钮,可以在上电时用镊子短接跳线位置实现同样的功能。另外,为了调试和日后扩展,预留了其他引脚出来:PCB 设计如下:
3D预览如下:
拿到手的板子如下,我使用的是 CH554T 芯片:
设计 CH55X 代码:#ifndef USER_USB_RAM
#error "This example needs to be compiled with a USER USB setting"
#endif

#include "src/userUsbHidKeyboard/USBHIDKeyboard.h"

void setup() {
USBInit();
Serial0_begin(115200);
}

void loop() {

while (Serial0_available()) {
    char serialChar = Serial0_read();
    Keyboard_write(serialChar);
}
}
可以看到非常简单,就是从 Serial0 接收,然后从 USB 转发出去。具体的下载方法请参考之前的文章【参考1】。特别注意,板子上芯片使用3.3V供电,因此,下载时需要使用如下设置:
上面完成之后就可以开始给 FireBeetle编写代码了。代码流程如下:
1.上电后检查 SD 卡,如果无法识别就停止运行;2.读取 INITDIR 指定的”LABZ”,枚举下面的全部文件,将文件名保存在 FilePool 结构体中。这个结构体中可以保存10个文件(对于 ESP32 来说,内存足够大,能够保存更多的文件,但是相信使用时不会有人想用按键来选择一个个的文件);3.进入 loop 循环,在屏幕上显示当前选中的文件信息,包括文件名,大小和该文件是目录下第几个文件。OLED 上有一个方向键,提供了4个方向加确定,具体是通过下面的多个电阻实现,在代码中通过read_key_analog() 函数来给出当前的选择值
4.为了便于调试,代码中还提供了使用串口的调试方法,输入“a”表示上一个,输入“z”表示下一个,”x”确定;5.在运行时如果收到确认键,就打开 SD 卡上对应名字的文件,然后将内容输出给 CH55X。
完整代码如下:#include "DFRobot_OLED12864.h"
#include "FS.h"
#include "SD_MMC.h"


//设置文件名最大长度(目录名+文件名)
//超过这个长度的文件名会被忽略掉
const int MAXNAMELENGTH=16;
#define ADC_BIT4096
#define ADC_SECTION 5
#define EVERYSHOW 127

//读取这个目录下的内容
char* INITDIR="/LABZ";

const uint8_t pin_analogKey = A0;

//文件名列表
struct {
    int      size;
    char   name;
} FilePool;

// 枚举到的文件数量
int FileCounter=0;

enum enum_key_analog {
key_analog_no,
key_analog_right,
key_analog_center,
key_analog_up,
key_analog_left,
key_analog_down,
} eKey_analog;

const uint8_t I2C_OLED_addr = 0x3c;
const uint8_t pin_character_cs = D5, keyA = D3, keyB = D8;
DFRobot_OLED12864 OLED(I2C_OLED_addr, pin_character_cs);

enum_key_analog read_key_analog(void)
{
int adValue = analogRead(pin_analogKey);
if(adValue > ADC_BIT * (ADC_SECTION * 2 - 1) / (ADC_SECTION * 2)) {
    return key_analog_no;
} else if(adValue > ADC_BIT * (ADC_SECTION * 2 - 3) / (ADC_SECTION * 2)) {
    return key_analog_right;
} else if(adValue > ADC_BIT * (ADC_SECTION * 2 - 5) / (ADC_SECTION * 2)) {
    return key_analog_center;
} else if(adValue > ADC_BIT * (ADC_SECTION * 2 - 7) / (ADC_SECTION * 2)) {
    return key_analog_up;
} else if(adValue > ADC_BIT * (ADC_SECTION * 2 - 9) / (ADC_SECTION * 2)) {
    return key_analog_left;
} else {
    return key_analog_down;
}
}

void setup(){
    Serial.begin(115200);
    Serial2.begin(115200);
    pinMode(keyA, INPUT);
    pinMode(keyB, INPUT);
   
    OLED.init();
    OLED.flipScreenVertically();
    OLED.clear();
    OLED.disStr(0, 0, "自动键盘机");
    OLED.display();

    if(!SD_MMC.begin()){
      Serial.println("Card Mount Failed");
      OLED.disStr(0, 16, "SD挂载失败");
      OLED.display();
      return;
    }
    uint8_t cardType = SD_MMC.cardType();

    if(cardType == CARD_NONE){
      Serial.println("No SD_MMC card attached");
      OLED.disStr(0, 16, "无SD卡");
      OLED.display();      
      return;
    }
      
    //打开目录
    File root = SD_MMC.open(INITDIR);
    if(!root){
      Serial.println("Failed to open directory");
      OLED.disStr(0, 16, "无法读取目录");
      OLED.display();         
      return;
    }
    if(!root.isDirectory()){
      Serial.println("Not a directory");
      OLED.disStr(0, 16, "目录错误");
      OLED.display();
      return;
    }

    // 枚举 INITDIR 目录下的所有文件
    File file = root.openNextFile();
    while(file){
      if(file.isDirectory()==false){
            Serial.print("FILE: ");
            Serial.print(file.name());
            Serial.print("SIZE: ");
            Serial.println(file.size());
            
            // 只记录文件名小于最大长度的文件
            if (strlen(file.name())<=MAXNAMELENGTH+strlen(INITDIR)+1) {
                // 将文件名保存起来
                char *p=(char*) file.name();
                strcpy(FilePool.name, p+strlen(INITDIR)+1);
                FilePool.size=file.size();
                FileCounter++;
            }
      }
      file = root.openNextFile();
    }

   
   if (FileCounter==0) {
      Serial.println("No file in directory!");
      OLED.disStr(0, 16, "目录中无文件");
      OLED.display();
      return ;
    }   

/*
   for (int i=0;i<FileCounter;i++) {
      Serial.println(FilePool.name);
    }
*/

}

// 上一个文件的编号
int LastIndex=0xFF;
// 当前文件编号
int CurrentIndex=0;

void loop(){
    charc=0xFF;
    char Buffer1,Buffer2;
   
    eKey_analog = read_key_analog();

    // 如果当前文件编号和之前的记录不同,说明有文件切换动作
    if (LastIndex!=CurrentIndex) {
      // 串口输出
      Serial.println("****************");
      Serial.println(FilePool.name);
      Serial.println(FilePool.size);
      sprintf(Buffer1, "[%d/%d]", CurrentIndex+1, FileCounter);
      Serial.println(Buffer1);
      Serial.println("****************");

      // 屏幕输出
      OLED.clear();
      OLED.disStr(0, 0, "当前文件");
      OLED.disStr(0, 16, FilePool.name);
      sprintf(Buffer2, "%d bytes", FilePool.size);
      OLED.disStr(0, 32, Buffer2);
      OLED.disStr(0, 48, Buffer1);
      OLED.display();
      LastIndex=CurrentIndex;
      eKey_analog=key_analog_no;
      delay(700);
      }
      
    while (Serial.available() > 0)
    {
      byte k=Serial.read();
      if ((k=='a')||(k=='z')||(k=='x')) {
            c=k;
          }
    }
      if ((c=='a')||(eKey_analog==key_analog_up)) {
                  CurrentIndex++;
                  if (CurrentIndex==FileCounter) {
                        CurrentIndex=0;
                  }
            }
      if ((c=='z')||(eKey_analog==key_analog_down)) {
                  CurrentIndex--;
                  if (CurrentIndex==-1) {
                        CurrentIndex=FileCounter-1;
                  }
            }
      if ((c=='x')||(digitalRead(keyA)==LOW)) {
                  char Buffer;
                  strcpy(Buffer, INITDIR);
                  strcat(Buffer, "/");
                  strcat(Buffer, FilePool.name);
                  Serial.printf("Reading file: %s\n", Buffer);
            
                  File file = SD_MMC.open(Buffer);
                  if(!file){
                      Serial.println("Failed to open file for reading");
                      OLED.clear();
                      OLED.disStr(0, 0, "读取文件失败");
                      OLED.disStr(0, 16,"按B键返回");
                      OLED.display();
                      while (digitalRead(keyB)==HIGH) {}                  
                  } else {
                      Serial.print("Read from file: ");
                      int ByteSend=0;
                      while(file.available()){
                        char v=file.read();
                        //Serial.write(v);
                        Serial2.write(v);
                        delay(20);
                        ByteSend++;   
                        if (ByteSend%EVERYSHOW ==0) {                     
                        OLED.clear();
                        OLED.disStr(0, 0, FilePool.name);
                        sprintf(Buffer1, "[%d/%d]", ByteSend, FilePool.size);
                        OLED.disStr(0, 16,Buffer1);
                        OLED.display(); }                           
                      }
                     
                      OLED.clear();
                      OLED.disStr(0, 0, FilePool.name);
                      sprintf(Buffer1, "[%d/%d]", ByteSend, FilePool.size);
                      OLED.disStr(0, 16,Buffer1);
                      OLED.disStr(0, 32,"发送完成,B键返回");
                      OLED.display();
                      // 强制刷新当前文件名
                      LastIndex=0xFF;
                      while (digitalRead(keyB)==HIGH) {}
                  }
            }
         
}

FireBeetle CH55X 电路图和 PCB:
OLED 的电路图:
参考:
1. https://mc.dfrobot.com.cn/thread-308130-1-1.html 国产主控芯片打造 USB 提醒器





zoologist 发表于 2021-3-16 21:00:26

工作的视频:

https://www.bilibili.com/video/BV1M54y187t1
作品用到的板子:




页: [1]
查看完整版本: FireBeetle ESP32 制作一个自动输入器