NodeMcu 作为 MQTT 智能家居中的 TTS

本次应用使用 NodeMcu 连接讯飞的 XFS5152CE 语音合成芯片(开发板)并且连接一个小型功放作为 MQTT TTS 设备。除了 使用这种 TTS ,对于 HomeAssistant 来说,还有许多的 TTS 服务,如百度TTS、谷歌 TTS、微软 TTS ,之所以做这么一款设备,是因为这样可以实现一个纯局域网环境下的智能家居设备,当然也许还有其他好处,此处不表。

代码中使用的 GPIO 可通过下图进行进行查询。在此再次提醒诸位使用 Arduino IDE 为 NodeMcu 编程,NodeMcu 的引脚号与 GPIO 标号不一致。不要再出现明明程序是对的,引脚输出缺不正确这种情况了。

NodeMCU GPIO号与引脚对应图

使用硬件:

  • MicroUSB 母头(作为供电接口)
  • NodeMcu(MQTT 订阅者,并且进行编码转换,将转换后的字符串以指定格式发送给 TTS 芯片)
  • 科大讯飞 XFS5152CE 语音合成芯片(开发板)
  • 小型功放板(注意输入电压,如果电压不是5.0V 则需要电源模块(升压模块))
  • 4Ω 3W 喇叭两个(功放板要能推得动)
  • 3.5 mm 音频接头公头一个
  • 运行 HomeAssistant 服务设备
  • 运行 MQTT 服务设备(也可将此服务与 HomeAssistant 安装在同一设备上,或者使用其他公司提供的 MQTT 服务器)

MQTT 设备接线图


MQTT 设备接线图

以下为 NodeMcu 代码:

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <SoftwareSerial.h>

#define MQTT_VERSION MQTT_VERSION_3_1_1
#define iFlyTek_ResetPin 12
#define iFlyTek_RDYPin 14
#define WIFI_RX 5
#define WIFI_TX 4

// Wifi: SSID 和 密码
const char* WIFI_SSID = "YourWifiSSID"; //改成你的 WiFi 名称
const char* WIFI_PASSWORD = "YourWifiPassword"; //改成你的 WiFi 密码

// MQTT: ID, 服务端 IP, 端口号, 用户名 和 密码
const PROGMEM char* MQTT_CLIENT_ID = "ESP1";
const PROGMEM char* MQTT_SERVER_IP = "192.168.31.202"; //改成你的 MQTT 服务 IP 地址
const PROGMEM uint16_t MQTT_SERVER_PORT = 1883;  //改成你的 MQTT 服务端口号
const PROGMEM char* MQTT_USER = "YourMQTTUserName"; //改成你的 MQTT 服务用户名
const PROGMEM char* MQTT_PASSWORD = "YourMQTTUserPassword"; //改成你的 MQTT 服务密码

// MQTT: 主题
const char* MQTT_iFlyTek_TOPIC = "mqtt/ESP1/iFlyTek";

WiFiClient wifiClient;
PubSubClient client(wifiClient);
SoftwareSerial MySerial(WIFI_RX, WIFI_TX);

//芯片复位
void reset_iFlyTek()
{
  digitalWrite(iFlyTek_ResetPin, LOW);
  delay(50);
  digitalWrite(iFlyTek_ResetPin, HIGH);
}

bool get_iFlyTek_Ready()
{
  bool isReady = digitalRead(iFlyTek_RDYPin);
  isReady = !isReady;
  return isReady;
}

//用来获取该字符在 UTF8 格式中占用的内存大小
int get_utf8_size(const char pInput)
{
  char c = pInput;
  if (c < 0x80) return 1;
  if (c >= 0x80 && c < 0xC0) return 0;
  if (c >= 0xC0 && c < 0xE0) return 2;
  if (c >= 0xE0 && c < 0xF0) return 3;
  return -1;
}

//获取整个字符数组以 UNICODE 编码时占用的大小
unsigned int get_output_size(char* ch, int size)
{
  unsigned int outSize = 0;
  char* input = ch;
  int inputPos = 0;
  while (inputPos < size)
  {
    int utf8Size = get_utf8_size(*input);
    if (utf8Size > 0)
    {
      outSize = outSize + 2;
      input = input + utf8Size;
      inputPos = inputPos + utf8Size;
    }
    else
    {
      outSize = 0;
      break;
    }
  }
  return outSize;
}

//以小端形式编码
//将单个 UTF8 编码的字符转换成以 UNICODE 编码
int utf8_to_unicode(const char* pInput, char *Unic)
{
  // b1 表示UTF-8编码的pInput中的高字节, b2 表示次高字节, ...
  char b1, b2, b3;

  *Unic = 0x0; // 把 *Unic 初始化为全零
  int utfbytes = get_utf8_size(*pInput);
  char *pOutput = Unic;
  switch ( utfbytes )
  {
    case 1:
      *pOutput     = *pInput;
      *(pOutput + 1) = 0x00;
      utfbytes    = 1;
      break;
    case 2:
      b1 = *pInput;
      b2 = *(pInput + 1);
      if ( (b2 & 0xE0) != 0x80 )
        return 0;
      *pOutput     = (b1 << 6) + (b2 & 0x3F);
      *(pOutput + 1) = (b1 >> 2) & 0x07;
      break;
    case 3:
      b1 = *pInput;
      b2 = *(pInput + 1);
      b3 = *(pInput + 2);
      if ( ((b2 & 0xC0) != 0x80) || ((b3 & 0xC0) != 0x80) )
        return 0;
      *pOutput     = (b2 << 6) + (b3 & 0x3F);
      *(pOutput + 1) = (b1 << 4) + ((b2 >> 2) & 0x0F);
      break;
    default:
      return 0;
      break;
  }
  return utfbytes;
}

void sendString(String string)
{
  int size = string.length();
  //Serial.println(string);
  //Serial.print("string.length();: ");
  //Serial.println(size);
  size =  size + 1;

  char buffer[size];
  string.toCharArray(buffer, size);
  char* input = buffer;
  size = size - 1;
  unsigned int outSize = get_output_size(buffer, size);
  //Serial.print("outSize: ");
  //Serial.println(outSize);
  if (outSize > 0)
  {
    char outCh[outSize];
    char* output = outCh;
    int inputPos = 0;
    while (inputPos < size)
    {
      int uft8Size = get_utf8_size(*input);
      if (uft8Size > 0)
      {
        utf8_to_unicode(input, output);
        output = output + 2;
        input = input + uft8Size;
        inputPos = inputPos + uft8Size;
      }
      else
      {
        break;
      }
    }
    unsigned char frameHeader = 0xFD;
    unsigned char temp = 0x00;
    unsigned int frameLength = outSize + 2;
    unsigned char dataHeader[2] = {0x01, 0x03};
    MySerial.write(frameHeader);
    //Serial.print(frameHeader, HEX);
    if (frameLength < 255)
    {
      MySerial.write(temp);
      //Serial.print(temp, HEX);
    }
    MySerial.write(frameLength);
    //Serial.print(frameLength, HEX);
    MySerial.write(dataHeader[0]);
    //Serial.print(dataHeader[0], HEX);
    MySerial.write(dataHeader[1]);
    //Serial.print(dataHeader[1], HEX);
    //Serial.print("   ");
    for (int index = 0; index < outSize ; ++index)
    {
      MySerial.write(outCh[index]);
      //Serial.print(outCh[index], HEX);
      //Serial.print(" ");
    }
    //    Serial.println(" ");
  }
}

// 接收来自 MQTT 的消息
void callback(char* p_topic, byte* p_payload, unsigned int p_length) {
  // concat the payload into a string
  //  Serial.println("call back has called");
  String payload;
  for (uint8_t i = 0; i < p_length; i++) {
    payload.concat((char)p_payload[i]);
  }

  // handle message topic
  if (String(MQTT_iFlyTek_TOPIC).equals(p_topic)) {
    //    Serial.println("MQTT receive Data: ");
    //    Serial.println(payload);
    sendString (payload);
  }
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    //    Serial.print("INFO: Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASSWORD)) {
      //      Serial.println("INFO: connected");
      delay(1);
      // Once connected, publish an announcement...
    } else {
      //      Serial.print("ERROR: failed, rc=");
      //      Serial.print(client.state());
      //      Serial.println("DEBUG: try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void setup() {
  // init the serial
  Serial.begin(9600);
  MySerial.begin(9600);

  pinMode(iFlyTek_ResetPin, OUTPUT);
  pinMode(iFlyTek_RDYPin, INPUT);

  // init the WiFi connection
  //  Serial.println();
  //  Serial.println();
  //  Serial.print("INFO: Connecting to ");
  WiFi.mode(WIFI_STA);
  //  Serial.println(WIFI_SSID);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    //    Serial.print("- - - | ");
  }

  //  Serial.println("");
  //Serial.println("INFO: WiFi connected");
  //  Serial.print("INFO: IP address: ");
  //  Serial.println(WiFi.localIP());

  // init the MQTT connection
  client.setServer(MQTT_SERVER_IP, MQTT_SERVER_PORT);
  client.setCallback(callback);

  reset_iFlyTek();
}

void loop() {
  if (!client.connected()) {
    reconnect();
    /*boolean subOK = */ client.subscribe(MQTT_iFlyTek_TOPIC);
    //Serial.print("MQTT: sub status: ");
    //Serial.println(subOK);
  }
  client.loop();
}

HomeAssistant 配置:

input_text:
  iflytek:
    name: iFlyTek
    min: 0
    max: 5000

mqtt:
#改成你的 MQTT 服务端口号
  broker: 192.168.31.202
#改成你的 MQTT 服务端口号
  port: 1883
#改成你的 MQTT 服务用户名
  username: YourMQTTUserName
#改成你的 MQTT 服务密码
  password: YourMQTTUserPassword

automation: !include automations.yaml

automations.yaml 文件:

- id: iFlyTek
  alias: iFlyTekTTS
  trigger:
    platform: state
    entity_id: input_text.iflytek
  action:
    service: mqtt.publish
    data_template:
      topic: "mqtt/ESP1/iFlyTek"
      payload: "{{states.input_text.iflytek.state}}"

后续我会将 Arduino 代码封装成库,方便直接调用。这个坑留着以后填。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容