diff options
Diffstat (limited to 'htdocs')
-rw-r--r-- | htdocs/AFB.js | 217 | ||||
-rw-r--r-- | htdocs/binding.css | 99 | ||||
-rw-r--r-- | htdocs/binding.js | 276 | ||||
-rw-r--r-- | htdocs/index.html | 142 |
4 files changed, 734 insertions, 0 deletions
diff --git a/htdocs/AFB.js b/htdocs/AFB.js new file mode 100644 index 0000000..2cf3aec --- /dev/null +++ b/htdocs/AFB.js @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2017, 2018 "IoT.bzh" + * Author: José Bollo <jose.bollo@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. + */ +AFB = function(base, initialtoken){ + +if (typeof base != "object") + base = { base: base, token: initialtoken }; + +var initial = { + base: base.base || "api", + token: base.token || initialtoken || "HELLO", + host: base.host || window.location.host, + url: base.url || undefined +}; + +var urlws = initial.url || "ws://"+initial.host+"/"+initial.base; + +/*********************************************/ +/**** ****/ +/**** AFB_context ****/ +/**** ****/ +/*********************************************/ +var AFB_context; +{ + var UUID = undefined; + var TOKEN = initial.token; + + 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(on_open, on_abort) { + var u = urlws, p = '?'; + if (AFB_context.token) { + u = u + '?x-afb-token=' + AFB_context.token; + p = '&'; + } + if (AFB_context.uuid) + u = u + p + 'x-afb-uuid=' + AFB_context.uuid; + this.ws = new WebSocket(u, [ PROTO1 ]); + this.url = u; + 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 = on_open; + this.onabort = on_abort; + } + + function onerror(event) { + var f = this.onabort; + if (f) { + delete this.onopen; + delete this.onabort; + 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) { + var err = { + jtype: 'afb-reply', + request: { + status: 'disconnected', + info: 'server hung up' + } + }; + for (var id in this.pendings) { + try { this.pendings[id][1](err); } catch (x) {/*NOTHING*/} + } + 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]; + try { p[offset](ans); } catch (x) {/*TODO?*/} + } + } + + 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.ws.onopen = + this.ws.onerror = + this.ws.onclose = + this.ws.onmessage = + this.onopen = + this.onabort = function(){}; + } + + function call(method, request, callid) { + return new Promise((function(resolve, reject){ + var id, arr; + if (callid) { + id = String(callid); + if (id in this.pendings) + throw new Error("pending callid("+id+") exists"); + } else { + 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, + url: urlws +}; +}; + diff --git a/htdocs/binding.css b/htdocs/binding.css new file mode 100644 index 0000000..9ee1303 --- /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: lightgreen; + 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..9de9608 --- /dev/null +++ b/htdocs/binding.js @@ -0,0 +1,276 @@ +var afbVshlCapabilities; +var ws; +var evtIdx = 0; +var count = 0; + +//********************************************** +// Logger +//********************************************** +var log = { + command: function (url, api, verb, query) { + console.log("subscribe api=" + api + " verb=" + verb + " query=", query); + var question = url + "/" + 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(url, api, verb, query) { + log.command(url, 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; + }); +}; + + +//********************************************** +// connect - establish Websocket connection +//********************************************** + +function connect(elemID, api, verb, query) { + connectVshlCapabilities(elemID, api, verb, query); +} + +//********************************************** + +// +var lastCallId; + +function onCapabilityEvent(eventDataObj) { + log.event(eventDataObj); + if (eventDataObj.event == "vshl-capabilities/dial") { + lastCallId = JSON.parse(eventDataObj.data).callId; + console.log("New Dial Directive received. Sending ringing state back"); + // send ringing state back. + triggerCallStateChangedAction("OUTBOUND_RINGING"); + } +} + +function triggerPhoneConnectionStateChanged(newState) { + var paramsJson = { + "action" : "connection_state_changed", + "payload": { + "state" : newState + } + } + callbinder(afbVshlCapabilities.url, 'vshl-capabilities', 'phonecontrol/publish', paramsJson); +} + +function triggerCallStateChangedAction(newCallstate) { + var query = { + "action": "call_state_changed", + "payload": { + "callId": lastCallId, + "state": newCallstate + } + } + callbinder(afbVshlCapabilities.url, 'vshl-capabilities', 'phonecontrol/publish', query); +} + +function guid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); +} + +function triggerCallStateInBoundRingingAction() { + var callId = guid(); + var state = "INBOUND_RINGING"; + var query = { + "action": "call_state_changed", + "payload": { + "callId": callId, + "state": state + } + } + callbinder(afbVshlCapabilities.url, 'vshl-capabilities', 'phonecontrol/publish', query); +} + +function connectVshlCapabilities(elemID, api, verb, query) { + + function onopen() { + document.getElementById("main").style.visibility = "visible"; + document.getElementById("connected").innerHTML = "VSHL Capabilities Binder WS Active"; + document.getElementById("connected").style.background = "lightgreen"; + ws.onevent("*", onCapabilityEvent); + } + + function onabort() { + document.getElementById("main").style.visibility = "hidden"; + document.getElementById("connected").innerHTML = "Connected Closed"; + document.getElementById("connected").style.background = "red"; + } + + var urlparams = { + base: "api", + token: "HELLO", + }; + const vshlCapabilitiesAddressInput = document.getElementById('vshl-capabilities-address'); + urlparams.host = vshlCapabilitiesAddressInput.value; + + afbVshlCapabilities = new AFB(urlparams, "HELLO"); + ws = new afbVshlCapabilities.ws(onopen, onabort); +} + +function clearPre(preId) { + const pre = document.getElementById(preId); + while (pre && pre.firstChild) { + pre.removeChild(pre.firstChild); + } +} + +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(afbVshlCapabilities.url, 'vshl-capabilities', 'guiMetadata/subscribe', query); + modal.close(); + }); + + // makes modal appear (adds `open` attribute) + modal.showModal(); +} + +function showPhoneControlEventChooserDialog() { + const modal = document.getElementById('phonecontrol-event-chooser'); + const subscribeBtn = document.getElementById('phonecontrol-subscribe-btn'); + + subscribeBtn.addEventListener('click', (evt) => { + const dial = document.getElementById('phonecontrol-dial').checked; + const redial = document.getElementById('phonecontrol-redial').checked; + const answer = document.getElementById('phonecontrol-answer').checked; + const stop = document.getElementById('phonecontrol-stop').checked; + const sendDtmf = document.getElementById('phonecontrol-send_dtmf').checked; + + const query = {"actions":[]}; + + if (dial) + query.actions.push('dial'); + if (redial) + query.actions.push('redial'); + if (answer) + query.actions.push('answer'); + if (stop) + query.actions.push('stop'); + if (sendDtmf) + query.actions.push('send_dtmf'); + + callbinder(afbVshlCapabilities.url, 'vshl-capabilities', 'phonecontrol/subscribe', query); + modal.close(); + }); + + // makes modal appear (adds `open` attribute) + modal.showModal(); +} + +function showNavigationEventCHooserDialod() { + const modal = document.getElementById('navigation-event-chooser'); + const subscribeBtn = document.getElementById('navigation-subscribe-btn'); + + subscribeBtn.addEventListener('click', (evt) => { + const setDestination = document.getElementById('set_destination').checked; + const cancelNavigation = document.getElementById('cancel_navigation').checked; + + const query = {"actions":[]}; + + if (setDestination) + query.actions.push('set_destination'); + if (cancelNavigation) + query.actions.push('cancel_navigation'); + + callbinder(afbVshlCapabilities.url, 'vshl-capabilities', 'navigation/subscribe', query); + modal.close(); + }); + + // makes modal appear (adds `open` attribute) + modal.showModal(); +} + +function triggerButtonPressedAction(button) { + var paramsJson = { + "action" : "button_pressed", + "payload": { + "button" : button + } + } + callbinder(afbVshlCapabilities.url, 'vshl-capabilities', 'playbackcontroller/publish', paramsJson); +}
\ No newline at end of file diff --git a/htdocs/index.html b/htdocs/index.html new file mode 100644 index 0000000..f322881 --- /dev/null +++ b/htdocs/index.html @@ -0,0 +1,142 @@ +<html> + +<head> + <title>VSHL CAPABILITIES API Test</title> + <link rel="stylesheet" href="binding.css"> + <script type="text/javascript" src="AFB.js"></script> + <script type="text/javascript" src="binding.js"></script> +</head> + +<body class="page-content" onload="connect()"> + + <h1>Voice Service High Level Support 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> + VSHL CAPABILITIES URL: <input type="text" id="vshl-capabilities-address" value="localhost:1111" onchange="connectVshlCapabilities()"> + <br> + <br> + + <div> + <P>Phone Connection Status UI</p> + <button onclick="triggerPhoneConnectionStateChanged('CONNECTED')">Connected</button> + <button onclick="triggerPhoneConnectionStateChanged('DISCONNECTED')">Disconnected</button> + <P>Phone Call Control Inbound Ringing UI</p> + <button onclick="triggerCallStateInBoundRingingAction()">Simulate Inbound Ringing</button> + <P>Phone Call Control UI</p> + <button onclick="triggerCallStateChangedAction('ACTIVE')">Accept</button> + <button onclick="triggerCallStateChangedAction('IDLE')">Reject</button> + </div> + <div> + <p>Playback Controller UI</p> + <button onclick="triggerButtonPressedAction('play')">play</button> + <button onclick="triggerButtonPressedAction('pause')">pause</button> + <button onclick="triggerButtonPressedAction('next')">next</button> + <button onclick="triggerButtonPressedAction('previous')">previous</button> + <button onclick="triggerButtonPressedAction('skip-forward')">skip-forward</button> + <button onclick="triggerButtonPressedAction('skip-backward')">skip-backward</button> + </div> + + <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> + + <dialog id="phonecontrol-event-chooser"> + <h3 class="dialogheader">Subscribe to the following phone control messages</h3> + <div> + <ol> + <li> + <input type="checkbox" id="phonecontrol-dial" checked> + <label>phonecontrol/dial</label> + </li> + <li> + <input type="checkbox" id="phonecontrol-redial" checked> + <label>phonecontrol/redial</label> + </li> + <li> + <input type="checkbox" id="phonecontrol-answer" checked> + <label>phonecontrol/answer</label> + </li> + <li> + <input type="checkbox" id="phonecontrol-stop" checked> + <label>phonecontrol/stop</label> + </li> + <li> + <input type="checkbox" id="phonecontrol-send_dtmf" checked> + <label>phonecontrol/send_dtmf</label> + </li> + </ol> + </div> + <footer> + <button id="phonecontrol-subscribe-btn" type="button" style="margin: 10px">Subscribe</button> + </footer> + </dialog> + + <dialog id="navigation-event-chooser"> + <h3 class="dialogheader">Subscribe to the following navigation messages</h3> + <div> + <ol> + <li> + <input type="checkbox" id="set_destination" checked> + <label>set_destination</label> + </li> + <li> + <input type="checkbox" id="cancel_navigation" checked> + <label>cancel_navigation</label> + </li> + </ol> + </div> + <footer> + <button id="navigation-subscribe-btn" type="button" style="margin: 10px">Subscribe</button> + </footer> + </dialog> + + <div id="top" class="row"> + <div id='actions' class="col1"> + <div> + <h2>VSHL CAPABILITIES APIs</h2> + <p>Speech framework's VSHL Capabilities APIs</p> + <button onclick="showTemplateUIEventChooserDialog();">Subscribe to GUI Metadata</button> + <button onclick="showPhoneControlEventChooserDialog();">Subscribe to Phonecontrol messages</button> + <button onclick="showNavigationEventCHooserDialod();">Subscribe to Navigation messages</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> |