In the previous post, we created a simple Huddle Room system… but the logic is tightly-coupled, it’s rigid, and I wouldn’t call it a framework. Let’s refactor it!
There are 3 areas that I think will be easy to immediately refactor:
- Display control
- Switcher control
- Room vacancy timeout
Display control
Let’s start off by creating a new class named Display
:

Enter this into Display.cs:
using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
namespace HuddleRoom
{
public class Display
{
private ComPort _port;
public Display(ComPort port, ComPort.ComPortSpec comspec)
{
_port = port;
_port.SetComPortSpec(comspec);
_port.SerialDataReceived += onDataReceived;
}
private void onDataReceived(ComPort port, ComPortSerialDataEventArgs args)
{
}
public void PowerOn()
{
}
public void PowerOff()
{
}
}
}
In ControlSystem.cs, at the top of our ControlSystem
class, add a new member for our display:
private Am300 _dmRx;
private DmTx201C _dmTx;
private GlsOdtCCn _occSensor;
private CTimer _vacancyTimer;
private Display _display;
Now in the InitializeSystem
method, let’s use the _display
member to handle our display logic:
_dmRx = new Am300(0x15, this);
_display = new Display(_dmRx.ComPorts[1], displayComSpec);
if (_dmRx.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
ErrorLog.Error("Unable to register {0} on IP ID {1}!", _dmRx.Name, _dmRx.ID);
Since our event handler moved into the Display.cs file, we can delete it from our ControlSystem
class. Go ahead and remove the OnDisplayDataReceived
method starting on line 90. Let’s also modify the TurnSystemOn
and TurnSystemOff
methods:
public void TurnSystemOn()
{
_display.PowerOn();
}
public void TurnSystemOff()
{
_display.PowerOff();
}
Build the project and make sure everything works the same as before. Now let’s tackle the switcher.
Switcher control
A simple video switcher is defined by a number of inputs and outputs. Using an AirMedia as a switcher works a little differently, but we can at least lay the groundwork for supporting other AV switchers in the future. Let’s create a new class named AudioVideoSwitcher
:

Add this code to AudioVideoSwitcher.cs:
using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro.DM.AirMedia;
namespace HuddleRoom
{
public class AudioVideoSwitcher
{
public AudioVideoSwitcher()
{
}
}
public enum AirMediaInputs
{
None = 0,
AirMedia = 1,
HDMI = 2,
DM = 3,
AirBoard = 4
}
public class AirMediaSwitcher : AudioVideoSwitcher
{
private AmX00 _airmedia;
public AirMediaSwitcher(AmX00 device) : base()
{
_airmedia = device;
}
public void Switch(AirMediaInputs input)
{
try
{
AmX00DisplayControl.eAirMediaX00VideoSource source = AmX00DisplayControl.eAirMediaX00VideoSource.NA;
switch (input)
{
case AirMediaInputs.None:
source = AmX00DisplayControl.eAirMediaX00VideoSource.PinPointUxLandingPage;
break;
case AirMediaInputs.AirMedia:
source = AmX00DisplayControl.eAirMediaX00VideoSource.AirMedia;
break;
case AirMediaInputs.HDMI:
source = AmX00DisplayControl.eAirMediaX00VideoSource.HDMI;
break;
case AirMediaInputs.DM:
source = AmX00DisplayControl.eAirMediaX00VideoSource.DM;
break;
case AirMediaInputs.AirBoard:
source = AmX00DisplayControl.eAirMediaX00VideoSource.AirBoard;
break;
default:
throw new InvalidOperationException("unrecognized AirMedia input value: " + input);
}
_airmedia.DisplayControl.VideoOut = source;
}
catch (Exception e)
{
ErrorLog.Error("Exception in AirMediaSwitcher::Switch: {0}", e.Message);
}
}
}
}
It’s important to surround the _airmedia.DisplayControl.VideoOut
in a try
block because not every AirMedia supports the same inputs. Now go back to ControlSystem.cs and use our AudioVideoSwitcher
class. Add a new member to our ControlSystem
class:
public class ControlSystem : CrestronControlSystem
{
private Am300 _dmRx;
private DmTx201C _dmTx;
private GlsOdtCCn _occSensor;
private CTimer _vacancyTimer;
private Display _display;
private AudioVideoSwitcher _switcher;
public ControlSystem()
: base()
{
We need to instantiate our switcher inside InitializeSystem
:
if (this.SupportsEthernet)
{
_dmRx = new Am300(0x15, this);
_display = new Display(_dmRx.ComPorts[1], displayComSpec);
_switcher = new AirMediaSwitcher(_dmRx);
if (_dmRx.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
ErrorLog.Error("Unable to register {0} on IP ID {1}!", _dmRx.Name, _dmRx.ID);
_dmTx = new DmTx201C(0x14, this);
_dmTx.HdmiInput.InputStreamChange += OnLaptopHDMI;
_dmTx.VgaInput.InputStreamChange += OnLaptopVGA;
if (_dmTx.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
ErrorLog.Error("Unable to register {0} on IP ID {1}!", _dmTx.Name, _dmTx.ID);
}
Now we need to update OnLaptopHDMI
and OnLaptopVGA
to use our switcher object:
private void OnLaptopHDMI(EndpointInputStream inputStream, EndpointInputStreamEventArgs args)
{
var hdmiStream = inputStream as EndpointHdmiInput;
var switcher = _switcher as AirMediaSwitcher;
switch (args.EventId)
{
case EndpointInputStreamEventIds.SyncDetectedFeedbackEventId:
if (hdmiStream.SyncDetectedFeedback.BoolValue)
switcher.Switch(AirMediaInputs.DM);
else
switcher.Switch(AirMediaInputs.AirMedia);
break;
}
}
private void OnLaptopVGA(EndpointInputStream inputStream, EndpointInputStreamEventArgs args)
{
var vgaStream = inputStream as EndpointVgaInput;
var switcher = _switcher as AirMediaSwitcher;
switch (args.EventId)
{
case EndpointInputStreamEventIds.SyncDetectedFeedbackEventId:
if (vgaStream.SyncDetectedFeedback.BoolValue)
switcher.Switch(AirMediaInputs.DM);
else
switcher.Switch(AirMediaInputs.AirMedia);
break;
}
}
We can also remove the ShowAirMedia
and ShowLaptop
methods from ControlSystem
. Build the project and test to make sure everything works the same as before.
Room vacancy
Same routine as before, let’s create a new class named RoomOccupancy
:
using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro.GeneralIO;
namespace HuddleRoom
{
public class RoomOccupancy
{
public delegate void OccupancyChangeHandler();
private GlsOccupancySensorBase _sensor;
private CTimer _vacancyTimer;
private long _timeout;
public OccupancyChangeHandler RoomOccupied;
public OccupancyChangeHandler RoomVacant;
public RoomOccupancy(GlsOccupancySensorBase sensor, int seconds)
{
_sensor = sensor;
_sensor.GlsOccupancySensorChange += OnOccupancySensorChange;
_vacancyTimer = new CTimer(OnRoomVacantTimeout, Timeout.Infinite);
_timeout = seconds * 1000;
}
private void OnOccupancySensorChange(GlsOccupancySensorBase device, GlsOccupancySensorChangeEventArgs args)
{
switch (args.EventId)
{
case GlsOccupancySensorBase.RoomOccupiedFeedbackEventId:
_vacancyTimer.Stop();
if (RoomOccupied != null)
RoomOccupied();
break;
case GlsOccupancySensorBase.RoomVacantFeedbackEventId:
_vacancyTimer.Reset(_timeout);
break;
}
}
private void OnRoomVacantTimeout(Object o)
{
if (RoomVacant != null)
RoomVacant();
}
}
}
And use it in our ControlSystem.cs file. We can also get rid of our _vacancyTimer
since that’s moved into our RoomOccupancy
class:
private Am300 _dmRx;
private DmTx201C _dmTx;
private GlsOdtCCn _occSensor;
private Display _display;
private AudioVideoSwitcher _switcher;
private RoomOccupancy _occupancy;
if (this.SupportsCresnet)
{
_occSensor = new GlsOdtCCn(0x97, this);
_occupancy = new RoomOccupancy(_occSensor, 15 * 60 * 60); // 15 minutes
_occupancy.RoomOccupied = TurnSystemOn;
_occupancy.RoomVacant = TurnSystemOff;
if (_occSensor.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
ErrorLog.Error("Unable to register {0} on Cresnet ID {1}!", _occSensor.Name, _occSensor.ID);
}
We can also delete the OnOccupancySensorChange
and OnRoomVacantTimeout
methods from our ControlSystem
class since that’s now handled within RoomOccupancy
.
Build the project and make sure there are no errors. Load it and make sure no exceptions occur. We can’t really test this system yet (unless you have all the equipment), but the next part will walk through adding console commands to test our logic.
Final thoughts
We moved a lot of code out of our ControlSystem
class into supporting classes. This will help us when we try to glue new pieces together in the future. For comparison, ControlSystem.cs from Part 1 was 166 lines long. Now it’s only 123:
using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.CrestronThread;
using Crestron.SimplSharpPro.DM.AirMedia;
using Crestron.SimplSharpPro.DM.Endpoints;
using Crestron.SimplSharpPro.DM.Endpoints.Transmitters;
using Crestron.SimplSharpPro.GeneralIO;
namespace HuddleRoom
{
public class ControlSystem : CrestronControlSystem
{
private Am300 _dmRx;
private DmTx201C _dmTx;
private GlsOdtCCn _occSensor;
private Display _display;
private AudioVideoSwitcher _switcher;
private RoomOccupancy _occupancy;
public ControlSystem()
: base()
{
try
{
Thread.MaxNumberOfUserThreads = 40;
}
catch (Exception e)
{
ErrorLog.Error("Error in ControlSystem constructor: {0}", e.Message);
}
}
public override void InitializeSystem()
{
ComPort.ComPortSpec displayComSpec = new ComPort.ComPortSpec {
BaudRate = ComPort.eComBaudRates.ComspecBaudRate9600,
DataBits = ComPort.eComDataBits.ComspecDataBits8,
Parity = ComPort.eComParityType.ComspecParityNone,
StopBits = ComPort.eComStopBits.ComspecStopBits1,
SoftwareHandshake = ComPort.eComSoftwareHandshakeType.ComspecSoftwareHandshakeNone,
HardwareHandShake = ComPort.eComHardwareHandshakeType.ComspecHardwareHandshakeNone
};
try
{
if (this.SupportsEthernet)
{
_dmRx = new Am300(0x15, this);
_display = new Display(_dmRx.ComPorts[1], displayComSpec);
_switcher = new AirMediaSwitcher(_dmRx);
if (_dmRx.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
ErrorLog.Error("Unable to register {0} on IP ID {1}!", _dmRx.Name, _dmRx.ID);
_dmTx = new DmTx201C(0x14, this);
_dmTx.HdmiInput.InputStreamChange += OnLaptopHDMI;
_dmTx.VgaInput.InputStreamChange += OnLaptopVGA;
if (_dmTx.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
ErrorLog.Error("Unable to register {0} on IP ID {1}!", _dmTx.Name, _dmTx.ID);
}
if (this.SupportsCresnet)
{
_occSensor = new GlsOdtCCn(0x97, this);
_occupancy = new RoomOccupancy(_occSensor, 15 * 60 * 60); // 15 minutes
_occupancy.RoomOccupied = TurnSystemOn;
_occupancy.RoomVacant = TurnSystemOff;
if (_occSensor.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
ErrorLog.Error("Unable to register {0} on Cresnet ID {1}!", _occSensor.Name, _occSensor.ID);
}
}
catch (Exception e)
{
ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
}
}
private void OnLaptopHDMI(EndpointInputStream inputStream, EndpointInputStreamEventArgs args)
{
var hdmiStream = inputStream as EndpointHdmiInput;
var switcher = _switcher as AirMediaSwitcher;
switch (args.EventId)
{
case EndpointInputStreamEventIds.SyncDetectedFeedbackEventId:
if (hdmiStream.SyncDetectedFeedback.BoolValue)
switcher.Switch(AirMediaInputs.DM);
else
switcher.Switch(AirMediaInputs.AirMedia);
break;
}
}
private void OnLaptopVGA(EndpointInputStream inputStream, EndpointInputStreamEventArgs args)
{
var vgaStream = inputStream as EndpointVgaInput;
var switcher = _switcher as AirMediaSwitcher;
switch (args.EventId)
{
case EndpointInputStreamEventIds.SyncDetectedFeedbackEventId:
if (vgaStream.SyncDetectedFeedback.BoolValue)
switcher.Switch(AirMediaInputs.DM);
else
switcher.Switch(AirMediaInputs.AirMedia);
break;
}
}
public void TurnSystemOn()
{
_display.PowerOn();
}
public void TurnSystemOff()
{
_display.PowerOff();
}
}
}
As always, code is available on GitHub here.