cancel
Showing results for 
Search instead for 
Did you mean: 
cancel
Announcements
Community

Collab

CE9.6.x - In-Room Control and Macros - USB input devices, HTTP POST / PUT and Hiding default UI buttons

3865
Views
75
Helpful
5
Comments
Cisco Employee

Hi everyone!

Its soon Christmas and I have been reviewing the features of the upcoming CE9.6.1 release (currently targeted for early January) and I can tell you that there is a few interesting features that comes with this release! In particular I am talking about custom HTTP POST and PUT requests that will open the door to countless new scenarios that has not been possible before. In combination with USB input devices and hiding of the default UI buttons this release is soaked in integration awesomeness.

HttpClient POST in xAPI and Macros

Feature summary


Allow utilization of the codec HttpClient to do custom HTTP(S) POST and PUT requests on demand via the xAPI or using Macros. The requests can be sent to a third party server for triggering new actions or for data collection.

Feature description and functional overview


Purpose
The purpose of this feature is to allow on-demand data collection or distribution and not rely on webhooks. A webhook (synonymous with the all familiar HttpFeedback in this context) is just the same thing, a HTTP(S) POST coming from the Room Device to some server (for example TMS relies on these webhooks to collect data from the Room Devices). The problem is that a HttpFeedback Webhook is quite specific to what it will send (including structure of the payload) and it also needs to be triggered by an event on the endpoint to fire the request.

 

The difference that comes with HttpClient POST / PUT is that you can send the data whenever you want, you choose what data to send and how to structure it. With that you can adapt the request to an already established service running on your network and not vice versa.

 

Note: For this feature to be useful you should have a service that can take the requests coming from the Room Device, parse, store, process or whatever suits a given use-case. This is what the feature is for: data collection from the Room Device or use as a webhook to trigger other actions or tasks in third-party services... with me so far? 

 

Functional overview

 

Screen Shot 2018-10-18 at 20.19.09.png

Lets take a look at a very high-level diagram, the XMLAPI and the Macros are both channels to utilize the xAPI. Using the added xAPI command HttpClient Post or Put you can send customized data to third-party servers. If you wonder if POST is supported back to the endpoint the answer is partly "yes" and partly "no". "Yes" because it is and has been supported for a long time but currently only via the XMLAPI (/putxml) so this is not new. "No" because the new HttpClient only supports outgoing data and will not have access to incoming response payloads or headers, only the status code and a brief error description, but for now this should not be a big issue.

Getting started!

Now this is currently not yet available so, I am sorry for being a tease, but its something to look forward to and I could not wait to write about this!

 

To get started you must first enable the HttpClient using a xConfiguration (or via the web interface):

 

xConfiguration HttpClient Mode: On

This is required to send request, which means you also can disallow outgoing data for integration users for example.

 

The actual request is performed using a xCommand, lets look at our arsenal:

 

xcommand //httpclient ?
xCommand HttpClient Allow Hostname Add
    Expression(r): <S: 2, 200>
xCommand HttpClient Allow Hostname Clear
xCommand HttpClient Allow Hostname List
xCommand HttpClient Allow Hostname Remove
    Id(r): <0..9>
xCommand HttpClient Post
AllowInsecureHTTPS: <False, True> (default False) Header[N]: <S: 0, 1024> Url(r): <S: 8, 2048> xCommand HttpClient Put
AllowInsecureHTTPS: <False, True> (default False) Header[N]: <S: 0, 1024> Url(r): <S: 8, 2048>

 

There is a few commands here, but lets not rush it to avoid confusion and jump right to the good stuff!

 

Lets try out the HttpClient Post from one endpoint (EP1) to another (EP2) imagining that the XMLAPI on EP2 device is the "Third-party remote HTTP Server" in the diagram above.

 

mohm$ echo admin:1234 | base64
YWRtaW46MTIzNAo=

In order to authenticate to a different endpoint I need to base64 encode the credentials for this example, see above. This is needed for the HttpClient Post "Authorization" Header.

 

The HttpClient Post and Put commands supports up to 10 headers and are issued as follows directly into the xAPI (this is via SSH):

 

xCommand HttpClient Post Header: "Authorization: Basic YWRtaW46MTIzNAo=" Header: "Content-Type: text/xml" Url: http://codec-ip/putxml

The above example should give you an idea of how you issue multiple headers in the xAPI. For each header you need a new "Header: "Header1" Header: "Header2" etc.. ". Usually it is enough with a Content-Type and an Authorization header but that depends on the server.

 

So the next part is the data, the actual payload that we will send to the remote server. You need to send some data that will be posted to the third party server (and constructed in a way so that the server can parse it). When you hit enter on the above command, nothing seems to happen - well duh, its a multi line command. Now that we have the authorization and data type in the headers the command expects that you insert whatever data you are going to send. Below is how it looks like after I have issued the command:

 

xCommand HttpClient Post Header: "Authorization: Basic YWRtaW46MTIzNAo=" Header: "Content-Type: text/xml" Url: http://codec-ip/putxml
<Configuration><SystemUnit><Name>Hello World!</Name></SystemUnit></Configuration>
. OK *r HttpClientPostResult (status=OK): *r HttpClientPostResult StatusCode: 200 ** end

See description of the above below:

 

Command
xCommand HttpClient Post Header: "Authorization: Basic YWRtaW46MTIzNAo=" Header: "Content-Type: text/xml" Url: http://10.47.97.195/putxml <enter>
Data (Payload) - The data you type in here should really match the Content-Type Header, which in this case is text/xml
<Configuration><SystemUnit><Name>Hello World!</Name></SystemUnit></Configuration> <enter> End input
. <enter> OK *r HttpClientPostResult (status=OK): *r HttpClientPostResult StatusCode: 200 ** end

The xAPI will output the StatusCode only. If the server returns data with the request it will not be displayed, which is  the biggest limitation of this feature.

 

Now that we know that the HttpClient Post works as expected (yes, I checked that EP2 changed the displayname to "Hello World!") we can go ahead and create a simple REST API just for the fun of it and try it with a Macro!

 

I will not go through what a REST API is or go through how its done, I will just show you the code of an "embarrassingly" simple REST API in Python 3.7 using Flask so think of this as a very simplified simulation of the real thing. This is just for learning the nature of the HttpClient Post and Put.

 

api.py

from flask import Flask
from flask import abort
from flask import jsonify
from flask import request

app = Flask(__name__)

database = [
{ 'call': 1,
'destination': u'jaeger@pacific.rim'
},
{
'call': 2,
'destination': u'king@kong.com'
}
]

@app.route('/api', methods=['POST'])
def create_call():
if not request.json or 'destination' not in request.json:
abort(400)
else:
new_call = {'call': database[-1]['call'] + 1,
'destination': request.json['destination']}

database.append(new_call)

return jsonify({'response':database}), 200

if __name__ == '__main__':
app.run(debug=True, host='serverIP') #Change server IP to the host where you run this script

 

mohm$ python3.7 api.py
 * Running on http://serverIP:5000/ 

 

Ok, the whole point of the above is to collect call destinations from an incoming POST to the http://serverIP/api I will return the full database upon a successful add of a new call. I expect the server to return a call: 3 with the destination I manually assign, since this is a custom request I don´t have to actually wait for the endpoint to make a call before its sent. Lets try it.

 

I have enabled "log ctx httpclient debug 9" on the codec so we can see the request in and out (you can also use wireshark):

 

log ctx httpclient debug 9
log output on

xcommand HttpClient Post Header: "Content-Type: application/json" Url: http://serverIP:5000/api
{"destination":"new@destination.com"}
.

OK

CuilApp[1]: User admin1/shell about to execute command '/HttpClient/Post Header: Content-Type: application/json Url: http://serverIP:5000/api' from serverIP. HttpClient[8]: Engine::addNewRequest CuilApp[1]: HTTP(77) => POST http://serverIP:5000/api HttpClient[1]: HTTP: Outgoing => POST http://serverIP:5000/api HttpClient[3]: [curl] Connected to serverIP (serverIP) port 5000 (#2099) HttpClient[3]: [header OUT] POST /api HTTP/1.1 HttpClient[3]: [header OUT] Host: serverIP:5000 HttpClient[3]: [header OUT] Accept: */* HttpClient[3]: [header OUT] Content-Type: application/json HttpClient[3]: [header OUT] User-Agent: Cisco/CE HttpClient[3]: [header OUT] Accept-Charset: ISO-8859-1,utf-8 HttpClient[3]: [header OUT] Content-Length: 37 HttpClient[3]: [header OUT] ---- HttpClient[9]: [data OUT] {"destination":"new@destination.com"} HttpClient[3]: [header IN] HTTP/1.0 200 OK HttpClient[3]: [header IN] Content-Type: application/json HttpClient[3]: [header IN] Content-Length: 240 HttpClient[3]: [header IN] Server: Werkzeug/0.14.1 Python/3.7.0 HttpClient[3]: [header IN] Date: Thu, 18 Oct 2018 19:33:18 GMT HttpClient[9]: [data IN] { HttpClient[9]: [data IN] "response": [ HttpClient[9]: [data IN] { HttpClient[9]: [data IN] "call": 1, HttpClient[9]: [data IN] "destination": "jaeger@pacific.rim" HttpClient[9]: [data IN] }, HttpClient[9]: [data IN] { HttpClient[9]: [data IN] "call": 2, HttpClient[9]: [data IN] "destination": "king@kong.com" HttpClient[9]: [data IN] }, HttpClient[9]: [data IN] { HttpClient[9]: [data IN] "call": 3, HttpClient[9]: [data IN] "destination": "new@destination.com" HttpClient[9]: [data IN] } HttpClient[9]: [data IN] ] HttpClient[9]: [data IN] } HttpClient[9]: [data IN] ---- CuilApp[1]: HTTP(77) <= 200

By doing this we can confirm functionality before we put it into a Macro like this:

const xapi = require('xapi');
var payload;
var data = {'destination': ''};

function sendPOSTtoAPI(data){
  payload = JSON.stringify(data);
  xapi.command('HttpClient Post', { 
    Header: "Content-Type: application/json", 
    Url: 'http://serverIP:5000/api'
  }, 
    payload)
.then((result) => {
    console.log("success:" + result.StatusCode)
  })
  .catch((err) => {
      console.log("failed: " + err.message)
  }); } xapi.event.on('OutgoingCallIndication', (status) => { xapi.status.get('Call', {'CallId': status.CallId}).then((call) => { data.destination = call[0].RemoteNumber; sendPOSTtoAPI(data); } ); });

The above Macro basically looks for a OutgoingCallIndication event. You may be thinking "isnt this just another webhook?". YES it is but I get to choose what data I´ll be sending which is only the remote number that the device is trying to call). Once it has the OutgoingCallIndication (the ID of the call) it looks up the call details and finds the remote number which is then forwarded to my super advanced Python REST API.

 

So, lets see..

 

Making a few calls and this is displayed in the macro console if you print the payload and the event using console.log (not included in the macro):

22:21:56 callMonitor'[{"id":"21","AnswerState":"Unanswered","CallType":"Video","CallbackNumber":"spark:test@test1.com","DeviceType":"Endpoint","Direction":"Outgoing","DisplayName":"","Duration":"0","Encryption":{"Type":"Unknown"},"FacilityServiceId":"0","HoldReason":"None","PlacedOnHold":"False","Protocol":"Spark","ReceiveCallRate":"6000","RemoteNumber":"test@test1.com","Status":"Dialling","TransmitCallRate":"6000"}]'
22:21:56 callMonitor'{"destination":"test@test1.com"}'
22:22:05 callMonitor'[{"id":"22","AnswerState":"Unanswered","CallType":"Video","CallbackNumber":"spark:test@test2.com","DeviceType":"Endpoint","Direction":"Outgoing","DisplayName":"","Duration":"0","Encryption":{"Type":"Unknown"},"FacilityServiceId":"0","HoldReason":"None","PlacedOnHold":"False","Protocol":"Spark","ReceiveCallRate":"6000","RemoteNumber":"test@test2.com","Status":"Dialling","TransmitCallRate":"6000"}]'
22:22:05 callMonitor'{"destination":"test@test2.com"}'

Looking at the logs, as from above I see that the entries are added successfully. This was just one use case but you can re-use what you have learned to send whatever data you want to collect. Knock yourself out!

 

PUT REQEUSTS

PUT requests and allow lists for hosts

 

A PUT request is essentially the same as a POST, but within a REST API the different method have different meaning. When you design a REST API you may want to filter the operations based on the methods where POST = Create new and PUT = Modify existing. The PUT request is handled the same was as the POST demonstrated above, but how the server side handles the request methods may be different. This is anyway up to the developer / integrator to decide and handle as such.

 

xCommand HttpClient Put
AllowInsecureHTTPS: <False, True> (default False) Header[N]: <S: 0, 1024> Url(r): <S: 8, 2048>

 

Allow lists

Lets move over to the prevention stuff. You may setup host allow lists, which means that you can list up the hosts that the HttpClient can talk to. If the allow list has been populated with one or more host filter it will check if the host exists in this list before sending the request. If not the request will be denied. This is useful when you have multiple users for example a user with integrator rights but not admin access. The admin decides which service the codec is allowed to send data to. 

 

xCommand HttpClient Allow Hostname Add
    Expression(r): <S: 2, 200>
xCommand HttpClient Allow Hostname Clear
xCommand HttpClient Allow Hostname List
xCommand HttpClient Allow Hostname Remove
    Id(r): <0..9>

To whitelist a host use the following command type in the hostname or IP as the expresstion (whatever is used to connect to the server):

 

xCommand HttpClient Allow Hostname Add Expression: 10.10.10.10
xCommand HttpClient Allow Hostname Add Expression: server.domain.com

 

Don´t include http:// or custom port numbers only the host name or IP. If a POST request is sent to a server that is not in the allow list:

 

xCommand HttpClient Post Header: "Content-Type: application/json" Url: http://10.10.10.20:5000/api
.

OK
*r HttpClientPostResult (status=Error):
*r HttpClientPostResult Message: "URL not allowed by rule"
** end

If the host is not reachable:

 

xcommand HttpClient Post Header: "Content-Type: application/json" Url: http://10.10.10.25:5000/api
.

OK
*r HttpClientPostResult (status=Error):
*r HttpClientPostResult Message: "Timeout was reached"
** end

 

This concludes the functional section as an example on how to utilize this feature for custom data collection, feel free to use code found in this article to learn and experiment. 

 

Other use-cases (Webex Teams and BOTS)

Integrate with Webex Teams to post messages or create a space in webex teams when something happens on the device (requires a bot / user API access token).

 

Ok, even though we went through the functional overview and you should now have an idea of how it works, lets do an example of sending a message to a user in Webex Teams whenever a user makes a call, we will use and build on the same Macro we have used so far, we will also learn how to authenticate the certificate. Lets start:

 

xCommand HttpClient Post Url: https://api.ciscospark.com/v1/messages
.

OK
*r HttpClientPostResult (status=Error):
*r HttpClientPostResult Message: "Peer certificate cannot be authenticated with given CA certificates"
** end

Ok, so what I did above is just to test the connection the the Webex Teams API, and it won´t let me since the Certificate cannot be verified by any of my custom Root CA´s, well actually I don´t believe there are any custom Root CA´s available for the HttpClient, this will hopefully improve in the future. If you really want to verify the certificate let´s work around it using a few simple steps:

 

In Firefox:

 

Screen Shot 2018-10-22 at 09.12.27.png

 

 

Hover the mouse pointer over the green padlock. Verified by: GoDaddy.com, Inc. OK so lets find that Root CA, go to the Preferences -> Privacy & Security -> View Certificates (at the bottom) and locate the GoDaddy Root CA.

 

Screen Shot 2018-10-22 at 09.15.02.png

 

Now export this CA as x509 (PEM) and save it wherever.

 

Go to the codec web interface and add the exported certificate to the list of Certificate Authorities (not Service Certificate):

 

Screen Shot 2018-10-22 at 09.29.15.png

 

 

Once done, check it:

 

xcommand HttpClient Post Url: https://api.ciscospark.com/v1/messages
.

OK
*r HttpClientPostResult (status=Error):
*r HttpClientPostResult StatusCode: 401
** end

401 Unauthorized as expected. Good, lets proceed to the Macro since the status code confirms that I can now send data to Webex Teams (we need the Authorization header).

 

const xapi = require('xapi');
var data = {'destination': ''};
var accesstoken = 'BOT-ACCESSTOKEN';

function sendPOSTtoWebexTeams(data){
  var payload = JSON.stringify({'toPersonEmail':'YOUR@EMAIL.ADDRESS',
                 'markdown': "<blockquote class=success>Hey! LAB-101-RoomKit just made a call to: " + data.destination + "</blockquote>"});
                 
  console.log(payload);
                 
  xapi.command('HttpClient Post', { 
    Header: ["Content-Type: application/json", "Authorization: Bearer " + accesstoken], 
    Url: "https://api.ciscospark.com/v1/messages"
  }, 
    payload)
.then((result) => {
    console.log("success:" + result.StatusCode)
  })
  .catch((err) => {
      console.log("failed: " + err.message)
  }); } xapi.event.on('OutgoingCallIndication', (status) => { xapi.status.get('Call', {'CallId': status.CallId}).then((call) => { data.destination = call[0].RemoteNumber; console.log(JSON.stringify(call)); sendPOSTtoWebexTeams(data); } ); });

So I have just made a small modification to the previous Macro (mainly adding a bot access token and changing the URL and payload). More info on the Webex Teams API´s can be found on https://developer.webex.com

 

Note that the accesstoken is removed from the above example and you must add your own for this to work, change the "BOT-ACCESSTOKEN" with your bots accesstoken (acquired from the website link above) and the "YOUR@EMAIL.ADDRESS" with your email address (must be tied to a Webex Teams account). ALSO note that, in order to send multiple headers using a Macro you have to issue the headers in an Array.

 

*snip*
Header: ["Content-Type: application/json", "Authorization: Bearer " + accesstoken]
*snip*

If I now make a call to someone I should get a ping from my bot with the number the device is trying to call. You can modify the script to fetch the device name or to however you want the text to look like. I successfully received a message:

 

Screen Shot 2018-10-22 at 09.47.44.png

 

Need to send data to other public API´s or your own service? Just follow the same process as above to enable certificate verification or if you trust the service you can bypass the ceritifcate verification, see below for instructions. 

Known Issues & Limitations


HTTPS Requires (by default) a signed CA certificate to authenticate the remote server certificate.

 

If you are attempting to connect to a host over HTTPS the HttpClient will try to authenticate the installed certificate with the server certificate. Self-signed certificates will therefore not work by default. In order to use secure HTTPS without bypass you must install a signed CA certificate.

 

xcommand HttpClient Post Url: https://api.ciscospark.com/v1/messages
.

OK
*r HttpClientPostResult (status=Error):
*r HttpClientPostResult Message: "Peer certificate cannot be authenticated with given CA certificates"
** end

To workaround the issue you can bypass the certificate verification (although not recommended) to get the request through. This is a flag that has to be set per request. Firstly you have to enable the codec to send insecure HTTPS requests, if this is set to False you will get an error (Insecure HTTPS not allowed). Once set to "True" you can send insecure HTTPS requests. This is not recommended, but we implemented a way for customers to do this against self-signed certificatge servers they may be testing against. See below for an example. Here I do not need a signed CA to go forward with the request.

 

xcommand HttpClient Post Url: https://api.ciscospark.com/v1/messages Header: "Content-Type: application/json" AllowInsecureHTTPS: True
.

OK
*r HttpClientPostResult (status=Error):
*r HttpClientPostResult Message: "Insecure HTTPS not allowed"
** end
xConfiguration HttpClient AllowInsecureHTTPS: True
** end

OK
xcommand HttpClient Post Url: https://api.ciscospark.com/v1/messages Header: "Content-Type: application/json" AllowInsecureHTTPS: True
.

OK
*r HttpClientPostResult (status=Error):
*r HttpClientPostResult StatusCode: 401
** end

 

Payload IN and headers IN is not currently supported. 

We do not currently support fetching feedback from the server when doing a HTTP POST or PUT via Macro or xAPI, but it can be seen in the logs if a high debug level is enabled. The only feedback we get is the request status code and a description.

 

Support for 3rd party USB input devices (keyboards for use with In-Room Control)

Now, after looking at the HTTP POST / PUT feature I want to talk about adding support for USB keyboards and just to make it clear, this is not a "replace the Touch 10 keyboard input" feature,  as in, we do not offer it as a supplement for typing in long and complicated URI´s quicker ;), no, no, this is a In-Room Control feature where every key-click on the keyboard generates an event, and events can be used for doing cool stuff! I hope you are as excited as me for this one.

Summary


We continue to take In-Room Controls to new levels, now supporting 3rd party USB input devices (as in input devices that are seen by the codec as generic "keyboards")! With this simple feature you can do awesome stuff for example, control your device behavior using a generic Bluetooth remote (with USB dongle) or USB keyboard, imagine starting the meeting and composing input sources into the main video while walking around in the room with a wireless USB clicker from Logitech?! Play with that idea for a moment before we explore the world of opportunities with this feature. 

Feature summary


We will take a look at how to utilize third-party input devices to provide an overview of how this feature works and how it can benefit certain scenarios. This feature opens up a whole lot of new integration angles for customization of device behavior. In other words, pure awesomeness! See below for a couple of generic devices we will be utilizing for this walk through:

 

 Screen Shot 2018-10-16 at 11.46.49.pngRegular (found in trash) USB keyboardScreen Shot 2018-10-16 at 11.41.48.pngUSB media remote, bought of the shelf at a general electric shop

Feature description, functional overview, compatibility 


Purpose
The purpose of this feature is to extend In-Room Control with more options for third party integration on our newest portfolio (yea unfortunately this is only supported on the Cisco Webex Room Series, and the DX70 and DX80 which is kind of a comfort and actually have a few use-cases). A wireless remote control like the one in the image above can for example, replace some functionality of a Touch 10 with the benefit of being able to carry it around and for example unmute the device from a distance (just saying, I actually use this remote in all my meetings now, its really handy ;)).

 

Functional overview

The generic USB input device will when a button is pressed, generate an event in the xAPI allowing macros or third-party control devices react upon the event in the same manner as the Touch 10 In-Room Control widgets generate events upon state changes. 

 

So lets get started by hooking up our wireless remote control to a Cisco Webex Codec Plus. We will use the available USB-A input on the device.

 Screen Shot 2018-10-16 at 12.03.56.pngCisco Webex Codec Plus <3

Now SSH into the Codec Plus (xAPI) and let the magic happen...

 

xConfig //inputdevice
*c xConfiguration Peripherals InputDevice Mode: Off
** end

OK

xConfiguration Peripherals InputDevice Mode: On
** end

OK

xFeedback Register /event/userinterface/inputdevice
** end

OK

xFeedback list
/event/userinterface/inputdevice
** end

OK

 

What is happening above is that I first of all, enable the feature which is required for the device to even see my connected keyboard. Then I register a session feedback, this is basically the same as the macro xapi.event.on() method and it can be used to trigger actual events but I mostly use it to test events to see what they produce. Now, press a few buttons on the third-party USB input device to make sure it works! I call them USB input devices since these generic keyboard imitators come in many shapes (USB Super Nintendo controllers? you bet!).

 

*e UserInterface InputDevice Key Action Key: KEY_ENTER
*e UserInterface InputDevice Key Action Code: 28
*e UserInterface InputDevice Key Action Type: Pressed
** end
*e UserInterface InputDevice Key Action Key: KEY_ENTER
*e UserInterface InputDevice Key Action Code: 28
*e UserInterface InputDevice Key Action Type: Released
** end
*e UserInterface InputDevice Key Action Key: KEY_SLEEP
*e UserInterface InputDevice Key Action Code: 142
*e UserInterface InputDevice Key Action Type: Pressed
** end
*e UserInterface InputDevice Key Action Key: KEY_SLEEP
*e UserInterface InputDevice Key Action Code: 142
*e UserInterface InputDevice Key Action Type: Released
** end
*e UserInterface InputDevice Key Action Key: KEY_VOLUMEUP
*e UserInterface InputDevice Key Action Code: 115
*e UserInterface InputDevice Key Action Type: Pressed
** end
*e UserInterface InputDevice Key Action Key: KEY_VOLUMEUP
*e UserInterface InputDevice Key Action Code: 115
*e UserInterface InputDevice Key Action Type: Released
** end
*e UserInterface InputDevice Key Action Key: KEY_VOLUMEDOWN
*e UserInterface InputDevice Key Action Code: 114
*e UserInterface InputDevice Key Action Type: Pressed
** end
*e UserInterface InputDevice Key Action Key: KEY_VOLUMEDOWN
*e UserInterface InputDevice Key Action Code: 114
*e UserInterface InputDevice Key Action Type: Released
** end

 

It works! So, now that we can generate events based on button presses on the USB device we can implement reactions to the events. As you see above the button presses generates two different events, one for "Pressed" and one for "Released". So if you press and hold one button you only see one event "Pressed" until you release the button which will generate the second event "Released" which may be handy if you want to implement camera control from the USB device for example.

 

We can create a remote controller that monitors the events via webhooks, directly in a SSH session or just create a macro (depending on what functionality we want to implement). For this walk through I will use macros for the examples.

 

Let´s first make something simple, like bringing the standby, volume up and volume down buttons to life. Below is an example macro, yes they also come in different shapes and forms and below is just an example on how it can be done, feel free to use anything in this blog post and improve it! 

 

const xapi = require('xapi');

function com(command) {
  xapi.command(command);
  log(command);
}

function log(event) {
  console.log(event);
}

function notify(message) {
  xapi.command('UserInterface Message TextLine Display', {
    Text: message,
    duration: 3
  });
}

function init() {
  let standbyState;
  xapi.status.get('Standby').then((state) => {standbyState = state.State === 'Off' ? false : true; })
  xapi.status.on('Standby', state => {
      standbyState = state.State === 'Off' ? false : true;
  });
 
  xapi.event.on('UserInterface InputDevice Key Action', press => {
    if (press.Type == "Pressed") {
      switch (press.Key) {
            default:
            break;
        }
    } else if (press.Type == "Released") {
        switch (press.Key) {
          case 'KEY_VOLUMEUP':
            com('Audio Volume Increase');
            break;
          case 'KEY_VOLUMEDOWN':
            com('Audio Volume Decrease');
            break;
          case 'KEY_SLEEP':
            com(standbyState ? 'Standby Deactivate' : 'Standby Activate');
            break;
          default:
            notify(press.Key + ' is not bound!');
            break;
          }
      }
  });
}

init();

This macro simply listens for the InputDevice and if it sees an event containing "KEY_VOLUMEUP", "KEY_VOLUMEDOWN" or "KEY_SLEEP" it will execute the related commands:

 

KEY_VOLUMEUP: Audio Volume Increase
KEY_VOLUMEDOWN: Audio Volume Decrease
KEY_SLEEP: If in standby or halfwake send Deactivate, If not in standby send Activate

If the key that is pressed is not found in the key map dict it will display a notification on the screen that the key is not usable (see below). This is useful when you start adding new event triggers, just press a button and see what it is called and type it into the switch case structure, quick and easy!

 

 Screen Shot 2018-10-16 at 13.22.53.png

Now, lets do one cool thing. Creating a camera control function for the arrow keys, utilizing the "pressed" event.

 

const xapi = require('xapi');

function com(command, args='') {
  xapi.command(command, args);
  log(command + ' ' + JSON.stringify(args));
}

function log(event) {
  console.log(event);
}

function notify(message) {
  xapi.command('UserInterface Message TextLine Display', {
    Text: message,
    duration: 3
  });
}

function cameraControl(motor, direction, cameraId='1') {
  com('Camera Ramp', { 'CameraId': cameraId,
                       [motor]: direction
  });
}

function init() {
  let standbyState;
  xapi.status.get('Standby').then((state) => {standbyState = state.State === 'Off' ? false : true; });
  xapi.status.on('Standby', state => {
      standbyState = state.State === 'Off' ? false : true; 
  });
  
  xapi.event.on('UserInterface InputDevice Key Action', press => {
    if (press.Type == "Pressed") {
      switch (press.Key) {
        case "KEY_LEFT":
          cameraControl('Pan', 'Left');
          break;
        case "KEY_RIGHT":
          cameraControl('Pan', 'Right');
          break;
        case "KEY_UP":
          cameraControl('Tilt', 'Up');
          break;
        case "KEY_DOWN":
          cameraControl('Tilt', 'Down');
          break;
        default:
          break;
        }
    } else if (press.Type == "Released") {
        switch (press.Key) {
          case "KEY_LEFT":
            cameraControl('Pan', 'Stop');
          break;
          case "KEY_RIGHT":
            cameraControl('Pan', 'Stop');
          break;
          case "KEY_UP":
            cameraControl('Tilt', 'Stop');
          break;
          case "KEY_DOWN":
            cameraControl('Tilt', 'Stop');
          break;
          case 'KEY_VOLUMEUP':
            com('Audio Volume Increase');
            break;
          case 'KEY_VOLUMEDOWN':
            com('Audio Volume Decrease');
            break;
          case 'KEY_SLEEP':
            com(standbyState ? 'Standby Deactivate' : 'Standby Activate');
            break;
          default:
            notify(press.Key + ' is not bound!');
            break;
          }
      } 
  });
}

init();

 

The above code that was added will move the camera as long as the event type shows "Pressed". When the button is released a new event is generated hitting the same key binding only to "Stop" the camera movement.

 

We could bind a lot more keys and do other stuff as well, but the concept is the same, I would just need to create more key-bindings. The sky is the limit to what you can do with this feature.

 

 

 

Configuration


xConfiguration Peripherals InputDevice Mode: On

Troubleshooting


When you connect a keyboard, make first make sure that the events are generated by accessing the codec API and issue:

 

xFeedback Register /event/userinterface/inputdevice

Use a few buttons to see that the events are displayed. If they are showing the feature is working as it should. The rest is up to the integrator.

 

The keyboard can be seen in the Peripherals menu from the web interface or via the xAPI (xStatus):

 

 

xStatus //Peripherals

*s Peripherals ConnectedDevice 1017 HardwareInfo: "1997:2433" *s Peripherals ConnectedDevice 1017 ID: "1997:2433:/dev/input/event3" *s Peripherals ConnectedDevice 1017 Name: " :Mini Keyboard" *s Peripherals ConnectedDevice 1017 SoftwareInfo: "" *s Peripherals ConnectedDevice 1017 Status: Connected *s Peripherals ConnectedDevice 1017 Type: InputDevice *s Peripherals ConnectedDevice 1017 UpgradeStatus: None *s Peripherals ConnectedDevice 1018 HardwareInfo: "1997:2433" *s Peripherals ConnectedDevice 1018 ID: "1997:2433:/dev/input/event4" *s Peripherals ConnectedDevice 1018 Name: " :Mini Keyboard" *s Peripherals ConnectedDevice 1018 SoftwareInfo: "" *s Peripherals ConnectedDevice 1018 Status: Connected *s Peripherals ConnectedDevice 1018 Type: InputDevice *s Peripherals ConnectedDevice 1018 UpgradeStatus: None *s Peripherals ConnectedDevice 1019 HardwareInfo: "1997:2433" *s Peripherals ConnectedDevice 1019 ID: "1997:2433:/dev/input/mouse0" *s Peripherals ConnectedDevice 1019 Name: " :Mini Keyboard" *s Peripherals ConnectedDevice 1019 SoftwareInfo: "" *s Peripherals ConnectedDevice 1019 Status: Connected *s Peripherals ConnectedDevice 1019 Type: InputDevice *s Peripherals ConnectedDevice 1019 UpgradeStatus: None ** end

 

The above shows that my remote is seen as two different keyboards and a mouse which is correct. The mouse device can be ignored as we do not support it and it wont generate events. But as long as the keyboard works as it should then all should be well.

 

Here is a CEC macro I created to control my AppleTV, same basic macro, different functionality based on key click:

const xapi = require('xapi');

function com(command, args='') {
  xapi.command(command, args).catch(e => {
      com('UserInterface Message TextLine Display', {
        Text: e.message,
        duration: 3
      });
    });
  log(command + ' ' + JSON.stringify(args));
}

function log(event) {
  console.log(event);
}

function notify(message) {
  com('UserInterface Message TextLine Display', {
    Text: message,
    duration: 3
  });
}

function init() {
  
  xapi.event.on('UserInterface InputDevice Key Action', press => {
    if (press.Type == "Pressed") {
      switch (press.Key) {
        case "KEY_PLAYPAUSE": 
            com('Video CEC Input KeyClick', {ConnectorId: 5, NamedKey: 'Play'});
            break;
        case "KEY_UP": 
            com('Video CEC Input KeyClick', {ConnectorId: 5, NamedKey: 'Up'});
            break;
        case "KEY_DOWN": 
            com('Video CEC Input KeyClick', {ConnectorId: 5, NamedKey: 'Down'});
            break;
        case "KEY_LEFT": 
            com('Video CEC Input KeyClick', {ConnectorId: 5, NamedKey: 'Left'});
            break;
        case "KEY_RIGHT": 
            com('Video CEC Input KeyClick', {ConnectorId: 5, NamedKey: 'Right'});
            break;
        case "KEY_ENTER": 
            com('Video CEC Input KeyClick', {ConnectorId: 5, NamedKey: 'Ok'});
            break;
        case "CODE_273": 
            com('Video CEC Input KeyClick', {ConnectorId: 5, NamedKey: 'Back'});
            break;
        default:
          notify(press.Key + ' is not bound!');
          break;
        }
    } else if (press.Type == "Released") {
        switch (press.Key) {
          default:
            break;
          }
      } 
  });
}

init();

For this use case you might notice that I have chosen to use the "Pressed event", this is because I expect the cursor to move the second I press the right up left or down button, not when I release it.. it just does not feel natural. But there you see, the options are many and.. we might see HDCP coming for more devices in CE9.6.1 where this sweet thing can come handy ;)!

 

const xapi = require('xapi');

function com(command, args='') {
  xapi.command(command, args).catch(e => {
      com('UserInterface Message TextLine Display', {
        Text: e.message,
        duration: 3
      });
    });
  log(command + ' ' + JSON.stringify(args));
}

function log(event) {
  console.log(event);
}

function notify(message) {
  com('UserInterface Message TextLine Display', {
    Text: message,
    duration: 3
  });
}

function composer(sources) {
  var layout = 'equal';
  com('Video Input SetMainVideoSource', {
        ConnectorId: sources,
        Layout: layout
    });
}

function cameraControl(motor, direction, cameraId='1') {
  log(motor);
  com('Camera Ramp', {
            'CameraId': cameraId,
            [motor]: direction
          });
}

function presentation(share) {
  com('Presentation ' + share);
  return (share === "Stop") ? "Start":"Stop";
}

function init() {
  let standbyState;
  let presentationState = 'Start';
  let compose = 1;
  let sources;
  let muteState;
  var selfviewTimer;
  
  xapi.status.get('Standby').then((state) => {standbyState = state.State === 'Off' ? false : true; });
  xapi.status.get('Audio Microphones Mute').then((state) => { muteState = state === 'Off' ? false : true; });
  xapi.status.on('Standby', state => {
    standbyState = state.State === 'Off' ? false : true; 
  });
  xapi.status.on('Audio Microphones Mute', state => {
    muteState = state === 'Off' ? false : true;
  });
  
  xapi.event.on('UserInterface InputDevice Key Action', press => {
    if (press.Type == "Pressed") {
      switch (press.Key) {
        case "KEY_LEFT":
          cameraControl('Pan', 'Left');
          break;
        case "KEY_RIGHT":
          cameraControl('Pan', 'Right');
          break;
        case "KEY_UP":
          cameraControl('Tilt', 'Up');
          break;
        case "KEY_DOWN":
          cameraControl('Tilt', 'Down');
          break;
        case "CODE_272":
          cameraControl('Zoom', 'In');
          break;
        case "CODE_273":
          cameraControl('Zoom', 'Out');
          break;
        case "KEY_PAGEUP":
          selfviewTimer = setTimeout(function() { 
            com('Video Selfview Set', {
              Mode: 'On', 
              FullscreenMode: 'On'
          })}, 2 * 1000);
          break;
        default:
          break;
        }
    } else if (press.Type == "Released") {
        switch (press.Key) {
          case "KEY_LEFT":
            cameraControl('Pan', 'Stop');
          break;
          case "KEY_RIGHT":
            cameraControl('Pan', 'Stop');
          break;
          case "KEY_UP":
            cameraControl('Tilt', 'Stop');
          break;
          case "KEY_DOWN":
            cameraControl('Tilt', 'Stop');
          break;
          case "CODE_272":
            cameraControl('Zoom', 'Stop');
          break;
          case "CODE_273":
            cameraControl('Zoom', 'Stop');
          break;
          case "KEY_ENTER":
            presentationState = presentation(presentationState);
          break;
          case 'KEY_VOLUMEUP':
            com('Audio Volume Increase');
            break;
          case 'KEY_VOLUMEDOWN':
            com('Audio Volume Decrease');
            break;
          case 'KEY_SLEEP':
            com(standbyState ? 'Standby Deactivate' : 'Standby Activate');
            break;
          case 'KEY_PLAYPAUSE':
            com(muteState ? 'Audio Microphones Unmute' : 'Audio Microphones Mute');
            break;
          case "KEY_PAGEDOWN":
            com('Video Selfview Set', {
              Mode: 'Off',
              FullscreenMode: 'Off'
            });
          break;
          case "KEY_PAGEUP":
            clearTimeout(selfviewTimer);  
            com('Video Selfview Set', {
              Mode: 'On'
          });
          break;
          case "KEY_COMPOSE":
            compose = compose + 1;
            if (compose > 3) {
              compose = 1;
            }
            sources = Array.from({length: compose}, (v, k) => k+1);
            composer(sources);
          break;
          default:
            notify(press.Key + ' is not bound!');
            break;
          }
      } 
  });
}

init();

In the above macro I have added a bit more functionality to mute and unmute, turn on/off self view - even if you hold down the "enable selfview" button for 2 seconds it will enable self view in full screen. Hey, just use the imagination, combine it with the HTTP POST / PUT feature, like make your bot send a message to your secretary that you need a coffee in the {meetingRoom} and that without ANY external control devices!!!

 

Is there any limitations? No USB hubs are supported, the amount of USB-A ports is your limit to the connected USB devices.

We might not support all the dongles out there (too many to test), but feel free to let us know if you find any issues.

 

Hide default feature buttons in UI (Call, Share, Meetings)

One of the most asked features has finally arrived. Being able to customize the UI as you please, well we are not quite there yet but at least this is one step towards the goal, you asked for it and you got it, congratulations!

 

With this feature you get the ability to hide the big green call button, the blue share button, and all the other buttons as well if you like, conveniently the In-Room Control buttons are not affected by it, so you now control which buttons should be visible.

 

This also means exceptional control over the user experience with our devices, which means that this feature should be used with some level of caution as hiding the buttons without creating some kind of custom user flow can be confusing and leave the users with nothing on the Touch 10 panel or on the screen. Yes, literally no buttons in the UI. But let´s move on, and I promise this will not be a long one.

Summary


This feature adds the ability to hide the default feature buttons in UI while still exposing custom In-Room Control panels. This allows for a even more customizable UI but still not fully qualified as "Kiosk mode" just yet.

This feature simply adds a series of configurations that allow you to hide / display certain feature buttons in the UI that has previously not been possible to hide.

Feature description, functional overview


Purpose
Customers that wanted to create a custom UI for their users has been unable to do so as the Call button and Share button is visible together with the Custom buttons. This may confuse the users in scenarios where they should not use the Call or Share buttons, but rather use the Custom setup that defines the use-cases that the customer wants their users to utilize.

 

This feature is not for everyone, but for those that requires special use-cases that involves removing some of the default buttons in the UI.

 

Functional overview

 

xconfig //userinterface/features ?
*? xConfiguration UserInterface Features Call End: <Auto, Hidden>
*? xConfiguration UserInterface Features Call MidCallControls: <Auto, Hidden>
*? xConfiguration UserInterface Features Call Start: <Auto, Hidden>
*? xConfiguration UserInterface Features HideAll: <False, True>
*? xConfiguration UserInterface Features Share Start: <Auto, Hidden>

OK

The above configurations will also be available to configure via the web interface of the Room Device. The default configurations are in bold. You can hide the call button, which hides the default UI feature for making a call or do directory look-ups / favorites / recent calls etc, and will also hide the Add button for adding participants while in a call. Hide the share button, which in turn hides the default UI for sharing and previewing sources in and out of call.

 

You cannot hide other single buttons that are displayed based upon provisioning. For example; Meetings, Extension Mobility, Voicemail etc. If those buttons must be hidden, you have to use the following:

 

xConfiguration UserInterface Features HideAll: <False, True>

This will hide all the default feature buttons except custom In-Room Control buttons. An alternative is to not provision these settings from the backend.

 

You can also hide buttons that are default for In-Call scenarios, for example "End call" can be hidden by itself or you can hide all the default In-Call buttons except In-Room Controls. Let's take a look at some examples.

 

I want to create a custom UI for my employees where they can dial into specific meeting room that is used regularly. We don´t make external calls anyway and it's only between the meeting rooms that calls take place.

 

Screen Shot 2018-10-24 at 12.59.29.png

 

 

My UI looks like the above on the Touch 10.

 

xConfiguration UserInterface Features HideAll: True

 

After issuing the command above the UI will look like this:

 

Screen Shot 2018-10-24 at 13.09.55.png

 

 

Not very user-friendly, but here is where the In-Room Control magic comes into play. We want to have user friendly, self-explaining buttons that simply defines the use-case.

 

Open the In-Room Control editor found on the web interface (Integration -> In-Room Control)

 

Screen Shot 2018-10-24 at 13.22.34.png

 

 

The above is the design that I want, perfectly adapted to my company's use-case for this Room Device, nice and clean. I push the design to the codec and now it looks like this:

 

Screen Shot 2018-10-24 at 13.25.43.png

 

 

Screen Shot 2018-10-24 at 13.27.17.png

 

 

There is little the user can do wrong here, but this is exactly how I want it to be. I don´t want them to make random calls or do mistakes. I need to keep it simple for my users. The three entries I have here are the meeting rooms that we have and the user only have to click "Dial" and they will dial the correct number, they don´t have to remember any URI details or know how to navigate the phonebook.

 

In order to make use of this In-Room Control panel I need to have a control device or create a Macro. I will not do that in this feature walk-through since the ability to do that has been there for a while. Please refer to the tutorial on how to give In-Room Control buttons life (its basically the same concept as the "third-party USB input devices" above). Just imagine that when you press one of these buttons it just works! :)

 

This feature is about designing a UI tailored for the customers' use-cases ("Kiosk Mode" like setups - not even sure if I am allowed to use that term for this feature ;) ). I can create a lot of In-Room Control panels like this on the UI and add almost any functionality only limited to imagination.


OK, this was the general "Out of call" UI. We have another UI which is "In-Call".

 

I make a call by pressing one of the meeting room buttons only to discover the below:

 

Screen Shot 2018-10-24 at 13.38.29.png

 

 

It should not come as a surprise since I have executed the Hide All features command and the In-Room Control UI element that I pushed was only visible "Out of call". I need at least some functionality for In-Call. Back to the drawing board:

 

Screen Shot 2018-10-24 at 13.47.17.png

 

 

I mark this button to be displayed in calls only and push it to the Room Device. Now I have UI for the In-Call scenario limiting my users to use the functionality I have implemented. Some of you might think, what the... is he doing (I am creating a fake UI that does not work after all), but I am just trying to give you ideas on how you can set things up, I am sure you can do this way better than me. 

 

Screen Shot 2018-10-24 at 13.50.49.pngScreen Shot 2018-10-24 at 13.51.34.png

Voila! The only thing missing now is a survey asking my users how the experience was, of which I am using the new cool HttpClient feature to send to my server.. maybe another time, there are lots of examples on the developer community! :)

 

Make sure you visit: https://developer.cisco.com/site/roomdevices/

Known Issues & Limitations


You cannot hide buttons that has fluctuating appearance (depends on scenario / configuration / setup) for example, Meetings, Extension Mobility or Voicemail by them self. To hide these buttons you must hide all the feature buttons or make the back end service not provision them to the devices.

 

Maybe we will see more of this UI customization features going forward.. at least I am both optimistic and excited! CE9.6 is going to be a FRESH start of 2019! I hope you enjoyed my humble post.

 

Merry Christmas and a Happy New Year! :)

 

/Magnus

 

 

 

5 Comments
Beginner

Magnus,

As always, you have great insight into the products!

Thanks,
Justin

Magnus - thanks for sharing, this is a great preview that gives good context to the upcoming features as well as allowing us to think of ways to use them before they are released, much appreciated!

Beginner

Thank you for the info. Removing default call button was one of the main problems for us. 

Beginner

 This looks great. At the moment I use a DX70 registered to CUCM with extension mobility (EM). If I want to change my EM PIN I have to borrow a colleagues 8845 IP endpoint. 

 I guess I can now code a macro that uses the new HTTP functionality to make a REST call to CUCM, changing my PIN. Definitely worth a look. 

 

Many thanks for this 

Cisco Employee

Fantastic article. Well done!!


BTW, can you also do a HTTP GET from a Macro?

CreatePlease to create content