Huddle Room: Part 2

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:

Right-click on the project in Solution Explorer and select Add > Class.

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.

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 )

Facebook photo

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

Connecting to %s