comment 0

VC-4: Websocket-Sharp

The first version of my web console bastardized the Control Concepts module. Since we’re targeting the VC-4, we have unrestricted access to standard libraries that fall outside the Crestron sandbox. In this part, I’ll swap out WebsocketServer.dll for a NuGet package containing a WebsocketServer class instead.

The Back-end

First, we need to manage the packages for our project. Right-click the project name in Solution Explorer and select Manage NuGet Packages. Select the Browse tab and type in websocket-sharp. You should see WebSocketSharpFork by sta appear in the list. Select it and click Install:

With this added to our project, we can remove WebsocketServer from our project references. Next, we’ll start modifying WebLogger.cs to use the new package:

using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using WebSocketSharp;
using WebSocketSharp.Server;

namespace WebsocketLogger
{

The WebSocketServer class uses behavior classes to handle client interaction, so we’ll end up moving a lot of our work into another class named WebLoggerService. I put this in the same file, just above the WebLogger class. Start with a skeleton, and we’ll fill it out together:

namespace WebsocketLogger
{
    class WebLoggerService : WebSocketBehavior
    {
        public WebLoggerService()
        {
        }

        protected override void OnOpen()
        {
        }

        protected override void OnClose(CloseEventArgs e)
        {
        }
    }

    class WebLogger
    {

I’m going to track the client connection status here, so let’s add that in first:

class WebLoggerService : WebSocketBehavior
{
    private bool _connected;

    public WebLoggerService()
    {
        _connected = false;
    }

    protected override void OnOpen()
    {
        _connected = true;
    }

    protected override void OnClose(CloseEventArgs e)
    {
        _connected = false;
    }
}

I’d also like to maintain the backlog of messages here, so add that next. I mostly just cut and pasted this from the WebLogger class below:

class WebLoggerService : WebSocketBehavior
{
    private List<string> _backlog;
    private bool _connected;

    public WebLoggerService()
    {
        _backlog = new List<string>();
        _connected = false;
    }

    protected override void OnOpen()
    {
        _connected = true;
        SendSerial(1, "-- CONNECTED --");

        if (_backlog.Count > 0)
        {
            foreach (var msg in _backlog)
                SendSerial(1, msg);
        }

        _backlog.Clear();
    }

    protected override void OnClose(CloseEventArgs e)
    {
        _connected = false;
    }

    protected void SendSerial(ushort channel, string text)
    {
        // Encode data to match CCI's format
        Send(String.Format("STRING[{0},{1}]", channel, text));
    }
}

The Control Concepts class had a SetIndirectTextSignal method that formatted the websocket message so the receiving client knew it was a Digital, Analog, or Serial signal. We’re doing the same thing with our SendSerial method so we don’t have to touch the JavaScript embedded in our web page. And to make logging a little easier, we’ll add a WriteLine method just below SendSerial:

public void WriteLine(string msg, params object[] args)
{
    var text = String.Format(msg, args);

    if (_connected)
        SendSerial(1, text);
    else
        _backlog.Add(text);
}

Now we can rewrite the WebLogger class to use this new behavior service:

class WebLogger
{
    private WebSocketServer _server;
    private WebLoggerService _logger;

    public WebLogger()
    {
        try
        {
            _server = new WebSocketServer(54321);
            _logger = new WebLoggerService();

            _server.AddWebSocketService<WebLoggerService>("/", () => _logger);
        }
        catch (Exception e)
        {
            ErrorLog.Error("Exception in WebLogger constructor: {0}", e.Message);
        }
    }

    public void Start()
    {
        _server.Start();
    }

    public void Stop()
    {
        _server.Stop();
    }

    public void WriteLine(string msg, params object[] args)
    {
        try
        {
            _logger.WriteLine(msg, args);
        }
        catch (Exception e)
        {
            ErrorLog.Error("Exception in WriteLine: {0}", e.Message);
        }
    }
}

Build your solution and update the program loaded on VC-4. Click Yes to let the server handle restarting our rooms:

The Front-end

There is one change I’d like to make to the JavaScript in our web page. It’s in the onMessage function:

// STRING[SIG,DATA]
else if (msg.indexOf("STRING[") == 0)
{
    const text = parseInt(getBoundString(msg, "STRING[", ","), 10);
    const value = getBoundString_EndLastIndex(msg, ",", "]");                   
                    
    // set receiving text
    const consoleElement = document.querySelector('#console');
    consoleElement.innerHTML += value + "\n";
}

We’ll add the newline at the end of the new text rather than encoding it into our message. OK, you should be able to open this page in your favorite web browser, and connect to the program running on VC-4 again:

Yay! We didn’t break anything!

Hit the Disconnect button then Clear. With a clean console, hit Connect again, and…

Uh oh, we broke something?

Why can’t we reconnect? Let’s look in the system error log on VC-4 to get a better idea:

$ sudo journalctl -u virtualcontrol.service -f
...
CustomAppManager_1[41959]: 4/15/2021 4:01:54 PM|Error|WebSocketBehavior.Start|This session has already been started.
...

Huh, I guess our session never ended from the first connection? A Google search returns a GitHub issue that suggests I’m using the library incorrectly. Let’s make one small change to how we create our server and see what happens.

Back to the Back-end

We need to update the WebLogger constructor:

public WebLogger()
{
    try
    {
        _server = new WebSocketServer(54321);

        _server.AddWebSocketService<WebLoggerService>("/", () =>
        {
            _logger = new WebLoggerService();
            return _logger;
        });
    }
    catch (Exception e)
    {
        ErrorLog.Error("Exception in WebLogger constructor: {0}", e.Message);
    }
}

This works! We can reconnect again! But creating a new WebLoggerService every time a client connects means our backlog gets wiped out each time. Let’s try moving the backlog back to our WebLogger class and pass it in as a parameter to our WebLoggerService:

class WebLogger
{
    private WebSocketServer _server;
    private WebLoggerService _logger;
    private List<string> _backlog;

    public WebLogger()
    {
        try
        {
            _backlog = new List<string>();
            _server = new WebSocketServer(54321);

            _server.AddWebSocketService<WebLoggerService>("/", () =>
            {
                _logger = new WebLoggerService(_backlog);
                return _logger;
            });
        }
        catch (Exception e)
        {
            ErrorLog.Error("Exception in WebLogger constructor: {0}", e.Message);
        }
    }

And there’s a good chance our program tries to log something before any client connects, which means _logger might still be null. We can check for this in the WriteLine method to avoid throwing an exception:

public void WriteLine(string msg, params object[] args)
{
    try
    {
        if (_logger == null)
            _backlog.Add(String.Format(msg, args));
        else
            _logger.WriteLine(msg, args);
    }
    catch (Exception e)
    {
        ErrorLog.Error("Exception in WriteLine: {0}", e.Message);
    }
}

Now we need to update our WebLoggerService constructor to match:

public WebLoggerService(List<string> backlog)
{
    _backlog = backlog;
}

We can also get rid of the _connected state and just track the websocket status when we need to:

protected override void OnOpen()
{
    SendSerial(1, "-- CONNECTED --");

    if (_backlog.Count > 0)
    {
        foreach (var msg in _backlog)
            SendSerial(1, msg);
    }

    _backlog.Clear();
}

protected override void OnClose(CloseEventArgs e)
{
}

public void WriteLine(string msg, params object[] args)
{
    var text = String.Format(msg, args);

    if (this.State == WebSocketState.Open)
        SendSerial(1, text);
    else
        _backlog.Add(text);
}

Build it and test it. Seems to work the way we want, right? There’s one last thing I’d like to fix before we call it good.

Shutting Down Gracefully

There’s an unhandled exception I’ve noticed in the error logs when our program stops:

TLDM[2289]: **Program 1 Stopped**
CustomAppManager_1[43076]: 4/15/2021 4:50:51 PM|Fatal|WebSocketServer.receiveRequest|System.Threading.ThreadAbortException: Thread was being aborted.
CustomAppManager_1[43076]: at (wrapper managed-to-native) System.Net.Sockets.Socket.Accept_internal(intptr,int&,bool)
CustomAppManager_1[43076]: at System.Net.Sockets.Socket.Accept_internal (System.Net.Sockets.SafeSocketHandle safeHandle, System.Int32& error, System.Boolean blocking) [0x0000c] in <8ca332cfeece4059ac195f3048bcbe22>:0
CustomAppManager_1[43076]: at System.Net.Sockets.Socket.Accept () [0x00008] in <8ca332cfeece4059ac195f3048bcbe22>:0
CustomAppManager_1[43076]: at System.Net.Sockets.TcpListener.AcceptTcpClient () [0x0001e] in <8ca332cfeece4059ac195f3048bcbe22>:0
CustomAppManager_1[43076]: at WebSocketSharp.Server.WebSocketServer.receiveRequest () [0x00012] in <1fc1e12f21eb4681aa69c484f58d0121>:0

Since we can trap when the program is stopping, we should probably shutdown the server gracefully. We’ll need to add this code to ControlSystem.cs:

public ControlSystem()
    : base()
{
    try
    {
        Thread.MaxNumberOfUserThreads = 20;

        CrestronEnvironment.ProgramStatusEventHandler += ProgramEventHandler;

        _log = new WebLogger();
    }
    catch (Exception e)
    {
        ErrorLog.Error("Error in Constructor: {0}", e.Message);
    }
}

And add the new ProgramEventHandler method below:

private void ProgramEventHandler(eProgramStatusEventType eventType)
{
    if (eventType == eProgramStatusEventType.Stopping)
    {
        if (_log != null)
            _log.Stop();

        if (_looper != null)
            _looper.Abort();
    }
}

Now we can clean things up a little before our program shuts down. How nice!

If you’d like to grab an updated version of this code, it’s available on GitHub.

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