P201 Projector Exam

You can grab this short example from my GitHub repository at https://github.com/kielthecoder/Full-Crestron-Examples. This was the class project / final exam for the Intermediate Crestron Programming class (that turned into P201).

Getting Started

Create a new .NET Framework 4.7 project, add the Crestron NuGet packages, then slim everything down to the bare essentials. I get everything down to a minimal ControlSystem.cs then make sure it builds and loads to a 4-series processor (RMC4).

I wanted to systematically add functions to the XPanel, since it was broken up into a few groups:

Managing the UI

I did want to spend a little extra time and build out a UI management class. Typically in these programs, you want all user interfaces to track each other. That means a button press can come in from any connected touchpanel, but all connected touchpanels should update to show the current state. In this program, there was just the one XPanel, but I think it will help us later on if we build out UI.cs now:

using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.DeviceSupport;


namespace P201_Projector_Exam
{
    internal class UI
    {
        private List<BasicTriList> _panels;

        public event EventHandler<uint> Press;
        public event EventHandler<uint> Release;

        public UI()
        {
            _panels = new List<BasicTriList>();

            Press += CommonPress;
            Release += CommonRelease;
        }

        public void Add(BasicTriList tp)
        {
            tp.SigChange += _ui_SigChange;

            if (tp.Register() == eDeviceRegistrationUnRegistrationResponse.Success)
            {
                _panels.Add(tp);
            }
            else
            {
                ErrorLog.Error("Unable to register touchpanel at ID {0:X2}!", tp.ID);
            }
        }

        public void SetFeedback(uint sig, bool value)
        {
            foreach (var tp in _panels)
            {
                tp.BooleanInput[sig].BoolValue = value;
            }
        }

        public void SetAnalog(uint sig, ushort value)
        {
            foreach (var tp in _panels)
            {
                tp.UShortInput[sig].UShortValue = value;
            }
        }

        public void SetSerial(uint sig, string value)
        {
            foreach (var tp in _panels)
            {
                tp.StringInput[sig].StringValue = value;
            }
        }

        private void _ui_SigChange(BasicTriList dev, SigEventArgs args)
        {
            switch (args.Sig.Type)
            {
                case eSigType.Bool:
                    if (args.Sig.BoolValue)
                    {
                        if (Press != null)
                            Press(dev, args.Sig.Number);
                    }
                    else
                    {
                        if (Release != null)
                            Release(dev, args.Sig.Number);
                    }

                    break;
            }
        }

        private void CommonPress(object dev, uint sig)
        {
            // TODO
        }

        private void CommonRelease(object dev, uint sig)
        {
            // TODO
        }
    }
}

This class will collect our touchpanel objects into one list so we can push the same feedback to all of them at the same time using SetFeedback, SetAnalog, and SetSerial (which is closer to SIMPL naming conventions). We also add a common SigChange event handler that will fire Press and Release events accordingly. I prefer using these two events because I’ve had bugs enter my programs when I forget that both a press and a release trigger a SigChange. I usually only care about presses anyway.

If our panel registers successfully, we add it to our list. Otherwise, we spit out an error message and our program does nothing.

We can update InitializeSystem to use our UI class to register our XPanel:

public override void InitializeSystem()
{
    try
    {
        // UI class manages all connected touchpanels
        _ui = new UI();

        // This is _NOT_ a mistake!  My original exam predates SmartGraphics, so an
        // XPanel executable was provided with the class materials.  It also ran on
        // a PRO2, but 2-series won't run S# programs.
        _ui.Add(new Xpanel(0x03, this));
    }
    catch (Exception e)
    {
        ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
    }
}

Relays

I figured the easiest place to start would be the screen control since it’s just relays. Looking at my SIMPL program, we had to pulse the relays for 20 seconds to get the screen to travel the full distance. There must have also been some notes about not pulsing both relays simultaneously. I’ve created a new ScreenControl.cs class to wrap our logic in:

using System;
using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;

namespace P201_Projector_Exam
{
    internal class ScreenControl
    {
        private UI _ui;
        private Relay _up;
        private Relay _down;

        private int _travel;
        private int _duration;
        private Thread _pulse;
        
        public ScreenControl(UI ui, Relay upRelay, Relay downRelay, int seconds)
        {
            _ui = ui;
            _ui.Press += _ui_Press;

            _up = upRelay;
            _down = downRelay;
            _travel = 0;                // 0 = stopped, 1 = up, 2 = down
            _duration = seconds * 1000; // Convert to milliseconds for Thread.Sleep
        }

        private void _ui_Press(object dev, uint sig)
        {
            switch (sig)
            {
                case 1:
                    ScreenUp();
                    break;
                case 2:
                    ScreenDown();
                    break;
            }
        }

        private void Pulse(object obj)
        {
            var relay = (Relay)obj;

            CrestronConsole.Print("Pulsing relay...");
            relay.Close();
            
            Thread.Sleep(_duration);
            
            relay.Open();
            CrestronConsole.PrintLine("done.");

            // Stop moving
            _travel = 0;
            _ui.SetFeedback(1, false);
            _ui.SetFeedback(2, false);
        }

        public void ScreenUp()
        {
            // Stop moving if we're traveling downward
            if (_travel == 2)
            {
                _pulse.Abort();
                _down.Open();
                _ui.SetFeedback(2, false);
                _travel = 0;
            }

            if (_travel == 0)
            {
                // Indicate we're traveling upward
                _travel = 1;
                _ui.SetFeedback(1, true);

                // Pulse relay for 20 seconds
                _pulse = new Thread(Pulse);
                _pulse.Start(_up);
            }
        }

        public void ScreenDown()
        {
            // Stop moving if we're traveling upward
            if (_travel == 1)
            {
                _pulse.Abort();
                _up.Open();
                _ui.SetFeedback(1, false);
                _travel = 0;
            }

            if (_travel == 0)
            {
                // Indicate we're traveling downward
                _travel = 2;
                _ui.SetFeedback(2, true);

                // Pulse relay for 20 seconds
                _pulse = new Thread(Pulse);
                _pulse.Start(_down);
            }
        }
    }
}

Our ScreenControl class hooks into UI.Press to handle digital joins 1 and 2 since those are the Up and Down buttons on the XPanel. I also decided to expose the ScreenUp and ScreenDown methods since we’ll need them later when we create the system macro logic.

We check to see if the screen is currently traveling in the opposite direction and stop it before sending it in the new direction. Once the duration has expired, we open the relay again and say that it has stopped moving.

Here’s a good example of picking the standard System.Threading.Thread class over what Crestron provides in Crestron.SimplSharpPro.CrestronThread. I would say always prefer the standard System classes when possible since they are tested by more programmers and the documentation is far better.

This is my first stab at writing this class, so the relay control and UI elements are tightly coupled. If I were planning to reuse this class in another project, I’d try to move the UI logic into another event that fires when the relays open and close.

Back in InitializeSystem, we add ScreenControl to our program (if our controller has relay support):

if (this.SupportsRelay)
{
    // Screen relays must be latched for 20 seconds
    _screen = new ScreenControl(_ui, this.RelayPorts[1], this.RelayPorts[2], 20);
}
else
{
    _screen = null;
}

Analog Ramps

The next area I wanted to tackle was volume control. I haven’t used ramps much in SIMPL#, so this was a learning experience for me. I decided to keep it simple and, rather than adding ramps to our UI class (which is probably where it belongs), I’m going to use the ones built into the Crestron.SimplSharpPro.Sig class.

using System;
using Crestron.SimplSharpPro;

namespace P201_Projector_Exam
{
    internal class VolumeControl
    {
        private UI _ui;
        private Sig _gauge;

        private bool _muteOn;

        public VolumeControl(UI ui, Sig gauge)
        {
            _ui = ui;
            _ui.Press += _ui_Press;
            _ui.Release += _ui_Release;

            _gauge = gauge;
        }

        private void _ui_Press(object dev, uint sig)
        {
            switch (sig)
            {
                case 29: // Volume Up
                    _ui.SetFeedback(29, true);
                    _gauge.CreateRamp(65535, 500); // 5s
                    SetMute(false);
                    break;
                case 30: // Volume Down
                    _ui.SetFeedback(30, true);
                    _gauge.CreateRamp(0, 500); // 5s
                    SetMute(false);
                    break;
                case 31: // Volume Mute
                    SetMute(!_muteOn);
                    break;
                case 32: // Preset 1
                    _ui.SetFeedback(32, true);
                    SetPreset(16383); // 25%
                    break;
                case 33: // Preset 2
                    _ui.SetFeedback(33, true);
                    SetPreset(32767); // 50%
                    break;
                case 34: // Preset 3
                    _ui.SetFeedback(34, true);
                    SetPreset(49151); // 75%
                    break;
            }
        }

        private void _ui_Release(object dev, uint sig)
        {
            switch (sig)
            {
                case 29: // Volume Up
                    _ui.SetFeedback(29, false);
                    _gauge.StopRamp();
                    break;
                case 30: // Volume Down
                    _ui.SetFeedback(30, false);
                    _gauge.StopRamp();
                    break;
                case 32: // Preset 1
                    _ui.SetFeedback(32, false);
                    break;
                case 33: // Preset 2
                    _ui.SetFeedback(33, false);
                    break;
                case 34: // Preset 3
                    _ui.SetFeedback(34, false);
                    break;
            }
        }

        public void SetMute(bool mute)
        {
            _muteOn = mute;
            _ui.SetFeedback(31, _muteOn);
        }

        public void SetPreset(ushort value)
        {
            SetMute(false);
            _gauge.CreateRamp(value, 400);
        }
    }
}

Again, I hook into our UI Press and Release events. One thing I forgot about pre-SmartGraphics panels was that even for momentary press feedback, everything is driven by the control system. You can see the corresponding SetFeedback(join, true) and SetFeedback(join, false) statements.

One thing I like about this design is all the volume control logic is in one place. However, we’re still tightly-coupled between UI and function. And the join numbers are hard-coded. It would be nice if we could make this class more re-usable for a future project.

Subpages

To get much further into the touchpanel layout, I need to add support for system power and source selection. I’m going to add a couple of private variables to our ControlSystem class to track the room state (rather than querying a real projector or switcher):

public class ControlSystem : CrestronControlSystem
{
    private UI _ui;
    private ScreenControl _screen;
    private VolumeControl _volume;

    private bool _systemOn;
    private int _src;

    public ControlSystem()
        : base()
    {

Lets update InitializeSystem to trap button presses:

public override void InitializeSystem()
{
    try
    {
        var xp = new Xpanel(0x03, this);

        // UI class manages all connected touchpanels
        _ui = new UI();
        _ui.Press += _ui_Press;

        // Different UI elements hook into UI manager
                
        if (this.SupportsRelay)
        {
            // Screen relays must be latched for 20 seconds
            _screen = new ScreenControl(_ui, this.RelayPorts[1], this.RelayPorts[2], 20);
        }
        else
        {
            _screen = null;
        }

        _volume = new VolumeControl(_ui, xp.UShortInput[1]);

        // This is _NOT_ a mistake!  My original exam predates SmartGraphics, so an
        // XPanel executable was provided with the class materials.  It also ran on
        // a PRO2, but 2-series won't run S# programs.
        _ui.Add(xp);
    }
    catch (Exception e)
    {
        ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
    }
}

Then add _ui_Press below that to handle what those button presses do:

private void _ui_Press(object dev, uint sig)
{
    switch (sig)
    {
        case 3: // Power toggle
            _systemOn = !_systemOn;
            _ui.SetFeedback(3, _systemOn);
            _ui_SelectSource(_src);
            break;

        case 8: // Start Up
            _systemOn = true;
            _src = 1;
            _ui.SetFeedback(3, _systemOn);
            _ui_SelectSource(1); // vcr
            break;

        case 9: // Shut Down
            _systemOn = false;
            _src = 0;
            _ui.SetFeedback(3, _systemOn);
            _ui_SelectSource(0); // nothing
            break;

        case 40: // Source - VCR
            _ui_SelectSource(1);
            break;

        case 41: // Source - DVD
            _ui_SelectSource(2);
            break;

        case 42: // Source - PC
            _ui_SelectSource(3);
            break;
    }
}

Add _ui_SelectSource below that to handle source selection feedback since it’s a lot of duplicated logic:

private void _ui_SelectSource(int src)
{
    _src = src;

    _ui.SetFeedback(40, _systemOn && _src == 1); // VCR
    _ui.SetFeedback(41, _systemOn && _src == 2); // DVD
    _ui.SetFeedback(42, _systemOn && _src == 3); // PC
}

Note that the feedback will only be true if _systemOn is true. The subpages on our XPanel track the same join numbers as our buttons:

Next we’ll work on the VCR and DVD buttons.

IR Control

The exam specified specific VCR and DVD players to control. I copied the two IR drivers out of the Crestron database and added them to my project: Mitsubishi DD-2000 and Mitsubishi HSU-770.

Lets create a new VcrControl class to handle loading the IR driver and responding to button presses:

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

namespace P201_Projector_Exam
{
    internal class VcrControl
    {
        private UI _ui;
        private IROutputPort _port;

        public VcrControl(UI ui, IROutputPort port, string irFileName)
        {
            _ui = ui;
            _ui.Press += _ui_Press;
            _ui.Release += _ui_Release;

            _port = port;

            CrestronConsole.PrintLine("VCR: Loading {0}...", irFileName);
            _port.LoadIRDriver(irFileName);
        }

        private void _ui_Press(object dev, uint sig)
        {
            switch (sig)
            {
                case 11: // Rewind
                    _port.Press("REW");
                    _ui.SetFeedback(11, true);
                    break;
                case 12: // Forward
                    _port.Press("FFWD");
                    _ui.SetFeedback(12, true);
                    break;
                case 13: // Stop
                    _port.Press("STOP");
                    _ui.SetFeedback(13, true);
                    break;
                case 14: // Pause
                    _port.Press("PAUSE");
                    _ui.SetFeedback(14, true);
                    break;
                case 15: // Play
                    _port.Press("PLAY");
                    _ui.SetFeedback(15, true);
                    break;
            }
        }

        private void _ui_Release(object dev, uint sig)
        {
            switch (sig)
            {
                case 11: // Rewind
                    _port.Release();
                    _ui.SetFeedback(11, false);
                    break;
                case 12: // Forward
                    _port.Release();
                    _ui.SetFeedback(12, false);
                    break;
                case 13: // Stop
                    _port.Release();
                    _ui.SetFeedback(13, false);
                    break;
                case 14: // Pause
                    _port.Release();
                    _ui.SetFeedback(14, false);
                    break;
                case 15: // Play
                    _port.Release();
                    _ui.SetFeedback(15, false);
                    break;
            }
        }
    }
}

Since all the button feedback is momentary, we need to hook into the Press and Release events on the UI manager. The tricky part for IR control is knowing the name of the signal to press. I had to look at my SIMPL program to see what names to use: PLAY, STOP, PAUSE, etc.

Our DvdControl class looks roughly the same:

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

namespace P201_Projector_Exam
{
    internal class DvdControl
    {
        private UI _ui;
        private IROutputPort _port;

        public DvdControl(UI ui, IROutputPort port, string irFileName)
        {
            _ui = ui;
            _ui.Press += _ui_Press;
            _ui.Release += _ui_Release;

            _port = port;

            CrestronConsole.PrintLine("DVD: Loading {0}...", irFileName);
            _port.LoadIRDriver(irFileName);
        }

        private void _ui_Press(object dev, uint sig)
        {
            switch (sig)
            {
                case 16: // Previous
                    _port.Press("|<<SKIP");
                    _ui.SetFeedback(16, true);
                    break;
                case 17: // Rewind
                    _port.Press("REV");
                    _ui.SetFeedback(17, true);
                    break;
                case 18: // Menu
                    _port.Press("MENU");
                    _ui.SetFeedback(18, true);
                    break;
                case 19: // Forward
                    _port.Press("FWD");
                    _ui.SetFeedback(19, true);
                    break;
                case 20: // Next
                    _port.Press("SKIP>>|");
                    _ui.SetFeedback(20, true);
                    break;
                case 21: // Stop
                    _port.Press("STOP");
                    _ui.SetFeedback(21, true);
                    break;
                case 22: // Play
                    _port.Press("PLAY");
                    _ui.SetFeedback(22, true);
                    break;
                case 23: // Pause
                    _port.Press("PAUSE");
                    _ui.SetFeedback(23, true);
                    break;
                case 24: // Up
                    _port.Press("UP");
                    _ui.SetFeedback(24, true);
                    break;
                case 25: // Down
                    _port.Press("DOWN");
                    _ui.SetFeedback(25, true);
                    break;
                case 26: // Left
                    _port.Press("LEFT");
                    _ui.SetFeedback(26, true);
                    break;
                case 27: // Right
                    _port.Press("RIGHT");
                    _ui.SetFeedback(27, true);
                    break;
                case 28: // Enter
                    _port.Press("ENTER");
                    _ui.SetFeedback(28, true);
                    break;
            }
        }

        private void _ui_Release(object dev, uint sig)
        {
            switch (sig)
            {
                case 16: // Previous
                    _port.Release();
                    _ui.SetFeedback(16, false);
                    break;
                case 17: // Rewind
                    _port.Release();
                    _ui.SetFeedback(17, false);
                    break;
                case 18: // Menu
                    _port.Release();
                    _ui.SetFeedback(18, false);
                    break;
                case 19: // Forward
                    _port.Release();
                    _ui.SetFeedback(19, false);
                    break;
                case 20: // Next
                    _port.Release();
                    _ui.SetFeedback(20, false);
                    break;
                case 21: // Stop
                    _port.Release();
                    _ui.SetFeedback(21, false);
                    break;
                case 22: // Play
                    _port.Release();
                    _ui.SetFeedback(22, false);
                    break;
                case 23: // Pause
                    _port.Release();
                    _ui.SetFeedback(23, false);
                    break;
                case 24: // Up
                    _port.Release();
                    _ui.SetFeedback(24, false);
                    break;
                case 25: // Down
                    _port.Release();
                    _ui.SetFeedback(25, false);
                    break;
                case 26: // Left
                    _port.Release();
                    _ui.SetFeedback(26, false);
                    break;
                case 27: // Right
                    _port.Release();
                    _ui.SetFeedback(27, false);
                    break;
                case 28: // Enter
                    _port.Release();
                    _ui.SetFeedback(28, false);
                    break;
            }
        }
    }
}

So much repetition! These two classes are a good place to look for some refactoring opportunities.

They also need to be tied into our ControlSystem class:

public class ControlSystem : CrestronControlSystem
{
    private UI _ui;
    private ScreenControl _screen;
    private VcrControl _vcr;
    private DvdControl _dvd;
    private VolumeControl _volume;

Test our controller for IR support, then create the instances:

public override void InitializeSystem()
{
    try
    {
        var xp = new Xpanel(0x03, this);

        // UI class manages all connected touchpanels
        _ui = new UI();
        _ui.Press += _ui_Press;

        // Different UI elements hook into UI manager
                
        if (this.SupportsRelay)
        {
            // Screen relays must be latched for 20 seconds
            _screen = new ScreenControl(_ui, this.RelayPorts[1], this.RelayPorts[2], 20);
        }
        else
        {
            _screen = null;
        }

        if (this.SupportsIROut)
        {
            _vcr = new VcrControl(_ui, this.IROutputPorts[1], Path.Combine(Directory.GetApplicationDirectory(), "MITSUBISHI HSU-770 VHS VCR.ir"));
            _dvd = new DvdControl(_ui, this.IROutputPorts[2], Path.Combine(Directory.GetApplicationDirectory(), "MITSUBISHI DD-2000.ir"));
        }
        else
        {
            _vcr = null;
            _dvd = null;
        }

        _volume = new VolumeControl(_ui, xp.UShortInput[1]);

        // This is _NOT_ a mistake!  My original exam predates SmartGraphics, so an
        // XPanel executable was provided with the class materials.  It also ran on
        // a PRO2, but 2-series won't run S# programs.
        _ui.Add(xp);
    }
    catch (Exception e)
    {
        ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
    }
}

Indirect Text

Projector control on the exam was very simple: we just had to send whatever commands were meant for the projector to the touchpanel instead. Here is the ProjectorControl class. Nothing new here besides using UI.SetSerial to send text to our touchpanel:

using System;

namespace P201_Projector_Exam
{
    internal class ProjectorControl
    {
        private UI _ui;

        private bool _powerOn;
        public bool PowerOn
        {
            get
            {
                return _powerOn;
            }
            set
            {
                if (_powerOn != value)
                {
                    if (value)
                        TurnOn();
                    else
                        TurnOff();
                }
            }
        }

        public ProjectorControl(UI ui)
        {
            _ui = ui;
        }

        public void TurnOn()
        {
            _ui.SetSerial(2, "\x0200PON\x03");
            _powerOn = true;
        }

        public void TurnOff()
        {
            _ui.SetSerial(2, "\x0200POF\x03");
            _powerOn = false;
        }

        public void SetInput(int input)
        {
            _ui.SetSerial(2, String.Format("\x0200IN{0}\x03", input));
        }
    }
}

Don’t forget to add to ControlSystem:

public class ControlSystem : CrestronControlSystem
{
    private UI _ui;
    private ProjectorControl _proj;
    private ScreenControl _screen;
    private VcrControl _vcr;
    private DvdControl _dvd;
    private VolumeControl _volume;

public override void InitializeSystem()
{
    try
    {
        var xp = new Xpanel(0x03, this);

        // UI class manages all connected touchpanels
        _ui = new UI();
        _ui.Press += _ui_Press;

        // Different UI elements hook into UI manager
        _proj = new ProjectorControl(_ui);
        
        if (this.SupportsRelay)
        {
            // Screen relays must be latched for 20 seconds
            _screen = new ScreenControl(_ui, this.RelayPorts[1], this.RelayPorts[2], 20);
        }
        else
        {
            _screen = null;
        }

        if (this.SupportsIROut)
        {
            _vcr = new VcrControl(_ui, this.IROutputPorts[1], Path.Combine(Directory.GetApplicationDirectory(), "MITSUBISHI HSU-770 VHS VCR.ir"));
            _dvd = new DvdControl(_ui, this.IROutputPorts[2], Path.Combine(Directory.GetApplicationDirectory(), "MITSUBISHI DD-2000.ir"));
        }
        else
        {
            _vcr = null;
            _dvd = null;
        }

        _volume = new VolumeControl(_ui, xp.UShortInput[1]);

        // This is _NOT_ a mistake!  My original exam predates SmartGraphics, so an
        // XPanel executable was provided with the class materials.  It also ran on
        // a PRO2, but 2-series won't run S# programs.
        _ui.Add(xp);
    }
    catch (Exception e)
    {
        ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
    }
}

Macros

The touchpanel layout has two macro buttons: Start Up and Shut Down. At the time I wrote the original program, I did these as steppers. In SIMPL#, things work a little differently because we’ve moved a lot of the logic into separate classes. We can create a new SystemMacros class to touch the public interfaces we’ve exposed in those other classes:

using System;
using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;

namespace P201_Projector_Exam
{
    internal class SystemMacros
    {
        private UI _ui;
        private ProjectorControl _proj;
        private ScreenControl _screen;
        private VolumeControl _volume;

        public SystemMacros(UI ui, ProjectorControl proj, ScreenControl screen, VolumeControl volume)
        {
            _ui = ui;
            _ui.Press += _ui_Press;
            _ui.Release += _ui_Release;

            _proj = proj;
            _screen = screen;
            _volume = volume;
        }

        private void _ui_Press(object dev, uint sig)
        {
            switch (sig)
            {
                case 8: // Start Up
                    CrestronConsole.PrintLine("StartUp Macro running");
                    _ui.SetFeedback(8, true);

                    if (_proj != null)
                        _proj.TurnOn();

                    if (_screen != null)
                        _screen.ScreenDown();

                    if (_volume != null)
                        _volume.SetPreset(49151); // 75%

                    break;
                case 9: // Shut Down
                    CrestronConsole.PrintLine("ShutDown Macro running");
                    _ui.SetFeedback(9, true);

                    if (_proj != null)
                        _proj.TurnOff();

                    if (_screen != null)
                        _screen.ScreenUp();

                    if (_volume != null)
                        _volume.SetMute(true);

                    break;
            }
        }

        private void _ui_Release(object dev, uint sig)
        {
            switch (sig)
            {
                case 8: // Start Up
                    _ui.SetFeedback(8, false);
                    break;
                case 9: // Shut Down
                    _ui.SetFeedback(9, false);
                    break;
            }
        }
    }
}

We perform a lot of checks to make sure the different subsystems aren’t null because our ControlSystem may not have initialized those depending on the available hardware (e.g.: no relay or IR ports). This operation isn’t the same as a stepper–which inserts delays between steps–but looking at my 0.1s spacing I don’t think there was a requirement to delay anything.

More Indirect Text

The last two areas to cover include more indirect text: putting the current date and programmer information onto the panel. The date is easier, so lets start there. I want to handle the date in a separate thread:

public class ControlSystem : CrestronControlSystem
{
    private UI _ui;
    private ProjectorControl _proj;
    private ScreenControl _screen;
    private VcrControl _vcr;
    private DvdControl _dvd;
    private VolumeControl _volume;
    private SystemMacros _macros;

    private Thread _dateRefresh;

We’ll initialize the thread at the bottom of InitializeSystem:

        _volume = new VolumeControl(_ui, xp.UShortInput[1]);
        _macros = new SystemMacros(_ui, _proj, _screen, _volume);

        // This is _NOT_ a mistake!  My original exam predates SmartGraphics, so an
        // XPanel executable was provided with the class materials.  It also ran on
        // a PRO2, but 2-series won't run S# programs.
        _ui.Add(xp);

        _dateRefresh = new Thread(SerializeDate, null);
    }
    catch (Exception e)
    {
        ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
    }
}

Then add the SerializeDate callback to the ControlSystem class:

private object SerializeDate(object userObj)
{
    string now = String.Empty;
    string then = String.Empty;

    while (true)
    {
        // 10/02/2022
        now = DateTime.Today.ToShortDateString();

        if (!now.Equals(then))
        {
            _ui.SetSerial(1, now);
            then = now;
        }

        Thread.Sleep(300000); // 5 minutes
    }
}

The code loops forever. At the top we use the ToShortDateString method on DateTime.Today to get something close to what I want. We could also format the date any way that we want, this is just a convenenience method. If this string doesn’t match what we previously sent to the touchpanel, we update the touchpanel, then remember this date string. Then sleep for 5 minutes and do it again. Somewhere around midnight there’s going to be up to a 5 minute delay before the date refreshes. But chances are slim anybody is looking at the touchpanel around midnight anyway.

But this loop never ends, what happens if our program restarts? We need to catch when the controller is stopping our program. I usually add this to the ControlSystem constructor:

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

        CrestronEnvironment.ProgramStatusEventHandler += ProgramStatusChange;
    }
    catch (Exception e)
    {
        ErrorLog.Error("Error in the constructor: {0}", e.Message);
    }
}

Then add the ProgramStatusChange handler below:

private void ProgramStatusChange(eProgramStatusEventType type)
{
    if (type == eProgramStatusEventType.Stopping)
    {
        if (_dateRefresh != null)
            _dateRefresh.Abort();
    }
}

We can Abort the _dateRefresh thread when our program is stopping. Remember, our thread might be sleeping for up to 5 minutes. I don’t want to wait that long for it to stop.

And for the programmer information, I’m going to add yet another class to tie into those button presses. Here’s ProgrammerInfo.cs:

using System;
using System.Linq;

namespace P201_Projector_Exam
{
    internal class ProgrammerInfo
    {
        private UI _ui;

        private uint[] _sigs = { 61, 62, 63, 64, 65, 66, 67, 68, 69, 70 };

        public ProgrammerInfo(UI ui)
        {
            _ui = ui;
            _ui.Press += _ui_Press;
            _ui.Release += _ui_Release;
        }

        private void _ui_Press(object dev, uint sig)
        {
            if (_sigs.Contains(sig))
            {
                _ui.SetFeedback(sig, true);

                switch (sig)
                {
                    case 61:
                        _ui.SetSerial(3, "Kiel Lofstrand");
                        break;
                    case 62:
                        _ui.SetSerial(3, "Providea Conferencing");
                        break;
                    case 63:
                        _ui.SetSerial(3, "1297 Flynn Rd");
                        break;
                    case 64:
                        _ui.SetSerial(3, "Suite 100");
                        break;
                    case 65:
                        _ui.SetSerial(3, "Camarillo, CA");
                        break;
                    case 66:
                        _ui.SetSerial(3, "93012-8015");
                        break;
                    case 67:
                        _ui.SetSerial(3, "klofstrand@provideallc.com");
                        break;
                    case 68:
                        _ui.SetSerial(3, "805-616-5995");
                        break;
                    case 69:
                        _ui.SetSerial(3, "Denver, CO");
                        break;
                    case 70:
                        _ui.SetSerial(3, "Dec 19 - Dec 21");
                        break;
                }
            }
        }

        private void _ui_Release(object dev, uint sig)
        {
            if (_sigs.Contains(sig))
            {
                _ui.SetFeedback(sig, false);
            }
        }
    }
}

This time I decided I’d use an array of which join numbers I want to handle. In _ui_Press and _ui_Release, I check to see if the sig number passed in can be found in _sigs (the signals we are monitoring). If we match one, we set the feedback true or false. Depending on which button was pressed, we send different information to the touchpanel.

Remember we need to tie this into our _ui object in InitializeSystem too:

public override void InitializeSystem()
{
    try
    {
        var xp = new Xpanel(0x03, this);

        // UI class manages all connected touchpanels
        _ui = new UI();
        _ui.Press += _ui_Press;

        // Different UI elements hook into UI manager
        _proj = new ProjectorControl(_ui);
                
        if (this.SupportsRelay)
        {
            // Screen relays must be latched for 20 seconds
            _screen = new ScreenControl(_ui, this.RelayPorts[1], this.RelayPorts[2], 20);
        }
        else
        {
            _screen = null;
        }

        if (this.SupportsIROut)
        {
            _vcr = new VcrControl(_ui, this.IROutputPorts[1], Path.Combine(Directory.GetApplicationDirectory(), "MITSUBISHI HSU-770 VHS VCR.ir"));
            _dvd = new DvdControl(_ui, this.IROutputPorts[2], Path.Combine(Directory.GetApplicationDirectory(), "MITSUBISHI DD-2000.ir"));
        }
        else
        {
            _vcr = null;
            _dvd = null;
        }

        _volume = new VolumeControl(_ui, xp.UShortInput[1]);
        _macros = new SystemMacros(_ui, _proj, _screen, _volume);
        _info = new ProgrammerInfo(_ui);

        // This is _NOT_ a mistake!  My original exam predates SmartGraphics, so an
        // XPanel executable was provided with the class materials.  It also ran on
        // a PRO2, but 2-series won't run S# programs.
        _ui.Add(xp);

        _dateRefresh = new Thread(SerializeDate, null);
    }
    catch (Exception e)
    {
        ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
    }
}

Build this program and send it to your controller and the XPanel should be complete.

Conclusion

This was a fun trip down memory lane. I spent so long writing this post, I kind of forgot what I wanted to say about this program. Oh well, that’s what happens when work gets busy! Don’t think I would use much of what we built in this example, but it does give me ideas for how to attack the next one.

Thanks for reading!

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 )

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