diff --git a/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortNames.cs b/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortNames.cs index 1377c9d4..70b9adef 100644 --- a/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortNames.cs +++ b/Essentials Core/PepperDashEssentialsBase/Routing/RoutingPortNames.cs @@ -184,8 +184,20 @@ namespace PepperDash.Essentials.Core.Routing /// public const string VgaIn = "vgaIn"; /// + /// vgaIn1 + /// + public const string VgaIn1 = "vgaIn1"; + /// /// vgaOut /// public const string VgaOut = "vgaOut"; + /// + /// IPC/OPS + /// + public const string IpcOps = "ipcOps"; + /// + /// MediaPlayer + /// + public const string MediaPlayer = "mediaPlayer"; } } \ No newline at end of file diff --git a/Essentials Devices Common/Essentials Devices Common/Display/AvocorVTFDisplay.cs b/Essentials Devices Common/Essentials Devices Common/Display/AvocorVTFDisplay.cs new file mode 100644 index 00000000..ada97eef --- /dev/null +++ b/Essentials Devices Common/Essentials Devices Common/Display/AvocorVTFDisplay.cs @@ -0,0 +1,649 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.CrestronThread; +using Crestron.SimplSharpPro; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Devices.Displays +{ + /// + /// + /// + public class AvocorDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, ICommunicationMonitor, IInputDisplayPort1, + IInputHdmi1, IInputHdmi2, IInputHdmi3, IInputHdmi4, IInputVga1 + { + public IBasicCommunication Communication { get; private set; } + public CommunicationGather PortGather { get; private set; } + + public StatusMonitorBase CommunicationMonitor { get; private set; } + + public byte ID { get; private set; } + + bool _PowerIsOn; + bool _IsWarmingUp; + bool _IsCoolingDown; + ushort _VolumeLevelForSig; + int _LastVolumeSent; + bool _IsMuted; + RoutingInputPort _CurrentInputPort; + //byte[] IncomingBuffer = new byte[]{}; + ActionIncrementer VolumeIncrementer; + bool VolumeIsRamping; + public bool IsInStandby { get; private set; } + + protected override Func PowerIsOnFeedbackFunc { get { return () => _PowerIsOn; } } + protected override Func IsCoolingDownFeedbackFunc { get { return () => _IsCoolingDown; } } + protected override Func IsWarmingUpFeedbackFunc { get { return () => _IsWarmingUp; } } + protected override Func CurrentInputFeedbackFunc { get { return () => _CurrentInputPort.Key; } } + + /// + /// Constructor for IBasicCommunication + /// + public AvocorDisplay(string key, string name, IBasicCommunication comm, string id) + : base(key, name) + { + Communication = comm; + //Communication.BytesReceived += new EventHandler(Communication_BytesReceived); + + PortGather = new CommunicationGather(Communication, '\x08'); + PortGather.IncludeDelimiter = true; + PortGather.LineReceived += new EventHandler(PortGather_LineReceived); + + ID = id == null ? (byte)0x01 : Convert.ToByte(id, 16); // If id is null, set default value of 0x01, otherwise assign value passed in constructor + Init(); + } + + /// + /// Constructor for TCP + /// + public AvocorDisplay(string key, string name, string hostname, int port, string id) + : base(key, name) + { + Communication = new GenericTcpIpClient(key + "-tcp", hostname, port, 5000); + + PortGather = new CommunicationGather(Communication, '\x0d'); + PortGather.IncludeDelimiter = true; + PortGather.LineReceived += new EventHandler(PortGather_LineReceived); + + ID = id == null ? (byte)0x01 : Convert.ToByte(id, 16); // If id is null, set default value of 0x01, otherwise assign value passed in constructor + Init(); + } + + /// + /// Constructor for COM + /// + public AvocorDisplay(string key, string name, ComPort port, ComPort.ComPortSpec spec, string id) + : base(key, name) + { + Communication = new ComPortController(key + "-com", port, spec); + + PortGather = new CommunicationGather(Communication, '\x0d'); + PortGather.IncludeDelimiter = true; + PortGather.LineReceived += new EventHandler(PortGather_LineReceived); + + ID = id == null ? (byte)0x01 : Convert.ToByte(id, 16); // If id is null, set default value of 0x01, otherwise assign value passed in constructor + Init(); + } + + void AddRoutingInputPort(RoutingInputPort port, byte fbMatch) + { + port.FeedbackMatchObject = fbMatch; + InputPorts.Add(port); + } + + void Init() + { + WarmupTime = 10000; + CooldownTime = 8000; + + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 30000, 120000, 300000, StatusGet); + DeviceManager.AddDevice(CommunicationMonitor); + + VolumeIncrementer = new ActionIncrementer(655, 0, 65535, 800, 80, + v => SetVolume((ushort)v), + () => _LastVolumeSent); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi1), this), 0x09); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi2), this), 0x10); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi3), this), 0x11); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn4, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi4), this), 0x12); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.HdmiIn5, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, new Action(InputHdmi5), this), 0x17); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.DisplayPortIn1, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DisplayPort, new Action(InputDisplayPort1), this), 0x13); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.VgaIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Dvi, new Action(InputVga1), this), 0x00); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.IpcOps, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Composite, new Action(InputIpcOps), this), 0x14); + + AddRoutingInputPort(new RoutingInputPort(RoutingPortNames.MediaPlayer, eRoutingSignalType.Video, + eRoutingPortConnectionType.Vga, new Action(InputMediaPlayer), this), 0x18); + + VolumeLevelFeedback = new IntFeedback(() => { return _VolumeLevelForSig; }); + MuteFeedback = new BoolFeedback(() => _IsMuted); + + StatusGet(); + } + + /// + /// + /// + /// + public override bool CustomActivate() + { + Communication.Connect(); + CommunicationMonitor.StatusChange += (o, a) => { Debug.Console(2, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + return true; + } + + public override List Feedbacks + { + get + { + var list = base.Feedbacks; + list.AddRange(new List + { + VolumeLevelFeedback, + MuteFeedback, + CurrentInputFeedback + }); + return list; + } + } + + ///// + ///// / + ///// + ///// + //void Communication_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e) + //{ + // // This is probably not thread-safe buffering + // // Append the incoming bytes with whatever is in the buffer + // var newBytes = new byte[IncomingBuffer.Length + e.Bytes.Length]; + // IncomingBuffer.CopyTo(newBytes, 0); + // e.Bytes.CopyTo(newBytes, IncomingBuffer.Length); + + // if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1 + // Debug.Console(2, this, "Received:{0}", ComTextHelper.GetEscapedText(newBytes)); + + // // Need to find AA FF and have + // for (int i = 0; i < newBytes.Length; i++) + // { + // if (newBytes[i] == 0xAA && newBytes[i + 1] == 0xFF) + // { + // newBytes = newBytes.Skip(i).ToArray(); // Trim off junk if there's "dirt" in the buffer + + // // parse it + // // If it's at least got the header, then process it, + // while (newBytes.Length > 4 && newBytes[0] == 0xAA && newBytes[1] == 0xFF) + // { + // var msgLen = newBytes[3]; + // // if the buffer is shorter than the header (3) + message (msgLen) + checksum (1), + // // give and save it for next time + // if (newBytes.Length < msgLen + 4) + // break; + + // // Good length, grab the message + // var message = newBytes.Skip(4).Take(msgLen).ToArray(); + + // // At this point, the ack/nak is the first byte + // if (message[0] == 0x41) + // { + // switch (message[1]) // type byte + // { + // case 0x00: // General status + // UpdatePowerFB(message[2], message[5]); // "power" can be misrepresented when the display sleeps + // UpdateInputFb(message[5]); + // UpdateVolumeFB(message[3]); + // UpdateMuteFb(message[4]); + // UpdateInputFb(message[5]); + // break; + + // case 0x11: + // UpdatePowerFB(message[2]); + // break; + + // case 0x12: + // UpdateVolumeFB(message[2]); + // break; + + // case 0x13: + // UpdateMuteFb(message[2]); + // break; + + // case 0x14: + // UpdateInputFb(message[2]); + // break; + + // default: + // break; + // } + // } + // // Skip over what we've used and save the rest for next time + // newBytes = newBytes.Skip(5 + msgLen).ToArray(); + // } + + // break; // parsing will mean we can stop looking for header in loop + // } + // } + + // // Save whatever partial message is here + // IncomingBuffer = newBytes; + //} + + void PortGather_LineReceived(object sender, GenericCommMethodReceiveTextArgs e) + { + Debug.Console(1, this, "Receivied: '{0}'", ComTextHelper.GetEscapedText(e.Text)); + + if (e.Text.IndexOf("\x50\x4F\x57") > -1) + { + // Power Status Response + + var value = e.Text.ToCharArray(); + + switch (value[6]) + { + case '\x00': + { + _PowerIsOn = false; + break; + } + case '\x01': + { + _PowerIsOn = false; + break; + } + } + + PowerIsOnFeedback.FireUpdate(); + + } + else if (e.Text.IndexOf("\x4D\x49\x4E") > -1) + { + var value = e.Text.ToCharArray(); + + var b = value[6]; + + var newInput = InputPorts.FirstOrDefault(i => i.FeedbackMatchObject.Equals(b)); + if (newInput != null && newInput != _CurrentInputPort) + { + _CurrentInputPort = newInput; + CurrentInputFeedback.FireUpdate(); + } + + } + else if (e.Text.IndexOf("\x56\x4F\x4C") > -1) + { + // Volume Status Response + + var value = e.Text.ToCharArray(); + + var b = value[6]; + + var newVol = (ushort)NumericalHelpers.Scale((double)b, 0, 100, 0, 65535); + if (!VolumeIsRamping) + _LastVolumeSent = newVol; + if (newVol != _VolumeLevelForSig) + { + _VolumeLevelForSig = newVol; + VolumeLevelFeedback.FireUpdate(); + } + } + + } + + /// + /// + /// + void UpdatePowerFB(byte powerByte) + { + var newVal = powerByte == 1; + if (newVal != _PowerIsOn) + { + _PowerIsOn = newVal; + PowerIsOnFeedback.FireUpdate(); + } + } + + /// + /// Updates power status from general updates where source is included. + /// Compensates for errant standby / power off hiccups by ignoring + /// power off states with input < 0x10 + /// + void UpdatePowerFB(byte powerByte, byte inputByte) + { + // This should reject errant power feedbacks when switching away from input on standby. + if (powerByte == 0x01 && inputByte < 0x10) + IsInStandby = true; + if (powerByte == 0x00 && IsInStandby) // Ignore power off if coming from standby - glitch + { + IsInStandby = false; + return; + } + + UpdatePowerFB(powerByte); + } + + /// + /// + /// + void UpdateVolumeFB(byte b) + { + var newVol = (ushort)NumericalHelpers.Scale((double)b, 0, 100, 0, 65535); + if (!VolumeIsRamping) + _LastVolumeSent = newVol; + if (newVol != _VolumeLevelForSig) + { + _VolumeLevelForSig = newVol; + VolumeLevelFeedback.FireUpdate(); + } + } + + /// + /// + /// + void UpdateMuteFb(byte b) + { + var newMute = b == 1; + if (newMute != _IsMuted) + { + _IsMuted = newMute; + MuteFeedback.FireUpdate(); + } + } + + /// + /// + /// + void UpdateInputFb(byte b) + { + var newInput = InputPorts.FirstOrDefault(i => i.FeedbackMatchObject.Equals(b)); + if (newInput != null && newInput != _CurrentInputPort) + { + _CurrentInputPort = newInput; + CurrentInputFeedback.FireUpdate(); + } + } + + /// + /// Formats an outgoing message. Replaces third byte with ID and replaces last byte with checksum + /// + /// + void SendBytes(byte[] b) + { + b[1] = ID; + + var command = b.ToString(); + + Debug.Console(1, this, "Sending: '{0}'",ComTextHelper.GetEscapedText(b)); + + Communication.SendBytes(b); + } + + + /// + /// + /// + public void StatusGet() + { + PowerGet(); + + InputGet(); + } + + /// + /// + /// + public override void PowerOn() + { + //Send(PowerOnCmd); + SendBytes(new byte[] { 0x07, ID, 0x02, 0x50, 0x4F, 0x57, 0x01, 0x08, 0x0d }); + if (!PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) + { + _IsWarmingUp = true; + IsWarmingUpFeedback.FireUpdate(); + // Fake power-up cycle + WarmupTimer = new CTimer(o => + { + _IsWarmingUp = false; + _PowerIsOn = true; + IsWarmingUpFeedback.FireUpdate(); + PowerIsOnFeedback.FireUpdate(); + }, WarmupTime); + } + } + + /// + /// + /// + public override void PowerOff() + { + // If a display has unreliable-power off feedback, just override this and + // remove this check. + if (!_IsWarmingUp && !_IsCoolingDown) // PowerIsOnFeedback.BoolValue && + { + //Send(PowerOffCmd); + SendBytes(new byte[] { 0x07, ID, 0x02, 0x50, 0x4F, 0x57, 0x00, 0x08, 0x0d }); + _IsCoolingDown = true; + _PowerIsOn = false; + PowerIsOnFeedback.FireUpdate(); + IsCoolingDownFeedback.FireUpdate(); + // Fake cool-down cycle + CooldownTimer = new CTimer(o => + { + _IsCoolingDown = false; + IsCoolingDownFeedback.FireUpdate(); + }, CooldownTime); + } + } + + public override void PowerToggle() + { + if (PowerIsOnFeedback.BoolValue && !IsWarmingUpFeedback.BoolValue) + PowerOff(); + else if (!PowerIsOnFeedback.BoolValue && !IsCoolingDownFeedback.BoolValue) + PowerOn(); + } + + public void PowerGet() + { + SendBytes(new byte[] { 0x07, ID, 0x01, 0x50, 0x4F, 0x57, 0x08, 0x0d }); + } + + public void InputHdmi1() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, 0x09, 0x08, 0x0d }); + } + + public void InputHdmi2() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, 0x10, 0x08, 0x0d }); + } + + public void InputHdmi3() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, 0x11, 0x08, 0x0d }); + } + + public void InputHdmi4() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, 0x12, 0x08, 0x0d }); + } + + public void InputHdmi5() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, 0x17, 0x08, 0x0d }); + } + + public void InputDisplayPort1() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, 0x13, 0x08, 0x0d }); + } + + public void InputVga1() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, 0x00, 0x08, 0x0d }); + } + + public void InputIpcOps() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, 0x14, 0x08, 0x0d }); + } + + public void InputMediaPlayer() + { + SendBytes(new byte[] { 0x07, ID, 0x02, 0x4D, 0x49, 0x4E, 0x18, 0x08, 0x0d }); + } + + public void InputGet() + { + SendBytes(new byte[] { 0x07, ID, 0x01, 0x4D, 0x49, 0x4E, 0x08, 0x0d }); + } + + public void VolumeGet() + { + SendBytes(new byte[] { 0x07, ID, 0x01, 0x56, 0x4F, 0x4C, 0x08, 0x0d }); + } + + + /// + /// Executes a switch, turning on display if necessary. + /// + /// + public override void ExecuteSwitch(object selector) + { + //if (!(selector is Action)) + // Debug.Console(1, this, "WARNING: ExecuteSwitch cannot handle type {0}", selector.GetType()); + + if (_PowerIsOn) + (selector as Action)(); + else // if power is off, wait until we get on FB to send it. + { + // One-time event handler to wait for power on before executing switch + EventHandler handler = null; // necessary to allow reference inside lambda to handler + handler = (o, a) => + { + if (!_IsWarmingUp) // Done warming + { + IsWarmingUpFeedback.OutputChange -= handler; + (selector as Action)(); + } + }; + IsWarmingUpFeedback.OutputChange += handler; // attach and wait for on FB + PowerOn(); + } + } + + /// + /// Scales the level to the range of the display and sends the command + /// + /// + public void SetVolume(ushort level) + { + _LastVolumeSent = level; + var scaled = (int)NumericalHelpers.Scale(level, 0, 65535, 0, 100); + // The inputs to Scale ensure that byte won't overflow + SendBytes(new byte[] { 0x07, ID, 0x02, 0x56, 0x4F, 0x4C, Convert.ToByte(scaled), 0x08, 0x0d }); + } + + #region IBasicVolumeWithFeedback Members + + public IntFeedback VolumeLevelFeedback { get; private set; } + + public BoolFeedback MuteFeedback { get; private set; } + + /// + /// + /// + public void MuteOff() + { + SendBytes(new byte[] { 0xAA, 0x13, 0x00, 0x01, 0x00, 0x00 }); + } + + /// + /// + /// + public void MuteOn() + { + SendBytes(new byte[] { 0xAA, 0x13, 0x00, 0x01, 0x01, 0x00 }); + } + + /// + /// + /// + public void MuteGet() + { + SendBytes(new byte[] { 0xAA, 0x13, 0x00, 0x00, 0x00 }); + } + + #endregion + + #region IBasicVolumeControls Members + + /// + /// + /// + public void MuteToggle() + { + if (_IsMuted) + MuteOff(); + else + MuteOn(); + } + + /// + /// + /// + /// + public void VolumeDown(bool pressRelease) + { + if (pressRelease) + { + VolumeIncrementer.StartDown(); + VolumeIsRamping = true; + } + else + { + VolumeIsRamping = false; + VolumeIncrementer.Stop(); + } + } + + /// + /// + /// + /// + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + { + VolumeIncrementer.StartUp(); + VolumeIsRamping = true; + } + else + { + VolumeIsRamping = false; + VolumeIncrementer.Stop(); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Essentials Devices Common/Essentials Devices Common/Display/DeviceFactory.cs b/Essentials Devices Common/Essentials Devices Common/Display/DeviceFactory.cs index 7c1107fe..dc778610 100644 --- a/Essentials Devices Common/Essentials Devices Common/Display/DeviceFactory.cs +++ b/Essentials Devices Common/Essentials Devices Common/Display/DeviceFactory.cs @@ -36,6 +36,12 @@ namespace PepperDash.Essentials.Devices.Displays var comm = CommFactory.CreateCommForDevice(dc); if (comm != null) return new SamsungMDC(dc.Key, dc.Name, comm, dc.Properties["id"].Value()); + } + if (typeName == "avocorvtf") + { + var comm = CommFactory.CreateCommForDevice(dc); + if (comm != null) + return new AvocorDisplay(dc.Key, dc.Name, comm, null); } } diff --git a/Essentials Devices Common/Essentials Devices Common/Display/InputInterfaces.cs b/Essentials Devices Common/Essentials Devices Common/Display/InputInterfaces.cs index ca237a4c..74a7ae97 100644 --- a/Essentials Devices Common/Essentials Devices Common/Display/InputInterfaces.cs +++ b/Essentials Devices Common/Essentials Devices Common/Display/InputInterfaces.cs @@ -11,6 +11,7 @@ namespace PepperDash.Essentials.Devices.Displays public interface IInputHdmi3 { void InputHdmi3(); } public interface IInputHdmi4 { void InputHdmi4(); } public interface IInputDisplayPort1 { void InputDisplayPort1(); } - public interface IInputDisplayPort2 { void InputDisplayPort2(); } + public interface IInputDisplayPort2 { void InputDisplayPort2(); } + public interface IInputVga1 { void InputVga1(); } } \ No newline at end of file diff --git a/Essentials Devices Common/Essentials Devices Common/Essentials Devices Common.csproj b/Essentials Devices Common/Essentials Devices Common/Essentials Devices Common.csproj index 194e4332..a4afa35c 100644 --- a/Essentials Devices Common/Essentials Devices Common/Essentials Devices Common.csproj +++ b/Essentials Devices Common/Essentials Devices Common/Essentials Devices Common.csproj @@ -105,6 +105,7 @@ + diff --git a/Release Package/PepperDashEssentials.cpz b/Release Package/PepperDashEssentials.cpz index 32760aac..d1d14eaf 100644 Binary files a/Release Package/PepperDashEssentials.cpz and b/Release Package/PepperDashEssentials.cpz differ diff --git a/Release Package/PepperDashEssentials.dll b/Release Package/PepperDashEssentials.dll index 43858084..35bcce9e 100644 Binary files a/Release Package/PepperDashEssentials.dll and b/Release Package/PepperDashEssentials.dll differ