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, IHasDoNotDisturbMode, IHasHalfWakeMode, IHasCallHold, IJoinCalls { private CiscoSparkCodecPropertiesConfig _config; 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; } private 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; } public IntFeedback RingtoneVolumeFeedback { 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"; public IntFeedback PresentationSourceFeedback { get; private set; } public BoolFeedback PresentationSendingLocalOnlyFeedback { get; private set; } public BoolFeedback PresentationSendingLocalRemoteFeedback { get; private set; } /// /// Used to track the current connector used for the presentation source /// private int _presentationSource; /// /// Used to track the connector that is desired to be the current presentation source (until the command is send) /// private int _desiredPresentationSource; private string _presentationSourceKey; private bool _presentationLocalOnly; private bool _presentationLocalRemote; private string _phonebookMode = "Local"; // Default to Local private uint _phonebookResultsLimit = 255; // Could be set later by config. private CTimer _loginMessageReceivedTimer; private CTimer _retryConnectionTimer; // **___________________________________________________________________** // Timers to be moved to the global system timer at a later point.... private CTimer BookingsRefreshTimer; private 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()); _config = props; // 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; DoNotDisturbModeIsOnFeedback = new BoolFeedback(() => CodecStatus.Status.Conference.DoNotDisturb.BoolValue); HalfWakeModeIsOnFeedback = new BoolFeedback(() => CodecStatus.Status.Standby.State.Value.ToLower() == "halfwake"); EnteringStandbyModeFeedback = new BoolFeedback(() => CodecStatus.Status.Standby.State.Value.ToLower() == "enteringstandby"); PresentationViewMaximizedFeedback = new BoolFeedback(() => _currentPresentationView == "Maximized"); RingtoneVolumeFeedback = new IntFeedback(() => CodecConfiguration.Configuration.Audio.SoundsAndAlerts.RingVolume.Volume); PresentationSourceFeedback = new IntFeedback(() => _presentationSource); PresentationSendingLocalOnlyFeedback = new BoolFeedback(() => _presentationLocalOnly); PresentationSendingLocalRemoteFeedback = new BoolFeedback(() => _presentationLocalRemote); 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); 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 = new Action(() => { StandbyIsOnFeedback.FireUpdate(); HalfWakeModeIsOnFeedback.FireUpdate(); EnteringStandbyModeFeedback.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(); }; CodecStatus.Status.Conference.DoNotDisturb.ValueChangedAction = DoNotDisturbModeIsOnFeedback.FireUpdate; CodecConfiguration.Configuration.Audio.SoundsAndAlerts.RingVolume.ValueChangedAction = RingtoneVolumeFeedback.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); PhonebookSyncState.InitialSyncCompleted += new EventHandler(PhonebookSyncState_InitialSyncCompleted); return base.CustomActivate(); } void PhonebookSyncState_InitialSyncCompleted(object sender, EventArgs e) { OnDirectoryResultReturned(DirectoryRoot); } #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/Conference/DoNotDisturb" + 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/Bookings" + Delimiter + prefix + "/Event/CameraPresetListUpdated" + Delimiter + prefix + "/Event/UserInterface/Presentation/ExternalSource/Selected/SourceIdentifier" + Delimiter + prefix + "/Event/CallDisconnect" + Delimiter; // Keep CallDisconnect last to detect when feedback registration completes correctly } #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) { // Check for camera config info first if (_config.CameraInfo.Count > 0) { Debug.Console(0, this, "Reading codec cameraInfo from config properties."); SetUpCameras(_config.CameraInfo); } else { Debug.Console(0, this, "No cameraInfo defined in video codec config. Attempting to get camera info from codec status data"); try { var cameraInfo = new List(); foreach (var camera in CodecStatus.Status.Cameras.Camera) { var id = Convert.ToUInt16(camera.id); var info = new CameraInfo() { CameraNumber = id, Name = string.Format("{0} {1}", camera.Manufacturer, camera.Model), SourceId = camera.DetectedConnector.ConnectorId }; cameraInfo.Add(info); } Debug.Console(0, this, "Successfully got cameraInfo for {0} cameras from codec.", cameraInfo.Count); SetUpCameras(cameraInfo); } catch (Exception ex) { Debug.Console(2, this, "Error generating camera info from codec status data: {0}", ex); } } //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); // Fire the ready event SetIsReady(); } 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 != null && conference.Presentation.LocalInstance == null) { // Handles an empty presentation object response return; } if (conference.Presentation.LocalInstance.Count > 0) { if (!string.IsNullOrEmpty(conference.Presentation.LocalInstance[0].ghost)) { _presentationSource = 0; _presentationLocalOnly = false; _presentationLocalRemote = false; } else if (conference.Presentation.LocalInstance[0].Source != null) { _presentationSource = conference.Presentation.LocalInstance[0].Source.IntValue; // Check for any values in the SendingMode property if (conference.Presentation.LocalInstance.Any((i) => !string.IsNullOrEmpty(i.SendingMode.Value))) { _presentationLocalOnly = conference.Presentation.LocalInstance.Any((i) => i.SendingMode.LocalOnly); _presentationLocalRemote = conference.Presentation.LocalInstance.Any((i) => i.SendingMode.LocalRemote); } } PresentationSourceFeedback.FireUpdate(); PresentationSendingLocalOnlyFeedback.FireUpdate(); PresentationSendingLocalRemoteFeedback.FireUpdate(); } // 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); tempActiveCall.IsOnHold = tempActiveCall.Status == eCodecCallStatus.OnHold; 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(call.Duration != null) { if(!string.IsNullOrEmpty(call.Duration.Value)) { tempActiveCall.Duration = call.Duration.DurationValue; changeDetected = true; } } if(call.PlacedOnHold != null) { tempActiveCall.IsOnHold = call.PlacedOnHold.BoolValue; 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), Duration = call.Duration.DurationValue, IsOnHold = call.PlacedOnHold.BoolValue, }; // 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) { if (ex is Newtonsoft.Json.JsonReaderException) { Debug.Console(1, this, "Received malformed response from codec. Unable to serialize. Disconnecting and attmpting to recconnect"); Communication.Disconnect(); Initialize(); } 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) { Debug.Console(2, this, "Directory result returned"); 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"); } #region IHasCallHold Members public void HoldCall(CodecActiveCallItem activeCall) { SendText(string.Format("xCommand Call Hold CallId: {0}", activeCall.Id)); } public void ResumeCall(CodecActiveCallItem activeCall) { SendText(string.Format("xCommand Call Resume CallId: {0}", activeCall.Id)); } #endregion #region IJoinCalls public void JoinCall(CodecActiveCallItem activeCall) { SendText(string.Format("xCommand Call Join CallId: {0}", activeCall.Id)); } public void JoinAllCalls() { StringBuilder ids = new StringBuilder(); foreach (var call in ActiveCalls) { if (call.IsActiveCall) { ids.Append(string.Format(" CallId: {0}", call.Id)); } } if (ids.Length > 0) { SendText(string.Format("xCommand Call Join {0}", ids.ToString())); } } #endregion /// /// Sends tones to the last connected call /// /// public override void SendDtmf(string s) { SendText(string.Format("xCommand Call DTMFSend CallId: {0} DTMFString: \"{1}\"", GetCallId(), s)); } /// /// Sends tones to a specific call /// /// /// public override void SendDtmf(string s, CodecActiveCallItem activeCall) { SendText(string.Format("xCommand Call DTMFSend CallId: {0} DTMFString: \"{1}\"", activeCall.Id, s)); } public void SelectPresentationSource(int source) { _desiredPresentationSource = source; StartSharing(); } /// /// Sets the ringtone volume level /// /// level from 0 - 100 in increments of 5 public void SetRingtoneVolume(int volume) { if (volume < 0 || volume > 100) { Debug.Console(0, this, "Cannot set ringtone volume to '{0}'. Value must be between 0 - 100", volume); return; } if (volume % 5 != 0) { Debug.Console(0, this, "Cannot set ringtone volume to '{0}'. Value must be between 0 - 100 and a multiple of 5", volume); return; } SendText(string.Format("xConfiguration Audio SoundsAndAlerts RingVolume: {0}", volume)); } /// /// 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 (_desiredPresentationSource > 0) SendText(string.Format("xCommand Presentation Start PresentationSource: {0} SendingMode: {1}", _desiredPresentationSource, sendingMode)); } /// /// Stops sharing the current presentation /// public override void StopSharing() { _desiredPresentationSource = 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) { var joinMap = new CiscoCodecJoinMap(joinStart); var customJoins = JoinMapHelper.TryGetJoinMapAdvancedForDevice(joinMapKey); if (customJoins != null) { joinMap.SetCustomJoinData(customJoins); } if (bridge != null) { bridge.AddJoinMap(Key, joinMap); } LinkVideoCodecToApi(this, trilist, joinStart, joinMapKey, bridge); LinkCiscoCodecToApi(trilist, joinMap); } public void LinkCiscoCodecToApi(BasicTriList trilist, CiscoCodecJoinMap joinMap) { // Custom commands to codec trilist.SetStringSigAction(joinMap.CommandToDevice.JoinNumber, (s) => this.SendText(s)); var dndCodec = this as IHasDoNotDisturbMode; if (dndCodec != null) { dndCodec.DoNotDisturbModeIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.ActivateDoNotDisturbMode.JoinNumber]); dndCodec.DoNotDisturbModeIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.DeactivateDoNotDisturbMode.JoinNumber]); trilist.SetSigFalseAction(joinMap.ActivateDoNotDisturbMode.JoinNumber, () => dndCodec.ActivateDoNotDisturbMode()); trilist.SetSigFalseAction(joinMap.DeactivateDoNotDisturbMode.JoinNumber, () => dndCodec.DeactivateDoNotDisturbMode()); trilist.SetSigFalseAction(joinMap.ToggleDoNotDisturbMode.JoinNumber, () => dndCodec.ToggleDoNotDisturbMode()); } var halfwakeCodec = this as IHasHalfWakeMode; if (halfwakeCodec != null) { halfwakeCodec.StandbyIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.ActivateStandby.JoinNumber]); halfwakeCodec.StandbyIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.DeactivateStandby.JoinNumber]); halfwakeCodec.HalfWakeModeIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.ActivateHalfWakeMode.JoinNumber]); halfwakeCodec.EnteringStandbyModeFeedback.LinkInputSig(trilist.BooleanInput[joinMap.EnteringStandbyMode.JoinNumber]); trilist.SetSigFalseAction(joinMap.ActivateStandby.JoinNumber, () => halfwakeCodec.StandbyActivate()); trilist.SetSigFalseAction(joinMap.DeactivateStandby.JoinNumber, () => halfwakeCodec.StandbyDeactivate()); trilist.SetSigFalseAction(joinMap.ActivateHalfWakeMode.JoinNumber, () => halfwakeCodec.HalfwakeActivate()); } // Ringtone volume trilist.SetUShortSigAction(joinMap.RingtoneVolume.JoinNumber, (u) => SetRingtoneVolume(u)); RingtoneVolumeFeedback.LinkInputSig(trilist.UShortInput[joinMap.RingtoneVolume.JoinNumber]); // Presentation Source trilist.SetUShortSigAction(joinMap.PresentationSource.JoinNumber, (u) => SelectPresentationSource(u)); PresentationSourceFeedback.LinkInputSig(trilist.UShortInput[joinMap.PresentationSource.JoinNumber]); PresentationSendingLocalOnlyFeedback.LinkInputSig(trilist.BooleanInput[joinMap.PresentationLocalOnly.JoinNumber]); PresentationSendingLocalRemoteFeedback.LinkInputSig(trilist.BooleanInput[joinMap.PresentationLocalRemote.JoinNumber]); } /// /// 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(List cameraInfo) { // Add the internal camera Cameras = new List(); var camCount = CodecStatus.Status.Cameras.Camera.Count; Debug.Console(0, this, "Codec reports {0} cameras", camCount); // Deal with the case of 1 or no reported cameras if (camCount <= 1) { var internalCamera = new CiscoSparkCamera(Key + "-camera1", "Near End", this, 1); if (camCount > 0) { // Try to get the capabilities from the codec if (CodecStatus.Status.Cameras.Camera[0] != null && CodecStatus.Status.Cameras.Camera[0].Capabilities != null) { internalCamera.SetCapabilites(CodecStatus.Status.Cameras.Camera[0].Capabilities.Options.Value); } } Cameras.Add(internalCamera); DeviceManager.AddDevice(internalCamera); } else { // Setup all the cameras for (int i = 0; i < camCount; i++) { var cam = CodecStatus.Status.Cameras.Camera[i]; var id = (uint)i; var name = string.Format("Camera {0}", id); // Check for a config object that matches the camera number var camInfo = cameraInfo.FirstOrDefault(c => c.CameraNumber == i + 1); if (camInfo != null) { id = (uint)camInfo.SourceId; name = camInfo.Name; } var key = string.Format("{0}-camera{1}", Key, id); var camera = new CiscoSparkCamera(key, name, this, id); if (cam.Capabilities != null) { camera.SetCapabilites(cam.Capabilities.Options.Value); } Cameras.Add(camera); } } // 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); 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 = Cameras[0]; ; // 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); var ciscoCam = camera as CiscoSparkCamera; if (ciscoCam != null) { SendText(string.Format("xCommand Video Input SetMainVideoSource SourceId: {0}", ciscoCam.CameraId)); } } 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 == null) return false; return CodecConfiguration.Configuration.Conference.AutoAnswer.Mode.Value.ToLower() == "on"; } } 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(); } #region IHasDoNotDisturbMode Members public BoolFeedback DoNotDisturbModeIsOnFeedback { get; private set; } public void ActivateDoNotDisturbMode() { SendText("xCommand Conference DoNotDisturb Activate"); } public void DeactivateDoNotDisturbMode() { SendText("xCommand Conference DoNotDisturb Deactivate"); } public void ToggleDoNotDisturbMode() { if (DoNotDisturbModeIsOnFeedback.BoolValue) { DeactivateDoNotDisturbMode(); } else { ActivateDoNotDisturbMode(); } } #endregion #region IHasHalfWakeMode Members public BoolFeedback HalfWakeModeIsOnFeedback { get; private set; } public BoolFeedback EnteringStandbyModeFeedback { get; private set; } public void HalfwakeActivate() { SendText("xCommand Standby Halfwake"); } #endregion } /// /// 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); } } }