Na primeira parte deste artigo, fizemos toda a preparação do nosso cenário. Preparamos o Watson no Bluemix, importamos o Watson Conversation Service, preparamos os serviços de Object Storage no Bluemix, o serviço Watson IoT Platform e o Raspberry Pi. Também desenvolvemos o Node-RED no Raspberry Pi.
Agora, na sequência, faremos o desenvolvimento final do aplicativo móvel. Veremos como combinar o poder do Watson com a simplicidade da plataforma IBM IoT para criar um home assistant, que controlará alguns dispositivos eletrônicos básicos. Depois, você pode usar este tutorial como um guia para construir seu próprio aplicativo.
7. Desenvolva o aplicativo móvel
Como este aplicativo é semelhante a uma interface de mensagens de bate-papo, usei um widget de interface de usuário iOS popular chamado JSQMessagesViewController. Existe um código de exemplo semelhante que eu poderia usar, mas está escrito no Objective C. No entanto, ainda é uma boa referência para usar.
Desenvolver a interface do usuário
Prepare o projeto Xcode
No Xcode, crie um Aplicativo de Visualização Única clicando em File > Project. Selecione Single View Application e especifique o Nome do Produto: Home Assistant.
Vá para a pasta do projeto e inicialize o CocoaPods. Um Podfile é gerado.
pod init
Adicione esta linha ao Podfile para instalar o widget JSQMessagesViewController.
pod 'JSQMessagesViewController'
Execute o comando para instalar as dependências JSQMessagesViewController. UmA ÁREA de trabalho Xcode é geradA. Você deve reabrir o Xcode usando a Área de Trabalho (*.wcworkspace) em vez do Projeto (*.xcodeproj).
pod install
No arquivo ViewController.swift, importe o módulo JSQMessagesViewController e altere a classe ViewController para herdar da classe JSQMessagesViewController. Declare a série de mensagens para manter as mensagens de bate-papo.
import JSQMessagesViewController class ViewController: JSQMessagesViewController { var messages = [JSQMessage]() }
Finalmente, digite o seguinte código em Info.plist para ativar o microfone.
<key>NSMicrophoneUsageDescription</key> <string>Need microphone to talk to Watson</string><
Configure a interface do usuário e crie uma extensão
Para configurar a interface do usuário e criar uma extensão:
Crie um arquivo chamado UIExt.swift para conter toda a lógica relacionada à UI como uma extensão Swift. Então, declare uma extensão.
extension ViewController { }
Crie uma função SetupUI() que:
- Inicializa o title, senderId e senderDisplayName.
- Cria um botão de microfone e registra eventos touchDown e touchUpInside para funções de retorno callback.
- Registra um item de menu (sintetiza para a função 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() }
Adicione o ícone do microfone como Ativo/Assets. Clique em Assets.xcassets e arraste os arquivos de ícone para a área de trabalho para criar os conjuntos de imagens.
Implementar funções de gerenciamento para a interface do usuário
No arquivo UIExt.swift:
Adicione as seguintes funções para lidar com o evento de pressionamento do botão do microfone. Quando você pressiona este botão para falar um comando, ele começa a transmitir para o serviço Speech to Text do Watson.
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() }
Adicione as seguintes funções para lidar com o evento de liberação do botão do microfone, que interrompe a transmissão para o serviço Speech to Text do Watson.
func didReleaseMicrophoneButton(sender: UIButton){ let microphoneImage = UIImage(named:"microphone")! sender.setImage(microphoneImage, for: .normal) // speech-to-text stop streaming self.sttStopStreaming() }
Faça um override da função didPressSend() para manipular quando o botão enviar é pressionado. O código adiciona a mensagem à matriz de mensagens, envia uma requisição e recebe uma resposta do serviço Conversation do Watson.
override func didPressSend( _ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) { send(text) }
Substituir as funções de retorno de chamada/callback da UI
No arquivo UIExt.swift, adicione as seguintes funções:
Faça um override do callback collectionView| Função numberOfItemsInSection para retornar o número de contagem de mensagens na matriz.
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.messages.count }
Faça um override do callback collectionView | Função cellForItemAt que define o texto e processa a cor de uma célula específica no caminho do índice.
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 }
Faça um override do callback collectionView | Função messageBubbleImageDataForItemAt para retornar se é uma bolha de entrada ou de saída para uma célula específica no caminho do índice.
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 } }
Faça um override do callback collectionView | Função atributedTextForMessageBubbleTopLabelAt para definir e retornar o nome do remetente como o rótulo em cima da bolha da mensagem, ao lado dos atributos de estilo.
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 } }
Faça um override do callback collectionView | Função HeightForMessageBubbleTopLabelAt para retornar a altura do rótulo do texto.
override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForMessageBubbleTopLabelAt indexPath: IndexPath!) -> CGFloat { if let _ = firstMessage(at: indexPath) { return kJSQMessagesCollectionViewCellLabelHeightDefault } else { return 0.0 } }
Funções de utilidade
Finalmente, também no arquivo UIExt.swift, adicione as seguintes funções:
A função send() reproduz um som, acrescenta a mensagem enviada à matriz de mensagens, chama a função finishSendingMessage() e chama a função conversationRequestResponse() que lida com a interação com o serviço Conversation do Watson.
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) }
A função firstMessage() retorna a primeira mensagem que não é anterior ao mesmo remetente para indicar o remetente na parte superior da bolha de texto.
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 }
A função didReceiveConversationResponse() é chamada quando o Conversation do Watson retorna uma resposta. A função reproduz um som, acrescenta a mensagem à matriz de mensagens e envia a resposta ao serviço Text to Speech do Watson para sintetizar a frase. Finalmente, ele chama a função 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) } }
Veja o arquivo UIExt.swift para a implementação completa (incluído nos arquivos em Obter o código/Get the code)
Interface com os Serviços Watson
Agora é hora de implementar a interface com o Serviço do Watson/Watson Service.
Prepare o SDK do Watson
Instale o SDK do Watson Swift. Você deve usar o Carthage para fazer isso e uma maneira de instalar o Carthage é usando Homebrew (um gerenciador de pacotes para MacOS).
Instale o Homebrew.
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
Instale Carthage.
brew update brew install carthage
Crie um Cartfile na pasta do projeto e adicione a seguinte linha.
github "https://github.com/watson-developer-cloud/swift-sdk"
Instale o framework do Swift SDK.
carthage update --platform iOS
Se você receber um erro semelhante ao:
Module compiled with Swift 3.1 cannot be imported in Swift 3.0.2
Em seguida, recompile os binários com este comando:
carthage update --platform iOS --no-use-binaries
Adicione o framework do Swift SDK ao projeto, navegando para General > Linked Frameworks > Libraries. Clique no ícone +.
Clique em Add Other e navegue até a pasta Carthage/Build/iOS. Selecione os frameworks: TextToSpeechV1.framework, SpeechToTextV1.framework, ConversationV1.framework e RestKit.framework.
Quando você terminar, você deve ter os seguintes frameworks adicionados.
Copie os frameworks em seu aplicativo para torná-los acessíveis em tempo de execução. Vá para a guia Build Phases, clique no ícone + e selecione New Run Script Phase.
No Script de Execução, especifique:
/usr/local/bin/carthage copy-frameworks
Especifique os seguintes Arquivos de Entrada clicando no ícone +.
$(SRCROOT)/Carthage/Build/iOS/TextToSpeechV1.framework $(SRCROOT)/Carthage/Build/iOS/SpeechToTextV1.framework $(SRCROOT)/Carthage/Build/iOS/ConversationV1.framework $(SRCROOT)/Carthage/Build/iOS/RestKit.framew
Finalmente, insira as seguintes linhas no arquivo Info.plist como código fonte para se conectar à IoT Plataform do Watson.
<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>
Inicialização
Crie um arquivo chamado WatsonExt.swift para conter o código que interage com os serviços Text to Speech, Speech to Text e Conversation do Watson. Declare uma extensão para o ViewController.
Para inicializar o serviço Text to Speech do Watson, obtenha o nome de usuário e a senha das Credenciais de Serviço do Bluemix.
textToSpeech = TextToSpeech(username: Credentials.TextToSpeechUsername, password: Credentials.TextToSpeechPassword)
Para inicializar o serviço Speech to Text do Watson, obtenha o nome de usuário e a senha das Credenciais do Serviço Bluemix.
speechToTextSession = SpeechToTextSession(username: Credentials.SpeechToTextUsername, password: Credentials.SpeechToTextPassword)
Adicione um callback para lidar com os resultados de speech-to-text.
speechToTextSession?.onResults = onResults
Finalmente, para inicializar o serviço Conversation do Watson, obtenha o nome de usuário e a senha das Credenciais do Serviço Bluemix. Além disso, você precisa do WORKSPACE_ID. Na primeira conexão, o Serviço Conversation define um diálogo conversation_start que retorna a resposta inicial (geralmente configurada como saudação). Portanto, a função didReceiveConversationResponse() é chamada para anexar a mensagem e usar o serviço Text to Speech para sintetizar o texto. Isso salva o contexto, que será usado para uma interação subsequente.
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 }
Serviço Speech to Text do Watson
A função sttStartStreaming() é usada para se conectar ao serviço Speech to Text e pode iniciar a requisição de transmissão. Ela também inicia a entrada do microfone. A função é chamada quando um usuário pressiona o botão do microfone.
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() }
A função sttStopStreaming() é usada para parar o serviço Speech-to-Text. Ela também para a entrada do microfone. A função é chamada quando um usuário libera o botão do microfone.
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() }
A função callback onResults() atualiza o widget de visualização de texto com base no melhor resultado de transcrição do serviço Speech to Text.
func onResults(results: SpeechRecognitionResults) { self.inputToolbar.contentView.textView.text = results.bestTranscript self.inputToolbar.toggleSendButtonEnabled() }
Text to Speech do Watson
A função ttsSynthesize() é usada para sintetizar uma fala baseada em uma frase, usando o serviço Text to Speech. Ela reproduz os dados de áudio sintetizados.
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() } }
Conversação de Watson
A função conversationRequestResponse() lida com a interação com o serviço Conversation. Ela envia a requisição (em texto) e recebe uma resposta do serviço Conversation. Ela, então, chama a função didReceiveConversationResponse() para lidar com o texto da resposta. Finalmente, ela chama a função 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) } }
A função issueCommand() decifra essa intenção do serviço Conversation e envia o comando para o Raspberry Pi através do serviço IoT Platform do Watson.
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 } } } }
Interface com os serviços da IoT Plataform do Watson
Esta seção explica como você configura a interface com os serviços da IoT Platform do Watson.
Prepare o projeto Xcode
Adicione o seguinte código ao Podfile para instalar o CocoaMQTT, que usa um protocolo chamado MQTT e fornece um bom Swift SDK para se comunicar com os serviços da IoT Platform do Watson. Além disso, adicione SwiftyJSON para analisar e formatar mensagens JSON.
pod 'CocoaMQTT' pod 'SwiftyJSON'
Execute o comando para instalar as dependências CocoaMQTT e SwiftyJSON.
pod install
Inicialização
Declare uma variável global no arquivo AppDelegate.swift porque a conexão MQTT deve ser tratada no nível do aplicativo e não no nível de exibição. O cliente MQTT é inicializado com o nome do host, a porta e o ID do cliente.
let mqttClient = CocoaMQTT(clientID: Credentials.WatsonIOTClientID, host: Credentials.WatsonIOTHost, port: UInt16(Credentials.WatsonIOTPort))
- nome do host: [ORG_ID] .messaging.internetofthings.ibmcloud.com
- porta: 1883
- ID do cliente: a: [ORG_ID]: [API_KEY]
Crie um arquivo chamado MqttExt.swift para conter o código que interage com o serviço IoT Platform do Watson. Declare uma extensão para o ViewController e implemente o CocoaMQTTDelegate.
extension ViewController : CocoaMQTTDelegate { }
Inicialize o nome do usuário com [API_KEY], a senha com [AUTH_TOKEN] e configure-se como o delegado/delegate.
mqttClient.username = Credentials.WatsonIOTUsername mqttClient.password = Credentials.WatsonIOTPassword mqttClient.keepAlive = 60 mqttClient.delegate = self
Conecte-se quando o aplicativo tornar-se ativo.
func applicationDidBecomeActive(_ application: UIApplication) { mqttClient.connect() }
Desconecte quando o aplicativo entrar no plano de fundo.
func applicationDidEnterBackground(_ application: UIApplication) { mqttClient.disconnect() }
Implemente callbacks de MQTT
Implemente a seguinte callback para receber um evento bem-sucedido de conexão. Quando conectado, ligue para se inscrever no tópico do evento, que está no formato 2/type/[DEV_TYPE]/id/[DEV_ID]/evt/+/fmt/json, onde + é um curinga.
func mqtt(_ mqtt: CocoaMQTT, didConnect host: String, port: Int) { mqttClient.subscribe(eventTopic) }
Implemente a função de callback didReceiveMessage() para lidar com eventos do dispositivo. Se a função recebe um status da câmera, baixe a imagem do serviço de Armazenamento de Objetos/ Object Storage. Caso contrário, se a função recebe um status da luz, anexe uma mensagem à conversa.
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 } }
Enviar um comando para o dispositivo
Implemente esta função para enviar um comando para o dispositivo. O formato do tópico é 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) } }
Recupere imagem dos serviços de Armazenamento de Objetos/ Object Storage
Para recuperar a imagem do serviço de Armazenamento de Objetos, você deve preparar seu projeto Xcode e, então, inicializá-lo.
Prepare o projeto Xcode
Adicione o seguinte código ao Podfile para instalar o framework BluemixObjectStorage.
pod 'BluemixObjectStorage'
Execute o comando para instalar as dependências BluemixObjectStorage.
pod install
Inicialização
Crie um arquivo chamado ObjectStorageExt.swift para conter o código que interage com o serviço de Object Storage e declare uma extensão para ViewController.
extension ViewController { }
Inicialize o serviço com [OS_PROJECTID], [OS_USERNAME] e [OS_PASSWORD], que você salvou anteriormente.
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") } }
Implemente a função downloadPictureFromObjectStorage() para baixar a foto, que é tirada e carregada pelo aplicativo 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") } } }
Conclusão
O tutorial mostrou como um aplicativo móvel pode usar os serviços Conversation, Text to Speech e Speech to Text do Watson para entender os comandos do usuário. Esses comandos decifrados são, então, usados para controlar dispositivos através do serviço IoT Platform do Watson. O tutorial também explicou como integrar um Raspberry Pi como um gateway doméstico para receber comandos e enviar eventos do e para o aplicativo móvel. Finalmente, ele mostra como armazenar imagens usando o serviço Object Storage.
Agradeço a Chung Kit Chan por sua ajuda na revisão deste tutorial.
***
Khong Kok Sing faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://www.ibm.com/developerworks/mobile/library/cc-build-home-assistant-watson-iot-platform/