语音助手学习四

由于wifi配置和KEY都是写死在程序里,使用不方便,所以本期任务有,通过屏幕设置参数,参数保存在断电不丢失的flash中。

1修改分区(非必要

Arduino IDE增加ESP32flash分区配置选项_arduino ide partition scheme:-CSDN博客

给arduino添加.cpp和.h后,编译不通过,说连接器的问题,搞半天不懂,直接放在主文件下面了

2断电存储

ESP32重要库示例详解(一):EEPROM之Preferences库_esp32 数据保存preferences-CSDN博客使用Arduino开发ESP32(18):使用Preferences保存数据_esp32 preferences-CSDN博客

void preferece_write(void)
{
  Preferences preferences;
  preferences.begin("my-app", false); // 使用“my-app”命名空间,以防止键名冲突
  preferences.putString("ssid_value", ssid);//写入,ssid在键盘设置完数据后赋值
  preferences.end();                        //原文链接:https://blog.csdn.net/idfengming/article/details/138726347  
}
 void preferece_read(void)
{
  Preferences preferences;
  preferences.begin("my-app", false); // 使用“my-app”命名空间,以防止键名冲突    
  ssid = preferences.getString("ssid_value", "no find"); // 获取计数器值,不存在则返回no find
  Serial.printf("Current ssid value: ");  Serial.println(ssid);
  preferences.end();                        //原文链接:https://blog.csdn.net/idfengming/article/details/138726347  
} 

注意

像这个就是错的

3添加设置界面

连接不到wifi时才可以进入设置界面

4,写键盘界面

参考代码不难,改改参数即可实现,整合即可,耗时几天

5总结

很多详细函数没有放,因为有一些bug,在这里只给大家提供思路

历史版本

#include <WiFi.h>
#include "time.h"
#include "sntp.h"
#include <mbedtls/md.h>
#include <base64.h>
#include <Base64_Arturo.h>
#include <ArduinoWebsockets.h>
#include <ArduinoJson.h>
#include <driver/i2s.h>
#include "SPI.h"
#include "TFT_eSPI.h"
#include "U8g2_for_TFT_eSPI.h"
#include <HTTPClient.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Preferences.h>
#include <iostream>
#include <cstdint>
#include <string>
TFT_eSPI tft = TFT_eSPI();  // tft instance
U8g2_for_TFT_eSPI u8f;      // U8g2 font instance
#define FONT u8g2_font_wqy16_t_gb2312
uint16_t x = 0, y = 0;
#define I2S_WS 17
#define I2S_SD 3
#define I2S_SCK 18
#define I2S_PORT_0 I2S_NUM_0
#define SAMPLE_RATE 16000

#define RECORD_TIME_SECONDS 60
#define BUFFER_SIZE (SAMPLE_RATE * RECORD_TIME_SECONDS)
#define BUTTON_PIN 20//应该接的触摸屏输入引脚
#define I2S_LR_RX 46
#define I2S_PORT_1 I2S_NUM_1
#define MAX98357_LRC 47
#define MAX98357_BCLK 48
#define MAX98357_DIN 45
#define CHUNK_SIZE 2048
#define width_my 260

int16_t audioData[2560];
int16_t* pcm_data;  //录音缓存区
uint recordingSize = 0;

// char* psramBuffer = (char*)ps_malloc(512000);
String odl_answer;

String answer_list[10];
uint8_t answer_list_num = 0;
bool answer_ste = 0;

char* ssid = "wan";
char* password = "13";

//讯飞STT 的key
const char* STTAPPID = "6";
//const char* STTAPISecret = "********";
const char* STTAPISecret = "NG";
const char* STTAPIKey = "dec";

//火山引擎(豆包)的key
const char* apiKey = "8b06";
const char* endpointId = "ep";
const String doubao_system = "你是一个可以聊天的朋友,回答问题比较简洁。";  //定义豆包的人设
//const String doubao_system = "你是一个旅行达人,回答问题比较简洁。";
//const String doubao_system = "你是一个生活助理,回答问题比较简洁。";

//讯飞 语音合成kye
const char* TTSAPPID = "6b1";
const char* TTSAPISecret = "NGM";
const char* TTSAPIKey = "de";

using namespace websockets;
WebsocketsClient client;
WebsocketsClient clientTTS;

const char* ntpServer1 = "ntp.org";
const char* ntpServer2 = "ntp.ntsc.ac.cn";
const long gmtOffset_sec = 3600;
const int daylightOffset_sec = 3600;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
bool timeste = 0;
String stttext = "";
bool sttste = 0;
String getDateTime(void);
void setup_ntp_client(void);
void status(const char *msg);
void drawKeypad(char keyLabel[][5]);
void set_key_and_wifi(void);
void setup() {
  Serial.begin(115200);//串口初始化
  pinMode(BUTTON_PIN, INPUT);应该初始化触摸屏输入引脚
  tft.begin();
  uint16_t calData[5] = { 400, 3553, 204, 3617, 7 };//这些元素分别表示触摸屏的校准参数。
  tft.setTouch(calData);//这些校准参数应用到触摸屏上。

  tft.setRotation(3);//设置TFT显示屏的旋转角度为3(逆时针旋转90度)
  tft.fillScreen(TFT_BLACK);
  u8f.begin(tft);  // connect u8g2 procedures to TFT_eSPI//将u8g2库的绘图函数与TFT_eSPI库连接起来,以便在TFT显示屏上绘制图形。
  u8f.setFont(FONT);//设置字体为预定义的FONT变量所指定的字体。此句加载了字库,占据了非常大的程序空间
  u8f.setForegroundColor(TFT_WHITE);  // apply color
  u8f.setBackgroundColor(TFT_BLUE);
  tft.fillRoundRect(0, 0, width_my, 40, 8, TFT_BLUE);//画上面的边框
  u8f.setCursor(10, 30);//打印坐标,配合下一句使用
  u8f.print("WiFi连接中...");  
  Preferences preferences;
  preferences.begin("my-app", false); // 使用“my-app”命名空间,以防止键名冲突
  
  Serial.printf("Connecting to %s ", ssid);
  preferece_read();//读取内存值
  status(ssid); 
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {   
    x=y=0;
    preferece_read();//读取内存值
    status(ssid); 
    Serial.print(".");
    u8f.setCursor(280, 30);//打印坐标,配合下一句使用
    u8f.print("设置");  
    u8f.setCursor(0, 30);//打印坐标,配合下一句使用
    u8f.print("wifi设置错误,请重试");  
    delay(900);
    tft.getTouch(&x, &y);
    Serial.printf("x:%u,y:%u\n", x,y);
    if (320-x > 260 and 240-y > 0 and 240-y < 30)  
    { 
      set_key_and_wifi();
      preferece_write();
      WiFi.begin(ssid, password);
      tft.fillScreen(TFT_BLACK); 
    }
  }
  Serial.println(" CONNECTED");
  tft.fillRoundRect(0, 0, width_my, 40, 8, TFT_BLUE);
  u8f.setCursor(10, 30);
  u8f.print("WiFi连接成功");
  delay(1000);
  tft.fillRoundRect(0, 0, width_my, 40, 8, TFT_BLUE);
  u8f.setCursor(10, 30);
  u8f.print("NTP网络对时中...");
  setup_ntp_client();//
  getDateTime();//更新时间

  tft.fillRoundRect(0, 0, width_my, 40, 8, TFT_BLUE);
  u8f.setCursor(10, 30);
  u8f.print("对时成功");
  delay(1000);
  tft.fillRoundRect(0, 0, width_my, 40, 8, TFT_BLUE);
  u8f.setCursor(10, 30);
  u8f.print("准备录音");

  Serial.println("Setup I2S ...");
  i2s_install();
  i2s_setpin();
  esp_err_t err = i2s_start(I2S_PORT_0);//应该固定IIC引脚
  if (err != ESP_OK) {
    Serial.printf("I2S start failed (I2S_PORT_0): %d\n", err);
    while (true)
      ;
  }

  // run callback when messages are received下面都是回调函数
  client.onMessage([&](WebsocketsMessage message) {  //STT ws连接的回调函数,相当于中断
    Serial.print("Got Message: ");
    Serial.println(message.data());
    JsonDocument doc;// 获取语音识别结果中的单词列表
    DeserializationError error = deserializeJson(doc, message.data());//提取列表中message字段的信息
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.f_str());
      return;
    }
    JsonArray ws = doc["data"]["result"]["ws"];// 获取语音识别结果中的单词列表
    for (JsonObject word : ws) {// 遍历单词列表,将每个单词拼接到stttext字符串中
      int bg = word["bg"];
      const char* w = word["cw"][0]["w"];
      stttext += w;
    }
    if (doc["data"]["status"] == 2) {  //收到结束标志
      sttste = 1;    // 设置状态为1,表示语音识别完成
      Serial.print("stttext");
      Serial.println(stttext);
    }
  });

  clientTTS.onMessage([&](WebsocketsMessage message) {  //讯飞TTS的 wx连接回调函数
    //Serial.print("Got Message: ");

    DynamicJsonDocument responseJson(51200);//最大容量为51200字节。
    DeserializationError error = deserializeJson(responseJson, message.data());
    const char* response = responseJson["data"]["audio"].as<String>().c_str();
    int response_len = responseJson["data"]["audio"].as<String>().length();//提取音频长度
    //Serial.printf("lan: %d  \n", response_len);

    //分段获取PCM音频数据并输出到I2S上
    for (int i = 0; i < response_len; i += CHUNK_SIZE) {
      int remaining = min(CHUNK_SIZE, response_len);                                       // 计算剩余数据长度
      char chunk[CHUNK_SIZE];                                                              // 创建一个缓冲区来存储读取的数据
      int decoded_length = Base64_Arturo.decode(chunk, (char*)(response + i), remaining);  // 从response中解码数据到chunk
      size_t bytes_written = 0;
      i2s_write(I2S_PORT_1, chunk, decoded_length, &bytes_written, portMAX_DELAY);
    }

    if (responseJson["data"]["status"].as<int>() == 2) {  //收到结束标志
      Serial.println("Playing complete.");
      delay(200);
      i2s_zero_dma_buffer(I2S_PORT_1);  // 清空I2S DMA缓冲区
    }
  });
}

bool rste = 0;
bool wste = 0;
void loop() {
  if (digitalRead(BUTTON_PIN) == LOW) {
    delay(20);
    if (digitalRead(BUTTON_PIN) == LOW) {
      tft.getTouch(&x, &y);
      if (x > 260 and y > 0 and y < 60)  
      {set_key_and_wifi();}           
      stttext = "";
      Serial.println("Recording...");
      tft.fillRoundRect(0, 0, 320, 40, 8, TFT_BLUE);
      u8f.setCursor(10, 30);
      u8f.print("录音中...");
      tft.fillRoundRect(0, 40, 320, 200, 8, TFT_BLACK);
      tft.fillCircle(285,120,25,TFT_RED);
      tft.drawLine(270,105,300,135,TFT_WHITE);
      tft.drawLine(300,105,270,135,TFT_WHITE);
      size_t bytes_read = 0;
      recordingSize = 0;
      pcm_data = reinterpret_cast<int16_t*>(ps_malloc(BUFFER_SIZE * 2));//分配内存
      if (!pcm_data) {
        Serial.println("Failed to allocate memory for pcm_data");
      }
      x = 0, y = 0;
      while (digitalRead(BUTTON_PIN) == LOW) {  //开始循环录音,将录制结果保存在pcm_data中
//从I2S端口0读取音频数据。audioData是一个缓冲区,用于存储读取到的数据。sizeof(audioData)表示缓冲区的大小。&bytes_read是一个指向变量的指针,用于存储实际读取到的字节数。portMAX_DELAY表示无限等待直到有数据可读。
        esp_err_t result = i2s_read(I2S_PORT_0, &audioData, sizeof(audioData), &bytes_read, portMAX_DELAY);
        memcpy(pcm_data + recordingSize, audioData, bytes_read);//将读取到的音频数据复制到pcm_data数组中
        recordingSize += bytes_read / 2;//更新已录制音频数据的大小。int16_t是两个字节,所以/2      
//        tft.getTouch(&x, &y);//是否要下移一行?
      }
      tft.getTouch(&x, &y);
      x = 320 - x;
      y = 240 - y;
      Serial.printf("x,y = %d , %d \n", x, y);

      if (x > 260 and y > 95 and y < 145) {//松开位置在小圈内
        tft.fillRoundRect(0, 0, 320, 40, 8, TFT_BLUE);
        tft.fillRoundRect(0, 40, 320, 200, 8, TFT_BLACK);
        u8f.setCursor(10, 30);
        u8f.print("取消录音");
        delay(1000);
        tft.fillRoundRect(0, 0, 320, 40, 8, TFT_BLUE);
        u8f.setCursor(10, 30);
        u8f.print("准备录音");
      } else {
        Serial.printf("Total bytes read: %d\n", recordingSize);
        Serial.println("Recording complete.");
        tft.fillRoundRect(0, 0, 320, 40, 8, TFT_BLUE);
        u8f.setCursor(10, 30);
        u8f.print("录音结束,发送音频...");
        STTsend();  //STT请求开始,这里发送语音去shibie//向讯飞STT发送音频数据
      }
      free(pcm_data);
    }
  }

  if (client.available()) {//在许多网络库和框架中,客户端对象会使用事件驱动的方式来处理网络通信。这意味着当有数据到达时,或者连接状态发生变化时,客户端对象会触发相应的事件。通过调用client.poll()函数,客户端可以轮询这些事件并执行相应的操作。
    client.poll();
  }
  if (clientTTS.available()) {
    clientTTS.poll();
  }
  if (sttste) {  //接收到STT数据,进行下一步处理???????sttste为回调函数中收到语音识别结果置1(中断)
    Serial.println(stttext);
    tft.fillRoundRect(0, 0, 320, 40, 8, TFT_BLUE);
    u8f.setCursor(10, 30);
    u8f.print("接收识别结果...");
    tft.fillRoundRect(0, 40, 320, 200, 8, TFT_BLACK);
    printtext(stttext, 50);
    delay(100);

    //保存最近的5次对话到列表中,这里应该保存问题
    stttext.replace("\n", "");
    answer_list[answer_list_num] = stttext;
    answer_list_num++;
    if (answer_list_num > 9) {
      for (int i = 0; i < 9; i++) {
        answer_list[i] = answer_list[i + 1];
      }
      answer_list[9] = "";
      answer_list_num = 9;
    }

    for (int i = 0; i < answer_list_num + 1; i++) {//每次循环都打印5个对话?
      Serial.print("answer_list_num: ");
      Serial.println(i);
      Serial.print("answer_list: ");
      Serial.println(answer_list[i]);
    }

    String answer = "";

    //向豆包发送请求
    while (answer == "" || answer == "Error") {
      answer = POSTtoDoubao(answer_list, answer_list_num);//接收一个字符串数组answerlist和一个整数listnum作为参数,是上面的识别结果
      if (answer == "Error") {
        Serial.println("doupao POST出错重新提交");
      }
    }

    //保存最近的5次对话到列表中,这里应该保存回答
    answer.replace("\n", "");
    answer_list[answer_list_num] = answer;
    answer_list_num++;
    if (answer_list_num > 9) {
      for (int i = 0; i < 9; i++) {
        answer_list[i] = answer_list[i + 1];
      }
      answer_list[9] = "";
      answer_list_num = 9;
    }

    printtext(answer, 120);

    //向TTS发送请求
    if (answer != NULL) {//如果有问题
      postTTS(answer);//向讯飞TTS发送请求
    } else {
      Serial.println("回答内容为空,取消TTS发送。");
    }

    Serial.println();
    tft.fillRoundRect(0, 0, 320, 40, 8, TFT_BLUE);
    u8f.setBackgroundColor(TFT_BLUE);
    u8f.setForegroundColor(TFT_WHITE);
    u8f.setCursor(10, 30);
    u8f.print("准备录音");
    sttste = 0;
  }
  delay(50);
}
void setup_ntp_client() {  //这段代码是用于设置NTP客户端的。
  timeClient.begin();//它调用timeClient.begin()来启动NTP客户端。
  // 设置时区
  // GMT +1 = 3600
  // GMT +8 = 28800
  // GMT -1 = -3600
  // GMT 0 = 0
  timeClient.setTimeOffset(0);//设置时区偏移量
}
String getDateTime() {
  // 请求网络时间
  timeClient.update();

  unsigned long epochTime = timeClient.getEpochTime();
  Serial.print("Epoch Time: ");
  Serial.println(epochTime);

  String timeString = unixTimeToGMTString(epochTime);

  // 打印结果
  Serial.println(timeString);
  return timeString;
}

String unixTimeToGMTString(time_t unixTime) {
  char buffer[80];
  struct tm timeinfo;
  gmtime_r(&unixTime, &timeinfo);
  strftime(buffer, sizeof(buffer), "%a, %d %b %Y %H:%M:%S GMT", &timeinfo);
  return String(buffer);
}

void i2s_install() {
  const i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = i2s_bits_per_sample_t(16),
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,  // default interrupt priority
    .dma_buf_count = 8,
    .dma_buf_len = 1024,
    .use_apll = false
  };

  esp_err_t err = i2s_driver_install(I2S_PORT_0, &i2s_config, 0, NULL);
  if (err != ESP_OK) {
    Serial.printf("I2S driver install failed (I2S_PORT_0): %d\n", err);
    while (true)
      ;
  }
  i2s_config_t i2sOut_config = {
    .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = i2s_bits_per_sample_t(16),
    .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 8,
    .dma_buf_len = 1024
  };

  err = i2s_driver_install(I2S_PORT_1, &i2sOut_config, 0, NULL);
  if (err != ESP_OK) {
    Serial.printf("I2S driver install failed (I2S_PORT_1): %d\n", err);
    while (true)
      ;
  }
}

void i2s_setpin() {
  const i2s_pin_config_t pin_config = {
    .bck_io_num = I2S_SCK,
    .ws_io_num = I2S_WS,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = I2S_SD
  };

  esp_err_t err = i2s_set_pin(I2S_PORT_0, &pin_config);
  if (err != ESP_OK) {
    Serial.printf("I2S set pin failed (I2S_PORT_0): %d\n", err);
    while (true)
      ;
  }

  const i2s_pin_config_t i2sOut_pin_config = {
    .bck_io_num = MAX98357_BCLK,
    .ws_io_num = MAX98357_LRC,
    .data_out_num = MAX98357_DIN,
    .data_in_num = -1
  };

  err = i2s_set_pin(I2S_PORT_1, &i2sOut_pin_config);
  if (err != ESP_OK) {
    Serial.printf("I2S set pin failed (I2S_PORT_1): %d\n", err);
    while (true)
      ;
  }
}
//向讯飞STT发送音频数据
void STTsend() {
  uint8_t status = 0;
  int dataSize = 1280 * 8;
  int audioDataSize = recordingSize * 2;
  uint lan = (audioDataSize) / dataSize;
  uint lan_end = (audioDataSize) % dataSize;
  if (lan_end > 0) {
    lan++;
  }

  //Serial.printf("byteDatasize: %d , lan: %d , lan_end: %d \n", audioDataSize, lan, lan_end);
  String host_url = XF_wsUrl(STTAPISecret, STTAPIKey, "/v2/iat", "ws-api.xfyun.cn");
  Serial.println("Connecting to server.");
  bool connected = client.connect(host_url);
  if (connected) {
    Serial.println("Connected!");
  } else {
    Serial.println("Not Connected!");
  }
  //分段向STT发送PCM音频数据
  for (int i = 0; i < lan; i++) {

    if (i == (lan - 1)) {
      status = 2;
    }
    if (status == 0) {
      String input = "{";
      input += "\"common\":{ \"app_id\":\"6b\" },";
      input += "\"business\":{\"domain\": \"iat\", \"language\": \"zh_cn\", \"accent\": \"mandarin\", \"vinfo\":1,\"vad_eos\":10000},";
      input += "\"data\":{\"status\": 0, \"format\": \"audio/L16;rate=16000\",\"encoding\": \"raw\",\"audio\":\"";
      String base64audioString = base64::encode((uint8_t*)pcm_data, dataSize);
      input += base64audioString;
      input += "\"}}";
      Serial.printf("input: %d , status: %d \n", i, status);
      client.send(input);
      status = 1;
    } else if (status == 1) {
      String input = "{";
      input += "\"data\":{\"status\": 1, \"format\": \"audio/L16;rate=16000\",\"encoding\": \"raw\",\"audio\":\"";
      String base64audioString = base64::encode((uint8_t*)pcm_data + (i * dataSize), dataSize);
      input += base64audioString;
      input += "\"}}";
      //Serial.printf("input: %d , status: %d \n", i, status);
      client.send(input);
    } else if (status == 2) {
      if (lan_end == 0) {
        String input = "{";
        input += "\"data\":{\"status\": 2, \"format\": \"audio/L16;rate=16000\",\"encoding\": \"raw\",\"audio\":\"";
        String base64audioString = base64::encode((uint8_t*)pcm_data + (i * dataSize), dataSize);
        input += base64audioString;
        input += "\"}}";
        Serial.printf("input: %d , status: %d \n", i, status);
        client.send(input);
      }
      if (lan_end > 0) {
        String input = "{";
        input += "\"data\":{\"status\": 2, \"format\": \"audio/L16;rate=16000\",\"encoding\": \"raw\",\"audio\":\"";

        String base64audioString = base64::encode((uint8_t*)pcm_data + (i * dataSize), lan_end);

        input += base64audioString;
        input += "\"}}";
        Serial.printf("input: %d , status: %d \n", i, status);
        client.send(input);
      }
    }
    delay(30);
  }
}

//处理url格式
String formatDateForURL(String dateString) {
  // 替换空格为 "+"
  dateString.replace(" ", "+");
  dateString.replace(",", "%2C");
  dateString.replace(":", "%3A");
  return dateString;
}

//构造讯飞ws连接url
String XF_wsUrl(const char* Secret, const char* Key, String request, String host) {
  String timeString = getDateTime();
  String signature_origin = "host: " + host;
  signature_origin += "\n";
  signature_origin += "date: ";
  signature_origin += timeString;
  signature_origin += "\n";
  signature_origin += "GET " + request + " HTTP/1.1";
  // Serial.println("\nsignature_origin result:");
  // Serial.println(signature_origin);

  // 使用 mbedtls 计算 HMAC-SHA256
  unsigned char hmacResult[32];  // SHA256 产生的哈希结果长度为 32 字节
  mbedtls_md_context_t ctx;
  mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
  mbedtls_md_init(&ctx);
  mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1);  // 1 表示 HMAC
  mbedtls_md_hmac_starts(&ctx, (const unsigned char*)Secret, strlen(Secret));
  mbedtls_md_hmac_update(&ctx, (const unsigned char*)signature_origin.c_str(), signature_origin.length());
  mbedtls_md_hmac_finish(&ctx, hmacResult);
  mbedtls_md_free(&ctx);
  //打印签名结果
  // Serial.println("HMAC-SHA256 result:");
  // for (int i = 0; i < 32; i++) {
  //   Serial.printf("%02x", hmacResult[i]);
  // }

  // 对结果进行 Base64 编码
  String base64Result = base64::encode(hmacResult, 32);
  //打印 Base64 编码结果
  // Serial.println("\nBase64 encoded result:");
  // Serial.println(base64Result);

  String authorization_origin = "api_key=\"";
  authorization_origin += Key;
  authorization_origin += "\", algorithm=\"hmac-sha256\", headers=\"host date request-line\", signature=\"";
  authorization_origin += base64Result;
  authorization_origin += "\"";
  // Serial.println("\nauthorization_origin encoded result:");
  // Serial.println(authorization_origin);

  String authorization = base64::encode(authorization_origin);
  // Serial.println("\nauthorization encoded result:");
  // Serial.println(authorization);

  String url = "ws://" + host + request;
  url += "?authorization=";
  url += authorization;
  url += "&date=";
  url += formatDateForURL(timeString);
  url += "&host=" + host;
  // Serial.println("\nurl encoded result:");
  // Serial.println(url);
  return url;
}

//向豆包发送请求
String POSTtoDoubao(String* answerlist, int listnum) {//接收一个字符串数组answerlist和一个整数listnum作为参数
  Serial.println("POSTtoDoubao..");
  String answer;

  HTTPClient http;
  http.begin("https://ark.cn-beijing.volces.com/api/v3/chat/completions");
  http.addHeader("Content-Type", "application/json");
  http.addHeader("Authorization", "Bearer " + String(apiKey));

  DynamicJsonDocument requestJson(5120);
  requestJson["model"] = endpointId;
  JsonArray list = requestJson.createNestedArray("messages");

  JsonObject item = list.createNestedObject();
  item["role"] = "system";
  item["content"] = doubao_system;

  for (int i = 0; i < listnum; i += 2) {
    item = list.createNestedObject();
    item["role"] = "user";
    item["content"] = answerlist[i];
    Serial.print("answer user: ");
    Serial.println(answerlist[i]);
    if (listnum > 1 and i != listnum - 1) {
      if (answerlist[i + 1] != "") {
        item = list.createNestedObject();
        item["role"] = "assistant";
        item["content"] = answerlist[i + 1];
      }
      Serial.print("answer assistant: ");
      Serial.println(answerlist[i + 1]);
    }
  }

  requestJson["stream"] = false;
  String requestBody;
  serializeJson(requestJson, requestBody);
  Serial.print("payload: ");
  Serial.println(requestBody);

  int httpResponseCode = http.POST(requestBody);

  if (httpResponseCode > 0) {
    String response = http.getString();
    Serial.println("HTTP Response Code: " + String(httpResponseCode));
    Serial.println("Response: " + response);
    DynamicJsonDocument doc(1024);

    // 处理结果 非流试 \"stream\": false}";
    deserializeJson(doc, response);
    String content = doc["choices"][0]["message"]["content"];
    Serial.println("Doubao Response:");
    Serial.println(content);
    answer = content;
  } else {
    Serial.println("Error on HTTP request");
    return answer = "Error";
    http.end();
  }

  http.end();
  return answer;
}

void printtext(String text, uint16_t h) {//h是显示的高度
  u8f.setBackgroundColor(TFT_BLACK);//设置背景色为黑色
  u8f.setForegroundColor(TFT_WHITE);//前景色为白色
  uint length = text.length();
  uint lan = 0;
  uint n = 0;
  for (int i = 0; i < length; i++) {
    unsigned char firstByte = text[i];

    // 检查是否是多字节字符的开始
    if ((firstByte & 0x80) != 0) {
      // 对于UTF-8编码的汉字,第一个字节通常以1110开头
      if ((firstByte >= 0xE0) && (firstByte <= 0xEF)) {
        // 读取接下来的两个字节来确保是完整的UTF-8汉字字符

        unsigned char secondByte = text[++i];
        unsigned char thirdByte = text[++i];

        if ((secondByte >= 0x80) && (secondByte <= 0xBF) && (thirdByte >= 0x80) && (thirdByte <= 0xBF)) {

          u8f.setCursor(3 + n * 8, h + 20 + (lan * 20));
          u8f.print(text.substring(i - 2, i + 1));
          n = n + 2;
          if (n > 36) {
            lan++;
            n = 0;
          }
          //}
        }
      }
    } else {
      u8f.setCursor(3 + n * 8, h + 20 + (lan * 20));
      u8f.print(text[i]);
      n++;
      if (n > 36) {
        lan++;
        n = 0;
      }
    }
  }
}

char* generateUUID() {
  static char uuid_str[37];
  uint32_t uuid_part1 = random(2147483647);
  uint32_t uuid_part2 = random(2147483647);
  sprintf(uuid_str, "%08lX-%04lX-%04lX-%04lX-%08lX",
          (uuid_part1 >> 16) & 0xFFFFFFFF,
          (uuid_part1 >> 16) & 0xFFFF,
          (uuid_part1 >> 0) & 0xFFFF,
          (uuid_part2 >> 16) & 0xFFFF,
          (uuid_part2 >> 0) & 0xFFFFFFFF);
  return uuid_str;
}

//向讯飞TTS发送请求
void postTTS(String texttts) {
  String TTSurl = XF_wsUrl(TTSAPISecret, TTSAPIKey, "/v2/tts", "ws-api.xfyun.cn");
  bool connected = clientTTS.connect(TTSurl);
  if (connected) {
    Serial.println("Connected!");
  } else {
    Serial.println("Not Connected!");
  }

  String TTStextbase64 = base64::encode(texttts);
  DynamicJsonDocument requestJson(51200);
  requestJson["common"]["app_id"] = TTSAPPID;
  requestJson["business"]["aue"] = "raw";
  requestJson["business"]["vcn"] = "xiaoyan";
  requestJson["business"]["pitch"] = 50;
  requestJson["business"]["speed"] = 50;
  requestJson["business"]["tte"] = "UTF8";
  requestJson["business"]["auf"] = "audio/L16;rate=16000";
  requestJson["data"]["status"] = 2;
  requestJson["data"]["text"] = TTStextbase64;

  String payload;
  serializeJson(requestJson, payload);
  // Serial.print("payload: ");
  // Serial.println(payload);
  clientTTS.send(payload);
}
///
/

// Keypad start position, key sizes and spacing键盘起始位置、按键大小和间距
#define KEY_X 30 // Centre of key
#define KEY_Y 96
#define KEY_W 45// Width and height
#define KEY_H 30
#define KEY_SPACING_X 2 // X and Y gap
#define KEY_SPACING_Y 2
#define KEY_TEXTSIZE 1   // Font size multiplier
//定义一个模式,0是数字,1是小写字母,2是大写字母
uint8_t mode_key=0; 
// Using two fonts since numbers are nice when bold使用两种字体,因为数字粗体时很好看
#define LABEL1_FONT &FreeSansOblique12pt7b // Key label font 1
#define LABEL2_FONT &FreeSansBold12pt7b    // Key label font 2

// Numeric display box size and location数字显示框大小和位置
#define DISP_X 1
#define DISP_Y 15
#define DISP_W 315
#define DISP_H 45
#define DISP_TSIZE 3
#define DISP_TCOLOR TFT_CYAN

// Number length, buffer for storing it and character index数字长度、存储缓冲区和字符索引
#define NUM_LEN 36
char numberBuffer[NUM_LEN + 1] = "";
uint8_t numberIndex = 0;

// We have a status line for messages,我们有一个状态行用于消息和字符索引
#define STATUS_X 120 // Centred on this
#define STATUS_Y 5

// Create 15 keys for the keypad
char keyLabel[30][5] = {"e-E","n-e", "Del", "Send", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "0", "#","-" ,"","","","","","","","","","","","",""};
char keyLabel_e[30][5] = {"e-E","n-e", "Del", "Send", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"};
char keyLabel_E[30][5] = {"e-E","n-e", "Del", "Send", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
uint16_t keyColor[30] = {TFT_RED,TFT_RED, TFT_DARKGREY, TFT_DARKGREEN,TFT_BLUE,
                         TFT_BLUE, TFT_BLUE, TFT_BLUE,TFT_BLUE,TFT_BLUE,
                         TFT_BLUE, TFT_BLUE, TFT_BLUE,TFT_BLUE,TFT_BLUE,
                         TFT_BLUE, TFT_BLUE, TFT_BLUE,TFT_BLUE,TFT_BLUE,
                         TFT_BLUE, TFT_BLUE, TFT_BLUE,TFT_BLUE,TFT_BLUE,
                         TFT_BLUE, TFT_BLUE, TFT_BLUE,TFT_BLUE,TFT_BLUE,
                        };
//选择四个参数
char keylab[8][9] = {"wifi-id","wifi-key","stt-id","stt-key", "doub-id", "doub-key","tts-id","tts-key"};
uint16_t keylabcol[8] = {TFT_BLUE,TFT_BLUE, TFT_BLUE, TFT_BLUE,TFT_BLUE,TFT_BLUE, TFT_BLUE, TFT_BLUE};
// Invoke the TFT_eSPI button class and create all the button objects调用TFT_eSPI按钮类并创建所有按钮对象
TFT_eSPI_Button key[30];

char* key_set(void) {
  drawKeypad(keyLabel);
  while(1){
  uint16_t t_x = 0, t_y = 0; // To store the touch coordinates
  // Pressed will be set true is there is a valid touch on the screen如果屏幕上有有效的触摸,则Pressed将设置为true
  bool pressed = tft.getTouch(&t_x, &t_y);
  t_x=320-t_x;
  t_y=240-t_y;
  // / Check if any key coordinate boxes contain the touch coordinates检查是否有任何按键坐标框包含触摸坐标
  for (uint8_t b = 0; b < 30; b++) {
    if (pressed && key[b].contains(t_x, t_y)) {
      key[b].press(true);  // tell the button it is pressed
    } else {
      key[b].press(false);  // tell the button it is NOT pressed
    }
  }
  // Check if any key has changed state检查是否有任何钥匙的状态发生了变化
  for (uint8_t b = 0; b < 30; b++) {
    if (b < 4) tft.setFreeFont(LABEL1_FONT);//设置字体
    else tft.setFreeFont(LABEL2_FONT);
    if (key[b].justReleased()) key[b].drawButton();     // draw normal绘制正常色,检查按键是否刚刚被释放。如果该方法返回true,则执行下一行代码。
    if (key[b].justPressed()) {
      key[b].drawButton(true);  // draw invert,绘制反色
      // if a numberpad button, append the relevant # to the numberBuffer
      if (b >= 4&&mode_key==0) {
        if (numberIndex < NUM_LEN) {//限幅NUM_LEN个
          numberBuffer[numberIndex] = keyLabel[b][0];
          numberIndex++;
          numberBuffer[numberIndex] = 0; // zero terminate
        }
        status(""); // Clear the old status,消除状态栏
      }
      else if (b >= 4&&mode_key==1) {
        if (numberIndex < NUM_LEN) {//限幅NUM_LEN个
          numberBuffer[numberIndex] = keyLabel_e[b][0];
          numberIndex++;
          numberBuffer[numberIndex] = 0; // zero terminate
        }
        status(""); // Clear the old status,消除状态栏
      }
      else if(b >= 4&&mode_key==3) {
        if (numberIndex < NUM_LEN) {//限幅NUM_LEN个
          numberBuffer[numberIndex] = keyLabel_E[b][0];
          numberIndex++;
          numberBuffer[numberIndex] = 0; // zero terminate
        }
        status(""); // Clear the old status,消除状态栏
      }

      // Del button, so delete last char
      if (b == 2) {status("del one number");
        numberBuffer[numberIndex] = 0;
        if (numberIndex > 0) {
          numberIndex--;
          numberBuffer[numberIndex] = 0;//' ';
        }
        status(""); // Clear the old status
      }
      if (b == 3) {
        status("save");        
//        Serial.println(numberBuffer);
        tft.fillScreen(TFT_BLACK);  
        return numberBuffer;
      }
      if (b == 0) {
        status("e_E");
        if(mode_key==0||mode_key==3){mode_key=1;drawKeypad(keyLabel_e);}
        else {mode_key=3;drawKeypad(keyLabel_E);}
      }
      if (b == 1) {
        status("n_e");
        if(mode_key==0)
        {mode_key=1;drawKeypad(keyLabel_e);}
        else {mode_key=0;drawKeypad(keyLabel);}
      }
      // 更新数字显示字段
      tft.setTextColor(DISP_TCOLOR);     // Set the font colour
      tft.setTextDatum(TL_DATUM);        // Use top left corner as text coord datum使用左上角作为文本坐标基准=     
      if(strlen(numberBuffer)>10){      
        tft.setTextFont(0);
        tft.setTextSize(2);}
      else {tft.setFreeFont(&FreeSans18pt7b);}  // Choose a nicefont that fits box选择一件适合盒子的好衣服
      // Draw the string, the value returned is the width in pixels绘制字符串,返回的值是宽度(像素)
      int xwidth = tft.drawString(numberBuffer, DISP_X + 4, DISP_Y + 12);
      // Now cover up the rest of the line up by drawing a black rectangle.  No flicker this way现在,通过绘制一个黑色矩形来覆盖其余的线条。这样不会闪烁
      // but it will not work with italic or oblique fonts due to character overlap.但由于字符重叠,它不适用于斜体或斜字体。
      tft.fillRect(DISP_X + 4 + xwidth, DISP_Y + 1, DISP_W - xwidth - 5, DISP_H - 2, TFT_BLACK);
      delay(10); // UI debouncing
    }
  }
}}
void drawKeypad(char keyLabel[][5])
{
  // Draw the keys
  for (uint8_t row = 0; row < 5; row++) {
    for (uint8_t col = 0; col < 6; col++) {
      uint8_t b = col + row* 6;      
      tft.setFreeFont(LABEL2_FONT);
      key[b].initButton(&tft, KEY_X + col * (KEY_W + KEY_SPACING_X),
                        KEY_Y + row * (KEY_H + KEY_SPACING_Y), // x, y, w, h, outline, fill, text
                        KEY_W, KEY_H, TFT_WHITE, keyColor[b], TFT_WHITE,
                        keyLabel[b], KEY_TEXTSIZE);
      key[b].drawButton();
    }
  }
}

// Print something in the mini status bar
void status(const char *msg) {
  tft.setTextPadding(240);
  //tft.setCursor(STATUS_X, STATUS_Y);
  tft.setTextColor(TFT_WHITE, TFT_DARKGREY);
  tft.setTextFont(0);
  tft.setTextDatum(TC_DATUM);
  tft.setTextSize(1);
  tft.drawString(msg, STATUS_X, STATUS_Y);
}

#define KEY2_X 80 // Centre of key
#define KEY2_Y 80
#define KEY2_W 110// Width and height
#define KEY2_H 30
#define KEY2_SPACING_X 20 // X and Y gap
#define KEY2_SPACING_Y 10
TFT_eSPI_Button key2[8];
void set_key_and_wifi()
{ 
  char flag=0;
  // Clear the screen
  tft.fillScreen(TFT_BLACK);
  // 显示按键
  for (uint8_t row = 0; row < 4; row++) {
    for (uint8_t col = 0; col < 2; col++) {
      uint8_t b = col + row* 2;
      
      tft.setFreeFont(LABEL2_FONT);
      key[b].initButton(&tft, KEY2_X + col * (KEY2_W + KEY2_SPACING_X),
                        KEY2_Y + row * (KEY2_H + KEY2_SPACING_Y), // x, y, w, h, outline, fill, text
                        KEY2_W, KEY2_H, TFT_WHITE, keylabcol[b], TFT_WHITE,
                        keylab[b], KEY_TEXTSIZE);
      key[b].drawButton();
    }
  }
 delay(200);
while(1)
{ uint16_t t_x = 0, t_y = 0; // To store the touch coordinates
  // 如果屏幕上有有效的触摸,则Pressed将设置为true
  bool pressed = tft.getTouch(&t_x, &t_y);
  t_x=320-t_x;
  t_y=240-t_y;
  if (t_x > 260 and t_y > 0 and t_y < 60)  /返回
  {Serial.printf("im in back");break;}  /返回
  // 检查是否有任何按键坐标框包含触摸坐标
  for (uint8_t b = 0; b < 8; b++) {
    if (pressed && key[b].contains(t_x, t_y)) {
      key[b].press(true);  // tell the button it is pressed
    } else {
      key[b].press(false);  // tell the button it is NOT pressed
    }
  }
  for (uint8_t b = 0; b < 8; b++) {
    tft.setFreeFont(LABEL2_FONT);
    if (key[b].justReleased()) key[b].drawButton();     // draw normal绘制正常色,检查按键是否刚刚被释放。如果该方法返回true,则执行下一行代码。
    if (key[b].justPressed()) {
      key[b].drawButton(true);  // draw invert,绘制反色
      // if a numberpad button, append the relevant # to the numberBuffer
      if (b ==0) {//设置WiFiid
      tft.fillScreen(TFT_BLACK);      // Clear the screen
      ssid=key_set(); 
//      Serial.println(ssid);
      flag=1;    
      break;
      }
      if (b ==1) {//设置WiFiid
      tft.fillScreen(TFT_BLACK);      // Clear the screen
      password=key_set(); 
      Serial.println(password);
      flag=1;    
      break;
      }
}}
delay(100);
if(flag==1)//每一次设置参数返回后重新绘制屏幕
{
  flag=0;
  // Clear the screen
  tft.fillScreen(TFT_BLACK);
  u8f.setCursor(280, 30);//打印坐标,配合下一句使用
  u8f.print("返回");  
  // 显示按键
  for (uint8_t row = 0; row < 4; row++) {
    for (uint8_t col = 0; col < 2; col++) {
      uint8_t b = col + row* 2;
      
      tft.setFreeFont(LABEL2_FONT);
      key[b].initButton(&tft, KEY2_X + col * (KEY2_W + KEY2_SPACING_X),
                        KEY2_Y + row * (KEY2_H + KEY2_SPACING_Y), // x, y, w, h, outline, fill, text
                        KEY2_W, KEY2_H, TFT_WHITE, keylabcol[b], TFT_WHITE,
                        keylab[b], KEY_TEXTSIZE);
      key[b].drawButton();
    }
  }}
}}
/

void preferece_write(void)
{
  Preferences preferences;
  preferences.begin("my-app", false); // 使用“my-app”命名空间,以防止键名冲突
  uint32_t ssid_value = string_to_uint32(ssid);
  preferences.putUInt("ssid_value", ssid_value);//写入
  preferences.end();                        //原文链接:https://blog.csdn.net/idfengming/article/details/138726347  
}
 void preferece_read(void)
{
  Preferences preferences;
  preferences.begin("my-app", false); // 使用“my-app”命名空间,以防止键名冲突    
  uint32_t ssid32 = preferences.getUInt("ssid_value", 0); // 获取计数器值,不存在则设为0
  String reversed_string = uint32_to_string(ssid32);
  Serial.printf("Current reversed_string value: ");  Serial.println(reversed_string);
 //   char* ptr = new char[reversed_string.length() + 1]; // 分配足够的内存空间来存储字符串及其结尾的空字符
 //   strcpy(ssid, reversed_string.c_str()); // 将字符串复制到字符指针所指向的内存中
    ssid= const_cast<char*>(reversed_string.c_str()); // 将字符串转换为字符指针
//    delete[] ptr; // 释放分配的内存
//  ssid=reversed_string;
  preferences.end();                        //原文链接:https://blog.csdn.net/idfengming/article/details/138726347  
} 
uint32_t string_to_uint32(const char* str) {
    uint32_t result = 0;
    for (int i = 0; str[i] != '\0'; ++i) {
        result = (result << 8) | static_cast<uint32_t>(str[i]);
    }
    return result;
}
String uint32_to_string(uint32_t value) {
    String result;
    while (value > 0) {
        result = static_cast<char>(value & 0xFF) + result;
        value >>= 8;
    }
    return result;
}

6.BUG

6.1当大模型的回答超过几百字时,此时屏幕已经快显示完了,语音回答道最后会无限回播最后一段语音,发现问题出现在这段

  clientTTS.onMessage([&](WebsocketsMessage message) {  //讯飞TTS的 wx连接回调函数
    //Serial.print("Got Message: ");

    DynamicJsonDocument responseJson(51200);//最大容量为51200字节。
    DeserializationError error = deserializeJson(responseJson, message.data());
    const char* response = responseJson["data"]["audio"].as<String>().c_str();
    int response_len = responseJson["data"]["audio"].as<String>().length();//提取音频长度
    Serial.printf("lan: %d  \n", response_len);

    //分段获取PCM音频数据并输出到I2S上
    for (int i = 0; i < response_len; i += CHUNK_SIZE) {
      int remaining = min(CHUNK_SIZE, response_len);                                       // 计算剩余数据长度
      char chunk[CHUNK_SIZE];                                                              // 创建一个缓冲区来存储读取的数据
      int decoded_length = Base64_Arturo.decode(chunk, (char*)(response + i), remaining);  // 从response中解码数据到chunk
      size_t bytes_written = 0;
      i2s_write(I2S_PORT_1, chunk, decoded_length, &bytes_written, portMAX_DELAY);
    }

    if (responseJson["data"]["status"].as<int>() == 2) {  //收到结束标志
      Serial.println("Playing complete.");
      delay(200);
      i2s_zero_dma_buffer(I2S_PORT_1);  // 清空I2S DMA缓冲区
    }
  });
}

明显没有走完,否则会显示Playing complete

相关文档左错误时,右正确的返回值,发现date.status没有返回2,且程序没有卡死

原因是没有接收到结束命令,不知道错在哪里,实验得出收到TTS信号时会一直在回调函数里播放,结束后才会继续执行主函数,直接在主函数里加一个消音吧,这样不会一直重复最后的了,能跑就行。

6.2博主的授时只执行一次,有时会失败,所以改了一下,1000是按经验确定的,因为正常情况下远远大于1000,不正常的时候远小于1000.

String getDateTime() {
  // 请求网络时间
  timeClient.update();

  unsigned long epochTime = timeClient.getEpochTime();

  while(epochTime<1000)
  {
  timeClient.update();
  epochTime = timeClient.getEpochTime();
  Serial.println("Epoch Time again: ");
  }
  Serial.print("Epoch Time: ");
  Serial.println(epochTime);
  String timeString = unixTimeToGMTString(epochTime);

  // 打印结果
  Serial.println(timeString);
  return timeString;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值