Networked Media Open Specifications

How To practical examples

←Controller implementation tutorial · Index↑

Introduction

This section showcases practical examples on the journey to implementing MS-05 / IS-12 in a device and as such provides a practical example of how to implement an NMOS Control Framework Device on a Linux system using the NMOS Control Mock Device.

Note: The NMOS Control Mock Device application is a cross platform application and can run on Linux, MacOS and Windows.

The HOWTO is meant to provide a relatively simple, easy-to-follow “recipe” for getting a controllable NMOS Node up and running in a short period of time. It is not intended to be a tutorial article, and therefore, it excludes explanation regarding the NMOS Control Framework. The reader is referred to the tutorial section for more depth coverage of the framework.

The reader is assumed to have some experience with an NMOS infrastructure including IS-04 and NMOS Registration and Discovery (RDS). Also some experience in JavaScript and/or typescript will be helpful although not entirely required.

This HOW-TO also demonstrates to a certain extent how to construct and operate an NMOS Controller that interacts with the NMOS Control Device. In the HOW-TO, you will be the controller - creating, issuing and reading information from JSON commands and responses sent and received via a standard WebSocket connecting you with the controlled device.

HOWTO Steps

This HOWTO will present the steps to install, modify and try out the NMOS Control Framework. The HOW-TO uses a fresh Ubuntu 20.04 installation.

Basic Installation

This section will cover the basic steps to get a mock NMOS Controllable Node running on your system:

Non-standard control classes

This section will explain how the NCMD has created a non-standard control class to expose vendor specific functionality in the form of a GainControl class.

Installing the Mock NMOS Control Device

Install the NMOS Mock control device from its location on the public GitHub repo.

Follow the steps below from any directory on your Ubuntu host:

Install the required packages

You will need docker for running the NMOS RDS and npm for running the mock NMOS Controllable Node. We first assume a fresh install of Ubuntu 20.04 and so do an update for packages prior to installation of required dependencies.

sudo apt-get update
sudo apt install -y npm
sudo apt install -y docker.io

Now install an NMOS RDS. We will use the RDS from EasyNMOS

You will run the RDS from one terminal window and the mock node from another. Open a terminal window and perform the following commands:

sudo docker pull rhastie/nmos-cpp:latest

Expected Output

latest: Pulling from rhastie/nmos-cpp
3b65ec22a9e9: Pull complete 
964e9f4b2501: Pull complete 
5312be12420b: Pull complete 
037321a10163: Pull complete 
4f4fb700ef54: Pull complete 
Digest: sha256:bd2cdeb5263d555cfe0e427099251d287f3f343af09789e82354deb3049d4a2d
Status: Downloaded newer image for rhastie/nmos-cpp:latest
docker.io/rhastie/nmos-cpp:latest

Run and Verify the RDS

sudo docker run -d -p 80:8010 -p 8011:8011 --name RDS rhastie/nmos-cpp:latest
sudo docker ps

Expected Output

The output of sudo docker ps will show the EasyNMOS NMOS docker image is up and running and required ports are mapped from the host’s network to the docker container network.

CONTAINER ID   IMAGE                     COMMAND                 CREATED         STATUS         PORTS                                                                                                                   NAMES
9a8890946623   rhastie/nmos-cpp:latest   "/home/entrypoint.sh"   9 seconds ago   Up 8 seconds   1883/tcp, 11000-11001/tcp, 5353/udp, 0.0.0.0:8011->8011/tcp, :::8011->8011/tcp, 0.0.0.0:80->8010/tcp, :::80->8010/tcp   RDS

Verify the web server in the EasyNMOS docker container is up and running by opening http://localhost/admin on your host. You should see the welcome screen for the open-source NVIDIA NMOS Commissioning Controller.

Now install the mock device from the repo:

git clone https://github.com/AMWA-TV/nmos-device-control-mock.git
cd nmos-device-control-mock/code
npm install
npm run build-and-start

Expected output

The output of npm run build-and-start will show status as the mock node is built and ran. The last few lines of the output will show the mock device registering with the RDS running as part of the docker image started above.

App started
Configuration: Reading config.json
Configuration - CheckIdentifiers()
Configuration - CheckDistinguishingInformation()
RegistrationClient - RegisterOrUpdateResource(resourceType:node)
Server started on port 8080
RegistrationClient - RegisterOrUpdateResource(resourceType:device)
RegistrationClient - RegisterOrUpdateResource(resourceType:receiver)

Locate the NMOS IS-12 Control WebSocket

You now have all the NMOS items needed to interact with the NMOS Control mock node. Since IS-12 uses a WebSocket control endpoint we will next browse the RDS registry to find the advertised WebSocket endpoint and use a Chrome extension that allows opening that WebSocket and sending and receiving IS-12 JSON formatted commands and responses.

Navigate in your preferred browser to the devices query location: http://127.0.0.1/x-nmos/query/v1.3/devices/

Expected output

[
  {
    "controls": [
      {
        "href": "http://127.0.0.1:8080/x-nmos/connection/v1.1/",
        "type": "urn:x-nmos:control:sr-ctrl/v1.1"
      },
      {
        "href": "http://127.0.0.1:8080/x-nmos/connection/v1.0/",
        "type": "urn:x-nmos:control:sr-ctrl/v1.0"
      },
      {
        "href": "ws://127.0.0.1:8080/x-nmos/ncp/v1.0/connect",
        "type": "urn:x-nmos:control:ncp/v1.0"
      }
    ],
    "description": "NC-01 device",
    "id": "7977373c-70f6-4e62-b713-3431f1ac4a2f",
    "label": "NC-01 device",
    "node_id": "e0f9e1a3-2a2f-4f00-b02e-76d8286e1d98",
    "receivers": [
      "18eae0e9-dcf4-40ff-88a3-cb553993d1b8"
    ],
    "senders": [],
    "tags": {
      "urn:x-nmos:tag:asset:function/v1.0": [
        "UHD Decoder"
      ],
      "urn:x-nmos:tag:asset:instance-id/v1.0": [
        "XYZ123-456789-D"
      ],
      "urn:x-nmos:tag:asset:manufacturer/v1.0": [
        "ACME-D"
      ],
      "urn:x-nmos:tag:asset:product/v1.0": [
        "Widget Pro-D"
      ]
    },
    "type": "urn:x-nmos:device:generic",
    "version": "1686060930:0"
  }
]

In the controls section of the JSON response you will find a control with the type of urn:x-nmos:control:ncp/v1.0. The href of this control is the uri we need to connect to:

ws://127.0.0.1:8080/x-nmos/ncp/v1.0/connect

This is the IS-12 WebSocket control endpoint used to interact with the device.

Install Chrome WebSocket Plugin

We will install a Chrome WebSocket extension. Other WebSocket clients will work equally well.

Details on installing Chrome extensions can be found here. Follow the instructions to open the Chrome Web Store and search for WebSocket King Client. Install this extension.

After completing the installation of WebSocket King open the extension in a new browser window. Copy and paste the WebSocket located in the RDS for the NC-01 NMOS Control Mock node into the connections field and click connect. The Connect button should turn to Disconnect indicating a successful connection to the NC-01 WebSocket endpoint.

Next we will verify the ability to read and write to the device model on the mock node.
For purposes of this HOW-TO we will focus on working with the root block and its properties. Note that in an actual system most of the manual steps we are performing here would be performed by an NMOS Controller using the IS-12 specification. For more information about implementing an IS-12 NMOS Controller see the HOW-TO section for Controller implementations.

Next retrieve the members of the root block by sending the following JSON formatted command to the NC-01 WebSocket. In the JSON command the value of oid 1 indicates we are directing this command at the root block. The methodId with level 1 and index 1 is the getter method and the id level and index of 2 and 2 respectively targets the 2p2 members property of the root block.

{
  "messageType": 0,
  "commands": [
    {
      "handle": 1,
      "oid": 1,
      "methodId": {
        "level": 1,
        "index": 1
      },
      "arguments": {
        "id": {
          "level": 2,
          "index": 2
        }
      }
    }
  ]
}

Expected Output

The device responds with a JSON containing NcBlockMemberDescriptor member descriptors for the root block. Every member class can be determined by reading the classId field and they can be targeted with commands by using their oid value.

{
  "messageType": 1,
  "responses": [
    {
      "handle": 1,
      "result": {
        "status": 200,
        "value": [
          {
            "role": "DeviceManager",
            "oid": 2,
            "constantOid": true,
            "classId": [
              1,
              3,
              1
            ],
            "userLabel": "Device manager",
            "owner": 1,
            "description": "The device manager offers information about the product this device is representing"
          },
          {
            "role": "ClassManager",
            "oid": 3,
            "constantOid": true,
            "classId": [
              1,
              3,
              2
            ],
            "userLabel": "Class manager",
            "owner": 1,
            "description": "The class manager offers access to control class and data type descriptors"
          },
          {
            "role": "receivers",
            "oid": 10,
            "constantOid": true,
            "classId": [
              1,
              1
            ],
            "userLabel": "Receivers",
            "owner": 1,
            "description": "Receivers block"
          },
          {
            "role": "stereo-gain",
            "oid": 31,
            "constantOid": true,
            "classId": [
              1,
              1
            ],
            "userLabel": "Stereo gain",
            "owner": 1,
            "description": "Stereo gain block"
          },
          {
            "role": "ExampleControl",
            "oid": 111,
            "constantOid": true,
            "classId": [
              1,
              2,
              0,
              2
            ],
            "userLabel": "Example control worker",
            "owner": 1,
            "description": "Example control worker"
          },
          {
            "role": "IdentBeacon",
            "oid": 51,
            "constantOid": true,
            "classId": [
              1,
              2,
              2
            ],
            "userLabel": "Identification beacon",
            "owner": 1,
            "description": "Identification beacon"
          }
        ]
      }
    }
  ]
}

In the response above we can see that the “Stereo gain” member is a block because of its classId of [1, 1]. That means we can next retrieve the members of the “Stereo gain” block by sending the following JSON formatted command to the NC-01 WebSocket. In the JSON command the value of oid 31 indicates we are directing this command at the “Stereo gain” object.

{
  "messageType": 0,
  "commands": [
    {
      "handle": 2,
      "oid": 31,
      "methodId": {
        "level": 1,
        "index": 1
      },
      "arguments": {
        "id": {
          "level": 2,
          "index": 2
        }
      }
    }
  ]
}

Expected Output

The device responds with a JSON containing the member descriptors for the “Stereo gain” block.

{
  "messageType": 1,
  "responses": [
    {
      "handle": 2,
      "result": {
        "status": 200,
        "value": [
          {
            "role": "channel-gain",
            "oid": 21,
            "constantOid": true,
            "classId": [
              1,
              1
            ],
            "userLabel": "Channel gain",
            "owner": 31,
            "description": "Channel gain block"
          },
          {
            "role": "master-gain",
            "oid": 24,
            "constantOid": true,
            "classId": [
              1,
              2,
              0,
              1
            ],
            "userLabel": "Master gain",
            "owner": 31,
            "description": "Master gain"
          }
        ]
      }
    }
  ]
}

Repeating the process of retrieving the members for all nested blocks allows a controller to discover the entire device model.

Read, Write the root block user label

You will now make use of the generic Get method (level 1, index 1) to read the value of the userLabel (level 1, index 6) property of the root block. All classes contain the userLabel property as defined in NcObject.

{
  "messageType": 0,
  "commands": [
    {
      "handle": 3,
      "oid": 1,
      "methodId": {
        "level": 1,
        "index": 1
      },
      "arguments": {
        "id": {
          "level": 1,
          "index": 6
        }
      }
    }
  ]
}

Expected Output

The device responds to the above command with a JSON formatted response containing a result of type NcMethodResultPropertyValue holding the value of “Root”.

{
  "messageType": 3,
  "responses": [
    {
      "handle": 3,
      "result": {
        "status": 200,
        "value": "Root"
      }
    }
  ]
}

Now we will set the userLabel (1p6) to “My mock device HOWTO” and verify the change has taken effect. We will use the generic Set method for this (level 1, index 2). Copy and paste the following JSON formatted command to set the new value:

{
  "messageType": 0,
  "commands": [
    {
      "handle": 4,
      "oid": 1,
      "methodId": {
        "level": 1,
        "index": 2
      },
      "arguments": {
        "id": {
          "level": 1,
          "index": 6
        },
        "value": "My mock device HOWTO"
      }
    }
  ]
}

Expected result

The command should be accepted with no errors. The JSON response to the command should indicate a status of 200 (Ok).

Now retrieve the userLabel property again.

{
  "messageType": 0,
  "commands": [
    {
      "handle": 5,
      "oid": 1,
      "methodId": {
        "level": 1,
        "index": 1
      },
      "arguments": {
        "id": {
          "level": 1,
          "index": 6
        }
      }
    }
  ]
}

Expected result

The device returns the new value of “My mock device HOWTO” in the response.

{
  "messageType": 1,
  "responses": [
    {
      "handle": 5,
      "result": {
        "status": 200,
        "value": "My mock device HOWTO"
      }
    }
  ]
}

Subscribe to property changes for the userLabel

Add a subscription to changes on the root block by sending a Subscription message. Paste in the JSON formatted command below to subscribe for changes. Note that the message targets the root block by including its oid of 1 in the subscriptions array.

{
  "messageType": 3,
  "subscriptions": [
    1
  ]
}

Expected result

The device will respond with a SubscriptionResponse message indicating the subscription request was accepted.

{
  "messageType": 4,
  "subscriptions": [
    1
  ]
}

Modify the userLabel value and check a notification is received

Now whenever we modify the value of the userLabel property we can see notifications arriving.

Try it now by changing the userLabel property again to “My mock device HOWTO changed”

{
  "messageType": 0,
  "commands": [
    {
      "handle": 6,
      "oid": 1,
      "methodId": {
        "level": 1,
        "index": 2
      },
      "arguments": {
        "id": {
          "level": 1,
          "index": 6
        },
        "value": "My mock device HOWTO changed"
      }
    }
  ]
}

Expected notification

Since you subscribed for changes on the root block you should see notifications of that change. In the JSON notification you see the oid of the root block along with the propertyId that was changed (userLabel or 1p6). Finally, the changeType and new property value are provided in the eventData.

{
  "messageType": 2,
  "notifications": [
    {
      "oid": 1,
      "eventId": {
        "level": 1,
        "index": 1
      },
      "eventData": {
        "propertyId": {
          "level": 1,
          "index": 6
        },
        "changeType": 0,
        "value": "My mock device HOWTO changed",
        "sequenceItemIndex": null
      }
    }
  ]
}

Section conclusions

In this section of the HOW-TO guide you have setup all required NMOS infrastructure to run and interacted with an NMOS Device that provides NMOS IS-12 functionality. You have provisioned an NMOS RDS so that the NMOS device can register its control endpoint for IS-12 in the form of a standard WebSocket. You have installed the NC-01 mock device and used it to explore the device model by finding nested block members. You have also retrieved and set the userLabel property of the root block. You have used manual copy-and-paste of JSON protocol messages to act as an human-in-the-loop NMOS Controller.

In addition you have subscribed to property changes for root block and verified your WebSocket session has received notifications when you changed the userLabel property.

Non-standard control classes

In the previous section we have learned how to explore the device model by getting the members of each nested block. One of the blocks was the “Stereo gain” block which returned the following member descriptors:

{
  "messageType": 1,
  "responses": [
    {
      "handle": 2,
      "result": {
        "status": 200,
        "value": [
          {
            "role": "channel-gain",
            "oid": 21,
            "constantOid": true,
            "classId": [
              1,
              1
            ],
            "userLabel": "Channel gain",
            "owner": 31,
            "description": "Channel gain block"
          },
          {
            "role": "master-gain",
            "oid": 24,
            "constantOid": true,
            "classId": [
              1,
              2,
              0,
              1
            ],
            "userLabel": "Master gain",
            "owner": 31,
            "description": "Master gain"
          }
        ]
      }
    }
  ]
}

We can see that one of the members is the “Master gain” which is a non-standard control class because its classId property contains an authority key of 0 (see NcClassId in the framework for more details). The classId property also informs us that the class is derived from the standard class NcWorker.

One of the members of our root block discovered in the previous section was the Class Manager:

{
  "messageType": 1,
  "responses": [
    {
      "handle": 1,
      "result": {
        "status": 200,
        "value": [
          {
            "role": "DeviceManager",
            "oid": 2,
            "constantOid": true,
            "classId": [
              1,
              3,
              1
            ],
            "userLabel": "Device manager",
            "owner": 1,
            "description": "The device manager offers information about the product this device is representing"
          },
          {
            "role": "ClassManager",
            "oid": 3,
            "constantOid": true,
            "classId": [
              1,
              3,
              2
            ],
            "userLabel": "Class manager",
            "owner": 1,
            "description": "The class manager offers access to control class and data type descriptors"
          },
          ...
        ]
      }
    }
  ]
}

The Class Manager allows us to retrieve a class descriptor for our “Master gain” control by sending the following command for oid 3 representing the Class Manager, invoking method GetControlClass (3m1) with arguments including the classId of our “Master gain”.

{
  "messageType": 0,
  "commands": [
    {
      "handle": 7,
      "oid": 3,
      "methodId": {
        "level": 3,
        "index": 1
      },
      "arguments":
      {
        "classId": [1, 2, 0, 1],
        "includeInherited": false
      }
    }
  ]
}

Expected result

The device returns the class descriptor in the response. This contains a name, description and descriptors for all of its properties and methods.

{
  "messageType": 1,
  "responses": [
    {
      "handle": 7,
      "result": {
        "status": 200,
        "value": {
          "description": "GainControl class descriptor",
          "classId": [
            1,
            2,
            0,
            1
          ],
          "name": "GainControl",
          "fixedRole": null,
          "properties": [
            {
              "description": "Gain value",
              "id": {
                "level": 3,
                "index": 1
              },
              "name": "gainValue",
              "typeName": "NcFloat32",
              "isReadOnly": false,
              "isNullable": false,
              "isSequence": false,
              "isDeprecated": false,
              "constraints": null
            }
          ],
          "methods": [],
          "events": []
        }
      }
    }
  ]
}

This reveals that our “Master gain” object is an instance of the GainControl non-standard class which holds a numeric property named gainValue (level 3 index 1).

As exemplified in the previous section we can now use the Get and Set methods to retrieve and set the gainValue property.

Getting the value by targeting the “Master gain” oid of 24 and its gainValue property with level 3 and index 1:

{
  "messageType": 0,
  "commands": [
    {
      "handle": 8,
      "oid": 24,
      "methodId": {
        "level": 1,
        "index": 1
      },
      "arguments": {
        "id": {
          "level": 3,
          "index": 1
        }
      }
    }
  ]
}

Gives us the following response where the gainValue is 0.

{
  "messageType": 1,
  "responses": [
    {
      "handle": 8,
      "result": {
        "status": 200,
        "value": 0
      }
    }
  ]
}

And setting the value to 21

{
  "messageType": 0,
  "commands": [
    {
      "handle": 9,
      "oid": 24,
      "methodId": {
        "level": 1,
        "index": 2
      },
      "arguments": {
        "id": {
          "level": 3,
          "index": 1
        },
        "value": 21
      }
    }
  ]
}

Gives us the following response

{
  "messageType": 1,
  "responses": [
    {
      "handle": 9,
      "result": {
        "status": 200
      }
    }
  ]
}

And Getting the value again returns

{
  "messageType": 1,
  "responses": [
    {
      "handle": 10,
      "result": {
        "status": 200,
        "value": 21
      }
    }
  ]
}

Similar to the previous section we can now subscribe to the “Master gain” for property changes by adding its oid to our subscriptions list.

{
  "messageType": 3,
  "subscriptions": [
    1,
    24
  ]
}

A visual diagram which summarizes the steps taken in the mock device to implement the GainControl is available below.

Non standard-model
GainControl model

The full implementation of the GainControl can be viewed in the code base of the mock device repository

Section conclusions

In this section you learned how non-standard control classes are created and exposed by a device in order to include vendor specific functionality. You saw how simple inheritance can create additional capabilities whilst maintaining discoverability, subscription for change events and control over a standard WebSocket.

Overall conclusions

This HOW-TO has shown how to work with the NMOS Control Framework. You have explored a simple NMOS Device that uses NMOS IS-04 to advertise its control endpoint with an NMOS RDS. You have worked with the IS-12 protocol to discover a non-standard GainControl used to express the “Master gain” inside a “Stereo gain” block.

Further Directions

All code and APIs provided by NMOS are completely open and free for any purposes, private or commercial. You are encouraged to continue exploration of how the NMOS Control Framework can enable compelling User-Stories for your customers and differentiate your products in the expanding NMOS community of vendors and users while at the same time contributing to an open-standards based approach.

←Controller implementation tutorial · Index↑