comment 0

CWS: Part 2

In this part, I’m going to look at adding route handlers to our CWS server. I’ll continue adding to the program we wrote in CWS: Part 1, so grab those files from GitHub if you’d like to follow along.

Hello World

Let’s add a new class named HelloRequest to our project:

using System;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;

namespace CwsDemo
{
    public class HelloRequest : IHttpCwsHandler
    {
        public void ProcessRequest(HttpCwsContext context)
        {
            var method = context.Request.HttpMethod;

            switch (method)
            {
                case "GET":
                    context.Response.StatusCode = 200;  // OK
                    context.Response.Write("Hello World!", true);
                    break;
                default:
                    context.Response.StatusCode = 501;  // Not implemented
                    context.Response.Write(
                        String.Format("{0} method not implemented!", method), true);
                    break;
            }
        }
    }
}

This class inherits the IHttpCwsHandler interface so we can use it to process CWS requests. All we do in this handler is check to see if we get an HTTP GET request, otherwise, we return an error code (method not implemented).

But how do we bolt this class onto our CWS server? Let’s revisit InitializeSystem in our ControlSystem class:

public override void InitializeSystem()
{
    try
    {
        _api = new HttpCwsServer("/api");

        var hello = new HttpCwsRoute("hello/");
        hello.RouteHandler = new HelloRequest();
        _api.AddRoute(hello);

        _api.Register();
    }
    catch (Exception e)
    {
        ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
    }
}

We create a new HttpCwsRoute that handles the hello/ path. Then, we assign a new HelloRequest to handle requests to this route. When our CWS server sees the matching route, the request is dispatched to that RouteHandler.

You’ll notice that we replaced the generic event handler with this route instead. If we’d left it in, it would still process the incoming request, but so would our HelloRequest class. This might be desirable behavior in some cases, but not in this case.

Now to test this, I’m going to use Postman. This lets me save API testing routes so I can quickly run them again when we make changes to the program. Here are examples of making GET and PUT requests to our CWS server:

GET request = 200 OK
PUT request = 501 Not Implemented

Hello {Your Name Here}

One area that is horribly documented is the URL pattern matching rules. There’s a brief mention of it in the help file:

URL pattern cannot start with a '/' or '~' character and it cannot contain a '?' character. It should not also start with the virtual folder the server is registered for, i.e. {device}/Presets/{id}/Recall is correct while /API/{device}/Presets/{id}/Recall is not in both ways.

Why are some parts of the path enclosed in curly braces? You can capture parts of the URL and bind them to names so that you can reference them in the request handler later on. For example, if we add a second handler to our ControlSystem class, we can capture user names with it:

public override void InitializeSystem()
{
    try
    {
        _api = new HttpCwsServer("/api");

        var hello = new HttpCwsRoute("hello/");
        hello.RouteHandler = new HelloRequest();
        _api.AddRoute(hello);

        var helloName = new HttpCwsRoute("hello/{NAME}");
        helloName.RouteHandler = new HelloRequest();
        _api.AddRoute(helloName);

        _api.Register();
    }
    catch (Exception e)
    {
        ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
    }
}

Now we can match the hello/ URL, but we can also match if a name shows up at the end. In the HelloRequest handler, we can check to see if this name was passed in:

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

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

            if (context.Request.RouteData.Values.ContainsKey("NAME"))
            {
                var name = context.Request.RouteData.Values["NAME"];

                context.Response.Write(
                    String.Format("Hello, {0}!", name),
                    true);
            }
            else
            {
                context.Response.Write("Hello World!", true);
            }
            break;
        default:
            context.Response.StatusCode = 501;  // Not implemented
            context.Response.Write(
                String.Format("{0} method not implemented!", method), true);
            break;
    }
}

We check if context.Request.RouteData.Values contains any of the keys we tried to bind to. In this case, we’re looking for NAME (which we passed in as {NAME} to our URL pattern). If it exists, we greet the user by name. If not, they get Hello World.

Hello JSON

OK time to do something a bit more practical. Let’s add the ability to update the stored name to something besides “World,” and while we’re at it, let’s switch to JSON to exchange data instead of HTML. We’ll need to add a reference to Newtonsoft JSON to our project:

In HelloRequest.cs, we’ll pull in the JSON and Crestron I/O namespaces:

using System;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;

namespace CwsDemo
{

First, let’s give our HelloRequest class some shared state. Normally, we’d use some type of configuration class to store this permanently somewhere, so this is just for illustrative purposes:

public class HelloRequest : IHttpCwsHandler
{
    private static string _name = "World";

    public void ProcessRequest(HttpCwsContext context)
    {

Now lets change how we reply to GET requests:

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

        var temp = new Dictionary<string, string>();

        if (context.Request.RouteData.Values.ContainsKey("NAME"))
        {
            temp["name"] = context.Request.RouteData.Values["NAME"].ToString();
            context.Response.Write(JsonConvert.SerializeObject(temp), true);
        }
        else
        {
            temp["name"] = _name;
            context.Response.Write(JsonConvert.SerializeObject(temp), true);
        }
        break;
    default:

Whoa, there’s a lot going on here. We set context.Response.ContentType to application/json so that the client knows we’re sending JSON data now, not HTML. Then we create a Dictionary named temp since it’s a good representation of a simple JavaScript object. If the request route contains a name, we pack that into our temp object and return it, serialized as JSON. If it’s just a request to our hello/ route, we pack the class _name into our temp object and return that.

Lets give our class a way to update _name. Add a case for PUT just below GET:

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

    using (var reader = new StreamReader(context.Request.InputStream))
    {
        var obj = JsonConvert.DeserializeObject<Dictionary<string, string>>(reader.ReadToEnd());
        
        if (obj.ContainsKey("name"))
        {
            _name = obj["name"];
            context.Response.Write("{ \"status\": \"OK\" }", true);
        }
    }

    break;
default:

This will take a PUT request with a JSON body and update _name in our class with whatever is passed in. We expect an object that looks something like this in the HTTP body:

{
  "name": "Kiel"
}

Build this and load it to your processor. If you use Postman to send a put request to /cws/api/hello, you can change the name stored in the HelloRequest class:

And if I try to hit the same URL from Firefox, I get:

Next Time

I’d like to provide a nice system configuration page where users can change the room name, inputs, outputs, and contact information without having to rebuild or restart the program. I think this may entail a bit more work than can be squeezed into one post, but hopefully we’ll get there.

The code for this part is available on GitHub if you’d like to grab a copy.

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