comment 0

SIMPL# Pro Primer: Part 5

Our goal in this project is to program a simple huddle room system. We’re going to design something that looks like this:

Over time, we’ll flesh this program out into something with more features, but for this part, it’s going to remain pretty bare-bones.

This system is going to operate very simply:

  • The display automatically powers on or off depending if any source is currently active
  • Since we have one display, only one source can be viewed at any time
  • Laptop 1 has a higher priority over Laptop 2
  • If both laptops are connected, Laptop 1 will display until it is unplugged, then we’ll automatically switch to Laptop 2

Add a new project to your solution. Name it Part5.

Like Part 4, strip ControlSystem.cs down to its essentials and we’ll start adding some code. First, lets make sure we have switching capabilities and fail if we don’t. Update InitializeSystem like so:

public override void InitializeSystem()
{
   try
   {
      // Currently only runs on DMPS architecture
      if (this.SystemControl == null)
      {
         // Eventually we'll handle external switchers, too
         ErrorLog.Error("Sorry, this program only runs on DMPS3 processors!");
      }
      else
      {

      }
   }
   catch (Exception e)
   {
      ErrorLog.Error("Error in InitializeSystem: {0}", e.StackTrace);
   }
}

If you want, try loading this program to a non-DMPS processor to see that it does fail. You can check the error log (type errlog) to see our message:

Add a reference to Crestron.SimplSharpPro.DM.dll to our project. Update our using statements to include some DM namespaces:

using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.CrestronThread;
using Crestron.SimplSharpPro.Diagnostics;
using Crestron.SimplSharpPro.DeviceSupport;
using Crestron.SimplSharpPro.DM;
using Crestron.SimplSharpPro.DM.Cards;
using Crestron.SimplSharpPro.DM.Endpoints;
using Crestron.SimplSharpPro.DM.Endpoints.Receivers;
using Crestron.SimplSharpPro.DM.Endpoints.Transmitters;

Add a transmitter to our class:

public class ControlSystem : CrestronControlSystem
{
   private DmTx4k202C _tx1;

   public ControlSystem()
      : base()
   {
      // ...snip...
   }
}

And initialize it in the InitializeSystem method:

public override void InitializeSystem()
{
   try
   {
      // Currently only runs on DMPS architecture
      if (this.SystemControl == null)
      {
         // Eventually we'll handle external switchers, too
         ErrorLog.Error("Sorry, this program only runs on DMPS3 processors!");
      }
      else
      {
         // Assume DM transmitter is connected to DM input 6
         _tx1 = new DmTx200C2G(0x14, this.SwitcherInputs[6] as DMInput);
         _tx1.HdmiInput.InputStreamChange += new EndpointInputStreamChangeEventHandler(tx_InputStreamChange);
         _tx1.VgaInput.InputStreamChange += new EndpointInputStreamChangeEventHandler(tx_InputStreamChange);
      }
   }
   catch (Exception e)
   {
      ErrorLog.Error("Error in InitializeSystem: {0}", e.StackTrace);
   }
}

Knowing which constructor to use depends on how it connects to our system. Since we’re using a non-4K DMPS, we need to assign an IP ID in our program (0x14), and we need to tell the DMPS which input it is attached to. We do this by casting this.SwitcherInputs[6] as DMInput. We also don’t need to worry about calling Register() on the endpoint since the DMPS will take care of that for us. I wish the documentation was clearer about how this works.

We also register a few EndpointInputStreamChangeEventHandlers. When something changes about the input stream (source detected, format changed, etc.) we’ll be notified about it. We need to register a handler for HdmiInput and VgaInput. Name it tx_InputStreamChange and define it like so:

void tx_InputStreamChange(EndpointInputStream inputStream,
   EndpointInputStreamEventArgs args)
{
   if (args.EventId == EndpointInputStreamEventIds.SyncDetectedFeedbackEventId)
   {
      switch (inputStream.StreamType)
      {
         case eDmStreamType.Hdmi:
            var hdmiStream = inputStream as EndpointHdmiInput;
            if (hdmiStream.SyncDetectedFeedback.BoolValue)
            {
               CrestronConsole.PrintLine("HDMI detected on {0}",
                  hdmiStream);
            }
            else
            {
               CrestronConsole.PrintLine("HDMI not detected on {0}",
                  hdmiStream);
            }
            break;
         case eDmStreamType.Vga:
            var vgaStream = inputStream as EndpointVgaInput;
            if (vgaStream.SyncDetectedFeedback.BoolValue)
            {
               CrestronConsole.PrintLine("VGA detected on {0}",
                  vgaStream);
            }
            else
            {
               CrestronConsole.PrintLine("VGA not detected on {0}",
                  vgaStream);
            }
            break;
      }
   }
}

The first thing we check in our event handler is the EventId. This is a way of SIMPL# telling us what caused our method to fire. We only care about sync detection right now, so we check for that. All of the event IDs for InputStreamChange are defined in the EndpointInputStreamEventIds class.

Once we know it’s sync detect we’re responding to, we need to know what stream type we’re handling. Remember: we’re using the same event handler for both the HDMI and VGA inputs on our transmitter. If it’s HDMI, we cast inputStream as EndpointHdmiInput; VGA, we cast inputStream as EndpointVgaInput. For now, we just check each respective stream for SyncDetectedFeedback and print what we know to the console.

If you load this program and plug a laptop into one of the transmitters, you’ll see messages print to the console when sync is detected or lost.

Sync detection on our DM-TX transmitter

Since we’re going to have multiple sources, and we want to keep track of which ones are plugged in, it’s probably worth refactoring how we setup our transmitters. Go back to the top of our ControlSystem class and add:

public class ControlSystem : CrestronControlSystem
{
   public const uint NumberOfSources = 2;

   private DMInput[] _source;
   private bool[] _sourceAvailable;

   private DmTx200C2G _tx1;
   private DmTx200C2G _tx2;

   // ...snip...
}

In our constructor, lets create arrays to track available sources:

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

      _sourceAvailable = new bool[NumberOfSources];
      _source = new DMInput[NumberOfSources];
   }
   catch (Exception e)
   {
      ErrorLog.Error("Error in ControlSystem constructor: {0}", e.StackTrace);
   }
}

If we want to add additional sources in the future, we’ll need to remember to update NumberOfSources to match. Lets update InitializeSystem to track our inputs and we’ll register event handlers a little differently:

public override void InitializeSystem()
{
   try
   {
      // Currently only runs on DMPS architecture
      if (this.SystemControl == null)
      {
         // Eventually we'll handle external switchers, too
         ErrorLog.Error("Sorry, this program only runs on DMPS3 processors!");
      }
      else
      {
         // Assume DM transmitter is connected to DM input 6
         _source[0] = this.SwitcherInputs[6] as DMInput;
         _tx1 = new DmTx200C2G(0x14, _source[0]);
         _tx1.HdmiInput.InputStreamChange +=
            new EndpointInputStreamChangeEventHandler(
               (input, args) => tx_InputStreamChange(0, input, args));
         _tx1.VgaInput.InputStreamChange +=
            new EndpointInputStreamChangeEventHandler(
               (input, args) => tx_InputStreamChange(0, input, args));

         // Assume DM transmitter is connected to DM input 7
         _source[1] = this.SwitcherInputs[7] as DMInput;
         _tx2 = new DmTx200C2G(0x15, _source[1]);
         _tx2.HdmiInput.InputStreamChange +=
            new EndpointInputStreamChangeEventHandler(
               (input, args) => tx_InputStreamChange(1, input, args));
         _tx2.VgaInput.InputStreamChange +=
            new EndpointInputStreamChangeEventHandler(
               (input, args) => tx_InputStreamChange(1, input, args));
      }
   }
   catch (Exception e)
   {
      ErrorLog.Error("Error in InitializeSystem: {0}", e.StackTrace);
   }
}

Remember that arrays are based off of zero in SIMPL# unless it corresponds to a device. So _source[0] is the 1st source in our array but this.SwitcherInputs[6] is the 6th input on the DMPS. It’s confusing.

Notice that we’ve changed how we create our EndpointInputStreamChangeEventHandler. We wrap it in a lambda expression that passes in which source it is (either 0 or 1 in this case). That means we’ll need to update our tx_InputStreamChange method to match:

void tx_InputStreamChange(uint src,
   EndpointInputStream inputStream,
   EndpointInputStreamEventArgs args)
{
   if (args.EventId == EndpointInputStreamEventIds.SyncDetectedFeedbackEventId)
   {
      switch (inputStream.StreamType)
      {
         case eDmStreamType.Hdmi:
            var hdmiStream = inputStream as EndpointHdmiInput;
            _sourceAvailable[src] = hdmiStream.SyncDetectedFeedback.BoolValue;
            if (_sourceAvailable[src])
               CrestronConsole.PrintLine("HDMI detected on source {0}", src);
            else
               CrestronConsole.PrintLine("HDMI not detected on source {0}", src);
            break;
         case eDmStreamType.Vga:
            var vgaStream = inputStream as EndpointVgaInput;
            _sourceAvailable[src] = vgaStream.SyncDetectedFeedback.BoolValue;
            if (_sourceAvailable[src])
               CrestronConsole.PrintLine("VGA detected on source {0}", src);
            else
               CrestronConsole.PrintLine("VGA not detected on source {0}", src);
            break;
      }
      AutoRouteVideo();
   }
}

And lets add an AutoRouteVideo method to handle switching the video when sync detect changes:

void AutoRouteVideo()
{
   for (uint i = 0; i < NumberOfSources; i++)
   {
      if (_sourceAvailable[i])
      {
         // Make route
         (this.SwitcherOutputs[3] as DMOutput).VideoOut = _source[i];
         return;
      }
   }

   // Clear route
   (this.SwitcherOutputs[3] as DMOutput).VideoOut = null;
}

This method will loop over our _sourceAvailable array looking for the first one that has sync detected. This way, higher priority sources (like Laptop 1) appear first in the array and we’ll route those first. If we get all the way through the array, we clear the route by setting SwitcherOutput[3] to null. Similar to when we were dealing with the inputs for our endpoint constructors, we need to cast the outputs to DMOutput here.

We’re almost done! We just need to add display control through a DM-RMC-SCALER-C. Let’s add a couple members to the top of our class:

public class ControlSystem : CrestronControlSystem
{
   public const uint NumberOfSources = 2;

   // Samsung MDC protocol
   public const string PowerOn = "\xAA\x11\x01\x01\x01\x14";
   public const string PowerOff = "\xAA\x11\x01\x01\x00\x13";

   private DMInput[] _source;
   private bool[] _sourceAvailable;

   private DmTx200C2G _tx1;
   private DmTx200C2G _tx2;
   private DmRmcScalerC _rmc1;

   // ...snip...
}

We’ve created a couple strings that contain the commands we’re going to pass to our display. This will make it easier to fix later if we realize we messed up and we won’t have to change a bunch of places in our program. We also define _rmc1 which we’ll initialize in InitializeSystem:

public override void InitializeSystem()
{
   try
   {
      // Currently only runs on DMPS architecture
      if (this.SystemControl == null)
      {
         // Eventually we'll handle external switchers, too
         ErrorLog.Error("Sorry, this program only runs on DMPS3 processors!");
      }
      else
      {
         var control = this.SystemControl as Dmps3SystemControl;
         control.SystemPowerOn();

         // Samsung MDC = 9600 baud, 8 data bits, no parity, 1 stop bit
         var displayComSettings = new ComPort.ComPortSpec();
         displayComSettings.Protocol = ComPort.eComProtocolType.ComspecProtocolRS232;
         displayComSettings.BaudRate = ComPort.eComBaudRates.ComspecBaudRate9600;
         displayComSettings.DataBits = ComPort.eComDataBits.ComspecDataBits8;
         displayComSettings.Parity = ComPort.eComParityType.ComspecParityNone;
         displayComSettings.StopBits = ComPort.eComStopBits.ComspecStopBits1;
         displayComSettings.HardwareHandShake = ComPort.eComHardwareHandshakeType.ComspecHardwareHandshakeNone;
         displayComSettings.SoftwareHandshake = ComPort.eComSoftwareHandshakeType.ComspecSoftwareHandshakeNone;

         // Assume DM transmitter is connected to DM input 6
         _source[0] = this.SwitcherInputs[6] as DMInput;
         _tx1 = new DmTx200C2G(0x14, _source[0]);
         _tx1.HdmiInput.InputStreamChange +=
            new EndpointInputStreamChangeEventHandler(
               (input, args) => tx_InputStreamChange(0, input, args));
         _tx1.VgaInput.InputStreamChange +=
            new EndpointInputStreamChangeEventHandler(
               (input, args) => tx_InputStreamChange(0, input, args));

         // Assume DM transmitter is connected to DM input 7
         _source[1] = this.SwitcherInputs[7] as DMInput;
         _tx2 = new DmTx200C2G(0x15, _source[1]);
         _tx2.HdmiInput.InputStreamChange +=
            new EndpointInputStreamChangeEventHandler(
               (input, args) => tx_InputStreamChange(1, input, args));
         _tx2.VgaInput.InputStreamChange +=
            new EndpointInputStreamChangeEventHandler(
               (input, args) => tx_InputStreamChange(1, input, args));

         // Assume DM roombox is connected to DM output 3
         _rmc1 = new DmRmcScalerC(0x16, this.SwitcherOutputs[3] as DMOutput);
         _rmc1.ComPorts[1].SetComPortSpec(displayComSettings);
      }
   }
   catch (Exception e)
   {
      ErrorLog.Error("Error in InitializeSystem: {0}", e.StackTrace);
   }
}

The first part we add to InitializeSystem turns the system on. We access this by casting var control = this.SystemControl as Dmps3SystemControl. Then we can issue control.SystemPowerOn(). Since we have a DMPS3-300-C, it won’t route audio or video unless it’s powered on.

The next addition sets up our COM port settings for talking to a Samsung display using the MDC protocol. It’s a lot of typing, unfortunately.

The last addition instantiates a DmRmcScalerC on SwitcherOutputs[3]. Remember we have to cast as DMOutput to pass into the constructor. Then we apply the COM port spec using our displayComSettings structure. We don’t register any feedback handlers here because our example doesn’t need it.

And the last change we need to make is to AutoRouteVideo to make sure our display powers on or off if something is routed to it.

void AutoRouteVideo()
{
    for (uint i = 0; i < NumberOfSources; i++)
    {
        if (_sourceAvailable[i])
        {
            // Make route and power on display
            (this.SwitcherOutputs[3] as DMOutput).VideoOut = _source[i];
            _rmc1.ComPorts[1].Send(PowerOn);
            return;
        }
    }

    // Clear route and power off display
    (this.SwitcherOutputs[3] as DMOutput).VideoOut = null;
    _rmc1.ComPorts[1].Send(PowerOff);
}

And that’s it! Build this project (Shift+F6) and send it to your DMPS. When you plug in a laptop to the first transmitter, it will automatically route and turn on the display. If you plug in a 2nd laptop, you’ll see that unless you plug in a 1st laptop again. Unplug everything and the display shuts off.

If you want to download the whole project, it’s available on GitHub here: https://github.com/kielthecoder/SimplSharpPrimer

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