Huddle Room: Part 1

This series of posts is going to put together some of what we learned in the Simple Sharp Primer and VC-4 posts. For an idea, here is what we’re going to build in Part 1:

A simple Huddle Room space

Let’s take a quick inventory of what’s in this system:

  • RMC3 is the brain deciding all the business logic
  • DM-TX-201-C is the transmitter for our laptop connection at the table
  • AM-300 is our receiver/wireless presentation at the display
  • GLS-ODT-C-CN is our occupancy sensor
  • PW-2407RU is power for our sensor (since RMC3 doesn’t provide power over Cresnet)

How should our system function? Here’s a typical user story:

  • User walks into room.
  • Occupancy sensor triggers and display powers on, defaulting to AM-300 help screen
  • If user connects a laptop, that is displayed on screen instead
  • When the user disconnects laptop, image goes back to AM-300 help screen
  • If the room is vacant for 15 minutes, the display powers off

I think we can bang this out in one blog post, don’t you? It’s not so different from what we’ve already done.

Create a new project

Create a new project in VS2008 (since we’re working with a 3-series processor). If you need a refresher on this part, read through my Primer series. Here’s a screenshot of my New Project dialog:

Getting the hang of it!

Open up ControlSystem.cs and replace it with the bare-minimum program:

using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.CrestronThread;

namespace HuddleRoom
{
    public class ControlSystem : CrestronControlSystem
    {
        public ControlSystem()
            : base()
        {
            try
            {
                Thread.MaxNumberOfUserThreads = 40;
            }
            catch (Exception e)
            {
                ErrorLog.Error("Error in ControlSystem constructor: {0}", e.Message);
            }
        }

        public override void InitializeSystem()
        {
            try
            {
                CrestronConsole.PrintLine("MaxNumberOfUserThreads = {0}", Thread.MaxNumberOfUserThreads);
            }
            catch (Exception e)
            {
                ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
            }
        }
    }
}

Build this and load it to your processor (it doesn’t have to be an RMC3). Once the program has started, you’ll see it report back how many user threads are allowed:

Surprisngly, MaxNumberOfUserThreads defaults to zero if you don’t set it.

Add devices

Let’s define all of our devices as members of our ControlSystem class. Don’t forget to add the necessary references to our project so we can pull in the correct assemblies:

At the top of the ControlSystem.cs file, add some using directives:

using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.CrestronThread;
using Crestron.SimplSharpPro.DM.AirMedia;
using Crestron.SimplSharpPro.DM.Endpoints.Transmitters;
using Crestron.SimplSharpPro.GeneralIO;

Now we can add a couple members to our ControlSystem class. Add these at the top of the class declaration, just above the constructor:

public class ControlSystem : CrestronControlSystem
{
    private Am300 _dmRx;
    private DmTx201C _dmTx;
    private GlsOdtCCn _occSensor;

    public ControlSystem()
        : base()
    {

We also want to easily track which equipment is actually available and registered in our system, so let’s also add these properties just below the devices we just added:

public class ControlSystem : CrestronControlSystem
{
    private Am300 _dmRx;
    private DmTx201C _dmTx;
    private GlsOdtCCn _occSensor;

    public bool HasAirMedia { get; private set; }
    public bool HasLaptop { get; private set; }
    public bool HasOccSensor { get; private set; }

    public ControlSystem()
        : base()
    {

Now we can register our devices in InitializeSystem:

public override void InitializeSystem()
{
    HasAirMedia = false;
    HasLaptop = false;
    HasOccSensor = false;

    try
    {
        if (this.SupportsEthernet)
        {
            _dmRx = new Am300(0x15, this);
            if (_dmRx.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
                ErrorLog.Error("Unable to register {0} on IP ID {1}!", _dmRx.Name, _dmRx.ID);

            _dmTx = new DmTx201C(0x14, this);
            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);
            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);
    }
}

Build and load this program, and you should see a normal startup, but now there’s an Update Request from the _occSensor object:

Nothing physically connected but program is trying to get status of GLS-ODT-C-CN

Hypothetically, if we were to load the same program into Slot 2 on the processor, our registrations will fail because they’re already being allocated to the program in Slot 1. If we did this and check errlog, we can see the registrations fail (it prints our error messages):

Don’t run two copies of our program on the same processor, it won’t work!

Occupancy sensor

Let’s start by adding an event handler for the occupancy sensor:

if (this.SupportsCresnet)
{
    _occSensor = new GlsOdtCCn(0x97, this);
    _occSensor.GlsOccupancySensorChange += OnOccupancySensorChange;
    if (_occSensor.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
        ErrorLog.Error("Unable to register {0} on Cresnet ID {1}!", _occSensor.Name, _occSensor.ID);
}

And define OnOccupancySensorChange after InitializeSystem:

private void OnOccupancySensorChange(GlsOccupancySensorBase device, GlsOccupancySensorChangeEventArgs args)
{
    switch (args.EventId)
    {
        case GlsOccupancySensorBase.RoomOccupiedFeedbackEventId:
            break;
        case GlsOccupancySensorBase.RoomVacantFeedbackEventId:
            break;
    }
}

Remember our user story: we want the system to turn off after 15 minutes of no activity. We can use a timer to track this. At the top of our class, let’s add another member:

public class ControlSystem : CrestronControlSystem
{
    private Am300 _dmRx;
    private DmTx201C _dmTx;
    private GlsOdtCCn _occSensor;
    private CTimer _vacancyTimer;

Now in InitializeSystem, we’ll create the timer but tell it not to start right away. I also removed the HasAirMedia, HasLaptop, and HasOccSensor properties for now since I don’t think I’m going to end up using them yet:

public override void InitializeSystem()
{
    try
    {
        _vacancyTimer = new CTimer(OnRoomVacantTimeout, Timeout.Infinite);

        if (this.SupportsEthernet)
        {
            _dmRx = new Am300(0x15, this);
            if (_dmRx.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
                ErrorLog.Error("Unable to register {0} on IP ID {1}!", _dmRx.Name, _dmRx.ID);

            _dmTx = new DmTx201C(0x14, this);
            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);
            _occSensor.GlsOccupancySensorChange += OnOccupancySensorChange;
            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);
    }
}

And let’s create the OnRoomVacantTimeout callback, TurnSystemOn, and TurnSystemOff methods:

private void OnRoomVacantTimeout(Object o)
{
    TurnSystemOff();
}

public void TurnSystemOn()
{
    // TODO
}

public void TurnSystemOff()
{
    // TODO
}

Let’s modify OnOccupancySensorChange to start/stop our timer based on the room occupancy status:

private void OnOccupancySensorChange(GlsOccupancySensorBase device, GlsOccupancySensorChangeEventArgs args)
{
    switch (args.EventId)
    {
        case GlsOccupancySensorBase.RoomOccupiedFeedbackEventId:
            _vacancyTimer.Stop();
            TurnSystemOn();
            break;
        case GlsOccupancySensorBase.RoomVacantFeedbackEventId:
            _vacancyTimer.Reset(15 * 60 * 60 * 1000); // 15 minutes (in ms)
            break;
    }
}

As of right now, TurnSystemOn and TurnSystemOff don’t do anything. We’ll fill this out as soon as we get control of our other devices.

AM-300

We’re going to use an AM-300 as a DM receiver and wireless presentation source. It’s going to handle a lot. I’m really hoping this doesn’t turn into an unwieldy amount of code. First let’s figure out how we control which input we’re looking at on-screen.

Let’s create ShowAirMedia and ShowLaptop methods to make this easier:

public void ShowAirMedia()
{
    try
    {
        _dmRx.DisplayControl.VideoOut = AmX00DisplayControl.eAirMediaX00VideoSource.AirMedia;
    }
    catch (Exception e)
    {
        ErrorLog.Error("Exception in ShowAirMedia: {0}", e.Message);
    }
}

public void ShowLaptop()
{
    try
    {
        _dmRx.DisplayControl.VideoOut = AmX00DisplayControl.eAirMediaX00VideoSource.DM;
    }
    catch (Exception e)
    {
        ErrorLog.Error("Exception in ShowLaptop: {0}", e.Message);
    }
}

Wrapping these in try / catch blocks is important because not all AirMedias feature the same input selections. If we try to do something the hardware doesn’t support, we’ll read about it in the Error Log later.

Since the AM-300 also has an RS-232 port we’re going to use for display control, let’s configure that in InitializeSystem:

_dmRx = new Am300(0x15, this);
_dmRx.ComPorts[1].SetComPortSpec(displayComSpec);
_dmRx.ComPorts[1].SerialDataReceived += OnDisplayDataReceived;
if (_dmRx.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
    ErrorLog.Error("Unable to register {0} on IP ID {1}!", _dmRx.Name, _dmRx.ID);

We can create displayComSpec at the top of InitializeSystem since we only need it to call SetComPortSpec:

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
    {

Next, create the OnDisplayDataReceived event handler. We’ll figure out what it does later:

private void OnDisplayDataReceived(ComPort port, ComPortSerialDataEventArgs args)
{
    // TODO
}

Let’s revisit TurnSystemOn and TurnSystemOff and make sure we send the appropriate power commands to the display (just assume a Sharp monitor for now):

public void TurnSystemOn()
{
    _dmRx.ComPorts[1].Send("POWR1   \r");
}

public void TurnSystemOff()
{
    _dmRx.ComPorts[1].Send("POWR0   \r");
}

DM-TX-201-C

Lastly, we need to handle when the user plugs their laptop into the DM transmitter at the table. We want to automatically switch the AirMedia to and from the DM input.

Add a namespace to the top of the program that I forgot to include earlier:

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;

Let’s update InitializeSystem to register some new event handlers:

_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);

And let’s create them below where our other handlers are defined:

private void OnLaptopHDMI(EndpointInputStream inputStream, EndpointInputStreamEventArgs args)
{
    var hdmiStream = inputStream as EndpointHdmiInput;

    switch (args.EventId)
    {
        case EndpointInputStreamEventIds.SyncDetectedFeedbackEventId:
            if (hdmiStream.SyncDetectedFeedback.BoolValue)
                ShowLaptop();
            else
                ShowAirMedia();

            break;
    }
}

private void OnLaptopVGA(EndpointInputStream inputStream, EndpointInputStreamEventArgs args)
{
    var vgaStream = inputStream as EndpointVgaInput;

    switch (args.EventId)
    {
        case EndpointInputStreamEventIds.SyncDetectedFeedbackEventId:
            if (vgaStream.SyncDetectedFeedback.BoolValue)
                ShowLaptop();
            else
                ShowAirMedia();

            break;
    }
}

When the user plugs or unplugs their laptop, we check the value of SyncDetectedFeedback on the appropriate input to see whether there is a signal or not. If there’s a signal, we switch to showing Laptop (on the AM-300), and if not, we switch back to Airmedia.

And done!

Actually, I think that covers everything in the user story at the start of this post. Not bad! It’s roughly 150 lines of tightly-coupled code! I think in the next post, we’ll explore decoupling the program a bit and try to factor out a few classes to help us see what’s going on a little easier.

So until next time, please check out GitHub for the code for this project.

2 thoughts on “Huddle Room: Part 1

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