低成本作弊神器?使用ESP32将通义千问AI接入学生计算器(更新按钮支持)

上一篇文章 ←查看上一篇文章内容

上一篇文章中写了思路以及实现代码,但是我忘记了一个重要的问题,那就是如果答案太长了,就这么一小块160*80的屏幕怎么回显全部答案呢?

当然,拍摄也不是手动的,就很无厘头

所以此次增加三个按钮,拍摄 上翻页 下翻页

硬件连接:

  1. ESP32ST7735S 0.96英寸TFT彩屏 的连接方式如下:

    • TFT_MISO -> 无需连接(SPI不需要)
    • TFT_MOSI -> GPIO23
    • TFT_SCLK -> GPIO18
    • TFT_CS -> GPIO5
    • TFT_DC -> GPIO16
    • TFT_RST -> GPIO17
    • 3.3V -> 3.3V
    • GND -> GND
  2. 按钮连接

    • 上翻页按钮:一端连接到GPIO34,另一端连接到GND
    • 下翻页按钮:一端连接到GPIO35,另一端连接到GND
    • 拍摄按钮:一端连接到GPIO32,另一端连接到GND

安装必要库:

  • Adafruit_ST7735:打开Arduino IDE,进入工具 -> 库管理,搜索Adafruit_ST7735并安装。
  • Adafruit_GFX:打开Arduino IDE,进入工具 -> 库管理,搜索Adafruit_GFX并安装。

代码:

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>

// WiFi
const char* ssid = "SSID";
const char* password = "PASSWORD";

// 通义千问API设置
const char* api_url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
const char* api_key = "写自己的";

// 摄像头配置
#define PWDN_GPIO_NUM    32
#define RESET_GPIO_NUM   -1
#define XCLK_GPIO_NUM    0
#define SIOD_GPIO_NUM    26
#define SIOC_GPIO_NUM    27

#define Y9_GPIO_NUM      35
#define Y8_GPIO_NUM      34
#define Y7_GPIO_NUM      39
#define Y6_GPIO_NUM      36
#define Y5_GPIO_NUM      21
#define Y4_GPIO_NUM      19
#define Y3_GPIO_NUM      18
#define Y2_GPIO_NUM      5
#define VSYNC_GPIO_NUM   25
#define HREF_GPIO_NUM    23
#define PCLK_GPIO_NUM    22

// TFT LCD配置
#define TFT_CS 5
#define TFT_DC 16
#define TFT_RST 17
#define TFT_MOSI 23
#define TFT_SCLK 18

// 按钮配置
#define UP_BUTTON_PIN 34
#define DOWN_BUTTON_PIN 35
#define SHOOT_BUTTON_PIN 32
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);
int currentLine = 0;
int maxLines = 10;  // 每页显示的最大行数
String answer = "";

void setup() {
  // 初始化串口通信
  Serial.begin(115200);

  // 初始化TFT LCD
  tft.initR(INITR_BLACKTAB);  // 初始化屏幕
  tft.setRotation(1);  // 设置屏幕方向
  tft.fillScreen(ST7735_BLACK);

  // 初始化摄像头
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_UXGA;  // 合适的分辨率
  config.jpeg_quality = 12;  // JPEG质量
  config.fb_count = 1;

  // 初始化摄像头
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  // 连接到WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  Serial.println("Connected to WiFi");

  // 设置HTTP客户端
  HTTPClient http;
  http.begin(api_url);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("Authorization", "Bearer " + String(api_key));

  // 初始化按钮
  pinMode(UP_BUTTON_PIN, INPUT_PULLUP);
  pinMode(DOWN_BUTTON_PIN, INPUT_PULLUP);
  pinMode(SHOOT_BUTTON_PIN, INPUT_PULLUP);
}

void loop() {
  // 处理按钮输入
  handleButtons();
}

void handleButtons() {
  static unsigned long lastDebounceTimeUp = 0;
  static unsigned long lastDebounceTimeDown = 0;
  static unsigned long lastDebounceTimeShoot = 0;
  static int lastButtonStateUp = HIGH;
  static int lastButtonStateDown = HIGH;
  static int lastButtonStateShoot = HIGH;
  static int debounceDelay = 50;

  int readingUp = digitalRead(UP_BUTTON_PIN);
  int readingDown = digitalRead(DOWN_BUTTON_PIN);
  int readingShoot = digitalRead(SHOOT_BUTTON_PIN);

  if (readingUp != lastButtonStateUp) {
    lastDebounceTimeUp = millis();
  }
  if (readingDown != lastButtonStateDown) {
    lastDebounceTimeDown = millis();
  }
  if (readingShoot != lastButtonStateShoot) {
    lastDebounceTimeShoot = millis();
  }

  if ((millis() - lastDebounceTimeUp) > debounceDelay) {
    if (readingUp != lastButtonStateUp) {
      if (readingUp == LOW) {
        currentLine--;
        if (currentLine < 0) {
          currentLine = 0;
        }
        displayAnswer();
      }
      lastButtonStateUp = readingUp;
    }
  }

  if ((millis() - lastDebounceTimeDown) > debounceDelay) {
    if (readingDown != lastButtonStateDown) {
      if (readingDown == LOW) {
        currentLine++;
        if (currentLine * maxLines >= answer.length()) {
          currentLine = answer.length() / maxLines;
        }
        displayAnswer();
      }
      lastButtonStateDown = readingDown;
    }
  }

  if ((millis() - lastDebounceTimeShoot) > debounceDelay) {
    if (readingShoot != lastButtonStateShoot) {
      if (readingShoot == LOW) {
        takePhotoAndProcess();
      }
      lastButtonStateShoot = readingShoot;
    }
  }
}

void takePhotoAndProcess() {
  // 拍摄照片
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    return;
  }

  // 将照片转换为Base64编码
  size_t encodedLen = base64_encoded_length(fb->len);
  char *encodedData = (char *)malloc(encodedLen + 1);
  base64_encode(encodedData, (const uint8_t *)fb->buf, fb->len);

  // 构造JSON请求体
  String payload = "{\"messages\":[{\"role\":\"user\",\"content\":\"![](data:image/jpeg;base64," + String(encodedData) + ")\"}]}";

  // 发送POST请求
  HTTPClient http;
  http.begin(api_url);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("Authorization", "Bearer " + String(api_key));

  int httpResponseCode = http.POST(payload);
  if (httpResponseCode > 0) {
    // 读取响应
    String response = http.getString();
    Serial.println(response);

    // 解析JSON响应
    DynamicJsonDocument doc(1024);
    DeserializationError error = deserializeJson(doc, response);
    if (!error) {
      answer = doc["choices"][0]["message"]["content"].as<String>();  // 根据实际API返回的字段名称进行调整
      Serial.println("Answer: " + answer);

      // 初始化显示
      currentLine = 0;
      displayAnswer();
    } else {
      Serial.println("Error parsing JSON");
    }
  } else {
    Serial.println("Error on sending POST: " + String(httpResponseCode));
  }

  // 释放资源
  esp_camera_fb_return(fb);
  free(encodedData);
}

void displayAnswer() {
  tft.fillScreen(ST7735_BLACK);
  tft.setTextColor(ST7735_WHITE);
  tft.setTextSize(1);  // 设置字体大小

  int y = 0;
  int lines = 0;
  int startLine = currentLine * maxLines;
  int endLine = min(startLine + maxLines, answer.length());

  for (int i = startLine; i < endLine; i++) {
    if (answer[i] == '\n') {
      lines++;
      y += 8;  // 每行的高度
    } else {
      tft.drawChar(i - startLine, 0, y, answer[i], ST7735_WHITE, ST7735_BLACK, 1);
    }
  }
}

 

详细解释

  • 变量声明

    • lastDebounceTimeUplastDebounceTimeDownlastDebounceTimeShoot:记录上次检测到按钮状态变化的时间。
    • lastButtonStateUplastButtonStateDownlastButtonStateShoot:记录上次的按钮状态。
    • debounceDelay:去抖延迟时间,单位为ms。
  • 读取按钮状态

    • digitalRead(UP_BUTTON_PIN):读取上翻页按钮的状态。
    • digitalRead(DOWN_BUTTON_PIN):读取下翻页按钮的状态。
    • digitalRead(SHOOT_BUTTON_PIN):读取拍摄按钮的状态。
  • 检测状态变化

    • 如果当前读取的按钮状态与上次不同,则更新lastDebounceTime
    • 如果从上次状态变化到现在的时间超过了debounceDelay,则检查按钮状态是否真的发生了变化。
    • 如果按钮状态确实发生了变化且按钮被按下(即读取到LOW),则执行相应的动作。
  • 上翻页按钮

    • 减少currentLine的值,确保不会小于0。
    • 调用displayAnswer()函数更新屏幕显示。
  • 下翻页按钮

    • 增加currentLine的值,确保不会超过答案的总行数。
    • 调用displayAnswer()函数更新屏幕显示。
  • 拍摄按钮
            调用takePhotoAndProcess()函数进行拍摄和处理。

  • 显示答案:

     displayAnswer()函数负责在TFT LCD上显示当前页的答案。

等我做出来完整的我再发布视频教程!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值