using System; using System.Collections.Generic; using System.Linq; using Crestron.SimplSharp; using Crestron.SimplSharpPro.DeviceSupport; using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.Routing; using Serilog.Events; using Feedback = PepperDash.Essentials.Core.Feedback; namespace PepperDash.Essentials.Devices.Common.Displays { /// /// Abstract base class for display devices that provides common display functionality /// including power control, input switching, and routing capabilities. /// public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources { private RoutingInputPort _currentInputPort; /// /// Gets or sets the current input port that is selected on the display. /// public RoutingInputPort CurrentInputPort { get { return _currentInputPort; } protected set { if (_currentInputPort == value) return; _currentInputPort = value; InputChanged?.Invoke(this, _currentInputPort); } } /// /// Event that is raised when the input changes on the display. /// public event InputChangedEventHandler InputChanged; /// /// Event that is raised when the current source information changes. /// public event SourceInfoChangeHandler CurrentSourceChange; /// /// Gets or sets the CurrentSourceInfoKey /// public string CurrentSourceInfoKey { get; set; } /// /// Gets or sets the current source information for the display. /// public SourceListItem CurrentSourceInfo { get { return _CurrentSourceInfo; } set { if (value == _CurrentSourceInfo) return; var handler = CurrentSourceChange; if (handler != null) handler(_CurrentSourceInfo, ChangeType.WillChange); _CurrentSourceInfo = value; if (handler != null) handler(_CurrentSourceInfo, ChangeType.DidChange); } } SourceListItem _CurrentSourceInfo; /// public Dictionary CurrentSources { get; private set; } /// public Dictionary CurrentSourceKeys { get; private set; } /// public event EventHandler CurrentSourcesChanged; /// /// Gets feedback indicating whether the display is currently cooling down after being powered off. /// public BoolFeedback IsCoolingDownFeedback { get; protected set; } /// /// Gets or sets the IsWarmingUpFeedback /// public BoolFeedback IsWarmingUpFeedback { get; private set; } /// /// Gets or sets the UsageTracker /// public UsageTracking UsageTracker { get; set; } /// /// Gets or sets the WarmupTime /// public uint WarmupTime { get; set; } /// /// Gets or sets the CooldownTime /// public uint CooldownTime { get; set; } /// /// Abstract function that must be implemented by derived classes to provide the cooling down feedback value. /// Must be implemented by concrete sub-classes. /// abstract protected Func IsCoolingDownFeedbackFunc { get; } /// /// Abstract function that must be implemented by derived classes to provide the warming up feedback value. /// Must be implemented by concrete sub-classes. /// abstract protected Func IsWarmingUpFeedbackFunc { get; } /// /// Timer used for managing display warmup timing. /// protected CTimer WarmupTimer; /// /// Timer used for managing display cooldown timing. /// protected CTimer CooldownTimer; #region IRoutingInputs Members /// /// Gets the collection of input ports available on this display device. /// public RoutingPortCollection InputPorts { get; private set; } #endregion /// /// Initializes a new instance of the DisplayBase class. /// /// The unique key identifier for this display device. /// The friendly name for this display device. protected DisplayBase(string key, string name) : base(key, name) { IsCoolingDownFeedback = new BoolFeedback("IsCoolingDown", IsCoolingDownFeedbackFunc); IsWarmingUpFeedback = new BoolFeedback("IsWarmingUp", IsWarmingUpFeedbackFunc); InputPorts = new RoutingPortCollection(); CurrentSources = new Dictionary { { eRoutingSignalType.Audio, null }, { eRoutingSignalType.Video, null }, }; CurrentSourceKeys = new Dictionary { { eRoutingSignalType.Audio, string.Empty }, { eRoutingSignalType.Video, string.Empty }, }; } /// /// Powers on the display device. Must be implemented by derived classes. /// public abstract void PowerOn(); /// /// Powers off the display device. Must be implemented by derived classes. /// public abstract void PowerOff(); /// /// Toggles the power state of the display device. Must be implemented by derived classes. /// public abstract void PowerToggle(); /// /// Gets the collection of feedback objects for this display device. /// /// public virtual FeedbackCollection Feedbacks { get { return new FeedbackCollection { IsCoolingDownFeedback, IsWarmingUpFeedback }; } } /// /// Executes a switch to the specified input on the display device. Must be implemented by derived classes. /// /// The selector object that identifies which input to switch to. public abstract void ExecuteSwitch(object selector); /// /// Links the display device to an API using a trilist, join start, join map key, and bridge. /// This overload uses serialized join map configuration. /// /// The display device to link. /// The BasicTriList for communication. /// The starting join number for the device. /// The key for the join map configuration. /// The EISC API bridge instance. protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) { var joinMap = new DisplayControllerJoinMap(joinStart); var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); if (!string.IsNullOrEmpty(joinMapSerialized)) joinMap = JsonConvert.DeserializeObject(joinMapSerialized); if (bridge != null) { bridge.AddJoinMap(Key, joinMap); } else { Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); } LinkDisplayToApi(displayDevice, trilist, joinMap); } /// /// Links the display device to an API using a trilist and join map. /// This overload uses a pre-configured join map instance. /// /// The display device to link. /// The BasicTriList for communication. /// The join map configuration for the device. protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, DisplayControllerJoinMap joinMap) { Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); Debug.LogMessage(LogEventLevel.Information, "Linking to Display: {0}", displayDevice.Name); trilist.StringInput[joinMap.Name.JoinNumber].StringValue = displayDevice.Name; var commMonitor = displayDevice as ICommunicationMonitor; if (commMonitor != null) { commMonitor.CommunicationMonitor.IsOnlineFeedback.LinkInputSig(trilist.BooleanInput[joinMap.IsOnline.JoinNumber]); } var inputNumber = 0; var inputKeys = new List(); var inputNumberFeedback = new IntFeedback(() => inputNumber); // Two way feedbacks var twoWayDisplay = displayDevice as TwoWayDisplayBase; if (twoWayDisplay != null) { trilist.SetBool(joinMap.IsTwoWayDisplay.JoinNumber, true); twoWayDisplay.CurrentInputFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Information, "CurrentInputFeedback_OutputChange {0}", a.StringValue); inputNumberFeedback.LinkInputSig(trilist.UShortInput[joinMap.InputSelect.JoinNumber]); } // Power Off trilist.SetSigTrueAction(joinMap.PowerOff.JoinNumber, () => { inputNumber = 102; inputNumberFeedback.FireUpdate(); displayDevice.PowerOff(); }); var twoWayDisplayDevice = displayDevice as TwoWayDisplayBase; if (twoWayDisplayDevice != null) { twoWayDisplayDevice.PowerIsOnFeedback.OutputChange += (o, a) => { if (!a.BoolValue) { inputNumber = 102; inputNumberFeedback.FireUpdate(); } else { inputNumber = 0; inputNumberFeedback.FireUpdate(); } }; twoWayDisplayDevice.PowerIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.PowerOff.JoinNumber]); twoWayDisplayDevice.PowerIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.PowerOn.JoinNumber]); } // PowerOn trilist.SetSigTrueAction(joinMap.PowerOn.JoinNumber, () => { inputNumber = 0; inputNumberFeedback.FireUpdate(); displayDevice.PowerOn(); }); for (int i = 0; i < displayDevice.InputPorts.Count; i++) { if (i < joinMap.InputNamesOffset.JoinSpan) { inputKeys.Add(displayDevice.InputPorts[i].Key); var tempKey = inputKeys.ElementAt(i); trilist.SetSigTrueAction((ushort)(joinMap.InputSelectOffset.JoinNumber + i), () => displayDevice.ExecuteSwitch(displayDevice.InputPorts[tempKey].Selector)); Debug.LogMessage(LogEventLevel.Verbose, displayDevice, "Setting Input Select Action on Digital Join {0} to Input: {1}", joinMap.InputSelectOffset.JoinNumber + i, displayDevice.InputPorts[tempKey].Key.ToString()); trilist.StringInput[(ushort)(joinMap.InputNamesOffset.JoinNumber + i)].StringValue = displayDevice.InputPorts[i].Key.ToString(); } else Debug.LogMessage(LogEventLevel.Information, displayDevice, "Device has {0} inputs. The Join Map allows up to {1} inputs. Discarding inputs {2} - {3} from bridge.", displayDevice.InputPorts.Count, joinMap.InputNamesOffset.JoinSpan, i + 1, displayDevice.InputPorts.Count); } Debug.LogMessage(LogEventLevel.Verbose, displayDevice, "Setting Input Select Action on Analog Join {0}", joinMap.InputSelect); trilist.SetUShortSigAction(joinMap.InputSelect.JoinNumber, (a) => { if (a == 0) { displayDevice.PowerOff(); inputNumber = 0; } else if (a > 0 && a < displayDevice.InputPorts.Count && a != inputNumber) { displayDevice.ExecuteSwitch(displayDevice.InputPorts.ElementAt(a - 1).Selector); inputNumber = a; } else if (a == 102) { displayDevice.PowerToggle(); } if (twoWayDisplay != null) inputNumberFeedback.FireUpdate(); }); var volumeDisplay = displayDevice as IBasicVolumeControls; if (volumeDisplay == null) return; trilist.SetBoolSigAction(joinMap.VolumeUp.JoinNumber, volumeDisplay.VolumeUp); trilist.SetBoolSigAction(joinMap.VolumeDown.JoinNumber, volumeDisplay.VolumeDown); trilist.SetSigTrueAction(joinMap.VolumeMute.JoinNumber, volumeDisplay.MuteToggle); var volumeDisplayWithFeedback = volumeDisplay as IBasicVolumeWithFeedback; if (volumeDisplayWithFeedback == null) return; trilist.SetSigTrueAction(joinMap.VolumeMuteOn.JoinNumber, volumeDisplayWithFeedback.MuteOn); trilist.SetSigTrueAction(joinMap.VolumeMuteOff.JoinNumber, volumeDisplayWithFeedback.MuteOff); trilist.SetUShortSigAction(joinMap.VolumeLevel.JoinNumber, volumeDisplayWithFeedback.SetVolume); volumeDisplayWithFeedback.VolumeLevelFeedback.LinkInputSig(trilist.UShortInput[joinMap.VolumeLevel.JoinNumber]); volumeDisplayWithFeedback.MuteFeedback.LinkInputSig(trilist.BooleanInput[joinMap.VolumeMute.JoinNumber]); volumeDisplayWithFeedback.MuteFeedback.LinkInputSig(trilist.BooleanInput[joinMap.VolumeMuteOn.JoinNumber]); volumeDisplayWithFeedback.MuteFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.VolumeMuteOff.JoinNumber]); } /// public virtual void SetCurrentSource(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) { foreach (eRoutingSignalType type in Enum.GetValues(typeof(eRoutingSignalType))) { var flagValue = Convert.ToInt32(type); // Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag). // (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set. if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0) { this.LogDebug("Skipping {type}", type); continue; } this.LogDebug("setting {type}", type); if (signalType.HasFlag(type)) { UpdateCurrentSources(type, sourceListKey, sourceListItem); } } // Raise the CurrentSourcesChanged event CurrentSourcesChanged?.Invoke(this, EventArgs.Empty); } private void UpdateCurrentSources(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) { if (CurrentSources.ContainsKey(signalType)) { CurrentSources[signalType] = sourceListItem; } else { CurrentSources.Add(signalType, sourceListItem); } // Update the current source key for the specified signal type if (CurrentSourceKeys.ContainsKey(signalType)) { CurrentSourceKeys[signalType] = sourceListKey; } else { CurrentSourceKeys.Add(signalType, sourceListKey); } } } /// /// Abstract base class for two-way display devices that provide feedback capabilities. /// Extends DisplayBase with routing feedback and power control feedback functionality. /// public abstract class TwoWayDisplayBase : DisplayBase, IRoutingFeedback, IHasPowerControlWithFeedback { /// /// Gets feedback for the current input selection on the display. /// public StringFeedback CurrentInputFeedback { get; private set; } /// /// Abstract function that must be implemented by derived classes to provide the current input feedback value. /// Must be implemented by concrete sub-classes. /// abstract protected Func CurrentInputFeedbackFunc { get; } /// /// Gets feedback indicating whether the display is currently powered on. /// public BoolFeedback PowerIsOnFeedback { get; protected set; } /// /// Abstract function that must be implemented by derived classes to provide the power state feedback value. /// Must be implemented by concrete sub-classes. /// abstract protected Func PowerIsOnFeedbackFunc { get; } /// /// Gets the default mock display instance for testing and development purposes. /// public static MockDisplay DefaultDisplay { get { if (_DefaultDisplay == null) _DefaultDisplay = new MockDisplay("default", "Default Display"); return _DefaultDisplay; } } static MockDisplay _DefaultDisplay; /// /// Initializes a new instance of the TwoWayDisplayBase class. /// /// The unique key identifier for this display device. /// The friendly name for this display device. public TwoWayDisplayBase(string key, string name) : base(key, name) { CurrentInputFeedback = new StringFeedback(CurrentInputFeedbackFunc); WarmupTime = 7000; CooldownTime = 15000; PowerIsOnFeedback = new BoolFeedback("PowerOnFeedback", PowerIsOnFeedbackFunc); Feedbacks.Add(CurrentInputFeedback); Feedbacks.Add(PowerIsOnFeedback); PowerIsOnFeedback.OutputChange += PowerIsOnFeedback_OutputChange; } void PowerIsOnFeedback_OutputChange(object sender, EventArgs e) { if (UsageTracker != null) { if (PowerIsOnFeedback.BoolValue) UsageTracker.StartDeviceUsage(); else UsageTracker.EndDeviceUsage(); } } /// /// Event that is raised when a numeric switch change occurs on the display. /// public event EventHandler NumericSwitchChange; /// /// Raise an event when the status of a switch object changes. /// /// Arguments defined as IKeyName sender, output, input, and eRoutingSignalType protected void OnSwitchChange(RoutingNumericEventArgs e) { var newEvent = NumericSwitchChange; if (newEvent != null) newEvent(this, e); } } }