由于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;
}