comment 0

CWS: Part 4

This post is going to be the last one covering CWS (for now). We’ve created an API that lets us view the current system settings, now we just need a way to update them. In this part, I’ll go over how to take in and apply new settings.

Some Utilities

To make our lives easier, I’d like to add some helper methods to our SystemSettings class:

using System;

namespace CwsDemo
{
    public class SystemSettings
    {
        public const int MaxInputs = 8;
        public const int MaxOutputs = 8;

        public string name { get; set; }
        public string location { get; set; }
        public string helpNumber { get; set; }

        public string[] inputs { get; set; }
        public string[] outputs { get; set; }

        public SystemSettings()
        {
            inputs = new string[MaxInputs];
            outputs = new string[MaxOutputs];
        }

        public void Reset()
        {
            name = "My Room";
            location = "My Office";
            helpNumber = "5555";

            for (int i = 0; i < MaxInputs; i++)
            {
                inputs[i] = String.Format("Input {0}", i + 1);
            }

            for (int i = 0; i < MaxOutputs; i++)
            {
                outputs[i] = String.Format("Output {0}", i + 1);
            }
        }

        public void Copy(SystemSettings newSettings)
        {
            if (newSettings.name != null)
                name = newSettings.name;

            if (newSettings.location != null)
                location = newSettings.location;
            
            if (newSettings.helpNumber != null)
                helpNumber = newSettings.helpNumber;

            for (int i = 0; i < newSettings.inputs.Length; i++)
            {
                if (newSettings.inputs[i] != null)
                    inputs[i] = newSettings.inputs[i];
            }

            for (int i = 0; i < newSettings.outputs.Length; i++)
            {
                if (newSettings.outputs[i] != null)
                    outputs[i] = newSettings.outputs[i];
            }
        }
    }
}

I’ve moved the default values to a new method named Reset so we can call it once when our program starts rather than every time we create a SystemSettings object. This allows us to write the Copy method where we check to see if a value isn’t null before copying it.

This means we’ll have to update our constructor method in RoomRequest to call Reset after creating our _settings object:

public class RoomRequest : IHttpCwsHandler
{
    private SystemSettings _settings;

    public RoomRequest()
    {
        _settings = new SystemSettings();
        _settings.Reset();
    }

    public void ProcessRequest(HttpCwsContext context)
    {

The Backend

We’ve already created the scaffold for this part in the last post, so now we can flesh out ProcessRequest in RoomRequest:

public void ProcessRequest(HttpCwsContext context)
{
    var method = context.Request.HttpMethod;

    switch (method)
    {
        case "GET":
            context.Response.StatusCode = 200;  // OK
            context.Response.ContentType = "application/json";

            if (context.Request.RouteData.Values.ContainsKey("PROPERTY"))
            {
                var prop = context.Request.RouteData.Values["PROPERTY"];
                context.Response.Write(GetPropertyJSON(prop.ToString()), true);
            }
            else
            {
                try
                {
                    context.Response.Write(JsonConvert.SerializeObject(_settings), true);
                }
                catch (Exception e)
                {
                    ErrorLog.Error("Exception in RoomRequest.ProcessRequest: {0}",
                        e.Message);
                }
            }

            break;
        case "PUT":
            context.Response.StatusCode = 200;  // OK
            context.Response.ContentType = "application/json";

            SystemSettings newSettings;

            try
            {
                using (var reader = new StreamReader(context.Request.InputStream))
                {
                    newSettings = JsonConvert.DeserializeObject<SystemSettings>
                        (reader.ReadToEnd());
                }

                _settings.Copy(newSettings);
            }
            catch (Exception e)
            {
                ErrorLog.Error("Exception in RoomRequest.ProcessRequest: {0}",
                    e.Message);
            }

            context.Response.Write("{ \"status\": \"OK\" }", true);
            break;
        default:
            context.Response.StatusCode = 501;  // Not implemented
            context.Response.Write(
                String.Format("{0} method not implemented!", method),
                true);
            break;
    }
}

Build the solution, load it to your processor, and now you can use Postman to check if it’s possible to change settings. Send the following PUT request to /cws/api/room/:

{
    "name": "Boardroom",
    "location": "Building 100",
    "helpNumber": "555-5555",
    "inputs": [
        "Laptop 1",
        "Laptop 2",
        "Room PC",
        "Cable TV",
        "Blu-ray"
    ],
    "outputs": [
        "Display 1",
        "Display 2"
    ]
}

Now Postman will return the new values you specified. Notice how anything we didn’t pass in is left unchanged:

The Frontend

We’re all done with the SIMPL# backend. Now let’s add a little more polish to the frontend. Start by adding a Save button to the top of settings.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="style.css">
    <title>System Settings</title>
</head>
<body>
    <header>
        <h1>System Settings</h1>
        <button id="save-changes">Save Changes</button>
    </header>

    <div class="group">
        <h2>Global Settings</h2>
        <div>
            <label for="name">Name:</label>
            <input type="text" id="name" />
        </div>
        <div>
            <label for="location">Location:</label>
            <input type="text" id="location" />
        </div>
        <div>
            <label for="helpNumber">Help Number:</label>
            <input type="text" id="helpNumber" />
        </div>
    </div>

    <div class="group" id="inputs">
        <h2>Inputs</h2>
    </div>

    <div class="group" id="outputs">
        <h2>Outputs</h2>
    </div>

    <script src="app.js"></script>
</body>
</html>

Add some styling to style.css for the new elements:

header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
}

#save-changes {
    visibility: hidden;
    background-color: rgb(0, 160, 0);
    color: white;
    border: solid 2px rgb(0, 80, 0);
    width: 200px;
    height: 50px;
}

We won’t see anything yet because the default visibility for our Save button is hidden.

The last part we’re going to add is more JavaScript to app.js:

const nameInput = document.getElementById('name');
const locationInput = document.getElementById('location');
const helpNumberInput = document.getElementById('helpNumber');

const inputs = document.getElementById('inputs');
const outputs = document.getElementById('outputs');

const saveBtn = document.getElementById('save-changes');
saveBtn.addEventListener('click', (e) => {
    putSettings();
});

We grab a reference to the new Save button we created and register an event handler for clicks. The event handler calls a new function named putSettings that we can define right underneath there:

async function putSettings () {
    newSettings = {
        name: nameInput.value,
        location: locationInput.value,
        helpNumber: helpNumberInput.value,
        inputs: [],
        outputs: []
    }

    Array.from(inputs.getElementsByTagName('INPUT')).forEach((item) => {
        newSettings.inputs.push(item.value);
    });

    Array.from(outputs.getElementsByTagName('INPUT')).forEach((item) => {
        newSettings.outputs.push(item.value);
    });

    let result = await fetch('/cws/api/room', {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(newSettings)
    });
    let data = await result.json();

    if (data.status === 'OK') {
        Array.from(document.getElementsByTagName('INPUT')).forEach((item) => {
            item.style.borderColor = 'black';
        });

        saveBtn.style.visibility = 'hidden';
    }
}

Note that we need to declare this function async because we use awaits with the fetch API. We build up a newSettings object that we’re going to send to our CWS API. The skeleton of this object is seen on lines 14 – 20. Then we grab each INPUT tag in the inputs collection and push its value onto our object. Same for outputs.

Since we need to use the PUT method to send new values to our API, we need to define the init parameter to fetch with the configuration we want to use. We tell it to use the PUT method, we’re sending JSON data, and the body contains our newSettings turned into a JSON string.

Then we wait for the reply to be OK before hiding the Save button and making sure all of our entry fields are set to black. Which brings me to the last bit we’ll update in getSettings:

async function getSettings () {
    let result = await fetch('/cws/api/room');
    let data = await result.json();

    nameInput.value = data.name;
    locationInput.value = data.location;
    helpNumberInput.value = data.helpNumber;

    let i = 1;
    
    data.inputs.forEach((item) => {
        let div = document.createElement('div');
        
        let label = document.createElement('label');
        label.setAttribute('for', `input-${i}`);
        label.innerText = `Input ${i}: `;
        div.appendChild(label);
        
        div.appendChild(document.createTextNode(' '));

        let textField = document.createElement('input');
        textField.setAttribute('type', 'text');
        textField.setAttribute('id', `input-${i}`);
        textField.setAttribute('value', item);
        div.appendChild(textField);

        inputs.appendChild(div);
        i += 1;
    });

    i = 1;

    data.outputs.forEach((item) => {
        let div = document.createElement('div');
        
        let label = document.createElement('label');
        label.setAttribute('for', `output-${i}`);
        label.innerText = `Output ${i}: `;
        div.appendChild(label);

        div.appendChild(document.createTextNode(' '));

        let textField = document.createElement('input');
        textField.setAttribute('type', 'text');
        textField.setAttribute('id', `output-${i}`);
        textField.setAttribute('value', item);
        div.appendChild(textField);

        outputs.appendChild(div);
        i += 1;
    });

    Array.from(document.getElementsByTagName('INPUT')).forEach((item) => {
        item.addEventListener('change', () => {
            item.style.borderColor = 'red';
            saveBtn.style.visibility = 'visible';
        });
    });
}

We add an event listener for changes to each text field. If they change, we turn the border red and show the Save button. Simple! Here’s a demo of it in action:

If I hit Save Changes and refresh the page, it keeps our settings intact:

The End

Thus ends my planned tour of CWS (Crestron Web Scripting), but there’s still plenty more left unexplored. We could add more routes, a better way to permanently store our configuration, threading awareness, you name it. This topic might come up again the future, but usually by Part 4, I’m tired of writing about it!

Grab the final code from GitHub if you would like to browse through it. Thanks for reading!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s