This post picks up right where the last one left off. I got VC-4 running on an RHEL instance, but now we’re back to some of the challenges that VC-4 brings… like printing to a console window.
Better Logging
Control Concepts has an excellent Websocket Server demo available on the Application Market. If you open the program, you’ll see a Websocket Server Lite module that gives you access to digitals, analogs, and serials (almost like a touchpanel, you might say):

There’s also a sample HTML5 page that acts as the Websocket Client from your web browser:

I’d like to use this module to setup a logging console we can connect to from a web page and catch messages as they spit out from our program. The module, however, is provided as a SIMPL User Macro, and we can’t run SIMPL Windows programs on VC-4. But that doesn’t stop us from being able to use the .NET assembly in a SIMPL# Pro program. Unzip the WebsocketServer.clz file and grab the WebsocketServer.dll file.
Create a new .NET Class Library (.NET Framework 4.7) project in Visual Studio 2019. Use NuGet to add the Crestron dependencies to your new project. Drop the WebsocketServer.dll file into the root directory of your project and add it as a reference. If any of this seems unfamiliar, check out my last post. You can delete the Class1.cs file from your project, we won’t need it. Update ControlSystem.cs to contain the following:
using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.CrestronThread;
using WebsocketServer;
namespace WebsocketLogger
{
public class ControlSystem : CrestronControlSystem
{
private WebsocketSrvr _server;
public ControlSystem()
: base()
{
try
{
Thread.MaxNumberOfUserThreads = 20;
_server = new WebsocketSrvr();
}
catch (Exception e)
{
ErrorLog.Error("Error in Constructor: {0}", e.Message);
}
}
public override void InitializeSystem()
{
try
{
_server.Initialize(8081);
_server.OnClientConnectedChange += OnClientConnected;
_server.StartServer();
}
catch (Exception e)
{
ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
}
}
private void OnClientConnected(ushort state)
{
if (state == 0)
{
// Disconnected
}
else
{
// Connected
_server.SetIndirectTextSignal(1, "-- CONNECTED --");
}
}
}
}
I’m going to load this program over the demo program on my CP4 and see if the provided HTML5 page connects OK:

Websockets on VC-4
Let’s try loading this to our VC-4 instance and see what happens:


Once this program has started, try connecting the example HTML5 page to it again. Hmm… doesn’t work this time:

Checking the error log on the VC-4 server, I don’t see anything that says the program failed to start. Let’s try changing the port number to something higher, like 54321. You’ll need to recompile the program, load it, update the port number in the HTML5 example and see if that fixes anything. Still no luck? Maybe something is blocking the websocket connection? Running netstat -ln on the VC-4 server, I can see our port listening for connections:
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:54321 0.0.0.0:* LISTEN
But if I port scan from outside the server, I see a firewall is filtering it:

On the server, we can check the firewall status:
$ sudo firewall-cmd --state
running
OK but which ports are allowed?
$ sudo firewall-cmd --list-ports
161/udp 41794/tcp 41794/udp 41796/tcp 80/tcp 443/tcp
All we need to do is add our logger port:
$ sudo firewall-cmd --add-port=54321/tcp
success
Try connecting from the HTML5 web page again and… success! The last thing to do is make sure this firewall rule persists across reboots:
$ sudo firewall-cmd --runtime-to-permanent
success
Create a WebLogger class
Back in VS 2019, let’s add a new class to our project named WebLogger. We’ll take a lot of the logic from ControlSystem and move it into this class:
using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using WebsocketServer;
namespace WebsocketLogger
{
class WebLogger
{
private WebsocketSrvr _server;
private bool _clientConnected;
private List<string> _backlog;
public WebLogger()
{
try
{
_server = new WebsocketSrvr();
_server.Initialize(54321);
_server.OnClientConnectedChange += OnClientConnected;
_backlog = new List<string>();
_clientConnected = false;
}
catch (Exception e)
{
ErrorLog.Error("Exception in WebLogger constructor: {0}", e.Message);
}
}
public void Start()
{
_server.StartServer();
}
public void Stop()
{
_server.StopServer();
}
public void WriteLine(string msg, params object[] args)
{
var text = String.Format(msg, args) + "\n";
if (_clientConnected)
{
_server.SetIndirectTextSignal(1, text);
}
else
{
_backlog.Add(text);
}
}
private void OnClientConnected(ushort state)
{
if (state == 0)
{
// Disconnected
_clientConnected = false;
}
else
{
// Connected
_clientConnected = true;
_server.SetIndirectTextSignal(1, "\n-- CONNECTED --\n");
if (_backlog.Count > 0)
{
foreach (var msg in _backlog)
{
_server.SetIndirectTextSignal(1, msg);
}
}
_backlog.Clear();
}
}
}
}
We’ll use a List to keep track of any log messages that might have been missed by our console. Once we connect, we’ll blast out any missed messages. Now update ControlSystem to use our new class:
using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.CrestronThread;
namespace WebsocketLogger
{
public class ControlSystem : CrestronControlSystem
{
private WebLogger _log;
private Thread _looper;
public ControlSystem()
: base()
{
try
{
Thread.MaxNumberOfUserThreads = 20;
_log = new WebLogger();
}
catch (Exception e)
{
ErrorLog.Error("Error in Constructor: {0}", e.Message);
}
}
public override void InitializeSystem()
{
try
{
_log.Start();
_log.WriteLine("System is ready!");
_looper = new Thread(Loop, null);
}
catch (Exception e)
{
ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
}
}
private object Loop(object o)
{
while (true)
{
_log.WriteLine("Sleeping for 2 seconds...");
Thread.Sleep(2000);
}
}
}
}
This program just writes to the log every 2 seconds. If you wait a while before connecting, you should see a blast of messages. Bulid and load this to the VC-4 server. Replace the old program we’d loaded previously. Now lets create a console!
HTML Console
I’m going to hack apart the sample HTML5 page included with the Websocket Server demo. Create a new file named console.html with this:
<!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 href="style.css" rel="stylesheet">
<title>HTML5 Console</title>
<script src="console.js"></script>
</head>
<body>
<div id="top">
<h1>Web Console</h1>
<input type="text" id="host" placeholder="Host:Port" />
<button id="connect">Connect</button>
<button id="clear">Clear</button>
</div>
<pre id="console"></pre>
</body>
</html>
Now create style.css:
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
}
input[type=text] {
width: 20rem;
padding: 10px;
}
button {
background-color: steelblue;
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
}
#top {
background-color: lightgray;
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 1rem;
transition: 2s linear;
z-index: 1;
}
#top h1 {
margin: 0 0 1rem 0;
}
#console {
position: absolute;
top: 8rem;
}
And lastly, create console.js:
var websocket = null;
function getBoundString(msg, startChar, stopChar)
{
let response = "";
if (msg != null && msg.length > 0)
{
let start = msg.indexOf(startChar);
if (start >= 0)
{
start += startChar.length;
let end = msg.indexOf(stopChar, start);
if (start < end)
{
response = msg.substring(start, end);
}
}
}
return response;
}
function getBoundString_EndLastIndex(msg, startChar, stopChar)
{
let response = "";
if (msg != null && msg.length > 0)
{
let start = msg.indexOf(startChar);
if (start >= 0)
{
start += startChar.length;
let end = msg.lastIndexOf(stopChar);
if (start < end)
{
response = msg.substring(start, end);
}
}
}
return response;
}
function onMessage(event)
{
if (event != null)
{
const msg = event.data;
//ON[CHANNEL]
if (msg.indexOf("ON[") == 0)
{
const channel = parseInt(getBoundString(msg, "ON[", "]"), 10);
/*
if (isNaN(channel) == false)
{
var button = document.getElementById("fb" + channel);
if (button != null)
button.style.background = "green";
}
*/
}
//OFF[CHANNEL]
else if (msg.indexOf("OFF[") == 0)
{
const channel = parseInt(getBoundString(msg, "OFF[", "]"), 10);
/*
if (isNaN(channel) == false)
{
var button = document.getElementById("fb" + channel);
if (button != null)
button.style.background = "";
}
*/
}
// LEVEL[LEVEL,VALUE]
else if (msg.indexOf("LEVEL[") == 0)
{
const level = parseInt(getBoundString(msg, "LEVEL[", ","), 10);
const value = parseInt(getBoundString(msg, ",", "]"), 10);
/*
// set slider level
var slider = document.getElementById("sliderInput" + level);
if (slider != null)
slider.value = value;
// set feedback text
var text = document.getElementById("analogValue" + level);
if (text != null)
text.innerHTML = "" + value;
*/
}
// 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;
}
}
}
function doSend(message)
{
if (websocket !== null) {
websocket.send(message);
}
}
function socketclose()
{
if (websocket !== null) {
websocket.close();
}
}
function doPush(channel)
{
doSend("PUSH[" + channel + "]");
}
function doRelease(channel)
{
doSend("RELEASE[" + channel + "]");
}
function sendLevel(sig)
{
/*
var inputRange = document.getElementById("sliderInput" + sig);
if (inputRange != null)
doSend("LEVEL[" + sig + "," + inputRange.value + "]");
*/
}
function sendString(sig)
{
/*
var inputText = document.getElementById("stringInput" + sig);
if (inputText != null)
doSend("STRING[" + sig + "," + inputText.value + "]");
*/
}
function startWebsocket()
{
const hostAddress = document.querySelector('#host');
const ip = hostAddress.value;
if (ip != '')
{
let wsUri = "ws://";
if (ip.includes(':')) {
wsUri += ip;
}
else {
wsUri += ip + ":8081/";
}
websocket = new WebSocket(wsUri);
websocket.onopen = function (event) {
const topArea = document.querySelector('#top');
const connectBtn = document.querySelector('#connect');
topArea.style.backgroundColor = 'lightgreen';
connectBtn.innerHTML = 'Disconnect';
connectBtn.style.backgroundColor = 'red';
connectBtn.disabled = false;
};
websocket.onclose = function (event) {
const topArea = document.querySelector('#top');
const hostAddress = document.querySelector('#host');
const connectBtn = document.querySelector('#connect');
topArea.style.backgroundColor = 'lightgray';
connectBtn.innerHTML = 'Connect';
connectBtn.style.backgroundColor = 'steelblue';
hostAddress.disabled = false;
connectBtn.disabled = false;
};
websocket.onmessage = function(event) {
onMessage(event)
};
websocket.onerror = function(event) {
// todo
};
}
}
window.addEventListener("DOMContentLoaded", function () {
const hostAddress = document.querySelector('#host');
const connectBtn = document.querySelector('#connect');
const clearBtn = document.querySelector('#clear');
connectBtn.addEventListener('click', function () {
if (hostAddress.value !== '') {
if (connectBtn.innerHTML == 'Connect') {
hostAddress.disabled = true;
connectBtn.disabled = true;
startWebsocket();
}
else {
socketclose();
}
}
});
clearBtn.addEventListener('click', function () {
const consoleElement = document.querySelector('#console');
consoleElement.innerHTML = '';
});
});
Most of these functions came from the Control Concepts demo, I just adapted them to work with the HTML we created in console.html
.
Now you can use a web browser to open console.html and watch it print messages from our program!

Accessing the Console
We could open this file locally from our laptop any time we want to fire up the console, but wouldn’t it be handy to keep it with the program? If you zip the 3 files together, you can load it as a “configuration page” to VC-4:

When your program restarts, you can browse to http://address/VirtualControl/Rooms/program-tag/Html/console.html to access your console.
I’m sure this console can be improved upon, so please grab it from GitHub and see if you can make it work for you. Thanks for reading!
I was able to get it work as a local page on my PC. However loaded it tot he VC4 server and browsed to page and it will not load. Were you able to test this on a VC4 and verify it works?
LikeLike
Hi Chris, I just re-read this post and it looks like I had to open firewall ports on the VC4 server to allow the connection. Are you sure your websocket port isn’t getting filtered? I’d like to test this again to see if it still works on AlmaLinux 8.6 since that’s what my VC4 server is running now.
LikeLike
Yep, confirmed it works on AlmaLinux 8.6 after I’ve opened port 8081. Also, since the WebSocket isn’t secured, you have to view console.html using HTTP. Not ideal, I’ll say that.
LikeLike
Kiel, Opened up port 8081 and when I put the IP and Port into the console page it just hangs when I click connect. Did you have to do anything else special to make it work.
LikeLike