diff --git a/PepperDashEssentials/ControlSystem.cs b/PepperDashEssentials/ControlSystem.cs index b454cec9..be78aaf9 100644 --- a/PepperDashEssentials/ControlSystem.cs +++ b/PepperDashEssentials/ControlSystem.cs @@ -506,8 +506,11 @@ namespace PepperDash.Essentials { DeviceManager.AddDevice(room); - Debug.Console(0, Debug.ErrorLogLevel.Notice, "Room is EssentialsHuddleVtc1Room, attempting to add to DeviceManager with Fusion with IP-ID {0:X2}", fusionIpId); - DeviceManager.AddDevice(new EssentialsHuddleVtc1FusionController((IEssentialsHuddleVtc1Room)room, fusionIpId, fusionJoinMapKey)); + if (!(room is EssentialsCombinedHuddleVtc1Room)) + { + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Room is EssentialsHuddleVtc1Room, attempting to add to DeviceManager with Fusion with IP-ID {0:X2}", fusionIpId); + DeviceManager.AddDevice(new EssentialsHuddleVtc1FusionController((IEssentialsHuddleVtc1Room)room, fusionIpId, fusionJoinMapKey)); + } Debug.Console(0, Debug.ErrorLogLevel.Notice, "Attempting to build Mobile Control Bridge..."); diff --git a/PepperDashEssentials/PepperDashEssentials.csproj b/PepperDashEssentials/PepperDashEssentials.csproj index d172076a..e4a0cff6 100644 --- a/PepperDashEssentials/PepperDashEssentials.csproj +++ b/PepperDashEssentials/PepperDashEssentials.csproj @@ -144,6 +144,7 @@ + diff --git a/PepperDashEssentials/Properties/ControlSystem.cfg b/PepperDashEssentials/Properties/ControlSystem.cfg index d2c3b2c7..384bf949 100644 --- a/PepperDashEssentials/Properties/ControlSystem.cfg +++ b/PepperDashEssentials/Properties/ControlSystem.cfg @@ -1,7 +1,7 @@  - 192.168.10.1 -
auto 192.168.10.1
+ Test RMC3 +
auto 192.168.1.40;username crestron
Program01 Internal Flash
\ No newline at end of file diff --git a/PepperDashEssentials/Room/Config/EssentialsRoomConfig.cs b/PepperDashEssentials/Room/Config/EssentialsRoomConfig.cs index 9824ec67..5a2bd34f 100644 --- a/PepperDashEssentials/Room/Config/EssentialsRoomConfig.cs +++ b/PepperDashEssentials/Room/Config/EssentialsRoomConfig.cs @@ -39,6 +39,10 @@ namespace PepperDash.Essentials.Room.Config { return new EssentialsDualDisplayRoom(roomConfig); } + if (typeName == "combinedhuddlevtc1") + { + return new EssentialsCombinedHuddleVtc1Room(roomConfig); + } return typeName != "techroom" ? null : new EssentialsTechRoom(roomConfig); } @@ -201,6 +205,12 @@ namespace PepperDash.Essentials.Room.Config [JsonProperty("zeroVolumeWhenSwtichingVolumeDevices")] public bool ZeroVolumeWhenSwtichingVolumeDevices { get; set; } + /// + /// Indicates if this room represents a combination of other rooms + /// + [JsonProperty("isRoomCombinationScenario")] + public bool IsRoomCombinationScenario { get; set; } + public EssentialsRoomPropertiesConfig() { LogoLight = new EssentialsLogoPropertiesConfig(); diff --git a/PepperDashEssentials/Room/Types/EssentialsCombinedHuddleVtc1Room.cs b/PepperDashEssentials/Room/Types/EssentialsCombinedHuddleVtc1Room.cs new file mode 100644 index 00000000..9dec21f2 --- /dev/null +++ b/PepperDashEssentials/Room/Types/EssentialsCombinedHuddleVtc1Room.cs @@ -0,0 +1,821 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using Newtonsoft.Json; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Room.Config; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Devices.Common.VideoCodec; +using PepperDash.Essentials.Devices.Common.AudioCodec; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; + +namespace PepperDash.Essentials +{ + public class EssentialsCombinedHuddleVtc1Room : EssentialsRoomBase, IEssentialsHuddleVtc1Room + { + private bool _codecExternalSourceChange; + public event EventHandler CurrentVolumeDeviceChange; + public event SourceInfoChangeHandler CurrentSourceChange; + + + //************************ + // Call-related stuff + + public BoolFeedback InCallFeedback { get; private set; } + + ///// + ///// Make this more specific + ///// + //public List ActiveCalls { get; private set; } + + /// + /// States: 0 for on hook, 1 for video, 2 for audio, 3 for telekenesis + /// + public IntFeedback CallTypeFeedback { get; private set; } + + /// + /// + /// + public BoolFeedback PrivacyModeIsOnFeedback { get; private set; } + + /// + /// When something in the room is sharing with the far end or through other means + /// + public BoolFeedback IsSharingFeedback { get; private set; } + + //************************ + + protected override Func OnFeedbackFunc + { + get + { + return () => + { + + var displays = Displays.OfType().ToList(); + + var val = CurrentSourceInfo != null + && CurrentSourceInfo.Type == eSourceListItemType.Route + && displays.Count > 0; + //&& disp.PowerIsOnFeedback.BoolValue; + return val; + }; + } + } + /// + /// + /// + protected override Func IsWarmingFeedbackFunc + { + get + { + return () => Displays.OfType().Any((d) => d.IsWarmingUpFeedback.BoolValue); + } + } + /// + /// + /// + protected override Func IsCoolingFeedbackFunc + { + get + { + return () => Displays.OfType().Any((d) => d.IsCoolingDownFeedback.BoolValue); + } + } + + public EssentialsConferenceRoomPropertiesConfig PropertiesConfig { get; private set; } + + private List Displays; + + public IRoutingSinkWithSwitching DefaultDisplay { get; private set; } + + public IBasicVolumeControls DefaultAudioDevice { get; private set; } + public IBasicVolumeControls DefaultVolumeControls { get; private set; } + + public VideoCodecBase VideoCodec { get; private set; } + + public AudioCodecBase AudioCodec { get; private set; } + + public bool ExcludeFromGlobalFunctions { get; set; } + + public string DefaultSourceItem { get; set; } + + public ushort DefaultVolume { get; set; } + + /// + /// If room is off, enables power on to last source. Default true + /// + public bool EnablePowerOnToLastSource { get; set; } + string LastSourceKey; + + /// + /// Sets the volume control device, and attaches/removes InUseTrackers with "audio" + /// tag to device. + /// + public IBasicVolumeControls CurrentVolumeControls + { + get { return _CurrentAudioDevice; } + set + { + if (value == _CurrentAudioDevice) return; + + var oldDev = _CurrentAudioDevice; + // derigister this room from the device, if it can + if (oldDev is IInUseTracking) + (oldDev as IInUseTracking).InUseTracker.RemoveUser(this, "audio"); + var handler = CurrentVolumeDeviceChange; + if (handler != null) + CurrentVolumeDeviceChange(this, new VolumeDeviceChangeEventArgs(oldDev, value, ChangeType.WillChange)); + _CurrentAudioDevice = value; + if (handler != null) + CurrentVolumeDeviceChange(this, new VolumeDeviceChangeEventArgs(oldDev, value, ChangeType.DidChange)); + // register this room with new device, if it can + if (_CurrentAudioDevice is IInUseTracking) + (_CurrentAudioDevice as IInUseTracking).InUseTracker.AddUser(this, "audio"); + } + } + IBasicVolumeControls _CurrentAudioDevice; + + /// + /// The SourceListItem last run - containing names and icons + /// + public SourceListItem CurrentSourceInfo + { + get { return _CurrentSourceInfo; } + set + { + if (value == _CurrentSourceInfo) return; + + var handler = CurrentSourceChange; + // remove from in-use tracker, if so equipped + if (_CurrentSourceInfo != null && _CurrentSourceInfo.SourceDevice is IInUseTracking) + (_CurrentSourceInfo.SourceDevice as IInUseTracking).InUseTracker.RemoveUser(this, "control"); + + if (handler != null) + handler(_CurrentSourceInfo, ChangeType.WillChange); + + _CurrentSourceInfo = value; + + // add to in-use tracking + if (_CurrentSourceInfo != null && _CurrentSourceInfo.SourceDevice is IInUseTracking) + (_CurrentSourceInfo.SourceDevice as IInUseTracking).InUseTracker.AddUser(this, "control"); + if (handler != null) + handler(_CurrentSourceInfo, ChangeType.DidChange); + + var vc = VideoCodec as IHasExternalSourceSwitching; + if (vc != null && !_codecExternalSourceChange) + { + vc.SetSelectedSource(CurrentSourceInfoKey); + } + + _codecExternalSourceChange = false; + } + } + SourceListItem _CurrentSourceInfo; + + public string CurrentSourceInfoKey { get; set; } + + /// + /// "codecOsd" + /// + public string DefaultCodecRouteString { get { return "codecOsd"; } } + + /// + /// Temporary implementation. Returns the schedule-ready object or null if none. Fow now, + /// always returns the VideoCodec if it is capable + /// + public IHasScheduleAwareness ScheduleSource { get { return VideoCodec as IHasScheduleAwareness; } } + + CCriticalSection SourceSelectLock = new CCriticalSection(); + + public EssentialsCombinedHuddleVtc1Room(DeviceConfig config) + : base(config) + { + try + { + PropertiesConfig = JsonConvert.DeserializeObject + (config.Properties.ToString()); + + VideoCodec = DeviceManager.GetDeviceForKey(PropertiesConfig.VideoCodecKey) as + PepperDash.Essentials.Devices.Common.VideoCodec.VideoCodecBase; + + + if (VideoCodec == null) + throw new ArgumentNullException("codec cannot be null"); + + AudioCodec = DeviceManager.GetDeviceForKey(PropertiesConfig.AudioCodecKey) as + PepperDash.Essentials.Devices.Common.AudioCodec.AudioCodecBase; + if (AudioCodec == null) + Debug.Console(0, this, "No Audio Codec Found"); + + DefaultAudioDevice = DeviceManager.GetDeviceForKey(PropertiesConfig.DefaultAudioKey) as IBasicVolumeControls; + + Displays = new List(); + + Initialize(); + } + catch (Exception e) + { + Debug.Console(1, this, "Error building room: \n{0}", e); + } + } + + void Initialize() + { + try + { + if (DefaultAudioDevice is IBasicVolumeControls) + DefaultVolumeControls = DefaultAudioDevice as IBasicVolumeControls; + else if (DefaultAudioDevice is IHasVolumeDevice) + DefaultVolumeControls = (DefaultAudioDevice as IHasVolumeDevice).VolumeDevice; + CurrentVolumeControls = DefaultVolumeControls; + + + // Combines call feedback from both codecs if available + InCallFeedback = new BoolFeedback(() => + { + bool inAudioCall = false; + bool inVideoCall = false; + + if (AudioCodec != null) + inAudioCall = AudioCodec.IsInCall; + + if (VideoCodec != null) + inVideoCall = VideoCodec.IsInCall; + + if (inAudioCall || inVideoCall) + return true; + else + return false; + }); + + SetupDisplays(); + + // Get Microphone Privacy object, if any MUST HAPPEN AFTER setting InCallFeedback + this.MicrophonePrivacy = EssentialsRoomConfigHelper.GetMicrophonePrivacy(PropertiesConfig, this); + + Debug.Console(2, this, "Microphone Privacy Config evaluated."); + + // Get emergency object, if any + this.Emergency = EssentialsRoomConfigHelper.GetEmergency(PropertiesConfig, this); + + Debug.Console(2, this, "Emergency Config evaluated."); + + + VideoCodec.CallStatusChange += (o, a) => this.InCallFeedback.FireUpdate(); + VideoCodec.IsReadyChange += (o, a) => { this.SetCodecExternalSources(); SetCodecBranding(); }; + + if (AudioCodec != null) + AudioCodec.CallStatusChange += (o, a) => this.InCallFeedback.FireUpdate(); + + IsSharingFeedback = new BoolFeedback(() => VideoCodec.SharingContentIsOnFeedback.BoolValue); + VideoCodec.SharingContentIsOnFeedback.OutputChange += (o, a) => this.IsSharingFeedback.FireUpdate(); + + // link privacy to VC (for now?) + PrivacyModeIsOnFeedback = new BoolFeedback(() => VideoCodec.PrivacyModeIsOnFeedback.BoolValue); + VideoCodec.PrivacyModeIsOnFeedback.OutputChange += (o, a) => this.PrivacyModeIsOnFeedback.FireUpdate(); + + CallTypeFeedback = new IntFeedback(() => 0); + + SetSourceListKey(); + + EnablePowerOnToLastSource = true; + } + catch (Exception e) + { + Debug.Console(0, this, "Error Initializing Room: {0}", e); + } + } + + private void SetupDisplays() + { + //DefaultDisplay = DeviceManager.GetDeviceForKey(PropertiesConfig.DefaultDisplayKey) as IRoutingSinkWithSwitching; + + var destinationList = ConfigReader.ConfigObject.DestinationLists[PropertiesConfig.DestinationListKey]; + + foreach (var destination in destinationList) + { + var dest = destination.Value.SinkDevice as IRoutingSinkWithSwitching; + + if (dest != null) + { + Displays.Add(dest); + } + + var display = dest as DisplayBase; + if (display != null) + { + // Link power, warming, cooling to display + var dispTwoWay = display as IHasPowerControlWithFeedback; + if (dispTwoWay != null) + { + dispTwoWay.PowerIsOnFeedback.OutputChange += (o, a) => + { + if (dispTwoWay.PowerIsOnFeedback.BoolValue != OnFeedback.BoolValue) + { + //if (!dispTwoWay.PowerIsOnFeedback.BoolValue) + // CurrentSourceInfo = null; + OnFeedback.FireUpdate(); + } + if (dispTwoWay.PowerIsOnFeedback.BoolValue) + { + SetDefaultLevels(); + } + }; + } + + display.IsWarmingUpFeedback.OutputChange += (o, a) => + { + IsWarmingUpFeedback.FireUpdate(); + if (!IsWarmingUpFeedback.BoolValue) + (CurrentVolumeControls as IBasicVolumeWithFeedback).SetVolume(DefaultVolume); + }; + display.IsCoolingDownFeedback.OutputChange += (o, a) => + { + IsCoolingDownFeedback.FireUpdate(); + }; + + } + } + } + + private void SetSourceListKey() + { + if (!string.IsNullOrEmpty(PropertiesConfig.SourceListKey)) + { + SetSourceListKey(PropertiesConfig.SourceListKey); + } + else + { + SetSourceListKey(Key); + } + + SetCodecExternalSources(); + } + + protected override void CustomSetConfig(DeviceConfig config) + { + var newPropertiesConfig = JsonConvert.DeserializeObject(config.Properties.ToString()); + + if (newPropertiesConfig != null) + PropertiesConfig = newPropertiesConfig; + + ConfigWriter.UpdateRoomConfig(config); + } + + public override bool CustomActivate() + { + // Add Occupancy object from config + if (PropertiesConfig.Occupancy != null) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Setting Occupancy Provider for room"); + this.SetRoomOccupancy(DeviceManager.GetDeviceForKey(PropertiesConfig.Occupancy.DeviceKey) as + IOccupancyStatusProvider, PropertiesConfig.Occupancy.TimeoutMinutes); + } + + this.LogoUrlLightBkgnd = PropertiesConfig.LogoLight.GetLogoUrlLight(); + this.LogoUrlDarkBkgnd = PropertiesConfig.LogoDark.GetLogoUrlDark(); + + this.DefaultSourceItem = PropertiesConfig.DefaultSourceItem; + this.DefaultVolume = (ushort)(PropertiesConfig.Volumes.Master.Level * 65535 / 100); + + return base.CustomActivate(); + } + + /// + /// + /// + protected override void EndShutdown() + { + VideoCodec.EndAllCalls(); + + SetDefaultLevels(); + + RunDefaultPresentRoute(); + + CrestronEnvironment.Sleep(1000); + + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Shutting down room"); + + RunRouteAction("roomOff"); + VideoCodec.StopSharing(); + VideoCodec.StandbyActivate(); + } + + /// + /// Routes the default source item, if any. Returns true when default route exists + /// + public override bool RunDefaultPresentRoute() + { + if (DefaultSourceItem != null) + RunRouteAction(DefaultSourceItem); + + return DefaultSourceItem != null; + } + + /// + /// Sets up the room when started into call mode without presenting a source + /// + /// + public bool RunDefaultCallRoute() + { + RunRouteAction(DefaultCodecRouteString); + return true; + } + + public void RunRouteActionCodec(string routeKey, string sourceListKey) + { + _codecExternalSourceChange = true; + RunRouteAction(routeKey, sourceListKey); + } + + /// + /// + /// + /// + public void RunRouteAction(string routeKey) + { + RunRouteAction(routeKey, new Action(() => { })); + } + + /// + /// + /// + /// + /// + /// + public void RunRouteAction(string routeKey, string sourceListKey) + { + if (string.IsNullOrEmpty(sourceListKey)) + { + Debug.Console(1, this, "No sourceListKey present. RunRouteAction assumes default source list."); + RunRouteAction(routeKey, new Action(() => { })); + } + else + { + Debug.Console(1, this, "sourceListKey present but not yet implemented"); + throw new NotImplementedException(); + } + } + + /// + /// + /// + /// + /// + /// + public void RunRouteAction(string routeKey, string sourceListKey, Action successCallback) + { + if (string.IsNullOrEmpty(sourceListKey)) + { + RunRouteAction(routeKey, successCallback); + } + else + throw new NotImplementedException(); + } + + + /// + /// Gets a source from config list SourceListKey and dynamically build and executes the + /// route or commands + /// + /// + public void RunRouteAction(string routeKey, Action successCallback) + { + // Run this on a separate thread + new CTimer(o => + { + // try to prevent multiple simultaneous selections + SourceSelectLock.TryEnter(); + + try + { + + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Run route action '{0}'", routeKey); + var dict = ConfigReader.ConfigObject.GetSourceListForKey(SourceListKey); + if (dict == null) + { + Debug.Console(1, this, "WARNING: Config source list '{0}' not found", SourceListKey); + return; + } + + // Try to get the list item by it's string key + if (!dict.ContainsKey(routeKey)) + { + Debug.Console(1, this, "WARNING: No item '{0}' found on config list '{1}'", + routeKey, SourceListKey); + return; + } + + // End usage timer on last source + if (!string.IsNullOrEmpty(LastSourceKey)) + { + var usageLastSource = dict[LastSourceKey].SourceDevice as IUsageTracking; + if (usageLastSource != null && usageLastSource.UsageTracker != null) + { + try + { + // There MAY have been failures in here. Protect + usageLastSource.UsageTracker.EndDeviceUsage(); + } + catch (Exception e) + { + Debug.Console(1, this, "*#* EXCEPTION in end usage tracking:\r{0}", e); + } + } + } + + // Let's run it + var item = dict[routeKey]; + if (routeKey.ToLower() != "roomoff") + { + + LastSourceKey = routeKey; + } + else + CurrentSourceInfoKey = null; + + // hand off the individual routes to this helper + foreach (var route in item.RouteList) + DoRouteItem(route); + + // Start usage timer on routed source + var usageNewSource = item.SourceDevice as IUsageTracking; + if (usageNewSource != null && usageNewSource.UsageTracker != null) // Have to make sure there is a usage tracker! + { + (item.SourceDevice as IUsageTracking).UsageTracker.StartDeviceUsage(); + } + + // See if this can be moved into common, base-class method ------------- + + + // Set volume control, using default if non provided + IBasicVolumeControls volDev = null; + // Handle special cases for volume control + if (string.IsNullOrEmpty(item.VolumeControlKey) + || item.VolumeControlKey.Equals("$defaultAudio", StringComparison.OrdinalIgnoreCase)) + volDev = DefaultVolumeControls; + //else if (item.VolumeControlKey.Equals("$defaultDisplay", StringComparison.OrdinalIgnoreCase)) + // volDev = DefaultDisplay as IBasicVolumeControls; + // Or a specific device, probably rarely used. + else + { + var dev = DeviceManager.GetDeviceForKey(item.VolumeControlKey); + if (dev is IBasicVolumeControls) + volDev = dev as IBasicVolumeControls; + else if (dev is IHasVolumeDevice) + volDev = (dev as IHasVolumeDevice).VolumeDevice; + } + + if (volDev != CurrentVolumeControls) + { + // zero the volume on the device we are leaving. + // Set the volume to default on device we are entering + if (ZeroVolumeWhenSwtichingVolumeDevices && CurrentVolumeControls is IBasicVolumeWithFeedback) + { + var vd = CurrentVolumeControls as IBasicVolumeWithFeedback; + SavedVolumeLevels[vd] = (uint)vd.VolumeLevelFeedback.IntValue; + vd.SetVolume(0); + } + + CurrentVolumeControls = volDev; + if (ZeroVolumeWhenSwtichingVolumeDevices && CurrentVolumeControls is IBasicVolumeWithFeedback) + { + var vd = CurrentVolumeControls as IBasicVolumeWithFeedback; + ushort vol = (SavedVolumeLevels.ContainsKey(vd) ? (ushort)SavedVolumeLevels[vd] : DefaultVolume); + vd.SetVolume(vol); + } + } + // ----------------------------------------------------------------------- + + + + // store the name and UI info for routes + if (item.SourceKey == "$off") + { + CurrentSourceInfoKey = routeKey; + CurrentSourceInfo = null; + } + else if (item.SourceKey != null) + { + CurrentSourceInfoKey = routeKey; + CurrentSourceInfo = item; + } + + OnFeedback.FireUpdate(); + + if (OnFeedback.BoolValue) + { + if (VideoCodec.UsageTracker.InUseTracker.InUseFeedback.BoolValue) + { + Debug.Console(1, this, "Video Codec in use, deactivating standby on codec"); + VideoCodec.StandbyDeactivate(); + } + + if (VideoCodec.StandbyIsOnFeedback.BoolValue) + { + VideoCodec.StandbyDeactivate(); + } + else + { + Debug.Console(1, this, "Video codec not in standby. No need to wake."); + } + } + else + { + Debug.Console(1, this, "Room OnFeedback state: {0}", OnFeedback.BoolValue); + } + + // report back when done + if (successCallback != null) + successCallback(); + } + catch (Exception e) + { + Debug.Console(1, this, "ERROR in routing: {0}", e); + } + + SourceSelectLock.Leave(); + }, 0); // end of CTimer + } + + /// + /// + /// + /// + void DoRouteItem(SourceRouteListItem route) + { + // if there is a $defaultAll on route, run two separate + if (route.DestinationKey.Equals("$defaultAll", StringComparison.OrdinalIgnoreCase)) + { + foreach (var display in Displays) + { + var tempVideo = new SourceRouteListItem + { + DestinationKey = display.Key, + SourceKey = route.SourceKey, + Type = eRoutingSignalType.Video + }; + DoRoute(tempVideo); + } + } + else + DoRoute(route); + } + + /// + /// + /// + /// + /// + bool DoRoute(SourceRouteListItem route) + { + IRoutingSink dest = null; + + if (route.DestinationKey.Equals("$defaultaudio", StringComparison.OrdinalIgnoreCase)) + dest = DefaultAudioDevice as IRoutingSink; + //else if (route.DestinationKey.Equals("$defaultDisplay", StringComparison.OrdinalIgnoreCase)) + // dest = DefaultDisplay; + else + dest = DeviceManager.GetDeviceForKey(route.DestinationKey) as IRoutingSink; + + if (dest == null) + { + Debug.Console(1, this, "Cannot route, unknown destination '{0}'", route.DestinationKey); + return false; + } + + if (route.SourceKey.Equals("$off", StringComparison.OrdinalIgnoreCase)) + { + dest.ReleaseRoute(); + if (dest is IHasPowerControl) + (dest as IHasPowerControl).PowerOff(); + + } + else + { + var source = DeviceManager.GetDeviceForKey(route.SourceKey) as IRoutingOutputs; + if (source == null) + { + Debug.Console(1, this, "Cannot route unknown source '{0}' to {1}", route.SourceKey, route.DestinationKey); + return false; + } + dest.ReleaseAndMakeRoute(source, route.Type); + } + return true; + } + + public override void RoomVacatedForTimeoutPeriod(object o) + { + //Implement this + } + + /// + /// Does what it says + /// + public override void SetDefaultLevels() + { + Debug.Console(1, this, "Restoring default levels"); + var vc = CurrentVolumeControls as IBasicVolumeWithFeedback; + if (vc != null) + vc.SetVolume(DefaultVolume); + } + /// + /// Will power the room on with the last-used source + /// + public override void PowerOnToDefaultOrLastSource() + { + if (!EnablePowerOnToLastSource || LastSourceKey == null) + return; + RunRouteAction(LastSourceKey); + } + + /// + /// Runs "roomOff" action on all rooms not set to ExcludeFromGlobalFunctions + /// + public static void AllRoomsOff() + { + var allRooms = DeviceManager.AllDevices.Where(d => + d is IEssentialsRoom && !(d as IEssentialsHuddleSpaceRoom).ExcludeFromGlobalFunctions); + foreach (var room in allRooms) + (room as IEssentialsHuddleSpaceRoom).RunRouteAction("roomOff"); + } + + + /// + /// Setup the external sources for the Cisco Touch 10 devices that support IHasExternalSourceSwitch + /// + private void SetCodecExternalSources() + { + var videoCodecWithExternalSwitching = VideoCodec as IHasExternalSourceSwitching; + + if (videoCodecWithExternalSwitching == null || !videoCodecWithExternalSwitching.ExternalSourceListEnabled) + { + return; + } + + try + { + // Get the tie line that the external switcher is connected to + string codecInputConnectorName = ConfigReader.ConfigObject.TieLines.SingleOrDefault( + x => x.DestinationKey == VideoCodec.Key && x.DestinationPort == videoCodecWithExternalSwitching.ExternalSourceInputPort).DestinationPort; + + videoCodecWithExternalSwitching.ClearExternalSources(); + videoCodecWithExternalSwitching.RunRouteAction = RunRouteActionCodec; + var srcList = ConfigReader.ConfigObject.SourceLists.SingleOrDefault(x => x.Key == SourceListKey).Value.OrderBy(kv => kv.Value.Order); ; + + foreach (var kvp in srcList) + { + var srcConfig = kvp.Value; + + if (kvp.Key != DefaultCodecRouteString && kvp.Key != "roomOff") + { + videoCodecWithExternalSwitching.AddExternalSource(codecInputConnectorName, kvp.Key, srcConfig.PreferredName, PepperDash.Essentials.Devices.Common.VideoCodec.Cisco.eExternalSourceType.desktop); + videoCodecWithExternalSwitching.SetExternalSourceState(kvp.Key, PepperDash.Essentials.Devices.Common.VideoCodec.Cisco.eExternalSourceMode.Ready); + } + } + } + catch (Exception e) + { + Debug.Console(2, this, "Error setting codec external sources: {0}", e); + } + } + + private void SetCodecBranding() + { + var vcWithBranding = VideoCodec as IHasBranding; + + if (vcWithBranding == null) return; + + Debug.Console(1, this, "Setting Codec Branding"); + vcWithBranding.InitializeBranding(Key); + } + + #region IPrivacy Members + + + public void PrivacyModeOff() + { + VideoCodec.PrivacyModeOff(); + } + + public void PrivacyModeOn() + { + VideoCodec.PrivacyModeOn(); + } + + public void PrivacyModeToggle() + { + VideoCodec.PrivacyModeToggle(); + } + + #endregion + + } +} \ No newline at end of file diff --git a/PepperDashEssentials/Room/Types/EssentialsHuddleVtc1Room.cs b/PepperDashEssentials/Room/Types/EssentialsHuddleVtc1Room.cs index a6563491..f429b8c8 100644 --- a/PepperDashEssentials/Room/Types/EssentialsHuddleVtc1Room.cs +++ b/PepperDashEssentials/Room/Types/EssentialsHuddleVtc1Room.cs @@ -101,7 +101,7 @@ namespace PepperDash.Essentials } } - public EssentialsHuddleVtc1PropertiesConfig PropertiesConfig { get; private set; } + public EssentialsConferenceRoomPropertiesConfig PropertiesConfig { get; private set; } public IRoutingSinkWithSwitching DefaultDisplay { get; private set; } public IBasicVolumeControls DefaultAudioDevice { get; private set; } @@ -210,7 +210,7 @@ namespace PepperDash.Essentials { PropertiesConfig = JsonConvert.DeserializeObject (config.Properties.ToString()); - DefaultDisplay = DeviceManager.GetDeviceForKey(PropertiesConfig.DefaultDisplayKey) as IRoutingSinkWithSwitching; + DefaultDisplay = DeviceManager.GetDeviceForKey((PropertiesConfig as EssentialsHuddleVtc1PropertiesConfig).DefaultDisplayKey) as IRoutingSinkWithSwitching; VideoCodec = DeviceManager.GetDeviceForKey(PropertiesConfig.VideoCodecKey) as PepperDash.Essentials.Devices.Common.VideoCodec.VideoCodecBase; diff --git a/PepperDashEssentials/Room/Types/Interfaces/IEssentialsHuddleVtc1Room.cs b/PepperDashEssentials/Room/Types/Interfaces/IEssentialsHuddleVtc1Room.cs index 5a6e9f59..4b13145c 100644 --- a/PepperDashEssentials/Room/Types/Interfaces/IEssentialsHuddleVtc1Room.cs +++ b/PepperDashEssentials/Room/Types/Interfaces/IEssentialsHuddleVtc1Room.cs @@ -10,7 +10,9 @@ namespace PepperDash.Essentials public interface IEssentialsHuddleVtc1Room : IEssentialsRoom, IHasCurrentSourceInfoChange, IPrivacy, IHasCurrentVolumeControls, IRunRouteAction, IRunDefaultCallRoute, IHasVideoCodec, IHasAudioCodec, IHasDefaultDisplay, IHasInCallFeedback { - EssentialsHuddleVtc1PropertiesConfig PropertiesConfig { get; } + EssentialsConferenceRoomPropertiesConfig PropertiesConfig { get; } + + bool ExcludeFromGlobalFunctions { get; } void RunRouteAction(string routeKey); diff --git a/PepperDashEssentials/UI/EssentialsTouchpanelController.cs b/PepperDashEssentials/UI/EssentialsTouchpanelController.cs index 72e22b9c..441381bd 100644 --- a/PepperDashEssentials/UI/EssentialsTouchpanelController.cs +++ b/PepperDashEssentials/UI/EssentialsTouchpanelController.cs @@ -16,6 +16,8 @@ namespace PepperDash.Essentials { public class EssentialsTouchpanelController : EssentialsDevice, IHasBasicTriListWithSmartObject { + private CrestronTouchpanelPropertiesConfig _propertiesConfig; + public BasicTriListWithSmartObject Panel { get; private set; } public PanelDriverBase PanelDriver { get; private set; } @@ -27,7 +29,14 @@ namespace PepperDash.Essentials : base(key, name) { Panel = tsw; - tsw.LoadSmartObjects(sgdPath); + + if (!string.IsNullOrEmpty(sgdPath)) + Panel.LoadSmartObjects(sgdPath); + else + Debug.Console(1, this, "No SGD file path defined"); + + + tsw.SigChange += Panel_SigChange; } @@ -37,7 +46,7 @@ namespace PepperDash.Essentials Panel = dge; if (!string.IsNullOrEmpty(sgdPath)) - dge.LoadSmartObjects(sgdPath); + Panel.LoadSmartObjects(sgdPath); else Debug.Console(1, this, "No SGD file path defined"); @@ -50,6 +59,7 @@ namespace PepperDash.Essentials public EssentialsTouchpanelController(string key, string name, string type, CrestronTouchpanelPropertiesConfig props, uint id) : base(key, name) { + _propertiesConfig = props; Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Creating touchpanel hardware..."); type = type.ToLower(); @@ -61,6 +71,8 @@ namespace PepperDash.Essentials app.ParameterProjectName.Value = props.ProjectName; Panel = app; } + else if (type == "xpanel") + Panel = new XpanelForSmartGraphics(id, Global.ControlSystem); else if (type == "tsw550") Panel = new Tsw550(id, Global.ControlSystem); else if (type == "tsw552") @@ -134,10 +146,194 @@ namespace PepperDash.Essentials Panel.LoadSmartObjects(sgdName); Panel.SigChange += Panel_SigChange; + AddPostActivationAction(() => + { + // Check for IEssentialsRoomCombiner in DeviceManager and if found, subscribe to its event + var roomCombiner = DeviceManager.AllDevices.FirstOrDefault((d) => d is IEssentialsRoomCombiner) as IEssentialsRoomCombiner; + + if (roomCombiner != null) + { + // Subscribe to the even + roomCombiner.RoomCombinationScenarioChanged += new EventHandler(roomCombiner_RoomCombinationScenarioChanged); + + // Connect to the initial roomKey + if (roomCombiner.CurrentScenario != null) + { + // Use the current scenario + DetermineRoomKeyFromScenario(roomCombiner.CurrentScenario); + } + else + { + // Current Scenario not yet set. Use default + SetupPanelDrivers(_propertiesConfig.DefaultRoomKey); + } + } + else + { + // No room combiner, use the default key + SetupPanelDrivers(_propertiesConfig.DefaultRoomKey); + } + }); } + void roomCombiner_RoomCombinationScenarioChanged(object sender, EventArgs e) + { + var roomCombiner = sender as IEssentialsRoomCombiner; + + DetermineRoomKeyFromScenario(roomCombiner.CurrentScenario); + } + + /// + /// Determines the room key to use based on the scenario + /// + /// + void DetermineRoomKeyFromScenario(IRoomCombinationScenario scenario) + { + string newRoomKey = null; + + if (scenario.UiMap.ContainsKey(Key)) + { + newRoomKey = scenario.UiMap[Key]; + } + else if (scenario.UiMap.ContainsKey(_propertiesConfig.DefaultRoomKey)) + { + newRoomKey = scenario.UiMap[_propertiesConfig.DefaultRoomKey]; + } + + SetupPanelDrivers(newRoomKey); + } + + + /// + /// Sets up drivers and links them to the room specified + /// + /// key of room to link the drivers to + void SetupPanelDrivers(string roomKey) + { + // Clear out any existing actions + Panel.ClearAllSigActions(); + + Debug.Console(0, this, "Linking TP '{0}' to Room '{1}'", Key, roomKey); + + var mainDriver = new EssentialsPanelMainInterfaceDriver(Panel, _propertiesConfig); + // Then the sub drivers + + // spin up different room drivers depending on room type + var room = DeviceManager.GetDeviceForKey(roomKey); + if (room is IEssentialsHuddleSpaceRoom) + { + // Screen Saver Driver + + mainDriver.ScreenSaverController = new ScreenSaverController(mainDriver, _propertiesConfig); + + // Header Driver + Debug.Console(0, this, "Adding header driver"); + mainDriver.HeaderDriver = new EssentialsHeaderDriver(mainDriver, _propertiesConfig); + + // AV Driver + Debug.Console(0, this, "Adding huddle space AV driver"); + var avDriver = new EssentialsHuddlePanelAvFunctionsDriver(mainDriver, _propertiesConfig); + avDriver.DefaultRoomKey = roomKey; + mainDriver.AvDriver = avDriver; + avDriver.CurrentRoom = room as IEssentialsHuddleSpaceRoom; + + // Environment Driver + if (avDriver.CurrentRoom.PropertiesConfig.Environment != null && avDriver.CurrentRoom.PropertiesConfig.Environment.DeviceKeys.Count > 0) + { + Debug.Console(0, this, "Adding environment driver"); + mainDriver.EnvironmentDriver = new EssentialsEnvironmentDriver(mainDriver, _propertiesConfig); + + mainDriver.EnvironmentDriver.GetDevicesFromConfig(avDriver.CurrentRoom.PropertiesConfig.Environment); + } + + mainDriver.HeaderDriver.SetupHeaderButtons(avDriver, avDriver.CurrentRoom); + + if (Panel is TswFt5ButtonSystem) + { + var tsw = Panel as TswFt5ButtonSystem; + // Wire up hard keys + tsw.Power.UserObject = new Action(b => { if (!b) avDriver.PowerButtonPressed(); }); + if (mainDriver.EnvironmentDriver != null) + tsw.Lights.UserObject = new Action(b => + { + if (!b) + { + mainDriver.EnvironmentDriver.Toggle(); + } + }); + tsw.Up.UserObject = new Action(avDriver.VolumeUpPress); + tsw.Down.UserObject = new Action(avDriver.VolumeDownPress); + } + } + else if (room is IEssentialsHuddleVtc1Room) + { + Debug.Console(0, this, "Adding huddle space VTC AV driver"); + + // Screen Saver Driver + mainDriver.ScreenSaverController = new ScreenSaverController(mainDriver, _propertiesConfig); + + // Header Driver + mainDriver.HeaderDriver = new EssentialsHeaderDriver(mainDriver, _propertiesConfig); + + // AV Driver + var avDriver = new EssentialsHuddleVtc1PanelAvFunctionsDriver(mainDriver, _propertiesConfig); + + var codecDriver = new PepperDash.Essentials.UIDrivers.VC.EssentialsVideoCodecUiDriver(Panel, avDriver, + (room as IEssentialsHuddleVtc1Room).VideoCodec, mainDriver.HeaderDriver); + avDriver.SetVideoCodecDriver(codecDriver); + avDriver.DefaultRoomKey = roomKey; + mainDriver.AvDriver = avDriver; + avDriver.CurrentRoom = room as IEssentialsHuddleVtc1Room; + + // Environment Driver + if (avDriver.CurrentRoom.PropertiesConfig.Environment != null && avDriver.CurrentRoom.PropertiesConfig.Environment.DeviceKeys.Count > 0) + { + Debug.Console(0, this, "Adding environment driver"); + mainDriver.EnvironmentDriver = new EssentialsEnvironmentDriver(mainDriver, _propertiesConfig); + + mainDriver.EnvironmentDriver.GetDevicesFromConfig(avDriver.CurrentRoom.PropertiesConfig.Environment); + } + + mainDriver.HeaderDriver.SetupHeaderButtons(avDriver, avDriver.CurrentRoom); + + + if (Panel is TswFt5ButtonSystem) + { + var tsw = Panel as TswFt5ButtonSystem; + // Wire up hard keys + tsw.Power.UserObject = new Action(b => { if (!b) avDriver.EndMeetingPress(); }); + if (mainDriver.EnvironmentDriver != null) + tsw.Lights.UserObject = new Action(b => + { + if (!b) + { + mainDriver.EnvironmentDriver.Toggle(); + } + }); + tsw.Up.UserObject = new Action(avDriver.VolumeUpPress); + tsw.Down.UserObject = new Action(avDriver.VolumeDownPress); + } + + LoadAndShowDriver(mainDriver); + } + else + { + Debug.Console(0, this, "ERROR: Cannot load AvFunctionsDriver for room '{0}'", roomKey); + } + + } + public void LoadAndShowDriver(PanelDriverBase driver) { + if (PanelDriver != null) + { + var mainDriver = PanelDriver as EssentialsPanelMainInterfaceDriver; + if (mainDriver != null) + { + mainDriver.Dispose(); + } + } + PanelDriver = driver; driver.Show(); } @@ -148,7 +344,6 @@ namespace PepperDash.Essentials PanelDriver.BackButtonPressed(); } - void ExtenderSystemReservedSigs_DeviceExtenderSigChange(DeviceExtender currentDeviceExtender, SigEventArgs args) { // If the sig is transitioning on, mark it in case it was home button that transitioned it @@ -225,119 +420,6 @@ namespace PepperDash.Essentials var panelController = new EssentialsTouchpanelController(dc.Key, dc.Name, dc.Type, props, comm.IpIdInt); - panelController.AddPostActivationAction(() => - { - var mainDriver = new EssentialsPanelMainInterfaceDriver(panelController.Panel, props); - // Then the sub drivers - - // spin up different room drivers depending on room type - var room = DeviceManager.GetDeviceForKey(props.DefaultRoomKey); - if (room is IEssentialsHuddleSpaceRoom) - { - // Screen Saver Driver - mainDriver.ScreenSaverController = new ScreenSaverController(mainDriver, props); - - // Header Driver - Debug.Console(0, panelController, "Adding header driver"); - mainDriver.HeaderDriver = new EssentialsHeaderDriver(mainDriver, props); - - // AV Driver - Debug.Console(0, panelController, "Adding huddle space AV driver"); - var avDriver = new EssentialsHuddlePanelAvFunctionsDriver(mainDriver, props); - avDriver.DefaultRoomKey = props.DefaultRoomKey; - mainDriver.AvDriver = avDriver; - avDriver.CurrentRoom = room as IEssentialsHuddleSpaceRoom; - - // Environment Driver - if (avDriver.CurrentRoom.PropertiesConfig.Environment != null && avDriver.CurrentRoom.PropertiesConfig.Environment.DeviceKeys.Count > 0) - { - Debug.Console(0, panelController, "Adding environment driver"); - mainDriver.EnvironmentDriver = new EssentialsEnvironmentDriver(mainDriver, props); - - mainDriver.EnvironmentDriver.GetDevicesFromConfig(avDriver.CurrentRoom.PropertiesConfig.Environment); - } - - mainDriver.HeaderDriver.SetupHeaderButtons(avDriver, avDriver.CurrentRoom); - - panelController.LoadAndShowDriver(mainDriver); // This is a little convoluted. - - if (panelController.Panel is TswFt5ButtonSystem) - { - var tsw = panelController.Panel as TswFt5ButtonSystem; - // Wire up hard keys - tsw.Power.UserObject = new Action(b => { if (!b) avDriver.PowerButtonPressed(); }); - //tsw.Home.UserObject = new Action(b => { if (!b) HomePressed(); }); - if (mainDriver.EnvironmentDriver != null) - tsw.Lights.UserObject = new Action(b => - { - if (!b) - { - //mainDriver.AvDriver.PopupInterlock.ShowInterlockedWithToggle(mainDriver.EnvironmentDriver.BackgroundSubpageJoin); - mainDriver.EnvironmentDriver.Toggle(); - } - }); - tsw.Up.UserObject = new Action(avDriver.VolumeUpPress); - tsw.Down.UserObject = new Action(avDriver.VolumeDownPress); - } - } - else if (room is IEssentialsHuddleVtc1Room) - { - Debug.Console(0, panelController, "Adding huddle space VTC AV driver"); - - // Screen Saver Driver - mainDriver.ScreenSaverController = new ScreenSaverController(mainDriver, props); - - // Header Driver - mainDriver.HeaderDriver = new EssentialsHeaderDriver(mainDriver, props); - - // AV Driver - var avDriver = new EssentialsHuddleVtc1PanelAvFunctionsDriver(mainDriver, props); - - var codecDriver = new PepperDash.Essentials.UIDrivers.VC.EssentialsVideoCodecUiDriver(panelController.Panel, avDriver, - (room as IEssentialsHuddleVtc1Room).VideoCodec, mainDriver.HeaderDriver); - avDriver.SetVideoCodecDriver(codecDriver); - avDriver.DefaultRoomKey = props.DefaultRoomKey; - mainDriver.AvDriver = avDriver; - avDriver.CurrentRoom = room as IEssentialsHuddleVtc1Room; - - // Environment Driver - if (avDriver.CurrentRoom.PropertiesConfig.Environment != null && avDriver.CurrentRoom.PropertiesConfig.Environment.DeviceKeys.Count > 0) - { - Debug.Console(0, panelController, "Adding environment driver"); - mainDriver.EnvironmentDriver = new EssentialsEnvironmentDriver(mainDriver, props); - - mainDriver.EnvironmentDriver.GetDevicesFromConfig(avDriver.CurrentRoom.PropertiesConfig.Environment); - } - - mainDriver.HeaderDriver.SetupHeaderButtons(avDriver, avDriver.CurrentRoom); - - panelController.LoadAndShowDriver(mainDriver); // This is a little convoluted. - - if (panelController.Panel is TswFt5ButtonSystem) - { - var tsw = panelController.Panel as TswFt5ButtonSystem; - // Wire up hard keys - tsw.Power.UserObject = new Action(b => { if (!b) avDriver.EndMeetingPress(); }); - //tsw.Home.UserObject = new Action(b => { if (!b) HomePressed(); }); - if (mainDriver.EnvironmentDriver != null) - tsw.Lights.UserObject = new Action(b => - { - if (!b) - { - //mainDriver.AvDriver.PopupInterlock.ShowInterlockedWithToggle(mainDriver.EnvironmentDriver.BackgroundSubpageJoin); - mainDriver.EnvironmentDriver.Toggle(); - } - }); - tsw.Up.UserObject = new Action(avDriver.VolumeUpPress); - tsw.Down.UserObject = new Action(avDriver.VolumeDownPress); - } - } - else - { - Debug.Console(0, panelController, "ERROR: Cannot load AvFunctionsDriver for room '{0}'", props.DefaultRoomKey); - } - }); - return panelController; } } diff --git a/PepperDashEssentials/UI/SubpageReferenceListSourceItem.cs b/PepperDashEssentials/UI/SubpageReferenceListSourceItem.cs index 3e1869cc..27409007 100644 --- a/PepperDashEssentials/UI/SubpageReferenceListSourceItem.cs +++ b/PepperDashEssentials/UI/SubpageReferenceListSourceItem.cs @@ -14,6 +14,8 @@ namespace PepperDash.Essentials { public SourceListItem SourceItem { get; private set; } + private IHasCurrentSourceInfoChange _room; + public SubpageReferenceListSourceItem(uint index, SubpageReferenceList owner, SourceListItem sourceItem, Action routeAction) : base(index, owner) @@ -25,6 +27,7 @@ namespace PepperDash.Essentials public void RegisterForSourceChange(IHasCurrentSourceInfoChange room) { + _room = room; room.CurrentSourceChange -= room_CurrentSourceInfoChange; room.CurrentSourceChange += room_CurrentSourceInfoChange; } @@ -44,6 +47,9 @@ namespace PepperDash.Essentials { Owner.BoolInputSig(Index, 1).UserObject = null; Owner.StringInputSig(Index, 1).StringValue = ""; + + if(_room != null) + _room.CurrentSourceChange -= room_CurrentSourceInfoChange; } /// diff --git a/PepperDashEssentials/UIDrivers/Essentials/EssentialsPanelMainInterfaceDriver.cs b/PepperDashEssentials/UIDrivers/Essentials/EssentialsPanelMainInterfaceDriver.cs index 9b92fc41..2b787063 100644 --- a/PepperDashEssentials/UIDrivers/Essentials/EssentialsPanelMainInterfaceDriver.cs +++ b/PepperDashEssentials/UIDrivers/Essentials/EssentialsPanelMainInterfaceDriver.cs @@ -11,7 +11,7 @@ namespace PepperDash.Essentials /// /// /// - public class EssentialsPanelMainInterfaceDriver : PanelDriverBase, IHasScreenSaverController + public class EssentialsPanelMainInterfaceDriver : PanelDriverBase, IHasScreenSaverController, IDisposable { CTimer InactivityTimer; @@ -69,6 +69,35 @@ namespace PepperDash.Essentials } } + #region IDisposable Members + + public void Dispose() + { + var avDriver = AvDriver as PanelDriverBase; + if (avDriver != null) + { + avDriver.Hide(); + } + if (ScreenSaverController != null) + { + ScreenSaverController.Dispose(); + } + if (HeaderDriver != null) + { + HeaderDriver.Hide(); + } + if (EnvironmentDriver != null) + { + EnvironmentDriver.Hide(); + } + if (CurrentChildDriver != null) + { + CurrentChildDriver.Hide(); + } + } + + #endregion + void ExtenderTouchDetectionReservedSigs_DeviceExtenderSigChange(Crestron.SimplSharpPro.DeviceExtender currentDeviceExtender, Crestron.SimplSharpPro.SigEventArgs args) { @@ -130,7 +159,7 @@ namespace PepperDash.Essentials if(CurrentChildDriver != null) CurrentChildDriver.BackButtonPressed(); } - } + } public interface IHasScreenSaverController { diff --git a/PepperDashEssentials/UIDrivers/EssentialsHuddle/EssentialsHuddlePanelAvFunctionsDriver.cs b/PepperDashEssentials/UIDrivers/EssentialsHuddle/EssentialsHuddlePanelAvFunctionsDriver.cs index 78a06301..e58172de 100644 --- a/PepperDashEssentials/UIDrivers/EssentialsHuddle/EssentialsHuddlePanelAvFunctionsDriver.cs +++ b/PepperDashEssentials/UIDrivers/EssentialsHuddle/EssentialsHuddlePanelAvFunctionsDriver.cs @@ -306,7 +306,7 @@ namespace PepperDash.Essentials TriList.SetSigFalseAction(UIBoolJoin.DisplayPowerTogglePress, () => { - if (CurrentRoom != null && CurrentRoom.DefaultDisplay is IHasPowerControl) + if (CurrentRoom != null && CurrentRoom.DefaultDisplay != null && CurrentRoom.DefaultDisplay is IHasPowerControl) (CurrentRoom.DefaultDisplay as IHasPowerControl).PowerToggle(); }); diff --git a/PepperDashEssentials/UIDrivers/EssentialsHuddleVTC/EssentialsHuddleVtc1PanelAvFunctionsDriver.cs b/PepperDashEssentials/UIDrivers/EssentialsHuddleVTC/EssentialsHuddleVtc1PanelAvFunctionsDriver.cs index 26bdaee9..44091362 100644 --- a/PepperDashEssentials/UIDrivers/EssentialsHuddleVTC/EssentialsHuddleVtc1PanelAvFunctionsDriver.cs +++ b/PepperDashEssentials/UIDrivers/EssentialsHuddleVTC/EssentialsHuddleVtc1PanelAvFunctionsDriver.cs @@ -354,7 +354,7 @@ namespace PepperDash.Essentials TriList.SetSigFalseAction(UIBoolJoin.DisplayPowerTogglePress, () => { - if (CurrentRoom != null && CurrentRoom.DefaultDisplay is IHasPowerControl) + if (CurrentRoom != null && CurrentRoom.DefaultDisplay != null && CurrentRoom.DefaultDisplay is IHasPowerControl) (CurrentRoom.DefaultDisplay as IHasPowerControl).PowerToggle(); }); diff --git a/PepperDashEssentials/UIDrivers/ScreenSaverController.cs b/PepperDashEssentials/UIDrivers/ScreenSaverController.cs index e6d60d5b..0f75e2a2 100644 --- a/PepperDashEssentials/UIDrivers/ScreenSaverController.cs +++ b/PepperDashEssentials/UIDrivers/ScreenSaverController.cs @@ -12,7 +12,7 @@ namespace PepperDash.Essentials /// /// Driver responsible for controlling the screenshaver showing the client logo, MC connection information and QR Code. Moves the elements around to prevent screen burn in /// - public class ScreenSaverController : PanelDriverBase + public class ScreenSaverController : PanelDriverBase, IDisposable { /// @@ -42,7 +42,7 @@ namespace PepperDash.Essentials PositionInterlock = new JoinedSigInterlock(parent.TriList); - var cmdName = String.Format("shwscrsvr-{0}", parent.TriList.ID); + var cmdName = String.Format("shwscrsvr-{0:X2}", parent.TriList.ID); CrestronConsole.AddNewConsoleCommand((o) => Show(), cmdName, "Shows Panel Screensaver", ConsoleAccessLevelEnum.AccessOperator); @@ -51,6 +51,8 @@ namespace PepperDash.Essentials public override void Show() { + Debug.Console(2, "Showing ScreenSaverController: {0:X2}", TriList.ID); + if (_parent.AvDriver != null) { _parent.AvDriver.PopupInterlock.ShowInterlocked(UIBoolJoin.MCScreenSaverVisible); @@ -65,10 +67,11 @@ namespace PepperDash.Essentials public override void Hide() { - Debug.Console(1, "Hiding ScreenSaverController"); + Debug.Console(2, "Hiding ScreenSaverController: {0:X2}", TriList.ID); if (PositionTimer != null) { + Debug.Console(2, "Stopping PositionTimer: {0:X2}", TriList.ID); PositionTimer.Stop(); PositionTimer.Dispose(); PositionTimer = null; @@ -86,6 +89,8 @@ namespace PepperDash.Essentials void StartPositionTimer() { + Debug.Console(2, "Starting Position Timer: {0:X2}", TriList.ID); + if (PositionTimer == null) { PositionTimer = new CTimer((o) => PositionTimerExpired(), PositionTimeoutMs); @@ -117,7 +122,7 @@ namespace PepperDash.Essentials CurrentPositionIndex = 0; } - Debug.Console(1, "ScreenSaver Position Timer Expired: Setting new position: {0}", CurrentPositionIndex); + Debug.Console(2, "ScreenSaver Position Timer Expired: Setting new position: {0} ID: {1:X2}", CurrentPositionIndex, TriList.ID); } // @@ -129,9 +134,19 @@ namespace PepperDash.Essentials void ClearAllPositions() { - Debug.Console(1, "Hiding all screensaver positions"); + Debug.Console(2, "Hiding all screensaver positions: {0:X2}", TriList.ID); + PositionInterlock.HideAndClear(); } + + #region IDisposable Members + + public void Dispose() + { + Hide(); + } + + #endregion } } \ No newline at end of file diff --git a/PepperDashEssentials/UIDrivers/VC/EssentialsVideoCodecUiDriver.cs b/PepperDashEssentials/UIDrivers/VC/EssentialsVideoCodecUiDriver.cs index cc5ac265..40671238 100644 --- a/PepperDashEssentials/UIDrivers/VC/EssentialsVideoCodecUiDriver.cs +++ b/PepperDashEssentials/UIDrivers/VC/EssentialsVideoCodecUiDriver.cs @@ -632,7 +632,6 @@ namespace PepperDash.Essentials.UIDrivers.VC VCControlsInterlock.StatusChanged += new EventHandler(VCControlsInterlock_StatusChanged); - var codecOffCameras = Codec as IHasCameraOff; var supportsCameraOffMode = Codec.SupportsCameraOff; @@ -643,6 +642,7 @@ namespace PepperDash.Essentials.UIDrivers.VC if (codecAutoCameras != null && supportsAutoCameraMode) { + CameraModeList.SetItemButtonAction(1,(b) => codecAutoCameras.CameraAutoModeOn()); TriList.SmartObjects[UISmartObjectJoin.VCCameraMode].BooleanInput["Item 1 Visible"].BoolValue = true; codecAutoCameras.CameraAutoModeIsOnFeedback.LinkInputSig(CameraModeList.SmartObject.BooleanInput["Item 1 Selected"]); @@ -672,6 +672,7 @@ namespace PepperDash.Essentials.UIDrivers.VC } }; + } // Manual button always visible @@ -683,6 +684,7 @@ namespace PepperDash.Essentials.UIDrivers.VC if (codecOffCameras != null && supportsCameraOffMode) { + TriList.SmartObjects[UISmartObjectJoin.VCCameraMode].BooleanInput["Item 3 Visible"].BoolValue = true; codecOffCameras.CameraIsOffFeedback.LinkInputSig(CameraModeList.SmartObject.BooleanInput["Item 3 Selected"]); CameraModeList.SetItemButtonAction(3, (b) => codecOffCameras.CameraOff()); @@ -710,6 +712,7 @@ namespace PepperDash.Essentials.UIDrivers.VC } }; + } } diff --git a/devjson commands.json b/devjson commands.json index 62e675d1..c7ed7291 100644 --- a/devjson commands.json +++ b/devjson commands.json @@ -42,3 +42,6 @@ devjson:2 {"deviceKey":"display01Comm-com", "methodName":"SendText", "params": [ devjson:10 {"deviceKey":"dmLink-ssh", "methodName":"Connect", "params": []} +devjson:2 {"deviceKey":"roomCombiner", "methodName":"SetRoomCombinationScenario", "params": ["combined"]} + +devjson:2 {"deviceKey":"roomCombiner", "methodName":"SetRoomCombinationScenario", "params": ["divided"]} \ No newline at end of file diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IMobileControl.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IMobileControl.cs index ba9e6609..bb800b44 100644 --- a/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IMobileControl.cs +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/DeviceTypeInterfaces/IMobileControl.cs @@ -8,7 +8,7 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces /// public interface IMobileControl : IKeyed { - void CreateMobileControlRoomBridge(EssentialsRoomBase room, IMobileControl parent); + void CreateMobileControlRoomBridge(IEssentialsRoom room, IMobileControl parent); void LinkSystemMonitorToAppServer(); } diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/EssentialsRoomCombiner.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/EssentialsRoomCombiner.cs index 6d80913f..820d363f 100644 --- a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/EssentialsRoomCombiner.cs +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/EssentialsRoomCombiner.cs @@ -54,13 +54,20 @@ namespace PepperDash.Essentials.Core SetupPartitionStateProviders(); SetRooms(); + + if (isInAutoMode) + { + DetermineRoomCombinationScenario(); + } + else + { + SetRoomCombinationScenario(_propertiesConfig.defaultScenarioKey); + } }); } void CreateScenarios() { - RoomCombinationScenarios = new List(); - foreach (var scenarioConfig in _propertiesConfig.Scenarios) { var scenario = new RoomCombinationScenario(scenarioConfig); @@ -160,12 +167,26 @@ namespace PepperDash.Essentials.Core { return _currentScenario; } - set + private set { if (value != _currentScenario) { + // Deactivate the old scenario first + if (_currentScenario != null) + { + _currentScenario.Deactivate(); + } + _currentScenario = value; - Debug.Console(1, this, "Current Scenario: {0}", _currentScenario.Name); + + // Activate the new scenario + if (_currentScenario != null) + { + _currentScenario.Activate(); + + Debug.Console(1, this, "Current Scenario: {0}", _currentScenario.Name); + } + var handler = RoomCombinationScenarioChanged; if (handler != null) { @@ -220,9 +241,11 @@ namespace PepperDash.Essentials.Core // Get the scenario var scenario = RoomCombinationScenarios.FirstOrDefault((s) => s.Key.Equals(scenarioKey)); + // Set the parition states from the scenario manually if (scenario != null) { + Debug.Console(0, this, "Manually setting scenario to '{0}'", scenario.Key); foreach (var partitionState in scenario.PartitionStates) { var partition = Partitions.FirstOrDefault((p) => p.Key.Equals(partitionState.PartitionKey)); @@ -231,15 +254,25 @@ namespace PepperDash.Essentials.Core { if (partitionState.PartitionPresent) { + Debug.Console(0, this, "Manually setting state to Present for: '{0}'", partition.Key); partition.SetPartitionStatePresent(); } else { + Debug.Console(0, this, "Manually setting state to Not Present for: '{0}'", partition.Key); partition.SetPartitionStateNotPresent(); } } + else + { + Debug.Console(1, this, "Unable to find partition with key: '{0}'", partitionState.PartitionKey); + } } } + else + { + Debug.Console(1, this, "Unable to find scenario with key: '{0}'", scenarioKey); + } } #endregion diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs index 05295f42..6fb15042 100644 --- a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs @@ -30,7 +30,7 @@ namespace PepperDash.Essentials.Core /// /// The list of rooms keys that can be combined /// - [JsonProperty("roomMap")] + [JsonProperty("roomKeys")] public List RoomKeys {get; set;} /// @@ -87,6 +87,9 @@ namespace PepperDash.Essentials.Core [JsonProperty("partitionStates")] public List PartitionStates { get; set; } + /// + /// Determines which UI devices get mapped to which room in this scenario. The Key should be the key of the UI device and the Value should be the key of the room to map to + /// [JsonProperty("uiMap")] public Dictionary UiMap { get; set; } diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/IEssentialsRoomCombiner.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/IEssentialsRoomCombiner.cs index c0c8101b..e6bdd983 100644 --- a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/IEssentialsRoomCombiner.cs +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/IEssentialsRoomCombiner.cs @@ -78,6 +78,11 @@ namespace PepperDash.Essentials.Core /// void Activate(); + /// + /// Deactivates this room combination scenario + /// + void Deactivate(); + /// /// The state of the partitions that would activate this scenario /// diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/RoomCombinationScenario.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/RoomCombinationScenario.cs index a5534edc..460241a0 100644 --- a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/RoomCombinationScenario.cs +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Room/Combining/RoomCombinationScenario.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; +using PepperDash.Core; + using Newtonsoft.Json; namespace PepperDash.Essentials.Core @@ -27,9 +29,9 @@ namespace PepperDash.Essentials.Core public BoolFeedback IsActiveFeedback { get; private set; } - List activationActions; + private List activationActions; - List deactivationActions; + private List deactivationActions; public RoomCombinationScenario(RoomCombinationScenarioConfig config) { @@ -52,6 +54,8 @@ namespace PepperDash.Essentials.Core public void Activate() { + Debug.Console(1, "Activating Scenario: '{0}' with {1} action(s) defined", Name, activationActions.Count); + if (activationActions != null) { foreach (var action in activationActions) @@ -66,6 +70,8 @@ namespace PepperDash.Essentials.Core public void Deactivate() { + Debug.Console(1, "Deactivating Scenario: '{0}' with {1} action(s) defined", Name, deactivationActions.Count); + if (deactivationActions != null) { foreach (var action in deactivationActions) diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/TriListExtensions.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/TriListExtensions.cs index d9cadb24..bfbb73f0 100644 --- a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/TriListExtensions.cs +++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Touchpanels/TriListExtensions.cs @@ -206,6 +206,27 @@ namespace PepperDash.Essentials.Core return ClearSigAction(tl.StringOutput[sigNum]) as StringOutputSig; } + /// + /// Clears all actions on all sigs + /// + public static void ClearAllSigActions(this BasicTriList t1) + { + foreach (var sig in t1.BooleanOutput) + { + ClearSigAction(sig); + } + + foreach (var sig in t1.UShortOutput) + { + ClearSigAction(sig); + } + + foreach (var sig in t1.StringOutput) + { + ClearSigAction(sig); + } + } + /// /// Helper method to set the value of a bool Sig on TriList /// diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodec.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodec.cs index cf48ba1d..0a13434d 100644 --- a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodec.cs +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/CiscoCodec/CiscoSparkCodec.cs @@ -1,2198 +1,2198 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Crestron.SimplSharp; -using Crestron.SimplSharpPro.CrestronThread; -using Crestron.SimplSharpPro.DeviceSupport; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -using PepperDash.Core; -using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.Bridges; -using PepperDash.Essentials.Core.Config; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using PepperDash.Essentials.Core.Routing; -using PepperDash.Essentials.Devices.Common.Cameras; -using PepperDash.Essentials.Devices.Common.Codec; -using PepperDash.Essentials.Devices.Common.VideoCodec; -using PepperDash.Essentials.Core.Queues; - -namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco -{ - enum eCommandType { SessionStart, SessionEnd, Command, GetStatus, GetConfiguration }; - public enum eExternalSourceType {camera, desktop, document_camera, mediaplayer, PC, whiteboard, other} - public enum eExternalSourceMode {Ready, NotReady, Hidden, Error} - - public class CiscoSparkCodec : VideoCodecBase, IHasCallHistory, IHasCallFavorites, IHasDirectory, - IHasScheduleAwareness, IOccupancyStatusProvider, IHasCodecLayouts, IHasCodecSelfView, - ICommunicationMonitor, IRouting, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IHasExternalSourceSwitching, IHasBranding, IHasCameraOff, IHasCameraMute - { - private bool _externalSourceChangeRequested; - - public event EventHandler DirectoryResultReturned; - - private CTimer _brandingTimer; - - public CommunicationGather PortGather { get; private set; } - - public StatusMonitorBase CommunicationMonitor { get; private set; } - - private GenericQueue ReceiveQueue; - - public BoolFeedback PresentationViewMaximizedFeedback { get; private set; } - - string CurrentPresentationView; - - public BoolFeedback RoomIsOccupiedFeedback { get; private set; } - - public IntFeedback PeopleCountFeedback { get; private set; } - - public BoolFeedback CameraAutoModeIsOnFeedback { get; private set; } - - public BoolFeedback SelfviewIsOnFeedback { get; private set; } - - public StringFeedback SelfviewPipPositionFeedback { get; private set; } - - public StringFeedback LocalLayoutFeedback { get; private set; } - - public BoolFeedback LocalLayoutIsProminentFeedback { get; private set; } - - public BoolFeedback FarEndIsSharingContentFeedback { get; private set; } - - private CodecCommandWithLabel CurrentSelfviewPipPosition; - - private CodecCommandWithLabel CurrentLocalLayout; - - /// - /// List the available positions for the selfview PIP window - /// - public List SelfviewPipPositions = new List() - { - new CodecCommandWithLabel("CenterLeft", "Center Left"), - new CodecCommandWithLabel("CenterRight", "Center Right"), - new CodecCommandWithLabel("LowerLeft", "Lower Left"), - new CodecCommandWithLabel("LowerRight", "Lower Right"), - new CodecCommandWithLabel("UpperCenter", "Upper Center"), - new CodecCommandWithLabel("UpperLeft", "Upper Left"), - new CodecCommandWithLabel("UpperRight", "Upper Right"), - }; - - /// - /// Lists the available options for local layout - /// - public List LocalLayouts = new List() - { - //new CodecCommandWithLabel("auto", "Auto"), - //new CiscoCodecLocalLayout("custom", "Custom"), // Left out for now - new CodecCommandWithLabel("equal","Equal"), - new CodecCommandWithLabel("overlay","Overlay"), - new CodecCommandWithLabel("prominent","Prominent"), - new CodecCommandWithLabel("single","Single") - }; - - private CiscoCodecConfiguration.RootObject CodecConfiguration = new CiscoCodecConfiguration.RootObject(); - - private CiscoCodecStatus.RootObject CodecStatus = new CiscoCodecStatus.RootObject(); - - public CodecCallHistory CallHistory { get; private set; } - - public CodecCallFavorites CallFavorites { get; private set; } - - /// - /// The root level of the directory - /// - public CodecDirectory DirectoryRoot { get; private set; } - - /// - /// Represents the current state of the directory and is computed on get - /// - public CodecDirectory CurrentDirectoryResult - { - get - { - if (DirectoryBrowseHistory.Count > 0) - return DirectoryBrowseHistory[DirectoryBrowseHistory.Count - 1]; - else - return DirectoryRoot; - } - } - - public BoolFeedback CurrentDirectoryResultIsNotDirectoryRoot { get; private set; } - - /// - /// Tracks the directory browse history when browsing beyond the root directory - /// - public List DirectoryBrowseHistory { get; private set; } - - public CodecScheduleAwareness CodecSchedule { get; private set; } - - /// - /// Gets and returns the scaled volume of the codec - /// - protected override Func VolumeLevelFeedbackFunc - { - get - { - return () => CrestronEnvironment.ScaleWithLimits(CodecStatus.Status.Audio.Volume.IntValue, 100, 0, 65535, 0); - } - } - - protected override Func PrivacyModeIsOnFeedbackFunc - { - get - { - return () => CodecStatus.Status.Audio.Microphones.Mute.BoolValue; - } - } - - protected override Func StandbyIsOnFeedbackFunc - { - get - { - return () => CodecStatus.Status.Standby.State.BoolValue; - } - } - - /// - /// Gets the value of the currently shared source, or returns null - /// - protected override Func SharingSourceFeedbackFunc - { - get - { - return () => PresentationSourceKey; - } - } - - protected override Func SharingContentIsOnFeedbackFunc - { - get - { - return () => CodecStatus.Status.Conference.Presentation.Mode.BoolValue; - } - } - - protected Func FarEndIsSharingContentFeedbackFunc - { - get - { - return () => CodecStatus.Status.Conference.Presentation.Mode.Value == "Receiving"; - } - } - - protected override Func MuteFeedbackFunc - { - get - { - return () => CodecStatus.Status.Audio.VolumeMute.BoolValue; - } - } - - protected Func RoomIsOccupiedFeedbackFunc - { - get - { - return () => CodecStatus.Status.RoomAnalytics.PeoplePresence.BoolValue; - } - } - - protected Func PeopleCountFeedbackFunc - { - get - { - return () => CodecStatus.Status.RoomAnalytics.PeopleCount.Current.IntValue; - } - } - - protected Func SpeakerTrackIsOnFeedbackFunc - { - get - { - return () => CodecStatus.Status.Cameras.SpeakerTrack.Status.BoolValue; - } - } - - protected Func SelfViewIsOnFeedbackFunc - { - get - { - return () => CodecStatus.Status.Video.Selfview.Mode.BoolValue; - } - } - - protected Func SelfviewPipPositionFeedbackFunc - { - get - { - return () => CurrentSelfviewPipPosition.Label; - } - } - - protected Func LocalLayoutFeedbackFunc - { - get - { - return () => CurrentLocalLayout.Label; - } - } - - protected Func LocalLayoutIsProminentFeedbackFunc - { - get - { - return () => CurrentLocalLayout.Label == "Prominent"; - } - } - - - private string CliFeedbackRegistrationExpression; - - private CodecSyncState SyncState; - - public CodecPhonebookSyncState PhonebookSyncState { get; private set; } - - private StringBuilder JsonMessage; - - private bool JsonFeedbackMessageIsIncoming; - - public bool CommDebuggingIsOn; - - string Delimiter = "\r\n"; - - /// - /// Used to track the current connector used for the presentation source - /// - int PresentationSource; - - string PresentationSourceKey; - - string PhonebookMode = "Local"; // Default to Local - - uint PhonebookResultsLimit = 255; // Could be set later by config. - - CTimer LoginMessageReceivedTimer; - CTimer RetryConnectionTimer; - - // **___________________________________________________________________** - // Timers to be moved to the global system timer at a later point.... - CTimer BookingsRefreshTimer; - CTimer PhonebookRefreshTimer; - // **___________________________________________________________________** - - public RoutingInputPort CodecOsdIn { get; private set; } - public RoutingInputPort HdmiIn2 { get; private set; } - public RoutingInputPort HdmiIn3 { get; private set; } - public RoutingOutputPort HdmiOut1 { get; private set; } - public RoutingOutputPort HdmiOut2 { get; private set; } - - - // Constructor for IBasicCommunication - public CiscoSparkCodec(DeviceConfig config, IBasicCommunication comm) - : base(config) - { - var props = JsonConvert.DeserializeObject(config.Properties.ToString()); - - // Use the configured phonebook results limit if present - if (props.PhonebookResultsLimit > 0) - { - PhonebookResultsLimit = props.PhonebookResultsLimit; - } - - // The queue that will collect the repsonses in the order they are received - ReceiveQueue = new GenericQueue(this.Key + "-rxQueue", 25); - - RoomIsOccupiedFeedback = new BoolFeedback(RoomIsOccupiedFeedbackFunc); - PeopleCountFeedback = new IntFeedback(PeopleCountFeedbackFunc); - CameraAutoModeIsOnFeedback = new BoolFeedback(SpeakerTrackIsOnFeedbackFunc); - SelfviewIsOnFeedback = new BoolFeedback(SelfViewIsOnFeedbackFunc); - SelfviewPipPositionFeedback = new StringFeedback(SelfviewPipPositionFeedbackFunc); - LocalLayoutFeedback = new StringFeedback(LocalLayoutFeedbackFunc); - LocalLayoutIsProminentFeedback = new BoolFeedback(LocalLayoutIsProminentFeedbackFunc); - FarEndIsSharingContentFeedback = new BoolFeedback(FarEndIsSharingContentFeedbackFunc); - CameraIsOffFeedback = new BoolFeedback(() => CodecStatus.Status.Video.Input.MainVideoMute.BoolValue); - CameraIsMutedFeedback = CameraIsOffFeedback; - SupportsCameraOff = true; - - PresentationViewMaximizedFeedback = new BoolFeedback(() => CurrentPresentationView == "Maximized"); - - Communication = comm; - - if (props.CommunicationMonitorProperties != null) - { - CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); - } - else - { - CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 30000, 120000, 300000, "xStatus SystemUnit Software Version\r"); - } - - if (props.Sharing != null) - AutoShareContentWhileInCall = props.Sharing.AutoShareContentWhileInCall; - - ShowSelfViewByDefault = props.ShowSelfViewByDefault; - - DeviceManager.AddDevice(CommunicationMonitor); - - PhonebookMode = props.PhonebookMode; - - SyncState = new CodecSyncState(Key + "--Sync"); - - PhonebookSyncState = new CodecPhonebookSyncState(Key + "--PhonebookSync"); - - SyncState.InitialSyncCompleted += new EventHandler(SyncState_InitialSyncCompleted); - - PortGather = new CommunicationGather(Communication, Delimiter); - PortGather.IncludeDelimiter = true; - PortGather.LineReceived += this.Port_LineReceived; - - CodecInfo = new CiscoCodecInfo(CodecStatus, CodecConfiguration); - - CallHistory = new CodecCallHistory(); - - - if (props.Favorites != null) - { - CallFavorites = new CodecCallFavorites(); - CallFavorites.Favorites = props.Favorites; - } - - DirectoryRoot = new CodecDirectory(); - - DirectoryBrowseHistory = new List(); - - CurrentDirectoryResultIsNotDirectoryRoot = new BoolFeedback(() => DirectoryBrowseHistory.Count > 0); - - CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate(); - - CodecSchedule = new CodecScheduleAwareness(); - - //Set Feedback Actions - SetFeedbackActions(); - - CodecOsdIn = new RoutingInputPort(RoutingPortNames.CodecOsd, eRoutingSignalType.Audio | eRoutingSignalType.Video, - eRoutingPortConnectionType.Hdmi, new Action(StopSharing), this); - HdmiIn2 = new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.Audio | eRoutingSignalType.Video, - eRoutingPortConnectionType.Hdmi, new Action(SelectPresentationSource1), this); - HdmiIn3 = new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.Audio | eRoutingSignalType.Video, - eRoutingPortConnectionType.Hdmi, new Action(SelectPresentationSource2), this); - - HdmiOut1 = new RoutingOutputPort(RoutingPortNames.HdmiOut1, eRoutingSignalType.Audio | eRoutingSignalType.Video, - eRoutingPortConnectionType.Hdmi, null, this); - HdmiOut2 = new RoutingOutputPort(RoutingPortNames.HdmiOut2, eRoutingSignalType.Audio | eRoutingSignalType.Video, - eRoutingPortConnectionType.Hdmi, null, this); - - InputPorts.Add(CodecOsdIn); - InputPorts.Add(HdmiIn2); - InputPorts.Add(HdmiIn3); - OutputPorts.Add(HdmiOut1); - - SetUpCameras(); - - CreateOsdSource(); - - ExternalSourceListEnabled = props.ExternalSourceListEnabled; - ExternalSourceInputPort = props.ExternalSourceInputPort; - - if (props.UiBranding == null) - { - return; - } - Debug.Console(2, this, "Setting branding properties enable: {0} _brandingUrl {1}", props.UiBranding.Enable, - props.UiBranding.BrandingUrl); - - BrandingEnabled = props.UiBranding.Enable; - - _brandingUrl = props.UiBranding.BrandingUrl; - } - - private void SetFeedbackActions() - { - CodecStatus.Status.Audio.Volume.ValueChangedAction = VolumeLevelFeedback.FireUpdate; - CodecStatus.Status.Audio.VolumeMute.ValueChangedAction = MuteFeedback.FireUpdate; - CodecStatus.Status.Audio.Microphones.Mute.ValueChangedAction = PrivacyModeIsOnFeedback.FireUpdate; - CodecStatus.Status.Standby.State.ValueChangedAction = StandbyIsOnFeedback.FireUpdate; - CodecStatus.Status.RoomAnalytics.PeoplePresence.ValueChangedAction = RoomIsOccupiedFeedback.FireUpdate; - CodecStatus.Status.RoomAnalytics.PeopleCount.Current.ValueChangedAction = PeopleCountFeedback.FireUpdate; - CodecStatus.Status.Cameras.SpeakerTrack.Status.ValueChangedAction = CameraAutoModeIsOnFeedback.FireUpdate; - CodecStatus.Status.Cameras.SpeakerTrack.Availability.ValueChangedAction = () => { SupportsCameraAutoMode = CodecStatus.Status.Cameras.SpeakerTrack.Availability.BoolValue; }; - CodecStatus.Status.Video.Selfview.Mode.ValueChangedAction = SelfviewIsOnFeedback.FireUpdate; - CodecStatus.Status.Video.Selfview.PIPPosition.ValueChangedAction = ComputeSelfviewPipStatus; - CodecStatus.Status.Video.Layout.LayoutFamily.Local.ValueChangedAction = ComputeLocalLayout; - CodecStatus.Status.Conference.Presentation.Mode.ValueChangedAction = () => - { - SharingContentIsOnFeedback.FireUpdate(); - FarEndIsSharingContentFeedback.FireUpdate(); - }; - - try - { - CodecStatus.Status.Video.Input.MainVideoMute.ValueChangedAction = CameraIsOffFeedback.FireUpdate; - } - catch (Exception ex) - { - Debug.Console(0, this, "Error setting MainVideuMute Action: {0}", ex); - - if (ex.InnerException != null) - { - Debug.Console(0, this, "Error setting MainVideuMute Action: {0}", ex); - } - } - } - - /// - /// Creates the fake OSD source, and connects it's AudioVideo output to the CodecOsdIn input - /// to enable routing - /// - void CreateOsdSource() - { - OsdSource = new DummyRoutingInputsDevice(Key + "[osd]"); - DeviceManager.AddDevice(OsdSource); - var tl = new TieLine(OsdSource.AudioVideoOutputPort, CodecOsdIn); - TieLineCollection.Default.Add(tl); - } - - public void InitializeBranding(string roomKey) - { - Debug.Console(1, this, "Initializing Branding for room {0}", roomKey); - - if (!BrandingEnabled) - { - return; - } - - var mcBridgeKey = String.Format("mobileControlBridge-{0}", roomKey); - - var mcBridge = DeviceManager.GetDeviceForKey(mcBridgeKey) as IMobileControlRoomBridge; - - if (!String.IsNullOrEmpty(_brandingUrl)) - { - Debug.Console(1, this, "Branding URL found: {0}", _brandingUrl); - if (_brandingTimer != null) - { - _brandingTimer.Stop(); - _brandingTimer.Dispose(); - } - - _brandingTimer = new CTimer((o) => - { - if (_sendMcUrl) - { - SendMcBrandingUrl(mcBridge); - _sendMcUrl = false; - } - else - { - SendBrandingUrl(); - _sendMcUrl = true; - } - }, 0, 15000); - } else if (String.IsNullOrEmpty(_brandingUrl)) - { - Debug.Console(1, this, "No Branding URL found"); - if (mcBridge == null) return; - - Debug.Console(2, this, "Setting QR code URL: {0}", mcBridge.QrCodeUrl); - - mcBridge.UserCodeChanged += (o, a) => SendMcBrandingUrl(mcBridge); - mcBridge.UserPromptedForCode += (o, a) => DisplayUserCode(mcBridge.UserCode); - - SendMcBrandingUrl(mcBridge); - } - } - - /// - /// Displays the code for the specified duration - /// - /// Mobile Control user code - private void DisplayUserCode(string code) - { - SendText(string.Format("xcommand userinterface message alert display title:\"Mobile Control User Code:\" text:\"{0}\" duration: 30", code)); - } - - private void SendMcBrandingUrl(IMobileControlRoomBridge mcBridge) - { - if (mcBridge == null) - { - return; - } - - Debug.Console(1, this, "Sending url: {0}", mcBridge.QrCodeUrl); - - SendText("xconfiguration userinterface custommessage: \"Scan the QR code with a mobile phone to get started\""); - SendText("xconfiguration userinterface osd halfwakemessage: \"Tap the touch panel or scan the QR code with a mobile phone to get started\""); - - var checksum = !String.IsNullOrEmpty(mcBridge.QrCodeChecksum) - ? String.Format("checksum: {0} ", mcBridge.QrCodeChecksum) - : String.Empty; - - SendText(String.Format( - "xcommand userinterface branding fetch {1}type: branding url: {0}", - mcBridge.QrCodeUrl, checksum)); - SendText(String.Format( - "xcommand userinterface branding fetch {1}type: halfwakebranding url: {0}", - mcBridge.QrCodeUrl, checksum)); - } - - private void SendBrandingUrl() - { - Debug.Console(1, this, "Sending url: {0}", _brandingUrl); - - SendText(String.Format("xcommand userinterface branding fetch type: branding url: {0}", - _brandingUrl)); - SendText(String.Format("xcommand userinterface branding fetch type: halfwakebranding url: {0}", - _brandingUrl)); - } - /// - /// Starts the HTTP feedback server and syncronizes state of codec - /// - /// - public override bool CustomActivate() - { - CrestronConsole.AddNewConsoleCommand(SetCommDebug, "SetCodecCommDebug", "0 for Off, 1 for on", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(GetPhonebook, "GetCodecPhonebook", "Triggers a refresh of the codec phonebook", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(GetBookings, "GetCodecBookings", "Triggers a refresh of the booking data for today", ConsoleAccessLevelEnum.AccessOperator); - - return base.CustomActivate(); - } - - #region Overrides of Device - - public override void Initialize() - { - var socket = Communication as ISocketStatus; - if (socket != null) - { - socket.ConnectionChange += new EventHandler(socket_ConnectionChange); - } - - Communication.Connect(); - - CommunicationMonitor.Start(); - - const string prefix = "xFeedback register "; - - CliFeedbackRegistrationExpression = - prefix + "/Configuration" + Delimiter + - prefix + "/Status/Audio" + Delimiter + - prefix + "/Status/Call" + Delimiter + - prefix + "/Status/Conference/Presentation" + Delimiter + - prefix + "/Status/Cameras/SpeakerTrack" + Delimiter + - prefix + "/Status/RoomAnalytics" + Delimiter + - prefix + "/Status/RoomPreset" + Delimiter + - prefix + "/Status/Standby" + Delimiter + - prefix + "/Status/Video/Selfview" + Delimiter + - prefix + "/Status/Video/Layout" + Delimiter + - prefix + "/Status/Video/Input/MainVideoMute" + Delimiter + - prefix + "/Bookings" + Delimiter + - prefix + "/Event/CallDisconnect" + Delimiter + - prefix + "/Event/Bookings" + Delimiter + - prefix + "/Event/CameraPresetListUpdated" + Delimiter + - prefix + "/Event/UserInterface/Presentation/ExternalSource/Selected/SourceIdentifier" + Delimiter; - } - - #endregion - - /// - /// Fires when initial codec sync is completed. Used to then send commands to get call history, phonebook, bookings, etc. - /// - /// - /// - void SyncState_InitialSyncCompleted(object sender, EventArgs e) - { - // Fire the ready event - SetIsReady(); - //CommDebuggingIsOn = false; - - GetCallHistory(); - - PhonebookRefreshTimer = new CTimer(CheckCurrentHour, 3600000, 3600000); // check each hour to see if the phonebook should be downloaded - GetPhonebook(null); - - BookingsRefreshTimer = new CTimer(GetBookings, 900000, 900000); // 15 minute timer to check for new booking info - GetBookings(null); - } - - public void SetCommDebug(string s) - { - if (s == "1") - { - CommDebuggingIsOn = true; - Debug.Console(0, this, "Comm Debug Enabled."); - } - else - { - CommDebuggingIsOn = false; - Debug.Console(0, this, "Comm Debug Disabled."); - } - } - - void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) - { - Debug.Console(1, this, "Socket status change {0}", e.Client.ClientStatus); - if (e.Client.IsConnected) - { - if(!SyncState.LoginMessageWasReceived) - LoginMessageReceivedTimer = new CTimer(o => DisconnectClientAndReconnect(), 5000); - } - else - { - SyncState.CodecDisconnected(); - PhonebookSyncState.CodecDisconnected(); - - if (PhonebookRefreshTimer != null) - { - PhonebookRefreshTimer.Stop(); - PhonebookRefreshTimer = null; - } - - if (BookingsRefreshTimer != null) - { - BookingsRefreshTimer.Stop(); - BookingsRefreshTimer = null; - } - } - } - - void DisconnectClientAndReconnect() - { - Debug.Console(1, this, "Retrying connection to codec."); - - Communication.Disconnect(); - - RetryConnectionTimer = new CTimer(o => Communication.Connect(), 2000); - - //CrestronEnvironment.Sleep(2000); - - //Communication.Connect(); - } - - /// - /// Gathers responses from the codec (including the delimiter. Responses are checked to see if they contain JSON data and if so, the data is collected until a complete JSON - /// message is received before forwarding the message to be deserialized. - /// - /// - /// - void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) - { - if (CommDebuggingIsOn) - { - if (!JsonFeedbackMessageIsIncoming) - Debug.Console(1, this, "RX: '{0}'", args.Text); - } - - if (args.Text == "{" + Delimiter) // Check for the beginning of a new JSON message - { - JsonFeedbackMessageIsIncoming = true; - - if (CommDebuggingIsOn) - Debug.Console(1, this, "Incoming JSON message..."); - - JsonMessage = new StringBuilder(); - } - else if (args.Text == "}" + Delimiter) // Check for the end of a JSON message - { - JsonFeedbackMessageIsIncoming = false; - - JsonMessage.Append(args.Text); - - if (CommDebuggingIsOn) - Debug.Console(1, this, "Complete JSON Received:\n{0}", JsonMessage.ToString()); - - // Enqueue the complete message to be deserialized - - ReceiveQueue.Enqueue(new ProcessStringMessage(JsonMessage.ToString(), DeserializeResponse)); - - return; - } - - if(JsonFeedbackMessageIsIncoming) - { - JsonMessage.Append(args.Text); - - //Debug.Console(1, this, "Building JSON:\n{0}", JsonMessage.ToString()); - return; - } - - if (!SyncState.InitialSyncComplete) - { - switch (args.Text.Trim().ToLower()) // remove the whitespace - { - case "*r login successful": - { - SyncState.LoginMessageReceived(); - - if(LoginMessageReceivedTimer != null) - LoginMessageReceivedTimer.Stop(); - - SendText("xPreferences outputmode json"); - break; - } - case "xpreferences outputmode json": - { - if (!SyncState.InitialStatusMessageWasReceived) - SendText("xStatus"); - break; - } - case "xfeedback register /event/calldisconnect": - { - SyncState.FeedbackRegistered(); - break; - } - } - } - - - } - - /// - /// Appends the delimiter and send the command to the codec - /// - /// - public void SendText(string command) - { - if (CommDebuggingIsOn) - Debug.Console(1, this, "Sending: '{0}'", command); - - Communication.SendText(command + Delimiter); - } - - void DeserializeResponse(string response) - { - try - { - //// Serializer settings. We want to ignore null values and missing members - //JsonSerializerSettings settings = new JsonSerializerSettings(); - //settings.NullValueHandling = NullValueHandling.Ignore; - //settings.MissingMemberHandling = MissingMemberHandling.Ignore; - //settings.ObjectCreationHandling = ObjectCreationHandling.Auto; - - if (response.IndexOf("\"Status\":{") > -1 || response.IndexOf("\"Status\": {") > -1) - { - // Status Message - - // Temp object so we can inpsect for call data before simply deserializing - CiscoCodecStatus.RootObject tempCodecStatus = new CiscoCodecStatus.RootObject(); - - JsonConvert.PopulateObject(response, tempCodecStatus); - - // Check to see if the message contains /Status/Conference/Presentation/LocalInstance and extract source value - var conference = tempCodecStatus.Status.Conference; - - if (conference.Presentation.LocalInstance.Count > 0) - { - if (!string.IsNullOrEmpty(conference.Presentation.LocalInstance[0].ghost)) - PresentationSource = 0; - else if (conference.Presentation.LocalInstance[0].Source != null) - { - PresentationSource = conference.Presentation.LocalInstance[0].Source.IntValue; - } - } - - // Check to see if this is a call status message received after the initial status message - if (tempCodecStatus.Status.Call.Count > 0) - { - // Iterate through the call objects in the response - foreach (CiscoCodecStatus.Call call in tempCodecStatus.Status.Call) - { - var tempActiveCall = ActiveCalls.FirstOrDefault(c => c.Id.Equals(call.id)); - - if (tempActiveCall != null) - { - bool changeDetected = false; - - eCodecCallStatus newStatus = eCodecCallStatus.Unknown; - - // Update properties of ActiveCallItem - if(call.Status != null) - if (!string.IsNullOrEmpty(call.Status.Value)) - { - tempActiveCall.Status = CodecCallStatus.ConvertToStatusEnum(call.Status.Value); - - if (newStatus == eCodecCallStatus.Connected) - GetCallHistory(); - - changeDetected = true; - } - if (call.CallType != null) - if (!string.IsNullOrEmpty(call.CallType.Value)) - { - tempActiveCall.Type = CodecCallType.ConvertToTypeEnum(call.CallType.Value); - changeDetected = true; - } - if (call.DisplayName != null) - if (!string.IsNullOrEmpty(call.DisplayName.Value)) - { - tempActiveCall.Name = call.DisplayName.Value; - changeDetected = true; - } - if (call.Direction != null) - { - if (!string.IsNullOrEmpty(call.Direction.Value)) - { - tempActiveCall.Direction = CodecCallDirection.ConvertToDirectionEnum(call.Direction.Value); - changeDetected = true; - } - } - - if (changeDetected) - { - SetSelfViewMode(); - OnCallStatusChange(tempActiveCall); - ListCalls(); - } - } - else if( call.ghost == null ) // if the ghost value is present the call has ended already - { - // Create a new call item - var newCallItem = new CodecActiveCallItem() - { - Id = call.id, - Status = CodecCallStatus.ConvertToStatusEnum(call.Status.Value), - Name = call.DisplayName.Value, - Number = call.RemoteNumber.Value, - Type = CodecCallType.ConvertToTypeEnum(call.CallType.Value), - Direction = CodecCallDirection.ConvertToDirectionEnum(call.Direction.Value) - }; - - // Add it to the ActiveCalls List - ActiveCalls.Add(newCallItem); - - ListCalls(); - - SetSelfViewMode(); - OnCallStatusChange(newCallItem); - } - - } - - } - - // Check for Room Preset data (comes in partial, so we need to handle these responses differently to prevent appending duplicate items - var tempPresets = tempCodecStatus.Status.RoomPreset; - - if (tempPresets.Count > 0) - { - // Create temporary list to store the existing items from the CiscoCodecStatus.RoomPreset collection - List existingRoomPresets = new List(); - // Add the existing items to the temporary list - existingRoomPresets.AddRange(CodecStatus.Status.RoomPreset); - // Populate the CodecStatus object (this will append new values to the RoomPreset collection - JsonConvert.PopulateObject(response, CodecStatus); - - JObject jResponse = JObject.Parse(response); - - IList roomPresets = jResponse["Status"]["RoomPreset"].Children().ToList(); - // Iterate the new items in this response agains the temporary list. Overwrite any existing items and add new ones. - foreach (var preset in tempPresets) - { - // First fine the existing preset that matches the id - var existingPreset = existingRoomPresets.FirstOrDefault(p => p.id.Equals(preset.id)); - if (existingPreset != null) - { - Debug.Console(1, this, "Existing Room Preset with ID: {0} found. Updating.", existingPreset.id); - - JToken updatedPreset = null; - - // Find the JToken from the response with the matching id - foreach (var jPreset in roomPresets) - { - if (jPreset["id"].Value() == existingPreset.id) - updatedPreset = jPreset; - } - - if (updatedPreset != null) - { - // use PopulateObject to overlay the partial data onto the existing object - JsonConvert.PopulateObject(updatedPreset.ToString(), existingPreset); - } - - } - else - { - Debug.Console(1, this, "New Room Preset with ID: {0}. Adding.", preset.id); - existingRoomPresets.Add(preset); - } - } - - // Replace the list in the CodecStatus object with the processed list - CodecStatus.Status.RoomPreset = existingRoomPresets; - - // Generecise the list - NearEndPresets = RoomPresets.GetGenericPresets(CodecStatus.Status.RoomPreset); - - var handler = CodecRoomPresetsListHasChanged; - if (handler != null) - { - handler(this, new EventArgs()); - } - } - else - { - JsonConvert.PopulateObject(response, CodecStatus); - } - - if (!SyncState.InitialStatusMessageWasReceived) - { - SyncState.InitialStatusMessageReceived(); - - if (!SyncState.InitialConfigurationMessageWasReceived) - SendText("xConfiguration"); - } - } - else if (response.IndexOf("\"Configuration\":{") > -1 || response.IndexOf("\"Configuration\": {") > -1) - { - // Configuration Message - - JsonConvert.PopulateObject(response, CodecConfiguration); - - if (!SyncState.InitialConfigurationMessageWasReceived) - { - SyncState.InitialConfigurationMessageReceived(); - if (!SyncState.FeedbackWasRegistered) - { - SendText(CliFeedbackRegistrationExpression); - } - } - - } - else if (response.IndexOf("\"Event\":{") > -1 || response.IndexOf("\"Event\": {") > -1) - { - if (response.IndexOf("\"CallDisconnect\":{") > -1 || response.IndexOf("\"CallDisconnect\": {") > -1) - { - CiscoCodecEvents.RootObject eventReceived = new CiscoCodecEvents.RootObject(); - - JsonConvert.PopulateObject(response, eventReceived); - - EvalutateDisconnectEvent(eventReceived); - } - else if (response.IndexOf("\"Bookings\":{") > -1 || response.IndexOf("\"Bookings\": {") > -1) // The list has changed, reload it - { - GetBookings(null); - } - - else if (response.IndexOf("\"UserInterface\":{") > -1 || response.IndexOf("\"UserInterface\": {") > -1) // External Source Trigger - { - CiscoCodecEvents.RootObject eventReceived = new CiscoCodecEvents.RootObject(); - JsonConvert.PopulateObject(response, eventReceived); - Debug.Console(2, this, "*** Got an External Source Selection {0} {1}", eventReceived, eventReceived.Event.UserInterface, eventReceived.Event.UserInterface.Presentation.ExternalSource.Selected.SourceIdentifier.Value); - - if (RunRouteAction != null && !_externalSourceChangeRequested) - { - RunRouteAction(eventReceived.Event.UserInterface.Presentation.ExternalSource.Selected.SourceIdentifier.Value, null); - } - - _externalSourceChangeRequested = false; - } - } - else if (response.IndexOf("\"CommandResponse\":{") > -1 || response.IndexOf("\"CommandResponse\": {") > -1) - { - // CommandResponse Message - - if (response.IndexOf("\"CallHistoryRecentsResult\":{") > -1 || response.IndexOf("\"CallHistoryRecentsResult\": {") > -1) - { - var codecCallHistory = new CiscoCallHistory.RootObject(); - - JsonConvert.PopulateObject(response, codecCallHistory); - - CallHistory.ConvertCiscoCallHistoryToGeneric(codecCallHistory.CommandResponse.CallHistoryRecentsResult.Entry); - } - else if (response.IndexOf("\"CallHistoryDeleteEntryResult\":{") > -1 || response.IndexOf("\"CallHistoryDeleteEntryResult\": {") > -1) - { - GetCallHistory(); - } - else if (response.IndexOf("\"PhonebookSearchResult\":{") > -1 || response.IndexOf("\"PhonebookSearchResult\": {") > -1) - { - var codecPhonebookResponse = new CiscoCodecPhonebook.RootObject(); - - JsonConvert.PopulateObject(response, codecPhonebookResponse); - - if (!PhonebookSyncState.InitialPhonebookFoldersWasReceived) - { - // Check if the phonebook has any folders - PhonebookSyncState.InitialPhonebookFoldersReceived(); - - PhonebookSyncState.SetPhonebookHasFolders(codecPhonebookResponse.CommandResponse.PhonebookSearchResult.Folder.Count > 0); - - if (PhonebookSyncState.PhonebookHasFolders) - { - DirectoryRoot.AddFoldersToDirectory(CiscoCodecPhonebook.GetRootFoldersFromSearchResult(codecPhonebookResponse.CommandResponse.PhonebookSearchResult)); - } - - // Get the number of contacts in the phonebook - GetPhonebookContacts(); - } - else if (!PhonebookSyncState.NumberOfContactsWasReceived) - { - // Store the total number of contacts in the phonebook - PhonebookSyncState.SetNumberOfContacts(Int32.Parse(codecPhonebookResponse.CommandResponse.PhonebookSearchResult.ResultInfo.TotalRows.Value)); - - DirectoryRoot.AddContactsToDirectory(CiscoCodecPhonebook.GetRootContactsFromSearchResult(codecPhonebookResponse.CommandResponse.PhonebookSearchResult)); - - PhonebookSyncState.PhonebookRootEntriesReceived(); - - PrintDirectory(DirectoryRoot); - } - else if (PhonebookSyncState.InitialSyncComplete) - { - var directoryResults = new CodecDirectory(); - - if(codecPhonebookResponse.CommandResponse.PhonebookSearchResult.ResultInfo.TotalRows.Value != "0") - directoryResults = CiscoCodecPhonebook.ConvertCiscoPhonebookToGeneric(codecPhonebookResponse.CommandResponse.PhonebookSearchResult); - - PrintDirectory(directoryResults); - - DirectoryBrowseHistory.Add(directoryResults); - - OnDirectoryResultReturned(directoryResults); - - } - } - else if (response.IndexOf("\"BookingsListResult\":{") > -1) - { - var codecBookings = new CiscoCodecBookings.RootObject(); - - JsonConvert.PopulateObject(response, codecBookings); - - if(codecBookings.CommandResponse.BookingsListResult.ResultInfo.TotalRows.Value != "0") - CodecSchedule.Meetings = CiscoCodecBookings.GetGenericMeetingsFromBookingResult(codecBookings.CommandResponse.BookingsListResult.Booking); - - BookingsRefreshTimer.Reset(900000, 900000); - } - - } - - } - catch (Exception ex) - { - Debug.Console(1, this, "Error Deserializing feedback from codec: {0}", ex); - } - } - - /// - /// Call when directory results are updated - /// - /// - void OnDirectoryResultReturned(CodecDirectory result) - { - CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate(); - - // This will return the latest results to all UIs. Multiple indendent UI Directory browsing will require a different methodology - var handler = DirectoryResultReturned; - if (handler != null) - { - handler(this, new DirectoryEventArgs() - { - Directory = result, - DirectoryIsOnRoot = !CurrentDirectoryResultIsNotDirectoryRoot.BoolValue - }); - } - - PrintDirectory(result); - } - - /// - /// Evaluates an event received from the codec - /// - /// - void EvalutateDisconnectEvent(CiscoCodecEvents.RootObject eventReceived) - { - if (eventReceived.Event.CallDisconnect != null) - { - var tempActiveCall = ActiveCalls.FirstOrDefault(c => c.Id.Equals(eventReceived.Event.CallDisconnect.CallId.Value)); - - // Remove the call from the Active calls list - if (tempActiveCall != null) - { - ActiveCalls.Remove(tempActiveCall); - - ListCalls(); - - SetSelfViewMode(); - // Notify of the call disconnection - SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, tempActiveCall); - - GetCallHistory(); - } - } - } - - /// - /// - /// - /// - public override void ExecuteSwitch(object selector) - { - (selector as Action)(); - PresentationSourceKey = selector.ToString(); - } - - /// - /// This is necessary for devices that are "routers" in the middle of the path, even though it only has one output and - /// may only have one input. - /// - public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) - { - ExecuteSwitch(inputSelector); - PresentationSourceKey = inputSelector.ToString(); - } - - - /// - /// Gets the ID of the last connected call - /// - /// - public string GetCallId() - { - string callId = null; - - if (ActiveCalls.Count > 1) - { - var lastCallIndex = ActiveCalls.Count - 1; - callId = ActiveCalls[lastCallIndex].Id; - } - else if (ActiveCalls.Count == 1) - callId = ActiveCalls[0].Id; - - return callId; - - } - - public void GetCallHistory() - { - SendText("xCommand CallHistory Recents Limit: 20 Order: OccurrenceTime"); - } - - /// - /// Required for IHasScheduleAwareness - /// - public void GetSchedule() - { - GetBookings(null); - } - - /// - /// Gets the bookings for today - /// - /// - public void GetBookings(object command) - { - Debug.Console(1, this, "Retrieving Booking Info from Codec. Current Time: {0}", DateTime.Now.ToLocalTime()); - - SendText("xCommand Bookings List Days: 1 DayOffset: 0"); - } - - /// - /// Checks to see if it is 2am (or within that hour) and triggers a download of the phonebook - /// - /// - public void CheckCurrentHour(object o) - { - if (DateTime.Now.Hour == 2) - { - Debug.Console(1, this, "Checking hour to see if phonebook should be downloaded. Current hour is {0}", DateTime.Now.Hour); - - GetPhonebook(null); - PhonebookRefreshTimer.Reset(3600000, 3600000); - } - } - - /// - /// Triggers a refresh of the codec phonebook - /// - /// Just to allow this method to be called from a console command - public void GetPhonebook(string command) - { - PhonebookSyncState.CodecDisconnected(); - - DirectoryRoot = new CodecDirectory(); - - GetPhonebookFolders(); - } - - private void GetPhonebookFolders() - { - // Get Phonebook Folders (determine local/corporate from config, and set results limit) - SendText(string.Format("xCommand Phonebook Search PhonebookType: {0} ContactType: Folder", PhonebookMode)); - } - - private void GetPhonebookContacts() - { - // Get Phonebook Folders (determine local/corporate from config, and set results limit) - SendText(string.Format("xCommand Phonebook Search PhonebookType: {0} ContactType: Contact Limit: {1}", PhonebookMode, PhonebookResultsLimit)); - } - - /// - /// Searches the codec phonebook for all contacts matching the search string - /// - /// - public void SearchDirectory(string searchString) - { - SendText(string.Format("xCommand Phonebook Search SearchString: \"{0}\" PhonebookType: {1} ContactType: Contact Limit: {2}", searchString, PhonebookMode, PhonebookResultsLimit)); - } - - /// - /// // Get contents of a specific folder in the phonebook - /// - /// - public void GetDirectoryFolderContents(string folderId) - { - SendText(string.Format("xCommand Phonebook Search FolderId: {0} PhonebookType: {1} ContactType: Any Limit: {2}", folderId, PhonebookMode, PhonebookResultsLimit)); - } - - /// - /// Sets the parent folder contents or the directory root as teh current directory and fires the event. Used to browse up a level - /// - /// - public void GetDirectoryParentFolderContents() - { - var currentDirectory = new CodecDirectory(); - - if (DirectoryBrowseHistory.Count > 0) - { - var lastItemIndex = DirectoryBrowseHistory.Count - 1; - var parentDirectoryContents = DirectoryBrowseHistory[lastItemIndex]; - - DirectoryBrowseHistory.Remove(DirectoryBrowseHistory[lastItemIndex]); - - currentDirectory = parentDirectoryContents; - - } - else - { - currentDirectory = DirectoryRoot; - } - - OnDirectoryResultReturned(currentDirectory); - } - - /// - /// Clears the session browse history and fires the event with the directory root - /// - public void SetCurrentDirectoryToRoot() - { - DirectoryBrowseHistory.Clear(); - - OnDirectoryResultReturned(DirectoryRoot); - } - - /// - /// Prints the directory to console - /// - /// - void PrintDirectory(CodecDirectory directory) - { - if (Debug.Level > 0) - { - Debug.Console(1, this, "Directory Results:\n"); - - foreach (DirectoryItem item in directory.CurrentDirectoryResults) - { - if (item is DirectoryFolder) - { - Debug.Console(1, this, "[+] {0}", item.Name); - } - else if (item is DirectoryContact) - { - Debug.Console(1, this, "{0}", item.Name); - } - } - Debug.Console(1, this, "Directory is on Root Level: {0}", !CurrentDirectoryResultIsNotDirectoryRoot.BoolValue); - } - - } - - /// - /// Simple dial method - /// - /// - public override void Dial(string number) - { - SendText(string.Format("xCommand Dial Number: \"{0}\"", number)); - } - - /// - /// Dials a specific meeting - /// - /// - public override void Dial(Meeting meeting) - { - foreach (Call c in meeting.Calls) - { - Dial(c.Number, c.Protocol, c.CallRate, c.CallType, meeting.Id); - } - } - - /// - /// Detailed dial method - /// - /// - /// - /// - /// - /// - public void Dial(string number, string protocol, string callRate, string callType, string meetingId) - { - SendText(string.Format("xCommand Dial Number: \"{0}\" Protocol: {1} CallRate: {2} CallType: {3} BookingId: {4}", number, protocol, callRate, callType, meetingId)); - } - - public override void EndCall(CodecActiveCallItem activeCall) - { - SendText(string.Format("xCommand Call Disconnect CallId: {0}", activeCall.Id)); - } - - public override void EndAllCalls() - { - foreach (CodecActiveCallItem activeCall in ActiveCalls) - { - SendText(string.Format("xCommand Call Disconnect CallId: {0}", activeCall.Id)); - } - } - - public override void AcceptCall(CodecActiveCallItem item) - { - SendText("xCommand Call Accept"); - } - - public override void RejectCall(CodecActiveCallItem item) - { - SendText("xCommand Call Reject"); - } - - public override void SendDtmf(string s) - { - SendText(string.Format("xCommand Call DTMFSend CallId: {0} DTMFString: \"{1}\"", GetCallId(), s)); - } - - public void SelectPresentationSource(int source) - { - PresentationSource = source; - - StartSharing(); - } - - /// - /// Select source 1 as the presetnation source - /// - public void SelectPresentationSource1() - { - SelectPresentationSource(2); - } - - /// - /// Select source 2 as the presetnation source - /// - public void SelectPresentationSource2() - { - SelectPresentationSource(3); - } - - /// - /// Starts presentation sharing - /// - public override void StartSharing() - { - string sendingMode = string.Empty; - - if (IsInCall) - sendingMode = "LocalRemote"; - else - sendingMode = "LocalOnly"; - - if(PresentationSource > 0) - SendText(string.Format("xCommand Presentation Start PresentationSource: {0} SendingMode: {1}", PresentationSource, sendingMode)); - } - - /// - /// Stops sharing the current presentation - /// - public override void StopSharing() - { - PresentationSource = 0; - - SendText("xCommand Presentation Stop"); - } - - public override void PrivacyModeOn() - { - SendText("xCommand Audio Microphones Mute"); - } - - public override void PrivacyModeOff() - { - SendText("xCommand Audio Microphones Unmute"); - } - - public override void PrivacyModeToggle() - { - SendText("xCommand Audio Microphones ToggleMute"); - } - - public override void MuteOff() - { - SendText("xCommand Audio Volume Unmute"); - } - - public override void MuteOn() - { - SendText("xCommand Audio Volume Mute"); - } - - public override void MuteToggle() - { - SendText("xCommand Audio Volume ToggleMute"); - } - - /// - /// Increments the voluem - /// - /// - public override void VolumeUp(bool pressRelease) - { - SendText("xCommand Audio Volume Increase"); - } - - /// - /// Decrements the volume - /// - /// - public override void VolumeDown(bool pressRelease) - { - SendText("xCommand Audio Volume Decrease"); - } - - /// - /// Scales the level and sets the codec to the specified level within its range - /// - /// level from slider (0-65535 range) - public override void SetVolume(ushort level) - { - var scaledLevel = CrestronEnvironment.ScaleWithLimits(level, 65535, 0, 100, 0); - SendText(string.Format("xCommand Audio Volume Set Level: {0}", scaledLevel)); - } - - /// - /// Recalls the default volume on the codec - /// - public void VolumeSetToDefault() - { - SendText("xCommand Audio Volume SetToDefault"); - } - - /// - /// Puts the codec in standby mode - /// - public override void StandbyActivate() - { - SendText("xCommand Standby Activate"); - } - - /// - /// Wakes the codec from standby - /// - public override void StandbyDeactivate() - { - SendText("xCommand Standby Deactivate"); - } - - public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) - { - LinkVideoCodecToApi(this, trilist, joinStart, joinMapKey, bridge); - } - - /// - /// Reboots the codec - /// - public void Reboot() - { - SendText("xCommand SystemUnit Boot Action: Restart"); - } - - /// - /// Sets SelfView Mode based on config - /// - void SetSelfViewMode() - { - if (!IsInCall) - { - SelfViewModeOff(); - } - else - { - if (ShowSelfViewByDefault) - SelfViewModeOn(); - else - SelfViewModeOff(); - } - } - - /// - /// Turns on Selfview Mode - /// - public void SelfViewModeOn() - { - SendText("xCommand Video Selfview Set Mode: On"); - } - - /// - /// Turns off Selfview Mode - /// - public void SelfViewModeOff() - { - SendText("xCommand Video Selfview Set Mode: Off"); - } - - /// - /// Toggles Selfview mode on/off - /// - public void SelfViewModeToggle() - { - string mode = string.Empty; - - if (CodecStatus.Status.Video.Selfview.Mode.BoolValue) - mode = "Off"; - else - mode = "On"; - - SendText(string.Format("xCommand Video Selfview Set Mode: {0}", mode)); - } - - /// - /// Sets a specified position for the selfview PIP window - /// - /// - public void SelfviewPipPositionSet(CodecCommandWithLabel position) - { - SendText(string.Format("xCommand Video Selfview Set Mode: On PIPPosition: {0}", position.Command)); - } - - /// - /// Toggles to the next selfview PIP position - /// - public void SelfviewPipPositionToggle() - { - if (CurrentSelfviewPipPosition != null) - { - var nextPipPositionIndex = SelfviewPipPositions.IndexOf(CurrentSelfviewPipPosition) + 1; - - if (nextPipPositionIndex >= SelfviewPipPositions.Count) // Check if we need to loop back to the first item in the list - nextPipPositionIndex = 0; - - SelfviewPipPositionSet(SelfviewPipPositions[nextPipPositionIndex]); - } - } - - /// - /// Sets a specific local layout - /// - /// - public void LocalLayoutSet(CodecCommandWithLabel layout) - { - SendText(string.Format("xCommand Video Layout LayoutFamily Set Target: local LayoutFamily: {0}", layout.Command)); - } - - /// - /// Toggles to the next local layout - /// - public void LocalLayoutToggle() - { - if(CurrentLocalLayout != null) - { - var nextLocalLayoutIndex = LocalLayouts.IndexOf(CurrentLocalLayout) + 1; - - if (nextLocalLayoutIndex >= LocalLayouts.Count) // Check if we need to loop back to the first item in the list - nextLocalLayoutIndex = 0; - - LocalLayoutSet(LocalLayouts[nextLocalLayoutIndex]); - } - } - - /// - /// Toggles between single/prominent layouts - /// - public void LocalLayoutToggleSingleProminent() - { - if (CurrentLocalLayout != null) - { - if (CurrentLocalLayout.Label != "Prominent") - LocalLayoutSet(LocalLayouts.FirstOrDefault(l => l.Label.Equals("Prominent"))); - else - LocalLayoutSet(LocalLayouts.FirstOrDefault(l => l.Label.Equals("Single"))); - } - - } - - /// - /// - /// - public void MinMaxLayoutToggle() - { - if (PresentationViewMaximizedFeedback.BoolValue) - CurrentPresentationView = "Minimized"; - else - CurrentPresentationView = "Maximized"; - - SendText(string.Format("xCommand Video PresentationView Set View: {0}", CurrentPresentationView)); - PresentationViewMaximizedFeedback.FireUpdate(); - } - - /// - /// Calculates the current selfview PIP position - /// - void ComputeSelfviewPipStatus() - { - CurrentSelfviewPipPosition = SelfviewPipPositions.FirstOrDefault(p => p.Command.ToLower().Equals(CodecStatus.Status.Video.Selfview.PIPPosition.Value.ToLower())); - - if(CurrentSelfviewPipPosition != null) - SelfviewIsOnFeedback.FireUpdate(); - } - - /// - /// Calculates the current local Layout - /// - void ComputeLocalLayout() - { - CurrentLocalLayout = LocalLayouts.FirstOrDefault(l => l.Command.ToLower().Equals(CodecStatus.Status.Video.Layout.LayoutFamily.Local.Value.ToLower())); - - if (CurrentLocalLayout != null) - LocalLayoutFeedback.FireUpdate(); - } - - public void RemoveCallHistoryEntry(CodecCallHistory.CallHistoryEntry entry) - { - SendText(string.Format("xCommand CallHistory DeleteEntry CallHistoryId: {0} AcknowledgeConsecutiveDuplicates: True", entry.OccurrenceHistoryId)); - } - - #region IHasCameraSpeakerTrack - - public void CameraAutoModeToggle() - { - if (!CameraAutoModeIsOnFeedback.BoolValue) - { - SendText("xCommand Cameras SpeakerTrack Activate"); - } - else - { - SendText("xCommand Cameras SpeakerTrack Deactivate"); - } - } - - public void CameraAutoModeOn() - { - if (CameraIsOffFeedback.BoolValue) - { - CameraMuteOff(); - } - - SendText("xCommand Cameras SpeakerTrack Activate"); - } - - public void CameraAutoModeOff() - { - if (CameraIsOffFeedback.BoolValue) - { - CameraMuteOff(); - } - - SendText("xCommand Cameras SpeakerTrack Deactivate"); - } - - #endregion - - /// - /// Builds the cameras List. Could later be modified to build from config data - /// - void SetUpCameras() - { - // Add the internal camera - Cameras = new List(); - - var internalCamera = new CiscoSparkCamera(Key + "-camera1", "Near End", this, 1); - - if(CodecStatus.Status.Cameras.Camera.Count > 0) - internalCamera.SetCapabilites(CodecStatus.Status.Cameras.Camera[0].Capabilities.Options.Value); - else - // Somehow subscribe to the event on the Options.Value property and update when it changes. - - Cameras.Add(internalCamera); - - // Add the far end camera - var farEndCamera = new CiscoFarEndCamera(Key + "-cameraFar", "Far End", this); - Cameras.Add(farEndCamera); - - SelectedCameraFeedback = new StringFeedback(() => SelectedCamera.Key); - - ControllingFarEndCameraFeedback = new BoolFeedback(() => SelectedCamera is IAmFarEndCamera); - - DeviceManager.AddDevice(internalCamera); - DeviceManager.AddDevice(farEndCamera); - - NearEndPresets = new List(15); - - FarEndRoomPresets = new List(15); - - // Add the far end presets - for (int i = 1; i <= FarEndRoomPresets.Capacity; i++) - { - var label = string.Format("Far End Preset {0}", i); - FarEndRoomPresets.Add(new CodecRoomPreset(i, label, true, false)); - } - - SelectedCamera = internalCamera; ; // call the method to select the camera and ensure the feedbacks get updated. - } - - #region IHasCodecCameras Members - - public event EventHandler CameraSelected; - - public List Cameras { get; private set; } - - public StringFeedback SelectedCameraFeedback { get; private set; } - - private CameraBase _selectedCamera; - - /// - /// Returns the selected camera - /// - public CameraBase SelectedCamera - { - get - { - return _selectedCamera; - } - private set - { - _selectedCamera = value; - SelectedCameraFeedback.FireUpdate(); - ControllingFarEndCameraFeedback.FireUpdate(); - if (CameraIsOffFeedback.BoolValue) - CameraMuteOff(); - - var handler = CameraSelected; - if (handler != null) - { - handler(this, new CameraSelectedEventArgs(SelectedCamera)); - } - } - } - - public void SelectCamera(string key) - { - var camera = Cameras.FirstOrDefault(c => c.Key.IndexOf(key, StringComparison.OrdinalIgnoreCase) > -1); - if (camera != null) - { - Debug.Console(2, this, "Selected Camera with key: '{0}'", camera.Key); - SelectedCamera = camera; - } - else - Debug.Console(2, this, "Unable to select camera with key: '{0}'", key); - } - - public CameraBase FarEndCamera { get; private set; } - - public BoolFeedback ControllingFarEndCameraFeedback { get; private set; } - - #endregion - - /// - /// - /// - public class CiscoCodecInfo : VideoCodecInfo - { - public CiscoCodecStatus.RootObject CodecStatus { get; private set; } - - public CiscoCodecConfiguration.RootObject CodecConfiguration { get; private set; } - - public override bool MultiSiteOptionIsEnabled - { - get - { - if (!string.IsNullOrEmpty(CodecStatus.Status.SystemUnit.Software.OptionKeys.MultiSite.Value) && CodecStatus.Status.SystemUnit.Software.OptionKeys.MultiSite.Value.ToLower() == "true") - return true; - else - return false; - } - - } - public override string IpAddress - { - get - { - if (CodecConfiguration.Configuration.Network != null) - { - if (CodecConfiguration.Configuration.Network.Count > 0) - return CodecConfiguration.Configuration.Network[0].IPv4.Address.Value; - } - return string.Empty; - } - } - public override string E164Alias - { - get - { - if (CodecConfiguration.Configuration.H323 != null && CodecConfiguration.Configuration.H323.H323Alias.E164 != null) - { - return CodecConfiguration.Configuration.H323.H323Alias.E164.Value; - } - else - { - return string.Empty; - } - } - } - public override string H323Id - { - get - { - if (CodecConfiguration.Configuration.H323 != null && CodecConfiguration.Configuration.H323.H323Alias != null - && CodecConfiguration.Configuration.H323.H323Alias.ID != null) - { - return CodecConfiguration.Configuration.H323.H323Alias.ID.Value; - } - else - { - return string.Empty; - } - } - } - public override string SipPhoneNumber - { - get - { - if (CodecStatus.Status.SIP != null && CodecStatus.Status.SIP.Registration.Count > 0) - { - var match = Regex.Match(CodecStatus.Status.SIP.Registration[0].URI.Value, @"(\d+)"); // extract numbers only - if (match.Success) - { - Debug.Console(1, "Extracted phone number as '{0}' from string '{1}'", match.Groups[1].Value, CodecStatus.Status.SIP.Registration[0].URI.Value); - return match.Groups[1].Value; - } - else - { - Debug.Console(1, "Unable to extract phone number from string: '{0}'", CodecStatus.Status.SIP.Registration[0].URI.Value); - return string.Empty; - } - } - else - { - Debug.Console(1, "Unable to extract phone number. No SIP Registration items found"); - return string.Empty; - } - } - } - - public override string SipUri - { - get - { - if (CodecStatus.Status.SIP != null && CodecStatus.Status.SIP.AlternateURI.Primary.URI.Value != null) - { - return CodecStatus.Status.SIP.AlternateURI.Primary.URI.Value; - } - else if (CodecStatus.Status.UserInterface != null && - CodecStatus.Status.UserInterface.ContactInfo.ContactMethod[0].Number.Value != null) - { - return CodecStatus.Status.UserInterface.ContactInfo.ContactMethod[0].Number.Value; - } - else - return string.Empty; - } - } - - public override bool AutoAnswerEnabled - { - get - { - if (CodecConfiguration.Configuration.Conference.AutoAnswer.Mode.Value.ToLower() == "on") - return true; - else - return false; - } - } - - public CiscoCodecInfo(CiscoCodecStatus.RootObject status, CiscoCodecConfiguration.RootObject configuration) - { - CodecStatus = status; - CodecConfiguration = configuration; - } - } - - - #region IHasCameraPresets Members - - public event EventHandler CodecRoomPresetsListHasChanged; - - public List NearEndPresets { get; private set; } - - public List FarEndRoomPresets { get; private set; } - - public void CodecRoomPresetSelect(int preset) - { - Debug.Console(1, this, "Selecting Preset: {0}", preset); - if (SelectedCamera is IAmFarEndCamera) - SelectFarEndPreset(preset); - else - SendText(string.Format("xCommand RoomPreset Activate PresetId: {0}", preset)); - } - - public void CodecRoomPresetStore(int preset, string description) - { - SendText(string.Format("xCommand RoomPreset Store PresetId: {0} Description: \"{1}\" Type: All", preset, description)); - } - - #endregion - - public void SelectFarEndPreset(int preset) - { - SendText(string.Format("xCommand Call FarEndControl RoomPreset Activate CallId: {0} PresetId: {1}", GetCallId(), preset)); - } - - - #region IHasExternalSourceSwitching Members - - /// - /// Wheather the Cisco supports External Source Lists or not - /// - public bool ExternalSourceListEnabled - { - get; - private set; - } - - /// - /// The name of the RoutingInputPort to which the upstream external switcher is connected - /// - public string ExternalSourceInputPort { get; private set; } - - public bool BrandingEnabled { get; private set; } - private string _brandingUrl; - private bool _sendMcUrl; - - /// - /// Adds an external source to the Cisco - /// - /// - /// - /// - public void AddExternalSource(string connectorId, string key, string name, eExternalSourceType type) - { - int id = 2; - if (connectorId.ToLower() == "hdmiin3") - { - id = 3; - } - SendText(string.Format("xCommand UserInterface Presentation ExternalSource Add ConnectorId: {0} SourceIdentifier: \"{1}\" Name: \"{2}\" Type: {3}", id, key, name, type.ToString())); - // SendText(string.Format("xCommand UserInterface Presentation ExternalSource State Set SourceIdentifier: \"{0}\" State: Ready", key)); - Debug.Console(2, this, "Adding ExternalSource {0} {1}", connectorId, name); - - } - - - /// - /// Sets the state of the External Source - /// - /// - /// - public void SetExternalSourceState(string key, eExternalSourceMode mode) - { - SendText(string.Format("xCommand UserInterface Presentation ExternalSource State Set SourceIdentifier: \"{0}\" State: {1}", key, mode.ToString())); - } - /// - /// Clears all external sources on the codec - /// - public void ClearExternalSources() - { - SendText("xCommand UserInterface Presentation ExternalSource RemoveAll"); - - } - - /// - /// Sets the selected source of the available external sources on teh Touch10 UI - /// - public void SetSelectedSource(string key) - { - SendText(string.Format("xCommand UserInterface Presentation ExternalSource Select SourceIdentifier: {0}", key)); - _externalSourceChangeRequested = true; - } - - /// - /// Action that will run when the External Source is selected. - /// - public Action RunRouteAction { private get; set; } - - - - - - - #endregion - #region ExternalDevices - - - - #endregion - - #region IHasCameraOff Members - - public BoolFeedback CameraIsOffFeedback { get; private set; } - - public void CameraOff() - { - CameraMuteOn(); - } - - #endregion - - public BoolFeedback CameraIsMutedFeedback { get; private set; } - - /// - /// Mutes the outgoing camera video - /// - public void CameraMuteOn() - { - SendText("xCommand Video Input MainVideo Mute"); - } - - /// - /// Unmutes the outgoing camera video - /// - public void CameraMuteOff() - { - SendText("xCommand Video Input MainVideo Unmute"); - } - - /// - /// Toggles the camera mute state - /// - public void CameraMuteToggle() - { - if (CameraIsMutedFeedback.BoolValue) - CameraMuteOff(); - else - CameraMuteOn(); - } - } - - - /// - /// Represents a codec command that might need to have a friendly label applied for UI feedback purposes - /// - public class CodecCommandWithLabel - { - public string Command { get; set; } - public string Label { get; set; } - - public CodecCommandWithLabel(string command, string label) - { - Command = command; - Label = label; - } - } - - /// - /// Tracks the initial sycnronization state of the codec when making a connection - /// - public class CodecSyncState : IKeyed - { - bool _InitialSyncComplete; - - public event EventHandler InitialSyncCompleted; - - public string Key { get; private set; } - - public bool InitialSyncComplete - { - get { return _InitialSyncComplete; } - private set - { - if (value == true) - { - var handler = InitialSyncCompleted; - if (handler != null) - handler(this, new EventArgs()); - } - _InitialSyncComplete = value; - } - } - - public bool LoginMessageWasReceived { get; private set; } - - public bool InitialStatusMessageWasReceived { get; private set; } - - public bool InitialConfigurationMessageWasReceived { get; private set; } - - public bool FeedbackWasRegistered { get; private set; } - - public CodecSyncState(string key) - { - Key = key; - CodecDisconnected(); - } - - public void LoginMessageReceived() - { - LoginMessageWasReceived = true; - Debug.Console(1, this, "Login Message Received."); - CheckSyncStatus(); - } - - public void InitialStatusMessageReceived() - { - InitialStatusMessageWasReceived = true; - Debug.Console(1, this, "Initial Codec Status Message Received."); - CheckSyncStatus(); - } - - public void InitialConfigurationMessageReceived() - { - InitialConfigurationMessageWasReceived = true; - Debug.Console(1, this, "Initial Codec Configuration Message Received."); - CheckSyncStatus(); - } - - public void FeedbackRegistered() - { - FeedbackWasRegistered = true; - Debug.Console(1, this, "Initial Codec Feedback Registration Successful."); - CheckSyncStatus(); - } - - public void CodecDisconnected() - { - LoginMessageWasReceived = false; - InitialConfigurationMessageWasReceived = false; - InitialStatusMessageWasReceived = false; - FeedbackWasRegistered = false; - InitialSyncComplete = false; - } - - void CheckSyncStatus() - { - if (LoginMessageWasReceived && InitialConfigurationMessageWasReceived && InitialStatusMessageWasReceived && FeedbackWasRegistered) - { - InitialSyncComplete = true; - Debug.Console(1, this, "Initial Codec Sync Complete!"); - } - else - InitialSyncComplete = false; - } - } - - public class CiscoSparkCodecFactory : EssentialsDeviceFactory - { - public CiscoSparkCodecFactory() - { - TypeNames = new List() { "ciscospark", "ciscowebex", "ciscowebexpro", "ciscoroomkit", "ciscosparkpluscodec" }; - } - - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.Console(1, "Factory Attempting to create new Cisco Codec Device"); - - var comm = CommFactory.CreateCommForDevice(dc); - return new VideoCodec.Cisco.CiscoSparkCodec(dc, comm); - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro.CrestronThread; +using Crestron.SimplSharpPro.DeviceSupport; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Bridges; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Devices.Common.VideoCodec; +using PepperDash.Essentials.Core.Queues; + +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco +{ + enum eCommandType { SessionStart, SessionEnd, Command, GetStatus, GetConfiguration }; + public enum eExternalSourceType {camera, desktop, document_camera, mediaplayer, PC, whiteboard, other} + public enum eExternalSourceMode {Ready, NotReady, Hidden, Error} + + public class CiscoSparkCodec : VideoCodecBase, IHasCallHistory, IHasCallFavorites, IHasDirectory, + IHasScheduleAwareness, IOccupancyStatusProvider, IHasCodecLayouts, IHasCodecSelfView, + ICommunicationMonitor, IRouting, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IHasExternalSourceSwitching, IHasBranding, IHasCameraOff, IHasCameraMute + { + private bool _externalSourceChangeRequested; + + public event EventHandler DirectoryResultReturned; + + private CTimer _brandingTimer; + + public CommunicationGather PortGather { get; private set; } + + public StatusMonitorBase CommunicationMonitor { get; private set; } + + private GenericQueue ReceiveQueue; + + public BoolFeedback PresentationViewMaximizedFeedback { get; private set; } + + string CurrentPresentationView; + + public BoolFeedback RoomIsOccupiedFeedback { get; private set; } + + public IntFeedback PeopleCountFeedback { get; private set; } + + public BoolFeedback CameraAutoModeIsOnFeedback { get; private set; } + + public BoolFeedback SelfviewIsOnFeedback { get; private set; } + + public StringFeedback SelfviewPipPositionFeedback { get; private set; } + + public StringFeedback LocalLayoutFeedback { get; private set; } + + public BoolFeedback LocalLayoutIsProminentFeedback { get; private set; } + + public BoolFeedback FarEndIsSharingContentFeedback { get; private set; } + + private CodecCommandWithLabel CurrentSelfviewPipPosition; + + private CodecCommandWithLabel CurrentLocalLayout; + + /// + /// List the available positions for the selfview PIP window + /// + public List SelfviewPipPositions = new List() + { + new CodecCommandWithLabel("CenterLeft", "Center Left"), + new CodecCommandWithLabel("CenterRight", "Center Right"), + new CodecCommandWithLabel("LowerLeft", "Lower Left"), + new CodecCommandWithLabel("LowerRight", "Lower Right"), + new CodecCommandWithLabel("UpperCenter", "Upper Center"), + new CodecCommandWithLabel("UpperLeft", "Upper Left"), + new CodecCommandWithLabel("UpperRight", "Upper Right"), + }; + + /// + /// Lists the available options for local layout + /// + public List LocalLayouts = new List() + { + //new CodecCommandWithLabel("auto", "Auto"), + //new CiscoCodecLocalLayout("custom", "Custom"), // Left out for now + new CodecCommandWithLabel("equal","Equal"), + new CodecCommandWithLabel("overlay","Overlay"), + new CodecCommandWithLabel("prominent","Prominent"), + new CodecCommandWithLabel("single","Single") + }; + + private CiscoCodecConfiguration.RootObject CodecConfiguration = new CiscoCodecConfiguration.RootObject(); + + private CiscoCodecStatus.RootObject CodecStatus = new CiscoCodecStatus.RootObject(); + + public CodecCallHistory CallHistory { get; private set; } + + public CodecCallFavorites CallFavorites { get; private set; } + + /// + /// The root level of the directory + /// + public CodecDirectory DirectoryRoot { get; private set; } + + /// + /// Represents the current state of the directory and is computed on get + /// + public CodecDirectory CurrentDirectoryResult + { + get + { + if (DirectoryBrowseHistory.Count > 0) + return DirectoryBrowseHistory[DirectoryBrowseHistory.Count - 1]; + else + return DirectoryRoot; + } + } + + public BoolFeedback CurrentDirectoryResultIsNotDirectoryRoot { get; private set; } + + /// + /// Tracks the directory browse history when browsing beyond the root directory + /// + public List DirectoryBrowseHistory { get; private set; } + + public CodecScheduleAwareness CodecSchedule { get; private set; } + + /// + /// Gets and returns the scaled volume of the codec + /// + protected override Func VolumeLevelFeedbackFunc + { + get + { + return () => CrestronEnvironment.ScaleWithLimits(CodecStatus.Status.Audio.Volume.IntValue, 100, 0, 65535, 0); + } + } + + protected override Func PrivacyModeIsOnFeedbackFunc + { + get + { + return () => CodecStatus.Status.Audio.Microphones.Mute.BoolValue; + } + } + + protected override Func StandbyIsOnFeedbackFunc + { + get + { + return () => CodecStatus.Status.Standby.State.BoolValue; + } + } + + /// + /// Gets the value of the currently shared source, or returns null + /// + protected override Func SharingSourceFeedbackFunc + { + get + { + return () => PresentationSourceKey; + } + } + + protected override Func SharingContentIsOnFeedbackFunc + { + get + { + return () => CodecStatus.Status.Conference.Presentation.Mode.BoolValue; + } + } + + protected Func FarEndIsSharingContentFeedbackFunc + { + get + { + return () => CodecStatus.Status.Conference.Presentation.Mode.Value == "Receiving"; + } + } + + protected override Func MuteFeedbackFunc + { + get + { + return () => CodecStatus.Status.Audio.VolumeMute.BoolValue; + } + } + + protected Func RoomIsOccupiedFeedbackFunc + { + get + { + return () => CodecStatus.Status.RoomAnalytics.PeoplePresence.BoolValue; + } + } + + protected Func PeopleCountFeedbackFunc + { + get + { + return () => CodecStatus.Status.RoomAnalytics.PeopleCount.Current.IntValue; + } + } + + protected Func SpeakerTrackIsOnFeedbackFunc + { + get + { + return () => CodecStatus.Status.Cameras.SpeakerTrack.Status.BoolValue; + } + } + + protected Func SelfViewIsOnFeedbackFunc + { + get + { + return () => CodecStatus.Status.Video.Selfview.Mode.BoolValue; + } + } + + protected Func SelfviewPipPositionFeedbackFunc + { + get + { + return () => CurrentSelfviewPipPosition.Label; + } + } + + protected Func LocalLayoutFeedbackFunc + { + get + { + return () => CurrentLocalLayout.Label; + } + } + + protected Func LocalLayoutIsProminentFeedbackFunc + { + get + { + return () => CurrentLocalLayout.Label == "Prominent"; + } + } + + + private string CliFeedbackRegistrationExpression; + + private CodecSyncState SyncState; + + public CodecPhonebookSyncState PhonebookSyncState { get; private set; } + + private StringBuilder JsonMessage; + + private bool JsonFeedbackMessageIsIncoming; + + public bool CommDebuggingIsOn; + + string Delimiter = "\r\n"; + + /// + /// Used to track the current connector used for the presentation source + /// + int PresentationSource; + + string PresentationSourceKey; + + string PhonebookMode = "Local"; // Default to Local + + uint PhonebookResultsLimit = 255; // Could be set later by config. + + CTimer LoginMessageReceivedTimer; + CTimer RetryConnectionTimer; + + // **___________________________________________________________________** + // Timers to be moved to the global system timer at a later point.... + CTimer BookingsRefreshTimer; + CTimer PhonebookRefreshTimer; + // **___________________________________________________________________** + + public RoutingInputPort CodecOsdIn { get; private set; } + public RoutingInputPort HdmiIn2 { get; private set; } + public RoutingInputPort HdmiIn3 { get; private set; } + public RoutingOutputPort HdmiOut1 { get; private set; } + public RoutingOutputPort HdmiOut2 { get; private set; } + + + // Constructor for IBasicCommunication + public CiscoSparkCodec(DeviceConfig config, IBasicCommunication comm) + : base(config) + { + var props = JsonConvert.DeserializeObject(config.Properties.ToString()); + + // Use the configured phonebook results limit if present + if (props.PhonebookResultsLimit > 0) + { + PhonebookResultsLimit = props.PhonebookResultsLimit; + } + + // The queue that will collect the repsonses in the order they are received + ReceiveQueue = new GenericQueue(this.Key + "-rxQueue", 25); + + RoomIsOccupiedFeedback = new BoolFeedback(RoomIsOccupiedFeedbackFunc); + PeopleCountFeedback = new IntFeedback(PeopleCountFeedbackFunc); + CameraAutoModeIsOnFeedback = new BoolFeedback(SpeakerTrackIsOnFeedbackFunc); + SelfviewIsOnFeedback = new BoolFeedback(SelfViewIsOnFeedbackFunc); + SelfviewPipPositionFeedback = new StringFeedback(SelfviewPipPositionFeedbackFunc); + LocalLayoutFeedback = new StringFeedback(LocalLayoutFeedbackFunc); + LocalLayoutIsProminentFeedback = new BoolFeedback(LocalLayoutIsProminentFeedbackFunc); + FarEndIsSharingContentFeedback = new BoolFeedback(FarEndIsSharingContentFeedbackFunc); + CameraIsOffFeedback = new BoolFeedback(() => CodecStatus.Status.Video.Input.MainVideoMute.BoolValue); + CameraIsMutedFeedback = CameraIsOffFeedback; + SupportsCameraOff = true; + + PresentationViewMaximizedFeedback = new BoolFeedback(() => CurrentPresentationView == "Maximized"); + + Communication = comm; + + if (props.CommunicationMonitorProperties != null) + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + } + else + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 30000, 120000, 300000, "xStatus SystemUnit Software Version\r"); + } + + if (props.Sharing != null) + AutoShareContentWhileInCall = props.Sharing.AutoShareContentWhileInCall; + + ShowSelfViewByDefault = props.ShowSelfViewByDefault; + + DeviceManager.AddDevice(CommunicationMonitor); + + PhonebookMode = props.PhonebookMode; + + SyncState = new CodecSyncState(Key + "--Sync"); + + PhonebookSyncState = new CodecPhonebookSyncState(Key + "--PhonebookSync"); + + SyncState.InitialSyncCompleted += new EventHandler(SyncState_InitialSyncCompleted); + + PortGather = new CommunicationGather(Communication, Delimiter); + PortGather.IncludeDelimiter = true; + PortGather.LineReceived += this.Port_LineReceived; + + CodecInfo = new CiscoCodecInfo(CodecStatus, CodecConfiguration); + + CallHistory = new CodecCallHistory(); + + + if (props.Favorites != null) + { + CallFavorites = new CodecCallFavorites(); + CallFavorites.Favorites = props.Favorites; + } + + DirectoryRoot = new CodecDirectory(); + + DirectoryBrowseHistory = new List(); + + CurrentDirectoryResultIsNotDirectoryRoot = new BoolFeedback(() => DirectoryBrowseHistory.Count > 0); + + CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate(); + + CodecSchedule = new CodecScheduleAwareness(); + + //Set Feedback Actions + SetFeedbackActions(); + + CodecOsdIn = new RoutingInputPort(RoutingPortNames.CodecOsd, eRoutingSignalType.Audio | eRoutingSignalType.Video, + eRoutingPortConnectionType.Hdmi, new Action(StopSharing), this); + HdmiIn2 = new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.Audio | eRoutingSignalType.Video, + eRoutingPortConnectionType.Hdmi, new Action(SelectPresentationSource1), this); + HdmiIn3 = new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.Audio | eRoutingSignalType.Video, + eRoutingPortConnectionType.Hdmi, new Action(SelectPresentationSource2), this); + + HdmiOut1 = new RoutingOutputPort(RoutingPortNames.HdmiOut1, eRoutingSignalType.Audio | eRoutingSignalType.Video, + eRoutingPortConnectionType.Hdmi, null, this); + HdmiOut2 = new RoutingOutputPort(RoutingPortNames.HdmiOut2, eRoutingSignalType.Audio | eRoutingSignalType.Video, + eRoutingPortConnectionType.Hdmi, null, this); + + InputPorts.Add(CodecOsdIn); + InputPorts.Add(HdmiIn2); + InputPorts.Add(HdmiIn3); + OutputPorts.Add(HdmiOut1); + + SetUpCameras(); + + CreateOsdSource(); + + ExternalSourceListEnabled = props.ExternalSourceListEnabled; + ExternalSourceInputPort = props.ExternalSourceInputPort; + + if (props.UiBranding == null) + { + return; + } + Debug.Console(2, this, "Setting branding properties enable: {0} _brandingUrl {1}", props.UiBranding.Enable, + props.UiBranding.BrandingUrl); + + BrandingEnabled = props.UiBranding.Enable; + + _brandingUrl = props.UiBranding.BrandingUrl; + } + + private void SetFeedbackActions() + { + CodecStatus.Status.Audio.Volume.ValueChangedAction = VolumeLevelFeedback.FireUpdate; + CodecStatus.Status.Audio.VolumeMute.ValueChangedAction = MuteFeedback.FireUpdate; + CodecStatus.Status.Audio.Microphones.Mute.ValueChangedAction = PrivacyModeIsOnFeedback.FireUpdate; + CodecStatus.Status.Standby.State.ValueChangedAction = StandbyIsOnFeedback.FireUpdate; + CodecStatus.Status.RoomAnalytics.PeoplePresence.ValueChangedAction = RoomIsOccupiedFeedback.FireUpdate; + CodecStatus.Status.RoomAnalytics.PeopleCount.Current.ValueChangedAction = PeopleCountFeedback.FireUpdate; + CodecStatus.Status.Cameras.SpeakerTrack.Status.ValueChangedAction = CameraAutoModeIsOnFeedback.FireUpdate; + CodecStatus.Status.Cameras.SpeakerTrack.Availability.ValueChangedAction = () => { SupportsCameraAutoMode = CodecStatus.Status.Cameras.SpeakerTrack.Availability.BoolValue; }; + CodecStatus.Status.Video.Selfview.Mode.ValueChangedAction = SelfviewIsOnFeedback.FireUpdate; + CodecStatus.Status.Video.Selfview.PIPPosition.ValueChangedAction = ComputeSelfviewPipStatus; + CodecStatus.Status.Video.Layout.LayoutFamily.Local.ValueChangedAction = ComputeLocalLayout; + CodecStatus.Status.Conference.Presentation.Mode.ValueChangedAction = () => + { + SharingContentIsOnFeedback.FireUpdate(); + FarEndIsSharingContentFeedback.FireUpdate(); + }; + + try + { + CodecStatus.Status.Video.Input.MainVideoMute.ValueChangedAction = CameraIsOffFeedback.FireUpdate; + } + catch (Exception ex) + { + Debug.Console(0, this, "Error setting MainVideuMute Action: {0}", ex); + + if (ex.InnerException != null) + { + Debug.Console(0, this, "Error setting MainVideuMute Action: {0}", ex); + } + } + } + + /// + /// Creates the fake OSD source, and connects it's AudioVideo output to the CodecOsdIn input + /// to enable routing + /// + void CreateOsdSource() + { + OsdSource = new DummyRoutingInputsDevice(Key + "[osd]"); + DeviceManager.AddDevice(OsdSource); + var tl = new TieLine(OsdSource.AudioVideoOutputPort, CodecOsdIn); + TieLineCollection.Default.Add(tl); + } + + public void InitializeBranding(string roomKey) + { + Debug.Console(1, this, "Initializing Branding for room {0}", roomKey); + + if (!BrandingEnabled) + { + return; + } + + var mcBridgeKey = String.Format("mobileControlBridge-{0}", roomKey); + + var mcBridge = DeviceManager.GetDeviceForKey(mcBridgeKey) as IMobileControlRoomBridge; + + if (!String.IsNullOrEmpty(_brandingUrl)) + { + Debug.Console(1, this, "Branding URL found: {0}", _brandingUrl); + if (_brandingTimer != null) + { + _brandingTimer.Stop(); + _brandingTimer.Dispose(); + } + + _brandingTimer = new CTimer((o) => + { + if (_sendMcUrl) + { + SendMcBrandingUrl(mcBridge); + _sendMcUrl = false; + } + else + { + SendBrandingUrl(); + _sendMcUrl = true; + } + }, 0, 15000); + } else if (String.IsNullOrEmpty(_brandingUrl)) + { + Debug.Console(1, this, "No Branding URL found"); + if (mcBridge == null) return; + + Debug.Console(2, this, "Setting QR code URL: {0}", mcBridge.QrCodeUrl); + + mcBridge.UserCodeChanged += (o, a) => SendMcBrandingUrl(mcBridge); + mcBridge.UserPromptedForCode += (o, a) => DisplayUserCode(mcBridge.UserCode); + + SendMcBrandingUrl(mcBridge); + } + } + + /// + /// Displays the code for the specified duration + /// + /// Mobile Control user code + private void DisplayUserCode(string code) + { + SendText(string.Format("xcommand userinterface message alert display title:\"Mobile Control User Code:\" text:\"{0}\" duration: 30", code)); + } + + private void SendMcBrandingUrl(IMobileControlRoomBridge mcBridge) + { + if (mcBridge == null) + { + return; + } + + Debug.Console(1, this, "Sending url: {0}", mcBridge.QrCodeUrl); + + SendText("xconfiguration userinterface custommessage: \"Scan the QR code with a mobile phone to get started\""); + SendText("xconfiguration userinterface osd halfwakemessage: \"Tap the touch panel or scan the QR code with a mobile phone to get started\""); + + var checksum = !String.IsNullOrEmpty(mcBridge.QrCodeChecksum) + ? String.Format("checksum: {0} ", mcBridge.QrCodeChecksum) + : String.Empty; + + SendText(String.Format( + "xcommand userinterface branding fetch {1}type: branding url: {0}", + mcBridge.QrCodeUrl, checksum)); + SendText(String.Format( + "xcommand userinterface branding fetch {1}type: halfwakebranding url: {0}", + mcBridge.QrCodeUrl, checksum)); + } + + private void SendBrandingUrl() + { + Debug.Console(1, this, "Sending url: {0}", _brandingUrl); + + SendText(String.Format("xcommand userinterface branding fetch type: branding url: {0}", + _brandingUrl)); + SendText(String.Format("xcommand userinterface branding fetch type: halfwakebranding url: {0}", + _brandingUrl)); + } + /// + /// Starts the HTTP feedback server and syncronizes state of codec + /// + /// + public override bool CustomActivate() + { + CrestronConsole.AddNewConsoleCommand(SetCommDebug, "SetCodecCommDebug", "0 for Off, 1 for on", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(GetPhonebook, "GetCodecPhonebook", "Triggers a refresh of the codec phonebook", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(GetBookings, "GetCodecBookings", "Triggers a refresh of the booking data for today", ConsoleAccessLevelEnum.AccessOperator); + + return base.CustomActivate(); + } + + #region Overrides of Device + + public override void Initialize() + { + var socket = Communication as ISocketStatus; + if (socket != null) + { + socket.ConnectionChange += new EventHandler(socket_ConnectionChange); + } + + Communication.Connect(); + + CommunicationMonitor.Start(); + + const string prefix = "xFeedback register "; + + CliFeedbackRegistrationExpression = + prefix + "/Configuration" + Delimiter + + prefix + "/Status/Audio" + Delimiter + + prefix + "/Status/Call" + Delimiter + + prefix + "/Status/Conference/Presentation" + Delimiter + + prefix + "/Status/Cameras/SpeakerTrack" + Delimiter + + prefix + "/Status/RoomAnalytics" + Delimiter + + prefix + "/Status/RoomPreset" + Delimiter + + prefix + "/Status/Standby" + Delimiter + + prefix + "/Status/Video/Selfview" + Delimiter + + prefix + "/Status/Video/Layout" + Delimiter + + prefix + "/Status/Video/Input/MainVideoMute" + Delimiter + + prefix + "/Bookings" + Delimiter + + prefix + "/Event/CallDisconnect" + Delimiter + + prefix + "/Event/Bookings" + Delimiter + + prefix + "/Event/CameraPresetListUpdated" + Delimiter + + prefix + "/Event/UserInterface/Presentation/ExternalSource/Selected/SourceIdentifier" + Delimiter; + } + + #endregion + + /// + /// Fires when initial codec sync is completed. Used to then send commands to get call history, phonebook, bookings, etc. + /// + /// + /// + void SyncState_InitialSyncCompleted(object sender, EventArgs e) + { + // Fire the ready event + SetIsReady(); + //CommDebuggingIsOn = false; + + GetCallHistory(); + + PhonebookRefreshTimer = new CTimer(CheckCurrentHour, 3600000, 3600000); // check each hour to see if the phonebook should be downloaded + GetPhonebook(null); + + BookingsRefreshTimer = new CTimer(GetBookings, 900000, 900000); // 15 minute timer to check for new booking info + GetBookings(null); + } + + public void SetCommDebug(string s) + { + if (s == "1") + { + CommDebuggingIsOn = true; + Debug.Console(0, this, "Comm Debug Enabled."); + } + else + { + CommDebuggingIsOn = false; + Debug.Console(0, this, "Comm Debug Disabled."); + } + } + + void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + Debug.Console(1, this, "Socket status change {0}", e.Client.ClientStatus); + if (e.Client.IsConnected) + { + if(!SyncState.LoginMessageWasReceived) + LoginMessageReceivedTimer = new CTimer(o => DisconnectClientAndReconnect(), 5000); + } + else + { + SyncState.CodecDisconnected(); + PhonebookSyncState.CodecDisconnected(); + + if (PhonebookRefreshTimer != null) + { + PhonebookRefreshTimer.Stop(); + PhonebookRefreshTimer = null; + } + + if (BookingsRefreshTimer != null) + { + BookingsRefreshTimer.Stop(); + BookingsRefreshTimer = null; + } + } + } + + void DisconnectClientAndReconnect() + { + Debug.Console(1, this, "Retrying connection to codec."); + + Communication.Disconnect(); + + RetryConnectionTimer = new CTimer(o => Communication.Connect(), 2000); + + //CrestronEnvironment.Sleep(2000); + + //Communication.Connect(); + } + + /// + /// Gathers responses from the codec (including the delimiter. Responses are checked to see if they contain JSON data and if so, the data is collected until a complete JSON + /// message is received before forwarding the message to be deserialized. + /// + /// + /// + void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) + { + if (CommDebuggingIsOn) + { + if (!JsonFeedbackMessageIsIncoming) + Debug.Console(1, this, "RX: '{0}'", args.Text); + } + + if (args.Text == "{" + Delimiter) // Check for the beginning of a new JSON message + { + JsonFeedbackMessageIsIncoming = true; + + if (CommDebuggingIsOn) + Debug.Console(1, this, "Incoming JSON message..."); + + JsonMessage = new StringBuilder(); + } + else if (args.Text == "}" + Delimiter) // Check for the end of a JSON message + { + JsonFeedbackMessageIsIncoming = false; + + JsonMessage.Append(args.Text); + + if (CommDebuggingIsOn) + Debug.Console(1, this, "Complete JSON Received:\n{0}", JsonMessage.ToString()); + + // Enqueue the complete message to be deserialized + + ReceiveQueue.Enqueue(new ProcessStringMessage(JsonMessage.ToString(), DeserializeResponse)); + + return; + } + + if(JsonFeedbackMessageIsIncoming) + { + JsonMessage.Append(args.Text); + + //Debug.Console(1, this, "Building JSON:\n{0}", JsonMessage.ToString()); + return; + } + + if (!SyncState.InitialSyncComplete) + { + switch (args.Text.Trim().ToLower()) // remove the whitespace + { + case "*r login successful": + { + SyncState.LoginMessageReceived(); + + if(LoginMessageReceivedTimer != null) + LoginMessageReceivedTimer.Stop(); + + SendText("xPreferences outputmode json"); + break; + } + case "xpreferences outputmode json": + { + if (!SyncState.InitialStatusMessageWasReceived) + SendText("xStatus"); + break; + } + case "xfeedback register /event/calldisconnect": + { + SyncState.FeedbackRegistered(); + break; + } + } + } + + + } + + /// + /// Appends the delimiter and send the command to the codec + /// + /// + public void SendText(string command) + { + if (CommDebuggingIsOn) + Debug.Console(1, this, "Sending: '{0}'", command); + + Communication.SendText(command + Delimiter); + } + + void DeserializeResponse(string response) + { + try + { + //// Serializer settings. We want to ignore null values and missing members + //JsonSerializerSettings settings = new JsonSerializerSettings(); + //settings.NullValueHandling = NullValueHandling.Ignore; + //settings.MissingMemberHandling = MissingMemberHandling.Ignore; + //settings.ObjectCreationHandling = ObjectCreationHandling.Auto; + + if (response.IndexOf("\"Status\":{") > -1 || response.IndexOf("\"Status\": {") > -1) + { + // Status Message + + // Temp object so we can inpsect for call data before simply deserializing + CiscoCodecStatus.RootObject tempCodecStatus = new CiscoCodecStatus.RootObject(); + + JsonConvert.PopulateObject(response, tempCodecStatus); + + // Check to see if the message contains /Status/Conference/Presentation/LocalInstance and extract source value + var conference = tempCodecStatus.Status.Conference; + + if (conference.Presentation.LocalInstance.Count > 0) + { + if (!string.IsNullOrEmpty(conference.Presentation.LocalInstance[0].ghost)) + PresentationSource = 0; + else if (conference.Presentation.LocalInstance[0].Source != null) + { + PresentationSource = conference.Presentation.LocalInstance[0].Source.IntValue; + } + } + + // Check to see if this is a call status message received after the initial status message + if (tempCodecStatus.Status.Call.Count > 0) + { + // Iterate through the call objects in the response + foreach (CiscoCodecStatus.Call call in tempCodecStatus.Status.Call) + { + var tempActiveCall = ActiveCalls.FirstOrDefault(c => c.Id.Equals(call.id)); + + if (tempActiveCall != null) + { + bool changeDetected = false; + + eCodecCallStatus newStatus = eCodecCallStatus.Unknown; + + // Update properties of ActiveCallItem + if(call.Status != null) + if (!string.IsNullOrEmpty(call.Status.Value)) + { + tempActiveCall.Status = CodecCallStatus.ConvertToStatusEnum(call.Status.Value); + + if (newStatus == eCodecCallStatus.Connected) + GetCallHistory(); + + changeDetected = true; + } + if (call.CallType != null) + if (!string.IsNullOrEmpty(call.CallType.Value)) + { + tempActiveCall.Type = CodecCallType.ConvertToTypeEnum(call.CallType.Value); + changeDetected = true; + } + if (call.DisplayName != null) + if (!string.IsNullOrEmpty(call.DisplayName.Value)) + { + tempActiveCall.Name = call.DisplayName.Value; + changeDetected = true; + } + if (call.Direction != null) + { + if (!string.IsNullOrEmpty(call.Direction.Value)) + { + tempActiveCall.Direction = CodecCallDirection.ConvertToDirectionEnum(call.Direction.Value); + changeDetected = true; + } + } + + if (changeDetected) + { + SetSelfViewMode(); + OnCallStatusChange(tempActiveCall); + ListCalls(); + } + } + else if( call.ghost == null ) // if the ghost value is present the call has ended already + { + // Create a new call item + var newCallItem = new CodecActiveCallItem() + { + Id = call.id, + Status = CodecCallStatus.ConvertToStatusEnum(call.Status.Value), + Name = call.DisplayName.Value, + Number = call.RemoteNumber.Value, + Type = CodecCallType.ConvertToTypeEnum(call.CallType.Value), + Direction = CodecCallDirection.ConvertToDirectionEnum(call.Direction.Value) + }; + + // Add it to the ActiveCalls List + ActiveCalls.Add(newCallItem); + + ListCalls(); + + SetSelfViewMode(); + OnCallStatusChange(newCallItem); + } + + } + + } + + // Check for Room Preset data (comes in partial, so we need to handle these responses differently to prevent appending duplicate items + var tempPresets = tempCodecStatus.Status.RoomPreset; + + if (tempPresets.Count > 0) + { + // Create temporary list to store the existing items from the CiscoCodecStatus.RoomPreset collection + List existingRoomPresets = new List(); + // Add the existing items to the temporary list + existingRoomPresets.AddRange(CodecStatus.Status.RoomPreset); + // Populate the CodecStatus object (this will append new values to the RoomPreset collection + JsonConvert.PopulateObject(response, CodecStatus); + + JObject jResponse = JObject.Parse(response); + + IList roomPresets = jResponse["Status"]["RoomPreset"].Children().ToList(); + // Iterate the new items in this response agains the temporary list. Overwrite any existing items and add new ones. + foreach (var preset in tempPresets) + { + // First fine the existing preset that matches the id + var existingPreset = existingRoomPresets.FirstOrDefault(p => p.id.Equals(preset.id)); + if (existingPreset != null) + { + Debug.Console(1, this, "Existing Room Preset with ID: {0} found. Updating.", existingPreset.id); + + JToken updatedPreset = null; + + // Find the JToken from the response with the matching id + foreach (var jPreset in roomPresets) + { + if (jPreset["id"].Value() == existingPreset.id) + updatedPreset = jPreset; + } + + if (updatedPreset != null) + { + // use PopulateObject to overlay the partial data onto the existing object + JsonConvert.PopulateObject(updatedPreset.ToString(), existingPreset); + } + + } + else + { + Debug.Console(1, this, "New Room Preset with ID: {0}. Adding.", preset.id); + existingRoomPresets.Add(preset); + } + } + + // Replace the list in the CodecStatus object with the processed list + CodecStatus.Status.RoomPreset = existingRoomPresets; + + // Generecise the list + NearEndPresets = RoomPresets.GetGenericPresets(CodecStatus.Status.RoomPreset); + + var handler = CodecRoomPresetsListHasChanged; + if (handler != null) + { + handler(this, new EventArgs()); + } + } + else + { + JsonConvert.PopulateObject(response, CodecStatus); + } + + if (!SyncState.InitialStatusMessageWasReceived) + { + SyncState.InitialStatusMessageReceived(); + + if (!SyncState.InitialConfigurationMessageWasReceived) + SendText("xConfiguration"); + } + } + else if (response.IndexOf("\"Configuration\":{") > -1 || response.IndexOf("\"Configuration\": {") > -1) + { + // Configuration Message + + JsonConvert.PopulateObject(response, CodecConfiguration); + + if (!SyncState.InitialConfigurationMessageWasReceived) + { + SyncState.InitialConfigurationMessageReceived(); + if (!SyncState.FeedbackWasRegistered) + { + SendText(CliFeedbackRegistrationExpression); + } + } + + } + else if (response.IndexOf("\"Event\":{") > -1 || response.IndexOf("\"Event\": {") > -1) + { + if (response.IndexOf("\"CallDisconnect\":{") > -1 || response.IndexOf("\"CallDisconnect\": {") > -1) + { + CiscoCodecEvents.RootObject eventReceived = new CiscoCodecEvents.RootObject(); + + JsonConvert.PopulateObject(response, eventReceived); + + EvalutateDisconnectEvent(eventReceived); + } + else if (response.IndexOf("\"Bookings\":{") > -1 || response.IndexOf("\"Bookings\": {") > -1) // The list has changed, reload it + { + GetBookings(null); + } + + else if (response.IndexOf("\"UserInterface\":{") > -1 || response.IndexOf("\"UserInterface\": {") > -1) // External Source Trigger + { + CiscoCodecEvents.RootObject eventReceived = new CiscoCodecEvents.RootObject(); + JsonConvert.PopulateObject(response, eventReceived); + Debug.Console(2, this, "*** Got an External Source Selection {0} {1}", eventReceived, eventReceived.Event.UserInterface, eventReceived.Event.UserInterface.Presentation.ExternalSource.Selected.SourceIdentifier.Value); + + if (RunRouteAction != null && !_externalSourceChangeRequested) + { + RunRouteAction(eventReceived.Event.UserInterface.Presentation.ExternalSource.Selected.SourceIdentifier.Value, null); + } + + _externalSourceChangeRequested = false; + } + } + else if (response.IndexOf("\"CommandResponse\":{") > -1 || response.IndexOf("\"CommandResponse\": {") > -1) + { + // CommandResponse Message + + if (response.IndexOf("\"CallHistoryRecentsResult\":{") > -1 || response.IndexOf("\"CallHistoryRecentsResult\": {") > -1) + { + var codecCallHistory = new CiscoCallHistory.RootObject(); + + JsonConvert.PopulateObject(response, codecCallHistory); + + CallHistory.ConvertCiscoCallHistoryToGeneric(codecCallHistory.CommandResponse.CallHistoryRecentsResult.Entry); + } + else if (response.IndexOf("\"CallHistoryDeleteEntryResult\":{") > -1 || response.IndexOf("\"CallHistoryDeleteEntryResult\": {") > -1) + { + GetCallHistory(); + } + else if (response.IndexOf("\"PhonebookSearchResult\":{") > -1 || response.IndexOf("\"PhonebookSearchResult\": {") > -1) + { + var codecPhonebookResponse = new CiscoCodecPhonebook.RootObject(); + + JsonConvert.PopulateObject(response, codecPhonebookResponse); + + if (!PhonebookSyncState.InitialPhonebookFoldersWasReceived) + { + // Check if the phonebook has any folders + PhonebookSyncState.InitialPhonebookFoldersReceived(); + + PhonebookSyncState.SetPhonebookHasFolders(codecPhonebookResponse.CommandResponse.PhonebookSearchResult.Folder.Count > 0); + + if (PhonebookSyncState.PhonebookHasFolders) + { + DirectoryRoot.AddFoldersToDirectory(CiscoCodecPhonebook.GetRootFoldersFromSearchResult(codecPhonebookResponse.CommandResponse.PhonebookSearchResult)); + } + + // Get the number of contacts in the phonebook + GetPhonebookContacts(); + } + else if (!PhonebookSyncState.NumberOfContactsWasReceived) + { + // Store the total number of contacts in the phonebook + PhonebookSyncState.SetNumberOfContacts(Int32.Parse(codecPhonebookResponse.CommandResponse.PhonebookSearchResult.ResultInfo.TotalRows.Value)); + + DirectoryRoot.AddContactsToDirectory(CiscoCodecPhonebook.GetRootContactsFromSearchResult(codecPhonebookResponse.CommandResponse.PhonebookSearchResult)); + + PhonebookSyncState.PhonebookRootEntriesReceived(); + + PrintDirectory(DirectoryRoot); + } + else if (PhonebookSyncState.InitialSyncComplete) + { + var directoryResults = new CodecDirectory(); + + if(codecPhonebookResponse.CommandResponse.PhonebookSearchResult.ResultInfo.TotalRows.Value != "0") + directoryResults = CiscoCodecPhonebook.ConvertCiscoPhonebookToGeneric(codecPhonebookResponse.CommandResponse.PhonebookSearchResult); + + PrintDirectory(directoryResults); + + DirectoryBrowseHistory.Add(directoryResults); + + OnDirectoryResultReturned(directoryResults); + + } + } + else if (response.IndexOf("\"BookingsListResult\":{") > -1) + { + var codecBookings = new CiscoCodecBookings.RootObject(); + + JsonConvert.PopulateObject(response, codecBookings); + + if(codecBookings.CommandResponse.BookingsListResult.ResultInfo.TotalRows.Value != "0") + CodecSchedule.Meetings = CiscoCodecBookings.GetGenericMeetingsFromBookingResult(codecBookings.CommandResponse.BookingsListResult.Booking); + + BookingsRefreshTimer.Reset(900000, 900000); + } + + } + + } + catch (Exception ex) + { + Debug.Console(1, this, "Error Deserializing feedback from codec: {0}", ex); + } + } + + /// + /// Call when directory results are updated + /// + /// + void OnDirectoryResultReturned(CodecDirectory result) + { + CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate(); + + // This will return the latest results to all UIs. Multiple indendent UI Directory browsing will require a different methodology + var handler = DirectoryResultReturned; + if (handler != null) + { + handler(this, new DirectoryEventArgs() + { + Directory = result, + DirectoryIsOnRoot = !CurrentDirectoryResultIsNotDirectoryRoot.BoolValue + }); + } + + PrintDirectory(result); + } + + /// + /// Evaluates an event received from the codec + /// + /// + void EvalutateDisconnectEvent(CiscoCodecEvents.RootObject eventReceived) + { + if (eventReceived.Event.CallDisconnect != null) + { + var tempActiveCall = ActiveCalls.FirstOrDefault(c => c.Id.Equals(eventReceived.Event.CallDisconnect.CallId.Value)); + + // Remove the call from the Active calls list + if (tempActiveCall != null) + { + ActiveCalls.Remove(tempActiveCall); + + ListCalls(); + + SetSelfViewMode(); + // Notify of the call disconnection + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, tempActiveCall); + + GetCallHistory(); + } + } + } + + /// + /// + /// + /// + public override void ExecuteSwitch(object selector) + { + (selector as Action)(); + PresentationSourceKey = selector.ToString(); + } + + /// + /// This is necessary for devices that are "routers" in the middle of the path, even though it only has one output and + /// may only have one input. + /// + public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) + { + ExecuteSwitch(inputSelector); + PresentationSourceKey = inputSelector.ToString(); + } + + + /// + /// Gets the ID of the last connected call + /// + /// + public string GetCallId() + { + string callId = null; + + if (ActiveCalls.Count > 1) + { + var lastCallIndex = ActiveCalls.Count - 1; + callId = ActiveCalls[lastCallIndex].Id; + } + else if (ActiveCalls.Count == 1) + callId = ActiveCalls[0].Id; + + return callId; + + } + + public void GetCallHistory() + { + SendText("xCommand CallHistory Recents Limit: 20 Order: OccurrenceTime"); + } + + /// + /// Required for IHasScheduleAwareness + /// + public void GetSchedule() + { + GetBookings(null); + } + + /// + /// Gets the bookings for today + /// + /// + public void GetBookings(object command) + { + Debug.Console(1, this, "Retrieving Booking Info from Codec. Current Time: {0}", DateTime.Now.ToLocalTime()); + + SendText("xCommand Bookings List Days: 1 DayOffset: 0"); + } + + /// + /// Checks to see if it is 2am (or within that hour) and triggers a download of the phonebook + /// + /// + public void CheckCurrentHour(object o) + { + if (DateTime.Now.Hour == 2) + { + Debug.Console(1, this, "Checking hour to see if phonebook should be downloaded. Current hour is {0}", DateTime.Now.Hour); + + GetPhonebook(null); + PhonebookRefreshTimer.Reset(3600000, 3600000); + } + } + + /// + /// Triggers a refresh of the codec phonebook + /// + /// Just to allow this method to be called from a console command + public void GetPhonebook(string command) + { + PhonebookSyncState.CodecDisconnected(); + + DirectoryRoot = new CodecDirectory(); + + GetPhonebookFolders(); + } + + private void GetPhonebookFolders() + { + // Get Phonebook Folders (determine local/corporate from config, and set results limit) + SendText(string.Format("xCommand Phonebook Search PhonebookType: {0} ContactType: Folder", PhonebookMode)); + } + + private void GetPhonebookContacts() + { + // Get Phonebook Folders (determine local/corporate from config, and set results limit) + SendText(string.Format("xCommand Phonebook Search PhonebookType: {0} ContactType: Contact Limit: {1}", PhonebookMode, PhonebookResultsLimit)); + } + + /// + /// Searches the codec phonebook for all contacts matching the search string + /// + /// + public void SearchDirectory(string searchString) + { + SendText(string.Format("xCommand Phonebook Search SearchString: \"{0}\" PhonebookType: {1} ContactType: Contact Limit: {2}", searchString, PhonebookMode, PhonebookResultsLimit)); + } + + /// + /// // Get contents of a specific folder in the phonebook + /// + /// + public void GetDirectoryFolderContents(string folderId) + { + SendText(string.Format("xCommand Phonebook Search FolderId: {0} PhonebookType: {1} ContactType: Any Limit: {2}", folderId, PhonebookMode, PhonebookResultsLimit)); + } + + /// + /// Sets the parent folder contents or the directory root as teh current directory and fires the event. Used to browse up a level + /// + /// + public void GetDirectoryParentFolderContents() + { + var currentDirectory = new CodecDirectory(); + + if (DirectoryBrowseHistory.Count > 0) + { + var lastItemIndex = DirectoryBrowseHistory.Count - 1; + var parentDirectoryContents = DirectoryBrowseHistory[lastItemIndex]; + + DirectoryBrowseHistory.Remove(DirectoryBrowseHistory[lastItemIndex]); + + currentDirectory = parentDirectoryContents; + + } + else + { + currentDirectory = DirectoryRoot; + } + + OnDirectoryResultReturned(currentDirectory); + } + + /// + /// Clears the session browse history and fires the event with the directory root + /// + public void SetCurrentDirectoryToRoot() + { + DirectoryBrowseHistory.Clear(); + + OnDirectoryResultReturned(DirectoryRoot); + } + + /// + /// Prints the directory to console + /// + /// + void PrintDirectory(CodecDirectory directory) + { + if (Debug.Level > 0) + { + Debug.Console(1, this, "Directory Results:\n"); + + foreach (DirectoryItem item in directory.CurrentDirectoryResults) + { + if (item is DirectoryFolder) + { + Debug.Console(1, this, "[+] {0}", item.Name); + } + else if (item is DirectoryContact) + { + Debug.Console(1, this, "{0}", item.Name); + } + } + Debug.Console(1, this, "Directory is on Root Level: {0}", !CurrentDirectoryResultIsNotDirectoryRoot.BoolValue); + } + + } + + /// + /// Simple dial method + /// + /// + public override void Dial(string number) + { + SendText(string.Format("xCommand Dial Number: \"{0}\"", number)); + } + + /// + /// Dials a specific meeting + /// + /// + public override void Dial(Meeting meeting) + { + foreach (Call c in meeting.Calls) + { + Dial(c.Number, c.Protocol, c.CallRate, c.CallType, meeting.Id); + } + } + + /// + /// Detailed dial method + /// + /// + /// + /// + /// + /// + public void Dial(string number, string protocol, string callRate, string callType, string meetingId) + { + SendText(string.Format("xCommand Dial Number: \"{0}\" Protocol: {1} CallRate: {2} CallType: {3} BookingId: {4}", number, protocol, callRate, callType, meetingId)); + } + + public override void EndCall(CodecActiveCallItem activeCall) + { + SendText(string.Format("xCommand Call Disconnect CallId: {0}", activeCall.Id)); + } + + public override void EndAllCalls() + { + foreach (CodecActiveCallItem activeCall in ActiveCalls) + { + SendText(string.Format("xCommand Call Disconnect CallId: {0}", activeCall.Id)); + } + } + + public override void AcceptCall(CodecActiveCallItem item) + { + SendText("xCommand Call Accept"); + } + + public override void RejectCall(CodecActiveCallItem item) + { + SendText("xCommand Call Reject"); + } + + public override void SendDtmf(string s) + { + SendText(string.Format("xCommand Call DTMFSend CallId: {0} DTMFString: \"{1}\"", GetCallId(), s)); + } + + public void SelectPresentationSource(int source) + { + PresentationSource = source; + + StartSharing(); + } + + /// + /// Select source 1 as the presetnation source + /// + public void SelectPresentationSource1() + { + SelectPresentationSource(2); + } + + /// + /// Select source 2 as the presetnation source + /// + public void SelectPresentationSource2() + { + SelectPresentationSource(3); + } + + /// + /// Starts presentation sharing + /// + public override void StartSharing() + { + string sendingMode = string.Empty; + + if (IsInCall) + sendingMode = "LocalRemote"; + else + sendingMode = "LocalOnly"; + + if(PresentationSource > 0) + SendText(string.Format("xCommand Presentation Start PresentationSource: {0} SendingMode: {1}", PresentationSource, sendingMode)); + } + + /// + /// Stops sharing the current presentation + /// + public override void StopSharing() + { + PresentationSource = 0; + + SendText("xCommand Presentation Stop"); + } + + public override void PrivacyModeOn() + { + SendText("xCommand Audio Microphones Mute"); + } + + public override void PrivacyModeOff() + { + SendText("xCommand Audio Microphones Unmute"); + } + + public override void PrivacyModeToggle() + { + SendText("xCommand Audio Microphones ToggleMute"); + } + + public override void MuteOff() + { + SendText("xCommand Audio Volume Unmute"); + } + + public override void MuteOn() + { + SendText("xCommand Audio Volume Mute"); + } + + public override void MuteToggle() + { + SendText("xCommand Audio Volume ToggleMute"); + } + + /// + /// Increments the voluem + /// + /// + public override void VolumeUp(bool pressRelease) + { + SendText("xCommand Audio Volume Increase"); + } + + /// + /// Decrements the volume + /// + /// + public override void VolumeDown(bool pressRelease) + { + SendText("xCommand Audio Volume Decrease"); + } + + /// + /// Scales the level and sets the codec to the specified level within its range + /// + /// level from slider (0-65535 range) + public override void SetVolume(ushort level) + { + var scaledLevel = CrestronEnvironment.ScaleWithLimits(level, 65535, 0, 100, 0); + SendText(string.Format("xCommand Audio Volume Set Level: {0}", scaledLevel)); + } + + /// + /// Recalls the default volume on the codec + /// + public void VolumeSetToDefault() + { + SendText("xCommand Audio Volume SetToDefault"); + } + + /// + /// Puts the codec in standby mode + /// + public override void StandbyActivate() + { + SendText("xCommand Standby Activate"); + } + + /// + /// Wakes the codec from standby + /// + public override void StandbyDeactivate() + { + SendText("xCommand Standby Deactivate"); + } + + public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + LinkVideoCodecToApi(this, trilist, joinStart, joinMapKey, bridge); + } + + /// + /// Reboots the codec + /// + public void Reboot() + { + SendText("xCommand SystemUnit Boot Action: Restart"); + } + + /// + /// Sets SelfView Mode based on config + /// + void SetSelfViewMode() + { + if (!IsInCall) + { + SelfViewModeOff(); + } + else + { + if (ShowSelfViewByDefault) + SelfViewModeOn(); + else + SelfViewModeOff(); + } + } + + /// + /// Turns on Selfview Mode + /// + public void SelfViewModeOn() + { + SendText("xCommand Video Selfview Set Mode: On"); + } + + /// + /// Turns off Selfview Mode + /// + public void SelfViewModeOff() + { + SendText("xCommand Video Selfview Set Mode: Off"); + } + + /// + /// Toggles Selfview mode on/off + /// + public void SelfViewModeToggle() + { + string mode = string.Empty; + + if (CodecStatus.Status.Video.Selfview.Mode.BoolValue) + mode = "Off"; + else + mode = "On"; + + SendText(string.Format("xCommand Video Selfview Set Mode: {0}", mode)); + } + + /// + /// Sets a specified position for the selfview PIP window + /// + /// + public void SelfviewPipPositionSet(CodecCommandWithLabel position) + { + SendText(string.Format("xCommand Video Selfview Set Mode: On PIPPosition: {0}", position.Command)); + } + + /// + /// Toggles to the next selfview PIP position + /// + public void SelfviewPipPositionToggle() + { + if (CurrentSelfviewPipPosition != null) + { + var nextPipPositionIndex = SelfviewPipPositions.IndexOf(CurrentSelfviewPipPosition) + 1; + + if (nextPipPositionIndex >= SelfviewPipPositions.Count) // Check if we need to loop back to the first item in the list + nextPipPositionIndex = 0; + + SelfviewPipPositionSet(SelfviewPipPositions[nextPipPositionIndex]); + } + } + + /// + /// Sets a specific local layout + /// + /// + public void LocalLayoutSet(CodecCommandWithLabel layout) + { + SendText(string.Format("xCommand Video Layout LayoutFamily Set Target: local LayoutFamily: {0}", layout.Command)); + } + + /// + /// Toggles to the next local layout + /// + public void LocalLayoutToggle() + { + if(CurrentLocalLayout != null) + { + var nextLocalLayoutIndex = LocalLayouts.IndexOf(CurrentLocalLayout) + 1; + + if (nextLocalLayoutIndex >= LocalLayouts.Count) // Check if we need to loop back to the first item in the list + nextLocalLayoutIndex = 0; + + LocalLayoutSet(LocalLayouts[nextLocalLayoutIndex]); + } + } + + /// + /// Toggles between single/prominent layouts + /// + public void LocalLayoutToggleSingleProminent() + { + if (CurrentLocalLayout != null) + { + if (CurrentLocalLayout.Label != "Prominent") + LocalLayoutSet(LocalLayouts.FirstOrDefault(l => l.Label.Equals("Prominent"))); + else + LocalLayoutSet(LocalLayouts.FirstOrDefault(l => l.Label.Equals("Single"))); + } + + } + + /// + /// + /// + public void MinMaxLayoutToggle() + { + if (PresentationViewMaximizedFeedback.BoolValue) + CurrentPresentationView = "Minimized"; + else + CurrentPresentationView = "Maximized"; + + SendText(string.Format("xCommand Video PresentationView Set View: {0}", CurrentPresentationView)); + PresentationViewMaximizedFeedback.FireUpdate(); + } + + /// + /// Calculates the current selfview PIP position + /// + void ComputeSelfviewPipStatus() + { + CurrentSelfviewPipPosition = SelfviewPipPositions.FirstOrDefault(p => p.Command.ToLower().Equals(CodecStatus.Status.Video.Selfview.PIPPosition.Value.ToLower())); + + if(CurrentSelfviewPipPosition != null) + SelfviewIsOnFeedback.FireUpdate(); + } + + /// + /// Calculates the current local Layout + /// + void ComputeLocalLayout() + { + CurrentLocalLayout = LocalLayouts.FirstOrDefault(l => l.Command.ToLower().Equals(CodecStatus.Status.Video.Layout.LayoutFamily.Local.Value.ToLower())); + + if (CurrentLocalLayout != null) + LocalLayoutFeedback.FireUpdate(); + } + + public void RemoveCallHistoryEntry(CodecCallHistory.CallHistoryEntry entry) + { + SendText(string.Format("xCommand CallHistory DeleteEntry CallHistoryId: {0} AcknowledgeConsecutiveDuplicates: True", entry.OccurrenceHistoryId)); + } + + #region IHasCameraSpeakerTrack + + public void CameraAutoModeToggle() + { + if (!CameraAutoModeIsOnFeedback.BoolValue) + { + SendText("xCommand Cameras SpeakerTrack Activate"); + } + else + { + SendText("xCommand Cameras SpeakerTrack Deactivate"); + } + } + + public void CameraAutoModeOn() + { + if (CameraIsOffFeedback.BoolValue) + { + CameraMuteOff(); + } + + SendText("xCommand Cameras SpeakerTrack Activate"); + } + + public void CameraAutoModeOff() + { + if (CameraIsOffFeedback.BoolValue) + { + CameraMuteOff(); + } + + SendText("xCommand Cameras SpeakerTrack Deactivate"); + } + + #endregion + + /// + /// Builds the cameras List. Could later be modified to build from config data + /// + void SetUpCameras() + { + // Add the internal camera + Cameras = new List(); + + var internalCamera = new CiscoSparkCamera(Key + "-camera1", "Near End", this, 1); + + if(CodecStatus.Status.Cameras.Camera.Count > 0) + internalCamera.SetCapabilites(CodecStatus.Status.Cameras.Camera[0].Capabilities.Options.Value); + else + // Somehow subscribe to the event on the Options.Value property and update when it changes. + + Cameras.Add(internalCamera); + + // Add the far end camera + var farEndCamera = new CiscoFarEndCamera(Key + "-cameraFar", "Far End", this); + Cameras.Add(farEndCamera); + + SelectedCameraFeedback = new StringFeedback(() => SelectedCamera.Key); + + ControllingFarEndCameraFeedback = new BoolFeedback(() => SelectedCamera is IAmFarEndCamera); + + DeviceManager.AddDevice(internalCamera); + DeviceManager.AddDevice(farEndCamera); + + NearEndPresets = new List(15); + + FarEndRoomPresets = new List(15); + + // Add the far end presets + for (int i = 1; i <= FarEndRoomPresets.Capacity; i++) + { + var label = string.Format("Far End Preset {0}", i); + FarEndRoomPresets.Add(new CodecRoomPreset(i, label, true, false)); + } + + SelectedCamera = internalCamera; ; // call the method to select the camera and ensure the feedbacks get updated. + } + + #region IHasCodecCameras Members + + public event EventHandler CameraSelected; + + public List Cameras { get; private set; } + + public StringFeedback SelectedCameraFeedback { get; private set; } + + private CameraBase _selectedCamera; + + /// + /// Returns the selected camera + /// + public CameraBase SelectedCamera + { + get + { + return _selectedCamera; + } + private set + { + _selectedCamera = value; + SelectedCameraFeedback.FireUpdate(); + ControllingFarEndCameraFeedback.FireUpdate(); + if (CameraIsOffFeedback.BoolValue) + CameraMuteOff(); + + var handler = CameraSelected; + if (handler != null) + { + handler(this, new CameraSelectedEventArgs(SelectedCamera)); + } + } + } + + public void SelectCamera(string key) + { + var camera = Cameras.FirstOrDefault(c => c.Key.IndexOf(key, StringComparison.OrdinalIgnoreCase) > -1); + if (camera != null) + { + Debug.Console(2, this, "Selected Camera with key: '{0}'", camera.Key); + SelectedCamera = camera; + } + else + Debug.Console(2, this, "Unable to select camera with key: '{0}'", key); + } + + public CameraBase FarEndCamera { get; private set; } + + public BoolFeedback ControllingFarEndCameraFeedback { get; private set; } + + #endregion + + /// + /// + /// + public class CiscoCodecInfo : VideoCodecInfo + { + public CiscoCodecStatus.RootObject CodecStatus { get; private set; } + + public CiscoCodecConfiguration.RootObject CodecConfiguration { get; private set; } + + public override bool MultiSiteOptionIsEnabled + { + get + { + if (!string.IsNullOrEmpty(CodecStatus.Status.SystemUnit.Software.OptionKeys.MultiSite.Value) && CodecStatus.Status.SystemUnit.Software.OptionKeys.MultiSite.Value.ToLower() == "true") + return true; + else + return false; + } + + } + public override string IpAddress + { + get + { + if (CodecConfiguration.Configuration.Network != null) + { + if (CodecConfiguration.Configuration.Network.Count > 0) + return CodecConfiguration.Configuration.Network[0].IPv4.Address.Value; + } + return string.Empty; + } + } + public override string E164Alias + { + get + { + if (CodecConfiguration.Configuration.H323 != null && CodecConfiguration.Configuration.H323.H323Alias.E164 != null) + { + return CodecConfiguration.Configuration.H323.H323Alias.E164.Value; + } + else + { + return string.Empty; + } + } + } + public override string H323Id + { + get + { + if (CodecConfiguration.Configuration.H323 != null && CodecConfiguration.Configuration.H323.H323Alias != null + && CodecConfiguration.Configuration.H323.H323Alias.ID != null) + { + return CodecConfiguration.Configuration.H323.H323Alias.ID.Value; + } + else + { + return string.Empty; + } + } + } + public override string SipPhoneNumber + { + get + { + if (CodecStatus.Status.SIP != null && CodecStatus.Status.SIP.Registration.Count > 0) + { + var match = Regex.Match(CodecStatus.Status.SIP.Registration[0].URI.Value, @"(\d+)"); // extract numbers only + if (match.Success) + { + Debug.Console(1, "Extracted phone number as '{0}' from string '{1}'", match.Groups[1].Value, CodecStatus.Status.SIP.Registration[0].URI.Value); + return match.Groups[1].Value; + } + else + { + Debug.Console(1, "Unable to extract phone number from string: '{0}'", CodecStatus.Status.SIP.Registration[0].URI.Value); + return string.Empty; + } + } + else + { + Debug.Console(1, "Unable to extract phone number. No SIP Registration items found"); + return string.Empty; + } + } + } + + public override string SipUri + { + get + { + if (CodecStatus.Status.SIP != null && CodecStatus.Status.SIP.AlternateURI.Primary.URI.Value != null) + { + return CodecStatus.Status.SIP.AlternateURI.Primary.URI.Value; + } + else if (CodecStatus.Status.UserInterface != null && + CodecStatus.Status.UserInterface.ContactInfo.ContactMethod[0].Number.Value != null) + { + return CodecStatus.Status.UserInterface.ContactInfo.ContactMethod[0].Number.Value; + } + else + return string.Empty; + } + } + + public override bool AutoAnswerEnabled + { + get + { + if (CodecConfiguration.Configuration.Conference.AutoAnswer.Mode.Value.ToLower() == "on") + return true; + else + return false; + } + } + + public CiscoCodecInfo(CiscoCodecStatus.RootObject status, CiscoCodecConfiguration.RootObject configuration) + { + CodecStatus = status; + CodecConfiguration = configuration; + } + } + + + #region IHasCameraPresets Members + + public event EventHandler CodecRoomPresetsListHasChanged; + + public List NearEndPresets { get; private set; } + + public List FarEndRoomPresets { get; private set; } + + public void CodecRoomPresetSelect(int preset) + { + Debug.Console(1, this, "Selecting Preset: {0}", preset); + if (SelectedCamera is IAmFarEndCamera) + SelectFarEndPreset(preset); + else + SendText(string.Format("xCommand RoomPreset Activate PresetId: {0}", preset)); + } + + public void CodecRoomPresetStore(int preset, string description) + { + SendText(string.Format("xCommand RoomPreset Store PresetId: {0} Description: \"{1}\" Type: All", preset, description)); + } + + #endregion + + public void SelectFarEndPreset(int preset) + { + SendText(string.Format("xCommand Call FarEndControl RoomPreset Activate CallId: {0} PresetId: {1}", GetCallId(), preset)); + } + + + #region IHasExternalSourceSwitching Members + + /// + /// Wheather the Cisco supports External Source Lists or not + /// + public bool ExternalSourceListEnabled + { + get; + private set; + } + + /// + /// The name of the RoutingInputPort to which the upstream external switcher is connected + /// + public string ExternalSourceInputPort { get; private set; } + + public bool BrandingEnabled { get; private set; } + private string _brandingUrl; + private bool _sendMcUrl; + + /// + /// Adds an external source to the Cisco + /// + /// + /// + /// + public void AddExternalSource(string connectorId, string key, string name, eExternalSourceType type) + { + int id = 2; + if (connectorId.ToLower() == "hdmiin3") + { + id = 3; + } + SendText(string.Format("xCommand UserInterface Presentation ExternalSource Add ConnectorId: {0} SourceIdentifier: \"{1}\" Name: \"{2}\" Type: {3}", id, key, name, type.ToString())); + // SendText(string.Format("xCommand UserInterface Presentation ExternalSource State Set SourceIdentifier: \"{0}\" State: Ready", key)); + Debug.Console(2, this, "Adding ExternalSource {0} {1}", connectorId, name); + + } + + + /// + /// Sets the state of the External Source + /// + /// + /// + public void SetExternalSourceState(string key, eExternalSourceMode mode) + { + SendText(string.Format("xCommand UserInterface Presentation ExternalSource State Set SourceIdentifier: \"{0}\" State: {1}", key, mode.ToString())); + } + /// + /// Clears all external sources on the codec + /// + public void ClearExternalSources() + { + SendText("xCommand UserInterface Presentation ExternalSource RemoveAll"); + + } + + /// + /// Sets the selected source of the available external sources on teh Touch10 UI + /// + public void SetSelectedSource(string key) + { + SendText(string.Format("xCommand UserInterface Presentation ExternalSource Select SourceIdentifier: {0}", key)); + _externalSourceChangeRequested = true; + } + + /// + /// Action that will run when the External Source is selected. + /// + public Action RunRouteAction { private get; set; } + + + + + + + #endregion + #region ExternalDevices + + + + #endregion + + #region IHasCameraOff Members + + public BoolFeedback CameraIsOffFeedback { get; private set; } + + public void CameraOff() + { + CameraMuteOn(); + } + + #endregion + + public BoolFeedback CameraIsMutedFeedback { get; private set; } + + /// + /// Mutes the outgoing camera video + /// + public void CameraMuteOn() + { + SendText("xCommand Video Input MainVideo Mute"); + } + + /// + /// Unmutes the outgoing camera video + /// + public void CameraMuteOff() + { + SendText("xCommand Video Input MainVideo Unmute"); + } + + /// + /// Toggles the camera mute state + /// + public void CameraMuteToggle() + { + if (CameraIsMutedFeedback.BoolValue) + CameraMuteOff(); + else + CameraMuteOn(); + } + } + + + /// + /// Represents a codec command that might need to have a friendly label applied for UI feedback purposes + /// + public class CodecCommandWithLabel + { + public string Command { get; set; } + public string Label { get; set; } + + public CodecCommandWithLabel(string command, string label) + { + Command = command; + Label = label; + } + } + + /// + /// Tracks the initial sycnronization state of the codec when making a connection + /// + public class CodecSyncState : IKeyed + { + bool _InitialSyncComplete; + + public event EventHandler InitialSyncCompleted; + + public string Key { get; private set; } + + public bool InitialSyncComplete + { + get { return _InitialSyncComplete; } + private set + { + if (value == true) + { + var handler = InitialSyncCompleted; + if (handler != null) + handler(this, new EventArgs()); + } + _InitialSyncComplete = value; + } + } + + public bool LoginMessageWasReceived { get; private set; } + + public bool InitialStatusMessageWasReceived { get; private set; } + + public bool InitialConfigurationMessageWasReceived { get; private set; } + + public bool FeedbackWasRegistered { get; private set; } + + public CodecSyncState(string key) + { + Key = key; + CodecDisconnected(); + } + + public void LoginMessageReceived() + { + LoginMessageWasReceived = true; + Debug.Console(1, this, "Login Message Received."); + CheckSyncStatus(); + } + + public void InitialStatusMessageReceived() + { + InitialStatusMessageWasReceived = true; + Debug.Console(1, this, "Initial Codec Status Message Received."); + CheckSyncStatus(); + } + + public void InitialConfigurationMessageReceived() + { + InitialConfigurationMessageWasReceived = true; + Debug.Console(1, this, "Initial Codec Configuration Message Received."); + CheckSyncStatus(); + } + + public void FeedbackRegistered() + { + FeedbackWasRegistered = true; + Debug.Console(1, this, "Initial Codec Feedback Registration Successful."); + CheckSyncStatus(); + } + + public void CodecDisconnected() + { + LoginMessageWasReceived = false; + InitialConfigurationMessageWasReceived = false; + InitialStatusMessageWasReceived = false; + FeedbackWasRegistered = false; + InitialSyncComplete = false; + } + + void CheckSyncStatus() + { + if (LoginMessageWasReceived && InitialConfigurationMessageWasReceived && InitialStatusMessageWasReceived && FeedbackWasRegistered) + { + InitialSyncComplete = true; + Debug.Console(1, this, "Initial Codec Sync Complete!"); + } + else + InitialSyncComplete = false; + } + } + + public class CiscoSparkCodecFactory : EssentialsDeviceFactory + { + public CiscoSparkCodecFactory() + { + TypeNames = new List() { "ciscospark", "ciscowebex", "ciscowebexpro", "ciscoroomkit", "ciscosparkpluscodec" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.Console(1, "Factory Attempting to create new Cisco Codec Device"); + + var comm = CommFactory.CreateCommForDevice(dc); + return new VideoCodec.Cisco.CiscoSparkCodec(dc, comm); + } + } } \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVC.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVC.cs index 78302fcc..ac7514e0 100644 --- a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVC.cs +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVC.cs @@ -431,8 +431,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec m.MinutesBeforeMeeting = 5; m.Id = i.ToString(); m.Organizer = "Employee " + 1; - m.StartTime = DateTime.Now.AddMinutes(6).AddHours(i); - m.EndTime = DateTime.Now.AddHours(i).AddMinutes(16); + m.StartTime = DateTime.Now.AddMinutes(5).AddHours(i); + m.EndTime = DateTime.Now.AddHours(i).AddMinutes(50); m.Title = "Meeting " + i; m.Calls.Add(new Call() { Number = i + "meeting@fake.com"}); _CodecSchedule.Meetings.Add(m); @@ -602,6 +602,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec CameraAutoModeIsOnFeedback = new BoolFeedback(() => _CameraAutoModeIsOn); + SupportsCameraAutoMode = true; + CameraAutoModeIsOnFeedback.FireUpdate(); DeviceManager.AddDevice(internalCamera); @@ -609,7 +611,18 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec NearEndPresets = new List(15); // Fix the capacity to emulate Cisco - NearEndPresets = PropertiesConfig.Presets; + if (PropertiesConfig.Presets != null && PropertiesConfig.Presets.Count > 0) + { + NearEndPresets = PropertiesConfig.Presets; + } + else + { + for (int i = 1; i <= NearEndPresets.Capacity; i++) + { + var label = string.Format("Near End Preset {0}", i); + NearEndPresets.Add(new CodecRoomPreset(i, label, true, false)); + } + } FarEndRoomPresets = new List(15); // Fix the capacity to emulate Cisco diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVcPropertiesConfig.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVcPropertiesConfig.cs index 6a790af6..d294aff1 100644 --- a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVcPropertiesConfig.cs +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/MockVC/MockVcPropertiesConfig.cs @@ -18,5 +18,11 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec [JsonProperty("presets")] public List Presets { get; set; } + + public MockVcPropertiesConfig() + { + Favorites = new List(); + Presets = new List(); + } } } \ No newline at end of file