diff options
author | Naveen Bobbili <nbobbili@amazon.com> | 2018-11-12 16:12:38 -0800 |
---|---|---|
committer | Naveen Bobbili <nbobbili@amazon.com> | 2018-11-13 15:05:41 -0800 |
commit | b6abca2edcb36c0c0848d1cd8dc291f23293aa80 (patch) | |
tree | a838812e0b66f0695cb6cf0f8bebfa38315ce8b8 /htdocs | |
parent | be70712f89eacd20dca413bcce46e4aa26b5709e (diff) |
SPEC-1924: AGL Speech Framework's Voice Service High Level 1.0 Release.
Details:
1) Control plugin implementation for VSHL 1.0
2) Exposed APIs that are documented in the confluence page
https://confluence.automotivelinux.org/display/SPE/Speech+EG+Architecture
3) Implemented 39 unit tests based on GTest framework to test all the low
level components of VSHL binding.
4) Implemented a HTML5 based VSHL API tester application to test VSHL APIs.
API specification:
https://confluence.automotivelinux.org/display/SPE/Speech+EG+Architecture#SpeechEGArchitecture-HighLevelVoiceService
Test performed:
1) Tested AGL service running Alexa Auto SDK https://github.com/alexa/aac-sdk on Ubuntu 16.04 and Renesas R-Car M3 board.
License:
Apache 2.0
Developers/Owners:
Naveen Bobbili (nbobbili@amazon.com)
Prakash Buddhiraja (buddhip@amazon.com)
Shotaro Uchida (shotaru@amazon.co.jp)
Change-Id: I3370f4ad65aff030f24f4ad571fb02d525bbfbca
Signed-off-by: Naveen Bobbili <nbobbili@amazon.com>
Diffstat (limited to 'htdocs')
-rw-r--r-- | htdocs/AFB-websock.js | 177 | ||||
-rw-r--r-- | htdocs/CMakeLists.txt | 33 | ||||
-rw-r--r-- | htdocs/binding.css | 99 | ||||
-rw-r--r-- | htdocs/binding.js | 226 | ||||
-rw-r--r-- | htdocs/index.html | 95 |
5 files changed, 630 insertions, 0 deletions
diff --git a/htdocs/AFB-websock.js b/htdocs/AFB-websock.js new file mode 100644 index 0000000..3d4831f --- /dev/null +++ b/htdocs/AFB-websock.js @@ -0,0 +1,177 @@ +var urlWS; +var urlhttp; + +AFB = function(base, initialtoken){ + +urlWS = "ws://"+window.location.host+"/"+base; +urlhttp = "http://"+window.location.host+"/"+base; + +/*********************************************/ +/**** ****/ +/**** AFB_context ****/ +/**** ****/ +/*********************************************/ +var AFB_context; +{ + var UUID = undefined; + var TOKEN = initialtoken; + + var context = function(token, uuid) { + this.token = token; + this.uuid = uuid; + } + + context.prototype = { + get token() {return TOKEN;}, + set token(tok) {if(tok) TOKEN=tok;}, + get uuid() {return UUID;}, + set uuid(id) {if(id) UUID=id;} + }; + + AFB_context = new context(); +} +/*********************************************/ +/**** ****/ +/**** AFB_websocket ****/ +/**** ****/ +/*********************************************/ +var AFB_websocket; +{ + var CALL = 2; + var RETOK = 3; + var RETERR = 4; + var EVENT = 5; + + var PROTO1 = "x-afb-ws-json1"; + + AFB_websocket = function(onopen, onabort) { + var u = urlWS; + if (AFB_context.token) { + u = u + '?x-afb-token=' + AFB_context.token; + if (AFB_context.uuid) + u = u + '&x-afb-uuid=' + AFB_context.uuid; + } + this.ws = new WebSocket(u, [ PROTO1 ]); + this.pendings = {}; + this.awaitens = {}; + this.counter = 0; + this.ws.onopen = onopen.bind(this); + this.ws.onerror = onerror.bind(this); + this.ws.onclose = onclose.bind(this); + this.ws.onmessage = onmessage.bind(this); + this.onopen = onopen; + this.onabort = onabort; + this.onclose = onabort; + } + + function onerror(event) { + var f = this.onabort; + if (f) { + delete this.onopen; + delete this.onabort; + f && f(this); + } + this.onerror && this.onerror(this); + } + + function onopen(event) { + var f = this.onopen; + delete this.onopen; + delete this.onabort; + f && f(this); + } + + function onclose(event) { + for (var id in this.pendings) { + var ferr = this.pendings[id].onerror; + ferr && ferr(null, this); + } + this.pendings = {}; + this.onclose && this.onclose(); + } + + function fire(awaitens, name, data) { + var a = awaitens[name]; + if (a) + a.forEach(function(handler){handler(data);}); + var i = name.indexOf("/"); + if (i >= 0) { + a = awaitens[name.substring(0,i)]; + if (a) + a.forEach(function(handler){handler(data);}); + } + a = awaitens["*"]; + if (a) + a.forEach(function(handler){handler(data);}); + } + + function reply(pendings, id, ans, offset) { + if (id in pendings) { + var p = pendings[id]; + delete pendings[id]; + var f = p[offset]; + f(ans); + } + } + + function onmessage(event) { + var obj = JSON.parse(event.data); + var code = obj[0]; + var id = obj[1]; + var ans = obj[2]; + AFB_context.token = obj[3]; + switch (code) { + case RETOK: + reply(this.pendings, id, ans, 0); + break; + case RETERR: + reply(this.pendings, id, ans, 1); + break; + case EVENT: + default: + fire(this.awaitens, id, ans); + break; + } + } + + function close() { + this.ws.close(); + this.onabort(); + } + + function call(method, request) { + return new Promise((function(resolve, reject){ + var id, arr; + do { + id = String(this.counter = 4095 & (this.counter + 1)); + } while (id in this.pendings); + this.pendings[id] = [ resolve, reject ]; + arr = [CALL, id, method, request ]; + if (AFB_context.token) arr.push(AFB_context.token); + this.ws.send(JSON.stringify(arr)); + }).bind(this)); + } + + function onevent(name, handler) { + var id = name; + var list = this.awaitens[id] || (this.awaitens[id] = []); + list.push(handler); + } + + AFB_websocket.prototype = { + close: close, + call: call, + onevent: onevent + }; +} +/*********************************************/ +/**** ****/ +/**** ****/ +/**** ****/ +/*********************************************/ +return { + context: AFB_context, + ws: AFB_websocket +}; +}; + diff --git a/htdocs/CMakeLists.txt b/htdocs/CMakeLists.txt new file mode 100644 index 0000000..7aa0ce1 --- /dev/null +++ b/htdocs/CMakeLists.txt @@ -0,0 +1,33 @@ +########################################################################### +# Copyright 2015, 2016, 2017 IoT.bzh +# +# author: Fulup Ar Foll <fulup@iot.bzh> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################### + + + +################################################## +# HTML Testing Files +################################################## +PROJECT_TARGET_ADD(htdocs) + + file(GLOB SOURCE_FILES "*.html" "*.js" "*.jpg" "*.css" "assets") + + add_input_files("${SOURCE_FILES}") + + SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES + LABELS "HTDOCS" + OUTPUT_NAME ${TARGET_NAME} + ) diff --git a/htdocs/binding.css b/htdocs/binding.css new file mode 100644 index 0000000..99f84b4 --- /dev/null +++ b/htdocs/binding.css @@ -0,0 +1,99 @@ +body.page-content { + height: 100%; + width: auto; + margin-top: 20px; + background-size: cover; + background-position: center; +} + +img { + float: right; +} + +ol { + display: flex; + flex-direction: column; +} + +#question, +#output, +#outevt { + white-space: pre-wrap; +} + +div.row { + display: flex; + flex-direction: row; +} + +div.col1 { + flex-basis: 0; + flex-grow: 1; + width: 100%; +} + +div.col2 { + flex-basis: 0; + flex-grow: 1; + width: 30ch; +} + +button { + margin-right: 10px; + padding: 6px 8px; + font-size: large; +} + +pre { + outline: 1px solid #ccc; + padding: 5px; + margin: 5px; + background-color: white; + opacity: 0.85; + min-height: 5pc; + max-height: 5pc; + overflow: auto; +} + +.string { + color: green; +} + +.number { + color: darkorange; +} + +.boolean { + color: blue; +} + +.null { + color: magenta; +} + +.key { + color: red; +} + +dialog { + padding: 0; + border: 0; + border-radius: 0.6rem; + box-shadow: 0 0 1em black; +} + +dialog::backdrop { + /* make the backdrop a semi-transparent black */ + background-color: rgba(0, 0, 0, 0.4); +} + +h3.dialogheader { + background-color: rgb(177, 177, 236); + padding: 1ch; +} + +footer { + display: flex; + align-items: center; + justify-content: center; +}
\ No newline at end of file diff --git a/htdocs/binding.js b/htdocs/binding.js new file mode 100644 index 0000000..c24d62e --- /dev/null +++ b/htdocs/binding.js @@ -0,0 +1,226 @@ +var afb = new AFB("api", "mysecret"); +var ws; +var evtIdx = 0; +var count = 0; + + +//********************************************** +// Logger +//********************************************** +var log = { + command: function (api, verb, query) { + console.log("subscribe api=" + api + " verb=" + verb + " query=", query); + var question = urlWS + "/" + api + "/" + verb + "?query=" + JSON.stringify(query); + log._write("question", count + ": " + log.syntaxHighlight(question)); + }, + + event: function (obj) { + console.log("gotevent:" + JSON.stringify(obj)); + log._write("outevt", (evtIdx++) + ": " + JSON.stringify(obj)); + }, + + reply: function (obj) { + console.log("replyok:" + JSON.stringify(obj)); + log._write("output", count + ": OK: " + log.syntaxHighlight(obj)); + }, + + error: function (obj) { + console.log("replyerr:" + JSON.stringify(obj)); + log._write("output", count + ": ERROR: " + log.syntaxHighlight(obj)); + }, + + _write: function (element, msg) { + var el = document.getElementById(element); + el.innerHTML += msg + '\n'; + + // auto scroll down + setTimeout(function () { + el.scrollTop = el.scrollHeight; + }, 100); + }, + + syntaxHighlight: function (json) { + if (typeof json !== 'string') { + json = JSON.stringify(json, undefined, 2); + } + json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); + return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { + var cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return '<span class="' + cls + '">' + match + '</span>'; + }); + }, +}; + +//********************************************** +// Generic function to call binder +//*********************************************** +function callbinder(api, verb, query) { + log.command(api, verb, query); + + // ws.call return a Promise + return ws.call(api + '/' + verb, query) + .then(function (res) { + log.reply(res); + count++; + return res; + }) + .catch(function (err) { + log.reply(err); + count++; + throw err; + }); +}; + +//********************************************** +// Init - establish Websocket connection +//********************************************** +function init(elemID, api, verb, query) { + + function onopen() { + document.getElementById("main").style.visibility = "visible"; + document.getElementById("connected").innerHTML = "Binder WS Active"; + document.getElementById("connected").style.background = "lightgreen"; + ws.onevent("*", log.event); + } + + function onabort() { + document.getElementById("main").style.visibility = "hidden"; + document.getElementById("connected").innerHTML = "Connected Closed"; + document.getElementById("connected").style.background = "red"; + } + + ws = new afb.ws(onopen, onabort); +} + +function clearPre(preId) { + const pre = document.getElementById(preId); + while (pre && pre.firstChild) { + pre.removeChild(pre.firstChild); + } +} + +function fetchAndRenderVoiceAgents() { + const agentsDiv = document.getElementById('agentsDiv'); + while (agentsDiv.firstChild) { + agentsDiv.removeChild(agentsDiv.firstChild); + } + + const api = 'vshl'; + const verb = 'enumerateVoiceAgents'; + const query = {}; + + log.command(api, verb, query); + + return ws.call(api + '/' + verb, query) + .then(function (res) { + log.reply(res); + for (let index = 0; index < res.response.agents.length; ++index) { + let voiceAgent = res.response.agents[index]; + addVoiceAgent(agentsDiv, voiceAgent, res.response.default == voiceAgent.id); + } + }) + .catch(function (err) { + log.reply(err); + console.log(JSON.stringify(err)); + }); +} + +function addVoiceAgent(containerDiv, voiceAgent, isDefault) { + const agentDiv = document.createElement("div"); + + const agentName = document.createElement("h2"); + agentName.innerHTML = voiceAgent.name; + agentDiv.appendChild(agentName); + + const agentDescription = document.createElement("p"); + agentDescription.innerHTML = voiceAgent.description; + agentDiv.appendChild(agentDescription); + + if (!isDefault) { + const setDefaultBtn = document.createElement("button"); + setDefaultBtn.addEventListener('click', (evt) => { + const query = {"id": voiceAgent.id}; + callbinder('vshl', 'setDefaultVoiceAgent', query); + fetchAndRenderVoiceAgents(); + }); + setDefaultBtn.innerHTML = 'SetDefault'; + agentDiv.appendChild(setDefaultBtn); + } + + const subscribeBtn = document.createElement("button"); + subscribeBtn.addEventListener('click', (evt) => { + showAgentEventChooserDialog(voiceAgent.id); + }); + subscribeBtn.innerHTML = 'Subscribe'; + agentDiv.appendChild(subscribeBtn); + + containerDiv.appendChild(agentDiv); +} + +function showAgentEventChooserDialog(voiceAgentId) { + const modal = document.getElementById('agent-event-chooser'); + const subscribeBtn = document.getElementById('agent-subscribe-btn'); + + subscribeBtn.addEventListener('click', (evt) => { + const authState = document.getElementById('authstate').checked; + const dialogState = document.getElementById('dialogstate').checked; + const connectionState = document.getElementById('connectionstate').checked; + + const query = { + "va_id": voiceAgentId, + "events":[] + }; + if (authState) + query.events.push('voice_authstate_event'); + if (dialogState) + query.events.push('voice_dialogstate_event'); + if (connectionState) + query.events.push('voice_connectionstate_event'); + + callbinder('vshl', 'subscribe', query); + modal.close(); + }); + + // makes modal appear (adds `open` attribute) + modal.showModal(); +} + +function showTemplateUIEventChooserDialog() { + const modal = document.getElementById('templateui-event-chooser'); + const subscribeBtn = document.getElementById('templateui-subscribe-btn'); + + subscribeBtn.addEventListener('click', (evt) => { + const renderTemplate = document.getElementById('render_template').checked; + const clearTemplate = document.getElementById('clear_template').checked; + const renderPlayerInfo = document.getElementById('render_player_info').checked; + const clearPlayerInfo = document.getElementById('clear_player_info').checked; + + const query = {"actions":[]}; + + if (renderTemplate) + query.actions.push('render_template'); + if (clearTemplate) + query.actions.push('clear_template'); + if (renderPlayerInfo) + query.actions.push('render_player_info'); + if (clearPlayerInfo) + query.actions.push('clear_player_info'); + + callbinder('vshl', 'guiMetadata/subscribe', query); + modal.close(); + }); + + // makes modal appear (adds `open` attribute) + modal.showModal(); +}
\ No newline at end of file diff --git a/htdocs/index.html b/htdocs/index.html new file mode 100644 index 0000000..5a84ee8 --- /dev/null +++ b/htdocs/index.html @@ -0,0 +1,95 @@ +<html> + +<head> + <title>VSHL API Test</title> + <link rel="stylesheet" href="binding.css"> + <script type="text/javascript" src="AFB-websock.js"></script> + <script type="text/javascript" src="binding.js"></script> +</head> + +<body class="page-content" onload="init()"> + + <h1>Voice Service High Level API Tester</h1> + + <button id="connected" onclick="init()">Binder WS Fail</button> + <button id="monitoring" onclick="window.open('/monitoring/monitor.html','_monitor_ctl')">Debug/Monitoring</a> + </button> + <button onclick="clearPre('question'); clearPre('output'); clearPre('outevt');">Clear</button> + + <br> + <br> + + <dialog id="agent-event-chooser"> + <h3 class="dialogheader">Subscribe to the following agent events</h3> + <div> + <ol> + <li> + <input type="checkbox" id="authstate" checked> + <label>voice_authstate_event</label> + </li> + <li> + <input type="checkbox" id="dialogstate" checked> + <label>voice_dialogstate_event</label> + </li> + <li> + <input type="checkbox" id="connectionstate" checked> + <label>voice_connectionstate_event</label> + </li> + </ol> + </div> + <footer> + <button id="agent-subscribe-btn" type="button" style="margin: 10px">Subscribe</button> + </footer> + </dialog> + + <dialog id="templateui-event-chooser"> + <h3 class="dialogheader">Subscribe to the following GUI Metadata Messages</h3> + <div> + <ol> + <li> + <input type="checkbox" id="render_template" checked> + <label>render_template</label> + </li> + <li> + <input type="checkbox" id="clear_template" checked> + <label>clear_template</label> + </li> + <li> + <input type="checkbox" id="render_player_info" checked> + <label>render_player_info</label> + </li> + <li> + <input type="checkbox" id="clear_player_info" checked> + <label>clear_player_info</label> + </li> + </ol> + </div> + <footer> + <button id="templateui-subscribe-btn" type="button" style="margin: 10px">Subscribe</button> + </footer> + </dialog> + + <div id="top" class="row"> + <div id='actions' class="col1"> + <div> + <h2>VSHL APIs</h2> + <p>APIs that are voiceagent agnostic</p> + <button onclick="callbinder('vshl','startListening',{});">startListening</button> + <button onclick="fetchAndRenderVoiceAgents();">enumerateAgents</button> + <button onclick="showTemplateUIEventChooserDialog();">Subscribe to GUI Metadata</button> + </div> + + <div id="agentsDiv"> + </div> + </div> + + <div id="main" style="visibility:hidden" class="col2"> + <ol> + <li>Question <pre id="question"></pre> + <li>Response <pre id="output"></pre> + <li>Events: <pre id="outevt"></pre> + </ol> + </div> + </div> + +</body>
\ No newline at end of file |