Quick Docs Macro Exercise - Part 1 - Start Simple (section rxe-m1)
Section rxe-m1 Requirements
Note
This lab assumes you have access to a Cisco RoomOS Device that is already setup and ready for use. If your device is not registered and online, please do so before beginning
Additional Lessons
- It's recommended you have run through Accessing RoomOS xAPI via Macros in Macros rxp-6 at a minimum before proceeding with this Exercise
Hardware
- A Laptop
- A Cisco Desk, Board or Room Series Device running the most recent On Premise or Cloud Stable software
- A Touch Controller is required when working on a Room Series Device. Either Room navigator or 3rd part touch display
- Preferred Device: Cisco Desk Pro
- A minimum of 1 camera (Either Integrated or External)
Software
- Laptop
- RoomOS Device
- Either the current On Premise or Cloud Stable release
Network
- Local Access to the RoomOS Device over port 80/443
- General access to the Internet
Part 1 Solution Outline
In section rxe-m1 of this lab, we'll build a Macro that will allow folks to either View a Document or Scan a QR code to take the document with them on your device.
We'll review how to:
- Build a UserInterface
- Map those UserInterface elements to actions driven by an accompanying macro
- Integrate a 3rd party QR code service, leveraging APIs external to the Codec
Start with the UserInterface (rxe-m1.1)
Lesson: Create a New Panel Button (rxe-m1.1.1)
Starting with the UserInterface elements helps use visualize our solution and determine how to then structure our Macro
-
Task:
- Create a new Panel
- Assign this panel's id as:
wx1_QrDocs
- Assign this panel's name as:
Room Docs
- Assign this panel's location as:
HomeScreenAndCallControls
- Pick any icon you wish
- Pick any Panel Color you wish
- Save your Extension
- You should be able to see your Panel on the Codec's Touch Interface
Challenge: Rename the Page to Room Docs
- Rename the Page Name from
Page
to:Room Docs
- Assign the PageId as:
wx1_QrDocs~RoomDocs
Lesson: Add Solution Widgets (rxe-m1.1.2)
-
Task:
- Rename Row 1 to:
Lab 1451
- Add 2 Buttons to this row
- Set the following properties for the left button
- Rename Button Text
Button
to:Open Site
- Assign WidgetId:
wx1_QrDocs~OpenSite~https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/
- Set the Widget Width to 2
- Rename Button Text
- Set the following properties for the Right button
- Rename Button Text
Button
to:Open QrCode 🔳
- Assign WidgetId:
wx1_QrDocs~OpenQrCode~https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/
- Set the Widget Width to 2
- Rename Button Text
- Set the following properties for the left button
- Add 2 Buttons to this row
- Add a second row, with the name:
Reimagine Workspaces
- Add 2 Buttons to this row
- Set the following properties for the left button
- Rename Button Text
Button
to:Open Site
- Assign WidgetId:
wx1_QrDocs~OpenSite~https://www.webex.com/us/en/workspaces.html
- Set the Widget Width to 2
- Rename Button Text
- Set the following properties for the Right button
- Rename Button Text
Button
to:Open QrCode 🔳
- Assign WidgetId:
wx1_QrDocs~OpenQrCode~https://www.webex.com/us/en/workspaces.html
- Set the Widget Width to 2
- Rename Button Text
- Set the following properties for the left button
- Add 2 Buttons to this row
- Save your Extension
- You should be able to see your Panel on the Codec's Touch Interface
- Rename Row 1 to:
Add your Organization's HomePage 
You may have noticed the WidgetIds have a URL as apart of the ID
We'll be using this in the Macro we write later, to open this page
Here is how we're structuring WidgetIds for this lab
Example
-
WidgetId Structure: appName~Action~URL
- appName:
wx1_QrDocs
-> Let's us associate this interface with our Macro Script and helps make the WidgetId more unique - action:
OpenSite
orOpenQrCode
-> This will tell use how we want to open our URL - URL: This is the URL we want to open, or build a QR Code for
- String Separator:
~
-> Using a unique character, such as this tilde, can help use split up and access this information later in the Macro
- appName:
Is this WidgetId format mandatory?
Widget Ids are strings. This is just one of many examples of how you can structure your widgets. It's not required to structure them like this, with the exception of this lab.
So long as they are unique in your customization, you should be good to go
- Task:
- Create a 3rd Row
- Add in an Open Site button and an Open QrCode 🔳 with your Organizations Home Page as the Url
- Be sure to assign
wx1_QrDocs
as the appName and the correct action to the button they represent
Hint
OpenSite:
wx1_QrDocs~OpenSite~YOUR_ORG_HOMEPAGE
OpenQrCode: wx1_QrDocs~OpenQrCode~YOUR_ORG_HOMEPAGE
Create a New Macro and Subscribe (rxe-m1.2)
Abstract
Now that we have our UserInterface in place, we can start to build our solution
We'll be using the Macro Editor for this solution, but know that you can do these very same tasks from any other avenue we explored in Part 2 of this lab
The benefit of Macros, is that they come with every Cisco Codec running Ce9.2 or newer (with the exception of the Sx10). So no need to procure hardware or spin up a service to start building a solution
Lesson: Create a new Macro (rxe-m1.2.1)
If you missed Section 2.6 in part 2 of this lab, please watch this Vidcast
Vidcast: Macro Editor IDE Review
-
Task:
- Select Create new macro in the Macro Editor
- Name this macro Room Docs
- Save and Activate+ the Room Docs Macro
Review how To
if NOT already logged into the endpoint, follow these steps
- Open a Browser and enter your Codec's IP as a URL and hit enter
- Login with your Username and Password
Device Login Page If in the UI Extensions Editor
- Click on the Device Name in the Top Left hand corner of the UI Extensions Editor to get back to the Device Home Page
Lesson: Subscribing to Widget Actions (rxe-m1.2.2)
-
xAPI: xEvent UserInterface Widget Action
-
Task:
-
Subscribe the xAPI Path above using Macro Syntax
-
Take the Callback information generated by this event, and log it to the Macro Console using
console.log()
-
Once complete, Save and Activate the macro (if inactive)
-
Start pressing the Open Site and Open QrCode 🔳 buttons contained within your
Room Docs
panel - Monitor the Macro Console Output
-
Compare your Macro
Note
It's ok if your macro's structure doesn't match 1:1 with the examples below
As long as they information we access is the same, then format is up to the developers preference
import xapi from 'xapi';
xapi.Event.UserInterface.Extensions.Widget.Action.on(({WidgetId, Type, Value}) => {
console.log({WidgetId, Type, Value})
})
Compare your Macro Console
Time | Macro | WidgetId | Type | Value |
---|---|---|---|---|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenQrCode~https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/ | pressed |
|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenQrCode~https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/ | released |
|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenQrCode~https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/ | clicked |
|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenQrCode~https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/ | pressed |
|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenQrCode~https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/ | released |
|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenQrCode~https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/ | clicked |
|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenSite~https://www.webex.com/us/en/workspaces.html | pressed |
|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenSite~https://www.webex.com/us/en/workspaces.html | released |
|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenSite~https://www.webex.com/us/en/workspaces.html | clicked |
|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenQrCode~https://www.webex.com/us/en/workspaces.html | pressed |
|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenQrCode~https://www.webex.com/us/en/workspaces.html | released |
|
HH:MM:SS | Room Docs | wx1_QrDocs~OpenQrCode~https://www.webex.com/us/en/workspaces.html | clicked |
|
Why does each Widget click have 3 events fire?
Good Catch ! Many widgets offer 1-3 events that will fire based on the conditions of the action
This is important to know, as you may only want an action to happen once, not upwards of 3 times, when building your automation
We can use techniques in ES6 JS to filter out what we're interested in for these scenarios
For reference, here's a list of Widgets and their event payloads
Click on each table below to review each Widget
Key | Value |
---|---|
WidgetId | Assigned by the Developer on Widget Instantiation |
Type | changed |
Value | on or off |
Key | Value |
---|---|
WidgetId | Assigned by the Developer on Widget Instantiation |
Type | pressed , released , or changed |
Value | Integer between 0 and 255 |
Key | Value |
---|---|
WidgetId | Assigned by the Developer on Widget Instantiation |
Type | pressed , released , or clicked |
Value | N/A |
Key | Value |
---|---|
WidgetId | Assigned by the Developer on Widget Instantiation |
Type | pressed , released |
Value | Assigned by the Developer on Widget Instantiation |
Key | Value |
---|---|
WidgetId | Assigned by the Developer on Widget Instantiation |
Type | pressed , released , or clicked |
Value | N/A |
Key | Value |
---|---|
WidgetId | Assigned by the Developer on Widget Instantiation |
Type | pressed , released , or clicked |
Value | increment or decrement |
Does not fire events
Key | Value |
---|---|
WidgetId | Assigned by the Developer on Widget Instantiation |
Type | pressed , released , or clicked |
Value | up , down , left , right , center |
Does not fire events
Lesson: Refine and Filter our Widget information (rxe-m1.2.3)
Info
As you saw in the previous example Widgets can produce 1-3 events to fire whenever you interact with them
It's best to filter this down to the specific even in question
In macros, we can use the tools offered to us by ES6 JS to complete this task
-
xAPI: xEvent UserInterface Widget Action
-
Task:
- Replace your original console.log with console.debug
- This will hide this log in the debug group, we can use this later to check our work
- Use an
if
ES6 JS statement to filter our WidgetId Event and confirm the widget belongs to our customization- This can be done using ES6 JS's
.includes('wx1_QrDocs')
prototype
- This can be done using ES6 JS's
- Use an
if
ES6 JS statement to filter our Type event and check to see if it matches released- This can be done using ES6 JS's Equality
==
or Strict Equality===
operators - We won't be using
pressedorclicked
- This can be done using ES6 JS's Equality
- Use the
.split('~')
ES6 JS prototype to split out our app, action and url and assign them to objects using our tilde as the string separator - Use a
switch
ES6 JS statement to handleOpenSite
andOpenQrCode
actions respectively- Place console.log(action, url) below the
OpenSite
case and console.warn(action, url) below theOpenQrCode
case
- Place console.log(action, url) below the
Click the Tabs Below to see how each Task above is implemented
import xapi from 'xapi'; xapi.Event.UserInterface.Extensions.Widget.Action.on(({ WidgetId, Type, Value }) => { console.debug({ WidgetId, Type, Value }); // <-- Changed to Debug if (WidgetId.includes(`wx1_QrDocs`)) { // <-- Asks if WidgetIn includes wx1_QrDocs }; });
Why Use
includes()
?We structured all of our WidgetIds with the same prefix wx1_QrDocs
This helps make our Widget References Unique, but also helps us ignore any widgets that don't include wx1_QrDocs
If a solution were to have the same widgetId, then that 1 widget could have solutions fire from 2 different integration sources. In some cases, that's fine or even necessary, but it's best practice to make your widgets as unique to your solution as possible and for your solution to ignore any other chatter on the Codec
import xapi from 'xapi'; xapi.Event.UserInterface.Extensions.Widget.Action.on(({ WidgetId, Type, Value }) => { console.debug({ WidgetId, Type, Value }); if (WidgetId.includes(`wx1_QrDocs`)) { if (Type == 'released') { // <-- Asked if the Type is equal to released }; }; });
import xapi from 'xapi'; xapi.Event.UserInterface.Extensions.Widget.Action.on(({ WidgetId, Type, Value }) => { console.debug({ WidgetId, Type, Value }); if (WidgetId.includes(`wx1_QrDocs`)) { if (Type == 'released') { const [app, action, url] = WidgetId.split(`~`); // <-- Extract our App, Action and Url and assigned them to the object they represent }; }; });
Why
.split()
our widget?This gives use new objects to work with. The benefit of how we implemented our WidgetId structure[app~action~url] allows us to make our code a bit more flexible
Now, if you wanted to add more sites, all you would need to do is add more buttons to the UI Extension following the WidgetId structure we set, and your Macro Code doesn't need to be modified to allow it to work
import xapi from 'xapi'; xapi.Event.UserInterface.Extensions.Widget.Action.on(({ WidgetId, Type, Value }) => { console.debug({ WidgetId, Type, Value }); if (WidgetId.includes(`wx1_QrDocs`)) { if (Type == 'released') { const [app, action, url] = WidgetId.split(`~`); switch (action) { // <-- Switch what code we run when: case 'OpenSite': // <-- The OpenSite action comes in console.log(action, url); break; case 'OpenQrCode':// <-- The OpenQrCode action comes in console.warn(action, url); break; default: // <-- The When an Unknown action comes in console.error(`Unknown Action Fired: [${action}]`) break; }; }; }; });
Why not use
if
statement instead ofswitch
If statements could work, but as you use more and more if statements, the code becomes a bit harder to read
Using switches, if we wanted to define another action, it would be as simple as adding another case and adding codec below it to define how that case runs
Both do the same job, some implementations are easier on the eyes than others
-
Once complete, Save and Activate the macro (if inactive)
-
Start pressing the Open Site and Open QrCode 🔳 buttons contained within your
Room Docs
panel - Monitor the Macro Console Output
- Replace your original console.log with console.debug
Compare your Macro
import xapi from 'xapi';
xapi.Event.UserInterface.Extensions.Widget.Action.on(({ WidgetId, Type, Value }) => {
console.debug({ WidgetId, Type, Value });
if (WidgetId.includes(`wx1_QrDocs`)) {
if (Type == 'released') {
const [app, action, url] = WidgetId.split(`~`);
switch (action) {
case 'OpenSite':
console.log(action, url);
break;
case 'OpenQrCode':
console.warn(action, url);
break;
default:
console.error(`Unknown Action Fired: [${action}]`)
break;
};
};
};
});
Compare your Macro Console
Time | Macro | Logs |
---|---|---|
HH:MM:SS | Room Docs | QJS Ready |
HH:MM:SS | Room Docs | OpenQrCode https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/ |
HH:MM:SS | Room Docs | OpenSite https://www.webex.com/us/en/workspaces.html |
HH:MM:SS | Room Docs | OpenSite https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/ |
HH:MM:SS | Room Docs | OpenQrCode https://www.webex.com/us/en/workspaces.html |
Define and finalize solution (rxe-m1.3)
Info
Now that we can access the data from our widgets as we expect, we can start to implement the function of opening a Web Page on out touch interface
Lesson: Defining our OpenSite
function (rxe-m1.3.1)
Important
We'll be using the WebView Display API
If you're working on a Desk or Board Series Endpoint use the parameter Target: OSD in the following lessons
If you're working on a Room Series Endpoint with a Room Navigator attached use the parameter Target: Controller in the following lessons
Note
- Room Series with 3rd Party Touch can use OSD as a Target value
- Room Series with Touch 10 CAN NOT use the Controller Target Value
- If you have no touch display or room Navigator, you will need to build a WebView Close Panel
- This is not covered in this lab
-
xAPI: xCommand UserInterface WebView Display
-
Task:
- Declare a new async function called openSite with the following function parameters
- url
- target = 'OSD'
- Note: change OSD to Controller if a Room Navigator touch panel is available
- Within this function:
- Structure the xAPI reference above using Macro Syntax with the following parameters and values
- Url:
url
- Target:
target
- Url:
- Log a Successful response from your xAPI call
- Catch and log an error from your xAPI call
- Structure the xAPI reference above using Macro Syntax with the following parameters and values
- Replace
console.log(action, url);
under the OpenSite case within your WidgetActions event with this new openSite() function call and pass in theurl
object into your function
View
openSite(url, target)
functionasync function openSite(url, target = 'OSD') { try { const openPage = await xapi.Command.UserInterface.WebView.Display({ Url: url, Target: target }) console.log(`Site Webview Opened for: [${url}]`, openPage); } catch (e) { const err = { Context: `Failed to open Site WebView to: [${url}]`, ...e }; console.error(err); } }
-
Once complete, Save and Activate the macro (if inactive)
-
Start pressing the Open Site and Open QrCode 🔳 buttons contained within your
Room Docs
panel - Monitor the Device OSD and Macro Console Output
- Declare a new async function called openSite with the following function parameters
Compare your Macro
import xapi from 'xapi';
async function openSite(url, target = 'OSD') { //<-- Declare and define your openSite function
try {
const openPage = await xapi.Command.UserInterface.WebView.Display({
Url: url,
Target: target
})
console.log(`Site Webview Opened for: [${url}]`, openPage); //<-- Log a Successful Response
} catch (e) {
const err = {
Context: `Failed to open Site WebView to: [${url}]`,
...e
};
console.error(err); //<-- Catch and log an Error
}
}
xapi.Event.UserInterface.Extensions.Widget.Action.on(({ WidgetId, Type, Value }) => {
console.debug({ WidgetId, Type, Value });
if (WidgetId.includes(`wx1_QrDocs`)) {
if (Type == 'released') {
const [app, action, url] = WidgetId.split(`~`);
// ↑ url object
switch (action) {
case 'OpenSite':
openSite(url); //<-- Run the openSite() function and pass in the url object
break;
case 'OpenQrCode':
console.warn(action, url);
break;
default:
console.error(`Unknown Action Fired: [${action}]`);
break;
};
};
};
});
Compare your Macro Console
Time | Macro | Logs |
---|---|---|
HH:MM:SS | Room Docs | QJS Ready |
HH:MM:SS | Room Docs | Site Webview Opened for: [https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/] |
HH:MM:SS | Room Docs | OpenQrCode https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/ |
HH:MM:SS | Room Docs | Site Webview Opened for: [https://www.webex.com/us/en/workspaces.html] |
HH:MM:SS | Room Docs | OpenQrCode https://www.webex.com/us/en/workspaces.html |
Lesson: Defining our OpenQrCode
function (rxe-m1.3.2)
Making QR Codes is NOT a normal function of the Codec
In this lesson, in order to generate a QR code for our customization, then we'll need to leverage a 3rd party QR API Service
This is how you can introduce more functionality to your device by leveraging the World of APIs
Always Keep Security Top of Mind!
It's easy to fall outside your organization's security policy especially when building new solutions.
Make sure you work with your, or your Customer's, Information Security teams before implementing a new solution that requires Integration Authentication of any kind or reaches out on the internet.
The security implications of a solution can force you building your solution using a different integration method, or even stop a project all together.
-
xAPI: xCommand UserInterface WebView Display
-
Task:
- Declare a new async function called openQrCode with the following function parameters
- url
- target = 'OSD'
- Note: change OSD to Controller if a Room Navigator touch panel is available
- Within this function:
- Use ES6 JS's
encodeURI()
function to modify the URL in and assign it to a new object calledencodedUrl
- This is because we'll be wrapping our target URL within a URL from our QR Code Service
- Add your
encodedUrl
object to the data Url Parameter in the URL and assign it to a new object calledqrURL
https://api.qrserver.com/v1/create-qr-code/?data=encodedUrl
- Structure the xAPI reference above using Macro Syntax with the following parameters and values
- Url:
formattedURL
- Target:
target
- Url:
- Log a Successful response from your xAPI call
- Catch and log an error from your xAPI call
- Use ES6 JS's
- Replace
console.warn(action, url);
under the OpenQrCode case within your WidgetActions event with this new openQrCode() function call and pass in theurl
object into your function
View
openQrCode(url, target)
functionasync function openQrCode(url, target = 'OSD') { //<-- Declare and define your openQrCode function const encodedUrl = encodeURI(url); //<-- Encode the URL that was provided const qrURL = `https://api.qrserver.com/v1/create-qr-code/?data=` + encodedUrl; try { const openPage = await xapi.Command.UserInterface.WebView.Display({ Url: qrURL, Target: target }) console.log(`QR Webview Opened for: [${url}]`, openPage, qrURL); //<-- Log a Successful Response } catch (e) { const err = { Context: `Failed to open QR WebView to: [${url}]`, QrUrl: qrURL, ...e }; console.error(err); //<-- Catch and log an Error } }
-
Once complete, Save and Activate the macro (if inactive)
-
Start pressing the Open Site and Open QrCode 🔳 buttons contained within your
Room Docs
panel - Monitor the Device OSD and Macro Console Output
- Declare a new async function called openQrCode with the following function parameters
Compare your Macro
import xapi from 'xapi';
async function openSite(url, target = 'OSD') {
try {
const openPage = await xapi.Command.UserInterface.WebView.Display({
Url: url,
Target: target
})
console.log(`Site Webview Opened for: [${url}]`, openPage);
} catch (e) {
const err = {
Context: `Failed to open Site WebView to: [${url}]`,
...e
};
throw new Error(err);
}
}
async function openQrCode(url, target = 'OSD') { //<-- Declare and define your openQrCode function
const encodedUrl = encodeURI(url); //<-- Encode the URL that was provided
const qrURL = `https://api.qrserver.com/v1/create-qr-code/?data=` + encodedUrl;
try {
const openPage = await xapi.Command.UserInterface.WebView.Display({
Url: qrURL,
Target: target
})
console.log(`QR Webview Opened for: [${url}]`, openPage, qrURL); //<-- Log a Successful Response
} catch (e) {
const err = {
Context: `Failed to open QR WebView to: [${url}]`,
QrUrl: qrURL,
...e
};
console.error(err); //<-- Catch and log an Error
}
}
xapi.Event.UserInterface.Extensions.Widget.Action.on(({ WidgetId, Type, Value }) => {
console.debug({ WidgetId, Type, Value });
if (WidgetId.includes(`wx1_QrDocs`)) {
if (Type == 'released') {
const [app, action, url] = WidgetId.split(`~`);
// ↑ url object
switch (action) {
case 'OpenSite':
openSite(url);
break;
case 'OpenQrCode':
openQrCode(url); //<-- Run the openQrCode() function and pass in the url object
break;
default:
console.error(`Unknown Action Fired: [${action}]`);
break;
};
};
};
});
Compare your Macro Console
Time | Macro | Logs |
---|---|---|
HH:MM:SS | Room Docs | QJS Ready |
HH:MM:SS | Room Docs | Site Webview Opened for: [https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/] |
HH:MM:SS | Room Docs | QR Webview Opened for: [https://webexcc-sa.github.io/LAB-1451/wx1_1451_part_3/] |
HH:MM:SS | Room Docs | Site Webview Opened for: [https://www.webex.com/us/en/workspaces.html] |
HH:MM:SS | Room Docs | QR Webview Opened for: [https://www.webex.com/us/en/workspaces.html] |
Abstract
🥳 You made a UI and a Macro 🎉
But we can do a few more things to make this a really rugged solution that enabled simpler deployment and allows us to scale a bit better
And we can do this in several ways