内容


使用 Watson 和 IoT Platform 服务构建家庭助理移动应用程序

Comments

过去,构建家庭助理需要做大量工作,而且可能在技术上存在挑战。但是,借助 IBM Watson 和其他互补性云服务,您现在能够轻松地进行创造和创新。

本教程将介绍如何将 Watson 的强大功能与 IBM IoT Platform 的简单性相结合,创建一个家庭助理来控制一些基本的电子设备(灯和相机)。可以扩展该框架来控制连接的真实家电。您可以使用本教程作为指导,构建自己的应用程序。

家庭助理移动应用程序使用 iOS Swift 开发,它使用键盘输入和语音命令与 IBM Watson Conversation 进行通信;而且用户界面还支持发送消息或讲话。我使用了 Watson Text to Speech 和 Speech to Text 服务来实现此对话。Watson Conversation 已经过训练,能够根据语音命令或输入消息来推断意图。根据这些意图,家庭助理应用程序会通过 IoT Platform 服务和一个家庭网关 (Raspberry Pi) 向设备发送命令。家庭网关控制各种设备,并通过 IoT Platform 服务将设备事件发送给家庭助理应用程序。该系统还使用 Object Storage 服务来存储家庭网关上传的照片。

您可以下载该家庭助理的代码,作为构建自己的应用程序的起点。

应用程序的流
应用程序的流

该应用程序的基本工作流是:

  • (1) 用户口头发出一个命令或在家庭助理的用户界面中键入一个命令。
  • (2,3)使用 Watson Speech to Text 服务将语音命令转录为文本。如果用户键入命令,则跳过这一步。
  • (4,5)将已转录的文本转发给 Watson Conversation,使其能够从用户的语音中推断出意图。
  • (6) 收到意图时,家庭助理程序会构造一条 JSON 消息的命令并将其发送给 IoT Platform。
  • (7) 一个 Node-RED 应用程序在家庭网关上运行,它会订阅命令主题并接收命令消息。
  • (8a,8b)家庭网关执行相应操作,包括开灯或关灯,或者拍照。
  • (8c) 将拍摄的照片上传到 Object Storage。
  • (9) 完成任务后,Node-RED 应用程序将设备事件(包含状态)作为一个 JSON 消息发送给 IoT Platform。
  • (10) 家庭助理(已订阅该设备主题)收到设备事件。
  • (10a) 从 Object Storage 下载拍摄的照片并显示在用户界面上。
  • (11,12)家庭助理会提取状态文本,并将它转发给 Watson Text to Speech 服务,以便将其转换为音频。
  • (13) 然后,家庭助理向用户播放该音频。

下面的屏幕截图显示了这个家庭助理移动应用程序。

移动应用程序的屏幕截图
移动应用程序的屏幕截图

构建您的应用程序需要做的准备工作

要构建一个类似家庭助理的应用程序,需要熟悉:

  • iOS 开发环境
  • Swift 编程
  • Node-RED 开发环境
  • Watson Developer SDK
  • IoT Platform 和 MQTT 协议

为了构建该应用程序,我使用了以下开发环境、硬件和 Bluemix 服务:

开发环境

Bluemix 服务

硬件

  • Raspberry Pi 3B Starter Kit,包括 SD 卡和电源适配器(带 USB 线)
  • Adafruit NeoPixel 散射 8 毫米通孔 LED - 5 个装
  • 适合 Raspberry Pi 3B/2B/B+ 的 500 万像素网络摄像机模板块 1080 p x720 p 快速版
  • 母对母跨接电缆 1P-1P
  • 0.1µF 电容器
  • 560 Ω 电阻器
  • 以太网线(仅第一次引导硬件时需要)
  • USB 键盘(仅第一次引导硬件时需要)
  • HDMI 显示线(仅第一次引导硬件时需要)
  • 鼠标(仅第一次引导硬件时需要)
1

在 Bluemix 上准备 Watson 服务

从 Bluemix 目录,选择并创建以下服务。确保您保存了所有凭证。

  1. 登录到 Bluemix
  2. 单击 Catalog
  3. 单击 Services 列表下的 Watson
  4. 单击 Conversation
  5. 创建该服务后,单击 Service credentials > View credentials 并记下服务凭证名称。
  6. 单击 Create

重复这些步骤,以创建 Watson Text to Speech 和 Watson Speech to Text 服务的实例。

2

导入 Watson Conversation 服务

  1. 创建 Watson Conversation 服务实例后,单击 Launch tool 启动它。
  2. 可以单击 Create 开始创建一个新对话并定义意图、实体和对话,或者单击 Import 导入一个完整的工作区。对于本教程,我导入一个文件。
  3. 单击 Import,选择 workspace-homeassistant.json(包含在 “获取代码” 部分的文件中),然后单击 Import
  4. 返回到工作区,单击 3 个竖排的点组成的图标以打开菜单。选择 View details。这会加载工作区的细节,包括工作区 ID。
  5. 复制 WORKSPACE_ID。从移动应用程序使用 Conversation 服务时需要此信息。

现在我将介绍 Conversation 服务中定义的工件。对话框很简单,因为我们更加关心各种 Bluemix 服务、移动应用程序和在 Raspberry Pi 上运行的 Node-RED 之间的集成。

Conversation 服务主要用于根据用户的消息或语音交互来推断意图。对话框用于执行对话。

对话框说明
Start 这是对话开始对话框。
Greetings 处理来自用户的问候。
On Light 将用户命令转换为 “开灯” 意图。
Off Light 将用户命令转换为 “关灯” 意图。
Take Picture 将用户命令转换为 “拍照” 意图。
Else 这是处理所有其他情况的容器。
对话开始的屏幕截图
对话开始的屏幕截图
3

在 Bluemix 上准备 Object Storage 服务

  1. 从 Bluemix 目录,选择 Storage > Object Storage 并创建一个实例。记住选择 Free 等级。
  2. 创建 Object Storage 后,复制凭证。保存 OS_PROJECTIDOS_USERIDOS_USERNAMEOS_PASSWORD,从 Node-RED 和移动应用程序连接 Object Storage 时需要它们。
    {
      "auth_url": "https://identity.open.softlayer.com",
      "project": "object_storage_750f36e0_f61b_42c0_9458_0876db9f3e36",
      "projectId": [OS_PROJECTID],
      "region": "dallas",
      "userId": [OS_USERID],
      "username": [OS_USERNAME],
      "password": [OS_PASSWORD],
      "domainId": "508e3dcdff0a4d7eb7246a6852bdcc16",
      "domainName": "1307809",
      "role": "admin"
    }
4

准备 Watson IoT Platform 服务

要准备 Watson IoT Platform 服务,需要将 Raspberry Pi 作为“设备” 并将移动应用程序作为“应用程序”连接到 IoT Platform。

将 Raspberry Pi 作为设备连接到 IoT Platform

  1. 从 Bluemix 目录,单击 Internet of Things > Internet of Things Platform 来创建 IoT Platform 服务的一个实例。
  2. 选择 Lite 计划,然后单击 Create
  3. 在 Internet of Things Platform 页面上单击 Launch
  4. 从左侧导航栏,单击 Devices
  5. 选择 Device Type 选项卡,然后单击 Create Type 创建该设备类型。
  6. 在 Create Device Type 窗口中,单击 Create device type 并指定名称和描述。保存 DEV_TYPE 名称。 Create device type 窗口
    Create device type 窗口
  7. 单击 Next,直到您的设备类型创建完成。
  8. 再次单击左侧导航栏中的 Devices,然后单击 Add device
  9. Add Device 窗口中,从 Choose Device Type 下拉列表中选择您的设备并单击 Next添加一个设备
    添加一个设备
  10. Add Device > Define Info 窗口中,指定设备 ID 和序列号(在 Node-RED 中用于建立连接)。将要使用的 DEV_ID 保存在 Node-RED Credentials 配置中。
  11. 接受其他字段的默认值,然后单击 Next,直到已添加该设备。此时将会显示一个身份验证令牌。将要使用的 ORG_IDDEV_IDAUTH_TOKEN 保存在 Node-RED Credentials 配置中。 凭证
    凭证

也可以从这个 developerWorks 诀窍中获取更多细节

将移动应用程序连接到 IoT Platform

  1. 从左侧导航栏,单击 Apps 转到 Apps 页面。
  2. 单击 Generate API Key 为您的移动应用程序生成一个 API 密钥。
  3. 在 Generate API Key 窗口中,选择 Standard Application 作为 API 角色,保存 API_KEYAPP_AUTH_TOKEN 供以后在移动应用程序中使用。单击 GenerateGenerate API key 窗口
    Generate API key 窗口
5

准备 Raspberry Pi

可以按照这些详细指令来设置 Raspberry Pi。我使用了带 Pixel 的 Raspbian Jessie,因为它安装了后面需要的 Node-RED。

默认情况下未设置 wifi 连接。您需要通过以太网电缆将 Raspberry Pi 连接到路由器。还需要连接 USB 键盘和 HDMI 显示线。

  1. 单击右上角的 wifi 按钮来连接您的 wifi 网络。
  2. 选择 Pi > Preferences > Raspberry Pi Configuration 来启用所需的接口:SSH 和 VNC,使您无需键盘和屏幕就能访问 Raspberry Pi,还有 Remote GPIO 和 Camera。 Raspberry Pi 配置窗口
    Raspberry Pi 配置窗口
  3. 根据下图将 Raspberry Pi 与 LED 和相机相连。
    • GPIO 3 V 引脚连接到 LED 的 5 V 引脚。
    • GPIO GND 引脚连接到 LED 的 GND 引脚。
    • 在连接到 LED 的 DIN 引脚之前,GPIO18 引脚要先连接到电阻器 (560 Ω)。
    • 电容器 (0.1 µF) 并行连接到 5 V 引脚和 GND。
    • 相机连接到 Raspberry Pi 上的 Camera CSI。
    连接 Raspberry Pi
    连接 Raspberry Pi
6

在 Raspberry Pi 上开发 Node-RED

接下来,您需要在 Raspberry Pi 上开发和准备 Node-RED 环境。

在 Raspberry Pi 上准备 Node-RED 环境

  1. 从终端,通过 ssh 连接到 Raspberry Pi 并导航到 Node-RED 文件夹。您可能需要登录到路由器来检查 IP 地址。
    ssh pi@ipaddr-raspberrypi
  2. 安装 node-red-node-pi-neopixel 来控制 NeoPixel LED。
    curl -sS get.pimoroni.com/unicornhat | bash
    npm install node-red-node-pi-neopixel
  3. 安装 node-red-contrib-camerapi 来控制相机。
    npm install node-red-contrib-camerapi
  4. 安装 node-red-contrib-objectstore 来访问 Bluemix Object Store。
    npm install node-red-contrib-objectstore

    其他所需的节点、Watson IoT 输入和输出节点已预先安装。

  5. 运行下面的命令,将 Node-RED 配置为在系统引导时自动启动。
    sudo systemctl enable nodered.service
  6. 打开浏览器并指向 http://ipaddr-raspberrypi:1880,以访问 Node-RED。您会发现面板中安装了以下节点。 NodeRED 面板
    NodeRED 面板

开发流来控制 LED

执行这一步之前,请确保已完成上一步,在 Raspberry Pi 中准备好了 Node-RED 环境。然后,需要从浏览器导航到 Node-RED 应用程序。

  1. 按以下方式连接该流。它接收一个命令,确定意图,执行意图(打开或关闭 LED),然后向移动应用程序发送一个事件。 连接流
    连接流

下表解释了该流的每部分所控制的功能。

节点名称节点类型说明
Light cmd Watson IoT 输入节点 用于通过 IoT Platform 从移动应用程序接收命令。格式为 iot-2/cmd/[CMD_TYPE]/fmt/json,其中 [CMD_TYPE] 是在 Command 字段中定义的(例如 “light”)。
编辑 Watson IoT 节点
编辑 Watson IoT 节点
它使用了在 raspberrypi 配置对象中定义的凭证。
Decide Intent 功能节点 从有效负载中获取意图,重新格式化有效负载来打开或关闭 LED。
action = msg.payload.action
object = msg.payload.object
intent = msg.payload.intent
if (intent == "OnLight") {
    msg.payload = "#ffffff"
    return [msg, null]
} else if (intent == "OffLight") {
    msg.payload = "#000000"
    return [null, msg];
}
return msg;
NeoPixel LED rpi neopixels 节点 按以下方式配置此节点:
Edit rpi-neopixels 窗口
Edit rpi-neopixels 窗口
Format Status On 功能节点 Construct a status 'on' message in JSON
var jsonObj = { "dev":"light", "status": "on"};
msg.payload = JSON.stringify(jsonObj)
return msg;
Format Status Off 功能节点 Construct a status 'off' message in JSON
var jsonObj = { "dev":"light", "status": "off"};
msg.payload = JSON.stringify(jsonObj)
return msg;
Light Event Watson IoT 输出节点 用于通过 IoT Platform 从移动应用程序发送设备事件。格式为 iot-2/type/[DEV_TYPE]/id/[DEV_ID]/evt/[EVT_TYPE]/fmt/json,其中:
  • [DEV_TYPE] 是在 IoT Platform 服务中定义的。
  • [DEV_ID] 是在 IoT Platform 服务中定义的。
  • [EVT_TYPE] 是在字段 Event type 中配置的(例如 light)。
编辑 Watson IoT 节点
编辑 Watson IoT 节点
raspberrypi wiotp-credentials 的配置对象 Inject 节点用于测试 LED 的打开和关闭功能。Debug 节点用于将消息有效负载发送到右侧的 Debug 窗口。 Edit wiotp-credentials 窗口
Edit wiotp-credentials 窗口

从 IoT Platform 获取并供 Watson IoT 节点使用的 Credentials 配置对象包括:
  • 组织:ORG_ID
  • 服务器名称:ORG_ID.messaging.internetofthings.ibmcloud.com
  • 设备类型:DEV_TYPE
  • 设备 ID:DEV_ID
  • 身份验证令牌:AUTH_TOKEN
  • 名称:raspberrypi

开发一个流来拍照并上传到 Object Storage

在执行这一步之前,请确保已完成在 Raspberry Pi 中准备 Node-RED 环境的步骤。然后,需要从浏览器导航到 Node-RED 应用程序。

  1. 连接该流,如下图所示。该流接收到一个拍照命令,完成拍照后,它将照片上传到 Object Storage 并生成一个事件。 捕获一个照片流
    捕获一个照片流

下表解释了该流的每部分所控制的功能。

节点名称节点类型说明
Camera Cmd Watson IoT 输入节点 用于通过 IoT Platform 从移动应用程序接收命令。格式为 iot-2/cmd/[CMD_TYPE]/fmt/json,其中 [CMD_TYPE] 是在 Command 字段(例如 camera)中定义的。
编辑 Watson IoT 节点
编辑 Watson IoT 节点
它使用了在 raspberrypi 配置对象中定义的凭证。
Take Photo camerapi 拍照节点 使用 Raspberry Pi 相机拍照。文件模式 “Generate” 在文件夹中创建了一个文件。文件名、文件夹名和格式可分别在 msg.filename、msg.filepath 和 msg.fileformat 中找到。
编辑 camerapi-takephoto 模式
编辑 camerapi-takephoto 模式
ObjectStorage Upload os-put 节点 获取拍摄的图像文件并将该文件上传到 Bluemix 中的 Object Storage。该命令获取输入消息中指定的文件(msg.filename、msg.filepath 和 msg.fileformat),然后将该文件上传到该节点中指定的容器中。成功上传后,它在 msg.url 中返回 URL,在 msg.objectname 中返回对象名称。它从 Object Storage 配置中获取服务凭证。
编辑 os-put 节点
编辑 os-put 节点
Format Event 功能节点 在 JSON 中构造一个包含 URL、对象名和容器名的事件消息,让移动应用程序知道从何处下载该文件。
var json = { "url": msg.url, 
"objectname": msg.objectname, 
"containername": "visual-recognition-images"};
msg.payload = JSON.stringify(json);
return msg;
Camera Event Watson IoT 输出节点 用于通过 IoT Platform 从移动应用程序发送设备事件。格式为 iot-2/type/[DEV_TYPE]/id/[DEV_ID]/evt/[EVT_TYPE]/fmt/json,其中:
  • [DEV_TYPE] 是在 IoT Platform 服务中定义的。
  • [DEV_ID] 是在 IoT Platform 服务中定义的。
  • [EVT_TYPE] 是在字段 Event type(例如 camera)中配置的。
编辑 Watson IoT 节点
编辑 Watson IoT 节点
'none' os-config 节点 定义 Object Storage 服务的登录凭证。这些信息可从您之前保存的 Object Storage Credentials 中获取。
  • 配置信息:API Based
  • 地区:Dallas
  • 租户 Id:OS_PROJECTID
  • 用户 Id:OS_USERID
  • 用户名:OS_USERNAME
  • 密码:OS_PASSWORD
编辑 os-config 节点
编辑 os-config 节点
7

开发移动应用程序

因为此应用程序类似于一个聊天消息接口,所以我使用了流行的 iOS 用户界面小部件 JSQMessagesViewController。我可以使用一段类似的示例代码,但它是用 Objective C 编写的。不过它仍可提供有用的参考。

开发用户界面

准备 Xcode 项目

  1. 从 Xcode,单击 File > Project 创建一个单视图应用程序。 选择 Single View Application 并指定产品名称:Home Assistant。
  2. 转到项目文件夹并初始化 CocoaPods。生成了一个 Podfile。
    pod init
  3. 将这行代码添加到该 Podfile 来安装 JSQMessagesViewController 小部件。
    pod 'JSQMessagesViewController'
  4. 运行下面的命令来安装 JSQMessagesViewController 依赖项。生成了一个 Xcode 工作区。应该使用 Workspace (*.wcworkspace) 而不是 Project (*.xcodeproj) 来重新打开 Xcode。
    pod install
  5. 在 ViewController.swift 文件中,导入 JSQMessagesViewController 模块并将 ViewController 类更改为继承自 JSQMessagesViewController 类。声明消息数组来保存聊天消息。
    import JSQMessagesViewController
    class ViewController: JSQMessagesViewController {
      var messages = [JSQMessage]()
    }
  6. 最后,在 Info.plist 中输入以下代码来启用麦克风。
    <key>NSMicrophoneUsageDescription</key>
      <string>Need microphone to talk to Watson</string><

设置用户界面并创建一个扩展

要设置用户界面并创建一个扩展,请执行以下操作:

  1. 创建一个名为 UIExt.swift 的文件,以包含所有 UI 相关逻辑作为 Swift 扩展。然后声明一个扩展。
    extension ViewController {
    }
  2. 创建一个 SetupUI() 函数用于:
    • 初始化 titlesenderIdsenderDisplayName
    • 创建一个麦克风按钮并向 callback 函数注册 touchDowntouchUpInside 事件。
    • 注册一个菜单项(合成为 callback 函数)。
    func setupUI() {
        self.title = "Watson Chat"
        self.senderId = UIDevice.current.identifierForVendor?.uuidString
        self.senderDisplayName = UIDevice.current.identifierForVendor?.uuidString
        
        JSQMessagesCollectionViewCell.registerMenuAction(#selector(synthesize(sender:)))
        
        // Create mic button
        let microphoneImage = UIImage(named:"microphone")!
        let microphoneButton = UIButton(type: .custom)
        microphoneButton.setImage(microphoneImage, for: .normal)
        microphoneButton.imageView?.contentMode = UIViewContentMode.scaleAspectFit
        self.inputToolbar.contentView.leftBarButtonItem = microphoneButton
        
        // Add press and release mic button
        microphoneButton.addTarget(self, action:#selector(didPressMicrophoneButton), for: .touchDown)
        microphoneButton.addTarget(self, action:#selector(didReleaseMicrophoneButton), for: .touchUpInside)
        
        setAudioPortToSpeaker()
      }
  3. 添加麦克风图标作为资产。单击 Assets.xcassets,将图标文件拖到工作区来创建图像集。
该图显示麦克风图标未被按下
该图显示麦克风图标未被按下
该图显示麦克风图标被按下
该图显示麦克风图标被按下

实现用户界面的处理函数

在 UIExt.swift 文件中:

  1. 添加以下函数来处理麦克风按钮按下事件。按下此按钮来说出命令时,它就会开始将信息传输到 Watson Speech to Text 服务。
    func didPressMicrophoneButton(sender: UIButton) {
        let microphonePressedImage = UIImage(named:"microphone_pressed")!
        sender.setImage(microphonePressedImage, for: .normal)
        AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))
        // Clear the input text
        self.inputToolbar.contentView.textView.text = ""
        // speech-to-text startStreaming
        sttStartStreaming()
      }
  2. 添加以下函数来处理麦克风按钮释放事件,这会停止向 Watson Speech to Text 服务传输数据。
    func didReleaseMicrophoneButton(sender: UIButton){
        let microphoneImage = UIImage(named:"microphone")!
        sender.setImage(microphoneImage, for: .normal)
        // speech-to-text stop streaming
        self.sttStopStreaming()
      }
  3. 重写 didPressSend() 函数来处理发送按钮按下事件。该代码将消息附加到消息数组,然后向 Watson Conversation 服务发送一条请求并接收一条响应。
    override func didPressSend(
    _ button: UIButton!, 
    withMessageText text: String!, 
    senderId: String!, 
    senderDisplayName: String!, 
    date: Date!) {
        send(text)
      }

重写 UI 回调函数

在 UIExt.swift 文件中,添加以下函数:

  1. 重写 collectionView | numberOfItemsInSection 回调函数,以便返回数组中的消息数量。
    override func collectionView(_ collectionView: UICollectionView, 
        numberOfItemsInSection section: Int) -> Int {
        return self.messages.count
      }
  2. 重写 collectionView | cellForItemAt 回调函数,该函数负责设置索引路径上的特定单元的文本并呈现它的颜色。
      override func collectionView(_ collectionView: UICollectionView, 
        cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
        let message = self.messages[indexPath.item]
        if !message.isMediaMessage {
          if message.senderId == self.senderId {
            cell.textView.textColor = UIColor(R: 0x72, G: 0x9B, B: 0x79)
          } else {
            cell.textView.textColor = UIColor(R: 0x47, G: 0x5B, B: 0x63)
          }
          let attributes : [String:AnyObject] = 
              [NSForegroundColorAttributeName:cell.textView.textColor!, NSUnderlineStyleAttributeName: 1 as AnyObject]
          cell.textView.linkTextAttributes = attributes
        }
        return cell
      }
  3. 重写 collectionView | messageBubbleImageDataForItemAt 回调函数,以返回响应来表明它是索引路径上的特定单元的传入还是传出气泡。
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, 
        messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
        let data = messages[indexPath.row]
        switch(data.senderId) {
        case self.senderId:
          return self.outgoingBubble
        default:
          return self.incomingBubble
        }
      }
  4. 重写 collectionView | attributedTextForMessageBubbleTopLabelAt 回调函数,以便设置并返回发送者名称作为消息气泡上的标签(在样式属性旁边):
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, 
        attributedTextForMessageBubbleTopLabelAt indexPath: IndexPath!) -> NSAttributedString! {
        if let message = firstMessage(at: indexPath) {
          let paragraphStyle = NSMutableParagraphStyle()
          paragraphStyle.alignment = NSTextAlignment.left
          let attrs = [
            NSParagraphStyleAttributeName: paragraphStyle,
            NSBaselineOffsetAttributeName: NSNumber(value: 0),
            NSForegroundColorAttributeName: UIColor(R: 0x1e, G: 0x90, B: 0xff)
          ]
          return NSAttributedString(string: message.senderDisplayName, attributes: attrs)
        } else {
          return nil
        }
      }
  5. 重写 collectionView | heightForMessageBubbleTopLabelAt 回调函数,以便返回文本标签的高度。
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, 
      layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, 
      heightForMessageBubbleTopLabelAt indexPath: IndexPath!) -> CGFloat {
        if let _ = firstMessage(at: indexPath) {
          return kJSQMessagesCollectionViewCellLabelHeightDefault
        } else {
          return 0.0
        }
      }

实用程序函数

最后,同样在 UIExt.swift 文件中,添加以下函数:

  1. send() 函数播放一段声音,将发送的消息附加到消息数组中,调用 finishSendingMessage() 函数,然后调用 conversationRequestResponse() 函数来处理与 Watson Conversation 服务的交互。
    func send(_ text: String) {
        setAudioPortToSpeaker()
        JSQSystemSoundPlayer.jsq_playMessageSentSound()
        let message = JSQMessage(senderId: self.senderId, senderDisplayName: self.senderDisplayName, date: Date(), text: text)    
        self.messages.append(message!)
        self.finishSendingMessage(animated: true)
        self.conversationRequestResponse(text)
      }
  2. firstMessage() 函数返回第一条不是来自同一个发送者的消息,以便在文本气泡顶部指示发送者。
    func firstMessage(at: IndexPath) -> JSQMessage! {
        let message = self.messages[at.item]
        if message.senderId == self.senderId {
          return nil
        }
        if at.item - 1 > 0 {
          let previousMessage = self.messages[at.item-1]
          if previousMessage.senderId == message.senderId {
            return nil
          }
        }
        return message
      }
  3. didReceiveConversationResponse() 函数在 Watson Conversation 返回响应时调用。该函数播放一段声音,将消息附加到消息数组,并将响应分派给 Watson Text to Speech 服务来合成句子。最后,它会调用 finishReceiveMessage() 函数。
    func didReceiveConversationResponse(_ response: [String]) {
        let sentence = re,sponse.joined(separator: " ")
        if sentence == "" { return }
        setAudioPortToSpeaker()
        JSQSystemSoundPlayer.jsq_playMessageReceivedSound()
        let message = JSQMessage(senderId: "Home Assistant", senderDisplayName: "Home Assistant", date: Date(), text: sentence)
        self.messages.append(message!)
        
        DispatchQueue.main.async {
          // text-to-speech synthesize
          self.ttsSynthesize(sentence)
          self.reloadMessagesView()
          self.finishReceivingMessage(animated: true)
        }
      }

参阅 UIExt.swift 文件查看完整的实现(包含在 “获取代码” 部分中的文件中)。

连接 Watson 服务

现在是时候实现与 Watson 服务的连接了。

准备 Watson SDK

  1. 安装 Watson Swift SDK。必须使用 Carthage 完成此任务,而安装 Carthage 的一种方法是使用 Homebrew(MacOS 的一个包管理器)。
  2. 安装 Homebrew。
    /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  3. 安装 Carthage。
    brew update
    brew install carthage
  4. 在项目文件夹中创建一个 Cartfile 并添加下面这行代码。
    github "https://github.com/watson-developer-cloud/swift-sdk"
  5. 安装 Swift SDK 框架。
    carthage update --platform iOS
  6. 如果获得类似下面这样的错误:
    Module compiled with Swift 3.1 cannot be imported in Swift 3.0.2

    则使用此命令重新编译二进制文件:
    carthage update --platform iOS --no-use-binaries
  7. 导航到 General > Linked Frameworks > Libraries,以便将 Swift SDK 框架添加到项目中。单击 + 图标。 Project 窗口显示了 General 选项卡
    Project 窗口显示了 General 选项卡
  8. 单击 Add Other 并导航到 Carthage/Build/iOS 文件夹。选择以下框架:TextToSpeechV1.framework、SpeechToTextV1.framework、ConversationV1.framework 和 RestKit.framework。 Add frameworks and libraries 窗口
    Add frameworks and libraries 窗口

    完成后,您应该已添加以下框架。

    Linked frameworks and libraries 窗口
    Linked frameworks and libraries 窗口
  9. 将这些框架复制到您的应用程序中,以便可在运行时访问它们。转到 Build Phases 选项卡,单击 + 图标,然后选择 New Run Script PhaseBuild phases 选项卡窗口
    Build phases 选项卡窗口
  10. 在 Run Script 中,指定:
    /usr/local/bin/carthage copy-frameworks
    Run script 窗口
    Run script 窗口
  11. 单击 + 图标指定以下输入文件。
    $(SRCROOT)/Carthage/Build/iOS/TextToSpeechV1.framework
    $(SRCROOT)/Carthage/Build/iOS/SpeechToTextV1.framework
    $(SRCROOT)/Carthage/Build/iOS/ConversationV1.framework
    $(SRCROOT)/Carthage/Build/iOS/RestKit.framew
    输入文件
    输入文件
  12. 最后,在 Info.plist 文件中输入以下行,作为连接到 Watson IoT Platform 的源代码。
    <key>NSAppTransportSecurity</key>
    <dict>
      <key>NSExceptionDomains</key>
      <dict>
        <key>watsonplatform.net</key>
        <dict>
          <key>NSTemporaryExceptionRequiresForwardSecrecy</key>
          <false/>
          <key>NSIncludesSubdomains</key>
          <true/>
          <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
          <true/>
          <key>NSTemporaryExceptionMinimumTLSVersion</key>
           <string>TLSv1.0</string>
        </dict>
      </dict>
    </dict>

初始化

  1. 创建一个名为 WatsonExt.swift 的文件,以便包含连接 Watson Text to Speech、Speech to Text 和 Conversation 服务的代码。为 ViewController 声明一个扩展。
  2. 要初始化 Watson Text to Speech 服务,需要从 Bluemix Service Credentials 获取用户名和密码。
    textToSpeech = TextToSpeech(username: Credentials.TextToSpeechUsername,
                                password: Credentials.TextToSpeechPassword)
  3. 要初始化 Watson Speech to Text 服务,需要从 Bluemix Service Credentials 获取用户名和密码。
    speechToTextSession = SpeechToTextSession(username: Credentials.SpeechToTextUsername,
                                              password: Credentials.SpeechToTextPassword)
  4. 添加一个回调来处理语音到文本的转换结果。
    speechToTextSession?.onResults = onResults
  5. 最后,要初始化 Watson Conversation 服务,需要从 Bluemix Service Credentials 获取用户名和密码。此外,还需要使用 WORKSPACE_ID。在第一次连接时,Conversation 服务会定义一个 conversation_start 对话框,用于返回初始响应(通常配置为问候语)。因此,调用 didReceiveConversationResponse() 函数来附加该消息,并使用 Text to Speech 服务来合成该文本。保存上下文,后续交互中会用到它们。
    conversation = Conversation(username: Credentials.ConversationUsername,
                                password: Credentials.ConversationPassword,
                                version: "2017-03-12")
        let failure = { (error: Error) in print(error) }
        conversation?.message(withWorkspace: Credentials.ConversationWorkspaceID, failure: failure) {
          response in
          print("output.text: \(response.output.text)")
          self.didReceiveConversationResponse(response.output.text)
          self.context = response.context
        }

Watson Speech to Text 服务

sttStartStreaming() 函数用于连接到 Speech to Text 服务,而且可以启动传输请求。它还可以启动麦克风输入。以下函数会在用户按下麦克风按钮时调用。

func sttStartStreaming() {
    // define settings
    var settings = RecognitionSettings(contentType: .opus)
    settings.continuous = true
    settings.interimResults = true

    self.speechToTextSession?.connect()
    self.speechToTextSession?.startRequest(settings: settings)
    self.speechToTextSession?.startMicrophone()
  }

sttStopStreaming() 函数用于停止 Speech-to-Text 服务。它也可以停止麦克风输入。以下函数会在用户释放麦克风按钮时调用。

func sttStopStreaming() {
    self.speechToTextSession?.stopMicrophone()
    self.speechToTextSession?.stopRequest()
    // No need to disconnect -- the connection will timeout if the microphone
    // is not used again within 30 seconds. This avoids the overhead of
    // connecting and disconnecting the session with every press of the
    // microphone button.
    //self.speechToTextSession?.disconnect()
    //self.speechToText?.stopRecognizeMicrophone()
  }

onResults() 回调函数基于来自 Speech to Text 服务的最佳转录结果来更新文本视图小部件。

func onResults(results: SpeechRecognitionResults) {
    self.inputToolbar.contentView.textView.text = results.bestTranscript
    self.inputToolbar.toggleSendButtonEnabled()
  }

Watson Text to Speech

ttsSynthesize() 函数使用了 Text to Speech 服务,基于一个句子来合成一条语音。它会播放合成的音频数据。

func ttsSynthesize(_ sentence: String) {
    // Synthesize the text
    let failure = { (error: Error) in print(error) }
    self.textToSpeech?.synthesize(sentence, voice: SynthesisVoice.gb_Kate.rawValue, failure: failure) { data in
      self.audioPlayer = try! AVAudioPlayer(data: data)
      self.audioPlayer.prepareToPlay()
      self.audioPlayer.play()
    }
  }

Watson Conversation

conversationRequestResponse() 函数处理与 Conversation 服务的交互。它向 Conversation 服务发送请求(文本形式)并接收响应。然后它会调用 didReceiveConversationResponse() 函数来处理响应文本。最后,它会调用 issueCommand() 函数。

func conversationRequestResponse(_ text: String) {
    let failure = { (error: Error) in print(error) }
    let request = MessageRequest(text: text, context: self.context)
    self.conversation?.message(withWorkspace: Credentials.ConversationWorkspaceID,
                               request: request,
                               failure: failure) {
    response in
      print(response.output.text)
      self.didReceiveConversationResponse(response.output.text)
      self.context = response.context
      // issue command based on intents and entities
      print("appl_action: \(response.context.json["appl_action"])")
      self.issueCommand(intents: response.intents, entities: response.entities)
    }
  }

issueCommand() 函数破译来自 Conversation 服务的意图,并通过 Watson IoT Platform 服务将命令发送到 Raspberry Pi。

func issueCommand(intents: [Intent], entities: [Entity]) {
    
    for intent in intents {
      print("intent: \(intent.intent), confidence: \(intent.confidence) ")
    }
    for entity in entities {
      print("entity: \(entity.entity), value: \(entity.value)")
    }
    
    for intent in intents {
      if intent.confidence > 0.9 {
        switch intent.intent {
        case "OnLight":
          let command = Command(action: "On", object: "Light", intent: intent.intent)
          sendToDevice(command, subtopic: "light")
        case "OffLight":
          let command = Command(action: "Off", object: "Light", intent: intent.intent)
            sendToDevice(command, subtopic: "light")
        case "TakePicture":
          let command = Command(action: "Take", object: "Picture", intent: intent.intent)
          sendToDevice(command, subtopic: "camera")
        default:
          print("No such command")
          return
        }
      }
    }
  }

连接 Watson IoT Platform 服务

本节将介绍如何设置与 Watson IoT Platform 服务的连接。

准备 Xcode 项目

  1. 将以下代码添加到 Podfile 来安装 CocoaMQTT,后者使用了 MQTT 协议并提供了一个实用的 Swift SDK 来与 Watson IoT Platform 服务通信。另外,添加 SwiftyJSON 来解析和格式化 JSON 消息。
    pod 'CocoaMQTT'
    pod 'SwiftyJSON'
  2. 运行下面的命令来安装 CocoaMQTT 和 SwiftyJSON 依赖项。
    pod install

初始化

  1. 在 AppDelegate.swift 文件中声明一个全局变量,因为 MQTT 连接必须在应用程序级别上进行处理,而不是在视图级别上。使用主机名、端口和客户端 ID 初始化 MQTT 客户端。
    let mqttClient = CocoaMQTT(clientID: Credentials.WatsonIOTClientID, 
    host: Credentials.WatsonIOTHost, port: UInt16(Credentials.WatsonIOTPort))
    • host name:[ORG_ID].messaging.internetofthings.ibmcloud.com
    • port:1883
    • client ID:a:[ORG_ID]:[API_KEY]
  2. 创建一个名为 MqttExt.swift 的文件,以包含连接 Watson IoT Platform 服务的代码。为 ViewController 声明一个扩展并实现 CocoaMQTTDelegate
    extension ViewController : CocoaMQTTDelegate {
    }
  3. 将用户名初始化为 [API_KEY],将密码初始化为 [AUTH_TOKEN],并将它自己设置为代理。
    mqttClient.username = Credentials.WatsonIOTUsername
    mqttClient.password = Credentials.WatsonIOTPassword
    mqttClient.keepAlive = 60
    mqttClient.delegate = self
  4. 在应用程序激活时建立连接。
    func applicationDidBecomeActive(_ application: UIApplication) {
        mqttClient.connect()
      }
  5. 在应用程序进入后台时断开连接。
    func applicationDidEnterBackground(_ application: UIApplication) {
        mqttClient.disconnect()
       }

实现 MQTT 回调

  1. 实现下面的回调来接收连接成功事件。连接后,调用该回调来订阅该事件主题,主题的格式为 iot-2/type/[DEV_TYPE]/id/[DEV_ID]/evt/+/fmt/json,其中 + 是通配符。
    func mqtt(_ mqtt: CocoaMQTT, didConnect host: String, port: Int) {
        mqttClient.subscribe(eventTopic)
    }
  2. 实现 didReceiveMessage() 回调函数来处理来自设备的事件。如果该函数从相机收到一个状态,则从 Object Storage 服务下载该照片。否则,如果该函数从 light 收到一个状态,则向对话附加一条消息。
    func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16) {
        var json : JSON = JSON.null
        if let message = message.string {
          json = JSON.init(parseJSON: message)
        } else {
          return  // do nothing
        }
        let cameraTopic = "iot-2/type/\(Credentials.DevType)/id/\(Credentials.DevId)/evt/camera/fmt/json"
        let lightTopic = "iot-2/type/\(Credentials.DevType)/id/\(Credentials.DevId)/evt/light/fmt/json"
        switch message.topic {
        case cameraTopic:
          //ObjectStorage
          if let objectname = json["d"]["objectname"].string {
            if let containername = json["d"]["containername"].string {
              self.downloadPictureFromObjectStorage(containername: containername, objectname: objectname)
            }
          }
        case lightTopic:
          if let status = json["d"]["status"].string {
            switch status {
            case "on":
              self.didReceiveConversationResponse(["Light is on"])
            case "off":
              self.didReceiveConversationResponse(["Light is off"])
            default:
              break
            }
          }
        default:
          break
        }
      }

向设备发送命令

  1. 实现此函数来向设备发送命令。主题的格式为 iot-2/type/[DEV_TYPE]/id/[DEV_ID]/cmd/[light|camera]/fmt/json
    func sendToDevice(_ command: Command, subtopic: String) {
        if let json = command.toJSON() {
          let topic = "iot-2/type/\(Credentials.DevType)/id/\(Credentials.DevId)/cmd/\(subtopic)/fmt/json"
          let message = CocoaMQTTMessage(topic: topic, string: json)
          print("publish message \(json)")
          mqttClient.publish(message)
        }
      }

利用 Object Storage 服务检索图像

要利用 Object Storage 服务检索图像,必须准备好您的 Xcode 项目,然后初始化它。

准备 Xcode 项目

  1. 将以下代码添加到 Podfile 来安装 BluemixObjectStorage 框架。
    pod 'BluemixObjectStorage'
  2. 运行下面的命令来安装 BluemixObjectStorage 依赖项。
    pod install

初始化

  1. 创建一个名为 ObjectStorageExt.swift 的文件,以包含连接 Object Storage 服务的代码并为 ViewController 声明一个扩展。
    extension ViewController {
    }
  2. 使用之前保存的 [OS_PROJECTID][OS_USERNAME][OS_PASSWORD] 初始化该服务。
    self.objectStorage = ObjectStorage(projectId: Credentials.ObjectStorageProjectId)
        objectStorage.connect(userId: Credentials.ObjectStorageUserId,
                           password: Credentials.ObjectStoragePassword,
                           region: ObjectStorage.Region.Dallas) {
                              error in
                              if let error = error {
                                print("objectstorage connect error :: \(error)")
                              } else {
                                print("objectstorage connect success")
                              }
        }
  3. 实现 downloadPictureFromObjectStorage() 函数来下载 Node-RED 应用程序拍摄并上传的照片。
    func downloadPictureFromObjectStorage(containername: String, objectname: String) {
        self.objectStorage.retrieve(container: containername) {
          error, container in
          if let error = error {
            print("retrieve container error :: \(error)")
          } else if let container = container {
            container.retrieve(object: objectname) {
              error, object in
              if let error = error {
                print("retrieve object error :: \(error)")
              } else if let object = object {
                print("retrieve object success :: \(object.name)")
                guard let data = object.data else {
                  return
                }
                if let image = UIImage(data: data) {
                  self.addPicture(image)
                  self.didReceiveConversationResponse(["Picture taken"])
                }
              } else {
                print("retrieve object exception")
              }
            }
          } else {
            print("retrieve container exception")
          }
        }
      }

结束语

本教程介绍了一个移动应用程序如何使用 Watson Conversation、Text to Speech 和 Speech to Text 服务来理解用户命令。然后通过 Watson IoT Platform 服务使用这些破译的命令来控制设备。教程中还解释了如何集成 Raspberry Pi 作为家庭网关,从移动应用程序接收命令并向其发送事件。最后,介绍了如何使用 Object Storage 服务存储图像。

感谢 Chung Kit Chan 帮助评审本教程。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=认知计算, 物联网, 移动开发
ArticleID=1047813
ArticleTitle=使用 Watson 和 IoT Platform 服务构建家庭助理移动应用程序
publish-date=07192017