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, IHasFeedback { 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; handler?.Invoke(_CurrentSourceInfo, ChangeType.WillChange); _CurrentSourceInfo = value; handler?.Invoke(_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); Feedbacks.Add(IsCoolingDownFeedback); Feedbacks.Add(IsWarmingUpFeedback); 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; private set; } = new FeedbackCollection(); /// /// 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) { this.LogDebug("Linking to Trilist {ipId}", trilist.ID.ToString("X")); this.LogDebug("Linking to Display: {displayName}", displayDevice.Name); trilist.StringInput[joinMap.Name.JoinNumber].StringValue = displayDevice.Name; if (displayDevice is ICommunicationMonitor commMonitor) { commMonitor.CommunicationMonitor.IsOnlineFeedback.LinkInputSig(trilist.BooleanInput[joinMap.IsOnline.JoinNumber]); } // TODO: revisit this as there could be issues with this approach var inputNumber = 0; var inputKeys = new List(); var inputNumberFeedback = new IntFeedback("inputNumber", () => inputNumber); // Add input number feedback to the device feedback collection to keep it around... Feedbacks.Add(inputNumberFeedback); // Two way feedbacks if (displayDevice is TwoWayDisplayBase twoWayDisplay) { trilist.SetBool(joinMap.IsTwoWayDisplay.JoinNumber, true); twoWayDisplay.CurrentInputFeedback.OutputChange += (o, a) => this.LogDebug("CurrentInputFeedback_OutputChange {input}", a.StringValue); inputNumberFeedback.LinkInputSig(trilist.UShortInput[joinMap.InputSelect.JoinNumber]); twoWayDisplay.PowerIsOnFeedback.OutputChange += (o, a) => { if (!a.BoolValue) { inputNumber = 102; inputNumberFeedback.FireUpdate(); } else { inputNumber = 0; inputNumberFeedback.FireUpdate(); } }; twoWayDisplay.PowerIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.PowerOff.JoinNumber]); twoWayDisplay.PowerIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.PowerOn.JoinNumber]); } // Power Off trilist.SetSigTrueAction(joinMap.PowerOff.JoinNumber, () => { inputNumber = 102; inputNumberFeedback.FireUpdate(); displayDevice.PowerOff(); }); // PowerOn trilist.SetSigTrueAction(joinMap.PowerOn.JoinNumber, () => { inputNumber = 0; inputNumberFeedback.FireUpdate(); displayDevice.PowerOn(); }); for (int i = 0; i < displayDevice.InputPorts.Count; i++) { var localindex = i; if (localindex >= joinMap.InputNamesOffset.JoinSpan) { this.LogWarning("Device has {inputCount} inputs. The Join Map allows up to {joinSpan} inputs. Discarding inputs {discardStart} - {discardEnd} from bridge.", displayDevice.InputPorts.Count, joinMap.InputNamesOffset.JoinSpan, localindex + 1, displayDevice.InputPorts.Count); continue; } else { inputKeys.Add(displayDevice.InputPorts[localindex].Key); var tempKey = inputKeys.ElementAt(localindex); trilist.SetSigTrueAction((ushort)(joinMap.InputSelectOffset.JoinNumber + localindex), () => displayDevice.ExecuteSwitch(displayDevice.InputPorts[tempKey].Selector)); this.LogDebug("Setting Input Select Action on Digital Join {joinNumber} to Input: {input}", joinMap.InputSelectOffset.JoinNumber + localindex, displayDevice.InputPorts[tempKey].Key); trilist.SetString((uint)(joinMap.InputNamesOffset.JoinNumber + localindex), displayDevice.InputPorts[localindex].Key); } } this.LogDebug("Setting Input Select Action on Analog Join {inputSelectJoin}", joinMap.InputSelect); trilist.SetUShortSigAction(joinMap.InputSelect.JoinNumber, (requestedInput) => { if (requestedInput == 0) { displayDevice.PowerOff(); inputNumber = 0; return; } // using 1-based indexing for inputs coming from SIMPL, so need to check if the input is <= the count, not < if (requestedInput > 0 && requestedInput <= displayDevice.InputPorts.Count && requestedInput != inputNumber) { displayDevice.ExecuteSwitch(displayDevice.InputPorts.ElementAt(requestedInput - 1).Selector); inputNumber = requestedInput; return; } if (requestedInput == 102) { displayDevice.PowerToggle(); return; } if (displayDevice is TwoWayDisplayBase) { 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); } } } }